Source: wave/player.js

import PN from "../pn.js";
import { Note, Tune } from "./encoder.js";
import { first } from "./harmonic.js";
import { Instruments } from "../instruments/instruments.js";
import parseSongInput from "../input/parseStringInput.js";
import pitchFrequencies from "../pitchFrequencies.json";
import { readWavFile } from "../input/wavProcessor.js";

const pitch = {
  a4: 440,
  c4: 423,
  c5: 523,
  e3: 449,
  e4: 559,
  e5: 659,
  f2: 398,
  f5: 698,
  b4: 494,
  b2: 294,
  b3: 394,
  b5: 988,
  a3: 220,
  a5: 880,
  g5: 784,
  d3: 190,
  d4: 293,
  c3: 131,
  g3: 196,
  g4: 296,
  f3: 174,
  f4: 274,
};

/**
 * Creates a musical note with the given note name.
 * If no instrument is selected, it defaults to Piano.
 *
 * @param {string|number} noteName - The name or number of the musical note (e.g., "A4", "440").
 * @returns {Note|undefined} - A new Note instance with the specified pitch, or undefined if the note is not found.
 *
 * @example
 * // Select an instrument and create a note
 * PN.instrument('cello');    // Select piano instrument
 * const note = createNote('C4'); // Creates a note with frequency for C4
 * console.log(note);         // Logs the created note
 * PN.save();
 */

function createNote(noteName) {
  // Set default instrument to piano if no instrument is selected
  if (!PN.currentInstrument) {
    console.log("No instrument selected, defaulting to Banjo.");
    PN.currentInstrument = new Instruments().Banjo(); // Default to piano
  }

  var frequency = pitchFrequencies[noteName];
  if (!frequency) {
    frequency = parseInt(noteName);
    if (isNaN(frequency)) {
      console.log(`Note ${noteName} not found!`);
      return;
    }
  }

  if (typeof noteName === "number") {
    frequency = noteName;
  }
  if (!frequency) {
    console.log(`Please enter a number or musical note`);
    return;
  }

  const note = new Note(
    [frequency], // Frequency for the note
    [0], // No accidentals
    PN.duration, // Use PN's duration
    PN.currentInstrument, // Use the selected instrument's envelope
    PN.harmonic1,
    PN.harmonic2,
    PN.step,
    PN.filter,
    PN.filter2,
    PN.width1,
    PN.width2,
    PN.filterEnv,
    PN.lfoWave,
    PN.glideTime,
    PN.volume, // Use PN's volume level
    PN.multiplier,
    PN.noiseLevel,
    PN.noiseState,
    PN.ratio,
  );

  console.log(`Created note ${noteName} with frequency ${frequency}`);
  return note;
}

// Function to create a note
function makeNote(noteString, section, score) {
  let length = section.length || score.length;
  let volume = section.volume || score.volume;
  let instrument = section.instrument || score.instrument;
  let harmonic = section.harmonic || score.harmonic;
  let harmonic1 = PN.harmonic1;
  let harmonic2 = PN.harmonic2;
  let step = PN.step;
  let filter = PN.filter;
  let filter2 = PN.filter2;
  let width1 = PN.width1;
  let width2 = PN.width2;
  let filterEnv = PN.filterEnv;
  let lfoWave = PN.lfoWave;
  let glideTime = PN.glideTime;
  let multiplier = PN.multiplier;
  let noiseLevel = PN.noiseLevel;
  let noiseState = PN.noiseState;
  let ratio = PN.ratio;

  const note = new Note(
    pitch,
    [],
    length,
    PN.currentInstrument,
    harmonic1,
    harmonic2,
    step,
    filter,
    filter2,
    width1,
    width2,
    filterEnv,
    lfoWave,
    glideTime,
    volume,
    multiplier,
    noiseLevel,
    noiseState,
    ratio,
  );

  const parts = noteString.split(":");
  const noteLength = parts.length > 1 ? parseFloat(parts[0]) : 1.0;
  const pitches = parts.length > 1 ? parts[1] : parts[0];

  note.length *= noteLength;

  if (pitches.includes("-")) {
    const chords = pitches.split("-");
    chords.forEach((chord) => process(note, chord));
  } else {
    process(note, pitches);
  }

  return note;
}

