|
500 | 500 | <article class="panel hero-copy"> |
501 | 501 | <div class="hero-title-row"> |
502 | 502 | <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> |
504 | 504 | </div> |
505 | 505 | <p> |
506 | 506 | Live microphone analysis with persistent history, waveform plus Hilbert-envelope |
@@ -672,6 +672,13 @@ <h1>Streaming Audio Workbench</h1> |
672 | 672 | <button class="secondary" id="panRightButton">Pan right</button> |
673 | 673 | <input id="panSlider" type="range" min="0" max="0" step="0.01" value="0" /> |
674 | 674 | </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> |
675 | 682 | <div class="control"> |
676 | 683 | <label>Analysis and view</label> |
677 | 684 | <div class="split-row"> |
@@ -782,7 +789,7 @@ <h2 class="plot-title">MFCC / CPP View</h2> |
782 | 789 | </main> |
783 | 790 |
|
784 | 791 | <script> |
785 | | - const buildVersion = "26.04.17-121438"; |
| 792 | + const buildVersion = "26.04.23-213956"; |
786 | 793 |
|
787 | 794 | const state = { |
788 | 795 | audioContext: null, |
@@ -832,6 +839,8 @@ <h2 class="plot-title">MFCC / CPP View</h2> |
832 | 839 | resetButton: document.getElementById("resetButton"), |
833 | 840 | saveAudioButton: document.getElementById("saveAudioButton"), |
834 | 841 | screenshotButton: document.getElementById("screenshotButton"), |
| 842 | + reverseAudioButton: document.getElementById("reverseAudioButton"), |
| 843 | + viewToSelectionButton: document.getElementById("viewToSelectionButton"), |
835 | 844 | analysisRateSelect: document.getElementById("analysisRateSelect"), |
836 | 845 | spectrogramFrameRange: document.getElementById("spectrogramFrameRange"), |
837 | 846 | spectrogramFrameOutput: document.getElementById("spectrogramFrameOutput"), |
@@ -1032,10 +1041,42 @@ <h2 class="plot-title">MFCC / CPP View</h2> |
1032 | 1041 | updateSpectrogramFrameOutput(); |
1033 | 1042 | } |
1034 | 1043 |
|
| 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 | + |
1035 | 1076 | function syncControlsToState() { |
1036 | 1077 | elements.analysisRateSelect.value = String(state.analysisRate); |
1037 | 1078 | elements.playbackModeSelect.value = state.playbackMode; |
1038 | | - elements.viewDurationSelect.value = String(state.viewDurationSec); |
| 1079 | + syncViewDurationControlValue(); |
1039 | 1080 | elements.spectrogramFrameRange.value = String(Math.round(state.spectrogramFrameSec * 1000)); |
1040 | 1081 | elements.windowWidthRange.value = String(Math.round(state.selectionWidthSec * 1000)); |
1041 | 1082 | elements.preemphasisToggle.checked = state.usePreemphasis; |
@@ -1225,6 +1266,58 @@ <h2 class="plot-title">MFCC / CPP View</h2> |
1225 | 1266 | return mono; |
1226 | 1267 | } |
1227 | 1268 |
|
| 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 | + |
1228 | 1321 | async function stopPlayback() { |
1229 | 1322 | const source = state.playbackSource; |
1230 | 1323 | if (source) { |
@@ -2918,6 +3011,8 @@ <h2 class="plot-title">MFCC / CPP View</h2> |
2918 | 3011 | elements.resetButton.addEventListener("click", resetEverything); |
2919 | 3012 | elements.saveAudioButton.addEventListener("click", exportBufferedAudioAsWav); |
2920 | 3013 | elements.screenshotButton.addEventListener("click", capturePageScreenshot); |
| 3014 | + elements.reverseAudioButton.addEventListener("click", reverseBufferedAudio); |
| 3015 | + elements.viewToSelectionButton.addEventListener("click", setViewToSelection); |
2921 | 3016 | elements.analysisRateSelect.addEventListener("change", (event) => { |
2922 | 3017 | state.analysisRate = event.target.value; |
2923 | 3018 | scheduleRender(); |
|
0 commit comments