pood.re/blog

It's the pood.re blog. Cool. You'll find me talking about things I did, or things I want to do, or things I like, or things I don't like. Let's say you'll find me talking about things. Topics include: computing & computers, gender/sexuality/diversity and politics. Sensitive topics will be warned about.

Trivial file formats

2020-08-21

Here are some super-simple easy-to-write file formats for quickly getting graphics and sound out of you program, no library required.

For images: PPM

PPM (Portable PixMap) is a very simple format. The specification is simple enough to fit in a tweet.

PPM files can be written either as 100% ASCII text, like shown here, or with a binary body for more efficient transmission. More info on the PPM Wikipedia page.

Header format

We are writing a PPM file (full traditional 16 million colors, 3×8 = 24 bits per pixel), in ASCII mode:

P3

We write the image width and height as two ASCII numbers separated by a space. Couldn't be simpler.

1920 1080

We are writing one integer per channel. The max value for a 8 bit byte is of course:

255

And then we just write pixels by outputting three ASCII integers separated by a space, then a newline. How simple is that.

Sample code

#!/usr/bin/env python3

width = 1920
height = 1080

# This is the header. That's it. Note the trailing newline
print("P3\n{} {}\n255".format(width, height))

# From now on, we just print 3 integers in [0; 255] (red, green, blue)
# for every pixel from left to right then top to bottom.
for x in range(width):
	for y in range(height):
		# Pure green
		color = "0 255 0"

		print(color)

Bonus: videos using PPM

As seen on Null Program, you can pretty much pipe PPM image after PPM image into ffmpeg for recording, or mpv for live visualization.

For audio: WAV

Just like PPM, the WAV format is quite simple to work with. Just write a header and then write raw audio data until you're finished.

WAV can be complex if you care to look into it, but I once reverse-engineered a minimum viable header in C in about 30 minutes, and it's always served me well:

#define SAMPLE_RATE 44100

#define BPS 32
#define SAMPLE_TYPE uint32_t
#define SAMPLE_TYPE_MAX UINT32_MAX

// WAV header with parametrable channel count, BPS, and sample rate.
static char *wav_header[] = {
        "RIFF$\x00\x00\x80WAVEfmt \x10\x00\x00\x00\x01\x00",
       	// Here goes the channel count (2 bytes)
        // Here goes the sample rate for every channel (4 bytes)
        // (not sure why, but it has to be written twice)
        "\x01\x00",
        // Here goes the bits per sample count (2 bytes long)
        "data\x00\x00\x00\x80"
};

// Print a correct WAV header
void print_header() {
       	fwrite(wav_header[0], 1, 22, stdout);

	// Always mono audio
        uint16_t channel_count = 1;
       	fwrite(&channel_count, sizeof(channel_count), 1, stdout);

       	// Usually 44100. Some softare doesn't like other sample rates
       	uint32_t sample_rate = SAMPLE_RATE;
       	fwrite(&sample_rate, sizeof(sample_rate), 1, stdout);
       	fwrite(&sample_rate, sizeof(sample_rate), 1, stdout); // ?

       	fwrite(wav_header[1], 1, 2, stdout);

       	// Usually 8, 16 or 32.
       	// Be wary of endianess when writing the body!
       	uint16_t bps = BPS;
        fwrite(&bps, sizeof(bps), 1, stdout);

       	fwrite(wav_header[2], 1, 8, stdout);
}

And here's the code to write a single sample to the WAV file.

// Print a sample in range [0; 1] in the correct format to the WAV file.
void print_sample(double next_sample) {
	SAMPLE_TYPE value = next_sample * SAMPLE_TYPE_MAX;

	fwrite(&value, sizeof(value), 1, stdout);
}

Then, a simple ./program > output.wav for recording, or a ./program | mpv for live audio will work as expected.

You can even forgo the header completely and output pure PCM audio if you're piping into a program like aplay or sox, which allow you to set sample rate and consorts.

Conclusion

UNIX nerds like small tools that do one job and one job well. Using such simple formats that are trivial to read and write, we let the other tools (such as ffmpeg or sox or even Pandoc) do their job and convert our output for us.

Using just a few pipes, you can concentrate on what you're actually supposed to do and let the cool multitools and swiss army knives do the heavy and boring lifting.