speechflow 1.7.0 → 2.0.0
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/CHANGELOG.md +23 -0
- package/README.md +425 -146
- package/etc/claude.md +5 -5
- package/etc/speechflow.yaml +2 -2
- package/package.json +3 -3
- package/speechflow-cli/dst/speechflow-main-api.js +6 -5
- package/speechflow-cli/dst/speechflow-main-api.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-graph.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-main-graph.js +35 -13
- package/speechflow-cli/dst/speechflow-main-graph.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-status.js +3 -7
- package/speechflow-cli/dst/speechflow-main-status.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +3 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +4 -2
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js +4 -2
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js +1 -2
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js +32 -5
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.d.ts +0 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +1 -6
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.d.ts +0 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +9 -9
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-google.d.ts +17 -0
- package/speechflow-cli/dst/speechflow-node-a2t-google.js +320 -0
- package/speechflow-cli/dst/speechflow-node-a2t-google.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js +6 -4
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +6 -11
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +6 -5
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-google.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-t2a-google.js +218 -0
- package/speechflow-cli/dst/speechflow-node-t2a-google.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.d.ts +2 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +19 -6
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-openai.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-t2a-openai.js +195 -0
- package/speechflow-cli/dst/speechflow-node-t2a-openai.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.d.ts +17 -0
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js +608 -0
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/{speechflow-node-t2t-transformers.d.ts → speechflow-node-t2t-opus.d.ts} +1 -3
- package/speechflow-cli/dst/speechflow-node-t2t-opus.js +159 -0
- package/speechflow-cli/dst/speechflow-node-t2t-opus.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-profanity.d.ts +11 -0
- package/speechflow-cli/dst/speechflow-node-t2t-profanity.js +118 -0
- package/speechflow-cli/dst/speechflow-node-t2t-profanity.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-punctuation.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js +220 -0
- package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js.map +1 -0
- package/speechflow-cli/dst/{speechflow-node-t2t-openai.d.ts → speechflow-node-t2t-spellcheck.d.ts} +2 -2
- package/speechflow-cli/dst/{speechflow-node-t2t-openai.js → speechflow-node-t2t-spellcheck.js} +47 -99
- package/speechflow-cli/dst/speechflow-node-t2t-spellcheck.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +3 -6
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-summary.d.ts +16 -0
- package/speechflow-cli/dst/speechflow-node-t2t-summary.js +241 -0
- package/speechflow-cli/dst/speechflow-node-t2t-summary.js.map +1 -0
- package/speechflow-cli/dst/{speechflow-node-t2t-ollama.d.ts → speechflow-node-t2t-translate.d.ts} +2 -2
- package/speechflow-cli/dst/{speechflow-node-t2t-transformers.js → speechflow-node-t2t-translate.js} +53 -115
- package/speechflow-cli/dst/speechflow-node-t2t-translate.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-x2x-filter.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js +10 -0
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-device.js +3 -3
- package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-exec.d.ts +12 -0
- package/speechflow-cli/dst/speechflow-node-xio-exec.js +223 -0
- package/speechflow-cli/dst/speechflow-node-xio-exec.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-file.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-file.js +80 -67
- package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +2 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-vban.d.ts +17 -0
- package/speechflow-cli/dst/speechflow-node-xio-vban.js +330 -0
- package/speechflow-cli/dst/speechflow-node-xio-vban.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.d.ts +39 -0
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.js +500 -0
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js +2 -1
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-audio.js +5 -6
- package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-error.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-error.js +5 -7
- package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-llm.d.ts +35 -0
- package/speechflow-cli/dst/speechflow-util-llm.js +363 -0
- package/speechflow-cli/dst/speechflow-util-llm.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util-misc.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-misc.js +4 -4
- package/speechflow-cli/dst/speechflow-util-misc.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-queue.js +3 -3
- package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-stream.js +4 -2
- package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-util.js +1 -0
- package/speechflow-cli/dst/speechflow-util.js.map +1 -1
- package/speechflow-cli/etc/oxlint.jsonc +2 -1
- package/speechflow-cli/package.json +34 -17
- package/speechflow-cli/src/lib.d.ts +5 -0
- package/speechflow-cli/src/speechflow-main-api.ts +6 -5
- package/speechflow-cli/src/speechflow-main-graph.ts +40 -13
- package/speechflow-cli/src/speechflow-main-status.ts +4 -8
- package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +4 -0
- package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +4 -2
- package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-expander.ts +4 -2
- package/speechflow-cli/src/speechflow-node-a2a-gender.ts +2 -2
- package/speechflow-cli/src/speechflow-node-a2a-pitch.ts +1 -2
- package/speechflow-cli/src/speechflow-node-a2a-wav.ts +33 -6
- package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +6 -11
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +13 -12
- package/speechflow-cli/src/speechflow-node-a2t-google.ts +322 -0
- package/speechflow-cli/src/speechflow-node-a2t-openai.ts +8 -4
- package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +7 -11
- package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +6 -5
- package/speechflow-cli/src/speechflow-node-t2a-google.ts +206 -0
- package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +22 -6
- package/speechflow-cli/src/speechflow-node-t2a-openai.ts +179 -0
- package/speechflow-cli/src/speechflow-node-t2a-supertonic.ts +701 -0
- package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +2 -1
- package/speechflow-cli/src/speechflow-node-t2t-opus.ts +136 -0
- package/speechflow-cli/src/speechflow-node-t2t-profanity.ts +93 -0
- package/speechflow-cli/src/speechflow-node-t2t-punctuation.ts +201 -0
- package/speechflow-cli/src/{speechflow-node-t2t-openai.ts → speechflow-node-t2t-spellcheck.ts} +48 -107
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +3 -6
- package/speechflow-cli/src/speechflow-node-t2t-summary.ts +229 -0
- package/speechflow-cli/src/speechflow-node-t2t-translate.ts +181 -0
- package/speechflow-cli/src/speechflow-node-x2x-filter.ts +16 -3
- package/speechflow-cli/src/speechflow-node-x2x-trace.ts +3 -3
- package/speechflow-cli/src/speechflow-node-xio-device.ts +4 -7
- package/speechflow-cli/src/speechflow-node-xio-exec.ts +210 -0
- package/speechflow-cli/src/speechflow-node-xio-file.ts +93 -80
- package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +3 -2
- package/speechflow-cli/src/speechflow-node-xio-vban.ts +325 -0
- package/speechflow-cli/src/speechflow-node-xio-webrtc.ts +533 -0
- package/speechflow-cli/src/speechflow-node-xio-websocket.ts +2 -1
- package/speechflow-cli/src/speechflow-util-audio-wt.ts +4 -4
- package/speechflow-cli/src/speechflow-util-audio.ts +10 -10
- package/speechflow-cli/src/speechflow-util-error.ts +9 -7
- package/speechflow-cli/src/speechflow-util-llm.ts +367 -0
- package/speechflow-cli/src/speechflow-util-misc.ts +4 -4
- package/speechflow-cli/src/speechflow-util-queue.ts +4 -4
- package/speechflow-cli/src/speechflow-util-stream.ts +5 -3
- package/speechflow-cli/src/speechflow-util.ts +1 -0
- package/speechflow-ui-db/package.json +9 -9
- package/speechflow-ui-st/package.json +9 -9
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +0 -293
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +0 -1
- package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +0 -281
- 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
|
+
}
|