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 seconds
  • stop - 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 storage
  • options - object, containing additional options
    • output - string, name of the output method to use; currently only "waveform" is supported
    • outputOptions - object, additional options for the output method
      • in the case of output: "waveform":
        • pin: pin, speaker pin; like for example H0 on Jolt.js
        • pin_neg: pin, optional second speaker pin; like for example H1 on Jolt.js
    • loop - boolean, whether to automagically restart playback at the beginning once it reaches the end of the file
    • loopCount - integer, optional number of times to loop the audio; minimum is one, default is to loop forever
    • bytesPerSample - integer, default is 1, but you can also use 2 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.