From 9dcf94ab01269311b4e5d39be23c95560904c626 Mon Sep 17 00:00:00 2001 From: skal Date: Tue, 27 Jan 2026 23:09:27 +0100 Subject: 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. --- tools/spectool.cc | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 tools/spectool.cc (limited to 'tools/spectool.cc') 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 +#include + +#include "miniaudio.h" + +#include + +// 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 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 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 [output]\n"); + printf("Commands:\n"); + printf(" analyze Analyze an audio file and " + "save as a spectrogram.\n"); + printf( + " play 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; +} -- cgit v1.2.3