mmplayer 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3d59002ee9d5711a811c980108d0e29c41b19811
4
- data.tar.gz: 66a9b65bfb2b421e4fdcba73a04eec30123610af
3
+ metadata.gz: 14f79ab624c5547f520491380f1fd012f649daad
4
+ data.tar.gz: a2387b2bdd5e1787dbb3a5913cd9518633a69b58
5
5
  SHA512:
6
- metadata.gz: a6b900ed7f38488e1f8705a0f4facdf0cf0dc6d44b0fae68c4fb2d06cb5b55b262637890d0c737640b7ca345df1d882a01cbfe1f5b505b44a619db4a2e8461cd
7
- data.tar.gz: 9d8b70983896579a071930a32dc9a4de903694383fb7d30cdb4efcf144125ec6dd2ef2d0bab7c78851e6310102f9f1e98333dd3d2bca22c1519b04bf66992014
6
+ metadata.gz: 10967965c48de9cb4053ce32b41b907b18e1ecc54b33e23f4c9cd63f8d1ca6c7f991ed02fc2694fd5bbd42774874f6eb9a8cafdfcf77b8196a3e9e92f345d57f
7
+ data.tar.gz: 1c21b76e1044de417220d6706bba6b1300f11d90da9e437ef0a14ecaa1cae550738298b3f679414acfd32946fda2444be6bfb597ca2d897ed717c0e7b5d8deb9
@@ -13,18 +13,16 @@ require "unimidi"
13
13
 
14
14
  # modules
15
15
  require "mmplayer/helper/numbers"
16
- require "mmplayer/instructions/midi"
17
- require "mmplayer/instructions/player"
16
+ require "mmplayer/instructions"
17
+ require "mmplayer/midi"
18
+ require "mmplayer/player"
18
19
 
19
20
  # classes
20
21
  require "mmplayer/context"
21
- require "mmplayer/message_handler"
22
- require "mmplayer/midi"
23
- require "mmplayer/player"
24
22
 
25
23
  module MMPlayer
26
24
 
27
- VERSION = "0.0.7"
25
+ VERSION = "0.0.8"
28
26
 
29
27
  # Shortcut to Context constructor
30
28
  def self.new(*args, &block)
@@ -15,8 +15,8 @@ module MMPlayer
15
15
  # @option options [Fixnum] :receive_channel (also: :rx_channel) A MIDI channel to subscribe to. By default, responds to all
16
16
  # @yield
17
17
  def initialize(midi_input, options = {}, &block)
18
- @midi = MIDI.new(midi_input, :receive_channel => options[:receive_channel] || options[:rx_channel])
19
- @player = Player.new(:flags => options[:mplayer_flags])
18
+ @midi = MIDI::Wrapper.new(midi_input, :receive_channel => options[:receive_channel] || options[:rx_channel])
19
+ @player = Player::Wrapper.new(:flags => options[:mplayer_flags])
20
20
  instance_eval(&block) if block_given?
21
21
  end
22
22
 
@@ -0,0 +1,9 @@
1
+ require "mmplayer/instructions/midi"
2
+ require "mmplayer/instructions/player"
3
+
4
+ module MMPlayer
5
+
6
+ module Instructions
7
+ end
8
+
9
+ end
@@ -5,6 +5,14 @@ module MMPlayer
5
5
  # Instructions dealing with the MPlayer
6
6
  module Player
7
7
 
8
+ # Assign a callback for updating progress
9
+ # @param [Proc] callback The callback to execute when progress is updated
10
+ # @return [Hash]
11
+ def on_progress(&callback)
12
+ @player.add_progress_callback(&callback)
13
+ end
14
+ alias_method :progress, :on_progress
15
+
8
16
  # Assign a callback for when a file finishes playback
9
17
  # @param [Proc] callback The callback to execute when a file finishes playback
10
18
  # @return [Hash]
@@ -19,7 +27,7 @@ module MMPlayer
19
27
  # Add delegators to local player methods
20
28
  def self.included(base)
21
29
  base.send(:extend, Forwardable)
22
- base.send(:def_delegators, :@player, :active?, :play, :progress)
30
+ base.send(:def_delegators, :@player, :active?, :play)
23
31
  end
24
32
 
25
33
  # Add all of the MPlayer::Slave methods to the context as instructions
@@ -1,83 +1,9 @@
1
- module MMPlayer
2
-
3
- # Wrapper for MIDI functionality
4
- class MIDI
5
-
6
- attr_reader :channel, :config, :listener, :message_handler
7
-
8
- # @param [UniMIDI::Input] input
9
- # @param [Hash] options
10
- # @option options [Fixnum] :receive_channel A MIDI channel to subscribe to. By default, responds to all
11
- def initialize(input, options = {})
12
- @channel = options[:receive_channel]
13
- @message_handler = MessageHandler.new
14
- @listener = MIDIEye::Listener.new(input)
15
- end
16
-
17
- # Add a callback for a given MIDI system message
18
- # @param [String, Symbol] command The MIDI system command eg :start, :stop
19
- # @param [Proc] callback The callback to execute when the given MIDI command is received
20
- # @return [Hash]
21
- def add_system_callback(command, &callback)
22
- @message_handler.add_callback(:system, command, &callback)
23
- end
24
-
25
- # Add a callback for a given MIDI note
26
- # @param [Fixnum, String, nil] note The MIDI note to add a callback for eg 64 "E4"
27
- # @param [Proc] callback The callback to execute when the given MIDI note is received
28
- # @return [Hash]
29
- def add_note_callback(note, &callback)
30
- @message_handler.add_note_callback(note, &callback)
31
- end
32
-
33
- # Add a callback for a given MIDI control change
34
- # @param [Fixnum, nil] index The MIDI control change index to add a callback for eg 10
35
- # @param [Proc] callback The callback to execute when the given MIDI control change is received
36
- # @return [Hash]
37
- def add_cc_callback(index, &callback)
38
- @message_handler.add_callback(:cc, index, &callback)
39
- end
1
+ require "mmplayer/midi/message_handler"
2
+ require "mmplayer/midi/wrapper"
40
3
 
41
- # Stop the MIDI listener
42
- # @return [Boolean]
43
- def stop
44
- @listener.stop
45
- end
46
-
47
- # Change the subscribed MIDI channel (or nil for all)
48
- # @param [Fixnum, nil] channel
49
- # @return [Fixnum, nil]
50
- def channel=(channel)
51
- @listener.event.clear
52
- @channel = channel
53
- populate_listener if @listener.running?
54
- @channel
55
- end
56
-
57
- # Start the MIDI listener
58
- # @return [Boolean]
59
- def start
60
- populate_listener
61
- @listener.start(:background => true)
62
- true
63
- end
64
-
65
- # Whether the player is subscribed to all channels
66
- # @return [Boolean]
67
- def omni?
68
- @channel.nil?
69
- end
70
-
71
- private
72
-
73
- # Populate the MIDI listener callback
74
- def populate_listener
75
- @listener.on_message do |event|
76
- message = event[:message]
77
- @message_handler.process(@channel, message)
78
- end
79
- end
4
+ module MMPlayer
80
5
 
6
+ module MIDI
81
7
  end
82
8
 
83
9
  end
@@ -0,0 +1,112 @@
1
+ module MMPlayer
2
+
3
+ module MIDI
4
+ # Directs what should happen when messages are received
5
+ class MessageHandler
6
+
7
+ attr_reader :callback
8
+
9
+ def initialize
10
+ @callback = {
11
+ :cc => {},
12
+ :note => {},
13
+ :system => {}
14
+ }
15
+ end
16
+
17
+ # Add a callback for a given MIDI message type
18
+ # @param [Symbol] type The MIDI message type (eg :note, :cc)
19
+ # @param [Fixnum, String] key The ID of the message eg note number/cc index
20
+ # @param [Proc] callback The callback to execute when the given MIDI command is received
21
+ # @return [Hash]
22
+ def add_callback(type, key, &callback)
23
+ @callback[type][key] = callback
24
+ @callback[type]
25
+ end
26
+
27
+ # Add a callback for a given MIDI note
28
+ # @param [Symbol] type The MIDI message type (eg :note, :cc)
29
+ # @param [Fixnum, String] note
30
+ # @param [Proc] callback The callback to execute when the given MIDI command is received
31
+ # @return [Hash]
32
+ def add_note_callback(note, &callback)
33
+ note = MIDIMessage::Constant.value(:note, note) if note.kind_of?(String)
34
+ add_callback(:note, note, &callback)
35
+ end
36
+
37
+ # Process a message for the given channel
38
+ # @param [Fixnum, nil] channel
39
+ # @param [MIDIMessage] message
40
+ # @return [Boolean, nil]
41
+ def process(channel, message)
42
+ case message
43
+ when MIDIMessage::SystemCommon, MIDIMessage::SystemRealtime then system_message(message)
44
+ else
45
+ channel_message(channel, message)
46
+ end
47
+ end
48
+
49
+ # Find and call a note received callback if it exists
50
+ # @param [MIDIMessage] message
51
+ # @return [Boolean, nil]
52
+ def note_message(message)
53
+ call_callback(:note, message.note, message.velocity) |
54
+ call_catch_all_callback(:note, message)
55
+ end
56
+
57
+ # Find and call a cc received callback if it exists
58
+ # @param [MIDIMessage] message
59
+ # @return [Boolean, nil]
60
+ def cc_message(message)
61
+ call_callback(:cc, message.index, message.value) |
62
+ call_catch_all_callback(:cc, message)
63
+ end
64
+
65
+ # Find and call a system message callback if it exists
66
+ # @param [MIDIMessage] message
67
+ # @return [Boolean, nil]
68
+ def system_message(message)
69
+ name = message.name.downcase.to_sym
70
+ call_callback(:system, name)
71
+ end
72
+
73
+ # Find and call a channel message callback if it exists for the given message and channel
74
+ # @param [Fixnum, nil] channel
75
+ # @param [MIDIMessage] message
76
+ # @return [Boolean, nil]
77
+ def channel_message(channel, message)
78
+ if channel.nil? || message.channel == channel
79
+ case message
80
+ when MIDIMessage::NoteOn then note_message(message)
81
+ when MIDIMessage::ControlChange then cc_message(message)
82
+ end
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ # Execute the catch-all callback for the given type if it exists
89
+ # @param [Symbol] type
90
+ # @param [MIDIMessage] message
91
+ # @return [Boolean]
92
+ def call_catch_all_callback(type, message)
93
+ call_callback(type, nil, message)
94
+ end
95
+
96
+ # Execute the callback for the given type and key and pass it the given args
97
+ # @param [Symbol] type
98
+ # @param [Object] key
99
+ # @param [*Object] arguments
100
+ # @return [Boolean]
101
+ def call_callback(type, key, *arguments)
102
+ unless (callback = @callback[type][key]).nil?
103
+ callback.call(*arguments)
104
+ true
105
+ end
106
+ end
107
+
108
+ end
109
+
110
+ end
111
+
112
+ end
@@ -0,0 +1,85 @@
1
+ module MMPlayer
2
+
3
+ module MIDI
4
+ # Wrapper for MIDI functionality
5
+ class Wrapper
6
+
7
+ attr_reader :channel, :config, :listener, :message_handler
8
+
9
+ # @param [UniMIDI::Input] input
10
+ # @param [Hash] options
11
+ # @option options [Fixnum] :receive_channel A MIDI channel to subscribe to. By default, responds to all
12
+ def initialize(input, options = {})
13
+ @channel = options[:receive_channel]
14
+ @message_handler = MessageHandler.new
15
+ @listener = MIDIEye::Listener.new(input)
16
+ end
17
+
18
+ # Add a callback for a given MIDI system message
19
+ # @param [String, Symbol] command The MIDI system command eg :start, :stop
20
+ # @param [Proc] callback The callback to execute when the given MIDI command is received
21
+ # @return [Hash]
22
+ def add_system_callback(command, &callback)
23
+ @message_handler.add_callback(:system, command, &callback)
24
+ end
25
+
26
+ # Add a callback for a given MIDI note
27
+ # @param [Fixnum, String, nil] note The MIDI note to add a callback for eg 64 "E4"
28
+ # @param [Proc] callback The callback to execute when the given MIDI note is received
29
+ # @return [Hash]
30
+ def add_note_callback(note, &callback)
31
+ @message_handler.add_note_callback(note, &callback)
32
+ end
33
+
34
+ # Add a callback for a given MIDI control change
35
+ # @param [Fixnum, nil] index The MIDI control change index to add a callback for eg 10
36
+ # @param [Proc] callback The callback to execute when the given MIDI control change is received
37
+ # @return [Hash]
38
+ def add_cc_callback(index, &callback)
39
+ @message_handler.add_callback(:cc, index, &callback)
40
+ end
41
+
42
+ # Stop the MIDI listener
43
+ # @return [Boolean]
44
+ def stop
45
+ @listener.stop
46
+ end
47
+
48
+ # Change the subscribed MIDI channel (or nil for all)
49
+ # @param [Fixnum, nil] channel
50
+ # @return [Fixnum, nil]
51
+ def channel=(channel)
52
+ @listener.event.clear
53
+ @channel = channel
54
+ populate_listener if @listener.running?
55
+ @channel
56
+ end
57
+
58
+ # Start the MIDI listener
59
+ # @return [Boolean]
60
+ def start
61
+ populate_listener
62
+ @listener.start(:background => true)
63
+ true
64
+ end
65
+
66
+ # Whether the player is subscribed to all channels
67
+ # @return [Boolean]
68
+ def omni?
69
+ @channel.nil?
70
+ end
71
+
72
+ private
73
+
74
+ # Populate the MIDI listener callback
75
+ def populate_listener
76
+ @listener.on_message do |event|
77
+ message = event[:message]
78
+ @message_handler.process(@channel, message)
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+ end
@@ -1,248 +1,11 @@
1
- module MMPlayer
2
-
3
- # Wrapper for MPlayer functionality
4
- class Player
5
-
6
- # @param [Hash] options
7
- # @option options [String] :flags MPlayer command-line flags to use on startup
8
- def initialize(options = {})
9
- @flags = "-fixed-vo -idle"
10
- @flags += " #{options[:flags]}" unless options[:flags].nil?
11
- @messenger = Messenger.new
12
- @callback = {}
13
- @playback_state = {
14
- :eof => false,
15
- :play => false,
16
- :pause => false
17
- }
18
- @player_threads = []
19
- end
20
-
21
- # Play a media file
22
- # @param [String] file
23
- # @return [Boolean]
24
- def play(file)
25
- ensure_player(file)
26
- if @player.nil?
27
- false
28
- else
29
- with_player_thread do
30
- @player.load_file(file)
31
- handle_start
32
- end
33
- true
34
- end
35
- end
36
-
37
- # Is MPlayer active?
38
- # @return [Boolean]
39
- def active?
40
- !@player.nil?
41
- end
42
-
43
- # Is the player paused?
44
- # @return [Boolean]
45
- def paused?
46
- @playback_state[:pause]
47
- end
48
- alias_method :pause?, :paused?
49
-
50
- # Toggles pause
51
- # @return [Boolean]
52
- def pause
53
- @playback_state[:pause] = !@playback_state[:pause]
54
- @player.pause
55
- @playback_state[:pause]
56
- end
57
-
58
- # Handle events while the player is running
59
- # @return [Boolean]
60
- def playback_loop
61
- loop do
62
- handle_eof if eof?
63
- sleep(0.05)
64
- end
65
- true
66
- end
67
-
68
- # Add a callback to be called at the end of playback of a media file
69
- # @param [Proc] block
70
- # @return [Boolean]
71
- def add_end_of_file_callback(&block)
72
- @callback[:end_of_file] = block
73
- true
74
- end
75
-
76
- # Media progress information
77
- # eg {
78
- # :length => 90.3,
79
- # :percent => 44,
80
- # :position => 40.1
81
- # }
82
- # Length and position are in seconds
83
- # @return [Hash, nil]
84
- def progress
85
- unless (time = poll_mplayer_progress).nil?
86
- time[:percent] = get_percentage(time)
87
- time
88
- end
89
- end
90
-
91
- # Shortcut to send a message to the MPlayer
92
- # @return [Object]
93
- def mplayer_send(method, *args, &block)
94
- if @player.nil? && MPlayer::Slave.method_defined?(method)
95
- # warn
96
- else
97
- @messenger.send_message { @player.send(method, *args, &block) }
98
- end
99
- end
100
-
101
- # Does the MPlayer respond to the given message?
102
- # @return [Boolean]
103
- def mplayer_respond_to?(method, include_private = false)
104
- (@player.nil? && MPlayer::Slave.method_defined?(method)) ||
105
- @player.respond_to?(method)
106
- end
107
-
108
- # Cause MPlayer to exit
109
- # @return [Boolean]
110
- def quit
111
- @player.quit
112
- @player_threads.each(&:kill)
113
- true
114
- end
115
-
116
- private
117
-
118
- # Has the end of a media file been reached?
119
- # @return [Boolean]
120
- def eof?
121
- @playback_state[:play] && !@playback_state[:eof] && !@playback_state[:pause] && get_player_output.size < 1
122
- end
123
-
124
- # Get player output from stdout
125
- def get_player_output
126
- @player.stdout.gets.inspect.strip.gsub(/(\\n|[\\"])/, '').strip
127
- end
1
+ require "mmplayer/player/invoker"
2
+ require "mmplayer/player/messenger"
3
+ require "mmplayer/player/state"
4
+ require "mmplayer/player/wrapper"
128
5
 
129
- # Handle the end of playback for a single media file
130
- def handle_eof
131
- @playback_state[:eof] = true
132
- @playback_state[:play] = false
133
- STDOUT.flush
134
- @callback[:end_of_file].call unless @callback[:end_of_file].nil?
135
- true
136
- end
137
-
138
- # Handle the beginning of playback for a single media file
139
- def handle_start
140
- loop until get_player_output.size > 1
141
- @playback_state[:play] = true
142
- @playback_state[:eof] = false
143
- end
144
-
145
- # Get progress percentage from the MPlayer report
146
- def get_percentage(report)
147
- percent = (report[:position] / report[:length]) * 100
148
- percent.round
149
- end
150
-
151
- # Poll MPlayer for progress information
152
- def poll_mplayer_progress
153
- time = nil
154
- @messenger.send_message do
155
- time = {
156
- :length => get_mplayer_float("time_length"),
157
- :position => get_mplayer_float("time_pos")
158
- }
159
- end
160
- time
161
- end
162
-
163
- # Poll a single MPlayer value for the given key
164
- def get_mplayer_float(key)
165
- @player.get(key).strip.to_f
166
- end
167
-
168
- # Call the given block within a new thread
169
- def with_player_thread(&block)
170
- thread = Thread.new do
171
- begin
172
- yield
173
- rescue Exception => exception
174
- Thread.main.raise(exception)
175
- end
176
- end
177
- thread.abort_on_exception = true
178
- @player_threads << thread
179
- thread
180
- end
181
-
182
- # Ensure that the MPlayer process is invoked
183
- # @param [String] file The media file to invoke MPlayer with
184
- # @return [MPlayer::Slave]
185
- def ensure_player(file)
186
- if @player.nil? && @player_threads.empty?
187
- with_player_thread do
188
- @player = MPlayer::Slave.new(file, :options => @flags)
189
- handle_start
190
- end
191
- end
192
- end
193
-
194
- # Handle sending MPlayer messages
195
- class Messenger
196
-
197
- FREQUENCY_LIMIT = 0.1 # Throttle messages to 1 per this number seconds
198
-
199
- def initialize
200
- @messages = []
201
- end
202
-
203
- # Send mplayer a message asynch
204
- # @return [Hash, nil]
205
- def send_message(&block)
206
- timestamp = Time.now.to_f
207
- # Throttled messages are disregarded
208
- if @messages.empty? || !throttle?(timestamp, @messages.last[:timestamp])
209
- thread = Thread.new do
210
- begin
211
- yield
212
- rescue Exception => exception
213
- Thread.main.raise(exception)
214
- end
215
- end
216
- thread.abort_on_exception = true
217
- record_message(thread, timestamp)
218
- end
219
- end
220
-
221
- private
222
-
223
- # Should adding a message be throttled for the given timestamp?
224
- # @param [Float] timestamp
225
- # @param [Float] last_timestamp
226
- # @return [Boolean]
227
- def throttle?(timestamp, last_timestamp)
228
- timestamp - last_timestamp <= FREQUENCY_LIMIT
229
- end
230
-
231
- # Record that a message has been sent
232
- # @param [Thread] thread
233
- # @param [Float] timestamp
234
- # @return [Hash]
235
- def record_message(thread, timestamp)
236
- message = {
237
- :thread => thread,
238
- :timestamp => timestamp
239
- }
240
- @messages << message
241
- message
242
- end
243
-
244
- end
6
+ module MMPlayer
245
7
 
8
+ module Player
246
9
  end
247
10
 
248
11
  end
@@ -0,0 +1,45 @@
1
+ module MMPlayer
2
+
3
+ module Player
4
+
5
+ # Invoke MPlayer
6
+ class Invoker
7
+
8
+ attr_reader :player, :thread
9
+
10
+ # @param [Hash] options
11
+ # @option options [String] :flags MPlayer command-line flags to use on startup
12
+ def initialize(options = {})
13
+ @flags = "-fixed-vo -idle"
14
+ @flags += " #{options[:flags]}" unless options[:flags].nil?
15
+ @player.nil?
16
+ @thread = nil
17
+ end
18
+
19
+ def destroy
20
+ @thread.kill unless @thread.nil?
21
+ end
22
+
23
+ # Ensure that the MPlayer process is invoked
24
+ # @param [String] file The media file to invoke MPlayer with
25
+ # @param [MMplayer::Player::State] state
26
+ # @return [MPlayer::Slave]
27
+ def ensure_invoked(file, state)
28
+ if @player.nil? && @thread.nil?
29
+ @thread = Thread.new do
30
+ begin
31
+ @player = MPlayer::Slave.new(file, :options => @flags)
32
+ state.handle_start
33
+ rescue Exception => exception
34
+ Thread.main.raise(exception)
35
+ end
36
+ end
37
+ @thread.abort_on_exception
38
+ end
39
+ @player
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,57 @@
1
+ module MMPlayer
2
+
3
+ module Player
4
+
5
+ # Handle sending MPlayer messages
6
+ class Messenger
7
+
8
+ FREQUENCY_LIMIT = 0.1 # Throttle messages to 1 per this number seconds
9
+
10
+ def initialize
11
+ @messages = []
12
+ end
13
+
14
+ # Send mplayer a message asynch
15
+ # @return [Hash, nil]
16
+ def send_message(&block)
17
+ timestamp = Time.now.to_f
18
+ # Throttled messages are disregarded
19
+ if @messages.empty? || !throttle?(timestamp, @messages.last[:timestamp])
20
+ thread = Thread.new do
21
+ begin
22
+ yield
23
+ rescue Exception => exception
24
+ Thread.main.raise(exception)
25
+ end
26
+ end
27
+ thread.abort_on_exception = true
28
+ record_message(thread, timestamp)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ # Should adding a message be throttled for the given timestamp?
35
+ # @param [Float] timestamp
36
+ # @param [Float] last_timestamp
37
+ # @return [Boolean]
38
+ def throttle?(timestamp, last_timestamp)
39
+ timestamp - last_timestamp <= FREQUENCY_LIMIT
40
+ end
41
+
42
+ # Record that a message has been sent
43
+ # @param [Thread] thread
44
+ # @param [Float] timestamp
45
+ # @return [Hash]
46
+ def record_message(thread, timestamp)
47
+ message = {
48
+ :thread => thread,
49
+ :timestamp => timestamp
50
+ }
51
+ @messages << message
52
+ message
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,44 @@
1
+ module MMPlayer
2
+
3
+ module Player
4
+
5
+ class State
6
+
7
+ attr_accessor :eof, :pause, :play
8
+ alias_method :eof?, :eof
9
+ alias_method :pause?, :pause
10
+ alias_method :paused?, :pause
11
+ alias_method :play?, :play
12
+ alias_method :playing?, :play
13
+
14
+ def initialize
15
+ @eof = false
16
+ @play = false
17
+ @pause = false
18
+ end
19
+
20
+ def toggle_pause
21
+ @pause = !@pause
22
+ end
23
+
24
+ def progressing?
25
+ @play && !@pause
26
+ end
27
+
28
+ def eof_reached?
29
+ @play && !@eof && !@pause
30
+ end
31
+
32
+ def handle_eof
33
+ @eof = true
34
+ @play = false
35
+ end
36
+
37
+ def handle_start
38
+ @play = true
39
+ @eof = false
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,209 @@
1
+ module MMPlayer
2
+
3
+ module Player
4
+
5
+ # Wrapper for MPlayer functionality
6
+ class Wrapper
7
+
8
+ attr_reader :player, :state
9
+
10
+ # @param [Hash] options
11
+ # @option options [String] :flags MPlayer command-line flags to use on startup
12
+ def initialize(options = {})
13
+ @invoker = Invoker.new(options)
14
+ @messenger = Messenger.new
15
+ @callback = {}
16
+ @state = State.new
17
+ @threads = []
18
+ end
19
+
20
+ # Play a media file
21
+ # @param [String] file
22
+ # @return [Boolean]
23
+ def play(file)
24
+ @player ||= @invoker.ensure_invoked(file, @state)
25
+ if @player.nil?
26
+ false
27
+ else
28
+ @threads << with_thread do
29
+ @player.load_file(file)
30
+ handle_start
31
+ end
32
+ true
33
+ end
34
+ end
35
+
36
+ # Is MPlayer active?
37
+ # @return [Boolean]
38
+ def active?
39
+ !(@player ||= @invoker.player).nil?
40
+ end
41
+
42
+ # Toggles pause
43
+ # @return [Boolean]
44
+ def pause
45
+ @state.toggle_pause
46
+ @player.pause
47
+ @state.pause?
48
+ end
49
+
50
+ # Handle events while the player is running
51
+ # @return [Boolean]
52
+ def playback_loop
53
+ loop do
54
+ if handle_progress?
55
+ @threads << with_thread { handle_progress }
56
+ end
57
+ handle_eof if handle_eof?
58
+ sleep(0.05)
59
+ end
60
+ true
61
+ end
62
+
63
+ # Add a callback to be called when progress is updated during playback
64
+ # @param [Proc] block
65
+ # @return [Boolean]
66
+ def add_progress_callback(&block)
67
+ @callback[:progress] = block
68
+ true
69
+ end
70
+
71
+ # Add a callback to be called at the end of playback of a media file
72
+ # @param [Proc] block
73
+ # @return [Boolean]
74
+ def add_end_of_file_callback(&block)
75
+ @callback[:end_of_file] = block
76
+ true
77
+ end
78
+
79
+ # Shortcut to send a message to the MPlayer
80
+ # @return [Object]
81
+ def mplayer_send(method, *args, &block)
82
+ if @player.nil? && MPlayer::Slave.method_defined?(method)
83
+ # warn
84
+ else
85
+ @messenger.send_message do
86
+ @player.send(method, *args, &block)
87
+ end
88
+ end
89
+ end
90
+
91
+ # Does the MPlayer respond to the given message?
92
+ # @return [Boolean]
93
+ def mplayer_respond_to?(method, include_private = false)
94
+ (@player.nil? && MPlayer::Slave.method_defined?(method)) ||
95
+ @player.respond_to?(method)
96
+ end
97
+
98
+ # Cause MPlayer to exit
99
+ # @return [Boolean]
100
+ def quit
101
+ @player.quit
102
+ @threads.each(&:kill)
103
+ @invoker.destroy
104
+ true
105
+ end
106
+
107
+ private
108
+
109
+ def handle_progress?
110
+ @state.progressing? && progress_callback?
111
+ end
112
+
113
+ def progress_callback?
114
+ !@callback[:progress].nil?
115
+ end
116
+
117
+ def eof_callback?
118
+ !@callback[:end_of_file].nil?
119
+ end
120
+
121
+ def handle_eof?
122
+ eof? && eof_callback?
123
+ end
124
+
125
+ # Has the end of a media file been reached?
126
+ # @return [Boolean]
127
+ def eof?
128
+ @state.eof_reached? && get_player_output.size < 1
129
+ end
130
+
131
+ # Get player output from stdout
132
+ def get_player_output
133
+ @player.stdout.gets.inspect.strip.gsub(/(\\n|[\\"])/, '').strip
134
+ end
135
+
136
+ def handle_progress
137
+ poll_mplayer_progress do |time|
138
+ time[:percent] = get_percentage(time)
139
+ # do the check again for thread safety
140
+ @callback[:progress].call(time) if handle_progress?
141
+ end
142
+ end
143
+
144
+ # Handle the end of playback for a single media file
145
+ def handle_eof
146
+ # do this check again for thread safety
147
+ if @state.eof_reached?
148
+ STDOUT.flush
149
+ @callback[:end_of_file].call
150
+ @state.handle_eof
151
+ end
152
+ true
153
+ end
154
+
155
+ # Handle the beginning of playback for a single media file
156
+ def handle_start
157
+ loop until get_player_output.size > 1
158
+ @state.handle_start
159
+ end
160
+
161
+ # Get progress percentage from the MPlayer report
162
+ def get_percentage(report)
163
+ percent = (report[:position] / report[:length]) * 100
164
+ percent.round
165
+ end
166
+
167
+ # Poll MPlayer for progress information
168
+ # Media progress information
169
+ # eg {
170
+ # :length => 90.3,
171
+ # :percent => 44,
172
+ # :position => 40.1
173
+ # }
174
+ # Length and position are in seconds
175
+ def poll_mplayer_progress(&block)
176
+ time = nil
177
+ @messenger.send_message do
178
+ time = {
179
+ :length => get_mplayer_float("time_length"),
180
+ :position => get_mplayer_float("time_pos")
181
+ }
182
+ yield(time)
183
+ end
184
+ time
185
+ end
186
+
187
+ # Poll a single MPlayer value for the given key
188
+ def get_mplayer_float(key)
189
+ result = @player.get(key)
190
+ result.strip.to_f
191
+ end
192
+
193
+ # Call the given block within a new thread
194
+ def with_thread(&block)
195
+ thread = Thread.new do
196
+ begin
197
+ yield
198
+ rescue Exception => exception
199
+ Thread.main.raise(exception)
200
+ end
201
+ end
202
+ thread.abort_on_exception = true
203
+ thread
204
+ end
205
+
206
+ end
207
+
208
+ end
209
+ end
@@ -8,13 +8,13 @@ class MMPlayer::ContextTest < Minitest::Test
8
8
  @input = Object.new
9
9
  @context = MMPlayer::Context.new(@input)
10
10
  @player = Object.new
11
- @context.player.stubs(:ensure_player).returns(@player)
11
+ @context.player.stubs(:player).returns(@player)
12
12
  @context.player.stubs(:quit).returns(true)
13
13
  @context.player.stubs(:active?).returns(true)
14
14
  end
15
15
 
16
16
  teardown do
17
- @context.player.unstub(:ensure_player)
17
+ @context.player.unstub(:player)
18
18
  @context.player.unstub(:quit)
19
19
  @context.player.unstub(:active?)
20
20
  end
@@ -10,6 +10,22 @@ class MMPlayer::Instructions::PlayerTest < Minitest::Test
10
10
  assert @context.kind_of?(MMPlayer::Instructions::Player)
11
11
  end
12
12
 
13
+ context "#on_end_of_file" do
14
+
15
+ setup do
16
+ @context.player.expects(:add_end_of_file_callback).once.returns({})
17
+ end
18
+
19
+ teardown do
20
+ @context.player.unstub(:add_end_of_file_callback)
21
+ end
22
+
23
+ should "assign callback" do
24
+ refute_nil @context.on_end_of_file { something }
25
+ end
26
+
27
+ end
28
+
13
29
  context "#method_missing" do
14
30
 
15
31
  setup do
@@ -1,11 +1,11 @@
1
1
  require "helper"
2
2
 
3
- class MMPlayer::MessageHandlerTest < Minitest::Test
3
+ class MMPlayer::MIDI::MessageHandlerTest < Minitest::Test
4
4
 
5
5
  context "MessageHandler" do
6
6
 
7
7
  setup do
8
- @handler = MMPlayer::MessageHandler.new
8
+ @handler = MMPlayer::MIDI::MessageHandler.new
9
9
  end
10
10
 
11
11
  context "#note_message" do
@@ -6,7 +6,7 @@ class MMPlayer::MIDITest < Minitest::Test
6
6
 
7
7
  setup do
8
8
  @input = Object.new
9
- @midi = MMPlayer::MIDI.new(@input)
9
+ @midi = MMPlayer::MIDI::Wrapper.new(@input)
10
10
  end
11
11
 
12
12
  context "#start" do
@@ -5,17 +5,16 @@ class MMPlayer::PlayerTest < Minitest::Test
5
5
  context "Player" do
6
6
 
7
7
  setup do
8
- @player = MMPlayer::Player.new
8
+ @player = MMPlayer::Player::Wrapper.new
9
9
  @mplayer = Object.new
10
10
  @mplayer.stubs(:load_file).returns(true)
11
11
  out = Object.new
12
12
  out.stubs(:gets).returns("")
13
13
  @mplayer.stubs(:stdout).returns(out)
14
14
  @mplayer.stubs(:get).returns("0.1\n")
15
- @player.stubs(:ensure_player).returns(@mplayer)
15
+ @player.stubs(:player).returns(@mplayer)
16
16
  @player.instance_variable_set("@player", @mplayer)
17
- @player.instance_variable_set("@player_threads", [Thread.new {}])
18
- @player.send(:ensure_player, "")
17
+ @player.instance_variable_set("@threads", [Thread.new {}])
19
18
  end
20
19
 
21
20
  context "#mplayer_send" do
@@ -38,7 +37,6 @@ class MMPlayer::PlayerTest < Minitest::Test
38
37
  context "#mplayer_respond_to?" do
39
38
 
40
39
  setup do
41
- @player.send(:ensure_player, "")
42
40
  @mplayer.expects(:respond_to?).with(:hello).once.returns(true)
43
41
  end
44
42
 
@@ -67,12 +65,12 @@ class MMPlayer::PlayerTest < Minitest::Test
67
65
 
68
66
  setup do
69
67
  @mplayer.expects(:quit).once
70
- @player.instance_variable_get("@player_threads").first.expects(:kill).once
68
+ @player.instance_variable_get("@threads").first.expects(:kill).once
71
69
  end
72
70
 
73
71
  teardown do
74
72
  @mplayer.unstub(:quit)
75
- @player.instance_variable_get("@player_threads").first.unstub(:kill)
73
+ @player.instance_variable_get("@threads").first.unstub(:kill)
76
74
  end
77
75
 
78
76
  should "exit MPlayer and kill the player thread" do
@@ -103,11 +101,11 @@ class MMPlayer::PlayerTest < Minitest::Test
103
101
  context "#play" do
104
102
 
105
103
  setup do
106
- @player.expects(:ensure_player).once.returns(@mplayer)
104
+ @player.expects(:player).once.returns(@mplayer)
107
105
  end
108
106
 
109
107
  teardown do
110
- @player.unstub(:ensure_player)
108
+ @player.unstub(:player)
111
109
  end
112
110
 
113
111
  should "lazily invoke mplayer and play" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mmplayer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ari Russo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-08 00:00:00.000000000 Z
11
+ date: 2015-05-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -190,7 +190,7 @@ dependencies:
190
190
  - - ">="
191
191
  - !ruby/object:Gem::Version
192
192
  version: 0.4.3
193
- description: Define interactions between MIDI input and MPlayer
193
+ description: Control MPlayer with MIDI
194
194
  email:
195
195
  - ari.russo@gmail.com
196
196
  executables: []
@@ -202,17 +202,23 @@ files:
202
202
  - lib/mmplayer.rb
203
203
  - lib/mmplayer/context.rb
204
204
  - lib/mmplayer/helper/numbers.rb
205
+ - lib/mmplayer/instructions.rb
205
206
  - lib/mmplayer/instructions/midi.rb
206
207
  - lib/mmplayer/instructions/player.rb
207
- - lib/mmplayer/message_handler.rb
208
208
  - lib/mmplayer/midi.rb
209
+ - lib/mmplayer/midi/message_handler.rb
210
+ - lib/mmplayer/midi/wrapper.rb
209
211
  - lib/mmplayer/player.rb
212
+ - lib/mmplayer/player/invoker.rb
213
+ - lib/mmplayer/player/messenger.rb
214
+ - lib/mmplayer/player/state.rb
215
+ - lib/mmplayer/player/wrapper.rb
210
216
  - test/context_test.rb
211
217
  - test/helper.rb
212
218
  - test/helper/numbers_test.rb
213
219
  - test/instructions/midi_test.rb
214
220
  - test/instructions/player_test.rb
215
- - test/message_handler_test.rb
221
+ - test/midi/message_handler_test.rb
216
222
  - test/midi_test.rb
217
223
  - test/player_test.rb
218
224
  homepage: http://github.com/arirusso/mmplayer
@@ -1,109 +0,0 @@
1
- module MMPlayer
2
-
3
- # Directs what should happen when messages are received
4
- class MessageHandler
5
-
6
- attr_reader :callback
7
-
8
- def initialize
9
- @callback = {
10
- :cc => {},
11
- :note => {},
12
- :system => {}
13
- }
14
- end
15
-
16
- # Add a callback for a given MIDI message type
17
- # @param [Symbol] type The MIDI message type (eg :note, :cc)
18
- # @param [Fixnum, String] key The ID of the message eg note number/cc index
19
- # @param [Proc] callback The callback to execute when the given MIDI command is received
20
- # @return [Hash]
21
- def add_callback(type, key, &callback)
22
- @callback[type][key] = callback
23
- @callback[type]
24
- end
25
-
26
- # Add a callback for a given MIDI note
27
- # @param [Symbol] type The MIDI message type (eg :note, :cc)
28
- # @param [Fixnum, String] note
29
- # @param [Proc] callback The callback to execute when the given MIDI command is received
30
- # @return [Hash]
31
- def add_note_callback(note, &callback)
32
- note = MIDIMessage::Constant.value(:note, note) if note.kind_of?(String)
33
- add_callback(:note, note, &callback)
34
- end
35
-
36
- # Process a message for the given channel
37
- # @param [Fixnum, nil] channel
38
- # @param [MIDIMessage] message
39
- # @return [Boolean, nil]
40
- def process(channel, message)
41
- case message
42
- when MIDIMessage::SystemCommon, MIDIMessage::SystemRealtime then system_message(message)
43
- else
44
- channel_message(channel, message)
45
- end
46
- end
47
-
48
- # Find and call a note received callback if it exists
49
- # @param [MIDIMessage] message
50
- # @return [Boolean, nil]
51
- def note_message(message)
52
- call_callback(:note, message.note, message.velocity) |
53
- call_catch_all_callback(:note, message)
54
- end
55
-
56
- # Find and call a cc received callback if it exists
57
- # @param [MIDIMessage] message
58
- # @return [Boolean, nil]
59
- def cc_message(message)
60
- call_callback(:cc, message.index, message.value) |
61
- call_catch_all_callback(:cc, message)
62
- end
63
-
64
- # Find and call a system message callback if it exists
65
- # @param [MIDIMessage] message
66
- # @return [Boolean, nil]
67
- def system_message(message)
68
- name = message.name.downcase.to_sym
69
- call_callback(:system, name)
70
- end
71
-
72
- # Find and call a channel message callback if it exists for the given message and channel
73
- # @param [Fixnum, nil] channel
74
- # @param [MIDIMessage] message
75
- # @return [Boolean, nil]
76
- def channel_message(channel, message)
77
- if channel.nil? || message.channel == channel
78
- case message
79
- when MIDIMessage::NoteOn then note_message(message)
80
- when MIDIMessage::ControlChange then cc_message(message)
81
- end
82
- end
83
- end
84
-
85
- private
86
-
87
- # Execute the catch-all callback for the given type if it exists
88
- # @param [Symbol] type
89
- # @param [MIDIMessage] message
90
- # @return [Boolean]
91
- def call_catch_all_callback(type, message)
92
- call_callback(type, nil, message)
93
- end
94
-
95
- # Execute the callback for the given type and key and pass it the given args
96
- # @param [Symbol] type
97
- # @param [Object] key
98
- # @param [*Object] arguments
99
- # @return [Boolean]
100
- def call_callback(type, key, *arguments)
101
- unless (callback = @callback[type][key]).nil?
102
- callback.call(*arguments)
103
- true
104
- end
105
- end
106
-
107
- end
108
-
109
- end