sono
Version:
A simple yet powerful JavaScript library for working with Web Audio
459 lines (378 loc) • 12 kB
JavaScript
import context from './context';
import BufferSource from './source/buffer-source';
import Effects from './effects';
import Emitter from './utils/emitter';
import file from './utils/file';
import firefox from './utils/firefox';
import utils from './utils/utils';
import isSafeNumber from './utils/isSafeNumber';
import Loader from './utils/loader';
import AudioSource from './source/audio-source';
import MediaSource from './source/media-source';
import MicrophoneSource from './source/microphone-source';
import OscillatorSource from './source/oscillator-source';
export default class Sound extends Emitter {
constructor(config) {
super();
this.id = config.id || null;
this._context = config.context || context;
this._destination = config.destination || this._context.destination;
this._effects = new Effects(this._context);
this._gain = this._context.createGain();
this._config = config;
this._data = null;
this._fadeTimeout = null;
this._isTouchLocked = false;
this._loader = null;
this._loop = false;
this._offset = 0;
this._playbackRate = 1;
this._playWhenReady = null;
this._source = null;
this._wave = null;
this._userData = {};
this._effects.setDestination(this._gain);
this._gain.connect(this._destination);
this._onEnded = this._onEnded.bind(this);
this._onLoad = this._onLoad.bind(this);
this._onLoadError = this._onLoadError.bind(this);
}
prepare(newConfig = null, force = false) {
const skipLoad = !force && !this._source && !!this._config.deferLoad;
if (newConfig) {
const configSrc = file.getSrc(newConfig);
const src = file.getSupportedFile(configSrc) || this._config.src;
this._config = Object.assign(this._config, newConfig, {src});
}
if (this._source && this._data && this._data.tagName) {
this._source.load(this._config.src);
} else {
this._loader = new Loader(this._config.src, skipLoad);
this._loader.audioContext = !!this._config.asMediaElement || this._context.isFake ? null : this._context;
this._loader.isTouchLocked = this._isTouchLocked;
this._loader.once('loaded', this._onLoad);
this._loader.once('error', this._onLoadError);
}
return this;
}
load(config = null) {
this.stop();
this._source = null;
if (!config || file.containsURL(config)) {
if (this._loader) {
this._loader.destroy();
}
this.prepare(config, true);
this._loader.start();
} else {
this.data = config.data || config;
}
return this;
}
play(delay, offset) {
if (!this._source || this._isTouchLocked) {
this._playWhenReady = () => {
if (this._source) {
this.play(delay, offset);
}
};
if (!!this._config.deferLoad) {
if (!this._loader) {
this.prepare(null, true);
}
this._loader.start(true);
}
return this;
}
this._playWhenReady = null;
this._effects.setSource(this._source.sourceNode);
if (this._offset && typeof offset === 'undefined') {
offset = this._offset;
this._offset = 0;
}
this._source.play(delay, offset);
if (this._source.hasOwnProperty('volume')) {
this._source.volume = this._gain.gain.value;
}
if (this._source.hasOwnProperty('loop')) {
this._source.loop = this._loop;
}
this.emit('play', this);
return this;
}
pause() {
this._source && this._source.pause();
this.emit('pause', this);
return this;
}
stop(delay) {
this._source && this._source.stop(delay || 0);
this.emit('stop', this);
return this;
}
seek(value) {
this.currentTime = value;
return this;
}
fade(volume, duration = 1) {
if (!this._source) {
return this;
}
const param = this._gain.gain;
if (this._context && !this._context.isFake && !firefox) {
const time = this._context.currentTime;
param.cancelScheduledValues(time);
param.setValueAtTime(param.value, time);
param.linearRampToValueAtTime(volume, time + duration);
} else {
this._fadePolyfill(volume, duration);
}
this.emit('fade', this, volume);
return this;
}
_fadePolyfill(toVolume, duration) {
const ramp = (value, step) => {
this._fadeTimeout = window.setTimeout(() => {
this.volume = this.volume + (value - this.volume) * 0.2;
if (Math.abs(this.volume - value) > 0.05) {
ramp(value, step);
return;
}
this.volume = value;
}, step * 1000);
};
window.clearTimeout(this._fadeTimeout);
ramp(toVolume, duration / 10);
return this;
}
unload() {
this._source && this._source.destroy();
this._loader && this._loader.destroy();
this._data = null;
this._playWhenReady = null;
this._source = null;
this._loader = null;
this._config.deferLoad = true;
this.emit('unload', this);
}
reload() {
return this.load(null, true);
}
destroy() {
this._source && this._source.destroy();
this._effects && this._effects.destroy();
this._gain && this._gain.disconnect();
if (this._loader) {
this._loader.off('loaded');
this._loader.off('error');
this._loader.destroy();
}
this._gain = null;
this._context = null;
this._destination = null;
this._data = null;
this._playWhenReady = null;
this._source = null;
this._effects = null;
this._loader = null;
this._config = null;
this.emit('destroy', this);
this.off();
}
waveform(length) {
if (!this._wave) {
this._wave = utils.waveform();
}
if (!this._data) {
this.once('ready', () => this._wave(this._data, length));
}
return this._wave(this._data, length);
}
get context() {
return this._context;
}
get currentTime() {
return this._source ? this._source.currentTime : this._offset;
}
set currentTime(value) {
if (this._source) {
const playing = this._source.playing;
this._source.stop();
this._source.currentTime = value;
if (playing) {
this.play(0, value);
}
} else {
this._offset = value;
}
}
get data() {
return this._data;
}
set data(value) {
if (!value) {
return;
}
this._data = value;
this._createSource(value);
}
get duration() {
return this._source ? this._source.duration : 0;
}
get effects() {
return this._effects._nodes;
}
set effects(value) {
this._effects.removeAll().add(value);
}
get fx() {
return this.effects;
}
set fx(value) {
this.effects = value;
}
get ended() {
return !!this._source && this._source.ended;
}
get frequency() {
return this._source ? this._source.frequency : 0;
}
set frequency(value) {
if (this._source && this._source.hasOwnProperty('frequency')) {
this._source.frequency = value;
}
}
get gain() {
return this._gain;
}
// for media element source
get groupVolume() {
return this._source.groupVolume;
}
set groupVolume(value) {
if (this._source && this._source.hasOwnProperty('groupVolume')) {
this._source.groupVolume = value;
}
}
set isTouchLocked(value) {
this._isTouchLocked = value;
if (this._loader) {
this._loader.isTouchLocked = value;
}
if (!value && this._playWhenReady) {
this._playWhenReady();
}
}
get loader() {
return this._loader;
}
get loop() {
return this._loop;
}
set loop(value) {
this._loop = !!value;
if (this._source && this._source.hasOwnProperty('loop') && this._source.loop !== this._loop) {
this._source.loop = this._loop;
}
}
get singlePlay() {
return this._config.singlePlay;
}
set singlePlay(value) {
this._config.singlePlay = value;
this._source.singlePlay = value;
}
get config() {
return this._config;
}
get paused() {
return !!this._source && this._source.paused;
}
get playing() {
return !!this._source && this._source.playing;
}
get playbackRate() {
return this._playbackRate;
}
set playbackRate(value) {
this._playbackRate = value;
if (this._source) {
this._source.playbackRate = value;
}
}
get progress() {
return this._source ? this._source.progress || 0 : 0;
}
get sourceInfo() {
return this._source && this._source.info ? this._source.info : {};
}
get sourceNode() {
return this._source ? this._source.sourceNode : null;
}
get volume() {
return this._gain.gain.value;
}
set volume(value) {
if (!isSafeNumber(value)) {
return;
}
window.clearTimeout(this._fadeTimeout);
value = Math.min(Math.max(value, 0), 1);
const param = this._gain.gain;
const time = this._context.currentTime;
param.cancelScheduledValues(time);
param.value = value;
if (!firefox) {
param.setValueAtTime(value, time);
}
if (this._source && this._source.hasOwnProperty('volume')) {
this._source.volume = value;
}
}
get userData() {
return this._userData;
}
_createSource(data) {
const isAudioBuffer = file.isAudioBuffer(data);
if (isAudioBuffer || file.isMediaElement(data)) {
const Fn = isAudioBuffer ? BufferSource : MediaSource;
this._source = new AudioSource(Fn, data, this._context, this._onEnded);
this._source.singlePlay = !!this._config.singlePlay;
this._source.playbackRate = this._playbackRate;
this._source.currentTime = this._offset;
} else if (file.isMediaStream(data)) {
this._source = new MicrophoneSource(data, this._context);
} else if (file.isOscillatorType((data && data.type) || data)) {
this._source = new OscillatorSource(data.type || data, this._context);
} else {
throw new Error('Cannot detect data type: ' + data);
}
this._effects.setSource(this._source.sourceNode);
this.emit('ready', this);
if (this._playWhenReady) {
this._playWhenReady();
}
}
_onEnded() {
this.emit('ended', this);
}
_onLoad(data) {
this._data = data;
this.emit('loaded', this);
this._createSource(data);
}
_onLoadError(err) {
if (this.listenerCount('error')) {
this.emit('error', this, err);
return;
}
console.error('Sound load error', this._loader.url);
}
}
// expose for unit tests
Sound.__source = {
BufferSource,
MediaSource,
MicrophoneSource,
OscillatorSource
};