mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 13:13:10 -04:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fbcd28f09a | |||
| bfb32d89a9 | |||
| 6e18bd8d57 | |||
| 9d22a9f9ba | |||
| c0411c389f | |||
| 163f8b53fd | |||
| d447b9cf95 | |||
| c0c6963112 | |||
| e793cb671a | |||
| f6fc55b43e | |||
| 56d992def9 | |||
| eef51f9228 | |||
| 56dd5bb604 | |||
| c2fc2272c5 | |||
| 18a198df9b | |||
| 870e7e4f32 | |||
| 6b219ee11a | |||
| 38af4af4ff | |||
| 69a3e27eec | |||
| 9606ce0d51 | |||
| 229dfe487a | |||
| 592b982990 | |||
| 6633b373bc | |||
| 5a8d369fa6 | |||
| 747ea8e6a3 | |||
| a693f89796 | |||
| b93ac61a7a | |||
| 6a066a5304 | |||
| 4ca57b308f | |||
| 97e2d89e61 | |||
| f5ca1ff49d | |||
| c456186f0f | |||
| ebbbcfb071 | |||
| 7492c9383a | |||
| 51a16b45a6 | |||
| 89d65ad09d | |||
| a92834de42 | |||
| c8270a98e9 | |||
| 12d8e22b46 | |||
| 78a8ca2b4f | |||
| c90bd23a53 | |||
| 725ddc805d | |||
| 00c0df4bfb | |||
| 7a17ec6863 | |||
| 081f93cb79 | |||
| 6d5fb0687c |
@@ -970,6 +970,57 @@ const generic_params = [
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[['room', 'size']],
|
[['room', 'size']],
|
||||||
|
/**
|
||||||
|
* Reverb lowpass starting frequency (in hertz).
|
||||||
|
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
|
||||||
|
*
|
||||||
|
* @name roomlp
|
||||||
|
* @synonyms rlp
|
||||||
|
* @param {number} frequency between 0 and 20000hz
|
||||||
|
* @example
|
||||||
|
* s("bd sd").room(0.5).rlp(10000)
|
||||||
|
* @example
|
||||||
|
* s("bd sd").room(0.5).rlp(5000)
|
||||||
|
*/
|
||||||
|
['roomlp', 'rlp'],
|
||||||
|
/**
|
||||||
|
* Reverb lowpass frequency at -60dB (in hertz).
|
||||||
|
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
|
||||||
|
*
|
||||||
|
* @name roomdim
|
||||||
|
* @synonyms rdim
|
||||||
|
* @param {number} frequency between 0 and 20000hz
|
||||||
|
* @example
|
||||||
|
* s("bd sd").room(0.5).rlp(10000).rdim(8000)
|
||||||
|
* @example
|
||||||
|
* s("bd sd").room(0.5).rlp(5000).rdim(400)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
['roomdim', 'rdim'],
|
||||||
|
/**
|
||||||
|
* Reverb fade time (in seconds).
|
||||||
|
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
|
||||||
|
*
|
||||||
|
* @name roomfade
|
||||||
|
* @synonyms rfade
|
||||||
|
* @param {number} seconds for the reverb to fade
|
||||||
|
* @example
|
||||||
|
* s("bd sd").room(0.5).rlp(10000).rfade(0.5)
|
||||||
|
* @example
|
||||||
|
* s("bd sd").room(0.5).rlp(5000).rfade(4)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
['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}.
|
* Sets the room size of the reverb, see {@link room}.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
export * from './midibridge.mjs';
|
export * from './midibridge.mjs';
|
||||||
export * from './utils.mjs';
|
export * from './utils.mjs';
|
||||||
export * from './oscbridge.mjs';
|
export * from './oscbridge.mjs';
|
||||||
|
export * from './webaudiobridge.mjs';
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@strudel.cycles/webaudio": "^0.9.0",
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"@tauri-apps/api": "^1.4.0"
|
"@tauri-apps/api": "^1.4.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
import { Invoke } from './utils.mjs';
|
||||||
|
import { duration, isNote, midiToFreq, noteToMidi, Pattern, valueToMidi } from '@strudel.cycles/core';
|
||||||
|
import { getAudioContext, getEnvelope, getSound, logger } from '@strudel.cycles/webaudio';
|
||||||
|
|
||||||
|
export const desktopAudio = async (value, deadline, hapDuration) => {
|
||||||
|
const ac = getAudioContext();
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
throw new Error(
|
||||||
|
`expected hap.value to be an object, but got "${value}". Hint: append .note() or .s() to the end`,
|
||||||
|
'error',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let t = ac.currentTime + deadline;
|
||||||
|
|
||||||
|
let {
|
||||||
|
note = 'C2',
|
||||||
|
s = 'triangle',
|
||||||
|
bank,
|
||||||
|
source,
|
||||||
|
gain = 0.8,
|
||||||
|
// low pass
|
||||||
|
cutoff = 8000,
|
||||||
|
resonance = 1,
|
||||||
|
// high pass
|
||||||
|
hcutoff = 0,
|
||||||
|
hresonance = 1,
|
||||||
|
// band pass
|
||||||
|
bandf = 0,
|
||||||
|
bandq = 1,
|
||||||
|
//
|
||||||
|
coarse,
|
||||||
|
crush,
|
||||||
|
shape,
|
||||||
|
pan,
|
||||||
|
vowel,
|
||||||
|
delay,
|
||||||
|
delayfeedback,
|
||||||
|
delaytime,
|
||||||
|
orbit = 1,
|
||||||
|
room,
|
||||||
|
roomfade,
|
||||||
|
roomlp,
|
||||||
|
roomdim,
|
||||||
|
size,
|
||||||
|
ir,
|
||||||
|
i = 0,
|
||||||
|
velocity = 1,
|
||||||
|
analyze, // analyser wet
|
||||||
|
fft = 8, // fftSize 0 - 10
|
||||||
|
speed = 1, // sample playback speed
|
||||||
|
begin = 0,
|
||||||
|
end = 1,
|
||||||
|
loop = 0,
|
||||||
|
loopBegin = 0,
|
||||||
|
loopEnd = 1,
|
||||||
|
attack,
|
||||||
|
decay,
|
||||||
|
sustain,
|
||||||
|
release,
|
||||||
|
lpattack,
|
||||||
|
lpdecay,
|
||||||
|
lpsustain,
|
||||||
|
lprelease,
|
||||||
|
lpenv,
|
||||||
|
hpattack,
|
||||||
|
hpdecay,
|
||||||
|
hpsustain,
|
||||||
|
hprelease,
|
||||||
|
hpenv,
|
||||||
|
bpattack,
|
||||||
|
bpdecay,
|
||||||
|
bpsustain,
|
||||||
|
bprelease,
|
||||||
|
bpenv,
|
||||||
|
n = 0,
|
||||||
|
freq,
|
||||||
|
unit,
|
||||||
|
} = value;
|
||||||
|
|
||||||
|
value.duration = hapDuration;
|
||||||
|
if (bank && s) {
|
||||||
|
s = `${bank}_${s}`;
|
||||||
|
}
|
||||||
|
if (freq !== undefined && note !== undefined) {
|
||||||
|
logger('[sampler] hap has note and freq. ignoring note', 'warning');
|
||||||
|
}
|
||||||
|
let midi = valueToMidi({ freq, note }, 36);
|
||||||
|
let transpose = midi - 36;
|
||||||
|
|
||||||
|
let sampleUrl;
|
||||||
|
let baseUrl;
|
||||||
|
let path;
|
||||||
|
const waveForms = new Set(['sine', 'square', 'saw', 'sawtooth', 'triangle']);
|
||||||
|
if (waveForms.has(s)) {
|
||||||
|
sampleUrl = 'none';
|
||||||
|
} else {
|
||||||
|
if (getSound(s).data.baseUrl !== undefined) {
|
||||||
|
baseUrl = getSound(s).data.baseUrl;
|
||||||
|
if (baseUrl === './piano/') {
|
||||||
|
path = 'https://strudel.tidalcycles.org/';
|
||||||
|
} else if (baseUrl === './EmuSP12/') {
|
||||||
|
path = 'https://strudel.tidalcycles.org/';
|
||||||
|
} else {
|
||||||
|
path = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let map = getSound(s).data.samples;
|
||||||
|
if (Array.isArray(map)) {
|
||||||
|
sampleUrl =
|
||||||
|
path !== undefined ? path + map[n % map.length].replace('./', '') : map[n % map.length].replace('./', '');
|
||||||
|
} else {
|
||||||
|
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||||
|
// object format will expect keys as notes
|
||||||
|
const closest = Object.keys(map)
|
||||||
|
.filter((k) => !k.startsWith('_'))
|
||||||
|
.reduce(
|
||||||
|
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
transpose = -midiDiff(closest); // semitones to repitch
|
||||||
|
sampleUrl =
|
||||||
|
path !== undefined
|
||||||
|
? path + map[closest][n % map[closest].length].replace('./', '')
|
||||||
|
: map[closest][n % map[closest].length].replace('./', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNote(note)) {
|
||||||
|
note = noteToMidi(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
const playbackRate = 1.0 * Math.pow(2, transpose / 12);
|
||||||
|
|
||||||
|
if (delay !== 0) {
|
||||||
|
delay = Math.abs(delay);
|
||||||
|
delayfeedback = Math.abs(delayfeedback);
|
||||||
|
delaytime = Math.abs(delaytime);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ir_path;
|
||||||
|
|
||||||
|
let url;
|
||||||
|
if (ir !== undefined) {
|
||||||
|
if (getSound(ir).data.baseUrl !== undefined) {
|
||||||
|
baseUrl = getSound(ir).data.baseUrl;
|
||||||
|
if (baseUrl === './piano/') {
|
||||||
|
ir_path = 'https://strudel.tidalcycles.org/';
|
||||||
|
} else if (baseUrl === './EmuSP12/') {
|
||||||
|
ir_path = 'https://strudel.tidalcycles.org/';
|
||||||
|
} else {
|
||||||
|
ir_path = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ir_samples = getSound(ir);
|
||||||
|
if (Array.isArray(ir_samples)) {
|
||||||
|
url =
|
||||||
|
ir_path !== undefined
|
||||||
|
? ir_path + ir_samples[n % ir_samples.length].replace('./', '')
|
||||||
|
: ir_samples[n % ir_samples.length].replace('./', '');
|
||||||
|
} else if (typeof ir_samples === 'object') {
|
||||||
|
console.log(
|
||||||
|
'obk',
|
||||||
|
Object.values(ir_samples.data.samples)[i & Object.values(ir_samples.data.samples).length].replace('./', ''),
|
||||||
|
);
|
||||||
|
url =
|
||||||
|
ir_path !== undefined
|
||||||
|
? ir_path +
|
||||||
|
Object.values(ir_samples.data.samples)[i & Object.values(ir_samples.data.samples).length].replace('./', '')
|
||||||
|
: Object.values(ir_samples.data.samples)[i & Object.values(ir_samples.data.samples).length].replace('./', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('url', url);
|
||||||
|
|
||||||
|
let folder;
|
||||||
|
if (baseUrl !== undefined) {
|
||||||
|
folder = baseUrl.replace('./', '') + '/' + s + '/';
|
||||||
|
} else {
|
||||||
|
folder = 'misc';
|
||||||
|
}
|
||||||
|
const dirname = folder + '/' + s + '/';
|
||||||
|
const offset = (t - getAudioContext().currentTime) * 1000;
|
||||||
|
const roundedOffset = Math.round(offset);
|
||||||
|
const messagesfromjs = [];
|
||||||
|
const packages = {
|
||||||
|
loop: [loop, loopBegin, loopEnd],
|
||||||
|
delay: [delay, delaytime, delayfeedback],
|
||||||
|
reverb: [room, size, roomfade, roomlp, roomdim,ir, url],
|
||||||
|
lpf: [cutoff, resonance],
|
||||||
|
hpf: [hcutoff, hresonance],
|
||||||
|
bpf: [bandf, bandq],
|
||||||
|
adsr: [attack, decay, sustain, release],
|
||||||
|
lpenv: [lpattack, lpdecay, lpsustain, lprelease, lpenv],
|
||||||
|
hpenv: [hpattack, hpdecay, hpsustain, hprelease, hpenv],
|
||||||
|
bpenv: [bpattack, bpdecay, bpsustain, bprelease, bpenv],
|
||||||
|
};
|
||||||
|
|
||||||
|
messagesfromjs.push({
|
||||||
|
note: midiToFreq(note),
|
||||||
|
offset: roundedOffset,
|
||||||
|
waveform: s,
|
||||||
|
lpf: packages.lpf,
|
||||||
|
hpf: packages.hpf,
|
||||||
|
bpf: packages.bpf,
|
||||||
|
duration: hapDuration,
|
||||||
|
velocity: velocity,
|
||||||
|
delay: packages.delay,
|
||||||
|
reverb: packages.reverb,
|
||||||
|
orbit: orbit,
|
||||||
|
speed: speed,
|
||||||
|
begin: begin,
|
||||||
|
end: end,
|
||||||
|
looper: packages.loop,
|
||||||
|
adsr: packages.adsr,
|
||||||
|
lpenv: packages.lpenv,
|
||||||
|
hpenv: packages.hpenv,
|
||||||
|
bpenv: packages.bpenv,
|
||||||
|
n: n,
|
||||||
|
sampleurl: sampleUrl,
|
||||||
|
dirname: dirname,
|
||||||
|
unit: unit,
|
||||||
|
playbackrate: playbackRate,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (messagesfromjs.length) {
|
||||||
|
setTimeout(() => {
|
||||||
|
Invoke('sendwebaudio', { messagesfromjs });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hap2value = (hap) => {
|
||||||
|
hap.ensureObjectValue();
|
||||||
|
return { ...hap.value, velocity: hap.context.velocity };
|
||||||
|
};
|
||||||
|
export const webaudioDesktopOutputTrigger = (t, hap, ct, cps) =>
|
||||||
|
desktopAudio(hap2value(hap), t - ct, hap.duration / cps, cps);
|
||||||
|
export const webaudioDesktopOutput = (hap, deadline, hapDuration) =>
|
||||||
|
desktopAudio(hap2value(hap), deadline, hapDuration);
|
||||||
|
|
||||||
|
Pattern.prototype.webaudio = function () {
|
||||||
|
return this.onTrigger(webaudioDesktopOutputTrigger);
|
||||||
|
};
|
||||||
Generated
+18
@@ -181,6 +181,9 @@ importers:
|
|||||||
'@strudel.cycles/core':
|
'@strudel.cycles/core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core
|
||||||
|
'@strudel.cycles/webaudio':
|
||||||
|
specifier: ^0.9.0
|
||||||
|
version: link:../webaudio
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
version: 1.4.0
|
version: 1.4.0
|
||||||
@@ -1426,6 +1429,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.21.5):
|
/@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==}
|
resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1441,6 +1445,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.21.5):
|
/@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==}
|
resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1454,6 +1459,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-class-static-block@7.20.7(@babel/core@7.21.5):
|
/@babel/plugin-proposal-class-static-block@7.20.7(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ==}
|
resolution: {integrity: sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.12.0
|
'@babel/core': ^7.12.0
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1468,6 +1474,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.21.5):
|
/@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==}
|
resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1479,6 +1486,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.21.5):
|
/@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
|
resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1490,6 +1498,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.21.5):
|
/@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==}
|
resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1501,6 +1510,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.21.5):
|
/@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==}
|
resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1512,6 +1522,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.21.5):
|
/@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==}
|
resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1523,6 +1534,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.21.5):
|
/@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==}
|
resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1534,6 +1546,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.21.5):
|
/@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==}
|
resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1548,6 +1561,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.21.5):
|
/@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==}
|
resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1559,6 +1573,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-optional-chaining@7.20.7(@babel/core@7.21.5):
|
/@babel/plugin-proposal-optional-chaining@7.20.7(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==}
|
resolution: {integrity: sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1571,6 +1586,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.21.5):
|
/@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==}
|
resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1584,6 +1600,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-private-property-in-object@7.20.5(@babel/core@7.21.5):
|
/@babel/plugin-proposal-private-property-in-object@7.20.5(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==}
|
resolution: {integrity: sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1599,6 +1616,7 @@ packages:
|
|||||||
/@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.21.5):
|
/@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.21.5):
|
||||||
resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==}
|
resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Generated
+1218
-43
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,13 @@ tauri = { version = "1.4.0", features = ["fs-all"] }
|
|||||||
midir = "0.9.1"
|
midir = "0.9.1"
|
||||||
tokio = { version = "1.29.0", features = ["full"] }
|
tokio = { version = "1.29.0", features = ["full"] }
|
||||||
rosc = "0.10.1"
|
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"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||||
@@ -31,5 +38,5 @@ custom-protocol = ["tauri/custom-protocol"]
|
|||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
lto = true
|
lto = "fat"
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
|
|||||||
+38
-24
@@ -4,12 +4,20 @@
|
|||||||
mod midibridge;
|
mod midibridge;
|
||||||
mod oscbridge;
|
mod oscbridge;
|
||||||
mod loggerbridge;
|
mod loggerbridge;
|
||||||
|
mod webaudiobridge;
|
||||||
|
// mod superdough;
|
||||||
|
// mod reverbgen;
|
||||||
|
mod webaudio;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use loggerbridge::Logger;
|
use loggerbridge::Logger;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tokio::sync::mpsc;
|
use tokio::{
|
||||||
use tokio::sync::Mutex;
|
sync::mpsc,
|
||||||
|
sync::Mutex
|
||||||
|
};
|
||||||
|
|
||||||
// the payload type must implement `Serialize` and `Clone`.
|
// the payload type must implement `Serialize` and `Clone`.
|
||||||
#[derive(Clone, serde::Serialize)]
|
#[derive(Clone, serde::Serialize)]
|
||||||
struct Payload {
|
struct Payload {
|
||||||
@@ -21,27 +29,33 @@ fn main() {
|
|||||||
let (async_output_transmitter_midi, async_output_receiver_midi) = mpsc::channel(1);
|
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_input_transmitter_osc, async_input_receiver_osc) = mpsc::channel(1);
|
||||||
let (async_output_transmitter_osc, async_output_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
|
tauri::Builder
|
||||||
::default()
|
::default()
|
||||||
.manage(midibridge::AsyncInputTransmit {
|
.manage(midibridge::AsyncInputTransmit {
|
||||||
inner: Mutex::new(async_input_transmitter_midi),
|
inner: Mutex::new(async_input_transmitter_midi),
|
||||||
})
|
})
|
||||||
.manage(oscbridge::AsyncInputTransmit {
|
.manage(oscbridge::AsyncInputTransmit {
|
||||||
inner: Mutex::new(async_input_transmitter_osc),
|
inner: Mutex::new(async_input_transmitter_osc),
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc])
|
.manage(webaudiobridge::AsyncInputTransmitWebAudio {
|
||||||
.setup(|app| {
|
inner: Mutex::new(async_input_transmitter_webaudio),
|
||||||
let window = Arc::new(app.get_window("main").unwrap());
|
})
|
||||||
let logger = Logger { window };
|
.invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc, webaudiobridge::sendwebaudio])
|
||||||
midibridge::init(
|
.setup(|app| {
|
||||||
logger.clone(),
|
let window = Arc::new(app.get_window("main").unwrap());
|
||||||
async_input_receiver_midi,
|
let logger = Logger { window };
|
||||||
async_output_receiver_midi,
|
midibridge::init(
|
||||||
async_output_transmitter_midi
|
logger.clone(),
|
||||||
);
|
async_input_receiver_midi,
|
||||||
oscbridge::init(logger, async_input_receiver_osc, async_output_receiver_osc, async_output_transmitter_osc);
|
async_output_receiver_midi,
|
||||||
Ok(())
|
async_output_transmitter_midi
|
||||||
})
|
);
|
||||||
.run(tauri::generate_context!())
|
oscbridge::init(logger, async_input_receiver_osc, async_output_receiver_osc, async_output_transmitter_osc);
|
||||||
.expect("error while running tauri application");
|
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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
mod superdough;
|
||||||
|
mod reverbgen;
|
||||||
|
|
||||||
|
pub use self::superdough::*;
|
||||||
|
pub use self::reverbgen::*;
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use mini_moka::sync::Cache;
|
||||||
|
use rand_distr::{Distribution, Normal};
|
||||||
|
use web_audio_api::AudioBuffer;
|
||||||
|
use web_audio_api::context::{AudioContext, BaseAudioContext, OfflineAudioContext};
|
||||||
|
use web_audio_api::node::{AudioNode, DynamicsCompressorNode};
|
||||||
|
use crate::webaudio::{create_filepath_generic, download_file, Reverb, Sampler, WebAudioInstrument};
|
||||||
|
use crate::webaudiobridge::WebAudioMessage;
|
||||||
|
|
||||||
|
fn apply_decay(input: &mut [f32], decay_factor: f32) {
|
||||||
|
let len = input.len();
|
||||||
|
|
||||||
|
for i in 0..len {
|
||||||
|
// Multiply each sample by an exponentially decreasing factor
|
||||||
|
let decay_amt = (-(i as f32) / (len as f32) * decay_factor).exp();
|
||||||
|
input[i] *= decay_amt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_fir_filter(input: &mut [f32], coefficients: &[f32]) {
|
||||||
|
let num_coeffs = coefficients.len();
|
||||||
|
let len = input.len();
|
||||||
|
|
||||||
|
let input_copy = input.to_vec();
|
||||||
|
|
||||||
|
for i in num_coeffs..len {
|
||||||
|
let mut acc = 0.0;
|
||||||
|
for j in 0..num_coeffs {
|
||||||
|
acc += input_copy[i - j] * coefficients[j];
|
||||||
|
}
|
||||||
|
input[i] = acc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sinc(x: f32, fc: f32) -> f32 {
|
||||||
|
let pi = std::f32::consts::PI;
|
||||||
|
if x == 0.0 {
|
||||||
|
2.0 * fc
|
||||||
|
} else {
|
||||||
|
let arg = 2.0 * pi * fc * x;
|
||||||
|
arg.sin() / arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hamming(n: usize, size: usize) -> f32 {
|
||||||
|
0.54 - 0.46 * ((2. * std::f32::consts::PI * n as f32) / ((size - 1) as f32)).cos()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_coefficients_lp(size: usize, fc: f32) -> Vec<f32> {
|
||||||
|
let mut h = vec![0.0; size];
|
||||||
|
let m = (size - 1) / 2;
|
||||||
|
|
||||||
|
for n in 0..size {
|
||||||
|
h[n] = sinc((n as f32 - m as f32), fc);
|
||||||
|
h[n] *= hamming(n, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sum: f32 = h.iter().sum();
|
||||||
|
for n in 0..size {
|
||||||
|
h[n] /= sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
h
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_impulse_response(
|
||||||
|
len: usize,
|
||||||
|
decay_factor: f32,
|
||||||
|
fir_coeffs: &[f32],
|
||||||
|
sample_rate: f32,
|
||||||
|
fade: f32,
|
||||||
|
) -> Vec<f32> {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let normal = Normal::new(0.0, 1.0).unwrap();
|
||||||
|
let fade_in_samples = (fade * sample_rate) as usize; // Compute the number of samples for the fade-in duration
|
||||||
|
|
||||||
|
let mut impulse_response: Vec<f32> = (0..len)
|
||||||
|
.map(|_| normal.sample(&mut rng) as f32)
|
||||||
|
.map(|v| v.max(-1.0).min(1.0)) // Clamp values to the -1.0 to 1.0 range
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Apply fade-in to the starting samples
|
||||||
|
for i in 0..fade_in_samples.min(len) {
|
||||||
|
impulse_response[i] *= i as f32 / fade_in_samples as f32;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_decay(&mut impulse_response, decay_factor);
|
||||||
|
apply_fir_filter(&mut impulse_response, fir_coeffs);
|
||||||
|
|
||||||
|
impulse_response
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_to_buffer(buffer: &mut AudioBuffer, data: &[f32]) {
|
||||||
|
let num_chans = buffer.number_of_channels();
|
||||||
|
|
||||||
|
for i in 0..num_chans {
|
||||||
|
let chan = buffer.get_channel_data_mut(i);
|
||||||
|
for j in 0..chan.len() {
|
||||||
|
chan[j] = data[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_reverb<T: WebAudioInstrument>(mut audio_context: &AudioContext, compressor: &DynamicsCompressorNode, cache: &Cache<String, AudioBuffer>, message: &WebAudioMessage, instrument: &mut T, reverbs: &mut HashMap<usize, Reverb>) {
|
||||||
|
if message.reverb.ir.is_none() {
|
||||||
|
let key = format!("reverb: {}{}{}{}{}", message.reverb.room.unwrap_or(0.0), message.reverb.size.unwrap_or(1.0), message.reverb.roomlp.unwrap_or(0.0), message.reverb.roomfade.unwrap_or(0.0), message.reverb.roomdim.unwrap_or(0.0));
|
||||||
|
if let Some(ir) = cache.get(&key) {
|
||||||
|
let reverb = reverbs.entry(message.orbit).or_insert({
|
||||||
|
let mut reverb = Reverb::new(&audio_context, message.reverb.room, message.reverb.size, message.reverb.roomlp, message.reverb.roomfade, message.reverb.roomdim, &compressor);
|
||||||
|
reverb.convolver.set_buffer(ir.clone());
|
||||||
|
instrument.connect_reverb(&reverb.convolver);
|
||||||
|
reverb
|
||||||
|
});
|
||||||
|
|
||||||
|
if reverb.room.unwrap_or(0.0) != message.reverb.room.unwrap_or(0.0)
|
||||||
|
|| reverb.roomlp.unwrap_or(0.0) != message.reverb.roomlp.unwrap_or(0.0)
|
||||||
|
|| reverb.roomfade.unwrap_or(0.0) != message.reverb.roomfade.unwrap_or(0.0)
|
||||||
|
|| reverb.roomdim.unwrap_or(0.3) != message.reverb.roomdim.unwrap_or(0.0)
|
||||||
|
|| reverb.roomsize.unwrap_or(0.0) != message.reverb.size.unwrap_or(0.0)
|
||||||
|
|| reverb.roomfade.unwrap_or(0.0) != message.reverb.roomfade.unwrap_or(0.0) {
|
||||||
|
reverb.room = message.reverb.room;
|
||||||
|
reverb.roomlp = message.reverb.roomlp;
|
||||||
|
reverb.roomfade = message.reverb.roomfade;
|
||||||
|
reverb.roomdim = message.reverb.roomdim;
|
||||||
|
reverb.roomsize = message.reverb.size;
|
||||||
|
|
||||||
|
let len = (audio_context.sample_rate() * message.reverb.size.unwrap_or(0.6)) as usize;
|
||||||
|
let decay = message.reverb.roomdim.unwrap_or(2.0);
|
||||||
|
let num_coeffs = 101;
|
||||||
|
let fade = message.reverb.roomfade.unwrap_or(0.01);
|
||||||
|
let cutoff = message.reverb.roomlp.unwrap_or(1000.0) / audio_context.sample_rate();
|
||||||
|
let cache_cache = cache.clone();
|
||||||
|
let mut buffer = audio_context.create_buffer(2, len, 44100.0);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// GENERATE IR
|
||||||
|
let fir = generate_coefficients_lp(num_coeffs, cutoff);
|
||||||
|
let ir = create_impulse_response(len, decay, &fir, 44100.0, fade);
|
||||||
|
write_to_buffer(&mut buffer, &ir);
|
||||||
|
cache_cache.insert(key, buffer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
reverb.convolver.set_buffer(ir);
|
||||||
|
instrument.connect_reverb(&reverb.convolver);
|
||||||
|
|
||||||
|
// Generate new IR buffer if it doesn't exist
|
||||||
|
} else {
|
||||||
|
let len = (audio_context.sample_rate() * message.reverb.size.unwrap_or(1.0)) as usize;
|
||||||
|
let decay = 4.0;
|
||||||
|
let num_coeffs = 101;
|
||||||
|
let fade = 2.4;
|
||||||
|
let cutoff = 500.0 / audio_context.sample_rate();
|
||||||
|
let cache_cache = cache.clone();
|
||||||
|
let mut buffer = audio_context.create_buffer(2, len, 44100.0);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// Generate IR and write to buffer
|
||||||
|
let fir = generate_coefficients_lp(num_coeffs, cutoff);
|
||||||
|
let ir = create_impulse_response(len, decay, &fir, 44100.0, fade);
|
||||||
|
write_to_buffer(&mut buffer, &ir);
|
||||||
|
cache_cache.insert(key, buffer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if message.reverb.ir.is_some() {
|
||||||
|
let message_url = &match message.reverb.url.clone() {
|
||||||
|
Some(url) => url,
|
||||||
|
None => "todo".to_string(),
|
||||||
|
};
|
||||||
|
let (ir_url, ir_file_path, ir_file_path_clone) = create_filepath_generic(message_url, &message.dirname);
|
||||||
|
let ir_cache_clone = cache.clone();
|
||||||
|
let requested_size = message.reverb.size.unwrap_or(2.0).clone();
|
||||||
|
if let Some(ir_buffer) = cache.get(&ir_file_path_clone) {
|
||||||
|
let reverb = reverbs.entry(message.orbit).or_insert({
|
||||||
|
let mut reverb = Reverb::new(&audio_context, message.reverb.room, message.reverb.size, message.reverb.roomlp, message.reverb.roomfade, message.reverb.roomdim, &compressor);
|
||||||
|
reverb.convolver.set_buffer(ir_buffer.clone());
|
||||||
|
instrument.connect_reverb(&reverb.convolver);
|
||||||
|
reverb
|
||||||
|
});
|
||||||
|
|
||||||
|
if reverb.roomsize.unwrap_or(0.0) != message.reverb.size.unwrap_or(0.0) {
|
||||||
|
adjust_length(&mut audio_context, cache, message, ir_file_path_clone, requested_size, reverb);
|
||||||
|
}
|
||||||
|
|
||||||
|
reverb.convolver.set_buffer(ir_buffer);
|
||||||
|
instrument.connect_reverb(&reverb.convolver);
|
||||||
|
} else {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Ok(file1) = File::open(&ir_file_path) {
|
||||||
|
create_new_ir_buffer_from_file(&ir_file_path, ir_cache_clone, requested_size, file1);
|
||||||
|
} else if tokio::fs::metadata(&ir_file_path.clone()).await.is_err() {
|
||||||
|
download_file(ir_url, &ir_file_path).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_new_ir_buffer_from_file(ir_file_path: &String, ir_cache_clone: Cache<String, AudioBuffer>, requested_size: f32, file1: File) {
|
||||||
|
let context = OfflineAudioContext::new(2, 400, 44100.0);
|
||||||
|
let ir_buffer = context.decode_audio_data_sync(file1).expect("Failed to decode file.");
|
||||||
|
let new_length = ir_buffer.sample_rate() * requested_size;
|
||||||
|
let mut new_buffer = context.create_buffer(ir_buffer.number_of_channels(), new_length as usize, ir_buffer.sample_rate());
|
||||||
|
for ch in 0..ir_buffer.number_of_channels() {
|
||||||
|
let old_data = ir_buffer.get_channel_data(ch);
|
||||||
|
let new_data = new_buffer.get_channel_data_mut(ch);
|
||||||
|
|
||||||
|
for i in 0..new_length as usize {
|
||||||
|
if i < old_data.len() {
|
||||||
|
new_data[i] = old_data[i];
|
||||||
|
} else {
|
||||||
|
new_data[i] = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ir_cache_clone.insert(ir_file_path.clone(), new_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adjust_length(audio_context: &mut &AudioContext, cache: &Cache<String, AudioBuffer>, message: &WebAudioMessage, ir_file_path_clone: String, requested_size: f32, reverb: &mut Reverb) {
|
||||||
|
let buf = reverb.convolver.buffer().unwrap();
|
||||||
|
reverb.roomsize = message.reverb.size;
|
||||||
|
let new_length = buf.sample_rate() * requested_size;
|
||||||
|
let mut new_buffer = audio_context.create_buffer(buf.number_of_channels(), new_length as usize, buf.sample_rate());
|
||||||
|
|
||||||
|
for ch in 0..buf.number_of_channels() {
|
||||||
|
let old_data = buf.get_channel_data(ch);
|
||||||
|
let new_data = new_buffer.get_channel_data_mut(ch);
|
||||||
|
|
||||||
|
for i in 0..new_length as usize {
|
||||||
|
if i < old_data.len() {
|
||||||
|
new_data[i] = old_data[i];
|
||||||
|
} else {
|
||||||
|
new_data[i] = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reverb.convolver.set_buffer(new_buffer.clone());
|
||||||
|
cache.insert(ir_file_path_clone.clone(), new_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
use reqwest::Url;
|
||||||
|
use tokio::fs;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use web_audio_api::{
|
||||||
|
context::{AudioContext, BaseAudioContext},
|
||||||
|
AudioBuffer,
|
||||||
|
node::{AudioBufferSourceNode, AudioNode, AudioScheduledSourceNode, BiquadFilterNode, BiquadFilterType, GainNode, OscillatorNode, OscillatorType},
|
||||||
|
node::BiquadFilterType::{Bandpass, Highpass, Lowpass},
|
||||||
|
};
|
||||||
|
use web_audio_api::node::{ConvolverNode, DelayNode, DynamicsCompressorNode};
|
||||||
|
use crate::webaudiobridge::WebAudioMessage;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct DelayMessage {
|
||||||
|
pub wet: Option<f32>,
|
||||||
|
pub delay_time: Option<f32>,
|
||||||
|
pub feedback: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ReverbMessage {
|
||||||
|
pub room: Option<f32>,
|
||||||
|
pub size: Option<f32>,
|
||||||
|
pub roomfade: Option<f32>,
|
||||||
|
pub roomlp: Option<f32>,
|
||||||
|
pub roomdim: Option<f32>,
|
||||||
|
pub ir: Option<String>,
|
||||||
|
pub url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct LoopMessage {
|
||||||
|
pub is_loop: u8,
|
||||||
|
pub loop_start: f64,
|
||||||
|
pub loop_end: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct LPFMessage {
|
||||||
|
pub frequency: f32,
|
||||||
|
pub resonance: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct HPFMessage {
|
||||||
|
pub frequency: f32,
|
||||||
|
pub resonance: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct BPFMessage {
|
||||||
|
pub frequency: f32,
|
||||||
|
pub resonance: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct ADSRMessage {
|
||||||
|
pub attack: Option<f64>,
|
||||||
|
pub decay: Option<f64>,
|
||||||
|
pub sustain: Option<f32>,
|
||||||
|
pub release: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct FilterADSRMessage {
|
||||||
|
pub attack: Option<f64>,
|
||||||
|
pub decay: Option<f64>,
|
||||||
|
pub sustain: Option<f64>,
|
||||||
|
pub release: Option<f64>,
|
||||||
|
pub env: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub trait WebAudioInstrument {
|
||||||
|
fn set_adsr(&mut self, t: f64, adsr: &ADSRMessage, velocity: f32, duration: f64);
|
||||||
|
fn play(&mut self, t: f64, message: &WebAudioMessage, duration: f64);
|
||||||
|
fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec<BiquadFilterNode>;
|
||||||
|
fn connect_delay(&mut self, delay: &DelayNode);
|
||||||
|
fn connect_reverb(&mut self, reverb: &ConvolverNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub struct Delay {
|
||||||
|
pub delay: DelayNode,
|
||||||
|
pub input: GainNode,
|
||||||
|
pub output: GainNode,
|
||||||
|
pub feedback: GainNode,
|
||||||
|
pub pre_gain: GainNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Delay {
|
||||||
|
pub fn new(context: &AudioContext, compressor: &DynamicsCompressorNode) -> Self {
|
||||||
|
let delay = context.create_delay(1.);
|
||||||
|
let input = context.create_gain();
|
||||||
|
let output = context.create_gain();
|
||||||
|
let feedback = context.create_gain();
|
||||||
|
let pre_gain = context.create_gain();
|
||||||
|
output.connect(compressor);
|
||||||
|
delay.connect(&output);
|
||||||
|
feedback.connect(&delay);
|
||||||
|
pre_gain.connect(&feedback);
|
||||||
|
input.connect(&pre_gain);
|
||||||
|
Self { delay, input, output, feedback, pre_gain }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub struct Reverb {
|
||||||
|
pub convolver: ConvolverNode,
|
||||||
|
pub room: Option<f32>,
|
||||||
|
pub roomsize: Option<f32>,
|
||||||
|
pub roomfade: Option<f32>,
|
||||||
|
pub roomlp: Option<f32>,
|
||||||
|
pub roomdim: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reverb {
|
||||||
|
pub(crate) fn new(context: &AudioContext, room: Option<f32>, roomsize: Option<f32>, roomfade: Option<f32>, roomlp: Option<f32>, roomdim: Option<f32>, compressor: &DynamicsCompressorNode) -> Self {
|
||||||
|
let mut convolver = context.create_convolver();
|
||||||
|
convolver.connect(compressor);
|
||||||
|
Self { convolver, room, roomsize, roomfade, roomlp, roomdim }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Synth {
|
||||||
|
pub oscillator: OscillatorNode,
|
||||||
|
pub envelope: GainNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Synth {
|
||||||
|
pub fn new(context: &mut AudioContext) -> Self {
|
||||||
|
let oscillator = context.create_oscillator();
|
||||||
|
let envelope = context.create_gain();
|
||||||
|
Self { oscillator, envelope }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_frequency(&mut self, frequency: &f32) {
|
||||||
|
self.oscillator.frequency().set_value(*frequency);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_waveform(&mut self, waveform: &str) {
|
||||||
|
match waveform {
|
||||||
|
"sine" => self.oscillator.set_type(OscillatorType::Sine),
|
||||||
|
"square" => self.oscillator.set_type(OscillatorType::Square),
|
||||||
|
"triangle" => self.oscillator.set_type(OscillatorType::Triangle),
|
||||||
|
"saw" | "sawtooth" => self.oscillator.set_type(OscillatorType::Sawtooth),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebAudioInstrument for Synth {
|
||||||
|
fn set_adsr(&mut self, t: f64, adsr: &ADSRMessage, velocity: f32, duration: f64) {
|
||||||
|
let attack = adsr.attack.unwrap_or(0.001);
|
||||||
|
let decay = adsr.decay.unwrap_or(0.05);
|
||||||
|
let sustain = adsr.sustain.unwrap_or(0.6);
|
||||||
|
let release = adsr.release.unwrap_or(0.01);
|
||||||
|
self.envelope.gain()
|
||||||
|
.set_value_at_time(0.0, t)
|
||||||
|
.linear_ramp_to_value_at_time(velocity, t + attack)
|
||||||
|
.exponential_ramp_to_value_at_time((sustain + 0.0001) * velocity, t + attack + decay)
|
||||||
|
// .set_value_at_time((sustain + 0.00001) * velocity, t + duration)
|
||||||
|
.exponential_ramp_to_value_at_time(0.000001, t + duration + release);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn play(&mut self, t: f64, message: &WebAudioMessage, release: f64) {
|
||||||
|
self.oscillator.start();
|
||||||
|
self.oscillator.stop_at(t + message.duration + release);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec<BiquadFilterNode> {
|
||||||
|
let mut filters = Vec::new();
|
||||||
|
if message.bpf.frequency > 0.0 {
|
||||||
|
let mut bpf = context.create_biquad_filter();
|
||||||
|
bpf.set_type(Bandpass);
|
||||||
|
bpf.frequency().set_value(message.bpf.frequency);
|
||||||
|
bpf.q().set_value(message.bpf.resonance);
|
||||||
|
filters.push(bpf);
|
||||||
|
} else if message.lpf.frequency > 0.0 {
|
||||||
|
let mut lpf = context.create_biquad_filter();
|
||||||
|
lpf.set_type(Lowpass);
|
||||||
|
lpf.frequency().set_value(message.lpf.frequency);
|
||||||
|
lpf.q().set_value(message.lpf.resonance);
|
||||||
|
filters.push(lpf);
|
||||||
|
} else if message.hpf.frequency > 0.0 {
|
||||||
|
let mut hpf = context.create_biquad_filter();
|
||||||
|
hpf.set_type(Highpass);
|
||||||
|
hpf.frequency().set_value(message.hpf.frequency);
|
||||||
|
hpf.q().set_value(message.hpf.resonance);
|
||||||
|
filters.push(hpf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !filters.is_empty() {
|
||||||
|
self.oscillator.connect(filters.first().unwrap());
|
||||||
|
filters.last().unwrap().connect(&self.envelope);
|
||||||
|
} else {
|
||||||
|
self.oscillator.connect(&self.envelope);
|
||||||
|
};
|
||||||
|
|
||||||
|
filters
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect_delay(&mut self, delay: &DelayNode) {
|
||||||
|
self.envelope.connect(delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect_reverb(&mut self, reverb: &ConvolverNode) {
|
||||||
|
self.envelope.connect(reverb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Sampler {
|
||||||
|
pub sample: AudioBufferSourceNode,
|
||||||
|
pub envelope: GainNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sampler {
|
||||||
|
pub fn new(context: &mut AudioContext, audio_buffer: AudioBuffer) -> Self {
|
||||||
|
let mut sample = context.create_buffer_source();
|
||||||
|
sample.set_buffer(audio_buffer);
|
||||||
|
let envelope = context.create_gain();
|
||||||
|
Self { sample, envelope }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebAudioInstrument for Sampler {
|
||||||
|
fn set_adsr(&mut self, t: f64, adsr: &ADSRMessage, velocity: f32, duration: f64) {
|
||||||
|
let attack = adsr.attack.unwrap_or(0.001);
|
||||||
|
let decay = adsr.decay.unwrap_or(0.001);
|
||||||
|
let sustain = adsr.sustain.unwrap_or(1.0);
|
||||||
|
let release = adsr.release.unwrap_or(0.01);
|
||||||
|
self.envelope.gain()
|
||||||
|
.set_value_at_time(0.0, t)
|
||||||
|
.linear_ramp_to_value_at_time(velocity, t + attack)
|
||||||
|
.linear_ramp_to_value_at_time((sustain + 0.00001) * velocity, t + attack + decay)
|
||||||
|
.set_value_at_time((sustain + 0.00001) * velocity, t + duration)
|
||||||
|
.linear_ramp_to_value_at_time(0.0, t + duration + release);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn play(&mut self, t: f64, message: &WebAudioMessage, release: f64) {
|
||||||
|
let buffer_duration = release;
|
||||||
|
let (start_at, stop_at) = if message.speed < 0.0 {
|
||||||
|
(buffer_duration, t + message.duration + 0.2)
|
||||||
|
} else {
|
||||||
|
(message.begin * buffer_duration, t + message.duration + message.adsr.release.unwrap_or(0.01))
|
||||||
|
};
|
||||||
|
if message.looper.is_loop > 0 {
|
||||||
|
self.sample.set_loop(true);
|
||||||
|
self.sample.set_loop_start(message.looper.loop_start);
|
||||||
|
self.sample.set_loop_end(message.looper.loop_end);
|
||||||
|
self.sample.start_at_with_offset(
|
||||||
|
t,
|
||||||
|
self.sample.loop_start(),
|
||||||
|
);
|
||||||
|
self.sample.stop_at(t + message.duration + message.adsr.release.unwrap_or(0.01));
|
||||||
|
} else {
|
||||||
|
self.sample.start_at_with_offset(
|
||||||
|
t,
|
||||||
|
start_at,
|
||||||
|
);
|
||||||
|
self.sample.stop_at(stop_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec<BiquadFilterNode> {
|
||||||
|
let mut filters = Vec::new();
|
||||||
|
if message.bpf.frequency > 0.0 {
|
||||||
|
let mut bpf = context.create_biquad_filter();
|
||||||
|
bpf.set_type(Bandpass);
|
||||||
|
bpf.frequency().set_value(message.bpf.frequency);
|
||||||
|
bpf.q().set_value(message.bpf.resonance);
|
||||||
|
filters.push(bpf);
|
||||||
|
} else if message.lpf.frequency > 0.0 {
|
||||||
|
let mut lpf = context.create_biquad_filter();
|
||||||
|
lpf.set_type(Lowpass);
|
||||||
|
lpf.frequency().set_value(message.lpf.frequency);
|
||||||
|
lpf.q().set_value(message.lpf.resonance);
|
||||||
|
filters.push(lpf);
|
||||||
|
} else if message.hpf.frequency > 0.0 {
|
||||||
|
let mut hpf = context.create_biquad_filter();
|
||||||
|
hpf.set_type(Highpass);
|
||||||
|
hpf.frequency().set_value(message.hpf.frequency);
|
||||||
|
hpf.q().set_value(message.hpf.resonance);
|
||||||
|
filters.push(hpf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !filters.is_empty() {
|
||||||
|
self.sample.connect(filters.first().unwrap());
|
||||||
|
filters.last().unwrap().connect(&self.envelope);
|
||||||
|
} else {
|
||||||
|
self.sample.connect(&self.envelope);
|
||||||
|
};
|
||||||
|
filters
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect_delay(&mut self, delay: &DelayNode) {
|
||||||
|
self.envelope.connect(delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect_reverb(&mut self, reverb: &ConvolverNode) {
|
||||||
|
self.envelope.connect(reverb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_filter_envelope_on(adsr: &FilterADSRMessage) -> bool {
|
||||||
|
adsr.env.is_some() || adsr.attack.is_some() || adsr.decay.is_some() || adsr.sustain.is_some() || adsr.release.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_filter_adsr(filter_node: &BiquadFilterNode, message: &WebAudioMessage, filter: &BiquadFilterType, now: f64) {
|
||||||
|
let env = match filter {
|
||||||
|
Lowpass => message.lpenv,
|
||||||
|
Highpass => message.hpenv,
|
||||||
|
Bandpass => message.bpenv,
|
||||||
|
_ => message.lpenv,
|
||||||
|
};
|
||||||
|
|
||||||
|
let freq = match filter {
|
||||||
|
Lowpass => message.lpf.frequency,
|
||||||
|
Highpass => message.hpf.frequency,
|
||||||
|
Bandpass => message.bpf.frequency,
|
||||||
|
_ => 8000.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let offset = env.env.unwrap_or(1.0) * 0.5;
|
||||||
|
let min = (2f32.powf(-offset as f32) * freq).clamp(0.0, 20000.0);
|
||||||
|
let max = (2f32.powf((env.env.unwrap_or(1.0) - offset) as f32) * freq).clamp(0.0, 20000.0);
|
||||||
|
let range = max - min;
|
||||||
|
let peak = min + range;
|
||||||
|
let sustain_level = min + env.sustain.unwrap_or(1.0) as f32 * range;
|
||||||
|
|
||||||
|
filter_node.frequency().set_value_at_time(min, now)
|
||||||
|
.linear_ramp_to_value_at_time(peak, now + env.attack.unwrap_or(0.01))
|
||||||
|
.linear_ramp_to_value_at_time(sustain_level, now + env.attack.unwrap_or(0.01) + env.decay.unwrap_or(0.01))
|
||||||
|
// .set_value_at_time(sustain_level, now + message.duration)
|
||||||
|
.linear_ramp_to_value_at_time(min, now + message.duration + env.release.unwrap_or(0.01));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_delay<T: WebAudioInstrument>(audio_context: &mut AudioContext, delays: &mut HashMap<usize, Delay>, compressor: &DynamicsCompressorNode, message: &WebAudioMessage, t: f64, instrument: &mut T) {
|
||||||
|
delays.entry(message.orbit).or_insert({
|
||||||
|
Delay::new(&audio_context, &compressor)
|
||||||
|
});
|
||||||
|
let delay = delays.get(&message.orbit).unwrap();
|
||||||
|
instrument.connect_delay(&delay.delay);
|
||||||
|
delay.delay.delay_time().set_value(message.delay.delay_time.unwrap_or(0.25));
|
||||||
|
delay.feedback.gain().set_value(message.delay.feedback.unwrap_or(0.5));
|
||||||
|
delay.pre_gain.gain().set_value_at_time(message.delay.wet.unwrap_or(0.25), t + message.delay.delay_time.unwrap_or(0.5) as f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_delay_synth(audio_context: &mut AudioContext, delays: &mut HashMap<usize, Delay>, compressor: &DynamicsCompressorNode, message: &WebAudioMessage, t: f64, synth: &mut Synth) {
|
||||||
|
delays.entry(message.orbit).or_insert({
|
||||||
|
Delay::new(&audio_context, &compressor)
|
||||||
|
});
|
||||||
|
let delay = delays.get(&message.orbit).unwrap();
|
||||||
|
synth.envelope.connect(&delay.input);
|
||||||
|
delay.delay.delay_time().set_value(message.delay.delay_time.unwrap_or(0.25));
|
||||||
|
delay.feedback.gain().set_value(message.delay.feedback.unwrap_or(0.5));
|
||||||
|
delay.pre_gain.gain().set_value_at_time(message.delay.wet.unwrap_or(0.25), t + message.delay.delay_time.unwrap_or(0.5) as f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn download_file(ir_url: Url, ir_file_path: &String) {
|
||||||
|
let response = reqwest::get(ir_url)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to send GET request"));
|
||||||
|
let bytes = response.bytes().await.unwrap();
|
||||||
|
let path = Path::new(&ir_file_path);
|
||||||
|
let mut file = create_file_and_dirs(path).await;
|
||||||
|
file.write_all(&bytes)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to write to file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_filepath_generic(url_string: &str, dirname: &str) -> (Url, String, String) {
|
||||||
|
let url = Url::parse(url_string).expect("failed to parse url");
|
||||||
|
let filename = url.path_segments()
|
||||||
|
.and_then(Iterator::last)
|
||||||
|
.and_then(|name| if name.is_empty() { None } else { Some(name) })
|
||||||
|
.unwrap_or("tmp.ben");
|
||||||
|
let file_path = format!("samples/{}{}", dirname, filename);
|
||||||
|
let file_path_clone = file_path.clone();
|
||||||
|
|
||||||
|
(url, file_path, file_path_clone)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_file_and_dirs(path: &Path) -> fs::File {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent).await.unwrap();
|
||||||
|
}
|
||||||
|
let file = fs::File::create(path).await.unwrap();
|
||||||
|
file
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
use std::{
|
||||||
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
|
fs::File,
|
||||||
|
path::Path,
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use mini_moka::sync::Cache;
|
||||||
|
use tokio::{
|
||||||
|
sync::{mpsc, Mutex},
|
||||||
|
io::AsyncWriteExt,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use web_audio_api::{
|
||||||
|
context::BaseAudioContext,
|
||||||
|
AudioBuffer,
|
||||||
|
context::{AudioContext, AudioContextLatencyCategory, AudioContextOptions},
|
||||||
|
node::AudioNode,
|
||||||
|
};
|
||||||
|
use web_audio_api::context::OfflineAudioContext;
|
||||||
|
use crate::webaudio::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct WebAudioMessage {
|
||||||
|
pub note: f32,
|
||||||
|
pub instant: Instant,
|
||||||
|
pub offset: f64,
|
||||||
|
pub waveform: String,
|
||||||
|
pub lpf: LPFMessage,
|
||||||
|
pub hpf: HPFMessage,
|
||||||
|
pub bpf: BPFMessage,
|
||||||
|
pub duration: f64,
|
||||||
|
pub velocity: f32,
|
||||||
|
pub delay: DelayMessage,
|
||||||
|
pub reverb: ReverbMessage,
|
||||||
|
pub orbit: usize,
|
||||||
|
pub speed: f32,
|
||||||
|
pub begin: f64,
|
||||||
|
pub end: f64,
|
||||||
|
pub looper: LoopMessage,
|
||||||
|
pub adsr: ADSRMessage,
|
||||||
|
pub lpenv: FilterADSRMessage,
|
||||||
|
pub hpenv: FilterADSRMessage,
|
||||||
|
pub bpenv: FilterADSRMessage,
|
||||||
|
pub n: usize,
|
||||||
|
pub sampleurl: String,
|
||||||
|
pub dirname: String,
|
||||||
|
pub unit: Option<String>,
|
||||||
|
pub playbackrate: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AsyncInputTransmitWebAudio {
|
||||||
|
pub inner: Mutex<mpsc::Sender<Vec<WebAudioMessage>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
async_input_receiver: mpsc::Receiver<Vec<WebAudioMessage>>,
|
||||||
|
mut async_output_receiver: mpsc::Receiver<Vec<WebAudioMessage>>,
|
||||||
|
async_output_transmitter: mpsc::Sender<Vec<WebAudioMessage>>,
|
||||||
|
) {
|
||||||
|
tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await });
|
||||||
|
let (sender, receiver) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
|
let message_queue: Arc<Mutex<Vec<WebAudioMessage>>> = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
/* ...........................................................
|
||||||
|
Listen For incoming messages and add to queue
|
||||||
|
............................................................*/
|
||||||
|
let message_queue_clone = Arc::clone(&message_queue);
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
loop {
|
||||||
|
if let Some(package) = async_output_receiver.recv().await {
|
||||||
|
let mut message_queue = message_queue_clone.lock().await;
|
||||||
|
let messages = package;
|
||||||
|
for message in messages {
|
||||||
|
(*message_queue).push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let message_queue_clone = Arc::clone(&message_queue);
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
/* ...........................................................
|
||||||
|
Process queued messages
|
||||||
|
............................................................*/
|
||||||
|
loop {
|
||||||
|
let mut message_queue = message_queue_clone.lock().await;
|
||||||
|
|
||||||
|
// Iterate over each message, play and remove messages when they are ready
|
||||||
|
message_queue.retain(|message| {
|
||||||
|
if message.instant.elapsed().as_millis() < message.offset as u128 {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
match sender.send(message.clone()) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ...........................................................
|
||||||
|
Create and setup WebAudio
|
||||||
|
............................................................*/
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let latency_hint = match std::env::var("WEB_AUDIO_LATENCY").as_deref() {
|
||||||
|
Ok("playback") => AudioContextLatencyCategory::Playback,
|
||||||
|
_ => AudioContextLatencyCategory::default(),
|
||||||
|
};
|
||||||
|
let mut audio_context = AudioContext::new(AudioContextOptions {
|
||||||
|
latency_hint,
|
||||||
|
..AudioContextOptions::default()
|
||||||
|
});
|
||||||
|
let _ = audio_context.create_buffer_source();
|
||||||
|
let mut delays: HashMap<usize, Delay> = HashMap::new();
|
||||||
|
let mut reverbs: HashMap<usize, Reverb> = HashMap::new();
|
||||||
|
|
||||||
|
let compressor = audio_context.create_dynamics_compressor();
|
||||||
|
compressor.threshold().set_value(-50.0);
|
||||||
|
compressor.connect(&audio_context.destination());
|
||||||
|
|
||||||
|
let cache: Cache<String, AudioBuffer> = Cache::builder()
|
||||||
|
.max_capacity(31 * 1024 * 1024)
|
||||||
|
.time_to_idle(Duration::from_secs(40))
|
||||||
|
.build();
|
||||||
|
let ir_cache: Cache<String, AudioBuffer> = Cache::builder().max_capacity(31 * 1024 * 1024).build();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match receiver.recv() {
|
||||||
|
Ok(message) => {
|
||||||
|
match message.waveform.as_str() {
|
||||||
|
"sine" | "square" | "triangle" | "saw" | "sawtooth" => {
|
||||||
|
let t = audio_context.current_time();
|
||||||
|
let mut synth = Synth::new(&mut audio_context);
|
||||||
|
synth.set_frequency(&message.note);
|
||||||
|
synth.set_waveform(&message.waveform);
|
||||||
|
let filters = synth.set_filters(&mut audio_context, &message);
|
||||||
|
if message.delay.wet.is_some() {
|
||||||
|
apply_delay(&mut audio_context, &mut delays, &compressor, &message, t, &mut synth);
|
||||||
|
}
|
||||||
|
if message.reverb.room.is_some() && message.reverb.room.unwrap() > 0.0 {
|
||||||
|
apply_reverb(&audio_context, &compressor, &ir_cache, &message, &mut synth, &mut reverbs);
|
||||||
|
}
|
||||||
|
synth.envelope.connect(&compressor);
|
||||||
|
synth.play(t, &message, message.adsr.release.unwrap_or(0.001));
|
||||||
|
synth.set_adsr(t, &message.adsr, message.velocity, message.duration);
|
||||||
|
if is_filter_envelope_on(&message.lpenv) || is_filter_envelope_on(&message.hpenv) || is_filter_envelope_on(&message.bpenv) {
|
||||||
|
for f in &filters {
|
||||||
|
apply_filter_adsr(f, &message, &f.type_(), t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let t = audio_context.current_time();
|
||||||
|
|
||||||
|
// Create filepaths
|
||||||
|
let message_url = &message.sampleurl.clone();
|
||||||
|
let (url, file_path, file_path_clone) = create_filepath_generic(message_url, &message.dirname);
|
||||||
|
|
||||||
|
// Download file if it doesn't exist
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if tokio::fs::metadata(&file_path.clone()).await.is_err() {
|
||||||
|
let response = reqwest::get(url)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to send GET request"));
|
||||||
|
|
||||||
|
let bytes = response.bytes().await.unwrap();
|
||||||
|
let path = Path::new(&file_path);
|
||||||
|
let mut file = create_file_and_dirs(path).await;
|
||||||
|
file.write_all(&bytes)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to write to file"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Play the buffer, if exists
|
||||||
|
if let Some(audio_buffer) = cache.get(&file_path_clone) {
|
||||||
|
let audio_buffer_duration = audio_buffer.duration();
|
||||||
|
let mut sampler = Sampler::new(&mut audio_context, audio_buffer);
|
||||||
|
|
||||||
|
// Playback speed
|
||||||
|
match message.unit {
|
||||||
|
Some(_) => {
|
||||||
|
sampler.sample.playback_rate().set_value(message.speed * audio_buffer_duration as f32 * 1.0);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
sampler.sample.playback_rate().set_value(message.speed * message.playbackrate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADSR
|
||||||
|
sampler.set_adsr(t, &message.adsr, message.velocity, message.duration);
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
let filters = sampler.set_filters(&mut audio_context, &message);
|
||||||
|
if is_filter_envelope_on(&message.lpenv) || is_filter_envelope_on(&message.hpenv) || is_filter_envelope_on(&message.bpenv) {
|
||||||
|
for f in &filters {
|
||||||
|
apply_filter_adsr(f, &message, &f.type_(), t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay
|
||||||
|
if message.delay.wet.is_some() {
|
||||||
|
apply_delay(&mut audio_context, &mut delays, &compressor, &message, t, &mut sampler);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reverb
|
||||||
|
if message.reverb.room.is_some() && message.reverb.room.unwrap() > 0.0 {
|
||||||
|
apply_reverb(&audio_context, &compressor, &ir_cache, &message, &mut sampler, &mut reverbs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to output
|
||||||
|
sampler.envelope.connect(&compressor);
|
||||||
|
sampler.play(t, &message, message.adsr.release.unwrap_or(0.001));
|
||||||
|
|
||||||
|
// Decode file, if it exists
|
||||||
|
} else if let Ok(file) = File::open(&file_path_clone) {
|
||||||
|
let cache_clone = cache.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let context = OfflineAudioContext::new(2, 88200, 44100.0);
|
||||||
|
let audio_buffer = context.decode_audio_data_sync(file).expect("Failed to decode file");
|
||||||
|
cache_clone.insert(file_path_clone.clone(), audio_buffer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn async_process_model(
|
||||||
|
mut input_receiver: mpsc::Receiver<Vec<WebAudioMessage>>,
|
||||||
|
output_transmitter: mpsc::Sender<Vec<WebAudioMessage>>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
while let Some(input) = input_receiver.recv().await {
|
||||||
|
let output = input;
|
||||||
|
output_transmitter.send(output).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct MessageFromJS {
|
||||||
|
note: f32,
|
||||||
|
offset: f64,
|
||||||
|
waveform: String,
|
||||||
|
lpf: (f32, f32),
|
||||||
|
hpf: (f32, f32),
|
||||||
|
bpf: (f32, f32),
|
||||||
|
duration: f64,
|
||||||
|
velocity: f32,
|
||||||
|
delay: (Option<f32>, Option<f32>, Option<f32>),
|
||||||
|
reverb: (Option<f32>, Option<f32>, Option<f32>, Option<f32>, Option<f32>, Option<String>, Option<String>),
|
||||||
|
orbit: usize,
|
||||||
|
speed: f32,
|
||||||
|
begin: f64,
|
||||||
|
end: f64,
|
||||||
|
looper: (u8, f64, f64),
|
||||||
|
adsr: (Option<f64>, Option<f64>, Option<f32>, Option<f64>),
|
||||||
|
lpenv: (Option<f64>, Option<f64>, Option<f64>, Option<f64>, Option<f64>),
|
||||||
|
hpenv: (Option<f64>, Option<f64>, Option<f64>, Option<f64>, Option<f64>),
|
||||||
|
bpenv: (Option<f64>, Option<f64>, Option<f64>, Option<f64>, Option<f64>),
|
||||||
|
n: usize,
|
||||||
|
sampleurl: String,
|
||||||
|
dirname: String,
|
||||||
|
unit: Option<String>,
|
||||||
|
playbackrate: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from JS
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn sendwebaudio(
|
||||||
|
messagesfromjs: Vec<MessageFromJS>,
|
||||||
|
state: tauri::State<'_, AsyncInputTransmitWebAudio>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let async_proc_input_tx = state.inner.lock().await;
|
||||||
|
let mut messages_to_process: Vec<WebAudioMessage> = Vec::new();
|
||||||
|
|
||||||
|
for m in messagesfromjs {
|
||||||
|
let message_to_process = WebAudioMessage {
|
||||||
|
note: m.note,
|
||||||
|
instant: Instant::now(),
|
||||||
|
offset: m.offset,
|
||||||
|
waveform: m.waveform,
|
||||||
|
lpf: LPFMessage {
|
||||||
|
frequency: m.lpf.0,
|
||||||
|
resonance: m.lpf.1,
|
||||||
|
},
|
||||||
|
hpf: HPFMessage {
|
||||||
|
frequency: m.hpf.0,
|
||||||
|
resonance: m.hpf.1,
|
||||||
|
},
|
||||||
|
bpf: BPFMessage {
|
||||||
|
frequency: m.bpf.0,
|
||||||
|
resonance: m.bpf.1,
|
||||||
|
},
|
||||||
|
duration: m.duration,
|
||||||
|
velocity: m.velocity,
|
||||||
|
delay: DelayMessage {
|
||||||
|
wet: m.delay.0,
|
||||||
|
delay_time: m.delay.1,
|
||||||
|
feedback: m.delay.2,
|
||||||
|
},
|
||||||
|
reverb: ReverbMessage {
|
||||||
|
room: m.reverb.0,
|
||||||
|
size: m.reverb.1,
|
||||||
|
roomfade: m.reverb.2,
|
||||||
|
roomlp: m.reverb.3,
|
||||||
|
roomdim: m.reverb.4,
|
||||||
|
ir: m.reverb.5,
|
||||||
|
url: m.reverb.6,
|
||||||
|
},
|
||||||
|
orbit: m.orbit,
|
||||||
|
speed: m.speed,
|
||||||
|
begin: m.begin,
|
||||||
|
end: m.end,
|
||||||
|
looper: LoopMessage {
|
||||||
|
is_loop: m.looper.0,
|
||||||
|
loop_start: m.looper.1,
|
||||||
|
loop_end: m.looper.2,
|
||||||
|
},
|
||||||
|
adsr: ADSRMessage {
|
||||||
|
attack: m.adsr.0,
|
||||||
|
decay: m.adsr.1,
|
||||||
|
sustain: m.adsr.2,
|
||||||
|
release: m.adsr.3,
|
||||||
|
},
|
||||||
|
lpenv: FilterADSRMessage {
|
||||||
|
attack: m.lpenv.0,
|
||||||
|
decay: m.lpenv.1,
|
||||||
|
sustain: m.lpenv.2,
|
||||||
|
release: m.lpenv.3,
|
||||||
|
env: m.lpenv.4,
|
||||||
|
},
|
||||||
|
hpenv: FilterADSRMessage {
|
||||||
|
attack: m.hpenv.0,
|
||||||
|
decay: m.hpenv.1,
|
||||||
|
sustain: m.hpenv.2,
|
||||||
|
release: m.hpenv.3,
|
||||||
|
env: m.hpenv.4,
|
||||||
|
},
|
||||||
|
bpenv: FilterADSRMessage {
|
||||||
|
attack: m.bpenv.0,
|
||||||
|
decay: m.bpenv.1,
|
||||||
|
sustain: m.bpenv.2,
|
||||||
|
release: m.bpenv.3,
|
||||||
|
env: m.bpenv.4,
|
||||||
|
},
|
||||||
|
n: m.n,
|
||||||
|
sampleurl: m.sampleurl,
|
||||||
|
dirname: m.dirname,
|
||||||
|
unit: m.unit,
|
||||||
|
playbackrate: m.playbackrate,
|
||||||
|
};
|
||||||
|
messages_to_process.push(message_to_process);
|
||||||
|
}
|
||||||
|
|
||||||
|
async_proc_input_tx.send(messages_to_process).await.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core';
|
import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core';
|
||||||
import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react';
|
import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react';
|
||||||
import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio';
|
import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||||
|
import { webaudioDesktopOutput } from '@strudel/desktopbridge';
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from '@supabase/supabase-js';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import React, { createContext, useCallback, useEffect, useState, useMemo } from 'react';
|
import React, { createContext, useCallback, useEffect, useState, useMemo } from 'react';
|
||||||
@@ -132,7 +133,7 @@ export function Repl({ embedded = false }) {
|
|||||||
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
|
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
|
||||||
useStrudel({
|
useStrudel({
|
||||||
initialCode: '// LOADING...',
|
initialCode: '// LOADING...',
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: isTauri() ? webaudioDesktopOutput : webaudioOutput,
|
||||||
getTime,
|
getTime,
|
||||||
beforeEval: async () => {
|
beforeEval: async () => {
|
||||||
setPending(true);
|
setPending(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user