Browse Source

Раздели PCM-сессии и исправь частоту и перемотку звука

master
evgeny 1 day ago
parent
commit
217e5795bf
  1. 2
      tools/chatgui/src/audiodevicesettingspage.cpp
  2. 10
      tools/chatgui/src/mainwindow.cpp
  3. 36
      tools/chatgui/src/messagedelegate.cpp
  4. 3
      tools/chatgui/src/messagedelegate.h
  5. 2
      tools/chatgui/src/messagelist.cpp
  6. 155
      tools/chatgui/src/sound_manager.cpp
  7. 40
      tools/chatgui/src/sound_manager.h
  8. 47
      tools/chatgui/src/videoplayer_engine.cpp
  9. 2
      tools/chatgui/src/videoplayer_engine.h
  10. 18
      tools/chatgui/src/voiceplayback.cpp
  11. 5
      tools/chatgui/src/voiceplayback.h
  12. 36
      tools/chatgui/tests/test_audio_recovery.cpp
  13. 2
      tools/chatgui/tests/test_voice_file.cpp

2
tools/chatgui/src/audiodevicesettingspage.cpp

@ -411,7 +411,7 @@ void AudioDeviceSettingsPage::onTestSpeaker() {
std::vector<int16_t> 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) {

10
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();
}

36
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<QAbstractItemModel*>(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<QAbstractItemModel*>(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;
}

3
tools/chatgui/src/messagedelegate.h

@ -1,4 +1,5 @@
#pragma once
#include <cstdint>
#include <QStyledItemDelegate>
#include <functional>
@ -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<void(const QString&)> cb) { m_videoOpenCb = std::move(cb); }

2
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); });
});
}
}

155
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<std::vector<int16_t>> 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<std::vector<int16_t>> 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<PcmSession>();
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) {

40
tools/chatgui/src/sound_manager.h

@ -6,6 +6,7 @@
#include <QByteArray>
#include <vector>
#include <memory>
#include <map>
#include <cstdint>
#include <QTimer>
#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<std::vector<int16_t>> 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<std::vector<int16_t>> 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<std::vector<int16_t>> 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<std::vector<int16_t>> m_currentPcmOwner;
std::map<PlaybackId, std::unique_ptr<PcmSession>> m_pcmSessions;
PlaybackId m_nextPlaybackId = 0;
bool m_initialized = false;
bool m_enabled = true;
float m_volume = 0.8f;

47
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<std::vector<int16_t>> 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);
}
}

2
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<std::pair<QImage, qint64>> m_pending;
QImage m_renderFrame;
QTimer m_presentTimer;
uint64_t m_audioPlayback = 0;
int m_audioSampleRate = 0;
QElapsedTimer m_elapsed;
qint64 m_elapsedBaseMs = 0;

18
tools/chatgui/src/voiceplayback.cpp

@ -73,13 +73,17 @@ int VoicePlayer::decodeOpusFile(const QString& filePath, std::vector<int16_t>& 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<void(float)> 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<void(float, uint64_t)> ready) {
if (s_decoding) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice: decoder busy"); ready(0, 0); return; }
struct Result {
std::shared_ptr<std::vector<int16_t>> pcm = std::make_shared<std::vector<int16_t>>();
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();

5
tools/chatgui/src/voiceplayback.h

@ -9,8 +9,11 @@ class QObject;
class VoicePlayer {
public:
static void playOpusFile(const QString& filePath, QObject* context, std::function<void(float)> ready);
static void playOpusFile(const QString& filePath, QObject* context, std::function<void(float, uint64_t)> 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<int16_t>& outPcm,
int& outSampleRate, int& outChannels, float& outDurationSec);

36
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<int16_t> 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<int16_t> 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<int16_t> 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);

2
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); }

Loading…
Cancel
Save