// 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;
}