Skip to content

Instantly share code, notes, and snippets.

@postspectacular
Last active November 16, 2022 17:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save postspectacular/522784ca13259ef60002835c8fc281f5 to your computer and use it in GitHub Desktop.
Save postspectacular/522784ca13259ef60002835c8fc281f5 to your computer and use it in GitHub Desktop.
Generative audio sequencer & WAV file export using thi.ng/dsp
/*
* 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