Skip to content

Instantly share code, notes, and snippets.

@anytizer
Created August 15, 2026 01:02
Show Gist options
  • Select an option

  • Save anytizer/7c143a39f1bbc33e0c299091a3ae9005 to your computer and use it in GitHub Desktop.

Select an option

Save anytizer/7c143a39f1bbc33e0c299091a3ae9005 to your computer and use it in GitHub Desktop.
Samples Scanner (.wav) for Qt/LMMS - work in progress.
#include <QApplication>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QFile>
#include <QDataStream>
#include <QFileInfo>
#include <QDir>
#include <QVBoxLayout>
#include <QWidget>
#include <QList>
#include <QStringList>
#include <QHeaderView>
#include <QMessageBox>
#include <QImage>
#include <QPainter>
#include <QPixmap>
#include <QStyledItemDelegate>
#include <QStyleOptionViewItem>
#include <QVariant>
#include <QPalette>
#include <QColor>
#include <QSizePolicy>
#include <QtMath>
// ---------------------------------------------------------
// WAV metadata
// ---------------------------------------------------------
struct WavInfo {
QString fileName;
QString filePath;
qint64 fileSize = 0;
quint32 sampleRate = 0;
quint16 channels = 0;
quint16 bitsPerSample = 0;
quint32 dataSize = 0;
qint64 dataOffset = 0;
double durationSeconds = 0.0;
};
// ---------------------------------------------------------
// Parse WAV header
// ---------------------------------------------------------
WavInfo parseWavFile(const QString &filePath)
{
WavInfo info;
QFileInfo fileInfo(filePath);
info.fileName = fileInfo.fileName();
info.filePath = filePath;
info.fileSize = fileInfo.size();
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly))
return info;
QDataStream in(&file);
in.setByteOrder(QDataStream::LittleEndian);
char chunk[4];
// RIFF
if (file.read(chunk, 4) != 4 ||
QByteArray(chunk, 4) != "RIFF") {
return info;
}
quint32 chunkSize;
in >> chunkSize;
Q_UNUSED(chunkSize);
// WAVE
if (file.read(chunk, 4) != 4 ||
QByteArray(chunk, 4) != "WAVE") {
return info;
}
// Parse WAV chunks
while (!file.atEnd()) {
if (file.read(chunk, 4) != 4)
break;
quint32 subChunkSize;
in >> subChunkSize;
QByteArray chunkId(chunk, 4);
if (chunkId == "fmt ") {
quint16 audioFormat;
quint32 byteRate;
quint16 blockAlign;
in >> audioFormat
>> info.channels
>> info.sampleRate
>> byteRate
>> blockAlign
>> info.bitsPerSample;
Q_UNUSED(audioFormat);
Q_UNUSED(byteRate);
Q_UNUSED(blockAlign);
if (subChunkSize > 16) {
file.seek(
file.pos() +
(subChunkSize - 16));
}
}
else if (chunkId == "data") {
info.dataOffset = file.pos();
info.dataSize = subChunkSize;
break;
}
else {
file.seek(
file.pos() +
subChunkSize);
}
}
if (info.sampleRate > 0 &&
info.channels > 0 &&
info.bitsPerSample > 0) {
quint32 byteRate =
info.sampleRate *
info.channels *
(info.bitsPerSample / 8);
if (byteRate > 0) {
info.durationSeconds =
static_cast<double>(
info.dataSize) /
byteRate;
}
}
return info;
}
// ---------------------------------------------------------
// Create waveform image using a common time scale.
//
// All WAV images represent the same total duration:
//
// max(1 second, longest WAV duration)
//
// The time axis is divided into 32 equal sections.
// A faded vertical line is drawn every 8 sections.
//
// The image background is transparent.
// ---------------------------------------------------------
QImage createWaveformImage(
const WavInfo &wav,
double commonDurationSeconds,
int width = 200,
int height = 100)
{
width = qMin(width, 200);
height = qMax(height, 50);
QImage image(
width,
height,
QImage::Format_ARGB32);
// Transparent background.
image.fill(Qt::transparent);
if (commonDurationSeconds <= 0.0)
return image;
QPainter painter(&image);
painter.setRenderHint(
QPainter::Antialiasing,
false);
// -----------------------------------------------------
// 32 time divisions.
//
// Faded lines at:
//
// 8/32
// 16/32
// 24/32
// -----------------------------------------------------
const int divisions = 32;
painter.setPen(
QPen(
QColor(180, 180, 180, 45),
1));
for (int division = 8;
division < divisions;
division += 8) {
int x =
qRound(
(static_cast<double>(division) /
divisions) *
(width - 1));
painter.drawLine(
x,
0,
x,
height - 1);
}
// -----------------------------------------------------
// WAV validation
// -----------------------------------------------------
if (wav.dataOffset <= 0 ||
wav.dataSize == 0 ||
wav.channels == 0 ||
wav.bitsPerSample == 0 ||
wav.sampleRate == 0) {
return image;
}
QFile file(wav.filePath);
if (!file.open(QIODevice::ReadOnly))
return image;
if (!file.seek(wav.dataOffset))
return image;
const int bytesPerSample =
wav.bitsPerSample / 8;
const int bytesPerFrame =
bytesPerSample * wav.channels;
if (bytesPerSample <= 0 ||
bytesPerFrame <= 0) {
return image;
}
const qint64 totalFrames =
wav.dataSize / bytesPerFrame;
if (totalFrames <= 0)
return image;
// -----------------------------------------------------
// Center line
// -----------------------------------------------------
const int centerY =
height / 2;
painter.setPen(
QPen(
QColor(100, 100, 100, 130),
1));
painter.drawLine(
0,
centerY,
width - 1,
centerY);
// -----------------------------------------------------
// Waveform
// -----------------------------------------------------
painter.setPen(
QPen(
QColor("#4FC3F7"),
1));
// Every image uses the SAME time-to-pixel mapping.
for (int x = 0;
x < width;
++x) {
const double startTime =
(static_cast<double>(x) /
width) *
commonDurationSeconds;
const double endTime =
(static_cast<double>(x + 1) /
width) *
commonDurationSeconds;
// Nothing to draw after this WAV ends.
if (startTime >= wav.durationSeconds)
continue;
const double clippedEndTime =
qMin(
endTime,
wav.durationSeconds);
qint64 startFrame =
static_cast<qint64>(
startTime *
wav.sampleRate);
qint64 endFrame =
static_cast<qint64>(
clippedEndTime *
wav.sampleRate);
if (startFrame < 0)
startFrame = 0;
if (endFrame > totalFrames)
endFrame = totalFrames;
if (endFrame <= startFrame)
endFrame = startFrame + 1;
if (endFrame > totalFrames)
endFrame = totalFrames;
float minValue = 1.0f;
float maxValue = -1.0f;
bool foundSample = false;
for (qint64 frame = startFrame;
frame < endFrame;
++frame) {
qint64 sampleOffset =
wav.dataOffset +
frame * bytesPerFrame;
if (!file.seek(sampleOffset))
break;
QByteArray sampleData =
file.read(bytesPerFrame);
if (sampleData.size() <
bytesPerFrame) {
break;
}
// First channel only.
const char *p =
sampleData.constData();
float value = 0.0f;
if (wav.bitsPerSample == 8) {
quint8 sample =
static_cast<quint8>(
static_cast<unsigned char>(
p[0]));
value =
(static_cast<float>(sample) -
128.0f) /
128.0f;
}
else if (wav.bitsPerSample == 16) {
qint16 sample =
static_cast<qint16>(
static_cast<quint8>(p[0]) |
(static_cast<qint16>(
static_cast<quint8>(p[1]))
<< 8));
value =
static_cast<float>(sample) /
32768.0f;
}
else if (wav.bitsPerSample == 24) {
qint32 sample =
static_cast<qint32>(
static_cast<quint8>(p[0]) |
(static_cast<qint32>(
static_cast<quint8>(p[1]))
<< 8) |
(static_cast<qint32>(
static_cast<quint8>(p[2]))
<< 16));
if (sample & 0x800000)
sample |= 0xFF000000;
value =
static_cast<float>(sample) /
8388608.0f;
}
else if (wav.bitsPerSample == 32) {
qint32 sample =
static_cast<qint32>(
static_cast<quint8>(p[0]) |
(static_cast<qint32>(
static_cast<quint8>(p[1]))
<< 8) |
(static_cast<qint32>(
static_cast<quint8>(p[2]))
<< 16) |
(static_cast<qint32>(
static_cast<quint8>(p[3]))
<< 24));
value =
static_cast<float>(sample) /
2147483648.0f;
}
else {
return image;
}
minValue =
qMin(minValue, value);
maxValue =
qMax(maxValue, value);
foundSample = true;
}
if (!foundSample)
continue;
int y1 =
centerY -
static_cast<int>(
maxValue *
(height / 2 - 2));
int y2 =
centerY -
static_cast<int>(
minValue *
(height / 2 - 2));
painter.drawLine(
x,
y1,
x,
y2);
}
return image;
}
// ---------------------------------------------------------
// Format file size
// ---------------------------------------------------------
QString formatFileSize(qint64 bytes)
{
if (bytes < 1024)
return QString("%1 B").arg(bytes);
if (bytes < 1024 * 1024) {
return QString("%1 KB")
.arg(
static_cast<double>(bytes) /
1024.0,
0,
'f',
1);
}
if (bytes <
1024LL * 1024LL * 1024LL) {
return QString("%1 MB")
.arg(
static_cast<double>(bytes) /
(1024.0 * 1024.0),
0,
'f',
1);
}
return QString("%1 GB")
.arg(
static_cast<double>(bytes) /
(1024.0 * 1024.0 * 1024.0),
0,
'f',
2);
}
// ---------------------------------------------------------
// Waveform delegate
//
// The waveform is LEFT aligned inside the graph cell.
// ---------------------------------------------------------
class WaveformDelegate : public QStyledItemDelegate
{
public:
explicit WaveformDelegate(
QObject *parent = nullptr)
: QStyledItemDelegate(parent)
{
}
void paint(
QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override
{
QVariant value =
index.data(Qt::DecorationRole);
if (!value.isValid()) {
QStyledItemDelegate::paint(
painter,
option,
index);
return;
}
QPixmap pixmap =
qvariant_cast<QPixmap>(value);
if (pixmap.isNull()) {
QStyledItemDelegate::paint(
painter,
option,
index);
return;
}
// Draw selection background.
if (option.state &
QStyle::State_Selected) {
painter->fillRect(
option.rect,
option.palette.highlight());
}
QRect available =
option.rect.adjusted(
2,
2,
-2,
-2);
// Fit image to available height/width.
QPixmap scaled =
pixmap.scaled(
available.size(),
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
// LEFT ALIGN.
const int x =
available.left();
// Vertically center.
const int y =
available.top() +
(available.height() -
scaled.height()) / 2;
painter->drawPixmap(
x,
y,
scaled);
}
};
// ---------------------------------------------------------
// Populate table
// ---------------------------------------------------------
void populateWavTable(
QTableWidget &table,
const QList<WavInfo> &wavList,
double commonDurationSeconds)
{
table.setRowCount(
wavList.size());
for (int i = 0;
i < wavList.size();
++i) {
const WavInfo &wav =
wavList[i];
// File Name
table.setItem(
i,
0,
new QTableWidgetItem(
wav.fileName));
// File Size
table.setItem(
i,
1,
new QTableWidgetItem(
formatFileSize(
wav.fileSize)));
// Sample Rate
table.setItem(
i,
2,
new QTableWidgetItem(
QString::number(
wav.sampleRate)));
// Channels
table.setItem(
i,
3,
new QTableWidgetItem(
QString::number(
wav.channels)));
// Bits/Sample
table.setItem(
i,
4,
new QTableWidgetItem(
QString::number(
wav.bitsPerSample)));
// Playtime
table.setItem(
i,
5,
new QTableWidgetItem(
QString::number(
wav.durationSeconds,
'f',
2)));
// WAV Graph
QImage waveform =
createWaveformImage(
wav,
commonDurationSeconds,
200,
50);
QTableWidgetItem *graphItem =
new QTableWidgetItem();
graphItem->setData(
Qt::DecorationRole,
QPixmap::fromImage(
waveform));
table.setItem(
i,
6,
graphItem);
}
}
// ---------------------------------------------------------
// Main
// ---------------------------------------------------------
int main(
int argc,
char *argv[])
{
QApplication app(
argc,
argv);
// -----------------------------------------------------
// Dark application palette
// -----------------------------------------------------
QPalette darkPalette;
darkPalette.setColor(
QPalette::Window,
QColor("#202020"));
darkPalette.setColor(
QPalette::WindowText,
QColor("#E0E0E0"));
darkPalette.setColor(
QPalette::Base,
QColor("#181818"));
darkPalette.setColor(
QPalette::AlternateBase,
QColor("#222222"));
darkPalette.setColor(
QPalette::ToolTipBase,
QColor("#303030"));
darkPalette.setColor(
QPalette::ToolTipText,
QColor("#FFFFFF"));
darkPalette.setColor(
QPalette::Text,
QColor("#E0E0E0"));
darkPalette.setColor(
QPalette::Button,
QColor("#303030"));
darkPalette.setColor(
QPalette::ButtonText,
QColor("#E0E0E0"));
darkPalette.setColor(
QPalette::BrightText,
Qt::red);
darkPalette.setColor(
QPalette::Highlight,
QColor("#264F78"));
darkPalette.setColor(
QPalette::HighlightedText,
Qt::white);
app.setPalette(
darkPalette);
// -----------------------------------------------------
// Main window
// -----------------------------------------------------
QWidget window;
window.setWindowTitle(
"WAV Sample Browser");
QVBoxLayout layout(
&window);
// -----------------------------------------------------
// Table
// -----------------------------------------------------
QTableWidget table;
table.setColumnCount(7);
table.setHorizontalHeaderLabels({
"File Name",
"File Size",
"Sample Rate",
"Channels",
"Bits/Sample",
"Playtime (s)",
"WAV Graph"
});
// Read-only.
table.setEditTriggers(
QAbstractItemView::NoEditTriggers);
// Select entire row.
table.setSelectionBehavior(
QAbstractItemView::SelectRows);
// Only one row at a time.
table.setSelectionMode(
QAbstractItemView::SingleSelection);
// Remove numeric row header.
table.verticalHeader()->setVisible(
false);
// Fill parent.
table.setSizePolicy(
QSizePolicy::Expanding,
QSizePolicy::Expanding);
// -----------------------------------------------------
// Dark table appearance
// -----------------------------------------------------
table.setStyleSheet(
"QTableWidget {"
" background-color: #181818;"
" alternate-background-color: #222222;"
" color: #E0E0E0;"
" gridline-color: #383838;"
" selection-background-color: #264F78;"
" selection-color: #FFFFFF;"
"}"
""
"QHeaderView::section {"
" background-color: #303030;"
" color: #E0E0E0;"
" border: 1px solid #404040;"
" padding: 4px;"
"}"
""
"QTableCornerButton::section {"
" background-color: #303030;"
" border: 1px solid #404040;"
"}"
);
table.setAlternatingRowColors(
true);
// -----------------------------------------------------
// Fixed row height: 50 pixels
// -----------------------------------------------------
table.verticalHeader()
->setSectionResizeMode(
QHeaderView::Fixed);
table.verticalHeader()
->setDefaultSectionSize(
50);
// -----------------------------------------------------
// Column sizing
// -----------------------------------------------------
// Filename.
table.setColumnWidth(
0,
200);
// Playtime.
table.setColumnWidth(
5,
150);
// WAV Graph uses remaining table width.
table.horizontalHeader()
->setSectionResizeMode(
6,
QHeaderView::Stretch);
table.horizontalHeader()
->setSectionResizeMode(
0,
QHeaderView::Interactive);
table.horizontalHeader()
->setSectionResizeMode(
1,
QHeaderView::ResizeToContents);
table.horizontalHeader()
->setSectionResizeMode(
2,
QHeaderView::ResizeToContents);
table.horizontalHeader()
->setSectionResizeMode(
3,
QHeaderView::ResizeToContents);
table.horizontalHeader()
->setSectionResizeMode(
4,
QHeaderView::ResizeToContents);
table.horizontalHeader()
->setSectionResizeMode(
5,
QHeaderView::Interactive);
// -----------------------------------------------------
// Waveform delegate
// -----------------------------------------------------
table.setItemDelegateForColumn(
6,
new WaveformDelegate(
&table));
// -----------------------------------------------------
// Scan WAV files at runtime
// -----------------------------------------------------
QDir samplesDir(
"../samples");
QStringList filePaths =
samplesDir.entryList(
QStringList()
<< "*.wav"
<< "*.WAV",
QDir::Files |
QDir::Readable,
QDir::Name);
QList<WavInfo> wavList;
for (const QString &fileName :
filePaths) {
QString fullPath =
samplesDir.absoluteFilePath(
fileName);
WavInfo info =
parseWavFile(
fullPath);
if (info.sampleRate > 0 &&
info.channels > 0 &&
info.bitsPerSample > 0 &&
info.dataSize > 0) {
wavList.append(
info);
}
}
// -----------------------------------------------------
// Determine common waveform duration.
//
// Minimum = 1 second.
// Otherwise = longest sample.
// -----------------------------------------------------
double longestDuration = 0.0;
for (const WavInfo &wav :
wavList) {
longestDuration =
qMax(
longestDuration,
wav.durationSeconds);
}
const double commonDurationSeconds =
qMax(
1.0,
longestDuration);
// -----------------------------------------------------
// Populate table
// -----------------------------------------------------
populateWavTable(
table,
wavList,
commonDurationSeconds);
// -----------------------------------------------------
// Double-click handler
// -----------------------------------------------------
QObject::connect(
&table,
&QTableWidget::cellDoubleClicked,
[&table](
int row,
int /*column*/) {
QTableWidgetItem *item =
table.item(
row,
0);
if (!item)
return;
QMessageBox::information(
&table,
"Sample Selected",
QString(
"Sample file: %1")
.arg(
item->text()));
});
// -----------------------------------------------------
// Table fills parent.
// -----------------------------------------------------
layout.addWidget(
&table,
1);
// -----------------------------------------------------
// Initial window size: 800 x 500
// -----------------------------------------------------
window.resize(
700,
450);
window.show();
return app.exec();
}
@anytizer

Copy link
Copy Markdown
Author
output

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment