diff --git a/tools/chatgui/src/audiodevicesettingspage.cpp b/tools/chatgui/src/audiodevicesettingspage.cpp index 92afa60b..abe29758 100644 --- a/tools/chatgui/src/audiodevicesettingspage.cpp +++ b/tools/chatgui/src/audiodevicesettingspage.cpp @@ -411,7 +411,7 @@ void AudioDeviceSettingsPage::onTestSpeaker() { std::vector pcm(samples); for (int i = 0; i < samples; i++) pcm[i] = (int16_t)(12000.0 * sin(2.0 * M_PI * 440.0 * i / sampleRate)); - SoundManager::instance()->playRawPcm(pcm.data(), samples, sampleRate, 1); + SoundManager::instance()->playRawPcm(pcm.data(), samples, sampleRate, 1, {}, true); } void AudioDeviceSettingsPage::buildCompressorUI(QVBoxLayout* layout) { diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 4cafbf81..8e9113c0 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -947,7 +947,7 @@ void MainWindow::autoPlayCheckNewMessage(const QString& channelId) { qPrintable(channelId), (long long)msg.id, qPrintable(fullPath)); if (!m_autoPlayTimer->isActive()) m_autoPlayTimer->start(200); - if (!SoundManager::instance()->isCurrentPcmPlaying()) + if (!VoicePlayer::isPlaying()) autoPlayTimerTick(); return; } @@ -972,7 +972,7 @@ void MainWindow::autoPlayCheckNewMessage(const QString& channelId) { } void MainWindow::autoPlayTimerTick() { - if (VoicePlayer::isPreparing() || SoundManager::instance()->isCurrentPcmPlaying()) + if (VoicePlayer::isPreparing() || VoicePlayer::isPlaying()) return; while (!m_autoPlayQueue.isEmpty()) { @@ -1000,8 +1000,8 @@ void MainWindow::autoPlayTimerTick() { DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "auto-play: playing ch=%s msg=%lld file=%s dur=%.1fs", qPrintable(chId), (long long)msgId, qPrintable(fullPath), (double)dur); MessageDelegate::stopAllAutoPlay(); - VoicePlayer::playOpusFile(fullPath, this, [fullPath](float actualDur) { - MessageDelegate::startAutoPlay(fullPath, actualDur); + VoicePlayer::playOpusFile(fullPath, this, [fullPath](float actualDur, uint64_t playbackId) { + MessageDelegate::startAutoPlay(fullPath, actualDur, playbackId); }); return; } @@ -1033,6 +1033,6 @@ void MainWindow::onAttachmentDownloadedForAutoPlay(const QString& chId, int64_t return; if (!m_autoPlayTimer->isActive()) m_autoPlayTimer->start(200); - if (!SoundManager::instance()->isCurrentPcmPlaying()) + if (!VoicePlayer::isPlaying()) autoPlayTimerTick(); } diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index 2c60fde1..42eff6a8 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -29,6 +29,7 @@ MessageDelegate::MessageDelegate(QObject *parent) : QStyledItemDelegate(parent) {} struct VoicePlayState { + uint64_t playbackId = 0; bool loading = false; bool playing = false; bool paused = false; @@ -56,7 +57,7 @@ static void ensureVoiceTimer() { continue; } if (it->paused) { anyActive = true; continue; } - if (!sm->isCurrentPcmPlaying()) { + if (!sm->isPcmPlaying(it->playbackId)) { it->playing = false; it->paused = false; anyActive = true; @@ -177,16 +178,16 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, /* Start playback then seek */ st.loading = true; QPersistentModelIndex persistent(index); - VoicePlayer::playOpusFile(filePath, this, [filePath, clickFrac, persistent](float actualDur) { + VoicePlayer::playOpusFile(filePath, this, [filePath, clickFrac, persistent](float actualDur, uint64_t playbackId) { s_voiceStates[filePath].loading = false; if (actualDur <= 0) return; - MessageDelegate::startAutoPlay(filePath, actualDur); + MessageDelegate::startAutoPlay(filePath, actualDur, playbackId); float position = clickFrac * actualDur; auto& state = s_voiceStates[filePath]; state.startMs -= (qint64)(position * 1000); state.pausedPosSec = position; state.displayFrac = clickFrac; state.displayElapsed = position; - int rate = SoundManager::instance()->currentPcmSampleRate(); - if (rate > 0) SoundManager::instance()->seekCurrentPcm((unsigned long long)(position * rate)); + int rate = SoundManager::instance()->pcmSampleRate(playbackId); + if (rate > 0) SoundManager::instance()->seekPcm(playbackId, (unsigned long long)(position * rate)); if (persistent.isValid()) const_cast(persistent.model())->setData(persistent, 1, MsgVoicePlayStateRole); }); s_voiceTimer->start(); @@ -199,9 +200,9 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, { st.playing = true; s_voiceTimer->start(); } st.displayFrac = (targetSec > 0 && dur > 0) ? (targetSec / dur) : 0; st.displayElapsed = targetSec; - int sr = SoundManager::instance()->currentPcmSampleRate(); + int sr = SoundManager::instance()->pcmSampleRate(st.playbackId); if (sr > 0) - SoundManager::instance()->seekCurrentPcm((unsigned long long)(targetSec * sr)); + SoundManager::instance()->seekPcm(st.playbackId, (unsigned long long)(targetSec * sr)); } return true; } @@ -219,7 +220,7 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, st.pausedPosSec = (now - st.startMs) / 1000.0f; st.playing = false; st.paused = true; - SoundManager::instance()->pauseCurrentPcm(); + SoundManager::instance()->pausePcm(st.playbackId); model->setData(index, 2, MsgVoicePlayStateRole); return true; } @@ -232,7 +233,7 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, st.startMs = now - (qint64)(st.pausedPosSec * 1000.0); st.displayFrac = (dur > 0) ? (st.pausedPosSec / dur) : 0; st.displayElapsed = st.pausedPosSec; - SoundManager::instance()->resumeCurrentPcm(); + SoundManager::instance()->resumePcm(st.playbackId); s_voiceTimer->start(); model->setData(index, 1, MsgVoicePlayStateRole); return true; @@ -243,15 +244,15 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, for (auto it = s_voiceStates.begin(); it != s_voiceStates.end(); ++it) { it->playing = false; it->paused = false; } - SoundManager::instance()->stopCurrentPcm(); + VoicePlayer::stop(); } st.loading = true; QPersistentModelIndex persistent(index); - VoicePlayer::playOpusFile(filePath, this, [filePath, persistent](float actualDur) { + VoicePlayer::playOpusFile(filePath, this, [filePath, persistent](float actualDur, uint64_t playbackId) { s_voiceStates[filePath].loading = false; if (actualDur <= 0) return; - MessageDelegate::startAutoPlay(filePath, actualDur); + MessageDelegate::startAutoPlay(filePath, actualDur, playbackId); if (persistent.isValid()) const_cast(persistent.model())->setData(persistent, 1, MsgVoicePlayStateRole); }); s_voiceTimer->start(); @@ -913,9 +914,9 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio elapsed = st->pausedPosSec; } else if (isPlaying) { SoundManager* sm = SoundManager::instance(); - if (sm->isCurrentPcmPlaying()) { - int sr = sm->currentPcmSampleRate(); - if (sr > 0) elapsed = (float)((double)sm->currentPcmCursor() / sr); + if (sm->isPcmPlaying(st->playbackId)) { + int sr = sm->pcmSampleRate(st->playbackId); + if (sr > 0) elapsed = (float)((double)sm->pcmCursor(st->playbackId) / sr); } if (elapsed <= 0) { qint64 now = QDateTime::currentMSecsSinceEpoch(); @@ -1504,10 +1505,11 @@ void MessageDelegate::drawAvatar(QPainter *p, const Layout &L, p->restore(); } -void MessageDelegate::startAutoPlay(const QString& filePath, float actualDurationSec) { +void MessageDelegate::startAutoPlay(const QString& filePath, float actualDurationSec, uint64_t playbackId) { if (actualDurationSec <= 0) return; ensureVoiceTimer(); VoicePlayState& st = s_voiceStates[filePath]; + st.playbackId = playbackId; st.playing = true; st.paused = false; st.durationSec = (actualDurationSec > 0) ? actualDurationSec : 1.0f; @@ -1520,7 +1522,7 @@ void MessageDelegate::startAutoPlay(const QString& filePath, float actualDuratio } void MessageDelegate::stopAllAutoPlay() { - VoicePlayer::cancelPending(); + VoicePlayer::stop(); for (auto it = s_voiceStates.begin(); it != s_voiceStates.end(); ++it) { it->playing = false; it->paused = false; it->loading = false; } diff --git a/tools/chatgui/src/messagedelegate.h b/tools/chatgui/src/messagedelegate.h index d758f128..db8c8981 100644 --- a/tools/chatgui/src/messagedelegate.h +++ b/tools/chatgui/src/messagedelegate.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -55,7 +56,7 @@ public: const QStyleOptionViewItem &option, const QModelIndex &index) override; - static void startAutoPlay(const QString& filePath, float actualDurationSec); + static void startAutoPlay(const QString& filePath, float actualDurationSec, uint64_t playbackId); static void stopAllAutoPlay(); void setVideoOpenCallback(std::function cb) { m_videoOpenCb = std::move(cb); } diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index a5e3ab54..f6d8e41a 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -960,7 +960,7 @@ void MessageList::onAttachmentDownloaded(const QString& chId, int64_t msgId) { if (!fp.isEmpty() && QFile::exists(fp)) { QTimer::singleShot(500, this, [this, fp]() { MessageDelegate::stopAllAutoPlay(); - VoicePlayer::playOpusFile(fp, this, [fp](float actualDur) { MessageDelegate::startAutoPlay(fp, actualDur); }); + VoicePlayer::playOpusFile(fp, this, [fp](float actualDur, uint64_t playbackId) { MessageDelegate::startAutoPlay(fp, actualDur, playbackId); }); }); } } diff --git a/tools/chatgui/src/sound_manager.cpp b/tools/chatgui/src/sound_manager.cpp index 7f495f68..29884c13 100644 --- a/tools/chatgui/src/sound_manager.cpp +++ b/tools/chatgui/src/sound_manager.cpp @@ -89,6 +89,10 @@ void SoundManager::openOutput() { void SoundManager::recoverAudio() { if (!m_initialized) return; + 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"); emit contextAboutToReset(); @@ -112,14 +116,34 @@ void SoundManager::setDevices(const QString& output, const QString& input) { recoverAudio(); } -void SoundManager::clearCurrentPcm() { - if (m_currentPcmSound) { ma_sound_uninit(m_currentPcmSound); delete m_currentPcmSound; m_currentPcmSound = nullptr; } - if (m_currentPcmBuffer) { - ma_audio_buffer* buf = (ma_audio_buffer*)m_currentPcmBuffer; - ma_audio_buffer_uninit(buf); delete buf; m_currentPcmBuffer = nullptr; +/* Detach ждёт читателя графа; владелец PCM освобождается последним. */ +SoundManager::PcmSession::~PcmSession() { + if (sound) { ma_sound_uninit(sound); delete sound; } + if (buffer) { ma_audio_buffer_uninit(buffer); delete buffer; } +} + +SoundManager::PcmSession* SoundManager::pcmSession(PlaybackId id) const { + auto it = m_pcmSessions.find(id); + return it == m_pcmSessions.end() ? nullptr : it->second.get(); +} + +/* Новый узел имеет пустой read/resampler cache; общий PCM и handle сохраняются. */ +bool SoundManager::createPcmNode(PcmSession& session, bool playing) { + auto* sound = new ma_sound{}; + ma_result result = ma_sound_init_from_data_source(m_engine, (ma_data_source*)session.buffer, 0, nullptr, sound); + if (result != MA_SUCCESS) { + delete sound; + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: node init failed result=%d", result); + return false; } - m_currentPcmSampleRate = 0; - m_currentPcmOwner.reset(); + session.sound = sound; + ma_sound_set_volume(sound, m_volume); + if (playing && ma_sound_start(sound) != MA_SUCCESS) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: node start failed"); + ma_sound_uninit(sound); delete sound; session.sound = nullptr; + return false; + } + return true; } void SoundManager::shutdown() { @@ -128,7 +152,7 @@ void SoundManager::shutdown() { m_initialized = false; emit contextAboutToReset(); m_output.close(); - clearCurrentPcm(); + 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; } @@ -278,75 +302,84 @@ ma_context* SoundManager::context() { return m_context; } -void SoundManager::playRawPcm(const int16_t* pcm, int sampleCount, int sampleRate, int channels, - std::shared_ptr> owner) { - if (!m_engine || !m_initialized) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "SoundManager::playRawPcm: not initialized (engine=%p init=%d)", - (void*)m_engine, (int)m_initialized); - return; +SoundManager::PlaybackId SoundManager::playRawPcm(const int16_t* pcm, int frameCount, int sampleRate, int channels, + std::shared_ptr> owner, bool transient) { + if (!m_engine || !m_initialized || !pcm || frameCount <= 0 || sampleRate <= 0 || channels < 1 || channels > 2) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: invalid PCM frames=%d rate=%d ch=%d ready=%d", + frameCount, sampleRate, channels, m_initialized); + return 0; } - clearCurrentPcm(); - m_currentPcmOwner = std::move(owner); - m_currentPcmSampleRate = sampleRate; - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "SoundManager::playRawPcm: %d samples %dHz %dch volume=%.2f", - sampleCount, sampleRate, channels, m_volume); - - ma_audio_buffer_config bufCfg = ma_audio_buffer_config_init( - ma_format_s16, (ma_uint32)channels, (ma_uint64)sampleCount, pcm, NULL); - ma_audio_buffer* buf = new ma_audio_buffer; - m_currentPcmBuffer = buf; - ma_result r = m_currentPcmOwner ? ma_audio_buffer_init(&bufCfg, buf) : ma_audio_buffer_init_copy(&bufCfg, buf); - if (r != MA_SUCCESS) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "SoundManager: audio_buffer_init_copy failed (%d)", r); - delete buf; m_currentPcmBuffer = nullptr; - return; + if (owner && (pcm != owner->data() || (size_t)frameCount > owner->size() / channels)) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: PCM owner does not contain requested frames"); + return 0; } - - m_currentPcmSound = new ma_sound; - r = ma_sound_init_from_data_source(m_engine, (ma_data_source*)buf, 0, nullptr, m_currentPcmSound); - if (r != MA_SUCCESS) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "SoundManager: PCM sound init failed (%d)", r); - ma_audio_buffer_uninit(buf); delete buf; m_currentPcmBuffer = nullptr; - delete m_currentPcmSound; m_currentPcmSound = nullptr; - return; + auto session = std::make_unique(); + session->owner = std::move(owner); + session->frames = frameCount; session->rate = sampleRate; session->transient = transient; + ma_audio_buffer_config config = ma_audio_buffer_config_init(ma_format_s16, channels, frameCount, pcm, nullptr); + config.sampleRate = sampleRate; + auto* buffer = new ma_audio_buffer{}; + ma_result result = session->owner ? ma_audio_buffer_init(&config, buffer) : ma_audio_buffer_init_copy(&config, buffer); + if (result != MA_SUCCESS) { + delete buffer; + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: PCM buffer init failed result=%d", result); + return 0; } + session->buffer = buffer; + if (!createPcmNode(*session, true)) return 0; + PlaybackId id = ++m_nextPlaybackId; + m_pcmSessions.emplace(id, std::move(session)); + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "playback: started id=%llu frames=%d rate=%d ch=%d transient=%d", + (unsigned long long)id, frameCount, sampleRate, channels, transient); + return id; +} - ma_sound_set_volume(m_currentPcmSound, m_volume); - r = ma_sound_start(m_currentPcmSound); - ma_uint64 cursor = 0; - ma_sound_get_cursor_in_pcm_frames(m_currentPcmSound, &cursor); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, - "SoundManager::playRawPcm: started rc=%d is_playing=%d cursor=%llu/%d", - (int)r, (int)ma_sound_is_playing(m_currentPcmSound), - (unsigned long long)cursor, sampleCount); +void SoundManager::pausePcm(PlaybackId id) { + auto* session = pcmSession(id); + if (session && session->sound) ma_sound_stop(session->sound); } -void SoundManager::pauseCurrentPcm() { - if (m_currentPcmSound) ma_sound_stop(m_currentPcmSound); +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); } -void SoundManager::resumeCurrentPcm() { - if (m_currentPcmSound) ma_sound_start(m_currentPcmSound); +void SoundManager::stopPcm(PlaybackId id) { + if (m_pcmSessions.erase(id)) DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "playback: stopped id=%llu", (unsigned long long)id); } -void SoundManager::stopCurrentPcm() { - clearCurrentPcm(); +uint64_t SoundManager::pcmCursor(PlaybackId id) const { + auto* session = pcmSession(id); + if (!session || !session->sound) return 0; + uint64_t rendered = ma_sound_get_time_in_pcm_frames(session->sound); + return std::min(session->frames, session->origin + rendered * (uint64_t)session->rate / 48000); } -unsigned long long SoundManager::currentPcmCursor() const { - if (!m_currentPcmSound) return 0; - ma_uint64 cursor = 0; - ma_sound_get_cursor_in_pcm_frames(m_currentPcmSound, &cursor); - return (unsigned long long)cursor; +bool SoundManager::isPcmPlaying(PlaybackId id) const { + auto* session = pcmSession(id); + return session && session->sound && ma_sound_is_playing(session->sound); } -bool SoundManager::isCurrentPcmPlaying() const { - if (!m_currentPcmSound) return false; - return ma_sound_is_playing(m_currentPcmSound) != MA_FALSE; +int SoundManager::pcmSampleRate(PlaybackId id) const { + auto* session = pcmSession(id); + return session ? session->rate : 0; } -void SoundManager::seekCurrentPcm(unsigned long long pcmFrame) { - if (m_currentPcmSound) ma_sound_seek_to_pcm_frame(m_currentPcmSound, pcmFrame); +bool SoundManager::seekPcm(PlaybackId id, uint64_t frame) { + auto* session = pcmSession(id); + if (!session || !session->sound) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "playback: seek missing id=%llu", (unsigned long long)id); return false; } + bool playing = ma_sound_is_playing(session->sound); + ma_sound_uninit(session->sound); delete session->sound; session->sound = nullptr; + session->origin = std::min(frame, session->frames); + ma_result result = ma_audio_buffer_seek_to_pcm_frame(session->buffer, session->origin); + if (result != MA_SUCCESS || !createPcmNode(*session, playing)) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: seek failed id=%llu frame=%llu result=%d", + (unsigned long long)id, (unsigned long long)frame, result); + return false; + } + DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "playback: seek id=%llu frame=%llu", (unsigned long long)id, (unsigned long long)session->origin); + return true; } static QString decodeDeviceName(const char* name) { diff --git a/tools/chatgui/src/sound_manager.h b/tools/chatgui/src/sound_manager.h index 56e724bc..9536d241 100644 --- a/tools/chatgui/src/sound_manager.h +++ b/tools/chatgui/src/sound_manager.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include "audio_device.h" @@ -47,16 +48,17 @@ public: QString playbackDevice() const { return m_playbackId; } QString captureDevice() const { return m_captureId; } - /* owner удерживает готовый PCM без копии в GUI; без owner короткие буферы копируются. */ - void playRawPcm(const int16_t* pcm, int sampleCount, int sampleRate, int channels, - std::shared_ptr> owner = {}); - void pauseCurrentPcm(); - void resumeCurrentPcm(); - void stopCurrentPcm(); - void seekCurrentPcm(unsigned long long pcmFrame); - unsigned long long currentPcmCursor() const; - bool isCurrentPcmPlaying() const; - int currentPcmSampleRate() const { return m_currentPcmSampleRate; } + using PlaybackId = uint64_t; + /* GUI: независимая сессия. owner удерживает PCM до detach; без owner данные копируются. */ + PlaybackId playRawPcm(const int16_t* pcm, int frameCount, int sampleRate, int channels, + std::shared_ptr> owner = {}, bool transient = false); + void pausePcm(PlaybackId id); + void resumePcm(PlaybackId id); + void stopPcm(PlaybackId id); + bool seekPcm(PlaybackId id, uint64_t frame); + uint64_t pcmCursor(PlaybackId id) const; + bool isPcmPlaying(PlaybackId id) const; + int pcmSampleRate(PlaybackId id) const; struct DeviceInfo { QString name; @@ -75,7 +77,17 @@ private: SoundManager(); ~SoundManager(); - void clearCurrentPcm(); + struct PcmSession { + ma_sound* sound = nullptr; + ma_audio_buffer* buffer = nullptr; + std::shared_ptr> owner; + uint64_t frames = 0, origin = 0; + int rate = 0; + bool transient = false; + ~PcmSession(); + }; + PcmSession* pcmSession(PlaybackId id) const; + bool createPcmNode(PcmSession& session, bool playing); void recoverAudio(); bool openContext(); void openOutput(); @@ -96,10 +108,8 @@ private: QString m_playbackId, m_captureId; ma_backend m_backend = ma_backend_null; bool m_backendSelected = false; - ma_sound* m_currentPcmSound = nullptr; - void* m_currentPcmBuffer = nullptr; - int m_currentPcmSampleRate = 0; - std::shared_ptr> m_currentPcmOwner; + std::map> m_pcmSessions; + PlaybackId m_nextPlaybackId = 0; bool m_initialized = false; bool m_enabled = true; float m_volume = 0.8f; diff --git a/tools/chatgui/src/videoplayer_engine.cpp b/tools/chatgui/src/videoplayer_engine.cpp index d96d23a9..3ee68865 100644 --- a/tools/chatgui/src/videoplayer_engine.cpp +++ b/tools/chatgui/src/videoplayer_engine.cpp @@ -295,11 +295,22 @@ void VideoPlayerEngine::open(const QString& path) { m_worker->moveToThread(m_thread); connect(m_thread, &QThread::started, m_worker, &Worker::run); - connect(m_worker, &Worker::opened, this, &VideoPlayerEngine::onWorkerOpened); - connect(m_worker, &Worker::audioDecoded, this, &VideoPlayerEngine::onAudioDecoded); - connect(m_worker, &Worker::frameDecoded, this, &VideoPlayerEngine::onFrameDecoded); - connect(m_worker, &Worker::error, this, &VideoPlayerEngine::onWorkerError); - connect(m_worker, &Worker::finished, this, &VideoPlayerEngine::onWorkerFinished); + uint64_t generation = ++m_openGeneration; + connect(m_worker, &Worker::opened, this, [this, generation](int w, int h, qint64 duration, bool audio) { + if (generation == m_openGeneration) onWorkerOpened(w, h, duration, audio); + }); + connect(m_worker, &Worker::audioDecoded, this, [this, generation](auto pcm, int rate, int channels, qint64 duration) { + if (generation == m_openGeneration) onAudioDecoded(pcm, rate, channels, duration); + }); + connect(m_worker, &Worker::frameDecoded, this, [this, generation](const QImage& frame, qint64 pts, int epoch) { + if (generation == m_openGeneration) onFrameDecoded(frame, pts, epoch); + }); + connect(m_worker, &Worker::error, this, [this, generation](const QString& error) { + if (generation == m_openGeneration) onWorkerError(error); + }); + connect(m_worker, &Worker::finished, this, [this, generation]() { + if (generation == m_openGeneration) onWorkerFinished(); + }); DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoEngine: open %s", qPrintable(path)); m_thread->start(); @@ -308,7 +319,7 @@ void VideoPlayerEngine::open(const QString& path) { void VideoPlayerEngine::play() { if (m_state != Paused && m_state != Playing) return; m_paused.storeRelaxed(0); - if (m_hasAudio) SoundManager::instance()->resumeCurrentPcm(); + if (m_hasAudio) SoundManager::instance()->resumePcm(m_audioPlayback); else { m_elapsedBaseMs = m_positionMs; m_elapsed.restart(); } setState(Playing); m_presentTimer.start(); @@ -317,7 +328,7 @@ void VideoPlayerEngine::play() { void VideoPlayerEngine::pause() { if (m_state != Playing) return; m_paused.storeRelaxed(1); - if (m_hasAudio) SoundManager::instance()->pauseCurrentPcm(); + if (m_hasAudio) SoundManager::instance()->pausePcm(m_audioPlayback); setState(Paused); m_presentTimer.stop(); } @@ -336,8 +347,8 @@ void VideoPlayerEngine::seekMs(qint64 ms) { m_seekReq.storeRelaxed(1); clearPending(); if (m_hasAudio) { - int sr = SoundManager::instance()->currentPcmSampleRate(); - if (sr > 0) SoundManager::instance()->seekCurrentPcm((unsigned long long)(ms * sr / 1000)); + int sr = SoundManager::instance()->pcmSampleRate(m_audioPlayback); + if (sr > 0) SoundManager::instance()->seekPcm(m_audioPlayback, (unsigned long long)(ms * sr / 1000)); } else { m_elapsedBaseMs = ms; m_elapsed.restart(); @@ -348,11 +359,12 @@ void VideoPlayerEngine::seekMs(qint64 ms) { } void VideoPlayerEngine::stop() { + ++m_openGeneration; m_abort.storeRelaxed(1); m_presentTimer.stop(); clearPending(); m_renderFrame = QImage(); - if (m_hasAudio) SoundManager::instance()->stopCurrentPcm(); + if (m_hasAudio) SoundManager::instance()->stopPcm(m_audioPlayback); setState(Stopped); m_positionMs = 0; m_durationMs = 0; @@ -373,9 +385,9 @@ void VideoPlayerEngine::close() { qint64 VideoPlayerEngine::clockMs() const { if (m_hasAudio && m_audioSampleRate > 0) { - if (m_state == Finished && !SoundManager::instance()->isCurrentPcmPlaying()) + if (m_state == Finished && !SoundManager::instance()->isPcmPlaying(m_audioPlayback)) return m_durationMs; /* аудио закончилось — часы в конец */ - unsigned long long cur = SoundManager::instance()->currentPcmCursor(); + unsigned long long cur = SoundManager::instance()->pcmCursor(m_audioPlayback); return (qint64)(cur * 1000 / (unsigned long long)m_audioSampleRate); } return m_elapsedBaseMs + m_elapsed.elapsed(); @@ -410,15 +422,16 @@ void VideoPlayerEngine::onAudioDecoded(std::shared_ptr> pcm int sampleRate, int channels, qint64 durationMs) { if (pcm && !pcm->empty() && channels > 0) { m_audioSampleRate = sampleRate; - SoundManager::instance()->playRawPcm(pcm->data(), (int)(pcm->size() / (size_t)channels), - sampleRate, channels); - if (!SoundManager::instance()->isCurrentPcmPlaying()) { + SoundManager::instance()->stopPcm(m_audioPlayback); + m_audioPlayback = SoundManager::instance()->playRawPcm(pcm->data(), (int)(pcm->size() / (size_t)channels), + sampleRate, channels, pcm); + if (!SoundManager::instance()->isPcmPlaying(m_audioPlayback)) { /* аудио-устройство недоступно — переходим на системные часы */ m_hasAudio = false; m_audioSampleRate = 0; DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "VideoEngine: audio not playing, fallback to system clock"); } else { - DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoEngine: audio started %dHz %dch", sampleRate, channels); + DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoEngine: audio started %dHz %dch", sampleRate, channels, pcm); } } else { m_hasAudio = false; @@ -471,7 +484,7 @@ void VideoPlayerEngine::presentTick() { if (m_state == Finished && m_pending.empty()) { m_presentTimer.stop(); - if (m_hasAudio) SoundManager::instance()->stopCurrentPcm(); + if (m_hasAudio) SoundManager::instance()->stopPcm(m_audioPlayback); } } diff --git a/tools/chatgui/src/videoplayer_engine.h b/tools/chatgui/src/videoplayer_engine.h index 5463479a..8cc77061 100644 --- a/tools/chatgui/src/videoplayer_engine.h +++ b/tools/chatgui/src/videoplayer_engine.h @@ -65,6 +65,7 @@ private: qint64 clockMs() const; void clearPending(); + uint64_t m_openGeneration = 0; QString m_path; QThread* m_thread = nullptr; class Worker* m_worker = nullptr; @@ -88,6 +89,7 @@ private: std::deque> m_pending; QImage m_renderFrame; QTimer m_presentTimer; + uint64_t m_audioPlayback = 0; int m_audioSampleRate = 0; QElapsedTimer m_elapsed; qint64 m_elapsedBaseMs = 0; diff --git a/tools/chatgui/src/voiceplayback.cpp b/tools/chatgui/src/voiceplayback.cpp index b699cdf7..b4f4eb80 100644 --- a/tools/chatgui/src/voiceplayback.cpp +++ b/tools/chatgui/src/voiceplayback.cpp @@ -73,13 +73,17 @@ int VoicePlayer::decodeOpusFile(const QString& filePath, std::vector& o static unsigned s_decodeGeneration = 0; /* Только GUI-поток. */ static bool s_decoding = false; +static uint64_t s_playbackId = 0; void VoicePlayer::cancelPending() { s_decodeGeneration++; } +void VoicePlayer::stop() { cancelPending(); SoundManager::instance()->stopPcm(s_playbackId); s_playbackId = 0; } +bool VoicePlayer::isPlaying() { return SoundManager::instance()->isPcmPlaying(s_playbackId); } +uint64_t VoicePlayer::playbackId() { return s_playbackId; } bool VoicePlayer::isPreparing() { return s_decoding; } /* Декодирование и выделение PCM выполняются в worker; GUI принимает готовый буфер. */ -void VoicePlayer::playOpusFile(const QString& filePath, QObject* context, std::function ready) { - if (s_decoding) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice: decoder busy"); ready(0); return; } +void VoicePlayer::playOpusFile(const QString& filePath, QObject* context, std::function ready) { + if (s_decoding) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice: decoder busy"); ready(0, 0); return; } struct Result { std::shared_ptr> pcm = std::make_shared>(); int rate = 0, channels = 0; @@ -101,9 +105,13 @@ void VoicePlayer::playOpusFile(const QString& filePath, QObject* context, std::f QObject::connect(thread, &QThread::finished, QCoreApplication::instance(), [result, receiver, ready, generation]() { s_decoding = false; if (!receiver || generation != s_decodeGeneration) return; - if (result->duration > 0) SoundManager::instance()->playRawPcm(result->pcm->data(), - (int)(result->pcm->size() / result->channels), result->rate, result->channels, result->pcm); - ready(result->duration); + if (result->duration > 0) { + SoundManager::instance()->stopPcm(s_playbackId); + s_playbackId = SoundManager::instance()->playRawPcm(result->pcm->data(), + (int)(result->pcm->size() / result->channels), result->rate, result->channels, result->pcm); + if (!s_playbackId) result->duration = 0; + } + ready(result->duration, s_playbackId); }); QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater); thread->start(); diff --git a/tools/chatgui/src/voiceplayback.h b/tools/chatgui/src/voiceplayback.h index fb84132a..7ddcd1f9 100644 --- a/tools/chatgui/src/voiceplayback.h +++ b/tools/chatgui/src/voiceplayback.h @@ -9,8 +9,11 @@ class QObject; class VoicePlayer { public: - static void playOpusFile(const QString& filePath, QObject* context, std::function ready); + static void playOpusFile(const QString& filePath, QObject* context, std::function ready); static void cancelPending(); + static void stop(); + static bool isPlaying(); + static uint64_t playbackId(); static bool isPreparing(); static int decodeOpusFile(const QString& filePath, std::vector& outPcm, int& outSampleRate, int& outChannels, float& outDurationSec); diff --git a/tools/chatgui/tests/test_audio_recovery.cpp b/tools/chatgui/tests/test_audio_recovery.cpp index fe0f6326..bb585640 100644 --- a/tools/chatgui/tests/test_audio_recovery.cpp +++ b/tools/chatgui/tests/test_audio_recovery.cpp @@ -84,19 +84,19 @@ static void pulse_recovery(SoundManager* manager, const QString& control) { REQUIRE(manager->context()->backend == ma_backend_pulseaudio); AudioRecorder recorder; recorder.init(); recorder.startRecording(); std::vector pcm(48000 * 20, 1000); - manager->playRawPcm(pcm.data(), (int)pcm.size(), 48000, 1); + auto pcmId = manager->playRawPcm(pcm.data(), (int)pcm.size(), 48000, 1); for (int i = 0; i < 100 && recorder.durationMs() < 300; ++i) pump(50); - REQUIRE(recorder.durationMs() >= 300 && manager->currentPcmCursor() > 0); + REQUIRE(recorder.durationMs() >= 300 && manager->pcmCursor(pcmId) > 0); int duration = recorder.durationMs(); - auto cursor = manager->currentPcmCursor(); + auto cursor = manager->pcmCursor(pcmId); QFile ready(control + "/ready"); REQUIRE(ready.open(QIODevice::WriteOnly)); ready.close(); for (int i = 0; i < 100 && manager->context(); ++i) pump(50); REQUIRE(!manager->context()); QFile closed(control + "/closed"); REQUIRE(closed.open(QIODevice::WriteOnly)); closed.close(); for (int i = 0; i < 150 && (!manager->context() || recorder.durationMs() <= duration + 200); ++i) pump(50); - REQUIRE(manager->context() && recorder.durationMs() > duration + 200 && manager->currentPcmCursor() > cursor); + REQUIRE(manager->context() && recorder.durationMs() > duration + 200 && manager->pcmCursor(pcmId) > cursor); qInfo("PulseAudio progress: capture=%d -> %dms playback=%llu -> %llu frames", duration, recorder.durationMs(), - (unsigned long long)cursor, manager->currentPcmCursor()); + (unsigned long long)cursor, (unsigned long long)manager->pcmCursor(pcmId)); recorder.stopRecording(); manager->shutdown(); REQUIRE(devices.empty()); qInfo("real PulseAudio: capture/playback progress restored after server restart PASS"); @@ -163,16 +163,16 @@ int main(int argc, char** argv) { REQUIRE(radio->start(7) == 0); AudioRecorder recorder; recorder.init(); recorder.startRecording(); std::vector notification(48000 * 10, 1000); - manager->playRawPcm(notification.data(), (int)notification.size(), 48000, 1); + auto pcmId = manager->playRawPcm(notification.data(), (int)notification.size(), 48000, 1); pump(60); - auto cursor = manager->currentPcmCursor(); + auto cursor = manager->pcmCursor(pcmId); REQUIRE(cursor > 0); int previousContext = contextStarts; failContext = true; failContexts = 2; pump(1800); REQUIRE(contextStarts >= previousContext + 3 && manager->context()); REQUIRE(call->callId() == 42 && radio->groupId() == 7 && recorder.isRecording()); - REQUIRE(manager->currentPcmCursor() > cursor && manager->isCurrentPcmPlaying()); + REQUIRE(manager->pcmCursor(pcmId) > cursor && manager->isPcmPlaying(pcmId)); call->stop(); radio->stop(); recorder.stopRecording(); int previousStarts = starts; pump(600); @@ -192,6 +192,26 @@ int main(int argc, char** argv) { pump(1000); REQUIRE(manager->context() && captureStarts == previousCapture); + // Частота источника и управление сессией независимы от соседнего проигрывателя. + std::vector pcm24(24000 * 2, 2000), pcm48(48000 * 2, -1000); + auto player24 = manager->playRawPcm(pcm24.data(), 48000, 24000, 1); + auto player48 = manager->playRawPcm(pcm48.data(), 96000, 48000, 1); + REQUIRE(player24 && player48 && player24 != player48); + pump(150); + uint64_t cursor24 = manager->pcmCursor(player24), cursor48 = manager->pcmCursor(player48); + REQUIRE(cursor24 > 1000 && cursor24 < 6000 && cursor48 > cursor24 * 1.8 && cursor48 < cursor24 * 2.2); + manager->pausePcm(player24); + cursor24 = manager->pcmCursor(player24); + pump(80); + REQUIRE(manager->pcmCursor(player24) == cursor24 && manager->pcmCursor(player48) > cursor48); + REQUIRE(manager->seekPcm(player24, 12000)); + REQUIRE(manager->pcmCursor(player24) == 12000 && !manager->isPcmPlaying(player24)); + manager->resumePcm(player24); + manager->stopPcm(player48); + pump(60); + REQUIRE(manager->pcmCursor(player24) > 12000 && manager->pcmCursor(player48) == 0); + manager->stopPcm(player24); + // Предел TX действует до помещения задачи в uasync; старое поколение не отправляется после stop/start. uasync_poll(instance.ua, 0); REQUIRE(call_audio_start(99) == 0); diff --git a/tools/chatgui/tests/test_voice_file.cpp b/tools/chatgui/tests/test_voice_file.cpp index ce65547c..b96d0b0a 100644 --- a/tools/chatgui/tests/test_voice_file.cpp +++ b/tools/chatgui/tests/test_voice_file.cpp @@ -35,7 +35,7 @@ int main(int argc, char** argv) { reject(valid.left(14)); // Обрезанный пакет. REQUIRE(file.open(QIODevice::WriteOnly | QIODevice::Truncate)); REQUIRE(file.write(valid) == valid.size()); file.close(); bool called = false; - VoicePlayer::playOpusFile(path, &app, [&](float) { called = true; }); + VoicePlayer::playOpusFile(path, &app, [&](float, uint64_t) { called = true; }); REQUIRE(VoicePlayer::isPreparing()); VoicePlayer::cancelPending(); QElapsedTimer deadline; deadline.start(); while (VoicePlayer::isPreparing() && deadline.elapsed() < 5000) { app.processEvents(); QThread::msleep(1); }