Need help testing 32X Sandbox

Announce (tech) demos or games releases

Moderator: Mask of Destiny

KillaMaaki
Very interested
Posts: 84
Joined: Sat Feb 28, 2015 9:22 pm

Need help testing 32X Sandbox

Post by KillaMaaki » Wed Jul 26, 2017 8:54 pm

Hello,
I'm doing some stuff on the 32X to feel my way around the hardware and prototyping individual components that would be useful to a game.
Currently working on music and sound playback using the 32X PWM audio. Now, I don't own an actual 32X and money's kinda tight so up until now I've just been testing in Kega Fusion and occasionally tossing a ROM over to a guy I know on Discord who DOES have a 32X.
Unfortunately, once I got to working on audio, he reported that no sound was being produced whatsoever. Even worse, once I added sound effects, trying to play a sound effect froze the whole damn thing on his 32X. Trying to step through my PWM code with Chilly so far hasn't yielded any significant errors on my part (though it could be in my song loading/playback code rather than my PWM code, but I can't figure out how that'd be the case)
So to rule out of the possibility of faulty hardware I need help testing this on other 32Xs. Ideally, I'd love to get feedback on results (if music is heard playing), whether the demo plays sounds properly or not (A, B, and C buttons should play sounds. It'll be easy to tell if it freezes - there's a bouncing face thingy), and what hardware configuration is used (Genesis model, 32X model, audio cable setup, whether any other peripherals are attached, etc).

If anyone can test this on real hardware and report back, I'd be very grateful!
Attachments
OutRom.zip
(234.3 KiB) Downloaded 239 times

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

Re: Need help testing 32X Sandbox

Post by Chilly Willy » Wed Jul 26, 2017 9:37 pm

Yeah, does the same thing on my 32X. No music and when you press the trigger, it freezes solid. You definitely have a bug in the sound code somewhere. You might check the mixer code.

Oh, one other thing to check - the "semaphore" code. It's possible it's not working on real hardware since you have real CPUs with real caches.

KillaMaaki
Very interested
Posts: 84
Joined: Sat Feb 28, 2015 9:22 pm

Re: Need help testing 32X Sandbox

Post by KillaMaaki » Wed Jul 26, 2017 9:55 pm

Oh shit. I completely forgot about caching entirely. That's gonna yield a lot of extra investigation, but I do have something to go on now potentially.

EDIT: Also I think in the next build I'll have it log song header info to the screen, plus maybe a random event from somewhere in the middle of the song, to ensure that it is loading song info correctly.

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

Re: Need help testing 32X Sandbox

Post by Chilly Willy » Wed Jul 26, 2017 11:03 pm

Caching can cause interesting problems on real hardware that never occur in emulators. My sound code tends to flush the cache lines associated with the memory occupied by the mixer structures when set for sound effects, and flush the memory used for song structures when switching songs. Forgetting to flush the cache made for some interesting fun in those cases when I first started on coding sound using the slave SH2. :D

This is a specific case of trying to use sdram data shared between processors. Writing the data isn't an issue since writing does cache-through, writing to both the cache and the memory at the same time. Reading is the issue - you need to flush that memory from the cache before reading. Even if the memory is set as non-cachable, you STILL need to flush the cache or it will come from the cache. The cache/no-cache bits in the pointer are only considered if the data is not found in the cache.

Also, while you might think the easiest thing to do is simply purge the cache, this will also purge your code, slowing down things tremendously. Purging once right at the start of a song is fine. Purging for each new sfx started is NOT fine.

Oh, and be sure to align structures and buffers for cache lines... 16 bytes. For example

Code: Select all

static uint8_t __attribute__((aligned(16))) data[MAX_CHUNK_SIZE];
Makes it much easier to flush. If you're using the cache functions I include with my examples, you can flush a variable using something like

Code: Select all

    CacheClearLine(&music_playing);
and purging the cache at the start of a song can be done like this

Code: Select all

    // purge the cache
    CacheControl(0); // cache disabled
    CacheControl(SH2_CCTL_CP | SH2_CCTL_CE); // cache purge and enable

KillaMaaki
Very interested
Posts: 84
Joined: Sat Feb 28, 2015 9:22 pm

Re: Need help testing 32X Sandbox

Post by KillaMaaki » Thu Jul 27, 2017 2:42 am

OK, so just so I'm understanding this properly...

1.) Currently, loading a new song entails grabbing a pointer to the song data in ROM, reading the header data to determine number of events, malloc'ing a new song - as well as freeing the last song if any - memcpy'ing the header data, and then manually parsing the remaining bytes into a SongEvent array. I'm now doing a CacheClearLine( &song ) just before my check that the previous song isn't null, and calling free( song ) if not null. After that, as this is the very start of the song, I can purge the cache and not have to worry about cache clearing individual state variables, correct?

2.) After that, I understand that I should not purge the cache, but clear individual cache lines as necessary. How many places / how often should I be doing this? Does it make sense to do it anywhere something could change? If the slave CPU is the only CPU that will ever be changing and reading a variable, does it have to worry about the cache at all? Is there any worry of a slowdown from doing that too much, or would it just be possibly unnecessary but not harmful either?

EDIT: Attached a version of the ROM which has a ton of CacheClearLine calls sprinkled around (maybe too many? just about every place it accesses voices it cache clears the voice pointer, and also at the beginning of every buffer it cache clears all playback state information like song pointer, current event index, current event ticks, is playing, etc. also completely purges the cache after loading a new song)

EDIT: Changed ROM for debug info. Should display number of events in song (4453) and number of patches used (8). Also displays info about event #800 in the song (event type = 1, channel = 6, param1 = 209, param2 = 118, delay = 0). Pressing buttons should log messages to the screen. It logs the button press message ("A pressed...") before playing the sound, and then logs the rest of the message after ("Playing sound 0"), so if it freezes when playing the sound presumably you would only see the first half of the message logged.
Attachments
OutRom.zip
(234.89 KiB) Downloaded 217 times

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

Re: Need help testing 32X Sandbox

Post by Chilly Willy » Thu Jul 27, 2017 6:18 pm

1) I hope you're not calling malloc/free from the slave! Bad things will happen as the heap belongs to the master. You need a separate heap with separate allocator calls to allocate memory from the slave. If the master is doing all this, the slave will need to flush all data affected, or purge the cache. Purging the cache will deal with all variables/arrays, but do it too often and you'll spend all your time refilling the cache with code.

2) You need to flush any cache lines that use data written by the master. Anything else written by the slave is fine not flushed... unless it's affected by interrupt code. Then it not only needs to be flushed and non-cachable, it needs to be volatile as well.

The rom prints

Song loaded!
Num evenets: 4453
Num patches: 8

Event #800 info:
Event type: 1
Channel: 6
Param1: 209
Param2: 118
Delay: 0

There's no sound at all during this. When I press A, it then prints

A pressed...

Still no sound. It's also now hung at this point.

KillaMaaki
Very interested
Posts: 84
Joined: Sat Feb 28, 2015 9:22 pm

Re: Need help testing 32X Sandbox

Post by KillaMaaki » Thu Jul 27, 2017 7:58 pm

Oh. Yeah, I am. I could just move the song loading over to the master so the master's responsible for doing the malloc. Alternatively I could pre-allocate the maximum size of song I expect to have upfront

Interestingly, it seems that the song is being loaded just fine - the fact that it's correctly loading an event somewhere in an arbitrary position of the song (compared between emulator and hardware) would indicate that I think. So aside from the fact that I should fix the malloc, seems like song loading is working as intended. That leaves song playback, then. I wonder if there's a way I can debug what the current event and remaining delay is at any given time... as well as perhaps what each voice is doing.

KillaMaaki
Very interested
Posts: 84
Joined: Sat Feb 28, 2015 9:22 pm

Re: Need help testing 32X Sandbox

Post by KillaMaaki » Fri Jul 28, 2017 3:31 am

OK, so here's a new build. The main SH2 is now responsible for the song loading and the malloc. Additionally, I now grab some debug playback state info from the song engine and display it on the screen (displays the current event, the current ticks, and the state of each voice). This is super laggy because of all the text being printed out, but as far as I can tell should not affect the slave CPU. Also, I do not lock the mixer while getting the debug info, in case attempting to grab a mixer lock is causing a hang (as far as I can tell, because I'm not trying to change the state variables whatsoever this should be safe, I'm just grabbing them for debug display purposes).
Pressing buttons doesn't display anything in this build. But it displays frame count too, so a hang should be obvious anyway (frame counter will stop counting up)
Attachments
OutRom.zip
(235.36 KiB) Downloaded 208 times

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

Re: Need help testing 32X Sandbox

Post by Chilly Willy » Fri Jul 28, 2017 4:41 pm

You can do things like set the comm registers to values that tells the 68000 to print messages. You can set the background color register in the VDP. Things like that.

This rom instantly went to
Current event: 326
Current delay: 1399

So you clearly have some problem in processing the delays. The frame count starts at 0 and seems to advance about 2 per second. It's currently 1800 and climbing.

Voice 0 to 7 are 1, and 8 to 15 are 0.

As soon as I press a button, it halts. The frame is now stuck at 2460.

KillaMaaki
Very interested
Posts: 84
Joined: Sat Feb 28, 2015 9:22 pm

Re: Need help testing 32X Sandbox

Post by KillaMaaki » Fri Jul 28, 2017 7:11 pm

Well that's bizarre. It should jump to 326-ish and then keep going (there's a lot of zero delay events at the start). So it keeps processing events perfectly fine right up until 326... and then it just halts there??? o.O

OK so... that event would be event type 0, so it stops a note playing. And that looks like:

Code: Select all

// stop a voice playing in a channel
Voice* voice = channel->ActiveNotes[ event.Data1 ];
keyOff( voice );
And then there's the event processing, which looks like this:

Code: Select all

int remainder = numSamples;

// process remaining delay first
if( currentTicks > 0 )
{
	int len = MIN( currentTicks, remainder );
	processBuffer( buffer, len );
	currentTicks -= len;
	remainder -= len;
	buffer += ( len << 1 );
}
	
// keep processing events as long as we have buffer remaining
while( remainder > 0 )
{
	// process next event
	SongEvent event = song->EventList[ currentEvent++ ];
	processEvent( event );
	
	// mix until the end of the delay, or until the end of the buffer
	
	// convert midi ticks into audio samples
	currentTicks = ( event.Delay * ticksToSamples ) >> 14;
	if( currentTicks > 0 )
	{
		int len = MIN( currentTicks, remainder );
		processBuffer( buffer, len );
		currentTicks -= len;
		remainder -= len;
		buffer += ( len << 1 );
	}
}
This called every time the audio buffer is filled. There's two possibilities I see here:

- It hangs the minute it encounters a non-zero-delay event, which causes it to call processBuffer. That doesn't make sense to me, because from what you've said it stops on event 326. It doesn't even fetch an event if there's a delay it's processing, and the very previous event, event 325, does have a delay. So it processed the delay of 325 first, then when it got to 326 it stopped (event 325 just so happens to be the first event in the song that has a delay, btw)
- The other possibility is that event 326 hangs it on the processEvent line. This seems weird to me as well, but it does retrieve the voice from the channel's voice pointer table (one pointer per song note in the 0-255 range). Maybe the pointer is null or something and it just throws a hissy? I've added a null check now, that version is attached.
Attachments
OutRom.zip
(235.43 KiB) Downloaded 210 times

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

Re: Need help testing 32X Sandbox

Post by Chilly Willy » Sat Jul 29, 2017 3:02 am

Acts exactly like the other - same info on screen.

And your processing event codes in the music makes no sense at all! It SHOULD look something like this:

The following code is called from the pwm dma int, where the length of the sound buffer is set to the number of samples per 140Hz tick (or 70 or 35 Hz ticks... most music still sounds good when processed at 35Hz instead of 140). You would vary the length when you hit one of your tempo commands you added, but the code to process the score should be VERY much like this.

Code: Select all

