speechflow 1.7.1 → 2.0.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.
Files changed (177) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +388 -120
  3. package/etc/claude.md +5 -5
  4. package/etc/speechflow.yaml +2 -2
  5. package/package.json +3 -3
  6. package/speechflow-cli/dst/speechflow-main-api.js.map +1 -1
  7. package/speechflow-cli/dst/speechflow-main-cli.js +1 -0
  8. package/speechflow-cli/dst/speechflow-main-cli.js.map +1 -1
  9. package/speechflow-cli/dst/speechflow-main-graph.d.ts +1 -0
  10. package/speechflow-cli/dst/speechflow-main-graph.js +30 -9
  11. package/speechflow-cli/dst/speechflow-main-graph.js.map +1 -1
  12. package/speechflow-cli/dst/speechflow-main-nodes.js +1 -0
  13. package/speechflow-cli/dst/speechflow-main-nodes.js.map +1 -1
  14. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +1 -0
  15. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
  16. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +7 -9
  17. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
  18. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +1 -0
  19. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -1
  20. package/speechflow-cli/dst/speechflow-node-a2a-expander.js +8 -9
  21. package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
  22. package/speechflow-cli/dst/speechflow-node-a2a-filler.js +2 -0
  23. package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -1
  24. package/speechflow-cli/dst/speechflow-node-a2a-gender.js +1 -1
  25. package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
  26. package/speechflow-cli/dst/speechflow-node-a2a-meter.js +1 -1
  27. package/speechflow-cli/dst/speechflow-node-a2a-pitch.js +11 -9
  28. package/speechflow-cli/dst/speechflow-node-a2a-pitch.js.map +1 -1
  29. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js +1 -0
  30. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js.map +1 -1
  31. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -1
  32. package/speechflow-cli/dst/speechflow-node-a2a-speex.js +4 -2
  33. package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -1
  34. package/speechflow-cli/dst/speechflow-node-a2a-vad.js +19 -22
  35. package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
  36. package/speechflow-cli/dst/speechflow-node-a2a-wav.js +31 -4
  37. package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
  38. package/speechflow-cli/dst/speechflow-node-a2t-amazon.d.ts +0 -1
  39. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +2 -11
  40. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
  41. package/speechflow-cli/dst/speechflow-node-a2t-google.d.ts +16 -0
  42. package/speechflow-cli/dst/speechflow-node-a2t-google.js +314 -0
  43. package/speechflow-cli/dst/speechflow-node-a2t-google.js.map +1 -0
  44. package/speechflow-cli/dst/speechflow-node-a2t-openai.js +6 -1
  45. package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
  46. package/speechflow-cli/dst/speechflow-node-t2a-amazon.d.ts +1 -1
  47. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +27 -7
  48. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
  49. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.d.ts +1 -1
  50. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +5 -3
  51. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
  52. package/speechflow-cli/dst/speechflow-node-t2a-google.d.ts +15 -0
  53. package/speechflow-cli/dst/speechflow-node-t2a-google.js +215 -0
  54. package/speechflow-cli/dst/speechflow-node-t2a-google.js.map +1 -0
  55. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.d.ts +1 -1
  56. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +27 -6
  57. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
  58. package/speechflow-cli/dst/speechflow-node-t2a-openai.d.ts +15 -0
  59. package/speechflow-cli/dst/speechflow-node-t2a-openai.js +192 -0
  60. package/speechflow-cli/dst/speechflow-node-t2a-openai.js.map +1 -0
  61. package/speechflow-cli/dst/speechflow-node-t2a-supertonic.d.ts +17 -0
  62. package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js +619 -0
  63. package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js.map +1 -0
  64. package/speechflow-cli/dst/speechflow-node-t2t-amazon.js +0 -2
  65. package/speechflow-cli/dst/speechflow-node-t2t-amazon.js.map +1 -1
  66. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
  67. package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -1
  68. package/speechflow-cli/dst/{speechflow-node-t2t-transformers.d.ts → speechflow-node-t2t-opus.d.ts} +1 -3
  69. package/speechflow-cli/dst/speechflow-node-t2t-opus.js +161 -0
  70. package/speechflow-cli/dst/speechflow-node-t2t-opus.js.map +1 -0
  71. package/speechflow-cli/dst/speechflow-node-t2t-profanity.d.ts +11 -0
  72. package/speechflow-cli/dst/speechflow-node-t2t-profanity.js +118 -0
  73. package/speechflow-cli/dst/speechflow-node-t2t-profanity.js.map +1 -0
  74. package/speechflow-cli/dst/speechflow-node-t2t-punctuation.d.ts +13 -0
  75. package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js +220 -0
  76. package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js.map +1 -0
  77. package/speechflow-cli/dst/{speechflow-node-t2t-openai.d.ts → speechflow-node-t2t-spellcheck.d.ts} +2 -2
  78. package/speechflow-cli/dst/{speechflow-node-t2t-openai.js → speechflow-node-t2t-spellcheck.js} +48 -100
  79. package/speechflow-cli/dst/speechflow-node-t2t-spellcheck.js.map +1 -0
  80. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +8 -8
  81. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  82. package/speechflow-cli/dst/speechflow-node-t2t-summary.d.ts +16 -0
  83. package/speechflow-cli/dst/speechflow-node-t2t-summary.js +241 -0
  84. package/speechflow-cli/dst/speechflow-node-t2t-summary.js.map +1 -0
  85. package/speechflow-cli/dst/{speechflow-node-t2t-ollama.d.ts → speechflow-node-t2t-translate.d.ts} +2 -2
  86. package/speechflow-cli/dst/{speechflow-node-t2t-transformers.js → speechflow-node-t2t-translate.js} +53 -115
  87. package/speechflow-cli/dst/speechflow-node-t2t-translate.js.map +1 -0
  88. package/speechflow-cli/dst/speechflow-node-x2x-filter.js +2 -0
  89. package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
  90. package/speechflow-cli/dst/speechflow-node-xio-exec.d.ts +12 -0
  91. package/speechflow-cli/dst/speechflow-node-xio-exec.js +224 -0
  92. package/speechflow-cli/dst/speechflow-node-xio-exec.js.map +1 -0
  93. package/speechflow-cli/dst/speechflow-node-xio-file.d.ts +1 -0
  94. package/speechflow-cli/dst/speechflow-node-xio-file.js +78 -67
  95. package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
  96. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
  97. package/speechflow-cli/dst/speechflow-node-xio-vban.d.ts +17 -0
  98. package/speechflow-cli/dst/speechflow-node-xio-vban.js +330 -0
  99. package/speechflow-cli/dst/speechflow-node-xio-vban.js.map +1 -0
  100. package/speechflow-cli/dst/speechflow-node-xio-webrtc.d.ts +39 -0
  101. package/speechflow-cli/dst/speechflow-node-xio-webrtc.js +502 -0
  102. package/speechflow-cli/dst/speechflow-node-xio-webrtc.js.map +1 -0
  103. package/speechflow-cli/dst/speechflow-node-xio-websocket.js +9 -9
  104. package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
  105. package/speechflow-cli/dst/speechflow-util-audio.js +8 -5
  106. package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -1
  107. package/speechflow-cli/dst/speechflow-util-error.d.ts +1 -0
  108. package/speechflow-cli/dst/speechflow-util-error.js +5 -0
  109. package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
  110. package/speechflow-cli/dst/speechflow-util-llm.d.ts +35 -0
  111. package/speechflow-cli/dst/speechflow-util-llm.js +363 -0
  112. package/speechflow-cli/dst/speechflow-util-llm.js.map +1 -0
  113. package/speechflow-cli/dst/speechflow-util-queue.js +2 -1
  114. package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -1
  115. package/speechflow-cli/dst/speechflow-util.d.ts +1 -0
  116. package/speechflow-cli/dst/speechflow-util.js +2 -0
  117. package/speechflow-cli/dst/speechflow-util.js.map +1 -1
  118. package/speechflow-cli/etc/oxlint.jsonc +2 -1
  119. package/speechflow-cli/package.json +35 -18
  120. package/speechflow-cli/src/lib.d.ts +5 -0
  121. package/speechflow-cli/src/speechflow-main-api.ts +16 -16
  122. package/speechflow-cli/src/speechflow-main-cli.ts +1 -0
  123. package/speechflow-cli/src/speechflow-main-graph.ts +38 -14
  124. package/speechflow-cli/src/speechflow-main-nodes.ts +1 -0
  125. package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +1 -0
  126. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +8 -10
  127. package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +1 -0
  128. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +9 -10
  129. package/speechflow-cli/src/speechflow-node-a2a-filler.ts +2 -0
  130. package/speechflow-cli/src/speechflow-node-a2a-gender.ts +3 -3
  131. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +2 -2
  132. package/speechflow-cli/src/speechflow-node-a2a-pitch.ts +11 -9
  133. package/speechflow-cli/src/speechflow-node-a2a-rnnoise-wt.ts +1 -0
  134. package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +1 -1
  135. package/speechflow-cli/src/speechflow-node-a2a-speex.ts +5 -3
  136. package/speechflow-cli/src/speechflow-node-a2a-vad.ts +20 -23
  137. package/speechflow-cli/src/speechflow-node-a2a-wav.ts +31 -4
  138. package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +6 -18
  139. package/speechflow-cli/src/speechflow-node-a2t-google.ts +315 -0
  140. package/speechflow-cli/src/speechflow-node-a2t-openai.ts +12 -7
  141. package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +32 -10
  142. package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +6 -4
  143. package/speechflow-cli/src/speechflow-node-t2a-google.ts +203 -0
  144. package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +33 -10
  145. package/speechflow-cli/src/speechflow-node-t2a-openai.ts +176 -0
  146. package/speechflow-cli/src/speechflow-node-t2a-supertonic.ts +710 -0
  147. package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +3 -4
  148. package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +2 -2
  149. package/speechflow-cli/src/speechflow-node-t2t-google.ts +1 -1
  150. package/speechflow-cli/src/speechflow-node-t2t-opus.ts +137 -0
  151. package/speechflow-cli/src/speechflow-node-t2t-profanity.ts +93 -0
  152. package/speechflow-cli/src/speechflow-node-t2t-punctuation.ts +201 -0
  153. package/speechflow-cli/src/speechflow-node-t2t-spellcheck.ts +188 -0
  154. package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +8 -8
  155. package/speechflow-cli/src/speechflow-node-t2t-summary.ts +229 -0
  156. package/speechflow-cli/src/speechflow-node-t2t-translate.ts +181 -0
  157. package/speechflow-cli/src/speechflow-node-x2x-filter.ts +2 -0
  158. package/speechflow-cli/src/speechflow-node-xio-exec.ts +211 -0
  159. package/speechflow-cli/src/speechflow-node-xio-file.ts +91 -80
  160. package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +2 -2
  161. package/speechflow-cli/src/speechflow-node-xio-vban.ts +325 -0
  162. package/speechflow-cli/src/speechflow-node-xio-webrtc.ts +535 -0
  163. package/speechflow-cli/src/speechflow-node-xio-websocket.ts +9 -9
  164. package/speechflow-cli/src/speechflow-util-audio.ts +10 -5
  165. package/speechflow-cli/src/speechflow-util-error.ts +9 -0
  166. package/speechflow-cli/src/speechflow-util-llm.ts +367 -0
  167. package/speechflow-cli/src/speechflow-util-queue.ts +3 -3
  168. package/speechflow-cli/src/speechflow-util.ts +2 -0
  169. package/speechflow-ui-db/package.json +9 -9
  170. package/speechflow-ui-st/package.json +9 -9
  171. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +0 -293
  172. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +0 -1
  173. package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +0 -1
  174. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +0 -1
  175. package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +0 -281
  176. package/speechflow-cli/src/speechflow-node-t2t-openai.ts +0 -247
  177. package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +0 -247
