plllayer 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: