VOGONS


(Dual) OPL2 vs OPL3?

Topic actions

First post, by superfury

User metadata
Rank l33t++
Rank
l33t++

Is it easy to add OPL3 support to accurate OPL2 emulation?

From what I can see it's just some (compared to dual OPL2) cross-chip AM/FM synthesis (4-OP mode for up to 6 lower dual-chip channels), stereo field select (left on/off, right on/off) and some extra 4 waveforms (perhaps created based on binary logic and the existing OPL2 waveforms?).

For 4-OP mode, simply bridge the OPL2 left chip(port 220h) to the right chip (port 222h) instead of left chip directly to output on the backend for AM or FM synthesis? In this case, that's operator 2 to 3 in the diagrams (counting 1 through 4)?

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

Reply 1 of 11, by mkarcher

User metadata
Rank l33t
Rank
l33t

As far as I know, there is a comparison between the digital sound output of the OPL2 and the OPL3, and they found out that the OPL3 does not generate bitwise identical sample data compared to the OPL2. Alas, I don't remember whether this was some blog post or a VOGONs post.

Reply 2 of 11, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've right now simply started implementing the OPL3 based on my OPL2 emulation for now.

The only thing I'm currently stumbling on is the logarithmic sawtooth (waveform #7).
Is it like a normal sine at double speed, but inverted in that case? Since it should be OPL2 compatible with it's lookup tables (probably the same method as OPL2?), it should probably be derived of the 256-entry lookup table too (of a quarter sine wave)?

Any idea how the logarithmic sawtooth is derived from the LogSin tables?

Last edited by superfury on 2025-06-14, 16:04. Edited 1 time in total.

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

Reply 3 of 11, by mkarcher

User metadata
Rank l33t
Rank
l33t
superfury wrote on Yesterday, 14:25:

The only thing I'm currently stumbling on is the logarithmic sawtooth (waveform #7).

I assume the logarithmic sawtooth does not use a lookup table, but directly sends the phase accumulator value to the DAC, which uses a kind of floating-point/logarithmic encoding.

Reply 4 of 11, by superfury

User metadata
Rank l33t++
Rank
l33t++
mkarcher wrote on Yesterday, 14:29:
superfury wrote on Yesterday, 14:25:

The only thing I'm currently stumbling on is the logarithmic sawtooth (waveform #7).

I assume the logarithmic sawtooth does not use a lookup table, but directly sends the phase accumulator value to the DAC, which uses a kind of floating-point/logarithmic encoding.

But if it didn't use the lookup tables, it would have to resort to very slow floating point algorithmic, wouldn't it? I'd assume it uses some kind of trick like it does with the other waveforms (using the first quarter of the sinus) and perhaps the sign bit too?

It's only got one LogSin lookup table after all. And seeing as it doesn't perform the Exponential table lookup until later in the process, it probably does something to the LogSin table lookup input (index) or the table's output to archieve the 8th wave generation?

Edit: Or supplying a direct mantissa/exponent value set depending on the lookup entry perhaps?

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

Reply 5 of 11, by mkarcher

User metadata
Rank l33t
Rank
l33t
superfury wrote on Yesterday, 16:06:
mkarcher wrote on Yesterday, 14:29:

I assume the logarithmic sawtooth does not use a lookup table, but directly sends the phase accumulator value to the DAC, which uses a kind of floating-point/logarithmic encoding.

But if it didn't use the lookup tables, it would have to resort to very slow floating point algorithmic, wouldn't it?

No, that's not what I meant. You get a phase angle as integer, which you use as index into the LogSin table to obtain sine-like waveforms. As you already understand correctly, the lookup into this table is modified to obtain different shapes of sine-derived waveforms. The cool thing about the output of that table being logarithmic is that all multiplications to shape the volume (like the tremolo effect and the envelope generator) are implemented as additions, making both the computation easy and allowing a high dynamic range with a limited number of bits.

Going through the "exp" table is the last step of synthesis for each operator, converting the logarithmic value that can easily be multiplied into a linear value that can easily be mixed by adding the voices or used as linear phase delta for FM.

My suggestion for the log waveform was not entirely correct, I didn't think about the exp table, so talking about "value to the DAC" was nonsense, but the main idea is that you most easily implement a waveform like that within the OPL3 architecture by using the top bit of the accumulator as sign bit. If it is set, you flip all the other bits, and then just treat those bits as a logarithmic value. Putting the triangle-shaped input the exp table (later) will create the "logarithmic sawtooth".

superfury wrote on Yesterday, 16:06:

Edit: Or supplying a direct mantissa/exponent value set depending on the lookup entry perhaps?

No lookup. The phase angle directly is interpreted as mantissa/exponent!

Reply 6 of 11, by superfury

User metadata
Rank l33t++
Rank
l33t++

I did some experimenting. I simply adjusted the logsin lookup for that case with (according to the format of the floating point using mantissa/exponent).

Since the format of the mantissa/exponent/lookup lookup table is lower 8 bits, upper 3 bits, and a sign bit (implemented as bit 15, which is way out of range for ease of lookup).
Thus the lowest 8 bits, higher 3 bits and top bit(sign bit) are given to the exp lookup table.
Simply shift left the 8-bit index (range of 0-255) left by 3 to move the top 3 bits into the exponent input, remaining lower 5 bits into the mantissa input and the sign input used directly). In my emulator's case, the sign/mantissa/exponent lookup is done using a simple lookup table (64K entries). The bottom 8 bits are the mantissa, bits 8-10 are the exponent and bit 15 is the sign).

So the LogSin sine wave lookup function simply checks for the specific case of the derived square wave, then if that's detected perform:
- output from the table is instead the table location shifted left by 3 bits.
- If the lookup is supposed to be performed for the second/third quarters (it keeps a 2-bit value which is the PI portion of the wave being requested, thus it's a simple case of the two bits of that not being equal (thus #1 or #2 (the quarter being 0-based, so it's first quarter, second quarter, third quarter, fourth quarter when read in decimal, or 00b, 01b, 10b, 11b))).
The final part that's applied to the normal sign determination (using bit 1 of the quarter/half sine indicator mentioned above) is determined as usual.

I seem to get a normal inverted-ish sine wave-ish result now (or something that at least looks correct)? Only of course it's exponential-based instead, as the input to the exponential table is simply linear in nature (with the mantissa/exponent generating the curve directly). The lowest 3 bits of the 'angle' in the mantiassa part are cleared of course.

Below file is simply generated by performing the sinus lookup for 1 sinus per second, then passing it's output through the exponential table and converting the result to a valid 16-bit PCM format range for mono output.

The attachment adlibwave.7z is no longer available

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

Reply 7 of 11, by mkarcher

User metadata
Rank l33t
Rank
l33t
superfury wrote on Yesterday, 17:02:

Simply shift left the 8-bit index (range of 0-255) left by 3 to move the top 3 bits into the exponent input, remaining lower 5 bits into the mantissa input and the sign input used directly).

Yeah, that's what I meant to say.

superfury wrote on Yesterday, 17:02:

Below file is simply generated by performing the sinus lookup for 1 sinus per second, then passing it's output through the exponential table and converting the result to a valid 16-bit PCM format range for mono output.

The attachment adlibwave.7z is no longer available

Looks very similar to https://doomwiki.org/wiki/OPL_emulation , so I think you got it correct. Switching audacity to "dB" display (which generates a logarithmic scale) shows the expected sawtooth for wave shape number 8. On the other hand, I notice that you get no points more quiet than around -51dB, which indicates a resolution of just 9 bits + sign, i.e. 10 bits total. Is the exp table in the OPL2 that coarse, or did you hit some resolution limit in your emulator?

Reply 8 of 11, by superfury

User metadata
Rank l33t++
Rank
l33t++

Hmmm.. Assuming sign bit isn't included in the 'floating' point decimal numbers used, how many bits are remaining in the mantissa/exponent part that's the low bits? Or do I simply need to ignore those bits?
I mean the bits that get input into the Exponential table formula.

	//Reverse the range given! Input 0=Maximum volume, Input max=No output.
if (v > MaximumExponential) v = MaximumExponential; //Limit to the maximum value available!
v = MaximumExponential - v; //Reverse our range to get the correct value!
#ifdef IS_LONGDOUBLE
return sign * (DOUBLE)(OPL2_ExpTable[v & 0xFF] + 1024) * pow(2.0L, (DOUBLE)(v >> 8)); //Lookup normally with the specified sign, mantissa(8 bits translated to 10 bits) and exponent(3 bits taken from the high part of the input)!
#else
return sign * (DOUBLE)(OPL2_ExpTable[v & 0xFF] + 1024) * pow(2.0, (DOUBLE)(v >> 8)); //Lookup normally with the specified sign, mantissa(8 bits translated to 10 bits) and exponent(3 bits taken from the high part of the input)!

In this case that's the MaximumExponential variable.
The last builds have it set to "(0x3F<<5)+(0x1FF(being silence)<<3)+logsintable[0]".

It effectively determines the range of the ExpTable formula inputs (low 8 bits being mantissa and top bits being exponent).
As can be seen, it's used by my OPL2 implementation to invert the range to get maximum range of inputs.
Edit: Tried adjusting it to 1FFFh instead (8 bits mantissa, 5 bits exponent, being the "v" variable range), seeing as that's at least what the implementation needs (including volume envelopes etc)?
Even the lookup table itself far exceeds 11 bits itself? At least 13 bits including the volume envelope's silence value (1FFh)?

The new output with OPL3 defines enabled to enable the unfinished OPL3 emulation (forced on).

The attachment adlibwave.7z is no longer available

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

Reply 9 of 11, by mkarcher

User metadata
Rank l33t
Rank
l33t
superfury wrote on Yesterday, 21:44:
[…]
Show full quote
	//Reverse the range given! Input 0=Maximum volume, Input max=No output.
if (v > MaximumExponential) v = MaximumExponential; //Limit to the maximum value available!
v = MaximumExponential - v; //Reverse our range to get the correct value!
#ifdef IS_LONGDOUBLE
return sign * (DOUBLE)(OPL2_ExpTable[v & 0xFF] + 1024) * pow(2.0L, (DOUBLE)(v >> 8)); //Lookup normally with the specified sign, mantissa(8 bits translated to 10 bits) and exponent(3 bits taken from the high part of the input)!
#else
return sign * (DOUBLE)(OPL2_ExpTable[v & 0xFF] + 1024) * pow(2.0, (DOUBLE)(v >> 8)); //Lookup normally with the specified sign, mantissa(8 bits translated to 10 bits) and exponent(3 bits taken from the high part of the input)!

That code looks sensible. Looking at your wave file, I find that all the positive samples are 10 variable bits with five one bits concatenated to them (so the are xx1F, xx3F, xx5F, xx7F, xx9F, xxBF, xxDF or xxFF), and the negative samples are looking likewise. While the 8-bit logarithmic "mantissa" is supposed to be expanded to a 10-bit linear mantissa, having a sign bit, 10 variable bits and the remaining bits "padding" makes sense, but as the output of the logsin is between 0 and 2137, I would expect v>>8 to vary between 0 and 8. Assuming we process the values of the LogSin table directly, we would have MaximumExponental at 2137. The first three LogSin samples after negation are 0, 406 and 594. That is "exponent zero, mantissa zero", "exponent one, mantissa 150" and "exponent two, mantissa 82". Translating that using this lookup formula yields 1024, 2*(1024+513) = 3074 and 5116. The maximum value, 2137, is exponent 8, mantissa 89, which yields 333568. If you normalize 333568 to 32767, the lowest three values translate to 101, 302 and 503. Yet, your wave file has 95, 287 and 479, each of them being a multiple of 32 minus 1.

It seems somewhere in the processing chain used to generate the wave file you attached, some resolution got lost. It's likely not the OPL emulation code, though.

The log-sawtooth waveform seems to be impacted by this more than it should. Are you cutting the 2nd and 3rd quarter to plain zero? I'd expect that shifting the index just by 2 bits instead of 3 bits, and using a 9-bit index instead is more plausible. But if you want to be sure, you should check what NukedOPL does.

Reply 10 of 11, by superfury

User metadata
Rank l33t++
Rank
l33t++

For the 2nd and 3rd quarter of the log-sawtooth waveform, it should just return the first entry #0 (entry #0) of the LogSin table, thus giving silence (that's based on the detection of the first,second,third,fourth quarter of the waveform directly)? From what I see in Audacity, it looks like the 3rd quarter position has that correctly, but somehow the 2nd quarter of the wave doesn't somehow (slightly above 0 perhaps)?

The inputs to that logsin-to-exponentional function are simply the direct outputs of the sinus lookup. The normal rendering also adds other things, like volume envelope attenuation (calculated value (range of 0 through 1FFh)<<3), channel volume attenuation (setting<<5, but 0 when ignored for a channel) and ksl ROM also adds attenuation (based on Dosbox's code from what I remember, eventually shifted left by 3) as does tremolo (0 through 26).

I also improved my OPL2/dual OPL2 player (VGM/DRO files) to support OPL3. If it detects OPL3 being emulated with dual OPL2 files, it will simply enable OPL3 mode (register 5 bit 0 at base+3) manually and set the channel registers to left and right channels for dual OPL2 compatibility. It doesn't enforce any registers to be read-only on those bits though, so if a song currently overwrites them, it'll change the left/right panning of the instruments.
Edit: Somehow, after playing a file once, the OPL2 sounds get messed up somehow? I only adjusted the DRO player a bit to clear all registers properly before starting to play any DRO file (and set register 05h on the second chip to 00h for OPL2 compatiblity mode).
Edit: I think I managed to fix the issue. There was some initialization and post-playback clearing issue with OPL2/OPL3 registers and compatibility modes.

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

Reply 11 of 11, by superfury

User metadata
Rank l33t++
Rank
l33t++

Hmmm... Seeing as almost all of OPL3 is implemented right now (except the 4-OP mode, which is just a wrapping of inputs to outputs and skipping of some channels for the 2nd set of 3 channels (channel 4-6 on a Dual OPL2 chip configuration perspective, for each of the two chips) when it's enabled for one of the first 3 channels (channel 0 skipping channel 3, channel 1 skipping channel 4, channel 2 skipping channel 5),

How is the 4-OP mode implemented? Does each set of 2 channels (0/3, 1 /4, 2/5) have it's own frequency setting for example? Of course disabling feedback on the higher channel from what I understand?
So can you configure the first 2 operators frequency independently from the latter 2 operators? Them just being linked by the modulation (FM or AM)?

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