examples/midi/midifile.js
// This is an example of the use MusicAPI's MidiFile class to
// perform .mid files.
//
// Select one of our example files by setting iperf to 0,1,2 (3,4 are stress tests)
// NB: reported programs are 0-indexed, but GeneralMIDI is 1-indexed.
//
const iperf = 0;
let performance = [
  {f: "bwv772.mid", speed: 1, gain: 3, 
    channelProgs: [7], // harpsichord
    // chan:0-11, prog:6  (we rely on multi-voices, single instrument)
  },
  {f: "bach_846.mid", speed: 1, gain: 6, 
    channelProgs: [1,null,26,27,28],
    // chan:0, prog:0
    // chan:2, prog:0 (override:25)
    // chan:3, prog:0 (override:26)
    // chan:4, prog:0 (override:27)
  }, 
  {f: "lotus.mid",  speed: 1, gain: 3, 
    trackProgs: [null, 49, 81, "percussion", "percussion", 25, 26],
    // lotus.mid has 7 tracks, format 1
    // track:1 chan:0 prog:48
    // track:2 chan:8 prog:81
    // track:5 chan:11 prog:25  
    // track:6 chan:11 prog:25  <-- includes re-struck keys
    // inferred: track:3 chan:9
    // inferred: track:4 chan:9
  },
  // This file taxes most systems by design. 
  // performance by reducing the note-release time. Even then it can require
  {f: "Black_MIDI_Team.mid", gain: 3, speed: .5,  R: .05,
    trackProgs: [null, null, 1, 11, 21, 64, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    // uses 17 tracks, 0 and 1 seem unused
    // at one point we need > 1800 live voices 
  },
][iperf];

performance.programMapper = []; // key is chan, value is prog
const {f, programs, speed, gain, channelProgs, trackProgs, R} = performance;
console.log("loading " + f + "...");
let uri = path.join(this.GetCWD(), "midifiles", f);

// instantiate a MidiFile object.
let midiFile = new MidiFile();
// asyncrhonously load th file
let err = await midiFile.Load(uri);
console.log("loading done");
if(err) return;

// Start up audio engine with reverb effect.
let scene = await Ascene.BeginFiber(this);
let dac = scene.GetDAC();
dac.Show();
await dac.LoadPreset({Gain: gain});
let fx = await scene.NewAnode("Hz.Freeverb");
fx.Show();
scene.Chain(fx, dac);

// create instruments, associated with either track or channel.
let progs = trackProgs?.length ? trackProgs : channelProgs;
let mode = trackProgs?.length ? "Track" : "Chan";
let handlers = [];;
for(let i=0;i<progs.length;i++)
{
  let prog = progs[i];
  if(prog != null)
  {
    let inst = await scene.NewAnode("GeneralMIDI", {
      name: `${mode}${i}`,
      preset: {
        presetName:prog,
        R: R ?? .1
      }});
    handlers.push(inst);
    scene.Chain(inst, fx);
    inst.Show();
  }
  else
    handlers.push(null);
}

// MyNH is a class that reroutes midi events to the designated instrument.
// MIDI 1 is constrained to 16 channels so it's conventional to predefine
// a mapping between channel and program for a track.  Some MIDI files
// choose to map between track and program.  Some MIDI files choose to
// change instruments mid-track.  This isn't currently supported.
class MyNH extends NoteHandler
{
  constructor(handlers, mode)
  {
    super();
    this.handlers = handlers;
    this.mode = mode; // track or chan
    this.track = 0;
    if(mode == "Track")
      this.getInst = this.getTrackInst;
    else
      this.getInst = this.getChanInst;
  }

  SetTrack(trk)
  {
    if(this.track != trk)
    {
      this.track = trk;
    }
  }

  SetChannel(chan)
  {
    if(this.chan != chan)
    {
      // console.log("Set Chan " + chan);
      this.chan = chan;
    }
  }

  NoteOn(key, velpct, etime)
  {
    this.getInst(this.chan).NoteOn(key, velpct, etime);
  }

  NoteOff(key, velpct, etime)
  {
    this.getInst(this.chan).NoteOff(key, velpct, etime);
  }

  MidiEvent(msg, etime)
  {
    let ich = msg[0] & 0x0F;
    this.getInst(ich).MidiEvent(msg, etime);
  }

  getTrackInst()
  {
    return this.handlers[this.track];
  }

  getChanInst(chan)
  {
    return this.handlers[chan % this.handlers.length];
  }
} // end MyNH

let masterhandler = new MyNH(handlers, mode);

// establish midi performance configurations
// nb: to diagnose issues, we can also perform a single track
let cfg =
{
  speed,   
  setTempo: "once", // "once" | "never" | "always" (busted for bach)
  programChange: "allow",
  // programMapper is invoked when a MIDI programChange event occurs.
  // Note that not all MIDI files request program changes. 
  // programMapper can remap incoming information to a new program 
  // and optional channel. These are sent as MIDI to the selected 
  // instrument to perform  a program change.  In the case of 
  // GeneralMIDI, program-change messages are ignored.
  // Here we log the program changes, then encode decisions based
  // on the log, into performance.programs. These are used
  // by our custom NoteHandler to select an instrument for a channel.
  programMapper: function(prog, trk, chan)
  {
    performance.programMapper[chan] = prog;
    console.log(`track:${trk} chan:${chan} prog:${prog} `);
    return prog;
  },
  dumpTrack: null, // -1 means all, null means none, otherwise a track
  dumpJoin: true
};
if(cfg.dumpTrack != null)
  midiFile.Dump(cfg);

// this is where it all happens -----------------------------------------
console.log(`${f} has ${midiFile.GetNumTracks()} tracks, format ${midiFile.GetFormat()}`);
for await (const val of midiFile.Perform(scene, masterhandler, .5, cfg))
{
  yield;
}