Files
dwroller/scripts/gen-music-placeholders.js

152 lines
5.1 KiB
JavaScript

// gen-music-placeholders.js
// Synthesizes short loopable ambient WAV files (one per mood) using only Node stdlib.
// Output: media/music/<mood>.wav
// Each file: mono, 16-bit, 22050 Hz, ~8 seconds, low-amplitude, non-silent.
// License: CC0 (synthesized placeholder — replace with real DRM-free tracks).
const fs = require('fs');
const path = require('path');
const SAMPLE_RATE = 22050;
const DURATION_SEC = 8;
const NUM_SAMPLES = SAMPLE_RATE * DURATION_SEC;
const BITS_PER_SAMPLE = 16;
const BYTES_PER_SAMPLE = BITS_PER_SAMPLE / 8;
const CHANNELS = 1;
const MOODS = {
combat: {
title: 'Combat — Driving Tension',
// Fast pulsing low sine + harsh high noise burst
generate(i) {
const t = i / SAMPLE_RATE;
const pulse = Math.sin(2 * Math.PI * 80 * t) * 0.15;
const drive = Math.sin(2 * Math.PI * 160 * t) * 0.08;
const noise = (Math.random() * 2 - 1) * 0.04;
const accent = Math.sin(2 * Math.PI * 40 * t) * Math.sin(2 * Math.PI * 2 * t) * 0.1;
return pulse + drive + noise + accent;
},
},
investigation: {
title: 'Investigation — Low Ambient Drone',
generate(i) {
const t = i / SAMPLE_RATE;
const drone1 = Math.sin(2 * Math.PI * 55 * t) * 0.12;
const drone2 = Math.sin(2 * Math.PI * 82.5 * t) * 0.08;
const sub = Math.sin(2 * Math.PI * 30 * t) * 0.1;
const shimmer = Math.sin(2 * Math.PI * 220 * t) * 0.02 * Math.sin(2 * Math.PI * 0.25 * t);
return drone1 + drone2 + sub + shimmer;
},
},
tension: {
title: 'Tension — Ominous Unease',
generate(i) {
const t = i / SAMPLE_RATE;
const low = Math.sin(2 * Math.PI * 45 * t) * 0.12;
const dissonance = Math.sin(2 * Math.PI * 67 * t) * 0.08;
const trill = Math.sin(2 * Math.PI * 330 * t) * 0.03 * (1 + 0.5 * Math.sin(2 * Math.PI * 3 * t));
const scrape = (Math.random() * 2 - 1) * 0.02 * Math.abs(Math.sin(2 * Math.PI * 1.5 * t));
return low + dissonance + trill + scrape;
},
},
victory: {
title: 'Victory — Warm Resolution',
generate(i) {
const t = i / SAMPLE_RATE;
const root = Math.sin(2 * Math.PI * 130.8 * t) * 0.12;
const third = Math.sin(2 * Math.PI * 164.8 * t) * 0.09;
const fifth = Math.sin(2 * Math.PI * 196 * t) * 0.07;
const octave = Math.sin(2 * Math.PI * 261.6 * t) * 0.05;
const warmth = Math.sin(2 * Math.PI * 65.4 * t) * 0.08;
return root + third + fifth + octave + warmth;
},
},
eerie: {
title: 'Eerie — Dissonant Sparse',
generate(i) {
const t = i / SAMPLE_RATE;
const sparse = Math.sin(2 * Math.PI * 110 * t) * 0.06 * (Math.sin(2 * Math.PI * 0.15 * t) > 0.5 ? 1 : 0.05);
const dissonant = Math.sin(2 * Math.PI * 117 * t) * 0.06 * (Math.sin(2 * Math.PI * 0.15 * t) > 0.5 ? 1 : 0.05);
const whisper = (Math.random() * 2 - 1) * 0.015 * (Math.sin(2 * Math.PI * 0.5 * t) > 0 ? 1 : 0);
const sub = Math.sin(2 * Math.PI * 25 * t) * 0.08;
return sparse + dissonant + whisper + sub;
},
},
};
function writeWav(filePath, samples) {
const numBytes = samples.length * BYTES_PER_SAMPLE;
const buf = Buffer.alloc(44 + numBytes);
// RIFF header
buf.write('RIFF', 0);
buf.writeUInt32LE(36 + numBytes, 4);
buf.write('WAVE', 8);
// fmt chunk
buf.write('fmt ', 12);
buf.writeUInt32LE(16, 16);
buf.writeUInt16LE(1, 20); // PCM
buf.writeUInt16LE(CHANNELS, 22);
buf.writeUInt32LE(SAMPLE_RATE, 24);
buf.writeUInt32LE(SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS, 28);
buf.writeUInt16LE(BYTES_PER_SAMPLE * CHANNELS, 32);
buf.writeUInt16LE(BITS_PER_SAMPLE, 34);
// data chunk
buf.write('data', 36);
buf.writeUInt32LE(numBytes, 40);
for (let i = 0; i < samples.length; i++) {
let val = samples[i];
// Clamp to [-1, 1]
if (val > 1) val = 1;
if (val < -1) val = -1;
// Convert to signed 16-bit
const int16 = val >= 0 ? Math.floor(val * 32767) : Math.ceil(val * 32768);
buf.writeInt16LE(int16, 44 + i * BYTES_PER_SAMPLE);
}
fs.writeFileSync(filePath, buf);
}
function generateMood(mood, config) {
const samples = [];
for (let i = 0; i < NUM_SAMPLES; i++) {
samples.push(config.generate(i));
}
return samples;
}
// Main
const outDir = path.join(__dirname, '..', 'media', 'music');
fs.mkdirSync(outDir, { recursive: true });
const manifest = [];
for (const [mood, config] of Object.entries(MOODS)) {
const samples = generateMood(mood, config);
const filePath = path.join(outDir, `${mood}.wav`);
writeWav(filePath, samples);
// Verify non-silent
let maxAmp = 0;
for (const s of samples) {
if (Math.abs(s) > maxAmp) maxAmp = Math.abs(s);
}
console.log(`Generated: ${filePath} (${samples.length} samples, max amplitude: ${maxAmp.toFixed(4)})`);
manifest.push({
mood,
file: `${mood}.wav`,
title: config.title,
license: 'CC0 (synthesized placeholder)',
});
}
// Write manifest
const manifestPath = path.join(outDir, 'manifest.json');
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
console.log(`\nManifest written: ${manifestPath}`);
console.log('Done. All placeholder tracks are CC0 synthesized audio.');