Add .configure() on web release

This commit is contained in:
Geraint 2025-02-10 16:48:11 +00:00
parent eaf484a9f7
commit 49b2f89ae6
7 changed files with 135 additions and 52 deletions

View File

@ -109,12 +109,12 @@
<label>tonality limit</label> <label>tonality limit</label>
<input type="range" min="2000" max="20000" step="100" data-key="tonalityHz" class="diagram-red"> <input type="range" min="2000" max="20000" step="100" data-key="tonalityHz" class="diagram-red">
<input type="number" min="2000" max="20000" step="1" data-key="tonalityHz" class="diagram-red"> <input type="number" min="2000" max="20000" step="1" data-key="tonalityHz" class="diagram-red">
<label>shelf freq</label> <label>block (ms)</label>
<input type="range" min="4000" max="12000" step="100" data-key="shelfFreq"> <input type="range" min="50" max="180" step="1" data-key="blockMs" class="diagram-green">
<input type="number" min="4000" max="12000" step="100" data-key="shelfFreq"> <input type="number" min="50" max="180" step="1" data-key="blockMs" class="diagram-green">
<label>shelf dB</label> <label>overlap</label>
<input type="range" min="-24" max="12" step="0.1" data-key="shelfDb"> <input type="range" min="2" max="8" step="0.1" data-key="overlap" class="diagram-green">
<input type="number" min="-24" max="12" step="0.1" data-key="shelfDb"> <input type="number" min="2" max="8" step="0.1" data-key="overlap" class="diagram-green">
</div> </div>
<script type="module"> <script type="module">
import SignalsmithStretch from "../release/SignalsmithStretch.mjs"; import SignalsmithStretch from "../release/SignalsmithStretch.mjs";
@ -126,7 +126,22 @@
let audioContext = new AudioContext(); let audioContext = new AudioContext();
let stretch; let stretch;
let audioDuration = 1; let audioDuration = 1;
let controlValuesInitial = {
active: false,
rate: 1,
semitones: 0,
loopStart: 0,
loopEnd: 0 // disabled (<= start), but this gets set when we load an audio file
};
let controlValues = Object.assign({}, controlValuesInitial);
let configValuesInitial = {
tonalityHz: 8000,
blockMs: 120,
overlap: 4
};
let configValues = Object.assign({}, configValuesInitial);
// add scope, for fun // add scope, for fun
let scope = await Scope(audioContext); let scope = await Scope(audioContext);
scope.connect(audioContext.destination); scope.connect(audioContext.destination);
@ -134,9 +149,6 @@
scopeFrame.id = 'scope'; scopeFrame.id = 'scope';
document.body.appendChild(scopeFrame); document.body.appendChild(scopeFrame);
let filter = audioContext.createBiquadFilter();
filter.connect(scope);
// Drop zone // Drop zone
document.body.ondragover = event => { document.body.ondragover = event => {
event.preventDefault(); event.preventDefault();
@ -169,9 +181,10 @@
stretch.disconnect(); stretch.disconnect();
} }
stretch = await SignalsmithStretch(audioContext); stretch = await SignalsmithStretch(audioContext);
stretch.connect(filter); stretch.connect(scope);
await stretch.addBuffers(channelBuffers); await stretch.addBuffers(channelBuffers);
controlValues.loopEnd = audioDuration; controlValues.loopEnd = audioDuration;
configChanged();
controlsChanged(); controlsChanged();
} }
@ -179,49 +192,75 @@
let response = await fetch('loop.mp3'); let response = await fetch('loop.mp3');
handleArrayBuffer(await response.arrayBuffer()); handleArrayBuffer(await response.arrayBuffer());
let controlValuesInitial = {
active: false,
rate: 1,
semitones: 0,
tonalityHz: 8000,
shelfFreq: 8000,
shelfDb: 0
};
let controlValues = Object.assign({}, controlValuesInitial);
$('#playstop').onclick = e => { $('#playstop').onclick = e => {
controlValues.active = !controlValues.active; controlValues.active = !controlValues.active;
controlsChanged(0.15); // play state schedules slightly in the future because of latency, otherwise it ends up fading in to jump ahead. controlsChanged(0.15);
}; };
$$('#controls input').forEach(input => { $$('#controls input').forEach(input => {
let isCheckbox = input.type == 'checkbox'; let isCheckbox = input.type == 'checkbox';
let key = input.dataset.key; let key = input.dataset.key;
input.oninput = input.onchange = e => { input.oninput = input.onchange = e => {
controlValues[key] = isCheckbox ? input.checked : parseFloat(input.value); let value = isCheckbox ? input.checked : parseFloat(input.value);
controlsChanged(); if (key in controlValues) {
controlValues[key] = value;
controlsChanged();
} else if (key in configValues) {
configValues[key] = value;
configChanged();
}
}; };
if (!isCheckbox) input.ondblclick = e => { if (!isCheckbox) input.ondblclick = e => {
controlValues[key] = controlValuesInitial[key]; if (key in controlValues) {
controlsChanged(); controlValues[key] = controlValuesInitial[key];
controlsChanged();
} else if (key in configValues) {
configValues[key] = configValuesInitial[key];
configChanged();
}
}; };
}); });
function controlsChanged(scheduleAhead) { function controlsChanged(scheduleAhead) {
let playing = controlValues.active; $('#playstop').innerHTML = '<svg alt="toggle play" height="1em" width="1em" viewbox="0 0 8 8" style="vertical-align:middle"><path d="' + (controlValues.active ? 'M1 1L3 1 3 7 1 7ZM5 1 7 1 7 7 5 7Z' : 'M1 0L8 4 1 8') + '" fill="currentColor"/></svg>';
$('#playstop').innerHTML = '<svg alt="toggle play" height="1em" width="1em" viewbox="0 0 8 8" style="vertical-align:middle"><path d="' + (playing ? 'M1 1L3 1 3 7 1 7ZM5 1 7 1 7 7 5 7Z' : 'M1 0L8 4 1 8') + '" fill="currentColor"/></svg>';
$$('#controls input').forEach(input => { $$('#controls input').forEach(input => {
let key = input.dataset.key; let key = input.dataset.key;
let value = controlValues[key]; if (key in controlValues) {
// Update value if it doesn't match let value = controlValues[key];
if (value !== parseFloat(input.value)) input.value = value; // Update value if it doesn't match
if (value !== parseFloat(input.value)) input.value = value;
}
}); });
if (stretch) { if (stretch) {
let obj = Object.assign({output: audioContext.currentTime + (scheduleAhead || 0)}, controlValues); let obj = Object.assign({output: audioContext.currentTime + (scheduleAhead || 0)}, controlValues);
stretch.schedule(obj); stretch.schedule(obj);
} }
filter.type = 'highshelf'; // https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode/type audioContext.resume();
filter.Q.value = 0.71; }
filter.frequency.value = controlValues.shelfFreq; controlsChanged();
filter.gain.value = controlValues.shelfDb; let configTimeout = null;
function configChanged() {
$$('#controls input').forEach(input => {
let key = input.dataset.key;
if (key in configValues) {
let value = configValues[key];
// Update value if it doesn't match
if (value !== parseFloat(input.value)) input.value = value;
}
});
if (configTimeout == null) {
configTimeout = setTimeout(_ => {
configTimeout = null;
if (stretch) {
stretch.configure({
tonalityHz: configValues.tonalityHz,
blockMs: configValues.blockMs,
intervalMs: configValues.blockMs/configValues.overlap
});
}
}, 50);
}
audioContext.resume(); audioContext.resume();
} }
controlsChanged(); controlsChanged();

View File

@ -49,5 +49,5 @@ em++ \
-sINITIAL_MEMORY=512kb -sALLOW_MEMORY_GROWTH=1 -sMEMORY_GROWTH_GEOMETRIC_STEP=0.5 -sABORTING_MALLOC=1 \ -sINITIAL_MEMORY=512kb -sALLOW_MEMORY_GROWTH=1 -sMEMORY_GROWTH_GEOMETRIC_STEP=0.5 -sABORTING_MALLOC=1 \
-sSTRICT=1 -sDYNAMIC_EXECUTION=0 -sSTRICT=1 -sDYNAMIC_EXECUTION=0
# Remove last 4 lines (UMD definition) # Remove UMD definition
node -e "let f=process.argv[1],fs=require('fs');fs.writeFileSync(f,fs.readFileSync(f,'utf8').split('\n').slice(0,-5).join('\n')+'\n')" "${OUTPUT_JS}" node -e "let f=process.argv[1],fs=require('fs');fs.writeFileSync(f,fs.readFileSync(f,'utf8').split(\"if (typeof exports === 'object' && typeof module === 'object') {\")[0])" "${OUTPUT_JS}"

View File

@ -16,6 +16,8 @@ extern "C" {
Sample * EMSCRIPTEN_KEEPALIVE setBuffers(int channels, int length) { Sample * EMSCRIPTEN_KEEPALIVE setBuffers(int channels, int length) {
buffers.resize(length*channels*2); buffers.resize(length*channels*2);
Sample *data = buffers.data(); Sample *data = buffers.data();
buffersIn.resize(0);
buffersOut.resize(0);
for (int c = 0; c < channels; ++c) { for (int c = 0; c < channels; ++c) {
buffersIn.push_back(data + length*c); buffersIn.push_back(data + length*c);
buffersOut.push_back(data + length*(c + channels)); buffersOut.push_back(data + length*(c + channels));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -26,6 +26,11 @@ function registerWorkletProcessor(Module, audioNodeKey) {
}]; }];
let remoteMethods = { let remoteMethods = {
configure: config => {
let blockChanged = (config.blockMs != this.config.blockMs || config.intervalMs != this.config.intervalMs);
Object.assign(this.config, config);
if (config.blockMs && blockChanged) this.configure();
},
setUpdateInterval: seconds => { setUpdateInterval: seconds => {
this.timeIntervalSamples = sampleRate*seconds; this.timeIntervalSamples = sampleRate*seconds;
}, },
@ -170,8 +175,18 @@ function registerWorkletProcessor(Module, audioNodeKey) {
}); });
} }
config = {
tonalityHz: 8000
};
configure() { configure() {
this.wasmModule._presetDefault(this.channels, sampleRate); if (this.config.blockMs) {
let blockSamples = Math.round(this.config.blockMs/1000*sampleRate);
let intervalSamples = Math.round((this.config.intervalMs || this.config.blockMs*0.25)/1000*sampleRate);
this.wasmModule._configure(this.channels, blockSamples, intervalSamples);
this.wasmModule._reset();
} else {
this.wasmModule._presetDefault(this.channels, sampleRate);
}
this.updateBuffers(); this.updateBuffers();
this.inputLatencySeconds = this.wasmModule._inputLatency()/sampleRate; this.inputLatencySeconds = this.wasmModule._inputLatency()/sampleRate;
this.outputLatencySeconds = this.wasmModule._outputLatency()/sampleRate; this.outputLatencySeconds = this.wasmModule._outputLatency()/sampleRate;
@ -210,7 +225,7 @@ function registerWorkletProcessor(Module, audioNodeKey) {
let currentMapSegment = this.timeMap[0]; let currentMapSegment = this.timeMap[0];
let wasmModule = this.wasmModule; let wasmModule = this.wasmModule;
wasmModule._setTransposeSemitones(currentMapSegment.semitones, 8000/sampleRate) wasmModule._setTransposeSemitones(currentMapSegment.semitones, this.config.tonalityHz/sampleRate)
// Check the input/output channel counts // Check the input/output channel counts
if (outputList[0].length != this.channels) { if (outputList[0].length != this.channels) {