The next thing we need is a sound mixer so we can test the output of our MOD player. We don't need to care about optimizing it right now, just need something quick so we can start developing the MOD player.
First thing I did was make a Voice structure. I tried to pack it as small as possible to use the least amount of memory.
/********************************************************** * mixing channel structure **********************************************************/ .struct 0 VOICE_SOURCE: .space 4 @ source address (0 = voice disabled) VOICE_REMAIN: .space 3 @ number of samples remaining until end VOICE_FRAC: .space 1 @ source fraction (0.8 fixed point) VOICE_C1: .space 1 @ rate of playback (fractional) VOICE_C2: .space 1 @ rate integer (lower 2 bits), volume (top 6 bits) VOICE_LOOP: .space 2 @ loop length of sample /2 VOICE_SIZE: @ 12 bytes total
12*8 channels = 96 bytes! We also need another thing here, a MIXING buffer! Uh oh, that's going to take up another 128 bytes!
/********************************************************** * MOD_MixBuffer[BUFFER_SIZE/2] (hwords) **********************************************************/ MOD_MixBuffer: .space BUFFER_SIZE
Next up... the basic sound mixer, here's mine:
.arm .align /*********************************************************** * MOD_MixChunk( target, voices ) * * Mix a chunk of audio data and write it to. ***********************************************************/ MOD_MixChunk: push {r4-r11, lr} push {r0} ldr r2,=MOD_MixBuffer @ clear mixing buffer mov r3, #0 mov r4, #0 mov r5, #0 mov r6, #0 stmia r2!, {r3-r6} stmia r2!, {r3-r6} stmia r2!, {r3-r6} stmia r2!, {r3-r6} stmia r2!, {r3-r6} stmia r2!, {r3-r6} stmia r2!, {r3-r6} stmia r2!, {r3-r6} mov r4, r1 @ r4 = voices mov r5, #4 @ r5 = iteration counter mixloop: ldmia r4, {r6,r7,r8} @ r6 = source cmp r6, #0 @ if source == 0 then do next voice beq mix_next_voice mov r9, r7, lsr#24 @ r9 = read bic r7, #0xFF000000 @ r7 = samples remaining lsr r10, r8, #10 and r10, #63 @ r10 = volume lsr r11, r8, #16 @ r11 = loop mov r8, r8, lsl#32-10 mov r8, r8, lsr#32-10 @ r8 = rate ldr r12,=MOD_MixBuffer @ r12 = work buffer mov r14, #64 push {r4-r5} @----------------------------------------------------------------------------------------- mixingloop: ldrh r0, [r12] @ read mix buffer mov r4, r9, lsr#8 @ get integer of read position ldrsb r4, [r6, r4] @ read signed sample mla r0, r4, r10, r0 @ multiply by volume and add to mix buffer strh r0, [r12], #2 @ store mix buffer entry add r9, r9, r8 cmp r9, r7, lsl#8 blt 1f cmp r11, #0 beq end_of_sample add r7, r7, r6 @ remaining += source sub r9, r9, r11, lsl#8 @ read -= loop add r6, r6, r9, asr#8 @ source += read and r9, #255 @ read &= 255 sub r7, r7, r6 @ remaining -= source 1: subs r14, #1 @ decrement counter bne mixingloop @ loop pop {r4-r5} @ restore channels,iteration counter add r6, r9, lsr#8 @ add read position to source sub r7, r9, lsr#8 @ subtract from remaining count orr r7, r7, r9, lsl#24 @ combine remaining | frac str r7, [r4, #VOICE_REMAIN] @ store b mix_next_voice end_of_sample: pop {r4-r5} mov r6, #0 @ CLEAR SOURCE mix_next_voice: str r6, [r4] @ save source add r4, #VOICE_SIZE @ get next voice subs r5, #1 @ decrement iterator bne mixloop @ loop pop {r0} @ pop mov r1, #64/2 @ loop 32 times ldr r2,=MOD_MixBuffer 1: ldr r3, [r2], #4 @ read 2 mixbuffer entries lsr r3, #8 @ pack into 1 word bic r3, #0xFF00 orr r3, r3, r3, lsr#8 strh r3, [r0], #2 @ store word to target subs r1, #1 @ decrement and loop bne 1b pop {r4-r11, lr} @ return bx lr
Wow, that is super inefficient! First of all, it does sample-end checking inside of the mixing loop (that's BAD). It kind of wastes the capability of the multiply there, and it also processes ONE sample each iteration. Later (after the player is working), we will optimize this function to achieve a much more efficient mixer.
Notice that we're not supporting panning here, we're just doing lame stereo. We'll have 2 voice sets. First set will be mixed and output to the left wave buffer, and the next 4 voices will be for the right wave buffer. In a real module player I would never consider leaving out panning support, but I'll do that for this challenge. You can't hear panning without headphones anyway.
Here is the new modified MOD_Routine() with mixing calls.
/*****************************************************************
* MOD_Routine
*
* Work-routine. Called every time half of the wave buffer
* gets processed.
*****************************************************************/
.thumb_func
MOD_Routine:
push {r4-r5} @ preserve registers
push {lr}
ldr r4,=bufferSlice @ if slice == 1 then reset DMA!
ldrb r5, [r4]
cmp r5, #0
beq 1f
bl MOD_ResetDMA
mov r0, #0 @ toggle slice
b 2f
1:
mov r0, #1 @ toggle slice
2:
strb r0, [r4]
lsl r5, #6 @ r5 = slice * 64
ldr r0,=MOD_WaveBufferL @ call mixing function
add r0, r5 @ use left wavebuffer as target
ldr r1,=MOD_VoicesLeft @ left voices as source
ldr r4,=MOD_MixChunk
bl _call_via_r4
ldr r0,=MOD_WaveBufferR @ generate right output
add r0, r5
ldr r1,=MOD_VoicesRight
bl _call_via_r4
pop {r3-r5} @ return arm/thumb
bx r3
To test that all of this works, I put some code in main.c to tell the first voice to play random data in the ROM :). We can start building the MOD player now.
| Previous: Basic Sound Implementation | Contents | Next: Basic MOD Playback |