Project Nayuki


Simple GUI FLAC player (Java)

Introduction

The goal of this mini-project was to make a FLAC audio player with a GUI and a working seek bar, while keeping the amount of implementation code small. The result is a program that delivers the promised features in ~650 lines of Java code. The tradeoff for smallness is that the code is less modular/reusable than ideal, has little explanatory and documentation comments, and ignores many error conditions. However, the result still successfully illustrates the modest effort needed to implement a seekable FLAC player.

Download source code: SimpleGuiFlacPlayer.java

This monolithic program is considered to be an amalgamation of SimpleDecodeFlacToWav, FrameInfo, FlacDecoder, and SeekableFlacPlayerGui which are published on other pages.

Code overview

Major components

Threads and call tree

Major classes and methods

class SimpleGuiFlacPlayer {
    (... Various fields for GUI ...)
    void main(String[] args);
    void setSliderPosition(double t);
    
    (... Various fields for worker ...)
    void doAudioDecoderWorkerLoop();
    void doWorkerIteration();
    
    class FlacDecoder {
        (... Various fields ...)
        void close();
        long[][] seekAndReadBlock(long samples);
        long[] findNextDecodableFrame(long filePos);
        Object[] readNextBlock();
        long[][] decodeSubframes(...);
        void decodeSubframe(...);
        void decodeRiceResiduals(...);
        
        class Stream {
            (... Various fields ...)
            void close();
            long getLength();
            long getPosition();
            void seekTo(long pos);
            int readByte();
            int readUint(int n);
            int readSignedInt(int n);
            void alignToByte();
        }
        
        class FormatException {}
    }
}

FLAC decoding

These are the steps to decode a FLAC audio file into raw uncompressed samples:

  1. Parse the stream info metadata block. This contains essential facts such as the sample rate, bit depth, number of channels, and total number of samples (i.e. clip length).

  2. Skip all other metadata blocks (tags, pictures, etc.).

  3. Sequentially decode every audio frame until the end of file is reached.

  4. To decode a frame, first confirm its sync code, then parse about a dozen header fields.

  5. Next, decode every subframe in the frame, with one subframe per audio channel.

  6. To decode a subframe, first read its header fields to determine the encoding parameters.

  7. A subframe encoded in constant mode or verbatim mode is easily handled.

  8. When a subframe is encoded in fixed prediction mode or linear predictive coding (LPC) mode, first the uncompressed warm-up samples are read, then the remain samples are decoded using Rice coding, and finally LPC restoration is applied.

  9. When all the subframes of a frame are decoded, there may be a bit more work to decode stereo encoding modes such as mid-side coding.

To seek in a FLAC file, we can search entries in the embedded seek table or search blindly in the whole audio file. Unfortunately, many publishers chose to omit seek tables in FLAC files, so the latter method is better in practice. Here is how seeking works:

  1. Suppose we want the playback position to jump to a specific audio sample offset in the file.

  2. We use binary search over the whole file data to narrow down which frame to ultimately decode.

  3. Define the range start as the file position of the foremost frame (i.e. immediately after the header metadata ends) and the range end as the end of the file.

  4. In each iteration, calculate the middle file position as the average of the range start and end.

  5. Starting at the middle position, read forward and try to find a sync sequence.

  6. When a sync code is found, try decoding the frame starting there. If decoding fails, then most likely some audio data accidentally mimicked a sync code and this wasn’t a real frame, so keep reading forward to find a valid sync and frame.

  7. If decoding succeeded, then we can look at the sample offset encoded in the frame header. Depending on whether it is less than or greater than the sample offset we want to seek to, we either set start = middle or end = middle.

  8. After binary search terminates, the value of the range start must satisfy the constraint that the first frame found starting at that file offset will have a sample offset less than or equal to the requested seek position.

  9. We seek to the range start, find a sync, and decode the next frame.

  10. If the frame’s end sample position is after the requested seek position, then we return the appropriate suffix of the frame’s samples as the result.

  11. Otherwise the frame’s end is not after the requested seek position, then we advance forward and decode the next frame, repeating until the frame’s data falls in the desired range of sample offsets.