Implementing an audio processor which now also takes care of the RNNoise part
This commit is contained in:
parent
1aab9e630a
commit
22d9059ad5
@ -52,7 +52,7 @@ set(NODEJS_SOURCE_FILES
|
|||||||
|
|
||||||
src/audio/js/AudioPlayer.cpp
|
src/audio/js/AudioPlayer.cpp
|
||||||
src/audio/js/AudioOutputStream.cpp
|
src/audio/js/AudioOutputStream.cpp
|
||||||
|
src/audio/js/AudioProcessor.cpp
|
||||||
src/audio/js/AudioRecorder.cpp
|
src/audio/js/AudioRecorder.cpp
|
||||||
src/audio/js/AudioConsumer.cpp
|
src/audio/js/AudioConsumer.cpp
|
||||||
src/audio/js/AudioFilter.cpp
|
src/audio/js/AudioFilter.cpp
|
||||||
@ -88,7 +88,7 @@ endif()
|
|||||||
|
|
||||||
add_nodejs_module(${MODULE_NAME} ${SOURCE_FILES} ${NODEJS_SOURCE_FILES})
|
add_nodejs_module(${MODULE_NAME} ${SOURCE_FILES} ${NODEJS_SOURCE_FILES})
|
||||||
target_link_libraries(${MODULE_NAME} ${NODEJS_LIBRARIES})
|
target_link_libraries(${MODULE_NAME} ${NODEJS_LIBRARIES})
|
||||||
#target_compile_options(${MODULE_NAME} PUBLIC "-fPIC")
|
target_compile_definitions(${MODULE_NAME} PRIVATE "NOMINMAX")
|
||||||
|
|
||||||
find_package(PortAudio REQUIRED)
|
find_package(PortAudio REQUIRED)
|
||||||
include_directories(${PortAudio_INCLUDE_DIR})
|
include_directories(${PortAudio_INCLUDE_DIR})
|
||||||
@ -173,6 +173,7 @@ else()
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
||||||
add_definitions(-DNO_OPEN_SSL)
|
add_definitions(-DNO_OPEN_SSL)
|
||||||
target_link_libraries(${MODULE_NAME} ${REQUIRED_LIBRARIES})
|
target_link_libraries(${MODULE_NAME} ${REQUIRED_LIBRARIES})
|
||||||
target_compile_definitions(${MODULE_NAME} PUBLIC -DNODEJS_API)
|
target_compile_definitions(${MODULE_NAME} PUBLIC -DNODEJS_API)
|
||||||
|
588
native/serverconnection/exports/exports.d.ts
vendored
588
native/serverconnection/exports/exports.d.ts
vendored
@ -1,291 +1,367 @@
|
|||||||
declare module "tc-native/connection" {
|
export enum ServerType {
|
||||||
export enum ServerType {
|
UNKNOWN,
|
||||||
UNKNOWN,
|
TEASPEAK,
|
||||||
TEASPEAK,
|
TEAMSPEAK
|
||||||
TEAMSPEAK
|
}
|
||||||
|
|
||||||
|
export enum PlayerState {
|
||||||
|
BUFFERING,
|
||||||
|
PLAYING,
|
||||||
|
STOPPING,
|
||||||
|
STOPPED
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NativeVoiceClient {
|
||||||
|
client_id: number;
|
||||||
|
|
||||||
|
callback_playback: () => any;
|
||||||
|
callback_stopped: () => any;
|
||||||
|
|
||||||
|
callback_state_changed: (new_state: PlayerState) => any;
|
||||||
|
|
||||||
|
get_state() : PlayerState;
|
||||||
|
|
||||||
|
get_volume() : number;
|
||||||
|
set_volume(volume: number) : void;
|
||||||
|
|
||||||
|
abort_replay();
|
||||||
|
get_stream() : audio.playback.AudioOutputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NativeVoiceConnection {
|
||||||
|
register_client(client_id: number) : NativeVoiceClient;
|
||||||
|
available_clients() : NativeVoiceClient[];
|
||||||
|
unregister_client(client_id: number);
|
||||||
|
|
||||||
|
audio_source() : audio.record.AudioConsumer;
|
||||||
|
set_audio_source(consumer: audio.record.AudioConsumer | undefined);
|
||||||
|
|
||||||
|
decoding_supported(codec: number) : boolean;
|
||||||
|
encoding_supported(codec: number) : boolean;
|
||||||
|
|
||||||
|
get_encoder_codec() : number;
|
||||||
|
set_encoder_codec(codec: number);
|
||||||
|
|
||||||
|
/* could may throw an exception when the underlying voice sender has been deallocated */
|
||||||
|
enable_voice_send(flag: boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NativeServerConnection {
|
||||||
|
callback_voice_data: (buffer: Uint8Array, client_id: number, codec_id: number, flag_head: boolean, packet_id: number) => any;
|
||||||
|
callback_command: (command: string, arguments: any, switches: string[]) => any;
|
||||||
|
callback_disconnect: (reason?: string) => any;
|
||||||
|
_voice_connection: NativeVoiceConnection;
|
||||||
|
server_type: ServerType;
|
||||||
|
|
||||||
|
connected(): boolean;
|
||||||
|
|
||||||
|
connect(properties: {
|
||||||
|
remote_host: string,
|
||||||
|
remote_port: number,
|
||||||
|
|
||||||
|
timeout: number,
|
||||||
|
|
||||||
|
callback: (error: number) => any,
|
||||||
|
|
||||||
|
identity_key: string | undefined, /* if identity_key empty, then an ephemeral license will be created */
|
||||||
|
teamspeak: boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
disconnect(reason: string | undefined, callback: (error: number) => any) : boolean;
|
||||||
|
|
||||||
|
error_message(id: number) : string;
|
||||||
|
|
||||||
|
send_command(command: string, arguments: any[], switches: string[]);
|
||||||
|
send_voice_data(buffer: Uint8Array, codec_id: number, header: boolean);
|
||||||
|
send_voice_data_raw(buffer: Float32Array, channels: number, sample_rate: number, header: boolean);
|
||||||
|
|
||||||
|
/* ping in microseconds */
|
||||||
|
current_ping() : number;
|
||||||
|
|
||||||
|
statistics() : { voice_bytes_received: number, voice_bytes_send: number, control_bytes_received: number, control_bytes_send } | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spawn_server_connection() : NativeServerConnection;
|
||||||
|
export function destroy_server_connection(connection: NativeServerConnection);
|
||||||
|
|
||||||
|
export namespace ft {
|
||||||
|
export interface TransferObject {
|
||||||
|
name: string;
|
||||||
|
direction: "upload" | "download";
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PlayerState {
|
export interface FileTransferSource extends TransferObject {
|
||||||
BUFFERING,
|
total_size: number;
|
||||||
PLAYING,
|
|
||||||
STOPPING,
|
|
||||||
STOPPED
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NativeVoiceClient {
|
export interface FileTransferTarget extends TransferObject {
|
||||||
client_id: number;
|
expected_size: number;
|
||||||
|
|
||||||
callback_playback: () => any;
|
|
||||||
callback_stopped: () => any;
|
|
||||||
|
|
||||||
callback_state_changed: (new_state: PlayerState) => any;
|
|
||||||
|
|
||||||
get_state() : PlayerState;
|
|
||||||
|
|
||||||
get_volume() : number;
|
|
||||||
set_volume(volume: number) : void;
|
|
||||||
|
|
||||||
abort_replay();
|
|
||||||
get_stream() : audio.playback.AudioOutputStream;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NativeVoiceConnection {
|
export interface NativeFileTransfer {
|
||||||
register_client(client_id: number) : NativeVoiceClient;
|
handle: TransferObject;
|
||||||
available_clients() : NativeVoiceClient[];
|
|
||||||
unregister_client(client_id: number);
|
|
||||||
|
|
||||||
audio_source() : audio.record.AudioConsumer;
|
callback_finished: (aborted?: boolean) => any;
|
||||||
set_audio_source(consumer: audio.record.AudioConsumer | undefined);
|
callback_start: () => any;
|
||||||
|
callback_progress: (current: number, max: number) => any;
|
||||||
|
callback_failed: (message: string) => any;
|
||||||
|
|
||||||
decoding_supported(codec: number) : boolean;
|
/**
|
||||||
encoding_supported(codec: number) : boolean;
|
* @return true if the connect method has been executed successfully
|
||||||
|
* false if the connect fails, callback_failed will be called with the exact reason
|
||||||
get_encoder_codec() : number;
|
*/
|
||||||
set_encoder_codec(codec: number);
|
start() : boolean;
|
||||||
|
|
||||||
/* could may throw an exception when the underlying voice sender has been deallocated */
|
|
||||||
enable_voice_send(flag: boolean);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NativeServerConnection {
|
export interface TransferOptions {
|
||||||
callback_voice_data: (buffer: Uint8Array, client_id: number, codec_id: number, flag_head: boolean, packet_id: number) => any;
|
remote_address: string;
|
||||||
callback_command: (command: string, arguments: any, switches: string[]) => any;
|
remote_port: number;
|
||||||
callback_disconnect: (reason?: string) => any;
|
|
||||||
_voice_connection: NativeVoiceConnection;
|
|
||||||
server_type: ServerType;
|
|
||||||
|
|
||||||
connected(): boolean;
|
transfer_key: string;
|
||||||
|
client_transfer_id: number;
|
||||||
|
server_transfer_id: number;
|
||||||
|
|
||||||
connect(properties: {
|
object: TransferObject;
|
||||||
remote_host: string,
|
|
||||||
remote_port: number,
|
|
||||||
|
|
||||||
timeout: number,
|
|
||||||
|
|
||||||
callback: (error: number) => any,
|
|
||||||
|
|
||||||
identity_key: string | undefined, /* if identity_key empty, then an ephemeral license will be created */
|
|
||||||
teamspeak: boolean
|
|
||||||
});
|
|
||||||
|
|
||||||
disconnect(reason: string | undefined, callback: (error: number) => any) : boolean;
|
|
||||||
|
|
||||||
error_message(id: number) : string;
|
|
||||||
|
|
||||||
send_command(command: string, arguments: any[], switches: string[]);
|
|
||||||
send_voice_data(buffer: Uint8Array, codec_id: number, header: boolean);
|
|
||||||
send_voice_data_raw(buffer: Float32Array, channels: number, sample_rate: number, header: boolean);
|
|
||||||
|
|
||||||
/* ping in microseconds */
|
|
||||||
current_ping() : number;
|
|
||||||
|
|
||||||
statistics() : { voice_bytes_received: number, voice_bytes_send: number, control_bytes_received: number, control_bytes_send } | undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spawn_server_connection() : NativeServerConnection;
|
export function upload_transfer_object_from_file(path: string, name: string) : FileTransferSource;
|
||||||
export function destroy_server_connection(connection: NativeServerConnection);
|
export function upload_transfer_object_from_buffer(buffer: ArrayBuffer) : FileTransferSource;
|
||||||
|
|
||||||
export namespace ft {
|
export function download_transfer_object_from_buffer(target_buffer: ArrayBuffer) : FileTransferTarget;
|
||||||
export interface TransferObject {
|
export function download_transfer_object_from_file(path: string, name: string, expectedSize: number) : FileTransferTarget;
|
||||||
name: string;
|
|
||||||
direction: "upload" | "download";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileTransferSource extends TransferObject {
|
export function destroy_connection(connection: NativeFileTransfer);
|
||||||
total_size: number;
|
export function spawn_connection(transfer: TransferOptions) : NativeFileTransfer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileTransferTarget extends TransferObject {
|
export namespace audio {
|
||||||
expected_size: number;
|
export interface AudioDevice {
|
||||||
}
|
name: string;
|
||||||
|
driver: string;
|
||||||
|
|
||||||
export interface NativeFileTransfer {
|
device_id: string;
|
||||||
handle: TransferObject;
|
|
||||||
|
|
||||||
callback_finished: (aborted?: boolean) => any;
|
input_supported: boolean;
|
||||||
callback_start: () => any;
|
output_supported: boolean;
|
||||||
callback_progress: (current: number, max: number) => any;
|
|
||||||
callback_failed: (message: string) => any;
|
|
||||||
|
|
||||||
/**
|
input_default: boolean;
|
||||||
* @return true if the connect method has been executed successfully
|
output_default: boolean;
|
||||||
* false if the connect fails, callback_failed will be called with the exact reason
|
|
||||||
*/
|
|
||||||
start() : boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransferOptions {
|
|
||||||
remote_address: string;
|
|
||||||
remote_port: number;
|
|
||||||
|
|
||||||
transfer_key: string;
|
|
||||||
client_transfer_id: number;
|
|
||||||
server_transfer_id: number;
|
|
||||||
|
|
||||||
object: TransferObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function upload_transfer_object_from_file(path: string, name: string) : FileTransferSource;
|
|
||||||
export function upload_transfer_object_from_buffer(buffer: ArrayBuffer) : FileTransferSource;
|
|
||||||
|
|
||||||
export function download_transfer_object_from_buffer(target_buffer: ArrayBuffer) : FileTransferTarget;
|
|
||||||
export function download_transfer_object_from_file(path: string, name: string, expectedSize: number) : FileTransferTarget;
|
|
||||||
|
|
||||||
export function destroy_connection(connection: NativeFileTransfer);
|
|
||||||
export function spawn_connection(transfer: TransferOptions) : NativeFileTransfer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace audio {
|
export namespace playback {
|
||||||
export interface AudioDevice {
|
export interface AudioOutputStream {
|
||||||
name: string;
|
sample_rate: number;
|
||||||
driver: string;
|
channels: number;
|
||||||
|
|
||||||
device_id: string;
|
get_buffer_latency() : number;
|
||||||
|
set_buffer_latency(value: number);
|
||||||
|
|
||||||
input_supported: boolean;
|
get_buffer_max_latency() : number;
|
||||||
output_supported: boolean;
|
set_buffer_max_latency(value: number);
|
||||||
|
|
||||||
input_default: boolean;
|
flush_buffer();
|
||||||
output_default: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace playback {
|
export interface OwnedAudioOutputStream extends AudioOutputStream {
|
||||||
export interface AudioOutputStream {
|
callback_underflow: () => any;
|
||||||
sample_rate: number;
|
callback_overflow: () => any;
|
||||||
channels: number;
|
|
||||||
|
|
||||||
get_buffer_latency() : number;
|
clear();
|
||||||
set_buffer_latency(value: number);
|
write_data(buffer: ArrayBuffer, interleaved: boolean);
|
||||||
|
write_data_rated(buffer: ArrayBuffer, interleaved: boolean, sample_rate: number);
|
||||||
|
|
||||||
get_buffer_max_latency() : number;
|
deleted() : boolean;
|
||||||
set_buffer_max_latency(value: number);
|
delete();
|
||||||
|
|
||||||
flush_buffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OwnedAudioOutputStream extends AudioOutputStream {
|
|
||||||
callback_underflow: () => any;
|
|
||||||
callback_overflow: () => any;
|
|
||||||
|
|
||||||
clear();
|
|
||||||
write_data(buffer: ArrayBuffer, interleaved: boolean);
|
|
||||||
write_data_rated(buffer: ArrayBuffer, interleaved: boolean, sample_rate: number);
|
|
||||||
|
|
||||||
deleted() : boolean;
|
|
||||||
delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function set_device(device_id: string);
|
|
||||||
export function current_device() : string;
|
|
||||||
|
|
||||||
export function create_stream() : OwnedAudioOutputStream;
|
|
||||||
|
|
||||||
export function get_master_volume() : number;
|
|
||||||
export function set_master_volume(volume: number);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace record {
|
export function set_device(device_id: string);
|
||||||
enum FilterMode {
|
export function current_device() : string;
|
||||||
Bypass,
|
|
||||||
Filter,
|
|
||||||
Block
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConsumeFilter {
|
export function create_stream() : OwnedAudioOutputStream;
|
||||||
get_name() : string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MarginedFilter {
|
export function get_master_volume() : number;
|
||||||
/* in seconds */
|
export function set_master_volume(volume: number);
|
||||||
get_margin_time() : number;
|
|
||||||
set_margin_time(value: number);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VADConsumeFilter extends ConsumeFilter, MarginedFilter {
|
|
||||||
get_level() : number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ThresholdConsumeFilter extends ConsumeFilter, MarginedFilter {
|
|
||||||
get_threshold() : number;
|
|
||||||
set_threshold(value: number);
|
|
||||||
|
|
||||||
get_attack_smooth() : number;
|
|
||||||
set_attack_smooth(value: number);
|
|
||||||
|
|
||||||
get_release_smooth() : number;
|
|
||||||
set_release_smooth(value: number);
|
|
||||||
|
|
||||||
set_analyze_filter(callback: (value: number) => any);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StateConsumeFilter extends ConsumeFilter {
|
|
||||||
is_consuming() : boolean;
|
|
||||||
set_consuming(flag: boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AudioConsumer {
|
|
||||||
readonly sampleRate: number;
|
|
||||||
readonly channelCount: number;
|
|
||||||
readonly frameSize: number;
|
|
||||||
|
|
||||||
get_filters() : ConsumeFilter[];
|
|
||||||
unregister_filter(filter: ConsumeFilter);
|
|
||||||
|
|
||||||
create_filter_vad(level: number) : VADConsumeFilter;
|
|
||||||
create_filter_threshold(threshold: number) : ThresholdConsumeFilter;
|
|
||||||
create_filter_state() : StateConsumeFilter;
|
|
||||||
|
|
||||||
set_filter_mode(mode: FilterMode);
|
|
||||||
get_filter_mode() : FilterMode;
|
|
||||||
|
|
||||||
toggle_rnnoise(enabled: boolean);
|
|
||||||
rnnoise_enabled() : boolean;
|
|
||||||
|
|
||||||
callback_data: (buffer: Float32Array) => any;
|
|
||||||
callback_ended: () => any;
|
|
||||||
callback_started: () => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DeviceSetResult = "success" | "invalid-device";
|
|
||||||
export interface AudioRecorder {
|
|
||||||
get_device() : string;
|
|
||||||
set_device(device_id: string, callback: (result: DeviceSetResult) => void); /* Recorder needs to be started afterwards */
|
|
||||||
|
|
||||||
start(callback: (result: boolean | string) => void);
|
|
||||||
started() : boolean;
|
|
||||||
stop();
|
|
||||||
|
|
||||||
get_volume() : number;
|
|
||||||
set_volume(volume: number);
|
|
||||||
|
|
||||||
create_consumer() : AudioConsumer;
|
|
||||||
get_consumers() : AudioConsumer[];
|
|
||||||
delete_consumer(consumer: AudioConsumer);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function create_recorder() : AudioRecorder;
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace sounds {
|
|
||||||
export enum PlaybackResult {
|
|
||||||
SUCCEEDED,
|
|
||||||
CANCELED,
|
|
||||||
SOUND_NOT_INITIALIZED,
|
|
||||||
FILE_OPEN_ERROR,
|
|
||||||
PLAYBACK_ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PlaybackSettings {
|
|
||||||
file: string;
|
|
||||||
volume?: number;
|
|
||||||
callback?: (result: PlaybackResult, message: string) => void;
|
|
||||||
}
|
|
||||||
export function playback_sound(settings: PlaybackSettings) : number;
|
|
||||||
export function cancel_playback(playback: number);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initialize(callback: () => any);
|
|
||||||
export function initialized() : boolean;
|
|
||||||
export function available_devices() : AudioDevice[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export namespace record {
|
||||||
|
enum FilterMode {
|
||||||
|
Bypass,
|
||||||
|
Filter,
|
||||||
|
Block
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConsumeFilter {
|
||||||
|
get_name() : string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarginedFilter {
|
||||||
|
/* in seconds */
|
||||||
|
get_margin_time() : number;
|
||||||
|
set_margin_time(value: number);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VADConsumeFilter extends ConsumeFilter, MarginedFilter {
|
||||||
|
get_level() : number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThresholdConsumeFilter extends ConsumeFilter, MarginedFilter {
|
||||||
|
get_threshold() : number;
|
||||||
|
set_threshold(value: number);
|
||||||
|
|
||||||
|
get_attack_smooth() : number;
|
||||||
|
set_attack_smooth(value: number);
|
||||||
|
|
||||||
|
get_release_smooth() : number;
|
||||||
|
set_release_smooth(value: number);
|
||||||
|
|
||||||
|
set_analyze_filter(callback: (value: number) => any);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StateConsumeFilter extends ConsumeFilter {
|
||||||
|
is_consuming() : boolean;
|
||||||
|
set_consuming(flag: boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AudioConsumer {
|
||||||
|
readonly sampleRate: number;
|
||||||
|
readonly channelCount: number;
|
||||||
|
readonly frameSize: number;
|
||||||
|
|
||||||
|
get_filters() : ConsumeFilter[];
|
||||||
|
unregister_filter(filter: ConsumeFilter);
|
||||||
|
|
||||||
|
create_filter_vad(level: number) : VADConsumeFilter;
|
||||||
|
create_filter_threshold(threshold: number) : ThresholdConsumeFilter;
|
||||||
|
create_filter_state() : StateConsumeFilter;
|
||||||
|
|
||||||
|
set_filter_mode(mode: FilterMode);
|
||||||
|
get_filter_mode() : FilterMode;
|
||||||
|
|
||||||
|
callback_data: (buffer: Float32Array) => any;
|
||||||
|
callback_ended: () => any;
|
||||||
|
callback_started: () => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AudioProcessorConfig {
|
||||||
|
"pipeline.maximum_internal_processing_rate": number,
|
||||||
|
"pipeline.multi_channel_render": boolean,
|
||||||
|
"pipeline.multi_channel_capture": boolean,
|
||||||
|
|
||||||
|
"pre_amplifier.enabled": boolean,
|
||||||
|
"pre_amplifier.fixed_gain_factor": number,
|
||||||
|
|
||||||
|
"high_pass_filter.enabled": boolean,
|
||||||
|
"high_pass_filter.apply_in_full_band": boolean,
|
||||||
|
|
||||||
|
"echo_canceller.enabled": boolean,
|
||||||
|
"echo_canceller.mobile_mode": boolean,
|
||||||
|
"echo_canceller.export_linear_aec_output": boolean,
|
||||||
|
"echo_canceller.enforce_high_pass_filtering": boolean,
|
||||||
|
|
||||||
|
"noise_suppression.enabled": boolean,
|
||||||
|
"noise_suppression.level": "low" | "moderate" | "high" | "very-high",
|
||||||
|
"noise_suppression.analyze_linear_aec_output_when_available": boolean,
|
||||||
|
|
||||||
|
"transient_suppression.enabled": boolean,
|
||||||
|
|
||||||
|
"voice_detection.enabled": boolean,
|
||||||
|
|
||||||
|
"gain_controller1.enabled": boolean,
|
||||||
|
"gain_controller1.mode": "adaptive-analog" | "adaptive-digital" | "fixed-digital",
|
||||||
|
"gain_controller1.target_level_dbfs": number,
|
||||||
|
"gain_controller1.compression_gain_db": number,
|
||||||
|
"gain_controller1.enable_limiter": boolean,
|
||||||
|
"gain_controller1.analog_level_minimum": number,
|
||||||
|
"gain_controller1.analog_level_maximum": number,
|
||||||
|
|
||||||
|
"gain_controller1.analog_gain_controller.enabled": boolean,
|
||||||
|
"gain_controller1.analog_gain_controller.startup_min_volume": number,
|
||||||
|
"gain_controller1.analog_gain_controller.clipped_level_min": number,
|
||||||
|
"gain_controller1.analog_gain_controller.enable_agc2_level_estimator": boolean,
|
||||||
|
"gain_controller1.analog_gain_controller.enable_digital_adaptive": boolean,
|
||||||
|
|
||||||
|
"gain_controller2.enabled": boolean,
|
||||||
|
|
||||||
|
"gain_controller2.fixed_digital.gain_db": number,
|
||||||
|
|
||||||
|
"gain_controller2.adaptive_digital.enabled": boolean,
|
||||||
|
"gain_controller2.adaptive_digital.vad_probability_attack": number,
|
||||||
|
"gain_controller2.adaptive_digital.level_estimator": "rms" | "peak",
|
||||||
|
"gain_controller2.adaptive_digital.level_estimator_adjacent_speech_frames_threshold": number,
|
||||||
|
"gain_controller2.adaptive_digital.use_saturation_protector": boolean,
|
||||||
|
"gain_controller2.adaptive_digital.initial_saturation_margin_db": number,
|
||||||
|
"gain_controller2.adaptive_digital.extra_saturation_margin_db": number,
|
||||||
|
"gain_controller2.adaptive_digital.gain_applier_adjacent_speech_frames_threshold": number,
|
||||||
|
"gain_controller2.adaptive_digital.max_gain_change_db_per_second": number,
|
||||||
|
"gain_controller2.adaptive_digital.max_output_noise_level_dbfs": number,
|
||||||
|
|
||||||
|
"residual_echo_detector.enabled": boolean,
|
||||||
|
"level_estimation.enabled": boolean,
|
||||||
|
"rnnoise.enabled": boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AudioProcessorStatistics {
|
||||||
|
output_rms_dbfs: number | undefined,
|
||||||
|
voice_detected: number | undefined,
|
||||||
|
echo_return_loss: number | undefined,
|
||||||
|
echo_return_loss_enhancement: number | undefined,
|
||||||
|
divergent_filter_fraction: number | undefined,
|
||||||
|
delay_median_ms: number | undefined,
|
||||||
|
delay_standard_deviation_ms: number | undefined,
|
||||||
|
residual_echo_likelihood: number | undefined,
|
||||||
|
residual_echo_likelihood_recent_max: number | undefined,
|
||||||
|
delay_ms: number | undefined,
|
||||||
|
rnnoise_volume: number | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AudioProcessor {
|
||||||
|
get_config() : AudioProcessorConfig;
|
||||||
|
apply_config(config: Partial<AudioProcessorConfig>);
|
||||||
|
|
||||||
|
get_statistics() : AudioProcessorStatistics;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DeviceSetResult = "success" | "invalid-device";
|
||||||
|
export interface AudioRecorder {
|
||||||
|
get_device() : string;
|
||||||
|
set_device(device_id: string, callback: (result: DeviceSetResult) => void); /* Recorder needs to be started afterwards */
|
||||||
|
|
||||||
|
start(callback: (result: boolean | string) => void);
|
||||||
|
started() : boolean;
|
||||||
|
stop();
|
||||||
|
|
||||||
|
get_volume() : number;
|
||||||
|
set_volume(volume: number);
|
||||||
|
|
||||||
|
create_consumer() : AudioConsumer;
|
||||||
|
get_consumers() : AudioConsumer[];
|
||||||
|
delete_consumer(consumer: AudioConsumer);
|
||||||
|
|
||||||
|
get_audio_processor() : AudioProcessor | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function create_recorder() : AudioRecorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace sounds {
|
||||||
|
export enum PlaybackResult {
|
||||||
|
SUCCEEDED,
|
||||||
|
CANCELED,
|
||||||
|
SOUND_NOT_INITIALIZED,
|
||||||
|
FILE_OPEN_ERROR,
|
||||||
|
PLAYBACK_ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlaybackSettings {
|
||||||
|
file: string;
|
||||||
|
volume?: number;
|
||||||
|
callback?: (result: PlaybackResult, message: string) => void;
|
||||||
|
}
|
||||||
|
export function playback_sound(settings: PlaybackSettings) : number;
|
||||||
|
export function cancel_playback(playback: number);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initialize(callback: () => any);
|
||||||
|
export function initialized() : boolean;
|
||||||
|
export function available_devices() : AudioDevice[];
|
||||||
}
|
}
|
@ -7,71 +7,69 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
|
|
||||||
namespace tc {
|
namespace tc::event {
|
||||||
namespace event {
|
class EventExecutor;
|
||||||
class EventExecutor;
|
class EventEntry {
|
||||||
class EventEntry {
|
friend class EventExecutor;
|
||||||
friend class EventExecutor;
|
public:
|
||||||
public:
|
virtual void event_execute(const std::chrono::system_clock::time_point& /* scheduled timestamp */) = 0;
|
||||||
virtual void event_execute(const std::chrono::system_clock::time_point& /* scheduled timestamp */) = 0;
|
virtual void event_execute_dropped(const std::chrono::system_clock::time_point& /* scheduled timestamp */) {}
|
||||||
virtual void event_execute_dropped(const std::chrono::system_clock::time_point& /* scheduled timestamp */) {}
|
|
||||||
|
|
||||||
std::unique_lock<std::timed_mutex> execute_lock(bool force) {
|
std::unique_lock<std::timed_mutex> execute_lock(bool force) {
|
||||||
if(force) {
|
if(force) {
|
||||||
return std::unique_lock<std::timed_mutex>(this->_execute_mutex);
|
return std::unique_lock<std::timed_mutex>(this->_execute_mutex);
|
||||||
} else {
|
} else {
|
||||||
auto lock = std::unique_lock<std::timed_mutex>(this->_execute_mutex, std::defer_lock);
|
auto lock = std::unique_lock<std::timed_mutex>(this->_execute_mutex, std::defer_lock);
|
||||||
if(this->execute_lock_timeout.count() > 0) {
|
if(this->execute_lock_timeout.count() > 0) {
|
||||||
(void) lock.try_lock_for(this->execute_lock_timeout);
|
(void) lock.try_lock_for(this->execute_lock_timeout);
|
||||||
} else {
|
} else {
|
||||||
(void) lock.try_lock();
|
(void) lock.try_lock();
|
||||||
}
|
}
|
||||||
return lock;
|
return lock;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool single_thread_executed() const { return this->_single_thread; }
|
[[nodiscard]] inline bool single_thread_executed() const { return this->_single_thread; }
|
||||||
inline void single_thread_executed(bool value) { this->_single_thread = value; }
|
inline void single_thread_executed(bool value) { this->_single_thread = value; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::chrono::nanoseconds execute_lock_timeout{0};
|
std::chrono::nanoseconds execute_lock_timeout{0};
|
||||||
private:
|
private:
|
||||||
void* _event_ptr = nullptr;
|
void* _event_ptr = nullptr;
|
||||||
bool _single_thread = true; /* if its set to true there might are some dropped executes! */
|
bool _single_thread = true; /* if its set to true there might are some dropped executes! */
|
||||||
std::timed_mutex _execute_mutex;
|
std::timed_mutex _execute_mutex;
|
||||||
};
|
};
|
||||||
|
|
||||||
class EventExecutor {
|
class EventExecutor {
|
||||||
public:
|
public:
|
||||||
explicit EventExecutor(const std::string& /* thread prefix */);
|
explicit EventExecutor(const std::string& /* thread prefix */);
|
||||||
virtual ~EventExecutor();
|
virtual ~EventExecutor();
|
||||||
|
|
||||||
bool initialize(int /* num threads */);
|
bool initialize(int /* num threads */);
|
||||||
bool schedule(const std::shared_ptr<EventEntry>& /* entry */);
|
bool schedule(const std::shared_ptr<EventEntry>& /* entry */);
|
||||||
bool cancel(const std::shared_ptr<EventEntry>& /* entry */); /* Note: Will not cancel already running executes */
|
bool cancel(const std::shared_ptr<EventEntry>& /* entry */); /* Note: Will not cancel already running executes */
|
||||||
void shutdown();
|
void shutdown();
|
||||||
private:
|
private:
|
||||||
struct LinkedEntry {
|
struct LinkedEntry {
|
||||||
LinkedEntry* previous;
|
LinkedEntry* previous;
|
||||||
LinkedEntry* next;
|
LinkedEntry* next;
|
||||||
|
|
||||||
std::chrono::system_clock::time_point scheduled;
|
std::chrono::system_clock::time_point scheduled;
|
||||||
std::weak_ptr<EventEntry> entry;
|
std::weak_ptr<EventEntry> entry;
|
||||||
};
|
};
|
||||||
static void _executor(EventExecutor*);
|
static void _executor(EventExecutor*);
|
||||||
void _shutdown(std::unique_lock<std::mutex>&);
|
void _shutdown(std::unique_lock<std::mutex>&);
|
||||||
void _reset_events(std::unique_lock<std::mutex>&);
|
void _reset_events(std::unique_lock<std::mutex>&);
|
||||||
|
|
||||||
bool should_shutdown = true;
|
bool should_shutdown = true;
|
||||||
|
|
||||||
std::vector<std::thread> threads;
|
std::vector<std::thread> threads;
|
||||||
std::mutex lock;
|
std::mutex lock;
|
||||||
std::condition_variable condition;
|
std::condition_variable condition;
|
||||||
|
|
||||||
LinkedEntry* head = nullptr;
|
LinkedEntry* head = nullptr;
|
||||||
LinkedEntry* tail = nullptr;
|
LinkedEntry* tail = nullptr;
|
||||||
|
|
||||||
std::string thread_prefix;
|
std::string thread_prefix;
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,21 +1,25 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "./AudioInput.h"
|
#include "./AudioInput.h"
|
||||||
#include "./AudioReframer.h"
|
#include "./AudioReframer.h"
|
||||||
#include "./AudioResampler.h"
|
#include "./AudioResampler.h"
|
||||||
#include "./AudioMerger.h"
|
#include "./AudioMerger.h"
|
||||||
|
#include "./AudioGain.h"
|
||||||
|
#include "./AudioInterleaved.h"
|
||||||
|
#include "./AudioOutput.h"
|
||||||
|
#include "./processing/AudioProcessor.h"
|
||||||
#include "../logger.h"
|
#include "../logger.h"
|
||||||
#include "AudioGain.h"
|
#include "AudioEventLoop.h"
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace tc;
|
using namespace tc;
|
||||||
using namespace tc::audio;
|
using namespace tc::audio;
|
||||||
|
|
||||||
AudioConsumer::AudioConsumer(tc::audio::AudioInput *handle, size_t channel_count, size_t sample_rate, size_t frame_size) :
|
AudioConsumer::AudioConsumer(size_t channel_count, size_t sample_rate, size_t frame_size) :
|
||||||
handle(handle),
|
channel_count{channel_count},
|
||||||
channel_count(channel_count),
|
sample_rate{sample_rate},
|
||||||
sample_rate(sample_rate) ,
|
frame_size{frame_size} {
|
||||||
frame_size(frame_size) {
|
|
||||||
if(this->frame_size > 0) {
|
if(this->frame_size > 0) {
|
||||||
this->reframer = std::make_unique<InputReframer>(channel_count, frame_size);
|
this->reframer = std::make_unique<InputReframer>(channel_count, frame_size);
|
||||||
this->reframer->on_frame = [&](const void* buffer) { this->handle_framed_data(buffer, this->frame_size); };
|
this->reframer->on_frame = [&](const void* buffer) { this->handle_framed_data(buffer, this->frame_size); };
|
||||||
@ -40,15 +44,60 @@ void AudioConsumer::process_data(const void *buffer, size_t samples) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioInput::AudioInput(size_t channels, size_t rate) : _channel_count(channels), _sample_rate(rate) {}
|
extern tc::audio::AudioOutput* global_audio_output;
|
||||||
AudioInput::~AudioInput() {
|
AudioInput::AudioInput(size_t channels, size_t sample_rate) :
|
||||||
this->close_device();
|
channel_count_{channels},
|
||||||
|
sample_rate_{sample_rate},
|
||||||
|
input_buffer{(sample_rate * channels * sizeof(float) * kInputBufferCapacityMs) / 1000}
|
||||||
|
{
|
||||||
|
this->event_loop_entry = std::make_shared<EventLoopCallback>(this);
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard lock(this->consumers_lock);
|
this->initialize_hook_handle = std::make_shared<AudioInitializeHook>();
|
||||||
for(const auto& consumer : this->_consumers)
|
this->initialize_hook_handle->input = this;
|
||||||
consumer->handle = nullptr;
|
|
||||||
|
std::weak_ptr weak_handle{this->initialize_hook_handle};
|
||||||
|
audio::initialize([weak_handle] {
|
||||||
|
auto handle = weak_handle.lock();
|
||||||
|
if(!handle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard lock{handle->mutex};
|
||||||
|
if(!handle->input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto processor = std::make_shared<AudioProcessor>();
|
||||||
|
if(!processor->initialize()) {
|
||||||
|
log_error(category::audio, tr("Failed to initialize audio processor."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global_audio_output->register_audio_processor(processor);
|
||||||
|
handle->input->audio_processor_ = processor;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
free(this->resample_buffer);
|
}
|
||||||
|
|
||||||
|
AudioInput::~AudioInput() {
|
||||||
|
{
|
||||||
|
std::lock_guard lock{this->initialize_hook_handle->mutex};
|
||||||
|
this->initialize_hook_handle->input = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
audio::encode_event_loop->cancel(this->event_loop_entry);
|
||||||
|
this->event_loop_entry->execute_lock(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this->audio_processor_) {
|
||||||
|
assert(global_audio_output);
|
||||||
|
global_audio_output->unregister_audio_processor(this->audio_processor_);
|
||||||
|
this->audio_processor_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->close_device();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioInput::set_device(const std::shared_ptr<AudioDevice> &device) {
|
void AudioInput::set_device(const std::shared_ptr<AudioDevice> &device) {
|
||||||
@ -68,7 +117,7 @@ void AudioInput::close_device() {
|
|||||||
this->input_recorder->stop_if_possible();
|
this->input_recorder->stop_if_possible();
|
||||||
this->input_recorder.reset();
|
this->input_recorder.reset();
|
||||||
}
|
}
|
||||||
this->_resampler = nullptr;
|
this->resampler_ = nullptr;
|
||||||
this->input_device = nullptr;
|
this->input_device = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +139,7 @@ bool AudioInput::record(std::string& error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(this->input_recorder->sample_rate() != this->sample_rate()) {
|
if(this->input_recorder->sample_rate() != this->sample_rate()) {
|
||||||
this->_resampler = std::make_unique<AudioResampler>(this->input_recorder->sample_rate(), this->sample_rate(), this->_channel_count);
|
this->resampler_ = std::make_unique<AudioResampler>(this->input_recorder->sample_rate(), this->sample_rate(), this->channel_count_);
|
||||||
}
|
}
|
||||||
|
|
||||||
this->input_recorder->register_consumer(this);
|
this->input_recorder->register_consumer(this);
|
||||||
@ -103,7 +152,7 @@ bool AudioInput::record(std::string& error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool AudioInput::recording() {
|
bool AudioInput::recording() {
|
||||||
return !!this->input_recorder;
|
return this->input_recorder != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioInput::stop() {
|
void AudioInput::stop() {
|
||||||
@ -114,80 +163,162 @@ void AudioInput::stop() {
|
|||||||
this->input_recorder.reset();
|
this->input_recorder.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::shared_ptr<AudioConsumer>> AudioInput::consumers() {
|
||||||
|
std::vector<std::shared_ptr<AudioConsumer>> result{};
|
||||||
|
result.reserve(10);
|
||||||
|
|
||||||
|
std::lock_guard consumer_lock{this->consumers_mutex};
|
||||||
|
result.reserve(this->consumers_.size());
|
||||||
|
|
||||||
|
this->consumers_.erase(std::remove_if(this->consumers_.begin(), this->consumers_.end(), [&](const std::weak_ptr<AudioConsumer>& weak_consumer) {
|
||||||
|
auto consumer = weak_consumer.lock();
|
||||||
|
if(!consumer) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push_back(consumer);
|
||||||
|
return false;
|
||||||
|
}), this->consumers_.end());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
std::shared_ptr<AudioConsumer> AudioInput::create_consumer(size_t frame_length) {
|
std::shared_ptr<AudioConsumer> AudioInput::create_consumer(size_t frame_length) {
|
||||||
auto result = std::shared_ptr<AudioConsumer>(new AudioConsumer(this, this->_channel_count, this->_sample_rate, frame_length));
|
auto result = std::shared_ptr<AudioConsumer>(new AudioConsumer(this->channel_count_, this->sample_rate_, frame_length));
|
||||||
{
|
{
|
||||||
std::lock_guard lock(this->consumers_lock);
|
std::lock_guard lock(this->consumers_mutex);
|
||||||
this->_consumers.push_back(result);
|
this->consumers_.push_back(result);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioInput::delete_consumer(const std::shared_ptr<AudioConsumer> &source) {
|
void AudioInput::allocate_input_buffer_samples(size_t samples) {
|
||||||
{
|
const auto expected_byte_size = samples * this->channel_count_ * sizeof(float);
|
||||||
std::lock_guard lock(this->consumers_lock);
|
if(expected_byte_size > this->input_buffer.capacity()) {
|
||||||
auto it = find(this->_consumers.begin(), this->_consumers.end(), source);
|
log_critical(category::audio, tr("Resampled audio input data would be larger than our input buffer capacity."));
|
||||||
if(it != this->_consumers.end())
|
return;
|
||||||
this->_consumers.erase(it);
|
|
||||||
}
|
|
||||||
|
|
||||||
source->handle = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioInput::consume(const void *input, size_t frameCount, size_t channels) {
|
|
||||||
if(channels != this->_channel_count) {
|
|
||||||
if(channels < 1 || channels > 2) {
|
|
||||||
log_critical(category::audio, tr("Channel count miss match (Received: {})!"), channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->ensure_resample_buffer_capacity(frameCount * this->_channel_count * sizeof(float));
|
|
||||||
audio::merge::merge_channels_interleaved(this->resample_buffer, this->_channel_count, input, channels, frameCount);
|
|
||||||
input = this->resample_buffer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this->_resampler) {
|
if(this->input_buffer.free_count() < expected_byte_size) {
|
||||||
const auto expected_size = this->_resampler->estimated_output_size(frameCount);
|
log_warn(category::audio, tr("Audio input buffer overflow."));
|
||||||
const auto expected_byte_size = expected_size * this->_channel_count * sizeof(float);
|
|
||||||
this->ensure_resample_buffer_capacity(expected_byte_size);
|
|
||||||
|
|
||||||
size_t sample_count{expected_size};
|
const auto free_samples = this->input_buffer.free_count() / this->channel_count_ / sizeof(float);
|
||||||
if(!this->_resampler->process(this->resample_buffer, input, frameCount, sample_count)) {
|
assert(samples >= free_samples);
|
||||||
|
|
||||||
|
const auto missing_samples = samples - free_samples;
|
||||||
|
this->input_buffer.advance_read_ptr(missing_samples * this->channel_count_ * sizeof(float));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioInput::consume(const void *input, size_t sample_count, size_t channels) {
|
||||||
|
constexpr static auto kTempBufferMaxSampleCount{1024 * 8};
|
||||||
|
float temp_buffer[kTempBufferMaxSampleCount];
|
||||||
|
|
||||||
|
/* TODO: Short circuit for silence here */
|
||||||
|
|
||||||
|
if(channels != this->channel_count_) {
|
||||||
|
if(channels < 1 || channels > 2) {
|
||||||
|
log_critical(category::audio, tr("AudioInput received audio data with an unsupported channel count of {}."), channels);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(sample_count * this->channel_count_ > kTempBufferMaxSampleCount) {
|
||||||
|
log_critical(category::audio, tr("Received audio chunk bigger than our temp stack buffer. Received {} samples but can hold only {}."), sample_count, kTempBufferMaxSampleCount / this->channel_count_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio::merge::merge_channels_interleaved(temp_buffer, this->channel_count_, input, channels, sample_count);
|
||||||
|
input = temp_buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this->resampler_) {
|
||||||
|
const auto expected_size = this->resampler_->estimated_output_size(sample_count);
|
||||||
|
this->allocate_input_buffer_samples(expected_size);
|
||||||
|
|
||||||
|
size_t resampled_sample_count{expected_size};
|
||||||
|
if(!this->resampler_->process(this->input_buffer.write_ptr(), input, sample_count, resampled_sample_count)) {
|
||||||
log_error(category::audio, tr("Failed to resample input audio."));
|
log_error(category::audio, tr("Failed to resample input audio."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
frameCount = sample_count;
|
this->input_buffer.advance_write_ptr(resampled_sample_count * this->channel_count_ * sizeof(float));
|
||||||
input = this->resample_buffer;
|
} else {
|
||||||
|
this->allocate_input_buffer_samples(sample_count);
|
||||||
|
|
||||||
audio::apply_gain(this->resample_buffer, this->_channel_count, frameCount, this->_volume);
|
const auto sample_byte_size = sample_count * this->channel_count_ * sizeof(float);
|
||||||
} else if(this->_volume != 1) {
|
memcpy(this->input_buffer.write_ptr(), input, sample_byte_size);
|
||||||
const auto byte_size = frameCount * this->_channel_count * sizeof(float);
|
this->input_buffer.advance_write_ptr(sample_byte_size);
|
||||||
this->ensure_resample_buffer_capacity(byte_size);
|
|
||||||
|
|
||||||
if(this->resample_buffer != input) {
|
|
||||||
memcpy(this->resample_buffer, input, byte_size);
|
|
||||||
input = this->resample_buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
audio::apply_gain(this->resample_buffer, this->_channel_count, frameCount, this->_volume);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto begin = std::chrono::system_clock::now();
|
audio::encode_event_loop->schedule(this->event_loop_entry);
|
||||||
for(const auto& consumer : this->consumers()) {
|
|
||||||
consumer->process_data(input, frameCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto end = std::chrono::system_clock::now();
|
|
||||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count();
|
|
||||||
if(ms > 5) {
|
|
||||||
log_warn(category::audio, tr("Processing of audio input needed {}ms. This could be an issue!"), std::chrono::duration_cast<chrono::milliseconds>(end - begin).count());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioInput::ensure_resample_buffer_capacity(size_t size) {
|
|
||||||
if(this->resample_buffer_size < size) {
|
void AudioInput::process_audio() {
|
||||||
free(this->resample_buffer);
|
const auto chunk_sample_count = (kChunkSizeMs * this->sample_rate_) / 1000;
|
||||||
this->resample_buffer = malloc(size);
|
while(true) {
|
||||||
this->resample_buffer_size = size;
|
auto available_sample_count = this->input_buffer.fill_count() / this->channel_count_ / sizeof(float);
|
||||||
|
if(available_sample_count < chunk_sample_count) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto input = this->input_buffer.read_ptr();
|
||||||
|
/*
|
||||||
|
* It's save to mutate the current memory.
|
||||||
|
* If overflows occur it could lead to wired artifacts but all memory access is save.
|
||||||
|
*/
|
||||||
|
this->process_audio_chunk((void*) input);
|
||||||
|
this->input_buffer.advance_read_ptr(chunk_sample_count * this->channel_count_ * sizeof(float));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AudioInput::process_audio_chunk(void *chunk) {
|
||||||
|
constexpr static auto kTempSampleBufferSize{1024 * 8};
|
||||||
|
constexpr static auto kMaxChannelCount{32};
|
||||||
|
|
||||||
|
const auto chunk_sample_count = this->chunk_sample_count();
|
||||||
|
float temp_sample_buffer[kTempSampleBufferSize];
|
||||||
|
float out_sample_buffer[kTempSampleBufferSize];
|
||||||
|
assert(memset(temp_sample_buffer, 0, sizeof(float) * kTempSampleBufferSize));
|
||||||
|
assert(memset(out_sample_buffer, 0, sizeof(float) * kTempSampleBufferSize));
|
||||||
|
|
||||||
|
if(auto processor{this->audio_processor_}; processor) {
|
||||||
|
assert(kTempSampleBufferSize >= chunk_sample_count * this->channel_count_ * sizeof(float));
|
||||||
|
|
||||||
|
audio::deinterleave(temp_sample_buffer, (const float*) chunk, this->channel_count_, chunk_sample_count);
|
||||||
|
webrtc::StreamConfig stream_config{(int) this->sample_rate_, this->channel_count_};
|
||||||
|
|
||||||
|
float* channel_ptr[kMaxChannelCount];
|
||||||
|
assert(memset(channel_ptr, 0, sizeof(float*) * kMaxChannelCount));
|
||||||
|
for(size_t channel{0}; channel < this->channel_count_; channel++) {
|
||||||
|
channel_ptr[channel] = temp_sample_buffer + (channel * chunk_sample_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto process_statistics = processor->process_stream(stream_config, channel_ptr);
|
||||||
|
if(!process_statistics.has_value()) {
|
||||||
|
/* TODO: Some kind of error message? */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio::interleave_vec(out_sample_buffer, channel_ptr, this->channel_count_, chunk_sample_count);
|
||||||
|
chunk = out_sample_buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO: Is this even needed if we have the processor? */
|
||||||
|
audio::apply_gain(chunk, this->channel_count_, chunk_sample_count, this->volume_);
|
||||||
|
|
||||||
|
auto begin = std::chrono::system_clock::now();
|
||||||
|
for(const auto& consumer : this->consumers()) {
|
||||||
|
consumer->process_data(out_sample_buffer, chunk_sample_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto end = std::chrono::system_clock::now();
|
||||||
|
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count();
|
||||||
|
if(ms > 5) {
|
||||||
|
log_warn(category::audio, tr("Processing of audio input needed {}ms. This could be an issue!"), std::chrono::duration_cast<chrono::milliseconds>(end - begin).count());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioInput::EventLoopCallback::event_execute(const chrono::system_clock::time_point &point) {
|
||||||
|
this->input->process_audio();
|
||||||
|
}
|
||||||
|
@ -5,29 +5,30 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include "AudioSamples.h"
|
#include "./AudioSamples.h"
|
||||||
#include "driver/AudioDriver.h"
|
#include "./driver/AudioDriver.h"
|
||||||
|
#include "../ring_buffer.h"
|
||||||
|
#include "../EventLoop.h"
|
||||||
|
|
||||||
class AudioInputSource;
|
|
||||||
namespace tc::audio {
|
namespace tc::audio {
|
||||||
class AudioInput;
|
class AudioInput;
|
||||||
class InputReframer;
|
class InputReframer;
|
||||||
class AudioResampler;
|
class AudioResampler;
|
||||||
|
class AudioProcessor;
|
||||||
|
class AudioInputSource;
|
||||||
|
|
||||||
class AudioConsumer {
|
class AudioConsumer {
|
||||||
friend class AudioInput;
|
friend class AudioInput;
|
||||||
public:
|
public:
|
||||||
AudioInput* handle;
|
size_t const channel_count;
|
||||||
|
size_t const sample_rate;
|
||||||
|
|
||||||
size_t const channel_count = 0;
|
size_t const frame_size;
|
||||||
size_t const sample_rate = 0;
|
|
||||||
|
|
||||||
size_t const frame_size = 0;
|
|
||||||
|
|
||||||
std::mutex on_read_lock{}; /* locked to access the function */
|
std::mutex on_read_lock{}; /* locked to access the function */
|
||||||
std::function<void(const void* /* buffer */, size_t /* samples */)> on_read;
|
std::function<void(const void* /* buffer */, size_t /* samples */)> on_read;
|
||||||
private:
|
private:
|
||||||
AudioConsumer(AudioInput* handle, size_t channel_count, size_t sample_rate, size_t frame_size);
|
AudioConsumer(size_t channel_count, size_t sample_rate, size_t frame_size);
|
||||||
|
|
||||||
std::unique_ptr<InputReframer> reframer;
|
std::unique_ptr<InputReframer> reframer;
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ namespace tc::audio {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class AudioInput : public AudioDeviceRecord::Consumer {
|
class AudioInput : public AudioDeviceRecord::Consumer {
|
||||||
friend class ::AudioInputSource;
|
friend class AudioInputSource;
|
||||||
public:
|
public:
|
||||||
AudioInput(size_t /* channels */, size_t /* sample rate */);
|
AudioInput(size_t /* channels */, size_t /* sample rate */);
|
||||||
virtual ~AudioInput();
|
virtual ~AudioInput();
|
||||||
@ -49,38 +50,57 @@ namespace tc::audio {
|
|||||||
[[nodiscard]] bool recording();
|
[[nodiscard]] bool recording();
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
std::deque<std::shared_ptr<AudioConsumer>> consumers() {
|
[[nodiscard]] std::vector<std::shared_ptr<AudioConsumer>> consumers();
|
||||||
std::lock_guard lock(this->consumers_lock);
|
[[nodiscard]] std::shared_ptr<AudioConsumer> create_consumer(size_t /* frame size */);
|
||||||
return this->_consumers;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<AudioConsumer> create_consumer(size_t /* frame size */);
|
[[nodiscard]] inline auto audio_processor() { return this->audio_processor_; }
|
||||||
void delete_consumer(const std::shared_ptr<AudioConsumer>& /* source */);
|
|
||||||
|
|
||||||
[[nodiscard]] inline size_t channel_count() const { return this->_channel_count; }
|
[[nodiscard]] inline size_t channel_count() const { return this->channel_count_; }
|
||||||
[[nodiscard]] inline size_t sample_rate() const { return this->_sample_rate; }
|
[[nodiscard]] inline size_t sample_rate() const { return this->sample_rate_; }
|
||||||
|
|
||||||
[[nodiscard]] inline float volume() const { return this->_volume; }
|
[[nodiscard]] inline float volume() const { return this->volume_; }
|
||||||
inline void set_volume(float value) { this->_volume = value; }
|
inline void set_volume(float value) { this->volume_ = value; }
|
||||||
private:
|
private:
|
||||||
|
constexpr static auto kInputBufferCapacityMs{750};
|
||||||
|
constexpr static auto kChunkSizeMs{10}; /* Must be 10ms else the audio processor will fuck up */
|
||||||
|
|
||||||
|
struct EventLoopCallback : public event::EventEntry {
|
||||||
|
AudioInput* const input;
|
||||||
|
|
||||||
|
explicit EventLoopCallback(AudioInput* input) : input{input} {}
|
||||||
|
void event_execute(const std::chrono::system_clock::time_point &point) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AudioInitializeHook {
|
||||||
|
std::mutex mutex{};
|
||||||
|
AudioInput* input{nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
void consume(const void *, size_t, size_t) override;
|
void consume(const void *, size_t, size_t) override;
|
||||||
void ensure_resample_buffer_capacity(size_t);
|
void process_audio();
|
||||||
|
void process_audio_chunk(void *);
|
||||||
|
|
||||||
size_t const _channel_count;
|
size_t const channel_count_;
|
||||||
size_t const _sample_rate;
|
size_t const sample_rate_;
|
||||||
|
|
||||||
std::mutex consumers_lock;
|
std::mutex consumers_mutex{};
|
||||||
std::deque<std::shared_ptr<AudioConsumer>> _consumers;
|
std::deque<std::weak_ptr<AudioConsumer>> consumers_{};
|
||||||
std::recursive_mutex input_source_lock;
|
std::recursive_mutex input_source_lock{};
|
||||||
|
|
||||||
std::unique_ptr<AudioResampler> _resampler{nullptr};
|
std::shared_ptr<EventLoopCallback> event_loop_entry{};
|
||||||
|
ring_buffer input_buffer;
|
||||||
|
|
||||||
|
std::unique_ptr<AudioResampler> resampler_{nullptr};
|
||||||
std::shared_ptr<AudioDevice> input_device{};
|
std::shared_ptr<AudioDevice> input_device{};
|
||||||
|
|
||||||
void* resample_buffer{nullptr};
|
float volume_{1.f};
|
||||||
size_t resample_buffer_size{0};
|
|
||||||
|
|
||||||
float _volume{1.f};
|
|
||||||
|
|
||||||
std::shared_ptr<AudioDeviceRecord> input_recorder{};
|
std::shared_ptr<AudioDeviceRecord> input_recorder{};
|
||||||
|
std::shared_ptr<AudioProcessor> audio_processor_{};
|
||||||
|
|
||||||
|
std::shared_ptr<AudioInitializeHook> initialize_hook_handle{};
|
||||||
|
|
||||||
|
void allocate_input_buffer_samples(size_t /* sample count */);
|
||||||
|
[[nodiscard]] inline auto chunk_sample_count() const { return (kChunkSizeMs * this->sample_rate_) / 1000; }
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -3,6 +3,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
#include <cstring>
|
||||||
#include "AudioInterleaved.h"
|
#include "AudioInterleaved.h"
|
||||||
|
|
||||||
using namespace tc;
|
using namespace tc;
|
||||||
@ -33,6 +34,15 @@ void audio::interleave(float *target, const float *source, size_t channel_count,
|
|||||||
source_ptr[channel] = source + (channel * sample_count);
|
source_ptr[channel] = source + (channel * sample_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audio::interleave_vec(target, source_ptr, channel_count, sample_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void audio::interleave_vec(float *target, const float *const *source, size_t channel_count, size_t sample_count) {
|
||||||
|
assert(channel_count <= kMaxChannelCount);
|
||||||
|
|
||||||
|
const float* source_ptr[kMaxChannelCount];
|
||||||
|
memcpy(source_ptr, source, channel_count * sizeof(float*));
|
||||||
|
|
||||||
for(size_t sample{0}; sample < sample_count; sample++) {
|
for(size_t sample{0}; sample < sample_count; sample++) {
|
||||||
for(size_t channel{0}; channel < channel_count; channel++) {
|
for(size_t channel{0}; channel < channel_count; channel++) {
|
||||||
*target++ = *source_ptr[channel]++;
|
*target++ = *source_ptr[channel]++;
|
||||||
|
@ -14,4 +14,11 @@ namespace tc::audio {
|
|||||||
size_t /* channel count */,
|
size_t /* channel count */,
|
||||||
size_t /* sample count */
|
size_t /* sample count */
|
||||||
);
|
);
|
||||||
|
|
||||||
|
extern void interleave_vec(
|
||||||
|
float* /* dest */,
|
||||||
|
const float * const * /* sources */,
|
||||||
|
size_t /* channel count */,
|
||||||
|
size_t /* sample count */
|
||||||
|
);
|
||||||
}
|
}
|
@ -10,8 +10,9 @@ InputReframer::InputReframer(size_t channels, size_t frame_size) : _frame_size(f
|
|||||||
}
|
}
|
||||||
|
|
||||||
InputReframer::~InputReframer() {
|
InputReframer::~InputReframer() {
|
||||||
if(this->buffer)
|
if(this->buffer) {
|
||||||
free(this->buffer);
|
free(this->buffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void InputReframer::process(const void *source, size_t samples) {
|
void InputReframer::process(const void *source, size_t samples) {
|
||||||
@ -23,15 +24,15 @@ void InputReframer::process(const void *source, size_t samples) {
|
|||||||
if(this->_buffer_index > 0) {
|
if(this->_buffer_index > 0) {
|
||||||
if(this->_buffer_index + samples > this->_frame_size) {
|
if(this->_buffer_index + samples > this->_frame_size) {
|
||||||
auto required = this->_frame_size - this->_buffer_index;
|
auto required = this->_frame_size - this->_buffer_index;
|
||||||
auto length = required * this->_channels * 4;
|
auto length = required * this->_channels * sizeof(float);
|
||||||
|
|
||||||
memcpy((char*) this->buffer + this->_buffer_index * 4 * this->_channels, source, length);
|
memcpy((char*) this->buffer + this->_buffer_index * sizeof(float) * this->_channels, source, length);
|
||||||
samples -= required;
|
samples -= required;
|
||||||
source = (char*) source + length;
|
source = (char*) source + length;
|
||||||
|
|
||||||
this->on_frame(this->buffer);
|
this->on_frame(this->buffer);
|
||||||
} else {
|
} else {
|
||||||
memcpy((char*) this->buffer + this->_buffer_index * 4 * this->_channels, source, samples * this->_channels * 4);
|
memcpy((char*) this->buffer + this->_buffer_index * sizeof(float) * this->_channels, source, samples * this->_channels * sizeof(float));
|
||||||
this->_buffer_index += samples;
|
this->_buffer_index += samples;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -39,12 +40,16 @@ void InputReframer::process(const void *source, size_t samples) {
|
|||||||
|
|
||||||
auto _on_frame = this->on_frame;
|
auto _on_frame = this->on_frame;
|
||||||
while(samples > this->_frame_size) {
|
while(samples > this->_frame_size) {
|
||||||
if(_on_frame)
|
if(_on_frame) {
|
||||||
_on_frame(source);
|
_on_frame(source);
|
||||||
|
}
|
||||||
|
|
||||||
samples -= this->_frame_size;
|
samples -= this->_frame_size;
|
||||||
source = (char*) source + this->_frame_size * this->_channels * 4;
|
source = (char*) source + this->_frame_size * this->_channels * sizeof(float);
|
||||||
}
|
}
|
||||||
if(samples > 0)
|
if(samples > 0) {
|
||||||
memcpy((char*) this->buffer, source, samples * this->_channels * 4);
|
memcpy((char*) this->buffer, source, samples * this->_channels * sizeof(float));
|
||||||
|
}
|
||||||
|
|
||||||
this->_buffer_index = samples;
|
this->_buffer_index = samples;
|
||||||
}
|
}
|
@ -1,4 +1,3 @@
|
|||||||
#include <iostream>
|
|
||||||
#include "FilterVad.h"
|
#include "FilterVad.h"
|
||||||
#include "../AudioMerger.h"
|
#include "../AudioMerger.h"
|
||||||
#include "../../logger.h"
|
#include "../../logger.h"
|
||||||
|
@ -32,9 +32,6 @@ NAN_MODULE_INIT(AudioConsumerWrapper::Init) {
|
|||||||
Nan::SetPrototypeMethod(klass, "get_filter_mode", AudioConsumerWrapper::_get_filter_mode);
|
Nan::SetPrototypeMethod(klass, "get_filter_mode", AudioConsumerWrapper::_get_filter_mode);
|
||||||
Nan::SetPrototypeMethod(klass, "set_filter_mode", AudioConsumerWrapper::_set_filter_mode);
|
Nan::SetPrototypeMethod(klass, "set_filter_mode", AudioConsumerWrapper::_set_filter_mode);
|
||||||
|
|
||||||
Nan::SetPrototypeMethod(klass, "rnnoise_enabled", AudioConsumerWrapper::rnnoise_enabled);
|
|
||||||
Nan::SetPrototypeMethod(klass, "toggle_rnnoise", AudioConsumerWrapper::toggle_rnnoise);
|
|
||||||
|
|
||||||
constructor_template().Reset(klass);
|
constructor_template().Reset(klass);
|
||||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||||
}
|
}
|
||||||
@ -61,17 +58,6 @@ AudioConsumerWrapper::~AudioConsumerWrapper() {
|
|||||||
|
|
||||||
lock_guard lock{this->execute_mutex};
|
lock_guard lock{this->execute_mutex};
|
||||||
this->unbind();
|
this->unbind();
|
||||||
if(this->_handle->handle) {
|
|
||||||
this->_handle->handle->delete_consumer(this->_handle);
|
|
||||||
this->_handle = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(auto& instance : this->rnnoise_processor) {
|
|
||||||
if(!instance) { continue; }
|
|
||||||
|
|
||||||
rnnoise_destroy((DenoiseState*) instance);
|
|
||||||
instance = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(auto index{0}; index < kInternalFrameBufferCount; index++) {
|
for(auto index{0}; index < kInternalFrameBufferCount; index++) {
|
||||||
if(!this->internal_frame_buffer[index]) { continue; }
|
if(!this->internal_frame_buffer[index]) { continue; }
|
||||||
@ -104,9 +90,11 @@ void AudioConsumerWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
|
|||||||
std::unique_ptr<DataEntry> buffer;
|
std::unique_ptr<DataEntry> buffer;
|
||||||
while(true) {
|
while(true) {
|
||||||
{
|
{
|
||||||
lock_guard lock(this->_data_lock);
|
lock_guard lock{this->_data_lock};
|
||||||
if(this->_data_entries.empty())
|
if(this->_data_entries.empty()) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
buffer = move(this->_data_entries.front());
|
buffer = move(this->_data_entries.front());
|
||||||
this->_data_entries.pop_front();
|
this->_data_entries.pop_front();
|
||||||
}
|
}
|
||||||
@ -155,7 +143,6 @@ void AudioConsumerWrapper::unbind() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const float kRnNoiseScale = -INT16_MIN;
|
|
||||||
void AudioConsumerWrapper::process_data(const void *buffer, size_t samples) {
|
void AudioConsumerWrapper::process_data(const void *buffer, size_t samples) {
|
||||||
if(samples != 960) {
|
if(samples != 960) {
|
||||||
logger::error(logger::category::audio, tr("Received audio frame with invalid sample count (Expected 960, Received {})"), samples);
|
logger::error(logger::category::audio, tr("Received audio frame with invalid sample count (Expected 960, Received {})"), samples);
|
||||||
@ -165,67 +152,6 @@ void AudioConsumerWrapper::process_data(const void *buffer, size_t samples) {
|
|||||||
lock_guard lock{this->execute_mutex};
|
lock_guard lock{this->execute_mutex};
|
||||||
if(this->filter_mode_ == FilterMode::BLOCK) { return; }
|
if(this->filter_mode_ == FilterMode::BLOCK) { return; }
|
||||||
|
|
||||||
/* apply input modifiers */
|
|
||||||
if(this->rnnoise) {
|
|
||||||
/* TODO: don't call reserve_internal_buffer every time and assume the buffers are initialized */
|
|
||||||
/* TODO: Maybe find out if the microphone is some kind of pseudo stero so we can handle it as mono? */
|
|
||||||
|
|
||||||
if(this->_handle->channel_count > 1) {
|
|
||||||
auto channel_count = this->_handle->channel_count;
|
|
||||||
this->reserve_internal_buffer(0, samples * channel_count * sizeof(float));
|
|
||||||
this->reserve_internal_buffer(1, samples * channel_count * sizeof(float));
|
|
||||||
|
|
||||||
for(size_t channel{0}; channel < channel_count; channel++) {
|
|
||||||
auto target_buffer = (float*) this->internal_frame_buffer[1];
|
|
||||||
auto source_buffer = (const float*) buffer + channel;
|
|
||||||
|
|
||||||
for(size_t index{0}; index < samples; index++) {
|
|
||||||
*target_buffer = *source_buffer * kRnNoiseScale;
|
|
||||||
source_buffer += channel_count;
|
|
||||||
target_buffer++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* rnnoise uses a frame size of 480 */
|
|
||||||
this->initialize_rnnoise(channel);
|
|
||||||
rnnoise_process_frame((DenoiseState*) this->rnnoise_processor[channel], (float*) this->internal_frame_buffer[0] + channel * samples, (const float*) this->internal_frame_buffer[1]);
|
|
||||||
rnnoise_process_frame((DenoiseState*) this->rnnoise_processor[channel], (float*) this->internal_frame_buffer[0] + channel * samples + 480, (const float*) this->internal_frame_buffer[1] + 480);
|
|
||||||
}
|
|
||||||
|
|
||||||
const float* channel_buffer_ptr[kMaxChannelCount];
|
|
||||||
for(size_t channel{0}; channel < channel_count; channel++) {
|
|
||||||
channel_buffer_ptr[channel] = (const float*) this->internal_frame_buffer[0] + channel * samples;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* now back again to interlanced */
|
|
||||||
auto target_buffer = (float*) this->internal_frame_buffer[1];
|
|
||||||
for(size_t index{0}; index < samples; index++) {
|
|
||||||
for(size_t channel{0}; channel < channel_count; channel++) {
|
|
||||||
*target_buffer = *(channel_buffer_ptr[channel]++) / kRnNoiseScale;
|
|
||||||
target_buffer++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer = this->internal_frame_buffer[1];
|
|
||||||
} else {
|
|
||||||
/* rnnoise uses a frame size of 480 */
|
|
||||||
this->reserve_internal_buffer(0, samples * sizeof(float));
|
|
||||||
|
|
||||||
auto target_buffer = (float*) this->internal_frame_buffer[0];
|
|
||||||
for(size_t index{0}; index < samples; index++) {
|
|
||||||
target_buffer[index] = ((float*) buffer)[index] * kRnNoiseScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->initialize_rnnoise(0);
|
|
||||||
rnnoise_process_frame((DenoiseState*) this->rnnoise_processor[0], target_buffer, target_buffer);
|
|
||||||
rnnoise_process_frame((DenoiseState*) this->rnnoise_processor[0], &target_buffer[480], &target_buffer[480]);
|
|
||||||
|
|
||||||
buffer = target_buffer;
|
|
||||||
for(size_t index{0}; index < samples; index++) {
|
|
||||||
target_buffer[index] /= kRnNoiseScale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool should_process{true};
|
bool should_process{true};
|
||||||
if(this->filter_mode_ == FilterMode::FILTER) {
|
if(this->filter_mode_ == FilterMode::FILTER) {
|
||||||
auto filters = this->filters();
|
auto filters = this->filters();
|
||||||
@ -350,12 +276,6 @@ void AudioConsumerWrapper::reserve_internal_buffer(int index, size_t target) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioConsumerWrapper::initialize_rnnoise(int channel) {
|
|
||||||
if(!this->rnnoise_processor[channel]) {
|
|
||||||
this->rnnoise_processor[channel] = rnnoise_create(nullptr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NAN_METHOD(AudioConsumerWrapper::_get_filters) {
|
NAN_METHOD(AudioConsumerWrapper::_get_filters) {
|
||||||
auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder());
|
auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder());
|
||||||
auto filters = handle->filters();
|
auto filters = handle->filters();
|
||||||
@ -459,20 +379,4 @@ NAN_METHOD(AudioConsumerWrapper::_set_filter_mode) {
|
|||||||
|
|
||||||
auto value = info[0].As<v8::Number>()->Int32Value(info.GetIsolate()->GetCurrentContext()).FromMaybe(0);
|
auto value = info[0].As<v8::Number>()->Int32Value(info.GetIsolate()->GetCurrentContext()).FromMaybe(0);
|
||||||
handle->filter_mode_ = (FilterMode) value;
|
handle->filter_mode_ = (FilterMode) value;
|
||||||
}
|
|
||||||
|
|
||||||
NAN_METHOD(AudioConsumerWrapper::rnnoise_enabled) {
|
|
||||||
auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder());
|
|
||||||
info.GetReturnValue().Set(handle->rnnoise);
|
|
||||||
}
|
|
||||||
|
|
||||||
NAN_METHOD(AudioConsumerWrapper::toggle_rnnoise) {
|
|
||||||
auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder());
|
|
||||||
|
|
||||||
if(info.Length() != 1 || !info[0]->IsBoolean()) {
|
|
||||||
Nan::ThrowError("invalid argument");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handle->rnnoise = info[0]->BooleanValue(info.GetIsolate());
|
|
||||||
}
|
}
|
@ -52,9 +52,6 @@ namespace tc::audio {
|
|||||||
static NAN_METHOD(_get_filter_mode);
|
static NAN_METHOD(_get_filter_mode);
|
||||||
static NAN_METHOD(_set_filter_mode);
|
static NAN_METHOD(_set_filter_mode);
|
||||||
|
|
||||||
static NAN_METHOD(toggle_rnnoise);
|
|
||||||
static NAN_METHOD(rnnoise_enabled);
|
|
||||||
|
|
||||||
std::shared_ptr<AudioFilterWrapper> create_filter(const std::string& /* name */, const std::shared_ptr<filter::Filter>& /* filter impl */);
|
std::shared_ptr<AudioFilterWrapper> create_filter(const std::string& /* name */, const std::shared_ptr<filter::Filter>& /* filter impl */);
|
||||||
void delete_filter(const AudioFilterWrapper*);
|
void delete_filter(const AudioFilterWrapper*);
|
||||||
|
|
||||||
@ -63,7 +60,7 @@ namespace tc::audio {
|
|||||||
return this->filter_;
|
return this->filter_;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline FilterMode filter_mode() const { return this->filter_mode_; }
|
[[nodiscard]] inline FilterMode filter_mode() const { return this->filter_mode_; }
|
||||||
inline std::shared_ptr<AudioConsumer> native_consumer() { return this->_handle; }
|
inline std::shared_ptr<AudioConsumer> native_consumer() { return this->_handle; }
|
||||||
|
|
||||||
std::mutex native_read_callback_lock;
|
std::mutex native_read_callback_lock;
|
||||||
@ -71,10 +68,6 @@ namespace tc::audio {
|
|||||||
private:
|
private:
|
||||||
AudioRecorderWrapper* _recorder;
|
AudioRecorderWrapper* _recorder;
|
||||||
|
|
||||||
/* preprocessors */
|
|
||||||
bool rnnoise{false};
|
|
||||||
std::array<void*, kMaxChannelCount> rnnoise_processor{nullptr};
|
|
||||||
|
|
||||||
std::mutex execute_mutex;
|
std::mutex execute_mutex;
|
||||||
std::shared_ptr<AudioConsumer> _handle;
|
std::shared_ptr<AudioConsumer> _handle;
|
||||||
|
|
||||||
@ -93,7 +86,6 @@ namespace tc::audio {
|
|||||||
void process_data(const void* /* buffer */, size_t /* samples */);
|
void process_data(const void* /* buffer */, size_t /* samples */);
|
||||||
|
|
||||||
void reserve_internal_buffer(int /* buffer */, size_t /* bytes */);
|
void reserve_internal_buffer(int /* buffer */, size_t /* bytes */);
|
||||||
void initialize_rnnoise(int /* channel */);
|
|
||||||
|
|
||||||
struct DataEntry {
|
struct DataEntry {
|
||||||
void* buffer = nullptr;
|
void* buffer = nullptr;
|
||||||
|
@ -32,8 +32,9 @@ NAN_MODULE_INIT(AudioOutputStreamWrapper::Init) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NAN_METHOD(AudioOutputStreamWrapper::NewInstance) {
|
NAN_METHOD(AudioOutputStreamWrapper::NewInstance) {
|
||||||
if(!info.IsConstructCall())
|
if(!info.IsConstructCall()) {
|
||||||
Nan::ThrowError("invalid invoke!");
|
Nan::ThrowError("invalid invoke!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,50 +3,48 @@
|
|||||||
#include <nan.h>
|
#include <nan.h>
|
||||||
#include <include/NanEventCallback.h>
|
#include <include/NanEventCallback.h>
|
||||||
|
|
||||||
namespace tc {
|
namespace tc::audio {
|
||||||
namespace audio {
|
class AudioResampler;
|
||||||
class AudioResampler;
|
class AudioOutputSource;
|
||||||
class AudioOutputSource;
|
|
||||||
|
|
||||||
class AudioOutputStreamWrapper : public Nan::ObjectWrap {
|
class AudioOutputStreamWrapper : public Nan::ObjectWrap {
|
||||||
public:
|
public:
|
||||||
static NAN_MODULE_INIT(Init);
|
static NAN_MODULE_INIT(Init);
|
||||||
static NAN_METHOD(NewInstance);
|
static NAN_METHOD(NewInstance);
|
||||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||||
static Nan::Persistent<v8::Function> my_constructor;
|
static Nan::Persistent<v8::Function> my_constructor;
|
||||||
return my_constructor;
|
return my_constructor;
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioOutputStreamWrapper(const std::shared_ptr<AudioOutputSource>& /* stream */, bool /* own */);
|
AudioOutputStreamWrapper(const std::shared_ptr<AudioOutputSource>& /* stream */, bool /* own */);
|
||||||
virtual ~AudioOutputStreamWrapper();
|
~AudioOutputStreamWrapper() override;
|
||||||
|
|
||||||
void do_wrap(const v8::Local<v8::Object>&);
|
void do_wrap(const v8::Local<v8::Object>&);
|
||||||
void drop_stream();
|
void drop_stream();
|
||||||
private:
|
private:
|
||||||
static ssize_t write_data(const std::shared_ptr<AudioOutputSource>&, void* source, size_t samples, bool interleaved);
|
static ssize_t write_data(const std::shared_ptr<AudioOutputSource>&, void* source, size_t samples, bool interleaved);
|
||||||
|
|
||||||
/* general methods */
|
/* general methods */
|
||||||
static NAN_METHOD(_get_buffer_latency);
|
static NAN_METHOD(_get_buffer_latency);
|
||||||
static NAN_METHOD(_set_buffer_latency);
|
static NAN_METHOD(_set_buffer_latency);
|
||||||
static NAN_METHOD(_get_buffer_max_latency);
|
static NAN_METHOD(_get_buffer_max_latency);
|
||||||
static NAN_METHOD(_set_buffer_max_latency);
|
static NAN_METHOD(_set_buffer_max_latency);
|
||||||
|
|
||||||
static NAN_METHOD(_flush_buffer);
|
static NAN_METHOD(_flush_buffer);
|
||||||
|
|
||||||
/* methods for owned streams only */
|
/* methods for owned streams only */
|
||||||
static NAN_METHOD(_write_data);
|
static NAN_METHOD(_write_data);
|
||||||
static NAN_METHOD(_write_data_rated);
|
static NAN_METHOD(_write_data_rated);
|
||||||
|
|
||||||
static NAN_METHOD(_clear);
|
static NAN_METHOD(_clear);
|
||||||
static NAN_METHOD(_deleted);
|
static NAN_METHOD(_deleted);
|
||||||
static NAN_METHOD(_delete);
|
static NAN_METHOD(_delete);
|
||||||
|
|
||||||
std::unique_ptr<AudioResampler> _resampler;
|
std::unique_ptr<AudioResampler> _resampler;
|
||||||
std::shared_ptr<AudioOutputSource> _own_handle;
|
std::shared_ptr<AudioOutputSource> _own_handle;
|
||||||
std::weak_ptr<AudioOutputSource> _handle;
|
std::weak_ptr<AudioOutputSource> _handle;
|
||||||
|
|
||||||
Nan::callback_t<> call_underflow;
|
Nan::callback_t<> call_underflow;
|
||||||
Nan::callback_t<> call_overflow;
|
Nan::callback_t<> call_overflow;
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
396
native/serverconnection/src/audio/js/AudioProcessor.cpp
Normal file
396
native/serverconnection/src/audio/js/AudioProcessor.cpp
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
//
|
||||||
|
// Created by WolverinDEV on 28/03/2021.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "./AudioProcessor.h"
|
||||||
|
#include "../../logger.h"
|
||||||
|
#include <NanStrings.h>
|
||||||
|
|
||||||
|
using namespace tc::audio;
|
||||||
|
|
||||||
|
NAN_MODULE_INIT(AudioProcessorWrapper::Init) {
|
||||||
|
auto klass = Nan::New<v8::FunctionTemplate>(AudioProcessorWrapper::NewInstance);
|
||||||
|
klass->SetClassName(Nan::New("AudioProcessor").ToLocalChecked());
|
||||||
|
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||||
|
|
||||||
|
Nan::SetPrototypeMethod(klass, "get_config", AudioProcessorWrapper::get_config);
|
||||||
|
Nan::SetPrototypeMethod(klass, "apply_config", AudioProcessorWrapper::apply_config);
|
||||||
|
|
||||||
|
Nan::SetPrototypeMethod(klass, "get_statistics", AudioProcessorWrapper::get_statistics);
|
||||||
|
|
||||||
|
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||||
|
}
|
||||||
|
|
||||||
|
NAN_METHOD(AudioProcessorWrapper::NewInstance) {
|
||||||
|
if(!info.IsConstructCall()) {
|
||||||
|
Nan::ThrowError("invalid invoke!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioProcessorWrapper::AudioProcessorWrapper(const std::shared_ptr<AudioProcessor> &processor) {
|
||||||
|
log_allocate("AudioProcessorWrapper", this);
|
||||||
|
|
||||||
|
this->registered_observer = new Observer{this};
|
||||||
|
this->weak_processor = processor;
|
||||||
|
processor->register_process_observer(this->registered_observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioProcessorWrapper::~AudioProcessorWrapper() noexcept {
|
||||||
|
log_allocate("AudioProcessorWrapper", this);
|
||||||
|
|
||||||
|
if(auto processor{this->weak_processor.lock()}; processor) {
|
||||||
|
processor->unregister_process_observer(this->registered_observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this->registered_observer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define PUT_VALUE(key, value) \
|
||||||
|
result->Set(context, Nan::LocalStringUTF8(#key), value).Check()
|
||||||
|
|
||||||
|
#define PUT_CONFIG(path) \
|
||||||
|
PUT_VALUE(path, Nan::New(config.path))
|
||||||
|
|
||||||
|
#define LOAD_CONFIG(path, ...) \
|
||||||
|
do { \
|
||||||
|
if(!load_config_value(context, js_config, #path, config.path, __VA_ARGS__)) { \
|
||||||
|
return; \
|
||||||
|
} \
|
||||||
|
} while(0)
|
||||||
|
|
||||||
|
NAN_METHOD(AudioProcessorWrapper::get_config) {
|
||||||
|
auto handle = Nan::ObjectWrap::Unwrap<AudioProcessorWrapper>(info.Holder());
|
||||||
|
auto processor = handle->weak_processor.lock();
|
||||||
|
if(!processor) {
|
||||||
|
Nan::ThrowError("processor passed away");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto config = processor->get_config();
|
||||||
|
auto result = Nan::New<v8::Object>();
|
||||||
|
auto context = info.GetIsolate()->GetCurrentContext();
|
||||||
|
|
||||||
|
PUT_CONFIG(pipeline.maximum_internal_processing_rate);
|
||||||
|
PUT_CONFIG(pipeline.multi_channel_render);
|
||||||
|
PUT_CONFIG(pipeline.multi_channel_capture);
|
||||||
|
|
||||||
|
PUT_CONFIG(pre_amplifier.enabled);
|
||||||
|
PUT_CONFIG(pre_amplifier.fixed_gain_factor);
|
||||||
|
|
||||||
|
PUT_CONFIG(high_pass_filter.enabled);
|
||||||
|
PUT_CONFIG(high_pass_filter.apply_in_full_band);
|
||||||
|
|
||||||
|
PUT_CONFIG(echo_canceller.enabled);
|
||||||
|
PUT_CONFIG(echo_canceller.mobile_mode);
|
||||||
|
PUT_CONFIG(echo_canceller.export_linear_aec_output); /* TODO: Consider removing? */
|
||||||
|
PUT_CONFIG(echo_canceller.enforce_high_pass_filtering);
|
||||||
|
|
||||||
|
PUT_CONFIG(noise_suppression.enabled);
|
||||||
|
switch (config.noise_suppression.level) {
|
||||||
|
using Level = webrtc::AudioProcessing::Config::NoiseSuppression::Level;
|
||||||
|
case Level::kLow:
|
||||||
|
PUT_VALUE(noise_suppression.level, Nan::LocalStringUTF8("low"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Level::kModerate:
|
||||||
|
PUT_VALUE(noise_suppression.level, Nan::LocalStringUTF8("moderate"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Level::kHigh:
|
||||||
|
PUT_VALUE(noise_suppression.level, Nan::LocalStringUTF8("high"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Level::kVeryHigh:
|
||||||
|
PUT_VALUE(noise_suppression.level, Nan::LocalStringUTF8("very-high"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
PUT_VALUE(noise_suppression.level, Nan::LocalStringUTF8("unknown"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
PUT_CONFIG(noise_suppression.analyze_linear_aec_output_when_available);
|
||||||
|
|
||||||
|
PUT_CONFIG(transient_suppression.enabled);
|
||||||
|
|
||||||
|
PUT_CONFIG(voice_detection.enabled);
|
||||||
|
|
||||||
|
PUT_CONFIG(gain_controller1.enabled);
|
||||||
|
switch (config.gain_controller1.mode) {
|
||||||
|
using Mode = webrtc::AudioProcessing::Config::GainController1::Mode;
|
||||||
|
case Mode::kAdaptiveAnalog:
|
||||||
|
PUT_VALUE(gain_controller1.mode, Nan::LocalStringUTF8("adaptive-analog"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mode::kAdaptiveDigital:
|
||||||
|
PUT_VALUE(gain_controller1.mode, Nan::LocalStringUTF8("adaptive-digital"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mode::kFixedDigital:
|
||||||
|
PUT_VALUE(gain_controller1.mode, Nan::LocalStringUTF8("fixed-digital"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
PUT_VALUE(gain_controller1.mode, Nan::LocalStringUTF8("unknown"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
PUT_CONFIG(gain_controller1.target_level_dbfs);
|
||||||
|
PUT_CONFIG(gain_controller1.compression_gain_db);
|
||||||
|
PUT_CONFIG(gain_controller1.enable_limiter);
|
||||||
|
PUT_CONFIG(gain_controller1.analog_level_minimum);
|
||||||
|
PUT_CONFIG(gain_controller1.analog_level_maximum);
|
||||||
|
|
||||||
|
PUT_CONFIG(gain_controller1.analog_gain_controller.enabled);
|
||||||
|
PUT_CONFIG(gain_controller1.analog_gain_controller.startup_min_volume);
|
||||||
|
PUT_CONFIG(gain_controller1.analog_gain_controller.clipped_level_min);
|
||||||
|
PUT_CONFIG(gain_controller1.analog_gain_controller.enable_agc2_level_estimator);
|
||||||
|
PUT_CONFIG(gain_controller1.analog_gain_controller.enable_digital_adaptive);
|
||||||
|
|
||||||
|
PUT_CONFIG(gain_controller2.enabled);
|
||||||
|
|
||||||
|
PUT_CONFIG(gain_controller2.fixed_digital.gain_db);
|
||||||
|
|
||||||
|
PUT_CONFIG(gain_controller2.adaptive_digital.enabled);
|
||||||
|
switch(config.gain_controller2.adaptive_digital.level_estimator) {
|
||||||
|
using LevelEstimator = webrtc::AudioProcessing::Config::GainController2::LevelEstimator;
|
||||||
|
|
||||||
|
case LevelEstimator::kPeak:
|
||||||
|
PUT_VALUE(gain_controller2.adaptive_digital.level_estimator, Nan::LocalStringUTF8("peak"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LevelEstimator::kRms:
|
||||||
|
PUT_VALUE(gain_controller2.adaptive_digital.level_estimator, Nan::LocalStringUTF8("rms"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
PUT_VALUE(gain_controller2.adaptive_digital.level_estimator, Nan::LocalStringUTF8("unknown"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
PUT_CONFIG(gain_controller2.adaptive_digital.vad_probability_attack);
|
||||||
|
PUT_CONFIG(gain_controller2.adaptive_digital.level_estimator_adjacent_speech_frames_threshold);
|
||||||
|
PUT_CONFIG(gain_controller2.adaptive_digital.use_saturation_protector);
|
||||||
|
PUT_CONFIG(gain_controller2.adaptive_digital.initial_saturation_margin_db);
|
||||||
|
PUT_CONFIG(gain_controller2.adaptive_digital.extra_saturation_margin_db);
|
||||||
|
PUT_CONFIG(gain_controller2.adaptive_digital.gain_applier_adjacent_speech_frames_threshold);
|
||||||
|
PUT_CONFIG(gain_controller2.adaptive_digital.max_gain_change_db_per_second);
|
||||||
|
PUT_CONFIG(gain_controller2.adaptive_digital.max_output_noise_level_dbfs);
|
||||||
|
|
||||||
|
PUT_CONFIG(residual_echo_detector.enabled);
|
||||||
|
PUT_CONFIG(level_estimation.enabled);
|
||||||
|
PUT_CONFIG(rnnoise.enabled);
|
||||||
|
|
||||||
|
info.GetReturnValue().Set(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T, typename = typename std::enable_if<std::is_arithmetic<T>::value, T>::type>
|
||||||
|
inline bool load_config_value(
|
||||||
|
const v8::Local<v8::Context>& context,
|
||||||
|
const v8::Local<v8::Object>& js_config,
|
||||||
|
const std::string_view& key,
|
||||||
|
T& value_ref,
|
||||||
|
T min_value = std::numeric_limits<T>::min(),
|
||||||
|
T max_value = std::numeric_limits<T>::max()
|
||||||
|
) {
|
||||||
|
auto maybe_value = js_config->Get(context, Nan::LocalStringUTF8(key));
|
||||||
|
if(maybe_value.IsEmpty() || maybe_value.ToLocalChecked()->IsNullOrUndefined()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
double value;
|
||||||
|
|
||||||
|
if(maybe_value.ToLocalChecked()->IsNumber()) {
|
||||||
|
value = maybe_value.ToLocalChecked()->NumberValue(context).ToChecked();
|
||||||
|
} else if(maybe_value.ToLocalChecked()->IsBoolean()) {
|
||||||
|
value = maybe_value.ToLocalChecked()->BooleanValue(v8::Isolate::GetCurrent());
|
||||||
|
} else {
|
||||||
|
Nan::ThrowError(Nan::LocalStringUTF8("property " + std::string{key} + " isn't a number or boolean"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(std::numeric_limits<T>::is_integer && (double) (T) value != value) {
|
||||||
|
Nan::ThrowError(Nan::LocalStringUTF8("property " + std::string{key} + " isn't an integer"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(value < (double) min_value) {
|
||||||
|
Nan::ThrowError(Nan::LocalStringUTF8("property " + std::string{key} + " exceeds min value of " + std::to_string(min_value)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(value > (double) max_value) {
|
||||||
|
Nan::ThrowError(Nan::LocalStringUTF8("property " + std::string{key} + " exceeds max value of " + std::to_string(max_value)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value_ref = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t kValueSize, typename T>
|
||||||
|
inline bool load_config_enum(
|
||||||
|
const v8::Local<v8::Context>& context,
|
||||||
|
const v8::Local<v8::Object>& js_config,
|
||||||
|
const std::string_view& key,
|
||||||
|
T& value_ref,
|
||||||
|
const std::array<std::pair<std::string_view, T>, kValueSize>& values
|
||||||
|
) {
|
||||||
|
auto maybe_value = js_config->Get(context, Nan::LocalStringUTF8(key));
|
||||||
|
if(maybe_value.IsEmpty() || maybe_value.ToLocalChecked()->IsNullOrUndefined()) {
|
||||||
|
return true;
|
||||||
|
} else if(!maybe_value.ToLocalChecked()->IsString()) {
|
||||||
|
Nan::ThrowError(Nan::LocalStringUTF8("property " + std::string{key} + " isn't a string"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto str_value = maybe_value.ToLocalChecked()->ToString(context).ToLocalChecked();
|
||||||
|
auto value = *Nan::Utf8String(str_value);
|
||||||
|
for(const auto& [ key, key_value ] : values) {
|
||||||
|
if(key != value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
value_ref = key_value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Nan::ThrowError(Nan::LocalStringUTF8("property " + std::string{key} + " contains an invalid enum value (" + value + ")"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define LOAD_ENUM(path, arg_count, ...) \
|
||||||
|
do { \
|
||||||
|
if(!load_config_enum<arg_count>(context, js_config, #path, config.path, {{ __VA_ARGS__ }})) { \
|
||||||
|
return; \
|
||||||
|
} \
|
||||||
|
} while(0)
|
||||||
|
|
||||||
|
NAN_METHOD(AudioProcessorWrapper::apply_config) {
|
||||||
|
auto handle = Nan::ObjectWrap::Unwrap<AudioProcessorWrapper>(info.Holder());
|
||||||
|
auto processor = handle->weak_processor.lock();
|
||||||
|
if (!processor) {
|
||||||
|
Nan::ThrowError("processor passed away");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(info.Length() != 1 || !info[0]->IsObject()) {
|
||||||
|
Nan::ThrowError("Invalid arguments");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto config = processor->get_config();
|
||||||
|
auto context = info.GetIsolate()->GetCurrentContext();
|
||||||
|
auto js_config = info[0]->ToObject(info.GetIsolate()->GetCurrentContext()).ToLocalChecked();
|
||||||
|
|
||||||
|
using GainControllerMode = webrtc::AudioProcessing::Config::GainController1::Mode;
|
||||||
|
using GainControllerLevelEstimator = webrtc::AudioProcessing::Config::GainController2::LevelEstimator;
|
||||||
|
using NoiseSuppressionLevel = webrtc::AudioProcessing::Config::NoiseSuppression::Level;
|
||||||
|
|
||||||
|
LOAD_CONFIG(pipeline.maximum_internal_processing_rate);
|
||||||
|
LOAD_CONFIG(pipeline.multi_channel_render);
|
||||||
|
LOAD_CONFIG(pipeline.multi_channel_capture);
|
||||||
|
|
||||||
|
LOAD_CONFIG(pre_amplifier.enabled);
|
||||||
|
LOAD_CONFIG(pre_amplifier.fixed_gain_factor);
|
||||||
|
|
||||||
|
LOAD_CONFIG(high_pass_filter.enabled);
|
||||||
|
LOAD_CONFIG(high_pass_filter.apply_in_full_band);
|
||||||
|
|
||||||
|
LOAD_CONFIG(echo_canceller.enabled);
|
||||||
|
LOAD_CONFIG(echo_canceller.mobile_mode);
|
||||||
|
LOAD_CONFIG(echo_canceller.export_linear_aec_output); /* TODO: Consider removing? */
|
||||||
|
LOAD_CONFIG(echo_canceller.enforce_high_pass_filtering);
|
||||||
|
|
||||||
|
LOAD_CONFIG(noise_suppression.enabled);
|
||||||
|
LOAD_ENUM(noise_suppression.level, 4,
|
||||||
|
{ "low", NoiseSuppressionLevel::kLow },
|
||||||
|
{ "moderate", NoiseSuppressionLevel::kModerate },
|
||||||
|
{ "high", NoiseSuppressionLevel::kHigh },
|
||||||
|
{ "very-high", NoiseSuppressionLevel::kVeryHigh }
|
||||||
|
);
|
||||||
|
LOAD_CONFIG(noise_suppression.analyze_linear_aec_output_when_available);
|
||||||
|
|
||||||
|
LOAD_CONFIG(transient_suppression.enabled);
|
||||||
|
|
||||||
|
LOAD_CONFIG(voice_detection.enabled);
|
||||||
|
|
||||||
|
LOAD_CONFIG(gain_controller1.enabled);
|
||||||
|
LOAD_ENUM(gain_controller1.mode, 3,
|
||||||
|
{ "adaptive-analog", GainControllerMode::kAdaptiveAnalog },
|
||||||
|
{ "adaptive-digital", GainControllerMode::kAdaptiveDigital },
|
||||||
|
{ "fixed-digital", GainControllerMode::kFixedDigital }
|
||||||
|
);
|
||||||
|
LOAD_CONFIG(gain_controller1.target_level_dbfs);
|
||||||
|
LOAD_CONFIG(gain_controller1.compression_gain_db);
|
||||||
|
LOAD_CONFIG(gain_controller1.enable_limiter);
|
||||||
|
LOAD_CONFIG(gain_controller1.analog_level_minimum);
|
||||||
|
LOAD_CONFIG(gain_controller1.analog_level_maximum);
|
||||||
|
|
||||||
|
LOAD_CONFIG(gain_controller1.analog_gain_controller.enabled);
|
||||||
|
LOAD_CONFIG(gain_controller1.analog_gain_controller.startup_min_volume);
|
||||||
|
LOAD_CONFIG(gain_controller1.analog_gain_controller.clipped_level_min);
|
||||||
|
LOAD_CONFIG(gain_controller1.analog_gain_controller.enable_agc2_level_estimator);
|
||||||
|
LOAD_CONFIG(gain_controller1.analog_gain_controller.enable_digital_adaptive);
|
||||||
|
|
||||||
|
LOAD_CONFIG(gain_controller2.enabled);
|
||||||
|
|
||||||
|
LOAD_CONFIG(gain_controller2.fixed_digital.gain_db);
|
||||||
|
|
||||||
|
LOAD_CONFIG(gain_controller2.adaptive_digital.enabled);
|
||||||
|
LOAD_ENUM(gain_controller2.adaptive_digital.level_estimator, 2,
|
||||||
|
{ "peak", GainControllerLevelEstimator::kPeak },
|
||||||
|
{ "rms", GainControllerLevelEstimator::kRms }
|
||||||
|
);
|
||||||
|
LOAD_CONFIG(gain_controller2.adaptive_digital.vad_probability_attack);
|
||||||
|
LOAD_CONFIG(gain_controller2.adaptive_digital.level_estimator_adjacent_speech_frames_threshold);
|
||||||
|
LOAD_CONFIG(gain_controller2.adaptive_digital.use_saturation_protector);
|
||||||
|
LOAD_CONFIG(gain_controller2.adaptive_digital.initial_saturation_margin_db);
|
||||||
|
LOAD_CONFIG(gain_controller2.adaptive_digital.extra_saturation_margin_db);
|
||||||
|
LOAD_CONFIG(gain_controller2.adaptive_digital.gain_applier_adjacent_speech_frames_threshold);
|
||||||
|
LOAD_CONFIG(gain_controller2.adaptive_digital.max_gain_change_db_per_second);
|
||||||
|
LOAD_CONFIG(gain_controller2.adaptive_digital.max_output_noise_level_dbfs);
|
||||||
|
|
||||||
|
LOAD_CONFIG(residual_echo_detector.enabled);
|
||||||
|
LOAD_CONFIG(level_estimation.enabled);
|
||||||
|
LOAD_CONFIG(rnnoise.enabled);
|
||||||
|
|
||||||
|
processor->apply_config(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define PUT_STATISTIC(path) \
|
||||||
|
do { \
|
||||||
|
if(config.path.has_value()) { \
|
||||||
|
PUT_VALUE(path, Nan::New(*config.path)); \
|
||||||
|
} else { \
|
||||||
|
PUT_VALUE(path, Nan::Undefined()); \
|
||||||
|
} \
|
||||||
|
} while(0)
|
||||||
|
|
||||||
|
NAN_METHOD(AudioProcessorWrapper::get_statistics) {
|
||||||
|
auto handle = Nan::ObjectWrap::Unwrap<AudioProcessorWrapper>(info.Holder());
|
||||||
|
auto processor = handle->weak_processor.lock();
|
||||||
|
if(!processor) {
|
||||||
|
Nan::ThrowError("processor passed away");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto config = processor->get_statistics();
|
||||||
|
auto result = Nan::New<v8::Object>();
|
||||||
|
auto context = info.GetIsolate()->GetCurrentContext();
|
||||||
|
|
||||||
|
PUT_STATISTIC(output_rms_dbfs);
|
||||||
|
PUT_STATISTIC(voice_detected);
|
||||||
|
PUT_STATISTIC(echo_return_loss);
|
||||||
|
PUT_STATISTIC(echo_return_loss_enhancement);
|
||||||
|
PUT_STATISTIC(divergent_filter_fraction);
|
||||||
|
PUT_STATISTIC(delay_median_ms);
|
||||||
|
PUT_STATISTIC(delay_standard_deviation_ms);
|
||||||
|
PUT_STATISTIC(residual_echo_likelihood);
|
||||||
|
PUT_STATISTIC(residual_echo_likelihood_recent_max);
|
||||||
|
PUT_STATISTIC(delay_ms);
|
||||||
|
PUT_STATISTIC(rnnoise_volume);
|
||||||
|
|
||||||
|
info.GetReturnValue().Set(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioProcessorWrapper::Observer::stream_processed(const AudioProcessor::Stats &stats) {
|
||||||
|
/* TODO! */
|
||||||
|
}
|
41
native/serverconnection/src/audio/js/AudioProcessor.h
Normal file
41
native/serverconnection/src/audio/js/AudioProcessor.h
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <nan.h>
|
||||||
|
#include "../processing/AudioProcessor.h"
|
||||||
|
|
||||||
|
namespace tc::audio {
|
||||||
|
class AudioProcessorWrapper : public Nan::ObjectWrap {
|
||||||
|
public:
|
||||||
|
static NAN_MODULE_INIT(Init);
|
||||||
|
static NAN_METHOD(NewInstance);
|
||||||
|
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||||
|
static Nan::Persistent<v8::Function> my_constructor;
|
||||||
|
return my_constructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit AudioProcessorWrapper(const std::shared_ptr<AudioProcessor>& /* processor */);
|
||||||
|
~AudioProcessorWrapper() override;
|
||||||
|
|
||||||
|
inline void wrap(v8::Local<v8::Object> object) {
|
||||||
|
Nan::ObjectWrap::Wrap(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
static NAN_METHOD(get_config);
|
||||||
|
static NAN_METHOD(apply_config);
|
||||||
|
|
||||||
|
static NAN_METHOD(get_statistics);
|
||||||
|
private:
|
||||||
|
struct Observer : public AudioProcessor::ProcessObserver {
|
||||||
|
public:
|
||||||
|
explicit Observer(AudioProcessorWrapper* wrapper) : wrapper{wrapper} {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
AudioProcessorWrapper* wrapper;
|
||||||
|
|
||||||
|
void stream_processed(const AudioProcessor::Stats &stats) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::weak_ptr<AudioProcessor> weak_processor{};
|
||||||
|
Observer* registered_observer{nullptr};
|
||||||
|
};
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "AudioRecorder.h"
|
#include "AudioRecorder.h"
|
||||||
#include "AudioConsumer.h"
|
#include "AudioConsumer.h"
|
||||||
|
#include "./AudioProcessor.h"
|
||||||
#include "../AudioInput.h"
|
#include "../AudioInput.h"
|
||||||
#include "../../logger.h"
|
#include "../../logger.h"
|
||||||
|
|
||||||
@ -46,6 +47,8 @@ NAN_MODULE_INIT(AudioRecorderWrapper::Init) {
|
|||||||
Nan::SetPrototypeMethod(klass, "create_consumer", AudioRecorderWrapper::_create_consumer);
|
Nan::SetPrototypeMethod(klass, "create_consumer", AudioRecorderWrapper::_create_consumer);
|
||||||
Nan::SetPrototypeMethod(klass, "delete_consumer", AudioRecorderWrapper::_delete_consumer);
|
Nan::SetPrototypeMethod(klass, "delete_consumer", AudioRecorderWrapper::_delete_consumer);
|
||||||
|
|
||||||
|
Nan::SetPrototypeMethod(klass, "get_audio_processor", AudioRecorderWrapper::get_audio_processor);
|
||||||
|
|
||||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +96,7 @@ std::shared_ptr<AudioConsumerWrapper> AudioRecorderWrapper::create_consumer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AudioRecorderWrapper::delete_consumer(const AudioConsumerWrapper* consumer) {
|
void AudioRecorderWrapper::delete_consumer(const AudioConsumerWrapper* consumer) {
|
||||||
shared_ptr<AudioConsumerWrapper> handle; /* need to keep the handle 'till everything has been finished */
|
std::shared_ptr<AudioConsumerWrapper> handle; /* need to keep the handle 'till everything has been finished */
|
||||||
{
|
{
|
||||||
lock_guard lock(this->consumer_mutex);
|
lock_guard lock(this->consumer_mutex);
|
||||||
for(auto& c : this->consumer_) {
|
for(auto& c : this->consumer_) {
|
||||||
@ -102,20 +105,22 @@ void AudioRecorderWrapper::delete_consumer(const AudioConsumerWrapper* consumer)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!handle)
|
|
||||||
return;
|
if(!handle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto it = find(this->consumer_.begin(), this->consumer_.end(), handle);
|
auto it = find(this->consumer_.begin(), this->consumer_.end(), handle);
|
||||||
if(it != this->consumer_.end())
|
if(it != this->consumer_.end()) {
|
||||||
this->consumer_.erase(it);
|
this->consumer_.erase(it);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
lock_guard lock(handle->execute_mutex); /* if we delete the consumer while executing strange stuff could happen */
|
lock_guard lock(handle->execute_mutex); /* if we delete the consumer while executing strange stuff could happen */
|
||||||
handle->unbind();
|
handle->unbind();
|
||||||
this->input_->delete_consumer(handle->_handle);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,4 +276,18 @@ NAN_METHOD(AudioRecorderWrapper::_set_volume) {
|
|||||||
NAN_METHOD(AudioRecorderWrapper::_get_volume) {
|
NAN_METHOD(AudioRecorderWrapper::_get_volume) {
|
||||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||||
info.GetReturnValue().Set(handle->input_->volume());
|
info.GetReturnValue().Set(handle->input_->volume());
|
||||||
|
}
|
||||||
|
|
||||||
|
NAN_METHOD(AudioRecorderWrapper::get_audio_processor) {
|
||||||
|
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||||
|
|
||||||
|
auto processor = handle->input_->audio_processor();
|
||||||
|
if(!processor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto js_object = Nan::NewInstance(Nan::New(AudioProcessorWrapper::constructor()), 0, nullptr).ToLocalChecked();
|
||||||
|
auto wrapper = new AudioProcessorWrapper(processor);
|
||||||
|
wrapper->wrap(js_object);
|
||||||
|
info.GetReturnValue().Set(js_object);
|
||||||
}
|
}
|
@ -23,7 +23,7 @@ namespace tc::audio {
|
|||||||
return my_constructor;
|
return my_constructor;
|
||||||
}
|
}
|
||||||
|
|
||||||
explicit AudioRecorderWrapper(std::shared_ptr<AudioInput> /* input */);
|
explicit AudioRecorderWrapper(std::shared_ptr<AudioInput> /* input */);
|
||||||
~AudioRecorderWrapper() override;
|
~AudioRecorderWrapper() override;
|
||||||
|
|
||||||
static NAN_METHOD(_get_device);
|
static NAN_METHOD(_get_device);
|
||||||
@ -40,6 +40,8 @@ namespace tc::audio {
|
|||||||
static NAN_METHOD(_set_volume);
|
static NAN_METHOD(_set_volume);
|
||||||
static NAN_METHOD(_get_volume);
|
static NAN_METHOD(_get_volume);
|
||||||
|
|
||||||
|
static NAN_METHOD(get_audio_processor);
|
||||||
|
|
||||||
std::shared_ptr<AudioConsumerWrapper> create_consumer();
|
std::shared_ptr<AudioConsumerWrapper> create_consumer();
|
||||||
void delete_consumer(const AudioConsumerWrapper*);
|
void delete_consumer(const AudioConsumerWrapper*);
|
||||||
|
|
||||||
|
@ -2,10 +2,234 @@
|
|||||||
// Created by WolverinDEV on 27/03/2021.
|
// Created by WolverinDEV on 27/03/2021.
|
||||||
//
|
//
|
||||||
|
|
||||||
#include "AudioProcessor.h"
|
#include "./AudioProcessor.h"
|
||||||
|
#include "../../logger.h"
|
||||||
|
#include <rnnoise.h>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
using namespace tc::audio;
|
using namespace tc::audio;
|
||||||
|
|
||||||
void AudioProcessor::analyze_reverse_stream(const float *const *data, const webrtc::StreamConfig &reverse_config) {
|
AudioProcessor::AudioProcessor() {
|
||||||
|
this->current_config.echo_canceller.enabled = true;
|
||||||
|
this->current_config.echo_canceller.mobile_mode = false;
|
||||||
|
|
||||||
|
this->current_config.gain_controller1.enabled = true;
|
||||||
|
this->current_config.gain_controller1.mode = webrtc::AudioProcessing::Config::GainController1::kAdaptiveAnalog;
|
||||||
|
this->current_config.gain_controller1.analog_level_minimum = 0;
|
||||||
|
this->current_config.gain_controller1.analog_level_maximum = 255;
|
||||||
|
|
||||||
|
this->current_config.gain_controller2.enabled = true;
|
||||||
|
|
||||||
|
this->current_config.high_pass_filter.enabled = true;
|
||||||
|
|
||||||
|
this->current_config.voice_detection.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioProcessor::~AudioProcessor() {
|
||||||
|
std::lock_guard processor_lock{this->processor_mutex};
|
||||||
|
delete this->processor;
|
||||||
|
|
||||||
|
for(auto& entry : this->rnnoise_processor) {
|
||||||
|
if(!entry) { continue; }
|
||||||
|
|
||||||
|
rnnoise_destroy((DenoiseState*) entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr static inline auto process_error_to_string(int error) {
|
||||||
|
switch (error) {
|
||||||
|
case 0: return "kNoError";
|
||||||
|
case -1: return "kUnspecifiedError";
|
||||||
|
case -2: return "kCreationFailedError";
|
||||||
|
case -3: return "kUnsupportedComponentError";
|
||||||
|
case -4: return "kUnsupportedFunctionError";
|
||||||
|
case -5: return "kNullPointerError";
|
||||||
|
case -6: return "kBadParameterError";
|
||||||
|
case -7: return "kBadSampleRateError";
|
||||||
|
case -8: return "kBadDataLengthError";
|
||||||
|
case -9: return "kBadNumberChannelsError";
|
||||||
|
case -10: return "kFileError";
|
||||||
|
case -11: return "kStreamParameterNotSetError";
|
||||||
|
case -12: return "kNotEnabledError";
|
||||||
|
case -13: return "kBadStreamParameterWarning";
|
||||||
|
default: return "unkown error code";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
inline std::ostream& operator<<(std::ostream &ss, const absl::optional<T>& optional) {
|
||||||
|
if(optional.has_value()) {
|
||||||
|
ss << "optional{" << *optional << "}";
|
||||||
|
} else {
|
||||||
|
ss << "nullopt";
|
||||||
|
}
|
||||||
|
return ss;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string statistics_to_string(const webrtc::AudioProcessingStats& stats) {
|
||||||
|
std::stringstream ss{};
|
||||||
|
|
||||||
|
ss << "AudioProcessingStats{";
|
||||||
|
ss << "output_rms_dbfs: " << stats.output_rms_dbfs << ", ";
|
||||||
|
ss << "voice_detected: " << stats.voice_detected << ", ";
|
||||||
|
ss << "echo_return_loss: " << stats.echo_return_loss << ", ";
|
||||||
|
ss << "echo_return_loss_enhancement: " << stats.echo_return_loss_enhancement << ", ";
|
||||||
|
ss << "divergent_filter_fraction: " << stats.divergent_filter_fraction << ", ";
|
||||||
|
ss << "delay_median_ms: " << stats.delay_median_ms << ", ";
|
||||||
|
ss << "delay_standard_deviation_ms: " << stats.delay_standard_deviation_ms << ", ";
|
||||||
|
ss << "residual_echo_likelihood: " << stats.residual_echo_likelihood << ", ";
|
||||||
|
ss << "residual_echo_likelihood_recent_max: " << stats.residual_echo_likelihood_recent_max << ", ";
|
||||||
|
ss << "delay_ms: " << stats.delay_ms;
|
||||||
|
ss << "}";
|
||||||
|
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioProcessor::initialize() {
|
||||||
|
std::lock_guard processor_lock{this->processor_mutex};
|
||||||
|
if(this->processor) {
|
||||||
|
/* double initialize */
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace webrtc;
|
||||||
|
|
||||||
|
AudioProcessingBuilder builder{};
|
||||||
|
this->processor = builder.Create();
|
||||||
|
if(!this->processor) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->apply_config_unlocked(this->current_config);
|
||||||
|
this->processor->Initialize();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioProcessor::Config AudioProcessor::get_config() const {
|
||||||
|
std::shared_lock processor_lock{this->processor_mutex};
|
||||||
|
return this->current_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioProcessor::apply_config(const AudioProcessor::Config &config) {
|
||||||
|
std::lock_guard processor_lock{this->processor_mutex};
|
||||||
|
this->apply_config_unlocked(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioProcessor::apply_config_unlocked(const Config &config) {
|
||||||
|
this->current_config = config;
|
||||||
|
if(this->processor) {
|
||||||
|
this->processor->ApplyConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this->current_config.rnnoise.enabled) {
|
||||||
|
this->rnnoise_volume = absl::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioProcessor::Stats AudioProcessor::get_statistics() const {
|
||||||
|
std::shared_lock processor_lock{this->processor_mutex};
|
||||||
|
return this->get_statistics_unlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioProcessor::Stats AudioProcessor::get_statistics_unlocked() const {
|
||||||
|
if(!this->processor) {
|
||||||
|
return AudioProcessor::Stats{};
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioProcessor::Stats result{this->processor->GetStatistics()};
|
||||||
|
result.rnnoise_volume = this->rnnoise_volume;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioProcessor::register_process_observer(ProcessObserver *observer) {
|
||||||
|
std::lock_guard processor_lock{this->processor_mutex};
|
||||||
|
this->process_observer.push_back(observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioProcessor::unregister_process_observer(ProcessObserver *observer) {
|
||||||
|
std::lock_guard processor_lock{this->processor_mutex};
|
||||||
|
auto index = std::find(this->process_observer.begin(), this->process_observer.end(), observer);
|
||||||
|
if(index == this->process_observer.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->process_observer.erase(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioProcessor::analyze_reverse_stream(const float *const *data, const webrtc::StreamConfig &reverse_config) {
|
||||||
|
std::shared_lock processor_lock{this->processor_mutex};
|
||||||
|
if(!this->processor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto result = this->processor->AnalyzeReverseStream(data, reverse_config);
|
||||||
|
if(result != webrtc::AudioProcessing::kNoError) {
|
||||||
|
log_error(category::audio, tr("Failed to process reverse stream: {}"), process_error_to_string(result));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AudioProcessor::Stats> AudioProcessor::process_stream(const webrtc::StreamConfig &config, float *const *buffer) {
|
||||||
|
|
||||||
|
std::shared_lock processor_lock{this->processor_mutex};
|
||||||
|
if(!this->processor) {
|
||||||
|
return std::nullopt;
|
||||||
|
} else if(config.num_channels() > kMaxChannelCount) {
|
||||||
|
log_error(category::audio, tr("AudioProcessor received input buffer with too many channels ({} channels but supported is only {})"), config.num_channels(), kMaxChannelCount);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this->current_config.rnnoise.enabled) {
|
||||||
|
if(config.sample_rate_hz() != 48000) {
|
||||||
|
log_warn(category::audio, tr("Don't apply RNNoise. Source sample rate isn't 480kHz ({}kHz)"), config.sample_rate_hz() / 1000);
|
||||||
|
this->rnnoise_volume.reset();
|
||||||
|
} else {
|
||||||
|
static const float kRnNoiseScale = -INT16_MIN;
|
||||||
|
|
||||||
|
double volume_sum{0};
|
||||||
|
for(size_t channel{0}; channel < config.num_channels(); channel++) {
|
||||||
|
if(!this->rnnoise_processor[channel]) {
|
||||||
|
this->rnnoise_processor[channel] = (void*) rnnoise_create(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
/* RNNoise uses a frame size of 10ms for 48kHz aka 480 samples */
|
||||||
|
auto buffer_ptr = buffer[channel];
|
||||||
|
for(size_t sample{0}; sample < 480; sample++) {
|
||||||
|
*buffer_ptr++ *= kRnNoiseScale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_sum += rnnoise_process_frame((DenoiseState*) this->rnnoise_processor[channel], buffer[channel], buffer[channel]);
|
||||||
|
|
||||||
|
{
|
||||||
|
auto buffer_ptr = buffer[channel];
|
||||||
|
for(size_t sample{0}; sample < 480; sample++) {
|
||||||
|
*buffer_ptr++ /= kRnNoiseScale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->rnnoise_volume = absl::make_optional(volume_sum / config.num_channels());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->processor->set_stream_delay_ms(2); /* TODO: Measure it and not just guess it! */
|
||||||
|
this->processor->set_stream_analog_level(0);
|
||||||
|
|
||||||
|
auto result = this->processor->ProcessStream(buffer, config, config, buffer);
|
||||||
|
if(result != webrtc::AudioProcessing::kNoError) {
|
||||||
|
log_error(category::audio, tr("Failed to process stream: {}"), process_error_to_string(result));
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto statistics = this->get_statistics_unlocked();
|
||||||
|
for(const auto& observer : this->process_observer) {
|
||||||
|
observer->stream_processed(statistics);
|
||||||
|
}
|
||||||
|
|
||||||
|
//log_trace(category::audio, tr("Processing stats: {}"), statistics_to_string(statistics));
|
||||||
|
return std::make_optional(std::move(statistics));
|
||||||
}
|
}
|
@ -1,11 +1,50 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <shared_mutex>
|
||||||
|
#include <optional>
|
||||||
#include <modules/audio_processing/include/audio_processing.h>
|
#include <modules/audio_processing/include/audio_processing.h>
|
||||||
|
|
||||||
namespace tc::audio {
|
namespace tc::audio {
|
||||||
class AudioProcessor : public std::enable_shared_from_this<AudioProcessor> {
|
class AudioProcessor {
|
||||||
public:
|
public:
|
||||||
|
struct Config : public webrtc::AudioProcessing::Config {
|
||||||
|
struct {
|
||||||
|
bool enabled{false};
|
||||||
|
} rnnoise;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Stats : public webrtc::AudioProcessingStats {
|
||||||
|
// The RNNoise returned sample volume
|
||||||
|
absl::optional<float> rnnoise_volume;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ProcessObserver {
|
||||||
|
public:
|
||||||
|
virtual void stream_processed(const AudioProcessor::Stats&) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
AudioProcessor();
|
||||||
|
virtual ~AudioProcessor();
|
||||||
|
|
||||||
|
[[nodiscard]] bool initialize();
|
||||||
|
|
||||||
|
[[nodiscard]] Config get_config() const;
|
||||||
|
void apply_config(const Config &/* config */);
|
||||||
|
|
||||||
|
[[nodiscard]] AudioProcessor::Stats get_statistics() const;
|
||||||
|
|
||||||
|
void register_process_observer(ProcessObserver* /* observer */);
|
||||||
|
/**
|
||||||
|
* Unregister a process observer.
|
||||||
|
* Note: Never call this within the observer callback!
|
||||||
|
* This will cause a deadlock.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
bool unregister_process_observer(ProcessObserver* /* observer */);
|
||||||
|
|
||||||
|
/* 10ms audio chunk */
|
||||||
|
[[nodiscard]] std::optional<AudioProcessor::Stats> process_stream( const webrtc::StreamConfig& /* config */, float* const* /* buffer */);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accepts deinterleaved float audio with the range [-1, 1]. Each element
|
* Accepts deinterleaved float audio with the range [-1, 1]. Each element
|
||||||
@ -14,6 +53,20 @@ namespace tc::audio {
|
|||||||
*/
|
*/
|
||||||
void analyze_reverse_stream(const float* const* data,
|
void analyze_reverse_stream(const float* const* data,
|
||||||
const webrtc::StreamConfig& reverse_config);
|
const webrtc::StreamConfig& reverse_config);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
constexpr static auto kMaxChannelCount{2};
|
||||||
|
|
||||||
|
mutable std::shared_mutex processor_mutex{};
|
||||||
|
|
||||||
|
Config current_config{};
|
||||||
|
std::vector<ProcessObserver*> process_observer{};
|
||||||
|
webrtc::AudioProcessing* processor{nullptr};
|
||||||
|
|
||||||
|
absl::optional<float> rnnoise_volume{};
|
||||||
|
std::array<void*, kMaxChannelCount> rnnoise_processor{nullptr};
|
||||||
|
|
||||||
|
[[nodiscard]] AudioProcessor::Stats get_statistics_unlocked() const;
|
||||||
|
void apply_config_unlocked(const Config &/* config */);
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -3,8 +3,6 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <NanGet.h>
|
|
||||||
#include <NanEventCallback.h>
|
|
||||||
#include "./SoundPlayer.h"
|
#include "./SoundPlayer.h"
|
||||||
#include "../AudioOutput.h"
|
#include "../AudioOutput.h"
|
||||||
#include "../file/wav.h"
|
#include "../file/wav.h"
|
||||||
@ -15,6 +13,11 @@
|
|||||||
#include "../AudioMerger.h"
|
#include "../AudioMerger.h"
|
||||||
#include "../AudioGain.h"
|
#include "../AudioGain.h"
|
||||||
|
|
||||||
|
#ifdef NODEJS_API
|
||||||
|
#include <NanGet.h>
|
||||||
|
#include <NanEventCallback.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef max
|
#ifdef max
|
||||||
#undef max
|
#undef max
|
||||||
#endif
|
#endif
|
||||||
@ -275,6 +278,7 @@ namespace tc::audio::sounds {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef NODEJS_API
|
||||||
NAN_METHOD(tc::audio::sounds::playback_sound_js) {
|
NAN_METHOD(tc::audio::sounds::playback_sound_js) {
|
||||||
if(info.Length() != 1 || !info[0]->IsObject()) {
|
if(info.Length() != 1 || !info[0]->IsObject()) {
|
||||||
Nan::ThrowError("invalid arguments");
|
Nan::ThrowError("invalid arguments");
|
||||||
@ -330,4 +334,5 @@ NAN_METHOD(tc::audio::sounds::cancel_playback_js) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancel_playback((sound_playback_id) info[0].As<v8::Number>()->Value());
|
cancel_playback((sound_playback_id) info[0].As<v8::Number>()->Value());
|
||||||
}
|
}
|
||||||
|
#endif
|
@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
|
#ifdef NODEJS_API
|
||||||
#include <nan.h>
|
#include <nan.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace tc::audio::sounds {
|
namespace tc::audio::sounds {
|
||||||
typedef uintptr_t sound_playback_id;
|
typedef uintptr_t sound_playback_id;
|
||||||
@ -26,6 +29,8 @@ namespace tc::audio::sounds {
|
|||||||
extern sound_playback_id playback_sound(const PlaybackSettings& /* settings */);
|
extern sound_playback_id playback_sound(const PlaybackSettings& /* settings */);
|
||||||
extern void cancel_playback(const sound_playback_id&);
|
extern void cancel_playback(const sound_playback_id&);
|
||||||
|
|
||||||
|
#ifdef NODEJS_API
|
||||||
extern NAN_METHOD(playback_sound_js);
|
extern NAN_METHOD(playback_sound_js);
|
||||||
extern NAN_METHOD(cancel_playback_js);
|
extern NAN_METHOD(cancel_playback_js);
|
||||||
|
#endif
|
||||||
}
|
}
|
@ -18,12 +18,10 @@
|
|||||||
#include "audio/js/AudioRecorder.h"
|
#include "audio/js/AudioRecorder.h"
|
||||||
#include "audio/js/AudioConsumer.h"
|
#include "audio/js/AudioConsumer.h"
|
||||||
#include "audio/js/AudioFilter.h"
|
#include "audio/js/AudioFilter.h"
|
||||||
|
#include "audio/js/AudioProcessor.h"
|
||||||
#include "audio/AudioEventLoop.h"
|
#include "audio/AudioEventLoop.h"
|
||||||
#include "audio/sounds/SoundPlayer.h"
|
#include "audio/sounds/SoundPlayer.h"
|
||||||
|
|
||||||
//#include <webrtc-audio-processing-1/modules/audio_processing/include/audio_processing.h>
|
|
||||||
#include <modules/audio_processing/include/audio_processing.h>
|
|
||||||
|
|
||||||
#ifndef WIN32
|
#ifndef WIN32
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#endif
|
#endif
|
||||||
@ -40,14 +38,6 @@ using namespace tc;
|
|||||||
using namespace tc::connection;
|
using namespace tc::connection;
|
||||||
using namespace tc::ft;
|
using namespace tc::ft;
|
||||||
|
|
||||||
void processor() {
|
|
||||||
webrtc::AudioProcessingBuilder builder{};
|
|
||||||
webrtc::AudioProcessing::Config config{};
|
|
||||||
|
|
||||||
auto processor = builder.Create();
|
|
||||||
//processor->AnalyzeReverseStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
void testTomMath(){
|
void testTomMath(){
|
||||||
mp_int x{};
|
mp_int x{};
|
||||||
mp_init(&x);
|
mp_init(&x);
|
||||||
@ -196,6 +186,7 @@ NAN_MODULE_INIT(init) {
|
|||||||
audio::recorder::AudioRecorderWrapper::Init(namespace_record);
|
audio::recorder::AudioRecorderWrapper::Init(namespace_record);
|
||||||
audio::recorder::AudioConsumerWrapper::Init(namespace_record);
|
audio::recorder::AudioConsumerWrapper::Init(namespace_record);
|
||||||
audio::recorder::AudioFilterWrapper::Init(namespace_record);
|
audio::recorder::AudioFilterWrapper::Init(namespace_record);
|
||||||
|
audio::AudioProcessorWrapper::Init(namespace_record);
|
||||||
|
|
||||||
{
|
{
|
||||||
auto enum_object = Nan::New<v8::Object>();
|
auto enum_object = Nan::New<v8::Object>();
|
||||||
|
@ -6,56 +6,55 @@
|
|||||||
|
|
||||||
#include "../../src/audio/AudioOutput.h"
|
#include "../../src/audio/AudioOutput.h"
|
||||||
#include "../../src/audio/AudioInput.h"
|
#include "../../src/audio/AudioInput.h"
|
||||||
|
#include "../../src/audio/AudioEventLoop.h"
|
||||||
#include "../../src/audio/filter/FilterVad.h"
|
#include "../../src/audio/filter/FilterVad.h"
|
||||||
#include "../../src/audio/filter/FilterThreshold.h"
|
#include "../../src/audio/filter/FilterThreshold.h"
|
||||||
#include "../../src/logger.h"
|
#include "../../src/logger.h"
|
||||||
|
|
||||||
#ifdef WIN32
|
|
||||||
#include <windows.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace tc;
|
using namespace tc;
|
||||||
|
|
||||||
|
tc::audio::AudioOutput* global_audio_output{nullptr};
|
||||||
int main() {
|
int main() {
|
||||||
std::string error{};
|
std::string error{};
|
||||||
|
|
||||||
Pa_Initialize();
|
|
||||||
|
|
||||||
logger::initialize_raw();
|
logger::initialize_raw();
|
||||||
|
tc::audio::init_event_loops();
|
||||||
tc::audio::initialize();
|
tc::audio::initialize();
|
||||||
tc::audio::await_initialized();
|
tc::audio::await_initialized();
|
||||||
|
|
||||||
std::shared_ptr<tc::audio::AudioDevice> default_playback{nullptr}, default_record{nullptr};
|
std::shared_ptr<tc::audio::AudioDevice> default_playback{nullptr}, default_record{nullptr};
|
||||||
for(auto& device : tc::audio::devices()) {
|
for(auto& device : tc::audio::devices()) {
|
||||||
if(device->is_output_default())
|
if(device->is_output_default()) {
|
||||||
default_playback = device;
|
default_playback = device;
|
||||||
if(device->is_input_default())
|
}
|
||||||
|
|
||||||
|
if(device->is_input_default()) {
|
||||||
default_record = device;
|
default_record = device;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
assert(default_record);
|
assert(default_record);
|
||||||
assert(default_playback);
|
assert(default_playback);
|
||||||
|
|
||||||
for(auto& dev : tc::audio::devices()) {
|
for(auto& dev : tc::audio::devices()) {
|
||||||
if(!dev->is_input_supported()) continue;
|
if(!dev->is_input_supported()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
auto playback_manager = std::make_unique<audio::AudioOutput>(2, 48000);
|
auto playback_manager = std::make_unique<audio::AudioOutput>(2, 48000);
|
||||||
if(!playback_manager->set_device(error, default_playback)) {
|
global_audio_output = &*playback_manager;
|
||||||
cerr << "Failed to open output device (" << error << ")" << endl;
|
|
||||||
return 1;
|
playback_manager->set_device(default_playback);
|
||||||
}
|
if(!playback_manager->playback(error)) {
|
||||||
if(!playback_manager->playback()) {
|
cerr << "failed to start playback: " << error << endl;
|
||||||
cerr << "failed to start playback" << endl;
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto input = std::make_unique<audio::AudioInput>(2, 48000);
|
auto input = std::make_unique<audio::AudioInput>(2, 48000);
|
||||||
if(!input->set_device(error, dev)) {
|
input->set_device(default_record);
|
||||||
cerr << "Failed to open input device (" << error << "): " << dev->id() << " (" << dev->name() << ")" << endl;
|
|
||||||
continue;
|
if(!input->record(error)) {
|
||||||
}
|
cerr << "failed to start record for " << dev->id() << " (" << dev->name() << "): " << error << endl;
|
||||||
if(!input->record()) {
|
|
||||||
cerr << "failed to start record for " << dev->id() << " (" << dev->name() << ")" << endl;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +93,7 @@ int main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
playback_manager.release(); //FIXME: Memory leak!
|
playback_manager.release(); //FIXME: Memory leak!
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this_thread::sleep_for(chrono::seconds(360));
|
this_thread::sleep_for(chrono::seconds(360));
|
||||||
@ -103,6 +103,5 @@ int main() {
|
|||||||
this_thread::sleep_for(chrono::seconds(1000));
|
this_thread::sleep_for(chrono::seconds(1000));
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
Pa_Terminate();
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
@ -1,14 +1,96 @@
|
|||||||
/// <reference path="../../exports/exports.d.ts" />
|
import * as path from "path";
|
||||||
console.log("Starting app");
|
module.paths.push(path.join(__dirname, "..", "..", "..", "build", "win32_x64"));
|
||||||
module.paths.push("../../build/linux_x64");
|
module.paths.push(path.join(__dirname, "..", "..", "..", "build", "linux_x64"));
|
||||||
module.paths.push("../../build/win32_x64");
|
|
||||||
|
|
||||||
const original_require = require;
|
import {audio} from "teaclient_connection.node";
|
||||||
require = (module => original_require(__dirname + "/../../../build/win32_x64/" + module + ".node")) as any;
|
import record = audio.record;
|
||||||
import * as handle from "teaclient_connection";
|
import playback = audio.playback;
|
||||||
require = original_require;
|
|
||||||
|
|
||||||
console.dir(handle.audio);
|
function printDevices() {
|
||||||
|
console.info("Available input devices:");
|
||||||
|
for(const device of audio.available_devices()) {
|
||||||
|
if(!device.input_supported) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info(" - " + device.driver + " - " + device.device_id + " (" + device.name + ")" + (device.input_default ? " (default)" : ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info("Available output devices:");
|
||||||
|
for(const device of audio.available_devices()) {
|
||||||
|
if(!device.output_supported) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info(" - " + device.driver + " - " + device.device_id + " (" + device.name + ")" + (device.output_default ? " (default)" : ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await new Promise(resolve => audio.initialize(resolve));
|
||||||
|
|
||||||
|
console.info("Audio initialized");
|
||||||
|
//printDevices();
|
||||||
|
|
||||||
|
const recorder = record.create_recorder();
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const defaultInput = audio.available_devices().find(device => device.input_default);
|
||||||
|
if(!defaultInput) {
|
||||||
|
reject("missing default input device");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder.set_device(defaultInput.device_id, result => {
|
||||||
|
if(result === "success") {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
recorder.start(result => {
|
||||||
|
if(typeof result === "boolean" && result) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = playback.create_stream();
|
||||||
|
const recorderConsumer = recorder.create_consumer();
|
||||||
|
|
||||||
|
if(output.channels !== recorderConsumer.channelCount) {
|
||||||
|
throw "miss matching channel count";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(output.sample_rate !== recorderConsumer.sampleRate) {
|
||||||
|
throw "miss matching sample rate";
|
||||||
|
}
|
||||||
|
|
||||||
|
recorderConsumer.callback_data = buffer => output.write_data(buffer.buffer, true);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
const processor = recorder.get_audio_processor();
|
||||||
|
if(!processor) { return; }
|
||||||
|
|
||||||
|
console.error("Config:\n%o", processor.get_config());
|
||||||
|
console.error("Statistics:\n%o", processor.get_statistics());
|
||||||
|
processor.apply_config({
|
||||||
|
"echo_canceller.enabled": false,
|
||||||
|
"rnnoise.enabled": true
|
||||||
|
});
|
||||||
|
}, 2500);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
handle.audio.initialize(() => {
|
handle.audio.initialize(() => {
|
||||||
console.log("Audio initialized");
|
console.log("Audio initialized");
|
||||||
|
|
||||||
@ -60,5 +142,6 @@ handle.audio.initialize(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
*/
|
|
||||||
});
|
});
|
||||||
|
*/
|
18
native/serverconnection/test/js/tsconfig.json
Normal file
18
native/serverconnection/test/js/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"teaclient_connection.node": ["../../exports/exports.d.ts"]
|
||||||
|
},
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es6",
|
||||||
|
"scripthost",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user