Music - OPL synthesis software engine for AY (part 2)

OPL synthesis on AY (part 2) 
djnzx48 

 8-bit samples

   Now I will explain the output method which
I used in this player.If you
are familiar with the chipAY,used in
Spectrum 128,you know that each channel, 
of which there are only three, can reproduce
only 16 individual volume levels ( 31
for  YM, but this is more complicated and requires use─
using envelopes). So, how is this pose─
allows us to output 8-bit samples? We
we can do this by combining thunder levels─
bones from three available channels. Since
distance between adjacent thunder levels─
dice varies, we can combine
them to create intermediate levels and
getting an 8-bit value.
   This method also has disadvantages. One
one of these disadvantages is that each
giving a sample requires changing three registers─
ditch, not one, but several registers
can be changed instantly. Bye Regis─
three are in an intermediate state,
unwantedintermediate levels─
lower volumes that cause distortion. More
one drawback is that this me─
This only works for monophonic
audio output fromAY.To obtain the correct
results on stereo requires a separate
4-bit output procedure.
   For efficiency we can use
table that gives the corresponding com─
bination of levels for each 8-bit value─
sample values. I've tried several different ones
approaches to creating such a table. Basic
the table accepts0 + 0 + 0as the very bottom─
level andf + f + fas the highest
- I tried to select the values manually,
to select a more suitable range,
which introduces less distortion, but I don't
I think the results were so good─
shi. I also tried to use the creature─
sound sample as input data─
useful for weighing the readings so that the most─
Most commonly used sample values
were closest to their ideal values.
Regardless of the method I use─
shaft, some distortion was inevitable, in
mainly those caused by intermediate
volume levels when switching from one─
from one level to another.
   If each sample is calculated as 
a + b + c, then the general difference between the two 
successive samples can be
modeled as

 |a1 - a2| + |b1 - b2| + |c1 - c2|

if the channels are updated sequentially. I
found that if one channel (say A )
applied for the largest of three values,
another channel  (B)  with an average value, and
the remaining channel (C) with the lowest value─
eat, then the total distance will be minimal─
valid for all possible combinations of values
channels.

Output program

   An example of code using the described al─
algorithm for achieving 8-bit sound,
close to the one used in the player:

setup: 
 ; select AY registers
 ld bc,#fffd  ;register port AY
 ld a,7  ;register AY
 out (c),a

 ; turn off tone and noise generation
 ld b,#bf  ;data port AY
 ld a,#3f  ;turn off tone and noise
 out (c), a

 ; ...

play_sample: 
;assume that the reading is generated and 
;stored in register A 

;table of volume levels, one byte each 
;for three AY channels for each 8-bit 
;counting values: 
 ld h,HIGH output_table

 ld bc,#fffd  ;register port AY
 ld a,8  ;select channel A
 out (c),a

 ld b,#bf  ;data port AY
 ld a,(hl) ;take the value for channel A
 out (c),a ;set the value there
inc l

 ld b,#ff  ;port register AY
 ld a,9  ;select channel B
 out (c),a

 ld b,#bf  ;data port AY
 ld a,(hl) ;take the value for channel B
 out (c),a ;set the value there
inc l

 ld b,#ff  ;port register AY
 ld a,10  ;select channel C
 out (c), a

 ld b,#bf  ;data port AY
 ld a,(hl) ;take the value for channel C
 out (c),a ;set the value there
inc l

   Using this method we can get
simple 8-bit output. But this code is not
ideal if we are going to withdraw a thousand─
chi samples per second. Please note
how should we switch between port
register select and data port eachtimes when we record to the channel. These days─
you:

 11-- ---- ---- --0- register selection
 10-- ---- ---- --0- data

   Volume levels we send
in AY, represent the four minor
bits that do not conflict with two bits─
mi used to distinguish between portsAY.
So we can encode part
port addressesAY in the table values─
tion and get the following:

