Decoding the FLAC audio format in Java

If you are looking for a way to decode FLAC audio files in a Java program, you are on the right track, but if you want to get to practical issues right away, start with the guide page for the Klipspringer codec library. If you want a longer introduction to sound and sound compression with a description of the Klipspringer FLAC decoding classes, keep reading this post.

This post was modified in June 2024 to conform to the revised FLAC decoding interface in Klipspringer version 4.

Sound, lossless compression, and FLAC

Sound is really micro changes in air density. You can measure it over a period of time with a microphone, store the measured values somehow, and then use them to have a speaker membrane reproduce the sound. Computers usually store the measured values as integers, the kind of numbers you can count on your digitals (fingers). For example, a CD stores each measurement, each sample, as two numbers from −32768 to +32767, one for the left and one for the right channel, and it has 44100 of those samples per second. You can call this data format pulse code modulation (PCM) at a 16-bit 44.1 kHz bitrate. The bitrate tells you far from everything about the sound quality, which also depends on the recording and mastering – there’s really no such thing as “CD quality sound” – but it imposes a limit on the level of detail. Once you have obtained a recording as a sequence of samples, the best you can do to retain sound quality is not to alter the data. Still, it’s nice if we can compress it (in the computer science sense, not the audio processing sense where compressing means intentionally altering the signal) and make it occupy less disk space or bandwidth. Some compression methods, like MP3, are lossy, which means that the encoder finds things in the sound data that it doesn’t think are important, and omits them. Others, like FLAC, are lossless, which means that the encoder just uses a more efficient representation than the plain PCM data, and decoding gets you that PCM data back exactly the way it was.

Lossless compression may sound impossible. If you store fewer or less accurate numbers, how can you not lose information? Well, if the sound you want to store is random noise, it is impossible to compress, but most of the time the sound data has some regularities. Say for example that you have sampled an extremely regular sound: 30 minutes of a consistent middle C tone playing for instance (in signal-processing terms, a pure 261.63 Hz sinus wave). In CD format, that would be stored using about 44 thousand 4-byte samples per second, which makes over 150 megabytes of data. But if you are able to represent the sound as a description like “a 30 minute long 261.63 Hz sinus wave, in 16 bits stereo with 44100 Hz sample rate”, plus something about time offset and volume, you can get by with something that’s about two million times smaller and still reproduce exactly the same sound. Obviously, music is more complex than that, but can still be subjected to the same basic idea: find components and regularities in the sound that can be represented more efficiently than with plain PCM through the use of more abstraction.

FLAC, the Free Lossless Audio Codec, is one format that encodes audio using lossless compression. FLAC files don’t come out as small as MP3s, but still save substantial storage space compared to plain PCM. You can find all information about it on the FLAC project website, including software for encoding and decoding PCM to and from FLAC. Most general software for playing and manipulating sound has incorporated libFLAC, the code library from that site, to get FLAC capability.

FLAC playing in Klipspringer

Being obsessive about both sound quality and storage space (data compression, albeit not for sound, is one of my specialties in my day job), I used the FLAC format when I transferred the contents of my CDs to a disk. For playing the files, I started out using MPlayer, but soon realized that it, as well as all other similar utilities I found, always played one file to its finish before starting to load the next one. This created a tiny gap in the sound (and not that tiny when playing music from an NFS mounted disk), since it took some time to start producing sound from the next file. That was extremely annoying when listening, for example, to string quartets where movements are played attacca, or house music where tracks are mixed into each other.

So I decided to extend Klipspringer with my own audio file player, one that did inter-track buffering across file boundaries to avoid gaps. Since it didn’t seem realistic at that point to get a Javascript program to decode and play FLACs without introducing new gaps, I went for coding my playing utility in a compiled language. I could have used C code that called libFLAC directly, but I shuddered at the thought of writing and debugging multi-threaded code (which would be necessary) in an almost primordial programming environment, without the smoothly integrated runtime checks and stack traces that we have come to expect from programming environments in the last 20 years or so. Instead I settled for Java, in which I knew how to write efficient multi-threaded code. However, Java code runs in its own virtual machine, JVM (which is a program, not a physical machine). It can’t directly invoke code libraries like libFLAC, which run natively on the CPU hardware in the surrounding operating system environment…

For a long time, I was happy to have found jFLAC, a Java port of libFLAC. Looking at its source code didn’t inspire much confidence however – it was messy, with lots of commented-out lines and other patchwork code smells, and it didn’t use modern Java features like the java.nio classes for I/O – but it worked without much effort when I used it in a way that was as simple as possible, without forcing me to look at the code too much.

