259 lines
8.0 KiB
HTML
259 lines
8.0 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Signalsmith Stretch Web Audio demo</title>
|
|
<link rel="stylesheet" href="/style/article/dist.css">
|
|
<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;
|
|
padding: 0;
|
|
position: fixed;
|
|
height: 100vh;
|
|
width: 100vw;
|
|
max-height: 100vh;
|
|
max-width: 100vw;
|
|
display: grid;
|
|
grid-template-areas: "playstop playback upload" "controls controls controls" "scope scope scope";
|
|
grid-template-columns: 3rem 1fr max-content;
|
|
grid-template-rows: 2.5rem 1fr 8rem;
|
|
|
|
font-size: 12pt;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
|
|
#controls {
|
|
grid-area: controls;
|
|
display: grid;
|
|
grid-template-columns: max-content 1fr max-content;
|
|
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%;
|
|
border: none;
|
|
}
|
|
#playstop {
|
|
grid-area: playstop;
|
|
}
|
|
#playback {
|
|
grid-area: playback;
|
|
height: 100%;
|
|
position: relative;
|
|
padding: 0;
|
|
margin: 0 1rex;
|
|
accent-color: currentcolor;
|
|
}
|
|
#playback::before {
|
|
content: '';
|
|
background: #DDD;
|
|
box-shadow: rgba(0, 0, 0, 0.267) 0px -8px 3px -8px inset, rgba(255, 255, 255, 0.2) 0px 6px 2px -4px inset;
|
|
position: absolute;
|
|
left: -1rem;
|
|
right: -1rem;
|
|
height: 100%;
|
|
z-index: -1;
|
|
}
|
|
#upload {
|
|
grid-area: upload;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<button id="playstop" alt="toggle play">...</button>
|
|
<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>rate</label>
|
|
<input type="range" min="0" max="4" step="0.01" data-key="rate" class="diagram-blue">
|
|
<input type="number" min="0" max="4" step="0.01" data-key="rate" class="diagram-blue">
|
|
<label>semitones</label>
|
|
<input type="range" min="-12" max="12" step="1" data-key="semitones" class="diagram-red">
|
|
<input type="number" min="-12" max="12" step="1" data-key="semitones" class="diagram-red">
|
|
<!--
|
|
<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">
|
|
-->
|
|
</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,
|
|
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.
|
|
};
|
|
$$('#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(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>';
|
|
|
|
$$('#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 + (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();
|
|
|
|
$('#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);
|
|
let playbackHeld = false;
|
|
function updatePlaybackPosition(e) {
|
|
let inputTime = parseFloat(playbackPosition.value);
|
|
let obj = Object.assign({}, controlValues);
|
|
if (playbackHeld) obj.rate = 0;
|
|
stretch.schedule(Object.assign({input: inputTime}, obj));
|
|
}
|
|
playbackPosition.onmousedown = e => {
|
|
playbackHeld = true;
|
|
};
|
|
playbackPosition.onmouseup = playbackPosition.onmousecancel = e => {
|
|
playbackHeld = false;
|
|
};
|
|
playbackPosition.oninput = playbackPosition.onchange = updatePlaybackPosition;
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|