Interrupt driven DMA PWM audio

Ask anything your want about the 32X Mushroom programming.

Moderator: BigEvilCorporation

Post Reply
Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Interrupt driven DMA PWM audio

Post by Chilly Willy » Wed Sep 19, 2012 9:16 pm

This is how you do interrupt driven DMA PWM audio on the 32X. The first thing to ask is, why? If you wish to do more than just audio on the slave sh2, you need to make your audio code interrupt driven. That way when you do something that takes a long time, the audio will interrupt the task as needed to generate audio. Otherwise you are stuck trying to break the tasks into small enough pieces that it doesn't interfere with your polled audio. With DMA'd buffers, that is a decent amount of time, but not enough for some things.

Here's our example binary for folks to try. It works on real hardware and on Fusion 3.64 (remember that 3.63 sounds like crap). If you use Gens/GS release 7 with my DMA PWM modifications, you need to make one more change to pwm.c to use int-driven dma pwm:

Change this

Code: Select all

        if (PWM_Mode & 0x0080)
        {
            // RPT => generate DREQ1 as well as INT
            SH2_DMA1_Request(&M_SH2, 1);
            SH2_DMA1_Request(&S_SH2, 1);
        }
to this

Code: Select all

        if (PWM_Mode & 0x0080)
        {
            // RPT => generate DREQ1 as well as INT
            SH2_DMA1_Request(&M_SH2, 1);
            SH2_DMA1_Request(&S_SH2, 1);

            if ((SH2_Read_Long(&S_SH2, 0xFFFFFF9C) & 7) == 7)
                SH2_Interrupt_Internal(&S_SH2, (SH2_Read_Long(&S_SH2, 0xFFFFFFA8)<<8) | ((SH2_Read_Word(&S_SH2, 0xFFFFFEE2) >> 8) & 0x000F));
        }
Now that you can play the example, UP/DOWN immediately goes to the previous/next songs, RIGHT/LEFT changes the volume up/down (it starts at max volume), and START pauses/resumes.

So how do we do interrupt driven dma pwm audio? First, assign the dma an exception entry in the exception table... I suggest the exception right after the autovectors since it's the first free entry not used by anything else, and easy to find. So your table looks like this at the end:

Code: Select all

        .long   slave_pwm       /* PWM interupt (Level 6 & 7) */
        .long   slave_cmd       /* Command interupt (Level 8 & 9) */
        .long   slave_hbi       /* H Blank interupt (Level 10 & 11 */
        .long   slave_vbi       /* V Blank interupt (Level 12 & 13) */
        .long   slave_rst       /* Reset Button (Level 14 & 15) */
        .long   slave_dma1      /* DMA1 TE INT */
Make sure you're using the slave table, not the master. Now you need the code for that exception.

Code: Select all

!-----------------------------------------------------------------------
! Slave DMA 1 TE INT handler
!-----------------------------------------------------------------------

slave_dma1:
        ! save registers
        sts.l   pr,@-r15
        mov.l   r0,@-r15
        mov.l   r1,@-r15
        mov.l   r2,@-r15
        mov.l   r3,@-r15
        mov.l   r4,@-r15
        mov.l   r5,@-r15
        mov.l   r6,@-r15
        mov.l   r7,@-r15

        mov.l   sd1_handler,r0
        jsr     @r0
        nop

        ! restore registers
        mov.l   @r15+,r7
        mov.l   @r15+,r6
        mov.l   @r15+,r5
        mov.l   @r15+,r4
        mov.l   @r15+,r3
        mov.l   @r15+,r2
        mov.l   @r15+,r1
        mov.l   @r15+,r0
        lds.l   @r15+,pr
        rte
        nop

        .align  2
sd1_handler:
        .long   _slave_dma1_handler
We push the registers that aren't saved by C, then call the C function, slave_dma1_handler(). Before I talk about that function, we need to see how to set up the dma in the slave code.

Code: Select all

void slave(void)
{
    uint16_t sample, ix;

    // init DMA
    SH2_DMA_SAR0 = 0;
    SH2_DMA_DAR0 = 0;
    SH2_DMA_TCR0 = 0;
    SH2_DMA_CHCR0 = 0;
    SH2_DMA_DRCR0 = 0;
    SH2_DMA_SAR1 = 0;
    SH2_DMA_DAR1 = 0x20004034; // storing a long here will set left and right
    SH2_DMA_TCR1 = 0;
    SH2_DMA_CHCR1 = 0;
    SH2_DMA_DRCR1 = 0;
    SH2_DMA_DMAOR = 1; // enable DMA

    SH2_DMA_VCR1 = 72; // set exception vector for DMA channel 1
    SH2_INT_IPRA = (SH2_INT_IPRA & 0xF0FF) | 0x0F00; // set DMA INT to priority 15

    // init the sound hardware
    MARS_PWM_MONO = 1;
    MARS_PWM_MONO = 1;
    MARS_PWM_MONO = 1;
    if (MARS_VDP_DISPMODE & MARS_NTSC_FORMAT)
        MARS_PWM_CYCLE = (((23011361 << 1)/SAMPLE_RATE + 1) >> 1) + 1; // for NTSC clock
    else
        MARS_PWM_CYCLE = (((22801467 << 1)/SAMPLE_RATE + 1) >> 1) + 1; // for PAL clock
    MARS_PWM_CTRL = 0x0185; // TM = 1, RTP, RMD = right, LMD = left

    sample = SAMPLE_MIN;
    /* ramp up to SAMPLE_CENTER to avoid click in audio (real 32X) */
    while (sample < SAMPLE_CENTER)
    {
        for (ix=0; ix<(SAMPLE_RATE*2)/(SAMPLE_CENTER - SAMPLE_MIN); ix++)
        {
            while (MARS_PWM_MONO & 0x8000) ; // wait while full
            MARS_PWM_MONO = sample;
        }
        sample++;
    }

    // initialize mixer
    MARS_SYS_COMM6 = MIXER_UNLOCKED; // sound subsystem running
    fill_buffer(&snd_buffer[0]); // fill first buffer
    slave_dma1_handler(); // start DMA

    SetSH2SR(2);
    while (1)
    {
        if (MARS_SYS_COMM4 == SSH2_WAITING)
            continue; // wait for command

        // do command in COMM4

        // done
        MARS_SYS_COMM4 = SSH2_WAITING;
    }
}
Notice that the VCR for DMA1 is set to 72. That's that exception vector we added to the slave table. Notice that IPRA bits 12 to 8 are set to 0xF (15). That's the exception priority. Tailor that to your needs knowing that CMD is 8, HBlank is 10, VBlank is 12, and the reset button is 14. Other than that, we do a "standard" set up of the audio. We fill the first buffer and call the handler to start off the dma. After that, the transfer-end dma interrupt will continue the process. We then fall in a loop where we look at COMM4 for a command from the Master SH2 or 68K. That's where you stick any tasks that take a long time for the slave to do. Those tasks may/will be interrupted by the dma interrupt as needed to fill buffers and start the next dma operation.

So how about that exception function?

Code: Select all

void slave_dma1_handler(void)
{
    static int32_t which = 0;

    while (MARS_SYS_COMM6 == MIXER_LOCK_MSH2) ; // locked by MSH2

    SH2_DMA_CHCR1; // read TE
    SH2_DMA_CHCR1 = 0; // clear TE

    if (which)
    {
        // start DMA on first buffer and fill second
        SH2_DMA_SAR1 = ((uint32_t)&snd_buffer[0]) | 0x20000000;
        SH2_DMA_TCR1 = num_samples; // number longs
        SH2_DMA_CHCR1 = 0x18E5; // dest fixed, src incr, size long, ext req, dack mem to dev, dack hi, dack edge, dreq rising edge, cycle-steal, dual addr, intr enabled, clear TE, dma enabled

        fill_buffer(&snd_buffer[MAX_NUM_SAMPLES * 2]);
    }
    else
    {
        // start DMA on second buffer and fill first
        SH2_DMA_SAR1 = ((uint32_t)&snd_buffer[MAX_NUM_SAMPLES * 2]) | 0x20000000;
        SH2_DMA_TCR1 = num_samples; // number longs
        SH2_DMA_CHCR1 = 0x18E5; // dest fixed, src incr, size long, ext req, dack mem to dev, dack hi, dack edge, dreq rising edge, cycle-steal, dual addr, intr enabled, clear TE, dma enabled

        fill_buffer(&snd_buffer[0]);
    }

    which ^= 1; // flip audio buffer
}
Note that read TE/clear TE set of lines - those are REQUIRED for dma interrupts to occur properly. Took a while to figure that out. Then it's merely a matter of starting the next DMA on the proper buffer, then calling fill_buffer on the other buffer. We're then done until the next interrupt.

See how easy that is? :lol:

I'll be releasing the full code for the example, along with my XM player, later in the week... maybe the week end.

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Wed Sep 26, 2012 4:58 am

And here is the full source code. There is an arc with my current linker scripts for the compiler, an arc with the xm player library and converter tool, and an arc with the 32X code:

ldscripts-32X.7z
libxmp-v1.1.7z
xmplayer-v1.1

The resultant binaries:
XMP-Doom-Wolf3D.zip

If you have any questions, be sure to ask.

EDIT: 2012-09-27 - Here's an update to the library:
libxmp-v1.2.7z
as well as binaries using various soundfonts:
XMPlayer-Doom-Wolf3D-RSO.7z
Last edited by Chilly Willy on Fri Sep 28, 2012 5:14 am, edited 1 time in total.

ammianus
Very interested
Posts: 124
Joined: Sun Jan 29, 2012 2:10 pm
Location: North America
Contact:

Post by ammianus » Thu Sep 27, 2012 11:20 pm

Just curious, have you changed the linker scripts at all? If so what's different?

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Fri Sep 28, 2012 12:04 am

ammianus wrote:Just curious, have you changed the linker scripts at all? If so what's different?
The 32X script (mars.ld) is probably the same. I included it in case someone was using a REALLY old version of my toolchain. The MD-side script (mars-md.ld) is new... it allows me to compile the MD side separate from the 32X side complete with C/C++ for the MD side code, although you should STILL be concerned with keeping the main loop on the MD side in work ram to avoid bus contention. The MD side code is really changed compared to older 32X stuff, having the ability to also init the CD in Mode 1 for playing CDDA with your 32X software (although in the example, I commented out initializing the CD since this example has no need for CDDA).

Note that although some emulators support Mode 1 for MD emulation, none support Mode 1 in 32X mode although it's exactly the same. That's partly why I did this XM player - I can't count on being able to play CDDA tracks in 32X games on emulators, so if I want SOME kind of music in emulation, I needed XM playing (I already had MOD playing, but I can only convert MIDI to XM, not MOD).

ammianus
Very interested
Posts: 124
Joined: Sun Jan 29, 2012 2:10 pm
Location: North America
Contact:

Post by ammianus » Fri Sep 28, 2012 12:36 am

That's interesting thanks.

One of the things I wanted to explore (maybe after sound :D ) was drawing some of the backgrounds with MD as some games did. Maybe I could take advantage of this structure to be able to that in C.

Can you use SGDK with these?

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Fri Sep 28, 2012 5:12 am

ammianus wrote:That's interesting thanks.

One of the things I wanted to explore (maybe after sound :D ) was drawing some of the backgrounds with MD as some games did. Maybe I could take advantage of this structure to be able to that in C.

Can you use SGDK with these?
You'd have to make some changes to SGDK to get it to work since it uses it's own crt0.s which is incompatible with the one I use. In other words, if you have to ask, no, you can't. :lol:

But I do have various routines setup in the support code on the MD side, like storing data to the MD VRAM. Look at the hw_32x.c file at the MD funtions.

By the way, new version of the library that fixes an issue with instruments using a large relative note at a high octave. Also binaries for various soundfonts. See the second post.

Stef
Very interested
Posts: 3131
Joined: Thu Nov 30, 2006 9:46 pm
Location: France - Sevres
Contact:

Post by Stef » Fri Sep 28, 2012 8:01 am

Chilly Willy wrote: You'd have to make some changes to SGDK to get it to work since it uses it's own crt0.s which is incompatible with the one I use. In other words, if you have to ask, no, you can't. :lol:

But I do have various routines setup in the support code on the MD side, like storing data to the MD VRAM. Look at the hw_32x.c file at the MD funtions.

By the way, new version of the library that fixes an issue with instruments using a large relative note at a high octave. Also binaries for various soundfonts. See the second post.
At some points, in a far far future maybe, i would really like to add both Mega CD and 32X development to SGDK.
Just having extra makefiles as makefile.mcd and makefile.32x to get it compile for these targets :)

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Fri Sep 28, 2012 5:44 pm

Stef wrote:
Chilly Willy wrote: You'd have to make some changes to SGDK to get it to work since it uses it's own crt0.s which is incompatible with the one I use. In other words, if you have to ask, no, you can't. :lol:

But I do have various routines setup in the support code on the MD side, like storing data to the MD VRAM. Look at the hw_32x.c file at the MD funtions.

By the way, new version of the library that fixes an issue with instruments using a large relative note at a high octave. Also binaries for various soundfonts. See the second post.
At some points, in a far far future maybe, i would really like to add both Mega CD and 32X development to SGDK.
Just having extra makefiles as makefile.mcd and makefile.32x to get it compile for these targets :)
Yeah, at some point, I'll probably work on trying to merge the stuff together, and then we can work out the details for the official support. :D

