VOGONS


IBM PC Speaker RC values?

Topic actions

First post, by superfury

User metadata
Rank l33t++
Rank
l33t++

I'm currently using a simple RC filter with a cutoff frequency of the PIT frequency(~1.19MHz) divided by 72(PIT samples for PWM). I want to make this accurate 100%, but in order to do that i need the capicitance of the speaker. I know that it's supposed to be 8 ohm, but without capicitance I cannot calculate the exact cutoff frequency.

So, the cutoff frequency is:
1/(2*PI*RC)=1/(2*PI*8*C) Hz.

So, in order to be 100% compatible, I need the capacitance of the speaker. Anyone knows that? I know that it's apparently a 0.5W speaker? Reenigne? Jepael?

I could find the value of 4.7uF for C(C being 0.0000047), resulting in a low-pass filter of about 4400Hz. Is that correct?

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

Reply 1 of 66, by reenigne

User metadata
Rank Oldbie
Rank
Oldbie
superfury wrote:
I'm currently using a simple RC filter with a cutoff frequency of the PIT frequency(~1.19MHz) divided by 72(PIT samples for PWM) […]
Show full quote

I'm currently using a simple RC filter with a cutoff frequency of the PIT frequency(~1.19MHz) divided by 72(PIT samples for PWM). I want to make this accurate 100%, but in order to do that i need the capicitance of the speaker. I know that it's supposed to be 8 ohm, but without capicitance I cannot calculate the exact cutoff frequency.

So, the cutoff frequency is:
1/(2*PI*RC)=1/(2*PI*8*C) Hz.

So, in order to be 100% compatible, I need the capacitance of the speaker. Anyone knows that? I know that it's apparently a 0.5W speaker? Reenigne? Jepael?

The speaker system as a whole acts as a low-pass filter, but that low-pass isn't an RC filter. The speaker is a coil - it has no significant capacitance. There is a 0.01uF capacitor and a 33 ohm resistor on the motherboard, which act as a low pass filter, but the cutoff of that is 482kHz! Obviously, the speaker isn't physically capable of oscillating that fast and even if it could, you wouldn't be able to hear it. The actual cutoff is determined by the mass of the speaker cone and the maximum acceleration of that mass that can be provided by the coil. But there's no point simulating all that - exactly the same process happens in the speaker of the machine (the host) that's running your emulator.

What the emulator needs to provide is a voltage waveform for the host speaker that as closely as possible approximates the voltage waveform that goes into the target PC speaker. Now, the voltage waveform that goes into the PC speaker is best modelled as a 1.193MHz 1-bit waveform (could even be a 4.77MHz 1-bit waveform but there's almost certainly no audible difference). Audio APIs on modern machines usually prefer a 44.1kHz 16-bit waveform, so you've got to convert from the former to the latter. In doing so, you have to throw away some information because a 44.1kHz waveform can't represent frequencies over 22.05kHz but a 1.193MHz waveform can. You have to filter out the frequencies that can't be represented (22kHz to 597kHz) or they'll alias into the 0-22kHz part of the spectrum which sounds terrible. This (not simulating the physical/electrical properties of the speaker) is where the need for a low-pass filter comes from.

The ideal low-pass filter in an emulator would just be at the Nyquist frequency of the output sample rate (i.e. 22.05kHz) but downsampling is more tractable if your filter has a roll-off - instead of being a sharp cut-off, the attenuation goes gradually from 0 to 100% as the frequency goes from (say) 20kHz to 22kHz. Then you can use a Lanczos filter instead of a Sinc filter, which requires a much smaller kernel. This is actually a big reason why we use a sample rate (44.1kHz) that can represent frequencies from 20kHz to 22kHz when we can't actually hear those frequencies.

Reply 2 of 66, by superfury

User metadata
Rank l33t++
Rank
l33t++

It's currently filtering at 16,57196759259259kHz(exactly 72 PIT samples at ~1.19kHz(which is exactly 14.31818MHz divided by 12)). Is that correct for the RC low-pass filter?

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

Reply 3 of 66, by reenigne

User metadata
Rank Oldbie
Rank
Oldbie
superfury wrote:

It's currently filtering at 16,57196759259259kHz(exactly 72 PIT samples at ~1.19kHz(which is exactly 14.31818MHz divided by 12)). Is that correct for the RC low-pass filter?

You might miss some high frequencies (16-20kHz) but then again the original PC speaker wasn't exactly a super high-fidelity device so should be fine.

The fact that you picked a cutoff which is an exact number of PIT cycles makes me think you're doing something wrong, though. If you're generating a 1.193MHz 1-bit waveform and resampling it to the output sample rate then you can use whatever cutoff frequency you like - it doesn't need to be an exact number of PIT cycles. If you're resampling by (for example) counting the number of PIT cycles that output is high over the duration of a particular output sample, then that will cause aliasing (I've made this exact mistake myself before, which is why the SoundBlaster sound in Digger Remastered sounds so bad).

Reply 4 of 66, by superfury

User metadata
Rank l33t++
Rank
l33t++

If the 72 PIT samples is incorrect for the low-pass filter, then what WOULD be the correct frequency to cut off at?

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

Reply 5 of 66, by reenigne

User metadata
Rank Oldbie
Rank
Oldbie
superfury wrote:

If the 72 PIT samples is incorrect for the low-pass filter, then what WOULD be the correct frequency to cut off at?

Anything between 20kHz (upper range of human hearing) and the Nyquist frequency of the output sample rate (i.e. 22.05kHz for a 44.1kHz output rate) would be ideal in my opinion. Though as I said above, you can have a gentle roll-off instead of a hard cut-off (and this is a good idea if you're using a finite impulse response filter, since you can get away with a smaller filter kernel).

Reply 6 of 66, by superfury

User metadata
Rank l33t++
Rank
l33t++

So if I change the low-pass RC filter on the PIT to 22050Hz, would the PWM on 8088 MPH and Links 2 still give the correct output?

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

Reply 7 of 66, by reenigne

User metadata
Rank Oldbie
Rank
Oldbie
superfury wrote:

So if I change the low-pass RC filter on the PIT to 22050Hz, would the PWM on 8088 MPH and Links 2 still give the correct output?

I don't know. How does your filter work? What does it sound like at the moment? I'd have thought that the difference between a 16kHz filter and a 22kHz filter would be pretty small, so if the 16kHz filter sounds wrong for 8088 MPH then you've probably got a bug (and it isn't that the filter cutoff is wrong).

Reply 8 of 66, by superfury

User metadata
Rank l33t++
Rank
l33t++

The filter is now completely moved to sound.c:

void updateSoundFilter(HIGHLOWPASSFILTER *filter, byte ishighpass, float cutoff_freq, float samplerate)
{
filter->isHighPass = ishighpass; //Highpass filter?
if (filter->isInit || (filter->cutoff_freq!=cutoff_freq) || (filter->samplerate!=samplerate) || (ishighpass!=filter->isHighPass)) //We're to update?
{
if (ishighpass) //High-pass filter?
{
float RC = (1.0f / (cutoff_freq * (2.0f * (float)PI))); //RC is used multiple times, calculate once!
filter->solid = (RC / (RC + (1.0f / samplerate))); //Solid value to use!
}
else //Low-pass filter?
{
float dt = (1.0f / samplerate); //DT is used multiple times, calculate once!
filter->solid = (dt / ((1.0f / (cutoff_freq * (2.0f * (float)PI))) + dt)); //Solid value to use!
}
}
filter->isHighPass = ishighpass; //Hi-pass filter?
filter->cutoff_freq = cutoff_freq; //New cutoff frequency!
filter->samplerate = samplerate; //New samplerate!
}

void initSoundFilter(HIGHLOWPASSFILTER *filter, byte ishighpass, float cutoff_freq, float samplerate)
{
filter->isInit = 1; //We're an Init!
filter->isFirstSample = 1; //We're the first sample!
updateSoundFilter(filter,ishighpass,cutoff_freq,samplerate); //Init our filter!
}

void applySoundFilter(HIGHLOWPASSFILTER *filter, float *currentsample)
{
INLINEREGISTER float last_result;
if (filter->isFirstSample) //No last? Only executed once when starting playback!
{
filter->sound_last_result = filter->sound_last_sample = *currentsample; //Save the current sample!
filter->isFirstSample = 0; //Not the first sample anymore!
return; //Abort: don't filter the first sample!
}
last_result = filter->sound_last_result; //Load the last result to process!
if (filter->isHighPass) //High-pass filter?
{
last_result = filter->solid * (last_result + *currentsample - filter->sound_last_sample);
}
else //Low-pass filter?
{
last_result = last_result + (filter->solid*(*currentsample-last_result));
}
filter->sound_last_sample = *currentsample; //The last sample that was processed!
*currentsample = filter->sound_last_result = last_result; //Give the new result!
}

That's the entire RC filter that's applied. In the case of the PC speaker(PIT), it's applied like this:

	//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)floor(SAFEDIV(speaker_ticktiming, speaker_tick)); //How many ticks to tick?
speaker_ticktiming -= (length*speaker_tick); //Rest the amount of ticks!

//Ticks the speaker when needed!
i = 0; //Init counter!
//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 = floor(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*SPEAKER_LOWPASSVOLUME):(SHRT_MIN*SPEAKER_LOWPASSVOLUME); //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!
applySoundFilter(&PCSpeakerFilter, &speaker_currentsample);
#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!
writeDoubleBufferedSound16(&pcspeaker_soundbuffer, (short)speaker_currentsample); //Write the sample to the buffer (mono buffer)!
++i; //Add time!
if (i == length) //Fully rendered?
{
return; //Next item!
}
}
}

It's initialization is:

	initSoundFilter(&PCSpeakerFilter,0,(float)SPEAKER_LOWPASS, (float)TIME_RATE); //Initialize our low-pass filter to use!

Where those constants are:

//What volume, in percent!
#define SPEAKER_VOLUME 100.0f

//Speaker playback rate!
#define SPEAKER_RATE 44100
//Use actual response as speaker rate! 60us responses!
//#define SPEAKER_RATE (1000000.0f/60.0f)
//Speaker buffer size!
#define SPEAKER_BUFFER 4096
//Speaker low pass filter values (if defined, it's used)!
#define SPEAKER_LOWPASS 20000.0f
//Speaker volume during filtering!
#define SPEAKER_LOWPASSVOLUME 0.5f

//Precise timing rate!
//The clock speed of the PIT (14.31818MHz divided by 12)!
#define MHZ14_RATE 12
#define TIME_RATE (MHZ14/12.0)

//Run the low pass at the 72 raw samples rate instead (16571Hz)!
#undef SPEAKER_LOWPASS
//Formula for 4.7uF and 8 Ohm results in: 1/(2*PI*RC)=1/(2*PI*8*0.0000047) Hz(~4.4kHz) instead of ~16kHz using 72 samples.
//#define SPEAKER_LOWPASS (1/(2.0f*PI*8*0.0000047))
#define SPEAKER_LOWPASS (TIME_RATE/72.0)

The type definition of the struct that contains all the filter storage:

typedef struct
{
byte isInit; //Initialized filter?
byte isFirstSample; //First sample?
float sound_last_result; //Last result!
float sound_last_sample; //Last sample!

float solid; //Solid value that doesn't change for the filter, until the filter is updated!

//General filter information and settings set for the filter!
byte isHighPass;
float cutoff_freq;
float samplerate;
} HIGHLOWPASSFILTER; //High or low pass filter!

The entire filter being based on:
http://stackoverflow.com/questions/13882038/i … ss-filters-in-c

Original code used:

#if LOW_PASS 
{
float RC = 1.0/(CUTOFF*2*3.14);
float dt = 1.0/SAMPLE_RATE;
float alpha = dt/(RC+dt);
float filteredArray[numSamples];
filteredArray[0] = data.recordedSamples[0];
for(i=1; i<numSamples; i++){
filteredArray[i] = filteredArray[i-1] + (alpha*(data.recordedSamples[i] - filteredArray[i-1]));
}
data.recordedSamples = filteredArray;
}
#endif
#if HIGH_PASS
{
float RC = 1.0/(CUTOFF*2*3.14);
float dt = 1.0/SAMPLE_RATE;
float alpha = RC/(RC + dt);
float filteredArray[numSamples];
filteredArray[0] = data.recordedSamples[0];
for (i = 1; i<numSamples; i++){
filteredArray[i] = alpha * (filteredArray[i-1] + data.recordedSamples[i] - data.recordedSamples[i-1]);
}
data.recordedSamples = filteredArray;
}
#endif

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

Reply 9 of 66, by reenigne

User metadata
Rank Oldbie
Rank
Oldbie

Looks like that stackoverflow post uses an infinite impulse response filter. I haven't played with those. How does it sound? You may have to apply some signal analysis techniques to figure out if the frequency response is suitable for this application.

Also, have you determined that your PIT implementation that generates the 1.193MHz waveform is correct? If you dump it to a file (1 byte, 0 or 255, per PIT cycle) you should be able to load it into a audio editor like Audacity or Adobe Audition (I use Cool Edit Pro for this, which is what Audition was called before Adobe bought it) and resample it to 44100Hz there to figure out if sounds right that way.

Reply 10 of 66, by Jepael

User metadata
Rank Oldbie
Rank
Oldbie

An RC filter may not be good enough here and sinc filter would be overkill.

So using 44100 Hz sampling rate, frequencies above 20 (or 22) kHz should have no energy at all. Assuming 40 dB of attenuation at 22 kHz is enough, and RC filter attenuates 20dB per decade, RC cutoff should be set two decades below 22 kHz, or 200 Hz! That's practically useless.

Biquads are a type of IIR filters that are basically two RC filters slabbed together, so they are second order filters with 40dB attenuation per decade. Add enough biquad stages together to get wanted passband and blocking characteristics. Basically, if it sounds good enough it is good enough.

Reply 11 of 66, by superfury

User metadata
Rank l33t++
Rank
l33t++

If I simply take the 60us response of the speaker and convert it back to PIT samples, I end up with 72÷0.9943180556
=72.411437763 PIT samples per response time. So the 72 samples seems roughly correct.

So if I simply apply two of the same low-pass 72-sample filters(or 60us filters) in serial fashion(applylowpassfilter1(&sample); applylowpassfilter2(&sample);) with both filters at the same frequency, I would end up with the correct output?

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

Reply 12 of 66, by reenigne

User metadata
Rank Oldbie
Rank
Oldbie
superfury wrote:

If I simply take the 60us response of the speaker

