UNPKG

d-piano

Version:

Web Audio instrument using Salamander Grand Piano samples

412 lines (411 loc) 12 kB
import { ToneAudioNode as m, Volume as A, Frequency as R, ToneAudioBuffers as $, ToneAudioBuffer as I, Midi as p, Sampler as x, ToneBufferSource as S, optionsFromArguments as b, Gain as O, isString as y } from "tone"; class w extends m { constructor(t) { super(t), this.name = "PianoComponent", this.input = void 0, this.output = new A({ context: this.context }), this._enabled = !1, this.volume = this.output.volume, this._loaded = !1, this.volume.value = t.volume, this._enabled = t.enabled, this.samples = t.samples; } /** * If the samples are loaded or not */ get loaded() { return this._loaded; } /** * Load the samples */ async load() { if (this._enabled) await this._internalLoad(), this._loaded = !0; else return Promise.resolve(); } } function P(n) { return R(n, "midi").toNote(); } function _(n, t) { return Math.random() * (t - n) + n; } function j(n) { return `rel${n - 20}.ogg`; } function F(n) { return `harmS${P(n).replace("#", "s")}.ogg`; } function L(n, t) { return `${P(n).replace("#", "s")}v${t}.ogg`; } const M = { 1: [8], 2: [6, 12], 3: [1, 7, 15], 4: [1, 5, 10, 15], 5: [1, 4, 8, 12, 16], 6: [1, 3, 7, 10, 13, 16], 7: [1, 3, 6, 9, 11, 13, 16], 8: [1, 3, 5, 7, 9, 11, 13, 16], 9: [1, 3, 5, 7, 9, 11, 13, 15, 16], 10: [1, 2, 3, 5, 7, 9, 11, 13, 15, 16], 11: [1, 2, 3, 5, 7, 9, 11, 13, 14, 15, 16], 12: [1, 2, 3, 4, 5, 7, 9, 11, 13, 14, 15, 16], 13: [1, 2, 3, 4, 5, 7, 9, 11, 12, 13, 14, 15, 16], 14: [1, 2, 3, 4, 5, 6, 7, 9, 11, 12, 13, 14, 15, 16], 15: [1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15, 16], 16: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] }, k = [ 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108 ]; function U(n, t) { return k.filter((e) => n <= e && e <= t); } const f = [21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87]; function q(n, t) { return f.filter((e) => n <= e && e <= t); } function E(n) { return f[0] <= n && n <= f[f.length - 1]; } const d = class d { static async getBufferMap(t, e) { const s = [], i = {}; return Object.entries(e).forEach(([o, r]) => { const l = d.getBuffer(t + r).then((a) => { i[o] = a; }); s.push(l); }), await Promise.allSettled(s), i; } static async getBuffers(t, e) { const s = new $(), i = []; return Object.entries(e).forEach(([o, r]) => { const l = d.getBuffer(t + r).then((a) => { s.add(o, a); }); i.push(l); }), await Promise.allSettled(i), s; } static getBuffer(t) { const e = d.cache[t]; if (!e) { const s = I.fromUrl(t); return d.cache[t] = s, s; } return e; } }; d.cache = {}; let g = d; class H extends w { constructor(t) { super(t), this._urls = {}; const e = q(t.minNote, t.maxNote); for (const s of e) this._urls[s] = F(s); } triggerAttack(t, e, s) { this._enabled && E(t) && this._sampler.triggerAttack(p(t).toNote(), e, s * _(0.5, 1)); } async _internalLoad() { const t = await g.getBufferMap(this.samples, this._urls); return new Promise((e) => { this._sampler = new x({ baseUrl: this.samples, onload: e, urls: t }).connect(this.output); }); } } class G extends w { constructor(t) { super(t), this._urls = {}; for (let e = t.minNote; e <= t.maxNote; e++) this._urls[e] = j(e); } async _internalLoad() { this._buffers = await g.getBuffers(this.samples, this._urls); } start(t, e, s) { this._enabled && this._buffers.has(t) && new S({ context: this.context, url: this._buffers.get(t) }).connect(this.output).start(e, 0, void 0, 0.015 * s * _(0.5, 1)); } } class V extends w { constructor(t) { super(t), this._downTime = 1 / 0, this._currentSound = null, this._downTime = 1 / 0; } async _internalLoad() { this._buffers = await g.getBuffers(this.samples, { down1: "pedalD1.ogg", down2: "pedalD2.ogg", up1: "pedalU1.ogg", up2: "pedalU2.ogg" }); } /** * Squash the current playing sound */ _squash(t) { this._currentSound && this._currentSound.state !== "stopped" && this._currentSound.stop(t), this._currentSound = null; } _playSample(t, e) { this._enabled && (this._currentSound = new S({ context: this.context, curve: "exponential", fadeIn: 0.05, fadeOut: 0.1, url: this._buffers.get(`${e}${Math.random() > 0.5 ? 1 : 2}`) }).connect(this.output), this._currentSound.start(t, _(0, 0.01), void 0, 0.1 * _(0.5, 1))); } /** * Put the pedal down */ down(t) { this._squash(t), this._downTime = t, this._playSample(t, "down"); } /** * Put the pedal up */ up(t) { this._squash(t), this._downTime = 1 / 0, this._playSample(t, "up"); } /** * Indicates if the pedal is down at the given time */ isDown(t) { return t >= this._downTime; } } class z extends m { constructor(t) { super(t), this.name = "PianoString", this._urls = {}, t.notes.forEach((e) => this._urls[e] = L(e, t.velocity)), this.samples = t.samples; } async load() { const t = await g.getBufferMap(this.samples, this._urls); return new Promise((e) => { this._sampler = this.output = new x({ attack: 0, baseUrl: this.samples, curve: "exponential", onload: e, release: 0.4, urls: t, volume: 3 }); }); } triggerAttack(t, e, s) { this._sampler.triggerAttack(t, e, s); } triggerRelease(t, e) { this._sampler.triggerRelease(t, e); } } class C extends w { constructor(t) { super(t); const e = U(t.minNote, t.maxNote), s = M[t.velocities].slice(); this._strings = s.map((i) => new z(Object.assign(t, { notes: e, velocity: i }))), this._activeNotes = /* @__PURE__ */ new Map(); } /** * Scale a value between a given range */ scale(t, e, s, i, o) { return (t - e) / (s - e) * (o - i) + i; } triggerAttack(t, e, s) { const i = this.scale(s, 0, 1, -0.5, this._strings.length - 0.51), o = Math.max(Math.round(i), 0); let r = 1 + i - o; this._strings.length === 1 && (r = s); const l = this._strings[o]; this._activeNotes.has(t) && this.triggerRelease(t, e), this._activeNotes.set(t, l), l.triggerAttack(p(t).toNote(), e, r); } triggerRelease(t, e) { this._activeNotes.has(t) && (this._activeNotes.get(t).triggerRelease(p(t).toNote(), e), this._activeNotes.delete(t)); } async _internalLoad() { await Promise.all(this._strings.map(async (t) => { await t.load(), t.connect(this.output); })); } } class N extends m { constructor() { super(b(N.getDefaults(), arguments)), this.name = "Piano", this.input = void 0, this.output = new O({ context: this.context }), this._heldNotes = /* @__PURE__ */ new Map(), this._loaded = !1; const t = b(N.getDefaults(), arguments); t.url.endsWith("/") || (t.url += "/"), this.maxPolyphony = t.maxPolyphony, this._heldNotes = /* @__PURE__ */ new Map(), this._sustainedNotes = /* @__PURE__ */ new Map(), this._strings = new C(Object.assign({}, t, { enabled: !0, samples: t.url, volume: t.volume.strings })).connect(this.output), this.strings = this._strings.volume, this._pedal = new V(Object.assign({}, t, { enabled: t.pedal, samples: t.url, volume: t.volume.pedal })).connect(this.output), this.pedal = this._pedal.volume, this._keybed = new G(Object.assign({}, t, { enabled: t.release, samples: t.url, volume: t.volume.keybed })).connect(this.output), this.keybed = this._keybed.volume, this._harmonics = new H(Object.assign({}, t, { enabled: t.release, samples: t.url, volume: t.volume.harmonics })).connect(this.output), this.harmonics = this._harmonics.volume; } static getDefaults() { return Object.assign(m.getDefaults(), { maxNote: 108, maxPolyphony: 32, minNote: 21, pedal: !0, release: !1, url: "https://tambien.github.io/Piano/audio/", velocities: 1, volume: { harmonics: 0, keybed: 0, pedal: 0, strings: 0 } }); } /** * Load all the samples */ async load() { await Promise.all([ this._strings.load(), this._pedal.load(), this._keybed.load(), this._harmonics.load() ]), this._loaded = !0; } /** * 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: t = this.immediate() } = {}) { return this.loaded && (t = this.toSeconds(t), this._pedal.isDown(t) || this._pedal.down(t)), this; } /** * Put the pedal up. Dampens sustained notes */ pedalUp({ time: t = this.immediate() } = {}) { if (this.loaded) { const e = this.toSeconds(t); this._pedal.isDown(e) && (this._pedal.up(e), this._sustainedNotes.forEach((s, i) => { this._heldNotes.has(i) || this._strings.triggerRelease(i, e); }), 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: t, midi: e, time: s = this.immediate(), velocity: i = 0.8 }) { return this.loaded && this.maxPolyphony > this._heldNotes.size + this._sustainedNotes.size ? (s = this.toSeconds(s), y(t) && (e = Math.round(p(t).toMidi())), this._heldNotes.has(e) || (this._heldNotes.set(e, { time: s, velocity: i }), this._strings.triggerAttack(e, s, i))) : console.warn("samples not loaded"), this; } /** * Release a held note. */ keyUp({ note: t, midi: e, time: s = this.immediate(), velocity: i = 0.8 }) { if (this.loaded && (s = this.toSeconds(s), y(t) && (e = Math.round(p(t).toMidi())), this._heldNotes.has(e))) { const o = this._heldNotes.get(e); this._heldNotes.delete(e); const r = Math.pow(Math.max(s - o.time, 0.1), 0.7), l = o.velocity; let a = 3 / r * l * i; a = Math.max(a, 0.4), a = Math.min(a, 4), this._pedal.isDown(s) ? this._sustainedNotes.has(e) || this._sustainedNotes.set(e, s) : (this._strings.triggerRelease(e, s), this._harmonics.triggerAttack(e, s, a)), this._keybed.start(e, s, i); } return this; } stopAll() { return this.pedalUp(), this._heldNotes.forEach((t, e) => { this.keyUp({ midi: e }); }), this; } } function T(n) { const t = Math.floor((n - 12) / 12), e = (n - 12) % 12; return `${["C", "Cs", "D", "Ds", "E", "F", "Fs", "G", "Gs", "A", "As", "B"][e]}${t}`; } function W(n, t) { return `${T(n).replace("#", "s")}v${t}.ogg`; } function K(n) { return `harmS${T(n).replace("#", "s")}.ogg`; } function J(n) { return `rel${n - 20}.ogg`; } async function X(n, t = {}) { const { baseUrl: e = "https://tambien.github.io/Piano/audio/", minNote: s = 21, maxNote: i = 108, includeHarmonics: o = !1, includePedal: r = !1, includeRelease: l = !1 } = t, a = e.endsWith("/") ? e : `${e}/`, D = M[n] || [8], v = U(s, i), h = []; for (const u of v) for (const c of D) h.push(a + W(u, c)); if (o) { const u = k.filter((c) => c >= 21 && c <= 87); for (const c of u) h.push(a + K(c)); } if (r && (h.push(a + "pedalD1.ogg"), h.push(a + "pedalD2.ogg"), h.push(a + "pedalU1.ogg"), h.push(a + "pedalU2.ogg")), l) for (const u of v) h.push(a + J(u)); const B = h.map( (u) => window.fetch(u).catch((c) => { console.error(c); }) ); await Promise.allSettled(B); } export { N as Piano, X as preloadSamples };