plllayer 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +89 -0
- data/Rakefile +6 -0
- data/lib/plllayer/synchronize.rb +11 -0
- data/lib/plllayer.rb +76 -7
- data/plllayer.gemspec +4 -3
- data/spec/10000ms.mp3 +0 -0
- data/spec/250ms.mp3 +0 -0
- data/spec/3000ms.mp3 +0 -0
- data/spec/invalid.mp3 +1 -0
- data/spec/mplayer_spec.rb +185 -0
- data/spec/nop_spec.rb +162 -0
- data/spec/playback.mp3 +1 -0
- data/spec/plllayer_spec.rb +84 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/time_helpers_spec.rb +87 -0
- metadata +15 -2
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# plllayer
|
2
|
+
|
3
|
+
`plllayer` provides a Ruby interface to an external media player, such as `mplayer`.
|
4
|
+
|
5
|
+
It takes a playlist to play (which may just be an `Array` of `String`s containing the paths to the audio files), and lets you control playback as the playlist plays, with commands such as `pause`, `resume`, `seek`, `skip`, and so on.
|
6
|
+
|
7
|
+
## Install
|
8
|
+
|
9
|
+
$ gem install plllayer
|
10
|
+
|
11
|
+
## Example
|
12
|
+
|
13
|
+
Here's how to play your music library in random order:
|
14
|
+
|
15
|
+
$ irb
|
16
|
+
irb> require "plllayer"
|
17
|
+
=> true
|
18
|
+
irb> player = Plllayer.new(Dir["Music/**/*.{mp3,m4a,ogg}"])
|
19
|
+
=> #<Plllayer: ... >
|
20
|
+
irb> player.shuffle
|
21
|
+
=> true
|
22
|
+
irb> player.play
|
23
|
+
=> true
|
24
|
+
irb>
|
25
|
+
|
26
|
+
Then, while it's playing, you can type `player.track` to see what song it's playing, `player.skip` to skip to the next song, and so on.
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
Make a `Plllayer` like this:
|
31
|
+
|
32
|
+
player = Plllayer.new
|
33
|
+
|
34
|
+
This initializes a `Plllayer` with an empty playlist, and will use whatever external audio player is available on the system. You can also pass in an initial playlist, or specify what external audio player you want to use:
|
35
|
+
|
36
|
+
player = Plllayer.new(playlist)
|
37
|
+
player = Plllayer.new(external_player: :mplayer)
|
38
|
+
|
39
|
+
Only `mplayer` is supported at the moment.
|
40
|
+
|
41
|
+
### Playlists
|
42
|
+
|
43
|
+
A playlist is just an `Array` of tracks. A track is either a `String` containing the path to an audio file, or an object with a `#location` attribute that returns the path.
|
44
|
+
|
45
|
+
A singleton playlist, with only one track, doesn't need to be in an `Array`. That's ugly. You can just pass a track object to any method that expects a playlist and it'll understand.
|
46
|
+
|
47
|
+
To tell the `Plllayer` what playlist to play, either initialize the `Plllayer` with the playlist or use `Plllayer#append`:
|
48
|
+
|
49
|
+
player = Plllayer.new
|
50
|
+
player.append(playlist)
|
51
|
+
player << more_tracks # the << operator is an alias for append
|
52
|
+
|
53
|
+
The playlist can be accessed by `Plllayer#playlist`.
|
54
|
+
|
55
|
+
The playlist can be shuffled using `Plllayer#shuffle`:
|
56
|
+
|
57
|
+
player.shuffle
|
58
|
+
|
59
|
+
If one of the tracks is currently playing, it will be kept at the top of the playlist while the rest of the tracks will be shuffled.
|
60
|
+
|
61
|
+
The playlist can also be sorted:
|
62
|
+
|
63
|
+
player.sort
|
64
|
+
|
65
|
+
This delegates to Ruby's `Array#sort`, which sorts the tracks using their `<=>` method. This also means you can pass a block to compare tracks by, like this:
|
66
|
+
|
67
|
+
player.sort { |a, b| [a.artist, a.album, a.track_number] <=> [b.artist, b.album, b.track_number] }
|
68
|
+
|
69
|
+
This is safe to do when a track is currently playing, and then the next track that plays will be whatever comes next in the sorted playlist.
|
70
|
+
|
71
|
+
Lastly, the player's playlist can be reset to an empty playlist using `Plllayer#clear`:
|
72
|
+
|
73
|
+
player.clear
|
74
|
+
|
75
|
+
This stops playback if it's currently playing.
|
76
|
+
|
77
|
+
### Playback commands
|
78
|
+
|
79
|
+
There are many commands that influence playback. Think of them as buttons on a physical media player: you can push them even when they don't apply to the state of the player. They always return false if this is the case, otherwise they return a truthy value.
|
80
|
+
|
81
|
+
See `lib/plllayer.rb` for all the available commands, with documentation.
|
82
|
+
|
83
|
+
## Todo
|
84
|
+
|
85
|
+
* Support multiple external players
|
86
|
+
* Work around mplayer's bug where it reports the length of the track totally wrong
|
87
|
+
* Fix bug with mplayer returning weird answers sometimes
|
88
|
+
* Write tests
|
89
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
class Module
|
2
|
+
def synchronize(method, mutex_name)
|
3
|
+
alias_method :"_unsynchronized_#{method}", method.to_sym
|
4
|
+
define_method(method.to_sym) do |*args, &blk|
|
5
|
+
instance_variable_get(mutex_name.to_sym).synchronize do
|
6
|
+
send(:"_unsynchronized_#{method}", *args, &blk)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
data/lib/plllayer.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require "open4"
|
2
2
|
|
3
|
-
require "plllayer/time_helpers
|
3
|
+
require "plllayer/time_helpers"
|
4
|
+
require "plllayer/synchronize"
|
5
|
+
|
4
6
|
require "plllayer/single_player"
|
5
7
|
require "plllayer/single_players/mplayer"
|
6
8
|
require "plllayer/single_players/nop"
|
@@ -49,6 +51,8 @@ class Plllayer
|
|
49
51
|
@paused = false
|
50
52
|
@playing = false
|
51
53
|
@repeat_mode = nil
|
54
|
+
|
55
|
+
@index_mutex = Mutex.new
|
52
56
|
end
|
53
57
|
|
54
58
|
# Append tracks to the playlist. Can be done while the playlist is playing.
|
@@ -57,8 +61,8 @@ class Plllayer
|
|
57
61
|
# An ArgumentError is raised when you try to pass a non-track.
|
58
62
|
#
|
59
63
|
# This method is aliased as the << operator.
|
60
|
-
def append(tracks)
|
61
|
-
tracks =
|
64
|
+
def append(*tracks)
|
65
|
+
tracks = tracks.flatten
|
62
66
|
tracks.each do |track|
|
63
67
|
if !track.is_a?(String) && !track.respond_to?(:location)
|
64
68
|
raise ArgumentError, "a #{track.class} is not a track (try adding a #location method)"
|
@@ -67,8 +71,74 @@ class Plllayer
|
|
67
71
|
@playlist += tracks
|
68
72
|
@playlist.dup
|
69
73
|
end
|
74
|
+
synchronize :append, :@index_mutex
|
70
75
|
alias :<< :append
|
71
76
|
|
77
|
+
# Insert one or more tracks in the playlist, right after the currently-playing
|
78
|
+
# track. If no track is playing, insert to the head of the playlist.
|
79
|
+
def insert(*tracks)
|
80
|
+
after = @index || -1
|
81
|
+
_unsynchronized_insert_at(after + 1, *tracks)
|
82
|
+
@playlist.dup
|
83
|
+
end
|
84
|
+
synchronize :insert, :@index_mutex
|
85
|
+
|
86
|
+
# Insert one or more tracks anywhere in the playlist. A negative index may be
|
87
|
+
# given to refer to the end of the playlist.
|
88
|
+
def insert_at(index, *tracks)
|
89
|
+
index = @playlist.length + index + 1 if index < 0
|
90
|
+
unless (0..@playlist.length).include? index
|
91
|
+
raise IndexError, "index is out of range"
|
92
|
+
end
|
93
|
+
@playlist.insert(index, *tracks)
|
94
|
+
@index += tracks.length if @index && index < @index
|
95
|
+
@playlist.dup
|
96
|
+
end
|
97
|
+
synchronize :insert_at, :@index_mutex
|
98
|
+
|
99
|
+
# Remove one or more tracks from the playlist by value. If the currently-playing
|
100
|
+
# track is removed, it will start playing the next track in the playlist.
|
101
|
+
def remove(*tracks)
|
102
|
+
n = nil
|
103
|
+
n = tracks.pop if tracks.last.is_a?(Fixnum)
|
104
|
+
index = 0
|
105
|
+
current_track_removed = false
|
106
|
+
@playlist = @playlist.inject([]) do |playlist, track|
|
107
|
+
if tracks.include?(track) && (n.nil? || n > 0)
|
108
|
+
n &&= n - 1
|
109
|
+
@index -= 1 if @index && index < @index
|
110
|
+
current_track_removed = true if index == @index
|
111
|
+
else
|
112
|
+
playlist << track
|
113
|
+
end
|
114
|
+
index += 1
|
115
|
+
playlist
|
116
|
+
end
|
117
|
+
_unsynchronized_change_track(0) if current_track_removed
|
118
|
+
@playlist.dup
|
119
|
+
end
|
120
|
+
synchronize :remove, :@index_mutex
|
121
|
+
|
122
|
+
# Remove one or more tracks from the playlist at a particular index. A negative
|
123
|
+
# index may be given to refer to the end of the playlist. If the currently-playing
|
124
|
+
# track is removed, it will start playing the next track in the playlist.
|
125
|
+
def remove_at(index, n = 1)
|
126
|
+
index = @playlist.length + index if index < 0
|
127
|
+
if @playlist.empty? || index < 0 || index + n > @playlist.length
|
128
|
+
raise IndexError, "index is out of range"
|
129
|
+
end
|
130
|
+
@playlist.slice!(index, n)
|
131
|
+
if @index && @index > index
|
132
|
+
if @index < index + n
|
133
|
+
_unsynchronized_change_track(index - @index)
|
134
|
+
else
|
135
|
+
@index -= n
|
136
|
+
end
|
137
|
+
end
|
138
|
+
@playlist.dup
|
139
|
+
end
|
140
|
+
synchronize :remove_at, :@index_mutex
|
141
|
+
|
72
142
|
# Returns a copy of the playlist.
|
73
143
|
def playlist
|
74
144
|
@playlist.dup
|
@@ -142,6 +212,7 @@ class Plllayer
|
|
142
212
|
false
|
143
213
|
end
|
144
214
|
end
|
215
|
+
synchronize :stop, :@index_mutex
|
145
216
|
|
146
217
|
# Pause playback.
|
147
218
|
def pause
|
@@ -342,9 +413,6 @@ class Plllayer
|
|
342
413
|
|
343
414
|
def change_track(by = 1, options = {})
|
344
415
|
if playing?
|
345
|
-
if options[:auto] && track.respond_to?(:increment_play_count)
|
346
|
-
track.increment_play_count
|
347
|
-
end
|
348
416
|
@index += by
|
349
417
|
if @repeat_mode
|
350
418
|
case @repeat_mode
|
@@ -358,13 +426,14 @@ class Plllayer
|
|
358
426
|
@single_player.stop if paused? && @single_player.active?
|
359
427
|
play_track if not paused?
|
360
428
|
else
|
361
|
-
|
429
|
+
_unsynchronized_stop
|
362
430
|
end
|
363
431
|
true
|
364
432
|
else
|
365
433
|
false
|
366
434
|
end
|
367
435
|
end
|
436
|
+
synchronize :change_track, :@index_mutex
|
368
437
|
|
369
438
|
def play_track
|
370
439
|
@single_player.play(track_path) do
|
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.
|
4
|
-
s.date = "2013-02-
|
3
|
+
s.version = "0.0.4"
|
4
|
+
s.date = "2013-02-12"
|
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"
|
@@ -10,8 +10,9 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.license = "MIT"
|
11
11
|
s.required_ruby_version = ">= 1.9.2"
|
12
12
|
|
13
|
-
s.files = ["Gemfile", "Gemfile.lock", "LICENSE", "plllayer.gemspec"]
|
13
|
+
s.files = ["Rakefile", "Gemfile", "Gemfile.lock", "LICENSE", "plllayer.gemspec", "README.md"]
|
14
14
|
s.files += Dir["lib/**/*.rb"]
|
15
|
+
s.files += Dir["spec/**/*.{rb,mp3}"]
|
15
16
|
|
16
17
|
%w(bundler open4).each do |gem_name|
|
17
18
|
s.add_runtime_dependency gem_name
|
data/spec/10000ms.mp3
ADDED
Binary file
|
data/spec/250ms.mp3
ADDED
Binary file
|
data/spec/3000ms.mp3
ADDED
Binary file
|
data/spec/invalid.mp3
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
this is an invalid mp3 file. let's see what mplayer does with it.
|
@@ -0,0 +1,185 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
PATH_3000MS = "spec/3000ms.mp3"
|
4
|
+
PATH_10000MS = "spec/10000ms.mp3"
|
5
|
+
NONEXISTANT_PATH = "spec/not_here.mp3"
|
6
|
+
INVALID_AUDIO_FILE = "spec/invalid.mp3"
|
7
|
+
|
8
|
+
describe Plllayer::SinglePlayers::MPlayer do
|
9
|
+
before(:each) do
|
10
|
+
@player = Plllayer::SinglePlayers::MPlayer.new
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:each) do
|
14
|
+
@player.stop
|
15
|
+
end
|
16
|
+
|
17
|
+
it "is not playing by default" do
|
18
|
+
@player.should_not be_playing
|
19
|
+
end
|
20
|
+
|
21
|
+
it "starts playing a single audio file" do
|
22
|
+
@player.play(PATH_3000MS)
|
23
|
+
@player.should be_playing
|
24
|
+
sleep 3.1
|
25
|
+
@player.should_not be_playing
|
26
|
+
end
|
27
|
+
|
28
|
+
it "executes a callback when the track is done playing" do
|
29
|
+
done_playing = false
|
30
|
+
@player.play(PATH_3000MS) { done_playing = true }
|
31
|
+
done_playing.should be_false
|
32
|
+
sleep 3.1
|
33
|
+
done_playing.should be_true
|
34
|
+
end
|
35
|
+
|
36
|
+
it "doesn't play non-existant files" do
|
37
|
+
expect { @player.play NONEXISTANT_PATH }.to raise_error(Plllayer::FileNotFoundError)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "doesn't play invalid audio files" do
|
41
|
+
expect { @player.play INVALID_AUDIO_FILE }.to raise_error(Plllayer::InvalidAudioFileError)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "stops playback" do
|
45
|
+
callback_called = false
|
46
|
+
@player.play(PATH_3000MS) { callback_called = true }
|
47
|
+
@player.stop
|
48
|
+
@player.should_not be_playing
|
49
|
+
sleep 3.1
|
50
|
+
callback_called.should be_false
|
51
|
+
end
|
52
|
+
|
53
|
+
it "pauses and resumes playback" do
|
54
|
+
@player.play(PATH_3000MS)
|
55
|
+
@player.should_not be_paused
|
56
|
+
@player.pause
|
57
|
+
@player.should be_paused
|
58
|
+
position = @player.position
|
59
|
+
sleep 0.3
|
60
|
+
@player.position.should be_within(100).of(position)
|
61
|
+
@player.resume
|
62
|
+
@player.should_not be_paused
|
63
|
+
sleep 0.3
|
64
|
+
@player.position.should_not be_within(100).of(position)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "seeks to an absolute position" do
|
68
|
+
@player.play(PATH_3000MS)
|
69
|
+
@player.pause
|
70
|
+
@player.seek(2000)
|
71
|
+
@player.position.should be_within(100).of(2000)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "seeks to a relative position" do
|
75
|
+
@player.play(PATH_10000MS)
|
76
|
+
@player.pause
|
77
|
+
position = @player.position
|
78
|
+
@player.seek(5000, :relative)
|
79
|
+
@player.position.should be_within(2500).of(position + 5000)
|
80
|
+
@player.seek(-4000, :relative)
|
81
|
+
@player.position.should be_within(2500).of(position + 1000)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "seeks to a percentage position" do
|
85
|
+
@player.play(PATH_3000MS)
|
86
|
+
@player.pause
|
87
|
+
@player.seek(50, :percent)
|
88
|
+
@player.position.should be_within(100).of(1500)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "has a default speed of 1" do
|
92
|
+
@player.speed.should eq(1.0)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "speeds up playback" do
|
96
|
+
@player.speed = 2.0
|
97
|
+
@player.play(PATH_3000MS)
|
98
|
+
@player.should be_playing
|
99
|
+
sleep 2.5
|
100
|
+
@player.should_not be_playing
|
101
|
+
end
|
102
|
+
|
103
|
+
it "slows down playback" do
|
104
|
+
@player.speed = 0.5
|
105
|
+
@player.play(PATH_3000MS)
|
106
|
+
@player.should be_playing
|
107
|
+
sleep 3.1
|
108
|
+
@player.should be_playing
|
109
|
+
sleep 3
|
110
|
+
@player.should_not be_playing
|
111
|
+
end
|
112
|
+
|
113
|
+
it "is not initially muted" do
|
114
|
+
@player.should_not be_muted
|
115
|
+
end
|
116
|
+
|
117
|
+
it "mutes the volume" do
|
118
|
+
@player.mute
|
119
|
+
@player.should be_muted
|
120
|
+
end
|
121
|
+
|
122
|
+
it "unmutes the volume" do
|
123
|
+
@player.mute
|
124
|
+
@player.unmute
|
125
|
+
@player.should_not be_muted
|
126
|
+
end
|
127
|
+
|
128
|
+
it "has a default volume of 50%" do
|
129
|
+
@player.volume.should eq(50)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "changes the volume" do
|
133
|
+
@player.volume = 100
|
134
|
+
@player.volume.should eq(100)
|
135
|
+
end
|
136
|
+
|
137
|
+
it "unmutes when changing the volume" do
|
138
|
+
@player.mute
|
139
|
+
@player.volume = 50
|
140
|
+
@player.should_not be_muted
|
141
|
+
end
|
142
|
+
|
143
|
+
it "keeps track of the position of playback" do
|
144
|
+
@player.play(PATH_3000MS)
|
145
|
+
@player.position.should be_within(100).of(100)
|
146
|
+
sleep 0.5
|
147
|
+
@player.position.should be_within(200).of(600)
|
148
|
+
end
|
149
|
+
|
150
|
+
it "tells you the length of the track in milliseconds" do
|
151
|
+
@player.play(PATH_3000MS)
|
152
|
+
@player.track_length.should be_within(100).of(3000)
|
153
|
+
end
|
154
|
+
|
155
|
+
it "persists speed, volume, and mute settings from track to track" do
|
156
|
+
@player.play(PATH_3000MS)
|
157
|
+
@player.speed = 2.0
|
158
|
+
@player.volume = 5
|
159
|
+
@player.mute
|
160
|
+
@player.stop
|
161
|
+
|
162
|
+
@player.play(PATH_3000MS)
|
163
|
+
@player.speed.should eq(2.0)
|
164
|
+
@player.volume.should eq(5)
|
165
|
+
@player.should be_muted
|
166
|
+
end
|
167
|
+
|
168
|
+
it "returns false for most commands when playback is stopped" do
|
169
|
+
@player.stop.should eq(false)
|
170
|
+
@player.pause.should eq(false)
|
171
|
+
@player.resume.should eq(false)
|
172
|
+
@player.seek(0).should eq(false)
|
173
|
+
@player.position.should eq(false)
|
174
|
+
@player.track_length.should eq(false)
|
175
|
+
end
|
176
|
+
|
177
|
+
it "returns false when double pausing resuming" do
|
178
|
+
@player.play(PATH_3000MS)
|
179
|
+
@player.pause.should be_true
|
180
|
+
@player.pause.should eq(false)
|
181
|
+
@player.resume.should be_true
|
182
|
+
@player.resume.should eq(false)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
data/spec/nop_spec.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
PATH = "spec/250ms.mp3"
|
4
|
+
NONEXISTANT_PATH = "spec/not_here.mp3"
|
5
|
+
|
6
|
+
describe Plllayer::SinglePlayers::Nop do
|
7
|
+
before(:each) do
|
8
|
+
@player = Plllayer::SinglePlayers::Nop.new
|
9
|
+
end
|
10
|
+
|
11
|
+
it "is not playing by default" do
|
12
|
+
@player.should_not be_playing
|
13
|
+
end
|
14
|
+
|
15
|
+
it "starts playing a single audio file" do
|
16
|
+
@player.play PATH
|
17
|
+
@player.should be_playing
|
18
|
+
sleep 0.5
|
19
|
+
@player.should_not be_playing
|
20
|
+
end
|
21
|
+
|
22
|
+
it "executes a callback when the track is done playing" do
|
23
|
+
done_playing = false
|
24
|
+
@player.play(PATH) { done_playing = true }
|
25
|
+
done_playing.should be_false
|
26
|
+
sleep 0.5
|
27
|
+
done_playing.should be_true
|
28
|
+
end
|
29
|
+
|
30
|
+
it "doesn't play non-existant files" do
|
31
|
+
expect { @player.play NONEXISTANT_PATH }.to raise_error(Plllayer::FileNotFoundError)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "stops playback" do
|
35
|
+
callback_called = false
|
36
|
+
@player.play(PATH) { callback_called = true }
|
37
|
+
@player.stop
|
38
|
+
@player.should_not be_playing
|
39
|
+
sleep 0.5
|
40
|
+
callback_called.should be_false
|
41
|
+
end
|
42
|
+
|
43
|
+
it "pauses and resumes playback" do
|
44
|
+
@player.play(PATH)
|
45
|
+
@player.should_not be_paused
|
46
|
+
@player.pause
|
47
|
+
@player.should be_paused
|
48
|
+
position = @player.position
|
49
|
+
sleep 0.2
|
50
|
+
@player.position.should eq(position)
|
51
|
+
@player.resume
|
52
|
+
@player.should_not be_paused
|
53
|
+
sleep 0.2
|
54
|
+
@player.position.should_not eq(position)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "seeks to an absolute position" do
|
58
|
+
@player.play(PATH)
|
59
|
+
@player.pause
|
60
|
+
@player.seek(100)
|
61
|
+
@player.position.should eq(100)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "seeks to a relative position" do
|
65
|
+
@player.play(PATH)
|
66
|
+
@player.pause
|
67
|
+
position = @player.position
|
68
|
+
@player.seek(10, :relative)
|
69
|
+
@player.position.should eq(position + 10)
|
70
|
+
@player.seek(-10, :relative)
|
71
|
+
@player.position.should eq(position)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "seeks to a percentage position" do
|
75
|
+
@player.play(PATH)
|
76
|
+
@player.pause
|
77
|
+
@player.seek(50, :percent)
|
78
|
+
@player.position.should be_within(1).of(125)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "has a default speed of 1" do
|
82
|
+
@player.speed.should eq(1.0)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "speeds up playback" do
|
86
|
+
@player.speed = 2.0
|
87
|
+
@player.play(PATH)
|
88
|
+
@player.should be_playing
|
89
|
+
sleep 0.15
|
90
|
+
@player.should_not be_playing
|
91
|
+
end
|
92
|
+
|
93
|
+
it "slows down playback" do
|
94
|
+
@player.speed = 0.5
|
95
|
+
@player.play(PATH)
|
96
|
+
@player.should be_playing
|
97
|
+
sleep 0.3
|
98
|
+
@player.should be_playing
|
99
|
+
sleep 0.3
|
100
|
+
@player.should_not be_playing
|
101
|
+
end
|
102
|
+
|
103
|
+
it "is not initially muted" do
|
104
|
+
@player.should_not be_muted
|
105
|
+
end
|
106
|
+
|
107
|
+
it "mutes the volume" do
|
108
|
+
@player.mute
|
109
|
+
@player.should be_muted
|
110
|
+
end
|
111
|
+
|
112
|
+
it "unmutes the volume" do
|
113
|
+
@player.mute
|
114
|
+
@player.unmute
|
115
|
+
@player.should_not be_muted
|
116
|
+
end
|
117
|
+
|
118
|
+
it "has a default volume of 50%" do
|
119
|
+
@player.volume.should eq(50)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "changes the volume" do
|
123
|
+
@player.volume = 100
|
124
|
+
@player.volume.should eq(100)
|
125
|
+
end
|
126
|
+
|
127
|
+
it "unmutes when changing the volume" do
|
128
|
+
@player.mute
|
129
|
+
@player.volume = 50
|
130
|
+
@player.should_not be_muted
|
131
|
+
end
|
132
|
+
|
133
|
+
it "keeps track of the position of playback" do
|
134
|
+
@player.play(PATH)
|
135
|
+
@player.position.should be_within(5).of(5)
|
136
|
+
sleep 0.1
|
137
|
+
@player.position.should be_within(10).of(105)
|
138
|
+
end
|
139
|
+
|
140
|
+
it "tells you the length of the track in milliseconds" do
|
141
|
+
@player.play(PATH)
|
142
|
+
@player.track_length.should eq(250)
|
143
|
+
end
|
144
|
+
|
145
|
+
it "returns false for most commands when playback is stopped" do
|
146
|
+
@player.stop.should eq(false)
|
147
|
+
@player.pause.should eq(false)
|
148
|
+
@player.resume.should eq(false)
|
149
|
+
@player.seek(0).should eq(false)
|
150
|
+
@player.position.should eq(false)
|
151
|
+
@player.track_length.should eq(false)
|
152
|
+
end
|
153
|
+
|
154
|
+
it "returns false when double pausing resuming" do
|
155
|
+
@player.play(PATH)
|
156
|
+
@player.pause.should be_true
|
157
|
+
@player.pause.should eq(false)
|
158
|
+
@player.resume.should be_true
|
159
|
+
@player.resume.should eq(false)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
data/spec/playback.mp3
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
this is an invalid mp3 file. let's see what mplayer does with it.
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
TRACK_1 = "spec/250ms.mp3"
|
4
|
+
TRACK_2 = "spec/3000ms.mp3"
|
5
|
+
TRACK_3 = "spec/10000ms.mp3"
|
6
|
+
|
7
|
+
describe Plllayer do
|
8
|
+
before(:each) do
|
9
|
+
@player = Plllayer.new(external_player: :nop)
|
10
|
+
end
|
11
|
+
|
12
|
+
after(:each) do
|
13
|
+
@player.stop
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "playlist control" do
|
17
|
+
it "appends tracks to the end of the playlist" do
|
18
|
+
@player.append(TRACK_1, TRACK_2)
|
19
|
+
@player << [TRACK_2, TRACK_3]
|
20
|
+
@player.playlist.should eq([TRACK_1, TRACK_2, TRACK_2, TRACK_3])
|
21
|
+
end
|
22
|
+
|
23
|
+
it "inserts tracks anywhere in the playlist" do
|
24
|
+
@player.insert_at(0, TRACK_1)
|
25
|
+
@player.playlist.should eq([TRACK_1])
|
26
|
+
@player.insert_at(0, TRACK_2)
|
27
|
+
@player.playlist.should eq([TRACK_2, TRACK_1])
|
28
|
+
@player.insert_at(1, TRACK_3)
|
29
|
+
@player.playlist.should eq([TRACK_2, TRACK_3, TRACK_1])
|
30
|
+
@player.insert_at(3, TRACK_1, TRACK_2, TRACK_3)
|
31
|
+
@player.playlist.should eq([TRACK_2, TRACK_3, TRACK_1, TRACK_1, TRACK_2, TRACK_3])
|
32
|
+
end
|
33
|
+
|
34
|
+
it "inserts tracks after the currently playing track" do
|
35
|
+
@player.insert(TRACK_1)
|
36
|
+
@player.playlist.should eq([TRACK_1])
|
37
|
+
@player.insert(TRACK_2)
|
38
|
+
@player.playlist.should eq([TRACK_2, TRACK_1])
|
39
|
+
@player.play
|
40
|
+
@player.insert(TRACK_3)
|
41
|
+
@player.playlist.should eq([TRACK_2, TRACK_3, TRACK_1])
|
42
|
+
end
|
43
|
+
|
44
|
+
it "removes tracks by object comparison" do
|
45
|
+
@player << [TRACK_1, TRACK_2, TRACK_1, TRACK_3, TRACK_1]
|
46
|
+
@player.remove(TRACK_1)
|
47
|
+
@player.playlist.should eq([TRACK_2, TRACK_3])
|
48
|
+
@player << [TRACK_1, TRACK_1, TRACK_1]
|
49
|
+
@player.remove(TRACK_1, 2)
|
50
|
+
@player.playlist.should eq([TRACK_2, TRACK_3, TRACK_1])
|
51
|
+
@player.remove(TRACK_1, TRACK_2, TRACK_3)
|
52
|
+
@player.playlist.should be_empty
|
53
|
+
end
|
54
|
+
|
55
|
+
it "removes one or more tracks anywhere in the playlist" do
|
56
|
+
@player << [TRACK_1, TRACK_2, TRACK_3]
|
57
|
+
@player.remove_at(1)
|
58
|
+
@player.playlist.should eq([TRACK_1, TRACK_3])
|
59
|
+
@player.remove_at(0, 2)
|
60
|
+
@player.playlist.should be_empty
|
61
|
+
end
|
62
|
+
|
63
|
+
it "continues playing while playlist is modified" do
|
64
|
+
@player << [TRACK_3, TRACK_2, TRACK_1]
|
65
|
+
@player.play
|
66
|
+
@player << TRACK_1
|
67
|
+
@player.playlist.should eq([TRACK_3, TRACK_2, TRACK_1, TRACK_1])
|
68
|
+
@player.track.should eq(TRACK_3)
|
69
|
+
@player.remove(TRACK_3)
|
70
|
+
@player.playlist.should eq([TRACK_2, TRACK_1, TRACK_1])
|
71
|
+
@player.track.should eq(TRACK_2)
|
72
|
+
@player.remove(TRACK_2, TRACK_1)
|
73
|
+
@player.should_not be_playing
|
74
|
+
end
|
75
|
+
|
76
|
+
it "raises index error when inserting or removing out of the playlist's bounds" do
|
77
|
+
expect { @player.insert_at(-3, TRACK_1) }.to raise_error(IndexError)
|
78
|
+
expect { @player.insert_at(2, TRACK_1) }.to raise_error(IndexError)
|
79
|
+
expect { @player.remove_at(-3) }.to raise_error(IndexError)
|
80
|
+
expect { @player.remove_at(0) }.to raise_error(IndexError)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "plllayer"
|
2
|
+
|
3
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
4
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
5
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
6
|
+
# loaded once.
|
7
|
+
#
|
8
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
11
|
+
config.run_all_when_everything_filtered = true
|
12
|
+
config.filter_run :focus
|
13
|
+
|
14
|
+
# Run specs in random order to surface order dependencies. If you find an
|
15
|
+
# order dependency and want to debug it, you can fix the order by providing
|
16
|
+
# the seed, which is printed after each run.
|
17
|
+
# --seed 1234
|
18
|
+
config.order = 'random'
|
19
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Plllayer::TimeHelpers do
|
4
|
+
describe :format_time do
|
5
|
+
it "formats a time of zero milliseconds" do
|
6
|
+
Plllayer.format_time(0).should eq("0:00")
|
7
|
+
end
|
8
|
+
|
9
|
+
it "formats a time of one millisecond" do
|
10
|
+
Plllayer.format_time(1).should eq("0:00.001")
|
11
|
+
end
|
12
|
+
|
13
|
+
it "formats a time of one second" do
|
14
|
+
Plllayer.format_time(1000).should eq("0:01")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "formats a time of one second plus a millisecond" do
|
18
|
+
Plllayer.format_time(1001).should eq("0:01.001")
|
19
|
+
end
|
20
|
+
|
21
|
+
it "formats a time of one minute and thirty seconds" do
|
22
|
+
Plllayer.format_time(90000).should eq("1:30")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "formats times of an hour" do
|
26
|
+
Plllayer.format_time(3600000).should eq("1:00:00")
|
27
|
+
end
|
28
|
+
|
29
|
+
it "formats times with multiple components" do
|
30
|
+
Plllayer.format_time(7200000 + 120000 + 2000 + 2).should eq("2:02:02.002")
|
31
|
+
end
|
32
|
+
|
33
|
+
it "can exclude milliseconds from the output" do
|
34
|
+
Plllayer.format_time(1500).should eq("0:01.500")
|
35
|
+
Plllayer.format_time(1500, include_milliseconds: false).should eq("0:01")
|
36
|
+
end
|
37
|
+
|
38
|
+
it "can't format negative times" do
|
39
|
+
expect { Plllayer.format_time(-1) }.to raise_error(ArgumentError)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe :parse_time do
|
44
|
+
it "parses times of zero" do
|
45
|
+
Plllayer.parse_time("0").should eq(0)
|
46
|
+
Plllayer.parse_time("0:00").should eq(0)
|
47
|
+
Plllayer.parse_time("0:00.000").should eq(0)
|
48
|
+
Plllayer.parse_time("0:00:00.000").should eq(0)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "parses milliseconds" do
|
52
|
+
Plllayer.parse_time("0.001").should eq(1)
|
53
|
+
Plllayer.parse_time("0:00.999").should eq(999)
|
54
|
+
Plllayer.parse_time("0:00:00.5").should eq(500)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "parses seconds" do
|
58
|
+
Plllayer.parse_time("1").should eq(1000)
|
59
|
+
Plllayer.parse_time("0:05").should eq(5000)
|
60
|
+
Plllayer.parse_time("0:00:09.500").should eq(9500)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "parses minutes" do
|
64
|
+
Plllayer.parse_time("2:30").should eq(150000)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "parses hours" do
|
68
|
+
Plllayer.parse_time("1:00:00").should eq(3600000)
|
69
|
+
Plllayer.parse_time("2:02:02.002").should eq(7200000 + 120000 + 2000 + 2)
|
70
|
+
Plllayer.parse_time("1000:00:00").should eq(1000 * 3600000)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "assumes empty components are zero" do
|
74
|
+
Plllayer.parse_time("1:").should eq(60000)
|
75
|
+
Plllayer.parse_time("1::.5").should eq(3600000 + 500)
|
76
|
+
Plllayer.parse_time("::.001").should eq(1)
|
77
|
+
Plllayer.parse_time("::6.").should eq(6000)
|
78
|
+
Plllayer.parse_time("").should eq(0)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "can't parse invalid strings" do
|
82
|
+
expect { Plllayer.parse_time("-1") }.to raise_error(ArgumentError)
|
83
|
+
expect { Plllayer.parse_time("1:00:00:00") }.to raise_error(ArgumentError)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
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.
|
4
|
+
version: 0.0.4
|
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: 2013-02-
|
12
|
+
date: 2013-02-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -82,15 +82,28 @@ executables: []
|
|
82
82
|
extensions: []
|
83
83
|
extra_rdoc_files: []
|
84
84
|
files:
|
85
|
+
- Rakefile
|
85
86
|
- Gemfile
|
86
87
|
- Gemfile.lock
|
87
88
|
- LICENSE
|
88
89
|
- plllayer.gemspec
|
90
|
+
- README.md
|
89
91
|
- lib/plllayer/single_player.rb
|
90
92
|
- lib/plllayer/single_players/mplayer.rb
|
91
93
|
- lib/plllayer/single_players/nop.rb
|
94
|
+
- lib/plllayer/synchronize.rb
|
92
95
|
- lib/plllayer/time_helpers.rb
|
93
96
|
- lib/plllayer.rb
|
97
|
+
- spec/mplayer_spec.rb
|
98
|
+
- spec/nop_spec.rb
|
99
|
+
- spec/plllayer_spec.rb
|
100
|
+
- spec/spec_helper.rb
|
101
|
+
- spec/time_helpers_spec.rb
|
102
|
+
- spec/10000ms.mp3
|
103
|
+
- spec/250ms.mp3
|
104
|
+
- spec/3000ms.mp3
|
105
|
+
- spec/invalid.mp3
|
106
|
+
- spec/playback.mp3
|
94
107
|
homepage: http://github.com/yjerem/plllayer
|
95
108
|
licenses:
|
96
109
|
- MIT
|