Newer Older

A brainfuck synthesizer

waves

For my latest in my series of 500 Programming Languages, I decided to write a (simple) synthesizer in brainfuck.

Introduction to brainfuck

brainfuck is an esoteric programming language based on the idea of making a language with the smallest possible compiler. Indeed, my Python brainfuck compiler is only 30 lines of Python code, and I'm sure there are some optimizations I could have done to make it shorter if that was my goal with the whole thing.

The only data structure you get is a single array (if it were infinite you'd have a good representation of a Turing machine), and all you can do is move around this array with a pointer, increment and decrement values, input and output values, and loop.

The entire language consists of 8 commands:

Anything other characters in a program are ignored, and thus comments can be inserted anywhere into the program. Just be careful not to type proper English sentences with commas and periods, or you might get some funny bugs until you realize what's happening.

That's really all there is to the language. There are a couple of areas of behavior that aren't standardized between interpreters, relating to the size of the array, the cell size, and handling end-of-file when inputing. Those are covered in more detail on the brainfuck Wikipedia page.

So, since this is such a simple language (to comprehend, not to write in), and I already provided a working compiler in my previous post, I'm going to do this post a little differently, and just cover the process I went through in writing my first non-trivial brainfuck program.

The idea

I tried to come up with an idea for a program you don't normally see implemented in brainfuck, and I've never seen anything done with brainfuck and audio, so I decided to write a simple (non-interactive) synthesizer.

My first brainfuck program

To start out with, I wrote a program to just generate one second of simple square wave, at 4kHz (the max for 8kHz sample-rate audio).

With 8000Hz audio, there are 8000 bytes per second, so if the wavelength is two bytes (one byte high, one byte low, makes a complete single wave), that makes the frequency 8000/2, or 4000kHz. If I doubled the wavelength, I'd get half the frequency, or 2000Hz.

    set counter to four thousand
> ++++ four
[ > +++++ +++++ times ten
[ > +++++ +++++ times ten
[ > +++++ +++++ times ten
[ <<<< + >>>> - ]
< -
]
< -
]
< -
]

keep a zero value and generate a 255 value
>> +++++ five
[ > +++++ times 5
[ > +++++ +++++ times ten
[ <<< + >>> - ]
< -
]
< -
]
< +++++ plus five (255 total)

<<
[
>> . output 255
< . output 0
<
- decrement counter
]

This demonstrates how even generating a large number in brainfuck is quite a chore. I had to use four nested loops and four different cells to calculate 41010*10. Still, it's better than having 4000 plusses at the beginning of my program.

The way loops are done in brainfuck reminds me of doing loops in assembly language. In fact, brainfuck in general reminds me of assembly language, but without all those convenient opcodes you're used to. :)

What's your frequency, brainfuck?

Next, I wanted to generate a lower frequency, working my way toward taking user input to determine frequency.

I picked the A below middle C, which has a nice, even frequency of 220Hz. When you divide 8000 by 220, you come up with a wavelength of approximately 36. Half low and half high makes that 18 bytes per half, so I needed to update the previous program to output 18 bytes of each value instead of just one byte per value.

    222 waves of 36 bytes each equals 8000 samples for 1 second of audio
> +++++ +++++ +++++ +++++ ++ 22
[ > +++++ +++++ times ten
[ << + >> - ]
< -
]
++ plus 2 more

>> +++++ five
[ > +++++ times 5
[ > +++++ +++++ times ten
[ <<< + >>> - ]
< -
]
< -
]
< +++++ plus five (255 total)

<<
[
>>>
+++++ +++++ +++++ +++ eighteen up and down = 220Hz
[
< . output 255 byte
> - subtract from counter
]
+++++ +++++ +++++ +++ eighteen up and down = 220Hz
[
<< . output 0 byte
>> - subtract from counter
]
<<<
-
]

This version adds two extra loops and an extra cell to generate eighteen 255s and eighteen zeroes for each iteration of the loop.

Like MIDI, but crappier

So, the next and final step was to allow input of notes to play, and the lengths to play each of those notes.

I found a helpful reference on the frequencies of musical notes.

Due to the fact that brainfuck only allows you to read a single byte at a time, and because it made the program simpler over all, I decided to have the two values input for each note represent the length, in waves, and the length of each half-wave. This allowed me to use the basic structure of the previous program, and to support a wider range of frequencies without having to handle multiple-byte input for notes.

    generate the 255 byte for output
>>> +++++ five
[ > +++++ times 5
[ > +++++ +++++ times ten
[ <<< + >>> - ]
< -
]
< -
]
< +++++ plus five (255 total)
<<

, read in the length of the first note (in waves)
> , read in the length of each half wave
[
<
[
>
[->>+>+>+<<<<] copy the value to two cells for use in the following loops plus an extra copy
>>>>[-<<<<+>>>>] copy the value back to the original location
<<
[
< . output 255 byte
> - subtract from counter
]
> goto next counter
[
< . output 0 byte (zeroed by the last loop)
> - subtract from counter
]
<<<<
-
]
, read in the length of the next note (in waves)
> , read in the length of each half wave
]

Since the half-wave length is input from the user, I can't simply re-generate the value each time through the main loop, so I had to make extra copies of the value, and then re-copy the value back to its original input location, since it's zeroed out when it's copied to the other location.

Like I said, it's harder than assembly language.

Now all you have to do is fire up your favorite hex editor and create a binary file with a list of lengths and notes (for example mary.notes), and save the output from the program into a file (for example, mary.raw), and you will have your very own brainfuck-generated music!

In an 8-bit unsigned 8kHz mono raw PCM file. Linux users can use aplay to play this file. It just so happens the default format is the same as the output of this file. This also means you can pipe the output of this program directly into aplay, as well.

All you need now is some sort of front-end which will interactively take keyboard input and translate it into notes, and you could use this for live performances. (note: you probably don't want to use this for live performances)

Learning more about brainfuck

I learned everything I know about brainfuck from the brainfuck Wikipedia page, and so can you!

Conclusion

This is probably not a language you want to use, like, ever.

Unless you're a huge nerd, like myself.

Then you might want to, just for the fun of it.

If it doesn't sound like fun to you, then you can safely say you're less of a nerd than me.

blog comments powered by Disqus