mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-21 20:55:12 -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']],
|
||||
/**
|
||||
* 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}.
|
||||
*
|
||||
|
||||
@@ -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 './utils.mjs';
|
||||
export * from './oscbridge.mjs';
|
||||
export * from './webaudiobridge.mjs';
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strudel.cycles/webaudio": "^0.9.0",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@tauri-apps/api": "^1.4.0"
|
||||
},
|
||||
|
||||
@@ -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':
|
||||
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
|
||||
@@ -1426,6 +1429,7 @@ packages:
|
||||
/@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
@@ -1441,6 +1445,7 @@ packages:
|
||||
/@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
@@ -1454,6 +1459,7 @@ packages:
|
||||
/@babel/plugin-proposal-class-static-block@7.20.7(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.12.0
|
||||
dependencies:
|
||||
@@ -1468,6 +1474,7 @@ packages:
|
||||
/@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
@@ -1479,6 +1486,7 @@ packages:
|
||||
/@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
@@ -1490,6 +1498,7 @@ packages:
|
||||
/@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
@@ -1501,6 +1510,7 @@ packages:
|
||||
/@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
@@ -1512,6 +1522,7 @@ packages:
|
||||
/@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
@@ -1523,6 +1534,7 @@ packages:
|
||||
/@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
@@ -1534,6 +1546,7 @@ packages:
|
||||
/@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
@@ -1548,6 +1561,7 @@ packages:
|
||||
/@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
@@ -1559,6 +1573,7 @@ packages:
|
||||
/@babel/plugin-proposal-optional-chaining@7.20.7(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
@@ -1571,6 +1586,7 @@ packages:
|
||||
/@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
@@ -1584,6 +1600,7 @@ packages:
|
||||
/@babel/plugin-proposal-private-property-in-object@7.20.5(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
@@ -1599,6 +1616,7 @@ packages:
|
||||
/@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.21.5):
|
||||
resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==}
|
||||
engines: {node: '>=4'}
|
||||
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
|
||||
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"
|
||||
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"
|
||||
|
||||
[features]
|
||||
# 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]
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
lto = "fat"
|
||||
opt-level = "s"
|
||||
|
||||
+38
-24
@@ -4,12 +4,20 @@
|
||||
mod midibridge;
|
||||
mod oscbridge;
|
||||
mod loggerbridge;
|
||||
mod webaudiobridge;
|
||||
// mod superdough;
|
||||
// mod reverbgen;
|
||||
mod webaudio;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use loggerbridge::Logger;
|
||||
use tauri::Manager;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::{
|
||||
sync::mpsc,
|
||||
sync::Mutex
|
||||
};
|
||||
|
||||
// the payload type must implement `Serialize` and `Clone`.
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct Payload {
|
||||
@@ -21,27 +29,33 @@ fn main() {
|
||||
let (async_output_transmitter_midi, async_output_receiver_midi) = mpsc::channel(1);
|
||||
let (async_input_transmitter_osc, async_input_receiver_osc) = mpsc::channel(1);
|
||||
let (async_output_transmitter_osc, async_output_receiver_osc) = mpsc::channel(1);
|
||||
let (async_input_transmitter_webaudio, async_input_receiver_webaudio) = mpsc::channel(1);
|
||||
let (async_output_transmitter_webaudio, async_output_receiver_webaudio) = mpsc::channel(1);
|
||||
tauri::Builder
|
||||
::default()
|
||||
.manage(midibridge::AsyncInputTransmit {
|
||||
inner: Mutex::new(async_input_transmitter_midi),
|
||||
})
|
||||
.manage(oscbridge::AsyncInputTransmit {
|
||||
inner: Mutex::new(async_input_transmitter_osc),
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc])
|
||||
.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);
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
::default()
|
||||
.manage(midibridge::AsyncInputTransmit {
|
||||
inner: Mutex::new(async_input_transmitter_midi),
|
||||
})
|
||||
.manage(oscbridge::AsyncInputTransmit {
|
||||
inner: Mutex::new(async_input_transmitter_osc),
|
||||
})
|
||||
.manage(webaudiobridge::AsyncInputTransmitWebAudio {
|
||||
inner: Mutex::new(async_input_transmitter_webaudio),
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc, webaudiobridge::sendwebaudio])
|
||||
.setup(|app| {
|
||||
let window = Arc::new(app.get_window("main").unwrap());
|
||||
let logger = Logger { window };
|
||||
midibridge::init(
|
||||
logger.clone(),
|
||||
async_input_receiver_midi,
|
||||
async_output_receiver_midi,
|
||||
async_output_transmitter_midi
|
||||
);
|
||||
oscbridge::init(logger, async_input_receiver_osc, async_output_receiver_osc, async_output_transmitter_osc);
|
||||
webaudiobridge::init(async_input_receiver_webaudio, async_output_receiver_webaudio, async_output_transmitter_webaudio);
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
@@ -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 { 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';
|
||||
@@ -132,7 +133,7 @@ export function Repl({ embedded = false }) {
|
||||
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
|
||||
useStrudel({
|
||||
initialCode: '// LOADING...',
|
||||
defaultOutput: webaudioOutput,
|
||||
defaultOutput: isTauri() ? webaudioDesktopOutput : webaudioOutput,
|
||||
getTime,
|
||||
beforeEval: async () => {
|
||||
setPending(true);
|
||||
|
||||
Reference in New Issue
Block a user