Mix live stream PCM into the shared desktop audio output

This commit is contained in:
evgeny committed 2026-10-20 16:27:24 +02:00
1 parent 94697c1af0
commit a0b6dd7ef5
2 files changed
+49 -4

No files matched your search

+36 -3
View File
@@ -82,6 +82,7 @@ bool SoundManager::openContext() {
void SoundManager::openOutput() {
for (auto* voice : m_voices) if (voice) voice->resetPlayback();
++m_outputGeneration;
if (m_output.initialized() && m_output.start()) return;
m_restoreSelections = true;
ma_device_config config = ma_device_config_init(ma_device_type_playback);
@@ -93,10 +94,13 @@ void SoundManager::openOutput() {
if (selected) config.playback.pDeviceID = &id;
auto callback = [this, previousRoute = uint64_t(0)](const void*, void* out, unsigned int frames) mutable {
uint64_t route = m_output.route();
if (previousRoute && route != previousRoute) for (auto* voice : m_voices) if (voice) voice->resetPlayback();
if (previousRoute && route != previousRoute) {
++m_outputGeneration;
for (auto* voice : m_voices) if (voice) voice->resetPlayback();
}
previousRoute = route;
m_renderErrors.fetch_add(renderOutput(m_engine, m_voices, static_cast<float*>(out), frames,
m_output.callbackTimeUs(), m_output.timingMeasured()));
m_output.callbackTimeUs(), m_output.timingMeasured(), this));
};
if (m_output.init(m_context, config, "sound-engine", 0, callback) && m_output.start()) {
m_actualOutput = resolveOutput(m_playbackId); return;
@@ -155,7 +159,7 @@ void SoundManager::recoverAudio() {
/* Последнее смешивание и ограничение: тот же PCM передаётся устройству и всем AEC этого выхода. */
unsigned SoundManager::renderOutput(ma_engine* engine, const std::array<VoiceAudioIo*, 2>& voices,
float* out, unsigned frames, int64_t firstTimeUs, bool measured) {
float* out, unsigned frames, int64_t firstTimeUs, bool measured, SoundManager* streams) {
unsigned errors = 0;
for (unsigned offset = 0; offset < frames;) {
unsigned count = std::min(frames - offset, 960u);
@@ -166,6 +170,19 @@ unsigned SoundManager::renderOutput(ma_engine* engine, const std::array<VoiceAud
}
for (auto* voice : voices) if (voice)
voice->mix(destination, count, firstTimeUs + (int64_t)offset * 1000000 / 48000, measured, frames);
if (streams) {
std::lock_guard<std::mutex> guard(streams->m_streamLock);
for (const auto& [id, mix] : streams->m_streams)
mix(destination, count, firstTimeUs + (int64_t)offset * 1000000 / 48000, streams->m_outputGeneration.load());
if (!streams->m_streams.empty() || streams->m_streamLimiter < 0.9999f) {
for (unsigned i = 0; i < count; ++i) {
float peak = std::max(std::abs(destination[i * 2]), std::abs(destination[i * 2 + 1]));
float target = peak > 1.0f ? 1.0f / peak : 1.0f;
streams->m_streamLimiter = std::min(target, streams->m_streamLimiter + (1.0f - streams->m_streamLimiter) / 4800.0f);
destination[i * 2] *= streams->m_streamLimiter; destination[i * 2 + 1] *= streams->m_streamLimiter;
}
}
}
for (unsigned i = 0; i < count * 2; ++i) {
float value = destination[i];
destination[i] = std::isfinite(value) ? std::clamp(value, -1.0f, 1.0f) : 0.0f;
@@ -177,6 +194,22 @@ unsigned SoundManager::renderOutput(ma_engine* engine, const std::array<VoiceAud
return errors;
}
bool SoundManager::attachStream(uint64_t id, StreamMix mix) {
if (!id || !mix || !m_initialized) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "audio stream attach invalid/uninitialized"); return false; }
std::lock_guard<std::mutex> guard(m_streamLock);
if (!m_streams.emplace(id, std::move(mix)).second) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "audio stream duplicate id=%llu", (unsigned long long)id); return false;
}
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "audio stream attached id=%llu count=%zu", (unsigned long long)id, m_streams.size());
return true;
}
void SoundManager::detachStream(uint64_t id) {
std::lock_guard<std::mutex> guard(m_streamLock);
if (m_streams.erase(id))
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "audio stream detached id=%llu count=%zu", (unsigned long long)id, m_streams.size());
}
/* Default и явный ID одного устройства получают один микшер. */
QString SoundManager::resolveOutput(const QString& requested) {
auto devices = enumeratePlaybackDevices();
+13 -1
View File
@@ -12,6 +12,7 @@
#include <map>
#include <array>
#include <cstdint>
#include <mutex>
#include <QTimer>
#include "audio_device.h"
@@ -56,6 +57,13 @@ public:
/* GUI: voice живёт до detachVoice; изменение списка выполняется при остановленном output. */
bool attachVoice(VoiceAudioIo* voice, const QString& output);
void detachVoice(VoiceAudioIo* voice);
/* GUI: callback добавляет PCM 48k stereo в общий float output; firstTimeUs — время первого sample.
* generation меняется после restart/смены устройства. detach ждёт текущий callback, не останавливая других.
* ID/владельцы отдельны от голосовых звонков; у каждого источника собственные media-часы. */
using StreamMix = std::function<void(float*, unsigned, int64_t, uint64_t)>;
bool attachStream(uint64_t id, StreamMix mix);
void detachStream(uint64_t id);
bool outputAvailable() const { return m_output.initialized() && !m_resetting; }
using PlaybackId = uint64_t;
/* GUI: независимая сессия. owner удерживает PCM до detach; без owner данные копируются. */
@@ -111,7 +119,11 @@ private:
bool openVoiceRoute(VoiceRoute& route);
void mergeVoiceRoutes();
static unsigned renderOutput(ma_engine* engine, const std::array<VoiceAudioIo*, 2>& voices,
float* out, unsigned frames, int64_t firstTimeUs, bool measured);
float* out, unsigned frames, int64_t firstTimeUs, bool measured, SoundManager* streams = nullptr);
std::mutex m_streamLock;
std::map<uint64_t, StreamMix> m_streams;
std::atomic<uint64_t> m_outputGeneration{0};
float m_streamLimiter = 1.0f; // Только output callback; быстрый attack, release 100ms.
std::atomic<unsigned> m_renderErrors{0};
std::array<VoiceAudioIo*, 2> m_voices{};
std::map<QString, std::unique_ptr<VoiceRoute>> m_voiceRoutes;