@mingxy/ocosay 1.1.11 → 1.1.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/dist/core/backends/index.d.ts +4 -7
- package/dist/core/backends/index.js +46 -15
- package/dist/core/backends/playsound-backend.d.ts +39 -0
- package/dist/core/backends/playsound-backend.js +189 -0
- package/dist/core/backends/powershell-backend.js +10 -1
- package/dist/core/backends/speaker-backend.d.ts +34 -0
- package/dist/core/backends/speaker-backend.js +163 -0
- package/dist/package.json +5 -3
- package/dist/plugin.js +431 -62
- package/package.json +5 -3
|
@@ -7,6 +7,8 @@ export { AfplayBackend } from './afplay-backend';
|
|
|
7
7
|
export { AplayBackend } from './aplay-backend';
|
|
8
8
|
export { PowerShellBackend } from './powershell-backend';
|
|
9
9
|
export { HowlerBackend } from './howler-backend';
|
|
10
|
+
export { PlaySoundBackend } from './playsound-backend';
|
|
11
|
+
export { SpeakerBackend } from './speaker-backend';
|
|
10
12
|
import { AudioBackend, BackendOptions } from './base';
|
|
11
13
|
/**
|
|
12
14
|
* 后端类型枚举
|
|
@@ -17,15 +19,10 @@ export declare enum BackendType {
|
|
|
17
19
|
APLAY = "aplay",
|
|
18
20
|
POWERSHELL = "powershell",
|
|
19
21
|
HOWLER = "howler",
|
|
22
|
+
PLAY_SOUND = "play-sound",
|
|
23
|
+
SPEAKER = "speaker",
|
|
20
24
|
AUTO = "auto"
|
|
21
25
|
}
|
|
22
|
-
export declare function isWsl(): boolean;
|
|
23
|
-
/**
|
|
24
|
-
* 创建音频后端
|
|
25
|
-
* @param type 后端类型,默认 AUTO(自动选择)
|
|
26
|
-
* @param options 后端配置选项
|
|
27
|
-
* @returns 音频后端实例
|
|
28
|
-
*/
|
|
29
26
|
export declare function createBackend(type?: BackendType, options?: BackendOptions): AudioBackend;
|
|
30
27
|
export declare function supportsStreaming(type: BackendType): boolean;
|
|
31
28
|
export declare function getDefaultBackendType(): BackendType;
|
|
@@ -7,12 +7,29 @@ export { AfplayBackend } from './afplay-backend';
|
|
|
7
7
|
export { AplayBackend } from './aplay-backend';
|
|
8
8
|
export { PowerShellBackend } from './powershell-backend';
|
|
9
9
|
export { HowlerBackend } from './howler-backend';
|
|
10
|
+
export { PlaySoundBackend } from './playsound-backend';
|
|
11
|
+
export { SpeakerBackend } from './speaker-backend';
|
|
12
|
+
import { execSync } from 'child_process';
|
|
10
13
|
import { NaudiodonBackend } from './naudiodon-backend';
|
|
11
14
|
import { AfplayBackend } from './afplay-backend';
|
|
12
15
|
import { AplayBackend } from './aplay-backend';
|
|
13
16
|
import { PowerShellBackend } from './powershell-backend';
|
|
14
17
|
import { HowlerBackend } from './howler-backend';
|
|
18
|
+
import { PlaySoundBackend } from './playsound-backend';
|
|
19
|
+
import { SpeakerBackend } from './speaker-backend';
|
|
15
20
|
import { logger } from '../../utils/logger';
|
|
21
|
+
function execCmd(cmd) {
|
|
22
|
+
try {
|
|
23
|
+
const output = execSync(cmd, { stdio: 'pipe', encoding: 'utf8' });
|
|
24
|
+
return { success: true, output };
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return { success: false, output: err.message || '' };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function isCommandAvailable(cmd) {
|
|
31
|
+
return execCmd(`which ${cmd}`).success;
|
|
32
|
+
}
|
|
16
33
|
/**
|
|
17
34
|
* 后端类型枚举
|
|
18
35
|
*/
|
|
@@ -23,6 +40,8 @@ export var BackendType;
|
|
|
23
40
|
BackendType["APLAY"] = "aplay";
|
|
24
41
|
BackendType["POWERSHELL"] = "powershell";
|
|
25
42
|
BackendType["HOWLER"] = "howler";
|
|
43
|
+
BackendType["PLAY_SOUND"] = "play-sound";
|
|
44
|
+
BackendType["SPEAKER"] = "speaker";
|
|
26
45
|
BackendType["AUTO"] = "auto";
|
|
27
46
|
})(BackendType || (BackendType = {}));
|
|
28
47
|
let naudiodonCache = null;
|
|
@@ -50,22 +69,16 @@ function isNaudiodonAvailable() {
|
|
|
50
69
|
return false;
|
|
51
70
|
}
|
|
52
71
|
}
|
|
53
|
-
|
|
54
|
-
if (process.platform !== 'linux')
|
|
55
|
-
return false;
|
|
72
|
+
function isSpeakerAvailable() {
|
|
56
73
|
try {
|
|
57
|
-
|
|
74
|
+
require.resolve('speaker');
|
|
75
|
+
return true;
|
|
58
76
|
}
|
|
59
|
-
catch {
|
|
77
|
+
catch (err) {
|
|
78
|
+
logger.debug({ err }, 'speaker not available');
|
|
60
79
|
return false;
|
|
61
80
|
}
|
|
62
81
|
}
|
|
63
|
-
/**
|
|
64
|
-
* 创建音频后端
|
|
65
|
-
* @param type 后端类型,默认 AUTO(自动选择)
|
|
66
|
-
* @param options 后端配置选项
|
|
67
|
-
* @returns 音频后端实例
|
|
68
|
-
*/
|
|
69
82
|
export function createBackend(type = BackendType.AUTO, options = {}) {
|
|
70
83
|
const platform = process.platform;
|
|
71
84
|
if (type !== BackendType.AUTO) {
|
|
@@ -90,10 +103,24 @@ export function createBackend(type = BackendType.AUTO, options = {}) {
|
|
|
90
103
|
case 'darwin':
|
|
91
104
|
return new AfplayBackend(options);
|
|
92
105
|
case 'linux':
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
// Linux 环境检测顺序:naudiodon → aplay → play-sound → speaker → Howler
|
|
107
|
+
if (isCommandAvailable('aplay')) {
|
|
108
|
+
const test = execCmd('aplay -l');
|
|
109
|
+
if (test.success && !test.output.includes('no soundcards')) {
|
|
110
|
+
return new AplayBackend(options);
|
|
111
|
+
}
|
|
95
112
|
}
|
|
96
|
-
|
|
113
|
+
// 检测 play-sound (ffplay)
|
|
114
|
+
if (isCommandAvailable('ffplay')) {
|
|
115
|
+
return new PlaySoundBackend(options);
|
|
116
|
+
}
|
|
117
|
+
// 检测 speaker (需要 speaker npm 包)
|
|
118
|
+
if (isSpeakerAvailable()) {
|
|
119
|
+
return new SpeakerBackend(options);
|
|
120
|
+
}
|
|
121
|
+
// 彻底失败,使用 Howler 作为最后的回退
|
|
122
|
+
logger.warn('All Linux audio backends failed, using HowlerBackend as fallback');
|
|
123
|
+
return new HowlerBackend(options);
|
|
97
124
|
case 'win32':
|
|
98
125
|
return new PowerShellBackend(options);
|
|
99
126
|
default:
|
|
@@ -112,6 +139,10 @@ function createBackendByType(type, options) {
|
|
|
112
139
|
return new PowerShellBackend(options);
|
|
113
140
|
case BackendType.HOWLER:
|
|
114
141
|
return new HowlerBackend(options);
|
|
142
|
+
case BackendType.PLAY_SOUND:
|
|
143
|
+
return new PlaySoundBackend(options);
|
|
144
|
+
case BackendType.SPEAKER:
|
|
145
|
+
return new SpeakerBackend(options);
|
|
115
146
|
default:
|
|
116
147
|
throw new Error(`Unknown backend type: ${type}`);
|
|
117
148
|
}
|
|
@@ -120,7 +151,7 @@ export function supportsStreaming(type) {
|
|
|
120
151
|
if (type === BackendType.AUTO) {
|
|
121
152
|
return isNaudiodonAvailable();
|
|
122
153
|
}
|
|
123
|
-
return type === BackendType.NAUDIODON;
|
|
154
|
+
return type === BackendType.NAUDIODON || type === BackendType.SPEAKER;
|
|
124
155
|
}
|
|
125
156
|
export function getDefaultBackendType() {
|
|
126
157
|
const platform = process.platform;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlaySound Backend - 跨平台音频播放后端
|
|
3
|
+
* 使用 play-sound npm 包调用系统工具(ffplay/aplay/mpg123)
|
|
4
|
+
* 支持 Linux/macOS/Windows,可无声卡播放(ffplay)
|
|
5
|
+
*/
|
|
6
|
+
import { AudioBackend, BackendOptions } from './base';
|
|
7
|
+
/**
|
|
8
|
+
* PlaySoundBackend - 使用 play-sound 包的后端
|
|
9
|
+
* play-sound 会自动选择最佳播放器:
|
|
10
|
+
* - Linux: ffplay > aplay > mpg123
|
|
11
|
+
* - macOS: afplay > aplay > mpg123
|
|
12
|
+
* - Windows: Powershell > vlc > afplay
|
|
13
|
+
* ffplay 支持无声卡播放(-nodisp -autoexit)
|
|
14
|
+
*/
|
|
15
|
+
export declare class PlaySoundBackend implements AudioBackend {
|
|
16
|
+
readonly name = "play-sound";
|
|
17
|
+
readonly supportsStreaming = false;
|
|
18
|
+
private player?;
|
|
19
|
+
private tempFile?;
|
|
20
|
+
private events?;
|
|
21
|
+
private _started;
|
|
22
|
+
private _paused;
|
|
23
|
+
private _stopped;
|
|
24
|
+
private chunks;
|
|
25
|
+
private hasEnded;
|
|
26
|
+
constructor(options?: BackendOptions);
|
|
27
|
+
start(filePath: string): void;
|
|
28
|
+
private playWithPlaySound;
|
|
29
|
+
write(chunk: Buffer): void;
|
|
30
|
+
end(): void;
|
|
31
|
+
pause(): void;
|
|
32
|
+
resume(): void;
|
|
33
|
+
stop(): void;
|
|
34
|
+
setVolume(_volume: number): void;
|
|
35
|
+
destroy(): void;
|
|
36
|
+
private cleanup;
|
|
37
|
+
private handleError;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=playsound-backend.d.ts.map
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlaySound Backend - 跨平台音频播放后端
|
|
3
|
+
* 使用 play-sound npm 包调用系统工具(ffplay/aplay/mpg123)
|
|
4
|
+
* 支持 Linux/macOS/Windows,可无声卡播放(ffplay)
|
|
5
|
+
*/
|
|
6
|
+
import { execFile } from 'child_process';
|
|
7
|
+
import { tmpdir } from 'os';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
10
|
+
// 白名单:只允许特定路径格式(禁止 - 防止命令注入)
|
|
11
|
+
const SAFE_PATH_REGEX = /^[\w\/\.]+$/;
|
|
12
|
+
/**
|
|
13
|
+
* PlaySoundBackend - 使用 play-sound 包的后端
|
|
14
|
+
* play-sound 会自动选择最佳播放器:
|
|
15
|
+
* - Linux: ffplay > aplay > mpg123
|
|
16
|
+
* - macOS: afplay > aplay > mpg123
|
|
17
|
+
* - Windows: Powershell > vlc > afplay
|
|
18
|
+
* ffplay 支持无声卡播放(-nodisp -autoexit)
|
|
19
|
+
*/
|
|
20
|
+
export class PlaySoundBackend {
|
|
21
|
+
name = 'play-sound';
|
|
22
|
+
supportsStreaming = false;
|
|
23
|
+
player;
|
|
24
|
+
tempFile;
|
|
25
|
+
events;
|
|
26
|
+
_started = false;
|
|
27
|
+
_paused = false;
|
|
28
|
+
_stopped = false;
|
|
29
|
+
// P0-4: 缓冲所有chunk,等end()时一次性写入文件
|
|
30
|
+
chunks = [];
|
|
31
|
+
hasEnded = false;
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
this.events = options.events;
|
|
34
|
+
}
|
|
35
|
+
start(filePath) {
|
|
36
|
+
if (this._started)
|
|
37
|
+
return;
|
|
38
|
+
if (!SAFE_PATH_REGEX.test(filePath)) {
|
|
39
|
+
throw new Error(`Invalid file path: ${filePath}`);
|
|
40
|
+
}
|
|
41
|
+
this.tempFile = filePath;
|
|
42
|
+
this._started = true;
|
|
43
|
+
this._stopped = false;
|
|
44
|
+
this.events?.onStart?.();
|
|
45
|
+
// 动态导入 play-sound
|
|
46
|
+
this.playWithPlaySound(filePath);
|
|
47
|
+
}
|
|
48
|
+
async playWithPlaySound(filePath) {
|
|
49
|
+
try {
|
|
50
|
+
// 异步导入 play-sound
|
|
51
|
+
const play = (await import('play-sound')).default;
|
|
52
|
+
// 配置播放器选项
|
|
53
|
+
const opts = {
|
|
54
|
+
players: ['ffplay', 'aplay', 'mpg123', 'afplay'] // 优先级
|
|
55
|
+
};
|
|
56
|
+
// 对于 ffplay,使用无声模式
|
|
57
|
+
if (process.platform === 'linux') {
|
|
58
|
+
// ffplay 无声卡播放参数
|
|
59
|
+
this.player = execFile('ffplay', [
|
|
60
|
+
'-nodisp', // 不显示窗口
|
|
61
|
+
'-autoexit', // 播放完自动退出
|
|
62
|
+
'-loglevel', 'error', // 减少日志输出
|
|
63
|
+
filePath
|
|
64
|
+
], (error) => {
|
|
65
|
+
if (this._stopped)
|
|
66
|
+
return;
|
|
67
|
+
if (error) {
|
|
68
|
+
this.handleError(error);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this._started = false;
|
|
72
|
+
this.events?.onEnd?.();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// 使用 play-sound 的默认行为
|
|
77
|
+
const audio = play;
|
|
78
|
+
const p = audio.play(filePath, (err) => {
|
|
79
|
+
if (this._stopped)
|
|
80
|
+
return;
|
|
81
|
+
if (err) {
|
|
82
|
+
this.handleError(err);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this._started = false;
|
|
86
|
+
this.events?.onEnd?.();
|
|
87
|
+
});
|
|
88
|
+
if (p && p.kill) {
|
|
89
|
+
this.player = p;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (this.player) {
|
|
93
|
+
this.player.on('error', (error) => {
|
|
94
|
+
this.handleError(error);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
this.handleError(err instanceof Error ? err : new Error(String(err)));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
write(chunk) {
|
|
103
|
+
if (this._stopped)
|
|
104
|
+
return;
|
|
105
|
+
// P0-4: 缓冲所有chunk,等end()时一次性写入
|
|
106
|
+
this.chunks.push(chunk);
|
|
107
|
+
}
|
|
108
|
+
end() {
|
|
109
|
+
if (this._stopped || this.hasEnded)
|
|
110
|
+
return;
|
|
111
|
+
this.hasEnded = true;
|
|
112
|
+
if (this.chunks.length === 0)
|
|
113
|
+
return;
|
|
114
|
+
// P0-4: 所有chunk缓冲完毕后,一次性写入文件并播放
|
|
115
|
+
this.tempFile = join(tmpdir(), `ocosay-${Date.now()}.wav`);
|
|
116
|
+
writeFileSync(this.tempFile, Buffer.concat(this.chunks));
|
|
117
|
+
this.chunks = [];
|
|
118
|
+
this.start(this.tempFile);
|
|
119
|
+
}
|
|
120
|
+
pause() {
|
|
121
|
+
if (!this._started || this._paused || this._stopped)
|
|
122
|
+
return;
|
|
123
|
+
if (this.player) {
|
|
124
|
+
try {
|
|
125
|
+
this.player.kill('SIGSTOP');
|
|
126
|
+
this._paused = true;
|
|
127
|
+
this.events?.onPause?.();
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
// SIGSTOP 可能失败
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
resume() {
|
|
135
|
+
if (!this._paused || this._stopped)
|
|
136
|
+
return;
|
|
137
|
+
if (this.player) {
|
|
138
|
+
try {
|
|
139
|
+
this.player.kill('SIGCONT');
|
|
140
|
+
this._paused = false;
|
|
141
|
+
this.events?.onResume?.();
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
// SIGCONT 可能失败
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
stop() {
|
|
149
|
+
this._stopped = true;
|
|
150
|
+
this._started = false;
|
|
151
|
+
this._paused = false;
|
|
152
|
+
if (this.player) {
|
|
153
|
+
try {
|
|
154
|
+
this.player.kill('SIGTERM');
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
// 忽略错误
|
|
158
|
+
}
|
|
159
|
+
this.player = undefined;
|
|
160
|
+
}
|
|
161
|
+
this.cleanup();
|
|
162
|
+
this.chunks = [];
|
|
163
|
+
this.hasEnded = false;
|
|
164
|
+
this.events?.onStop?.();
|
|
165
|
+
}
|
|
166
|
+
setVolume(_volume) {
|
|
167
|
+
// play-sound/ffplay 不支持命令行设置音量
|
|
168
|
+
}
|
|
169
|
+
destroy() {
|
|
170
|
+
this.stop();
|
|
171
|
+
}
|
|
172
|
+
cleanup() {
|
|
173
|
+
if (this.tempFile && this.tempFile.startsWith(tmpdir())) {
|
|
174
|
+
try {
|
|
175
|
+
if (existsSync(this.tempFile)) {
|
|
176
|
+
unlinkSync(this.tempFile);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
// 忽略清理错误
|
|
181
|
+
}
|
|
182
|
+
this.tempFile = undefined;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
handleError(error) {
|
|
186
|
+
this.events?.onError?.(error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=playsound-backend.js.map
|
|
@@ -6,7 +6,16 @@ import { spawn } from 'child_process';
|
|
|
6
6
|
import { tmpdir } from 'os';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
9
|
-
|
|
9
|
+
function isWsl() {
|
|
10
|
+
if (process.platform !== 'linux')
|
|
11
|
+
return false;
|
|
12
|
+
try {
|
|
13
|
+
return require('fs').readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft');
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
10
19
|
// 白名单:Windows/WSL 路径格式(禁止 - 防止命令注入)
|
|
11
20
|
// 允许: 字母数字 \w, Windows盘符 : \:, 反斜杠 \\, 下划线 _, 点 ., $ @ /, 以及 -
|
|
12
21
|
const SAFE_PATH_REGEX = /^[\w\:\\_.\-$@\/]+$/i;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speaker Backend - Linux/macOS 音频播放后端
|
|
3
|
+
* 使用 speaker npm 包直接输出 PCM 到 ALSA/PulseAudio
|
|
4
|
+
* 支持流式播放,但需要完整的音频头信息
|
|
5
|
+
*/
|
|
6
|
+
import { AudioBackend, BackendOptions } from './base';
|
|
7
|
+
/**
|
|
8
|
+
* SpeakerBackend - 使用 speaker 包的后端
|
|
9
|
+
* speaker 直接将 PCM 数据输出到系统音频设备
|
|
10
|
+
* 支持流式播放,但需要正确的音频格式参数
|
|
11
|
+
*/
|
|
12
|
+
export declare class SpeakerBackend implements AudioBackend {
|
|
13
|
+
readonly name = "speaker";
|
|
14
|
+
readonly supportsStreaming = true;
|
|
15
|
+
private speaker?;
|
|
16
|
+
private events?;
|
|
17
|
+
private _started;
|
|
18
|
+
private _paused;
|
|
19
|
+
private _stopped;
|
|
20
|
+
private _format;
|
|
21
|
+
constructor(options?: BackendOptions);
|
|
22
|
+
start(_filePath: string): void;
|
|
23
|
+
write(chunk: Buffer): void;
|
|
24
|
+
private isWavHeader;
|
|
25
|
+
private stripWavHeader;
|
|
26
|
+
private createSpeaker;
|
|
27
|
+
end(): void;
|
|
28
|
+
pause(): void;
|
|
29
|
+
resume(): void;
|
|
30
|
+
stop(): void;
|
|
31
|
+
destroy(): void;
|
|
32
|
+
private handleError;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=speaker-backend.d.ts.map
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speaker Backend - Linux/macOS 音频播放后端
|
|
3
|
+
* 使用 speaker npm 包直接输出 PCM 到 ALSA/PulseAudio
|
|
4
|
+
* 支持流式播放,但需要完整的音频头信息
|
|
5
|
+
*/
|
|
6
|
+
import Speaker from 'speaker';
|
|
7
|
+
/**
|
|
8
|
+
* SpeakerBackend - 使用 speaker 包的后端
|
|
9
|
+
* speaker 直接将 PCM 数据输出到系统音频设备
|
|
10
|
+
* 支持流式播放,但需要正确的音频格式参数
|
|
11
|
+
*/
|
|
12
|
+
export class SpeakerBackend {
|
|
13
|
+
name = 'speaker';
|
|
14
|
+
supportsStreaming = true;
|
|
15
|
+
speaker;
|
|
16
|
+
events;
|
|
17
|
+
_started = false;
|
|
18
|
+
_paused = false;
|
|
19
|
+
_stopped = false;
|
|
20
|
+
_format = {
|
|
21
|
+
channels: 1,
|
|
22
|
+
sampleRate: 16000,
|
|
23
|
+
bitDepth: 16,
|
|
24
|
+
signed: true,
|
|
25
|
+
float: false
|
|
26
|
+
};
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.events = options.events;
|
|
29
|
+
if (options.sampleRate) {
|
|
30
|
+
this._format.sampleRate = options.sampleRate;
|
|
31
|
+
}
|
|
32
|
+
if (options.channels) {
|
|
33
|
+
this._format.channels = options.channels;
|
|
34
|
+
}
|
|
35
|
+
if (options.format === 'wav') {
|
|
36
|
+
this._format.bitDepth = 16;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
start(_filePath) {
|
|
40
|
+
if (this._started)
|
|
41
|
+
return;
|
|
42
|
+
this._started = true;
|
|
43
|
+
this._stopped = false;
|
|
44
|
+
this._paused = false;
|
|
45
|
+
this.events?.onStart?.();
|
|
46
|
+
}
|
|
47
|
+
write(chunk) {
|
|
48
|
+
if (this._stopped || this._paused)
|
|
49
|
+
return;
|
|
50
|
+
if (!this._started) {
|
|
51
|
+
this.start('');
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
if (!this.speaker) {
|
|
55
|
+
this.createSpeaker();
|
|
56
|
+
}
|
|
57
|
+
if (this.speaker) {
|
|
58
|
+
// 检查是否是 WAV 文件头
|
|
59
|
+
if (this.isWavHeader(chunk)) {
|
|
60
|
+
// 跳过 WAV 头,只播放数据
|
|
61
|
+
const audioData = this.stripWavHeader(chunk);
|
|
62
|
+
if (audioData.length > 0) {
|
|
63
|
+
this.speaker.write(audioData);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.speaker.write(chunk);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
this.handleError(err instanceof Error ? err : new Error(String(err)));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
isWavHeader(chunk) {
|
|
76
|
+
// 检查 RIFF 头
|
|
77
|
+
if (chunk.length >= 44) {
|
|
78
|
+
const riff = chunk.toString('ascii', 0, 4);
|
|
79
|
+
const wave = chunk.toString('ascii', 8, 12);
|
|
80
|
+
return riff === 'RIFF' && wave === 'WAVE';
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
stripWavHeader(chunk) {
|
|
85
|
+
// 跳过 44 字节的 WAV 头
|
|
86
|
+
return chunk.slice(44);
|
|
87
|
+
}
|
|
88
|
+
createSpeaker() {
|
|
89
|
+
try {
|
|
90
|
+
const format = {
|
|
91
|
+
channels: this._format.channels,
|
|
92
|
+
sampleRate: this._format.sampleRate,
|
|
93
|
+
bitDepth: this._format.bitDepth,
|
|
94
|
+
signed: this._format.signed,
|
|
95
|
+
float: this._format.float
|
|
96
|
+
};
|
|
97
|
+
this.speaker = new Speaker(format);
|
|
98
|
+
this.speaker.on('close', () => {
|
|
99
|
+
if (!this._stopped) {
|
|
100
|
+
this._started = false;
|
|
101
|
+
this.events?.onEnd?.();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
this.speaker.on('error', (err) => {
|
|
105
|
+
this.handleError(err);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
this.handleError(err instanceof Error ? err : new Error(String(err)));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
end() {
|
|
113
|
+
if (this._stopped)
|
|
114
|
+
return;
|
|
115
|
+
if (this.speaker) {
|
|
116
|
+
try {
|
|
117
|
+
this.speaker.close();
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
// 忽略关闭错误
|
|
121
|
+
}
|
|
122
|
+
this.speaker = undefined;
|
|
123
|
+
}
|
|
124
|
+
this._started = false;
|
|
125
|
+
this._stopped = true;
|
|
126
|
+
this.events?.onEnd?.();
|
|
127
|
+
}
|
|
128
|
+
pause() {
|
|
129
|
+
if (this._started && !this._paused) {
|
|
130
|
+
this._paused = true;
|
|
131
|
+
this.events?.onPause?.();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
resume() {
|
|
135
|
+
// speaker 不支持暂停恢复
|
|
136
|
+
if (this._paused) {
|
|
137
|
+
this._paused = false;
|
|
138
|
+
this.events?.onResume?.();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
stop() {
|
|
142
|
+
this._stopped = true;
|
|
143
|
+
this._started = false;
|
|
144
|
+
this._paused = false;
|
|
145
|
+
if (this.speaker) {
|
|
146
|
+
try {
|
|
147
|
+
this.speaker.close();
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
// 忽略关闭错误
|
|
151
|
+
}
|
|
152
|
+
this.speaker = undefined;
|
|
153
|
+
}
|
|
154
|
+
this.events?.onStop?.();
|
|
155
|
+
}
|
|
156
|
+
destroy() {
|
|
157
|
+
this.stop();
|
|
158
|
+
}
|
|
159
|
+
handleError(error) {
|
|
160
|
+
this.events?.onError?.(error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=speaker-backend.js.map
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mingxy/ocosay",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.12",
|
|
4
4
|
"description": "OpenCode TTS 播放插件 - 支持豆包模式边接收边朗读",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/plugin.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsc && npm run build:plugin && node -e \"require('fs').copyFileSync('package.json', 'dist/package.json')\"",
|
|
29
|
-
"build:plugin": "esbuild src/plugin.ts --bundle --platform=node --format=esm --outdir=dist --sourcemap --external:@opencode-ai/plugin --external:axios --external:ws --external:zod",
|
|
29
|
+
"build:plugin": "esbuild src/plugin.ts --bundle --platform=node --format=esm --outdir=dist --sourcemap --external:@opencode-ai/plugin --external:axios --external:ws --external:zod --external:naudiodon --external:play-sound --external:speaker",
|
|
30
30
|
"watch": "tsc --watch",
|
|
31
31
|
"test": "jest",
|
|
32
32
|
"lint": "eslint src --ext .ts"
|
|
@@ -48,7 +48,9 @@
|
|
|
48
48
|
"zod": "^4.3.6"
|
|
49
49
|
},
|
|
50
50
|
"optionalDependencies": {
|
|
51
|
-
"naudiodon": "^2.3.6"
|
|
51
|
+
"naudiodon": "^2.3.6",
|
|
52
|
+
"play-sound": "^1.1.5",
|
|
53
|
+
"speaker": "^1.4.2"
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|
|
54
56
|
"@types/howler": "^2.2.12",
|