@audio/speaker 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/binding.gyp +38 -0
- package/browser.d.ts +16 -0
- package/browser.js +84 -0
- package/index.d.ts +22 -0
- package/index.js +82 -0
- package/native/miniaudio.h +95864 -0
- package/native/speaker.c +468 -0
- package/package.json +81 -0
- package/readme.md +123 -0
- package/src/backend.js +29 -0
- package/src/backends/miniaudio.js +65 -0
- package/src/backends/null.js +19 -0
- package/src/backends/process.js +56 -0
- package/stream.d.ts +4 -0
- package/stream.js +19 -0
package/binding.gyp
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"targets": [{
|
|
3
|
+
"target_name": "speaker",
|
|
4
|
+
"sources": ["native/speaker.c"],
|
|
5
|
+
"include_dirs": ["native"],
|
|
6
|
+
"cflags": ["-std=c99", "-O2"],
|
|
7
|
+
"xcode_settings": {
|
|
8
|
+
"OTHER_CFLAGS": ["-std=c99", "-O2"],
|
|
9
|
+
"MACOSX_DEPLOYMENT_TARGET": "10.13"
|
|
10
|
+
},
|
|
11
|
+
"msvs_settings": {
|
|
12
|
+
"VCCLCompilerTool": {
|
|
13
|
+
"AdditionalOptions": ["/O2"]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"conditions": [
|
|
17
|
+
["OS=='mac'", {
|
|
18
|
+
"libraries": [
|
|
19
|
+
"-framework CoreAudio",
|
|
20
|
+
"-framework AudioToolbox",
|
|
21
|
+
"-framework CoreFoundation"
|
|
22
|
+
]
|
|
23
|
+
}],
|
|
24
|
+
["OS=='linux'", {
|
|
25
|
+
"libraries": [
|
|
26
|
+
"-lpthread",
|
|
27
|
+
"-lm",
|
|
28
|
+
"-ldl"
|
|
29
|
+
]
|
|
30
|
+
}],
|
|
31
|
+
["OS=='win'", {
|
|
32
|
+
"libraries": [
|
|
33
|
+
"ole32.lib"
|
|
34
|
+
]
|
|
35
|
+
}]
|
|
36
|
+
]
|
|
37
|
+
}]
|
|
38
|
+
}
|
package/browser.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface BrowserSpeakerOptions {
|
|
2
|
+
sampleRate?: number
|
|
3
|
+
channels?: number
|
|
4
|
+
bitDepth?: 8 | 16 | 32
|
|
5
|
+
context?: AudioContext
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface WriteFn {
|
|
9
|
+
(chunk: Uint8Array | null, cb?: (err: Error | null, frames?: number) => void): void
|
|
10
|
+
end(): void
|
|
11
|
+
flush(cb?: () => void): void
|
|
12
|
+
close(): void
|
|
13
|
+
backend: 'webaudio'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function Speaker(opts?: BrowserSpeakerOptions): WriteFn
|
package/browser.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module audio-speaker/browser
|
|
3
|
+
*
|
|
4
|
+
* Browser audio output via Web Audio API.
|
|
5
|
+
*/
|
|
6
|
+
export default function Speaker(opts = {}) {
|
|
7
|
+
const ownCtx = !opts.context
|
|
8
|
+
const ctx = opts.context || new AudioContext({
|
|
9
|
+
sampleRate: opts.sampleRate || 44100
|
|
10
|
+
})
|
|
11
|
+
const channels = opts.channels || 2
|
|
12
|
+
const sampleRate = ctx.sampleRate
|
|
13
|
+
const bitDepth = opts.bitDepth || 16
|
|
14
|
+
|
|
15
|
+
let closed = false
|
|
16
|
+
|
|
17
|
+
write.end = () => close()
|
|
18
|
+
write.flush = (cb) => cb?.()
|
|
19
|
+
write.close = close
|
|
20
|
+
write.backend = 'webaudio'
|
|
21
|
+
|
|
22
|
+
return write
|
|
23
|
+
|
|
24
|
+
function write(chunk, cb) {
|
|
25
|
+
if (chunk == null || closed) {
|
|
26
|
+
cb?.(null)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// resume suspended context (autoplay policy)
|
|
31
|
+
const ready = ctx.state === 'suspended' ? ctx.resume() : Promise.resolve()
|
|
32
|
+
ready.then(() => playChunk(chunk, cb), cb)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function playChunk(chunk, cb) {
|
|
36
|
+
const bytesPerSample = bitDepth / 8
|
|
37
|
+
const frameCount = (chunk.length / bytesPerSample / channels) | 0
|
|
38
|
+
const audioBuffer = ctx.createBuffer(channels, frameCount, sampleRate)
|
|
39
|
+
|
|
40
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
41
|
+
const out = audioBuffer.getChannelData(ch)
|
|
42
|
+
if (bitDepth === 16) {
|
|
43
|
+
for (let i = 0; i < frameCount; i++) {
|
|
44
|
+
const idx = (i * channels + ch) * 2
|
|
45
|
+
out[i] = ((chunk[idx] | (chunk[idx + 1] << 8)) << 16 >> 16) / 32768
|
|
46
|
+
}
|
|
47
|
+
} else if (bitDepth === 32) {
|
|
48
|
+
const f32 = new DataView(chunk.buffer, chunk.byteOffset, chunk.byteLength)
|
|
49
|
+
for (let i = 0; i < frameCount; i++) {
|
|
50
|
+
out[i] = f32.getFloat32((i * channels + ch) * 4, true)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const source = ctx.createBufferSource()
|
|
56
|
+
source.buffer = audioBuffer
|
|
57
|
+
source.connect(ctx.destination)
|
|
58
|
+
source.onended = () => cb?.(null, frameCount)
|
|
59
|
+
source.start()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function close() {
|
|
63
|
+
if (closed) return
|
|
64
|
+
closed = true
|
|
65
|
+
if (ownCtx) ctx.close?.()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Consume an async iterable of PCM chunks through the speaker.
|
|
71
|
+
* @param {AsyncIterable} source - async iterable yielding PCM buffers
|
|
72
|
+
* @param {object} opts - speaker options (sampleRate, channels, bitDepth, etc.)
|
|
73
|
+
* @returns {Promise<void>} resolves when source is exhausted
|
|
74
|
+
*/
|
|
75
|
+
Speaker.from = async function(source, opts) {
|
|
76
|
+
const write = Speaker(opts)
|
|
77
|
+
try {
|
|
78
|
+
for await (let chunk of source) {
|
|
79
|
+
await new Promise((resolve, reject) => write(chunk, err => err ? reject(err) : resolve()))
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
write(null)
|
|
83
|
+
}
|
|
84
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface SpeakerOptions {
|
|
2
|
+
sampleRate?: number
|
|
3
|
+
channels?: number
|
|
4
|
+
bitDepth?: 8 | 16 | 24 | 32
|
|
5
|
+
bufferSize?: number
|
|
6
|
+
backend?: 'miniaudio' | 'process' | 'null'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WriteFn {
|
|
10
|
+
(chunk: Buffer | Uint8Array | AudioBuffer | null, cb?: (err: Error | null, frames?: number) => void): void
|
|
11
|
+
end(): void
|
|
12
|
+
flush(cb?: () => void): void
|
|
13
|
+
close(): void
|
|
14
|
+
backend: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function speaker(opts?: SpeakerOptions): WriteFn
|
|
18
|
+
|
|
19
|
+
declare namespace speaker {
|
|
20
|
+
/** Consume an async iterable of PCM chunks through the speaker. */
|
|
21
|
+
function from(source: AsyncIterable<Buffer | Uint8Array | AudioBuffer>, opts?: SpeakerOptions): Promise<void>
|
|
22
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module audio-speaker
|
|
3
|
+
*
|
|
4
|
+
* Output audio data to speaker.
|
|
5
|
+
* let write = speaker({ sampleRate: 44100 })
|
|
6
|
+
* write(pcmBuffer, cb)
|
|
7
|
+
* write(null) // end
|
|
8
|
+
*/
|
|
9
|
+
import { open } from './src/backend.js'
|
|
10
|
+
|
|
11
|
+
const defaults = {
|
|
12
|
+
sampleRate: 44100,
|
|
13
|
+
channels: 2,
|
|
14
|
+
bitDepth: 16,
|
|
15
|
+
bufferSize: 50
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// detect AudioBuffer (has getChannelData + numberOfChannels + sampleRate)
|
|
19
|
+
function isAudioBuffer(buf) {
|
|
20
|
+
return buf && typeof buf.getChannelData === 'function' && buf.numberOfChannels > 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// convert AudioBuffer → interleaved int16 PCM Buffer
|
|
24
|
+
function audioBufferToPCM(ab, bitDepth) {
|
|
25
|
+
const ch = ab.numberOfChannels
|
|
26
|
+
const len = ab.length
|
|
27
|
+
const bps = bitDepth / 8
|
|
28
|
+
const buf = Buffer.alloc(len * ch * bps)
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < len; i++) {
|
|
31
|
+
for (let c = 0; c < ch; c++) {
|
|
32
|
+
const sample = ab.getChannelData(c)[i]
|
|
33
|
+
const offset = (i * ch + c) * bps
|
|
34
|
+
if (bitDepth === 16) {
|
|
35
|
+
buf.writeInt16LE(Math.max(-32768, Math.min(32767, Math.round(sample * 32767))), offset)
|
|
36
|
+
} else if (bitDepth === 32) {
|
|
37
|
+
buf.writeFloatLE(sample, offset)
|
|
38
|
+
} else if (bitDepth === 8) {
|
|
39
|
+
buf[offset] = Math.max(0, Math.min(255, Math.round((sample + 1) * 127.5)))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return buf
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default function speaker(opts) {
|
|
47
|
+
const config = { ...defaults, ...opts }
|
|
48
|
+
const { name, device } = open(config, config.backend)
|
|
49
|
+
|
|
50
|
+
write.end = () => { device.close() }
|
|
51
|
+
write.flush = (cb) => { device.flush(cb) }
|
|
52
|
+
write.close = () => { device.close() }
|
|
53
|
+
write.backend = name
|
|
54
|
+
|
|
55
|
+
return write
|
|
56
|
+
|
|
57
|
+
function write(chunk, cb) {
|
|
58
|
+
if (chunk == null) {
|
|
59
|
+
device.flush(() => { device.close(); cb?.(null) })
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
const buf = isAudioBuffer(chunk) ? audioBufferToPCM(chunk, config.bitDepth) : chunk
|
|
63
|
+
device.write(buf, cb)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Consume an async iterable of PCM chunks through the speaker.
|
|
69
|
+
* @param {AsyncIterable} source - async iterable yielding PCM buffers
|
|
70
|
+
* @param {object} opts - speaker options (sampleRate, channels, bitDepth, etc.)
|
|
71
|
+
* @returns {Promise<void>} resolves when source is exhausted
|
|
72
|
+
*/
|
|
73
|
+
speaker.from = async function(source, opts) {
|
|
74
|
+
const write = speaker(opts)
|
|
75
|
+
try {
|
|
76
|
+
for await (let chunk of source) {
|
|
77
|
+
await new Promise((resolve, reject) => write(chunk, err => err ? reject(err) : resolve()))
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
await new Promise(r => write(null, r))
|
|
81
|
+
}
|
|
82
|
+
}
|