Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
build/
tests/
dr_flac.h
sokol_audio.h
dr_mp3.h
dr_libs/
sokol/
qoaconv
qoaplay
/*.qoa
55 changes: 55 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
cmake_minimum_required(VERSION 3.15)

project(QOA)

option(INCLUDE_FLAC "Add flac functionality to qoaconv" OFF)
option(INCLUDE_MP3 "Add mp3 functionality to qoaconv" OFF)

if (NOT EXISTS ./sokol)
execute_process(COMMAND git clone https://github.com/floooh/sokol)
endif()

set(QOAConvFlags "")
set(QOAPlayFlags "")

# LICENSING: DrLibs is declared public domain. If it is beneficial, it could be brought in.

set(DRLIBS_URL "https://github.com/mackron/dr_libs")
if (INCLUDE_FLAC)
if (NOT EXISTS ./dr_libs)
execute_process(COMMAND git clone ${DRLIBS_URL})
endif()
list(APPEND QOAConvFlags -DQOACONV_HAS_DRFLAC)
endif()
if (INCLUDE_MP3)
if (NOT EXISTS ./dr_libs)
execute_process(COMMAND git clone ${DRLIBS_URL})
endif()
list(APPEND QOAConvFlags -DQOACONV_HAS_DRMP3)
endif()

add_executable(QOAPlay
qoaplay.c)
add_executable(QOAConv
qoaconv.c)

list(APPEND QOAConvFlags -O3 -Wall -Wextra -std=gnu99)
list(APPEND QOAPlayFlags -O3 -Wall -Wextra -std=gnu99)
target_compile_options(QOAPlay PUBLIC ${QOAPlayFlags})
target_compile_options(QOAConv PUBLIC ${QOAConvFlags})
set(QOAPlayInclude "")
set(QOAConvInclude "")
list(APPEND QOAPlayInclude ./sokol)
if (INCLUDE_MP3 OR INCLUDE_FLAC)
list(APPEND QOAConvInclude dr_libs/)
endif()
target_include_directories(QOAConv PUBLIC ${QOAConvInclude})
target_include_directories(QOAPlay PUBLIC ${QOAPlayInclude})

if (WIN32)
target_link_libraries(QOAConv -lm)
target_link_libraries(QOAPlay -lole32)
elseif (UNIX)
target_link_libraries(QOAConv -lm)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You added this file (and are the reason you need a fix now), why don't you just squash it into the original commit?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Managed to squash that but my commit for fixing the unsigned -> unsigned int for some reason I can't squash.

target_link_libraries(QOAPlay -lpthread -lasound)
endif()
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ Audio samples in WAV & QOA format can be found at: https://qoaformat.org/samples
wearing headphones. You may unexpectedly produce garbage output that can damage
your ears. I had more than a few close calls.

## Building
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit adding cmake seems overkill for this project when for a while we didn't even need a Makefile.

This doesn't need a second parallel build system when the first is already overkill.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take a look at what the CMakeLists.txt does, It brings in everything automatically if it doesn't exist and marks the include directories appropriately. My main goal was to have it be accessible for most people instead of grabbing files off of a browser and pasting them off of GitHub. On top of that, more people are familiar with CMake. If you want to continue maintaining the Makefile you can do so.


Use `cmake -S . -B build` in the source directory containing qoaconv.c, qoaplay.c
This creates an isolated build folder called `build` to prevent clutter in the source tree.
To enable FLAC/MP3 support, add `-DINCLUDE_FLAC` / `-DINCLUDE_MP3` respectively to the
beginning of the aforementioned command. Then, run `cmake --build build` for the binaries.

## Alternative Implementations of QOA

- [pfusik/qoa-ci](https://github.com/pfusik/qoa-ci) - Ć, transpiling to
Expand Down
78 changes: 43 additions & 35 deletions qoa.h
Original file line number Diff line number Diff line change
Expand Up @@ -316,27 +316,33 @@ static inline int qoa_clamp_s16(int v) {
return v;
}

static inline qoa_uint64_t qoa_bswap64(qoa_uint64_t v) {
#if defined(__GNUC__) || defined(__clang__)
return __builtin_bswap64(v);
#elif defined(_MSC_VER)
return _byteswap_uint64(v);
#else
return (v >> 56) & 0xff |
(v >> 48) & 0xff |
(v >> 40) & 0xff |
(v >> 32) & 0xff |
(v >> 24) & 0xff |
(v >> 16) & 0xff |
(v >> 8) & 0xff |
(v >> 0) & 0xff ;
#endif
}

static inline qoa_uint64_t qoa_read_u64(const unsigned char *bytes, unsigned int *p) {
bytes += *p;
*p += 8;
return
((qoa_uint64_t)(bytes[0]) << 56) | ((qoa_uint64_t)(bytes[1]) << 48) |
((qoa_uint64_t)(bytes[2]) << 40) | ((qoa_uint64_t)(bytes[3]) << 32) |
((qoa_uint64_t)(bytes[4]) << 24) | ((qoa_uint64_t)(bytes[5]) << 16) |
((qoa_uint64_t)(bytes[6]) << 8) | ((qoa_uint64_t)(bytes[7]) << 0);
return qoa_bswap64(*(qoa_uint64_t*)bytes);
}

static inline void qoa_write_u64(qoa_uint64_t v, unsigned char *bytes, unsigned int *p) {
bytes += *p;
*p += 8;
bytes[0] = (v >> 56) & 0xff;
bytes[1] = (v >> 48) & 0xff;
bytes[2] = (v >> 40) & 0xff;
bytes[3] = (v >> 32) & 0xff;
bytes[4] = (v >> 24) & 0xff;
bytes[5] = (v >> 16) & 0xff;
bytes[6] = (v >> 8) & 0xff;
bytes[7] = (v >> 0) & 0xff;
*(qoa_uint64_t*)bytes = qoa_bswap64(v);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're assuming that the current machine is little endian and you have to byteswap. It will not break on big endian machines. Before the code didn't make any assumptions and worked on everything.

}


Expand Down Expand Up @@ -366,7 +372,7 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned
), bytes, &p);


for (int c = 0; c < channels; c++) {
for (unsigned int c = 0; c < channels; c++) {
/* If the weights have grown too large, reset them to 0. This may happen
with certain high-frequency sounds. This is a last resort and will
introduce quite a bit of noise, but should at least prevent pops/clicks */
Expand All @@ -385,7 +391,7 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned
/* Write the current LMS state */
qoa_uint64_t weights = 0;
qoa_uint64_t history = 0;
for (int i = 0; i < QOA_LMS_LEN; i++) {
for (unsigned int i = 0; i < QOA_LMS_LEN; i++) {
history = (history << 16) | (qoa->lms[c].history[i] & 0xffff);
weights = (weights << 16) | (qoa->lms[c].weights[i] & 0xffff);
}
Expand All @@ -395,9 +401,13 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned

/* We encode all samples with the channels interleaved on a slice level.
E.g. for stereo: (ch-0, slice 0), (ch 1, slice 0), (ch 0, slice 1), ...*/
for (int sample_index = 0; sample_index < frame_len; sample_index += QOA_SLICE_LEN) {

for (int c = 0; c < channels; c++) {
qoa_lms_t best_lms;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't you just do = {0}?

I also wouldn't move where this is declared.

memset(&best_lms, 0, sizeof(best_lms));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

memset is a big hammer to suddently spring for a warning about something that will never happen. What happens when someone wants to run this library on embedded and there is no memset?


for (unsigned int sample_index = 0; sample_index < frame_len; sample_index += QOA_SLICE_LEN) {

for (unsigned int c = 0; c < channels; c++) {
int slice_len = qoa_clamp(QOA_SLICE_LEN, 0, frame_len - sample_index);
int slice_start = sample_index * channels + c;
int slice_end = (sample_index + slice_len) * channels + c;
Expand All @@ -406,9 +416,8 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned
16 scalefactors, encode all samples for the current slice and
meassure the total squared error. */
qoa_uint64_t best_error = -1;
qoa_uint64_t best_slice;
qoa_lms_t best_lms;
int best_scalefactor;
qoa_uint64_t best_slice = 0;
int best_scalefactor = 0;

for (int sfi = 0; sfi < 16; sfi++) {
/* There is a strong correlation between the scalefactors of
Expand Down Expand Up @@ -488,9 +497,7 @@ void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len)
num_frames * QOA_LMS_LEN * 4 * qoa->channels + /* 4 * 4 bytes lms state per channel */
num_slices * 8 * qoa->channels; /* 8 byte slices */

unsigned char *bytes = QOA_MALLOC(encoded_size);

for (int c = 0; c < qoa->channels; c++) {
for (unsigned int c = 0; c < qoa->channels; c++) {
/* Set the initial LMS weights to {0, 0, -1, 2}. This helps with the
prediction of the first few ms of a file. */
qoa->lms[c].weights[0] = 0;
Expand All @@ -500,11 +507,12 @@ void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len)

/* Explicitly set the history samples to 0, as we might have some
garbage in there. */
for (int i = 0; i < QOA_LMS_LEN; i++) {
for (unsigned int i = 0; i < QOA_LMS_LEN; i++) {
qoa->lms[c].history[i] = 0;
}
}

unsigned char *bytes = QOA_MALLOC(encoded_size);

/* Encode the header and go through all frames */
unsigned int p = qoa_encode_header(qoa, bytes);
Expand All @@ -513,7 +521,7 @@ void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len)
#endif

int frame_len = QOA_FRAME_LEN;
for (int sample_index = 0; sample_index < qoa->samples; sample_index += frame_len) {
for (unsigned int sample_index = 0; sample_index < qoa->samples; sample_index += frame_len) {
frame_len = qoa_clamp(QOA_FRAME_LEN, 0, qoa->samples - sample_index);
const short *frame_samples = sample_data + sample_index * qoa->channels;
unsigned int frame_size = qoa_encode_frame(frame_samples, qoa, frame_len, bytes + p);
Expand Down Expand Up @@ -576,14 +584,14 @@ unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa

/* Read and verify the frame header */
qoa_uint64_t frame_header = qoa_read_u64(bytes, &p);
int channels = (frame_header >> 56) & 0x0000ff;
int samplerate = (frame_header >> 32) & 0xffffff;
int samples = (frame_header >> 16) & 0x00ffff;
int frame_size = (frame_header ) & 0x00ffff;
unsigned int channels = (frame_header >> 56) & 0x0000ff;
unsigned int samplerate = (frame_header >> 32) & 0xffffff;
unsigned int samples = (frame_header >> 16) & 0x00ffff;
unsigned int frame_size = (frame_header ) & 0x00ffff;

int data_size = frame_size - 8 - QOA_LMS_LEN * 4 * channels;
int num_slices = data_size / 8;
int max_total_samples = num_slices * QOA_SLICE_LEN;
unsigned int data_size = frame_size - 8 - QOA_LMS_LEN * 4 * channels;
unsigned int num_slices = data_size / 8;
unsigned int max_total_samples = num_slices * QOA_SLICE_LEN;

if (
channels != qoa->channels ||
Expand All @@ -596,7 +604,7 @@ unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa


/* Read the LMS state: 4 x 2 bytes history, 4 x 2 bytes weights per channel */
for (int c = 0; c < channels; c++) {
for (unsigned int c = 0; c < channels; c++) {
qoa_uint64_t history = qoa_read_u64(bytes, &p);
qoa_uint64_t weights = qoa_read_u64(bytes, &p);
for (int i = 0; i < QOA_LMS_LEN; i++) {
Expand All @@ -609,8 +617,8 @@ unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa


/* Decode all slices for all channels in this frame */
for (int sample_index = 0; sample_index < samples; sample_index += QOA_SLICE_LEN) {
for (int c = 0; c < channels; c++) {
for (unsigned int sample_index = 0; sample_index < samples; sample_index += QOA_SLICE_LEN) {
for (unsigned int c = 0; c < channels; c++) {
qoa_uint64_t slice = qoa_read_u64(bytes, &p);

int scalefactor = (slice >> 60) & 0xf;
Expand Down
9 changes: 3 additions & 6 deletions qoaplay.c
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ unsigned int qoaplay_decode(qoaplay_desc *qp, float *sample_data, int num_sample
}

/* Normalize to -1..1 floats and write to dest */
for (int c = 0; c < qp->info.channels; c++) {
for (unsigned c = 0; c < qp->info.channels; c++) {
sample_data[dst_index++] = qp->sample_data[src_index++] / 32768.0;
}
qp->sample_data_pos++;
Expand All @@ -150,10 +150,7 @@ int qoaplay_get_frame(qoaplay_desc *qp) {
return qp->sample_pos / QOA_FRAME_LEN;
}

void qoaplay_seek_frame(qoaplay_desc *qp, int frame) {
if (frame < 0) {
frame = 0;
}
void qoaplay_seek_frame(qoaplay_desc *qp, unsigned frame) {
if (frame > qp->info.samples / QOA_FRAME_LEN) {
frame = qp->info.samples / QOA_FRAME_LEN;
}
Expand Down Expand Up @@ -200,7 +197,7 @@ hand over to the platform's audio API. All file IO and decoding is done here. */

static void sokol_audio_cb(float* sample_data, int num_samples, int num_channels, void *user_data) {
qoaplay_desc *qoaplay = (qoaplay_desc *)user_data;
if (num_channels != qoaplay->info.channels) {
if ((unsigned)num_channels != qoaplay->info.channels) {
printf("Audio cb channels %d not equal qoa channels %d\n", num_channels, qoaplay->info.channels);
exit(1);
}
Expand Down