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.
Files changed (122) hide show
  1. package/data/schemas/options.json +16 -0
  2. package/dist/api/Alignment.js +2 -2
  3. package/dist/api/Alignment.js.map +1 -1
  4. package/dist/api/Recognition.js +2 -2
  5. package/dist/api/Recognition.js.map +1 -1
  6. package/dist/api/Synthesis.js +5 -4
  7. package/dist/api/Synthesis.js.map +1 -1
  8. package/dist/api/Translation.js +2 -2
  9. package/dist/api/Translation.js.map +1 -1
  10. package/dist/audio/AudioUtilities.d.ts +1 -0
  11. package/dist/audio/AudioUtilities.js +25 -7
  12. package/dist/audio/AudioUtilities.js.map +1 -1
  13. package/dist/cli/CLI.js +2 -2
  14. package/dist/cli/CLI.js.map +1 -1
  15. package/dist/recognition/WhisperSTT.js +2 -2
  16. package/dist/recognition/WhisperSTT.js.map +1 -1
  17. package/dist/subtitles/Subtitles.d.ts +10 -7
  18. package/dist/subtitles/Subtitles.js +268 -207
  19. package/dist/subtitles/Subtitles.js.map +1 -1
  20. package/docs/Options.md +4 -2
  21. package/package.json +7 -6
  22. package/src/alignment/DTWMfccSequenceAlignment.ts +43 -0
  23. package/src/alignment/DTWSequenceAlignment.ts +121 -0
  24. package/src/alignment/DTWSequenceAlignmentWindowed.ts +210 -0
  25. package/src/alignment/LevenshteinSequenceAlignment.ts +126 -0
  26. package/src/alignment/SpeechAlignment.ts +488 -0
  27. package/src/api/API.ts +12 -0
  28. package/src/api/APIOptions.ts +15 -0
  29. package/src/api/Alignment.ts +329 -0
  30. package/src/api/Common.ts +16 -0
  31. package/src/api/Denoising.ts +120 -0
  32. package/src/api/LanguageDetection.ts +286 -0
  33. package/src/api/Recognition.ts +344 -0
  34. package/src/api/Synthesis.ts +1735 -0
  35. package/src/api/Translation.ts +143 -0
  36. package/src/api/Vad.ts +172 -0
  37. package/src/audio/AudioBufferConversion.ts +248 -0
  38. package/src/audio/AudioPlayer.ts +358 -0
  39. package/src/audio/AudioRecorder.ts +91 -0
  40. package/src/audio/AudioUtilities.ts +392 -0
  41. package/src/audio/SoxPath.ts +24 -0
  42. package/src/cli/CLI.ts +1360 -0
  43. package/src/cli/CLIConfigFile.ts +91 -0
  44. package/src/cli/CLILauncher.ts +26 -0
  45. package/src/cli/CLIOptionsSchema.ts +54 -0
  46. package/src/cli/CLIParser.ts +41 -0
  47. package/src/cli/CLIStarter.ts +40 -0
  48. package/src/codecs/FFMpegTranscoder.ts +214 -0
  49. package/src/codecs/TIMITCodec.ts +17 -0
  50. package/src/codecs/WaveCodec.ts +260 -0
  51. package/src/denoising/RNNoise.ts +95 -0
  52. package/src/dsp/BiquadFilter.ts +488 -0
  53. package/src/dsp/FFT.ts +187 -0
  54. package/src/dsp/MFCC.ts +227 -0
  55. package/src/dsp/MelSpectogram.ts +145 -0
  56. package/src/dsp/Rubberband.ts +249 -0
  57. package/src/dsp/Sonic.ts +59 -0
  58. package/src/dsp/SpeexResampler.ts +79 -0
  59. package/src/math/VectorMath.ts +812 -0
  60. package/src/nlp/ChineseSegmentation.ts +68 -0
  61. package/src/nlp/CompromiseNLP.ts +113 -0
  62. package/src/nlp/EspeakPhonemizer.ts +168 -0
  63. package/src/nlp/IPA.ts +139 -0
  64. package/src/nlp/JapaneseSegmentation.ts +53 -0
  65. package/src/nlp/Lexicon.ts +119 -0
  66. package/src/nlp/PhoneConversion.ts +508 -0
  67. package/src/nlp/Segmentation.ts +237 -0
  68. package/src/nlp/TextNormalizer.ts +160 -0
  69. package/src/recognition/AmazonTranscribeSTT.ts +112 -0
  70. package/src/recognition/AzureCognitiveServicesSTT.ts +76 -0
  71. package/src/recognition/GoogleCloudSTT.ts +92 -0
  72. package/src/recognition/SileroSTT.ts +173 -0
  73. package/src/recognition/VoskSTT.ts +112 -0
  74. package/src/recognition/WhisperSTT.ts +1518 -0
  75. package/src/server/Client.ts +297 -0
  76. package/src/server/Server.ts +178 -0
  77. package/src/server/ServerStarter.ts +12 -0
  78. package/src/server/Worker.ts +400 -0
  79. package/src/server/WorkerStarter.ts +38 -0
  80. package/src/speech-language-detection/SileroLanguageDetection.ts +105 -0
  81. package/src/subtitles/Subtitles.ts +478 -0
  82. package/src/synthesis/AwsPollyTTS.ts +78 -0
  83. package/src/synthesis/AzureCognitiveServicesTTS.ts +146 -0
  84. package/src/synthesis/CoquiServerTTS.ts +29 -0
  85. package/src/synthesis/ElevenLabsTTS.ts +104 -0
  86. package/src/synthesis/EspeakTTS.ts +552 -0
  87. package/src/synthesis/FliteTTS.ts +387 -0
  88. package/src/synthesis/GoogleCloudTTS.ts +112 -0
  89. package/src/synthesis/GoogleTranslateTTS.ts +210 -0
  90. package/src/synthesis/MicrosoftEdgeTTS.ts +298 -0
  91. package/src/synthesis/SamTTS.ts +30 -0
  92. package/src/synthesis/SapiTTS.ts +222 -0
  93. package/src/synthesis/StreamlabsPollyTTS.ts +114 -0
  94. package/src/synthesis/SvoxPicoTTS.ts +318 -0
  95. package/src/synthesis/VitsTTS.ts +734 -0
  96. package/src/tests/Test.ts +24 -0
  97. package/src/text-language-detection/FastTextLanguageDetection.ts +53 -0
  98. package/src/text-language-detection/TinyLDLanguageDetection.ts +16 -0
  99. package/src/typings/Fillers.d.ts +41 -0
  100. package/src/utilities/BinaryArrayConversion.ts +159 -0
  101. package/src/utilities/Compression.ts +91 -0
  102. package/src/utilities/FileDownloader.ts +201 -0
  103. package/src/utilities/FileSystem.ts +265 -0
  104. package/src/utilities/Hashing.ts +230 -0
  105. package/src/utilities/Locale.ts +119 -0
  106. package/src/utilities/Logger.ts +72 -0
  107. package/src/utilities/NdArrayUtilities.ts +31 -0
  108. package/src/utilities/ObjectUtilities.ts +169 -0
  109. package/src/utilities/OpenPromise.ts +13 -0
  110. package/src/utilities/PackageManager.ts +97 -0
  111. package/src/utilities/Queue.ts +17 -0
  112. package/src/utilities/RandomGenerator.ts +237 -0
  113. package/src/utilities/SignalChannel.ts +22 -0
  114. package/src/utilities/TarballMaker.ts +68 -0
  115. package/src/utilities/Timeline.ts +231 -0
  116. package/src/utilities/Timer.ts +93 -0
  117. package/src/utilities/Utilities.ts +574 -0
  118. package/src/utilities/WasmMemoryManager.ts +516 -0
  119. package/src/utilities/WebReader.ts +55 -0
  120. package/src/utilities/WikipediaReader.ts +41 -0
  121. package/src/voice-activity-detection/SileroVAD.ts +86 -0
  122. 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
+ }