Android: MediaPlayer setVolume function - android

about the params
Set what to make the player no sound and full sound
Thanks

This function is actualy wonderful. Thanks to it you can create a volume scale with any number of steps!
Let's assume you want 50 steps:
int maxVolume = 50;
Then to set setVolume to any value in this range (0-49) you do this:
float log1=(float)(Math.log(maxVolume-currVolume)/Math.log(maxVolume));
yourMediaPlayer.setVolume(log1,log1); //set volume takes two paramater
Nice and easy! And DON'T use AudioManager to set volume! It will cause many side effects such as disabling silent mode, which will make your users mad!

Following user100858 solution I just post my exact code that works:
private final static int MAX_VOLUME = 100;
...
...
final float volume = (float) (1 - (Math.log(MAX_VOLUME - soundVolume) / Math.log(MAX_VOLUME)));
mediaPlayer.setVolume(volume, volume);
soundVolume is the volume you would like to set, between 0 and MAX_VOLUME.
So between 0 and 100 in this example.

For Android MediaPlayer.setVolume,
searching the web seems to show 0.0f for no sound, 1.0f for full sound.

The other answers here are not correct--or at least, they're not configured properly.
Perform the following test, using their code (e.g. that of Tomasz or ssuukk):
1) Set 100 as the "max volume"/number of steps, and submit the volume 50.
It returns: 0.150514997831991
2) Set 1000 as the "max volume"/number of steps, and submit the volume 500.
What does it return? The same value, 0.150514997831991, right?
Nope. Instead, it's: 0.100343331887994
In other words, the existing answers change how they scale the input volume-percent (i.e. the transformation curve) based on how many volume-steps you set.
I've spent the last few hours looking into this issue; enough that I don't feel like going into too much detail explaining the issue. Instead I'll just post the large code/comment block in my program concerning it. (it's in C#, for Xamarin Android, but the functionality should be the same for Java)
public enum VolumeScaleType
{
//Energy, // what MediaPlayer possibly treats passed values as
Amplitude, // what MediaPlayer most likely treats passed values as
Loudness // what people treat everyday volume values as (as in "that sounded 2 times as loud")
}
// MediaPlayer
/*public static void SetVolume_IncorrectSOApproach(this MediaPlayer s, double volume, VolumeScaleType volumeType = VolumeScaleType.Loudness)
{
const int maxVolume = 100;
var volume_toScale = volume * maxVolume;
double volume_scalar = volumeType == VolumeScaleType.Amplitude ? volume : (1 - (Math.Log(maxVolume - volume_toScale) / Math.Log(maxVolume)));
s.SetVolume((float)volume_scalar, (float)volume_scalar);
}*/
public static void SetVolume_MyPossiblyCorrectApproach(this MediaPlayer s, double volume, VolumeScaleType volumeType = VolumeScaleType.Loudness)
{
// Links:
// 1) http://en.wikipedia.org/wiki/Decibel
// 2) http://trace.wisc.edu/docs/2004-About-dB
// 3) http://hyperphysics.phy-astr.gsu.edu/hbase/sound/loud.html
// 4) http://www.animations.physics.unsw.edu.au/jw/dB.htm
// 5) http://www.soundmaskingblog.com/2012/06/saved_by_the_bell
// 6) http://www.campanellaacoustics.com/faq.html
// 7) http://physics.stackexchange.com/questions/9113/how-sound-intensity-db-and-sound-pressure-level-db-are-related
// 8) http://www.sengpielaudio.com/calculator-loudness.htm (note: page uses terms 'power/intensity' and 'pressure' differently; power/intensity: for whole shell at distance, pressure: field-quantity?)
// basic idea: you can think of one decibel (of gain), + or -, as *translating into* the given changes-in/multipliers-for energy, amplitude, or loudness
// (i.e. one decibel provides a specific amount to multiply energy, amplitude, and loudness values, such that they remain aligned realistically)
// note: the 'one decibel' unit is set up to correspond roughly to a change in loudness just substantial enough to be noticeable
// note: the 'quietest perceivable sound' example (standard) base has these absolute values: 'e' is 1 pico-watt per square-foot, 'a' is 20 micropascals, 'l' is the quietest-perceivable-loudness
// references (for q.p.s. base) | db (gain) | energy | amplitude | loudness
// ===============================================================================================
// actual silence | -inf | 0 | 0 | 0
// (a seeming silence) | -20 | e / 100 | a / 10 | 0 (would be l / 4, if 'l' weren't already for the quietest-perceivable-sound)
// (a seeming silence) | -10 | e / 10 | a / 3.16227/sqrt(10) | 0 (would be l / 2, if 'l' weren't already for the quietest-perceivable-sound)
// quietest perceivable sound | 0 | e | a | l
// ? | 1 | e * 1.258925 | a * 1.122018 | l * 1.071773
// rustling leaves | 10 | e * 10 | a * 3.16227/sqrt(10) | l * 2
// whisper, or rural nighttime | 20 | e * 100 | a * 10 | l * 4
// watch ticking | 30 | e * 1000 | a * 31.622/sqrt(100) | l * 8
// quiet speech, or rural daytime | 40 | e * 10000 | a * 100 | l * 16
// dishwasher in next room | 50 | e * 100000 | a * 316/sqrt(100000) | l * 32
// ordinary conversation | 60 | e * 1000000 | a * 1000 | l * 64
// ===============================================================================================
// assuming MediaPlayer.SetVolume treats passed values as Amplitude
Func<double, double> convertLoudnessToAmplitude = loudness=>Math.Pow(10, Math.Log(loudness, 4));
var volume_amplitude = volumeType == VolumeScaleType.Amplitude ? volume : convertLoudnessToAmplitude(volume);
s.SetVolume((float)volume_amplitude, (float)volume_amplitude);
// assuming MediaPlayer.SetVolume treats passed values as Energy
//Func<double, double> convertLoudnessToEnergy = loudness=>Math.Pow(100, Math.Log(loudness, 4));
//var volume_energy = volumeType == VolumeScaleType.Energy ? volume : convertLoudnessToEnergy(volume);
//s.SetVolume((float)volume_energy, (float)volume_energy);
}
Conclusion
The documentation is sparse, so I can't know for sure if I have the right scaling-system/type-of-unit the SetVolume method expects.
Assuming it expects an Amplitude value, the code above may be the correct volume setting code for it. (taking desired Loudness, linear, as an input, and outputting/setting the Amplitude value needed for the built-in SetVolume method)
I'm not sure it's correct, though, and am too tired to confirm. If anyone has further thoughts, feel free to add them. (3+ hours is enough to spend on an issue like this, in one day)
Edit
After listening carefully, and comparing the loudness-fade effect by:
Just submitting the desired loudness to the SetVolume method.
Exponentiating (basically) the desired-loudness before sending it in, to make it an Amplitude (or the like) value that the SetVolume method says it expects.
I find that option 1 seems to be closer to a linear loudness fade-in! In other words... from actually listening and comparing the basic approach, with the various transformation approaches shown here, it seems the documentation is wrong and the SetVolume method does in fact just expect the loudness value on a linear scale. (perhaps they've updated it to work more intuitively in one of the recent API versions, but haven't updated the docs?)
If so, that sure makes it easy. That's what I'm going with for now. (though I'll keep the exponentiation/scale-fixing approach as a program setting, I suppose, just to have an excuse to keep some result of all that time invested!)

The recommended answer is wrong, as stated Venryx. Log math doesn't work that way (you have to subtract, not divide logs to make them work how you want).
No matter, it looks like Android Volume setting is now proportionate to Loudness linearly... so 0.5 is 50% as loud as 1.0, and 0.1 is 10%, etc. No need for complicated Log math to convert decibels to loudness. Just set it linearly as is intuitive to most people.

I have tried Android MediaPlayer.setVolume, but this function is useless.
I think we should use the function below
AudioManager mAudioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, maxVolume * mLastProgress / 10, 0);

