diff options
-rw-r--r-- | OutputBuffer.cpp | 59 | ||||
-rw-r--r-- | OutputBuffer.h | 49 | ||||
-rw-r--r-- | radio_output.cpp | 152 | ||||
-rw-r--r-- | radio_output.h | 75 | ||||
-rw-r--r-- | rtlfmradio.pro | 6 | ||||
-rw-r--r-- | rtlfmradiotunercontrol.cpp | 113 |
6 files changed, 189 insertions, 265 deletions
diff --git a/OutputBuffer.cpp b/OutputBuffer.cpp new file mode 100644 index 0000000..c6fe30a --- /dev/null +++ b/OutputBuffer.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017 Konsulko Group + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <OutputBuffer.h> + +OutputBuffer::OutputBuffer(const QAudioFormat &format, QObject *parent) + : QIODevice(parent) +{ + m_sample_size = format.bytesPerFrame(); +} + +OutputBuffer::~OutputBuffer() +{ +} + +void OutputBuffer::start() +{ + open(QIODevice::ReadWrite); +} + +void OutputBuffer::stop() +{ + close(); + QMutexLocker locker(&m_mutex); + m_buffer.clear(); +} + +qint64 OutputBuffer::readData(char *data, qint64 len) +{ + QMutexLocker locker(&m_mutex); + int n = qMin((qint64) m_buffer.size(), len); + + // Make sure reads are in units of the sample size + n = n - (n % m_sample_size); + + if (!m_buffer.isEmpty()) { + memcpy(data, m_buffer.constData(), n); + m_buffer.remove(0, n); + } + return n; +} + +qint64 OutputBuffer::writeData(const char *data, qint64 len) +{ + QMutexLocker locker(&m_mutex); + m_buffer.append(data, len); + return len; +} + +qint64 OutputBuffer::bytesAvailable() +{ + QMutexLocker locker(&m_mutex); + return m_buffer.size() + QIODevice::bytesAvailable(); +} diff --git a/OutputBuffer.h b/OutputBuffer.h new file mode 100644 index 0000000..d1773ae --- /dev/null +++ b/OutputBuffer.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 Konsulko Group + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Simple buffer device to act as input device for QAudioInput. + * + * It basically wraps QByteArray with some additional locking that + * the QBuffer class does not provide. It also ensures that reads + * return in units of the given format's sample size. This is + * required to handle the RTL-SDR library output sometimes arriving + * with half a sample at the end. QAudioOutput will blindly pass + * that half sample along and trigger a PulseAudio error if we do + * not handle it here. + */ + +#ifndef OUTPUTBUFFER_H +#define OUTPUTBUFFER_H + +#include <QtCore/QIODevice> +#include <QtCore/QMutex> +#include <QtMultimedia/QAudioFormat> + +class OutputBuffer : public QIODevice +{ + Q_OBJECT + +public: + OutputBuffer(const QAudioFormat &format, QObject *parent); + ~OutputBuffer(); + + void start(); + void stop(); + + qint64 readData(char *data, qint64 maxlen); + qint64 writeData(const char *data, qint64 len); + qint64 bytesAvailable(); + +private: + QMutex m_mutex; + QByteArray m_buffer; + unsigned int m_sample_size; +}; + +#endif // OUTPUTBUFFER_H diff --git a/radio_output.cpp b/radio_output.cpp deleted file mode 100644 index 350bf75..0000000 --- a/radio_output.cpp +++ /dev/null @@ -1,152 +0,0 @@ -/* - * A standalone AM/FM Radio QML plugin (for RTL2832U and Maxim hardware) - * Copyright © 2015-2016 Manuel Bachmann <manuel.bachmann@iot.bzh> - * Copyright © 2016 Scott Murray <scott.murray@konsulko.com> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <iostream> -#include "radio_output.h" -#include "rtl_fm.h" - -RadioOutputAlsa::RadioOutputAlsa() : RadioOutputImplementation(), - dev(NULL), - hw_params(NULL) -{ - unsigned int rate = 24000; - - if (snd_pcm_open(&dev, "default", SND_PCM_STREAM_PLAYBACK, 0) < 0) { - std::cerr << "Could not open primary ALSA device" << std::endl; - works = false; - return; - } - - snd_pcm_hw_params_malloc(&hw_params); - snd_pcm_hw_params_any(dev, hw_params); - - snd_pcm_hw_params_set_access (dev, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); - snd_pcm_hw_params_set_format (dev, hw_params, SND_PCM_FORMAT_S16_LE); - snd_pcm_hw_params_set_rate_near (dev, hw_params, &rate, 0); - snd_pcm_hw_params_set_channels (dev, hw_params, 2); - - if (snd_pcm_hw_params (dev, hw_params) < 0) { - std::cerr << "Could not set hardware parameters" << std::endl; - works = false; - } else { - works = true; - } - snd_pcm_hw_params_free (hw_params); - - snd_pcm_prepare (dev); -} - -RadioOutputAlsa::~RadioOutputAlsa() -{ - snd_pcm_close (dev); -} - -bool RadioOutputAlsa::play(void *buf, int len) -{ - int16_t *cbuf = (int16_t *)buf; - int frames = len / 2; - int res; - - if ((res = snd_pcm_writei(dev, cbuf, frames)) != frames) { - snd_pcm_recover(dev, res, 0); - snd_pcm_prepare(dev); - } - //snd_pcm_drain(dev); - - return true; -} - - -RadioOutputPulse::RadioOutputPulse() : RadioOutputImplementation(), - pa(NULL), - pa_spec(NULL) -{ - int error; - - pa_spec = (pa_sample_spec*) malloc(sizeof(pa_sample_spec)); - pa_spec->format = PA_SAMPLE_S16LE; - pa_spec->rate = 24000; - pa_spec->channels = 2; - - if (!(pa = pa_simple_new(NULL, "qtmultimedia-rtlfm-radio-plugin", PA_STREAM_PLAYBACK, NULL, - "radio-output", pa_spec, NULL, NULL, &error))) { - std::cerr << "Error connecting to PulseAudio : " << pa_strerror(error) << std::endl; - works = false; - } else { - std::cerr << "RadioOutputPulse::RadioOutputPulse: Connected to PulseAudio" << std::endl; - works = true; - } - - extra = 0; - output_buf = new unsigned char[RTL_FM_MAXIMUM_BUF_LENGTH]; - - free(pa_spec); -} - -RadioOutputPulse::~RadioOutputPulse() -{ - pa_simple_free(pa); - delete [] output_buf; -} - -bool RadioOutputPulse::play(void *buf, int len) -{ - int error; - size_t n = len * 2; - void *p; - - if (!buf) { - std::cerr << "Error buf == null!" << std::endl; - return false; - } - - // Handle the rtl_fm code giving us an odd number of samples, which - // PA does not like. This extra buffer copying approach is not - // particularly efficient, but works for now. It looks feasible to - // hack in something in the demod and output thread routines in - // rtl_fm.c to handle it there if more performance is required. - p = output_buf; - if(extra) { - memcpy(output_buf, extra_buf, sizeof(int16_t)); - if((extra + len) % 2) { - // We end up with len + 1 samples, n remains the same, store the extra - memcpy(output_buf + sizeof(int16_t), buf, n - 2); - memcpy(extra_buf, ((unsigned char*) buf) + n - 2, sizeof(int16_t)); - } else { - // We end up with an extra sample - memcpy(output_buf + sizeof(int16_t), buf, n); - n += 2; - extra = 0; - } - } else if(len % 2) { - // We have an extra sample, store it, and decrease n - n -= 2; - memcpy(output_buf + sizeof(int16_t), buf, n); - memcpy(extra_buf, ((unsigned char*) buf) + n, sizeof(int16_t)); - extra = 1; - } else { - p = buf; - } - - if (pa_simple_write(pa, p, n, &error) < 0) - std::cerr << "Error writing " << n << " bytes to PulseAudio : " << pa_strerror(error) << std::endl; - //pa_simple_drain(pa, &error); - - return true; -} diff --git a/radio_output.h b/radio_output.h deleted file mode 100644 index 5051101..0000000 --- a/radio_output.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - * A standalone AM/FM Radio QML plugin (for RTL2832U and Maxim hardware) - * Copyright © 2015-2016 Manuel Bachmann <manuel.bachmann@iot.bzh> - * Copyright © 2016 Scott Murray <scott.murray@konsulko.com> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#ifndef RADIO_OUTPUT_H -#define RADIO_OUTPUT_H - -#include <alsa/asoundlib.h> - -#include <pulse/simple.h> -#include <pulse/error.h> - - /* - * RadioOutputImplementation is a virtual class, with 2 implementations : - * - RadioOutputAlsa ; - * - RadioOutputPulse ; - */ -class RadioOutputImplementation -{ -public: - RadioOutputImplementation() {}; - virtual ~RadioOutputImplementation() {}; - - virtual bool play(void*, int) = 0; - - bool works; -}; - - -class RadioOutputAlsa : public RadioOutputImplementation -{ -public: - RadioOutputAlsa(); - ~RadioOutputAlsa() override; - -private: - bool play(void *, int) override; - - snd_pcm_t *dev; - snd_pcm_hw_params_t *hw_params; -}; - -class RadioOutputPulse : public RadioOutputImplementation -{ -public: - RadioOutputPulse(); - ~RadioOutputPulse() override; - -private: - bool play(void *, int) override; - - pa_simple *pa; - pa_sample_spec *pa_spec; - - unsigned int extra; - int16_t extra_buf[1]; - unsigned char *output_buf; -}; - -#endif // RTLSDR_RADIO_H diff --git a/rtlfmradio.pro b/rtlfmradio.pro index 5824ec7..200984e 100644 --- a/rtlfmradio.pro +++ b/rtlfmradio.pro @@ -1,11 +1,11 @@ TEMPLATE = lib CONFIG += plugin c++11 link_pkgconfig -PKGCONFIG += librtlsdr libpulse-simple alsa +PKGCONFIG += librtlsdr TARGET = rtlfmradio QT = multimedia -HEADERS = rtlfmradioplugin.h rtlfmradioservice.h rtlfmradiotunercontrol.h radio_output.h rtl_fm.h convenience/convenience.h -SOURCES = rtlfmradioplugin.cpp rtlfmradioservice.cpp rtlfmradiotunercontrol.cpp radio_output.cpp rtl_fm.c convenience/convenience.c +HEADERS = rtlfmradioplugin.h rtlfmradioservice.h rtlfmradiotunercontrol.h OutputBuffer.h rtl_fm.h convenience/convenience.h +SOURCES = rtlfmradioplugin.cpp rtlfmradioservice.cpp rtlfmradiotunercontrol.cpp OutputBuffer.cpp rtl_fm.c convenience/convenience.c DISTFILES += rtlfmradio.json target.path = $$[QT_INSTALL_PLUGINS]/mediaservice diff --git a/rtlfmradiotunercontrol.cpp b/rtlfmradiotunercontrol.cpp index 27a8d08..937411a 100644 --- a/rtlfmradiotunercontrol.cpp +++ b/rtlfmradiotunercontrol.cpp @@ -1,6 +1,6 @@ /* * Copyright (C) 2016, The Qt Company Ltd. All Rights Reserved. - * Copyright (C) 2016, Scott Murray <scott.murray@konsulko.com> + * Copyright (C) 2016, 2017 Konsulko Group * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -8,12 +8,13 @@ */ #include "rtlfmradiotunercontrol.h" -#include "radio_output.h" #include "rtl_fm.h" #include <QtCore/QDebug> #include <QtCore/QTimer> #include <QtCore/QSettings> +#include <QtMultimedia/QAudioOutput> +#include "OutputBuffer.h" // Structure to describe FM band plans, all values in Hz. struct fmBandPlan { @@ -22,12 +23,23 @@ struct fmBandPlan { unsigned int freqStep; }; +// Structure to hold output context for RTL-SDR callback +struct OutputContext { + OutputBuffer *buffer; + bool muted; +}; + class RtlFmRadioTunerControl::Private { public: Private(RtlFmRadioTunerControl *parent); ~Private(); + bool muted() { return mOutputContext.muted; } + void setMuted(bool muted) { mOutputContext.muted = muted; } + void outputStart(void); + void outputStop(void); + private: RtlFmRadioTunerControl *q; @@ -38,7 +50,6 @@ public: QRadioTuner::StereoMode stereoMode; int signalStrength; int volume; - static bool muted; bool searching; QRadioTuner::Error error; QString errorString; @@ -49,6 +60,13 @@ public: struct fmBandPlan fmBandPlan; private: + void outputInit(void); + + static void rtlOutputThreadFunc(int16_t *result, int result_len, void *ctx); + + QAudioOutput* mAudioOutput; + OutputContext mOutputContext; + QMap<QString, struct fmBandPlan> knownFmBandPlans = { { QString::fromUtf8("US"), { .minFreq = 87900000, .maxFreq = 107900000, .freqStep = 200000 } }, { QString::fromUtf8("JP"), { .minFreq = 76100000, .maxFreq = 89900000, .freqStep = 100000 } }, @@ -56,14 +74,8 @@ private: { QString::fromUtf8("ITU-1"), { .minFreq = 87500000, .maxFreq = 108000000, .freqStep = 50000 } }, { QString::fromUtf8("ITU-2"), { .minFreq = 87900000, .maxFreq = 107900000, .freqStep = 50000 } } }; - - static void output_thread_fn(int16_t *result, int result_len, void *ctx); - static RadioOutputImplementation *mRadioOutput; }; -bool RtlFmRadioTunerControl::Private::muted; -RadioOutputImplementation *RtlFmRadioTunerControl::Private::mRadioOutput; - RtlFmRadioTunerControl::Private::Private(RtlFmRadioTunerControl *parent) : q(parent), state(QRadioTuner::StoppedState), @@ -76,28 +88,11 @@ RtlFmRadioTunerControl::Private::Private(RtlFmRadioTunerControl *parent) searchOne(true), step(0) { - mRadioOutput = NULL; - muted = false; - - // Initialize output - // Note that the optional ALSA support is currently not entirely - // functional and needs further debugging. - char *impl_env = getenv("RADIO_OUTPUT"); - if (impl_env) { - if (strcasecmp(impl_env, "Pulse") == 0) - mRadioOutput = new RadioOutputPulse(); - if (strcasecmp(impl_env, "Alsa") == 0) - mRadioOutput = new RadioOutputAlsa(); - } - if (!mRadioOutput) { - mRadioOutput = new RadioOutputPulse(); - if (!mRadioOutput->works) - mRadioOutput = new RadioOutputAlsa(); - } + outputInit(); // Initialize RTL-SDR dongle present = false; - if(rtl_fm_init(frequency, 200000, 48000, output_thread_fn, NULL) < 0) { + if(rtl_fm_init(frequency, 200000, 48000, rtlOutputThreadFunc, &mOutputContext) < 0) { qDebug("%s: no RTL USB adapter?", Q_FUNC_INFO); } else { present = true; @@ -132,15 +127,59 @@ RtlFmRadioTunerControl::Private::Private(RtlFmRadioTunerControl *parent) }); } -void RtlFmRadioTunerControl::Private::output_thread_fn(int16_t *result, int result_len, void *ctx) +void RtlFmRadioTunerControl::Private::outputInit(void) { - if (!muted) - mRadioOutput->play((void*) result, result_len); + // Set up the format + QAudioFormat format; + format.setSampleRate(24000); + format.setChannelCount(2); + format.setSampleSize(16); + format.setCodec("audio/pcm"); + format.setByteOrder(QAudioFormat::LittleEndian); + format.setSampleType(QAudioFormat::SignedInt); + + QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice()); + if (!info.isFormatSupported(format)) { + qWarning() << "Raw audio format not supported by backend, cannot play audio."; + return; + } + + // Create output + mAudioOutput = new QAudioOutput(format); + mAudioOutput->setCategory(QString("radio")); + + // Initialize context to connect RTL-SDR library to QAudioOutput, + // including output buffer and muted state + mOutputContext.buffer = new OutputBuffer(format, q); + mOutputContext.muted = false; +} + +void RtlFmRadioTunerControl::Private::outputStart(void) +{ + mOutputContext.buffer->start(); + mAudioOutput->start(mOutputContext.buffer); +} + +void RtlFmRadioTunerControl::Private::outputStop(void) +{ + mAudioOutput->stop(); + mOutputContext.buffer->stop(); +} + +void RtlFmRadioTunerControl::Private::rtlOutputThreadFunc(int16_t *result, int result_len, void *ctx) +{ + const char *p = (const char *) result; + OutputContext *context = reinterpret_cast<OutputContext*>(ctx); + + if (!context->muted) { + context->buffer->writeData(p, result_len * 2); + } } RtlFmRadioTunerControl::Private::~Private() { - delete mRadioOutput; + delete mAudioOutput; + delete mOutputContext.buffer; rtl_fm_cleanup(); } @@ -174,6 +213,8 @@ void RtlFmRadioTunerControl::setBand(QRadioTuner::Band band) return; d->band = band; emit bandChanged(band); +#else + Q_UNUSED(band); #endif } @@ -297,14 +338,14 @@ void RtlFmRadioTunerControl::setVolume(int volume) bool RtlFmRadioTunerControl::isMuted() const { - return d->muted; + return d->muted(); } void RtlFmRadioTunerControl::setMuted(bool muted) { - if (d->muted == muted) + if (d->muted() == muted) return; - d->muted = muted; + d->setMuted(muted); emit mutedChanged(muted); } @@ -366,6 +407,7 @@ void RtlFmRadioTunerControl::start() //rtl_fm_stop(); rtl_fm_start(); + d->outputStart(); d->state = QRadioTuner::ActiveState; emit stateChanged(d->state); qDebug() << "RtlFmRadioTunerControl::start - exit\n"; @@ -383,6 +425,7 @@ void RtlFmRadioTunerControl::stop() } rtl_fm_stop(); + d->outputStop(); d->state = QRadioTuner::StoppedState; emit stateChanged(d->state); qDebug() << "RtlFmRadioTunerControl::stop - exit\n"; |