trunk/src/osd/modules/sound/js_sound.js
r0 | r253173 | |
| 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; |