Jonah Begleiter
Jonah Begleiter

Reputation: 3

Is there a better way to get MacOS Volume as an integer value? (Bash)

I'm trying to get an integer value for MacOS's output volume for use in an Übersicht widget, and I have a working (but messy) solution. The issue is that the value is stored as a percentage, rounded to the nearest whole number. I considered using linear regression to get a function that would return a value 1-64, but decimals are... not a thing in bash. I also need to do this in one line, which contributes to the overall messiness in the code. In any case, this is my current code:

vol=$(osascript -e 'get volume settings' | cut -f2 -d':' | cut -f1 -d','); [ \"$vol\" = \"2\" ] &&let vol=1; [ \"$vol\" = \"3\" ] &&let vol=2; [ \"$vol\" = \"5\" ] &&let vol=3; [ \"$vol\" = \"6\" ] &&let vol=4; [ \"$vol\" = \"8\" ] &&let vol=5; [ \"$vol\" = \"9\" ] &&let vol=6; [ \"$vol\" = \"11\" ] &&let vol=7; [ \"$vol\" = \"13\" ] &&let vol=8; [ \"$vol\" = \"14\" ] &&let vol=9; [ \"$vol\" = \"16\" ] &&let vol=10; [ \"$vol\" = \"17\" ] &&let vol=11; [ \"$vol\" = \"19\" ] &&let vol=12; [ \"$vol\" = \"20\" ] &&let vol=13; [ \"$vol\" = \"22\" ] &&let vol=14; [ \"$vol\" = \"23\" ] &&let vol=15; [ \"$vol\" = \"25\" ] &&let vol=16; [ \"$vol\" = \"26\" ] &&let vol=17; [ \"$vol\" = \"28\" ] &&let vol=18; [ \"$vol\" = \"30\" ] &&let vol=19; [ \"$vol\" = \"31\" ] &&let vol=20; [ \"$vol\" = \"33\" ] &&let vol=21; [ \"$vol\" = \"34\" ] &&let vol=22; [ \"$vol\" = \"36\" ] &&let vol=23; [ \"$vol\" = \"38\" ] &&let vol=24; [ \"$vol\" = \"39\" ] &&let vol=25; [ \"$vol\" = \"41\" ] &&let vol=26; [ \"$vol\" = \"42\" ] &&let vol=27; [ \"$vol\" = \"44\" ] &&let vol=28; [ \"$vol\" = \"45\" ] &&let vol=29; [ \"$vol\" = \"47\" ] &&let vol=30; [ \"$vol\" = \"48\" ] &&let vol=31; [ \"$vol\" = \"50\" ] &&let vol=32; [ \"$vol\" = \"51\" ] &&let vol=33; [ \"$vol\" = \"54\" ] &&let vol=34; [ \"$vol\" = \"55\" ] &&let vol=35; [ \"$vol\" = \"56\" ] &&let vol=36; [ \"$vol\" = \"58\" ] &&let vol=37; [ \"$vol\" = \"60\" ] &&let vol=38; [ \"$vol\" = \"61\" ] &&let vol=39; [ \"$vol\" = \"62\" ] &&let vol=40; [ \"$vol\" = \"65\" ] &&let vol=41; [ \"$vol\" = \"66\" ] &&let vol=42; [ \"$vol\" = \"67\" ] &&let vol=43; [ \"$vol\" = \"68\" ] &&let vol=44; [ \"$vol\" = \"70\" ] &&let vol=45; [ \"$vol\" = \"72\" ] &&let vol=46; [ \"$vol\" = \"74\" ] &&let vol=47; [ \"$vol\" = \"75\" ] &&let vol=48; [ \"$vol\" = \"76\" ] &&let vol=49; [ \"$vol\" = \"78\" ] &&let vol=50; [ \"$vol\" = \"79\" ] &&let vol=51; [ \"$vol\" = \"81\" ] &&let vol=52; [ \"$vol\" = \"83\" ] &&let vol=53; [ \"$vol\" = \"85\" ] &&let vol=54; [ \"$vol\" = \"86\" ] &&let vol=55; [ \"$vol\" = \"88\" ] &&let vol=56; [ \"$vol\" = \"89\" ] &&let vol=57; [ \"$vol\" = \"91\" ] &&let vol=58; [ \"$vol\" = \"92\" ] &&let vol=59; [ \"$vol\" = \"94\" ] &&let vol=60; [ \"$vol\" = \"95\" ] &&let vol=61; [ \"$vol\" = \"97\" ] &&let vol=62; [ \"$vol\" = \"98\" ] &&let vol=63; [ \"$vol\" = \"100\" ] &&let vol=64; mute=$(osascript -e 'get volume settings' | cut -f2 -d'd' | cut -f2 -d':'); [ \"$mute\" = \"true\" ] &&let vol=0; echo $vol

Essentially I'm just getting the value, converting it to a number 1-64, setting that value to zero if the system is muted, and returning that value. Ideas? I'm very new to bash, so please excuse my distinct lack of basic knowledge!

Upvotes: 0

Views: 158

Answers (2)

CJK
CJK

Reputation: 6112

If all you require, as you said, is the output volume, then you don't need awk. If you don't need mathematical accuracy when it comes to rounding a non-integer decimal to an integer, you can use this:

osascript <<< "(output volume of (get volume settings) * 0.64) as integer"

Notably, this takes 0.5 to 0 and 2.5 to 2, but most other halves behave normally (this is to do with internal floating point errors).

If you wish to preserve accuracy, then you can use this:

osascript <<< "round ((output volume of (get volume settings)) * 0.64) rounding as taught in school"

This will round up or down in the way one would (or should) in a maths exam.

Surround that all with vol=$( ... ) for variable assignment, replacing the three dots with one of the two expressions above.


Muted/Not muted

Using the second of the above two to demonstrate, the way to assign the value 0 to the bash variable vol if the volume is muted is like this:

vol=$(( $(osascript <<< "tell (get volume settings) to ¬
    if not output muted then tell (output volume * 0.64) ¬
    to continue round it rounding as taught in school") ))

I've split the AppleScript over several lines for readability, which you can keep as it is, or remove the continuation characters (¬) and put it on a single line if that's what you require.

This uses bash's arithmetic expansion notation, $(( ... )), inside of which is the call to osascript, which now conditionally returns the value of the output volume if and only if the system volume is not muted.

If the system volume is muted, then the script returns nothing—that is, an empty expression—which, a bash arithmetic expression just so happens to evaluate as 0.

Just if you're interested…

There's a couple of other ways to employ bash arithmetic expressions to achieve a similar effect:

  • You could choose to move the entire assignment inside the arithmetic expansion:

    (( vol=$(osascript <<< "tell (get volume settings) to ¬
        if not output muted then tell (output volume * 0.64) ¬
        to continue round it rounding as taught in school")+0 ))
    

    In this situation, we must add 0 to the expression: this is because one of the possible outcomes of the AppleScript is for it to return nothing, which would lead to a non-sensical mathematical expression, namely (( vol= )). However, (( vol=+0 )) is absolutely fine, and doesn't change the value of the result for when the AppleScript returns a number.

    This syntax form can be useful because whether or not the system volume is muted will be reflected in the return status of this line of bash code, i.e. if the system volume is muted, the return status is non-zero.

  • Another way to evaluate arithmetic expressions is bash is to use let. Since the variable assignment takes place as part of the arithmetic expression itself, it will also require the addition of 0 to ensure a complete and valid calculation in all scenarios:

    let vol=$(osascript <<< "tell (get volume settings) to ¬
        if not output muted then tell (output volume * 0.64) ¬
        to continue round it rounding as taught in school")+0
    

    And, again, the return status will be non-zero if the system volume is muted (note that this doesn't mean the value of vol will be non-zero; it will, in fact, be necessarily zero).

If the return status is not something you'll be utilising, then all three of these forms are essentially the same, with none of them conferring any specific advantages or disadvantages compared to the other two (unless one finds a particular syntax to be more visually jarring).

I've purposely not used bc to evaluate these arithmetic expressions. bc is the principle mathematics program used most often in bash, particularly when doing floating point arithmetic. However, the overarching theme of my solution is in keeping the solution as simple as possible, and not relying on a chain of pipelines into external programs in situations where it's unnecessary—and also less efficient—than taking advantage of what bash is able to do natively.

Upvotes: 0

jeremysprofile
jeremysprofile

Reputation: 11504

TL;DR: awk is king, just use awk:

osascript -e 'get volume settings' | awk '-F:|,' '/true/ { print 0; exit; } { print int($2 / (100 / 64)) }' 

Here's some of the old things I tried before CharlesDuffy showed his power level:

Decimals can be a thing in bash, you just need a tool. As the comments suggested, awk is really good at this sort of thing:

osascript -e 'get volume settings' | awk '-F:|,' '{print int($2 / (100 / 64))}'

Lastly, you can just check if true or false is in the string, and reset volume based on that:

# if we see "true" in the string, we're muted and should set volume to 0
osascript -e 'get volume settings' | grep "true" &>/dev/null && vol=0;

Putting that all together, we get

osascript -e 'get volume settings' | awk '-F:|,' '{print int($2 / (100 / 64))}';
osascript -e 'get volume settings' | grep "true" &>/dev/null && vol=0;
echo "$vol"

You can always shove these together on one line is that's necessary.

You can make it even shorter at the cost of readability, or longer and save the call to osascript so you only call it once.

Upvotes: 1

Related Questions