Skip to content

Instantly share code, notes, and snippets.

@uriel1998
Forked from jaspervdj/volume.rb
Created February 10, 2012 17:55
Show Gist options
  • Save uriel1998/1791270 to your computer and use it in GitHub Desktop.
Save uriel1998/1791270 to your computer and use it in GitHub Desktop.
Set PulseAudio volume, mute, unmute, and change default sink (and automagically switch running audio streams) from the commandline
#!/usr/bin/ruby
#
# Moved to: https://github.com/uriel1998/volumerb
#
# This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
# Unported License. To view a copy of this license, visit
# http://creativecommons.org/licenses/by-sa/3.0/.
#
# Forked/derived from original by Jasper Van der Jeugt (jaspervdj);
# large chunks rewritten by Steven Saus (uriel1998)
# This is designed to set your pulseaudio volume settings, muting, and
# default sink from the command line. It will even switch currently
# running streams to the new sink when you switch it!
# Usage will be largely for folks who are using keybinds for volume, etc.
# Originally wrote this to work with ALSA cards - that's what I have.
# (see line 46) but the code at line 45 *should* work for anything
# that's set up as a device in PulseAudio. This is written to work
# with the version of PulseAudio I have on my system - 0.9.22.
# By perusing the online PulseAudio documentation, it looks like this
# *should* still work just fine for later versions. Please ping me one
# way or another so I can properly document/fix it...
# To do: -Set toggle (like the q toggle) so that actions only impact
# current default sink
# -Add in toggle so simple output available for notify-osd
# 20121114:uriel1998: Added introduction/description above
# 20121114:uriel1998: Standardized indentations
# 20121114:uriel1998: Rewrote if/else statements into case statements
# 20121114:uriel1998: Added usage instructions when no arguments given
# 20121114:uriel1998: Added quiet option
# 20121114:uriel1998: Added explicit setting of volume
# 20121114:uriel1998: Added explicit muting/unmuting instead of just
# toggling mute
# 20121114:uriel1998: Formatted output to be somewhat more human-
# readable, including padding & volume percentages
# 20121114:uriel1998: Added function to allow changing default sink
# 20121114:uriel1998: Added code to switch playing streams to new sink.
# 20121114:uriel1998: Rewrote array; arranged by sink id instead of name
# 20120210:uriel1998: Changed bit of code toggling mute
# 20120210:uriel1998: Added output, so it could be piped to a notify-osd
# or somesuch if desired. (superceded 20121114)
# 20120210:uriel1998: Original fork from https://gist.github.com/814634
# Function to test if argument is number, from
#http://stackoverflow.com/questions/5661466/test-if-string-is-a-number-in-ruby-on-rails
class String
def is_number?
true if Float(self) rescue false
end
end
# Pulseaudio volume control
class Pulse
attr_reader :volumes, :mutes
# Constructor
def initialize
dump = `pacmd dump`.lines
@volumes = {}
@mutes = {}
@names = {}
@id = {}
$longname = 0
dump.each do |line|
args = line.split
#We are using the id to actually enumerate this bloody array.
if !args[2].nil? and args[2].include? "device_id" then # this should work for any recognized device
#if args[1] == "module-alsa-card" then # this works if you only have ALSA cards.
s1 = args[2].sub("device_id=\"","")
s2 = s1.sub("\"","")
number = s2.to_i
@id[number] = number
crapstring = args[3].sub("name=\"","")
@names[number] = crapstring.sub("\"","")
if @names[number].length > $longname
$longname = @names[number].length
end
else
if @names.keys.length > 0 # we already have something in the array
@names.keys.each do |sink|
if !args[1].nil? and args[1].include? @names[sink] # and if it's a sink we recognize AND not nil...
result = case args[0]
when "set-default-sink" then $defaultsink = args[1].sub("alsa_output.","") # static variable
when "set-sink-volume" then @volumes[@id[sink]] = args[2].hex
when "set-sink-mute" then @mutes[@id[sink]] = args[2]
end
end
end
end
end
end
# Adjust the volume with the given increment for every sink
def volume_set_relative(increment)
@volumes.keys.each do |sink|
volume = @volumes[sink] + increment
volume = [[0, volume].max, 0x10000].min
@volumes[sink] = volume
`pacmd set-sink-volume #{sink} #{"0x%x" % volume}`
end
end
# Adjust the volume with the given increment for every sink
def volume_set_absolute(setvol)
if setvol < 100.1
@volumes.keys.each do |sink|
volume = setvol * 65536 / 100
@volumes[sink] = volume
`pacmd set-sink-volume #{sink} #{"0x%x" % volume}`
end
end
end
# Toggle the mute setting for every sink
def mute_toggle
@mutes.keys.each do |sink|
if @mutes[sink] == "yes"
`pacmd set-sink-mute #{sink} no`
else
`pacmd set-sink-mute #{sink} yes`
end
end
end
def mute(setting)
@mutes.keys.each do |sink|
`pacmd set-sink-mute #{sink} #{setting}`
end
end
# give me that sweet percentage value.
def percentage(vol)
return vol * 100 / 65536
end
def setdefault
@names.keys.each do |sink|
if $defaultsink.include? @names[sink]
puts "#{@id[sink]}. #{@names[sink]} *"
else
puts "#{@id[sink]}. #{@names[sink]}"
end
end
puts "Which sink shall be set as default (enter the number)"
scratch = STDIN.gets.chomp
newdefault = scratch.to_i
if @id.include? newdefault
`pacmd set-default-sink #{newdefault}`
# Now to move all current playing stuff to the new sink....
dump2 = `pacmd list-sink-inputs`.lines
@inputs = {}
counter = 0
dump2.each do |line|
args = line.split
if args[0] == "index:" # We need to find the item index for each playing stream
@inputs[counter] = args[1]
counter += 1
end
end
# And now to shift them all to the new sink.
count2 = 0
while count2 < counter
`pacmd move-sink-input #{@inputs[count2]} #{newdefault}`
count2 += 1
end
else
puts "That input index does not exist. You silly person."
end
end
# so we can have nice things.
def padstring(strlen)
dyncounter = 0
counter = $longname.to_i - strlen.to_i
padder = " "
while dyncounter < counter do
padder << " "
dyncounter += 1
end
return padder
end
# Report out settings for each sink.
def status
# needed to get new values
initialize
puts "##Current status##########################################"
puts "ID Sink Name#{padstring(11)} Mute Vol Default"
@id.keys.each do |sink|
# making volume into a percentage for humans
# Not sure why I have to pass to a subprocess to make it do, but...
volpercent = percentage(@volumes[sink])
if $defaultsink.include? @names[sink]
puts "#{@id[sink]}. #{@names[sink]}#{padstring(@names[sink].length)} #{@mutes[sink]} #{volpercent}% *"
else
puts "#{@id[sink]}. #{@names[sink]}#{padstring(@names[sink].length)} #{@mutes[sink]} #{volpercent}%"
end
end
puts "##########################################################"
end
end
# Control code
p = Pulse.new
unless ARGV.length > 0
puts "\nUsage: ruby volume.rb [0-100|up|down|toggle|mute|unmute|default] [q]\n[0-100] - set percentage of max volume for all sinks\nup|down - Changes volume on all sinks\ntoggle|mute|unmute - Sets mute on all sinks\ndefault - Select default sink from commandline\nq - quiet; no status output\n"
else
if ARGV.first.is_number?
absvolume = ARGV.first.to_i
p.volume_set_absolute(absvolume)
else
result = case ARGV.first
when "up" then p.volume_set_relative 0x1000
when "down" then p.volume_set_relative -0x1000
when "toggle" then p.mute_toggle
when "mute" then p.mute("yes")
when "unmute" then p.mute("no")
when "default" then p.setdefault
# status not needed; it's included
end
end
end
# Always give us the results.
if !ARGV.include? "q"
p.status
end
end
@andrew-aladev
Copy link

thanks. your script is epic. but use volume_set_relative +-0x300 it will change volume more softly

@andrew-aladev
Copy link

0x200 rulez for asus xonar u1

@andrew-aladev
Copy link

I will report pulseaudio-utils maintainer to include this (or something similar) as pulse-mixer

@uriel1998
Copy link
Author

Please make sure to credit jaspervdj; he did most of the hard work. I just tweaked it a bit. :)

And yeah, I do like the keyboard shortcuts to be pretty bit volume jumps. I usually use them for "OH CRAP THE PHONE IS RINGING" sorts of things. :)

@Squelsh
Copy link

Squelsh commented Mar 4, 2013

Great script. But:
There is a little bug, that swaps the volumes between different sinks when they do not have the same levels at program start. But as my ruby knowledge is miserable, I can not fix it ):

CmdLine Output that shows the prob of alternating the devices:

hermann@saab:~$ ./pa_helper.ruby up
##Current status##########################################
ID Sink Name       Mute Vol Default
0. pci-0000_00_1b.0  no 51% *
1. pci-0000_01_00.1  no 28%
##########################################################
hermann@saab:~$ ./pa_helper.ruby up
##Current status##########################################
ID Sink Name       Mute Vol Default
0. pci-0000_00_1b.0  no 28% *
1. pci-0000_01_00.1  no 52%
##########################################################
hermann@saab:~$ ./pa_helper.ruby up
##Current status##########################################
ID Sink Name       Mute Vol Default
0. pci-0000_00_1b.0  no 53% *
1. pci-0000_01_00.1  no 29%
##########################################################
hermann@saab:~$ ./pa_helper.ruby up
##Current status##########################################
ID Sink Name       Mute Vol Default
0. pci-0000_00_1b.0  no 30% *
1. pci-0000_01_00.1  no 54%
##########################################################
hermann@saab:~$ ./pa_helper.ruby up
##Current status##########################################
ID Sink Name       Mute Vol Default
0. pci-0000_00_1b.0  no 54% *
1. pci-0000_01_00.1  no 31%
##########################################################

@asds
Copy link

asds commented May 7, 2013

Thanks for the script. Since (K)ubuntu upgrade to 13.04 it isn't working properly anymore for me. I found that the reason for this is that the pacmd dump gives the wrong device index number out. pacmd list-sinks gives the right sink index number though. I don't know how to do the enumration with pacmd list-sinks, because the syntax in pacmd list-sinks is different. It's not like in pacmd dump device_id="1", but it is: index: 1
Also the name of the device is not name="....", but name: ..... in pacmd list-sinks.
My workaround is to delete the if condition from 151 to 173, not the content, but only the "if" (151), "else" (171) and "end" (173).
Then the script works for me again, since I can give the script the right index of my sink, else the script would tell me that that sink number doesn't exist (and that I'd be a silly person ^^).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment