mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
Fix 8bit playback Enable 24bit playback Enable 32bit playback Upmix mono to stereo Set the sample format appropriately based upon the bits per channel in the format chunk instead of defaulting to 16 bits
264 lines
7.2 KiB
C++
264 lines
7.2 KiB
C++
/**
|
|
* @file NotificationAudio.cpp
|
|
* @brief Implementation of NotificationAudio class
|
|
*/
|
|
#include "NotificationAudio.h"
|
|
#include "BWFFile.h"
|
|
#include "SoundOutput.h"
|
|
|
|
#include <QLoggingCategory>
|
|
|
|
Q_DECLARE_LOGGING_CATEGORY(notificationaudio_js8)
|
|
|
|
/******************************************************************************/
|
|
// Public Implementation
|
|
/******************************************************************************/
|
|
/**
|
|
* @brief Constructs a NotificationAudio object.
|
|
* @param parent The parent QObject.
|
|
* @return None.
|
|
*/
|
|
NotificationAudio::NotificationAudio(QObject *parent)
|
|
: QObject{parent}, m_stream{new SoundOutput} {
|
|
connect(m_stream.data(), &SoundOutput::status, this,
|
|
&NotificationAudio::status);
|
|
connect(m_stream.data(), &SoundOutput::error, this,
|
|
&NotificationAudio::error);
|
|
}
|
|
|
|
/**
|
|
* @brief Destructs the NotificationAudio object.
|
|
*/
|
|
NotificationAudio::~NotificationAudio() { stop(); }
|
|
|
|
/**
|
|
* @brief Handles status messages from the SoundOutput.
|
|
* @param message The status message.
|
|
*/
|
|
void NotificationAudio::status(QString const message) {
|
|
if (message == "Idle")
|
|
stop();
|
|
}
|
|
|
|
/**
|
|
* @brief Handles error messages from the SoundOutput.
|
|
* @param message The error message.
|
|
*/
|
|
void NotificationAudio::error(QString const message) {
|
|
qCDebug(notificationaudio_js8) << "notification error:" << message;
|
|
}
|
|
|
|
/**
|
|
* @brief Sets the audio device and buffer size.
|
|
* @param device The QAudioDevice to use.
|
|
* @param msBuffer The buffer size in milliseconds.
|
|
*/
|
|
void NotificationAudio::setDevice(QAudioDevice const &device,
|
|
unsigned const msBuffer) {
|
|
m_device = device;
|
|
m_msBuffer = msBuffer;
|
|
}
|
|
|
|
/**
|
|
* @brief Plays an audio file from the specified file path.
|
|
* @param filePath The path to the audio file.
|
|
*/
|
|
void NotificationAudio::play(QString const &filePath) {
|
|
if (auto const it = m_cache.constFind(filePath); it != m_cache.constEnd()) {
|
|
playEntry(it);
|
|
return;
|
|
}
|
|
|
|
BWFFile file(QAudioFormat{}, filePath);
|
|
if (!file.open(QIODevice::ReadOnly))
|
|
return;
|
|
|
|
QByteArray data = file.readAll();
|
|
if (data.isEmpty())
|
|
return;
|
|
|
|
QAudioFormat fmt = file.format();
|
|
|
|
if (!m_device.isFormatSupported(fmt)) {
|
|
error("Requested output audio format is not supported on device.");
|
|
return;
|
|
}
|
|
|
|
// If source is 24-bit packed PCM, repack to Int32
|
|
if (file.bitsPerSample() == 24) {
|
|
QByteArray converted = pcm24le_to_int32le(data);
|
|
if (converted.isEmpty()) {
|
|
error("Bad 24-bit PCM size");
|
|
return;
|
|
}
|
|
data = std::move(converted);
|
|
fmt.setSampleFormat(QAudioFormat::Int32);
|
|
// keep sampleRate + channelCount from file.format()
|
|
}
|
|
|
|
// Mono -> stereo so mono files play through both speakers
|
|
if (!upmixMonoToStereoInPlace(fmt, data)) {
|
|
error("Unsupported mono upmix format");
|
|
return;
|
|
}
|
|
|
|
playEntry(m_cache.emplace(filePath, fmt, data));
|
|
}
|
|
|
|
/**
|
|
* @brief Stops audio playback.
|
|
*/
|
|
void NotificationAudio::stop() { m_stream->stop(); }
|
|
|
|
/******************************************************************************/
|
|
// Private Implementation
|
|
/******************************************************************************/
|
|
|
|
void NotificationAudio::playEntry(Cache::const_iterator const it) {
|
|
if (m_buffer.isOpen())
|
|
m_buffer.close();
|
|
|
|
auto const &[format, data] = *it;
|
|
|
|
m_buffer.setData(data);
|
|
|
|
if (m_buffer.open(QIODevice::ReadOnly)) {
|
|
m_stream->setDeviceFormat(m_device, format, m_msBuffer);
|
|
m_stream->restart(&m_buffer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Convert 24-bit PCM to 32-bit PCM
|
|
* @param PCM data byte array
|
|
*/
|
|
QByteArray NotificationAudio::pcm24le_to_int32le(const QByteArray& in)
|
|
{
|
|
if ((in.size() % 3) != 0) return {};
|
|
|
|
const int samples = in.size() / 3;
|
|
QByteArray out;
|
|
out.resize(samples * 4);
|
|
|
|
const uint8_t* src = reinterpret_cast<const uint8_t*>(in.constData());
|
|
int32_t* dst = reinterpret_cast<int32_t*>(out.data());
|
|
|
|
for (int i = 0; i < samples; ++i) {
|
|
int32_t v = (int32_t(src[0]) ) |
|
|
(int32_t(src[1]) << 8) |
|
|
(int32_t(src[2]) << 16);
|
|
|
|
// sign extend 24 -> 32
|
|
if (v & 0x00800000) v |= 0xFF000000;
|
|
|
|
// scale to full 32-bit range (optional but common)
|
|
dst[i] = v << 8;
|
|
|
|
src += 3;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* @brief Convert mono PCM data to stereo
|
|
* @param fmt The audio format
|
|
* @param data The audio data byte array
|
|
* @return true if upmix was successful, false otherwise
|
|
*/
|
|
bool NotificationAudio::upmixMonoToStereoInPlace(QAudioFormat& fmt, QByteArray& data)
|
|
{
|
|
if (fmt.channelCount() != 1)
|
|
return true; // nothing to do
|
|
|
|
switch (fmt.sampleFormat()) {
|
|
case QAudioFormat::UInt8: {
|
|
// 1 byte/sample -> 2 bytes/frame (L,R)
|
|
QByteArray out;
|
|
out.resize(data.size() * 2);
|
|
|
|
const uint8_t* src = reinterpret_cast<const uint8_t*>(data.constData());
|
|
uint8_t* dst = reinterpret_cast<uint8_t*>(out.data());
|
|
|
|
for (int i = 0; i < data.size(); ++i) {
|
|
const uint8_t s = src[i];
|
|
*dst++ = s; // L
|
|
*dst++ = s; // R
|
|
}
|
|
|
|
data = std::move(out);
|
|
fmt.setChannelCount(2);
|
|
return true;
|
|
}
|
|
|
|
case QAudioFormat::Int16: {
|
|
if ((data.size() % 2) != 0) return false;
|
|
const int samples = data.size() / 2;
|
|
|
|
QByteArray out;
|
|
out.resize(samples * 4);
|
|
|
|
const int16_t* src = reinterpret_cast<const int16_t*>(data.constData());
|
|
int16_t* dst = reinterpret_cast<int16_t*>(out.data());
|
|
|
|
for (int i = 0; i < samples; ++i) {
|
|
const int16_t s = src[i];
|
|
*dst++ = s; // L
|
|
*dst++ = s; // R
|
|
}
|
|
|
|
data = std::move(out);
|
|
fmt.setChannelCount(2);
|
|
return true;
|
|
}
|
|
|
|
case QAudioFormat::Int32: {
|
|
if ((data.size() % 4) != 0) return false;
|
|
const int samples = data.size() / 4;
|
|
|
|
QByteArray out;
|
|
out.resize(samples * 8);
|
|
|
|
const int32_t* src = reinterpret_cast<const int32_t*>(data.constData());
|
|
int32_t* dst = reinterpret_cast<int32_t*>(out.data());
|
|
|
|
for (int i = 0; i < samples; ++i) {
|
|
const int32_t s = src[i];
|
|
*dst++ = s; // L
|
|
*dst++ = s; // R
|
|
}
|
|
|
|
data = std::move(out);
|
|
fmt.setChannelCount(2);
|
|
return true;
|
|
}
|
|
|
|
case QAudioFormat::Float: {
|
|
// Qt float samples are 32-bit
|
|
if ((data.size() % 4) != 0) return false;
|
|
const int samples = data.size() / 4;
|
|
|
|
QByteArray out;
|
|
out.resize(samples * 8); // mono float32 -> stereo float32
|
|
|
|
const float* src = reinterpret_cast<const float*>(data.constData());
|
|
float* dst = reinterpret_cast<float*>(out.data());
|
|
|
|
for (int i = 0; i < samples; ++i) {
|
|
const float s = src[i];
|
|
*dst++ = s; // L
|
|
*dst++ = s; // R
|
|
}
|
|
|
|
data = std::move(out);
|
|
fmt.setChannelCount(2);
|
|
return true;
|
|
}
|
|
|
|
default:
|
|
return false; // unsupported here
|
|
}
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
Q_LOGGING_CATEGORY(notificationaudio_js8, "notificationaudio.js8", QtWarningMsg)
|