static void Handle_MUS(void)
{
    CacheClearLine(&music_playing);

    // process music if playing
    if (music_playing)
    {
        if (music_playing < 0)
        {
            music_playing = 0;                    // music now off
        }
        else
        {
            int32_t i, j;

            if (music_playing > 1)
            {
                music_playing = 1;

                // purge the cache
                CacheControl(0); // cache disabled
                CacheControl(SH2_CCTL_CP | SH2_CCTL_CE); // cache purge and enable

                // start music from beginning
                (*midi_dev->tracks)[0].track_ptr = (*midi_dev->tracks)[0].track;
                // default channel settings
                for (i=0; i<16; i++)
                {
                    midi_dev->channels[i].instrument = 0;
                    midi_dev->channels[i].bank = (i == 15) ? 120 << 7 : 121 << 7;
                    midi_dev->channels[i].modulation = 0;
                    midi_dev->channels[i].port_time = 0;
                    midi_dev->channels[i].rpdh = 0;
                    midi_dev->channels[i].rpdl = 0;
                    midi_dev->channels[i].volume = 100 << 7;
                    midi_dev->channels[i].pan = 0x40 << 7;
                    midi_dev->channels[i].expression = 0x7F << 7;
                    midi_dev->channels[i].sustain = 0;
                    midi_dev->channels[i].portamento = 0;
                    midi_dev->channels[i].sostenuto = 0;
                    midi_dev->channels[i].soft = 0;
                    midi_dev->channels[i].port_note = 255;
                    midi_dev->channels[i].reverb = 40;
                    midi_dev->channels[i].chorus = 0;
                    midi_dev->channels[i].rpn = (0x7F << 7) | 0x7F;
                    midi_dev->channels[i].pressure = 0x40;
                    midi_dev->channels[i].pitch = 0x40 << 7;
                    midi_dev->channels[i].prange = 2 << 7;
                    midi_dev->channels[i].ftune = 0x40 << 7;
                    midi_dev->channels[i].ctune = 0x40 << 7;
                    midi_dev->channels[i].mrange = 0x40;
                    midi_dev->channels[i].drumkit = 0;
                }
                set_mus_samples(315 * BEATS_PER_PASS * SAMPLE_RATE / 44100);
            }

            MUS_DELAY -= BEATS_PER_PASS;        // 1 = 140Hz, 2 = 70Hz, 4 = 35Hz
            if (MUS_DELAY > 0)
                return;

            do
            {
                int32_t next_time;
                uint8_t event;
                uint8_t note;
                uint8_t time;
                uint8_t ctrl;
                uint8_t value;
                uint8_t channel;
                uint8_t volume;

                do
                {
                    event = MUS_NEXT;
                    channel = event & 15;
                    switch((event >> 4) & 7)
                    {
                    case 0:                     // Release
                        note = MUS_NEXT & 0x7F;
                        for (j=0; j<NUM_MUS_MIXERS, j++)
                            if (midi_dev->voices[j].chan == channel && midi_dev->voices[j].note == note)
                                break;
                        if (j < NUM_MUS_MIXERS)
                        {
                            midi_dev->voices[j].flags |= VCF_NOTEOFF;
                            midi_dev->voices[j].velocity = 64; // no release velocity -> use default
                        }
                        break;
                    case 1:                     // Play note
                        note = MUS_NEXT;
                        volume = 255;           // don't change volume
                        if (note & 0x80)
                        {                       // set volume as well
                            note &= 0x7F;
                            volume = MUS_NEXT;
                        }
                        // find mixer/voice (linked)
                        for (j=0; j<NUM_MUS_MIXERS; j++)
                            if (midi_dev->available & (1 << j))
                                break; // found free mixer/voice
                        if (j == NUM_MUS_MIXERS)
                            for (j=0; j<NUM_MUS_MIXERS; j++)
                                if (midi_dev->voices[j].flags & VCF_NOTEOFF)
                                    break; // found voice in key-off state
                        if (j < NUM_MUS_MIXERS)
                        {
                            midi_dev->available &= ~(1 << j); // mixer in use
                            midi_dev->voices[j].note = note; // voice in use
                            midi_dev->voices[j].chan = channel; // back link
                            midi_dev->voices[j].pressure = midi_dev->channels[i].pressure;
                            midi_dev->voices[j].velocity = 64; // no velocity, use default
                            midi_dev->voices[j].port_note = midi_dev->channels[i].port_note;
                            midi_dev->voices[j].flags = VCF_ACTIVE|VCF_NOTEON; // key-on voice
                        }
                        if (volume < 128)
                            midi_dev->channels[i].volume = volume << 7;
                        midi_dev->channels[i].port_note = 255; // reset after note on
                        break;
                    case 2:                     // Pitch
                        value = MUS_NEXT;
                        mus_channel[channel].pitch = (value << 6); // all 8 bits significant
                        break;
                    case 3:                     // System Event
                        ctrl = MUS_NEXT;
                        // if the control takes a value, it is 0
                        switch (ctrl & 0x7F)
                        {
                        case 0:
                            // set channel instrument
                            if ((midi_dev->channels[channel].bank >> 7) != 120)
                            {
                                // melodic channel
                                midi_dev->channels[channel].instrument = 0;
                            }
                            else
                            {
                                // percussive channel
                                midi_dev->channels[i].drumkit = 0;
                            }
                            break;
                        case 1:
                            // set channel bank LSB - hardcode MSB to 120/121 (drum/melody)
                            midi_dev->channels[channel].bank = ((channel == 15) ? 120 : 121) << 7;
                            break;
                        case 2:
                            // set channel modulation MSB
                            midi_dev->channels[channel].modulation = 0;
                            break;
                        case 3:
                            // set channel volume
                            midi_dev->channels[channel].vol = 0;
                            break;
                        case 4:
                            // set channel pan MSB
                            midi_dev->channels[channel].pan = 0;
                            break;
                        case 5:
                            // set channel expression MSB
                            midi_dev->channels[channel].expression = 0;
                            break;
                        case 6:
                            // set channel reverb send level
                            midi_dev->channels[channel].reverb = 0;
                            break;
                        case 7:
                            // set channel chorus send level
                            midi_dev->channels[channel].chorus = 0;
                            break;
                        case 8:
                            // set channel sustain switch
                            midi_dev->channels[channel].sustain = 0;
                            break;
                        case 9:
                            // set channel soft switch
                            midi_dev->channels[channel].soft = 0;
                            break;
                        case 10:
                            // all sounds off
                            for (j=0; j<NUM_MUS_MIXERS; j++)
                            {
                                midi_dev->mixers[j].data = NULL; // mixer free
                                midi_dev->voices[j].flags = 0; // stopped
                                midi_dev->voices[j].note = 255; // available
                            }
                            midi_dev->available = ALL_MIXERS;
                            break;
                        case 11:
                            // all notes off
                            for (j=0; j<NUM_MUS_MIXERS; j++)
                                if (midi_dev->voices[j].flags & VCF_ACTIVE)
                                    midi_dev->voices[j].flags |= VCF_NOTEOFF;
                            break;
                        case 12:
                            // mono on
                        case 13:
                            // poly on
                            // just do all notes off
                            for (j=0; j<NUM_MUS_MIXERS; j++)
                                if (midi_dev->voices[j].flags & VCF_ACTIVE)
                                    midi_dev->voices[j].flags |= VCF_NOTEOFF;
                            break;
                        case 14:
                            // reset all controllers
                            for (j=0; j<16; j++)
                            {
                                // actually doesn't reset ALL controllers, just these
                                midi_dev->channels[j].modulation = 0;
                                midi_dev->channels[j].expression = 0x7F << 7;
                                midi_dev->channels[j].sustain = 0;
                                midi_dev->channels[j].portamento = 0;
                                midi_dev->channels[j].sostenuto = 0;
                                midi_dev->channels[j].soft = 0;
                                midi_dev->channels[j].rpn = (0x7F << 7) | 0x7F;
                                midi_dev->channels[j].pressure = 0;
                                midi_dev->channels[j].pitch = 0x40 << 7;
                            }
                            break;
                        }
                        break;
                    case 4:                     // Change control
                        ctrl = MUS_NEXT;
                        value = MUS_NEXT;
                        switch(ctrl & 0x7F)
                        {
                        case 0:
                            // set channel instrument
                            if ((midi_dev->channels[channel].bank >> 7) != 120)
                            {
                                // melodic channel
                                midi_dev->channels[channel].instrument = value;
                            }
                            else
                            {
                                // percussive channel
                                if (value > 127)
                                {
                                    value -= 100; // MUS percussion to MIDI note
                                    midi_dev->channels[channel].instrument = value;
                                }
                                else
                                    midi_dev->channels[i].drumkit = value;
                            }
                            break;
                        case 1:
                            // set channel bank LSB - hardcode MSB to 120/121
                            midi_dev->channels[channel].bank = ((channel == 15) ? 120 : 121) << 7;
                            midi_dev->channels[channel].bank |= value & 0x7F;
                            break;
                        case 2:
                            // set channel modulation MSB
                            midi_dev->channels[channel].modulation = (value & 0x7F) << 7;
                            break;
                        case 3:
                            // set channel volume MSB
                            midi_dev->channels[channel].vol = (value & 0x7F) << 7;
                            break;
                        case 4:
                            // set channel pan MSB
                            midi_dev->channels[channel].pan = (value & 0x7F) << 7;
                            break;
                        case 5:
                            // set channel expression MSB
                            midi_dev->channels[channel].expression = (value & 0x7F) << 7;
                            break;
                        case 6:
                            // set channel reverb send level
                            midi_dev->channels[channel].reverb = value & 0x7F;
                            break;
                        case 7:
                            // set channel chorus send level
                            midi_dev->channels[channel].chorus = value & 0x7F;
                            break;
                        case 8:
                            // set channel sustain switch
                            midi_dev->channels[channel].sustain = value & 0x7F;
                            break;
                        case 9:
                            // set channel soft switch
                            midi_dev->channels[channel].soft = value & 0x7F;
                            break;
                        case 10:
                            // all sounds off (if value 0)
                            if (!value)
                            {
                                for (j=0; j<NUM_MUS_MIXERS; j++)
                                {
                                    midi_dev->mixers[j].data = NULL; // mixer free
                                    midi_dev->voices[j].flags = 0; // stopped
                                    midi_dev->voices[j].note = 255; // available
                                }
                                midi_dev->available = ALL_MIXERS;
                            }
                            break;
                        case 11:
                            // all notes off (if value == 0)
                            if (!value)
                                for (j=0; j<NUM_MUS_MIXERS; j++)
                                    if (midi_dev->voices[j].flags & VCF_ACTIVE)
                                        midi_dev->voices[j].flags |= VCF_NOTEOFF;
                            break;
                        case 12:
                            // mono on (value = number channels)
                        case 13:
                            // poly on (if value == 0)
                            // just do all notes off
                            for (j=0; j<NUM_MUS_MIXERS; j++)
                                if (midi_dev->voices[j].flags & VCF_ACTIVE)
                                    midi_dev->voices[j].flags |= VCF_NOTEOFF;
                            break;
                        case 14:
                            // reset all controllers (if value 0)
                            if (!value)
                                for (j=0; j<16; j++)
                                {
                                    // actually doesn't reset ALL controllers, just these
                                    midi_dev->channels[j].modulation = 0;
                                    midi_dev->channels[j].expression = 0x7F << 7;
                                    midi_dev->channels[j].sustain = 0;
                                    midi_dev->channels[j].portamento = 0;
                                    midi_dev->channels[j].sostenuto = 0;
                                    midi_dev->channels[j].soft = 0;
                                    midi_dev->channels[j].rpn = (0x7F << 7) | 0x7F;
                                    midi_dev->channels[j].pressure = 0;
                                    midi_dev->channels[j].pitch = 0x40 << 7;
                                }
                            break;
                        }
                        break;
                    case 5:                     // End of measure
                        break;
                    case 6:                     // End score
                        // song done - stop all mixers, clear all voices
                        for (j=0; j<NUM_MUS_MIXERS; j++)
                        {
                            midi_dev->mixers[j].data = NULL;
                            midi_dev->voices[j].flags = 0; // stopped
                            midi_dev->voices[j].note = 255; // available
                        }
                        midi_dev->available = ALL_MIXERS;

                        if (midi_dev->looping)
                        {
                            music_playing = 2;    // start from beginning
                        }
                        else
                        {
                            music_playing = 0;    // stop
                        }
                        return;
                        break;
                    case 7:                     // Unused, but one data byte
                        value = MUS_NEXT;
                        break;
                    }

                } while (!(event & 0x80));      // not last event

                // now get the time until the next event(s)
                next_time = 0;
                time = MUS_NEXT;
                while (time & 0x80)
                {
                    // while msb set, accumulate 7 bits
                    next_time |= (int32_t)(time & 0x7f);
                    next_time <<= 7;
                    time = MUS_NEXT;
                }
                next_time |= (int32_t)time;
                MUS_DELAY += next_time;

            } while (MUS_DELAY <= 0);
        }
    }
}
where

Code: Select all

#define MUS_NEXT *(*midi_dev->tracks)[0].track_ptr++
#define MUS_DELAY (*midi_dev->tracks)[0].track_delay

KillaMaaki
Very interested
Posts: 84
Joined: Sat Feb 28, 2015 9:22 pm

Re: Need help testing 32X Sandbox

Post by KillaMaaki » Sat Jul 29, 2017 3:51 am

I'm not sure why it doesn't make sense? Could you elaborate? The whole design of that code is to deal with mixing segments of an audio buffer with an arbitrary tickrate and arbitrary delay values, where the tickrate may not (and indeed usually does not) divide evenly into the audio buffer (and the tickrate is *completely* arbitrary, unlike the standard MUS format).

And that still doesn't explain why it doesn't work.

Here's two more builds. Both have sound effects disabled. One build literally just sits there incrementing the current event index and resetting the timer delay ticks. It does no audio processing. It should just sit there with a steadily increasing "Current event:" value.
The other build still does no audio processing or song event processing, but fetches events from the song's event list and uses their delay values (and if the event type is 6, resets current event index back to zero).

These cases are honestly at this point just sanity checks to ensure that I'm not doing anything wrong in my delay processing somehow, though I can't for the life of me figure out how that would be the case.
Attachments
OutRom_SongEvent.zip
(234.77 KiB) Downloaded 206 times
OutRom_ConstantEvent.zip
(234.73 KiB) Downloaded 198 times

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

