@mingxy/ocosay 1.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.
Files changed (126) hide show
  1. package/README.md +556 -0
  2. package/TECH_PLAN.md +352 -0
  3. package/__mocks__/@opencode-ai/plugin.ts +32 -0
  4. package/dist/config.d.ts +26 -0
  5. package/dist/config.d.ts.map +1 -0
  6. package/dist/config.js +95 -0
  7. package/dist/config.js.map +1 -0
  8. package/dist/core/backends/afplay-backend.d.ts +33 -0
  9. package/dist/core/backends/afplay-backend.d.ts.map +1 -0
  10. package/dist/core/backends/afplay-backend.js +144 -0
  11. package/dist/core/backends/afplay-backend.js.map +1 -0
  12. package/dist/core/backends/aplay-backend.d.ts +33 -0
  13. package/dist/core/backends/aplay-backend.d.ts.map +1 -0
  14. package/dist/core/backends/aplay-backend.js +142 -0
  15. package/dist/core/backends/aplay-backend.js.map +1 -0
  16. package/dist/core/backends/base.d.ts +94 -0
  17. package/dist/core/backends/base.d.ts.map +1 -0
  18. package/dist/core/backends/base.js +6 -0
  19. package/dist/core/backends/base.js.map +1 -0
  20. package/dist/core/backends/index.d.ts +29 -0
  21. package/dist/core/backends/index.d.ts.map +1 -0
  22. package/dist/core/backends/index.js +114 -0
  23. package/dist/core/backends/index.js.map +1 -0
  24. package/dist/core/backends/naudiodon-backend.d.ts +52 -0
  25. package/dist/core/backends/naudiodon-backend.d.ts.map +1 -0
  26. package/dist/core/backends/naudiodon-backend.js +123 -0
  27. package/dist/core/backends/naudiodon-backend.js.map +1 -0
  28. package/dist/core/backends/powershell-backend.d.ts +34 -0
  29. package/dist/core/backends/powershell-backend.d.ts.map +1 -0
  30. package/dist/core/backends/powershell-backend.js +154 -0
  31. package/dist/core/backends/powershell-backend.js.map +1 -0
  32. package/dist/core/player.d.ts +97 -0
  33. package/dist/core/player.d.ts.map +1 -0
  34. package/dist/core/player.js +268 -0
  35. package/dist/core/player.js.map +1 -0
  36. package/dist/core/speaker.d.ts +97 -0
  37. package/dist/core/speaker.d.ts.map +1 -0
  38. package/dist/core/speaker.js +218 -0
  39. package/dist/core/speaker.js.map +1 -0
  40. package/dist/core/stream-player.d.ts +107 -0
  41. package/dist/core/stream-player.d.ts.map +1 -0
  42. package/dist/core/stream-player.js +272 -0
  43. package/dist/core/stream-player.js.map +1 -0
  44. package/dist/core/stream-reader.d.ts +86 -0
  45. package/dist/core/stream-reader.d.ts.map +1 -0
  46. package/dist/core/stream-reader.js +172 -0
  47. package/dist/core/stream-reader.js.map +1 -0
  48. package/dist/core/streaming-synthesizer.d.ts +51 -0
  49. package/dist/core/streaming-synthesizer.d.ts.map +1 -0
  50. package/dist/core/streaming-synthesizer.js +103 -0
  51. package/dist/core/streaming-synthesizer.js.map +1 -0
  52. package/dist/core/types.d.ts +141 -0
  53. package/dist/core/types.d.ts.map +1 -0
  54. package/dist/core/types.js +37 -0
  55. package/dist/core/types.js.map +1 -0
  56. package/dist/index.d.ts +40 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +179 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/plugin.d.ts +4 -0
  61. package/dist/plugin.d.ts.map +1 -0
  62. package/dist/plugin.js +151 -0
  63. package/dist/plugin.js.map +1 -0
  64. package/dist/providers/base.d.ts +55 -0
  65. package/dist/providers/base.d.ts.map +1 -0
  66. package/dist/providers/base.js +95 -0
  67. package/dist/providers/base.js.map +1 -0
  68. package/dist/providers/minimax.d.ts +84 -0
  69. package/dist/providers/minimax.d.ts.map +1 -0
  70. package/dist/providers/minimax.js +387 -0
  71. package/dist/providers/minimax.js.map +1 -0
  72. package/dist/tools/tts.d.ts +147 -0
  73. package/dist/tools/tts.d.ts.map +1 -0
  74. package/dist/tools/tts.js +232 -0
  75. package/dist/tools/tts.js.map +1 -0
  76. package/jest.config.js +15 -0
  77. package/package.json +49 -0
  78. package/src/config.ts +121 -0
  79. package/src/core/backends/afplay-backend.ts +162 -0
  80. package/src/core/backends/aplay-backend.ts +160 -0
  81. package/src/core/backends/base.ts +117 -0
  82. package/src/core/backends/index.ts +128 -0
  83. package/src/core/backends/naudiodon-backend.ts +164 -0
  84. package/src/core/backends/powershell-backend.ts +173 -0
  85. package/src/core/player.ts +322 -0
  86. package/src/core/speaker.ts +283 -0
  87. package/src/core/stream-player.ts +326 -0
  88. package/src/core/stream-reader.ts +190 -0
  89. package/src/core/streaming-synthesizer.ts +123 -0
  90. package/src/core/types.ts +185 -0
  91. package/src/index.ts +233 -0
  92. package/src/plugin.ts +166 -0
  93. package/src/providers/base.ts +150 -0
  94. package/src/providers/minimax.ts +515 -0
  95. package/src/tools/tts.ts +277 -0
  96. package/src/types/naudiodon.d.ts +19 -0
  97. package/tests/__mocks__/@opencode-ai/plugin.ts +32 -0
  98. package/tests/backends.test.ts +831 -0
  99. package/tests/index.test.ts +201 -0
  100. package/tests/integration-test.d.ts +6 -0
  101. package/tests/integration-test.d.ts.map +1 -0
  102. package/tests/integration-test.js +84 -0
  103. package/tests/integration-test.js.map +1 -0
  104. package/tests/integration-test.ts +93 -0
  105. package/tests/p1-fixes.test.ts +160 -0
  106. package/tests/plugin.test.ts +311 -0
  107. package/tests/provider.test.d.ts +2 -0
  108. package/tests/provider.test.d.ts.map +1 -0
  109. package/tests/provider.test.js +69 -0
  110. package/tests/provider.test.js.map +1 -0
  111. package/tests/provider.test.ts +87 -0
  112. package/tests/speaker.test.d.ts +2 -0
  113. package/tests/speaker.test.d.ts.map +1 -0
  114. package/tests/speaker.test.js +63 -0
  115. package/tests/speaker.test.js.map +1 -0
  116. package/tests/speaker.test.ts +232 -0
  117. package/tests/stream-player.test.ts +303 -0
  118. package/tests/stream-reader.test.ts +269 -0
  119. package/tests/streaming-synthesizer.test.ts +225 -0
  120. package/tests/tts-tools.test.ts +270 -0
  121. package/tests/types.test.d.ts +2 -0
  122. package/tests/types.test.d.ts.map +1 -0
  123. package/tests/types.test.js +61 -0
  124. package/tests/types.test.js.map +1 -0
  125. package/tests/types.test.ts +63 -0
  126. package/tsconfig.json +22 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Aplay Backend - Linux 平台音频播放后端
