289 changes: 135 additions & 154 deletions mythtv/libs/libmyth/audio/audiooutputgraph.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
55 changes: 25 additions & 30 deletions mythtv/libs/libmyth/audio/audiooutputgraph.h
Original file line number Diff line number Diff line change
@@ -1,51 +1,46 @@
#ifndef AUDIOOUTPUTGRAPH_H
#define AUDIOOUTPUTGRAPH_H
#include <cstdint>

#include "mythexp.h"
// Qt
#include <QMutex>

// MythTV
#include "mythexp.h"
#include "visual.h"

class MythImage;
class MythPainter;

class MPUBLIC AudioOutputGraph : public MythTV::Visual
{
public:
public:
AudioOutputGraph();
~AudioOutputGraph() override;

// Properties
void SetPainter(MythPainter* /*painter*/);
void SetSampleRate(unsigned sample_rate);
void SetSampleCount(unsigned sample_count);

void SetSilenceLevel(int db = -72) { m_dBsilence = db; }
void SetQuietLevel(int db = -60) { m_dBquiet = db; }
void SetLoudLevel(int db = -12) { m_dBLoud = db; }
void SetMaxLevel(int db = -6) { m_dbMax = db; }

// Operations
MythImage *GetImage(std::chrono::milliseconds timecode) const;
void SetPainter(MythPainter* Painter);
void SetSampleRate(uint16_t SampleRate);
void SetSampleCount(uint16_t SampleCount);
void SetSilenceLevel(int Db = -72) { m_dBsilence = Db; }
void SetQuietLevel(int Db = -60) { m_dBquiet = Db; }
void SetLoudLevel(int Db = -12) { m_dBLoud = Db; }
void SetMaxLevel(int Db = -6) { m_dbMax = Db; }
MythImage* GetImage(std::chrono::milliseconds Timecode) const;
void Reset();

// MythTV::Visual implementation
public:
void add(const void *b, unsigned long b_len, std::chrono::milliseconds timecode,
int channels, int bits) override; // Visual
void prepare() override; // Visual

// Implementation
private:
MythPainter *m_painter {nullptr};
int m_dBsilence {-72};
int m_dBquiet {-60};
int m_dBLoud {-12};
int m_dbMax {-6};
public:
void add(const void * Buffer, unsigned long Length, std::chrono::milliseconds Timecode,
int Channnels, int Bits) override;
void prepare() override;

private:
MythPainter* m_painter { nullptr };
int m_dBsilence { -72 };
int m_dBquiet { -60 };
int m_dBLoud { -12 };
int m_dbMax { -6 };
class Buffer;
Buffer * const m_buffer {nullptr};
Buffer* const m_buffer { nullptr };
QMutex mutable m_mutex;
};

#endif // AUDIOOUTPUTGRAPH_H
#endif