Generative audio sequencer & WAV file export using thi.ng/dsp
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (c) 2022 Karsten Schmidt | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in all | |
* copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
* SOFTWARE. | |
*/ | |
import { download } from "@thi.ng/dl-asset"; | |
import { | |
adsr, | |
ADSR, | |
AGen, | |
biquadLP, | |
Delay, | |
IGen, | |
IProc, | |
Osc, | |
osc, | |
pipe, | |
product, | |
saw, | |
sin, | |
SVF, | |
svfLP, | |
} from "@thi.ng/dsp"; | |
import { wavByteArray } from "@thi.ng/dsp-io-wav"; | |
import { clamp01, clamp11 } from "@thi.ng/math"; | |
import { pickRandom, SYSTEM } from "@thi.ng/random"; | |
import { | |
comp, | |
cycle, | |
mapIndexed, | |
push, | |
take, | |
transduce, | |
} from "@thi.ng/transducers"; | |
const FS = 48000; | |
interface Voice { | |
/** | |
* Oscillator (w/ arbitrary waveform) | |
*/ | |
osc: Osc; | |
/** | |
* Volume envelope | |
*/ | |
env: ADSR; | |
/** | |
* Filter (or effect in general) | |
*/ | |
filter: SVF; | |
/** | |
* Combined generator (possibly with effects) | |
*/ | |
gen: IGen<number>; | |
} | |
// Major scale (12 tone equal temperament) | |
// https://en.wikipedia.org/wiki/Equal_temperament | |
const SCALE = [0, 2, 4, 5, 7, 9, 11]; | |
/** | |
* Convert semitone from scale to absolute frequency (in Hz). | |
* | |
* @param semiTone | |
* @param transpose | |
* @param scale | |
*/ | |
const freqForTone = (semiTone: number, transpose = 0, scale = SCALE) => { | |
const limit = scale.length; | |
let octave = Math.floor(semiTone / limit); | |
semiTone %= limit; | |
if (semiTone < 0) { | |
semiTone += limit; | |
octave--; | |
} | |
return Math.pow(2, (octave * 12 + scale[semiTone] + transpose) / 12.0); | |
}; | |
/** | |
* Creates & initializes a voice (aka oscillator + envelope) for given frequency | |
* (in Hz). Initial volume is set to zero. | |
* | |
* @param freq | |
* @param maxGain | |
*/ | |
const defVoice = (freq: number, maxGain: number): Voice => { | |
// define oscillator function | |
const voiceOsc = osc(saw, freq / FS, maxGain); | |
// define volume envelope | |
// https://en.wikipedia.org/wiki/Envelope_(music)#ADSR | |
const env = adsr({ | |
a: FS * 0.01, | |
d: FS * 0.05, | |
s: 0.8, | |
slen: 0, | |
r: FS * 0.5, | |
}); | |
// turn down volume until activated | |
env.setGain(0); | |
// lowpass filter | |
const filter = svfLP(1000 / FS); | |
return { | |
osc: voiceOsc, | |
env, | |
filter, | |
// multiply osc with envelope | |
// then pipe through filter & feedback delay (w/ its own filter)... | |
gen: pipe( | |
product(voiceOsc, env), | |
filter, | |
new FilterFeedbackDelay(FS * pickRandom([0.25, 0.375, 0.5])) | |
), | |
}; | |
}; | |
/** | |
* This class will be part of next @thi.ng/dsp release... (i.e. this is a | |
* variation of the existing FeedbackDelay proc, but with additional filter/proc | |
* possibility for the feedback itself) | |
*/ | |
export class FilterFeedbackDelay extends Delay<number> { | |
constructor( | |
n: number, | |
public filter: IProc<number, number> = biquadLP(1000 / FS, 1.1), | |
protected _feedback = 0.8 | |
) { | |
super(n, 0); | |
this.setFeedback(_feedback); | |
} | |
next(x: number) { | |
return super.next( | |
x + this.filter.next(this._buf[this._rpos] * this._feedback) | |
); | |
} | |
feedback() { | |
return this._feedback; | |
} | |
setFeedback(feedback: number) { | |
this._feedback = clamp01(feedback); | |
} | |
} | |
/** | |
* Random polyphonic sequencer | |
*/ | |
class Sequencer extends AGen<number> { | |
voices: Voice[]; | |
duration: Osc; | |
constructor(baseOctave = 6, numOctaves = 3) { | |
super(0); | |
const numNotes = SCALE.length; | |
const noteRange = numOctaves * numNotes; | |
const maxGain = 3 / noteRange; | |
this.voices = transduce( | |
comp( | |
take(noteRange), | |
mapIndexed((i, tone) => | |
defVoice( | |
freqForTone(tone, Math.floor(baseOctave + i / 12) * 12), | |
maxGain | |
) | |
) | |
), | |
push(), | |
cycle(SCALE) | |
); | |
// LFO for modulating attack length | |
this.duration = osc(sin, 0.1 / FS, 0.15 * FS, 0.15 * FS); | |
} | |
next() { | |
const dur = this.duration.next(); | |
// only tiny chance of new note/voice trigger per frame | |
// (`4 / FS` means statistically 4 triggers per second) | |
if (SYSTEM.float() < 4 / FS) { | |
// choose random voice | |
const voice = pickRandom(this.voices); | |
// reset envelope & set attack time | |
voice.env.reset(); | |
voice.env.setAttack(dur); | |
voice.env.setGain(SYSTEM.minmax(0.2, 1)); | |
// pick random cutoff freq & resonance for voice's filter | |
voice.filter.setFreq(SYSTEM.minmax(200, 8000) / FS); | |
voice.filter.setQ(SYSTEM.minmax(0.5, 0.95)); | |
} | |
// mixdown of all voices (ensure [-1..1] interval) | |
return this.voices.reduce( | |
(acc, voice) => clamp11(acc + voice.gen.next()), | |
0 | |
); | |
} | |
} | |
// generate a few seconds of audio & save as WAV file | |
download( | |
`seq-${Date.now()}.wav`, | |
// render audio into byte array for output | |
wavByteArray( | |
// wav format configuration | |
{ sampleRate: FS, channels: 1, length: FS * 60, bits: 16 }, | |
// stream producer | |
new Sequencer(6, 5) | |
) | |
); | |
// view waveform: https://audiotoolset.com/editor |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment