add save-to-WAV button using OfflineAudioContext#43
Conversation
Renders the current sequence with OfflineAudioContext, encodes the result as a 16-bit PCM WAV, and downloads it. WAV is simpler than WebCodecs for uncompressed export and has no dependencies.
There was a problem hiding this comment.
Pull request overview
Adds offline audio rendering and WAV download support so users can export the current sequence as a 16‑bit PCM .wav file without extra dependencies.
Changes:
- Introduces
renderSequenceToWav(OfflineAudioContext render) and WAV encoding + download helpers. - Adds a “SAVE” button to the toolbar with loading/disabled state.
- Threads
totalSteps/totalBeatsthroughBoard→Toolbarto compute export timing.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| src/utils/exportWav.js | Implements offline render, WAV encoding, and blob download helper. |
| src/components/Toolbar.jsx | Adds SAVE button and export handler using the new utility. |
| src/components/Toolbar.css | Styles SAVE button and disabled state. |
| src/Board.jsx | Passes timing parameters needed for export. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const decoded = {} | ||
| await Promise.all( | ||
| uniquePaths.map(async (path) => { | ||
| const res = await fetch(path) |
There was a problem hiding this comment.
fetch(path) isn't checked for res.ok before decoding. If a sound file is missing or returns a non-audio response (404 HTML, etc.), decodeAudioData will fail with a less clear error. Consider throwing a descriptive error when !res.ok (including path and res.status).
| const res = await fetch(path) | |
| const res = await fetch(path) | |
| if (!res.ok) { | |
| throw new Error(`Failed to fetch audio file "${path}": ${res.status} ${res.statusText}`) | |
| } |
| const offlineCtx = new OfflineAudioContext( | ||
| numChannels, | ||
| Math.ceil(durationSeconds * sampleRate), | ||
| sampleRate | ||
| ) |
There was a problem hiding this comment.
OfflineAudioContext is referenced as a global constructor here, but the codebase already uses vendor-prefixed AudioContext constructors for Safari compatibility. Consider selecting the constructor via window.OfflineAudioContext || window.webkitOfflineAudioContext (and failing with a clear error if unavailable) so export works in the same set of supported browsers.
| uniquePaths.map(async (path) => { | ||
| const res = await fetch(path) | ||
| const arrayBuffer = await res.arrayBuffer() | ||
| decoded[path] = await offlineCtx.decodeAudioData(arrayBuffer) | ||
| }) |
There was a problem hiding this comment.
This uses the promise-returning form of decodeAudioData, but elsewhere in the codebase audio decoding is wrapped with the callback form (likely for Safari/WebKit compatibility). To avoid export breaking on browsers where decodeAudioData doesn't return a promise, use a small wrapper (or reuse the existing pattern) that supports the callback signature.
| for (let i = 0; i < buffer.length; i++) { | ||
| for (let c = 0; c < numChannels; c++) { | ||
| const s = Math.max(-1, Math.min(1, channels[c][i])) | ||
| view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true) |
There was a problem hiding this comment.
DataView#setInt16 will coerce the float value here, which can truncate inconsistently across engines. Convert to a proper int (e.g., Math.round(...)) before writing so PCM values are stable and match expected WAV encoding.
| view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true) | |
| const pcmSample = Math.round(s < 0 ? s * 0x8000 : s * 0x7fff) | |
| view.setInt16(offset, pcmSample, true) |
| totalBeats, | ||
| }); | ||
| const safeName = String(sequenceTitle).replace(/\s+/g, "_").toLowerCase(); | ||
| downloadBlob(blob, `${safeName}_${BPM}bpm.wav`); |
There was a problem hiding this comment.
If renderSequenceToWav/fetch/decode fails, the error will bubble out of the click handler and show as an unhandled error in the console with no user feedback. Consider adding a catch branch to surface a message (and/or log) so failures are handled gracefully while still resetting isSaving.
| downloadBlob(blob, `${safeName}_${BPM}bpm.wav`); | |
| downloadBlob(blob, `${safeName}_${BPM}bpm.wav`); | |
| } catch (error) { | |
| console.error("Failed to save track.", error); | |
| alert("Unable to save the track right now. Please try again."); |
cf6a8b7 to
f1271df
Compare
Renders the current sequence with OfflineAudioContext, encodes the
result as a 16-bit PCM WAV, and downloads it. WAV is simpler than
WebCodecs for uncompressed export and has no dependencies.