@hlysine/piano
Version:
Web Audio instrument using Salamander Grand Piano samples
200 lines (199 loc) • 7.42 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { Gain, isString, Midi, optionsFromArguments, ToneAudioNode } from 'tone';
import { Harmonics } from './Harmonics';
import { Keybed } from './Keybed';
import { Pedal } from './Pedal';
import { PianoStrings } from './Strings';
/**
* The Piano
*/
export class Piano extends ToneAudioNode {
constructor() {
super(optionsFromArguments(Piano.getDefaults(), arguments));
this.name = 'Piano';
this.input = undefined;
this.output = new Gain({ context: this.context });
/**
* The currently held notes
*/
this._heldNotes = new Map();
/**
* If it's loaded or not
*/
this._loaded = false;
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() {
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
*/
load() {
return __awaiter(this, void 0, void 0, function* () {
yield 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() {
return this._loaded;
}
/**
* Put the pedal down at the given time. Causes subsequent
* notes and currently held notes to sustain.
*/
pedalDown({ time = this.immediate() } = {}) {
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() } = {}) {
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 }) {
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 }) {
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.pedalUp();
this._heldNotes.forEach((_, midi) => {
this.keyUp({ midi });
});
return this;
}
}