play_sample: 
 ; our count in register A

 ld h,HIGH output_table
 ld bc,#fffd ;register port AY

 ld a,8  ;select channel A
 out (c),a

 ld a,(hl) ;take the value for channel A
 out (#fd),a ;set the value there
inc l

ld a,9 ;select channel B
 out (c),a

 ld a,(hl) ;take the value for channel B
 out (#fd),a ;set the value there
inc l

 ld a,10  ;select channel C
 out (c),a

 ld a,(hl) ;take the value for channel C
 out (#fd),a ;set the value there
inc l

   We have now eliminated the need for─
load into register B the required port address,
saving 38 cycles per count. But there is more
one improvement we can make.
   When we write to port #fffd, to select─
select the desired registerAY for recording, select─
The earlier register is remembered. If we record─
we put it in only one registerAY,this is a hut─
depends on necessitychoose one and the other
same register several times. Currently─
We write data to the channels in order 
A, B, C, A, B, C,which requires choosing a re─ 
hyster for each new channel. But what,
if we alternate the order of channels?
Something like this:

 count 0: output A, B, C
 count 1: output C, B, A
 count 2: output A, B, C
 count 3: output C, B, A
count 4: output A, B, C

...and so on. In this case, the last register,
selected during each sample, coincident
gives with the first register selected during
next sample. We can do this
having two different inference procedures that
we alternate. Like this:

play_sample0: 
 ; our count in register A

 ld a,(hl) ;take the value for channel A
 out (#fd),a ;set the value there
inc l

 ld a,9  ;select channelB
 out (c),a

 ld a,(hl) ;take the value for channel B
 out (#fd),a ;set the value there
inc l

 ld a,10  ;select channel C
 out (c),a

 ld a,(hl) ;take the value for channel C
 out (#fd),a ;set the value there
inc l

 ; ...

play_sample1: 
 ; our count in register A

 ld a,(hl) ;take the value for channel C
 out (#fd),a ;set the value there
dec l

 ld a,9  ;select channel B
 out (c),a

 ld a,(hl) ;take the value forchannel B
 out (#fd),a ;set the value there
dec l

 ld a,8  ;select channel A
 out (c),a

 ld a,(hl) ;take the value for channel A
 out (#fd),a ;set the value there
dec l

   Now we have won additionally19ta─
who, with only5OUT per sample, and not6.
There is one more advantage: we no longer
you need to reload the value table address
volume, just increase and decrease
pointer. (This is important! If we change the time─
channel dock, but let's read the volume table
in the same order for each ots─
couple, we will cause a sharp buzzing, because
channelsAandC quickly change their levels.)
   There's only one last addition left─
connection to our output program. We must
get waveform data from buffer to pa─
crumple, and byewe do it, we can too
mix drum samples from offsetIY.
In an earlier version I copied samples
drums into the buffer with expandedLDI,but this
turned out to be too slow. Here's the end─
a separate pair of procedures, each of which
takes a total of134cycles and25
byte:

 ;cycles ;bytes 
sample_out_routine_ay_mono_0: 
 ; get the countdown data
 ld a,(hl)  ;7 / 7 ;1 / 1
 inc l  ;4 / 11 ;1 / 2

 add a,(iy+0)  ;19 / 30 ;3 / 5
 ld e,a  ;4 / 34 ;1 / 6

 ; channel A output
 ld a,(de)  ;7 / 41 ;1 / 7
 out (#fd),a  ;11 / 52 ;2 / 9
 inc d  ;4 / 56    ;1 / 10

   ; вывод канала B
   ld a,#09       ;7 / 63    ;2 / 12
   out (c),a      ;12 / 75   ;2 / 14
   ld a,(de)      ;7 / 82    ;1 / 15
   out (#fd),a    ;11 / 93   ;2 / 17
   inc d          ;4 / 97    ;1 / 18

   ; вывод канала C
   ld a,#0a       ;7 / 104   ;2 / 20
   out (c),a      ;12 / 116  ;2 / 22
   ld a,(de)      ;7 / 123   ;1 / 23
   out (#fd),a    ;11 / 134  ;2 / 25

sample_out_routine_ay_mono_1: 
   ; получаем данные отсчёта
   ld a,(hl)      ;7 / 7     ;1 /  1
   inc l          ;4 / 11    ;1 /  2

   add a,(iy+0)   ;19 /30   ;3 /  5
   ld e,a         ;4 / 34    ;1 /  6

   ; вывод канала C
   ld a,(de)      ;7 / 41    ;1 /  7
   out (#fd),a    ;11 / 52   ;2 /  9
   dec d          ;4 / 56    ;1 / 10

   ; вывод канала B
   ld a,#09       ;7 / 63    ;2 / 12
   out (c),a      ;12 / 75   ;2 / 14
   ld a,(de)      ;7 / 82    ;1 / 15
   out (#fd),a    ;11 / 93   ;2 / 17
   dec d          ;4 / 97    ;1 / 18

   ; вывод канала A
   ld a,#08       ;7 / 104   ;2 / 20
   out (c),a      ;12 / 116  ;2 / 22
   ld a,(de)      ;7 / 123   ;1 / 23
   out (#fd),a    ;11 / 134  ;2 / 25
Other output methods

   Along with the output method for mono chips
AY,I have developed a program that allows you to use─ 
use other output methods. To them
include the output routine forSpecDrum
(essentially an 8-bit DAC providing
higher output quality), one for
one channel of the microcircuitAY (intended
to achieve monophonic playback─
division on a stereo chip, where the combination carried─
how many channels no longer work), and
also one for the left and right channels─
catching stereo chipAY (tone channels 
A, B  and C on the left and sampling channel D on the right). 
Each additional output method is expanded─
nut to make it exactly the same length─
we and the execution time are the same as the first me─
Output code,25byte and134cycle. It's easy─
makes it possible to introducechanges in each program─
me in which it is used, during
execution (no need to recompile─
lations).

Timings

   To achieve what the player
takes strictly constant time, order
execution must be carefully planned─
van. Some branches require
more bars than others, so inst─
actions that have no other purpose, cro─
I mean, how to waste time, it turned out to be very convenient─
tee. Of all the instructions available onZ80, 
EX (SP),HL  is completed for the longest time 
time relative to size in bytes (19
cycles to  1 byte), next - EX (SP),IX
( 23 ticks to  2 bytes).
   The most universal, in my opinion, is inst─
The manual was ADD HL,HL.Among its useful
properties: uses only one byte of memory─
ty, executed in 11 cycles, do not change
registers, exceptHL, and there is no appeal to pro─
arbitrary memory address.
   Other instructions that I found on─
were usefulRLDandRRD (18cycles, by2ba─
yta each), arranged in pairs, potencies─
undesirable effects of left shift
are canceled by a subsequent shift to the right.
Usually I need delays of5cycles, but
they could only be obtained with the help
conditionalRETwith a false condition. For such
cases were thoroughly checked─
ka to make sure that the condition cannot
be true!
   This table shows a list of useful things─
No instructions for synchronization are in order
efficiency (measured by the ratio ta─
who to bytes).

The clock bytes command spoils efficiency 
======= ===== ===== ============= ====== 
EX (SP),HL 19 1 19 (SP),HL 
ADD HL,rr 11 1 11 HL,F 
RRD/RLD 18 2 9 (HL),AF 
CPI 16 2 8 HL,BC 
CP (HL) 7 1 7 F 
LD A,(rr) 7 1 7 A 
INC rr 6 1 6 rr 
JR $+2 12 2 6 
RET cc 5 1 5 
LD A,R 9 2 4.5 AF 
NOP 4 1 4 

 Memory usage

   Along with clocks, memory is also
scarce resource, and it needs to be effective─
effectively used to store more
several 8-bit audio samples.  When
speed takes priority, some memory
will inevitably be used deployed
cycles, but I still found ways to save─
save several hundred bytes in different places.
   Of all the things that can be spent
wasted memory, tables with aligned data─
nym are probably one of the most
new.Table aligned to256bytes
spendsup to  255  extra bytes of memory, or in
average 127.5 bytes.
   To smooth out the problem I moved
all tables whose size was equal256
byte (or a multiple of it) to the beginning of the bank pa─
wrinkle. This allows you to keep them together without
wasting extra space.
   What about aligned tables of length
less256 bytes? Placing them next to each other
friend creates useless unused
space. In my case I wanted to have
ability to perform efficient indexing─
tion with the low byte of the address (to avoid─
costly arithmetic), so these
the tables had to fit within 
256 bytes. However, I realized that none of them 
doesn't really need alignment
tobeginning of the 256-byte segment.
   Thus, I was able to place these ta─
blitzes more or less anywhere in prog─
frames. The assembler macro issues a warning─
change if the table accidentally crosses
256-byte boundary, this makes it possible
know when to look for another place to
table placement. The only special one─
ity associated withWith this approach, I conclude
The problem is that the indexes in these tables are not
completely predictable and not based on the base,
equal to 0, but since the assembler generates
these indexes are at the compilation stage, then in prin─
Overall this is not a big problem. B
in general, leaving tables unaligned
there were significant memory savings.

 Conclusions

   This project didn't turn out quite like that.
how I naively imagined him at the beginning
development two years ago, but in some
he turned out to be better in relations, and I
I learned a lot during the development process.
   His true potential is yet to come
properly used, basically
due to the lack of a tracker (music come─
elk manually write in the form of instructions 
DB ). But sooner or later this may change─ 
to be.
 Will there be further experiments with
sound for  Speccy? Stay tuned and
find out! (Or maybe not...)

Share your thoughts about the article