From 9a8b269ee8d24304d5c5c40f78b5dc41d12e46ec Mon Sep 17 00:00:00 2001 From: Joshua Ahmath Hairston Date: Mon, 13 Apr 2026 17:09:40 -0400 Subject: [PATCH 1/7] working audio file picker for the sampler --- components/recorder.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/components/recorder.js b/components/recorder.js index e086f90..1d62114 100644 --- a/components/recorder.js +++ b/components/recorder.js @@ -1255,7 +1255,7 @@ export function Recorder({ submit, accompaniment }) { const dispatch = useDispatch(); const [min, setMinute] = useState(0); const [sec, setSecond] = useState(0); - const [isSamplerLoaded, setSamplerLoaded] = useState(false); + const isSamplerLoadedRef = useRef(false); const [hasPermission, setHasPermission] = useState(false); const tRecorder = useRef(null); const sampler = useRef(null); @@ -1303,7 +1303,7 @@ export function Recorder({ submit, accompaniment }) { await start(); onEnabled(); } - + // TODO: Make two different onNotes so that they can't both run at the same time function onNote(e) { const accidental = e.note.accidental let note = e.note.name; @@ -1314,11 +1314,13 @@ export function Recorder({ submit, accompaniment }) { // sampler.current.triggerAttackRelease(note, 4); // if(hasPermission === true) { console.log("note on: ", note); - loaded().then(() => { + if (sampler.current && isSamplerLoadedRef.current) { // would be nice for the user to have notes not play for the full duration // once they stop holding the key sampler.current.triggerAttackRelease(note, 4); - }); + } else { + console.warn("Sampler not ready yet, skipping note:", note); + } } // InstrumentConfigEditor and MidiTable are defined outside of Recorder @@ -1576,15 +1578,21 @@ export function Recorder({ submit, accompaniment }) { G4: "/audio/viola_g4.wav", }; - sampler.current = new Sampler(samples, { - onload: () => { - setSamplerLoaded(true); + const newSampler = new Sampler(samples).toDestination(); + sampler.current = newSampler; + loaded().then(() => { + if (sampler.current === newSampler) { + isSamplerLoadedRef.current = true; } - }).toDestination(); + }).catch((err) => { + console.error("Sampler failed to load audio:", err, "samples:", samples); + }); return () => { window.removeEventListener("keydown", handleKeyDown); + isSamplerLoadedRef.current = false; if (sampler.current) { sampler.current.dispose(); + sampler.current = null; } }; }, [audioFileUrl]); From 0bc376343c07ce0c8e9acc27c8effac7f6bcc700 Mon Sep 17 00:00:00 2001 From: Joshua Ahmath Hairston Date: Mon, 13 Apr 2026 20:58:18 -0400 Subject: [PATCH 2/7] keyboard implementation works --- components/recorder.js | 201 +++++++++++++++++------------------------ 1 file changed, 81 insertions(+), 120 deletions(-) diff --git a/components/recorder.js b/components/recorder.js index 1d62114..7485e50 100644 --- a/components/recorder.js +++ b/components/recorder.js @@ -1,6 +1,6 @@ 'use client'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { BiRename } from 'react-icons/bi'; import { Card, @@ -785,10 +785,26 @@ function Config({ RecordingTypeChanged, value }) { ); } //TODO: maybe I should put this somewhere else? +const DEFAULT_KEY_MAP = "awsedftgyhuj"; +const KEY_MAP_NOTES = [ + { name: 'C', octave: 4, accidental: undefined }, + { name: 'C', octave: 4, accidental: '#' }, + { name: 'D', octave: 4, accidental: undefined }, + { name: 'D', octave: 4, accidental: '#' }, + { name: 'E', octave: 4, accidental: undefined }, + { name: 'F', octave: 4, accidental: undefined }, + { name: 'F', octave: 4, accidental: '#' }, + { name: 'G', octave: 4, accidental: undefined }, + { name: 'G', octave: 4, accidental: '#' }, + { name: 'A', octave: 4, accidental: undefined }, + { name: 'A', octave: 4, accidental: '#' }, + { name: 'B', octave: 4, accidental: undefined }, +]; + const emptyDraft = () => ({ name: "", description: "", - settings: {}, + settings: { keyMap: DEFAULT_KEY_MAP }, file: null, }); @@ -991,7 +1007,7 @@ function MidiTable({ value, onChange }) { ) } -function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null, onMidiDeviceSelect = null }) { +function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null, onMidiDeviceSelect = null, onKeyMapChange = null }) { const [configs, setConfigs] = useState([]); const [selectedId, setSelectedId] = useState(null); const [draft, setDraft] = useState(emptyDraft()); @@ -1021,6 +1037,9 @@ function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null if (onAudioFileChange) { onAudioFileChange(res[0].file || null); } + if (onKeyMapChange) { + onKeyMapChange(res[0].settings?.keyMap || DEFAULT_KEY_MAP); + } } else { setSelectedId(null); @@ -1028,6 +1047,9 @@ function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null if (onAudioFileChange) { onAudioFileChange(null); } + if (onKeyMapChange) { + onKeyMapChange(DEFAULT_KEY_MAP); + } } } catch (error) { setError(String(error)); @@ -1057,6 +1079,9 @@ function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null if (config.settings?.midiDeviceName && onMidiDeviceSelect) { onMidiDeviceSelect(config.settings.midiDeviceName); } + if (onKeyMapChange) { + onKeyMapChange(config.settings?.keyMap || DEFAULT_KEY_MAP); + } }; //TODO: Maybe put this in the useeffect? (like use it there as there's repeated code) const onNew = () => { @@ -1065,6 +1090,20 @@ function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null if (onAudioFileChange) { onAudioFileChange(null); } + if (onKeyMapChange) { + onKeyMapChange(DEFAULT_KEY_MAP); + } + } + + const handleKeyMapChange = (e) => { + const keyMap = e.target.value; + setDraft({ + ...draft, + settings: { + ...draft.settings, + keyMap, + }, + }); } const handleMidiDeviceChange = (e) => { @@ -1135,6 +1174,9 @@ function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null if (onAudioFileChange) { onAudioFileChange(res.file || null); } + if (onKeyMapChange) { + onKeyMapChange(res.settings?.keyMap || DEFAULT_KEY_MAP); + } // this is a function refference passed from the parent to let it know we saved successfully //TODO: I don't see a point in this if statement @@ -1219,6 +1261,18 @@ function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null onChange={handleMidiDeviceChange} /> +
+ +
); } //TODO: maybe I should put this somewhere else? const DEFAULT_KEY_MAP = "awsedftgyhuj"; +const DEFAULT_MIDI_INPUT_MODE = "device"; const KEY_MAP_NOTES = [ { name: 'C', octave: 4, accidental: undefined }, { name: 'C', octave: 4, accidental: '#' }, @@ -804,7 +804,7 @@ const KEY_MAP_NOTES = [ const emptyDraft = () => ({ name: "", description: "", - settings: { keyMap: DEFAULT_KEY_MAP }, + settings: { keyMap: DEFAULT_KEY_MAP, midiInputMode: DEFAULT_MIDI_INPUT_MODE }, file: null, }); @@ -1007,7 +1007,7 @@ function MidiTable({ value, onChange }) { ) } -function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null, onMidiDeviceSelect = null, onKeyMapChange = null }) { +function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null, onMidiDeviceSelect = null, onKeyMapChange = null, onMidiInputModeChange = null }) { const [configs, setConfigs] = useState([]); const [selectedId, setSelectedId] = useState(null); const [draft, setDraft] = useState(emptyDraft()); @@ -1040,6 +1040,9 @@ function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null if (onKeyMapChange) { onKeyMapChange(res[0].settings?.keyMap || DEFAULT_KEY_MAP); } + if (onMidiInputModeChange) { + onMidiInputModeChange(res[0].settings?.midiInputMode || DEFAULT_MIDI_INPUT_MODE); + } } else { setSelectedId(null); @@ -1050,6 +1053,9 @@ function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null if (onKeyMapChange) { onKeyMapChange(DEFAULT_KEY_MAP); } + if (onMidiInputModeChange) { + onMidiInputModeChange(DEFAULT_MIDI_INPUT_MODE); + } } } catch (error) { setError(String(error)); @@ -1082,6 +1088,9 @@ function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null if (onKeyMapChange) { onKeyMapChange(config.settings?.keyMap || DEFAULT_KEY_MAP); } + if (onMidiInputModeChange) { + onMidiInputModeChange(config.settings?.midiInputMode || DEFAULT_MIDI_INPUT_MODE); + } }; //TODO: Maybe put this in the useeffect? (like use it there as there's repeated code) const onNew = () => { @@ -1093,6 +1102,23 @@ function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null if (onKeyMapChange) { onKeyMapChange(DEFAULT_KEY_MAP); } + if (onMidiInputModeChange) { + onMidiInputModeChange(DEFAULT_MIDI_INPUT_MODE); + } + } + + const handleMidiInputModeChange = (e) => { + const mode = e.target.value; + setDraft({ + ...draft, + settings: { + ...draft.settings, + midiInputMode: mode, + }, + }); + if (onMidiInputModeChange) { + onMidiInputModeChange(mode); + } } const handleKeyMapChange = (e) => { @@ -1177,6 +1203,9 @@ function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null if (onKeyMapChange) { onKeyMapChange(res.settings?.keyMap || DEFAULT_KEY_MAP); } + if (onMidiInputModeChange) { + onMidiInputModeChange(res.settings?.midiInputMode || DEFAULT_MIDI_INPUT_MODE); + } // this is a function refference passed from the parent to let it know we saved successfully //TODO: I don't see a point in this if statement @@ -1255,6 +1284,19 @@ function InstrumentConfigEditor({ show, onSaved = null, onAudioFileChange = null
+
+ +
{ + midiInputModeRef.current = midiInputMode; + }, [midiInputMode]); const handleClose = () => setShow(false); const handleShow = () => setShow(true); @@ -1344,7 +1391,7 @@ export function Recorder({ submit, accompaniment }) { setHasPermission(true); webmidiIndex.current = 0; webmidiInput.current = WebMidi.inputs[webmidiIndex.current]; // FIXME: we need to list the inputs from the loop above in the config ui so the user can select their thing - webmidiInput.current.channels[1].addListener("noteon", onNote); + webmidiInput.current.channels[1].addListener("noteon", onMidiNote); } } @@ -1358,25 +1405,30 @@ export function Recorder({ submit, accompaniment }) { await start(); onEnabled(); } - // TODO: Make two different onNotes so that they can't both run at the same time - function onNote(e) { + function playNote(e) { const accidental = e.note.accidental let note = e.note.name; if (accidental != undefined) { note += e.note.accidental; } note += e.note.octave; - // sampler.current.triggerAttackRelease(note, 4); - // if(hasPermission === true) { console.log("note on: ", note); if (sampler.current && isSamplerLoadedRef.current) { - // would be nice for the user to have notes not play for the full duration - // once they stop holding the key sampler.current.triggerAttackRelease(note, 4); } else { console.warn("Sampler not ready yet, skipping note:", note); } + } + + + function onMidiNote(e) { + if (midiInputModeRef.current !== "device") return; + playNote(e); + } + function onKeyboardNote(e) { + if (midiInputModeRef.current !== "keyboard") return; + playNote(e); } // InstrumentConfigEditor and MidiTable are defined outside of Recorder @@ -1389,12 +1441,12 @@ export function Recorder({ submit, accompaniment }) { console.log("Selected MIDI device index:", deviceIndex); // Remove listener from old device if it exists if (webmidiInput.current) { - webmidiInput.current.channels[1].removeListener("noteon", onNote); + webmidiInput.current.channels[1].removeListener("noteon", onMidiNote); } // Update to new device webmidiIndex.current = deviceIndex; webmidiInput.current = WebMidi.inputs[deviceIndex]; - webmidiInput.current.channels[1].addListener("noteon", onNote); + webmidiInput.current.channels[1].addListener("noteon", onMidiNote); }; @@ -1549,7 +1601,7 @@ export function Recorder({ submit, accompaniment }) { const handleKeyDown = (event) => { console.log("Key pressed:", event.key); if (event.key in keyboardMap) { - onNote(keyboardMap[event.key]); + onKeyboardNote(keyboardMap[event.key]); } }; window.addEventListener("keydown", handleKeyDown); @@ -1615,7 +1667,7 @@ export function Recorder({ submit, accompaniment }) { )} {hasPermission && ( <> - + )} From ae23fbe967b98867095d9ee7e7eb9bafe5e30a8b Mon Sep 17 00:00:00 2001 From: Joshua Ahmath Hairston Date: Wed, 15 Apr 2026 12:53:30 -0400 Subject: [PATCH 4/7] added functionality for the notes stopping when the user relases the note --- components/recorder.js | 78 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/components/recorder.js b/components/recorder.js index 033130f..ca2ea91 100644 --- a/components/recorder.js +++ b/components/recorder.js @@ -49,6 +49,7 @@ import { FaRegTrashAlt, } from 'react-icons/fa'; import WaveSurfer from 'wavesurfer.js'; +import { MdOutlineKeyboard } from 'react-icons/md'; // Create a silent audio buffer as scratch audio to initialize wavesurfer const createSilentAudio = () => { @@ -1391,7 +1392,8 @@ export function Recorder({ submit, accompaniment }) { setHasPermission(true); webmidiIndex.current = 0; webmidiInput.current = WebMidi.inputs[webmidiIndex.current]; // FIXME: we need to list the inputs from the loop above in the config ui so the user can select their thing - webmidiInput.current.channels[1].addListener("noteon", onMidiNote); + webmidiInput.current.channels[1].addListener("noteon", onMidiNoteOn); + webmidiInput.current.channels[1].addListener("noteoff", onMidiNoteOff); } } @@ -1405,6 +1407,7 @@ export function Recorder({ submit, accompaniment }) { await start(); onEnabled(); } + /* function playNote(e) { const accidental = e.note.accidental let note = e.note.name; @@ -1419,16 +1422,52 @@ export function Recorder({ submit, accompaniment }) { console.warn("Sampler not ready yet, skipping note:", note); } } +*/ + function getNoteString(noteObj) { + let note = noteObj.name; + if (noteObj.accidental !== undefined) { + note += noteObj.accidental; + } + note += noteObj.octave; + return note; + } + + + function startNote(noteObj) { + const note = getNoteString(noteObj); + console.log("note on: ", note); + if (sampler.current && isSamplerLoadedRef.current) { + sampler.current.triggerAttack(note); + } else { + console.warn("Sampler not ready yet, skipping note:", note); + } + } + function stopNote(noteObj) { + const note = getNoteString(noteObj); + console.log("note off:", note); + if (sampler.current && isSamplerLoadedRef.current) { + sampler.current.triggerRelease(note); + } + } + function onMidiNoteOn(e) { + if (midiInputModeRef.current !== "device") return; + startNote(e.note); + } - function onMidiNote(e) { + function onMidiNoteOff(e) { if (midiInputModeRef.current !== "device") return; - playNote(e); + stopNote(e.note); + } + + function onKeyboardNoteOff(e) { + if (midiInputModeRef.current !== "keyboard") return; + stopNote(e.note); } - function onKeyboardNote(e) { + function onKeyboardNoteOn(e) { if (midiInputModeRef.current !== "keyboard") return; - playNote(e); + startNote(e.note); } // InstrumentConfigEditor and MidiTable are defined outside of Recorder @@ -1441,12 +1480,14 @@ export function Recorder({ submit, accompaniment }) { console.log("Selected MIDI device index:", deviceIndex); // Remove listener from old device if it exists if (webmidiInput.current) { - webmidiInput.current.channels[1].removeListener("noteon", onMidiNote); + webmidiInput.current.channels[1].removeListener("noteon", onMidiNoteOn); + webmidiInput.current.channels[1].removeListener("noteoff", onMidiNoteOff); } // Update to new device webmidiIndex.current = deviceIndex; webmidiInput.current = WebMidi.inputs[deviceIndex]; - webmidiInput.current.channels[1].addListener("noteon", onMidiNote); + webmidiInput.current.channels[1].addListener("noteon", onMidiNoteOn); + webmidiInput.current.channels[1].addListener("noteoff", onMidiNoteOff); }; @@ -1597,16 +1638,31 @@ export function Recorder({ submit, accompaniment }) { }; }, [audioFileUrl]); + const pressedKeysRef = useRef(new Set()); useEffect(() => { const handleKeyDown = (event) => { - console.log("Key pressed:", event.key); - if (event.key in keyboardMap) { - onKeyboardNote(keyboardMap[event.key]); - } + const key = event.key.toLowerCase(); + console.log("Key pressed:", key); + if (!(key in keyboardMap)) return; + if (pressedKeysRef.current.has(key)) return; + + pressedKeysRef.current.add(key); + onKeyboardNoteOn(keyboardMap[key]); + }; + + const handleKeyUp = (event) => { + const key = event.key.toLowerCase(); + if (!(key in keyboardMap)) return; + + pressedKeysRef.current.delete(key); + onKeyboardNoteOff(keyboardMap[key]); }; + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); return () => { window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keyup", handleKeyUp); }; }, [keyboardMap]); From 41ff3f5cd0c4805d6d5600299c747a78b78f6ff6 Mon Sep 17 00:00:00 2001 From: Joshua Ahmath Hairston Date: Mon, 20 Apr 2026 12:49:17 -0400 Subject: [PATCH 5/7] fixed a lot of bugs, and added some things, but this is working --- api.js | 17 ++ components/recorder.js | 520 +++++++++++++++++++++-------------------- 2 files changed, 285 insertions(+), 252 deletions(-) diff --git a/api.js b/api.js index 1576796..97b5402 100644 --- a/api.js +++ b/api.js @@ -258,6 +258,23 @@ export async function mutateInstrumentConfiguration(config_id, instrument_config return json; } +export async function deleteInstrumentConfiguration(config_id) { + const token = await getDjangoToken(); + if (!token) return; + + const API = `${process.env.NEXT_PUBLIC_BACKEND_HOST}/api`; + const url = `${API}/configs/${config_id}/`; + + const response = await fetch(url, { + method: 'DELETE', + headers: { Authorization: `Token ${token}` }, + }); + + if (response.status !== 204 && (response.status < 200 || response.status >= 300)) { + throw new Error(`${response.status}: ${response.statusText}`); + } +} + export async function createInstrumentConfiguration(instrument_config, audioFile = null) { const endpoint = `configs/`; if (audioFile) { diff --git a/components/recorder.js b/components/recorder.js index ca2ea91..98fcc5c 100644 --- a/components/recorder.js +++ b/components/recorder.js @@ -26,7 +26,7 @@ import { AudioDropModal } from './audio/silenceDetect'; import { catchSilence, setupAudioContext } from '../lib/dawUtils'; import StatusIndicator from './statusIndicator'; import styles from '../styles/recorder.module.css'; -import { getInstrumentConfigurations, mutateInstrumentConfiguration, createInstrumentConfiguration } from "../api"; +import { getInstrumentConfigurations, mutateInstrumentConfiguration, createInstrumentConfiguration, deleteInstrumentConfiguration } from "../api"; import MicRecorder from 'mic-recorder-to-mp3'; import { IoSettingsSharp } from "react-icons/io5"; import Modal from 'react-bootstrap/Modal' @@ -779,35 +779,13 @@ function Config({ RecordingTypeChanged, value }) { Pick recording type: ); } //TODO: maybe I should put this somewhere else? -const DEFAULT_KEY_MAP = "awsedftgyhuj"; -const DEFAULT_MIDI_INPUT_MODE = "device"; -const KEY_MAP_NOTES = [ - { name: 'C', octave: 4, accidental: undefined }, - { name: 'C', octave: 4, accidental: '#' }, - { name: 'D', octave: 4, accidental: undefined }, - { name: 'D', octave: 4, accidental: '#' }, - { name: 'E', octave: 4, accidental: undefined }, - { name: 'F', octave: 4, accidental: undefined }, - { name: 'F', octave: 4, accidental: '#' }, - { name: 'G', octave: 4, accidental: undefined }, - { name: 'G', octave: 4, accidental: '#' }, - { name: 'A', octave: 4, accidental: undefined }, - { name: 'A', octave: 4, accidental: '#' }, - { name: 'B', octave: 4, accidental: undefined }, -]; - -const emptyDraft = () => ({ - name: "", - description: "", - settings: { keyMap: DEFAULT_KEY_MAP, midiInputMode: DEFAULT_MIDI_INPUT_MODE }, - file: null, -}); function AudioViewer({ src }) { const containerW = useRef(null); @@ -992,14 +970,14 @@ function AudioViewer({ src }) { function MidiTable({ value, onChange }) { return ( -
-
-
- + {/* Name + Description */} +
+ + +
+ +
+ + {mode === "midi" && ( +
+
-
-
+ )} + +
+ + {/* Audio Sample */}
- -
-
- -
-
- -
-
- {typeof draft.file === 'string' && draft.file && ( -
- Current file: {draft.file} +
+ Current: {draft.file.split('/').pop()}
)} + {fileError && ( +
{fileError}
+ )}
-
- -
+
); } +const DEFAULT_KEY_MAP = "awsedftgyhujk"; +const KEY_MAP_NOTES = [ + { name: 'C', octave: 4, accidental: undefined }, + { name: 'C', octave: 4, accidental: '#' }, + { name: 'D', octave: 4, accidental: undefined }, + { name: 'D', octave: 4, accidental: '#' }, + { name: 'E', octave: 4, accidental: undefined }, + { name: 'F', octave: 4, accidental: undefined }, + { name: 'F', octave: 4, accidental: '#' }, + { name: 'G', octave: 4, accidental: undefined }, + { name: 'G', octave: 4, accidental: '#' }, + { name: 'A', octave: 4, accidental: undefined }, + { name: 'A', octave: 4, accidental: '#' }, + { name: 'B', octave: 4, accidental: undefined }, + { name: 'C', octave: 5, accidental: undefined }, +]; + +const emptyDraft = () => ({ + name: "", + description: "", + settings: { keyMap: DEFAULT_KEY_MAP }, + file: null, +}); + export function Recorder({ submit, accompaniment }) { // const Mp3Recorder = new MicRecorder({ bitRate: 128 }); // 128 is default already const [isRecording, setIsRecording] = useState(false); @@ -1353,7 +1372,8 @@ export function Recorder({ submit, accompaniment }) { const [min, setMinute] = useState(0); const [sec, setSecond] = useState(0); const isSamplerLoadedRef = useRef(false); - const [hasPermission, setHasPermission] = useState(false); + const [hasKeyboardPermission, setHasKeyboardPermission] = useState(false); + const [hasMidiPermission, setHasMidiPermission] = useState(false); const tRecorder = useRef(null); const sampler = useRef(null); const webmidiInput = useRef(null); @@ -1368,12 +1388,13 @@ export function Recorder({ submit, accompaniment }) { }); const [show, setShow] = useState(false); const [audioFileUrl, setAudioFileUrl] = useState(null); + const [persistedConfigId, setPersistedConfigId] = useState(null); const [keyMappings, setKeyMappings] = useState(DEFAULT_KEY_MAP); - const [midiInputMode, setMidiInputMode] = useState(DEFAULT_MIDI_INPUT_MODE); - const midiInputModeRef = useRef(DEFAULT_MIDI_INPUT_MODE); + const recordingTypeRef = useRef(recordingType); + const pressedKeysRef = useRef(new Set()); useEffect(() => { - midiInputModeRef.current = midiInputMode; - }, [midiInputMode]); + recordingTypeRef.current = recordingType; + }, [recordingType]); const handleClose = () => setShow(false); const handleShow = () => setShow(true); @@ -1384,45 +1405,33 @@ export function Recorder({ submit, accompaniment }) { setBlobData(); }, [partType]); - function onEnabled() { - - if (WebMidi.inputs.length < 1 && recordingType == "midi") { - console.error('tried to give permission, but no inputs'); - } else { - setHasPermission(true); - webmidiIndex.current = 0; - webmidiInput.current = WebMidi.inputs[webmidiIndex.current]; // FIXME: we need to list the inputs from the loop above in the config ui so the user can select their thing - webmidiInput.current.channels[1].addListener("noteon", onMidiNoteOn); - webmidiInput.current.channels[1].addListener("noteoff", onMidiNoteOff); + async function enableKeyboard() { + if (!hasKeyboardPermission) { + await start(); } + setHasKeyboardPermission(true); } - - async function enableMidiTone() { + async function enableMidi() { await WebMidi.enable().catch((err) => { alert(err); return; }); - console.log("before start"); - await start(); - onEnabled(); - } - /* - function playNote(e) { - const accidental = e.note.accidental - let note = e.note.name; - if (accidental != undefined) { - note += e.note.accidental; + if (WebMidi.inputs.length < 1) { + console.error('tried to give permission, but no inputs'); + return; } - note += e.note.octave; - console.log("note on: ", note); - if (sampler.current && isSamplerLoadedRef.current) { - sampler.current.triggerAttackRelease(note, 4); - } else { - console.warn("Sampler not ready yet, skipping note:", note); + if (!hasKeyboardPermission) { + await start(); } + webmidiIndex.current = 0; + webmidiInput.current = WebMidi.inputs[webmidiIndex.current]; + webmidiInput.current.channels[1].addListener("noteon", onMidiNoteOn); + webmidiInput.current.channels[1].addListener("noteoff", onMidiNoteOff); + setHasMidiPermission(true); + setHasKeyboardPermission(true); } -*/ + function getNoteString(noteObj) { let note = noteObj.name; if (noteObj.accidental !== undefined) { @@ -1439,7 +1448,7 @@ export function Recorder({ submit, accompaniment }) { if (sampler.current && isSamplerLoadedRef.current) { sampler.current.triggerAttack(note); } else { - console.warn("Sampler not ready yet, skipping note:", note); + alert("No audio file loaded. Please open settings and upload an audio sample before playing."); } } @@ -1451,22 +1460,22 @@ export function Recorder({ submit, accompaniment }) { } } function onMidiNoteOn(e) { - if (midiInputModeRef.current !== "device") return; + if (recordingTypeRef.current !== "midi") return; startNote(e.note); } function onMidiNoteOff(e) { - if (midiInputModeRef.current !== "device") return; + if (recordingTypeRef.current !== "midi") return; stopNote(e.note); } function onKeyboardNoteOff(e) { - if (midiInputModeRef.current !== "keyboard") return; + if (recordingTypeRef.current !== "keyboard") return; stopNote(e.note); } function onKeyboardNoteOn(e) { - if (midiInputModeRef.current !== "keyboard") return; + if (recordingTypeRef.current !== "keyboard") return; startNote(e.note); } // InstrumentConfigEditor and MidiTable are defined outside of Recorder @@ -1495,7 +1504,9 @@ export function Recorder({ submit, accompaniment }) { const startRecording = (ev) => { if (isBlocked) { console.error('cannot record, microphone permissions are blocked'); - } else if (recordingType == "mic") { + return; + } + if (recordingType === "mic") { //TODO make a prompt for the user to select if they are using midi or an instrument accompanimentRef.current.play(); recorder @@ -1505,6 +1516,11 @@ export function Recorder({ submit, accompaniment }) { }) .catch((err) => console.error('problem starting recording', err)); } else { + const permitted = recordingType === "midi" ? hasMidiPermission : hasKeyboardPermission; + if (!permitted) { + alert(`Please open settings and click "Enable ${recordingType === "midi" ? "MIDI" : "Keyboard"}" before recording.`); + return; + } accompanimentRef.current.play(); tRecorder.current.start().then(() => { setIsRecording(true); @@ -1549,10 +1565,7 @@ export function Recorder({ submit, accompaniment }) { data: blob, }, ]); - // a.href = url; - // a.textContent = "Listen to recording"; - // a.download = "test.ogg" - // document.body.appendChild(a); + setIsRecording(false); }) .catch((e) => console.error('error stopping recording midi/keyboard')) @@ -1608,26 +1621,16 @@ export function Recorder({ submit, accompaniment }) { tRecorder.current = new toneRecorder(); getDestination().connect(tRecorder.current); - const samples = audioFileUrl - ? { C5: audioFileUrl } - : { - C5: "/audio/viola_c5.wav", - A4: "/audio/viola_a4.wav", - B4: "/audio/viola_b4.wav", - D4: "/audio/viola_d4.wav", - E4: "/audio/viola_e4.wav", - F4: "/audio/viola_f4.wav", - G4: "/audio/viola_g4.wav", - }; + if (!audioFileUrl) return; - const newSampler = new Sampler(samples).toDestination(); + const newSampler = new Sampler({ C5: audioFileUrl }).toDestination(); sampler.current = newSampler; loaded().then(() => { if (sampler.current === newSampler) { isSamplerLoadedRef.current = true; } }).catch((err) => { - console.error("Sampler failed to load audio:", err, "samples:", samples); + console.error("Sampler failed to load audio:", err); }); return () => { isSamplerLoadedRef.current = false; @@ -1638,9 +1641,11 @@ export function Recorder({ submit, accompaniment }) { }; }, [audioFileUrl]); - const pressedKeysRef = useRef(new Set()); useEffect(() => { + const isTypingTarget = (el) => ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName); + const handleKeyDown = (event) => { + if (isTypingTarget(event.target)) return; const key = event.key.toLowerCase(); console.log("Key pressed:", key); if (!(key in keyboardMap)) return; @@ -1651,6 +1656,7 @@ export function Recorder({ submit, accompaniment }) { }; const handleKeyUp = (event) => { + if (isTypingTarget(event.target)) return; const key = event.key.toLowerCase(); if (!(key in keyboardMap)) return; @@ -1710,26 +1716,36 @@ export function Recorder({ submit, accompaniment }) { - - - Recording Settings + + + Recording Settings -
-
- - {(recordingType === "midi" || recordingType === "keyboard") && ( - - )} - {hasPermission && ( - <> - - - - )} - -
+
+ + {recordingType === "keyboard" && !hasKeyboardPermission && ( + + )} + {recordingType === "midi" && !hasMidiPermission && ( + + )}
+ {((recordingType === "keyboard" && hasKeyboardPermission) || (recordingType === "midi" && hasMidiPermission)) && ( + + )} From 8fabeafeafe201f6968df9999466d1f0b6b69a08 Mon Sep 17 00:00:00 2001 From: Joshua Ahmath Hairston Date: Mon, 20 Apr 2026 20:10:31 -0400 Subject: [PATCH 6/7] added more error checking --- components/recorder.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/components/recorder.js b/components/recorder.js index 98fcc5c..e3c490a 100644 --- a/components/recorder.js +++ b/components/recorder.js @@ -1004,6 +1004,7 @@ function InstrumentConfigEditor({ show, mode, onSaved, onAudioFileChange, onMidi const [error, setError] = useState(""); const [nameError, setNameError] = useState(""); const [fileError, setFileError] = useState(""); + const [midiError, setMidiError] = useState(""); useEffect(() => { if (!show) { @@ -1117,6 +1118,12 @@ function InstrumentConfigEditor({ show, mode, onSaved, onAudioFileChange, onMidi } setNameError(""); + if (mode === "midi" && !draft.settings?.midiDeviceName) { + setMidiError("Please select a MIDI device before saving."); + return; + } + setMidiError(""); + const isNew = selectedId === null; const hasNoFile = !draft.file; if (isNew && hasNoFile) { @@ -1237,7 +1244,7 @@ function InstrumentConfigEditor({ show, mode, onSaved, onAudioFileChange, onMidi ) : ( <> - + {configs.map((config) => (
)} From 1ad8576d55a1a4dde5ae724086bcbca93cf601ea Mon Sep 17 00:00:00 2001 From: Joshua Ahmath Hairston Date: Mon, 20 Apr 2026 21:25:59 -0400 Subject: [PATCH 7/7] updated recorder --- components/recorder.js | 81 +++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/components/recorder.js b/components/recorder.js index e3c490a..6f62377 100644 --- a/components/recorder.js +++ b/components/recorder.js @@ -994,11 +994,9 @@ function toRelativeMediaUrl(url) { return parsed.pathname + parsed.search; } -function InstrumentConfigEditor({ show, mode, onSaved, onAudioFileChange, onMidiDeviceSelect, onKeyMapChange, persistedSelectedId, onSelectedIdChange }) { - const [configs, setConfigs] = useState([]); +function InstrumentConfigEditor({ show, mode, onSaved, onAudioFileChange, onMidiDeviceSelect, onKeyMapChange, persistedSelectedId, onSelectedIdChange, configs, setConfigs }) { const [selectedId, setSelectedId] = useState(null); const [draft, setDraft] = useState(emptyDraft()); - const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [deleting, setDeleting] = useState(false); const [error, setError] = useState(""); @@ -1007,42 +1005,32 @@ function InstrumentConfigEditor({ show, mode, onSaved, onAudioFileChange, onMidi const [midiError, setMidiError] = useState(""); useEffect(() => { - if (!show) { - return; - } - (async () => { - setError(""); - setLoading(true); - try { - const res = await getInstrumentConfigurations() - setConfigs(res); - //TODO: remember the last selected config? - if (res.length > 0) { - const initial = res.find((c) => c.id === persistedSelectedId) || res[0]; - console.log(`initial config: ${JSON.stringify(initial)}`); - setSelectedId(initial.id); - setDraft({ - name: initial.name, - description: initial.description, - settings: initial.settings, - file: initial.file || null, - }); - onAudioFileChange(toRelativeMediaUrl(initial.file)); - onKeyMapChange(initial.settings?.keyMap || DEFAULT_KEY_MAP); - } - else { - setSelectedId(null); - setDraft(emptyDraft()); - onAudioFileChange(null); - onKeyMapChange(DEFAULT_KEY_MAP); + if (!show) return; + setError(""); + try { + if (configs.length > 0) { + const initial = configs.find((c) => c.id === persistedSelectedId) || configs[0]; + setSelectedId(initial.id); + setDraft({ + name: initial.name, + description: initial.description, + settings: initial.settings, + file: initial.file || null, + }); + onAudioFileChange(toRelativeMediaUrl(initial.file)); + onKeyMapChange(initial.settings?.keyMap || DEFAULT_KEY_MAP); + if (mode === "midi" && initial.settings?.midiDeviceName) { + onMidiDeviceSelect(initial.settings.midiDeviceName); } - } catch (error) { - setError(String(error)); - } finally { - setLoading(false); + } else { + setSelectedId(null); + setDraft(emptyDraft()); + onAudioFileChange(null); + onKeyMapChange(DEFAULT_KEY_MAP); } - })(); - + } catch (error) { + setError(String(error)); + } }, [show]); const onSelectedConfig = (id) => { @@ -1173,6 +1161,7 @@ function InstrumentConfigEditor({ show, mode, onSaved, onAudioFileChange, onMidi file: res.file || null, }); + onSelectedIdChange(res.id); onAudioFileChange(toRelativeMediaUrl(res.file)); onKeyMapChange(res.settings?.keyMap || DEFAULT_KEY_MAP); onSaved(); @@ -1215,10 +1204,6 @@ function InstrumentConfigEditor({ show, mode, onSaved, onAudioFileChange, onMidi } }; - if (loading) { - return
Loading configurations...
; - } - if (error) { return
Error: {error}
; } @@ -1399,6 +1384,18 @@ export function Recorder({ submit, accompaniment }) { const [show, setShow] = useState(false); const [audioFileUrl, setAudioFileUrl] = useState(null); const [persistedConfigId, setPersistedConfigId] = useState(null); + const [configs, setConfigs] = useState([]); + + useEffect(() => { + (async () => { + try { + const res = await getInstrumentConfigurations(); + setConfigs(res); + } catch (err) { + console.error('Failed to load instrument configurations:', err); + } + })(); + }, []); const [keyMappings, setKeyMappings] = useState(DEFAULT_KEY_MAP); const recordingTypeRef = useRef(recordingType); const pressedKeysRef = useRef(new Set()); @@ -1754,6 +1751,8 @@ export function Recorder({ submit, accompaniment }) { onKeyMapChange={setKeyMappings} persistedSelectedId={persistedConfigId} onSelectedIdChange={setPersistedConfigId} + configs={configs} + setConfigs={setConfigs} /> )}