├── .gitignore ├── run.sh ├── chunks ├── xa ├── xb ├── xc ├── xd ├── xe ├── xf ├── xg ├── xh ├── xi ├── xj ├── xk ├── xl ├── xm ├── xn ├── xo ├── xp ├── xq ├── xr ├── xs ├── xt ├── xu ├── xv ├── xw ├── xx ├── xy └── .DS_Store ├── README.md ├── LICENSE ├── visualiser.js ├── eventtarget.js ├── index.html └── mp3chunksplayer.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python -m SimpleHTTPServer 8282 3 | -------------------------------------------------------------------------------- /chunks/xa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xa -------------------------------------------------------------------------------- /chunks/xb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xb -------------------------------------------------------------------------------- /chunks/xc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xc -------------------------------------------------------------------------------- /chunks/xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xd -------------------------------------------------------------------------------- /chunks/xe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xe -------------------------------------------------------------------------------- /chunks/xf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xf -------------------------------------------------------------------------------- /chunks/xg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xg -------------------------------------------------------------------------------- /chunks/xh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xh -------------------------------------------------------------------------------- /chunks/xi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xi -------------------------------------------------------------------------------- /chunks/xj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xj -------------------------------------------------------------------------------- /chunks/xk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xk -------------------------------------------------------------------------------- /chunks/xl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xl -------------------------------------------------------------------------------- /chunks/xm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xm -------------------------------------------------------------------------------- /chunks/xn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xn -------------------------------------------------------------------------------- /chunks/xo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xo -------------------------------------------------------------------------------- /chunks/xp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xp -------------------------------------------------------------------------------- /chunks/xq: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xq -------------------------------------------------------------------------------- /chunks/xr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xr -------------------------------------------------------------------------------- /chunks/xs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xs -------------------------------------------------------------------------------- /chunks/xt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xt -------------------------------------------------------------------------------- /chunks/xu: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xu -------------------------------------------------------------------------------- /chunks/xv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xv -------------------------------------------------------------------------------- /chunks/xw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xw -------------------------------------------------------------------------------- /chunks/xx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xx -------------------------------------------------------------------------------- /chunks/xy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/xy -------------------------------------------------------------------------------- /chunks/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/72lions/PlayingChunkedMP3-WebAudioAPI/HEAD/chunks/.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Playing a chunked MP3 with the Web Audio API 2 | ============================= 3 | 4 | Play a chunked MP3 with the Web Audio API without having to wait for all the pieces to be loaded. 5 | 6 | In this proof of concept **(currently only tested and working in Chrome 24+)** I've split the mp3 in 25 parts by using the unix split command. The moment the first part is loaded then the playback starts immediately and it loads the second part. 7 | When the second part is loaded then then I create a new AudioBuffer by combining the old and the new, and I change the buffer of the AudioSourceNode with the new one. At that point I start playing again from the new AudioBuffer. 8 | 9 | A lot of thanks to Paul (@aerotwist) for his suggestions and to Theo for letting me use his awesome track Breathe In. 10 | 11 | #Example 12 | 13 | http://72lions.github.com/PlayingChunkedMP3-WebAudioAPI 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Theodoros Tsiridis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /visualiser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The visualiser class 3 | * 4 | * @constructor 5 | * @class Visualiser 6 | * @param {String} id The id of the canvas element. 7 | */ 8 | var Visualiser = function(id) { 9 | 10 | EventTarget.call(this); 11 | 12 | /** 13 | * The width of the canvas 14 | * 15 | * @private 16 | * @type {Number} 17 | */ 18 | var CANVAS_WIDTH = 1024; 19 | 20 | /** 21 | * The height of the canvas 22 | * 23 | * @private 24 | * @type {Number} 25 | */ 26 | var CANVAS_HEIGHT = 300; 27 | 28 | /** 29 | * The canvas dom element 30 | * 31 | * @private 32 | * @type {DOMElement} 33 | */ 34 | var _canvas; 35 | 36 | /** 37 | * The context of the canvas element 38 | * 39 | * @private 40 | * @type {CanvasRenderingContext2D} 41 | */ 42 | var _ctx; 43 | 44 | /** 45 | * The gradient 46 | * 47 | * @type {CanvasGradient} 48 | */ 49 | var _gradient; 50 | 51 | /** 52 | * Draws on the canvas based on the data 53 | * 54 | * @param {Array} data An array containing all the data. 55 | * @return {Visualiser} Returns a reference to this instance. 56 | */ 57 | this.draw = function(data) { 58 | _ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 59 | _ctx.fillStyle = _gradient; 60 | for (var i = 0, len = data.length; i < len; i += 1) { 61 | var value = data[i]; 62 | _ctx.fillRect(i * 3, CANVAS_HEIGHT - value, 1, CANVAS_HEIGHT); 63 | } 64 | 65 | return this; 66 | }; 67 | 68 | /** 69 | * Initializes the class by loading the first chunk 70 | * 71 | * @return {Visualiser} Returns a reference to this instance. 72 | */ 73 | this.init = function() { 74 | console.log('Visualiser initialized...'); 75 | 76 | _canvas = document.getElementById(id); 77 | _canvas.width = CANVAS_WIDTH; 78 | _canvas.height = CANVAS_HEIGHT; 79 | _ctx = _canvas.getContext('2d'); 80 | 81 | _gradient = _ctx.createLinearGradient(0, 0, 0, 300); 82 | 83 | _gradient.addColorStop(1, '#ff0000'); 84 | _gradient.addColorStop(0.75, '#ff0000'); 85 | _gradient.addColorStop(0.25, '#ff0000'); 86 | _gradient.addColorStop(0, '#f3f3f3'); 87 | 88 | return this; 89 | }; 90 | }; 91 | -------------------------------------------------------------------------------- /eventtarget.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Event target is used as a mixin so that the classes can 3 | * support dispatch events and add events commands 4 | * 5 | * @class EventTarget 6 | * @constructor 7 | * 8 | */ 9 | var EventTarget = (function() { 10 | 11 | /** 12 | * The object that will hold all the event listeners 13 | * 14 | * @private 15 | * @type {Object} 16 | */ 17 | this._listeners = {}; 18 | 19 | /** 20 | * Registers an event 21 | * 22 | * @param {String} type The event type. 23 | * @param {Function} listener The callback function. 24 | * @param {Object} ctx The context. 25 | * @param {Number} priority The priority. 26 | */ 27 | this.bind = function(type, listener, ctx, priority) { 28 | 29 | if (typeof priority === 'undefined') { 30 | priority = 0; 31 | } 32 | 33 | if (typeof listener !== 'undefined' && listener !== null) { 34 | 35 | var obj = {callback: listener, context: ctx, priority: priority}; 36 | var exists = false; 37 | var events; 38 | 39 | if (this._listeners[type] === undefined) { 40 | this._listeners[type] = []; 41 | } 42 | 43 | events = this._listeners[type]; 44 | 45 | for (var i = 0; i < events.length; i++) { 46 | 47 | if (events[i].callback === listener && events[i].context === ctx) { 48 | exists = true; 49 | break; 50 | } 51 | 52 | } 53 | 54 | if (exists === false) { 55 | this._listeners[type].push(obj); 56 | this._listeners[type].sort(_sortByPriorityDesc); 57 | } 58 | 59 | } 60 | 61 | }; 62 | 63 | /** 64 | * Custom sorting function based on priority descending 65 | * 66 | * @private 67 | * @function 68 | * @param {Object} a The first element to compare each priority. 69 | * @param {Object} b The second element to compare each priority. 70 | * @return {Number} The result of the comparison. 71 | */ 72 | var _sortByPriorityDesc = function(a, b) { 73 | return (b.priority - a.priority); //causes an array to be sorted numerically and ascending 74 | }; 75 | 76 | /** 77 | * Dispatches an event 78 | * 79 | * @param {String} type The event type. 80 | * @param {Object} params The object. 81 | * @param {Object} extra Any extra arguments. 82 | */ 83 | this.trigger = function(type, params, extra) { 84 | var events = this._listeners[type]; 85 | params = params || {}; 86 | if (typeof events !== 'undefined') { 87 | for (var i = 0; i < events.length; i++) { 88 | var event = events[i]; 89 | event.callback.call(event.context, {type: type, params: params, extra: extra || {}}); 90 | } 91 | } 92 | 93 | }; 94 | 95 | /** 96 | * Removes an event 97 | * 98 | * @param {String} type The event type. 99 | * @param {Function} listener The callback function. 100 | * @param {Object} ctx The context. 101 | * 102 | */ 103 | this.unbind = function(type, listener, ctx) { 104 | var index = -1; 105 | var events = this._listeners[type]; 106 | if (typeof listener !== 'undefined') { 107 | if (typeof events !== 'undefined') { 108 | for (var i = 0; i < events.length; i++) { 109 | if (events[i].callback === listener && events[i].context === ctx) { 110 | index = i; 111 | break; 112 | } 113 | } 114 | 115 | if (index !== - 1) { 116 | this._listeners[type].splice(index, 1); 117 | } 118 | } 119 | } else { 120 | this._listeners[type] = []; 121 | } 122 | }; 123 | 124 | }); 125 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Web Audio API - Playing a chunked mp3 file 6 | 55 | 56 | 57 | 58 |
59 |

60 | In this proof of concept (currently only tested and working in Chrome 24+) I've split the mp3 in 25 parts by using the unix split command. 61 | The moment the first part is loaded then the playback starts immediately and it loads the second part. 62 | When the second part is loaded then I create a new AudioBuffer by combining the old and the new, 63 | and I change the buffer of the AudioSourceNode with the new one. At that point I start playing again 64 | from the new AudioBuffer. 65 |

66 | A lot of thanks to Paul (@aerotwist) for his suggestions and to Theo for letting me use his awesome track Breathe In. 67 |

68 |

Download source or see the progject on Github

69 | Debug: 70 |
71 |

 72 |   
 73 |   
 74 |   
 75 |   
103 |   
116 | 
117 | 
118 | 


--------------------------------------------------------------------------------
/mp3chunksplayer.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * The MP3ChunksPlayer class
  3 |  *
  4 |  * @constructor
  5 |  * @class MP3ChunksPlayer
  6 |  */
  7 | var MP3ChunksPlayer = function() {
  8 | 
  9 |   EventTarget.call(this);
 10 | 
 11 |   /**
 12 |    * A reference to this specific instance.
 13 |    *
 14 |    *@private
 15 |    * @type {MP3ChunksPlayer}
 16 |    */
 17 |   var _self = this;
 18 | 
 19 |   /**
 20 |    * The ArrayBuffer that will have the new chunks appended
 21 |    *
 22 |    * @private
 23 |    * @type {ArrayBuffer}
 24 |    */
 25 |   var _activeBuffer;
 26 | 
 27 |   /**
 28 |    * The counter that holds the how many chunks we have loaded
 29 |    *
 30 |    * @private
 31 |    * @type {Number}
 32 |    */
 33 |   var _totalChunksLoaded = 0;
 34 | 
 35 |   /**
 36 |    * An array with all the individual files
 37 |    * @private
 38 |    * @type {Array}
 39 |    */
 40 |   var _files = [
 41 |     'xa',
 42 |     'xb',
 43 |     'xc',
 44 |     'xd',
 45 |     'xe',
 46 |     'xf',
 47 |     'xg',
 48 |     'xh',
 49 |     'xi',
 50 |     'xj',
 51 |     'xk',
 52 |     'xl',
 53 |     'xm',
 54 |     'xn',
 55 |     'xo',
 56 |     'xp',
 57 |     'xq',
 58 |     'xr',
 59 |     'xs',
 60 |     'xt',
 61 |     'xu',
 62 |     'xv',
 63 |     'xw',
 64 |     'xx',
 65 |     'xy'];
 66 | 
 67 |   /**
 68 |    * The AudioContext
 69 |    *
 70 |    * @private
 71 |    * @type {AudioContext}
 72 |    */
 73 |   var _context;
 74 | 
 75 |   /**
 76 |    * The audio buffer
 77 |    *
 78 |    * @private
 79 |    * @type {AudioBuffer}
 80 |    */
 81 |   var _audioBuffer;
 82 | 
 83 |   /**
 84 |    * The audio source is responsible for playing the music
 85 |    *
 86 |    * @private
 87 |    * @type {AudioBufferSourceNode}
 88 |    */
 89 |   var _audioSource;
 90 | 
 91 |   /**
 92 |    * The analyser
 93 |    *
 94 |    * @private
 95 |    * @type {AnalyserNode}
 96 |    */
 97 |   var _analyser;
 98 | 
 99 |   /**
100 |    * It is responsible for loading all the different chunks
101 |    *
102 |    * @private
103 |    * @type {XMLHttpRequest}
104 |    */
105 |   var _request = new XMLHttpRequest();
106 | 
107 |   /**
108 |    * Creates a new Uint8Array based on two different ArrayBuffers
109 |    *
110 |    * @private
111 |    * @param {ArrayBuffers} buffer1 The first buffer.
112 |    * @param {ArrayBuffers} buffer2 The second buffer.
113 |    * @return {ArrayBuffers} The new ArrayBuffer created out of the two.
114 |    */
115 |   var _appendBuffer = function(buffer1, buffer2) {
116 |     var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
117 |     var buff1 = new Uint8Array(buffer1);
118 |     var buff2 = new Uint8Array(buffer2);
119 |     tmp.set(buff1, 0);
120 |     tmp.set(buff2, buffer1.byteLength);
121 |     return tmp.buffer;
122 |   };
123 | 
124 |   /**
125 |    * Instantiates all the WebAudio items
126 |    *
127 |    * @private
128 |    */
129 |   var _initializeWebAudio = function() {
130 |     _context = new AudioContext();
131 |     _analyser = _context.createAnalyser();
132 |     _analyser.fftSize = 2048;
133 |   };
134 | 
135 |   /**
136 |    * Plays the music from the point that it currently is.
137 |    *
138 |    * @private
139 |    */
140 |   var _play = function() {
141 |     // Adding a bit of  scheduling so that we won't have single digit milisecond overlaps.
142 |     // Thanks to Chris Wilson for his suggestion.
143 |     var scheduledTime = 0.015;
144 | 
145 |     try {
146 |       _audioSource.stop(scheduledTime);
147 |     } catch (e) {}
148 | 
149 |     _audioSource = _context.createBufferSource();
150 |     _audioSource.buffer = _audioBuffer;
151 |     _audioSource.connect(_analyser);
152 |     _audioSource.connect(_context.destination);
153 |     var currentTime = _context.currentTime + 0.010 || 0;
154 |     _audioSource.start(scheduledTime - 0.005, currentTime, _audioBuffer.duration - currentTime);
155 |     _audioSource.playbackRate.value = 1;
156 |     _self.trigger('message', ['AudioBuffer is replaced!']);
157 |   };
158 | 
159 |   /**
160 |    * Is triggered when a new chunk is loaded and makes sure to add the
161 |    * new chunk to a new ArrayBuffer.
162 |    *
163 |    * @private
164 |    */
165 |   var _onChunkLoaded = function() {
166 |     console.log('Chunk loaded!');
167 |     _self.trigger('message', ['Chunk loaded!']);
168 |     if (_totalChunksLoaded === 0) {
169 |       _initializeWebAudio();
170 |       _activeBuffer = _request.response;
171 |     } else {
172 |       _self.trigger('message', ['Chunk is appended!']);
173 |       _activeBuffer = _appendBuffer(_activeBuffer, _request.response);
174 |     }
175 | 
176 |     // Use decodeAudioData so that we don't block the main thread.
177 |     _context.decodeAudioData(_activeBuffer.slice(0), function(buf) {
178 |       _self.trigger('message', ['AudioData decoded!']);
179 |       _audioBuffer = buf;
180 |       _play();
181 |     });
182 | 
183 |     // If this is the first chunk then trigger play
184 |     if (_totalChunksLoaded === 0) {
185 |       _self.trigger('play');
186 |     }
187 | 
188 |     _totalChunksLoaded++;
189 |     if (_totalChunksLoaded < _files.length) {
190 |       setTimeout(function() {
191 |         _loadChunk(_totalChunksLoaded);
192 |       }, 3000);
193 |     }
194 |   };
195 | 
196 |   /**
197 |    * Loads a specific chunk based on an index in an array.
198 |    *
199 |    * @private
200 |    * @param  {Number} index The index of the chunk in the files array.
201 |    */
202 |   var _loadChunk = function(index) {
203 |     _self.trigger('message', ['Loading chunk', _files[index], '...']);
204 |     _request.open('GET', 'chunks/' + _files[index], true);
205 |     _request.send();
206 |   };
207 | 
208 |   /**
209 |    * It gets the visualisation data
210 |    *
211 |    * @return {Uint8Array} The array that holds the visualisation data.
212 |    */
213 |   this.getVisualisationData = function() {
214 |     // get the average for the first channel
215 |     var array = new Uint8Array(_analyser.frequencyBinCount);
216 |     _analyser.getByteFrequencyData(array);
217 | 
218 |     return array;
219 | 
220 |   };
221 | 
222 |   /**
223 |    * Initializes the class by loading the first chunk
224 |    *
225 |    * @return {MP3ChunksPlayer} Returns a reference to this instance.
226 |    */
227 |   this.init = function() {
228 |     console.log('MP3ChunksPlayer initialized!');
229 | 
230 |     _request.responseType = 'arraybuffer';
231 |     _request.addEventListener('load', _onChunkLoaded, false);
232 | 
233 |     _loadChunk(_totalChunksLoaded);
234 | 
235 |     return this;
236 |   };
237 | };
238 | 


--------------------------------------------------------------------------------