Sound für synchrone Wiedergabe geändert

This commit is contained in:
2026-10-06 19:10:34 +02:00
parent bc564c5371
commit 98f6f1c903
12 changed files with 572 additions and 409 deletions

View File

@@ -15,8 +15,13 @@ find_package(Qt6 REQUIRED COMPONENTS
Network
)
# ALSA (audio passthrough, same as the HDMI viewer)
find_package(PkgConfig REQUIRED)
pkg_check_modules(ALSA REQUIRED alsa)
set(SOURCES
src/main.cpp
src/audiothread.cpp
src/mainwindow.cpp
src/serialworker.cpp
src/rawview.cpp
@@ -30,6 +35,7 @@ set(SOURCES
)
set(HEADERS
include/audiothread.h
include/mainwindow.h
include/serialworker.h
include/rawview.h
@@ -45,7 +51,7 @@ set(HEADERS
qt6_add_resources(RESOURCES resources.qrc)
add_executable(uartscope ${SOURCES} ${HEADERS} ${RESOURCES})
target_include_directories(uartscope PRIVATE include)
target_include_directories(uartscope PRIVATE include ${ALSA_INCLUDE_DIRS})
target_link_libraries(uartscope PRIVATE
Qt6::Core
@@ -53,6 +59,7 @@ target_link_libraries(uartscope PRIVATE
Qt6::Widgets
Qt6::SerialPort
Qt6::Network
${ALSA_LIBRARIES}
)
# V4L2 uses Linux kernel headers directly (linux/videodev2.h)

37
include/audiothread.h Normal file
View File

@@ -0,0 +1,37 @@
#pragma once
#include <QThread>
#include <QString>
#include <atomic>
#include <alsa/asoundlib.h>
// Audio passthrough: captures from the grabber's ALSA device and plays it
// straight to the default output with small buffers (low latency, so the
// sound stays in sync with the video preview). Same implementation as in
// the HDMI viewer.
class AudioThread : public QThread
{
Q_OBJECT
public:
explicit AudioThread(const QString &device, QObject *parent = nullptr);
~AudioThread() override;
void stop();
signals:
void errorOccurred(const QString &message);
protected:
void run() override;
private:
QString m_device;
std::atomic<bool> m_running{false};
// PCM parameters
static constexpr unsigned int k_defaultRate = 48000;
static constexpr unsigned int k_defaultChannels = 2;
static constexpr snd_pcm_uframes_t k_periodFrames = 1024;
};

View File

@@ -11,6 +11,7 @@
#include <QProcess>
#include "v4l2worker.h"
#include "audiothread.h"
// A uniform, icon-only push button used for the video toolbar (start/stop,
// freeze, screenshot, record, fullscreen, audio, ...). All toolbar buttons
@@ -142,14 +143,12 @@ private:
// Audio passthrough: plays the grabber's ALSA capture device (e.g. the
// "USB Audio" card that accompanies a /dev/videoN HDMI/USB grabber)
// straight to the default output, the same way the HDMI viewer does it.
// Implemented by shelling out to `ffplay -f alsa -i <device>` with
// -nodisp (audio only, no extra window) - same approach as recording,
// just piping the other direction (device -> speakers instead of
// frames -> file).
// straight to the default output. Uses the same native ALSA
// capture->playback thread (AudioThread) as the HDMI viewer, with small
// buffers so the sound stays in sync with the video preview.
QComboBox *m_audioCombo = nullptr;
QPushButton *m_audioBtn = nullptr;
QProcess *m_audioProcess = nullptr;
AudioThread *m_audioThread = nullptr;
bool m_audioActive = false;
// Fullscreen overlay window (nullptr when not open). Owns itself via

140
src/audiothread.cpp Normal file
View File

@@ -0,0 +1,140 @@
#include "audiothread.h"
#include <QDebug>
#include <vector>
#include <cstring>
#include <cerrno>
AudioThread::AudioThread(const QString &device, QObject *parent)
: QThread(parent), m_device(device)
{}
AudioThread::~AudioThread()
{
stop();
wait();
}
void AudioThread::stop()
{
m_running = false;
}
// ── ALSA helper ──────────────────────────────────────────────────────────────
static bool setHwParams(snd_pcm_t *handle,
unsigned int &rate, unsigned int &channels,
snd_pcm_uframes_t periodFrames,
QString &errMsg)
{
snd_pcm_hw_params_t *params;
snd_pcm_hw_params_alloca(&params);
snd_pcm_hw_params_any(handle, params);
auto check = [&](int rc, const char *what) -> bool {
if (rc < 0) {
errMsg = QString("%1: %2").arg(what).arg(snd_strerror(rc));
return false;
}
return true;
};
if (!check(snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED), "access")) return false;
if (!check(snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE), "format")) return false;
if (!check(snd_pcm_hw_params_set_channels_near(handle, params, &channels), "channels")) return false;
if (!check(snd_pcm_hw_params_set_rate_near(handle, params, &rate, nullptr), "rate")) return false;
snd_pcm_uframes_t bufferFrames = periodFrames * 4;
snd_pcm_hw_params_set_buffer_size_near(handle, params, &bufferFrames);
snd_pcm_hw_params_set_period_size_near(handle, params, &periodFrames, nullptr);
if (!check(snd_pcm_hw_params(handle, params), "hw_params")) return false;
return true;
}
void AudioThread::run()
{
m_running = true;
snd_pcm_t *captureHandle = nullptr;
snd_pcm_t *playbackHandle = nullptr;
// ── Open capture ─────────────────────────────────────────────────────────
const QString captureDevice = m_device;
int rc = snd_pcm_open(&captureHandle, captureDevice.toLocal8Bit().constData(),
SND_PCM_STREAM_CAPTURE, 0);
if (rc < 0) {
emit errorOccurred(QString("Audio capture '%1' could not be opened: %2")
.arg(captureDevice).arg(snd_strerror(rc)));
return;
}
unsigned int rate = k_defaultRate;
unsigned int channels = k_defaultChannels;
snd_pcm_uframes_t period = k_periodFrames;
QString errMsg;
if (!setHwParams(captureHandle, rate, channels, period, errMsg)) {
emit errorOccurred("Audio capture HW params: " + errMsg);
snd_pcm_close(captureHandle);
return;
}
snd_pcm_prepare(captureHandle);
qDebug() << "Audio capture:" << rate << "Hz," << channels << "ch";
// ── Open playback ────────────────────────────────────────────────────────
rc = snd_pcm_open(&playbackHandle, "default", SND_PCM_STREAM_PLAYBACK, 0);
if (rc < 0) {
emit errorOccurred(QString("Audio playback could not be opened: %1")
.arg(snd_strerror(rc)));
snd_pcm_close(captureHandle);
return;
}
if (!setHwParams(playbackHandle, rate, channels, period, errMsg)) {
emit errorOccurred("Audio playback HW params: " + errMsg);
snd_pcm_close(captureHandle);
snd_pcm_close(playbackHandle);
return;
}
snd_pcm_prepare(playbackHandle);
// ── Loop: capture -> playback ────────────────────────────────────────────
std::vector<qint16> buffer(period * channels);
while (m_running) {
snd_pcm_sframes_t framesRead =
snd_pcm_readi(captureHandle, buffer.data(), period);
if (framesRead < 0) {
if (framesRead == -EPIPE) {
snd_pcm_prepare(captureHandle);
continue;
}
qWarning() << "Audio capture error:" << snd_strerror(static_cast<int>(framesRead));
msleep(10);
continue;
}
snd_pcm_sframes_t written = 0;
while (written < framesRead && m_running) {
snd_pcm_sframes_t w =
snd_pcm_writei(playbackHandle,
buffer.data() + written * static_cast<snd_pcm_sframes_t>(channels),
static_cast<snd_pcm_uframes_t>(framesRead - written));
if (w < 0) {
if (w == -EPIPE) {
snd_pcm_prepare(playbackHandle);
break;
}
qWarning() << "Audio playback error:" << snd_strerror(static_cast<int>(w));
break;
}
written += w;
}
}
snd_pcm_drain(playbackHandle);
snd_pcm_close(captureHandle);
snd_pcm_close(playbackHandle);
}

View File

@@ -663,45 +663,17 @@ void VideoWidget::startAudio()
return;
}
m_audioProcess = new QProcess(this);
// Native ALSA capture -> playback loop (same AudioThread as the HDMI
// viewer): small period/buffer sizes keep the latency low, so the sound
// stays in sync with the video preview.
m_audioThread = new AudioThread(device, this);
connect(m_audioProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) {
m_infoLabel->setText(tr("Audio error: %1").arg(m_audioProcess->errorString()));
connect(m_audioThread, &AudioThread::errorOccurred, this, [this](const QString &msg) {
m_infoLabel->setText(tr("Audio error: %1").arg(msg));
stopAudio();
});
connect(m_audioProcess,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, [this](int exitCode, QProcess::ExitStatus status) {
if (m_audioActive && status == QProcess::NormalExit && exitCode != 0)
m_infoLabel->setText(tr("ffplay (audio) exited with code %1").arg(exitCode));
m_audioActive = false;
m_audioBtn->setChecked(false);
m_audioBtn->setText(QStringLiteral("🔈"));
if (m_audioProcess) {
m_audioProcess->deleteLater();
m_audioProcess = nullptr;
}
});
// -nodisp: audio only, no extra video window (we already show the
// preview ourselves). Same trick the HDMI viewer uses under the hood.
const QStringList args = {
"-hide_banner", "-loglevel", "error",
"-nodisp",
"-fflags", "nobuffer",
"-f", "alsa", "-i", device,
};
m_audioProcess->start("ffplay", args);
if (!m_audioProcess->waitForStarted(3000)) {
QMessageBox::critical(this, tr("Audio failed"),
tr("Could not start ffplay for audio playback. Is ffmpeg (which includes "
"ffplay) installed and available on PATH?"));
m_audioProcess->deleteLater();
m_audioProcess = nullptr;
m_audioBtn->setChecked(false);
return;
}
m_audioThread->start();
m_audioActive = true;
m_audioBtn->setText(QStringLiteral("🔊"));
@@ -709,14 +681,21 @@ void VideoWidget::startAudio()
void VideoWidget::stopAudio()
{
AudioThread *thread = m_audioThread;
m_audioThread = nullptr;
m_audioActive = false;
if (thread) {
thread->stop();
// snd_pcm_readi() blocks until a period has been captured; if the
// grabber vanished, don't hang the UI forever.
if (!thread->wait(2000)) {
thread->terminate();
thread->wait();
}
thread->deleteLater();
}
m_audioBtn->setChecked(false);
m_audioBtn->setText(QStringLiteral("🔈"));
if (m_audioProcess) {
m_audioProcess->terminate();
if (!m_audioProcess->waitForFinished(1500))
m_audioProcess->kill();
// finished() handler (connected in startAudio()) cleans it up.
}
}

View File

@@ -1,6 +1,6 @@
# Maintainer: diabolus <your@email.com>
pkgname=uartscope
pkgver=1.1.0.r3.g4a44d04
pkgver=1.1.0.r5.gbc564c5
pkgrel=1
pkgdesc="Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect"
arch=('x86_64' 'aarch64')

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -3,12 +3,12 @@
pkgname = uartscope
pkgbase = uartscope
xdata = pkgtype=pkg
pkgver = 1.1.0.r3.g4a44d04-1
pkgver = 1.1.0.r5.gbc564c5-1
pkgdesc = Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect
url = https://git.projekt-hirnfrei.de/diabolus/uartscope
builddate = 1789496502
builddate = 1790467495
packager = Unknown Packager
size = 463857
size = 476145
arch = x86_64
license = MIT
conflict = uartscope

Submodule uartscope-git/src/uartscope updated: 4a44d04c75...bc564c5371

View File

@@ -1,3 +1,3 @@
4a44d04c755e0162c5e7aec23b9cf4fe111114f0 not-for-merge branch 'main' of https://git.projekt-hirnfrei.de/diabolus/uartscope
bc564c537179619565695a702c83316097adf343 not-for-merge branch 'main' of https://git.projekt-hirnfrei.de/diabolus/uartscope
cc102c93eb17f7b910d8e74c3505f198bed77f10 not-for-merge tag 'v1.0.0' of https://git.projekt-hirnfrei.de/diabolus/uartscope
36923e16461bc15086c84187790dbcb5f0a5030f not-for-merge tag 'v1.1.0' of https://git.projekt-hirnfrei.de/diabolus/uartscope