VOGONS


Reply 60 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've made a little modification to the way the Tremolo&Vibrato signals are generated:
https://bitbucket.org/superfury/x86emu/src/38 … lib.c?at=master

I just made it use the LogSin&Exponential functions to create a sin() wave in the format the OPL2 uses, then converted the resulting -1 to +1 wave to a triangular wave by reversing the sin() using asin() to convert it back into 2*PI*frequenxy*time for the OPL2 signal, then using the code from the pspsdk's wavegen example to convert the timestamp into a triangular wave.

Can you see what's causing the noise when trying to generate output? Playing any song introduces strong noise into the sound(this wasn't the case with the original float-style synthesis), instead of being pure sound. This happens with most recordings I have, except for some parts of Nautical.

Edit: Fixed most of the noise by reversing the volume envelope, substracting the volume from the samples, clipping to 0 (when <=gain it becomes 0, else it's between 0 and gain). Thus both gains giving 63(Silence) give zero output and both being 0(Full volume) gives full output.

Thus this fixes most of noise in the songs. The feedback(at least I think it's the feedback) still gives noisy output through?
https://bitbucket.org/superfury/x86emu/src/21 … lib.c?at=master

Can either you see what's going wrong here?

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

Reply 61 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

Just thought about something: the documentation says the gain is added to the logsin. But the formulas given for volume are unsigned(positive) values((Attenuation<<5)+(VolEnv<<3)). When this is added to the LogSin, it would only make the signal louder instead of attenuating it. Thus the addition is actually negative(thus substraction, clipping to zero)? This would make the volume actually go the way it's supposed to: from silence(0V, attack phase) to max(MaxV), back to sustain(part/none of MaxV, decay phase), hold(sustain) and finally back to silence(0V, release phase)?

Anyone can confirm this? It would make no sense to actually add the attenuation to the sine wave. Then it isn't attenuation, but simply amplification?

Also during the main menu screen of Ultima VI, there's noise in the background together with the most prominent foreground tones(The pumm, pum pum pum, pumm, pum, pum low tone part it starts with). Is the feedback correctly handled?

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

Reply 62 of 112, by Jepael

User metadata
Rank Oldbie
Rank
Oldbie
superfury wrote:

Just thought about something: the documentation says the gain is added to the logsin. But the formulas given for volume are unsigned(positive) values((Attenuation<<5)+(VolEnv<<3)). When this is added to the LogSin, it would only make the signal louder instead of attenuating it. Thus the addition is actually negative(thus substraction, clipping to zero)? This would make the volume actually go the way it's supposed to: from silence(0V, attack phase) to max(MaxV), back to sustain(part/none of MaxV, decay phase), hold(sustain) and finally back to silence(0V, release phase)?

Anyone can confirm this? It would make no sense to actually add the attenuation to the sine wave. Then it isn't attenuation, but simply amplification?

No, it's very simple:

logsin 0 is peak amplitude, logsin 2137 is minimum amplitude near zero crossing (maximum attenuation).
volume 0 is peak volume, volume 63 is minimum volume (max attenuation).
envelope 0 is peak envelope, envelope 511 is minimum volume (max attenuation).

When you sum attenuations together, 0 is peak amplitude, and any number larger than 0 will only decrease amplitude.

Reply 63 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

So in that way:
gain = (evenlope shl 3) + (attenuation<<5)
signal = LogSin[t]
output1 = exp[signal+gain]

But the exponential goes from 0 to maximum. Since it starts at 2137+gain(which is maximum at the start, thus (0x3F<<5)+(0x1FF<<3)=7E0+FF8=17D8), thus has a top of 2137+6104=8241. So 8241 taken from the Exponential table would be 0V and 0 taken from the Exponential table would be the maximum voltage?

So I should reverse the value that comes out of LogSin[t]+gain by substracting it from 8241(after removing the sign), then add the sign back to the result and use that to look it up in the Exponential table?

Thus the value to look up is 14-bits wide excluding the 1-bit sign, thus it's actually a 15-bits signed number inputted into the Exponential table?

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

Reply 64 of 112, by stohrendorf

User metadata
Rank Newbie
Rank
Newbie

The core identity used is: sin(x)/alpha = exp(ln(sin(x)/alpha)) = exp(ln(sin(x)) - ln(alpha)). Remember that the sinLog table is stored negated, thus it's effectively (with exp'(x) = exp(-x)): exp'(-ln(sin(x)) + ln(alpha))). With ln(alpha) = env, this is pretty much how to to do it.

Reply 65 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've implemented the latest information gotten from Jepael and my last post into my calculations:
https://bitbucket.org/superfury/x86emu/src/b3 … lib.c?at=master

A little recording of playing some songs with that code(beware, it plays back at high speed because the DRO player in x86EMU isn't synchronized with the core emulation, and thus is out of sync sending the OPL2 output instructions way too fast(realtime) for the OPL2 emulation to play back and record(sub-realtime because the total emulation is too heavy for the CPU), which runs below 100% speed on the laptop it's recorded):
https://www.dropbox.com/s/py89p5s8fhcqhoy/adl … 6_1548.wav?dl=0

0:00 Main opening theme of Ultima VI
0:40 Main menu theme of Ultima VI
1:01 Introduction theme of Ultima VI
2:28 Main menu theme of Ultima VI again
4:41 Rhythm.DRO
2:26 Megarace's NGLoop song
3:32 The Zeliard.DRO from the same thread as Rhythm.DRO
3:53 Megarace's Newsan theme (first level theme)

Is the noise at the main menu theme of Ultima VI correct? I don't think it should be hearable there?

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

Reply 66 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

A little re-recording, having adjusted the main emulation loop and DRO player to run from the main emulation loop to fix the speed issue:
https://www.dropbox.com/s/lgo4awe4r4gyjkl/adl … 6_1854.wav?dl=0

0:00 A recording of megarace's NGLoop song
0:49 Megarace's Newsan song

Anyone knows why it sounds so noisy at times? Is there a problem with the modulation or feedback?
https://bitbucket.org/superfury/x86emu/src/7c … lib.c?at=master

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

Reply 67 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

One strange thing I just found on Adlib Tracker II documentation:
http://battleofthebits.org/lyceum/View/Utilis … lib+Tracker+II/

Panning, Finetune, Feedback, FM/AM: Global instrument settings. Panning is simple. Hard left, Center, Hard right. Finetune is also simple. Feedback is how much does the modulator affect the carrier, and lastly, FM/AM just chooses if the modulator should modulate the carrier, or if it should just produce its own tone.

This literally says that the feedback specified affects the degree the modulator affects the carrier. I thought feedback is actually the degree the modulator modulates itself(literal feedback), the result of this self modulation modulating the carrier? Which one is correct?

1. With feedback: Modulator (last output) x feedback strength -> Modulator x 2 x PI -> Carrier -> Out
2. Modulator x feedback strength -> Carrier -> Out(as described in that article in this post)

Which one is correct? I currently am emulating case 1. Is this actually correct or have I made a mistake?

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

Reply 68 of 112, by stohrendorf

User metadata
Rank Newbie
Rank
Newbie

http://map.grauw.nl/resources/sound/yamaha_ymf262.pdf page 12: feedback denotes the feedback (self-modulation) of the first operator, cnt denotes the connection type (additive or phase-modulated synthesis). Or (simplified):

    int16_t channelOutput = m_operators[0]->nextSample(feedback());
pushFeedback(channelOutput);

if(!m_cnt)
{
// CNT = 0, the operators are in series, with the first in feedback.
channelOutput = m_operators[1]->nextSample(channelOutput);
}
else
{
// CNT = 1, the operators are in parallel, with the first in feedback.
channelOutput += m_operators[1]->nextSample();
}

Reply 69 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

And what values does pushfeedback push and feedback() return? Is this value just the last outputted sample of the modulator(channelOutput value in your code, so if it's 0x1234 for the first sample, the first call of feedback() returns 0x0000, The second call 0x1234, etc.)?

Also, how does the feedback strength affect this all?

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

Reply 70 of 112, by nukeykt

User metadata
Rank Member
Rank
Member

Verified feedback algorithm:

//reg_fb - value of feedback register
//fb - calculated feedback value that goes to modulator
//out - output of modulator
//prevout - previous output of modulator
//generate - FM synthesis
//
//you should store 'prevout' and 'out' values for next iteration
//range of 'out' is +-4096
if(reg_fb == 0)
{
fb = 0;
}
else
{
fb = (prevout + out) >> (9-reg_fb);
}
prevout = out;
out = generate(fb);

Reply 71 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

Is this correct?

OPTINLINE float calcModulator(float modulator)
{
return modulator*PI2; //Calculate current modulation!
}

OPTINLINE float calcFeedback(byte channel, ADLIBOP *operator)
{
return ((operator->lastsignal[0]+operator->lastsignal[1])*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 sample; //Our variables?
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_kslAdd; //Add KSL!

//Now apply the gain!
result += gain; //Simply add the gain!
result2 = OPL2_Exponential(result); //Translate to Exponential range!

skipvolenv: //Skip vol env operator!
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!
}

adlibch[channel].feedback is a value of the list of [0.0, PI/16, PI/8, PI/4, PI/2, PI, 2*PI and 4*PI]. The modulation range used is degrees (The sine wave wraps around 360 degrees, or 2*PI of modulation and frequencytime, which is calculated at calcModulator. The output of calcModulator is literally the range of 0.0-1.0 for 0-360 degrees. Although it's -360 to +360 degrees in the case of the adlib modulation).

My latest source code (also a bit optimized):
https://bitbucket.org/superfury/x86emu/src/da … lib.c?at=master

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

Reply 72 of 112, by nukeykt

User metadata
Rank Member
Rank
Member
superfury wrote:
Is this correct? […]
Show full quote

Is this correct?

OPTINLINE float calcModulator(float modulator)
{
return modulator*PI2; //Calculate current modulation!
}

OPTINLINE float calcFeedback(byte channel, ADLIBOP *operator)
{
return ((operator->lastsignal[0]+operator->lastsignal[1])*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 sample; //Our variables?
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_kslAdd; //Add KSL!

//Now apply the gain!
result += gain; //Simply add the gain!
result2 = OPL2_Exponential(result); //Translate to Exponential range!

skipvolenv: //Skip vol env operator!
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!
}

adlibch[channel].feedback is a value of the list of [0.0, PI/16, PI/8, PI/4, PI/2, PI, 2*PI and 4*PI]. The modulation range used is degrees (The sine wave wraps around 360 degrees, or 2*PI of modulation and frequencytime, which is calculated at calcModulator. The output of calcModulator is literally the range of 0.0-1.0 for 0-360 degrees. Although it's -360 to +360 degrees in the case of the adlib modulation).

Real chip assumes that 1024 units is 2*pi. Operator output is in range +-4096 units or 8*pi. Let's sum two previous samples. Result is in range +-8192 or 16*pi. With alogritnm (out1+out2)>>(9-fb) we'll get values from ymf262 manual. I see that output of operator is +-1.0 in your code and that your feedback table matches with table in manual. But sum of two sample is in range +-2.0, so you should divide it by 2.0. I also noticed that you multiply modulator with 2*pi in FM synthesis. I think this should be 8*pi, or i'm wrong?

Reply 73 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

The 2*PI is actually the period of one sine wave.

calcOPL2Signal(waveform,frequency,phase,freq0,time) gives the LogSin result that's requested.
The LogSin table is looked up by normal sin() convertion (by executing modulo 2*PI and using that phase to determine the index into the OPL LogSin table. This is looked up and the LogSin entry is returned, with the sign bit set when it's the second half of the wave(modulo 2PI>=PI, reverse lookup when modulo PI>=0.5*PI). The rest is simply a factor conversion from modulo 0-0.5PI to 0-255 for the index into the table, which is reversed as described the previous sentence by negating it.).
The phase (which repeats every 2*PI(360 degrees of time)) looked up is the following calculation given to the LogSin table OPL2SinWave(const float r), although there's a little step between it (OPL2_Sin(byte signal, float frequencytime)) which converts the full LogSin wave, silencing and reversing it's output for the other 3 waves (absolute, absolute silenced, sawtooth(=half absolute silenced)):

OPTINLINE word calcOPL2Signal(byte wave, float frequency, float phase, float *freq0, float *time) //Calculates a signal for input to the adlib synth!
{
float ftp;
if (frequency != *freq0) { //Frequency changed?
*time *= (*freq0 / frequency);
}

ftp = frequency; //Frequency!
ftp *= *time; //Time!
ftp *= PI2; //Apply frequencytime ratio!
ftp += phase; //Add phase!
*freq0 = frequency; //Update new frequency!
return OPL2_Sin(wave, ftp); //Give the generated sample!
}

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

Reply 74 of 112, by nukeykt

User metadata
Rank Member
Rank
Member
superfury wrote:
The 2*PI is actually the period of one sine wave. […]
Show full quote

The 2*PI is actually the period of one sine wave.

calcOPL2Signal(waveform,frequency,phase,freq0,time) gives the LogSin result that's requested.
The LogSin table is looked up by normal sin() convertion (by executing modulo 2*PI and using that phase to determine the index into the OPL LogSin table. This is looked up and the LogSin entry is returned, with the sign bit set when it's the second half of the wave(modulo 2PI>=PI, reverse lookup when modulo PI>=0.5*PI). The rest is simply a factor conversion from modulo 0-0.5PI to 0-255 for the index into the table, which is reversed as described the previous sentence by negating it.).
The phase (which repeats every 2*PI(360 degrees of time)) looked up is the following calculation given to the LogSin table OPL2SinWave(const float r), although there's a little step between it (OPL2_Sin(byte signal, float frequencytime)) which converts the full LogSin wave, silencing and reversing it's output for the other 3 waves (absolute, absolute silenced, sawtooth(=half absolute silenced)):

OPTINLINE word calcOPL2Signal(byte wave, float frequency, float phase, float *freq0, float *time) //Calculates a signal for input to the adlib synth!
{
float ftp;
if (frequency != *freq0) { //Frequency changed?
*time *= (*freq0 / frequency);
}

ftp = frequency; //Frequency!
ftp *= *time; //Time!
ftp *= PI2; //Apply frequencytime ratio!
ftp += phase; //Add phase!
*freq0 = frequency; //Update new frequency!
return OPL2_Sin(wave, ftp); //Give the generated sample!
}

In real chip period of wave is 1024 samples. Output of each operator is in range +-4096. Output of operator directly goes as modulator to next operator, so phase modulation is in +-4096 too. And operator calculated in such way: Exponent(Envelope + LogSin(OpPhase + Modulator)). Since LogSin assumes that period is 1024(2*PI), i think modulator is 4096*2*PI/1024 = 8*PI or 4 periods.

Reply 75 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've changed the OPL2 modulation according to your hint to use 8PI(4 periods) instead of 2PI(1 period):
https://bitbucket.org/superfury/x86emu/src/f0 … lib.c?at=master

It still doesn't quite sound like it's supposed to (testing using Ultima VI opening music+Introduction and Megarace recordings from Dosbox). Mainly the volume sounds a bit messed up(some things too much on the foreground?).

Of course you can hear the difference if you'll simply pull the repository and compile it yourself(using MinGW, Linux or Visual C++(The project is made with Visual Studio Community 2015)).

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

Reply 76 of 112, by nukeykt

User metadata
Rank Member
Rank
Member

I think you have to use value&0x3f here:
https://bitbucket.org/superfury/x86emu/src/60 … ult#adlib.c-316

EDIT:
You also have to multiply KSL value with 8 here:
https://bitbucket.org/superfury/x86emu/src/60 … ult#adlib.c-604

Reply 77 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

Just applied those fixes you've suggested:
https://bitbucket.org/superfury/x86emu/src/40 … lib.c?at=master

It does sound better now, less noise in there. Parts of Megarace's Maeva song (The atlantis level afaik?) sounds like bubbles bubbling up? Is this correct?
Also parts of some Megarace songs (Newfac, especially hearable with Maeva at https://youtu.be/4f-uyybtG_4?t=26 ) seem a little different on the modulating side. The modulation goes up and down. But my emulation currently restarts the volume envelope starting volume at Silence(0x1FF) each time it's triggered on (goes from off-state to on-start in it's Key On). So it keeps restarting at 0x1FF -> ... -> 0 -> ... -> sustain -> ... -> 0x1FF over the course the bit is toggled off - on(until sustain or continue after sustain depending on the setting of rhythm envelope vs melodic envelope with sustain) - off. Is this correct? Or does retriggering notes start at the last volume envelope(previous key on leftover) and attack until 0 with normal slope? Does the volume envelope need to be reset after each note off-on transition?

In my emulation the effect restarts each time the new notes starts, instead of flowing into the next note?

I just adjusted the adlib emulation to only reset the volume envelope to maximum attenuation (0x1FF) when the volume envelope isn't running (not in Attack, Decay, Sustain or Release state) when triggering the note(Volenvstatus==0, thus not a running envelope). This seems to largely fix the effect in Maeva. Although it seems to go down too fast(release rate)? Or is this just me?

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

Reply 78 of 112, by superfury

User metadata
Rank l33t++
Rank
l33t++

The music generated by the OPL2 now sounds kind of dampened. Like somebody put a piece of cloth over the adlib speakers. Anyone can see what's causing this to happen? It sounds fine otherwise.

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

Some parts sound like bubbles bubbling up in Maeva, but I don't hear this in the youtube recordings? Is this a bug in my emulator or the youtube recordings (It IS a underwater/Atlantis level after all)?

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

Reply 79 of 112, by nukeykt

User metadata
Rank Member
Rank
Member

Today i tested your code and made some fixes. Now it sounds better. But there are still some bugs.
Here's modified adlib.c:

Filename
adlib.c
File size
51.8 KiB
Downloads
70 downloads
File license
Fair use/fair dealing exception

Doom 2 MAP01 played on fixed adlib.c:
https://www.dropbox.com/s/opsgi9y5mvo6ecn/doo … dlib_c.mp3?dl=0

Doom 2 MAP01 played on real OPL2:
https://www.dropbox.com/s/21ilwm70dp5b5ay/doo … ym3812.wav?dl=0