UNPKG

nanananana

Version:

Play the Batman theme song from your terminal

1,016 lines (900 loc) 42.2 kB
#!/usr/bin/env node /** * nanananana - Play the Batman theme song from your terminal * This program uses native Node.js capabilities to play the sound * without any external dependencies. */ // ANSI escape sequences for some styling const RESET = '\x1b[0m'; const YELLOW = '\x1b[33m'; const BOLD = '\x1b[1m'; const BLACK_BG = '\x1b[40m'; // Define the notes frequencies across multiple octaves // Octave 2 const NOTE_C2 = 65.41; const NOTE_Cs2 = 69.30; // C#2 const NOTE_Db2 = 69.30; // Same as C#2 const NOTE_D2 = 73.42; const NOTE_Ds2 = 77.78; // D#2 const NOTE_Eb2 = 77.78; // Same as D#2 const NOTE_E2 = 82.41; const NOTE_F2 = 87.31; const NOTE_Fs2 = 92.50; // F#2 const NOTE_Gb2 = 92.50; // Same as F#2 const NOTE_G2 = 98.00; const NOTE_Gs2 = 103.83; // G#2 const NOTE_Ab2 = 103.83; // Same as G#2 const NOTE_A2 = 110.00; const NOTE_As2 = 116.54; // A#2 const NOTE_Bb2 = 116.54; // Same as A#2 const NOTE_B2 = 123.47; // Octave 3 const NOTE_C3 = 130.81; const NOTE_Cs3 = 138.59; // C#3 const NOTE_Db3 = 138.59; // Same as C#3 const NOTE_D3 = 146.83; const NOTE_Ds3 = 155.56; // D#3 const NOTE_Eb3 = 155.56; // Same as D#3 const NOTE_E3 = 164.81; const NOTE_F3 = 174.61; const NOTE_Fs3 = 185.00; // F#3 const NOTE_Gb3 = 185.00; // Same as F#3 const NOTE_G3 = 196.00; const NOTE_Gs3 = 207.65; // G#3 const NOTE_Ab3 = 207.65; // Same as G#3 const NOTE_A3 = 220.00; const NOTE_As3 = 233.08; // A#3 const NOTE_Bb3 = 233.08; // Same as A#3 const NOTE_B3 = 246.94; // Octave 4 (middle C is C4) const NOTE_C4 = 261.63; const NOTE_Cs4 = 277.18; // C#4 const NOTE_Db4 = 277.18; // Same as C#4 const NOTE_D4 = 293.66; const NOTE_Ds4 = 311.13; // D#4 const NOTE_Eb4 = 311.13; // Same as D#4 const NOTE_E4 = 329.63; const NOTE_F4 = 349.23; const NOTE_Fs4 = 369.99; // F#4 const NOTE_Gb4 = 369.99; // Same as F#4 const NOTE_G4 = 392.00; const NOTE_Gs4 = 415.30; // G#4 const NOTE_Ab4 = 415.30; // Same as G#4 const NOTE_A4 = 440.00; // A440 concert pitch standard const NOTE_As4 = 466.16; // A#4 const NOTE_Bb4 = 466.16; // Same as A#4 const NOTE_B4 = 493.88; // Octave 5 const NOTE_C5 = 523.25; const NOTE_Cs5 = 554.37; // C#5 const NOTE_Db5 = 554.37; // Same as C#5 const NOTE_D5 = 587.33; const NOTE_Ds5 = 622.25; // D#5 const NOTE_Eb5 = 622.25; // Same as D#5 const NOTE_E5 = 659.26; const NOTE_F5 = 698.46; const NOTE_Fs5 = 739.99; // F#5 const NOTE_Gb5 = 739.99; // Same as F#5 const NOTE_G5 = 783.99; const NOTE_Gs5 = 830.61; // G#5 const NOTE_Ab5 = 830.61; // Same as G#5 const NOTE_A5 = 880.00; const NOTE_As5 = 932.33; // A#5 const NOTE_Bb5 = 932.33; // Same as A#5 const NOTE_B5 = 987.77; // Octave 6 const NOTE_C6 = 1046.50; const NOTE_Cs6 = 1108.73; // C#6 const NOTE_Db6 = 1108.73; // Same as C#6 const NOTE_D6 = 1174.66; const NOTE_Ds6 = 1244.51; // D#6 const NOTE_Eb6 = 1244.51; // Same as D#6 const NOTE_E6 = 1318.51; const NOTE_F6 = 1396.91; const NOTE_Fs6 = 1479.98; // F#6 const NOTE_Gb6 = 1479.98; // Same as F#6 const NOTE_G6 = 1567.98; const NOTE_Gs6 = 1661.22; // G#6 const NOTE_Ab6 = 1661.22; // Same as G#6 const NOTE_A6 = 1760.00; const NOTE_As6 = 1864.66; // A#6 const NOTE_Bb6 = 1864.66; // Same as A#6 const NOTE_B6 = 1975.53; const basicSongParts = { lowNanaNanaNanaNana: [ [[NOTE_G3, NOTE_D4, NOTE_D5], 0.22], [[NOTE_G3, NOTE_D4, NOTE_D5], 0.22], [[NOTE_G3, NOTE_Db4, NOTE_Db5], 0.22], [[NOTE_G3, NOTE_Db4, NOTE_Db5], 0.22], [[NOTE_G3, NOTE_C4, NOTE_C5], 0.22], [[NOTE_G3, NOTE_C4, NOTE_C5], 0.22], [[NOTE_G3, NOTE_Db4, NOTE_Db5], 0.22], [[NOTE_G3, NOTE_Db4, NOTE_Db5], 0.22], ], lowNanaNanaNanaNanaWithBatman: [ [[NOTE_G3, NOTE_D4, NOTE_D5], 0.22, [NOTE_G4, NOTE_D5, NOTE_F5, NOTE_G5], 0.44], // left hand, left hand duration, right hand, diff right hand notes duration [[NOTE_G3, NOTE_D4, NOTE_D5], 0.22], // if no right hand, play only left hand and wait for right hand previous notes durations to finish [[NOTE_G3, NOTE_Db4, NOTE_Db5], 0.22, [NOTE_G4, NOTE_D5, NOTE_F5, NOTE_G5], 1.44], // left hand, left hand duration, right hand, longer right hand duration [[NOTE_G3, NOTE_Db4, NOTE_Db5], 0.22], [[NOTE_G3, NOTE_C4, NOTE_C5], 0.22], [[NOTE_G3, NOTE_C4, NOTE_C5], 0.22], [[NOTE_G3, NOTE_Db4, NOTE_Db5], 0.22], [[NOTE_G3, NOTE_Db4, NOTE_Db5], 0.22], ] }; // Batman theme notation (frequency, duration in seconds) // The classic "Na na na na na na na na BATMAN!" // The classic "Na na na na na na na na BATMAN!" const basicTheme = [ ...basicSongParts.lowNanaNanaNanaNana, ...basicSongParts.lowNanaNanaNanaNana, ...basicSongParts.lowNanaNanaNanaNanaWithBatman, ...basicSongParts.lowNanaNanaNanaNana, ...basicSongParts.lowNanaNanaNanaNana, ...basicSongParts.lowNanaNanaNanaNanaWithBatman, ...basicSongParts.lowNanaNanaNanaNana, ...basicSongParts.lowNanaNanaNanaNana, ...basicSongParts.lowNanaNanaNanaNanaWithBatman, ...basicSongParts.lowNanaNanaNanaNana, ...basicSongParts.lowNanaNanaNanaNana, ...basicSongParts.lowNanaNanaNanaNanaWithBatman, ...basicSongParts.lowNanaNanaNanaNana, ...basicSongParts.lowNanaNanaNanaNana, ...basicSongParts.lowNanaNanaNanaNanaWithBatman, ...basicSongParts.lowNanaNanaNanaNana, ...basicSongParts.lowNanaNanaNanaNana, ...basicSongParts.lowNanaNanaNanaNanaWithBatman, ]; /** * Creates a PCM Wave data for a sound of specified frequency and duration * @param {number|number[]|null} freq - The frequency in Hz or array of frequencies for chords, null for silence * @param {number} duration - The duration in seconds * @param {number} sampleRate - The sample rate (default 44100) * @param {Object} options - Additional options for sound generation * @returns {Int16Array} - PCM audio data */ function createWave(freq, duration, sampleRate = 44100, options = {}) { const MAX_AMPLITUDE = 32760; // Maximum amplitude for 16-bit audio (slightly below max to prevent clipping) const numSamples = Math.ceil(sampleRate * duration); const buffer = new Int16Array(numSamples); // Set default options const opts = { fadeIn: 0.005, // 5ms default fade in (as fraction of duration) fadeOut: 0.05, // 5% default fade out (as fraction of duration) sustainLevel: 0.8, // Volume level during sustain phase (80% for piano-like sound) decayRate: 0.5, // How much the sound decays over time (0 = no decay, 1 = full decay) attackCurve: 0.8, // Attack curve shape (higher = more curved, more piano-like) releaseCurve: 0.7, // Release curve shape (higher = more curved) harmonicRichness: 0.3, // Amount of harmonic content (0-1) ...options }; // Handle silence/rest case if (freq === null) { return buffer; // Return a buffer of zeros (silence) } // Handle single note case directly if (!Array.isArray(freq)) { // Simple sine wave for a single frequency for (let i = 0; i < numSamples; i++) { // Calculate envelope with piano-like ADSR (Attack, Decay, Sustain, Release) const position = i / numSamples; // Position in the sample (0 to 1) let envelope = 1.0; // Improved piano-like envelope const attackTime = Math.min(opts.fadeIn * numSamples, numSamples * 0.05); // Max 5% for attack const decayTime = Math.min(0.15 * numSamples, numSamples * 0.3); // 15-30% decay phase const releaseStartPosition = 0.9; // Start release at 90% of the duration const releaseTime = numSamples * (1 - releaseStartPosition); // Last 10% for release if (i < attackTime) { // Attack phase - curved rise for piano-like attack const normalizedPosition = i / attackTime; envelope = Math.pow(normalizedPosition, opts.attackCurve); // Curved attack } else if (i < attackTime + decayTime) { // Decay phase - exponential falloff to sustain level const decayPosition = (i - attackTime) / decayTime; envelope = 1.0 - ((1.0 - opts.sustainLevel) * decayPosition); } else if (position > releaseStartPosition) { // Release phase - exponential falloff const releasePosition = (i - (numSamples * releaseStartPosition)) / releaseTime; envelope = opts.sustainLevel * Math.pow(1.0 - releasePosition, opts.releaseCurve); } else { // Sustain phase with gentle decay throughout const sustainPosition = (i - (attackTime + decayTime)) / (numSamples * releaseStartPosition - (attackTime + decayTime)); const decayAmount = opts.decayRate * sustainPosition * (1.0 - opts.sustainLevel) * 0.3; envelope = Math.max(opts.sustainLevel * 0.7, opts.sustainLevel - decayAmount); } buffer[i] = Math.round(MAX_AMPLITUDE * envelope * Math.sin(2 * Math.PI * freq * i / sampleRate)); } return buffer; } // For chords, use a different approach with harmonics const frequencies = Array.isArray(freq) ? freq : [freq]; // Sort frequencies from lowest to highest const sortedFrequencies = [...frequencies].sort((a, b) => a - b); // Get the root (lowest) frequency const rootFrequency = sortedFrequencies[0]; // Generate a more complex waveform with harmonics based on the root note // This creates a richer sound that sounds like a chord without the beating effect for (let i = 0; i < numSamples; i++) { // Apply piano-like envelope const position = i / numSamples; // Position in the sample (0 to 1) let envelope = 1.0; // Improved piano-like envelope const attackTime = Math.min(opts.fadeIn * numSamples, numSamples * 0.05); // Max 5% for attack const decayTime = Math.min(0.15 * numSamples, numSamples * 0.3); // 15-30% decay phase const releaseStartPosition = 0.9; // Start release at 90% of the duration const releaseTime = numSamples * (1 - releaseStartPosition); // Last 10% for release if (i < attackTime) { // Attack phase - curved rise for piano-like attack const normalizedPosition = i / attackTime; envelope = Math.pow(normalizedPosition, opts.attackCurve); // Curved attack } else if (i < attackTime + decayTime) { // Decay phase - exponential falloff to sustain level const decayPosition = (i - attackTime) / decayTime; envelope = 1.0 - ((1.0 - opts.sustainLevel) * decayPosition); } else if (position > releaseStartPosition) { // Release phase - exponential falloff const releasePosition = (i - (numSamples * releaseStartPosition)) / releaseTime; envelope = opts.sustainLevel * Math.pow(1.0 - releasePosition, opts.releaseCurve); } else { // Sustain phase with gentle decay throughout const sustainPosition = (i - (attackTime + decayTime)) / (numSamples * releaseStartPosition - (attackTime + decayTime)); const decayAmount = opts.decayRate * sustainPosition * (1.0 - opts.sustainLevel) * 0.3; envelope = Math.max(opts.sustainLevel * 0.7, opts.sustainLevel - decayAmount); } // Start with the fundamental frequency (root note) let sample = Math.sin(2 * Math.PI * rootFrequency * i / sampleRate); // Add all the chord tones for (let j = 1; j < sortedFrequencies.length; j++) { const noteFreq = sortedFrequencies[j]; // Find the ratio between frequencies (important for harmonic relationships) const ratio = noteFreq / rootFrequency; // Adjust amplitude based on position in the chord (higher notes are softer) // This simulates the natural attenuation of higher frequencies in a piano const noteAmplitude = 0.7 / Math.sqrt(j + 1); // Add the fundamental of this note sample += noteAmplitude * Math.sin(2 * Math.PI * noteFreq * i / sampleRate); // Add the first overtone (one octave higher) for richness // Higher notes have less pronounced overtones (like a real piano) const overtoneStrength = opts.harmonicRichness * (1.0 - (j / sortedFrequencies.length) * 0.5); sample += noteAmplitude * overtoneStrength * 0.3 * Math.sin(2 * Math.PI * noteFreq * 2 * i / sampleRate); // Add subtle second overtone (octave + fifth) sample += noteAmplitude * overtoneStrength * 0.15 * Math.sin(2 * Math.PI * noteFreq * 3 * i / sampleRate); // Add harmonic interactions based on interval relationships // Perfect fifth (~1.5 ratio) if (ratio > 1.4 && ratio < 1.6) { sample += 0.12 * Math.sin(2 * Math.PI * rootFrequency * 1.5 * i / sampleRate); } // Octave (~2.0 ratio) if (ratio > 1.9 && ratio < 2.1) { sample += 0.15 * Math.sin(2 * Math.PI * rootFrequency * 2 * i / sampleRate); } // Major third (~1.25 ratio) if (ratio > 1.24 && ratio < 1.27) { sample += 0.10 * Math.sin(2 * Math.PI * rootFrequency * 1.25 * i / sampleRate); } } // Add a bit of the first and second overtones for all chords (makes them sound richer) const baseOvertone = 0.2 * opts.harmonicRichness; sample += baseOvertone * Math.sin(2 * Math.PI * rootFrequency * 2 * i / sampleRate); sample += baseOvertone * 0.5 * Math.sin(2 * Math.PI * rootFrequency * 3 * i / sampleRate); // Piano strings also vibrate slightly at inharmonic frequencies // This subtle effect adds realism to the sound if (opts.harmonicRichness > 0.2) { const inharmonicity = 0.05 * opts.harmonicRichness; sample += inharmonicity * Math.sin(2 * Math.PI * rootFrequency * 2.01 * i / sampleRate); sample += inharmonicity * 0.5 * Math.sin(2 * Math.PI * rootFrequency * 3.02 * i / sampleRate); } // Normalize the sample between -1 and 1 // Account for all added harmonics to prevent clipping const totalHarmonics = 1.0 + baseOvertone * 1.5 + (opts.harmonicRichness > 0.2 ? 0.075 * opts.harmonicRichness : 0); const normalizationFactor = totalHarmonics + (sortedFrequencies.length - 1) * 0.7; sample = Math.max(-1.0, Math.min(1.0, sample / normalizationFactor)); // Apply envelope and scale to our amplitude range buffer[i] = Math.round(MAX_AMPLITUDE * envelope * sample); } return buffer; } /** * Plays a tone of specified frequency for a given duration * @param {number|number[]|null} freq - The frequency in Hz or array of frequencies for chords, null for silence * @param {number} duration - The duration in seconds * @param {Object} options - Additional options for sound generation * @returns {Promise} - Resolves when the tone finishes playing */ async function playTone(freq, duration, options = {}) { return new Promise(resolve => { // Handle silence/rest case if (freq === null) { setTimeout(resolve, duration * 1000); return; } // Calculate envelope options based on duration const envelopeOptions = { // For longer notes, increase the decay rate for a more natural piano-like sound decayRate: Math.min(0.7, duration * 0.5), // More decay for longer notes ...options }; if (process.platform === 'win32') { // Windows has a native beep function, but it can only play one frequency at a time // For chords on Windows, we'll use the root frequency for simplicity if (Array.isArray(freq)) { // Get the most important frequency from the chord (usually the lowest) const mainFreq = freq[0]; const { exec } = require('child_process'); const playDuration = Math.round(duration * 1000); // Play the main note of the chord exec(`powershell -c "[console]::beep(${Math.round(mainFreq)}, ${playDuration})"`, () => resolve()); if (playDuration > 50) { setTimeout(resolve, playDuration - 10); } } else { // Single note on Windows const { exec } = require('child_process'); const playDuration = Math.round(duration * 1000); exec(`powershell -c "[console]::beep(${Math.round(freq)}, ${playDuration})"`, () => resolve()); if (playDuration > 50) { setTimeout(resolve, playDuration - 10); } } } else { // For other platforms we can properly mix frequencies into a single wave const sampleRate = 44100; const wave = createWave(freq, duration, sampleRate, envelopeOptions); // Convert to Buffer for stdout const buffer = Buffer.from(wave.buffer); // On macOS, we can use afplay with a process if (process.platform === 'darwin') { const fs = require('fs'); const { spawn } = require('child_process'); const tempFile = `/tmp/nananana-${Date.now()}.wav`; // Simple WAV header (44 bytes) const header = Buffer.alloc(44); // RIFF chunk descriptor header.write('RIFF', 0); header.writeUInt32LE(36 + buffer.length, 4); // File size header.write('WAVE', 8); // "fmt " sub-chunk header.write('fmt ', 12); header.writeUInt32LE(16, 16); // Length of format data header.writeUInt16LE(1, 20); // PCM format header.writeUInt16LE(1, 22); // Mono channel header.writeUInt32LE(sampleRate, 24); // Sample rate header.writeUInt32LE(sampleRate * 2, 28); // Byte rate (SampleRate * NumChannels * BitsPerSample/8) header.writeUInt16LE(2, 32); // Block align (NumChannels * BitsPerSample/8) header.writeUInt16LE(16, 34); // Bits per sample // "data" sub-chunk header.write('data', 36); header.writeUInt32LE(buffer.length, 40); // Data size // Write the WAV file fs.writeFileSync(tempFile, Buffer.concat([header, buffer])); // Play with afplay const play = spawn('afplay', [tempFile]); play.on('close', () => { // Cleanup the temp file try { fs.unlinkSync(tempFile); } catch (e) { // Ignore errors in cleanup } resolve(); }); } else { // For Linux, we can't really do chords with the terminal bell // So we'll just use the first frequency if it's an array process.stdout.write('\u0007'); // Try playing through aplay if available (for better Linux support) if (Array.isArray(freq)) { try { const { spawnSync } = require('child_process'); // Check if aplay is available const checkAplay = spawnSync('which', ['aplay']); if (checkAplay.status === 0) { // aplay exists, try to use it for better sound quality const fs = require('fs'); const { spawn } = require('child_process'); const tempFile = `/tmp/nananana-${Date.now()}.wav`; // Create a simple WAV file similar to macOS approach const header = Buffer.alloc(44); // Same WAV header as in macOS block header.write('RIFF', 0); header.writeUInt32LE(36 + buffer.length, 4); header.write('WAVE', 8); header.write('fmt ', 12); header.writeUInt32LE(16, 16); header.writeUInt16LE(1, 20); header.writeUInt16LE(1, 22); header.writeUInt32LE(sampleRate, 24); header.writeUInt32LE(sampleRate * 2, 28); header.writeUInt16LE(2, 32); header.writeUInt16LE(16, 34); header.write('data', 36); header.writeUInt32LE(buffer.length, 40); // Write and play fs.writeFileSync(tempFile, Buffer.concat([header, buffer])); spawn('aplay', [tempFile]); // Cleanup after playing setTimeout(() => { try { fs.unlinkSync(tempFile); } catch (e) { } }, duration * 1000 + 100); } } catch (err) { // Fall back to default behavior } } // Wait for the duration to finish setTimeout(resolve, duration * 1000); } } }); } /** * Plays a sequence of notes with the specified timings * @param {Array} sequence - Array of [frequency, duration] pairs or [leftHand, leftDuration, rightHand, rightDuration] */ async function playSequence(sequence) { for (const item of sequence) { if (item.length === 2) { // Standard format: [frequency, duration] const [freq, duration] = item; await playTone(freq, duration); // Minimal gap between notes (or no gap at all) await new Promise(resolve => setTimeout(resolve, 5)); } else if (item.length === 4) { // Piano-style format: [leftHand, leftDuration, rightHand, rightDuration] const [leftHandFreq, leftHandDuration, rightHandFreq, rightHandDuration] = item; // Start both hands simultaneously and track them independently const leftHandPromise = playTone(leftHandFreq, leftHandDuration); const rightHandPromise = playTone(rightHandFreq, rightHandDuration); // Wait for both to complete (the longer of the two will determine the total time) await Promise.all([leftHandPromise, rightHandPromise]); // Minimal gap between notes await new Promise(resolve => setTimeout(resolve, 5)); } } } /** * Displays Batman ASCII art */ function displayBatmanLogo() { const lines = ` > var alfredNumber = 867-5309; > callAlfred(alfredNumber); > ... ${YELLOW}${BOLD} > NaN NaN NaN NaN NaN NaN NaN NaN ${BLACK_BG} ____________________________________________________________ | _, _ _ ,_ | | .o888P Y8o8Y Y888o. BBBBB A TTTTTTT | | d88888 88888 88888b B B A A I | | d888888b_ _d88888b_ _d888888b BBBBB A A I | | 8888888888888888888888888888888 B B AAAAAAA I | | 8888888888888888888888888888888 BBBBB A A I | | 8888888888888888888888888888888 | | 'JGS8P"Y888P"Y888P"Y888P"Y8888' MM MM A NN N | | Y888 '8' Y8P '8' 888Y MMM MMM A A N N N | | "8o V o8" M M M AAAAA N N N | | ' ' M M A A N NN | |____________________________________________________________| ${RESET} `.split('\n'); // prints a line each quarter second while music is playing lines.forEach((line, index) => { setTimeout(() => { process.stdout.write(line + '\n'); }, index * ((120 / 152) * 1000)); }); } /** * Main function to play the Batman theme */ async function playBatmanTheme() { // Display Batman logo displayBatmanLogo(); // Check if we should play the extended theme const args = process.argv.slice(2); const playBasic = args.includes('--basic'); if (playBasic) { // Play the classic theme if (process.platform === 'darwin') { await playSequenceAsOneFile(basicTheme); } else { await playSequence(basicTheme); } } else { const enhancedTheme = createPianoStyleBatmanTheme(); if (process.platform === 'darwin') { await playSequenceAsOneFile(enhancedTheme); } else { await playSequence(enhancedTheme); } } } /** * For macOS, plays all notes as a single continuous WAV file * This eliminates gaps between notes completely * @param {Array} sequence - Array of [frequency, duration] pairs or [leftHand, leftDuration, rightHand, rightDuration] */ async function playSequenceAsOneFile(sequence) { if (process.platform !== 'darwin') { return playSequence(sequence); } const fs = require('fs'); const { spawn } = require('child_process'); const sampleRate = 44100; // Calculate total duration and sample count let totalDuration = 0; for (const item of sequence) { if (item.length === 2) { // Standard [freq, duration] format totalDuration += item[1]; } else if (item.length === 4) { // Piano-style [leftHand, leftDuration, rightHand, rightDuration] // Use the maximum of the two durations totalDuration += Math.max(item[1], item[3]); } } const totalSamples = Math.ceil(sampleRate * totalDuration); // Create a buffer for the entire sequence const buffer = new Int16Array(totalSamples); // Fill the buffer with the sequence of notes let sampleIndex = 0; // Track the previous note for crossfading let prevNoteBuffer = null; let crossfadeLength = Math.floor(sampleRate * 0.01); // 10ms crossfade for (const item of sequence) { if (item.length === 2) { // Standard [freq, duration] format const [freq, duration] = item; // Generate the current note with piano envelope based on duration const envelopeOptions = { decayRate: Math.min(0.7, duration * 0.5) // More decay for longer notes }; const noteBuffer = createWave(freq, duration, sampleRate, envelopeOptions); // Apply crossfade if we have a previous note if (prevNoteBuffer && crossfadeLength > 0) { // We'll only crossfade at the beginning of this note // to smoothly transition from the previous note for (let i = 0; i < Math.min(crossfadeLength, noteBuffer.length); i++) { const fadeRatio = i / crossfadeLength; // 0 to 1 const prevFadeRatio = 1 - fadeRatio; // Crossfade the last few samples of prev note with first few of current if (sampleIndex - crossfadeLength + i >= 0 && sampleIndex - crossfadeLength + i < buffer.length) { buffer[sampleIndex - crossfadeLength + i] = Math.round((prevNoteBuffer[prevNoteBuffer.length - crossfadeLength + i] * prevFadeRatio) + (noteBuffer[i] * fadeRatio)); } } } // Copy the main part of this note (after any crossfade section) for (let i = (prevNoteBuffer ? crossfadeLength : 0); i < noteBuffer.length; i++) { if (sampleIndex < buffer.length) { buffer[sampleIndex++] = noteBuffer[i]; } } // Save this note for possible crossfade with the next prevNoteBuffer = noteBuffer; } else if (item.length === 4) { // Piano-style [leftHand, leftDuration, rightHand, rightDuration] const [leftHandFreq, leftHandDuration, rightHandFreq, rightHandDuration] = item; // Generate buffers for both hands with appropriate envelopes const leftHandOptions = { decayRate: Math.min(0.7, leftHandDuration * 0.5) // More decay for longer notes }; const rightHandOptions = { decayRate: Math.min(0.7, rightHandDuration * 0.5) // More decay for longer notes }; const leftHandBuffer = createWave(leftHandFreq, leftHandDuration, sampleRate, leftHandOptions); const rightHandBuffer = createWave(rightHandFreq, rightHandDuration, sampleRate, rightHandOptions); // Mix the two hands together with crossfade from previous const maxDuration = Math.max(leftHandDuration, rightHandDuration); const maxSamples = Math.ceil(sampleRate * maxDuration); const mixedBuffer = new Int16Array(maxSamples); // First mix the two hands together for (let i = 0; i < maxSamples; i++) { let sample = 0; // Add left hand if still playing if (i < leftHandBuffer.length) { sample += leftHandBuffer[i] * 0.5; // Scale to prevent clipping } // Add right hand if still playing if (i < rightHandBuffer.length) { sample += rightHandBuffer[i] * 0.5; // Scale to prevent clipping } mixedBuffer[i] = Math.min(32767, Math.max(-32768, sample)); // Prevent clipping } // Apply crossfade if we have a previous note if (prevNoteBuffer && crossfadeLength > 0) { for (let i = 0; i < Math.min(crossfadeLength, mixedBuffer.length); i++) { const fadeRatio = i / crossfadeLength; // 0 to 1 const prevFadeRatio = 1 - fadeRatio; if (sampleIndex - crossfadeLength + i >= 0 && sampleIndex - crossfadeLength + i < buffer.length) { buffer[sampleIndex - crossfadeLength + i] = Math.round((prevNoteBuffer[prevNoteBuffer.length - crossfadeLength + i] * prevFadeRatio) + (mixedBuffer[i] * fadeRatio)); } } } // Copy the mixed buffer to the main buffer for (let i = (prevNoteBuffer ? crossfadeLength : 0); i < mixedBuffer.length; i++) { if (sampleIndex < buffer.length) { buffer[sampleIndex++] = mixedBuffer[i]; } } // Save this note for possible crossfade with the next prevNoteBuffer = mixedBuffer; } } // Convert the filled buffer to a Node.js Buffer const audioBuffer = Buffer.from(buffer.buffer); // Create a temporary WAV file const tempFile = `/tmp/nananana-full-${Date.now()}.wav`; // Create WAV header const header = Buffer.alloc(44); // RIFF chunk descriptor header.write('RIFF', 0); header.writeUInt32LE(36 + audioBuffer.length, 4); // File size header.write('WAVE', 8); // "fmt " sub-chunk header.write('fmt ', 12); header.writeUInt32LE(16, 16); // Length of format data header.writeUInt16LE(1, 20); // PCM format header.writeUInt16LE(1, 22); // Mono channel header.writeUInt32LE(sampleRate, 24); // Sample rate header.writeUInt32LE(sampleRate * 2, 28); // Byte rate (SampleRate * NumChannels * BitsPerSample/8) header.writeUInt16LE(2, 32); // Block align (NumChannels * BitsPerSample/8) header.writeUInt16LE(16, 34); // Bits per sample // "data" sub-chunk header.write('data', 36); header.writeUInt32LE(audioBuffer.length, 40); // Data size // Write the complete WAV file fs.writeFileSync(tempFile, Buffer.concat([header, audioBuffer])); // Play the file return new Promise(resolve => { const play = spawn('afplay', [tempFile]); play.on('close', () => { // Cleanup try { fs.unlinkSync(tempFile); } catch (e) { // Ignore cleanup errors } resolve(); }); }); } /** * Creates a demonstration of piano-style playing with independent left and right hands */ function createPianoStyleDemo() { return [ // Simple left hand / right hand combination [[NOTE_C3, NOTE_E3, NOTE_G3], 0.4, [NOTE_E4, NOTE_G4], 0.2], // Left hand chord with right hand melody [null, 0, [NOTE_C5], 0.2], // Only right hand plays [[NOTE_G2, NOTE_B2, NOTE_D3], 0.4, [NOTE_D4, NOTE_G4], 0.2], // Next chord with melody [null, 0, [NOTE_B4], 0.2], // Only right hand plays // More complex example with longer right hand notes over changing left hand [[NOTE_C3, NOTE_G3], 0.3, [NOTE_E4, NOTE_G4, NOTE_C5], 0.9], // Right hand holds longer [[NOTE_G2, NOTE_D3], 0.3, null, 0], // Left hand changes while right hand continues [[NOTE_E2, NOTE_B2], 0.3, null, 0], // Left hand changes again // Final chord [[NOTE_C3, NOTE_E3, NOTE_G3, NOTE_C4], 0.5, [NOTE_E4, NOTE_G4, NOTE_C5], 0.8] // Final resolution with right hand lingering ]; } /** * Creates an enhanced piano-style Batman theme with independent left and right hand parts * This uses the piano-style format: [leftHand, leftDuration, rightHand, rightDuration] */ function createPianoStyleBatmanTheme() { // Define the left hand bass accompaniment /** * default duration (152BPM = 1.579s per whole note) */ const BPM = 152; const wholeNote = (240 / BPM) * 1.10; // 1.579 sec + 10% for sustain const halfNote = wholeNote / 2; const quarterNote = wholeNote / 4; const eightNote = wholeNote / 8; // Define the theme with piano-style independent hands return [ // Na na na na na na na na [[NOTE_G2, NOTE_D3], eightNote, [NOTE_D4], eightNote], // Na [[NOTE_G2, NOTE_D3], eightNote, [NOTE_D4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_F2, NOTE_C3], eightNote, [NOTE_C4], eightNote], // na [[NOTE_F2, NOTE_C3], eightNote, [NOTE_C4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na // 2s // Na na na na na na na na [[NOTE_G2, NOTE_D3], eightNote, [NOTE_D4], eightNote], // Na [[NOTE_G2, NOTE_D3], eightNote, [NOTE_D4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_F2, NOTE_C3], eightNote, [NOTE_C4], eightNote], // na [[NOTE_F2, NOTE_C3], eightNote, [NOTE_C4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na // 3s // Na na na na na na na na [[NOTE_G2, NOTE_D3], eightNote, [NOTE_D4], eightNote], // Na [[NOTE_G2, NOTE_D3], eightNote, [NOTE_D4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_F2, NOTE_C3], eightNote, [NOTE_C4], eightNote], // na [[NOTE_F2, NOTE_C3], eightNote, [NOTE_C4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na // 5s // Na na na na na na na na [[NOTE_G2, NOTE_D3], eightNote, [NOTE_D4], eightNote], // Na [[NOTE_G2, NOTE_D3], eightNote, [NOTE_D4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_F2, NOTE_C3], eightNote, [NOTE_C4], eightNote], // na [[NOTE_F2, NOTE_C3], eightNote, [NOTE_C4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na // 7s // BATMAN! [[NOTE_G2, NOTE_D3], eightNote, [NOTE_G3, NOTE_D4, NOTE_F4, NOTE_G4], quarterNote], // BAT- [[NOTE_G2, NOTE_D3], eightNote], [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_G3, NOTE_D4, NOTE_F4, NOTE_G4], 3*quarterNote], // MAN! [[NOTE_Gb2, NOTE_Db3], eightNote], [[NOTE_F2, NOTE_C3], eightNote], [[NOTE_F2, NOTE_C3], eightNote], [[NOTE_Gb2, NOTE_Db3], eightNote], [[NOTE_Gb2, NOTE_Db3], eightNote], // 8s // Na na na na na na na na [[NOTE_G2, NOTE_D3], eightNote, NOTE_D4, eightNote], // Na [[NOTE_G2, NOTE_D3], eightNote, NOTE_D4, eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, NOTE_Db4, eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_F2, NOTE_C3], eightNote, [NOTE_C4], eightNote], // na [[NOTE_F2, NOTE_C3], eightNote, [NOTE_C4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na // 10s // BATMAN! [[NOTE_G2, NOTE_D3], eightNote, [NOTE_G3, NOTE_D4, NOTE_F4, NOTE_G4], quarterNote], // BAT- [[NOTE_G2, NOTE_D3], eightNote], [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_G3, NOTE_D4, NOTE_F4, NOTE_G4], 3*quarterNote], // MAN! [[NOTE_Gb2, NOTE_Db3], eightNote], [[NOTE_F2, NOTE_C3], eightNote], [[NOTE_F2, NOTE_C3], eightNote], [[NOTE_Gb2, NOTE_Db3], eightNote], [[NOTE_Gb2, NOTE_Db3], eightNote], // 12s // Na na na na na na na na [[NOTE_G2, NOTE_D3], eightNote, [NOTE_D4], eightNote], // Na [[NOTE_G2, NOTE_D3], eightNote, [NOTE_D4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_F2, NOTE_C3], eightNote, [NOTE_C4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na // 13s // BATMAN! [[NOTE_C3, NOTE_G3], eightNote, [NOTE_C4, NOTE_G4, NOTE_Bb4, NOTE_C5], quarterNote], // BAT- [[NOTE_C3, NOTE_G3], eightNote], [[NOTE_C3, NOTE_Gb3], eightNote], [[NOTE_C3, NOTE_Gb3], eightNote, [NOTE_C4, NOTE_G4, NOTE_Bb4, NOTE_C5], 3*quarterNote], // MAN! [[NOTE_C3, NOTE_F3], eightNote], [[NOTE_C3, NOTE_F3], eightNote], [[NOTE_C3, NOTE_Gb3], eightNote], [[NOTE_C3, NOTE_Gb3], eightNote], // 15s // Na na na na na na na na [[NOTE_C3, NOTE_G3], eightNote], // Na [[NOTE_C3, NOTE_G3], eightNote], // na [[NOTE_C3, NOTE_Gb3], eightNote], // na [[NOTE_C3, NOTE_Gb3], eightNote], // na [[NOTE_C3, NOTE_F3], eightNote], // na [[NOTE_C3, NOTE_F3], eightNote], // na [[NOTE_C3, NOTE_Gb3], eightNote], // na [[NOTE_C3, NOTE_G3], eightNote], // na // 16s // BATMAN! [[NOTE_G2, NOTE_D3], eightNote, [NOTE_G3, NOTE_D4, NOTE_F4, NOTE_G4], quarterNote], // BAT- [[NOTE_G2, NOTE_D3], eightNote], [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_G3, NOTE_D4, NOTE_F4, NOTE_G4], 3*quarterNote], // MAN! [[NOTE_Gb2, NOTE_Db3], eightNote], [[NOTE_F2, NOTE_C3], eightNote], [[NOTE_F2, NOTE_C3], eightNote], [[NOTE_Gb2, NOTE_Db3], eightNote], [[NOTE_Gb2, NOTE_Db3], eightNote], // 18s // Na na na na na na na na [[NOTE_G2, NOTE_D3], eightNote, [NOTE_D4], eightNote], // Na [[NOTE_G2, NOTE_D3], eightNote, [NOTE_D4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na [[NOTE_F2, NOTE_C3], eightNote, [NOTE_C4], eightNote], // na [[NOTE_Gb2, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // na // 19s [[NOTE_D3, NOTE_A3], eightNote, [NOTE_D4, NOTE_A4, NOTE_C5, NOTE_D5], quarterNote], // BAT- [[NOTE_D3, NOTE_A3], eightNote], [[NOTE_D3, NOTE_Ab3], eightNote, [NOTE_D4, NOTE_A4, NOTE_C5, NOTE_D5], quarterNote], // MAN! [[NOTE_D3, NOTE_Ab3], eightNote], [[NOTE_D3, NOTE_G3], eightNote], [[NOTE_D3, NOTE_G3], eightNote, [NOTE_D4, NOTE_Ab4, NOTE_A4, NOTE_D5], eightNote], [[NOTE_D3, NOTE_Ab3], eightNote, [NOTE_D4, NOTE_Ab4, NOTE_A4, NOTE_D5], eightNote], [[NOTE_D3, NOTE_A3], eightNote, [NOTE_D4, NOTE_Ab4, NOTE_A4, NOTE_D5], eightNote], [[NOTE_C3, NOTE_G3], eightNote, [NOTE_C4, NOTE_G4, NOTE_Bb4, NOTE_C5], quarterNote], [[NOTE_C3, NOTE_G3], eightNote], [[NOTE_C3, NOTE_Gb3], eightNote, [NOTE_C4, NOTE_G4, NOTE_Bb4, NOTE_C5], quarterNote], [[NOTE_C3, NOTE_Gb3], eightNote], [[NOTE_C3, NOTE_F3], eightNote], [[NOTE_C3, NOTE_F3], eightNote, [NOTE_C4, NOTE_Gb4, NOTE_G4, NOTE_C5], eightNote], [[NOTE_C3, NOTE_Gb3], eightNote, [NOTE_C4, NOTE_Gb4, NOTE_G4, NOTE_C5], eightNote], [[NOTE_C3, NOTE_G3], eightNote, [NOTE_C4, NOTE_Gb4, NOTE_G4, NOTE_C5], eightNote], // 21s [[NOTE_G2, NOTE_D3], eightNote, [NOTE_G3, NOTE_D4, NOTE_F4, NOTE_G4], quarterNote], // BAT- [[NOTE_D3, NOTE_D3], eightNote], [[NOTE_G2, NOTE_Db3], eightNote, [NOTE_G3, NOTE_D4, NOTE_F4, NOTE_G4], 3*quarterNote], // MAN! [[NOTE_D3, NOTE_Db3], eightNote], [[NOTE_D3, NOTE_C3], eightNote], [[NOTE_D3, NOTE_C3], eightNote], [[NOTE_D3, NOTE_Db3], eightNote], [[NOTE_D3, NOTE_Db3], eightNote], [[NOTE_D3, NOTE_D3], eightNote, [NOTE_D4], eightNote], [[NOTE_D3, NOTE_D3], eightNote, [NOTE_D4], eightNote], [[NOTE_D3, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], [[NOTE_D3, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], [[NOTE_D3, NOTE_C3], eightNote, [NOTE_C4], eightNote], [[NOTE_D3, NOTE_C3], eightNote, [NOTE_C4], eightNote], [[NOTE_D3, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], [[NOTE_D3, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // 24s [[NOTE_G2, NOTE_D3], eightNote, [NOTE_G3, NOTE_D4, NOTE_F4, NOTE_G4], quarterNote], // BAT- [[NOTE_D3, NOTE_D3], eightNote], [[NOTE_G2, NOTE_Db3], eightNote, [NOTE_G3, NOTE_D4, NOTE_F4, NOTE_G4], 3*quarterNote], // MAN! [[NOTE_D3, NOTE_Db3], eightNote], [[NOTE_D3, NOTE_C3], eightNote], [[NOTE_D3, NOTE_C3], eightNote], [[NOTE_D3, NOTE_Db3], eightNote], [[NOTE_D3, NOTE_Db3], eightNote], [[NOTE_D3, NOTE_D3], eightNote, [NOTE_D4], eightNote], [[NOTE_D3, NOTE_D3], eightNote, [NOTE_D4], eightNote], [[NOTE_D3, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], [[NOTE_D3, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], [[NOTE_D3, NOTE_C3], eightNote, [NOTE_C4], eightNote], [[NOTE_D3, NOTE_C3], eightNote, [NOTE_C4], eightNote], [[NOTE_D3, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], [[NOTE_D3, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // 27s [[NOTE_G2, NOTE_D3], eightNote, [NOTE_G3, NOTE_D4, NOTE_F4, NOTE_G4], quarterNote], // BAT- [[NOTE_D3, NOTE_D3], eightNote], [[NOTE_G2, NOTE_Db3], eightNote, [NOTE_G3, NOTE_D4, NOTE_F4, NOTE_G4], 3*quarterNote], // MAN! [[NOTE_D3, NOTE_Db3], eightNote], [[NOTE_D3, NOTE_C3], eightNote], [[NOTE_D3, NOTE_C3], eightNote], [[NOTE_D3, NOTE_Db3], eightNote], [[NOTE_D3, NOTE_Db3], eightNote], [[NOTE_D3, NOTE_D3], eightNote, [NOTE_D4], eightNote], [[NOTE_D3, NOTE_D3], eightNote, [NOTE_D4], eightNote], [[NOTE_D3, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], [[NOTE_D3, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], [[NOTE_D3, NOTE_C3], eightNote, [NOTE_C4], eightNote], [[NOTE_D3, NOTE_C3], eightNote, [NOTE_C4], eightNote], [[NOTE_D3, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], [[NOTE_D3, NOTE_Db3], eightNote, [NOTE_Db4], eightNote], // 31s ]; } // Run the theme const args = process.argv.slice(2); if (args.includes('--help')) { console.log(` ${YELLOW}${BOLD}nananana Theme Player${RESET} run ${BOLD}npx nananana${RESET} to call alfred Options: --basic Play the basic piano-style Batman theme with full piano sound --piano Play a demo of the piano-style playing capabilities --help Show this help message `); process.exit(0); } else if (args.includes('--piano')) { // Run a demo of the piano-style playing (async () => { console.log(`${YELLOW}${BOLD}Demonstrating piano-style playing with independent hands...${RESET}`); const pianoDemo = createPianoStyleDemo(); // On macOS, use the optimized method if (process.platform === 'darwin') { await playSequenceAsOneFile(pianoDemo); } else { await playSequence(pianoDemo); } console.log(`${YELLOW}${BOLD}Piano demo complete!${RESET}`); })(); } else { // Run the Batman theme by default playBatmanTheme().catch(console.error); }