mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-12 14:18:36 +00:00
539 lines
18 KiB
JavaScript
539 lines
18 KiB
JavaScript
import { T as ToneAudioNode, V as Volume, F as Frequency, M as Midi, S as Sampler, a as ToneAudioBuffers, b as ToneBufferSource, o as optionsFromArguments, G as Gain, i as isString } from '../common/index-b6fc655f.js';
|
|
import '../common/webmidi.min-97732fd4.js';
|
|
import '../common/_commonjsHelpers-8c19dec8.js';
|
|
|
|
var __awaiter = (undefined && undefined.__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());
|
|
});
|
|
};
|
|
/**
|
|
* Base class for the other components
|
|
*/
|
|
class PianoComponent extends ToneAudioNode {
|
|
constructor(options) {
|
|
super(options);
|
|
this.name = 'PianoComponent';
|
|
this.input = undefined;
|
|
this.output = new Volume({ context: this.context });
|
|
/**
|
|
* If the component is enabled or not
|
|
*/
|
|
this._enabled = false;
|
|
/**
|
|
* The volume output of the component
|
|
*/
|
|
this.volume = this.output.volume;
|
|
/**
|
|
* Boolean indication of if the component is loaded or not
|
|
*/
|
|
this._loaded = false;
|
|
this.volume.value = options.volume;
|
|
this._enabled = options.enabled;
|
|
this.samples = options.samples;
|
|
}
|
|
/**
|
|
* If the samples are loaded or not
|
|
*/
|
|
get loaded() {
|
|
return this._loaded;
|
|
}
|
|
/**
|
|
* Load the samples
|
|
*/
|
|
load() {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (this._enabled) {
|
|
yield this._internalLoad();
|
|
this._loaded = true;
|
|
}
|
|
else {
|
|
return Promise.resolve();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// import * as Tone from '../node_modules/tone/Tone'
|
|
function midiToNote(midi) {
|
|
const frequency = Frequency(midi, 'midi');
|
|
const ret = frequency.toNote();
|
|
return ret;
|
|
}
|
|
function randomBetween(low, high) {
|
|
return Math.random() * (high - low) + low;
|
|
}
|
|
|
|
function getReleasesUrl(midi) {
|
|
return `rel${midi - 20}.[mp3|ogg]`;
|
|
}
|
|
function getHarmonicsUrl(midi) {
|
|
return `harmS${midiToNote(midi).replace('#', 's')}.[mp3|ogg]`;
|
|
}
|
|
function getNotesUrl(midi, vel) {
|
|
return `${midiToNote(midi).replace('#', 's')}v${vel}.[mp3|ogg]`;
|
|
}
|
|
/**
|
|
* Maps velocity depths to Salamander velocities
|
|
*/
|
|
const velocitiesMap = {
|
|
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],
|
|
};
|
|
/**
|
|
* All the notes of audio samples
|
|
*/
|
|
const allNotes = [
|
|
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 getNotesInRange(min, max) {
|
|
return allNotes.filter(note => min <= note && note <= max);
|
|
}
|
|
/**
|
|
* All the notes of audio samples
|
|
*/
|
|
const harmonics = [21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87];
|
|
function getHarmonicsInRange(min, max) {
|
|
return harmonics.filter(note => min <= note && note <= max);
|
|
}
|
|
function inHarmonicsRange(note) {
|
|
return harmonics[0] <= note && note <= harmonics[harmonics.length - 1];
|
|
}
|
|
|
|
class Harmonics extends PianoComponent {
|
|
constructor(options) {
|
|
super(options);
|
|
this._urls = {};
|
|
const notes = getHarmonicsInRange(options.minNote, options.maxNote);
|
|
for (const n of notes) {
|
|
this._urls[n] = getHarmonicsUrl(n);
|
|
}
|
|
}
|
|
triggerAttack(note, time, velocity) {
|
|
if (this._enabled && inHarmonicsRange(note)) {
|
|
this._sampler.triggerAttack(Midi(note).toNote(), time, velocity * randomBetween(0.5, 1));
|
|
}
|
|
}
|
|
_internalLoad() {
|
|
return new Promise(onload => {
|
|
this._sampler = new Sampler({
|
|
baseUrl: this.samples,
|
|
onload,
|
|
urls: this._urls,
|
|
}).connect(this.output);
|
|
});
|
|
}
|
|
}
|
|
|
|
class Keybed extends PianoComponent {
|
|
constructor(options) {
|
|
super(options);
|
|
/**
|
|
* The urls to load
|
|
*/
|
|
this._urls = {};
|
|
for (let i = options.minNote; i <= options.maxNote; i++) {
|
|
this._urls[i] = getReleasesUrl(i);
|
|
}
|
|
}
|
|
_internalLoad() {
|
|
return new Promise(success => {
|
|
this._buffers = new ToneAudioBuffers(this._urls, success, this.samples);
|
|
});
|
|
}
|
|
start(note, time, velocity) {
|
|
if (this._enabled && this._buffers.has(note)) {
|
|
const source = new ToneBufferSource({
|
|
url: this._buffers.get(note),
|
|
context: this.context,
|
|
}).connect(this.output);
|
|
// randomize the velocity slightly
|
|
source.start(time, 0, undefined, 0.015 * velocity * randomBetween(0.5, 1));
|
|
}
|
|
}
|
|
}
|
|
|
|
class Pedal extends PianoComponent {
|
|
constructor(options) {
|
|
super(options);
|
|
this._downTime = Infinity;
|
|
this._currentSound = null;
|
|
this._downTime = Infinity;
|
|
}
|
|
_internalLoad() {
|
|
return new Promise((success) => {
|
|
this._buffers = new ToneAudioBuffers({
|
|
down1: 'pedalD1.mp3',
|
|
down2: 'pedalD2.mp3',
|
|
up1: 'pedalU1.mp3',
|
|
up2: 'pedalU2.mp3',
|
|
}, success, this.samples);
|
|
});
|
|
}
|
|
/**
|
|
* Squash the current playing sound
|
|
*/
|
|
_squash(time) {
|
|
if (this._currentSound && this._currentSound.state !== 'stopped') {
|
|
this._currentSound.stop(time);
|
|
}
|
|
this._currentSound = null;
|
|
}
|
|
_playSample(time, dir) {
|
|
if (this._enabled) {
|
|
this._currentSound = new ToneBufferSource({
|
|
url: this._buffers.get(`${dir}${Math.random() > 0.5 ? 1 : 2}`),
|
|
context: this.context,
|
|
curve: 'exponential',
|
|
fadeIn: 0.05,
|
|
fadeOut: 0.1,
|
|
}).connect(this.output);
|
|
this._currentSound.start(time, randomBetween(0, 0.01), undefined, 0.1 * randomBetween(0.5, 1));
|
|
}
|
|
}
|
|
/**
|
|
* Put the pedal down
|
|
*/
|
|
down(time) {
|
|
this._squash(time);
|
|
this._downTime = time;
|
|
this._playSample(time, 'down');
|
|
}
|
|
/**
|
|
* Put the pedal up
|
|
*/
|
|
up(time) {
|
|
this._squash(time);
|
|
this._downTime = Infinity;
|
|
this._playSample(time, 'up');
|
|
}
|
|
/**
|
|
* Indicates if the pedal is down at the given time
|
|
*/
|
|
isDown(time) {
|
|
return time > this._downTime;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A single velocity of strings
|
|
*/
|
|
class PianoString extends ToneAudioNode {
|
|
constructor(options) {
|
|
super(options);
|
|
this.name = 'PianoString';
|
|
this._urls = {};
|
|
// create the urls
|
|
options.notes.forEach(note => this._urls[note] = getNotesUrl(note, options.velocity));
|
|
this.samples = options.samples;
|
|
}
|
|
load() {
|
|
return new Promise(onload => {
|
|
this._sampler = this.output = new Sampler({
|
|
attack: 0,
|
|
baseUrl: this.samples,
|
|
curve: 'exponential',
|
|
onload,
|
|
release: 0.4,
|
|
urls: this._urls,
|
|
volume: 3,
|
|
});
|
|
});
|
|
}
|
|
triggerAttack(note, time, velocity) {
|
|
this._sampler.triggerAttack(note, time, velocity);
|
|
}
|
|
triggerRelease(note, time) {
|
|
this._sampler.triggerRelease(note, time);
|
|
}
|
|
}
|
|
|
|
var __awaiter$1 = (undefined && undefined.__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());
|
|
});
|
|
};
|
|
/**
|
|
* Manages all of the hammered string sounds
|
|
*/
|
|
class PianoStrings extends PianoComponent {
|
|
constructor(options) {
|
|
super(options);
|
|
const notes = getNotesInRange(options.minNote, options.maxNote);
|
|
const velocities = velocitiesMap[options.velocities].slice();
|
|
this._strings = velocities.map(velocity => {
|
|
const string = new PianoString(Object.assign(options, {
|
|
notes, velocity,
|
|
}));
|
|
return string;
|
|
});
|
|
this._activeNotes = new Map();
|
|
}
|
|
/**
|
|
* Scale a value between a given range
|
|
*/
|
|
scale(val, inMin, inMax, outMin, outMax) {
|
|
return ((val - inMin) / (inMax - inMin)) * (outMax - outMin) + outMin;
|
|
}
|
|
triggerAttack(note, time, velocity) {
|
|
const scaledVel = this.scale(velocity, 0, 1, -0.5, this._strings.length - 0.51);
|
|
const stringIndex = Math.max(Math.round(scaledVel), 0);
|
|
let gain = 1 + scaledVel - stringIndex;
|
|
if (this._strings.length === 1) {
|
|
gain = velocity;
|
|
}
|
|
const sampler = this._strings[stringIndex];
|
|
if (this._activeNotes.has(note)) {
|
|
this.triggerRelease(note, time);
|
|
}
|
|
this._activeNotes.set(note, sampler);
|
|
sampler.triggerAttack(Midi(note).toNote(), time, gain);
|
|
}
|
|
triggerRelease(note, time) {
|
|
// trigger the release of all of the notes at that velociy
|
|
if (this._activeNotes.has(note)) {
|
|
this._activeNotes.get(note).triggerRelease(Midi(note).toNote(), time);
|
|
this._activeNotes.delete(note);
|
|
}
|
|
}
|
|
_internalLoad() {
|
|
return __awaiter$1(this, void 0, void 0, function* () {
|
|
yield Promise.all(this._strings.map((s) => __awaiter$1(this, void 0, void 0, function* () {
|
|
yield s.load();
|
|
s.connect(this.output);
|
|
})));
|
|
});
|
|
}
|
|
}
|
|
|
|
var __awaiter$2 = (undefined && undefined.__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());
|
|
});
|
|
};
|
|
/**
|
|
* The Piano
|
|
*/
|
|
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$2(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;
|
|
}
|
|
}
|
|
|
|
var __awaiter$3 = (undefined && undefined.__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());
|
|
});
|
|
};
|
|
|
|
export { Piano };
|