Skip to content

Commit

Permalink
FEAT(client, ui): Added JACK transport recording options for external…
Browse files Browse the repository at this point in the history
… per-user audio capture/processing
  • Loading branch information
IsaMorphic committed Jan 27, 2023
1 parent 72d2d55 commit 200d103
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 20 deletions.
32 changes: 21 additions & 11 deletions src/mumble/AudioOutput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,21 @@ void AudioOutput::initializeMixer(const unsigned int *chanmasks, bool forceheadp
}
}

void AudioOutput::prepareOutputBuffers(unsigned int frameCount, QList< AudioOutputBuffer * > *qlMix,
QList< AudioOutputBuffer * > *qlDel) {
// Get the users that are currently talking (and are thus serving as an audio source)
QMultiHash< const ClientUser *, AudioOutputBuffer * >::const_iterator it = qmOutputs.constBegin();
while (it != qmOutputs.constEnd()) {
AudioOutputBuffer *buffer = it.value();
if (!buffer->prepareSampleBuffer(frameCount)) {
qlDel->append(buffer);
} else {
qlMix->append(buffer);
}
++it;
}
}

bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
#ifdef USE_MANUAL_PLUGIN
positions.clear();
Expand Down Expand Up @@ -431,23 +446,18 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {

bool prioritySpeakerActive = false;

// Get the users that are currently talking (and are thus serving as an audio source)
// Detect whether priority speaker is active.
QMultiHash< const ClientUser *, AudioOutputBuffer * >::const_iterator it = qmOutputs.constBegin();
while (it != qmOutputs.constEnd()) {
AudioOutputBuffer *buffer = it.value();
if (!buffer->prepareSampleBuffer(frameCount)) {
qlDel.append(buffer);
} else {
qlMix.append(buffer);

const ClientUser *user = it.key();
if (user && user->bPrioritySpeaker) {
prioritySpeakerActive = true;
}
const ClientUser *user = it.key();
if (user && user->bPrioritySpeaker) {
prioritySpeakerActive = true;
}
++it;
}

prepareOutputBuffers(frameCount, &qlMix, &qlDel);

if (Global::get().prioritySpeakerActiveOverride) {
prioritySpeakerActive = true;
}
Expand Down
3 changes: 3 additions & 0 deletions src/mumble/AudioOutput.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ private slots:
void initializeMixer(const unsigned int *chanmasks, bool forceheadphone = false);
bool mix(void *output, unsigned int frameCount);

virtual void prepareOutputBuffers(unsigned int frameCount, QList< AudioOutputBuffer * > *qlMix,
QList< AudioOutputBuffer * > *qlDel);

public:
void wipe();

Expand Down
8 changes: 5 additions & 3 deletions src/mumble/EnumStringConversions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@
PROCESS(Settings::WindowLayout, LayoutHybrid, "Hybrid") \
PROCESS(Settings::WindowLayout, LayoutCustom, "Custom")

#define RECORDING_MODE_VALUES \
PROCESS(Settings::RecordingMode, RecordingMixdown, "Mixdown") \
PROCESS(Settings::RecordingMode, RecordingMultichannel, "Multichannel")
#define RECORDING_MODE_VALUES \
PROCESS(Settings::RecordingMode, RecordingMixdown, "Mixdown") \
PROCESS(Settings::RecordingMode, RecordingMultichannel, "Multichannel") \
PROCESS(Settings::RecordingMode, RecordingMultichannelAndTransport, "MultichannelAndTransport") \
PROCESS(Settings::RecordingMode, RecordingTransportStandalone, "MultichannelAndTransport")

#define SEARCH_USER_ACTION_VALUES \
PROCESS(Search::SearchDialog::UserAction, NONE, "None") \
Expand Down
106 changes: 106 additions & 0 deletions src/mumble/JackAudio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#include "JackAudio.h"
#include "AudioOutputBuffer.h"
#include "ServerHandler.h"
#include "VoiceRecorder.h"

#include "Utils.h"
#include "Global.h"
Expand Down Expand Up @@ -232,6 +235,7 @@ JackAudioSystem::JackAudioSystem() : bAvailable(false), users(0), client(nullptr
RESOLVE(jack_port_by_name)
RESOLVE(jack_port_flags)
RESOLVE(jack_port_get_buffer)
RESOLVE(jack_port_rename)
RESOLVE(jack_ringbuffer_create)
RESOLVE(jack_ringbuffer_free)
RESOLVE(jack_ringbuffer_mlock)
Expand Down Expand Up @@ -525,6 +529,22 @@ bool JackAudioSystem::disconnectPort(jack_port_t *port) {
return true;
}

bool JackAudioSystem::renamePort(jack_port_t *port, const char *name) {
QMutexLocker lock(&qmWait);

if (!client || !port) {
return false;
}

const auto ret = jack_port_rename(client, port, name);
if (ret != 0) {
qWarning("JackAudioSystem: unable to rename port - jack_port_rename() returned %i", ret);
return false;
}

return true;
}

// Ringbuffer functions do not have locks.
// They are single-consumer single-producer, lockless.
jack_ringbuffer_t *JackAudioSystem::ringbufferCreate(const size_t size) {
Expand Down Expand Up @@ -960,8 +980,23 @@ bool JackAudioOutput::unregisterPorts() {
}
}

for (auto port : userPorts.values()) {
if (!port) {
continue;
}

if (!jas->unregisterPort(port)) {
qWarning("JackAudioOutput: unable to unregister port named \"%s\"",
userPorts.key(port).toStdString().c_str());
ret = false;
}
}



outputBuffers.clear();
ports.clear();
userPorts.clear();

return ret;
}
Expand Down Expand Up @@ -1059,6 +1094,77 @@ bool JackAudioOutput::process(const jack_nframes_t frames) {
return true;
}

void JackAudioOutput::prepareOutputBuffers(unsigned int frameCount, QList< AudioOutputBuffer * > *qlMix,
QList< AudioOutputBuffer * > *qlDel) {
ServerHandlerPtr sh = Global::get().sh;
VoiceRecorderPtr recorder;
if (sh) {
recorder = Global::get().sh->recorder;
}

// Keep track of ports we've filled the audio of.
QList< jack_port_t * > qlPortsYetToFill(userPorts.values());

// Register user ports based on who is currently present in qmOutputs and route their audio to JACK.
// Don't care if users get removed for now. TODO: maybe at some point we do.
QMultiHash< const ClientUser *, AudioOutputBuffer * >::const_iterator it = qmOutputs.constBegin();
while (it != qmOutputs.constEnd()) {
const ClientUser *user = it.key();
AudioOutputBuffer *audio = it.value();

if (!user || !recorder || (recorder && !recorder->isTransportEnabled())) {
if (audio->prepareSampleBuffer(frameCount)) {
qlMix->append(audio);
} else {
qlDel->append(audio);
}

++it;
continue;
}

QString qsPortName = user->qsName;
qsPortName = qsPortName.prepend("user_");
if (!userPorts[qsPortName]) {
auto port = jas->registerPort(qsPortName.toStdString().c_str(), JackPortIsOutput);
if (!port) {
qWarning("JackAudioOutput: unable to register user port \"%s\"", qsPortName.toStdString().c_str());
} else {
userPorts[qsPortName] = port;
}
}

if (audio->prepareSampleBuffer(frameCount)) {
qlPortsYetToFill.removeOne(userPorts[qsPortName]);
qlMix->append(audio);

auto outputBuffer = (float *) jas->getPortBuffer(userPorts[qsPortName], frameCount);
if (audio->bStereo) {
// Mix down stereo to mono. TODO: stereo record support
// frame: for a stereo stream, the [LR] pair inside ...[LR]LRLRLR.... is a frame
for (unsigned int i = 0; i < frameCount; ++i) {
outputBuffer[i] = (audio->pfBuffer[2 * i] / 2.0 + audio->pfBuffer[2 * i + 1] / 2.0);
}
} else {
for (unsigned int i = 0; i < frameCount; ++i) {
outputBuffer[i] = audio->pfBuffer[i];
}
}
} else {
qlDel->append(audio);
}

++it;
}

for (auto port : qlPortsYetToFill) {
if (port) {
auto outputBuffer = jas->getPortBuffer(port, frameCount);
memset(outputBuffer, 0, sizeof(float) * frameCount);
}
}
}

void JackAudioOutput::run() {
if (!bReady) {
return;
Expand Down
7 changes: 7 additions & 0 deletions src/mumble/JackAudio.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ struct jack_ringbuffer_data_t {
};

typedef QVector< jack_port_t * > JackPorts;
typedef QHash< QString, jack_port_t * > JackNamedPorts;
typedef QVector< jack_default_audio_sample_t * > JackBuffers;
typedef QVector< jack_ringbuffer_t * > JackRingBuffers;

class JackAudioInit;

Expand Down Expand Up @@ -68,6 +70,7 @@ class JackAudioSystem : public QObject {
int (*jack_port_unregister)(jack_client_t *client, jack_port_t *port);
int (*jack_port_flags)(const jack_port_t *port);
void *(*jack_port_get_buffer)(jack_port_t *port, jack_nframes_t frames);
int (*jack_port_rename)(jack_client_t *client, jack_port_t *port, const char *name);
void (*jack_free)(void *ptr);
jack_client_t *(*jack_client_open)(const char *client_name, jack_options_t options, jack_status_t *status, ...);
jack_nframes_t (*jack_get_sample_rate)(jack_client_t *client);
Expand Down Expand Up @@ -100,6 +103,7 @@ class JackAudioSystem : public QObject {
bool unregisterPort(jack_port_t *port);
bool connectPort(jack_port_t *sourcePort, jack_port_t *destinationPort);
bool disconnectPort(jack_port_t *port);
bool renamePort(jack_port_t *port, const char *name);

jack_ringbuffer_t *ringbufferCreate(const size_t size);
void ringbufferFree(jack_ringbuffer_t *buffer);
Expand Down Expand Up @@ -159,8 +163,11 @@ class JackAudioOutput : public AudioOutput {
QMutex qmWait;
QSemaphore qsSleep;
JackPorts ports;
JackNamedPorts userPorts;
JackBuffers outputBuffers;
jack_ringbuffer_t *buffer;
void prepareOutputBuffers(unsigned int frameCount, QList< AudioOutputBuffer * > *qlMix,
QList< AudioOutputBuffer * > *qlDel) override;

public:
bool isReady();
Expand Down
7 changes: 6 additions & 1 deletion src/mumble/Settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,12 @@ struct Settings {
enum WindowLayout { LayoutClassic, LayoutStacked, LayoutHybrid, LayoutCustom };
enum AlwaysOnTopBehaviour { OnTopNever, OnTopAlways, OnTopInMinimal, OnTopInNormal };
enum ProxyType { NoProxy, HttpProxy, Socks5Proxy };
enum RecordingMode { RecordingMixdown, RecordingMultichannel };
enum RecordingMode {
RecordingMixdown,
RecordingMultichannel,
RecordingMultichannelAndTransport,
RecordingTransportStandalone
};

typedef QPair< QList< QSslCertificate >, QSslKey > KeyPair;

Expand Down
6 changes: 5 additions & 1 deletion src/mumble/VoiceRecorder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ void VoiceRecorder::run() {
break;
}

while (!m_abort && !m_recordBuffer.isEmpty()) {
while (!(m_config.mixDownMode && m_config.transportEnable) && !m_abort && !m_recordBuffer.isEmpty()) {
boost::shared_ptr< RecordBuffer > rb;
{
QMutexLocker l(&m_bufferLock);
Expand Down Expand Up @@ -430,6 +430,10 @@ bool VoiceRecorder::isInMixDownMode() const {
return m_config.mixDownMode;
}

bool VoiceRecorder::isTransportEnabled() const {
return m_config.transportEnable;
}

QString VoiceRecorderFormat::getFormatDescription(VoiceRecorderFormat::Format fm) {
switch (fm) {
case VoiceRecorderFormat::WAV:
Expand Down
9 changes: 8 additions & 1 deletion src/mumble/VoiceRecorder.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ class VoiceRecorder : public QThread {
/// True if multi channel recording is disabled.
bool mixDownMode;

/// True if one of the two transport options is enabled.
bool transportEnable;

/// The current recording format.
VoiceRecorderFormat::Format recordingFormat;
};
Expand Down Expand Up @@ -119,8 +122,12 @@ class VoiceRecorder : public QThread {
/// Returns a reference to the record user which is used to record local audio.
RecordUser &getRecordUser() const;

/// Returns true if the recorder is recording mixed down data instead of multichannel
/// Returns true if the recorder is recording mixed down data instead of multichannel.
bool isInMixDownMode() const;

/// Returns true if the recorder was instructed to enable an external transport.
bool isTransportEnabled() const;

signals:
/// Emitted if an error is encountered
void error(int err, QString strerr);
Expand Down
33 changes: 30 additions & 3 deletions src/mumble/VoiceRecorderDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,14 @@ VoiceRecorderDialog::VoiceRecorderDialog(QWidget *p) : QDialog(p), qtTimer(new Q

qleTargetDirectory->setText(Global::get().s.qsRecordingPath);
qleFilename->setText(Global::get().s.qsRecordingFile);

qrbDownmix->setChecked(Global::get().s.rmRecordingMode == Settings::RecordingMixdown);
qrbMultichannel->setChecked(Global::get().s.rmRecordingMode == Settings::RecordingMultichannel);
qrbMultichannelAndTransport->setChecked(Global::get().s.rmRecordingMode
== Settings::RecordingMultichannelAndTransport);
qrbTransportStandalone->setChecked(Global::get().s.rmRecordingMode == Settings::RecordingTransportStandalone);

qgbOutput->setDisabled(qrbTransportStandalone->isChecked());

QString qsTooltip = QString::fromLatin1("%1"
"<table>"
Expand Down Expand Up @@ -94,8 +100,12 @@ void VoiceRecorderDialog::closeEvent(QCloseEvent *evt) {
Global::get().s.qsRecordingFile = qleFilename->text();
if (qrbDownmix->isChecked())
Global::get().s.rmRecordingMode = Settings::RecordingMixdown;
else
else if (qrbMultichannel->isChecked())
Global::get().s.rmRecordingMode = Settings::RecordingMultichannel;
else if (qrbMultichannelAndTransport->isChecked())
Global::get().s.rmRecordingMode = Settings::RecordingMultichannelAndTransport;
else if (qrbTransportStandalone->isChecked())
Global::get().s.rmRecordingMode = Settings::RecordingTransportStandalone;

int i = qcbFormat->currentIndex();
Global::get().s.iRecordingFormat = (i == -1) ? 0 : i;
Expand All @@ -106,6 +116,22 @@ void VoiceRecorderDialog::closeEvent(QCloseEvent *evt) {
QDialog::closeEvent(evt);
}

void VoiceRecorderDialog::on_qrbDownmix_clicked() {
qgbOutput->setEnabled(true);
}

void VoiceRecorderDialog::on_qrbMultichannel_clicked() {
qgbOutput->setEnabled(true);
}

void VoiceRecorderDialog::on_qrbMultichannelAndTransport_clicked() {
qgbOutput->setEnabled(true);
}

void VoiceRecorderDialog::on_qrbTransportStandalone_clicked() {
qgbOutput->setDisabled(true);
}

void VoiceRecorderDialog::on_qpbStart_clicked() {
if (!Global::get().uiSession || !Global::get().sh) {
QMessageBox::critical(this, tr("Recorder"), tr("Unable to start recording. Not connected to a server."));
Expand Down Expand Up @@ -164,7 +190,8 @@ void VoiceRecorderDialog::on_qpbStart_clicked() {
VoiceRecorder::Config config;
config.sampleRate = ao->getMixerFreq();
config.fileName = dir.absoluteFilePath(basename + QLatin1Char('.') + suffix);
config.mixDownMode = qrbDownmix->isChecked();
config.mixDownMode = qrbDownmix->isChecked() || qrbTransportStandalone->isChecked();
config.transportEnable = qrbMultichannelAndTransport->isChecked() || qrbTransportStandalone->isChecked();
config.recordingFormat = static_cast< VoiceRecorderFormat::Format >(ifm);

if (config.sampleRate == 0) {
Expand Down Expand Up @@ -261,7 +288,7 @@ void VoiceRecorderDialog::reset(bool resettimer) {
qpbStop->setText(tr("S&top"));

qgbMode->setEnabled(true);
qgbOutput->setEnabled(true);
qgbOutput->setDisabled(qrbTransportStandalone->isChecked());

if (resettimer)
qlTime->setText(QLatin1String("00:00:00"));
Expand Down
4 changes: 4 additions & 0 deletions src/mumble/VoiceRecorderDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class VoiceRecorderDialog : public QDialog, private Ui::VoiceRecorderDialog {

void closeEvent(QCloseEvent *evt) Q_DECL_OVERRIDE;
public slots:
void on_qrbDownmix_clicked();
void on_qrbMultichannel_clicked();
void on_qrbMultichannelAndTransport_clicked();
void on_qrbTransportStandalone_clicked();
void on_qpbStart_clicked();
void on_qpbStop_clicked();
void on_qtTimer_timeout();
Expand Down
Loading

0 comments on commit 200d103

Please sign in to comment.