Line 0
Link Here
|
|
|
1 |
/* |
2 |
* EsounD Server |
3 |
* Copyright 2013 Samuel Mannehed for Cendio AB |
4 |
*/ |
5 |
|
6 |
function ESD(defaults) { |
7 |
"use strict"; |
8 |
|
9 |
var that = {}, // Public API methods |
10 |
conf = {}, // Configuration attributes |
11 |
|
12 |
// Pre-declare private functions used before definitions (jslint) |
13 |
handle_message, connect_audio, disconnect_audio, buffer_audio, |
14 |
parse_stream, parse_format, parse_rate, |
15 |
|
16 |
// |
17 |
// Private ESD namespace variables |
18 |
// |
19 |
esd_host = '', |
20 |
esd_port = 4910, |
21 |
esd_path = '', |
22 |
|
23 |
ws = null, // Websock object |
24 |
|
25 |
ws_loop_timeout = 1000, // time until we loop the ws connection |
26 |
ws_loop_timer = null, // timer for looping the ws connection |
27 |
|
28 |
authenticated = false, // true when the esd connection is authenticated |
29 |
little_endian = false, |
30 |
sixteen_bits = false, |
31 |
stream = false, // true while streaming data |
32 |
play = false, // true while playing sound |
33 |
nrOfChannels = 1, |
34 |
rate = 0, // Sample rate of audio |
35 |
name = "", // name of esd stream |
36 |
|
37 |
context = null, // audio context |
38 |
node = null, // audio processing node |
39 |
buff = new Array(); // buffer for audio data |
40 |
|
41 |
// Configuration attributes |
42 |
Util.conf_defaults(conf, that, defaults, [ |
43 |
['encrypt', 'rw', 'bool', false, 'Use TLS/SSL/wss encryption'] |
44 |
]); |
45 |
|
46 |
// |
47 |
// Setup routines |
48 |
// |
49 |
|
50 |
// Create the public API interface and initialize values that stay |
51 |
// constant across connect/disconnect |
52 |
function constructor() { |
53 |
|
54 |
ws = new Websock(); |
55 |
ws.on('message', handle_message); |
56 |
ws.on('open', function() { |
57 |
console.log("ESD: WebSocket on-open event"); |
58 |
}); |
59 |
ws.on('close', function(e) { |
60 |
var msg = ""; |
61 |
if (e.code) { |
62 |
msg = " (code: " + e.code; |
63 |
if (e.reason) |
64 |
msg += ", reason: " + e.reason; |
65 |
msg += ")"; |
66 |
} |
67 |
console.log("ESD: WebSocket on-close event, msg: " + msg); |
68 |
|
69 |
// Loop |
70 |
ws_loop_timer = setTimeout(function() { |
71 |
connect(); |
72 |
}, ws_loop_timeout); |
73 |
}); |
74 |
ws.on('error', function(e) { |
75 |
console.log("ESD: WebSocket on-error event, data: " + e.data); |
76 |
if (ws_loop_timer) { |
77 |
console.log("ESD: Clearing websocket loop timer"); |
78 |
clearTimeout(ws_loop_timer); |
79 |
ws_loop_timer = null; |
80 |
} |
81 |
}); |
82 |
|
83 |
ws.init(); |
84 |
|
85 |
/* Check web-socket-js if no builtin WebSocket support */ |
86 |
if (Websock_native) { |
87 |
console.log("ESD: Using native WebSockets"); |
88 |
} else { |
89 |
console.log("ESD: Using web-socket-js bridge. Flash version: " + |
90 |
Util.Flash.version); |
91 |
} |
92 |
|
93 |
try { |
94 |
if (window.AudioContext) |
95 |
context = new AudioContext(); |
96 |
else |
97 |
context = new webkitAudioContext(); |
98 |
} catch(e) { |
99 |
console.log("ESD: Web Audio API is not supported in this browser: " + e); |
100 |
} |
101 |
|
102 |
return that; // Return the public API interface |
103 |
} |
104 |
|
105 |
// On iOS devices the sound needs to start from an user initiated event |
106 |
window.addEventListener('touchstart', function() { |
107 |
|
108 |
if (context.createScriptProcessor) |
109 |
node = context.createScriptProcessor(4096, 0, 1); |
110 |
else |
111 |
node = context.createJavaScriptNode(4096, 0, 1); |
112 |
|
113 |
var source = context.createBufferSource(); |
114 |
var buffer = context.createBuffer(1, 1024, context.sampleRate) |
115 |
var data = buffer.getChannelData(0); |
116 |
for (var i = 0; i < data.length; i++) |
117 |
data[i] = 0; |
118 |
source.buffer = buffer; |
119 |
source.loop = true; |
120 |
source.connect(node); |
121 |
node.connect(context.destination); |
122 |
|
123 |
source.start(0); |
124 |
|
125 |
}, false); |
126 |
|
127 |
function connect() { |
128 |
var uri; |
129 |
|
130 |
if (typeof UsingSocketIO !== "undefined") { |
131 |
uri = "http://" + esd_host + ":" + esd_port + "/" + esd_path; |
132 |
} else { |
133 |
if (conf.encrypt) |
134 |
uri = "wss://"; |
135 |
else |
136 |
uri = "ws://"; |
137 |
uri += esd_host + ":" + esd_port + "/" + esd_path; |
138 |
} |
139 |
console.log("ESD: connecting to " + uri); |
140 |
ws.open(uri, ['binary','base64','audio']); |
141 |
} |
142 |
|
143 |
function disconnect() { |
144 |
ws.close(); |
145 |
} |
146 |
|
147 |
// |
148 |
// Utility routines |
149 |
// |
150 |
|
151 |
handle_message = function() { |
152 |
if (ws.rQlen() === 0) { return; } |
153 |
|
154 |
if (stream && play) { |
155 |
buffer_audio(); |
156 |
return; |
157 |
} |
158 |
|
159 |
var opcode, esdkey, endian; |
160 |
|
161 |
// If we are not authenticated yet |
162 |
if (!authenticated) { |
163 |
if (ws.rQwait("init", 20, 0)) { return; } |
164 |
|
165 |
esdkey = ws.rQshiftBytes(16); |
166 |
endian = ws.rQshiftStr(4); |
167 |
little_endian = (endian == 'NDNE'); |
168 |
if (little_endian) |
169 |
ws.send([1,0,0,0]); |
170 |
else |
171 |
ws.send([0,0,0,1]); |
172 |
authenticated = true; |
173 |
|
174 |
console.log("ESD: authenticated"); |
175 |
|
176 |
} else { |
177 |
opcode = ws.rQshiftBytes(4); |
178 |
|
179 |
switch (opcode[0]) { |
180 |
case 1: //lock |
181 |
case 2: //unlock |
182 |
case 3: //stream-play |
183 |
if (ws.rQwait("stream-play", 136, 4)) { return; } |
184 |
|
185 |
// Parse the header |
186 |
parse_format(ws.rQshiftBytes(4)); |
187 |
parse_rate(ws.rQshiftBytes(4)); |
188 |
name = ws.rQshiftStr(128); |
189 |
|
190 |
console.log("ESD: --- Audio header --- "); |
191 |
console.log((sixteen_bits ? "16bit " : "") + |
192 |
((nrOfChannels == 1) ? "mono " : "stereo ") + |
193 |
(stream ? "stream " : "") + |
194 |
(play ? "play" : "")); |
195 |
console.log(rate + " Hz"); |
196 |
console.log(name); |
197 |
|
198 |
connect_audio(); |
199 |
case 5: //stream-mon |
200 |
case 6: //sample-cache |
201 |
case 7: //sample-free |
202 |
case 8: //sample-play |
203 |
case 9: //sample-loop |
204 |
case 10: //sample-stop |
205 |
case 11: //sample-kill |
206 |
case 12: //standby |
207 |
case 13: //resume |
208 |
case 14: //sample-getid |
209 |
case 15: //stream-filter |
210 |
case 16: //server-info |
211 |
case 17: //server-all-info |
212 |
case 18: //subscribe |
213 |
case 19: //unsubjcribe |
214 |
case 20: //stream-pan |
215 |
case 21: //sample-pan |
216 |
case 22: //standby-mode |
217 |
case 23: //latency |
218 |
} |
219 |
} |
220 |
}; |
221 |
|
222 |
connect_audio = function() { |
223 |
// Reset the node |
224 |
if (node != null) |
225 |
node.disconnect(); |
226 |
|
227 |
if (context.createScriptProcessor) |
228 |
node = context.createScriptProcessor(4096, 0, nrOfChannels); |
229 |
else |
230 |
node = context.createJavaScriptNode(4096, 0, nrOfChannels); |
231 |
node.onaudioprocess = parse_stream; |
232 |
node.connect(context.destination); |
233 |
}; |
234 |
|
235 |
disconnect_audio = function() { |
236 |
|
237 |
node.onaudioprocess = null; |
238 |
node.disconnect(); |
239 |
|
240 |
authenticated = false; |
241 |
little_endian = false; |
242 |
sixteen_bits = false; |
243 |
stream = false; |
244 |
play = false; |
245 |
nrOfChannels = 1; |
246 |
rate = 0; |
247 |
name = ""; |
248 |
|
249 |
buff = new Array(); |
250 |
|
251 |
console.log("ESD: Closed audio connection"); |
252 |
}; |
253 |
|
254 |
buffer_audio = function() { |
255 |
var ws_data = ws.rQshiftBytes(ws.rQlen()); |
256 |
var l = buff.length; |
257 |
buff = buff.concat(ws_data); |
258 |
console.log("buffered " + (buff.length - l)); |
259 |
}; |
260 |
|
261 |
parse_stream = function(e) { |
262 |
var audio, n, channels, chan_positions, i16; |
263 |
|
264 |
if (buff.length > 0) { |
265 |
// Take buffered data |
266 |
audio = buff.splice(0, e.outputBuffer.length*4); |
267 |
console.log("playing " + audio.length) |
268 |
|
269 |
n = e.outputBuffer.numberOfChannels; |
270 |
channels = new Array(n); |
271 |
chan_positions = new Array(n); |
272 |
for (var c = 0; c < n; c++) { |
273 |
channels[c] = e.outputBuffer.getChannelData(c); |
274 |
channels[c].sampleRate = rate; |
275 |
chan_positions[c] = 0; |
276 |
} |
277 |
|
278 |
for (var i = 0; i < audio.length; i += 2) { |
279 |
|
280 |
// handle endian and convert to 16bit |
281 |
if (little_endian) |
282 |
i16 = audio[i] + (audio[i+1] << 8); |
283 |
else |
284 |
i16 = (audio[i+1] << 8) + audio[i]; |
285 |
|
286 |
// handle signed |
287 |
if (i16 > 32767) |
288 |
i16 -= 65536; |
289 |
|
290 |
// put audio on the channels |
291 |
for (var c = 0; c < n; c++) { |
292 |
if ((i/2) % n == c) { |
293 |
(channels[c])[chan_positions[c]] = i16 / 32767; |
294 |
chan_positions[c]++; |
295 |
} |
296 |
} |
297 |
} |
298 |
} else { |
299 |
disconnect_audio(); |
300 |
} |
301 |
}; |
302 |
|
303 |
parse_format = function(ws_data) { |
304 |
var format = "", length = ws_data.length; |
305 |
if (little_endian) { |
306 |
for (var i = length - 1; i >= 0; i--) |
307 |
format += ws_data[i].toString(16); |
308 |
} else { |
309 |
for (var i = 0; i < length; i++) |
310 |
format += ws_data[i].toString(16); |
311 |
} |
312 |
sixteen_bits = format.charAt(format.length - 1) == 1; |
313 |
nrOfChannels = format.charAt(format.length - 2); |
314 |
stream = format.charAt(format.length - 3) == 0; |
315 |
play = format.charAt(format.length - 4) == 1; |
316 |
}; |
317 |
|
318 |
parse_rate = function(ws_data) { |
319 |
var length = ws_data.length; |
320 |
if (little_endian) { |
321 |
for (var i = length - 1; i >= 0; i--) |
322 |
rate += ws_data[i] * Math.pow(2, 8 * i); |
323 |
} else { |
324 |
for (var i = 0; i < length; i++) |
325 |
rate += ws_data[i] * Math.pow(2, 8 * i); |
326 |
} |
327 |
}; |
328 |
|
329 |
// |
330 |
// Public API interface functions |
331 |
// |
332 |
|
333 |
that.connect = function(host, port, path) { |
334 |
esd_host = host; |
335 |
esd_port = port; |
336 |
esd_path = (path !== undefined) ? path : ""; |
337 |
|
338 |
if ((!esd_host) || (!esd_port)) |
339 |
return fail("Must set host and port"); |
340 |
connect(); |
341 |
}; |
342 |
|
343 |
that.disconnect = function() { |
344 |
disconnect(); |
345 |
}; |
346 |
|
347 |
return constructor(); // Return the public API interface |
348 |
|
349 |
} // End of ESD() |