VOGONS


Reply 20 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

The fallthrough of case 3 into case 2 is correct: case 3 is case 2 with each second quarter 0(giving a sinus based sawtooth at double the sinus frequency).

So all I need to so is apply the volume envelope before setting the new last signal and divide the new lastsignal by 2?

About the SHRT_MAX: I'm currently clipping to fix that without modifying the volume of the sepetate adlib channels (it's at the end of the adlibgensample() function). It seems to work with multiple sounds. If I would give every channel a maximum range of 8195, wouldn't the sound be too soft when only one channel is playing?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 21 of 112, by Jepael

User metadata
Rank Oldbie
Rank
Oldbie
superfury wrote:

So all I need to so is apply the volume envelope before setting the new last signal and divide the new lastsignal by 2?

You can keep unmodified previous samples, and divide the sum of the two. Or, you can halve the output value before sending it to lastsignal. Or you can change your feedback factor table. Or you can divide the result when you set the value read from feedback table. As long as you halve the value before using it as modulation.

superfury wrote:

About the SHRT_MAX: I'm currently clipping to fix that without modifying the volume of the sepetate adlib channels (it's at the end of the adlib sample function). It seems to work with multiple sounds. If I would give every channel a maximum range of 8195, wouldn't the sound be too soft when only one channel is playing?

Yes, but real chip can output (more than) 8 (but not 9) operators in 16 bits worth of data without clipping because single channel is worth 13 bits (including sign bit). You can currently output only 1 operator without clipping. It does not sound like original if it has to clip while playing more than 1 channel.

Yes, of course it is softer but that's what you have to do to fit at least 8 channels into 16 bits.

Reply 22 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

This is my current version:
https://bitbucket.org/superfury/x86emu/src/05 … lib.c?at=master

Currently a total divide of 128 to get the feedback set close to the youtube reference (I'm testing with Supaplex atm). Is this correct?

Btw how does Dosbox deal with this? Keeping the total range of all adlib channels together at maximum volume without modifying volume of each channel seperate? Afaik Dosbox should be reasonably accurate afaik? Or does it let the mixer deal with it (clip when mixing)?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 23 of 112, by Jepael

User metadata
Rank Oldbie
Rank
Oldbie
superfury wrote:

Currently a total divide of 128 to get the feedback set close to the youtube reference (I'm testing with Supaplex atm). Is this correct?

I honestly don't know if it is correct based on the calculations I assumed it should have been just divide by 2.
So adlibcalcsignal gives out -1 to +1?
Outputlevel is 0 to 1?
Volenv is 0 to 1?
Therefore the output to feedback range is also -1 to +1, before converting to PCM integer with huge multiplication?

superfury wrote:

Btw how does Dosbox deal with this? Keeping the total range of all adlib channels together at maximum volume without modifying volume of each channel seperate? Afaik Dosbox should be reasonably accurate afaik? Or does it let the mixer deal with it (clip when mixing)?

But your idea about maximum volume is too loud. The whole 8 operators summed together fills the 16-bit full scale range, so that must mean a single operator is running at one eighth of the full 16-bit scale. Yes, DosBOX OPL emulation is quite good.

Reply 24 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've changed the output range to 4095 instead of SHRT_MAX-1.

So adlibcalcsignal gives out -1 to +1? Outputlevel is 0 to 1? Volenv is 0 to 1? Therefore the output to feedback range is also - […]
Show full quote

So adlibcalcsignal gives out -1 to +1?
Outputlevel is 0 to 1?
Volenv is 0 to 1?
Therefore the output to feedback range is also -1 to +1, before converting to PCM integer with huge multiplication?

This is correct. The huge multiplication used is stored in feedbacklookup (0, PI/16, PI/8, PI/4, PI/2, PI, PI*2, PI*4). So the resulting signals (range -1 to +1) gets multiplied with this feedbacklookup value (range PI/16 to PI*4). This value is used as modulation in the same way OP1 modulates OP2 when using 'FM' synthesis mode(it's PM actually). So I should normalize those values to a range of -1 to +1 instead of multiples of PI?

Edit: I've changed the range of feedback to 0-1 (converting the lookup table by multiplying with 1/(4*PI)). I've also changed the divide by 128 back to 2. Is this correct?

My latest version:
https://bitbucket.org/superfury/x86emu/src/e0 … lib.c?at=master

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 25 of 112, by Jepael

User metadata
Rank Oldbie
Rank
Oldbie

I also noticed the FM (PM) modulation needs fixing. If the modulator outputs -1 to +1 range, it would need to modulate the carrier phase with also at something * pi range. Why? Because original chip operator outputs -4085 to +4084 and the sine wave has 1024 phases for full wave (2*pi). Is that almost 4*2*pi then?

Reply 26 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

So I simply need to multiply result with 4085/1024 before sending it as input to calcOperator (after else //FM synthesis)?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 27 of 112, by Jepael

User metadata
Rank Oldbie
Rank
Oldbie
superfury wrote:

So I simply need to multiply result with 4085/1024 before sending it as input to calcOperator (after else //FM synthesis)?

I think so. I was going to point out you forgot the 2*pi about it, but it appears adlibwave does 2*pi multiplication already.

I did not notice this earlier, so it could be that now the feedback is off. I am more better at what range converts to what range in general, but I am not so good following your code which range should be fed to which function to achieve a specific range in the end.

Reply 28 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

So I would simply need to divide the feedback value by 2*PI before calling adlibwave in order to fix the feedback?

My latest version:
https://bitbucket.org/superfury/x86emu/src/6b … lib.c?at=master

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 29 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've just tried the 8086 hacked version of Wolfenstein 3D ( http://www.youtube.com/watch?v=5f7gW5X24ao ) on my x86EMU emulator with adlib. It seems to detect both normal and EMS memory (1MB out of 4MB) correctly. The highhat it uses (feedback?) during the opening sounds odd (it sounds more like a distorted tone than a highhat (although it glitches pretty much for some reason)). Is the way feedback is handled correct?

https://bitbucket.org/superfury/x86emu/src/28 … lib.c?at=master

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 30 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've implemented the LogSin using the current adlibWave function, but using this function instead of sin(frequencytime*2):

OPTINLINE float OPL2SinWave(const float frequencytime)
{
float index;
byte PIpart=0;
frequencytime = fmod(frequencytime,2.0f*PI);
if (frequencytime>=PI) //Second half?
{
PIpart = 2; //Second half!
frequencytime -= PI; //Take the half!
}
if (frequencytime>=(0.5*PI)) //Past quarter?
{
PIpart |= 1; //Second half!
frequencytime -= (0.5*PI); //Take the quarter!
}
frequencytime *= (1/(0.5*PI))*255.0f; //Convert to full range!
if (PIpart&1) //Reversed quarter?
{
frequencytime = 255.0f-frequencytime; //Reverse us!
}
if (PIpart&2) //Second half?
{
return 0.0f-(OPL2_LogSinTable[(int)frequencytime]*(1/256.0f)); //First quarter lookup reversed!
}
return (float)OPL2_LogSinTable[(int)frequencytime]*(1/256.0f); //First quarter lookup normal!
}

Is this correct? I don't know how to implement the two tables to the current emulation yet, so it's only used in the wave generator.

Edit: I've improved it a bit, but I get white noise together with the samples when I use the part below the return sin();

byte lastdecodedlocation = 0xFF;
byte PIpart=0;

OPTINLINE float OPL2SinWave(const float r)
{
return sinf(r);
float index;
byte location; //The location in the table to use!
index = fmod(r,PI2); //Loop the sinus infinitely!
PIpart = 0; //Reset PI part to use!
if (index>=(float)PI) //Second half?
{
PIpart = 2; //Second half!
}
if (fmod(index,(float)PI)>=(0.5f*(float)PI)) //Past quarter?
{
PIpart |= 1; //Second half!
}
index = (fmod(index,(0.5f*(float)PI))/(0.5f*(float)PI))*255.0f; //Convert to full range!
location = (byte)index; //Set the location to use!
if (PIpart&1) //Reversed quarter?
{
location = ~location; //Reverse us!
++location; //Reversed!
}

lastdecodedlocation = location; //Save the location for reference during sanity checks!
if (PIpart&2) //Second half is negative?
{
return -OPL2_LogSinTable[location]; //First quarter lookup reversed!
}
return OPL2_LogSinTable[location]; //First quarter lookup normal!
}

The tables are built using the commented source page:

//Source of the Exp and LogSin tables: https://docs.google.com/document/d/18IGx18NQY_Q1PJVZ-bHywao9bhsDoAqoIn1rIm42nwo/edit
for (i = 0;i < 0x100;++i) //Initialise the exponentional and log-sin tables!
{
OPL2_ExpTable[i] = round((pow(2, i / 256) - 1) * 1024);
OPL2_LogSinTable[i] = round(-log(sin((i + 0.5)*PI / 256 / 2)) / log(2) * 256);
}

The arrays themselves:

float OPL2_ExpTable[0x100], OPL2_LogSinTable[0x100]; //The OPL2 Exponential and Log-Sin tables!

Applying the ExpTable to the float-to-word result before the 9 channels are mixed mutes the sound(no sound anymore). Although it's value is in the range 0-1(sample before multiplying to get channel sample to be added to get the 16-bit PCM sample to be played. That's effectively that 4096 multiplication).

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 31 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've changed the functions a bit, but I seem to get strange waveforms using a simple test signal:

OPTINLINE float OPL2SinWave(const float r)
{
float index;
float entry; //The entry to convert!
byte location; //The location in the table to use!
byte PIpart = 0; //Default: part 0!
index = fmod(r,PI2); //Loop the sinus infinitely!
if (index>=(float)PI) //Second half?
{
PIpart = 2; //Second half!
}
if (fmod(index,(float)PI)>=(0.5f*(float)PI)) //Past quarter?
{
PIpart |= 1; //Second half!
}
index = (fmod(index,(0.5f*(float)PI))/(0.5f*(float)PI))*255.0f; //Convert to full range!
location = (byte)index; //Set the location to use!
if (PIpart&1) //Reversed quarter(first and third quarter)?
{
location = 255-location; //Reverse us!
}

entry = OPL2_LogSinTable[255-location]; //First quarter lookup normal!
entry = OPL2_LogSinTable[255] + entry; //Reverse the curve!
if (PIpart & 2) //Second half is negative?
{
entry = -entry; //First quarter lookup reversed!
}
return entry; //Give the processed entry!
}

float expfactor = 1.0f;

OPTINLINE float OPL2_Exponential(float v)
{
const float explookup = (1.0f/3137.0f)*255.0f; //Exp lookup!
if (v>=0.0f) //Positive?
return OPL2_ExpTable[(int)(v*explookup)]*expfactor; //Convert to exponent!
else //Negative?
return (-OPL2_ExpTable[(int)((-v)*explookup)])*expfactor; //Convert to negative exponent!
}

expfactor is simply set to the last entry to convert to 0-1 range:

	expfactor = (1.0f/OPL2_ExpTable[255]); //The highest volume conversion to apply with our exponential table!

Also improved the tables a bit (accuracy problem was causing the ExpTable to be filled with 0.0 values):

	for (i = 0;i < 0x100;++i) //Initialise the exponentional and log-sin tables!
{
OPL2_ExpTable[i] = round((pow(2, (float)i / 256.0f) - 1.0f) * 1024.0f);
OPL2_LogSinTable[i] = round(-log(sin((i + 0.5)*PI / 256.0f / 2.0f)) / log(2.0f) * 256.0f);
}

I now get a waveform, which results in a bit of a strange 'sinus':

Snap 2016-04-21 at 16.01.33.jpg
Filename
Snap 2016-04-21 at 16.01.33.jpg
File size
25.89 KiB
Views
1297 views
File comment
The waveform that gets output (at ~49kHz), viewed in Wavepad Sound Editor.
File license
Fair use/fair dealing exception

Is this correct? Aren't the curves supposed to be round using the table? It's supposed to be a D#.

		adlibsetreg(0x20, 0x21); //Modulator multiple to 1!
adlibsetreg(0x40, 0x10); //Modulator level about 40dB!
adlibsetreg(0x60, 0xF7); //Modulator attack: quick; decay long!
adlibsetreg(0x80, 0xFF); //Modulator sustain: medium; release: medium
adlibsetreg(0xA0, 0x98); //Set voice frequency's LSB (it'll be a D#)!
adlibsetreg(0x23, 0x21); //Set the carrier's multiple to 1!
adlibsetreg(0x43, 0x00); //Set the carrier to maximum volume (about 47dB).
adlibsetreg(0x63, 0xFF); //Carrier attack: quick; decay: long!
adlibsetreg(0x83, 0x0F); //Carrier sustain: medium; release: medium!
adlibsetreg(0xB0, 0x31); //Turn the voice on; set the octave and freq MSB!

Edit: I seem to have fixed it by implementing the 0th entry as max instead of 255th entry(which is 0). This gives a proper waveform(sort of a sinus, although not perfectly round):

OPTINLINE float OPL2SinWave(const float r)
{
float index;
float entry; //The entry to convert!
byte location; //The location in the table to use!
byte PIpart = 0; //Default: part 0!
index = fmod(r,PI2); //Loop the sinus infinitely!
if (index>=(float)PI) //Second half?
{
PIpart = 2; //Second half!
}
if (fmod(index,(float)PI)>=(0.5f*(float)PI)) //Past quarter?
{
PIpart |= 1; //Second half!
}
index = (fmod(index,(0.5f*(float)PI))/(0.5f*(float)PI))*255.0f; //Convert to full range!
location = (byte)index; //Set the location to use!
if (PIpart&1) //Reversed quarter(first and third quarter)?
{
location = 255-location; //Reverse us!
}

entry = OPL2_LogSinTable[0] - OPL2_LogSinTable[location]; //First quarter lookup! Reverse the signal (it goes from Umax to Umin instead of a normal sin(0PI to 0.5PI))
if (PIpart & 2) //Second half is negative?
{
entry = -entry; //First quarter lookup reversed!
}
return entry; //Give the processed entry!
}

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 32 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

The adlib emulation is now correctly generating sound again(this time using the two lookup tables (Exponential and Log-sin tables) instead of the old sin() functions).

I've now adjusted the adlib to (re)start the modulator and/or carrier signals depending on the channel being turned on/off:
- Melodic channels affect both the modulator and generator channels.
- Bass drum affects both channels as well.
- Snare drum affects channel 7 carrier channel.
- Tom-tom affects channel 7 modulator channel.
- Cymbal affects channel 8 carrier channel.
- Hi-hat affects channel 8 modulator channel.

The channel numbers are from 0-8 in the above description (0-based).

I assume the Bass drum is a normal melodic channel mostly? How does the RNG bit(1-bit value generated every time an output signal is generated for all channels), which is based on the shifts of the RNG register (23-bit shift register) affect this signal?

How is the output of the other rhythm channels generated? What's the difference between those rhythm channels and normal melodic channels? How does the RNG 1-bit value affect those? Is it simply used as the modulator, with the modulator or carrier (see above list) being used as the carrier instead?

So:
Snare drum = RNG bit -> channel 7 carrier -> output
Tom-tom = RNG bit -> channel 7 modulator -> output
Cymbal = RNG bit -> channel 8 carrier -> output
Hi-hat = RNG bit -> channel 8 modulator -> output

Is this correct? How does the RNG affect those 4 outputs? Is the carrier/modulator being used as carrier correct? Jepael?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 33 of 112, by Jepael

User metadata
Rank Oldbie
Rank
Oldbie

I've not very knowledgeable about the rhythm mode, but the RNG may be applied differently to each channel because each rhythm instrument needs different timbre. At least in one very simple test case which I don't remember, the RNG was just xoring a certain phase bit, so the real chip matched what was in MAME source code.

Reply 34 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

I looked at the OPL3 source code on http://bisqwit.iki.fi/source/opl3emu.html and implemented parts based on it into my emulation (from fmopl.c):

	//Determine the modulator and carrier to use!
op1 = adliboperators[0][curchan]; //First operator number!
op2 = adliboperators[1][curchan]; //Second operator number!
op1frequency = adlibfreq(op1, curchan); //Load the first frequency!

if (adlibpercussion && (curchan >= 6) && (curchan <= 8)) //We're percussion?
{
result = 0.0f; //Initialise the result!
//Calculations based on http://bisqwit.iki.fi/source/opl3emu.html fmopl.c
switch (curchan) //What channel?
{
case 6: //Bass drum?
//Generate Bass drum samples!
//Calculate the frequency to use!
result = calcOperator(curchan, op1, op1frequency, 0.0f,1); //Calculate the modulator for feedback!
if (adlibch[curchan].synthmode) //Additive synthesis?
{
//Special on Bass Drum: Additive synthesis(Operator 1) is ignored.
result = calcOperator(curchan, op2, adlibfreq(op2, curchan), 0.0f, 0); //Calculate the carrier without applied modulator additive!
}
else //FM synthesis?
{
result = calcOperator(curchan, op2, adlibfreq(op2, curchan), OPL2_Exponential(result), 0); //Calculate the carrier with applied modulator!
}

result = OPL2_Exponential(result); //Apply the exponential!

result *= adlib_scaleFactor; //Convert to output scale (We're only going from -1.0 to +1.0 up to this point), convert to signed 16-bit scale!
return (short)result; //Give the result, converted to short!
break;

//Comments with information from fmopl.c:
/* Phase generation is based on: */

/* HH (13) channel 7->slot 1 combined with channel 8->slot 2 (same combination as TOP CYMBAL but different output phases) */

/* SD (16) channel 7->slot 1 */

/* TOM (14) channel 8->slot 1 */

/* TOP (17) channel 7->slot 1 combined with channel 8->slot 2 (same combination as HIGH HAT but different output phases) */


/* Envelope generation based on: */

/* HH channel 7->slot1 */

/* SD channel 7->slot2 */

/* TOM channel 8->slot1 */

/* TOP channel 8->slot2 */
case 7: //Hi-hat/Snare drum? High-hat uses modulator, Snare drum uses Carrier signals.
if (adlibop[op1].volenvstatus) //Hi-hat?
{
//Still unimplemented until the calculations have been figured out.
}
if (adlibop[op2].volenvstatus) //Snare drum?
{
//Derive frequency from channel 0.
Show last 28 lines
					result = calcOperator(curchan, op1, op1frequency, 0.0f,1); //Calculate the modulator for feedback!
tempphase = (result>=0.0f)?0x200:0x100; //Bit8=0(Positive) then 0x100, else 0x200!
tempphase ^= (OPL2_RNG<<8); //Noise bits XOR'es phase by 0x100!
result = calcOperator(curchan, op2, adlibfreq(op2, curchan), OPL2_Exponential((float)tempphase), 0); //Calculate the carrier with applied modulator!
result = OPL2_Exponential(result); //Apply the exponential!
}
result *= 0.5; //We only have half(two channels combined)!
result *= adlib_scaleFactor; //Convert to output scale (We're only going from -1.0 to +1.0 up to this point), convert to signed 16-bit scale!
return (short)result; //Give the result, converted to short!
break;
case 8: //Tom-tom/Cymbal? Tom-tom uses Modulator, Cymbal uses Carrier signals.
if (adlibop[op1].volenvstatus) //Tom-tom?
{
result = calcOperator(curchan, op1, adlibfreq(op1, curchan), 0.0f, 0); //Calculate the carrier with applied modulator!
result = OPL2_Exponential(result); //Apply the exponential!
}
if (adlibop[op2].volenvstatus) //Cymbal?
{
//Still unimplemented until the calculations have been figured out.
}
result *= 0.5; //We only have half(two channels combined)!
result *= adlib_scaleFactor; //Convert to output scale (We're only going from -1.0 to +1.0 up to this point), convert to signed 16-bit scale!
return (short)result; //Give the result, converted to short!
break;
}
return 0; //Percussion isn't supported yet for this channel!
... Normal melodic channel procesing ...
}

I also extended the key-on function by adding better on detection and applying:

void writeadlibKeyON(byte channel, byte forcekeyon)
{
byte keyon;
byte oldkeyon;
oldkeyon = adlibch[channel].keyon; //Current&old key on!
keyon = ((adlibregmem[0xB0 + (channel&0xF)] >> 5) & 1)?3:0; //New key on for melodic channels? Affect both operators! This disturbs percussion mode!
if (adlibpercussion && (channel&0x80)) //Percussion enabled and percussion channel changed?
{
switch (channel&0xF) //What channel?
{
case 6: //Bass drum? Uses the channel normally!
keyon = (adlibregmem[0xBD]&0x10)?3:0; //Bass drum on? Key on on both operators!
channel = 6; //Use channel 6!
break;
case 7: //Hi-hat/Snare drum? High-hat uses modulator, Snare drum uses Carrier signals.
keyon = adlibregmem[0xBD]; //Snare drum/Hi-hat on?
keyon = ((keyon>>2)&2)|(keyon&1); //Shift the information to modulator and carrier positions!
channel = 7; //Use channel 7!
break;
case 8: //Tom-tom/Cymbal? Tom-tom uses Modulator, Cymbal uses Carrier signals.
keyon = adlibregmem[0xBD]; //Cymbal/hi-hat on? Use the full register for processing!
keyon = ((keyon>>2)&1)|(keyon&2); //Shift the information to modulator and carrier positions!
channel = 8; //Use channel 8!
break;
default: //Unknown channel?
//New key on for melodic channels? Don't change anything!
break;
}
}
if ((adliboperators[0][channel]!=0xFF) && (((keyon&1) && ((oldkeyon^keyon)&1)) || (forcekeyon&1))) //Key ON on operator #1?
{
adlibop[adliboperators[0][channel]].volenvstatus = 1; //Start attacking!
adlibop[adliboperators[0][channel]].volenvcalculated = adlibop[adliboperators[0][channel]].volenv = 0.0025f;
adlibop[adliboperators[0][channel]].freq0 = adlibop[adliboperators[0][channel]].time = 0.0f; //Initialise operator signal!
memset(&adlibop[adliboperators[0][channel]].lastsignal, 0, sizeof(adlibop[0].lastsignal)); //Reset the last signals!
}
if ((adliboperators[1][channel]!=0xFF) && (((keyon&2) && ((oldkeyon^keyon)&2)) || (forcekeyon&2))) //Key ON on operator #2?
{
adlibop[adliboperators[1][channel]].volenvstatus = 1; //Start attacking!
adlibop[adliboperators[1][channel]].volenvcalculated = adlibop[adliboperators[0][channel]].volenv = 0.0025f;
adlibop[adliboperators[1][channel]].freq0 = adlibop[adliboperators[1][channel]].time = 0.0f; //Initialise operator signal!
memset(&adlibop[adliboperators[1][channel]].lastsignal, 0, sizeof(adlibop[1].lastsignal)); //Reset the last signals!
}
adlibch[channel].freq = adlibregmem[0xA0 + channel] | ((adlibregmem[0xB0 + channel] & 3) << 8);
adlibch[channel].convfreq = ((double)adlibch[channel].freq * 0.7626459);
adlibch[channel].keyon = keyon | forcekeyon; //Key is turned on?
adlibch[channel].octave = (adlibregmem[0xB0 + channel] >> 2) & 7;
}

As you can see it now applies key-on to either both the modulator and carrier (melodic channel) or to modulator or carrier signals (Drum channels, except Bass drum).

The RNG is simply updated once a sample of the adlib is generating (one time for all channels, so about 49000 times per second, before the adlib starts generating a sample for each of the 9 channels):

uint_32 OPL2_RNGREG = 0;
uint_32 OPL2_RNG = 0; //The current random generated sample!

OPTINLINE void OPL2_stepRNG() //Runs at the sampling rate!
{
OPL2_RNG = ( (OPL2_RNGREG) ^ (OPL2_RNGREG>>14) ^ (OPL2_RNGREG>>15) ^ (OPL2_RNGREG>>22) ) & 1; //Get the current RNG!
OPL2_RNGREG = (OPL2_RNG<<22) | (OPL2_RNGREG>>1);
}

Is this correct? Trying to run Eye of the Beholder seems to give strange sounds at some points? Or is this simply because the OPL2 now uses the lookup tables?

The key-On is detected by looking at the 0xBD register's bits:

	case 0xA0:
case 0xB0:
if (portnum <= 0xB8)
{ //octave, freq, key on
if ((portnum & 0xF) > 8) goto unsupporteditem; //Ignore A9-AF!
portnum &= 0xF; //Only take the lower nibble (the channel)!
writeadlibKeyON((byte)portnum,0); //Write to this port! Don't force the key on!
}
else if (portnum == 0xBD) //Percussion settings etc.
{
adlibpercussion = (value & 0x20)?1:0; //Percussion enabled?
if (((oldval^value)&0x1F) && adlibpercussion) //Percussion enabled and changed state?
{
writeadlibKeyON(0x86,0); //Write to this port(Bass drum)! Don't force the key on!
writeadlibKeyON(0x87,0); //Write to this port(Snare drum/Tom-tom)! Don't force the key on!
writeadlibKeyON(0x88,0); //Write to this port(Cymbal/Hi-hat)! Don't force the key on!
}
}
break;

Btw forcing the key on (value 1 given) is only used when in CSM mode (Composite Speech synthesis Mode). This simply calls the writeadlibKeyON(channel,3) function when either or both timers expire.

Also I seem to notice that applying the 640K memory hole (640K-1M memory being nonexistant) seems to cause stuff like Eye of the Beholder to stop running (crashing into the BIOS at FFFF:FFFF) for some reason?

//Direct memory access (for the entire emulator)
byte MMU_directrb(uint_32 realaddress) //Direct read from real memory (with real data direct)!
{
byte invalidlocation=0;
//Apply the 640K memory hole!
if (realaddress&0x100000) //1MB+?
{
realaddress -= (0x100000-0xA0000); //Patch to less memory to make memory linear!
}
else if (realaddress>=0xA0000) //640K+ memory hole addressed?
{
invalidlocation = 1; //We're an invalid location!
}
if ((realaddress>=MMU.size) || invalidlocation) //Overflow/invalid location?
{
MMU.invaddr = 1; //Signal invalid address!
execNMI(1); //Execute an NMI from memory!
return 0xFF; //Nothing there!
}
return MMU.memory[realaddress]; //Get data, wrap arround!
}

byte LOG_MMU_WRITES = 0; //Log MMU writes?

void MMU_directwb(uint_32 realaddress, byte value) //Direct write to real memory (with real data direct)!
{
byte invalidlocation=0;
if (LOG_MMU_WRITES) //Data debugging?
{
dolog("debugger","MMU: Writing to real %08X=%02X (%c)",realaddress,value,value?value:0x20);
}
//Apply the 640K memory hole!
if (realaddress&0x100000) //1MB+?
{
realaddress -= (0x100000-0xA0000); //Patch to less memory to make memory linear!
}
else if (realaddress>=0xA0000) //640K+ memory hole?
{
invalidlocation = 1; //We're an invalid location!
}
if ((realaddress>=MMU.size) || invalidlocation) //Overflow/invalid location?
{
MMU.invaddr = 1; //Signal invalid address!
execNMI(1); //Execute an NMI from memory!
return; //Abort: can't write here!
}
MMU.memory[realaddress] = value; //Set data, full memory protection!
if (realaddress>user_memory_used) //More written than present in memory (first write to addr)?
{
user_memory_used = realaddress; //Update max memory used!
}
}

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 35 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've implemented an optimized x86EMU adaption of the code in opl3emu's fmopl.c:

OPTINLINE short adlibsample(uint8_t curchan) {
byte op7_0, op7_1, op8_0, op8_1; //The four slots used during Drum samples!
byte tempop_phase; //Current phase of an operator!
float result,immresult; //The operator result and the final result!
byte op1,op2; //The two operators to use!
float op1frequency;
curchan &= 0xF;
if (curchan >= NUMITEMS(adlibch)) return 0; //No sample with invalid channel!

//Determine the modulator and carrier to use!
op1 = adliboperators[0][curchan]; //First operator number!
op2 = adliboperators[1][curchan]; //Second operator number!
op1frequency = adlibfreq(op1, curchan); //Load the first frequency!

if (adlibpercussion && (curchan >= 6) && (curchan <= 8)) //We're percussion?
{
register uint_32 tempphase;
result = 0.0f; //Initialise the result!
//Calculations based on http://bisqwit.iki.fi/source/opl3emu.html fmopl.c
//Load our four operators for processing!
op7_0 = adliboperators[0][7];
op7_1 = adliboperators[1][7];
op8_0 = adliboperators[0][8];
op8_1 = adliboperators[1][8];
switch (curchan) //What channel?
{
case 6: //Bass drum?
//Generate Bass drum samples!
//Calculate the frequency to use!
result = calcOperator(curchan, op1, op1frequency, 0.0f,1,op1,1); //Calculate the modulator for feedback!
if (adlibch[curchan].synthmode) //Additive synthesis?
{
//Special on Bass Drum: Additive synthesis(Operator 1) is ignored.
result = calcOperator(curchan, op2, adlibfreq(op2, curchan), 0.0f, 0,op2,1); //Calculate the carrier without applied modulator additive!
}
else //FM synthesis?
{
result = calcOperator(curchan, op2, adlibfreq(op2, curchan), OPL2_Exponential(result), 0,op2,1); //Calculate the carrier with applied modulator!
}

result = OPL2_Exponential(result*2.0f); //Apply the exponential! The volume is always doubled!

result *= adlib_scaleFactor; //Convert to output scale (We're only going from -1.0 to +1.0 up to this point), convert to signed 16-bit scale!
return (short)result; //Give the result, converted to short!
break;

//Comments with information from fmopl.c:
/* Phase generation is based on: */
/* HH (13) channel 7->slot 1 combined with channel 8->slot 2 (same combination as TOP CYMBAL but different output phases) */
/* SD (16) channel 7->slot 1 */
/* TOM (14) channel 8->slot 1 */
/* TOP (17) channel 7->slot 1 combined with channel 8->slot 2 (same combination as HIGH HAT but different output phases) */


/* Envelope generation based on: */
/* HH channel 7->slot1 */
/* SD channel 7->slot2 */
/* TOM channel 8->slot1 */

/* TOP channel 8->slot2 */
Show last 89 lines
				//So phase modulation is based on the Modulator signal. The volume envelope is in the Modulator signal (Hi-hat/Tom-tom) or Carrier signal ()
case 7: //Hi-hat(Carrier)/Snare drum(Modulator)? High-hat uses modulator, Snare drum uses Carrier signals.
immresult = 0.0f; //Initialize immediate result!
if (adlibop[op7_1].volenvstatus) //Snare drum on modulator?
{
//Derive frequency from channel 0.
tempphase = 0x100<<((getphase(op7_0)>>8)&1); //Bit8=0(Positive) then 0x100, else 0x200! Based on the phase to generate!
tempphase ^= (OPL2_RNG<<8); //Noise bits XOR'es phase by 0x100 when set!
result = calcOperator(curchan, op7_0, op1frequency, 0.0f,1,op7_0,(!adlibop[op8_1].volenvstatus && !adlibop[op7_0].volenvstatus)); //Calculate the modulator, but only use the current time(position in the sine wave)!
result = calcOperator(curchan, op7_1, adlibfreq(op7_1, curchan), ((((float)tempphase)/(float)0x300)*(float)(PI2)), 0,op7_1,1); //Calculate the carrier with applied modulator!
immresult += OPL2_Exponential(result); //Apply the exponential!
}
if (adlibop[op7_0].volenvstatus) //Hi-hat on carrier?
{
//Derive frequency from channel 7(modulator) and 8(carrier).~
tempop_phase = getphase(op7_0); //Save the phase!
tempphase = (tempop_phase>>2);
tempphase ^= (tempop_phase>>7);
tempphase |= (tempop_phase>>3);
tempphase &= 1; //Only 1 bit is used!
tempphase = tempphase?(0x200|(0xD0>>2)):0xD0;
tempop_phase = getphase(op8_1); //Calculate the phase of channel 8 carrier signal!
if (((tempop_phase>>3)^(tempop_phase>>5))&1) tempphase = 0x200|(0xD0>>2);
if (tempphase&0x200)
{
if (OPL2_RNG) tempphase = 0x2D0;
}
else if (OPL2_RNG) tempphase = (0xD0>>2);

result = calcOperator(curchan, op7_0, adlibfreq(op7_0, curchan), 0.0f,1,op7_0,!adlibop[op8_1].volenvstatus); //Calculate the modulator, but only use the current time(position in the sine wave)!
result = calcOperator(curchan, op7_1, adlibfreq(op7_1, curchan), ((((float)tempphase)/(float)0x300)*(float)(PI2)), 0,op7_0,1); //Calculate the carrier with applied modulator!
immresult += OPL2_Exponential(result); //Apply the exponential!
}
result = immresult; //Load the resulting channel!
result *= 0.5f; //We only have half(two channels combined)!
result *= adlib_scaleFactor; //Convert to output scale (We're only going from -1.0 to +1.0 up to this point), convert to signed 16-bit scale!
return (short)result; //Give the result, converted to short!
break;
case 8: //Tom-tom(Carrier)/Cymbal(Modulator)? Tom-tom uses Modulator, Cymbal uses Carrier signals.
immresult = 0.0f; //Initialize immediate result!
if (adlibop[op8_1].volenvstatus) //Cymbal(Modulator)?
{
//Derive frequency from channel 7(modulator) and 8(carrier).
tempop_phase = getphase(op7_0); //Save the phase!
tempphase = (tempop_phase>>2);
tempphase ^= (tempop_phase>>7);
tempphase |= (tempop_phase>>3);
tempphase &= 1; //Only 1 bit is used!
tempphase <<= 9; //0x200 when 1 makes it become 0x300
tempphase |= 0x100; //0x100 is always!
tempop_phase = getphase(op8_1); //Calculate the phase of channel 8 carrier signal!
if (((tempop_phase>>3)^(tempop_phase>>5))&1) tempphase = 0x300;

result = calcOperator(curchan, op7_0, adlibfreq(op7_0, curchan), 0.0f,1,op7_0,1); //Calculate the modulator, but only use the current time(position in the sine wave)!
result = calcOperator(curchan, op7_1, adlibfreq(op7_1, curchan), ((((float)tempphase)/(float)0x300)*(float)(PI2)), 0,op8_1,1); //Calculate the carrier with applied modulator!
immresult += OPL2_Exponential(result); //Apply the exponential!
}
if (adlibop[op8_0].volenvstatus) //Tom-tom(Carrier)?
{
result = calcOperator(curchan, op8_0, adlibfreq(op8_0, curchan), 0.0f, 0,op8_0,1); //Calculate the carrier with applied modulator!
immresult += OPL2_Exponential(result); //Apply the exponential!
}
result = immresult; //Load the resulting channel!
result *= 0.5f; //We only have half(two channels combined)!
result *= adlib_scaleFactor; //Convert to output scale (We're only going from -1.0 to +1.0 up to this point), convert to signed 16-bit scale!
return (short)result; //Give the result, converted to short!
break;
}
return 0; //Percussion isn't supported yet for this channel!
}

//Operator 1!
//Calculate the frequency to use!
result = calcOperator(curchan, op1, op1frequency, 0.0f,1,op1,1); //Calculate the modulator for feedback!

if (adlibch[curchan].synthmode) //Additive synthesis?
{
result += calcOperator(curchan, op2, adlibfreq(op2, curchan), 0.0f, 0,op2,1); //Calculate the carrier without applied modulator additive!
}
else //FM synthesis?
{
result = calcOperator(curchan, op2, adlibfreq(op2, curchan), OPL2_Exponential(result), 0,op2,1); //Calculate the carrier with applied modulator!
}

result = OPL2_Exponential(result); //Apply the exponential!

result *= adlib_scaleFactor; //Convert to output scale (We're only going from -1.0 to +1.0 up to this point), convert to signed 16-bit scale!
return (short)result; //Give the result, converted to short!
}

Although some stuff sounds fine (Bass drum and Tom-tom afaik, as they're the same(in additive synthesis mode, fm mode just adds the modulator's modulating the bass drum carrier)) the other rhythm parts give incorrect results as far as I know (don't have a reference). The hi-hat sounds too clean (although some small vibrato-like vibration is present) and the cymbal(I think) seems too hard.

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 36 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've improved it a bit (also added a DRO file player to the current MIDI player (thus now renamed Music player for that reason) to test with actual correct OPL2 output recordings):

//Calculate an operator signal!
OPTINLINE float calcOperator(byte curchan, byte operator, float frequency, float modulator, byte feedback, byte volenvoperator, byte updateoperator)
{
if (operator==0xFF) return 0.0f; //Invalid operator!
float result,feedbackresult; //Our variables?
//Generate the signal!
if (feedback) //Apply channel feedback?
{
modulator = adlibop[operator].lastsignal[0]; //Take the previous last signal!
modulator += adlibop[operator].lastsignal[1]; //Take the last signal!
modulator *= adlibch[curchan].feedback; //Calculate current feedback!
modulator = OPL2_Exponential(modulator); //Apply the exponential conversion!
}

//Generate the correct signal!
result = calcAdlibSignal(adlibop[operator].wavesel&wavemask, modulator, frequency?frequency:adlibop[operator].lastfreq, &adlibop[operator].freq0, &adlibop[operator].time); //Take the last frequency or current frequency!
if (volenvoperator==0xFF) goto skipvolenv;
result *= adlibop[volenvoperator].outputlevel; //Apply the output level to the operator!
result *= adlibop[volenvoperator].volenv; //Apply current volume of the ADSR envelope!
skipvolenv: //Skip vol env operator!
if (frequency && updateoperator) //Running operator and allowed to update our signal?
{
feedbackresult = result; //Load the current feedback value!
feedbackresult *= 0.5f; //Prevent overflow (we're adding two values together, so take half the value calculated)!
adlibop[operator].lastsignal[0] = adlibop[operator].lastsignal[1]; //Set last signal #0 to #1(shift into the older one)!
adlibop[operator].lastsignal[1] = feedbackresult; //Set the feedback result!
adlibop[operator].lastfreq = frequency; //We were last running at this frequency!
incop(operator,frequency); //Increase time for the operator when allowed to increase (frequency=0 during PCM output)!
}
return result; //Give the result!
}

float adlib_scaleFactor = 65535.0f / 1018.0f; //We're running 8 channels in a 16-bit space, so 1/8 of SHRT_MAX

OPTINLINE uint_32 getphase(byte operator) //Get the current phrase of the operator!
{
return (word)((fmod(adlibop[operator].time,PI2)/PI2)*511.0f); //512 points (9-bits value?)
}

OPTINLINE short adlibsample(uint8_t curchan) {
byte op7_0, op7_1, op8_0, op8_1; //The four slots used during Drum samples!
byte tempop_phase; //Current phase of an operator!
float result,immresult; //The operator result and the final result!
byte op1,op2; //The two operators to use!
float op1frequency;
curchan &= 0xF;
if (curchan >= NUMITEMS(adlibch)) return 0; //No sample with invalid channel!

//Determine the modulator and carrier to use!
op1 = adliboperators[0][curchan]; //First operator number!
op2 = adliboperators[1][curchan]; //Second operator number!
op1frequency = adlibfreq(op1, curchan); //Load the first frequency!

if (adlibpercussion && (curchan >= 6) && (curchan <= 8)) //We're percussion?
{
register uint_32 tempphase;
result = 0.0f; //Initialise the result!
//Calculations based on http://bisqwit.iki.fi/source/opl3emu.html fmopl.c
//Load our four operators for processing!
op7_0 = adliboperators[0][7];
Show last 111 lines
		op7_1 = adliboperators[1][7];
op8_0 = adliboperators[0][8];
op8_1 = adliboperators[1][8];
switch (curchan) //What channel?
{
case 6: //Bass drum?
//Generate Bass drum samples!
//Calculate the frequency to use!
result = calcOperator(curchan, op1, op1frequency, 0.0f,1,op1,1); //Calculate the modulator for feedback!
if (adlibch[curchan].synthmode) //Additive synthesis?
{
//Special on Bass Drum: Additive synthesis(Operator 1) is ignored.
result = calcOperator(curchan, op2, adlibfreq(op2, curchan), 0.0f, 0,op2,1); //Calculate the carrier without applied modulator additive!
}
else //FM synthesis?
{
result = calcOperator(curchan, op2, adlibfreq(op2, curchan), OPL2_Exponential(result), 0,op2,1); //Calculate the carrier with applied modulator!
}

result = OPL2_Exponential(result*2.0f); //Apply the exponential! The volume is always doubled!

result *= adlib_scaleFactor; //Convert to output scale (We're only going from -1.0 to +1.0 up to this point), convert to signed 16-bit scale!
return (short)result; //Give the result, converted to short!
break;

//Comments with information from fmopl.c:
/* Phase generation is based on: */
/* HH (13) channel 7->slot 1 combined with channel 8->slot 2 (same combination as TOP CYMBAL but different output phases) */
/* SD (16) channel 7->slot 1 */
/* TOM (14) channel 8->slot 1 */
/* TOP (17) channel 7->slot 1 combined with channel 8->slot 2 (same combination as HIGH HAT but different output phases) */


/* Envelope generation based on: */
/* HH channel 7->slot1 */
/* SD channel 7->slot2 */
/* TOM channel 8->slot1 */

/* TOP channel 8->slot2 */
//So phase modulation is based on the Modulator signal. The volume envelope is in the Modulator signal (Hi-hat/Tom-tom) or Carrier signal ()
case 7: //Hi-hat(Carrier)/Snare drum(Modulator)? High-hat uses modulator, Snare drum uses Carrier signals.
immresult = 0.0f; //Initialize immediate result!
if (adlibop[op7_1].volenvstatus) //Snare drum on modulator?
{
//Derive frequency from channel 0.
tempphase = 0x100<<((getphase(op7_0)>>8)&1); //Bit8=0(Positive) then 0x100, else 0x200! Based on the phase to generate!
tempphase ^= (OPL2_RNG<<8); //Noise bits XOR'es phase by 0x100 when set!
result = calcOperator(curchan, op7_0, op1frequency, 0.0f,1,op7_0,(!adlibop[op8_1].volenvstatus && !adlibop[op7_0].volenvstatus)); //Calculate the modulator, but only use the current time(position in the sine wave)!
result = calcOperator(curchan, op7_1, adlibfreq(op7_1, curchan), OPL2_Exponential((((float)tempphase)/(float)0x300)*3137.0f), 0,op7_1,1); //Calculate the carrier with applied modulator!
immresult += OPL2_Exponential(result); //Apply the exponential!
}
if (adlibop[op7_0].volenvstatus) //Hi-hat on carrier?
{
//Derive frequency from channel 7(modulator) and 8(carrier).~
tempop_phase = getphase(op7_0); //Save the phase!
tempphase = (tempop_phase>>2);
tempphase ^= (tempop_phase>>7);
tempphase |= (tempop_phase>>3);
tempphase &= 1; //Only 1 bit is used!
tempphase = tempphase?(0x200|(0xD0>>2)):0xD0;
tempop_phase = getphase(op8_1); //Calculate the phase of channel 8 carrier signal!
if (((tempop_phase>>3)^(tempop_phase>>5))&1) tempphase = 0x200|(0xD0>>2);
if (tempphase&0x200)
{
if (OPL2_RNG) tempphase = 0x2D0;
}
else if (OPL2_RNG) tempphase = (0xD0>>2);

result = calcOperator(curchan, op7_0, adlibfreq(op7_0, curchan), 0.0f,1,op7_0,!adlibop[op8_1].volenvstatus); //Calculate the modulator, but only use the current time(position in the sine wave)!
result = calcOperator(curchan, op7_1, adlibfreq(op7_1, curchan), OPL2_Exponential((((float)tempphase)/(float)0x300)*3137.0f), 0,op7_0,1); //Calculate the carrier with applied modulator!
immresult += OPL2_Exponential(result); //Apply the exponential!
}
result = immresult; //Load the resulting channel!
result *= 0.5f; //We only have half(two channels combined)!
result *= adlib_scaleFactor; //Convert to output scale (We're only going from -1.0 to +1.0 up to this point), convert to signed 16-bit scale!
return (short)result; //Give the result, converted to short!
break;
case 8: //Tom-tom(Carrier)/Cymbal(Modulator)? Tom-tom uses Modulator, Cymbal uses Carrier signals.
immresult = 0.0f; //Initialize immediate result!
if (adlibop[op8_1].volenvstatus) //Cymbal(Modulator)?
{
//Derive frequency from channel 7(modulator) and 8(carrier).
tempop_phase = getphase(op7_0); //Save the phase!
tempphase = (tempop_phase>>2);
tempphase ^= (tempop_phase>>7);
tempphase |= (tempop_phase>>3);
tempphase &= 1; //Only 1 bit is used!
tempphase <<= 9; //0x200 when 1 makes it become 0x300
tempphase |= 0x100; //0x100 is always!
tempop_phase = getphase(op8_1); //Calculate the phase of channel 8 carrier signal!
if (((tempop_phase>>3)^(tempop_phase>>5))&1) tempphase = 0x300;

result = calcOperator(curchan, op7_0, adlibfreq(op7_0, curchan), 0.0f,1,op7_0,1); //Calculate the modulator, but only use the current time(position in the sine wave)!
result = calcOperator(curchan, op7_1, adlibfreq(op7_1, curchan), OPL2_Exponential((((float)tempphase)/(float)0x300)*3137.0f), 0,op8_1,1); //Calculate the carrier with applied modulator!
immresult += OPL2_Exponential(result); //Apply the exponential!
}
if (adlibop[op8_0].volenvstatus) //Tom-tom(Carrier)?
{
result = calcOperator(curchan, op8_0, adlibfreq(op8_0, curchan), 0.0f, 0,op8_0,1); //Calculate the carrier with applied modulator!
immresult += OPL2_Exponential(result); //Apply the exponential!
}
result = immresult; //Load the resulting channel!
result *= 0.5f; //We only have half(two channels combined)!
result *= adlib_scaleFactor; //Convert to output scale (We're only going from -1.0 to +1.0 up to this point), convert to signed 16-bit scale!
return (short)result; //Give the result, converted to short!
break;
}
return 0; //Percussion isn't supported yet for this channel!
}
... Melodic channel processing follows after this ...

The sound doesn't sound right, none of the instruments give correct output for some reason.

The song played is the Rhythm.dro file from DOSbox' OPL Emulation - Rhythm Sounds

Filename
recording_63.zip
File size
736.51 KiB
Downloads
54 downloads
File comment
The recording of the latest x86EMU commit playing the Rhythm.dro file.
File license
Fair use/fair dealing exception

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 37 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

Tried implementing https://github.com/stohrendorf/ppplay/blob/ma … pegenerator.cpp , but this doesn't work yet (see latest commit at https://bitbucket.org/superfury/x86emu/src/56 … lib.c?at=master ).

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 38 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

The ADSR is now implemented (goes from 0 to 64(attack), then back to 0 during the decay and release phases). The problem left is that, while the volume envelope and volume are now implemented according to http://yehar.com/blog/?p=665 , it practically does nothing: The input data (the result from the LogSin table) has a range of 0 to 2137. Adding the volume envelope (range 0-64, uses times 8(SHL 3)) and volume itself (SHL 5=times 32) results in adding up to 2520 to the sample from the LogSin table. Thus a result of 0-4657. Thus the volume is only half dictated by the envelope and volume. Having zero volume and envelope (=silence) will result in the source signal being full strength(2137) instead of 0?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io