Sound für synchrone Wiedergabe geändert
This commit is contained in:
@@ -15,8 +15,13 @@ find_package(Qt6 REQUIRED COMPONENTS
|
|||||||
Network
|
Network
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ALSA (audio passthrough, same as the HDMI viewer)
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(ALSA REQUIRED alsa)
|
||||||
|
|
||||||
set(SOURCES
|
set(SOURCES
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
|
src/audiothread.cpp
|
||||||
src/mainwindow.cpp
|
src/mainwindow.cpp
|
||||||
src/serialworker.cpp
|
src/serialworker.cpp
|
||||||
src/rawview.cpp
|
src/rawview.cpp
|
||||||
@@ -30,6 +35,7 @@ set(SOURCES
|
|||||||
)
|
)
|
||||||
|
|
||||||
set(HEADERS
|
set(HEADERS
|
||||||
|
include/audiothread.h
|
||||||
include/mainwindow.h
|
include/mainwindow.h
|
||||||
include/serialworker.h
|
include/serialworker.h
|
||||||
include/rawview.h
|
include/rawview.h
|
||||||
@@ -45,7 +51,7 @@ set(HEADERS
|
|||||||
qt6_add_resources(RESOURCES resources.qrc)
|
qt6_add_resources(RESOURCES resources.qrc)
|
||||||
add_executable(uartscope ${SOURCES} ${HEADERS} ${RESOURCES})
|
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
|
target_link_libraries(uartscope PRIVATE
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
@@ -53,6 +59,7 @@ target_link_libraries(uartscope PRIVATE
|
|||||||
Qt6::Widgets
|
Qt6::Widgets
|
||||||
Qt6::SerialPort
|
Qt6::SerialPort
|
||||||
Qt6::Network
|
Qt6::Network
|
||||||
|
${ALSA_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
# V4L2 uses Linux kernel headers directly (linux/videodev2.h)
|
# 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 <QProcess>
|
||||||
|
|
||||||
#include "v4l2worker.h"
|
#include "v4l2worker.h"
|
||||||
|
#include "audiothread.h"
|
||||||
|
|
||||||
// A uniform, icon-only push button used for the video toolbar (start/stop,
|
// A uniform, icon-only push button used for the video toolbar (start/stop,
|
||||||
// freeze, screenshot, record, fullscreen, audio, ...). All toolbar buttons
|
// 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
|
// Audio passthrough: plays the grabber's ALSA capture device (e.g. the
|
||||||
// "USB Audio" card that accompanies a /dev/videoN HDMI/USB grabber)
|
// "USB Audio" card that accompanies a /dev/videoN HDMI/USB grabber)
|
||||||
// straight to the default output, the same way the HDMI viewer does it.
|
// straight to the default output. Uses the same native ALSA
|
||||||
// Implemented by shelling out to `ffplay -f alsa -i <device>` with
|
// capture->playback thread (AudioThread) as the HDMI viewer, with small
|
||||||
// -nodisp (audio only, no extra window) - same approach as recording,
|
// buffers so the sound stays in sync with the video preview.
|
||||||
// just piping the other direction (device -> speakers instead of
|
|
||||||
// frames -> file).
|
|
||||||
QComboBox *m_audioCombo = nullptr;
|
QComboBox *m_audioCombo = nullptr;
|
||||||
QPushButton *m_audioBtn = nullptr;
|
QPushButton *m_audioBtn = nullptr;
|
||||||
QProcess *m_audioProcess = nullptr;
|
AudioThread *m_audioThread = nullptr;
|
||||||
bool m_audioActive = false;
|
bool m_audioActive = false;
|
||||||
|
|
||||||
// Fullscreen overlay window (nullptr when not open). Owns itself via
|
// 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;
|
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) {
|
connect(m_audioThread, &AudioThread::errorOccurred, this, [this](const QString &msg) {
|
||||||
m_infoLabel->setText(tr("Audio error: %1").arg(m_audioProcess->errorString()));
|
m_infoLabel->setText(tr("Audio error: %1").arg(msg));
|
||||||
stopAudio();
|
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
|
m_audioThread->start();
|
||||||
// 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_audioActive = true;
|
m_audioActive = true;
|
||||||
m_audioBtn->setText(QStringLiteral("🔊"));
|
m_audioBtn->setText(QStringLiteral("🔊"));
|
||||||
@@ -709,14 +681,21 @@ void VideoWidget::startAudio()
|
|||||||
|
|
||||||
void VideoWidget::stopAudio()
|
void VideoWidget::stopAudio()
|
||||||
{
|
{
|
||||||
|
AudioThread *thread = m_audioThread;
|
||||||
|
m_audioThread = nullptr;
|
||||||
m_audioActive = false;
|
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->setChecked(false);
|
||||||
m_audioBtn->setText(QStringLiteral("🔈"));
|
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>
|
# Maintainer: diabolus <your@email.com>
|
||||||
pkgname=uartscope
|
pkgname=uartscope
|
||||||
pkgver=1.1.0.r3.g4a44d04
|
pkgver=1.1.0.r5.gbc564c5
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect"
|
pkgdesc="Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect"
|
||||||
arch=('x86_64' 'aarch64')
|
arch=('x86_64' 'aarch64')
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -3,12 +3,12 @@
|
|||||||
pkgname = uartscope
|
pkgname = uartscope
|
||||||
pkgbase = uartscope
|
pkgbase = uartscope
|
||||||
xdata = pkgtype=pkg
|
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
|
pkgdesc = Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect
|
||||||
url = https://git.projekt-hirnfrei.de/diabolus/uartscope
|
url = https://git.projekt-hirnfrei.de/diabolus/uartscope
|
||||||
builddate = 1789496502
|
builddate = 1790467495
|
||||||
packager = Unknown Packager
|
packager = Unknown Packager
|
||||||
size = 463857
|
size = 476145
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
license = MIT
|
license = MIT
|
||||||
conflict = uartscope
|
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
|
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
|
36923e16461bc15086c84187790dbcb5f0a5030f not-for-merge tag 'v1.1.0' of https://git.projekt-hirnfrei.de/diabolus/uartscope
|
||||||
|
|||||||
Reference in New Issue
Block a user