2024-10-22 12:24:55 +01:00

222 lines
6.4 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Signalsmith Stretch Web Audio demo</title>
<style>
#start-overlay {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
z-index: 100;
}
button {
justify-content: center;
align-items: center;
flex-grow: 1;
font: inherit;
}
body {
margin: 0;
height: 100vh;
width: 100vw;
max-height: 100vh;
max-width: 100vw;
display: grid;
grid-template-areas: "playback upload" "controls controls" "scope scope";
grid-template-columns: 1fr 4rem;
grid-template-rows: 2.5rem 1fr calc(min(30vh, 8rem));
font-size: 12pt;
font-family: Arial, sans-serif;
}
#controls {
grid-area: controls;
display: grid;
grid-template-columns: 4rem 1fr 5rem;
grid-auto-rows: max-content;
padding: 1rem;
align-content: space-evenly;
}
#controls label {
grid-column: 1;
text-align: right;
padding-right: 1ex;
}
#controls input[type=range], #controls input[type=checkbox] {
grid-column: 2;
}
#controls .input[type=number] {
grid-column: 3;
}
#scope {
grid-area: scope;
width: 100%;
height: 100%;
border: none;
}
#playback {
grid-area: playback;
background: #DDD;
}
#upload {
grid-area: upload;
background: #EEE;
border: 2px dashed #CCC;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<input id="playback" type="range" value="0" min="0" max="1" step="0.001">
<input id="upload-file" type="file" style="visibility: hidden" accept="audio/*">
<button id="upload">upload</button>
<div id="controls">
<label>active</label>
<input type="checkbox" data-key="active">
<label>rate</label>
<input type="range" min="0" max="4" step="0.01" data-key="rate">
<input type="number" min="0" max="4" step="0.01" data-key="rate">
<label>semitones</label>
<input type="range" min="-12" max="12" step="1" data-key="semitones">
<input type="number" min="-12" max="12" step="1" data-key="semitones">
<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">
</div>
<script type="module">
import SignalsmithStretch from "./release/SignalsmithStretch.mjs";
import Scope from './Scope.mjs';
let $ = document.querySelector.bind(document);
let $$ = document.querySelectorAll.bind(document);
(async () => {
let audioContext = new AudioContext();
let stretch;
let audioDuration = 1;
// add scope, for fun
let scope = await Scope(audioContext);
scope.connect(audioContext.destination);
let scopeFrame = scope.openInterface();
scopeFrame.id = 'scope';
document.body.appendChild(scopeFrame);
let filter = audioContext.createBiquadFilter();
filter.connect(scope);
// Drop zone
document.body.ondragover = event => {
event.preventDefault();
}
document.body.ondrop = handleDrop;
function handleDrop(event) {
event.preventDefault();
var dt = event.dataTransfer;
handleFile(dt.items ? dt.items[0].getAsFile() : dt.files[0]);
}
function handleFile(file) {
return new Promise((pass, fail) => {
var reader = new FileReader();
reader.onload = e => pass(handleArrayBuffer(reader.result));
reader.onerror = fail;
reader.readAsArrayBuffer(file);
});
}
async function handleArrayBuffer(arrayBuffer) {
let audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
audioDuration = audioBuffer.duration;
let channelBuffers = []
for (let c = 0; c < audioBuffer.numberOfChannels; ++c) {
channelBuffers.push(audioBuffer.getChannelData(c));
}
// fresh node
if (stretch) {
stretch.stop();
stretch.disconnect();
}
stretch = await SignalsmithStretch(audioContext);
stretch.connect(filter);
await stretch.addBuffers(channelBuffers);
controlValues.loopEnd = audioDuration;
controlsChanged();
}
// fetch audio and add buffer
let response = await fetch('loop.mp3');
handleArrayBuffer(await response.arrayBuffer());
let controlValuesInitial = {
active: false,
rate: 1,
semitones: 0,
shelfFreq: 8000,
shelfDb: 0
};
let controlValues = Object.assign({}, controlValuesInitial);
$$('#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();
};
if (!isCheckbox) input.ondblclick = e => {
controlValues[key] = controlValuesInitial[key];
controlsChanged();
};
});
function controlsChanged() {
$$('#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 (stretch) {
let obj = Object.assign({output: audioContext.currentTime + 0.15}, 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();
$('#upload').onclick = e => $('#upload-file').click();
$('#upload-file').onchange = async e => {
stretch.stop();
await handleFile($('#upload-file').files[0]).catch(e => alert(e.message));
if (stretch) {
controlValues.active = true;
controlsChanged();
}
}
let playbackPosition = $('#playback');
setInterval(_ => {
playbackPosition.max = audioDuration;
playbackPosition.value = stretch?.inputTime;
}, 100);
playbackPosition.oninput = playbackPosition.onchange = e => {
let inputTime = parseFloat(playbackPosition.value);
stretch.schedule(Object.assign({input: inputTime}, controlValues));
};
})();
</script>
</body>
</html>