From 74fae04499336c45b0d94ae1c2ac6a64fa7e8823 Mon Sep 17 00:00:00 2001 From: djdiskmachine <110535302+djdiskmachine@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:28:49 +0000 Subject: [PATCH 1/2] Fix regression for monowave Conditionally drag the start position together with the loop window Preserves old behavior for single cycle stuff Regression caused pitch drift and microtonality --- .../Instruments/SampleInstrument.cpp | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/sources/Application/Instruments/SampleInstrument.cpp b/sources/Application/Instruments/SampleInstrument.cpp index 6fd63f13..33aca908 100644 --- a/sources/Application/Instruments/SampleInstrument.cpp +++ b/sources/Application/Instruments/SampleInstrument.cpp @@ -1087,31 +1087,63 @@ void SampleInstrument::ProcessCommand(int channel,FourCC cc,ushort value) { if (!source_) return ; switch(cc) { - case I_CMD_LPOF: + case I_CMD_LPOF: { + // LPOF (Loop Offset) shifts the loop window (rendLoopStart_, + // rendLoopEnd_) within the sample. Two coexisting use cases: + // + // 1) Wavetable / single-cycle synthesis (SILM_OSC): + // The loop length defines the oscillator's pitch (freq * length / + // driverRate). LPOF is used to scan across stored waveforms or + // modulate timbre (e.g. PWM) WITHOUT changing pitch. We must NOT + // move the playhead here — doing so makes pitch glide as the bounds + // shift mid-cycle, producing unwanted microtonal artifacts. + // + // 2) Granular stretching (every other mode — loop, ping-pong, loopsync, + // oneshot, sliced or not): + // Advancing the playhead alongside the loop window drags playback + // through the source faster than the note's natural rate, decoupling + // stretch speed from pitch. With short loops + LPOF + HOP in a + // table, this produces timestretch / breakbeat-style scrubbing. + // + // In oneshot or slice modes the playhead may cross into territory + // the user didn't anticipate (notes ending early, slices bleeding + // into neighbors). That's intentional — these are useful artifacts, + // not bugs. + + SampleInstrumentLoopMode loopmode = + (SampleInstrumentLoopMode)loopMode_->GetInt(); + bool dragPlayhead = (loopmode != SILM_OSC); + if (value > 0x8000) { + // Backward shift (two's complement): 0xFFFF = -1, 0x8001 = -32767 int shift = (int)(0x10000 - value); - if (shift > - rp->rendLoopStart_) { // Clamp so to not back out of sample + if (shift > rp->rendLoopStart_) { // Don't push start below sample 0 shift = rp->rendLoopStart_; } rp->rendLoopEnd_ -= shift; rp->rendLoopStart_ -= shift; - rp->position_ -= shift; - } else if (value > 0) { // LPOF 0000 is a no-op, this is not that + if (dragPlayhead) { + rp->position_ -= shift; + } + } else if (value > 0) { // LPOF 0000 is a no-op int sampleSize = source_->GetSize(rp->midiNote_); int shift = (int)value; - if (rp->rendLoopEnd_ + shift >= - sampleSize) { // Clamp so to not play outside sample + // Clamp so rendLoopEnd_ doesn't escape the sample. When the window + // hits the end, further forward LPOFs become no-ops — the loop is + // parked at the boundary until something resets it. + if (rp->rendLoopEnd_ + shift >= sampleSize) { shift = sampleSize - rp->rendLoopEnd_; } if (shift > 0) { rp->rendLoopEnd_ += shift; rp->rendLoopStart_ += shift; - rp->position_ += shift; + if (dragPlayhead) { + rp->position_ += shift; + } } } break; - + } case I_CMD_PLOF: { if (!source_) return; From 19578fbddea7151148cc31e9f0c7825c54f7b6ac Mon Sep 17 00:00:00 2001 From: djdiskmachine <110535302+djdiskmachine@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:35:11 +0000 Subject: [PATCH 2/2] Update CHANGELOG Add qualifier info in tips_and_tricks --- CHANGELOG | 2 ++ docs/wiki/tips_and_tricks.md | 9 ++++++--- sources/Application/Model/Project.h | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 164e0e3e..efe659c2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,8 @@ Various additions Fix audio dying on multithreaded systems (#236) * MIDI queue protected by mutex lock on X64 * Fixes "audio suddenly dies" error + Fix regression caused in https://github.com/djdiskmachine/LittleGPTracker/pull/245 + Fix introduced microtonality for single cycle osc, thank you @INFU-AV <3 1.6.0-bacon11 Slices decoupled from loop mode diff --git a/docs/wiki/tips_and_tricks.md b/docs/wiki/tips_and_tricks.md index 50da5bc8..678c6e44 100644 --- a/docs/wiki/tips_and_tricks.md +++ b/docs/wiki/tips_and_tricks.md @@ -128,11 +128,14 @@ loops — just chop anything away! You can use `LPOF` in a looping table to scroll a short loop window through a sample independently of playback pitch — a form of granular timestretching. Set a short loop on the instrument, then add a table that advances the loop position every tick: ``` -00 LPOF xxxx ---- ---- -01 HOP 0000 ---- ---- +00 LPOF xyzq +01 HOP 0000 ``` -Each tick, the loop window shifts forward by `xxxx` samples. The note's pitch is still determined by the loop length and oscillator tuning; the *content* of the loop changes as it travels through the sample. This lets you play a sample at a different speed than its pitch — stretching or compressing its duration without affecting the note you hear. +Each tick, the loop window shifts forward by `xyzq` samples. The note's pitch is still determined by the loop length and note tuning tuning; the *content* of the loop changes as it travels through the sample. This lets you play a sample at a different speed than its pitch — stretching or compressing its duration without affecting the note you hear. +This technique works in `LOOP`, `LOOP_PINGPONG`, `LOOPSYNC` and`ONESHOT` modes. +In `OSCILLATOR` mode (wavetable) — `LPOF` only shifts the waveform +window without advancing playback, preserving pitch stability for timbre scanning using monowave or single-cycle waveforms. **Sync to BPM (V_sync)** diff --git a/sources/Application/Model/Project.h b/sources/Application/Model/Project.h index 5dea7d7b..cd92d950 100644 --- a/sources/Application/Model/Project.h +++ b/sources/Application/Model/Project.h @@ -21,7 +21,7 @@ #define PROJECT_NUMBER "1" #define PROJECT_RELEASE "6" -#define BUILD_COUNT "0-bacon11" +#define BUILD_COUNT "0-bacon12" #define MAX_TAP 3