Sound für synchrone Wiedergabe geändert
This commit is contained in:
@@ -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
37
include/audiothread.h
Normal 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;
|
||||
};
|
||||
@@ -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
140
src/audiothread.cpp
Normal 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(¶ms);
|
||||
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);
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
@@ -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
|
||||
|
||||
Binary file not shown.
Submodule uartscope-git/src/uartscope updated: 4a44d04c75...bc564c5371
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user