But at one point I wanted to make fast forward and rewind more efficient by seeking in the FLAC files, jumping to any position in the sound data. That’s difficult in a compressed format, because you can’t just skip to any position in the file and start reading samples, like you can with plain PCM. You have to begin decoding at some sort of block start position, and you have to know the exact corresponding time point the decoded data. A FLAC file can, and normally does, contain a seek table to make this possible, but when I tried the seek method in jFLAC to make use of the seek table, it didn’t work as I expected. To investigate why, I would have to dive deeper into the jFLAC code, and possibly modify it, and I would rather not do that.

Instead, I decided to do what I had been wanting to do for a long time: throw out jFLAC and integrate libFLAC into my track player.

FLAC decoding with Java native interface

In principle, it’s simple to have Java code call compiled C code through the use of JNI, the Java native interface: define a class where you declare some methods native, write some C source code implementing them, compile the C code into a dynamic library, and then inform Java about where to look for that library when you start the JVM. Later posts on this blog include a tutorial on how it’s done. My efforts resulted in a class FlacDecoder, implemented in a combination of Java and C, which I made as simple to use as I could. I’ll walk you through how it’s used in the next section, and then comment a bit on its implementation in the section after that, and finally I provide some hints about included header files and compiling. You can copy or be inspired in doing your own Java-based FLAC decoding.

FlacDecoder usage

FlacDecoder implements the interface AudioDecoder, which has a nested interface Target. The idea is that you write a class that implements AudioDecoder.Target, and make it do whatever it is you want to do with the decoded audio data. The interface looks like this:

  interface Target {
      void metadata(PcmFormat format, long totalFrames) throws IOException;
      boolean write(ByteBuffer buffer) throws IOException;
      void close() throws IOException;
  }

The methods you implement are:

  • metadata, which the FLAC decoder calls to inform your target about the audio format (with a (PcmFormat object, which inherits from AudioFormat), so you can set up what to do with the PCM data once it arrives .;
  • write, which is where the actual PCM data arrives, one block at a time, so that you can send it on to the output or a file or whatever, returning true if it’s fine to continue or false if you want decoding to stop; and
  • close, which is called to inform the target that decoding is ending, whether that’s normally or prematurely.

In your program, you create a FlacDecoder object, passing its constructor an instance of your target class and the full path name of the FLAC file you want to decode. Then, on that object, you:

  • first call decodeMetadata, to get the decoding info sent to your target’s metadata method, and then
  • call decodeAll to get the blocks pushed to your write method until the end of the file.

Alternatively, you can call decodeAll directly and get both stream info and audio data in one sweep.

The decodeAll method takes a long argument fromFrame, which specifies the position to start playing from. (By a frame we mean one sample per channel, for instance stereo audio has two sample values per frame.) A zero argument plays from the beginning, while a positive number skips over the specified number of sample frames. For instance, to seek twelve and a half seconds into a file with sample rate 44100, the number to provide is 12.5 × 44100 = 551250. As of Klipspringer version 3, decodeAll can be called again after playback has been aborted by Target.write returning false, to resume playing the same file from any position without the file being closed.

FlacDecoder implementation

The FlacDecoder constructor calls the native method create to initialize a decoder on the native side, and gets a long back as a handle. In the rest of the Java-side methods, this handle gets passed along when calling any of the native methods, to specify which decoder is being used.

In the C implementation of the native methods, you can see that this handle is actually a pointer to a struct that holds decoder data. It is cast to and from jlong (the C side equivalent of a Java long) through intptr_t, to avoid getting warnings on architectures where pointers are smaller than jlongs. The struct looks like this:

typedef struct {
    // Members set in create.
    FLAC__StreamDecoder *decoder;       // FLAC interface
    jobject jthis;                      // FlacDecoder object
    jmethodID metadataMid;              // jthis metadata callback method
    jmethodID writeMid;                 // jthis write callback method
    FILE *file;                         // set in create, kept open until delete

    void *restrict buffer;              // allocated in first metadata_cb
    JNIEnv *env;                        // for passing handle through to FLAC callbacks
} DecoderRecord;

Here, decoder is the handle for the actual libFLAC decoder, jthis is the FlacDecoder object, streamInfoMid and writeMid are the method identifiers for methods in the decoder object (which in turn invoke the target object), file is the handle for the FLAC file, buffer is the memory area that’s wrapped in a ByteBuffer when calling writeBlock, and env is the Java environment that is active during a call to call to nativeSeek or nativeDecodeAll, so that the native side callback functions metadata_callback, write_callback, and error_callback can access it.

Apart from some JNI strangeness, the code to interface between the Java and C interfaces is pretty straightforward.

Instructions, documentation, and sample code

You can find instructions for downloading and getting started in the Klipspringer codec library guide. There are several demo programs for converting and playing FLAC files in the examples section of the source files. The examples are included in the Klipspringer FLAC distribution, along with Javadocs for the package. More advanced usage examples can be found in the net.avadeaux.klipspringer package classes.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.