|
|
@@ -1,330 +1,311 @@ |
|
|
#include "audiooutputgraph.h" |
|
|
|
|
|
#include <climits> |
|
|
#include <cmath> |
|
|
#include <cstdint> |
|
|
|
|
|
// Qt |
|
|
#include <QtGlobal> |
|
|
#include <QImage> |
|
|
#include <QByteArray> |
|
|
#include <QPair> |
|
|
#include <QMutexLocker> |
|
|
|
|
|
// MythTV |
|
|
#include "audiooutputgraph.h" |
|
|
#include "mythlogging.h" |
|
|
#include "mythpainter.h" |
|
|
#include "mythimage.h" |
|
|
#include "compat.h" |
|
|
|
|
|
// Std |
|
|
#include <climits> |
|
|
#include <cmath> |
|
|
#include <cstdint> |
|
|
|
|
|
using namespace std::chrono_literals; |
|
|
|
|
|
#define LOC QString("AOG::%1").arg(__func__) |
|
|
#define LOC QString("AOG: ") |
|
|
|
|
|
const int kBufferMilliSecs = 500; |
|
|
|
|
|
/* |
|
|
* Audio data buffer |
|
|
*/ |
|
|
class AudioOutputGraph::Buffer : public QByteArray |
|
|
{ |
|
|
public: |
|
|
public: |
|
|
Buffer() = default; |
|
|
|
|
|
// Properties |
|
|
void SetMaxSamples(unsigned samples) { m_maxSamples = samples; } |
|
|
void SetSampleRate(unsigned sample_rate) { m_sampleRate = sample_rate; } |
|
|
|
|
|
void SetMaxSamples(uint16_t Samples) { m_maxSamples = Samples; } |
|
|
void SetSampleRate(uint16_t SampleRate) { m_sampleRate = SampleRate; } |
|
|
static inline int BitsPerChannel() { return sizeof(short) * CHAR_BIT; } |
|
|
inline int Channels() const { return m_channels; } |
|
|
|
|
|
inline std::chrono::milliseconds Next() const { return m_tcNext; } |
|
|
inline std::chrono::milliseconds Next() const { return m_tcNext; } |
|
|
inline std::chrono::milliseconds First() const { return m_tcFirst; } |
|
|
|
|
|
using range_t = QPair<std::chrono::milliseconds, std::chrono::milliseconds>; |
|
|
range_t Avail(std::chrono::milliseconds timecode) const |
|
|
using Range = std::pair<std::chrono::milliseconds, std::chrono::milliseconds>; |
|
|
Range Avail(std::chrono::milliseconds Timecode) const |
|
|
{ |
|
|
if (timecode == 0ms || timecode == -1ms) |
|
|
timecode = m_tcNext; |
|
|
if (Timecode == 0ms || Timecode == -1ms) |
|
|
Timecode = m_tcNext; |
|
|
|
|
|
std::chrono::milliseconds tc1 = timecode - Samples2MS(m_maxSamples / 2); |
|
|
if (tc1 < m_tcFirst) |
|
|
tc1 = m_tcFirst; |
|
|
std::chrono::milliseconds first = Timecode - Samples2MS(m_maxSamples / 2); |
|
|
if (first < m_tcFirst) |
|
|
first = m_tcFirst; |
|
|
|
|
|
std::chrono::milliseconds tc2 = tc1 + Samples2MS(m_maxSamples); |
|
|
if (tc2 > m_tcNext) |
|
|
std::chrono::milliseconds second = first + Samples2MS(m_maxSamples); |
|
|
if (second > m_tcNext) |
|
|
{ |
|
|
tc2 = m_tcNext; |
|
|
if (tc2 < tc1 + Samples2MS(m_maxSamples)) |
|
|
second = m_tcNext; |
|
|
if (second < first + Samples2MS(m_maxSamples)) |
|
|
{ |
|
|
tc1 = tc2 - Samples2MS(m_maxSamples); |
|
|
if (tc1 < m_tcFirst) |
|
|
tc1 = m_tcFirst; |
|
|
first = second - Samples2MS(m_maxSamples); |
|
|
if (first < m_tcFirst) |
|
|
first = m_tcFirst; |
|
|
} |
|
|
} |
|
|
return {tc1, tc2}; |
|
|
return {first, second}; |
|
|
} |
|
|
|
|
|
int Samples(range_t avail) const |
|
|
int Samples(Range Available) const |
|
|
{ |
|
|
return MS2Samples(avail.second - avail.first); |
|
|
return MS2Samples(Available.second - Available.first); |
|
|
} |
|
|
|
|
|
// Operations |
|
|
void Empty() |
|
|
{ |
|
|
m_tcFirst = m_tcNext = 0ms; |
|
|
m_bits = m_channels = 0; |
|
|
resize(0); |
|
|
} |
|
|
|
|
|
void Append(const void *b, unsigned long len, std::chrono::milliseconds timecode, int channels, int bits) |
|
|
void Append(const void * Buffer, unsigned long Length, std::chrono::milliseconds Timecode, |
|
|
int Channels, int Bits) |
|
|
{ |
|
|
if (m_bits != bits || m_channels != channels) |
|
|
if (m_bits != Bits || m_channels != Channels) |
|
|
{ |
|
|
LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("(%1, %2 channels, %3 bits)") |
|
|
.arg(timecode.count()).arg(channels).arg(bits)); |
|
|
|
|
|
Resize(channels, bits); |
|
|
m_tcNext = m_tcFirst = timecode; |
|
|
LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("%1, %2 channels, %3 bits") |
|
|
.arg(Timecode.count()).arg(Channels).arg(Bits)); |
|
|
Resize(Channels, Bits); |
|
|
m_tcNext = m_tcFirst = Timecode; |
|
|
} |
|
|
|
|
|
unsigned samples = Bytes2Samples(len); |
|
|
std::chrono::milliseconds tcNext = timecode + Samples2MS(samples); |
|
|
auto samples = Bytes2Samples(static_cast<unsigned>(Length)); |
|
|
std::chrono::milliseconds tcNext = Timecode + Samples2MS(samples); |
|
|
|
|
|
if (qAbs((timecode - m_tcNext).count()) <= 1) |
|
|
if (qAbs((Timecode - m_tcNext).count()) <= 1) |
|
|
{ |
|
|
Append(b, len, bits); |
|
|
Append(Buffer, Length, Bits); |
|
|
m_tcNext = tcNext; |
|
|
} |
|
|
else if (timecode >= m_tcFirst && tcNext <= m_tcNext) |
|
|
else if (Timecode >= m_tcFirst && tcNext <= m_tcNext) |
|
|
{ |
|
|
// Duplicate |
|
|
return; |
|
|
} |
|
|
else |
|
|
{ |
|
|
LOG(VB_PLAYBACK, LOG_INFO, LOC + QString(" discontinuity %1 -> %2") |
|
|
.arg(m_tcNext.count()).arg(timecode.count())); |
|
|
LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("Discontinuity %1 -> %2") |
|
|
.arg(m_tcNext.count()).arg(Timecode.count())); |
|
|
|
|
|
Resize(channels, bits); |
|
|
Append(b, len, bits); |
|
|
m_tcFirst = timecode; |
|
|
Resize(Channels, Bits); |
|
|
Append(Buffer, Length, Bits); |
|
|
m_tcFirst = Timecode; |
|
|
m_tcNext = tcNext; |
|
|
} |
|
|
|
|
|
int overflow = size() - m_sizeMax; |
|
|
auto overflow = size() - m_sizeMax; |
|
|
if (overflow > 0) |
|
|
{ |
|
|
remove(0, overflow); |
|
|
m_tcFirst = m_tcNext - Samples2MS(Bytes2Samples(m_sizeMax)); |
|
|
m_tcFirst = m_tcNext - Samples2MS(Bytes2Samples(static_cast<unsigned>(m_sizeMax))); |
|
|
} |
|
|
} |
|
|
|
|
|
const int16_t* Data16(range_t avail) const |
|
|
const int16_t* Data16(Range Available) const |
|
|
{ |
|
|
unsigned start = MS2Samples(avail.first - m_tcFirst); |
|
|
return reinterpret_cast< const int16_t* >(constData() + start * BytesPerSample()); |
|
|
auto start = MS2Samples(Available.first - m_tcFirst); |
|
|
return reinterpret_cast<const int16_t*>(constData() + (static_cast<uint>(start) * BytesPerSample())); |
|
|
} |
|
|
|
|
|
protected: |
|
|
inline unsigned BytesPerSample() const |
|
|
protected: |
|
|
inline uint BytesPerSample() const |
|
|
{ |
|
|
return m_channels * ((m_bits + 7) / 8); |
|
|
return static_cast<uint>(m_channels * ((m_bits + 7) / 8)); |
|
|
} |
|
|
|
|
|
inline unsigned Bytes2Samples(unsigned bytes) const |
|
|
inline unsigned Bytes2Samples(unsigned Bytes) const |
|
|
{ |
|
|
return (m_channels && m_bits) ? bytes / BytesPerSample() : 0; |
|
|
return (m_channels && m_bits) ? Bytes / BytesPerSample() : 0; |
|
|
} |
|
|
|
|
|
inline std::chrono::milliseconds Samples2MS(unsigned samples) const |
|
|
inline std::chrono::milliseconds Samples2MS(unsigned Samples) const |
|
|
{ |
|
|
return m_sampleRate ? std::chrono::milliseconds((samples * 1000UL + m_sampleRate - 1) / m_sampleRate) : 0ms; // round up |
|
|
return m_sampleRate ? std::chrono::milliseconds((Samples * 1000UL + m_sampleRate - 1) / m_sampleRate) : 0ms; // round up |
|
|
} |
|
|
|
|
|
inline unsigned MS2Samples(std::chrono::milliseconds msec) const |
|
|
inline int MS2Samples(std::chrono::milliseconds Msecs) const |
|
|
{ |
|
|
return msec > 0ms ? (msec.count() * m_sampleRate) / 1000 : 0; // NB round down |
|
|
return Msecs > 0ms ? static_cast<int>((Msecs.count() * m_sampleRate) / 1000) : 0; // NB round down |
|
|
} |
|
|
|
|
|
void Append(const void *b, unsigned long len, int bits) |
|
|
void Append(const void * Buffer, unsigned long Length, int Bits) |
|
|
{ |
|
|
switch (bits) |
|
|
switch (Bits) |
|
|
{ |
|
|
case 8: |
|
|
// 8bit unsigned to 16bit signed |
|
|
{ |
|
|
unsigned long cnt = len; |
|
|
int n = size(); |
|
|
resize(n + sizeof(int16_t) * cnt); |
|
|
const auto *s = reinterpret_cast< const uchar* >(b); |
|
|
auto *p = reinterpret_cast< int16_t* >(data() + n); |
|
|
while (cnt--) |
|
|
*p++ = (int16_t(*s++) - CHAR_MAX) << (16 - CHAR_BIT); |
|
|
auto count = Length; |
|
|
auto n = size(); |
|
|
resize(n + static_cast<int>(sizeof(int16_t) * count)); |
|
|
const auto * src = reinterpret_cast<const uchar*>(Buffer); |
|
|
auto * dst = reinterpret_cast<int16_t*>(data() + n); |
|
|
while (count--) |
|
|
*dst++ = static_cast<int16_t>((static_cast<int16_t>(*src++) - CHAR_MAX) << (16 - CHAR_BIT)); |
|
|
} |
|
|
break; |
|
|
|
|
|
case 16: |
|
|
append( reinterpret_cast< const char* >(b), len); |
|
|
append(reinterpret_cast<const char*>(Buffer), static_cast<int>(Length)); |
|
|
break; |
|
|
|
|
|
case 32: |
|
|
// 32bit float to 16bit signed |
|
|
{ |
|
|
unsigned long cnt = len / sizeof(float); |
|
|
int n = size(); |
|
|
resize(n + sizeof(int16_t) * cnt); |
|
|
unsigned long count = Length / sizeof(float); |
|
|
auto n = size(); |
|
|
resize(n + static_cast<int>(sizeof(int16_t) * count)); |
|
|
const float f((1 << 15) - 1); |
|
|
const auto *s = reinterpret_cast< const float* >(b); |
|
|
auto *p = reinterpret_cast< int16_t* >(data() + n); |
|
|
while (cnt--) |
|
|
*p++ = int16_t(f * *s++); |
|
|
const auto * src = reinterpret_cast<const float*>(Buffer); |
|
|
auto * dst = reinterpret_cast<int16_t*>(data() + n); |
|
|
while (count--) |
|
|
*dst++ = static_cast<int16_t>(f * (*src++)); |
|
|
} |
|
|
break; |
|
|
|
|
|
default: |
|
|
append( reinterpret_cast< const char* >(b), len); |
|
|
append(reinterpret_cast<const char*>(Buffer), static_cast<int>(Length)); |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
private: |
|
|
void Resize(int channels, int bits) |
|
|
private: |
|
|
void Resize(int Channels, int Bits) |
|
|
{ |
|
|
m_bits = bits; |
|
|
m_channels = channels; |
|
|
m_sizeMax = ((m_sampleRate * kBufferMilliSecs) / 1000) * BytesPerSample(); |
|
|
m_bits = Bits; |
|
|
m_channels = Channels; |
|
|
m_sizeMax = static_cast<int>(((m_sampleRate * kBufferMilliSecs) / 1000) * BytesPerSample()); |
|
|
resize(0); |
|
|
} |
|
|
|
|
|
private: |
|
|
unsigned m_maxSamples {0}; |
|
|
unsigned m_sampleRate {44100}; |
|
|
std::chrono::milliseconds m_tcFirst {0ms}, m_tcNext {0ms}; |
|
|
int m_bits {0}; |
|
|
int m_channels {0}; |
|
|
int m_sizeMax {0}; |
|
|
private: |
|
|
std::chrono::milliseconds m_tcFirst { 0ms }; |
|
|
std::chrono::milliseconds m_tcNext { 0ms }; |
|
|
uint16_t m_maxSamples { 0 }; |
|
|
uint16_t m_sampleRate { 44100 }; |
|
|
int m_bits { 0 }; |
|
|
int m_channels { 0 }; |
|
|
int m_sizeMax { 0 }; |
|
|
}; |
|
|
|
|
|
|
|
|
/* |
|
|
* Audio graphic |
|
|
*/ |
|
|
AudioOutputGraph::AudioOutputGraph() : |
|
|
m_buffer(new AudioOutputGraph::Buffer()) |
|
|
{ } |
|
|
{ |
|
|
} |
|
|
|
|
|
AudioOutputGraph::~AudioOutputGraph() |
|
|
{ |
|
|
delete m_buffer; |
|
|
} |
|
|
|
|
|
void AudioOutputGraph::SetPainter(MythPainter* painter) |
|
|
void AudioOutputGraph::SetPainter(MythPainter* Painter) |
|
|
{ |
|
|
QMutexLocker lock(&m_mutex); |
|
|
m_painter = painter; |
|
|
m_painter = Painter; |
|
|
} |
|
|
|
|
|
void AudioOutputGraph::SetSampleRate(unsigned sample_rate) |
|
|
void AudioOutputGraph::SetSampleRate(uint16_t SampleRate) |
|
|
{ |
|
|
LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("(%1)") |
|
|
.arg(sample_rate)); |
|
|
|
|
|
LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("Set sample rate %1)").arg(SampleRate)); |
|
|
QMutexLocker lock(&m_mutex); |
|
|
m_buffer->SetSampleRate(sample_rate); |
|
|
m_buffer->SetSampleRate(SampleRate); |
|
|
} |
|
|
|
|
|
void AudioOutputGraph::SetSampleCount(unsigned sample_count) |
|
|
void AudioOutputGraph::SetSampleCount(uint16_t SampleCount) |
|
|
{ |
|
|
LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("(%1)") |
|
|
.arg(sample_count)); |
|
|
|
|
|
LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("Set sample count %1").arg(SampleCount)); |
|
|
QMutexLocker lock(&m_mutex); |
|
|
m_buffer->SetMaxSamples(sample_count); |
|
|
m_buffer->SetMaxSamples(SampleCount); |
|
|
} |
|
|
|
|
|
void AudioOutputGraph::prepare() |
|
|
{ |
|
|
} |
|
|
|
|
|
void AudioOutputGraph::add(const void *buf, unsigned long len, |
|
|
std::chrono::milliseconds timecode, int channels, int bits) |
|
|
void AudioOutputGraph::add(const void * Buffer, unsigned long Length, |
|
|
std::chrono::milliseconds Timecode, int Channnels, int Bits) |
|
|
{ |
|
|
QMutexLocker lock(&m_mutex); |
|
|
m_buffer->Append(buf, len, timecode, channels, bits); |
|
|
m_buffer->Append(Buffer, Length, Timecode, Channnels, Bits); |
|
|
} |
|
|
|
|
|
void AudioOutputGraph::Reset() |
|
|
{ |
|
|
LOG(VB_PLAYBACK, LOG_INFO, LOC); |
|
|
|
|
|
LOG(VB_PLAYBACK, LOG_INFO, LOC + "Reset"); |
|
|
QMutexLocker lock(&m_mutex); |
|
|
m_buffer->Empty(); |
|
|
} |
|
|
|
|
|
MythImage *AudioOutputGraph::GetImage(std::chrono::milliseconds timecode) const |
|
|
MythImage* AudioOutputGraph::GetImage(std::chrono::milliseconds Timecode) const |
|
|
{ |
|
|
QMutexLocker lock(&m_mutex); |
|
|
Buffer::range_t avail = m_buffer->Avail(timecode); |
|
|
Buffer::Range avail = m_buffer->Avail(Timecode); |
|
|
|
|
|
LOG(VB_PLAYBACK, LOG_INFO, LOC + |
|
|
QString("(%1) using [%2..%3] avail [%4..%5]") |
|
|
.arg(timecode.count()).arg(avail.first.count()).arg(avail.second.count()) |
|
|
QString("GetImage for timecode %1 using [%2..%3] available [%4..%5]") |
|
|
.arg(Timecode.count()).arg(avail.first.count()).arg(avail.second.count()) |
|
|
.arg(m_buffer->First().count()).arg(m_buffer->Next().count()) ); |
|
|
|
|
|
int width = m_buffer->Samples(avail); |
|
|
auto width = m_buffer->Samples(avail); |
|
|
if (width <= 0) |
|
|
return nullptr; |
|
|
|
|
|
const unsigned range = 1U << AudioOutputGraph::Buffer::BitsPerChannel(); |
|
|
const double threshold = 20 * log10(1.0 / range); // 16bit=-96.3296dB => ~6dB/bit |
|
|
const int height = (int)-ceil(threshold); // 96 |
|
|
const auto threshold = 20 * log10(1.0 / range); // 16bit=-96.3296dB => ~6dB/bit |
|
|
auto height = static_cast<const int>(-ceil(threshold)); // 96 |
|
|
if (height <= 0) |
|
|
return nullptr; |
|
|
|
|
|
const int channels = m_buffer->Channels(); |
|
|
|
|
|
// Assume signed 16 bit/sample |
|
|
const auto * p = m_buffer->Data16(avail); |
|
|
const auto * data = m_buffer->Data16(avail); |
|
|
const auto * max = reinterpret_cast<const int16_t*>(m_buffer->constData() + m_buffer->size()); |
|
|
if (p >= max) |
|
|
if (data >= max) |
|
|
return nullptr; |
|
|
|
|
|
if ((p + (channels * width)) >= max) |
|
|
if ((data + (channels * width)) >= max) |
|
|
{ |
|
|
LOG(VB_GENERAL, LOG_WARNING, LOC + " Buffer overflow. Clipping samples."); |
|
|
width = static_cast<int>(max - p) / channels; |
|
|
LOG(VB_GENERAL, LOG_WARNING, LOC + "Buffer overflow. Clipping samples."); |
|
|
width = static_cast<int>(max - data) / channels; |
|
|
} |
|
|
|
|
|
QImage image(width, height, QImage::Format_ARGB32); |
|
|
image.fill(0); |
|
|
|
|
|
for (int x = 0; x < width; ++x) |
|
|
{ |
|
|
int left = p[0]; |
|
|
int right = channels > 1 ? p[1] : left; |
|
|
p += channels; |
|
|
|
|
|
unsigned avg = qAbs(left) + qAbs(right); |
|
|
double db = 20 * log10( (double)(avg ? avg : 1) / range); |
|
|
|
|
|
int idb = (int)ceil(db); |
|
|
QRgb rgb = idb <= m_dBsilence ? qRgb(255, 255, 255) |
|
|
: idb <= m_dBquiet ? qRgb( 0, 255, 255) |
|
|
: idb <= m_dBLoud ? qRgb( 0, 255, 0) |
|
|
: idb <= m_dbMax ? qRgb(255, 255, 0) |
|
|
: qRgb(255, 0, 0); |
|
|
|
|
|
int v = height - (int)(height * (db / threshold)); |
|
|
auto left = data[0]; |
|
|
auto right = channels > 1 ? data[1] : left; |
|
|
data += channels; |
|
|
|
|
|
auto avg = qAbs(left) + qAbs(right); |
|
|
double db = 20 * log10(static_cast<double>(avg ? avg : 1) / range); |
|
|
auto idb = static_cast<int>(ceil(db)); |
|
|
auto rgb = idb <= m_dBsilence ? qRgb(255, 255, 255) : |
|
|
idb <= m_dBquiet ? qRgb( 0, 255, 255) : |
|
|
idb <= m_dBLoud ? qRgb( 0, 255, 0) : |
|
|
idb <= m_dbMax ? qRgb(255, 255, 0) : |
|
|
qRgb(255, 0, 0); |
|
|
|
|
|
int v = height - static_cast<int>(height * (db / threshold)); |
|
|
if (v >= height) |
|
|
v = height - 1; |
|
|
else if (v < 0) |
|
|
v = 0; |
|
|
|
|
|
for (int y = 0; y <= v; ++y) |
|
|
image.setPixel(x, height - 1 - y, rgb); |
|
|
} |
|
|
|
|
|
auto *mi = new MythImage(m_painter); |
|
|
mi->Assign(image); |
|
|
return mi; |
|
|
auto * result = new MythImage(m_painter); |
|
|
result->Assign(image); |
|
|
return result; |
|
|
} |