mmplayer 0.0.7 → 0.0.8

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.
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