Sound in Basic I - Programmable Sound Generator PSG

At the time of writing this post, it is planned for Commander X16 has three main ways to make sound:

  • Programmable Sounds Generator (PSG) in VERA
  • Pulse-Code Modulation (PCM) playback system in VERA
  • Yamaha YM2151 sound chip

PCM playback is not very useful for BASIC programmers but both PSG and YM2151 can be used effectively in BASIC programs. This post is about PSG.

! Important Update !

Before we go too deep into this post it is important to emphasize that ROM version R42 introduced dedicated BASIC commands to deal with sound. Specifically for using PSG sound system we have following "new" commands:


PSGCHORD - Starts or stops simultaneous notes on VERA PSG

PSGFREQ  - Plays a frequency in Hz on VERA PSG

PSGINIT  - Stops sound and reinitializes VERA PSG

PSGNOTE  - Plays a musical note on VERA PSG

PSGPAN   - Sets stereo panning on VERA PSG

PSGPLAY  - Plays a series of notes on VERA PSG

PSGVOL   - Sets voice volume on VERA PSG

PSGWAV   - Sets waveform on VERA PSG


The document below was written before those commands existed. It might still be useful to read it for better understanding on how PSG works but it is probably easier to use new commands instead of VPOKE-ing directly into VERA PSG Registers. Of course teh below examples still work just fine.

PSG Capabilities

Let’s summarize capabilities of Programmable Sound Generator

  • 16 independent channels or voices
  • Each voice can have any of four available waveforms
  • Each voice can be played in stereo with on/off control over left and right side
  • Each voice has independent volume control from 0 - silent to 63 - Max volume
  • 16 bit frequency value

We can also list some of the features that PSG can’t do, at least not directly in hardware.

  • It does not support ADSR envelopes
  • It doesn’t have timers to turn sound off, that has to be done programmatically

Registers in VRAM

VERA is responsible for PSG playback so we have to send data to VRAM where VERA can read it from. We already know that command to write to VRAM directly is VPOKE so where do we write?

Space reserved for PSG is from $1F9C0 - $1F9FF so total of 64 bytes. With 16 voices that means each voice has four bytes or registers to configure it. Or in other words following are memory locations per voice:

$1F9C0 - $1F9C3 Voice 0
$1F9C4 - $1F9C7 Voice 1
$1F9C8 - $1F9CB Voice 2
...

$1F9FC - $1F9FF Voice 15

Now let’s look at what those four registers look like:

Register Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
0 Frequency Bits 0-7
1 Frequency Bits 8-15
2 Right Left Volume
3 Waveform Pulse width

I think it is becoming quite clear that programming sound using PSG is not very difficult. Let’s describe each of the registers in more detail. I think it makes most sense to go from back to front.

Register 3 - Waveform

We have two bits (Bit 6 and 7) to choose between four available Waveforms. The values are:

00 - Pulse
01 - Sawtooth
10 - Triangle
11 - Noise

Or to represent them graphically:


There are plenty of resources online that describe the characteristics of these waveforms and that is far beyond the scope of this post. However it is important to highlight a few things.

  • PSG does not generate Sine waves, however Triangle is very similar and produces very warm sound and for most cases can be used instead of sine wave.
  • Noise is not completely random and also changes with frequency so we can have different pitch noise which is very handy.
  • Square wave is the sound we most commonly associate with the 8bit home computer era, however Pulse is not the same as square and we will discuss differences in the next paragraph.

Six bits of this register (Bits 0 to 5) are used to determine the width of the Pulse wave. Again let’s use graphics to illustrate how it works.


As we see all these three examples have the same frequency and therefore the same pitch but the width of hills and valleys is not which causes it to sound differently.

With 6 bits values can be between 0 and 63 and value 0 has a very narrow hill and therefore sounds very “thin”. Maximum valley of 63 will cause that hills and valleys are of the same width and the wave of that shape is what we call “square wave” and is much fuller and stronger.

Register 2 - Volume

Very simple and straightforward register. Bit 6 and 7 determine which side of stereo output the particular voice will sound. As expected 0 means off and 1 means on but left and right are switched if we assume that bit 7 is leftmost:

00 - no sound
10 - sound on the right side only
01 - sound on the left side only
11 - sound on both sides

The remaining 6 bits are used to set volume. 6 bits have range 0-63 where 0 means sound does not generated and 63 is the loudest. Note that we can’t define volume per side so both left and right side are played at the same volume or not at all.

Registers 0 and 1 - Frequency

Two bytes are available to determine the frequency and there is a formula that can be used to calculate the values in order to hit the exact frequency. Here is copy from the official manual

sample_rate = 25MHz / 512 = 48828.125 Hz
output_frequency = sample_rate / (2^17) * frequency_word

To make your life a bit easier I calculated most of the music notes in hearable range:

Note ID Note Frequency Hi Lo Note ID Note Frequency Hi Lo Note ID Note Frequency Hi Lo
1 C0 16.35 0 44 37 C3 130.81 1 95 73 C6 1046.50 10 249
2 C#0/Db0 17.32 0 46 38 C#3/Db3 138.59 1 116 74 C#6/Db6 1108.73 11 160
3 D0 18.35 0 49 39 D3 146.83 1 138 75 D6 1174.66 12 81
4 D#0/Eb0 19.45 0 52 40 D#3/Eb3 155.56 1 162 76 D#6/Eb6 1244.51 13 13
5 E0 20.60 0 55 41 E3 164.81 1 186 77 E6 1318.51 13 211
6 F0 21.83 0 59 42 F3 174.61 1 213 78 F6 1396.91 14 166
7 F#0/Gb0 23.12 0 62 43 F#3/Gb3 185.00 1 241 79 F#6/Gb6 1479.98 15 133
8 G0 24.50 0 66 44 G3 196.00 2 14 80 G6 1567.98 16 113
9 G#0/Ab0 25.96 0 70 45 G#3/Ab3 207.65 2 45 81 G#6/Ab6 1661.22 17 107
10 A0 27.50 0 74 46 A3 220.00 2 79 82 A6 1760.00 18 116
11 A#0/Bb0 29.14 0 78 47 A#3/Bb3 233.08 2 114 83 A#6/Bb6 1864.66 19 141
12 B0 30.87 0 83 48 B3 246.94 2 151 84 B6 1975.53 20 183
13 C1 32.70 0 88 49 C4 261.63 2 190 85 C7 2093.00 21 242
14 C#1/Db1 34.65 0 93 50 C#4/Db4 277.18 2 232 86 C#7/Db7 2217.46 23 64
15 D1 36.71 0 99 51 D4 293.66 3 20 87 D7 2349.32 24 162
16 D#1/Eb1 38.89 0 104 52 D#4/Eb4 311.13 3 67 88 D#7/Eb7 2489.02 26 25
17 E1 41.20 0 111 53 E4 329.63 3 117 89 E7 2637.02 27 167
18 F1 43.65 0 117 54 F4 349.23 3 169 90 F7 2793.83 29 76
19 F#1/Gb1 46.25 0 124 55 F#4/Gb4 369.99 3 225 91 F#7/Gb7 2959.96 31 10
20 G1 49.00 0 132 56 G4 392.00 4 28 92 G7 3135.96 32 226
21 G#1/Ab1 51.91 0 139 57 G#4/Ab4 415.30 4 91 93 G#7/Ab7 3322.44 34 215
22 A1 55.00 0 148 58 A4 440.00 4 157 94 A7 3520.00 36 233
23 A#1/Bb1 58.27 0 156 59 A#4/Bb4 466.16 4 227 95 A#7/Bb7 3729.31 39 27
24 B1 61.74 0 166 60 B4 493.88 5 46 96 B7 3951.07 41 110
25 C2 65.41 0 176 61 C5 523.25 5 125 97 C8 4186.01 43 229
26 C#2/Db2 69.30 0 186 62 C#5/Db5 554.37 5 208 98 C#8/Db8 4434.92 46 129
27 D2 73.42 0 197 63 D5 587.33 6 41 99 D8 4698.63 49 69
28 D#2/Eb2 77.78 0 209 64 D#5/Eb5 622.25 6 134 100 D#8/Eb8 4978.03 52 51
29 E2 82.41 0 221 65 E5 659.25 6 234 101 E8 5274.04 55 77
30 F2 87.31 0 234 66 F5 698.46 7 83 102 F8 5587.65 58 151
31 F#2/Gb2 92.50 0 248 67 F#5/Gb5 739.99 7 194 103 F#8/Gb8 5919.91 62 19
32 G2 98.00 1 7 68 G5 783.99 8 57 104 G8 6271.93 65 196
33 G#2/Ab2 103.83 1 23 69 G#5/Ab5 830.61 8 182 105 G#8/Ab8 6644.88 69 173
34 A2 110.00 1 39 70 A5 880.00 9 58 106 A8 7040.00 73 210
35 A#2/Bb2 116.54 1 57 71 A#5/Bb5 932.33 9 199 107 A#8/Bb8 7458.62 78 54
36 B2 123.47 1 75 72 B5 987.77 10 92 108 B8 7902.13 82 220

Armed with those values we can simply VPOKE them into registers and get the correct tone.

Example code

Ok enough theory let’s make some noise. Below are a few examples from simplest to more complicated.

Beep

Single beep in square wave by setting four registers of Voice 0, waiting in empty FOR loop and then setting volume to 0 to turn it off:

Falling

In the following demo we will be changing the frequency or pitch as we play the sound and make it sound like something is falling. We are using triangle waveform:

I used very common code to split value to high (Most significant) and low (Least significant) bytes:

MSB = INT(VALUE/256)
LSB = VALUE AND 255

Engine throttling

This demo will be a little more interactive and we will use Sawtooth waveform. The rest is very similar to previous one except that we are reading Joystick (or Keyboard mapped to Joystick keys) to increase the frequency and with that simulating higher RPMs and when released the engine RPM is slowly dropping.

Explode

The last demo will use the final waveform, the noise. To achieve a good explosion sound this time we will not be changing frequency but instead the volume to sound explosion slowly fading.

What next

One of the problems of programming sound in BASIC is that in BASIC we don’t have very precise control over timing so music is not easy to program or at least to make it sound great is not easy.

For that reason I wrote couple of libraries in Assembly to make including sound effects and background music to BASIC programs easier, links below:

Simplest Sound Effects Library for BASIC

Music Player for BASIC Programs


Comments

Popular posts from this blog

Commander X16 Premium Keyboard