d-piano
Version:
Web Audio instrument using Salamander Grand Piano samples
412 lines (411 loc) • 12 kB
JavaScript
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
};