Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions docs/wiki/tips_and_tricks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**

Expand Down
50 changes: 41 additions & 9 deletions sources/Application/Instruments/SampleInstrument.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion sources/Application/Model/Project.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading