Intro
Many developers have been trying to recreate computer entertainment technology from the past. This trend is known as retro-programming. One of the popular ideas of this trend is mimicking the tech first used in Commodore 64. This device still holds the official Guinness World Record as the best selling single computer model in history. Commodore 64, also referred to as C64, scored sales about 30 mln units between 1982 and 1994 according to independent estimates. Commodore 64 is an iconic machine, a great contribution to the gaming, digital music and art industries we know today.
One of the breakthrough technologies that made Commodore 64 so popular was its built-in sound chip, the MOS Technology 6581 SID (Sound Interface Device). It was capable of supporting three sound channels. It might look ridiculous now, but at the time it was a very impressive achievement for a relatively affordable home device. Also, this particular engine was much appreciated for its warm and very typical sound signature. Some of the most iconic game soundtracks of all time were created using SID.
This article provides an insight on Commodore 64's sound capabilities and how they were used to create music. We will then learn to build a Commodore 64 app using assembler in TASS.
This project is based on an open-source GitHub repo: https://github.com/r4dx/vicious
The Icon: C64 Music Engine
Introduced in 1982, the C64 was famous, among other features, for its sound capabilities. Its sound hardware consisted of a SID chip that supported three voices with envelope generators and a filter. This feature enabled creating complex soundscapes and music compositions, so this chip quickly became a long-time favourite among musicians and composers. In those days, computer music was primarily created by programming the SID chip directly in machine language. This process was complex and time-consuming. It also required programming skills in addition to music talent. However, the results were often impressive.
You can find more about the famous chip and its capabilities here:
- The SID Sound Chip of the Commodore 64: https://www.c64-wiki.com/wiki/SID
- An Overview of SID Chip Music Engines on the Commodore 64 https://www.c64-wiki.com/wiki/Music_Engine
Modern day: TASS and its capabilities
64tass is an open-source, multi-pass macro assembler for the 65xx processor series. It is written in highly portable C and is aimed at achieving 100% syntax compatibility with Omicron TAS, C64’s native TurboAssembler. The assembler is compatible with all major 6502 CPU variants, including MOS 6502 and 65C02, and supports features like arbitrary-precision integers, Unicode character strings, built-in linker with section support and various memory models. The software is in the phase of active development and is hosted on SourceForge platform (see link below).
Programming a SID chip using TASS, musicians were able to take full advantage of the C64 sound capabilities and create some interesting pieces of music:
- C64 Rob Hubbard's "Sanxion" oscilloscope view
- The Great Giana Sisters / C64 Intro
- C64 music in HQ stereo - The last Ninja - music by Ben Daglish & Anthony Lees
- Rob Hubbard - Commando C64
To learn more about the TASS macro assembler, you can check out this link: http://tass64.sourceforge.net/
Getting started
Let us install the basic tools to start our retro-programming experiment:
- To install 64tass, you need to download the latest version from the official website (http://tass64.sourceforge.net/) and extract it to a folder on your computer. Once extracted, you can add the folder to your PATH environment variable so that you can run the 64tass assembler from the command line
- GTK3VICE Emulator: This is a Commodore 64 emulator that is used to run and test your code. To install GTK3VICE, you need to download the latest version from the official website (https://sourceforge.net/projects/vice-emu/files/releases/binaries/windows/GTK3VICE-3.5-win64.7z/download) and extract it to a folder on your computer. Then, you can add the folder to your PATH environment variable so that you can run the GTK3VICE emulator from the command line.
64tass.exe -a --list=listing.asm --cbm-prg main.asm sid.asm
if errorlevel 1 exit
x64sc.exe a.out
The make.bat file provided is a simple batch file for compiling and running the code we are writing. Here's a brief explanation:
- 64tass.exe -a --list=listing.asm --cbm-prg main.asm sid.asm Compiles the main.asm and sid.asm source files into a single Commodore 64 executable file, a.out. The -a option enables the listing output and the --list option specifies the output file name to be listing.asm which is a version where macros are expanded. The --cbm-prg option specifies the output format to be a Commodore 64 program file.
- x64sc.exe a.out This starts the GTK3VICE emulator and loads the a.out binary, allowing you to listen to the music you have created. With these tools in place, you are now ready to start retro-programming.
Diving into the code
The core code is defined in the sid.asm. Let us have a better understanding of it.
The program defines several macros making it easier to set up the SID chip's audio channels and filter. For example, the setup
macro sets up a voice channel to play a specific waveform with a specific set of attack, decay, sustain, and release values. The macro also accepts an optional argument for the pulse wave duty cycle, which sets the duty cycle of the pulse waveform.
setup .macro voice, waveform, attack, decay, sustain, release, pulse_duty_cycle=0
pha
voice_reg = VOICE_REGS[\voice]
.st voice_reg + PulseWaveDutyCycleLo, #(\pulse_duty_cycle & $00ff)
.st voice_reg + PulseWaveDutyCycleHi, #(\pulse_duty_cycle>>8 & $000f)
.set_ctr_reg \voice, \waveform
.st voice_reg + AttackDecay, #(\attack << 4 | \decay)
.st voice_reg + SustainRelease, #(\sustain << 4 | \release)
pla
.endm
Please note the usage of st
macros - it is employed to store a value in a memory address.
The setup_filter
macro sets up the SID chip's filter, which enables filtering specific frequencies from the audio output. This macro takes arguments for the cutoff frequency, resonance, and voice channel mask, which defines what channels will be affected by the filter.
setup_filter .macro cutOffFreq, resonance, v1=false, v2=false, v3=false, hi_pass=false, lo_pass=false, band_pass=false, mute_v3=false
.set_cutoff \cutOffFreq
voice_mask := 0
.if \v1
voice_mask |= %00000001
.endif
.if \v2
voice_mask |= %00000010
.endif
.if \v3
voice_mask |= %00000100
.endif
.st FilterResonanceAndRouting, #(\resonance << 4 | voice_mask)
mode := 0
.if \mute_v3
mode |= %10000000
.endif
.if \hi_pass
mode |= %01000000
.endif
.if \band_pass
mode |= %00100000
.endif
.if \lo_pass
mode |= %00010000
.endif
.st FilterModeVolumeControl, #mode
.endm
The encode
macro is used to translate musical sheet notation into a series of numerical values that can be played by the SID chip. It takes as input a musical sheet in the form of a string and a default octave. The musical sheet string is then parsed, with each note being converted into a numerical representation based on the octave and the note name.
The encode
macro uses the NOTES
dictionary to map note names to numerical values. The NOTES
dictionary contains the notes from C
to B
and x
which represents a skip note. The pack
macro is used to combine the octave and note index into a single numerical value that can be played by the SID chip.
The encode
macro is important because it enables the creation of music using a human-readable format, which can then be easily translated into the numerical format required by the SID chip. It provides a convenient way to describe music without having to manually enter numerical values for each note.
NOTES = {"C":0,"C#":1,"Db":1,"D":2, "D#":3, "Eb":3,"E":4,"F":5,"F#":6,"Gb":6,"G":7,"G#":8,"Ab":8,"A":9,"A#":10,"Bb":10,"B":11,"x":12}
SKIP_NOTE = $ff
encode .macro sheet, default_octave
DELIMITERS = [" "]
last := ""
.for i := 0, i < len(\sheet), i += 1
sym := \sheet[i]
.if !(sym in DELIMITERS)
last ..= sym
.if i != len(\sheet) - 1
.continue
.endif
.endif
.if len(last) == 0
.continue
.endif
octave := \default_octave
note_idx := 0
.if len(last) <= 3
.if last in NOTES
note_idx := NOTES[last]
.else
octave := last[0] - '0'
note_idx := NOTES[last[1:]]
.endif
.else
.error "Note should be defined by <= 3 symbols: " .. repr(last)
.endif
.if octave > 7 || octave < 0
.error "Incorrect octave in note: " .. repr(last)
.endif
; .warn repr(last) .. " = octave: " .. repr(octave) .. " note: " .. repr(note_idx)
.pack octave, note_idx
last := ""
.endfor
.endm
This is used in v1, v2 and v3 macros which represent 3 voices (or different tracks so to say) in SID.
v1 .segment sheet, octave=4
jmp sid__v1_bytes_end
sid__v1_bytes:
.encode \sheet, \octave
sid__v1_bytes_end:
.endm
The play_next_uses_xy
macro is a macro that is used to play the next note in a sequence of musical notes. The XY
in the macro name refers to the X and Y registers.
Conclusion
This is a brief intro into SID programming and a very primitive macro engine to build your music in human-readable format.
Take a look at the main.asm and try to guess which 80s movie OST is it.
Top comments (1)
Nice overview, thank you for covering the topic.