-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathindex.js
More file actions
82 lines (73 loc) · 2.29 KB
/
index.js
File metadata and controls
82 lines (73 loc) · 2.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/**
* @module audio-speaker
*
* Output audio data to speaker.
* let write = speaker({ sampleRate: 44100 })
* write(pcmBuffer, cb)
* write(null) // end
*/
import { open } from './src/backend.js'
const defaults = {
sampleRate: 44100,
channels: 2,
bitDepth: 16,
bufferSize: 50
}
// detect AudioBuffer (has getChannelData + numberOfChannels + sampleRate)
function isAudioBuffer(buf) {
return buf && typeof buf.getChannelData === 'function' && buf.numberOfChannels > 0
}
// convert AudioBuffer → interleaved int16 PCM Buffer
function audioBufferToPCM(ab, bitDepth) {
const ch = ab.numberOfChannels
const len = ab.length
const bps = bitDepth / 8
const buf = Buffer.alloc(len * ch * bps)
for (let i = 0; i < len; i++) {
for (let c = 0; c < ch; c++) {
const sample = ab.getChannelData(c)[i]
const offset = (i * ch + c) * bps
if (bitDepth === 16) {
buf.writeInt16LE(Math.max(-32768, Math.min(32767, Math.round(sample * 32767))), offset)
} else if (bitDepth === 32) {
buf.writeFloatLE(sample, offset)
} else if (bitDepth === 8) {
buf[offset] = Math.max(0, Math.min(255, Math.round((sample + 1) * 127.5)))
}
}
}
return buf
}
export default function speaker(opts) {
const config = { ...defaults, ...opts }
const { name, device } = open(config, config.backend)
write.end = () => { device.close() }
write.flush = (cb) => { device.flush(cb) }
write.close = () => { device.close() }
write.backend = name
return write
function write(chunk, cb) {
if (chunk == null) {
device.flush(() => { device.close(); cb?.(null) })
return
}
const buf = isAudioBuffer(chunk) ? audioBufferToPCM(chunk, config.bitDepth) : chunk
device.write(buf, cb)
}
}
/**
* Consume an async iterable of PCM chunks through the speaker.
* @param {AsyncIterable} source - async iterable yielding PCM buffers
* @param {object} opts - speaker options (sampleRate, channels, bitDepth, etc.)
* @returns {Promise<void>} resolves when source is exhausted
*/
speaker.from = async function(source, opts) {
const write = speaker(opts)
try {
for await (let chunk of source) {
await new Promise((resolve, reject) => write(chunk, err => err ? reject(err) : resolve()))
}
} finally {
await new Promise(r => write(null, r))
}
}