@@ -0,0 +1,325 @@
1
+ /*
2
+ ** SpeechFlow - Speech Processing Flow Graph
3
+ ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
+ */
6
+
7
+ /* standard dependencies */
8
+ import Stream from "node:stream"
9
+
10
+ /* external dependencies */
11
+ import { DateTime } from "luxon"
12
+ import { VBANServer, VBANAudioPacket,
13
+ EBitsResolutions, ECodecs } from "vban"
14
+
15
+ /* internal dependencies */
16
+ import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
17
+ import * as util from "./speechflow-util"
18
+
19
+ /* VBAN sample rate index to Hz mapping */
20
+ const sampleRateToIndex: { [ rate: number ]: number } = {
21
+ 6000: 0, 12000: 1, 24000: 2, 48000: 3, 96000: 4, 192000: 5, 384000: 6,
22
+ 8000: 7, 16000: 8, 32000: 9, 64000: 10, 128000: 11, 256000: 12, 512000: 13,
23
+ 11025: 14, 22050: 15, 44100: 16, 88200: 17, 176400: 18, 352800: 19, 705600: 20
24
+ }
25
+
26
+ /* SpeechFlow node for VBAN networking */
27
+ export default class SpeechFlowNodeXIOVBAN extends SpeechFlowNode {
28
+ /* declare official node name */
29
+ public static name = "xio-vban"
30
+
31
+ /* internal state */
32
+ private server: VBANServer | null = null
33
+ private chunkQueue: util.SingleQueue<SpeechFlowChunk> | null = null
34
+ private frameCounter = 0
35
+ private targetAddress = ""
36
+ private targetPort = 0
37
+
38
+ /* construct node */
39
+ constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
40
+ super(id, cfg, opts, args)
41
+
42
+ /* declare node configuration parameters */
43
+ this.configure({
44
+ listen: { type: "string", pos: 0, val: "", match: /^(?:|\d+|.+?:\d+)$/ },
45
+ connect: { type: "string", pos: 1, val: "", match: /^(?:|.+?:\d+)$/ },
46
+ stream: { type: "string", pos: 2, val: "Stream", match: /^.{1,16}$/ },
47
+ mode: { type: "string", pos: 3, val: "rw", match: /^(?:r|w|rw)$/ }
48
+ })
49
+
50
+ /* sanity check parameters */
51
+ if (this.params.listen === "" && this.params.connect === "")
52
+ throw new Error("VBAN node requires either listen or connect mode")
53
+ if (this.params.mode === "r" && this.params.listen === "")
54
+ throw new Error("VBAN read mode requires a listen address")
55
+ if (this.params.mode === "w" && this.params.connect === "")
56
+ throw new Error("VBAN write mode requires a connect address")
57
+
58
+ /* VBAN only handles audio */
59
+ if (this.params.mode === "rw") {
60
+ this.input = "audio"
61
+ this.output = "audio"
62
+ }
63
+ else if (this.params.mode === "r") {
64
+ this.input = "none"
65
+ this.output = "audio"
66
+ }
67
+ else if (this.params.mode === "w") {
68
+ this.input = "audio"
69
+ this.output = "none"
70
+ }
71
+ }
72
+
73
+ /* parse address:port string */
74
+ private parseAddress (addr: string, defaultPort: number): { host: string, port: number } {
75
+ if (addr.match(/^\d+$/))
76
+ return { host: "0.0.0.0", port: Number.parseInt(addr, 10) }
77
+ const m = addr.match(/^(.+?):(\d+)$/)
78
+ if (m === null)
79
+ return { host: addr, port: defaultPort }
80
+ return { host: m[1], port: Number.parseInt(m[2], 10) }
81
+ }
82
+
83
+ /* open node */
84
+ async open () {
85
+ /* create VBAN server */
86
+ this.server = new VBANServer({
87
+ application: {
88
+ applicationName: "SpeechFlow",
89
+ manufacturerName: "Dr. Ralf S. Engelschall",
90
+ deviceName: this.id
91
+ }
92
+ })
93
+
94
+ /* setup error handling */
95
+ this.server.on("error", (err: Error) => {
96
+ this.log("error", `VBAN error: ${err.message}`)
97
+ })
98
+
99
+ /* setup chunk queue for incoming audio */
100
+ this.chunkQueue = new util.SingleQueue<SpeechFlowChunk>()
101
+
102
+ /* determine target for sending */
103
+ if (this.params.connect !== "") {
104
+ const target = this.parseAddress(this.params.connect, 6980)
105
+ this.targetAddress = target.host
106
+ this.targetPort = target.port
107
+ }
108
+
109
+ /* handle incoming VBAN packets */
110
+ this.server.on("message", (packet: any, sender: { address: string, port: number }) => {
111
+ if (this.params.mode === "w")
112
+ return
113
+
114
+ /* only handle audio packets */
115
+ if (!(packet instanceof VBANAudioPacket))
116
+ return
117
+
118
+ /* optionally filter by stream name */
119
+ if (this.params.stream !== "" && packet.streamName !== this.params.stream)
120
+ return
121
+
122
+ /* get audio data from packet */
123
+ if (!Buffer.isBuffer(packet.data)) {
124
+ this.log("warning", "VBAN packet data is not a Buffer")
125
+ return
126
+ }
127
+ const data = packet.data
128
+
129
+ /* convert audio format if necessary */
130
+ let audioBuffer: Buffer
131
+ const bitResolution = packet.bitResolution
132
+ if (bitResolution === EBitsResolutions.VBAN_DATATYPE_INT16) {
133
+ /* 16-bit signed integer - matches our format */
134
+ audioBuffer = data
135
+ }
136
+ else if (bitResolution === EBitsResolutions.VBAN_DATATYPE_BYTE8) {
137
+ /* 8-bit unsigned to 16-bit signed */
138
+ audioBuffer = Buffer.alloc(data.length * 2)
139
+ for (let i = 0; i < data.length; i++) {
140
+ const sample = ((data[i] - 128) / 128) * 32767
141
+ audioBuffer.writeInt16LE(Math.round(sample), i * 2)
142
+ }
143
+ }
144
+ else if (bitResolution === EBitsResolutions.VBAN_DATATYPE_INT24) {
145
+ /* 24-bit signed to 16-bit signed */
146
+ const samples = Math.floor(data.length / 3)
147
+ audioBuffer = Buffer.alloc(samples * 2)
148
+ for (let i = 0; i < samples; i++) {
149
+ const b0 = data[i * 3]
150
+ const b1 = data[i * 3 + 1]
151
+ const b2 = data[i * 3 + 2]
152
+ const value = ((b2 << 16) | (b1 << 8) | b0) & 0xFFFFFF
153
+ const signed = value > 0x7FFFFF ? value - 0x1000000 : value
154
+ const sample = (signed / 0x800000) * 32767
155
+ audioBuffer.writeInt16LE(Math.round(sample), i * 2)
156
+ }
157
+ }
158
+ else if (bitResolution === EBitsResolutions.VBAN_DATATYPE_INT32) {
159
+ /* 32-bit signed to 16-bit signed */
160
+ const samples = Math.floor(data.length / 4)
161
+ audioBuffer = Buffer.alloc(samples * 2)
162
+ for (let i = 0; i < samples; i++) {
163
+ const value = data.readInt32LE(i * 4)
164
+ const sample = (value / 0x80000000) * 32767
165
+ audioBuffer.writeInt16LE(Math.round(sample), i * 2)
166
+ }
167
+ }
168
+ else if (bitResolution === EBitsResolutions.VBAN_DATATYPE_FLOAT32) {
169
+ /* 32-bit float to 16-bit signed */
170
+ const samples = Math.floor(data.length / 4)
171
+ audioBuffer = Buffer.alloc(samples * 2)
172
+ for (let i = 0; i < samples; i++) {
173
+ const value = data.readFloatLE(i * 4)
174
+ const sample = Math.max(-32768, Math.min(32767, Math.round(value * 32767)))
175
+ audioBuffer.writeInt16LE(sample, i * 2)
176
+ }
177
+ }
178
+ else if (bitResolution === EBitsResolutions.VBAN_DATATYPE_FLOAT64) {
179
+ /* 64-bit float to 16-bit signed */
180
+ const samples = Math.floor(data.length / 8)
181
+ audioBuffer = Buffer.alloc(samples * 2)
182
+ for (let i = 0; i < samples; i++) {
183
+ const value = data.readDoubleLE(i * 8)
184
+ const sample = Math.max(-32768, Math.min(32767, Math.round(value * 32767)))
185
+ audioBuffer.writeInt16LE(sample, i * 2)
186
+ }
187
+ }
188
+ else {
189
+ /* unsupported format */
190
+ this.log("warning", `unsupported VBAN bit resolution: ${bitResolution}`)
191
+ return
192
+ }
193
+
194
+ /* handle channel conversion if needed */
195
+ const channels = packet.nbChannel + 1
196
+ if (channels > 1 && this.config.audioChannels === 1) {
197
+ /* downmix to mono */
198
+ const samples = audioBuffer.length / 2 / channels
199
+ const monoBuffer = Buffer.alloc(samples * 2)
200
+ for (let i = 0; i < samples; i++) {
201
+ let sum = 0
202
+ for (let ch = 0; ch < channels; ch++)
203
+ sum += audioBuffer.readInt16LE((i * channels + ch) * 2)
204
+ monoBuffer.writeInt16LE(Math.round(sum / channels), i * 2)
205
+ }
206
+ audioBuffer = monoBuffer
207
+ }
208
+
209
+ /* create chunk with timing information */
210
+ const now = DateTime.now()
211
+ const start = now.diff(this.timeZero)
212
+ const duration = util.audioBufferDuration(audioBuffer,
213
+ this.config.audioSampleRate, this.config.audioBitDepth, this.config.audioChannels)
214
+ const end = start.plus(duration * 1000)
215
+ const chunk = new SpeechFlowChunk(start, end, "final", "audio", audioBuffer)
216
+ this.chunkQueue?.write(chunk)
217
+ })
218
+
219
+ /* setup listening */
220
+ this.server.on("listening", () => {
221
+ const address = this.server!.address()
222
+ this.log("info", `VBAN listening on ${address.address}:${address.port}`)
223
+ })
224
+
225
+ /* bind to listen port */
226
+ if (this.params.listen !== "") {
227
+ const listen = this.parseAddress(this.params.listen, 6980)
228
+ this.server.bind(listen.port, listen.host)
229
+ }
230
+ else
231
+ /* still need to bind for sending */
232
+ this.server.bind(0)
233
+
234
+ /* create duplex stream */
235
+ const self = this
236
+ const reads = new util.PromiseSet<void>()
237
+ this.stream = new Stream.Duplex({
238
+ writableObjectMode: true,
239
+ readableObjectMode: true,
240
+ decodeStrings: false,
241
+ highWaterMark: 1,
242
+ write (chunk: SpeechFlowChunk, encoding, callback) {
243
+ if (self.params.mode === "r") {
244
+ callback(new Error("write operation on read-only node"))
245
+ return
246
+ }
247
+ if (chunk.type !== "audio") {
248
+ callback(new Error("VBAN only supports audio type"))
249
+ return
250
+ }
251
+ if (self.targetAddress === "") {
252
+ callback(new Error("no VBAN target address configured"))
253
+ return
254
+ }
255
+
256
+ /* get audio buffer */
257
+ const audioBuffer = chunk.payload as Buffer
258
+
259
+ /* determine VBAN sample rate index */
260
+ const sampleRateIndex = sampleRateToIndex[self.config.audioSampleRate]
261
+ if (sampleRateIndex === undefined) {
262
+ callback(new Error(`unsupported sample rate for VBAN: ${self.config.audioSampleRate}`))
263
+ return
264
+ }
265
+
266
+ /* calculate number of samples */
267
+ const bytesPerSample = self.config.audioBitDepth / 8
268
+ const nbSample = (audioBuffer.length / bytesPerSample / self.config.audioChannels) - 1
269
+ if (nbSample < 0 || nbSample > 255)
270
+ self.log("warning", `VBAN nbSample out of range: ${nbSample} (clamped to 0-255)`)
271
+
272
+ /* create VBAN audio packet */
273
+ const packet = new VBANAudioPacket({
274
+ streamName: self.params.stream,
275
+ srIndex: sampleRateIndex,
276
+ nbSample: Math.min(255, Math.max(0, nbSample)),
277
+ nbChannel: self.config.audioChannels - 1,
278
+ bitResolution: EBitsResolutions.VBAN_DATATYPE_INT16,
279
+ codec: ECodecs.VBAN_CODEC_PCM,
280
+ frameCounter: self.frameCounter++
281
+ }, audioBuffer)
282
+
283
+ /* send packet */
284
+ self.server!.send(packet, self.targetPort, self.targetAddress)
285
+ .then(() => callback())
286
+ .catch((err: Error) => callback(err))
287
+ },
288
+ async final (callback) {
289
+ await reads.awaitAll()
290
+ callback()
291
+ },
292
+ read (size: number) {
293
+ if (self.params.mode === "w")
294
+ throw new Error("read operation on write-only node")
295
+ reads.add(self.chunkQueue!.read().then((chunk) => {
296
+ this.push(chunk, "binary")
297
+ }).catch((err: Error) => {
298
+ self.log("warning", `read on chunk queue operation failed: ${err}`)
299
+ this.push(null)
300
+ }))
301
+ }
302
+ })
303
+ }
304
+
305
+ /* close node */
306
+ async close () {
307
+ /* drain and clear chunk queue reference */
308
+ if (this.chunkQueue !== null) {
309
+ this.chunkQueue.drain()
310
+ this.chunkQueue = null
311
+ }
312
+
313
+ /* close VBAN server */
314
+ if (this.server !== null) {
315
+ this.server.close()
316
+ this.server = null
317
+ }
318
+
319
+ /* shutdown stream */
320
+ if (this.stream !== null) {
321
+ await util.destroyStream(this.stream)
322
+ this.stream = null
323
+ }
324
+ }
325
+ }