Skip to content

Commit

Permalink
feat(Voice): voice support for Windows and macOS
Browse files Browse the repository at this point in the history
Refactored the entire voice subsystem, now with a generalized API.
Added ffplay-static to handle playback on Windows and macOS.
Added an option to specify additional ffmpeg args (filters etc.).
Linux requires installed ffmpeg, static does not include pulse support.
macOS compatibility not tested.

Partially resolves issue #10.
  • Loading branch information
Garifullin Ruslan committed Jan 5, 2021
1 parent c7bce6b commit 1b08596
Show file tree
Hide file tree
Showing 16 changed files with 461 additions and 131 deletions.
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"discord.js": "^12.5.1",
"env-paths": "^2.2.0",
"ffmpeg-static": "^4.2.7",
"ffplay-static": "^3.2.2",
"i18n": "^0.13.2",
"markdown-it": "^12.0.4",
"node-notifier": "^8.0.1",
Expand Down
25 changes: 13 additions & 12 deletions src/components/VoicePanel/VoicePanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
QWidget,
} from '@nodegui/nodegui';
import { Input, Mixer } from 'audio-mixer';
import { ChildProcessWithoutNullStreams } from 'child_process';
import {
Client,
Constants,
Expand All @@ -28,7 +27,9 @@ import { ConfigManager } from '../../utilities/ConfigManager';
import { createLogger } from '../../utilities/Console';
import { Events as AppEvents } from '../../utilities/Events';
import { __ } from '../../utilities/StringProvider';
import { createPlaybackStream, createRecordStream } from '../../utilities/VoiceStreams';
import { PlaybackStream } from '../../utilities/voice/PlaybackStream';
import { RecordStream } from '../../utilities/voice/RecordStream';
import { voiceProvider as vp } from '../../utilities/voice/VoiceProviderManager';
import { DIconButton } from '../DIconButton/DIconButton';
import { NoiseReductor } from './NoiseReductor';
import { Silence } from './Silence';
Expand Down Expand Up @@ -58,13 +59,13 @@ export class VoicePanel extends QWidget {

private currentPlaybackDevice?: string;

private playbackStream?: ChildProcessWithoutNullStreams;
private playbackStream?: PlaybackStream;

private playbackVolumeTransformer?: VolumeTransformer;

private currentRecordDevice?: string;

private recordStream?: ChildProcessWithoutNullStreams;
private recordStream?: RecordStream;

private recordVolumeTransformer?: VolumeTransformer;

Expand Down Expand Up @@ -157,8 +158,8 @@ export class VoicePanel extends QWidget {
private handleDisconnectButton() {
this.statusLabel.setText("<font color='#f04747'>Disconnecting</font>");
this.connection?.disconnect();
this.recordStream?.kill();
this.playbackStream?.kill();
this.recordStream?.end();
this.playbackStream?.end();
this.mixer?.close();
this.streams.clear();
this.hide();
Expand Down Expand Up @@ -230,22 +231,22 @@ export class VoicePanel extends QWidget {
}

private initPlayback() {
if (!this.connection || !this.channel) {
if (!this.connection || !this.channel || !vp) {
return;
}

const { channel } = this;

this.mixer?.close();
this.playbackStream?.kill();
this.playbackStream?.end();
this.streams.clear();

pipeline(
(this.mixer = new Mixer(MIXER_OPTIONS)), // Initialize the member streams mixer
((this.playbackVolumeTransformer = new VolumeTransformer({
type: 's16le',
})) as unknown) as Transform, // Change the volume
(this.playbackStream = createPlaybackStream()).stdin, // Output to a playback stream
(this.playbackStream = vp.createPlaybackStream()).stream, // Output to a playback stream
(err) => err && debug("Couldn't finish playback pipeline.", err)
);

Expand All @@ -255,14 +256,14 @@ export class VoicePanel extends QWidget {
}

private initRecord() {
if (!this.connection) {
if (!this.connection || !vp) {
return;
}

this.connection.dispatcher.end();

const recorder = pipeline(
(this.recordStream = createRecordStream()).stdout, // Open a recording stream
(this.recordStream = vp.createRecordStream()).stream, // Open a recording stream
((this.recordVolumeTransformer = new VolumeTransformer({
type: 's16le',
})) as unknown) as Transform,
Expand All @@ -277,7 +278,7 @@ export class VoicePanel extends QWidget {
private async joinChannel(channel: VoiceChannel) {
const { infoLabel, statusLabel, createConnection, initPlayback, initRecord } = this;

if (process.platform !== 'linux') {
if (!vp) {
VoicePanel.openVoiceNotSupportedDialog(channel);

return;
Expand Down
2 changes: 2 additions & 0 deletions src/utilities/IConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export type VoiceSettings = {
inputVolume?: number;
outputVolume?: number;
inputSensitivity?: number;
playbackOptions?: string;
recordOptions?: string;
};

export type OverlaySettings = {
Expand Down
87 changes: 0 additions & 87 deletions src/utilities/VoiceStreams.ts

This file was deleted.

35 changes: 35 additions & 0 deletions src/utilities/voice/DarwinVoiceProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* eslint-disable global-require */
import { ID, S16LE_OPTIONS } from './VoiceOptions';
import { VoiceProvider } from './VoiceProvider';

/**
* VoiceProvider for systems on macOS.
* THIS WAS NOT TESTED. I do not have a macOS system to test this
* implementation's behaviour, most likely it's similar to Windows.
* TODO: Input/output devices detection.
*/
export class DarwinVoiceProvider extends VoiceProvider {
public PLAYBACK_OPTIONS = [...S16LE_OPTIONS, ID.PLAYBACK_OPTIONS, '-'];

public RECORD_OPTIONS = [
'-f',
'avfoundation',
'-i',
`"none:${ID.RECORD_DEVICE}"`,
ID.RECORD_OPTIONS,
...S16LE_OPTIONS,
'-',
];

public readonly PLAYBACK_DEFAULT_DEVICE = '0';

public readonly RECORD_DEFAULT_DEVICE = '0';

createPlaybackStream = this.createFFplayPlaybackStream;

createRecordStream = this.createFFmpegRecordStream;

getInputDevices = () => ['0'];

getOutputDevices = () => ['0'];
}
14 changes: 14 additions & 0 deletions src/utilities/voice/FFProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { platform } from 'os';
import { join } from 'path';

export function getFFmpeg() {
if (platform() === 'linux') {
return 'ffmpeg';
}

return join(__dirname, `ffmpeg${platform() === 'win32' ? '.exe' : ''}`);
}

export function getFFplay() {
return join(__dirname, `ffplay${platform() === 'win32' ? '.exe' : ''}`);
}
77 changes: 77 additions & 0 deletions src/utilities/voice/LinuxVoiceProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { execSync } from 'child_process';
import { createLogger } from '../Console';
import { ID, S16LE_OPTIONS } from './VoiceOptions';
import { VoiceProvider } from './VoiceProvider';

const { error } = createLogger('LinuxVoiceProvider');

/**
* VoiceProvider for systems on Linux with PulseAudio support.
*/
export class LinuxVoiceProvider extends VoiceProvider {
public PLAYBACK_OPTIONS = [
...S16LE_OPTIONS,
ID.PLAYBACK_OPTIONS,
'-i',
'-',
'-f',
'pulse',
'-buffer_duration',
'5',
'-name',
ID.APP_NAME,
ID.PLAYBACK_DEVICE,
];

public RECORD_OPTIONS = [
'-f',
'pulse',
'-name',
ID.APP_NAME,
'-i',
ID.RECORD_DEVICE,
ID.RECORD_OPTIONS,
...S16LE_OPTIONS,
'-',
];

public readonly PLAYBACK_DEFAULT_DEVICE = 'default';

public readonly RECORD_DEFAULT_DEVICE = 'default';

createPlaybackStream = this.createFFmpegPlaybackStream;

createRecordStream = this.createFFmpegRecordStream;

getInputDevices = () => {
try {
const sourcesOut = execSync('pactl list sources short').toString();
const sources = sourcesOut
.split('\n')
.map((value) => value.split('\t')[1])
.filter((value) => value);

return sources;
} catch (e) {
error(e);

return [this.RECORD_DEFAULT_DEVICE];
}
};

getOutputDevices = () => {
try {
const sinksOut = execSync('pactl list sinks short').toString();
const sinks = sinksOut
.split('\n')
.map((value) => value.split('\t')[1])
.filter((value) => value);

return sinks;
} catch (e) {
error(e);

return [this.PLAYBACK_DEFAULT_DEVICE];
}
};
}
7 changes: 7 additions & 0 deletions src/utilities/voice/PlaybackStream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Writable } from 'stream';

export type PlaybackStream = {
stream: Writable;
device: string;
end(): void;
};
7 changes: 7 additions & 0 deletions src/utilities/voice/RecordStream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Readable } from 'stream';

export type RecordStream = {
stream: Readable;
device: string;
end(): void;
};
Loading

0 comments on commit 1b08596

Please sign in to comment.