Everything I have seen here has fallen short of my expectations. The main problem I had was that on a scale of 0 to 50, 25 was never in the middle but rather much closer to the maximum sound. The log functions proposed here made almost no difference for me.
To read more on the math, see this answer.
Variables
Linear input value = x // User-specified input value
Linear scale min,max = x1,x2 // My pre-determined range of 0-50 on my UI
Log scale min,max = y1,y2 // Normalizes the log result to between 0-1
Log value result = z // The output to pass to the setVolume() method
Formula where change decelerates as value goes up (simplest form)
Problem with this approach is that this is the opposite of what we want with android because it seems to already be doing this by default. It's already incrementing too fast when the values are still low when you pass linear values and this accentuates this effect even further.
x1 + (log(x) - log(x1)) / (log(x2) - log(x1)) * (y2 - y1) = z
Results of this function
Formula where change accelerates as value goes up
This is the approach that works for me; flipping the input to keep the same rate of change, but inversed. With this, I get just about a perfect mid-volume at around 25 and it's a very smooth hearing experience from 0 all the way to 50.
y2 - (x1 + (log(x2 + x1 - x) - log(x1)) / (log(x2) - log(x1)) * (y2 - y1)) = z
Results of this function

This code breaks down volume into 10 equal sections and increase or descrease volume.
Button decreaseVolButton = (Button) findViewById(R.id.decrease_volumn);
Button increaseVolButton = (Button) findViewById(R.id.increase_volumn);
final MediaPlayer mediaPlayer = MediaPlayer.create(MainActivity.this, R.raw.sample);
decreaseVolButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
volume = (float) (volume - 0.1);
mediaPlayer.setVolume(volume, volume);
}
});
increaseVolButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
volume = (float) (volume + 0.1);
mediaPlayer.setVolume(volume, volume);
}
});
If you want to set the volume to no sound then pass (0f,0f)
If you want to set the volume to full sound then pass (1f,1f)

