mvlc 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +13 -0
- data/README.md +11 -0
- data/lib/mvlc.rb +34 -0
- data/lib/mvlc/context.rb +67 -0
- data/lib/mvlc/helper/numbers.rb +26 -0
- data/lib/mvlc/instructions.rb +9 -0
- data/lib/mvlc/instructions/midi.rb +46 -0
- data/lib/mvlc/instructions/player.rb +50 -0
- data/lib/mvlc/midi.rb +14 -0
- data/lib/mvlc/midi/message_handler.rb +112 -0
- data/lib/mvlc/midi/wrapper.rb +115 -0
- data/lib/mvlc/player.rb +14 -0
- data/lib/mvlc/player/state.rb +44 -0
- data/lib/mvlc/player/wrapper.rb +148 -0
- data/lib/mvlc/thread.rb +28 -0
- data/test/context_test.rb +65 -0
- data/test/helper.rb +6 -0
- data/test/helper/numbers_test.rb +57 -0
- data/test/instructions/midi_test.rb +78 -0
- data/test/instructions/player_test.rb +66 -0
- data/test/media/1.mov +0 -0
- data/test/media/2.mov +0 -0
- data/test/media/3.mov +0 -0
- data/test/midi/message_handler_test.rb +315 -0
- data/test/midi/wrapper_test.rb +229 -0
- data/test/player/wrapper_test.rb +51 -0
- metadata +250 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 052f8904644cacbb9b81cc9c9e2e72eef3150cb8
|
4
|
+
data.tar.gz: 88428d64855c4252236e557dd4f68b3748f921a2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e7e0ba57b4fb5f7e33435f6353187dd8ad57c0c282ea5a8fadc769bbfd03914154da75d0e704bc00318517fa3ca5ae970b1e3065a0faa534d9bee845d695cc5c
|
7
|
+
data.tar.gz: 7c36b2b70a1042f21dac9e9c1ee257a1249c104893b4b422908e564342fd7e10c061cd548bf81f7b08b531cd7a18faee0c0bad59a65addc83d2ad2857fa2cb33
|
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2017 Ari Russo
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# mmplayer
|
2
|
+
|
3
|
+
Control [VLC Media Player](https://en.wikipedia.org/wiki/VLC_media_player) with MIDI
|
4
|
+
|
5
|
+
This is a fork of [mmplayer](https://github.com/arirusso/mvlc), a gem to control MPlayer with MIDI
|
6
|
+
|
7
|
+
## License
|
8
|
+
|
9
|
+
Apache 2.0, See LICENSE file
|
10
|
+
|
11
|
+
Copyright (c) 2017 [Ari Russo](http://arirusso.com)
|
data/lib/mvlc.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# MVLC
|
2
|
+
# Control VLC media player with MIDI
|
3
|
+
#
|
4
|
+
# (c)2017 Ari Russo
|
5
|
+
# Apache 2.0 License
|
6
|
+
|
7
|
+
# libs
|
8
|
+
require "forwardable"
|
9
|
+
require "midi-eye"
|
10
|
+
require "scale"
|
11
|
+
require "timeout"
|
12
|
+
require "unimidi"
|
13
|
+
require "vlc-client"
|
14
|
+
|
15
|
+
# modules
|
16
|
+
require "mvlc/helper/numbers"
|
17
|
+
require "mvlc/instructions"
|
18
|
+
require "mvlc/midi"
|
19
|
+
require "mvlc/player"
|
20
|
+
require "mvlc/thread"
|
21
|
+
|
22
|
+
# classes
|
23
|
+
require "mvlc/context"
|
24
|
+
|
25
|
+
module MVLC
|
26
|
+
|
27
|
+
VERSION = "0.0.1"
|
28
|
+
|
29
|
+
# Shortcut to Context constructor
|
30
|
+
def self.new(*args, &block)
|
31
|
+
Context.new(*args, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
data/lib/mvlc/context.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
module MVLC
|
2
|
+
|
3
|
+
# DSL context for interfacing an instance of MPlayer with MIDI
|
4
|
+
class Context
|
5
|
+
|
6
|
+
include Helper::Numbers
|
7
|
+
include Instructions::MIDI
|
8
|
+
include Instructions::Player
|
9
|
+
|
10
|
+
attr_reader :midi, :player
|
11
|
+
|
12
|
+
# @param [UniMIDI::Input, Array<UniMIDI::Input>] midi_input
|
13
|
+
# @param [Hash] options
|
14
|
+
# @option options [Integer] :midi_buffer_length Length of MIDI message buffer in seconds
|
15
|
+
# @option options [String] :mplayer_flags The command-line flags to invoke MPlayer with
|
16
|
+
# @option options [Integer] :receive_channel (also: :rx_channel) A MIDI channel to subscribe to. By default, responds to all
|
17
|
+
# @yield
|
18
|
+
def initialize(midi_input, options = {}, &block)
|
19
|
+
midi_options = {
|
20
|
+
:buffer_length => options[:midi_buffer_length],
|
21
|
+
:receive_channel => options[:receive_channel] || options[:rx_channel]
|
22
|
+
}
|
23
|
+
@midi = MIDI.new(midi_input, midi_options)
|
24
|
+
@player = Player.new(:flags => options[:mplayer_flags])
|
25
|
+
instance_eval(&block) if block_given?
|
26
|
+
end
|
27
|
+
|
28
|
+
# Start listening for MIDI
|
29
|
+
# Note that MPlayer will start when Context#play (aka Instructions::Player#play) is called
|
30
|
+
# @param [Hash] options
|
31
|
+
# @option options [Boolean] :background Whether to run in a background thread
|
32
|
+
# @return [Boolean]
|
33
|
+
def start(options = {})
|
34
|
+
@midi.start
|
35
|
+
begin
|
36
|
+
@playback_thread = playback_loop
|
37
|
+
@playback_thread.join unless !!options[:background]
|
38
|
+
rescue SystemExit, Interrupt
|
39
|
+
stop
|
40
|
+
end
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
# Stop the player
|
45
|
+
# @return [Boolean]
|
46
|
+
def stop
|
47
|
+
@midi.stop
|
48
|
+
@player.quit
|
49
|
+
@playback_thread.kill
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Main playback loop
|
56
|
+
def playback_loop
|
57
|
+
::MVLC::Thread.new(:timeout => false) do
|
58
|
+
until @player.active?
|
59
|
+
sleep(0.1)
|
60
|
+
end
|
61
|
+
@player.playback_loop
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module MVLC
|
2
|
+
|
3
|
+
module Helper
|
4
|
+
|
5
|
+
# Number conversion
|
6
|
+
module Numbers
|
7
|
+
|
8
|
+
# Converts a percentage to a 7-bit int value eg 50 -> 0x40
|
9
|
+
# @param [Integer] num
|
10
|
+
# @return [Integer]
|
11
|
+
def to_midi_value(num)
|
12
|
+
Scale.transform(num).from(0..100).to(0..127.0).round
|
13
|
+
end
|
14
|
+
|
15
|
+
# Converts a MIDI 7-bit int value to a percentage eg 0x40 -> 50
|
16
|
+
# @param [Integer] num
|
17
|
+
# @return [Integer]
|
18
|
+
def to_percent(num)
|
19
|
+
Scale.transform(num).from(0..127).to(0..100.0).round
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module MVLC
|
2
|
+
|
3
|
+
module Instructions
|
4
|
+
|
5
|
+
# Instructions for dealing with MIDI
|
6
|
+
module MIDI
|
7
|
+
|
8
|
+
# Set the MIDI channel to receive messages on
|
9
|
+
# @param [Integer, nil] num The channel number 0-15 or nil for all
|
10
|
+
def receive_channel(num)
|
11
|
+
@midi.channel = num
|
12
|
+
end
|
13
|
+
alias_method :rx_channel, :receive_channel
|
14
|
+
|
15
|
+
# Assign a callback for a given MIDI system command
|
16
|
+
# @param [String, Symbol] note A MIDI system command eg :start, :continue, :stop
|
17
|
+
# @param [Proc] callback The callback to execute when a matching message is received
|
18
|
+
# @return [Hash]
|
19
|
+
def on_system(command, &callback)
|
20
|
+
@midi.add_system_callback(command, &callback)
|
21
|
+
end
|
22
|
+
alias_method :system, :on_system
|
23
|
+
|
24
|
+
# Assign a callback for a given MIDI note
|
25
|
+
# @param [Integer, String] note A MIDI note eg 64 "F4" or nil for all
|
26
|
+
# @param [Proc] callback The callback to execute when a matching message is received
|
27
|
+
# @return [Hash]
|
28
|
+
def on_note(note = nil, &callback)
|
29
|
+
@midi.add_note_callback(note, &callback)
|
30
|
+
end
|
31
|
+
alias_method :note, :on_note
|
32
|
+
|
33
|
+
# Assign a callback for the given MIDI control change
|
34
|
+
# @param [Integer] index The MIDI control change index to assign the callback for or nil for all
|
35
|
+
# @param [Proc] callback The callback to execute when a matching message is received
|
36
|
+
# @return [Hash]
|
37
|
+
def on_cc(index = nil, &callback)
|
38
|
+
@midi.add_cc_callback(index, &callback)
|
39
|
+
end
|
40
|
+
alias_method :cc, :on_cc
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module MVLC
|
2
|
+
|
3
|
+
module Instructions
|
4
|
+
|
5
|
+
# Instructions dealing with the MPlayer
|
6
|
+
module Player
|
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
|
+
|
16
|
+
# Assign a callback for when a file finishes playback
|
17
|
+
# @param [Proc] callback The callback to execute when a file finishes playback
|
18
|
+
# @return [Hash]
|
19
|
+
def on_end_of_file(&callback)
|
20
|
+
@player.add_end_of_file_callback(&callback)
|
21
|
+
end
|
22
|
+
alias_method :end_of_file, :on_end_of_file
|
23
|
+
alias_method :eof, :on_end_of_file
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Add delegators to local player methods
|
28
|
+
def self.included(base)
|
29
|
+
base.send(:extend, Forwardable)
|
30
|
+
base.send(:def_delegators, :@player, :active?, :play)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Add all of the MPlayer::Slave methods to the context as instructions
|
34
|
+
def method_missing(method, *args, &block)
|
35
|
+
if @player.respond_to?(method)
|
36
|
+
@player.send(method, *args, &block)
|
37
|
+
else
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Add all of the MPlayer::Slave methods to the context as instructions
|
43
|
+
def respond_to_missing?(method, include_private = false)
|
44
|
+
super || @player.respond_to?(method)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
data/lib/mvlc/midi.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
module MVLC
|
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 [Integer, 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 [Integer, 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 [Integer, nil] channel
|
39
|
+
# @param [MIDIMessage] message
|
40
|
+
# @return [Boolean, nil]
|
41
|
+
def process(channel, message)
|
42
|
+
case message
|
43
|
+
when MIDIMessage::SystemCommon, MIDIMessage::SystemExclusive, 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 [Integer, 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,115 @@
|
|
1
|
+
module MVLC
|
2
|
+
|
3
|
+
module MIDI
|
4
|
+
|
5
|
+
# Wrapper for MIDI functionality
|
6
|
+
class Wrapper
|
7
|
+
|
8
|
+
attr_reader :channel, :listener, :message_handler
|
9
|
+
|
10
|
+
# @param [UniMIDI::Input, Array<UniMIDI::Input>] input
|
11
|
+
# @param [Hash] options
|
12
|
+
# @option options [Integer] :buffer_length Length of MIDI message buffer in seconds
|
13
|
+
# @option options [Integer] :receive_channel A MIDI channel to subscribe to. By default, responds to all
|
14
|
+
def initialize(input, options = {})
|
15
|
+
@buffer_length = options[:buffer_length]
|
16
|
+
@channel = options[:receive_channel]
|
17
|
+
|
18
|
+
@message_handler = MessageHandler.new
|
19
|
+
@listener = MIDIEye::Listener.new(input)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Add a callback for a given MIDI system message
|
23
|
+
# @param [String, Symbol] command The MIDI system command eg :start, :stop
|
24
|
+
# @param [Proc] callback The callback to execute when the given MIDI command is received
|
25
|
+
# @return [Hash]
|
26
|
+
def add_system_callback(command, &callback)
|
27
|
+
@message_handler.add_callback(:system, command, &callback)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Add a callback for a given MIDI note
|
31
|
+
# @param [Integer, String, nil] note The MIDI note to add a callback for eg 64 "E4"
|
32
|
+
# @param [Proc] callback The callback to execute when the given MIDI note is received
|
33
|
+
# @return [Hash]
|
34
|
+
def add_note_callback(note, &callback)
|
35
|
+
@message_handler.add_note_callback(note, &callback)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Add a callback for a given MIDI control change
|
39
|
+
# @param [Integer, nil] index The MIDI control change index to add a callback for eg 10
|
40
|
+
# @param [Proc] callback The callback to execute when the given MIDI control change is received
|
41
|
+
# @return [Hash]
|
42
|
+
def add_cc_callback(index, &callback)
|
43
|
+
@message_handler.add_callback(:cc, index, &callback)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Stop the MIDI listener
|
47
|
+
# @return [Boolean]
|
48
|
+
def stop
|
49
|
+
@listener.stop
|
50
|
+
end
|
51
|
+
|
52
|
+
# Change the subscribed MIDI channel (or nil for all)
|
53
|
+
# @param [Integer, nil] channel
|
54
|
+
# @return [Integer, nil]
|
55
|
+
def channel=(channel)
|
56
|
+
@listener.event.clear
|
57
|
+
@channel = channel
|
58
|
+
initialize_listener if @listener.running?
|
59
|
+
@channel
|
60
|
+
end
|
61
|
+
|
62
|
+
# Start the MIDI listener
|
63
|
+
# @return [Boolean]
|
64
|
+
def start
|
65
|
+
initialize_listener
|
66
|
+
@start_time = Time.now.to_i
|
67
|
+
@listener.start(:background => true)
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
# Whether the player is subscribed to all channels
|
72
|
+
# @return [Boolean]
|
73
|
+
def omni?
|
74
|
+
@channel.nil?
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Elapsed time since start in seconds
|
80
|
+
# @return [Integer]
|
81
|
+
def now
|
82
|
+
Time.now.to_i - @start_time
|
83
|
+
end
|
84
|
+
|
85
|
+
# Should the given MIDI event be processed or thrown away?
|
86
|
+
# @param [Hash] event
|
87
|
+
# @return [Boolean]
|
88
|
+
def process_event?(event)
|
89
|
+
@buffer_length.nil? ||
|
90
|
+
event[:timestamp].nil? ||
|
91
|
+
event[:timestamp].to_i >= now - @buffer_length
|
92
|
+
end
|
93
|
+
|
94
|
+
# Handle a new MIDI event received
|
95
|
+
# @param [Hash] event
|
96
|
+
# @return [Hash]
|
97
|
+
def handle_new_event(event)
|
98
|
+
if process_event?(event)
|
99
|
+
message = event[:message]
|
100
|
+
@message_handler.process(@channel, message)
|
101
|
+
event
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Populate the MIDI listener callback
|
106
|
+
# @return [MIDIEye::Listener]
|
107
|
+
def initialize_listener
|
108
|
+
@listener.on_message { |event| handle_new_event(event) }
|
109
|
+
@listener
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|