From 3a3af42cc6fd0a7b56428afa8a49437e40b29379 Mon Sep 17 00:00:00 2001 From: Bill Somerville Date: Fri, 10 May 2019 17:27:52 +0100 Subject: [PATCH] Tool to exercise QAudioInput The tool record_time_signal is designed to measure the performance of QAudioInput. The intended use is to record a short period of live audio from an on-air time signal of known good quaility, the basic required parameters are an audio input device, an output file name (.WAV), a start second in a minute, and a duration in seconds. So for example to record the time signal ticks and fast data at the top of the minute: $ record_time_signal -o wwv.wav -s 55 -d 15 will record 15s of audio at 48000Hz sample rate, stereo, from the default audio input device, starting at second 55. This will use a separate timer to stop the recording which is likely to leave the output file a little short due to buffer latency. The buffer size can be adjusted using the '-b ' option. The tool also supoorts a different mechanism to time the recording which uses the audio progress via a notify signal. This should ensure at least the requested duration is recorded The shorter the notify interval the closer teh final size shoould be to the requested duration. Use the '-d ' option to adjust the notify interval. $ record_time_signal -o wwv.wav -s 55 -d 15 -n 100 Non-default audio devices can be selected, use the '-I' option to list the available input devices with an index number that can be used to select the device using the 'R ' option. Other options are available, use '-h' for details. --- Audio/tools/record_time_signal.cpp | 231 +++++++++++++++++++++++++++++ CMakeLists.txt | 3 + 2 files changed, 234 insertions(+) create mode 100644 Audio/tools/record_time_signal.cpp diff --git a/Audio/tools/record_time_signal.cpp b/Audio/tools/record_time_signal.cpp new file mode 100644 index 000000000..4958c5743 --- /dev/null +++ b/Audio/tools/record_time_signal.cpp @@ -0,0 +1,231 @@ +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "revision_utils.hpp" +#include "Audio/BWFFile.hpp" + +namespace +{ + QTextStream qtout {stdout}; +} + +class Recorder final + : public QObject +{ + Q_OBJECT; + +public: + Recorder (int start, int duration, QString const& output, QAudioDeviceInfo const& source_device, QAudioFormat const& format, int notify_interval, int buffer_size) + : source_ {source_device, format} + , notify_interval_ {notify_interval} + , output_ {format, output} + , duration_ {duration} + { + if (!output_.open (BWFFile::WriteOnly)) throw std::invalid_argument {QString {"cannot open output file \"%1\""}.arg (output).toStdString ()}; + + if (buffer_size) source_.setBufferSize (format.bytesForFrames (buffer_size)); + if (notify_interval_) + { + source_.setNotifyInterval (notify_interval); + connect (&source_, &QAudioInput::notify, this, &Recorder::notify); + } + + QTimer::singleShot (int ((((start - (QDateTime::currentMSecsSinceEpoch () / 1000) % 60) + 60) % 60) * 1000), Qt::PreciseTimer, this, &Recorder::start); + } + + Q_SIGNAL void done (); + +private: + Q_SLOT void start () + { + qtout << "started recording at " << QDateTime::currentDateTimeUtc ().toString ("hh:mm:ss.zzz UTC") << endl; + source_.start (&output_); + if (!notify_interval_) QTimer::singleShot (duration_ * 1000, Qt::PreciseTimer, this, &Recorder::stop); + qtout << QString {"buffer size used is: %1"}.arg (source_.bufferSize ()) << endl; + } + + Q_SLOT void notify () + { + auto length = source_.elapsedUSecs (); + qtout << QString {"%1 US recorded\r"}.arg (length) << flush; + if (length >= duration_ * 1000 * 1000) stop (); + } + + Q_SLOT void stop () + { + auto length = source_.elapsedUSecs (); + source_.stop (); + qtout << QString {"%1 uS recorded "}.arg (length) << '(' << source_.format ().framesForBytes (output_.size ()) << " frames recorded)\n"; + qtout << "stopped recording at " << QDateTime::currentDateTimeUtc ().toString ("hh:mm:ss.zzz UTC") << endl; + Q_EMIT done (); + } + + QAudioInput source_; + int notify_interval_; + BWFFile output_; + int duration_; +}; + +#include "record_time_signal.moc" + +int main(int argc, char *argv[]) +{ + QCoreApplication app {argc, argv}; + try + { + ::setlocale (LC_NUMERIC, "C"); // ensure number forms are in + // consistent format, do this + // after instantiating + // QApplication so that Qt has + // correct l18n + + // Override programs executable basename as application name. + app.setApplicationName ("WSJT-X Record Time Signal"); + app.setApplicationVersion (version ()); + + QCommandLineParser parser; + parser.setApplicationDescription ( + "\nTool to determine and experiment with QAudioInput latencies\n\n" + "\tUse the -I option to list available recording device numbers\n" + ); + auto help_option = parser.addHelpOption (); + auto version_option = parser.addVersionOption (); + + parser.addOptions ({ + {{"I", "list-audio-inputs"}, + app.translate ("main", "List the available audio input devices")}, + {{"s", "start-time"}, + app.translate ("main", "Record from seconds"), + app.translate ("main", "start-time")}, + {{"d", "duration"}, + app.translate ("main", "Recording seconds"), + app.translate ("main", "duration")}, + {{"o", "output"}, + app.translate ("main", "Save output as "), + app.translate ("main", "output-file")}, + {{"f", "force"}, + app.translate ("main", "Overwrite existing file")}, + {{"r", "sample-rate"}, + app.translate ("main", "Record at "), + app.translate ("main", "sample-rate")}, + {{"c", "num-channels"}, + app.translate ("main", "Record channels"), + app.translate ("main", "num")}, + {{"R", "recording-device-number"}, + app.translate ("main", "Record from "), + app.translate ("main", "device-number")}, + {{"n", "notify-interval"}, + app.translate ("main", "use notify signals every milliseconds, zero to use a timer"), + app.translate ("main", "interval")}, + {{"b", "buffer-size"}, + app.translate ("main", "audio buffer size "), + app.translate ("main", "frames")}, + }); + parser.process (app); + + auto input_devices = QAudioDeviceInfo::availableDevices (QAudio::AudioInput); + if (parser.isSet ("I")) + { + int n {0}; + for (auto const& device : input_devices) + { + qtout << ++n << " - [" << device.deviceName () << ']' << endl; + } + return 0; + } + + bool ok; + int start = parser.value ("s").toInt (&ok); + if (!ok) throw std::invalid_argument {"start time not a number"}; + int duration = parser.value ("d").toInt (&ok); + if (!ok) throw std::invalid_argument {"duration not a number"}; + int sample_rate {48000}; + if (parser.isSet ("r")) + { + sample_rate = parser.value ("r").toInt (&ok); + if (!ok) throw std::invalid_argument {"sample rate not a number"}; + } + int num_channels {2}; + if (parser.isSet ("c")) + { + num_channels = parser.value ("c").toInt (&ok); + if (!ok) throw std::invalid_argument {"channel count not a number"}; + } + int notify_interval {0}; + if (parser.isSet ("n")) + { + notify_interval = parser.value ("n").toInt (&ok); + if (!ok) throw std::invalid_argument {"notify interval not a number"}; + } + int buffer_size {0}; + if (parser.isSet ("b")) + { + buffer_size = parser.value ("b").toInt (&ok); + if (!ok) throw std::invalid_argument {"buffer size not a number"}; + } + int input_device {0}; + if (parser.isSet ("R")) + { + input_device = parser.value ("R").toInt (&ok); + if (!ok || 0 >= input_device || input_device > input_devices.size ()) + { + throw std::invalid_argument {"invalid recording device"}; + } + } + if (!parser.isSet ("o")) throw std::invalid_argument {"output file required"}; + QFileInfo ofi {parser.value ("o")}; + if (!ofi.suffix ().size () && ofi.fileName ()[ofi.fileName ().size () - 1] != QChar {'.'}) + { + ofi.setFile (ofi.filePath () + ".wav"); + } + if (!parser.isSet ("f") && ofi.isFile ()) + { + throw std::invalid_argument {"set the `-force' option to overwrite an existing output file"}; + } + + QAudioFormat audio_format; + audio_format.setSampleRate (sample_rate); + audio_format.setChannelCount (num_channels); + audio_format.setSampleSize (16); + audio_format.setSampleType (QAudioFormat::SignedInt); + audio_format.setCodec ("audio/pcm"); + + auto source = input_device ? input_devices[input_device] : QAudioDeviceInfo::defaultInputDevice (); + if (!source.isFormatSupported (audio_format)) + { + qtout << "warning, requested format not supported, using nearest" << endl; + audio_format = source.nearestFormat (audio_format); + } + + // run the application + Recorder record {start, duration, ofi.filePath (), source, audio_format, notify_interval, buffer_size}; + QObject::connect (&record, &Recorder::done, &app, &QCoreApplication::quit); + return app.exec(); + } + catch (std::exception const& e) + { + std::cerr << "Error: " << e.what () << '\n'; + } + catch (...) + { + std::cerr << "Unexpected fatal error\n"; + throw; // hoping the runtime might tell us more about the exception + } + return -1; +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 3fbb879dc..eb8ece5ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1289,6 +1289,9 @@ target_link_libraries (ft4sim_mult wsjt_fort wsjt_cxx) add_executable (ft4d lib/ft4/ft4d.f90 wsjtx.rc) target_link_libraries (ft4d wsjt_fort wsjt_cxx) +add_executable (record_time_signal Audio/tools/record_time_signal.cpp) +target_link_libraries (record_time_signal wsjt_cxx wsjt_qtmm wsjt_qt) + endif(WSJT_BUILD_UTILS) # build the main application