Of ByteBeat, weak linkage and compiler magic

I recently came across some very interesting one-line programs, which generate some primitive music from simple logic expressions – a genre which has come to be known as “bytebeat”.

Being in need of a pointless-but-fun project requiring little to no concentration, I immediately wanted to play with these programs. Implementing the logic directly in an FPGA would be perfectly possible, but I actually chose not to this time – instead I wanted to compile them for the EightThirtyTwo CPU and run the sound through the four-channel DMA sound “chip” contained in EightThirtyTwoDemos (modeled very much after the Amiga’s beloved Paula chip!).

Of particular interest was https://github.com/kragen/viznut-music – a repository containing a bunch of ready-to-go one-line programs. The only problem is they’re setup to write their output to stdout using putchar(), from whence it must be piped into an audio player capable of handling unsigned 8-bit samples.

In the 832 ecosystem I don’t even have stdin or stdout, let alone any concept of pipes, and while I could modify each demo in turn to send data to the audio hardware instead of stdout, that’s a lot of duplicated work – it feels like there should be a more elegant way.

Well I don’t have pipes, but what I do have is a putchar() function which is declared with weak linkage. This is a concept I first encountered when tinkering with the ZPU some years ago, and I was immediately struck by its usefulness: In short, it allows a function (usually in a library) to be defined in such a way that it can be overriden by a subsequently declared version of the same function – a bit like how member functions of a C++ class can override member functions of its parent class. Without weak linkage, having two functions with the same globally-declared name would trigger a linker error.

I make frequent use of this overriding capability in DeMISTify, since it allows me to implement a default version of most functions which can be replaced at compile time if a core has specific needs; for example, most cores read the MiST config string over SPI, but the MiSTery Atari ST core has its options hardcoded in the firmware. All I have to do is supply replacement configstring_begin() and configstring_next() functions which fetch the next byte from an internal string rather than over SPI, and the rest of the firmware can remain unmodified. Likewise, I have a demo in EightThirtyTwoDemos which creates a framebuffer and overrides putchar() to write bitmapped text to that framebuffer. Because other text-related functions call putchar() to do their text output, this is all that’s required to make the system output to the screen instead of the usual UART.

The exact syntax for weak linkage varies between compilers – where it’s supported at all. With the (now quite old and outdated) GCC used with ZPU, simply add “__attribute__ ((weak))” to the function definition, like so:

__attribute__ ((weak)) void weakfunction() {
...
}

With my EightThirtyTwo backend for vbcc, I just use “__weak” as an attribute, like so:

__weak void weakfunction() {
...
}

(This is specific to the EightThirtyTwo vbcc backend, and not currently available on other targets.)

So the gameplan for making the demos in viznut-music work is to write a small wrapper which provides a custom putchar() function. This will write incoming bytes to a buffer and sends that buffer to the audio hardware when appropriate. We will also use a constructor function to set up the audio hardware automatically. All we have to do is compile that wrapper to an object file which will be included at link time when compiling the individual demos – the demos themselves will be unmodified.

The wrapper looks like this:

#include <stdio.h>
#include <hw/soundhw.h>
#include <hw/interrupts.h>
#include <hw/uart.h>
#include <signals.h>

/* It would be interesting to be able to play
   multiple streams simultaneously, so we'll
   include support for all four audio channels. */
#define CHANNELS 4

/* Declare a signal bit to be used in interrupts */
#define SIGNAL_BIT_AUDIO 0

/* Audio buffer */
#define BUFFER_SIZE 1024
char buffer[CHANNELS][BUFFER_SIZE];

void putbyte(int channel,int c)
{
	static int ptr[CHANNELS]={0,0,0,0};

	/* If this is channel 0 and we're about to cross a
	   buffer boundary, wait for an audio interrupt */
	if(!channel && (ptr[channel]&(BUFFER_SIZE/2-1))
			==(BUFFER_SIZE/2-1))
		WaitSignal(1<<SIGNAL_BIT_AUDIO);

	/* When we start writing to one half of the buffer
	   we start playing the other half. */
	if((ptr[channel]&(BUFFER_SIZE/2-1))==0)
	{
		REG_SOUNDCHANNEL[channel].DAT=buffer[channel]
			+(ptr[channel]^(BUFFER_SIZE/2));
		REG_SOUNDCHANNEL[channel].PERIOD=440;
		REG_SOUNDCHANNEL[channel].LEN=256;
		REG_SOUNDCHANNEL[channel].VOL=64;
		REG_SOUNDCHANNEL[channel].TRIGGER=1;
	}
	/* Write to the buffer, converting from signed to unsigned */
	buffer[channel][ptr[channel]]=c^0x80;
	ptr[channel]=(ptr[channel]+1)&(BUFFER_SIZE-1);
}

/* Interrupt function to signal the main task
   when an audio block has been played. */
static void audio_interrupt(void *userdata)
{
	SetSignal(SIGNAL_BIT_AUDIO);
}

static struct InterruptHandler handler =
{
	0,
	audio_interrupt,
	0,
	INTERRUPT_AUDIO
};

/* Constructor to initialise the audio hardware
   and set up the interrupt */
__constructor(200.main_init) void init()
{
	AddInterruptHandler(&handler);
	REG_SOUNDCHANNEL[0].MODE=SOUND_MODE_INT_F;
	REG_SOUNDCHANNEL[0].VOL=64;
	REG_SOUNDCHANNEL[1].VOL=64;
	REG_SOUNDCHANNEL[2].VOL=64;
	REG_SOUNDCHANNEL[3].VOL=64;
	EnableInterrupts();
}

/* Will never be called if the demo never exits, but
   for the sake of completeness... */
__destructor(200.main_init) void deinit()
{
	DisableInterrupts();
	RemoveInterruptHandler(&handler);
}

/* And finally our custom implementation of putchar */

int putchar(int c)
{
	putbyte(0,c)
	return(c);
}

So let’s compile, say… 9-vizunut.c and see what happens:

main(t){for(t=0;;t++)putchar((t>>6|t|t>>(t>>16))*10+((t>>11)&7));}
Surprisingly elaborate for such a simple program, isn’t it?

You might have noticed that I made provision in the wrapper code for playing multiple channels simultaneously – most of the demo programs produce sounds with the same tempo and in the same key, so overlaying them shouldn’t sound too cacophonous, so let’s pick a few likely candidates…

I’m going to use e-skurk-raer.c for channel 0, d-viznut-xpansive-varjohukka.c for channel 1, f-xpansive-lost-in-space.c for channel 2 and c-miiro.c for channel 3.

To allow the mix to develop over time I’m going to add some low-freuency modulation on channels 1 and 3, as well – so the 4-channel composite ByteBeat program looks like this:

extern void putbyte();

void main()
{
	int t;
	int t2;
	for(t=0;;t++) {
		t2=t>>6;
		putbyte(0,((t&4096)?((t*(t^t%255)|(t>>4))>>1):(t>>3)|((t&8192)?t<<2:t)));
		putbyte(1,(((t>>19) ^ t>>16)*64+63) & (t>>7|t|t>>6)*10+4*(t&t>>13|t>>6));
		putbyte(2,((t*(t>>8|t>>9)&46&t>>8))^(t&t>>13|t>>6));
		putbyte(3,(((t>>18) ^ t>>14)*48+15) & (t*5&(t>>7)|t*3&(t*4>>10)));
	}
}
And here’s a snippet of the resulting audio…

Leave a Reply

Your email address will not be published.