VOGONS


Reply 20 of 55, by superfury

User metadata
Rank l33t++
Rank
l33t++

This is my current rendering loop converting the PIT samples into a 44.1kHz sample:

			//render_ticks contains the output samples to process! Calculate the duty cycle by low pass filter and use it to generate a sample!
for (dutycyclei = render_ticks;dutycyclei;)
{
if (!readfifobuffer(PITchannels[2].rawsignal, &currentsample)) break; //Failed to read the sample? Stop counting!
speaker_currentsample = currentsample?SHRT_MAX:SHRT_MIN; //Convert the current result to the 16-bit data, signed instead of unsigned!
#ifdef SPEAKER_LOGRAW
writeWAVMonoSample(speakerlograw,(short)speaker_currentsample); //Log the mono sample to the WAV file, converted as needed!
#endif
#ifdef SPEAKER_LOWPASS
//We're applying the low pass filter for the speaker!
applySoundLowpassFilter(SPEAKER_LOWPASS, TIME_RATE, &speaker_currentsample, &speaker_last_result, &speaker_last_sample, &speaker_first_sample);
#endif
#ifdef SPEAKER_LOGDUTY
writeWAVMonoSample(speakerlogduty,(short)speaker_currentsample); //Log the mono sample to the WAV file, converted as needed!
#endif
}

The output once again sounds exactly the same. The low pass filter is set to (1000000.0f/60.0f) Hz, so about 16666 2/3 Hz. It doesn't change the actual output much (although being accurate output according to your latest information, provided the PIT is generating correct samples at 1.19MHz(I think it doesn't, as both the raw and duty recordings give invalid data with the 'jamming'/noise signal through it, like about 50-100Hz at the 8088 MPH credits)?).

This is my current emulation code: https://bitbucket.org/superfury/x86emu/src/d2 … pit.c?at=master

It seems to still generate the same output, even though it's only filtering the 1.19MHz stream using a 16666 2/3kHz low pass filter and downsampling it to 44.1kHz by skipping 1.19MHz samples. This process seems to work, because output is the same with that and when playing either recorded .wav file using Windows 10's audio player.

So I think I can safely conclude that either the CPU isn't running at correct speeds (causing the strange effects) and/or the PIT 1.19MHz stream is at fault. Anyone can see what's going wrong there(top half of tickPIT function or simply listen to the recordings)?

These are the current recordings: http://www.filedropper.com/speakerlog201603021934
Speakerduty.wav contains the PIT 2 samples which are low pass filtered at 16666 2/3Hz.
Speakerraw.wav contains the PIT 2 samples simply converted to SHRT_MIN and SHRT_MAX(-32768 and 32767), so it's resultant square wave.

Warning with those recordings: If the PC speaker is turned off by software(e.g. outputting 0s) it will create the minimum voltage (-5V) as Direct Current (DC) which will blow up/burn through non-AC coupled speakers with ~4 seconds of silence, except when the player/OS removes the 0Hz frequency.

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

Reply 21 of 55, by Jepael

User metadata
Rank Oldbie
Rank
Oldbie
superfury wrote:

Although the PC speaker is driven by 0V and 5V signals, so your silent signal of 50% duty cycle (over a period of 4 seconds) will produce 2.5V DC instead of 0V on a real PC speaker, thus it might burn the coil of the non-AC coupled PC speaker?

No, it won't burn the PC speaker, and yes, PWM silence is square wave at 50% duty at some high-enough frequency like 16kHz so there will be 2.5V DC offset over the PC speaker, but also 440 Hz or 20 Hz tone is a square wave at 50% duty so there will be 2.5V DC offset over the PC speaker, so there is no difference. And it won't burn. And unless you have very idiotic devices between your software and loudspeakers, you are not able to provide DC offset to your speakers, stop worrying about it for now 😀

In fact, you talked so much before I had a chance, but I was going to compare the original algorithm to a speaker, and the speaker does react to every 1.19MHz bit, because the speaker cannot hold its position still for 72 bits while calculating the average of next 72 bits.

So, otherwise the PIT bitstream to PCM conversion is now correct - 1.19MHz 1-bit audio is lowpass filtered to 16kHz so it ends up being 1.19MHz 16-bit or something, and then you take (approximately) 1 sample every (approximately) 27 samples to downsample to 44.1 kHz 16-bit.

Only thing I suggest thinking at some point is that the 16kHz filter you use is a first order filter, it simulates a simple RC filter. If you want to downsample to 44.1 kHz, your filter should block all frequency content above 22.05 kHz (or to leave some margin for the transition band, everything above 20kHz really). Since you use a first order filter, it will have 3dB attenuation point at 16kHz, and slope of 6dB per octave, so at 32kHz, it will attenuate only 6dB. So really at 20kHz, it does not block much over 3dB, and you still have some aliasing error. So I suggest using a better filter and/or lowering the cutoff frequency.

Reply 22 of 55, by superfury

User metadata
Rank l33t++
Rank
l33t++

Any idea on the filter to use instead(same with the high pass filter of 1Hz used in the sound module for protection)? It is the only simple and clear filter I could find in C code. Only highly complicated formulas otherwise(never did any high level algebra after middle school to be able to fully understand those complicated formulas). Do know some basic math stuff concerning formulas, got me this far:) Just a little bit past middle/high school on that point(don't know what the correct term is in English, vmbo-t(with a little bit of havo, quick dictionary search gets me "school of higher general secondary education") as it's called here in nl. Although I did do a proper education after it (financial sector) although finally ending up in the ICT:) Though the emulator is a little hobby of mine the past, let's see, 6-7 years to get from nothing to the current version (biggest part just finding and fixing bugs and making it faster to be workable, only summer last year finally gotten it properly functioning(the CPU and VGA emulation optimization mainly, although only using the psp(or jpcsp) for development until starting the windows port(and eventually breaking the psp version for some reason, could just be too slow a CPU right now though, until better optimized for it(difficult without proper tools for it besides jpcsp and homebrew devkit))).

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

Reply 23 of 55, by gdjacobs

User metadata
Rank l33t++
Rank
l33t++

Well, guys, I don't know too much about programming the PC speaker, but I'm familiar with PWM wave synthesis, so I'll blast in here.

The simplest operation of a square wave output will be a direct square wave tone. I won't dwell on this because it's uninteresting and not difficult to model for the emulator. However, when the speaker is driven with a square wave at a frequency well beyond it's physical response capability, the square can be used to position the cone according to the duty cycle of the wave. By varying the duty cycle at a speed that is within the response capability of the speaker, you can create an approximation of a wave output.

This technique is used all the time in class D and class "T" audio amplifiers as well as AC power inverters of all sizes by pretty much everyone but Siemens.

All hail the Great Capacitor Brand Finder

Reply 24 of 55, by Jepael

User metadata
Rank Oldbie
Rank
Oldbie
gdjacobs wrote:

Well, guys, I don't know too much about programming the PC speaker, but I'm familiar with PWM wave synthesis, so I'll blast in here.

The simplest operation of a square wave output will be a direct square wave tone. I won't dwell on this because it's uninteresting and not difficult to model for the emulator. However, when the speaker is driven with a square wave at a frequency well beyond it's physical response capability, the square can be used to position the cone according to the duty cycle of the wave. By varying the duty cycle at a speed that is within the response capability of the speaker, you can create an approximation of a wave output.

This technique is used all the time in class D and class "T" audio amplifiers as well as AC power inverters of all sizes by pretty much everyone but Siemens.

Correct, except nowadays PWM is old technology and it starts to be more PDM (pulse density modulation) because modern AD and DA converters are sigma-delta converters anyway so some audio recorders and players can directly work with DSD bitstreams that is also used on SACD discs. With identical bit clocks, PWM sort of forces you to "pack" all the one bits to the start of the pulse period and zeroes to the end of the period, so you have the PWM carrier wave running at sampling rate. With PDM using same bit clock, you get the ones and zeroes distributed freely in a way to represent the underlying analog audio signal more accurately, so in essence, you can position the speaker more accurately.

Reply 25 of 55, by gdjacobs

User metadata
Rank l33t++
Rank
l33t++

Sigma delta is a superior method of conversion, but I really didn't want to push the dialog that far at this point. I didn't discuss the pluses and minuses of high clock vs higher clock, triangle vs saw wave clock signal, etc which would be the next step in the conversation. Besides, how much fidelity would you expect out of a crappy PC speaker.

All hail the Great Capacitor Brand Finder

Reply 26 of 55, by superfury

User metadata
Rank l33t++
Rank
l33t++

My current understanding of PC Speaker sampled output is that you generate PWM to create samples. The PWM itself can indeed be done at any speed, but with the PC speaker it only makes sense when executing pulses within 60 us? Since it takes 60us to move fully from off to on state or backwards? Using this period(thus the required timing required to send pulses within) to effectively set the duty for that period by setting an on and off state in that fraction of time results in the speaker moving to the correct position.

So if you send 1s for 60 us, 1s for 60 us, 0s for 60 us and finally 0s for 60us, you effectively get samples 1,1,0,0 as a result. But to get the correct 50% duty, you need to send 1s for 30us, 0s for 30us,1s for 30us, 0s for 30us,1s for 30us, 0s for 30us,1s for 30us, 0s for 30us. This will result in the correct 0.5, 0.5, 0.5, 0.5 (50% duty) samples.

That's as far as I understand all the information combined.

Last edited by superfury on 2016-03-03, 06:59. 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 27 of 55, by gdjacobs

User metadata
Rank l33t++
Rank
l33t++

Well, I'm not sure how fast software can drive the PC speaker. There's purpose to driving it with a high frequency input (10s or 100s of khz) as I noted. Generally a higher frequency of PWM, the better your distortion characteristics will be. The simplest way to convert back would be to average over the target sample period.

It's actually very doubtful that a full range speaker will be able to operate at 33khz, but that's not really the point. You want to capture higher rate operation of the speaker because that's what allows more accurate positioning of the cone and higher fidelity output in games like Mean Streets and Links. The mechanical and electrical averaging performed by the coil and spring in the cone can be performed in software when you average the PC speaker output as you convert to PCM.

All hail the Great Capacitor Brand Finder

Reply 28 of 55, by superfury

User metadata
Rank l33t++
Rank
l33t++

It currently creates the 1.19MHz samples (just PIT rendering 1s and 0s based on it's input by the emulated CPU). Then it first low pass filters it with 16666 2/3kHz to filter out the higher signals. Finally it downsamples it to 44.1kHz by skipping samples (~26 PIT samples skipped for every 44.1kHz sample to be retrieved).

I still think that although the conversion is correct, there's still a problem with the signal the PIT itself generates (conversion from timer data, gate status and other factors influencing PIT operation(like the modes themselves) to the 1.19MHz PIT data stream).

This is the current PIT code itself (first half of the rendering, the second half is the conversion mentioned above), taken from the current commit:
https://bitbucket.org/superfury/x86emu/src/d2 … pit.c?at=master

	time_ticktiming += timepassed; //Add the amount of time passed to the PIT timing!

//Render 1.19MHz samples for the time that has passed!
length = (uint_32)SAFEDIV(time_ticktiming, time_tick); //How many ticks to tick?
time_ticktiming -= (length*time_tick); //Rest the amount of ticks!

if (length) //Anything to tick at all?
{
for (channel=0;channel<3;channel++)
{
byte mode,outputmask;
mode = PITchannels[channel].mode; //Current mode!
outputmask = (channel==2)?((PCSpeakerPort&2)>>1):1; //Mask output on/off for this timer!

switch (mode) //What mode are we rendering?
{
case 0: //Interrupt on Terminal Count? Is One-Shot without Gate Input?
case 1: //One-shot mode?
for (tickcounter = length;tickcounter;--tickcounter) //Tick all needed!
{
//Length counts the amount of ticks to render!
switch (PITchannels[channel].status) //What status?
{
case 0: //Output goes low/high?
PITchannels[channel].channel_status = mode; //We're high when mode 1, else low with mode 0!
PITchannels[channel].status = 1; //Skip to 1: we're ready to run already!
break;
case 1: //Wait for next rising edge of gate input?
if (!mode) //No wait on mode 0?
{
PITchannels[channel].status = 2;
goto mode0_2;
}
else if (PITchannels[channel].gatewenthigh) //Mode 1 waits for gate to become high!
{
PITchannels[channel].gatewenthigh = 0; //Not went high anymore!
PITchannels[channel].status = 2;
goto mode0_2;
}
break;
case 2: //Output goes low and we start counting to rise! After timeout we become 4(inactive) with mode 1!
mode0_2:
if (PITchannels[channel].reload)
{
PITchannels[channel].reload = 0; //Not reloading anymore!
PITchannels[channel].channel_status = 0; //Lower output!
reloadticker(channel); //Reload the counter!
}

oldvalue = PITchannels[channel].ticker; //Save old ticker for checking for overflow!

if (mode) --PITchannels[channel].ticker; //Mode 1 always ticks?
else if ((PCSpeakerPort&1) || (channel<2)) --PITchannels[channel].ticker; //Mode 0 ticks when gate is high!

if ((!PITchannels[channel].ticker) && oldvalue) //Timeout when ticking? We're done!
{
PITchannels[channel].channel_status = 1; //We're high again!
}
break;
case 4: //Inactive?
Show last 161 lines
						break;
default: //Unsupported! Ignore any input!
break;
}
writefifobuffer(PITchannels[channel].rawsignal, PITchannels[channel].channel_status&outputmask); //Add the data to the raw signal!
}
break;
case 2: //Also Rate Generator mode?
case 6: //Rate Generator mode?
for (tickcounter = length;tickcounter;--tickcounter) //Tick all needed!
{
//Length counts the amount of ticks to render!
switch (PITchannels[channel].status) //What status?
{
case 0: //Output going high! See below! Wait for reload register to be written!
PITchannels[channel].channel_status = 1; //We're high!
break;
case 1: //We're starting the count?
if (PITchannels[channel].reload)
{
reload2:
PITchannels[channel].reload = 0; //Not reloading!
reloadticker(channel); //Reload the counter!
PITchannels[channel].channel_status = 1; //We're high!
PITchannels[channel].status = 2; //Start counting!
}
break;
case 2: //We start counting to rise!!
if (PITchannels[channel].gatewenthigh) //Gate went high?
{
PITchannels[channel].gatewenthigh = 0; //Not anymore!
goto reload2; //Reload and execute!
}
if (((PCSpeakerPort & 1) && (channel==2)) || (channel<2)) //We're high or undefined?
{
--PITchannels[channel].ticker; //Decrement?
switch (PITchannels[channel].ticker) //Two to one? Go low!
{
case 1:
PITchannels[channel].channel_status = 0; //We're going low during this phase!
break;
case 0:
PITchannels[channel].channel_status = 1; //We're going high again during this phase!
reloadticker(channel); //Reload the counter!
break;
default: //No action taken!
break;
}
}
else //We're low? Output=High and wait for reload!
{
PITchannels[channel].channel_status = 1; //We're going high again during this phase!
}
break;
default: //Unsupported! Ignore any input!
break;
}
writefifobuffer(PITchannels[channel].rawsignal, PITchannels[channel].channel_status&outputmask); //Add the data to the raw signal!
}
break;
//mode 2==6 and mode 3==7.
case 7: //Also Square Wave mode?
case 3: //Square Wave mode?
for (tickcounter = length;tickcounter;--tickcounter) //Tick all needed!
{
//Length counts the amount of ticks to render!
switch (PITchannels[channel].status) //What status?
{
case 0: //Output going high! See below! Wait for reload register to be written!
PITchannels[channel].channel_status = 1; //We're high!
if (PITchannels[channel].reload)
{
PITchannels[channel].reload = 0; //Not reloading!
reloadticker(channel); //Reload the counter!
PITchannels[channel].status = 1; //Next status: we're loaded and ready to run!
}
break;
case 1: //We start counting to rise!!
if (PITchannels[channel].gatewenthigh)
{
PITchannels[channel].gatewenthigh = 0; //Not anymore!
PITchannels[channel].reload = 0; //Reloaded!
reloadticker(channel); //Gate going high reloads the ticker immediately!
}
if ((PCSpeakerPort&1) || (channel<2)) //To tick at all?
{
PITchannels[channel].ticker -= 2; //Decrement by 2 instead?
switch (PITchannels[channel].ticker)
{
case 0: //Even counts decreased to 0!
case 0xFFFF: //Odd counts decreased to -1/0xFFFF.
PITchannels[channel].channel_status ^= 1; //We're toggling during this phase!
PITchannels[channel].reload = 0; //Reloaded!
reloadticker(channel); //Reload the next value to tick!
break;
default: //No action taken!
break;
}
}
break;
default: //Unsupported! Ignore any input!
break;
}
writefifobuffer(PITchannels[channel].rawsignal, PITchannels[channel].channel_status&outputmask); //Add the data to the raw signal!
}
break;
case 4: //Software Triggered Strobe?
case 5: //Hardware Triggered Strobe?
for (tickcounter = length;tickcounter;--tickcounter) //Tick all needed!
{
switch (PITchannels[channel].status) //What status?
{
case 0: //Output going high! See below! Wait for reload register to be written!
PITchannels[channel].channel_status = 1; //We're high!
break;
case 1: //We're starting the count or waiting for rising gate(mode 5)?
if (PITchannels[channel].reload)
{
pit45_reload: //Reload PIT modes 4&5!
if ((mode == 4) || ((PITchannels[channel].gatewenthigh) && (mode == 5))) //Reload when allowed!
{
PITchannels[channel].gatewenthigh = 0; //Reset gate high flag!
PITchannels[channel].reload = 0; //Not reloading!
reloadticker(channel); //Reload the counter!
PITchannels[channel].status = 2; //Start counting!
}
}
break;
case 2: //We start counting to rise!!
case 3: //We're counting, but ignored overflow?
if (PITchannels[channel].reload || (((mode==5) && PITchannels[channel].gatewenthigh))) //We're reloaded?
{
goto pit45_reload; //Reload when allowed!
}
if (((PCSpeakerPort & 1) && (channel == 2)) || (channel<2)) //We're high or undefined?
{
--PITchannels[channel].ticker; //Decrement?
if (!PITchannels[channel].ticker && (PITchannels[channel].status!=3)) //One to zero? Go low when not overflown already!
{
PITchannels[channel].channel_status = 0; //We're going low during this phase!
PITchannels[channel].status = 3; //We're ignoring any further overflows from now on!
}
else
{
PITchannels[channel].channel_status = 1; //We're going high again any other phase!
}
}
else //We're low? Output=High and wait for reload!
{
PITchannels[channel].channel_status = 1; //We're going high again during this phase!
}
break;
default: //Unsupported mode! Ignore any input!
break;
}
writefifobuffer(PITchannels[channel].rawsignal, PITchannels[channel].channel_status&outputmask); //Add the data to the raw signal!
}
break;
}
}
}

Also the 60 us intervals I used in older commits&builds to average are based on this(currently removed in emulation itself) (from osdev):

How It Works
The PC Speaker takes approximately 60 millionths of a second to change positions. This means that if the position of the speaker is changed from "in" to "out" and then changed back in less than 60 milliseconds, the speaker did not have enough time to fully reach the "out" position. By precisely adjusting the amount of time that the speaker is left "out", the speaker's position can be set to anywhere between "in" and "out", allowing the speaker to form more complex sounds.

Although it says milliseconds it's actually microseconds? 1 millisecond = 1/1000th second, 1 microsecond = 1 millionths second(1/1000000 second)

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

Reply 29 of 55, by Scali

User metadata
Rank l33t
Rank
l33t
superfury wrote:

but with the PC speaker it only makes sense when executing pulses within 60 us? Since it takes 60us to move fully from off to on state or backwards?

No it doesn't make sense, forget that 60 us metric as I already said!
The point is NOT to move the cone fully from one extreme to the other.
You should think of it like this:
When you apply 0v to the speaker, it is at one extreme of its range.
When you apply 5v to the speaker, it moves to the other extreme of its range.
The PIT controls this output. The PIT's output is low (0) when a new count is loaded, and goes high (1) when the count reaches 0 (or was it the other way around? Doesn't really matter).
So this is how we can perform 1-bit PWM on the speaker.

In order to create a sound, the cone needs to move according to a certain waveform. This will make the cone move the air according to that waveform, which results in the sound waves we can hear.
Let's take a sawtooth as example: /\/\.
Let's simplify it by taking 5 different cone positions to approximate the wave:
-1, -0.5, 0, 0.5, 1

Now, for -1, we need to have it at one extreme. We can do this by setting a duty cycle of 0 as PWM data. As a result the output always remains high. So the speaker is always at one extreme.
For 1, we do the opposite, we send a duty cycle that is as large as our update interval, so that the output always remains low. The speaker is always at the other extreme.
For 0, we need to have it halfway between these extremes. We can approximate this by setting a duty cycle that is half or our update interval.
You drive the speaker low for half the time, and then you drive the speaker high the other half of the time. This means that on average, the speaker cone is in the halfway position. The speaker cone cannot move infinitely fast (it needs to abide by the laws of physics, the whole reason why speakers tend to have a dropoff towards higher frequencies, and why smaller/lighter cones can produce higher frequencies better).
So as a result, it will start moving towards one extreme at the start of the interval, and when the timer reaches 0 halfway, its direction will be reversed, moving back. As long as the interval is short enough, the speaker will not actually move very much, and will approximate the desired position for the given sample.

The same story goes for the -0.5 and 0.5 positions, except that you use 25% and 75% duty cycles respectively to 'balance' the speaker cone at these positions.

What you need to take away from this is that for the theory of PWM, neither the update interval nor the exact speaker characteristics are all that relevant for the concept to work.
The shorter the interval, the more accurate the positioning of the cone will be.
Likewise, the faster the cone can move, the shorter the interval needs to be for a given level of accuracy.

But this is not an exact science. Different PCs use different speaker cones, with different characteristics. In fact, a lot of PCs do not even use speaker cones at all. They use piezo buzzers instead. These piezo buzzers can move much faster than a speaker cone can, but still PWM works very well on them in practice.

So what you basically need to do, is to determine the desired (not actual, forget about the speaker physics) cone position from the 1-bit stream coming from the PIT. You can do this by downsampling the stream to any frequency. That doesn't matter. The cone position at a given time can always be approximated by taking an average for a given window of samples.
That is basically also what the speaker itself does. It doesn't have a concept of PWM frequency, phase, starting point or anything. It just tries to follow the stream of pulses, no matter when it starts, or how quickly the pulses are sent.

http://scalibq.wordpress.com/just-keeping-it- … ro-programming/

Reply 30 of 55, by gdjacobs

User metadata
Rank l33t++
Rank
l33t++

Take the PWM signal, determine the average over your desired sample length for each sample (I'd recommend something like 10 or 20 khz sampling rate), quantize it if necessary, then filter it as desired.

pcm_pwm_signals_480.gif

You can see here how a simple sinusoidal wave can be represented by the average duty cycle of a PWM signal for each sample period. This is the simplest representation of direct digital synthesis using a switch mode circuit. This is what your PC speaker does, only the drive on the speaker is much quicker.

All hail the Great Capacitor Brand Finder

Reply 31 of 55, by Scali

User metadata
Rank l33t
Rank
l33t

The 'cheat' method like DOSBox uses doesn't sound exactly like PWM would. It implicitly filters out the carrier frequency that automatically results from sending a new pulse to the speaker everytime you update the PIT counter (the PIT output to the speaker always starts low and ends high at each interval, so if you send at 16 KHz, you always get an audible 16 KHz 'whistle').

http://scalibq.wordpress.com/just-keeping-it- … ro-programming/

Reply 32 of 55, by gdjacobs

User metadata
Rank l33t++
Rank
l33t++
Scali wrote:

the PIT output to the speaker always starts low and ends high at each interval, so if you send at 16 KHz, you always get an audible 16 KHz 'whistle'.

Thanks, Intel!

All hail the Great Capacitor Brand Finder

Reply 33 of 55, by Scali

User metadata
Rank l33t
Rank
l33t
gdjacobs wrote:

Thanks, Intel!

I think we have IBM to thank, not sure if Intel ever meant for their PIT to be abused as a PWM-based sound source 😀
IBM could have chosen to add a proper audio chip to the PC, but I guess they wanted to save a few dimes.
But perhaps even IBM didn't consider using PWM. The people at Access Software actually patented their implementation (RealSound(TM)), although they weren't the first to do PWM on the PC speaker.

http://scalibq.wordpress.com/just-keeping-it- … ro-programming/

Reply 34 of 55, by superfury

User metadata
Rank l33t++
Rank
l33t++

So I should only low pass the PIT 1/0(binary) stream with 16 2/3 kHz, then downsample to 44.1kHz(which is done by skipping those ~25 calculated samples for getting each 44.1kHz sample)? The low pass filter will actually do the hard work? Or is actual 20kHz averaging before down/upsampling still needed as gdjacobs post with those images? So first average, then low pass filter@16 2/3kHz, finally downsample to 44.1kHz? Or first low pass filter, then average, finally downsample?

But I'm still using a RC low pass filter. Will this give correct output? Or would I need a different filter here? If so, do you have some source code somewhere to use (as I can't do much with those compilicated formulas itself that are found on wikipedia etc(except the RC lowpass(and high pass) filter, which is already implemented in the emulation currently))?

I imagine the PWM song is using mode 0 or 1 (whose only difference is when the timer starts counting and the output from the moment the mode is set until the timer starts(reload value is loaded), which is 0 in mode 0 and 1 in mode 1)? Otherwise both are about the same mode of operation)? Mode 0 should be the easiest to use? So go low when timer set, go high until either reloaded or the mode is set. Also mode 1 waits on raising gate before starting the timer, while mode 0 starts immediately. Although mode 1 also sets output high instead of low(mode 0) until the reload counter is set after setting the mode.

Can RealSound(TM) actually be patented? PWM is a generally known technique, so it can't actually be patented? Else wikipedia's article along with half the industry using PWM to change motors would be illegal if used for sound? They can patent their games, but the technique of producing sound (PWM sample generation) can't be patented?

Also, how large must this sample period be? Always 72 samples? Or some floating point number based on a frequency(like 1 second divided by 60 us)? If this needs to be used at all? According to gdjacobs I need to use a certain sampling period. But that would break software actually using different sizes than 72 samples for output (If the others are correct about PWM to PCM conversions in a PC speaker, with their information on it not using 60us or 72 samples at all, except when it uses a different time? If so, what time(period) should be used?)

Looking at the pictures by gdjacobs tells me that I should simply:
1. Take the average of the current sample (0-1) and the current PIT sample (0/1) to get the current sinus-like signal?
2. Low pass filter it with 20kHz to get the smooth signal.
3. Downsample the resulting signal to 44.1kHz to get the output for the renderer.

Is this correct? So every sample (the sinus wave in the bottom picture) is simply the average of the current sample(0/1) and the previous result(0.0-1.0, the new previous result is the output of the current sample average during this step), resulting in a Sinus-wave like average moving up and down? Finally the low pass filter will smooth it out(20kHz filter at a rate of 1.19MHz), and the downsampling will make sure it's quantized to 44.1kHz for playback?

So from the point PIT2 setting the mode (based on osdev information on the PIT):
Mode 0: Output goes low, After reload is set starts the countdown, When expires Output goes high and stays high until next reload or mode set (which will affect Output based on the mode set).
Mode 1: Output goes high, After reload is set it starts waiting for the gate to go raise (from low to high), After that happens Output goes low and starts the PIT starts to count down. Reloading the counter or setting the mode will affect the Output based on the mode set.

Is this correct?

The point is NOT to move the cone fully from one extreme to the other.

I never said I move the cone from 0 to 1 in one go (this would result in a simple PWM direct output, defeating the whole purpose of the emulation of it). It moves in that period of 60us(or 72 PIT samples(where either 60us or 72 samples might be rounded down/up from the actual value in the PC speaker. Maybe a whole different number between, or different from, both intervals.) or whatever the correct time is to move it 100% from 0 to 1 or 1 to 0) fully from 0 to 1 or backwards. So it takes 60us/72 PIT samples@1.19MHz to move from 0 to 1 or back to 0. So this is the time that would need to be averaged according to gdjacobs' post if that's true(to generate the real 'PC Speaker PCM' samples as our ears should receive it).

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

Reply 35 of 55, by Scali

User metadata
Rank l33t
Rank
l33t
superfury wrote:

So I should only low pass the PIT 1/0(binary) stream with 16 2/3 kHz, then downsample to 44.1kHz(which is done by skipping those ~25 calculated samples for getting each 44.1kHz sample)? The low pass filter will actually do the hard work? Or is actual 20kHz averaging before down/upsampling still needed as gdjacobs post with those images? So first average, then low pass filter@16 2/3kHz, finally downsample to 44.1kHz? Or first low pass filter, then average, finally downsample?

Yea, basically you 'blur' the samples together with the filter. This will remove the high-frequency components, and convert the 0..1 range to a larger range, depending on your filter's resolution.
So a simple linear filter should already give somewhat acceptable results.

I haven't actually written such a routine myself, but I might give it a try. I only wrote the opposite routine so far: one that converts a PCM sample to PWM, and play it on real hardware.

superfury wrote:

I imagine the PWM song is using mode 0 or 1 (whose only difference is when the timer starts counting and the output from the moment the mode is set until the timer starts(reload value is loaded), which is 0 in mode 0 and 1 in mode 1)? Otherwise both are about the same mode of operation)? Mode 0 should be the easiest to use? So go low when timer set, go high until either reloaded or the mode is set. Also mode 1 waits on raising gate before starting the timer, while mode 0 starts immediately. Although mode 1 also sets output high instead of low(mode 0) until the reload counter is set after setting the mode.

Yes, if your PIT emulation correctly outputs a 0 or 1 in all cases, it doesn't matter which mode the PIT uses. The same routine will work for any kind of PWM, and also for 'conventional' PC speaker output (square wave mode).

superfury wrote:

Can RealSound(TM) actually be patented? PWM is a generally known technique, so it can't actually be patented? Else wikipedia's article along with half the industry using PWM to change motors would be illegal if used for sound? They can patent their games, but the technique of producing sound (PWM sample generation) can't be patented?

Yes, basically the patent seems to be "Using PWM on a machine with a speaker to play digitized sound". So PWM itself isn't patented, but this specific application of PWM is.

superfury wrote:

Also, how large must this sample period be?

That basically depends on what kind of filter you implement. The most simple filter would have a window of size 1.19 MHz/44.1 KHz (assuming 44.1 KHz output).
You could also have a more complicated filter, where you have some overlap of the windows from one output sample to the next. You could also apply different weights to each input, eg a Gaussian-like weighting, where the outer inputs have less effect on the current output sample than the ones near the center of the window.

superfury wrote:

But that would break software actually using different sizes than 72 samples for output

No it won't. Your filter simply gives you the average signal sent to the speaker over time, which is an approximation of where the speaker cone would be.
There is no difference between the average of 4 PWM values of X or 2 PWM values of 2*X.
Where N is your interval.
Eg, let's take N = 8, and X=2.
If we output a new PWM count every 8 samples, with 4 PWM values of 2, you get 4 times the following sequence:
11000000
Which is 32 samples in total, where 8 samples are 1, the others are 0. The average is 8/32 = 0.25 as your 'DC value'.
Instead, if we output a new PWM count ever 16 samples, and write 2 PWM values of 2*X = 4, we get 2 times the following sequence:
1111000000000000
We again have 32 samples in total, where 8 samples are 1, the others are 0. So again, the average is 8/32 = 0.25 as the 'DC value'.

What if we don't have an integer multiple? Let's say there will be 2.38 samples in our window of 32 samples.
Well, then the PWM routine would have to output (4/2.38)*X per sample. So we'd get 1.68 bits of 1 in each sample, and we get that 2.38 times.
Which on average would still be 8 bits in a window of 32 samples.

So it works for every possible carrier frequency of PWM data.
And as you can figure, it also doesn't really matter where exactly the 1s or 0s are within the window. You'd get slight phase differences, but that doesn't really affect the perceived sound.
If you really wanted, you could re-sync your resampling routine based on PIT commands or speaker on/off.
That is, you can detect in code when your speaker should be 'in rest', and the first time the speaker moves, you can 'snap' your resampling window to that, so at least your output is consistently in the same phase at every run.

superfury wrote:

I never said I move the cone from 0 to 1 in one go (this would result in a simple PWM direct output, defeating the whole purpose of the emulation of it).

But why do you hold to that metric of 60us at all then? Firstly, the metric is meaningless (as you see above, we do not need to consider any kind of speaker properties, we just want to get an average 'DC' value over time), secondly, the metric is wrong, because as said, there is too much variation in speakers. Even if the 60us value is true for one speaker, it certainly doesn't hold for all speakers out there.
Whoever wrote that, has no idea what they were talking about, I'm afraid.

http://scalibq.wordpress.com/just-keeping-it- … ro-programming/

Reply 36 of 55, by superfury

User metadata
Rank l33t++
Rank
l33t++

This is the current processing, generating samples from the PIT until the current time is reached(timepassed is the time the CPU, PIT and all timed hardware has executed the current instruction).
speaker_tick is the amount of ns for each 44.1kHz sample to be generated. Essentially 1000000000/SPEAKER_RATE, where SPEAKER_RATE is 44100.0f.
ticklength is (1/SPEAKER_RATE)*TIME_RATE, where TIME_RATE is ~1.19MHz in Hertz.

Code:

	//PC speaker output!
