summaryrefslogtreecommitdiff
path: root/src
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 /src
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 'src')
-rw-r--r--src/audio/gen.h1
-rw-r--r--src/audio/tracker.cc33
-rw-r--r--src/audio/tracker.h3
3 files changed, 34 insertions, 3 deletions
diff --git a/src/audio/gen.h b/src/audio/gen.h
index 94f4ee3..370971b 100644
--- a/src/audio/gen.h
+++ b/src/audio/gen.h
@@ -17,6 +17,7 @@ struct NoteParams {
float harmonic_decay;
float pitch_randomness;
float amp_randomness;
+ float offset_sec = 0.0f;
};
// Generates a single note into a new spectrogram buffer
diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc
index 67c197f..1c0a9b2 100644
--- a/src/audio/tracker.cc
+++ b/src/audio/tracker.cc
@@ -5,6 +5,7 @@
#include "util/debug.h"
#include "util/fatal_error.h"
#include <cstring>
+#include <random>
#include <vector>
static uint32_t g_last_trigger_idx = 0;
@@ -190,8 +191,9 @@ static int get_free_pattern_slot() {
// Helper to trigger a single note event (OPTIMIZED with caching)
// start_offset_samples: How many samples into the future to trigger (for
// sample-accurate timing)
+// volume_mult: Additional volume multiplier (for humanization)
static void trigger_note_event(const TrackerEvent& event,
- int start_offset_samples) {
+ int start_offset_samples, float volume_mult = 1.0f) {
#if defined(DEBUG_LOG_TRACKER)
// VALIDATION: Check sample_id bounds
if (event.sample_id >= g_tracker_samples_count) {
@@ -227,7 +229,7 @@ static void trigger_note_event(const TrackerEvent& event,
}
// Trigger voice with sample-accurate offset
- synth_trigger_voice(cached_synth_id, event.volume, event.pan,
+ synth_trigger_voice(cached_synth_id, event.volume * volume_mult, event.pan,
start_offset_samples);
}
@@ -287,7 +289,32 @@ void tracker_update(float music_time_sec, float dt_music_sec) {
(int)((event_music_time - music_time_sec) / tempo_scale * 32000.0f);
}
- trigger_note_event(event, sample_offset);
+ // Apply humanization if enabled
+ float volume_mult = 1.0f;
+ if (g_tracker_score.humanize_seed != 0) {
+ // Deterministic per-event RNG: hash seed with pattern/event indices
+ uint32_t event_hash = g_tracker_score.humanize_seed ^
+ (active.pattern_id << 16) ^ active.next_event_idx;
+ std::minstd_rand rng(event_hash);
+ std::uniform_real_distribution<float> dist(-1.0f, 1.0f);
+
+ // Timing variation: jitter by % of beat duration
+ if (g_tracker_score.timing_variation_pct > 0.0f) {
+ float beat_sec = 60.0f / g_tracker_score.bpm;
+ float jitter = dist(rng) *
+ (g_tracker_score.timing_variation_pct / 100.0f) *
+ beat_sec;
+ sample_offset += (int)(jitter / tempo_scale * 32000.0f);
+ }
+
+ // Volume variation: vary by %
+ if (g_tracker_score.volume_variation_pct > 0.0f) {
+ volume_mult +=
+ dist(rng) * (g_tracker_score.volume_variation_pct / 100.0f);
+ }
+ }
+
+ trigger_note_event(event, sample_offset, volume_mult);
active.next_event_idx++;
}
diff --git a/src/audio/tracker.h b/src/audio/tracker.h
index 8e7a63f..8e89a4e 100644
--- a/src/audio/tracker.h
+++ b/src/audio/tracker.h
@@ -31,6 +31,9 @@ struct TrackerScore {
const TrackerPatternTrigger* triggers;
uint32_t num_triggers;
float bpm; // BPM is used only for playback scaling (1 unit = 4 beats)
+ uint32_t humanize_seed = 0;
+ float timing_variation_pct = 0.0f;
+ float volume_variation_pct = 0.0f;
};
// Global music data generated by tracker_compiler