Skip to content

add save-to-WAV button using OfflineAudioContext#43

Open
joeshub wants to merge 1 commit intomasterfrom
claude/save-track-to-file-GAtTX
Open

add save-to-WAV button using OfflineAudioContext#43
joeshub wants to merge 1 commit intomasterfrom
claude/save-track-to-file-GAtTX

Conversation

@joeshub
Copy link
Copy Markdown
Collaborator

@joeshub joeshub commented Apr 30, 2026

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.

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.
Copilot AI review requested due to automatic review settings April 30, 2026 22:22
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/totalBeats through BoardToolbar to 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.

Comment thread src/utils/exportWav.js
const decoded = {}
await Promise.all(
uniquePaths.map(async (path) => {
const res = await fetch(path)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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}`)
}

Copilot uses AI. Check for mistakes.
Comment thread src/utils/exportWav.js
Comment on lines +10 to +14
const offlineCtx = new OfflineAudioContext(
numChannels,
Math.ceil(durationSeconds * sampleRate),
sampleRate
)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/utils/exportWav.js
Comment on lines +19 to +23
uniquePaths.map(async (path) => {
const res = await fetch(path)
const arrayBuffer = await res.arrayBuffer()
decoded[path] = await offlineCtx.decodeAudioData(arrayBuffer)
})
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/utils/exportWav.js
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)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
totalBeats,
});
const safeName = String(sequenceTitle).replace(/\s+/g, "_").toLowerCase();
downloadBlob(blob, `${safeName}_${BPM}bpm.wav`);
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.");

Copilot uses AI. Check for mistakes.
@joeshub joeshub force-pushed the claude/save-track-to-file-GAtTX branch from cf6a8b7 to f1271df Compare April 30, 2026 22:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants