khavnu / kraigsandroid

Automatically exported from code.google.com/p/kraigsandroid
0 stars 0 forks source link

Alarm fade volume shall be not be linear #92

Open GoogleCodeExporter opened 8 years ago

GoogleCodeExporter commented 8 years ago
It's a very common problem among people making volume controls to make them 
linear. They don't realize that humans don't sense sound linearly. This is 
especially important when fading the alarm volume. Setting “volume = time” 
is wrong. Setting something like “volume = Math.pow(time, 4)” is a more 
accurate approximation. This should not take you more than a minute to fix.

This interesting article explains the problem: 
http://www.dr-lex.be/info-stuff/volumecontrols.html

What steps will reproduce the problem?
- Set the alarm to fade from 0% to 100% over 30 seconds.

What is the expected output?
- I expect it to sound half as loud after 15 seconds and a quarter as loud 
after 7.5 seconds
(1.7% after 0.5 seconds)
(3.3% after 1 second)
(6.7% after 2 seconds)
(25.0% after 7.5 seconds)
(50.0% after 15 seconds)

What do you see instead?
- The alarm sounds way too loud already after 2 seconds, and then just slightly 
increases for the remaining 29 seconds.
(35.9% after 0.5 seconds)
(42.7% after 1 second)
(50.8% after 2 seconds)
(70.7% after 7.5 seconds)
(84.1% after 15 seconds)

What version of the product are you using? On what operating system?
- 1.9 from F-droid on Android 2.2

Original issue reported on code.google.com by fred...@portstrom.com on 7 Jan 2014 at 7:08

GoogleCodeExporter commented 8 years ago
Yup, most certainly an issue.
Fred posted an excellent description of the issue and a great link for 
understanding what's going on.  However, if you don't care to read up on the 
math and just want to know what code to change, I did a custom build with the 
following change:

NotificationService.java (line 396): 
"MediaSingleton.INSTANCE.setVolume(start*start*start*start);"

That's it.  No other changes are needed.  Works like a charm!  You could use 
Math.pow() if you want as well.  Makes my mornings so much nicer.

This is also a duplicate of issue 41, so you can close both with this super 
simple change.  :-)

Thanks for making a great app.

Original comment by jrdaw...@gmail.com on 26 Jan 2014 at 3:31

GoogleCodeExporter commented 8 years ago
[deleted comment]
GoogleCodeExporter commented 8 years ago
[deleted comment]
GoogleCodeExporter commented 8 years ago
Thanks jrdaw. I tried your modified line of code. It works very well.

As I tried it I found one more thing to change. The article I previously linked 
to describes perception of sound as logarithmic, and points out that there is 
no zero on a logarithmic scale, and that a minimum level must be set according 
to the dynamic range of the sound system in use. As there rarely is a 
specification of the sound system, the article suggests cheating by using the 
simplified formula pow(volume, 4), which does have an absolute zero point.

When I tried there was nothing but noise when the volume is below 12.5%. I 
believe this applies to all Android devices, as this is limited by the 16 bit 
sample size of the audio. This means that it's not good to rely on the zero 
point of the simplified formula. I recommend that the default setting of the 
start fade volume is set to 12% instead of 0% as it is currently.

I seem to have missed the duplicate issue despite looking through all issues 
before making this one.

Summary of suggested changes:
- Change NotificationService.java line 396 to: 
MediaSingleton.INSTANCE.setVolume(start * start * start * start);
- Change AlarmSettings.java line 69 to: volumeStartPercent = 12;

Original comment by fred...@portstrom.com on 29 Jan 2014 at 5:14

GoogleCodeExporter commented 8 years ago
This post is long and you might not care to read it all... see the "Summary" at 
the bottom for the short version.

Ok, I've spent some more time looking into this, and while in many ways I'm 
just more confused, I do have what I think is a better answer.

First off, I spent a while looking into the 16 bit thing (which is correct) and 
trying to figure out how a DAC (Digital to Analog Converter) works to see what 
is happening in the phone.  Learned a lot, not entirely sure how much of it was 
useful.

Everyone agrees, humans perceive sound logarithmically, which means an 
equivalent variation in input levels at low volumes (e.g. moving the dial by 
10% on a linear scale) seems like large volume change versus the same variation 
at high volumes.  How to deal with that seems to be not so simple.

I found tons of people talking about this issue online, and tons of different 
formulas people use to address this issue.  The original linked article 
suggests volume=e^(6.908*x) / 1000, or it's close approximation, x^4.  As Fred 
pointed out, this particular function has problems at the low end of the scale, 
where anything under 12% volume doesn't even register as sound.

The reason for there being so many different formulas is not exactly clear, 
though my guess is that perceived sound volume is somewhat relative (everyone 
hears a bit differently), and what people expect of a volume slider also 
varies.  Some functions provide better control at high volumes, some better 
control at low volumes.  However, this is just my personal speculation.

Personally, I imagine a slider having equal control at all volumes.  What does 
that mean exactly, I'm not sure.  However, I would like to feel that adjusting 
the slider by 5% anywhere (either at low or high volumes) feel like a slight 
and equivalent increase in volume.  

After watching some Khan Academy videos to refresh my memory on logarithms and 
log-scale plotting, I calculated my own formula (which turns out to be one of 
the ones I found elsewhere).  The formula is:  (10^x -1 )/9, and doesn't 
require taking into account things like the dynamic range of the device, but 
when I listen to it, seems to be exactly what I would expect a volume control 
to sound like.  I will attempt to explain how I came about this formula:

If you imagine a piece of horizontal log paper, with 10 vertical lines 
representing the numbers 1 to 10 (0 never exists on log paper), you first start 
with a large gap between line 1 and 2, then progressively smaller gaps until 
you have lines 9 and 10 almost touching.  This is a log scale.  I can take the 
line labeled 1 to represent my lowest volume (i.e. no sound), and 10 to 
represent maximum volume.  To find out what 50% volume means (i.e. half-way 
between the lines 1 (which is 10^0) and 10 (which is 10^1)), I can use the 
formula 10^0.5, which gives me 3.16.  So, halfway between the 1 and the 10 is 
slightly right of 3.  Cool!  So I can get the location of any volume percentage 
x, by using the formula 10^x.  This will give me values between 1 and 10.  
Unfortunately, the Android MediaPlayer requires values between 0 and 1, so we 
need to translate our formula to produce the appropriate values.  To do this, 
we first subtract 1 (i.e. 10^x - 1), giving us values that range from 0 to 9.  
Then we divide by 9, giving us values from 0 to 1.  The final formula is  (10^x 
-1)/9.

When I use this formula, I hear sound immediately (i.e. 1% volume is very low, 
but still audible), and the increase in volume is noticeable and (to my ears at 
least) roughly equal along the entire scale.

This formula requires the Math library, which isn't a big issue here because it 
is already loaded.  Of course, if you wanted, you could generate a polynomial 
using the least-square method to get a very similar function without needing 
the Math library.   Some online web page that computes the least-square gave me 
this function:  0.8x^3 - 0.1x^2 + 0.35x - 0.004.  This approximation is 
ridiculously similar, so can be used in place of the previous function.

SUMMARY of our understanding of this issue so far:
- *Any* logarithmic function for volume level is better than a linear one.  
Using "start*start*start*start" will work just fine.
- I personally feel that "MediaSingleton.INSTANCE.setVolume( (float)( 
(Math.pow(10, start)-1f)/9f ) );" is a nicer function.

Feel free to give them both a try and pick the one you like better.

Original comment by jrdaw...@gmail.com on 30 Jan 2014 at 1:17

GoogleCodeExporter commented 8 years ago
“The formula is:  (10^x -1 )/9, and doesn't require taking into account 
things like the dynamic range of the device”

Not true. The requirement to take into account the dynamic range cannot be 
magically eliminated. You have arbitrarily selected a dynamic range from 10^0 
to 10^1, and you could just as well have started at 10^-1 or ended at 10^2 to 
get a higher dynamic range.

The formula 10 ^ x in the range 0 < x < 1 matches the approximation x ^ 4 in 
the range .752781 < x < 1. It's easier to see that .752781 is an arbitrary 
chosen number than the equal 10^0.

The difference this formula does is that the volume increases quicker in the 
beginning. I've tried it. It causes the volume to increase too quickly in the 
beginning for me. I still think that the best is to start at 12.5% volume and 
call setVolume(start * start * start * start).

Original comment by fred...@portstrom.com on 30 Jan 2014 at 12:55

GoogleCodeExporter commented 8 years ago
Ahh, I stand partly corrected...  Probably shouldn't post late at night.

The originally linked article uses the formula e^6.908x/1000.  These are 
seemingly odd numbers, so I converted from base 'e' to base 10.  The equivalent 
formula is then 10^3x / 1000, which in my mind is much easier to grasp.  This 
represents, using my log paper analogy, the space between 1 and 1000, whereas I 
used the space between 1 and 10.  My version of this same curve would be: 
10^3x-1/999.  These two formulas are essentially identical (the slight 
difference being I make my formula go through (0,0) while the other doesn't but 
gets really close).  

Going back to the article shows that the 1-1000 range covers 60dB, where the 
1-10 range covers 20dB.  While I would be willing to agree my phone sucks, I 
can't imagine is sucks so bad I only have a 20dB range, yet that formula does 
seem the most equal to me. 

A generic version of this formula is:  (10^ax - 1)/(10^a -1).  In my case, I 
picked a=1, Fred likes a=3.  It would appear that which value of 'a' you use 
depends on the quality of your phone and your personal taste in what you think 
sounds right.

You could, if you cared to, allow customization of the range by letting the 
user select a value of 'a'.  For an alarm clock, that might be overkill.  

Even more overkill would be allowing the user to pick a 'floor'.  This would be 
the lowest value that produces sound on their phone (in Fred's case of 12.5% in 
the x^4 formula is 0.00025).  You could then adjust the formula to have both 
the slope 'a' and the floor 'f', so that sound starts being produced 
immediately yet increases comfortably per the device's range and user's 
preference.  This formula would be:  (10^ax - 1)(1 - f)/(10^a - 1) + f.

Finally, after having woken up this morning to my alarm with slope a=1, I agree 
it goes up in volume too fast.  Now, if I were to create a volume slider for a 
music player, I'd pick a=1, but for waking up, it seems I actually prefer a 
non-linear increase in volume, so the quite music fades in more gradually, only 
getting loud towards the end to make sure I'm awake.

So, what am I'm going to do...  Since I've already got the guts of this app 
open and compiling it is easy, I'm using my uber-overkill formula with a floor 
of 0.0005 and a slope of 3, which I've determined works well on my phone and 
for my preference in volume increase.  After that, I'm going to stop worrying 
about it and get on with my life (though it was both informative and fun doing 
this, I don't plan on doing this forever, lol).

What I recommend as a code change in the app...  Stick to 
"start*start*start*start".  Why?  It works, it's easy, and users can pick a 
starting volume using the existing interface if they're not happy with the 
little bit of silence at the start.

Cheers,

Original comment by jrdaw...@gmail.com on 30 Jan 2014 at 5:29