Skip to content

Commit

Permalink
feat: implement application passthrough
Browse files Browse the repository at this point in the history
  • Loading branch information
Curve committed Feb 21, 2021
1 parent bb1205b commit f16a630
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 34 deletions.
52 changes: 41 additions & 11 deletions src/helper/audio/audio.hpp
Expand Up @@ -3,7 +3,6 @@

#include <atomic>
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <miniaudio.h>
Expand Down Expand Up @@ -37,6 +36,29 @@ namespace Soundux
return driver == "protocol-native.c" && resampleMethod != "peaks";
}
};
struct PulsePlaybackStream
{
std::uint32_t id;
std::string sink;
std::string name;
std::string driver;

operator bool() const
{
return driver == "protocol-native.c";
}
};
struct PulseData
{
std::uint32_t sinkMonitorId;
std::uint32_t nullSinkModuleId;
std::uint32_t loopbackModuleId;
std::string pulseDefaultSource;

std::uint32_t passthroughModuleId;
std::uint32_t passthroughLoopbackSinkModuleId;
std::uint32_t passthroughLoopbackMonitorModuleId;
};
#endif
struct PlayingSound
{
Expand Down Expand Up @@ -72,18 +94,20 @@ namespace Soundux
static void data_callback(ma_device *device, void *output, const void *input, std::uint32_t frameCount);

#if defined(__linux__)
void setupSink();
void deleteSink() const;
void setupPulse();
void unloadPulse() const;
void unloadLeftOverModules();
void fetchDefaultPulseSource();

std::uint32_t sinkMonitorId;
std::uint32_t nullSinkModuleId;
std::uint32_t loopbackModuleId;
std::string pulseDefaultSource;
PulseData pulseData;
std::optional<PulseRecordingStream> currentApplication;
std::optional<PulsePlaybackStream> currentApplicationPassthrough;

std::shared_mutex recordingStreamMutex;
std::map<std::string, PulseRecordingStream> recordingStreams;

std::shared_mutex playbackStreamMutex;
std::map<std::string, PulsePlaybackStream> playbackStreams;
#endif

void onFinished(ma_device *);
Expand All @@ -98,6 +122,7 @@ namespace Soundux
bool stop(const std::uint32_t &);
std::optional<PlayingSound> pause(const std::uint32_t &);
std::optional<PlayingSound> resume(const std::uint32_t &);
std::optional<PlayingSound> repeat(const std::uint32_t &, bool);
std::optional<PlayingSound> seek(const std::uint32_t &, std::uint64_t);
std::optional<PlayingSound> play(const Objects::Sound &, const std::optional<AudioDevice> & = std::nullopt,
bool = false);
Expand All @@ -110,14 +135,19 @@ namespace Soundux

#if defined(__linux__)
AudioDevice *sinkAudioDevice;
void refreshRecordingStreams();
void moveBackCurrentApplication();
void revertDefaultSourceToOriginal();
void setDefaultSourceToSoundboardSink();
void moveApplicationToSinkMonitor(const PulseRecordingStream &);
void revertDefaultSourceToOriginal() const;
bool moveApplicationToSinkMonitor(const std::string &);

void refreshRecordingStreams();
std::vector<PulseRecordingStream> getRecordingStreams();
std::optional<PulseRecordingStream> getRecordingStream(const std::string &);

void refreshPlaybackStreams();
std::vector<PulsePlaybackStream> getPlaybackStreams();

void moveBackApplicationFromPassthrough();
std::optional<PulsePlaybackStream> moveApplicationToApplicationPassthrough(const std::string &);
#endif

Audio();
Expand Down
177 changes: 154 additions & 23 deletions src/helper/audio/linux/linux.cpp
@@ -1,7 +1,7 @@
#include <optional>
#if defined(__linux__)
#include "../audio.hpp"
#include "fancy.hpp"
#include <optional>
#include <regex>
#include <sstream>

Expand Down Expand Up @@ -33,19 +33,34 @@ auto splitByNewLine(const std::string &str)

namespace Soundux::Objects
{
void Audio::setupSink()
void Audio::setupPulse()
{
fetchDefaultPulseSource();

auto moduleId = exec("pactl load-module module-null-sink sink_name=soundux_sink rate=44100 "
"sink_properties=device.description=soundux_sink");
moduleId.erase(std::remove(moduleId.begin(), moduleId.end(), '\n'), moduleId.end());
nullSinkModuleId = std::stoi(moduleId);
pulseData.nullSinkModuleId = std::stoi(moduleId);

auto loopbackId = exec("pactl load-module module-loopback rate=44100 source=\"" + pulseDefaultSource +
"\" sink=\"soundux_sink\""); // NOLINT
auto loopbackId = exec("pactl load-module module-loopback rate=44100 source=" + pulseData.pulseDefaultSource +
" sink=soundux_sink"); // NOLINT
loopbackId.erase(std::remove(loopbackId.begin(), loopbackId.end(), '\n'), loopbackId.end());
loopbackModuleId = std::stoi(loopbackId);
pulseData.loopbackModuleId = std::stoi(loopbackId);

auto passThroughId = exec("pactl load-module module-null-sink sink_name=soundux_sink_passthrough rate=44100 "
"sink_properties=device.description=soundux_sink_passthrough");
passThroughId.erase(std::remove(passThroughId.begin(), passThroughId.end(), '\n'), passThroughId.end());
pulseData.passthroughModuleId = std::stoi(passThroughId);

auto passThroughSink = exec("pactl load-module module-loopback latency_msec=1 "
"source=soundux_sink_passthrough.monitor sink=soundux_sink");
passThroughSink.erase(std::remove(passThroughSink.begin(), passThroughSink.end(), '\n'), passThroughSink.end());
pulseData.passthroughLoopbackSinkModuleId = std::stoi(passThroughSink);

auto passThroughLoopback = exec("pactl load-module module-loopback source=soundux_sink_passthrough.monitor");
passThroughLoopback.erase(std::remove(passThroughLoopback.begin(), passThroughLoopback.end(), '\n'),
passThroughLoopback.end());
pulseData.passthroughLoopbackMonitorModuleId = std::stoi(passThroughLoopback);

static const std::regex sourceRegex(R"rgx((.*#(\d+))$|(Name: (.+)))rgx");
auto sources = exec("LC_ALL=C pactl list sources");
Expand All @@ -63,18 +78,24 @@ namespace Soundux::Objects

if (match[4].matched && match[4] == "soundux_sink.monitor")
{
sinkMonitorId = std::stoi(deviceId);
pulseData.sinkMonitorId = std::stoi(deviceId);
return;
}
}
}

Fancy::fancy.logTime().failure() << "Failed to find monitor of soundux sink!" << std::endl;
}
void Audio::deleteSink() const
void Audio::unloadPulse() const
{
system(("pactl unload-module " + std::to_string(loopbackModuleId)).c_str()); // NOLINT
system(("pactl unload-module " + std::to_string(nullSinkModuleId)).c_str()); // NOLINT
system(("pactl unload-module " + std::to_string(pulseData.loopbackModuleId)).c_str()); // NOLINT
system(("pactl unload-module " + std::to_string(pulseData.nullSinkModuleId)).c_str()); // NOLINT

system(("pactl unload-module " + std::to_string(pulseData.passthroughModuleId)).c_str()); // NOLINT
// NOLINTNEXTLINE
system(("pactl unload-module " + std::to_string(pulseData.passthroughLoopbackSinkModuleId)).c_str());
// NOLINTNEXTLINE
system(("pactl unload-module " + std::to_string(pulseData.passthroughLoopbackMonitorModuleId)).c_str());
}
void Audio::fetchDefaultPulseSource()
{
Expand All @@ -94,17 +115,18 @@ namespace Soundux::Objects
<< "Default Source is Soundux Sink, this should not happen!" << std::endl;
return;
}
pulseDefaultSource = match[1];
Fancy::fancy.logTime() << "Default Pulse Source was saved: " << pulseDefaultSource << std::endl;
pulseData.pulseDefaultSource = match[1];
Fancy::fancy.logTime()
<< "Default Pulse Source was saved: " << pulseData.pulseDefaultSource << std::endl;
}
}
}
}
void Audio::revertDefaultSourceToOriginal()
void Audio::revertDefaultSourceToOriginal() const
{
if (!pulseDefaultSource.empty())
if (!pulseData.pulseDefaultSource.empty())
{
system(("pactl set-default-source " + pulseDefaultSource).c_str()); // NOLINT
system(("pactl set-default-source " + pulseData.pulseDefaultSource).c_str()); // NOLINT
}
else
{
Expand All @@ -116,11 +138,22 @@ namespace Soundux::Objects
{
system("pactl set-default-source soundboard_sink.monitor"); // NOLINT
}
void Audio::moveApplicationToSinkMonitor(const PulseRecordingStream &stream)
bool Audio::moveApplicationToSinkMonitor(const std::string &streamName)
{
moveBackCurrentApplication();
currentApplication = stream;
system(("pactl move-source-output " + std::to_string(stream.id) + " soundux_sink.monitor").c_str()); // NOLINT

std::shared_lock lock(recordingStreamMutex);
if (recordingStreams.find(streamName) != recordingStreams.end())
{
auto &stream = recordingStreams.at(streamName);
currentApplication = stream;
// NOLINTNEXTLINE
system(("pactl move-source-output " + std::to_string(stream.id) + " soundux_sink.monitor").c_str());
return true;
}
Fancy::fancy.logTime().failure() << "Failed to find PulseRecordingStream with name: " << streamName
<< std::endl;
return false;
}
void Audio::moveBackCurrentApplication()
{
Expand Down Expand Up @@ -186,6 +219,78 @@ namespace Soundux::Objects
recordingStreams.insert({stream.name, stream});
}
}
void Audio::refreshPlaybackStreams()
{
auto sourceList = exec("LC_ALL=C pactl list sink-inputs");

std::vector<PulsePlaybackStream> fetchedStreams;
static const auto playbackStreamRegex =
std::regex(R"rgx((.*#(\d+))|(Driver: (.+))|(Sink: (\d+))|(.*application\.name.* = "(.+)"))rgx");

PulsePlaybackStream stream;
std::smatch match;

for (const auto &line : splitByNewLine(sourceList))
{
if (std::regex_search(line, match, playbackStreamRegex))
{
if (match[2].matched)
{
if (stream)
{
fetchedStreams.push_back(stream);
}

stream = {};
stream.id = std::stoi(match[2]);
}
else if (match[4].matched)
{
stream.driver = match[4];
}
else if (match[6].matched)
{
stream.sink = match[6];
}
else if (match[8].matched)
{
stream.name = match[8];
}
}
}
if (stream && std::find(fetchedStreams.begin(), fetchedStreams.end(), stream) == fetchedStreams.end())
{
fetchedStreams.push_back(stream);
}

std::unique_lock lock(playbackStreamMutex);
for (const auto &stream : fetchedStreams)
{
playbackStreams.insert({stream.name, stream});
}
}
void Audio::unloadLeftOverModules()
{
auto loadedModules = exec("LC_ALL=C pactl list modules");
static const std::regex moduleRegex(R"rgx((Module #(\d+))|(Argument: .*(soundux_sink).*))rgx");

std::smatch match;
std::string currentModuleId;
for (const auto &line : splitByNewLine(loadedModules))
{
if (std::regex_search(line, match, moduleRegex))
{
if (match[2].matched)
{
currentModuleId = match[2];
}
else if (match[4].matched)
{
system(("pactl unload-module " + currentModuleId).c_str()); // NOLINT
}
}
}
}
std::vector<PulseRecordingStream> Audio::getRecordingStreams()
{
std::shared_lock lock(recordingStreamMutex);
Expand All @@ -196,14 +301,40 @@ namespace Soundux::Objects
}
return rtn;
}
std::optional<PulseRecordingStream> Audio::getRecordingStream(const std::string &name)
std::vector<PulsePlaybackStream> Audio::getPlaybackStreams()
{
std::shared_lock lock(recordingStreamMutex);
if (recordingStreams.find(name) != recordingStreams.end())
std::shared_lock lock(playbackStreamMutex);
std::vector<PulsePlaybackStream> rtn;
for (const auto &stream : playbackStreams)
{
rtn.push_back(stream.second);
}
return rtn;
}
void Audio::moveBackApplicationFromPassthrough()
{
if (currentApplicationPassthrough)
{
// NOLINTNEXTLINE
system(("pactl move-sink-input " + std::to_string(currentApplicationPassthrough->id) + " " +
currentApplicationPassthrough->sink)
.c_str());
}
}
std::optional<PulsePlaybackStream> Audio::moveApplicationToApplicationPassthrough(const std::string &name)
{
moveBackApplicationFromPassthrough();

std::shared_lock lock(playbackStreamMutex);
if (playbackStreams.find(name) != playbackStreams.end())
{
return recordingStreams.at(name);
auto &stream = playbackStreams.at(name);
currentApplicationPassthrough = stream;
// NOLINTNEXTLINE
system(("pactl move-sink-input " + std::to_string(stream.id) + " soundux_sink_passthrough").c_str());
return *currentApplicationPassthrough;
}
Fancy::fancy.logTime().warning() << "Could not find PulseRecordingStream with name " << name << std::endl;
Fancy::fancy.logTime().failure() << "Failed to find PulsePlaybackStream with name: " << name << std::endl;
return std::nullopt;
}
} // namespace Soundux::Objects
Expand Down

0 comments on commit f16a630

Please sign in to comment.