diff options
| -rw-r--r-- | Makefile.am | 6 | ||||
| -rw-r--r-- | pica-client/audiodevice.cpp | 29 | ||||
| -rw-r--r-- | pica-client/audiodevice.h | 22 | ||||
| -rw-r--r-- | pica-client/audiovideocallcontroller.cpp | 88 | ||||
| -rw-r--r-- | pica-client/audiovideocallcontroller.h | 17 | ||||
| -rw-r--r-- | pica-client/toneplayer/toneplayer.cpp | 445 | ||||
| -rw-r--r-- | pica-client/toneplayer/toneplayer.h | 81 |
7 files changed, 687 insertions, 1 deletions
diff --git a/Makefile.am b/Makefile.am index eccdf2f..a603801 100644 --- a/Makefile.am +++ b/Makefile.am @@ -95,6 +95,8 @@ pica-client/filetransfercontroller.cpp \ pica-client/filetransfercontroller.h \ pica-client/sound.cpp \ pica-client/sound.h \ +pica-client/toneplayer/toneplayer.cpp \ +pica-client/toneplayer/toneplayer.h \ pica-client/settings.cpp \ pica-client/settings.h \ pica-client/mediadevice.cpp \ @@ -152,6 +154,7 @@ moc_showpicaiddialog.cpp \ moc_dhparam.cpp \ moc_filetransfercontroller.cpp \ moc_nodesdialog.cpp \ +moc_toneplayer.cpp \ ui_mainwindow.h moc_%.cpp: pica-client/%.h @@ -160,6 +163,9 @@ moc_%.cpp: pica-client/%.h moc_%.cpp: pica-client/dialogs/%.h $(MOC) $< -o $@ +moc_%.cpp: pica-client/toneplayer/%.h + $(MOC) $< -o $@ + pica-client/mainwindow.cpp: ui_mainwindow.h #ui_mainwindow.h: pica-client/mainwindow.ui diff --git a/pica-client/audiodevice.cpp b/pica-client/audiodevice.cpp index d2995e3..17dd1b9 100644 --- a/pica-client/audiodevice.cpp +++ b/pica-client/audiodevice.cpp @@ -1,3 +1,19 @@ +/* + (c) Copyright 2012 - 2018 Anton Sviridenko + https://picapica.im + + 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, version 3. + + 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 <https://www.gnu.org/licenses/>. +*/ #include "audiodevice.h" #ifdef Q_OS_LINUX @@ -68,3 +84,16 @@ QList<MediaDeviceInfo> AudioDevice::Enumerate(enum MediaDeviceStreamDirection di #endif return result; } + +QString AudioDevice::PlatformDriverName() +{ +#if defined(Q_OS_LINUX) + return QStringLiteral("alsa"); +#elif defined(Q_OS_MACOS) || defined(Q_OS_MAC) + return QStringLiteral("audiotoolbox"); +#elif defined(Q_OS_WIN) + return QStringLiteral("dsound"); +#else + return QStringLiteral("oss"); +#endif +} diff --git a/pica-client/audiodevice.h b/pica-client/audiodevice.h index 472dd03..09edbdc 100644 --- a/pica-client/audiodevice.h +++ b/pica-client/audiodevice.h @@ -1,3 +1,19 @@ +/* + (c) Copyright 2012 - 2018 Anton Sviridenko + https://picapica.im + + 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, version 3. + + 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 <https://www.gnu.org/licenses/>. +*/ #ifndef AUDIODEVICE_H #define AUDIODEVICE_H @@ -12,6 +28,12 @@ public: virtual void Play(); virtual void Close(); virtual QList<MediaDeviceInfo> Enumerate(enum MediaDeviceStreamDirection dir); + + // Name of the FFmpeg libavdevice muxer/demuxer for the host platform's + // native audio API (e.g. "alsa" on Linux, "audiotoolbox" on macOS, + // "dsound" on Windows). Shared by every component that opens an audio + // device through FFmpeg (capture, playback, TonePlayer). + static QString PlatformDriverName(); }; #endif // AUDIODEVICE_H diff --git a/pica-client/audiovideocallcontroller.cpp b/pica-client/audiovideocallcontroller.cpp index 9eeb89c..65d9f3f 100644 --- a/pica-client/audiovideocallcontroller.cpp +++ b/pica-client/audiovideocallcontroller.cpp @@ -1,8 +1,26 @@ +/* + (c) Copyright 2012 - 2018 Anton Sviridenko + https://picapica.im + + 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, version 3. + + 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 <https://www.gnu.org/licenses/>. +*/ #include "audiovideocallcontroller.h" #include "globals.h" #include "skynet.h" +#include "settings.h" #include <QByteArray> #include <QDebug> +#include <QMetaObject> AudioVideoCallController::AudioVideoCallController(QObject *parent) : QObject(parent), callwindow(0), is_active(false) @@ -12,23 +30,86 @@ AudioVideoCallController::AudioVideoCallController(QObject *parent) connect(skynet, SIGNAL(CallAccepted(QByteArray)), this, SLOT(call_accepted(QByteArray))); connect(skynet, SIGNAL(CallRejected(QByteArray)), this, SLOT(call_rejected(QByteArray))); connect(skynet, SIGNAL(CallHungup(QByteArray)), this, SLOT(call_hungup(QByteArray))); + + // Earpiece tones (ringback/busy/unreachable) and the incoming call bell + // are played by two TonePlayer instances, each living on its own + // QThread since TonePlayer::play() blocks the calling thread for the + // duration of the tone sequence. + toneplayer = new TonePlayer(); + toneplayer->moveToThread(&toneplayer_thread); + toneplayer_thread.start(); + + ringtoneplayer = new TonePlayer(); + ringtoneplayer->moveToThread(&ringtoneplayer_thread); + ringtoneplayer_thread.start(); +} + +AudioVideoCallController::~AudioVideoCallController() +{ + stopTones(); + + toneplayer_thread.quit(); + toneplayer_thread.wait(); + delete toneplayer; + + ringtoneplayer_thread.quit(); + ringtoneplayer_thread.wait(); + delete ringtoneplayer; +} + +void AudioVideoCallController::playEarpieceTone(void (TonePlayer::*tone)()) +{ + Settings st(config_dbname); + QString dev = st.loadValue("audio.playback_device", "default").toString(); + + toneplayer->stop(); + QMetaObject::invokeMethod(toneplayer, "setDeviceName", Qt::QueuedConnection, Q_ARG(QString, dev)); + if (tone == &TonePlayer::playCallInProgress) + QMetaObject::invokeMethod(toneplayer, "playCallInProgress", Qt::QueuedConnection); + else if (tone == &TonePlayer::playUnreachable) + QMetaObject::invokeMethod(toneplayer, "playUnreachable", Qt::QueuedConnection); + else if (tone == &TonePlayer::playBusy) + QMetaObject::invokeMethod(toneplayer, "playBusy", Qt::QueuedConnection); +} + +void AudioVideoCallController::playRingTone() +{ + Settings st(config_dbname); + QString dev = st.loadValue("audio.ring_device", "default").toString(); + + ringtoneplayer->stop(); + QMetaObject::invokeMethod(ringtoneplayer, "setDeviceName", Qt::QueuedConnection, Q_ARG(QString, dev)); + QMetaObject::invokeMethod(ringtoneplayer, "playClassicRingtone", Qt::QueuedConnection); +} + +void AudioVideoCallController::stopTones() +{ + toneplayer->stop(); + ringtoneplayer->stop(); } void AudioVideoCallController::call_failed(QString reason) { qDebug() << "Call failed: " << reason << "\n"; + /* play unreachable */ + playEarpieceTone(&TonePlayer::playUnreachable); } void AudioVideoCallController::call_accepted(QByteArray peer_id) { + stopTones(); } void AudioVideoCallController::call_rejected(QByteArray peer_id) { + /* play busy tone */ + playEarpieceTone(&TonePlayer::playBusy); } void AudioVideoCallController::call_hungup(QByteArray peer_id) { + /* play busy tone */ + playEarpieceTone(&TonePlayer::playBusy); } void AudioVideoCallController::start_call(QByteArray peer_id) @@ -44,6 +125,8 @@ void AudioVideoCallController::start_call(QByteArray peer_id) connect(callwindow, SIGNAL(start_call_pressed()), this, SLOT(initiate_call())); connect(callwindow, SIGNAL(hang_call_pressed()), this, SLOT(end_call())); callwindow->show(); + /* play ringing tone */ + playEarpieceTone(&TonePlayer::playCallInProgress); } void AudioVideoCallController::call_from(QByteArray peer_id) @@ -62,7 +145,7 @@ void AudioVideoCallController::call_from(QByteArray peer_id) callwindow->show(); /* play ring tone */ - + playRingTone(); } void AudioVideoCallController::initiate_call() @@ -72,11 +155,14 @@ void AudioVideoCallController::initiate_call() void AudioVideoCallController::accept_call() { + stopTones(); skynet->AcceptCall(m_peer_id); } void AudioVideoCallController::end_call() { + stopTones(); + if (is_active) skynet->HangupCall(m_peer_id); else diff --git a/pica-client/audiovideocallcontroller.h b/pica-client/audiovideocallcontroller.h index 82e1dc8..08f94af 100644 --- a/pica-client/audiovideocallcontroller.h +++ b/pica-client/audiovideocallcontroller.h @@ -18,15 +18,18 @@ #define AUDIOVIDEOCALLCONTROLLER_H #include <QObject> +#include <QThread> #include "callwindow.h" #include "audiodevice.h" #include "videodevice.h" +#include "toneplayer/toneplayer.h" class AudioVideoCallController : public QObject { Q_OBJECT public: explicit AudioVideoCallController(QObject *parent = nullptr); + ~AudioVideoCallController(); // is call active (i.e. are voice/video packets exchanged now) bool const isActive() {return is_active;} void processCall(); @@ -46,6 +49,20 @@ private: VideoDevice *cam; bool is_active; QByteArray m_peer_id; + + // Earpiece tones (ringback, busy, special information/unreachable), + // played through the "audio.playback_device" setting. + TonePlayer *toneplayer; + QThread toneplayer_thread; + + // Incoming call bell, played through the "audio.ring_device" setting. + TonePlayer *ringtoneplayer; + QThread ringtoneplayer_thread; + + void playEarpieceTone(void (TonePlayer::*tone)()); + void playRingTone(); + void stopTones(); + private slots: // button handlers void initiate_call(); diff --git a/pica-client/toneplayer/toneplayer.cpp b/pica-client/toneplayer/toneplayer.cpp new file mode 100644 index 0000000..d40e1e9 --- /dev/null +++ b/pica-client/toneplayer/toneplayer.cpp @@ -0,0 +1,445 @@ +/* + (c) Copyright 2012 - 2026 Anton Sviridenko + https://picapica.im + + 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, version 3. + + 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 <https://www.gnu.org/licenses/>. +*/ +#include "toneplayer.h" +#include "../audiodevice.h" + +#include <QDebug> +#include <cmath> + +// FFmpeg headers +extern "C" { +#include <libavformat/avformat.h> +#include <libavdevice/avdevice.h> +#include <libavcodec/avcodec.h> +#include <libavutil/opt.h> +#include <libavutil/channel_layout.h> +#include <libavutil/samplefmt.h> +#include <libavutil/frame.h> +#include <libavutil/mem.h> +#include <libavutil/error.h> +} + +// FFmpeg 5.1 (libavutil 57.24.100) replaced AVCodecContext/AVFrame's +// channels + channel_layout fields with the AVChannelLayout ch_layout API. +// The old fields were deprecated then and removed outright in FFmpeg 7.0. +// The legacy branch keeps us building on FFmpeg 4.x (Ubuntu 20.04 / 22.04). +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 24, 100) +# define TP_HAVE_CH_LAYOUT 1 +#else +# define TP_HAVE_CH_LAYOUT 0 +#endif + +TonePlayer::TonePlayer(QObject *parent) + : QObject(parent), + m_abort(0), + m_deviceName("default"), + m_sampleRate(48000), + m_channels(1), // mono + m_volume(0.6) +{ +} + +TonePlayer::~TonePlayer() +{ + stop(); +} + +void TonePlayer::setSequence(const QVector<Tone> &sequence) +{ + m_sequence = sequence; +} + +void TonePlayer::stop() +{ + m_abort.storeRelaxed(1); +} + +void TonePlayer::setDeviceName(const QString &deviceName) +{ + m_deviceName = deviceName; +} + +void TonePlayer::setSampleRate(int sampleRate) +{ + m_sampleRate = sampleRate; +} + +void TonePlayer::setVolume(double vol) +{ + m_volume = qBound(0.0, vol, 1.0); +} + +void TonePlayer::playSequence(const QVector<Tone> &sequence) +{ + m_sequence = sequence; + m_abort.storeRelaxed(0); + play(); +} + +void TonePlayer::playCallInProgress() +{ + // "440:1500,0:3000,440:1500,0:3000,440:1500,0:0" + QVector<Tone> seq; + seq << Tone(440.0, 1500) << Tone(0.0, 3000) + << Tone(440.0, 1500) << Tone(0.0, 3000) + << Tone(440.0, 1500) << Tone(0.0, 0); + playSequence(seq); +} + +void TonePlayer::playUnreachable() +{ + // "950:330,1400:330,1800:330,0:0" + QVector<Tone> seq; + seq << Tone(950.0, 330) << Tone(1400.0, 330) << Tone(1800.0, 330) + << Tone(0.0, 0); + playSequence(seq); +} + +void TonePlayer::playBusy() +{ + // "440:400,0:400,440:400,0:400,440:400,0:0" + QVector<Tone> seq; + seq << Tone(440.0, 400) << Tone(0.0, 400) + << Tone(440.0, 400) << Tone(0.0, 400) + << Tone(440.0, 400) << Tone(0.0, 0); + playSequence(seq); +} + +void TonePlayer::playClassicRingtone() +{ + // Unlike playCallInProgress/playUnreachable/playBusy, which are steady + // single-frequency tones heard in the earpiece, this imitates the sound + // of an electromechanical bell ringer: the clapper striking the gongs + // produces a fast warble between two close pitches rather than one pure + // tone. We approximate that by alternating two frequencies every 40 ms + // (~12.5 Hz trill) for the classic 2 s on / 4 s off cadence, 3 bursts. + const int trillSegment_ms = 40; + const int ringDuration_ms = 2000; + const int silenceDuration_ms = 4000; + const double freqA = 1000.0; + const double freqB = 1200.0; + const int bursts = 3; + + QVector<Tone> seq; + for (int burst = 0; burst < bursts; ++burst) { + for (int t = 0; t < ringDuration_ms; t += trillSegment_ms) { + int segLen = qMin(trillSegment_ms, ringDuration_ms - t); + double freq = ((t / trillSegment_ms) % 2 == 0) ? freqA : freqB; + seq << Tone(freq, segLen); + } + seq << Tone(0.0, burst == bursts - 1 ? 0 : silenceDuration_ms); + } + playSequence(seq); +} + +static QString ff_errstr(int err) +{ + char buf[256] = {0}; + av_strerror(err, buf, sizeof(buf)); + return QString::fromLocal8Bit(buf); +} + +void TonePlayer::play() +{ + if (m_sequence.isEmpty()) { + emit finishedPlaying(); + return; + } + + avdevice_register_all(); +#if LIBAVFORMAT_VERSION_MAJOR < 58 + av_register_all(); + avcodec_register_all(); +#endif + + AVFormatContext *oc = nullptr; + const QByteArray deviceUtf8 = m_deviceName.toUtf8(); + const char *device = deviceUtf8.constData(); + const QByteArray driverUtf8 = AudioDevice::PlatformDriverName().toUtf8(); + const char *driver = driverUtf8.constData(); + + int ret = avformat_alloc_output_context2(&oc, nullptr, driver, device); + if (ret < 0 || !oc) { + QString msg = QString("Could not allocate '%1' output context: %2").arg(driver, ff_errstr(ret)); + qWarning() << msg; + emit errorOccured(msg); + return; + } + + const AVCodecID codec_id = AV_CODEC_ID_PCM_S16LE; + const AVCodec *codec = avcodec_find_encoder(codec_id); + if (!codec) { + QString msg = "PCM encoder not found"; + qWarning() << msg; + emit errorOccured(msg); + avformat_free_context(oc); + return; + } + + AVCodecContext *codecCtx = avcodec_alloc_context3(codec); + if (!codecCtx) { + QString msg = "Could not alloc codec context"; + qWarning() << msg; + emit errorOccured(msg); + avformat_free_context(oc); + return; + } + + codecCtx->sample_fmt = AV_SAMPLE_FMT_S16; + codecCtx->sample_rate = m_sampleRate; +#if TP_HAVE_CH_LAYOUT + av_channel_layout_default(&codecCtx->ch_layout, m_channels); +#else + codecCtx->channels = m_channels; + codecCtx->channel_layout = av_get_default_channel_layout(m_channels); +#endif + codecCtx->time_base = AVRational{1, m_sampleRate}; + + if ((ret = avcodec_open2(codecCtx, codec, nullptr)) < 0) { + QString msg = QString("Could not open codec: %1").arg(ff_errstr(ret)); + qWarning() << msg; + emit errorOccured(msg); + avcodec_free_context(&codecCtx); + avformat_free_context(oc); + return; + } + + AVStream *st = avformat_new_stream(oc, nullptr); + if (!st) { + QString msg = "Could not create stream"; + qWarning() << msg; + emit errorOccured(msg); + avcodec_free_context(&codecCtx); + avformat_free_context(oc); + return; + } + + if ((ret = avcodec_parameters_from_context(st->codecpar, codecCtx)) < 0) { + QString msg = QString("Failed to copy codec params: %1").arg(ff_errstr(ret)); + qWarning() << msg; + emit errorOccured(msg); + avcodec_free_context(&codecCtx); + avformat_free_context(oc); + return; + } + st->time_base = codecCtx->time_base; + + if (!(oc->oformat->flags & AVFMT_NOFILE)) { + if ((ret = avio_open(&oc->pb, device, AVIO_FLAG_WRITE)) < 0) { + QString msg = QString("Could not open '%1' device '%2': %3").arg(driver, m_deviceName, ff_errstr(ret)); + qWarning() << msg; + emit errorOccured(msg); + avcodec_free_context(&codecCtx); + avformat_free_context(oc); + return; + } + } + + if ((ret = avformat_write_header(oc, nullptr)) < 0) { + QString msg = QString("Error writing header: %1").arg(ff_errstr(ret)); + qWarning() << msg; + emit errorOccured(msg); + if (!(oc->oformat->flags & AVFMT_NOFILE)) avio_closep(&oc->pb); + avcodec_free_context(&codecCtx); + avformat_free_context(oc); + return; + } + + AVFrame *frame = av_frame_alloc(); + if (!frame) { + QString msg = "Could not allocate AVFrame"; + qWarning() << msg; + emit errorOccured(msg); + if (!(oc->oformat->flags & AVFMT_NOFILE)) avio_closep(&oc->pb); + avcodec_free_context(&codecCtx); + avformat_free_context(oc); + return; + } + + frame->format = codecCtx->sample_fmt; + frame->sample_rate = codecCtx->sample_rate; +#if TP_HAVE_CH_LAYOUT + if ((ret = av_channel_layout_copy(&frame->ch_layout, &codecCtx->ch_layout)) < 0) { + QString msg = QString("Could not copy channel layout to frame: %1").arg(ff_errstr(ret)); + qWarning() << msg; + emit errorOccured(msg); + if (!(oc->oformat->flags & AVFMT_NOFILE)) avio_closep(&oc->pb); + av_frame_free(&frame); + avcodec_free_context(&codecCtx); + avformat_free_context(oc); + return; + } +#else + frame->channels = codecCtx->channels; + frame->channel_layout = codecCtx->channel_layout; +#endif + + int frame_size = codecCtx->frame_size; + if (frame_size <= 0) frame_size = 1024; + + frame->nb_samples = frame_size; + if ((ret = av_frame_get_buffer(frame, 0)) < 0) { + QString msg = QString("Could not allocate initial frame buffer: %1 (frame_size=%2)") + .arg(ff_errstr(ret)).arg(frame_size); + qWarning() << msg; + emit errorOccured(msg); + if (!(oc->oformat->flags & AVFMT_NOFILE)) avio_closep(&oc->pb); + av_frame_free(&frame); + avcodec_free_context(&codecCtx); + avformat_free_context(oc); + return; + } + + int64_t pts = 0; + double phase = 0.0; + const double two_pi = 2.0 * M_PI; + +#if TP_HAVE_CH_LAYOUT + int chs = codecCtx->ch_layout.nb_channels; +#else + int chs = codecCtx->channels; +#endif + if (chs <= 0) chs = 1; + + for (int idx = 0; idx < m_sequence.size(); ++idx) { + if (m_abort.loadRelaxed()) break; + + const Tone &t = m_sequence[idx]; + if (t.frequency == 0.0 && t.duration_ms == 0) { + break; + } + + if (t.duration_ms <= 0) continue; + + int64_t total_samples = (int64_t)qRound((m_sampleRate * t.duration_ms) / 1000.0); + int64_t samples_written = 0; + + double phase_inc = 0.0; + if (t.frequency > 0.0) { + phase_inc = two_pi * t.frequency / double(m_sampleRate); + } + + while (samples_written < total_samples) { + if (m_abort.loadRelaxed()) break; + + int nb_samples = frame_size; + if (nb_samples > (int)(total_samples - samples_written)) + nb_samples = int(total_samples - samples_written); + + if ((ret = av_frame_make_writable(frame)) < 0) { + QString msg = QString("av_frame_make_writable failed: %1").arg(ff_errstr(ret)); + qWarning() << msg; + emit errorOccured(msg); + break; + } + frame->nb_samples = nb_samples; + + int16_t *out = reinterpret_cast<int16_t*>(frame->data[0]); + + for (int i = 0; i < nb_samples; ++i) { + double sampleVal = 0.0; + if (t.frequency > 0.0) { + sampleVal = sin(phase) * m_volume; + phase += phase_inc; + if (phase > 1e9 || phase < -1e9) phase = fmod(phase, two_pi); + } else { + sampleVal = 0.0; + } + + int16_t ival = static_cast<int16_t>(qBound(-1.0, sampleVal, 1.0) * 32767.0); + for (int ch = 0; ch < chs; ++ch) { + out[i * chs + ch] = ival; + } + } + + frame->pts = pts; + pts += frame->nb_samples; + + ret = avcodec_send_frame(codecCtx, frame); + if (ret < 0) { + QString msg = QString("Error sending frame to encoder: %1").arg(ff_errstr(ret)); + qWarning() << msg; + emit errorOccured(msg); + break; + } + + while (ret >= 0) { + AVPacket *pkt = av_packet_alloc(); + if (!pkt) { + QString msg = "av_packet_alloc failed"; + qWarning() << msg; + emit errorOccured(msg); + ret = AVERROR(ENOMEM); + break; + } + + ret = avcodec_receive_packet(codecCtx, pkt); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + av_packet_free(&pkt); + break; + } else if (ret < 0) { + QString msg = QString("Error encoding audio frame: %1").arg(ff_errstr(ret)); + qWarning() << msg; + emit errorOccured(msg); + av_packet_free(&pkt); + break; + } + + pkt->stream_index = st->index; + + ret = av_interleaved_write_frame(oc, pkt); + if (ret < 0) { + QString msg = QString("Error writing packet to device: %1").arg(ff_errstr(ret)); + qWarning() << msg; + emit errorOccured(msg); + av_packet_free(&pkt); + break; + } + + av_packet_free(&pkt); + } + samples_written += nb_samples; + } + if (m_abort.loadRelaxed()) break; + } + + avcodec_send_frame(codecCtx, nullptr); + while (true) { + AVPacket *pkt = av_packet_alloc(); + if (!pkt) break; + ret = avcodec_receive_packet(codecCtx, pkt); + if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN)) { + av_packet_free(&pkt); + break; + } else if (ret < 0) { + av_packet_free(&pkt); + break; + } + pkt->stream_index = st->index; + av_interleaved_write_frame(oc, pkt); + av_packet_free(&pkt); + } + + av_write_trailer(oc); + + if (!(oc->oformat->flags & AVFMT_NOFILE)) avio_closep(&oc->pb); + av_frame_free(&frame); + avcodec_free_context(&codecCtx); + avformat_free_context(oc); + + emit finishedPlaying(); +} diff --git a/pica-client/toneplayer/toneplayer.h b/pica-client/toneplayer/toneplayer.h new file mode 100644 index 0000000..b3e237d --- /dev/null +++ b/pica-client/toneplayer/toneplayer.h @@ -0,0 +1,81 @@ +/* + (c) Copyright 2012 - 2026 Anton Sviridenko + https://picapica.im + + 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, version 3. + + 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 <https://www.gnu.org/licenses/>. +*/ +#pragma once + +#include <QObject> +#include <QVector> +#include <QAtomicInt> +#include <QtGlobal> +#include <QString> + +// Simple tone descriptor: frequency in Hz, duration in milliseconds. +struct Tone { + double frequency; // Hz + int duration_ms; // milliseconds + Tone(double f = 0.0, int d = 0): frequency(f), duration_ms(d) {} +}; + +class TonePlayer : public QObject +{ + Q_OBJECT +public: + explicit TonePlayer(QObject *parent = nullptr); + ~TonePlayer() override; + + // Replace the sequence to play. Call before play(). + void setSequence(const QVector<Tone> &sequence); + + // Stop playback early. This will cause play() to return. + Q_INVOKABLE void stop(); + + // Set the platform playback device name (e.g. "default" or "hw:0" on + // ALSA, a CoreAudio/DirectSound device name on macOS/Windows). Default: + // "default". Typically set from the "audio.playback_device" or + // "audio.ring_device" setting (see SettingsDialog). Q_INVOKABLE so it + // can be queued across threads via QMetaObject::invokeMethod(), the + // same way play() is invoked when TonePlayer lives on its own QThread. + Q_INVOKABLE void setDeviceName(const QString &deviceName); + + // Set playback parameters (optional before play) + void setSampleRate(int sampleRate); + void setVolume(double vol); // 0.0 .. 1.0 + +signals: + void finishedPlaying(); + void errorOccured(const QString &err); + +public slots: + void play(); + + // Standard telephony tone sequences. Each replaces the current sequence + // and plays it to completion (blocking, like play()). + void playCallInProgress(); // ringing tone: 440 Hz, 1.5 s on / 3 s off, 3 bursts + void playUnreachable(); // special information tone: 950/1400/1800 Hz, 330 ms each + void playBusy(); // busy tone: 440 Hz, 400 ms on / 400 ms off, 3 bursts + void playClassicRingtone(); // electromechanical bell: warbling trill, 2 s on / 4 s off, 3 bursts + +private: + // Set the sequence, clear any pending stop request and play it. + void playSequence(const QVector<Tone> &sequence); + + QVector<Tone> m_sequence; + QAtomicInt m_abort; + QString m_deviceName; + int m_sampleRate; + int m_channels; // always 1 (mono) by design + double m_volume; +}; |
