├── .gitignore ├── .gitmodules ├── README.md ├── css └── style.css ├── experiments ├── application.css ├── experiment.js ├── images │ └── noise.png └── index.html ├── images ├── down.png ├── equal.png ├── github-banner.png ├── github.png ├── header.png ├── logo.png ├── mhd.png ├── nopicture.png ├── ofm.png ├── pause.png ├── play.png └── up.png ├── index.html ├── mhd.js ├── officialfm.js ├── scripts ├── jquery.js ├── madcat.sh ├── sink.min.js └── uglify ├── src ├── arraybuffer │ ├── ajaxstream.js │ ├── arraystream.js │ ├── bytestream.js │ ├── filestream.js │ └── substream.js ├── binaryajax.js ├── binarystring │ ├── ajaxstream.js │ ├── bytestream.js │ ├── filestream.js │ ├── stringstream.js │ └── substream.js ├── bit.js ├── frame.js ├── huffman.js ├── id3v22stream.js ├── id3v23stream.js ├── imdct_s.js ├── layer3.js ├── mad.js ├── mp3file.js ├── player.js ├── rq_table.js ├── stream.js └── synth.js └── tests └── node ├── aliasreduce.js ├── check.rb ├── dct.js ├── fastdsct.js ├── full_frame.js ├── graph.svg ├── huffdecode.js ├── imdct.js ├── one_second_beep.mp3 ├── one_second_of_silence.mp3 ├── out.raw ├── output.js ├── pcm.png ├── plot-pcm.gnuplot ├── plot-sample.gnuplot ├── reorder.js ├── sample.png ├── sample.svg ├── scalefac.js └── typed-array.js /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .*.swp 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "audiolib.js"] 2 | path = audiolib.js 3 | url = git://github.com/jussi-kalliokoski/audiolib.js.git 4 | [submodule "sink.js"] 5 | path = sink.js 6 | url = https://github.com/jussi-kalliokoski/sink.js.git 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Deprecated! Please use [mp3.js](https://github.com/audiocogs/mp3.js) and [aurora.js](https://github.com/audiocogs/aurora.js) instead. 2 | 3 | ## About 4 | 5 | jsmad is a pure javascript MP3 decoder, based on [libmad](http://www.underbit.com/products/mad/), with an ID3 decoder written from scratch. 6 | 7 | For example, jsmad allows Firefox 4.0+ to play MP3s without any Flash. Faster loading times. Fewer security holes. No 64-bit headaches on Linux. Fewer memory leaks. 8 | 9 | jsmad opens up a whole world of realtime audio applications implemented in javascript: 10 | dj-mixers, samplers, sequencers, all these applications benefit from using mp3s as audio source. 11 | 12 | jsmad is released under the GPLv2 license. 13 | 14 | ## Demo 15 | 16 | See a live demo here: http://jsmad.org/ in collaboration with [official.fm](http://official.fm/) and using the [musicmetric](http://musicmetric.com/) API 17 | 18 | It works out of the box under Firefox 4.0 and above. On Chrome 13.0+, you have to enable manually the Web Audio API in 'about:flags', then restart the browser and it should work fine! No Opera support at the moment. 19 | 20 | ## Authors 21 | 22 | * [@nddrylliog](http://twitter.com/nddrylliog) - lead developer 23 | * [@jensnockert](http://twitter.com/jensnockert) - helped porting & debugging the code at MusicHackDay Berlin 24 | * [@mgeorgi](http://twitter.com/mgeorgi) - helped debugging the code after MusicHackDay Berlin 25 | 26 | Special thanks to [@antoinem](http://twitter.com/antoinem) for the Demo design and particularly to [@_romac](http://twitter.com/_romac) for adding features & keeping the demo server alive! 27 | 28 | ## Porting notes 29 | 30 | Obviously, porting low-level C code to Javascript isn't an easy task. Some things had to be 31 | adapted pretty heavily. jsmad is not the result of an automatic translation - all 15K+ lines 32 | of code were translated by hand by @nddrylliog and @jensnockert during MusicHackDay Berlin. 33 | Then, @mgeorgi helped us a lot with the debugging process, and @antoinem did the design of the demo 34 | during MusicHackDay Barcelona. 35 | 36 | It performs well enough to decode and play MP3s in realtime on Firefox on modern computers, 37 | although if you do lots of things at once, Firefox might forget at all about scheduled tasks 38 | and let the soundcard underflow. There is a rescue mechanism for that in the demo, which works 39 | most of the time. 40 | 41 | jsmad will undoubtedly be an interesting benchmark for browser's javascript implementations. 42 | We would love to get feedback from the Mozilla, Google Chrome, and Opera team - shoot us a note! 43 | 44 | ## Accuracy 45 | 46 | The output from jsmad is NOT representative of the output quality of libmad itself. jsmad hasn't been 47 | tested for ISO/IEC 11172-4 computational accuracy requirements compliance. JavaScript number crunching 48 | has always been a bad idea, and we're aware of that - we've done it to push the limits of what is being 49 | done with JavaScript, much in the spirit of [pdf.js](https://github.com/andreasgal/pdf.js) 50 | 51 | ## License 52 | 53 | jsmad is available under the terms of the GNU General Public License, Version 2. Please note that 54 | under the GPL, there is absolutely no warranty of any kind, to the extent permitted by the law. 55 | 56 | What GPL means for a javascript library is not exactly clear to us. What's clear is that you have 57 | to release any fork/changes under the GPL as well, so that everyone can profit from it. However, 58 | using it on a commercial platform is probably alright. Remember: no guarantees, and we'd love to know 59 | about it! 60 | 61 | libmad has commercial license. As for jsmad, we're in a sort of grey legal area. If you're from 62 | Underbit and want to work this out with us please drop me a note at amos@official.fm 63 | 64 | ## Future 65 | 66 | What's next? A few things: 67 | 68 | - Strings are still used in the core decoding routines instead of Uint8Arrays - this should change 69 | - Optimizations, always 70 | - Better buffering strategy - player.js is still pretty naive and we stumble now and then onto buffer underflow 71 | - MPEG Layer I and II are not supported, only Layer III is - it should be pretty trivial but we had no interest for it in the first place. 72 | - MPEG 2.5 is not supported. 73 | - Free bitrate streams are not supported (this is different from VBR - VBR is supported) 74 | - Most of ID3v2.2 and ID3v2.3 are implemented, but some tags are mising. 75 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica; 3 | margin:0; 4 | padding:0; 5 | background:#fa5945 url(../images/header.png) repeat-x top left; 6 | } 7 | #container { 8 | width:960px; 9 | margin:0 auto; 10 | } 11 | 12 | h1 { 13 | background:transparent url(../images/logo.png) no-repeat top left; 14 | width: 157px; 15 | height: 180px; 16 | display: block; 17 | float:left; 18 | margin:20px 40px 20px 20px; 19 | } 20 | h1 span { display:none; } 21 | 22 | h2 { font-size: 30px; line-height: 38px; margin:0; } 23 | h3 { font-size: 22px; margin: 10px 0 20px; } 24 | 25 | h3 small { font-size: 14px; color:#FA5945 !important; font-weight:normal !important; margin-left:10px; } 26 | 27 | a, a:active { color:#490CCE; outline:0;} 28 | a:hover { color:#0A28AC; } 29 | 30 | div.github { 31 | -moz-transform: rotate(2deg); 32 | -moz-transform-origin: left top; 33 | background: url(../images/github.png) no-repeat 16px 10px rgba(255, 255, 255, 0.5); 34 | box-shadow: 0 0 27px rgba(0, 0, 0, 0.08); 35 | float: right; 36 | padding: 30px 30px 30px 91px; 37 | position: relative; 38 | top: -3px; 39 | } 40 | 41 | div.select { 42 | top: 66px; 43 | clear:right; 44 | position: relative; 45 | } 46 | div.select span { 47 | margin:0 10px; 48 | } 49 | form { display: inline-block;} 50 | 51 | label { color:#FFE400; font-size: 18px; line-height:22px; margin-bottom: 3px; text-shadow: rgba(0,0,0,0.3) 1px 1px 1px; display:block;} 52 | label.ofm { background:url(../images/ofm.png) no-repeat top left; padding-left:20px;} 53 | 54 | input { font-size:18px; } 55 | 56 | #content { 57 | clear:both; 58 | } 59 | 60 | #ID3 { 61 | background:#fff; 62 | padding:20px; 63 | min-height:400px; 64 | position:relative; 65 | } 66 | 67 | #ID3:before { 68 | content: ' '; 69 | height: 0; 70 | position: absolute; 71 | width: 0; 72 | border: 15px solid transparent; 73 | border-bottom-color: #fff; 74 | top: 0%; 75 | left: 90px; 76 | margin-top: -30px; 77 | } 78 | 79 | div.player { 80 | float:left; 81 | margin-right:20px; 82 | } 83 | 84 | div.picture { 85 | width:400px; 86 | height:400px; 87 | display: block; 88 | } 89 | div.picture img.picture { 90 | width:400px; 91 | height:400px; 92 | display: block; 93 | } 94 | 95 | a.button { 96 | width:100px; 97 | height:100px; 98 | display:block; 99 | position:relative; 100 | top:-250px; 101 | left:150px; 102 | } 103 | 104 | a.play { 105 | background: transparent url(../images/play.png) no-repeat top left; 106 | opacity:0.7; 107 | } 108 | a.play:hover {opacity:1;} 109 | 110 | a.pause { 111 | background: transparent url(../images/pause.png) no-repeat top left; 112 | opacity:0.7; 113 | } 114 | a.pause:hover {opacity:1;} 115 | 116 | div.timeline { 117 | width:360px; 118 | height:20px; 119 | border: 1px solid rgba(255,228,0,0.7); 120 | display:block; 121 | position:relative; 122 | top:-140px; 123 | left:20px; 124 | } 125 | div#progressbar { width: 0; height: 20px; background-color: #FA5945; } 126 | div#preloadbar { width: 0; height: 20px; background-color: rgba(255,228,0,0.5); } 127 | 128 | div.meta { background-color: rgba(0, 0, 0, 0.1); width:460px; padding:20px; font-size:16px; display:inline-block; } 129 | 130 | #shareTrack { 131 | text-align: right; 132 | margin: 20px 0 40px 0; 133 | } 134 | 135 | #playUrl { 136 | width: 320px; 137 | } 138 | 139 | #footer { 140 | clear:both; 141 | padding:0 20px 20px; 142 | color:#FFE400; 143 | font-size:15px; 144 | } 145 | #footer a { background:#fff; padding:2px 5px; -moz-border-radius:3px;font-size: 15px; 146 | padding: 3px 5px 1px; 147 | position: relative; 148 | top: -1px; 149 | font-weight:bold; 150 | text-decoration:none; 151 | color:#FA5945;} 152 | #footer a:hover { color:#000;} 153 | #footer a span {font-weight:normal;color:#000; font-style:italic;font-size:12px;} 154 | #footer a.mhd { background:transparent; top:0;} 155 | #footer img { position: relative;top: 4px; } 156 | -------------------------------------------------------------------------------- /experiments/application.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | -moz-user-select: -moz-none; 4 | } 5 | 6 | html { 7 | font: 18px/27px 'TeXGyreHerosBold'; 8 | color: white; 9 | } 10 | 11 | body { 12 | width: 35em; 13 | margin: 2em auto; 14 | background: url("images/noise.png") #3c3c46; 15 | -webkit-user-select: none; 16 | user-select: none; 17 | } 18 | 19 | span.even { 20 | color: #bc4; 21 | } 22 | 23 | h1, h2 { 24 | text-shadow: #1f2329 0 1px 0; 25 | } 26 | 27 | #outer { 28 | position: absolute; 29 | width: 36em; 30 | margin-left: -18em; 31 | left: 50%; 32 | top: 20%; 33 | margin-top: -125px; 34 | } 35 | 36 | #content { 37 | font: 100% 'TeXGyreHerosRegular'; 38 | } 39 | 40 | h2.slogan { 41 | width: 100%; 42 | left: 0; 43 | text-align: center; 44 | } 45 | 46 | h1.logo { 47 | font: 5em 'TeXGyreHerosBold'; 48 | color: white; 49 | margin-bottom: 0.6em; 50 | text-align: center; 51 | } 52 | 53 | h1.lovelogo { 54 | width: 100%; 55 | text-align: center; 56 | font: 3.4em 'TeXGyreHerosBold'; 57 | } 58 | 59 | h1 a, h1 a:visited{ 60 | color: white; 61 | text-decoration: none; 62 | } 63 | 64 | em { 65 | font-style: normal; 66 | color: #bc4; 67 | } 68 | 69 | a:visited { 70 | color: #444; 71 | } 72 | 73 | button:hover, input[type=submit]:hover, input[type=button]:hover, select:focus 74 | { 75 | background-color: rgb(106, 120, 145); 76 | } 77 | 78 | button:active, input[type=submit]:active, input[type=button]:active 79 | { 80 | background-color: rgb(37, 41, 47); 81 | background-image: -webkit-gradient( 82 | linear, 83 | left top, 84 | left bottom, 85 | color-stop(0.00, rgba(0, 0, 0, 0.3)), 86 | color-stop(0.50, rgba(0, 0, 0, 0.0)) 87 | ); 88 | background-image: -webkit-linear-gradient( 89 | top, 90 | rgba(0, 0, 0, 0.3) 0%, 91 | rgba(0, 0, 0, 0.0) 50% 92 | ); 93 | background-image: -moz-linear-gradient( 94 | top, 95 | rgba(0, 0, 0, 0.3) 0%, 96 | rgba(0, 0, 0, 0.0) 50% 97 | ); 98 | background-image: linear-gradient( 99 | top, 100 | rgba(0, 0, 0, 0.3) 0%, 101 | rgba(0, 0, 0, 0.0) 50% 102 | ); 103 | border-color: rgb(36, 40, 46); 104 | border-bottom-color: rgb(81, 83, 91); 105 | } 106 | 107 | textarea.code { 108 | display: block; 109 | margin-left: 10px; 110 | margin-right: 10px; 111 | font-size: 75%; 112 | min-height: 40px; 113 | max-width: 90%; 114 | min-width: 90%; 115 | } 116 | 117 | textarea.code:disabled { 118 | color: #fff; 119 | } 120 | 121 | .avatar, .avatar img { 122 | width: 80px; 123 | height: 80px; 124 | } 125 | 126 | .avatar { 127 | display: inline-block; 128 | border: 2px solid white; 129 | vertical-align: middle; 130 | border-radius: 0.15em; 131 | background: white; 132 | } 133 | 134 | .avatar img { 135 | border-radius: 0.15em; 136 | margin-bottom: 2px; 137 | } 138 | 139 | h2.slogan { 140 | font-size: 1.7em; 141 | bottom: 6%; 142 | } 143 | 144 | form { 145 | width: 100%; 146 | text-align: center; 147 | } 148 | 149 | ul.steps { 150 | font-size: 1.6em; 151 | width: 12em; 152 | margin-left: auto; 153 | margin-right: auto; 154 | } 155 | 156 | ul.steps li { 157 | padding: 0.3em; 158 | list-style-type: none; 159 | border-bottom: 1px solid white; 160 | color: #ddd; 161 | box-shadow: 0 0 35px #111111 inset; 162 | background: #222; 163 | } 164 | 165 | ul.steps li a { 166 | color: #ddd; 167 | text-decoration: none; 168 | } 169 | 170 | ul.steps li:first-child { 171 | border-radius: 8px 8px 0 0; 172 | } 173 | 174 | ul.steps li:last-child { 175 | border: none; 176 | border-radius: 0 0 8px 8px; 177 | } 178 | 179 | ul.steps li:hover { 180 | cursor: pointer; 181 | color: white; 182 | background: #444; 183 | } 184 | 185 | ul.steps span { 186 | display: inline-block; 187 | vertical-align: middle; 188 | width: 1.8em; 189 | height: 1.8em; 190 | font-size: 0.8em; 191 | text-align: center; 192 | padding: 0; 193 | color: white; 194 | text-shadow: -1px -1px 2px #444, 2px 2px 2px #222; 195 | } 196 | 197 | ul.steps span img { 198 | height: 1.8em; 199 | } 200 | 201 | #signup form { 202 | position: relative; 203 | margin: 10% 0; 204 | text-align: center; 205 | } 206 | 207 | #signup button { 208 | font-size: 2.2em; 209 | width: 9em; 210 | height: 2.7em; 211 | padding-right: 0.6em; 212 | padding-top: 0.3em; 213 | padding-bottom: 0; 214 | padding-left: 0; 215 | } 216 | 217 | #signup button img { 218 | vertical-align: middle; 219 | margin-bottom: 0.3em; 220 | margin-right: 0.3em; 221 | } 222 | 223 | h2.greeting, h2.status { 224 | margin-top: 0.1em; 225 | margin-bottom: 1.3em; 226 | } 227 | 228 | div.player, .dashboard { 229 | border: 2px solid #CCC; 230 | padding-top: 1em; 231 | padding-left: 1.2em; 232 | border-radius: 0.4em; 233 | background-color: #111; 234 | } 235 | 236 | div.signout form input[type=submit] { 237 | border: none; 238 | background: transparent; 239 | color: #A44; 240 | font-size: 100%; 241 | float: right; 242 | margin-bottom: 0.5em; 243 | margin-right: 0.5em; 244 | box-shadow: none; 245 | padding: 0; 246 | 247 | } 248 | 249 | div.signout form input[type=submit]:hover { 250 | text-decoration: underline; 251 | cursor: pointer; 252 | } 253 | 254 | div.clear { 255 | clear: both; 256 | } 257 | 258 | iframe.player { 259 | border: none; 260 | width: 220px; 261 | height: 350px; 262 | margin-left: auto; 263 | margin-right: auto; 264 | display: block; 265 | overflow: hidden; 266 | margin-bottom: 10px; 267 | scrolling: no; 268 | } 269 | -------------------------------------------------------------------------------- /experiments/experiment.js: -------------------------------------------------------------------------------- 1 | var domReady = function () 2 | { 3 | var fileChooser = document.forms['uploadData']['fileChooser']; 4 | fileChooser.onchange = function(ev) { 5 | readFile() 6 | } 7 | 8 | function readFile() { 9 | var file = fileChooser.files[0]; 10 | console.log("Reading file " + file.name); 11 | 12 | new Mad.FileStream(file, function(stream) { 13 | var mp3 = new Mad.MP3File(stream); 14 | console.log("File loaded"); 15 | var mpeg = mp3.getMpegStream(); 16 | 17 | var howmuch = 10000; 18 | var ss1 = Date.now(); 19 | console.log("Decoding first " + howmuch + " frames"); 20 | var frame = new Mad.Frame(); 21 | 22 | for(var i = 0; i < howmuch; i += 1) { 23 | frame = Mad.Frame.decode(frame, mpeg); 24 | } 25 | var ss2 = Date.now(); 26 | console.log("Done in " + (ss2 - ss1) + "ms"); 27 | }); 28 | } 29 | }; 30 | document.addEventListener("DOMContentLoaded", domReady, false); 31 | -------------------------------------------------------------------------------- /experiments/images/noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audiocogs/jsmad/3edad8b2cae8b61d692f14094cdb52ce9792f7bb/experiments/images/noise.png -------------------------------------------------------------------------------- /experiments/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |