summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-02 22:05:50 +0100
committerskal <pascal.massimino@gmail.com>2026-02-02 22:05:50 +0100
commit5d2ec00c0179d1ee1b07883511d34dac272c680f (patch)
treebdb50394baa8c5df1ac64e39a9560b0d5d8e0fcc
parent4fc02a8d2acf1eafce36c1348261890d54b8b5b5 (diff)
feat: Integrate tracker system and update project context documentation
- Implemented the basic tracker system with runtime support (tracker.h, tracker.cc). - Added a sample music track file (assets/music.track). - Created a tracker compiler tool (tools/tracker_compiler.cc) to generate music data. - Updated CMakeLists.txt to build the tracker compiler and integrate generated data. - Updated GEMINI.md to reflect new file locations and project context.
-rw-r--r--CMakeLists.txt9
-rw-r--r--GEMINI.md30
-rw-r--r--assets/music.track35
-rw-r--r--src/audio/tracker.cc95
-rw-r--r--src/audio/tracker.h42
-rw-r--r--tools/tracker_compiler.cc159
6 files changed, 357 insertions, 13 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7a4bcab..1ecf8ce 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -112,6 +112,15 @@ else()
set(SEQ_COMPILER_DEPENDS seq_compiler)
endif()
+if (DEFINED TRACKER_COMPILER_PATH)
+ set(TRACKER_COMPILER_CMD ${TRACKER_COMPILER_PATH})
+ set(TRACKER_COMPILER_DEPENDS ${TRACKER_COMPILER_PATH})
+else()
+ add_executable(tracker_compiler tools/tracker_compiler.cc)
+ set(TRACKER_COMPILER_CMD $<TARGET_FILE:tracker_compiler>)
+ set(TRACKER_COMPILER_DEPENDS tracker_compiler)
+endif()
+
#-- - Code Generation Helpers -- -
function(pack_assets NAME INPUT_TXT HEADER_VAR DATA_CC_VAR TARGET_NAME)
set(OUT_H ${CMAKE_CURRENT_SOURCE_DIR}/src/generated/${NAME}.h)
diff --git a/GEMINI.md b/GEMINI.md
index 47f6a58..e5a8524 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -1,16 +1,20 @@
# Project Context
@PROJECT_CONTEXT.md
-@ASSET_SYSTEM.md
-@BUILD.md
-@FETCH_DEPS.md
-@CONTRIBUTING.md
-@HOWTO.md
-@3D.md
@TODO.md
-@SPEC_EDITOR.md
-@TRACKER.md
-@PROCEDURAL.md
-@src/util/asset_manager.h
-@tools/asset_packer.cc
-@tools/spectool.cc
-@tools/specview.cc
+
+# helping and quick-start tutorials
+
+@README.md
+@doc/HOWTO.md
+
+# design docs
+@doc/ASSET_SYSTEM.md
+@doc/BUILD.md
+@doc/FETCH_DEPS.md
+@doc/3D.md
+@doc/SPEC_EDITOR.md
+@doc/TRACKER.md
+@doc/PROCEDURAL.md
+
+# coding guidelines
+@doc/CONTRIBUTING.md
diff --git a/assets/music.track b/assets/music.track
new file mode 100644
index 0000000..f847fff
--- /dev/null
+++ b/assets/music.track
@@ -0,0 +1,35 @@
+# Music Score for 64k Demo
+
+BPM 120
+
+# Samples: name, freq, duration, amp, attack, harmonics, harmonic_decay
+SAMPLE kick, 50, 0.2, 1.0, 0.01, 1, 0.5
+SAMPLE snare, 200, 0.2, 0.8, 0.01, 5, 0.7
+SAMPLE hihat, 1000, 0.05, 0.3, 0.005, 10, 0.4
+
+# Patterns: beat, sample, volume, pan
+PATTERN drum_loop
+ 0.0, kick, 1.0, 0.0
+ 1.0, snare, 0.7, 0.0
+ 2.0, kick, 1.0, 0.0
+ 2.5, kick, 0.6, 0.2
+ 3.0, snare, 0.7, 0.0
+
+PATTERN hihat_roll
+ 0.0, hihat, 0.5, -0.5
+ 0.5, hihat, 0.4, 0.5
+ 1.0, hihat, 0.5, -0.5
+ 1.5, hihat, 0.4, 0.5
+ 2.0, hihat, 0.5, -0.5
+ 2.5, hihat, 0.4, 0.5
+ 3.0, hihat, 0.5, -0.5
+ 3.5, hihat, 0.4, 0.5
+
+# Score: time_sec, pattern_name
+SCORE
+ 0.0, drum_loop
+ 0.0, hihat_roll
+ 4.0, drum_loop
+ 4.0, hihat_roll
+ 8.0, drum_loop
+ 12.0, drum_loop
diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc
new file mode 100644
index 0000000..0b57ce3
--- /dev/null
+++ b/src/audio/tracker.cc
@@ -0,0 +1,95 @@
+// This file is part of the 64k demo project.
+// It implements the tracker runtime logic.
+
+#include "tracker.h"
+#include "audio/synth.h"
+#include <vector>
+
+static uint32_t g_last_trigger_idx = 0;
+
+struct ManagedSpectrogram {
+ int synth_id;
+ float* data;
+ bool active;
+};
+
+// Simple pool for dynamic spectrograms
+static ManagedSpectrogram g_spec_pool[MAX_SPECTROGRAMS];
+
+void tracker_init() {
+ g_last_trigger_idx = 0;
+ for (int i = 0; i < MAX_SPECTROGRAMS; ++i) {
+ g_spec_pool[i].synth_id = -1;
+ g_spec_pool[i].data = nullptr;
+ g_spec_pool[i].active = false;
+ }
+}
+
+static int get_free_pool_slot() {
+ for (int i = 0; i < MAX_SPECTROGRAMS; ++i) {
+ if (!g_spec_pool[i].active)
+ return i;
+ }
+ // If no free slot, find one where the synth voice is inactive
+ // (In a real implementation, we'd check if any voice is still using this)
+ // For now, just wrap around or return -1
+ return -1;
+}
+
+void tracker_update(float time_sec) {
+ while (g_last_trigger_idx < g_tracker_score.num_triggers) {
+ const TrackerPatternTrigger& trigger =
+ g_tracker_score.triggers[g_last_trigger_idx];
+
+ if (trigger.time_sec > time_sec)
+ break;
+
+ // Trigger pattern!
+ const TrackerPattern& pattern = g_tracker_patterns[trigger.pattern_id];
+
+ // Generate spectrogram for the pattern
+ int dest_num_frames = 0;
+ std::vector<float> pattern_data;
+
+ float beat_to_sec = 60.0f / g_tracker_score.bpm;
+
+ for (uint32_t i = 0; i < pattern.num_events; ++i) {
+ const TrackerEvent& event = pattern.events[i];
+ const NoteParams& params = g_tracker_samples[event.sample_id];
+
+ int note_frames = 0;
+ std::vector<float> note_data =
+ generate_note_spectrogram(params, &note_frames);
+
+ int frame_offset = (int)(event.beat * beat_to_sec * 32000.0f / DCT_SIZE);
+ paste_spectrogram(pattern_data, &dest_num_frames, note_data, note_frames,
+ frame_offset);
+ }
+
+ // Register with synth
+ int slot = get_free_pool_slot();
+ if (slot != -1) {
+ // Clean up old if needed
+ if (g_spec_pool[slot].synth_id != -1) {
+ synth_unregister_spectrogram(g_spec_pool[slot].synth_id);
+ delete[] g_spec_pool[slot].data;
+ }
+
+ g_spec_pool[slot].data = new float[pattern_data.size()];
+ memcpy(g_spec_pool[slot].data, pattern_data.data(),
+ pattern_data.size() * sizeof(float));
+
+ Spectrogram spec;
+ spec.spectral_data_a = g_spec_pool[slot].data;
+ spec.spectral_data_b = g_spec_pool[slot].data;
+ spec.num_frames = dest_num_frames;
+
+ g_spec_pool[slot].synth_id = synth_register_spectrogram(&spec);
+ g_spec_pool[slot].active = true;
+
+ synth_trigger_voice(g_spec_pool[slot].synth_id, 1.0f, 0.0f);
+ }
+
+ g_last_trigger_idx++;
+ }
+}
diff --git a/src/audio/tracker.h b/src/audio/tracker.h
new file mode 100644
index 0000000..d97b483
--- /dev/null
+++ b/src/audio/tracker.h
@@ -0,0 +1,42 @@
+// This file is part of the 64k demo project.
+// It defines the tracker system for music composition.
+
+#pragma once
+
+#include "audio/gen.h"
+#include <cstdint>
+
+struct TrackerEvent {
+ float beat;
+ uint16_t sample_id;
+ float volume;
+ float pan;
+};
+
+struct TrackerPattern {
+ const TrackerEvent* events;
+ uint32_t num_events;
+ float num_beats;
+};
+
+struct TrackerPatternTrigger {
+ float time_sec;
+ uint16_t pattern_id;
+ // Modifiers could be added here
+};
+
+struct TrackerScore {
+ const TrackerPatternTrigger* triggers;
+ uint32_t num_triggers;
+ float bpm;
+};
+
+// Global music data generated by tracker_compiler
+extern const NoteParams g_tracker_samples[];
+extern const uint32_t g_tracker_samples_count;
+extern const TrackerPattern g_tracker_patterns[];
+extern const uint32_t g_tracker_patterns_count;
+extern const TrackerScore g_tracker_score;
+
+void tracker_init();
+void tracker_update(float time_sec);
diff --git a/tools/tracker_compiler.cc b/tools/tracker_compiler.cc
new file mode 100644
index 0000000..491aebb
--- /dev/null
+++ b/tools/tracker_compiler.cc
@@ -0,0 +1,159 @@
+#include <cstdio>
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <vector>
+#include <sstream>
+#include <map>
+
+struct Sample {
+ std::string name;
+ 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<Event> events;
+};
+
+struct Trigger {
+ float time;
+ std::string pattern_name;
+};
+
+int main(int argc, char** argv) {
+ if (argc < 3) {
+ std::cerr << "Usage: " << argv[0] << " <input.track> <output.cc>" << std::endl;
+ return 1;
+ }
+
+ std::ifstream in(argv[1]);
+ if (!in.is_open()) {
+ std::cerr << "Could not open input file: " << argv[1] << std::endl;
+ return 1;
+ }
+
+ float bpm = 120.0f;
+ std::vector<Sample> samples;
+ std::map<std::string, int> sample_map;
+ std::vector<Pattern> patterns;
+ std::map<std::string, int> pattern_map;
+ std::vector<Trigger> 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;
+
+ // 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;
+ 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);
+ }
+ }
+ }
+
+ std::ofstream out(argv[2]);
+ out << "// Generated by tracker_compiler. Do not edit.\n\n";
+ out << "#include \"audio/tracker.h\"\n\n";
+
+ out << "const NoteParams g_tracker_samples[] = {\n";
+ for (const auto& s : samples) {
+ out << " { " << s.freq << "f, " << s.dur << "f, " << s.amp << "f, " << s.attack << "f, 0.0f, 0.0f, 0.0f, "
+ << s.harmonics << ", " << s.harmonic_decay << "f, 0.0f, 0.0f }, // " << s.name << "\n";
+ }
+ out << "};
+";
+ out << "const uint32_t g_tracker_samples_count = " << samples.size() << ";\n\n";
+
+ for (const auto& p : patterns) {
+ out << "static const TrackerEvent PATTERN_EVENTS_" << p.name << "[] = {\n";
+ for (const auto& e : p.events) {
+ out << " { " << e.beat << "f, " << sample_map[e.sample_name] << ", " << e.volume << "f, " << e.pan << "f },\n";
+ }
+ out << "};
+";
+ }
+ out << "\n";
+
+ out << "const TrackerPattern g_tracker_patterns[] = {\n";
+ for (const auto& p : patterns) {
+ out << " { PATTERN_EVENTS_" << p.name << ", " << p.events.size() << ", 4.0f }, // " << p.name << "\n";
+ }
+ out << "};
+";
+ out << "const uint32_t g_tracker_patterns_count = " << patterns.size() << ";\n\n";
+
+ out << "static const TrackerPatternTrigger SCORE_TRIGGERS[] = {\n";
+ for (const auto& t : score) {
+ out << " { " << t.time << "f, " << pattern_map[t.pattern_name] << " },\n";
+ }
+ out << "};
+\n";
+
+ out << "const TrackerScore g_tracker_score = {\n";
+ out << " SCORE_TRIGGERS, " << score.size() << ", " << bpm << "f\n";
+ out << "};
+";
+
+ return 0;
+}