diff --git a/CHANGELOG b/CHANGELOG index b1d0494d..4a0a0ebb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +1.6.0-bacon10 + Allow greater granular stretching using LPOF + Documentation in tips and tricks 1.6.0-bacon5 Auto-load last project on startup * Application can now load the last project automatically on startup diff --git a/docs/wiki/What-is-LittlePiggyTracker.md b/docs/wiki/What-is-LittlePiggyTracker.md index c9494964..b05c0e7a 100644 --- a/docs/wiki/What-is-LittlePiggyTracker.md +++ b/docs/wiki/What-is-LittlePiggyTracker.md @@ -391,7 +391,7 @@ In oscillator modes, under 0x80 the feedback of specified length is added to the - pingpong will start at "start" and bounce the loop between loop start and loop end. - oscillator is a special mode where the loop selection (from loop start to loop end) is taken as oscillator data and automatically tuned. Experiment with different settings, do not forget 'root note' is your friend to tune the oscillator back in a useful range - looper sync will automatically tune a loop so that it plays exactly 16 bars. Use the root note to play twice faster/slower - - slicer will cut the sample into "slices" amount of samples, mapped from C-2 (the lowest possible note) up to amount of slices. Example: slices == 4 will give you four slices mapped to C-2, C#-2, D-2, D#-2 +- **slices:** divides the sample into equal slices, mapped chromatically from C-2 upward. Set to 1 to disable. Example: slices == 4 gives four slices on C-2, C#-2, D-2, D#-2. When slices > 1, the active loop mode applies within each slice — use "loop" for looped slices, "none" for one-shot. - **start:** start point of the sample regardless of if loop is enabled; in hex - **loop Start:** start point of the sample when loop is enabled; in hex - **loop End:** end point of the sample; in hex. You can play samples backwards by setting the end value lower than the start! diff --git a/docs/wiki/tips_and_tricks.md b/docs/wiki/tips_and_tricks.md index 15d84b16..8699ac18 100644 --- a/docs/wiki/tips_and_tricks.md +++ b/docs/wiki/tips_and_tricks.md @@ -107,6 +107,47 @@ Using this, you can assign up to 256 individual slices from C-2 (C minus two) up Of course, you are not limited to drum loops. Just chop anything away ! The example project BETA uses Slice mode to chop up a dank AF sample by Basscarrier +### Granular timestretching with LPOF + +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 ---- ---- +``` + +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. + +**Sync to BPM (V_sync)** + +To advance the window in lock-step with the song tempo, you want the total shift per beat to equal the sample length. With 6 ticks per row (default), the per-tick shift is: + +``` +V_sync = (fileSampleRate × 10) / BPM +``` + +This gives the `LPOF` value (in decimal; convert to hex to enter it) that moves the window in sync with your BPM. + +**Reference table — V_sync values** + +| BPM | 44100 Hz | 22050 Hz | 11025 Hz | 8000 Hz | +|-----|----------|----------|----------|---------| +| 90 | `1388` (5000) | `09C4` (2500) | `04E2` (1250) | `038E` (910) | +| 160 | `0AC4` (2756) | `0562` (1378) | `02B1` (689) | `01F4` (500) | +| 175 | `09D8` (2520) | `04EC` (1260) | `0276` (630) | `01C7` (455) | + +To stretch to *half speed* (two bars to traverse), halve the V_sync value. To double speed, double it. + +**Speeding up vs. slowing down** + +- **Speeding up** (forward `LPOF` values `0001`–`7FFF`): The window travels through the sample faster than realtime. Once it reaches the end it clamps there — no artefacts. +- **Slowing down**: Use a smaller V_sync value. The loop replays more cycles of each grain, effectively stretching time. Works best with long source samples. +- **Backward scrolling**: Values `8001`–`FFFF` shift the loop window backward (two's complement: `FFFF` = −1, `FF00` = −256, etc.). + +**Amen break example (160 BPM, 44100 Hz)** + +The classic Amen break is ~136 BPM. To play it in sync at 160 BPM, use `LPOF 0CB4` (3252 samples/tick). The window advances ~18% faster than its natural rate, stretching it up to 160 BPM without retuning the sample. + #Autorun your Pig on GP2X Let's face it, playing live is a busy affair, and god help you if you have to reboot the gp2x for whatever reason. Luckily the GP2X allows you to drop a simple text file in the SD card root that will automatically boot any program. Here's how to get a piggy running [midi](./dfs_midi_cable.md) to Autorun. diff --git a/sources/Application/Instruments/SampleInstrument.cpp b/sources/Application/Instruments/SampleInstrument.cpp index 31954cbc..29beb92c 100644 --- a/sources/Application/Instruments/SampleInstrument.cpp +++ b/sources/Application/Instruments/SampleInstrument.cpp @@ -230,35 +230,37 @@ bool SampleInstrument::Start(int channel,unsigned char midinote,bool cleanstart) }*/ rp->reverse_=false ; - float driverRate=float(Audio::GetInstance()->GetSampleRate()) ; - - switch (loopmode) { - case SILM_ONESHOT: - case SILM_LOOP: - case SILM_LOOP_PINGPONG: - - // Compute speed factor - // if instrument sampled below 44.1Khz, should - // travel slower in sample - - rp->rendFirst_ = start_->GetInt(); - rp->position_= float(rp->rendFirst_); - rp->baseSpeed_=fl2fp(source_->GetSampleRate(rp->midiNote_)/driverRate) ; - rp->reverse_=(rp->rendLoopEnd_position_) ; - - break ; - - case SILM_OSC: -// case SILM_OSCFINE: - { - - float freq=261.6255653006f ; // C3 -/* if (loopmode==SILM_OSCFINE) { - freq=float(pow(2.0,-0.75))*440; // C3 - }*/ - int length=rp->rendLoopEnd_-rp->rendLoopStart_ ; - if (length==0) length=1 ; - if (length<0) { + float driverRate = float(Audio::GetInstance()->GetSampleRate()); + + switch (loopmode) { + case SILM_ONESHOT: + case SILM_LOOP: + case SILM_LOOP_PINGPONG: + + // Compute speed factor + // if instrument sampled below 44.1Khz, should + // travel slower in sample + + rp->rendFirst_ = start_->GetInt(); + rp->position_ = float(rp->rendFirst_); + rp->baseSpeed_ = + fl2fp(source_->GetSampleRate(rp->midiNote_) / driverRate); + rp->reverse_ = (rp->rendLoopEnd_ < rp->position_); + + break; + + case SILM_OSC: + // case SILM_OSCFINE: + { + + float freq = 261.6255653006f; // C3 + /* if (loopmode==SILM_OSCFINE) { + freq=float(pow(2.0,-0.75))*440; // C3 + }*/ + int length = rp->rendLoopEnd_ - rp->rendLoopStart_; + if (length == 0) + length = 1; + if (length < 0) { rp->reverse_=true ; length=-length ; } ; @@ -301,17 +303,15 @@ bool SampleInstrument::Start(int channel,unsigned char midinote,bool cleanstart) case SILM_LAST: NAssert(0) ; break ; - } - + } - - // Compute octave & note difference from root + // Compute octave & note difference from root - float fineTune=float(fineTune_->GetInt()-0x7F) ; - fineTune/=float(0x80) ; - int offset=midinote-rootNote ; - if (loopmode == SILM_SLICE) { - offset = rootNote-source_->GetRootNote(rp->midiNote_); + float fineTune = float(fineTune_->GetInt() - 0x7F); + fineTune /= float(0x80); + int offset = midinote - rootNote; + if (loopmode == SILM_SLICE) { + offset = rootNote - source_->GetRootNote(rp->midiNote_); } while (offset>127) { @@ -1097,41 +1097,51 @@ void SampleInstrument::ProcessCommand(int channel,FourCC cc,ushort value) { if (!source_) return ; switch(cc) { - case I_CMD_LPOF: - - if (value>0x8000) { - value=0x10000-value ; - if (rp->rendLoopStart_>value) { - rp->rendLoopEnd_-=value ; - rp->rendLoopStart_-=value ; - } - } else { - if (rp->rendLoopEnd_+valueGetSize(rp->midiNote_)) { - rp->rendLoopEnd_+=value ; - rp->rendLoopStart_+=value ; - } ; - } - break ; - - case I_CMD_PLOF: - { - if (!source_) return ; - int wavSize=source_->GetSize(rp->midiNote_) ; - float chkSize=wavSize/256.0f ; - int absShft=value>>8 ; - if (absShft!=0) { - rp->position_=chkSize*absShft ; - } ; - int relSfht=value&0xFF ; - if (relSfht>127) relSfht=relSfht-256 ; - rp->position_+=relSfht*chkSize ; - while (rp->position_<0) { - rp->position_=wavSize+rp->position_ ; - } ; - while (rp->position_>=wavSize) { - rp->position_-=wavSize; - } ; - rp->couldClick_=SHOULD_KILL_CLICKS ; + case I_CMD_LPOF: + if (value > 0x8000) { + int shift = (int)(0x10000 - value); + if (shift > + rp->rendLoopStart_) { // Clamp so to not back out of sample + 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 + int sampleSize = source_->GetSize(rp->midiNote_); + int shift = (int)value; + if (rp->rendLoopEnd_ + shift >= + sampleSize) { // Clamp so to not play outside sample + shift = sampleSize - rp->rendLoopEnd_; + } + if (shift > 0) { + rp->rendLoopEnd_ += shift; + rp->rendLoopStart_ += shift; + rp->position_ += shift; + } + } + break; + + case I_CMD_PLOF: { + if (!source_) + return; + int wavSize = source_->GetSize(rp->midiNote_); + float chkSize = wavSize / 256.0f; + int absShft = value >> 8; + if (absShft != 0) { + rp->position_ = chkSize * absShft; + }; + int relSfht = value & 0xFF; + if (relSfht > 127) + relSfht = relSfht - 256; + rp->position_ += relSfht * chkSize; + while (rp->position_ < 0) { + rp->position_ = wavSize + rp->position_; + }; + while (rp->position_ >= wavSize) { + rp->position_ -= wavSize; + }; + rp->couldClick_ = SHOULD_KILL_CLICKS; } break ; diff --git a/sources/Application/Model/Project.h b/sources/Application/Model/Project.h index 9d07dc70..5dea7d7b 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-bacon6" +#define BUILD_COUNT "0-bacon11" #define MAX_TAP 3