diff options
| -rw-r--r-- | CMakeLists.txt | 5 | ||||
| -rw-r--r-- | src/audio/gen.cc | 138 | ||||
| -rw-r--r-- | src/audio/gen.h | 38 | ||||
| -rw-r--r-- | src/main.cc | 65 | ||||
| -rw-r--r-- | tools/spectool.cc | 80 |
5 files changed, 320 insertions, 6 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 349c9e6..7f42c47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,7 @@ add_executable(demo64k src/platform.cc src/gpu/gpu.cc src/audio/audio.cc + src/audio/gen.cc src/audio/fdct.cc src/audio/idct.cc src/audio/window.cc @@ -161,6 +162,7 @@ if(DEMO_BUILD_TESTS) src/audio/audio.cc src/audio/window.cc src/audio/fdct.cc + src/audio/gen.cc src/audio/synth.cc src/audio/idct.cc third_party/glfw3webgpu/glfw3webgpu.c @@ -188,11 +190,12 @@ endif() # Tools option(DEMO_BUILD_TOOLS "Build tools" OFF) -if(DEMO_BUILD_TOOLS) +if(DEMO_BUILD_TOOLS OR DEMO_BUILD_TESTS) add_executable(spectool tools/spectool.cc src/platform.cc src/audio/audio.cc + src/audio/gen.cc src/audio/fdct.cc src/audio/idct.cc src/audio/window.cc diff --git a/src/audio/gen.cc b/src/audio/gen.cc new file mode 100644 index 0000000..3666116 --- /dev/null +++ b/src/audio/gen.cc @@ -0,0 +1,138 @@ +// This file is part of the 64k demo project. +// It implements the procedural audio generation logic. +// Uses IDCT/FDCT to synthesize notes in the frequency domain. + +#include "audio/gen.h" +#include "audio/dct.h" +#include "audio/window.h" +#include <math.h> +#include <stdlib.h> +#include <string.h> + +std::vector<float> generate_note_spectrogram(const NoteParams ¶ms, + int *out_num_frames) { + int num_frames = (int)(params.duration_sec * 32000.0f / DCT_SIZE); + if (num_frames < 1) + num_frames = 1; + *out_num_frames = num_frames; + + std::vector<float> spec_data(num_frames * DCT_SIZE, 0.0f); + float window[WINDOW_SIZE]; + hamming_window_512(window); + + float phase = 0.0f; + float time_step = 1.0f / 32000.0f; + + for (int f = 0; f < num_frames; ++f) { + float pcm_chunk[DCT_SIZE] = {0}; + float frame_time = f * DCT_SIZE * time_step; + + // Generate PCM for this frame + for (int i = 0; i < DCT_SIZE; ++i) { + float t = frame_time + i * time_step; + + // Envelope (Simple Attack) + float env = 1.0f; + if (t < params.attack_sec) { + env = t / params.attack_sec; + } + + // Vibrato + float vib = + sinf(t * params.vibrato_rate * 2.0f * 3.14159f) * params.vibrato_depth; + + // Randomness + float pitch_rnd = + ((float)rand() / RAND_MAX - 0.5f) * params.pitch_randomness; + float amp_rnd = ((float)rand() / RAND_MAX - 0.5f) * params.amp_randomness; + + float sample = 0.0f; + for (int h = 1; h <= params.num_harmonics; ++h) { + float h_amp = powf(params.harmonic_decay, h - 1); + float freq = (params.base_freq + vib + pitch_rnd) * h; + sample += sinf(phase * h) * h_amp; + } + + // Update phase for fundamental (approximate, since freq changes) + phase += + (params.base_freq + vib + pitch_rnd) * 2.0f * 3.14159f * time_step; + + pcm_chunk[i] = sample * params.amplitude * env * (1.0f + amp_rnd); + } + + // 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); + + // Copy to buffer + for (int i = 0; i < DCT_SIZE; ++i) { + spec_data[f * DCT_SIZE + i] = dct_chunk[i]; + } + } + + return spec_data; +} + +void paste_spectrogram(std::vector<float> &dest_data, int *dest_num_frames, + const std::vector<float> &src_data, int src_num_frames, + int frame_offset) { + if (src_num_frames <= 0) + return; + + int needed_frames = frame_offset + src_num_frames; + if (needed_frames > *dest_num_frames) { + dest_data.resize(needed_frames * DCT_SIZE, 0.0f); + *dest_num_frames = needed_frames; + } + + for (int f = 0; f < src_num_frames; ++f) { + int dst_frame_idx = frame_offset + f; + if (dst_frame_idx < 0) + continue; + + for (int i = 0; i < DCT_SIZE; ++i) { + dest_data[dst_frame_idx * DCT_SIZE + i] += src_data[f * DCT_SIZE + i]; + } + } +} + +void apply_spectral_noise(std::vector<float> &data, int num_frames, + float amount) { + for (int f = 0; f < num_frames; ++f) { + for (int i = 0; i < DCT_SIZE; ++i) { + float rnd = ((float)rand() / RAND_MAX - 0.5f) * 2.0f * amount; + data[f * DCT_SIZE + i] *= (1.0f + rnd); + } + } +} + +void apply_spectral_lowpass(std::vector<float> &data, int num_frames, + float cutoff_ratio) { + int cutoff_bin = (int)(cutoff_ratio * DCT_SIZE); + if (cutoff_bin < 0) + cutoff_bin = 0; + if (cutoff_bin >= DCT_SIZE) + return; + + for (int f = 0; f < num_frames; ++f) { + for (int i = cutoff_bin; i < DCT_SIZE; ++i) { + data[f * DCT_SIZE + i] = 0.0f; + } + } +} + +void apply_spectral_comb(std::vector<float> &data, int num_frames, + float period_bins, float depth) { + for (int i = 0; i < DCT_SIZE; ++i) { + float mod = + 1.0f - depth * (0.5f + 0.5f * cosf((float)i / period_bins * 6.28318f)); + for (int f = 0; f < num_frames; ++f) { + data[f * DCT_SIZE + i] *= mod; + } + } +}
\ No newline at end of file diff --git a/src/audio/gen.h b/src/audio/gen.h new file mode 100644 index 0000000..f490115 --- /dev/null +++ b/src/audio/gen.h @@ -0,0 +1,38 @@ +// This file is part of the 64k demo project. +// It defines the procedural audio generation interface. +// Shared between the offline spectool and the runtime demo. + +#pragma once +#include <vector> + +struct NoteParams { + float base_freq; + float duration_sec; + float amplitude; + float attack_sec; + float decay_sec; // Unused for now + float vibrato_rate; + float vibrato_depth; + int num_harmonics; + float harmonic_decay; + float pitch_randomness; + float amp_randomness; +}; + +// Generates a single note into a new spectrogram buffer +std::vector<float> generate_note_spectrogram(const NoteParams ¶ms, + int *out_num_frames); + +// Pastes a source spectrogram into a destination spectrogram at a given frame +// offset Expands destination if necessary +void paste_spectrogram(std::vector<float> &dest_data, int *dest_num_frames, + const std::vector<float> &src_data, int src_num_frames, + int frame_offset); + +// Post-processing effects +void apply_spectral_noise(std::vector<float> &data, int num_frames, + float amount); +void apply_spectral_lowpass(std::vector<float> &data, int num_frames, + float cutoff_ratio); +void apply_spectral_comb(std::vector<float> &data, int num_frames, + float period_bins, float depth); diff --git a/src/main.cc b/src/main.cc index 5da76ba..0bc28d6 100644 --- a/src/main.cc +++ b/src/main.cc @@ -4,6 +4,7 @@ #include "assets.h" // Include generated asset header #include "audio/audio.h" +#include "audio/gen.h" #include "audio/synth.h" #include "gpu/gpu.h" #include "platform.h" @@ -12,6 +13,7 @@ #include <math.h> #include <stdio.h> #include <string.h> +#include <vector> #define DEMO_BPM 128.0f #define SECONDS_PER_BEAT (60.0f / DEMO_BPM) @@ -44,6 +46,65 @@ int register_spec_asset(AssetId id) { static float g_spec_buffer_a[SPEC_FRAMES * DCT_SIZE]; static float g_spec_buffer_b[SPEC_FRAMES * DCT_SIZE]; +// Global storage for the melody to ensure it persists +std::vector<float> g_melody_data; + +int generate_melody() { + g_melody_data.clear(); + int melody_frames = 0; + + // Simple C Minor pentatonic-ish sequence + float notes[] = {261.63f, 311.13f, 349.23f, 392.00f, 466.16f, 523.25f}; + int num_notes = 6; + + // 128 beats at 128 BPM = 60 seconds + // Generate a random sequence + srand(12345); // Fixed seed for reproducibility + + for (int i = 0; i < 128; ++i) { + if (i % 4 == 0) continue; // Rest on beat 1 of every bar + + NoteParams params = {}; + params.base_freq = notes[rand() % num_notes]; + if (rand() % 4 == 0) params.base_freq *= 2.0f; // Occasional octave up + + params.duration_sec = (rand() % 2 == 0) ? 0.2f : 0.4f; + params.amplitude = 0.4f; + params.attack_sec = 0.05f; + params.decay_sec = 0.1f; + params.vibrato_rate = 6.0f; + params.vibrato_depth = 1.5f; + params.num_harmonics = 4; + params.harmonic_decay = 0.6f; + params.pitch_randomness = 0.5f; + params.amp_randomness = 0.05f; + + int note_frames = 0; + std::vector<float> note_data = generate_note_spectrogram(params, ¬e_frames); + + // Apply some post-processing for texture + apply_spectral_noise(note_data, note_frames, 0.2f); // Add grit + if (i % 2 == 0) { + apply_spectral_comb(note_data, note_frames, 10.0f, 0.8f); // Phaser-like effect + } + + // Calculate offset in frames + // i is the beat index (quarter notes) + // 1 beat = 60 / 128 seconds = 0.46875 sec + float beat_time = i * SECONDS_PER_BEAT; + int frame_offset = (int)(beat_time * 32000.0f / DCT_SIZE); + + paste_spectrogram(g_melody_data, &melody_frames, note_data, note_frames, frame_offset); + } + + Spectrogram spec; + spec.spectral_data_a = g_melody_data.data(); + spec.spectral_data_b = g_melody_data.data(); + spec.num_frames = melody_frames; + + return synth_register_spectrogram(&spec); +} + void generate_tone(float *buffer, float freq) { memset(buffer, 0, SPEC_FRAMES * DCT_SIZE * sizeof(float)); for (int frame = 0; frame < SPEC_FRAMES; ++frame) { @@ -88,6 +149,10 @@ int main(int argc, char **argv) { const Spectrogram bass_spec = {g_spec_buffer_a, g_spec_buffer_b, SPEC_FRAMES}; int bass_id = synth_register_spectrogram(&bass_spec); + // Generate and play melody + int melody_id = generate_melody(); + synth_trigger_voice(melody_id, 0.6f, 0.0f); + double last_beat_time = 0.0; int beat_count = 0; diff --git a/tools/spectool.cc b/tools/spectool.cc index 29a2952..7733b98 100644 --- a/tools/spectool.cc +++ b/tools/spectool.cc @@ -4,6 +4,7 @@ #include "audio/audio.h" #include "audio/dct.h" +#include "audio/gen.h" #include "audio/synth.h" #include "audio/window.h" #include "platform.h" @@ -12,6 +13,9 @@ #include "miniaudio.h" +#include <math.h> +#include <stdlib.h> +#include <time.h> #include <vector> // Simple .spec file format: @@ -149,6 +153,60 @@ int play_spec(const char *in_path) { return 0; } +int test_gen(const char *out_path) { + printf("Generating test spectrogram -> %s\n", out_path); + + std::vector<float> track_data; + int track_frames = 0; + + // Generate a simple C Major scale + float freqs[] = {261.63f, 293.66f, 329.63f, 349.23f, 392.00f, 440.00f, 493.88f, 523.25f}; + + srand(time(NULL)); + + for (int i = 0; i < 8; ++i) { + NoteParams params; + params.base_freq = freqs[i]; + params.duration_sec = 0.5f; + params.amplitude = 0.5f; + params.attack_sec = 0.05f; + params.decay_sec = 0.1f; + params.vibrato_rate = 5.0f; + params.vibrato_depth = 2.0f; + params.num_harmonics = 5; + params.harmonic_decay = 0.5f; + params.pitch_randomness = 1.0f; + params.amp_randomness = 0.05f; + + int note_frames = 0; + std::vector<float> note_data = generate_note_spectrogram(params, ¬e_frames); + + // Paste at 0.4s intervals (overlap) + int offset = (int)(i * 0.4f * 32000.0f / DCT_SIZE); + paste_spectrogram(track_data, &track_frames, note_data, note_frames, offset); + } + + // Write to file (Duplicate logic, but fine for now) + 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 = track_frames; + + fwrite(&header, sizeof(SpecHeader), 1, f_out); + fwrite(track_data.data(), sizeof(float), track_data.size(), f_out); + fclose(f_out); + + printf("Generated %d frames.\n", track_frames); + return 0; +} + void print_usage() { printf("Usage: spectool <command> <input> [output]\n"); printf("Commands:\n"); @@ -156,26 +214,38 @@ void print_usage() { "save as a spectrogram.\n"); printf( " play <input.spec> Play a spectrogram file.\n"); + printf(" test_gen <output.spec> Generate a test spectrogram.\n"); } int main(int argc, char **argv) { - if (argc < 3) { + if (argc < 2) { 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"); + printf("Error: 'analyze' command requires input and output files.\n"); print_usage(); return 1; } - return analyze_audio(input_path, argv[3]); + return analyze_audio(argv[2], argv[3]); } else if (strcmp(command, "play") == 0) { - return play_spec(input_path); + if (argc < 3) { + printf("Error: 'play' command requires an input file.\n"); + print_usage(); + return 1; + } + return play_spec(argv[2]); + } else if (strcmp(command, "test_gen") == 0) { + if (argc < 3) { + printf("Error: 'test_gen' command requires an output file.\n"); + print_usage(); + return 1; + } + return test_gen(argv[2]); } else { printf("Error: Unknown command '%s'\n", command); print_usage(); |
