Skip to content

Instantly share code, notes, and snippets.

@sloev
Created August 14, 2025 10:20
Show Gist options
  • Save sloev/1ec025fe22e354fb1df7a7c32c591a0e to your computer and use it in GitHub Desktop.
Save sloev/1ec025fe22e354fb1df7a7c32c591a0e to your computer and use it in GitHub Desktop.
// paulstretch.js
// Copyright (C) 2014 Sébastien Piquemal
// Copyright (C) 2006-2011 Nasca Octavian Paul
import * as utils from './utils.js'
import * as blockHelpers from './block-helpers.js'
import * as arrayHelpers from './array-helpers.js'
export class PaulStretch {
constructor(numberOfChannels, ratio, winSize) {
this.numberOfChannels = numberOfChannels;
this.ratio = ratio
this.winSize = winSize || 4096 * 4
this.halfWinSize = this.winSize / 2
this.samplesIn = new utils.Samples()
this.samplesOut = new utils.Samples()
this.setRatio(this.ratio)
this.blockIn = blockHelpers.newBlock(this.numberOfChannels, this.winSize)
this.blockOut = blockHelpers.newBlock(this.numberOfChannels, this.winSize)
this.winArray = utils.createWindow(this.winSize)
this.phaseArray = new Float32Array(this.halfWinSize + 1)
this.rephase = utils.makeRephaser(this.winSize)
}
// Sets the stretch ratio. Note that blocks that have already been processed are using the old ratio.
setRatio(val) {
this.ratio = val
this.samplesIn.setDisplacePos((this.winSize * 0.5) / this.ratio)
}
// Returns the number of frames waiting to be processed
writeQueueLength() { return this.samplesIn.getFramesAvailable() }
// Returns the number of frames already processed
readQueueLength() { return this.samplesOut.getFramesAvailable() }
// Reads processed samples to `block`. Returns `block`, or `null` if there wasn't enough processed frames.
read(block) {
return this.samplesOut.read(block)
}
// Pushes `block` to the processing queue. Beware! The block is not copied, so make sure not to modify it afterwards.
write(block) {
this.samplesIn.write(block)
}
// Process samples from the queue. Returns the number of processed frames that were generated
process() {
// Read a block to blockIn
if (this.samplesIn.read(this.blockIn) === null) return 0
// get the windowed buffer
utils.applyWindow(this.blockIn, this.winArray)
// Randomize phases for each channel
for (let ch = 0; ch < this.numberOfChannels; ch++) {
arrayHelpers.map(this.phaseArray, function () { return Math.random() * 2 * Math.PI })
this.rephase(this.blockIn[ch], this.phaseArray)
}
// overlap-add the output
utils.applyWindow(this.blockIn, this.winArray)
for (let ch = 0; ch < this.numberOfChannels; ch++) {
arrayHelpers.add(
this.blockIn[ch].subarray(0, this.halfWinSize),
this.blockOut[ch].subarray(this.halfWinSize, this.winSize)
)
}
// Generate the output
this.blockOut = this.blockIn.map(function (chArray) { return arrayHelpers.duplicate(chArray) },this)
this.samplesOut.write(this.blockOut.map(function (chArray) { return chArray.subarray(0, this.halfWinSize) }, this))
return this.halfWinSize
}
toString() {
return 'PaulStretch(' + numberOfChannels + 'X' + this.winSize + ')'
}
}
export default PaulStretch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment