1. The long journey to 1k real-time ray-tracing
  2. Making of the visual
  3. Making of the music (part1) by ern0
  4. Making of the music (part2) by TomCat
  5. Download page and source code

Making of the music (part 2) by TomCat

PC-Speaker softsynth

Sample

To make the PC speaker work like a PWM device, we set the PIT's channel #2 to operating mode zero (interrupt-on-terminal-count).

 MOV AL,90H
 OUT 43H,AL

Then we can output a 6-bit sample. Value 0 will be the silence, value 63 is the highest volume.

IRQ:
 PUSHA
 MOV AL,2   ; Sample [0...127]
 SHR AL,1   ; scaled to 6-bit
 JZ .1      ; zero means pause (no sound)
 OUT 42H,AL ; out the 6bit sample
.1:
 ...
 POPA
IRET

Tempo and notes

We set the timer counter to value 68 (later on called Divider), because we want 129 bpm for tempo.

 MOV AL,68
 OUT 40H,AL
 MOV AL,0
 OUT 40H,AL

Then the player frequency will be 17,5 kHz.
Hz=105000000/88/Divider,
bpm(low)=Hz*60sec/2/2/2/2/2/2/2/2/2/2/2/2/2/2,
bpm(high)=Hz*60sec/2/2/2/2/2/2/2/2/2/2/2/2/2.

To play musical notes, we need to multiply the carrier wave by some constant values. We can compute these values depending on the frequencies, for example the A4 note computed like this: A_4=(32*256*44000/Hz+50)/100.
I have an inlcude file for notes: notes.inc

Drums

We had great fun with pc-speaker softsynth drums in a prod released earlier.
And this very simple drum pattern (bd - ht - sn/bd - ht) was used in another intro too, but with a different tempo: qblove

Bass drum is a simple division: A constant value divided by the time (or timer counter).

KICKDRUM:
 MOV AX,16128   ; constant value
 CWD            ; extend the value to 2 words
 MOV CX,8191    ; get some lower bits of
 AND CX,SI      ; the timer counter (SI)
 INC CX         ; avoiding division by zero
 DIV CX         ; get one bit only
 AND AL,64      ; 0 or 64 will be the volume (the sample)

Comparing an SN (on the left) to a BD (on the right), the snare is much longer, and more random.

Hihat also needs some random noise, so HT and SN starts with the same code snippet.

 XCHG AX,DI    ; DI: pointer to video memory, which is quite random here :)
 MUL SI        ; SI: the timer counter
 XCHG AX,DX    ; AX: random number depending on time
 AND AX,32     ; get only one random bit

Chords

One chord consists of four summed notes.

 MOV BP,NOTES
CHORDS:
 SALC            ; AL: zero
 MOV AH,[BP+DI]  ; AX: pitch F#, ...
 MUL SI          ; SI: the timer counter
 AND DX,1FH      ; DX: get a saw wave sample
 ADD BH,DL       ; BH: sample (sum of the notes)
 INC DI          ; next note
 TEST DI,3       ; loop 4x
 JNZ CHORDS

First, the idea of making a cover of a popular song in a restricted environment is great fun. That is why the length of the notes is other than power of 2.

The lengths of the chords are 7, 8 and 17. I had to convert this lengths to 8, 8 and 16. So I shifted the start, and put silence at the last beat.

 TEST SI,1111000000000000B
 JZ SKIP               ; no chords at the last beat
 ...                   ; chords here
DRUMS:
 SUB SI,1000000000000B ; shifting back the drums by one beat

But after it sucked at Revision, we've changed the music. I believe ern0 found much better chords. I prefer the new song's mood.

Pattern

The stored data for musical notes is almost always shrinkable. The raw data is 32 bytes long:

NOTES:
 DB A_3,B_3,D_4,Fs4
 DB B_3,D_4,E_4,Gs4
 DB Cs4,E_4,Fs4,A_4
 DB Cs4,E_4,Fs4,A_4

 DB B_3,D_4,Fs4,B_4
 DB B_3,Cs4,E_4,Gs4
 DB Cs4,E_4,Fs4,A_4
 DB Cs4,E_4,Fs4,A_4

Here is the simple decoder:

 MOV DI,SI
 SHRD DI,BX,2+16-5
 AND DI,7*4

Look, we can save 12 bytes on the data...

NOTES:
 DB A_3,B_3,D_4,Fs4
 DB B_3,D_4,E_4,Gs4

 DB B_3,D_4,Fs4,B_4
 DB B_3,Cs4,E_4,Gs4

 DB Cs4,E_4,Fs4,A_4

and we lost only 7 bytes on the decoder.

 MOV DI,4*4
 SHR BL,1
 JC .1
 MOV DI,AX
 SHLD DI,SI,3
 AND DI,3*4
.1:

Envelopes

We have two envelopes. Simple ADSR: no attack, just release.

Preparing CL and CH registers before the chord loop:

 MOV AX,BX       ; BL: pattern
 MOV CX,3
 SHR AL,1
 JC .1
 MOV CH,128
.1:
 DEC AX          ; skipping the drums-only intro
 SHR AL,1
 AND CL,AL

One envelope for tone...

 IMUL AX,SI,-16
 OR AL,111B
 ROR AX,CL
 MUL BX          ; DH: sample



and one for the volume...

 IMUL AX,SI,-1
 OR AH,CH        ; CH: 128 or 0
 MUL DX          ; DH: sample

Mastering

After adding together every instrument, drum, and chord, I test the maximum volume. I always store the debug information to memory, and print it using an external util.

if maxvol=1
 CMP [FS:8000H],BH
 JAE .1
 MOV [FS:8000H],BH
.1:
end if

This time the maximum sample value was 181. So I will scale down to 7 bits only.

 MOV AL,180       ; vol 181 -> vol 127
 MUL BH           ; mastering
 XCHG BX,AX
DONE:
 MOV [BP+IRQ.SAMPLE-1],BH

But at the cutoff part, I skip this downscaling, and the chords become even louder.

Here is the whole generated song:


If you liked this writeup, then leave a comment at the download page :)
And make sure you have also read the History of Chrome Revenge's born by TomCat