94 |
--------------------------------------------------------------------------------
/docs/effects/flanger.md:
--------------------------------------------------------------------------------
1 | # Flanger
2 |
3 | [View source code](../../src/effects/flanger.js)
4 |
5 | [View example](http://stinkstudios.github.io/sono/examples/flanger.html)
6 |
7 | Creates a sweeping filter effect
8 |
9 | ```javascript
10 | import flanger from 'sono/effects/flanger';
11 |
12 | const sound = sono.create('boom.mp3');
13 |
14 | const flange = sound.effects.add(flanger({
15 | stereo: true,
16 | delay: 0.005,
17 | feedback: 0.5,
18 | frequency: 0.025,
19 | gain: 0.002
20 | }));
21 | ```
22 |
23 | ## Stereo flanger
24 |
25 | ```javascript
26 | import {stereoFlanger} from 'sono/effects/flanger';
27 |
28 | const sound = sono.create('boom.mp3');
29 |
30 | const flange = sound.effects.add(stereoFlanger({
31 | delay: 0.005,
32 | feedback: 0.5,
33 | frequency: 0.025,
34 | gain: 0.002
35 | }));
36 | ```
37 |
38 | ## Mono flanger
39 |
40 | ```javascript
41 | import {monoFlanger} from 'sono/effects/flanger';
42 |
43 | const sound = sono.create('boom.mp3');
44 |
45 | const flange = sound.effects.add(monoFlanger({
46 | delay: 0.005,
47 | feedback: 0.5,
48 | frequency: 0.025,
49 | gain: 0.002
50 | }));
51 | ```
52 |
--------------------------------------------------------------------------------
/docs/effects/panner.md:
--------------------------------------------------------------------------------
1 | # Panner
2 |
3 | [View source code](../../src/effects/panner.js)
4 |
5 | [View example](http://stinkstudios.github.io/sono/examples/three.html)
6 |
7 | ## Pan left/right
8 |
9 | ```javascript
10 | import panner from 'sono/effects/panner';
11 |
12 | const sound = sono.create('boom.mp3');
13 |
14 | const pan = sound.effects.add(panner());
15 |
16 | // pan fully right:
17 | pan.set(1);
18 |
19 | // pan fully left:
20 | pan.set(-1);
21 | ```
22 |
23 | ## 3d panning
24 |
25 | Pass vectors of the 'listener' (i.e. the camera) and the origin of the sound to play the sound in 3d audio.
26 |
27 | The listener is global and doesn't need to be updated for every panner object.
28 |
29 | ```javascript
30 | import panner from 'sono/effects/panner';
31 |
32 | const sound = sono.create('boom.mp3');
33 |
34 | const pan = sound.effects.add(panner());
35 |
36 | function update() {
37 | window.requestAnimationFrame(update);
38 |
39 | // update the 3d position and orientation (forward vector) of the sound
40 | pan.setPosition(x, y, z);
41 | pan.setOrientation(x, y, z);
42 |
43 | // update listener position and orientation to 3d camera Vectors
44 | pan.setListenerPosition(x, y, z);
45 | pan.setListenerOrientation(x, y, z);
46 | }
47 | ```
48 |
49 | ## Pass xyz or 3d vector objects
50 |
51 | ```javascript
52 | // accepts xyz or a 3d vector object
53 | pan.setPosition(source.position);
54 | pan.setOrientation(source.forward);
55 |
56 | pan.setListenerPosition(camera.position);
57 | pan.setListenerOrientation(camera.forward);
58 | ```
59 |
60 | ## Set global listener position and orientation using static methods
61 |
62 | ```javascript
63 | import panner from 'sono/effects/panner';
64 |
65 | function update() {
66 | window.requestAnimationFrame(update);
67 |
68 | panner.setListenerPosition(x, y, z);
69 | panner.setListenerOrientation(x, y, z);
70 | }
71 | ```
72 |
73 | ## Configure how distance and angle affect the sound
74 |
75 | ```javascript
76 | import panner from 'sono/effects/panner';
77 |
78 | const sound = sono.create('boom.mp3');
79 |
80 | const pan = sound.effects.add(panner({
81 | panningModel: 'HRTF',
82 | distanceModel: 'linear',
83 | refDistance: 1,
84 | maxDistance: 1000,
85 | rolloffFactor: 1,
86 | coneInnerAngle: 360,
87 | coneOuterAngle: 0,
88 | coneOuterGain: 0
89 | }));
90 | ```
91 |
92 | ## Set defaults for all panner nodes
93 |
94 | ```javascript
95 | import panner from 'sono/effects/panner';
96 |
97 | panner.defaults = {
98 | panningModel: 'HRTF',
99 | distanceModel: 'linear',
100 | refDistance: 1,
101 | maxDistance: 1000,
102 | rolloffFactor: 1,
103 | coneInnerAngle: 360,
104 | coneOuterAngle: 0,
105 | coneOuterGain: 0
106 | };
107 | ```
108 |
--------------------------------------------------------------------------------
/docs/effects/phaser.md:
--------------------------------------------------------------------------------
1 | # Phaser
2 |
3 | [View source code](../../src/effects/phaser.js)
4 |
5 | [View example](http://stinkstudios.github.io/sono/examples/phaser.html)
6 |
7 | Creates a sweeping filter effect
8 |
9 | ```javascript
10 | import phaser from 'sono/effects/phaser';
11 |
12 | const sound = sono.create('boom.mp3');
13 |
14 | const phaser = sound.effects.add(phaser({
15 | stages: 8,
16 | frequency: 0.5,
17 | gain: 300,
18 | feedback: 0.5
19 | }));
20 | ```
21 |
--------------------------------------------------------------------------------
/docs/effects/reverb.md:
--------------------------------------------------------------------------------
1 | # Reverb
2 |
3 | [View source code](../../src/effects/reverb.js)
4 |
5 | [View example](http://stinkstudios.github.io/sono/examples/reverb.html)
6 |
7 | ```javascript
8 | import reverb from 'sono/effects/reverb';
9 |
10 | const sound = sono.create('boom.mp3');
11 | const room = sound.effects.add(reverb({time: 1, decay: 5}));
12 | sound.play();
13 |
14 | // update multiple properties:
15 | room.update({
16 | time: 0.5,
17 | decay: 3,
18 | reverse: true
19 | });
20 | ```
21 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | ## Install
4 |
5 | ```javascript
6 | npm i -S sono
7 | ```
8 |
9 | ## Import
10 |
11 | ```javascript
12 | import sono from 'sono';
13 | ```
14 |
15 | ## Create a sound object
16 |
17 | Use the returned reference:
18 | ```javascript
19 | const sound = sono.create('boom.mp3');
20 | sound.play();
21 | ```
22 |
23 | Or use an Id:
24 | ```javascript
25 | sono.create({
26 | id: 'boom',
27 | url: 'boom.mp3'
28 | });
29 | sono.play('boom');
30 | ```
31 |
32 | ## Add some effects
33 |
34 | Set an array of effects:
35 | ```javascript
36 | import echo from 'sono/effects/echo';
37 | import reverb from 'sono/effects/reverb';
38 |
39 | const sound = sono.create('boom.mp3');
40 | sound.effects = [echo(), reverb()];
41 | sound.play();
42 | ```
43 |
44 | Or use the `add` function to return the reference:
45 | ```javascript
46 | import echo from 'sono/effects/echo';
47 | import reverb from 'sono/effects/reverb';
48 |
49 | const sound = sono.create('boom.mp3');
50 | const echo = sound.effects.add(echo());
51 | const reverb = sound.effects.add(reverb());
52 | sound.play();
53 | ```
54 |
55 | ## Log info on browser support
56 |
57 | ```javascript
58 | import sono from 'sono';
59 | sono.log(); // sono 0.2.0 Supported:true WebAudioAPI:true TouchLocked:false Extensions:ogg,mp3,opus,wav,m4a
60 | ```
61 |
62 | ## Further documentation
63 |
64 | [Sounds](./sounds.md)
65 |
66 | [Effects](./effects.md)
67 |
68 | [Controls](./controls.md)
69 |
70 | [Utils](./utils.md)
71 |
--------------------------------------------------------------------------------
/docs/loading.md:
--------------------------------------------------------------------------------
1 | # Loading
2 |
3 | ## Load multiple with config and callbacks
4 |
5 | ```javascript
6 | sono.load({
7 | url: [{
8 | id: 'foo',
9 | url: 'foo.mp3'
10 | }, {
11 | id: 'bar',
12 | url: ['bar.ogg', 'bar.mp3'],
13 | loop: true,
14 | volume: 0.5
15 | }],
16 | onComplete: sounds => console.log(sounds),
17 | onProgress: progress => console.log(progress)
18 | });
19 |
20 | const foo = sono.get('foo');
21 | sono.play('bar');
22 | ```
23 |
24 | ## Load single with config options and callbacks
25 |
26 | ```javascript
27 | const sound = sono.load({
28 | id: 'foo',
29 | src: ['foo.ogg', 'foo.mp3'],
30 | loop: true,
31 | volume: 0.2,
32 | onComplete: sound => console.log(sound),
33 | onProgress: progress => console.log(progress)
34 | });
35 | ```
36 |
37 | ## Load single
38 |
39 | ```javascript
40 | sono.load({
41 | url: 'foo.mp3',
42 | onComplete: sound => console.log(sound),
43 | onProgress: progress => console.log(progress)
44 | });
45 | ```
46 |
47 | ## Load multiple
48 |
49 | ```javascript
50 | sono.load({
51 | url: [
52 | {url: 'foo.mp3'},
53 | {url: 'bar.mp3'}
54 | ],
55 | onComplete: sounds => console.log(sounds),
56 | onProgress: progress => console.log(progress)
57 | });
58 | ```
59 |
60 | ## Check support
61 |
62 | ```javascript
63 | sono.isSupported;
64 | sono.hasWebAudio;
65 |
66 | sono.canPlay.ogg;
67 | sono.canPlay.mp3;
68 | sono.canPlay.opus
69 | sono.canPlay.wav;
70 | sono.canPlay.m4a;
71 | ```
72 |
--------------------------------------------------------------------------------
/docs/sounds.md:
--------------------------------------------------------------------------------
1 | # Sound
2 |
3 | [View source code](../src/core/sound.js)
4 |
5 | ## Create
6 |
7 | ```javascript
8 | const sound = sono.create('boom.mp3');
9 | ```
10 |
11 | ## Create and configure
12 |
13 | ```javascript
14 | const sound = sono.create({
15 | id: 'boom',
16 | url: ['boom.ogg', 'boom.mp3'],
17 | loop: true,
18 | volume: 0.5,
19 | effects: [
20 | sono.echo()
21 | ]
22 | });
23 | ```
24 |
25 | ## Create an oscillator
26 |
27 | ```javascript
28 | const squareWave = sono.create('square');
29 | squareWave.frequency = 200;
30 | ```
31 |
32 | ## Create from HTMLMediaElement
33 |
34 | ```javascript
35 | const video = document.querySelector('video');
36 | const videoSound = sono.create(videoEl);
37 | ```
38 |
39 | ## Control and update
40 |
41 | ```javascript
42 | const sound = sono.create('boom.mp3');
43 | // playback
44 | sound.play();
45 | sound.pause();
46 | sound.stop();
47 | // play with 200ms delay
48 | sound.play(0.2);
49 | // set volume
50 | sound.volume = 0.5;
51 | // fade out volume to 0 over 2 seconds
52 | sound.fade(0, 2);
53 | // seek to 0.5 seconds
54 | sound.seek(0.5);
55 | // play sound at double speed
56 | sound.playbackRate = 2;
57 | // play sound at half speed
58 | sound.playbackRate = 0.5;
59 | // loop
60 | sound.loop = true;
61 | // play at 3s
62 | sound.currentTime = 3;
63 | ```
64 |
65 | ## Get properties
66 |
67 | ```javascript
68 | const sound = sono.create('boom.mp3');
69 | console.log(sound.context);
70 | console.log(sound.currentTime);
71 | console.log(sound.duration);
72 | console.log(sound.effects);
73 | console.log(sound.ended);
74 | console.log(sound.loop);
75 | console.log(sound.paused);
76 | console.log(sound.playing);
77 | console.log(sound.progress);
78 | console.log(sound.volume);
79 | console.log(sound.playbackRate);
80 | ```
81 |
82 | ## Special properties
83 |
84 | ```javascript
85 | // raw sound data (AudioBuffer, MediaElement, MediaStream, Oscillator type)
86 | console.log(sound.data);
87 | // frequency for Oscillator source type
88 | console.log(sound.frequency);
89 | // output node (GainNode)
90 | console.log(sound.gain);
91 | ```
92 |
93 | ## Methods can be chained
94 |
95 | ```javascript
96 | const sound = sono.create({
97 | id: 'boom',
98 | url: 'boom.ogg',
99 | volume: 0
100 | })
101 | .on('ended', sound => dispatch('ended', sound.id))
102 | .play()
103 | .fade(1, 2)
104 | ```
105 |
106 | ## Add and remove event listeners
107 |
108 | ```javascript
109 | sono.create('boom.ogg')
110 | .on('pause', sound => dispatch('pause', sound))
111 | .on('play', sound => dispatch('play', sound))
112 | .once('ended', sound => {
113 | sound.off('pause');
114 | sound.off('play');
115 | dispatch('ended', sound);
116 | })
117 | .play();
118 | ```
119 |
120 | ## Methods
121 |
122 | ```javascript
123 | sound.play(delay, offset)
124 | sound.pause()
125 | sound.stop()
126 | sound.seek(time)
127 | sound.fade(volume, duration)
128 | sound.unload()
129 | sound.reload()
130 | sound.destroy()
131 | sound.waveform(length)
132 | ```
133 |
134 | ## Properties
135 |
136 | ```javascript
137 | sound.context
138 | sound.currentTime
139 | sound.data
140 | sound.duration
141 | sound.effects
142 | sound.ended
143 | sound.frequency
144 | sound.gain
145 | sound.loop
146 | sound.paused
147 | sound.playbackRate
148 | sound.playing
149 | sound.progress
150 | sound.volume
151 | ```
152 |
153 | ## Events
154 |
155 | ```javascript
156 | sound
157 | .on('loaded', (sound) => console.log('loaded'))
158 | .on('ready', (sound) => console.log('ready'))
159 | .on('play', (sound) => console.log('play'))
160 | .on('pause', (sound) => console.log('pause'))
161 | .on('stop', (sound) => console.log('stop'))
162 | .on('fade', (sound, volume) => console.log('fade'))
163 | .on('ended', (sound) => console.log('ended'))
164 | .on('unload', (sound) => console.log('unload'))
165 | .on('error', (sound, err) => console.error('error'))
166 | .on('destroy', (sound) => console.log('destroy'));
167 | ```
168 |
--------------------------------------------------------------------------------
/docs/utils.md:
--------------------------------------------------------------------------------
1 | # Utils
2 |
3 | ## Clone an AudioBuffer
4 |
5 | ```javascript
6 | const cloned = sono.utils.cloneBuffer(sound.data);
7 | ```
8 |
9 | ## Reverse an AudioBuffer
10 |
11 | ```javascript
12 | const reversed = sono.utils.reverseBuffer(sound.data);
13 | ```
14 |
15 | ## Convert currentTime seconds into time code string
16 |
17 | ```javascript
18 | const timeCode = sono.utils.timeCode(217.8); // '03:37'
19 | ```
20 |
21 | ## Extra utils
22 |
23 | ```javascript
24 | import 'sono/utils';
25 | ```
26 |
27 | [microphone](./utils/microphone.md)
28 |
29 | [recorder](./utils/recorder.md)
30 |
31 | [waveform](./utils/waveform.md)
32 |
33 | [waveformer](./utils/waveformer.md)
34 |
--------------------------------------------------------------------------------
/docs/utils/microphone.md:
--------------------------------------------------------------------------------
1 | # Microphone
2 |
3 | [View source code](../../src/utils/microphone.js)
4 |
5 | ```javascript
6 | import microphone from 'sono/utils';
7 | import analyser from 'sono/effects';
8 |
9 | const mic = microphone(stream => {
10 | // user allowed mic
11 | const sound = sono.create(stream);
12 | const analyse = sound.effects.add(analyser());
13 | }, err => {
14 | // user denied mic
15 | }, err => {
16 | // error
17 | });
18 | mic.connect();
19 | ```
20 |
--------------------------------------------------------------------------------
/docs/utils/recorder.md:
--------------------------------------------------------------------------------
1 | # Recorder
2 |
3 | [View source code](../../src/utils/recorder.js)
4 |
5 | ## Record audio from the mix or microphone to a new audio buffer
6 |
7 | ```javascript
8 | import recorder from 'sono/utils/recorder';
9 |
10 | const record = recorder()
11 |
12 | record.start(sound)
13 |
14 | record.getDuration();
15 |
16 | const buffer = record.stop();
17 | ```
18 |
19 | ## Record a microphone stream
20 |
21 | ```javascript
22 | import 'sono/utils/recorder';
23 | import 'sono/utils/microphone';
24 |
25 | let micSound;
26 | let recorder;
27 |
28 | function onMicConnected(stream) {
29 | micSound = sono.create(stream);
30 | // add recorder, setting passThrough to false
31 | // to avoid feedback loop between mic and speakers
32 | recorder = sono.utils.recorder(false);
33 | recorder.start(micSound);
34 | };
35 |
36 | stopButton.addEventListener('click', function() {
37 | const buffer = recorder.stop();
38 | const recordedSound = sono.create(buffer);
39 | recordedSound.play();
40 | });
41 |
42 | const mic = sono.utils.microphone(onMicConnected);
43 | mic.connect();
44 | ```
45 |
--------------------------------------------------------------------------------
/docs/utils/waveform.md:
--------------------------------------------------------------------------------
1 | # Waveform
2 |
3 | [View source code](../../src/utils/waveform.js)
4 |
5 | ```javascript
6 | import sono from 'sono';
7 | import 'sono/utils/waveform';
8 |
9 | const sound = sono.create('boom.mp3');
10 | sound.on('ready', () => {
11 | // request sound waveform
12 | const waveform = sound.waveform(640);
13 |
14 | // draw waveform
15 | for (let i = 0; i < waveform.length; i++) {
16 | const value = waveform[i];
17 | }
18 | });
19 | ```
20 |
--------------------------------------------------------------------------------
/docs/utils/waveformer.md:
--------------------------------------------------------------------------------
1 | # Waveformer
2 |
3 | [View source code](../../src/utils/waveformer.js)
4 |
5 | ## Get a sound's waveform and draw it to a canvas element:
6 |
7 | ```javascript
8 | const wave = sono.utils.waveformer({
9 | sound: sound,
10 | width: 200,
11 | height: 100,
12 | color: '#333333',
13 | bgColor: '#DDDDDD'
14 | });
15 | document.body.appendChild(wave.canvas);
16 | ```
17 |
18 | ## Supply your own canvas el
19 |
20 | ```javascript
21 | const canvasEl = document.querySelector('canvas');
22 | const wave = sono.utils.waveformer({
23 | waveform: sound.waveform(canvasEl.width),
24 | canvas: canvasEl,
25 | color: 'green'
26 | });
27 | ```
28 |
29 | ## Color can be a function
30 |
31 | ```javascript
32 | const waveformer = sono.utils.waveformer({
33 | waveform: sound.waveform(canvasEl.width),
34 | canvas: canvasEl,
35 | color: (position, length) => {
36 | return position / length < sound.progress ? 'red' : 'yellow';
37 | }
38 | });
39 | ```
40 |
41 | ## Shape can be circular
42 |
43 | ```javascript
44 | const waveformer = sono.utils.waveformer({
45 | shape: 'circular',
46 | sound: sound,
47 | canvas: canvasEl,
48 | color: 'black'
49 | });
50 | ```
51 |
52 | ## Draw the output of an AnalyserNode to a canvas
53 |
54 | ```javascript
55 | const sound = sono.create('foo.ogg');
56 | const analyser = sound.effects.add(sono.analyser({
57 | fftSize: 512,
58 | smoothing: 0.7
59 | }));
60 |
61 | const waveformer = sono.utils.waveformer({
62 | waveform: analyser.getFrequencies(),
63 | canvas: document.querySelector('canvas'),
64 | color: (position, length) => {
65 | const hue = (position / length) * 360;
66 | return `hsl(${hue}, 100%, 40%)`;
67 | },
68 | // normalise the value from the analyser
69 | transform: value => value / 256
70 | });
71 |
72 | // update the waveform
73 | function update() {
74 | window.requestAnimationFrame(update);
75 | // request frequencies from the analyser
76 | analyser.getFrequencies();
77 | // update the waveformer display
78 | waveformer();
79 | }
80 | update();
81 | ```
82 |
--------------------------------------------------------------------------------
/examples/analyser.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | sono - examples - 3d
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 | import sono from 'sono';
25 | import 'sono/effects';
26 |
27 | const sound = sono.create({
28 | url: ['beats.ogg', 'beats.mp3'],
29 | loop: true
30 | })
31 | .play();
32 |
33 | const analyser = sound.effects.add(sono.analyser({
34 | fftSize: 128
35 | }));
36 |
37 | function averageAmplitude(wave) {
38 | let sum = 0;
39 | for (let i = 0; i < wave.length; i++) {
40 | sum += wave[i];
41 | }
42 | return sum / wave.length / 256;
43 | }
44 |
45 | function update() {
46 | window.requestAnimationFrame(update);
47 |
48 | const value = averageAmplitude(analyser.getWaveform());
49 |
50 | if (value < min) {
51 | min = value;
52 | }
53 |
54 | if (value > max) {
55 | max = value;
56 | }
57 |
58 | const range = (max - min) || 1;
59 | const norm = (value - min) / range;
60 |
61 | if (norm > threshold) {
62 | // got a peak so animate, speed up, glow etc
63 | }
64 | }
65 | update();
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/examples/audio/.gitinclude:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/.gitinclude
--------------------------------------------------------------------------------
/examples/audio/.htaccess:
--------------------------------------------------------------------------------
1 |
2 | Header add Access-Control-Allow-Origin "*"
3 | Header add Access-Control-Allow-Methods: "GET,POST,OPTIONS,DELETE,PUT"
4 | Header add Access-Control-Allow-Headers: "Content-Type"
5 |
6 | RewriteEngine on
7 | RewriteBase /
8 |
--------------------------------------------------------------------------------
/examples/audio/bullet.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/bullet.mp3
--------------------------------------------------------------------------------
/examples/audio/bullet.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/bullet.ogg
--------------------------------------------------------------------------------
/examples/audio/collect.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/collect.mp3
--------------------------------------------------------------------------------
/examples/audio/collect.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/collect.ogg
--------------------------------------------------------------------------------
/examples/audio/dnb-loop.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/dnb-loop.mp3
--------------------------------------------------------------------------------
/examples/audio/dnb-loop.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/dnb-loop.ogg
--------------------------------------------------------------------------------
/examples/audio/hit.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/hit.mp3
--------------------------------------------------------------------------------
/examples/audio/hit.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/hit.ogg
--------------------------------------------------------------------------------
/examples/audio/select.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/select.mp3
--------------------------------------------------------------------------------
/examples/audio/select.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/select.ogg
--------------------------------------------------------------------------------
/examples/background.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | sono - examples - play in background
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | play in background
17 |
18 |
19 |
23 |
24 |
25 | import sono from 'sono';
26 |
27 | sono.playInBackground = true;
28 |
29 | sono.create({url: 'dnb-loop.ogg', loop: true}).play();
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/examples/css/index.css:
--------------------------------------------------------------------------------
1 | @import "normalize.css";
2 | @import "sections.css";
3 | @import "player.css";
4 | @import "control.css";
5 |
--------------------------------------------------------------------------------
/examples/css/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */
2 |
3 | html {
4 | -ms-text-size-adjust: 100%;
5 | -webkit-text-size-adjust: 100%;
6 | }
7 |
8 | article,
9 | aside,
10 | details,
11 | figcaption,
12 | figure,
13 | footer,
14 | header,
15 | hgroup,
16 | main,
17 | menu,
18 | nav,
19 | section,
20 | summary {
21 | display: block;
22 | }
23 |
24 | audio,
25 | canvas,
26 | progress,
27 | video {
28 | display: inline-block;
29 | vertical-align: baseline;
30 | }
31 |
32 | audio:not([controls]) {
33 | display: none;
34 | height: 0;
35 | }
36 |
37 | [hidden],
38 | template {
39 | display: none;
40 | }
41 |
42 | a {
43 | background-color: transparent;
44 | }
45 |
46 | a:active,
47 | a:hover {
48 | outline: 0;
49 | }
50 |
51 | abbr[title] {
52 | border-bottom: 1px dotted;
53 | }
54 |
55 | b,
56 | strong {
57 | font-weight: 700;
58 | }
59 |
60 | dfn {
61 | font-style: italic;
62 | }
63 |
64 | h1 {
65 | font-size: 2em;
66 | }
67 |
68 | mark {
69 | background: #ff0;
70 | color: #000;
71 | }
72 |
73 | small {
74 | font-size: 80%;
75 | }
76 |
77 | sub,
78 | sup {
79 | font-size: 75%;
80 | line-height: 0;
81 | position: relative;
82 | vertical-align: baseline;
83 | }
84 |
85 | sup {
86 | top: -.5em;
87 | }
88 |
89 | sub {
90 | bottom: -.25em;
91 | }
92 |
93 | img {
94 | border: 0;
95 | }
96 |
97 | svg:not(:root) {
98 | overflow: hidden;
99 | }
100 |
101 | hr {
102 | -moz-box-sizing: content-box;
103 | box-sizing: content-box;
104 | height: 0;
105 | }
106 |
107 | pre {
108 | overflow: auto;
109 | }
110 |
111 | code,
112 | kbd,
113 | pre,
114 | samp {
115 | font-family: monospace,monospace;
116 | font-size: 1em;
117 | }
118 |
119 | button,
120 | input,
121 | optgroup,
122 | select,
123 | textarea {
124 | color: inherit;
125 | font: inherit;
126 | margin: 0;
127 | }
128 |
129 | button {
130 | overflow: visible;
131 | }
132 |
133 | button,
134 | select {
135 | text-transform: none;
136 | }
137 |
138 | button,
139 | html input[type=button],
140 | input[type=reset],
141 | input[type=submit] {
142 | -webkit-appearance: button;
143 | cursor: pointer;
144 | }
145 |
146 | button[disabled],
147 | html input[disabled] {
148 | cursor: default;
149 | }
150 |
151 | button::-moz-focus-inner,
152 | input::-moz-focus-inner {
153 | border: 0;
154 | padding: 0;
155 | }
156 |
157 | input {
158 | line-height: normal;
159 | }
160 |
161 | input[type=checkbox],
162 | input[type=radio] {
163 | -moz-box-sizing: border-box;
164 | box-sizing: border-box;
165 | padding: 0;
166 | }
167 |
168 | input[type=number]::-webkit-inner-spin-button,
169 | input[type=number]::-webkit-outer-spin-button {
170 | height: auto;
171 | }
172 |
173 | input[type=search] {
174 | -webkit-appearance: textfield;
175 | -moz-box-sizing: content-box;
176 | box-sizing: content-box;
177 | }
178 |
179 | input[type=search]::-webkit-search-cancel-button,
180 | input[type=search]::-webkit-search-decoration {
181 | -webkit-appearance: none;
182 | }
183 |
184 | legend {
185 | border: 0;
186 | padding: 0;
187 | }
188 |
189 | textarea {
190 | overflow: auto;
191 | }
192 |
193 | optgroup {
194 | font-weight: 700;
195 | }
196 |
197 | table {
198 | border-collapse: collapse;
199 | border-spacing: 0;
200 | }
201 |
202 | td,
203 | th {
204 | padding: 0;
205 | }
206 |
207 |
208 |
209 | html {
210 | background: inherit;
211 | color: inherit;
212 | }
213 |
214 | a {
215 | color: #069;
216 | text-decoration: none;
217 | }
218 |
219 | a:active,
220 | a:focus,
221 | a:hover {
222 | color: #069;
223 | text-decoration: underline;
224 | }
225 |
226 | blockquote,
227 | dd,
228 | dl,
229 | figure,
230 | h1,
231 | h2,
232 | h3,
233 | h4,
234 | h5,
235 | h6,
236 | p,
237 | pre {
238 | margin: 0;
239 | }
240 |
241 | button {
242 | background: 0 0;
243 | border: 0;
244 | padding: 0;
245 | }
246 |
247 | button:focus {
248 | outline: dotted 1px;
249 | outline: -webkit-focus-ring-color auto 5px;
250 | }
251 |
252 | fieldset {
253 | border: 0;
254 | margin: 0;
255 | padding: 0;
256 | }
257 |
258 | iframe {
259 | border: 0;
260 | }
261 |
262 | ol,
263 | ul {
264 | list-style: none;
265 | margin: 0;
266 | padding: 0;
267 | }
268 |
269 | [tabindex="-1"]:focus {
270 | outline: 0!important;
271 | }
272 |
--------------------------------------------------------------------------------
/examples/css/player.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Player
3 | */
4 |
5 | :root {
6 | --player-height: 760px;
7 | --player-width: 760px;
8 | --radius: 180px;
9 | --diameter: calc(var(--radius) * 2);
10 | --margin: calc((var(--player-width) - var(--diameter)) / 2);
11 | }
12 |
13 | .Player {
14 | height: var(--player-height);
15 | width: var(--player-width);
16 | position: relative;
17 | margin: 0 auto;
18 | z-index: 1;
19 | }
20 |
21 | @media (--narrow) {
22 | .Player {
23 | height: calc(var(--player-height) * 0.6);
24 | width: calc(var(--player-width) * 0.6);
25 | transform: scale(0.6) translate(-40%, -40%);
26 | }
27 | }
28 |
29 | @media (--mobile) {
30 | .Player {
31 | height: calc(var(--player-height) * 0.4);
32 | width: calc(var(--player-width) * 0.4);
33 | transform: scale(0.4) translate(-80%, -80%);
34 | }
35 | }
36 |
37 | .Player-inner {
38 | border-radius: 50%;
39 | box-shadow: inset 1px 1px 8px #aaa;
40 | height: var(--diameter);
41 | position: relative;
42 | width: var(--diameter);
43 | top: var(--margin);
44 | left: var(--margin);
45 | }
46 |
47 | .Player-canvas {
48 | left: 0;
49 | position: absolute;
50 | top: 0;
51 | z-index: 0;
52 | }
53 |
54 | .Player-control {
55 | background: #fff;
56 | border-radius: 50%;
57 | height: calc(100% - 10px);
58 | left: 5px;
59 | position: absolute;
60 | top: 5px;
61 | width: calc(100% - 10px);
62 | z-index: 1;
63 | }
64 |
65 | .Player-play {
66 | width: 0;
67 | height: 0;
68 | border-top: 60px solid transparent;
69 | border-bottom: 60px solid transparent;
70 | border-left: 80px solid var(--color-main);
71 | position: absolute;
72 | left: 50%;
73 | top: 50%;
74 | transform: translate(-40%, -50%);
75 | }
76 |
77 | .Player-pause {
78 | border-left: 36px solid var(--color-main);
79 | border-right: 36px solid var(--color-main);
80 | display: none;
81 | height: 100px;
82 | left: 50%;
83 | position: absolute;
84 | top: 50%;
85 | transform: translate(-50%, -50%);
86 | width: 90px;
87 | }
88 |
89 | .Player.is-playing .Player-play {
90 | display: none;
91 | }
92 |
93 | .Player.is-playing .Player-pause {
94 | display: block;
95 | }
96 |
97 | .Player-mask {
98 | height: 100%;
99 | position: absolute;
100 | width: 50%;
101 | overflow: hidden;
102 | }
103 |
104 | .Player-maskA {
105 | left: 50%;
106 | }
107 |
108 | .Player-half {
109 | background-color: var(--color-dark);
110 | height: 100%;
111 | position: absolute;
112 | width: 100%;
113 | }
114 |
115 | .Player-halfA {
116 | border-radius: 100% / 50%;
117 | border-top-right-radius: 0;
118 | border-bottom-right-radius: 0;
119 | left: -100%;
120 | transform-origin: right center;
121 | transform: rotate(0deg);
122 | }
123 |
124 | .Player-halfB {
125 | border-radius: 100% / 50%;
126 | border-top-left-radius: 0;
127 | border-bottom-left-radius: 0;
128 | left: 100%;
129 | transform-origin: left center;
130 | transform: rotate(0deg);
131 | }
132 |
133 |
134 | /*
135 | * player top
136 | */
137 |
138 | .PlayerTop {
139 | background-color: #e7e9db;
140 | border-bottom: 1px solid white;
141 | display: flex;
142 | height: 60px;
143 | left: 0;
144 | position: fixed;
145 | top: 0;
146 | transform: translateY(-100%);
147 | transition: transform 0.3s ease-out;
148 | width: 100%;
149 | z-index: 2;
150 | }
151 |
152 | .PlayerTop.is-active {
153 | transform: translateY(0);
154 | }
155 |
156 | .PlayerTop-control {
157 | flex: none;
158 | height: 100%;
159 | position: relative;
160 | width: 80px;
161 | }
162 |
163 | .PlayerTop-canvas {
164 | width: 100%;
165 | height: 100%;
166 | }
167 |
168 | .PlayerTop-play {
169 | width: 0;
170 | height: 0;
171 | border-top: 20px solid transparent;
172 | border-bottom: 20px solid transparent;
173 | border-left: 30px solid var(--color-main);
174 | position: absolute;
175 | left: 50%;
176 | top: 50%;
177 | transform: translate(-40%, -50%);
178 | }
179 |
180 | .PlayerTop-pause {
181 | display: none;
182 | border-left: 12px solid var(--color-main);
183 | border-right: 12px solid var(--color-main);
184 | height: 36px;
185 | left: 50%;
186 | position: absolute;
187 | top: 50%;
188 | transform: translate(-50%, -50%);
189 | width: 30px;
190 | }
191 |
192 | .PlayerTop.is-playing .PlayerTop-play {
193 | display: none;
194 | }
195 |
196 | .PlayerTop.is-playing .PlayerTop-pause {
197 | display: block;
198 | }
199 |
--------------------------------------------------------------------------------
/examples/css/sections.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --color-main: #bbcccc;
3 | --color-dark: #aabbbb;
4 | }
5 |
6 | @custom-media --narrow (max-width: 760px);
7 | @custom-media --mobile (max-width: 400px);
8 |
9 | html {
10 | height: 100%;
11 | }
12 |
13 | body {
14 | background-color: #FFF;
15 | color: #000;
16 | font-family: Helvetica, sans-serif;
17 | font-size: 16px;
18 | height: 100%;
19 | letter-spacing: 0.05em;
20 | line-height: 1.5;
21 | margin: 0;
22 | width: 100%;
23 | }
24 |
25 | main {
26 | overflow-x: hidden;
27 | width: 100%;
28 | }
29 |
30 | h1,
31 | h2,
32 | h3 {
33 | color: #303030;
34 | font-weight: 400;
35 | margin: 0.5em 0;
36 | }
37 |
38 | h1 {
39 | font-size: 48px;
40 | line-height: 1;
41 | margin: 0.4em 0 0.2em;
42 | }
43 |
44 | h2 {
45 | font-size: 32px;
46 | }
47 |
48 | h3 {
49 |
50 | }
51 |
52 | header {
53 | text-align: center;
54 | width: 100%;
55 | margin: 40px 0 20px;
56 | }
57 |
58 | section {
59 |
60 | }
61 |
62 | pre {
63 | margin: 50px 0;
64 | width: 100%;
65 | }
66 |
67 | @media (--mobile) {
68 | pre {
69 | margin: 20px 0;
70 | }
71 | }
72 |
73 | code {
74 | border-radius: 8px;
75 | margin: 0 auto;
76 | max-width: 800px;
77 | min-width: 320px;
78 | width: 80%;
79 | }
80 |
81 | code ul {
82 | margin-left: 3.5em;
83 | }
84 |
85 | @media (--mobile) {
86 | code {
87 | font-size: 60%;
88 | }
89 | }
90 |
91 | nav {
92 | padding: 0 0 50px;
93 | text-align: center;
94 | }
95 |
96 | nav a {
97 | text-decoration: underline;
98 | font-size: 32px;
99 | }
100 |
101 | * {
102 | box-sizing: border-box;
103 | }
104 |
105 | .dg select {
106 | color: black;
107 | }
108 |
109 | header nav {
110 | padding: 5px;
111 | position: absolute;
112 | right: 0;
113 | top: 0;
114 | }
115 |
116 | header nav ul {
117 | display: flex;
118 | }
119 |
120 | header nav a {
121 | font-size: 20px;
122 | margin: 0 10px;
123 | }
124 |
125 | @media (--mobile) {
126 | header nav {
127 | width: 100%;
128 | }
129 |
130 | header nav ul {
131 | justify-content: center;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/examples/distortion.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | sono - examples - distortion
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
25 |
26 |
27 | import sono from 'sono';
28 | import 'sono/effects';
29 |
30 | const sound = sono.create({
31 | url: 'hit.mp3',
32 | loop: true,
33 | effects: [sono.distortion({
34 | level: 0.5,
35 | dry: 1,
36 | wet: 1
37 | })]
38 | });
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
170 |
171 |
172 |
--------------------------------------------------------------------------------
/examples/echo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | sono - examples - echo
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
25 |
26 |
27 | import sono from 'sono';
28 | import 'sono/effects';
29 |
30 | const sound = sono.create({
31 | url: 'hit.mp3',
32 | loop: true,
33 | effects: [sono.echo({
34 | delay: 0.4,
35 | feedback: 0.6,
36 | dry: 1,
37 | wet: 1
38 | })]
39 | });
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
191 |
192 |
193 |
--------------------------------------------------------------------------------
/examples/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/favicon.ico
--------------------------------------------------------------------------------
/examples/img/.gitinclude:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/img/.gitinclude
--------------------------------------------------------------------------------
/examples/img/back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/img/back.png
--------------------------------------------------------------------------------
/examples/img/bottom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/img/bottom.png
--------------------------------------------------------------------------------
/examples/img/front.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/img/front.png
--------------------------------------------------------------------------------
/examples/img/left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/img/left.png
--------------------------------------------------------------------------------
/examples/img/right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/img/right.png
--------------------------------------------------------------------------------
/examples/img/top.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/img/top.png
--------------------------------------------------------------------------------
/examples/js/base-url.js:
--------------------------------------------------------------------------------
1 | window.isLocalHost = /^(?:https?:\/\/)?(?:localhost|192\.168)/.test(window.location.href);
2 | window.baseURL = window.isLocalHost ? '/examples/audio/' : 'https://ianmcgregor.co/prototypes/audio/';
3 |
--------------------------------------------------------------------------------
/examples/js/disable.js:
--------------------------------------------------------------------------------
1 | if (window.location.search.slice(1) === 'nowebaudio') {
2 | window.AudioContext = window.webkitAudioContext = undefined;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/js/recorder.js:
--------------------------------------------------------------------------------
1 | /* eslint no-var: 0 */
2 | /* eslint strict: 0 */
3 |
4 | (function() {
5 |
6 | var sono = window.sono;
7 | var ui = window.ui;
8 |
9 | sono.log();
10 |
11 | var player,
12 | sound,
13 | recorder,
14 | analyser,
15 | canvas = document.querySelector('[data-waveform]'),
16 | context = canvas.getContext('2d');
17 |
18 | function onConnect(stream) {
19 | sound = sono.create(stream);
20 | recorder = sono.utils.recorder(false);
21 | analyser = sound.effects.add(sono.analyser({fftSize: 1024}));
22 | analyser.maxDecibels = -60;
23 | recorder.start(sound);
24 | update();
25 | }
26 |
27 | var mic = sono.utils.microphone(onConnect);
28 |
29 | if (!mic.isSupported) {
30 | document.querySelector('[data-warning]')
31 | .classList.add('is-visible');
32 | }
33 |
34 | function toggle() {
35 | if (recorder && recorder.isRecording) {
36 | var recording = recorder.stop();
37 | console.log(recording);
38 | createPlayer(recording);
39 | mic.disconnect();
40 | } else {
41 | if (mic.stream) {
42 | // recorder.start(sound);
43 | } else {
44 | mic.connect();
45 | }
46 | if (player) {
47 | player.destroy();
48 | player.el.classList.remove('is-active');
49 | }
50 | }
51 | }
52 |
53 | var control = ui.createToggle({
54 | el: document.querySelector('[data-mic-toggle]'),
55 | name: 'Record',
56 | value: false
57 | }, function() {
58 | toggle();
59 | });
60 |
61 | function createPlayer(buffer) {
62 | console.log('createPlayer');
63 | player = ui.createPlayer({
64 | el: document.querySelector('[data-player-top]'),
65 | sound: sono.create(buffer)
66 | .play()
67 | });
68 | player.el.classList.add('is-active');
69 | }
70 |
71 | function update() {
72 | window.requestAnimationFrame(update);
73 |
74 | if (player) {
75 | player();
76 | }
77 |
78 | control.setLabel(recorder.getDuration()
79 | .toFixed(1));
80 |
81 | var width = canvas.width,
82 | height = canvas.height,
83 | frequencyBinCount = analyser.frequencyBinCount,
84 | barWidth = Math.max(1, Math.round(width / frequencyBinCount)),
85 | magnitude,
86 | percent,
87 | hue;
88 |
89 | context.fillStyle = '#ffffff';
90 | context.fillRect(0, 0, width, height);
91 |
92 | var waveData = analyser.getFrequencies();
93 | var freqData = analyser.getWaveform();
94 |
95 | for (var i = 0; i < frequencyBinCount; i++) {
96 | magnitude = freqData[i];
97 | percent = magnitude / 256;
98 | hue = i / frequencyBinCount * 360;
99 | context.fillStyle = 'hsl(' + hue + ', 100%, 30%)';
100 | context.fillRect(barWidth * i, height, barWidth, 0 - height * percent);
101 |
102 | magnitude = waveData[i];
103 | percent = magnitude / 512;
104 | hue = i / frequencyBinCount * 360;
105 | context.fillStyle = 'hsl(' + hue + ', 100%, 50%)';
106 | context.fillRect(barWidth * i, height - height * percent - 1, 2, 2);
107 | }
108 | }
109 |
110 | }());
111 |
--------------------------------------------------------------------------------
/examples/multi-play.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | sono - examples - multiPlay
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
23 |
24 |
25 |
26 |
27 |
28 | sono.create({
29 | id: 'sound',
30 | url: 'hit.ogg',
31 | multiPlay: true
32 | });
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/examples/oscillator.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | sono - examples - oscillator
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | const sineWave = sono.create('sine');
28 | sineWave.frequency = 100;
29 | sineWave.volume = 0.1;
30 | sineWave.play();
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
141 |
142 |
143 |
144 |
145 |
146 |
--------------------------------------------------------------------------------
/examples/pixi.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | sono - examples - PixiJS / Multi play
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
Arrow keys to move around, space to shoot.
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | import sono from 'sono';
31 | import {loaders} from 'pixi.js';
32 |
33 | const ext = sono.canPlay.ogg ? 'ogg' : 'mp3';
34 |
35 | if (sono.hasWebAudio) {
36 | const {Resource} = loaders;
37 | Resource.setExtensionLoadType(ext, Resource.LOAD_TYPE.XHR);
38 | Resource.setExtensionXhrType(ext, Resource.XHR_RESPONSE_TYPE.BUFFER);
39 | }
40 |
41 | const sounds = [{
42 | name: 'music',
43 | url: `audio/space-shooter.${ext}`,
44 | loop: true,
45 | volume: 0.8
46 | }, {
47 | name: 'shoot',
48 | url: `audio/shoot3.${ext}`,
49 | volume: 0.4
50 | }, {
51 | name: 'explode',
52 | url: `audio/explode2.${ext}`,
53 | volume: 0.9
54 | }];
55 |
56 | const loader = new loaders.Loader();
57 |
58 | loader.add(sounds);
59 |
60 | loader.onComplete.once(() => {
61 | sounds.forEach(sound => {
62 | const src = loader.resources[sound.name].data;
63 | const config = Object.assign({}, sound, {src});
64 | sono.create(config);
65 | });
66 |
67 | sono.get('shoot').effects.add(reverb();
68 | sono.play('music');
69 | });
70 |
71 | loader.load();
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/examples/recorder.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | sono - examples - recorder
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 | Microphone access is not supported in this browser
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
37 |
38 |
39 | var passThrough = true,
40 | sound,
41 | recorder;
42 | var onConnect = function(stream) {
43 | // the sound coming through the micophone
44 | sound = sono.create(stream);
45 | // start recorder
46 | recorder = sono.utils.recorder(passThrough);
47 | recorder.start(sound);
48 |
49 | setTimeout(function() {
50 | // recorder returns recorded sound when stopped
51 | var recording = recorder.stop();
52 | // create a new sound object from the recording
53 | sono.create(recording).play();
54 | }, 2000);
55 | };
56 | // propmt user to connect their mic
57 | sono.utils.microphone(onConnect);
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/examples/three.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | sono - examples - 3d
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
Use the arrow keys to move around.
25 |
26 |
27 |
28 |
29 |
30 |
31 | import sono from 'sono';
32 | import 'sono/effects';
33 |
34 | const sound = sono.create({
35 | url: 'pulsar.mp3',
36 | loop: true,
37 | effects: [
38 | sono.panner({
39 | maxDistance: 1000
40 | })
41 | ]
42 | });
43 | const mesh = new THREE.Mesh(geometry, material);
44 | // set source position to the position vector of sound source
45 | sound.effects[0].setPosition(mesh.position);
46 |
47 | function update() {
48 | window.requestAnimationFrame(update);
49 | // set listener position to the position vector of the camera
50 | sono.panner.setListenerPosition(camera.position);
51 | // set listener orientation to the forward vector of the camera
52 | const forward = new THREE.Vector3(0, 0, -1);
53 | forward.applyQuaternion(camera.quaternion);
54 | sono.panner.setListenerOrientation(forward.normalize());
55 | }
56 | update();
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/examples/video/.gitinclude:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/video/.gitinclude
--------------------------------------------------------------------------------
/examples/volume.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | sono - examples - volume
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
122 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/examples/wet_dry.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | sono - examples - wet / dry
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
136 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 |
3 | const files = [];
4 |
5 | if (process.env.WA === 'no') {
6 | files.push('test/kill-wa.js');
7 | }
8 |
9 | if (process.env.TRAVIS) {
10 | files.push('test/is-travis.js');
11 | }
12 |
13 | const configuration = {
14 |
15 | // How long to wait for a message from a browser before disconnecting
16 | browserNoActivityTimeout: 30000,
17 |
18 | // base path, that will be used to resolve files and exclude
19 | basePath: '',
20 |
21 | client: {
22 | mocha: {
23 | timeout: 20000
24 | }
25 | },
26 |
27 | plugins: [
28 | 'karma-mocha',
29 | 'karma-chai',
30 | 'karma-chrome-launcher',
31 | 'karma-firefox-launcher',
32 | 'karma-safari-launcher'
33 | ],
34 |
35 | // frameworks to use
36 | frameworks: ['mocha', 'chai'],
37 |
38 | // list of files / patterns to load in the browser
39 | files: files.concat([
40 | {pattern: 'test/audio/*.{ogg,mp3}', watched: false, included: false, served: true, nocache: false},
41 | 'test/helper.js',
42 | 'dist/sono.js',
43 | 'test/**/*.spec.js'
44 | ]),
45 |
46 | // list of files to exclude
47 | exclude: [
48 | // 'test/playback.spec.js'
49 | ],
50 |
51 | // test results reporter to use
52 | // possible values: 'dots', 'progress'
53 | reporters: ['progress'],
54 |
55 | // web server port
56 | port: 9876,
57 |
58 | // enable / disable colors in the output (reporters and logs)
59 | colors: true,
60 |
61 | // level of logging
62 | // possible values: config.LOG_DISABLE || config.LOG_ERROR ||
63 | // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
64 | logLevel: config.LOG_WARN,
65 |
66 | // enable / disable watching file and executing tests whenever any file changes
67 | autoWatch: true,
68 |
69 | // Start these browsers, currently available:
70 | browsers: [
71 | 'Chrome',
72 | 'Firefox',
73 | 'Safari'
74 | ],
75 |
76 | // For Travis
77 | customLaunchers: {
78 | Chrome_travis_ci: {
79 | base: 'Chrome',
80 | flags: ['--no-sandbox']
81 | }
82 | },
83 |
84 | // If browser does not capture in given timeout [ms], kill it
85 | captureTimeout: 60000,
86 |
87 | // Continuous Integration mode
88 | // if true, it capture browsers, run tests and exit
89 | singleRun: false
90 | };
91 |
92 | if (process.env.TRAVIS) {
93 | configuration.browsers = ['Chrome_travis_ci'];
94 | }
95 |
96 | config.set(configuration);
97 | };
98 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sono",
3 | "version": "2.1.6",
4 | "description": "A simple yet powerful JavaScript library for working with Web Audio",
5 | "keywords": [
6 | "WebAudio",
7 | "Web Audio",
8 | "WebAudioAPI",
9 | "Web Audio API",
10 | "audio",
11 | "sound"
12 | ],
13 | "main": "core/sono.js",
14 | "scripts": {
15 | "prepublish": "npm run lib",
16 | "test": "eslint 'src/**/*.js' && karma start --single-run --browsers Chrome && WA=no karma start --single-run --browsers Chrome",
17 | "build": "NODE_ENV=production rollup -c && rollup -c && npm run lib",
18 | "start": "rollup -c -w",
19 | "start:test": "rollup -c -w | karma start",
20 | "lint": "eslint 'src/**/*.js'; exit 0",
21 | "examples": "browser-sync start -s --files 'index.html, examples/**/*.html, examples/**/*.js, examples/**/*.css' --no-notify",
22 | "examples:js": "babel examples/js/src --out-dir examples/js/",
23 | "examples:css": "postcss examples/css/index.css --use postcss-import postcss-custom-media postcss-custom-properties postcss-calc autoprefixer -o examples/css/styles.css",
24 | "examples:watch:css": "onchange 'examples/**/*.css' -e 'examples/css/styles.css' -- npm run examples:css",
25 | "examples:watch:js": "onchange 'examples/js/src/*.js' -- npm run examples:js",
26 | "build:examples": "npm run examples:js && npm run examples:css",
27 | "lib": "rimraf core effects utils && babel src --out-dir ./"
28 | },
29 | "repository": {
30 | "type": "git",
31 | "url": "https://github.com/Stinkstudios/sono"
32 | },
33 | "author": "ianmcgregor",
34 | "license": "MIT",
35 | "readmeFilename": "README.md",
36 | "dependencies": {
37 | "core-js": "^2.4.1",
38 | "events": "^1.1.1"
39 | },
40 | "devDependencies": {
41 | "autoprefixer": "^6.7.7",
42 | "babel-cli": "^6.24.1",
43 | "babel-core": "^6.24.1",
44 | "babel-eslint": "^7.2.3",
45 | "babel-plugin-external-helpers": "^6.22.0",
46 | "babel-plugin-transform-runtime": "^6.23.0",
47 | "babel-preset-es2015": "^6.24.1",
48 | "browser-sync": "^2.18.8",
49 | "chai": "^3.5.0",
50 | "eslint": "^4.18.2",
51 | "karma": "^1.6.0",
52 | "karma-chai": "^0.1.0",
53 | "karma-chrome-launcher": "^2.0.0",
54 | "karma-firefox-launcher": "^1.0.1",
55 | "karma-mocha": "^1.3.0",
56 | "karma-safari-launcher": "^1.0.0",
57 | "mocha": "^3.3.0",
58 | "postcss-calc": "^5.3.1",
59 | "postcss-cli": "^3.2.0",
60 | "postcss-custom-media": "^5.0.1",
61 | "postcss-custom-properties": "^5.0.2",
62 | "postcss-import": "^9.1.0",
63 | "rimraf": "^2.6.1",
64 | "rollup": "^0.41.6",
65 | "rollup-plugin-babel": "^2.7.1",
66 | "rollup-plugin-commonjs": "^8.0.2",
67 | "rollup-plugin-node-resolve": "^3.0.0",
68 | "rollup-plugin-strip": "^1.1.1",
69 | "rollup-plugin-uglify": "^1.0.2",
70 | "rollup-watch": "^3.2.2"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import commonjs from 'rollup-plugin-commonjs';
3 | import nodeResolve from 'rollup-plugin-node-resolve';
4 | import strip from 'rollup-plugin-strip';
5 | import uglify from 'rollup-plugin-uglify';
6 |
7 | const prod = process.env.NODE_ENV === 'production';
8 |
9 | export default {
10 | entry: './src/index.js',
11 | format: 'umd',
12 | moduleName: 'sono',
13 | dest: (prod ? 'dist/sono.min.js' : 'dist/sono.js'),
14 | sourceMap: !prod,
15 | plugins: [
16 | nodeResolve({
17 | jsnext: true,
18 | main: true,
19 | preferBuiltins: false
20 | }),
21 | commonjs({
22 | include: [
23 | 'node_modules/core-js/**',
24 | 'node_modules/events/**'
25 | ]
26 | }),
27 | babel({
28 | babelrc: false,
29 | exclude: 'node_modules/**',
30 | presets: [
31 | ['es2015', {loose: true, modules: false}]
32 | ],
33 | plugins: [
34 | 'external-helpers'
35 | ]
36 | }),
37 | (prod && strip({sourceMap: false})),
38 | (prod && uglify())
39 | ]
40 | };
41 |
--------------------------------------------------------------------------------
/src/core/context.js:
--------------------------------------------------------------------------------
1 | import dummy from './utils/dummy';
2 | import FakeContext from './utils/fake-context';
3 | import iOS from './utils/iOS';
4 |
5 | const desiredSampleRate = 44100;
6 |
7 | const Ctx = window.AudioContext || window.webkitAudioContext || FakeContext;
8 |
9 | let context = new Ctx();
10 |
11 | if (!context) {
12 | context = new FakeContext();
13 | }
14 |
15 | // Check if hack is necessary. Only occurs in iOS6+ devices
16 | // and only when you first boot the iPhone, or play a audio/video
17 | // with a different sample rate
18 | // https://github.com/Jam3/ios-safe-audio-context/blob/master/index.js
19 | if (iOS && context.sampleRate !== desiredSampleRate) {
20 | dummy(context);
21 | context.close(); // dispose old context
22 | context = new Ctx();
23 | }
24 |
25 | // Handles bug in Safari 9 OSX where AudioContext instance starts in 'suspended' state
26 | if (context.state === 'suspended' && typeof context.resume === 'function') {
27 | window.setTimeout(() => context.resume(), 1000);
28 | }
29 |
30 | export default context;
31 |
--------------------------------------------------------------------------------
/src/core/effects.js:
--------------------------------------------------------------------------------
1 | export default class Effects {
2 | constructor(context) {
3 | this.context = context;
4 | this._destination = null;
5 | this._source = null;
6 |
7 | this._nodes = [];
8 | this._nodes.has = node => this.has(node);
9 | this._nodes.add = node => this.add(node);
10 | this._nodes.remove = node => this.remove(node);
11 | this._nodes.toggle = (node, force) => this.toggle(node, force);
12 | this._nodes.removeAll = () => this.removeAll();
13 |
14 | Object.keys(Effects.prototype).forEach(key => {
15 | if (!this._nodes.hasOwnProperty(key) && typeof Effects.prototype[key] === 'function') {
16 | this._nodes[key] = this[key].bind(this);
17 | }
18 | });
19 | }
20 |
21 | setSource(node) {
22 | this._source = node;
23 | this._updateConnections();
24 | return node;
25 | }
26 |
27 | setDestination(node) {
28 | this._connectToDestination(node);
29 | return node;
30 | }
31 |
32 | has(node) {
33 | if (!node) {
34 | return false;
35 | }
36 | return this._nodes.indexOf(node) > -1;
37 | }
38 |
39 | add(node) {
40 | if (!node) {
41 | return null;
42 | }
43 | if (this.has(node)) {
44 | return node;
45 | }
46 | if (Array.isArray(node)) {
47 | let n;
48 | for (let i = 0; i < node.length; i++) {
49 | n = this.add(node[i]);
50 | }
51 | return n;
52 | }
53 | this._nodes.push(node);
54 | this._updateConnections();
55 | return node;
56 | }
57 |
58 | remove(node) {
59 | if (!node) {
60 | return null;
61 | }
62 | if (!this.has(node)) {
63 | return node;
64 | }
65 | const l = this._nodes.length;
66 | for (let i = 0; i < l; i++) {
67 | if (node === this._nodes[i]) {
68 | this._nodes.splice(i, 1);
69 | break;
70 | }
71 | }
72 | node.disconnect();
73 | this._updateConnections();
74 | return node;
75 | }
76 |
77 | toggle(node, force) {
78 | force = !!force;
79 | const hasNode = this.has(node);
80 | if (arguments.length > 1 && hasNode === force) {
81 | return this;
82 | }
83 | if (hasNode) {
84 | this.remove(node);
85 | } else {
86 | this.add(node);
87 | }
88 | return this;
89 | }
90 |
91 | removeAll() {
92 | while (this._nodes.length) {
93 | const node = this._nodes.pop();
94 | node.disconnect();
95 | }
96 | this._updateConnections();
97 | return this;
98 | }
99 |
100 | destroy() {
101 | this.removeAll();
102 | this.context = null;
103 | this._destination = null;
104 | if (this._source) {
105 | this._source.disconnect();
106 | }
107 | this._source = null;
108 | }
109 |
110 | _connect(a, b) {
111 | a.disconnect();
112 | // console.log('> connect output', (a.name || a.constructor.name), 'to input', (b.name || b.constructor.name));
113 | a.connect(b._in || b);
114 | }
115 |
116 | _connectToDestination(node) {
117 | const lastNode = this._nodes[this._nodes.length - 1] || this._source;
118 |
119 | if (lastNode) {
120 | this._connect(lastNode, node);
121 | }
122 |
123 | this._destination = node;
124 | }
125 |
126 | _updateConnections() {
127 | if (!this._source) {
128 | return;
129 | }
130 |
131 | // console.log('updateConnections');
132 |
133 | let node,
134 | prev;
135 |
136 | for (let i = 0; i < this._nodes.length; i++) {
137 | node = this._nodes[i];
138 | prev = i === 0 ? this._source : this._nodes[i - 1];
139 | this._connect(prev, node);
140 | }
141 |
142 | if (this._destination) {
143 | this._connectToDestination(this._destination);
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/core/group.js:
--------------------------------------------------------------------------------
1 | import Effects from './effects';
2 |
3 | export default function Group(context, destination) {
4 | const sounds = [];
5 | const effects = new Effects(context);
6 | const gain = context.createGain();
7 | let preMuteVolume = 1;
8 | let group = null;
9 |
10 | if (context) {
11 | effects.setSource(gain);
12 | effects.setDestination(destination || context.destination);
13 | }
14 |
15 | /*
16 | * Add / remove
17 | */
18 |
19 | function find(soundOrId, callback) {
20 | let found;
21 |
22 | if (!soundOrId && soundOrId !== 0) {
23 | return found;
24 | }
25 |
26 | sounds.some(function(sound) {
27 | if (sound === soundOrId || sound.id === soundOrId) {
28 | found = sound;
29 | return true;
30 | }
31 | return false;
32 | });
33 |
34 | if (found && callback) {
35 | return callback(found);
36 | }
37 |
38 | return found;
39 | }
40 |
41 | function remove(soundOrId) {
42 | find(soundOrId, (sound) => sounds.splice(sounds.indexOf(sound), 1));
43 | return group;
44 | }
45 |
46 | function add(sound) {
47 | sound.gain.disconnect();
48 | sound.gain.connect(gain);
49 |
50 | sounds.push(sound);
51 |
52 | sound.once('destroy', remove);
53 |
54 | return group;
55 | }
56 |
57 | /*
58 | * Controls
59 | */
60 |
61 | function play(delay, offset) {
62 | sounds.forEach((sound) => sound.play(delay, offset));
63 | return group;
64 | }
65 |
66 | function pause() {
67 | sounds.forEach((sound) => {
68 | if (sound.playing) {
69 | sound.pause();
70 | }
71 | });
72 | return group;
73 | }
74 |
75 | function resume() {
76 | sounds.forEach((sound) => {
77 | if (sound.paused) {
78 | sound.play();
79 | }
80 | });
81 | return group;
82 | }
83 |
84 | function stop() {
85 | sounds.forEach((sound) => sound.stop());
86 | return group;
87 | }
88 |
89 | function seek(percent) {
90 | sounds.forEach((sound) => sound.seek(percent));
91 | return group;
92 | }
93 |
94 | function mute() {
95 | preMuteVolume = group.volume;
96 | group.volume = 0;
97 | return group;
98 | }
99 |
100 | function unMute() {
101 | group.volume = preMuteVolume || 1;
102 | return group;
103 | }
104 |
105 | function setVolume(value) {
106 | group.volume = value;
107 | return group;
108 | }
109 |
110 | function fade(volume, duration) {
111 | if (context) {
112 | const param = gain.gain;
113 | const time = context.currentTime;
114 |
115 | param.cancelScheduledValues(time);
116 | param.setValueAtTime(param.value, time);
117 | // param.setValueAtTime(volume, time + duration);
118 | param.linearRampToValueAtTime(volume, time + duration);
119 | // param.setTargetAtTime(volume, time, duration);
120 | // param.exponentialRampToValueAtTime(Math.max(volume, 0.0001), time + duration);
121 | } else {
122 | sounds.forEach((sound) => sound.fade(volume, duration));
123 | }
124 |
125 | return group;
126 | }
127 |
128 | /*
129 | * Load
130 | */
131 |
132 | function load() {
133 | sounds.forEach((sound) => sound.load());
134 | }
135 |
136 | /*
137 | * Unload
138 | */
139 |
140 | function unload() {
141 | sounds.forEach((sound) => sound.unload());
142 | }
143 |
144 | /*
145 | * Destroy
146 | */
147 |
148 | function destroy() {
149 | while (sounds.length) {
150 | sounds.pop()
151 | .destroy();
152 | }
153 | }
154 |
155 | /*
156 | * Api
157 | */
158 |
159 | group = {
160 | add,
161 | find,
162 | remove,
163 | play,
164 | pause,
165 | resume,
166 | stop,
167 | seek,
168 | setVolume,
169 | mute,
170 | unMute,
171 | fade,
172 | load,
173 | unload,
174 | destroy,
175 | gain,
176 | get effects() {
177 | return effects._nodes;
178 | },
179 | set effects(value) {
180 | effects.removeAll().add(value);
181 | },
182 | get fx() {
183 | return this.effects;
184 | },
185 | set fx(value) {
186 | this.effects = value;
187 | },
188 | get sounds() {
189 | return sounds;
190 | },
191 | get volume() {
192 | return gain.gain.value;
193 | },
194 | set volume(value) {
195 | if (isNaN(value)) {
196 | return;
197 | }
198 |
199 | value = Math.min(Math.max(value, 0), 1);
200 |
201 | if (context) {
202 | gain.gain.cancelScheduledValues(context.currentTime);
203 | gain.gain.value = value;
204 | gain.gain.setValueAtTime(value, context.currentTime);
205 | } else {
206 | gain.gain.value = value;
207 | }
208 | sounds.forEach((sound) => {
209 | if (!sound.context) {
210 | sound.groupVolume = value;
211 | }
212 | });
213 | }
214 | };
215 |
216 | return group;
217 | }
218 |
219 | Group.Effects = Effects;
220 |
--------------------------------------------------------------------------------
/src/core/source/buffer-source.js:
--------------------------------------------------------------------------------
1 | export default function BufferSource(buffer, context, endedCallback) {
2 | const api = {};
3 | let ended = false;
4 | let loop = false;
5 | let paused = false;
6 | let cuedAt = 0;
7 | let playbackRate = 1;
8 | let playing = false;
9 | let sourceNode = null;
10 | let startedAt = 0;
11 |
12 | function createSourceNode() {
13 | if (!sourceNode && context) {
14 | sourceNode = context.createBufferSource();
15 | sourceNode.buffer = buffer;
16 | }
17 | return sourceNode;
18 | }
19 |
20 | /*
21 | * Controls
22 | */
23 |
24 | function stop() {
25 | if (sourceNode) {
26 | sourceNode.onended = null;
27 | try {
28 | sourceNode.disconnect();
29 | sourceNode.stop(0);
30 | } catch (e) {}
31 | sourceNode = null;
32 | }
33 |
34 | paused = false;
35 | cuedAt = 0;
36 | playing = false;
37 | startedAt = 0;
38 | }
39 |
40 | function pause() {
41 | const elapsed = context.currentTime - startedAt;
42 | stop();
43 | cuedAt = elapsed;
44 | playing = false;
45 | paused = true;
46 | }
47 |
48 | function endedHandler() {
49 | stop();
50 | ended = true;
51 | if (typeof endedCallback === 'function') {
52 | endedCallback(api);
53 | }
54 | }
55 |
56 | function play(delay = 0, offset = 0) {
57 | if (playing) {
58 | return;
59 | }
60 |
61 | delay = delay ? context.currentTime + delay : 0;
62 |
63 | if (offset) {
64 | cuedAt = 0;
65 | }
66 |
67 | if (cuedAt) {
68 | offset = cuedAt;
69 | }
70 |
71 | while (offset > api.duration) {
72 | offset = offset % api.duration;
73 | }
74 |
75 | createSourceNode();
76 | sourceNode.onended = endedHandler;
77 | sourceNode.start(delay, offset);
78 |
79 | sourceNode.loop = loop;
80 | sourceNode.playbackRate.value = playbackRate;
81 |
82 | startedAt = context.currentTime - offset;
83 | ended = false;
84 | paused = false;
85 | cuedAt = 0;
86 | playing = true;
87 | }
88 |
89 |
90 | /*
91 | * Destroy
92 | */
93 |
94 | function destroy() {
95 | stop();
96 | buffer = null;
97 | context = null;
98 | endedCallback = null;
99 | sourceNode = null;
100 | }
101 |
102 | /*
103 | * Getters & Setters
104 | */
105 |
106 | Object.defineProperties(api, {
107 | play: {
108 | value: play
109 | },
110 | pause: {
111 | value: pause
112 | },
113 | stop: {
114 | value: stop
115 | },
116 | destroy: {
117 | value: destroy
118 | },
119 | currentTime: {
120 | get: function() {
121 | if (cuedAt) {
122 | return cuedAt;
123 | }
124 | if (startedAt) {
125 | let time = context.currentTime - startedAt;
126 | while (time > api.duration) {
127 | time = time % api.duration;
128 | }
129 | return time;
130 | }
131 | return 0;
132 | },
133 | set: function(value) {
134 | cuedAt = value;
135 | }
136 | },
137 | duration: {
138 | get: function() {
139 | return buffer ? buffer.duration : 0;
140 | }
141 | },
142 | ended: {
143 | get: function() {
144 | return ended;
145 | }
146 | },
147 | loop: {
148 | get: function() {
149 | return loop;
150 | },
151 | set: function(value) {
152 | loop = !!value;
153 | if (sourceNode) {
154 | sourceNode.loop = loop;
155 | }
156 | }
157 | },
158 | paused: {
159 | get: function() {
160 | return paused;
161 | }
162 | },
163 | playbackRate: {
164 | get: function() {
165 | return playbackRate;
166 | },
167 | set: function(value) {
168 | playbackRate = value;
169 | if (sourceNode) {
170 | sourceNode.playbackRate.value = playbackRate;
171 | }
172 | }
173 | },
174 | playing: {
175 | get: function() {
176 | return playing;
177 | }
178 | },
179 | progress: {
180 | get: function() {
181 | return api.duration ? api.currentTime / api.duration : 0;
182 | }
183 | },
184 | sourceNode: {
185 | get: function() {
186 | return createSourceNode();
187 | }
188 | }
189 | });
190 |
191 | return Object.freeze(api);
192 | }
193 |
--------------------------------------------------------------------------------
/src/core/source/microphone-source.js:
--------------------------------------------------------------------------------
1 | export default function MicrophoneSource(stream, context) {
2 | let ended = false,
3 | paused = false,
4 | cuedAt = 0,
5 | playing = false,
6 | sourceNode = null, // MicrophoneSourceNode
7 | startedAt = 0;
8 |
9 | function createSourceNode() {
10 | if (!sourceNode && context) {
11 | sourceNode = context.createMediaStreamSource(stream);
12 | // HACK: stops moz garbage collection killing the stream
13 | // see https://support.mozilla.org/en-US/questions/984179
14 | if (navigator.mozGetUserMedia) {
15 | window.mozHack = sourceNode;
16 | }
17 | }
18 | return sourceNode;
19 | }
20 |
21 | /*
22 | * Controls
23 | */
24 |
25 | function play(delay) {
26 | delay = delay ? context.currentTime + delay : 0;
27 |
28 | createSourceNode();
29 | sourceNode.start(delay);
30 |
31 | startedAt = context.currentTime - cuedAt;
32 | ended = false;
33 | playing = true;
34 | paused = false;
35 | cuedAt = 0;
36 | }
37 |
38 | function stop() {
39 | if (sourceNode) {
40 | try {
41 | sourceNode.stop(0);
42 | } catch (e) {}
43 | sourceNode = null;
44 | }
45 | ended = true;
46 | paused = false;
47 | cuedAt = 0;
48 | playing = false;
49 | startedAt = 0;
50 | }
51 |
52 | function pause() {
53 | const elapsed = context.currentTime - startedAt;
54 | stop();
55 | cuedAt = elapsed;
56 | playing = false;
57 | paused = true;
58 | }
59 |
60 | /*
61 | * Destroy
62 | */
63 |
64 | function destroy() {
65 | stop();
66 | context = null;
67 | sourceNode = null;
68 | stream = null;
69 | window.mozHack = null;
70 | }
71 |
72 | /*
73 | * Api
74 | */
75 |
76 | const api = {
77 | play,
78 | pause,
79 | stop,
80 | destroy,
81 |
82 | duration: 0,
83 | progress: 0
84 | };
85 |
86 | /*
87 | * Getters & Setters
88 | */
89 |
90 | Object.defineProperties(api, {
91 | currentTime: {
92 | get: function() {
93 | if (cuedAt) {
94 | return cuedAt;
95 | }
96 | if (startedAt) {
97 | return context.currentTime - startedAt;
98 | }
99 | return 0;
100 | },
101 | set: function(value) {
102 | cuedAt = value;
103 | }
104 | },
105 | ended: {
106 | get: function() {
107 | return ended;
108 | }
109 | },
110 | paused: {
111 | get: function() {
112 | return paused;
113 | }
114 | },
115 | playing: {
116 | get: function() {
117 | return playing;
118 | }
119 | },
120 | sourceNode: {
121 | get: function() {
122 | return createSourceNode();
123 | }
124 | }
125 | });
126 |
127 | return Object.freeze(api);
128 | }
129 |
--------------------------------------------------------------------------------
/src/core/source/oscillator-source.js:
--------------------------------------------------------------------------------
1 | export default function OscillatorSource(type, context) {
2 | let ended = false,
3 | paused = false,
4 | cuedAt = 0,
5 | playing = false,
6 | sourceNode = null, // OscillatorSourceNode
7 | startedAt = 0,
8 | frequency = 200,
9 | api = null;
10 |
11 | function createSourceNode() {
12 | if (!sourceNode && context) {
13 | sourceNode = context.createOscillator();
14 | sourceNode.type = type;
15 | sourceNode.frequency.value = frequency;
16 | }
17 | return sourceNode;
18 | }
19 |
20 | /*
21 | * Controls
22 | */
23 |
24 | function play(delay) {
25 | delay = delay || 0;
26 | if (delay) {
27 | delay = context.currentTime + delay;
28 | }
29 |
30 | createSourceNode();
31 | sourceNode.start(delay);
32 |
33 | if (cuedAt) {
34 | startedAt = context.currentTime - cuedAt;
35 | } else {
36 | startedAt = context.currentTime;
37 | }
38 |
39 | ended = false;
40 | playing = true;
41 | paused = false;
42 | cuedAt = 0;
43 | }
44 |
45 | function stop() {
46 | if (sourceNode) {
47 | try {
48 | sourceNode.stop(0);
49 | } catch (e) {}
50 | sourceNode = null;
51 | }
52 | ended = true;
53 | paused = false;
54 | cuedAt = 0;
55 | playing = false;
56 | startedAt = 0;
57 | }
58 |
59 | function pause() {
60 | const elapsed = context.currentTime - startedAt;
61 | stop();
62 | cuedAt = elapsed;
63 | playing = false;
64 | paused = true;
65 | }
66 |
67 | /*
68 | * Destroy
69 | */
70 |
71 | function destroy() {
72 | stop();
73 | context = null;
74 | sourceNode = null;
75 | }
76 |
77 | /*
78 | * Api
79 | */
80 |
81 | api = {
82 | play: play,
83 | pause: pause,
84 | stop: stop,
85 | destroy: destroy
86 | };
87 |
88 | /*
89 | * Getters & Setters
90 | */
91 |
92 | Object.defineProperties(api, {
93 | currentTime: {
94 | get: function() {
95 | if (cuedAt) {
96 | return cuedAt;
97 | }
98 | if (startedAt) {
99 | return context.currentTime - startedAt;
100 | }
101 | return 0;
102 | },
103 | set: function(value) {
104 | cuedAt = value;
105 | }
106 | },
107 | duration: {
108 | value: 0
109 | },
110 | ended: {
111 | get: function() {
112 | return ended;
113 | }
114 | },
115 | frequency: {
116 | get: function() {
117 | return frequency;
118 | },
119 | set: function(value) {
120 | frequency = value;
121 | if (sourceNode) {
122 | sourceNode.frequency.value = value;
123 | }
124 | }
125 | },
126 | paused: {
127 | get: function() {
128 | return paused;
129 | }
130 | },
131 | playing: {
132 | get: function() {
133 | return playing;
134 | }
135 | },
136 | progress: {
137 | value: 0
138 | },
139 | sourceNode: {
140 | get: function() {
141 | return createSourceNode();
142 | }
143 | }
144 | });
145 |
146 | return Object.freeze(api);
147 | }
148 |
--------------------------------------------------------------------------------
/src/core/utils/dummy.js:
--------------------------------------------------------------------------------
1 | export default function dummy(context) {
2 | const buffer = context.createBuffer(1, 1, context.sampleRate);
3 | const source = context.createBufferSource();
4 | source.buffer = buffer;
5 | source.connect(context.destination);
6 | source.start(0);
7 | source.stop(0);
8 | source.disconnect();
9 | }
10 |
--------------------------------------------------------------------------------
/src/core/utils/emitter.js:
--------------------------------------------------------------------------------
1 | import events from 'events';
2 | const {EventEmitter} = events;
3 |
4 | export default class Emitter extends EventEmitter {
5 | constructor() {
6 | super();
7 | }
8 |
9 | off (type, listener) {
10 | if (listener) {
11 | return this.removeListener(type, listener);
12 | }
13 | if (type) {
14 | return this.removeAllListeners(type);
15 | }
16 | return this.removeAllListeners();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/core/utils/fake-context.js:
--------------------------------------------------------------------------------
1 | export default function FakeContext() {
2 |
3 | const startTime = Date.now();
4 |
5 | function fn() {}
6 |
7 | function param() {
8 | return {
9 | value: 1,
10 | defaultValue: 1,
11 | linearRampToValueAtTime: fn,
12 | setValueAtTime: fn,
13 | exponentialRampToValueAtTime: fn,
14 | setTargetAtTime: fn,
15 | setValueCurveAtTime: fn,
16 | cancelScheduledValues: fn
17 | };
18 | }
19 |
20 | function fakeNode() {
21 | return {
22 | connect: fn,
23 | disconnect: fn,
24 | // analyser
25 | frequencyBinCount: 0,
26 | smoothingTimeConstant: 0,
27 | fftSize: 0,
28 | minDecibels: 0,
29 | maxDecibels: 0,
30 | getByteTimeDomainData: fn,
31 | getByteFrequencyData: fn,
32 | getFloatTimeDomainData: fn,
33 | getFloatFrequencyData: fn,
34 | // gain
35 | gain: param(),
36 | // panner
37 | panningModel: 0,
38 | setPosition: fn,
39 | setOrientation: fn,
40 | setVelocity: fn,
41 | distanceModel: 0,
42 | refDistance: 0,
43 | maxDistance: 0,
44 | rolloffFactor: 0,
45 | coneInnerAngle: 360,
46 | coneOuterAngle: 360,
47 | coneOuterGain: 0,
48 | // filter:
49 | type: 0,
50 | frequency: param(),
51 | Q: param(),
52 | detune: param(),
53 | // delay
54 | delayTime: param(),
55 | // convolver
56 | buffer: 0,
57 | // compressor
58 | threshold: param(),
59 | knee: param(),
60 | ratio: param(),
61 | attack: param(),
62 | release: param(),
63 | reduction: param(),
64 | // distortion
65 | oversample: 0,
66 | curve: 0,
67 | // buffer
68 | sampleRate: 1,
69 | length: 0,
70 | duration: 0,
71 | numberOfChannels: 0,
72 | getChannelData: function() {
73 | return [];
74 | },
75 | copyFromChannel: fn,
76 | copyToChannel: fn,
77 | // listener
78 | dopplerFactor: 0,
79 | speedOfSound: 0,
80 | // osc
81 | start: fn
82 | };
83 | }
84 |
85 | // ie9
86 | if (!window.Uint8Array) {
87 | window.Uint8Array = window.Float32Array = Array;
88 | }
89 |
90 | return {
91 | isFake: true,
92 | activeSourceCount: 0,
93 | createAnalyser: fakeNode,
94 | createBuffer: fakeNode,
95 | createBufferSource: fakeNode,
96 | createMediaElementSource: fakeNode,
97 | createMediaStreamSource: fakeNode,
98 | createBiquadFilter: fakeNode,
99 | createChannelMerger: fakeNode,
100 | createChannelSplitter: fakeNode,
101 | createDynamicsCompressor: fakeNode,
102 | createConvolver: fakeNode,
103 | createDelay: fakeNode,
104 | createGain: fakeNode,
105 | createOscillator: fakeNode,
106 | createPanner: fakeNode,
107 | createScriptProcessor: fakeNode,
108 | createWaveShaper: fakeNode,
109 | decodeAudioData: fn,
110 | destination: fakeNode,
111 | listener: fakeNode(),
112 | sampleRate: 44100,
113 | state: '',
114 | get currentTime() {
115 | return (Date.now() - startTime) / 1000;
116 | }
117 | };
118 | }
119 |
--------------------------------------------------------------------------------
/src/core/utils/file.js:
--------------------------------------------------------------------------------
1 | const extensions = [];
2 | const canPlay = {};
3 |
4 | /*
5 | * Initial tests
6 | */
7 |
8 | const tests = [
9 | {
10 | ext: 'ogg',
11 | type: 'audio/ogg; codecs="vorbis"'
12 | }, {
13 | ext: 'mp3',
14 | type: 'audio/mpeg;'
15 | }, {
16 | ext: 'opus',
17 | type: 'audio/ogg; codecs="opus"'
18 | }, {
19 | ext: 'wav',
20 | type: 'audio/wav; codecs="1"'
21 | }, {
22 | ext: 'm4a',
23 | type: 'audio/x-m4a;'
24 | }, {
25 | ext: 'm4a',
26 | type: 'audio/aac;'
27 | }
28 | ];
29 |
30 | let el = document.createElement('audio');
31 | if (el) {
32 | tests.forEach(function(test) {
33 | const canPlayType = !!el.canPlayType(test.type);
34 | if (canPlayType && extensions.indexOf(test.ext) === -1) {
35 | extensions.push(test.ext);
36 | }
37 | canPlay[test.ext] = canPlayType;
38 | });
39 | el = null;
40 | }
41 |
42 | /*
43 | * find a supported file
44 | */
45 |
46 | function getFileExtension(url) {
47 | if (typeof url !== 'string') {
48 | return '';
49 | }
50 | // from DataURL
51 | if (url.slice(0, 5) === 'data:') {
52 | const match = url.match(/data:audio\/(ogg|mp3|opus|wav|m4a)/i);
53 | if (match && match.length > 1) {
54 | return match[1].toLowerCase();
55 | }
56 | }
57 | // from Standard URL
58 | url = url.split('?')[0];
59 | url = url.slice(url.lastIndexOf('/') + 1);
60 |
61 | const a = url.split('.');
62 | if (a.length === 1 || (a[0] === '' && a.length === 2)) {
63 | return '';
64 | }
65 | return a.pop().toLowerCase();
66 | }
67 |
68 | function getSupportedFile(fileNames) {
69 | let name;
70 |
71 | if (Array.isArray(fileNames)) {
72 | // if array get the first one that works
73 | for (let i = 0; i < fileNames.length; i++) {
74 | name = fileNames[i];
75 | const ext = getFileExtension(name);
76 | if (extensions.indexOf(ext) > -1) {
77 | break;
78 | }
79 | }
80 | } else if (typeof fileNames === 'object') {
81 | // if not array and is object
82 | Object.keys(fileNames).some(function(key) {
83 | name = fileNames[key];
84 | const ext = getFileExtension(name);
85 | return extensions.indexOf(ext) > -1;
86 | });
87 | }
88 | // if string just return
89 | return name || fileNames;
90 | }
91 |
92 | /*
93 | * infer file types
94 | */
95 |
96 | function isAudioBuffer(data) {
97 | return !!(data && window.AudioBuffer && data instanceof window.AudioBuffer);
98 | }
99 |
100 | function isArrayBuffer(data) {
101 | return !!(data && window.ArrayBuffer && data instanceof window.ArrayBuffer);
102 | }
103 |
104 | function isMediaElement(data) {
105 | return !!(data && window.HTMLMediaElement && data instanceof window.HTMLMediaElement);
106 | }
107 |
108 | function isMediaStream(data) {
109 | return !!(
110 | data && typeof data.getAudioTracks === 'function' && data.getAudioTracks().length &&
111 | window.MediaStreamTrack && data.getAudioTracks()[0] instanceof window.MediaStreamTrack
112 | );
113 | }
114 |
115 | function isOscillatorType(data) {
116 | return !!(
117 | data && typeof data === 'string' &&
118 | (data === 'sine' || data === 'square' || data === 'sawtooth' || data === 'triangle')
119 | );
120 | }
121 |
122 | function isURL(data) {
123 | return !!(data && typeof data === 'string' && (data.indexOf('.') > -1 || data.slice(0, 5) === 'data:'));
124 | }
125 |
126 | function containsURL(config) {
127 | if (!config || isMediaElement(config)) {
128 | return false;
129 | }
130 | // string, array or object with src/url/data property that is string, array or arraybuffer
131 | const src = getSrc(config);
132 | return isURL(src) || isArrayBuffer(src) || (Array.isArray(src) && isURL(src[0]));
133 | }
134 |
135 | function getSrc(config) {
136 | return config.src || config.url || config.data || config;
137 | }
138 |
139 | export default {
140 | canPlay,
141 | containsURL,
142 | extensions,
143 | getFileExtension,
144 | getSrc,
145 | getSupportedFile,
146 | isAudioBuffer,
147 | isArrayBuffer,
148 | isMediaElement,
149 | isMediaStream,
150 | isOscillatorType,
151 | isURL
152 | };
153 |
--------------------------------------------------------------------------------
/src/core/utils/firefox.js:
--------------------------------------------------------------------------------
1 | export default navigator && /Firefox/i.test(navigator.userAgent);
2 |
--------------------------------------------------------------------------------
/src/core/utils/iOS.js:
--------------------------------------------------------------------------------
1 | export default navigator && /(iPhone|iPad|iPod)/i.test(navigator.userAgent);
2 |
--------------------------------------------------------------------------------
/src/core/utils/isDefined.js:
--------------------------------------------------------------------------------
1 | export default function isDefined(value) {
2 | return typeof value !== 'undefined';
3 | }
4 |
--------------------------------------------------------------------------------
/src/core/utils/isSafeNumber.js:
--------------------------------------------------------------------------------
1 | export default function isSafeNumber(value) {
2 | return typeof value === 'number' && !isNaN(value) && isFinite(value);
3 | }
4 |
--------------------------------------------------------------------------------
/src/core/utils/log.js:
--------------------------------------------------------------------------------
1 | export default function log(api) {
2 | const title = 'sono ' + api.VERSION,
3 | info = 'Supported:' + api.isSupported +
4 | ' WebAudioAPI:' + api.hasWebAudio +
5 | ' TouchLocked:' + api.isTouchLocked +
6 | ' State:' + (api.context && api.context.state) +
7 | ' Extensions:' + api.file.extensions;
8 |
9 | if (navigator.userAgent.indexOf('Chrome') > -1) {
10 | const args = [
11 | '%c ♫ ' + title +
12 | ' ♫ %c ' + info + ' ',
13 | 'color: #FFFFFF; background: #379F7A',
14 | 'color: #1F1C0D; background: #E0FBAC'
15 | ];
16 | console.log.apply(console, args);
17 | } else if (window.console && window.console.log.call) {
18 | console.log.call(console, title + ' ' + info);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/core/utils/pageVisibility.js:
--------------------------------------------------------------------------------
1 | export default function pageVisibility(onHidden, onShown) {
2 | let enabled = false;
3 | let hidden = null;
4 | let visibilityChange = null;
5 |
6 | if (typeof document.hidden !== 'undefined') {
7 | hidden = 'hidden';
8 | visibilityChange = 'visibilitychange';
9 | } else if (typeof document.mozHidden !== 'undefined') {
10 | hidden = 'mozHidden';
11 | visibilityChange = 'mozvisibilitychange';
12 | } else if (typeof document.msHidden !== 'undefined') {
13 | hidden = 'msHidden';
14 | visibilityChange = 'msvisibilitychange';
15 | } else if (typeof document.webkitHidden !== 'undefined') {
16 | hidden = 'webkitHidden';
17 | visibilityChange = 'webkitvisibilitychange';
18 | }
19 |
20 | function onChange() {
21 | if (document[hidden]) {
22 | onHidden();
23 | } else {
24 | onShown();
25 | }
26 | }
27 |
28 | function enable(value) {
29 | enabled = value;
30 |
31 | if (enabled) {
32 | document.addEventListener(visibilityChange, onChange, false);
33 | } else {
34 | document.removeEventListener(visibilityChange, onChange);
35 | }
36 | }
37 |
38 | if (typeof visibilityChange !== 'undefined') {
39 | enable(true);
40 | }
41 |
42 | return {
43 | get enabled() {
44 | return enabled;
45 | },
46 | set enabled(value) {
47 | enable(value);
48 | }
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/src/core/utils/sound-group.js:
--------------------------------------------------------------------------------
1 | import Group from '../group';
2 |
3 | export default function SoundGroup(context, destination) {
4 | const group = new Group(context, destination);
5 | const sounds = group.sounds;
6 | let playbackRate = 1,
7 | loop = false,
8 | src;
9 |
10 | function getSource() {
11 | if (!sounds.length) {
12 | return;
13 | }
14 |
15 | src = sounds.slice(0)
16 | .sort((a, b) => b.duration - a.duration)[0];
17 | }
18 |
19 | const add = group.add;
20 | group.add = function(sound) {
21 | add(sound);
22 | getSource();
23 | return group;
24 | };
25 |
26 | const remove = group.remove;
27 | group.remove = function(soundOrId) {
28 | remove(soundOrId);
29 | getSource();
30 | return group;
31 | };
32 |
33 | Object.defineProperties(group, {
34 | currentTime: {
35 | get: function() {
36 | return src ? src.currentTime : 0;
37 | },
38 | set: function(value) {
39 | this.stop();
40 | this.play(0, value);
41 | }
42 | },
43 | duration: {
44 | get: function() {
45 | return src ? src.duration : 0;
46 | }
47 | },
48 | // ended: {
49 | // get: function() {
50 | // return src ? src.ended : false;
51 | // }
52 | // },
53 | loop: {
54 | get: function() {
55 | return loop;
56 | },
57 | set: function(value) {
58 | loop = !!value;
59 | sounds.forEach(function(sound) {
60 | sound.loop = loop;
61 | });
62 | }
63 | },
64 | paused: {
65 | get: function() {
66 | // return src ? src.paused : false;
67 | return !!src && src.paused;
68 | }
69 | },
70 | progress: {
71 | get: function() {
72 | return src ? src.progress : 0;
73 | }
74 | },
75 | playbackRate: {
76 | get: function() {
77 | return playbackRate;
78 | },
79 | set: function(value) {
80 | playbackRate = value;
81 | sounds.forEach(function(sound) {
82 | sound.playbackRate = playbackRate;
83 | });
84 | }
85 | },
86 | playing: {
87 | get: function() {
88 | // return src ? src.playing : false;
89 | return !!src && src.playing;
90 | }
91 | }
92 | });
93 |
94 | return group;
95 | }
96 |
--------------------------------------------------------------------------------
/src/core/utils/touchLock.js:
--------------------------------------------------------------------------------
1 | import iOS from './iOS';
2 | import dummy from './dummy';
3 |
4 | export default function touchLock(context, callback) {
5 | const locked = iOS;
6 |
7 | function unlock() {
8 | if (context && context.state === 'suspended') {
9 | context.resume()
10 | .then(() => {
11 | dummy(context);
12 | unlocked();
13 | });
14 | } else {
15 | unlocked();
16 | }
17 | }
18 |
19 | function unlocked() {
20 | document.body.removeEventListener('touchstart', unlock);
21 | document.body.removeEventListener('touchend', unlock);
22 | callback();
23 | }
24 |
25 | function addListeners() {
26 | document.body.addEventListener('touchstart', unlock, false);
27 | document.body.addEventListener('touchend', unlock, false);
28 | }
29 |
30 | if (locked) {
31 | if (document.readyState === 'loading') {
32 | document.addEventListener('DOMContentLoaded', addListeners);
33 | } else {
34 | addListeners();
35 | }
36 | }
37 |
38 | return locked;
39 | }
40 |
--------------------------------------------------------------------------------
/src/core/utils/utils.js:
--------------------------------------------------------------------------------
1 | import context from '../context';
2 |
3 | let offlineCtx;
4 | /*
5 | In contrast with a standard AudioContext, an OfflineAudioContext doesn't render
6 | the audio to the device hardware;
7 | instead, it generates it, as fast as it can, and outputs the result to an AudioBuffer.
8 | */
9 | function getOfflineContext(numOfChannels, length, sampleRate) {
10 | if (offlineCtx) {
11 | return offlineCtx;
12 | }
13 | numOfChannels = numOfChannels || 2;
14 | sampleRate = sampleRate || 44100;
15 | length = sampleRate || numOfChannels;
16 |
17 | const OfflineCtx = window.OfflineAudioContext || window.webkitOfflineAudioContext;
18 |
19 | offlineCtx = (OfflineCtx ? new OfflineCtx(numOfChannels, length, sampleRate) : null);
20 |
21 | return offlineCtx;
22 | }
23 |
24 |
25 | /*
26 | * clone audio buffer
27 | */
28 |
29 | function cloneBuffer(buffer, offset = 0, length = buffer.length) {
30 | if (!context || context.isFake) {
31 | return buffer;
32 | }
33 | const numChannels = buffer.numberOfChannels;
34 | const cloned = context.createBuffer(numChannels, length, buffer.sampleRate);
35 | for (let i = 0; i < numChannels; i++) {
36 | cloned.getChannelData(i)
37 | .set(buffer.getChannelData(i).slice(offset, offset + length));
38 | }
39 | return cloned;
40 | }
41 |
42 | /*
43 | * reverse audio buffer
44 | */
45 |
46 | function reverseBuffer(buffer) {
47 | const numChannels = buffer.numberOfChannels;
48 | for (let i = 0; i < numChannels; i++) {
49 | Array.prototype.reverse.call(buffer.getChannelData(i));
50 | }
51 | return buffer;
52 | }
53 |
54 | /*
55 | * ramp audio param
56 | */
57 |
58 | function ramp(param, fromValue, toValue, duration, linear) {
59 | if (context.isFake) {
60 | return;
61 | }
62 |
63 | param.setValueAtTime(fromValue, context.currentTime);
64 |
65 | if (linear) {
66 | param.linearRampToValueAtTime(toValue, context.currentTime + duration);
67 | } else {
68 | param.exponentialRampToValueAtTime(toValue, context.currentTime + duration);
69 | }
70 | }
71 |
72 | /*
73 | * get frequency from min to max by passing 0 to 1
74 | */
75 |
76 | function getFrequency(value) {
77 | if (context.isFake) {
78 | return 0;
79 | }
80 | // get frequency by passing number from 0 to 1
81 | // Clamp the frequency between the minimum value (40 Hz) and half of the
82 | // sampling rate.
83 | const minValue = 40;
84 | const maxValue = context.sampleRate / 2;
85 | // Logarithm (base 2) to compute how many octaves fall in the range.
86 | const numberOfOctaves = Math.log(maxValue / minValue) / Math.LN2;
87 | // Compute a multiplier from 0 to 1 based on an exponential scale.
88 | const multiplier = Math.pow(2, numberOfOctaves * (value - 1.0));
89 | // Get back to the frequency value between min and max.
90 | return maxValue * multiplier;
91 | }
92 |
93 | /*
94 | * Format seconds as timecode string
95 | */
96 |
97 | function timeCode(seconds, delim = ':') {
98 | // const h = Math.floor(seconds / 3600);
99 | // const m = Math.floor((seconds % 3600) / 60);
100 | const m = Math.floor(seconds / 60);
101 | const s = Math.floor((seconds % 3600) % 60);
102 | // const hr = (h < 10 ? '0' + h + delim : h + delim);
103 | const mn = (m < 10 ? '0' + m : m) + delim;
104 | const sc = (s < 10 ? '0' + s : s);
105 | // return hr + mn + sc;
106 | return mn + sc;
107 | }
108 |
109 | export default {
110 | getOfflineContext,
111 | cloneBuffer,
112 | reverseBuffer,
113 | ramp,
114 | getFrequency,
115 | timeCode
116 | };
117 |
--------------------------------------------------------------------------------
/src/effects/abstract-direct-effect.js:
--------------------------------------------------------------------------------
1 | import context from '../core/context';
2 |
3 | export default class AbstractDirectEffect {
4 | constructor(node) {
5 | this._node = this._in = this._out = node;
6 | }
7 |
8 | connect(node) {
9 | this._node.connect(node._in || node);
10 | }
11 |
12 | disconnect(...args) {
13 | this._node.disconnect(args);
14 | }
15 |
16 | update() {
17 | throw new Error('update must be overridden');
18 | }
19 |
20 | get context() {
21 | return context;
22 | }
23 |
24 | get numberOfInputs() {
25 | return 1;
26 | }
27 |
28 | get numberOfOutputs() {
29 | return 1;
30 | }
31 |
32 | get channelCount() {
33 | return 1;
34 | }
35 |
36 | get channelCountMode() {
37 | return 'max';
38 | }
39 |
40 | get channelInterpretation() {
41 | return 'speakers';
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/effects/abstract-effect.js:
--------------------------------------------------------------------------------
1 | import context from '../core/context';
2 | import isSafeNumber from '../core/utils/isSafeNumber';
3 |
4 | export default class AbstractEffect {
5 | constructor(node = null, nodeOut = null, enabled = true) {
6 | this._node = node;
7 | this._nodeOut = nodeOut || node;
8 | this._enabled;
9 |
10 | this._in = this.context.createGain();
11 | this._out = this.context.createGain();
12 | this._wet = this.context.createGain();
13 | this._dry = this.context.createGain();
14 |
15 | this._in.connect(this._dry);
16 | this._wet.connect(this._out);
17 | this._dry.connect(this._out);
18 |
19 | this.enable(enabled);
20 | }
21 |
22 | enable(b) {
23 | if (b === this._enabled) {
24 | return;
25 | }
26 |
27 | this._enabled = b;
28 |
29 | this._in.disconnect();
30 |
31 | if (b) {
32 | this._in.connect(this._dry);
33 | this._in.connect(this._node);
34 | this._nodeOut.connect(this._wet);
35 | } else {
36 | this._nodeOut.disconnect();
37 | this._in.connect(this._out);
38 | }
39 | }
40 |
41 | get wet() {
42 | return this._wet.gain.value;
43 | }
44 |
45 | set wet(value) {
46 | this.setSafeParamValue(this._wet.gain, value);
47 | }
48 |
49 | get dry() {
50 | return this._dry.gain.value;
51 | }
52 |
53 | set dry(value) {
54 | this.setSafeParamValue(this._dry.gain, value);
55 | }
56 |
57 | connect(node) {
58 | this._out.connect(node._in || node);
59 | }
60 |
61 | disconnect(...args) {
62 | this._out.disconnect(args);
63 | }
64 |
65 | setSafeParamValue(param, value) {
66 | if (!isSafeNumber(value)) {
67 | console.warn(this, 'Attempt to set invalid value ' + value + ' on AudioParam');
68 | return;
69 | }
70 | param.value = value;
71 | }
72 |
73 | update() {
74 | throw new Error('update must be overridden');
75 | }
76 |
77 | get context() {
78 | return context;
79 | }
80 |
81 | get numberOfInputs() {
82 | return 1;
83 | }
84 |
85 | get numberOfOutputs() {
86 | return 1;
87 | }
88 |
89 | get channelCount() {
90 | return 1;
91 | }
92 |
93 | get channelCountMode() {
94 | return 'max';
95 | }
96 |
97 | get channelInterpretation() {
98 | return 'speakers';
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/effects/compressor.js:
--------------------------------------------------------------------------------
1 | import AbstractEffect from './abstract-effect';
2 | import sono from '../core/sono';
3 |
4 | class Compressor extends AbstractEffect {
5 | constructor({attack = 0.003, knee = 30, ratio = 12, release = 0.25, threshold = -24, wet = 1, dry = 1} = {}) {
6 | super(sono.context.createDynamicsCompressor());
7 |
8 | this.wet = wet;
9 | this.dry = dry;
10 | this.update({threshold, knee, ratio, attack, release});
11 | }
12 |
13 | update(options) {
14 | // min decibels to start compressing at from -100 to 0
15 | this.setSafeParamValue(this._node.threshold, options.threshold);
16 | // decibel value to start curve to compressed value from 0 to 40
17 | this.setSafeParamValue(this._node.knee, options.knee);
18 | // amount of change per decibel from 1 to 20
19 | this.setSafeParamValue(this._node.ratio, options.ratio);
20 | // seconds to reduce gain by 10db from 0 to 1 - how quickly signal adapted when volume increased
21 | this.setSafeParamValue(this._node.attack, options.attack);
22 | // seconds to increase gain by 10db from 0 to 1 - how quickly signal adapted when volume redcuced
23 | this.setSafeParamValue(this._node.release, options.release);
24 | }
25 |
26 | get threshold() {
27 | return this._node.threshold.value;
28 | }
29 |
30 | set threshold(value) {
31 | this.setSafeParamValue(this._node.threshold, value);
32 | }
33 |
34 | get knee() {
35 | return this._node.knee.value;
36 | }
37 |
38 | set knee(value) {
39 | this.setSafeParamValue(this._node.knee, value);
40 | }
41 |
42 | get ratio() {
43 | return this._node.ratio.value;
44 | }
45 |
46 | set ratio(value) {
47 | this.setSafeParamValue(this._node.ratio, value);
48 | }
49 |
50 | get attack() {
51 | return this._node.attack.value;
52 | }
53 |
54 | set attack(value) {
55 | this.setSafeParamValue(this._node.attack, value);
56 | }
57 |
58 | get release() {
59 | return this._node.release.value;
60 | }
61 |
62 | set release(value) {
63 | this.setSafeParamValue(this._node.release, value);
64 | }
65 | }
66 |
67 | export default sono.register('compressor', opts => new Compressor(opts));
68 |
--------------------------------------------------------------------------------
/src/effects/convolver.js:
--------------------------------------------------------------------------------
1 | import AbstractEffect from './abstract-effect';
2 | import sono from '../core/sono';
3 | import file from '../core/utils/file';
4 | import Loader from '../core/utils/loader';
5 | import Sound from '../core/sound';
6 |
7 | class Convolver extends AbstractEffect {
8 | constructor({impulse, wet = 1, dry = 1} = {}) {
9 | super(sono.context.createConvolver(), null, false);
10 |
11 | this._loader = null;
12 |
13 | this.wet = wet;
14 | this.dry = dry;
15 | this.update({impulse});
16 | }
17 |
18 | _load(src) {
19 | if (sono.context.isFake) {
20 | return;
21 | }
22 | if (this._loader) {
23 | this._loader.destroy();
24 | }
25 | this._loader = new Loader(src);
26 | this._loader.audioContext = sono.context;
27 | this._loader.once('complete', impulse => this.update({impulse}));
28 | this._loader.once('error', error => console.error(error));
29 | this._loader.start();
30 | }
31 |
32 | update({impulse}) {
33 | if (!impulse) {
34 | return this;
35 | }
36 |
37 | if (file.isAudioBuffer(impulse)) {
38 | this._node.buffer = impulse;
39 | this.enable(true);
40 | return this;
41 | }
42 |
43 | if (impulse instanceof Sound) {
44 | if (impulse.data) {
45 | this.update({impulse: impulse.data});
46 | } else {
47 | impulse.once('ready', sound => this.update({
48 | impulse: sound.data
49 | }));
50 | }
51 | return this;
52 | }
53 |
54 | if (file.isArrayBuffer(impulse)) {
55 | this._load(impulse);
56 | return this;
57 | }
58 |
59 | if (file.isURL(file.getSupportedFile(impulse))) {
60 | this._load(file.getSupportedFile(impulse));
61 | }
62 |
63 | return this;
64 | }
65 |
66 | get impulse() {
67 | return this._node.buffer;
68 | }
69 |
70 | set impulse(impulse) {
71 | this.update({impulse});
72 | }
73 | }
74 |
75 | export default sono.register('convolver', opts => new Convolver(opts));
76 |
--------------------------------------------------------------------------------
/src/effects/distortion.js:
--------------------------------------------------------------------------------
1 | import AbstractEffect from './abstract-effect';
2 | import isSafeNumber from '../core/utils/isSafeNumber';
3 | import sono from '../core/sono';
4 |
5 | // up-sample before applying curve for better resolution result 'none', '2x' or '4x'
6 | // oversample: '2x'
7 | // oversample: '4x'
8 |
9 | class Distortion extends AbstractEffect {
10 | constructor({level = 1, samples = 22050, oversample = 'none', wet = 1, dry = 0} = {}) {
11 | super(sono.context.createWaveShaper(), null, false);
12 |
13 | this._node.oversample = oversample || 'none';
14 |
15 | this._samples = samples || 22050;
16 |
17 | this._curve = new Float32Array(this._samples);
18 |
19 | this._level;
20 |
21 | this._enabled = false;
22 |
23 | this.wet = wet;
24 | this.dry = dry;
25 | this.update({level});
26 | }
27 |
28 | update({level}) {
29 | if (level === this._level || !isSafeNumber(level)) {
30 | return;
31 | }
32 |
33 | this.enable(level > 0);
34 |
35 | if (!this._enabled) {
36 | return;
37 | }
38 |
39 | const k = level * 100;
40 | const deg = Math.PI / 180;
41 | const y = 2 / this._samples;
42 |
43 | let x;
44 | for (let i = 0; i < this._samples; ++i) {
45 | x = i * y - 1;
46 | this._curve[i] = (3 + k) * x * 20 * deg / (Math.PI + k * Math.abs(x));
47 | }
48 |
49 | this._level = level;
50 | this._node.curve = this._curve;
51 | }
52 |
53 | get level() {
54 | return this._level;
55 | }
56 |
57 | set level(level) {
58 | this.update({level});
59 | }
60 | }
61 |
62 | export default sono.register('distortion', opts => new Distortion(opts));
63 |
--------------------------------------------------------------------------------
/src/effects/echo.js:
--------------------------------------------------------------------------------
1 | import AbstractEffect from './abstract-effect';
2 | import sono from '../core/sono';
3 |
4 | class Echo extends AbstractEffect {
5 | constructor({delay = 0.5, feedback = 0.5, wet = 1, dry = 1} = {}) {
6 | super(sono.context.createDelay(), sono.context.createGain());
7 |
8 | this._delay = this._node;
9 | this._feedback = this._nodeOut;
10 |
11 | this._delay.connect(this._feedback);
12 | this._feedback.connect(this._delay);
13 |
14 | this.wet = wet;
15 | this.dry = dry;
16 | this.update({delay, feedback});
17 | }
18 |
19 | enable(value) {
20 | super.enable(value);
21 |
22 | if (this._feedback && value) {
23 | this._feedback.connect(this._delay);
24 | }
25 | }
26 |
27 | update(options) {
28 | this.delay = options.delay;
29 | this.feedback = options.feedback;
30 | }
31 |
32 | get delay() {
33 | return this._delay.delayTime.value;
34 | }
35 |
36 | set delay(value) {
37 | this.setSafeParamValue(this._delay.delayTime, value);
38 | }
39 |
40 | get feedback() {
41 | return this._feedback.gain.value;
42 | }
43 |
44 | set feedback(value) {
45 | this.setSafeParamValue(this._feedback.gain, value);
46 | }
47 | }
48 |
49 | export default sono.register('echo', opts => new Echo(opts));
50 |
--------------------------------------------------------------------------------
/src/effects/flanger.js:
--------------------------------------------------------------------------------
1 | import AbstractEffect from './abstract-effect';
2 | import sono from '../core/sono';
3 |
4 | class MonoFlanger extends AbstractEffect {
5 | constructor({delay = 0.005, feedback = 0.5, frequency = 0.002, gain = 0.25, wet = 1, dry = 1} = {}) {
6 | super(sono.context.createDelay());
7 |
8 | this._delay = this._node;
9 | this._feedback = sono.context.createGain();
10 | this._lfo = sono.context.createOscillator();
11 | this._gain = sono.context.createGain();
12 | this._lfo.type = 'sine';
13 |
14 | this._delay.connect(this._feedback);
15 | this._feedback.connect(this._in);
16 |
17 | this._lfo.connect(this._gain);
18 | this._gain.connect(this._delay.delayTime);
19 | this._lfo.start(0);
20 |
21 | this.wet = wet;
22 | this.dry = dry;
23 | this.update({delay, feedback, frequency, gain});
24 | }
25 |
26 | update(options) {
27 | this.delay = options.delay;
28 | this.frequency = options.frequency;
29 | this.gain = options.gain;
30 | this.feedback = options.feedback;
31 | }
32 |
33 | get delay() {
34 | return this._delay.delayTime.value;
35 | }
36 |
37 | set delay(value) {
38 | this.setSafeParamValue(this._delay.delayTime, value);
39 | }
40 |
41 | get frequency() {
42 | return this._lfo.frequency.value;
43 | }
44 |
45 | set frequency(value) {
46 | this.setSafeParamValue(this._lfo.frequency, value);
47 | }
48 |
49 | get gain() {
50 | return this._gain.gain.value;
51 | }
52 |
53 | set gain(value) {
54 | this.setSafeParamValue(this._gain.gain, value);
55 | }
56 |
57 | get feedback() {
58 | return this._feedback.gain.value;
59 | }
60 |
61 | set feedback(value) {
62 | this.setSafeParamValue(this._feedback.gain, value);
63 | }
64 | }
65 |
66 | sono.register('monoFlanger', opts => new MonoFlanger(opts));
67 |
68 | class StereoFlanger extends AbstractEffect {
69 | constructor({delay = 0.003, feedback = 0.5, frequency = 0.5, gain = 0.005, wet = 1, dry = 1} = {}) {
70 | super(sono.context.createChannelSplitter(2), sono.context.createChannelMerger(2));
71 |
72 | this._splitter = this._node;
73 | this._merger = this._nodeOut;
74 | this._feedbackL = sono.context.createGain();
75 | this._feedbackR = sono.context.createGain();
76 | this._lfo = sono.context.createOscillator();
77 | this._lfoGainL = sono.context.createGain();
78 | this._lfoGainR = sono.context.createGain();
79 | this._delayL = sono.context.createDelay();
80 | this._delayR = sono.context.createDelay();
81 |
82 | this._lfo.type = 'sine';
83 |
84 | this._splitter.connect(this._delayL, 0);
85 | this._splitter.connect(this._delayR, 1);
86 |
87 | this._delayL.connect(this._feedbackL);
88 | this._delayR.connect(this._feedbackR);
89 |
90 | this._feedbackL.connect(this._delayR);
91 | this._feedbackR.connect(this._delayL);
92 |
93 | this._delayL.connect(this._merger, 0, 0);
94 | this._delayR.connect(this._merger, 0, 1);
95 |
96 | this._lfo.connect(this._lfoGainL);
97 | this._lfo.connect(this._lfoGainR);
98 | this._lfoGainL.connect(this._delayL.delayTime);
99 | this._lfoGainR.connect(this._delayR.delayTime);
100 | this._lfo.start(0);
101 |
102 | this.wet = wet;
103 | this.dry = dry;
104 | this.update({delay, feedback, frequency, gain});
105 | }
106 |
107 | update(options) {
108 | this.delay = options.delay;
109 | this.frequency = options.frequency;
110 | this.gain = options.gain;
111 | this.feedback = options.feedback;
112 | }
113 |
114 | get delay() {
115 | return this._delayL.delayTime.value;
116 | }
117 |
118 | set delay(value) {
119 | this.setSafeParamValue(this._delayL.delayTime, value);
120 | this._delayR.delayTime.value = this._delayL.delayTime.value;
121 | }
122 |
123 | get frequency() {
124 | return this._lfo.frequency.value;
125 | }
126 |
127 | set frequency(value) {
128 | this.setSafeParamValue(this._lfo.frequency, value);
129 | }
130 |
131 | get gain() {
132 | return this._lfoGainL.gain.value;
133 | }
134 |
135 | set gain(value) {
136 | this.setSafeParamValue(this._lfoGainL.gain, value);
137 | this._lfoGainR.gain.value = 0 - this._lfoGainL.gain.value;
138 | }
139 |
140 | get feedback() {
141 | return this._feedbackL.gain.value;
142 | }
143 |
144 | set feedback(value) {
145 | this.setSafeParamValue(this._feedbackL.gain, value);
146 | this._feedbackR.gain.value = this._feedbackL.gain.value;
147 | }
148 | }
149 |
150 | sono.register('stereoFlanger', opts => new StereoFlanger(opts));
151 |
152 | export default sono.register('flanger', (opts = {}) => {
153 | return opts.stereo ? new StereoFlanger(opts) : new MonoFlanger(opts);
154 | });
155 |
--------------------------------------------------------------------------------
/src/effects/index.js:
--------------------------------------------------------------------------------
1 | import analyser from './analyser';
2 | import compressor from './compressor';
3 | import convolver from './convolver';
4 | import distortion from './distortion';
5 | import echo from './echo';
6 | import filter from './filter';
7 | import flanger from './flanger';
8 | import panner from './panner';
9 | import phaser from './phaser';
10 | import reverb from './reverb';
11 |
12 | export {
13 | analyser,
14 | compressor,
15 | convolver,
16 | distortion,
17 | echo,
18 | filter,
19 | flanger,
20 | panner,
21 | phaser,
22 | reverb
23 | };
24 |
25 | export default {
26 | analyser,
27 | compressor,
28 | convolver,
29 | distortion,
30 | echo,
31 | filter,
32 | flanger,
33 | panner,
34 | phaser,
35 | reverb
36 | };
37 |
--------------------------------------------------------------------------------
/src/effects/phaser.js:
--------------------------------------------------------------------------------
1 | import AbstractEffect from './abstract-effect';
2 | import sono from '../core/sono';
3 |
4 | class Phaser extends AbstractEffect {
5 | constructor({stages = 8, feedback = 0.5, frequency = 0.5, gain = 300, wet = 0.8, dry = 0.8} = {}) {
6 | stages = stages || 8;
7 |
8 | const filters = [];
9 | for (let i = 0; i < stages; i++) {
10 | filters.push(sono.context.createBiquadFilter());
11 | }
12 |
13 | const first = filters[0];
14 | const last = filters[filters.length - 1];
15 |
16 | super(first, last);
17 |
18 | this._stages = stages;
19 | this._feedback = sono.context.createGain();
20 | this._lfo = sono.context.createOscillator();
21 | this._lfoGain = sono.context.createGain();
22 | this._lfo.type = 'sine';
23 |
24 | for (let i = 0; i < filters.length; i++) {
25 | const filter = filters[i];
26 | filter.type = 'allpass';
27 | filter.frequency.value = 1000 * i;
28 | this._lfoGain.connect(filter.frequency);
29 | // filter.Q.value = 10;
30 |
31 | if (i > 0) {
32 | filters[i - 1].connect(filter);
33 | }
34 | }
35 |
36 | this._lfo.connect(this._lfoGain);
37 | this._lfo.start(0);
38 |
39 | this._nodeOut.connect(this._feedback);
40 | this._feedback.connect(this._node);
41 |
42 | this.wet = wet;
43 | this.dry = dry;
44 | this.update({frequency, gain, feedback});
45 | }
46 |
47 | enable(value) {
48 | super.enable(value);
49 |
50 | if (this._feedback) {
51 | this._feedback.disconnect();
52 | }
53 |
54 | if (value && this._feedback) {
55 | this._nodeOut.connect(this._feedback);
56 | this._feedback.connect(this._node);
57 | }
58 | }
59 |
60 | update(options) {
61 | this.frequency = options.frequency;
62 | this.gain = options.gain;
63 | this.feedback = options.feedback;
64 | }
65 |
66 | get stages() {
67 | return this._stages;
68 | }
69 |
70 | get frequency() {
71 | return this._lfo.frequency.value;
72 | }
73 |
74 | set frequency(value) {
75 | this.setSafeParamValue(this._lfo.frequency, value);
76 | }
77 |
78 | get gain() {
79 | return this._lfoGain.gain.value;
80 | }
81 |
82 | set gain(value) {
83 | this.setSafeParamValue(this._lfoGain.gain, value);
84 | }
85 |
86 | get feedback() {
87 | return this._feedback.gain.value;
88 | }
89 |
90 | set feedback(value) {
91 | this.setSafeParamValue(this._feedback.gain, value);
92 | }
93 | }
94 |
95 | export default sono.register('phaser', opts => new Phaser(opts));
96 |
--------------------------------------------------------------------------------
/src/effects/reverb.js:
--------------------------------------------------------------------------------
1 | import AbstractEffect from './abstract-effect';
2 | import sono from '../core/sono';
3 | import isSafeNumber from '../core/utils/isSafeNumber';
4 | import isDefined from '../core/utils/isDefined';
5 |
6 | function createImpulseResponse({time, decay, reverse, buffer}) {
7 | const rate = sono.context.sampleRate;
8 | const length = Math.floor(rate * time);
9 |
10 | let impulseResponse;
11 |
12 | if (buffer && buffer.length === length) {
13 | impulseResponse = buffer;
14 | } else {
15 | impulseResponse = sono.context.createBuffer(2, length, rate);
16 | }
17 |
18 | const left = impulseResponse.getChannelData(0);
19 | const right = impulseResponse.getChannelData(1);
20 |
21 | let n, e;
22 | for (let i = 0; i < length; i++) {
23 | n = reverse ? length - i : i;
24 | e = Math.pow(1 - n / length, decay);
25 | left[i] = (Math.random() * 2 - 1) * e;
26 | right[i] = (Math.random() * 2 - 1) * e;
27 | }
28 |
29 | return impulseResponse;
30 | }
31 |
32 | class Reverb extends AbstractEffect {
33 | constructor({time = 1, decay = 5, reverse = false, wet = 1, dry = 1} = {}) {
34 | super(sono.context.createConvolver());
35 |
36 | this._convolver = this._node;
37 |
38 | this._length = 0;
39 | this._impulseResponse = null;
40 | this._opts = {};
41 |
42 | this.wet = wet;
43 | this.dry = dry;
44 | this.update({time, decay, reverse});
45 | }
46 |
47 | update({time, decay, reverse}) {
48 | let changed = false;
49 | if (time !== this._opts.time && isSafeNumber(time)) {
50 | this._opts.time = time;
51 | changed = true;
52 | }
53 | if (decay !== this._opts.decay && isSafeNumber(decay)) {
54 | this._opts.decay = decay;
55 | changed = true;
56 | }
57 | if (isDefined(reverse) && reverse !== this._reverse) {
58 | this._opts.reverse = reverse;
59 | changed = true;
60 | }
61 | if (!changed) {
62 | return;
63 | }
64 |
65 | this._opts.buffer = time <= 0 ? null : createImpulseResponse(this._opts);
66 | this._convolver.buffer = this._opts.buffer;
67 | }
68 |
69 | get time() {
70 | return this._opts.time;
71 | }
72 |
73 | set time(value) {
74 | this.update({time: value});
75 | }
76 |
77 | get decay() {
78 | return this._opts.decay;
79 | }
80 |
81 | set decay(value) {
82 | this.update({decay: value});
83 | }
84 |
85 | get reverse() {
86 | return this._opts.reverse;
87 | }
88 |
89 | set reverse(value) {
90 | this.update({reverse: value});
91 | }
92 | }
93 |
94 | export default sono.register('reverb', opts => new Reverb(opts));
95 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import sono from './core/sono';
2 | import './effects';
3 | import './utils';
4 |
5 | export default sono;
6 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import microphone from './microphone';
2 | import recorder from './recorder';
3 | import waveform from './waveform';
4 | import waveformer from './waveformer';
5 |
6 | export {
7 | microphone,
8 | recorder,
9 | waveform,
10 | waveformer
11 | };
12 |
13 | export default {
14 | microphone,
15 | recorder,
16 | waveform,
17 | waveformer
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/microphone.js:
--------------------------------------------------------------------------------
1 | import sono from '../core/sono';
2 |
3 | function microphone(connected, denied, error) {
4 | navigator.getUserMedia =
5 | navigator.mediaDevices.getUserMedia ||
6 | navigator.getUserMedia ||
7 | navigator.webkitGetUserMedia ||
8 | navigator.mozGetUserMedia ||
9 | navigator.msGetUserMedia;
10 |
11 | error = error || function(err) {
12 | console.error(err);
13 | };
14 |
15 | const isSupported = !!navigator.getUserMedia;
16 | const api = {};
17 | let stream = null;
18 |
19 | function onConnect(micStream) {
20 | stream = micStream;
21 | connected(stream);
22 | }
23 |
24 | function onError(e) {
25 | if (denied && e.name === 'PermissionDeniedError' || e === 'PERMISSION_DENIED') {
26 | denied();
27 | } else {
28 | error(e.message || e);
29 | }
30 | }
31 |
32 | function connect() {
33 | if (!isSupported) {
34 | return api;
35 | }
36 |
37 | if (navigator.mediaDevices.getUserMedia) {
38 | navigator.mediaDevices.getUserMedia({
39 | audio: true
40 | }).then(onConnect).catch(onError);
41 | } else {
42 | navigator.getUserMedia({
43 | audio: true
44 | }, onConnect, onError);
45 | }
46 | return api;
47 | }
48 |
49 | function disconnect() {
50 | if (stream.stop) {
51 | stream.stop();
52 | } else {
53 | stream.getAudioTracks()[0].stop();
54 | }
55 | stream = null;
56 | return api;
57 | }
58 |
59 | return Object.assign(api, {
60 | connect,
61 | disconnect,
62 | isSupported,
63 | get stream() {
64 | return stream;
65 | }
66 | });
67 | }
68 |
69 | export default sono.register('microphone', microphone, sono.utils);
70 |
--------------------------------------------------------------------------------
/src/utils/recorder.js:
--------------------------------------------------------------------------------
1 | import sono from '../core/sono';
2 |
3 | function recorder(passThrough = false) {
4 | const bufferLength = 4096;
5 | const buffersL = [];
6 | const buffersR = [];
7 | let startedAt = 0;
8 | let stoppedAt = 0;
9 | let script = null;
10 | let isRecording = false;
11 | let soundOb = null;
12 |
13 | const input = sono.context.createGain();
14 | const output = sono.context.createGain();
15 | output.gain.value = passThrough ? 1 : 0;
16 |
17 | const node = {
18 | _in: input,
19 | _out: output,
20 | connect(n) {
21 | output.connect(n._in || n);
22 | },
23 | disconnect(...args) {
24 | output.disconnect(args);
25 | }
26 | };
27 |
28 | function mergeBuffers(buffers, length) {
29 | const buffer = new Float32Array(length);
30 | let offset = 0;
31 | for (let i = 0; i < buffers.length; i++) {
32 | buffer.set(buffers[i], offset);
33 | offset += buffers[i].length;
34 | }
35 | return buffer;
36 | }
37 |
38 | function getBuffer() {
39 | if (!buffersL.length) {
40 | return sono.context.createBuffer(2, bufferLength, sono.context.sampleRate);
41 | }
42 | const recordingLength = buffersL.length * bufferLength;
43 | const buffer = sono.context.createBuffer(2, recordingLength, sono.context.sampleRate);
44 | buffer.getChannelData(0)
45 | .set(mergeBuffers(buffersL, recordingLength));
46 | buffer.getChannelData(1)
47 | .set(mergeBuffers(buffersR, recordingLength));
48 | return buffer;
49 | }
50 |
51 | function destroyScriptProcessor() {
52 | if (script) {
53 | script.onaudioprocess = null;
54 | input.disconnect();
55 | script.disconnect();
56 | }
57 | }
58 |
59 | function createScriptProcessor() {
60 | destroyScriptProcessor();
61 |
62 | script = sono.context.createScriptProcessor(bufferLength, 2, 2);
63 | input.connect(script);
64 | script.connect(output);
65 | script.connect(sono.context.destination);
66 | // output.connect(sono.context.destination);
67 |
68 |
69 | script.onaudioprocess = function(event) {
70 | const inputL = event.inputBuffer.getChannelData(0);
71 | const inputR = event.inputBuffer.getChannelData(1);
72 |
73 | if (passThrough) {
74 | const outputL = event.outputBuffer.getChannelData(0);
75 | const outputR = event.outputBuffer.getChannelData(1);
76 | outputL.set(inputL);
77 | outputR.set(inputR);
78 | }
79 |
80 | if (isRecording) {
81 | buffersL.push(new Float32Array(inputL));
82 | buffersR.push(new Float32Array(inputR));
83 | }
84 | };
85 | }
86 |
87 | return {
88 | start(sound) {
89 | if (!sound) {
90 | return;
91 | }
92 | createScriptProcessor();
93 | buffersL.length = 0;
94 | buffersR.length = 0;
95 | startedAt = sono.context.currentTime;
96 | stoppedAt = 0;
97 | soundOb = sound;
98 | sound.effects.add(node);
99 | isRecording = true;
100 | },
101 | stop() {
102 | soundOb.effects.remove(node);
103 | soundOb = null;
104 | stoppedAt = sono.context.currentTime;
105 | isRecording = false;
106 | destroyScriptProcessor();
107 | return getBuffer();
108 | },
109 | getDuration() {
110 | if (!isRecording) {
111 | return stoppedAt - startedAt;
112 | }
113 | return sono.context.currentTime - startedAt;
114 | },
115 | get isRecording() {
116 | return isRecording;
117 | }
118 | };
119 | }
120 |
121 | export default sono.register('recorder', recorder, sono.utils);
122 |
--------------------------------------------------------------------------------
/src/utils/waveform.js:
--------------------------------------------------------------------------------
1 | import sono from '../core/sono';
2 |
3 | function waveform() {
4 | let buffer,
5 | wave;
6 |
7 | return function(audioBuffer, length) {
8 | if (!window.Float32Array || !window.AudioBuffer) {
9 | return [];
10 | }
11 |
12 | const sameBuffer = buffer === audioBuffer;
13 | const sameLength = wave && wave.length === length;
14 | if (sameBuffer && sameLength) {
15 | return wave;
16 | }
17 |
18 | wave = new Float32Array(length);
19 |
20 | if (!audioBuffer) {
21 | return wave;
22 | }
23 |
24 | // cache for repeated calls
25 | buffer = audioBuffer;
26 |
27 | const chunk = Math.floor(buffer.length / length),
28 | resolution = 5, // 10
29 | incr = Math.max(Math.floor(chunk / resolution), 1);
30 | let greatest = 0;
31 |
32 | for (let i = 0; i < buffer.numberOfChannels; i++) {
33 | // check each channel
34 | const channel = buffer.getChannelData(i);
35 | for (let j = 0; j < length; j++) {
36 | // get highest value within the chunk
37 | for (let k = j * chunk, l = k + chunk; k < l; k += incr) {
38 | // select highest value from channels
39 | let a = channel[k];
40 | if (a < 0) {
41 | a = -a;
42 | }
43 | if (a > wave[j]) {
44 | wave[j] = a;
45 | }
46 | // update highest overall for scaling
47 | if (a > greatest) {
48 | greatest = a;
49 | }
50 | }
51 | }
52 | }
53 | // scale up
54 | const scale = 1 / greatest;
55 | for (let i = 0; i < wave.length; i++) {
56 | wave[i] *= scale;
57 | }
58 |
59 | return wave;
60 | };
61 | }
62 |
63 | export default sono.register('waveform', waveform, sono.utils);
64 |
--------------------------------------------------------------------------------
/src/utils/waveformer.js:
--------------------------------------------------------------------------------
1 | import sono from '../core/sono';
2 |
3 | const halfPI = Math.PI / 2;
4 | const twoPI = Math.PI * 2;
5 |
6 | function waveformer(config) {
7 |
8 | const style = config.style || 'fill', // 'fill' or 'line'
9 | shape = config.shape || 'linear', // 'circular' or 'linear'
10 | color = config.color || 0,
11 | bgColor = config.bgColor,
12 | lineWidth = config.lineWidth || 1,
13 | percent = config.percent || 1,
14 | originX = config.x || 0,
15 | originY = config.y || 0,
16 | transform = config.transform;
17 |
18 | let canvas = config.canvas,
19 | width = config.width || (canvas && canvas.width),
20 | height = config.height || (canvas && canvas.height);
21 |
22 | let ctx = null, currentColor, i, x, y,
23 | radius, innerRadius, centerX, centerY;
24 |
25 | if (!canvas && !config.context) {
26 | canvas = document.createElement('canvas');
27 | width = width || canvas.width;
28 | height = height || canvas.height;
29 | canvas.width = width;
30 | canvas.height = height;
31 | }
32 |
33 | if (shape === 'circular') {
34 | radius = config.radius || Math.min(height / 2, width / 2);
35 | innerRadius = config.innerRadius || radius / 2;
36 | centerX = originX + width / 2;
37 | centerY = originY + height / 2;
38 | }
39 |
40 | ctx = config.context || canvas.getContext('2d');
41 |
42 | function clear() {
43 | if (bgColor) {
44 | ctx.fillStyle = bgColor;
45 | ctx.fillRect(originX, originY, width, height);
46 | } else {
47 | ctx.clearRect(originX, originY, width, height);
48 | }
49 |
50 | ctx.lineWidth = lineWidth;
51 |
52 | currentColor = null;
53 |
54 | if (typeof color !== 'function') {
55 | ctx.strokeStyle = color;
56 | ctx.beginPath();
57 | }
58 | }
59 |
60 | function updateColor(position, length, value) {
61 | if (typeof color === 'function') {
62 | const newColor = color(position, length, value);
63 | if (newColor !== currentColor) {
64 | currentColor = newColor;
65 | ctx.stroke();
66 | ctx.strokeStyle = currentColor;
67 | ctx.beginPath();
68 | }
69 | }
70 | }
71 |
72 | function getValue(value, position, length) {
73 | if (typeof transform === 'function') {
74 | return transform(value, position, length);
75 | }
76 | return value;
77 | }
78 |
79 | function getWaveform(value, length) {
80 | if (value && typeof value.waveform === 'function') {
81 | return value.waveform(length);
82 | }
83 | if (value) {
84 | return value;
85 | }
86 | if (config.waveform) {
87 | return config.waveform;
88 | }
89 | if (config.sound) {
90 | return config.sound.waveform(length);
91 | }
92 | return null;
93 | }
94 |
95 | function update(wave) {
96 |
97 | clear();
98 |
99 | if (shape === 'circular') {
100 | const waveform = getWaveform(wave, 360);
101 | const length = Math.floor(waveform.length * percent);
102 |
103 | const step = twoPI / length;
104 | let angle, magnitude, sine, cosine;
105 |
106 | for (i = 0; i < length; i++) {
107 | const value = getValue(waveform[i], i, length);
108 | updateColor(i, length, value);
109 |
110 | angle = i * step - halfPI;
111 | cosine = Math.cos(angle);
112 | sine = Math.sin(angle);
113 |
114 | if (style === 'fill') {
115 | x = centerX + innerRadius * cosine;
116 | y = centerY + innerRadius * sine;
117 | ctx.moveTo(x, y);
118 | }
119 |
120 | magnitude = innerRadius + (radius - innerRadius) * value;
121 | x = centerX + magnitude * cosine;
122 | y = centerY + magnitude * sine;
123 |
124 | if (style === 'line' && i === 0) {
125 | ctx.moveTo(x, y);
126 | }
127 |
128 | ctx.lineTo(x, y);
129 | }
130 |
131 | if (style === 'line') {
132 | ctx.closePath();
133 | }
134 | } else {
135 |
136 | const waveform = getWaveform(wave, width);
137 | const maxX = width - lineWidth / 2;
138 | let length = Math.min(waveform.length, maxX);
139 | length = Math.floor(length * percent);
140 | const stepX = maxX / length;
141 |
142 | for (i = 0; i < length; i++) {
143 | const value = getValue(waveform[i], i, length);
144 | updateColor(i, length, value);
145 |
146 | if (style === 'line' && i > 0) {
147 | ctx.lineTo(x, y);
148 | }
149 |
150 | x = originX + i * stepX;
151 | y = originY + height - Math.round(height * value);
152 | y = Math.floor(Math.min(y, originY + height - lineWidth / 2));
153 |
154 | if (style === 'fill') {
155 | x = Math.ceil(x + lineWidth / 2);
156 | ctx.moveTo(x, y);
157 | ctx.lineTo(x, originY + height);
158 | } else {
159 | ctx.lineTo(x, y);
160 | }
161 | }
162 | }
163 | ctx.stroke();
164 | }
165 |
166 | update.canvas = canvas;
167 |
168 | if (config.waveform || config.sound) {
169 | update();
170 | }
171 |
172 | return update;
173 | }
174 |
175 | export default sono.register('waveformer', waveformer, sono.utils);
176 |
--------------------------------------------------------------------------------
/test/api.spec.js:
--------------------------------------------------------------------------------
1 | describe('sono API', () => {
2 |
3 | describe('misc', () => {
4 | it('should exist', () => {
5 | expect(sono).to.be.an('object');
6 | });
7 |
8 | it('should have context property', () => {
9 | expect(sono).to.have.property('context');
10 | });
11 |
12 | it('should have hasWebAudio bool', () => {
13 | expect(sono.hasWebAudio).to.be.a('boolean');
14 | });
15 |
16 | it('should have isSupported bool', () => {
17 | expect(sono.isSupported).to.be.a('boolean');
18 | });
19 |
20 | it('should have VERSION string', () => {
21 | expect(sono.VERSION).to.be.a('string');
22 | });
23 |
24 | it('should have log func', () => {
25 | expect(sono.log).to.be.a('function');
26 | });
27 |
28 | it('should have log playInBackground getter/setter', () => {
29 | expect(sono.playInBackground).to.be.a('boolean');
30 | });
31 | });
32 |
33 | describe('create', () => {
34 | it('should have expected api', () => {
35 | expect(sono.create).to.be.a('function');
36 | expect(sono.create.length).to.eql(1);
37 | });
38 |
39 | it('should return new Sound', () => {
40 | const sound = sono.createSound({
41 | id: 'newsoundA',
42 | data: new Audio()
43 | });
44 | expect(sound).to.exist;
45 | expect(sound.id).to.eql('newsoundA');
46 | expect(sono.getSound(sound.id)).to.exist;
47 | });
48 | });
49 |
50 | describe('destroy', () => {
51 | it('should have expected api', () => {
52 | expect(sono.destroy).to.be.a('function');
53 | expect(sono.destroy.length).to.eql(1);
54 | });
55 |
56 | it('should destroy existing sound by id', () => {
57 | sono.createSound({
58 | id: 'killme',
59 | data: new Audio()
60 | });
61 | expect(sono.getSound('killme')).to.exist;
62 | sono.destroy('killme');
63 | expect(sono.getSound('killme')).to.not.exist;
64 | });
65 |
66 | it('should destroy existing sound by instance', () => {
67 | const sound = sono.createSound({
68 | id: 'killmeagain',
69 | data: new Audio()
70 | });
71 | expect(sound).to.exist;
72 | sono.destroy(sound);
73 | expect(sono.getSound('killmeagain')).to.not.exist;
74 | });
75 | });
76 |
77 | describe('getSound', () => {
78 | it('should have expected api', () => {
79 | expect(sono.getSound).to.be.a('function');
80 | expect(sono.getSound.length).to.eql(1);
81 | });
82 |
83 | it('should return existing sound', () => {
84 | sono.createSound({
85 | id: 'yep',
86 | data: new Audio()
87 | });
88 | expect(sono.getSound('yep')).to.exist;
89 | expect(sono.getSound('yep').id).to.eql('yep');
90 | });
91 |
92 | it('should return null for non-existant sound', () => {
93 | expect(sono.getSound('nope')).to.not.exist;
94 | });
95 | });
96 |
97 | describe('controls', () => {
98 | it('should have expected members', () => {
99 | expect(sono.mute).to.be.a('function');
100 | expect(sono.unMute).to.be.a('function');
101 | expect(sono.pauseAll).to.be.a('function');
102 | expect(sono.resumeAll).to.be.a('function');
103 | expect(sono.stopAll).to.be.a('function');
104 | expect(sono.play).to.be.a('function');
105 | expect(sono.pause).to.be.a('function');
106 | expect(sono.stop).to.be.a('function');
107 | expect(sono.volume).to.be.a('number');
108 | expect(sono.fade).to.be.a('function');
109 | });
110 |
111 | it('should have get/set volume', () => {
112 | const desc = Object.getOwnPropertyDescriptor(sono, 'volume');
113 | expect(desc.get).to.be.a('function');
114 | expect(desc.set).to.be.a('function');
115 | });
116 | });
117 |
118 | describe('load', () => {
119 | it('should have expected api', () => {
120 | expect(sono.load).to.be.a('function');
121 | expect(sono.load.length).to.eql(1);
122 | });
123 | });
124 |
125 | describe('canPlay', () => {
126 | it('should have expected members', () => {
127 | expect(sono.canPlay).to.be.an('object');
128 | expect(sono.canPlay.ogg).to.be.a('boolean');
129 | expect(sono.canPlay.mp3).to.be.a('boolean');
130 | expect(sono.canPlay.opus).to.be.a('boolean');
131 | expect(sono.canPlay.wav).to.be.a('boolean');
132 | expect(sono.canPlay.m4a).to.be.a('boolean');
133 | });
134 | });
135 |
136 | describe('effects', () => {
137 | it('should have effects module', () => {
138 | expect(sono.effects).to.exist;
139 | });
140 | });
141 |
142 | describe('utils', () => {
143 | it('should have utils module', () => {
144 | expect(sono.utils).to.be.an('object');
145 | });
146 | });
147 |
148 | });
149 |
--------------------------------------------------------------------------------
/test/audio/blip.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/test/audio/blip.mp3
--------------------------------------------------------------------------------
/test/audio/blip.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/test/audio/blip.ogg
--------------------------------------------------------------------------------
/test/audio/bloop.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/test/audio/bloop.mp3
--------------------------------------------------------------------------------
/test/audio/bloop.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/test/audio/bloop.ogg
--------------------------------------------------------------------------------
/test/audio/long.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/test/audio/long.mp3
--------------------------------------------------------------------------------
/test/audio/long.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/test/audio/long.ogg
--------------------------------------------------------------------------------
/test/destroy.spec.js:
--------------------------------------------------------------------------------
1 | describe('Destroy', () => {
2 |
3 | let sound;
4 |
5 | beforeEach(() => {
6 | sono.destroyAll();
7 | });
8 |
9 | it('should have one sound', () => {
10 | sound = sono.createSound({
11 | id: 'sine',
12 | type: 'sine'
13 | });
14 | expect(sono.sounds.length).to.eql(1);
15 | });
16 |
17 | it('should have zero sounds', () => {
18 | sound.destroy();
19 | expect(sono.sounds.length).to.eql(0);
20 | });
21 |
22 | });
23 |
--------------------------------------------------------------------------------
/test/file.spec.js:
--------------------------------------------------------------------------------
1 | describe('file', function() {
2 |
3 | const el = document.createElement('audio');
4 | const file = sono.file;
5 |
6 | it('should get audio type', function() {
7 | expect(file.isAudioBuffer(el)).to.be.false;
8 | expect(file.isMediaElement(el)).to.be.true;
9 | });
10 |
11 | it('should get file extension', function() {
12 | expect(file.getFileExtension).to.be.a('function');
13 | expect(file.getFileExtension('audio/foo.ogg')).to.eql('ogg');
14 | expect(file.getFileExtension('audio/foo.ogg?foo=bar')).to.eql('ogg');
15 | expect(file.getFileExtension('./audio/foo.ogg')).to.eql('ogg');
16 | expect(file.getFileExtension('../audio/foo.ogg')).to.eql('ogg');
17 | expect(file.getFileExtension('../../audio/foo')).to.eql('');
18 | expect(file.getFileExtension('../../audio/foo.ogg')).to.eql('ogg');
19 | expect(file.getFileExtension('http://www.example.com/audio/foo.ogg')).to.eql('ogg');
20 | expect(file.getFileExtension('http://www.example.com/audio/foo.ogg?foo=bar')).to.eql('ogg');
21 | expect(file.getFileExtension('data:audio/ogg;base64,T2dnUwAC')).to.eql('ogg');
22 | expect(file.getFileExtension('data:audio/mp3;base64,T2dnUwAC')).to.eql('mp3');
23 | });
24 |
25 | it('should get file', function() {
26 | expect(file.extensions).to.be.an('array');
27 | expect(file.extensions.length).to.be.at.least(1);
28 | expect(file.getSupportedFile).to.be.a('function');
29 | expect(file.getSupportedFile(['audio/foo.ogg', 'audio/foo.mp3'])).to.be.a('string');
30 | expect(file.getSupportedFile({foo: 'audio/foo.ogg', bar: 'audio/foo.mp3'})).to.be.a('string');
31 | expect(file.getSupportedFile('audio/foo.ogg')).to.be.a('string');
32 | expect(file.getSupportedFile('data:audio/ogg;base64,T2dnUwAC')).to.be.a('string');
33 | });
34 |
35 | it('should have canPlay hash', function() {
36 | expect(file.canPlay).to.be.an('object');
37 | expect(file.canPlay.ogg).to.be.a('boolean');
38 | expect(file.canPlay.mp3).to.be.a('boolean');
39 | expect(file.canPlay.opus).to.be.a('boolean');
40 | expect(file.canPlay.wav).to.be.a('boolean');
41 | expect(file.canPlay.m4a).to.be.a('boolean');
42 | });
43 |
44 | });
45 |
--------------------------------------------------------------------------------
/test/group.spec.js:
--------------------------------------------------------------------------------
1 | describe('Group', () => {
2 |
3 | describe('group', () => {
4 | it('should have expected api', () => {
5 | expect(sono.group).to.be.a('function');
6 | expect(sono.group.length).to.eql(1);
7 | });
8 | it('should return new Group', () => {
9 | const group = sono.group();
10 | expect(group).to.exist;
11 | });
12 | });
13 |
14 | describe('add sound', () => {
15 | let sound;
16 |
17 | beforeEach((done) => {
18 | sono.load({
19 | id: 'foo',
20 | url: [
21 | '/base/test/audio/blip.ogg',
22 | '/base/test/audio/blip.mp3'
23 | ],
24 | onComplete: function(s) {
25 | sound = s;
26 | done();
27 | }
28 | });
29 | });
30 |
31 | afterEach(() => {
32 | sono.destroy(sound.id);
33 | });
34 |
35 | it('should return new Group', () => {
36 | const group = sono.group();
37 | expect(group).to.exist;
38 | group.add(sound);
39 | expect(group.sounds.length).to.eql(1);
40 | });
41 | });
42 |
43 | describe('control', () => {
44 | const group = sono.group();
45 |
46 | it('should have zero volume', () => {
47 | group.volume = 0;
48 | expect(group.volume).to.eql(0);
49 | });
50 |
51 | it('should have 1 volume', () => {
52 | group.volume = 1;
53 | expect(group.volume).to.eql(1);
54 | });
55 | });
56 |
57 | });
58 |
--------------------------------------------------------------------------------
/test/helper.js:
--------------------------------------------------------------------------------
1 | console.log('Running tests with Web Audio API?', !!(window.AudioContext || window.webkitAudioContext));
2 |
--------------------------------------------------------------------------------
/test/is-travis.js:
--------------------------------------------------------------------------------
1 | window.isTravis = true;
2 |
--------------------------------------------------------------------------------
/test/kill-wa.js:
--------------------------------------------------------------------------------
1 | window.AudioContext = window.webkitAudioContext = undefined;
2 | // window.MediaElementAudioSourceNode = Object;
3 | // window.AudioNode = Object;
4 | // window.AudioBuffer = Object;
5 |
--------------------------------------------------------------------------------
/test/playback.spec.js:
--------------------------------------------------------------------------------
1 | describe('sono playback', () => {
2 |
3 | describe('create', () => {
4 | const config = {
5 | id: 'playback-create',
6 | url: [
7 | '/base/test/audio/blip.ogg',
8 | '/base/test/audio/blip.mp3'
9 | ]
10 | };
11 |
12 | let sound;
13 |
14 | beforeEach((done) => {
15 | sound = sono.create(config)
16 | .on('error', (s, err) => console.error('error', err, s))
17 | .on('loaded', () => console.log('loaded'))
18 | .on('ready', () => console.log('ready'))
19 | .on('play', () => console.log('play'))
20 | .on('ended', () => done());
21 | sound.play();
22 | });
23 |
24 | afterEach(() => {
25 | sono.destroy(sound.id);
26 | });
27 |
28 | it('should get ended callback', () => {
29 | expect(sound).to.exist;
30 | });
31 | });
32 |
33 | describe('play and end', () => {
34 | let sound,
35 | ended = false;
36 |
37 | beforeEach((done) => {
38 | function onComplete(loadedSound) {
39 | sound = loadedSound
40 | .on('error', (s, err) => console.error('error', err, s))
41 | .on('loaded', () => console.log('loaded'))
42 | .on('ready', () => console.log('ready'))
43 | .on('play', () => console.log('play'))
44 | .on('ended', () => {
45 | ended = true;
46 | done();
47 | });
48 | sound.play();
49 | }
50 | sono.load({
51 | url: [
52 | '/base/test/audio/blip.ogg',
53 | '/base/test/audio/blip.mp3'
54 | ],
55 | onComplete: onComplete,
56 | onError: function(err) {
57 | console.error(err);
58 | }
59 | });
60 | });
61 |
62 | afterEach(() => {
63 | sono.destroy(sound.id);
64 | });
65 |
66 | it('should get ended callback', () => {
67 | expect(sound).to.exist;
68 | expect(ended).to.be.true;
69 | });
70 | });
71 |
72 | describe('play when ready', () => {
73 | let sound,
74 | ended = false;
75 |
76 | beforeEach((done) => {
77 | sound = sono.create({
78 | url: [
79 | '/base/test/audio/blip.ogg',
80 | '/base/test/audio/blip.mp3'
81 | ]
82 | })
83 | .on('error', (s, err) => console.error('error', err, s))
84 | .on('loaded', () => console.log('loaded'))
85 | .on('ready', () => console.log('ready'))
86 | .on('play', () => console.log('play'))
87 | .on('ended', () => {
88 | ended = true;
89 | done();
90 | })
91 | .play(0.1, 0.1);
92 | });
93 |
94 | afterEach(() => {
95 | sono.destroy(sound);
96 | });
97 |
98 | it('should have played', () => {
99 | expect(sound).to.exist;
100 | expect(ended).to.be.true;
101 | });
102 | });
103 |
104 | describe('currentTime and duration', () => {
105 | let sound = null;
106 | let ended = false;
107 |
108 | beforeEach((done) => {
109 | sound = sono.create({
110 | url: [
111 | '/base/test/audio/blip.ogg',
112 | '/base/test/audio/blip.mp3'
113 | ],
114 | loop: true
115 | })
116 | .on('error', (s, err) => console.error('error', err, s))
117 | .on('loaded', () => console.log('loaded'))
118 | .on('ready', () => console.log('ready'))
119 | .on('play', () => {
120 | window.setTimeout(() => done(), 1000);
121 | })
122 | .on('ended', () => {
123 | ended = true;
124 | })
125 | .play();
126 | });
127 |
128 | afterEach(() => {
129 | sono.destroy(sound);
130 | });
131 |
132 | it('should get duration above 0 and currentTime below duration', () => {
133 | expect(sound).to.exist;
134 | expect(sound.playing).to.be.true;
135 | expect(ended).to.be.false;
136 | expect(sound.currentTime).to.be.a('number');
137 | expect(sound.duration).to.be.a('number');
138 | expect(sound.duration).to.be.above(0);
139 | expect(sound.currentTime).to.be.below(sound.duration + 0.01);
140 | });
141 | });
142 |
143 | });
144 |
--------------------------------------------------------------------------------
/test/seek.spec.js:
--------------------------------------------------------------------------------
1 | describe('seek', () => {
2 |
3 | let sound = null;
4 |
5 | describe('after load', () => {
6 | const config = {
7 | id: 'seek-after',
8 | url: [
9 | '/base/test/audio/long.ogg',
10 | '/base/test/audio/long.mp3'
11 | ]
12 | };
13 | beforeEach((done) => {
14 | sound = sono.create(config)
15 | .on('error', (snd, err) => console.error('error', err, snd))
16 | .on('loaded', () => console.log('loaded'))
17 | .on('ready', () => done());
18 | });
19 |
20 | afterEach(() => {
21 | sono.destroy(sound);
22 | });
23 |
24 | it('should set currentTime to 1', () => {
25 | sound.currentTime = 1;
26 | expect(sound.currentTime).to.eql(1);
27 | });
28 |
29 | it('should set seek to 1', () => {
30 | sound.seek(1);
31 | expect(sound.currentTime).to.eql(1);
32 | });
33 |
34 | it('should set currentTime to 0', () => {
35 | sound.currentTime = 0;
36 | expect(sound.currentTime).to.eql(0);
37 | });
38 |
39 | it('should not auto play after seek', (done) => {
40 | sound.currentTime = 1;
41 | setTimeout(() => {
42 | expect(sound.currentTime).to.eql(1);
43 | expect(sound.playing).to.be.false;
44 | done();
45 | }, 500);
46 | });
47 |
48 | it('should jump to 1 and continue playing', (done) => {
49 | expect(sound.currentTime).to.eql(0);
50 | sound.play();
51 | sound.currentTime = 1;
52 | setTimeout(() => {
53 | expect(sound.playing).to.be.true;
54 | expect(sound.currentTime).to.be.at.least(1);
55 | done();
56 | }, 500);
57 | });
58 | });
59 |
60 | describe('before load', () => {
61 | const config = {
62 | id: 'seek-before',
63 | url: [
64 | '/base/test/audio/long.ogg',
65 | '/base/test/audio/long.mp3'
66 | ],
67 | deferLoad: true
68 | };
69 | beforeEach((done) => {
70 | sound = sono.create(config)
71 | .on('error', (snd, err) => console.error('error', err, snd));
72 | done();
73 | });
74 |
75 | afterEach(() => {
76 | sono.destroy(sound);
77 | });
78 |
79 | it('should set currentTime to 1', () => {
80 | sound.currentTime = 1;
81 | expect(sound.currentTime).to.eql(1);
82 | });
83 |
84 | it('should set seek to 1', () => {
85 | sound.seek(1);
86 | expect(sound.currentTime).to.eql(1);
87 | });
88 |
89 | it('should set currentTime to 0', () => {
90 | sound.currentTime = 0;
91 | expect(sound.currentTime).to.eql(0);
92 | });
93 |
94 | it('should start from seeked time when loaded', (done) => {
95 | sound.currentTime = 1;
96 | sound.on('play', () => {
97 | expect(sound.currentTime).to.be.at.least(1);
98 | done();
99 | });
100 | sound.play();
101 | });
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/test/sound.spec.js:
--------------------------------------------------------------------------------
1 | describe('Sound', () => {
2 | const sound = new sono.__test.Sound({
3 | context: sono.context,
4 | deferLoad: true
5 | });
6 |
7 | it('should have id property', () => {
8 | expect(sound).to.have.property('id');
9 | sound.id = 'some-id';
10 | expect(sound.id).to.eql('some-id');
11 | });
12 |
13 | it('should have data property', () => {
14 | expect(sound).to.have.property('data');
15 | });
16 |
17 | it('should have expected members (controls)', () => {
18 | expect(sound.play).to.be.a('function');
19 | expect(sound.pause).to.be.a('function');
20 | expect(sound.stop).to.be.a('function');
21 | expect(sound.volume).to.be.a('number');
22 | expect(sound.playbackRate).to.be.a('number');
23 | expect(sono.fade).to.be.a('function');
24 | });
25 |
26 | it('should have expected members (state)', () => {
27 | expect(sound.loop).to.be.a('boolean');
28 | expect(sound.duration).to.be.a('number');
29 | expect(sound.currentTime).to.be.a('number');
30 | expect(sound.progress).to.be.a('number');
31 | expect(sound.playing).to.be.a('boolean');
32 | expect(sound.paused).to.be.a('boolean');
33 | });
34 |
35 | it('should have expected members (effects)', () => {
36 | expect(sound.effects).to.exist;
37 | expect(sound.effects.add).to.be.a('function');
38 | expect(sound.effects.remove).to.be.a('function');
39 | expect(sound.effects.toggle).to.be.a('function');
40 | expect(sound.effects.removeAll).to.be.a('function');
41 | });
42 |
43 | it('should have chainable methods', () => {
44 | const a = sound.play()
45 | .pause()
46 | .load()
47 | .stop()
48 | .fade(1)
49 | .play();
50 |
51 | expect(a).to.be.an('object');
52 | expect(a.currentTime).to.be.a('number');
53 | });
54 |
55 | it('should have event emitter', () => {
56 | expect(sound.on).to.be.a('function');
57 | expect(sound.off).to.be.a('function');
58 | expect(sound.once).to.be.a('function');
59 | });
60 |
61 | });
62 |
--------------------------------------------------------------------------------
/test/utils.spec.js:
--------------------------------------------------------------------------------
1 | describe('utils', () => {
2 | const utils = sono.utils;
3 |
4 | describe('buffer', () => {
5 | const expectedAudioBufferType = sono.hasWebAudio ? window.AudioBuffer : Object;
6 | const expectedValue = sono.hasWebAudio ? 1 : 0;
7 | const buffer = sono.context.createBuffer(1, 4096, sono.context.sampleRate);
8 |
9 | it('should clone buffer', () => {
10 | const cloned = utils.cloneBuffer(buffer);
11 | expect(cloned).to.be.an.instanceof(expectedAudioBufferType);
12 | expect(cloned).to.eql(buffer);
13 | });
14 |
15 | it('should reverse buffer', () => {
16 | const data = buffer.getChannelData(0);
17 | data[0] = expectedValue;
18 | expect(data[0]).to.eql(expectedValue);
19 | utils.reverseBuffer(buffer);
20 | expect(data[0]).to.eql(0);
21 | expect(data[data.length - 1]).to.eql(expectedValue);
22 | });
23 | });
24 |
25 | describe('timecode', () => {
26 | it('should format timecode', () => {
27 | expect(utils.timeCode(217.8)).to.eql('03:37');
28 | });
29 | });
30 |
31 | describe('recorder', () => {
32 | it('should have expected api', () => {
33 | expect(utils.recorder).to.be.a('function');
34 | expect(utils.recorder()).to.exist;
35 | });
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/test/volume.spec.js:
--------------------------------------------------------------------------------
1 | describe('volume', () => {
2 |
3 | describe('sound', () => {
4 | const config = {
5 | id: 'volume-test',
6 | url: [
7 | '/base/test/audio/blip.ogg',
8 | '/base/test/audio/blip.mp3'
9 | ],
10 | volume: 0.8
11 | };
12 |
13 | let sound;
14 |
15 | beforeEach((done) => {
16 | sound = sono.create(config)
17 | .on('error', (s, err) => console.error('error', err, s))
18 | .on('loaded', () => console.log('loaded'))
19 | .on('ready', () => console.log('ready'))
20 | .on('play', () => console.log('play'))
21 | .on('ended', () => done());
22 | sound.play();
23 | });
24 |
25 | afterEach(() => {
26 | sono.destroy(sound.id);
27 | });
28 |
29 | it('should get initial volume', () => {
30 | expect(sound).to.exist;
31 | expect(Math.round(sound.volume * 100)).to.eql(80);
32 | });
33 |
34 | it('should change volume', () => {
35 | sound.volume = 0.5;
36 | expect(Math.round(sound.volume * 100)).to.eql(50);
37 | });
38 |
39 | it('should clamp value', () => {
40 | sound.volume = 2;
41 | expect(sound.volume).to.eql(1);
42 | sound.volume = -1;
43 | expect(sound.volume).to.eql(0);
44 | });
45 | });
46 |
47 | });
48 |
--------------------------------------------------------------------------------