Why making it so complicated? I am using this simple formula:
public float getVolume() {
float currVolume = (float) sp.getInt("volume", 10);
float maxVolume = 15.0f;
float result = currVolume / maxVolume;
return result;
}
and setting this value in media player, like:
player.setVolume(getVolume(), getVolume());

Since volume scales linearly, needless for complicated log function. Adjust the maxVolume, it's 100 steps in the sample code below, accordingly to the step you prefer and it should work. Hope this would help.
MediaPlayer myPlayer = MediaPlayer.create(MainActivity.this, R.raw.myAudioResource);
final float maxVolume = 100.0f;
float currentVolume = 5.0f;
myPlayer.setVolume(currentVolume/maxVolume, currentVolume/maxVolume);
myPlayer.start();

Well, I made the following code and it slightly works:
public class MainActivity extends Activity {
float volumeLevel = 0.5f;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
volumeUp = findViewById(R.id.volUp);
volumeDown = findViewById(R.id.volDown);
song = MediaPlayer.create(this, R.raw.audioFile);
volumeUp.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v){
volumeLevel = volumeLevel + 0.1f;
song.setVolume(volumeLevel,volumeLevel);
}
});
volumeDown.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
volumeLevel = volumeLevel - 0.1f;
song.setVolume(volumeLevel,volumeLevel);
}
});
}
}

Related

Add Toast in public class corresponding to any event

There all, i'm a newbie in android programming and i've a school task to analysis some project. So, i choose a Guitar Tuner project from github called pTune (link: here).
I've read the code and analyze it, from what i read and see there are a needle with an arc meter to see if the input sound (guitar voice) was fit.
In that project i want to display Toast if the tuning process are fit when the dial in 90 degree. So, i've add this Toast but its not displayed..
if (relativeFrequency == targetFrequency){
Toast.makeText(PTuneActivity.this, "FIT", Toast.LENGTH_SHORT).show();
}
That code placed in updateDisplay class, i've placed it in other class but still not work as i want.
public void updateDisplay(float frequency) {
// Calculate difference between target and measured frequency,
// given that the measured frequency can be a factor of target.
float difference = 0;
if (frequency > targetFrequency) {
int divisions = (int) (frequency / targetFrequency);
float modified = targetFrequency * (float) divisions;
if (frequency - modified > targetFrequency / 2) {
modified += targetFrequency;
divisions++;
}
difference = (frequency - modified) / (float) divisions;
} else {
// If target is greater than measured, just use difference.
difference = frequency - targetFrequency;
}
float relativeFrequency = targetFrequency + difference;
// Update TextView
if (relativeFrequency < 1000f)
t.setText(String.format("%.1f Hz", relativeFrequency));
else
t.setText(String.format("%.2f kHz", relativeFrequency/1000));
//My code
if (relativeFrequency == targetFrequency){
Toast.makeText(PTuneActivity.this, "FIT", Toast.LENGTH_SHORT).show();
}
// Update DialView
float value = difference / (targetFrequency / 2) * 90;
dial.update(value);
}
I know this is useless in real implementation but i want to learn android programming.
You're testing equality between 2 floats, one comes from an input ? It might never be exactly equal. You should add some log and an "else" statement to see if it goes inside the "if" because the Toast may not be the real problem
i've just try it by my self by digging and its worked for me, though its not good at all but i thinks ok, just add this line of code upper dial.update(value);
if (frequency >= targetFrequency) {
Toast.makeText(PTuneActivity.this, "FIT", Toast.LENGTH_SHORT).show();
}

Smoother Android SeekBar

I'm not strictly looking for an implementation of this idea but if someone has already made it, that would be awesome and I'd like to see it. Otherwise:
I'm trying to implement a couple SeekBars in my Android's waveform generator app. I have a couple controls such as: volume, frequency, low pass filter cutoff frequency, resonance, pitch bend, etc.
The problem with my SeekBars are that they sound too step-y and I want it to sound more analog-ish (smoother if you will). In my iOS implementation of the app, the native UISliders did a good job and I didn't hear any step-like movements. However, the SeekBars aren't very smooth and tend to jump value to value (lets say like from 10 to 100 with a max value of 1000).
I was wondering if it might be best if I just design my own custom UI for a smoother slider or if there is one already. Also, is it possible that my audio thread is interrupting the SeekBar's functionality and causing these jumps/step-like behavior?
Things I've tried already:
Lowpass the seekbar progress in the listener's onProgressChanged. This doesn't really work (if it jumped from 5 to 100 for example, this would give me a value in between but that still doesn't give a full smooth-like behavior).
// b = 0.99, a = 0.01
// Follows simple lowpass: yn = (xn * b) + (yn1 * a)
public double lowpass(int xn) {
double yn = (xn * b) + (lastProgress * a);
lastProgress = yn;
return yn;
}
If there is a huge jump (like 5 to 100), I would call a while loop to increment the audio context's variables by 1. The problem with this is if I was trying to do a pitch bend (a 14bit number so that's 16384 values total), it would take too long to get to the target value (the pitch bend does sound cool though). for example (obviously this only accounts for progress going up):
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
int myProgress = seekBar.getProgress();
while (myProgress < progress) {
// This will increase the audio context's frequency variable by one every loop until we've reached our target progress
audioContext.setFrequency(myProgress++);
}
}
Thanks!
First, figure out what is the fastest you want the volume to increase. For this example, I'll use 1 second (1000ms) to change from 0 to 1.0. (0% to 100%)
When you enter the loop, record the current time. Then, for each iteration of your loop, check the time passed and increment the necessary amount.
// example, myLoop(0.25, 0.75, startTime);
double myLoop(double start, double end, long startTime) {
long now = System.currentTimeMillis(); // (100ms since startTime)
double span = end - start; // the span in value, 0 to 1.0 (=0.5)
double timeSpan = (now - startTime) / (span * 1000); // time since started / total time of change (100/500 = 0.2)
return timeSpan * span + start; // (0.2 * 0.5 = 0.1) + 0.25 = 0.35, the value for this loop
}
(untested code)

Problems to set volume of Cocos2d-X Simple Audio Engine?

