diff options
| -rw-r--r-- | CMakeLists.txt | 18 | ||||
| -rw-r--r-- | HOWTO.md | 30 | ||||
| -rw-r--r-- | src/audio/synth.cpp | 10 | ||||
| -rw-r--r-- | src/audio/synth.h | 1 | ||||
| -rw-r--r-- | tools/spectool.cpp | 161 |
5 files changed, 220 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index c9b8cad..7828ada 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,3 +59,21 @@ if(DEMO_BUILD_TESTS) ) add_test(NAME SynthEngineTest COMMAND test_synth) endif() + +option(DEMO_BUILD_TOOLS "Build tools" OFF) +if(DEMO_BUILD_TOOLS) + add_executable(spectool + tools/spectool.cpp + src/platform.cpp + src/audio/audio.cpp + src/audio/fdct.cpp + src/audio/idct.cpp + src/audio/window.cpp + src/audio/synth.cpp + ) + target_include_directories(spectool PRIVATE + src + third_party + ) + target_link_libraries(spectool PRIVATE glfw) +endif() @@ -38,3 +38,33 @@ cd build ctest cd .. ``` + +## Tools + +### Spectrogram Tool (`spectool`) + +A command-line tool for analyzing WAV and MP3 files into spectrograms and playing them back. + +#### Building the Tool + +To build `spectool`, you need to enable the `DEMO_BUILD_TOOLS` option in CMake. + +```bash +cmake -S . -B build -DDEMO_BUILD_TOOLS=ON +cmake --build build +``` +The executable will be located at `build/spectool`. + +#### Usage + +**Analyze an audio file:** +```bash +./build/spectool analyze path/to/input.wav path/to/output.spec +# or +./build/spectool analyze path/to/input.mp3 path/to/output.spec +``` + +**Play a spectrogram file:** +```bash +./build/spectool play path/to/input.spec +``` diff --git a/src/audio/synth.cpp b/src/audio/synth.cpp index f009876..3c20b0b 100644 --- a/src/audio/synth.cpp +++ b/src/audio/synth.cpp @@ -155,3 +155,13 @@ void synth_render(float* output_buffer, int num_frames) { output_buffer[i * 2 + 1] = right_sample; } } + +int synth_get_active_voice_count() { + int count = 0; + for (int i = 0; i < MAX_VOICES; ++i) { + if (g_voices[i].active) { + count++; + } + } + return count; +} diff --git a/src/audio/synth.h b/src/audio/synth.h index ce9825d..17770a7 100644 --- a/src/audio/synth.h +++ b/src/audio/synth.h @@ -22,3 +22,4 @@ void synth_commit_update(int spectrogram_id); void synth_trigger_voice(int spectrogram_id, float volume, float pan); void synth_render(float* output_buffer, int num_frames); +int synth_get_active_voice_count(); diff --git a/tools/spectool.cpp b/tools/spectool.cpp new file mode 100644 index 0000000..3b3d3aa --- /dev/null +++ b/tools/spectool.cpp @@ -0,0 +1,161 @@ +#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" + +// Define MINIAUDIO_IMPLEMENTATION in one C/C++ file. +#define MINIAUDIO_IMPLEMENTATION +#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; +} |
