Skip to content

Commit 9e82234

Browse files
Fix regression for monowave (#249)
* 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 Add qualifier info in tips_and_tricks
1 parent ad9b6f3 commit 9e82234

4 files changed

Lines changed: 50 additions & 13 deletions

File tree

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Various additions
2020
Fix audio dying on multithreaded systems (#236)
2121
* MIDI queue protected by mutex lock on X64
2222
* Fixes "audio suddenly dies" error
23+
Fix regression caused in https://github.com/djdiskmachine/LittleGPTracker/pull/245
24+
Fix introduced microtonality for single cycle osc, thank you @INFU-AV <3
2325

2426
1.6.0-bacon11
2527
Slices decoupled from loop mode

docs/wiki/tips_and_tricks.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,14 @@ loops — just chop anything away!
128128
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:
129129

130130
```
131-
00 LPOF xxxx ---- ----
132-
01 HOP 0000 ---- ----
131+
00 LPOF xyzq
132+
01 HOP 0000
133133
```
134134

135-
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.
135+
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.
136+
This technique works in `LOOP`, `LOOP_PINGPONG`, `LOOPSYNC` and`ONESHOT` modes.
137+
In `OSCILLATOR` mode (wavetable) — `LPOF` only shifts the waveform
138+
window without advancing playback, preserving pitch stability for timbre scanning using monowave or single-cycle waveforms.
136139

137140
**Sync to BPM (V_sync)**
138141

sources/Application/Instruments/SampleInstrument.cpp

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,31 +1087,63 @@ void SampleInstrument::ProcessCommand(int channel,FourCC cc,ushort value) {
10871087
if (!source_) return ;
10881088

10891089
switch(cc) {
1090-
case I_CMD_LPOF:
1090+
case I_CMD_LPOF: {
1091+
// LPOF (Loop Offset) shifts the loop window (rendLoopStart_,
1092+
// rendLoopEnd_) within the sample. Two coexisting use cases:
1093+
//
1094+
// 1) Wavetable / single-cycle synthesis (SILM_OSC):
1095+
// The loop length defines the oscillator's pitch (freq * length /
1096+
// driverRate). LPOF is used to scan across stored waveforms or
1097+
// modulate timbre (e.g. PWM) WITHOUT changing pitch. We must NOT
1098+
// move the playhead here — doing so makes pitch glide as the bounds
1099+
// shift mid-cycle, producing unwanted microtonal artifacts.
1100+
//
1101+
// 2) Granular stretching (every other mode — loop, ping-pong, loopsync,
1102+
// oneshot, sliced or not):
1103+
// Advancing the playhead alongside the loop window drags playback
1104+
// through the source faster than the note's natural rate, decoupling
1105+
// stretch speed from pitch. With short loops + LPOF + HOP in a
1106+
// table, this produces timestretch / breakbeat-style scrubbing.
1107+
//
1108+
// In oneshot or slice modes the playhead may cross into territory
1109+
// the user didn't anticipate (notes ending early, slices bleeding
1110+
// into neighbors). That's intentional — these are useful artifacts,
1111+
// not bugs.
1112+
1113+
SampleInstrumentLoopMode loopmode =
1114+
(SampleInstrumentLoopMode)loopMode_->GetInt();
1115+
bool dragPlayhead = (loopmode != SILM_OSC);
1116+
10911117
if (value > 0x8000) {
1118+
// Backward shift (two's complement): 0xFFFF = -1, 0x8001 = -32767
10921119
int shift = (int)(0x10000 - value);
1093-
if (shift >
1094-
rp->rendLoopStart_) { // Clamp so to not back out of sample
1120+
if (shift > rp->rendLoopStart_) { // Don't push start below sample 0
10951121
shift = rp->rendLoopStart_;
10961122
}
10971123
rp->rendLoopEnd_ -= shift;
10981124
rp->rendLoopStart_ -= shift;
1099-
rp->position_ -= shift;
1100-
} else if (value > 0) { // LPOF 0000 is a no-op, this is not that
1125+
if (dragPlayhead) {
1126+
rp->position_ -= shift;
1127+
}
1128+
} else if (value > 0) { // LPOF 0000 is a no-op
11011129
int sampleSize = source_->GetSize(rp->midiNote_);
11021130
int shift = (int)value;
1103-
if (rp->rendLoopEnd_ + shift >=
1104-
sampleSize) { // Clamp so to not play outside sample
1131+
// Clamp so rendLoopEnd_ doesn't escape the sample. When the window
1132+
// hits the end, further forward LPOFs become no-ops — the loop is
1133+
// parked at the boundary until something resets it.
1134+
if (rp->rendLoopEnd_ + shift >= sampleSize) {
11051135
shift = sampleSize - rp->rendLoopEnd_;
11061136
}
11071137
if (shift > 0) {
11081138
rp->rendLoopEnd_ += shift;
11091139
rp->rendLoopStart_ += shift;
1110-
rp->position_ += shift;
1140+
if (dragPlayhead) {
1141+
rp->position_ += shift;
1142+
}
11111143
}
11121144
}
11131145
break;
1114-
1146+
}
11151147
case I_CMD_PLOF: {
11161148
if (!source_)
11171149
return;

sources/Application/Model/Project.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
#define PROJECT_NUMBER "1"
2323
#define PROJECT_RELEASE "6"
24-
#define BUILD_COUNT "0-bacon11"
24+
#define BUILD_COUNT "0-bacon12"
2525

2626
#define MAX_TAP 3
2727

0 commit comments

Comments
 (0)