@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 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
+ }