patchmaster 0.0.0
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.
- data/bin/patchmaster +20 -0
- data/lib/patchmaster.rb +15 -0
- data/lib/patchmaster/app/info_window.rb +32 -0
- data/lib/patchmaster/app/info_window_contents.txt +15 -0
- data/lib/patchmaster/app/list_window.rb +26 -0
- data/lib/patchmaster/app/main.rb +161 -0
- data/lib/patchmaster/app/patch_window.rb +61 -0
- data/lib/patchmaster/app/pm_window.rb +41 -0
- data/lib/patchmaster/app/prompt_window.rb +61 -0
- data/lib/patchmaster/connection.rb +90 -0
- data/lib/patchmaster/consts.rb +439 -0
- data/lib/patchmaster/dsl.rb +227 -0
- data/lib/patchmaster/filter.rb +23 -0
- data/lib/patchmaster/io.rb +87 -0
- data/lib/patchmaster/list.rb +121 -0
- data/lib/patchmaster/list_container.rb +36 -0
- data/lib/patchmaster/patch.rb +48 -0
- data/lib/patchmaster/patchmaster.rb +168 -0
- data/lib/patchmaster/predicates.rb +125 -0
- data/lib/patchmaster/song.rb +24 -0
- data/lib/patchmaster/song_list.rb +39 -0
- data/lib/patchmaster/sorted_song_list.rb +15 -0
- data/test/test_helper.rb +58 -0
- metadata +88 -0
data/bin/patchmaster
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#
|
|
3
|
+
# usage: patchmaster [-i] [pm_file]
|
|
4
|
+
#
|
|
5
|
+
# Starts PatchMaster and optionally loads pm_file.
|
|
6
|
+
#
|
|
7
|
+
# The -n flag tells PatchMaster to not use MIDI. All MIDI errors such as not
|
|
8
|
+
# being able to connect to the MIDI devices speicified in pm_file are
|
|
9
|
+
# ignored, and no MIDI data is sent/received. That is useful if you want to
|
|
10
|
+
# run PatchMaster without actually talking to any MIDI devices.
|
|
11
|
+
|
|
12
|
+
require 'patchmaster'
|
|
13
|
+
|
|
14
|
+
pm = PM::Main.instance
|
|
15
|
+
if ARGV[0] == '-n'
|
|
16
|
+
pm.no_midi!
|
|
17
|
+
ARGV.shift
|
|
18
|
+
end
|
|
19
|
+
pm.load(ARGV[0]) if ARGV[0]
|
|
20
|
+
pm.run
|
data/lib/patchmaster.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require 'patchmaster/consts'
|
|
2
|
+
require 'patchmaster/predicates'
|
|
3
|
+
require 'patchmaster/song_list'
|
|
4
|
+
require 'patchmaster/song'
|
|
5
|
+
require 'patchmaster/patch'
|
|
6
|
+
require 'patchmaster/connection'
|
|
7
|
+
require 'patchmaster/filter'
|
|
8
|
+
require 'patchmaster/io'
|
|
9
|
+
require 'patchmaster/patchmaster'
|
|
10
|
+
require 'patchmaster/dsl'
|
|
11
|
+
require 'patchmaster/app/main'
|
|
12
|
+
|
|
13
|
+
def message(str)
|
|
14
|
+
PM::Main.instance.message(str)
|
|
15
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'curses'
|
|
2
|
+
|
|
3
|
+
module PM
|
|
4
|
+
class InfoWindow
|
|
5
|
+
|
|
6
|
+
CONTENTS = File.join(File.dirname(__FILE__), 'info_window_contents.txt')
|
|
7
|
+
|
|
8
|
+
include Curses
|
|
9
|
+
|
|
10
|
+
attr_reader :win
|
|
11
|
+
|
|
12
|
+
TITLE = ' PatchMaster '
|
|
13
|
+
|
|
14
|
+
def initialize(rows, cols, row, col)
|
|
15
|
+
@win = Window.new(rows, cols, row, col)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def draw
|
|
19
|
+
@win.setpos(0, (@win.maxx() - TITLE.length) / 2)
|
|
20
|
+
@win.attron(A_REVERSE) {
|
|
21
|
+
@win.addstr(TITLE)
|
|
22
|
+
}
|
|
23
|
+
@win.addstr("\n")
|
|
24
|
+
IO.foreach(CONTENTS) { |line| @win.addstr(line) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def refresh
|
|
28
|
+
@win.refresh
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'patchmaster/app/pm_window'
|
|
2
|
+
|
|
3
|
+
module PM
|
|
4
|
+
class ListWindow < PmWindow
|
|
5
|
+
|
|
6
|
+
attr_reader :list
|
|
7
|
+
|
|
8
|
+
def set_contents(title, list)
|
|
9
|
+
@title, @list = title, list
|
|
10
|
+
draw
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def draw
|
|
14
|
+
super
|
|
15
|
+
return unless @list
|
|
16
|
+
|
|
17
|
+
@list.each_with_index do |thing, i|
|
|
18
|
+
@win.setpos(i+1, 1)
|
|
19
|
+
@win.attron(A_REVERSE) if thing == @list.curr
|
|
20
|
+
@win.addstr(make_fit(" #{thing.name} "))
|
|
21
|
+
@win.attroff(A_REVERSE) if thing == @list.curr
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
require 'curses'
|
|
2
|
+
require 'singleton'
|
|
3
|
+
%w(list patch info prompt).each { |w| require "patchmaster/app/#{w}_window" }
|
|
4
|
+
|
|
5
|
+
module PM
|
|
6
|
+
|
|
7
|
+
class Main
|
|
8
|
+
|
|
9
|
+
DEBUG_FILE = '/tmp/pm_debug.txt'
|
|
10
|
+
|
|
11
|
+
include Singleton
|
|
12
|
+
include Curses
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@pm = PatchMaster.instance
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def no_midi!
|
|
19
|
+
@pm.no_midi!
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# File must be a Ruby file that returns an array of song lists.
|
|
23
|
+
def load(file)
|
|
24
|
+
@pm.load(file)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run
|
|
28
|
+
if $DEBUG
|
|
29
|
+
@debug_file = File.open(DEBUG_FILE, 'a')
|
|
30
|
+
end
|
|
31
|
+
@pm.start
|
|
32
|
+
begin
|
|
33
|
+
config_curses
|
|
34
|
+
create_windows
|
|
35
|
+
message("Welcome to PatchMaster")
|
|
36
|
+
|
|
37
|
+
loop do
|
|
38
|
+
begin
|
|
39
|
+
refresh_all
|
|
40
|
+
ch = getch
|
|
41
|
+
message("ch = #{ch}") if $DEBUG
|
|
42
|
+
case ch
|
|
43
|
+
when ?j, Key::DOWN
|
|
44
|
+
@pm.next_patch
|
|
45
|
+
when ?k, Key::UP
|
|
46
|
+
@pm.prev_patch
|
|
47
|
+
when ?n, Key::LEFT
|
|
48
|
+
@pm.next_song
|
|
49
|
+
when ?p, Key::RIGHT
|
|
50
|
+
@pm.prev_song
|
|
51
|
+
when ?g
|
|
52
|
+
name = PromptWindow.new('Go To Song', 'Go to song:').gets
|
|
53
|
+
@pm.goto_song(name)
|
|
54
|
+
when ?t
|
|
55
|
+
name = PromptWindow.new('Go To Song List', 'Go to Song List:').gets
|
|
56
|
+
@pm.goto_song_list(name)
|
|
57
|
+
when Key::F1
|
|
58
|
+
help
|
|
59
|
+
when 27 # escape
|
|
60
|
+
@pm.panic
|
|
61
|
+
message('Panic sent')
|
|
62
|
+
when ?l
|
|
63
|
+
file = PromptWindow.new('Load', 'Load file:').gets
|
|
64
|
+
begin
|
|
65
|
+
@pm.load(file)
|
|
66
|
+
rescue => ex
|
|
67
|
+
message(ex.to_s)
|
|
68
|
+
end
|
|
69
|
+
when ?s
|
|
70
|
+
file = PromptWindow.new('Save', 'Save into file:').gets
|
|
71
|
+
begin
|
|
72
|
+
@pm.save(file)
|
|
73
|
+
rescue => ex
|
|
74
|
+
message(ex.to_s)
|
|
75
|
+
end
|
|
76
|
+
when ?q
|
|
77
|
+
break
|
|
78
|
+
end
|
|
79
|
+
rescue => ex
|
|
80
|
+
message(ex.to_s)
|
|
81
|
+
if $DEBUG
|
|
82
|
+
@debug_file.puts caller.join("\n")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
ensure
|
|
87
|
+
close_screen
|
|
88
|
+
@pm.stop
|
|
89
|
+
if $DEBUG
|
|
90
|
+
@debug_file.close
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def config_curses
|
|
96
|
+
init_screen
|
|
97
|
+
cbreak # unbuffered input
|
|
98
|
+
noecho # do not show typed keys
|
|
99
|
+
stdscr.keypad(true) # enable arrow keys
|
|
100
|
+
curs_set(0) # cursor: 0 = invisible, 1 = normal
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def create_windows
|
|
104
|
+
top_height = (lines() - 1) * 2 / 3
|
|
105
|
+
bot_height = (lines() - 1) - top_height
|
|
106
|
+
top_width = cols() / 3
|
|
107
|
+
|
|
108
|
+
sls_height = top_height / 3
|
|
109
|
+
sl_height = top_height - sls_height
|
|
110
|
+
|
|
111
|
+
@song_lists_win = ListWindow.new(sls_height, top_width, 0, 0, nil)
|
|
112
|
+
@song_lists_win.set_contents('Song Lists', @pm.song_lists)
|
|
113
|
+
@song_list_win = ListWindow.new(sl_height, top_width, sls_height, 0, 'Song List')
|
|
114
|
+
@song_win = ListWindow.new(top_height, top_width, 0, top_width, 'Song')
|
|
115
|
+
@patch_win = PatchWindow.new(bot_height, cols(), top_height, 0, 'Patch')
|
|
116
|
+
@message_win = Window.new(1, cols(), lines()-1, 0)
|
|
117
|
+
|
|
118
|
+
@info_win = InfoWindow.new(top_height, cols() - (top_width * 2) - 1, 0, top_width * 2 + 1)
|
|
119
|
+
@info_win.draw
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def help
|
|
123
|
+
message("Help: not yet implemented")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def message(str)
|
|
127
|
+
if @message_win
|
|
128
|
+
@message_win.clear
|
|
129
|
+
@message_win.addstr(str)
|
|
130
|
+
@message_win.refresh
|
|
131
|
+
else
|
|
132
|
+
$stderr.puts str
|
|
133
|
+
end
|
|
134
|
+
if $DEBUG
|
|
135
|
+
@debug_file.puts "#{Time.now} #{str}"
|
|
136
|
+
@debug_file.flush
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def refresh_all
|
|
141
|
+
set_window_data
|
|
142
|
+
[@song_lists_win, @song_list_win, @song_win, @patch_win].map(&:draw)
|
|
143
|
+
[stdscr, @song_lists_win, @song_list_win, @song_win, @info_win, @patch_win, @message_win].map(&:refresh)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def set_window_data
|
|
147
|
+
song_list = @pm.curr_song_list
|
|
148
|
+
@song_list_win.set_contents(song_list.name, song_list.songs)
|
|
149
|
+
song = song_list.curr_song
|
|
150
|
+
if song
|
|
151
|
+
@song_win.set_contents(song.name, song.patches)
|
|
152
|
+
patch = song.curr_patch
|
|
153
|
+
@patch_win.patch = patch
|
|
154
|
+
else
|
|
155
|
+
@song_win.set_contents(nil, nil)
|
|
156
|
+
@patch_win.patch = nil
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require 'patchmaster/app/pm_window'
|
|
2
|
+
|
|
3
|
+
module PM
|
|
4
|
+
class PatchWindow < PmWindow
|
|
5
|
+
|
|
6
|
+
attr_reader :patch
|
|
7
|
+
|
|
8
|
+
def patch=(patch)
|
|
9
|
+
@title = patch ? patch.name : nil
|
|
10
|
+
@patch = patch
|
|
11
|
+
draw
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def draw
|
|
15
|
+
super
|
|
16
|
+
@win.setpos(1, 1)
|
|
17
|
+
draw_headers
|
|
18
|
+
return unless @patch
|
|
19
|
+
|
|
20
|
+
max_len = @win.maxx - 3 # minus 2 for borders
|
|
21
|
+
@patch.connections.each_with_index do |connection, i|
|
|
22
|
+
@win.setpos(i+2, 1)
|
|
23
|
+
draw_connection(connection, max_len)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def draw_headers
|
|
28
|
+
@win.attron(A_REVERSE) {
|
|
29
|
+
str = " Input Chan | Output Chan | Prog | Zone | Xpose | Filter"
|
|
30
|
+
str << ' ' * (@win.maxx - 2 - str.length)
|
|
31
|
+
@win.addstr(str)
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def draw_connection(connection, max_len)
|
|
36
|
+
str = " #{'%16s' % connection.input.name}"
|
|
37
|
+
str << " #{connection.input_chan ? ('%2d' % (connection.input_chan+1)) : ' '} |"
|
|
38
|
+
str << " #{'%16s' % connection.output.name}"
|
|
39
|
+
str << " #{'%2d' % (connection.output_chan+1)} |"
|
|
40
|
+
str << if connection.pc?
|
|
41
|
+
" #{'%3d' % connection.pc_prog} |"
|
|
42
|
+
else
|
|
43
|
+
" |"
|
|
44
|
+
end
|
|
45
|
+
str << if connection.zone
|
|
46
|
+
" #{'%3s' % connection.note_num_to_name(connection.zone.begin)}" +
|
|
47
|
+
" - #{'%3s' % connection.note_num_to_name(connection.zone.end)} |"
|
|
48
|
+
else
|
|
49
|
+
' |'
|
|
50
|
+
end
|
|
51
|
+
str << if connection.xpose && connection.xpose > 0
|
|
52
|
+
" #{'%2d' % connection.xpose.to_i} |"
|
|
53
|
+
else
|
|
54
|
+
" |"
|
|
55
|
+
end
|
|
56
|
+
str << " #{connection.filter}"
|
|
57
|
+
@win.addstr(make_fit(str))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require 'curses'
|
|
2
|
+
|
|
3
|
+
module PM
|
|
4
|
+
class PmWindow
|
|
5
|
+
|
|
6
|
+
include Curses
|
|
7
|
+
|
|
8
|
+
attr_reader :win, :title_prefix
|
|
9
|
+
attr_accessor :title
|
|
10
|
+
|
|
11
|
+
# If title is nil then list's name will be used
|
|
12
|
+
def initialize(rows, cols, row, col, title_prefix)
|
|
13
|
+
@win = Window.new(rows, cols, row, col)
|
|
14
|
+
@title_prefix = title_prefix
|
|
15
|
+
@max_contents_len = @win.maxx - 3 # 2 for borders
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def refresh
|
|
19
|
+
@win.refresh
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def draw
|
|
23
|
+
@win.clear
|
|
24
|
+
@win.box(?|, ?-)
|
|
25
|
+
return unless @title_prefix || @title
|
|
26
|
+
|
|
27
|
+
@win.setpos(0, 1)
|
|
28
|
+
@win.attron(A_REVERSE) {
|
|
29
|
+
@win.addch(' ')
|
|
30
|
+
@win.addstr("#{@title_prefix}: ") if @title_prefix
|
|
31
|
+
@win.addstr(@title) if @title
|
|
32
|
+
@win.addch(' ')
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def make_fit(str)
|
|
37
|
+
str = str[0..@max_contents_len] if str.length > @max_contents_len
|
|
38
|
+
str
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require 'curses'
|
|
2
|
+
|
|
3
|
+
module PM
|
|
4
|
+
class PromptWindow
|
|
5
|
+
|
|
6
|
+
MAX_WIDTH = 30
|
|
7
|
+
|
|
8
|
+
include Curses
|
|
9
|
+
|
|
10
|
+
def initialize(title, prompt)
|
|
11
|
+
@title, @prompt = title, prompt
|
|
12
|
+
width = cols() / 2
|
|
13
|
+
width = MAX_WIDTH if width > MAX_WIDTH
|
|
14
|
+
@win = Window.new(4, width, lines() / 3, (cols() - width) / 2)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def gets
|
|
18
|
+
draw
|
|
19
|
+
str = read_string
|
|
20
|
+
cleanup
|
|
21
|
+
str
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def draw
|
|
25
|
+
@win.box(?|, ?-)
|
|
26
|
+
@win.setpos(0, 1)
|
|
27
|
+
@win.attron(A_REVERSE) {
|
|
28
|
+
@win.addstr(" #@title ")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@win.setpos(1, 1)
|
|
32
|
+
@win.addstr(@prompt)
|
|
33
|
+
|
|
34
|
+
@win.setpos(2, 1)
|
|
35
|
+
@win.attron(A_REVERSE) {
|
|
36
|
+
@win.addstr(' ' * (@win.maxx() - 2))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@win.setpos(2, 1)
|
|
40
|
+
@win.refresh
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def read_string
|
|
44
|
+
nocbreak
|
|
45
|
+
echo
|
|
46
|
+
curs_set(1)
|
|
47
|
+
str = nil
|
|
48
|
+
@win.attron(A_REVERSE) {
|
|
49
|
+
str = @win.getstr
|
|
50
|
+
}
|
|
51
|
+
curs_set(0)
|
|
52
|
+
noecho
|
|
53
|
+
cbreak
|
|
54
|
+
str
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def cleanup
|
|
58
|
+
@win.close
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require 'patchmaster/consts'
|
|
2
|
+
|
|
3
|
+
module PM
|
|
4
|
+
|
|
5
|
+
# A Connection connects an InputDevice to an OutputDevice. Whenever MIDI
|
|
6
|
+
# data arrives at the InputDevice it is optionally modified or filtered,
|
|
7
|
+
# then the remaining modified data is sent to the OutputDevice.
|
|
8
|
+
class Connection
|
|
9
|
+
|
|
10
|
+
attr_accessor :input, :input_chan, :output, :output_chan,
|
|
11
|
+
:pc_prog, :zone, :xpose, :filter
|
|
12
|
+
|
|
13
|
+
# If input_chan is nil than all messages from input will be sent to
|
|
14
|
+
# output.
|
|
15
|
+
#
|
|
16
|
+
# All channels (input_chan, output_chan, etc.) are 1-based here but are
|
|
17
|
+
# turned into 0-based channels for later use.
|
|
18
|
+
def initialize(input, input_chan, output, output_chan, filter=nil, opts={})
|
|
19
|
+
@input, @input_chan, @output, @output_chan, @filter = input, input_chan, output, output_chan, filter
|
|
20
|
+
@pc_prog, @zone, @xpose = opts[:pc_prog], opts[:zone], opts[:xpose]
|
|
21
|
+
|
|
22
|
+
@input_chan -= 1 if @input_chan
|
|
23
|
+
@output_chan -= 1 if @output_chan
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def start(start_bytes=nil)
|
|
27
|
+
midi_out(@output, start_bytes) if start_bytes
|
|
28
|
+
midi_out(@output, [PROGRAM_CHANGE + @output_chan, @pc_prog]) if pc?
|
|
29
|
+
@input.add_connection(self)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def stop
|
|
33
|
+
@input.remove_connection(self)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def accept_from_input?(bytes)
|
|
37
|
+
return true if @input_chan == nil
|
|
38
|
+
return true if bytes[0] >= 0xf0
|
|
39
|
+
(bytes[0] & 0xf0) >= 0x80 && (bytes[0] & 0x0f) == @input_chan
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns true if the +@zone+ is nil (allowing all notes throught) or if
|
|
43
|
+
# +@zone+ is a Range and +note+ is inside +@zone+.
|
|
44
|
+
def inside_zone?(note)
|
|
45
|
+
@zone == nil || @zone.include?(note)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def midi_in(bytes)
|
|
49
|
+
return unless accept_from_input?(bytes)
|
|
50
|
+
|
|
51
|
+
# TODO handle running bytes
|
|
52
|
+
high_nibble = bytes[0] & 0xf0
|
|
53
|
+
case high_nibble
|
|
54
|
+
when NOTE_ON, NOTE_OFF, POLY_PRESSURE
|
|
55
|
+
return unless inside_zone?(bytes[1])
|
|
56
|
+
bytes[0] = high_nibble + @output_chan
|
|
57
|
+
bytes[1] = ((bytes[1] + @xpose) & 0xff) if @xpose
|
|
58
|
+
when CONTROLLER, PROGRAM_CHANGE, CHANNEL_PRESSURE, PITCH_BEND
|
|
59
|
+
bytes[0] = high_nibble + @output_chan
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
bytes = @filter.call(self, bytes) if @filter
|
|
63
|
+
if bytes && bytes.size > 0
|
|
64
|
+
midi_out(@output, bytes)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def midi_out(device, bytes)
|
|
69
|
+
device.midi_out(bytes)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def pc?
|
|
73
|
+
@pc_prog != nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def note_num_to_name(n)
|
|
77
|
+
oct = (n / 12) - 1
|
|
78
|
+
note = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'][n % 12]
|
|
79
|
+
"#{note}#{oct}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def to_s
|
|
83
|
+
str = "#{@input.name} ch #{@input_chan ? @input_chan+1 : 'all'} -> #{@output.name} ch #{@output_chan+1}"
|
|
84
|
+
str << "; pc #@pc_prog" if pc?
|
|
85
|
+
str << "; xpose #@xpose" if @xpose
|
|
86
|
+
str << "; zone #{note_num_to_name(@zone.begin)}..#{note_num_to_name(@zone.end)}" if @zone
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
end # PM
|