From cf6a8b75d061bd688acd7e070496614dca7617d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 22:41:53 +0000 Subject: [PATCH] add lo-fi Opus effect via WebCodecs Adds a bitrate dropdown next to the SAVE button. Selecting a non-zero value runs the rendered AudioBuffer through AudioEncoder (Opus) at the chosen bitrate and immediately back through AudioDecoder. The decoded PCM is then written to WAV and downloaded, so the saved file audibly contains Opus codec artifacts (more pronounced at lower bitrates). --- src/components/Toolbar.css | 16 ++++++- src/components/Toolbar.jsx | 49 ++++++++++++++++++-- src/utils/exportWav.js | 8 ++-- src/utils/lofiCodec.js | 95 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 src/utils/lofiCodec.js diff --git a/src/components/Toolbar.css b/src/components/Toolbar.css index abc344f..49453e3 100644 --- a/src/components/Toolbar.css +++ b/src/components/Toolbar.css @@ -24,12 +24,26 @@ } .button_save { - margin-left: var(--spacer); + margin-left: calc(var(--spacer) / 2); padding: 0 10px; cursor: pointer; letter-spacing: 0.05em; } + .select_lofi { + margin-left: var(--spacer); + padding: 0 10px; + text-transform: uppercase; + border-radius: 0; + -webkit-appearance: none; + -webkit-border-radius: 0px; + } + + .select_lofi:disabled { + opacity: 0.5; + cursor: not-allowed; + } + .button_save:disabled { cursor: progress; opacity: 0.6; diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index 25ff0f8..16753bc 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -1,9 +1,22 @@ import { useContext, memo, useState } from "react"; import { sequenceList } from "../constants/config"; import { Context } from "../Provider"; -import { renderSequenceToWav, downloadBlob } from "../utils/exportWav"; +import { + renderSequence, + audioBufferToWavBlob, + downloadBlob, +} from "../utils/exportWav"; +import { applyOpusLofi, isWebCodecsSupported } from "../utils/lofiCodec"; import "./Toolbar.css"; +const LOFI_OPTIONS = [ + { value: 0, label: "Hi-Fi" }, + { value: 64000, label: "64k Opus" }, + { value: 32000, label: "32k Opus" }, + { value: 16000, label: "16k Opus" }, + { value: 6000, label: "6k Opus" }, +]; + const ToolBar = ({ setStartTime, setPastLapse, @@ -17,6 +30,7 @@ const ToolBar = ({ const { sequence, selectSequence } = useContext(Context); const { id: selectedSequenceID, title: sequenceTitle } = sequence; const [isSaving, setIsSaving] = useState(false); + const [lofiBitrate, setLofiBitrate] = useState(0); function togglePlayback() { if (isSequencePlaying) { @@ -39,14 +53,25 @@ const ToolBar = ({ async function saveTrack() { setIsSaving(true); try { - const blob = await renderSequenceToWav({ + // Opus only natively supports 8/12/16/24/48kHz; render at 48k when lo-fi + // is enabled to avoid an extra resampling step before the encoder. + const sampleRate = lofiBitrate ? 48000 : 44100; + const rendered = await renderSequence({ sequence, BPM: Number(BPM), totalSteps, totalBeats, + sampleRate, }); + const finalBuffer = lofiBitrate + ? await applyOpusLofi(rendered, lofiBitrate) + : rendered; const safeName = String(sequenceTitle).replace(/\s+/g, "_").toLowerCase(); - downloadBlob(blob, `${safeName}_${BPM}bpm.wav`); + const suffix = lofiBitrate ? `_opus${lofiBitrate / 1000}k` : ""; + downloadBlob( + audioBufferToWavBlob(finalBuffer), + `${safeName}_${BPM}bpm${suffix}.wav`, + ); } finally { setIsSaving(false); } @@ -103,6 +128,24 @@ const ToolBar = ({ +