#!/usr/bin/env node

import fs from 'node:fs';
import path from 'node:path';

const args = process.argv.slice(2);

function getArg(flag, fallback) {
  const index = args.indexOf(flag);
  if (index === -1 || index === args.length - 1) {
    return fallback;
  }
  return args[index + 1];
}

const seconds = Number(getArg('--seconds', '10'));
const outPath = path.resolve(getArg('--out', 'audio/norn-coding-bed-test.wav'));
const sampleRate = 44100;
const bpm = 122;
const beatLength = 60 / bpm;
const barLength = beatLength * 4;
const totalSamples = Math.floor(seconds * sampleRate);
const left = new Float32Array(totalSamples);
const right = new Float32Array(totalSamples);

const progression = [
  { root: 45, chord: [57, 60, 64], accent: [69, 72, 76] }, // Am
  { root: 41, chord: [53, 57, 60], accent: [65, 69, 72] }, // F
  { root: 36, chord: [48, 52, 55], accent: [64, 67, 72] }, // C
  { root: 43, chord: [55, 59, 62], accent: [67, 71, 74] }, // G
  { root: 45, chord: [57, 60, 64], accent: [69, 72, 76] }, // Am
];

let noiseSeed = 0x12345678;

function nextNoise() {
  noiseSeed = (1664525 * noiseSeed + 1013904223) >>> 0;
  return (noiseSeed / 0xffffffff) * 2 - 1;
}

function midiToHz(note) {
  return 440 * Math.pow(2, (note - 69) / 12);
}

function clamp(value) {
  return Math.max(-1, Math.min(1, value));
}

function softClip(value) {
  return Math.tanh(value * 1.4);
}

function oscSine(freq, time) {
  return Math.sin(2 * Math.PI * freq * time);
}

function oscTri(freq, time) {
  return (2 / Math.PI) * Math.asin(Math.sin(2 * Math.PI * freq * time));
}

function oscSaw(freq, time) {
  const phase = freq * time;
  return 2 * (phase - Math.floor(phase + 0.5));
}

function expEnv(timeSince, decay) {
  return timeSince < 0 ? 0 : Math.exp(-timeSince * decay);
}

function kickAt(timeSince) {
  if (timeSince < 0 || timeSince > 0.24) {
    return 0;
  }

  const pitch = 132 - 78 * Math.min(timeSince / 0.08, 1);
  const body = oscSine(pitch, timeSince) * expEnv(timeSince, 17);
  const click = oscTri(1900, timeSince) * expEnv(timeSince, 95) * 0.18;
  return body * 0.95 + click;
}

function snareAt(timeSince) {
  if (timeSince < 0 || timeSince > 0.22) {
    return 0;
  }

  const noise = nextNoise() * expEnv(timeSince, 22);
  const body = oscSine(198, timeSince) * expEnv(timeSince, 28) * 0.22;
  return noise * 0.34 + body;
}

function hatAt(timeSince, open) {
  const length = open ? 0.16 : 0.06;
  if (timeSince < 0 || timeSince > length) {
    return 0;
  }

  const decay = open ? 26 : 54;
  const raw = nextNoise();
  const brightness = oscSine(9200, timeSince) * 0.08 + oscSine(6800, timeSince) * 0.06;
  return (raw * 0.17 + brightness) * expEnv(timeSince, decay);
}

function sidechainAmount(time) {
  const phase = time % beatLength;
  return 1 - 0.28 * Math.exp(-phase * 15);
}

function bassNoteForStep(barIndex, stepInBar) {
  const current = progression[barIndex % progression.length];
  const root = current.root;
  const fifth = root + 7;
  const octave = root + 12;
  const pattern = [root, root, fifth, root, octave, root, fifth, root];
  return midiToHz(pattern[stepInBar % pattern.length]);
}

function arpNoteForStep(barIndex, stepInBar) {
  const current = progression[barIndex % progression.length];
  const pattern = [0, 1, 2, 1, 2, 1, 0, 2];
  return midiToHz(current.accent[pattern[stepInBar % pattern.length]]);
}

for (let sample = 0; sample < totalSamples; sample += 1) {
  const time = sample / sampleRate;
  const barIndex = Math.floor(time / barLength) % progression.length;
  const barTime = time % barLength;
  const current = progression[barIndex];

  let mixL = 0;
  let mixR = 0;

  // Pad chord.
  let pad = 0;
  for (const note of current.chord) {
    const freq = midiToHz(note);
    pad += oscSine(freq, time) * 0.12;
    pad += oscSine(freq * 0.5, time) * 0.035;
  }
  pad += oscSine(midiToHz(current.root), time) * 0.03;
  pad *= sidechainAmount(time) * 0.7;
  mixL += pad;
  mixR += pad;

  // Bass pulse on eighth notes.
  const bassStepLength = beatLength / 2;
  const bassStep = Math.floor(barTime / bassStepLength);
  const bassStepStart = bassStep * bassStepLength;
  const bassTimeSince = barTime - bassStepStart;
  const bassGate = bassTimeSince < 0.21 ? 1 : 0;
  const bassFreq = bassNoteForStep(barIndex, bassStep);
  const bassEnv = expEnv(bassTimeSince, 9) * bassGate;
  const bass =
    (oscTri(bassFreq, time) * 0.22 + oscSine(bassFreq * 0.5, time) * 0.1) *
    bassEnv *
    sidechainAmount(time);
  mixL += bass;
  mixR += bass;

  // Bright arpeggio.
  const arpStepLength = beatLength / 2;
  const arpStep = Math.floor(barTime / arpStepLength);
  const arpStepStart = arpStep * arpStepLength;
  const arpTimeSince = barTime - arpStepStart;
  const arpFreq = arpNoteForStep(barIndex, arpStep);
  const arpEnv = Math.min(1, arpTimeSince * 40) * expEnv(arpTimeSince, 10);
  const arp =
    (oscSaw(arpFreq, time) * 0.07 + oscSine(arpFreq * 2, time) * 0.05 + oscSine(arpFreq, time) * 0.04) *
    arpEnv;
  const arpPan = arpStep % 2 === 0 ? -0.22 : 0.22;
  mixL += arp * (1 - arpPan);
  mixR += arp * (1 + arpPan);

  // Kick every beat with an extra pickup before the fourth beat.
  const kickPattern = [0, 1, 2, 2.75, 3];
  let kick = 0;
  for (const beat of kickPattern) {
    kick += kickAt(barTime - beat * beatLength);
  }
  mixL += kick * 0.92;
  mixR += kick * 0.92;

  // Snare on beats 2 and 4.
  let snare = 0;
  for (const beat of [1, 3]) {
    snare += snareAt(barTime - beat * beatLength);
  }
  mixL += snare * 0.75;
  mixR += snare * 0.75;

  // Hats on offbeats with occasional longer accent.
  let hat = 0;
  for (let step = 0; step < 8; step += 1) {
    const start = step * bassStepLength;
    const open = step === 3 || step === 7;
    hat += hatAt(barTime - start, open);
  }
  mixL += hat * 0.4;
  mixR += hat * 0.38;

  // Gentle master fade in/out.
  const fadeIn = Math.min(1, time / 0.35);
  const fadeOut = Math.min(1, (seconds - time) / 0.5);
  const masterEnv = Math.max(0, Math.min(fadeIn, fadeOut));

  left[sample] = softClip(mixL * masterEnv);
  right[sample] = softClip(mixR * masterEnv);
}

let peak = 0;
for (let i = 0; i < totalSamples; i += 1) {
  peak = Math.max(peak, Math.abs(left[i]), Math.abs(right[i]));
}

const targetPeak = 0.92;
const gain = peak > 0 ? targetPeak / peak : 1;

const pcmData = Buffer.alloc(totalSamples * 4);
for (let i = 0; i < totalSamples; i += 1) {
  const l = Math.round(clamp(left[i] * gain) * 32767);
  const r = Math.round(clamp(right[i] * gain) * 32767);
  pcmData.writeInt16LE(l, i * 4);
  pcmData.writeInt16LE(r, i * 4 + 2);
}

const header = Buffer.alloc(44);
header.write('RIFF', 0);
header.writeUInt32LE(36 + pcmData.length, 4);
header.write('WAVE', 8);
header.write('fmt ', 12);
header.writeUInt32LE(16, 16);
header.writeUInt16LE(1, 20);
header.writeUInt16LE(2, 22);
header.writeUInt32LE(sampleRate, 24);
header.writeUInt32LE(sampleRate * 4, 28);
header.writeUInt16LE(4, 32);
header.writeUInt16LE(16, 34);
header.write('data', 36);
header.writeUInt32LE(pcmData.length, 40);

fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, Buffer.concat([header, pcmData]));

console.log(`Wrote ${outPath}`);
