Skip to content

Commit

Permalink
Add support for decoding the Quite Ok Audio Format (QOA)
Browse files Browse the repository at this point in the history
  • Loading branch information
ssievert42 committed Jan 6, 2025
1 parent 3c11580 commit d2e3e0d
Show file tree
Hide file tree
Showing 6 changed files with 515 additions and 0 deletions.
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
: nRF5x: ensure TIMER1_IRQHandler doesn't always wake idle loop up (fix #1900)
Add support for decoding the Quite Ok Audio Format (QOA)

2v25 : ESP32C3: Get analogRead working correctly
Graphics: Adjust image alignment when rotating images to avoid cropping (fix #2535)
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ ifeq ($(USE_JIT),1)
SOURCES += src/jsjit.c src/jsjitc.c
endif

ifeq ($(USE_QOA),1)
INCLUDE += -I$(ROOT)/libs/audio/qoa
WRAPPERSOURCES += libs/audio/qoa/jswrap_qoa.c
SOURCES += libs/audio/qoa/qoa.c
endif

endif # BOOTLOADER ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DON'T USE STUFF ABOVE IN BOOTLOADER

Expand Down
209 changes: 209 additions & 0 deletions libs/audio/qoa/jswrap_qoa.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
* This file is part of Espruino, a JavaScript interpreter for Microcontrollers
*
* Copyright (C) 2013 Gordon Williams <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* ----------------------------------------------------------------------------
* JavaScript methods for working with the Quite Ok Audio Format (QOA)
* ----------------------------------------------------------------------------
*/

#include "jswrap_qoa.h"
#include "qoa.h"
#include "jsinteractive.h"

/*JSON{
"type" : "class",
"class" : "QOA"
}
Class for working with the [Quite Ok Audio Format (QOA)](https://qoaformat.org).
QOA does reasonably fast lossy audio compression at 3.2 bits per sample.
You can use the [reference encoder](https://github.com/phoboslab/qoa) to encode some audio,
decode that audio on-device and then play decoded audio with the `Waveform` class.
*/

/*JSON{
"type" : "staticmethod",
"class" : "QOA",
"name" : "initDecode",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_qoa_init_decode",
"params" : [
["header","JsVar","an arraybuffer containing (at least) the header of the qoa file"]
],
"return" : ["JsVar","an object {firstFramePos, qoaState} containing the first frame pos as an integer and the qoa decoder state as an ArrayBuffer, or null on error"]
}
Initialise a QOA decoding context.
*/
JsVar *jswrap_qoa_init_decode(JsVar *header) {
if (!jsvIsArrayBuffer(header)) {
jsiConsolePrint("error: header is not an arraybuffer\n");
return NULL;
}
unsigned char headerBuf[QOA_MIN_FILESIZE]; {
JsVar *headerStr = jsvGetArrayBufferBackingString(header, NULL);
size_t readChars = jsvGetStringChars(headerStr, 0, headerBuf, QOA_MIN_FILESIZE);
jsvUnLock(headerStr);
if (readChars < QOA_MIN_FILESIZE) {
jsiConsolePrint("error: not enough data read from header\n");
return NULL;
}
}
qoa_desc *qoa = NULL;
JsVar *qoaStateBuf = jsvNewArrayBufferWithPtr(sizeof(qoa_desc), (char **) &qoa);
uint32_t firstFramePos = qoa_decode_header(headerBuf, QOA_MIN_FILESIZE, qoa);
if (!firstFramePos) {
jsvUnLock(qoaStateBuf);
jsiConsolePrint("error: failed to decode QOA header\n");
return NULL;
}
if (qoa->channels > 1) {
jsvUnLock(qoaStateBuf);
jsiConsolePrint("error: too many channels; only single channel audio is supported at the moment\n");
return NULL;
}
JsVar *result = jsvNewObject();
jsvAddNamedChildAndUnLock(result, jsvNewFromInteger((JsVarInt) firstFramePos), "firstFramePos");
jsvAddNamedChildAndUnLock(result, qoaStateBuf, "qoaState");
return result;
}

/*JSON{
"type" : "staticproperty",
"class" : "QOA",
"name" : "FRAME_LEN",
"generate" : "jswrap_qoa_frame_len",
"return" : ["JsVar","Number of samples in a frame of decoded audio."]
}*/
JsVar *jswrap_qoa_frame_len() {
return jsvNewFromInteger(QOA_FRAME_LEN);
}

/*JSON{
"type" : "staticmethod",
"class" : "QOA",
"name" : "decode",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_qoa_decode",
"params" : [
["encoded","JsVar","an ArrayBuffer containing some frames of a qoa file"],
["decoded","JsVar","an ArrayBuffer to write decoded samples into"],
["options","JsVar","an object containing {offset, qoaState, bits, fill}; see below"]
],
"return" : ["JsVar","an object with two integers: {frameLen, frameSize}, or null on error"]
}
Decode some QOA encoded audio samples.
Data is read from the "encoded" buffer and written to the "decoded" buffer.
Note that the "decoded" buffer needs to be at least `QOA.FRAME_LEN` long and match the number of bits per sample.
Decoded samples are unsigned integers and take up either 8 or 16 bits, depending on the `bits` argument.
The default is 16 bits per sample, which is what QOA usually decodes to.
But you may choose 8 bits per sample if you want to save some RAM.
The options object must contain:
- offset: where to start reading from the encoded data buffer
- qoaState: an ArrayBuffer containing the qoa decoder state
And can optionally contain:
- bits: number of bits per sample to write to the decoded data buffer, either 8 or 16 (default is 16)
- fill: bool; true to fill remaining decoded data buffer with silence
*/
JsVar *jswrap_qoa_decode(JsVar *encoded, JsVar *decoded, JsVar *options) {
bool use16Bit = true;
bool fillRestOfBuffer = false;
int offsetVal = 0; {
JsVar *offset = jsvObjectGetChildIfExists(options, "offset");
if (jsvIsIntegerish(offset)) {
offsetVal = jsvGetInteger(offset);
} else {
jsiConsolePrint("error: options must contain a numeric \"offset\"\n");
jsvUnLock(offset);
return NULL;
}
jsvUnLock(offset);
} {
JsVar *bits = jsvObjectGetChildIfExists(options, "bits");
if (jsvIsIntegerish(bits)) {
int bitsVal = jsvGetInteger(bits);
if (bitsVal == 8) {
use16Bit = false;
} else if (bitsVal == 16) {
use16Bit = true;
} else {
jsiConsolePrint("error: bits must be either 8 or 16\n");
jsvUnLock(bits);
return NULL;
}
}
jsvUnLock(bits);
} {
JsVar *fill = jsvObjectGetChildIfExists(options, "fill");
if (jsvGetBool(fill)) {
fillRestOfBuffer = true;
}
jsvUnLock(fill);
} {
uint32_t decodedBufStrOffset = 0;
JsVar *decodedBufStr = jsvGetArrayBufferBackingString(decoded, &decodedBufStrOffset);
size_t decodedBufLength = jsvGetStringLength(decodedBufStr) - decodedBufStrOffset;
size_t minNeededDecodedBufLength = QOA_FRAME_LEN * (use16Bit ? 2 : 1);
jsvUnLock(decodedBufStr);
if (decodedBufLength < minNeededDecodedBufLength) {
jsiConsolePrintf("error: encoded data buffer not big enough; must be at least %d samples long\n", QOA_FRAME_LEN);
return NULL;
}
}
JsVar *qoaState = jsvObjectGetChildIfExists(options, "qoaState");
if (qoaState == NULL) {
jsiConsolePrint("error: options must contain \"qoaState\"\n");
jsvUnLock(qoaState);
return NULL;
}
uint32_t qoaStateStrOffset = 0;
JsVar *qoaStateStr = jsvGetArrayBufferBackingString(qoaState, &qoaStateStrOffset);
if (!jsvIsFlatString(qoaStateStr)) {
jsiConsolePrintf("error: qoaState isn't backed by a contiguous area of memory\n");
jsvUnLock2(qoaState, qoaStateStr);
return NULL;
} {
size_t qoaStateLength = jsvGetStringLength(qoaStateStr) - qoaStateStrOffset;
if (qoaStateLength != sizeof(qoa_desc)) {
jsvUnLock2(qoaState, qoaStateStr);
jsiConsolePrintf("error: qoaState has wrong size (wanted: %d given: %d)\n", sizeof(qoa_desc), qoaStateLength);
return NULL;
}
}
qoa_desc *qoa = (qoa_desc *) (jsvGetFlatStringPointer(qoaStateStr) + qoaStateStrOffset);
JsVar *encodedStr = jsvGetArrayBufferBackingString(encoded, NULL);
static const size_t frameSize = QOA_FRAME_SIZE(1, QOA_SLICES_PER_FRAME);
unsigned char encodedData[frameSize];
size_t readChars = jsvGetStringChars(encodedStr, offsetVal, encodedData, frameSize);
jsvUnLock(encodedStr);
unsigned int frameLen;
JsvArrayBufferIterator it;
jsvArrayBufferIteratorNew(&it, decoded, 0);
unsigned int encodedFrameSize = qoa_decode_frame(encodedData, readChars, qoa, &(it.it), &frameLen, use16Bit);
if (fillRestOfBuffer) {
while (jsvStringIteratorHasChar(&(it.it))) {
if (use16Bit) {
jsvStringIteratorSetCharAndNext(&(it.it), 0);
jsvStringIteratorSetCharAndNext(&(it.it), 1 << 7);
} else {
jsvStringIteratorSetCharAndNext(&(it.it), 1 << 7);
}
}
}
jsvArrayBufferIteratorFree(&it);
jsvUnLock2(qoaState, qoaStateStr);
JsVar *result = jsvNewObject();
jsvAddNamedChildAndUnLock(result, jsvNewFromInteger((JsVarInt) frameLen), "frameLen");
jsvAddNamedChildAndUnLock(result, jsvNewFromInteger((JsVarInt) encodedFrameSize), "frameSize");
return result;
}
24 changes: 24 additions & 0 deletions libs/audio/qoa/jswrap_qoa.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* This file is part of Espruino, a JavaScript interpreter for Microcontrollers
*
* Copyright (C) 2013 Gordon Williams <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* ----------------------------------------------------------------------------
* JavaScript methods for working with the Quite Ok Audio Format (QOA)
* ----------------------------------------------------------------------------
*/

#ifndef JSWRAP_QOA_H_
#define JSWRAP_QOA_H_

#include "jsvar.h"

JsVar *jswrap_qoa_frame_len();
JsVar *jswrap_qoa_init_decode(JsVar *header);
JsVar *jswrap_qoa_decode(JsVar *encoded, JsVar *decoded, JsVar *options);

#endif //JSWRAP_QOA_H_
Loading

0 comments on commit d2e3e0d

Please sign in to comment.