Skip to content

About

Lightweight FLAC audio decoder/encoder library optimized for ESP32 embedded devices. Supports native FLAC and Ogg FLAC with streaming decode and automatic format detection.

Resources

Code of conduct

Security policy

Stars

7 stars

Watchers

0 watching

Forks

Repository files navigation

microFLAC - Embedded FLAC Decoder and Encoder

CI Component Registry

A FLAC (Free Lossless Audio Codec) decoder and encoder optimized for ESP32 embedded devices. The decoder supports both native FLAC and Ogg FLAC containers with automatic format detection; the encoder produces native FLAC from 4-32 bit PCM in 1-8 channels. Designed as an ESP-IDF component with PSRAM support and Xtensa assembly optimizations for ESP32/ESP32-S3.

A project from the Open Home Foundation

Features

Decoding

  • Native FLAC and Ogg FLAC: Automatic container detection from first 4 bytes
  • Unified streaming API: Single decode() method handles container detection, header parsing, and frame decoding
  • All FLAC bit depths: 4-bit through 32-bit samples
  • CRC validation: Optional frame integrity checking with CRC-8 (header) and CRC-16 (data)
  • PSRAM support: Configurable memory placement with automatic fallback
  • Metadata extraction: Album art, Vorbis comments, seektable, and more with configurable size limits
  • Xtensa optimizations: LPC prediction with hardware multiply-accumulate on ESP32/ESP32-S3; C fallback on RISC-V chips (C3, C6, P4) and host

Encoding

  • 4-32 bit PCM input, 1-8 channels: Packed interleaved bytes in exactly the layout the decoder outputs (PcmFormat)
  • Fixed predictors, plus opt-in LPC: Fixed predictors (orders 0-4) always; linear prediction up to order 12 on request (up to 24 bits), typically 7-9% smaller on music. All integer arithmetic, so output is identical on every platform
  • Opt-in Rice partitioning and LPC stereo search: With both and LPC order 8, output is within 0.01% of reference flac -5
  • Stereo decorrelation: Each frame takes the cheapest of the four channel assignments
  • Wasted-bits detection: 16-bit audio in a 24-bit stream codes nearly as small as a 16-bit stream
  • No buffering, no allocation: encode() reads one block straight from the caller's buffer; all working memory is the ~2.1 KB object plus up to ~4.3 KB of task stack
  • Finished stream header: After finish(), a same-size header carrying the total sample count and frame-size range can overwrite the first

Quick Start

ESP-IDF Component

Add via the ESP Component Registry by listing it in your project's idf_component.yml:

dependencies:
  esphome/micro-flac: "^0.3.0"

Then configure via idf.py menuconfig (Component config → microFLAC), and include and use it:

#include "micro_flac/flac_decoder.h"

using namespace micro_flac;

FLACDecoder decoder;

// Optional: configure metadata limits before first decode() call
decoder.set_max_metadata_size(FLAC_METADATA_TYPE_PICTURE, 50 * 1024);  // 50KB album art
decoder.set_max_metadata_size(FLAC_METADATA_TYPE_VORBIS_COMMENT, 4096);

// Decode in a loop (works with both .flac and .oga files automatically)
uint8_t* output = nullptr;
size_t output_size_bytes = 0;

while (have_data) {
    size_t bytes_consumed = 0, samples_decoded = 0;
    auto result = decoder.decode(input, input_len, output, output_size_bytes,
                                 bytes_consumed, samples_decoded);
    input += bytes_consumed;
    input_len -= bytes_consumed;

    if (result == FLAC_DECODER_HEADER_READY) {
        // Stream info now available, allocate output buffer
        const auto& info = decoder.get_stream_info();
        output_size_bytes = info.max_block_size() * info.num_channels()
                          * info.bytes_per_sample();  // or use get_output_buffer_size_samples() for int32_t* path
        output = new uint8_t[output_size_bytes];
    } else if (result == FLAC_DECODER_SUCCESS) {
        // Process samples_decoded interleaved PCM samples in output
    } else if (result == FLAC_DECODER_NEED_MORE_DATA) {
        // Read more data into buffer and try again
    } else if (result == FLAC_DECODER_END_OF_STREAM) {
        break;
    } else {
        break;  // Negative values are errors
    }
}
delete[] output;

Encoding Example

#include "micro_flac/flac_encoder.h"

using namespace micro_flac;

// 44.1kHz 16-bit stereo input; FLACEncoderOptions (block size, LPC, Rice
// partitioning, wasted bits) can be passed as a second argument.
FLACEncoder encoder(PcmFormat{44100, 2, 16});

std::vector<uint8_t> out(encoder.get_max_output_bytes());  // fits the header and any frame
size_t bytes_written = 0;

// The first call reports an unsupported configuration
if (encoder.write_header(out.data(), out.size(), bytes_written) != FLAC_ENCODER_SUCCESS) {
    // handle error
}
// write out.data()[0 .. bytes_written) to the output stream/file

while (have_input) {
    size_t bytes_consumed = 0;
    auto result = encoder.encode(input, input_len, out.data(), out.size(),
                                 bytes_consumed, bytes_written);
    if (result == FLAC_ENCODER_NEED_MORE_DATA) {
        // Less than one block (encoder.get_input_block_bytes()) is available and nothing
        // was consumed: append more input and call again
    } else if (result == FLAC_ENCODER_SUCCESS) {
        // write out.data()[0 .. bytes_written) to the output stream/file
        input += bytes_consumed;
        input_len -= bytes_consumed;
    } else {
        break;  // negative values are errors
    }
}

// Encode whatever is left (less than one block) as the final frame
if (encoder.finish(input, input_len, out.data(), out.size(), bytes_written) == FLAC_ENCODER_SUCCESS) {
    // write out.data()[0 .. bytes_written) to the output stream/file
}

// Optional, for seekable output: the finished header carries the total sample
// count and frame sizes, and is the same size as the first one
if (encoder.write_header(out.data(), out.size(), bytes_written) == FLAC_ENCODER_SUCCESS) {
    // overwrite the first bytes_written (FLACEncoder::HEADER_BYTES) of the output with it
}

The stream is decodable exactly as emitted. The header written before finish() leaves STREAMINFO's total sample count and minimum/maximum frame size as "unknown" (spec-legal); rewriting it after finish() fills them in. The MD5 signature is always left unset; host_examples/wav_to_flac shows how a caller computes it and writes it into the finished header's last 16 bytes.

PlatformIO

Add to platformio.ini:

[env:esp32dev]
platform = espressif32
framework = espidf
lib_deps =
    esphome/micro-flac@^0.3.0

# Configure memory preference
build_flags =
    -DMICRO_FLAC_MEMORY_PREFER_PSRAM

Host Build (Linux/macOS)

cd host_examples/flac_to_wav
cmake -B build && cmake --build build
./build/flac_to_wav input.flac output.wav       # Native FLAC
./build/flac_to_wav input.oga output.wav        # Ogg FLAC

cd ../wav_to_flac
cmake -B build && cmake --build build
./build/wav_to_flac input.wav output.flac       # Encode

API Reference

Decoder Methods

Method Description
decode(input, len, uint8_t* output, output_size_bytes, bytes_consumed, samples_decoded) Decode with native byte packing (e.g., 2 bytes for 16-bit, 3 for 24-bit), little-endian on every host. output_size_bytes in bytes (max_block_size * channels * bytes_per_sample).
decode(input, len, int32_t* output, output_size_samples, bytes_consumed, samples_decoded) Decode with 32-bit left-justified output (all bit depths → 4 bytes), as int32_t in the host's byte order. output_size_samples in samples (max_block_size * channels or get_output_buffer_size_samples()).
get_stream_info() Get stream info struct (sample rate, channels, bit depth, etc.) after HEADER_READY. Use get_stream_info().is_valid() to check if parsed.
get_output_buffer_size_samples() Get required output buffer size in samples (max_block_size * num_channels)
reset() Reset decoder state for decoding a new stream (preserves configuration)
set_crc_check_enabled(bool) Enable/disable CRC validation
set_max_metadata_size(type, size) Set max stored size for a metadata type (call before first decode())
get_metadata_block(type) Get a specific metadata block by type (album art, tags, etc.)
get_metadata_blocks() Get all stored metadata blocks

Note: The decoder always outputs signed samples at all bit depths, including 8-bit. WAV files require unsigned 8-bit samples - consumers writing WAV must add 128 to each byte themselves.

FLACStreamInfo Methods

Available after decode() returns FLAC_DECODER_HEADER_READY via decoder.get_stream_info():

Method Description
sample_rate() Sample rate in Hz (e.g., 44100, 48000)
num_channels() Channel count (1=mono, 2=stereo, etc.)
bits_per_sample() Bit depth (4-32)
min_block_size() / max_block_size() Block size range in samples
total_samples_per_channel() Total samples per channel (0 if unknown)
bytes_per_sample() Bytes per sample, rounded up (e.g., 2 for 16-bit, 3 for 24-bit)
md5_signature() Pointer to 16-byte MD5 signature of unencoded audio data
is_valid() Whether STREAMINFO has been parsed

Decoder Result Codes

decode() returns FLACDecoderResult: non-negative values indicate success/informational states, negative values indicate errors. See flac_decoder.h for the full enum.

Code Value Description
FLAC_DECODER_SUCCESS 0 Frame decoded successfully
FLAC_DECODER_HEADER_READY 1 Header parsed, stream info available (allocate output buffer now)
FLAC_DECODER_END_OF_STREAM 2 No more frames to decode
FLAC_DECODER_NEED_MORE_DATA 3 Not enough input data; feed more and call decode() again
FLAC_DECODER_ERROR_INVALID_ARGUMENT -21 input is null (even with input_len 0), or output is null after HEADER_READY. Fix the argument and retry; the decoder's state is unchanged

Encoder Methods

FLACEncoder encodes interleaved 4-32 bit PCM in 1-8 channels to native FLAC with a fixed block size: every frame but the last holds exactly block_size samples per channel. It is configured at construction, or later with reset(format, options), from two structs:

  • PcmFormat (micro_flac/pcm_format.h) describes the input. Construct it as PcmFormat{sample_rate, num_channels, bits_per_sample} (1-1048575 Hz, 1-8 channels, 4-32 bits) and read it back through sample_rate(), num_channels(), bits_per_sample(), bytes_per_sample(), bytes_per_frame() and is_valid(), as in the other micro-* libraries. Samples are interleaved, little-endian, signed and ceil(bits_per_sample() / 8) bytes wide, with other depths left-justified (a 12-bit sample fills the top 12 bits of its 2 bytes). This is exactly what the decoder's uint8_t* decode() produces. 2-byte samples must be 2-byte aligned; 4-byte samples need no alignment.
  • FLACEncoderOptions controls compression (see Encoding Performance for the costs):
    • block_size (16-65535, default 4096).
    • lpc (default off): also try linear prediction on each subframe, keeping it where it is estimated smaller. Typically 7-9% smaller on 16-bit music. Streams of 17-24 bits use 64-bit arithmetic, much slower on an ESP32 (see Encoding Performance). Streams of 25-32 bits ignore it and use the fixed predictors only.
    • max_lpc_order (1-12, default 8): the highest LPC order tried. Orders 4, 8 and 12 come out about 6.8%, 8.6% and 9% smaller than the fixed predictors alone.
    • lpc_stereo_search (default off): with lpc on stereo, choose each frame's channel assignment after designing LPC for all four candidates (left, right, mid, side) rather than from the fixed predictors. Typically 0.01-0.25% smaller (median about 0.06%), up to about 0.5%.
    • max_rice_partition_order (0-6, default 0): split each subframe's residuals into up to 2^order partitions with their own Rice parameters. About 1% smaller at block size 4096, 0.3% at 1152.
    • wasted_bits (default on): code a subframe whose samples all end in the same k zero bits k bits shallower (RFC 9639 §9.2.2), as with 16-bit audio in a 24-bit stream.

Every format is unpacked 256 samples per channel at a time into a 2 KB buffer inside the encoder object, which never allocates.

Method Description
FLACEncoder(format, options = {}) Construct for one input format. Never fails and allocates nothing; the configuration is validated here and reported by the first call below
write_header(output, output_size_bytes, bytes_written) Write the HEADER_BYTES (42) byte fLaC marker and STREAMINFO block. Before finish() the total sample count and frame sizes are "unknown"; afterwards they are filled in (unless too large for STREAMINFO's fields), at the same size, so the finished header can overwrite the first one
encode(input, input_len, output, output_size_bytes, bytes_consumed, bytes_written) Consume exactly one block (get_input_block_bytes()) and write one frame, or return FLAC_ENCODER_NEED_MORE_DATA without consuming anything if input_len is shorter than a block
finish(input, input_len, output, output_size_bytes, bytes_written) Encode the remainder (0 bytes up to one block, whole sample frames) as the final frame and end the stream
reset() Reset stream state for encoding a new stream (preserves configuration)
reset(format, options = {}) Reset and reconfigure, leaving the encoder as the constructor would. Never fails; a rejected configuration is reported by the next call. Re-check the output buffer against get_max_output_bytes() afterwards
get_max_output_bytes() Output buffer size that fits the header and every frame; constant until reset(format, options) reconfigures the encoder
get_input_block_bytes() Input bytes one encode() call consumes (block_size * num_channels * bytes_per_sample)
get_total_samples_encoded() Samples per channel encoded in the current stream
get_pcm_format() / get_options() The current configuration, from the constructor or the last reset(format, options)

Encoder Result Codes

Every call returns FLACEncoderResult: non-negative values are success/informational, negative values are errors. An error consumes no input and reports no output (bytes_written is 0; the output buffer's contents are unspecified). OUTPUT_BUFFER_TOO_SMALL and INVALID_ARGUMENT can be fixed and retried; BAD_CONFIG persists until reset(format, options) supplies a supported configuration; after STREAM_FINISHED or STREAM_TOO_LONG, reset() starts a new stream (finish() with no input still ends a too-long stream cleanly).

Code Value Description
FLAC_ENCODER_SUCCESS 0 The call did its work (check bytes_written)
FLAC_ENCODER_NEED_MORE_DATA 1 encode() was given less than one block; nothing consumed
FLAC_ENCODER_ERROR_OUTPUT_BUFFER_TOO_SMALL -1 Output buffer smaller than get_max_output_bytes() (or HEADER_BYTES for write_header())
FLAC_ENCODER_ERROR_INVALID_ARGUMENT -2 Null output; null input to encode(), or to finish() with bytes (finish(nullptr, 0, ...) ends a stream with no remainder); misaligned 2-byte-sample input, or a finish() remainder that is not whole sample frames or is longer than one block
FLAC_ENCODER_ERROR_BAD_CONFIG -3 Unsupported PcmFormat or FLACEncoderOptions. Persists until reset(format, options) with a supported configuration
FLAC_ENCODER_ERROR_STREAM_FINISHED -4 encode() or finish() after finish(); call reset() to start a new stream
FLAC_ENCODER_ERROR_STREAM_TOO_LONG -5 The next frame number would exceed the format's 2^31 - 1

Output Bound Guarantee

get_max_output_bytes() is a hard upper bound on what any call can write. A call with a frame to write checks the output size first and returns FLAC_ENCODER_ERROR_OUTPUT_BUFFER_TOO_SMALL without touching output if it is smaller. The bound holds for every input because a FIXED or LPC subframe is written only when it is provably smaller than VERBATIM.

Configuration

Configure via ESP-IDF menuconfig (Component config → microFLAC) or compile flags. Host CMake builds take the same feature switches as options (-DMICRO_FLAC_ENABLE_OGG=OFF, -DMICRO_FLAC_ENCODER_ENABLE_LPC=OFF, -DMICRO_FLAC_ENCODER_ENABLE_RICE_PARTITIONS=OFF).

Option Default Kconfig / Compile Flag Notes
Memory preference Prefer PSRAM CONFIG_MICRO_FLAC_PREFER_PSRAM / -DMICRO_FLAC_MEMORY_PREFER_PSRAM Also: prefer internal, PSRAM-only, internal-only
CRC checking Enabled Runtime: set_crc_check_enabled(bool) CRC-8 (header) and CRC-16 (data)
Xtensa assembly Enabled (ESP32/S3) CONFIG_MICRO_FLAC_ENABLE_XTENSA_ASM MULL/MULSH and hardware loops for LPC
Ogg FLAC support Enabled CONFIG_MICRO_FLAC_ENABLE_OGG / -DMICRO_FLAC_DISABLE_OGG Disabling saves ~3-5 KB flash
Encoder LPC Enabled CONFIG_MICRO_FLAC_ENCODER_ENABLE_LPC / -DMICRO_FLAC_ENCODER_DISABLE_LPC Disabling saves ~19 KB flash (ESP32-S3); the LPC options are then ignored. FLACEncoder::LPC_AVAILABLE reports it
Encoder Rice partitioning Enabled CONFIG_MICRO_FLAC_ENCODER_ENABLE_RICE_PARTITIONS / -DMICRO_FLAC_ENCODER_DISABLE_RICE_PARTITIONS Disabling saves ~7 KB flash (ESP32-S3); max_rice_partition_order is then ignored. FLACEncoder::RICE_PARTITIONS_AVAILABLE reports it

Performance

Decoding performance for 48kHz stereo audio (full frame, CRC enabled):

Chip Clock 16-bit 24-bit
ESP32 (internal SRAM) 240 MHz ~12x realtime n/a
ESP32 (PSRAM) 240 MHz ~8x realtime n/a
ESP32-S3 240 MHz ~30x realtime ~19x realtime
ESP32-P4 360 MHz ~25x realtime ~18x realtime

ESP32-S3 and ESP32-P4 numbers are measured with the working buffer in PSRAM (the default); PSRAM is fast enough on these chips that switching to internal SRAM only saves ~2-4% on the S3 and well under 1% on the P4. On the original ESP32, PSRAM is much slower than internal SRAM, so placing the working buffer in internal memory (CONFIG_MICRO_FLAC_PREFER_INTERNAL=y) is roughly 30-35% faster and is recommended for performance-sensitive use. Performance also varies with block size, prediction order, and sample depth (24-bit requires 64-bit arithmetic). See examples/decode_benchmark/README.md for detailed benchmarks, streaming overhead analysis, and instructions for running your own.

Encoding Performance

Encoding the 30-second 48 kHz stereo clip on an ESP32-S3 at 240 MHz (examples/encode_benchmark). Sizes are relative to the default settings at block size 4096.

Settings 16-bit 24-bit Size
Defaults, block size 4096 21.3x realtime 15.9x realtime baseline
Defaults, block size 1152 20.9x realtime 15.7x realtime same
max_rice_partition_order 6 17.9x realtime 13.5x realtime -0.6%
lpc, order 4 11.3x realtime n/a -4.6%
lpc, order 8 9.0x realtime 4.1x realtime -4.9%
lpc, order 12 8.1x realtime n/a -4.9%
lpc order 8 + lpc_stereo_search 6.4x realtime n/a -5.2%
lpc order 8 + search + partitions 5.9x realtime n/a -5.8%

At 32 bits (the clip's first 20 seconds widened with 16 bits of noise; fixed predictors only), the defaults encode at 3.8x realtime and with max_rice_partition_order 6 at 3.3x: 25-32 bit streams are analyzed in 64-bit arithmetic.

On a typical music corpus LPC gains more (7-9%) than on this clip. A host encodes several times faster. LPC on 24-bit audio takes 64-bit arithmetic, which Xtensa builds from 32-bit halves, hence its lower speed.

Memory Usage

Allocation Size Notes
Decoder object ~290 bytes Stack or heap
Block samples buffer max_block_size × channels × 4 Typically 16-64KB
Metadata blocks Variable Configurable per type
Output buffer max_block_size × channels × bytes_per_sample Allocated by user
Encoder object ~2.1 KB Stack or heap. encode() itself needs up to about 1.6 KB of task stack, 2.3 KB with partitioning, 3.4 KB with LPC, or 4.3 KB with the stereo search
Encoder output buffer get_max_output_bytes() Allocated by user; fits one worst-case frame
Encoder input One block (get_input_block_bytes()) The caller's buffer, unpacked a chunk at a time

PSRAM is recommended for the decoder's block samples buffer to conserve internal RAM.

Testing

The decoder is validated against the FLAC test suite with bit-perfect output compared to ffmpeg.

cd host_examples/flac_to_wav
cmake -B build && cmake --build build
python3 test_flac_decoder.py

Tests validate bit-perfect PCM output, MD5 signatures, various bit depths (8-32; the encoder's round-trip suite covers 4-7), sample rates, channel counts (1-8), embedded album art, byte-by-byte streaming, and Ogg FLAC containers. See host_examples/flac_to_wav/TESTING.md for full test suite details, test categories, and troubleshooting.

# ESP32 benchmark
cd examples/decode_benchmark
pio run -e esp32s3 -t upload -t monitor

The encoder is validated with a round-trip suite: synthetic and real-audio WAV sources are encoded, decoded back with the decoder above (and, when available, ffmpeg/flac), and checked for bit-exact PCM.

cd host_examples/wav_to_flac
cmake -B build && cmake --build build
python3 test_flac_encoder.py

Unit tests for the bit writer and the encoder's configuration and argument handling live in tests/encoder:

cd tests/encoder
cmake -B build && cmake --build build
ctest --test-dir build
# ESP32 benchmark
cd examples/encode_benchmark
pio run -e esp32s3 -t upload -t monitor

Advanced Features

32-bit Sample Output Mode

For embedded systems, unpacking 24-bit samples (3 bytes each) can be inefficient. Use the int32_t* overload of decode() to get left-justified (MSB-aligned) 32-bit samples:

// Allocate a 32-bit output buffer after HEADER_READY
size_t output_size_samples = decoder.get_output_buffer_size_samples();
int32_t* output = new int32_t[output_size_samples];

// Use the int32_t* decode overload - 24-bit audio is shifted left by 8, 16-bit by 16, etc.
auto result = decoder.decode(input, input_len, output, output_size_samples,
                             bytes_consumed, samples_decoded);

Metadata Extraction

Configure size limits before the first decode() call:

// Enable album art up to 50KB
decoder.set_max_metadata_size(FLAC_METADATA_TYPE_PICTURE, 50 * 1024);

// Increase Vorbis comment storage
decoder.set_max_metadata_size(FLAC_METADATA_TYPE_VORBIS_COMMENT, 4096);

// After decode() returns FLAC_DECODER_HEADER_READY, access metadata:
const FLACMetadataBlock* art = decoder.get_metadata_block(FLAC_METADATA_TYPE_PICTURE);
if (art) {
    // art->data contains the raw FLAC PICTURE block; parse per the FLAC spec
    // to extract the embedded image (type, MIME, description, then image bytes)
    // art->length is the total block size in bytes
}

const FLACMetadataBlock* tags = decoder.get_metadata_block(FLAC_METADATA_TYPE_VORBIS_COMMENT);
if (tags) {
    // Parse Vorbis comments: ARTIST=..., ALBUM=..., etc.
}

Default limits (conservative for embedded): STREAMINFO is always stored; all others are 0 bytes (skipped by default).

Custom Memory Allocation

Override FLAC_MALLOC and FLAC_FREE at compile time (-DFLAC_MALLOC=my_custom_malloc -DFLAC_FREE=my_custom_free). Custom functions must match void* malloc(size_t) and void free(void*) signatures.

Known Limitations

  • No seeking support: Decoder is designed for streaming, not random access
  • No automatic MD5 validation: The MD5 signature is extracted from STREAMINFO and accessible via get_stream_info().md5_signature(), but not automatically validated during decoding
  • No LPC above 24 bits: 25-32 bit streams use the fixed predictors only (lpc is ignored there). A fixed order whose residuals would reach 2^31, the limit RFC 9639 §9.2.7.3 sets, is never used; with none left, the subframe is VERBATIM. Full-scale 32-bit audio therefore compresses less than reference flac, whose LPC can still predict it
  • Encoder uses a fixed block size: Every frame but the last is block_size samples; there is no variable-blocksize mode
  • Streamable subset is up to the caller: Block sizes above 16384, or above 4608 at 48 kHz or below, bit depths other than 8/12/16/20/24, and sample rates no frame header code can carry (above 65535 Hz, other than multiples of 10 up to 655350 Hz and whole kHz up to 255 kHz; these are read from STREAMINFO) fall outside RFC 9639's streamable subset (§7), which a strict subset-only decoder may refuse. The defaults are within it
  • Encoder writes no MD5 signature: STREAMINFO's MD5 is left zeroed ("unknown"). The caller can hash the input and patch the digest into the finished header, as wav_to_flac does
  • Encoder output is native FLAC only: No Ogg FLAC container, seektables, or Vorbis comments
  • Encoder has no Xtensa assembly: C implementation on all targets

License

Apache License 2.0

Links

About

Lightweight FLAC audio decoder/encoder library optimized for ESP32 embedded devices. Supports native FLAC and Ogg FLAC with streaming decode and automatic format detection.

Resources

Code of conduct

Security policy

Stars

7 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages