Compare commits

..

100 Commits

Author SHA1 Message Date
Jade Rowland 1bae027aaa Merge branch 'tidalcycles:main' into ableton_link_integration 2023-10-05 23:07:41 -04:00
Felix Roos 0b888ac54d Merge pull request #718 from Bubobubobubobubo/reverb
Better convolution reverb by generating impulse responses
2023-10-05 00:04:17 +02:00
Felix Roos 75099abbc1 remove unused reverb method 2023-10-05 00:03:02 +02:00
Felix Roos 3c4f835d8b consistent naming + simplify 2023-10-05 00:00:47 +02:00
Felix Roos ce842f7561 simplify createReverb 2023-10-04 23:56:00 +02:00
Felix Roos 508af7eb72 update internal reverb param names 2023-10-04 23:48:53 +02:00
Felix Roos a4386a617a Merge branch 'main' into reverb 2023-10-04 22:10:58 +02:00
Felix Roos 767ba713a6 Merge pull request #723 from tidalcycles/slider-afterthoughts
Slider afterthoughts
2023-10-04 10:31:52 +02:00
Felix Roos 020e85906d fix: slider crash on some platforms 2023-10-04 09:42:10 +02:00
Felix Roos d1ffdd57a5 Merge pull request #713 from Bubobubobubobubo/noise
Add 'white', 'pink' and 'brown' oscillators + refactor synth
2023-10-03 16:39:06 +02:00
Felix Roos 9c9323e040 snapshot 2023-10-03 12:42:56 +02:00
Felix Roos 376cf09565 rename zzfx noise to znoise 2023-10-03 12:41:57 +02:00
Felix Roos 047129223e cache noise 2023-10-03 12:25:47 +02:00
Felix Roos 4b64168faa fix: imports 2023-10-03 12:20:28 +02:00
Felix Roos 2bc6d08410 proper dry wet + pull out noise to extra file 2023-10-03 12:19:30 +02:00
Felix Roos 484bb6b11f refactor synth
- separate waveform / noise oscillators
- pull noise out of getOscillator
- put fm into getOscillator
- simplify overall value plumbing
2023-10-03 10:03:09 +02:00
Felix Roos a0884e2a03 add noise heading + hihat example 2023-10-03 09:09:49 +02:00
Felix Roos 1e352fdf80 codeformat 2023-10-03 08:51:35 +02:00
Felix Roos 83dfbb84c2 Merge branch 'main' into noise 2023-10-03 08:50:57 +02:00
Felix Roos 9043064543 snapshot 2023-10-03 08:36:52 +02:00
Felix Roos a3649148c1 more logical naming + update docs 2023-10-03 08:35:15 +02:00
Felix Roos 3a69a2561c half working cps link 2023-10-02 21:21:11 +02:00
Felix Roos 30a0059ef3 remove todo 2023-10-02 20:59:35 +02:00
Felix Roos b1fcf6ca34 cleanup 2023-10-02 20:59:10 +02:00
Felix Roos f2534310e8 fix: default cyclist 2023-10-02 20:57:25 +02:00
Felix Roos 25be8fca0d encapsulate cyclist 2023-10-02 20:56:45 +02:00
Felix Roos 28966739f6 feat: add step as slider param 2023-10-02 20:44:51 +02:00
Felix Roos f089037f18 add cmake instructions 2023-10-02 08:35:41 +02:00
Jade Rowland e818ca9f03 Merge branch 'tidalcycles:main' into ableton_link_integration 2023-10-01 20:44:09 -04:00
Jade Rowland 0efcf84c46 cleaning up 2023-10-01 20:30:47 -04:00
Jade Rowland 5db39b8e9e fix clock being set to 0 2023-10-01 19:45:44 -04:00
Jade Rowland 2127a8f642 cleaning things up 2023-10-01 19:19:49 -04:00
Felix Roos dcd75e0384 Merge branch 'main' into reverb 2023-10-01 23:48:48 +02:00
Felix Roos dfdd9e02ca eslint ignore reverbGen + snapshot 2023-10-01 23:22:32 +02:00
Felix Roos 275796c241 fix: reverbs[orbit] is undefined 2023-10-01 23:21:02 +02:00
Felix Roos 6ca99e33ab codeformat 2023-10-01 23:20:05 +02:00
Raphael Forment 1909caf769 Connecting new reverb documentation 2023-10-01 14:50:29 +02:00
Raphael Forment 2d07eeb518 Connecting all parameters to convolution generator 2023-10-01 14:44:34 +02:00
Felix Roos 1c0da7cd49 Merge branch 'main' of https://github.com/tidalcycles/strudel 2023-10-01 14:20:54 +02:00
Alex McLean fd316c81c0 support mininotation '..' range operator, fixes #715 (#716)
* support mininotation .. range operator, fixes #715

* remove logs
2023-10-01 13:20:49 +01:00
Felix Roos edbd437d7b hotfix: add missing dependency 2023-10-01 14:20:30 +02:00
Felix Roos 9be1f9c18b Merge pull request #714 from tidalcycles/ui-widgets
widgets
2023-10-01 14:15:09 +02:00
Felix Roos 96f06d8026 fix: import 2023-10-01 14:09:07 +02:00
Felix Roos 935d8e8aea fix: comment 2023-10-01 14:08:41 +02:00
Felix Roos 21b99b3810 remove comment 2023-10-01 14:07:48 +02:00
Felix Roos c051a1249d remove checkbox 2023-10-01 14:06:32 +02:00
Felix Roos 564697e175 add extra sliderWithID function
+ add warning to slider function
2023-10-01 14:05:16 +02:00
Felix Roos d4bf358eae use raw css instead of tailwind 2023-10-01 13:56:57 +02:00
Felix Roos f84d5ba3a0 add back some margin 2023-10-01 13:44:41 +02:00
Felix Roos b550ff038c simplify: use native events 2023-10-01 13:42:29 +02:00
Felix Roos c6ecd31ea1 improve mouse tracking 2023-10-01 13:27:44 +02:00
Raphael Forment e600b91a85 bugfixes for parameter passing 2023-10-01 12:02:17 +02:00
Raphael Forment abff279707 Document reverb controls 2023-10-01 11:52:24 +02:00
Jade Rowland 1d85d60b11 cleaning up 2023-10-01 00:46:40 -04:00
Jade Rowland d5c6cb85ea use cps instead of bpm 2023-09-30 22:18:31 -04:00
Felix Roos d7bc309eeb fix: import 2023-10-01 00:36:34 +02:00
Felix Roos 33c40e5ef8 fix some odd number / string problems 2023-10-01 00:33:10 +02:00
Felix Roos 062d926900 make sliders work! 2023-10-01 00:24:31 +02:00
Raphael Forment 0ad0046a76 Replacing old reverb by better convolution 2023-09-30 22:04:22 +02:00
Felix Roos b36cee93f7 add slider function to scope 2023-09-30 21:22:49 +02:00
Felix Roos 2731e70fb7 use loc as slider id 2023-09-30 21:03:28 +02:00
Felix Roos 276cf858fc match slider as function 2023-09-30 20:39:03 +02:00
Raphael Forment b2acff40c4 Add documentation about noise sources 2023-09-30 15:17:22 +02:00
Raphael Forment e3333e716f Cap noise amount to 1 2023-09-30 15:08:09 +02:00
Raphael Forment bb7b8c2fab Fix noise parameter and FM parameters compatibility 2023-09-30 14:59:43 +02:00
Raphael Forment 389c7be264 Add noise parameter for base oscillators 2023-09-30 14:39:44 +02:00
Raphael Forment c2481e460b Add pink, white and brown oscillators 2023-09-30 14:07:33 +02:00
Jade Rowland fe8c4e2bcd cyclist bridge 2023-09-29 23:27:43 -04:00
Felix Roos c93e4a951a match number.slider 2023-09-29 12:58:16 +02:00
Felix Roos aa094bf930 checkbox + number slider 2023-09-29 10:40:00 +02:00
Jade Rowland 6eda7af14b play sync now accurate 2023-09-29 01:01:01 -04:00
Felix Roos 8a741952df Merge pull request #699 from tidalcycles/midi-in
Midi in
2023-09-28 11:03:09 +02:00
Felix Roos 0dcc55ee16 prevent error 2023-09-28 10:48:31 +02:00
Felix Roos 6c3a3b9e29 dedupe 2023-09-28 10:40:44 +02:00
Felix Roos e4eff8c837 Merge remote-tracking branch 'origin/main' into midi-in 2023-09-28 10:40:24 +02:00
Felix Roos 527b895e3c Merge pull request #710 from tidalcycles/midi-clock
add midi clock support
2023-09-27 22:53:49 +02:00
Felix Roos 4eb0a7b7c0 send start with accurate timing 2023-09-27 22:42:35 +02:00
Felix Roos f11462bf41 sync start / stop automatically too 2023-09-27 22:28:52 +02:00
Felix Roos dea1c31701 use midicmd instead of clock 2023-09-27 22:10:21 +02:00
Jade Rowland 6719c6127b figuring out backend integration 2023-09-27 15:28:23 -04:00
Felix Roos 68ab43b3ab support midi clock via "clock" control
(not on desktop yet)
2023-09-27 21:26:24 +02:00
Felix Roos cf72e3bba5 fix: add conditional imports to eval scope
+ fire postMessage on start
2023-09-27 21:25:30 +02:00
Jade Rowland 172006a3b3 improving listener 2023-09-25 22:32:31 -04:00
Jade Rowland 209122b8f7 start offset 2023-09-25 19:29:14 -04:00
Felix Roos 62743edf45 bump superdough to 0.9.9 2023-09-25 23:57:19 +02:00
Felix Roos eec3752b5a cleanup 2023-09-25 23:57:09 +02:00
Felix Roos 006fd6c733 Merge pull request #707 from tidalcycles/superdough-worklet
add dough function for raw dsp
2023-09-25 23:06:30 +02:00
Felix Roos 7078e20200 less garbage 2023-09-25 22:56:44 +02:00
Felix Roos 52c01abbe9 encapsulate .dough 2023-09-25 22:39:03 +02:00
Felix Roos 6e26f39751 add dough function for raw dsp 2023-09-25 22:34:31 +02:00
Jade Rowland d7fe491c84 play stop working 2023-09-22 14:27:03 -04:00
Jade Rowland db36636ab2 play working kinda 2023-09-21 23:08:30 -04:00
Jade Rowland 789cad3e97 figuring things out 2023-09-21 22:57:05 -04:00
Felix Roos 30cba7b935 Merge branch 'main' into midi-in 2023-09-17 17:26:42 +02:00
Felix Roos 30d96dcb65 remove log 2023-09-14 16:28:47 +02:00
Felix Roos 3a69fd50bb fix: linting errors 2023-09-14 09:39:31 +02:00
Felix Roos 0f72729f0d midi cc input poc 2023-09-14 09:36:06 +02:00
Felix Roos 0a8874180c generalize getDevice + begin midiIn implementation 2023-09-13 13:48:33 +02:00
Felix Roos a6501474bb Merge branch 'fix-eval-scope' into midi-in 2023-09-13 13:00:52 +02:00
Felix Roos 4cb93ddcde fix: eval scope 2023-09-13 12:59:51 +02:00
46 changed files with 1559 additions and 2810 deletions
+2 -1
View File
@@ -18,4 +18,5 @@ vite.config.js
**/*.json
**/dev-dist
**/dist
/src-tauri/target/**/*
/src-tauri/target/**/*
reverbGen.mjs
+1
View File
@@ -1,3 +1,4 @@
export * from './codemirror.mjs';
export * from './highlight.mjs';
export * from './flash.mjs';
export * from './slider.mjs';
+135
View File
@@ -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
View File
@@ -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'],
+6
View File
@@ -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();
+5 -2
View File
@@ -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);
+2 -1
View File
@@ -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;
+77
View File
@@ -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 });
}
}
+1 -1
View File
@@ -7,4 +7,4 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './midibridge.mjs';
export * from './utils.mjs';
export * from './oscbridge.mjs';
export * from './webaudiobridge.mjs';
export * from './cyclistbridge.mjs';
-1
View File
@@ -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"
},
-244
View File
@@ -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
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import { Pattern, isPattern, logger } 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
View File
@@ -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;
+7 -1
View File
@@ -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});
+11
View File
@@ -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`);
}
+6
View File
@@ -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,
+2
View File
@@ -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,
+13
View File
@@ -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 };
}
+79
View File
@@ -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 });
}
+22
View File
@@ -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;
}
+1
View File
@@ -10,3 +10,4 @@ export * from './helpers.mjs';
export * from './synth.mjs';
export * from './zzfx.mjs';
export * from './logger.mjs';
export * from './dspworklet.mjs';
+63
View File
@@ -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 -1
View File
@@ -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",
+24 -17
View File
@@ -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
+129
View File
@@ -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;
+19 -8
View File
@@ -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);
}
+97 -74
View File
@@ -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);
},
};
}
+2 -2
View File
@@ -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,
+36 -6
View File
@@ -5,7 +5,7 @@ import { isNoteWithOctave } from '@strudel.cycles/core';
import { getLeafLocations } from '@strudel.cycles/mini';
export function transpiler(input, options = {}) {
const { wrapAsync = false, addReturn = true, emitMiniLocations = true } = 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 -1
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as strudel from '@strudel.cycles/core';
import { superdough, getAudioContext, setLogger } 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);
};
+3 -3
View File
@@ -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
+85 -1147
View File
File diff suppressed because it is too large Load Diff
+2 -8
View File
@@ -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
View File
@@ -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:
+175
View File
@@ -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
View File
@@ -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");
}
-5
View File
@@ -1,5 +0,0 @@
mod superdough;
mod reverbgen;
pub use self::superdough::*;
pub use self::reverbgen::*;
-241
View File
@@ -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);
}
-393
View File
@@ -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
}
-368
View 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())
}
+108 -8
View File
@@ -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 ]",
]
`;
+1
View File
@@ -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:*",
+21 -5
View File
@@ -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).
+19
View File
@@ -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:
+21 -9
View File
@@ -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],
);