Re: Need help testing 32X Sandbox

Post by Chilly Willy » Sat Jul 29, 2017 3:26 pm

Let's deal with the roms first. The Constant Event rom smoothly progresses through the events while the frames advance. The delays are all rather random, so it looks like they read right. The events are over 400, so I don't think this is the problem.

The Song Event rom sticks at event 0 while HUGE delays count down (from like 210000). The frame advances.

Now, let's talk about computer music. The tracker format started simply - you had one track per audio channel, and the score was processed once per vblank int (Euro... 50Hz; for NTSC, they simply skipped one int per 6 vblanks). The vblank tick was a fixed beat rate. When they eventually wanted higher/variable beat rates, they switched to using one of the CIA timers (on the Amiga), or repurposed the 13.8Hz PIT timer on the PC. The score was still processed once per tick, but now the tick rate could vary, though 125 bpm tended to be the standard default value for the tempo.

For these old players, the instruments simply ran constantly in the background, looping if they were a looped instrument, while the score would change the volume or frequency or instrument asynchronously at the tempo. Let's look at a simple example: the SCD PCM chip. The gate array has one timer that can be used by programs for timing. You would set the tempo like this

Code: Select all

| void pcm_set_timer(uint16_t bpm);
| TIMER = 32552 / (bpm * 2) - 1
|   should be 32552 * 5 / (bpm * 2) - 1, but that's too big for TIMER, so
|   we leave out the * 5 and compensate in the int handler
    .global pcm_set_timer
