I have previously posted concerning this project, but thought it could be instructive for some of our members to take a look at the inner workings.

While musicians, and the Omnicalc play() command, typically think of notes in terms of octave and counts of a beat (e.g. quarter note, half note, etc.), Floppy Tunes operates on raw timing of oscillations, so we need to translate traditional pitch and beat counts to a wavelength and some notion of how many times to oscillate to obtain a uniform timing for different notes.

We can compute wavelengths for standard note values ourselves using two pieces of information. First, the standard tuning note, is an A, and it has a frequency 440hz - this means it oscillates 440 times per second, so to find the length of a single oscillation, we just take the reciprocal. Second, we know that every change in octave represents a doubling in pitch, and the standard musical scale is divided into 12 evenly spaced notes. So multiplying the frequency by 12√2 should allow us to step our notes. But since we're working with wavelength, we again take the reciprocal. Thus our final expression is (1/440)(1/(12√2))^x, which can be simplified to ((12√2)^(-x))/440, and we can fill in our table of wavelengths (note, that Floppy Tunes actually uses the time between the positive and negative portions of the wave form, which is half these values).

In an ideal world, there should be a simple expression to normalize our wavelength timings (for example, the frequency), but in our case it's a little bit empirical, since we have to deal with the cost of actually instructing the hardware to generate oscillations. Thankfully, others have done the hard work of tabulating them. If you look at this table, you'll notice the last column starts off very close to tracking with the frequency (values for A start off 220, 440) and then drifts as the wavelength is shorter and the CPU + IO costs grow.

But we still don't typically read music in seconds, we read music in beats, and then assign a tempo, usually in beats per minute. So our first bit of mathematical code is handling this conversion:

def bpmToSeconds(bpm, divisor, default = 4):
   return (60. / bpm) * default / divisor

We typically measure beats in some number of quarter notes, but since this isn't always the case, we add in a little bit of extra logic to allow this to be changed.

Next we use a little-known API to help us process a play() string. Python's regular expression module, re, also includes a Scanner class, to assist in tokenization. All we have to do is provide regular expressions for each token class, and an event handler to further process those tokens:

scanner = re.Scanner([
         (r"X", s_X),
         (r"T[0-9]{2,3}", s_T),
         (r"M[NSL]", s_M),
         (r"L[0-9]{1,2}", s_L),
         (r"<|>", s_UpDown),
         (r"P[0-9]{1,2}\.*", s_P),
         (r"N[0-9]{1,2}", s_N),
         (r"[A-G][+-]?[0-9]{0,2}\.*", s_AG)])

Here, we copied implementation (and grammar details) from the Detached Solutions documentation page for Omnicalc's play() command. This required a little bit of fiddling, since there is a fair bit of internal state that we had to keep track of. Finally, a little bit of book-keeping was needed to make sure we didn't overflow our cycle-counts on very high notes (mostly a problem for extended rests). You can see the implementations for all of the event handlers here, but they're pretty much exactly what you think they should be from reading the documentation.

I didn't want my end-users to be dependent on an assembler, so I pack the resulting data directly into a .bin, using two python functions to simulate .db and .dw:

def cantbin(verb,arg):
   import sys
   print ""
   raise TypeError("Don't know how to %s this %s: %s" % (verb,repr(type(arg)),repr(arg)))

def db(*args):
   return "".join([chr(arg) if type(arg) == int else (arg if type(arg) == str else cantbin("db",arg)) for arg in args])

def dw(*args):
   # z80 is Little-Endian
   return "".join([(chr(arg % 256) + chr(arg / 256)) if type(arg) == int and not arg / 65536 else cantbin("dw",arg) for arg in args])

def header(title, artist, album):
   return   db(0xBB,0x6D)+\
def footer():
   return dw(0,0)

Tacking it all together is the following awful one-liner:

      output = "".join([header(*argv[2:])] + [dw(wvlen,panic(cycles)) for wvlen, cycles in reduce(lambda a,b:a+b,scanner.scan(instr)[0],[])] + [footer()])

This flattens the data produced from each individual token into a single long list, using reduce, then calls dw() on the whole thing, and squishes it between our header and footer. We also apply any last-minute rounding and throw errors on bad data with our call to panic().
Register to Join the Conversation
Have your own thoughts to add to this or any other topic? Want to ask a question, offer a suggestion, share your own programs and projects, upload a file to the file archives, get help with calculator and computer programming, or simply chat with like-minded coders and tech and calculator enthusiasts via the site-wide AJAX SAX widget? Registration for a free Cemetech account only takes a minute.

» Go to Registration page
Page 1 of 1
» All times are UTC - 5 Hours
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum