diff options
| author | skal <pascal.massimino@gmail.com> | 2026-01-27 23:09:27 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-01-27 23:10:49 +0100 |
| commit | 9dcf94ab01269311b4e5d39be23c95560904c626 (patch) | |
| tree | ac271af3d51d7ee1bce6827f81e97f1f463336db /tools | |
| parent | 364d9e60e3c27cb131a598fe5f83deb74493319f (diff) | |
feat: Implement spectool & specview; refactor coding style; update docs
This commit introduces new tools for spectrogram manipulation and visualization, establishes a consistent coding style, and updates project documentation.
Key changes include:
- **Spectrogram Tools:
- : A command-line utility for analyzing WAV/MP3 files into custom spectrogram format and playing back these spectrograms via the synth engine.
- : A command-line tool for visualizing spectrogram files as ASCII art in the console.
- **Coding Style Enforcement:
- Added a configuration file enforcing LLVM-based style with 2-space indentation, no tabs, and an 80-column line limit.
- Renamed all C++ source files from to for project consistency.
- Applied automatic formatting using
exit across the entire codebase.
- **Documentation & Workflow:
- Created to define a commit policy requiring tests to pass before committing.
- Updated with instructions for building and using and , and referenced .
- Updated and to reflect the new tools, audio architecture decisions (real-time additive synthesis, double-buffering for dynamic updates, WAV/MP3 support), coding style, and development workflow.
- **Build System:
- Modified to:
- Include new targets for and under the option.
- Update source file extensions to .
- Add a new end-to-end test for to the suite.
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/spectool.cc | 165 | ||||
| -rw-r--r-- | tools/spectool.cpp | 159 | ||||
| -rw-r--r-- | tools/specview.cc | 94 |
3 files changed, 259 insertions, 159 deletions
diff --git a/tools/spectool.cc b/tools/spectool.cc new file mode 100644 index 0000000..49c6384 --- /dev/null +++ b/tools/spectool.cc @@ -0,0 +1,165 @@ +#include "audio/audio.h" +#include "audio/dct.h" +#include "audio/synth.h" +#include "audio/window.h" +#include "platform.h" +#include <stdio.h> +#include <string.h> + +#include "miniaudio.h" + +#include <vector> + +// Simple .spec file format: +// char[4] magic = "SPEC" +// int32_t version = 1 +// int32_t dct_size +// int32_t num_frames +// float[num_frames * dct_size] data +struct SpecHeader { + char magic[4]; + int32_t version; + int32_t dct_size; + int32_t num_frames; +}; + +int analyze_audio(const char *in_path, const char *out_path) { + printf("Analyzing %s -> %s\n", in_path, out_path); + + ma_decoder_config config = ma_decoder_config_init(ma_format_f32, 1, 32000); + ma_decoder decoder; + if (ma_decoder_init_file(in_path, &config, &decoder) != MA_SUCCESS) { + printf("Error: Failed to open or decode audio file: %s\n", in_path); + return 1; + } + + std::vector<float> spec_data; + float pcm_chunk[DCT_SIZE]; + float window[WINDOW_SIZE]; + hamming_window_512(window); + + ma_uint64 frames_read; + while (ma_decoder_read_pcm_frames(&decoder, pcm_chunk, DCT_SIZE, + &frames_read) == MA_SUCCESS && + frames_read > 0) { + if (frames_read < DCT_SIZE) { + // Zero-pad the last chunk if it's smaller + memset(pcm_chunk + frames_read, 0, + (DCT_SIZE - frames_read) * sizeof(float)); + } + + // Apply window + for (int i = 0; i < DCT_SIZE; ++i) { + pcm_chunk[i] *= window[i]; + } + + // Apply FDCT + float dct_chunk[DCT_SIZE]; + fdct_512(pcm_chunk, dct_chunk); + + // Add to spectrogram data + spec_data.insert(spec_data.end(), dct_chunk, dct_chunk + DCT_SIZE); + } + + ma_decoder_uninit(&decoder); + + // Write to .spec file + FILE *f_out = fopen(out_path, "wb"); + if (!f_out) { + printf("Error: Failed to open output file: %s\n", out_path); + return 1; + } + + SpecHeader header; + memcpy(header.magic, "SPEC", 4); + header.version = 1; + header.dct_size = DCT_SIZE; + header.num_frames = spec_data.size() / DCT_SIZE; + + fwrite(&header, sizeof(SpecHeader), 1, f_out); + fwrite(spec_data.data(), sizeof(float), spec_data.size(), f_out); + fclose(f_out); + + printf("Analysis complete. Wrote %d spectral frames.\n", header.num_frames); + return 0; +} + +int play_spec(const char *in_path) { + printf("Playing %s\n", in_path); + + FILE *f_in = fopen(in_path, "rb"); + if (!f_in) { + printf("Error: Failed to open input file: %s\n", in_path); + return 1; + } + + SpecHeader header; + if (fread(&header, sizeof(SpecHeader), 1, f_in) != 1 || + strncmp(header.magic, "SPEC", 4) != 0) { + printf("Error: Invalid spectrogram file format.\n"); + fclose(f_in); + return 1; + } + + std::vector<float> spec_data(header.num_frames * header.dct_size); + fread(spec_data.data(), sizeof(float), spec_data.size(), f_in); + fclose(f_in); + + platform_init(); + audio_init(); + + Spectrogram spec; + spec.spectral_data_a = spec_data.data(); + spec.spectral_data_b = + spec_data.data(); // Point both to the same buffer for playback + spec.num_frames = header.num_frames; + + int spec_id = synth_register_spectrogram(&spec); + synth_trigger_voice(spec_id, 0.7f, 0.0f); + + printf("Playing... Press Ctrl+C to exit.\n"); + while (synth_get_active_voice_count() > 0 && !platform_should_close()) { + platform_poll(); + } + + audio_shutdown(); + platform_shutdown(); + + return 0; +} + +void print_usage() { + printf("Usage: spectool <command> <input> [output]\n"); + printf("Commands:\n"); + printf(" analyze <input.wav|.mp3> <output.spec> Analyze an audio file and " + "save as a spectrogram.\n"); + printf( + " play <input.spec> Play a spectrogram file.\n"); +} + +int main(int argc, char **argv) { + if (argc < 3) { + print_usage(); + return 1; + } + + const char *command = argv[1]; + const char *input_path = argv[2]; + + if (strcmp(command, "analyze") == 0) { + if (argc < 4) { + printf("Error: 'analyze' command requires an output file.\n"); + print_usage(); + return 1; + } + return analyze_audio(input_path, argv[3]); + } else if (strcmp(command, "play") == 0) { + return play_spec(input_path); + } else { + printf("Error: Unknown command '%s'\n", command); + print_usage(); + return 1; + } + + return 0; +} diff --git a/tools/spectool.cpp b/tools/spectool.cpp deleted file mode 100644 index defb118..0000000 --- a/tools/spectool.cpp +++ /dev/null @@ -1,159 +0,0 @@ -#include <stdio.h> -#include <string.h> -#include "audio/dct.h" -#include "audio/window.h" -#include "audio/synth.h" -#include "platform.h" -#include "audio/audio.h" - -#include "miniaudio.h" - -#include <vector> - -// Simple .spec file format: -// char[4] magic = "SPEC" -// int32_t version = 1 -// int32_t dct_size -// int32_t num_frames -// float[num_frames * dct_size] data -struct SpecHeader { - char magic[4]; - int32_t version; - int32_t dct_size; - int32_t num_frames; -}; - -int analyze_audio(const char* in_path, const char* out_path) { - printf("Analyzing %s -> %s\n", in_path, out_path); - - ma_decoder_config config = ma_decoder_config_init(ma_format_f32, 1, 32000); - ma_decoder decoder; - if (ma_decoder_init_file(in_path, &config, &decoder) != MA_SUCCESS) { - printf("Error: Failed to open or decode audio file: %s\n", in_path); - return 1; - } - - std::vector<float> spec_data; - float pcm_chunk[DCT_SIZE]; - float window[WINDOW_SIZE]; - hamming_window_512(window); - - ma_uint64 frames_read; - while (ma_decoder_read_pcm_frames(&decoder, pcm_chunk, DCT_SIZE, &frames_read) == MA_SUCCESS && frames_read > 0) { - if (frames_read < DCT_SIZE) { - // Zero-pad the last chunk if it's smaller - memset(pcm_chunk + frames_read, 0, (DCT_SIZE - frames_read) * sizeof(float)); - } - - // Apply window - for (int i = 0; i < DCT_SIZE; ++i) { - pcm_chunk[i] *= window[i]; - } - - // Apply FDCT - float dct_chunk[DCT_SIZE]; - fdct_512(pcm_chunk, dct_chunk); - - // Add to spectrogram data - spec_data.insert(spec_data.end(), dct_chunk, dct_chunk + DCT_SIZE); - } - - ma_decoder_uninit(&decoder); - - // Write to .spec file - FILE* f_out = fopen(out_path, "wb"); - if (!f_out) { - printf("Error: Failed to open output file: %s\n", out_path); - return 1; - } - - SpecHeader header; - memcpy(header.magic, "SPEC", 4); - header.version = 1; - header.dct_size = DCT_SIZE; - header.num_frames = spec_data.size() / DCT_SIZE; - - fwrite(&header, sizeof(SpecHeader), 1, f_out); - fwrite(spec_data.data(), sizeof(float), spec_data.size(), f_out); - fclose(f_out); - - printf("Analysis complete. Wrote %d spectral frames.\n", header.num_frames); - return 0; -} - -int play_spec(const char* in_path) { - printf("Playing %s\n", in_path); - - FILE* f_in = fopen(in_path, "rb"); - if (!f_in) { - printf("Error: Failed to open input file: %s\n", in_path); - return 1; - } - - SpecHeader header; - if (fread(&header, sizeof(SpecHeader), 1, f_in) != 1 || strncmp(header.magic, "SPEC", 4) != 0) { - printf("Error: Invalid spectrogram file format.\n"); - fclose(f_in); - return 1; - } - - std::vector<float> spec_data(header.num_frames * header.dct_size); - fread(spec_data.data(), sizeof(float), spec_data.size(), f_in); - fclose(f_in); - - platform_init(); - audio_init(); - - Spectrogram spec; - spec.spectral_data_a = spec_data.data(); - spec.spectral_data_b = spec_data.data(); // Point both to the same buffer for playback - spec.num_frames = header.num_frames; - - int spec_id = synth_register_spectrogram(&spec); - synth_trigger_voice(spec_id, 0.7f, 0.0f); - - printf("Playing... Press Ctrl+C to exit.\n"); - while (synth_get_active_voice_count() > 0 && !platform_should_close()) { - platform_poll(); - } - - - audio_shutdown(); - platform_shutdown(); - - return 0; -} - -void print_usage() { - printf("Usage: spectool <command> <input> [output]\n"); - printf("Commands:\n"); - printf(" analyze <input.wav|.mp3> <output.spec> Analyze an audio file and save as a spectrogram.\n"); - printf(" play <input.spec> Play a spectrogram file.\n"); -} - -int main(int argc, char** argv) { - if (argc < 3) { - print_usage(); - return 1; - } - - const char* command = argv[1]; - const char* input_path = argv[2]; - - if (strcmp(command, "analyze") == 0) { - if (argc < 4) { - printf("Error: 'analyze' command requires an output file.\n"); - print_usage(); - return 1; - } - return analyze_audio(input_path, argv[3]); - } else if (strcmp(command, "play") == 0) { - return play_spec(input_path); - } else { - printf("Error: Unknown command '%s'\n", command); - print_usage(); - return 1; - } - - return 0; -} diff --git a/tools/specview.cc b/tools/specview.cc new file mode 100644 index 0000000..77b362c --- /dev/null +++ b/tools/specview.cc @@ -0,0 +1,94 @@ +#include <stdio.h> +#include <string.h> +#include <vector> +#include <algorithm> // For std::max_element + +// Redefine SpecHeader from spectool.cc +struct SpecHeader { + char magic[4]; + int32_t version; + int32_t dct_size; + int32_t num_frames; +}; + +void print_usage() { + printf("Usage: specview <input.spec>\n"); + printf("Displays an ASCII representation of a spectrogram file.\n"); +} + +int main(int argc, char** argv) { + if (argc < 2) { + print_usage(); + return 1; + } + + const char* input_path = argv[1]; + + FILE* f_in = fopen(input_path, "rb"); + if (!f_in) { + printf("Error: Failed to open input file: %s\n", input_path); + return 1; + } + + SpecHeader header; + if (fread(&header, sizeof(SpecHeader), 1, f_in) != 1 || strncmp(header.magic, "SPEC", 4) != 0) { + printf("Error: Invalid spectrogram file format.\n"); + fclose(f_in); + return 1; + } + + if (header.version != 1) { + printf("Error: Unsupported spectrogram version %d.\n", header.version); + fclose(f_in); + return 1; + } + + std::vector<float> spec_data(header.num_frames * header.dct_size); + if (fread(spec_data.data(), sizeof(float), spec_data.size(), f_in) != spec_data.size()) { + printf("Error: Failed to read all spectrogram data.\n"); + fclose(f_in); + return 1; + } + fclose(f_in); + + printf("Spectrogram: %s\n", input_path); + printf(" DCT Size: %d\n", header.dct_size); + printf(" Num Frames: %d\n", header.num_frames); + + // Find max magnitude for normalization + float max_mag = 0.0f; + for (float val : spec_data) { + max_mag = std::max(max_mag, fabsf(val)); + } + if (max_mag == 0.0f) max_mag = 1.0f; // Avoid division by zero + + // ASCII visualization + const char* gradient = " .:-=+*#%@"; + int gradient_len = strlen(gradient); + + printf("\nASCII Visualization:\n"); + for (int frame = 0; frame < header.num_frames; ++frame) { + printf("%4d: ", frame); + const float* current_frame_data = spec_data.data() + frame * header.dct_size; + + // Average bins into fewer columns for better fit on console + const int display_cols = 80; // Max console width + const int bins_per_col = header.dct_size / display_cols; // Divide into 80 bins + + for (int col = 0; col < display_cols; ++col) { + float sum_mag = 0.0f; + for (int bin_idx = 0; bin_idx < bins_per_col; ++bin_idx) { + int current_bin = col * bins_per_col + bin_idx; + if (current_bin < header.dct_size) { + sum_mag += fabsf(current_frame_data[current_bin]); + } + } + float avg_mag = sum_mag / bins_per_col; + int char_idx = (int)((avg_mag / max_mag) * (gradient_len - 1)); + printf("%c", gradient[char_idx]); + } + printf("\n"); + } + + return 0; +} |
