1 | /*
|
---|
2 | ----------------------------------------------------------
|
---|
3 | Web Audio API - OGG or MPEG Soundbank
|
---|
4 | ----------------------------------------------------------
|
---|
5 | http://webaudio.github.io/web-audio-api/
|
---|
6 | ----------------------------------------------------------
|
---|
7 | */
|
---|
8 |
|
---|
9 | (function(root) { 'use strict';
|
---|
10 |
|
---|
11 | window.AudioContext && (function() {
|
---|
12 | var audioContext = null; // new AudioContext();
|
---|
13 | var useStreamingBuffer = false; // !!audioContext.createMediaElementSource;
|
---|
14 | var midi = root.WebAudio = {api: 'webaudio'};
|
---|
15 | var ctx; // audio context
|
---|
16 | var sources = {};
|
---|
17 | var effects = {};
|
---|
18 | var masterVolume = 127;
|
---|
19 | var audioBuffers = {};
|
---|
20 | ///
|
---|
21 | midi.audioBuffers = audioBuffers;
|
---|
22 | midi.send = function(data, delay) { };
|
---|
23 | midi.setController = function(channelId, type, value, delay) { };
|
---|
24 |
|
---|
25 | midi.setVolume = function(channelId, volume, delay) {
|
---|
26 | if (delay) {
|
---|
27 | setTimeout(function() {
|
---|
28 | masterVolume = volume;
|
---|
29 | }, delay * 1000);
|
---|
30 | } else {
|
---|
31 | masterVolume = volume;
|
---|
32 | }
|
---|
33 | };
|
---|
34 |
|
---|
35 | midi.programChange = function(channelId, program, delay) {
|
---|
36 | // if (delay) {
|
---|
37 | // return setTimeout(function() {
|
---|
38 | // var channel = root.channels[channelId];
|
---|
39 | // channel.instrument = program;
|
---|
40 | // }, delay);
|
---|
41 | // } else {
|
---|
42 | var channel = root.channels[channelId];
|
---|
43 | channel.instrument = program;
|
---|
44 | // }
|
---|
45 | };
|
---|
46 |
|
---|
47 | midi.pitchBend = function(channelId, program, delay) {
|
---|
48 | // if (delay) {
|
---|
49 | // setTimeout(function() {
|
---|
50 | // var channel = root.channels[channelId];
|
---|
51 | // channel.pitchBend = program;
|
---|
52 | // }, delay);
|
---|
53 | // } else {
|
---|
54 | var channel = root.channels[channelId];
|
---|
55 | channel.pitchBend = program;
|
---|
56 | // }
|
---|
57 | };
|
---|
58 |
|
---|
59 | midi.noteOn = function(channelId, noteId, velocity, delay) {
|
---|
60 | delay = delay || 0;
|
---|
61 |
|
---|
62 | /// check whether the note exists
|
---|
63 | var channel = root.channels[channelId];
|
---|
64 | var instrument = channel.instrument;
|
---|
65 | var bufferId = instrument + '' + noteId;
|
---|
66 | var buffer = audioBuffers[bufferId];
|
---|
67 | if (!buffer) {
|
---|
68 | // console.log(MIDI.GM.byId[instrument].id, instrument, channelId);
|
---|
69 | return;
|
---|
70 | }
|
---|
71 |
|
---|
72 | /// convert relative delay to absolute delay
|
---|
73 | if (delay < ctx.currentTime) {
|
---|
74 | delay += ctx.currentTime;
|
---|
75 | }
|
---|
76 |
|
---|
77 | /// create audio buffer
|
---|
78 | if (useStreamingBuffer) {
|
---|
79 | var source = ctx.createMediaElementSource(buffer);
|
---|
80 | } else { // XMLHTTP buffer
|
---|
81 | var source = ctx.createBufferSource();
|
---|
82 | source.buffer = buffer;
|
---|
83 | }
|
---|
84 |
|
---|
85 | /// add effects to buffer
|
---|
86 | if (effects) {
|
---|
87 | var chain = source;
|
---|
88 | for (var key in effects) {
|
---|
89 | chain.connect(effects[key].input);
|
---|
90 | chain = effects[key];
|
---|
91 | }
|
---|
92 | }
|
---|
93 |
|
---|
94 | /// add gain + pitchShift
|
---|
95 | var gain = (velocity / 127) * (masterVolume / 127) * 2 - 1;
|
---|
96 | source.connect(ctx.destination);
|
---|
97 | source.playbackRate.value = 1; // pitch shift
|
---|
98 | source.gainNode = ctx.createGain(); // gain
|
---|
99 | source.gainNode.connect(ctx.destination);
|
---|
100 | source.gainNode.gain.value = Math.min(1.0, Math.max(-1.0, gain));
|
---|
101 | source.connect(source.gainNode);
|
---|
102 | ///
|
---|
103 | if (useStreamingBuffer) {
|
---|
104 | if (delay) {
|
---|
105 | return setTimeout(function() {
|
---|
106 | buffer.currentTime = 0;
|
---|
107 | buffer.play()
|
---|
108 | }, delay * 1000);
|
---|
109 | } else {
|
---|
110 | buffer.currentTime = 0;
|
---|
111 | buffer.play()
|
---|
112 | }
|
---|
113 | } else {
|
---|
114 | source.start(delay || 0);
|
---|
115 | }
|
---|
116 | ///
|
---|
117 | sources[channelId + '' + noteId] = source;
|
---|
118 | ///
|
---|
119 | return source;
|
---|
120 | };
|
---|
121 |
|
---|
122 | midi.noteOff = function(channelId, noteId, delay) {
|
---|
123 | delay = delay || 0;
|
---|
124 |
|
---|
125 | /// check whether the note exists
|
---|
126 | var channel = root.channels[channelId];
|
---|
127 | var instrument = channel.instrument;
|
---|
128 | var bufferId = instrument + '' + noteId;
|
---|
129 | var buffer = audioBuffers[bufferId];
|
---|
130 | if (buffer) {
|
---|
131 | if (delay < ctx.currentTime) {
|
---|
132 | delay += ctx.currentTime;
|
---|
133 | }
|
---|
134 | ///
|
---|
135 | var source = sources[channelId + '' + noteId];
|
---|
136 | if (source) {
|
---|
137 | if (source.gainNode) {
|
---|
138 | // @Miranet: 'the values of 0.2 and 0.3 could of course be used as
|
---|
139 | // a 'release' parameter for ADSR like time settings.'
|
---|
140 | // add { 'metadata': { release: 0.3 } } to soundfont files
|
---|
141 | var gain = source.gainNode.gain;
|
---|
142 | gain.linearRampToValueAtTime(gain.value, delay);
|
---|
143 | gain.linearRampToValueAtTime(-1.0, delay + 0.3);
|
---|
144 | }
|
---|
145 | ///
|
---|
146 | if (useStreamingBuffer) {
|
---|
147 | if (delay) {
|
---|
148 | setTimeout(function() {
|
---|
149 | buffer.pause();
|
---|
150 | }, delay * 1000);
|
---|
151 | } else {
|
---|
152 | buffer.pause();
|
---|
153 | }
|
---|
154 | } else {
|
---|
155 | if (source.noteOff) {
|
---|
156 | source.noteOff(delay + 0.5);
|
---|
157 | } else {
|
---|
158 | source.stop(delay + 0.5);
|
---|
159 | }
|
---|
160 | }
|
---|
161 | ///
|
---|
162 | delete sources[channelId + '' + noteId];
|
---|
163 | ///
|
---|
164 | return source;
|
---|
165 | }
|
---|
166 | }
|
---|
167 | };
|
---|
168 |
|
---|
169 | midi.chordOn = function(channel, chord, velocity, delay) {
|
---|
170 | var res = {};
|
---|
171 | for (var n = 0, note, len = chord.length; n < len; n++) {
|
---|
172 | res[note = chord[n]] = midi.noteOn(channel, note, velocity, delay);
|
---|
173 | }
|
---|
174 | return res;
|
---|
175 | };
|
---|
176 |
|
---|
177 | midi.chordOff = function(channel, chord, delay) {
|
---|
178 | var res = {};
|
---|
179 | for (var n = 0, note, len = chord.length; n < len; n++) {
|
---|
180 | res[note = chord[n]] = midi.noteOff(channel, note, delay);
|
---|
181 | }
|
---|
182 | return res;
|
---|
183 | };
|
---|
184 |
|
---|
185 | midi.stopAllNotes = function() {
|
---|
186 | for (var sid in sources) {
|
---|
187 | var delay = 0;
|
---|
188 | if (delay < ctx.currentTime) {
|
---|
189 | delay += ctx.currentTime;
|
---|
190 | }
|
---|
191 | var source = sources[sid];
|
---|
192 | source.gain.linearRampToValueAtTime(1, delay);
|
---|
193 | source.gain.linearRampToValueAtTime(0, delay + 0.3);
|
---|
194 | if (source.noteOff) { // old api
|
---|
195 | source.noteOff(delay + 0.3);
|
---|
196 | } else { // new api
|
---|
197 | source.stop(delay + 0.3);
|
---|
198 | }
|
---|
199 | delete sources[sid];
|
---|
200 | }
|
---|
201 | };
|
---|
202 |
|
---|
203 | midi.setEffects = function(list) {
|
---|
204 | if (ctx.tunajs) {
|
---|
205 | for (var n = 0; n < list.length; n ++) {
|
---|
206 | var data = list[n];
|
---|
207 | var effect = new ctx.tunajs[data.type](data);
|
---|
208 | effect.connect(ctx.destination);
|
---|
209 | effects[data.type] = effect;
|
---|
210 | }
|
---|
211 | } else {
|
---|
212 | return console.log('Effects module not installed.');
|
---|
213 | }
|
---|
214 | };
|
---|
215 |
|
---|
216 | midi.connect = function(opts) {
|
---|
217 | root.setDefaultPlugin(midi);
|
---|
218 | midi.setContext(ctx || createAudioContext(), opts.onsuccess);
|
---|
219 | };
|
---|
220 |
|
---|
221 | midi.getContext = function() {
|
---|
222 | return ctx;
|
---|
223 | };
|
---|
224 |
|
---|
225 | midi.setContext = function(newCtx, onload, onprogress, onerror) {
|
---|
226 | ctx = newCtx;
|
---|
227 |
|
---|
228 | /// tuna.js effects module - https://github.com/Dinahmoe/tuna
|
---|
229 | if (typeof Tuna !== 'undefined' && !ctx.tunajs) {
|
---|
230 | ctx.tunajs = new Tuna(ctx);
|
---|
231 | }
|
---|
232 |
|
---|
233 | /// loading audio files
|
---|
234 | var urls = [];
|
---|
235 | var notes = root.keyToNote;
|
---|
236 | for (var key in notes) urls.push(key);
|
---|
237 | ///
|
---|
238 | var waitForEnd = function(instrument) {
|
---|
239 | for (var key in bufferPending) { // has pending items
|
---|
240 | if (bufferPending[key]) return;
|
---|
241 | }
|
---|
242 | ///
|
---|
243 | if (onload) { // run onload once
|
---|
244 | onload();
|
---|
245 | onload = null;
|
---|
246 | }
|
---|
247 | };
|
---|
248 | ///
|
---|
249 | var requestAudio = function(soundfont, instrumentId, index, key) {
|
---|
250 | var url = soundfont[key];
|
---|
251 | if (url) {
|
---|
252 | bufferPending[instrumentId] ++;
|
---|
253 | loadAudio(url, function(buffer) {
|
---|
254 | buffer.id = key;
|
---|
255 | var noteId = root.keyToNote[key];
|
---|
256 | audioBuffers[instrumentId + '' + noteId] = buffer;
|
---|
257 | ///
|
---|
258 | if (-- bufferPending[instrumentId] === 0) {
|
---|
259 | var percent = index / 87;
|
---|
260 | // console.log(MIDI.GM.byId[instrumentId], 'processing: ', percent);
|
---|
261 | soundfont.isLoaded = true;
|
---|
262 | waitForEnd(instrument);
|
---|
263 | }
|
---|
264 | }, function(err) {
|
---|
265 | // console.log(err);
|
---|
266 | });
|
---|
267 | }
|
---|
268 | };
|
---|
269 | ///
|
---|
270 | var bufferPending = {};
|
---|
271 | for (var instrument in root.Soundfont) {
|
---|
272 | var soundfont = root.Soundfont[instrument];
|
---|
273 | if (soundfont.isLoaded) {
|
---|
274 | continue;
|
---|
275 | }
|
---|
276 | ///
|
---|
277 | var synth = root.GM.byName[instrument];
|
---|
278 | var instrumentId = synth.number;
|
---|
279 | ///
|
---|
280 | bufferPending[instrumentId] = 0;
|
---|
281 | ///
|
---|
282 | for (var index = 0; index < urls.length; index++) {
|
---|
283 | var key = urls[index];
|
---|
284 | requestAudio(soundfont, instrumentId, index, key);
|
---|
285 | }
|
---|
286 | }
|
---|
287 | ///
|
---|
288 | setTimeout(waitForEnd, 1);
|
---|
289 | };
|
---|
290 |
|
---|
291 | /* Load audio file: streaming | base64 | arraybuffer
|
---|
292 | ---------------------------------------------------------------------- */
|
---|
293 | function loadAudio(url, onload, onerror) {
|
---|
294 | if (useStreamingBuffer) {
|
---|
295 | var audio = new Audio();
|
---|
296 | audio.src = url;
|
---|
297 | audio.controls = false;
|
---|
298 | audio.autoplay = false;
|
---|
299 | audio.preload = false;
|
---|
300 | audio.addEventListener('canplay', function() {
|
---|
301 | onload && onload(audio);
|
---|
302 | });
|
---|
303 | audio.addEventListener('error', function(err) {
|
---|
304 | onerror && onerror(err);
|
---|
305 | });
|
---|
306 | document.body.appendChild(audio);
|
---|
307 | } else if (url.indexOf('data:audio') === 0) { // Base64 string
|
---|
308 | var base64 = url.split(',')[1];
|
---|
309 | var buffer = Base64Binary.decodeArrayBuffer(base64);
|
---|
310 | ctx.decodeAudioData(buffer, onload, onerror);
|
---|
311 | } else { // XMLHTTP buffer
|
---|
312 | var request = new XMLHttpRequest();
|
---|
313 | request.open('GET', url, true);
|
---|
314 | request.responseType = 'arraybuffer';
|
---|
315 | request.onload = function() {
|
---|
316 | ctx.decodeAudioData(request.response, onload, onerror);
|
---|
317 | };
|
---|
318 | request.send();
|
---|
319 | }
|
---|
320 | };
|
---|
321 |
|
---|
322 | function createAudioContext() {
|
---|
323 | return new (window.AudioContext || window.webkitAudioContext)();
|
---|
324 | };
|
---|
325 | })();
|
---|
326 | })(MIDI); |
---|