Sound in Assembly - Use YM2151 to write Synthesizer

We will build on top of the introduction of using YM2151 in a previous blog post and create a simple synthesizer. If you are new to sound programming on Commander X16 I strongly recommend reading it first.

Sound in Assembly - YM2151 using ROM functions

The goal of the project is to use as wide a keyboard as possible. If we use the whole second row of keys on the keyboard from Tab to backslash we get 14 white keys. Above them we can then place 10 black keys for a total of 24 keys for 24 notes.

In addition we want to be able to switch octaves up and down and of course change instruments.

Commander X16 comes many preset instruments (patches) so let’s use big chunk of them 0-79 and to make access to them easier let’s make 8 shortcuts using function keys 1-8 for following instruments:

F1 - Grand Piano

F2 - Church Organ

F3 - Vibraphone

F4 - Harmonica

F5 - Jazz Guitar

F6 - Flute

F7 - Violin

F8 - Trumpet

In order to make fully functional synth we have to figure out how to

  • Insert our custom Keyboard handler to be able to catch key presses
  • And write the User Interface using custom tiles

OK, enough theory, let’s write a synthesizer program. It is so simple I wrote the complete functionality in one afternoon and then of course I couldn’t help myself and spent a full week on User interface.

Keyboard Handler

In order to have a functioning keyboard on a synth we have to write a fast keyboard handler. Commander X16 has the default keyboard handler, of course, but we want to replace it with our own.

That is a very simple procedure, the address of the handler is for our convenience stored in RAM and we can overwrite it with an address pointing to our custom handler. It is located at $032E.

Keyboard is connected using the PS/2 interface which is essentially a serial interface. That means that all keystrokes come in sequentially and we can’t read the state of keys using a matrix like it was done in the old days. Every time the key is pressed or released an interrupt is triggered which calls the routine (let’s call it keyboard handler)that the address $032E /$032F is pointing to. A register will contain the code of the key pressed or released. For example if key 1 is pressed the code will be $02 or %00000010 in binary and when the 1 key is released the value in A register when keyboard handler is called will be $82 or %10000010 in binary. As we can see it is the same value with only difference being bit 7 being cleared for press and set for release. This will become very handy when we want to know when the key was pressed and when released for certain instruments with sustained type of sound (e.g. organ) vs percussion style instruments (like piano).

When we understand that the keyboard handler is very simple, we can either create a lookup table of all the keys we will use in our program and loop through them and then decide what to do with each. Another way would be to create a lookup table of all possible keys and identify the one by just using key id as Index into it. I first wrote the one using loop and because this is not professional instrument but just a demo and I didn't perceive any noticable lag I just went with it.

Most of Keyboard Handler is pretty straightforward but all operation depends on the data structure where I keep status of every key:

Pressed - Flag that is set only when the key is pressed. It is indication for thein program to find available channel and trigger the note. It is cleared right after.

State - Keeping the state of the key 1 for pressed and 0 when released.

Channel - in this field we store the Channel number being used. This one is set after the key is pressed, channel found and note triggered. It is used to release the right channel after the key is released.

Released - Flag that is set when the key is released. It is indication for the main program to clear used channel and make it available again.

Sound functions

All the necessary techniques are covered in my previous article and the sound functiosn for Synth are xtremely simple and short. All we need is

YMInit - Switches to ROM bank 10 and Initializes YM2151

LoadInstrument - Loads the same patch set in variable Instrument to all 8 channels

PlayNote - This function first need to find first available channel (out of 8) and sets the Octave/Note and triggers the note.

ClearChannel - Finally we clear the channel of the note that was just released.

That is in total only 40 assembly commands.

Main loop

Main loop of the Synth is again extremely simple and very easy to understand. We are just looping though all the key options and check for changes in key pressed and released and update their new states accordingly or leave them as they are if there are no changes. If there was change we simply call PlayNote or ReleaseChannel depending on the change. The whole loop is copied here:

With that we have fully functional Synth player but without any visual representation on what is going on.

Graphics

As is usually the case with my projects, I spent 90% of the time in making the program look good. In this case it meant figuring out the layout of the screen and then drawing all the custom tiles.

The game runs in 40x30 tile mode in 256 colors and I used up almost 256 tiles


Binary can be downloaded here

Source code is available here

Comments

Popular posts from this blog

Commander X16 Premium Keyboard