2020-08-19 17:33:57 +00:00
|
|
|
import {AudioRecorderBacked, DeviceList, IDevice,} from "tc-shared/audio/recorder";
|
2020-08-13 11:05:37 +00:00
|
|
|
import {Registry} from "tc-shared/events";
|
|
|
|
import {
|
|
|
|
AbstractInput,
|
2020-09-07 10:42:00 +00:00
|
|
|
FilterMode,
|
2020-08-13 11:05:37 +00:00
|
|
|
InputConsumer,
|
2020-08-19 17:33:57 +00:00
|
|
|
InputConsumerType,
|
|
|
|
InputEvents,
|
2020-11-07 12:16:07 +00:00
|
|
|
MediaStreamRequestResult,
|
2020-08-13 11:05:37 +00:00
|
|
|
InputState,
|
|
|
|
LevelMeter,
|
|
|
|
NodeInputConsumer
|
|
|
|
} from "tc-shared/voice/RecorderBase";
|
|
|
|
import * as log from "tc-shared/log";
|
2020-08-19 17:36:17 +00:00
|
|
|
import {LogCategory, logDebug, logWarn} from "tc-shared/log";
|
2020-08-13 11:05:37 +00:00
|
|
|
import * as aplayer from "./player";
|
|
|
|
import {JAbstractFilter, JStateFilter, JThresholdFilter} from "./RecorderFilter";
|
|
|
|
import {Filter, FilterType, FilterTypeClass} from "tc-shared/voice/Filter";
|
2020-08-19 17:33:57 +00:00
|
|
|
import {inputDeviceList} from "tc-backend/web/audio/RecorderDeviceList";
|
2020-11-07 12:16:07 +00:00
|
|
|
import {requestMediaStream} from "tc-backend/web/media/Stream";
|
2020-08-13 11:05:37 +00:00
|
|
|
|
|
|
|
declare global {
|
|
|
|
interface MediaStream {
|
|
|
|
stop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface WebIDevice extends IDevice {
|
|
|
|
groupId: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class WebAudioRecorder implements AudioRecorderBacked {
|
|
|
|
createInput(): AbstractInput {
|
|
|
|
return new JavascriptInput();
|
|
|
|
}
|
|
|
|
|
|
|
|
async createLevelMeter(device: IDevice): Promise<LevelMeter> {
|
2020-08-19 17:33:57 +00:00
|
|
|
const meter = new JavascriptLevelMeter(device as any);
|
2020-08-13 11:05:37 +00:00
|
|
|
await meter.initialize();
|
|
|
|
return meter;
|
|
|
|
}
|
|
|
|
|
|
|
|
getDeviceList(): DeviceList {
|
|
|
|
return inputDeviceList;
|
|
|
|
}
|
2020-10-01 08:56:54 +00:00
|
|
|
|
|
|
|
isRnNoiseSupported() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleRnNoise(target: boolean) { throw "not supported"; }
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class JavascriptInput implements AbstractInput {
|
|
|
|
public readonly events: Registry<InputEvents>;
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
private state: InputState = InputState.PAUSED;
|
|
|
|
private deviceId: string | undefined;
|
|
|
|
private consumer: InputConsumer;
|
|
|
|
|
|
|
|
private currentStream: MediaStream;
|
|
|
|
private currentAudioStream: MediaStreamAudioSourceNode;
|
2020-08-13 11:05:37 +00:00
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
private audioContext: AudioContext;
|
|
|
|
private sourceNode: AudioNode; /* last node which could be connected to the target; target might be the _consumer_node */
|
|
|
|
private audioNodeCallbackConsumer: ScriptProcessorNode;
|
|
|
|
private readonly audioScriptProcessorCallback;
|
|
|
|
private audioNodeVolume: GainNode;
|
2020-08-13 11:05:37 +00:00
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
/* The node is connected to the audio context. Used for the script processor so it has a sink */
|
|
|
|
private audioNodeMute: GainNode;
|
2020-08-13 11:05:37 +00:00
|
|
|
|
|
|
|
private registeredFilters: (Filter & JAbstractFilter<AudioNode>)[] = [];
|
2020-08-19 17:33:57 +00:00
|
|
|
private inputFiltered: boolean = false;
|
2020-09-07 10:42:00 +00:00
|
|
|
private filterMode: FilterMode = FilterMode.Block;
|
2020-08-13 11:05:37 +00:00
|
|
|
|
2020-11-07 12:16:07 +00:00
|
|
|
private startPromise: Promise<MediaStreamRequestResult | true>;
|
2020-08-13 11:05:37 +00:00
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
private volumeModifier: number = 1;
|
2020-08-13 11:05:37 +00:00
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.events = new Registry<InputEvents>();
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
aplayer.on_ready(() => this.handleAudioInitialized());
|
|
|
|
this.audioScriptProcessorCallback = this.handleAudio.bind(this);
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
2020-10-01 08:56:54 +00:00
|
|
|
destroy() { }
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
private handleAudioInitialized() {
|
|
|
|
this.audioContext = aplayer.context();
|
|
|
|
this.audioNodeMute = this.audioContext.createGain();
|
|
|
|
this.audioNodeMute.gain.value = 0;
|
|
|
|
this.audioNodeMute.connect(this.audioContext.destination);
|
2020-08-13 11:05:37 +00:00
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
this.audioNodeCallbackConsumer = this.audioContext.createScriptProcessor(1024 * 4);
|
|
|
|
this.audioNodeCallbackConsumer.connect(this.audioNodeMute);
|
2020-08-13 11:05:37 +00:00
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
this.audioNodeVolume = this.audioContext.createGain();
|
|
|
|
this.audioNodeVolume.gain.value = this.volumeModifier;
|
2020-08-13 11:05:37 +00:00
|
|
|
|
|
|
|
this.initializeFilters();
|
2020-08-19 17:33:57 +00:00
|
|
|
if(this.state === InputState.INITIALIZING) {
|
|
|
|
this.start().catch(error => {
|
|
|
|
logWarn(LogCategory.AUDIO, tr("Failed to automatically start audio recording: %s"), error);
|
|
|
|
});
|
|
|
|
}
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private initializeFilters() {
|
2020-08-19 17:33:57 +00:00
|
|
|
this.registeredFilters.forEach(e => e.finalize());
|
2020-08-13 11:05:37 +00:00
|
|
|
this.registeredFilters.sort((a, b) => a.priority - b.priority);
|
2020-09-07 10:42:00 +00:00
|
|
|
if(!this.audioContext || !this.audioNodeVolume) {
|
|
|
|
return;
|
|
|
|
}
|
2020-08-19 17:33:57 +00:00
|
|
|
|
2020-09-07 10:42:00 +00:00
|
|
|
if(this.filterMode === FilterMode.Block) {
|
|
|
|
this.switchSourceNode(this.audioNodeMute);
|
|
|
|
} else if(this.filterMode === FilterMode.Filter) {
|
2020-08-19 17:33:57 +00:00
|
|
|
const activeFilters = this.registeredFilters.filter(e => e.isEnabled());
|
|
|
|
|
|
|
|
let chain = "output <- ";
|
|
|
|
let currentSource: AudioNode = this.audioNodeVolume;
|
|
|
|
for(const f of activeFilters) {
|
|
|
|
f.initialize(this.audioContext, currentSource);
|
|
|
|
f.setPaused(false);
|
|
|
|
|
|
|
|
currentSource = f.audioNode;
|
|
|
|
chain += FilterType[f.type] + " <- ";
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
2020-08-19 17:33:57 +00:00
|
|
|
chain += "input";
|
2020-08-19 17:36:17 +00:00
|
|
|
logDebug(LogCategory.AUDIO, tr("Input filter chain: %s"), chain);
|
2020-08-19 17:33:57 +00:00
|
|
|
|
|
|
|
this.switchSourceNode(currentSource);
|
2020-09-07 10:42:00 +00:00
|
|
|
} else if(this.filterMode === FilterMode.Bypass) {
|
|
|
|
this.switchSourceNode(this.audioNodeVolume);
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
2020-09-07 10:42:00 +00:00
|
|
|
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
private handleAudio(event: AudioProcessingEvent) {
|
|
|
|
if(this.consumer?.type !== InputConsumerType.CALLBACK) {
|
2020-08-13 11:05:37 +00:00
|
|
|
return;
|
2020-08-19 17:33:57 +00:00
|
|
|
}
|
2020-08-13 11:05:37 +00:00
|
|
|
|
2020-09-07 10:42:00 +00:00
|
|
|
if(this.consumer.callbackAudio) {
|
|
|
|
this.consumer.callbackAudio(event.inputBuffer);
|
2020-08-19 17:33:57 +00:00
|
|
|
}
|
2020-08-13 11:05:37 +00:00
|
|
|
|
2020-09-07 10:42:00 +00:00
|
|
|
if(this.consumer.callbackBuffer) {
|
2020-08-13 11:05:37 +00:00
|
|
|
log.warn(LogCategory.AUDIO, tr("AudioInput has callback buffer, but this isn't supported yet!"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-07 12:16:07 +00:00
|
|
|
async start() : Promise<MediaStreamRequestResult | true> {
|
2020-08-19 17:33:57 +00:00
|
|
|
while(this.startPromise) {
|
2020-08-13 11:05:37 +00:00
|
|
|
try {
|
2020-08-19 17:33:57 +00:00
|
|
|
await this.startPromise;
|
|
|
|
} catch {}
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
if(this.state != InputState.PAUSED)
|
|
|
|
return;
|
2020-08-13 11:05:37 +00:00
|
|
|
|
2020-09-02 09:41:51 +00:00
|
|
|
/* do it async since if the doStart fails on the first iteration, we're setting the start promise, after it's getting cleared */
|
|
|
|
return await (this.startPromise = Promise.resolve().then(() => this.doStart()));
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
2020-11-07 12:16:07 +00:00
|
|
|
private async doStart() : Promise<MediaStreamRequestResult | true> {
|
2020-08-13 11:05:37 +00:00
|
|
|
try {
|
2020-09-02 09:41:51 +00:00
|
|
|
if(this.state != InputState.PAUSED) {
|
2020-08-13 11:05:37 +00:00
|
|
|
throw tr("recorder already started");
|
2020-09-02 09:41:51 +00:00
|
|
|
}
|
2020-08-13 11:05:37 +00:00
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
this.state = InputState.INITIALIZING;
|
2020-09-02 09:41:51 +00:00
|
|
|
let deviceId;
|
|
|
|
if(this.deviceId === IDevice.NoDeviceId) {
|
|
|
|
throw tr("no device selected");
|
|
|
|
} else if(this.deviceId === IDevice.DefaultDeviceId) {
|
|
|
|
deviceId = undefined;
|
2020-08-19 17:33:57 +00:00
|
|
|
}
|
2020-08-13 11:05:37 +00:00
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
if(!this.audioContext) {
|
|
|
|
/* Awaiting the audio context to be initialized */
|
|
|
|
return;
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
2020-11-07 12:16:07 +00:00
|
|
|
const requestResult = await requestMediaStream(deviceId, undefined, "audio");
|
2020-08-19 17:33:57 +00:00
|
|
|
if(!(requestResult instanceof MediaStream)) {
|
|
|
|
this.state = InputState.PAUSED;
|
|
|
|
return requestResult;
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
2020-08-19 17:33:57 +00:00
|
|
|
this.currentStream = requestResult;
|
2020-08-13 11:05:37 +00:00
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
for(const filter of this.registeredFilters) {
|
|
|
|
if(filter.isEnabled()) {
|
|
|
|
filter.setPaused(false);
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
}
|
2020-08-19 17:33:57 +00:00
|
|
|
/* TODO: Only add if we're really having a callback consumer */
|
|
|
|
this.audioNodeCallbackConsumer.addEventListener('audioprocess', this.audioScriptProcessorCallback);
|
|
|
|
|
|
|
|
this.currentAudioStream = this.audioContext.createMediaStreamSource(this.currentStream);
|
|
|
|
this.currentAudioStream.connect(this.audioNodeVolume);
|
|
|
|
|
|
|
|
this.state = InputState.RECORDING;
|
2020-09-07 10:42:00 +00:00
|
|
|
this.updateFilterStatus(true);
|
2020-08-13 11:05:37 +00:00
|
|
|
|
2020-11-07 12:16:07 +00:00
|
|
|
return true;
|
2020-08-13 11:05:37 +00:00
|
|
|
} catch(error) {
|
2020-08-19 17:33:57 +00:00
|
|
|
if(this.state == InputState.INITIALIZING) {
|
|
|
|
this.state = InputState.PAUSED;
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
2020-08-19 17:33:57 +00:00
|
|
|
|
2020-08-13 11:05:37 +00:00
|
|
|
throw error;
|
|
|
|
} finally {
|
2020-08-19 17:33:57 +00:00
|
|
|
this.startPromise = undefined;
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async stop() {
|
2020-08-19 17:33:57 +00:00
|
|
|
/* await the start */
|
|
|
|
if(this.startPromise) {
|
|
|
|
try {
|
|
|
|
await this.startPromise;
|
|
|
|
} catch {}
|
|
|
|
}
|
2020-08-13 11:05:37 +00:00
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
this.state = InputState.PAUSED;
|
|
|
|
if(this.currentAudioStream) {
|
|
|
|
this.currentAudioStream.disconnect();
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
if(this.currentStream) {
|
|
|
|
if(this.currentStream.stop) {
|
|
|
|
this.currentStream.stop();
|
2020-08-13 11:05:37 +00:00
|
|
|
} else {
|
2020-08-19 17:33:57 +00:00
|
|
|
this.currentStream.getTracks().forEach(value => {
|
2020-08-13 11:05:37 +00:00
|
|
|
value.stop();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
this.currentStream = undefined;
|
|
|
|
this.currentAudioStream = undefined;
|
2020-08-13 11:05:37 +00:00
|
|
|
for(const f of this.registeredFilters) {
|
2020-08-19 17:33:57 +00:00
|
|
|
if(f.isEnabled()) {
|
|
|
|
f.setPaused(true);
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
if(this.audioNodeCallbackConsumer) {
|
|
|
|
this.audioNodeCallbackConsumer.removeEventListener('audioprocess', this.audioScriptProcessorCallback);
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-09-02 09:41:51 +00:00
|
|
|
async setDeviceId(deviceId: string) {
|
2020-08-19 17:33:57 +00:00
|
|
|
if(this.deviceId === deviceId)
|
2020-08-13 11:05:37 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this.stop();
|
|
|
|
} catch(error) {
|
|
|
|
log.warn(LogCategory.AUDIO, tr("Failed to stop previous record session (%o)"), error);
|
|
|
|
}
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
this.deviceId = deviceId;
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
createFilter<T extends FilterType>(type: T, priority: number): FilterTypeClass<T> {
|
|
|
|
let filter: JAbstractFilter<AudioNode> & Filter;
|
|
|
|
switch (type) {
|
|
|
|
case FilterType.STATE:
|
|
|
|
filter = new JStateFilter(priority);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case FilterType.THRESHOLD:
|
|
|
|
filter = new JThresholdFilter(priority);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case FilterType.VOICE_LEVEL:
|
|
|
|
throw tr("voice filter isn't supported!");
|
|
|
|
|
|
|
|
default:
|
|
|
|
throw tr("unknown filter type");
|
|
|
|
}
|
|
|
|
|
2020-09-07 10:42:00 +00:00
|
|
|
filter.callback_active_change = () => this.updateFilterStatus(false);
|
2020-08-19 17:33:57 +00:00
|
|
|
filter.callback_enabled_change = () => this.initializeFilters();
|
|
|
|
|
2020-08-13 11:05:37 +00:00
|
|
|
this.registeredFilters.push(filter);
|
|
|
|
this.initializeFilters();
|
2020-09-07 10:42:00 +00:00
|
|
|
this.updateFilterStatus(false);
|
2020-08-13 11:05:37 +00:00
|
|
|
return filter as any;
|
|
|
|
}
|
|
|
|
|
|
|
|
supportsFilter(type: FilterType): boolean {
|
|
|
|
switch (type) {
|
|
|
|
case FilterType.THRESHOLD:
|
|
|
|
case FilterType.STATE:
|
|
|
|
return true;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
resetFilter() {
|
|
|
|
for(const filter of this.registeredFilters) {
|
|
|
|
filter.finalize();
|
|
|
|
filter.enabled = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.registeredFilters = [];
|
|
|
|
this.initializeFilters();
|
2020-09-07 10:42:00 +00:00
|
|
|
this.updateFilterStatus(false);
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
removeFilter(filterInstance: Filter) {
|
|
|
|
const index = this.registeredFilters.indexOf(filterInstance as any);
|
|
|
|
if(index === -1) return;
|
|
|
|
|
|
|
|
const [ filter ] = this.registeredFilters.splice(index, 1);
|
|
|
|
filter.finalize();
|
|
|
|
filter.enabled = false;
|
|
|
|
|
|
|
|
this.initializeFilters();
|
2020-09-07 10:42:00 +00:00
|
|
|
this.updateFilterStatus(false);
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
2020-09-07 10:42:00 +00:00
|
|
|
private calculateCurrentFilterStatus() {
|
|
|
|
switch (this.filterMode) {
|
|
|
|
case FilterMode.Block:
|
|
|
|
return true;
|
|
|
|
|
|
|
|
case FilterMode.Bypass:
|
|
|
|
return false;
|
|
|
|
|
|
|
|
case FilterMode.Filter:
|
|
|
|
return this.registeredFilters.filter(e => e.isEnabled()).filter(e => e.active).length > 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private updateFilterStatus(forceUpdate: boolean) {
|
|
|
|
let filtered = this.calculateCurrentFilterStatus();
|
2020-08-19 17:33:57 +00:00
|
|
|
if(filtered === this.inputFiltered && !forceUpdate)
|
2020-08-13 11:05:37 +00:00
|
|
|
return;
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
this.inputFiltered = filtered;
|
2020-08-13 11:05:37 +00:00
|
|
|
if(filtered) {
|
2020-08-19 17:33:57 +00:00
|
|
|
this.events.fire("notify_voice_end");
|
2020-08-13 11:05:37 +00:00
|
|
|
} else {
|
2020-08-19 17:33:57 +00:00
|
|
|
this.events.fire("notify_voice_start");
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
isRecording(): boolean {
|
|
|
|
return !this.inputFiltered;
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
async setConsumer(consumer: InputConsumer) {
|
|
|
|
if(this.consumer) {
|
|
|
|
if(this.consumer.type == InputConsumerType.NODE) {
|
2020-09-07 10:42:00 +00:00
|
|
|
if(this.sourceNode) {
|
|
|
|
this.consumer.callbackDisconnect(this.sourceNode);
|
|
|
|
}
|
2020-08-19 17:33:57 +00:00
|
|
|
} else if(this.consumer.type === InputConsumerType.CALLBACK) {
|
2020-09-07 10:42:00 +00:00
|
|
|
if(this.sourceNode) {
|
2020-08-19 17:33:57 +00:00
|
|
|
this.sourceNode.disconnect(this.audioNodeCallbackConsumer);
|
2020-09-07 10:42:00 +00:00
|
|
|
}
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(consumer) {
|
|
|
|
if(consumer.type == InputConsumerType.CALLBACK) {
|
2020-09-07 10:42:00 +00:00
|
|
|
if(this.sourceNode) {
|
2020-08-19 17:33:57 +00:00
|
|
|
this.sourceNode.connect(this.audioNodeCallbackConsumer);
|
2020-09-07 10:42:00 +00:00
|
|
|
}
|
2020-08-13 11:05:37 +00:00
|
|
|
} else if(consumer.type == InputConsumerType.NODE) {
|
2020-09-07 10:42:00 +00:00
|
|
|
if(this.sourceNode) {
|
|
|
|
consumer.callbackNode(this.sourceNode);
|
|
|
|
}
|
2020-08-13 11:05:37 +00:00
|
|
|
} else {
|
|
|
|
throw "native callback consumers are not supported!";
|
|
|
|
}
|
|
|
|
}
|
2020-08-19 17:33:57 +00:00
|
|
|
this.consumer = consumer;
|
|
|
|
}
|
|
|
|
|
|
|
|
private switchSourceNode(newNode: AudioNode) {
|
|
|
|
if(this.consumer) {
|
|
|
|
if(this.consumer.type == InputConsumerType.NODE) {
|
|
|
|
const node_consumer = this.consumer as NodeInputConsumer;
|
|
|
|
if(this.sourceNode) {
|
2020-09-07 10:42:00 +00:00
|
|
|
node_consumer.callbackDisconnect(this.sourceNode);
|
2020-08-19 17:33:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if(newNode) {
|
2020-09-07 10:42:00 +00:00
|
|
|
node_consumer.callbackNode(newNode);
|
2020-08-19 17:33:57 +00:00
|
|
|
}
|
|
|
|
} else if(this.consumer.type == InputConsumerType.CALLBACK) {
|
|
|
|
this.sourceNode.disconnect(this.audioNodeCallbackConsumer);
|
|
|
|
if(newNode) {
|
|
|
|
newNode.connect(this.audioNodeCallbackConsumer);
|
|
|
|
}
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
}
|
2020-08-19 17:33:57 +00:00
|
|
|
|
|
|
|
this.sourceNode = newNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
currentConsumer(): InputConsumer | undefined {
|
|
|
|
return this.consumer;
|
|
|
|
}
|
|
|
|
|
|
|
|
currentDeviceId(): string | undefined {
|
|
|
|
return this.deviceId;
|
|
|
|
}
|
|
|
|
|
|
|
|
currentState(): InputState {
|
|
|
|
return this.state;
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
getVolume(): number {
|
|
|
|
return this.volumeModifier;
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
setVolume(volume: number) {
|
|
|
|
if(volume === this.volumeModifier)
|
2020-08-13 11:05:37 +00:00
|
|
|
return;
|
2020-08-19 17:33:57 +00:00
|
|
|
this.volumeModifier = volume;
|
|
|
|
this.audioNodeVolume.gain.value = volume;
|
|
|
|
}
|
|
|
|
|
|
|
|
isFiltered(): boolean {
|
|
|
|
return this.state === InputState.RECORDING ? this.inputFiltered : true;
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
2020-09-07 10:42:00 +00:00
|
|
|
|
|
|
|
getFilterMode(): FilterMode {
|
|
|
|
return this.filterMode;
|
|
|
|
}
|
|
|
|
|
|
|
|
setFilterMode(mode: FilterMode) {
|
|
|
|
if(this.filterMode === mode) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.filterMode = mode;
|
|
|
|
this.updateFilterStatus(false);
|
|
|
|
this.initializeFilters();
|
|
|
|
}
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
class JavascriptLevelMeter implements LevelMeter {
|
|
|
|
private static meterInstances: JavascriptLevelMeter[] = [];
|
|
|
|
private static meterUpdateTask: number;
|
2020-08-13 11:05:37 +00:00
|
|
|
|
|
|
|
readonly _device: WebIDevice;
|
|
|
|
|
|
|
|
private _callback: (num: number) => any;
|
|
|
|
|
|
|
|
private _context: AudioContext;
|
|
|
|
private _gain_node: GainNode;
|
|
|
|
private _source_node: MediaStreamAudioSourceNode;
|
|
|
|
private _analyser_node: AnalyserNode;
|
|
|
|
|
|
|
|
private _media_stream: MediaStream;
|
|
|
|
|
|
|
|
private _analyse_buffer: Uint8Array;
|
|
|
|
|
|
|
|
private _current_level = 0;
|
|
|
|
|
|
|
|
constructor(device: WebIDevice) {
|
|
|
|
this._device = device;
|
|
|
|
}
|
|
|
|
|
|
|
|
async initialize() {
|
|
|
|
try {
|
|
|
|
await new Promise((resolve, reject) => {
|
|
|
|
const timeout = setTimeout(reject, 5000);
|
|
|
|
aplayer.on_ready(() => {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} catch(error) {
|
|
|
|
throw tr("audio context timeout");
|
|
|
|
}
|
|
|
|
this._context = aplayer.context();
|
|
|
|
if(!this._context) throw tr("invalid context");
|
|
|
|
|
|
|
|
this._gain_node = this._context.createGain();
|
|
|
|
this._gain_node.gain.setValueAtTime(0, 0);
|
|
|
|
|
|
|
|
/* analyser node */
|
|
|
|
this._analyser_node = this._context.createAnalyser();
|
|
|
|
|
|
|
|
const optimal_ftt_size = Math.ceil(this._context.sampleRate * (JThresholdFilter.update_task_interval / 1000));
|
|
|
|
this._analyser_node.fftSize = Math.pow(2, Math.ceil(Math.log2(optimal_ftt_size)));
|
|
|
|
|
|
|
|
if(!this._analyse_buffer || this._analyse_buffer.length < this._analyser_node.fftSize)
|
|
|
|
this._analyse_buffer = new Uint8Array(this._analyser_node.fftSize);
|
|
|
|
|
|
|
|
/* starting stream */
|
2020-11-07 12:16:07 +00:00
|
|
|
const _result = await requestMediaStream(this._device.deviceId, this._device.groupId, "audio");
|
2020-08-13 11:05:37 +00:00
|
|
|
if(!(_result instanceof MediaStream)){
|
2020-11-07 12:16:07 +00:00
|
|
|
if(_result === MediaStreamRequestResult.ENOTALLOWED)
|
2020-08-13 11:05:37 +00:00
|
|
|
throw tr("No permissions");
|
2020-11-07 12:16:07 +00:00
|
|
|
if(_result === MediaStreamRequestResult.ENOTSUPPORTED)
|
2020-08-13 11:05:37 +00:00
|
|
|
throw tr("Not supported");
|
2020-11-07 12:16:07 +00:00
|
|
|
if(_result === MediaStreamRequestResult.EBUSY)
|
2020-08-13 11:05:37 +00:00
|
|
|
throw tr("Device busy");
|
2020-11-07 12:16:07 +00:00
|
|
|
if(_result === MediaStreamRequestResult.EUNKNOWN)
|
2020-08-13 11:05:37 +00:00
|
|
|
throw tr("an error occurred");
|
|
|
|
throw _result;
|
|
|
|
}
|
|
|
|
this._media_stream = _result;
|
|
|
|
|
|
|
|
this._source_node = this._context.createMediaStreamSource(this._media_stream);
|
|
|
|
this._source_node.connect(this._analyser_node);
|
|
|
|
this._analyser_node.connect(this._gain_node);
|
|
|
|
this._gain_node.connect(this._context.destination);
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
JavascriptLevelMeter.meterInstances.push(this);
|
|
|
|
if(JavascriptLevelMeter.meterInstances.length == 1) {
|
|
|
|
clearInterval(JavascriptLevelMeter.meterUpdateTask);
|
|
|
|
JavascriptLevelMeter.meterUpdateTask = setInterval(() => JavascriptLevelMeter._analyse_all(), JThresholdFilter.update_task_interval) as any;
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy() {
|
2020-08-19 17:33:57 +00:00
|
|
|
JavascriptLevelMeter.meterInstances.remove(this);
|
|
|
|
if(JavascriptLevelMeter.meterInstances.length == 0) {
|
|
|
|
clearInterval(JavascriptLevelMeter.meterUpdateTask);
|
|
|
|
JavascriptLevelMeter.meterUpdateTask = 0;
|
2020-08-13 11:05:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if(this._source_node) {
|
|
|
|
this._source_node.disconnect();
|
|
|
|
this._source_node = undefined;
|
|
|
|
}
|
|
|
|
if(this._media_stream) {
|
|
|
|
if(this._media_stream.stop)
|
|
|
|
this._media_stream.stop();
|
|
|
|
else
|
|
|
|
this._media_stream.getTracks().forEach(value => {
|
|
|
|
value.stop();
|
|
|
|
});
|
|
|
|
this._media_stream = undefined;
|
|
|
|
}
|
|
|
|
if(this._gain_node) {
|
|
|
|
this._gain_node.disconnect();
|
|
|
|
this._gain_node = undefined;
|
|
|
|
}
|
|
|
|
if(this._analyser_node) {
|
|
|
|
this._analyser_node.disconnect();
|
|
|
|
this._analyser_node = undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-07 10:42:00 +00:00
|
|
|
getDevice(): IDevice {
|
2020-08-13 11:05:37 +00:00
|
|
|
return this._device;
|
|
|
|
}
|
|
|
|
|
2020-09-07 10:42:00 +00:00
|
|
|
setObserver(callback: (value: number) => any) {
|
2020-08-13 11:05:37 +00:00
|
|
|
this._callback = callback;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static _analyse_all() {
|
2020-08-19 17:33:57 +00:00
|
|
|
for(const instance of [...this.meterInstances])
|
2020-08-13 11:05:37 +00:00
|
|
|
instance._analyse();
|
|
|
|
}
|
|
|
|
|
|
|
|
private _analyse() {
|
|
|
|
this._analyser_node.getByteTimeDomainData(this._analyse_buffer);
|
|
|
|
|
2020-08-19 17:33:57 +00:00
|
|
|
this._current_level = JThresholdFilter.calculateAudioLevel(this._analyse_buffer, this._analyser_node.fftSize, this._current_level, .75);
|
2020-08-13 11:05:37 +00:00
|
|
|
if(this._callback)
|
|
|
|
this._callback(this._current_level);
|
|
|
|
}
|
2020-08-19 17:33:57 +00:00
|
|
|
}
|