I am developing a game using Cocos2d-X 2.2.6 on top of Marmalade SDK 7.8.0 and I am having some problems to set the sound effect volume of theCocos Denshion Simple Audio Engine.
My code is this:
void AudioHelper::init()
{
s3eResult result;
context = GameContext::getInstance();
s3eSoundSetInt(S3E_SOUND_DEFAULT_FREQ, 44100);
audioEngine = SimpleAudioEngine::sharedEngine();
preloadSoundEffects();
setVolume(context->getSoundVolume());
}
void AudioHelper::preloadSoundEffects()
{
unsigned int i;
CCArray *keysArray;
CCString *key;
const CCString *sound;
keysArray = soundEffects->allKeys();
for (i = 0; i < keysArray->count(); i++)
{
key = (CCString *) keysArray->objectAtIndex(i);
sound = soundEffects->valueForKey(key->getCString());
audioEngine->preloadEffect(sound->getCString());
}
}
void AudioHelper::setVolume(int volumeLevel)
{
float volume;
// Note: there are 6 volume levels in my game, they go from
// 0 (max volume) to 5 (no sound).
volume = 1 - volumeLevel / 5.0;
audioEngine->setEffectsVolume(volume);
}
void AudioHelper::playSoundEffect(const char *effectKey)
{
const CCString *sound;
int volume;
sound = soundEffects->valueForKey(effectKey);
if (sound != NULL)
audioEngine->playEffect(sound->getCString());
}
The problem is that the sound effects volume doesn't change when I call the setVolume method, except when I set the volume level parameter to 5 (no sound). When this happens no sounds are played (as expected).
What I am doing wrong?
did you log actual values in this code?
void AudioHelper::setVolume(int volumeLevel)
{
float volume;
// Note: there are 6 volume levels in my game, they go from
// 0 (max volume) to 5 (no sound).
volume = 1 - volumeLevel / 5.0;
audioEngine->setEffectsVolume(volume);
}
you cannot mix float and int calculations in such a way. float volume here would be 1 or 0 anyway, because you are using volumeLevel as type int. your code should look like this:
void AudioHelper::setVolume(int volumeLevel)
{
float volume;
// Note: there are 6 volume levels in my game, they go from
// 0 (max volume) to 5 (no sound).
volume = 1.0f - (float)volumeLevel / 5.0f;
audioEngine->setEffectsVolume(volume);
}
also take into account that there is some limitations using CocosDenshion at win platform (no volume changing there).
s3eSound expects values between 0-255 when setting the volume.
You can trace the current volume by calling:
s3eSoundGetInt(S3E_SOUND_VOLUME)
each time you set the volume to a specific value and see whether S3E_SOUND_VOLUME changes as expected.
I notice you use:
volume = 1 - volumeLevel / 5.0;
This results a value between 1 and 0 with 0.20 decrements if providing input values from 1 to 5.
Either these values are being set wrongly or being converted incorrectly by the Simple Audio Engine to the s3eSound values.
Tracing out both will give you more insight into what might be going wrong.

Fire events at specific at specific progress as the seek bar progresses Android

I followed this tutorial to create an audio player.
Now I want to add some events that fires events when the player reaches a specific progress.
For example, at 00:32 of the current audio, display a Toast.
The interface should look like this :
The white dots represents the events in this case.
Any idea?
Calculate the total duration of the supported audio file, using getDuration() method, use getCurrentPosition() method to find out if the seekbar has progressed your desired point. Fire events accordingly.
I found it guys, here is my code (that worked perfectly)
private ImageView CreateWhiteDot(int time) {
ImageView WhiteDot = new ImageView(getApplicationContext());
float x = 0;
float y = 0;
WhiteDot.setImageResource(R.drawable.stop_dot_inactive);
x = sbStopPlayerSlider.getX() + 15;
x += ((sbStopPlayerSlider.getWidth() - 30) * time * 1000)
/ mpAudioPlayer.getDuration();
WhiteDot.setX(x);
y = sbStopPlayerSlider.getTranslationY()
+ sbStopPlayerSlider.getHeight() / 2;
WhiteDot.setY(y);
return WhiteDot;
}

Android Compass orientation on unreliable (Low pass filter)

