-
+ {/* Name + Description */}
+
+
+
+
+ {mode === "midi" && (
+
+
+ {midiError && (
+
{midiError}
+ )}
-
-
-
-
+ )}
+
+
+
+ {/* Audio Sample */}
-
+ {fieldLabel('Audio Sample (C5, mp3 or wav)')}
+
{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);
@@ -1255,8 +1366,9 @@ export function Recorder({ submit, accompaniment }) {
const dispatch = useDispatch();
const [min, setMinute] = useState(0);
const [sec, setSecond] = useState(0);
- const [isSamplerLoaded, setSamplerLoaded] = useState(false);
- const [hasPermission, setHasPermission] = useState(false);
+ const isSamplerLoadedRef = useRef(false);
+ const [hasKeyboardPermission, setHasKeyboardPermission] = useState(false);
+ const [hasMidiPermission, setHasMidiPermission] = useState(false);
const tRecorder = useRef(null);
const sampler = useRef(null);
const webmidiInput = useRef(null);
@@ -1271,6 +1383,25 @@ 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());
+ useEffect(() => {
+ recordingTypeRef.current = recordingType;
+ }, [recordingType]);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
@@ -1281,45 +1412,78 @@ 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", onNote);
+ 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();
+ if (WebMidi.inputs.length < 1) {
+ console.error('tried to give permission, but no inputs');
+ return;
+ }
+ 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 onNote(e) {
- const accidental = e.note.accidental
- let note = e.note.name;
- if (accidental != undefined) {
- note += e.note.accidental;
+ function getNoteString(noteObj) {
+ let note = noteObj.name;
+ if (noteObj.accidental !== undefined) {
+ note += noteObj.accidental;
}
- note += e.note.octave;
- // sampler.current.triggerAttackRelease(note, 4);
- // if(hasPermission === true) {
+ note += noteObj.octave;
+ return note;
+ }
+
+
+ function startNote(noteObj) {
+ const note = getNoteString(noteObj);
console.log("note on: ", note);
- loaded().then(() => {
- // 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);
- });
+ if (sampler.current && isSamplerLoadedRef.current) {
+ sampler.current.triggerAttack(note);
+ } else {
+ alert("No audio file loaded. Please open settings and upload an audio sample before playing.");
+ }
+ }
+ 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 (recordingTypeRef.current !== "midi") return;
+ startNote(e.note);
+ }
+
+ function onMidiNoteOff(e) {
+ if (recordingTypeRef.current !== "midi") return;
+ stopNote(e.note);
+ }
+
+ function onKeyboardNoteOff(e) {
+ if (recordingTypeRef.current !== "keyboard") return;
+ stopNote(e.note);
+ }
+
+ function onKeyboardNoteOn(e) {
+ if (recordingTypeRef.current !== "keyboard") return;
+ startNote(e.note);
}
// InstrumentConfigEditor and MidiTable are defined outside of Recorder
@@ -1332,12 +1496,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", onNote);
+ 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", onNote);
+ webmidiInput.current.channels[1].addListener("noteon", onMidiNoteOn);
+ webmidiInput.current.channels[1].addListener("noteoff", onMidiNoteOff);
};
@@ -1345,7 +1511,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
@@ -1355,6 +1523,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);
@@ -1399,10 +1572,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'))
@@ -1427,114 +1597,15 @@ export function Recorder({ submit, accompaniment }) {
setBlobInfo(newInfo);
}
// Start of integration
- const keyboardMap = {
- 'a': {
- qwerty: 'a',
- note: {
- name: 'C',
- octave: 4,
- accidental: undefined,
- }
- },
- 'w': {
- qwerty: 'w',
- note: {
- name: 'C',
- octave: 4,
- accidental: '#',
- }
- },
- 's': {
- qwerty: 's',
- note: {
- name: 'D',
- octave: 4,
- accidental: undefined,
- }
- },
- 'e': {
- qwerty: 'e',
- note: {
- name: 'D',
- octave: 4,
- accidental: '#',
- }
- },
- 'd': {
- qwerty: 'd',
- note: {
- name: 'E',
- octave: 4,
- accidental: undefined,
- }
- },
- 'f': {
- qwerty: 'f',
- note: {
- name: 'F',
- octave: 4,
- accidental: undefined,
- }
- },
- 't': {
- qwerty: 't',
- note: {
- name: 'F',
- octave: 4,
- accidental: '#',
- }
- },
- 'g': {
- qwerty: 'g',
- note: {
- name: 'G',
- octave: 4,
- accidental: undefined,
- }
- },
-
- 'y': {
- qwerty: 'y',
- note: {
- name: 'G',
- octave: 4,
- accidental: '#',
- }
- },
- 'h': {
- qwerty: 'h',
- note: {
- name: 'A',
- octave: 4,
- accidental: undefined,
- }
- },
- 'u': {
- qwerty: 'u',
- note: {
- name: 'A',
- octave: 4,
- accidental: '#',
- }
- },
- 'j': {
- qwerty: 'j',
- note: {
- name: 'B',
- octave: 4,
- accidental: undefined,
- }
- },
- 'k': {
- qwerty: 'k',
- note: {
- name: 'C',
- octave: 5,
- accidental: undefined,
- }
- },
-
- }
+ const keyboardMap = useMemo(() => {
+ const map = {};
+ for (let i = 0; i < KEY_MAP_NOTES.length; i++) {
+ const key = keyMappings[i];
+ if (!key) continue;
+ map[key] = { qwerty: key, note: KEY_MAP_NOTES[i] };
+ }
+ return map;
+ }, [keyMappings]);
// check for recording permissions
useEffect(() => {
if (
@@ -1554,41 +1625,60 @@ export function Recorder({ submit, accompaniment }) {
setIsBlocked(true);
});
}
- const handleKeyDown = (event) => {
- console.log("Key pressed:", event.key);
- if (event.key in keyboardMap) {
- onNote(keyboardMap[event.key]);
- }
- }
- window.addEventListener("keydown", handleKeyDown);
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;
- sampler.current = new Sampler(samples, {
- onload: () => {
- setSamplerLoaded(true);
+ const newSampler = new Sampler({ C5: audioFileUrl }).toDestination();
+ sampler.current = newSampler;
+ loaded().then(() => {
+ if (sampler.current === newSampler) {
+ isSamplerLoadedRef.current = true;
}
- }).toDestination();
+ }).catch((err) => {
+ console.error("Sampler failed to load audio:", err);
+ });
return () => {
- window.removeEventListener("keydown", handleKeyDown);
+ isSamplerLoadedRef.current = false;
if (sampler.current) {
sampler.current.dispose();
+ sampler.current = null;
}
};
}, [audioFileUrl]);
+ 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;
+ if (pressedKeysRef.current.has(key)) return;
+
+ pressedKeysRef.current.add(key);
+ onKeyboardNoteOn(keyboardMap[key]);
+ };
+
+ const handleKeyUp = (event) => {
+ if (isTypingTarget(event.target)) return;
+ 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]);
+
useEffect(() => {
let interval = null;
if (isRecording) {
@@ -1633,26 +1723,38 @@ 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)) && (
+
+ )}