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 = ({ +