mvlc 0.0.1
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 +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
|