mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-24 05:57:17 -04:00
Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1bae027aaa | |||
| 0b888ac54d | |||
| 75099abbc1 | |||
| 3c4f835d8b | |||
| ce842f7561 | |||
| 508af7eb72 | |||
| a4386a617a | |||
| 767ba713a6 | |||
| 020e85906d | |||
| d1ffdd57a5 | |||
| 9c9323e040 | |||
| 376cf09565 | |||
| 047129223e | |||
| 4b64168faa | |||
| 2bc6d08410 | |||
| 484bb6b11f | |||
| a0884e2a03 | |||
| 1e352fdf80 | |||
| 83dfbb84c2 | |||
| 9043064543 | |||
| a3649148c1 | |||
| 3a69a2561c | |||
| 30a0059ef3 | |||
| b1fcf6ca34 | |||
| f2534310e8 | |||
| 25be8fca0d | |||
| 28966739f6 | |||
| f089037f18 | |||
| e818ca9f03 | |||
| 0efcf84c46 | |||
| 5db39b8e9e | |||
| 2127a8f642 | |||
| dcd75e0384 | |||
| dfdd9e02ca | |||
| 275796c241 | |||
| 6ca99e33ab | |||
| 1909caf769 | |||
| 2d07eeb518 | |||
| 1c0da7cd49 | |||
| fd316c81c0 | |||
| edbd437d7b | |||
| 9be1f9c18b | |||
| 96f06d8026 | |||
| 935d8e8aea | |||
| 21b99b3810 | |||
| c051a1249d | |||
| 564697e175 | |||
| d4bf358eae | |||
| f84d5ba3a0 | |||
| b550ff038c | |||
| c6ecd31ea1 | |||
| e600b91a85 | |||
| abff279707 | |||
| 1d85d60b11 | |||
| d5c6cb85ea | |||
| d7bc309eeb | |||
| 33c40e5ef8 | |||
| 062d926900 | |||
| 0ad0046a76 | |||
| b36cee93f7 | |||
| 2731e70fb7 | |||
| 276cf858fc | |||
| b2acff40c4 | |||
| e3333e716f | |||
| bb7b8c2fab | |||
| 389c7be264 | |||
| c2481e460b | |||
| fe8c4e2bcd | |||
| c93e4a951a | |||
| aa094bf930 | |||
| 6eda7af14b | |||
| 8a741952df | |||
| 0dcc55ee16 | |||
| 6c3a3b9e29 | |||
| e4eff8c837 | |||
| 527b895e3c | |||
| 4eb0a7b7c0 | |||
| f11462bf41 | |||
| dea1c31701 | |||
| 6719c6127b | |||
| 68ab43b3ab | |||
| cf72e3bba5 | |||
| 172006a3b3 | |||
| 209122b8f7 | |||
| 62743edf45 | |||
| eec3752b5a | |||
| 006fd6c733 | |||
| 7078e20200 | |||
| 52c01abbe9 | |||
| 6e26f39751 | |||
| d7fe491c84 | |||
| db36636ab2 | |||
| 789cad3e97 | |||
| 30cba7b935 | |||
| 30d96dcb65 | |||
| 3a69fd50bb | |||
| 0f72729f0d | |||
| 0a8874180c | |||
| a6501474bb | |||
| 4cb93ddcde |
+2
-1
@@ -18,4 +18,5 @@ vite.config.js
|
||||
**/*.json
|
||||
**/dev-dist
|
||||
**/dist
|
||||
/src-tauri/target/**/*
|
||||
/src-tauri/target/**/*
|
||||
reverbGen.mjs
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './codemirror.mjs';
|
||||
export * from './highlight.mjs';
|
||||
export * from './flash.mjs';
|
||||
export * from './slider.mjs';
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
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)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
+16
-14
@@ -655,6 +655,15 @@ const generic_params = [
|
||||
* .vib("<.5 1 2 4 8 16>:12")
|
||||
*/
|
||||
[['vib', 'vibmod'], 'vibrato', 'v'],
|
||||
/**
|
||||
* Adds pink noise to the mix
|
||||
*
|
||||
* @name noise
|
||||
* @param {number | Pattern} wet wet amount
|
||||
* @example
|
||||
* sound("<white pink brown>/2")
|
||||
*/
|
||||
['noise'],
|
||||
/**
|
||||
* Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set
|
||||
*
|
||||
@@ -1011,30 +1020,23 @@ 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 size, sz
|
||||
* @synonyms rsize, sz, size
|
||||
* @example
|
||||
* s("bd sd").room(.8).roomsize("<0 1 2 4 8>")
|
||||
* s("bd sd").room(.8).rsize(1)
|
||||
* @example
|
||||
* s("bd sd").room(.8).rsize(4)
|
||||
*
|
||||
*/
|
||||
// TODO: find out why :
|
||||
// s("bd sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc()
|
||||
// .. does not work. Is it because room is only one effect?
|
||||
['size', 'sz', 'roomsize'],
|
||||
['roomsize', 'size', 'sz', 'rsize'],
|
||||
// ['sagogo'],
|
||||
// ['sclap'],
|
||||
// ['sclaves'],
|
||||
@@ -1204,7 +1206,7 @@ const generic_params = [
|
||||
['pitchJump'],
|
||||
['pitchJumpTime'],
|
||||
['lfo', 'repeatTime'],
|
||||
['noise'],
|
||||
['znoise'], // noise on the frequency or as bubo calls it "frequency fog" :)
|
||||
['zmod'],
|
||||
['zcrush'], // like crush but scaled differently
|
||||
['zdelay'],
|
||||
|
||||
@@ -2343,3 +2343,9 @@ 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();
|
||||
|
||||
@@ -16,14 +16,17 @@ export function repl({
|
||||
transpiler,
|
||||
onToggle,
|
||||
editPattern,
|
||||
createCyclist,
|
||||
}) {
|
||||
const scheduler = new Cyclist({
|
||||
const cyclistParams = {
|
||||
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);
|
||||
|
||||
@@ -43,7 +43,8 @@ function createClock(
|
||||
clear();
|
||||
};
|
||||
const getPhase = () => phase;
|
||||
const setPhase = (_phase) => (phase = _phase);
|
||||
// setCallback
|
||||
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
|
||||
return { setDuration, start, stop, pause, duration, interval, getPhase, setPhase, minLatency };
|
||||
}
|
||||
export default createClock;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -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 './webaudiobridge.mjs';
|
||||
export * from './cyclistbridge.mjs';
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strudel.cycles/webaudio": "^0.9.0",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@tauri-apps/api": "^1.4.0"
|
||||
},
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
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);
|
||||
};
|
||||
+71
-18
@@ -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 } from '@strudel.cycles/core';
|
||||
import { Pattern, isPattern, logger, ref } 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(outputs) {
|
||||
return outputs.map((o) => `'${o.name}'`).join(' | ');
|
||||
function getMidiDeviceNamesString(devices) {
|
||||
return devices.map((o) => `'${o.name}'`).join(' | ');
|
||||
}
|
||||
|
||||
export function enableWebMidi(options = {}) {
|
||||
@@ -52,30 +52,41 @@ export function enableWebMidi(options = {}) {
|
||||
});
|
||||
});
|
||||
}
|
||||
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
|
||||
const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||
|
||||
// output?: string | number, outputs: typeof WebMidi.outputs
|
||||
function getDevice(output, outputs) {
|
||||
if (!outputs.length) {
|
||||
function getDevice(indexOrName, devices) {
|
||||
if (!devices.length) {
|
||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||
}
|
||||
if (typeof output === 'number') {
|
||||
return outputs[output];
|
||||
if (typeof indexOrName === 'number') {
|
||||
return devices[indexOrName];
|
||||
}
|
||||
if (typeof output === 'string') {
|
||||
return outputByName(output);
|
||||
const byName = (name) => devices.find((output) => output.name.includes(name));
|
||||
if (typeof indexOrName === 'string') {
|
||||
return byName(indexOrName);
|
||||
}
|
||||
// attempt to default to first IAC device if none is specified
|
||||
const IACOutput = outputs.find((output) => output.name.includes('IAC'));
|
||||
const device = IACOutput ?? outputs[0];
|
||||
const IACOutput = byName('IAC');
|
||||
const device = IACOutput ?? devices[0];
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${getMidiDeviceNamesString(WebMidi.outputs)}`,
|
||||
`🔌 MIDI device '${device ? device : ''}' not found. Use one of ${getMidiDeviceNamesString(devices)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return IACOutput ?? outputs[0];
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
Pattern.prototype.midi = function (output) {
|
||||
@@ -103,6 +114,7 @@ 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);
|
||||
@@ -113,7 +125,7 @@ Pattern.prototype.midi = function (output) {
|
||||
const timeOffsetString = `+${offset}`;
|
||||
|
||||
// destructure value
|
||||
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
|
||||
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd } = 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
|
||||
@@ -125,7 +137,7 @@ Pattern.prototype.midi = function (output) {
|
||||
time: timeOffsetString,
|
||||
});
|
||||
}
|
||||
if (ccv && ccn) {
|
||||
if (ccv !== undefined && ccn !== undefined) {
|
||||
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
||||
throw new Error('expected ccv to be a number between 0 and 1');
|
||||
}
|
||||
@@ -135,5 +147,46 @@ 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;
|
||||
}
|
||||
|
||||
+246
-191
@@ -200,20 +200,21 @@ function peg$parse(input, options) {
|
||||
var peg$c23 = "*";
|
||||
var peg$c24 = "?";
|
||||
var peg$c25 = ":";
|
||||
var peg$c26 = "struct";
|
||||
var peg$c27 = "target";
|
||||
var peg$c28 = "euclid";
|
||||
var peg$c29 = "slow";
|
||||
var peg$c30 = "rotL";
|
||||
var peg$c31 = "rotR";
|
||||
var peg$c32 = "fast";
|
||||
var peg$c33 = "scale";
|
||||
var peg$c34 = "//";
|
||||
var peg$c35 = "cat";
|
||||
var peg$c36 = "$";
|
||||
var peg$c37 = "setcps";
|
||||
var peg$c38 = "setbpm";
|
||||
var peg$c39 = "hush";
|
||||
var peg$c26 = "..";
|
||||
var peg$c27 = "struct";
|
||||
var peg$c28 = "target";
|
||||
var peg$c29 = "euclid";
|
||||
var peg$c30 = "slow";
|
||||
var peg$c31 = "rotL";
|
||||
var peg$c32 = "rotR";
|
||||
var peg$c33 = "fast";
|
||||
var peg$c34 = "scale";
|
||||
var peg$c35 = "//";
|
||||
var peg$c36 = "cat";
|
||||
var peg$c37 = "$";
|
||||
var peg$c38 = "setcps";
|
||||
var peg$c39 = "setbpm";
|
||||
var peg$c40 = "hush";
|
||||
|
||||
var peg$r0 = /^[1-9]/;
|
||||
var peg$r1 = /^[eE]/;
|
||||
@@ -255,64 +256,67 @@ function peg$parse(input, options) {
|
||||
var peg$e30 = peg$literalExpectation("*", false);
|
||||
var peg$e31 = peg$literalExpectation("?", false);
|
||||
var peg$e32 = peg$literalExpectation(":", false);
|
||||
var peg$e33 = peg$literalExpectation("struct", false);
|
||||
var peg$e34 = peg$literalExpectation("target", false);
|
||||
var peg$e35 = peg$literalExpectation("euclid", false);
|
||||
var peg$e36 = peg$literalExpectation("slow", false);
|
||||
var peg$e37 = peg$literalExpectation("rotL", false);
|
||||
var peg$e38 = peg$literalExpectation("rotR", false);
|
||||
var peg$e39 = peg$literalExpectation("fast", false);
|
||||
var peg$e40 = peg$literalExpectation("scale", false);
|
||||
var peg$e41 = peg$literalExpectation("//", false);
|
||||
var peg$e42 = peg$classExpectation(["\n"], true, false);
|
||||
var peg$e43 = peg$literalExpectation("cat", false);
|
||||
var peg$e44 = peg$literalExpectation("$", false);
|
||||
var peg$e45 = peg$literalExpectation("setcps", false);
|
||||
var peg$e46 = peg$literalExpectation("setbpm", false);
|
||||
var peg$e47 = peg$literalExpectation("hush", false);
|
||||
var peg$e33 = peg$literalExpectation("..", false);
|
||||
var peg$e34 = peg$literalExpectation("struct", false);
|
||||
var peg$e35 = peg$literalExpectation("target", false);
|
||||
var peg$e36 = peg$literalExpectation("euclid", false);
|
||||
var peg$e37 = peg$literalExpectation("slow", false);
|
||||
var peg$e38 = peg$literalExpectation("rotL", false);
|
||||
var peg$e39 = peg$literalExpectation("rotR", false);
|
||||
var peg$e40 = peg$literalExpectation("fast", false);
|
||||
var peg$e41 = peg$literalExpectation("scale", false);
|
||||
var peg$e42 = peg$literalExpectation("//", false);
|
||||
var peg$e43 = peg$classExpectation(["\n"], true, false);
|
||||
var peg$e44 = peg$literalExpectation("cat", false);
|
||||
var peg$e45 = peg$literalExpectation("$", false);
|
||||
var peg$e46 = peg$literalExpectation("setcps", false);
|
||||
var peg$e47 = peg$literalExpectation("setbpm", false);
|
||||
var peg$e48 = peg$literalExpectation("hush", false);
|
||||
|
||||
var peg$f0 = function() { return parseFloat(text()); };
|
||||
var peg$f1 = function(chars) { return new AtomStub(chars.join("")) };
|
||||
var peg$f2 = function(s) { return s };
|
||||
var peg$f3 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; };
|
||||
var peg$f4 = function(a) { return a };
|
||||
var peg$f5 = function(s) { s.arguments_.alignment = 'slowcat'; return s; };
|
||||
var peg$f6 = function(a) { return x => x.options_['weight'] = a };
|
||||
var peg$f7 = function(a) { return x => x.options_['reps'] = a };
|
||||
var peg$f8 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
|
||||
var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
|
||||
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
|
||||
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
|
||||
var peg$f12 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
|
||||
var peg$f13 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||
var peg$f1 = function() { return parseInt(text()); };
|
||||
var peg$f2 = function(chars) { return new AtomStub(chars.join("")) };
|
||||
var peg$f3 = function(s) { return s };
|
||||
var peg$f4 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; };
|
||||
var peg$f5 = function(a) { return a };
|
||||
var peg$f6 = function(s) { s.arguments_.alignment = 'slowcat'; return s; };
|
||||
var peg$f7 = function(a) { return x => x.options_['weight'] = a };
|
||||
var peg$f8 = function(a) { return x => x.options_['reps'] = a };
|
||||
var peg$f9 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
|
||||
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
|
||||
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
|
||||
var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
|
||||
var peg$f13 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
|
||||
var peg$f14 = function(s) { return x => x.options_['ops'].push({ type_: "range", arguments_ :{ element:s } }) };
|
||||
var peg$f15 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||
for (const op of ops) {
|
||||
op(result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
var peg$f14 = function(s) { return new PatternStub(s, 'fastcat'); };
|
||||
var peg$f15 = function(tail) { return { alignment: 'stack', list: tail }; };
|
||||
var peg$f16 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
|
||||
var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
|
||||
var peg$f18 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
||||
var peg$f19 = function(sc) { return sc; };
|
||||
var peg$f20 = function(s) { return { name: "struct", args: { mini:s }}};
|
||||
var peg$f21 = function(s) { return { name: "target", args : { name:s}}};
|
||||
var peg$f22 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
|
||||
var peg$f23 = function(a) { return { name: "stretch", args :{ amount: a}}};
|
||||
var peg$f24 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
|
||||
var peg$f25 = function(a) { return { name: "shift", args :{ amount: a}}};
|
||||
var peg$f26 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
|
||||
var peg$f27 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
|
||||
var peg$f28 = function(s, v) { return v};
|
||||
var peg$f29 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
|
||||
var peg$f30 = function(sg) {return sg};
|
||||
var peg$f31 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
|
||||
var peg$f32 = function(sc) { return sc };
|
||||
var peg$f33 = function(c) { return c };
|
||||
var peg$f34 = function(v) { return new CommandStub("setcps", { value: v})};
|
||||
var peg$f35 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
|
||||
var peg$f36 = function() { return new CommandStub("hush")};
|
||||
var peg$f16 = function(s) { return new PatternStub(s, 'fastcat'); };
|
||||
var peg$f17 = function(tail) { return { alignment: 'stack', list: tail }; };
|
||||
var peg$f18 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
|
||||
var peg$f19 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
|
||||
var peg$f20 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
||||
var peg$f21 = function(sc) { return sc; };
|
||||
var peg$f22 = function(s) { return { name: "struct", args: { mini:s }}};
|
||||
var peg$f23 = function(s) { return { name: "target", args : { name:s}}};
|
||||
var peg$f24 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
|
||||
var peg$f25 = function(a) { return { name: "stretch", args :{ amount: a}}};
|
||||
var peg$f26 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
|
||||
var peg$f27 = function(a) { return { name: "shift", args :{ amount: a}}};
|
||||
var peg$f28 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
|
||||
var peg$f29 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
|
||||
var peg$f30 = function(s, v) { return v};
|
||||
var peg$f31 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
|
||||
var peg$f32 = function(sg) {return sg};
|
||||
var peg$f33 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
|
||||
var peg$f34 = function(sc) { return sc };
|
||||
var peg$f35 = function(c) { return c };
|
||||
var peg$f36 = function(v) { return new CommandStub("setcps", { value: v})};
|
||||
var peg$f37 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
|
||||
var peg$f38 = function() { return new CommandStub("hush")};
|
||||
var peg$currPos = 0;
|
||||
var peg$savedPos = 0;
|
||||
var peg$posDetailsCache = [{ line: 1, column: 1 }];
|
||||
@@ -651,6 +655,26 @@ function peg$parse(input, options) {
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parseintneg() {
|
||||
var s0, s1, s2;
|
||||
|
||||
s0 = peg$currPos;
|
||||
s1 = peg$parseminus();
|
||||
if (s1 === peg$FAILED) {
|
||||
s1 = null;
|
||||
}
|
||||
s2 = peg$parseint();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f1();
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parseminus() {
|
||||
var s0;
|
||||
|
||||
@@ -884,7 +908,7 @@ function peg$parse(input, options) {
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f1(s2);
|
||||
s0 = peg$f2(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -920,7 +944,7 @@ function peg$parse(input, options) {
|
||||
if (s6 !== peg$FAILED) {
|
||||
s7 = peg$parsews();
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f2(s4);
|
||||
s0 = peg$f3(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -968,7 +992,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
s8 = peg$parsews();
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f3(s4, s7);
|
||||
s0 = peg$f4(s4, s7);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1000,7 +1024,7 @@ function peg$parse(input, options) {
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f4(s2);
|
||||
s0 = peg$f5(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1040,7 +1064,7 @@ function peg$parse(input, options) {
|
||||
if (s6 !== peg$FAILED) {
|
||||
s7 = peg$parsews();
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f5(s4);
|
||||
s0 = peg$f6(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1090,6 +1114,9 @@ function peg$parse(input, options) {
|
||||
s0 = peg$parseop_degrade();
|
||||
if (s0 === peg$FAILED) {
|
||||
s0 = peg$parseop_tail();
|
||||
if (s0 === peg$FAILED) {
|
||||
s0 = peg$parseop_range();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1115,7 +1142,7 @@ function peg$parse(input, options) {
|
||||
s2 = peg$parsenumber();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f6(s2);
|
||||
s0 = peg$f7(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1143,7 +1170,7 @@ function peg$parse(input, options) {
|
||||
s2 = peg$parsenumber();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f7(s2);
|
||||
s0 = peg$f8(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1197,7 +1224,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s13 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f8(s3, s7, s11);
|
||||
s0 = peg$f9(s3, s7, s11);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1237,7 +1264,7 @@ function peg$parse(input, options) {
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f9(s2);
|
||||
s0 = peg$f10(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1265,7 +1292,7 @@ function peg$parse(input, options) {
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f10(s2);
|
||||
s0 = peg$f11(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1295,7 +1322,7 @@ function peg$parse(input, options) {
|
||||
s2 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f11(s2);
|
||||
s0 = peg$f12(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1319,7 +1346,35 @@ function peg$parse(input, options) {
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f12(s2);
|
||||
s0 = peg$f13(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parseop_range() {
|
||||
var s0, s1, s2;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 2) === peg$c26) {
|
||||
s1 = peg$c26;
|
||||
peg$currPos += 2;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e33); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f14(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1345,7 +1400,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parseslice_op();
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f13(s1, s2);
|
||||
s0 = peg$f15(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1370,7 +1425,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f14(s1);
|
||||
s1 = peg$f16(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1419,7 +1474,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f15(s1);
|
||||
s1 = peg$f17(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1468,7 +1523,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f16(s1);
|
||||
s1 = peg$f18(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1489,7 +1544,7 @@ function peg$parse(input, options) {
|
||||
s2 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f17(s1, s2);
|
||||
s0 = peg$f19(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1509,7 +1564,7 @@ function peg$parse(input, options) {
|
||||
s2 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f18(s1, s2);
|
||||
s0 = peg$f20(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1532,7 +1587,7 @@ function peg$parse(input, options) {
|
||||
s6 = peg$parsequote();
|
||||
if (s6 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f19(s4);
|
||||
s0 = peg$f21(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1582,19 +1637,19 @@ function peg$parse(input, options) {
|
||||
var s0, s1, s2, s3;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 6) === peg$c26) {
|
||||
s1 = peg$c26;
|
||||
if (input.substr(peg$currPos, 6) === peg$c27) {
|
||||
s1 = peg$c27;
|
||||
peg$currPos += 6;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e33); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e34); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsemini_or_operator();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f20(s3);
|
||||
s0 = peg$f22(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1611,12 +1666,12 @@ function peg$parse(input, options) {
|
||||
var s0, s1, s2, s3, s4, s5;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 6) === peg$c27) {
|
||||
s1 = peg$c27;
|
||||
if (input.substr(peg$currPos, 6) === peg$c28) {
|
||||
s1 = peg$c28;
|
||||
peg$currPos += 6;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e34); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e35); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
@@ -1627,7 +1682,7 @@ function peg$parse(input, options) {
|
||||
s5 = peg$parsequote();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f21(s4);
|
||||
s0 = peg$f23(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1652,12 +1707,12 @@ function peg$parse(input, options) {
|
||||
var s0, s1, s2, s3, s4, s5, s6, s7;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 6) === peg$c28) {
|
||||
s1 = peg$c28;
|
||||
if (input.substr(peg$currPos, 6) === peg$c29) {
|
||||
s1 = peg$c29;
|
||||
peg$currPos += 6;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e35); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e36); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
@@ -1672,7 +1727,7 @@ function peg$parse(input, options) {
|
||||
s7 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f22(s3, s5, s7);
|
||||
s0 = peg$f24(s3, s5, s7);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1692,35 +1747,6 @@ function peg$parse(input, options) {
|
||||
function peg$parseslow() {
|
||||
var s0, s1, s2, s3;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 4) === peg$c29) {
|
||||
s1 = peg$c29;
|
||||
peg$currPos += 4;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e36); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f23(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parserotL() {
|
||||
var s0, s1, s2, s3;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 4) === peg$c30) {
|
||||
s1 = peg$c30;
|
||||
@@ -1729,35 +1755,6 @@ function peg$parse(input, options) {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e37); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f24(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parserotR() {
|
||||
var s0, s1, s2, s3;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 4) === peg$c31) {
|
||||
s1 = peg$c31;
|
||||
peg$currPos += 4;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e38); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsenumber();
|
||||
@@ -1776,16 +1773,16 @@ function peg$parse(input, options) {
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parsefast() {
|
||||
function peg$parserotL() {
|
||||
var s0, s1, s2, s3;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 4) === peg$c32) {
|
||||
s1 = peg$c32;
|
||||
if (input.substr(peg$currPos, 4) === peg$c31) {
|
||||
s1 = peg$c31;
|
||||
peg$currPos += 4;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e39); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e38); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
@@ -1805,16 +1802,74 @@ function peg$parse(input, options) {
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parserotR() {
|
||||
var s0, s1, s2, s3;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 4) === peg$c32) {
|
||||
s1 = peg$c32;
|
||||
peg$currPos += 4;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e39); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f27(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parsefast() {
|
||||
var s0, s1, s2, s3;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 4) === peg$c33) {
|
||||
s1 = peg$c33;
|
||||
peg$currPos += 4;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e40); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f28(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parsescale() {
|
||||
var s0, s1, s2, s3, s4, s5;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 5) === peg$c33) {
|
||||
s1 = peg$c33;
|
||||
if (input.substr(peg$currPos, 5) === peg$c34) {
|
||||
s1 = peg$c34;
|
||||
peg$currPos += 5;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e40); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e41); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
@@ -1834,7 +1889,7 @@ function peg$parse(input, options) {
|
||||
s5 = peg$parsequote();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f27(s4);
|
||||
s0 = peg$f29(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1859,12 +1914,12 @@ function peg$parse(input, options) {
|
||||
var s0, s1, s2, s3;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 2) === peg$c34) {
|
||||
s1 = peg$c34;
|
||||
if (input.substr(peg$currPos, 2) === peg$c35) {
|
||||
s1 = peg$c35;
|
||||
peg$currPos += 2;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e41); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e42); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = [];
|
||||
@@ -1873,7 +1928,7 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s3 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e42); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e43); }
|
||||
}
|
||||
while (s3 !== peg$FAILED) {
|
||||
s2.push(s3);
|
||||
@@ -1882,7 +1937,7 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s3 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e42); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e43); }
|
||||
}
|
||||
}
|
||||
s1 = [s1, s2];
|
||||
@@ -1899,12 +1954,12 @@ function peg$parse(input, options) {
|
||||
var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 3) === peg$c35) {
|
||||
s1 = peg$c35;
|
||||
if (input.substr(peg$currPos, 3) === peg$c36) {
|
||||
s1 = peg$c36;
|
||||
peg$currPos += 3;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e43); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e44); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
@@ -1926,7 +1981,7 @@ function peg$parse(input, options) {
|
||||
s9 = peg$parsemini_or_operator();
|
||||
if (s9 !== peg$FAILED) {
|
||||
peg$savedPos = s7;
|
||||
s7 = peg$f28(s5, s9);
|
||||
s7 = peg$f30(s5, s9);
|
||||
} else {
|
||||
peg$currPos = s7;
|
||||
s7 = peg$FAILED;
|
||||
@@ -1943,7 +1998,7 @@ function peg$parse(input, options) {
|
||||
s9 = peg$parsemini_or_operator();
|
||||
if (s9 !== peg$FAILED) {
|
||||
peg$savedPos = s7;
|
||||
s7 = peg$f28(s5, s9);
|
||||
s7 = peg$f30(s5, s9);
|
||||
} else {
|
||||
peg$currPos = s7;
|
||||
s7 = peg$FAILED;
|
||||
@@ -1963,7 +2018,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s8 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f29(s5, s6);
|
||||
s0 = peg$f31(s5, s6);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2009,7 +2064,7 @@ function peg$parse(input, options) {
|
||||
s4 = peg$parsecomment();
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f30(s1);
|
||||
s0 = peg$f32(s1);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2020,18 +2075,18 @@ function peg$parse(input, options) {
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 36) {
|
||||
s3 = peg$c36;
|
||||
s3 = peg$c37;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s3 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e44); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e45); }
|
||||
}
|
||||
if (s3 !== peg$FAILED) {
|
||||
s4 = peg$parsews();
|
||||
s5 = peg$parsemini_or_operator();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f31(s1, s5);
|
||||
s0 = peg$f33(s1, s5);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2056,7 +2111,7 @@ function peg$parse(input, options) {
|
||||
s1 = peg$parsemini_or_operator();
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f32(s1);
|
||||
s1 = peg$f34(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
if (s0 === peg$FAILED) {
|
||||
@@ -2089,7 +2144,7 @@ function peg$parse(input, options) {
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f33(s2);
|
||||
s0 = peg$f35(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2102,19 +2157,19 @@ function peg$parse(input, options) {
|
||||
var s0, s1, s2, s3;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 6) === peg$c37) {
|
||||
s1 = peg$c37;
|
||||
if (input.substr(peg$currPos, 6) === peg$c38) {
|
||||
s1 = peg$c38;
|
||||
peg$currPos += 6;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e45); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e46); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f34(s3);
|
||||
s0 = peg$f36(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2131,19 +2186,19 @@ function peg$parse(input, options) {
|
||||
var s0, s1, s2, s3;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 6) === peg$c38) {
|
||||
s1 = peg$c38;
|
||||
if (input.substr(peg$currPos, 6) === peg$c39) {
|
||||
s1 = peg$c39;
|
||||
peg$currPos += 6;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e46); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e47); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f35(s3);
|
||||
s0 = peg$f37(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2160,16 +2215,16 @@ function peg$parse(input, options) {
|
||||
var s0, s1;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.substr(peg$currPos, 4) === peg$c39) {
|
||||
s1 = peg$c39;
|
||||
if (input.substr(peg$currPos, 4) === peg$c40) {
|
||||
s1 = peg$c40;
|
||||
peg$currPos += 4;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e47); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e48); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f36();
|
||||
s1 = peg$f38();
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
|
||||
@@ -79,6 +79,9 @@ frac
|
||||
int
|
||||
= zero / (digit1_9 DIGIT*)
|
||||
|
||||
intneg
|
||||
= minus? int { return parseInt(text()); }
|
||||
|
||||
minus
|
||||
= "-"
|
||||
|
||||
@@ -123,7 +126,7 @@ slice = step / sub_cycle / polymeter / slow_sequence
|
||||
|
||||
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
||||
// at this point, we assume we can represent them as regular sequence operators
|
||||
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail
|
||||
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail / op_range
|
||||
|
||||
op_weight = "@" a:number
|
||||
{ return x => x.options_['weight'] = a }
|
||||
@@ -146,6 +149,9 @@ op_degrade = "?"a:number?
|
||||
op_tail = ":" s:slice
|
||||
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
|
||||
|
||||
op_range = ".." s:slice
|
||||
{ return x => x.options_['ops'].push({ type_: "range", arguments_ :{ element:s } }) }
|
||||
|
||||
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
||||
slice_with_ops = s:slice ops:slice_op*
|
||||
{ const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||
|
||||
@@ -45,6 +45,17 @@ const applyOptions = (parent, enter) => (pat, i) => {
|
||||
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
|
||||
break;
|
||||
}
|
||||
case 'range': {
|
||||
const friend = enter(op.arguments_.element);
|
||||
pat = strudel.reify(pat);
|
||||
const arrayRange = (start, stop, step = 1) =>
|
||||
Array.from({ length: Math.abs(stop - start) / step + 1 }, (value, index) =>
|
||||
start < stop ? start + index * step : start - index * step,
|
||||
);
|
||||
let range = (apat, bpat) => apat.squeezeBind((a) => bpat.bind((b) => strudel.fastcat(...arrayRange(a, b))));
|
||||
pat = range(pat, friend);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.warn(`operator "${op.type_}" not implemented`);
|
||||
}
|
||||
|
||||
@@ -184,6 +184,12 @@ describe('mini', () => {
|
||||
it('supports lists', () => {
|
||||
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
|
||||
});
|
||||
it('supports ranges', () => {
|
||||
expect(minV('0 .. 4')).toEqual([0, 1, 2, 3, 4]);
|
||||
});
|
||||
it('supports patterned ranges', () => {
|
||||
expect(minS('[<0 1> .. <2 4>]*2')).toEqual(minS('[0 1 2] [1 2 3 4]'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeafLocation', () => {
|
||||
|
||||
@@ -15,10 +15,11 @@ import {
|
||||
updateMiniLocations,
|
||||
} from '@strudel/codemirror';
|
||||
import './style.css';
|
||||
import { sliderPlugin } from '@strudel/codemirror/slider.mjs';
|
||||
|
||||
export { flash, highlightMiniLocations, updateMiniLocations };
|
||||
|
||||
const staticExtensions = [javascript(), flashField, highlightExtension];
|
||||
const staticExtensions = [javascript(), flashField, highlightExtension, sliderPlugin];
|
||||
|
||||
export default function CodeMirror({
|
||||
value,
|
||||
|
||||
@@ -19,6 +19,7 @@ function useStrudel({
|
||||
drawContext,
|
||||
drawTime = [-2, 2],
|
||||
paintOptions = {},
|
||||
createCyclist, // (params) => Cyclist
|
||||
}) {
|
||||
const id = useMemo(() => s4(), []);
|
||||
canvasId = canvasId || `canvas-${id}`;
|
||||
@@ -45,6 +46,7 @@ function useStrudel({
|
||||
onEvalError?.(err);
|
||||
},
|
||||
getTime,
|
||||
createCyclist,
|
||||
drawContext,
|
||||
transpiler,
|
||||
editPattern,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { updateWidgets } from '@strudel/codemirror';
|
||||
|
||||
// i know this is ugly.. in the future, repl needs to run without react
|
||||
export function useWidgets(view) {
|
||||
const [widgets, setWidgets] = useState([]);
|
||||
useEffect(() => {
|
||||
if (view) {
|
||||
updateWidgets(view, widgets);
|
||||
}
|
||||
}, [view, widgets]);
|
||||
return { widgets, setWidgets };
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
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 });
|
||||
}
|
||||
@@ -112,3 +112,25 @@ export function createFilter(
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
// stays 1 until .5, then fades out
|
||||
let wetfade = (d) => (d < 0.5 ? 1 : 1 - (d - 0.5) / 0.5);
|
||||
|
||||
// mix together dry and wet nodes. 0 = only dry 1 = only wet
|
||||
// still not too sure about how this could be used more generally...
|
||||
export function drywet(dry, wet, wetAmount = 0) {
|
||||
const ac = getAudioContext();
|
||||
if (!wetAmount) {
|
||||
return dry;
|
||||
}
|
||||
let dry_gain = ac.createGain();
|
||||
let wet_gain = ac.createGain();
|
||||
dry.connect(dry_gain);
|
||||
wet.connect(wet_gain);
|
||||
dry_gain.gain.value = wetfade(wetAmount);
|
||||
wet_gain.gain.value = wetfade(1 - wetAmount);
|
||||
let mix = ac.createGain();
|
||||
dry_gain.connect(mix);
|
||||
wet_gain.connect(mix);
|
||||
return mix;
|
||||
}
|
||||
|
||||
@@ -10,3 +10,4 @@ export * from './helpers.mjs';
|
||||
export * from './synth.mjs';
|
||||
export * from './zzfx.mjs';
|
||||
export * from './logger.mjs';
|
||||
export * from './dspworklet.mjs';
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
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,6 +1,6 @@
|
||||
{
|
||||
"name": "superdough",
|
||||
"version": "0.9.8",
|
||||
"version": "0.9.9",
|
||||
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
if (typeof AudioContext !== 'undefined') {
|
||||
AudioContext.prototype.impulseResponse = function (duration, channels = 1) {
|
||||
const length = this.sampleRate * duration;
|
||||
const impulse = this.createBuffer(channels, length, this.sampleRate);
|
||||
const IR = impulse.getChannelData(0);
|
||||
for (let i = 0; i < length; i++) IR[i] = (2 * Math.random() - 1) * Math.pow(1 - i / length, duration);
|
||||
return impulse;
|
||||
};
|
||||
import reverbGen from './reverbGen.mjs';
|
||||
|
||||
AudioContext.prototype.createReverb = function (duration) {
|
||||
if (typeof AudioContext !== 'undefined') {
|
||||
AudioContext.prototype.generateReverb = reverbGen.generateReverb;
|
||||
AudioContext.prototype.createReverb = function (duration, fade, lp, dim) {
|
||||
const convolver = this.createConvolver();
|
||||
convolver.setDuration = (d) => {
|
||||
convolver.buffer = this.impulseResponse(d);
|
||||
convolver.duration = duration;
|
||||
return convolver;
|
||||
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(duration);
|
||||
convolver.generate(duration, fade, lp, dim);
|
||||
return convolver;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: make the reverb more exciting
|
||||
// check out https://blog.gskinner.com/archives/2019/02/reverb-web-audio-api.html
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// 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;
|
||||
@@ -107,17 +107,25 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
|
||||
}
|
||||
|
||||
let reverbs = {};
|
||||
function getReverb(orbit, duration = 2) {
|
||||
|
||||
function getReverb(orbit, duration = 2, fade, lp, dim) {
|
||||
// If no reverb has been created for a given orbit, create one
|
||||
if (!reverbs[orbit]) {
|
||||
const ac = getAudioContext();
|
||||
const reverb = ac.createReverb(duration);
|
||||
const reverb = ac.createReverb(duration, fade, lp, dim);
|
||||
reverb.connect(getDestination());
|
||||
reverbs[orbit] = reverb;
|
||||
}
|
||||
if (reverbs[orbit].duration !== duration) {
|
||||
reverbs[orbit] = reverbs[orbit].setDuration(duration);
|
||||
reverbs[orbit].duration = duration;
|
||||
|
||||
if (
|
||||
reverbs[orbit].duration !== duration ||
|
||||
reverbs[orbit].fade !== fade ||
|
||||
reverbs[orbit].lp !== lp ||
|
||||
reverbs[orbit].dim !== dim
|
||||
) {
|
||||
reverbs[orbit].generate(duration, fade, lp, dim);
|
||||
}
|
||||
|
||||
return reverbs[orbit];
|
||||
}
|
||||
|
||||
@@ -215,7 +223,10 @@ export const superdough = async (value, deadline, hapDuration) => {
|
||||
delaytime = 0.25,
|
||||
orbit = 1,
|
||||
room,
|
||||
size = 2,
|
||||
roomfade = 0.1,
|
||||
roomlp = 15000,
|
||||
roomdim = 1000,
|
||||
roomsize = 2,
|
||||
velocity = 1,
|
||||
analyze, // analyser wet
|
||||
fft = 8, // fftSize 0 - 10
|
||||
@@ -353,8 +364,8 @@ export const superdough = async (value, deadline, hapDuration) => {
|
||||
}
|
||||
// reverb
|
||||
let reverbSend;
|
||||
if (room > 0 && size > 0) {
|
||||
const reverbNode = getReverb(orbit, size);
|
||||
if (room > 0 && roomsize > 0) {
|
||||
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim);
|
||||
reverbSend = effectSend(post, reverbNode, room);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { registerSound, getAudioContext } from './superdough.mjs';
|
||||
import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs';
|
||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||
|
||||
const mod = (freq, range = 1, type = 'sine') => {
|
||||
const ctx = getAudioContext();
|
||||
@@ -20,75 +21,26 @@ const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => {
|
||||
return mod(modfreq, modgain, wave);
|
||||
};
|
||||
|
||||
const waveforms = ['sine', 'square', 'triangle', 'sawtooth'];
|
||||
const noises = ['pink', 'white', 'brown'];
|
||||
|
||||
export function registerSynthSounds() {
|
||||
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
|
||||
[...waveforms, ...noises].forEach((s) => {
|
||||
registerSound(
|
||||
wave,
|
||||
s,
|
||||
(t, value, onended) => {
|
||||
// destructure adsr here, because the default should be different for synths and samples
|
||||
let {
|
||||
attack = 0.001,
|
||||
decay = 0.05,
|
||||
sustain = 0.6,
|
||||
release = 0.01,
|
||||
fmh: fmHarmonicity = 1,
|
||||
fmi: fmModulationIndex,
|
||||
fmenv: fmEnvelopeType = 'lin',
|
||||
fmattack: fmAttack,
|
||||
fmdecay: fmDecay,
|
||||
fmsustain: fmSustain,
|
||||
fmrelease: fmRelease,
|
||||
fmvelocity: fmVelocity,
|
||||
fmwave: fmWaveform = 'sine',
|
||||
vib = 0,
|
||||
vibmod = 0.5,
|
||||
} = 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 { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;
|
||||
|
||||
// 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;
|
||||
let sound;
|
||||
if (waveforms.includes(s)) {
|
||||
sound = getOscillator(s, t, value);
|
||||
} else {
|
||||
sound = getNoiseOscillator(s, t);
|
||||
}
|
||||
|
||||
let { node: o, stop, triggerRelease } = sound;
|
||||
|
||||
// turn down
|
||||
const g = gainNode(0.3);
|
||||
|
||||
@@ -104,10 +56,9 @@ export function registerSynthSounds() {
|
||||
node: o.connect(g).connect(envelope),
|
||||
stop: (releaseTime) => {
|
||||
releaseEnvelope(releaseTime);
|
||||
fmEnvelope?.stop(releaseTime);
|
||||
triggerRelease?.(releaseTime);
|
||||
let end = releaseTime + release;
|
||||
stop(end);
|
||||
stopFm?.(end);
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -146,36 +97,108 @@ export function waveformN(partials, type) {
|
||||
return osc;
|
||||
}
|
||||
|
||||
export function getOscillator({ s, freq, t, vib, vibmod, partials }) {
|
||||
// Make oscillator with partial count
|
||||
// expects one of waveforms as s
|
||||
export function getOscillator(
|
||||
s,
|
||||
t,
|
||||
{
|
||||
n: partials,
|
||||
note,
|
||||
freq,
|
||||
vib = 0,
|
||||
vibmod = 0.5,
|
||||
noise = 0,
|
||||
// fm
|
||||
fmh: fmHarmonicity = 1,
|
||||
fmi: fmModulationIndex,
|
||||
fmenv: fmEnvelopeType = 'lin',
|
||||
fmattack: fmAttack,
|
||||
fmdecay: fmDecay,
|
||||
fmsustain: fmSustain,
|
||||
fmrelease: fmRelease,
|
||||
fmvelocity: fmVelocity,
|
||||
fmwave: fmWaveform = 'sine',
|
||||
},
|
||||
) {
|
||||
let ac = getAudioContext();
|
||||
let o;
|
||||
// If no partials are given, use stock waveforms
|
||||
if (!partials || s === 'sine') {
|
||||
o = getAudioContext().createOscillator();
|
||||
o.type = s || 'triangle';
|
||||
} else {
|
||||
}
|
||||
// generate custom waveform if partials are given
|
||||
else {
|
||||
o = waveformN(partials, s);
|
||||
}
|
||||
|
||||
// get frequency from note...
|
||||
note = note || 36;
|
||||
if (typeof note === 'string') {
|
||||
note = noteToMidi(note); // e.g. c3 => 48
|
||||
}
|
||||
// get frequency
|
||||
if (!freq && typeof note === 'number') {
|
||||
freq = midiToFreq(note); // + 48);
|
||||
}
|
||||
|
||||
// set frequency
|
||||
o.frequency.value = Number(freq);
|
||||
o.start(t);
|
||||
|
||||
// FM
|
||||
let stopFm, fmEnvelope;
|
||||
if (fmModulationIndex) {
|
||||
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
|
||||
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
|
||||
// no envelope by default
|
||||
modulator.connect(o.frequency);
|
||||
} else {
|
||||
fmAttack = fmAttack ?? 0.001;
|
||||
fmDecay = fmDecay ?? 0.001;
|
||||
fmSustain = fmSustain ?? 1;
|
||||
fmRelease = fmRelease ?? 0.001;
|
||||
fmVelocity = fmVelocity ?? 1;
|
||||
fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
|
||||
if (fmEnvelopeType === 'exp') {
|
||||
fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
|
||||
fmEnvelope.node.maxValue = fmModulationIndex * 2;
|
||||
fmEnvelope.node.minValue = 0.00001;
|
||||
}
|
||||
modulator.connect(fmEnvelope.node);
|
||||
fmEnvelope.node.connect(o.frequency);
|
||||
}
|
||||
stopFm = stop;
|
||||
}
|
||||
|
||||
// Additional oscillator for vibrato effect
|
||||
let vibrato_oscillator;
|
||||
let vibratoOscillator;
|
||||
if (vib > 0) {
|
||||
vibrato_oscillator = getAudioContext().createOscillator();
|
||||
vibrato_oscillator.frequency.value = vib;
|
||||
vibratoOscillator = getAudioContext().createOscillator();
|
||||
vibratoOscillator.frequency.value = vib;
|
||||
const gain = getAudioContext().createGain();
|
||||
// Vibmod is the amount of vibrato, in semitones
|
||||
gain.gain.value = vibmod * 100;
|
||||
vibrato_oscillator.connect(gain);
|
||||
vibratoOscillator.connect(gain);
|
||||
gain.connect(o.detune);
|
||||
vibrato_oscillator.start(t);
|
||||
vibratoOscillator.start(t);
|
||||
}
|
||||
|
||||
let noiseMix;
|
||||
if (noise) {
|
||||
noiseMix = getNoiseMix(o, noise, t);
|
||||
}
|
||||
|
||||
return {
|
||||
node: o,
|
||||
node: noiseMix?.node || o,
|
||||
stop: (time) => {
|
||||
vibrato_oscillator?.stop(time);
|
||||
vibratoOscillator?.stop(time);
|
||||
noiseMix?.stop(time);
|
||||
stopFm?.(time);
|
||||
o.stop(time);
|
||||
},
|
||||
triggerRelease: (time) => {
|
||||
fmEnvelope?.stop(time);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const getZZFX = (value, t) => {
|
||||
pitchJump = 0,
|
||||
pitchJumpTime = 0,
|
||||
lfo = 0,
|
||||
noise = 0,
|
||||
znoise = 0,
|
||||
zmod = 0,
|
||||
zcrush = 0,
|
||||
zdelay = 0,
|
||||
@@ -54,7 +54,7 @@ export const getZZFX = (value, t) => {
|
||||
pitchJump,
|
||||
pitchJumpTime,
|
||||
lfo,
|
||||
noise,
|
||||
znoise,
|
||||
zmod,
|
||||
zcrush,
|
||||
zdelay,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { isNoteWithOctave } from '@strudel.cycles/core';
|
||||
import { getLeafLocations } from '@strudel.cycles/mini';
|
||||
|
||||
export function transpiler(input, options = {}) {
|
||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true } = options;
|
||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
||||
|
||||
let ast = parse(input, {
|
||||
ecmaVersion: 2022,
|
||||
@@ -16,9 +16,9 @@ export function transpiler(input, options = {}) {
|
||||
let miniLocations = [];
|
||||
const collectMiniLocations = (value, node) => {
|
||||
const leafLocs = getLeafLocations(`"${value}"`, node.start); // stimmt!
|
||||
//const withOffset = leafLocs.map((offsets) => offsets.map((o) => o + node.start));
|
||||
miniLocations = miniLocations.concat(leafLocs);
|
||||
};
|
||||
let widgets = [];
|
||||
|
||||
walk(ast, {
|
||||
enter(node, parent /* , prop, index */) {
|
||||
@@ -35,6 +35,18 @@ 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();
|
||||
@@ -64,15 +76,14 @@ export function transpiler(input, options = {}) {
|
||||
if (!emitMiniLocations) {
|
||||
return { output };
|
||||
}
|
||||
return { output, miniLocations };
|
||||
return { output, miniLocations, widgets };
|
||||
}
|
||||
|
||||
function isStringWithDoubleQuotes(node, locations, code) {
|
||||
const { raw, type } = node;
|
||||
if (type !== 'Literal') {
|
||||
if (node.type !== 'Literal') {
|
||||
return false;
|
||||
}
|
||||
return raw[0] === '"';
|
||||
return node.raw[0] === '"';
|
||||
}
|
||||
|
||||
function isBackTickString(node, parent) {
|
||||
@@ -94,3 +105,22 @@ 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;
|
||||
}
|
||||
|
||||
@@ -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 } from 'superdough';
|
||||
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
|
||||
const { Pattern, logger } = strudel;
|
||||
|
||||
setLogger(logger);
|
||||
@@ -35,3 +35,7 @@ export function webaudioScheduler(options = {}) {
|
||||
onTrigger: strudel.getTrigger({ defaultOutput, getTime }),
|
||||
});
|
||||
}
|
||||
|
||||
Pattern.prototype.dough = function () {
|
||||
return this.onTrigger(doughTrigger, 1);
|
||||
};
|
||||
|
||||
Generated
+3
-3
@@ -181,9 +181,6 @@ 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
|
||||
@@ -591,6 +588,9 @@ importers:
|
||||
'@strudel.cycles/xen':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/xen
|
||||
'@strudel/codemirror':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/codemirror
|
||||
'@strudel/desktopbridge':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/desktopbridge
|
||||
|
||||
Generated
+85
-1147
File diff suppressed because it is too large
Load Diff
@@ -21,13 +21,7 @@ tauri = { version = "1.4.0", features = ["fs-all"] }
|
||||
midir = "0.9.1"
|
||||
tokio = { version = "1.29.0", features = ["full"] }
|
||||
rosc = "0.10.1"
|
||||
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"
|
||||
rusty_link = "0.3.6"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
@@ -38,5 +32,5 @@ custom-protocol = ["tauri/custom-protocol"]
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
lto = "fat"
|
||||
lto = true
|
||||
opt-level = "s"
|
||||
|
||||
+2
-1
@@ -4,7 +4,8 @@ Rust source files for building native desktop apps using Tauri
|
||||
|
||||
## Usage
|
||||
|
||||
Install [Rust](https://rustup.rs/) on your system.
|
||||
- Install [Rust](https://rustup.rs/) on your system.
|
||||
- Install `cmake` on your system. OSX: `brew install cmake`, Linux: `sudo apt-get install cmake`
|
||||
|
||||
From the project root:
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
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(())
|
||||
}
|
||||
+41
-38
@@ -4,20 +4,14 @@
|
||||
mod midibridge;
|
||||
mod oscbridge;
|
||||
mod loggerbridge;
|
||||
mod webaudiobridge;
|
||||
// mod superdough;
|
||||
// mod reverbgen;
|
||||
mod webaudio;
|
||||
|
||||
mod ablelinkbridge;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ablelinkbridge::AbeLinkState;
|
||||
use loggerbridge::Logger;
|
||||
use tauri::Manager;
|
||||
use tokio::{
|
||||
sync::mpsc,
|
||||
sync::Mutex
|
||||
};
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::Mutex;
|
||||
// the payload type must implement `Serialize` and `Clone`.
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct Payload {
|
||||
@@ -29,33 +23,42 @@ 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),
|
||||
})
|
||||
.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");
|
||||
::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");
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
mod superdough;
|
||||
mod reverbgen;
|
||||
|
||||
pub use self::superdough::*;
|
||||
pub use self::reverbgen::*;
|
||||
@@ -1,241 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -2959,6 +2959,15 @@ exports[`runs examples > example "never" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "noise" example index 0 1`] = `
|
||||
[
|
||||
"[ (0/1 → 1/1) ⇝ 2/1 | s:white ]",
|
||||
"[ 0/1 ⇜ (1/1 → 2/1) | s:white ]",
|
||||
"[ (2/1 → 3/1) ⇝ 4/1 | s:pink ]",
|
||||
"[ 2/1 ⇜ (3/1 → 4/1) | s:pink ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "note" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c ]",
|
||||
@@ -3654,16 +3663,107 @@ exports[`runs examples > example "room" example index 1 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "roomdim" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
|
||||
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
|
||||
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
|
||||
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
|
||||
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
|
||||
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
|
||||
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
|
||||
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "roomdim" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
|
||||
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
|
||||
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
|
||||
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
|
||||
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
|
||||
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
|
||||
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
|
||||
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "roomfade" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
|
||||
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
|
||||
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
|
||||
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
|
||||
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
|
||||
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
|
||||
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
|
||||
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "roomfade" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
|
||||
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
|
||||
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
|
||||
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
|
||||
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
|
||||
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
|
||||
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
|
||||
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "roomlp" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 ]",
|
||||
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 ]",
|
||||
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 ]",
|
||||
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 ]",
|
||||
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 ]",
|
||||
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 ]",
|
||||
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 ]",
|
||||
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "roomlp" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 ]",
|
||||
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 ]",
|
||||
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 ]",
|
||||
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 ]",
|
||||
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 ]",
|
||||
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 ]",
|
||||
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 ]",
|
||||
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "roomsize" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd room:0.8 size:0 ]",
|
||||
"[ 1/2 → 1/1 | s:sd room:0.8 size:0 ]",
|
||||
"[ 1/1 → 3/2 | s:bd room:0.8 size:1 ]",
|
||||
"[ 3/2 → 2/1 | s:sd room:0.8 size:1 ]",
|
||||
"[ 2/1 → 5/2 | s:bd room:0.8 size:2 ]",
|
||||
"[ 5/2 → 3/1 | s:sd room:0.8 size:2 ]",
|
||||
"[ 3/1 → 7/2 | s:bd room:0.8 size:4 ]",
|
||||
"[ 7/2 → 4/1 | s:sd room:0.8 size:4 ]",
|
||||
"[ 0/1 → 1/2 | s:bd room:0.8 roomsize:1 ]",
|
||||
"[ 1/2 → 1/1 | s:sd room:0.8 roomsize:1 ]",
|
||||
"[ 1/1 → 3/2 | s:bd room:0.8 roomsize:1 ]",
|
||||
"[ 3/2 → 2/1 | s:sd room:0.8 roomsize:1 ]",
|
||||
"[ 2/1 → 5/2 | s:bd room:0.8 roomsize:1 ]",
|
||||
"[ 5/2 → 3/1 | s:sd room:0.8 roomsize:1 ]",
|
||||
"[ 3/1 → 7/2 | s:bd room:0.8 roomsize:1 ]",
|
||||
"[ 7/2 → 4/1 | s:sd room:0.8 roomsize:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "roomsize" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd room:0.8 roomsize:4 ]",
|
||||
"[ 1/2 → 1/1 | s:sd room:0.8 roomsize:4 ]",
|
||||
"[ 1/1 → 3/2 | s:bd room:0.8 roomsize:4 ]",
|
||||
"[ 3/2 → 2/1 | s:sd room:0.8 roomsize:4 ]",
|
||||
"[ 2/1 → 5/2 | s:bd room:0.8 roomsize:4 ]",
|
||||
"[ 5/2 → 3/1 | s:sd room:0.8 roomsize:4 ]",
|
||||
"[ 3/1 → 7/2 | s:bd room:0.8 roomsize:4 ]",
|
||||
"[ 7/2 → 4/1 | s:sd room:0.8 roomsize:4 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"@strudel.cycles/mini": "workspace:*",
|
||||
"@strudel.cycles/osc": "workspace:*",
|
||||
"@strudel.cycles/react": "workspace:*",
|
||||
"@strudel/codemirror": "workspace:*",
|
||||
"@strudel.cycles/serial": "workspace:*",
|
||||
"@strudel.cycles/soundfonts": "workspace:*",
|
||||
"@strudel.cycles/tonal": "workspace:*",
|
||||
|
||||
@@ -183,24 +183,40 @@ 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} />
|
||||
|
||||
## room
|
||||
## Reverb
|
||||
|
||||
### 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).
|
||||
|
||||
@@ -23,6 +23,25 @@ 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:
|
||||
|
||||
@@ -4,10 +4,9 @@ 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 } from '@strudel.cycles/core';
|
||||
import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger, Cyclist } 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';
|
||||
@@ -23,6 +22,8 @@ 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();
|
||||
|
||||
@@ -34,25 +35,27 @@ const supabase = createClient(
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
|
||||
);
|
||||
|
||||
const modules = [
|
||||
let 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.concat([
|
||||
modules = modules.concat([
|
||||
import('@strudel/desktopbridge/loggerbridge.mjs'),
|
||||
import('@strudel/desktopbridge/midibridge.mjs'),
|
||||
import('@strudel/desktopbridge/oscbridge.mjs'),
|
||||
import('@strudel/desktopbridge/cyclistbridge.mjs'),
|
||||
]);
|
||||
} else {
|
||||
modules.concat([import('@strudel.cycles/midi'), import('@strudel.cycles/osc')]);
|
||||
modules = modules.concat([import('@strudel.cycles/midi'), import('@strudel.cycles/osc')]);
|
||||
}
|
||||
|
||||
const modulesLoading = evalScope(
|
||||
@@ -129,11 +132,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: isTauri() ? webaudioDesktopOutput : webaudioOutput,
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime,
|
||||
beforeEval: async () => {
|
||||
setPending(true);
|
||||
@@ -143,6 +146,7 @@ export function Repl({ embedded = false }) {
|
||||
},
|
||||
afterEval: ({ code, meta }) => {
|
||||
setMiniLocations(meta.miniLocations);
|
||||
setWidgets(meta.widgets);
|
||||
setPending(false);
|
||||
setLatestCode(code);
|
||||
window.location.hash = '#' + code2hash(code);
|
||||
@@ -150,10 +154,18 @@ export function Repl({ embedded = false }) {
|
||||
onEvalError: (err) => {
|
||||
setPending(false);
|
||||
},
|
||||
onToggle: (play) => !play && cleanupDraw(false),
|
||||
onToggle: (play) => {
|
||||
if (!play) {
|
||||
cleanupDraw(false);
|
||||
window.postMessage('strudel-stop');
|
||||
} else {
|
||||
window.postMessage('strudel-start');
|
||||
}
|
||||
},
|
||||
drawContext,
|
||||
// drawTime: [0, 6],
|
||||
paintOptions,
|
||||
createCyclist: (p) => (isTauri() ? new CyclistBridge(p) : new Cyclist(p)),
|
||||
});
|
||||
|
||||
// init code
|
||||
@@ -213,7 +225,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],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user