import React, { useEffect, useRef } from 'react'; import { useCurrentFrame, useVideoConfig, interpolate, Easing, AbsoluteFill, } from 'remotion'; import * as Tone from 'tone'; // ============================================================================= // COMPOSITION CONFIG // ============================================================================= export const compositionConfig = { id: 'EnhancedPianoSynth', durationInSeconds: 25, fps: 30, width: 1920, height: 1080, }; // ============================================================================= // STYLE CONSTANTS // ============================================================================= const COLORS = { primary: '#1a1a2e', secondary: '#16213e', accent: '#e94560', gold: '#f4d03f', background: '#0f0f1a', whiteKey: '#fefefe', whiteKeyPressed: '#e8e8e8', blackKey: '#1a1a1a', blackKeyPressed: '#333333', text: '#ffffff', glow: '#4fc3f7', } as const; const TYPOGRAPHY = { fontFamily: 'Inter, system-ui, sans-serif', } as const; const EASINGS = { easeOut: Easing.bezier(0.33, 1, 0.68, 1), easeIn: Easing.bezier(0.32, 0, 0.67, 0), easeInOut: Easing.bezier(0.37, 0, 0.63, 1), bounce: Easing.bezier(0.34, 1.4, 0.64, 1), }; // ============================================================================= // JINGLE BELLS MELODY DATA // ============================================================================= type NoteEvent = { note: string; startFrame: number; duration: number; velocity: number; // 0-1 for dynamics }; const BPM = 140; const FRAMES_PER_BEAT = (30 * 60) / BPM; const createMelody = (): NoteEvent[] => { const melody: { note: string; beat: number; dur: number; vel?: number }[] = [ // "Jingle bells, jingle bells" - accented { note: 'E4', beat: 0, dur: 1, vel: 0.9 }, { note: 'E4', beat: 1, dur: 1, vel: 0.7 }, { note: 'E4', beat: 2, dur: 2, vel: 0.85 }, { note: 'E4', beat: 4, dur: 1, vel: 0.9 }, { note: 'E4', beat: 5, dur: 1, vel: 0.7 }, { note: 'E4', beat: 6, dur: 2, vel: 0.85 }, // "Jingle all the way" { note: 'E4', beat: 8, dur: 1, vel: 0.9 }, { note: 'G4', beat: 9, dur: 1, vel: 0.95 }, { note: 'C4', beat: 10, dur: 1.5, vel: 0.8 }, { note: 'D4', beat: 11.5, dur: 0.5, vel: 0.6 }, { note: 'E4', beat: 12, dur: 4, vel: 1.0 }, // "Oh what fun it is to ride" { note: 'F4', beat: 16, dur: 1, vel: 0.85 }, { note: 'F4', beat: 17, dur: 1, vel: 0.75 }, { note: 'F4', beat: 18, dur: 1.5, vel: 0.8 }, { note: 'F4', beat: 19.5, dur: 0.5, vel: 0.6 }, { note: 'F4', beat: 20, dur: 1, vel: 0.85 }, { note: 'E4', beat: 21, dur: 1, vel: 0.8 }, { note: 'E4', beat: 22, dur: 0.5, vel: 0.7 }, { note: 'E4', beat: 22.5, dur: 0.5, vel: 0.65 }, // "In a one horse open sleigh" { note: 'E4', beat: 23, dur: 1, vel: 0.75 }, { note: 'D4', beat: 24, dur: 1, vel: 0.8 }, { note: 'D4', beat: 25, dur: 1, vel: 0.75 }, { note: 'E4', beat: 26, dur: 1, vel: 0.85 }, { note: 'D4', beat: 27, dur: 2, vel: 0.9 }, { note: 'G4', beat: 29, dur: 2, vel: 1.0 }, // Second verse { note: 'E4', beat: 32, dur: 1, vel: 0.9 }, { note: 'E4', beat: 33, dur: 1, vel: 0.7 }, { note: 'E4', beat: 34, dur: 2, vel: 0.85 }, { note: 'E4', beat: 36, dur: 1, vel: 0.9 }, { note: 'E4', beat: 37, dur: 1, vel: 0.7 }, { note: 'E4', beat: 38, dur: 2, vel: 0.85 }, { note: 'E4', beat: 40, dur: 1, vel: 0.9 }, { note: 'G4', beat: 41, dur: 1, vel: 0.95 }, { note: 'C4', beat: 42, dur: 1.5, vel: 0.8 }, { note: 'D4', beat: 43.5, dur: 0.5, vel: 0.6 }, { note: 'E4', beat: 44, dur: 4, vel: 1.0 }, { note: 'F4', beat: 48, dur: 1, vel: 0.85 }, { note: 'F4', beat: 49, dur: 1, vel: 0.75 }, { note: 'F4', beat: 50, dur: 1.5, vel: 0.8 }, { note: 'F4', beat: 51.5, dur: 0.5, vel: 0.6 }, { note: 'F4', beat: 52, dur: 1, vel: 0.85 }, { note: 'E4', beat: 53, dur: 1, vel: 0.8 }, { note: 'E4', beat: 54, dur: 0.5, vel: 0.7 }, { note: 'E4', beat: 54.5, dur: 0.5, vel: 0.65 }, { note: 'G4', beat: 55, dur: 1, vel: 0.9 }, { note: 'G4', beat: 56, dur: 1, vel: 0.85 }, { note: 'F4', beat: 57, dur: 1, vel: 0.8 }, { note: 'D4', beat: 58, dur: 1, vel: 0.85 }, { note: 'C4', beat: 59, dur: 4, vel: 1.0 }, ]; return melody.map((m) => ({ note: m.note, startFrame: Math.round(m.beat * FRAMES_PER_BEAT) + 30, duration: Math.round(m.dur * FRAMES_PER_BEAT), velocity: m.vel ?? 0.8, })); }; const MELODY = createMelody(); // ============================================================================= // PIANO KEY DEFINITIONS // ============================================================================= type KeyDef = { note: string; isBlack: boolean; x: number; }; const WHITE_KEY_WIDTH = 80; const BLACK_KEY_WIDTH = 50; const WHITE_KEY_HEIGHT = 280; const BLACK_KEY_HEIGHT = 170; const createKeyboard = (): KeyDef[] => { const keys: KeyDef[] = []; const whiteNotes = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; const blackNoteOffsets: Record = { C: 0.65, D: 0.65, E: null, F: 0.65, G: 0.6, A: 0.7, B: null, }; let xPos = 0; const octaves = [3, 4, 5]; octaves.forEach((octave) => { whiteNotes.forEach((note) => { keys.push({ note: `${note}${octave}`, isBlack: false, x: xPos, }); const blackOffset = blackNoteOffsets[note]; if (blackOffset !== null) { keys.push({ note: `${note}#${octave}`, isBlack: true, x: xPos + WHITE_KEY_WIDTH * blackOffset, }); } xPos += WHITE_KEY_WIDTH; }); }); return keys; }; const KEYS = createKeyboard(); const KEYBOARD_WIDTH = 21 * WHITE_KEY_WIDTH; // ============================================================================= // SYNTHESIS APPROACHES - Choose your sound! // ============================================================================= /** * APPROACH 1: Layered Synthesis * Combines multiple oscillators for richer harmonics */ const createLayeredPiano = () => { // Main tone - warm triangle const mainSynth = new Tone.PolySynth(Tone.Synth, { oscillator: { type: 'triangle' }, envelope: { attack: 0.005, decay: 0.4, sustain: 0.3, release: 1.5, }, }); // Harmonic layer - adds brightness const harmonicSynth = new Tone.PolySynth(Tone.Synth, { oscillator: { type: 'sine', partialCount: 4 }, envelope: { attack: 0.001, decay: 0.2, sustain: 0.1, release: 0.8, }, }); // Sub layer - adds body const subSynth = new Tone.PolySynth(Tone.Synth, { oscillator: { type: 'sine' }, envelope: { attack: 0.01, decay: 0.6, sustain: 0.2, release: 2, }, }); // Effects const reverb = new Tone.Reverb({ decay: 2.5, wet: 0.3 }); const compressor = new Tone.Compressor(-20, 4); // Routing mainSynth.volume.value = -6; harmonicSynth.volume.value = -18; subSynth.volume.value = -12; mainSynth.connect(compressor); harmonicSynth.connect(compressor); subSynth.connect(compressor); compressor.connect(reverb); reverb.toDestination(); return { play: (note: string, duration: number, velocity: number) => { const vel = velocity * 0.8; mainSynth.triggerAttackRelease(note, duration, undefined, vel); harmonicSynth.triggerAttackRelease(note, duration * 0.5, undefined, vel * 0.4); // Sub plays octave below for bass notes const noteNum = Tone.Frequency(note).toMidi(); if (noteNum < 60) { subSynth.triggerAttackRelease(note, duration, undefined, vel * 0.3); } }, dispose: () => { mainSynth.dispose(); harmonicSynth.dispose(); subSynth.dispose(); reverb.dispose(); compressor.dispose(); }, }; }; /** * APPROACH 2: FM Synthesis Piano * Uses frequency modulation for bell-like piano tones */ const createFMPiano = () => { const fmSynth = new Tone.PolySynth(Tone.FMSynth, { harmonicity: 3, modulationIndex: 10, oscillator: { type: 'sine' }, envelope: { attack: 0.001, decay: 0.5, sustain: 0.2, release: 1.2, }, modulation: { type: 'square' }, modulationEnvelope: { attack: 0.002, decay: 0.3, sustain: 0, release: 0.5, }, }); const eq = new Tone.EQ3({ low: 2, mid: 0, high: -4, }); const reverb = new Tone.Reverb({ decay: 2, wet: 0.25 }); const chorus = new Tone.Chorus(4, 2.5, 0.5).start(); fmSynth.volume.value = -8; fmSynth.chain(eq, chorus, reverb, Tone.Destination); return { play: (note: string, duration: number, velocity: number) => { fmSynth.triggerAttackRelease(note, duration, undefined, velocity * 0.7); }, dispose: () => { fmSynth.dispose(); eq.dispose(); reverb.dispose(); chorus.dispose(); }, }; }; /** * APPROACH 3: Physical Modeling Style * Attempts to simulate string resonance with filtered noise */ const createPhysicalPiano = () => { // Main oscillator const mainSynth = new Tone.PolySynth(Tone.Synth, { oscillator: { type: 'fatsawtooth', count: 2, spread: 10, }, envelope: { attack: 0.002, decay: 0.3, sustain: 0.25, release: 1.8, }, }); // Hammer noise - simulates the attack transient const noiseSynth = new Tone.NoiseSynth({ noise: { type: 'white' }, envelope: { attack: 0.001, decay: 0.05, sustain: 0, release: 0.03, }, }); // Filter for the main sound const filter = new Tone.Filter({ type: 'lowpass', frequency: 2000, Q: 1, }); // Envelope follower for filter const filterEnv = new Tone.FrequencyEnvelope({ attack: 0.001, decay: 0.4, sustain: 0.2, release: 1, baseFrequency: 800, octaves: 3, }); const reverb = new Tone.Reverb({ decay: 3, wet: 0.35 }); const compressor = new Tone.Compressor(-15, 3); // Routing mainSynth.volume.value = -10; noiseSynth.volume.value = -24; filterEnv.connect(filter.frequency); mainSynth.connect(filter); noiseSynth.connect(filter); filter.connect(compressor); compressor.connect(reverb); reverb.toDestination(); return { play: (note: string, duration: number, velocity: number) => { filterEnv.triggerAttack(); mainSynth.triggerAttackRelease(note, duration, undefined, velocity * 0.6); noiseSynth.triggerAttackRelease('16n', undefined, velocity * 0.5); setTimeout(() => filterEnv.triggerRelease(), duration * 1000); }, dispose: () => { mainSynth.dispose(); noiseSynth.dispose(); filter.dispose(); filterEnv.dispose(); reverb.dispose(); compressor.dispose(); }, }; }; /** * APPROACH 4: Electric Piano (Rhodes-style) * Classic electric piano sound with tremolo */ const createElectricPiano = () => { const epSynth = new Tone.PolySynth(Tone.FMSynth, { harmonicity: 2, modulationIndex: 1.5, oscillator: { type: 'sine' }, envelope: { attack: 0.001, decay: 1, sustain: 0.3, release: 2, }, modulation: { type: 'sine' }, modulationEnvelope: { attack: 0.001, decay: 0.5, sustain: 0.2, release: 1, }, }); // Classic EP effects const tremolo = new Tone.Tremolo(4, 0.3).start(); const chorus = new Tone.Chorus(1.5, 3.5, 0.7).start(); const eq = new Tone.EQ3({ low: -3, mid: 2, high: 1 }); const reverb = new Tone.Reverb({ decay: 1.5, wet: 0.2 }); epSynth.volume.value = -6; epSynth.chain(tremolo, chorus, eq, reverb, Tone.Destination); return { play: (note: string, duration: number, velocity: number) => { epSynth.triggerAttackRelease(note, duration, undefined, velocity * 0.7); }, dispose: () => { epSynth.dispose(); tremolo.dispose(); chorus.dispose(); eq.dispose(); reverb.dispose(); }, }; }; /** * APPROACH 5: Bells/Celesta Style * Bright, bell-like tones perfect for holiday music! */ const createBellPiano = () => { const bellSynth = new Tone.PolySynth(Tone.FMSynth, { harmonicity: 8, modulationIndex: 20, oscillator: { type: 'sine' }, envelope: { attack: 0.001, decay: 0.8, sustain: 0.1, release: 2, }, modulation: { type: 'sine' }, modulationEnvelope: { attack: 0.001, decay: 0.4, sustain: 0, release: 1, }, }); // Shimmer effect const chorus = new Tone.Chorus(3, 5, 0.5).start(); const reverb = new Tone.Reverb({ decay: 4, wet: 0.4 }); const delay = new Tone.FeedbackDelay('8n', 0.2); bellSynth.volume.value = -10; bellSynth.chain(chorus, delay, reverb, Tone.Destination); return { play: (note: string, duration: number, velocity: number) => { bellSynth.triggerAttackRelease(note, duration * 0.8, undefined, velocity * 0.5); }, dispose: () => { bellSynth.dispose(); chorus.dispose(); reverb.dispose(); delay.dispose(); }, }; }; // ============================================================================= // SYNTH TYPE SELECTOR // ============================================================================= type SynthType = 'layered' | 'fm' | 'physical' | 'electric' | 'bell'; const createPiano = (type: SynthType) => { switch (type) { case 'layered': return createLayeredPiano(); case 'fm': return createFMPiano(); case 'physical': return createPhysicalPiano(); case 'electric': return createElectricPiano(); case 'bell': return createBellPiano(); default: return createLayeredPiano(); } }; // ============================================================================= // CHANGE THIS TO SWITCH SOUNDS // ============================================================================= const SELECTED_SYNTH: SynthType = 'layered'; // ============================================================================= // HELPER: Check if key is pressed at current frame // ============================================================================= const isKeyPressed = ( note: string, frame: number ): { pressed: boolean; intensity: number; velocity: number } => { for (const event of MELODY) { if (event.note === note) { const endFrame = event.startFrame + event.duration; if (frame >= event.startFrame && frame < endFrame) { const noteProgress = (frame - event.startFrame) / event.duration; const intensity = interpolate(noteProgress, [0, 0.1, 0.3, 1], [1, 1, 0.8, 0.6], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return { pressed: true, intensity, velocity: event.velocity }; } } } return { pressed: false, intensity: 0, velocity: 0 }; }; // ============================================================================= // PARTICLE SYSTEM // ============================================================================= const seededRandom = (seed: number): number => { const x = Math.sin(seed * 9999) * 10000; return x - Math.floor(x); }; type Particle = { x: number; y: number; size: number; speed: number; delay: number; hue: number; }; const createParticles = (count: number): Particle[] => { return Array.from({ length: count }, (_, i) => ({ x: seededRandom(i * 1) * 100, y: seededRandom(i * 2) * 100, size: seededRandom(i * 3) * 4 + 2, speed: seededRandom(i * 4) * 0.5 + 0.3, delay: seededRandom(i * 5) * 60, hue: seededRandom(i * 6) * 60 + 180, })); }; const PARTICLES = createParticles(50); // ============================================================================= // COMPONENTS // ============================================================================= const PianoKey: React.FC<{ keyDef: KeyDef; frame: number; }> = ({ keyDef, frame }) => { const { pressed, intensity, velocity } = isKeyPressed(keyDef.note, frame); const pressAnimation = interpolate(pressed ? intensity : 0, [0, 1], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // Glow intensity based on velocity const glowIntensity = pressed ? velocity : 0; if (keyDef.isBlack) { return (
); } return (
{pressed && (
)}
); }; const NoteVisualizer: React.FC<{ frame: number }> = ({ frame }) => { const visibleNotes = MELODY.filter((event) => { const distanceFromStart = frame - event.startFrame; return distanceFromStart > -90 && distanceFromStart < event.duration + 30; }); const keyboardStartX = (1920 - KEYBOARD_WIDTH) / 2; return (
{visibleNotes.map((event, idx) => { const keyDef = KEYS.find((k) => k.note === event.note && !k.isBlack); if (!keyDef) return null; const distanceFromStart = frame - event.startFrame; const y = interpolate(distanceFromStart, [-90, 0], [0, 400], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const opacity = interpolate( distanceFromStart, [-90, -60, 0, event.duration], [0, 1, 1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', } ); const noteHeight = (event.duration / FRAMES_PER_BEAT) * 40; const noteWidth = 20 + event.velocity * 15; return (
); })}
); }; const BackgroundParticles: React.FC<{ frame: number }> = ({ frame }) => { return ( <> {PARTICLES.map((particle, i) => { const yOffset = ((frame + particle.delay) * particle.speed) % 120; const opacity = interpolate(yOffset, [0, 20, 100, 120], [0, 0.6, 0.6, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return (
); })} ); }; const Title: React.FC<{ frame: number }> = ({ frame }) => { const titleOpacity = interpolate(frame, [0, 30], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.easeOut, }); const titleY = interpolate(frame, [0, 30], [-30, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.easeOut, }); const synthNames: Record = { layered: 'Layered Piano', fm: 'FM Piano', physical: 'Physical Model', electric: 'Electric Piano', bell: 'Celesta Bells', }; return (

🎄 JINGLE BELLS 🎄

{synthNames[SELECTED_SYNTH]}

); }; // ============================================================================= // AUDIO COMPONENT // ============================================================================= const PianoAudio: React.FC = () => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const pianoRef = useRef | null>(null); const scheduledNotesRef = useRef>(new Set()); useEffect(() => { pianoRef.current = createPiano(SELECTED_SYNTH); return () => { pianoRef.current?.dispose(); }; }, []); useEffect(() => { if (!pianoRef.current) return; const currentTime = frame / fps; MELODY.forEach((event) => { const noteStartTime = event.startFrame / fps; const noteDuration = event.duration / fps; const noteId = `${event.note}-${event.startFrame}`; if ( currentTime >= noteStartTime && currentTime < noteStartTime + 0.05 && !scheduledNotesRef.current.has(noteId) ) { scheduledNotesRef.current.add(noteId); pianoRef.current?.play(event.note, noteDuration, event.velocity); } }); }, [frame, fps]); return null; }; // ============================================================================= // MAIN COMPONENT // ============================================================================= const EnhancedPianoSynth: React.FC = () => { const frame = useCurrentFrame(); const { width, height } = useVideoConfig(); const keyboardY = height - WHITE_KEY_HEIGHT - 100; const keyboardX = (width - KEYBOARD_WIDTH) / 2; return (
<div style={{ position: 'absolute', top: keyboardY - 400, left: 0, right: 0 }}> <NoteVisualizer frame={frame} /> </div> <div style={{ position: 'absolute', top: keyboardY, left: keyboardX, width: KEYBOARD_WIDTH, height: WHITE_KEY_HEIGHT, }} > {KEYS.filter((k) => !k.isBlack).map((keyDef) => ( <PianoKey key={keyDef.note} keyDef={keyDef} frame={frame} /> ))} {KEYS.filter((k) => k.isBlack).map((keyDef) => ( <PianoKey key={keyDef.note} keyDef={keyDef} frame={frame} /> ))} </div> <div style={{ position: 'absolute', top: keyboardY + WHITE_KEY_HEIGHT, left: keyboardX - 20, width: KEYBOARD_WIDTH + 40, height: 60, background: 'linear-gradient(180deg, #2a2a2a 0%, #1a1a1a 100%)', borderRadius: '0 0 12px 12px', boxShadow: '0 10px 40px rgba(0,0,0,0.5)', }} /> <PianoAudio /> </AbsoluteFill> ); }; export default EnhancedPianoSynth;