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?
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.
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?
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).
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).
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).
1void updateSoundFilter(HIGHLOWPASSFILTER *filter, byte ishighpass, float cutoff_freq, float samplerate) 2{ 3 filter->isHighPass = ishighpass; //Highpass filter? 4 if (filter->isInit || (filter->cutoff_freq!=cutoff_freq) || (filter->samplerate!=samplerate) || (ishighpass!=filter->isHighPass)) //We're to update? 5 { 6 if (ishighpass) //High-pass filter? 7 { 8 float RC = (1.0f / (cutoff_freq * (2.0f * (float)PI))); //RC is used multiple times, calculate once! 9 filter->solid = (RC / (RC + (1.0f / samplerate))); //Solid value to use! 10 } 11 else //Low-pass filter? 12 { 13 float dt = (1.0f / samplerate); //DT is used multiple times, calculate once! 14 filter->solid = (dt / ((1.0f / (cutoff_freq * (2.0f * (float)PI))) + dt)); //Solid value to use! 15 } 16 } 17 filter->isHighPass = ishighpass; //Hi-pass filter? 18 filter->cutoff_freq = cutoff_freq; //New cutoff frequency! 19 filter->samplerate = samplerate; //New samplerate! 20} 21 22void initSoundFilter(HIGHLOWPASSFILTER *filter, byte ishighpass, float cutoff_freq, float samplerate) 23{ 24 filter->isInit = 1; //We're an Init! 25 filter->isFirstSample = 1; //We're the first sample! 26 updateSoundFilter(filter,ishighpass,cutoff_freq,samplerate); //Init our filter! 27} 28 29void applySoundFilter(HIGHLOWPASSFILTER *filter, float *currentsample) 30{ 31 INLINEREGISTER float last_result; 32 if (filter->isFirstSample) //No last? Only executed once when starting playback! 33 { 34 filter->sound_last_result = filter->sound_last_sample = *currentsample; //Save the current sample! 35 filter->isFirstSample = 0; //Not the first sample anymore! 36 return; //Abort: don't filter the first sample! 37 } 38 last_result = filter->sound_last_result; //Load the last result to process! 39 if (filter->isHighPass) //High-pass filter? 40 { 41 last_result = filter->solid * (last_result + *currentsample - filter->sound_last_sample); 42 } 43 else //Low-pass filter? 44 { 45 last_result = last_result + (filter->solid*(*currentsample-last_result)); 46 } 47 filter->sound_last_sample = *currentsample; //The last sample that was processed! 48 *currentsample = filter->sound_last_result = last_result; //Give the new result! 49}
That's the entire RC filter that's applied. In the case of the PC speaker(PIT), it's applied like this:
1 //PC speaker output! 2 speaker_ticktiming += timepassed; //Get the amount of time passed for the PC speaker (current emulated time passed according to set speed)! 3 if ((speaker_ticktiming >= speaker_tick) && enablespeaker) //Enough time passed to render the physical PC speaker and enabled? 4 { 5 length = (uint_32)floor(SAFEDIV(speaker_ticktiming, speaker_tick)); //How many ticks to tick? 6 speaker_ticktiming -= (length*speaker_tick); //Rest the amount of ticks! 7 8 //Ticks the speaker when needed! 9 i = 0; //Init counter! 10 //Generate the samples from the output signal! 11 for (;;) //Generate samples! 12 { 13 //Average our input ticks! 14 PITchannels[2].samplesleft += ticklength; //Add our time to the sample time processed! 15 tempf = floor(PITchannels[2].samplesleft); //Take the rounded number of samples to process! 16 PITchannels[2].samplesleft -= tempf; //Take off the samples we've processed! 17 render_ticks = (uint_32)tempf; //The ticks to render! 18 19 //render_ticks contains the output samples to process! Calculate the duty cycle by low pass filter and use it to generate a sample! 20 for (dutycyclei = render_ticks;dutycyclei;) 21 { 22 if (!readfifobuffer(PITchannels[2].rawsignal, ¤tsample)) break; //Failed to read the sample? Stop counting! 23 speaker_currentsample = currentsample?(SHRT_MAX*SPEAKER_LOWPASSVOLUME):(SHRT_MIN*SPEAKER_LOWPASSVOLUME); //Convert the current result to the 16-bit data, signed instead of unsigned! 24 #ifdef SPEAKER_LOGRAW 25 writeWAVMonoSample(speakerlograw,(short)speaker_currentsample); //Log the mono sample to the WAV file, converted as needed! 26 #endif 27 #ifdef SPEAKER_LOWPASS 28 //We're applying the low pass filter for the speaker! 29 applySoundFilter(&PCSpeakerFilter, &speaker_currentsample); 30 #endif 31 #ifdef SPEAKER_LOGDUTY 32 writeWAVMonoSample(speakerlogduty,(short)speaker_currentsample); //Log the mono sample to the WAV file, converted as needed! 33 #endif 34 } 35 36 //Add the result to our buffer! 37 writeDoubleBufferedSound16(&pcspeaker_soundbuffer, (short)speaker_currentsample); //Write the sample to the buffer (mono buffer)! 38 ++i; //Add time! 39 if (i == length) //Fully rendered? 40 { 41 return; //Next item! 42 } 43 } 44 }
It's initialization is:
1 initSoundFilter(&PCSpeakerFilter,0,(float)SPEAKER_LOWPASS, (float)TIME_RATE); //Initialize our low-pass filter to use!
Where those constants are:
1//What volume, in percent! 2#define SPEAKER_VOLUME 100.0f 3 4//Speaker playback rate! 5#define SPEAKER_RATE 44100 6//Use actual response as speaker rate! 60us responses! 7//#define SPEAKER_RATE (1000000.0f/60.0f) 8//Speaker buffer size! 9#define SPEAKER_BUFFER 4096 10//Speaker low pass filter values (if defined, it's used)! 11#define SPEAKER_LOWPASS 20000.0f 12//Speaker volume during filtering! 13#define SPEAKER_LOWPASSVOLUME 0.5f 14 15//Precise timing rate! 16//The clock speed of the PIT (14.31818MHz divided by 12)! 17#define MHZ14_RATE 12 18#define TIME_RATE (MHZ14/12.0) 19 20//Run the low pass at the 72 raw samples rate instead (16571Hz)! 21#undef SPEAKER_LOWPASS 22//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. 23//#define SPEAKER_LOWPASS (1/(2.0f*PI*8*0.0000047)) 24#define SPEAKER_LOWPASS (TIME_RATE/72.0)
The type definition of the struct that contains all the filter storage:
1typedef struct 2{ 3 byte isInit; //Initialized filter? 4 byte isFirstSample; //First sample? 5 float sound_last_result; //Last result! 6 float sound_last_sample; //Last sample! 7 8 float solid; //Solid value that doesn't change for the filter, until the filter is updated! 9 10 //General filter information and settings set for the filter! 11 byte isHighPass; 12 float cutoff_freq; 13 float samplerate; 14} HIGHLOWPASSFILTER; //High or low pass filter!
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.
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.
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?
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?
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.
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?
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.
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.
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)?
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.
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.