Раздели аудиопотоки и привяжи AEC к фактическому общему выходу
This commit is contained in:
1 parent
d7fb0c83f9
commit
bd082a26bf
30 files changed
+1839
-412
No files matched your search
@@ -1,11 +1,13 @@
|
||||
#include "miniaudio.h"
|
||||
|
||||
#include "sound_manager.h"
|
||||
#include "voice_audio_io.h"
|
||||
#include "../transport/audio_diagnostics.h"
|
||||
#include <QFile>
|
||||
#include <QDebug>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include "../../lib/debug_config.h"
|
||||
|
||||
SoundManager* SoundManager::instance() {
|
||||
@@ -20,6 +22,14 @@ SoundManager::~SoundManager() {
|
||||
|
||||
extern "C" int audio_context_failed(ma_context* context);
|
||||
|
||||
static uint64_t pulse_output_key(const QString& key) {
|
||||
ma_device_id id{};
|
||||
if (!SoundManager::deviceId(key, &id)) return 0;
|
||||
uint64_t hash = UINT64_C(14695981039346656037);
|
||||
for (const char* name = id.pulse; *name; ++name) { hash ^= (unsigned char)*name; hash *= UINT64_C(1099511628211); }
|
||||
return hash;
|
||||
}
|
||||
|
||||
SoundManager::SoundManager() {
|
||||
m_clock.start();
|
||||
m_recoveryTimer.setInterval(200);
|
||||
@@ -65,11 +75,15 @@ bool SoundManager::openContext() {
|
||||
m_backendSelected = true;
|
||||
m_contextRetryDelay = 250;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "SoundManager: context ready backend=%s", ma_get_backend_name(m_backend));
|
||||
m_resetting = false;
|
||||
emit contextRestored();
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoundManager::openOutput() {
|
||||
for (auto* voice : m_voices) if (voice) voice->resetPlayback();
|
||||
if (m_output.initialized() && m_output.start()) return;
|
||||
m_restoreSelections = true;
|
||||
ma_device_config config = ma_device_config_init(ma_device_type_playback);
|
||||
config.playback.format = ma_format_f32;
|
||||
config.playback.channels = 2;
|
||||
@@ -77,33 +91,213 @@ void SoundManager::openOutput() {
|
||||
ma_device_id id;
|
||||
bool selected = deviceId(m_playbackId, &id);
|
||||
if (selected) config.playback.pDeviceID = &id;
|
||||
auto callback = [this](const void*, void* out, unsigned int frames) {
|
||||
ma_engine_read_pcm_frames(m_engine, out, frames, nullptr);
|
||||
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();
|
||||
previousRoute = route;
|
||||
m_renderErrors.fetch_add(renderOutput(m_engine, m_voices, static_cast<float*>(out), frames, m_output.callbackTimeUs()));
|
||||
};
|
||||
if (m_output.init(m_context, config, "sound-engine", 0, callback) && m_output.start()) return;
|
||||
if (m_output.init(m_context, config, "sound-engine", 0, callback) && m_output.start()) {
|
||||
m_actualOutput = resolveOutput(m_playbackId); return;
|
||||
}
|
||||
if (!selected) return;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "SoundManager: selected output unavailable, trying default");
|
||||
config.playback.pDeviceID = nullptr;
|
||||
if (m_output.init(m_context, config, "sound-engine", 0, callback)) m_output.start();
|
||||
if (m_output.init(m_context, config, "sound-engine", 0, callback) && m_output.start()) m_actualOutput = resolveOutput({});
|
||||
}
|
||||
|
||||
void SoundManager::recoverAudio() {
|
||||
if (!m_initialized) return;
|
||||
unsigned errors = m_renderErrors.exchange(0);
|
||||
if (errors) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "audio: mixer read failures=%u; output replaced with silence", errors);
|
||||
for (auto it = m_pcmSessions.begin(); it != m_pcmSessions.end();) {
|
||||
if (it->second->transient && !isPcmPlaying(it->first)) it = m_pcmSessions.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
if (m_context && audio_context_failed(m_context)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "SoundManager: context failed, closing dependent devices");
|
||||
m_resetting = true;
|
||||
emit contextAboutToReset();
|
||||
m_output.close();
|
||||
for (auto& [key, route] : m_voiceRoutes) route->device.close();
|
||||
ma_context_uninit(m_context); delete m_context; m_context = nullptr;
|
||||
m_contextRetryAt = m_clock.elapsed() + 250;
|
||||
}
|
||||
if (!m_context && m_clock.elapsed() >= m_contextRetryAt) openContext();
|
||||
if (!m_context) return;
|
||||
if (m_output.needsRecovery()) { m_output.close(); m_output.retryLater(); }
|
||||
if (m_output.retryReady()) openOutput();
|
||||
if (m_output.needsRecovery()) {
|
||||
m_output.close();
|
||||
for (auto* voice : m_voices) if (voice) voice->resetPlayback();
|
||||
m_output.retryLater();
|
||||
}
|
||||
bool reopened = m_output.retryReady();
|
||||
if (reopened) openOutput();
|
||||
for (auto& [key, route] : m_voiceRoutes) {
|
||||
if (route->device.needsRecovery()) {
|
||||
route->device.close();
|
||||
for (auto* voice : route->voices) if (voice) voice->resetPlayback();
|
||||
route->device.retryLater();
|
||||
}
|
||||
if (route->device.retryReady()) openVoiceRoute(*route);
|
||||
}
|
||||
mergeVoiceRoutes();
|
||||
if (m_restoreSelections && m_output.initialized() && (m_backend != ma_backend_pulseaudio || m_output.route())) {
|
||||
m_restoreSelections = false;
|
||||
auto voices = m_voices;
|
||||
for (auto* voice : voices) if (voice) {
|
||||
QString wanted = m_voiceSelections.at(voice);
|
||||
bool same = m_backend == ma_backend_pulseaudio ? pulse_output_key(wanted) == m_output.route() : wanted == m_actualOutput;
|
||||
if (!wanted.isEmpty() && wanted == resolveOutput(wanted) && !same) attachVoice(voice, wanted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Последнее смешивание и ограничение: тот же PCM передаётся устройству и всем AEC этого выхода. */
|
||||
unsigned SoundManager::renderOutput(ma_engine* engine, const std::array<VoiceAudioIo*, 2>& voices,
|
||||
float* out, unsigned frames, int64_t firstTimeUs) {
|
||||
unsigned errors = 0;
|
||||
for (unsigned offset = 0; offset < frames;) {
|
||||
unsigned count = std::min(frames - offset, 960u);
|
||||
float* destination = out + offset * 2;
|
||||
std::fill_n(destination, count * 2, 0.0f);
|
||||
if (engine && ma_engine_read_pcm_frames(engine, destination, count, nullptr) != MA_SUCCESS) {
|
||||
++errors; std::fill_n(destination, count * 2, 0.0f);
|
||||
}
|
||||
for (auto* voice : voices) if (voice) voice->mix(destination, count);
|
||||
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;
|
||||
}
|
||||
for (auto* voice : voices) if (voice)
|
||||
voice->reference(destination, count, firstTimeUs + (int64_t)offset * 1000000 / 48000);
|
||||
offset += count;
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
/* Default и явный ID одного устройства получают один микшер. */
|
||||
QString SoundManager::resolveOutput(const QString& requested) {
|
||||
auto devices = enumeratePlaybackDevices();
|
||||
for (const auto& device : devices) if (device.id == requested) return requested;
|
||||
for (const auto& device : devices) if (device.isDefault) return device.id;
|
||||
return {};
|
||||
}
|
||||
|
||||
bool SoundManager::openVoiceRoute(VoiceRoute& route) {
|
||||
if (!m_context || m_resetting) return false;
|
||||
for (auto* voice : route.voices) if (voice) voice->resetPlayback();
|
||||
if (route.device.initialized() && route.device.start()) return true;
|
||||
ma_device_config config = ma_device_config_init(ma_device_type_playback);
|
||||
config.playback.format = ma_format_f32; config.playback.channels = 2; config.sampleRate = 48000;
|
||||
ma_device_id id;
|
||||
bool selected = deviceId(route.wanted, &id);
|
||||
if (selected) config.playback.pDeviceID = &id;
|
||||
auto callback = [this, &route, previousRoute = uint64_t(0)](const void*, void* out, unsigned frames) mutable {
|
||||
uint64_t physical = route.device.route();
|
||||
if (previousRoute && physical != previousRoute) for (auto* voice : route.voices) if (voice) voice->resetPlayback();
|
||||
previousRoute = physical;
|
||||
if (physical && physical == m_output.route()) {
|
||||
std::fill_n(static_cast<float*>(out), frames * 2, 0.0f); return; // GUI объединит совпавшие маршруты.
|
||||
}
|
||||
renderOutput(nullptr, route.voices, static_cast<float*>(out), frames, route.device.callbackTimeUs());
|
||||
};
|
||||
if (route.device.init(m_context, config, "voice-output", 0, callback) && route.device.start()) {
|
||||
route.actual = resolveOutput(route.wanted); return true;
|
||||
}
|
||||
if (!selected) return false;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: voice output unavailable, trying default");
|
||||
config.playback.pDeviceID = nullptr;
|
||||
if (route.device.init(m_context, config, "voice-output", 0, callback) && route.device.start()) {
|
||||
route.actual = resolveOutput({}); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SoundManager::attachVoice(VoiceAudioIo* voice, const QString& output) {
|
||||
detachVoice(voice);
|
||||
if (!voice || !m_initialized || !m_context || m_resetting) return false;
|
||||
size_t attached = std::count_if(m_voices.begin(), m_voices.end(), [](auto* v) { return v != nullptr; });
|
||||
for (const auto& [key, route] : m_voiceRoutes)
|
||||
attached += std::count_if(route->voices.begin(), route->voices.end(), [](auto* v) { return v != nullptr; });
|
||||
if (attached >= 2) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: only one call and one radio source are supported"); return false; }
|
||||
QString key = resolveOutput(output);
|
||||
QString root = m_actualOutput.isEmpty() ? resolveOutput(m_playbackId) : m_actualOutput;
|
||||
bool same = key == root;
|
||||
if (m_backend == ma_backend_pulseaudio && m_output.route()) same = pulse_output_key(key) == m_output.route();
|
||||
if (same) {
|
||||
m_output.pause();
|
||||
auto slot = std::find(m_voices.begin(), m_voices.end(), nullptr);
|
||||
if (slot == m_voices.end()) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: too many voice sources on output"); openOutput(); return false; }
|
||||
*slot = voice; voice->resetPlayback();
|
||||
openOutput();
|
||||
} else {
|
||||
auto& route = m_voiceRoutes[key];
|
||||
if (!route) { route = std::make_unique<VoiceRoute>(); route->wanted = key; }
|
||||
route->device.pause();
|
||||
auto slot = std::find(route->voices.begin(), route->voices.end(), nullptr);
|
||||
if (slot == route->voices.end()) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: too many voice sources on separate output"); openVoiceRoute(*route); return false; }
|
||||
*slot = voice; voice->resetPlayback();
|
||||
openVoiceRoute(*route);
|
||||
mergeVoiceRoutes();
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: voice attached to shared output default=%d", output.isEmpty());
|
||||
m_voiceSelections[voice] = output;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoundManager::detachVoice(VoiceAudioIo* voice) {
|
||||
if (!voice) return;
|
||||
m_voiceSelections.erase(voice);
|
||||
auto slot = std::find(m_voices.begin(), m_voices.end(), voice);
|
||||
if (slot != m_voices.end()) {
|
||||
m_output.pause(); *slot = nullptr;
|
||||
if (m_initialized && m_context && !m_resetting) openOutput();
|
||||
}
|
||||
for (auto it = m_voiceRoutes.begin(); it != m_voiceRoutes.end();) {
|
||||
auto& route = *it->second;
|
||||
auto entry = std::find(route.voices.begin(), route.voices.end(), voice);
|
||||
if (entry == route.voices.end()) { ++it; continue; }
|
||||
route.device.pause(); *entry = nullptr;
|
||||
if (std::all_of(route.voices.begin(), route.voices.end(), [](auto* v) { return !v; })) it = m_voiceRoutes.erase(it);
|
||||
else { if (!m_resetting) openVoiceRoute(route); ++it; }
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback на уже используемый выход не оставляет два независимых референса одного динамика. */
|
||||
void SoundManager::mergeVoiceRoutes() {
|
||||
if (m_resetting || !m_output.initialized()) return;
|
||||
for (auto first = m_voiceRoutes.begin(); first != m_voiceRoutes.end(); ++first) {
|
||||
auto& target = *first->second;
|
||||
for (auto next = std::next(first); next != m_voiceRoutes.end();) {
|
||||
auto& source = *next->second;
|
||||
uint64_t physical = target.device.route();
|
||||
if (!physical || physical != source.device.route()) { ++next; continue; }
|
||||
target.device.pause(); source.device.close();
|
||||
for (auto* voice : source.voices) if (voice) {
|
||||
auto slot = std::find(target.voices.begin(), target.voices.end(), nullptr);
|
||||
if (slot == target.voices.end()) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: voice route merge capacity exceeded"); return; }
|
||||
*slot = voice;
|
||||
}
|
||||
next = m_voiceRoutes.erase(next);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: merged voice streams on physical route=%016llx", (unsigned long long)physical);
|
||||
openVoiceRoute(target);
|
||||
}
|
||||
}
|
||||
for (auto it = m_voiceRoutes.begin(); it != m_voiceRoutes.end();) {
|
||||
auto& route = *it->second;
|
||||
bool same = m_backend == ma_backend_pulseaudio ? m_output.route() && m_output.route() == route.device.route()
|
||||
: route.actual == m_actualOutput;
|
||||
if (!route.device.initialized() || !same) { ++it; continue; }
|
||||
m_output.pause(); route.device.close();
|
||||
for (auto* voice : route.voices) if (voice) {
|
||||
auto slot = std::find(m_voices.begin(), m_voices.end(), nullptr);
|
||||
if (slot == m_voices.end()) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: output merge capacity exceeded"); break; }
|
||||
*slot = voice; voice->resetPlayback();
|
||||
}
|
||||
it = m_voiceRoutes.erase(it);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: merged voice fallback with shared output");
|
||||
openOutput();
|
||||
}
|
||||
}
|
||||
|
||||
void SoundManager::setDevices(const QString& output, const QString& input) {
|
||||
@@ -111,7 +305,7 @@ void SoundManager::setDevices(const QString& output, const QString& input) {
|
||||
bool outputChanged = output != m_playbackId;
|
||||
m_playbackId = output; m_captureId = input;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: device selection changed output_default=%d input_default=%d", output.isEmpty(), input.isEmpty());
|
||||
if (outputChanged) { m_output.close(); m_output.retryNow(); }
|
||||
if (outputChanged) { m_output.close(); m_actualOutput.clear(); m_output.retryNow(); }
|
||||
emit devicesChanged();
|
||||
recoverAudio();
|
||||
}
|
||||
@@ -137,6 +331,7 @@ bool SoundManager::createPcmNode(PcmSession& session, bool playing) {
|
||||
return false;
|
||||
}
|
||||
session.sound = sound;
|
||||
session.detached = false;
|
||||
ma_sound_set_volume(sound, m_volume);
|
||||
if (playing && ma_sound_start(sound) != MA_SUCCESS) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: node start failed");
|
||||
@@ -150,14 +345,11 @@ void SoundManager::shutdown() {
|
||||
if (!m_initialized) return;
|
||||
m_recoveryTimer.stop();
|
||||
m_initialized = false;
|
||||
m_resetting = true;
|
||||
emit contextAboutToReset();
|
||||
m_output.close();
|
||||
m_output.close(); m_voiceRoutes.clear(); m_voices = {}; m_voiceSelections.clear();
|
||||
m_pcmSessions.clear();
|
||||
|
||||
for (auto it = m_sounds.begin(); it != m_sounds.end(); ++it) {
|
||||
if (it->sound) { ma_sound_uninit(it->sound); delete it->sound; }
|
||||
if (it->decoder) { ma_decoder_uninit(it->decoder); delete it->decoder; }
|
||||
}
|
||||
m_sounds.clear();
|
||||
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: before ma_engine_uninit engine=%p", (void*)m_engine);
|
||||
@@ -197,47 +389,49 @@ SoundManager::EventSettings SoundManager::eventSettings(const QString& name) con
|
||||
|
||||
bool SoundManager::loadSound(const QString& name, const QString& filePath) {
|
||||
if (!m_initialized) {
|
||||
qWarning("SoundManager: not initialized, cannot load %s", qPrintable(name));
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: not initialized, cannot load '%s'", qPrintable(name));
|
||||
return false;
|
||||
}
|
||||
if (m_sounds.contains(name)) {
|
||||
qWarning("SoundManager: sound '%s' already loaded", qPrintable(name));
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "SoundManager: sound '%s' already loaded", qPrintable(name));
|
||||
return false;
|
||||
}
|
||||
|
||||
QFile f(filePath);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
qWarning("SoundManager: cannot open %s", qPrintable(filePath));
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: cannot open '%s'", qPrintable(filePath));
|
||||
return false;
|
||||
}
|
||||
|
||||
LoadedSound ls;
|
||||
ls.mp3Data = f.readAll();
|
||||
QByteArray encoded = f.readAll();
|
||||
f.close();
|
||||
|
||||
ls.decoder = new ma_decoder;
|
||||
ma_decoder_config decConfig = ma_decoder_config_init_default();
|
||||
ma_result result = ma_decoder_init_memory(ls.mp3Data.constData(), ls.mp3Data.size(),
|
||||
&decConfig, ls.decoder);
|
||||
if (result != MA_SUCCESS) {
|
||||
qWarning("SoundManager: ma_decoder_init_memory failed for %s (%d)", qPrintable(name), result);
|
||||
delete ls.decoder;
|
||||
/* Декодируем короткие сигналы при загрузке, до playback: hardware callback читает только PCM. */
|
||||
ma_decoder_config decConfig = ma_decoder_config_init(ma_format_s16, 2, 48000);
|
||||
ma_uint64 frames = 0; void* decoded = nullptr;
|
||||
ma_result result = ma_decode_memory(encoded.constData(), encoded.size(), &decConfig, &frames, &decoded);
|
||||
if (result != MA_SUCCESS || !frames) {
|
||||
ma_free(decoded, &decConfig.allocationCallbacks);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: decode '%s' failed result=%d frames=%llu",
|
||||
qPrintable(name), result, (unsigned long long)frames);
|
||||
return false;
|
||||
}
|
||||
|
||||
ls.sound = new ma_sound;
|
||||
result = ma_sound_init_from_data_source(m_engine, (ma_data_source*)&ls.decoder->ds,
|
||||
0, NULL, ls.sound);
|
||||
LoadedSound ls;
|
||||
ls.pcm = std::make_shared<PcmSession>();
|
||||
ls.pcm->frames = frames; ls.pcm->rate = 48000;
|
||||
ma_audio_buffer_config bufferConfig = ma_audio_buffer_config_init(ma_format_s16, 2, frames, decoded, nullptr);
|
||||
bufferConfig.sampleRate = 48000;
|
||||
ls.pcm->buffer = new ma_audio_buffer{};
|
||||
result = ma_audio_buffer_init_copy(&bufferConfig, ls.pcm->buffer);
|
||||
ma_free(decoded, &decConfig.allocationCallbacks);
|
||||
if (result != MA_SUCCESS) {
|
||||
qWarning("SoundManager: sound init failed for %s (%d)", qPrintable(name), result);
|
||||
ma_decoder_uninit(ls.decoder); delete ls.decoder;
|
||||
delete ls.sound;
|
||||
delete ls.pcm->buffer; ls.pcm->buffer = nullptr;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: signal PCM init failed result=%d", result);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!createPcmNode(*ls.pcm, false)) return false;
|
||||
m_sounds.insert(name, ls);
|
||||
qDebug("SoundManager: loaded '%s' from %s (%lld bytes)",
|
||||
qPrintable(name), qPrintable(filePath), (long long)ls.mp3Data.size());
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "SoundManager: predecoded '%s' frames=%llu rate=48000 ch=2",
|
||||
qPrintable(name), (unsigned long long)frames);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -254,12 +448,15 @@ void SoundManager::play(const QString& name) {
|
||||
return;
|
||||
}
|
||||
|
||||
ma_sound* s = it->sound;
|
||||
auto& pcm = *it->pcm;
|
||||
if (pcm.sound) { ma_sound_uninit(pcm.sound); delete pcm.sound; pcm.sound = nullptr; }
|
||||
ma_audio_buffer_seek_to_pcm_frame(pcm.buffer, 0);
|
||||
if (!createPcmNode(pcm, false)) return;
|
||||
ma_sound* s = pcm.sound;
|
||||
float effectiveVolume = m_volume * es.volume;
|
||||
ma_sound_set_volume(s, effectiveVolume);
|
||||
ma_sound_stop(s);
|
||||
ma_sound_seek_to_pcm_frame(s, 0);
|
||||
ma_sound_start(s);
|
||||
if (ma_sound_start(s) != MA_SUCCESS) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: signal start failed '%s'", qPrintable(name));
|
||||
}
|
||||
|
||||
void SoundManager::playLoop(const QString& name) {
|
||||
@@ -278,12 +475,15 @@ void SoundManager::playLoop(const QString& name) {
|
||||
return;
|
||||
}
|
||||
|
||||
ma_sound* s = it->sound;
|
||||
auto& pcm = *it->pcm;
|
||||
if (pcm.sound) { ma_sound_uninit(pcm.sound); delete pcm.sound; pcm.sound = nullptr; }
|
||||
ma_audio_buffer_seek_to_pcm_frame(pcm.buffer, 0);
|
||||
if (!createPcmNode(pcm, false)) return;
|
||||
ma_sound* s = pcm.sound;
|
||||
ma_sound_stop(s);
|
||||
ma_sound_seek_to_pcm_frame(s, 0);
|
||||
ma_sound_set_volume(s, m_volume * es.volume);
|
||||
ma_sound_set_looping(s, MA_TRUE);
|
||||
ma_sound_start(s);
|
||||
if (ma_sound_start(s) != MA_SUCCESS) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: loop start failed '%s'", qPrintable(name)); return; }
|
||||
m_loopName = name;
|
||||
}
|
||||
|
||||
@@ -292,7 +492,8 @@ void SoundManager::stopLoop() {
|
||||
auto it = m_sounds.find(m_loopName);
|
||||
m_loopName.clear();
|
||||
if (it == m_sounds.end()) return;
|
||||
ma_sound* s = it->sound;
|
||||
ma_sound* s = it->pcm->sound;
|
||||
if (!s) return;
|
||||
ma_sound_stop(s);
|
||||
ma_sound_set_looping(s, MA_FALSE);
|
||||
ma_sound_seek_to_pcm_frame(s, 0);
|
||||
@@ -336,13 +537,26 @@ SoundManager::PlaybackId SoundManager::playRawPcm(const int16_t* pcm, int frameC
|
||||
|
||||
void SoundManager::pausePcm(PlaybackId id) {
|
||||
auto* session = pcmSession(id);
|
||||
if (session && session->sound) ma_sound_stop(session->sound);
|
||||
if (!session || !session->sound) return;
|
||||
ma_result result = ma_sound_stop(session->sound);
|
||||
/* Stop меняет флаг; detach также ждёт уже начатое чтение и фиксирует cursor. */
|
||||
if (result == MA_SUCCESS && !session->detached) {
|
||||
result = ma_node_detach_output_bus(session->sound, 0);
|
||||
if (result == MA_SUCCESS) session->detached = true;
|
||||
}
|
||||
if (result != MA_SUCCESS) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: pause failed id=%llu result=%d", (unsigned long long)id, result);
|
||||
}
|
||||
|
||||
void SoundManager::resumePcm(PlaybackId id) {
|
||||
auto* session = pcmSession(id);
|
||||
if (session && session->sound && ma_sound_start(session->sound) != MA_SUCCESS)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: resume failed id=%llu", (unsigned long long)id);
|
||||
if (!session || !session->sound) return;
|
||||
ma_result result = MA_SUCCESS;
|
||||
if (session->detached) {
|
||||
result = ma_node_attach_output_bus(session->sound, 0, ma_engine_get_endpoint(m_engine), 0);
|
||||
if (result == MA_SUCCESS) session->detached = false;
|
||||
}
|
||||
if (result == MA_SUCCESS) result = ma_sound_start(session->sound);
|
||||
if (result != MA_SUCCESS) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: resume failed id=%llu result=%d", (unsigned long long)id, result);
|
||||
}
|
||||
|
||||
void SoundManager::stopPcm(PlaybackId id) {
|
||||
|
||||
Reference in new issue
Block a user