// Function to process a single note or chord
function process(note, pitchStr) {
  pitchStr = pitchStr.toUpperCase();
  if (pitchStr === "Z") return; // Rest

  if (pitchStr.length < 2 || pitchStr.length > 3) {
    throw new Error(`Invalid note structure: ${pitchStr}`);
  }

  const basePitch = pitchStr.slice(0, 2);
  if (!pitchFrequencies[basePitch]) {
    throw new Error(`Note doesn't exist: ${basePitch}`);
  }

  note.pitch.push(pitchFrequencies[basePitch]);

  const accidental = pitchStr.slice(-1);
  if (accidental === "#") {
    note.accidental.push(1);
  } else if (accidental === "b") {
    note.accidental.push(-1);
  } else {
    note.accidental.push(0);
  }
}

/**
 * Creates a song based on the input data.
 * The song is constructed from a series of note strings and durations.
 * Notes are processed and encoded into a Tune instance.
 *
 * @param {Array} songData - An array of song sections and note data, in string format.
 * @returns {Array} - The encoded song data.
 *
 * @example
 * // Define song data
 * const songData = [
 *  "ch1[1.5:A4-F5]",
 *  "ch2[0.5:C4]",
 *  "ch1[2.0:G3-E4-D4]"
 * ];
 *
 * // Create a song
 * PN.setVolume(0.5); // Set volume level
 * const song = createSong(songData); // Create a song from the input data
 * console.log(song); // Logs the encoded song data
 */

// Helper to create a song
function createSong(songData) {
  const score = parseSongInput(songData);
  const myTune = new Tune(score.key, [], []);

  for (const section of score.sections) {
    for (const n of section.C1) {
      const note = makeNote(n, section, score);
      myTune.ch1.push(note);
    }
    for (const n of section.C2) {
      const note = makeNote(n, section, score);
      myTune.ch2.push(note);
    }
  }

  // Concatenate all notes to form the song
  var songDataOutput = myTune.encodePlane();
  PN.songDataOutput = songDataOutput;
  return songDataOutput;
}

/**
 * Creates a musical note with the given note name.
 * If no instrument is selected, it defaults to Piano.
 *
 * @param {string} noteName - The name of audio file you wish to replicate.
 * @returns {File|undefined} - A new audio file instance with the specified pitch, or undefined if the note is not found.
 *
 * @example
 * // Select an instrument and create a note
 *      // Wrap everything in an async function
 *      async function runPNExample() {
 *          console.log(PN);  // This should print the PN object
 *           PN.instrument('Piano'); // Select the instrument
 *           // PN.setVolume(0.5); // Set volume (optional)
 *
 *           // Wait for PN.singVoice to complete
 *           const song = await PN.singVoice('recording2.wav');
 *
 *           console.log("Song created:", song);
 *           console.log(PN.volume);  // Logs the current volume
 *               setTimeout(() => {
 *                   PN.save(); // Call save after the delay
 *                   saveLogToFile(logMessages);
 *
 *               }, 8000); // Delay in milliseconds (5000ms = 5s)
 *       }
 *
 *       // Run the function
 *       runPNExample();
 */

function singVoice(audioFile) {
  const numChannels = 100;

  readWavFile(audioFile, (voiceFrequencies) => {
    const skeleton = voiceFrequencies;

    const myTune = new Tune(PN.key, ...Array(numChannels).fill([]));
    const channels = Array.from(
      { length: numChannels },
      (_, i) => myTune[`ch${i + 1}`],
    );

    for (var i = 0; i < skeleton.length; i++) {
      console.log(`Voice number ${i + 1}`);

      skeleton[i].forEach((data, index) => {
        const note = new Note(
          [Math.round(data[0])], // Frequency
          [0], // No accidentals
          data[2], // Duration
          PN.currentInstrument, // Oscillators
          PN.harmonic1,
          PN.harmonic2,
          PN.step,
          PN.filter,
          PN.filter2,
          PN.width1,
          PN.width2,
          PN.filterEnv,
          PN.lfoWave,
          PN.glideTime,
          data[1], // Volume
          PN.multiplier,
          PN.noiseLevel,
          PN.noiseState,
          PN.ratio,
        );

        if (note) {
          channels[i].push(note);
        }
      });
    }

    const songDataOutput = myTune.encode();
    PN.songDataOutput = songDataOutput;
    return songDataOutput;
  });
}

export { createNote, createSong, singVoice };