On the C64 there was no threading, so how did games handle treating at the same time music and game code? Music requires exact timings, and the SID had to be instructed to change the note at the right time. How did they achieve this? I suspect master usage of interrupts, but I want to know more.
-
1Not sure if this answers it, but a lot of info on C64 audio is here: icce.rug.nl/~soundscapes/VOLUME08/Loops_and_bloops.shtml– forestCommented Jul 25, 2019 at 1:13
-
16What does "there was no threading" mean? That's a feature of software really. And software on the C64 may be threaded, just like the software on any other machine.– Omar and LorraineCommented Jul 25, 2019 at 7:44
-
5@Wilson I imagine OP is talking about either hardware threads or operating system threads. The C64 did not have either of those, but in theory yes, you could implement a pure software scheduler. I don't think you'd want to that on a ~1 MHz processor though. There are better alternatives for async processing.– forestCommented Jul 25, 2019 at 9:01
-
1Not a complete answer (I'm only sending you to a link), but The 8-bit Guy" is a good resource for this topic. This, for instance, is not terribly off-topic: youtube.com/watch?v=q_3d1x2VPxk&t=3m16s– KlaymenDKCommented Jul 25, 2019 at 9:03
-
5@Wilson Well, read the below answer. IRQ handlers, whether triggered by a configurable internal timer or during each screen refresh, can be a very useful way to do async processing.– forestCommented Jul 25, 2019 at 9:15
3 Answers
Your assumptions about timing and interrupts are correct. Actually, it is surprisingly easy to add SID music to games because they tend to follow a basic pattern.
- Compose the tune in a C64 SID editor. There are many of these.
- The SID editor generates a SID file, which actually includes both the data and code for playback. This file is easily incorporated as a DATA block into the game's assembly code. Later, attempts were made to standardize the format for "PSID files" for the HVSC project.
- Initialize the code in the SID file with a call to some specified base/load address.
- Set up a VBLANK interrupt.
- In your VBLANK IRQ handler, call the subroutine provided in the SID file. It will be a specific offset from the base/load address.
Since the heavy-lifting is already done by the SID editor, the programmer's part is easy. As with any game on a platform like the C64, there's normally already a VBLANK IRQ to handle things like sprite updates. So it just becomes one extra sub-routine to call in your handler.
The correct playback frequency is "guaranteed" based on the video running at either 50 Hz (PAL) or 60 Hz (NTSC). Naturally, this means you have to compose the music specifically for either PAL or NTSC C64's, lest you have the playback speed faster or slower than you intended for the composition.
-
Are SID files essentially Mod/Tracker files for the C64, or were they conceptually different in how they stored note, duration, adsr, and other information?– Geo...Commented Jul 26, 2019 at 2:16
-
@Geo... SID files are computer programs, basically C64 machine code and data. They can use any kind of scheme to store the necessary information. Commented Jul 26, 2019 at 13:17
-
@nwellnhof - thanks. I just re-read this and saw bullet item number 2. I need to improve my reading skills. lol.– Geo...Commented Jul 26, 2019 at 14:24
-
@Brian H, do all SID tracks use VBLANK frequency or are also some other tracks that use higher frequency? How high can these frequencies get? Commented Jul 26, 2019 at 22:04
-
Ok, so the timings were guaranteed by the fact that the VBLANK interrupt was always going to run at a fixed frequency, and thus the music routine was able to calculate the exact time when to change the note just by counting the number of interrupts. That's pretty impressive. Commented Jul 26, 2019 at 22:31
Mostly, the same way that embedded code still does it today.
Embedded code doesn't use threads. In general, it also makes less use of stack and usually no use at all of dynamic memory allocation. Instead we make widespread use of global variables (or at least static variables) which are statically allocated. Reasons are twofold: firstly it's faster, and that matters when your processor is not very powerful; and secondly if you have limited stack/heap available then running out of stack/heap is a very real problem. If what you're programming is something like a game where the processing loop keeps running with the same data, instead of a word processor where new files have to be loaded in, then there's no point in making life harder for yourself.
Of course if you have lots of global variables then you have an encapsulation problem. Structured programming solves that problem at the cost of stack/heap usage, and associated processing overhead. The alternative of course is that you acknowledge the problem exists and be careful. If the expected interfaces to a block of code are clear, it doesn't matter too much if you can see the internals, so long as you're careful not to mess around with them. Or of course the third option is that you write spaghetti code - and that was not at all uncommon back in the 8-bit days!
Back to the processing. As described by BrianH, you'd have an interrupt triggered at a regular time interval. That time interval gives you a window of processing, during which all your game play, all your sound, all your display updates, and anything else needs to happen. This is real-time processing, and there is no difference conceptually from running a game like this, or running the control loop on your car engine controller.
For anything which doesn't need to be updated that fast, you can run that processing every 10 ticks of the main loop, say. Or if you have a larger chunk of calculation which doesn't have to be run fast but takes a lot of number-crunching, then you can run part of it each time through the main loop. Both are popular strategies in embedded code, if you have a low-end micro without prioritised interrupts.
There are already good answers, so I just add a few thoughts. But first, to make this a complete answer, the basic concept:
You're doing all the SID register updates from an ISR (interrupt service routine) that's triggered regularly. That's by the way the "lowest level" progamming model to achieve some sort of parallelism. Other things that should work in the background (like e.g. querying input devices like keyboard, joystick, mouse) are often implemented in ISRs as well.
So yes, basically it's simple. Whether you use a music editor generating some code or you write all the code yourself, it's always the same scheme: One routine initializes the sound chip and some internal data, this is only called once to start the music. The other routine is designed to be called regularly and updates registers as needed.
Now you need to decide what you use as the source of your interrupt. The C64 has CIA chips with timers that can generate interrupts, they seem like a good choice at first, as the timing is guaranteed. Most of the time, you will instead use the VIC (graphics chip) for the interrupt source anyways, even if this means added complexity if your music should sound somewhat similar on PAL and NTSC machines, because they have differing frequencies. The reason is that you need to do other work in VIC-triggered interrupts anyways for anything displayed on the screen that is non-trivial, and often the timing for this is critical. So, using a timer interrupt for music, it would happen at different places on the screen, and eventually delay your timing-critical VIC interrupt from being served, thus ruining your display. Therefore, you pick a place on the screen where you know you don't have to do anything else (might be the border area), arrange to get a raster IRQ from the VIC there and in the ISR, call your music play routine.
This covers most needs and is indeed very simple to do, escpecially if you use a music editor (or tracker) to compose your music, which generates ready-to-call routines for you. But there are at least two scenarios that are a lot more complex:
You might need/want sound effects that require SID register updates much more often than once per screen frame. A typical example is playback of digital samples. I won't go into depth here, as there are a lot of approaches, but it requires tricky coding and you are very limited with what can happen on the screen at the same time.
You need to access the floppy drive while playing music. Unfortunately, this can't be done using the builtin I/O routines. For ...*) reasons, floppy I/O is done completely in software on the C64 (bit-banging) and the builtin routines are very timing sensitive, they only work with IRQs completely disabled. The solution here is to use own routines with a different communication protocol (which requires to reprogram the floppy drive side as well). To know more about that, ask google for "IRQ loaders".
*) basically: crappy/buggy hardware that didn't work as designed, namely the VIA I/O chips in the floppy...
-
1Ah. The last point explains why occasionally the commodore would keep playing the last note while loading, typically in poorly made crack intros. The music controlling routines would not trigger anymore, and the SID just keeps playing the last instructed tone. Commented Jul 29, 2019 at 20:55
-
2Yes, but that's really sloppy crap coding :D Either go the extra mile for a custom loader that works with IRQ enabled, or at least stop the music before loading ;) Commented Jul 30, 2019 at 6:57