export const APP_NAME = 'Music Visualizer'; export type NoteKind = 'attack' | 'sustain'; export type NoteEvent = { /** Unique per performance occurrence. */ eventId: string; /** Verovio SVG g.note id, shared by repeated performances of the same notehead. */ scoreId: string; time: number; duration: number; pitch: number; velocity: number; staff: number; /** SVG user coordinates (outer viewBox). */ x: number; y: number; kind: NoteKind; grace?: boolean; }; export type ScrollKey = { /** Music seconds. */ time: number; /** Left edge of the visible score in SVG user units. */ x: number; }; export type ScoreMeta = { title: string; subtitle: string; composer: string; svgWidth: number; svgHeight: number; /** Last sounding note end, in music seconds. */ musicDuration: number; /** Audio duration, in seconds. */ audioDuration: number; measureCount: number; scoreNoteCount: number; midiNoteCount: number; mappedNoteCount: number; warnings: string[]; unsupported: string[]; }; export type VisualSettings = { width: number; height: number; fps: number; introSeconds: number; outroSeconds: number; /** Score strip height as a fraction of the video height. 0.3 - 0.75. */ scoreHeightRatio: number; /** Active note target position as a fraction of viewport width. 0.25 - 0.6. */ scrollLead: number; paperColor: string; /** Paper opacity; 0 makes the score float on the background with no paper at all. */ paperAlpha: number; /** Ink colour for the engraved notation. */ scoreColor: string; background: string; staffColors: string[]; /** Ball radius multiplier around the default. */ ballScale: number; /** * Amplifies the orb's jump along the sheet normal. Resting height above the * ink stays put; 0 slides every hop flat onto the paper. */ ballLift: number; /** Glow intensity multiplier. */ glow: number; showTitle: boolean; showProgress: boolean; titleFont: string; audioOffsetMs: number; /** Camera pitch in degrees. Positive values tip the top of the score away. */ tiltX: number; /** Camera yaw in degrees. Positive values turn the right side away. */ tiltY: number; /** Camera roll in degrees. Negative values lift the right side of the staff. */ tiltZ: number; /** CSS perspective distance in pixels. Smaller values give stronger convergence. */ perspective: number; /** Extra zoom applied to the tilted score plane so it still fills the frame. */ tiltZoom: number; }; export type RenderData = { version: 1; meta: ScoreMeta; events: NoteEvent[]; activations: NoteEvent[]; scroll: ScrollKey[]; svgUrl: string; audioUrl: string; settings: VisualSettings; }; export type JobStatus = 'queued' | 'running' | 'ready' | 'rendering' | 'done' | 'error'; export type JobStage = | 'queued' | 'reading' | 'synthesizing' | 'engraving' | 'mapping' | 'building' | 'rendering' | 'done'; export type JobPublic = { id: string; status: JobStatus; stage: JobStage; progress: number; message: string; error?: string; warnings?: string[]; unsupported?: string[]; data?: RenderData; videoUrl?: string; createdAt: string; inputName?: string; inputFormat?: string; }; export type CreateJobResponse = { id: string; }; export type HealthResponse = { ok: boolean; musescore: { available: boolean; path?: string; version?: string }; chrome: { available: boolean; note: string }; ffmpeg: { available: boolean; note: string }; version: string; }; export const DEFAULT_SETTINGS: VisualSettings = { width: 1920, height: 1080, fps: 30, introSeconds: 0.65, outroSeconds: 1.8, scoreHeightRatio: 0.5, scrollLead: 0.38, paperColor: '#c9c2b6', paperAlpha: 1, scoreColor: '#14110d', background: '#111015', staffColors: ['#bda0ff', '#ffbc6d', '#8fe3c8', '#ff8fa3'], ballScale: 1, ballLift: 1, glow: 1, showTitle: true, showProgress: true, titleFont: "Georgia, 'Times New Roman', 'Noto Serif SC', 'Songti SC', serif", audioOffsetMs: 0, tiltX: 12, tiltY: 0, tiltZ: -5, perspective: 1800, tiltZoom: 1.12, }; export const RESOLUTION_PRESETS = [ {id: 'landscape', label: '横屏 · 16:9', width: 1920, height: 1080}, {id: 'vertical', label: '竖屏 · 9:16', width: 1080, height: 1920}, {id: 'square', label: '方形 · 1:1', width: 1080, height: 1080}, ] as const; export const MAX_UPLOAD_BYTES = 30 * 1024 * 1024; export const MAX_RENDER_SECONDS = 20 * 60;