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>
<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">
<label>shelf freq</label>
<input type="range" min="4000" max="12000" step="100" data-key="shelfFreq">
<input type="number" min="4000" max="12000" step="100" data-key="shelfFreq">
<label>shelf dB</label>
<input type="range" min="-24" max="12" step="0.1" data-key="shelfDb">
<input type="number" min="-24" max="12" step="0.1" data-key="shelfDb">
<label>block (ms)</label>
<input type="range" min="50" max="180" step="1" data-key="blockMs" class="diagram-green">
<input type="number" min="50" max="180" step="1" data-key="blockMs" class="diagram-green">
<label>overlap</label>
<input type="range" min="2" max="8" step="0.1" data-key="overlap" class="diagram-green">
<input type="number" min="2" max="8" step="0.1" data-key="overlap" class="diagram-green">
</div>
<script type="module">
import SignalsmithStretch from "../release/SignalsmithStretch.mjs";
@ -126,7 +126,22 @@
let audioContext = new AudioContext();
let stretch;
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
let scope = await Scope(audioContext);
scope.connect(audioContext.destination);
@ -134,9 +149,6 @@
scopeFrame.id = 'scope';
document.body.appendChild(scopeFrame);
let filter = audioContext.createBiquadFilter();
filter.connect(scope);
// Drop zone
document.body.ondragover = event => {
event.preventDefault();
@ -169,9 +181,10 @@
stretch.disconnect();
}
stretch = await SignalsmithStretch(audioContext);
stretch.connect(filter);
stretch.connect(scope);
await stretch.addBuffers(channelBuffers);
controlValues.loopEnd = audioDuration;
configChanged();
controlsChanged();
}
@ -179,49 +192,75 @@
let response = await fetch('loop.mp3');
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 => {
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 => {
let isCheckbox = input.type == 'checkbox';
let key = input.dataset.key;
input.oninput = input.onchange = e => {
controlValues[key] = isCheckbox ? input.checked : parseFloat(input.value);
controlsChanged();
let value = isCheckbox ? input.checked : parseFloat(input.value);
if (key in controlValues) {
controlValues[key] = value;
controlsChanged();
} else if (key in configValues) {
configValues[key] = value;
configChanged();
}
};
if (!isCheckbox) input.ondblclick = e => {
controlValues[key] = controlValuesInitial[key];
controlsChanged();
if (key in controlValues) {
controlValues[key] = controlValuesInitial[key];
controlsChanged();
} else if (key in configValues) {
configValues[key] = configValuesInitial[key];
configChanged();
}
};
});
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="' + (playing ? '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="' + (controlValues.active ? '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 => {
let key = input.dataset.key;
let value = controlValues[key];
// Update value if it doesn't match
if (value !== parseFloat(input.value)) input.value = value;
if (key in controlValues) {
let value = controlValues[key];
// Update value if it doesn't match
if (value !== parseFloat(input.value)) input.value = value;
}
});
if (stretch) {
let obj = Object.assign({output: audioContext.currentTime + (scheduleAhead || 0)}, controlValues);
stretch.schedule(obj);
}
filter.type = 'highshelf'; // https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode/type
filter.Q.value = 0.71;
filter.frequency.value = controlValues.shelfFreq;
filter.gain.value = controlValues.shelfDb;
audioContext.resume();
}
controlsChanged();
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();
}
controlsChanged();

View File

@ -49,5 +49,5 @@ em++ \
-sINITIAL_MEMORY=512kb -sALLOW_MEMORY_GROWTH=1 -sMEMORY_GROWTH_GEOMETRIC_STEP=0.5 -sABORTING_MALLOC=1 \
-sSTRICT=1 -sDYNAMIC_EXECUTION=0
# Remove last 4 lines (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}"
# Remove UMD definition
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) {
buffers.resize(length*channels*2);
Sample *data = buffers.data();
buffersIn.resize(0);
buffersOut.resize(0);
for (int c = 0; c < channels; ++c) {
buffersIn.push_back(data + length*c);
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 = {
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 => {
this.timeIntervalSamples = sampleRate*seconds;
},
@ -170,8 +175,18 @@ function registerWorkletProcessor(Module, audioNodeKey) {
});
}
config = {
tonalityHz: 8000
};
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.inputLatencySeconds = this.wasmModule._inputLatency()/sampleRate;
this.outputLatencySeconds = this.wasmModule._outputLatency()/sampleRate;
@ -210,7 +225,7 @@ function registerWorkletProcessor(Module, audioNodeKey) {
let currentMapSegment = this.timeMap[0];
let wasmModule = this.wasmModule;
wasmModule._setTransposeSemitones(currentMapSegment.semitones, 8000/sampleRate)
wasmModule._setTransposeSemitones(currentMapSegment.semitones, this.config.tonalityHz/sampleRate)
// Check the input/output channel counts
if (outputList[0].length != this.channels) {