Simple Sound Mixer

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 ImplementationContentsNext: Basic MOD Playback