Code for Reading 26
Download
You can also download a ZIP file containing this code.
Pitch.ts
import assert from 'assert';
/**
* Pitch is an immutable type representing the frequency of a musical note.
* Standard music notation represents pitches by letters: A, B, C, ..., G.
* Pitches can be sharp or flat, or whole octaves up or down from these
* primitive generators.
*
* <p> For example:
* <br> new Pitch('C') makes middle C
* <br> new Pitch('C').transpose(1) makes C-sharp
* <br> new Pitch('E').transpose(-1) makes E-flat
* <br> new Pitch('C').transpose(OCTAVE) makes high C
* <br> new Pitch('C').transpose(-OCTAVE) makes low C
*/
export class Pitch {
private constructor(
private readonly value: number
) {
this.checkRep();
}
/*
* Rep invariant: value is an integer.
*
* Abstraction function AF(value):
* AF(0),...,AF(11) map to middle C, C-sharp, D, ..., A, A-sharp, B.
* AF(i+12n) maps to n octaves above middle AF(i)
* AF(i-12n) maps to n octaves below middle AF(i)
*
* Safety from rep exposure:
* all fields are private and immutable.
*/
private checkRep(): void {
assert(this.value === Math.floor(this.value));
}
private static readonly SCALE: {[letter:string]: number} = {
'A': 9,
'B': 11,
'C': 0,
'D': 2,
'E': 4,
'F': 5,
'G': 7,
};
private static readonly VALUE_TO_STRING: Array<string> = [
"C", "^C", "D", "^D", "E", "F", "^F", "G", "^G", "A", "^A", "B"
];
/**
* Middle C.
*/
public static readonly MIDDLE_C: Pitch = Pitch.make('C');
/**
* Number of pitches in an octave.
*/
public static readonly OCTAVE: number = 12;
/**
* Make a Pitch named `letter` in the middle octave of the piano keyboard.
* For example, new Pitch('C') constructs middle C.
*
* @param letter must be in {'A',...,'G'}
* @returns pitch of the given note in the middle octave
*/
public static make(letter: string): Pitch {
const value = Pitch.SCALE[letter];
if (value === undefined) { throw new Error(letter + " must be in the range A-G"); }
return new Pitch(value);
}
/**
* @param semitonesUp must be integer
* @returns pitch made by transposing this pitch by semitonesUp semitones;
* for example, middle C transposed by 12 semitones is high C, and
* E transposed by -1 semitones is E flat
*/
public transpose(semitonesUp: number): Pitch {
if (Math.floor(semitonesUp) !== semitonesUp) {
throw new Error(semitonesUp + ' must be integer');
}
return new Pitch(this.value + semitonesUp);
}
/**
* @param that Pitch to compare `this` with
* @returns number of semitones between this and that; i.e., n such that
* that.transpose(n).equalValue(this)
*/
public difference(that: Pitch): number {
return this.value - that.value;
}
/**
* @param that Pitch to compare `this` with
* @returns true iff this and that represent the same pitch, must be an
* equivalence relation (reflexive, symmetric, and transitive)
*/
public equalValue(that: Pitch): boolean {
return this.value === that.value;
}
/**
* @returns this pitch in abc music notation
* @see "http://abcnotation.com/examples/"
*/
public toString(): string {
let suffix = "";
let v = this.value;
while (v < 0) {
suffix += ",";
v += Pitch.OCTAVE;
}
while (v >= Pitch.OCTAVE) {
suffix += "'";
v -= Pitch.OCTAVE;
}
return Pitch.VALUE_TO_STRING[v] + suffix;
}
}
Instrument.ts
/**
* Instrument represents a musical instrument.
*
* These instruments are the 128 standard General MIDI Level 1 instruments.
* See: http://www.midi.org/about-midi/gm/gm1sound.shtml
*/
export enum Instrument {
// Order is important in this enumeration because an instrument's
// position must correspond to its MIDI program number.
PIANO,
BRIGHT_PIANO,
ELECTRIC_GRAND,
HONKY_TONK_PIANO,
ELECTRIC_PIANO_1,
ELECTRIC_PIANO_2,
HARPSICHORD,
CLAVINET,
// ... omitting 120 more instruments here ...
}
Music.ts
import { SequencePlayer } from './SequencePlayer';
/**
* Music represents a piece of music played by multiple instruments.
*/
export interface Music {
/**
* Total duration of this piece in beats.
*/
duration: number;
/**
* Play this piece.
*
* @param player player to play on
* @param atBeat when to play
*/
play(player: SequencePlayer, atBeat:number): void;
/**
* @param that Music to compare `this` with
* @returns true iff this and that represent the same Music expression.
* Must be an equivalence relation (reflexive, symmetric, and transitive).
*/
equalValue(that: Music): boolean;
}
MusicLanguage.ts
import assert from 'assert';
import { Pitch } from './Pitch';
import { Instrument } from './Instrument';
import { Note } from './Note';
import { Rest } from './Rest';
import { Concat } from './Concat';
import { Music } from './Music';
/**
* MusicLanguage defines functions for constructing and manipulating Music expressions.
*/
////////////////////////////////////////////////////
// Factory functions
////////////////////////////////////////////////////
/**
* Make Music from a string using a variant of abc notation
* (see http://abcnotation.com/examples/).
*
* <p> The notation consists of whitespace-delimited symbols representing
* either notes or rests. The vertical bar | may be used as a delimiter
* for measures; notes() treats it as a space.
* Grammar:
* <pre>
* notes ::= symbol*
* symbol ::= . duration // for a rest
* | pitch duration // for a note
* pitch ::= accidental letter octave*
* accidental ::= empty string // for natural,
* | _ // for flat,
* | ^ // for sharp
* letter ::= [A-G]
* octave ::= ' // to raise one octave
* | , // to lower one octave
* duration ::= empty string // for 1-beat duration
* | /n // for 1/n-beat duration
* | n // for n-beat duration
* | n/m // for n/m-beat duration
* </pre>
* <p> Examples (assuming 4/4 common time, i.e. 4 beats per measure):
* C quarter note, middle C
* A'2 half note, high A
* _D/2 eighth note, middle D flat
*
* @param notes string of notes and rests in simplified abc notation given above
* @param instr instrument to play the notes with
* @returns the music in notes played by instr
*/
export function notes(notes: string, instr: Instrument): Music {
let music: Music = rest(0);
for (const sym of notes.split(/[\s|]+/)) {
if (sym !== '') {
music = concat(music, parseSymbol(sym, instr));
}
}
return music;
}
/* Parse a symbol into a Note or a Rest. */
function parseSymbol(symbol: string, instr: Instrument): Music {
const m = symbol.match(/^(?<pitch>[^\/0-9]+)(?<numerator>[0-9]+)?(?<denominator>\/[0-9]+)?$/);
if ( ! (m && m.groups)) { throw new Error("couldn't understand " + symbol); }
const { pitch, numerator, denominator } = m.groups;
assert(pitch);
let duration = 1.0;
if (numerator) { duration *= parseInt(numerator); }
if (denominator) { duration /= parseInt(denominator.substring(1)); }
if (pitch === ".") {
return rest(duration);
} else {
return note(duration, parsePitch(pitch), instr);
}
}
/* Parse a symbol into a Pitch. */
function parsePitch(symbol: string): Pitch {
if (symbol.endsWith("'")) {
return parsePitch(symbol.substring(0, symbol.length-1)).transpose(Pitch.OCTAVE);
} else if (symbol.endsWith(",")) {
return parsePitch(symbol.substring(0, symbol.length-1)).transpose(-Pitch.OCTAVE);
} else if (symbol.startsWith("^")) {
return parsePitch(symbol.substring(1)).transpose(1);
} else if (symbol.startsWith("_")) {
return parsePitch(symbol.substring(1)).transpose(-1);
} else if (symbol.length != 1) {
throw new Error("can't understand " + symbol);
} else {
return Pitch.make(symbol.charAt(0));
}
}
/**
* @param duration duration in beats, must be >= 0
* @param pitch pitch to play
* @param instrument instrument to use
* @returns pitch played by instrument for duration beats
*/
export function note(duration: number, pitch: Pitch, instrument: Instrument ): Music {
return new Note(duration, pitch, instrument);
}
/**
* @param duration duration in beats, must be >= 0
* @returns rest that lasts for duration beats
*/
export function rest(duration: number): Music {
return new Rest(duration);
}
////////////////////////////////////////////////////
// Producers
////////////////////////////////////////////////////
/**
* @param m1 first piece of music
* @param m2 second piece of music
* @returns m1 followed by m2
*/
export function concat(m1: Music, m2: Music): Music {
return new Concat(m1, m2);
}
Note.ts
import assert from 'assert';
import { Music } from './Music';
import { SequencePlayer } from './SequencePlayer';
import { Pitch } from './Pitch';
import { Instrument } from './Instrument';
/**
* Note represents a note played by an instrument.
*/
export class Note implements Music {
/**
* Make a Note played by instrument for duration beats.
*
* @param duration duration in beats, must be >= 0
* @param pitch pitch to play
* @param instrument instrument to use
*/
public constructor(
public readonly duration: number,
public readonly pitch: Pitch,
public readonly instrument: Instrument
) {
this.checkRep();
}
private checkRep(): void {
assert(this.duration >= 0);
}
/**
* Play this note.
*/
public play(player: SequencePlayer, atBeat: number): void {
player.addNote(this.instrument, this.pitch, atBeat, this.duration);
}
/**
* @inheritdoc
*/
public equalValue(that: Music): boolean {
return (that instanceof Note)
&& this.duration === that.duration
&& this.instrument === that.instrument
&& this.pitch.equalValue(that.pitch);
}
public toString(): string {
return this.pitch.toString() + this.duration;
}
}
Rest.ts
import assert from 'assert';
import { Music } from './Music';
import { SequencePlayer } from './SequencePlayer';
/**
* Rest represents a pause in a piece of music.
*/
export class Rest implements Music {
/**
* Make a Rest that lasts for duration beats.
*
* @param duration duration in beats, must be >= 0
*/
public constructor(
public readonly duration: number
) {
this.checkRep();
}
private checkRep(): void {
assert(this.duration >= 0);
}
/**
* Play this rest.
*/
public play(player: SequencePlayer, atBeat: number):void {
return;
}
/**
* @inheritdoc
*/
public equalValue(that: Music): boolean {
return (that instanceof Rest)
&& this.duration === that.duration;
}
public toString(): string {
return "." + this.duration;
}
}
Concat.ts
import assert from 'assert';
import { Music } from './Music';
import { SequencePlayer } from './SequencePlayer';
/**
* Concat represents two pieces of music played one after the other.
*/
export class Concat implements Music {
/**
* Make a Music sequence that plays first followed by second.
*
* @param first music to play first
* @param second music to play second
*/
public constructor(
public readonly first: Music,
public readonly second: Music
) {
this.checkRep();
}
private checkRep(): void {
}
/**
* @returns duration of this concatenation
*/
public get duration(): number {
return this.first.duration + this.second.duration;
}
/**
* Play this concatenation.
*/
public play(player: SequencePlayer, atBeat: number): void {
this.first.play(player, atBeat);
this.second.play(player, atBeat + this.first.duration);
}
/**
* @inheritdoc
*/
public equalValue(that: Music): boolean {
return (that instanceof Concat)
&& this.first.equalValue(that.first)
&& this.second.equalValue(that.second);
}
public toString(): string {
return this.first + " " + this.second;
}
}
ScaleSequence.ts
import { Instrument } from '../Instrument';
import { Pitch } from '../Pitch';
import { SequencePlayer } from '../SequencePlayer';
import { MidiSequencePlayer } from '../MidiSequencePlayer';
/**
* Play an octave up and back down starting from middle C.
*/
export async function main(): Promise<void> {
const piano = Instrument.PIANO;
// create a new player
const beatsPerMinute = 120; // a beat is a quarter note, so this is 120 quarter notes per minute
const ticksPerBeat = 2;
const player: SequencePlayer = new MidiSequencePlayer(beatsPerMinute, ticksPerBeat);
// addNote(instr, pitch, startBeat, numBeats) schedules a note with pitch value 'pitch'
// played by 'instr' starting at 'startBeat' to be played for 'numBeats' beats.
const numBeats = 1;
let startBeat = 0;
player.addNote(piano, Pitch.make('C'), startBeat++, numBeats);
player.addNote(piano, Pitch.make('D'), startBeat++, numBeats);
player.addNote(piano, Pitch.make('E'), startBeat++, numBeats);
player.addNote(piano, Pitch.make('F'), startBeat++, numBeats);
player.addNote(piano, Pitch.make('G'), startBeat++, numBeats);
player.addNote(piano, Pitch.make('A'), startBeat++, numBeats);
player.addNote(piano, Pitch.make('B'), startBeat++, numBeats);
player.addNote(piano, Pitch.make('C').transpose(Pitch.OCTAVE), startBeat++, numBeats);
player.addNote(piano, Pitch.make('B'), startBeat++, numBeats);
player.addNote(piano, Pitch.make('A'), startBeat++, numBeats);
player.addNote(piano, Pitch.make('G'), startBeat++, numBeats);
player.addNote(piano, Pitch.make('F'), startBeat++, numBeats);
player.addNote(piano, Pitch.make('E'), startBeat++, numBeats);
player.addNote(piano, Pitch.make('D'), startBeat++, numBeats);
player.addNote(piano, Pitch.make('C'), startBeat++, numBeats);
// play!
console.log('playing now...');
await player.start();
console.log('playing done');
}
if (require.main === module) {
main();
}
ScaleMusic.ts
import { Music } from '../Music';
import { notes } from '../MusicLanguage';
import { Instrument } from '../Instrument';
import { playOnMidiPlayer } from '../MusicPlayer';
/**
* Play an octave up and back down starting from middle C.
*/
export async function main(): Promise<void> {
// parse simplified abc into a Music
const scale: Music = notes("C D E F G A B C' B A G F E D C", Instrument.PIANO);
console.log(scale.toString());
// play!
console.log('playing now...');
await playOnMidiPlayer(scale);
console.log('playing done');
}
if (require.main === module) {
main();
}
RowYourBoatInitial.ts
import { Music } from '../Music';
import { notes } from '../MusicLanguage';
import { Instrument } from '../Instrument';
import { playOnMidiPlayer } from '../MusicPlayer';
/**
* Play a song.
*/
export async function main(): Promise<void> {
const rowYourBoat: Music =
notes("C C C3/4 D/4 E |" // Row, row, row your boat,
+ "E3/4 D/4 E3/4 F/4 G2 |" // Gently down the stream.
+ "C'/3 C'/3 C'/3 G/3 G/3 G/3 E/3 E/3 E/3 C/3 C/3 C/3 |"
// Merrily, merrily, merrily, merrily,
+ "G3/4 F/4 E3/4 D/4 C2", // Life is but a dream.
Instrument.PIANO);
console.log(rowYourBoat.toString());
// play!
console.log('playing now...');
await playOnMidiPlayer(rowYourBoat);
console.log('playing done');
}
if (require.main === module) {
main();
}
SequencePlayer.ts
import assert from 'assert';
import { Instrument } from './Instrument';
import { Pitch } from './Pitch';
/**
* Schedules and plays a sequence of notes at given times.
*/
export interface SequencePlayer {
/**
* Schedule a note to be played starting at startBeat for the duration numBeats.
*
* @param instr instrument for the note
* @param pitch pitch value of the note
* @param startBeat the starting beat (while playing, must be now or in the future)
* @param numBeats the number of beats the note is played
*/
addNote(instr: Instrument, pitch: Pitch, startBeat: number, numBeats: number): void;
/**
* Play the scheduled music.
*
* @returns (a promise that) resolves after the music has played
*/
start(): Promise<void>;
}
MidiSequencePlayer.ts
import assert from 'assert';
import { performance } from 'perf_hooks';
import { Instrument } from './Instrument';
import { Pitch } from './Pitch';
import { SequencePlayer } from './SequencePlayer';
import JZZ from 'jzz';
import JZZ_MIDI_SMF from 'jzz-midi-smf';
import type { MTrk } from 'jzz-midi-smf';
JZZ_MIDI_SMF(JZZ);
const { MIDI } = JZZ;
/**
* Default tempo.
*/
export const DEFAULT_BEATS_PER_MINUTE = 120;
/**
* Default MIDI ticks per beat.
*/
export const DEFAULT_TICKS_PER_BEAT = 64;
// MIDI note number representing middle C
const MIDI_NOTE_MIDDLE_C = 60;
// MIDI marker meta message type
const MIDI_META_MARKER = 6;
/**
* @returns the MIDI note number for a pitch, defined as the number of
* semitones above C 5 octaves below middle C; for example,
* middle C is note 60
*/
function getMidiNote(pitch: Pitch): number {
return MIDI_NOTE_MIDDLE_C + pitch.difference(Pitch.MIDDLE_C);
}
/**
* Schedules and plays a sequence of notes using the MIDI synthesizer.
*/
export class MidiSequencePlayer implements SequencePlayer {
// queued MIDI events before playback begins
private readonly queued: MTrk;
// MIDI output device to handle callbacks
private readonly midiOut;
// time at which playback began, iff started
private playStartTime: number|undefined;
// counter of future MIDI events
private unplayedEvents: number = 0;
// instruments that have been used (selected into MIDI channels) so far
// maps an instrument to a MIDI channel number
private readonly channelForInstrument: Map<Instrument, number> = new Map();
// event callback functions
private readonly callbacks: Map<number, ()=>void> = new Map();
private readonly doneCallback = -1;
/**
* Make a new MIDI sequence player.
*
* @param beatsPerMinute number of beats per minute
* @param ticksPerBeat number of MIDI ticks per beat
*/
public constructor(
private readonly beatsPerMinute: number = DEFAULT_BEATS_PER_MINUTE,
private readonly ticksPerBeat: number = DEFAULT_TICKS_PER_BEAT
) {
this.queued = new MIDI.SMF.MTrk();
const thisMSP = this;
this.midiOut = JZZ.Widget({
_receive(this: ReturnType<typeof JZZ.Widget>, msg: typeof MIDI & { ff?: number }) {
if (msg.isSMF() && msg.ff === MIDI_META_MARKER) {
// meta event indicates `addEvent(..)` callback
const id = parseInt(msg.getText());
const callback = thisMSP.callbacks.get(id);
if (callback) {
thisMSP.callbacks.delete(id);
callback();
}
}
// send this message to the MIDI output device
this.emit(msg);
if (--thisMSP.unplayedEvents <= 0) {
// no future events, `play()` is done
thisMSP.callbacks.get(thisMSP.doneCallback)?.();
}
}
});
this.checkRep();
}
private checkRep() {
assert(this.beatsPerMinute >= 0);
assert(this.ticksPerBeat >= 0);
}
/** @returns milliseconds since playback began (requires currently playing) */
private msSincePlayStart(beat: number): number {
assert(this.playStartTime !== undefined);
return this.ms(beat) - (performance.now() - this.playStartTime);
}
/** @returns beats in milliseconds */
private ms(beats: number): number {
return beats / this.beatsPerMinute * 60 * 1000;
}
/**
* @inheritdoc
*/
public addNote(instr: Instrument, pitch: Pitch, startBeat: number, numBeats: number): void {
const channel = this.getChannel(instr);
const note = getMidiNote(pitch);
if (this.playStartTime !== undefined) {
const startTime = this.msSincePlayStart(startBeat);
this.midiOut.wait(startTime).noteOn(channel, note);
// TODO: (removing 1 ms avoids some timing glitches) find a better way to stream events
this.midiOut.wait(startTime + this.ms(numBeats) - 1).noteOff(channel, note);
} else {
this.queued.add(startBeat * this.ticksPerBeat, MIDI.noteOn(channel, note));
this.queued.add((startBeat + numBeats) * this.ticksPerBeat, MIDI.noteOff(channel, note));
}
this.unplayedEvents += 2;
}
public addEvent(callback: (beat:number)=>void, atBeat: number): void {
let id = 0;
while (this.callbacks.has(id)) { id++; }
this.callbacks.set(id, () => callback(atBeat));
if (this.playStartTime !== undefined) {
this.midiOut.wait(this.msSincePlayStart(atBeat)).smfMarker(id.toString());
} else {
this.queued.add(atBeat * this.ticksPerBeat, MIDI.smfMarker(id.toString()));
}
this.unplayedEvents++;
}
/**
* @inheritdoc
*/
public async start(): Promise<void> {
await this.midiOut.connect(await JZZ().openMidiOut().or('cannot open MIDI output port'));
for (const midi of this.queued) {
this.midiOut.wait(midi.tt / this.ticksPerBeat / this.beatsPerMinute * 60 * 1000).send(midi);
}
this.playStartTime = performance.now();
return new Promise((resolve) => {
this.callbacks.set(this.doneCallback, resolve);
});
}
/**
* Get a MIDI channel for the given instrument, allocating one if necessary.
*
* @param instr instrument
* @returns channel for the instrument
*/
private getChannel(instr: Instrument): number {
// check whether this instrument already has a channel
let channel = this.channelForInstrument.get(instr);
if (channel === undefined) {
channel = this.channelForInstrument.size;
// TODO: check whether nextChannel exceeds # of possible channels for this midi device
if (this.playStartTime !== undefined) {
this.midiOut.send(MIDI.program(channel, instr));
} else {
this.queued.add(0, MIDI.program(channel, instr));
}
this.unplayedEvents++;
this.channelForInstrument.set(instr, channel);
}
return channel;
}
}
MusicPlayer.ts
import { SequencePlayer } from './SequencePlayer';
import { MidiSequencePlayer } from './MidiSequencePlayer';
import { Music } from './Music';
/**
* Play music with the MIDI synthesizer.
*
* @param music music to play
* @returns (a promise that) resolves after the music has played
* @throws Error if MIDI device unavailable or MIDI play fails
*/
export function playOnMidiPlayer(music: Music): Promise<void> {
const player: SequencePlayer = new MidiSequencePlayer();
// load the player with a sequence created from music (add a small delay at the beginning)
const warmup = 0.125;
music.play(player, warmup);
// start playing
return player.start();
}