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; |