pcm_set_timer:
    move.l  #32552,d0
    move.l  4(sp),d1
    cmpi.l  #64,d1
    bhs.b   0f
    moveq   #64,d1                  /* lowest tempo TIMER can handle */
0:
    add.w   d1,d1
    divu.w  d1,d0
    subq.w  #1,d0
    ble.b   1f                      /* safety check */
    move.w  d0,TIMER.w
1:
    rts
Notice how our timer can't handle too large a value, so I left out the *5 you'd normally use in calculating the tempo. To compensate, I multiply the event delays by 5. So the tempo is 5X as fast, but the delays are 5X as long, so it evens out. You may or may not need to work around issues like this on various platforms.

The first thing the timer int callback does is process the score

Code: Select all

  if (gMod)
    PlayMOD(gMod);
This is the regular MOD callback entry standard for all mod players. Then it processes any channel handling that may be needed after the score processes events. We start by checking if the the tempo or timescale changed. Normally you'd just have tempo, but due to limits in the ram for the PCM in the SCD, I added a timescale for when instruments are resampled to fit in ram.

Code: Select all

  if ((tempo != gMod->BeatsPerMinute) || (scale != gMod->TimeScale))
  {
    tempo = gMod->BeatsPerMinute;
    scale = gMod->TimeScale;

    uint16_t bpm = 0;
    if (scale & 0x20)
      bpm += (tempo << 1);
    if (scale & 0x10)
      bpm += tempo;
    if (scale & 0x08)
      bpm += (tempo >> 1);
    if (scale & 0x04)
      bpm += (tempo >> 2);
    if (scale & 0x02)
      bpm += (tempo >> 3);
    if (scale & 0x01)
      bpm += (tempo >> 4);

    pcm_set_timer(bpm);
  }
Now handle the pcm channels

Code: Select all

  /* update channels */
  for (i = 0; i < gMod->NumberOfChannels; i++)
  {
    pcm_set_ctrl(0xC0 + i); // turn on pcm chip and select channel

    if (voices[i].pending & PENDING_STOP)
    {
      pcm_set_off(i);
      pcm_set_env(0);
      voices[i].pending &= ~PENDING_STOP;
    }

    if (voices[i].pending & PENDING_START)
    {
      pcm_set_off(i);
      pcm_set_start(voices[i].start, voices[i].offset);
      pcm_set_loop((voices[i].start << 8) + voices[i].loop);
      voices[i].pending |= PENDING_VOL|PENDING_PER;
    }

    if (voices[i].pending & PENDING_VOL)
    {
      pcm_set_env(voices[i].vol);
      pcm_set_pan(voices[i].pan);
      voices[i].pending &= ~PENDING_VOL;
    }

    if (voices[i].pending & PENDING_PER)
    {
      pcm_set_period(voices[i].period);
      voices[i].pending &= ~PENDING_PER;
    }

    if (voices[i].pending & PENDING_START)
    {
      pcm_set_on(i);
      voices[i].pending &= ~PENDING_START;
    }
  }
