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