Sunday, 25 February 2018

How Patchwork (patch)works

As you may know, the Commodore 64 has 64kb of RAM.   On boot-up, however, that 64kb isn't all accessible from the machine directly. Certain blocks of the RAM space are patched out with various ROMs and the I/O space for the custom chips. (The VIC-II graphics chip, timing/port control and our beloved SID chip)

Here's the RAM setup when your Commodore 64 boots up:

$0000-$9fff - Mostly free (some kernal things are patched into zeropage and there's also the stack)
$a000-$bfff - Basic ROM
$c000-$cfff - Free
$d000-$dfff - VIC-II,SID,I/O,Character Set address space
$e000-$ffff - Kernal OS ROM

This isn't a fixed setup, however.  By changing the bit values in the $01 register you can swap out the ROMs and the chip address space however you like, either on a permanent or temporary basis.   This mostly applies to assembly language programs, however some BASIC games would make copies of the Character Set into RAM using the above method.   (if you ever saw a "Please Wait" message
in a BASIC game and then a minute of nothing it was probably doing that)

LDA #$35 , STA $01

A large amount of standalone video games swap out the Basic and Kernal because they don't use them.   This is achieved by setting the register $01 to #$35 and gives you 60kb accessible at any time.   While the CPU can see all the RAM without a problem the VIC-II chip can only see
16kb ram blocks, so from a graphical standpoint you'll usually set one area of RAM as 'the graphics ram' and copy anything else there as needed.

Patchwork relies on the fact you can also swap out the SID chip address space into RAM.  This needs a bit of background first to explain.

How music players work:

A music player is usually split into two parts, the music driver and the music data.  The music driver will have a specific set of addresses the game/demo can call to initialize, play and stop the music.  For 8-bit machines these are usually 'per frame', meaning every time the raster beam passes over the screen the music driver is called to keep a constant tempo.  Your typical music driver when
called will do some housekeeping (updating where it is in the song, changing instruments etc.) and then write to the set of registers where the soundchip is.  In our case the standard SID chip sits at the $d400-$d41c address space.  For custom machines with extra SIDs their addresses can be at any place in the $d400-$dfff range but that first SID is always at $d400.

LDA #$34 , STA $01

So, with that knowledge if we call the music player normally it'll write to the SID registers and you'll hear the notes for that frame of music.  But what if we swap out the $d000-$dfff register space to RAM first, by setting $01 to value #$34.   Now what happens?

Well the music player runs fine, but the register data gets written into RAM instead of to the SID chip so we don't hear it.  If we copy that data somewhere and then re-enable the SID chip (with LDA #$35 , STA $01 again) we still don't hear the music, because as far as the SID chip is concerned nothing new has been written to it.  Switching out the SID chip also doesn't stop the SID chip playing whatever is already in the registers, it just stops it being able to receive new data until it's enabled again.

If we then write the data we copied back to the SID chip we'll hear the new frame of music.  With this simple setup we can play a SID tune, but we have the option to manipulate the SID data first before it goes out to the chip.  You may have seen some of my other videos where I get the SID chip to pretend to be other systems. (like the Spectrum or NES)  This is using the above method, by manipulating the data we get from the music driver and then writing that manipulated data back to the SID in realtime.

What Patchwork does with the data:

Patchwork is a bit different in that it doesn't use that data in real-time, instead it stores it into an incremental buffer.  Because it's mostly a proof-of-concept I decided to store the data uncompressed, meaning every frame 25 bytes are written to memory.  (the registers after the volume control aren't used in normal music playback)  This means Patchwork has a limit of 27 seconds of music, but if I come back to this project in the future there are plenty of opportunities for improving that.

So, now we've captured some sid data into a buffer we don't need the original song to play it back.  If we send that buffered data to the SID chip every frame it'll play that instead.  

This method does has some drawbacks however.  Because the SID chip has some timing inconsistencies with the way the ADSR envelopes and the waveform Gate works, sometimes the CPU cycle count in a music driver is the reason the music sounds how it does.  This is especially
true of older game music drivers, most modern drivers have some form of "Hard-Restart" enabled which is a way to get SID chip playback pretty much 100% consistent on every frame.

You can read more on the subject in this thread , by people far more knowledgeable than myself.  One day I'll write up how the AY music player works which had it's own unique challenges. :)

Anyway, so for some game music drivers the time between SID register writes is a factor in the reliability of their playback.  This isn't a complaint on my part.  What this means (to me) is the way the driver has been developed and the sound of it have a symbiotic relationship, and are so heavily fused together that changes to one directly affect the other.

On Patchwork's end it doesn't try to emulate the playback of different music drivers, it just wants to write the register data back to the SID chip with one method.   There are a couple of ways to try and fix the 'problem' though they don't cover all cases:

  1. It's more consistent to write SID registers backwards.  I don't know the exact reasons for this but I assume hitting the ADSR registers first before the Waveform has more of a success rate.
  2. Putting a cycle delay between register writes.  For the SID player I've put in a few NOPs between writes, in Patchwork's own driver there's enough CPU manipulation between each section that the register writes are spaced out a bit.
  3. Manipulating the data before it gets to the SID.  I've only used this on Patchwork's driver side.  Because instruments can be made at any point in a recording the SID registers can be in any state.  This means that while you may have lined up one instrument to play at it's beginning with the Gate enabled, other channels could be in a 'note-off' (gate off) state, or something else.   To get around this I artificially force the Gate back on for two frames at the start of each channel.   This helps with consistency when playing back because two frames is usually enough time for the ADSR to have reset to a constant state, even if it's not completed it's cycle before being switched off again.  This third option can change the sound of the instrument for those first two frames, but as we're using 'cut up' parts of an existing song it's going to sound a bit different anyway.
Manipulating the SID data:

So now we have the data we've recorded, why not change it around a bit?  The pattern editor has a few different pitch effects, and the filter and channel assignments can be changed around.

Loops and speed changes:

Because each frame is basically a contained moment in time (like a sample) we don't have to play them in order.  This means Patchwork can play an instrument in reverse and loop forwards or backwards if required.  We can also speed up or slow down the playback, by delaying the frame increment with a timer (slower) or skipping through the data frames at a faster rate. (faster)  I think slower has some possibilities but in hindsight faster isn't all that useful.

What is different to a sample, however, is that the SID chip is expecting a note to go through a particular cycle of events.  The creator of the SID chip ( Robert Yannes ) went on to co-found Ensoniq, and the chip follows a traditional synthesis setup. That being:
  1. At the start of a note the Gate (equivalant to a note-on in midi) is enabled.
  2. While the Gate is active the Attack, Decay and Sustain values of the ADSR are cycled through by the SID chip.
  3. When the Gate is disabled (a note-off) the Release cycle of the ADSR starts. If the first three stages haven't been completed in time it stops (afaik) wherever it is and goes straight to the Release timing.
Played notes on the SID don't exist in a bubble, wherever they have got to in that cycle can affect how the next note plays.   This is the important bit, the next note that plays may not play at the right volume, or at all if the ADSR isn't in a ready state to begin again.   As far as I know this is why techniques like 'Hard-Restart' (linked earlier on) were developed.  It's also why I manipulate the first couple of frames of an instrument directly.  Even if the song data is playing backwards it should at least START with a solid note setup. :)

