All files / src/mod player.ts

0% Statements 0/65
0% Branches 0/24
0% Functions 0/6
0% Lines 0/62

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169                                                                                                                                                                                                                                                                                                                                                 
import { SoundSystem } from "../digital/audio";
import { createPlayHeadClock, toSignature, Signature, createTempo, TickCallbackContainer } from "../metronome";
import { samplePlayer } from "./audio";
import { Song } from "./types";
import { SoundOn, NoOp, Tracker, Track } from "../digital/types";

type MODSong = {
  readonly song: Song;
  /** 4/4, 4/8, 7/12, etc */
  timeSignature: Signature;
  /** Beats per minute. Mod track commands can (and will)
   * change this value. */
  currentBpm: number
  currentPattern: number,
  currentRow: number,
}
export type MODRunWithTracks = MODSong & Tracker;
export type MODRun = MODRunWithTracks & TickCallbackContainer;
 
///////////////////////////////////////////
 
export function createRun(system: SoundSystem, song: Song): MODRun {
  if (song.type !== 'M.K.' && song.type !== 'FLT4') {
    throw new Error("Currently only support M.K mod files (4 tracks)");
  }

  const run: MODRun = {
    song: song,
    timeSignature: toSignature("4/4"),
    currentBpm: 125,
    currentPattern: 0,
    currentRow: 0,
    tracks: [],
    output: system.context().createGain(),
    tickListeners: [],
  };
 
  for (let i = 0; i < 4; i++) {
    const pan = system.context().createPanner();
    pan.orientationX.setValueAtTime(0, 0);
    pan.orientationY.setValueAtTime(0, 0);
    pan.orientationZ.setValueAtTime(-1, 0);
    pan.positionX.setValueAtTime(0, 0);
    pan.positionY.setValueAtTime(0, 0);
    pan.positionZ.setValueAtTime(0, 0);
 
    const vol = system.context().createGain();
    pan.connect(vol);
    vol.connect(run.output);

    vol.gain.setValueAtTime(1, 0);

    const t = {
      // TODO: wrong now with the type change
      lastFn: [[NoOp]],
      idx: 0,
      vol,
      pan,
    } as Track;

    run.tracks.push(t);
  }

  // connect the song output to the speakers
  run.output.connect(system.masterGainNode());
  return run;
}
 
/** Clean up after the song run has played */
export function destroyRun(system: SoundSystem, run: MODRun) {
  run.output.disconnect(system.masterGainNode());
}

const flushTracks = (run: MODRun) => {
  for (let i = 0; i < 4; i++) {
    run.tracks[i].lastFn[0][0]();
    run.tracks[i].lastFn[0][0] = NoOp;
  }
}
 
const useInstrument = (system: SoundSystem, run: MODRun, which: number): SoundOn => {
  return samplePlayer(system, run.song, which);
};
 
export function insertRun(system: SoundSystem, run: MODRun) {
  let tempo = createTempo(
    480,
    500000, // default 120bpm
  );
 
  run.tickListeners.push(
    (tick, time, bi) => {
      // run.currentBpm = run.bpm; // + 65;

      // Metronome example
      if (bi.beat === 1 && bi.tick === 0) {
        // random("A-5", time, .2);
      }
 
      // A row should play on a beat.
      if (bi.tick % 4 === 0) {
        // random("Eb5", time, .2);
        const pos = run.currentRow; // 
 
        // position should advance by one row on a beat
        if (pos < 64) {
          // const l = run.currentBpm / 60 / 4;
 
          for (let c = 0; c < 4; c++) {
            const n1 = run.song.patterns[run.currentPattern][run.currentRow][c];
 
            ///////////////////////////////////////////
            // Look at all commands...
            if (n1.effect.command === 0xF) {  // tempo
              /*
               *   00 -\
               *   01  | One beat, 4 division,
               *   02  | ticks in between (default 6)
               *   03 -/
               *
               *                    24 * beats/minute
               * divisions/minute = -----------------
               *                    ticks/division
               */
              let z = n1.effect.argX * 16 + n1.effect.argY;
              if (z === 0) z = 1;
              if (z <= 32) {
                // ticks / divisions
                tempo = createTempo(z * 6, tempo.tempo);
                // run.timeSignature.noteBeats = z;
              } else {
                console.log("bpm:", z, z + 65);
                // run.currentBpm = z; // + 65;
              }
            }
            if (n1.effect.command === 0xC) { // volume
              const volume = (n1.effect.argX * 16 + n1.effect.argY) / 64;
              run.tracks[c].vol.gain.setValueAtTime(volume, time);
            }
            ///////////////////////////////////////////
 
            // run.tracks[c].vol.gain.setValueAtTime(1, time);
            // const p = c % 2 === 0 ? 1 : -1;
            // run.tracks[c].pan.positionX.setValueAtTime(p, time);
 
            if (n1.note !== "") {
              const instrument = useInstrument(system, run, n1.sample - 1);
              run.tracks[c].lastFn[0][0]();
              run.tracks[c].lastFn[0][0] = NoOp;
              run.tracks[c].lastFn[0][0] = instrument(n1.note, time, run.tracks[c].pan);
            }
          }
 
          // TODO: cycle patterns...
          run.currentRow = pos + 1;
        } else if (pos === 64) {
          flushTracks(run);
          // TODO: on done...
        }
      }
 
      // console.log(run.currentPattern.toString(16), run.currentRow.toString(16));
      return [tempo, run.timeSignature];
    }
  );
 
  createPlayHeadClock(system, tempo, run.timeSignature, run);
}