add funasr demo

This commit is contained in:
2025-04-16 23:47:32 +08:00
parent bd789de7b6
commit 2377318446
25 changed files with 842 additions and 345 deletions

144
src/recorder/index.ts Normal file
View File

@@ -0,0 +1,144 @@
import assert from 'assert';
import { logDebug, logInfo } from '../logger/index.ts';
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
import recorders from './recorders/index.ts';
import Stream from 'stream';
export type RecordingOptions = {
/* 采样率默认为16000 */
sampleRate?: number;
/* 声道数默认为1 */
channels?: number;
/* 是否压缩音频默认为false */
compress?: boolean;
/* 录音开始的音量阈值默认为0.5 */
threshold?: number;
/* 开始录音的音量阈值 */
thresholdStart?: number;
/* 结束录音的音量阈值 */
thresholdEnd?: number;
/* 录音结束的静默时间,默认为'1.0'秒 */
silence?: string;
/* 使用的录音器,默认为'sox' */
recorder?: string;
/* 是否在静默时结束录音默认为false */
endOnSilence?: boolean;
/* 音频类型,默认为'wav' */
audioType?: string;
};
/**
* node-record-lpcm16
* https://github.com/gillesdemey/node-record-lpcm16
*/
export class Recording {
options: RecordingOptions;
cmd: string;
args: string[];
cmdOptions: any;
process: ChildProcessWithoutNullStreams;
_stream: Stream.Readable;
constructor(options?: RecordingOptions) {
const defaults = {
sampleRate: 16000,
channels: 1,
compress: false,
threshold: 0.5,
thresholdStart: null,
thresholdEnd: null,
silence: '1.0',
recorder: 'sox',
endOnSilence: false,
audioType: 'wav',
};
this.options = Object.assign(defaults, options);
const recorder = recorders[this.options.recorder];
if (!recorder) {
throw new Error(`No such recorder found: ${this.options.recorder}`);
}
const { cmd, args, spawnOptions = {} } = recorder(this.options);
this.cmd = cmd;
this.args = args;
this.cmdOptions = Object.assign({ encoding: 'binary', stdio: 'pipe' }, spawnOptions);
logDebug(`Started recording`);
logDebug('options', this.options);
logDebug(` ${this.cmd} ${this.args.join(' ')}`);
return this.start();
}
start() {
const { cmd, args, cmdOptions } = this;
const cp = spawn(cmd, args, cmdOptions);
const rec = cp.stdout;
const err = cp.stderr;
this.process = cp; // expose child process
this._stream = rec; // expose output stream
cp.on('close', (code) => {
if (code === 0) return;
rec.emit(
'error',
`${this.cmd} has exited with error code ${code}.
Enable debugging with the environment variable DEBUG=record.`,
);
});
err.on('data', (chunk) => {
logDebug(`STDERR: ${chunk}`);
});
rec.on('data', (chunk) => {
logDebug(`Recording ${chunk.length} bytes`);
});
rec.on('end', () => {
logDebug('Recording ended');
});
return this;
}
stop() {
assert(this.process, 'Recording not yet started');
this.process.kill();
}
pause() {
assert(this.process, 'Recording not yet started');
this.process.kill('SIGSTOP');
this._stream.pause();
logDebug('Paused recording');
}
resume() {
assert(this.process, 'Recording not yet started');
this.process.kill('SIGCONT');
this._stream.resume();
logDebug('Resumed recording');
}
isPaused() {
assert(this.process, 'Recording not yet started');
return this._stream.isPaused();
}
stream() {
assert(this._stream, 'Recording not yet started');
return this._stream;
}
}
export const record = (...args) => new Recording(...args);

View File

@@ -0,0 +1,23 @@
// On some systems (RasPi), arecord is the prefered recording binary
export default (options: any) => {
const cmd = 'arecord';
const args = [
'-q', // show no progress
'-r',
options.sampleRate, // sample rate
'-c',
options.channels, // channels
'-t',
options.audioType, // audio type
'-f',
'S16_LE', // Sample format
'-', // pipe
];
if (options.device) {
args.unshift('-D', options.device);
}
return { cmd, args };
};

View File

@@ -0,0 +1,11 @@
import path from 'path';
import sox from './sox.ts';
import rec from './rec.ts';
import arecord from './arecord.ts';
export default {
sox,
rec,
arecord,
};

View File

@@ -0,0 +1,32 @@
export default (options: any) => {
const cmd = 'rec';
let args = [
'-q', // show no progress
'-r',
options.sampleRate, // sample rate
'-c',
options.channels, // channels
'-e',
'signed-integer', // sample encoding
'-b',
'16', // precision (bits)
'-t',
options.audioType, // audio type
'-', // pipe
];
if (options.endOnSilence) {
args = args.concat([
'silence',
'1',
'0.1',
options.thresholdStart || options.threshold + '%',
'1',
options.silence,
options.thresholdEnd || options.threshold + '%',
]);
}
return { cmd, args };
};

View File

@@ -0,0 +1,39 @@
export default (options: any) => {
const cmd = 'sox';
let args = [
'--default-device',
'--no-show-progress', // show no progress
'--rate',
options.sampleRate, // sample rate
'--channels',
options.channels, // channels
'--encoding',
'signed-integer', // sample encoding
'--bits',
'16', // precision (bits)
'--type',
options.audioType, // audio type
'-', // pipe
];
if (options.endOnSilence) {
args = args.concat([
'silence',
'1',
'0.1',
options.thresholdStart || options.threshold + '%',
'1',
options.silence,
options.thresholdEnd || options.threshold + '%',
]);
}
const spawnOptions: any = {};
if (options.device) {
spawnOptions.env = { ...process.env, AUDIODEV: options.device };
}
return { cmd, args, spawnOptions };
};