plllayer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,19 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ plllayer (0.0.1)
5
+ bundler
6
+ open4
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ open4 (1.3.0)
12
+ rake (10.0.2)
13
+
14
+ PLATFORMS
15
+ ruby
16
+
17
+ DEPENDENCIES
18
+ plllayer!
19
+ rake
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2012 Jeremy Ruten
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,110 @@
1
+ class Plllayer
2
+ # A SinglePlayer takes care of playing a single track, and controlling the
3
+ # playback with commands like pause, resume, seek, and so on. It probably
4
+ # starts an external audio player process to do this job. This class is an
5
+ # interface that is to be implemented for different audio players. Then the
6
+ # user can choose which SinglePlayer to use based on what audio players they
7
+ # have installed.
8
+ #
9
+ # All methods that perform an action should return false if the action isn't
10
+ # applicable, and return a truthy value otherwise.
11
+ class SinglePlayer
12
+ # Returns true if a track is currently loaded, i.e. either playing or
13
+ # paused.
14
+ def playing?
15
+ raise NotImplementedError
16
+ end
17
+
18
+ # Get the current track path which was passed to the play method.
19
+ def track_path
20
+ raise NotImplementedError
21
+ end
22
+
23
+ # Begin playing a track. The track_path should be a String representing a
24
+ # path to an audio file. The &on_end callback should be called when the track
25
+ # is finished playing.
26
+ def play(track_path, &on_end)
27
+ raise NotImplementedError
28
+ end
29
+
30
+ # Stop playback.
31
+ def stop
32
+ raise NotImplementedError
33
+ end
34
+
35
+ # Pauses playback.
36
+ def pause
37
+ raise NotImplementedError
38
+ end
39
+
40
+ # Resumes playback.
41
+ def resume
42
+ raise NotImplementedError
43
+ end
44
+
45
+ # Seek to a particular position in the track. Different types can be
46
+ # supported, such as absolute, relative, or percent. All times are specified
47
+ # in milliseconds. A NotImplementedError should be raised when a certain type
48
+ # isn't supported.
49
+ def seek(where, type = :absolute)
50
+ case type
51
+ when :absolute
52
+ raise NotImplementedError
53
+ when :relative
54
+ raise NotImplementedError
55
+ when :percent
56
+ raise NotImplementedError
57
+ else
58
+ raise NotImplementedError
59
+ end
60
+ end
61
+
62
+ # Gets the current playback speed. The speed is a multiplier. For example,
63
+ # double speed is 2 and half-speed is 0.5. Normal speed is 1.
64
+ def speed
65
+ raise NotImplementedError
66
+ end
67
+
68
+ # Sets the playback speed. The speed is a multiplier. For example, for
69
+ # double speed you'd set it to 2 and for half-speed you'd set it to 0.5. And
70
+ # for normal speed: 1.
71
+ def speed=(new_speed)
72
+ raise NotImplementedError
73
+ end
74
+
75
+ # Returns true if audio is muted.
76
+ def muted?
77
+ raise NotImplementedError
78
+ end
79
+
80
+ # Mutes the audio player.
81
+ def mute
82
+ raise NotImplementedError
83
+ end
84
+
85
+ # Unmutes the audio player.
86
+ def unmute
87
+ raise NotImplementedError
88
+ end
89
+
90
+ # Get the current volume as a percentage.
91
+ def volume
92
+ raise NotImplementedError
93
+ end
94
+
95
+ # Set the volume as a percentage. The player is automatically unmuted.
96
+ def volume=(new_volume)
97
+ raise NotImplementedError
98
+ end
99
+
100
+ # Returns the current time into the song, in milliseconds.
101
+ def position
102
+ raise NotImplementedError
103
+ end
104
+
105
+ # Returns the length of the current track, in milliseconds.
106
+ def track_length
107
+ raise NotImplementedError
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,218 @@
1
+ class Plllayer
2
+ module SinglePlayers
3
+ # This is the SinglePlayer implementation for mplayer. It requires mplayer
4
+ # to be in your $PATH. It uses open4 to start up and communicate with the
5
+ # mplayer process, and mplayer's slave protocol to issue commands to mplayer.
6
+ class MPlayer < Plllayer::SinglePlayer
7
+ attr_reader :track_path
8
+
9
+ # Create a new MPlayer. The mplayer process is only started when the #play
10
+ # method is called to start playing a song. The process quits when the
11
+ # song ends. Even though a new process is started for each song, the
12
+ # MPlayer object keeps track of the volume, speed, and mute properties and
13
+ # sets these properties when a new song is played.
14
+ def initialize
15
+ @paused = false
16
+ @muted = false
17
+ @volume = 50
18
+ @speed = 1.0
19
+ @track_path = nil
20
+ end
21
+
22
+ def playing?
23
+ not @track_path.nil?
24
+ end
25
+
26
+ def play(track_path, &on_end)
27
+ # Make sure we're only playing one song at any one time.
28
+ _quit
29
+
30
+ if File.exists? track_path
31
+ # Run the mplayer process in slave mode, passing it the location of
32
+ # the track's audio file.
33
+ cmd = ["mplayer", "-slave", "-quiet", track_path]
34
+ @pid, @stdin, @stdout, @stderr = Open4.popen4(*cmd)
35
+
36
+ # This should skip past mplayer's initial lines of output so we can
37
+ # start reading its replies to our commands.
38
+ until @stdout.gets["playback"]
39
+ end
40
+
41
+ @paused = false
42
+ @track_path = track_path
43
+
44
+ # Persist the previous speed, volume, and mute properties into this
45
+ # process.
46
+ self.speed = @speed
47
+ self.volume = @volume
48
+ mute if @muted
49
+
50
+ # Start a thread that waits for the mplayer process to end, then calls
51
+ # the end of song callback. If the #quit method is called, this thread
52
+ # will be killed if it's still waiting for the process to end.
53
+ @quit_hook_active = false
54
+ @quit_hook = Thread.new do
55
+ Process.wait(@pid)
56
+ @quit_hook_active = true
57
+ @paused = false
58
+ @track_path = nil
59
+ on_end.call
60
+ end
61
+
62
+ true
63
+ else
64
+ false
65
+ end
66
+ end
67
+
68
+ def stop
69
+ _quit
70
+ end
71
+
72
+ def pause
73
+ if not @paused
74
+ @paused = true
75
+ _command "pause"
76
+ else
77
+ false
78
+ end
79
+ end
80
+
81
+ def resume
82
+ if @paused
83
+ @paused = false
84
+ _command "pause"
85
+ else
86
+ false
87
+ end
88
+ end
89
+
90
+ def seek(where, type = :absolute)
91
+ # mplayer talks seconds, not milliseconds.
92
+ seconds = where.to_f / 1_000
93
+ case type
94
+ when :absolute
95
+ _command "seek #{seconds} 2", expect_answer: true
96
+ when :relative
97
+ _command "seek #{seconds} 0", expect_answer: true
98
+ when :percent
99
+ _command "seek #{where} 1", expect_answer: true
100
+ else
101
+ raise NotImplementedError
102
+ end
103
+ end
104
+
105
+ def speed
106
+ @speed
107
+ end
108
+
109
+ def speed=(new_speed)
110
+ @speed = new_speed.to_f
111
+ answer = _command "speed_set #{@speed}", expect_answer: true
112
+ !!answer
113
+ end
114
+
115
+ def muted?
116
+ @muted
117
+ end
118
+
119
+ def mute
120
+ @muted = true
121
+ answer = _command "mute 1", expect_answer: true
122
+ !!answer
123
+ end
124
+
125
+ def unmute
126
+ @muted = false
127
+ answer = _command "mute 0", expect_answer: true
128
+ !!answer
129
+ end
130
+
131
+ def volume
132
+ @volume
133
+ end
134
+
135
+ def volume=(new_volume)
136
+ @muted = false
137
+ @volume = new_volume.to_f
138
+ answer = _command "volume #{@volume} 1", expect_answer: true
139
+ !!answer
140
+ end
141
+
142
+ def position
143
+ answer = _command "get_time_pos", expect_answer: /^ANS_TIME_POSITION=([0-9.]+)$/
144
+ answer ? (answer.to_f * 1000).to_i : false
145
+ end
146
+
147
+ def track_length
148
+ answer = _command "get_time_length", expect_answer: /^ANS_LENGTH=([0-9.]+)$/
149
+ answer ? (answer.to_f * 1000).to_i : false
150
+ end
151
+
152
+ private
153
+
154
+ # Issue a command to mplayer through the slave protocol. False is returned
155
+ # if the process is dead (not playing anything).
156
+ #
157
+ # If the :expect_answer option is set to true, this will wait for a legible
158
+ # answer back from mplayer, and send it as a return value. If :expect_answer
159
+ # is set to a Regexp, the answer mplayer gives back will be matched to that
160
+ # Regexp and the first match will be returned. If there are no matches, nil
161
+ # will be returned.
162
+ def _command(cmd, options = {})
163
+ if _alive? and playing?
164
+ # If the player is paused, prefix the command with "pausing ".
165
+ # Otherwise it unpauses when it runs a command. The only exception to
166
+ # this is when the "pause" command itself is issued.
167
+ if @paused and cmd != "pause"
168
+ cmd = "pausing #{cmd}"
169
+ end
170
+
171
+ # Send the command to mplayer.
172
+ @stdin.puts cmd
173
+
174
+ if options[:expect_answer]
175
+ # Read lines of output from mplayer until we get an actual message.
176
+ answer = "\n"
177
+ while answer == "\n"
178
+ answer = @stdout.gets.sub("\e[A\r\e[K", "")
179
+ answer = "\n" if options[:expect_answer].is_a?(Regexp) && answer !~ options[:expect_answer]
180
+ end
181
+
182
+ if options[:expect_answer].is_a? Regexp
183
+ matches = answer.match(options[:expect_answer])
184
+ answer = matches && matches[1]
185
+ end
186
+
187
+ answer
188
+ else
189
+ true
190
+ end
191
+ else
192
+ false
193
+ end
194
+ end
195
+
196
+ # Quit the mplayer process, stopping playback. The end of song callback
197
+ # will not be called if this method is called.
198
+ def _quit
199
+ if _alive?
200
+ @quit_hook.kill unless @quit_hook_active
201
+ _command "quit"
202
+ @paused = false
203
+ @track_path = nil
204
+ end
205
+ true
206
+ end
207
+
208
+ # Check if the mplayer process is still around.
209
+ def _alive?
210
+ return false if @pid.nil?
211
+ Process.getpgid(@pid)
212
+ true
213
+ rescue Errno::ESRCH
214
+ false
215
+ end
216
+ end
217
+ end
218
+ end
data/lib/plllayer.rb ADDED
@@ -0,0 +1,307 @@
1
+ require "open4"
2
+
3
+ require "plllayer/single_player"
4
+ require "plllayer/single_players/mplayer"
5
+
6
+ class Plllayer
7
+ attr_reader :repeat_mode
8
+
9
+ def initialize(*args)
10
+ options = {}
11
+ options = args.pop if args.last.is_a? Hash
12
+
13
+ @single_player = Plllayer::SinglePlayers::MPlayer.new
14
+ @playlist = []
15
+ append(args.first) unless args.empty?
16
+ @index = nil
17
+ @paused = false
18
+ @playing = false
19
+ @repeat_mode = nil
20
+
21
+ at_exit { stop }
22
+ end
23
+
24
+ def load(playlist = nil)
25
+ clear
26
+ append(playlist)
27
+ end
28
+
29
+ def append(tracks)
30
+ tracks = Array(tracks).dup
31
+ tracks.select! { |track| track.respond_to?(:location) || (track.is_a?(String) && File.exists?(track)) }
32
+ @playlist += tracks
33
+ @playlist.dup
34
+ end
35
+ alias :<< :append
36
+
37
+ def playlist
38
+ @playlist.dup
39
+ end
40
+
41
+ def track
42
+ @index ? @playlist[@index] : nil
43
+ end
44
+
45
+ def track_index
46
+ @index
47
+ end
48
+
49
+ def track_path
50
+ if track.respond_to? :location
51
+ track.location
52
+ else
53
+ track
54
+ end
55
+ end
56
+
57
+ def clear
58
+ stop
59
+ @playlist.clear
60
+ true
61
+ end
62
+
63
+ def paused?
64
+ @paused
65
+ end
66
+
67
+ def playing?
68
+ @playing
69
+ end
70
+
71
+ def play
72
+ unless @playlist.empty?
73
+ @playing = true
74
+ @paused = false
75
+ @index = 0
76
+ play_track
77
+ true
78
+ else
79
+ false
80
+ end
81
+ end
82
+
83
+ def stop
84
+ @single_player.stop
85
+ @track = nil
86
+ @index = nil
87
+ @paused = false
88
+ @playing = false
89
+ true
90
+ end
91
+
92
+ def pause
93
+ if playing?
94
+ @paused = true
95
+ @single_player.pause
96
+ else
97
+ false
98
+ end
99
+ end
100
+
101
+ def resume
102
+ if playing?
103
+ @paused = false
104
+ if @single_player.playing?
105
+ @single_player.resume
106
+ else
107
+ play_track @track
108
+ @playlist_paused = false
109
+ true
110
+ end
111
+ else
112
+ false
113
+ end
114
+ end
115
+
116
+ def repeat(one_or_all_or_off)
117
+ case one_or_all_or_off
118
+ when :one
119
+ @repeat_mode = :one
120
+ when :all
121
+ @repeat_mode = :all
122
+ when :off
123
+ @repeat_mode = nil
124
+ end
125
+ true
126
+ end
127
+
128
+ def restart
129
+ change_track(0)
130
+ end
131
+
132
+ def back(n = 1)
133
+ change_track(-n)
134
+ end
135
+
136
+ def skip(n = 1)
137
+ change_track(n)
138
+ end
139
+
140
+ def seek(where)
141
+ if paused? && !@single_player.playing?
142
+ resume
143
+ pause
144
+ end
145
+
146
+ case where
147
+ when Integer
148
+ @single_player.seek(where, :relative)
149
+ when Range
150
+ seconds = where.begin * 60 + where.end
151
+ @single_player.seek(seconds * 1000, :absolute)
152
+ when String
153
+ @single_player.seek(parse_time(where), :absolute)
154
+ when Hash
155
+ if where[:abs]
156
+ if where[:abs].is_a? Integer
157
+ @single_player.seek(where[:abs], :absolute)
158
+ else
159
+ seek(where[:abs])
160
+ end
161
+ elsif where[:percent]
162
+ @single_player.seek(where[:percent], :percent)
163
+ end
164
+ end
165
+ end
166
+
167
+ def speed
168
+ @single_player.speed || 1.0
169
+ end
170
+
171
+ def speed=(new_speed)
172
+ @single_player.speed = new_speed
173
+ end
174
+
175
+ def mute
176
+ @single_player.mute
177
+ end
178
+
179
+ def unmute
180
+ @single_player.unmute
181
+ end
182
+
183
+ def muted?
184
+ @single_player.muted?
185
+ end
186
+
187
+ def volume
188
+ @single_player.volume
189
+ end
190
+
191
+ def volume=(new_volume)
192
+ @single_player.volume = new_volume
193
+ end
194
+
195
+ def shuffle
196
+ current_track = track
197
+ @playlist.shuffle!
198
+ if playing?
199
+ index = @playlist.index(current_track)
200
+ @playlist[0], @playlist[index] = @playlist[index], @playlist[0]
201
+ @index = 0
202
+ end
203
+ true
204
+ end
205
+
206
+ def sort(&by)
207
+ current_track = track
208
+ @playlist.sort! &by
209
+ if playing?
210
+ @index = @playlist.index(current_track)
211
+ end
212
+ true
213
+ end
214
+
215
+ def position
216
+ @single_player.position || 0
217
+ end
218
+
219
+ def formatted_position(options = {})
220
+ format_time(position, options)
221
+ end
222
+
223
+ def track_length
224
+ if paused? && !@single_player.playing?
225
+ resume
226
+ pause
227
+ end
228
+ @single_player.track_length
229
+ end
230
+
231
+ def formatted_track_length(options = {})
232
+ if length = track_length
233
+ format_time(length, options)
234
+ end
235
+ end
236
+
237
+ private
238
+
239
+ def change_track(by = 1, options = {})
240
+ if playing?
241
+ if options[:auto] && track.respond_to?(:increment_play_count)
242
+ track.increment_play_count
243
+ end
244
+ @index += by
245
+ if @repeat_mode
246
+ case @repeat_mode
247
+ when :one
248
+ @index -= by if options[:auto]
249
+ when :all
250
+ @index %= @playlist.length
251
+ end
252
+ end
253
+ if track && @index >= 0
254
+ @single_player.stop if paused? && @single_player.active?
255
+ play_track if not paused?
256
+ else
257
+ stop
258
+ end
259
+ true
260
+ else
261
+ false
262
+ end
263
+ end
264
+
265
+ def play_track
266
+ @single_player.play(track_path) do
267
+ change_track(1, auto: true)
268
+ ActiveRecord::Base.connection.close if defined?(ActiveRecord)
269
+ end
270
+ @paused = false
271
+ true
272
+ end
273
+
274
+ # Helper method to format a number of milliseconds as a string like
275
+ # "1:03:56.555". The only option is :include_milliseconds, true by default. If
276
+ # false, milliseconds won't be included in the formatted string.
277
+ def format_time(milliseconds, options = {})
278
+ ms = milliseconds % 1000
279
+ seconds = (milliseconds / 1000) % 60
280
+ minutes = (milliseconds / 60000) % 60
281
+ hours = milliseconds / 3600000
282
+
283
+ if ms.zero? || options[:include_milliseconds] == false
284
+ ms_string = ""
285
+ else
286
+ ms_string = ".%03d" % [ms]
287
+ end
288
+
289
+ if hours > 0
290
+ "%d:%02d:%02d%s" % [hours, minutes, seconds, ms_string]
291
+ else
292
+ "%d:%02d%s" % [minutes, seconds, ms_string]
293
+ end
294
+ end
295
+
296
+ # Helper method to parse a string like "1:03:56.555" and return the number of
297
+ # milliseconds that time length represents.
298
+ def parse_time(string)
299
+ parts = string.split(":").map(&:to_f)
300
+ parts = [0] + parts if parts.length == 2
301
+ hours, minutes, seconds = parts
302
+ seconds = hours * 3600 + minutes * 60 + seconds
303
+ milliseconds = seconds * 1000
304
+ milliseconds.to_i
305
+ end
306
+ end
307
+
data/plllayer.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "plllayer"
3
+ s.version = "0.0.1"
4
+ s.date = "2012-12-06"
5
+ s.summary = "An audio playback library for Ruby."
6
+ s.description = "plllayer is an audio playback library for Ruby. It is a Ruby interface to some external media player, such as mplayer."
7
+ s.author = "Jeremy Ruten"
8
+ s.email = "jeremy.ruten@gmail.com"
9
+ s.homepage = "http://github.com/yjerem/plllayer"
10
+ s.license = "MIT"
11
+ s.required_ruby_version = ">= 1.9.2"
12
+
13
+ s.files = ["Gemfile", "Gemfile.lock", "LICENSE", "plllayer.gemspec"]
14
+ s.files += Dir["lib/**/*.rb"]
15
+
16
+ %w(bundler open4).each do |gem_name|
17
+ s.add_runtime_dependency gem_name
18
+ end
19
+
20
+ %w(rake).each do |gem_name|
21
+ s.add_development_dependency gem_name
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: plllayer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jeremy Ruten
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: open4
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: plllayer is an audio playback library for Ruby. It is a Ruby interface
63
+ to some external media player, such as mplayer.
64
+ email: jeremy.ruten@gmail.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - Gemfile
70
+ - Gemfile.lock
71
+ - LICENSE
72
+ - plllayer.gemspec
73
+ - lib/plllayer/single_player.rb
74
+ - lib/plllayer/single_players/mplayer.rb
75
+ - lib/plllayer.rb
76
+ homepage: http://github.com/yjerem/plllayer
77
+ licenses:
78
+ - MIT
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: 1.9.2
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 1.8.23
98
+ signing_key:
99
+ specification_version: 3
100
+ summary: An audio playback library for Ruby.
101
+ test_files: []