summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-14 18:06:24 +0100
committerskal <pascal.massimino@gmail.com>2026-02-14 18:06:24 +0100
commite9dde3cea39e69d6188a7f49034f6d95e4c8b6b4 (patch)
treeb3b383eff5f0d1d18bdbb781c331e6a31fcb25a7 /tools
parentb4c901700de0d9e867b9fd0c0c6a6586578f8480 (diff)
feat(tracker): add sample offset and humanization
Implements two tracker realism features: 1. Sample Offset (compile-time): - Add offset_sec field to NoteParams and Sample structs - Parse OFFSET parameter in SAMPLE directive - Apply timing shift during compilation (zero runtime cost) - Use for attack-heavy samples to align perceived beat 2. Humanization (runtime, deterministic): - Add humanize_seed, timing_variation_pct, volume_variation_pct to TrackerScore - Parse HUMANIZE directive with SEED/TIMING/VOLUME params - Apply per-event RNG jitter using std::minstd_rand - Deterministic: same seed produces identical output Both features work in real-time playback and WAV export. Test file: data/test_humanize.track Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'tools')
-rw-r--r--tools/tracker_compiler.cc65
1 files changed, 49 insertions, 16 deletions
diff --git a/tools/tracker_compiler.cc b/tools/tracker_compiler.cc
index de635cd..6bc22be 100644
--- a/tools/tracker_compiler.cc
+++ b/tools/tracker_compiler.cc
@@ -98,6 +98,7 @@ struct Sample {
// Parameters for generated samples
float freq, dur, amp, attack, harmonic_decay;
int harmonics;
+ float offset_sec = 0.0f;
};
struct Event {
@@ -409,6 +410,9 @@ int main(int argc, char** argv) {
}
float bpm = 120.0f;
+ uint32_t humanize_seed = 0;
+ float timing_variation_pct = 0.0f;
+ float volume_variation_pct = 0.0f;
std::vector<Sample> samples;
std::map<std::string, int> sample_map;
std::vector<Pattern> patterns;
@@ -432,6 +436,17 @@ int main(int argc, char** argv) {
if (cmd == "BPM") {
ss >> bpm;
+ } else if (cmd == "HUMANIZE") {
+ std::string key;
+ while (ss >> key) {
+ if (key == "SEED") {
+ ss >> humanize_seed;
+ } else if (key == "TIMING") {
+ ss >> timing_variation_pct;
+ } else if (key == "VOLUME") {
+ ss >> volume_variation_pct;
+ }
+ }
} else if (cmd == "SAMPLE") {
Sample s;
std::string name;
@@ -443,12 +458,13 @@ int main(int argc, char** argv) {
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
+ // Parse optional KEY-VALUE parameters for asset samples
+ std::string token;
+ while (ss >> token) {
+ if (token == "OFFSET") {
+ ss >> s.offset_sec;
+ }
+ }
} else {
s.type = GENERATED;
// Very simple parsing: freq, dur, amp, attack, harmonics,
@@ -456,6 +472,14 @@ int main(int argc, char** argv) {
char comma;
ss >> s.freq >> comma >> s.dur >> comma >> s.amp >> comma >> s.attack >>
comma >> s.harmonics >> comma >> s.harmonic_decay;
+
+ // Parse optional KEY-VALUE parameters after the standard params
+ std::string token;
+ while (ss >> token) {
+ if (token == "OFFSET") {
+ ss >> s.offset_sec;
+ }
+ }
}
sample_map[s.name] = samples.size();
@@ -565,9 +589,9 @@ int main(int argc, char** argv) {
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",
+ "0.0f, 0.0f, %.3ff }, // %s\n",
s.freq, s.dur, s.amp, s.attack, s.harmonics, s.harmonic_decay,
- s.name.c_str());
+ s.offset_sec, s.name.c_str());
} else {
fprintf(out_file, " { 0 }, // %s (ASSET)\n", s.name.c_str());
}
@@ -594,17 +618,24 @@ int main(int argc, char** argv) {
});
}
+ const float BEATS_PER_UNIT = 4.0f;
+ const float unit_duration_sec = (BEATS_PER_UNIT / bpm) * 60.0f;
+
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, " { %.2ff, %d, %.1ff, %.1ff },\n", e.unit_time,
- sample_map[e.sample_name], e.volume, e.pan);
+ // Apply compile-time sample offset
+ int sample_id = sample_map[e.sample_name];
+ float adjusted_time = e.unit_time;
+ if (sample_id >= 0 && sample_id < (int)samples.size()) {
+ const Sample& sample = samples[sample_id];
+ if (sample.offset_sec != 0.0f) {
+ adjusted_time = e.unit_time - (sample.offset_sec / unit_duration_sec);
+ }
+ }
+ fprintf(out_file, " { %.2ff, %d, %.1ff, %.1ff },\n", adjusted_time,
+ sample_id, e.volume, e.pan);
}
fprintf(out_file, "};\n");
}
@@ -628,7 +659,9 @@ int main(int argc, char** argv) {
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, " SCORE_TRIGGERS, %zu, %.1ff, %u, %.1ff, %.1ff\n",
+ score.size(), bpm, humanize_seed, timing_variation_pct,
+ volume_variation_pct);
fprintf(out_file, "};\n\n");
// Analyze resource requirements