1# This is a native implementation of Ecasound's control interface for Ruby. 2# Copyright (C) 2003 - 2004 Jan Weil <jan.weil@web.de> 3# 4# This library is free software; you can redistribute it and/or 5# modify it under the terms of the GNU Lesser General Public 6# License as published by the Free Software Foundation; either 7# version 2.1 of the License, or (at your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with this program; if not, write to the Free Software 16# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 17# --------------------------------------------------------------------------- 18=begin 19= ruby-ecasound 20 21Example: 22 23require "ecasound" 24eci = Ecasound::ControlInterface.new(ecasound_args) 25ecasound-response = eci.command("iam-command-here") 26... 27 28TODO: 29Is there a chance that the ecasound process gets zombified? 30 31=end 32 33require "timeout" 34require "thread" 35 36class File 37 def self::which(prog, path=ENV['PATH']) 38 path.split(File::PATH_SEPARATOR).each do |dir| 39 f = File::join(dir, prog) 40 if File::executable?(f) && ! File::directory?(f) 41 return f 42 end 43 end 44 end 45end # File 46 47class VersionString < String 48 attr_reader :numbers 49 50 def initialize(str) 51 if str.split(".").length() != 3 52 raise("Versioning scheme must be major.minor.micro") 53 end 54 super(str) 55 @numbers = [] 56 str.split(".").each {|s| @numbers.push(s.to_i())} 57 end 58 59 def <=>(other) 60 numbers.each_index do |i| 61 if numbers[i] < other.numbers[i] 62 return -1 63 elsif numbers[i] > other.numbers[i] 64 return 1 65 elsif i < 2 66 next 67 end 68 end 69 return 0 70 end 71end # VersionString 72 73module Ecasound 74 75REQUIRED_VERSION = VersionString.new("2.2.0") 76TIMEOUT = 15 # seconds before sync is called 'lost' 77 78class EcasoundError < RuntimeError; end 79class EcasoundCommandError < EcasoundError 80 attr_accessor :command, :error 81 def initialize(command, error) 82 @command = command 83 @error = error 84 end 85end 86 87class ControlInterface 88 @@ecasound = ENV['ECASOUND'] || File::which("ecasound") 89 90 if not File::executable?(@@ecasound.to_s) 91 raise("ecasound executable not found") 92 else 93 @@version = VersionString.new(`#{@@ecasound} --version`.split("\n")[0][/\d\.\d\.\d/]) 94 if @@version < REQUIRED_VERSION 95 raise("ecasound version #{REQUIRED_VERSION} or newer required, found: #{@@version}") 96 end 97 end 98 99 def initialize(args = nil) 100 @mutex = Mutex.new() 101 @ecapipe = IO.popen("-", "r+") # fork! 102 103 if @ecapipe.nil? 104 # child 105 $stderr.reopen(open("/dev/null", "w")) 106 exec("#{@@ecasound} #{args.to_s} -c -D -d:256 ") 107 else 108 @ecapipe.sync = true 109 # parent 110 command("int-output-mode-wellformed") 111 end 112 end 113 114 def cleanup() 115 @ecapipe.close() 116 end 117 118 def command(cmd) 119 @mutex.synchronize do 120 cmd.strip!() 121 #puts "command: #{cmd}" 122 123 @ecapipe.write(cmd + "\n") 124 125 # ugly hack but the process gets stuck otherwise -kvehmanen 126 if cmd == "quit" 127 return nil 128 end 129 130 response = "" 131 begin 132 # TimeoutError is raised unless response is complete 133 Timeout.timeout(TIMEOUT) do 134 loop do 135 response += read() 136 break if response =~ /256 ([0-9]{1,5}) (\-|i|li|f|s|S|e)\r\n(.*)\r\n\r\n/m 137 end 138 end 139 rescue TimeoutError 140 raise(EcasoundError, "lost synchronisation to ecasound subprocess\nlast command was: '#{cmd}'") 141 end 142 143 content = $3[0, $1.to_i()] 144 145 #puts "type: '#{$2}'" 146 #puts "length: #{$1}" 147 #puts "content: #{content}" 148 149 case $2 150 when "e" 151 raise(EcasoundCommandError.new(cmd, content)) 152 when "-" 153 return nil 154 when "s" 155 return content 156 when "S" 157 return content.split(",") 158 when "f" 159 return content.to_f() 160 when "i", "li" 161 return content.to_i() 162 else 163 raise(EcasoundError, "parsing of ecasound's output produced an unknown return type") 164 end 165 end 166 end 167 168 private 169 170 def read() 171 buffer = "" 172 while select([@ecapipe], nil, nil, 0) 173 buffer += @ecapipe.read(1) || "" 174 end 175 return buffer 176 end 177end # ControlInterface 178 179end # Ecasound:: 180