
QOA Library
Library for working with the Quite Ok Audio Format (QOA).
QOA does reasonably fast lossy audio compression at 3.2 bits per sample.
This library currently supports decoding QOA-encoded audio on any board that you can upload inline C to.
Sound can then be played back with the Waveform class.
Encode some audio
To encode some audio, you can either use the form on this web page and let your browser do the heavy lifting, or compile and use the reference encoder.
Using the in-browser method is rather straightforward:
Just select a file and a "download" popup should appear a moment later (everything runs locally in your browser).
This downsamples your audio file to the selected sample rate (default is 8000 Hz), converts it to mono and encodes it.
Using the reference encoder is a bit more work, mainly because you need to compile it and downsample and covert your audio to mono before passing it to the encoder yourself.
Starting with an audio file "audio.mp3", you can create a QOA file, using ffmpeg to do the downsampling and mono conversion, like this:
ffmpeg -i audio.mp3 -ar 8000 -ac 1 audio.wav
./qoaconv audio.wav audio.qoa
Decoding and playback
To play a file called "audio.qoa" from storage on Jolt.js, using the Waveform class, with a speaker connected to H0
and H1
, you can do:
let qoa = require("QOA");
let handle = qoa.play("audio.qoa", {output: "waveform", outputOptions: {pin: H0, pin_neg: H1}});
The handle
is an object of the form:
durationSeconds
- integer, duration of the audio in secondsstop
- function, can be called to stop playback
You can also loop the file three times, and do something once playback finishes:
let qoa = require("QOA");
let handle = qoa.play("audio.qoa", {loop:1, loopCount: 3, output: "waveform", outputOptions: {pin: H0, pin_neg: H1}, onFinish: () => {print("playback finished");}});
The play()
function accepts the following arguments:
filename
- string, name of a file that can be read from storageoptions
- object, containing additional optionsoutput
- string, name of the output method to use; currently only "waveform" is supportedoutputOptions
- object, additional options for the output method- in the case of
output: "waveform"
:pin
: pin, speaker pin; like for exampleH0
on Jolt.jspin_neg
: pin, optional second speaker pin; like for exampleH1
on Jolt.js
- in the case of
loop
- boolean, whether to automagically restart playback at the beginning once it reaches the end of the fileloopCount
- integer, optional number of times to loop the audio; minimum is one, default is to loop foreverbytesPerSample
- integer, default is1
, but you can also use2
for 16 bits per sample mode (which might sound better, but will use more RAM)onFinish
- function, will be called when playback is done
You need to give it at least a filename, and a pin
.
If something goes wrong, play()
will throw an exception.
If you want to do the decoding yourself, for example to output audio using something other than the Waveform class, you are free to do so, but it's a tad more involved:
let qoa = require("QOA");
let filename = "audio.qoa";
let play = function () {
let bytesPerSample = 1;
let bitsPerSample = bytesPerSample * 8;
let s = require("Storage");
let headerBuf = E.toFlatString(s.read(filename, 0, qoa.MIN_FILESIZE));
if (headerBuf === undefined) throw new Error("Failed to allocate buffer for header data");
let initResult = qoa.initDecode(headerBuf, {bits: bitsPerSample});
headerBuf = undefined;
let state = initResult.state;
let firstFramePos = initResult.firstFramePos;
let sampleRate = initResult.sampleRate;
let durationSeconds = initResult.durationSeconds;
// you are free to choose another buffer size,
// but since QOA is decoded in frames with qoa.SAMPLES_PER_FRAME samples each,
// sizing your buffer accordingly makes things slightly less complicated
// hint: the qoa.play() function uses a different buffer size, to allow gapless looping
let bufferSize = qoa.SAMPLES_PER_FRAME * bytesPerSample;
let w = new Waveform(bufferSize, {doubleBuffer: true, bits: bitsPerSample});
analogWrite(H0, 0.5, {freq: sampleRate * 10});
analogWrite(H1, 0.5, {freq: sampleRate * 10});
let p = firstFramePos;
let nextBuffer = function (buf) {
let encoded = E.toFlatString(s.read(filename, p, qoa.ENCODED_FRAME_SIZE_BYTES));
// decode into buf, filling leftover space with silence
let decodeResult = qoa.decode(encoded, buf, state, {fill: 1});
p += decodeResult.readBytes;
return decodeResult.writtenSamples;
};
// fill buffers with initial data
nextBuffer(w.buffer);
nextBuffer(w.buffer2);
let stopOnNextBufferCallback = false;
w.on("buffer", (buf) => {
let decodedSamples = nextBuffer(buf);
if (stopOnNextBufferCallback) {
w.stop();
}
if (decodedSamples == 0) {
stopOnNextBufferCallback = true;
}
});
w.startOutput(H0, sampleRate, {pin_neg: H1, repeat: true});
w.on("finish", () => {
H0.read();
H1.read();
});
};
setWatch(function () {
play();
}, BTN, {repeat: true});
play();
This page is auto-generated from GitHub. If you see any mistakes or have suggestions, please let us know.