speaker_ticktiming += timepassed; //Get the amount of time passed for the PC speaker (current emulated time passed according to set speed)!
if ((speaker_ticktiming >= speaker_tick) && enablespeaker) //Enough time passed to render the physical PC speaker and enabled?
{
length = (uint_32)SAFEDIV(speaker_ticktiming, speaker_tick); //How many ticks to tick?
speaker_ticktiming -= (length*speaker_tick); //Rest the amount of ticks!

if (!FIFOBUFFER_LOCK) //Not locked?
{
lockaudio(); //Lock the audio!
}

//Ticks the speaker when needed!
i = 0; //Init counter!
uint_32 dutycyclei; //Input samples to process!
//Generate the samples from the output signal!
for (;;) //Generate samples!
{
//Average our input ticks!
PITchannels[2].samplesleft += ticklength; //Add our time to the sample time processed!
tempf = floorf(PITchannels[2].samplesleft); //Take the rounded number of samples to process!
PITchannels[2].samplesleft -= tempf; //Take off the samples we've processed!
render_ticks = (uint_32)tempf; //The ticks to render!

//render_ticks contains the output samples to process! Calculate the duty cycle by low pass filter and use it to generate a sample!
for (dutycyclei = render_ticks;dutycyclei;)
{
if (!readfifobuffer(PITchannels[2].rawsignal, &currentsample)) break; //Failed to read the sample? Stop counting!
speaker_currentsample = currentsample?SHRT_MAX:SHRT_MIN; //Convert the current result to the 16-bit data, signed instead of unsigned!
#ifdef SPEAKER_LOGRAW
writeWAVMonoSample(speakerlograw,(short)speaker_currentsample); //Log the mono sample to the WAV file, converted as needed!
#endif
#ifdef SPEAKER_LOWPASS
//We're applying the low pass filter for the speaker!
applySoundLowpassFilter(SPEAKER_LOWPASS, TIME_RATE, &speaker_currentsample, &speaker_last_result, &speaker_last_sample, &speaker_first_sample);
#endif
#ifdef SPEAKER_LOGDUTY
writeWAVMonoSample(speakerlogduty,(short)speaker_currentsample); //Log the mono sample to the WAV file, converted as needed!
#endif
}

//Add the result to our buffer!
writefifobuffer16(PITchannels[2].doublebuffer, (short)speaker_currentsample); //Write the sample to the buffer (mono buffer)!
movefifobuffer16(PITchannels[2].doublebuffer,PITchannels[2].buffer,PITDOUBLE_THRESHOLD); //Move any data to the destination once filled!
if (++i == length) //Fully rendered?
{
if (!FIFOBUFFER_LOCK) //Not locked?
{
unlockaudio(); //Unlock the audio!
}
return; //Next item!
}
}
if (!FIFOBUFFER_LOCK) //Not locked?
{
unlockaudio(); //Unlock the audio!
}
}

Although it doesn't average anything, it just converts to 16-bit depth, low pass filters at 16 2/3Hz and calculates the current 16-bit sample(floating point) in the inner loop, which is processing samples until the current 44.1kHz 16-bit sample is reached in time, which is finally passed to the rendering buffer for playback (with double buffering for speedup). The outer loop simply processes X 44.1kHz samples until current time is reached (usually 1 sample when working at full speed).

Well, that quoted text does contain a strange error (milliseconds instead of microseconds), so it indeed might be wrong.

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

Reply 38 of 55, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've made a new recording using the latest commit (which changes the low pass filter back to 20kHz, applies the filter on the 1.19MHz signal(which is increased to 16-bit depth using SHRT_MIN and SHRT_MAX) and logs both the raw samples (SPEAKER_LOGRAW to speakerraw.wav) and the filtered samples (SPEAKER_LOGDUTY to speakerduty.wav).

These are the recordings (both recorded at 1.19MHz samplerate): http://www.filedropper.com/speakerlog8088mph201603032200

Although it does say it's 0KB, it's actually 43.2MB (each recording is 1.263.460 kB, or 1.2GB in size unpacked).

Although it has more echo in the live playback (probably due to the writeWAVMonoSample taking so much time to write to the file and/or flush it to disk).

Edit: Just tried Links 2 again. No sound output at all. Looking at the raw audio in the .wav dump tells me it's only producing 16-bit samples containing 0x7FFF(value 32767), so the PIT is actually stuck to value 1 instead of running correctly rendering samples? The movie is running without problems.

Edit: Looking at the logs, I notice Links doesn't reload the counter anymore?

Edit: Fixed it by fixing the counter handling. 8088 MPH's volume still moves up and down constantly during normal mode 3(I think) square wave sound generation(until the credits), though.

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

Reply 39 of 55, by Jepael

User metadata
Rank Oldbie
Rank
Oldbie

The raw version looks and feels exactly like my MONOTONE dump renderer would output.

The filtered version looks like it does not have enough filtering.
You can see it during playing 8088 MPH music. For silence, MONOTONE dumps contain PIT value of 18, which is a 66kHz square wave tone. Since the 66kHz tone plays at amplitude 0.42, it is only -7.5 dB (my rough estimation). I can't imagine how audible the aliasing errors would be when downsampling only with this filter.

So you see, the filter you now have, if you set cutoff frequency to 20kHz, it does not mean it has cut everything off at 20kHz, it is only slowly starting to cut at 20kHz, and it will cut only by 20dB at 200 kHz. And in ideal case, you need the filter to have cut everything at 20kHz already before downsampling.

For starters, you could try what happens if you set the cutoff frequency to 2kHz instead of 20kHz. I bet it sounds more like an actual speaker would, instead of sounding far too bright and crispy. I tried 1kHz second order filter in Audacity but it was too muffled.

Also, I am not sure if it is a nice thing to make the PIT waveform to full scale (-32768 .. +32767) before filtering, as some filters (but not simple RC filters) have a bit of overshoot, so the samples could easily clip during converting and processing (unless you use floats and never truncate back to integers).