diff options
author | Tim Blechmann <[email protected]> | 2025-05-28 10:13:39 +0800 |
---|---|---|
committer | Tim Blechmann <[email protected]> | 2025-07-04 15:44:14 +0800 |
commit | f1d52a8986c0cb938ba28fe4a8bc3cbe73f35bc3 (patch) | |
tree | 880310396ae95d7f8fbfd500be84aedc5c26cf41 | |
parent | cf01e65fb05629b56315f4a4e254b0f4d18ebb4a (diff) |
Change-Id: Ia954fc3b592d1c957c2dcb93f063c3d521104789
Reviewed-by: Timur Pocheptsov <[email protected]>
Reviewed-by: Artem Dyomin <[email protected]>
-rw-r--r-- | tests/manual/CMakeLists.txt | 1 | ||||
-rw-r--r-- | tests/manual/minimal-audio-callback-io/CMakeLists.txt | 43 | ||||
-rw-r--r-- | tests/manual/minimal-audio-callback-io/Info.plist.in | 46 | ||||
-rw-r--r-- | tests/manual/minimal-audio-callback-io/minimal-audio-callback-io.cpp | 281 |
4 files changed, 371 insertions, 0 deletions
diff --git a/tests/manual/CMakeLists.txt b/tests/manual/CMakeLists.txt index 8bbe3b56d..1ba52c5de 100644 --- a/tests/manual/CMakeLists.txt +++ b/tests/manual/CMakeLists.txt @@ -6,6 +6,7 @@ add_subdirectory(mediadevices) add_subdirectory(mediaformats) add_subdirectory(media-metadata) add_subdirectory(minimal-audio-recorder) +add_subdirectory(minimal-audio-callback-io) add_subdirectory(minimal-player) add_subdirectory(minimal-screen-recorder) add_subdirectory(wasm) diff --git a/tests/manual/minimal-audio-callback-io/CMakeLists.txt b/tests/manual/minimal-audio-callback-io/CMakeLists.txt new file mode 100644 index 000000000..6c2e21d46 --- /dev/null +++ b/tests/manual/minimal-audio-callback-io/CMakeLists.txt @@ -0,0 +1,43 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(minimal-audio-callback-io LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/multimedia/minimal-audio-callback-io") + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + find_package(Qt6 REQUIRED COMPONENTS Multimedia MultimediaPrivate) +endif() + +qt_add_executable( minimal-audio-callback-io MACOSX_BUNDLE + minimal-audio-callback-io.cpp + ../../../src/3rdparty/dr_libs/dr_wav.h +) + +target_include_directories( minimal-audio-callback-io SYSTEM PRIVATE + ../../../src/3rdparty/dr_libs +) + +target_link_libraries( minimal-audio-callback-io PUBLIC + Qt::Multimedia + Qt::MultimediaPrivate +) + +install(TARGETS minimal-audio-callback-io + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) + +set_target_properties( minimal-audio-callback-io PROPERTIES + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in +) diff --git a/tests/manual/minimal-audio-callback-io/Info.plist.in b/tests/manual/minimal-audio-callback-io/Info.plist.in new file mode 100644 index 000000000..d97735d46 --- /dev/null +++ b/tests/manual/minimal-audio-callback-io/Info.plist.in @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + + <key>CFBundleName</key> + <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string> + <key>CFBundleExecutable</key> + <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string> + + <key>CFBundleVersion</key> + <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string> + <key>CFBundleShortVersionString</key> + <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string> + <key>CFBundleLongVersionString</key> + <string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string> + + <key>LSMinimumSystemVersion</key> + <string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string> + + <key>CFBundleGetInfoString</key> + <string>${MACOSX_BUNDLE_INFO_STRING}</string> + <key>NSHumanReadableCopyright</key> + <string>${MACOSX_BUNDLE_COPYRIGHT}</string> + + <key>CFBundleIconFile</key> + <string>${MACOSX_BUNDLE_ICON_FILE}</string> + + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + + <key>NSCameraUsageDescription</key> + <string>Qt Multimedia Example</string> + <key>NSMicrophoneUsageDescription</key> + <string>Qt Multimedia Example</string> + + <key>NSSupportsAutomaticGraphicsSwitching</key> + <true/> + </dict> +</plist>
\ No newline at end of file diff --git a/tests/manual/minimal-audio-callback-io/minimal-audio-callback-io.cpp b/tests/manual/minimal-audio-callback-io/minimal-audio-callback-io.cpp new file mode 100644 index 000000000..d36edac89 --- /dev/null +++ b/tests/manual/minimal-audio-callback-io/minimal-audio-callback-io.cpp @@ -0,0 +1,281 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtCore/QCommandLineOption> +#include <QtCore/QCommandLineParser> +#include <QtCore/QCoreApplication> +#include <QtCore/QDebug> +#include <QtCore/QDir> +#include <QtCore/QUuid> +#include <QtMultimedia/QAudioDevice> +#include <QtMultimedia/QAudioSink> +#include <QtMultimedia/QAudioSource> +#include <QtMultimedia/QMediaDevices> +#include <QtMultimedia/private/qaudiosystem_p.h> +#include <QtMultimedia/private/qaudioringbuffer_p.h> +#include <QtMultimedia/private/qautoresetevent_p.h> + +#include <ranges> +#include <variant> + +#define DR_WAV_IMPLEMENTATION // header-only use +#define DRWAV_API static // emit symbols with hidden visibility +#define DRWAV_PRIVATE static +#define DR_WAV_NO_WCHAR +#include "dr_wav.h" + +using namespace Qt::Literals; + +namespace { + +namespace CLI { + +struct Output +{ + int deviceIndex; + int bufferSize = 1024; +}; + +struct Input +{ + int deviceIndex; + int bufferSize = 1024; +}; + +struct ListDevices +{ +}; + +using Arguments = std::variant<ListDevices, Input, Output>; + +Arguments parseArguments(const QCoreApplication &app) +{ + QCommandLineParser parser; + parser.setApplicationDescription("Minimal test for audio callback io."); + parser.addHelpOption(); + parser.addVersionOption(); + + QCommandLineOption bufferSizeOption{ + { u"b"_s, u"buffer-size"_s }, + "Set the hardware buffer size in frames (integer). Used by 'input' and 'output' commands.", + "size", + "1024", + }; + + QCommandLineOption deviceIndexOption{ + { u"d"_s, u"device"_s }, + "Specify the device index (string). Used by 'input' and 'output' commands.", + "index", + "0", + }; + + parser.addOption(bufferSizeOption); + parser.addOption(deviceIndexOption); + parser.addPositionalArgument("command", "The command to execute: list-devices, input, output."); + parser.process(app); + + const QStringList positionalArguments = parser.positionalArguments(); + + if (positionalArguments.isEmpty()) { + qCritical() << "Error: No command specified."; + parser.showHelp(1); // Show help and exit with error code 1 + } + + const QString command = + positionalArguments.first().toLower(); // Get the first positional arg as the command + + if (command == u"list-devices"_s) + return ListDevices{}; + + auto parseBufferSize = [&]() -> int { + QString bufferSizeStr = parser.value(bufferSizeOption); + bool bufferSizeOk = false; + int bufferSize = bufferSizeStr.toInt(&bufferSizeOk); + if (!bufferSizeOk || bufferSize <= 0) { + qCritical() << "Error: Invalid buffer size '" << bufferSizeStr + << "'. Must be a positive integer."; + parser.showHelp(1); + } + return bufferSize; + }; + + auto parseDeviceIndex = [&]() -> int { + QString deviceIndexStr = parser.value(deviceIndexOption); + bool deviceIndexOk = false; + int deviceIndex = deviceIndexStr.toInt(&deviceIndexOk); + if (!deviceIndexOk || deviceIndex < 0) { + qCritical() << "Error: Invalid buffer size '" << deviceIndexStr + << "'. Must be a non-negative integer."; + parser.showHelp(1); + } + return deviceIndex; + }; + + if (command == u"input"_s) + return Input{ parseDeviceIndex(), parseBufferSize() }; + + if (command == u"output"_s) + return Output{ parseDeviceIndex(), parseBufferSize() }; + + qCritical() << "Error: Unknown command '" << command << "'."; + parser.showHelp(1); + Q_UNREACHABLE_RETURN(ListDevices{}); +} + +} // namespace CLI + +void printAudioDeviceList(const QList<QAudioDevice> &devices, const QString &title = QString()) +{ + QTextStream out(stdout); + out << title << "\n"; + + if (devices.isEmpty()) { + out << "No audio devices found.\n"; + out.flush(); // Ensure output is immediate + return; + } + + int index = 0; + for (const QAudioDevice &device : devices) { + QString description = device.description(); // User-friendly name + out << "\t" << index++ << ": " << description << "\n"; + } + out.flush(); // Ensure output is displayed immediately +} + +int runCommand(CLI::ListDevices) +{ + QList<QAudioDevice> inputs = QMediaDevices::audioInputs(); + QList<QAudioDevice> outputs = QMediaDevices::audioOutputs(); + + printAudioDeviceList(inputs, u"Audio Inputs"_s); + printAudioDeviceList(outputs, u"Audio Outputs"_s); + return 0; +} + +int runCommand(const CLI::Input &input) +{ + QTextStream out(stdout); + + QAudioDevice device = QMediaDevices::audioInputs().value(input.deviceIndex, QAudioDevice{}); + if (device.isNull()) { + qCritical() << "Error: Invalid input device index" << input.deviceIndex; + return 1; + }; + + QAudioFormat format = device.preferredFormat(); + format.setSampleFormat(QAudioFormat::Float); + QAudioSource source(device, format); + + out << "Opening " << device.description() << "\n"; + + QPlatformAudioSource *platformSource = QPlatformAudioSource::get(source); + platformSource->setHardwareBufferFrames(input.bufferSize); + + using namespace QtPrivate; + drwav_data_format wavFormat{ + .container = drwav_container_w64, + .format = DR_WAVE_FORMAT_IEEE_FLOAT, + .channels = static_cast<drwav_uint32>(format.channelCount()), + .sampleRate = static_cast<drwav_uint32>(format.sampleRate()), + .bitsPerSample = 32, + }; + + drwav wav; + + QString fileName = QDir::tempPath() + u"/"_s + + QUuid::createUuid().toString(QUuid::WithoutBraces) + u".wav"_s; + + drwav_bool32 fileOpen = + drwav_init_file_write(&wav, fileName.toUtf8().constData(), &wavFormat, nullptr); + if (!fileOpen) { + qCritical() << "Error opening WAV file for writing:" << fileName; + return 1; + } + + auto closeFile = qScopeGuard([&] { + if (!drwav_uninit(&wav)) + qCritical() << "Failed to close WAV file:" << fileName; + }); + + out << "Writing audio data to file: " << fileName << "\n"; + out.flush(); + + QAudioRingBuffer<float> ringBuffer{ 96000 * 10 }; // 10s + + QAutoResetEvent bufferReadyEvent; + bufferReadyEvent.callOnActivated([&] { + ringBuffer.consumeSome([&](QSpan<float> data) { + auto frames = data.size() / format.channelCount(); + + drwav_write_pcm_frames(&wav, frames, data.data()); + + QSpan<float> take = std::views::take(data, frames); + return take; + }); + }); + + platformSource->start([&](QSpan<const float> output) mutable { + ringBuffer.write(output); + bufferReadyEvent.set(); + }); + + if (source.error() != QAudio::NoError) { + qCritical() << "Error starting audio sink:" << source.error(); + return 1; + } + + return qApp->exec(); +} + +int runCommand(const CLI::Output &output) +{ + QAudioDevice device = QMediaDevices::audioOutputs().value(output.deviceIndex, QAudioDevice{}); + if (device.isNull()) { + qCritical() << "Error: Invalid output device index" << output.deviceIndex; + return 1; + }; + + QAudioFormat format = device.preferredFormat(); + format.setSampleFormat(QAudioFormat::Float); + QAudioSink sink(device, format); + + QPlatformAudioSink *platformSink = QPlatformAudioSink::get(sink); + platformSink->setHardwareBufferFrames(output.bufferSize); + + float phaseIncrement = 2 * M_PI * 220.f / format.sampleRate(); // 220 Hz tone + platformSink->start([&, phase = 0.f](QSpan<float> output) mutable { + int channels = format.channelCount(); + + while (!output.isEmpty()) { + std::ranges::fill(std::views::take(output, channels), std::sin(phase)); + + output = std::views::drop(output, channels); + phase += phaseIncrement; + if (phase >= 2 * M_PI) + phase -= 2 * M_PI; // Wrap phase + } + }); + + if (sink.error() != QAudio::NoError) { + qCritical() << "Error starting audio sink:" << sink.error(); + return 1; + } + + return qApp->exec(); +} + +} // namespace + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + QCoreApplication::setApplicationName("Minimal test for audio callback io"); + QCoreApplication::setApplicationVersion("1.0"); + + auto command = CLI::parseArguments(app); + + return std::visit([](auto &cmd) { + return runCommand(cmd); + }, command); +} |