echogarden 0.11.12 → 0.11.13
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/data/schemas/options.json +16 -0
- package/dist/api/Alignment.js +2 -2
- package/dist/api/Alignment.js.map +1 -1
- package/dist/api/Recognition.js +2 -2
- package/dist/api/Recognition.js.map +1 -1
- package/dist/api/Synthesis.js +5 -4
- package/dist/api/Synthesis.js.map +1 -1
- package/dist/api/Translation.js +2 -2
- package/dist/api/Translation.js.map +1 -1
- package/dist/audio/AudioUtilities.d.ts +1 -0
- package/dist/audio/AudioUtilities.js +25 -7
- package/dist/audio/AudioUtilities.js.map +1 -1
- package/dist/cli/CLI.js +2 -2
- package/dist/cli/CLI.js.map +1 -1
- package/dist/recognition/WhisperSTT.js +2 -2
- package/dist/recognition/WhisperSTT.js.map +1 -1
- package/dist/subtitles/Subtitles.d.ts +10 -7
- package/dist/subtitles/Subtitles.js +268 -207
- package/dist/subtitles/Subtitles.js.map +1 -1
- package/docs/Options.md +4 -2
- package/package.json +7 -6
- package/src/alignment/DTWMfccSequenceAlignment.ts +43 -0
- package/src/alignment/DTWSequenceAlignment.ts +121 -0
- package/src/alignment/DTWSequenceAlignmentWindowed.ts +210 -0
- package/src/alignment/LevenshteinSequenceAlignment.ts +126 -0
- package/src/alignment/SpeechAlignment.ts +488 -0
- package/src/api/API.ts +12 -0
- package/src/api/APIOptions.ts +15 -0
- package/src/api/Alignment.ts +329 -0
- package/src/api/Common.ts +16 -0
- package/src/api/Denoising.ts +120 -0
- package/src/api/LanguageDetection.ts +286 -0
- package/src/api/Recognition.ts +344 -0
- package/src/api/Synthesis.ts +1735 -0
- package/src/api/Translation.ts +143 -0
- package/src/api/Vad.ts +172 -0
- package/src/audio/AudioBufferConversion.ts +248 -0
- package/src/audio/AudioPlayer.ts +358 -0
- package/src/audio/AudioRecorder.ts +91 -0
- package/src/audio/AudioUtilities.ts +392 -0
- package/src/audio/SoxPath.ts +24 -0
- package/src/cli/CLI.ts +1360 -0
- package/src/cli/CLIConfigFile.ts +91 -0
- package/src/cli/CLILauncher.ts +26 -0
- package/src/cli/CLIOptionsSchema.ts +54 -0
- package/src/cli/CLIParser.ts +41 -0
- package/src/cli/CLIStarter.ts +40 -0
- package/src/codecs/FFMpegTranscoder.ts +214 -0
- package/src/codecs/TIMITCodec.ts +17 -0
- package/src/codecs/WaveCodec.ts +260 -0
- package/src/denoising/RNNoise.ts +95 -0
- package/src/dsp/BiquadFilter.ts +488 -0
- package/src/dsp/FFT.ts +187 -0
- package/src/dsp/MFCC.ts +227 -0
- package/src/dsp/MelSpectogram.ts +145 -0
- package/src/dsp/Rubberband.ts +249 -0
- package/src/dsp/Sonic.ts +59 -0
- package/src/dsp/SpeexResampler.ts +79 -0
- package/src/math/VectorMath.ts +812 -0
- package/src/nlp/ChineseSegmentation.ts +68 -0
- package/src/nlp/CompromiseNLP.ts +113 -0
- package/src/nlp/EspeakPhonemizer.ts +168 -0
- package/src/nlp/IPA.ts +139 -0
- package/src/nlp/JapaneseSegmentation.ts +53 -0
- package/src/nlp/Lexicon.ts +119 -0
- package/src/nlp/PhoneConversion.ts +508 -0
- package/src/nlp/Segmentation.ts +237 -0
- package/src/nlp/TextNormalizer.ts +160 -0
- package/src/recognition/AmazonTranscribeSTT.ts +112 -0
- package/src/recognition/AzureCognitiveServicesSTT.ts +76 -0
- package/src/recognition/GoogleCloudSTT.ts +92 -0
- package/src/recognition/SileroSTT.ts +173 -0
- package/src/recognition/VoskSTT.ts +112 -0
- package/src/recognition/WhisperSTT.ts +1518 -0
- package/src/server/Client.ts +297 -0
- package/src/server/Server.ts +178 -0
- package/src/server/ServerStarter.ts +12 -0
- package/src/server/Worker.ts +400 -0
- package/src/server/WorkerStarter.ts +38 -0
- package/src/speech-language-detection/SileroLanguageDetection.ts +105 -0
- package/src/subtitles/Subtitles.ts +478 -0
- package/src/synthesis/AwsPollyTTS.ts +78 -0
- package/src/synthesis/AzureCognitiveServicesTTS.ts +146 -0
- package/src/synthesis/CoquiServerTTS.ts +29 -0
- package/src/synthesis/ElevenLabsTTS.ts +104 -0
- package/src/synthesis/EspeakTTS.ts +552 -0
- package/src/synthesis/FliteTTS.ts +387 -0
- package/src/synthesis/GoogleCloudTTS.ts +112 -0
- package/src/synthesis/GoogleTranslateTTS.ts +210 -0
- package/src/synthesis/MicrosoftEdgeTTS.ts +298 -0
- package/src/synthesis/SamTTS.ts +30 -0
- package/src/synthesis/SapiTTS.ts +222 -0
- package/src/synthesis/StreamlabsPollyTTS.ts +114 -0
- package/src/synthesis/SvoxPicoTTS.ts +318 -0
- package/src/synthesis/VitsTTS.ts +734 -0
- package/src/tests/Test.ts +24 -0
- package/src/text-language-detection/FastTextLanguageDetection.ts +53 -0
- package/src/text-language-detection/TinyLDLanguageDetection.ts +16 -0
- package/src/typings/Fillers.d.ts +41 -0
- package/src/utilities/BinaryArrayConversion.ts +159 -0
- package/src/utilities/Compression.ts +91 -0
- package/src/utilities/FileDownloader.ts +201 -0
- package/src/utilities/FileSystem.ts +265 -0
- package/src/utilities/Hashing.ts +230 -0
- package/src/utilities/Locale.ts +119 -0
- package/src/utilities/Logger.ts +72 -0
- package/src/utilities/NdArrayUtilities.ts +31 -0
- package/src/utilities/ObjectUtilities.ts +169 -0
- package/src/utilities/OpenPromise.ts +13 -0
- package/src/utilities/PackageManager.ts +97 -0
- package/src/utilities/Queue.ts +17 -0
- package/src/utilities/RandomGenerator.ts +237 -0
- package/src/utilities/SignalChannel.ts +22 -0
- package/src/utilities/TarballMaker.ts +68 -0
- package/src/utilities/Timeline.ts +231 -0
- package/src/utilities/Timer.ts +93 -0
- package/src/utilities/Utilities.ts +574 -0
- package/src/utilities/WasmMemoryManager.ts +516 -0
- package/src/utilities/WebReader.ts +55 -0
- package/src/utilities/WikipediaReader.ts +41 -0
- package/src/voice-activity-detection/SileroVAD.ts +86 -0
- package/src/voice-activity-detection/WebRtcVAD.ts +76 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { extendDeep } from "../utilities/ObjectUtilities.js"
|
|
2
|
+
|
|
3
|
+
import { logToStderr } from "../utilities/Utilities.js"
|
|
4
|
+
import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevel, trimAudioEnd } from "../audio/AudioUtilities.js"
|
|
5
|
+
import { Logger } from "../utilities/Logger.js"
|
|
6
|
+
|
|
7
|
+
import { Timeline, addWordTextOffsetsToTimeline, wordTimelineToSegmentSentenceTimeline } from "../utilities/Timeline.js"
|
|
8
|
+
import { whisperOptionsDefaults, type WhisperOptions } from "../recognition/WhisperSTT.js"
|
|
9
|
+
import { formatLanguageCodeWithName, getShortLanguageCode, normalizeLanguageCode } from "../utilities/Locale.js"
|
|
10
|
+
import { EngineMetadata } from "./Common.js"
|
|
11
|
+
import { SpeechLanguageDetectionOptions, detectSpeechLanguage } from "./API.js"
|
|
12
|
+
import chalk from "chalk"
|
|
13
|
+
import { SubtitlesConfig, defaultSubtitlesBaseConfig } from "../subtitles/Subtitles.js"
|
|
14
|
+
|
|
15
|
+
const log = logToStderr
|
|
16
|
+
|
|
17
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
18
|
+
// Speech translation
|
|
19
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
20
|
+
export async function translateSpeech(input: AudioSourceParam, options: SpeechTranslationOptions): Promise<SpeechTranslationResult> {
|
|
21
|
+
const logger = new Logger()
|
|
22
|
+
const startTimestamp = logger.getTimestamp()
|
|
23
|
+
|
|
24
|
+
logger.start("Prepare for speech translation")
|
|
25
|
+
|
|
26
|
+
const inputRawAudio = await ensureRawAudio(input)
|
|
27
|
+
|
|
28
|
+
let sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
|
|
29
|
+
sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
|
|
30
|
+
sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
|
|
31
|
+
|
|
32
|
+
options = extendDeep(defaultSpeechTranslationOptions, options)
|
|
33
|
+
|
|
34
|
+
if (!options.sourceLanguage) {
|
|
35
|
+
logger.start("No language specified. Detecting speech language")
|
|
36
|
+
const { detectedLanguage } = await detectSpeechLanguage(inputRawAudio, options.languageDetection || {})
|
|
37
|
+
|
|
38
|
+
logger.end()
|
|
39
|
+
logger.logTitledMessage('Language detected', formatLanguageCodeWithName(detectedLanguage))
|
|
40
|
+
|
|
41
|
+
options.sourceLanguage = detectedLanguage
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
logger.start("Preprocess audio for translation")
|
|
45
|
+
|
|
46
|
+
const engine = options.engine!
|
|
47
|
+
const sourceLanguage = normalizeLanguageCode(options.sourceLanguage!)
|
|
48
|
+
const targetLanguage = options.targetLanguage!
|
|
49
|
+
|
|
50
|
+
let transcript: string
|
|
51
|
+
let timeline: Timeline | undefined
|
|
52
|
+
|
|
53
|
+
logger.start(`Load ${engine} module`)
|
|
54
|
+
|
|
55
|
+
switch (engine) {
|
|
56
|
+
case "whisper": {
|
|
57
|
+
const WhisperSTT = await import("../recognition/WhisperSTT.js")
|
|
58
|
+
|
|
59
|
+
const whisperOptions = options.whisper!
|
|
60
|
+
|
|
61
|
+
const shortSourceLanguageCode = getShortLanguageCode(sourceLanguage)
|
|
62
|
+
const shortTargetLanguageCode = getShortLanguageCode(targetLanguage)
|
|
63
|
+
|
|
64
|
+
const { modelName, modelDir, tokenizerDir } = await WhisperSTT.loadPackagesAndGetPaths(whisperOptions.model, shortSourceLanguageCode)
|
|
65
|
+
|
|
66
|
+
if (shortTargetLanguageCode != "en") {
|
|
67
|
+
throw new Error("Whisper translation only supports English as target language")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (modelName.endsWith(".en")) {
|
|
71
|
+
throw new Error("Whisper translation tasks are only possible with a multilingual model")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (shortSourceLanguageCode == "en" && shortTargetLanguageCode == "en") {
|
|
75
|
+
throw new Error("Both translation source and target language are English")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
logger.end();
|
|
79
|
+
|
|
80
|
+
({ transcript, timeline } = await WhisperSTT.recognize(sourceRawAudio, modelName, modelDir, tokenizerDir, "translate", sourceLanguage, whisperOptions))
|
|
81
|
+
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
default: {
|
|
86
|
+
throw new Error(`Engine '${options.engine}' is not supported`)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
addWordTextOffsetsToTimeline(timeline, transcript)
|
|
91
|
+
|
|
92
|
+
const { segmentTimeline } = await wordTimelineToSegmentSentenceTimeline(timeline, transcript, targetLanguage, 'single', 'preserve')
|
|
93
|
+
|
|
94
|
+
logger.end()
|
|
95
|
+
logger.log('')
|
|
96
|
+
logger.logDuration(`Total speech translation time`, startTimestamp, chalk.magentaBright)
|
|
97
|
+
|
|
98
|
+
return { transcript, timeline: segmentTimeline, wordTimeline: timeline, sourceLanguage, targetLanguage, inputRawAudio }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface SpeechTranslationResult {
|
|
102
|
+
transcript: string
|
|
103
|
+
timeline: Timeline
|
|
104
|
+
wordTimeline: Timeline
|
|
105
|
+
sourceLanguage: string
|
|
106
|
+
targetLanguage: string
|
|
107
|
+
inputRawAudio: RawAudio
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type SpeechTranslationEngine = "whisper"
|
|
111
|
+
|
|
112
|
+
export interface SpeechTranslationOptions {
|
|
113
|
+
engine?: SpeechTranslationEngine
|
|
114
|
+
|
|
115
|
+
sourceLanguage?: string
|
|
116
|
+
targetLanguage?: string
|
|
117
|
+
languageDetection?: SpeechLanguageDetectionOptions
|
|
118
|
+
subtitles?: SubtitlesConfig
|
|
119
|
+
|
|
120
|
+
whisper?: WhisperOptions
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const defaultSpeechTranslationOptions: SpeechTranslationOptions = {
|
|
124
|
+
engine: "whisper",
|
|
125
|
+
|
|
126
|
+
sourceLanguage: undefined,
|
|
127
|
+
targetLanguage: "en",
|
|
128
|
+
|
|
129
|
+
languageDetection: undefined,
|
|
130
|
+
|
|
131
|
+
subtitles: defaultSubtitlesBaseConfig,
|
|
132
|
+
|
|
133
|
+
whisper: whisperOptionsDefaults,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const speechTranslationEngines: EngineMetadata[] = [
|
|
137
|
+
{
|
|
138
|
+
id: 'whisper',
|
|
139
|
+
name: 'OpenAI Whisper',
|
|
140
|
+
description: "Uses Whisper's speech translation capability to produce an English transcript from speech in a different language.",
|
|
141
|
+
type: 'local'
|
|
142
|
+
}
|
|
143
|
+
]
|
package/src/api/Vad.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { extendDeep } from "../utilities/ObjectUtilities.js"
|
|
2
|
+
|
|
3
|
+
import { logToStderr } from "../utilities/Utilities.js"
|
|
4
|
+
import { AudioSourceParam, RawAudio, ensureRawAudio, } from "../audio/AudioUtilities.js"
|
|
5
|
+
import { Logger } from "../utilities/Logger.js"
|
|
6
|
+
|
|
7
|
+
import { Timeline } from "../utilities/Timeline.js"
|
|
8
|
+
import path from "path"
|
|
9
|
+
import { loadPackage } from "../utilities/PackageManager.js"
|
|
10
|
+
import { EngineMetadata } from "./Common.js"
|
|
11
|
+
import chalk from "chalk"
|
|
12
|
+
|
|
13
|
+
const log = logToStderr
|
|
14
|
+
|
|
15
|
+
export async function detectVoiceActivity(input: AudioSourceParam, options: VADOptions): Promise<VADResult> {
|
|
16
|
+
const logger = new Logger()
|
|
17
|
+
const startTimestamp = logger.getTimestamp()
|
|
18
|
+
|
|
19
|
+
logger.start("Prepare for voice activity detection")
|
|
20
|
+
|
|
21
|
+
const inputRawAudio = await ensureRawAudio(input)
|
|
22
|
+
|
|
23
|
+
let sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
|
|
24
|
+
|
|
25
|
+
options = extendDeep(defaultVADOptions, options)
|
|
26
|
+
|
|
27
|
+
logger.start(`Detect voice activity with ${options.engine}`)
|
|
28
|
+
|
|
29
|
+
let frameDurationSeconds: number
|
|
30
|
+
let frameProbabilities: number[]
|
|
31
|
+
|
|
32
|
+
switch (options.engine) {
|
|
33
|
+
case "webrtc": {
|
|
34
|
+
const WebRtcVAD = await import("../voice-activity-detection/WebRtcVAD.js")
|
|
35
|
+
|
|
36
|
+
const webrtcOptions = options.webrtc!
|
|
37
|
+
|
|
38
|
+
frameProbabilities = await WebRtcVAD.detectVoiceActivity(sourceRawAudio, webrtcOptions.frameDuration!)
|
|
39
|
+
frameDurationSeconds = webrtcOptions.frameDuration! / 1000
|
|
40
|
+
|
|
41
|
+
break
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
case "silero": {
|
|
45
|
+
const SileroVAD = await import("../voice-activity-detection/SileroVAD.js")
|
|
46
|
+
|
|
47
|
+
const sileroOptions = options.silero!
|
|
48
|
+
|
|
49
|
+
const modelDir = await loadPackage("silero-vad")
|
|
50
|
+
|
|
51
|
+
const modelPath = path.join(modelDir, "silero-vad.onnx")
|
|
52
|
+
const frameDuration = sileroOptions.frameDuration!
|
|
53
|
+
|
|
54
|
+
frameProbabilities = await SileroVAD.detectVoiceActivity(sourceRawAudio, modelPath, frameDuration)
|
|
55
|
+
frameDurationSeconds = sileroOptions.frameDuration! / 1000
|
|
56
|
+
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case "rnnoise": {
|
|
61
|
+
const RNNoise = await import("../denoising/RNNoise.js")
|
|
62
|
+
|
|
63
|
+
const rnnoiseOptions = options.rnnoise!
|
|
64
|
+
|
|
65
|
+
const { denoisedRawAudio, frameVadProbabilities } = await RNNoise.denoiseAudio(sourceRawAudio)
|
|
66
|
+
|
|
67
|
+
frameDurationSeconds = 0.01
|
|
68
|
+
frameProbabilities = frameVadProbabilities
|
|
69
|
+
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
default: {
|
|
74
|
+
throw new Error(`Engine '${options.engine}' is not supported`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const timeline: Timeline = []
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < frameProbabilities.length; i++) {
|
|
81
|
+
const frameProbability = frameProbabilities[i]
|
|
82
|
+
|
|
83
|
+
const startTime = i * frameDurationSeconds
|
|
84
|
+
const endTime = (i + 1) * frameDurationSeconds
|
|
85
|
+
|
|
86
|
+
if (frameProbability >= options.activityThreshold!) {
|
|
87
|
+
if (timeline.length == 0 || timeline[timeline.length - 1].text == "nonspeech") {
|
|
88
|
+
timeline.push({ type: "segment", text: "speech", startTime, endTime })
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
if (timeline.length == 0 || timeline[timeline.length - 1].text == "speech") {
|
|
93
|
+
timeline.push({ type: "segment", text: "nonspeech", startTime, endTime })
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
timeline[timeline.length - 1].endTime = endTime
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
logger.end()
|
|
102
|
+
logger.log('')
|
|
103
|
+
logger.logDuration(`Total voice activity detection time`, startTimestamp, chalk.magentaBright)
|
|
104
|
+
|
|
105
|
+
return { timeline, inputRawAudio }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface VADResult {
|
|
109
|
+
timeline: Timeline
|
|
110
|
+
inputRawAudio: RawAudio
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type VADEngine = "webrtc" | "silero" | "rnnoise"
|
|
114
|
+
|
|
115
|
+
export interface VADOptions {
|
|
116
|
+
engine?: VADEngine
|
|
117
|
+
|
|
118
|
+
activityThreshold?: number
|
|
119
|
+
|
|
120
|
+
webrtc?: {
|
|
121
|
+
frameDuration?: 10 | 20 | 30
|
|
122
|
+
mode?: 0 | 1 | 2 | 3
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
silero?: {
|
|
126
|
+
modelPath?: string
|
|
127
|
+
frameDuration?: 30 | 60 | 90
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
rnnoise?: {
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const defaultVADOptions: VADOptions = {
|
|
135
|
+
engine: "webrtc",
|
|
136
|
+
|
|
137
|
+
activityThreshold: 0.5,
|
|
138
|
+
|
|
139
|
+
webrtc: {
|
|
140
|
+
frameDuration: 30,
|
|
141
|
+
mode: 1
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
silero: {
|
|
145
|
+
modelPath: undefined,
|
|
146
|
+
frameDuration: 90,
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
rnnoise: {
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const vadEngines: EngineMetadata[] = [
|
|
154
|
+
{
|
|
155
|
+
id: 'webrtc',
|
|
156
|
+
name: 'WebRTC VAD',
|
|
157
|
+
description: 'A voice activity detector from the Chromium browser sources.',
|
|
158
|
+
type: 'local'
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'silero',
|
|
162
|
+
name: 'Silero VAD',
|
|
163
|
+
description: 'A voice activity detection model by Silero.',
|
|
164
|
+
type: 'local'
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: 'rnnoise',
|
|
168
|
+
name: 'RNNoise',
|
|
169
|
+
description: "Uses RNNoise's speech probabilities as VAD metrics.",
|
|
170
|
+
type: 'local'
|
|
171
|
+
}
|
|
172
|
+
]
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import AlawMulaw from "alawmulaw"
|
|
2
|
+
import * as BinaryArrayConversion from '../utilities/BinaryArrayConversion.js'
|
|
3
|
+
import { BitDepth, SampleFormat } from "../codecs/WaveCodec.js"
|
|
4
|
+
|
|
5
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
// Low level audio sample conversions
|
|
7
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
8
|
+
export function encodeToAudioBuffer(audioChannels: Float32Array[], targetBitDepth: BitDepth = 16, targetSampleFormat: SampleFormat = SampleFormat.PCM) {
|
|
9
|
+
const interleavedChannels = interleaveChannels(audioChannels)
|
|
10
|
+
|
|
11
|
+
if (targetSampleFormat == SampleFormat.PCM) {
|
|
12
|
+
if (targetBitDepth == 8) {
|
|
13
|
+
return Buffer.from(float32ToUint8Pcm(interleavedChannels).buffer)
|
|
14
|
+
} else if (targetBitDepth == 16) {
|
|
15
|
+
return BinaryArrayConversion.int16ToBufferLE(float32ToInt16Pcm(interleavedChannels))
|
|
16
|
+
} else if (targetBitDepth == 24) {
|
|
17
|
+
return BinaryArrayConversion.int24ToBufferLE(float32ToInt24Pcm(interleavedChannels))
|
|
18
|
+
} else if (targetBitDepth == 32) {
|
|
19
|
+
return BinaryArrayConversion.int32ToBufferLE(float32ToInt32Pcm(interleavedChannels))
|
|
20
|
+
} else {
|
|
21
|
+
throw new Error(`Unsupported PCM bit depth: ${targetBitDepth}`)
|
|
22
|
+
}
|
|
23
|
+
} else if (targetSampleFormat == SampleFormat.Float) {
|
|
24
|
+
if (targetBitDepth == 32) {
|
|
25
|
+
return BinaryArrayConversion.float32ToBufferLE(interleavedChannels)
|
|
26
|
+
} else if (targetBitDepth == 64) {
|
|
27
|
+
return BinaryArrayConversion.float64ToBufferLE(BinaryArrayConversion.float32Tofloat64(interleavedChannels))
|
|
28
|
+
} else {
|
|
29
|
+
throw new Error(`Unsupported float bit depth: ${targetBitDepth}`)
|
|
30
|
+
}
|
|
31
|
+
} else if (targetSampleFormat == SampleFormat.Alaw) {
|
|
32
|
+
if (targetBitDepth == 8) {
|
|
33
|
+
return Buffer.from(AlawMulaw.alaw.encode(float32ToInt16Pcm(interleavedChannels)))
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error(`Unsupported alaw bit depth: ${targetBitDepth}`)
|
|
36
|
+
}
|
|
37
|
+
} else if (targetSampleFormat == SampleFormat.Mulaw) {
|
|
38
|
+
if (targetBitDepth == 8) {
|
|
39
|
+
return Buffer.from(AlawMulaw.mulaw.encode(float32ToInt16Pcm(interleavedChannels)))
|
|
40
|
+
} else {
|
|
41
|
+
throw new Error(`Unsupported mulaw bit depth: ${targetBitDepth}`)
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
throw new Error(`Unsupported audio format: ${targetSampleFormat}`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function decodeToChannels(audioBuffer: Buffer, channelCount: number, sourceBitDepth: number, sourceSampleFormat: SampleFormat) {
|
|
49
|
+
let interleavedChannels: Float32Array
|
|
50
|
+
|
|
51
|
+
if (sourceSampleFormat == SampleFormat.PCM) {
|
|
52
|
+
if (sourceBitDepth == 8) {
|
|
53
|
+
interleavedChannels = uint8PcmToFloat32(audioBuffer)
|
|
54
|
+
} else if (sourceBitDepth == 16) {
|
|
55
|
+
interleavedChannels = int16PcmToFloat32(BinaryArrayConversion.bufferLEToInt16(audioBuffer))
|
|
56
|
+
} else if (sourceBitDepth == 24) {
|
|
57
|
+
interleavedChannels = int24PcmToFloat32(BinaryArrayConversion.bufferLEToInt24(audioBuffer))
|
|
58
|
+
} else if (sourceBitDepth == 32) {
|
|
59
|
+
interleavedChannels = int32PcmToFloat32(BinaryArrayConversion.bufferLEToInt32(audioBuffer))
|
|
60
|
+
} else {
|
|
61
|
+
throw new Error(`Unsupported PCM bit depth: ${sourceBitDepth}`)
|
|
62
|
+
}
|
|
63
|
+
} else if (sourceSampleFormat == SampleFormat.Float) {
|
|
64
|
+
if (sourceBitDepth == 32) {
|
|
65
|
+
interleavedChannels = BinaryArrayConversion.bufferLEToFloat32(audioBuffer)
|
|
66
|
+
} else if (sourceBitDepth == 64) {
|
|
67
|
+
interleavedChannels = BinaryArrayConversion.float64Tofloat32(BinaryArrayConversion.bufferLEToFloat64(audioBuffer))
|
|
68
|
+
} else {
|
|
69
|
+
throw new Error(`Unsupported float bit depth: ${sourceBitDepth}`)
|
|
70
|
+
}
|
|
71
|
+
} else if (sourceSampleFormat == SampleFormat.Alaw) {
|
|
72
|
+
if (sourceBitDepth == 8) {
|
|
73
|
+
interleavedChannels = int16PcmToFloat32(AlawMulaw.alaw.decode(audioBuffer))
|
|
74
|
+
} else {
|
|
75
|
+
throw new Error(`Unsupported alaw bit depth: ${sourceBitDepth}`)
|
|
76
|
+
}
|
|
77
|
+
} else if (sourceSampleFormat == SampleFormat.Mulaw) {
|
|
78
|
+
if (sourceBitDepth == 8) {
|
|
79
|
+
interleavedChannels = int16PcmToFloat32(AlawMulaw.mulaw.decode(audioBuffer))
|
|
80
|
+
} else {
|
|
81
|
+
throw new Error(`Unsupported mulaw bit depth: ${sourceBitDepth}`)
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
throw new Error(`Unsupported audio format: ${sourceSampleFormat}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return deInterleaveChannels(interleavedChannels, channelCount)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Int8 PCM <-> Float32 conversion
|
|
91
|
+
export function uint8PcmToFloat32(input: Uint8Array) {
|
|
92
|
+
const output = new Float32Array(input.length)
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < input.length; i++) {
|
|
95
|
+
const sample = input[i] - 128
|
|
96
|
+
output[i] = sample < 0 ? sample / 128 : sample / 127
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return output
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function float32ToUint8Pcm(input: Float32Array) {
|
|
103
|
+
const output = new Uint8Array(input.length)
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < input.length; i++) {
|
|
106
|
+
const sample = clampFloatSample(input[i])
|
|
107
|
+
output[i] = ((sample < 0 ? sample * 128 : sample * 127) | 0) + 128
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return output
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Int16 PCM <-> Float32 conversion
|
|
114
|
+
export function int16PcmToFloat32(input: Int16Array) {
|
|
115
|
+
const output = new Float32Array(input.length)
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < input.length; i++) {
|
|
118
|
+
const sample = input[i]
|
|
119
|
+
output[i] = sample < 0 ? sample / 32768 : sample / 32767
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return output
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function float32ToInt16Pcm(input: Float32Array) {
|
|
126
|
+
const output = new Int16Array(input.length)
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < input.length; i++) {
|
|
129
|
+
const sample = clampFloatSample(input[i])
|
|
130
|
+
output[i] = (sample < 0 ? sample * 32768 : sample * 32767) | 0
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return output
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Int24 PCM <-> Float32 conversion (uses int32 for storage)
|
|
137
|
+
export function int24PcmToFloat32(input: Int32Array) {
|
|
138
|
+
const output = new Float32Array(input.length)
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < input.length; i++) {
|
|
141
|
+
const sample = input[i]
|
|
142
|
+
output[i] = sample < 0 ? sample / 8388608 : sample / 8388607
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return output
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function float32ToInt24Pcm(input: Float32Array) {
|
|
149
|
+
const output = new Int32Array(input.length)
|
|
150
|
+
|
|
151
|
+
for (let i = 0; i < input.length; i++) {
|
|
152
|
+
const sample = clampFloatSample(input[i])
|
|
153
|
+
output[i] = (sample < 0 ? sample * 8388608 : sample * 8388607) | 0
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return output
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Int32 PCM <-> Float32 conversion
|
|
160
|
+
export function int32PcmToFloat32(input: Int32Array) {
|
|
161
|
+
const output = new Float32Array(input.length)
|
|
162
|
+
|
|
163
|
+
for (let i = 0; i < input.length; i++) {
|
|
164
|
+
const sample = input[i]
|
|
165
|
+
output[i] = sample < 0 ? sample / 2147483648 : sample / 2147483647
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return output
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function float32ToInt32Pcm(input: Float32Array) {
|
|
172
|
+
const output = new Int32Array(input.length)
|
|
173
|
+
|
|
174
|
+
for (let i = 0; i < input.length; i++) {
|
|
175
|
+
const sample = clampFloatSample(input[i])
|
|
176
|
+
output[i] = (sample < 0 ? sample * 2147483648 : sample * 2147483647) | 0
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return output
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
183
|
+
// Channel interleaving
|
|
184
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
185
|
+
export function interleaveChannels(channels: Float32Array[]) {
|
|
186
|
+
const channelCount = channels.length
|
|
187
|
+
|
|
188
|
+
if (channelCount == 0) {
|
|
189
|
+
throw new Error("Empty channel array received")
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (channelCount == 1) {
|
|
193
|
+
return channels[0]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const sampleCount = channels[0].length
|
|
197
|
+
const result = new Float32Array(sampleCount * channelCount)
|
|
198
|
+
|
|
199
|
+
let writeIndex = 0
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
202
|
+
for (let c = 0; c < channelCount; c++) {
|
|
203
|
+
result[writeIndex] = channels[c][i]
|
|
204
|
+
writeIndex += 1
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return result
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function deInterleaveChannels(interleavedChannels: Float32Array, channelCount: number) {
|
|
212
|
+
if (channelCount == 0) {
|
|
213
|
+
throw new Error("0 channel count received")
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (channelCount == 1) {
|
|
217
|
+
return [interleavedChannels]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (interleavedChannels.length % channelCount != 0) {
|
|
221
|
+
throw new Error(`Size of interleaved channels (${interleaveChannels.length}) is not a multiple of channel count (${channelCount})`)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const sampleCount = interleavedChannels.length / channelCount
|
|
225
|
+
const channels: Float32Array[] = []
|
|
226
|
+
|
|
227
|
+
for (let i = 0; i < channelCount; i++) {
|
|
228
|
+
channels.push(new Float32Array(sampleCount))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let readIndex = 0
|
|
232
|
+
|
|
233
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
234
|
+
for (let c = 0; c < channelCount; c++) {
|
|
235
|
+
channels[c][i] = interleavedChannels[readIndex]
|
|
236
|
+
readIndex += 1
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return channels
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
244
|
+
// Utilities
|
|
245
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
246
|
+
export function clampFloatSample(floatSample: number) {
|
|
247
|
+
return Math.max(-1, Math.min(floatSample, 1))
|
|
248
|
+
}
|