diff --git a/tools/chatgui/src/sound_manager.cpp b/tools/chatgui/src/sound_manager.cpp index c2eedcb8..16e180ae 100644 --- a/tools/chatgui/src/sound_manager.cpp +++ b/tools/chatgui/src/sound_manager.cpp @@ -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(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& 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::arraymix(destination, count, firstTimeUs + (int64_t)offset * 1000000 / 48000, measured, frames); + if (streams) { + std::lock_guard 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 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 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(); diff --git a/tools/chatgui/src/sound_manager.h b/tools/chatgui/src/sound_manager.h index 78a015f0..810343b3 100644 --- a/tools/chatgui/src/sound_manager.h +++ b/tools/chatgui/src/sound_manager.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #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; + 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& 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 m_streams; + std::atomic m_outputGeneration{0}; + float m_streamLimiter = 1.0f; // Только output callback; быстрый attack, release 100ms. std::atomic m_renderErrors{0}; std::array m_voices{}; std::map> m_voiceRoutes;