3
+ * 使用 ALSA 的 aplay 命令
4
+ */
5
+
6
+ import { execFile, ChildProcess } from 'child_process'
7
+ import { AudioBackend, AudioBackendEvents, BackendOptions } from './base'
8
+ import { tmpdir } from 'os'
9
+ import { join } from 'path'
10
+ import { writeFileSync, unlinkSync, existsSync } from 'fs'
11
+
12
+ // 白名单:只允许特定路径格式(禁止 - 防止命令注入)
13
+ const SAFE_PATH_REGEX = /^[\w\/\.]+$/
14
+
15
+ /**
16
+ * AplayBackend - Linux ALSA 音频播放后端
17
+ * 不支持真正的流式播放,需要先将数据写入临时文件
18
+ */
19
+ export class AplayBackend implements AudioBackend {
20
+ readonly name = 'aplay'
21
+ readonly supportsStreaming = false
22
+
23
+ private process?: ChildProcess
24
+ private tempFile?: string
25
+ private events?: AudioBackendEvents
26
+ private _started = false
27
+ private _paused = false
28
+ private _stopped = false
29
+ // P0-4: 缓冲所有chunk,等end()时一次性写入文件
30
+ private chunks: Buffer[] = []
31
+ private hasEnded = false
32
+
33
+ constructor(options: BackendOptions = {}) {
34
+ this.events = options.events
35
+ }
36
+
37
+ start(filePath: string): void {
38
+ if (this._started) return
39
+
40
+ if (!SAFE_PATH_REGEX.test(filePath)) {
41
+ throw new Error(`Invalid file path: ${filePath}`)
42
+ }
43
+
44
+ this.tempFile = filePath
45
+ this._started = true
46
+ this._stopped = false
47
+
48
+ this.events?.onStart?.()
49
+
50
+ // 启动播放进程
51
+ this.process = execFile('aplay', [filePath], (error) => {
52
+ if (this._stopped) return
53
+
54
+ if (error) {
55
+ this.handleError(error)
56
+ return
57
+ }
58
+
59
+ this._started = false
60
+ this.events?.onEnd?.()
61
+ })
62
+
63
+ this.process.on('error', (error) => {
64
+ this.handleError(error)
65
+ })
66
+ }
67
+
68
+ write(chunk: Buffer): void {
69
+ if (this._stopped) return
70
+ // P0-4: 缓冲所有chunk,等end()时一次性写入
71
+ this.chunks.push(chunk)
72
+ }
73
+
74
+ end(): void {
75
+ if (this._stopped || this.hasEnded) return
76
+ this.hasEnded = true
77
+
78
+ if (this.chunks.length === 0) return
79
+
80
+ // P0-4: 所有chunk缓冲完毕后,一次性写入文件并播放
81
+ this.tempFile = join(tmpdir(), `ocosay-${Date.now()}.wav`)
82
+ writeFileSync(this.tempFile, Buffer.concat(this.chunks))
83
+ this.chunks = []
84
+ this.start(this.tempFile)
85
+ }
86
+
87
+ pause(): void {
88
+ if (!this._started || this._paused || this._stopped) return
89
+
90
+ if (this.process) {
91
+ try {
92
+ this.process.kill('SIGSTOP')
93
+ this._paused = true
94
+ this.events?.onPause?.()
95
+ } catch (e) {
96
+ // SIGSTOP 可能失败
97
+ }
98
+ }
99
+ }
100
+
101
+ resume(): void {
102
+ if (!this._paused || this._stopped) return
103
+
104
+ if (this.process) {
105
+ try {
106
+ this.process.kill('SIGCONT')
107
+ this._paused = false
108
+ this.events?.onResume?.()
109
+ } catch (e) {
110
+ // SIGCONT 可能失败
111
+ }
112
+ }
113
+ }
114
+
115
+ stop(): void {
116
+ this._stopped = true
117
+ this._started = false
118
+ this._paused = false
119
+
120
+ if (this.process) {
121
+ try {
122
+ this.process.kill('SIGTERM')
123
+ } catch (e) {
124
+ // 忽略错误
125
+ }
126
+ this.process = undefined
127
+ }
128
+
129
+ this.cleanup()
130
+ this.chunks = []
131
+ this.hasEnded = false
132
+
133
+ this.events?.onStop?.()
134
+ }
135
+
136
+ setVolume(_volume: number): void {
137
+ // aplay 不支持命令行设置音量
138
+ }
139
+
140
+ destroy(): void {
141
+ this.stop()
142
+ }
143
+
144
+ private cleanup(): void {
145
+ if (this.tempFile && this.tempFile.startsWith(tmpdir())) {
146
+ try {
147
+ if (existsSync(this.tempFile)) {
148
+ unlinkSync(this.tempFile)
149
+ }
150
+ } catch (e) {
151
+ // 忽略清理错误
152
+ }
153
+ this.tempFile = undefined
154
+ }
155
+ }
156
+
157
+ private handleError(error: Error): void {
158
+ this.events?.onError?.(error)
159
+ }
160
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Audio Backend Interface
3
+ * 音频后端接口定义 - 统一各平台音频播放实现
4
+ */
5
+
6
+ /**
7
+ * 音频后端接口
8
+ * 定义各平台音频后端必须实现的方法
9
+ */
10
+ export interface AudioBackend {
11
+ /** 后端名称 */
12
+ readonly name: string
13
+
14
+ /** 是否支持真正的流式播放(边收边播) */
15
+ readonly supportsStreaming: boolean
16
+
17
+ /**
18
+ * 开始播放音频文件
19
+ * @param filePath 音频文件路径
20
+ */
21
+ start(filePath: string): void
22
+
23
+ /**
24
+ * 写入音频数据块(用于流式播放)
25
+ * @param chunk 音频数据块
26
+ */
27
+ write(chunk: Buffer): void
28
+
29
+ /**
30
+ * 结束写入,关闭流
31
+ */
32
+ end(): void
33
+
34
+ /**
35
+ * 暂停播放
36
+ */
37
+ pause(): void
38
+
39
+ /**
40
+ * 恢复播放
41
+ */
42
+ resume(): void
43
+
44
+ /**
45
+ * 停止播放
46
+ */
47
+ stop(): void
48
+
49
+ /**
50
+ * 获取当前播放位置(秒)
51
+ * 如果不支持返回 undefined
52
+ */
53
+ getCurrentTime?(): number | undefined
54
+
55
+ /**
56
+ * 获取音频总时长(秒)
57
+ * 如果不支持返回 undefined
58
+ */
59
+ getDuration?(): number | undefined
60
+
61
+ /**
62
+ * 设置音量
63
+ * @param volume 音量 0.0 - 1.0
64
+ */
65
+ setVolume?(volume: number): void
66
+
67
+ /**
68
+ * 销毁后端,释放资源
69
+ */
70
+ destroy(): void
71
+ }
72
+
73
+ /**
74
+ * 音频后端事件回调接口
75
+ */
76
+ export interface AudioBackendEvents {
77
+ /** 开始播放回调 */
78
+ onStart?: () => void
79
+
80
+ /** 播放结束回调 */
81
+ onEnd?: () => void
82
+
83
+ /** 错误回调 */
84
+ onError?: (error: Error) => void
85
+
86
+ /** 暂停回调 */
87
+ onPause?: () => void
88
+
89
+ /** 恢复回调 */
90
+ onResume?: () => void
91
+
92
+ /** 停止回调 */
93
+ onStop?: () => void
94
+
95
+ /** 进度回调(已写入字节数) */
96
+ onProgress?: (bytesWritten: number) => void
97
+ }
98
+
99
+ /**
100
+ * 后端配置选项
101
+ */
102
+ export interface BackendOptions {
103
+ /** 音频格式 (mp3, wav, flac) */
104
+ format?: 'mp3' | 'wav' | 'flac'
105
+
106
+ /** 采样率 (如 16000, 44100) */
107
+ sampleRate?: number
108
+
109
+ /** 声道数 (1 = 单声道, 2 = 立体声) */
110
+ channels?: number
111
+
112
+ /** 音量 0.0 - 1.0 */
113
+ volume?: number
114
+
115
+ /** 事件回调 */
116
+ events?: AudioBackendEvents
117
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Audio Backends - 多平台音频后端统一导出
3
+ */
4
+
5
+ // 接口和类型
6
+ export { AudioBackend, AudioBackendEvents, BackendOptions } from './base'
7
+
8
+ // 各平台后端实现
9
+ export { NaudiodonBackend } from './naudiodon-backend'
10
+ export { AfplayBackend } from './afplay-backend'
11
+ export { AplayBackend } from './aplay-backend'
12
+ export { PowerShellBackend } from './powershell-backend'
13
+
14
+ import { AudioBackend, BackendOptions } from './base'
15
+ import { NaudiodonBackend } from './naudiodon-backend'
16
+ import { AfplayBackend } from './afplay-backend'
17
+ import { AplayBackend } from './aplay-backend'
18
+ import { PowerShellBackend } from './powershell-backend'
19
+
20
+ /**
21
+ * 后端类型枚举
22
+ */
23
+ export enum BackendType {
24
+ NAUDIODON = 'naudiodon',
25
+ AFPLAY = 'afplay',
26
+ APLAY = 'aplay',
27
+ POWERSHELL = 'powershell',
28
+ AUTO = 'auto'
29
+ }
30
+
31
+ let naudiodonCache: any = null
32
+
33
+ async function tryLoadNaudiodon(): Promise<any> {
34
+ if (naudiodonCache !== null) {
35
+ return naudiodonCache
36
+ }
37
+ try {
38
+ naudiodonCache = await import('naudiodon')
39
+ return naudiodonCache
40
+ } catch (e) {
41
+ naudiodonCache = false
42
+ return null
43
+ }
44
+ }
45
+
46
+ function isNaudiodonAvailable(): boolean {
47
+ try {
48
+ require.resolve('naudiodon')
49
+ return true
50
+ } catch (e) {
51
+ return false
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 创建音频后端
57
+ * @param type 后端类型,默认 AUTO(自动选择)
58
+ * @param options 后端配置选项
59
+ * @returns 音频后端实例
60
+ */
61
+ export function createBackend(type: BackendType = BackendType.AUTO, options: BackendOptions = {}): AudioBackend {
62
+ const platform = process.platform
63
+
64
+ if (type !== BackendType.AUTO) {
65
+ return createBackendByType(type, options)
66
+ }
67
+
68
+ if (isNaudiodonAvailable()) {
69
+ try {
70
+ const naudiodon = require('naudiodon')
71
+ if (naudiodon) {
72
+ return new NaudiodonBackend(options)
73
+ }
74
+ } catch (e) {}
75
+ }
76
+
77
+ switch (platform) {
78
+ case 'darwin':
79
+ return new AfplayBackend(options)
80
+ case 'linux':
81
+ return new AplayBackend(options)
82
+ case 'win32':
83
+ return new PowerShellBackend(options)
84
+ default:
85
+ throw new Error(`Unsupported platform: ${platform}`)
86
+ }
87
+ }
88
+
89
+ function createBackendByType(type: BackendType, options: BackendOptions): AudioBackend {
90
+ switch (type) {
91
+ case BackendType.NAUDIODON:
92
+ return new NaudiodonBackend(options)
93
+ case BackendType.AFPLAY:
94
+ return new AfplayBackend(options)
95
+ case BackendType.APLAY:
96
+ return new AplayBackend(options)
97
+ case BackendType.POWERSHELL:
98
+ return new PowerShellBackend(options)
99
+ default:
100
+ throw new Error(`Unknown backend type: ${type}`)
101
+ }
102
+ }
103
+
104
+ export function supportsStreaming(type: BackendType): boolean {
105
+ if (type === BackendType.AUTO) {
106
+ return isNaudiodonAvailable()
107
+ }
108
+ return type === BackendType.NAUDIODON
109
+ }
110
+
111
+ export function getDefaultBackendType(): BackendType {
112
+ const platform = process.platform
113
+
114
+ if (supportsStreaming(BackendType.AUTO)) {
115
+ return BackendType.NAUDIODON
116
+ }
117
+
118
+ switch (platform) {
119
+ case 'darwin':
120
+ return BackendType.AFPLAY
121
+ case 'linux':
122
+ return BackendType.APLAY
123
+ case 'win32':
124
+ return BackendType.POWERSHELL
125
+ default:
126
+ return BackendType.NAUDIODON
127
+ }
128
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Naudiodon Backend - 基于 PortAudio 的跨平台音频播放后端
3
+ * 支持真正的流式播放(边收边播)
4
+ */
5
+
6
+ import { AudioBackend, AudioBackendEvents, BackendOptions } from './base'
7
+
8
+ class UnsupportedError extends Error {
9
+ constructor(message: string) {
10
+ super(message)
11
+ this.name = 'UnsupportedError'
12
+ }
13
+ }
14
+
15
+ interface NaudiodonAudioOutput {
16
+ start(): void
17
+ write(chunk: Buffer): void
18
+ end(): void
19
+ quit(): void
20
+ on(event: string, callback: (error: Error) => void): void
21
+ }
22
+
23
+ interface Naudiodon {
24
+ new (options: {
25
+ sampleRate?: number
26
+ channels?: number
27
+ bitDepth?: number
28
+ }): NaudiodonAudioOutput
29
+ }
30
+
31
+ declare global {
32
+ interface NodeModule {
33
+ require(id: 'naudiodon'): Naudiodon
34
+ }
35
+ }
36
+
37
+ export class NaudiodonBackend implements AudioBackend {
38
+ readonly name = 'naudiodon'
39
+ readonly supportsStreaming = true
40
+
41
+ private audioOutput?: NaudiodonAudioOutput
42
+ private events?: AudioBackendEvents
43
+ private _started = false
44
+ private _paused = false
45
+ private _stopped = false
46
+ private sampleRate: number
47
+ private channels: number
48
+ private volume = 1.0
49
+ private bytesWritten = 0
50
+
51
+ constructor(options: BackendOptions = {}) {
52
+ this.sampleRate = options.sampleRate || 16000
53
+ this.channels = options.channels || 1
54
+ this.events = options.events
55
+ this.volume = options.volume ?? 1.0
56
+ }
57
+
58
+ start(_filePath: string): void {
59
+ if (this._started) return
60
+
61
+ try {
62
+ const naudiodon = require('naudiodon') as Naudiodon
63
+ const AudioOutput = naudiodon as unknown as { new(options: { sampleRate: number; channels: number; bitDepth: number }): NaudiodonAudioOutput }
64
+
65
+ this.audioOutput = new AudioOutput({
66
+ sampleRate: this.sampleRate,
67
+ channels: this.channels,
68
+ bitDepth: 16
69
+ })
70
+
71
+ this.audioOutput.on('error', (error: Error) => {
72
+ this.handleError(error)
73
+ })
74
+
75
+ this.audioOutput.start()
76
+ this._started = true
77
+ this._stopped = false
78
+
79
+ this.events?.onStart?.()
80
+ } catch (error: any) {
81
+ if (error.code === 'MODULE_NOT_FOUND') {
82
+ throw new Error('naudiodon is not installed. Run: npm install naudiodon')
83
+ }
84
+ throw error
85
+ }
86
+ }
87
+
88
+ write(chunk: Buffer): void {
89
+ if (!this._started || this._stopped) return
90
+
91
+ if (this.audioOutput) {
92
+ const adjustedChunk = this.adjustVolume(chunk)
93
+ this.audioOutput.write(adjustedChunk)
94
+ this.bytesWritten += chunk.length
95
+ this.events?.onProgress?.(this.bytesWritten)
96
+ }
97
+ }
98
+
99
+ end(): void {
100
+ if (this.audioOutput) {
101
+ this.audioOutput.end()
102
+ }
103
+ }
104
+
105
+ pause(): void {
106
+ if (!this._started || this._paused || this._stopped) return
107
+ throw new UnsupportedError('naudiodon backend does not support pause')
108
+ }
109
+
110
+ resume(): void {
111
+ if (!this._paused || this._stopped) return
112
+ this._paused = false
113
+ this.events?.onResume?.()
114
+ }
115
+
116
+ stop(): void {
117
+ this._stopped = true
118
+ this._started = false
119
+ this._paused = false
120
+
121
+ if (this.audioOutput) {
122
+ try {
123
+ this.audioOutput.quit()
124
+ } catch (e) {
125
+ // 忽略退出错误
126
+ }
127
+ this.audioOutput = undefined
128
+ }
129
+
130
+ this.bytesWritten = 0
131
+ this.events?.onStop?.()
132
+ }
133
+
134
+ getCurrentTime(): number {
135
+ return this.bytesWritten / (this.sampleRate * this.channels * 2)
136
+ }
137
+
138
+ getDuration?(): number | undefined {
139
+ return undefined
140
+ }
141
+
142
+ setVolume(volume: number): void {
143
+ this.volume = Math.max(0, Math.min(1, volume))
144
+ }
145
+
146
+ destroy(): void {
147
+ this.stop()
148
+ }
149
+
150
+ private adjustVolume(chunk: Buffer): Buffer {
151
+ if (this.volume === 1.0) return chunk
152
+
153
+ const adjusted = Buffer.alloc(chunk.length)
154
+ for (let i = 0; i < chunk.length; i += 2) {
155
+ const sample = chunk.readInt16LE(i) * this.volume
156
+ adjusted.writeInt16LE(Math.round(sample), i)
157
+ }
158
+ return adjusted
159
+ }
160
+
161
+ private handleError(error: Error): void {
162
+ this.events?.onError?.(error)
163
+ }
164
+ }