4209 lines
178 KiB
Plaintext
4209 lines
178 KiB
Plaintext
########
|
|
##################
|
|
###### ######
|
|
#####
|
|
##### #### #### ## ##### #### #### #### #### #### #####
|
|
##### ## ## #### ## ## ## ### ## #### ## ## ##
|
|
##### ######## ## ## ## ##### ## ## ## ## ##
|
|
##### ## ## ######## ## ## ## ### ## ## #### ## ##
|
|
##### #### #### #### #### ##### #### #### #### #### #### ######
|
|
##### ##
|
|
###### ###### Issue #20
|
|
################## April 18, 2001
|
|
########
|
|
|
|
|
|
...............................................................................
|
|
|
|
|
|
Ineptitude: If you can't learn to do something well, learn to enjoy
|
|
doing it poorly.
|
|
-- "Demotivator" poster
|
|
|
|
...............................................................................
|
|
|
|
BSOUT
|
|
|
|
Hoo-ah! Time for another late (but hopefully great) issue of C=Hacking!
|
|
|
|
This issue features several nifty articles on both software and hardware
|
|
projects. There seems to be a lot of activity in the hardware area right
|
|
now, so hopefully we'll see more hardware articles in future issues.
|
|
|
|
In the software area, however...
|
|
|
|
If I may pithily berate for a moment, I'd like to observe that talking about
|
|
programming on comp.sys.cbm is -- this may come as a surprise to some -- not
|
|
the same as actually programming. I'd like to once again encourage those who
|
|
have been talking about projects, or have half-completed projects sitting
|
|
around on an FD disk somewhere, to go for it and get the job done!
|
|
|
|
Just like... um... C=Hacking (hey, it gets done... eventually...).
|
|
|
|
The format for this issue has changed a little, with all the news and stuff
|
|
moved into the previously skimpy "Jiffies" section. Therefore, this is now
|
|
just the 'me' column.
|
|
|
|
So 'me' would like to thank all the authors in this issue for their time
|
|
and effort (and patience), and all of the true C= Hackers out there for their
|
|
spirit and work and cool projects.
|
|
|
|
'Me' is also very happy to announce that he is getting married on August 17.
|
|
And heck, you're ALL INVITED (I think an SX-64 would make an excellent
|
|
wedding present, don't you?).
|
|
|
|
And finally, me still thinks the 64 is the niftiest computer ever made.
|
|
|
|
Onwards!
|
|
|
|
-Steve
|
|
|
|
.......
|
|
....
|
|
..
|
|
. C=H #20
|
|
|
|
::::::::::::::::::::::::::::::::::: Contents ::::::::::::::::::::::::::::::::::
|
|
|
|
BSOUT
|
|
o Voluminous ruminations from your unfettered editor.
|
|
|
|
|
|
Jiffies
|
|
o News, things, and stuff.
|
|
|
|
|
|
Side Hacking
|
|
|
|
o "Super/Turbo CPU VDC Hack", by Henry Sopko <henry.sopko@hwcn.org>
|
|
Normally it is not possible to access the VDC chip when using
|
|
a SuperCPU64 or Turbo CPU on a 128. The 1-wire hack described
|
|
in this article fixes that situation!
|
|
|
|
o "16K Memory Expansion for the VIC-20", by Per Olofsson
|
|
<MagerValp@cling.gu.se>. This article describes a nifty way to
|
|
add more memory to the VIC-20, along with some basic circuit
|
|
design information for the hardware neophyte (i.e. people like me!).
|
|
|
|
o "Quick Quadratic Splines", by moi <sjudd@ffd2.com>
|
|
A spline is a powerful tool for drawing a curve through an arbitrary
|
|
set of points -- for motion/animation, for arbitrary curves (like
|
|
fonts), and numerous other tasks. This article describes _quadratic_
|
|
splines and some fast C64 implementations, and includes a program
|
|
for experimenting with splines.
|
|
|
|
|
|
Main Articles
|
|
|
|
o "VIC-20 Kernal ROM Disassembly Project", by Richard Cini
|
|
<rcini@email.msn.com>
|
|
|
|
The ever-dependable Richard Cini has written the fourth article
|
|
in the quest for a complete disassembly of the VIC-20 kernal. This
|
|
installment focuses on device I/O routines: SETNAM, SETLF, OPEN,
|
|
and beyond.
|
|
|
|
o "MODs and Digital Mixing", by Jolse Maginnis
|
|
<jmaginni@postoffice.utas.edu.au>.
|
|
|
|
Josmod is a program for JOS, by Jolse, that can play Amiga MOD files
|
|
(and their newer successors). This article describes the general
|
|
functioning of the program, the layout of a MOD file, and how to mix
|
|
multiple digital samples in real-time (and hence play MODs!).
|
|
|
|
o "The C64 Digi", by Robin Harbron <macbeth@psw.ca>, Levente
|
|
Harsfalvi <levente@terrasoft.hu>, and Stephen Judd <sjudd@ffd2.com>
|
|
|
|
This article is, we hope, a complete reference on digital sampling
|
|
and the C64, including: general theory, SID hardware description,
|
|
and methods of playback (changing $d418, pulse width modulation,
|
|
and various tricks). Numerous code examples are given, along with
|
|
a program that does true 8-bit playback at 16KHz -- it requires a
|
|
SuperCPU, but it is most impressive, and chances are awfully good
|
|
that you've never heard a digi like this out of SID before.
|
|
|
|
|
|
.................................. Credits ...................................
|
|
|
|
Editor, The Big Kahuna, The Car'a'carn..... Stephen L. Judd
|
|
C=Hacking logo by.......................... Mark Lawrence
|
|
|
|
Special thanks to the folks who have helped out with reviewing and such,
|
|
to the article authors for being patient, and to all the cbm hackers that
|
|
make this possible!
|
|
|
|
Legal disclaimer:
|
|
1) If you screw it up it's your own fault!
|
|
2) If you use someone's stuff without permission you're a dork!
|
|
|
|
About the authors:
|
|
|
|
Jolse Maginnis is a 20 year old programmer and web page designer,
|
|
currently taking a break from CS studies. He first came into contact
|
|
with the C64 at just five or six years of age, when his parents brought
|
|
home their "work" computer. He started out playing games, then moved on
|
|
to BASIC, and then on to ML. He always wanted to be a demo coder, and in
|
|
1994 met up with a coder at a user's group meeting, and has since worked
|
|
on a variety of projects from NTSC fixing to writing demo pages and intros
|
|
and even a music collection. JOS is taking up all his C64 time and he
|
|
is otherwise playing/watching sports, out with his girlfriend, or at a
|
|
movie or concert somewhere. He'd just like to say that "everyone MUST
|
|
buy a SuperCPU, it's the way of the future" and that if he can afford
|
|
one, anyone can!
|
|
|
|
Richard Cini is a 31 year old vice president of Congress Financial
|
|
Corporation, and first became involved with Commodore 8-bits in 1981, when
|
|
his parents bought him a VIC-20 as a birthday present. Mostly he used it
|
|
for general BASIC programming, with some ML later on, for projects such as
|
|
controlling the lawn sprinkler system, and for a text-to-speech synthesyzer.
|
|
All his CBM stuff is packed up right now, along with his other "classic"
|
|
computers, including a PDP11/34 and a KIM-1. In addition to collecting
|
|
old computers Richard enjoys gardening, golf, and recently has gotten
|
|
interested in robotics. As to the C= community, he feels that it
|
|
is unique in being fiercely loyal without being evangelical, unlike
|
|
some other communities, while being extremely creative in making the
|
|
best use out of the 64.
|
|
|
|
Robin Harbron is a 28 year old internet tech support at a local
|
|
independent phone company. He first got involved with C= 8-bits
|
|
in 1980, playing with school PETs, and in 1983 his parents convinced
|
|
him to spend the extra money on a C64 instead of getting a VIC-20.
|
|
Like most of us he played a lot of games, typed in games out of
|
|
magazines, and tried to write his own games. Now he writes demos,
|
|
dabbles with Internet stuff, writes C= magazine articles, and, yes,
|
|
plays games. He is currently working on a few demos and a few games,
|
|
as well as the "in-progress-but-sometimes-stalled-for-a-real-long-time-
|
|
until-inspiration-hits-again Internet stuff". He is also working on
|
|
raising a family, and enjoys music (particularly playing bass and guitar),
|
|
church, ice hockey and cricket, and classic video games.
|
|
|
|
Levente Harsfalvi is a 26 year old microcontroller programmer who works at
|
|
a small local company. His first C= encounter was a Plus/4 at school, at
|
|
the age of 12, and later (1988) his parents bought a C-16. After learning
|
|
BASIC and ASM coding he joined a Plus/4 demo group (GOTU, and later Coroners),
|
|
and has worked on game conversions, an FLI editor, music software (including
|
|
a SID emulator to play c64 music on TED) and numerous other software and
|
|
hardware projects. More recently he has begun taking some measurements on
|
|
the Plus/4 to figure out things such as how the sound generator works and is
|
|
working on a C-16 demo. Outside of the C= he enjoys cycling and running,
|
|
and ~50km walking tours.
|
|
|
|
|
|
For information on the mailing list, ftp and web sites, send some email
|
|
to chacking-info@jbrain.com.
|
|
|
|
While http://www.ffd2.com/fridge/chacking is the main C=Hacking homepage,
|
|
C=Hacking is available many other places including
|
|
|
|
http://www.funet.fi/pub/cbm/magazines/c=hacking/
|
|
http://metalab.unc.edu/pub/micro/commodore/magazines/c=hacking/
|
|
|
|
|
|
................................... Jiffies ..................................
|
|
|
|
$01 Not _too_ long ago, an effort was made to write down the pin assignments
|
|
for the video port of all the major C= computers. The result, as compiled
|
|
by William Levak <wlevak@cyberspace.org>, is:
|
|
|
|
|
|
8 7
|
|
Commodore 6 Video Connector
|
|
3 1
|
|
|
|
5 4
|
|
2
|
|
Plus4 C16/C128
|
|
CBM-II VIC20 VIC20CR C64 Pin C64A/SX64 C64B/C/E
|
|
--------- ------------ ------- ---------- --- ---------- ----------
|
|
(R) Luminance +5 V +5 V Monochrome 1 Monochrome Monochrome (Y)
|
|
Ground Ground Ground Ground 2 Ground Ground
|
|
(B) V. Sync. Audio Audio Audio 3 Audio Audio (W)
|
|
(W) Video 50 Ohm Video Video Video 4 Video Video
|
|
(Y) H. Sync. Video Video Audio In 5 Audio In Audio In
|
|
6 Chroma Chroma (R)
|
|
7
|
|
8 +5 V
|
|
|
|
|
|
$02 "Professor Dredd" <profdredd@yahoo.com> has uploaded the programs
|
|
from "Inside Commodore DOS" to his webpage at
|
|
|
|
http://www.geocities.com/profdredd
|
|
|
|
|
|
$03 Todd Elliot has written a version of Pasi Ojala's zip/unzip program
|
|
for GEOS/Wheels, available at:
|
|
|
|
http://www.cs.tut.fi/~albert/Dev/gunzip-geos/
|
|
|
|
|
|
$04 Jeri has been hard at work on her video/cpu-board:
|
|
|
|
http://www.geocities.com/cm_easy/
|
|
|
|
|
|
$05 Jolse has been hard at work on JOS (but you'll have to wait until next
|
|
issue for the article!):
|
|
|
|
Here's the latest Jos news:
|
|
Support for CMD HD, including native partitions. (Read only atm, and no
|
|
1581 partitions)
|
|
Enhanced shell with filename completion and recursive wildcards.
|
|
Improvements to the GUI - the architecture now allows for a different
|
|
window interface styles transparent to the application.
|
|
Numerous showstopper bugs killed.
|
|
Improvements to the httpd server to allow directory listings.
|
|
Swiftlink/T232/Duart drivers.
|
|
Started writing tutorials for programmers wanting to give Jos a go.
|
|
|
|
Plus heaps more things not worth mentioning..
|
|
|
|
cya!,
|
|
Jolse
|
|
|
|
|
|
$06 Philip 'Pepto' Timmermann has made some very accurate measurements
|
|
(and RGB calculations) of VIC-II colors:
|
|
|
|
http://www.pepto.de/projects/colorvic/
|
|
|
|
|
|
$07 And finally, I have written a 2D graphics library for use by assembly
|
|
language programs (plot points, draw lines and circles, that kind of
|
|
thing). It's super-easy to use and pretty fast, so go ahead and use it
|
|
if you need some hires drawing routines!
|
|
|
|
For more information, pop on over to
|
|
|
|
http://www.ffd2.com/fridge/grlib/
|
|
|
|
|
|
|
|
................................ Side Hacking ................................
|
|
|
|
Super/Turbo CPU VDC Hack
|
|
for
|
|
Commodore 128 with SuperCPU 64 or Turbo Master CPU Accelerators
|
|
|
|
by Henry Sopko
|
|
(henry.sopko@hwcn.org)
|
|
|
|
As many of you probably know, accessing the VDC (8563) chip is not
|
|
possible when using a SuperCPU 64 (version 1 tested only!) or the
|
|
TurboMaster CPU (latest revision), until now. With this 1 wire hack,
|
|
you can have access to the C128 (flat only tested) VDC 80 column
|
|
chip with your SCPU 64 or TurboMaster CPU!
|
|
|
|
|
|
DISCLAIMER
|
|
--------------------------------------------------------------------
|
|
I take no responsibility whatsoever to any damage that may occur to
|
|
your Computer or SCPU 64/TM CPU resulting from this hardware hack!
|
|
You do this hack totally at YOUR OWN RISK!
|
|
|
|
[C=Hacking disclaimer: if you screw it up, it's your fault!]
|
|
|
|
--------------------------------------------------------------------
|
|
|
|
This hack was only tested on a flat C128, using CMD's SCPU 64 (version 1).
|
|
Tested also was the Turbo Master CPU 4.09 MHz accelerator from Schnedler
|
|
Systems (latest revision).
|
|
|
|
Parts required: (1) long wire (12 inches or so, cutting it to length).
|
|
|
|
INSTRUCTIONS
|
|
|
|
Dissassemble your C128, taking the metal shield completely off. With the
|
|
motherboard exposed, find the 8563 VDC chip (U22). After locating this chip,
|
|
remove it (take note of the notch position, so you correctly re-insert the
|
|
chip). Bend out PIN 9 (R/W) of the 8563 just enough so it does not touch the
|
|
socket or any metal (in the flat 128, theres not much room, so be carefull).
|
|
|
|
Now re-insert the 8563 chip. Take a wire (you can either solder the wire to
|
|
pin 9 as I did, or use Microclips -- your choice). Now, connect the wire by
|
|
soldering or using Microclips to PIN 5 on the CARTRIDGE EXPANSION PORT. Thats
|
|
it!
|
|
|
|
I have done this hack quite a while ago without any signs of problems
|
|
whatsoever. The C128 functions the same with or without a SCPU 64 or TM CPU
|
|
connected after preforming this hack. I really did this hack for the Turbo
|
|
Master CPU so I could access the VDC. Turns out, that the SCPU 64 accelerator
|
|
(and maybe others?) work with this hack as well. The better choice of course
|
|
is to buy a CMD SuperCPU 128 to take full advantage of the Commodore 128 in
|
|
both modes!
|
|
|
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
|
|
|
|
16K Memory Expansion for the Vic-20
|
|
|
|
By Per Olofsson <MagerValp@cling.gu.se>.
|
|
|
|
Thanks to Ruud Baltissen, Pasi Ojala and Spiro Trikaliotis for help and
|
|
suggestions. The latest version of this project may be found at
|
|
http://www.cling.gu.se/~cl3polof/64.html.
|
|
|
|
|
|
I tried to keep the expansion as simple as possible, requiring no chips
|
|
besides the two memory chips, and a minimum of soldering. It uses two 6264
|
|
SRAM chips (62256 also works) piggy-backed to the Kernal and Basic 2364
|
|
ROMs. It's recommended that you do the expansion one chip at a time, as this
|
|
greatly simplifies troubleshooting. After the first chip checks out OK, do the
|
|
second one. I'll start out by explaining a few basic principles.
|
|
|
|
o Static Electricity
|
|
|
|
We'll be using CMOS RAM chips, and they are very sensitive to static
|
|
electricity. If you don't have an anti-static wrist strap, make sure you touch
|
|
a grounded surface before you touch the chips. Do not work sitting on shag
|
|
carpet in a mohair sweater while petting the cat :)
|
|
|
|
o Be Careful With ICs
|
|
|
|
ICs are sensitive to heat, and keeping the soldering iron for too long on a
|
|
pin can toast the chip. If you need to redo a solder joint on a pin, wait for
|
|
it to cool down first.
|
|
|
|
o Up, Down, and Pin Numbering
|
|
|
|
The top of a DIP IC (the kind used in Vic-20s) is marked with a small notch,
|
|
looking something like this:
|
|
|
|
___ ___
|
|
1 =| V |= 8
|
|
2 =| |= 7
|
|
3 =| |= 6
|
|
4 =| |= 5
|
|
`-------'
|
|
|
|
As you can see, the pin numbers start at 1, and the first pin is the top left,
|
|
going down to the bottom left, then from the bottom right to the top right.
|
|
|
|
Piggy-backing
|
|
-------------
|
|
|
|
Piggy-backing is a quick way of adding another IC on top of an existing
|
|
one. Normally you would create a PCB with chip sockets, connect it to the
|
|
expansion port and populate it with memory chips, but with piggy-backing you
|
|
simply add chips on top of internal ones. This works when the new chip's pins
|
|
has signals that are identical to, or closely matches, the layout of the one
|
|
it's being mounted on top of.
|
|
|
|
6264 RAM
|
|
___ ___
|
|
2364 ROM NC 1 =| V |= 28 Vcc
|
|
___ ___ A12 2 =| |= 27 /WE
|
|
A7 1 =| V |= 24 Vcc A7 3 =| |= 26 CS2
|
|
A6 2 =| |= 23 A8 A6 4 =| |= 25 A8
|
|
A5 3 =| |= 22 A9 A5 5 =| |= 24 A9
|
|
A4 4 =| |= 21 A12 A4 6 =| |= 23 A11
|
|
A3 5 =| |= 20 /CS A3 7 =| |= 22 /OE
|
|
A2 6 =| |= 19 A10 A2 8 =| |= 21 A10
|
|
A1 7 =| |= 18 A11 A1 9 =| |= 20 /CS1
|
|
A0 8 =| |= 17 D7 A0 10 =| |= 19 D7
|
|
D0 9 =| |= 16 D6 D0 11 =| |= 18 D6
|
|
D1 10 =| |= 15 D5 D1 12 =| |= 17 D5
|
|
D2 11 =| |= 14 D4 D2 13 =| |= 16 D4
|
|
Vss 12 =| |= 13 D3 Vss 14 =| |= 15 D3
|
|
`-------' `-------'
|
|
|
|
As you can see there are 8 that don't match. However, we don't have to rewire
|
|
all of them. NC means "No Connection" so we can just ignore that pin. Note
|
|
that if you're using 62256 chips you'll have to wire this one too, see
|
|
below. We want to connect CS2 to Vcc, and we'll also swap A11 and A12, leaving
|
|
5 pins to solder on each chip. Swapping address bus pins works on RAM chips,
|
|
as it only affects how bits are stored internally -- when you read them out
|
|
again, they're swapped back. This also works for the databus.
|
|
|
|
We'll mount one 6264 on top of the Kernal ROM, and one 6264 on top of the
|
|
Basic ROM. The ROMs are marked UE11 and UE12 and can be found in the bottom
|
|
right of the motherboard. The wiring is identical for the two chips, except
|
|
for the /OE and /CS1 pins. The pins that we want to rewire we carefully bend
|
|
up so that they don't connect to the ROM. You want to bend them almost all the
|
|
way up (about 150 degrees) so that you can reach them with the soldering
|
|
iron. The pins are very sensitive, so make sure you bend the right pins --
|
|
bending a pin back again could easily break it.
|
|
|
|
Sometimes the pins on the RAM are too wide apart to make a good connection
|
|
when you piggy-back it. In this case, bend all the pins on the RAM slightly
|
|
inwards. You can do this by putting it on the side on a flat, hard surface and
|
|
press gently.
|
|
|
|
o Soldering A11/A12 and Vcc
|
|
|
|
You could get A11, A12, and Vcc from several places on the motherboard, but as
|
|
they're available on the ROMs we'll just solder small wires from the ROM to
|
|
the RAM. Remember that we're swapping A11 and A12, so connect pin 18 on the
|
|
ROM to pin 2 on the RAM. Connect pins 26 and 28 on the RAM to eachother.
|
|
|
|
o Soldering /WE
|
|
|
|
Pin 27 on the RAMs should be connected to pin 34 on the 6502 CPU. The CPU is
|
|
the 40-pin chip in socket UE10 on the motherboard, right next to the ROM
|
|
chips. Pin 34 is the 7th pin if you count from the top right pin on the CPU.
|
|
|
|
o /OE and /CS1
|
|
|
|
In the Vic-20 memory is divided into 8 blocks of 8K each. Block 0 is further
|
|
divided into 8 1K blocks, of which 5 are populated with RAM. Block 4 is used
|
|
by the VIC and the VIA chips, block 6 is the Basic ROM, and block 7 is the
|
|
kernal ROM. This leaves four blocks (1, 2, 3 and 5) available for cartridges
|
|
and RAM expansions. For RAM to be visible to the basic interpreter, you must
|
|
start by adding ram in block 1, then 2 and then 3. RAM in block 5 is never
|
|
detected by the basic. 8K cartridges use block 5, and 16K cartridges use block
|
|
5 together with another one, usually 3. To be as compatible as possible with
|
|
existing cartridges and to expand basic memory I'll use block 1 and 2 for this
|
|
expansion, but you could use any two of the available blocks you want. I've
|
|
added instructions for making the blocks selectable by switches below.
|
|
|
|
The block signals are available on the 74LS138 decoder chip marked UC5 on the
|
|
left side of the motherboard. Block 1 is on pin 14, block 2 is on pin 13,
|
|
block 3 is on pin 12 and block 5 is on pin 10. If you look closely you'll see
|
|
that the signals for block 1 and 2 go out from the chip a few mm to a small
|
|
pad. It's much easier to connect your wires to these pads than the pins on the
|
|
decoder chip. Solder a wire from block 1 to pin 20 and 22 on the first RAM
|
|
chip, and a wire from block 2 to pin 20 and 22 on the second RAM chip.
|
|
|
|
You're done!
|
|
|
|
That's all. When you power up the Vic-20, you should be greeted with a 19967
|
|
BYTES FREE message. If you've only done one chip so far, you'll get 11775
|
|
BYTES FREE, provided you connected it to block 1. If you've connected memory
|
|
to other blocks but not block 1, you'll just get the normal 3583 BYTES FREE
|
|
message. To test your memory, try this program:
|
|
|
|
10 input "test which block";b
|
|
20 s = b*8192 : t = s+8191 : e = 0
|
|
30 print "testing databus"
|
|
40 for a = s to t : poke a, 85 : if peek(a) 85 then gosub 1000
|
|
50 poke a, 170 : if peek(a) 170 then gosub 1000
|
|
60 next : de = e
|
|
70 e=0 : print "testing high address bus"
|
|
80 for a = s to t : poke a, int(a/256) : next
|
|
90 for a = s to t : if peek(a) int(a/256) then gosub 1000
|
|
100 next : he = e
|
|
110 e=0 : print "testing low address bus"
|
|
120 for a = s to t : poke a, a and 255 : next
|
|
130 for a = s to t : if peek(a) a and 255 then gosub 1000
|
|
140 next : print
|
|
150 print de;"errors found in databus test"
|
|
160 print he;"errors found in high address bus test"
|
|
170 print e;"errors found in low address bus test"
|
|
999 end
|
|
1000 e = e+1 : print "error at";a : return
|
|
|
|
The program takes a couple of minutes to run.
|
|
|
|
Troubleshooting
|
|
---------------
|
|
|
|
The computer doesn't start at all, or all you get is a black screen
|
|
|
|
You've probably shorted or toasted something. Not good, this could
|
|
have damaged the computer. Recheck all your soldering and make sure
|
|
that you haven't accidentally connected something wrong.
|
|
|
|
The computer powers up with 3583 bytes free
|
|
|
|
Memory in block 1 is either not connected or not working. Check the
|
|
connections between block 1, /CS1 and /OE, between R/W and /WE, and
|
|
between Vcc, CS2 and Vcc.
|
|
|
|
The computer powers up with something other than 3583, 11775 or 19967 bytes
|
|
free
|
|
|
|
Memory is functioning partially, check A11 and A12. This could also
|
|
indicate that a RAM chip is faulty.
|
|
|
|
The computer powers up with the correct number of bytes free, but the memory
|
|
test program fails
|
|
|
|
Memory is functioning partially. If the databus test fails, check the
|
|
connections between block 1, /CS1 and /OE, between R/W and /WE,
|
|
between Vcc, CS2 and Vcc, and D0 through D7. If the address bus test
|
|
fails, check A0 through A12.
|
|
|
|
Using 62256 chips instead of 6264
|
|
---------------------------------
|
|
|
|
You can freely substitute 62256 chips for the 6264. Only two pins differ:
|
|
|
|
6264 RAM 62256 RAM
|
|
___ ___ ___ ___
|
|
NC 1 =| V |= 28 Vcc A14 1 =| V |= 28 Vcc
|
|
A12 2 =| |= 27 /WE A12 2 =| |= 27 /WE
|
|
A7 3 =| |= 26 CS2 A7 3 =| |= 26 A13
|
|
A6 4 =| |= 25 A8 A6 4 =| |= 25 A8
|
|
A5 5 =| |= 24 A9 A5 5 =| |= 24 A9
|
|
A4 6 =| |= 23 A11 A4 6 =| |= 23 A11
|
|
A3 7 =| |= 22 /OE A3 7 =| |= 22 /OE
|
|
A2 8 =| |= 21 A10 A2 8 =| |= 21 A10
|
|
A1 9 =| |= 20 /CS1 A1 9 =| |= 20 /CS1
|
|
A0 10 =| |= 19 D7 A0 10 =| |= 19 D7
|
|
D0 11 =| |= 18 D6 D0 11 =| |= 18 D6
|
|
D1 12 =| |= 17 D5 D1 12 =| |= 17 D5
|
|
D2 13 =| |= 16 D4 D2 13 =| |= 16 D4
|
|
Vss 14 =| |= 15 D3 Vss 14 =| |= 15 D3
|
|
`-------' `-------'
|
|
|
|
Pin 26 (A13) can be connected to Vcc just like on the 6264, but we need to
|
|
wire pin 1 (A14) to either Vcc, Vss or another address bus pin. It's probably
|
|
easiest to wire it to pin 2 (A12). We won't be using the greater capacity of
|
|
the 62256 using this method (doing so would require some kind of decoder
|
|
logic) but sometimes 62256 chips are cheaper than 6264, or maybe you just have
|
|
some lying around.
|
|
|
|
Adding Block Select and Write Protect Switches
|
|
----------------------------------------------
|
|
|
|
Adding block select switches allows you to chose which blocks should be
|
|
populated on the fly. This allows you to chose between a basic expansion and a
|
|
"cartridge emulator" that allows you to load cartridge images into ram and run
|
|
them. I added one switch for each chip, giving the first chip the option
|
|
between block 1 and 3, and the second between 2 and 5.
|
|
|
|
block 1 -----o
|
|
\
|
|
o----- to /CS1 and /WE on chip 1
|
|
|
|
block 3 -----o
|
|
|
|
|
|
block 2 -----o
|
|
\
|
|
o----- to /CS1 and /WE on chip 2
|
|
|
|
block 5 -----o
|
|
|
|
As a kind of copy protection, some cartridge images try to modify themselves
|
|
to detect if they're running from RAM. If we add write protect switches we'll
|
|
be able to run those as well. Switch to write enabled while loading, then
|
|
switch to write protected and reset.
|
|
|
|
R/W -----o
|
|
\
|
|
o----- to /WE on chip 1
|
|
|
|
+5 V -----o
|
|
|
|
|
|
R/W -----o
|
|
\
|
|
o----- to /WE on chip 2
|
|
|
|
+5 V -----o
|
|
|
|
+5 V is the same as Vcc.
|
|
|
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
|
|
|
|
Quick Quadratic Splines
|
|
----------------------- S. Judd <sjudd@ffd2.com>
|
|
|
|
Splines are neat.
|
|
|
|
Splines are a way of drawing curves -- specifically, drawing a curve through
|
|
any set of _points_ that you choose; for example, a curve that starts at (0,0),
|
|
goes over to (100,50), then loops back to (50,0), and continues on through
|
|
any number of points can be drawn just by specifying the points.
|
|
|
|
This is a very useful thing! One place splines are used is in drawing fonts.
|
|
With a relatively small list of points, it becomes possible to draw letters
|
|
in arbitrary shapes (and scale those shapes easily). Another application
|
|
is animation -- it is possible to specify a long motion path using just
|
|
a few points along the path. Furthermore, an animated object might have
|
|
different parts that move differently -- arms, legs, head, etc. By just
|
|
specifying a few frames of the animation, splines can be used to generate
|
|
the in-between frames.
|
|
|
|
Chances are good that by now you are thinking about some of your own
|
|
applications for splines, so let's see how they work.
|
|
|
|
Most articles/books/etc. that talk about splines talk about "cubic splines".
|
|
This article is going to talk about "quadratic splines", which never seem
|
|
to be mentioned. Which is a pity, since quadratic splines are perfectly
|
|
adequate for many (if not most) spline applications and are far more
|
|
computationally efficient.
|
|
|
|
Graphically, a two-dimensional quadratic spline looks something like:
|
|
|
|
* P1
|
|
/ \
|
|
/ \
|
|
/ \
|
|
/ . . \
|
|
/ . . \
|
|
/. .\
|
|
/. .
|
|
/. * P2
|
|
.
|
|
.
|
|
P0 *
|
|
|
|
(Another brilliant display of ASCII art). The curve starts at P0, moves
|
|
towards P1, and then turns around and heads towards P2, where it ends. If
|
|
we draw lines from P0 to P1 and from P1 to P2, these lines are tangent to
|
|
the curve at the endpoints P0 and P2; that is, these lines say what
|
|
"direction" the curve is going at the endpoints. Using the above diagram,
|
|
it should be very easy to visualize how changing P1 changes the shape of
|
|
the curve.
|
|
|
|
So here, for your computing pleasure, is a quadratic spline:
|
|
|
|
P(t) = P0*(1-t)^2 + 2*P1*t*(1-t) + P2*t^2
|
|
|
|
where P0, P1, P2 are constant values, and t ranges from zero to one. If
|
|
you don't like equations, then how about a computer program to compute
|
|
the above:
|
|
|
|
10 P0=10:P1=100:P2=37
|
|
20 FOR T=0 TO 1 STEP 1/16
|
|
30 PRINT T,P0*(1-T)*(1-T) + 2*P1*T*(1-T) + P2*T*T
|
|
40 NEXT
|
|
|
|
This is actually very easy to understand. When t=0, P(t) = P0. As t starts
|
|
to increase, (1-t) starts to decrease and P(t) starts moving towards P1. As
|
|
t gets even larger P(t) starts moving towards P2, until finally, at t=1,
|
|
P(t) = P2.
|
|
|
|
You may be wondering why there is that 2*P1 in the equation, instead of
|
|
just P1, but that will become apparent shortly.
|
|
|
|
At this point there is an important thing to notice about the above
|
|
equations: adding _any_ constant to P0 P1 and P2 is just like adding the
|
|
constant to the entire spline P. Let P0=P0+C, P1=P1+C, P2=P2+C; the
|
|
spline is then
|
|
|
|
P'(t) = (P0+C)*(1-t)^2 + 2*(P1+C)*t*(1-t) + (P2+C)*t^2
|
|
= [ P0*(1-t)^2 + 2*P2*t*(1-t) + P2*t^2 ] + C*[(1-t)^2 + 2*t*(1-t) + t^2]
|
|
= P(t) + C
|
|
|
|
This is one reason that factor of 2 multiplying P1 is important -- it makes
|
|
the expression multiplying C add up to 1. This property means that splines
|
|
can be _translated_ easily. Note that one value you can translate everything
|
|
by is P0 -- subtract P0 from each point and the spline becomes
|
|
|
|
P(t) - P0 = 2*(P1-P0)*t*(1-t) + (P2-P1)*t^2
|
|
|
|
and hence
|
|
|
|
P(t) = 2*(P1-P0)*t*(1-t) + (P2-P1)*t^2 + P0
|
|
|
|
which gets rid of the P0*(1-t)^2 term. Depending on how the spline algorithm
|
|
works, this can save some computation time.
|
|
|
|
Now, the points P0, P1 etc. are called "control points". If you're on the ball
|
|
(or if you've tried the BASIC program above) you've noticed that while the
|
|
spline starts at P0 and ends at P2, it never actually reaches P1 -- it just
|
|
heads towards it but then heads away towards P2. If a different P1 is
|
|
chosen, the spline still starts at P0 and ends at P2 but takes a different
|
|
path between the endpoints. P1 controls the shape of the curve.
|
|
So now let's say we want a curve that passes through three points, s0, s1,
|
|
and s2, using a quadratic spline. P0 and P2 are easy to choose:
|
|
|
|
P0 = s0
|
|
P2 = s2
|
|
|
|
P1 can actually be chosen at this point to be any number of points. All we
|
|
do is choose a value for t -- call it t1 -- when the spline should hit s1:
|
|
|
|
P(t1) = s1 => P1 = (s1 - P0*(1-t1)^2 - P2*t1^2) / (2*(t1*(1-t1))
|
|
|
|
(just substitute t=t1 into the equation for P(t) and solve for P1). So if
|
|
t1=1/2, say, then
|
|
|
|
P1 = (s1 - P0/4 - P2/4) / (2 * 1/4)
|
|
= 2*s1 - (P0 + P2)/2
|
|
|
|
and the spline will hit s1 halfway through the iteration at t=1/2.
|
|
|
|
In a typical application, of course, the curve will need to go through tens,
|
|
hundreds, even thousands of points. Naturally, the way to do this is by
|
|
joining a bunch of splines together.
|
|
|
|
If we just choose a spline for every three points, using the above equation
|
|
for P1, there will typically be sharp corners where the splines join
|
|
together. Sometimes this is a good thing -- for example, when drawing
|
|
something like a valentine we might want sharp corners. But usually
|
|
what is needed is a nice smooth curve through all the points.
|
|
|
|
With quadratic splines this is a piece of cake. Each spline has three
|
|
control points: the two endpoints, and the middle control point P1. The
|
|
endpoints are simply chosen to be the points we want to pass through,
|
|
and P1 can then be chosen to make all the connections smooth.
|
|
|
|
That is, given a list of points s0, s1, s2, ... to put a curve through,
|
|
choose s0 and s1 to be the endpoints of the first spline, s1 and s2 to
|
|
be the endpoints of the second spline, and so on, and choose the middle
|
|
control points to make everything smooth.
|
|
|
|
"Smooth" here means a continuous first derivative -- if you don't know what
|
|
a derivative is then don't worry about it, it just means that the "slope" of
|
|
each spline is the same where they join together. Given a spline
|
|
|
|
S1(t) = P0*(1-t)^2 + 2*P1*t*(1-t) + P2*t^2
|
|
|
|
the derivative at t=1 is given by
|
|
|
|
S1' = 2*(P2 - P1)
|
|
|
|
P2-P1 is simply the line segment running from P1 to P2, which verifies what
|
|
was said earlier: at P2 the curve is tangent to a line drawn from P1 to P2.
|
|
|
|
* P1
|
|
/ \
|
|
/ \
|
|
/ \
|
|
/ . . \
|
|
/ . . \
|
|
/. .\
|
|
/. .
|
|
/. * P2
|
|
.
|
|
.
|
|
P0 *
|
|
|
|
P2-P1 is the direction the curve is headed in. So to join another spline
|
|
smoothly to the first one, simply extend this line segment and make sure
|
|
the second middle control point lies somewhere on that line:
|
|
|
|
* P1
|
|
/ \
|
|
/ \
|
|
/ \
|
|
/ . . \
|
|
/ . . \
|
|
/. .\
|
|
/. .
|
|
/. * P2
|
|
. . * P5
|
|
. \. ./
|
|
P0 * \ ../
|
|
\ /
|
|
* P4
|
|
|
|
Since the first spline is tangent to P2-P1, and the second spline is tangent
|
|
to P4-P2, the two splines have the same slope at P2, and join smoothly.
|
|
Note that P4 can be _anywhere_ along the P2-P1 line, and choosing different
|
|
values for P4 will change the curve.
|
|
|
|
To make this more precise, let the two splines be given by
|
|
|
|
S1(t) = P0*(1-t)^2 + 2*P1*t*(1-t) + P2*t^2
|
|
S2(t) = P3*(1-t)^2 + 2*P4*t*(1-t) + P5*t^2
|
|
|
|
To join them smoothly, the slope of the first spline must be proportional
|
|
to the slope of the second spline at the joint:
|
|
|
|
P4-P3 = c*(P2-P1)
|
|
|
|
where c is some constant (the _direction_ of the two slopes need to be the
|
|
same, but not the magnitude). So, to make the second spline fit smoothly
|
|
to the first we simply choose the middle control point P4 as
|
|
|
|
P4 = P3 + c*(P2-P1)
|
|
|
|
or, in plain English, starting from P3 (the joining point) move some
|
|
distance c in the direction P2-P1. To see why the factor c is important
|
|
just scroll up a page or two to the diagram, and imagine how the curve
|
|
changes as P4 slides back and forth along the line.
|
|
|
|
We now have the tools to construct a smooth curve that passes through any
|
|
points s0, s1, s2, s3, ... Let the first spline pass through s0, s1,
|
|
and s2, using the t=1/2 formula given earlier:
|
|
|
|
P0 = s0
|
|
P1 = 2*s1 - (s0 + s2)/2
|
|
P2 = s2
|
|
|
|
P1 could be chosen in other ways, of course, but the above usually works
|
|
pretty well. Then choose each succeeding spline to match up smoothly with
|
|
the previous spline:
|
|
|
|
P3 = s2 ;start at spline 1 endpoint
|
|
P4 = P3 + c1*(P2-P1) ;smooth joint
|
|
P5 = s3 ;next point
|
|
|
|
P6 = s3
|
|
P7 = P6 + c2*(P5-P4)
|
|
P8 = s4
|
|
|
|
and so on. (When drawing smooth curves, it is of course really only
|
|
necessary to store the middle control points P4 P7 etc. for each spline).
|
|
The choice of the constants c1 c2 etc. really depends on how you want the
|
|
curve to look.
|
|
|
|
As you can see above, each extra point requires another spline. One spline
|
|
per point may sound like a lot, but there are a lot of pixels in-between each
|
|
pair of points, and more importantly these are quadratic splines and hence
|
|
can be made to go very, very fast.
|
|
|
|
Computing the spline
|
|
--------------------
|
|
|
|
Note that the control points are one-time calculations; those three constant
|
|
values completely determine the spline. One way of drawing the spline is
|
|
to use some lookup tables for t^2, 2*t*(t-1), and (1-t)^2 and do a little
|
|
fast multiply magic. This is fast and straightforward, so I won't talk
|
|
about it much except to mention that signed multiplies may be required,
|
|
and each curve is limited to 256 points (t can go from 0 to 255 in the
|
|
lookup tables). Note also that it would be nice to have an "incremental"
|
|
plot routine, i.e. one that just updates pointers when necessary, instead
|
|
of recomputing all the bitmap pointers at every iteration.
|
|
|
|
Which of course brings us to a second way of drawing the spline: to ask
|
|
"how does the spline change when I increment t?"
|
|
|
|
Let's say that t is incremented by dt at each step (t -> t+dt). At each
|
|
step, then, the spline changes by
|
|
|
|
S(t+dt) = P0*(1-t-dt)^2 + P1*2*(t+dt)*(1-t-dt) + P2*(t+dt)^2
|
|
|
|
= S(t) + k1*dt^2 + k1*2*t*dt + k2
|
|
|
|
where k1 = P0 - 2*P1 + P2 and k2 = 2*dt*(P1 - P0). That is, to advance one
|
|
step, we add
|
|
|
|
k1*dt^2 + 2*k1*t*dt + k2
|
|
|
|
to the current value. Take a look at how the above value changes as t
|
|
changes:
|
|
|
|
t k1*dt^2 + 2*k1*t*dt + k2
|
|
-- ------------------------
|
|
0 k1*dt^2 + k2 = k1*dt^2 + k2
|
|
dt k1*dt^2 + 2*k1*dt^2 + k2 = 3*k1*dt^2 + k2
|
|
2*dt k1*dt^2 + 4*k1*dt^2 + k2 = 5*k1*dt^2 + k2
|
|
3*dt k1*dt^2 + 6*k1*dt^2 + k2 = 7*k1*dt^2 + k2
|
|
|
|
and so on. This means that all we have to do to advance the spline is
|
|
something like
|
|
|
|
c0 = k1*dt^2
|
|
C = c0 + k2
|
|
S = P0
|
|
:loop
|
|
S = S + C
|
|
plot S
|
|
C = C + 2*c0
|
|
loop
|
|
|
|
Which is really, really fast. Instead of specifying the spline with the
|
|
three values P0, P1, and P2 we can specify it by the three values P0, k1,
|
|
and k2.
|
|
|
|
It is possible to understand this iterative method somewhat. The update
|
|
constant k1 can be written as
|
|
|
|
k1 = (P0-P1) + (P2-P1)
|
|
|
|
whereas k2 goes like
|
|
|
|
k2 ~= (P1-P0).
|
|
|
|
P1-P0 is the "direction" line segment towards P1, so the curve starts moving
|
|
towards P1. But k1 starts to balance that out with the P0-P1 term (the line
|
|
segment pointing in the opposite direction), while moving towards P2 with the
|
|
P2-P1 term, which is the direction line segment from P1 to P2. Now, isn't
|
|
that all nice and clear now?
|
|
|
|
The Program
|
|
-----------
|
|
|
|
At the end of this article I've included a sort of "spline laboratory" --
|
|
it's a little BASIC program that lets you experiment with different aspects
|
|
of splines and see what they look like. It uses BLARG for the graphics, so
|
|
I've included that as well.
|
|
|
|
When you run it, it plots three points on the screen to draw a curve through.
|
|
You can move these points around, as well as add new points, draw the splines,
|
|
draw the control point "a-frames", and so on. It has a ton of commands,
|
|
basically because I just added them as I got curious about different things:
|
|
|
|
+/- Move to next/previous point
|
|
cursor keys Move current point
|
|
return Draw spline
|
|
space Clear screen
|
|
* Toggle point display
|
|
< > Decrease/increase accuracy (number of bits in iteration)
|
|
1 <- Increase/decrease time step (number of points in spline)
|
|
= List control points (numerical values)
|
|
. Draw control point a-frame
|
|
S Toggle smooth splines (see below)
|
|
A Add more points
|
|
Q Quit
|
|
|
|
Pressing "A" adds three points at a time, starting at the last point in
|
|
the curve. The reason for adding three points, instead of just one, is
|
|
because of the "S"mooth spline feature. When smooth splines are selected,
|
|
the program will draw a smooth curve through all the points. When toggled
|
|
off, the program will draw a spline through every three points, using the
|
|
"t=1/2 method" to select the middle control point.
|
|
|
|
The program uses the fast iteration method described earlier, using fixed
|
|
precision arithmetic (since this is what an assembly program would do).
|
|
The < and > keys are used to change this precision, since certain splines
|
|
need more precision than others.
|
|
|
|
Finally, the value CC=0.4 is set in line 10. This is the "smoothing"
|
|
constant, i.e. c1 in the equation for P4 below:
|
|
|
|
P3 = s2 ;start at spline 1 endpoint
|
|
P4 = P3 + c1*(P2-P1) ;smooth joint
|
|
P5 = s3 ;next point
|
|
|
|
You might want to experiment with different values, to see what happens
|
|
(or else let each spline have their own value).
|
|
|
|
Anyways, the program is not meant to be terribly profound -- just a starting
|
|
point and something to play with to work out your own ideas on!
|
|
|
|
Cubic Splines
|
|
-------------
|
|
|
|
Just for completeness, here is a cubic spline:
|
|
|
|
P(t) = P0*(1-t)^3 + 3*P1*t*(1-t)^2 + 3*P2*(1-t)*t^2 + P3*t^3
|
|
|
|
As you can see, it has four terms instead of three, and is cubic in t. But
|
|
the idea is the same -- at t=0 it starts at P0, as t increases moves
|
|
towards P1, then towards P2, and finally ends at P3.
|
|
|
|
You can also see that it is significantly more computationally involved than
|
|
the quadratic spline. Moreover, computing the middle control points P1 and P2
|
|
is also fairly involved -- much more involved than with the quadratic spline.
|
|
|
|
So, why use a cubic spline? Two reasons: first, you can specify the derivative
|
|
at _both_ endpoints -- the direction line P1-P0 will be the slope at the
|
|
starting point, and the direction P3-P2 will be the slope at the end point.
|
|
This gives some flexibility needed for certain applications.
|
|
|
|
Second, you can alternatively match _second_ derivatives at the connection
|
|
points, which again can be useful for certain applications.
|
|
|
|
For more information about cubic splines, just check out any decent graphics
|
|
book.
|
|
|
|
The Program
|
|
-----------
|
|
|
|
begin 644 slab.zip
|
|
M4$L#! H & $]LQQJSS3>D,0< /0( * 8FQA<F<N;6TN;P\ 0(4
|
|
M!18%!@T(+0=]_OY^"1$3%A,&!??W]U<#(FR(L/E!SS)AVIRI4]2S4P,LW#]U
|
|
MPL+^4\_'F4[Y_:DAIF.>?VK@A0>"!DA88.S^D04BAY[?O'_S XWSAQZ?]#YH
|
|
M1@S\/_3[H@OV)CBP<T?@RB'>9PH0^-2G+$%F?5H5Y-6@3JF"# IR:M&A4HL@
|
|
MW[3HU*E!CQ;1OBC2GR"=!I7J-&E0%L> @\R_M*A+D$R?6BDV5:1)G2XQ?BK4
|
|
MHD&,+;0(E)DND?X(=D\Q%8U3B88I'W0J%6F4:%(KT:TSU:)2HJN^*%2D19U$
|
|
MM;W2JD2)6%]5*XCG_@<9\P5MO\QY(W=;!-Z)]W(W>L=;@SOF&ISIWN,%[P(_
|
|
M#[Q&<B,7A=+X5,$SF7+XH4FE#F5:5/Q1J9''#?_4J%'R39\2+5H>=M(CW)O#
|
|
MUF"?%UJENJ[13^D;:OJA3YG"5@^P:W#XP?T/_VL\O7&2Q\4?GVU\]G'IQR4>
|
|
M!\D1..B"A/^#_ P<]D*0PH%#IY^Y?\#4_\&:_M]Y?T#TN88S-OSSR"'2FW32
|
|
M_WN/! X?8W+PL"MFHQXZ0>3HH;M')RWH(>-3/CQ&,. C@S=\CLD?0X?@J_<U
|
|
MXPZ+[X=N'[[YY"#ABT\N$KYZ,.V>F7P][)<D'\)<D<G?51-. LG*X2]8#PFI
|
|
M9'G)AFD0I#2FH==/#AL^A#<?))TTY7^V'X$9AW _G?K)ME-+ <>I@YP+$(&]
|
|
M-439(@J'$\K=]>FDX!6J[BT6.-+>889GXCU*IN=/=;Q!KK]=FB+.=A?GPA1A
|
|
M%S1>$:9%G(K;A9PA/U0-'CEL!=DG!UJZ8OJ D ."59P2W\VO5*G2MGR-[;P!
|
|
M&NRO0A@M6[&L/^BZ/XB*/?$QENU7/F0!/Q^S#ZINE7$,LK2_KN8SK</'FYIF
|
|
MX0 0ZTW!!^\9/G@4C#6#Y1O/8P7['S!0D! 1$A08\/_?GQ\?'AP@.1LF '1A
|
|
M 7I5+(/L[^J9"Y86B+,\1=6E!Y:O"%MC:8*D"Q;:M'&3%DCHG >22K\BX8&0
|
|
MM1=&:?BD:&3?K5\0ML&D_],&%(VXL$G'Z(4[QCRL-+#').@5$1;^![W1_VJ$
|
|
MT<*C44(NF&SXAF!NI_%(=<72HTL7Q*%PA;9J!+>Y<"GO5*8S0J:8$)!RKCDA
|
|
M<-4P$28$MXRK>K^1:H,T'-Q=\;W3Q3] \E;#39LD?Q)R'/RAMX'R@,"LM*@
|
|
M[15#^]DK\?.HR8:YP.'B)04T7$8.'"$DU^Z%^O]ZSWZ*RJ%OXP92N6A9DT8%
|
|
M0C=OANA]_<-TF/PD;/2E^Z=%$'-0<Z^_;_[3\)4#-'U"[L8PAXU=0.V!SI +
|
|
M;8'P+Y8W"?F&]_6<#1'>\R!/#XM"#D^9_U7>YY)"STD!_)^IT>'&"T(L7O9$
|
|
M[$:9!OT"[R[5SO]>YU;_BIV9*AWV&/Y =KR@P)(#B]B.@]'IAI_A8+E4=B[K
|
|
M&#:6M Q$JXC=I<GLS9<&"_H ]O UPJR8_ _-?&H,"B?O<):^\73?-O8]6K1,
|
|
MKP[KA4OQ"A_E*YP4(%8"MQI+1PG;H:I9H8H@]>G"IMC$Z@RL.2$.;#%=H>#9
|
|
MT]%N#;^1N!<T79J)JJ6LE_*9^X1.]F"*<=7)D4-&B!%5)CBF+!=&50D8RD<)
|
|
MIRU$7<1?>&UA-J;W<Z)YV2SS,[@N-83(NEL,,C?B$:I:V9S*\C8VI]K8!,0%
|
|
MY$T!WA4L-E[:K1*:?L-MS;<UFN;302(O;G"-L>$7$[HM[/5EW<"OT3C();WU
|
|
MYE)L5FS<K7&"$+YO6VQF&1E2(QOOM'Q;XFXA^S:JU@#COZE<\W!%Z7#S!4FA
|
|
M@YMV"S^$:@G5_K:WR\4Q ?:9%\OWFF2FQS=;V,$4-F9R:\U@:$OA=#&-&K[(
|
|
MY!E.)@MXE#PF/:K4WL=,.#\_9O/';!Q5IRRV&,F5Q+YT:I@>/!F^NY@SAD\D
|
|
MJC[1SWQ_#*Q ,9M/#89T2GE=QJ=,NIS&8/+>1_KF^6.(>:AB\Z?.O>2<4 Z;
|
|
M.B2;OL+)>X&P(I('X@A+62CM E#:5ML4R_BI;ZM(N1=5P[%J\!O45LR,_M-Y
|
|
M(BS!)#W!V/NDT?V1]X.!PN!?6Y0-/"B%;I1B9 KPH *D40$Y\6#! _P/L'5$
|
|
MZ"E!0F_96F#WA ,;$=\:8'>D*9#W:QQM2OX-^QIOY'QKC- ;]>2GQMGHN'4G
|
|
M!-F(>)!Y <*'$?-B";E53Q-@4QBI=3 OFH#)8.C[3BMAW'XKA(I%@\04#063
|
|
MAKU:&G!*O@W]B&%?=:$IOW493)8R'[Y!<"S9!47_=]3Z8"-$LT#(ZL%#A#[3
|
|
M44%R:M"-NH)$GMO/PJ%/GIGZ<*&%>9JI2:1ND/&F1]P8L8#M;I^.[GH:O 2,
|
|
M-'N#G'ELFN],(V?74&)P2@Q R:D',$$_,>$4F. $F&#XR./DN%OMG\QJBJ!!
|
|
MQ4P?9"8T= &/KT2408-GM#R8;10!:$X:H#.Y4T8@DA'Y" P&4$L#! H &
|
|
M %1LQQJZ:;<U#@8 %P) ) <W!L:6YE;&%B%!$2! ($!08%#@8.!PD(
|
|
M/@<>!W[__PT1$P<&! ,&!006]_?W)P,B3(BP(,#R,6T"#(E0(4"B5&7'U#DU
|
|
MJ=8BQK>I<^A0V3!=T@0X%.%#@(9!8AV*TB;-E"RS&.-0L1CC68P=H%:$% $^
|
|
M!NFT9TR9-W4>?3JUJE"0,F'"A D0,$(>_'LJ%*/K\"_,%,=XC./1#]EE(!]'
|
|
M^2\C# K0* SS!'UJTZ=/J2(%:91IT*, >R,T"M!)_F7JS)*O,9Z6[#'D#=GV
|
|
M$_KXGQSA4H#C8]Q=I_JA ,\CA HP<)*QD[*_3G&O>Q+7I3C>%PSP/T*R -6#
|
|
MA)&-0\F6XLH$J#&A6X"A@Y+4N1@$\64? _O,E"!/@VP< [ "O)D0+P!K%=E9
|
|
M!'7N55:QKHX)D&I"0=S<@1NX*N['3<B(FR-QVYK*SI+/CA-:XN8(W,#R^SGA
|
|
M)NYM.XAMOUD3(.^$G=I9I<A3#?>K45"R*5RKSD0[<ZDG!$59AU &\/N$H^C9
|
|
MAY%!*L3.2OL*%9;29A[:H*VT'U+A*6V.H6T+)\*3O#@5IK+F+]8PPWI5H2IK
|
|
M7F%MJCR4]17<0XW5<W\*U@*TJY"6-[?PYE7>/AM&=\0/,+'"6][<P]MGN-QR
|
|
M5IA+FJ-(>\Z<. %N5[A+FEV*L*08'!N5G!H>Q"&:Q!":9!*:1/ U8\>9M?(5
|
|
M]O+G*?Y6\KZ*^ ++O1EK;-CBPQR;0BW2Q<B:WD3ONH97]95&09O!"^5&G0P
|
|
MSE7C9.7\5>/A^>O >*FE9<O:'+E5ENI"D0$3T=+%+F09D+S.S#I@EX/UA38#
|
|
M8 +-##(I"TU 2*_GA3NZ>4JW2 ;I;NR%0 -X0@HS@/,9EUC&^D*E <>]'<16
|
|
M 19?:'X V@$#E+[P_("?=7".'FKXJ0"[+T0_L#$(/4V_4/Y )3932,Y@^?L+
|
|
M^0\,#"*M($FMC >HDJ%!@EAEHZ0:XZRLYJK$LU.5L8SXS&G^C&;8:T.^DX_S
|
|
MK@PA$LPB#\\B#\TB#YI%'CPM0XT$*C\6UJ98 S,D26#Q*PFYX;E7.FMA7LZ-
|
|
MZ'NLAQFZ).@8(&F&2PD$]J%/FT*M2K7(<9U2E?J4R:=_F@1YD9TU0R;Z4;]S
|
|
MAE8)K"Z3M.+K(%P*:<O1,M*NL2&8<);):EQ7"32O8<LI^ZK0<VRI)Q\9?Y9Q
|
|
M\"SCG&6< ?)LJ 9V"6@R!T3FO.%;@HLNWF$PYC%KY'JS#>G,$VE@J'2Y#?T2
|
|
M '487QVJ'6_#OP1U K3<$# !F0;VFJ7VJD^K3E%^5:E6BS"L-PPTWMH-!Q.
|
|
MY94L6=1CRW)B&DZ_WA"1Y@!.HCED^0 [.FQ,0%&6Y2GF4&RT)>NJ8E\MHV F
|
|
M<,T"#L\"KEG <8!%'4H"/ Y3/*[*: -\U>$EA+E)8$,RRHIUR DV5_):T3I\
|
|
MM5@#.X3U6HD694HU:,NF16;[4R*ML%GLUSN,Y5W)#FL3H 6W=JB; "U89],@
|
|
MI+B-B_N9-EN1M+92W"&QR@X#,@SPQY05&BE?IJQ4T,PTALY0LG"?B/^Q7)](
|
|
M(9[QH;-ZS5*/9JD'SU(/FJ5>L]3KTH?80E;4\"S\3 5Z4BN>\\2OD\CR.F\K
|
|
MY&9!Q@&"?2B>0,B$RJ#](%.QU 6H]B$[@84#O',0O0!]A(>RCL5F^,."!6+F
|
|
M+3_6OEE4ANVKEJM#+"\W'T-EXGCX"W^HP%QGPQA'58WSJG$\Q\O\YP\I%A@\
|
|
MPKL:V>T_] #O6:Y@V'8EM.<*G_&(:H2(PERN,'X9IRL,I7.8$%L6@"W"LUF/
|
|
M,9L);V,M9#I/B#(+N#2+#*<*D6<!B0$ZA5>7D5^M$(T6N/5K(3X_$.LL]I-H
|
|
M[ =ETDMG#!'Z 8I^!.5;U=3CZ)\"H:"L??R&1WRGJY/4[$/4?L"CD[53926\
|
|
MA'.(Y ^(=)(R!EIW0W1_H*136<X%.!PB_H."0+[;(>)>L-.#3AGYV?2^'Z+P
|
|
MA41>XBGR-E<A\CJ#;)KZ(9NG4=D3/D"*$9E]KWK8NXB7LPLC9F^G?4Q5%&<.
|
|
M^S,C6L->>5*_3)"J",VW[.03'T4SI@*<&A'<O==& 1Y+F&)C3W-E-., LT94
|
|
MO]#1AA502P$""@,* !@!/;,<:L\TWI#$' #T" "@
|
|
MI($ 8FQA<F<N;6TN;U!+ 0(* PH & %1LQQJZ:;<U#@8 %P) )
|
|
M "D@5D' !S<&QI;F5L86)02P4& ( @!O C@T
|
|
#
|
|
|
|
end
|
|
|
|
.......
|
|
....
|
|
..
|
|
. C=H #20
|
|
|
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
|
|
Main Articles:
|
|
|
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
|
|
|
|
VIC KERNAL Disassembly Project - Part IV
|
|
Richard Cini
|
|
February, 2001
|
|
|
|
Introduction
|
|
============
|
|
|
|
In the last installment of this series, we examined the six remaining
|
|
routines that are called from the IRQ and NMI vectors. The routines examined
|
|
are responsible for updating the jiffy clock, determining the location of
|
|
color RAM, scanning the keyboard, and reading and setting the cursor position.
|
|
Having fully completed the main processor vectors, we'll continue this
|
|
series by examining other Kernal routines.
|
|
|
|
More Subroutines
|
|
================
|
|
Much of the last three articles dealt with routines that were used in
|
|
the VIC's startup process, configuring the screen and I/O devices, and polling
|
|
the keyboard for user input.
|
|
|
|
Although the entire Kernal ROM consists of probably over 100
|
|
individual subroutines -- all indirectly callable in some way - the VIC's
|
|
developers really intended the user to only call the 39 routines accessible
|
|
through the jump table at the end of the ROM. When looking at this table,
|
|
we've so far only discussed only 10 of the routines contained therein.
|
|
|
|
It was designed in this fashion for multiple reasons. The functions
|
|
selected for the jump table were clearly the ones Commodore found to be most
|
|
useful for software programmers. The other routines were "internal" routines
|
|
used by the "public" routines and the BASIC ROM.
|
|
|
|
The jump table also hid code changes from the software programmer, although I
|
|
don't believe that there were ever any significant updates to the Kernel after
|
|
its initial release. The calling routine and parameters remained consistent
|
|
even if the underlying code changed. It also facilitated sharing programs
|
|
between machines with common lineage. For example, certain programs could be
|
|
shared between the PET, VIC and the C64. Of course, minor modifications of
|
|
these programs were required to account for different I/O locations.
|
|
|
|
This structure is no different than a modern operating system such as Windows,
|
|
or even the Macintosh, which had the concept of a ROM "toolbox."
|
|
|
|
Anyway, I digress...
|
|
|
|
When examining the functions available in the jump table, the following
|
|
distribution becomes apparent:
|
|
|
|
User I/O (screen/keyboard): 3 functions
|
|
Timekeeping: 3 functions
|
|
Memory management: 3 functions
|
|
Processor control: 4 functions
|
|
Device I/O: 26 functions
|
|
|
|
Commodore appeared to be giving greatest support for interfacing the VIC to
|
|
various devices. Many of these routines were used by the built-in BASIC
|
|
interpreter to support file I/O through the tape deck and the serial IEEE
|
|
port, as well as printer and RS232 support. Unfortunately, the only devices
|
|
that made it onto the IEEE bus were floppy drive and printer devices. If there
|
|
are others, I appreciate knowing about them.
|
|
|
|
Anyway, this provides a nice segue into looking at the device I/O
|
|
routines in some detail.
|
|
|
|
The first step is opening a device. All devices are eligible to be
|
|
opened in this manner. The OPEN function requires that the program call two
|
|
preparatory routines first. The first one, SETNAM sets the filename parameter
|
|
required by OPEN. Clearly only certain devices make use of a filename (such as
|
|
the cassette or disk drive devices). For devices not requiring a filename
|
|
pointer, just set the pointer to "null".
|
|
|
|
|
|
FE49 ;===================================================
|
|
FE49 ; ISETNM - Set filename (internal)
|
|
FE49 ; Call with .A = filename length, .X = LSB of filename
|
|
FE49 ; string, .Y = MSB of filename string
|
|
FE49
|
|
FE49 ISETNM
|
|
FE49 85 B7 STA FNMLEN ; set length
|
|
FE4B 86 BB STX FNPTR ; ptr L
|
|
FE4D 84 BC STY FNPTR+1 ; ptr H
|
|
FE4F 60 RTS
|
|
|
|
Calling SETNAM sets three Zpage variables that tell the OPEN routine
|
|
where in memory to find a string containing the filename. Setting the length
|
|
parameter to zero yields a null string.
|
|
|
|
When opening an RS232 channel, the filename string would contain two
|
|
ASCII characters representing the configuration values for the command
|
|
register and the control register.
|
|
|
|
The next call that is required is to set the underlying device
|
|
parameters for the OPEN call.
|
|
|
|
FE50 ;===================================================
|
|
FE50 ; ISETLF - Set logical file parameters (internal)
|
|
FE50 ; Call with .A = file number, .X = device # (0-30),
|
|
FE50 ; .Y = command
|
|
FE50 ISETLF
|
|
FE50 85 B8 STA LOGFIL ; file #
|
|
FE52 86 BA STX CHANNL ; device
|
|
FE54 84 B9 STY SECADR ; secondary address
|
|
FE56 60 RTS
|
|
|
|
The SETLFS parameters are fairly common: the file handle number, the
|
|
device to access, and any secondary address to send to the device. The
|
|
combination of SETNAM and SETLFS is analogous to this BASIC statement:
|
|
|
|
OPEN 2,8,1, "mydat"
|
|
|
|
where "2" corresponds to the file handle number, "8" is the device, and "1"
|
|
is the secondary address. These numbers are ultimately stored in their
|
|
respective data tables at location IOPEN_S3 in the OPEN routine.
|
|
|
|
Opening the RS232 channel does not require the use of a secondary
|
|
address.
|
|
|
|
During file I/O, it's helpful to check the status of the current
|
|
device by using the READST function. READST retrieves the value of the CSTAT
|
|
variable. CSTAT is set through a call to ISETMS1 (at $FE6A) by various I/O
|
|
routines based on device response.
|
|
|
|
|
|
FE57 ;===================================================
|
|
FE57 ; IRDST - Get I/O status word (internal)
|
|
FE57 ;
|
|
FE57 IRDST
|
|
FE57 A5 BA LDA CHANNL ; get current device
|
|
FE59 C9 02 CMP #$02 ; RS232?
|
|
FE5B D0 0B BNE ISETMS+2 ; no...branch to OS messages
|
|
FE5D AD 97 02 LDA RSSTAT ; get RS232 status
|
|
FE60 A9 00 LDA #$00
|
|
FE62 8D 97 02 STA RSSTAT
|
|
FE65 60 RTS
|
|
FE66
|
|
FE66
|
|
FE66 ;===================================================
|
|
FE66 ; ISETMS - Control OS messages (internal)
|
|
FE66 ; On entry, .A is message number. Bit7=1 for KERNEL messages, and
|
|
FE66 ; Bit6=1 for control messages. Bit0-Bit5 is message number.
|
|
FE66 ;
|
|
FE66 ; set flag for OS messages
|
|
FE66 ;
|
|
FE66 ISETMS
|
|
FE66 85 9D STA CMDMOD ;save message #
|
|
FE68 A5 90 LDA CSTAT ;get status
|
|
FE6A
|
|
FE6A ; set ST bits
|
|
FE6A ISETMS1
|
|
FE6A 05 90 ORA CSTAT ;twiddle bits based on .A
|
|
FE6C 85 90 STA CSTAT ;save status
|
|
FE6E 60 RTS
|
|
|
|
The return values of the READST call have different meanings depending
|
|
on what device is being accessed:
|
|
|
|
BIT Cassette Serial R/W Tape L/V
|
|
=== ======== ========== ========
|
|
0 Write timeout
|
|
1 Read timeout
|
|
2 Short block Short block
|
|
3 Long block Long block
|
|
4 Unrecoverable Any mismatch
|
|
read error
|
|
5 Checksum error Checksum error
|
|
6 End of file EOI line
|
|
7 End of tape Device not End of tape
|
|
present
|
|
|
|
So, after having set the file name information and setting the device
|
|
access parameters, we're ready to actually OPEN the file. The following code
|
|
is heavily commented, so I'll only expand where necessary.
|
|
|
|
The Kernal keeps track of the open files using a series of file handle
|
|
tables. A maximum of 10 logical files can be open at one time. FILTBL keeps
|
|
track of the logical file handle established in the call to SETLFS. SECATB
|
|
keeps track of the secondary address associated with a given logical file
|
|
number. Finally, the DEVTBL variable tracks which device number relates to the
|
|
file handle. These three tables mirror the variables in the SETLFS call.
|
|
|
|
F40A ;======================================================
|
|
F40A ; IOPEN - Open file (internal)
|
|
F40A ; Required prior calls: FFBA/SETLFS and FFBD/SETNAM
|
|
F40A ; No arguments
|
|
F40A IOPEN
|
|
F40A A6 B8 LDX LOGFIL ;get file number
|
|
F40C D0 03 BNE IOPEN_S1 ;F411 read from output file?
|
|
F40E 4C 8D F7 JMP IOERMS6 ;Yes, emit "NOT INPUT FILE" error
|
|
F411
|
|
F411 IOPEN_S1
|
|
F411 20 CF F3 JSR FIND ;locate file # in table
|
|
F414 D0 03 BNE IOPEN_S2 ;file number not found, so move on
|
|
F416 4C 81 F7 JMP IOERMS2 ;found, so emit "FILE OPEN" error
|
|
F419
|
|
F419 IOPEN_S2
|
|
F419 A6 98 LDX COPNFL ;get # of open files
|
|
F41B E0 0A CPX #$0A ;are there 10 files open already?
|
|
F41D 90 03 BCC IOPEN_S3 ;no, OK to open new file
|
|
F41F 4C 7E F7 JMP IOERMS1 ;more than 10, "TOO MANY FILES" error
|
|
F422
|
|
F422 IOPEN_S3 ; allocate file slot in table
|
|
F422 E6 98 INC COPNFL ;add 1 to count of open files
|
|
F424 A5 B8 LDA LOGFIL ;get file number from call
|
|
F426 9D 59 02 STA FILTBL,X ;save file # in table
|
|
F429 A5 B9 LDA SECADR ; get secondary address
|
|
F42B 09 60 ORA #%01100000 ;$60 make it a device command
|
|
F42D 85 B9 STA SECADR ;save it as secondary and add it to the
|
|
F42F 9D 6D 02 STA SECATB,X ; SA table for that open file
|
|
F432 A5 BA LDA CHANNL ;save device number to the
|
|
F434 9D 63 02 STA DEVTBL,X ; device table
|
|
F437
|
|
F437 ;Special handling for certain devices
|
|
F437 F0 5A BEQ IOPENRC ; keyboard device? Yes, return
|
|
F439
|
|
F439 C9 03 CMP #$03 ;Is the device the screen?
|
|
F43B F0 56 BEQ IOPENRC ; yes, then return clear
|
|
F43D 90 05 BCC IOPEN_S4 ;must then be the tape or RS232-branch
|
|
F43F
|
|
F43F ; Start IEEE stuff
|
|
F43F 20 95 F4 JSR SENDSA ;send secondary to IEEE bus and
|
|
F442 90 4F BCC IOPENRC ; return clear
|
|
F444
|
|
F444 IOPEN_S4
|
|
F444 C9 02 CMP #$02 ;RS232 (2)?
|
|
F446 D0 03 BNE IOPEN_S5 ;not RS232, so process tape device
|
|
F448 4C C7 F4 JMP SEROPN ;open RS232 device
|
|
F44B
|
|
F44B IOPEN_S5 ;Tape device (1)
|
|
F44B 20 4D F8 JSR GETBFA ;Get tape buffer address
|
|
F44E B0 03 BCS IOPEN_S6 ;MSB>2? No, continue with open
|
|
F450 4C 96 F7 JMP IOERMS9 ;Bad tape buffer, emit "ILLEGAL DEVICE
|
|
F453 ; NUMBER" error
|
|
F453
|
|
F453 IOPEN_S6 ;Continue with tape processing
|
|
F453 A5 B9 LDA SECADR ;get SA
|
|
F455 29 0F AND #%00001111 ;Are we in write/save mode?
|
|
F457 D0 1F BNE IOPEN2 ;yes, prompt for Play + Record
|
|
F459
|
|
F459 20 94 F8 JSR PLAYMS ;wait for "PLAY" key
|
|
F45C B0 36 BCS IOPENRC+1 ;$F494 return CY=1
|
|
F45E
|
|
F45E 20 47 F6 JSR SRCHMS ;print "Searching for [name]" message
|
|
F461 A5 B7 LDA FNMLEN ;get length of filename
|
|
F463 F0 0A BEQ IOPEN1 ;no name specified, so search for
|
|
F465 ; next header
|
|
F465
|
|
F465 20 67 F8 JSR LOCSPH ;search for specific tape header
|
|
F468 90 18 BCC IOPEN3 ;$F482 go get header
|
|
F46A F0 28 BEQ IOPENRC+1 ;$F494 return found CY=1
|
|
F46C
|
|
F46C IOPENA ; done searching and file was not found
|
|
F46C 4C 87 F7 JMP IOERMS4 ;F787 "FILE NOT FOUND" error
|
|
F46F
|
|
F46F IOPEN1 ; Get next tape header
|
|
F46F 20 AF F7 JSR LOCTPH ;search for next header
|
|
F472 F0 20 BEQ IOPENRC+1 ;$F494 return found CY=1
|
|
F474 90 0C BCC IOPEN3 ;$F482 go get header
|
|
F476 B0 F4 BCS IOPENA ;$F46C not found
|
|
F478
|
|
F478 IOPEN2 ; write tape header
|
|
F478 20 B7 F8 JSR RECDMS ;wait for REC & PLAY keys
|
|
F47B B0 17 BCS IOPENRC+1 ;$F494 return CY=1
|
|
F47D
|
|
F47D A9 04 LDA #$04 ;control byte ID for data header
|
|
F47F 20 E7 F7 JSR WRTPHD ;write tape header
|
|
F482
|
|
F482 IOPEN3
|
|
F482 A9 BF LDA #$BF ;pointer to tape buffer head
|
|
F484 A4 B9 LDY SECADR ; get secondary address
|
|
F486 C0 60 CPY #$60 ;SA=0 (read) mode?
|
|
F488 F0 07 BEQ IOPENRC-2 ;$F491 Yes, skip write
|
|
F48A
|
|
F48A A0 00 LDY #$00 ; write mode
|
|
F48C A9 02 LDA #$02 ;control byte ID for data block
|
|
F48E 91 B2 STA (TAPE1),Y ;write it to buffer
|
|
F490 98 TYA
|
|
F491 85 A6 STA BUFPNT ;save pointer
|
|
F493
|
|
F493 IOPENRC
|
|
F493 18 CLC
|
|
F494 60 RTS
|
|
|
|
The OPEN function spends most of its time saving parameter information
|
|
into variable tables and figuring out which device to open. If the OPEN call
|
|
is for the tape device, the code either reads or writes the respective tape
|
|
header.
|
|
|
|
OPEN calls nine other routines, six of which deal with tape input and
|
|
output. The three non-tape routines are FIND (find file number in table),
|
|
SENDSA (send secondary address), and SEROPN (open RS232 device). Since the
|
|
OPEN code spiders out so, I'm going to leave the tape-related routines to
|
|
another article.
|
|
|
|
FIND is a very easy routine. It searches through the file handle table
|
|
to see if the handle of the file we're trying to open is already being used as
|
|
a result of a previous call to OPEN. Clearly each file handle number has to be
|
|
unique to prevent confusion at the system level. If a file is already in use
|
|
with the same handle number, .X returns non-zero, enabling the calling routine
|
|
to drop into an error handler ("FILE OPEN" error).
|
|
|
|
F3CF ;==========================================================
|
|
F3CF ; FIND - Look for logical file number in open-file table
|
|
F3CF ; On entry to FIND1, .A=file#. On exit, .X=offset in file table
|
|
F3FC ; to matching file number.
|
|
F3CF
|
|
F3CF FIND
|
|
F3CF A9 00 LDA #$00
|
|
F3D1 85 90 STA CSTAT ;clear Status variable
|
|
F3D3 8A TXA ; .X is logical file number copied to
|
|
F3D4 ; .A for use in FIND1
|
|
F3D4 FIND1
|
|
F3D4 A6 98 LDX COPNFL ;get #of open files to count through
|
|
F3D6
|
|
F3D6 FINDLOOP ; loop
|
|
F3D6 CA DEX
|
|
F3D7 30 15 BMI FLATRBX ;$F3EE reached 0, then exit
|
|
F3D9 DD 59 02 CMP FILTBL,X ;is this the one?
|
|
F3DC D0 F8 BNE FINDLOOP ;$F3D6 no, try again
|
|
F3DE 60 RTS ;return with .X= offset into table
|
|
|
|
The FLATRB routine is not used in OPEN, but the FIND subroutine exits
|
|
through it. I don't know why this is since the FIND routine has a return path
|
|
of its own. Anyway, FLATRB takes an index number into the open file table and
|
|
sets the Zpage file variables according to the values pointed to by the
|
|
index. For example, if I call FLATRB with .X=0, the file variables will be set
|
|
with the information stored in location [0] of each of the file handle table,
|
|
the device table, and the secondary address table. So, one could select an
|
|
open file handle, use FIND to get the index number, and then use FLATRB to set
|
|
the Zpage variables to the right values for subsequent use.
|
|
|
|
F3DF ;==========================================================
|
|
F3DF ; FLATRB - Set file values
|
|
F3DF ; On entry, .X = offset in the file tables. Returns with
|
|
F3DF ; Zpage file variables set.
|
|
F3DF
|
|
F3DF FLATRB
|
|
F3DF BD 59 02 LDA FILTBL,X
|
|
F3E2 85 B8 STA LOGFIL ;get file handle number
|
|
F3E4 BD 63 02 LDA DEVTBL,X
|
|
F3E7 85 BA STA CHANNL ;get device
|
|
F3E9 BD 6D 02 LDA SECATB,X
|
|
F3EC 85 B9 STA SECADR ;get SA
|
|
F3EE
|
|
F3EE FLATRBX
|
|
F3EE 60 RTS
|
|
|
|
After the OPEN routine determines that the file handle to be opened is
|
|
unique, assigns the file variables to the various data tables, and determines
|
|
which device is being opened, the code forks. If the device is the keyboard
|
|
(device 0) or screen (device 3), OPEN returns. If it's the tape deck (device
|
|
1), the code continues by searching for or writing a tape header using the
|
|
information supplied in the OPEN call. If it's for the RS232 adapter (device
|
|
2), execution jumps to SEROPN to continue the opening process. Finally, if
|
|
it's an IEEE device (device >=4), then OPEN calls SENDSA to shift out the
|
|
secondary address. When SENDSA returns, OPEN returns to the original
|
|
caller. If SENDSA returns with the carry flag set (indicating an error
|
|
condition) and then returns, execution will ultimately fall through to an
|
|
"ILLEGAL DEVICE ERROR" error.
|
|
|
|
F495 ;==========================================================
|
|
F495 ; SENDSA - Send secondary address
|
|
F495
|
|
F495 SENDSA
|
|
F495 A5 B9 LDA SECADR ;get SA
|
|
F497 30 2C BMI SNDSARC ;$F4C5 less than 0, exit
|
|
F499
|
|
F499 A4 B7 LDY FNMLEN ;get filename length
|
|
F49B F0 28 BEQ SNDSARC ;$F4C5 no filename, just exit
|
|
F49D
|
|
F49D ; prepare to send filename string
|
|
F49D A5 BA LDA CHANNL ;get device number and...
|
|
F49F 20 18 EE JSR ILISTN+1 ; command it to listen
|
|
F4A2 A5 B9 LDA SECADR ;get SA
|
|
F4A4 09 F0 ORA #%11110000 ;$F0 make it into listen command
|
|
F4A6 20 C0 EE JSR ISECND ;sent it to IEEE bus
|
|
F4A9 A5 90 LDA CSTAT ;check status variable
|
|
F4AB 10 05 BPL SENDSA1 ;$F4B2 OK, then continue
|
|
F4AD
|
|
F4AD 68 PLA ;error, set stack for caller's caller
|
|
F4AE 68 PLA
|
|
F4AF 4C 8A F7 JMP IOERMS5 ;emit "DEVICE NOT PRESENT" error
|
|
F4B2
|
|
F4B2 SENDSA1 ; continue processing-send filename
|
|
F4B2 A5 B7 LDA FNMLEN ;get filename length
|
|
F4B4 F0 0C BEQ SNDSARU ;length is 0, so send unlisten command
|
|
F4B6 ; There is a filename, so send it to IEEE
|
|
F4B6 A0 00 LDY #$00 ; set loop counter
|
|
F4B8
|
|
F4B8 SENDSALP ; output filename to IEEE bus
|
|
F4B8 B1 BB LDA (FNPTR),Y ;get character
|
|
F4BA 20 E4 EE JSR ICIOUT ;send it
|
|
F4BD C8 INY ;get next one
|
|
F4BE C4 B7 CPY FNMLEN ; at the end?
|
|
F4C0 D0 F6 BNE SENDSALP ;no, then loop
|
|
F4C2
|
|
F4C2 SNDSARU ;done sending filename, so send
|
|
F4C2 20 04 EF JSR IUNLSN ; unlisten command to IEEE bus
|
|
F4C5
|
|
F4C5 SNDSARC
|
|
F4C5 18 CLC
|
|
F4C6 60 RTS
|
|
|
|
SENDSA is a simple routine that checks for a valid secondary address
|
|
and whether the OPEN command involves a filename. Then, the routine commands
|
|
the specified device to "listen" and sends the secondary address and the
|
|
filename to it. When completed, the code sends the "unlisten" command to the
|
|
device and returns success. If there is a problem sending data to the device,
|
|
a "device not present" error is generated.
|
|
|
|
The next routine is SEROPN. SEROPN is called by OPEN when it
|
|
determines that the device that's being opened is the RS232 port. Here's a
|
|
quick story about RS232 support in the VIC.
|
|
|
|
Based on some information that I've seen on the Web and what's available in
|
|
"Inside VIC" and the "VIC20 Programmer's Reference Guide", I've concluded that
|
|
the VIC hardware was originally designed to include a MOS 6551 ACIA
|
|
communications chip. The 6551, with appropriate level shifters, would have
|
|
provided true hardware support for RS232. But for cost reasons, board space
|
|
reasons, or both, the 6551 was dropped and emulated in software and in
|
|
hardware by using a spare port on one of the existing 6522 VIAs.
|
|
|
|
The Kernal includes a lot of code to manage RS232 FIFO buffers and to perform
|
|
the bit shifting through the User Port (port B of VIA 1). The User Port only
|
|
provides TTL-level RS232; Commodore and others sold an adapter that provided
|
|
the level-shifting hardware (typically TI 1488/1489 chips) and a DB25F
|
|
connector. The circuit is very simple and could have been built by any
|
|
resourceful hobbyist.
|
|
|
|
Without further delay, here's the code that completes the opening process for
|
|
the RS232 serial device:
|
|
|
|
F4C7 ;==========================================================
|
|
F4C7 ; SEROPN - Open RS-232
|
|
F4C7 ; "filename" contains the initialization data for the
|
|
F4C7 ; command and control registers
|
|
F4C7
|
|
F4C7 SEROPN
|
|
F4C7 A9 06 LDA #%00000110 ;set VIA DDR. PB[2:1] are DTR and RTS
|
|
F4C9 8D 12 91 STA D1DDRB ; signals, respectively
|
|
F4CC 8D 10 91 STA D1ORB
|
|
F4CF A9 EE LDA #%11101110 ;$EE. Set PCR for CB2/CA2 manual high
|
|
F4D1 8D 1C 91 STA D1PCR ; and CB1/CA1 for H->L IRQ trigger
|
|
F4D4 ; CB2 is RS232-TX and CB1 is RS232-RX
|
|
F4D4 A0 00 LDY #$00
|
|
F4D6 8C 97 02 STY RSSTAT ; clear status byte
|
|
F4D9
|
|
F4D9 SEROPLP
|
|
F4D9 C4 B7 CPY FNMLEN ;is the filename length = 0?
|
|
F4DB F0 0A BEQ SEROPN1 ;$F4E7 yes, go straight to open
|
|
F4DD
|
|
F4DD B1 BB LDA (FNPTR),Y ;copy first 4 chars of filename...
|
|
F4DF 99 93 02 STA M51CTR,Y ; to buffer and loop
|
|
F4E2 C8 INY
|
|
F4E3 C0 04 CPY #$04
|
|
F4E5 D0 F2 BNE SEROPLP ;$F4D9 loop
|
|
F4E7
|
|
F4E7 SEROPN1 ;done copying init_data
|
|
F4E7 20 27 F0 JSR BITCNT ;get number of data bits
|
|
F4EA 8E 98 02 STX BITNUM ;save data bits count
|
|
F4ED AD 93 02 LDA M51CTR ;get control register
|
|
F4F0 29 0F AND #%00001111 ;$0F isolate baud rate bits
|
|
F4F2 D0 00 BNE $+2 ;F4F4
|
|
F4F4
|
|
F4F4 ;convert baud rate bitmap to clock
|
|
F4F4 ; divisor
|
|
F4F4 0A ASL A ;*2
|
|
F4F5 AA TAX
|
|
F4F6 BD 5A FF LDA R232TB-2,X ;$FF5A,X baud rate H
|
|
F4F9 0A ASL A
|
|
F4FA A8 TAY
|
|
F4FB BD 5B FF LDA R232TB-1,X ;$FF5B,X baud rate L
|
|
F4FE 2A ROL A
|
|
F4FF 48 PHA
|
|
F500 98 TYA
|
|
F501 69 C8 ADC #$C8
|
|
F503 8D 99 02 STA BAUDOF ;save baud rate divisor
|
|
F506 68 PLA
|
|
F507 69 00 ADC #$00
|
|
F509 8D 9A 02 STA BAUDOF+1
|
|
F50C
|
|
F50C AD 94 02 LDA M51CDR ; command register
|
|
F50F 4A LSR A
|
|
F510 90 09 BCC SEROPN2 ;$F51B
|
|
F512
|
|
F512 AD 20 91 LDA D2ORB ;check DSR (data set ready)
|
|
F515 0A ASL A
|
|
F516 B0 03 BCS SEROPN2 ;$F51B ready, continue
|
|
F518 4C 16 F0 JMP DSRERR ;not ready, DSR error
|
|
F51B
|
|
F51B SEROPN2 ; device ready
|
|
F51B AD 9B 02 LDA RIDBE ;set RS232 buffer pointer
|
|
F51E 8D 9C 02 STA RIDBS ; -- RX --
|
|
F521 AD 9E 02 LDA RODBE
|
|
F524 8D 9D 02 STA RODBS ; -- TX --
|
|
F527
|
|
F527 ; prepare to "hide" buffer in memory
|
|
F527 20 75 FE JSR IMEMTP+2 ; get MEMTOP
|
|
F52A A5 F8 LDA RIBUF+1 ;has RX buffer been created?
|
|
F52C D0 05 BNE SEROPN3 ;$F533 yes, check on TX buffer
|
|
F52E 88 DEY ; else create input buffer
|
|
F52F 84 F8 STY RIBUF+1
|
|
F531 86 F7 STX RIBUF
|
|
F533
|
|
F533 SEROPN3
|
|
F533 A5 FA LDA ROBUF+1 ;TX buffer created?
|
|
F535 D0 05 BNE SEROPN4 ;$F53C yes, skip create
|
|
F537 88 DEY
|
|
F538 84 FA STY ROBUF+1 ;else create TX buffer
|
|
F53A 86 F9 STX ROBUF
|
|
F53C
|
|
F53C SEROPN4 ; set MEMTOP to hide buffers from BASIC
|
|
F53C 38 SEC
|
|
F53D A9 F0 LDA #$F0
|
|
F53F 4C 7B FE JMP STOTOP ;$FE7B set new MEMTOP
|
|
|
|
SEROPN begins by extracting the values of the command and control registers
|
|
from the "filename" string passed to it from the OPEN routine. SEROPN then
|
|
saves the number of data bits contained in a serial data frame and then
|
|
calculates the proper clock divisors for the selected baud rates. The 6551
|
|
transmit and receive clocks are emulated by using one of the timers in VIA1 to
|
|
shift out the data at the specific bit rate. Receiving bits is accomplished
|
|
through the NMI routine discussed in Part 2 of this series.
|
|
|
|
In a nifty trick, SEROPN allocates transmit and receive buffer memory at the
|
|
top of BASIC RAM and lowers the top of RAM variable to fake BASIC into
|
|
thinking that there's 512 bytes less of memory.
|
|
|
|
The SEROPN routine finally exits and returns to OPEN by a JMP to STOTOP, the
|
|
routine used to set the top of system memory.
|
|
|
|
Well, that's all that we have time for. Next time, we'll tackle some of the
|
|
tape routines related to the OPEN command.
|
|
.......
|
|
....
|
|
..
|
|
. C=H #20
|
|
|
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
|
|
|
|
Mods and digital mixing
|
|
----------------------- by Jolse Maginnis
|
|
jmaginni@postoffice.utas.edu.au
|
|
|
|
As most of you know, MOD playing on the C64 is nothing new, Nate/DAC wrote
|
|
modplay64 quite a while ago now, but there has never been any in depth look at
|
|
how a mod is played. So in this article I will discuss everything you need to
|
|
know about MODs and digital sound mixing (well the main bits anyway!)
|
|
|
|
Very recently I got sick of working on OS stuff and started working on my own
|
|
modplayer for JOS, called, wait for it... Josmod! I was really quite surprised
|
|
at the quality of sound it produced, particularly as it was only playing with
|
|
4 Bit Mono SID digis.
|
|
|
|
At that stage Josmod only played 4-channel Amiga format MOD's which have been
|
|
around for a long time but are now being outdated by newer formats such as
|
|
S3M, XM and IT. These new formats are in the same style as the old Amiga MOD
|
|
formats, except they allow for more channels, more instruments/samples and
|
|
different effects. Since the SCPU is a fast enough machine to mix and play
|
|
more than 4 channels at once, and I was sick of seeing so many MOD's that it
|
|
couldn't play because they were done in the newer formats, I added support for
|
|
S3M's and more recently, XM mods.
|
|
|
|
Josmod didn't take very much time to write at all, for one reason in
|
|
particular: it's written in C. Compared to writing an application in assembly,
|
|
C is so much easier to debug and test. That's one of the great things about
|
|
the 65816, you can write in a high level language and get away with it. One
|
|
part of Josmod had to be written in 65816, and that's the mixer, which has to
|
|
be optimized as it's where 90% of Josmod's time is spent.
|
|
|
|
Enough propaganda! On with the show..
|
|
|
|
-------------------------------------------
|
|
|
|
There are 3 main parts to any modplayer: the loader, the player routine & the
|
|
mixer.
|
|
|
|
First of all I will start by talking about the mixer as it's the simplest part
|
|
and also the part that deals with the actual sounds themselves.
|
|
|
|
The Mixer
|
|
---------
|
|
|
|
First of all, I'd like to point out that I'm no expert on digital sound, and I
|
|
only learned all this stuff myself when I started writing Josmod. Here's an
|
|
ASCII drawing of a digitally sampled sound:
|
|
|
|
127
|
|
|
|
-- --
|
|
/ \ -- / \
|
|
/ \ / \ / \
|
|
0------------------------------------------
|
|
\ / \ /
|
|
\ / --
|
|
\ /
|
|
---
|
|
-128
|
|
|
|
|
|
Yes I know it looks crap, but you get the idea. You'll note that it's a SIGNED
|
|
sample, which is very important when it comes to mixing.
|
|
|
|
The Amiga has 4 digital sound channels, each capable of playing its own set of
|
|
sampled data, so it can effective play four different sounds at the same time.
|
|
But what about the C64? SID's only capable of playing one digi at a time. How
|
|
on earth can we get it to play 2 samples at once, or 4? 8 or 16? C64's aren't
|
|
alone in their single channel output problem, most PC sound cards only have 1
|
|
channel for output (2 if you count Stereo). So how is it done? It's extremely
|
|
simple actually. You just add them together! But remember it's signed
|
|
addition!
|
|
|
|
The one thing that you have to worry about when adding though, is that you
|
|
don't overflow your result. For example, if you're adding two 8 bit numbers
|
|
together you could easily go higher than 127 or lower than -128, just by
|
|
having two loud samples. So it's best to use a 16 bit result, and handily the
|
|
65816 has 16-bit registers. :) On a side note, if a sample is mixed with an
|
|
exact opposite of itself, it turns to silence! I believe this is actually used
|
|
in some new cars to cut down on noise inside the car! [Editor's note: it is
|
|
also used in things like aviation headsets, and goes under terms like "Active
|
|
Noise Reduction" and "Active Noise Cancellation"]
|
|
|
|
Not only does the mixer have to mix samples together, it also has to control a
|
|
couple of other things, namely volume and pitch. The player reads the notes
|
|
and effects, and tells the mixer which samples to play, at what speed and how
|
|
loud they are supposed to be.
|
|
|
|
I'll start with volume, as it's quite an easy transformation. In Amiga MODs
|
|
volumes range from 0 (Silence) to 64 (Full volume). So a volume of 32 would be
|
|
half volume, and effectively divide the samples by 2. e.g. A sample of -40
|
|
would become -20 at volume 32, and a sample of 100 would become 50. The best
|
|
and fastest way to perform volume calculations in real time is of course
|
|
using a lookup table. So you end up with mixer code that looks like this:
|
|
(65816 remember!)
|
|
|
|
|
|
lda [Samp] ; Get the current sample
|
|
and #$ff ; It's 8 bits only
|
|
asl ; Multiply by 2 since the volume
|
|
tay ; table is 16 bit values
|
|
lda [VolTab],y ; Get the value!
|
|
|
|
|
|
VolTab contains a pointer to the appropriate volume table, which is calculated
|
|
with:
|
|
|
|
Sample*Volume/64
|
|
|
|
Volume was easy but what about pitch? Ordinarily if we were dealing with
|
|
normal 1 channel digis we'd simply change our CIA timer rate so data would
|
|
be fetched at a different speed, which is ideal. This is basically what the
|
|
Amiga does, except it's done in hardware, so doesn't steal valuable CPU
|
|
cycles. However, we can't do this as we're mixing various samples into one! So
|
|
instead we have to simulate different pitches by altering the sample position
|
|
at varying rates, rather than just going straight to the next sample.
|
|
|
|
So for example, if a sample needs to be played at twice its normal speed, we
|
|
add 2 to the sample pointer rather than 1, and it will sound twice as
|
|
fast. It's all very well adding values like 1 and 2, but what if the sample
|
|
needs to be 1.5 times the normal speed? It's easy solved with some fixed point
|
|
math (16 bits for the fraction, 16 bits for the integer part), and you end up
|
|
with something like this (remember that Samp is a pointer into the sample
|
|
data):
|
|
|
|
txa ; Add the fraction part
|
|
adc Low ; X is used to hold the low part
|
|
tax
|
|
lda Samp ; Now add the integer with carry
|
|
adc Hi
|
|
sta Samp
|
|
bcc noinc ; Crossed a bank boundary?
|
|
inc Samp+2
|
|
noinc ....
|
|
|
|
Very simple really isn't it?
|
|
|
|
Note that the tradeoff here is between playback rate and sample rate; that is,
|
|
if we have a sound sampled at 10000 samples per second, and take every other
|
|
sample, that's like taking 5000 samples per second. So instead of changing
|
|
the _playback_ rate -- which is what e.g. changing the CIA timers does -- we
|
|
instead effectively change the original _sample_ rate. Another thing worth
|
|
mentioning is that when the playback rate is greater than the sample rate, you
|
|
can improve output by "interpolating" the sample. Interpolating is basically
|
|
guessing what the sample would have sounded like if it were sampled at the
|
|
playback rate. I won't go into it any more than that, as adding it will slow
|
|
the mixer down. I believe Nate's latest modplay64 does interpolation.
|
|
|
|
That's basically all that the mixer does, but I've left out one part, which is
|
|
the part that adds the samples together. Rather than adding 4 values together
|
|
at a time the Josmod mixer uses a buffer to add all the values for 1 channel
|
|
in one go, and then adds all the samples for the next channel to that buffer,
|
|
etc. This means that the mixer can handle as many channels as it wants, rather
|
|
than being fixed to 4 channels. Here is the code for mixing 2 samples: (Assume
|
|
16 bit registers!)
|
|
|
|
lda [Samp]
|
|
and #$ff
|
|
asl
|
|
tay
|
|
lda [VolTab],y
|
|
adc (OutBuf) ; Add it to the buffer
|
|
sta (OutBuf)
|
|
txa
|
|
adc Low
|
|
tax
|
|
lda Samp
|
|
adc Hi
|
|
sta Samp
|
|
bcc ninc
|
|
inc Samp+2
|
|
ninc lda [Samp]
|
|
and #$ff
|
|
asl
|
|
tay
|
|
lda [VolTab],y
|
|
ldy #2 ; Next buffer position
|
|
adc (OutBuf),y
|
|
sta (OutBuf),y
|
|
txa
|
|
adc Low
|
|
tax
|
|
lda Samp
|
|
adc Hi
|
|
sta Samp
|
|
bcc ninc2
|
|
inc Samp+2
|
|
ninc2 ...
|
|
|
|
By changing Samp (and Low/Hi) the same code can add different samples to the
|
|
buffer. Josmod uses an unrolled mixing loop which does 16 samples before
|
|
looping, otherwise there'd be too much loop overhead.
|
|
|
|
You will note that at the end of mixing all the channels what we're left with
|
|
is a buffer that is full of 16 bit samples. Well, I believe the IDE64 team
|
|
have a made a 16 bit sound card... but generally this buffer is not going to
|
|
be of any use to us, until we convert it to 8 bit unsigned data. The SID uses
|
|
4 bit unsigned samples, and the DigiMax and other user port sound cards, use 8
|
|
bit unsigned, so it's the best format to convert it to.
|
|
|
|
Not only does it need to be converted to 8 bit unsigned, but remember the
|
|
overflow problem? Values that are too loud need to be clipped to their maximum
|
|
volume (127 or -128). So we can kill two birds with one stone here, and create
|
|
a post processing lookup table that converts the data and also clips loud
|
|
samples. If it were graphed the table would look something like this:
|
|
|
|
$ff -------
|
|
^ \
|
|
Clipped \
|
|
\
|
|
\
|
|
$80 ------------------------
|
|
\
|
|
\
|
|
\
|
|
\
|
|
0 --------
|
|
^
|
|
Clipped
|
|
|
|
So after all channels are mixed together, the buffer is postprocessed with
|
|
that table to prepare it for output on the sound device! Which incidentally
|
|
could be any device, as Josmod is device independent, it's up to the device
|
|
driver to deal with getting a CIA interrupt and what to do with the
|
|
samples. In the case of 4-bit SID this would be:
|
|
|
|
lda [Samp]
|
|
lsr
|
|
lsr
|
|
lsr
|
|
lsr
|
|
sta @$d418 ; @ means long addressing - $00d418 SID Volume register!
|
|
|
|
That's about all for the mixer, the only other thing that it needs to do is to
|
|
check if a sample has ended, and either stop it, or loop it back to some
|
|
point.
|
|
|
|
Bear in mind that the mixer I've described is a mono one, as all channels get
|
|
mixed into the one output buffer. A stereo one would mix channels into two
|
|
different output buffers.
|
|
|
|
I should mention that since I wrote this article, I've made some improvements
|
|
to the mixer. Namely pre-calculating the sample pointer offset, so that the
|
|
mixer loop doesn't need to calculate the next sample position for every
|
|
sample. So now the sample mixing code is much quicker:
|
|
|
|
|
|
lda [Samp]
|
|
and #$ff
|
|
asl
|
|
tay
|
|
lda [VolTab],y
|
|
adc (OutBuf) ; Add it to the buffer
|
|
sta (OutBuf)
|
|
|
|
ldy #1 ; The offset to the next sample
|
|
lda [Samp],y ; (precalculated)
|
|
and #$ff
|
|
asl
|
|
tay
|
|
lda [VolTab],y
|
|
ldy #2 ; Next buffer position
|
|
adc (OutBuf),y
|
|
sta (OutBuf),y
|
|
|
|
....
|
|
|
|
txa ; Add sample speed * 16
|
|
adc Low ; This code now only gets
|
|
tax ; executed once per 16
|
|
lda Samp ; samples rather than every
|
|
adc Hi ; sample.
|
|
sta Samp
|
|
bcc ninc2
|
|
inc Samp+2
|
|
ninc2 ...
|
|
|
|
These changes provide, at a guess, a dramatic increase of about 30 percent in
|
|
playback speed. The only drawback to this method is very slight loss of
|
|
accuracy, but I don't think you will be able to tell the difference.
|
|
|
|
The Loader
|
|
----------
|
|
|
|
You've now seen how to mix samples, so what you need to know next is how to
|
|
load in the samples from the modfile and also load the data that tells you how
|
|
to play them. Ok let's check out the .mod file format. Sizes are all in bytes.
|
|
|
|
Offs : Size
|
|
------------
|
|
0 : 20 : Module name
|
|
20 : 930 : Sample headers for 31 Samples
|
|
950 : 1 : Number of orders
|
|
951 : 1 : Unused?
|
|
952 : 128 : The orders
|
|
1080 : 4 : Identification string
|
|
1084 : ? : The patterns
|
|
? : ? : The 8 bit sample data for all samples
|
|
|
|
Ok first of all you need to know exactly what orders & patterns are.
|
|
|
|
Patterns contain all the information about what pitch and sample to play and
|
|
also contain information about effects which change the volume and pitch of
|
|
samples. Every pattern contains 64 rows of data. Each row holds note
|
|
information for the number of channels contained in the tune (normally 4 in
|
|
Mods). A pattern can be visualised as something like this:
|
|
|
|
1 2 3 4
|
|
------------------------------------
|
|
1: | 1 c-1 | 2 d-2 | 2 f#5 | 3 e-4 |
|
|
2: | 5 d-1 | - --- | - --- | 3 f-4 |
|
|
......
|
|
64: | 4 e-1 | 3 d-1 | - --- | - --- |
|
|
------------------------------------
|
|
|
|
That's a simplified version of what a pattern is like, as it doesn't include
|
|
any effect information.
|
|
|
|
So what does "1 c-1" mean?
|
|
|
|
Well the first 1 refers to the sample number, while "c-1" refers to the note
|
|
and octave to play (C natural, octave 1).
|
|
|
|
Orders will be familiar to anyone who's had any experience with some of the
|
|
SID editors/players. Rather than playing pattern 0, pattern 1, etc.. through
|
|
to the last pattern, orders allow you to choose the order in which patterns
|
|
are played, thus allowing you to repeat patterns easily. E.g. The order table
|
|
may look something like this: 0 1 2 3 2 2 4 3 5 6
|
|
|
|
You will notice that nowhere in the mod format does it mention how many
|
|
patterns are contained in the file.. To calculate this you look through the
|
|
order table and find the highest value, and use that as the number of patterns
|
|
in the file (+1).
|
|
|
|
Ok now let's have a look at the sample header format:
|
|
|
|
0 : 22 : Name
|
|
22 : 2 : Length (divided by 2)
|
|
24 : 1 : Finetune
|
|
25 : 1 : Default Volume (0-64)
|
|
26 : 2 : Loop position (divided by 2)
|
|
28 : 2 : Loop length (divided by 2)
|
|
30
|
|
|
|
One thing to be warey of here is the fact that the 16 bit values stored in
|
|
this structure are in big endian format, because Amigas use the 680x0 line of
|
|
processors. So to convert it for use with C64 it needs the high and low bytes
|
|
swapped around. Also I have no idea why the 16 bit values are stored divided
|
|
by 2... I guess this is something to do with playing the mod on the Amiga too,
|
|
or perhaps to allow larger than 64k samples?
|
|
|
|
Ok now we know what the format of a .mod file is, it's time to get it loaded
|
|
so our player can play it!
|
|
|
|
You will notice that the first 1084 bytes of the file are fixed, so one
|
|
approach to loading would be to load those first 1084 bytes and go on from
|
|
there, but the approach I use is to load each part of the header seperately,
|
|
which makes it easier to put the data in a format easier for the player. Ok
|
|
let's take a look at the C source for Josmod's .mod loader..
|
|
|
|
First a couple of declaration for identifying which mod format it is:
|
|
|
|
typedef struct IdentStruct {
|
|
char *String;
|
|
int channels;
|
|
} Ident;
|
|
|
|
static Ident idents[] = {
|
|
{"2CHN", 2},
|
|
{"M.K.", 4},
|
|
{"M!K!", 4},
|
|
{"FLT4", 4},
|
|
{"4CHN", 4},
|
|
{"6CHN", 6},
|
|
{"8CHN", 8},
|
|
{"CD81", 8}
|
|
};
|
|
|
|
int loadMod(char *name, ModHead *mp) {
|
|
|
|
/* Variable declarations */
|
|
|
|
FILE *fp;
|
|
S3MSamp *samp;
|
|
unsigned char *patp;
|
|
unsigned int i,j,temp,numpat,temp2;
|
|
long patsize;
|
|
char *samdata;
|
|
Ident *idp;
|
|
|
|
/* Open the mod */
|
|
|
|
fp = fopen(name, "rb");
|
|
if (!fp) {
|
|
perror("josmod");
|
|
exit(1);
|
|
}
|
|
|
|
/* Read the name of the mod */
|
|
|
|
fread(mp->Name,1,20,fp);
|
|
mp->Name[20]=0; // C strings are null terminated!
|
|
|
|
/* Read 31 sample headers */
|
|
|
|
samp = &mp->Samples[0];
|
|
for (i=0;i<31;i++) {
|
|
|
|
/* Read name and null terminate */
|
|
|
|
fread(samp->Name,1,22,fp);
|
|
samp->Name[22]=0;
|
|
|
|
|
|
/* Read length
|
|
Note: readWord() converts the 16 bit value
|
|
into C64 format
|
|
*/
|
|
|
|
samp->Length = readWord(fp) << 1;
|
|
|
|
/* Read and calculate finetune
|
|
Finetune is a 4 bit signed value
|
|
Values over 8 are negative values
|
|
*/
|
|
|
|
samp->Finetune = fgetc(fp);
|
|
if (samp->Finetune>=8)
|
|
samp->Finetune -= 16;
|
|
|
|
/* Read volume */
|
|
|
|
samp->Volume = fgetc(fp);
|
|
|
|
/* Read and calculate ending/looping positions */
|
|
|
|
temp = readWord(fp) << 1;
|
|
samp->Replen = readWord(fp) << 1;
|
|
|
|
/* If the loop length is higher than 2, this
|
|
is a looping sample, otherwise it's a standard
|
|
non looping sample that stops at the end */
|
|
|
|
if (samp->Replen > 2) {
|
|
samp->Looped = 1;
|
|
samp->End = (char *) temp + samp->Replen;
|
|
if ((unsigned int) samp->End > samp->Length)
|
|
samp->End = (char *) samp->Length;
|
|
} else {
|
|
samp->End = (char *) samp->Length;
|
|
samp->Looped = 0;
|
|
}
|
|
|
|
/* Go to next sample structure */
|
|
samp++;
|
|
}
|
|
|
|
/* Read orders and calculate number of patterns */
|
|
|
|
mp->NumOrders = fgetc(fp); fgetc(fp);
|
|
numpat=0;
|
|
for (i=0;i<128;i++) {
|
|
temp = fgetc(fp);
|
|
if (temp > numpat)
|
|
numpat = temp;
|
|
mp->Orders[i] = temp;
|
|
}
|
|
numpat++;
|
|
mp->NumPatterns = numpat;
|
|
|
|
/* Identify type of mod */
|
|
|
|
fread(mp->Type,1,4,fp);
|
|
mp->Type[4]=0;
|
|
|
|
mp->Channels=0;
|
|
idp = &idents[0];
|
|
for (i=0;i<8;i++) {
|
|
if (!strncmp(idp->String,mp->Type,4)) {
|
|
mp->Channels=idp->channels;
|
|
continue;
|
|
}
|
|
idp++;
|
|
}
|
|
|
|
/* If we couldn't identify it, this isn't a mod file!
|
|
close the file and return. Otherwise print number of
|
|
channels. */
|
|
|
|
if (!mp->Channels) {
|
|
fclose(fp);
|
|
return 0;
|
|
} else {
|
|
printf("Channels %d\n",mp->Channels);
|
|
}
|
|
|
|
|
|
/* Calculate pattern sizes and allocate memory for patterns */
|
|
|
|
mp->LineSize = 4 * mp->Channels;
|
|
mp->PatSize = 64 * mp->LineSize;
|
|
patsize = mp->PatSize * numpat;
|
|
patp = xmalloc(patsize);
|
|
mp->Patterns = patp;
|
|
|
|
/* Load patterns */
|
|
|
|
for (i=0;i<numpat;i++) {
|
|
for (j=mp->PatSize/4;j;j--) {
|
|
/* Each note is stored in 4 bytes.
|
|
Contained in the 4 bytes is:
|
|
|
|
Sample number.
|
|
Note
|
|
Effect
|
|
Effect Parameter
|
|
|
|
conv2Note() converts the mod format note format
|
|
into a format easier for Josmod.
|
|
|
|
*/
|
|
|
|
* (unsigned int *) (patp+2) = readWord(fp);
|
|
* (unsigned int *) patp = readWord(fp);
|
|
conv2Note(patp);
|
|
patp += 4;
|
|
}
|
|
}
|
|
|
|
/* Load Samples */
|
|
|
|
samp = &mp->Samples[0];
|
|
for (i=0;i<31;i++) {
|
|
printf("Sample %d: %s\n",i,samp->Name);
|
|
if (samp->Length) {
|
|
|
|
/* If the sample exists, allocate memory
|
|
for it, and read it in. Then fix up it's end
|
|
pointer, then call fixSamp() to prepare it for
|
|
mixer output. */
|
|
|
|
samdata = xmalloc((long) samp->Length + 128);
|
|
fread(samdata,1,samp->Length,fp);
|
|
samp->Samp = samdata;
|
|
samp->End = (unsigned long) samp->End + samdata;
|
|
fixSamp(samp->Looped, samp->End, samp->Replen, 128);
|
|
|
|
} else samp->Samp = NULL;
|
|
samp++;
|
|
}
|
|
|
|
/* Close the file and return successfully */
|
|
|
|
fclose(fp);
|
|
return 1;
|
|
}
|
|
|
|
That's all for the loader! Now that patterns, orders, and samples are all
|
|
loaded and in a suitable format, the player is ready to play it!
|
|
|
|
Unfortunately that's all for now, as the player is quite a complicated topic,
|
|
I'll save it for a later article. But before I finish, I do want to give you a
|
|
basic outline of what happens in the player, so you can see where the mixer
|
|
fits into all this.
|
|
|
|
The Player
|
|
----------
|
|
|
|
Mods have two speed variables which control the way a mod is played. One is
|
|
BPM, or Beats Per Minute. BPM specifies the speed at which the mod player
|
|
routine is called, the default value is 125 BPM, or 50 times a second. As we
|
|
all know, 50hz is the speed of PAL TV's, so it's no coincidence that this is
|
|
the default, as it allowed Amiga players to setup their play routines on an
|
|
interrupt synchronised with the screen, the same as with SID players.
|
|
|
|
BUT since we don't have the luxury of having hardware to play the samples for
|
|
us, it's not a good idea to hook our player to a raster interrupt, instead we
|
|
calculate how many output samples need to mixed before we update our mod. We
|
|
calculate that with the following:
|
|
|
|
playbackrate / (BPM*2 / 5)
|
|
|
|
This value is what I call the "TickSize". Everytime the player is updated, it
|
|
will mix TickSize bytes of samples, then loop and update the mod again, mix
|
|
TickSize bytes again, etc..
|
|
|
|
The other speed variable is the number of Ticks per beat, and defaults to 6.
|
|
This means that the mod player only fetches the next row of pattern data every
|
|
6 Ticks. E.g. After 6*TickSize samples have been mixed.
|
|
|
|
Each channel in the Mod has assocatied with it, a mixer structure, which looks
|
|
like this:
|
|
|
|
typedef struct {
|
|
char *Samp; // Current sample position
|
|
char *End; // End or loop position
|
|
unsigned long Replen; // How far to loop back
|
|
unsigned long Speed; // Speed/pitch of sample
|
|
int Low; // Fractional part of sample position
|
|
int Repeats; // Whether or not it repeats
|
|
int *VolTab; // Pointer to volume table
|
|
int Active; // Whether to mix this channel
|
|
// or not
|
|
} MixChan;
|
|
|
|
The player has to prepare these structures for each channel and on every tick
|
|
update the appropriate fields depending on the data contained in the patterns.
|
|
|
|
I'll leave you with psuedo code for what the player looks like:
|
|
|
|
tickbeat = 6
|
|
row = 64
|
|
tickcount = 0
|
|
orderup = 0
|
|
make all channels inactive
|
|
while (orderup < numorders) {
|
|
if (row == 64) {
|
|
get next pattern from orders
|
|
orderup++
|
|
}
|
|
if (tickcount == 0) {
|
|
get row data from pattern
|
|
process note - set sample speed
|
|
process sample number - set sample position volume
|
|
process effect - speed, volume and position effects
|
|
} else {
|
|
process effect
|
|
}
|
|
mix ticksize samples
|
|
if (mixing buffer full) {
|
|
play buffer
|
|
}
|
|
tickcount++
|
|
if (tickcount >= tickbeat) {
|
|
tickcount = 0
|
|
move to next row
|
|
row++
|
|
}
|
|
}
|
|
|
|
That's basically what the mixer does. The majority of the player code deals
|
|
with effects processing, most of which are easy to process. Effects include:
|
|
|
|
Portamento - sliding a note's pitch up or down.
|
|
Volume setting/sliding
|
|
BPM and Tick Beat changes
|
|
Pattern looping
|
|
Vibrato and Tremolo - Slight alterations in pitch and volume.
|
|
|
|
Some effects are only processed on Tick 0, and some are processed on other
|
|
ticks.
|
|
|
|
Anyway that's all for now, the player code will be examined in a later
|
|
article. In the meantime you can view the source code in HTML form at:
|
|
http://jos64.com/src/josmod.c.html
|
|
.......
|
|
....
|
|
..
|
|
. C=H #20
|
|
|
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
|
|
<=============
|
|
The C64 Digi
|
|
=============> Robin Harbron <macbeth@psw.ca>
|
|
Levente Harsfalvi <levente@terrasoft.hu>
|
|
Stephen Judd <sjudd@ffd2.com>
|
|
|
|
Introduction
|
|
------------
|
|
|
|
Digis -- digitally sampled audio -- are fairly common on the 64. This is
|
|
meant to be a comprehensive article on digis: how they work, examples,
|
|
different playback methods on the 64 (volume register and Pulse Width
|
|
Modulation), and some tricks. We'll even show you how to play 6-bit and
|
|
even 8-bit digis in high quality on a 64, which is really pretty neat to
|
|
hear.
|
|
|
|
The first part discusses digis from a fundamental point of view -- just
|
|
what a digi is, acoustic signals, and things like that. The most common
|
|
method of playing digis is via the volume register at $d418, and the next
|
|
two sections are devoted to this technique. Section two discusses some
|
|
SID fundamentals, and the reason why $d418 may be used for digis (and why
|
|
later-model SIDs don't play digis correctly); Section three discusses
|
|
$d418-digis from a software perspective: how to play them, tricks for
|
|
improving them, how to boost digis on 8580 SIDs, and how to detect what
|
|
kind of SID (6581 or 8580) is in the machine. The fourth and final part
|
|
of this article discusses pulse width modulation, and includes example source
|
|
code and a binary that plays a true 7-bit digi at around 16KHz -- something
|
|
which, we think, has never been done before.
|
|
|
|
Without further ado...
|
|
|
|
===============
|
|
Digis: Overview
|
|
===============
|
|
|
|
The whole point of playing a digi on a 64 is to provide something
|
|
for your ear to hear. So let's begin by discussing just what an acoustic
|
|
signal is and how that relates to digis.
|
|
|
|
Probably everyone knows that "sound" is how your ear responds to
|
|
changes in air pressure -- that is, when you clap your hands together,
|
|
it compresses the air between your hands in a special way, and that
|
|
higher pressure moves outwards into the surrounding air (since it's at
|
|
lower pressure). That pressure change propagates along and when it
|
|
encounters your ear it causes the ear drums to move, causing three little
|
|
bones to move, causing some fluid to move, causing tiny, exquisitely
|
|
sensitive hairs to move, transmitting a signal that your brain converts
|
|
to "sound".
|
|
|
|
An audio speaker also changes the air pressure in response to a
|
|
signal. If you take a coil of wire and change the voltage on it, it
|
|
generates a magnetic field; if a magnet is placed inside the coil, the
|
|
changing magnetic field will place a force on the magnet, causing it to
|
|
move, causing some air to be pushed along, causing a change in pressure,
|
|
causing a signal to propagate to your ear which your brain interprets as
|
|
Van Halen. All a stereo (CD player, etc.) does is send a varying voltage
|
|
signal to the speaker. As that voltage level goes up and down the magnet
|
|
moves back and forth, and so the speaker converts that electrical energy
|
|
into an accoustic wave.
|
|
|
|
For us, the trick is to coax SID into sending a specific voltage
|
|
signal to the speaker, the way a stereo or CD player might. And a CD player
|
|
is of course a very apt comparison, since it is itself a digi player.
|
|
|
|
Just for reference, a really good pair of ears can hear signals from
|
|
around 20Hz to 22KHz, with the sensitivity dropping considerably outside
|
|
of around 100Hz to 10KHz. A CD player has a playback rate of 44KHz, and
|
|
the highest frequency SID can generate from the frequency registers is
|
|
around 4KHz. If you've ever set SID to maximum frequency and heard just
|
|
how high 4KHz is, you can appreciate that even 10KHz is _really_ high, and
|
|
actually quite difficult to hear. In human speech, most of the information
|
|
content of vowel sounds is contained in the range 300Hz - 3KHz, and above
|
|
around 1KHz for consonant sounds; most information in musical sounds is in
|
|
the range 100Hz - 3KHz.
|
|
|
|
Discrete Sampling
|
|
|
|
To understand digis a little better, consider the more general
|
|
case of a discretely sampled signal -- a continuous signal sampled at
|
|
discrete time intervals. Let's say we had some device producing a
|
|
_continuous_ sinusoidal signal in time:
|
|
|
|
|
|
* *
|
|
* *
|
|
* *
|
|
* *
|
|
* *
|
|
* * *
|
|
* * *
|
|
* *
|
|
* *
|
|
* *
|
|
* *
|
|
* *
|
|
* *
|
|
-----------------------------------------------> time
|
|
|
|
(yes, I did miss my calling as an ASCII artist)
|
|
|
|
To turn the signal into a _discrete_ signal, we simply sample the
|
|
signal at discrete intervals of time. For example, let's say the above
|
|
signal lasts one second, and is input into a device which measures the
|
|
value every 1/4-second. The device will spit out four numbers: 0, 1, 0,
|
|
and -1:
|
|
|
|
*
|
|
|
|
|
|
* *
|
|
|
|
|
|
*
|
|
|
|
The sampling frequency here is four samples per second -- 4 Hz. If we were
|
|
to then play back this signal at the sampling frequency, we'd get a signal
|
|
like
|
|
|
|
**********
|
|
|
|
|
|
********** **********
|
|
|
|
|
|
**********
|
|
|
|
So one thing sampling does is to "staircase" a signal -- the sample becomes
|
|
some sort of "average" value over the sample period. Increasing the sample
|
|
rate -- taking more samples per second -- will smooth things out, and the
|
|
sampled signal will look (and sound!) more like the original signal.
|
|
|
|
Now let's say we just took two samples in that one second -- 2 Hz sampling
|
|
rate -- and just happened to catch the signal at its maximum and minimum
|
|
values (the peak and trough). Upon playback, the signal would look like
|
|
|
|
*********************
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*********************
|
|
|
|
That is, a square (pulse) wave. If you're on the ball, you've noticed
|
|
that the frequency of the new signal is 1 Hz -- exactly half the sampling
|
|
frequency. This is also called the Nyquist frequency. In general, the
|
|
_maximum_ frequency that can be captured in a discrete sample (called the
|
|
Nyquist critical frequency) is half the sampling frequency -- as you can
|
|
see above, it takes two data points to get a single (nonzero) frequency.
|
|
So, for example, the highest frequency a CD player -- which has a sampling/
|
|
playback rate of 44KHz -- can capture is 22KHz, well above the range of
|
|
normal human hearing.
|
|
|
|
Thus, increasing the sample rate increases the frequency range captured
|
|
in the discrete signal. This is why a digi at a high sample rate in general
|
|
sounds better than a digi sampled at a low sample rate.
|
|
|
|
BUT -- there is more to life than sample rate: there is also sample
|
|
resolution. The sample resolution -- 4-bit samples, 8-bit samples, etc. --
|
|
determines how accurately the sample measures the actual signal. For
|
|
example, let's say we sample sin(x) when x=0.5:
|
|
|
|
sin(0.5) = 0.4794255...
|
|
|
|
No matter what sample resolution we use, there will always be some error
|
|
in the measurement, and the _true_ value of the sample will be the
|
|
_measured_ value plus some error.
|
|
|
|
In general the sampling errors are random and uniformly distributed, so
|
|
the sampled signal corresponds to the original signal plus some noise (the
|
|
random errors). That is why you almost always hear some sort of hiss on
|
|
a normal C64 digi, which uses a resolution of 4 bits per sample.
|
|
|
|
So, increasing the sample _resolution_ decreases the amount of noise introduced
|
|
into the sampled signal (and increases the dynamic range), and increasing the
|
|
sample _rate_ increases the frequency range.
|
|
|
|
|
|
If you're _really_ on the ball, you've noticed that the 1-Hz square pulse
|
|
above actually contains frequencies higher than 1Hz, simply because a
|
|
square pulse contains higher harmonics in addition to the 1Hz fundamental
|
|
frequency. And you've also no doubt realized that the sampled pulse wave
|
|
would sound different than the original sine wave (due, of course, to the
|
|
added harmonics) -- it's at the right frequency, but it will sound like a
|
|
pulse wave instead of a sinusoid.
|
|
|
|
Have we somehow broken the Nyquist limit?
|
|
|
|
The answer is no, because of a nifty thing called the Discrete Sampling
|
|
Theorem, which says that, given the samples h_n of a bandwidth-limited
|
|
function h(t), the original function h(t) is given by
|
|
|
|
h(t) = dt * Sum{ h_n * sin(2*pi*f_c*(t-n*dt)) / (pi*(t-n*dt)) }
|
|
|
|
where dt is the sampling period and f_c is the cutoff/critical frequency.
|
|
|
|
What this means is that the original signal can be _reconstructed_ from the
|
|
discrete samples, not that it is _equivalent_ to the discrete samples.
|
|
The Nyquist limit is the highest frequency that can be _reconstructed_ from
|
|
the discrete samples, not the highest frequency that will be produced if you
|
|
"staircase" the discrete samples through a speaker. If the original
|
|
signal is bandwidth-limited, and there are at least two samples for the
|
|
highest frequency, then the signal can be completely reconstructed.
|
|
|
|
Since a "normal" digi contains all these extra frequencies, shouldn't a digi
|
|
sound "different" than a "true" analog signal? Sure. On the other hand, many
|
|
of the extra frequencies are beyond the range of human hearing, and the rest
|
|
can often be removed using a filter -- all CD players filter the output, for
|
|
example. So sometimes it is worthwhile to turn on a low/band pass filter
|
|
when playing a C64 digi, especially at lower sample rates.
|
|
|
|
And that more or less summarizes basic discrete sampling theory.
|
|
|
|
|
|
=============
|
|
D418 Playback -- Hardware
|
|
=============
|
|
|
|
The SID contains both analog and digital subparts on one silicon plate -- in
|
|
other words, it is a mixed signal device.
|
|
|
|
At the time, the SID was certainly the best of the microcomputer sound chips.
|
|
This may be mostly due to its mixed signal design, which the designers used
|
|
to solve certain problems.
|
|
|
|
The hard thing in a sound generator design is to implement waveforms, volume
|
|
control, and mixing. Things like that don't really fit into the digital
|
|
'either 0 or 1' philosophy, unless lot of data bits and arithmetic functions
|
|
are involved. In a fully digital sound chip, the waveforms could be generated
|
|
by ROM lookup tables. The mixing function could be derived from binary
|
|
addition, while the volume control from division or multiplication. Unless the
|
|
sound functionality is greatly simplified, the arithmetic functions must be
|
|
present and they must be implemented in hardware. Finally, the D/A conversion
|
|
could be done by (fast) pulse width modulation just at the output stage.
|
|
(Most of today's wavetable sound cards operate like this).
|
|
|
|
This method implies heavy arithmetic hardware, which was not an option for
|
|
designers back then. Still, most sound chips were fully digital, and all
|
|
suffer from the required compromises (i.e. generating square waves only,
|
|
no dedicated channel volume control, etc. - both TED and the VIC-I are obvious
|
|
examples).
|
|
|
|
The solution that one finds in the SID design is very straightforward: mixing
|
|
and variable volume level is problematic in a digital circuit when dealing
|
|
with waveforms, so simply avoid doing it. In the SID, only the microcomputer
|
|
interface, the registers, the oscillators (phase accumulating oscillators),
|
|
and other controller logic are digital; the mixing and volume control parts
|
|
are fully analog. There are digital to analog converters providing analog
|
|
voltage levels from the digital state variables. The SID D/As are in fact
|
|
'multiplying' D/As, having an analog input (AIN), an input base voltage
|
|
(IBASE), and a digital input. They operate by amplifying the input voltage
|
|
offset (AIN-IBASE) by a factor proportional to the number on the digital input
|
|
and adding this offset back to the base level.
|
|
|
|
This mixed signal design also allowed some other features to be implemented.
|
|
The most important one is the analog filter (that is, a two integrator loop,
|
|
bi-quadratic filter, according to Yannes). With that, the SID points beyond a
|
|
home computer sound chip - it is a true analog subtractive synth (marketing as
|
|
such was cancelled because of manufacturing capacity reasons).
|
|
|
|
Here is a detailed map on the SID inners (analog path; probably my most
|
|
beautiful ASCII ever :-D). Info can be found in the SID patents (US 4,677,890;
|
|
1986), the MOS 6581 technical document (can be found somewhere on the Net), or
|
|
the back of the Programmer's Reference Guide (PRG).
|
|
|
|
|
|
----------------- 11bit ------------
|
|
|Cutoff freq reg|-------->|Cutoff D/A|---------o
|
|
----------------- ------------ |
|
|
$d415-16 |
|
|
|
|
|
----------------- 4bit ------------ |
|
|
|Resonance reg. |-------->|Reson. D/A |-o |
|
|
----------------- ------------- | |
|
|
$d417.[4-7] | |
|
|
| |
|
|
=0 v v
|
|
----------- ----------- >o------------>| ------------------
|
|
|wave D/A |--->|env. D/A |-->o/ | | |
|
|
----------- ----------- o--->| | | |
|
|
^ ^ ^ =1 o--------|--->| |
|
|
|12bit |8bit | | | | |
|
|
| | | | | | |
|
|
----------- ----------- | | | | |
|
|
|OSC1 + | |ADSR cnt+| | | | | |
|
|
|wave sel.| |env. log.| | | | | |
|
|
----------- ----------- | | | | |
|
|
$d400-03, $d405-06, $d417.0 | | | |
|
|
$d404.[1-7] $d404.0 | | | FILTER |
|
|
| | | |
|
|
=0 | | | |
|
|
----------- ----------- >o----|------->| | |
|
|
|wave D/A |--->|env. D/A |-->o/ | | | |
|
|
----------- ----------- o--->| | | |
|
|
^ ^ ^ =1 | | | |
|
|
|12bit |8bit | | | | |
|
|
| | | | | | |
|
|
----------- ----------- | | | | |
|
|
|OSC2 + | |ADSR cnt+| | | | | |
|
|
|wave sel.| |env. log.| | | | | |
|
|
----------- ----------- | | | | |
|
|
$d407-0a, $d40c-0d, $d417.1 | | | |
|
|
$d40b.[1-7] $d40b.0 | | | LP BP HP |
|
|
| =0 | ------------------
|
|
=0 | >o->| | | |
|
|
----------- ----------- >o----|--o/ | *** o o o
|
|
|wave D/A |--->|env. D/A |-->o/ | o- | / / /
|
|
----------- ----------- o--->| ^ =1 | =0 V V V =1
|
|
^ ^ ^ =1 | | | o o o o o o
|
|
|12bit |8bit | | | | | | | | | |
|
|
| | | |$d418.7 |<-------o | |
|
|
----------- ----------- | | |<------------o |
|
|
|OSC3 + | |ADSR cnt+| | | |<-----------------o
|
|
|wave sel.| |env. log.| | | |
|
|
----------- ----------- | | |
|
|
$d40e-11, $d413-14, $d417.2 | |
|
|
$d412.[1-7] $d412.0 | | -----------------
|
|
| | | Master volume |AUDIO
|
|
=0 | o--->| D/A |----->
|
|
>o----|------->| ----------------- OUT
|
|
EXT IN --------------------->o/ | ^
|
|
o--->| |4bit
|
|
^ =1 ^ |
|
|
| | $d418.[0-3]
|
|
$d417.3 ^ |
|
|
| |
|
|
Analog mixing ---|--------|
|
|
|
|
***: Filter type select switches, $d418.[4-6] respectively
|
|
|
|
|
|
$d418 digis
|
|
-----------
|
|
|
|
The most common method of playing a digi is to use the register at $d418.
|
|
When someone plays a digi using the master volume register, the situation is
|
|
similar to the waveform D/A converters. Both D/As are multiplying D/As --
|
|
signal amplifiers whose amplification is proportional to the input digital
|
|
number. If there is a nonzero signal offset on the D/A input it will be
|
|
multiplied proportionally by this number.
|
|
|
|
Playing digis with $d418 is possible because there is indeed a relatively
|
|
large DC voltage offset on the master volume D/A. This offset is present
|
|
right from the moment when the SID is powered up.
|
|
|
|
Where can this DC offset come from?
|
|
|
|
There is a mixer before the master volume D/A (see figure). If there's a DC
|
|
offset on the D/A input, it must come from there. ...And going further,
|
|
the DC offset on the mixer must also come from somewhere. But where?
|
|
|
|
Signals come from the three ADSR volume D/As, the EXTIN line, and the three
|
|
outputs of the filter. Fortunately, all paths that go to the mixer have analog
|
|
switches (all paths can be disconnected from the mixer individually, if that's
|
|
needed).
|
|
|
|
The above analog switches are driven by the filter selection bits ($d417 bits
|
|
0-3), the voice 3 off bit ($d418 bit 7) and the filter type selection bits
|
|
($d418 bits 4-6).
|
|
|
|
After a reset, the filter selector bits are all 0 (all signals are routed
|
|
towards the master mixer), the 'voice 3 off' switch is on, and the filter type
|
|
selector bits are 0 (filter outputs are unconnected). In this state, only
|
|
EXTIN and the three SID voice signals are present on the mixer. EXTIN can
|
|
be eliminated as the source since it has no DC offset (as long as the computer
|
|
was not hacked, see notes on the 8580).
|
|
|
|
The ADSR volume D/A is similar to the previously mentioned multiplying D/As.
|
|
If the digital number on the input is 0, the input analog signal offset can't
|
|
pass through (as measurements verify). This is the case when SID is reset,
|
|
setting the envelope counters to zero. Therefore, nothing behind the ADSR
|
|
multiplying D/As can have any effect on the DC offset of the mixer.
|
|
|
|
So, the DC offset must come from the ADSR multiplying D/As. Another
|
|
measurement shows that even the mixer itself has a small DC offset.
|
|
|
|
|
|
Tests and results
|
|
-----------------
|
|
|
|
I did some tests that support this theory. They were done 'by hand', by simply
|
|
using a digital voltmeter + the FC3 monitor.
|
|
|
|
The chip was a 6581(R1), 0883, Hong Kong (an early 6581).
|
|
|
|
When turned on the voltage on the AUDIO OUT was about 5.5 volts (slowly
|
|
decreasing as it warmed up, stopping at about 5.43 after some 10 mins - all
|
|
subsequent tests were done after this time period).
|
|
|
|
Writing $0f to $d418 raised the output voltage to 6.15 volts. Therefore, the
|
|
maximum output amplitude that can be achieved when playing digis is 0.72 volts
|
|
in this 'mode' (without wiggling any other SID settings to achieve higher
|
|
voltage levels) -- remember that what counts is the maximum voltage
|
|
_difference_, not the maximum absolute voltage.
|
|
|
|
The next test is to determine if the mixer has its own DC offset (with all
|
|
possible paths are disconnected). It's possible to do. With the volume at
|
|
maximum (to maximize any effect), all voices are routed towards the filter
|
|
($d417 = $0f), while making sure that the filter outputs are not routed to the
|
|
mixer ($d418 = $0f). In this state no paths can drive the mixer. The result
|
|
is 5.39 volts. When the volume changes, the output also changes towards the
|
|
previous 5.43 volts --> there is a (very small) DC offset from just the mixer.
|
|
|
|
What could be the DC offset value of each individual SID voice (i.e. the base
|
|
level difference of the multiplying D/As)? Doing the above, but leaving one
|
|
voice routed to the mixer ($d417 = $0e, $0d or $0b) gives 5.69 volts.
|
|
5.69-5.43 = 0.26 volts, and 5.43 + 3*0.26 = 6.21, almost 6.15 volts.
|
|
|
|
To determine if the ADSR multiplying D/As act as expected, I used pulse
|
|
waves with zero frequency and 0 or $fff pulse width (two cases), to make the
|
|
input signal of the ADSR multiplying D/A the minimum and maximum possible
|
|
level. After careful checking, the output changed a few hundredth volts
|
|
(about 0.01 volt per voice). So the D/A doesn't close up completely, but
|
|
it's still O.K.
|
|
|
|
To prove that these offsets are equal for all voices, I did another test. Some
|
|
people know that the filter inverts phase (multiplies the input signal by -1).
|
|
Machine is reset, $d417 = $01, $d418 = $9f. (Voice 1 is routed through the
|
|
filter, voice 3 is cut off from the mixer completely ($d418.7), low pass
|
|
filter is selected, volume = $0f). The output voltage was 5.41 volts, just
|
|
very slightly below the "default" output level. This means that the DC of
|
|
voice2 + (-1*) DC of voice 1 resulted in about 0 relative offset. Doing
|
|
similar tests proved that the DC offsets for the voices match each other
|
|
almost exactly (within a few hundredths of a volt).
|
|
|
|
These measurements all support the idea that the DC offset comes from the
|
|
ADSR multiplying D/As, that the offset is mostly independent from the waveform
|
|
D/A converters (as long as sustain levels are 0), and that the offsets are
|
|
equal for all voices. In addition, a small DC offset is supplied by the master
|
|
signal mixer itself.
|
|
|
|
What if we try different sustain settings? For this test, set the volume to
|
|
maximum, as usual. Set the sustain level to $0f for all voices ($d406, $d40d,
|
|
$d414 = $f0). Start the attack, but with no waveform selected ($d404, $d40b,
|
|
$d412 = 01). The output level is now 5.21 volts, a little bit below the '0'
|
|
offset of the audio output! (Doing the test with just one voice (all
|
|
others disconnected), the output is 5.29 volts).
|
|
|
|
Finally, we can do some experiments with the pulse waveform. The pulse
|
|
waveform is useful for these tests, since at zero frequency we can set both
|
|
the minimum and the maximum constant DC levels at the voice D/A just by using
|
|
the pulse width registers. Reset the computer. Set voice 1 to zero frequency,
|
|
pulse level $0fff, sustain level 15, and $d404=$41 (pulse waveform + gate on).
|
|
Route only voice 1 through the mixer ($d417 = $0e). The output voltage is
|
|
similar to the test when no waveform was selected -- 5.29 volts! This seems to
|
|
show that "waveform accu = $0fff" is the same as when no waveform is selected
|
|
(i.e. the waveform D/A digital input pins are pulled high when they're not
|
|
driven, as seen in most other NMOS chips).
|
|
|
|
When the pulse width is 0 in the above test the output changes to 6.34 volts.
|
|
This seems to be strange (a multiplying D/A giving higher signal level for
|
|
multiplying something by 0).
|
|
|
|
Now, when the ADSR multiplying D/A is closed, the output is 5.70 volts. When
|
|
it's fully open, the output changes from 5.29 volts (wave acc= $fff) to 6.34
|
|
volts (wave acc = 0). One reasonable answer is that the base voltage of the
|
|
waveform D/A is higher, and the analog input is tied lower than the base
|
|
voltage of the ADSR D/A -- the effect is that the SID waveforms will lie
|
|
'around' the ADSR multiplying D/A base voltage, more or less symmetrically.
|
|
|
|
This was surely done intentionally, to reduce absolute voltage levels (for
|
|
linearity). In the 6581, the big DC offset is probably a result of having the
|
|
ADSR D/As and the master volume D/A at different base levels (the difference
|
|
appears as true DC offset on the master volume D/A). If both were the same
|
|
(presumably at VDD/2), and the waveform D/A parameters were selected similarly
|
|
(operation is symmetric to VDD/2), there would be no final DC offset at all.
|
|
Rather like the 8580...
|
|
|
|
|
|
Other issues
|
|
------------
|
|
|
|
So now we know why $d418 digis are possible - but still, there are some things
|
|
to note.
|
|
|
|
The DC offset on the master volume D/A changes with different SID settings,
|
|
and whatever affects the DC offset on the mixer will affect the digi
|
|
volume. For example, even the filter output signals have a small DC offset.
|
|
Just do a test - set the volume to 0f, then simply turn one filter route on
|
|
(for example, $d418 = $1f). You'll hear a small click (i.e. a small DC offset
|
|
change on the mixer), even if the filter has no input.
|
|
|
|
Moreover, as seen above, the DC offset can be eliminated completely (just by
|
|
SID register settings), leading to no audible digi sound at the output. In
|
|
other words, whatever affects the DC offset on the mixer _will_ affect the
|
|
digi volume.
|
|
|
|
One place where this is important is playing a digi with a tune: there's a
|
|
constantly changing signal going to the mixer instead of a constant DC offset,
|
|
so playing a digi on the master volume also causes distortion for both the SID
|
|
voices and the digi sound (since they're cross modulated). To reduce this
|
|
effect most 3+1 like SID + digi players play samples by writing 8-offset sample
|
|
values to $d418 (ie. adding 8 to 3-bit sample values and writing this to $d418
|
|
- see players used by Jeroen Tel and other famous composers using digi). This
|
|
trick reduces the modulating effect while still maintaining good digi volume.
|
|
|
|
The DC offsets used to create awful clicking sometimes. For example, the
|
|
filter inverts phase. If the filter is currently routed to the mixer, there'll
|
|
be a large 'click' (2 times the DC offset) when a voice is on and its routing
|
|
is changed to or from the filter.
|
|
|
|
|
|
The 8580
|
|
--------
|
|
|
|
This is a completely redesigned chip. I don't know details, but it was
|
|
probably redesigned by the time all other chips in the C64 were done for CSGs
|
|
new manufacturing technology and the C64c. It is a 'better' chip from the
|
|
technical side (but in my opinion it sounds crude in comparison to the 6581,
|
|
at least the R4 series). The 6581 was designed in months. Bob Yannes had to do
|
|
everything from scratch and use the manufacturing technology MOS currently had
|
|
(NMOS). And it shows. First, it has high background noise. The DC offsets are
|
|
really also a misfeature. The D/A converters are sometimes non-monotonic (at
|
|
least, the waveform D/As and the filter cutoff D/A have some drops at the
|
|
change of the most significant bits). The op-amps in the active (resonant)
|
|
filter are simple, linearized NMOS inverters ;-) (loopbacked, they act like
|
|
more or less linear op-amplifiers around VDD/2). And I still haven't mentioned
|
|
bugs in the digital side (ADSR envelope bugs). Because of the above, one
|
|
probably won't find two identical 6581 chips -- each sounds a little bit
|
|
different (mostly due to the filter). Since the active components of the
|
|
filter are far from ideal, the filter is strongly nonlinear (the cutoff curve
|
|
changes with signal amplitude). On the other hand, these things are what make
|
|
the SID sound so unique.
|
|
|
|
Most of the problems were fixed in the 8580. It has much less background
|
|
noise. The chips sound the same (there are hardly any differences between
|
|
different 8580s). Most of the DC offset issues (the clicks) were elminated. It
|
|
needs less power, and lower VDD level. Something was changed also in the
|
|
digital logic, but the ADSR part was not touched. The 'combined' waveforms are
|
|
a bit different (and more useable from the musician's point of view).
|
|
|
|
The clicks were reduced, which means that there is no (or no significant) DC
|
|
offset on the master volume D/A in the 8580.
|
|
|
|
(I have not done any measurements, but after listening to a lot of 3+1 channel
|
|
type musics, I have a strong suspicion that even if sounds are turned on, the
|
|
average DC offset on the master volume D/A is still minimal).
|
|
|
|
To fix this in software, you'll have to wait until the next section of this
|
|
article.
|
|
|
|
To fix this in hardware, people use a simple hack: take a resistor of about
|
|
330k and tie the SID EXTIN line to GND through that (directly, beside the
|
|
chip, on the mainboard).
|
|
|
|
The EXTIN line goes directly to the mixer, and thus the master volume D/A, or
|
|
can also be routed through the filter. In either case, unless the filter is
|
|
disconnected, the above hack will give a pretty large DC offset, similar to
|
|
the original 6581s. So, digi sounds can be played :-) (even with SID music
|
|
playing simultaneously, similar to the 6581).
|
|
|
|
This solution is good as a work around, but there's one thing to note: this is
|
|
not completely the same as the 6581 ADSR D/A offset voltage. At least, this
|
|
offset is negative (should that pin rather be tied to VDD?). Programs that
|
|
depend on the 6581s way of DC offsets will not work correctly (but I know of
|
|
very few such programs, so at worst you'll experience slightly different digi
|
|
sound only occasionally -- but hey, the 8580 sounds different anyway). Another
|
|
problem is that when EXTIN is routed through the filter the DC offset may
|
|
cause strong distortion since the DC operating point of the filter is changed
|
|
-- bad news if the 'semi-linear' amplifiers in the filter are picky about
|
|
absolute DC level. Some music (not neccessarily involved with digis) indeed
|
|
do route EXTIN through the filter, for noise reduction on older C64s (with the
|
|
earlier C64 mainboards that pick up lots of 'digital' background noise from
|
|
EXTIN). DC distortion can also occur occasionally for the same reason but on
|
|
the master volume D/A (the higher the difference from VDD/2, the greater the
|
|
risk of experiencing nonlinearity and clipping distortion).
|
|
|
|
|
|
Some final words
|
|
----------------
|
|
|
|
A lot of this information comes from Dag Lem, who is certainly the No. 1 SID
|
|
hacker for me ;-). Take a look at reSID, his SID emulator library (the sources
|
|
can be downloaded from somewhere). reSID contains so much reverse engineered
|
|
information of the real SID that you won't believe it -- check it out if
|
|
you're interested.
|
|
|
|
|
|
=============
|
|
D418 Playback -- Software
|
|
=============
|
|
|
|
$D418 digis are by far the most common playback method. The volume register
|
|
gives 16 different amplitudes (0-15), and so can provide 4-bit digi playback.
|
|
|
|
In its most basic form, this is an extremely easy routine to code. Simply
|
|
load each 4-bit sample, and store it in the volume register ($d418).
|
|
Assuming $fd/$fe are pointing to the beginning of a series of samples, the
|
|
following code will play it back:
|
|
|
|
ldy #0
|
|
|
|
:loop lda ($fd),y
|
|
sta $d418
|
|
|
|
ldx #5 ;some delay value
|
|
:delay dex
|
|
bne :delay
|
|
|
|
iny
|
|
bne :loop
|
|
|
|
inc $fe
|
|
jmp :loop
|
|
|
|
The ldx #5 would have to be adjusted depending on the speed of the
|
|
sample - the lower this number (not including zero) the faster the
|
|
sample will play back.
|
|
|
|
There are a number of improvements we could make to this code - first
|
|
of all, this method takes twice as much RAM to store the sample as is
|
|
necessary. Because we're dealing with 4-bit samples, we can store 2
|
|
samples in each byte. This can be handled simply by alternately
|
|
masking out the high bits (with AND #15) to play the sample stored in
|
|
the low nybble, and by shifting the high nybble down to the low nybble
|
|
to play the high nybble (LSR : LSR : LSR : LSR). A lookup table may also
|
|
be used to save processor cycles (but use more RAM).
|
|
|
|
Another improvement is to move the routine to zero-page, and use self-
|
|
modifying code. In general, this results in the fastest digi players.
|
|
|
|
We should of course have the routine check for the end of the sample --
|
|
typically just checking the high byte of the zero-page pointer is enough
|
|
(in this case, checking $fe). Typically digis are page aligned anyway, so
|
|
just zeroing out the unused part (if any) of the last page is fine.
|
|
|
|
Finally, it is often important that each sample of a digi is played back
|
|
at regular intervals. If the samples aren't played at a steady speed,
|
|
extra distortion is audible. In the example above, playback is steady
|
|
for a full page (256) of samples - but several extra cycles are added
|
|
by incrementing the zero-page pointer to the digi. The situation
|
|
worsens when we start adding extra code to check for the end of the
|
|
digi, and even the main loop starts getting irregular when we add the
|
|
code for the simple form of packing discussed earlier (2 4-bit samples
|
|
per byte).
|
|
|
|
These problems can be solved by careful cycle counting and adding NOP
|
|
and harmless BIT instructions in strategic places to make each
|
|
iteration the same number of cycles, regardless of which branch is
|
|
taken - people who have written a stable raster routine, or done some
|
|
Atari 2600 coding have likely done this sort of painstaking work
|
|
before.
|
|
|
|
|
|
NMI-driven digis
|
|
----------------
|
|
|
|
More commonly, however, we enlist the help of CIA #2 and have it
|
|
generate regular Non-Maskable Interrupts which we use to call our digi
|
|
player. This has two important advantages - first, it makes timing
|
|
much more simple. Second, it frees your main program to do other
|
|
things while the digi is playing "in the background".
|
|
|
|
To experiment, I pulled a 4-bit packed digi from the extras disk
|
|
included with Super Snapshot 5.22. It's the beginning seconds of the
|
|
introduction to Classic Star Trek (Space, the Final Frontier).
|
|
|
|
Here's the source for a fairly "frills-free" NMI based digi player,
|
|
with my comments after blocks of code:
|
|
|
|
start = $1400
|
|
end = $7cff
|
|
freq = 141
|
|
ptr = $fd
|
|
|
|
Labels start and end simply point to the beginning and end of the
|
|
digi. Freq isn't actually the frequency - it's the number of
|
|
processor cycles between interrupts necessary to play the digi at the
|
|
desired speed/pitch. If you know the frequency (in hz) of the digi,
|
|
simply divide your CIA clock speed (approximately 1000000 hz) by the
|
|
digi frequency. In this case, the digi runs at approximately 7100 hz.
|
|
|
|
We use two zero page locations to form a 16-bit pointer to the current
|
|
sample in the digi to play.
|
|
|
|
*= $1000
|
|
|
|
;disable interrupts
|
|
lda #$7f
|
|
sta $dc0d
|
|
sta $dd0d
|
|
lda $dc0d
|
|
lda $dd0d
|
|
sei
|
|
|
|
This code simply disables interrupts and initializes both CIA timers.
|
|
|
|
;blank screen
|
|
lda $d011
|
|
and #255-16
|
|
sta $d011
|
|
|
|
Just like erratically timed code can introduce distortion when a digi is
|
|
played back, the VIC steals cycles from the processor that can cause
|
|
interrupts to not occur precisely when you'd like them to. This routine will
|
|
work without the screen blanked, but the extra noise introduced when the
|
|
screen is on is noticeable when the time between samples is less than around
|
|
2.5-3 times the time the processor is stopped. Another option is to use some
|
|
multiple of the raster timing as the sampling rate, and start the routine on a
|
|
non-badline, to ensure that the interrupts never occur on a badline. (A final
|
|
option is to use a raster-driven interrupt for the digi; with the SCPU, it is
|
|
actually possible to drive an IFLI display and play a digi at the same time,
|
|
badlines and all -- email Robin for more info, or maybe wait for a future
|
|
article!). But the simplest thing to do is to blank the screen :).
|
|
|
|
;switch out roms
|
|
lda #$35
|
|
sta 1
|
|
|
|
;point to our player routine
|
|
lda #<nmi
|
|
sta $fffa
|
|
lda #>nmi
|
|
sta $fffb
|
|
|
|
Unless using the KERNAL routines is necessary in my program, I always
|
|
switch out the ROMs. One of the biggest benefits is that our NMI
|
|
routine will be immediately called, rather than using $0318/$0319 and
|
|
waiting for the KERNAL to indirectly call your routine.
|
|
|
|
;initialize player
|
|
lda #<start
|
|
sta ptr
|
|
lda #>start
|
|
sta ptr+1
|
|
|
|
ldy #0
|
|
sty flag
|
|
lda (ptr),y
|
|
sta sample
|
|
|
|
This section simply initializes the various memory locations that the
|
|
player uses - sets ptr/ptr+1 to point to the beginning of the digi,
|
|
loads the first sample, and clears the flag that handles the
|
|
alternating between the lower and upper nybble of the packed samples.
|
|
|
|
;setup CIA #2
|
|
lda #<freq
|
|
sta $dd04
|
|
lda #>freq
|
|
sta $dd05
|
|
|
|
Sets Timer A on CIA #2 to freq.
|
|
|
|
lda #%10000001
|
|
sta $dd0d
|
|
|
|
Enables Timer A interrupts on CIA #2.
|
|
|
|
lda #%00010001
|
|
sta $dd0e
|
|
|
|
Sets Timer A to run in continuous mode. As soon as Timer A counts
|
|
down to zero, it will automatically be reloaded to the last writes to
|
|
$dd04/$dd05 and begin counting down again.
|
|
|
|
endless jmp endless
|
|
|
|
For this example, we just put the computer in an endless loop.
|
|
|
|
nmi
|
|
pha
|
|
txa
|
|
pha
|
|
tya
|
|
pha
|
|
|
|
;play 4-bit sample
|
|
lda sample
|
|
and #15
|
|
sta $d418
|
|
|
|
We play the sample while all the code is still linear - before any
|
|
branches have occurred. This is to minimize the distorting effects I
|
|
mentioned earlier. The AND #15 is used so we don't inadvertently
|
|
enable the filter bits in $d418 with the high nybble packed into
|
|
sample.
|
|
|
|
;clear NMI source
|
|
lda $dd0d
|
|
|
|
By reading $dd0d, we are acknowledging the source of the interrupt,
|
|
and the CIA will now generate another interrupt next time Timer A
|
|
counts down to zero.
|
|
|
|
;just something to look at
|
|
inc $d020
|
|
|
|
;every other NMI do 1) or 2):
|
|
lda flag
|
|
bne lower
|
|
|
|
Now we deal with "unpacking" the samples.
|
|
|
|
;1) shift upper nybble down
|
|
upper lda sample
|
|
lsr a
|
|
lsr a
|
|
lsr a
|
|
lsr a
|
|
sta sample
|
|
jmp exit
|
|
|
|
When flag is set to zero, we shift the high nybble of sample down to
|
|
the low nybble so it's ready to be played next NMI.
|
|
|
|
;2) get a new packed sample
|
|
; then point to next
|
|
lower ldy #0
|
|
lda (ptr),y
|
|
sta sample
|
|
inc ptr
|
|
bne checkend
|
|
inc ptr+1
|
|
|
|
When flag is set to one, we load a new packed sample into sample, and
|
|
point ptr at the next packed sample.
|
|
|
|
;if end of sample, point to
|
|
;beginning again
|
|
checkend lda ptr
|
|
cmp #<end
|
|
bne exit
|
|
lda ptr+1
|
|
cmp #>end
|
|
bne exit
|
|
|
|
lda #<start
|
|
sta ptr
|
|
lda #>start
|
|
sta ptr+1
|
|
|
|
Simply check for the end of the digi, and if we've reached it, loop
|
|
back to the beginning of the digi.
|
|
|
|
;toggle flag and exit NMI
|
|
exit lda flag
|
|
eor #1
|
|
sta flag
|
|
|
|
pla
|
|
tay
|
|
pla
|
|
tax
|
|
pla
|
|
rti
|
|
|
|
;sample's lower nybble holds
|
|
;the 4-bit sample to played
|
|
;next NMI - the upper nybble
|
|
;holds the next nybble to be
|
|
;played on "odd" NMIs, and is
|
|
;undefined on "even" NMIs.
|
|
sample .byte 0
|
|
|
|
;flag simply toggles between 0
|
|
;and 1 - used to decide whether
|
|
;to play upper or lower nybble
|
|
flag .byte 0
|
|
|
|
|
|
Improving D418 Digis
|
|
--------------------
|
|
|
|
D418 digis tend to generate a lot of noise, because, of course, the 4-bit
|
|
sample resolution. Over the years people have come up with numerous tricks to
|
|
improve the sound of a d418 digi; here are some that we know of and have tried.
|
|
|
|
The first, and most obvious, thing to do is to use the low-pass filter, since
|
|
a lot of the noise is at higher frequencies. Unfortunately this won't work,
|
|
since the filters occur in SID before the volume amplifier -- all the filters
|
|
can do is change the DC offset that makes the digi possible. This trick
|
|
will work for methods that use SID voices, however (such as Pulse Width
|
|
Modulation, discussed in the next section).
|
|
|
|
Another trick is to "dither" the sound, as discussed in C=Hacking #11. The
|
|
idea here is to generate an intermediate "average" value by toggling between
|
|
two values. For example, if d418 is set to '8' half of the time, and '9' the
|
|
other half, its 'average' value will be 8.5. So this is somewhat like adding
|
|
an extra bit of resolution. In principle, you can extend this further: if it
|
|
is '8' one-third of the time and '9' for the remaining two-thirds, the average
|
|
value will be 8.66. And so on.
|
|
|
|
Now, we aren't _really_ increasing the sample resolution here, but are instead
|
|
increasing the sample playback rate -- we're playing two samples ('8' and '9'
|
|
for example) where before we played just one. Don't get too carried away
|
|
thinking about "average" voltage levels (after all, there is an average
|
|
voltage for the entire digi but that's not what you hear!) -- what's important
|
|
is how well the sampled signal represents the original signal. If the
|
|
original signal is rising from 8 to 9 during the sample interval, this type
|
|
of trick will work well.
|
|
|
|
Which leads us to another trick: interpolation. This is really a compression
|
|
trick, more than a 'resolution' trick. Let's say that one sample value is 5,
|
|
and the next value is 9. It might be reasonable to expect an 'intermediate'
|
|
value of 7, to play right after the 5. Once again, the idea is to increase
|
|
the playback rate to better-represent the original signal. This type of trick
|
|
increases the playback rate without increasing the amount of data -- and as
|
|
always, your mileage may vary. Many modern soundcards and CD-players use
|
|
interpolation.
|
|
|
|
Another curious trick is to add noise to the signal -- that is, the 4-bit
|
|
sample corresponds to the original signal plus noise. Sometimes, by adding
|
|
noise to the signal playback the noise can actually cancel! The 'dithering'
|
|
trick above can be viewed in this way.
|
|
|
|
|
|
Boosting 8580 Digis
|
|
-------------------
|
|
|
|
As most people know, there are 'old' SIDs (6581) and 'new' SIDs (8580), and
|
|
$d418 digis do not work right on 8580 SIDs, (such as in the 128D, most 128s,
|
|
and the 64C) for the reasons discussed earlier -- the 8580 does not have a
|
|
residual voltage leading into the amplitude modulator.
|
|
|
|
The software fix for this is pretty simple: have SID generate a signal, and
|
|
hence a voltage, for the volume register to modify. You can actually use
|
|
pretty much any waveform to do this, but a pulse is the simplest, since a
|
|
pulse wave just toggles between two voltage levels. Moreover, page 463 of
|
|
the PRG says, "The TEST bit, when set to a one, resets and locks Oscillator 1
|
|
at zero until the TEST bit is cleared. The Noise waveform output of
|
|
Oscillator 1 is also reset and the Pulse waveform output is held at a DC
|
|
level." So it's not really necessary to worry about the frequency or pulse
|
|
width, by using the test bit.
|
|
|
|
BUT -- it is very important to set the sustain level to $f. The ASDR envelope
|
|
generators generate the voltage. A sustain level of 0 gives no improvement.
|
|
|
|
So, to 'boost' a digi on a later-model SID, you can just turn on a pulse with
|
|
the test bit set:
|
|
|
|
LDA #$FF
|
|
STA $D406
|
|
LDA #$49
|
|
STA $D404
|
|
|
|
Setting more voices gives the digi a substantial extra boost:
|
|
|
|
LDA #$FF
|
|
STA $D406
|
|
STA $D406+7
|
|
STA $D406+14
|
|
LDA #$49
|
|
STA $D404
|
|
STA $D404+7
|
|
STA $D404+14
|
|
|
|
The moral is: if you're writing a digi routine, and want it to work on all
|
|
computers, be sure to boost the digi.
|
|
|
|
And for completeness, using more channels is a commonly used trick to enhance
|
|
digi resolution on the Plus/4. The TED digi resolution (the volume register)
|
|
is 3 bits. Fortunately, all channel on/off bits + the volume level are in the
|
|
same register ($ff11). If one source is on, the output DC is about half of the
|
|
level when both are turned on. This trick can be extended further to results
|
|
in a 'semi 4-bit' or 5-bit digi table (the dynamic range is enhanced, but
|
|
there are larger steps at the table end than at the start). This trick could
|
|
also be used in SID if the sound sources were accurately preset, but runs into
|
|
problems due to the non-matching SID-versions and having the control bits in
|
|
multiple registers.
|
|
|
|
|
|
SID Type Auto-Detect
|
|
--------------------
|
|
|
|
The following routine will detect what type of SID is in use. I've
|
|
tested it on a fair cross-section of my collection of computers - my
|
|
NTSC 128D, two 64Cs, two "breadbox" C-64s, and my PAL breadbox 64. In
|
|
all cases the code performed 100% accurately - but still, there may be
|
|
cases where it fails. I'd be interested to know if anyone finds any
|
|
faults in the routine, so I can improve it!
|
|
|
|
How does the routine work? I was told that the old SID (6581) and the
|
|
new SID (8580) behave differently when set to play combined
|
|
waveforms. I coded a fairly simple routine to use the REU to sample
|
|
$d41b (the upper 8 bits of Oscillator 3's waveform output) for a full
|
|
64k bank. Then I experimented with various frequencies and
|
|
combinations of waveforms on Oscillator 3 until I found consistently
|
|
different results with the two different SIDs.
|
|
|
|
When I combined the triangle and sawtooth waveforms and then sampled
|
|
$d41b I found that most of the time the oscillator was just putting
|
|
out zeros, with occasional bursts of numbers. These "bursts" were
|
|
consistently near $ff on the 8580, while the 6581 was always well
|
|
below $80 - often $3f was the highest it would get.
|
|
|
|
So, the detection code ended up being quite simple - I'll explain each
|
|
block of code:
|
|
|
|
|
|
*= $4000
|
|
|
|
start sei
|
|
lda #11
|
|
sta $d011
|
|
|
|
Disable bad-lines (by blanking the screen). This prevents badlines
|
|
from interfering with the detection process.
|
|
|
|
;sid setup here!
|
|
lda #$20
|
|
sta $d40e
|
|
sta $d40f
|
|
|
|
Set Oscillator 3's Frequency Control to $2020. I just randomly chose
|
|
this value when experimenting, and it worked, so I kept it. The trick
|
|
here is to set a value fast enough that the oscillator will make a
|
|
number of cycles (so we can get a good sample of the values coming
|
|
out) but not so fast that it might miss any of the "bursts" I was
|
|
mentioning earlier.
|
|
|
|
lda #%00110001
|
|
sta $d412
|
|
|
|
Combine the triangle and sawtooth waveforms and start the ADSR cycle.
|
|
|
|
ldx #0
|
|
stx high
|
|
|
|
loop lda $d41b
|
|
cmp high
|
|
bcc ahead
|
|
sta high
|
|
ahead dex
|
|
bne loop
|
|
|
|
This loop takes 256 samples of Oscillator 3's output, saving the
|
|
highest value in location high.
|
|
|
|
lda #%00110000
|
|
sta $d412
|
|
|
|
Stop Oscillator 3.
|
|
|
|
cli
|
|
lda #27
|
|
sta $d011
|
|
|
|
Turn the screen back on.
|
|
|
|
lda high
|
|
rts
|
|
|
|
high .byte 0
|
|
|
|
Return from the routine with the highest value sampled from Oscillator
|
|
3 in the accumulator. This allows you to branch based on the high
|
|
bit:
|
|
|
|
bmi SID8580
|
|
bpl SID6581
|
|
|
|
Voila!
|
|
|
|
|
|
======================
|
|
Pulse Width Modulation
|
|
======================
|
|
|
|
The primary limitation of using the volume register is, of course,
|
|
that it is only 4-bits. Pulse width modulation (PWM) allows us to get
|
|
around that limitation.
|
|
|
|
In general, there are lots of ways of transmitting information.
|
|
If you've ever used a radio you've encountered both amplitude modulation,
|
|
where the signal is encoded as the amplitude of some carrier wave, and
|
|
frequency modulation, where the signal is encoded by changing the frequency
|
|
of the carrier wave. In both cases, the idea is to strip out the encoded
|
|
information and throw away the carrier.
|
|
Yet another possibility is pulse width modulation: use a pulse
|
|
wave at some carrier frequency, and modulate the pulse width. Pulse width
|
|
modulation has several nice properties for transmitting signals; we can
|
|
take advantage of it to play digis.
|
|
|
|
Pulse waves, of course, take on only two possible values: zero and
|
|
one (low and high, etc.). Over a single period, a pulse wave will in general
|
|
be low for some amount of time and then high for some amount of time.
|
|
The _duty cycle_ of a pulse wave is the amount of time it spends in the high
|
|
state compared to the total period. For example, a square wave, which is
|
|
low exactly half the time and high the other half, has a duty cycle of 50%:
|
|
|
|
______ ______
|
|
| | | |
|
|
| | | |
|
|
_____| |____| |____ ...
|
|
|
|
Remember that, regarding SID, a signal like the above is simply a voltage
|
|
level. What is the _average_ voltage over a single period? Since a square
|
|
wave is zero half the time and one the other half the average value is
|
|
just 1/2. If instead the pulse had a duty cycle of 75%, it would be low
|
|
for 1/4 the cycle and high for 3/4, giving an average value of 3/4.
|
|
|
|
So the _average_ value of a single pulse is simply the duty cycle. So if
|
|
we change the duty cycle for each pulse we can essentially generate a
|
|
series of average voltage values -- and since a digi is nothing more than
|
|
a series of average signal values, we can use PWM to play a digi.
|
|
|
|
To make this more precise, let's say we had a digi sampled at 1KHz -- one
|
|
thousand samples per second. Since each sample value will be approximated
|
|
by a pulse, we need one thousand pulses per second. The duty cycle of
|
|
the first pulse will be the first sample value, the duty cycle of the
|
|
second pulse will be the second sample value, and so on. Note that
|
|
the sample rate is the carrier frequency -- the frequency of the modulated
|
|
pulse train, 1KHz in this case.
|
|
|
|
(Actually, to be more accurate, we need _at least_ 1000 pulses per second --
|
|
for example, we could use 2000 pulses per second, and represent each sample
|
|
value using two pulses. So the more correct statement is that the pulse
|
|
carrier frequency is the maximum sample playback frequency.).
|
|
|
|
The advantage for playing C64 digis is that we have much more resolution
|
|
for the pulse width, and probably not in the way you think! Because you
|
|
are probably thinking that SID has this nice 12-bit pulse width that
|
|
we can use here. The problem is that the absolute highest frequency SID
|
|
can produce, using the frequency registers, is about 4KHz, which would
|
|
be the maximum playback rate.
|
|
|
|
There's still another catch -- the carrier wave is still there! Imagine
|
|
trying to encode a signal that was constant, say 1/2 everywhere. To
|
|
generate a "digi" value of 1/2, you'd use a square wave, half down and
|
|
half up. So while the _average_ value of each pulse would be 1/2, the
|
|
actual signal would be a square wave at the carrier frequency (look at
|
|
the little picture above if you don't see it -- its average value is 1/2).
|
|
|
|
Trying to modulate a 4KHz carrier wave results in a piercing 4KHz tone,
|
|
and a _maximum_ sample rate of 4KHz (and this assumes that you can sync your
|
|
code up exactly with SID). So that's pretty worthless for digis.
|
|
|
|
- BUT -
|
|
|
|
What if we could change the voltage level manually? Let's say some
|
|
hypothetical machine language program toggled the voltage level
|
|
on each machine cycle -- the result would be a square wave of
|
|
frequency 0.5 _mega_ hertz. Okay, let's say it changed the voltage
|
|
level every 10 machine cycles -- the result would be a carrier
|
|
frequency of around 50 KHz. The point here is that a machine language
|
|
program can generate its own pulse waveform, and do so at much higher
|
|
frequencies than SID can produce.
|
|
|
|
Toggling the voltage levels turns out to be very simple. As was
|
|
described earlier, the way to "boost" digis on later SIDs is to use
|
|
a pulse waveform at frequency zero. Depending on the value of the
|
|
pulse width register, SID will set the output voltage to either high
|
|
or low. So all a program has to do is set up a pulse waveform at zero
|
|
frequency and use the pulse width registers to toggle the voltage --
|
|
set $d403 to either $00 or $ff to toggle low/high. (You could also use
|
|
$d418 to toggle low/hi, but this method should produce more uniform
|
|
results, and unlike $d418 can be filtered).
|
|
|
|
So now we're cooking -- we've got a program that can generate a pulse
|
|
train. The next step is to change the width of each pulse to represent
|
|
the sample values in our digi. Remember that the duty cycle -- the
|
|
percentage of time the pulse spends high -- is the average value for that
|
|
pulse. But also remember that each digi sample represets an average
|
|
value over the sample period. If the pulse period is equal to the sample
|
|
period, then _the duty cycle is exactly the sample value_!
|
|
|
|
Example: let's say that we have an 8-bit sampled digi, so that values go
|
|
from 0-255, and our program generates pulses with a period of 256 "ticks".
|
|
Now pick a sample value, say 56. All the program has to do is hold the
|
|
pulse high for 56 "ticks", and low for the remaining 255-56 = 199 "ticks",
|
|
and it will have the correct average value: 56/256. So a program to play
|
|
8-bit samples might look like
|
|
|
|
1 - Load .X with next sample value
|
|
2 - Load .Y with 256-.X
|
|
3 - Set pulse high
|
|
4 - Loop for .X iterations (each loop iteration is one "tick")
|
|
5 - Set pulse low
|
|
6 - Loop for .Y iterations
|
|
7 - Loop back to step 1
|
|
|
|
Let's say that each "tick" takes m cycles, and the sample size is 2^n, so
|
|
that there are 2^n ticks per sample. A stock machine runs at around
|
|
10^6 cycles/second, so...
|
|
|
|
(10^6 cycles/second) / (2^n ticks/sample * m cycles/tick)
|
|
= 10^6 cycles/second / (m * 2^n cycles/sample)
|
|
= 10^6 / (m * 2^n) samples/second
|
|
|
|
So, for example, let's say we had n=6-bit samples -- 2^6 = 64 -- and could
|
|
generate pulses with a resolution of one machine cycle -- m=1. Then
|
|
we could play that 6-bit sample at 10^6/64 = 15.6KHz. That is _really
|
|
very good_! In principle -- possibly using the CIA timers, possibly using
|
|
fixed delay loops, possibly using a massively unrolled loop -- this can
|
|
be done on a stock machine. (I did try using the CIA timers, but the
|
|
number of cycles to set up the timers was too big, and made it sound poor;
|
|
I've included the code below though.)
|
|
|
|
At this point it becomes a numbers game. As we increase the sample size
|
|
(increase m or n above), we _decrease_ the sampling rate -- if, in the
|
|
above example, we instead use 8-bit samples, the sampling frequency drops
|
|
by a factor of four to around 4 KHz. So there's a tradeoff between
|
|
resolution and sampling frequency.
|
|
|
|
AND... we still have this issue of the carrier frequency. You should be
|
|
able to convince yourself that the sampling frequency above is exactly
|
|
the carrier frequency. So with the 8-bit resolution example there
|
|
would be an awful 4KHz tone running through the playback. There are
|
|
only two ways to beat the carrier frequency: push it high enough that
|
|
you no longer hear it, or else push it high enough that you can use the
|
|
filters to dampen it down.
|
|
|
|
How high is high enough? You can judge for yourself, but 15 KHz is
|
|
pretty tough to hear, unless you have good ears and the volume is really
|
|
loud -- so 6-bit samples are within reach on a stock machine.
|
|
|
|
But add a SuperCPU into the picture, and the numbers get _really_ nice.
|
|
Everyone knows that a SCPU can interact with the C64 at 1MHz, and
|
|
hence generate pulses with 1MHz resolution, using code like
|
|
|
|
lda #$ff
|
|
sta $d403 ;Set level high
|
|
:loop lda $d011 ;wait for C64 cycle
|
|
dex
|
|
bne :loop
|
|
|
|
where .X contains the sample value. But what happens if we try to move
|
|
beyond that 1MHz? What if we put some NOPs into the above delay loop,
|
|
in place of the lda $d011? Well, in principle it means that the duty
|
|
cycles won't always be right, which corresponds to some sampling error.
|
|
In practice, however, it works _really well_! Consider what happens when
|
|
the above code is changed to:
|
|
|
|
:loop
|
|
nop
|
|
nop
|
|
dex
|
|
bne :loop
|
|
|
|
The earlier formula still applies, but now using 20MHz cycles:
|
|
|
|
20 * 10^6 / (m * 2^n) samples/second
|
|
|
|
In this example each loop iteration -- each "tick" -- is nine 20MHz cycles,
|
|
giving a playback rate of approximately 17Khz for 7-bit samples. Which
|
|
is TOTALLY COOL!
|
|
|
|
And it can even be pushed to 8-bit samples (although I personally don't think
|
|
they sound any better, at least with the code I've tried; maybe the code can
|
|
be improved). Using loops like
|
|
|
|
:loop
|
|
dex
|
|
beq :done
|
|
dex
|
|
beq :done
|
|
...
|
|
dex
|
|
bne :loop
|
|
:done
|
|
|
|
it is possible to "fine-tune" the loop tick to somewhere between 4-5 cycles,
|
|
giving a playback rate between 15KHz and 19KHz, for an 8-bit sample. Pretty
|
|
cool. The code is also a little more involved (with 7-bit samples we can
|
|
use BMI for the loop branches; not so with 8-bits). But it really is
|
|
possible to play 8-bit samples at 19KHz on a C64 (plus SuperCPU).
|
|
|
|
Using two voices
|
|
----------------
|
|
|
|
You may be thinking, Hey, we've got three pulse waves to work with, can
|
|
we improve the performance by using multiple pulses?
|
|
|
|
Let's say we have two pulses, P1 and P2, with the same period. When both
|
|
are activated, the pulses simply add together -- that is, the total voltage
|
|
is just the sum of the individual voltages, and therefore the _average_
|
|
voltage is the sum of the individual pulse averages:
|
|
|
|
avg voltage = D1 + D2
|
|
|
|
where D1 and D2 are the duty cycles of pulses P1 and P2. In the simplest
|
|
case, this gives us an extra bit of resolution -- if D1 and D2 are both
|
|
7-bit values, say, then D1+D2 is an 8-bit value.
|
|
|
|
-BUT-
|
|
|
|
Consider, for a moment, what would happen if we were to change the amplitude
|
|
of the second pulse -- that is, let's say the maximum voltage it took on
|
|
was 1/16 of the maximum voltage of the first pulse. The average voltage
|
|
would then be
|
|
|
|
avg = D1 + D2/16
|
|
|
|
This then gives us _four_ extra bits of resolution, with each bit to the
|
|
_right_ of the decimal place. For example, if D1 and D2 are 4-bit numbers,
|
|
with D1=xxxx and D2=yyyy, then the avg will be a number like xxxx.yyyy
|
|
(four bits to the left of the decimal place and four to the right).
|
|
|
|
Of course, we can change the pulse amplitude by changing the sustain
|
|
setting, so in principle this gives a very easy and efficient way of
|
|
playing high-resolution digis. In practice, I have not been able to
|
|
make it work very well. I used a sustain setting of 1 and split an
|
|
8-bit sample into two 4-bit pulses; I believe the result sounds better
|
|
than 4-bits, but certainly doesn't sound anywhere near 8-bits. My
|
|
suspicion is that it is because the second pulse voltage is not really
|
|
1/16 of the first pulse, which corresponds once again to adding noise
|
|
to the sample value.
|
|
|
|
To find out, we can just measure the output at different sustain levels.
|
|
The following table gives the voltage output for voice 1 using a pulse
|
|
waveform at zero frequency and volume 15:
|
|
|
|
Pulse Width Diff
|
|
SU 000 fff 000 fff
|
|
|
|
0f 6.34 5.29 .08 .07
|
|
0e 6.26 5.36 .02 .01
|
|
0d 6.24 5.37 .06 .05
|
|
0c 6.18 5.42 .03 .02
|
|
0b 6.15 5.44 .05 .03
|
|
0a 6.10 5.47 .03 .02
|
|
09 6.07 5.49 .04 .02
|
|
08 6.03 5.51 .03 .02
|
|
07 6.00 5.53 .05 .03
|
|
06 5.95 5.56 .03 .02
|
|
05 5.92 5.58 .05 .02
|
|
04 5.87 5.60 .04 .03
|
|
03 5.83 5.63 .04 .02
|
|
02 5.79 5.65 .06 .02
|
|
01 5.75 5.67 .05 .02
|
|
00 5.70 5.69
|
|
|
|
Voice 2 is identical within a few hundredths of a volt. If this test is
|
|
repeated using voices 1 and 2 simultaneously, the result is:
|
|
|
|
Pulse Width
|
|
SU 000 fff
|
|
0f 7.30 5.25
|
|
0e 7.12 5.36
|
|
0d 7.09 5.37 (!)
|
|
0c 6.95 5.46
|
|
0b 6.88 5.49
|
|
0a 6.78 5.54
|
|
09 6.72 5.58
|
|
08 6.62 5.62
|
|
07 6.58 5.65
|
|
06 6.47 5.70
|
|
05 6.40 5.73
|
|
04 6.31 5.78
|
|
03 6.22 5.82
|
|
02 6.13 5.87
|
|
01 6.07 5.90
|
|
00 5.97 5.95
|
|
|
|
Note the weird step at $0d -- the response is definitely not linear!
|
|
|
|
Now, to summarize, when using one voice, the "positive" amplitude (about the
|
|
mean 5.70V) is .64V and the "negative" amplitude is .41V, giving a spread of
|
|
1.05V. With two voices together, the amplitudes are 1.33V, 0.72V, and 2.05V
|
|
respectively. If the two signals were simply added together, the numbers
|
|
should be 1.28V, 0.82V, and 2.1V.
|
|
|
|
What we originally wanted was a signal like
|
|
|
|
D1 + D2/16
|
|
|
|
that is, another pulse that is 1/16 the value of the 'full' pulse. 1/16 of
|
|
the positive amplitude is .64V/16 = .04V, and 1/16 of the negative amplitude
|
|
is .41V/16 = .026V. A setting of sustain level 1, on the other hand, gives
|
|
voltage offsets of 0.05 and 0.02, giving approximately
|
|
|
|
.64V / .05V = D1 / 12.8
|
|
.41V / .02V = D1 / 20.5
|
|
|
|
So, in summary, whereas I wanted D1 + D2/16, I was actually getting something
|
|
that varied from D2/12.8 to D2/20.5, even if the two voices summed together
|
|
correctly.
|
|
|
|
There may still be a way to make all this work right, which would be great,
|
|
but I'm tired :). The code from my attempts is below.
|
|
|
|
I also could not get two 7-bit pulses to sound like an 8-bit pulse. I took
|
|
an 8-bit pulse and divided it in half, assiging each half to a pulse
|
|
(and giving the extra bit to pulse 2, if an extra bit was present).
|
|
I suspect that another issue is that it is impossible to update both
|
|
pulses simultaneously, meaning some delay between pulses, which translates
|
|
to adding -- surprise! -- noise to the signal. Perhaps it would be
|
|
more effective at lower resolutions, however.
|
|
|
|
If someone has some success using these techniques I'd be interested in
|
|
hearing it.
|
|
|
|
SID lockups
|
|
-----------
|
|
|
|
Blindly applying these PWM algorithms has a way of locking up SID -- like,
|
|
locking him up hard. To be honest, I don't have a good explanation for why
|
|
this happens, and I haven't yet found a good method of prevention -- toggling
|
|
the test bit, playing a real sound for a short time, toggling the gate bit,
|
|
and so on, just don't seem to "initialize" SID reliably enough. Sometimes the
|
|
code works, and sometimes it doesn't -- it's the same code both times. Often
|
|
resetting the machine will make things work; I'm not sure what hardware resets
|
|
take place within SID, but the kernal certainly zeros him out so that's a
|
|
possibility. The other observation is that playing a tune seems to 'clear
|
|
out' whatever is blocking SID. So there _must_ be some kind of software
|
|
solution to the problem.
|
|
|
|
In the example code pressing RESTORE restarts the code, which will usually
|
|
clear the 'blockage' after a tap or two, if it happens.
|
|
|
|
If anyone has some thoughts on this issue (or even better, an explanation
|
|
of what is going on!) I'd love to hear them.
|
|
|
|
========
|
|
The Code
|
|
========
|
|
|
|
And that about sums stuff up, we think. All that remains is a zipfile
|
|
containing code examples of the things we've discussed. The archive
|
|
contents are:
|
|
|
|
digi-$1200.o pwm-2cia.d.s pwm-jmj pwm8.d.s
|
|
digi-$1200.s pwm-cia.f.s pwm.d.s
|
|
pwm-$1200.s pwm-cia.g.s pwm2.c.s
|
|
|
|
The main example program is "pwm-jmj", for the SuperCPU. This file contains
|
|
code at $1000 and a sample at $1200 (the sample is a sample I made on my
|
|
Amiga years and years ago, of Jean Michel-Jarre; it's really not very clean,
|
|
but it still sounds pretty nifty). So, to run it just load and SYS 4096.
|
|
|
|
You _need_ a SCPU to run it; it plays the sample at 7-bits and around 16KHz,
|
|
using pulse width modulation (pwm-$1200.s is the source code). If you want
|
|
to compare it to a 4-bit $d418 digi, just load "digi-$1200.o",8,1 and
|
|
sys 4096. The other files are different code experiments -- using two
|
|
pulses, using the CIAs, 8-bit samples, and so on (all written in Sirius,
|
|
of course) and may provide ideas for anyone wanting to experiment further.
|
|
|
|
If you want to try your own sample with the code, I suggest converting it
|
|
to RAW format. RAW format is just the digi, with no headers, and uses
|
|
signed 8-bit numbers. It's what I converted the sample to, and what the
|
|
code is designed to play!
|
|
|
|
Okay, enough talk -- go listen to that cool 7-bit 16KHz JMJ digi!
|
|
|
|
begin 644 digibin.zip
|
|
M4$L#! H ! -IQQIT41>R90 &L , 9&EG:2TD,3(P,"YO ""D
|
|
M E!(7RH)A?8-+,0OU;]&%:@Y;&1!(H!&%R0^:(3!8B, U!H%"%E XD,#$H,T
|
|
M(D -3ZL(T%)LA'E*7Q) 2G(JX4A-E(=6'95!TV=.'[1TYO:5VI<,#+1P+Z$E
|
|
M@- ()A8F 0%02P,$"@ 8 _&C'&I6%<YMA 0 <@( P !D:6=I+20Q
|
|
M,C P+G,,"@$3 @,$4TH%:OO[6P8 1;W]_>W 0)4V5 E2.))CR8%610KU*)2
|
|
MDS8MZI2J3I#0KS=O@,W3F2L'&1/F2YDR7\*$ 7U#T$^E'@5),B:,YAM"?9K4
|
|
M*570/4&3-!JTX=2@3:$R+<+,,65L<U*G2:DV!,V4:%"0(WELNR'HJ52#@E@G
|
|
M:_NP8B%6[\6*AS:X>I)&C8+?:3PI4ZI%I4[A>J(T8];H*JJV0840_JCV!M6D
|
|
M)8Y.*"V4/(92I8)AI%J:,<'OA%Z5Z=2B((4^?3J5*LBG3LB:!NFG%DVB;I)$
|
|
M8<:H?%"G1!0^H>:&3)\^A5+N+1I7[-H0=-&G4L['(?'/=*H48@2_LWG0I45!
|
|
MT@0I-"G5*5;=@.*QP6.Q/V;1ACJO!DU*!==E(]&B6!N"%NJT*(ALJ#^ITZ$@
|
|
M6H.U!GY1[<H8$+^8=FA3*)FW":,M,!Y]*L7ASC%A8#P.>BB3GG<E^57L 5!+
|
|
M P0* 0 !@#Y:,<::WXHL L" !X P "P '!W;2TD,3(P,"YS3@<(K0/]
|
|
M'0,=" 5-!@@M!P,$!0@&" <6" 8%*!T$!00% Q4& P<&! 4D"!0%%@@&!1@-
|
|
M&/V-_OY^!/[^"!X(+@@.)P8>!@X'!@<>!GX$_NX/ 0(# @,"! ,T!AL%N_S\
|
|
M/ L P(#!P47!O?W]V>1(Z?_]!?HSN]WXOK3[=P9L\DO)BKVPS8Z;/BWWDB5
|
|
M2J1[04MZZ*3=OOC3M7L87T)[VK=T0\)8?&MJN$"">OVT9$K+G&C];X8N_ZKK
|
|
MM?_WPQZS37!-M;#\=LGUT\'LO6M%MWO<V=],Q>6%4KTI=;D[[<F^^&'/YZV_
|
|
M:;/+JQ;3;MI'EN"!%59N2&N*JP3K"@SAYY8]HMNMT16'-Q=66&*5P%GA3%=S
|
|
MO<>4DBA-0I2("P54CHCJM+4NQR1,[7G#*!.L%YNS2K;\;-I4BW=%ZHN47QFC
|
|
M]J>WF7%^3W!I'@?4VB=(/:]!_.8,G?XIMRZ'"2SJ0=AO=WX\1>7;KB['+HGG
|
|
MI;[2SK:_R2YQWDXM<#93E^>!Q%DBM[\IG6!><INX^\\5KLO1;([[H%_2SB?A
|
|
MA"MUF>VNL3[N*&ORM#;CT'_$%$^&HX_GL?Y)&BH/5M"OH".>+_43^K07A_$X
|
|
M_%9P3A7U: B<9J4AKS(-)IUS/=Q'O-X(/ K^RJ7S:.B4TO72>=S,DX_C" P=
|
|
MRJ;?SR;JF57<EG+[\5@2(3>A6-7#7%!+ P0* 0 !@#\:,<:]D)"*[$" #P
|
|
M!@ # '!W;2TR8VEA+F0N<TD'"*X#_AX#'@@&3@<&& X'% 85%@<&!P8%
|
|
M!Q@>! 44 P0&!0,'" 0%) @4 P87!@4^"/YN__^?!?__.#\8#P@''P8?%Q\'
|
|
M?P7_[PXB(Q(#)0P%!PP$!NS]W04+$003! 8#1F<&]_>G4:-&^5%>H"?NO!O3
|
|
MW/KD;K?C7W3(,!_V0@<4\%LB)T5>%\X-E3_5"W?KZK3=BZUN]7;^OL "":)Z
|
|
M0UL#?V)PA;-?T\_9PCP%_92_VBWDY^8I?[I;*?K/M'7<2UW6=?V_:4^W,\%F
|
|
M4H?Y5W.NZ2_F)=AN/^\IS_F)I93RFB[2TI0W7EW__W1G_TP_SB\$[,]4>WZI
|
|
M8VE--"VI:T*Z6-_J[A>N/=-]BW&7/G(NN?IP^GUKW5D$92#O)=)W9=4\K9>R
|
|
M!XL$7)<Y70P?6^UWNC?-U264A+)[CZI33E(M5O&?FT\'6R*X3'&+T\Y:@V$-
|
|
M%UA+D*SAZ1L-4R:549+;,W $+BWI8G1L7UV37B)0WK*8]Y+P+887U!RAZ=NP
|
|
M/,G#<#G.K.&!.\W9OFT;VY[)FS948/*]&H5A@L<1Z;%Q:^)"RE-*%S>$]?H%
|
|
M_J8VMMM,]ML]]Z\!6)6LM:/<L[KW%6H6U!9J((KT[3RF+!8%RFXR^F8A+OT8
|
|
M(VT1?V:\$W#6^Y)_YG2B@ZF6><HU;/Y0O=#=%S]9>WC2']ON?LWE2Y%*NNDB
|
|
MY=?[=;GS_[[51EZ15:))7B$I[_U/X/MT /'"#>JYI'[Z&2DSEKW,U)]T,64A
|
|
M,*S:,0ANR^DB3&C^Q.79] ?XQ1=E&GP6HE;O"2LMY?>C&I;SCB*'&&2#L[6O
|
|
M:HSQ>)O;C16>N?K.X^GB'H(WAH-=\6'GLS6<(GF$NO^CN84Q,(V%@$>5/%[P
|
|
M'(;0#,\9@Z*@U,7L!RUNID1R,TU:(A;@M"C\][I?%I,54$L#! H ! & /=H
|
|
MQQKFO<<O:P( &D% + <'=M+6-I82YF+G-'![T#_1T#'0@&318-" T'
|
|
M%!8%%@<&%P4H'00%! 4#!!4#!P@$!00#! @%% 4F!3T(_3W^_LX%_OY(+A@.
|
|
M" <>!AX7'@=^!/[N$0$# @,"$P(')28,!@3\#/V]!@L P<3!0,G%O?W]S>1
|
|
M(Z?\E!?$*9YWH[N;._G;-^%?5*BP'U;#!!+H6R(E1%H7SH8NG/;%WEQ[=O^R
|
|
M[6U?4[@OR,"";M^X<\#'AU0^S>N&.5.>I^">\FVS(<_/4^'TMU7X/[USQ9>Z
|
|
MK.OZ?[=/WZ1#2*>.\V_G?#=<3%WC5MT.<S,50(^O)E?KBLTE^94NS___]*?Y
|
|
MZ:$TOY#!KRNYV][FQ>XSW;?\=-O,+[547:L?V:H\K9?2HV_ OFG69:XKX;)M
|
|
M\T[_IKF]!*>X1'>75"DQ035*W30A:MC+FG'4V:X2O"NT_']^/KTZ$U1X&9U8
|
|
MX3)K@F&%IP\Q<)42-2E@@IR=QQ'5\:A<7>I*5'9H+U]V\"B#9W,O<1^O((2Z
|
|
M&W:,U7PYQJSP"!2O.MX)E([H&PUK&:P098JO*PV>>^X%]::=[1L:]]W,P^LD
|
|
M5AE'MUAT&L[7\GC8<REY ?-8MDGHS2#9,Y2B)5\B_-_O-M-/'!XGU16Z2^$4
|
|
M"Q3."E^Y:$'<[,,X+]4W9:9!QSRK1JNX4E>$SS4/96UA37^_.T2@%H^-,PZ5
|
|
M'SYF/DT:+>J4M(6='4JO3+'+L=/S;5TI[EUZM]]9KB[5(IE$+)+RGF!./STU
|
|
M?7^:4MW"Y?X^C+ K)4%<+'G*=SR.!JZ-<(N;>]+*=H-<;[ /RYAP[BL9^T'#
|
|
M9DA$-H-)$Z',8$;A'W1/I-(44$L#! H ! & /=HQQJHW6A*AP( .0% +
|
|
M <'=M+6-I82YG+G-&![T#_1T#'0@&30<�@-!Q06!387!"@=! 4$!0,$
|
|
M%0,'" 0%! ,%" 44!28%/0C]/?[^S@7^_D@N& X(!QX&'A<>!WX$_NX0 0,"
|
|
M P(3 C4'%@P5_ S]O08, ,' P8"%C<&]_?W)Y$C1_J17A"G=-Z-SM_<*=RN
|
|
M"?^B0H7]L!HFD$#?$C$QXKIP-G3QM"_VYMJSNY=M;_N:XGU!!A9T^X:= SXA
|
|
MI,II7KZ?,I5I#.XIWS8;\L(T%D]W6X7^TSM7>JG+NJ[_Y_?IFG0(Z=1A^NU4
|
|
MR/<74]>X5;?]U(Q%T!.J*=5ZMJFDO/+EZ?^?[C0_W9>G%[+UZ;:97FJYNB:8
|
|
M!J'7<^ZVMWFQ^XSW+<:N?F2NKO4\G+ZY.5U:!$H#N9?X[U*K\KA>2@\6=M"Z
|
|
M3/4<+MLV[W1OG-I+4(I+<G=9E9(258/UI@D1(&7-,"#YNTKPKH"5_PO3Z=19
|
|
M8N%E=&)%*JP)AA49/\HP)<XPG@CET^ W7%WJ.2K;MU?!-MT8;Q9S+W$?SX<1
|
|
M?.?['6,57XXP*S( >5YU=B*EXWV#8JV&%:Q,"?7<&#CG7E!OW-FN$3*_FZE_
|
|
M>8D4R-$MYNG8GZ\UU.^IG+* #<DV&;T9)'OZ<C2$KQ'^[W:;Z48_YR<8EXG4
|
|
M G,RILX>=BF>4O'+5,%:"!>UR&;.2_6-F;&',\VJT5(6J6?A<\V#2EM87TJN
|
|
MKDG>=SN/"'0CL7$&(0D?,YTF#>6JC9NVN+-]^56H<B5V?+ZMY]+>Y7>[G=5X
|
|
MB0MC%"57!#R?;GQ@VOXTI5JY7.GN W#CZF2(BB6/A3R)HX%KH]QB93YY9:M!
|
|
MJC?8!V1(./=E#/V@H=(GGDIOTD2HT#D8_+WN24JJ E!+ P0* ( !@ *:<<:
|
|
MTV5I'GL[ "4 !P '!W;2UJ;6H< $" P0%!QD'"0LN"QX?"P\+7PD+
|
|
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
|
|
MF%^O+B\>BE]1K%!WR%A@?:\F4IM_/XZ/5#U 800*4*Z/D]A!=E*C$^0(('+
|
|
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
|
|
M=42$U:'I%*C$TLG#:.]D+#9PASU6R6D&W,H%@4E2A1]>KQH\_GU*C^.LR
|
|
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
|
|
MTW&\BN5\28$HG:'*6JR/"BU;#=E56OJ^?3WR$1#M?D^JP�!T%F&KDQFS 2
|
|
MX_,:8.B&)G7NVQP_7HW![:@60F+)M79!9_;UI'CG.V"A)A ;<!E\P O,#\@)
|
|
MB7Z'(](-VC#.MT'=CF$HE; [&IT.=/I5YYNV<&_**"R7=TK;E'8$HU&RDHC&
|
|
M!/B@J I,AMJV](1-@*^(=/UXJD$X( UJ 8.X=$D<M56-:1*%(>P3^J-"0YQ,
|
|
MHFD.(AZ* =B!R8U%G& *"C>47VOKFZ'A_/D6NOGGFU:$612=:D!$#%JR?EAP
|
|
MOST 3PJ3\!C(2>,!:QWU# 7J!MX777E"=>_[:% :B/:Z[8DP2,(@) J1[4XO
|
|
M#_+^>8 $X1DL -$\@(.C"M,1&RP%^B\J6+52S7\ ?L*=]4%1+*]/@^7=<9HR
|
|
MC'_?44<@I/=Y3C6<_R6@\( 2#G40&C-0S%<_IS&;JR&]^TGH*).EHF)V!X%:
|
|
M4*![V(Y$0F@IH"E@X +1@DYDA,J@KJ%IK2I0GH?Z!O?OE\?1-7#\>30TM,/R
|
|
MO]?IUR?XH%*@H&B !ZPQT'L4!;)_?^(+3L8,E+ "5P[P1"!7EA+K>X*3.XM,
|
|
MHPD9?1'BHUA__W]O#)22\($1!!/H8F$#&P'*(.$"<@P%D(PO!P',3F( &3A"
|
|
M8>%Y"@%J&RH($AF06:;IHV%D0R,3 OB_'1H<[SX0'$-YFBHT9"EJM;XT.1*Q
|
|
M,W$RW='XX("F"*502(: )*U9LKW4"$3A>0\*<E"(= 9@I^( CL@ #@120A!#
|
|
MQ0B D)3*8D7AB!8H .:# 1@;UZ]?CSY4;J2O%;(J50<$@2(D:'*=#QZPR?C@
|
|
M@#(:*I'YWR9 &Q43.)+0?>)HHGC*5/\K7VL/]QM24"R3QM<EF4 NY "Q$53U
|
|
MOUGE =.S/Q.37A/(M8$$GLQ'8L4 QB@3.#2$05(DP/[&E9Y(*B+IB(0N,&0E
|
|
MTC.K'4ZO$5*NJA! J8SOXRN0D:\0\\0,]7&5(J'+3<#F$CD+-C>XR/C :PK
|
|
M*:(>0#6H"4*1:H0%Y!%%+$\XL/ %I"RAJ,U9XE.N)P72ZP1\#P+I@RH?MB/!
|
|
M[HT$&Q:F"2XOE)LY86YF8$^]F\I;_+8!I8*9AQZ$',H^WH9$*56'4(]0U2<F
|
|
M2X]T@Y6;(NCQ)81M4VR/_/Q%JEYEE/\&"NN-$PQ,HCC-*6Y8 4QP@X<KD3*%
|
|
MU>OT"#*%"= 0(OX#PDM)DJ]9G\1P1YXA \:#09"#+J:\_HT*VX-+BV^_=F!Y
|
|
MVHGCZMO#U<$1BBYA,+T55W-1!M/]T#7V/EAA2!M!;[5)8?2RI:6;RU;E&?H/
|
|
M2W^+$/M?%S_PHLRE/55^N/HKG% ",8:Z%^AOO'!8-V*%OW!_^@ C_P=(U^T+
|
|
MO@=8/Z'^]P3RUN=,7B]LSU3Z9X]7BLM_,&1_@&X,,)ZI=M*ZL6([.[S(D'S[
|
|
MZN*OG7:V)LS2DV./;FVG?FT@MXI_@Q!4<'9?CDU,X NB'S#CPI +-106-E[R
|
|
M8D<+:"L2H7<LLC(]1@;!NX#?[+XI@ERZ,\7DW:F"] ?P=JG7W!$"EUA)#&F#
|
|
M$OZU)N(!\ZI[_6 _#Z0)$$J6?E\^"VY<C]%I\A]S=I1(OQ_+F;XZE_$[6;I^
|
|
MP0E!>7$4SM8N4$_CD ?_3?'.3)F@=K^.;="!N[MA[?=..;<GB7L$\U5^JG5S
|
|
M$L<"L"K8+;Z'4)=DZVM_+T+\G^.'L.NW%&GA\ \HFB"MV8F2D+P\@,I WARA
|
|
MQ% WD#AA\7G$Z\=#Z*3HL,D)/8)\Z$^-IU=9O;MC+?#YM3G7Z\2[^93+C:.L
|
|
M>@V]HK@Z!*Q@7192>Z*2M7W>'\V]0"U VAQ=;X)H^PXOF+MTM?P&7)(&8XH&
|
|
M!O8 $EVX#$+'.D07)!6&0+M"-ZEFDS_\'P"<*)J0^?K]"BZA0'G_@)!G?BY'
|
|
MVE#C8/P &?S$&_JO]_24V.R,!DESLO+_=U2NP.7MVKM=<5F 5()N6%MQHC/-
|
|
M#@WN "4GK9$:2*'<757&-X%S%#.L-Z:0#(FJ21IFZ?U%P$P +MO!/A=:LOX
|
|
M1Q-K=90<' T(CF#.6AKB F-_2)WO!8X("87Z 15#SJ?4LJ;WTW^78T!PUN/-
|
|
MJZ?_#Z)XWY.KND558-4QZ6$;$^XFA-*MM^?L7(Y(IA\^6)G*>Z+8^(!2HY\C
|
|
M7;($N([X(*JR25PU@_V# TCUP _2+$4XDO@&". 9@<4<0<DJN%7=Z:$=^C@"
|
|
M^A;@L.4#5[!WB "BMZJY'2:[/GW@$U)3>,JIBT0D'A++=<C-H'A>50'#S2
|
|
M!$4-&XF:8D7\0!^+0C8G"#KN]_()-(D-I9DDFA+L#K;[:8;U55F,H-3T,PAB
|
|
M\+Z(2B0&8FA(AR4-S0:4P(347"[$(+F4;N1E42#K6B6%(268D 1E'>E=2COT
|
|
M,#B >TB,Q!$P&&"3:\Z4P)76K>KY08B[!R992HP_0$'Z<]OF8E4-@W1')$\#
|
|
M(/SO1N=ZUL3ZUB8QJUH&&^BR!#;(IYG>@DWXA/ JTSM-'3DE1M6B%5U7C?W%
|
|
M1V>'[2TM:B8#H##%H !S4$2B<((%@S1 B!\E+[:@8X<FY:!?@<3PP'YD#5*K
|
|
M%(T&(&[B6E#*9@-T$U%U%#0-0&SRO:L.N0\5,70=&9CJ4"2#I0]:E?:_<2-0
|
|
ME<34Q-1#"6F?4,&*#+R!48$68;"B?V)_4A,3F,LT ^,)3,$*=C(=J""I[H$"
|
|
M5@SD3"36^,#= NTP[/]?YA!%MWQYJ(&4$IJ=\[FH%")*-5VN>Z5D(PV&&HI^
|
|
M4$R0(,$"%!.R:GR U% 9C30:^2')("!NY@*A:Y- ED:WQZMUJ(YWT#"_[!C!
|
|
MBH(+S$Q8P)>._ IY&1!<0,&Q/@F/W;>KHZ!^%J^1\1IQE:63,G3#!CN%#Z@%
|
|
M)&H2Z(AP"QP@:N0FP@]F OZGYM#E4.5RYJ@JA3+KNV.06*--0GP#R9#(4ID_
|
|
MODJCHB;9(()&E4AR"G43.HVX+83([?:9_R4*+WAJR*U#(8TBH4Y'%0HM0Z&Y
|
|
M4 FO:)@+Q@(%Y)(HJK"ZA)6,+MW@R)J0'4Q)&M9'/"2F*!*A#9(J(A5AN5X&
|
|
M@1Q0Y.<@J$:Y)FG%ZBHG<+"_360$M\0+$Z"(P/2G-Y? STG5<NR@,C%G! @)
|
|
M#U"A$$>A[EB@/ZV,%#XKD%:8@#YB_?I6#=S?)_,^Y:!G1?V;'89,@(;["R!$
|
|
MY*8((K/\>1$9W \;D39B @APPG*AAHKY[UN3T;((5 @CT=&')"%X@5ZC7;1^
|
|
M?MLF?\$-U@7:C*H2U]O#()((O<DN<!MT:<E=?K>A$JMI7:6:\7D5U!OS@Q34
|
|
M'L&' Y@A@2LL%]9*IRWH[3>H/,4@[*'K[#&J4Y$'BC(.@ EA ]#'.=S%+(,
|
|
MQ.[&[E1LG3Q$DV@RMS0*LA\1VNF"X/XV0\D;([I&UYG X!S_X<T0#:T'5!U8
|
|
MIC>&]CP3'.DD#?\^#^H(81*%08@-X@PE/+4\@6%=@6M[?O+Q#$%0L(YYPP?D
|
|
MG#9RK++X48#5<69X## =+N?;AJ1(%+WP4-DP'.T<C,_9AUQ9\^O?MTXF. .Y
|
|
M [4%WN5*E"0>@B5LA0CV^8IZR*Y"/63#]-:E68![ *L(-O'>MEN;[FPY<N/&
|
|
MC 3?,>3(D1\_CBQY$N%W?NSX\>3%G0U'ODRY<F7/BC-CSFP(_#/W8BI/4VBW
|
|
M(U9'6#$K&27C1HK"*XOB]*D4*=DE4>$W,LQYQ^;%(^8!])_ @<_]0P&Z[%:G
|
|
M3Y>Z['G2I<D7(52'!3&^3S >38^G7_PW[MBC.X^./#FQ9$,8P8>D/( %%#A
|
|
M%@RXT.!#+AQY<_+<KT5[GEUZ=>O3MT<-,;>>O3KX[6(ID,<))!G+_&"YII*\
|
|
M>"&&H*RILI I^9(:]&YW-RKE!Q>"6.![&//.X*AZF\*33]"&7N>+*FB7QS=V
|
|
MD%1%2'GQ;IA1 JH>\F()Z_Y ^!5%7B@0(OT.%]PO<\;L>3-HC1ANG4%]-/Y(
|
|
MFTFC6OECC=8KF#OVLVWCY!'GV3>@^^YI"LM]6Y=MWKE[^?J)'!4.W\W[-JX:
|
|
M.W_:.:T?]FG0F3:J/^2)4I'6M\((+*LG97F_-'AT.=#FV+TMR]>7]YY=6W3H
|
|
MR9@1/R[4XA^,B' ?%CRH- Q6M%Y4)E'\GKERY]+[P:.G+AN8 ^X51V%3Y=^
|
|
M7=K555BV[UPK8>#):#.TE7AW:]@8)K73\H8UB3&IMZ.)#I\N D$7DLM)-?ES
|
|
M_VAZ/RBBE/RE?+?B=D@N$/"%Y(HAQ3:SX;)C<Y".H<:^$R=/'IN5$DK7CSY%
|
|
MVI3ITB80\*UBW=I5:]0L?L1NY]ZUB_=OGM+T*P !\J:ER^YK=R]>O'OZ_&_?
|
|
MOODL7)FLV;%CR88=&S9,G'\K&,4;_J,^NLACLN@_MRYO+0M1G('^@?'AB(JZ
|
|
MND"I*;W=>;(G^ET_?EM;OB"O$O_L62S^UK7N'9$/!=_UZ=6F2Y?N#%KSYLN6
|
|
M+U_NC-ES)458/M]9-*DK7['L^M7J<O+E'I!W;U](_MV[<D_V B_W:==2\RCS
|
|
M=^_.M>;Q;E\>^,(RF.2<D3,3H"B,BXR[VA4I%?QG0F%DED?XRI\_N[P\; E?
|
|
M'R^D1B]+3%]C!MFT@I-L*V!(KD##B;9&0AVABV >87Z=.C!"N!&8'^C/GS_*
|
|
M?+J56[3]M>Q9MF_EUL'-=@KP*%LM&B]O+X?@+8&O-6N6P+]ZA8#T5"EM/C>_
|
|
M4R5,8:)#WGQ+X9LXRH)QU/EJ$*]4T9?N /=[ZU+C':!)(K?3FIS8EE\DF'<\
|
|
MHDQ:,0I^<"'LV*YHAL;C;][\V0Y&.I^E+7ER7%#>R)J\?^>FI<@IE@CNW\.H
|
|
MO&3E:UAL/<[9JS?[E5;YZ\R#(X]^O;GNVL*8L$L'AH/_Z_.'UR+\Z\_X_KV)
|
|
M95B96/U>#^EI,>%I9#X*^VRJ-?G9GSF0+6">OLT*$LP+Z\ZMS"VF=X202%[<
|
|
M;=$A"KC%EM>: -#1#[PDIZ1;#^\(&ND- 8Y'MEBY4O.7J,"_ 0)^Z,Z7*IJ@
|
|
MDW11Q??C@_-!+GNYZ12V'V(W <%WFM0((!^UZ1'"VVRD/VV<=X]>L8W*^&7-
|
|
M'!.X$.#=W&:_98O1,GR6*4=V #G\P!():.*%"R% _/8LK5F<OY[#0EH:Z0S+
|
|
M6R>$4AP_?CPLCN6)')^[2NJ%H^NC$$^@'D>4<O5JH5$7_/#AQ9,[AM+RA,2Y
|
|
MVP1(\VETS83)B_>NO;JT]Z+RID:"1Q\-H2M"NUJ6IX454Q#<]G5J/'B">C3!
|
|
MM2XUF4:?CH3D<T DGS^?3HG#\^?92- 2EJR[A8S "CX052D-4]*!K900Z"O
|
|
M%T$0Q=)ON8+T)<7J=_[N!7?*E"I9N.#Z(Y;A1U2Y+!< ,I;Y"/-*N7?@6>J!
|
|
M4F/9,#(AW;%C!PP<,U!I: ?)"=8\&6424QA 18<61WPZV22^-].!^Q,1N%_A
|
|
M4!&6HKJL,G25)34)"DYG'AKQJ,1%)DB6IV(Y?OQX 1W481+M<A@J<PIC0O.A
|
|
M]OET20&%Q^-,AL4]3]UM3CQIKS@PKPL2_<Y)AB1-9QE11,P/IT)WA3@2U3#R
|
|
M\)Q@+V/__XY$-+'[L<^P@>,.@:.4\\</=01CQ(CACA *U!<O? YOU& 5T'TH
|
|
M$C4CRJE"%++P,FD-$D1X@)='?#RAB* ;%70G(G#D $0)[I? @V 4^E-1$I86
|
|
M64VR]D10L9$!$@\D4"/ B82R [#\_0R0G)A>Y.#KJ,^*&5&N)WNF% 3A+9C
|
|
M=Z1!M",& 0JID*>9P(@$&_@TPB)A 3X2#, CL0#'0?)H?RR.[R5(B-[T@T'5
|
|
M5>?RA!>'?@CZBN $<A2P QP.".!+0@9(5,*3L5=O/7O[?_1;H8/RQ"\&R?3I
|
|
M20?2"U31X5_,VC0#WA&#J4[A]^Y4!3'[A3<SQCOANI1,C/9AGD*5XO"S0/SE
|
|
M'//]_@$Z,,B%]- L=$IZ/$Z04GSP8E2?$6H@U^OPF'T[PD"!W@ '^ %ZM-]9
|
|
MCKI87K^^?.O/S^--':%Y5^BS'2+P1_X4._0#O=I!B$6#K7%I3 1B/-=;R1 6
|
|
M##W;_56V+" ":K[#<XL&6T\F,G@X=M.MVV-(QO+8GV+].A9MV[=SZ];IZ SV
|
|
M:M2D0XK.-%W$@$Z[M., EC,]%5I2,EJ3/C[\]RZK=7/VS5-;EAS9\>+#A0DM
|
|
M=*"7A)HE6]X\673MS;I7A:RM]Z?^/#9S9G?7U<U/[Y]QY+P[M!/ E+2D?)3T
|
|
MWDR%3(RS"-)W*@FP?P('?!YE!/!_ N>]=M\4[JQ$9M8K//X7)=R ]3>ZN\R
|
|
MWLB/=>QHWL&VZHTT">"$%@<*NE#F@-*F+PV42[T,YXZN/QW<$UN.E/L1E2+O
|
|
M#@9417F] 9Z<H%PB)C^S9U A:&NC1S#*<[T64<1[_I@ZGMET[-08:^L1@AND
|
|
MR1*@X)(F-^R\I$Y&@C?R!)KR@MOTA;VHKY/&/>*!3;F?0#VDR9,J%KSD9FJS
|
|
MN?G0H4_?KLVRLF]R-/WTJ9IR#S;JU*M3LV+5\C<=UHQ<.>T:U5X#JNF;_7S2
|
|
M>5#INW7CA)+7>AV:,RD7D#%9LF33J-)GRY;]ZG5KEH9Q6+9>F*A?F%(^]8@9
|
|
M7XHLZ&[8G2@AS Z2AP/C5;].7?ISI[6\9]JW.2=:#>" =#%P!2[X]*C7@0<F
|
|
M7!A1-<:3/)E2,1VJ6"[F(RY<%[Q^O-A>9V2D<NS2O:=W,YK*Y>OKDX.*0"9Z
|
|
MSJ@DZ3E2D' UL-\H\R6'%M0(?&BL$$$ &B(%@J,,LJ70HC=3FB1!A3)(D!]!
|
|
MAC!]@1%+TLQA24YR-;^*I1#YS"6^YI0U5$XC5S9K=D[B7L-7G 8T"ER"KP;Q
|
|
MU725V[I]._=/Z> SP1AFKH+]/I\D5 5NKEI*D\G8:1.4<5&D3Z4>S?F";2\B
|
|
MJ&:W_]@@#HV %,UF[-.?+]OEBQLOMBT?C^;/-_=]>_9K5KP=^O.E<6'$DH+Q
|
|
MRYL[^?O1I5%1XZE5J:)=YP;^N_8IECSHSZA&Z=:;*T^R<FT94[3DC@ENSJ ]
|
|
MJ3YJ5N1X+DY\.3#EARU[5C,I^D=O-GW+F6;^G+HUNDDOOR?I<S"\<#';BI)"
|
|
M0/1QKR?."\0_KV^C!Y#HAMWY#<;78RL"W72+%"%)HJ%X@HQD B=^B1NPIE_.
|
|
MI&GCKR7;1Y,N?6+NJ5[M@BF'Y7**5]5"#A9CV%,NR4HN\DF%#L&DFU!A$R4-
|
|
MXU2V^XMPA[V22TEFHT%_[HS!R=;9<T=!E4,5ZA\M^N.4]?@[ C_G3!6:6CR%
|
|
M2Y^8^R<L4 -&D50Q:L9D<CM-5D<AN->3SQY].7-G?,[*"C',KMU[6*7,7-FG
|
|
M,#)G'%KE*:*7.P-&&>+(C2,+Q!+Q8FZEK'+M6).)'Y[Z%!/V6F0E@9PG#FQ:
|
|
MEN;RCD95/#S8XO>^M9"_[;N8\GZ&!<VNO!V<(2F#]#K"E74;;7<F]O>@5F>2
|
|
MI"H5-&E.@EP \GQ4$GHPK> "KB[1!D0,(Q1PM:OA?U B)Y ?@0C,0J\_HZ9\
|
|
M\\8D7O,F#T<@H.BB29-L U'D9/&9+TC1+0*?98M8,@W0OVG3QB^6V4-*^R8-
|
|
M5=(Y](?'/HEBF.2,1$P2IE^ZN":O"/P6R$3:](IN5"P5 .] Z$90SQ231P5=
|
|
M@HMBZJPV] JN.*E&'-PQC*R1,@YNIFO;I#)U[!!$72.:19JVT95 DRQ:[([3
|
|
M)>LBO^PC^_DSF:R6W)O5E/TWE.-J.YF=*]-<)%D<L17XX.X$/9*,Q^B6L.)
|
|
M%VAQR*(R/.^?0*$+ RF_"77:_.!H;*"&0E24U)#\[X*E4Z"U_H9%0IW (462
|
|
M-(G"VS+W+U\TV,+A"9*FD(KT2(1*J!CXXP?/+J&,HB4J; @/PQ4^)^#S?1$Y
|
|
M2"63,P0NR.= 816B"[E/L.:(#+YDR$0@)PEYZ2 \'4HD]*Y!YG0G*ETIU-:I
|
|
M!N5Y.!/CFQ%0@5(95#9"=+Q8H&=S@;Y]VT+>&\1@<9SH<H$K@1<+3$C%Y2:F
|
|
ML?%#%E>>V=%(^%W@4 M!, C^9X>@,.]XD4""*F;$\28JVI+1C&!_1PT&@-50
|
|
M078 X43-"5&<-8)1_(#8R0"C4>.!'&8&G:::(,&Y"CZ4F &HR,JA#KC#'BT
|
|
M^B07&!$0$=P/(5 C?G!RT4)&"EF!JB9MBQ&^X,)34$3C +#QO\!A?S@BE4@4
|
|
MK%0O$#SN[P.)4%3TU0AU!$"""X@;SRLK</MV;1V%)E>D22)31*A(Y(3GN0?*
|
|
MR)8W&/1%HBM%U(SGQR/ P?DH%-!=1I>& 9 D"!?Y'PB"O"IJ5V)UZ*BB2C,(
|
|
M7(#:'.!)II(,0BFP0 %D'G"DBSS4)T,U2-3'R0="V&EZ+M AJ 4YP7]+)$UA
|
|
M(6 F!Y<M*$$H.03+GD# @0XJO61<L$#_N&+0%7_T+"?<4DO<VKRL"FPO5Z[L
|
|
MYM7W9\YLQ*8\2S.8:J'3<:9'63[]>H!!4YD!"+AH*J%=%3Q+:STTZLJ& ^H(
|
|
M$H/Q]M3+^QDZ0TA:E(R5$.$B18!"=T!SI_!1P KR"/(4DBN80$<"ML]L0!W^
|
|
M\61$>B)+/,[7TH&E!>YIAA>X)"SN'FX,S.@R'&C2U"""5B-VH0$<.Z TT=L3
|
|
M:2IUP><OB,"OL,#WOI'/,Z4PWT&)0SETVC)&#@RLI&)P;*VL++H5*N'\S\ZH
|
|
M _4"V"<@$HKN@OL/X,X$^T$>J1[_8 :-_<H!JG+U]-WX,"][>&_K^R<KS=*L
|
|
M%EM#3?T83BB+<GOY]L"RZY&D"^:#EP\^%&B@RSH!9N+R>=9(+^1$<D:0A;IA
|
|
M!R?K$/9<YZGEM-_\<O<#8C65$P^NB[\'%5JXY0O-X31N*&%6I<!) "ZA+L1+
|
|
M/LXB(HMQM2,5^/G]V3R-$':0,!_)WUV'XVXCEKMUW1R=D/ V"*MK+"=,+DQK
|
|
MX)]^GI:F8(UNU,S&UZ*3 IZV?U!)[& B"&1O089,$E1'ARY.6/%D#<>KUHS.
|
|
M"""04_<>[V_.(#5MX-V\B'8 :KL4;%N4T..(7C:0=C0'(7;-27F &!;=P.61
|
|
M\BT(EMR0!M"W?3'FT9(#XU&6HD.6(/K.D4T+3QOHA]_6\28W<787 7/^__F
|
|
MGOO7 TCS0Z(E]L^'1 @HGA^ G"JIKV)51"#1I$<OLB;+CQAV69 Z^FMUK7 8
|
|
MQ-]@0X01!DRNR""&\(,%@AU0"YVFTDHO9+$ #MS^&@B9X ?(OJ>>TY/@ 7;^
|
|
M@0Q-L@RY$%_%QN*6MM!\4KO [Q:L3>O?1+;2$%*;6>!Q6FQJ<B^^(0B:C'4%
|
|
M1W ?J(#(\*ZO\0"U5XXU/=Z);!!&XA24SOV&_O;]@9ZA^]@=\,X0K$JS7X"$
|
|
M567A$!<,8 *#FC>!&#@_>#5'R+$=(#?Q ]5;"T2:L@0?802=6L.<G! E3$?X
|
|
MZ?C03N=ABHSJ[\BL%NF\7@2/F&9P/C3:^.JJ0O T)!EK'3\]31FX1B"*9]$#
|
|
MDM9%;/,48)TLQ\I]*$5)%% X0A3M!Z"3PY]"2=$.(X6OMZ^1]X"EXP'9_@ 4
|
|
M(658M;0&HY1[ +&3W.GZ'50- 7<\."\\\W)W)H\+W.2&T>=HX 3GN#Z^GTU9
|
|
MM/W5/60(/& *I">H6*2X+7"W>.7W\^1^]*?P!]$.Q:D'W%<'1CE; -\#VE@H
|
|
MA>8$4F!\@.LABBT)#EV89 ,A>R>%%IA,J:+"J[I"U -T@6CIN!T"(V?-Z4!&
|
|
M1X$3FB7#-;@\KVV00M-J07AELQ$:X&*@F\4(" (C^.*P@QHP!ZCQ5&AY$ 3(
|
|
M"-#40!.63$X48D!!8$TV1P"#[CX'$#%A '<;":YF/A\=ZW4S@6:KB9(+Q._G
|
|
MM8%!) S3"QS'Q\>ZE"Z"?7P_((I@],&D <$,'1!QM*V"53-$\+60*=0#(1X:
|
|
M64)56BBTO"9I1?CA@Y*O2? , JX_GX- J%1--5)B(: 6:\OP*K3LJKGOH[!
|
|
MH,$)I OL**&SO-O1N;,&M&UDM,A:)P!U;:"(5+V51AS0P->NY4H-K#A1JTJ>
|
|
M&8&MH@+2_F 7@FE^Q>E71</[ @Q8>&@X0 J.K@GC(V,1V91$(']!)H&<*,>7
|
|
M-V6"Z]>7EVENETC<7EY4X/W]Y'2T-M$M2IV'Z(#+((;I!$P+& "CW6PPH#\]
|
|
M EF\$38@3E>=3-M EHX(#"- -0:0AT+.$PZ"G"ATSPD,Y*"A=!M!2D<D"L)5
|
|
MS^-K/(_-0PV-Y@;4IL#\$#1H7-R/W(PP <6E!IH3)%4[". 5ZA"L(^X3<SW0
|
|
M'N%2V/^C._[7TR1Y <BF=O)]0.VD\W0QCZ!P5,-]3_2($.*Z&=WL")QNCI"#
|
|
M?OEW/]D!T 8Y6YF'K%7*"1":_85.4@R#,X*T!:UD4+E(VOLK9/1@"XRE86@#
|
|
M?)]\1:.FGF%^;PJZ41#*A!M < )K J!0O#4H_4;T*JNN6.@#M4J[L()?R(YV
|
|
MXG3P1KV*N(4.4X**QH7")5)$J^Q]"CB[ _,HK&+[]^@:ZL 1/D"I2G0#XH\1
|
|
MJ*C('%+DD"(DI4%O%": )D*4V@9!07\)$V '^%7^%R7# 0/T"N)'KI]/3R-*
|
|
MF4&\0".* B&EJ>A#9GG RNYB.N>3D"#<]^?.E/39(((\$B:@&OOCGDAM!1.%
|
|
M'9"S"<A5A%;)E[']>5($NL.ZWB7+=UP@@,0&[*#LH*[[ :E1:MM;%6TX$C<)
|
|
M(V%,'CC 1$2,RQ9N("XS?OKOOGB>/P]B<3NR(TP("QA-3/L+8]$9,A DIO_Q
|
|
MY]RB$?#V(+<:?;C>6P?V:VQ)!EA)66L[V;^GM[O:I%8E!$Q?G^VSMQW^Y4@"
|
|
M"B+)N[=5F2OTY0B%EVG#?4&S+Y@S;=$-@#XA3IP1#.;RUX^3<(IP+#[=RU7"
|
|
MYW5P/#3.N/_[W. $,#_04M15O>#AX@8SL/A!"::6KXSG#E2HAW:DP,;?,ZH+
|
|
M$S589G187]-WX\8#1WQPN5A*15V[YC]/>WI,I!]*_@3H$3- F3'LFOH5$A!L
|
|
M $H0^!_TF74%S(K37^Q9TO^Z;03%,M^SGN!@E(= 0P[]M4D#3TASXKN4T9[^
|
|
MONEBA(W99QBELG0%-17@:8M!)EV;DP&O*_^6( NNXX!69)T^>WF KU!+ P0*
|
|
M 0 !@#Z:,<:33MY=.T! !4 P !P '!W;2YD+G-*!QB= _T= QT(!4T&
|
|
M""T'% 4(!@@'%@@&!2@=!"4#!18#%@0%) @$ P0%!@@&!1@-%_V=_OYN!/[^
|
|
M+A<>!PXG!1X&'@8''@=^!?[N#P$" P(# @0#- 8;!;O\_#P& 42]_?WMU&C
|
|
MIO_T%^$DG7=C^]N=PAW;Y!<3,OMA&QTZ[%O6*+5:E'4%+>FA$L]^<;?;9QA?
|
|
M?M_]YF[(CZ6WI(8)%^CKISE7F<OQUO]FZ))>?5GM__UPQK8)LJD7Y[_G0C]=
|
|
MS*XIJZZ>\;2_F4KS"][T3+6[^[8O;CCE^Y;?['9^]5+:FO:1)2A@A94UV"7%
|
|
M58)P!0;]"_,9T>WFHRL.;RZLL,(J$5EAF<Z;ZSVFE$1I$HQ$+A91.3*JTY9,
|
|
M-28_[?N&42:LE\U9I2V_AS;UTKH@^6<IU\H8M3]]Y\;R>X)+\SB@UCZ!Z+DT
|
|
MTF_NT.F?XO;3[C+5[!Q_7^17.>W^33O' CNUR-E<IEH>#-PUL?<WE1OX2][Q
|
|
MI_\^<:8:[>2\#^PEGJ1$C;B6J3K=)0MB7V%-GI9F'/J/F-+-<?C'^UC_)#XV
|
|
M+K"&?@T=\7REG]"GO32,U^$/6]ND'CYFF94&M\CX8-)RH3=P-KPW @^;^]IJ
|
|
MY4)#IU27U<J%F_($XCR!P2]CTP]FDX3"!&93#4L!4$L#! H ! & /9HQQIR
|
|
MY4!WL@( (<% ( <'=M,BYC+G-,!QB=!/T= QT(!BT7)@@6% 46" 8(
|
|
M!@@6*!T$!10#)@,&" 0&) 8%! ,%%A08#1C]_3W^S@7^_@X(#A<>!PX'#@<&
|
|
M'@8>!@<>!WX&_NX. 1)#! ,'%0P$/ :L_?T%#0 " P8#!@<&%P;W]_='D2,7
|
|
M[ 4_PDVY_S7=*]_$-YST'Q,J=H=M=)BP?]URJM6<;0--Z.3;_OA7;F\__%+[
|
|
MVG_B^M^51LKS?O(;YJ?\Z<\R_KYTNM+YX$]K_E3]<<FO_"S/FZK&+Y:?Y.>?
|
|
MJ73J/]?S^-K<_P.I?_O.N4,*Q25^2>S&>TK3^WTJ:-HW]\/$MF,]\VO.FODC
|
|
M+5U7[IN(QM$;\IWYF_MR?^)25]Z/>W]>AM^-2_TS0JC<\UNG^UJ;]OSDFY+\
|
|
M$TH+W->Y=/Y[L2^G_,)U@\A[U]_A%(,N%J8NN[IC=LO8=',:VKS]U)!)W?<[
|
|
MW+,7QVGY(3(?E59^0OX_OK_S^^M>;,_R"Z>L+8N2*U%I\=7=W(+Q#JYMV6N&
|
|
MJP7E"@6[)RYW0%>9NJY)=7-CA2NL%B@K/-.M(/XF%4J,5AS\;LG!47(&)6,L
|
|
MA:D$XTQ4K:+BB5JRUJBT&" W_2 4(+5LJ:I"DRU_E]8+IVV5ZHN5;65"[5[0
|
|
MUL/\O^#2//2HM?? D=P$MA=?7]8_ ZF?I<:8SM)$I>4V1-9*.%[%5JKU-,K7
|
|
M<E[;,B+2>/YK?E[Y40T73HU*BR:SW<7).&'9*B_AI[=!WH[#CK.CTHC:-39<
|
|
M$%_##[">1IRJXKSLHBKF:OKH_9S8_4@(25X3D1-,F9Z2=Z4;?_K,XI-.6Y.D
|
|
MK)E>@VSC*N$3\@V;%SY/*\,SSIX^]</S\+YJ[EC'ULE<997\U2&.*3PMAW-X
|
|
M;T!P5[.-YY3*BONREKWK1F6EXCQZ."<QO(XQ.YK1DA)'-&/,] 502P,$"@ $
|
|
M 8 ^FC'&LJ@5',0 @ _@, @ !P=VTX+F0N<T<^!WX#_@X' QX'!DX&
|
|
M!RX&) <&!P8'!@<&!2<> P85 R8#%@0&) <D%@<%!A<.%_Y^__^/!/__+Q<?
|
|
M!P\G!A\&'Q8?!G\%_^\/ 2(3! ,$ R4,!WP&7/W]'0H $@87!@<&]_?W9]&B
|
|
M%_R"%VQG['=*_>EVRAF;[!<?D_!A&Q<P\%O6_'G.7U?0DAZ;OMN7=+IV#^-+
|
|
M;D_[;C<DC^6W1 P4)-3KIYN8=2NIUO]ZZ#)>X;+:__MACTT=KB[,O+^]*?UT
|
|
M,+OFK+K9XVY^/97OBVIZL=J=]C0O:=B5\Y9?M\U]A>6\->\C:WC@P.H:><EQ
|
|
MU;"N8(2?<O>(+FY;5QW:7%FA+%8-S@I5Z!:N]@A3(J50) E?,C-1.1=5\I9B
|
|
M-3YY:L\;1AF?]'PQ9Q5_R\^F=6%Y79#^2CE6)M3^@C9QK+PGN#2/ VKM'5+/
|
|
MD1"_/D.G?XZ%?>V(I^TL\:LFW-3S<E_6;MI?-[=$+A1^FYM8K%8&%6>5W/XZ
|
|
MZX1YV6WJ[K^8S\5JG)QW'_JE[XQT'Y_G8E6N+@E$[BZE('M:S/8X]!PAS=)>
|
|
MET\BT.09989E4Q;W>![_7UW%/,"9P;]T9=;R,!Z(.D,$G.&^Q$V>@0ATV!"A
|
|
M$D0R?US_>@-Y*WBAM8XGK#C*HI4Q<26EUW(;U!L&-Y#]>55QX:'3<W1955RJ
|
|
M*Q.=>QK#&YCS:<R:EC+1F.-F*E!+ 0(* PH ! -IQQIT41>R90 &L
|
|
M , "D@0 !D:6=I+20Q,C P+F]02P$""@,* !@#\
|
|
M:,<:E85SFV$! !R @ # I(&/ 9&EG:2TD,3(P,"YS
|
|
M4$L! @H#"@ $ 8 ^6C'&FM^*+ + @ > , L *2!&@(
|
|
M '!W;2TD,3(P,"YS4$L! @H#"@ $ 8 _&C'&O9"0BNQ @ \ 8 P
|
|
M *2!3@0 '!W;2TR8VEA+F0N<U!+ 0(* PH ! & /=HQQKFO<<O
|
|
M:P( &D% + "D@2D' !P=VTM8VEA+F8N<U!+ 0(* PH
|
|
M! & /=HQQJHW6A*AP( .0% + "D@;T) !P=VTM8VEA
|
|
M+F<N<U!+ 0(* PH @ & IIQQK396D>>SL )0 ' "D
|
|
M@6T, !P=VTM:FUJ4$L! @H#"@ $ 8 ^FC'&DT[>73M 0 5 , <
|
|
M *2!#4@ '!W;2YD+G-02P$""@,* 0 !@#V:,<:<N5 =[(" "'
|
|
M!0 " I($?2@ <'=M,BYC+G-02P$""@,* 0 !@#Z:,<:
|
|
MRJ!4<Q " #^ P " I('W3 <'=M."YD+G-02P4&
|
|
/ H "@ O @ +4\
|
|
|
|
end
|
|
|
|
.......
|
|
....
|
|
..
|
|
. C=H #20
|
|
|
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
|
|
cu next time!
|
|
|
|
-S
|
|
.......
|
|
....
|
|
..
|
|
. - fin -
|
|
|
|
|
|
|