back to blog index

A musical quine/polyglot

This is a program which generates some music when ran as Python, or prints itself when ran as Perl.

popcorn_quine.py (977 bytes)

Here it is with Perl syntax highlighting:

q=0#=;$p=q(q=0#=;$p=q(%s);$d=q(%s);printf$p,$p,$d,10;%c);$d=q(

# generates a cover of Popcorn when ran as Python
# is a quine when ran as Perl

from math import tau,sin
import struct,wave
seq_a = [2, 0, 2, -3, -7, -3, -10]
seq_ap= [2, 0, 2, -2, -7, -2, -10]
seq_b = [2, 4, 5, 4, 5, 2, 4, 2, 4, 0, 2, 0]
notes = (seq_a * 2 + seq_b + [2, -3, 2] +
         seq_a * 2 + seq_b + [2,  4, 5] +
        [x+7 for x in seq_ap*2 + seq_b + [2, -3, 2]
                    + seq_a*2  + seq_b + [-3, 0, 2]])
lengths = (([1]*6 + [2])*2 + [1]*14 + [2])*4
out = [0]*1269040
for i,x in zip(lengths,notes):
    for st in range(662):
        t=st/44100
        tft=tau*880*2**(x/12)*t
        samp=sin(tft+.75*sin(2*tft)+sin(tft)/4)
        gain=t/.01 if t<.01 else 1-(t-.01)/.005
        out[q*10023+st]=int(samp*gain*32767*.4)
    q+=i
f=wave.open("tinypop.wav", 'wb')
f.setparams((1,2,44100,0,'NONE',''))
f.writeframes(b''.join(struct.pack('<h',x)for x in out))
f.close()
#);printf$p,$p,$d,10;

It's a relatively simple quine in Perl. The first line starts with a dummy string literal (using = as the delimiter), in order to differentiate between Python and Perl. The rest of the first line is the basic quine framework: a format string ($p) containing a description of the entire file with the strings themselves replaced with %s. Then $d contains the entire Python code, and the very last line prints out the format string with both of the string literals replaced into it (and a trailing newline for good measure). Perl's parenthesized string literals (q(text)) make this much easier than other languages as there's no need to escape anything in $p, because parenthesized literals can contain more parentheses, as long as they are matched up properly.

To Python, the code looks like this:

q=0#=;$p=q(q=0#=;$p=q(%s);$d=q(%s);printf$p,$p,$d,10;%c);$d=q(

# generates a cover of Popcorn when ran as Python
# is a quine when ran as Perl

from math import tau,sin
import struct,wave
seq_a = [2, 0, 2, -3, -7, -3, -10]
seq_ap= [2, 0, 2, -2, -7, -2, -10]
seq_b = [2, 4, 5, 4, 5, 2, 4, 2, 4, 0, 2, 0]
notes = (seq_a * 2 + seq_b + [2, -3, 2] +
         seq_a * 2 + seq_b + [2,  4, 5] +
        [x+7 for x in seq_ap*2 + seq_b + [2, -3, 2]
                    + seq_a*2  + seq_b + [-3, 0, 2]])
lengths = (([1]*6 + [2])*2 + [1]*14 + [2])*4
out = [0]*1269040
for i,x in zip(lengths,notes):
    for st in range(662):
        t=st/44100
        tft=tau*880*2**(x/12)*t
        samp=sin(tft+.75*sin(2*tft)+sin(tft)/4)
        gain=t/.01 if t<.01 else 1-(t-.01)/.005
        out[q*10023+st]=int(samp*gain*32767*.4)
    q+=i
f=wave.open("tinypop.wav", 'wb')
f.setparams((1,2,44100,0,'NONE',''))
f.writeframes(b''.join(struct.pack('<h',x)for x in out))
f.close()
#);printf$p,$p,$d,10;

The song itself is a cover of the beginning of Popcorn, originally by Gershon Kingsley, most famously covered by Hot Butter. This cover mostly reproduces the "pop" sound from Hot Butter's version, doesn't include any other instruments and is only the first ~30 seconds. (I am still quite a newbie at FM synthesis and haven't managed to reproduce the other instruments yet - maybe I will make an updated version of this some day, but for now I thought this was interesting enough to release on its own.)

The beginning of the code constructs the lengths and pitches of each note. It utilizes some repetition in the melody, the full notes array is constructed out of 3 basic repeating subsequences. The 2nd half of the song is just the first part but transposed up 7 semitones, and a few notes changed here and there (I think this is called a key change? But I have literally zero formal music education, so I'm not sure).

The notes array contains note pitches encoded as semitone offsets from A5 - this means the frequency of a note can be computed as 880 * 2^(x/12) where x is the note value. The lengths array is just number of half-beats at 132 bpm.

After the pitch/length data comes the main synthesizing code. There is a preallocated output array out containing 16-bit PCM samples. It's long enough for the whole song, and it is filled by just overwriting some parts with the synthesized tones.

The synthesizer itself is a very basic FM synthesizer with 2 modulators feeding into one carrier. When making it, I pretty much just messed around with the parameters until I got something that roughly sounded like the "pop" I was after. In the main loop (for st in ...), st is the sample index (number of samples since the start of the note), t is the amount of time since the start of the note (which is just st divided by the sample rate, 44.1KHz), and tft is the current phase of the carrier, computed as tau * frequency * t. It's used often enough in the FM calculation that I factored it out to a separate variable. The 2 modulators are combined with the base frequency using some ad hoc weights to generate the FM sample.

The gain is computed as a very simple pseudo-ADSR envelope (where the decay and sustain phases are both nonexistent) - 10ms of attack and 5ms of release. This means the volume starts at 0%, then linearly rises to 100% over the next 10ms, and then drops back down to 0% in the next 5ms. This is then just multiplied with the sample from FM and a final volume multiplier of 0.4.

The final sample (a floating-point number between -1 and 1) is then converted to a signed 16-bit integer and written to the output array. The magic constant in the index q*10023+st comes from [sample rate] * 60/[bpm] / 2, where the /2 is because a 1 in the lengths array corresponds to a half-beat duration. Plugging in the sample rate of 44.1KHz and bpm of 132, we get 44100*60/132/2 = 10022.7 which rounded to nearest integer gives 10023.

The end of the code just writes out the samples in wav format using python's built-in wave module. The parameters to the setparams call are: number of channels (1, mono), bytes per sample (2, 16-bit audio), sample rate (44100 Hz), number of frames (set to 0, automatically fixed by the writeframes call) and compression options ('NONE' for no compression - the only supported value). Then we just encode each sample as 16-bit little endian and write the frames to the file.

The very last line in the file is the final part of the Perl quine. It's just a comment in Python, but it terminates the string literal and prints out the formatted string in Perl.