Logo Search packages:      
Sourcecode: earcandy version File versions  Download package

ear_candy.py

#!/usr/bin/env python

# Ear Candy - Pulseaduio sound managment tool
# Copyright (C) 2008 Jason Taylor
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import gtk
import wnck
import gobject
import time
import datetime
import re
import os
import copy
import sys
import gconf
import time
import shutil
import pynotify
from xml.dom.minidom import *

from window.WindowWatcher import WindowWatcher
from pulseaudio.PulseAudio import PulseAudio 
from Client import Client
from Sink import Sink
from Threads import threaded
from TrayIcon import EarCandyStatusIcon
from EarCandyPrefs import EarCandayPref 
from VolumeSlider import EarCandyVolumeSlider 
from EarCandyDBus import EarCandyDBusClient

# Turn on gtk threading
gtk.gdk.threads_init()

def find_program_file(path):
    """Finds a program file, for example, a png included with the program.
    First looks for it in files/ under the parent directory of the parent directory
    of ear_candy.py
    Then looks for it in /usr/share/earcandy
    Returns the path of the file"""
    if os.path.exists(os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "files",path)):
        return os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "files",path)
    else:
        return os.path.join(sys.prefix, "share/earcandy", path)

gtk.window_set_default_icon_from_file(find_program_file("earsLabel.png"))

class EarCandy():
    def save(self):
        path = os.path.dirname(self.config_file)
        if not os.path.exists( path ):
            os.makedirs(path)

        # New document
        doc = Document()
        ec = doc.createElement("earcandy")
        doc.appendChild(ec)
      
        rules = doc.createElement("rules")
        ec.appendChild(rules)
        skip = copy.copy(self.ignore)
        for client in self.pa_clients.values():
            if not client.name in skip and client.category:
                # Creates user element
                el = doc.createElement("rule")
                rules.appendChild(el)
                client.to_xml(el)
                skip.append(client.name)
        

        doc.documentElement.setAttribute("fade_timer_speed", str(self.fade_timer_speed))
        doc.documentElement.setAttribute("mute_level", str(self.mute_level))
        doc.documentElement.setAttribute("tray_visible", str(self.tray.get_visible()))
        doc.documentElement.setAttribute("managed_output_name", str(self.managed_output_name))
        doc.documentElement.setAttribute("follow_new_outputs", str(self.follow_new_outputs))
        doc.documentElement.setAttribute("version", str(self.version))

        # Record outputs
        outputs = doc.createElement("outputs")
        for output in self.pa_outputs.values():
            # Creates user element
            el = doc.createElement("output")
            el.setAttribute("name", output)
            outputs.appendChild(el)
        ec.appendChild(outputs)


        fp = open(self.config_file,"w")
        doc.writexml(fp, "    ", "", "\n", "UTF-8")
        fp.close()

    def load(self):
        settings_version = 0
        xml = None
        doc = None

        # Load the defaults
        f = open(self.default_config_file, "r")
        xml = f.read()
        f.close()
        default_doc = parseString(xml)
        default_version = float(default_doc.documentElement.getAttribute("version"))

        # Check for user settings
        if os.path.exists( self.config_file ):
            # Load XML
            try:
                f = open(self.config_file, "r")
                xml = f.read()
                f.close()
                doc = parseString(xml)
                if doc.documentElement.hasAttribute("version"):
                    settings_version = float(doc.documentElement.getAttribute("version"))
            except:
                pass

        # Load defaults ?
        if not doc or default_version > settings_version:
            doc = default_doc
            
        # Load client rules
        for el in doc.getElementsByTagName("rule"):
            client = Client(self, "")
            client.from_xml(el)
            if client.category:
                print "Loaded client rules :", client.description
            self.pa_clients[client.name] = client

        self.fade_timer_speed = float(doc.documentElement.getAttribute("fade_timer_speed"))
        #self.mute_level = float(doc.documentElement.getAttribute("mute_level"))
        if doc.documentElement.hasAttribute("tray_visible"):
            self.tray.set_visible( doc.documentElement.getAttribute("tray_visible") == "True" )
        if doc.documentElement.hasAttribute("managed_output_name"):
            self.managed_output_name = doc.documentElement.getAttribute("managed_output_name")
        if doc.documentElement.hasAttribute("follow_new_outputs"):
            self.follow_new_outputs = doc.documentElement.getAttribute("follow_new_outputs") == "True"

    def __init__(self):
        self.version = 0.5
        self.active = True
        self.display = {"": "[ unknown ]", "phone" : "Phone (VoIP)", "video" : "Video Player", "music" : "Music Player", "event" : "Notification" }
        self.ignore = ["EsounD client (UNIX socket client)", "ear-candy", "Native client (UNIX socket client)", "PulseAudio Volume Control"]
        self.config_file = os.path.expanduser("~/.config/Ear Candy/settings.xml")     
        self.default_config_file = find_program_file("settings.xml")

        self.pa_clients = {}        # clients by name
        self.pa_clients_by_id = {}  # clients by id
        self.pa_sinks = {}
        self.pa_outputs = {}
        self.pa_output_descriptions = {}
        self.client_with_focus = None # client that has a focused window
        self.last_application = None
        self.primary_client = None # Client that gets foreground sound regardless of window focus
        self.managed_output_name = ""
        self.current_source_name = ""
        self.reset_all = False

        self.pref = None

        self.fade_timer_speed = 0.1 
        self.mute_level = 20
        self.follow_new_outputs = True

        self.priority_stack = { "" : [], "phone" : [], "video" : [], "music" : [] }      

        pass

    def run(self):
        self.ecb = EarCandyDBusClient(self)

        if self.ecb.is_running():
            print "Ear candy already running..."
            self.ecb.show_window()
            sys.exit(0)
        else:

            self.tray = EarCandyStatusIcon(self)
            
            self.slider = EarCandyVolumeSlider(self)

            self.ecb.start_service()
            self.load()
            self.pa = PulseAudio( self.on_new_pa_client, self.on_remove_pa_client, self.on_new_pa_sink, self.on_remove_pa_sink, self.on_new_pa_output, self.on_remove_pa_output, self.on_volume_change, self.pa_volume_meter)
            self.ww = WindowWatcher(self.on_active_window_change)

            self.select_client_thread()
            self.apply_volume_thread()
            
            gtk.main()
            self.exit()

    def show_notification(self, title, body, icon):
        try:
            pynotify.init( "Ear Candy" )
            n = pynotify.Notification(title, body, icon)
            n.show ()
        except:
            print "Unable to show notification"

    def set_active(self, active):
        self.active = active
        if self.slider: self.slider.update_active_status()
        self.tray.set_icon()


    def open_preferances(self):
        if not self.pref:
            self.pref = EarCandayPref(self)
        self.pref.run()

    def close_preferances(self):
        if self.pref:
            self.pref = None

    def get_current_sink_volume(self):
        self.pa.get_sink_info_by_name(self.managed_output_name)

    @threaded
    def apply_volume_thread(self):
        while True:
            time.sleep(self.fade_timer_speed)
            if self.active:
                gobject.idle_add(self.__adjust_volumes)      

    @threaded
    def select_client_thread(self):
        while True:
            time.sleep(0.5)
            if self.active:
                gobject.idle_add(self.__set_primary_client)  

    def __adjust_volumes(self):

        # Always update based on active sinks
        for sink in self.pa_sinks.values():
            if sink.set_volume():
                # set pa volume
                self.pa.set_sink_volume(sink.index, sink.volume, sink.channels)  


    def __set_window_stack(self):
        for client in self.pa_clients.values():
            # self.pref.update_client( client )

            # Select the primary client and order previous clients
            for key in self.priority_stack.keys():
                if client.category == key:
                    # Add client to category
                    if not client in self.priority_stack[key]:
                        if self.client_with_focus == client:
                            self.priority_stack[key].insert(0, client)
                        else:
                            self.priority_stack[key].append(client)

                    # reshuffle if active
                    elif self.client_with_focus == client and not self.priority_stack[key][0] == client:
                        self.priority_stack[key].remove(client)
                        self.priority_stack[key].insert(0, client)

                # If category has changed remove old entry
                elif client in self.priority_stack[key]:
                    self.priority_stack[key].remove(client)

    def __set_primary_client(self):

        # Toggle primary status
        flagFound = False
        for key in self.priority_stack.keys():
            if key:
                for client in self.priority_stack[key]:         

                    if not flagFound and self.active and client.is_active():
                        # Toggle old client to inactive
                        if self.primary_client:
                            self.primary_client.set_primary(False)
                        self.primary_client = client
                        client.set_primary(True)
                        flagFound = True
                    else:
                        client.set_primary(False)
            else:
                # all clients with no category should be treated as active for volume
                for client in self.priority_stack[key]:
                    client.set_primary(True)
                     

    def on_volume_change(self, level):
        self.slider.set_volume( level )
        #print self.managed_output_name, level

    def on_remove_pa_output(self, index):
        # /desktop/gnome/sound/default_mixer_device

        del( self.pa_outputs[index] )
        del( self.pa_output_descriptions[index] )

        # fall back to previous value...
        for value in self.pa_outputs.values():
            self.set_last_output(value)
            return

    def set_last_output(self, name):
        self.managed_output_name = name
        
        # Update gconf key that governs multimedia key controls
        gconf_key =  "/desktop/gnome/sound/default_mixer_device"
        prefix = "pulsemixer:"
        client = gconf.client_get_default()
        value = client.get_string(gconf_key)
        if not value == prefix + name:
            print
            print "== Change gnome default sound device =="
            client.set_string(gconf_key, prefix + name)

    def on_new_pa_output(self, index, output_name, output_description, startup):
        self.pa_outputs[index] = output_name
        self.pa_output_descriptions[index] = output_description
        self.save()
       
        if self.follow_new_outputs and (self.managed_output_name == "" or output_name.lower().count("usb") > 0):
            # Move all streams to the new output ;)
            if not startup: 
                self.show_notification("New sound device detected", "Moving all sound to new device...", "notification-audio-volume-high")
            self.set_last_output(output_name)
            self.move_all_sinks()

    def move_all_sinks(self):
        if self.managed_output_name:
            for sink in self.pa_sinks.values():
                if not sink.client.output:
                    self.pa.move_sink(sink.index, self.managed_output_name)

    def on_new_pa_sink(self, index, name, client_index, volume, sink_index, channels):

        if not self.pa_sinks.has_key(index):
            print 
            print "== pa sink input =="
            print "sink:", index, name
            if self.pa_clients_by_id.has_key(client_index):
                client = self.pa_clients_by_id[client_index]
                print "client:", client.name
                print "client index:", client_index
                print "category:", client.category
                sink = Sink(index, name, volume, client, channels)
                client.sinks[index] = sink
                self.pa_sinks[index] = sink

                # insure the sink input is on the correct output..
                output = None
                if self.follow_new_outputs and self.managed_output_name: 
                    output = self.managed_output_name
                
                if client.output:
                    output = client.output

                if output:               
                    self.pa.move_sink(sink.index, output)

            else:
                return
        else:
            self.pa_sinks[index].volume = volume
        
        if self.pref:
            self.pref.update_client( self.pa_clients_by_id[client_index] )
        self.on_active_window_change( self.last_application, "pa")

    def pa_volume_meter(self, index, level):
        if self.pa_sinks.has_key(index):
            sink = self.pa_sinks[index]
            sink.volume_meter = level
            if(level > sink.client.volume_step): sink.volume_meter_last_non_zero = time.mktime(datetime.datetime.now().timetuple())

    def on_remove_pa_sink(self, index):
        print 
        print "== pa remove sink input =="
        print index
        if self.pa_sinks.has_key(index):
            client = self.pa_sinks[index].client
            # Delete entry in store so that volume dosnt stay low :)
            self.pa.pa_ext_stream_restore_delete( self.pa_sinks[index].name )
            del( client.sinks[index] )
            del( self.pa_sinks[index] )
            if self.pref:
                self.pref.update_client( client )
            self.on_active_window_change( self.last_application, "pa")
        
    def get_unregistered_clients(self):
        clients = []
        # client names to skip from adding to list
        skip = copy.copy(self.ignore)
        count = 0
        for client in self.pa_clients.values():
            if client.category == "" and not client.icon and not client.name in skip:
                clients.append(client)
                skip.append( client.name )
        return clients

    def on_new_pa_client(self, index, name, pid, proplist):
        if not pid: pid = -1
        print 
        print "== pa sink client =="
        print "index:", index
        print "name:", name
        print proplist
        # Link all clients with same name into same object

        if not self.pa_clients.has_key(name):
            client = Client(self, name, int(pid))
            self.pa_clients[name] = client
        else:
            client = self.pa_clients[name]
            client.pid = int(pid)
        self.pa_clients_by_id[index] = client

        # Check windows for a match
        for application in self.ww.applications.values():
            if self.match_client_to_application(client, application, index): break

    def on_remove_pa_client(self, index):
        print "== pa remove client =="
        if self.pa_clients_by_id.has_key(index):
            print self.pa_clients_by_id[index].name
            client = self.pa_clients_by_id[index]

            # remove from by ID list
            del self.pa_clients_by_id[index]

            # Remove from priority array
            if client in self.priority_stack[client.category]:
                self.priority_stack[client.category].remove(client)

            # If not category then totaly delete the entry
            # if not client.category: del self.pa_clients[client.name]

            if self.pref:
                self.pref.update_client( client )
                if not client.is_active():
                    self.pref.remove_client_on_timer( client )

    def on_active_window_change(self, application, state):  #, pid, window_name, x, y, icon, fullscreen
        if application:
            # Need to run this if a client is added or removed...
            if state == "active":
                """print
                print "== active window changed =="
                print "window title:", application.window_name
                print "application:", application.name
                print "pid:", application.pid
                print "gid:", os.getpgid(application.pid)
                print "command:", application.command
                print "category:", application.category"""

            # set active / fullscreen flags
            if self.client_with_focus:
                self.client_with_focus.has_focus = False

            self.client_with_focus = None
            for client in self.pa_clients.values():
                if self.match_client_to_application(client, application, state): break

            self.last_application = application

        self.__set_window_stack()

    def match_client_to_application(self, client, application, index=0):
        if client.test_focus_window(application.pid, application.window_name, application.command, application.name): 

            client.fullscreen = application.fullscreen  
            if not client.icon : client.icon = application.icon
            if not  client.icon_name : client.icon_name = application.icon_name
            if not  client.description :client.description = application.description
            client.matched = True
            #if not state == "open": 
            self.client_with_focus = client

            if not client.category:
                client.category = application.category
            # Balance has no practical use I can think of....
            # Im going to leave it here in case someone thinks of something
            #if client.window_position_fade:
            #    client.balance = ((100 - (application.y / (gtk.gdk.screen_width() /  100))) * 2)  - 100
            #    print "BALANCE", client.balance
            #else:
            #    client.balance = 0
            if self.pref:
                self.pref.update_client( client )
            return True
        return False

    def clean_client_name(self, name):
        name = name.strip()
        alsa_plugin = "ALSA plug-in ["
        if name.startswith(alsa_plugin):
            name = name[len(alsa_plugin): -1]
        return name

    def exit(self):
        self.set_active(False)
        # Reset all volumes
        self.reset_all_volumes()
        sys.exit(0)

    def reset_all_volumes(self, deleteFile=True):
        print ""
        print "Resetting all volume levels..."
        for sink in self.pa_sinks.values():
            self.pa.set_sink_volume(sink.index, [100, 100, 100], sink.channels)  

    def set_auto_start(self, flag):
        filename = "earcandy.desktop"
        path = os.path.expanduser(os.getenv('XDG_CONFIG_HOME', '~/.config/autostart'))
        dest = os.path.join(path, filename)
        src = find_program_file(filename)

        if flag:
            # insure the autostart path exists
            if not os.path.exists(path):
                os.makedirs(path)

            if not os.path.exists( dest ):
                shutil.copyfile(src, dest)
        else:
            if os.path.exists( dest ):
                os.remove( dest )

    def is_auto_start(self):
        filename = "earcandy.desktop"
        dest = os.path.expanduser(os.getenv('XDG_CONFIG_HOME', '~/.config/autostart/' + filename))
        return os.path.exists(dest)

if __name__ == '__main__':

    os.chdir(os.path.dirname(sys.argv[0]))
    
    ec = EarCandy()
    ec.set_auto_start( True )
    ec.run()
    ec.save()
    

Generated by  Doxygen 1.6.0   Back to index