@@ -20,6 +20,73 @@
namespace qemu {
namespace {
constexpr auto sample_timeout = 100ms; ///< Longest a microphone read waits before the pipeline checks for shutdown.
/**
* @brief A `SetVolume` call: mute flag and per-channel volumes.
*/
using volume_t = std::pair<bool, std::vector<std::uint8_t>>;
/**
* @brief Last `SetVolume` of every guest stream, kept for as long as Sunshine runs.
* @details QEMU replays `Init` and `SetEnabled` to a new playback listener but not `SetVolume`,
* and the listener is registered again for every streaming session (it lives in Sunshine's
* audio context). Entries are keyed by VM (address, UUID and QEMU process id, so a restarted
* QEMU doesn't inherit them) and stream id, and dropped when QEMU closes the stream.
*/
class volume_cache_t {
public:
/**
* @brief Remember a stream's volume.
*
* @param vm VM key.
* @param id Stream id.
* @param volume Mute flag and volumes.
*/
void store(const std::string &vm, std::uint64_t id, const volume_t &volume) {
std::lock_guard lock {mutex};
volumes[{vm, id}] = volume;
}
/**
* @brief Look up a stream's volume.
*
* @param vm VM key.
* @param id Stream id.
* @return Last volume, or nothing when the guest never set one.
*/
std::optional<volume_t> find(const std::string &vm, std::uint64_t id) {
std::lock_guard lock {mutex};
if (auto it = volumes.find({vm, id}); it != volumes.end()) {
return it->second;
}
return std::nullopt;
}
/**
* @brief Forget a closed stream.
*
* @param vm VM key.
* @param id Stream id.
*/
void erase(const std::string &vm, std::uint64_t id) {
std::lock_guard lock {mutex};
volumes.erase({vm, id});
}
private:
std::mutex mutex; ///< Guards `volumes`.
std::map<std::pair<std::string, std::uint64_t>, volume_t> volumes; ///< Volumes by VM key and stream id.
};
/**
* @brief The process-wide volume cache.
*
* @return Cache.
*/
volume_cache_t &volume_cache() {
static volume_cache_t cache;
return cache;
}
} // namespace
/**
@@ -27,11 +94,25 @@
*/
class audio_hub_t: public audio_out_listener_t {
public:
/**
* @brief Create a hub for one VM.
*
* @param vm_key Identifies the VM in the volume cache.
*/
explicit audio_hub_t(std::string vm_key):
vm_key {std::move(vm_key)} {
}
void init(std::uint64_t id, const pcm_format_t &format) override {
std::lock_guard lock {mutex};
BOOST_LOG(info) << "qemu: guest audio stream "sv << id << ": "sv << to_string(format);
streams[id] = stream_state_t {format};
auto &stream = streams[id] = stream_state_t {format};
// a listener registered by an earlier streaming session may have received the volume
stream.volume = volume_cache().find(vm_key, id);
for (const auto &mixer : mixers) {
mixer->init(id, format);
if (stream.volume) {
mixer->set_volume(id, stream.volume->first, stream.volume->second);
}
}
}
@@ -40,6 +121,7 @@
std::lock_guard lock {mutex};
BOOST_LOG(debug) << "qemu: guest audio stream "sv << id << " closed"sv;
streams.erase(id);
volume_cache().erase(vm_key, id);
for (const auto &mixer : mixers) {
mixer->fini(id);
}
@@ -57,8 +139,10 @@
void set_volume(std::uint64_t id, bool mute, std::span<const std::uint8_t> volume) override {
std::lock_guard lock {mutex};
volume_t state {mute, std::vector<std::uint8_t>(volume.begin(), volume.end())};
volume_cache().store(vm_key, id, state);
if (auto it = streams.find(id); it != streams.end()) {
it->second.volume = std::make_pair(mute, std::vector<std::uint8_t>(volume.begin(), volume.end()));
it->second.volume = std::move(state);
}
for (const auto &mixer : mixers) {
mixer->set_volume(id, mute, volume);
@@ -128,20 +212,22 @@
struct stream_state_t {
pcm_format_t format; ///< PCM layout.
bool enabled {true}; ///< Last SetEnabled value.
std::optional<std::pair<bool, std::vector<std::uint8_t>>> volume; ///< Last SetVolume mute flag and volumes.
std::optional<volume_t> volume; ///< Last SetVolume mute flag and volumes.
};
std::string vm_key; ///< Identifies the VM in the volume cache.
mutable std::mutex mutex; ///< Guards everything below; held while forwarding so calls stay ordered.
std::map<std::uint64_t, stream_state_t> streams; ///< Streams by id.
std::vector<std::shared_ptr<audio_mixer_t>> mixers; ///< Attached mixers.
bool connected {true}; ///< Whether the listener connection is open.
};
std::shared_ptr<audio_output_t> audio_output_t::connect(std::shared_ptr<session_t> session) {
std::shared_ptr<audio_output_t> audio_output_t::connect(std::shared_ptr<session_t> session, const std::string &address) {
if (!session) {
return nullptr;
}
const auto pid = session->qemu_pid();
auto hub = std::make_shared<audio_hub_t>();
auto hub = std::make_shared<audio_hub_t>(address + '\n' + session->vm().uuid + '\n' + (pid ? std::to_string(*pid) : std::string {}));
auto registration = session->register_audio_out_listener(hub);
if (!registration) {
return nullptr;
@@ -261,7 +347,7 @@
std::lock_guard lock {mutex};
if (!output || !output->alive()) {
output.reset();
output = audio_output_t::connect(shared_session(address), address);
output = audio_output_t::connect(shared_session(address));
if (!output) {
BOOST_LOG(error) << "qemu: no guest audio; QEMU needs -audiodev dbus,id=<id> and -display dbus,audiodev=<id>"sv;
return nullptr;