trunk/src/osd/modules/sound/js_sound.js
| r253171 | r253172 | |
| 1 | | // license:BSD-3-Clause |
| 2 | | // copyright-holders:Grant Galitz, Katelyn Gadd |
| 3 | | /*************************************************************************** |
| 4 | | |
| 5 | | JSMAME web audio backend v0.3 |
| 6 | | |
| 7 | | Original by katelyn gadd - kg at luminance dot org ; @antumbral on twitter |
| 8 | | Substantial changes by taisel |
| 9 | | |
| 10 | | ***************************************************************************/ |
| 11 | | |
| 12 | | var jsmame_web_audio = (function () { |
| 13 | | |
| 14 | | var context = null; |
| 15 | | var gain_node = null; |
| 16 | | var eventNode = null; |
| 17 | | var sampleScale = 32766; |
| 18 | | var inputBuffer = new Float32Array(44100); |
| 19 | | var bufferSize = 44100; |
| 20 | | var start = 0; |
| 21 | | var rear = 0; |
| 22 | | var watchDogDateLast = null; |
| 23 | | var watchDogTimerEvent = null; |
| 24 | | |
| 25 | | function lazy_init () { |
| 26 | | //Make |
| 27 | | if (context) { |
| 28 | | //Return if already created: |
| 29 | | return; |
| 30 | | } |
| 31 | | if (typeof AudioContext != "undefined") { |
| 32 | | //Standard context creation: |
| 33 | | context = new AudioContext(); |
| 34 | | } |
| 35 | | else if (typeof webkitAudioContext != "undefined") { |
| 36 | | //Older webkit context creation: |
| 37 | | context = new webkitAudioContext(); |
| 38 | | } |
| 39 | | else { |
| 40 | | //API not found! |
| 41 | | return; |
| 42 | | } |
| 43 | | //Generate a volume control node: |
| 44 | | gain_node = context.createGain(); |
| 45 | | //Set initial volume to 1: |
| 46 | | gain_node.gain.value = 1.0; |
| 47 | | //Connect volume node to output: |
| 48 | | gain_node.connect(context.destination); |
| 49 | | //Initialize the streaming event: |
| 50 | | init_event(); |
| 51 | | }; |
| 52 | | |
| 53 | | function init_event() { |
| 54 | | //Generate a streaming node point: |
| 55 | | if (typeof context.createScriptProcessor == "function") { |
| 56 | | //Current standard compliant way: |
| 57 | | eventNode = context.createScriptProcessor(4096, 0, 2); |
| 58 | | } |
| 59 | | else { |
| 60 | | //Deprecated way: |
| 61 | | eventNode = context.createJavaScriptNode(4096, 0, 2); |
| 62 | | } |
| 63 | | //Make our tick function the audio callback function: |
| 64 | | eventNode.onaudioprocess = tick; |
| 65 | | //Connect stream to volume control node: |
| 66 | | eventNode.connect(gain_node); |
| 67 | | //WORKAROUND FOR FIREFOX BUG: |
| 68 | | initializeWatchDogForFirefoxBug(); |
| 69 | | }; |
| 70 | | |
| 71 | | function initializeWatchDogForFirefoxBug() { |
| 72 | | //TODO: decide if we want to user agent sniff firefox here, |
| 73 | | //since Google Chrome doesn't need this: |
| 74 | | watchDogDateLast = (new Date()).getTime(); |
| 75 | | if (watchDogTimerEvent === null) { |
| 76 | | watchDogTimerEvent = setInterval(function () { |
| 77 | | var timeDiff = (new Date()).getTime() - watchDogDateLast; |
| 78 | | if (timeDiff > 500) { |
| 79 | | disconnect_old_event(); |
| 80 | | init_event(); |
| 81 | | } |
| 82 | | }, 500); |
| 83 | | } |
| 84 | | }; |
| 85 | | |
| 86 | | function disconnect_old_event() { |
| 87 | | //Disconnect from audio graph: |
| 88 | | eventNode.disconnect(); |
| 89 | | //IIRC there was a firefox bug that did not GC this event when nulling the node itself: |
| 90 | | eventNode.onaudioprocess = null; |
| 91 | | //Null the glitched/unused node: |
| 92 | | eventNode = null; |
| 93 | | }; |
| 94 | | |
| 95 | | function set_mastervolume ( |
| 96 | | // even though it's 'attenuation' the value is negative, so... |
| 97 | | attenuation_in_decibels |
| 98 | | ) { |
| 99 | | lazy_init(); |
| 100 | | if (!context) return; |
| 101 | | |
| 102 | | // http://stackoverflow.com/questions/22604500/web-audio-api-working-with-decibels |
| 103 | | // seemingly incorrect/broken. figures. welcome to Web Audio |
| 104 | | // var gain_web_audio = 1.0 - Math.pow(10, 10 / attenuation_in_decibels); |
| 105 | | |
| 106 | | // HACK: Max attenuation in JSMESS appears to be 32. |
| 107 | | // Hit ' then left/right arrow to test. |
| 108 | | // FIXME: This is linear instead of log10 scale. |
| 109 | | var gain_web_audio = 1.0 + (+attenuation_in_decibels / +32); |
| 110 | | if (gain_web_audio < +0) |
| 111 | | gain_web_audio = +0; |
| 112 | | else if (gain_web_audio > +1) |
| 113 | | gain_web_audio = +1; |
| 114 | | |
| 115 | | gain_node.gain.value = gain_web_audio; |
| 116 | | }; |
| 117 | | |
| 118 | | function update_audio_stream ( |
| 119 | | pBuffer, // pointer into emscripten heap. int16 samples |
| 120 | | samples_this_frame // int. number of samples at pBuffer address. |
| 121 | | ) { |
| 122 | | lazy_init(); |
| 123 | | if (!context) return; |
| 124 | | |
| 125 | | for ( |
| 126 | | var i = 0, |
| 127 | | l = samples_this_frame | 0; |
| 128 | | i < l; |
| 129 | | i++ |
| 130 | | ) { |
| 131 | | var offset = |
| 132 | | // divide by sizeof(INT16) since pBuffer is offset |
| 133 | | // in bytes |
| 134 | | ((pBuffer / 2) | 0) + |
| 135 | | ((i * 2) | 0); |
| 136 | | |
| 137 | | var left_sample = HEAP16[offset]; |
| 138 | | var right_sample = HEAP16[(offset + 1) | 0]; |
| 139 | | |
| 140 | | // normalize from signed int16 to signed float |
| 141 | | var left_sample_float = left_sample / sampleScale; |
| 142 | | var right_sample_float = right_sample / sampleScale; |
| 143 | | |
| 144 | | inputBuffer[rear++] = left_sample_float; |
| 145 | | inputBuffer[rear++] = right_sample_float; |
| 146 | | if (rear == bufferSize) { |
| 147 | | rear = 0; |
| 148 | | } |
| 149 | | if (start == rear) { |
| 150 | | start += 2; |
| 151 | | if (start == bufferSize) { |
| 152 | | start = 0; |
| 153 | | } |
| 154 | | } |
| 155 | | } |
| 156 | | }; |
| 157 | | function tick (event) { |
| 158 | | //Find all output channels: |
| 159 | | for (var bufferCount = 0, buffers = []; bufferCount < 2; ++bufferCount) { |
| 160 | | buffers[bufferCount] = event.outputBuffer.getChannelData(bufferCount); |
| 161 | | } |
| 162 | | //Copy samples from the input buffer to the Web Audio API: |
| 163 | | for (var index = 0; index < 4096 && start != rear; ++index) { |
| 164 | | buffers[0][index] = inputBuffer[start++]; |
| 165 | | buffers[1][index] = inputBuffer[start++]; |
| 166 | | if (start == bufferSize) { |
| 167 | | start = 0; |
| 168 | | } |
| 169 | | } |
| 170 | | //Pad with silence if we're underrunning: |
| 171 | | while (index < 4096) { |
| 172 | | buffers[0][index] = 0; |
| 173 | | buffers[1][index++] = 0; |
| 174 | | } |
| 175 | | //Deep inside the bowels of vendors bugs, |
| 176 | | //we're using watchdog for a firefox bug, |
| 177 | | //where the user agent decides to stop firing events |
| 178 | | //if the user agent lags out due to system load. |
| 179 | | //Don't even ask.... |
| 180 | | watchDogDateLast = (new Date()).getTime(); |
| 181 | | } |
| 182 | | |
| 183 | | function get_context() { |
| 184 | | return context; |
| 185 | | }; |
| 186 | | |
| 187 | | function sample_count() { |
| 188 | | //TODO get someone to call this from the emulator, |
| 189 | | //so the emulator can do proper audio buffering by |
| 190 | | //knowing how many samples are left: |
| 191 | | if (!context) { |
| 192 | | //Use impossible value as an error code: |
| 193 | | return -1; |
| 194 | | } |
| 195 | | var count = rear - start; |
| 196 | | if (start > rear) { |
| 197 | | count += bufferSize; |
| 198 | | } |
| 199 | | return count; |
| 200 | | } |
| 201 | | |
| 202 | | return { |
| 203 | | set_mastervolume: set_mastervolume, |
| 204 | | update_audio_stream: update_audio_stream, |
| 205 | | get_context: get_context, |
| 206 | | sample_count: sample_count |
| 207 | | }; |
| 208 | | |
| 209 | | })(); |
| 210 | | |
| 211 | | window.jsmame_set_mastervolume = jsmame_web_audio.set_mastervolume; |
| 212 | | window.jsmame_update_audio_stream = jsmame_web_audio.update_audio_stream; |
| 213 | | window.jsmame_sample_count = jsmame_web_audio.sample_count; |