From aa094bf9309ccef568b1d34726aaf878437df802 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 29 Sep 2023 10:40:00 +0200 Subject: [PATCH 01/48] checkbox + number slider --- packages/codemirror/checkbox.mjs | 87 ++++++++++++ packages/codemirror/slider.mjs | 129 ++++++++++++++++++ packages/react/src/components/CodeMirror6.jsx | 4 +- 3 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 packages/codemirror/checkbox.mjs create mode 100644 packages/codemirror/slider.mjs diff --git a/packages/codemirror/checkbox.mjs b/packages/codemirror/checkbox.mjs new file mode 100644 index 00000000..279e2eb8 --- /dev/null +++ b/packages/codemirror/checkbox.mjs @@ -0,0 +1,87 @@ +import { WidgetType } from '@codemirror/view'; +import { ViewPlugin, Decoration } from '@codemirror/view'; +import { syntaxTree } from '@codemirror/language'; + +export class CheckboxWidget extends WidgetType { + constructor(checked) { + super(); + this.checked = checked; + } + + eq(other) { + return other.checked == this.checked; + } + + toDOM() { + let wrap = document.createElement('span'); + wrap.setAttribute('aria-hidden', 'true'); + wrap.className = 'cm-boolean-toggle'; + let box = wrap.appendChild(document.createElement('input')); + box.type = 'checkbox'; + box.checked = this.checked; + return wrap; + } + + ignoreEvent() { + return false; + } +} + +// EditorView +export function checkboxes(view) { + let widgets = []; + for (let { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: (node) => { + if (node.name == 'BooleanLiteral') { + let isTrue = view.state.doc.sliceString(node.from, node.to) == 'true'; + let deco = Decoration.widget({ + widget: new CheckboxWidget(isTrue), + side: 1, + }); + widgets.push(deco.range(node.from)); + } + }, + }); + } + return Decoration.set(widgets); +} + +export const checkboxPlugin = ViewPlugin.fromClass( + class { + decorations; //: DecorationSet + + constructor(view /* : EditorView */) { + this.decorations = checkboxes(view); + } + + update(update /* : ViewUpdate */) { + if (update.docChanged || update.viewportChanged) this.decorations = checkboxes(update.view); + } + }, + { + decorations: (v) => v.decorations, + + eventHandlers: { + mousedown: (e, view) => { + let target = e.target; /* as HTMLElement */ + if (target.nodeName == 'INPUT' && target.parentElement.classList.contains('cm-boolean-toggle')) + return toggleBoolean(view, view.posAtDOM(target)); + }, + }, + }, +); + +function toggleBoolean(view /* : EditorView */, pos /* : number */) { + let before = view.state.doc.sliceString(Math.max(0, pos), pos + 5).trim(); + let change; + if (!['true', 'false'].includes(before)) { + return false; + } + let insert = before === 'true' ? 'false' : 'true'; + change = { from: pos, to: pos + before.length, insert }; + view.dispatch({ changes: change }); + return true; +} diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs new file mode 100644 index 00000000..f03dba18 --- /dev/null +++ b/packages/codemirror/slider.mjs @@ -0,0 +1,129 @@ +import { WidgetType } from '@codemirror/view'; +import { ViewPlugin, Decoration } from '@codemirror/view'; +import { syntaxTree } from '@codemirror/language'; + +export class SliderWidget extends WidgetType { + constructor(value, min, max, from, to) { + super(); + this.value = value; + this.min = min; + this.max = max; + this.from = from; + this.to = to; + } + + eq(other) { + const isSame = other.value.toFixed(4) == this.value.toFixed(4); + return isSame; + } + + toDOM() { + let wrap = document.createElement('span'); + wrap.setAttribute('aria-hidden', 'true'); + wrap.className = 'cm-slider'; // inline-flex items-center + let slider = wrap.appendChild(document.createElement('input')); + slider.type = 'range'; + slider.min = this.min; + slider.max = this.max; + slider.step = (this.max - this.min) / 1000; + slider.value = this.value; + slider.from = this.from; + slider.to = this.to; + slider.className = 'w-16'; + return wrap; + } + + ignoreEvent() { + return false; + } +} + +// EditorView +export function sliders(view) { + let widgets = []; + for (let { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: (node) => { + if (node.name == 'Number') { + let value = view.state.doc.sliceString(node.from, node.to); + value = Number(value); + /* let min = Math.min(0, value); + let max = Math.max(value, 1); */ + let min = 0; + let max = 10; + //console.log('from', node.from, 'to', node.to); + let deco = Decoration.widget({ + widget: new SliderWidget(value, min, max, node.from, node.to), + side: 1, + }); + widgets.push(deco.range(node.from)); + } + }, + }); + } + return Decoration.set(widgets); +} + +let draggedSlider, init; +export const sliderPlugin = ViewPlugin.fromClass( + class { + decorations; //: DecorationSet + + constructor(view /* : EditorView */) { + this.decorations = sliders(view); + } + + update(update /* : ViewUpdate */) { + if (update.docChanged || update.viewportChanged) { + !init && (this.decorations = sliders(update.view)); + //init = true; + } + } + }, + { + decorations: (v) => v.decorations, + + eventHandlers: { + mousedown: (e, view) => { + let target = e.target; /* as HTMLElement */ + if (target.nodeName == 'INPUT' && target.parentElement.classList.contains('cm-slider')) { + draggedSlider = target; + // remember offsetLeft / clientWidth, as they will vanish inside mousemove events for some reason + draggedSlider._offsetLeft = draggedSlider.offsetLeft; + draggedSlider._clientWidth = draggedSlider.clientWidth; + return updateSliderValue(view, e); + } + }, + mouseup: () => { + draggedSlider = undefined; + }, + mousemove: (e, view) => { + draggedSlider && updateSliderValue(view, e); + }, + }, + }, +); + +function updateSliderValue(view, e) { + const mouseX = e.clientX; + let progress = (mouseX - draggedSlider._offsetLeft) / draggedSlider._clientWidth; + progress = Math.max(Math.min(1, progress), 0); + let min = Number(draggedSlider.min); + let max = Number(draggedSlider.max); + const next = Number(progress * (max - min) + min); + let insert = next.toFixed(2); + let before = view.state.doc.sliceString(draggedSlider.from, draggedSlider.to).trim(); + before = Number(before).toFixed(4); + if (before === next) { + return false; + } + //console.log('before', before, '->', insert); + let change = { from: draggedSlider.from, to: draggedSlider.to, insert }; + draggedSlider.to = draggedSlider.from + insert.length; + //console.log('change', change); + view.dispatch({ changes: change }); + + return true; +} diff --git a/packages/react/src/components/CodeMirror6.jsx b/packages/react/src/components/CodeMirror6.jsx index 0f1b2274..f3c764e0 100644 --- a/packages/react/src/components/CodeMirror6.jsx +++ b/packages/react/src/components/CodeMirror6.jsx @@ -15,10 +15,12 @@ import { updateMiniLocations, } from '@strudel/codemirror'; import './style.css'; +import { checkboxPlugin } from '@strudel/codemirror/checkbox.mjs'; +import { sliderPlugin } from '@strudel/codemirror/slider.mjs'; export { flash, highlightMiniLocations, updateMiniLocations }; -const staticExtensions = [javascript(), flashField, highlightExtension]; +const staticExtensions = [javascript(), flashField, highlightExtension, checkboxPlugin, sliderPlugin]; export default function CodeMirror({ value, From c93e4a951a003a21431e8cc9388968bc55dc68e7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 29 Sep 2023 12:58:16 +0200 Subject: [PATCH 02/48] match number.slider --- packages/codemirror/slider.mjs | 57 ++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index f03dba18..423d3651 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -38,6 +38,43 @@ export class SliderWidget extends WidgetType { } } +let nodeValue = (node, view) => view.state.doc.sliceString(node.from, node.to); + +// matches a number and returns slider widget +/* let matchNumber = (node, view) => { + if (node.name == 'Number') { + const value = view.state.doc.sliceString(node.from, node.to); + let min = 0; + let max = 10; + return Decoration.widget({ + widget: new SliderWidget(Number(value), min, max, node.from, node.to), + side: 0, + }); + } +}; */ +// matches something like 123.xxx and returns slider widget +let matchNumberSlider = (node, view) => { + if ( + node.name === 'MemberExpression' && + node.node.firstChild.name === 'Number' && + node.node.lastChild.name === 'PropertyName' + ) { + // node is sth like 123.xxx + let prop = nodeValue(node.node.lastChild, view); // get prop name (e.g. xxx) + if (prop === 'slider') { + let value = nodeValue(node.node.firstChild, view); // get number (e.g. 123) + // console.log('slider value', value); + let { from, to } = node.node.firstChild; + let min = 0; + let max = 10; + return Decoration.widget({ + widget: new SliderWidget(Number(value), min, max, from, to), + side: 0, + }); + } + } +}; + // EditorView export function sliders(view) { let widgets = []; @@ -46,20 +83,14 @@ export function sliders(view) { from, to, enter: (node) => { - if (node.name == 'Number') { - let value = view.state.doc.sliceString(node.from, node.to); - value = Number(value); - /* let min = Math.min(0, value); - let max = Math.max(value, 1); */ - let min = 0; - let max = 10; - //console.log('from', node.from, 'to', node.to); - let deco = Decoration.widget({ - widget: new SliderWidget(value, min, max, node.from, node.to), - side: 1, - }); - widgets.push(deco.range(node.from)); + let numberSlider = matchNumberSlider(node, view); + if (numberSlider) { + widgets.push(numberSlider.range(node.from)); } + /* let number = matchNumber(node, view); + if (number) { + widgets.push(number.range(node.from)); + } */ }, }); } From c2481e460b5c455662999fd61a8d2b9ec1ffb22c Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sat, 30 Sep 2023 14:07:33 +0200 Subject: [PATCH 03/48] Add pink, white and brown oscillators --- packages/superdough/synth.mjs | 282 ++++++++++++++++++++-------------- 1 file changed, 167 insertions(+), 115 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 633d0113..3ab3720f 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -20,100 +20,102 @@ const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => { return mod(modfreq, modgain, wave); }; + export function registerSynthSounds() { - ['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => { - registerSound( - wave, - (t, value, onended) => { - // destructure adsr here, because the default should be different for synths and samples - let { - attack = 0.001, - decay = 0.05, - sustain = 0.6, - release = 0.01, - fmh: fmHarmonicity = 1, - fmi: fmModulationIndex, - fmenv: fmEnvelopeType = 'lin', - fmattack: fmAttack, - fmdecay: fmDecay, - fmsustain: fmSustain, - fmrelease: fmRelease, - fmvelocity: fmVelocity, - fmwave: fmWaveform = 'sine', - vib = 0, - vibmod = 0.5, - } = value; - let { n, note, freq } = value; - // with synths, n and note are the same thing - note = note || 36; - if (typeof note === 'string') { - note = noteToMidi(note); // e.g. c3 => 48 - } - // get frequency - if (!freq && typeof note === 'number') { - freq = midiToFreq(note); // + 48); - } - // maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default) - // make oscillator - const { node: o, stop } = getOscillator({ - t, - s: wave, - freq, - vib, - vibmod, - partials: n, - }); - - // FM + FM envelope - let stopFm, fmEnvelope; - if (fmModulationIndex) { - const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform); - if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) { - // no envelope by default - modulator.connect(o.frequency); - } else { - fmAttack = fmAttack ?? 0.001; - fmDecay = fmDecay ?? 0.001; - fmSustain = fmSustain ?? 1; - fmRelease = fmRelease ?? 0.001; - fmVelocity = fmVelocity ?? 1; - fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t); - if (fmEnvelopeType === 'exp') { - fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t); - fmEnvelope.node.maxValue = fmModulationIndex * 2; - fmEnvelope.node.minValue = 0.00001; - } - modulator.connect(fmEnvelope.node); - fmEnvelope.node.connect(o.frequency); + ['sine', 'square', 'triangle', + 'sawtooth', 'pink', 'white', + 'brown'].forEach((wave) => { + registerSound( + wave, + (t, value, onended) => { + // destructure adsr here, because the default should be different for synths and samples + let { + attack = 0.001, + decay = 0.05, + sustain = 0.6, + release = 0.01, + fmh: fmHarmonicity = 1, + fmi: fmModulationIndex, + fmenv: fmEnvelopeType = 'lin', + fmattack: fmAttack, + fmdecay: fmDecay, + fmsustain: fmSustain, + fmrelease: fmRelease, + fmvelocity: fmVelocity, + fmwave: fmWaveform = 'sine', + vib = 0, + vibmod = 0.5, + } = value; + let { n, note, freq } = value; + // with synths, n and note are the same thing + note = note || 36; + if (typeof note === 'string') { + note = noteToMidi(note); // e.g. c3 => 48 + } + // get frequency + if (!freq && typeof note === 'number') { + freq = midiToFreq(note); // + 48); + } + // maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default) + // make oscillator + const { node: o, stop } = getOscillator({ + t, + s: wave, + freq, + vib, + vibmod, + partials: n, + }); + // FM + FM envelope + let stopFm, fmEnvelope; + if (fmModulationIndex) { + const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform); + if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) { + // no envelope by default + modulator.connect(o.frequency); + } else { + fmAttack = fmAttack ?? 0.001; + fmDecay = fmDecay ?? 0.001; + fmSustain = fmSustain ?? 1; + fmRelease = fmRelease ?? 0.001; + fmVelocity = fmVelocity ?? 1; + fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t); + if (fmEnvelopeType === 'exp') { + fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t); + fmEnvelope.node.maxValue = fmModulationIndex * 2; + fmEnvelope.node.minValue = 0.00001; + } + modulator.connect(fmEnvelope.node); + fmEnvelope.node.connect(o.frequency); + } + stopFm = stop; } - stopFm = stop; - } - // turn down - const g = gainNode(0.3); + // turn down + const g = gainNode(0.3); - // gain envelope - const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t); + // gain envelope + const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t); - o.onended = () => { - o.disconnect(); - g.disconnect(); - onended(); - }; - return { - node: o.connect(g).connect(envelope), - stop: (releaseTime) => { - releaseEnvelope(releaseTime); - fmEnvelope?.stop(releaseTime); - let end = releaseTime + release; - stop(end); - stopFm?.(end); - }, - }; - }, - { type: 'synth', prebake: true }, - ); - }); + o.onended = () => { + o.disconnect(); + g.disconnect(); + onended(); + }; + return { + node: o.connect(g).connect(envelope), + stop: (releaseTime) => { + releaseEnvelope(releaseTime); + fmEnvelope?.stop(releaseTime); + let end = releaseTime + release; + stop(end); + stopFm?.(end); + }, + }; + }, + { type: 'synth', prebake: true }, + ); + }); } export function waveformN(partials, type) { @@ -146,36 +148,86 @@ export function waveformN(partials, type) { return osc; } -export function getOscillator({ s, freq, t, vib, vibmod, partials }) { - // Make oscillator with partial count - let o; - if (!partials || s === 'sine') { - o = getAudioContext().createOscillator(); - o.type = s || 'triangle'; - } else { - o = waveformN(partials, s); - } - o.frequency.value = Number(freq); - o.start(t); +export function getNoiseOscillator({ t, ac, type = 'white' }) { + const bufferSize = 2 * ac.sampleRate; + const noiseBuffer = ac.createBuffer(1, bufferSize, ac.sampleRate); + const output = noiseBuffer.getChannelData(0); + let lastOut = 0; + let b0, b1, b2, b3, b4, b5, b6; + b0 = b1 = b2 = b3 = b4 = b5 = b6 = 0.0; - // Additional oscillator for vibrato effect - let vibrato_oscillator; - if (vib > 0) { - vibrato_oscillator = getAudioContext().createOscillator(); - vibrato_oscillator.frequency.value = vib; - const gain = getAudioContext().createGain(); - // Vibmod is the amount of vibrato, in semitones - gain.gain.value = vibmod * 100; - vibrato_oscillator.connect(gain); - gain.connect(o.detune); - vibrato_oscillator.start(t); + for (let i = 0; i < bufferSize; i++) { + if (type === 'white') { + output[i] = Math.random() * 2 - 1; + } else if (type === 'brown') { + let white = Math.random() * 2 - 1; + output[i] = (lastOut + (0.02 * white)) / 1.02; + lastOut = output[i]; + } else if (type === 'pink') { + let white = Math.random() * 2 - 1; + b0 = 0.99886 * b0 + white * 0.0555179; + b1 = 0.99332 * b1 + white * 0.0750759; + b2 = 0.96900 * b2 + white * 0.1538520; + b3 = 0.86650 * b3 + white * 0.3104856; + b4 = 0.55000 * b4 + white * 0.5329522; + b5 = -0.7616 * b5 - white * 0.0168980; + output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362; + output[i] *= 0.11; + b6 = white * 0.115926; + } } + const o = ac.createBufferSource(); + o.buffer = noiseBuffer; + o.loop = true; + o.start(t); + return { node: o, - stop: (time) => { - vibrato_oscillator?.stop(time); - o.stop(time); - }, + stop: (time) => o.stop(time) }; } + +export function getOscillator({ s, freq, t, vib, vibmod, partials }) { + // Make oscillator with partial count + let o; + + if (['pink', 'white', 'brown'].includes(s)) { + let noiseOscillator = getNoiseOscillator({ t: t, ac: getAudioContext(), type: s }) + return { + node: noiseOscillator.node, + stop: noiseOscillator.stop + } + } else { + if (!partials || s === 'sine') { + o = getAudioContext().createOscillator(); + o.type = s || 'triangle'; + } else { + o = waveformN(partials, s); + } + o.frequency.value = Number(freq); + o.start(t); + + // Additional oscillator for vibrato effect + let vibrato_oscillator; + if (vib > 0) { + vibrato_oscillator = getAudioContext().createOscillator(); + vibrato_oscillator.frequency.value = vib; + const gain = getAudioContext().createGain(); + // Vibmod is the amount of vibrato, in semitones + gain.gain.value = vibmod * 100; + vibrato_oscillator.connect(gain); + gain.connect(o.detune); + vibrato_oscillator.start(t); + } + + return { + node: o, + stop: (time) => { + vibrato_oscillator?.stop(time); + o.stop(time); + }, + }; + } +} + From 389c7be264c22fd7ce7fb2374dad9936ad4ffc15 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sat, 30 Sep 2023 14:39:44 +0200 Subject: [PATCH 04/48] Add noise parameter for base oscillators --- packages/superdough/synth.mjs | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 3ab3720f..aa7b63ee 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -45,6 +45,7 @@ export function registerSynthSounds() { fmwave: fmWaveform = 'sine', vib = 0, vibmod = 0.5, + noise = 0, } = value; let { n, note, freq } = value; // with synths, n and note are the same thing @@ -65,6 +66,7 @@ export function registerSynthSounds() { vib, vibmod, partials: n, + noise: noise, }); // FM + FM envelope let stopFm, fmEnvelope; @@ -188,8 +190,9 @@ export function getNoiseOscillator({ t, ac, type = 'white' }) { }; } -export function getOscillator({ s, freq, t, vib, vibmod, partials }) { +export function getOscillator({ s, freq, t, vib, vibmod, partials, noise }) { // Make oscillator with partial count + let ac = getAudioContext(); let o; if (['pink', 'white', 'brown'].includes(s)) { @@ -221,6 +224,33 @@ export function getOscillator({ s, freq, t, vib, vibmod, partials }) { vibrato_oscillator.start(t); } + if (noise > 0) { + // Two gain nodes to set the oscillators to their respective levels + let o_gain = ac.createGain(); + let n_gain = ac.createGain(); + o_gain.gain.setValueAtTime(1 - noise, ac.currentTime); + n_gain.gain.setValueAtTime(noise, ac.currentTime); + + // Instanciating a mixer to blend sources together + let mix_gain = ac.createGain(); + + // Connecting the main oscillator to the gain node + o.connect(o_gain).connect(mix_gain); + + // Instanciating a noise oscillator and connecting + const noiseOscillator = getNoiseOscillator({ t: t, ac: ac, type: 'pink' }); + noiseOscillator.node.connect(n_gain).connect(mix_gain); + + return { + node: mix_gain, + stop: (time) => { + vibrato_oscillator?.stop(time); + o.stop(time); + noiseOscillator.stop(time); + } + } + } + return { node: o, stop: (time) => { From bb7b8c2fabe72b520e9e312e2d2d02c8cb2e17c9 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sat, 30 Sep 2023 14:59:43 +0200 Subject: [PATCH 05/48] Fix noise parameter and FM parameters compatibility --- packages/superdough/synth.mjs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index aa7b63ee..cd523de8 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -59,7 +59,7 @@ export function registerSynthSounds() { } // maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default) // make oscillator - const { node: o, stop } = getOscillator({ + const { node: o, stop, dry_node = null } = getOscillator({ t, s: wave, freq, @@ -71,10 +71,10 @@ export function registerSynthSounds() { // FM + FM envelope let stopFm, fmEnvelope; if (fmModulationIndex) { - const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform); + const { node: modulator, stop } = fm(dry_node !== null ? dry_node : o, fmHarmonicity, fmModulationIndex, fmWaveform); if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) { // no envelope by default - modulator.connect(o.frequency); + modulator.connect(dry_node !== null ? dry_node.frequency : o.frequency); } else { fmAttack = fmAttack ?? 0.001; fmDecay = fmDecay ?? 0.001; @@ -88,7 +88,7 @@ export function registerSynthSounds() { fmEnvelope.node.minValue = 0.00001; } modulator.connect(fmEnvelope.node); - fmEnvelope.node.connect(o.frequency); + fmEnvelope.node.connect(dry_node !== null ? dry_node.frequency : o.frequency); } stopFm = stop; } @@ -243,6 +243,7 @@ export function getOscillator({ s, freq, t, vib, vibmod, partials, noise }) { return { node: mix_gain, + dry_node: o, stop: (time) => { vibrato_oscillator?.stop(time); o.stop(time); From e3333e716fc6b30da5a792eced45a5487dd59a35 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sat, 30 Sep 2023 15:08:09 +0200 Subject: [PATCH 06/48] Cap noise amount to 1 --- packages/superdough/synth.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index cd523de8..e5f84bcf 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -226,6 +226,7 @@ export function getOscillator({ s, freq, t, vib, vibmod, partials, noise }) { if (noise > 0) { // Two gain nodes to set the oscillators to their respective levels + noise = noise > 1 ? 1 : noise; let o_gain = ac.createGain(); let n_gain = ac.createGain(); o_gain.gain.setValueAtTime(1 - noise, ac.currentTime); From b2acff40c4354b5390334fcd9ec83b5aeacd2502 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sat, 30 Sep 2023 15:17:22 +0200 Subject: [PATCH 07/48] Add documentation about noise sources --- website/src/pages/learn/synths.mdx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 9f21204f..661ec860 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -23,6 +23,23 @@ The basic waveforms are `sine`, `sawtooth`, `square` and `triangle`, which can b If you don't set a `sound` but a `note` the default value for `sound` is `triangle`! +You can also use noise as a source by setting the waveform to: `white`, `pink` or `brown`. These are different +flavours of noise, here written from hard to soft. + +>") +.sound("/2") +.scope()`} +/> + +Some amount of pink noise can also be added to any oscillator by using the `noise` paremeter: + +").scope()`} +/> + ### Additive Synthesis To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform: From 276cf858fcf851bbe92a7b3f4a31dac405714c64 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 30 Sep 2023 20:39:03 +0200 Subject: [PATCH 08/48] match slider as function --- packages/codemirror/slider.mjs | 53 ++++++++++++++++------------------ website/tailwind.config.cjs | 1 + 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index 423d3651..63015051 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -13,7 +13,7 @@ export class SliderWidget extends WidgetType { } eq(other) { - const isSame = other.value.toFixed(4) == this.value.toFixed(4); + const isSame = other.value.toFixed(4) == this.value.toFixed(4) && other.min == this.min && other.max == this.max; return isSame; } @@ -29,7 +29,7 @@ export class SliderWidget extends WidgetType { slider.value = this.value; slider.from = this.from; slider.to = this.to; - slider.className = 'w-16'; + slider.className = 'w-16 translate-y-1.5'; return wrap; } @@ -52,26 +52,28 @@ let nodeValue = (node, view) => view.state.doc.sliceString(node.from, node.to); }); } }; */ -// matches something like 123.xxx and returns slider widget -let matchNumberSlider = (node, view) => { - if ( - node.name === 'MemberExpression' && - node.node.firstChild.name === 'Number' && - node.node.lastChild.name === 'PropertyName' - ) { - // node is sth like 123.xxx - let prop = nodeValue(node.node.lastChild, view); // get prop name (e.g. xxx) - if (prop === 'slider') { - let value = nodeValue(node.node.firstChild, view); // get number (e.g. 123) - // console.log('slider value', value); - let { from, to } = node.node.firstChild; - let min = 0; - let max = 10; - return Decoration.widget({ + +// matches something like slider(123) and returns slider widget +let matchSliderFunction = (node, view) => { + if (node.name === 'CallExpression' /* && node.node.firstChild.name === 'ArgList' */) { + let name = nodeValue(node.node.firstChild, view); // slider ? + if (name === 'slider') { + const args = node.node.lastChild.getChildren('Number'); + if (!args.length) { + return; + } + const [value, min = 0, max = 1] = args.map((node) => nodeValue(node, view)); + //console.log('slider value', value, min, max); + let { from, to } = args[0]; + let widget = Decoration.widget({ widget: new SliderWidget(Number(value), min, max, from, to), side: 0, }); + //widget._range = widget.range(from); + widget._range = widget.range(node.from); + return widget; } + // node is sth like 123.xxx } }; @@ -83,14 +85,11 @@ export function sliders(view) { from, to, enter: (node) => { - let numberSlider = matchNumberSlider(node, view); - if (numberSlider) { - widgets.push(numberSlider.range(node.from)); + let widget = matchSliderFunction(node, view); + // let widget = matchNumber(node, view); + if (widget) { + widgets.push(widget._range || widget.range(node.from)); } - /* let number = matchNumber(node, view); - if (number) { - widgets.push(number.range(node.from)); - } */ }, }); } @@ -150,11 +149,9 @@ function updateSliderValue(view, e) { if (before === next) { return false; } - //console.log('before', before, '->', insert); let change = { from: draggedSlider.from, to: draggedSlider.to, insert }; draggedSlider.to = draggedSlider.from + insert.length; - //console.log('change', change); view.dispatch({ changes: change }); - + window.postMessage({ type: 'slider-change', value: next }); return true; } diff --git a/website/tailwind.config.cjs b/website/tailwind.config.cjs index d92d5949..2c682d7d 100644 --- a/website/tailwind.config.cjs +++ b/website/tailwind.config.cjs @@ -7,6 +7,7 @@ module.exports = { content: [ './src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}', '../packages/react/src/**/*.{html,js,jsx,md,mdx,ts,tsx}', + '../packages/codemirror/slider.mjs', ], theme: { extend: { From 2731e70fb7c41b654d01cb2cd21a65afcfcbf758 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 30 Sep 2023 21:03:28 +0200 Subject: [PATCH 09/48] use loc as slider id --- packages/codemirror/slider.mjs | 2 +- packages/transpiler/transpiler.mjs | 25 ++++++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index 63015051..2e75959b 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -152,6 +152,6 @@ function updateSliderValue(view, e) { let change = { from: draggedSlider.from, to: draggedSlider.to, insert }; draggedSlider.to = draggedSlider.from + insert.length; view.dispatch({ changes: change }); - window.postMessage({ type: 'slider-change', value: next }); + window.postMessage({ type: 'cm-slider', value: next, loc: draggedSlider.from }); return true; } diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 78aae9f7..9ff558cb 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -35,6 +35,10 @@ export function transpiler(input, options = {}) { emitMiniLocations && collectMiniLocations(value, node); return this.replace(miniWithLocation(value, node)); } + if (isWidgetFunction(node)) { + // collectSliderLocations? + return this.replace(widgetWithLocation(node)); + } // TODO: remove pseudo note variables? if (node.type === 'Identifier' && isNoteWithOctave(node.name)) { this.skip(); @@ -68,11 +72,10 @@ export function transpiler(input, options = {}) { } function isStringWithDoubleQuotes(node, locations, code) { - const { raw, type } = node; - if (type !== 'Literal') { + if (node.type !== 'Literal') { return false; } - return raw[0] === '"'; + return node.raw[0] === '"'; } function isBackTickString(node, parent) { @@ -94,3 +97,19 @@ function miniWithLocation(value, node) { optional: false, }; } + +function isWidgetFunction(node) { + return node.type === 'CallExpression' && node.callee.name === 'slider'; +} + +function widgetWithLocation(node) { + const loc = node.arguments[0].start; + // add loc as identifier to first argument + // the slider function is assumed to be slider(loc, value, min?, max?) + node.arguments.unshift({ + type: 'Literal', + value: loc, + raw: loc + '', + }); + return node; +} From b36cee93f7e86b003e65b438caee14bdd24b976c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 30 Sep 2023 21:22:49 +0200 Subject: [PATCH 10/48] add slider function to scope --- packages/codemirror/index.mjs | 1 + packages/codemirror/slider.mjs | 25 +++++++++++++++++++++++-- packages/transpiler/transpiler.mjs | 10 ++++++---- website/src/repl/Repl.jsx | 1 + 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/codemirror/index.mjs b/packages/codemirror/index.mjs index bf7ce971..c847c32c 100644 --- a/packages/codemirror/index.mjs +++ b/packages/codemirror/index.mjs @@ -1,3 +1,4 @@ export * from './codemirror.mjs'; export * from './highlight.mjs'; export * from './flash.mjs'; +export * from './slider.mjs'; diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index 2e75959b..7695cee6 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -13,7 +13,12 @@ export class SliderWidget extends WidgetType { } eq(other) { - const isSame = other.value.toFixed(4) == this.value.toFixed(4) && other.min == this.min && other.max == this.max; + const isSame = + other.value.toFixed(4) == this.value.toFixed(4) && + other.min == this.min && + other.max == this.max && + other.from === this.from && + other.to === this.to; return isSame; } @@ -152,6 +157,22 @@ function updateSliderValue(view, e) { let change = { from: draggedSlider.from, to: draggedSlider.to, insert }; draggedSlider.to = draggedSlider.from + insert.length; view.dispatch({ changes: change }); - window.postMessage({ type: 'cm-slider', value: next, loc: draggedSlider.from }); + const id = 'slider_' + draggedSlider.from; // matches id generated in transpiler + window.postMessage({ type: 'cm-slider', value: next, id }); return true; } + +export let sliderValues = {}; + +export let slider = (id, value, min, max) => { + sliderValues[id] = value; // sync state at eval time (code -> state) + return ref(() => sliderValues[id]); // use state at query time +}; +if (typeof window !== 'undefined') { + window.addEventListener('message', (e) => { + if (e.data.type === 'cm-slider') { + // update state when slider is moved + sliderValues[e.data.id] = e.data.value; + } + }); +} diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 9ff558cb..28f7fdfa 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -98,18 +98,20 @@ function miniWithLocation(value, node) { }; } +// these functions are connected to @strudel/codemirror -> slider.mjs +// maybe someday there will be pluggable transpiler functions, then move this there function isWidgetFunction(node) { return node.type === 'CallExpression' && node.callee.name === 'slider'; } function widgetWithLocation(node) { - const loc = node.arguments[0].start; + const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id // add loc as identifier to first argument - // the slider function is assumed to be slider(loc, value, min?, max?) + // the slider function is assumed to be slider(id, value, min?, max?) node.arguments.unshift({ type: 'Literal', - value: loc, - raw: loc + '', + value: id, + raw: id, }); return node; } diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 4002eba8..9d80cc53 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -39,6 +39,7 @@ let modules = [ import('@strudel.cycles/mini'), import('@strudel.cycles/xen'), import('@strudel.cycles/webaudio'), + import('@strudel/codemirror'), import('@strudel.cycles/serial'), import('@strudel.cycles/soundfonts'), From 0ad0046a769db952c8e68f097a730374e2b94a1e Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sat, 30 Sep 2023 22:04:22 +0200 Subject: [PATCH 11/48] Replacing old reverb by better convolution --- packages/superdough/reverb.mjs | 34 ++--- packages/superdough/reverbGen.mjs | 209 +++++++++++++++++++++++++++++ packages/superdough/superdough.mjs | 9 +- 3 files changed, 236 insertions(+), 16 deletions(-) create mode 100644 packages/superdough/reverbGen.mjs diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs index e6d31f6a..4d5f655f 100644 --- a/packages/superdough/reverb.mjs +++ b/packages/superdough/reverb.mjs @@ -1,23 +1,27 @@ -if (typeof AudioContext !== 'undefined') { - AudioContext.prototype.impulseResponse = function (duration, channels = 1) { - const length = this.sampleRate * duration; - const impulse = this.createBuffer(channels, length, this.sampleRate); - const IR = impulse.getChannelData(0); - for (let i = 0; i < length; i++) IR[i] = (2 * Math.random() - 1) * Math.pow(1 - i / length, duration); - return impulse; - }; +import reverbGen from './reverbGen.mjs'; - AudioContext.prototype.createReverb = function (duration) { +if (typeof AudioContext !== 'undefined') { + AudioContext.prototype.generateReverb = reverbGen.generateReverb; + AudioContext.prototype.createReverb = function(duration, audioContext) { const convolver = this.createConvolver(); convolver.setDuration = (d) => { - convolver.buffer = this.impulseResponse(d); - convolver.duration = duration; - return convolver; + this.generateReverb( + { + audioContext, + sampleRate: 44100, + numChannels: 2, + decayTime: d, + fadeInTime: d, + lpFreqStart: 2000, + lpFreqEnd: 15000, + }, + (buffer) => { + convolver.buffer = buffer; + } + ); + convolver.duration = d; }; convolver.setDuration(duration); return convolver; }; } - -// TODO: make the reverb more exciting -// check out https://blog.gskinner.com/archives/2019/02/reverb-web-audio-api.html diff --git a/packages/superdough/reverbGen.mjs b/packages/superdough/reverbGen.mjs new file mode 100644 index 00000000..1d05ee82 --- /dev/null +++ b/packages/superdough/reverbGen.mjs @@ -0,0 +1,209 @@ +// Copyright 2014 Alan deLespinasse +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +"use strict"; + + +var reverbGen = {}; + +/** Generates a reverb impulse response. + + @param {!Object} params TODO: Document the properties. + @param {!function(!AudioBuffer)} callback Function to call when + the impulse response has been generated. The impulse response + is passed to this function as its parameter. May be called + immediately within the current execution context, or later. */ +reverbGen.generateReverb = function(params, callback) { + var audioContext = params.audioContext || new AudioContext(); + var sampleRate = params.sampleRate || 44100; + var numChannels = params.numChannels || 2; + // params.decayTime is the -60dB fade time. We let it go 50% longer to get to -90dB. + var totalTime = params.decayTime * 1.5; + var decaySampleFrames = Math.round(params.decayTime * sampleRate); + var numSampleFrames = Math.round(totalTime * sampleRate); + var fadeInSampleFrames = Math.round((params.fadeInTime || 0) * sampleRate); + // 60dB is a factor of 1 million in power, or 1000 in amplitude. + var decayBase = Math.pow(1 / 1000, 1 / decaySampleFrames); + var reverbIR = audioContext.createBuffer(numChannels, numSampleFrames, sampleRate); + for (var i = 0; i < numChannels; i++) { + var chan = reverbIR.getChannelData(i); + for (var j = 0; j < numSampleFrames; j++) { + chan[j] = randomSample() * Math.pow(decayBase, j); + } + for (var j = 0; j < fadeInSampleFrames; j++) { + chan[j] *= (j / fadeInSampleFrames); + } + } + + applyGradualLowpass(reverbIR, params.lpFreqStart || 0, params.lpFreqEnd || 0, params.decayTime, callback); +}; + +/** Creates a canvas element showing a graph of the given data. + + @param {!Float32Array} data An array of numbers, or a Float32Array. + @param {number} width Width in pixels of the canvas. + @param {number} height Height in pixels of the canvas. + @param {number} min Minimum value of data for the graph (lower edge). + @param {number} max Maximum value of data in the graph (upper edge). + @return {!CanvasElement} The generated canvas element. */ +reverbGen.generateGraph = function(data, width, height, min, max) { + var canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + var gc = canvas.getContext('2d'); + gc.fillStyle = '#000'; + gc.fillRect(0, 0, canvas.width, canvas.height); + gc.fillStyle = '#fff'; + var xscale = width / data.length; + var yscale = height / (max - min); + for (var i = 0; i < data.length; i++) { + gc.fillRect(i * xscale, height - (data[i] - min) * yscale, 1, 1); + } + return canvas; +} + +/** Saves an AudioBuffer as a 16-bit WAV file on the client's host + file system. Normalizes it to peak at +-32767, and optionally + truncates it if there's a lot of "silence" at the end. + + @param {!AudioBuffer} buffer The buffer to save. + @param {string} name Name of file to create. + @param {number?} opt_minTail Defines what counts as "silence" for + auto-truncating the buffer. If there is a point past which every + value of every channel is less than opt_minTail, then the buffer + is truncated at that point. This is expressed as an integer, + applying to the post-normalized and integer-converted + buffer. The default is 0, meaning don't truncate. */ +reverbGen.saveWavFile = function(buffer, name, opt_minTail) { + var bitsPerSample = 16; + var bytesPerSample = 2; + var sampleRate = buffer.sampleRate; + var numChannels = buffer.numberOfChannels; + var channels = getAllChannelData(buffer); + var numSampleFrames = channels[0].length; + var scale = 32767; + // Find normalization constant. + var max = 0; + for (var i = 0; i < numChannels; i++) { + for (var j = 0; j < numSampleFrames; j++) { + max = Math.max(max, Math.abs(channels[i][j])); + } + } + if (max) { + scale = 32767 / max; + } + // Find truncation point. + if (opt_minTail) { + var truncateAt = 0; + for (var i = 0; i < numChannels; i++) { + for (var j = 0; j < numSampleFrames; j++) { + var absSample = Math.abs(Math.round(scale * channels[i][j])); + if (absSample > opt_minTail) { + truncateAt = j; + } + } + } + numSampleFrames = truncateAt + 1; + } + var sampleDataBytes = bytesPerSample * numChannels * numSampleFrames; + var fileBytes = sampleDataBytes + 44; + var arrayBuffer = new ArrayBuffer(fileBytes); + var dataView = new DataView(arrayBuffer); + dataView.setUint32(0, 1179011410, true); // "RIFF" + dataView.setUint32(4, fileBytes - 8, true); // file length + dataView.setUint32(8, 1163280727, true); // "WAVE" + dataView.setUint32(12, 544501094, true); // "fmt " + dataView.setUint32(16, 16, true) // fmt chunk length + dataView.setUint16(20, 1, true); // PCM format + dataView.setUint16(22, numChannels, true); // NumChannels + dataView.setUint32(24, sampleRate, true); // SampleRate + var bytesPerSampleFrame = numChannels * bytesPerSample; + dataView.setUint32(28, sampleRate * bytesPerSampleFrame, true); // ByteRate + dataView.setUint16(32, bytesPerSampleFrame, true); // BlockAlign + dataView.setUint16(34, bitsPerSample, true); // BitsPerSample + dataView.setUint32(36, 1635017060, true); // "data" + dataView.setUint32(40, sampleDataBytes, true); + for (var j = 0; j < numSampleFrames; j++) { + for (var i = 0; i < numChannels; i++) { + dataView.setInt16(44 + j * bytesPerSampleFrame + i * bytesPerSample, + Math.round(scale * channels[i][j]), true); + } + } + var blob = new Blob([arrayBuffer], { 'type': 'audio/wav' }); + var url = window.URL.createObjectURL(blob); + var linkEl = document.createElement('a'); + linkEl.href = url; + linkEl.download = name; + linkEl.style.display = 'none'; + document.body.appendChild(linkEl); + linkEl.click(); +}; + +/** Applies a constantly changing lowpass filter to the given sound. + + @private + @param {!AudioBuffer} input + @param {number} lpFreqStart + @param {number} lpFreqEnd + @param {number} lpFreqEndAt + @param {!function(!AudioBuffer)} callback May be called + immediately within the current execution context, or later.*/ +var applyGradualLowpass = function(input, lpFreqStart, lpFreqEnd, lpFreqEndAt, callback) { + if (lpFreqStart == 0) { + callback(input); + return; + } + var channelData = getAllChannelData(input); + var context = new OfflineAudioContext(input.numberOfChannels, channelData[0].length, input.sampleRate); + var player = context.createBufferSource(); + player.buffer = input; + var filter = context.createBiquadFilter(); + + lpFreqStart = Math.min(lpFreqStart, input.sampleRate / 2); + lpFreqEnd = Math.min(lpFreqEnd, input.sampleRate / 2); + + filter.type = "lowpass"; + filter.Q.value = 0.0001; + filter.frequency.setValueAtTime(lpFreqStart, 0); + filter.frequency.linearRampToValueAtTime(lpFreqEnd, lpFreqEndAt); + + player.connect(filter); + filter.connect(context.destination); + player.start(); + context.oncomplete = function(event) { + callback(event.renderedBuffer); + }; + context.startRendering(); + + window.filterNode = filter; +}; + +/** @private + @param {!AudioBuffer} buffer + @return {!Array.} An array containing the Float32Array of each channel's samples. */ +var getAllChannelData = function(buffer) { + var channels = []; + for (var i = 0; i < buffer.numberOfChannels; i++) { + channels[i] = buffer.getChannelData(i); + } + return channels; +}; + +/** @private + @return {number} A random number from -1 to 1. */ +var randomSample = function() { + return Math.random() * 2 - 1; +}; + +export default reverbGen; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 289e8d97..9010c85d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -107,17 +107,24 @@ function getDelay(orbit, delaytime, delayfeedback, t) { } let reverbs = {}; + function getReverb(orbit, duration = 2) { + + // If no reverb has been created for a given orbit, create one if (!reverbs[orbit]) { const ac = getAudioContext(); - const reverb = ac.createReverb(duration); + const reverb = ac.createReverb(duration, getAudioContext()); reverb.connect(getDestination()); + console.log(reverb) reverbs[orbit] = reverb; } + + // Update the reverb duration if needed after instanciation if (reverbs[orbit].duration !== duration) { reverbs[orbit] = reverbs[orbit].setDuration(duration); reverbs[orbit].duration = duration; } + return reverbs[orbit]; } From 062d92690036394418717821499ebc71225e1114 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 00:24:31 +0200 Subject: [PATCH 12/48] make sliders work! --- packages/codemirror/slider.mjs | 149 +++++++++++------------- packages/react/src/hooks/useWidgets.mjs | 13 +++ packages/transpiler/transpiler.mjs | 14 ++- website/src/repl/Repl.jsx | 6 +- 4 files changed, 95 insertions(+), 87 deletions(-) create mode 100644 packages/react/src/hooks/useWidgets.mjs diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index 7695cee6..6510f511 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -1,6 +1,8 @@ -import { WidgetType } from '@codemirror/view'; -import { ViewPlugin, Decoration } from '@codemirror/view'; -import { syntaxTree } from '@codemirror/language'; +import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view'; +import { StateEffect, StateField } from '@codemirror/state'; + +export let sliderValues = {}; +const getSliderID = (from) => `slider_${from}`; export class SliderWidget extends WidgetType { constructor(value, min, max, from, to) { @@ -9,17 +11,12 @@ export class SliderWidget extends WidgetType { this.min = min; this.max = max; this.from = from; + this.originalFrom = from; this.to = to; } - eq(other) { - const isSame = - other.value.toFixed(4) == this.value.toFixed(4) && - other.min == this.min && - other.max == this.max && - other.from === this.from && - other.to === this.to; - return isSame; + eq() { + return false; } toDOM() { @@ -31,10 +28,15 @@ export class SliderWidget extends WidgetType { slider.min = this.min; slider.max = this.max; slider.step = (this.max - this.min) / 1000; - slider.value = this.value; + slider.originalValue = this.value.toFixed(2); + // to make sure the code stays in sync, let's save the original value + // becuase .value automatically clamps values so it'll desync with the code + slider.value = slider.originalValue; slider.from = this.from; + slider.originalFrom = this.originalFrom; slider.to = this.to; - slider.className = 'w-16 translate-y-1.5'; + slider.className = 'w-16 translate-y-1.5 mx-2'; + this.slider = slider; return wrap; } @@ -43,78 +45,50 @@ export class SliderWidget extends WidgetType { } } -let nodeValue = (node, view) => view.state.doc.sliceString(node.from, node.to); +export const setWidgets = StateEffect.define(); -// matches a number and returns slider widget -/* let matchNumber = (node, view) => { - if (node.name == 'Number') { - const value = view.state.doc.sliceString(node.from, node.to); - let min = 0; - let max = 10; - return Decoration.widget({ - widget: new SliderWidget(Number(value), min, max, node.from, node.to), - side: 0, - }); - } -}; */ - -// matches something like slider(123) and returns slider widget -let matchSliderFunction = (node, view) => { - if (node.name === 'CallExpression' /* && node.node.firstChild.name === 'ArgList' */) { - let name = nodeValue(node.node.firstChild, view); // slider ? - if (name === 'slider') { - const args = node.node.lastChild.getChildren('Number'); - if (!args.length) { - return; - } - const [value, min = 0, max = 1] = args.map((node) => nodeValue(node, view)); - //console.log('slider value', value, min, max); - let { from, to } = args[0]; - let widget = Decoration.widget({ - widget: new SliderWidget(Number(value), min, max, from, to), - side: 0, - }); - //widget._range = widget.range(from); - widget._range = widget.range(node.from); - return widget; - } - // node is sth like 123.xxx - } +export const updateWidgets = (view, widgets) => { + view.dispatch({ effects: setWidgets.of(widgets) }); }; -// EditorView -export function sliders(view) { - let widgets = []; - for (let { from, to } of view.visibleRanges) { - syntaxTree(view.state).iterate({ - from, - to, - enter: (node) => { - let widget = matchSliderFunction(node, view); - // let widget = matchNumber(node, view); - if (widget) { - widgets.push(widget._range || widget.range(node.from)); - } - }, - }); - } - return Decoration.set(widgets); +let draggedSlider; + +function getWidgets(widgetConfigs) { + return widgetConfigs.map(({ from, to, value, min, max }) => { + return Decoration.widget({ + widget: new SliderWidget(Number(value), min, max, from, to), + side: 0, + }).range(from /* , to */); + }); } -let draggedSlider, init; export const sliderPlugin = ViewPlugin.fromClass( class { decorations; //: DecorationSet constructor(view /* : EditorView */) { - this.decorations = sliders(view); + this.decorations = Decoration.set([]); } update(update /* : ViewUpdate */) { - if (update.docChanged || update.viewportChanged) { - !init && (this.decorations = sliders(update.view)); - //init = true; - } + update.transactions.forEach((tr) => { + if (tr.docChanged) { + this.decorations = this.decorations.map(tr.changes); + const iterator = this.decorations.iter(); + while (iterator.value) { + // when the widgets are moved, we need to tell the dom node the current position + // this is important because the updateSliderValue function has to work with the dom node + iterator.value.widget.slider.from = iterator.from; + iterator.value.widget.slider.to = iterator.to; + iterator.next(); + } + } + for (let e of tr.effects) { + if (e.is(setWidgets)) { + this.decorations = Decoration.set(getWidgets(e.value)); + } + } + }); } }, { @@ -124,6 +98,8 @@ export const sliderPlugin = ViewPlugin.fromClass( mousedown: (e, view) => { let target = e.target; /* as HTMLElement */ if (target.nodeName == 'INPUT' && target.parentElement.classList.contains('cm-slider')) { + e.preventDefault(); + e.stopPropagation(); draggedSlider = target; // remember offsetLeft / clientWidth, as they will vanish inside mousemove events for some reason draggedSlider._offsetLeft = draggedSlider.offsetLeft; @@ -141,6 +117,7 @@ export const sliderPlugin = ViewPlugin.fromClass( }, ); +// moves slider on mouse event function updateSliderValue(view, e) { const mouseX = e.clientX; let progress = (mouseX - draggedSlider._offsetLeft) / draggedSlider._clientWidth; @@ -149,30 +126,38 @@ function updateSliderValue(view, e) { let max = Number(draggedSlider.max); const next = Number(progress * (max - min) + min); let insert = next.toFixed(2); - let before = view.state.doc.sliceString(draggedSlider.from, draggedSlider.to).trim(); - before = Number(before).toFixed(4); - if (before === next) { + //let before = view.state.doc.sliceString(draggedSlider.from, draggedSlider.to).trim(); + let before = draggedSlider.originalValue; + before = Number(before).toFixed(2); + // console.log('before', before, 'insert', insert, 'v'); + if (before === insert) { return false; } - let change = { from: draggedSlider.from, to: draggedSlider.to, insert }; - draggedSlider.to = draggedSlider.from + insert.length; + const to = draggedSlider.from + before.length; + let change = { from: draggedSlider.from, to, insert }; + draggedSlider.originalValue = insert; + draggedSlider.value = insert; view.dispatch({ changes: change }); - const id = 'slider_' + draggedSlider.from; // matches id generated in transpiler + const id = getSliderID(draggedSlider.originalFrom); // matches id generated in transpiler window.postMessage({ type: 'cm-slider', value: next, id }); return true; } -export let sliderValues = {}; - +// user api export let slider = (id, value, min, max) => { sliderValues[id] = value; // sync state at eval time (code -> state) return ref(() => sliderValues[id]); // use state at query time }; +// update state when sliders are moved if (typeof window !== 'undefined') { window.addEventListener('message', (e) => { if (e.data.type === 'cm-slider') { - // update state when slider is moved - sliderValues[e.data.id] = e.data.value; + if (sliderValues[e.data.id] !== undefined) { + // update state when slider is moved + sliderValues[e.data.id] = e.data.value; + } else { + console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`); + } } }); } diff --git a/packages/react/src/hooks/useWidgets.mjs b/packages/react/src/hooks/useWidgets.mjs new file mode 100644 index 00000000..e7ca136a --- /dev/null +++ b/packages/react/src/hooks/useWidgets.mjs @@ -0,0 +1,13 @@ +import { useEffect, useState } from 'react'; +import { updateWidgets } from '@strudel/codemirror'; + +// i know this is ugly.. in the future, repl needs to run without react +export function useWidgets(view) { + const [widgets, setWidgets] = useState([]); + useEffect(() => { + if (view) { + updateWidgets(view, widgets); + } + }, [view, widgets]); + return { widgets, setWidgets }; +} diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 28f7fdfa..48b223d4 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -5,7 +5,7 @@ import { isNoteWithOctave } from '@strudel.cycles/core'; import { getLeafLocations } from '@strudel.cycles/mini'; export function transpiler(input, options = {}) { - const { wrapAsync = false, addReturn = true, emitMiniLocations = true } = options; + const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; let ast = parse(input, { ecmaVersion: 2022, @@ -16,9 +16,9 @@ export function transpiler(input, options = {}) { let miniLocations = []; const collectMiniLocations = (value, node) => { const leafLocs = getLeafLocations(`"${value}"`, node.start); // stimmt! - //const withOffset = leafLocs.map((offsets) => offsets.map((o) => o + node.start)); miniLocations = miniLocations.concat(leafLocs); }; + let widgets = []; walk(ast, { enter(node, parent /* , prop, index */) { @@ -37,6 +37,14 @@ export function transpiler(input, options = {}) { } if (isWidgetFunction(node)) { // collectSliderLocations? + emitWidgets && + widgets.push({ + from: node.arguments[0].start, + to: node.arguments[0].end, + value: node.arguments[0].value, + min: node.arguments[1]?.value ?? 0, + max: node.arguments[2]?.value ?? 1, + }); return this.replace(widgetWithLocation(node)); } // TODO: remove pseudo note variables? @@ -68,7 +76,7 @@ export function transpiler(input, options = {}) { if (!emitMiniLocations) { return { output }; } - return { output, miniLocations }; + return { output, miniLocations, widgets }; } function isStringWithDoubleQuotes(node, locations, code) { diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 9d80cc53..8fe43c6d 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -22,6 +22,7 @@ import Loader from './Loader'; import { settingPatterns } from '../settings.mjs'; import { code2hash, hash2code } from './helpers.mjs'; import { isTauri } from '../tauri.mjs'; +import { useWidgets } from '@strudel.cycles/react/src/hooks/useWidgets.mjs'; const { latestCode } = settingsMap.get(); @@ -129,7 +130,7 @@ export function Repl({ embedded = false }) { } = useSettings(); const paintOptions = useMemo(() => ({ fontFamily }), [fontFamily]); - + const { setWidgets } = useWidgets(view); const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } = useStrudel({ initialCode: '// LOADING...', @@ -143,6 +144,7 @@ export function Repl({ embedded = false }) { }, afterEval: ({ code, meta }) => { setMiniLocations(meta.miniLocations); + setWidgets(meta.widgets); setPending(false); setLatestCode(code); window.location.hash = '#' + code2hash(code); @@ -220,7 +222,7 @@ export function Repl({ embedded = false }) { const handleChangeCode = useCallback( (c) => { setCode(c); - started && logger('[edit] code changed. hit ctrl+enter to update'); + //started && logger('[edit] code changed. hit ctrl+enter to update'); }, [started], ); From 33c40e5ef84af31a1f395d1cf30416c9602aa9e8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 00:33:10 +0200 Subject: [PATCH 13/48] fix some odd number / string problems --- packages/codemirror/slider.mjs | 6 +++--- packages/transpiler/transpiler.mjs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index 6510f511..5c2dcdab 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -28,7 +28,7 @@ export class SliderWidget extends WidgetType { slider.min = this.min; slider.max = this.max; slider.step = (this.max - this.min) / 1000; - slider.originalValue = this.value.toFixed(2); + slider.originalValue = this.value; // to make sure the code stays in sync, let's save the original value // becuase .value automatically clamps values so it'll desync with the code slider.value = slider.originalValue; @@ -56,7 +56,7 @@ let draggedSlider; function getWidgets(widgetConfigs) { return widgetConfigs.map(({ from, to, value, min, max }) => { return Decoration.widget({ - widget: new SliderWidget(Number(value), min, max, from, to), + widget: new SliderWidget(value, min, max, from, to), side: 0, }).range(from /* , to */); }); @@ -133,7 +133,7 @@ function updateSliderValue(view, e) { if (before === insert) { return false; } - const to = draggedSlider.from + before.length; + const to = draggedSlider.from + draggedSlider.originalValue.length; let change = { from: draggedSlider.from, to, insert }; draggedSlider.originalValue = insert; draggedSlider.value = insert; diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 48b223d4..5fa9dc49 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -41,7 +41,7 @@ export function transpiler(input, options = {}) { widgets.push({ from: node.arguments[0].start, to: node.arguments[0].end, - value: node.arguments[0].value, + value: node.arguments[0].raw, // don't use value! min: node.arguments[1]?.value ?? 0, max: node.arguments[2]?.value ?? 1, }); From d7bc309eeb06bbe5f2f0e8aa12f9c9af2015452f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 00:36:34 +0200 Subject: [PATCH 14/48] fix: import --- packages/codemirror/slider.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index 5c2dcdab..44875e25 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -1,3 +1,4 @@ +import { ref } from '@strudel.cycles/core'; import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view'; import { StateEffect, StateField } from '@codemirror/state'; From abff27970746fecb4620d54a272abc5b67972d0c Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 1 Oct 2023 11:52:24 +0200 Subject: [PATCH 15/48] Document reverb controls --- packages/core/controls.mjs | 39 ++++++++++++++++++++++++++++-- packages/superdough/reverb.mjs | 14 ++++++++--- packages/superdough/superdough.mjs | 11 ++++++++- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 6cac6e54..216b7001 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -970,6 +970,41 @@ const generic_params = [ * */ [['room', 'size']], + /** + * Reverb lowpass starting frequency (in hertz). + * + * @name revlp + * @param {number} level between 0 and 20000hz + * @example + * s("bd sd").room(0.5).revlp(10000) + * @example + * s("bd sd").room(0.5).revlp(5000) + */ + ['revlp'], + /** + * Reverb lowpass frequency at -60dB (in hertz). + * + * @name revdim + * @param {number} level between 0 and 20000hz + * @example + * s("bd sd").room(0.5).revlp(10000).revdim(8000) + * @example + * s("bd sd").room(0.5).revlp(5000).revdim(400) + * + */ + ['revdim'], + /** + * Reverb fade time (in seconds). + * + * @name fade + * @param {number} seconds for the reverb to fade + * @example + * s("bd sd").room(0.5).revlp(10000).fade(0.5) + * @example + * s("bd sd").room(0.5).revlp(5000).fade(4) + * + */ + ['fade'], /** * Sets the room size of the reverb, see {@link room}. * @@ -1162,7 +1197,7 @@ const generic_params = [ ]; // TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13 -controls.createParam = function (names) { +controls.createParam = function(names) { const name = Array.isArray(names) ? names[0] : names; var withVal; @@ -1186,7 +1221,7 @@ controls.createParam = function (names) { const func = (...pats) => sequence(...pats).withValue(withVal); - const setter = function (...pats) { + const setter = function(...pats) { if (!pats.length) { return this.fmap(withVal); } diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs index 4d5f655f..505d0ac2 100644 --- a/packages/superdough/reverb.mjs +++ b/packages/superdough/reverb.mjs @@ -2,7 +2,13 @@ import reverbGen from './reverbGen.mjs'; if (typeof AudioContext !== 'undefined') { AudioContext.prototype.generateReverb = reverbGen.generateReverb; - AudioContext.prototype.createReverb = function(duration, audioContext) { + AudioContext.prototype.createReverb = function( + duration, + audioContext, + fade, + revlp, + revdim + ) { const convolver = this.createConvolver(); convolver.setDuration = (d) => { this.generateReverb( @@ -11,9 +17,9 @@ if (typeof AudioContext !== 'undefined') { sampleRate: 44100, numChannels: 2, decayTime: d, - fadeInTime: d, - lpFreqStart: 2000, - lpFreqEnd: 15000, + fadeInTime: fade, + lpFreqStart: revlp, + lpFreqEnd: revdim, }, (buffer) => { convolver.buffer = buffer; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 9010c85d..33c108f2 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -113,7 +113,13 @@ function getReverb(orbit, duration = 2) { // If no reverb has been created for a given orbit, create one if (!reverbs[orbit]) { const ac = getAudioContext(); - const reverb = ac.createReverb(duration, getAudioContext()); + const reverb = ac.createReverb( + duration, + getAudioContext(), + fade, + revlp, + revdim, + ); reverb.connect(getDestination()); console.log(reverb) reverbs[orbit] = reverb; @@ -222,6 +228,9 @@ export const superdough = async (value, deadline, hapDuration) => { delaytime = 0.25, orbit = 1, room, + fade = 0.1, + revlp = 15000, + revdim = 1000, size = 2, velocity = 1, analyze, // analyser wet From e600b91a8569685444bb1f719f9c6ed4b8910b9b Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 1 Oct 2023 12:02:17 +0200 Subject: [PATCH 16/48] bugfixes for parameter passing --- packages/superdough/reverb.mjs | 4 ++-- packages/superdough/superdough.mjs | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs index 505d0ac2..4c5bc1d1 100644 --- a/packages/superdough/reverb.mjs +++ b/packages/superdough/reverb.mjs @@ -3,14 +3,14 @@ import reverbGen from './reverbGen.mjs'; if (typeof AudioContext !== 'undefined') { AudioContext.prototype.generateReverb = reverbGen.generateReverb; AudioContext.prototype.createReverb = function( - duration, audioContext, + duration, fade, revlp, revdim ) { const convolver = this.createConvolver(); - convolver.setDuration = (d) => { + convolver.setDuration = (d, fade, revlp, revdim) => { this.generateReverb( { audioContext, diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 33c108f2..607c649d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -108,29 +108,27 @@ function getDelay(orbit, delaytime, delayfeedback, t) { let reverbs = {}; -function getReverb(orbit, duration = 2) { +function getReverb(orbit, duration = 2, fade, revlp, revdim) { // If no reverb has been created for a given orbit, create one if (!reverbs[orbit]) { const ac = getAudioContext(); const reverb = ac.createReverb( - duration, getAudioContext(), + duration, fade, revlp, - revdim, ); reverb.connect(getDestination()); console.log(reverb) reverbs[orbit] = reverb; } - // Update the reverb duration if needed after instanciation if (reverbs[orbit].duration !== duration) { - reverbs[orbit] = reverbs[orbit].setDuration(duration); + reverbs[orbit] = reverbs[orbit].setDuration( + duration, fade, revlp, revdim); reverbs[orbit].duration = duration; } - return reverbs[orbit]; } @@ -370,7 +368,7 @@ export const superdough = async (value, deadline, hapDuration) => { // reverb let reverbSend; if (room > 0 && size > 0) { - const reverbNode = getReverb(orbit, size); + const reverbNode = getReverb(orbit, size, fade, revlp, revdim); reverbSend = effectSend(post, reverbNode, room); } From c6ecd31ea11baef947f4f261f7edcea31290ebec Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 13:27:44 +0200 Subject: [PATCH 17/48] improve mouse tracking --- packages/codemirror/slider.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index 44875e25..d4fbcefa 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -36,7 +36,7 @@ export class SliderWidget extends WidgetType { slider.from = this.from; slider.originalFrom = this.originalFrom; slider.to = this.to; - slider.className = 'w-16 translate-y-1.5 mx-2'; + slider.className = 'w-16 translate-y-1'; this.slider = slider; return wrap; } @@ -97,7 +97,7 @@ export const sliderPlugin = ViewPlugin.fromClass( eventHandlers: { mousedown: (e, view) => { - let target = e.target; /* as HTMLElement */ + let target = e.target; if (target.nodeName == 'INPUT' && target.parentElement.classList.contains('cm-slider')) { e.preventDefault(); e.stopPropagation(); @@ -121,7 +121,8 @@ export const sliderPlugin = ViewPlugin.fromClass( // moves slider on mouse event function updateSliderValue(view, e) { const mouseX = e.clientX; - let progress = (mouseX - draggedSlider._offsetLeft) / draggedSlider._clientWidth; + let mx = 10; + let progress = (mouseX - draggedSlider._offsetLeft - mx) / (draggedSlider._clientWidth - mx * 2); progress = Math.max(Math.min(1, progress), 0); let min = Number(draggedSlider.min); let max = Number(draggedSlider.max); From b550ff038c33b1ecabff5f52e8916f0092740e31 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 13:42:29 +0200 Subject: [PATCH 18/48] simplify: use native events --- packages/codemirror/slider.mjs | 75 +++++++++------------------------- 1 file changed, 19 insertions(+), 56 deletions(-) diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index d4fbcefa..a4ee6a93 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -6,7 +6,7 @@ export let sliderValues = {}; const getSliderID = (from) => `slider_${from}`; export class SliderWidget extends WidgetType { - constructor(value, min, max, from, to) { + constructor(value, min, max, from, to, view) { super(); this.value = value; this.min = min; @@ -14,6 +14,7 @@ export class SliderWidget extends WidgetType { this.from = from; this.originalFrom = from; this.to = to; + this.view = view; } eq() { @@ -38,11 +39,23 @@ export class SliderWidget extends WidgetType { slider.to = this.to; slider.className = 'w-16 translate-y-1'; this.slider = slider; + slider.addEventListener('input', (e) => { + const next = e.target.value; + let insert = next; + //let insert = next.toFixed(2); + const to = slider.from + slider.originalValue.length; + let change = { from: slider.from, to, insert }; + slider.originalValue = insert; + slider.value = insert; + this.view.dispatch({ changes: change }); + const id = getSliderID(slider.originalFrom); // matches id generated in transpiler + window.postMessage({ type: 'cm-slider', value: Number(next), id }); + }); return wrap; } - ignoreEvent() { - return false; + ignoreEvent(e) { + return true; } } @@ -52,12 +65,10 @@ export const updateWidgets = (view, widgets) => { view.dispatch({ effects: setWidgets.of(widgets) }); }; -let draggedSlider; - -function getWidgets(widgetConfigs) { +function getWidgets(widgetConfigs, view) { return widgetConfigs.map(({ from, to, value, min, max }) => { return Decoration.widget({ - widget: new SliderWidget(value, min, max, from, to), + widget: new SliderWidget(value, min, max, from, to, view), side: 0, }).range(from /* , to */); }); @@ -86,7 +97,7 @@ export const sliderPlugin = ViewPlugin.fromClass( } for (let e of tr.effects) { if (e.is(setWidgets)) { - this.decorations = Decoration.set(getWidgets(e.value)); + this.decorations = Decoration.set(getWidgets(e.value, update.view)); } } }); @@ -94,57 +105,9 @@ export const sliderPlugin = ViewPlugin.fromClass( }, { decorations: (v) => v.decorations, - - eventHandlers: { - mousedown: (e, view) => { - let target = e.target; - if (target.nodeName == 'INPUT' && target.parentElement.classList.contains('cm-slider')) { - e.preventDefault(); - e.stopPropagation(); - draggedSlider = target; - // remember offsetLeft / clientWidth, as they will vanish inside mousemove events for some reason - draggedSlider._offsetLeft = draggedSlider.offsetLeft; - draggedSlider._clientWidth = draggedSlider.clientWidth; - return updateSliderValue(view, e); - } - }, - mouseup: () => { - draggedSlider = undefined; - }, - mousemove: (e, view) => { - draggedSlider && updateSliderValue(view, e); - }, - }, }, ); -// moves slider on mouse event -function updateSliderValue(view, e) { - const mouseX = e.clientX; - let mx = 10; - let progress = (mouseX - draggedSlider._offsetLeft - mx) / (draggedSlider._clientWidth - mx * 2); - progress = Math.max(Math.min(1, progress), 0); - let min = Number(draggedSlider.min); - let max = Number(draggedSlider.max); - const next = Number(progress * (max - min) + min); - let insert = next.toFixed(2); - //let before = view.state.doc.sliceString(draggedSlider.from, draggedSlider.to).trim(); - let before = draggedSlider.originalValue; - before = Number(before).toFixed(2); - // console.log('before', before, 'insert', insert, 'v'); - if (before === insert) { - return false; - } - const to = draggedSlider.from + draggedSlider.originalValue.length; - let change = { from: draggedSlider.from, to, insert }; - draggedSlider.originalValue = insert; - draggedSlider.value = insert; - view.dispatch({ changes: change }); - const id = getSliderID(draggedSlider.originalFrom); // matches id generated in transpiler - window.postMessage({ type: 'cm-slider', value: next, id }); - return true; -} - // user api export let slider = (id, value, min, max) => { sliderValues[id] = value; // sync state at eval time (code -> state) From f84d5ba3a02d7d498cef44df7baaee654ab378b0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 13:44:41 +0200 Subject: [PATCH 19/48] add back some margin --- packages/codemirror/slider.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index a4ee6a93..47bb7509 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -37,7 +37,7 @@ export class SliderWidget extends WidgetType { slider.from = this.from; slider.originalFrom = this.originalFrom; slider.to = this.to; - slider.className = 'w-16 translate-y-1'; + slider.className = 'w-16 translate-y-1 mr-1'; this.slider = slider; slider.addEventListener('input', (e) => { const next = e.target.value; From d4bf358eae42ccbd78e1c6b9df81f9f1bba44d8c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 13:56:57 +0200 Subject: [PATCH 20/48] use raw css instead of tailwind --- packages/codemirror/slider.mjs | 2 +- website/tailwind.config.cjs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index 47bb7509..e7be78f7 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -37,7 +37,7 @@ export class SliderWidget extends WidgetType { slider.from = this.from; slider.originalFrom = this.originalFrom; slider.to = this.to; - slider.className = 'w-16 translate-y-1 mr-1'; + slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)'; this.slider = slider; slider.addEventListener('input', (e) => { const next = e.target.value; diff --git a/website/tailwind.config.cjs b/website/tailwind.config.cjs index 2c682d7d..d92d5949 100644 --- a/website/tailwind.config.cjs +++ b/website/tailwind.config.cjs @@ -7,7 +7,6 @@ module.exports = { content: [ './src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}', '../packages/react/src/**/*.{html,js,jsx,md,mdx,ts,tsx}', - '../packages/codemirror/slider.mjs', ], theme: { extend: { From 564697e175e99ffea7430db1bb6d48d1757bf284 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 14:05:16 +0200 Subject: [PATCH 21/48] add extra sliderWithID function + add warning to slider function --- packages/codemirror/slider.mjs | 8 ++++++-- packages/transpiler/transpiler.mjs | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index e7be78f7..159e4fd1 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -108,8 +108,12 @@ export const sliderPlugin = ViewPlugin.fromClass( }, ); -// user api -export let slider = (id, value, min, max) => { +export let slider = (value) => { + console.warn('slider will only work when the transpiler is used... passing value as is'); + return pure(value); +}; +// function transpiled from slider = (value, min, max) +export let sliderWithID = (id, value, min, max) => { sliderValues[id] = value; // sync state at eval time (code -> state) return ref(() => sliderValues[id]); // use state at query time }; diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 5fa9dc49..37a937bc 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -121,5 +121,6 @@ function widgetWithLocation(node) { value: id, raw: id, }); + node.callee.name = 'sliderWithID'; return node; } From c051a1249d7a6adede3817f85cb3fa0992e33ba5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 14:06:32 +0200 Subject: [PATCH 22/48] remove checkbox --- packages/codemirror/checkbox.mjs | 87 ------------------- packages/react/src/components/CodeMirror6.jsx | 3 +- 2 files changed, 1 insertion(+), 89 deletions(-) delete mode 100644 packages/codemirror/checkbox.mjs diff --git a/packages/codemirror/checkbox.mjs b/packages/codemirror/checkbox.mjs deleted file mode 100644 index 279e2eb8..00000000 --- a/packages/codemirror/checkbox.mjs +++ /dev/null @@ -1,87 +0,0 @@ -import { WidgetType } from '@codemirror/view'; -import { ViewPlugin, Decoration } from '@codemirror/view'; -import { syntaxTree } from '@codemirror/language'; - -export class CheckboxWidget extends WidgetType { - constructor(checked) { - super(); - this.checked = checked; - } - - eq(other) { - return other.checked == this.checked; - } - - toDOM() { - let wrap = document.createElement('span'); - wrap.setAttribute('aria-hidden', 'true'); - wrap.className = 'cm-boolean-toggle'; - let box = wrap.appendChild(document.createElement('input')); - box.type = 'checkbox'; - box.checked = this.checked; - return wrap; - } - - ignoreEvent() { - return false; - } -} - -// EditorView -export function checkboxes(view) { - let widgets = []; - for (let { from, to } of view.visibleRanges) { - syntaxTree(view.state).iterate({ - from, - to, - enter: (node) => { - if (node.name == 'BooleanLiteral') { - let isTrue = view.state.doc.sliceString(node.from, node.to) == 'true'; - let deco = Decoration.widget({ - widget: new CheckboxWidget(isTrue), - side: 1, - }); - widgets.push(deco.range(node.from)); - } - }, - }); - } - return Decoration.set(widgets); -} - -export const checkboxPlugin = ViewPlugin.fromClass( - class { - decorations; //: DecorationSet - - constructor(view /* : EditorView */) { - this.decorations = checkboxes(view); - } - - update(update /* : ViewUpdate */) { - if (update.docChanged || update.viewportChanged) this.decorations = checkboxes(update.view); - } - }, - { - decorations: (v) => v.decorations, - - eventHandlers: { - mousedown: (e, view) => { - let target = e.target; /* as HTMLElement */ - if (target.nodeName == 'INPUT' && target.parentElement.classList.contains('cm-boolean-toggle')) - return toggleBoolean(view, view.posAtDOM(target)); - }, - }, - }, -); - -function toggleBoolean(view /* : EditorView */, pos /* : number */) { - let before = view.state.doc.sliceString(Math.max(0, pos), pos + 5).trim(); - let change; - if (!['true', 'false'].includes(before)) { - return false; - } - let insert = before === 'true' ? 'false' : 'true'; - change = { from: pos, to: pos + before.length, insert }; - view.dispatch({ changes: change }); - return true; -} diff --git a/packages/react/src/components/CodeMirror6.jsx b/packages/react/src/components/CodeMirror6.jsx index f3c764e0..a5af5312 100644 --- a/packages/react/src/components/CodeMirror6.jsx +++ b/packages/react/src/components/CodeMirror6.jsx @@ -15,12 +15,11 @@ import { updateMiniLocations, } from '@strudel/codemirror'; import './style.css'; -import { checkboxPlugin } from '@strudel/codemirror/checkbox.mjs'; import { sliderPlugin } from '@strudel/codemirror/slider.mjs'; export { flash, highlightMiniLocations, updateMiniLocations }; -const staticExtensions = [javascript(), flashField, highlightExtension, checkboxPlugin, sliderPlugin]; +const staticExtensions = [javascript(), flashField, highlightExtension, sliderPlugin]; export default function CodeMirror({ value, From 21b99b3810eeb9b114d8a39f83faab8c1bd97f40 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 14:07:48 +0200 Subject: [PATCH 23/48] remove comment --- packages/transpiler/transpiler.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 37a937bc..dced458f 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -36,7 +36,6 @@ export function transpiler(input, options = {}) { return this.replace(miniWithLocation(value, node)); } if (isWidgetFunction(node)) { - // collectSliderLocations? emitWidgets && widgets.push({ from: node.arguments[0].start, From 935d8e8aea53362c247d9e3541ece5105769fa0d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 14:08:41 +0200 Subject: [PATCH 24/48] fix: comment --- packages/transpiler/transpiler.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index dced458f..6eac171f 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -114,7 +114,7 @@ function isWidgetFunction(node) { function widgetWithLocation(node) { const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id // add loc as identifier to first argument - // the slider function is assumed to be slider(id, value, min?, max?) + // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) node.arguments.unshift({ type: 'Literal', value: id, From 96f06d802644c4ec1175328ec893d0df10f345c4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 14:09:07 +0200 Subject: [PATCH 25/48] fix: import --- packages/codemirror/slider.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index 159e4fd1..47b686c8 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -1,4 +1,4 @@ -import { ref } from '@strudel.cycles/core'; +import { ref, pure } from '@strudel.cycles/core'; import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view'; import { StateEffect, StateField } from '@codemirror/state'; From edbd437d7b8076a9ce3b020a6900b9e6a043d0ed Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 14:20:30 +0200 Subject: [PATCH 26/48] hotfix: add missing dependency --- pnpm-lock.yaml | 18 ++++++++++++++++++ website/package.json | 1 + 2 files changed, 19 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b48a93f..9f222a20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -588,6 +588,9 @@ importers: '@strudel.cycles/xen': specifier: workspace:* version: link:../packages/xen + '@strudel/codemirror': + specifier: workspace:* + version: link:../packages/codemirror '@strudel/desktopbridge': specifier: workspace:* version: link:../packages/desktopbridge @@ -1426,6 +1429,7 @@ packages: /@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.21.5): resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -1441,6 +1445,7 @@ packages: /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.21.5): resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -1454,6 +1459,7 @@ packages: /@babel/plugin-proposal-class-static-block@7.20.7(@babel/core@7.21.5): resolution: {integrity: sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead. peerDependencies: '@babel/core': ^7.12.0 dependencies: @@ -1468,6 +1474,7 @@ packages: /@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.21.5): resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -1479,6 +1486,7 @@ packages: /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.21.5): resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -1490,6 +1498,7 @@ packages: /@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.21.5): resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -1501,6 +1510,7 @@ packages: /@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.21.5): resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -1512,6 +1522,7 @@ packages: /@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.21.5): resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -1523,6 +1534,7 @@ packages: /@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.21.5): resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -1534,6 +1546,7 @@ packages: /@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.21.5): resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -1548,6 +1561,7 @@ packages: /@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.21.5): resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -1559,6 +1573,7 @@ packages: /@babel/plugin-proposal-optional-chaining@7.20.7(@babel/core@7.21.5): resolution: {integrity: sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -1571,6 +1586,7 @@ packages: /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.21.5): resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -1584,6 +1600,7 @@ packages: /@babel/plugin-proposal-private-property-in-object@7.20.5(@babel/core@7.21.5): resolution: {integrity: sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -1599,6 +1616,7 @@ packages: /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.21.5): resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} engines: {node: '>=4'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: diff --git a/website/package.json b/website/package.json index 6ac799d3..c8fec20c 100644 --- a/website/package.json +++ b/website/package.json @@ -28,6 +28,7 @@ "@strudel.cycles/mini": "workspace:*", "@strudel.cycles/osc": "workspace:*", "@strudel.cycles/react": "workspace:*", + "@strudel/codemirror": "workspace:*", "@strudel.cycles/serial": "workspace:*", "@strudel.cycles/soundfonts": "workspace:*", "@strudel.cycles/tonal": "workspace:*", From fd316c81c0b9529a25c8d08ede3f4f2c3ccce6c2 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Sun, 1 Oct 2023 13:20:49 +0100 Subject: [PATCH 27/48] support mininotation '..' range operator, fixes #715 (#716) * support mininotation .. range operator, fixes #715 * remove logs --- packages/mini/krill-parser.js | 437 +++++++++++++++++-------------- packages/mini/krill.pegjs | 8 +- packages/mini/mini.mjs | 11 + packages/mini/test/mini.test.mjs | 6 + 4 files changed, 270 insertions(+), 192 deletions(-) diff --git a/packages/mini/krill-parser.js b/packages/mini/krill-parser.js index e85e3363..7831087b 100644 --- a/packages/mini/krill-parser.js +++ b/packages/mini/krill-parser.js @@ -200,20 +200,21 @@ function peg$parse(input, options) { var peg$c23 = "*"; var peg$c24 = "?"; var peg$c25 = ":"; - var peg$c26 = "struct"; - var peg$c27 = "target"; - var peg$c28 = "euclid"; - var peg$c29 = "slow"; - var peg$c30 = "rotL"; - var peg$c31 = "rotR"; - var peg$c32 = "fast"; - var peg$c33 = "scale"; - var peg$c34 = "//"; - var peg$c35 = "cat"; - var peg$c36 = "$"; - var peg$c37 = "setcps"; - var peg$c38 = "setbpm"; - var peg$c39 = "hush"; + var peg$c26 = ".."; + var peg$c27 = "struct"; + var peg$c28 = "target"; + var peg$c29 = "euclid"; + var peg$c30 = "slow"; + var peg$c31 = "rotL"; + var peg$c32 = "rotR"; + var peg$c33 = "fast"; + var peg$c34 = "scale"; + var peg$c35 = "//"; + var peg$c36 = "cat"; + var peg$c37 = "$"; + var peg$c38 = "setcps"; + var peg$c39 = "setbpm"; + var peg$c40 = "hush"; var peg$r0 = /^[1-9]/; var peg$r1 = /^[eE]/; @@ -255,64 +256,67 @@ function peg$parse(input, options) { var peg$e30 = peg$literalExpectation("*", false); var peg$e31 = peg$literalExpectation("?", false); var peg$e32 = peg$literalExpectation(":", false); - var peg$e33 = peg$literalExpectation("struct", false); - var peg$e34 = peg$literalExpectation("target", false); - var peg$e35 = peg$literalExpectation("euclid", false); - var peg$e36 = peg$literalExpectation("slow", false); - var peg$e37 = peg$literalExpectation("rotL", false); - var peg$e38 = peg$literalExpectation("rotR", false); - var peg$e39 = peg$literalExpectation("fast", false); - var peg$e40 = peg$literalExpectation("scale", false); - var peg$e41 = peg$literalExpectation("//", false); - var peg$e42 = peg$classExpectation(["\n"], true, false); - var peg$e43 = peg$literalExpectation("cat", false); - var peg$e44 = peg$literalExpectation("$", false); - var peg$e45 = peg$literalExpectation("setcps", false); - var peg$e46 = peg$literalExpectation("setbpm", false); - var peg$e47 = peg$literalExpectation("hush", false); + var peg$e33 = peg$literalExpectation("..", false); + var peg$e34 = peg$literalExpectation("struct", false); + var peg$e35 = peg$literalExpectation("target", false); + var peg$e36 = peg$literalExpectation("euclid", false); + var peg$e37 = peg$literalExpectation("slow", false); + var peg$e38 = peg$literalExpectation("rotL", false); + var peg$e39 = peg$literalExpectation("rotR", false); + var peg$e40 = peg$literalExpectation("fast", false); + var peg$e41 = peg$literalExpectation("scale", false); + var peg$e42 = peg$literalExpectation("//", false); + var peg$e43 = peg$classExpectation(["\n"], true, false); + var peg$e44 = peg$literalExpectation("cat", false); + var peg$e45 = peg$literalExpectation("$", false); + var peg$e46 = peg$literalExpectation("setcps", false); + var peg$e47 = peg$literalExpectation("setbpm", false); + var peg$e48 = peg$literalExpectation("hush", false); var peg$f0 = function() { return parseFloat(text()); }; - var peg$f1 = function(chars) { return new AtomStub(chars.join("")) }; - var peg$f2 = function(s) { return s }; - var peg$f3 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; }; - var peg$f4 = function(a) { return a }; - var peg$f5 = function(s) { s.arguments_.alignment = 'slowcat'; return s; }; - var peg$f6 = function(a) { return x => x.options_['weight'] = a }; - var peg$f7 = function(a) { return x => x.options_['reps'] = a }; - var peg$f8 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }; - var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) }; - var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }; - var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) }; - var peg$f12 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }; - var peg$f13 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1}); + var peg$f1 = function() { return parseInt(text()); }; + var peg$f2 = function(chars) { return new AtomStub(chars.join("")) }; + var peg$f3 = function(s) { return s }; + var peg$f4 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; }; + var peg$f5 = function(a) { return a }; + var peg$f6 = function(s) { s.arguments_.alignment = 'slowcat'; return s; }; + var peg$f7 = function(a) { return x => x.options_['weight'] = a }; + var peg$f8 = function(a) { return x => x.options_['reps'] = a }; + var peg$f9 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }; + var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) }; + var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }; + var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) }; + var peg$f13 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }; + var peg$f14 = function(s) { return x => x.options_['ops'].push({ type_: "range", arguments_ :{ element:s } }) }; + var peg$f15 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1}); for (const op of ops) { op(result); } return result; }; - var peg$f14 = function(s) { return new PatternStub(s, 'fastcat'); }; - var peg$f15 = function(tail) { return { alignment: 'stack', list: tail }; }; - var peg$f16 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; }; - var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }; - var peg$f18 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }; - var peg$f19 = function(sc) { return sc; }; - var peg$f20 = function(s) { return { name: "struct", args: { mini:s }}}; - var peg$f21 = function(s) { return { name: "target", args : { name:s}}}; - var peg$f22 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}}; - var peg$f23 = function(a) { return { name: "stretch", args :{ amount: a}}}; - var peg$f24 = function(a) { return { name: "shift", args :{ amount: "-"+a}}}; - var peg$f25 = function(a) { return { name: "shift", args :{ amount: a}}}; - var peg$f26 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}}; - var peg$f27 = function(s) { return { name: "scale", args :{ scale: s.join("")}}}; - var peg$f28 = function(s, v) { return v}; - var peg$f29 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); }; - var peg$f30 = function(sg) {return sg}; - var peg$f31 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)}; - var peg$f32 = function(sc) { return sc }; - var peg$f33 = function(c) { return c }; - var peg$f34 = function(v) { return new CommandStub("setcps", { value: v})}; - var peg$f35 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})}; - var peg$f36 = function() { return new CommandStub("hush")}; + var peg$f16 = function(s) { return new PatternStub(s, 'fastcat'); }; + var peg$f17 = function(tail) { return { alignment: 'stack', list: tail }; }; + var peg$f18 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; }; + var peg$f19 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }; + var peg$f20 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }; + var peg$f21 = function(sc) { return sc; }; + var peg$f22 = function(s) { return { name: "struct", args: { mini:s }}}; + var peg$f23 = function(s) { return { name: "target", args : { name:s}}}; + var peg$f24 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}}; + var peg$f25 = function(a) { return { name: "stretch", args :{ amount: a}}}; + var peg$f26 = function(a) { return { name: "shift", args :{ amount: "-"+a}}}; + var peg$f27 = function(a) { return { name: "shift", args :{ amount: a}}}; + var peg$f28 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}}; + var peg$f29 = function(s) { return { name: "scale", args :{ scale: s.join("")}}}; + var peg$f30 = function(s, v) { return v}; + var peg$f31 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); }; + var peg$f32 = function(sg) {return sg}; + var peg$f33 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)}; + var peg$f34 = function(sc) { return sc }; + var peg$f35 = function(c) { return c }; + var peg$f36 = function(v) { return new CommandStub("setcps", { value: v})}; + var peg$f37 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})}; + var peg$f38 = function() { return new CommandStub("hush")}; var peg$currPos = 0; var peg$savedPos = 0; var peg$posDetailsCache = [{ line: 1, column: 1 }]; @@ -651,6 +655,26 @@ function peg$parse(input, options) { return s0; } + function peg$parseintneg() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$parseminus(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parseint(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f1(); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + function peg$parseminus() { var s0; @@ -884,7 +908,7 @@ function peg$parse(input, options) { if (s2 !== peg$FAILED) { s3 = peg$parsews(); peg$savedPos = s0; - s0 = peg$f1(s2); + s0 = peg$f2(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -920,7 +944,7 @@ function peg$parse(input, options) { if (s6 !== peg$FAILED) { s7 = peg$parsews(); peg$savedPos = s0; - s0 = peg$f2(s4); + s0 = peg$f3(s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -968,7 +992,7 @@ function peg$parse(input, options) { } s8 = peg$parsews(); peg$savedPos = s0; - s0 = peg$f3(s4, s7); + s0 = peg$f4(s4, s7); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1000,7 +1024,7 @@ function peg$parse(input, options) { s2 = peg$parseslice(); if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f4(s2); + s0 = peg$f5(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1040,7 +1064,7 @@ function peg$parse(input, options) { if (s6 !== peg$FAILED) { s7 = peg$parsews(); peg$savedPos = s0; - s0 = peg$f5(s4); + s0 = peg$f6(s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1090,6 +1114,9 @@ function peg$parse(input, options) { s0 = peg$parseop_degrade(); if (s0 === peg$FAILED) { s0 = peg$parseop_tail(); + if (s0 === peg$FAILED) { + s0 = peg$parseop_range(); + } } } } @@ -1115,7 +1142,7 @@ function peg$parse(input, options) { s2 = peg$parsenumber(); if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f6(s2); + s0 = peg$f7(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1143,7 +1170,7 @@ function peg$parse(input, options) { s2 = peg$parsenumber(); if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f7(s2); + s0 = peg$f8(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1197,7 +1224,7 @@ function peg$parse(input, options) { } if (s13 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f8(s3, s7, s11); + s0 = peg$f9(s3, s7, s11); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1237,7 +1264,7 @@ function peg$parse(input, options) { s2 = peg$parseslice(); if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f9(s2); + s0 = peg$f10(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1265,7 +1292,7 @@ function peg$parse(input, options) { s2 = peg$parseslice(); if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f10(s2); + s0 = peg$f11(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1295,7 +1322,7 @@ function peg$parse(input, options) { s2 = null; } peg$savedPos = s0; - s0 = peg$f11(s2); + s0 = peg$f12(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1319,7 +1346,35 @@ function peg$parse(input, options) { s2 = peg$parseslice(); if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f12(s2); + s0 = peg$f13(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseop_range() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c26) { + s1 = peg$c26; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e33); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseslice(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f14(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1345,7 +1400,7 @@ function peg$parse(input, options) { s3 = peg$parseslice_op(); } peg$savedPos = s0; - s0 = peg$f13(s1, s2); + s0 = peg$f15(s1, s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1370,7 +1425,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f14(s1); + s1 = peg$f16(s1); } s0 = s1; @@ -1419,7 +1474,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f15(s1); + s1 = peg$f17(s1); } s0 = s1; @@ -1468,7 +1523,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f16(s1); + s1 = peg$f18(s1); } s0 = s1; @@ -1489,7 +1544,7 @@ function peg$parse(input, options) { s2 = null; } peg$savedPos = s0; - s0 = peg$f17(s1, s2); + s0 = peg$f19(s1, s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1509,7 +1564,7 @@ function peg$parse(input, options) { s2 = null; } peg$savedPos = s0; - s0 = peg$f18(s1, s2); + s0 = peg$f20(s1, s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1532,7 +1587,7 @@ function peg$parse(input, options) { s6 = peg$parsequote(); if (s6 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f19(s4); + s0 = peg$f21(s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1582,19 +1637,19 @@ function peg$parse(input, options) { var s0, s1, s2, s3; s0 = peg$currPos; - if (input.substr(peg$currPos, 6) === peg$c26) { - s1 = peg$c26; + if (input.substr(peg$currPos, 6) === peg$c27) { + s1 = peg$c27; peg$currPos += 6; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e33); } + if (peg$silentFails === 0) { peg$fail(peg$e34); } } if (s1 !== peg$FAILED) { s2 = peg$parsews(); s3 = peg$parsemini_or_operator(); if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f20(s3); + s0 = peg$f22(s3); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1611,12 +1666,12 @@ function peg$parse(input, options) { var s0, s1, s2, s3, s4, s5; s0 = peg$currPos; - if (input.substr(peg$currPos, 6) === peg$c27) { - s1 = peg$c27; + if (input.substr(peg$currPos, 6) === peg$c28) { + s1 = peg$c28; peg$currPos += 6; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e34); } + if (peg$silentFails === 0) { peg$fail(peg$e35); } } if (s1 !== peg$FAILED) { s2 = peg$parsews(); @@ -1627,7 +1682,7 @@ function peg$parse(input, options) { s5 = peg$parsequote(); if (s5 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f21(s4); + s0 = peg$f23(s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1652,12 +1707,12 @@ function peg$parse(input, options) { var s0, s1, s2, s3, s4, s5, s6, s7; s0 = peg$currPos; - if (input.substr(peg$currPos, 6) === peg$c28) { - s1 = peg$c28; + if (input.substr(peg$currPos, 6) === peg$c29) { + s1 = peg$c29; peg$currPos += 6; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e35); } + if (peg$silentFails === 0) { peg$fail(peg$e36); } } if (s1 !== peg$FAILED) { s2 = peg$parsews(); @@ -1672,7 +1727,7 @@ function peg$parse(input, options) { s7 = null; } peg$savedPos = s0; - s0 = peg$f22(s3, s5, s7); + s0 = peg$f24(s3, s5, s7); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1692,35 +1747,6 @@ function peg$parse(input, options) { function peg$parseslow() { var s0, s1, s2, s3; - s0 = peg$currPos; - if (input.substr(peg$currPos, 4) === peg$c29) { - s1 = peg$c29; - peg$currPos += 4; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e36); } - } - if (s1 !== peg$FAILED) { - s2 = peg$parsews(); - s3 = peg$parsenumber(); - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s0 = peg$f23(s3); - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - return s0; - } - - function peg$parserotL() { - var s0, s1, s2, s3; - s0 = peg$currPos; if (input.substr(peg$currPos, 4) === peg$c30) { s1 = peg$c30; @@ -1729,35 +1755,6 @@ function peg$parse(input, options) { s1 = peg$FAILED; if (peg$silentFails === 0) { peg$fail(peg$e37); } } - if (s1 !== peg$FAILED) { - s2 = peg$parsews(); - s3 = peg$parsenumber(); - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s0 = peg$f24(s3); - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - return s0; - } - - function peg$parserotR() { - var s0, s1, s2, s3; - - s0 = peg$currPos; - if (input.substr(peg$currPos, 4) === peg$c31) { - s1 = peg$c31; - peg$currPos += 4; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e38); } - } if (s1 !== peg$FAILED) { s2 = peg$parsews(); s3 = peg$parsenumber(); @@ -1776,16 +1773,16 @@ function peg$parse(input, options) { return s0; } - function peg$parsefast() { + function peg$parserotL() { var s0, s1, s2, s3; s0 = peg$currPos; - if (input.substr(peg$currPos, 4) === peg$c32) { - s1 = peg$c32; + if (input.substr(peg$currPos, 4) === peg$c31) { + s1 = peg$c31; peg$currPos += 4; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e39); } + if (peg$silentFails === 0) { peg$fail(peg$e38); } } if (s1 !== peg$FAILED) { s2 = peg$parsews(); @@ -1805,16 +1802,74 @@ function peg$parse(input, options) { return s0; } + function peg$parserotR() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c32) { + s1 = peg$c32; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e39); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsews(); + s3 = peg$parsenumber(); + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f27(s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsefast() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c33) { + s1 = peg$c33; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e40); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsews(); + s3 = peg$parsenumber(); + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f28(s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + function peg$parsescale() { var s0, s1, s2, s3, s4, s5; s0 = peg$currPos; - if (input.substr(peg$currPos, 5) === peg$c33) { - s1 = peg$c33; + if (input.substr(peg$currPos, 5) === peg$c34) { + s1 = peg$c34; peg$currPos += 5; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e40); } + if (peg$silentFails === 0) { peg$fail(peg$e41); } } if (s1 !== peg$FAILED) { s2 = peg$parsews(); @@ -1834,7 +1889,7 @@ function peg$parse(input, options) { s5 = peg$parsequote(); if (s5 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f27(s4); + s0 = peg$f29(s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1859,12 +1914,12 @@ function peg$parse(input, options) { var s0, s1, s2, s3; s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c34) { - s1 = peg$c34; + if (input.substr(peg$currPos, 2) === peg$c35) { + s1 = peg$c35; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e41); } + if (peg$silentFails === 0) { peg$fail(peg$e42); } } if (s1 !== peg$FAILED) { s2 = []; @@ -1873,7 +1928,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e42); } + if (peg$silentFails === 0) { peg$fail(peg$e43); } } while (s3 !== peg$FAILED) { s2.push(s3); @@ -1882,7 +1937,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e42); } + if (peg$silentFails === 0) { peg$fail(peg$e43); } } } s1 = [s1, s2]; @@ -1899,12 +1954,12 @@ function peg$parse(input, options) { var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9; s0 = peg$currPos; - if (input.substr(peg$currPos, 3) === peg$c35) { - s1 = peg$c35; + if (input.substr(peg$currPos, 3) === peg$c36) { + s1 = peg$c36; peg$currPos += 3; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e43); } + if (peg$silentFails === 0) { peg$fail(peg$e44); } } if (s1 !== peg$FAILED) { s2 = peg$parsews(); @@ -1926,7 +1981,7 @@ function peg$parse(input, options) { s9 = peg$parsemini_or_operator(); if (s9 !== peg$FAILED) { peg$savedPos = s7; - s7 = peg$f28(s5, s9); + s7 = peg$f30(s5, s9); } else { peg$currPos = s7; s7 = peg$FAILED; @@ -1943,7 +1998,7 @@ function peg$parse(input, options) { s9 = peg$parsemini_or_operator(); if (s9 !== peg$FAILED) { peg$savedPos = s7; - s7 = peg$f28(s5, s9); + s7 = peg$f30(s5, s9); } else { peg$currPos = s7; s7 = peg$FAILED; @@ -1963,7 +2018,7 @@ function peg$parse(input, options) { } if (s8 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f29(s5, s6); + s0 = peg$f31(s5, s6); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2009,7 +2064,7 @@ function peg$parse(input, options) { s4 = peg$parsecomment(); } peg$savedPos = s0; - s0 = peg$f30(s1); + s0 = peg$f32(s1); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2020,18 +2075,18 @@ function peg$parse(input, options) { if (s1 !== peg$FAILED) { s2 = peg$parsews(); if (input.charCodeAt(peg$currPos) === 36) { - s3 = peg$c36; + s3 = peg$c37; peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e44); } + if (peg$silentFails === 0) { peg$fail(peg$e45); } } if (s3 !== peg$FAILED) { s4 = peg$parsews(); s5 = peg$parsemini_or_operator(); if (s5 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f31(s1, s5); + s0 = peg$f33(s1, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2056,7 +2111,7 @@ function peg$parse(input, options) { s1 = peg$parsemini_or_operator(); if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f32(s1); + s1 = peg$f34(s1); } s0 = s1; if (s0 === peg$FAILED) { @@ -2089,7 +2144,7 @@ function peg$parse(input, options) { if (s2 !== peg$FAILED) { s3 = peg$parsews(); peg$savedPos = s0; - s0 = peg$f33(s2); + s0 = peg$f35(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2102,19 +2157,19 @@ function peg$parse(input, options) { var s0, s1, s2, s3; s0 = peg$currPos; - if (input.substr(peg$currPos, 6) === peg$c37) { - s1 = peg$c37; + if (input.substr(peg$currPos, 6) === peg$c38) { + s1 = peg$c38; peg$currPos += 6; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e45); } + if (peg$silentFails === 0) { peg$fail(peg$e46); } } if (s1 !== peg$FAILED) { s2 = peg$parsews(); s3 = peg$parsenumber(); if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f34(s3); + s0 = peg$f36(s3); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2131,19 +2186,19 @@ function peg$parse(input, options) { var s0, s1, s2, s3; s0 = peg$currPos; - if (input.substr(peg$currPos, 6) === peg$c38) { - s1 = peg$c38; + if (input.substr(peg$currPos, 6) === peg$c39) { + s1 = peg$c39; peg$currPos += 6; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e46); } + if (peg$silentFails === 0) { peg$fail(peg$e47); } } if (s1 !== peg$FAILED) { s2 = peg$parsews(); s3 = peg$parsenumber(); if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f35(s3); + s0 = peg$f37(s3); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2160,16 +2215,16 @@ function peg$parse(input, options) { var s0, s1; s0 = peg$currPos; - if (input.substr(peg$currPos, 4) === peg$c39) { - s1 = peg$c39; + if (input.substr(peg$currPos, 4) === peg$c40) { + s1 = peg$c40; peg$currPos += 4; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e47); } + if (peg$silentFails === 0) { peg$fail(peg$e48); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f36(); + s1 = peg$f38(); } s0 = s1; diff --git a/packages/mini/krill.pegjs b/packages/mini/krill.pegjs index 72b453be..f52743bf 100644 --- a/packages/mini/krill.pegjs +++ b/packages/mini/krill.pegjs @@ -79,6 +79,9 @@ frac int = zero / (digit1_9 DIGIT*) +intneg + = minus? int { return parseInt(text()); } + minus = "-" @@ -123,7 +126,7 @@ slice = step / sub_cycle / polymeter / slow_sequence // slice modifier affects the timing/size of a slice (e.g. [a b c]@3) // at this point, we assume we can represent them as regular sequence operators -slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail +slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail / op_range op_weight = "@" a:number { return x => x.options_['weight'] = a } @@ -146,6 +149,9 @@ op_degrade = "?"a:number? op_tail = ":" s:slice { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) } +op_range = ".." s:slice + { return x => x.options_['ops'].push({ type_: "range", arguments_ :{ element:s } }) } + // a slice with an modifier applied i.e [bd@4 sd@3]@2 hh] slice_with_ops = s:slice ops:slice_op* { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1}); diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 3321e7bc..8e6b844f 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -45,6 +45,17 @@ const applyOptions = (parent, enter) => (pat, i) => { pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend); break; } + case 'range': { + const friend = enter(op.arguments_.element); + pat = strudel.reify(pat); + const arrayRange = (start, stop, step = 1) => + Array.from({ length: Math.abs(stop - start) / step + 1 }, (value, index) => + start < stop ? start + index * step : start - index * step, + ); + let range = (apat, bpat) => apat.squeezeBind((a) => bpat.bind((b) => strudel.fastcat(...arrayRange(a, b)))); + pat = range(pat, friend); + break; + } default: { console.warn(`operator "${op.type_}" not implemented`); } diff --git a/packages/mini/test/mini.test.mjs b/packages/mini/test/mini.test.mjs index 0c7f381e..6d9ac367 100644 --- a/packages/mini/test/mini.test.mjs +++ b/packages/mini/test/mini.test.mjs @@ -184,6 +184,12 @@ describe('mini', () => { it('supports lists', () => { expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']); }); + it('supports ranges', () => { + expect(minV('0 .. 4')).toEqual([0, 1, 2, 3, 4]); + }); + it('supports patterned ranges', () => { + expect(minS('[<0 1> .. <2 4>]*2')).toEqual(minS('[0 1 2] [1 2 3 4]')); + }); }); describe('getLeafLocation', () => { From 2d07eeb518e9b200bae4e38b818808ef2a388849 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 1 Oct 2023 14:44:34 +0200 Subject: [PATCH 28/48] Connecting all parameters to convolution generator --- packages/superdough/reverb.mjs | 5 ++++- packages/superdough/superdough.mjs | 20 ++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs index 4c5bc1d1..54019fc2 100644 --- a/packages/superdough/reverb.mjs +++ b/packages/superdough/reverb.mjs @@ -26,8 +26,11 @@ if (typeof AudioContext !== 'undefined') { } ); convolver.duration = d; + convolver.fade = fade; + convolver.revlp = revlp; + convolver.revdim = revdim; }; - convolver.setDuration(duration); + convolver.setDuration(duration, fade, revlp, revdim); return convolver; }; } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 607c649d..0d08c8ad 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -118,17 +118,29 @@ function getReverb(orbit, duration = 2, fade, revlp, revdim) { duration, fade, revlp, + revdim, ); reverb.connect(getDestination()); - console.log(reverb) reverbs[orbit] = reverb; + console.log(reverbs[orbit]); } - // Update the reverb duration if needed after instanciation - if (reverbs[orbit].duration !== duration) { + + if ( + reverbs[orbit].duration !== duration || + reverbs[orbit].fade !== fade || + reverbs[orbit].revlp !== revlp || + reverbs[orbit].revdim !== revdim + ) { reverbs[orbit] = reverbs[orbit].setDuration( - duration, fade, revlp, revdim); + duration, fade, revlp, revdim + ); reverbs[orbit].duration = duration; + reverbs[orbit].fade = fade; + reverbs[orbit].revlp = revlp; + reverbs[orbit].revdim = revdim; + } + return reverbs[orbit]; } From 1909caf769d83e89fbc6ace74b464f25179fe20d Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 1 Oct 2023 14:50:29 +0200 Subject: [PATCH 29/48] Connecting new reverb documentation --- website/src/pages/learn/effects.mdx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index f77ab4c4..062a1947 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -203,4 +203,16 @@ global effects use the same chain for all events of the same orbit: +## fade + + + +## revlp + + + +## revdim + + + Next, we'll look at strudel's support for [Csound](/learn/csound). From 6ca99e33aba268442e519760e1488d9e87460ab5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 23:20:05 +0200 Subject: [PATCH 30/48] codeformat --- packages/core/controls.mjs | 4 +-- packages/superdough/reverb.mjs | 10 ++------ packages/superdough/reverbGen.mjs | 40 ++++++++++++++---------------- packages/superdough/superdough.mjs | 15 ++--------- 4 files changed, 24 insertions(+), 45 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 216b7001..e72f006d 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1197,7 +1197,7 @@ const generic_params = [ ]; // TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13 -controls.createParam = function(names) { +controls.createParam = function (names) { const name = Array.isArray(names) ? names[0] : names; var withVal; @@ -1221,7 +1221,7 @@ controls.createParam = function(names) { const func = (...pats) => sequence(...pats).withValue(withVal); - const setter = function(...pats) { + const setter = function (...pats) { if (!pats.length) { return this.fmap(withVal); } diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs index 54019fc2..7151e7fc 100644 --- a/packages/superdough/reverb.mjs +++ b/packages/superdough/reverb.mjs @@ -2,13 +2,7 @@ import reverbGen from './reverbGen.mjs'; if (typeof AudioContext !== 'undefined') { AudioContext.prototype.generateReverb = reverbGen.generateReverb; - AudioContext.prototype.createReverb = function( - audioContext, - duration, - fade, - revlp, - revdim - ) { + AudioContext.prototype.createReverb = function (audioContext, duration, fade, revlp, revdim) { const convolver = this.createConvolver(); convolver.setDuration = (d, fade, revlp, revdim) => { this.generateReverb( @@ -23,7 +17,7 @@ if (typeof AudioContext !== 'undefined') { }, (buffer) => { convolver.buffer = buffer; - } + }, ); convolver.duration = d; convolver.fade = fade; diff --git a/packages/superdough/reverbGen.mjs b/packages/superdough/reverbGen.mjs index 1d05ee82..cac5d24a 100644 --- a/packages/superdough/reverbGen.mjs +++ b/packages/superdough/reverbGen.mjs @@ -12,9 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -"use strict"; - - var reverbGen = {}; /** Generates a reverb impulse response. @@ -24,7 +21,7 @@ var reverbGen = {}; the impulse response has been generated. The impulse response is passed to this function as its parameter. May be called immediately within the current execution context, or later. */ -reverbGen.generateReverb = function(params, callback) { +reverbGen.generateReverb = function (params, callback) { var audioContext = params.audioContext || new AudioContext(); var sampleRate = params.sampleRate || 44100; var numChannels = params.numChannels || 2; @@ -42,7 +39,7 @@ reverbGen.generateReverb = function(params, callback) { chan[j] = randomSample() * Math.pow(decayBase, j); } for (var j = 0; j < fadeInSampleFrames; j++) { - chan[j] *= (j / fadeInSampleFrames); + chan[j] *= j / fadeInSampleFrames; } } @@ -57,7 +54,7 @@ reverbGen.generateReverb = function(params, callback) { @param {number} min Minimum value of data for the graph (lower edge). @param {number} max Maximum value of data in the graph (upper edge). @return {!CanvasElement} The generated canvas element. */ -reverbGen.generateGraph = function(data, width, height, min, max) { +reverbGen.generateGraph = function (data, width, height, min, max) { var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; @@ -71,7 +68,7 @@ reverbGen.generateGraph = function(data, width, height, min, max) { gc.fillRect(i * xscale, height - (data[i] - min) * yscale, 1, 1); } return canvas; -} +}; /** Saves an AudioBuffer as a 16-bit WAV file on the client's host file system. Normalizes it to peak at +-32767, and optionally @@ -85,7 +82,7 @@ reverbGen.generateGraph = function(data, width, height, min, max) { is truncated at that point. This is expressed as an integer, applying to the post-normalized and integer-converted buffer. The default is 0, meaning don't truncate. */ -reverbGen.saveWavFile = function(buffer, name, opt_minTail) { +reverbGen.saveWavFile = function (buffer, name, opt_minTail) { var bitsPerSample = 16; var bytesPerSample = 2; var sampleRate = buffer.sampleRate; @@ -124,23 +121,22 @@ reverbGen.saveWavFile = function(buffer, name, opt_minTail) { dataView.setUint32(4, fileBytes - 8, true); // file length dataView.setUint32(8, 1163280727, true); // "WAVE" dataView.setUint32(12, 544501094, true); // "fmt " - dataView.setUint32(16, 16, true) // fmt chunk length - dataView.setUint16(20, 1, true); // PCM format + dataView.setUint32(16, 16, true); // fmt chunk length + dataView.setUint16(20, 1, true); // PCM format dataView.setUint16(22, numChannels, true); // NumChannels - dataView.setUint32(24, sampleRate, true); // SampleRate + dataView.setUint32(24, sampleRate, true); // SampleRate var bytesPerSampleFrame = numChannels * bytesPerSample; dataView.setUint32(28, sampleRate * bytesPerSampleFrame, true); // ByteRate - dataView.setUint16(32, bytesPerSampleFrame, true); // BlockAlign - dataView.setUint16(34, bitsPerSample, true); // BitsPerSample - dataView.setUint32(36, 1635017060, true); // "data" + dataView.setUint16(32, bytesPerSampleFrame, true); // BlockAlign + dataView.setUint16(34, bitsPerSample, true); // BitsPerSample + dataView.setUint32(36, 1635017060, true); // "data" dataView.setUint32(40, sampleDataBytes, true); for (var j = 0; j < numSampleFrames; j++) { for (var i = 0; i < numChannels; i++) { - dataView.setInt16(44 + j * bytesPerSampleFrame + i * bytesPerSample, - Math.round(scale * channels[i][j]), true); + dataView.setInt16(44 + j * bytesPerSampleFrame + i * bytesPerSample, Math.round(scale * channels[i][j]), true); } } - var blob = new Blob([arrayBuffer], { 'type': 'audio/wav' }); + var blob = new Blob([arrayBuffer], { type: 'audio/wav' }); var url = window.URL.createObjectURL(blob); var linkEl = document.createElement('a'); linkEl.href = url; @@ -159,7 +155,7 @@ reverbGen.saveWavFile = function(buffer, name, opt_minTail) { @param {number} lpFreqEndAt @param {!function(!AudioBuffer)} callback May be called immediately within the current execution context, or later.*/ -var applyGradualLowpass = function(input, lpFreqStart, lpFreqEnd, lpFreqEndAt, callback) { +var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt, callback) { if (lpFreqStart == 0) { callback(input); return; @@ -173,7 +169,7 @@ var applyGradualLowpass = function(input, lpFreqStart, lpFreqEnd, lpFreqEndAt, c lpFreqStart = Math.min(lpFreqStart, input.sampleRate / 2); lpFreqEnd = Math.min(lpFreqEnd, input.sampleRate / 2); - filter.type = "lowpass"; + filter.type = 'lowpass'; filter.Q.value = 0.0001; filter.frequency.setValueAtTime(lpFreqStart, 0); filter.frequency.linearRampToValueAtTime(lpFreqEnd, lpFreqEndAt); @@ -181,7 +177,7 @@ var applyGradualLowpass = function(input, lpFreqStart, lpFreqEnd, lpFreqEndAt, c player.connect(filter); filter.connect(context.destination); player.start(); - context.oncomplete = function(event) { + context.oncomplete = function (event) { callback(event.renderedBuffer); }; context.startRendering(); @@ -192,7 +188,7 @@ var applyGradualLowpass = function(input, lpFreqStart, lpFreqEnd, lpFreqEndAt, c /** @private @param {!AudioBuffer} buffer @return {!Array.} An array containing the Float32Array of each channel's samples. */ -var getAllChannelData = function(buffer) { +var getAllChannelData = function (buffer) { var channels = []; for (var i = 0; i < buffer.numberOfChannels; i++) { channels[i] = buffer.getChannelData(i); @@ -202,7 +198,7 @@ var getAllChannelData = function(buffer) { /** @private @return {number} A random number from -1 to 1. */ -var randomSample = function() { +var randomSample = function () { return Math.random() * 2 - 1; }; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 0d08c8ad..39e05e79 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -109,20 +109,12 @@ function getDelay(orbit, delaytime, delayfeedback, t) { let reverbs = {}; function getReverb(orbit, duration = 2, fade, revlp, revdim) { - // If no reverb has been created for a given orbit, create one if (!reverbs[orbit]) { const ac = getAudioContext(); - const reverb = ac.createReverb( - getAudioContext(), - duration, - fade, - revlp, - revdim, - ); + const reverb = ac.createReverb(getAudioContext(), duration, fade, revlp, revdim); reverb.connect(getDestination()); reverbs[orbit] = reverb; - console.log(reverbs[orbit]); } if ( @@ -131,14 +123,11 @@ function getReverb(orbit, duration = 2, fade, revlp, revdim) { reverbs[orbit].revlp !== revlp || reverbs[orbit].revdim !== revdim ) { - reverbs[orbit] = reverbs[orbit].setDuration( - duration, fade, revlp, revdim - ); + reverbs[orbit] = reverbs[orbit].setDuration(duration, fade, revlp, revdim); reverbs[orbit].duration = duration; reverbs[orbit].fade = fade; reverbs[orbit].revlp = revlp; reverbs[orbit].revdim = revdim; - } return reverbs[orbit]; From 275796c241f3740e71bc1366e501a4855d7cbc8d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 23:21:02 +0200 Subject: [PATCH 31/48] fix: reverbs[orbit] is undefined --- packages/superdough/superdough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 39e05e79..28f4c261 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -123,7 +123,7 @@ function getReverb(orbit, duration = 2, fade, revlp, revdim) { reverbs[orbit].revlp !== revlp || reverbs[orbit].revdim !== revdim ) { - reverbs[orbit] = reverbs[orbit].setDuration(duration, fade, revlp, revdim); + reverbs[orbit].setDuration(duration, fade, revlp, revdim); reverbs[orbit].duration = duration; reverbs[orbit].fade = fade; reverbs[orbit].revlp = revlp; From dfdd9e02ca43f7117fe995cd9baba57ed7bed561 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Oct 2023 23:22:32 +0200 Subject: [PATCH 32/48] eslint ignore reverbGen + snapshot --- .eslintignore | 3 +- test/__snapshots__/examples.test.mjs.snap | 78 +++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/.eslintignore b/.eslintignore index 13e635f3..58d3643d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -18,4 +18,5 @@ vite.config.js **/*.json **/dev-dist **/dist -/src-tauri/target/**/* \ No newline at end of file +/src-tauri/target/**/* +reverbGen.mjs \ No newline at end of file diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index e026f9c4..616fff12 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1773,6 +1773,32 @@ exports[`runs examples > example "every" example index 0 1`] = ` ] `; +exports[`runs examples > example "fade" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd room:0.5 revlp:10000 fade:0.5 ]", + "[ 1/2 → 1/1 | s:sd room:0.5 revlp:10000 fade:0.5 ]", + "[ 1/1 → 3/2 | s:bd room:0.5 revlp:10000 fade:0.5 ]", + "[ 3/2 → 2/1 | s:sd room:0.5 revlp:10000 fade:0.5 ]", + "[ 2/1 → 5/2 | s:bd room:0.5 revlp:10000 fade:0.5 ]", + "[ 5/2 → 3/1 | s:sd room:0.5 revlp:10000 fade:0.5 ]", + "[ 3/1 → 7/2 | s:bd room:0.5 revlp:10000 fade:0.5 ]", + "[ 7/2 → 4/1 | s:sd room:0.5 revlp:10000 fade:0.5 ]", +] +`; + +exports[`runs examples > example "fade" example index 1 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd room:0.5 revlp:5000 fade:4 ]", + "[ 1/2 → 1/1 | s:sd room:0.5 revlp:5000 fade:4 ]", + "[ 1/1 → 3/2 | s:bd room:0.5 revlp:5000 fade:4 ]", + "[ 3/2 → 2/1 | s:sd room:0.5 revlp:5000 fade:4 ]", + "[ 2/1 → 5/2 | s:bd room:0.5 revlp:5000 fade:4 ]", + "[ 5/2 → 3/1 | s:sd room:0.5 revlp:5000 fade:4 ]", + "[ 3/1 → 7/2 | s:bd room:0.5 revlp:5000 fade:4 ]", + "[ 7/2 → 4/1 | s:sd room:0.5 revlp:5000 fade:4 ]", +] +`; + exports[`runs examples > example "fast" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd ]", @@ -3607,6 +3633,58 @@ exports[`runs examples > example "rev" example index 0 1`] = ` ] `; +exports[`runs examples > example "revdim" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd room:0.5 revlp:10000 revdim:8000 ]", + "[ 1/2 → 1/1 | s:sd room:0.5 revlp:10000 revdim:8000 ]", + "[ 1/1 → 3/2 | s:bd room:0.5 revlp:10000 revdim:8000 ]", + "[ 3/2 → 2/1 | s:sd room:0.5 revlp:10000 revdim:8000 ]", + "[ 2/1 → 5/2 | s:bd room:0.5 revlp:10000 revdim:8000 ]", + "[ 5/2 → 3/1 | s:sd room:0.5 revlp:10000 revdim:8000 ]", + "[ 3/1 → 7/2 | s:bd room:0.5 revlp:10000 revdim:8000 ]", + "[ 7/2 → 4/1 | s:sd room:0.5 revlp:10000 revdim:8000 ]", +] +`; + +exports[`runs examples > example "revdim" example index 1 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd room:0.5 revlp:5000 revdim:400 ]", + "[ 1/2 → 1/1 | s:sd room:0.5 revlp:5000 revdim:400 ]", + "[ 1/1 → 3/2 | s:bd room:0.5 revlp:5000 revdim:400 ]", + "[ 3/2 → 2/1 | s:sd room:0.5 revlp:5000 revdim:400 ]", + "[ 2/1 → 5/2 | s:bd room:0.5 revlp:5000 revdim:400 ]", + "[ 5/2 → 3/1 | s:sd room:0.5 revlp:5000 revdim:400 ]", + "[ 3/1 → 7/2 | s:bd room:0.5 revlp:5000 revdim:400 ]", + "[ 7/2 → 4/1 | s:sd room:0.5 revlp:5000 revdim:400 ]", +] +`; + +exports[`runs examples > example "revlp" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd room:0.5 revlp:10000 ]", + "[ 1/2 → 1/1 | s:sd room:0.5 revlp:10000 ]", + "[ 1/1 → 3/2 | s:bd room:0.5 revlp:10000 ]", + "[ 3/2 → 2/1 | s:sd room:0.5 revlp:10000 ]", + "[ 2/1 → 5/2 | s:bd room:0.5 revlp:10000 ]", + "[ 5/2 → 3/1 | s:sd room:0.5 revlp:10000 ]", + "[ 3/1 → 7/2 | s:bd room:0.5 revlp:10000 ]", + "[ 7/2 → 4/1 | s:sd room:0.5 revlp:10000 ]", +] +`; + +exports[`runs examples > example "revlp" example index 1 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd room:0.5 revlp:5000 ]", + "[ 1/2 → 1/1 | s:sd room:0.5 revlp:5000 ]", + "[ 1/1 → 3/2 | s:bd room:0.5 revlp:5000 ]", + "[ 3/2 → 2/1 | s:sd room:0.5 revlp:5000 ]", + "[ 2/1 → 5/2 | s:bd room:0.5 revlp:5000 ]", + "[ 5/2 → 3/1 | s:sd room:0.5 revlp:5000 ]", + "[ 3/1 → 7/2 | s:bd room:0.5 revlp:5000 ]", + "[ 7/2 → 4/1 | s:sd room:0.5 revlp:5000 ]", +] +`; + exports[`runs examples > example "ribbon" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C3 ]", From 28966739f61222d83087355baf99413168ef5607 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 2 Oct 2023 20:44:51 +0200 Subject: [PATCH 33/48] feat: add step as slider param --- packages/codemirror/slider.mjs | 9 +++++---- packages/transpiler/transpiler.mjs | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index 47b686c8..62ec33c2 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -6,7 +6,7 @@ export let sliderValues = {}; const getSliderID = (from) => `slider_${from}`; export class SliderWidget extends WidgetType { - constructor(value, min, max, from, to, view) { + constructor(value, min, max, from, to, step, view) { super(); this.value = value; this.min = min; @@ -14,6 +14,7 @@ export class SliderWidget extends WidgetType { this.from = from; this.originalFrom = from; this.to = to; + this.step = step; this.view = view; } @@ -29,7 +30,7 @@ export class SliderWidget extends WidgetType { slider.type = 'range'; slider.min = this.min; slider.max = this.max; - slider.step = (this.max - this.min) / 1000; + slider.step = this.step ?? (this.max - this.min) / 1000; slider.originalValue = this.value; // to make sure the code stays in sync, let's save the original value // becuase .value automatically clamps values so it'll desync with the code @@ -66,9 +67,9 @@ export const updateWidgets = (view, widgets) => { }; function getWidgets(widgetConfigs, view) { - return widgetConfigs.map(({ from, to, value, min, max }) => { + return widgetConfigs.map(({ from, to, value, min, max, step }) => { return Decoration.widget({ - widget: new SliderWidget(value, min, max, from, to, view), + widget: new SliderWidget(value, min, max, from, to, step, view), side: 0, }).range(from /* , to */); }); diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 6eac171f..256be1d2 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -43,6 +43,7 @@ export function transpiler(input, options = {}) { value: node.arguments[0].raw, // don't use value! min: node.arguments[1]?.value ?? 0, max: node.arguments[2]?.value ?? 1, + step: node.arguments[3]?.value, }); return this.replace(widgetWithLocation(node)); } From a3649148c1ab0975a1a3486a3bd68313c762dbb5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 3 Oct 2023 08:35:15 +0200 Subject: [PATCH 34/48] more logical naming + update docs --- packages/core/controls.mjs | 43 +++++++++++++++++------------ packages/superdough/superdough.mjs | 12 ++++---- website/src/pages/learn/effects.mdx | 26 +++++++++-------- 3 files changed, 47 insertions(+), 34 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index e72f006d..63ffdaf4 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -972,53 +972,62 @@ const generic_params = [ [['room', 'size']], /** * Reverb lowpass starting frequency (in hertz). + * When this property is changed, the reverb will be recaculated, so only change this sparsely.. * - * @name revlp - * @param {number} level between 0 and 20000hz + * @name roomlp + * @synonyms rlp + * @param {number} frequency between 0 and 20000hz * @example - * s("bd sd").room(0.5).revlp(10000) + * s("bd sd").room(0.5).rlp(10000) * @example - * s("bd sd").room(0.5).revlp(5000) + * s("bd sd").room(0.5).rlp(5000) */ - ['revlp'], + ['roomlp', 'rlp'], /** * Reverb lowpass frequency at -60dB (in hertz). + * When this property is changed, the reverb will be recaculated, so only change this sparsely.. * - * @name revdim - * @param {number} level between 0 and 20000hz + * @name roomdim + * @synonyms rdim + * @param {number} frequency between 0 and 20000hz * @example - * s("bd sd").room(0.5).revlp(10000).revdim(8000) + * s("bd sd").room(0.5).rlp(10000).rdim(8000) * @example - * s("bd sd").room(0.5).revlp(5000).revdim(400) + * s("bd sd").room(0.5).rlp(5000).rdim(400) * */ - ['revdim'], + ['roomdim', 'rdim'], /** * Reverb fade time (in seconds). + * When this property is changed, the reverb will be recaculated, so only change this sparsely.. * - * @name fade + * @name roomfade + * @synonyms rfade * @param {number} seconds for the reverb to fade * @example - * s("bd sd").room(0.5).revlp(10000).fade(0.5) + * s("bd sd").room(0.5).rlp(10000).rfade(0.5) * @example - * s("bd sd").room(0.5).revlp(5000).fade(4) + * s("bd sd").room(0.5).rlp(5000).rfade(4) * */ - ['fade'], + ['roomfade', 'rfade'], /** * Sets the room size of the reverb, see {@link room}. + * When this property is changed, the reverb will be recaculated, so only change this sparsely.. * * @name roomsize * @param {number | Pattern} size between 0 and 10 - * @synonyms size, sz + * @synonyms rsize, sz, size * @example - * s("bd sd").room(.8).roomsize("<0 1 2 4 8>") + * s("bd sd").room(.8).rsize(1) + * @example + * s("bd sd").room(.8).rsize(4) * */ // TODO: find out why : // s("bd sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc() // .. does not work. Is it because room is only one effect? - ['size', 'sz', 'roomsize'], + ['roomsize', 'size', 'sz', 'rsize'], // ['sagogo'], // ['sclap'], // ['sclaves'], diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 28f4c261..e387ff6c 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -227,10 +227,10 @@ export const superdough = async (value, deadline, hapDuration) => { delaytime = 0.25, orbit = 1, room, - fade = 0.1, - revlp = 15000, - revdim = 1000, - size = 2, + roomfade = 0.1, + roomlp = 15000, + roomdim = 1000, + roomsize = 2, velocity = 1, analyze, // analyser wet fft = 8, // fftSize 0 - 10 @@ -368,8 +368,8 @@ export const superdough = async (value, deadline, hapDuration) => { } // reverb let reverbSend; - if (room > 0 && size > 0) { - const reverbNode = getReverb(orbit, size, fade, revlp, revdim); + if (room > 0 && roomsize > 0) { + const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim); reverbSend = effectSend(post, reverbNode, room); } diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index 062a1947..2ee6c44a 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -183,36 +183,40 @@ global effects use the same chain for all events of the same orbit: -## delay +## Delay + +### delay -## delaytime +### delaytime -## delayfeedback +### delayfeedback -## room +## Reverb + +### room -## roomsize +### roomsize -## fade +### roomfade - + -## revlp +### roomlp - + -## revdim +### roomdim - + Next, we'll look at strudel's support for [Csound](/learn/csound). From 904306454349862a7356f355c294e3e89f457842 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 3 Oct 2023 08:36:52 +0200 Subject: [PATCH 35/48] snapshot --- test/__snapshots__/examples.test.mjs.snap | 185 ++++++++++++---------- 1 file changed, 99 insertions(+), 86 deletions(-) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 616fff12..99d078ea 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1773,32 +1773,6 @@ exports[`runs examples > example "every" example index 0 1`] = ` ] `; -exports[`runs examples > example "fade" example index 0 1`] = ` -[ - "[ 0/1 → 1/2 | s:bd room:0.5 revlp:10000 fade:0.5 ]", - "[ 1/2 → 1/1 | s:sd room:0.5 revlp:10000 fade:0.5 ]", - "[ 1/1 → 3/2 | s:bd room:0.5 revlp:10000 fade:0.5 ]", - "[ 3/2 → 2/1 | s:sd room:0.5 revlp:10000 fade:0.5 ]", - "[ 2/1 → 5/2 | s:bd room:0.5 revlp:10000 fade:0.5 ]", - "[ 5/2 → 3/1 | s:sd room:0.5 revlp:10000 fade:0.5 ]", - "[ 3/1 → 7/2 | s:bd room:0.5 revlp:10000 fade:0.5 ]", - "[ 7/2 → 4/1 | s:sd room:0.5 revlp:10000 fade:0.5 ]", -] -`; - -exports[`runs examples > example "fade" example index 1 1`] = ` -[ - "[ 0/1 → 1/2 | s:bd room:0.5 revlp:5000 fade:4 ]", - "[ 1/2 → 1/1 | s:sd room:0.5 revlp:5000 fade:4 ]", - "[ 1/1 → 3/2 | s:bd room:0.5 revlp:5000 fade:4 ]", - "[ 3/2 → 2/1 | s:sd room:0.5 revlp:5000 fade:4 ]", - "[ 2/1 → 5/2 | s:bd room:0.5 revlp:5000 fade:4 ]", - "[ 5/2 → 3/1 | s:sd room:0.5 revlp:5000 fade:4 ]", - "[ 3/1 → 7/2 | s:bd room:0.5 revlp:5000 fade:4 ]", - "[ 7/2 → 4/1 | s:sd room:0.5 revlp:5000 fade:4 ]", -] -`; - exports[`runs examples > example "fast" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd ]", @@ -3633,58 +3607,6 @@ exports[`runs examples > example "rev" example index 0 1`] = ` ] `; -exports[`runs examples > example "revdim" example index 0 1`] = ` -[ - "[ 0/1 → 1/2 | s:bd room:0.5 revlp:10000 revdim:8000 ]", - "[ 1/2 → 1/1 | s:sd room:0.5 revlp:10000 revdim:8000 ]", - "[ 1/1 → 3/2 | s:bd room:0.5 revlp:10000 revdim:8000 ]", - "[ 3/2 → 2/1 | s:sd room:0.5 revlp:10000 revdim:8000 ]", - "[ 2/1 → 5/2 | s:bd room:0.5 revlp:10000 revdim:8000 ]", - "[ 5/2 → 3/1 | s:sd room:0.5 revlp:10000 revdim:8000 ]", - "[ 3/1 → 7/2 | s:bd room:0.5 revlp:10000 revdim:8000 ]", - "[ 7/2 → 4/1 | s:sd room:0.5 revlp:10000 revdim:8000 ]", -] -`; - -exports[`runs examples > example "revdim" example index 1 1`] = ` -[ - "[ 0/1 → 1/2 | s:bd room:0.5 revlp:5000 revdim:400 ]", - "[ 1/2 → 1/1 | s:sd room:0.5 revlp:5000 revdim:400 ]", - "[ 1/1 → 3/2 | s:bd room:0.5 revlp:5000 revdim:400 ]", - "[ 3/2 → 2/1 | s:sd room:0.5 revlp:5000 revdim:400 ]", - "[ 2/1 → 5/2 | s:bd room:0.5 revlp:5000 revdim:400 ]", - "[ 5/2 → 3/1 | s:sd room:0.5 revlp:5000 revdim:400 ]", - "[ 3/1 → 7/2 | s:bd room:0.5 revlp:5000 revdim:400 ]", - "[ 7/2 → 4/1 | s:sd room:0.5 revlp:5000 revdim:400 ]", -] -`; - -exports[`runs examples > example "revlp" example index 0 1`] = ` -[ - "[ 0/1 → 1/2 | s:bd room:0.5 revlp:10000 ]", - "[ 1/2 → 1/1 | s:sd room:0.5 revlp:10000 ]", - "[ 1/1 → 3/2 | s:bd room:0.5 revlp:10000 ]", - "[ 3/2 → 2/1 | s:sd room:0.5 revlp:10000 ]", - "[ 2/1 → 5/2 | s:bd room:0.5 revlp:10000 ]", - "[ 5/2 → 3/1 | s:sd room:0.5 revlp:10000 ]", - "[ 3/1 → 7/2 | s:bd room:0.5 revlp:10000 ]", - "[ 7/2 → 4/1 | s:sd room:0.5 revlp:10000 ]", -] -`; - -exports[`runs examples > example "revlp" example index 1 1`] = ` -[ - "[ 0/1 → 1/2 | s:bd room:0.5 revlp:5000 ]", - "[ 1/2 → 1/1 | s:sd room:0.5 revlp:5000 ]", - "[ 1/1 → 3/2 | s:bd room:0.5 revlp:5000 ]", - "[ 3/2 → 2/1 | s:sd room:0.5 revlp:5000 ]", - "[ 2/1 → 5/2 | s:bd room:0.5 revlp:5000 ]", - "[ 5/2 → 3/1 | s:sd room:0.5 revlp:5000 ]", - "[ 3/1 → 7/2 | s:bd room:0.5 revlp:5000 ]", - "[ 7/2 → 4/1 | s:sd room:0.5 revlp:5000 ]", -] -`; - exports[`runs examples > example "ribbon" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C3 ]", @@ -3732,16 +3654,107 @@ exports[`runs examples > example "room" example index 1 1`] = ` ] `; +exports[`runs examples > example "roomdim" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]", + "[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]", + "[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]", + "[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]", + "[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]", + "[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]", + "[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]", + "[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]", +] +`; + +exports[`runs examples > example "roomdim" example index 1 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]", + "[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]", + "[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]", + "[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]", + "[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]", + "[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]", + "[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]", + "[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]", +] +`; + +exports[`runs examples > example "roomfade" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]", + "[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]", + "[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]", + "[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]", + "[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]", + "[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]", + "[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]", + "[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]", +] +`; + +exports[`runs examples > example "roomfade" example index 1 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]", + "[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]", + "[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]", + "[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]", + "[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]", + "[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]", + "[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]", + "[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]", +] +`; + +exports[`runs examples > example "roomlp" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 ]", + "[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 ]", + "[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 ]", + "[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 ]", + "[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 ]", + "[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 ]", + "[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 ]", + "[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 ]", +] +`; + +exports[`runs examples > example "roomlp" example index 1 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 ]", + "[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 ]", + "[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 ]", + "[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 ]", + "[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 ]", + "[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 ]", + "[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 ]", + "[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 ]", +] +`; + exports[`runs examples > example "roomsize" example index 0 1`] = ` [ - "[ 0/1 → 1/2 | s:bd room:0.8 size:0 ]", - "[ 1/2 → 1/1 | s:sd room:0.8 size:0 ]", - "[ 1/1 → 3/2 | s:bd room:0.8 size:1 ]", - "[ 3/2 → 2/1 | s:sd room:0.8 size:1 ]", - "[ 2/1 → 5/2 | s:bd room:0.8 size:2 ]", - "[ 5/2 → 3/1 | s:sd room:0.8 size:2 ]", - "[ 3/1 → 7/2 | s:bd room:0.8 size:4 ]", - "[ 7/2 → 4/1 | s:sd room:0.8 size:4 ]", + "[ 0/1 → 1/2 | s:bd room:0.8 roomsize:1 ]", + "[ 1/2 → 1/1 | s:sd room:0.8 roomsize:1 ]", + "[ 1/1 → 3/2 | s:bd room:0.8 roomsize:1 ]", + "[ 3/2 → 2/1 | s:sd room:0.8 roomsize:1 ]", + "[ 2/1 → 5/2 | s:bd room:0.8 roomsize:1 ]", + "[ 5/2 → 3/1 | s:sd room:0.8 roomsize:1 ]", + "[ 3/1 → 7/2 | s:bd room:0.8 roomsize:1 ]", + "[ 7/2 → 4/1 | s:sd room:0.8 roomsize:1 ]", +] +`; + +exports[`runs examples > example "roomsize" example index 1 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd room:0.8 roomsize:4 ]", + "[ 1/2 → 1/1 | s:sd room:0.8 roomsize:4 ]", + "[ 1/1 → 3/2 | s:bd room:0.8 roomsize:4 ]", + "[ 3/2 → 2/1 | s:sd room:0.8 roomsize:4 ]", + "[ 2/1 → 5/2 | s:bd room:0.8 roomsize:4 ]", + "[ 5/2 → 3/1 | s:sd room:0.8 roomsize:4 ]", + "[ 3/1 → 7/2 | s:bd room:0.8 roomsize:4 ]", + "[ 7/2 → 4/1 | s:sd room:0.8 roomsize:4 ]", ] `; From 1e352fdf8001696b243cdb7f7fc777e34b151eb5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 3 Oct 2023 08:51:35 +0200 Subject: [PATCH 36/48] codeformat --- packages/superdough/synth.mjs | 215 +++++++++++++++-------------- website/src/pages/learn/synths.mdx | 5 +- 2 files changed, 111 insertions(+), 109 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index e5f84bcf..51d56993 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -20,104 +20,110 @@ const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => { return mod(modfreq, modgain, wave); }; - export function registerSynthSounds() { - ['sine', 'square', 'triangle', - 'sawtooth', 'pink', 'white', - 'brown'].forEach((wave) => { - registerSound( - wave, - (t, value, onended) => { - // destructure adsr here, because the default should be different for synths and samples - let { - attack = 0.001, - decay = 0.05, - sustain = 0.6, - release = 0.01, - fmh: fmHarmonicity = 1, - fmi: fmModulationIndex, - fmenv: fmEnvelopeType = 'lin', - fmattack: fmAttack, - fmdecay: fmDecay, - fmsustain: fmSustain, - fmrelease: fmRelease, - fmvelocity: fmVelocity, - fmwave: fmWaveform = 'sine', - vib = 0, - vibmod = 0.5, - noise = 0, - } = value; - let { n, note, freq } = value; - // with synths, n and note are the same thing - note = note || 36; - if (typeof note === 'string') { - note = noteToMidi(note); // e.g. c3 => 48 - } - // get frequency - if (!freq && typeof note === 'number') { - freq = midiToFreq(note); // + 48); - } - // maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default) - // make oscillator - const { node: o, stop, dry_node = null } = getOscillator({ - t, - s: wave, - freq, - vib, - vibmod, - partials: n, - noise: noise, - }); - // FM + FM envelope - let stopFm, fmEnvelope; - if (fmModulationIndex) { - const { node: modulator, stop } = fm(dry_node !== null ? dry_node : o, fmHarmonicity, fmModulationIndex, fmWaveform); - if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) { - // no envelope by default - modulator.connect(dry_node !== null ? dry_node.frequency : o.frequency); - } else { - fmAttack = fmAttack ?? 0.001; - fmDecay = fmDecay ?? 0.001; - fmSustain = fmSustain ?? 1; - fmRelease = fmRelease ?? 0.001; - fmVelocity = fmVelocity ?? 1; - fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t); - if (fmEnvelopeType === 'exp') { - fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t); - fmEnvelope.node.maxValue = fmModulationIndex * 2; - fmEnvelope.node.minValue = 0.00001; - } - modulator.connect(fmEnvelope.node); - fmEnvelope.node.connect(dry_node !== null ? dry_node.frequency : o.frequency); + ['sine', 'square', 'triangle', 'sawtooth', 'pink', 'white', 'brown'].forEach((wave) => { + registerSound( + wave, + (t, value, onended) => { + // destructure adsr here, because the default should be different for synths and samples + let { + attack = 0.001, + decay = 0.05, + sustain = 0.6, + release = 0.01, + fmh: fmHarmonicity = 1, + fmi: fmModulationIndex, + fmenv: fmEnvelopeType = 'lin', + fmattack: fmAttack, + fmdecay: fmDecay, + fmsustain: fmSustain, + fmrelease: fmRelease, + fmvelocity: fmVelocity, + fmwave: fmWaveform = 'sine', + vib = 0, + vibmod = 0.5, + noise = 0, + } = value; + let { n, note, freq } = value; + // with synths, n and note are the same thing + note = note || 36; + if (typeof note === 'string') { + note = noteToMidi(note); // e.g. c3 => 48 + } + // get frequency + if (!freq && typeof note === 'number') { + freq = midiToFreq(note); // + 48); + } + // maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default) + // make oscillator + const { + node: o, + stop, + dry_node = null, + } = getOscillator({ + t, + s: wave, + freq, + vib, + vibmod, + partials: n, + noise: noise, + }); + // FM + FM envelope + let stopFm, fmEnvelope; + if (fmModulationIndex) { + const { node: modulator, stop } = fm( + dry_node !== null ? dry_node : o, + fmHarmonicity, + fmModulationIndex, + fmWaveform, + ); + if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) { + // no envelope by default + modulator.connect(dry_node !== null ? dry_node.frequency : o.frequency); + } else { + fmAttack = fmAttack ?? 0.001; + fmDecay = fmDecay ?? 0.001; + fmSustain = fmSustain ?? 1; + fmRelease = fmRelease ?? 0.001; + fmVelocity = fmVelocity ?? 1; + fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t); + if (fmEnvelopeType === 'exp') { + fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t); + fmEnvelope.node.maxValue = fmModulationIndex * 2; + fmEnvelope.node.minValue = 0.00001; } - stopFm = stop; + modulator.connect(fmEnvelope.node); + fmEnvelope.node.connect(dry_node !== null ? dry_node.frequency : o.frequency); } + stopFm = stop; + } - // turn down - const g = gainNode(0.3); + // turn down + const g = gainNode(0.3); - // gain envelope - const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t); + // gain envelope + const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t); - o.onended = () => { - o.disconnect(); - g.disconnect(); - onended(); - }; - return { - node: o.connect(g).connect(envelope), - stop: (releaseTime) => { - releaseEnvelope(releaseTime); - fmEnvelope?.stop(releaseTime); - let end = releaseTime + release; - stop(end); - stopFm?.(end); - }, - }; - }, - { type: 'synth', prebake: true }, - ); - }); + o.onended = () => { + o.disconnect(); + g.disconnect(); + onended(); + }; + return { + node: o.connect(g).connect(envelope), + stop: (releaseTime) => { + releaseEnvelope(releaseTime); + fmEnvelope?.stop(releaseTime); + let end = releaseTime + release; + stop(end); + stopFm?.(end); + }, + }; + }, + { type: 'synth', prebake: true }, + ); + }); } export function waveformN(partials, type) { @@ -163,16 +169,16 @@ export function getNoiseOscillator({ t, ac, type = 'white' }) { output[i] = Math.random() * 2 - 1; } else if (type === 'brown') { let white = Math.random() * 2 - 1; - output[i] = (lastOut + (0.02 * white)) / 1.02; + output[i] = (lastOut + 0.02 * white) / 1.02; lastOut = output[i]; } else if (type === 'pink') { let white = Math.random() * 2 - 1; b0 = 0.99886 * b0 + white * 0.0555179; b1 = 0.99332 * b1 + white * 0.0750759; - b2 = 0.96900 * b2 + white * 0.1538520; - b3 = 0.86650 * b3 + white * 0.3104856; - b4 = 0.55000 * b4 + white * 0.5329522; - b5 = -0.7616 * b5 - white * 0.0168980; + b2 = 0.969 * b2 + white * 0.153852; + b3 = 0.8665 * b3 + white * 0.3104856; + b4 = 0.55 * b4 + white * 0.5329522; + b5 = -0.7616 * b5 - white * 0.016898; output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362; output[i] *= 0.11; b6 = white * 0.115926; @@ -186,7 +192,7 @@ export function getNoiseOscillator({ t, ac, type = 'white' }) { return { node: o, - stop: (time) => o.stop(time) + stop: (time) => o.stop(time), }; } @@ -196,11 +202,11 @@ export function getOscillator({ s, freq, t, vib, vibmod, partials, noise }) { let o; if (['pink', 'white', 'brown'].includes(s)) { - let noiseOscillator = getNoiseOscillator({ t: t, ac: getAudioContext(), type: s }) + let noiseOscillator = getNoiseOscillator({ t: t, ac: getAudioContext(), type: s }); return { node: noiseOscillator.node, - stop: noiseOscillator.stop - } + stop: noiseOscillator.stop, + }; } else { if (!partials || s === 'sine') { o = getAudioContext().createOscillator(); @@ -238,7 +244,7 @@ export function getOscillator({ s, freq, t, vib, vibmod, partials, noise }) { // Connecting the main oscillator to the gain node o.connect(o_gain).connect(mix_gain); - // Instanciating a noise oscillator and connecting + // Instanciating a noise oscillator and connecting const noiseOscillator = getNoiseOscillator({ t: t, ac: ac, type: 'pink' }); noiseOscillator.node.connect(n_gain).connect(mix_gain); @@ -249,8 +255,8 @@ export function getOscillator({ s, freq, t, vib, vibmod, partials, noise }) { vibrato_oscillator?.stop(time); o.stop(time); noiseOscillator.stop(time); - } - } + }, + }; } return { @@ -262,4 +268,3 @@ export function getOscillator({ s, freq, t, vib, vibmod, partials, noise }) { }; } } - diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 661ec860..83de3ca5 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -35,10 +35,7 @@ flavours of noise, here written from hard to soft. Some amount of pink noise can also be added to any oscillator by using the `noise` paremeter: -").scope()`} -/> +").scope()`} /> ### Additive Synthesis From a0884e2a038c654a722647237f5e760810ddcf63 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 3 Oct 2023 09:09:49 +0200 Subject: [PATCH 37/48] add noise heading + hihat example --- website/src/pages/learn/synths.mdx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 83de3ca5..432276ef 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -23,14 +23,19 @@ The basic waveforms are `sine`, `sawtooth`, `square` and `triangle`, which can b If you don't set a `sound` but a `note` the default value for `sound` is `triangle`! +## Noise + You can also use noise as a source by setting the waveform to: `white`, `pink` or `brown`. These are different flavours of noise, here written from hard to soft. +/2").scope()`} /> + +Here's a more musical example of how to use noise for hihats: + >") -.sound("/2") -.scope()`} + tune={`sound("bd*2,*8") +.decay(.04).sustain(0).scope()`} /> Some amount of pink noise can also be added to any oscillator by using the `noise` paremeter: From 484bb6b11f7c79220cc9bd0c8f5056a88232d90a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 3 Oct 2023 10:03:09 +0200 Subject: [PATCH 38/48] refactor synth - separate waveform / noise oscillators - pull noise out of getOscillator - put fm into getOscillator - simplify overall value plumbing --- packages/superdough/synth.mjs | 280 ++++++++++++++++------------------ 1 file changed, 135 insertions(+), 145 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 51d56993..f12be9c8 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -20,85 +20,26 @@ const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => { return mod(modfreq, modgain, wave); }; +const waveforms = ['sine', 'square', 'triangle', 'sawtooth']; +const noises = ['pink', 'white', 'brown']; + export function registerSynthSounds() { - ['sine', 'square', 'triangle', 'sawtooth', 'pink', 'white', 'brown'].forEach((wave) => { + [...waveforms, ...noises].forEach((s) => { registerSound( - wave, + s, (t, value, onended) => { // destructure adsr here, because the default should be different for synths and samples - let { - attack = 0.001, - decay = 0.05, - sustain = 0.6, - release = 0.01, - fmh: fmHarmonicity = 1, - fmi: fmModulationIndex, - fmenv: fmEnvelopeType = 'lin', - fmattack: fmAttack, - fmdecay: fmDecay, - fmsustain: fmSustain, - fmrelease: fmRelease, - fmvelocity: fmVelocity, - fmwave: fmWaveform = 'sine', - vib = 0, - vibmod = 0.5, - noise = 0, - } = value; - let { n, note, freq } = value; - // with synths, n and note are the same thing - note = note || 36; - if (typeof note === 'string') { - note = noteToMidi(note); // e.g. c3 => 48 - } - // get frequency - if (!freq && typeof note === 'number') { - freq = midiToFreq(note); // + 48); - } - // maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default) - // make oscillator - const { - node: o, - stop, - dry_node = null, - } = getOscillator({ - t, - s: wave, - freq, - vib, - vibmod, - partials: n, - noise: noise, - }); - // FM + FM envelope - let stopFm, fmEnvelope; - if (fmModulationIndex) { - const { node: modulator, stop } = fm( - dry_node !== null ? dry_node : o, - fmHarmonicity, - fmModulationIndex, - fmWaveform, - ); - if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) { - // no envelope by default - modulator.connect(dry_node !== null ? dry_node.frequency : o.frequency); - } else { - fmAttack = fmAttack ?? 0.001; - fmDecay = fmDecay ?? 0.001; - fmSustain = fmSustain ?? 1; - fmRelease = fmRelease ?? 0.001; - fmVelocity = fmVelocity ?? 1; - fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t); - if (fmEnvelopeType === 'exp') { - fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t); - fmEnvelope.node.maxValue = fmModulationIndex * 2; - fmEnvelope.node.minValue = 0.00001; - } - modulator.connect(fmEnvelope.node); - fmEnvelope.node.connect(dry_node !== null ? dry_node.frequency : o.frequency); - } - stopFm = stop; + let { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value; + + let sound; + if (waveforms.includes(s)) { + sound = getOscillator(s, t, value); + } else { + sound = getNoiseOscillator(t, s); } + let { node: o, stop, triggerRelease } = sound; + // turn down const g = gainNode(0.3); @@ -114,10 +55,9 @@ export function registerSynthSounds() { node: o.connect(g).connect(envelope), stop: (releaseTime) => { releaseEnvelope(releaseTime); - fmEnvelope?.stop(releaseTime); + triggerRelease?.(releaseTime); let end = releaseTime + release; stop(end); - stopFm?.(end); }, }; }, @@ -156,7 +96,9 @@ export function waveformN(partials, type) { return osc; } -export function getNoiseOscillator({ t, ac, type = 'white' }) { +// expects one of noises as type +export function getNoiseOscillator(t, type = 'white') { + const ac = getAudioContext(); const bufferSize = 2 * ac.sampleRate; const noiseBuffer = ac.createBuffer(1, bufferSize, ac.sampleRate); const output = noiseBuffer.getChannelData(0); @@ -189,82 +131,130 @@ export function getNoiseOscillator({ t, ac, type = 'white' }) { o.buffer = noiseBuffer; o.loop = true; o.start(t); - return { node: o, stop: (time) => o.stop(time), }; } -export function getOscillator({ s, freq, t, vib, vibmod, partials, noise }) { - // Make oscillator with partial count +// expects one of waveforms as s +export function getOscillator( + s, + t, + { + n: partials, + note, + freq, + vib = 0, + vibmod = 0.5, + noise = 0, + // fm + fmh: fmHarmonicity = 1, + fmi: fmModulationIndex, + fmenv: fmEnvelopeType = 'lin', + fmattack: fmAttack, + fmdecay: fmDecay, + fmsustain: fmSustain, + fmrelease: fmRelease, + fmvelocity: fmVelocity, + fmwave: fmWaveform = 'sine', + }, +) { let ac = getAudioContext(); let o; - - if (['pink', 'white', 'brown'].includes(s)) { - let noiseOscillator = getNoiseOscillator({ t: t, ac: getAudioContext(), type: s }); - return { - node: noiseOscillator.node, - stop: noiseOscillator.stop, - }; - } else { - if (!partials || s === 'sine') { - o = getAudioContext().createOscillator(); - o.type = s || 'triangle'; - } else { - o = waveformN(partials, s); - } - o.frequency.value = Number(freq); - o.start(t); - - // Additional oscillator for vibrato effect - let vibrato_oscillator; - if (vib > 0) { - vibrato_oscillator = getAudioContext().createOscillator(); - vibrato_oscillator.frequency.value = vib; - const gain = getAudioContext().createGain(); - // Vibmod is the amount of vibrato, in semitones - gain.gain.value = vibmod * 100; - vibrato_oscillator.connect(gain); - gain.connect(o.detune); - vibrato_oscillator.start(t); - } - - if (noise > 0) { - // Two gain nodes to set the oscillators to their respective levels - noise = noise > 1 ? 1 : noise; - let o_gain = ac.createGain(); - let n_gain = ac.createGain(); - o_gain.gain.setValueAtTime(1 - noise, ac.currentTime); - n_gain.gain.setValueAtTime(noise, ac.currentTime); - - // Instanciating a mixer to blend sources together - let mix_gain = ac.createGain(); - - // Connecting the main oscillator to the gain node - o.connect(o_gain).connect(mix_gain); - - // Instanciating a noise oscillator and connecting - const noiseOscillator = getNoiseOscillator({ t: t, ac: ac, type: 'pink' }); - noiseOscillator.node.connect(n_gain).connect(mix_gain); - - return { - node: mix_gain, - dry_node: o, - stop: (time) => { - vibrato_oscillator?.stop(time); - o.stop(time); - noiseOscillator.stop(time); - }, - }; - } - - return { - node: o, - stop: (time) => { - vibrato_oscillator?.stop(time); - o.stop(time); - }, - }; + // If no partials are given, use stock waveforms + if (!partials || s === 'sine') { + o = getAudioContext().createOscillator(); + o.type = s || 'triangle'; } + // generate custom waveform if partials are given + else { + o = waveformN(partials, s); + } + + // get frequency from note... + note = note || 36; + if (typeof note === 'string') { + note = noteToMidi(note); // e.g. c3 => 48 + } + // get frequency + if (!freq && typeof note === 'number') { + freq = midiToFreq(note); // + 48); + } + + // set frequency + o.frequency.value = Number(freq); + o.start(t); + + // FM + let stopFm, fmEnvelope; + if (fmModulationIndex) { + const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform); + if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) { + // no envelope by default + modulator.connect(o.frequency); + } else { + fmAttack = fmAttack ?? 0.001; + fmDecay = fmDecay ?? 0.001; + fmSustain = fmSustain ?? 1; + fmRelease = fmRelease ?? 0.001; + fmVelocity = fmVelocity ?? 1; + fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t); + if (fmEnvelopeType === 'exp') { + fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t); + fmEnvelope.node.maxValue = fmModulationIndex * 2; + fmEnvelope.node.minValue = 0.00001; + } + modulator.connect(fmEnvelope.node); + fmEnvelope.node.connect(o.frequency); + } + stopFm = stop; + } + + // Additional oscillator for vibrato effect + let vibratoOscillator; + if (vib > 0) { + vibratoOscillator = getAudioContext().createOscillator(); + vibratoOscillator.frequency.value = vib; + const gain = getAudioContext().createGain(); + // Vibmod is the amount of vibrato, in semitones + gain.gain.value = vibmod * 100; + vibratoOscillator.connect(gain); + gain.connect(o.detune); + vibratoOscillator.start(t); + } + + let noiseOscillator, noiseMix; + // noise mix + if (noise > 0) { + // Two gain nodes to set the oscillators to their respective levels + noise = noise > 1 ? 1 : noise; + let o_gain = ac.createGain(); + let n_gain = ac.createGain(); + o_gain.gain.setValueAtTime(1 - noise, ac.currentTime); + n_gain.gain.setValueAtTime(noise, ac.currentTime); + + // Instanciating a mixer to blend sources together + noiseMix = ac.createGain(); + + // Connecting the main oscillator to the gain node + o.connect(o_gain).connect(noiseMix); + + // Instanciating a noise oscillator and connecting + noiseOscillator = getNoiseOscillator(t, 'pink'); + noiseOscillator.node.connect(n_gain).connect(noiseMix); + } + + return { + node: noiseMix || o, + stop: (time) => { + vibratoOscillator?.stop(time); + noiseOscillator?.stop(time); + stopFm?.(time); + o.stop(time); + }, + triggerRelease: (time) => { + fmEnvelope?.stop(time); + }, + }; } From 2bc6d0841049aaac6943cec2cdf217905aa1ac13 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 3 Oct 2023 12:19:30 +0200 Subject: [PATCH 39/48] proper dry wet + pull out noise to extra file --- packages/superdough/helpers.mjs | 22 +++++++++++ packages/superdough/noise.mjs | 51 ++++++++++++++++++++++++ packages/superdough/synth.mjs | 70 ++++----------------------------- 3 files changed, 80 insertions(+), 63 deletions(-) create mode 100644 packages/superdough/noise.mjs diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 32ca0bb2..576ec3f1 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -112,3 +112,25 @@ export function createFilter( return filter; } + +// stays 1 until .5, then fades out +let wetfade = (d) => (d < 0.5 ? 1 : 1 - (d - 0.5) / 0.5); + +// mix together dry and wet nodes. 0 = only dry 1 = only wet +// still not too sure about how this could be used more generally... +export function drywet(dry, wet, wetAmount = 0) { + const ac = getAudioContext(); + if (!wetAmount) { + return dry; + } + let dry_gain = ac.createGain(); + let wet_gain = ac.createGain(); + dry.connect(dry_gain); + wet.connect(wet_gain); + dry_gain.gain.value = wetfade(wetAmount); + wet_gain.gain.value = wetfade(1 - wetAmount); + let mix = ac.createGain(); + dry_gain.connect(mix); + wet_gain.connect(mix); + return mix; +} diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs new file mode 100644 index 00000000..eeb2a9d0 --- /dev/null +++ b/packages/superdough/noise.mjs @@ -0,0 +1,51 @@ +import { drywet } from './helpers.mjs'; + +// expects one of noises as type +export function getNoiseOscillator(type = 'white', t) { + const ac = getAudioContext(); + const bufferSize = 2 * ac.sampleRate; + const noiseBuffer = ac.createBuffer(1, bufferSize, ac.sampleRate); + const output = noiseBuffer.getChannelData(0); + let lastOut = 0; + let b0, b1, b2, b3, b4, b5, b6; + b0 = b1 = b2 = b3 = b4 = b5 = b6 = 0.0; + + for (let i = 0; i < bufferSize; i++) { + if (type === 'white') { + output[i] = Math.random() * 2 - 1; + } else if (type === 'brown') { + let white = Math.random() * 2 - 1; + output[i] = (lastOut + 0.02 * white) / 1.02; + lastOut = output[i]; + } else if (type === 'pink') { + let white = Math.random() * 2 - 1; + b0 = 0.99886 * b0 + white * 0.0555179; + b1 = 0.99332 * b1 + white * 0.0750759; + b2 = 0.969 * b2 + white * 0.153852; + b3 = 0.8665 * b3 + white * 0.3104856; + b4 = 0.55 * b4 + white * 0.5329522; + b5 = -0.7616 * b5 - white * 0.016898; + output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362; + output[i] *= 0.11; + b6 = white * 0.115926; + } + } + + const o = ac.createBufferSource(); + o.buffer = noiseBuffer; + o.loop = true; + o.start(t); + return { + node: o, + stop: (time) => o.stop(time), + }; +} + +export function getNoiseMix(inputNode, wet, t) { + const noiseOscillator = getNoiseOscillator('pink', t); + const noiseMix = drywet(inputNode, noiseOscillator.node, wet); + return { + node: noiseMix, + stop: (time) => noiseOscillator?.stop(time), + }; +} diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index f12be9c8..8c07f34e 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,6 +1,7 @@ import { midiToFreq, noteToMidi } from './util.mjs'; import { registerSound, getAudioContext } from './superdough.mjs'; import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs'; +import { getNoiseMix } from './noise.mjs'; const mod = (freq, range = 1, type = 'sine') => { const ctx = getAudioContext(); @@ -35,7 +36,7 @@ export function registerSynthSounds() { if (waveforms.includes(s)) { sound = getOscillator(s, t, value); } else { - sound = getNoiseOscillator(t, s); + sound = getNoiseOscillator(s, t); } let { node: o, stop, triggerRelease } = sound; @@ -96,47 +97,6 @@ export function waveformN(partials, type) { return osc; } -// expects one of noises as type -export function getNoiseOscillator(t, type = 'white') { - const ac = getAudioContext(); - const bufferSize = 2 * ac.sampleRate; - const noiseBuffer = ac.createBuffer(1, bufferSize, ac.sampleRate); - const output = noiseBuffer.getChannelData(0); - let lastOut = 0; - let b0, b1, b2, b3, b4, b5, b6; - b0 = b1 = b2 = b3 = b4 = b5 = b6 = 0.0; - - for (let i = 0; i < bufferSize; i++) { - if (type === 'white') { - output[i] = Math.random() * 2 - 1; - } else if (type === 'brown') { - let white = Math.random() * 2 - 1; - output[i] = (lastOut + 0.02 * white) / 1.02; - lastOut = output[i]; - } else if (type === 'pink') { - let white = Math.random() * 2 - 1; - b0 = 0.99886 * b0 + white * 0.0555179; - b1 = 0.99332 * b1 + white * 0.0750759; - b2 = 0.969 * b2 + white * 0.153852; - b3 = 0.8665 * b3 + white * 0.3104856; - b4 = 0.55 * b4 + white * 0.5329522; - b5 = -0.7616 * b5 - white * 0.016898; - output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362; - output[i] *= 0.11; - b6 = white * 0.115926; - } - } - - const o = ac.createBufferSource(); - o.buffer = noiseBuffer; - o.loop = true; - o.start(t); - return { - node: o, - stop: (time) => o.stop(time), - }; -} - // expects one of waveforms as s export function getOscillator( s, @@ -224,32 +184,16 @@ export function getOscillator( vibratoOscillator.start(t); } - let noiseOscillator, noiseMix; - // noise mix - if (noise > 0) { - // Two gain nodes to set the oscillators to their respective levels - noise = noise > 1 ? 1 : noise; - let o_gain = ac.createGain(); - let n_gain = ac.createGain(); - o_gain.gain.setValueAtTime(1 - noise, ac.currentTime); - n_gain.gain.setValueAtTime(noise, ac.currentTime); - - // Instanciating a mixer to blend sources together - noiseMix = ac.createGain(); - - // Connecting the main oscillator to the gain node - o.connect(o_gain).connect(noiseMix); - - // Instanciating a noise oscillator and connecting - noiseOscillator = getNoiseOscillator(t, 'pink'); - noiseOscillator.node.connect(n_gain).connect(noiseMix); + let noiseMix; + if (noise) { + noiseMix = getNoiseMix(o, noise, t); } return { - node: noiseMix || o, + node: noiseMix?.node || o, stop: (time) => { vibratoOscillator?.stop(time); - noiseOscillator?.stop(time); + noiseMix?.stop(time); stopFm?.(time); o.stop(time); }, From 4b64168faa0c95b70edac6c6ec17ca178c72278a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 3 Oct 2023 12:20:28 +0200 Subject: [PATCH 40/48] fix: imports --- packages/superdough/noise.mjs | 1 + packages/superdough/synth.mjs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index eeb2a9d0..0e6c436e 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -1,4 +1,5 @@ import { drywet } from './helpers.mjs'; +import { getAudioContext } from './superdough.mjs'; // expects one of noises as type export function getNoiseOscillator(type = 'white', t) { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 8c07f34e..24d1d5ef 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,7 +1,7 @@ import { midiToFreq, noteToMidi } from './util.mjs'; import { registerSound, getAudioContext } from './superdough.mjs'; import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs'; -import { getNoiseMix } from './noise.mjs'; +import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; const mod = (freq, range = 1, type = 'sine') => { const ctx = getAudioContext(); From 047129223e62a0d161083853bcfe84a599e4ccc0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 3 Oct 2023 12:25:47 +0200 Subject: [PATCH 41/48] cache noise --- packages/superdough/noise.mjs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 0e6c436e..2c8c1d4a 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -1,9 +1,14 @@ import { drywet } from './helpers.mjs'; import { getAudioContext } from './superdough.mjs'; -// expects one of noises as type -export function getNoiseOscillator(type = 'white', t) { +let noiseCache = {}; + +// lazy generates noise buffers and keeps them forever +function getNoiseBuffer(type) { const ac = getAudioContext(); + if (noiseCache[type]) { + return noiseCache[type]; + } const bufferSize = 2 * ac.sampleRate; const noiseBuffer = ac.createBuffer(1, bufferSize, ac.sampleRate); const output = noiseBuffer.getChannelData(0); @@ -31,9 +36,15 @@ export function getNoiseOscillator(type = 'white', t) { b6 = white * 0.115926; } } + noiseCache[type] = noiseBuffer; + return noiseBuffer; +} +// expects one of noises as type +export function getNoiseOscillator(type = 'white', t) { + const ac = getAudioContext(); const o = ac.createBufferSource(); - o.buffer = noiseBuffer; + o.buffer = getNoiseBuffer(type); o.loop = true; o.start(t); return { From 376cf09565bb9c63a08c4eef43a6909186f00b29 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 3 Oct 2023 12:41:57 +0200 Subject: [PATCH 42/48] rename zzfx noise to znoise --- packages/core/controls.mjs | 11 ++++++++++- packages/superdough/zzfx.mjs | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 6cac6e54..39c2df2f 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -655,6 +655,15 @@ const generic_params = [ * .vib("<.5 1 2 4 8 16>:12") */ [['vib', 'vibmod'], 'vibrato', 'v'], + /** + * Adds pink noise to the mix + * + * @name noise + * @param {number | Pattern} wet wet amount + * @example + * sound("/2") + */ + ['noise'], /** * Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set * @@ -1153,7 +1162,7 @@ const generic_params = [ ['pitchJump'], ['pitchJumpTime'], ['lfo', 'repeatTime'], - ['noise'], + ['znoise'], // noise on the frequency or as bubo calls it "frequency fog" :) ['zmod'], ['zcrush'], // like crush but scaled differently ['zdelay'], diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index da505d74..a6af8260 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -20,7 +20,7 @@ export const getZZFX = (value, t) => { pitchJump = 0, pitchJumpTime = 0, lfo = 0, - noise = 0, + znoise = 0, zmod = 0, zcrush = 0, zdelay = 0, @@ -54,7 +54,7 @@ export const getZZFX = (value, t) => { pitchJump, pitchJumpTime, lfo, - noise, + znoise, zmod, zcrush, zdelay, From 9c9323e04099db415645d95a24e6969d3c21d113 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 3 Oct 2023 12:42:56 +0200 Subject: [PATCH 43/48] snapshot --- test/__snapshots__/examples.test.mjs.snap | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index e026f9c4..4a4c966d 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2959,6 +2959,15 @@ exports[`runs examples > example "never" example index 0 1`] = ` ] `; +exports[`runs examples > example "noise" example index 0 1`] = ` +[ + "[ (0/1 → 1/1) ⇝ 2/1 | s:white ]", + "[ 0/1 ⇜ (1/1 → 2/1) | s:white ]", + "[ (2/1 → 3/1) ⇝ 4/1 | s:pink ]", + "[ 2/1 ⇜ (3/1 → 4/1) | s:pink ]", +] +`; + exports[`runs examples > example "note" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c ]", From 020e85906d9ca9bb304eaedd2ae7814319eaf94b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 4 Oct 2023 09:42:10 +0200 Subject: [PATCH 44/48] fix: slider crash on some platforms --- packages/codemirror/slider.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index 62ec33c2..519e5610 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -91,8 +91,10 @@ export const sliderPlugin = ViewPlugin.fromClass( while (iterator.value) { // when the widgets are moved, we need to tell the dom node the current position // this is important because the updateSliderValue function has to work with the dom node - iterator.value.widget.slider.from = iterator.from; - iterator.value.widget.slider.to = iterator.to; + if (iterator.value?.widget?.slider) { + iterator.value.widget.slider.from = iterator.from; + iterator.value.widget.slider.to = iterator.to; + } iterator.next(); } } From 508af7eb72dfd2a56f3bad50355df987e0b75094 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 4 Oct 2023 23:48:53 +0200 Subject: [PATCH 45/48] update internal reverb param names --- packages/superdough/superdough.mjs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index e387ff6c..1e26cc9d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -108,11 +108,11 @@ function getDelay(orbit, delaytime, delayfeedback, t) { let reverbs = {}; -function getReverb(orbit, duration = 2, fade, revlp, revdim) { +function getReverb(orbit, duration = 2, fade, lp, dim) { // If no reverb has been created for a given orbit, create one if (!reverbs[orbit]) { const ac = getAudioContext(); - const reverb = ac.createReverb(getAudioContext(), duration, fade, revlp, revdim); + const reverb = ac.createReverb(getAudioContext(), duration, fade, lp, dim); reverb.connect(getDestination()); reverbs[orbit] = reverb; } @@ -120,14 +120,14 @@ function getReverb(orbit, duration = 2, fade, revlp, revdim) { if ( reverbs[orbit].duration !== duration || reverbs[orbit].fade !== fade || - reverbs[orbit].revlp !== revlp || - reverbs[orbit].revdim !== revdim + reverbs[orbit].lp !== lp || + reverbs[orbit].dim !== dim ) { - reverbs[orbit].setDuration(duration, fade, revlp, revdim); + reverbs[orbit].setDuration(duration, fade, lp, dim); reverbs[orbit].duration = duration; reverbs[orbit].fade = fade; - reverbs[orbit].revlp = revlp; - reverbs[orbit].revdim = revdim; + reverbs[orbit].lp = lp; + reverbs[orbit].dim = dim; } return reverbs[orbit]; From ce842f75612202b916c838a26e1fa7dd58ab3c35 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 4 Oct 2023 23:56:00 +0200 Subject: [PATCH 46/48] simplify createReverb --- packages/superdough/reverb.mjs | 4 ++-- packages/superdough/superdough.mjs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs index 7151e7fc..52d6983c 100644 --- a/packages/superdough/reverb.mjs +++ b/packages/superdough/reverb.mjs @@ -2,12 +2,12 @@ import reverbGen from './reverbGen.mjs'; if (typeof AudioContext !== 'undefined') { AudioContext.prototype.generateReverb = reverbGen.generateReverb; - AudioContext.prototype.createReverb = function (audioContext, duration, fade, revlp, revdim) { + AudioContext.prototype.createReverb = function (duration, fade, revlp, revdim) { const convolver = this.createConvolver(); convolver.setDuration = (d, fade, revlp, revdim) => { this.generateReverb( { - audioContext, + audioContext: this, sampleRate: 44100, numChannels: 2, decayTime: d, diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 1e26cc9d..0f2b3dd0 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -112,7 +112,7 @@ function getReverb(orbit, duration = 2, fade, lp, dim) { // If no reverb has been created for a given orbit, create one if (!reverbs[orbit]) { const ac = getAudioContext(); - const reverb = ac.createReverb(getAudioContext(), duration, fade, lp, dim); + const reverb = ac.createReverb(duration, fade, lp, dim); reverb.connect(getDestination()); reverbs[orbit] = reverb; } From 3c4f835d8b6fb8f520c7302bff4b83d5bd2dfe5d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 5 Oct 2023 00:00:47 +0200 Subject: [PATCH 47/48] consistent naming + simplify --- packages/superdough/reverb.mjs | 14 +++++++------- packages/superdough/superdough.mjs | 6 +----- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs index 52d6983c..de6c903e 100644 --- a/packages/superdough/reverb.mjs +++ b/packages/superdough/reverb.mjs @@ -2,9 +2,9 @@ import reverbGen from './reverbGen.mjs'; if (typeof AudioContext !== 'undefined') { AudioContext.prototype.generateReverb = reverbGen.generateReverb; - AudioContext.prototype.createReverb = function (duration, fade, revlp, revdim) { + AudioContext.prototype.createReverb = function (duration, fade, lp, dim) { const convolver = this.createConvolver(); - convolver.setDuration = (d, fade, revlp, revdim) => { + convolver.generate = (d, fade, lp, dim) => { this.generateReverb( { audioContext: this, @@ -12,8 +12,8 @@ if (typeof AudioContext !== 'undefined') { numChannels: 2, decayTime: d, fadeInTime: fade, - lpFreqStart: revlp, - lpFreqEnd: revdim, + lpFreqStart: lp, + lpFreqEnd: dim, }, (buffer) => { convolver.buffer = buffer; @@ -21,10 +21,10 @@ if (typeof AudioContext !== 'undefined') { ); convolver.duration = d; convolver.fade = fade; - convolver.revlp = revlp; - convolver.revdim = revdim; + convolver.lp = lp; + convolver.dim = dim; }; - convolver.setDuration(duration, fade, revlp, revdim); + convolver.generate(duration, fade, lp, dim); return convolver; }; } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 0f2b3dd0..ea8afc58 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -123,11 +123,7 @@ function getReverb(orbit, duration = 2, fade, lp, dim) { reverbs[orbit].lp !== lp || reverbs[orbit].dim !== dim ) { - reverbs[orbit].setDuration(duration, fade, lp, dim); - reverbs[orbit].duration = duration; - reverbs[orbit].fade = fade; - reverbs[orbit].lp = lp; - reverbs[orbit].dim = dim; + reverbs[orbit].generate(duration, fade, lp, dim); } return reverbs[orbit]; From 75099abbc125908df97ee80a12ede3abda5e1ee3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 5 Oct 2023 00:03:02 +0200 Subject: [PATCH 48/48] remove unused reverb method --- packages/superdough/reverbGen.mjs | 76 ------------------------------- 1 file changed, 76 deletions(-) diff --git a/packages/superdough/reverbGen.mjs b/packages/superdough/reverbGen.mjs index cac5d24a..49429937 100644 --- a/packages/superdough/reverbGen.mjs +++ b/packages/superdough/reverbGen.mjs @@ -70,82 +70,6 @@ reverbGen.generateGraph = function (data, width, height, min, max) { return canvas; }; -/** Saves an AudioBuffer as a 16-bit WAV file on the client's host - file system. Normalizes it to peak at +-32767, and optionally - truncates it if there's a lot of "silence" at the end. - - @param {!AudioBuffer} buffer The buffer to save. - @param {string} name Name of file to create. - @param {number?} opt_minTail Defines what counts as "silence" for - auto-truncating the buffer. If there is a point past which every - value of every channel is less than opt_minTail, then the buffer - is truncated at that point. This is expressed as an integer, - applying to the post-normalized and integer-converted - buffer. The default is 0, meaning don't truncate. */ -reverbGen.saveWavFile = function (buffer, name, opt_minTail) { - var bitsPerSample = 16; - var bytesPerSample = 2; - var sampleRate = buffer.sampleRate; - var numChannels = buffer.numberOfChannels; - var channels = getAllChannelData(buffer); - var numSampleFrames = channels[0].length; - var scale = 32767; - // Find normalization constant. - var max = 0; - for (var i = 0; i < numChannels; i++) { - for (var j = 0; j < numSampleFrames; j++) { - max = Math.max(max, Math.abs(channels[i][j])); - } - } - if (max) { - scale = 32767 / max; - } - // Find truncation point. - if (opt_minTail) { - var truncateAt = 0; - for (var i = 0; i < numChannels; i++) { - for (var j = 0; j < numSampleFrames; j++) { - var absSample = Math.abs(Math.round(scale * channels[i][j])); - if (absSample > opt_minTail) { - truncateAt = j; - } - } - } - numSampleFrames = truncateAt + 1; - } - var sampleDataBytes = bytesPerSample * numChannels * numSampleFrames; - var fileBytes = sampleDataBytes + 44; - var arrayBuffer = new ArrayBuffer(fileBytes); - var dataView = new DataView(arrayBuffer); - dataView.setUint32(0, 1179011410, true); // "RIFF" - dataView.setUint32(4, fileBytes - 8, true); // file length - dataView.setUint32(8, 1163280727, true); // "WAVE" - dataView.setUint32(12, 544501094, true); // "fmt " - dataView.setUint32(16, 16, true); // fmt chunk length - dataView.setUint16(20, 1, true); // PCM format - dataView.setUint16(22, numChannels, true); // NumChannels - dataView.setUint32(24, sampleRate, true); // SampleRate - var bytesPerSampleFrame = numChannels * bytesPerSample; - dataView.setUint32(28, sampleRate * bytesPerSampleFrame, true); // ByteRate - dataView.setUint16(32, bytesPerSampleFrame, true); // BlockAlign - dataView.setUint16(34, bitsPerSample, true); // BitsPerSample - dataView.setUint32(36, 1635017060, true); // "data" - dataView.setUint32(40, sampleDataBytes, true); - for (var j = 0; j < numSampleFrames; j++) { - for (var i = 0; i < numChannels; i++) { - dataView.setInt16(44 + j * bytesPerSampleFrame + i * bytesPerSample, Math.round(scale * channels[i][j]), true); - } - } - var blob = new Blob([arrayBuffer], { type: 'audio/wav' }); - var url = window.URL.createObjectURL(blob); - var linkEl = document.createElement('a'); - linkEl.href = url; - linkEl.download = name; - linkEl.style.display = 'none'; - document.body.appendChild(linkEl); - linkEl.click(); -}; - /** Applies a constantly changing lowpass filter to the given sound. @private