Skip to content

Instantly share code, notes, and snippets.

@djfm
Created November 5, 2019 15:03
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 djfm/44fee85efc48b96ca66299a4dc58a293 to your computer and use it in GitHub Desktop.
Save djfm/44fee85efc48b96ca66299a4dc58a293 to your computer and use it in GitHub Desktop.
const scale = {
c: 0,
'c#': 1,
d: 2,
'd#': 3,
e: 4,
f: 5,
'f#': 6,
g: 7,
'g#': 8,
a: 9,
'a#': 10,
b: 11
}
const freqs = [16.35, 17.32, 18.35, 19.45, 20.60, 21.83, 23.12, 24.50, 25.96, 27.50, 29.14, 30.87]
const baseFreq = note => {
const m = /^([a-g]#?)/.exec(note)
return freqs[scale[m[1]]]
}
const octaveMod = note => {
const m = /([+-]?\d+)$/.exec(note)
if(m) {
return +m[1]
}
return 0
}
const adsr = (
attack = 0.2,
release = 0.2
) => fn => (t, d) => {
if (t < attack * d) {
return t / (attack * d) * fn(t, d)
}
if (t > d * (1 - release)) {
return (1 - (t - d * (1 - release)) / (d * release)) * fn(t, d)
}
return fn(t, d)
}
const sin = (note, octave = 4) => adsr()((t, d) =>
Math.sin(
2 * Math.PI *
t * baseFreq(note) *
(2 ** (octave + octaveMod(note)))
))
const msin = (note, octave = 4) => (t, d) => {
let sample = 0;
for (let i = 0; i <= octave; i += 1) {
sample += sin(note, octave - i)(t, d) / (i + 1)
}
return 3 * sample / (octave + 1)
}
const mix = (...fns) => (t, d) =>
fns.reduce(
(sum, fn) => sum + fn(t, d),
0
) / fns.length
const seq = duration => (...fns) => t => {
const offset = Math.floor(t / duration * fns.length)
return fns[offset](t - offset * duration / fns.length, duration / fns.length)
}
const rep = (duration, times) => fn =>
t => fn(t - Math.floor(t / duration * times) * duration / times, duration / times)
const rhythm = r => fn => (t, d) => {
if (t < 0) {
t = 0
}
const beats = r.split(/\s+/)
const bd = d / beats.length
const b = Math.floor(t / bd)
const tb = t - b * bd
const beat = beats[b]
const tick = Math.floor(tb / bd * beat.length)
const tl = bd / beat.length
if(beat[tick] === 'x') {
return 0
}
let sa = 0
while(tick - sa > 0 && beat[tick - sa] === 'o') {
sa += 1
}
let sb = 1
while(tick + sb < beat.length && beat[tick + sb] === 'o') {
sb += 1
}
return fn(tb - (tick - sa) * tl, tl * (sa + sb))
}
const lowpass = freq => fn => {
let lastVal = 0
let lt = 0
let first = true
return t => {
if (first) {
lastVal = fn(t)
lt = t
first = false
return lastVal
}
const dt = t - lt
const rc = 1 / freq / 2 / Math.PI
const alpha = dt / (rc + dt)
lt = t
lastVal = lastVal + alpha * (fn(t) - lastVal)
return lastVal
}
}
const rbar = rhythm('tot ttt xto ttt')
const Am = mix(
msin('a'),
msin('c1'),
msin('e1')
)
const F = mix(
msin('f'),
msin('a'),
msin('c1')
)
const C = mix(
msin('c'),
msin('e'),
msin('g')
)
const G = mix(
msin('g'),
msin('d1'),
msin('b1')
)
const verseDuration = 16
const nVerses = 2
const duration = verseDuration * nVerses
const verseA = seq(verseDuration)(...[
Am, F, C, G
].map(rhythm('t t t t')))
const verseB = seq(verseDuration)(...[
sin('c1'), sin('e1'), sin('a1'),
sin('a1'), sin('f1'), sin('c1'),
sin('c1'), sin('e1'), sin('g1'),
sin('g1'), sin('d1'), sin('b1')
].map(rbar))
const bass = seq(verseDuration)(
sin('a'), sin('f'), sin('c'), sin('g')
)
const verse = mix(verseA, verseB, bass)
const song = lowpass(220)(
rep(duration, nVerses)(verse)
)
const ctx = new window.AudioContext()
const osc = ctx.createOscillator()
const buf = ctx.createBuffer(
2,
ctx.sampleRate * duration,
ctx.sampleRate
)
for(let c = 0; c < 2; c += 1) {
const data = buf.getChannelData(c)
for (let s = 0; s < data.length; s += 1) {
data[s] = song(s / ctx.sampleRate)
}
}
const src = ctx.createBufferSource()
src.buffer = buf
src.connect(ctx.destination)
src.start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment