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