Im creating an application where i need to position a ImageView depending on the Orientation of the device.
I use the values from a MagneticField and Accelerometer Sensors to calculate the device orientation with
SensorManager.getRotationMatrix(rotationMatrix, null, accelerometerValues, magneticFieldValues)
SensorManager.getOrientation(rotationMatrix, values);
double degrees = Math.toDegrees(values[0]);
My problem is that the positioning of the ImageView is very sensitive to changes in the orientation. Making the imageview constantly jumping around the screen. (because the degrees change)
I read that this can be because my device is close to things that can affect the magneticfield readings. But this is not the only reason it seems.
I tried downloading some applications and found that the "3D compass" and "Compass" remains extremely steady in its readings (when setting the noise filter up), i would like the same behavior in my application.
I read that i can tweak the "noise" of my readings by adding a "Low pass filter", but i have no idea how to implement this (because of my lack of Math).
Im hoping someone can help me creating a more steady reading on my device, Where a little movement to the device wont affect the current orientation.
Right now i do a small
if (Math.abs(lastReadingDegrees - newReadingDegrees) > 1) { updatePosition() }
To filter abit of the noise. But its not working very well :)
Though I havn't used the compass on Android, the basic processing shown below (in JavaScript) will probably work for you.
It's based on the low pass filter on the accelerometer that's recommended by the Windows Phone team with modifications to suit a compass (the cyclic behavior every 360").
I assume the compass reading is in degrees, a float between 0-360, and the output should be similar.
You want to accomplish 2 things in the filter:
If the change is small, to prevent gitter, gradually turn to that direction.
If the change is big, to prevent lag, turn to that direction immediatly (and it can be canceled if you want the compass to move only in a smooth way).
For that we will have 2 constants:
The easing float that defines how smooth the movement will be (1 is no smoothing and 0 is never updating, my default is 0.5). We will call it SmoothFactorCompass.
The threshold in which the distance is big enough to turn immediatly (0 is jump always, 360 is never jumping, my default is 30). We will call it SmoothThresholdCompass.
We have one variable saved across the calls, a float called oldCompass and it is the result of the algorithm.
So the variable defenition is:
var SmoothFactorCompass = 0.5;
var SmoothThresholdCompass = 30.0;
var oldCompass = 0.0;
and the function recieves newCompass, and returns oldCompass as the result.
if (Math.abs(newCompass - oldCompass) < 180) {
if (Math.abs(newCompass - oldCompass) > SmoothThresholdCompass) {
oldCompass = newCompass;
}
else {
oldCompass = oldCompass + SmoothFactorCompass * (newCompass - oldCompass);
}
}
else {
if (360.0 - Math.abs(newCompass - oldCompass) > SmoothThresholdCompass) {
oldCompass = newCompass;
}
else {
if (oldCompass > newCompass) {
oldCompass = (oldCompass + SmoothFactorCompass * ((360 + newCompass - oldCompass) % 360) + 360) % 360;
}
else {
oldCompass = (oldCompass - SmoothFactorCompass * ((360 - newCompass + oldCompass) % 360) + 360) % 360;
}
}
}
I see that the issue was opened 5 months ago and probably isn't relevant anymore, but I'm sure other programmers might find it useful.
Oded Elyada.
This lowpass filter works for angles in radians. Use the add function for each compass reading, then call average to get the average.
public class AngleLowpassFilter {
private final int LENGTH = 10;
private float sumSin, sumCos;
private ArrayDeque<Float> queue = new ArrayDeque<Float>();
public void add(float radians){
sumSin += (float) Math.sin(radians);
sumCos += (float) Math.cos(radians);
queue.add(radians);
if(queue.size() > LENGTH){
float old = queue.poll();
sumSin -= Math.sin(old);
sumCos -= Math.cos(old);
}
}
public float average(){
int size = queue.size();
return (float) Math.atan2(sumSin / size, sumCos / size);
}
}
Use Math.toDegrees() or Math.toRadians() to convert.
Keep in mind that, for example the average of 350 and 10 is not 180. My solution:
int difference = 0;
for(int i= 1;i <numberOfAngles;i++){
difference += ( (angles[i]- angles[0] + 180 + 360 ) % 360 ) - 180;
}
averageAngle = (360 + angles[0] + ( difference / numberOfAngles ) ) % 360;
A low pass filter (LPF) blocks fast changing signals and
allows only slow changes in the signals. This means any small
sudden changes will be ignored.
The standard way to implement this in software is to take a running average
of the last N samples and report that value. Start with N as small as 3 and
keep increasing N until you find sufficient smoothed out response in your app.
Do keep in mind that the higher you make N, slower the response of the system.
See my answer to this related question: Smoothing data from a sensor
A software low pass filter is basically a modified version of that. Indeed, in that answer I even provided this link to another related question: Low pass filter software?

Categories

Resources