Compare commits

..

46 Commits

Author SHA1 Message Date
Vasilii Milovidov fbcd28f09a path fix 2023-10-16 14:26:58 +04:00
Vasilii Milovidov bfb32d89a9 Various improvements + reverb tests 2023-10-16 14:24:47 +04:00
Vasilii Milovidov 6e18bd8d57 tests: reverb 2023-10-16 11:59:23 +04:00
Vasilii Milovidov 9d22a9f9ba tests: reverb 2023-10-07 19:11:35 +04:00
Vasilii Milovidov c0411c389f tests: reverb 2023-10-06 03:14:56 +04:00
Vasilii Milovidov 163f8b53fd tests: reverb 2023-10-06 01:53:22 +04:00
Vasilii Milovidov d447b9cf95 tests: reverb 2023-10-05 21:41:15 +04:00
Vasilii Milovidov c0c6963112 tests: reverb 2023-10-05 16:16:53 +04:00
Vasilii Milovidov e793cb671a tests: reverb 2023-10-05 02:58:58 +04:00
Vasilii Milovidov f6fc55b43e tests: reverb 2023-10-04 01:30:20 +04:00
Vasilii Milovidov 56d992def9 tests: reverb 2023-10-03 14:20:35 +04:00
Vasilii Milovidov eef51f9228 tests: reverb 2023-10-03 04:52:31 +04:00
Vasilii Milovidov 56dd5bb604 tests: reverb 2023-10-03 00:04:07 +04:00
Vasilii Milovidov c2fc2272c5 tests: reverb 2023-10-02 22:11:25 +04:00
Vasilii Milovidov 18a198df9b tests: reverb 2023-10-02 21:53:08 +04:00
Vasilii Milovidov 870e7e4f32 begin implementing orbits 2023-10-02 15:20:32 +04:00
Vasilii Milovidov 6b219ee11a Structural changes, cpu usage improvements, some other fixes 2023-09-30 01:08:36 +04:00
Vasilii Milovidov 38af4af4ff Adding structs and traits 2023-09-29 13:09:27 +04:00
Vasilii Milovidov 69a3e27eec various fixes 2023-09-26 21:30:58 +04:00
Vasilii Milovidov 9606ce0d51 first go at handling samples from the web 2023-09-26 18:42:18 +04:00
Vasilii Milovidov 229dfe487a first go at handling samples from the web 2023-09-26 18:42:15 +04:00
Vasilii Milovidov 592b982990 wip 2023-09-26 15:54:39 +04:00
Vasilii Milovidov 6633b373bc wip 2023-09-26 14:04:20 +04:00
Vasilii Milovidov 5a8d369fa6 wip 2023-09-26 14:00:58 +04:00
Vasilii Milovidov 747ea8e6a3 wip 2023-09-26 13:56:11 +04:00
Vasilii Milovidov a693f89796 remove orbits for now, more adsr tweaking 2023-09-22 15:28:08 +04:00
Vasilii Milovidov b93ac61a7a add compressor 2023-09-22 11:20:46 +04:00
Vasilii Milovidov 6a066a5304 adsr tests 2023-09-22 05:17:12 +04:00
Vasilii Milovidov 4ca57b308f adsr tests 2023-09-21 14:54:19 +04:00
Vasilii Milovidov 97e2d89e61 implementing delay 2023-09-21 04:32:31 +04:00
Vasilii Milovidov f5ca1ff49d prettier 2023-09-20 16:39:00 +04:00
Vasilii Milovidov c456186f0f make it closer to the js implementation 2023-09-20 16:36:03 +04:00
Vasilii Milovidov ebbbcfb071 looper and reverse tests 2023-09-20 05:35:58 +04:00
Vasilii Milovidov 7492c9383a looper and reverse tests 2023-09-20 05:21:43 +04:00
Vasilii Milovidov 51a16b45a6 more adsr tests 2023-09-20 04:48:59 +04:00
Vasilii Milovidov 89d65ad09d sample reverse 2023-09-20 02:40:43 +04:00
Vasilii Milovidov a92834de42 sample reverse 2023-09-20 02:31:04 +04:00
Vasilii Milovidov c8270a98e9 make filter nodes conditional 2023-09-19 12:26:35 +04:00
Vasilii Milovidov 12d8e22b46 adsr tests 2023-09-19 10:37:34 +04:00
Vasilii Milovidov 78a8ca2b4f organize 2023-09-19 10:12:48 +04:00
Vasilii Milovidov c90bd23a53 Gain and filter ADSR envelopes 2023-09-18 19:11:20 +04:00
Vasilii Milovidov 725ddc805d webaudio for tauri WIP 2023-09-18 14:52:10 +04:00
Vasilii Milovidov 00c0df4bfb webaudio for tauri WIP 2023-09-18 14:41:56 +04:00
Vasilii Milovidov 7a17ec6863 Merge branch 'web_audio_tests' into tauri_webaudio
# Conflicts:
#	packages/desktopbridge/index.mjs
#	packages/desktopbridge/package.json
#	packages/desktopbridge/webaudiobridge.mjs
#	src-tauri/Cargo.lock
#	src-tauri/Cargo.toml
#	src-tauri/src/main.rs
#	src-tauri/src/webaudiobridge.rs
2023-09-18 14:18:55 +04:00
Vasilii Milovidov 081f93cb79 webaudio for tauri 2023-09-18 14:18:00 +04:00
Vasilii Milovidov 6d5fb0687c rust webaudio for tauri tests 2023-09-05 01:59:25 +04:00
46 changed files with 2810 additions and 1559 deletions
+1 -2
View File
@@ -18,5 +18,4 @@ vite.config.js
**/*.json
**/dev-dist
**/dist
/src-tauri/target/**/*
reverbGen.mjs
/src-tauri/target/**/*
-1
View File
@@ -1,4 +1,3 @@
export * from './codemirror.mjs';
export * from './highlight.mjs';
export * from './flash.mjs';
export * from './slider.mjs';
-135
View File
@@ -1,135 +0,0 @@
import { ref, pure } from '@strudel.cycles/core';
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, step, view) {
super();
this.value = value;
this.min = min;
this.max = max;
this.from = from;
this.originalFrom = from;
this.to = to;
this.step = step;
this.view = view;
}
eq() {
return false;
}
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.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
slider.value = slider.originalValue;
slider.from = this.from;
slider.originalFrom = this.originalFrom;
slider.to = this.to;
slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
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(e) {
return true;
}
}
export const setWidgets = StateEffect.define();
export const updateWidgets = (view, widgets) => {
view.dispatch({ effects: setWidgets.of(widgets) });
};
function getWidgets(widgetConfigs, view) {
return widgetConfigs.map(({ from, to, value, min, max, step }) => {
return Decoration.widget({
widget: new SliderWidget(value, min, max, from, to, step, view),
side: 0,
}).range(from /* , to */);
});
}
export const sliderPlugin = ViewPlugin.fromClass(
class {
decorations; //: DecorationSet
constructor(view /* : EditorView */) {
this.decorations = Decoration.set([]);
}
update(update /* : ViewUpdate */) {
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
if (iterator.value?.widget?.slider) {
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, update.view));
}
}
});
}
},
{
decorations: (v) => v.decorations,
},
);
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
};
// update state when sliders are moved
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (e.data.type === 'cm-slider') {
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)}`);
}
}
});
}
+14 -16
View File
@@ -655,15 +655,6 @@ 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("<white pink brown>/2")
*/
['noise'],
/**
* Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set
*
@@ -1020,23 +1011,30 @@ const generic_params = [
*
*/
['roomfade', 'rfade'],
/**
* Sets the sample to use as an impulse response for the reverb.
*
* @name iresponse
* @param {string | Pattern} sample sample to set as the impulse response
* @example
* s("bd sd").room(.8).ir("<shaker_large:0 shaker_large:2>")
*
*/
[['ir', 'i'], 'iresponse'],
/**
* 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 rsize, sz, size
* @synonyms size, sz
* @example
* s("bd sd").room(.8).rsize(1)
* @example
* s("bd sd").room(.8).rsize(4)
* s("bd sd").room(.8).roomsize("<0 1 2 4 8>")
*
*/
// 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?
['roomsize', 'size', 'sz', 'rsize'],
['size', 'sz', 'roomsize'],
// ['sagogo'],
// ['sclap'],
// ['sclaves'],
@@ -1206,7 +1204,7 @@ const generic_params = [
['pitchJump'],
['pitchJumpTime'],
['lfo', 'repeatTime'],
['znoise'], // noise on the frequency or as bubo calls it "frequency fog" :)
['noise'],
['zmod'],
['zcrush'], // like crush but scaled differently
['zdelay'],
-6
View File
@@ -2343,9 +2343,3 @@ export const fit = register('fit', (pat) =>
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
return _loopAt(factor, pat, cps);
});
/** exposes a custom value at query time. basically allows mutating state without evaluation */
export const ref = (accessor) =>
pure(1)
.withValue(() => reify(accessor()))
.innerJoin();
+2 -5
View File
@@ -16,17 +16,14 @@ export function repl({
transpiler,
onToggle,
editPattern,
createCyclist,
}) {
const cyclistParams = {
const scheduler = new Cyclist({
interval,
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
onToggle,
};
const scheduler = createCyclist?.(cyclistParams) || new Cyclist(cyclistParams);
});
const setPattern = (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
+1 -2
View File
@@ -43,8 +43,7 @@ function createClock(
clear();
};
const getPhase = () => phase;
const setPhase = (_phase) => (phase = _phase);
// setCallback
return { setDuration, start, stop, pause, duration, interval, getPhase, setPhase, minLatency };
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
}
export default createClock;
-77
View File
@@ -1,77 +0,0 @@
import { Cyclist } from '@strudel.cycles/core';
import { logger } from '../core/logger.mjs';
import { Invoke } from '../../website/src/tauri.mjs';
import { listen } from '@tauri-apps/api/event';
export class CyclistBridge extends Cyclist {
constructor(params) {
super(params);
this.start_timer;
this.abeLinkListener = listen('abelink-event', async (e) => {
const payload = e?.payload;
if (payload == null) {
return;
}
const { started, cps, phase, timestamp } = payload;
if (cps !== this.cps) {
this.setCps(cps);
}
// TODO: I'm not sure how to hook this up this phase adjustment in Strudel
// a phase adjustment message is sent every 30 seconds from backend to keep clocks in sync
const phaseDiff = Math.abs(phase - this.clock.getPhase());
if (phaseDiff > 0.1) {
console.log('set phase from', this.clock.getPhase(), 'to', phase);
// hmmm this seems wrong...
//this.clock.setPhase(phase);
}
if (this.started !== started && started != null) {
if (started) {
// the time delay in ms that seems to occur when starting the clock. Unsure if this is standard across all clients
const evaluationTime = 140;
// when start message comes from abelink, delay starting cyclist clock until the start of the next abelink phase
this.start_timer = window.setTimeout(() => {
// TODO: evaluate the code so if another source triggers the play there will not be an error
logger('[cyclist] start');
this.clock.start();
this.setStarted(true);
}, timestamp - Date.now() - evaluationTime);
} else {
this.stop();
}
}
});
}
start() {
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
const linkmsg = {
cps: this.cps,
started: true,
timestamp: Date.now(),
phase: this.clock.getPhase(),
};
Invoke('sendabelinkmsg', { linkmsg });
}
stop() {
logger('[cyclist] stop');
this.clock.stop();
this.lastEnd = 0;
this.setStarted(false);
const linkmsg = {
// TODO: change this to value of "main" clock cps
cps: 0,
started: false,
timestamp: Date.now(),
phase: this.clock.getPhase(),
};
Invoke('sendabelinkmsg', { linkmsg });
}
}
+1 -1
View File
@@ -7,4 +7,4 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './midibridge.mjs';
export * from './utils.mjs';
export * from './oscbridge.mjs';
export * from './cyclistbridge.mjs';
export * from './webaudiobridge.mjs';
+1
View File
@@ -22,6 +22,7 @@
"url": "https://github.com/tidalcycles/strudel/issues"
},
"dependencies": {
"@strudel.cycles/webaudio": "^0.9.0",
"@strudel.cycles/core": "workspace:*",
"@tauri-apps/api": "^1.4.0"
},
+244
View File
@@ -0,0 +1,244 @@
import { Invoke } from './utils.mjs';
import { duration, isNote, midiToFreq, noteToMidi, Pattern, valueToMidi } from '@strudel.cycles/core';
import { getAudioContext, getEnvelope, getSound, logger } from '@strudel.cycles/webaudio';
export const desktopAudio = async (value, deadline, hapDuration) => {
const ac = getAudioContext();
if (typeof value !== 'object') {
throw new Error(
`expected hap.value to be an object, but got "${value}". Hint: append .note() or .s() to the end`,
'error',
);
}
let t = ac.currentTime + deadline;
let {
note = 'C2',
s = 'triangle',
bank,
source,
gain = 0.8,
// low pass
cutoff = 8000,
resonance = 1,
// high pass
hcutoff = 0,
hresonance = 1,
// band pass
bandf = 0,
bandq = 1,
//
coarse,
crush,
shape,
pan,
vowel,
delay,
delayfeedback,
delaytime,
orbit = 1,
room,
roomfade,
roomlp,
roomdim,
size,
ir,
i = 0,
velocity = 1,
analyze, // analyser wet
fft = 8, // fftSize 0 - 10
speed = 1, // sample playback speed
begin = 0,
end = 1,
loop = 0,
loopBegin = 0,
loopEnd = 1,
attack,
decay,
sustain,
release,
lpattack,
lpdecay,
lpsustain,
lprelease,
lpenv,
hpattack,
hpdecay,
hpsustain,
hprelease,
hpenv,
bpattack,
bpdecay,
bpsustain,
bprelease,
bpenv,
n = 0,
freq,
unit,
} = value;
value.duration = hapDuration;
if (bank && s) {
s = `${bank}_${s}`;
}
if (freq !== undefined && note !== undefined) {
logger('[sampler] hap has note and freq. ignoring note', 'warning');
}
let midi = valueToMidi({ freq, note }, 36);
let transpose = midi - 36;
let sampleUrl;
let baseUrl;
let path;
const waveForms = new Set(['sine', 'square', 'saw', 'sawtooth', 'triangle']);
if (waveForms.has(s)) {
sampleUrl = 'none';
} else {
if (getSound(s).data.baseUrl !== undefined) {
baseUrl = getSound(s).data.baseUrl;
if (baseUrl === './piano/') {
path = 'https://strudel.tidalcycles.org/';
} else if (baseUrl === './EmuSP12/') {
path = 'https://strudel.tidalcycles.org/';
} else {
path = '';
}
}
let map = getSound(s).data.samples;
if (Array.isArray(map)) {
sampleUrl =
path !== undefined ? path + map[n % map.length].replace('./', '') : map[n % map.length].replace('./', '');
} else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
const closest = Object.keys(map)
.filter((k) => !k.startsWith('_'))
.reduce(
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
null,
);
transpose = -midiDiff(closest); // semitones to repitch
sampleUrl =
path !== undefined
? path + map[closest][n % map[closest].length].replace('./', '')
: map[closest][n % map[closest].length].replace('./', '');
}
}
if (isNote(note)) {
note = noteToMidi(note);
}
const playbackRate = 1.0 * Math.pow(2, transpose / 12);
if (delay !== 0) {
delay = Math.abs(delay);
delayfeedback = Math.abs(delayfeedback);
delaytime = Math.abs(delaytime);
}
let ir_path;
let url;
if (ir !== undefined) {
if (getSound(ir).data.baseUrl !== undefined) {
baseUrl = getSound(ir).data.baseUrl;
if (baseUrl === './piano/') {
ir_path = 'https://strudel.tidalcycles.org/';
} else if (baseUrl === './EmuSP12/') {
ir_path = 'https://strudel.tidalcycles.org/';
} else {
ir_path = '';
}
}
let ir_samples = getSound(ir);
if (Array.isArray(ir_samples)) {
url =
ir_path !== undefined
? ir_path + ir_samples[n % ir_samples.length].replace('./', '')
: ir_samples[n % ir_samples.length].replace('./', '');
} else if (typeof ir_samples === 'object') {
console.log(
'obk',
Object.values(ir_samples.data.samples)[i & Object.values(ir_samples.data.samples).length].replace('./', ''),
);
url =
ir_path !== undefined
? ir_path +
Object.values(ir_samples.data.samples)[i & Object.values(ir_samples.data.samples).length].replace('./', '')
: Object.values(ir_samples.data.samples)[i & Object.values(ir_samples.data.samples).length].replace('./', '');
}
}
console.log('url', url);
let folder;
if (baseUrl !== undefined) {
folder = baseUrl.replace('./', '') + '/' + s + '/';
} else {
folder = 'misc';
}
const dirname = folder + '/' + s + '/';
const offset = (t - getAudioContext().currentTime) * 1000;
const roundedOffset = Math.round(offset);
const messagesfromjs = [];
const packages = {
loop: [loop, loopBegin, loopEnd],
delay: [delay, delaytime, delayfeedback],
reverb: [room, size, roomfade, roomlp, roomdim,ir, url],
lpf: [cutoff, resonance],
hpf: [hcutoff, hresonance],
bpf: [bandf, bandq],
adsr: [attack, decay, sustain, release],
lpenv: [lpattack, lpdecay, lpsustain, lprelease, lpenv],
hpenv: [hpattack, hpdecay, hpsustain, hprelease, hpenv],
bpenv: [bpattack, bpdecay, bpsustain, bprelease, bpenv],
};
messagesfromjs.push({
note: midiToFreq(note),
offset: roundedOffset,
waveform: s,
lpf: packages.lpf,
hpf: packages.hpf,
bpf: packages.bpf,
duration: hapDuration,
velocity: velocity,
delay: packages.delay,
reverb: packages.reverb,
orbit: orbit,
speed: speed,
begin: begin,
end: end,
looper: packages.loop,
adsr: packages.adsr,
lpenv: packages.lpenv,
hpenv: packages.hpenv,
bpenv: packages.bpenv,
n: n,
sampleurl: sampleUrl,
dirname: dirname,
unit: unit,
playbackrate: playbackRate,
});
if (messagesfromjs.length) {
setTimeout(() => {
Invoke('sendwebaudio', { messagesfromjs });
});
}
};
const hap2value = (hap) => {
hap.ensureObjectValue();
return { ...hap.value, velocity: hap.context.velocity };
};
export const webaudioDesktopOutputTrigger = (t, hap, ct, cps) =>
desktopAudio(hap2value(hap), t - ct, hap.duration / cps, cps);
export const webaudioDesktopOutput = (hap, deadline, hapDuration) =>
desktopAudio(hap2value(hap), deadline, hapDuration);
Pattern.prototype.webaudio = function () {
return this.onTrigger(webaudioDesktopOutputTrigger);
};
+18 -71
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import { Pattern, isPattern, logger, ref } from '@strudel.cycles/core';
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
import { noteToMidi } from '@strudel.cycles/core';
import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance:
@@ -15,8 +15,8 @@ function supportsMidi() {
return typeof navigator.requestMIDIAccess === 'function';
}
function getMidiDeviceNamesString(devices) {
return devices.map((o) => `'${o.name}'`).join(' | ');
function getMidiDeviceNamesString(outputs) {
return outputs.map((o) => `'${o.name}'`).join(' | ');
}
export function enableWebMidi(options = {}) {
@@ -52,41 +52,30 @@ export function enableWebMidi(options = {}) {
});
});
}
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
const outputByName = (name) => WebMidi.getOutputByName(name);
function getDevice(indexOrName, devices) {
if (!devices.length) {
// output?: string | number, outputs: typeof WebMidi.outputs
function getDevice(output, outputs) {
if (!outputs.length) {
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
}
if (typeof indexOrName === 'number') {
return devices[indexOrName];
if (typeof output === 'number') {
return outputs[output];
}
const byName = (name) => devices.find((output) => output.name.includes(name));
if (typeof indexOrName === 'string') {
return byName(indexOrName);
if (typeof output === 'string') {
return outputByName(output);
}
// attempt to default to first IAC device if none is specified
const IACOutput = byName('IAC');
const device = IACOutput ?? devices[0];
const IACOutput = outputs.find((output) => output.name.includes('IAC'));
const device = IACOutput ?? outputs[0];
if (!device) {
throw new Error(
`🔌 MIDI device '${device ? device : ''}' not found. Use one of ${getMidiDeviceNamesString(devices)}`,
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${getMidiDeviceNamesString(WebMidi.outputs)}`,
);
}
return IACOutput ?? devices[0];
}
// send start/stop messages to outputs when repl starts/stops
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (!WebMidi?.enabled) {
return;
}
if (e.data === 'strudel-stop') {
WebMidi.outputs.forEach((output) => output.sendStop());
}
// cannot start here, since we have no timing info, see sendStart below
});
return IACOutput ?? outputs[0];
}
Pattern.prototype.midi = function (output) {
@@ -114,7 +103,6 @@ Pattern.prototype.midi = function (output) {
return this.onTrigger((time, hap, currentTime, cps) => {
if (!WebMidi.enabled) {
console.log('not enabled');
return;
}
const device = getDevice(output, WebMidi.outputs);
@@ -125,7 +113,7 @@ Pattern.prototype.midi = function (output) {
const timeOffsetString = `+${offset}`;
// destructure value
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd } = hap.value;
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
@@ -137,7 +125,7 @@ Pattern.prototype.midi = function (output) {
time: timeOffsetString,
});
}
if (ccv !== undefined && ccn !== undefined) {
if (ccv && ccn) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1');
}
@@ -147,46 +135,5 @@ Pattern.prototype.midi = function (output) {
const scaled = Math.round(ccv * 127);
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
}
if (hap.whole.begin + 0 === 0) {
// we need to start here because we have the timing info
device.sendStart({ time: timeOffsetString });
}
if (['clock', 'midiClock'].includes(midicmd)) {
device.sendClock({ time: timeOffsetString });
} else if (['start'].includes(midicmd)) {
device.sendStart({ time: timeOffsetString });
} else if (['stop'].includes(midicmd)) {
device.sendStop({ time: timeOffsetString });
} else if (['continue'].includes(midicmd)) {
device.sendContinue({ time: timeOffsetString });
}
});
};
let listeners = {};
const refs = {};
export async function midin(input) {
const initial = await enableWebMidi(); // only returns on first init
const device = getDevice(input, WebMidi.inputs);
if (initial) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`,
);
refs[input] = {};
}
const cc = (cc) => ref(() => refs[input][cc] || 0);
listeners[input] && device.removeListener('midimessage', listeners[input]);
listeners[input] = (e) => {
const cc = e.dataBytes[0];
const v = e.dataBytes[1];
refs[input] && (refs[input][cc] = v / 127);
};
device.addListener('midimessage', listeners[input]);
return cc;
}
+191 -246
View File
@@ -200,21 +200,20 @@ function peg$parse(input, options) {
var peg$c23 = "*";
var peg$c24 = "?";
var peg$c25 = ":";
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$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$r0 = /^[1-9]/;
var peg$r1 = /^[eE]/;
@@ -256,67 +255,64 @@ 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("..", 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$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$f0 = function() { return parseFloat(text()); };
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});
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});
for (const op of ops) {
op(result);
}
return result;
};
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$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$currPos = 0;
var peg$savedPos = 0;
var peg$posDetailsCache = [{ line: 1, column: 1 }];
@@ -655,26 +651,6 @@ 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;
@@ -908,7 +884,7 @@ function peg$parse(input, options) {
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f2(s2);
s0 = peg$f1(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -944,7 +920,7 @@ function peg$parse(input, options) {
if (s6 !== peg$FAILED) {
s7 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f3(s4);
s0 = peg$f2(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -992,7 +968,7 @@ function peg$parse(input, options) {
}
s8 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f4(s4, s7);
s0 = peg$f3(s4, s7);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1024,7 +1000,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f5(s2);
s0 = peg$f4(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1064,7 +1040,7 @@ function peg$parse(input, options) {
if (s6 !== peg$FAILED) {
s7 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f6(s4);
s0 = peg$f5(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1114,9 +1090,6 @@ 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();
}
}
}
}
@@ -1142,7 +1115,7 @@ function peg$parse(input, options) {
s2 = peg$parsenumber();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f7(s2);
s0 = peg$f6(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1170,7 +1143,7 @@ function peg$parse(input, options) {
s2 = peg$parsenumber();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f8(s2);
s0 = peg$f7(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1224,7 +1197,7 @@ function peg$parse(input, options) {
}
if (s13 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f9(s3, s7, s11);
s0 = peg$f8(s3, s7, s11);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1264,7 +1237,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f10(s2);
s0 = peg$f9(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1292,7 +1265,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f11(s2);
s0 = peg$f10(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1322,7 +1295,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f12(s2);
s0 = peg$f11(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1346,35 +1319,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
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);
s0 = peg$f12(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1400,7 +1345,7 @@ function peg$parse(input, options) {
s3 = peg$parseslice_op();
}
peg$savedPos = s0;
s0 = peg$f15(s1, s2);
s0 = peg$f13(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1425,7 +1370,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f16(s1);
s1 = peg$f14(s1);
}
s0 = s1;
@@ -1474,7 +1419,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f17(s1);
s1 = peg$f15(s1);
}
s0 = s1;
@@ -1523,7 +1468,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f18(s1);
s1 = peg$f16(s1);
}
s0 = s1;
@@ -1544,7 +1489,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f19(s1, s2);
s0 = peg$f17(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1564,7 +1509,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f20(s1, s2);
s0 = peg$f18(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1587,7 +1532,7 @@ function peg$parse(input, options) {
s6 = peg$parsequote();
if (s6 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f21(s4);
s0 = peg$f19(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1637,19 +1582,19 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 6) === peg$c27) {
s1 = peg$c27;
if (input.substr(peg$currPos, 6) === peg$c26) {
s1 = peg$c26;
peg$currPos += 6;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e34); }
if (peg$silentFails === 0) { peg$fail(peg$e33); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
s3 = peg$parsemini_or_operator();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f22(s3);
s0 = peg$f20(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1666,12 +1611,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3, s4, s5;
s0 = peg$currPos;
if (input.substr(peg$currPos, 6) === peg$c28) {
s1 = peg$c28;
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$e35); }
if (peg$silentFails === 0) { peg$fail(peg$e34); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1682,7 +1627,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f23(s4);
s0 = peg$f21(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1707,12 +1652,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$c29) {
s1 = peg$c29;
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$e36); }
if (peg$silentFails === 0) { peg$fail(peg$e35); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1727,7 +1672,7 @@ function peg$parse(input, options) {
s7 = null;
}
peg$savedPos = s0;
s0 = peg$f24(s3, s5, s7);
s0 = peg$f22(s3, s5, s7);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1747,6 +1692,35 @@ 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;
@@ -1755,6 +1729,35 @@ 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();
@@ -1773,16 +1776,16 @@ function peg$parse(input, options) {
return s0;
}
function peg$parserotL() {
function peg$parsefast() {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c31) {
s1 = peg$c31;
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$e38); }
if (peg$silentFails === 0) { peg$fail(peg$e39); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1802,74 +1805,16 @@ 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$c34) {
s1 = peg$c34;
if (input.substr(peg$currPos, 5) === peg$c33) {
s1 = peg$c33;
peg$currPos += 5;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e41); }
if (peg$silentFails === 0) { peg$fail(peg$e40); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1889,7 +1834,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f29(s4);
s0 = peg$f27(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1914,12 +1859,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 2) === peg$c35) {
s1 = peg$c35;
if (input.substr(peg$currPos, 2) === peg$c34) {
s1 = peg$c34;
peg$currPos += 2;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e42); }
if (peg$silentFails === 0) { peg$fail(peg$e41); }
}
if (s1 !== peg$FAILED) {
s2 = [];
@@ -1928,7 +1873,7 @@ function peg$parse(input, options) {
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e43); }
if (peg$silentFails === 0) { peg$fail(peg$e42); }
}
while (s3 !== peg$FAILED) {
s2.push(s3);
@@ -1937,7 +1882,7 @@ function peg$parse(input, options) {
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e43); }
if (peg$silentFails === 0) { peg$fail(peg$e42); }
}
}
s1 = [s1, s2];
@@ -1954,12 +1899,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$c36) {
s1 = peg$c36;
if (input.substr(peg$currPos, 3) === peg$c35) {
s1 = peg$c35;
peg$currPos += 3;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e44); }
if (peg$silentFails === 0) { peg$fail(peg$e43); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1981,7 +1926,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) {
peg$savedPos = s7;
s7 = peg$f30(s5, s9);
s7 = peg$f28(s5, s9);
} else {
peg$currPos = s7;
s7 = peg$FAILED;
@@ -1998,7 +1943,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) {
peg$savedPos = s7;
s7 = peg$f30(s5, s9);
s7 = peg$f28(s5, s9);
} else {
peg$currPos = s7;
s7 = peg$FAILED;
@@ -2018,7 +1963,7 @@ function peg$parse(input, options) {
}
if (s8 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f31(s5, s6);
s0 = peg$f29(s5, s6);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2064,7 +2009,7 @@ function peg$parse(input, options) {
s4 = peg$parsecomment();
}
peg$savedPos = s0;
s0 = peg$f32(s1);
s0 = peg$f30(s1);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2075,18 +2020,18 @@ function peg$parse(input, options) {
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 36) {
s3 = peg$c37;
s3 = peg$c36;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e45); }
if (peg$silentFails === 0) { peg$fail(peg$e44); }
}
if (s3 !== peg$FAILED) {
s4 = peg$parsews();
s5 = peg$parsemini_or_operator();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f33(s1, s5);
s0 = peg$f31(s1, s5);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2111,7 +2056,7 @@ function peg$parse(input, options) {
s1 = peg$parsemini_or_operator();
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f34(s1);
s1 = peg$f32(s1);
}
s0 = s1;
if (s0 === peg$FAILED) {
@@ -2144,7 +2089,7 @@ function peg$parse(input, options) {
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f35(s2);
s0 = peg$f33(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2157,19 +2102,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$c37) {
s1 = peg$c37;
peg$currPos += 6;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e46); }
if (peg$silentFails === 0) { peg$fail(peg$e45); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f36(s3);
s0 = peg$f34(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2186,19 +2131,19 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 6) === peg$c39) {
s1 = peg$c39;
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$e47); }
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$f37(s3);
s0 = peg$f35(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2215,16 +2160,16 @@ function peg$parse(input, options) {
var s0, s1;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c40) {
s1 = peg$c40;
if (input.substr(peg$currPos, 4) === peg$c39) {
s1 = peg$c39;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e48); }
if (peg$silentFails === 0) { peg$fail(peg$e47); }
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f38();
s1 = peg$f36();
}
s0 = s1;
+1 -7
View File
@@ -79,9 +79,6 @@ frac
int
= zero / (digit1_9 DIGIT*)
intneg
= minus? int { return parseInt(text()); }
minus
= "-"
@@ -126,7 +123,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 / op_range
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail
op_weight = "@" a:number
{ return x => x.options_['weight'] = a }
@@ -149,9 +146,6 @@ 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});
-11
View File
@@ -45,17 +45,6 @@ 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`);
}
-6
View File
@@ -184,12 +184,6 @@ 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', () => {
@@ -15,11 +15,10 @@ import {
updateMiniLocations,
} from '@strudel/codemirror';
import './style.css';
import { sliderPlugin } from '@strudel/codemirror/slider.mjs';
export { flash, highlightMiniLocations, updateMiniLocations };
const staticExtensions = [javascript(), flashField, highlightExtension, sliderPlugin];
const staticExtensions = [javascript(), flashField, highlightExtension];
export default function CodeMirror({
value,
-2
View File
@@ -19,7 +19,6 @@ function useStrudel({
drawContext,
drawTime = [-2, 2],
paintOptions = {},
createCyclist, // (params) => Cyclist
}) {
const id = useMemo(() => s4(), []);
canvasId = canvasId || `canvas-${id}`;
@@ -46,7 +45,6 @@ function useStrudel({
onEvalError?.(err);
},
getTime,
createCyclist,
drawContext,
transpiler,
editPattern,
-13
View File
@@ -1,13 +0,0 @@
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 };
}
-79
View File
@@ -1,79 +0,0 @@
import { getAudioContext } from './superdough.mjs';
let worklet;
export async function dspWorklet(ac, code) {
const name = `dsp-worklet-${Date.now()}`;
const workletCode = `${code}
let __q = []; // trigger queue
class MyProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.t = 0;
this.stopped = false;
this.port.onmessage = (e) => {
if(e.data==='stop') {
this.stopped = true;
} else if(e.data?.dough) {
__q.push(e.data)
} else {
msg?.(e.data)
}
};
}
process(inputs, outputs, parameters) {
const output = outputs[0];
if(__q.length) {
for(let i=0;i<__q.length;++i) {
const deadline = __q[i].time-currentTime;
if(deadline<=0) {
trigger(__q[i].dough)
__q.splice(i,1)
}
}
}
for (let i = 0; i < output[0].length; i++) {
const out = dsp(this.t / sampleRate);
output.forEach((channel) => {
channel[i] = out;
});
this.t++;
}
return !this.stopped;
}
}
registerProcessor('${name}', MyProcessor);
`;
const base64String = btoa(workletCode);
const dataURL = `data:text/javascript;base64,${base64String}`;
await ac.audioWorklet.addModule(dataURL);
const node = new AudioWorkletNode(ac, name);
const stop = () => node.port.postMessage('stop');
return { node, stop };
}
const stop = () => {
if (worklet) {
worklet?.stop();
worklet?.node?.disconnect();
}
};
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (e.data === 'strudel-stop') {
stop();
} else if (e.data?.dough) {
worklet?.node.port.postMessage(e.data);
}
});
}
export const dough = async (code) => {
const ac = getAudioContext();
stop();
worklet = await dspWorklet(ac, code);
worklet.node.connect(ac.destination);
};
export function doughTrigger(t, hap, currentTime, duration, cps) {
window.postMessage({ time: t, dough: hap.value, currentTime, duration, cps });
}
-22
View File
@@ -112,25 +112,3 @@ 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;
}
-1
View File
@@ -10,4 +10,3 @@ export * from './helpers.mjs';
export * from './synth.mjs';
export * from './zzfx.mjs';
export * from './logger.mjs';
export * from './dspworklet.mjs';
-63
View File
@@ -1,63 +0,0 @@
import { drywet } from './helpers.mjs';
import { getAudioContext } from './superdough.mjs';
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);
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;
}
}
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 = getNoiseBuffer(type);
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),
};
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "0.9.9",
"version": "0.9.8",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
+17 -24
View File
@@ -1,30 +1,23 @@
import reverbGen from './reverbGen.mjs';
if (typeof AudioContext !== 'undefined') {
AudioContext.prototype.generateReverb = reverbGen.generateReverb;
AudioContext.prototype.createReverb = function (duration, fade, lp, dim) {
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;
};
AudioContext.prototype.createReverb = function (duration) {
const convolver = this.createConvolver();
convolver.generate = (d, fade, lp, dim) => {
this.generateReverb(
{
audioContext: this,
sampleRate: 44100,
numChannels: 2,
decayTime: d,
fadeInTime: fade,
lpFreqStart: lp,
lpFreqEnd: dim,
},
(buffer) => {
convolver.buffer = buffer;
},
);
convolver.duration = d;
convolver.fade = fade;
convolver.lp = lp;
convolver.dim = dim;
convolver.setDuration = (d) => {
convolver.buffer = this.impulseResponse(d);
convolver.duration = duration;
return convolver;
};
convolver.generate(duration, fade, lp, dim);
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
-129
View File
@@ -1,129 +0,0 @@
// 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.
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;
};
/** 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.<!Float32Array>} 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;
+8 -19
View File
@@ -107,25 +107,17 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
}
let reverbs = {};
function getReverb(orbit, duration = 2, fade, lp, dim) {
// If no reverb has been created for a given orbit, create one
function getReverb(orbit, duration = 2) {
if (!reverbs[orbit]) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim);
const reverb = ac.createReverb(duration);
reverb.connect(getDestination());
reverbs[orbit] = reverb;
}
if (
reverbs[orbit].duration !== duration ||
reverbs[orbit].fade !== fade ||
reverbs[orbit].lp !== lp ||
reverbs[orbit].dim !== dim
) {
reverbs[orbit].generate(duration, fade, lp, dim);
if (reverbs[orbit].duration !== duration) {
reverbs[orbit] = reverbs[orbit].setDuration(duration);
reverbs[orbit].duration = duration;
}
return reverbs[orbit];
}
@@ -223,10 +215,7 @@ export const superdough = async (value, deadline, hapDuration) => {
delaytime = 0.25,
orbit = 1,
room,
roomfade = 0.1,
roomlp = 15000,
roomdim = 1000,
roomsize = 2,
size = 2,
velocity = 1,
analyze, // analyser wet
fft = 8, // fftSize 0 - 10
@@ -364,8 +353,8 @@ export const superdough = async (value, deadline, hapDuration) => {
}
// reverb
let reverbSend;
if (room > 0 && roomsize > 0) {
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim);
if (room > 0 && size > 0) {
const reverbNode = getReverb(orbit, size);
reverbSend = effectSend(post, reverbNode, room);
}
+74 -97
View File
@@ -1,7 +1,6 @@
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
@@ -21,25 +20,74 @@ 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() {
[...waveforms, ...noises].forEach((s) => {
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
registerSound(
s,
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 } = value;
let sound;
if (waveforms.includes(s)) {
sound = getOscillator(s, t, value);
} else {
sound = getNoiseOscillator(s, t);
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,
});
let { node: o, stop, triggerRelease } = sound;
// 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;
}
// turn down
const g = gainNode(0.3);
@@ -56,9 +104,10 @@ export function registerSynthSounds() {
node: o.connect(g).connect(envelope),
stop: (releaseTime) => {
releaseEnvelope(releaseTime);
triggerRelease?.(releaseTime);
fmEnvelope?.stop(releaseTime);
let end = releaseTime + release;
stop(end);
stopFm?.(end);
},
};
},
@@ -97,108 +146,36 @@ export function waveformN(partials, type) {
return osc;
}
// 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();
export function getOscillator({ s, freq, t, vib, vibmod, partials }) {
// Make oscillator with partial count
let o;
// 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 {
} 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;
let vibrato_oscillator;
if (vib > 0) {
vibratoOscillator = getAudioContext().createOscillator();
vibratoOscillator.frequency.value = vib;
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;
vibratoOscillator.connect(gain);
vibrato_oscillator.connect(gain);
gain.connect(o.detune);
vibratoOscillator.start(t);
}
let noiseMix;
if (noise) {
noiseMix = getNoiseMix(o, noise, t);
vibrato_oscillator.start(t);
}
return {
node: noiseMix?.node || o,
node: o,
stop: (time) => {
vibratoOscillator?.stop(time);
noiseMix?.stop(time);
stopFm?.(time);
vibrato_oscillator?.stop(time);
o.stop(time);
},
triggerRelease: (time) => {
fmEnvelope?.stop(time);
},
};
}
+2 -2
View File
@@ -20,7 +20,7 @@ export const getZZFX = (value, t) => {
pitchJump = 0,
pitchJumpTime = 0,
lfo = 0,
znoise = 0,
noise = 0,
zmod = 0,
zcrush = 0,
zdelay = 0,
@@ -54,7 +54,7 @@ export const getZZFX = (value, t) => {
pitchJump,
pitchJumpTime,
lfo,
znoise,
noise,
zmod,
zcrush,
zdelay,
+6 -36
View File
@@ -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, emitWidgets = true } = options;
const { wrapAsync = false, addReturn = true, emitMiniLocations = 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 */) {
@@ -35,18 +35,6 @@ export function transpiler(input, options = {}) {
emitMiniLocations && collectMiniLocations(value, node);
return this.replace(miniWithLocation(value, node));
}
if (isWidgetFunction(node)) {
emitWidgets &&
widgets.push({
from: node.arguments[0].start,
to: node.arguments[0].end,
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));
}
// TODO: remove pseudo note variables?
if (node.type === 'Identifier' && isNoteWithOctave(node.name)) {
this.skip();
@@ -76,14 +64,15 @@ export function transpiler(input, options = {}) {
if (!emitMiniLocations) {
return { output };
}
return { output, miniLocations, widgets };
return { output, miniLocations };
}
function isStringWithDoubleQuotes(node, locations, code) {
if (node.type !== 'Literal') {
const { raw, type } = node;
if (type !== 'Literal') {
return false;
}
return node.raw[0] === '"';
return raw[0] === '"';
}
function isBackTickString(node, parent) {
@@ -105,22 +94,3 @@ function miniWithLocation(value, node) {
optional: false,
};
}
// 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 id = 'slider_' + node.arguments[0].start; // use loc of first arg for id
// add loc as identifier to first argument
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
node.arguments.unshift({
type: 'Literal',
value: id,
raw: id,
});
node.callee.name = 'sliderWithID';
return node;
}
+1 -5
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as strudel from '@strudel.cycles/core';
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
import { superdough, getAudioContext, setLogger } from 'superdough';
const { Pattern, logger } = strudel;
setLogger(logger);
@@ -35,7 +35,3 @@ export function webaudioScheduler(options = {}) {
onTrigger: strudel.getTrigger({ defaultOutput, getTime }),
});
}
Pattern.prototype.dough = function () {
return this.onTrigger(doughTrigger, 1);
};
+3 -3
View File
@@ -181,6 +181,9 @@ importers:
'@strudel.cycles/core':
specifier: workspace:*
version: link:../core
'@strudel.cycles/webaudio':
specifier: ^0.9.0
version: link:../webaudio
'@tauri-apps/api':
specifier: ^1.4.0
version: 1.4.0
@@ -588,9 +591,6 @@ 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
+1147 -85
View File
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -21,7 +21,13 @@ tauri = { version = "1.4.0", features = ["fs-all"] }
midir = "0.9.1"
tokio = { version = "1.29.0", features = ["full"] }
rosc = "0.10.1"
rusty_link = "0.3.6"
web-audio-api = { git = "https://github.com/orottier/web-audio-api-rs.git", branch = "main" }
reqwest = "0.11.20"
quick_cache = "0.4.0"
mini-moka = "0.10.2"
hound = "3.4.0"
rand = "0.8.5"
rand_distr = "0.4.3"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
@@ -32,5 +38,5 @@ custom-protocol = ["tauri/custom-protocol"]
[profile.release]
panic = "abort"
codegen-units = 1
lto = true
lto = "fat"
opt-level = "s"
+1 -2
View File
@@ -4,8 +4,7 @@ Rust source files for building native desktop apps using Tauri
## Usage
- Install [Rust](https://rustup.rs/) on your system.
- Install `cmake` on your system. OSX: `brew install cmake`, Linux: `sudo apt-get install cmake`
Install [Rust](https://rustup.rs/) on your system.
From the project root:
-175
View File
@@ -1,175 +0,0 @@
use std::time::{ Duration, SystemTime, UNIX_EPOCH };
use rusty_link::{ AblLink, SessionState };
use std::sync::Arc;
use tokio::sync::Mutex;
use serde::Deserialize;
use std::thread::sleep;
use crate::loggerbridge::Logger;
use tauri::Window;
fn bpm_to_cps(bpm: f64) -> f64 {
let cpm = bpm / 4.0;
return cpm / 60.0;
}
fn cps_to_bpm(cps: f64) -> f64 {
let cpm = cps * 60.0;
return cpm * 4.0;
}
fn current_unix_time() -> Duration {
let current_unix_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
return current_unix_time;
}
#[derive(Deserialize, Clone, serde::Serialize)]
pub struct LinkMsg {
pub started: bool,
pub cps: f64,
pub timestamp: u64,
pub phase: f64,
}
pub struct AbeLinkStateContainer {
pub abelink: Arc<Mutex<AbeLinkState>>,
}
pub struct AbeLinkState {
pub link: AblLink,
pub session_state: SessionState,
pub running: bool,
pub quantum: f64,
pub window: Arc<Window>,
}
impl AbeLinkState {
pub fn new(window: Arc<Window>) -> Self {
Self {
link: AblLink::new(120.0),
session_state: SessionState::new(),
running: true,
quantum: 4.0,
window,
}
}
pub fn unix_time_at_next_phase(&self) -> u64 {
let link_time_stamp = self.link.clock_micros();
let quantum = self.quantum;
let beat = self.session_state.beat_at_time(link_time_stamp, quantum);
let phase = self.session_state.phase_at_time(link_time_stamp, quantum);
let internal_time_at_next_phase = self.session_state.time_at_beat(beat + (quantum - phase), quantum);
let time_offset = Duration::from_micros((internal_time_at_next_phase - link_time_stamp) as u64);
let current_unix_time = current_unix_time();
let unix_time_at_next_phase = (current_unix_time + time_offset).as_millis();
return unix_time_at_next_phase as u64;
}
pub fn cps(&self) -> f64 {
let bpm = self.session_state.tempo();
let cps = bpm_to_cps(bpm);
return cps;
}
pub fn capture_app_state(&mut self) {
self.link.capture_app_session_state(&mut self.session_state);
}
pub fn commit_app_state(&mut self) {
self.link.commit_app_session_state(&self.session_state);
}
pub fn send(&self, payload: LinkMsg) {
let _ = self.window.emit("abelink-event", payload);
}
pub fn send_started(&self) {
let cps = self.cps();
let started = self.session_state.is_playing();
let payload = LinkMsg {
cps,
started,
timestamp: self.unix_time_at_next_phase(),
phase: 0.0,
};
self.send(payload);
}
pub fn send_cps(&self) {
let cps = self.cps();
let started = self.session_state.is_playing();
let phase = self.session_state.phase_at_time(self.link.clock_micros(), self.quantum);
let payload = LinkMsg {
cps,
started,
timestamp: current_unix_time().as_millis() as u64,
phase,
};
self.send(payload);
}
pub fn send_phase(&self) {
self.send_started();
}
}
pub fn init(_logger: Logger, abelink: Arc<Mutex<AbeLinkState>>) {
tauri::async_runtime::spawn(async move {
let mut prev_is_started = false;
let mut prev_cps = 0.0;
let mut time_since_last_phase_send = 0;
let sleep_time = 10;
/* .......................................................................
Evaluate Abelink State and send messages back to JS side when needed.
........................................................................*/
loop {
let mut state = abelink.lock().await;
state.capture_app_state();
if state.link.is_enabled() == false {
state.link.enable(true);
state.link.enable_start_stop_sync(true);
}
let started = state.session_state.is_playing();
if started != prev_is_started {
state.send_started();
prev_is_started = started;
} else if state.cps() != prev_cps && state.cps() != 0.0 {
state.send_cps();
prev_cps = state.cps();
// a phase sync message needs to be sent to strudel every 30 seconds to keep clock drift at bay
} else if time_since_last_phase_send > 30000 {
state.send_phase();
time_since_last_phase_send = 0;
}
drop(state);
sleep(Duration::from_millis(sleep_time));
time_since_last_phase_send = time_since_last_phase_send + sleep_time;
}
});
}
// Called from JS
#[tauri::command]
pub async fn sendabelinkmsg(linkmsg: LinkMsg, state: tauri::State<'_, AbeLinkStateContainer>) -> Result<(), String> {
let mut abelink = state.abelink.lock().await;
abelink.capture_app_state();
let started = abelink.session_state.is_playing();
let time_stamp = abelink.link.clock_micros();
let quantum = abelink.quantum;
let linkmsg_bpm = cps_to_bpm(linkmsg.cps);
if linkmsg.started != started {
abelink.session_state.set_is_playing_and_request_beat_at_time(linkmsg.started, time_stamp as u64, 0.0, quantum);
}
if linkmsg_bpm != abelink.session_state.tempo() && linkmsg_bpm != 0.0 {
abelink.session_state.set_tempo(linkmsg_bpm, time_stamp);
}
abelink.commit_app_state();
drop(abelink);
Ok(())
}
+38 -41
View File
@@ -4,14 +4,20 @@
mod midibridge;
mod oscbridge;
mod loggerbridge;
mod ablelinkbridge;
mod webaudiobridge;
// mod superdough;
// mod reverbgen;
mod webaudio;
use std::sync::Arc;
use ablelinkbridge::AbeLinkState;
use loggerbridge::Logger;
use tauri::Manager;
use tokio::sync::mpsc;
use tokio::sync::Mutex;
use tokio::{
sync::mpsc,
sync::Mutex
};
// the payload type must implement `Serialize` and `Clone`.
#[derive(Clone, serde::Serialize)]
struct Payload {
@@ -23,42 +29,33 @@ fn main() {
let (async_output_transmitter_midi, async_output_receiver_midi) = mpsc::channel(1);
let (async_input_transmitter_osc, async_input_receiver_osc) = mpsc::channel(1);
let (async_output_transmitter_osc, async_output_receiver_osc) = mpsc::channel(1);
let (async_input_transmitter_webaudio, async_input_receiver_webaudio) = mpsc::channel(1);
let (async_output_transmitter_webaudio, async_output_receiver_webaudio) = mpsc::channel(1);
tauri::Builder
::default()
.manage(midibridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_midi),
})
.manage(oscbridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_osc),
})
.invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc, ablelinkbridge::sendabelinkmsg])
.setup(|app| {
let window = Arc::new(app.get_window("main").unwrap());
let logger = Logger { window: window.clone() };
midibridge::init(
logger.clone(),
async_input_receiver_midi,
async_output_receiver_midi,
async_output_transmitter_midi
);
oscbridge::init(
logger.clone(),
async_input_receiver_osc,
async_output_receiver_osc,
async_output_transmitter_osc
);
// This state must be declared in the setup so it can be shared between invoked commands and the initialized function
let abelink = Arc::new(Mutex::new(AbeLinkState::new(window)));
app.manage(ablelinkbridge::AbeLinkStateContainer {
abelink: abelink.clone(),
});
ablelinkbridge::init(logger.clone(), abelink);
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
::default()
.manage(midibridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_midi),
})
.manage(oscbridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_osc),
})
.manage(webaudiobridge::AsyncInputTransmitWebAudio {
inner: Mutex::new(async_input_transmitter_webaudio),
})
.invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc, webaudiobridge::sendwebaudio])
.setup(|app| {
let window = Arc::new(app.get_window("main").unwrap());
let logger = Logger { window };
midibridge::init(
logger.clone(),
async_input_receiver_midi,
async_output_receiver_midi,
async_output_transmitter_midi
);
oscbridge::init(logger, async_input_receiver_osc, async_output_receiver_osc, async_output_transmitter_osc);
webaudiobridge::init(async_input_receiver_webaudio, async_output_receiver_webaudio, async_output_transmitter_webaudio);
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+5
View File
@@ -0,0 +1,5 @@
mod superdough;
mod reverbgen;
pub use self::superdough::*;
pub use self::reverbgen::*;
+241
View File
@@ -0,0 +1,241 @@
use std::collections::HashMap;
use std::fs::File;
use mini_moka::sync::Cache;
use rand_distr::{Distribution, Normal};
use web_audio_api::AudioBuffer;
use web_audio_api::context::{AudioContext, BaseAudioContext, OfflineAudioContext};
use web_audio_api::node::{AudioNode, DynamicsCompressorNode};
use crate::webaudio::{create_filepath_generic, download_file, Reverb, Sampler, WebAudioInstrument};
use crate::webaudiobridge::WebAudioMessage;
fn apply_decay(input: &mut [f32], decay_factor: f32) {
let len = input.len();
for i in 0..len {
// Multiply each sample by an exponentially decreasing factor
let decay_amt = (-(i as f32) / (len as f32) * decay_factor).exp();
input[i] *= decay_amt;
}
}
fn apply_fir_filter(input: &mut [f32], coefficients: &[f32]) {
let num_coeffs = coefficients.len();
let len = input.len();
let input_copy = input.to_vec();
for i in num_coeffs..len {
let mut acc = 0.0;
for j in 0..num_coeffs {
acc += input_copy[i - j] * coefficients[j];
}
input[i] = acc;
}
}
fn sinc(x: f32, fc: f32) -> f32 {
let pi = std::f32::consts::PI;
if x == 0.0 {
2.0 * fc
} else {
let arg = 2.0 * pi * fc * x;
arg.sin() / arg
}
}
fn hamming(n: usize, size: usize) -> f32 {
0.54 - 0.46 * ((2. * std::f32::consts::PI * n as f32) / ((size - 1) as f32)).cos()
}
pub fn generate_coefficients_lp(size: usize, fc: f32) -> Vec<f32> {
let mut h = vec![0.0; size];
let m = (size - 1) / 2;
for n in 0..size {
h[n] = sinc((n as f32 - m as f32), fc);
h[n] *= hamming(n, size);
}
let sum: f32 = h.iter().sum();
for n in 0..size {
h[n] /= sum;
}
h
}
pub fn create_impulse_response(
len: usize,
decay_factor: f32,
fir_coeffs: &[f32],
sample_rate: f32,
fade: f32,
) -> Vec<f32> {
let mut rng = rand::thread_rng();
let normal = Normal::new(0.0, 1.0).unwrap();
let fade_in_samples = (fade * sample_rate) as usize; // Compute the number of samples for the fade-in duration
let mut impulse_response: Vec<f32> = (0..len)
.map(|_| normal.sample(&mut rng) as f32)
.map(|v| v.max(-1.0).min(1.0)) // Clamp values to the -1.0 to 1.0 range
.collect();
// Apply fade-in to the starting samples
for i in 0..fade_in_samples.min(len) {
impulse_response[i] *= i as f32 / fade_in_samples as f32;
}
apply_decay(&mut impulse_response, decay_factor);
apply_fir_filter(&mut impulse_response, fir_coeffs);
impulse_response
}
pub fn write_to_buffer(buffer: &mut AudioBuffer, data: &[f32]) {
let num_chans = buffer.number_of_channels();
for i in 0..num_chans {
let chan = buffer.get_channel_data_mut(i);
for j in 0..chan.len() {
chan[j] = data[j];
}
}
}
pub fn apply_reverb<T: WebAudioInstrument>(mut audio_context: &AudioContext, compressor: &DynamicsCompressorNode, cache: &Cache<String, AudioBuffer>, message: &WebAudioMessage, instrument: &mut T, reverbs: &mut HashMap<usize, Reverb>) {
if message.reverb.ir.is_none() {
let key = format!("reverb: {}{}{}{}{}", message.reverb.room.unwrap_or(0.0), message.reverb.size.unwrap_or(1.0), message.reverb.roomlp.unwrap_or(0.0), message.reverb.roomfade.unwrap_or(0.0), message.reverb.roomdim.unwrap_or(0.0));
if let Some(ir) = cache.get(&key) {
let reverb = reverbs.entry(message.orbit).or_insert({
let mut reverb = Reverb::new(&audio_context, message.reverb.room, message.reverb.size, message.reverb.roomlp, message.reverb.roomfade, message.reverb.roomdim, &compressor);
reverb.convolver.set_buffer(ir.clone());
instrument.connect_reverb(&reverb.convolver);
reverb
});
if reverb.room.unwrap_or(0.0) != message.reverb.room.unwrap_or(0.0)
|| reverb.roomlp.unwrap_or(0.0) != message.reverb.roomlp.unwrap_or(0.0)
|| reverb.roomfade.unwrap_or(0.0) != message.reverb.roomfade.unwrap_or(0.0)
|| reverb.roomdim.unwrap_or(0.3) != message.reverb.roomdim.unwrap_or(0.0)
|| reverb.roomsize.unwrap_or(0.0) != message.reverb.size.unwrap_or(0.0)
|| reverb.roomfade.unwrap_or(0.0) != message.reverb.roomfade.unwrap_or(0.0) {
reverb.room = message.reverb.room;
reverb.roomlp = message.reverb.roomlp;
reverb.roomfade = message.reverb.roomfade;
reverb.roomdim = message.reverb.roomdim;
reverb.roomsize = message.reverb.size;
let len = (audio_context.sample_rate() * message.reverb.size.unwrap_or(0.6)) as usize;
let decay = message.reverb.roomdim.unwrap_or(2.0);
let num_coeffs = 101;
let fade = message.reverb.roomfade.unwrap_or(0.01);
let cutoff = message.reverb.roomlp.unwrap_or(1000.0) / audio_context.sample_rate();
let cache_cache = cache.clone();
let mut buffer = audio_context.create_buffer(2, len, 44100.0);
tokio::spawn(async move {
// GENERATE IR
let fir = generate_coefficients_lp(num_coeffs, cutoff);
let ir = create_impulse_response(len, decay, &fir, 44100.0, fade);
write_to_buffer(&mut buffer, &ir);
cache_cache.insert(key, buffer);
});
}
reverb.convolver.set_buffer(ir);
instrument.connect_reverb(&reverb.convolver);
// Generate new IR buffer if it doesn't exist
} else {
let len = (audio_context.sample_rate() * message.reverb.size.unwrap_or(1.0)) as usize;
let decay = 4.0;
let num_coeffs = 101;
let fade = 2.4;
let cutoff = 500.0 / audio_context.sample_rate();
let cache_cache = cache.clone();
let mut buffer = audio_context.create_buffer(2, len, 44100.0);
tokio::spawn(async move {
// Generate IR and write to buffer
let fir = generate_coefficients_lp(num_coeffs, cutoff);
let ir = create_impulse_response(len, decay, &fir, 44100.0, fade);
write_to_buffer(&mut buffer, &ir);
cache_cache.insert(key, buffer);
});
}
} else if message.reverb.ir.is_some() {
let message_url = &match message.reverb.url.clone() {
Some(url) => url,
None => "todo".to_string(),
};
let (ir_url, ir_file_path, ir_file_path_clone) = create_filepath_generic(message_url, &message.dirname);
let ir_cache_clone = cache.clone();
let requested_size = message.reverb.size.unwrap_or(2.0).clone();
if let Some(ir_buffer) = cache.get(&ir_file_path_clone) {
let reverb = reverbs.entry(message.orbit).or_insert({
let mut reverb = Reverb::new(&audio_context, message.reverb.room, message.reverb.size, message.reverb.roomlp, message.reverb.roomfade, message.reverb.roomdim, &compressor);
reverb.convolver.set_buffer(ir_buffer.clone());
instrument.connect_reverb(&reverb.convolver);
reverb
});
if reverb.roomsize.unwrap_or(0.0) != message.reverb.size.unwrap_or(0.0) {
adjust_length(&mut audio_context, cache, message, ir_file_path_clone, requested_size, reverb);
}
reverb.convolver.set_buffer(ir_buffer);
instrument.connect_reverb(&reverb.convolver);
} else {
tokio::spawn(async move {
if let Ok(file1) = File::open(&ir_file_path) {
create_new_ir_buffer_from_file(&ir_file_path, ir_cache_clone, requested_size, file1);
} else if tokio::fs::metadata(&ir_file_path.clone()).await.is_err() {
download_file(ir_url, &ir_file_path).await;
}
});
}
}
}
fn create_new_ir_buffer_from_file(ir_file_path: &String, ir_cache_clone: Cache<String, AudioBuffer>, requested_size: f32, file1: File) {
let context = OfflineAudioContext::new(2, 400, 44100.0);
let ir_buffer = context.decode_audio_data_sync(file1).expect("Failed to decode file.");
let new_length = ir_buffer.sample_rate() * requested_size;
let mut new_buffer = context.create_buffer(ir_buffer.number_of_channels(), new_length as usize, ir_buffer.sample_rate());
for ch in 0..ir_buffer.number_of_channels() {
let old_data = ir_buffer.get_channel_data(ch);
let new_data = new_buffer.get_channel_data_mut(ch);
for i in 0..new_length as usize {
if i < old_data.len() {
new_data[i] = old_data[i];
} else {
new_data[i] = 0.0;
}
}
}
ir_cache_clone.insert(ir_file_path.clone(), new_buffer);
}
fn adjust_length(audio_context: &mut &AudioContext, cache: &Cache<String, AudioBuffer>, message: &WebAudioMessage, ir_file_path_clone: String, requested_size: f32, reverb: &mut Reverb) {
let buf = reverb.convolver.buffer().unwrap();
reverb.roomsize = message.reverb.size;
let new_length = buf.sample_rate() * requested_size;
let mut new_buffer = audio_context.create_buffer(buf.number_of_channels(), new_length as usize, buf.sample_rate());
for ch in 0..buf.number_of_channels() {
let old_data = buf.get_channel_data(ch);
let new_data = new_buffer.get_channel_data_mut(ch);
for i in 0..new_length as usize {
if i < old_data.len() {
new_data[i] = old_data[i];
} else {
new_data[i] = 0.0;
}
}
}
reverb.convolver.set_buffer(new_buffer.clone());
cache.insert(ir_file_path_clone.clone(), new_buffer);
}
+393
View File
@@ -0,0 +1,393 @@
use std::collections::HashMap;
use std::path::Path;
use reqwest::Url;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use web_audio_api::{
context::{AudioContext, BaseAudioContext},
AudioBuffer,
node::{AudioBufferSourceNode, AudioNode, AudioScheduledSourceNode, BiquadFilterNode, BiquadFilterType, GainNode, OscillatorNode, OscillatorType},
node::BiquadFilterType::{Bandpass, Highpass, Lowpass},
};
use web_audio_api::node::{ConvolverNode, DelayNode, DynamicsCompressorNode};
use crate::webaudiobridge::WebAudioMessage;
#[derive(Clone, Copy, Debug)]
pub struct DelayMessage {
pub wet: Option<f32>,
pub delay_time: Option<f32>,
pub feedback: Option<f32>,
}
#[derive(Clone, Debug)]
pub struct ReverbMessage {
pub room: Option<f32>,
pub size: Option<f32>,
pub roomfade: Option<f32>,
pub roomlp: Option<f32>,
pub roomdim: Option<f32>,
pub ir: Option<String>,
pub url: Option<String>,
}
#[derive(Clone, Copy, Debug)]
pub struct LoopMessage {
pub is_loop: u8,
pub loop_start: f64,
pub loop_end: f64,
}
#[derive(Clone, Copy, Debug)]
pub struct LPFMessage {
pub frequency: f32,
pub resonance: f32,
}
#[derive(Clone, Copy, Debug)]
pub struct HPFMessage {
pub frequency: f32,
pub resonance: f32,
}
#[derive(Clone, Copy, Debug)]
pub struct BPFMessage {
pub frequency: f32,
pub resonance: f32,
}
#[derive(Debug, Copy, Clone)]
pub struct ADSRMessage {
pub attack: Option<f64>,
pub decay: Option<f64>,
pub sustain: Option<f32>,
pub release: Option<f64>,
}
#[derive(Debug, Copy, Clone)]
pub struct FilterADSRMessage {
pub attack: Option<f64>,
pub decay: Option<f64>,
pub sustain: Option<f64>,
pub release: Option<f64>,
pub env: Option<f64>,
}
pub trait WebAudioInstrument {
fn set_adsr(&mut self, t: f64, adsr: &ADSRMessage, velocity: f32, duration: f64);
fn play(&mut self, t: f64, message: &WebAudioMessage, duration: f64);
fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec<BiquadFilterNode>;
fn connect_delay(&mut self, delay: &DelayNode);
fn connect_reverb(&mut self, reverb: &ConvolverNode);
}
pub struct Delay {
pub delay: DelayNode,
pub input: GainNode,
pub output: GainNode,
pub feedback: GainNode,
pub pre_gain: GainNode,
}
impl Delay {
pub fn new(context: &AudioContext, compressor: &DynamicsCompressorNode) -> Self {
let delay = context.create_delay(1.);
let input = context.create_gain();
let output = context.create_gain();
let feedback = context.create_gain();
let pre_gain = context.create_gain();
output.connect(compressor);
delay.connect(&output);
feedback.connect(&delay);
pre_gain.connect(&feedback);
input.connect(&pre_gain);
Self { delay, input, output, feedback, pre_gain }
}
}
pub struct Reverb {
pub convolver: ConvolverNode,
pub room: Option<f32>,
pub roomsize: Option<f32>,
pub roomfade: Option<f32>,
pub roomlp: Option<f32>,
pub roomdim: Option<f32>,
}
impl Reverb {
pub(crate) fn new(context: &AudioContext, room: Option<f32>, roomsize: Option<f32>, roomfade: Option<f32>, roomlp: Option<f32>, roomdim: Option<f32>, compressor: &DynamicsCompressorNode) -> Self {
let mut convolver = context.create_convolver();
convolver.connect(compressor);
Self { convolver, room, roomsize, roomfade, roomlp, roomdim }
}
}
pub struct Synth {
pub oscillator: OscillatorNode,
pub envelope: GainNode,
}
impl Synth {
pub fn new(context: &mut AudioContext) -> Self {
let oscillator = context.create_oscillator();
let envelope = context.create_gain();
Self { oscillator, envelope }
}
pub fn set_frequency(&mut self, frequency: &f32) {
self.oscillator.frequency().set_value(*frequency);
}
pub fn set_waveform(&mut self, waveform: &str) {
match waveform {
"sine" => self.oscillator.set_type(OscillatorType::Sine),
"square" => self.oscillator.set_type(OscillatorType::Square),
"triangle" => self.oscillator.set_type(OscillatorType::Triangle),
"saw" | "sawtooth" => self.oscillator.set_type(OscillatorType::Sawtooth),
_ => {}
}
}
}
impl WebAudioInstrument for Synth {
fn set_adsr(&mut self, t: f64, adsr: &ADSRMessage, velocity: f32, duration: f64) {
let attack = adsr.attack.unwrap_or(0.001);
let decay = adsr.decay.unwrap_or(0.05);
let sustain = adsr.sustain.unwrap_or(0.6);
let release = adsr.release.unwrap_or(0.01);
self.envelope.gain()
.set_value_at_time(0.0, t)
.linear_ramp_to_value_at_time(velocity, t + attack)
.exponential_ramp_to_value_at_time((sustain + 0.0001) * velocity, t + attack + decay)
// .set_value_at_time((sustain + 0.00001) * velocity, t + duration)
.exponential_ramp_to_value_at_time(0.000001, t + duration + release);
}
fn play(&mut self, t: f64, message: &WebAudioMessage, release: f64) {
self.oscillator.start();
self.oscillator.stop_at(t + message.duration + release);
}
fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec<BiquadFilterNode> {
let mut filters = Vec::new();
if message.bpf.frequency > 0.0 {
let mut bpf = context.create_biquad_filter();
bpf.set_type(Bandpass);
bpf.frequency().set_value(message.bpf.frequency);
bpf.q().set_value(message.bpf.resonance);
filters.push(bpf);
} else if message.lpf.frequency > 0.0 {
let mut lpf = context.create_biquad_filter();
lpf.set_type(Lowpass);
lpf.frequency().set_value(message.lpf.frequency);
lpf.q().set_value(message.lpf.resonance);
filters.push(lpf);
} else if message.hpf.frequency > 0.0 {
let mut hpf = context.create_biquad_filter();
hpf.set_type(Highpass);
hpf.frequency().set_value(message.hpf.frequency);
hpf.q().set_value(message.hpf.resonance);
filters.push(hpf);
}
if !filters.is_empty() {
self.oscillator.connect(filters.first().unwrap());
filters.last().unwrap().connect(&self.envelope);
} else {
self.oscillator.connect(&self.envelope);
};
filters
}
fn connect_delay(&mut self, delay: &DelayNode) {
self.envelope.connect(delay);
}
fn connect_reverb(&mut self, reverb: &ConvolverNode) {
self.envelope.connect(reverb);
}
}
pub struct Sampler {
pub sample: AudioBufferSourceNode,
pub envelope: GainNode,
}
impl Sampler {
pub fn new(context: &mut AudioContext, audio_buffer: AudioBuffer) -> Self {
let mut sample = context.create_buffer_source();
sample.set_buffer(audio_buffer);
let envelope = context.create_gain();
Self { sample, envelope }
}
}
impl WebAudioInstrument for Sampler {
fn set_adsr(&mut self, t: f64, adsr: &ADSRMessage, velocity: f32, duration: f64) {
let attack = adsr.attack.unwrap_or(0.001);
let decay = adsr.decay.unwrap_or(0.001);
let sustain = adsr.sustain.unwrap_or(1.0);
let release = adsr.release.unwrap_or(0.01);
self.envelope.gain()
.set_value_at_time(0.0, t)
.linear_ramp_to_value_at_time(velocity, t + attack)
.linear_ramp_to_value_at_time((sustain + 0.00001) * velocity, t + attack + decay)
.set_value_at_time((sustain + 0.00001) * velocity, t + duration)
.linear_ramp_to_value_at_time(0.0, t + duration + release);
}
fn play(&mut self, t: f64, message: &WebAudioMessage, release: f64) {
let buffer_duration = release;
let (start_at, stop_at) = if message.speed < 0.0 {
(buffer_duration, t + message.duration + 0.2)
} else {
(message.begin * buffer_duration, t + message.duration + message.adsr.release.unwrap_or(0.01))
};
if message.looper.is_loop > 0 {
self.sample.set_loop(true);
self.sample.set_loop_start(message.looper.loop_start);
self.sample.set_loop_end(message.looper.loop_end);
self.sample.start_at_with_offset(
t,
self.sample.loop_start(),
);
self.sample.stop_at(t + message.duration + message.adsr.release.unwrap_or(0.01));
} else {
self.sample.start_at_with_offset(
t,
start_at,
);
self.sample.stop_at(stop_at);
}
}
fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec<BiquadFilterNode> {
let mut filters = Vec::new();
if message.bpf.frequency > 0.0 {
let mut bpf = context.create_biquad_filter();
bpf.set_type(Bandpass);
bpf.frequency().set_value(message.bpf.frequency);
bpf.q().set_value(message.bpf.resonance);
filters.push(bpf);
} else if message.lpf.frequency > 0.0 {
let mut lpf = context.create_biquad_filter();
lpf.set_type(Lowpass);
lpf.frequency().set_value(message.lpf.frequency);
lpf.q().set_value(message.lpf.resonance);
filters.push(lpf);
} else if message.hpf.frequency > 0.0 {
let mut hpf = context.create_biquad_filter();
hpf.set_type(Highpass);
hpf.frequency().set_value(message.hpf.frequency);
hpf.q().set_value(message.hpf.resonance);
filters.push(hpf);
}
if !filters.is_empty() {
self.sample.connect(filters.first().unwrap());
filters.last().unwrap().connect(&self.envelope);
} else {
self.sample.connect(&self.envelope);
};
filters
}
fn connect_delay(&mut self, delay: &DelayNode) {
self.envelope.connect(delay);
}
fn connect_reverb(&mut self, reverb: &ConvolverNode) {
self.envelope.connect(reverb);
}
}
pub fn is_filter_envelope_on(adsr: &FilterADSRMessage) -> bool {
adsr.env.is_some() || adsr.attack.is_some() || adsr.decay.is_some() || adsr.sustain.is_some() || adsr.release.is_some()
}
pub fn apply_filter_adsr(filter_node: &BiquadFilterNode, message: &WebAudioMessage, filter: &BiquadFilterType, now: f64) {
let env = match filter {
Lowpass => message.lpenv,
Highpass => message.hpenv,
Bandpass => message.bpenv,
_ => message.lpenv,
};
let freq = match filter {
Lowpass => message.lpf.frequency,
Highpass => message.hpf.frequency,
Bandpass => message.bpf.frequency,
_ => 8000.0,
};
let offset = env.env.unwrap_or(1.0) * 0.5;
let min = (2f32.powf(-offset as f32) * freq).clamp(0.0, 20000.0);
let max = (2f32.powf((env.env.unwrap_or(1.0) - offset) as f32) * freq).clamp(0.0, 20000.0);
let range = max - min;
let peak = min + range;
let sustain_level = min + env.sustain.unwrap_or(1.0) as f32 * range;
filter_node.frequency().set_value_at_time(min, now)
.linear_ramp_to_value_at_time(peak, now + env.attack.unwrap_or(0.01))
.linear_ramp_to_value_at_time(sustain_level, now + env.attack.unwrap_or(0.01) + env.decay.unwrap_or(0.01))
// .set_value_at_time(sustain_level, now + message.duration)
.linear_ramp_to_value_at_time(min, now + message.duration + env.release.unwrap_or(0.01));
}
pub fn apply_delay<T: WebAudioInstrument>(audio_context: &mut AudioContext, delays: &mut HashMap<usize, Delay>, compressor: &DynamicsCompressorNode, message: &WebAudioMessage, t: f64, instrument: &mut T) {
delays.entry(message.orbit).or_insert({
Delay::new(&audio_context, &compressor)
});
let delay = delays.get(&message.orbit).unwrap();
instrument.connect_delay(&delay.delay);
delay.delay.delay_time().set_value(message.delay.delay_time.unwrap_or(0.25));
delay.feedback.gain().set_value(message.delay.feedback.unwrap_or(0.5));
delay.pre_gain.gain().set_value_at_time(message.delay.wet.unwrap_or(0.25), t + message.delay.delay_time.unwrap_or(0.5) as f64);
}
pub fn apply_delay_synth(audio_context: &mut AudioContext, delays: &mut HashMap<usize, Delay>, compressor: &DynamicsCompressorNode, message: &WebAudioMessage, t: f64, synth: &mut Synth) {
delays.entry(message.orbit).or_insert({
Delay::new(&audio_context, &compressor)
});
let delay = delays.get(&message.orbit).unwrap();
synth.envelope.connect(&delay.input);
delay.delay.delay_time().set_value(message.delay.delay_time.unwrap_or(0.25));
delay.feedback.gain().set_value(message.delay.feedback.unwrap_or(0.5));
delay.pre_gain.gain().set_value_at_time(message.delay.wet.unwrap_or(0.25), t + message.delay.delay_time.unwrap_or(0.5) as f64);
}
pub async fn download_file(ir_url: Url, ir_file_path: &String) {
let response = reqwest::get(ir_url)
.await
.unwrap_or_else(|_| panic!("Failed to send GET request"));
let bytes = response.bytes().await.unwrap();
let path = Path::new(&ir_file_path);
let mut file = create_file_and_dirs(path).await;
file.write_all(&bytes)
.await
.unwrap_or_else(|_| panic!("Failed to write to file"));
}
pub fn create_filepath_generic(url_string: &str, dirname: &str) -> (Url, String, String) {
let url = Url::parse(url_string).expect("failed to parse url");
let filename = url.path_segments()
.and_then(Iterator::last)
.and_then(|name| if name.is_empty() { None } else { Some(name) })
.unwrap_or("tmp.ben");
let file_path = format!("samples/{}{}", dirname, filename);
let file_path_clone = file_path.clone();
(url, file_path, file_path_clone)
}
pub async fn create_file_and_dirs(path: &Path) -> fs::File {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await.unwrap();
}
let file = fs::File::create(path).await.unwrap();
file
}
+368
View File
@@ -0,0 +1,368 @@
use std::{
sync::Arc,
time::Duration,
fs::File,
path::Path,
time::Instant,
};
use std::collections::HashMap;
use mini_moka::sync::Cache;
use tokio::{
sync::{mpsc, Mutex},
io::AsyncWriteExt,
};
use serde::Deserialize;
use web_audio_api::{
context::BaseAudioContext,
AudioBuffer,
context::{AudioContext, AudioContextLatencyCategory, AudioContextOptions},
node::AudioNode,
};
use web_audio_api::context::OfflineAudioContext;
use crate::webaudio::*;
#[derive(Debug, Clone)]
pub struct WebAudioMessage {
pub note: f32,
pub instant: Instant,
pub offset: f64,
pub waveform: String,
pub lpf: LPFMessage,
pub hpf: HPFMessage,
pub bpf: BPFMessage,
pub duration: f64,
pub velocity: f32,
pub delay: DelayMessage,
pub reverb: ReverbMessage,
pub orbit: usize,
pub speed: f32,
pub begin: f64,
pub end: f64,
pub looper: LoopMessage,
pub adsr: ADSRMessage,
pub lpenv: FilterADSRMessage,
pub hpenv: FilterADSRMessage,
pub bpenv: FilterADSRMessage,
pub n: usize,
pub sampleurl: String,
pub dirname: String,
pub unit: Option<String>,
pub playbackrate: f32,
}
pub struct AsyncInputTransmitWebAudio {
pub inner: Mutex<mpsc::Sender<Vec<WebAudioMessage>>>,
}
pub fn init(
async_input_receiver: mpsc::Receiver<Vec<WebAudioMessage>>,
mut async_output_receiver: mpsc::Receiver<Vec<WebAudioMessage>>,
async_output_transmitter: mpsc::Sender<Vec<WebAudioMessage>>,
) {
tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await });
let (sender, receiver) = std::sync::mpsc::channel();
let message_queue: Arc<Mutex<Vec<WebAudioMessage>>> = Arc::new(Mutex::new(Vec::new()));
/* ...........................................................
Listen For incoming messages and add to queue
............................................................*/
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
loop {
if let Some(package) = async_output_receiver.recv().await {
let mut message_queue = message_queue_clone.lock().await;
let messages = package;
for message in messages {
(*message_queue).push(message);
}
}
}
});
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
/* ...........................................................
Process queued messages
............................................................*/
loop {
let mut message_queue = message_queue_clone.lock().await;
// Iterate over each message, play and remove messages when they are ready
message_queue.retain(|message| {
if message.instant.elapsed().as_millis() < message.offset as u128 {
return true;
};
match sender.send(message.clone()) {
Ok(_) => {}
Err(_) => {}
};
return false;
});
tokio::time::sleep(Duration::from_millis(1)).await;
}
});
/* ...........................................................
Create and setup WebAudio
............................................................*/
tauri::async_runtime::spawn(async move {
let latency_hint = match std::env::var("WEB_AUDIO_LATENCY").as_deref() {
Ok("playback") => AudioContextLatencyCategory::Playback,
_ => AudioContextLatencyCategory::default(),
};
let mut audio_context = AudioContext::new(AudioContextOptions {
latency_hint,
..AudioContextOptions::default()
});
let _ = audio_context.create_buffer_source();
let mut delays: HashMap<usize, Delay> = HashMap::new();
let mut reverbs: HashMap<usize, Reverb> = HashMap::new();
let compressor = audio_context.create_dynamics_compressor();
compressor.threshold().set_value(-50.0);
compressor.connect(&audio_context.destination());
let cache: Cache<String, AudioBuffer> = Cache::builder()
.max_capacity(31 * 1024 * 1024)
.time_to_idle(Duration::from_secs(40))
.build();
let ir_cache: Cache<String, AudioBuffer> = Cache::builder().max_capacity(31 * 1024 * 1024).build();
loop {
match receiver.recv() {
Ok(message) => {
match message.waveform.as_str() {
"sine" | "square" | "triangle" | "saw" | "sawtooth" => {
let t = audio_context.current_time();
let mut synth = Synth::new(&mut audio_context);
synth.set_frequency(&message.note);
synth.set_waveform(&message.waveform);
let filters = synth.set_filters(&mut audio_context, &message);
if message.delay.wet.is_some() {
apply_delay(&mut audio_context, &mut delays, &compressor, &message, t, &mut synth);
}
if message.reverb.room.is_some() && message.reverb.room.unwrap() > 0.0 {
apply_reverb(&audio_context, &compressor, &ir_cache, &message, &mut synth, &mut reverbs);
}
synth.envelope.connect(&compressor);
synth.play(t, &message, message.adsr.release.unwrap_or(0.001));
synth.set_adsr(t, &message.adsr, message.velocity, message.duration);
if is_filter_envelope_on(&message.lpenv) || is_filter_envelope_on(&message.hpenv) || is_filter_envelope_on(&message.bpenv) {
for f in &filters {
apply_filter_adsr(f, &message, &f.type_(), t);
}
}
}
_ => {
let t = audio_context.current_time();
// Create filepaths
let message_url = &message.sampleurl.clone();
let (url, file_path, file_path_clone) = create_filepath_generic(message_url, &message.dirname);
// Download file if it doesn't exist
tokio::spawn(async move {
if tokio::fs::metadata(&file_path.clone()).await.is_err() {
let response = reqwest::get(url)
.await
.unwrap_or_else(|_| panic!("Failed to send GET request"));
let bytes = response.bytes().await.unwrap();
let path = Path::new(&file_path);
let mut file = create_file_and_dirs(path).await;
file.write_all(&bytes)
.await
.unwrap_or_else(|_| panic!("Failed to write to file"));
}
});
// Play the buffer, if exists
if let Some(audio_buffer) = cache.get(&file_path_clone) {
let audio_buffer_duration = audio_buffer.duration();
let mut sampler = Sampler::new(&mut audio_context, audio_buffer);
// Playback speed
match message.unit {
Some(_) => {
sampler.sample.playback_rate().set_value(message.speed * audio_buffer_duration as f32 * 1.0);
}
_ => {
sampler.sample.playback_rate().set_value(message.speed * message.playbackrate);
}
}
// ADSR
sampler.set_adsr(t, &message.adsr, message.velocity, message.duration);
// Filters
let filters = sampler.set_filters(&mut audio_context, &message);
if is_filter_envelope_on(&message.lpenv) || is_filter_envelope_on(&message.hpenv) || is_filter_envelope_on(&message.bpenv) {
for f in &filters {
apply_filter_adsr(f, &message, &f.type_(), t);
}
}
// Delay
if message.delay.wet.is_some() {
apply_delay(&mut audio_context, &mut delays, &compressor, &message, t, &mut sampler);
};
// Reverb
if message.reverb.room.is_some() && message.reverb.room.unwrap() > 0.0 {
apply_reverb(&audio_context, &compressor, &ir_cache, &message, &mut sampler, &mut reverbs);
}
// Connect to output
sampler.envelope.connect(&compressor);
sampler.play(t, &message, message.adsr.release.unwrap_or(0.001));
// Decode file, if it exists
} else if let Ok(file) = File::open(&file_path_clone) {
let cache_clone = cache.clone();
tokio::spawn(async move {
let context = OfflineAudioContext::new(2, 88200, 44100.0);
let audio_buffer = context.decode_audio_data_sync(file).expect("Failed to decode file");
cache_clone.insert(file_path_clone.clone(), audio_buffer);
});
}
}
}
}
Err(_) => {}
}
}
});
}
pub async fn async_process_model(
mut input_receiver: mpsc::Receiver<Vec<WebAudioMessage>>,
output_transmitter: mpsc::Sender<Vec<WebAudioMessage>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
while let Some(input) = input_receiver.recv().await {
let output = input;
output_transmitter.send(output).await?;
}
Ok(())
}
#[derive(Deserialize)]
pub struct MessageFromJS {
note: f32,
offset: f64,
waveform: String,
lpf: (f32, f32),
hpf: (f32, f32),
bpf: (f32, f32),
duration: f64,
velocity: f32,
delay: (Option<f32>, Option<f32>, Option<f32>),
reverb: (Option<f32>, Option<f32>, Option<f32>, Option<f32>, Option<f32>, Option<String>, Option<String>),
orbit: usize,
speed: f32,
begin: f64,
end: f64,
looper: (u8, f64, f64),
adsr: (Option<f64>, Option<f64>, Option<f32>, Option<f64>),
lpenv: (Option<f64>, Option<f64>, Option<f64>, Option<f64>, Option<f64>),
hpenv: (Option<f64>, Option<f64>, Option<f64>, Option<f64>, Option<f64>),
bpenv: (Option<f64>, Option<f64>, Option<f64>, Option<f64>, Option<f64>),
n: usize,
sampleurl: String,
dirname: String,
unit: Option<String>,
playbackrate: f32,
}
// Called from JS
#[tauri::command]
pub async fn sendwebaudio(
messagesfromjs: Vec<MessageFromJS>,
state: tauri::State<'_, AsyncInputTransmitWebAudio>,
) -> Result<(), String> {
let async_proc_input_tx = state.inner.lock().await;
let mut messages_to_process: Vec<WebAudioMessage> = Vec::new();
for m in messagesfromjs {
let message_to_process = WebAudioMessage {
note: m.note,
instant: Instant::now(),
offset: m.offset,
waveform: m.waveform,
lpf: LPFMessage {
frequency: m.lpf.0,
resonance: m.lpf.1,
},
hpf: HPFMessage {
frequency: m.hpf.0,
resonance: m.hpf.1,
},
bpf: BPFMessage {
frequency: m.bpf.0,
resonance: m.bpf.1,
},
duration: m.duration,
velocity: m.velocity,
delay: DelayMessage {
wet: m.delay.0,
delay_time: m.delay.1,
feedback: m.delay.2,
},
reverb: ReverbMessage {
room: m.reverb.0,
size: m.reverb.1,
roomfade: m.reverb.2,
roomlp: m.reverb.3,
roomdim: m.reverb.4,
ir: m.reverb.5,
url: m.reverb.6,
},
orbit: m.orbit,
speed: m.speed,
begin: m.begin,
end: m.end,
looper: LoopMessage {
is_loop: m.looper.0,
loop_start: m.looper.1,
loop_end: m.looper.2,
},
adsr: ADSRMessage {
attack: m.adsr.0,
decay: m.adsr.1,
sustain: m.adsr.2,
release: m.adsr.3,
},
lpenv: FilterADSRMessage {
attack: m.lpenv.0,
decay: m.lpenv.1,
sustain: m.lpenv.2,
release: m.lpenv.3,
env: m.lpenv.4,
},
hpenv: FilterADSRMessage {
attack: m.hpenv.0,
decay: m.hpenv.1,
sustain: m.hpenv.2,
release: m.hpenv.3,
env: m.hpenv.4,
},
bpenv: FilterADSRMessage {
attack: m.bpenv.0,
decay: m.bpenv.1,
sustain: m.bpenv.2,
release: m.bpenv.3,
env: m.bpenv.4,
},
n: m.n,
sampleurl: m.sampleurl,
dirname: m.dirname,
unit: m.unit,
playbackrate: m.playbackrate,
};
messages_to_process.push(message_to_process);
}
async_proc_input_tx.send(messages_to_process).await.map_err(|e| e.to_string())
}
+8 -108
View File
@@ -2959,15 +2959,6 @@ 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 ]",
@@ -3663,107 +3654,16 @@ 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 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 ]",
"[ 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 ]",
]
`;
-1
View File
@@ -28,7 +28,6 @@
"@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:*",
+5 -21
View File
@@ -183,40 +183,24 @@ global effects use the same chain for all events of the same orbit:
<JsDoc client:idle name="orbit" h={0} />
## Delay
### delay
## delay
<JsDoc client:idle name="delay" h={0} />
### delaytime
## delaytime
<JsDoc client:idle name="delaytime" h={0} />
### delayfeedback
## delayfeedback
<JsDoc client:idle name="delayfeedback" h={0} />
## Reverb
### room
## room
<JsDoc client:idle name="room" h={0} />
### roomsize
## roomsize
<JsDoc client:idle name="roomsize" h={0} />
### roomfade
<JsDoc client:idle name="roomfade" h={0} />
### roomlp
<JsDoc client:idle name="roomlp" h={0} />
### roomdim
<JsDoc client:idle name="roomdim" h={0} />
Next, we'll look at strudel's support for [Csound](/learn/csound).
-19
View File
@@ -23,25 +23,6 @@ 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.
<MiniRepl client:idle tune={`sound("<white pink brown>/2").scope()`} />
Here's a more musical example of how to use noise for hihats:
<MiniRepl
client:idle
tune={`sound("bd*2,<white pink brown>*8")
.decay(.04).sustain(0).scope()`}
/>
Some amount of pink noise can also be added to any oscillator by using the `noise` paremeter:
<MiniRepl client:idle tune={`note("c3").noise("<0.1 0.25 0.5>").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:
+9 -21
View File
@@ -4,9 +4,10 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger, Cyclist } from '@strudel.cycles/core';
import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core';
import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react';
import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio';
import { webaudioDesktopOutput } from '@strudel/desktopbridge';
import { createClient } from '@supabase/supabase-js';
import { nanoid } from 'nanoid';
import React, { createContext, useCallback, useEffect, useState, useMemo } from 'react';
@@ -22,8 +23,6 @@ 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';
import { CyclistBridge } from '@strudel/desktopbridge/cyclistbridge.mjs';
const { latestCode } = settingsMap.get();
@@ -35,27 +34,25 @@ const supabase = createClient(
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
);
let modules = [
const modules = [
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel/codemirror'),
import('@strudel.cycles/serial'),
import('@strudel.cycles/soundfonts'),
import('@strudel.cycles/csound'),
];
if (isTauri()) {
modules = modules.concat([
modules.concat([
import('@strudel/desktopbridge/loggerbridge.mjs'),
import('@strudel/desktopbridge/midibridge.mjs'),
import('@strudel/desktopbridge/oscbridge.mjs'),
import('@strudel/desktopbridge/cyclistbridge.mjs'),
]);
} else {
modules = modules.concat([import('@strudel.cycles/midi'), import('@strudel.cycles/osc')]);
modules.concat([import('@strudel.cycles/midi'), import('@strudel.cycles/osc')]);
}
const modulesLoading = evalScope(
@@ -132,11 +129,11 @@ 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...',
defaultOutput: webaudioOutput,
defaultOutput: isTauri() ? webaudioDesktopOutput : webaudioOutput,
getTime,
beforeEval: async () => {
setPending(true);
@@ -146,7 +143,6 @@ export function Repl({ embedded = false }) {
},
afterEval: ({ code, meta }) => {
setMiniLocations(meta.miniLocations);
setWidgets(meta.widgets);
setPending(false);
setLatestCode(code);
window.location.hash = '#' + code2hash(code);
@@ -154,18 +150,10 @@ export function Repl({ embedded = false }) {
onEvalError: (err) => {
setPending(false);
},
onToggle: (play) => {
if (!play) {
cleanupDraw(false);
window.postMessage('strudel-stop');
} else {
window.postMessage('strudel-start');
}
},
onToggle: (play) => !play && cleanupDraw(false),
drawContext,
// drawTime: [0, 6],
paintOptions,
createCyclist: (p) => (isTauri() ? new CyclistBridge(p) : new Cyclist(p)),
});
// init code
@@ -225,7 +213,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],
);