(as a sidenote, thanks to the SID chip I got a hands-on education on how synths work in the '80s)

Pitchshift and Octaver:

Making an Octaver fx on the c64 is quite simple, sound pitch is stored as a frequency value which means an octave higher is achieved by dividing the note by 2, and dropping an octave by doubling it.  As it's an 8-bit machine you need to do some carry value checking as you move through the octaves but it's quite do-able and the loops are fast.

This is the first time I've tried making a Pitchshift on anything, and surprisingly it kinda works for the most part.  It's unfortunately the most CPU intensive effect in the driver though, right at the end of development I had to re-work the GUI code to get it working again on NTSC machines. 

One problem with making this effect is because the C64 pitch is stored as a frequency value, music drivers aren't locked into a particular tuning scale.  This means that, for example, middle C in one driver can have different values to another depending on what frequency table they use.  A pitchshifter fx has to go through two stages to get the effect we want:
  • Detect which note we are closest to already.
To do this the fastest method I found was to take the source pitch down to the lowest octave it can possibly reach.  At this point I can compare it against a baseline pitchtable (in this case the one at codebase64 made by mr.SID) to find the closest possible match.  During this process I store a few values:
  1. The pitch offset between the source note and known 'good' note so we can apply this back later to make it as close to the original frequency table as possible.
  2. How many octaves we had to move to get to the lowest octave so we can shift it back later.
  • Add/subtract the pitchshift value
Now we have an idea where the note is we can add/subtract the required amount of semi-tones to our frequency and take that value from the baseline pitchtable.  After which we can shift it back up to the octave it was in, or if we're subtracting drop it an octave lower.  We also re-apply the pitch offset we stored beforehand so it's shifted out a few cents to match the original note's pitch.

This probably isn't the most efficient way of doing it, and I thought there'd be more errors than I was getting but for the most part it seems to work ok.  Only wildly shifting notes/pitch tables seem to have a large problem.   The range is an octave above or below so maybe if I'd increased that the problem would be more apparent.

Both the octaver and pitchshift can be applied per channel so individual layers can be manipulated.  One thing I forgot to mention in the docs is that a noise waveform note isn't affected by any pitch changes, I decided to do this because it's mostly used for drums and you usually want those to be consistent in a song.

Sweeps and filters:

These effects are relatively simple, changes to the filter setup directly alter the values in $d417 and $d418, which are where the filter assign and type are stored.  I do a few basic checks to see if the filter is enabled at all and then clear the assigns so you don't hear silence.  (on the SID assigning a channel to the filter without having a filter enabled silences the channel)

The pitch sweeps take the first pitch value of the playing instrument as the base, and then apply the addition/subtraction value stored in the instrument each frame.  When the high value of the pitch register rolls over to zero I stop applying the pitch sweep and silence the channel.  

Likewise the filter sweep takes the first filter cut-off value and applies the value stored in the instrument, but at the start of each beat rather than per frame.  This allows you to get much more rapid filter sweep effects (which is mostly what this is used for) and also means that if you change the tempo the sweep time remains relatively consistent.

Future ideas:

As this was written mostly as a 'proof-of-concept' I stuck to a list of items I thought I could comfortably finish.   If I return to the project in future there are a few things I'd like to investigate:

Independent channel playback:

While there are pattern commands for leaving channels running, essentially Patchwork is playing one instrument at a time on all channels.  What would be a more flexible approach is having each channel able to run it's own instrument.  This would mean instruments would need to have their own channel assigns (saying if it's a 1-3 channel instrument) but it also means song parts can play on any channels rather than being fixed to their original positions.   The benefit of the latter is when using SID features like ring mod or sync, as they are controlled by their playing position in the channel order.

Recording compression:

I'll admit when using Patchwork I haven't really come up against the 27 second recording limit yet.  However it would be quite possible to pack the data down from it's 25 byte frame size.  One thing to take into consideration is that the sample stream isn't used in a linear fashion, instruments can play backwards and also when recording you can be in the middle of an existing stream.  Even though you've overwritten what's there any data left after you stop recording has to be maintained.

The easiest idea is to try a smaller frame format with a fixed size, while all registers can hold a full 8-bit value realistically not all of them do so.  If it's 10 bytes smaller that adds up and we don't have to do much data management.

Another idea is to split out each register as an RLE stream.  This may sound crazy with 25 registers but if you've done music data compression before you'll know that the individual elements of a stream don't rapidly change all that often.   There'll be some (like the waveform register) that won't work as well but notes, filter and ADSR should yield quite a saving.   The other benefit with this idea is that potentially the data management isn't as horrible as first imagined.  If you start recording in the middle of existing data you only need to change where the previous RLE streams have ended, and you can check forward where the next ones are and change their values as you record.

Midi sync:

The final thing, especially as I have liveplay options, is to put in some midi sync.   I had a very basic keyboard input working as a test but I'll need to implement a timed midi cache to allow it to work with sequencers.  They send a lot of data!

No comments:

Post a Comment