import { Gain, isString, Midi, optionsFromArguments, Param, ToneAudioNode, Unit, isDefined } from 'tone' import { Harmonics } from './Harmonics' import { Keybed } from './Keybed' import { Pedal } from './Pedal' import { PianoStrings } from './Strings' type ToneAudioNodeOptions = import('tone/build/esm/core/context/ToneAudioNode').ToneAudioNodeOptions export interface PianoOptions extends ToneAudioNodeOptions { /** * The number of velocity steps to load */ velocities: number, /** * The lowest note to load */ minNote: number, /** * The highest note to load */ maxNote: number, /** * If it should include a 'release' sounds composed of a keyclick and string harmonic */ release: boolean, /** * If the piano should include a 'pedal' sound. */ pedal: boolean, /** * The directory of the salamander grand piano samples */ url: string, /** * The maximum number of notes that can be held at once */ maxPolyphony: number, /** * Volume levels for each of the components (in decibels) */ volume: { pedal: number, strings: number, keybed: number, harmonics: number, } } interface KeyEvent { time?: Unit.Time; velocity?: number; note?: string; midi?: number; } interface PedalEvent { time?: Unit.Time; } /** * The Piano */ export class Piano extends ToneAudioNode { readonly name = 'Piano' readonly input = undefined readonly output = new Gain({ context: this.context }) /** * The string harmonics */ private _harmonics: Harmonics /** * The keybed release sound */ private _keybed: Keybed /** * The pedal */ private _pedal: Pedal /** * The strings */ private _strings: PianoStrings /** * The volume level of the strings output. This is the main piano sound. */ strings: Param<"decibels"> /** * The volume output of the pedal up and down sounds */ pedal: Param<"decibels"> /** * The volume of the string harmonics */ harmonics: Param<"decibels"> /** * The volume of the keybed click sound */ keybed: Param<"decibels"> /** * The maximum number of notes which can be held at once */ maxPolyphony: number; /** * The sustained notes */ private _sustainedNotes: Map /** * The currently held notes */ private _heldNotes: Map = new Map() /** * If it's loaded or not */ private _loaded: boolean = false constructor(options?: Partial); constructor() { super(optionsFromArguments(Piano.getDefaults(), arguments)) const options = optionsFromArguments(Piano.getDefaults(), arguments) // make sure it ends with a / if (!options.url.endsWith('/')) { options.url += '/' } this.maxPolyphony = options.maxPolyphony this._heldNotes = new Map() this._sustainedNotes = new Map() this._strings = new PianoStrings(Object.assign({}, options, { enabled: true, samples: options.url, volume: options.volume.strings, })).connect(this.output) this.strings = this._strings.volume this._pedal = new Pedal(Object.assign({}, options, { enabled: options.pedal, samples: options.url, volume: options.volume.pedal, })).connect(this.output) this.pedal = this._pedal.volume this._keybed = new Keybed(Object.assign({}, options, { enabled: options.release, samples: options.url, volume: options.volume.keybed, })).connect(this.output) this.keybed = this._keybed.volume this._harmonics = new Harmonics(Object.assign({}, options, { enabled: options.release, samples: options.url, volume: options.volume.harmonics, })).connect(this.output) this.harmonics = this._harmonics.volume } static getDefaults(): PianoOptions { return Object.assign(ToneAudioNode.getDefaults(), { maxNote: 108, minNote: 21, pedal: true, release: false, url: 'https://tambien.github.io/Piano/audio/', velocities: 1, maxPolyphony: 32, volume: { harmonics: 0, keybed: 0, pedal: 0, strings: 0, }, }) } /** * Load all the samples */ async load(): Promise { await Promise.all([ this._strings.load(), this._pedal.load(), this._keybed.load(), this._harmonics.load(), ]) this._loaded = true } /** * If all the samples are loaded or not */ get loaded(): boolean { return this._loaded } /** * Put the pedal down at the given time. Causes subsequent * notes and currently held notes to sustain. */ pedalDown({ time = this.immediate() }: PedalEvent = {}): this { if (this.loaded) { time = this.toSeconds(time) if (!this._pedal.isDown(time)) { this._pedal.down(time) } } return this } /** * Put the pedal up. Dampens sustained notes */ pedalUp({ time = this.immediate() }: PedalEvent = {}): this { if (this.loaded) { const seconds = this.toSeconds(time) if (this._pedal.isDown(seconds)) { this._pedal.up(seconds) // dampen each of the notes this._sustainedNotes.forEach((t, note) => { if (!this._heldNotes.has(note)) { this._strings.triggerRelease(note, seconds) } }) this._sustainedNotes.clear() } } return this } /** * Play a note. * @param note The note to play. If it is a number, it is assumed to be MIDI * @param velocity The velocity to play the note * @param time The time of the event */ keyDown({ note, midi, time = this.immediate(), velocity = 0.8 }: KeyEvent): this { if (this.loaded && this.maxPolyphony > this._heldNotes.size + this._sustainedNotes.size) { time = this.toSeconds(time) if (isString(note)) { midi = Math.round(Midi(note).toMidi()) } if (!this._heldNotes.has(midi)) { // record the start time and velocity this._heldNotes.set(midi, { time, velocity }) this._strings.triggerAttack(midi, time, velocity) } } else { console.warn('samples not loaded') } return this } /** * Release a held note. */ keyUp({ note, midi, time = this.immediate(), velocity = 0.8 }: KeyEvent): this { if (this.loaded) { time = this.toSeconds(time) if (isString(note)) { midi = Math.round(Midi(note).toMidi()) } if (this._heldNotes.has(midi)) { const prevNote = this._heldNotes.get(midi) this._heldNotes.delete(midi) // compute the release velocity const holdTime = Math.pow(Math.max(time - prevNote.time, 0.1), 0.7) const prevVel = prevNote.velocity let dampenGain = (3 / holdTime) * prevVel * velocity dampenGain = Math.max(dampenGain, 0.4) dampenGain = Math.min(dampenGain, 4) if (this._pedal.isDown(time)) { if (!this._sustainedNotes.has(midi)) { this._sustainedNotes.set(midi, time) } } else { // release the string sound this._strings.triggerRelease(midi, time) // trigger the harmonics sound this._harmonics.triggerAttack(midi, time, dampenGain) } // trigger the keybed release sound this._keybed.start(midi, time, velocity) } } return this } stopAll(): this { this.pedalUp() this._heldNotes.forEach((_, midi) => { this.keyUp({ midi }) }) return this } }