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(); if (store.state.selection.length > 0) store.deleteSelection(); else if (store.state.rangeTicks) store.rangeClearNotes(); } else if (ev.key === 'Home') { ev.preventDefault(); store.seekTo(0); } else if (ev.key === 'End') { ev.preventDefault(); store.seekToEnd(); } else if (mod && ev.key.toLowerCase() === 'e') { ev.preventDefault(); store.splitAtPlayhead(); } else if (mod && ev.key.toLowerCase() === 'l') { ev.preventDefault(); store.toggleLoop(); } else if (mod && ev.key.toLowerCase() === 'a') { ev.preventDefault(); store.selectAll(); } else if (mod && ev.key.toLowerCase() === 'c') { ev.preventDefault(); store.copySelection(); } else if (mod && ev.key.toLowerCase() === 'x') { ev.preventDefault(); store.cutSelection(); } else if (mod && ev.key.toLowerCase() === 'v') { ev.preventDefault(); store.pasteAtPlayhead(); } else if (mod && ev.key.toLowerCase() === 'd') { ev.preventDefault(); store.duplicateSelection(); } else if (ev.key.startsWith('Arrow') && store.state.selection.length > 0) { ev.preventDefault(); const song = store.song(); if (!song) return; const grid = store.snapTicks() || Math.round(song.ppq / 8); if (ev.key === 'ArrowLeft') store.nudgeSelection(-grid, 0); else if (ev.key === 'ArrowRight') store.nudgeSelection(grid, 0); else if (ev.key === 'ArrowUp') store.nudgeSelection(0, ev.shiftKey ? 12 : 1); else if (ev.key === 'ArrowDown') store.nudgeSelection(0, ev.shiftKey ? -12 : -1); } else if (ev.key === 'Escape') { store.setLibraryOpen(false); store.setSelection([]); store.setRange(null); } }; 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 sets the position; drag selects; drag the ruler for a time selection; double-click adds a note; alt+drag copies; ctrl+C/X/V/D/E and arrows act on the selection; space plays'}
); }