import { useEffect, useRef } from 'react'; import { store, useApp } from '../state/store.ts'; import { player } from '../audio/player.ts'; import { midiManager, type NoteInputEvent } from '../input/webmidi.ts'; import { keyboardPiano } from '../input/keyboardPiano.ts'; import { writeMidi } from '../midi/write.ts'; import type { LiveVoice } from '../instruments/types.ts'; import { DeviceBar } from './DeviceBar.tsx'; import { Transport } from './Transport.tsx'; import { TrackList } from './TrackList.tsx'; import { PianoRoll } from './PianoRoll.tsx'; import { Library } from './Library.tsx'; function download(name: string, data: Uint8Array, mime: string) { const blob = new Blob([data as BlobPart], { type: mime }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(url), 5000); } function inTextField(): boolean { const el = document.activeElement; return !!el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || (el as HTMLElement).isContentEditable); } export function App() { const app = useApp(); const fileRef = useRef(null); const voices = useRef(new Map()); // Route note input (any MIDI device, or the typing keyboard) to live // monitoring on the armed/active track, and to the recorder while // recording. useEffect(() => { const handle = (e: NoteInputEvent) => { const st = store.state; const song = st.rec?.song; if (!song) return; const targetId = st.armedTrackId ?? st.activeTrackId; const track = song.tracks.find((t) => t.id === targetId); const key = `${e.source}:${e.pitch}`; if (e.on) { voices.current.get(key)?.cut(); if (track) { try { voices.current.set(key, player.previewNoteOn(track.instrumentId, e.pitch, e.velocity)); } catch { /* audio blocked until a user gesture */ } } } else { voices.current.get(key)?.noteOff(); voices.current.delete(key); } if (player.state === 'recording') store.recorder.noteEvent(e, player.getPositionSec()); }; const u1 = midiManager.subscribe(handle); const u2 = keyboardPiano.subscribe(handle); void midiManager.request(); return () => { u1(); u2(); }; }, []); // Global shortcuts. useEffect(() => { const onKey = (ev: KeyboardEvent) => { if (inTextField()) return; const mod = ev.ctrlKey || ev.metaKey; if (ev.code === 'Space') { ev.preventDefault(); void store.togglePlay(); } else if (mod && !ev.shiftKey && ev.key.toLowerCase() === 'z') { ev.preventDefault(); store.undo(); } else if ((mod && ev.shiftKey && ev.key.toLowerCase() === 'z') || (mod && ev.key.toLowerCase() === 'y')) { ev.preventDefault(); store.redo(); } else if (ev.key === 'Delete' || ev.key === 'Backspace') { ev.preventDefault(); store.deleteSelection(); } else if (ev.key === 'Escape') { store.setLibraryOpen(false); store.setSelection([]); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); if (!app.ready) return
loading…
; const song = app.rec?.song ?? null; return (
midiboot store.renameSong(e.target.value)} title="Song name" /> { const f = e.target.files?.[0]; if (f) await store.importMidi(f.name, new Uint8Array(await f.arrayBuffer())); e.target.value = ''; }} />
{app.status || 'click in the grid to add notes; right-click deletes; shift+drag selects; space plays'}
); }