wip wip wip
beating my biyte
BACK 2 MISC
bytebeat is a small program(?) that takes in time as the only input, and spits out an audio signal. when it was first thought up, it was in C, and *only* used time. i'm not going to talk about that bytebeat, which iirc is called 'oneliner' bytebeat. i'm going to talk about javascript bytebeat, involving functions, variables, loops, among others. and i'm going to talk about coding it as space-efficiently as possible.
before i get into the nuances, i gotta explain a few things. bytebeat canonically has a domain of 0-255 for its signal, and it modulos down or up as needed. it also takes in time at a specific sample rate, from what i've seen usually 22050 or 44100 hz. that's all that's really needed to know.
now, before i get into javascript bytebeat, i really should explain the history of bytebeat itself. bytebeat was invented (i guess) by Viznut [1] in september 2011, and posted to youtube on the 26th [2]. this bytebeat was C code, not javascript, and consisted of small, single lines. demoscene site Pouet very quickly noticed this video, and had a thread up the next day [3]. people started to do javascript stuff almost immediately, and it seemed to be about as common as C bytebeats. but the poueters didn't consider things like Math.sin and etc 'kosher'.
in 2015, chiptune/remix site Battle of the Bits introduced the bytebeat format, for the Spring Tracks IV major battle. this was set up for Problems, however, because it did not limit the filesize. and thus, several entries were submitted that were crude, monstrous conjurations. looking at them, it's pretty dang obvious that they were not really programmed directly. no, there's walls of individual ticks and frequencies, capped by a sample rate bpm thing at the top and little functions handling the waveforms at the bottom. to put it plainly, i don't like these bytebeats. the next major that featured bytebeat featured the format bytebeat1k, which limited entries to 1 kilobyte in size. these were much more limited, but also much denser, making nonzero usage of code golfery.
and now i can seque into code golfing bytebeat. from what i can tell, a lot of the basic codegolfing techniques are in play here, but bytebeat seems to be a special case among code golfing. for one, you're playing with an audio signal instead of string, numerical, or whatever output, and for another, it's more subjective than anything because it's an art.
Sawtooth waves are far and away the easiest waveform to generate. Just put t in there, and you're done. But now you have a monotone beep blaring at you at maximum volumd. How do you turn it down? The most common way is to add a modulo operator to it, represented as % in javascript, and you replace `t` with `t%64` But, huh, now it's two octaves up. What happened?
The thing about pitch, is that it's in more places than you expect. Every sound has a pitch, even white noise has a form of pitch. One of the most important (and most well-known!) things about pitch is that it's directly related to cycles per second. Now, if we do a shallow bit of digging, we can find out why our saw is suddenly much higher.
Okay, so it's already established that output is limited to 0-255, and that it's pretty much directly coming from t here. And since we limited output to 0-64, our range has been divided by 4. There it is.
Square waves are a little harder: we need to do some binary math on t to make it work. To get the prototypical square wave, we can use t&128, where & stands for bitwise AND. If you dont know binary, bitwise AND basically means that if you can subtract that number and don't need to carry any ones in binary, it returns that number. It's a bit more nuanced than that but it fits for what I'm explaining. Okay, now that we put in t&128, we have a nice square wave beeping at us at half volume. It's half because it's limited to 128, but we can easily double it to get full volume `2*t&128`.
now we got a square wave, but theres a thing called pulse-width modulation. It involves changing the width of the peaks and troughs of the square wave. How do we access this? What's good to remember is that, nestled in the square wave, and all other waves, *there is still the underlying `t`*. And the square wave is 'up' whenever you can cleanly subtract 128 from t in binary. If you're like me, and are savvy with your math functions, you might see the modulo function under a coat of quantized paint. Let's lean into this: `t%256&128`[1].
Now something becomes apparent. If we modulate how fast `t` rises within the period of the square wave, we can modulate when `t%256` is above 128. This can be any function, but by far the least complex is a simple `t*x`. 1 and -1 are normal, and closer to 0 narrows the peak or trough, depending on sign.
[1] Operator precedence allows us to forgo parens: all bitwise binary operators are under the math ones.
Noise is different from other waves because it is random, and doesn't have a well-defined frequency. Noise is typically used in bytebeat to serve as percussion. To the average person, there is only white noise, but there are actually infinite colors of noise, based on their frequency distribution. For our purposes, we will only use noise that has a frequency spectrum of f^n, where f is the power at a given frequency band and n is any number.
The canonical form of noise is white noise, with a spectrum of n=0, and thus has a constant spectral power density. It is very simple to produce: a `random()` will suffice. However, nothing but white noise isn't that useful for percussion. It is possible to use white-noise percussion, but there isn't really any ability to make a good snare, which requires filtered noise. But bytebeat typically can't do much in the way of audio filtering. Why?
Let's talk about how frequency works again. Frequency is typically defined as cycles-per-time, but if we look back on how saws work, it's possible to also describe frequency as the rate of change of the signal. Low frequencies don't change the signal much with time, but high-frequency signals change it a lot. And if we remember that highpass filters only allow high frequencies, we'll figure that a highpassed signal will have a bias for high rates of change. But to know the rate of change requires the program to be able to look at the history of the signal, which bytebeat cannot easily do.
So if we can't look at the history of the signal, but that's exactly what filtering needs, then it's impossible to get filtered noise, isn't it? No nuanced drums for us. But we're missing something about noise that makes it special. Recall that white noise is random. In fact, it's *completely* random. At any sample, it is random, with no reference to anything before it. That's our key: noise *has no history*. We don't need to keep track of anything at all, we can just plug in `random()` to whatever we want and be fine. So that's what I did: `N=t&&((random()-0.5)+N*a),`. And this does nearly all colors of f^n noise, from infinitely-blue ("bluest") noise to apparently infinitely-red ("reddest") noise. Now that we can generate any color noise, we can see some interesting behaviour at the limits. With bluest noise, we can hear a high-pitched square wave, and the pitch changes with the sequencing rate. Thus, in the limit with an infinite sampling rate, bluest noise is a square wave of infinite frequency. And with reddest noise, we hear... Huh. That's just brown noise. What's happening here is that we have integrated noise, which is not exactly filtered noise. Indeed, brownian noise is by definition integrated noise. What we want is absolutely nothing, no change at all, though I don't know how to do that.