// timestretch_example.ino
//
// Phase vocoder time-stretching — Teensy 4.x + Audio Shield (SGTL5000)
//
// Controls (Serial):
// SPACE start / restart playback
// s stop playback
// p faster (decrease stretch)
// q slower (increase stretch)
// 1-3 load and play 01.WAV / 02.WAV / 03.WAV
// t transient threshold: 4 (sensitive — more phase resets)
// y transient threshold: 8 (default)
// u transient threshold: 16 (subtle — fewer phase resets)
// d toggle profiling report
// h print help
//
// Optional: pot wiper → A0 (outer legs to 3.3 V and GND)
// Pot maps to stretch STRETCH_MIN … STRETCH_MAX
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include "effect_phaseVocoder.h"
// ---------------------------------------------------------------------------
// Audio graph
// ---------------------------------------------------------------------------
AudioEffectPhaseVocoder vocoder;
// AudioOutputUSB audioOut;
AudioOutputI2S audioOut;
AudioControlSGTL5000 codec;
AudioConnection patchLeft (vocoder, 0, audioOut, 0);
AudioConnection patchRight(vocoder, 0, audioOut, 1);
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
static const int POT_PIN = A0;
static const float STRETCH_MIN = 0.5f; // 0.5 = half duration (2× faster)
static const float STRETCH_MAX = 1.5f; // 1.5 = 1.5× duration (slower)
static const int POT_READ_MS = 50;
static const float CONTROL_STEP = 0.01f;
static const bool USE_POT = false;
float stretch = 1.0f;
float pitchSt = 0.0f; // pitch shift in semitones
bool profiling = false;
// RAM CODE
#define NUM_WAVS 16 // avoid silly mistakes
const char *SMP_WAV[NUM_WAVS] = { "A01.WAV", "A02.WAV", "A03.WAV", "A04.WAV", "05.WAV", "06.WAV", "07.WAV", "08.WAV", "09.WAV", "10.WAV", "11.WAV", "12.WAV", "13.WAV", "14.WAV", "15.WAV", "16.WAV" };
File x_File;
int16_t *SMP_addr[NUM_WAVS];
uint32_t sizes[NUM_WAVS];
// ---------------------------------------------------------------------------
// Sample buffer
// DMAMEM places in OCRAM (512 KB) instead of DTCM.
// On Teensy 4.1 with PSRAM: replace DMAMEM with EXTMEM for much larger buffers.
// ---------------------------------------------------------------------------
EXTMEM int16_t sampleBuffer[44100 * 5]; // DMAMEM
static uint32_t sampleCount = 0;
static const char *sampleFiles[] = { "A01.WAV", "A02.WAV", "A03.WAV" };
static int currentSample = 0;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static float clampf(float v, float lo, float hi) {
return v < lo ? lo : v > hi ? hi : v;
}
static void applyStretch(float newStretch) {
stretch = clampf(newStretch, STRETCH_MIN, STRETCH_MAX);
vocoder.setStretch(stretch);
float durationS = stretch * (sampleCount / (float)AUDIO_SAMPLE_RATE_EXACT);
Serial.print("x Speed: "); Serial.print(1.0f / stretch, 2);
Serial.print("x Duration: "); Serial.print(durationS, 1); Serial.println("s");
}
static void printHelp() {
Serial.println("--- Controls ---");
Serial.println(" SPACE start / restart");
Serial.println(" s stop");
Serial.println(" p faster");
Serial.println(" q slower");
Serial.println(" 1-3 load 01.WAV / 02.WAV / 03.WAV");
Serial.println(" w pitch up 1 semitone");
Serial.println(" x pitch down 1 semitone");
Serial.println(" z reset pitch to 0");
Serial.println(" t transient threshold: 4 (sensitive)");
Serial.println(" y transient threshold: 8 (default)");
Serial.println(" u transient threshold: 16 (subtle)");
Serial.println(" d toggle profiling");
Serial.println(" e print per-section costs");
Serial.println(" h this help");
}
// ---------------------------------------------------------------------------
// WAV loader — handles mono and stereo (stereo: left channel only).
// Reads channel count from WAV header offset 22.
// ---------------------------------------------------------------------------
static bool loadSampleFromSD(const char *filename) {
File f = SD.open(filename);
if (!f) {
Serial.print("Could not open: "); Serial.println(filename);
return false;
}
f.seek(22);
int chanLo = f.read();
int chanHi = f.read();
if (chanLo < 0 || chanHi < 0) { f.close(); return false; }
const uint16_t numChannels = (uint16_t)((chanHi << 8) | chanLo);
f.seek(44); // skip 44-byte WAV header to raw PCM data
sampleCount = 0;
const uint32_t maxSamples = sizeof(sampleBuffer) / sizeof(sampleBuffer[0]);
while (f.available() && sampleCount < maxSamples) {
int lo = f.read();
int hi = f.read();
if (lo < 0 || hi < 0) break;
sampleBuffer[sampleCount++] = (int16_t)((hi << 8) | lo);
if (numChannels == 2) {
// skip right channel sample
if (f.read() < 0 || f.read() < 0) break;
}
}
f.close();
Serial.print("Loaded: "); Serial.print(filename);
Serial.print(" samples: "); Serial.print(sampleCount);
Serial.print(" ch: "); Serial.print(numChannels);
Serial.print(" dur: "); Serial.print(sampleCount / (float)AUDIO_SAMPLE_RATE_EXACT, 1);
Serial.println("s");
return sampleCount > 0;
}
static void loadAndPlay(int index) {
currentSample = index;
vocoder.stop();
if (loadSampleFromSD(sampleFiles[index])) {
// vocoder.setSample(sampleBuffer, sampleCount);
vocoder.setSample(SMP_addr[3], sizes[3]/2); //
vocoder.setLoop(true);
applyStretch(stretch);
vocoder.play();
}
}
// ---------------------------------------------------------------------------
// setup
// ---------------------------------------------------------------------------
void setup() {
AudioMemory(40);
Serial.begin(57600);
while (!Serial && millis() < 2000) {}
codec.enable();
codec.volume(0.5f);
if (!SD.begin(BUILTIN_SDCARD)) {
Serial.println("SD init failed");
return;
}
vocoder.setTransientThreshold(16.0f);
RAM_LOAD ();
loadAndPlay(0);
printHelp();
}
// ---------------------------------------------------------------------------
// loop
// ---------------------------------------------------------------------------
void loop() {
static uint32_t lastPotRead = 0;
static uint32_t lastProf = 0;
if (Serial.available() > 0) {
char key = Serial.read();
if (key == ' ') { vocoder.stop(); vocoder.play(); Serial.println("Playing"); }
else if (key == 's') { vocoder.stop(); Serial.println("Stopped"); }
else if (key == 'p') applyStretch(stretch - CONTROL_STEP); // faster
else if (key == 'q') applyStretch(stretch + CONTROL_STEP); // slower
else if (key >= '1' && key <= '3') loadAndPlay(key - '1');
else if (key == 'w') { pitchSt += 1.0f; vocoder.setPitchShift(pitchSt); Serial.print("Pitch: "); Serial.print(pitchSt, 0); Serial.println(" st"); }
else if (key == 'x') { pitchSt -= 1.0f; vocoder.setPitchShift(pitchSt); Serial.print("Pitch: "); Serial.print(pitchSt, 0); Serial.println(" st"); }
else if (key == 'z') { pitchSt = 0.0f; vocoder.setPitchShift(pitchSt); Serial.println("Pitch: 0 st"); }
else if (key == 't') { vocoder.setTransientThreshold(4.0f); Serial.println("Transient threshold: 4"); }
else if (key == 'y') { vocoder.setTransientThreshold(8.0f); Serial.println("Transient threshold: 8"); }
else if (key == 'u') { vocoder.setTransientThreshold(16.0f); Serial.println("Transient threshold: 16"); }
else if (key == 'd') { profiling = !profiling; Serial.println(profiling ? "Profiling: on" : "Profiling: off"); }
else if (key == 'e') {
float tW, tF, tA, tS, tI, tO;
if (vocoder.getProfilingDetailed(tW, tF, tA, tS, tI, tO)) {
Serial.println("--- Section costs (mean µs) ---");
Serial.print(" window fill: "); Serial.println(tW, 1);
Serial.print(" forward FFT: "); Serial.println(tF, 1);
Serial.print(" phase analysis: "); Serial.println(tA, 1);
Serial.print(" phase synthesis: "); Serial.println(tS, 1);
Serial.print(" inverse FFT: "); Serial.println(tI, 1);
Serial.print(" OLA + output: "); Serial.println(tO, 1);
}
}
else if (key == 'h') printHelp();
}
// Profiling report every 2 seconds
if (profiling && millis() - lastProf >= 2000) {
lastProf = millis();
float meanUs;
uint32_t peakUs;
if (vocoder.getProfiling(meanUs, peakUs)) {
const float budgetUs = 1e6f / (AUDIO_SAMPLE_RATE_EXACT / (float)AUDIO_BLOCK_SAMPLES);
Serial.print("update() mean: "); Serial.print(meanUs, 1);
Serial.print(" us peak: "); Serial.print(peakUs);
Serial.print(" us load: "); Serial.print(meanUs / budgetUs * 100.0f, 1);
Serial.println("%");
}
}
if (USE_POT && millis() - lastPotRead >= POT_READ_MS) {
lastPotRead = millis();
int raw = analogRead(POT_PIN);
applyStretch(STRETCH_MIN + (raw / 1023.0f) * (STRETCH_MAX - STRETCH_MIN));
}
}
///=========================================
void RAM_LOAD () {
int SMP_FILE_NUM=0;
for (int i = 0; i < NUM_WAVS; i++) {
x_File = SD.open(SMP_WAV[i], FILE_READ);
if (x_File)
{
sizes[i] = x_File.size();
SMP_addr[i] = (int16_t*) extmem_malloc(sizes[i]);
if (nullptr == SMP_addr[i])
Serial.printf("Failed to allocate %d in EXTMEM for %s\n", sizes[i], SMP_WAV[i]);
else {
if (sizes[i] != x_File.read(SMP_addr[i], sizes[i]))
{
Serial.printf("Failed to read in %s - wrong length\n", SMP_WAV[i]);
extmem_free(SMP_addr[i]); // free memory
SMP_addr[i] = nullptr; // mark as "not loaded"
}
else
Serial.printf("Read %s into memory at %08X; %d bytes\n", SMP_WAV[i], SMP_addr[i], sizes[i]);
}
x_File.close();
}
else
Serial.printf("Failed to open %s\n", SMP_WAV[i]);
}
}