VOGONS


First post, by superfury

User metadata
Rank l33t++
Rank
l33t++

The OPL2 melodic emulation works very well currently(Fully 100% accurate), but still some part is missing(or rather, it isn't completely finished yet. It's running, but not 100% correct output yet): the rhythm part of the OPL2 emulation. The formulas were originally taken from OPL3EMU, but that probably isn't completely correct for OPL2 emulation?

Current code(the bottom part of the code is the normal, 100% accurate, melodic part) for rendering rhythm channels:

OPTINLINE float adlibsample(uint8_t curchan) {
byte op6_1, op6_2, op7_1, op7_2, op8_1, op8_2; //The four slots used during Drum samples!
word tempop_phase; //Current phase of an operator!
float result;
float immresult; //The operator result and the final result!
byte op1,op2; //The two operators to use!
float op1frequency, op2frequency;
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); //Load the modulator frequency!
op2frequency = adlibfreq(op2); //Load the carrier frequency!

if (adlibpercussion && (curchan >= 6) && (curchan <= 8)) //We're percussion?
{
#ifndef ADLIB_RHYTHM
return 0.0f; //Disable percussion!
#else
INLINEREGISTER word tempphase;
result = 0; //Initialise the result!
//Calculations based on http://bisqwit.iki.fi/source/opl3emu.html fmopl.c
//Load our four operators for processing!
op6_1 = adliboperators[0][6];
op6_2 = adliboperators[1][6];
op7_1 = adliboperators[0][7];
op7_2 = adliboperators[1][7];
op8_1 = adliboperators[0][8];
op8_2 = adliboperators[1][8];
switch (curchan) //What channel?
{
case 6: //Bass drum?
//Generate Bass drum samples!
//Special on Bass Drum: Additive synthesis(Operator 1) is ignored.

//Calculate the frequency to use!
result = calcOperator(6, op6_1,op6_1,op6_1, adlibfreq(op6_1), 0.0f,0x00); //Calculate the modulator for feedback!

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

return result; //Apply the exponential! The volume is always doubled!
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) */


Show last 90 lines
				/* 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) //Hi-hat on Modulator?
{
//Derive frequency from channel 7(modulator) and 8(carrier).
tempop_phase = getphase(op7_1,adlibfreq(op7_1)); //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_2,adlibfreq(op8_2)); //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(8, op8_2,op8_2,op8_2,adlibfreq(op8_2), 0.0f,((adlibop[op8_2].volenvstatus)?1:0)); //Calculate the modulator, but only use the current time(position in the sine wave)!
result = calcOperator(7, op7_1,op7_1,op7_1,adlibfreq(op7_1), convertphase(tempphase), 2|((adlibop[op8_2].volenvstatus||adlibop[op7_2].volenvstatus)?0x09:0x08)); //Calculate the carrier with applied modulator!
immresult += result; //Apply the tremolo!
}
if (adlibop[op7_2].volenvstatus) //Snare drum on Carrier volume?
{
//Derive frequency from channel 0.
tempphase = 0x100 << ((getphase(op7_1, adlibfreq(op7_1)) >> 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(7, op7_2,op7_2,op7_2,adlibfreq(op7_2), convertphase(tempphase), 0); //Calculate the carrier with applied modulator!
result = calcOperator(7, op7_1,op7_1,op7_2,adlibfreq(op7_1), convertphase(tempphase), ((adlibop[op8_2].volenvstatus) ? 0x09 : 0x08)); //Calculate the carrier with applied modulator!
immresult += result; //Apply the tremolo!
}
result = immresult; //Load the resulting channel!
result *= 0.5; //We only have half(two channels combined)!
return 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) //Tom-tom(Modulator)?
{
result = calcOperator(8, op8_1, op8_1, op8_1, adlibfreq(op8_1), 0.0f, 0x8); //Calculate the carrier without applied modulator additive! Ignore volume!
immresult += result*2.0f; //Apply the exponential!
}
if (adlibop[op8_2].volenvstatus) //Cymbal(Carrier)?
{
//Derive frequency from channel 7(modulator) and 8(carrier).
tempop_phase = getphase(op7_1,adlibfreq(op7_1)); //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,adlibfreq(op8_1)); //Calculate the phase of channel 8 carrier signal!
if (((tempop_phase>>3)^(tempop_phase>>5))&1) tempphase = 0x300;

result = calcOperator(7, op7_1,op7_1,op7_1, adlibfreq(op7_1), 0.0f,0); //Calculate the modulator, but only use the current time(position in the sine wave)!
result = calcOperator(8, op8_2,op8_2,op8_2, adlibfreq(op8_2), convertphase(tempphase), 0x8); //Calculate the carrier with applied modulator! Use volume!
immresult += result; //Apply the exponential!
}
result = immresult; //Load the resulting channel!
result *= 0.5; //We only have half(two channels combined)!
return result; //Give the result, converted to short!
break;
}
#endif
//Not a percussion channel? Pass through!
}

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

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

return result; //Give the result!
}

The final comment("//Operator 1") is the start of the code running for melodic instrument rendering.

This is some extra logic for rendering(calculating operator output):

OPTINLINE float calcModulator(float modulator)
{
return (float)(modulator*(PI*8.0f)); //Calculate current modulation! 8 periods range!
}

OPTINLINE float calcFeedback(byte channel, ADLIBOP *operator)
{
return ((operator->lastsignal[0]+operator->lastsignal[1])*0.5f*adlibch[channel].feedback); //Calculate current feedback
}

//Calculate an operator signal!
OPTINLINE float calcOperator(byte channel, byte operator, byte timingoperator, byte volenvoperator, float frequency, float modulator, byte flags)
{
if (operator==0xFF) return 0.0f; //Invalid operator!
INLINEREGISTER word result, gain; //The result to give!
float result2; //The translated result!
float activemodulation;
//Generate the signal!
if (flags&0x80) //Apply channel feedback?
{
activemodulation = calcFeedback(channel,&adlibop[timingoperator]); //Apply this feedback signal!
}
else //Apply normal modulation?
{
activemodulation = calcModulator(modulator); //Use the normal modulator!
}

//Generate the correct signal! Ignore time by setting frequency to 0.0f(effectively disables time, keeping it stuck at 0(frequencytime))!
result = calcOPL2Signal(adlibop[operator].wavesel&wavemask, (frequency?frequency:adlibop[timingoperator].lastfreq),activemodulation, &adlibop[timingoperator].freq0, &adlibop[timingoperator].time); //Take the last frequency or current frequency!

//Calculate the gain!
gain = 0; //Init gain!
if (flags&2) //Special: ignore main volume control!
{
gain += outputtable[0]; //Always maximum volume, ignore the volume control!
}
else //Normal output level!
{
gain += adlibop[volenvoperator].outputlevel; //Current gain!
}
gain += adlibop[volenvoperator].gain; //Apply volume envelope and related calculations!
gain += adlibop[volenvoperator].m_kslAdd2; //Add KSL preprocessed!

//Now apply the gain!
result += gain; //Simply add the gain!
if (flags&8) //Double the volume?
{
result <<= 1; //Double the volume!
}
result2 = OPL2_Exponential(result); //Translate to Exponential range!

if (frequency && ((flags&1)==0)) //Running operator and allowed to update our signal?
{
adlibop[timingoperator].lastsignal[0] = adlibop[timingoperator].lastsignal[1]; //Previous last signal!
adlibop[timingoperator].lastsignal[1] = result2; //Set last signal #0 to #1(shift into the older one)!
adlibop[timingoperator].lastfreq = frequency; //We were last running at this frequency!
incop(timingoperator,frequency); //Increase time for the operator when allowed to increase (frequency=0 during PCM output)!
}
result2 = OPL2_Tremolo(operator,result2); //Apply tremolo as well, after applying the new feedback signal(don't include tremolo in it)!
return result2; //Give the translated result!
Show last 18 lines
}

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

OPTINLINE word getphase(byte operator, float frequency) //Get the current phrase of the operator!
{
return (word)(fmodf((adlibop[operator].time*frequency),1.0)*((float)0x3D0)); //Give the 10-bits value
}

word convertphase_real(word phase)
{
return ((phase&0x200)?0:SIGNBIT)|(word)((phase&0x1FF)*((1.0f/(float)0x1FF)*SIGNMASK)); //Give the phase to execute, as a full sinus modulation!
}

float convertphase(word phase)
{
return OPL2_Exponential(phaseconversion[phase]); //Lookup the phase translated!
}

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