FCEUX expansion sound

Discuss emulation of the Nintendo Entertainment System and Famicom.
NewRisingSun
Posts: 1593
Joined: Thu May 19, 2005 11:30 am

FCEUX expansion sound

Post by NewRisingSun »

Could somebody explain the FCEUX expansion sound interface to me so that I can add a mapper with expansion sound? I see that there is a structure named GameExpSound, and existing mapper emulations will the fields RChange, Kill, Fill and NeoFill with pointers to a mapper-specific routine. RChange appears to just set the sampling rate, Kill seems like the equivalent of a destructor, but then there are two different Fill functions. "Fill" takes a "Count" parameter, the expansion sound functions do some strange arithmetic with SOUNDTS, soundtsinc and dwave globals, then fill a Wave[dwave] array. NeoFill takes two parameters, Wave and Count, which seems more straightforward at least. But why are there two fill procedures, and why is the first one so weird?
User avatar
Dwedit
Posts: 5257
Joined: Fri Nov 19, 2004 7:35 pm

Re: FCEUX expansion sound

Post by Dwedit »

Whenever I find myself looking at unfamiliar code, I often look at other code that's related to what I'm trying to do, then copy-paste and edit it.

So maybe look at what the existing code is doing. There's audio for VRC6, VRC7, FDS, Gimmick, MMC5, and Namco. Surely one of those will show you what you should be doing.
Here come the fortune cookies! Here come the fortune cookies! They're wearing paper hats!
NewRisingSun
Posts: 1593
Joined: Thu May 19, 2005 11:30 am

Re: FCEUX expansion sound

Post by NewRisingSun »

:roll:

The initial post should have told you that this is what I had done, and why that did not answer my questions.
stan423321
Posts: 126
Joined: Wed Sep 09, 2020 3:08 am

Re: FCEUX expansion sound

Post by stan423321 »

I am not affiliated with FCEUX authors and all of the below is based on baseless speculation and opening the GitHub copy of FCEUX source as linked to by its homepage.

Regarding "why are there two" - look up GameExpSound definition to find this. The "you should look things up" reply was a bit frustrating, but I don't know how you missed it (or decided it wasn't worth bringing up). Are you operating on an older version?

Code: Select all

	   /* NeoFill is for sound devices that are emulated in a more
	      high-level manner(VRC7) in HQ mode.  Interestingly,
	      this device has slightly better sound quality(updated more
	      often) in lq mode than in high-quality mode.  Maybe that
     	      should be fixed. :)
	   */
(sound.h)

So, maybe there were two Fills for two modes. Emphasis on were. Now there's at least three.

In sound.cpp, there are no uses of NeoFill, only Fill (for "soundq fsetting" < 1) and HiFill (otherwise). It therefore appears the NeoFill is abandoned and replaced by HiFill and its friend HiSync, unless NeoFill is called somewhere completely different just to mess with us readers. The distinction whether HQ mode was replaced with HQ-er mode or evolved into this seems academic.

It can be assumed that the original Fill is a mess because it doesn't split responsibilities clearly and relies on implied and global arguments. The ((SOUNDTS << 16) / soundtsinc) result is typically used to compute the final pseudosample offset to be written to. Pseudosample, since mmc5.cpp, 69.cpp (Sunsoft 5B) and vrc6.cpp Fills increase each element of Wave array up to 16 times. HiFill's primary difference for these mappers seems to be lack of this merging in the corresponding WaveHi array, and splitting off HiSync.

The part absent from HiFill and moved to HiSync is that "Count" argument seems to be written to clock counters of individual channels after the fill operation. The HiSync's name seems more appropriate in this regard. The point of this thing seems to be that some mappers call their Fills internally early, perhaps to avoid changing sound state within a sampling loop.
NewRisingSun
Posts: 1593
Joined: Thu May 19, 2005 11:30 am

Re: FCEUX expansion sound

Post by NewRisingSun »

Thank you, Stan. Yes, I was actually thinking of Fill and HiFill, not NeoFill.
stan423321
Posts: 126
Joined: Wed Sep 09, 2020 3:08 am

Re: FCEUX expansion sound

Post by stan423321 »

I believe 69.cpp has the ni... ugh... least complicated implementations of Fill, HiFill and HiSync to start with. On the other hand, they implement the idea of partial early Fill update in a kinda stupid way, running the old settings until the end of the sound frame. But on the third hand, I don't know if FCEUX architecture even allows a more precise implementation, since bus writes don't carry a time parameter, but maybe there's a global for that.
NewRisingSun
Posts: 1593
Joined: Thu May 19, 2005 11:30 am

Re: FCEUX expansion sound

Post by NewRisingSun »

I have finished implementing expansion sound to one of the mappers I added. The experience was plainly horrifying. The following rules apply:
  • In a mapper's "Init", call a set-up function that is also called by the emulator whenever the sound settings change in the user interface:

    Code: Select all

    static void mapperSound_init (void) {
    	if (FSettings.SndRate) {
    		GameExpSound.Fill = mapperSound_fillBufferLow;
    		GameExpSound.HiFill = mapperSound_fillBufferHigh;
    		GameExpSound.HiSync = mapperSound_setSoundOffset;
    		GameExpSound.RChange = mapperSound_init;
    	}
    	MSM6585_init(&adpcm, FSettings.soundq >=1? 1789773: FSettings.SndRate*16, serveADPCM);
    }
    
    This set-up procedure needs to fill out the four callbacks in the global structure GameExpSound, plus tell its actual sound emulation core (MSM6585 in this case) what the sample generation rate should be. In "low quality" mode (FSettings.soundq = 0), it's "sixteen time the selected sampling rate", so FSettings.SndRate*16. In "high quality" modes, it's just the CPU clock. :roll:
  • The "low quality fill procedure" starts at the sound offset it received as a parameter during the previous invocation, fills "Wave" up to a point that it has to compute itself "(SOUNDTS <<16) / soundtsinc", and only uses the supplied variable "count" as the starting offset of the next invocation. :roll: :shock: Also, it expects you to perform oversampling by accumulating sixteen values, so you add to "Wave" with each offset divided by sixteen (hence >>4).

    Code: Select all

    static void mapperSound_fillBufferLow (int count) {
    	int i;
    	int end = (SOUNDTS <<16) /soundtsinc;
    	for (i = soundOffset; i < end; i++) {
    		MSM6585_run(&adpcm);
    		Wave[i >>4] += MSM6585_getOutput(&adpcm) >>1;
    	}
    	soundOffset = count;
    }
    
  • The "high quality fill procedure" is basically the same, except that it does not divide the offset by 16, adds to "WaveHi", and instead of getting a "count" variable to remember for the next time, it just starts where it left off the last time, unless the starting offset has been changed by the emulator calling the "HiSync" function (called "setSoundOffset" by me). Also, for some inscrutable reason, the output wave must have all positive samples, otherwise you'll get horrible distortion.

    Code: Select all

    static void mapperSound_fillBufferHigh () {
    	int i;
    	for (i = soundOffset; i < SOUNDTS; i++) {
    		MSM6585_run(&adpcm);
    		WaveHi[i] += MSM6585_getOutput(&adpcm)*8 +16384;
    	}
    	soundOffset = SOUNDTS;
    }
    
    static void mapperSound_setSoundOffset(int32 newSoundOffset) {
    	soundOffset = newSoundOffset;
    }
    
Whoever designed this dumbass software interface needs to have his head checked. :x I am not saying that my code or my designs are perfect, but this is awful. Could I have learned this from looking at the other mappers' expansion sound code? I stared at it for several hours, and understood nothing, so no, I could not have. :P
stan423321
Posts: 126
Joined: Wed Sep 09, 2020 3:08 am

Re: FCEUX expansion sound

Post by stan423321 »

So to restate, I think I have some understanding of the logic behind the "send next start offset" design from staring at the existing implementations, but it doesn't really work.

The "memorized offset to start the next write from" is something more akin to a program counter. Some mappers have multiple copies of the storage variable for it, one per channel.

Writes to registers corresponding to particular channels on these mappers cause their subfill procedures to be executed early. In this case, the channel's sound counter will move forward and stay there. Then, next time when sound.cpp invokes the overall mapper callback, the corresponding subchannel fill does not write to cirresponding entries in Wave/WaveHi a second time, as instructed by the sound counter already being ahead; afterwards it is reset. One could alternatively view the other, normal execution scenario, where channels keep executing but there were no setting changes, as emulation catch-up.

This could theoretically be useful to keep generating function executing on whole batches of samples, while also respecting timing of register writes for the purpose of generated sound. The problem is that there is seemingly no timer information associated with such a register write, so what the existing mappers actually seem to do is generating the entire "fill-frame" worth of channel's audio using old settings before applying the new ones. This is just as imprecise as using new settings for an entire "fill-frame", only it adds a bit of extra lag.

As an additional trivia, some of those mappers write some of the function pointers to GameExpSound on all audio register writes, instead of the init function. Perhaps that was meant as optimization for the case where a mapper is used without audio.

-----

I don't think using an unsigned sample format is particularly unheard of, though it could be documented better (perhaps in typing). There are also some filters involved, which may make the distortion worse.
NewRisingSun
Posts: 1593
Joined: Thu May 19, 2005 11:30 am

Re: FCEUX expansion sound

Post by NewRisingSun »

And that the low-quality variant wants signed and the high-quality variant wants unsigned samples, very odd. In any case, thank you for your feedback. :)