If the score turned off a channel, we stop it. If the score changed the volume, we set the channel volume. If the score changed the period or started a channel, we set the channel period. If the score started a channel, we now start it (the start, loop, volume, and period have been set at this point).

MUS was designed to play almost identically to MOD so that the standard methods of playback could be adapted. Only the score handling changed. The tempo was fixed at 140Hz (or 70Hz for one game). You process the score once per tempo tick, subtracting one from any delay until it's 0, then process any events. All the while, the sound channels run in the background.

To make timing on the 32X easier, I use the sound playback for timing. I set the number of samples per buffer to be exactly one tick worth of time. It doesn't need to be TOO exact - close enough is fine... you'll never hear the difference between say 375 samples, and 375.47138 samples (which you can't do). So in my 32X mod player, I set the tempo like this

Code: Select all

static void SetBeat(struct ModTag* Mod_p, uint8_t bpm)
{
    uint16_t nsmp = (SAMPLE_RATE * 5)/(bpm * 2);
    Mod_p->NumSamples = (nsmp > MAX_NUM_SAMPLES) ? MAX_NUM_SAMPLES : nsmp;
}
I check against the maximum number of samples that fit in a buffer in case the song uses a ridiculously low tempo, but otherwise, the number of samples sets the callback rate for processing the score. This would be done exactly the same for MUS playing where it can also change the tempo. Note that we always mix this many samples per callback. When the PWM DMA int occurs, we know we've played one tick worth of audio, so we start the next dma, we process the score (subtracting 1 from the delay until it's 0) processing any events, and mix NumSamples worth of samples into the next buffer knowing that NumSamples will give us one tick worth of delay at the current tempo.

We start the next dma, THEN process the score, THEN mix the next buffer - that way we have the most time possible to process the score and do the mixing.

What you should NEVER do is have a fixed buffer length that you try to fill by mixing 'delay' numbers of samples until the buffer is full. Samples never directly correspond to delay ticks in any case. At any reasonable sample rate for the 32X, you have HUNDREDS of samples per delay tick, not ONE sample. So any 32X MUS player (or derivative) should look just like my MOD player for 32X, but with the Handle_MUS code I posted rather than the PlayMOD code.

KillaMaaki
Very interested
Posts: 84
Joined: Sat Feb 28, 2015 9:22 pm

Re: Need help testing 32X Sandbox

Post by KillaMaaki » Sat Jul 29, 2017 7:05 pm

OK, I get what you're saying now, and that method does slightly bother me but perhaps only because I'm still thinking in modern audio programming terms (and, in fact, how I first prototyped my playback code before I started porting it to the 32X), where you DO just often have a fixed-size buffer and just fill that buffer until it is full. I'm not sure still why that is wrong per-se, but I can see your point anyway.

Back on the real problem at hand, which is the song delay. That's definitely not working the way it works in an emulator! In fact, the last time you tested it, it wasn't even stuck at 0, it was at 356! I'm not even sure what would have changed there, to be honest.
So here's a new version. THIS one works pretty much the way you're describing. Essentially it's changing the buffer length to be the size of a single tick. Then it just does this for every audio buffer:

Code: Select all

if( currentTicks > 0 )
	currentTicks--;
	
while( currentTicks == 0 )
{
	SongEvent event = song->EventList[ currentEvent++ ]; 	// grab the next event, advance event index
	processEvent( event );						// process the event
	currentTicks = event.Delay;					// set the delay counter
}

processBuffer( buffer, num_samples ); // fill this audio buffer
Attachments
OutRom.zip
(234.89 KiB) Downloaded 191 times

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

Re: Need help testing 32X Sandbox

Post by Chilly Willy » Sat Jul 29, 2017 7:23 pm

That rom fails to run half the time. Just a black screen. When it runs, the frame advances, the curr event is stuck at 0, and the delay is counting down from about 100920000.

And yes, filling a large fixed buffer until it's full isn't so much wrong as it is "new". It's how you do streaming on more powerful systems with lots of ram. Hell, I do that myself for MIDI on the Dreamcast and PSP. For older computers/consoles with little ram and even less power, it's not as good a method for playing music/sfx. That's one thing you really gotta skimp on with the 32X - there's damn little ram.

Post Reply