#include #include #include #include #include #include #include #include // Enum to differentiate between sample types enum SampleType { GENERATED, ASSET }; // Convert note name (e.g., "C4", "A#3", "Eb2") to frequency in Hz static float note_name_to_freq(const std::string& note_name) { if (note_name.empty()) return 0.0f; // Parse note (C, C#, D, etc.) and octave const char* str = note_name.c_str(); 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) { if (name.empty()) return false; const char first = name[0]; return (first >= 'A' && first <= '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"); // Calculate maximum simultaneous patterns for optimal resource allocation 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; } } // Add safety margin (2x) for overlapping pattern playback const int recommended_voices = max_simultaneous_patterns * 2; const int recommended_spectrograms = max_simultaneous_patterns * 2; fprintf(out_file, "// Resource usage analysis:\n"); fprintf(out_file, "// Maximum simultaneous pattern triggers: %d\n", max_simultaneous_patterns); fprintf(out_file, "// Recommended MAX_VOICES: %d (current: see synth.h)\n", recommended_voices); fprintf(out_file, "// Recommended MAX_SPECTROGRAMS: %d (current: see synth.h)\n", recommended_spectrograms); fclose(out_file); printf("Tracker compilation successful.\n"); printf(" Patterns: %zu\n", patterns.size()); printf(" Score triggers: %zu\n", score.size()); printf(" Max simultaneous patterns: %d\n", max_simultaneous_patterns); printf(" Recommended MAX_VOICES: %d\n", recommended_voices); printf(" Recommended MAX_SPECTROGRAMS: %d\n", recommended_spectrograms); return 0; }