@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
126 lines (114 loc) • 3.19 kB
text/typescript
/**
* allows to import multiple audio files to use as samples
*
*
*/
import {TypedAudioNode} from './_Base';
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {AudioBuilder} from '../../../core/audio/AudioBuilder';
import {Sampler, SamplerOptions} from 'tone/build/esm/instrument/Sampler';
import {PolyDictionary} from '../../../types/GlobalTypes';
import {StringParamLanguage} from '../../params/utils/OptionsController';
import {sanitizeUrl} from '../../../core/UrlHelper';
import {CoreLoaderAudio} from '../../../core/loader/Audio';
import {Poly} from '../../Poly';
import {AudioType} from '../../poly/registers/nodes/types/Audio';
export const SAMPLER_DEFAULTS: Partial<SamplerOptions> = {
attack: 0.01,
release: 1,
};
const DEFAULT_NOTE_MAP = {
A0: 'A0',
C1: 'C1',
'D#1': 'Ds1',
'F#1': 'Fs1',
A1: 'A1',
C2: 'C2',
'D#2': 'Ds2',
'F#2': 'Fs2',
A2: 'A2',
C3: 'C3',
'D#3': 'Ds3',
'F#3': 'Fs3',
A3: 'A3',
C4: 'C4',
'D#4': 'Ds4',
'F#4': 'Fs4',
A4: 'A4',
C5: 'C5',
'D#5': 'Ds5',
'F#5': 'Fs5',
A5: 'A5',
C6: 'C6',
'D#6': 'Ds6',
'F#6': 'Fs6',
A6: 'A6',
C7: 'C7',
'D#7': 'Ds7',
'F#7': 'Fs7',
A7: 'A7',
C8: 'C8',
};
class SamplerAudioParamsConfig extends NodeParamsConfig {
/** @param map of notes to load */
urlsMap = ParamConfig.STRING(JSON.stringify(DEFAULT_NOTE_MAP, null, 2), {
language: StringParamLanguage.JSON,
});
/** @param base url */
baseUrl = ParamConfig.STRING('./');
/** @param extension */
extension = ParamConfig.STRING('mp3');
}
const ParamsConfig = new SamplerAudioParamsConfig();
export class SamplerAudioNode extends TypedAudioNode<SamplerAudioParamsConfig> {
override paramsConfig = ParamsConfig;
static override type() {
return AudioType.SAMPLER;
}
override initializeNode() {
this.io.inputs.setCount(1);
}
override dispose(): void {
super.dispose();
Poly.blobs.clearBlobsForNode(this);
}
override async cook(inputContents: AudioBuilder[]) {
const envelopeBuilder = inputContents[0];
const envelopeParams = envelopeBuilder.envelopeParams() || SAMPLER_DEFAULTS;
let urlsJSON: PolyDictionary<string> = {};
let urls: string[] = [];
try {
const extension = this.pv.extension;
urlsJSON = JSON.parse(this.pv.urlsMap);
const noteNames = Object.keys(urlsJSON);
for (const noteName of noteNames) {
const urlFileName = urlsJSON[noteName];
const url = `${this.pv.baseUrl}/${urlFileName}.${extension}`;
urlsJSON[noteName] = sanitizeUrl(url);
urls.push(url);
}
} catch (err) {
this.states.error.set('urlsMap is invalid JSON');
}
// preload urls for export
const loaders: CoreLoaderAudio[] = urls.map(
(url) => new CoreLoaderAudio(url, this, {multiAssetsForNode: true})
);
await Promise.all(loaders.map((loader) => loader.load()));
// create sample
const sampler = new Sampler({
urls: urlsJSON,
attack: envelopeParams.attack,
release: envelopeParams.release,
baseUrl: '',
onerror: (err) => {
this.states.error.set(err.message);
},
onload: () => {
const audioBuilder = new AudioBuilder();
audioBuilder.setInstrument(sampler);
this.setAudioBuilder(audioBuilder);
},
});
}
}