@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.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Miniaudio backend — own N-API addon wrapping miniaudio.h
3
+ *
4
+ * Write strategy:
5
+ * - writeSync: non-blocking memcpy into ring buffer (zero overhead)
6
+ * - writeAsync: blocks on worker thread when ring buffer is full
7
+ * - Small writes (real-time rendering) → pure sync, no event loop round-trip
8
+ * - Large writes → sync partial + async remainder
9
+ *
10
+ * Load order:
11
+ * 1. @audio/speaker-{platform}-{arch} (platform package)
12
+ * 2. prebuilds/{platform}-{arch}/
13
+ * 3. build/Release/ or build/Debug/ (local node-gyp)
14
+ */
15
+ import { createRequire } from 'node:module'
16
+ import { join, dirname } from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
18
+ import { arch, platform } from 'node:os'
19
+
20
+ const require = createRequire(import.meta.url)
21
+ const root = join(dirname(fileURLToPath(import.meta.url)), '../..')
22
+ const plat = `${platform()}-${arch()}`
23
+
24
+ let addon
25
+ const loaders = [
26
+ () => require(`@audio/speaker-${plat}`),
27
+ () => require(join(root, 'packages', `speaker-${plat}`, 'speaker.node')),
28
+ () => require(join(root, 'prebuilds', plat, 'audio-speaker.node')),
29
+ () => require(join(root, 'prebuilds', plat, 'speaker.node')),
30
+ () => require(join(root, 'build', 'Release', 'speaker.node')),
31
+ () => require(join(root, 'build', 'Debug', 'speaker.node')),
32
+ ]
33
+ for (const load of loaders) {
34
+ try { addon = load(); break } catch {}
35
+ }
36
+
37
+ export function open({ sampleRate = 44100, channels = 2, bitDepth = 16, bufferSize = 50, capture = false } = {}) {
38
+ if (!addon) throw new Error('miniaudio addon not found — install @audio/speaker-' + plat + ' or run npm run build')
39
+ const handle = addon.open(sampleRate, channels, bitDepth, bufferSize, capture)
40
+
41
+ return {
42
+ // writeAsync blocks a worker thread until all data is written, then waits
43
+ // until the ring buffer drains below pull_threshold before firing cb.
44
+ // This means cb fires when the hardware *needs* more data — true pull pacing.
45
+ write(buf, cb) {
46
+ addon.writeAsync(handle, buf, (err, written) => cb?.(err, written))
47
+ },
48
+
49
+ read(buf) {
50
+ return addon.read(handle, buf)
51
+ },
52
+
53
+ flush(cb) {
54
+ const poll = () => {
55
+ if (addon.flush(handle)) cb?.()
56
+ else setTimeout(poll, 5)
57
+ }
58
+ poll()
59
+ },
60
+
61
+ close() {
62
+ addon.close(handle)
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Null backend — silent fallback for headless/CI environments.
3
+ * Maintains the timing contract: callback fires after the audio duration,
4
+ * preserving correct pacing for real-time render loops.
5
+ */
6
+ export function open({ sampleRate = 44100, channels = 2, bitDepth = 16 } = {}) {
7
+ const bytesPerFrame = channels * (bitDepth / 8)
8
+ let closed = false
9
+
10
+ return {
11
+ write(buf, cb) {
12
+ if (closed) return cb?.(new Error('closed'))
13
+ const frames = (buf.length / bytesPerFrame) | 0
14
+ setTimeout(() => cb?.(null, frames), frames / sampleRate * 1000)
15
+ },
16
+ flush(cb) { cb?.() },
17
+ close() { closed = true }
18
+ }
19
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Process backend — pipe PCM to system audio tools (last resort)
3
+ */
4
+ import { spawn, execSync } from 'node:child_process'
5
+ import { platform } from 'node:os'
6
+
7
+ function tryExec(cmd) {
8
+ try { execSync(cmd, { stdio: 'ignore' }); return true } catch { return false }
9
+ }
10
+
11
+ function findPlayer(sampleRate, channels, bitDepth) {
12
+ const os = platform()
13
+ const fmt = bitDepth === 8 ? 'u8' : bitDepth === 32 ? 'f32le' : `s${bitDepth}le`
14
+
15
+ if (tryExec('ffplay -version'))
16
+ return ['ffplay', ['-nodisp', '-autoexit', '-f', fmt, '-ar', sampleRate, '-ac', channels, '-']]
17
+
18
+ if (tryExec('play --version'))
19
+ return ['play', ['-t', 'raw', '-r', sampleRate, '-c', channels, '-b', bitDepth, '-e', 'signed-integer', '-L', '-']]
20
+
21
+ if (os === 'linux' && tryExec('aplay --version'))
22
+ return ['aplay', ['-f', fmt, '-r', sampleRate, '-c', channels]]
23
+
24
+ return null
25
+ }
26
+
27
+ export function open({ sampleRate = 44100, channels = 2, bitDepth = 16 } = {}) {
28
+ const player = findPlayer(sampleRate, channels, bitDepth)
29
+ if (!player) throw new Error('No audio player found (install ffplay or sox)')
30
+
31
+ const [cmd, args] = player
32
+ const proc = spawn(cmd, args.map(String), { stdio: ['pipe', 'ignore', 'ignore'] })
33
+
34
+ let closed = false
35
+
36
+ proc.on('close', () => { closed = true })
37
+
38
+ return {
39
+ write(buf, cb) {
40
+ if (closed) return cb?.(new Error('Process exited'))
41
+ proc.stdin.write(buf, cb)
42
+ },
43
+
44
+ flush(cb) {
45
+ if (closed) return cb?.()
46
+ proc.stdin.end(() => cb?.())
47
+ },
48
+
49
+ close() {
50
+ if (closed) return
51
+ closed = true
52
+ proc.stdin.end()
53
+ proc.kill()
54
+ }
55
+ }
56
+ }
package/stream.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { Writable } from 'node:stream'
2
+ import { SpeakerOptions } from './index.js'
3
+
4
+ export default function writable(opts?: SpeakerOptions): Writable
package/stream.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @module audio-speaker/stream
3
+ *
4
+ * Node.js Writable stream for audio output.
5
+ */
6
+ import { Writable } from 'node:stream'
7
+ import speaker from './index.js'
8
+
9
+ export default function writable(opts) {
10
+ const { sampleRate = 44100, channels = 2, bitDepth = 16, bufferSize = 50 } = opts || {}
11
+ const write = speaker(opts)
12
+
13
+ return new Writable({
14
+ highWaterMark: Math.round(sampleRate * channels * (bitDepth / 8) * bufferSize / 1000),
15
+ write(chunk, _, cb) { write(chunk, (err) => cb(err || null)) },
16
+ final(cb) { write.flush(() => { write.close(); cb() }) },
17
+ destroy(err, cb) { write.close(); cb(err) }
18
+ })
19
+ }