Where did this 60us response figure come from? (Not saying it's wrong, just curious).

superfury wrote:

So if I simply apply two of the same low-pass 72-sample filters(or 60us filters) in serial fashion(applylowpassfilter1(&sample); applylowpassfilter2(&sample);) with both filters at the same frequency, I would end up with the correct output?

How do you define "correct" here given that you're dealing with an analogue system which has variations between machines, and which also part of a very noisy system? If you were to model the power supply fan noise from an original IBM PC 5150 then the 8088 MPH mod player output would be quite difficult to hear. Assuming you're not doing that, you're making it closer to some "ideal" speaker, so why not just give it a frequency response that is as flat as you sensibly can?

What is the issue you're having with your current implementation anyway? Are you suffering from aliasing? Lacking high frequencies? Or is the output incorrect in some other way? What are you trying to achieve with this low-pass filter anyway?

Reply 13 of 66, by superfury

User metadata
Rank l33t++
Rank
l33t++

I'm trying to make the PWM output correct for ALL applications that can use it, with correct enough filters to give correct enough output to be heard the same way as a IBM PC, at 44.1kHz, as accurate and fast as possible.

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

Reply 14 of 66, by reenigne

User metadata
Rank Oldbie
Rank
Oldbie
superfury wrote:

I'm trying to make the PWM output correct for ALL applications that can use it, with correct enough filters to give correct enough output to be heard the same way as a IBM PC, at 44.1kHz, as accurate and fast as possible.

And does that not happen with the filter cutoff at 16kHz?

Reply 15 of 66, by Jepael

User metadata
Rank Oldbie
Rank
Oldbie
reenigne wrote:
superfury wrote:

If I simply take the 60us response of the speaker

Where did this 60us response figure come from? (Not saying it's wrong, just curious).

He is taking this article very literally: http://wiki.osdev.org/PC_Speaker

I think I have read that elsewhere about 60us as well, the OSDEV article might have been written based on that.

And no, the 60us should not be taken literally. The frequency response of a speaker is not that simply said.

Although, 1/60us is 16.7 kHz if interpreted as cutoff frequency, that's good enough.

It's just that a simulated first order RC filter with cutoff at 16kHz, will only attenuate 6dB/octave, so the amplitude of 32kHz is down by only 6dB, so that's pretty poor preventing aliasing when you need to remove everything at 22kHz.

Reply 16 of 66, by reenigne

User metadata
Rank Oldbie
Rank
Oldbie
Jepael wrote:

It's just that a simulated first order RC filter with cutoff at 16kHz, will only attenuate 6dB/octave, so the amplitude of 32kHz is down by only 6dB, so that's pretty poor preventing aliasing when you need to remove everything at 22kHz.

Right.

I'm thinking that if the current filter implementation sounds bad, superfury should take a look at libresample or libsamplerate (http://www.mega-nerd.com/SRC/) since they're designed for exactly this sort of thing. libsamplerate was what I used for preview output from the mod converter program that I used to generate the data for 8088 MPH, and it sounded great.

Reply 17 of 66, by superfury

User metadata
Rank l33t++
Rank
l33t++

If aliasing needs to be removed, at 22050Hz all samples would need to be quiet(fully attenuated), if I'm not mistaken? Then, theoretically speaking, using an IIR low-pass filter, what would need to be the cutoff frequency to do that, speaking in formulas(for accuracy converting to floating point frequency)?

Edit: Btw, I've moved the filters to a support module, since they're not used in the main sound processing(actually in the sound processing, as well as any relevant sound routine using it differently).
https://bitbucket.org/superfury/unipcemu/src/ … ers.h?at=master
https://bitbucket.org/superfury/unipcemu/src/ … ers.c?at=master

That's the current IIR filters based on the code given earlier(both linked to(stack) and code copy).

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

Reply 18 of 66, by reenigne

User metadata
Rank Oldbie
Rank
Oldbie
superfury wrote:

If aliasing needs to be removed, at 22050Hz all samples would need to be quiet(fully attenuated), if I'm not mistaken?

Not just at 22050Hz, at any frequency above that as well. And, for a general waveform you need eliminate all frequency components at and above 22050Hz while leaving as many of the frequency components below 22050Hz intact.

superfury wrote:

Then, theoretically speaking, using an IIR low-pass filter, what would need to be the cutoff frequency to do that,

Well, the cutoff is exactly that - 22050Hz. The method used to implement the filter (FIR, IIR, FFT) and its parameters has no bearing on that - only on the attenuation of frequencies below 22050Hz.

I'm not convinced you're going to have a lot of success implementing your own digital filtering code without understanding the underlying signal processing theory.

Reply 19 of 66, by superfury

User metadata
Rank l33t++
Rank
l33t++

So, if the cutoff frequency is set to 22kHz, won't there be higher frequencies still in the mix after filtering, although at 6dB more quiet for each octave above 22050Hz? Also, won't that mess up the PWM used at 72 PWM samples, since the calculated response is much faster? So you would need (22050/44100)*~1190000(1.19MHz) PIT samples instead of 72 in order for the PWM to give valid effects? Or is the only requirement that there is a low-pass filter present, no matter what it's cutoff frequency? Would a IIR low-pass filter at 22050Hz still allow PWM using a division of x out of 72(73 depths) to give correctly audible results? Or will that destroy the entire trick, since the frequency response is off(by a lot)?

Last edited by superfury on 2016-12-26, 10:05. Edited 1 time in total.

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