App Kotlin Multiplatform (Android/iOS/Desktop) con Compose Multiplatform: controllo intonazione (YIN), pratica guidata, metronomo, ear training, sfide, scansione spartiti via AI, spartiti bundled e pentagramma con glifi tipografici, travature, battute e spaziatura proporzionale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
214 lines
6.8 KiB
JavaScript
214 lines
6.8 KiB
JavaScript
import { writeFileSync } from "fs";
|
|
|
|
const SAMPLE_RATE = 44100;
|
|
const DURATION = 30; // seconds
|
|
const TOTAL_SAMPLES = SAMPLE_RATE * DURATION;
|
|
|
|
// Note frequencies (Hz)
|
|
const FREQ = {
|
|
C3: 130.81, E3: 164.81, G3: 196.0,
|
|
C4: 261.63, D4: 293.66, E4: 329.63, F4: 349.23,
|
|
G4: 392.0, A4: 440.0, B4: 493.88, C5: 523.25,
|
|
C2: 65.41, G2: 98.0,
|
|
};
|
|
|
|
// C major scale for the staff scene
|
|
const SCALE = [FREQ.C4, FREQ.D4, FREQ.E4, FREQ.F4, FREQ.G4, FREQ.A4, FREQ.B4, FREQ.C5];
|
|
|
|
function envelope(t, attack, sustain, release, totalDur) {
|
|
if (t < attack) return t / attack;
|
|
if (t < attack + sustain) return 1.0;
|
|
const rel = t - attack - sustain;
|
|
if (rel < release) return 1.0 - rel / release;
|
|
return 0;
|
|
}
|
|
|
|
function softSine(t, freq) {
|
|
// Sine with soft harmonics for a piano-like tone
|
|
return (
|
|
Math.sin(2 * Math.PI * freq * t) * 0.6 +
|
|
Math.sin(2 * Math.PI * freq * 2 * t) * 0.2 +
|
|
Math.sin(2 * Math.PI * freq * 3 * t) * 0.1 +
|
|
Math.sin(2 * Math.PI * freq * 4 * t) * 0.05
|
|
);
|
|
}
|
|
|
|
function padTone(t, freq) {
|
|
// Very soft, warm pad sound
|
|
return (
|
|
Math.sin(2 * Math.PI * freq * t) * 0.5 +
|
|
Math.sin(2 * Math.PI * freq * 1.001 * t) * 0.3 + // slight detune for warmth
|
|
Math.sin(2 * Math.PI * freq * 2 * t) * 0.1
|
|
);
|
|
}
|
|
|
|
// Simple reverb via comb filter
|
|
function applyReverb(samples, delay, decay) {
|
|
const delaySamples = Math.floor(delay * SAMPLE_RATE);
|
|
const out = new Float64Array(samples.length);
|
|
for (let i = 0; i < samples.length; i++) {
|
|
out[i] = samples[i];
|
|
if (i >= delaySamples) {
|
|
out[i] += out[i - delaySamples] * decay;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Generate samples
|
|
const left = new Float64Array(TOTAL_SAMPLES);
|
|
const right = new Float64Array(TOTAL_SAMPLES);
|
|
|
|
for (let i = 0; i < TOTAL_SAMPLES; i++) {
|
|
const t = i / SAMPLE_RATE; // time in seconds
|
|
let sampleL = 0;
|
|
let sampleR = 0;
|
|
|
|
// === 1. Ambient pad (entire duration, fades in/out) ===
|
|
const padEnv =
|
|
envelope(t, 3.0, 22.0, 5.0, DURATION) * 0.12;
|
|
|
|
// C major chord pad (C3, E3, G3)
|
|
const pad =
|
|
padTone(t, FREQ.C3) * 0.4 +
|
|
padTone(t, FREQ.E3) * 0.3 +
|
|
padTone(t, FREQ.G3) * 0.3;
|
|
|
|
sampleL += pad * padEnv;
|
|
sampleR += pad * padEnv;
|
|
|
|
// Sub bass
|
|
const bassEnv = envelope(t, 4.0, 20.0, 6.0, DURATION) * 0.08;
|
|
sampleL += Math.sin(2 * Math.PI * FREQ.C2 * t) * bassEnv;
|
|
sampleR += Math.sin(2 * Math.PI * FREQ.C2 * t) * bassEnv;
|
|
|
|
// === 2. Intro shimmer (0-6s) ===
|
|
if (t < 7) {
|
|
const shimmerEnv = envelope(t, 1.0, 3.5, 2.5, 7.0) * 0.06;
|
|
const shimmer =
|
|
Math.sin(2 * Math.PI * FREQ.G4 * t) * 0.5 +
|
|
Math.sin(2 * Math.PI * FREQ.C5 * t) * 0.3 +
|
|
Math.sin(2 * Math.PI * FREQ.E4 * t) * 0.2;
|
|
// Slight stereo spread
|
|
sampleL += shimmer * shimmerEnv * 1.1;
|
|
sampleR += shimmer * shimmerEnv * 0.9;
|
|
}
|
|
|
|
// === 3. Scale notes during staff scene (6-15s) ===
|
|
const scaleStart = 6.0; // seconds
|
|
const noteDuration = 1.0; // each note lasts 1s
|
|
for (let n = 0; n < SCALE.length; n++) {
|
|
const noteStart = scaleStart + n * noteDuration;
|
|
const noteT = t - noteStart;
|
|
if (noteT >= 0 && noteT < 2.0) {
|
|
// Note with decay
|
|
const noteEnv = envelope(noteT, 0.02, 0.3, 1.5, 2.0) * 0.22;
|
|
const note = softSine(t, SCALE[n]);
|
|
// Add fifth harmonic quietly
|
|
const fifth = softSine(t, SCALE[n] * 1.5) * 0.08;
|
|
sampleL += (note + fifth) * noteEnv;
|
|
sampleR += (note + fifth) * noteEnv;
|
|
}
|
|
}
|
|
|
|
// === 4. Feature section arpeggios (14-24s) ===
|
|
if (t >= 14 && t < 24) {
|
|
const arpNotes = [FREQ.C4, FREQ.E4, FREQ.G4, FREQ.C5];
|
|
const arpSpeed = 0.6; // seconds per note
|
|
const arpT = t - 14;
|
|
const arpIndex = Math.floor(arpT / arpSpeed) % arpNotes.length;
|
|
const noteT = arpT % arpSpeed;
|
|
const arpEnv = envelope(noteT, 0.01, 0.15, 0.4, arpSpeed) * 0.1;
|
|
const fadeInOut =
|
|
envelope(t - 14, 1.5, 6.0, 2.5, 10.0);
|
|
const arp = softSine(t, arpNotes[arpIndex]);
|
|
// Alternate stereo slightly
|
|
sampleL += arp * arpEnv * fadeInOut * (arpIndex % 2 === 0 ? 1.1 : 0.9);
|
|
sampleR += arp * arpEnv * fadeInOut * (arpIndex % 2 === 0 ? 0.9 : 1.1);
|
|
}
|
|
|
|
// === 5. Outro resolution chord (23-30s) ===
|
|
if (t >= 23) {
|
|
const outroT = t - 23;
|
|
const chordEnv = envelope(outroT, 2.0, 2.5, 2.5, 7.0) * 0.15;
|
|
// Rich C major chord
|
|
const chord =
|
|
padTone(t, FREQ.C3) * 0.25 +
|
|
padTone(t, FREQ.E3) * 0.2 +
|
|
padTone(t, FREQ.G3) * 0.2 +
|
|
padTone(t, FREQ.C4) * 0.2 +
|
|
padTone(t, FREQ.E4) * 0.1 +
|
|
padTone(t, FREQ.G4) * 0.05;
|
|
sampleL += chord * chordEnv;
|
|
sampleR += chord * chordEnv;
|
|
|
|
// High sparkle at the end
|
|
if (outroT > 1.0 && outroT < 4.0) {
|
|
const sparkleEnv = envelope(outroT - 1.0, 0.5, 1.0, 1.5, 3.0) * 0.04;
|
|
sampleL += Math.sin(2 * Math.PI * FREQ.C5 * 2 * t) * sparkleEnv;
|
|
sampleR += Math.sin(2 * Math.PI * FREQ.G4 * 2 * t) * sparkleEnv;
|
|
}
|
|
}
|
|
|
|
left[i] = sampleL;
|
|
right[i] = sampleR;
|
|
}
|
|
|
|
// Apply simple reverb
|
|
const reverbL = applyReverb(left, 0.08, 0.3);
|
|
const reverbR = applyReverb(right, 0.11, 0.28);
|
|
|
|
// Mix dry + wet
|
|
for (let i = 0; i < TOTAL_SAMPLES; i++) {
|
|
left[i] = left[i] * 0.65 + reverbL[i] * 0.35;
|
|
right[i] = right[i] * 0.65 + reverbR[i] * 0.35;
|
|
}
|
|
|
|
// Normalize
|
|
let maxAmp = 0;
|
|
for (let i = 0; i < TOTAL_SAMPLES; i++) {
|
|
maxAmp = Math.max(maxAmp, Math.abs(left[i]), Math.abs(right[i]));
|
|
}
|
|
const normFactor = maxAmp > 0 ? 0.85 / maxAmp : 1;
|
|
|
|
// Convert to 16-bit PCM WAV
|
|
const numChannels = 2;
|
|
const bitsPerSample = 16;
|
|
const byteRate = SAMPLE_RATE * numChannels * (bitsPerSample / 8);
|
|
const blockAlign = numChannels * (bitsPerSample / 8);
|
|
const dataSize = TOTAL_SAMPLES * numChannels * (bitsPerSample / 8);
|
|
const fileSize = 44 + dataSize;
|
|
|
|
const buffer = Buffer.alloc(fileSize);
|
|
let offset = 0;
|
|
|
|
// RIFF header
|
|
buffer.write("RIFF", offset); offset += 4;
|
|
buffer.writeUInt32LE(fileSize - 8, offset); offset += 4;
|
|
buffer.write("WAVE", offset); offset += 4;
|
|
|
|
// fmt chunk
|
|
buffer.write("fmt ", offset); offset += 4;
|
|
buffer.writeUInt32LE(16, offset); offset += 4;
|
|
buffer.writeUInt16LE(1, offset); offset += 2; // PCM
|
|
buffer.writeUInt16LE(numChannels, offset); offset += 2;
|
|
buffer.writeUInt32LE(SAMPLE_RATE, offset); offset += 4;
|
|
buffer.writeUInt32LE(byteRate, offset); offset += 4;
|
|
buffer.writeUInt16LE(blockAlign, offset); offset += 2;
|
|
buffer.writeUInt16LE(bitsPerSample, offset); offset += 2;
|
|
|
|
// data chunk
|
|
buffer.write("data", offset); offset += 4;
|
|
buffer.writeUInt32LE(dataSize, offset); offset += 4;
|
|
|
|
// Interleaved stereo samples
|
|
for (let i = 0; i < TOTAL_SAMPLES; i++) {
|
|
const l = Math.max(-1, Math.min(1, left[i] * normFactor));
|
|
const r = Math.max(-1, Math.min(1, right[i] * normFactor));
|
|
buffer.writeInt16LE(Math.round(l * 32767), offset); offset += 2;
|
|
buffer.writeInt16LE(Math.round(r * 32767), offset); offset += 2;
|
|
}
|
|
|
|
writeFileSync("public/music.wav", buffer);
|
|
console.log(`Generated music.wav (${(fileSize / 1024 / 1024).toFixed(1)} MB, ${DURATION}s stereo)`);
|