ammianus
Very interested
Posts: 124
Joined: Sun Jan 29, 2012 2:10 pm
Location: North America
Contact:

Post by ammianus » Sat Jan 26, 2013 3:14 pm

I had an issue compiling the xmconvert tool under libxmp for MinGW in Windows.

I got readfile.c:18 undefined reference to 'rindex'

Answer from MinGW-users email archive
index, rindex are non-ANSI version of strchr. strrchr respectivley.
msvcrt.dll exports only the ANSI names.

Change the references to the non-ANSI names to strchr and strrchr
This fixed the linking error, xmconvert is now being built.

In case anyone else runs into this.

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Sun Jan 27, 2013 1:32 am

Thanks for reporting that. It was from the original code. I've changed it on my copy.
:D

ammianus
Very interested
Posts: 124
Joined: Sun Jan 29, 2012 2:10 pm
Location: North America
Contact:

Post by ammianus » Mon Feb 25, 2013 1:26 am

I've finally gotten the libxmp and your slave dma code working in a stripped down version of my own project.

One conflict I had was that we were both using COMM6 to synchronize Master and Slave cpu, in your case for the sound mixer locking/unlocking, in mine for start up synchronization and certain shared memory write access control.

I am afraid to try to change too much with your mixer code since I am not familiar with that. But how to best allow for synchronizing these multiple activities with your existing code to avoid deadlocks?

My workaround has been to remove most of my own locking and unlocking for the time being.

Could one use any of the other COMM ports and leave COMM6 dedicated to mixer?

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Mon Feb 25, 2013 1:50 am

You can use the comm registers however you like... you might want to make a chart of what you want to do and what the program uses, then assign the registers accordingly. That way you don't get them mixed up or used in more than one place. I also hope you worked from my version with sound effects. I'll post it again here so that there's a latest copy in this thread as well.

xmplayer-sfx.zip

That mixes the music from the XM player with sound effects. Press A to start a looped sound effect of rainfall, press C to stop the rain, and press B to start a one-shot sound effect of a hit followed by an "oof". As per the original player, left/right changes the volume, and up/down goes through the list of xm songs.

The mixer function is in crt0.s and mixes an 8-bit signed mono sample into a 16-bit stereo buffer. I explained that in the other thread. The best docs are the code, which is pretty straightforward - the interrupt of the DMA ending starts the next buffer and calls fill_buffer(). The fill_buffer() function locks a simple semaphore for the slave, preventing the master from messing with the sound variables until we unlock it, then mixes the music, then mixes the sound effects, then unlocks the semaphore. Note that the master locks the semaphore before messing with the sound variables, so you shouldn't lock it too long or you will hold off the slave so long that you might get a click in the audio. Do everything you can BEFORE you lock, quickly update the sound vars, then unlock. Again, the example above tells you everything you need to know.

ammianus
Very interested
Posts: 124
Joined: Sun Jan 29, 2012 2:10 pm
Location: North America
Contact:

Post by ammianus » Mon Feb 25, 2013 2:03 am

Thanks, yes I am using the sfx version now, haven't added sfx into my own code, but that's the next step.

Though what I was having trouble determining is whether any of the code in your example is waiting explicitly on certain states that could block, e.g. MIXER_UNLOCKED, as opposed to the absence of other locks/states? Because other code might be "locking" other states e.g. some of the graphics code in my case which needs to process in the slave before the master can use it.


What do you do with the COMM4?
Last edited by ammianus on Mon Feb 25, 2013 3:01 am, edited 1 time in total.

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Mon Feb 25, 2013 2:38 am

Why are you setting the comm register directly to unlocked in the while loop? That's totally unnecessary. You'll notice that new_song() locks, starts the song, and unlocks (the proper way). Setting the register to unlocked in the while loop defeats the whole lock/unlock system. Only whichever cpu locked it last should unlock it.

Also, you're looping the song the wrong way. If you wish a song to loop, set that in xmp_start_song(). In the example, it's

Code: Select all

    song = xmp_start_song(index, 0);
Which means play the song once. It plays it once because it's going to go on to the next song when it's done. To loop the same song, you should change the 0 to N, where 1 to 254 means loop that many times, and 255 means loop forever. You probably want

Code: Select all

    song = xmp_start_song(index, 255);
That's why new_song() stops the music first - if you go to a menu and wish to play a menu song, new_song() will stop the forever looping song, and start the new one. Same for going to a new level - new_song() will stop whatever is playing and start the new song.

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Mon Dec 30, 2013 2:58 am

Here's a new link for xmplayer-sfx.zip.

Post Reply