Skip to content

Commit 9c20b8a

Browse files
committed
Add workbench reverse and zoom quick actions
1 parent e12314c commit 9c20b8a

1 file changed

Lines changed: 98 additions & 3 deletions

File tree

utilities/streaming-audio-workbench/streaming_audio_workbench.html

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@
500500
<article class="panel hero-copy">
501501
<div class="hero-title-row">
502502
<h1>Streaming Audio Workbench</h1>
503-
<span class="version-badge" id="versionLabel">26.04.17-121438</span>
503+
<span class="version-badge" id="versionLabel">26.04.23-213956</span>
504504
</div>
505505
<p>
506506
Live microphone analysis with persistent history, waveform plus Hilbert-envelope
@@ -672,6 +672,13 @@ <h1>Streaming Audio Workbench</h1>
672672
<button class="secondary" id="panRightButton">Pan right</button>
673673
<input id="panSlider" type="range" min="0" max="0" step="0.01" value="0" />
674674
</div>
675+
<div class="control">
676+
<label>Quick actions</label>
677+
<div class="action-row">
678+
<button class="secondary" id="reverseAudioButton">Reverse audio</button>
679+
<button class="secondary" id="viewToSelectionButton">View to selection</button>
680+
</div>
681+
</div>
675682
<div class="control">
676683
<label>Analysis and view</label>
677684
<div class="split-row">
@@ -782,7 +789,7 @@ <h2 class="plot-title">MFCC / CPP View</h2>
782789
</main>
783790

784791
<script>
785-
const buildVersion = "26.04.17-121438";
792+
const buildVersion = "26.04.23-213956";
786793

787794
const state = {
788795
audioContext: null,
@@ -832,6 +839,8 @@ <h2 class="plot-title">MFCC / CPP View</h2>
832839
resetButton: document.getElementById("resetButton"),
833840
saveAudioButton: document.getElementById("saveAudioButton"),
834841
screenshotButton: document.getElementById("screenshotButton"),
842+
reverseAudioButton: document.getElementById("reverseAudioButton"),
843+
viewToSelectionButton: document.getElementById("viewToSelectionButton"),
835844
analysisRateSelect: document.getElementById("analysisRateSelect"),
836845
spectrogramFrameRange: document.getElementById("spectrogramFrameRange"),
837846
spectrogramFrameOutput: document.getElementById("spectrogramFrameOutput"),
@@ -1032,10 +1041,42 @@ <h2 class="plot-title">MFCC / CPP View</h2>
10321041
updateSpectrogramFrameOutput();
10331042
}
10341043

1044+
function formatViewDurationLabel(durationSec) {
1045+
if (durationSec < 1) {
1046+
return `Selection (${Math.round(durationSec * 1000)} ms)`;
1047+
}
1048+
return `Selection (${durationSec.toFixed(durationSec < 10 ? 2 : 1)} s)`;
1049+
}
1050+
1051+
function syncViewDurationControlValue() {
1052+
const desiredValue = String(state.viewDurationSec);
1053+
let customOption = elements.viewDurationSelect.querySelector('option[data-custom-view="true"]');
1054+
const hasBuiltInMatch = Array.from(elements.viewDurationSelect.options).some(
1055+
(option) => option.value === desiredValue && option.dataset.customView !== "true",
1056+
);
1057+
1058+
if (hasBuiltInMatch) {
1059+
if (customOption) {
1060+
customOption.remove();
1061+
}
1062+
elements.viewDurationSelect.value = desiredValue;
1063+
return;
1064+
}
1065+
1066+
if (!customOption) {
1067+
customOption = document.createElement("option");
1068+
customOption.dataset.customView = "true";
1069+
elements.viewDurationSelect.appendChild(customOption);
1070+
}
1071+
customOption.value = desiredValue;
1072+
customOption.textContent = formatViewDurationLabel(state.viewDurationSec);
1073+
elements.viewDurationSelect.value = desiredValue;
1074+
}
1075+
10351076
function syncControlsToState() {
10361077
elements.analysisRateSelect.value = String(state.analysisRate);
10371078
elements.playbackModeSelect.value = state.playbackMode;
1038-
elements.viewDurationSelect.value = String(state.viewDurationSec);
1079+
syncViewDurationControlValue();
10391080
elements.spectrogramFrameRange.value = String(Math.round(state.spectrogramFrameSec * 1000));
10401081
elements.windowWidthRange.value = String(Math.round(state.selectionWidthSec * 1000));
10411082
elements.preemphasisToggle.checked = state.usePreemphasis;
@@ -1225,6 +1266,58 @@ <h2 class="plot-title">MFCC / CPP View</h2>
12251266
return mono;
12261267
}
12271268

1269+
async function reverseBufferedAudio() {
1270+
if (!state.totalSamples || !state.inputSampleRate) {
1271+
alert("There is no buffered audio to reverse yet.");
1272+
return;
1273+
}
1274+
1275+
await stopPlayback();
1276+
if (state.isStarted) {
1277+
await stopCapture();
1278+
}
1279+
1280+
const forward = extractSamples(0, state.totalSamples);
1281+
const reversed = new Float32Array(forward.length);
1282+
for (let index = 0; index < forward.length; index += 1) {
1283+
reversed[index] = forward[forward.length - 1 - index];
1284+
}
1285+
1286+
state.chunks = [reversed];
1287+
state.totalSamples = reversed.length;
1288+
state.viewportStartSec = 0.0;
1289+
state.playCursorSec = 0.0;
1290+
state.selectionCenterSec = state.selectionWidthSec / 2;
1291+
state.followLive = false;
1292+
elements.followLiveToggle.checked = false;
1293+
1294+
syncSelectionToViewport();
1295+
syncControlsToState();
1296+
updatePanSlider();
1297+
updateStatus();
1298+
scheduleRender();
1299+
}
1300+
1301+
function setViewToSelection() {
1302+
const totalDuration = totalDurationSec();
1303+
if (!totalDuration) {
1304+
alert("There is no buffered audio to zoom yet.");
1305+
return;
1306+
}
1307+
1308+
const selection = getSelectionBounds();
1309+
state.followLive = false;
1310+
elements.followLiveToggle.checked = false;
1311+
state.viewDurationSec = Math.max(0.015, selection.end - selection.start);
1312+
state.viewportStartSec = clamp(selection.start, 0, Math.max(0, totalDuration - state.viewDurationSec));
1313+
1314+
syncSelectionToViewport();
1315+
syncControlsToState();
1316+
updatePanSlider();
1317+
updateStatus();
1318+
scheduleRender();
1319+
}
1320+
12281321
async function stopPlayback() {
12291322
const source = state.playbackSource;
12301323
if (source) {
@@ -2918,6 +3011,8 @@ <h2 class="plot-title">MFCC / CPP View</h2>
29183011
elements.resetButton.addEventListener("click", resetEverything);
29193012
elements.saveAudioButton.addEventListener("click", exportBufferedAudioAsWav);
29203013
elements.screenshotButton.addEventListener("click", capturePageScreenshot);
3014+
elements.reverseAudioButton.addEventListener("click", reverseBufferedAudio);
3015+
elements.viewToSelectionButton.addEventListener("click", setViewToSelection);
29213016
elements.analysisRateSelect.addEventListener("change", (event) => {
29223017
state.analysisRate = event.target.value;
29233018
scheduleRender();

0 commit comments

Comments
 (0)