OPL synthesis on AY
djnzx48
Introduction
A series of sound chipsYamaha OPLappeared─
took place in the 80s and provided means
vom FM synthesis very recognizable music in
many famous computer and arcade games
games of that era.
In December2018I tried to restore─
give this form of sound generation using
128K Spectrum,partially inspired by de─
monstration ATARIN of the same year. Also by─
the attempt of the forum participantsWOSinfluenced
in the early 2000s, forceSpectrumto play─
produce files MOD.
After some initial failures, I
started the project again in February2020. B
April the player program was functional─
nationally completed, since then there have been additions─
Some refactorings and improvements have been made.
Although the PM synthesis used theremuch
simpler than the Yamaha chip, which is my engine
was supposed to imitate, but it produces three 8-
bit sound channel and drum/sam channel─
pilaf The built-in is used for output.
sound chipAY-3-8912,but if required
better sound quality,also supported
8-bit DACs such asSpecDrum.Finally,
you can use interesting alternatives─
new forms of synthesis, such as unison and syn─
chronization of the oscillator, as well as ornaments and
changes in pitch.
Start of development - sound output
Before you started, you had to approx.
determine the characteristics that should
have a player. Among other requirements I
set to use 8-bit samples
for better sound quality than conventional
forAY4-bit. I also wanted an hour─
The sampling rate was at least10
kHz. With Z80,operating at frequency3546900
Hz, this leaves approx.
3546900 / 10000 = 354 clock cycles.
Through experimentation I came to you─
filling in 331 clock tick for warehouses─
is taken from: 134 clock for the reference output, 4
beat on EXX to switch to alterna─
tive set of registers, three times 63 cycles
to generate three counts and the final─
new 4 cycles for reverse switching
ra registers.
134 Readout output
+ 4 EXX
+ 63 Generation of reference #1
+ 63 Generation of reference #2
+ 63 Generation of reference #3
+ 4 EXX
------------------------
=331 Amount
I no longer use such strict
ratios, since generation routines
counts take up different numbers so─
tov there are branches that allow you to perform about─
serving code. However, I tried to keep it in mind─
live the described cycle strictly in 331
beat. With the only exception: code, re─
key patterns - it is called enough─
definitely rare, and a waste of an extra hundred
the beats will not be perceptible.
For a given frequencyZ80331 the clock cycle will be
match the sampling frequency at─
measured 10.7 kHz.
Structure of the main loop
I'll start with a general description of the main cycle
playback, during which the buffer
sixty samples are generated from the output.
(Actually there are two output buffers─
yes, which switch so that
playback time of one was possible
fill in another.) 60 samples turned out to be
a reasonable compromise that allows you not to
mother too much memory in expanded
loops and at the same time get the code without overloading─
affected by management overhead
channel and melody state.
Counting Operations
====== ========
0-1 Reading pattern data for a channel
events,event/ornament processing
1-3 Reading pattern data for channel A,
setting parameters
3-13Generating samples for channel A
13-15Reading pattern data for channel B,
setting parameters
15-35Generating samples for channel B
35-37 Reading pattern data for channel C,
setting parameters
37-57Generating samples for channel C
57-59Updating the position in the melody, reading
pattern data for channel D,
setting sample pointers
The following table describes the use─
each of these channels:
Channel Used for
===== ================
A Bass channel (FM / unison)
B Tone channel (FM / sync
oscillators)
C Tone channel (FM)
D Sample channel
E Event channel
Channels A, B, and RGB7 are tonal. Ka─
cashA is unique, since it works on─
half the sampling rate of the other two and
intended for use as
bass channel. Generating samples for the channel
Andtakes only half the time compared to─
incompatibility with other tone channels.
Channel E was created in order to
tiya (commands affecting the state of the game─
tearer) could be unloaded into a separate
channel and notwasted execution time
other channels.
At the beginning of the main loop, the first task
is to issue a count from the output buffer─
ra, which is empty when qi is first performed─
cla.
Several types are used for control
Ymerov. The melody timer decreases every time
times during the passage of the main cycle, and when
reaching zero the player goes to
next line of the melody. Promotion by
melody line decreases the pattern timer,
when reaching zero the player switches─
dit to the next pattern. For each channel
also has its own timer indicating
how many lines do you need to advance to
playing the next note. And finally, at
Each instrument has its own timer, which─
This indicates when to switch
to play the next line in the instrument─
nta.
These timers are mainly used by qi─
glue for processing the event channel. New co─
being will only be processed when
the melody timer will be equal to the current speed─
these melody, and the event channel timer is
zero. This will indicate as new
melody line, and for the event, is present─
in this line. In other cases
the event channel is not processed, but instead
in order to waste time on the NOP series,
why not do something useful?
Therefore, this time is used for processing
weaves of ornaments.
Processing of ornaments
Ornaments change pitch with flow─
time for some integer by─
Lutons. They can be used for decoration─
sound, to weaken the attack of a note or
(perhaps most often) to create would─
strictly arpeggiated chords.
All three tone channels can be used
call for ornament processing. However, access─
a large amount of time allows processing─
only one channel. In addition, the main
the cycle is executed more than178times per second,
which is too fast for ornaments and adding─
creates audible low-frequency noise in the melodies─
diyu. Therefore, three channels are processed according to
queues over several iterations of the cycle─
la - each channel is only about60times
second, which is closer to traditional ornaments─
there's Spectrum. This cyclic sequence─
flair is performed using self─
modifying code updating each
times the address of the transition to one of the three subpro─
gram of ornament.
What does ornament processing look like for
one of the channels. First we load
the ornament indicator for this channel and
we read the meaning of the ornament from it. This
the value can be ORNAMENT_END, which means─
calls to the end of the ornament. If so, we
reset the pointer to the beginning of the orna cycle─
cop and readnew meaning, otherwise we
increment the pointer by one and save─
We use it for the next iteration.
Now we need to handle the height offset
notes. To do this, you need to take the offset
(bend) for the current channel and accumulated
the value of its offset, add them together and
save new value.
The next value we need is
the value of the base note corresponding to the pic─
last note played. If it is equal to well─
liu, we don't add anything and stop
ornament processing. Otherwise we add this
meaning to the meaning of the ornament, read
earlier. Both of these meanings are pre-mind─
sheath for two, so it is enough to take
high byte of the note table and read pos─
Therefore, two bytes, which will mean─
lowering the pitch of the note. Next we fold with nako─
captive value of the tone shift and we get
final value for pitch. Sohra─
We have it in our memory, we will need it later─
Xia.
Back to event processing
If there is a new event, then we interrupt
processing of the ornament and proceed to processing─
tke events.
Load the event template pointer and
We read from it the length of the event and its type.
The length value is used for installation
new event timer value. Next on─
go to the address of the event handler subroutine─
Tia according to the table and execute it.
Events in the melody are used to inform
work of everything,which is not a note. They
can tell the player to switch
between synthesis modes on a defined─
line channel, change the ornament or inst─
rument for the selected channel, apply change─
Lower the pitch of the channel or change
melody playback speed.
In fact, all that needs to be done is the procedure─
event processing framework is to update the non─
how many global variables containing
current state of the player. However, this
made a little more complicated by the fact that some─
Variables appear in memory several times─
just once. I'll explain why this is so.
The register is much faster to load nepo─
more by number than by memory addressing,
especially if this address is inconvenient to store
in the register. Please note the difference in
these pairs of instructions:
ld a,(nn) ;13 cycles
ld a,n ;7 clock cycles (6 clock cycles faster)
ld hl,(nn) ;16 cycles
ld hl,nn ;10 clock cycles (6 faster)
ld de,(nn) ;20 cycles
ld de,nn ;10 cycles (10 faster)
Where possible, variables are stored withusing the second method, directly
exactly in the player code. The disadvantage is─
th method is that the variables accessed
which is needed at different points in the game─
vator, are duplicated in memory and for consistency─
tentatively, all copies must be stored─
restored and renewed equally.
An example of when this type of caching
especially useful is state reset
instrument and channel ornament on the first
line what needs to be done every time,
when a new but─ is played on the channel
ta. To avoid re-reading
this state for each new note, is─
running properties of the current instrument and op─
notes are stored in subroutines for
each channel for direct download. These
properties must be copied to the required ones
places when changing tools and ornaments.
Processing channel A
After finishing the ornament processing /
events, channel processing begins A. Su─
There are two different methods for generating ots─
couples for channelA (FM or unison), between
which you can switch by changing the address
transition. These two methods are similar to each other
friend in the sense that they read the data─
new patterns for the channel and set
necessary parameters for generating a reading─
Comrade
At firstthe melody timer is read in─
ste with channel timer A. These timers are used─
used to check if a new one is available
note from the current pattern. If so, we
load the pattern index, get the length─
Well, the value of the note and save the pointer
pattern. The value of the note is then checked.
The zero value is used for denotation─
Pause values. In this case I used
setting the channel pitch to zero, when
at this point the sample pointer stops advancing─
moves on the sample and actually stops ge─
neration of sound. However, during the initial
After thorough testing, I found that the decree─
the tel stops at unpredictable
sample values, and in combination with nonlinearity─
nal outputAYand sound from other channels
strange artifacts were generated. Except
moreover, since the current tool is still
was active, clicks sounded every time,
when a new sample was installed. To
to avoid these problems, the sample pointer─
the pen is reset to zero and activated
fictitious dumb instrument - until
until a note (not a rest) is detected.
A melody consisting only of pauses can
be a little boring to listen to, but─
let's assume that we end up
encounter a note with a non-zero pitch value
tones. Firstly, its ornament is applied and
height changetones, since they have not yet
were processed for this note.(This time
there is not much code. This is because we need
only the ornament value from the first line
ornament that can be preserved directly
mine, and there is no need to accumulate again
pitch change value.) After
this we reset the ornament indicators and
tool to the first lines and load
initial sample pointers.
What to do if it’s not yet time to read the note
from the current pattern? Then we process─
I eat an instrument for a note that is already playing.
Each line of the instrument can reproduce─
last for a certain period of time
before moving to the next line of the tool─
one that allows for more compact storage
identical lines. In this case, they are used
previously mentioned tool timers
each channel.
The first thing we do is take a timer
channel toolA,reduce it and re─
We check the result for zero. Execution
decrement gives us a free check for
zero, but this requires that the timer value
the first line of each instrument was
increased by one.
Two tool pointers are stored:
one for the current tool line and one
to the next line. If the reduced tai─
instrument measures is not zero, we remain
on the same line using the current pointer─
the next line. Otherwise we will load─
I eatnext line pointer. Index
the next line may point beyond the end
definition of a tool, in this case we
return to the current line of the instrument.
Now we save a timer for this
strings along with the tool pointer
(as new current pointer).We load
factual information about the instrument (si─
carrier and modulator signals for FM or audio─
night signal for unison) into registers and
save the next line pointer.Then
we just need to load the note pitch from
pitch tables and finally start gene─
rify the sound.
Sound generation for channel A
For channel A there are two generation cycles
sound, one for each generator method─
tions of sound, and they are deployed for speed.
Below is an example of an FM count generation cycle
in channel A. It is given first because
is the simplest.
;counts 0,1 ;clocks
ld c,ixh ;8/8 ;phase generation
ld a,(bc) ;7/15 ;modulator wave byte
ld e,a ;4/19
ld a,(de) ;7/26;carrier wave byte
ld (hl),a ;7/33 ;save to buffer
inc l ;4/37
ld (hl),a ;7/44 ;save to buffer
inc l ;4/48
add ix,sp ;15/63 ;go to next. step
Although this is only one iteration of the loop, it
generates two samples in the buffer. In order to
ner 60 values for a full buffer,
the loop should be executed30times.
The following table shows the value
each register in the above frag─
mente code. Because we don't need it at all
stack in this player, we can use─
use the stack pointer for different purposes,
without worrying about whether it will be needed
PUSH / POPorCALL / RET.
Register Destination
======= ==========
A General purposeBC Pointer to the modulator wave
DE Pointer to carrier wave
HL Pointer to output buffer
SP Note pitch
IX Note counter / phase
First we take the high byte of the counter
notes and use it as an index
modulator waves. After receiving the byte in─
of the modulator we use it as a
ve index in the carrier wave. Copying a byte
carrier signal into the output buffer, duplicating
it because we need two values for
each pass through the cycle. Finally, we update─
Let's set up a note counter using the pitch of the notes.
located inSP.
Below is the cycle for generating unison samples
for channelA.It's a little more complicated. Occupies
same number of cycles as FM
generation, but is divided into two parts.
;counts 0,1 ;clocks
ld e,h ;4/4 ;first phase
ld a,(de) ;7/11 ;waveform byte
ld e,l ;4/15 ;second phase
add hl,sp ;11/26 ;next step
ex de,hl ;4/30
add a,(hl) ;7/37 ;+ waveform byte
rra ;4/41 ;reduce by half
ld (bc),a ;7/48 ;save to buffer
inc c ;4/52
ld (bc),a ;7/59 ;save to buffer
inc c ;4/63
;counts 2,3 ;clocks
ld l,d ;4/4 ;first phase
ld a,(hl) ;7/11 ;waveform byte
ld l,e ;4/15 ;second phase
add a,(hl) ;7/22 ;+ waveform bytes
rra ;4/26 ;reduce by half
ld (bc),a ;7/33 ;save to buffer
inc c ;4/37
ld (bc),a ;7/44 ;save to buffer
inc c ;4/48
ex de,hl ;4/52
add hl,sp ;11/63 ;next step
The following table shows the value
each register in the above frag─
ment code. Please note that the use─
bathroom here instructions EX DE,HLchanges
in places valuesDEandHL,so the table
describes their meanings at the beginning of the fragment
code.
Register Destination
======= ==========
A General purpose
BC Pointer to output buffer
DE Waveform pointer
HLNote counter / phase
SP Note pitch
The main idea of the unison method is
that the high and low bytes of the height
sounds are the same, and we use both hundred─
The high and low bytes of the note counter for
generating the final waveform. Pos─
since the low byte is gradually transferred
to senior as the note counter is updated,
we actually play two notes with very little
large differences in pitch, creating─
You can create a unison or chorus effect.
First we take the high byte of the counter
notes and use it as an index
for the waveform. Then we load the byte
the waveform indicated by this index is inA.
The low byte of the note counter is also used─
is used as an index for the waveform,
but we are not loading the signal form byte yet─
la. We add the pitch of the notes to the note counter,
to update it, and then swap it
DEandHL.Now thatHLis an indication─
lem to the desired signal byte, we can use
use ADDA,(HL),to add two
signal bytes together, andRRA,to lead
them into the correct range. The result then
stored (twice) in the sample buffer.
For the second half of the cycle we do this
the same as for the first half, but because
for the fact thatDEandHL are swapped, we
calculate and save the result before
How can we change them back and update again?
note counter.
Processing channel B
Now that all channel samplesAwere
generated into the output buffer, we save─
Hire the phase indicator so that it is available
next time, and go to channelB.
Method of generating samples for the channel B like this─
it can be either FM synthesis or synch─
Ronization of oscillators.
The preparation code is almost the same as for
channel A, so let's skip it and move on
straight to the generation of samples, starting with FM
synthesis.
Sound generation for channel B
FM synthesis for the channel A writes non─
directly into the sample buffer (overwrites─
changing its previous contents), but for two
other channels we have to mix ours─
raw samples with buffer contents.
This means that the sample generation cycle
channel B (expanded, of course) is very
differs from cycleA.However, channelsBandC
use the same procedure for ge─
FM negation to reduce duplication
code. Before entering the loop we reset
carry flag from alternate set
registers to signal that we are in
We are currently processing channelB.Later
channel code C will again enter the same loop with
the carry flag is cleared, and the thread is executed─
The discussion will end in the right place. It's fast─
faster than calling a subroutine and does not require
useSP.
This time, each iteration of the loop is silent─
much shorter:
;beats
ld c,ixh ;8/8 ;set the phase
ld a,(bc) ;7/15 ;waveform byte
ld e,a ;4/19
ld a,(de) ;7/26 ;carrier wave byte
add a,(hl) ;7/33 ;mix with
;buffer contents
ld (hl),a ;7/40 ;save to buffer
inc l ;4/44
add ix,sp ;15/59 ;go to next. phase
We are no longer duplicating readings, so
the loop must be executed for a full
60 iterations. Now the countdown is mixed with
counting from channelAbefore saving to
buffer.
Now about the synchronization of oscillators. This
effect obtained using high─
frequency carrier wave, which resets─
changes its phase whenever the waveform
modulator completes the cycle, creating inte─
fresh sound. The two waveforms have independent─
similar periods, and can be applied to them from─
change in pitch.
An example of one iteration of OS synchronization─
cylinders along with a table showing
useregisters:
;beats
add ix,sp ;15/15 ;next modulator phase
sbc a,a ;4/19 ;if carry, then a=#ff,
;otherwise a=#00
and e ;4/23 ;AND with carrier phase
add a,b ;4/27 ;next carrier phase
ld e,a ;4/31 ;save the carrier phase
ld a,(de) ;7/38 ;waveform byte
add a,(hl) ;7/45 ;mix with
;buffer contents
ld (hl),a ;7/52 ;save to buffer
inc l ;4/56
Register Destination
======= ==========
A General purpose
B Carrier phase step
DE Indexto the waveform
HL Pointer to output buffer
IX Modulator counter / phase
The first thing we do is increase
modulator phase, adding to it the value
modulation. Now we would like to check
whether the modulation waveform cycle is completed,
and if so, reset the carrier phase.
Luckily there is an easy way to do it
this without costly branches. Command
SBC A,AsubtractsAfrom itself with transfer, to
as a result A contains #ff, if the flag
transfer has been installed, or #00 in the opposite─
otherwise. If we then applyAND
result with the carrier phase, we must
receive either a constant phase or zero in
depending on whether the instruction is full─
tionADD IX,SPor not.
There is one problem with this approach: if
all notes have positive values, then─
wherethe carry flag will be the opposite
what we want. It will be installed
only after completion of the modulation cycle
signal, otherwise it will be reset─
shen. The solution to this problem is simple: invert─
type all values in the lookup table you─
honeycomb tone. This means that the signals are
all channels are now played back in volume─
in reverse order, but this side effect
is unlikely to be noticeable.
The carrier phase is then increased by
adding the carrier pitch value, and we're ─
we receive a byte of the signal shape that is mixed─
is calculated with a sample from channelA and saved
back to the buffer.
Channel Processing
Having completed processing of channelB,we went through
half way. The next channel isC,and
the only gene method it supports─
Walkie-talkie sound is FM synthesis.
Sound generation for channel C
The channel preparation code C is largely like this
same as for other channels, so I don't
I think I need to show it again.
Since channelCuses the generation code
together with the channel B,we just need
run this code again. However, we must─
we must be careful, there must be
pointer to playback in
the current sample so that it can be reproduced─
acted correctly.
Melody processing
After processing the channel C we can
start processing the state of the melody.Tai─
the measure of the melody decreases, and if it is equal
zero, we reset it back to the current
melody speed and reduce the patte timer─
rna. If it is also zero, we load
next melody pattern. Since this
the case is relatively rare
(once every few seconds), I decided not to
customize this piece of code clock by cycle, that's it─
mu time may be slightly exceeded.
We move on to the next five byte
record the song order and return to the point
looping if you reach the end. Then we
we get five pattern indices for the current─
position (one for each channel─
la), we look for each of them in the pattern table─
new to get the pattern address and save─
We take the pattern pointer into the variables for
this channel. We also copy the timer,
indicating the number of starting lines
in the pattern to the note.
The length of the new pattern is obtained from ka─
nalaD.
D channel processing
We're almost done, all that's left is about─
work channel D, the simplest of the five
channels. If it's time to play a sample in the channel
D,take the sample index and use it
to get the address and length of this sample
from the sample table. Otherwise no─
move the current sample pointer to60
samples minus the correction made
previously as part of channel processingC.If
the sample has finished playing, we are losing─
We create a 60-byte empty sample. That's it about─
channel operation D is completed.
General tasks
A little trick that appeared
because I ran out of bars in
certain place in the program. Location
jump address pairs are set to
based on the melody timer variable to
don't waste time on this processing somewhere
more.
The last thing we do is basically a cycle─
le - this is swapping two read buffers /
entries for the next iteration of the main
cycle andsave the channel phase C for future─
thing.
Share your thoughts about the article