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 +4 -4
- data/lib/mmplayer.rb +4 -6
- data/lib/mmplayer/context.rb +2 -2
- data/lib/mmplayer/instructions.rb +9 -0
- data/lib/mmplayer/instructions/player.rb +9 -1
- data/lib/mmplayer/midi.rb +4 -78
- data/lib/mmplayer/midi/message_handler.rb +112 -0
- data/lib/mmplayer/midi/wrapper.rb +85 -0
- data/lib/mmplayer/player.rb +6 -243
- data/lib/mmplayer/player/invoker.rb +45 -0
- data/lib/mmplayer/player/messenger.rb +57 -0
- data/lib/mmplayer/player/state.rb +44 -0
- data/lib/mmplayer/player/wrapper.rb +209 -0
- data/test/context_test.rb +2 -2
- data/test/instructions/player_test.rb +16 -0
- data/test/{message_handler_test.rb → midi/message_handler_test.rb} +2 -2
- data/test/midi_test.rb +1 -1
- data/test/player_test.rb +7 -9
- metadata +11 -5
- data/lib/mmplayer/message_handler.rb +0 -109
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 14f79ab624c5547f520491380f1fd012f649daad
|
4
|
+
data.tar.gz: a2387b2bdd5e1787dbb3a5913cd9518633a69b58
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 10967965c48de9cb4053ce32b41b907b18e1ecc54b33e23f4c9cd63f8d1ca6c7f991ed02fc2694fd5bbd42774874f6eb9a8cafdfcf77b8196a3e9e92f345d57f
|
7
|
+
data.tar.gz: 1c21b76e1044de417220d6706bba6b1300f11d90da9e437ef0a14ecaa1cae550738298b3f679414acfd32946fda2444be6bfb597ca2d897ed717c0e7b5d8deb9
|
data/lib/mmplayer.rb
CHANGED
@@ -13,18 +13,16 @@ require "unimidi"
|
|
13
13
|
|
14
14
|
# modules
|
15
15
|
require "mmplayer/helper/numbers"
|
16
|
-
require "mmplayer/instructions
|
17
|
-
require "mmplayer/
|
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.
|
25
|
+
VERSION = "0.0.8"
|
28
26
|
|
29
27
|
# Shortcut to Context constructor
|
30
28
|
def self.new(*args, &block)
|
data/lib/mmplayer/context.rb
CHANGED
@@ -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
|
|
@@ -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
|
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
|
data/lib/mmplayer/midi.rb
CHANGED
@@ -1,83 +1,9 @@
|
|
1
|
-
|
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
|
-
|
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
|
data/lib/mmplayer/player.rb
CHANGED
@@ -1,248 +1,11 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
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
|
-
|
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
|
data/test/context_test.rb
CHANGED
@@ -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(:
|
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(:
|
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
|
data/test/midi_test.rb
CHANGED
data/test/player_test.rb
CHANGED
@@ -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(:
|
15
|
+
@player.stubs(:player).returns(@mplayer)
|
16
16
|
@player.instance_variable_set("@player", @mplayer)
|
17
|
-
@player.instance_variable_set("@
|
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("@
|
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("@
|
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(:
|
104
|
+
@player.expects(:player).once.returns(@mplayer)
|
107
105
|
end
|
108
106
|
|
109
107
|
teardown do
|
110
|
-
@player.unstub(:
|
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.
|
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-
|
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:
|
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
|