plllayer 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,7 @@
1
1
  class Plllayer
2
+ # Raise this exception when the file at the given track path doesn't exist.
3
+ FileNotFoundError = Class.new(ArgumentError)
4
+
2
5
  # A SinglePlayer takes care of playing a single track, and controlling the
3
6
  # playback with commands like pause, resume, seek, and so on. It probably
4
7
  # starts an external audio player process to do this job. This class is an
@@ -9,7 +12,7 @@ class Plllayer
9
12
  # All methods that perform an action should return false if the action isn't
10
13
  # applicable, and return a truthy value otherwise.
11
14
  class SinglePlayer
12
- # Returns true if a track is currently loaded, i.e. either playing or
15
+ # Return true if a track is currently loaded, i.e. either playing or
13
16
  # paused.
14
17
  def playing?
15
18
  raise NotImplementedError
@@ -22,7 +25,8 @@ class Plllayer
22
25
 
23
26
  # Begin playing a track. The track_path should be a String representing a
24
27
  # path to an audio file. The &on_end callback should be called when the track
25
- # is finished playing.
28
+ # is finished playing. Should raise FileNotFoundError if the audio file
29
+ # doesn't exist.
26
30
  def play(track_path, &on_end)
27
31
  raise NotImplementedError
28
32
  end
@@ -32,12 +36,12 @@ class Plllayer
32
36
  raise NotImplementedError
33
37
  end
34
38
 
35
- # Pauses playback.
39
+ # Pause playback.
36
40
  def pause
37
41
  raise NotImplementedError
38
42
  end
39
43
 
40
- # Resumes playback.
44
+ # Resume playback.
41
45
  def resume
42
46
  raise NotImplementedError
43
47
  end
@@ -59,30 +63,30 @@ class Plllayer
59
63
  end
60
64
  end
61
65
 
62
- # Gets the current playback speed. The speed is a multiplier. For example,
66
+ # Get the current playback speed. The speed is a multiplier. For example,
63
67
  # double speed is 2 and half-speed is 0.5. Normal speed is 1.
64
68
  def speed
65
69
  raise NotImplementedError
66
70
  end
67
71
 
68
- # Sets the playback speed. The speed is a multiplier. For example, for
72
+ # Set the playback speed. The speed is a multiplier. For example, for
69
73
  # double speed you'd set it to 2 and for half-speed you'd set it to 0.5. And
70
74
  # for normal speed: 1.
71
75
  def speed=(new_speed)
72
76
  raise NotImplementedError
73
77
  end
74
78
 
75
- # Returns true if audio is muted.
79
+ # Return true if audio is muted.
76
80
  def muted?
77
81
  raise NotImplementedError
78
82
  end
79
83
 
80
- # Mutes the audio player.
84
+ # Mute the audio player.
81
85
  def mute
82
86
  raise NotImplementedError
83
87
  end
84
88
 
85
- # Unmutes the audio player.
89
+ # Unmute the audio player.
86
90
  def unmute
87
91
  raise NotImplementedError
88
92
  end
@@ -92,17 +96,17 @@ class Plllayer
92
96
  raise NotImplementedError
93
97
  end
94
98
 
95
- # Set the volume as a percentage. The player is automatically unmuted.
99
+ # Set the volume as a percentage. The player may be automatically unmuted.
96
100
  def volume=(new_volume)
97
101
  raise NotImplementedError
98
102
  end
99
103
 
100
- # Returns the current time into the song, in milliseconds.
104
+ # Return the current time into the song, in milliseconds.
101
105
  def position
102
106
  raise NotImplementedError
103
107
  end
104
108
 
105
- # Returns the length of the current track, in milliseconds.
109
+ # Return the length of the current track, in milliseconds.
106
110
  def track_length
107
111
  raise NotImplementedError
108
112
  end
@@ -27,42 +27,41 @@ class Plllayer
27
27
  # Make sure we're only playing one song at any one time.
28
28
  _quit
29
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
30
+ # Make sure the audio file exists.
31
+ raise FileNotFoundError, "file '#{track_path}' doesn't exist" unless File.exists? track_path
40
32
 
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
33
+ # Run the mplayer process in slave mode, passing it the location of
34
+ # the track's audio file.
35
+ cmd = ["mplayer", "-slave", "-quiet", track_path]
36
+ @pid, @stdin, @stdout, @stderr = Open4.popen4(*cmd)
61
37
 
62
- true
63
- else
64
- false
38
+ # This should skip past mplayer's initial lines of output so we can
39
+ # start reading its replies to our commands.
40
+ until @stdout.gets["playback"]
65
41
  end
42
+
43
+ @paused = false
44
+ @track_path = track_path
45
+
46
+ # Persist the previous speed, volume, and mute properties into this
47
+ # process.
48
+ self.speed = @speed
49
+ self.volume = @volume
50
+ mute if @muted
51
+
52
+ # Start a thread that waits for the mplayer process to end, then calls
53
+ # the end of song callback. If the #quit method is called, this thread
54
+ # will be killed if it's still waiting for the process to end.
55
+ @quit_hook_active = false
56
+ @quit_hook = Thread.new do
57
+ Process.wait(@pid)
58
+ @quit_hook_active = true
59
+ @paused = false
60
+ @track_path = nil
61
+ on_end.call
62
+ end
63
+
64
+ true
66
65
  end
67
66
 
68
67
  def stop
@@ -201,8 +200,10 @@ class Plllayer
201
200
  _command "quit"
202
201
  @paused = false
203
202
  @track_path = nil
203
+ true
204
+ else
205
+ false
204
206
  end
205
- true
206
207
  end
207
208
 
208
209
  # Check if the mplayer process is still around.
@@ -0,0 +1,37 @@
1
+ class Plllayer
2
+ module TimeHelpers
3
+ # Helper method to format a number of milliseconds as a string like
4
+ # "1:03:56.555". The only option is :include_milliseconds, true by default. If
5
+ # false, milliseconds won't be included in the formatted string.
6
+ def format_time(milliseconds, options = {})
7
+ ms = milliseconds % 1000
8
+ seconds = (milliseconds / 1000) % 60
9
+ minutes = (milliseconds / 60000) % 60
10
+ hours = milliseconds / 3600000
11
+
12
+ if ms.zero? || options[:include_milliseconds] == false
13
+ ms_string = ""
14
+ else
15
+ ms_string = ".%03d" % [ms]
16
+ end
17
+
18
+ if hours > 0
19
+ "%d:%02d:%02d%s" % [hours, minutes, seconds, ms_string]
20
+ else
21
+ "%d:%02d%s" % [minutes, seconds, ms_string]
22
+ end
23
+ end
24
+
25
+ # Helper method to parse a string like "1:03:56.555" and return the number of
26
+ # milliseconds that time length represents.
27
+ def parse_time(string)
28
+ parts = string.split(":").map(&:to_f)
29
+ parts = [0] + parts if parts.length == 2
30
+ hours, minutes, seconds = parts
31
+ seconds = hours * 3600 + minutes * 60 + seconds
32
+ milliseconds = seconds * 1000
33
+ milliseconds.to_i
34
+ end
35
+ end
36
+ end
37
+
data/lib/plllayer.rb CHANGED
@@ -1,16 +1,46 @@
1
1
  require "open4"
2
2
 
3
+ require "plllayer/time_helpers.rb"
3
4
  require "plllayer/single_player"
4
5
  require "plllayer/single_players/mplayer"
5
6
 
7
+ # Plllayer provides an interface to an external media player, such as mplayer. It
8
+ # contains a playlist of tracks, which may be as simple as an Array of paths to
9
+ # some audio files. You can then control the playback of this playlist by calling
10
+ # various command-like methods, like play, pause, seek, skip, shuffle, and so on.
6
11
  class Plllayer
12
+ # This pulls in the Plllayer.parse_time and Plllayer.format_time helper methods.
13
+ # See plllayer/time_helpers.rb.
14
+ extend TimeHelpers
15
+
16
+ SINGLE_PLAYERS = {
17
+ mplayer: Plllayer::SinglePlayers::MPlayer
18
+ }
19
+
7
20
  attr_reader :repeat_mode
8
21
 
22
+ # Create a new Plllayer. Optionally, you can pass in an initial playlist to be
23
+ # loaded (won't start playing until you call #play). You can also pass the
24
+ # :external_player option to specify the preferred external player to use.
25
+ # Otherwise, it will try to figure out what external players are available, and
26
+ # attempt to use the best one available.
27
+ #
28
+ # However, only mplayer is supported at the moment, so this option isn't useful
29
+ # right now.
30
+ #
31
+ # TODO: check if external player is available before trying to use it.
9
32
  def initialize(*args)
10
33
  options = {}
11
34
  options = args.pop if args.last.is_a? Hash
35
+ options[:external_player] ||= :mplayer
36
+
37
+ # Look up the single player class, raise error if it doesn't exist.
38
+ single_player_class = SINGLE_PLAYERS[options[:external_player].to_sym]
39
+ if single_player_class.nil?
40
+ raise NotImplementedError, "external player #{options[:external_player]} not supported"
41
+ end
12
42
 
13
- @single_player = Plllayer::SinglePlayers::MPlayer.new
43
+ @single_player = single_player_class.new
14
44
  @playlist = []
15
45
  append(args.first) unless args.empty?
16
46
  @index = nil
@@ -18,34 +48,47 @@ class Plllayer
18
48
  @playing = false
19
49
  @repeat_mode = nil
20
50
 
51
+ # Make sure the music stops playing once the Ruby script is exited.
21
52
  at_exit { stop }
22
53
  end
23
54
 
24
- def load(playlist = nil)
25
- clear
26
- append(playlist)
27
- end
28
-
55
+ # Append tracks to the playlist. Can be done while the playlist is playing.
56
+ # A track is either a String containing the path to the audio file, or an
57
+ # object with a #location method that returns the path to the audio file.
58
+ # An ArgumentError is raised when you try to pass a non-track.
59
+ #
60
+ # This method is aliased as the << operator.
29
61
  def append(tracks)
30
- tracks = Array(tracks).dup
31
- tracks.select! { |track| track.respond_to?(:location) || (track.is_a?(String) && File.exists?(track)) }
62
+ tracks = Array(tracks)
63
+ tracks.each do |track|
64
+ if !track.is_a?(String) && !track.respond_to?(:location)
65
+ raise ArgumentError, "a #{track.class} is not a track (try adding a #location method)"
66
+ end
67
+ end
32
68
  @playlist += tracks
33
69
  @playlist.dup
34
70
  end
35
71
  alias :<< :append
36
72
 
73
+ # Returns a copy of the playlist.
37
74
  def playlist
38
75
  @playlist.dup
39
76
  end
40
77
 
78
+ # Get the currently-playing track, or the track that's about to play if
79
+ # you're paused between tracks. Returns nil if playback is stopped.
41
80
  def track
42
81
  @index ? @playlist[@index] : nil
43
82
  end
44
83
 
84
+ # Get the index of the currently-playing track, or of the track that's
85
+ # about to play if you're paused between tracks. Returns nil if playback
86
+ # is stopped.
45
87
  def track_index
46
88
  @index
47
89
  end
48
90
 
91
+ # Get the path to the audio file of the currently-playing track.
49
92
  def track_path
50
93
  if track.respond_to? :location
51
94
  track.location
@@ -54,22 +97,29 @@ class Plllayer
54
97
  end
55
98
  end
56
99
 
100
+ # Stop playback and empty the playlist.
57
101
  def clear
58
102
  stop
59
103
  @playlist.clear
60
104
  true
61
105
  end
62
106
 
107
+ # Check if playback is paused.
63
108
  def paused?
64
109
  @paused
65
110
  end
66
111
 
112
+ # Check if the playlist is being played. Whether playback is paused doesn't
113
+ # affect this. False is only returned if either (1) #play was never called,
114
+ # (2) #stop has been called, or (3) playback stopped after finishing playing
115
+ # all the songs.
67
116
  def playing?
68
117
  @playing
69
118
  end
70
119
 
120
+ # Start playing the playlist from beginning to end.
71
121
  def play
72
- unless @playlist.empty?
122
+ if !@playlist.empty? && !playing?
73
123
  @playing = true
74
124
  @paused = false
75
125
  @index = 0
@@ -80,17 +130,23 @@ class Plllayer
80
130
  end
81
131
  end
82
132
 
133
+ # Stop playback.
83
134
  def stop
84
- @single_player.stop
85
- @track = nil
86
- @index = nil
87
- @paused = false
88
- @playing = false
89
- true
135
+ if playing?
136
+ @single_player.stop
137
+ @track = nil
138
+ @index = nil
139
+ @paused = false
140
+ @playing = false
141
+ true
142
+ else
143
+ false
144
+ end
90
145
  end
91
146
 
147
+ # Pause playback.
92
148
  def pause
93
- if playing?
149
+ if playing? && !paused?
94
150
  @paused = true
95
151
  @single_player.pause
96
152
  else
@@ -98,14 +154,14 @@ class Plllayer
98
154
  end
99
155
  end
100
156
 
157
+ # Resume playback.
101
158
  def resume
102
- if playing?
159
+ if playing? && paused?
103
160
  @paused = false
104
161
  if @single_player.playing?
105
162
  @single_player.resume
106
163
  else
107
164
  play_track @track
108
- @playlist_paused = false
109
165
  true
110
166
  end
111
167
  else
@@ -113,6 +169,13 @@ class Plllayer
113
169
  end
114
170
  end
115
171
 
172
+ # Set the repeat behaviour of the playlist. There are three possible values:
173
+ #
174
+ # :one repeat a single track over and over
175
+ # :all repeat the whole playlist, treating it like a circular array
176
+ # :off play songs consecutively, stop playback when done
177
+ #
178
+ # The default, of course, is :off.
116
179
  def repeat(one_or_all_or_off)
117
180
  case one_or_all_or_off
118
181
  when :one
@@ -121,22 +184,41 @@ class Plllayer
121
184
  @repeat_mode = :all
122
185
  when :off
123
186
  @repeat_mode = nil
187
+ else
188
+ raise ArgumentError
124
189
  end
125
190
  true
126
191
  end
127
192
 
193
+ # Play the currently-playing track from the beginning.
128
194
  def restart
129
195
  change_track(0)
130
196
  end
131
197
 
198
+ # Play the previous track. Pass a number to go back that many tracks. Treats
199
+ # the playlist like a circular array if the repeat mode is :all.
132
200
  def back(n = 1)
133
201
  change_track(-n)
134
202
  end
135
203
 
204
+ # Play the next track. Pass a number to go forward that many tracks. Treats
205
+ # the playlist like a circular array if the repeat mode is :all.
136
206
  def skip(n = 1)
137
207
  change_track(n)
138
208
  end
139
209
 
210
+ # Seek to a particular position within the currently-playing track. There are
211
+ # multiple ways to specify where to seek to:
212
+ #
213
+ # seek 10000 # seek 10000 milliseconds forward, relative to the current position
214
+ # seek -5000 # seek 5000 milliseconds backward
215
+ # seek "3:45" # seek to the absolute position 3 minutes and 45 seconds
216
+ # seek "1:03:45.123" # seek to 1 hour, 3 minutes, 45 seconds, 123 milliseconds
217
+ # seek 3..45 # syntax sugar for seeking to "3:45"
218
+ # seek 3..45.123 # syntax sugar for seeking to "3:45.123"
219
+ # seek abs: 150000 # seek to the absolute position 150000 milliseconds
220
+ # seek percent: 80 # seek to 80% of the way through the track
221
+ #
140
222
  def seek(where)
141
223
  if paused? && !@single_player.playing?
142
224
  resume
@@ -150,7 +232,7 @@ class Plllayer
150
232
  seconds = where.begin * 60 + where.end
151
233
  @single_player.seek(seconds * 1000, :absolute)
152
234
  when String
153
- @single_player.seek(parse_time(where), :absolute)
235
+ @single_player.seek(Plllayer.parse_time(where), :absolute)
154
236
  when Hash
155
237
  if where[:abs]
156
238
  if where[:abs].is_a? Integer
@@ -161,37 +243,51 @@ class Plllayer
161
243
  elsif where[:percent]
162
244
  @single_player.seek(where[:percent], :percent)
163
245
  end
246
+ else
247
+ raise ArgumentError, "seek doesn't take a #{where.class}"
164
248
  end
165
249
  end
166
250
 
251
+ # Get the playback speed as a Float. 1.0 is normal speed, 2.0 is double speed,
252
+ # 0.5 is half-speed, and so on.
167
253
  def speed
168
254
  @single_player.speed || 1.0
169
255
  end
170
256
 
257
+ # Set the playback speed as a Float. 1.0 is normal speed, 2.0 is double speed,
258
+ # 0.5 is half-speed, and so on.
171
259
  def speed=(new_speed)
172
260
  @single_player.speed = new_speed
173
261
  end
174
262
 
263
+ # Mute the volume.
175
264
  def mute
176
265
  @single_player.mute
177
266
  end
178
267
 
268
+ # Unmute the volume.
179
269
  def unmute
180
270
  @single_player.unmute
181
271
  end
182
272
 
273
+ # Check if volume is muted.
183
274
  def muted?
184
275
  @single_player.muted?
185
276
  end
186
277
 
278
+ # Get the volume, as a percentage.
187
279
  def volume
188
280
  @single_player.volume
189
281
  end
190
282
 
283
+ # Set the volume, as a percentage.
191
284
  def volume=(new_volume)
192
285
  @single_player.volume = new_volume
193
286
  end
194
287
 
288
+ # Shuffle the playlist. If this is done while the playlist is playing, the
289
+ # current song will go to the top of the playlist and the rest of the songs
290
+ # will be shuffled.
195
291
  def shuffle
196
292
  current_track = track
197
293
  @playlist.shuffle!
@@ -203,6 +299,10 @@ class Plllayer
203
299
  true
204
300
  end
205
301
 
302
+ # Sorts the playlist. Delegates to Array#sort, so a block may be passed to
303
+ # specify what the tracks should be sorted by.
304
+ #
305
+ # This method is safe to call while the playlist is playing.
206
306
  def sort(&by)
207
307
  current_track = track
208
308
  @playlist.sort! &by
@@ -212,14 +312,18 @@ class Plllayer
212
312
  true
213
313
  end
214
314
 
315
+ # Returns the current position of the currently-playing track, in milliseconds.
215
316
  def position
216
317
  @single_player.position || 0
217
318
  end
218
319
 
320
+ # Returns the current position of the currently-playing track, as a String
321
+ # like "1:23".
219
322
  def formatted_position(options = {})
220
- format_time(position, options)
323
+ Plllayer.format_time(position, options)
221
324
  end
222
325
 
326
+ # Returns the length of the currently-playing track, in milliseconds.
223
327
  def track_length
224
328
  if paused? && !@single_player.playing?
225
329
  resume
@@ -228,9 +332,10 @@ class Plllayer
228
332
  @single_player.track_length
229
333
  end
230
334
 
335
+ # Returns the length of the currently-playing track, as a String like "1:23".
231
336
  def formatted_track_length(options = {})
232
337
  if length = track_length
233
- format_time(length, options)
338
+ Plllayer.format_time(length, options)
234
339
  end
235
340
  end
236
341
 
@@ -270,38 +375,5 @@ class Plllayer
270
375
  @paused = false
271
376
  true
272
377
  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
378
  end
307
379
 
data/plllayer.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "plllayer"
3
- s.version = "0.0.1"
4
- s.date = "2012-12-06"
3
+ s.version = "0.0.2"
4
+ s.date = "2013-01-31"
5
5
  s.summary = "An audio playback library for Ruby."
6
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
7
  s.author = "Jeremy Ruten"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plllayer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-06 00:00:00.000000000 Z
12
+ date: 2013-01-31 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -72,6 +72,7 @@ files:
72
72
  - plllayer.gemspec
73
73
  - lib/plllayer/single_player.rb
74
74
  - lib/plllayer/single_players/mplayer.rb
75
+ - lib/plllayer/time_helpers.rb
75
76
  - lib/plllayer.rb
76
77
  homepage: http://github.com/yjerem/plllayer
77
78
  licenses: