Code for Reading 27
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'}
*/
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
* @return 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
* @return number of semitones between this and that; i.e., n such that
* that.transpose(n).equals(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 equals(that:Pitch):boolean {
return this.value === that.value;
}
/**
* @return 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,
CELESTA,
GLOCKENSPIEL,
MUSIC_BOX,
VIBRAPHONE,
MARIMBA,
XYLOPHONE,
TUBULAR_BELL,
DULCIMER,
HAMMOND_ORGAN,
PERC_ORGAN,
ROCK_ORGAN,
CHURCH_ORGAN,
REED_ORGAN,
ACCORDION,
HARMONICA,
TANGO_ACCORDION,
NYLON_STR_GUITAR,
STEEL_STRING_GUITAR,
JAZZ_ELECTRIC_GTR,
CLEAN_GUITAR,
MUTED_GUITAR,
OVERDRIVE_GUITAR,
DISTORTION_GUITAR,
GUITAR_HARMONICS,
ACOUSTIC_BASS,
FINGERED_BASS,
PICKED_BASS,
FRETLESS_BASS,
SLAP_BASS_1,
SLAP_BASS_2,
SYN_BASS_1,
SYN_BASS_2,
VIOLIN,
VIOLA,
CELLO,
CONTRABASS,
TREMOLO_STRINGS,
PIZZICATO_STRINGS,
ORCHESTRAL_HARP,
TIMPANI,
ENSEMBLE_STRINGS,
SLOW_STRINGS,
SYNTH_STRINGS_1,
SYNTH_STRINGS_2,
CHOIR_AAHS,
VOICE_OOHS,
SYN_CHOIR,
ORCHESTRA_HIT,
TRUMPET,
TROMBONE,
TUBA,
MUTED_TRUMPET,
FRENCH_HORN,
BRASS_ENSEMBLE,
SYN_BRASS_1,
SYN_BRASS_2,
SOPRANO_SAX,
ALTO_SAX,
TENOR_SAX,
BARITONE_SAX,
OBOE,
ENGLISH_HORN,
BASSOON,
CLARINET,
PICCOLO,
FLUTE,
RECORDER,
PAN_FLUTE,
BOTTLE_BLOW,
SHAKUHACHI,
WHISTLE,
OCARINA,
SYN_SQUARE_WAVE,
SYN_SAW_WAVE,
SYN_CALLIOPE,
SYN_CHIFF,
SYN_CHARANG,
SYN_VOICE,
SYN_FIFTHS_SAW,
SYN_BRASS_AND_LEAD,
FANTASIA,
WARM_PAD,
POLYSYNTH,
SPACE_VOX,
BOWED_GLASS,
METAL_PAD,
HALO_PAD,
SWEEP_PAD,
ICE_RAIN,
SOUNDTRACK,
CRYSTAL,
ATMOSPHERE,
BRIGHTNESS,
GOBLINS,
ECHO_DROPS,
SCI_FI,
SITAR,
BANJO,
SHAMISEN,
KOTO,
KALIMBA,
BAG_PIPE,
FIDDLE,
SHANAI,
TINKLE_BELL,
AGOGO,
STEEL_DRUMS,
WOODBLOCK,
TAIKO_DRUM,
MELODIC_TOM,
SYN_DRUM,
REVERSE_CYMBAL,
GUITAR_FRET_NOISE,
BREATH_NOISE,
SEASHORE,
BIRD,
TELEPHONE,
HELICOPTER,
APPLAUSE,
GUNSHOT,
};
Music.ts
import { SequencePlayer } from './SequencePlayer';
/**
* Music represents a piece of music played by multiple instruments.
*/
export interface Music {
/**
* @return 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).
*/
equals(that: Music):boolean;
}
MusicLanguage.ts
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
* @return 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 pitchSymbol = m.groups.pitch;
let duration: number = 1.0;
if (m.groups.numerator) duration *= parseInt(m.groups.numerator);
if (m.groups.denominator) duration /= parseInt(m.groups.denominator.substring(1));
if (pitchSymbol === ".") return rest(duration);
else return note(duration, parsePitch(pitchSymbol), 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[0]);
}
/**
* @param duration duration in beats, must be >= 0
* @param pitch pitch to play
* @param instrument instrument to use
* @return 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
* @return 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
* @return 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(
private _duration: number,
public readonly pitch: Pitch,
public readonly instrument: Instrument
) {
this.checkRep();
}
private checkRep(): void {
assert(this._duration >= 0);
}
/**
* @return duration of this note
*/
public duration(): number {
return this._duration;
}
/**
* Play this note.
*/
public play(player: SequencePlayer, atBeat: number):void {
player.addNote(this.instrument, this.pitch, atBeat, this._duration);
}
/**
* @link{Music.equals}
*/
public equals(that: Music):boolean {
return (that instanceof Note)
&& this._duration === that._duration
&& this.instrument === that.instrument
&& this.pitch.equals(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(
private readonly _duration: number
) {
this.checkRep();
}
private checkRep(): void {
assert(this._duration >= 0);
}
/**
* @return duration of this rest
*/
public duration(): number {
return this._duration;
}
/**
* Play this rest.
*/
public play(player: SequencePlayer, atBeat: number):void {
return;
}
/**
* @link{Music.equals}
*/
public equals(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 m1 followed by m2.
* @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 {
}
/**
* @return duration of this concatenation
*/
public 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());
}
/**
* @link{Music.equals}
*/
public equals(that: Music):boolean {
return (that instanceof Concat)
&& this.first.equals(that.first)
&& this.second.equals(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';
async function main() {
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.play();
console.log('playing done')
}
main();
ScaleMusic.ts
import { Music } from '../Music';
import { notes } from '../MusicLanguage';
import { Instrument } from '../Instrument';
import * as MusicPlayer from '../MusicPlayer';
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 MusicPlayer.play(scale);
console.log('playing done');
}
main();
RowYourBoatInitial.ts
import { Music } from '../Music';
import { notes } from '../MusicLanguage';
import { Instrument } from '../Instrument';
import * as MusicPlayer from '../MusicPlayer';
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 MusicPlayer.play(rowYourBoat);
console.log('playing done');
}
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.
*/
play():Promise<void>;
}
MidiSequencePlayer.ts
import assert from 'assert';
import { Instrument } from './Instrument';
import { Pitch } from './Pitch';
import { SequencePlayer } from './SequencePlayer';
import JZZ, { MIDI } from 'jzz';
// sadly can't find TS typing for this SMF plugin, so we'll muddle through for now
require('jzz-midi-smf')(JZZ);
const SMF:any = (MIDI as any).SMF;
/**
* 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;
/**
* @return 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 {
private readonly smf: any;
private readonly track: any;
// total length of sequence programmed so far
private durationInBeats: 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();
public constructor(
private readonly beatsPerMinute: number = DEFAULT_BEATS_PER_MINUTE,
private readonly ticksPerBeat: number = DEFAULT_TICKS_PER_BEAT
) {
this.smf = new SMF(0, ticksPerBeat);
this.track = new SMF.MTrk();
this.smf.push(this.track);
this.track.add(0, MIDI.smfBPM(beatsPerMinute)); // tempo 120 bpm
}
/**
* @link{SequencePlayer.addNote}
*/
public addNote(instr:Instrument, pitch:Pitch, startBeat:number, numBeats:number):void {
const channel = this.getChannel(instr);
const note = getMidiNote(pitch);
const endBeat = startBeat + numBeats;
this.track.add(startBeat * this.ticksPerBeat, MIDI.noteOn(channel, note));
this.track.add(endBeat * this.ticksPerBeat, MIDI.noteOff(channel, note));
this.durationInBeats = Math.max(this.durationInBeats, endBeat);
}
/**
* @link{SequencePlayer.play}
*/
public async play():Promise<void> {
// mark the end of the sequence, so that onEnd event fires
this.track.add(this.durationInBeats * this.ticksPerBeat, MIDI.smfEndOfTrack());
const port = await JZZ().openMidiOut().or('cannot open MIDI output port');
const player = this.smf.player();
const promise = new Promise<void>((resolve, reject) => {
player.onEnd = resolve;
});
player.connect(port);
player.play();
return promise;
}
/**
* Get a MIDI channel for the given instrument, allocating one if necessary.
* @param instr instrument
* @return 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
this.track.add(0, MIDI.program(channel, instr));
this.channelForInstrument.set(instr, channel);
}
return channel;
}
}
MusicPlayer.ts
import { SequencePlayer } from './SequencePlayer';
import { MidiSequencePlayer } from './MidiSequencePlayer';
import { Music } from './Music';
/**
* Play music.
* @param music music to play
* @throws MidiUnavailableException if MIDI device unavailable
* @throws InvalidMidiDataException if MIDI play fails
*/
export function play(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.play();
}