#!/bin/sed -Ef # echo | ./bach.sed | aplay -r44100 # This line contains list of pitches, each letter representing a single pitch and a group of 5 letters representing a chord, to be arpeggiated s/.*/ZBpqwACoqvACZBpswBEpquwAoqvADoZmZgmquykoqvxknsvzjmqwAjlqtxikpvyijmptgjmptdgkotfikpsfknpsbjmpscfmprejopqdjkoqdikpsdgkptdgkotdhmpudikpvdgkptdgkotafkns/ # Z is used as a placeholder for a common note sequence, used to save a bit of space s/Z/psvy/g # This line repeats notes in a way that produces the correct arpeggios s/..(...)/&\1&\1/g # Here, non-arpeggiated ending is added and after ';', there's a data table that contains letters and pitches corresponding to them # Format of the data table is [letter][value] where value is the wavelength of pitch corresponding to the letter, written in base 9 with space char (' ') representing value of 1 # All wavelengths are pre-calculated with assumption that the output is treated as 44.1khz, single-channel audio # Also, each value in this table must contain exactly 3 digits # Yes, this is a really weird way to store values, but there are reasons for it # And right after the pitch table (after the last [letter][3 digits] sequence) there's an addition table for base 9 # E.g. 844 basically means 8 == 4 + 4 and 523 means 5 == 2 + 3 # Yes, this is also really weird but trust me, it's necessary for a computation further in the code s/$/afjmptpmpmjmjgjgafvxACAxAxvxqtsqafsvyy;a828b62 c580d550e522f4 4g363h344i327j3 k270l255m242n230o2 8p207q 76r 67s 58t 50u 42v 35w 2 x 08y 03z088A083B074C070D062E05584473463352342232 2 / # Here we replace each letter with a matching value from a table, Z is used as a separator # This is done in a loop that only ends when there's no more replacement possible :a s/([a-zA-X])(.*\1(...))/Z\3\2/ ta # This expression adds a '!' for each multiplication by 3 you're supposed to do on the corresponding digit before adding everything up to get the number value # That's the reason for using base 9: it can be easily converted to unary by tripling each digit value a certain number of times s/(Z.)(.)(.)/\1!!!!\2!!\3Z/g # Now addition table is used to convert each digit into the unary value of this digit using the addition table # Space characters are used to represent the resulting unary value, that's why 1 is replaced with a space in the tables above # Again, done in a loop :b s/([2-8])(.*\1(..))/\3\2/ tb # Now triple every string that has an exclamation mark after it :c s/( +)!/\1\1\1/g tc # Get rid of useless remaining characters, leaving only spaces (which represent the wave length) and separators ('Z') s/[^ Z]//g # For each wave, find its midpoint and mark it with a '~' character # Note that a space has ASCII value of 32, lowest of any printable character, while '~' has the value of 126, highest of any printable characters s/( +) ( ?\1)/\1~\2/g # Now we turn any space that happens to be after a tilde into another tilde # This is done to produce a square waveform, with spaces being the low points of it, and tildes being the high :d s/~ /~~/g td # Now some extra magic happens: since we only created one cycle of each wave so far, we need to repeat them so that each note plays for a certain amount of time # I chose, arbitrarily, 0.25 of a second or exactly 11025 samples # Now, sed is pretty slow at repeating a string many many times and it took me a while to come up with a relatively fast way of doing it: # First regex in this loop replaces any string longer or equal to 11025 characters between two separators with the first 11025 characters of it # The next string repeats any string between two separators thrice # So basically each note gets exponentially longer until it hits the required limit # This way we avoid doing expensive matching too often and hence save on computation time :e s/Z([ ~]{11025})\W+Z/\1/g s/Z(\W+)Z/Z\1\1\1Z/g te # Done! Now as all computation is done, sed will print the resulting string