#include #include #include #include #include #include #include #include // Enum to differentiate between sample types enum SampleType { GENERATED, ASSET }; // Convert note name (e.g., "NOTE_C4", "NOTE_A#3", "NOTE_Eb2") to frequency in Hz // CRITICAL: Now requires "NOTE_" prefix (changed to prevent ASSET_* confusion) static float note_name_to_freq(const std::string& note_name) { if (note_name.size() < 7) // "NOTE_" + note + octave minimum return 0.0f; // Skip "NOTE_" prefix (5 characters) to get to the actual note const char* str = note_name.c_str() + 5; char note_char = str[0]; int semitone = 0; // Map note name to semitone (C=0, D=2, E=4, F=5, G=7, A=9, B=11) switch (note_char) { case 'C': semitone = 0; break; case 'D': semitone = 2; break; case 'E': semitone = 4; break; case 'F': semitone = 5; break; case 'G': semitone = 7; break; case 'A': semitone = 9; break; case 'B': semitone = 11; break; default: return 0.0f; // Invalid note } int idx = 1; // Check for sharp (#) or flat (b) if (str[idx] == '#') { semitone++; idx++; } else if (str[idx] == 'b') { semitone--; idx++; } // Parse octave int octave = atoi(&str[idx]); // A4 = 440 Hz is our reference (A4 = octave 4, semitone 9) // Formula: freq = 440 * 2^((semitone - 9 + 12*(octave - 4)) / 12) const int midi_note = semitone + 12 * (octave + 1); const int a4_midi = 69; // A4 = MIDI note 69 const float freq = 440.0f * powf(2.0f, (midi_note - a4_midi) / 12.0f); return freq; } static bool is_note_name(const std::string& name) { // CRITICAL FIX: Require "NOTE_" prefix to avoid false positives with ASSET_* // Valid: NOTE_E2, NOTE_A4, NOTE_C#3, NOTE_Bb5 // Invalid: ASSET_KICK_1, E2 (no prefix), etc. if (name.size() < 7) // "NOTE_" + note + octave = minimum 7 chars (e.g. "NOTE_C4") return false; if (name.substr(0, 5) != "NOTE_") return false; // Check that the 6th character (after "NOTE_") is a valid note letter A-G const char note_letter = name[5]; return (note_letter >= 'A' && note_letter <= 'G'); } struct Sample { std::string name; SampleType type = GENERATED; // Default to GENERATED std::string asset_id_name; // Store AssetId name for asset samples // Parameters for generated samples float freq, dur, amp, attack, harmonic_decay; int harmonics; }; struct Event { float beat; std::string sample_name; float volume, pan; }; struct Pattern { std::string name; std::vector events; }; struct Trigger { float time; std::string pattern_name; }; int main(int argc, char** argv) { if (argc < 3) { fprintf(stderr, "Usage: %s \n", argv[0]); return 1; } std::ifstream in(argv[1]); if (!in.is_open()) { fprintf(stderr, "Could not open input file: %s\n", argv[1]); return 1; } float bpm = 120.0f; std::vector samples; std::map sample_map; std::vector patterns; std::map pattern_map; std::vector score; std::string line; std::string current_section = ""; while (std::getline(in, line)) { if (line.empty() || line[0] == '#') continue; std::stringstream ss(line); std::string cmd; ss >> cmd; if (cmd == "BPM") { ss >> bpm; } else if (cmd == "SAMPLE") { Sample s; std::string name; ss >> name; if (name.back() == ',') name.pop_back(); s.name = name; if (name.rfind("ASSET_", 0) == 0) { s.type = ASSET; s.asset_id_name = name; // Parameters for asset samples are ignored, so we don't parse them // here. However, we must consume the rest of the line to avoid issues // if a comma is present. std::string dummy; while (ss >> dummy) { } // Consume rest of line } else { s.type = GENERATED; // Very simple parsing: freq, dur, amp, attack, harmonics, // harmonic_decay char comma; ss >> s.freq >> comma >> s.dur >> comma >> s.amp >> comma >> s.attack >> comma >> s.harmonics >> comma >> s.harmonic_decay; } sample_map[s.name] = samples.size(); samples.push_back(s); } else if (cmd == "PATTERN") { Pattern p; ss >> p.name; current_section = "PATTERN:" + p.name; patterns.push_back(p); pattern_map[p.name] = patterns.size() - 1; } else if (cmd == "SCORE") { current_section = "SCORE"; } else { if (current_section.rfind("PATTERN:", 0) == 0) { // Parse event: beat, sample, vol, pan Event e; float beat; std::stringstream ss2(line); ss2 >> beat; char comma; ss2 >> comma; std::string sname; ss2 >> sname; if (sname.back() == ',') sname.pop_back(); e.beat = beat; e.sample_name = sname; ss2 >> e.volume >> comma >> e.pan; // Auto-create SAMPLE entry for note names (e.g., "E2", "A4") if (is_note_name(sname) && sample_map.find(sname) == sample_map.end()) { Sample s; s.name = sname; s.type = GENERATED; s.freq = note_name_to_freq(sname); s.dur = 0.5f; // Default note duration s.amp = 1.0f; // Default amplitude s.attack = 0.01f; // Default attack s.harmonics = 3; // Default harmonics s.harmonic_decay = 0.6f; // Default decay sample_map[s.name] = samples.size(); samples.push_back(s); } patterns.back().events.push_back(e); } else if (current_section == "SCORE") { Trigger t; float time; std::stringstream ss2(line); ss2 >> time; char comma; ss2 >> comma; std::string pname; ss2 >> pname; t.time = time; t.pattern_name = pname; score.push_back(t); } } } FILE* out_file = fopen(argv[2], "w"); if (!out_file) { fprintf(stderr, "Could not open output file: %s\n", argv[2]); return 1; } fprintf(out_file, "// Generated by tracker_compiler. Do not edit.\n\n"); fprintf(out_file, "#include \"audio/tracker.h\"\n\n"); // Need to include assets.h for AssetId enum fprintf(out_file, "#include \"generated/assets.h\"\n\n"); fprintf(out_file, "const NoteParams g_tracker_samples[] = {\n"); for (const auto& s : samples) { if (s.type == GENERATED) { fprintf(out_file, " { %.1ff, %.2ff, %.1ff, %.2ff, 0.0f, 0.0f, 0.0f, %d, %.1ff, " "0.0f, 0.0f }, // %s\n", s.freq, s.dur, s.amp, s.attack, s.harmonics, s.harmonic_decay, s.name.c_str()); } else { fprintf(out_file, " { 0 }, // %s (ASSET)\n", s.name.c_str()); } } fprintf(out_file, "};\n"); fprintf(out_file, "const uint32_t g_tracker_samples_count = %zu;\n\n", samples.size()); fprintf(out_file, "const AssetId g_tracker_sample_assets[] = {\n"); for (const auto& s : samples) { if (s.type == ASSET) { fprintf(out_file, " AssetId::%s,\n", s.asset_id_name.c_str()); } else { fprintf(out_file, " AssetId::ASSET_LAST_ID,\n"); } } fprintf(out_file, "};\n\n"); for (const auto& p : patterns) { fprintf(out_file, "static const TrackerEvent PATTERN_EVENTS_%s[] = {\n", p.name.c_str()); for (const auto& e : p.events) { // When referencing a sample, we need to get its index or synth_id. // If it's an asset, the name starts with ASSET_. // For now, assume sample_map is used for both generated and asset // samples. This will need refinement if asset samples are not in // sample_map directly. fprintf(out_file, " { %.1ff, %d, %.1ff, %.1ff },\n", e.beat, sample_map[e.sample_name], e.volume, e.pan); } fprintf(out_file, "};\n"); } fprintf(out_file, "\n"); fprintf(out_file, "const TrackerPattern g_tracker_patterns[] = {\n"); for (const auto& p : patterns) { fprintf(out_file, " { PATTERN_EVENTS_%s, %zu, 4.0f }, // %s\n", p.name.c_str(), p.events.size(), p.name.c_str()); } fprintf(out_file, "};\n"); fprintf(out_file, "const uint32_t g_tracker_patterns_count = %zu;\n\n", patterns.size()); fprintf(out_file, "static const TrackerPatternTrigger SCORE_TRIGGERS[] = {\n"); for (const auto& t : score) { fprintf(out_file, " { %.1ff, %d },\n", t.time, pattern_map[t.pattern_name]); } fprintf(out_file, "};\n\n"); fprintf(out_file, "const TrackerScore g_tracker_score = {\n"); fprintf(out_file, " SCORE_TRIGGERS, %zu, %.1ff\n", score.size(), bpm); fprintf(out_file, "};\n\n"); // ============================================================================ // RESOURCE USAGE ANALYSIS // ============================================================================ // Count unique samples int asset_sample_count = 0; int generated_sample_count = 0; for (const auto& s : samples) { if (s.type == ASSET) { asset_sample_count++; } else { generated_sample_count++; } } // Calculate maximum simultaneous pattern triggers std::map time_pattern_count; for (const auto& t : score) { time_pattern_count[t.time]++; } int max_simultaneous_patterns = 0; for (const auto& entry : time_pattern_count) { if (entry.second > max_simultaneous_patterns) { max_simultaneous_patterns = entry.second; } } // Calculate maximum polyphony (events per pattern on average) int total_events = 0; for (const auto& p : patterns) { total_events += p.events.size(); } const int avg_events_per_pattern = patterns.empty() ? 0 : total_events / patterns.size(); const int estimated_max_polyphony = max_simultaneous_patterns * avg_events_per_pattern; // Conservative recommendations with safety margins // - Each asset sample needs 1 spectrogram slot (shared across all events) // - Each generated note needs 1 spectrogram slot PER EVENT (no caching yet) // - Add 50% safety margin for peak moments const int min_spectrograms = asset_sample_count + (generated_sample_count * estimated_max_polyphony); const int recommended_spectrograms = (int)(min_spectrograms * 1.5f); const int recommended_voices = estimated_max_polyphony * 2; fprintf(out_file, "// ============================================================\n"); fprintf(out_file, "// RESOURCE USAGE ANALYSIS (for synth.h configuration)\n"); fprintf(out_file, "// ============================================================\n"); fprintf(out_file, "// Total samples: %d (%d assets + %d generated notes)\n", (int)samples.size(), asset_sample_count, generated_sample_count); fprintf(out_file, "// Max simultaneous pattern triggers: %d\n", max_simultaneous_patterns); fprintf(out_file, "// Estimated max polyphony: %d voices\n", estimated_max_polyphony); fprintf(out_file, "// \n"); fprintf(out_file, "// REQUIRED (minimum to avoid pool exhaustion):\n"); fprintf(out_file, "// MAX_VOICES: %d\n", estimated_max_polyphony); fprintf(out_file, "// MAX_SPECTROGRAMS: %d (no caching)\n", min_spectrograms); fprintf(out_file, "// \n"); fprintf(out_file, "// RECOMMENDED (with 50%% safety margin):\n"); fprintf(out_file, "// MAX_VOICES: %d\n", recommended_voices); fprintf(out_file, "// MAX_SPECTROGRAMS: %d (no caching)\n", recommended_spectrograms); fprintf(out_file, "// \n"); fprintf(out_file, "// NOTE: With spectrogram caching by note parameters,\n"); fprintf(out_file, "// MAX_SPECTROGRAMS could be reduced to ~%d\n", asset_sample_count + generated_sample_count); fprintf(out_file, "// ============================================================\n\n"); fclose(out_file); printf("Tracker compilation successful.\n"); printf(" Patterns: %zu\n", patterns.size()); printf(" Score triggers: %zu\n", score.size()); printf(" Samples: %d (%d assets + %d generated)\n", (int)samples.size(), asset_sample_count, generated_sample_count); printf(" Max simultaneous patterns: %d\n", max_simultaneous_patterns); printf(" Estimated max polyphony: %d voices\n", estimated_max_polyphony); printf("\n"); printf("RESOURCE REQUIREMENTS:\n"); printf(" Required MAX_VOICES: %d\n", estimated_max_polyphony); printf(" Required MAX_SPECTROGRAMS: %d (without caching)\n", min_spectrograms); printf(" Recommended MAX_VOICES: %d (with safety margin)\n", recommended_voices); printf(" Recommended MAX_SPECTROGRAMS: %d (with safety margin)\n", recommended_spectrograms); printf(" With caching: MAX_SPECTROGRAMS could be ~%d\n", asset_sample_count + generated_sample_count); return 0; }