patchmaster 1.1.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/patchmaster +18 -7
- data/lib/patchmaster.rb +1 -0
- data/lib/patchmaster/code_chunk.rb +21 -0
- data/lib/patchmaster/code_key.rb +16 -0
- data/lib/patchmaster/connection.rb +1 -1
- data/lib/patchmaster/consts.rb +74 -41
- data/lib/patchmaster/curses/list_window.rb +2 -1
- data/lib/patchmaster/curses/main.rb +3 -10
- data/lib/patchmaster/cursor.rb +1 -1
- data/lib/patchmaster/dsl.rb +94 -22
- data/lib/patchmaster/filter.rb +5 -5
- data/lib/patchmaster/instrument.rb +5 -5
- data/lib/patchmaster/irb/irb.rb +21 -15
- data/lib/patchmaster/patchmaster.rb +12 -2
- data/lib/patchmaster/predicates.rb +1 -1
- data/lib/patchmaster/song.rb +1 -1
- data/lib/patchmaster/song_list.rb +2 -3
- data/lib/patchmaster/trigger.rb +8 -9
- data/lib/patchmaster/web/public/js/patchmaster.coffee +8 -5
- data/lib/patchmaster/web/public/js/patchmaster.js +16 -7
- data/lib/patchmaster/web/sinatra_app.rb +5 -9
- data/test/support/test_connection.rb +11 -0
- data/test/test_helper.rb +2 -50
- metadata +11 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c0a690b803a2cbb24400d24f16f576e77f528638
|
4
|
+
data.tar.gz: c88d291e8e2b4849f3202544a7c14fc1235037f7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a6c9c655d1c47ebbda7f687af55e2c51d717926a27e4942e1611bd0c599c426e3dbb3ca0b3ffd4b514daa92aedbccf61b04cebb03a6089d20719a6721b9cac8e
|
7
|
+
data.tar.gz: 962920f0541fda505f4264ed524d054a2d0bcbb9d63321353c131aefffa12f3a06f6ab2520d2c3f0a83ea61696b0c3410bc313e5ece3b0b85340349a720e1cb2
|
data/bin/patchmaster
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
#
|
3
|
-
# usage: patchmaster [-v] [-n] [-i] [-w] [-p port] [-d] [pm_file]
|
3
|
+
# usage: patchmaster [-l] [-v] [-n] [-i] [-w] [-p port] [-d] [pm_file]
|
4
4
|
#
|
5
5
|
# Starts PatchMaster and optionally loads pm_file.
|
6
6
|
#
|
7
|
+
# -l lists all available MIDI inputs and outputs and exits. This is exactply
|
8
|
+
# -the same as running the `unimidi list` command from your shell.
|
9
|
+
#
|
7
10
|
# -v outputs the version number and exits.
|
8
11
|
#
|
9
12
|
# The -n flag tells PatchMaster to not use MIDI. All MIDI errors such as not
|
@@ -29,6 +32,10 @@ port = nil
|
|
29
32
|
OptionParser.new do |opts|
|
30
33
|
opts.banner = "usage: patchmaster [options] [pm_file]"
|
31
34
|
opts.on("-d", "--debug", "Turn on debug mode") { $DEBUG = true }
|
35
|
+
opts.on("-l", "--list", "List MIDI inputs and outputs and exit") do
|
36
|
+
system("unimidi list")
|
37
|
+
exit 0
|
38
|
+
end
|
32
39
|
opts.on("-n", "--no-midi", "Turn off MIDI processing") { use_midi = false }
|
33
40
|
opts.on("-i", "--irb", "Use an IRB console") { gui = :irb }
|
34
41
|
opts.on("-w", "--web", "Use a Web browser GUI") { gui = :web }
|
@@ -47,23 +54,27 @@ end.parse!(ARGV)
|
|
47
54
|
|
48
55
|
# Must require patchmaster here, after handling options, because Singleton
|
49
56
|
# initialize code checks $DEBUG.
|
50
|
-
|
57
|
+
begin
|
58
|
+
require 'patchmaster'
|
59
|
+
require 'patchmaster/curses/main' # for function key symbols
|
60
|
+
rescue LoadError
|
61
|
+
$LOAD_PATH << File.join(__dir__, '../lib')
|
62
|
+
retry
|
63
|
+
end
|
51
64
|
|
52
65
|
pm = PM::PatchMaster.instance
|
53
66
|
pm.use_midi = use_midi
|
54
|
-
pm.load(ARGV[0]) if ARGV[0]
|
55
67
|
case gui
|
56
68
|
when :curses
|
57
|
-
require 'patchmaster/curses/main'
|
58
69
|
pm.gui = PM::Main.instance
|
59
|
-
pm.run
|
60
70
|
when :irb
|
61
71
|
require 'patchmaster/irb/irb'
|
62
|
-
|
72
|
+
pm.gui = PM::IRB.instance
|
63
73
|
when :web
|
64
74
|
require 'patchmaster/web/sinatra_app'
|
65
75
|
app = PM::SinatraApp.instance
|
66
76
|
app.port = port if port
|
67
77
|
pm.gui = app
|
68
|
-
pm.run
|
69
78
|
end
|
79
|
+
pm.load(ARGV[0]) if ARGV[0]
|
80
|
+
pm.run
|
data/lib/patchmaster.rb
CHANGED
@@ -0,0 +1,21 @@
|
|
1
|
+
module PM
|
2
|
+
|
3
|
+
# A CodeChunk holds a block of code (lambda, block, proc) and the text that
|
4
|
+
# created it as read in from a PatchMaster file.
|
5
|
+
class CodeChunk
|
6
|
+
|
7
|
+
attr_accessor :block, :text
|
8
|
+
|
9
|
+
def initialize(block, text=nil)
|
10
|
+
@block, @text = block, text
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(*args)
|
14
|
+
block.call(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
"#<PM::CodeChunk block=#{block.inspect}, text=#{text.inspect}>"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module PM
|
2
|
+
|
3
|
+
# A CodeKey holds a CodeChunk and remembers what key it is assigned to.
|
4
|
+
class CodeKey
|
5
|
+
|
6
|
+
attr_accessor :key, :code_chunk
|
7
|
+
|
8
|
+
def initialize(key, code_chunk)
|
9
|
+
@key, @code_chunk = key, code_chunk
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
@code_chunk.run
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -42,7 +42,7 @@ class Connection
|
|
42
42
|
def accept_from_input?(bytes)
|
43
43
|
return true if @input_chan == nil
|
44
44
|
return true unless bytes.channel?
|
45
|
-
bytes.
|
45
|
+
bytes.channel == @input_chan
|
46
46
|
end
|
47
47
|
|
48
48
|
# Returns true if the +@zone+ is nil (allowing all notes throught) or if
|
data/lib/patchmaster/consts.rb
CHANGED
@@ -87,30 +87,47 @@ EOS
|
|
87
87
|
# System reset
|
88
88
|
SYSTEM_RESET = 0xFF
|
89
89
|
|
90
|
+
#--
|
90
91
|
# Controller numbers
|
91
92
|
# = 0 - 31 = continuous, MSB
|
92
93
|
# = 32 - 63 = continuous, LSB
|
93
|
-
# = 64 - 97 = switches
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
94
|
+
# = 64 - 97 = momentary switches
|
95
|
+
#++
|
96
|
+
CC_BANK_SELECT = CC_BANK_SELECT_MSB = 0
|
97
|
+
CC_MOD_WHEEL = CC_MOD_WHEEL_MSB = 1
|
98
|
+
CC_BREATH_CONTROLLER = CC_BREATH_CONTROLLER_MSB = 2
|
99
|
+
CC_FOOT_CONTROLLER = CC_FOOT_CONTROLLER_MSB = 4
|
100
|
+
CC_PORTAMENTO_TIME = CC_PORTAMENTO_TIME_MSB = 5
|
101
|
+
CC_DATA_ENTRY = CC_DATA_ENTRY_MSB = 6
|
102
|
+
CC_VOLUME = CC_VOLUME_MSB = 7
|
103
|
+
CC_BALANCE = CC_BALANCE_MSB = 8
|
104
|
+
CC_PAN = CC_PAN_MSB = 10
|
105
|
+
CC_EXPRESSION_CONTROLLER = CC_EXPRESSION_CONTROLLER_MSB = 11
|
106
|
+
CC_GEN_PURPOSE_1 = CC_GEN_PURPOSE_1_MSB = 16
|
107
|
+
CC_GEN_PURPOSE_2 = CC_GEN_PURPOSE_2_MSB = 17
|
108
|
+
CC_GEN_PURPOSE_3 = CC_GEN_PURPOSE_3_MSB = 18
|
109
|
+
CC_GEN_PURPOSE_4 = CC_GEN_PURPOSE_4_MSB = 19
|
108
110
|
|
111
|
+
#--
|
109
112
|
# [32 - 63] are LSB for [0 - 31]
|
110
|
-
|
113
|
+
#++
|
114
|
+
CC_BANK_SELECT_LSB = CC_BANK_SELECT_MSB + 32
|
115
|
+
CC_MOD_WHEEL_LSB = CC_MOD_WHEEL_MSB + 32
|
116
|
+
CC_BREATH_CONTROLLER_LSB = CC_BREATH_CONTROLLER_MSB + 32
|
117
|
+
CC_FOOT_CONTROLLER_LSB = CC_FOOT_CONTROLLER_MSB + 32
|
118
|
+
CC_PORTAMENTO_TIME_LSB = CC_PORTAMENTO_TIME_MSB + 32
|
119
|
+
CC_DATA_ENTRY_LSB = CC_DATA_ENTRY_MSB + 32
|
120
|
+
CC_VOLUME_LSB = CC_VOLUME_MSB + 32
|
121
|
+
CC_BALANCE_LSB = CC_BALANCE_MSB + 32
|
122
|
+
CC_PAN_LSB = CC_PAN_MSB + 32
|
123
|
+
CC_EXPRESSION_CONTROLLER_LSB = CC_EXPRESSION_CONTROLLER_MSB + 32
|
124
|
+
CC_GEN_PURPOSE_1_LSB = CC_GEN_PURPOSE_1_MSB + 32
|
125
|
+
CC_GEN_PURPOSE_2_LSB = CC_GEN_PURPOSE_2_MSB + 32
|
126
|
+
CC_GEN_PURPOSE_3_LSB = CC_GEN_PURPOSE_3_MSB + 32
|
127
|
+
CC_GEN_PURPOSE_4_LSB = CC_GEN_PURPOSE_4_MSB + 32
|
111
128
|
|
112
129
|
#--
|
113
|
-
#
|
130
|
+
# Momentary switches:
|
114
131
|
#++
|
115
132
|
CC_SUSTAIN = 64
|
116
133
|
CC_PORTAMENTO = 65
|
@@ -145,31 +162,47 @@ EOS
|
|
145
162
|
CM_MONO_MODE_ON = 0x7E # Val = # chans
|
146
163
|
CM_POLY_MODE_ON = 0x7F # Val must be 0
|
147
164
|
|
148
|
-
# Controller names
|
149
165
|
CONTROLLER_NAMES = [
|
150
|
-
"
|
151
|
-
"Modulation",
|
152
|
-
"Breath Control",
|
153
|
-
"3",
|
154
|
-
"Foot Controller",
|
155
|
-
"Portamento Time",
|
156
|
-
"Data Entry",
|
157
|
-
"Volume",
|
158
|
-
"Balance",
|
159
|
-
"9",
|
160
|
-
"Pan",
|
161
|
-
"Expression Control",
|
162
|
-
"12", "13", "14", "15",
|
163
|
-
"General Controller 1",
|
164
|
-
"General Controller 2",
|
165
|
-
"General Controller 3",
|
166
|
-
"General Controller 4",
|
167
|
-
"20", "21", "22", "23", "24", "25
|
168
|
-
"30", "31",
|
169
|
-
|
170
|
-
"
|
171
|
-
"
|
172
|
-
"
|
166
|
+
"Bank Select (MSB)",
|
167
|
+
"Modulation (MSB)",
|
168
|
+
"Breath Control (MSB)",
|
169
|
+
"3 (MSB)",
|
170
|
+
"Foot Controller (MSB)",
|
171
|
+
"Portamento Time (MSB)",
|
172
|
+
"Data Entry (MSB)",
|
173
|
+
"Volume (MSB)",
|
174
|
+
"Balance (MSB)",
|
175
|
+
"9 (MSB)",
|
176
|
+
"Pan (MSB)",
|
177
|
+
"Expression Control (MSB)",
|
178
|
+
"12 (MSB)", "13 (MSB)", "14 (MSB)", "15 (MSB)",
|
179
|
+
"General Controller 1 (MSB)",
|
180
|
+
"General Controller 2 (MSB)",
|
181
|
+
"General Controller 3 (MSB)",
|
182
|
+
"General Controller 4 (MSB)",
|
183
|
+
"20 (MSB)", "21 (MSB)", "22 (MSB)", "23 (MSB)", "24 (MSB)", "25 (MSB)",
|
184
|
+
"26 (MSB)", "27 (MSB)", "28 (MSB)", "29 (MSB)", "30 (MSB)", "31 (MSB)",
|
185
|
+
|
186
|
+
"Bank Select (LSB)",
|
187
|
+
"Modulation (LSB)",
|
188
|
+
"Breath Control (LSB)",
|
189
|
+
"35 (LSB)",
|
190
|
+
"Foot Controller (LSB)",
|
191
|
+
"Portamento Time (LSB)",
|
192
|
+
"Data Entry (LSB)",
|
193
|
+
"Volume (LSB)",
|
194
|
+
"Balance (LSB)",
|
195
|
+
"41 (LSB)",
|
196
|
+
"Pan (LSB)",
|
197
|
+
"Expression Control (LSB)",
|
198
|
+
"44 (LSB)", "45 (LSB)", "46 (LSB)", "47 (LSB)",
|
199
|
+
"General Controller 1 (LSB)",
|
200
|
+
"General Controller 2 (LSB)",
|
201
|
+
"General Controller 3 (LSB)",
|
202
|
+
"General Controller 4 (LSB)",
|
203
|
+
"52 (LSB)", "53 (LSB)", "54 (LSB)", "55 (LSB)", "56 (LSB)", "57 (LSB)",
|
204
|
+
"58 (LSB)", "59 (LSB)", "60 (LSB)", "61 (LSB)", "62 (LSB)", "63 (LSB)",
|
205
|
+
|
173
206
|
"Sustain Pedal",
|
174
207
|
"Portamento",
|
175
208
|
"Sostenuto",
|
@@ -22,8 +22,9 @@ class ListWindow < PmWindow
|
|
22
22
|
return unless @list
|
23
23
|
|
24
24
|
curr_item = PM::PatchMaster.instance.send(@curr_item_method_sym)
|
25
|
-
|
25
|
+
return unless curr_item
|
26
26
|
|
27
|
+
curr_index = @list.index(curr_item)
|
27
28
|
if curr_index < @offset
|
28
29
|
@offset = curr_index
|
29
30
|
elsif curr_index >= @offset + visible_height
|
@@ -18,7 +18,6 @@ class Main
|
|
18
18
|
|
19
19
|
def initialize
|
20
20
|
@pm = PatchMaster.instance
|
21
|
-
@message_bindings = {}
|
22
21
|
end
|
23
22
|
|
24
23
|
def run
|
@@ -91,8 +90,10 @@ class Main
|
|
91
90
|
@pm.debug caller.join("\n")
|
92
91
|
end
|
93
92
|
|
94
|
-
msg_name = @message_bindings[ch]
|
93
|
+
msg_name = @pm.message_bindings[ch]
|
95
94
|
@pm.send_message(msg_name) if msg_name
|
95
|
+
code_key = @pm.code_bindings[ch]
|
96
|
+
code_key.run if code_key
|
96
97
|
end
|
97
98
|
ensure
|
98
99
|
clear
|
@@ -103,14 +104,6 @@ class Main
|
|
103
104
|
end
|
104
105
|
end
|
105
106
|
|
106
|
-
def bind_message(name, key_or_sym)
|
107
|
-
if FUNCTION_KEY_SYMBOLS.keys.include?(key_or_sym)
|
108
|
-
@message_bindings[FUNCTION_KEY_SYMBOLS[key_or_sym]] = name
|
109
|
-
else
|
110
|
-
@message_bindings[key_or_sym] = name
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
107
|
def config_curses
|
115
108
|
init_screen
|
116
109
|
cbreak # unbuffered input
|
data/lib/patchmaster/cursor.rb
CHANGED
@@ -2,7 +2,7 @@ module PM
|
|
2
2
|
|
3
3
|
# A PM::Cursor knows the current PM::SongList, PM::Song, and PM::Patch, how
|
4
4
|
# to move between songs and patches, and how to find them given name
|
5
|
-
# regexes.
|
5
|
+
# regexes.
|
6
6
|
class Cursor
|
7
7
|
|
8
8
|
attr_reader :song_list, :song, :patch
|
data/lib/patchmaster/dsl.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'unimidi'
|
2
|
+
require_relative './code_chunk'
|
2
3
|
|
3
4
|
module PM
|
4
5
|
|
@@ -18,6 +19,7 @@ class DSL
|
|
18
19
|
@outputs = {}
|
19
20
|
@triggers = []
|
20
21
|
@filters = []
|
22
|
+
@code_keys = []
|
21
23
|
@songs = {} # key = name, value = song
|
22
24
|
end
|
23
25
|
|
@@ -25,6 +27,7 @@ class DSL
|
|
25
27
|
contents = IO.read(file)
|
26
28
|
init
|
27
29
|
instance_eval(contents)
|
30
|
+
read_code_keys(contents)
|
28
31
|
read_triggers(contents)
|
29
32
|
read_filters(contents)
|
30
33
|
end
|
@@ -50,21 +53,42 @@ class DSL
|
|
50
53
|
raise "output: error creating output instrument \"#{name || sym}\" on output port #{port_num}: #{ex}"
|
51
54
|
end
|
52
55
|
alias_method :out, :output
|
56
|
+
alias_method :outp, :output
|
53
57
|
|
54
58
|
def message(name, bytes)
|
55
|
-
@pm.messages[name.downcase] = bytes
|
59
|
+
@pm.messages[name.downcase] = [name, bytes]
|
56
60
|
end
|
57
61
|
|
58
|
-
def message_key(
|
59
|
-
if
|
60
|
-
|
62
|
+
def message_key(key_or_sym, name)
|
63
|
+
if name.is_a?(Symbol)
|
64
|
+
name, key_or_sym = key_or_sym, name
|
65
|
+
$stderr.puts "WARNING: the arguments to message_key are now key first, then name."
|
66
|
+
$stderr.puts "I will use #{name} as the name and #{key_or_sym} as the key for now."
|
67
|
+
$stderr.puts "Please swap them for future compatability."
|
61
68
|
end
|
69
|
+
if key_or_sym.is_a?(String) && name.is_a?(String)
|
70
|
+
if name.length == 1 && key_or_sym.length > 1
|
71
|
+
name, key_or_sym = key_or_sym, name
|
72
|
+
$stderr.puts "WARNING: the arguments to message_key are now key first, then name."
|
73
|
+
$stderr.puts "I will use #{name} as the name and #{key_or_sym} as the key for now."
|
74
|
+
$stderr.puts "Please swap them for future compatability."
|
75
|
+
elsif name.length == 1 && key_or_sym.length == 1
|
76
|
+
raise "message_key: since both name and key are one-character strings, I can't tell which is which. Please make the name longer."
|
77
|
+
end
|
78
|
+
end
|
79
|
+
@pm.bind_message(name, to_binding_key(key_or_sym))
|
80
|
+
end
|
81
|
+
|
82
|
+
def code_key(key_or_sym, &block)
|
83
|
+
ck = CodeKey.new(to_binding_key(key_or_sym), CodeChunk.new(block))
|
84
|
+
@pm.bind_code(ck)
|
85
|
+
@code_keys << ck
|
62
86
|
end
|
63
87
|
|
64
88
|
def trigger(instrument_sym, bytes, &block)
|
65
89
|
instrument = @inputs[instrument_sym]
|
66
90
|
raise "trigger: error finding instrument #{instrument_sym}" unless instrument
|
67
|
-
t = Trigger.new(bytes, block)
|
91
|
+
t = Trigger.new(bytes, CodeChunk.new(block))
|
68
92
|
instrument.triggers << t
|
69
93
|
@triggers << t
|
70
94
|
end
|
@@ -145,7 +169,7 @@ class DSL
|
|
145
169
|
alias_method :x, :transpose
|
146
170
|
|
147
171
|
def filter(&block)
|
148
|
-
@conn.filter = Filter.new(block)
|
172
|
+
@conn.filter = Filter.new(CodeChunk.new(block))
|
149
173
|
@filters << @conn.filter
|
150
174
|
end
|
151
175
|
alias_method :f, :filter
|
@@ -173,6 +197,9 @@ class DSL
|
|
173
197
|
def save(file)
|
174
198
|
File.open(file, 'w') { |f|
|
175
199
|
save_instruments(f)
|
200
|
+
save_messages(f)
|
201
|
+
save_message_keys(f)
|
202
|
+
save_code_keys(f)
|
176
203
|
save_triggers(f)
|
177
204
|
save_songs(f)
|
178
205
|
save_song_lists(f)
|
@@ -181,18 +208,41 @@ class DSL
|
|
181
208
|
|
182
209
|
def save_instruments(f)
|
183
210
|
@pm.inputs.each do |instr|
|
184
|
-
f.puts "input #{instr.port_num}, :#{instr.sym}, #{
|
211
|
+
f.puts "input #{instr.port_num}, :#{instr.sym}, #{instr.name.inspect}"
|
185
212
|
end
|
186
213
|
@pm.outputs.each do |instr|
|
187
|
-
f.puts "output #{instr.port_num}, :#{instr.sym}, #{
|
214
|
+
f.puts "output #{instr.port_num}, :#{instr.sym}, #{instr.name.inspect}"
|
188
215
|
end
|
189
216
|
f.puts
|
190
217
|
end
|
191
218
|
|
219
|
+
def save_messages(f)
|
220
|
+
@pm.messages.each do |_, (correct_case_name, msg)|
|
221
|
+
f.puts "message #{correct_case_name.inspect}, #{msg.inspect}"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def save_message_keys(f)
|
226
|
+
@pm.message_bindings.each do |key, message_name|
|
227
|
+
f.puts "message_key #{to_save_key(key).inspect}, #{message_name.inspect}"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def save_code_keys(f)
|
232
|
+
@pm.code_bindings.values.each do |code_key|
|
233
|
+
str = if code_key.code_chunk.text[0] == '{'
|
234
|
+
"code_key(#{to_save_key(code_key.key).inspect}) #{code_key.code_chunk.text}"
|
235
|
+
else
|
236
|
+
"code_key #{to_save_key(code_key.key).inspect} #{code_key.code_chunk.text}"
|
237
|
+
end
|
238
|
+
f.puts str
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
192
242
|
def save_triggers(f)
|
193
243
|
@pm.inputs.each do |instrument|
|
194
244
|
instrument.triggers.each do |trigger|
|
195
|
-
str = "trigger :#{instrument.sym}, #{trigger.bytes.inspect} #{trigger.text}"
|
245
|
+
str = "trigger :#{instrument.sym}, #{trigger.bytes.inspect} #{trigger.code_chunk.text}"
|
196
246
|
f.puts str
|
197
247
|
end
|
198
248
|
end
|
@@ -201,7 +251,7 @@ class DSL
|
|
201
251
|
|
202
252
|
def save_songs(f)
|
203
253
|
@pm.all_songs.songs.each do |song|
|
204
|
-
f.puts "song #{
|
254
|
+
f.puts "song #{song.name.inspect} do"
|
205
255
|
song.patches.each { |patch| save_patch(f, patch) }
|
206
256
|
f.puts "end"
|
207
257
|
f.puts
|
@@ -209,7 +259,7 @@ class DSL
|
|
209
259
|
end
|
210
260
|
|
211
261
|
def save_patch(f, patch)
|
212
|
-
f.puts " patch #{
|
262
|
+
f.puts " patch #{patch.name.inspect} do"
|
213
263
|
f.puts " start_bytes #{patch.start_bytes.inspect}" if patch.start_bytes
|
214
264
|
patch.connections.each { |conn| save_connection(f, conn) }
|
215
265
|
f.puts " end"
|
@@ -222,29 +272,42 @@ class DSL
|
|
222
272
|
f.puts " prog_chg #{conn.pc_prog}" if conn.pc?
|
223
273
|
f.puts " zone #{conn.note_num_to_name(conn.zone.begin)}, #{conn.note_num_to_name(conn.zone.end)}" if conn.zone
|
224
274
|
f.puts " xpose #{conn.xpose}" if conn.xpose
|
225
|
-
f.puts " filter #{conn.filter.text}" if conn.filter
|
275
|
+
f.puts " filter #{conn.filter.code_chunk.text}" if conn.filter
|
226
276
|
f.puts " end"
|
227
277
|
end
|
228
278
|
|
229
279
|
def save_song_lists(f)
|
230
280
|
@pm.song_lists.each do |sl|
|
231
281
|
next if sl == @pm.all_songs
|
232
|
-
f.puts "song_list #{
|
282
|
+
f.puts "song_list #{sl.name.inspect}, ["
|
233
283
|
@pm.all_songs.songs.each do |song|
|
234
|
-
f.puts " #{
|
284
|
+
f.puts " #{song.name.inspect},"
|
235
285
|
end
|
236
286
|
f.puts "]"
|
237
287
|
end
|
238
288
|
end
|
239
289
|
|
240
|
-
def quoted(str)
|
241
|
-
"\"#{str.gsub('"', "\\\"")}\"" # ' <= un-confuse Emacs font-lock
|
242
|
-
end
|
243
|
-
|
244
290
|
# ****************************************************************
|
245
291
|
|
246
292
|
private
|
247
293
|
|
294
|
+
# Translate symbol like :f1 to the proper function key value.
|
295
|
+
def to_binding_key(key_or_sym)
|
296
|
+
if key_or_sym.is_a?(Symbol) && PM::Main::FUNCTION_KEY_SYMBOLS[key_or_sym]
|
297
|
+
key_or_sym = PM::Main::FUNCTION_KEY_SYMBOLS[key_or_sym]
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Translate function key values into symbol strings and other keys into
|
302
|
+
# double-quoted strings.
|
303
|
+
def to_save_key(key)
|
304
|
+
if PM::Main::FUNCTION_KEY_SYMBOLS.value?(key)
|
305
|
+
PM::Main::FUNCTION_KEY_SYMBOLS.key(key)
|
306
|
+
else
|
307
|
+
key
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
248
311
|
def read_triggers(contents)
|
249
312
|
read_block_text('trigger', @triggers, contents)
|
250
313
|
end
|
@@ -253,6 +316,10 @@ class DSL
|
|
253
316
|
read_block_text('filter', @filters, contents)
|
254
317
|
end
|
255
318
|
|
319
|
+
def read_code_keys(contents)
|
320
|
+
read_block_text('code_key', @code_keys, contents)
|
321
|
+
end
|
322
|
+
|
256
323
|
# Extremely simple block text reader. Relies on indentation to detect end
|
257
324
|
# of code block.
|
258
325
|
def read_block_text(name, containers, contents)
|
@@ -260,11 +327,13 @@ class DSL
|
|
260
327
|
in_block = false
|
261
328
|
block_indentation = nil
|
262
329
|
block_end_token = nil
|
330
|
+
chunk = nil
|
263
331
|
contents.each_line do |line|
|
264
332
|
if line =~ /^(\s*)#{name}\s*.*?(({|do|->\s*{|lambda\s*{)(.*))/
|
265
333
|
block_indentation, text = $1, $2
|
266
334
|
i += 1
|
267
|
-
containers[i].
|
335
|
+
chunk = containers[i].code_chunk
|
336
|
+
chunk.text = text + "\n"
|
268
337
|
in_block = true
|
269
338
|
block_end_token = case text
|
270
339
|
when /^{/
|
@@ -281,15 +350,18 @@ class DSL
|
|
281
350
|
indentation, text = $1, $2
|
282
351
|
if indentation.length <= block_indentation.length
|
283
352
|
if text =~ /^#{block_end_token}/
|
284
|
-
|
353
|
+
chunk.text << line
|
285
354
|
end
|
286
355
|
in_block = false
|
287
356
|
else
|
288
|
-
|
357
|
+
chunk.text << line
|
289
358
|
end
|
290
359
|
end
|
291
360
|
end
|
292
|
-
containers.each
|
361
|
+
containers.each do |thing|
|
362
|
+
text = thing.code_chunk.text
|
363
|
+
text.strip! if text
|
364
|
+
end
|
293
365
|
end
|
294
366
|
|
295
367
|
end
|
data/lib/patchmaster/filter.rb
CHANGED
@@ -5,18 +5,18 @@ module PM
|
|
5
5
|
# representation as well.
|
6
6
|
class Filter
|
7
7
|
|
8
|
-
attr_accessor :
|
8
|
+
attr_accessor :code_chunk
|
9
9
|
|
10
|
-
def initialize(
|
11
|
-
@
|
10
|
+
def initialize(code_chunk)
|
11
|
+
@code_chunk = code_chunk
|
12
12
|
end
|
13
13
|
|
14
14
|
def call(conn, bytes)
|
15
|
-
@
|
15
|
+
@code_chunk.run(conn, bytes)
|
16
16
|
end
|
17
17
|
|
18
18
|
def to_s
|
19
|
-
@text || '# no block text found'
|
19
|
+
@code_chunk.text || '# no block text found'
|
20
20
|
end
|
21
21
|
|
22
22
|
end
|
@@ -26,6 +26,7 @@ class InputInstrument < Instrument
|
|
26
26
|
super(sym, name, port_num, input_port(port_num, use_midi))
|
27
27
|
@connections = []
|
28
28
|
@triggers = []
|
29
|
+
@listener = nil
|
29
30
|
end
|
30
31
|
|
31
32
|
def add_connection(conn)
|
@@ -95,6 +96,7 @@ end
|
|
95
96
|
class MockInputPort
|
96
97
|
|
97
98
|
attr_reader :name
|
99
|
+
attr_accessor :buffer
|
98
100
|
|
99
101
|
# For MIDIEye::Listener
|
100
102
|
def self.is_compatible?(input)
|
@@ -104,6 +106,7 @@ class MockInputPort
|
|
104
106
|
# Constructor param is ignored; it's required by MIDIEye.
|
105
107
|
def initialize(arg)
|
106
108
|
@name = "MockInputPort #{arg}"
|
109
|
+
@buffer = []
|
107
110
|
end
|
108
111
|
|
109
112
|
def gets
|
@@ -116,18 +119,15 @@ class MockInputPort
|
|
116
119
|
|
117
120
|
def clear_buffer
|
118
121
|
end
|
119
|
-
|
120
|
-
# add this class to the Listener class' known input types
|
121
|
-
MIDIEye::Listener.input_types << self
|
122
|
-
|
123
122
|
end
|
124
123
|
|
125
124
|
class MockOutputPort
|
126
|
-
|
127
125
|
attr_reader :name
|
126
|
+
attr_accessor :buffer
|
128
127
|
|
129
128
|
def initialize(port_num)
|
130
129
|
@name = "MockOutputPort #{port_num}"
|
130
|
+
@buffer = []
|
131
131
|
end
|
132
132
|
|
133
133
|
def puts(data)
|
data/lib/patchmaster/irb/irb.rb
CHANGED
@@ -4,17 +4,28 @@ require 'tempfile'
|
|
4
4
|
|
5
5
|
$dsl = nil
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
module PM
|
8
|
+
class IRB
|
9
|
+
|
10
|
+
include Singleton
|
11
|
+
|
12
|
+
attr_reader :dsl
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@dsl = PM::DSL.new
|
16
|
+
@dsl.song("IRB Song")
|
17
|
+
@dsl.patch("IRB Patch")
|
18
|
+
end
|
19
|
+
|
20
|
+
# For bin/patchmaster.
|
21
|
+
def run
|
22
|
+
::IRB.start
|
23
|
+
end
|
24
|
+
end
|
9
25
|
end
|
10
26
|
|
11
27
|
def dsl
|
12
|
-
|
13
|
-
$dsl = PM::DSL.new
|
14
|
-
$dsl.song("IRB Song")
|
15
|
-
$dsl.patch("IRB Patch")
|
16
|
-
end
|
17
|
-
$dsl
|
28
|
+
PM::IRB.instance.dsl
|
18
29
|
end
|
19
30
|
|
20
31
|
# Return the current (only) patch.
|
@@ -33,8 +44,8 @@ def pm_help
|
|
33
44
|
puts IO.read(File.join(File.dirname(__FILE__), 'irb_help.txt'))
|
34
45
|
end
|
35
46
|
|
36
|
-
# The "panic" command is handled by
|
37
|
-
# all all-notes-off messages.
|
47
|
+
# The "panic" command is handled by the PM::DSL instance. This version
|
48
|
+
# (+panic!+) tells that +panic+ to send all all-notes-off messages.
|
38
49
|
def panic!
|
39
50
|
PM::PatchMaster.instance.panic(true)
|
40
51
|
end
|
@@ -54,8 +65,3 @@ def method_missing(sym, *args)
|
|
54
65
|
super
|
55
66
|
end
|
56
67
|
end
|
57
|
-
|
58
|
-
def start_patchmaster_irb(init_file=nil)
|
59
|
-
ENV['IRBRC'] = init_file if init_file
|
60
|
-
IRB.start
|
61
|
-
end
|
@@ -21,7 +21,7 @@ class PatchMaster < SimpleDelegator
|
|
21
21
|
include Singleton
|
22
22
|
|
23
23
|
attr_reader :inputs, :outputs, :all_songs, :song_lists
|
24
|
-
attr_reader :messages
|
24
|
+
attr_reader :messages, :message_bindings, :code_bindings
|
25
25
|
attr_accessor :use_midi
|
26
26
|
alias_method :use_midi?, :use_midi
|
27
27
|
attr_accessor :gui
|
@@ -37,6 +37,8 @@ class PatchMaster < SimpleDelegator
|
|
37
37
|
super(@cursor)
|
38
38
|
@use_midi = true
|
39
39
|
@gui = nil
|
40
|
+
@message_bindings = {}
|
41
|
+
@code_bindings = {}
|
40
42
|
|
41
43
|
if $DEBUG
|
42
44
|
@debug_file = File.open(DEBUG_FILE, 'a')
|
@@ -77,6 +79,14 @@ class PatchMaster < SimpleDelegator
|
|
77
79
|
raise("error saving #{file}: #{ex}" + caller.join("\n"))
|
78
80
|
end
|
79
81
|
|
82
|
+
def bind_message(name, key)
|
83
|
+
@message_bindings[key] = name
|
84
|
+
end
|
85
|
+
|
86
|
+
def bind_code(code_key)
|
87
|
+
@code_bindings[code_key.key] = code_key
|
88
|
+
end
|
89
|
+
|
80
90
|
# Initializes the cursor and all data.
|
81
91
|
def init_data
|
82
92
|
@cursor.clear
|
@@ -127,7 +137,7 @@ class PatchMaster < SimpleDelegator
|
|
127
137
|
# Send the message with the given +name+ to all outputs. Names are matched
|
128
138
|
# case-insensitively.
|
129
139
|
def send_message(name)
|
130
|
-
msg = @messages[name.downcase]
|
140
|
+
_correct_case_name, msg = @messages[name.downcase]
|
131
141
|
if !msg
|
132
142
|
message("Message \"#{name}\" not found")
|
133
143
|
return
|
data/lib/patchmaster/song.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module PM
|
2
2
|
|
3
|
-
# A SongList is a list of Songs
|
3
|
+
# A SongList is a list of Songs.
|
4
4
|
class SongList
|
5
5
|
|
6
6
|
attr_accessor :name, :songs
|
@@ -15,8 +15,7 @@ class SongList
|
|
15
15
|
end
|
16
16
|
|
17
17
|
# Returns the first Song that matches +name+. +name+ may be either a
|
18
|
-
# Regexp or a String. The match will be made case-insensitive.
|
19
|
-
# move or set the cursor.
|
18
|
+
# Regexp or a String. The match will be made case-insensitive.
|
20
19
|
def find(name_regex)
|
21
20
|
name_regex = Regexp.new(name_regex.to_s, true) # make case-insensitive
|
22
21
|
@songs.detect { |s| s.name =~ name_regex }
|
data/lib/patchmaster/trigger.rb
CHANGED
@@ -1,34 +1,33 @@
|
|
1
1
|
module PM
|
2
2
|
|
3
|
-
# A Trigger
|
4
|
-
# Instruments have zero or more triggers.
|
5
|
-
# sent to PM::PatchMaster.
|
3
|
+
# A Trigger executes code when it sees a particular array of bytes.
|
4
|
+
# Instruments have zero or more triggers.
|
6
5
|
#
|
7
6
|
# Since we want to save them to files, we store the text representation as
|
8
7
|
# well.
|
9
8
|
class Trigger
|
10
9
|
|
11
|
-
attr_accessor :bytes, :
|
10
|
+
attr_accessor :bytes, :code_chunk
|
12
11
|
|
13
|
-
def initialize(bytes,
|
14
|
-
@bytes, @
|
12
|
+
def initialize(bytes, code_chunk)
|
13
|
+
@bytes, @code_chunk = bytes, code_chunk
|
15
14
|
end
|
16
15
|
|
17
16
|
def method_missing(sym, *args)
|
18
17
|
PM::PatchMaster.instance.send(sym, *args)
|
19
18
|
end
|
20
19
|
|
21
|
-
# If +bytes+ matches our +@bytes+ array then run +@
|
20
|
+
# If +bytes+ matches our +@bytes+ array then run +@code_chunk+.
|
22
21
|
def signal(bytes)
|
23
22
|
if bytes == @bytes
|
24
23
|
pm = PM::PatchMaster.instance
|
25
|
-
pm
|
24
|
+
@code_chunk.run(pm)
|
26
25
|
pm.gui.refresh if pm.gui
|
27
26
|
end
|
28
27
|
end
|
29
28
|
|
30
29
|
def to_s
|
31
|
-
"#{@bytes.inspect} => #{(@text || '# no block text found').gsub(/\n\s*/, '; ')}"
|
30
|
+
"#{@bytes.inspect} => #{(@code_chunk.text || '# no block text found').gsub(/\n\s*/, '; ')}"
|
32
31
|
end
|
33
32
|
end
|
34
33
|
end
|
@@ -29,6 +29,7 @@ connection_row = (conn) ->
|
|
29
29
|
connection_rows = (connections) ->
|
30
30
|
rows = (connection_row(conn) for conn in connections)
|
31
31
|
$('#patch').html(CONN_HEADERS + "\n" + rows.join("\n"))
|
32
|
+
set_colors()
|
32
33
|
|
33
34
|
maybe_name = (data, key) -> if data[key] then data[key]['name'] else ''
|
34
35
|
|
@@ -48,21 +49,23 @@ kp = (action) ->
|
|
48
49
|
message(data['message']) if data['message']?
|
49
50
|
)
|
50
51
|
|
51
|
-
|
52
|
-
base_class = COLOR_SCHEMES[color_scheme_index]
|
52
|
+
remove_colors = () ->
|
53
53
|
if color_scheme_index >= 0
|
54
|
+
base_class = COLOR_SCHEMES[color_scheme_index]
|
54
55
|
$('body').removeClass(base_class)
|
55
56
|
$('.selected, th, td#appname').removeClass("reverse-#{base_class}")
|
56
57
|
$('tr, td, th').removeClass("#{base_class}-border")
|
57
58
|
|
58
|
-
|
59
|
-
|
59
|
+
set_colors = () ->
|
60
60
|
base_class = COLOR_SCHEMES[color_scheme_index]
|
61
61
|
$('body').addClass(base_class)
|
62
62
|
$('.selected, th, td#appname').addClass("reverse-#{base_class}")
|
63
63
|
$('tr, td, th').addClass("#{base_class}-border")
|
64
64
|
|
65
|
-
|
65
|
+
cycle_colors = () ->
|
66
|
+
remove_colors()
|
67
|
+
color_scheme_index = (color_scheme_index + 1) % COLOR_SCHEMES.length
|
68
|
+
set_colors()
|
66
69
|
|
67
70
|
bindings =
|
68
71
|
'j': 'next_patch'
|
@@ -56,7 +56,8 @@
|
|
56
56
|
}
|
57
57
|
return _results;
|
58
58
|
})();
|
59
|
-
|
59
|
+
$('#patch').html(CONN_HEADERS + "\n" + rows.join("\n"));
|
60
|
+
return set_colors();
|
60
61
|
};
|
61
62
|
|
62
63
|
maybe_name = function(data, key) {
|
@@ -88,20 +89,28 @@
|
|
88
89
|
});
|
89
90
|
};
|
90
91
|
|
91
|
-
|
92
|
-
var base_class, color_scheme;
|
93
|
-
base_class = COLOR_SCHEMES[color_scheme_index];
|
92
|
+
remove_colors = function() {
|
94
93
|
if (color_scheme_index >= 0) {
|
94
|
+
var base_class;
|
95
|
+
base_class = COLOR_SCHEMES[color_scheme_index];
|
95
96
|
$('body').removeClass(base_class);
|
96
97
|
$('.selected, th, td#appname').removeClass("reverse-" + base_class);
|
97
98
|
$('tr, td, th').removeClass("" + base_class + "-border");
|
98
99
|
}
|
99
|
-
|
100
|
+
};
|
101
|
+
|
102
|
+
set_colors = function() {
|
103
|
+
var base_class;
|
100
104
|
base_class = COLOR_SCHEMES[color_scheme_index];
|
101
105
|
$('body').addClass(base_class);
|
102
106
|
$('.selected, th, td#appname').addClass("reverse-" + base_class);
|
103
|
-
$('tr, td, th').addClass("" + base_class + "-border");
|
104
|
-
|
107
|
+
return $('tr, td, th').addClass("" + base_class + "-border");
|
108
|
+
};
|
109
|
+
|
110
|
+
cycle_colors = function() {
|
111
|
+
remove_colors();
|
112
|
+
color_scheme_index = (color_scheme_index + 1) % COLOR_SCHEMES.length;
|
113
|
+
return set_colors();
|
105
114
|
};
|
106
115
|
|
107
116
|
bindings = {
|
@@ -2,13 +2,6 @@ require 'sinatra'
|
|
2
2
|
require 'sinatra/json'
|
3
3
|
require 'singleton'
|
4
4
|
|
5
|
-
# ================================================================
|
6
|
-
# Settings
|
7
|
-
# ================================================================
|
8
|
-
|
9
|
-
set :run, true
|
10
|
-
set :root, File.dirname(__FILE__)
|
11
|
-
|
12
5
|
# ================================================================
|
13
6
|
# Helper methods
|
14
7
|
# ================================================================
|
@@ -106,7 +99,10 @@ end
|
|
106
99
|
|
107
100
|
module PM
|
108
101
|
|
109
|
-
class SinatraApp
|
102
|
+
class SinatraApp < Sinatra::Base
|
103
|
+
|
104
|
+
set :run, true
|
105
|
+
set :root, File.dirname(__FILE__)
|
110
106
|
|
111
107
|
include Singleton
|
112
108
|
|
@@ -118,7 +114,7 @@ class SinatraApp
|
|
118
114
|
end
|
119
115
|
|
120
116
|
def run
|
121
|
-
set(:port, @port) if @port
|
117
|
+
self.class.set(:port, @port) if @port
|
122
118
|
@pm.start
|
123
119
|
ensure
|
124
120
|
@pm.stop
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# A TestConnection records all bytes received and passes them straight
|
2
|
+
# through.
|
3
|
+
class TestConnection < PM::Connection
|
4
|
+
attr_accessor :bytes_received
|
5
|
+
|
6
|
+
def midi_in(bytes)
|
7
|
+
@bytes_received ||= []
|
8
|
+
@bytes_received += bytes
|
9
|
+
midi_out(bytes)
|
10
|
+
end
|
11
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -1,55 +1,7 @@
|
|
1
1
|
require 'test/unit'
|
2
2
|
require 'patchmaster'
|
3
|
+
require 'support/mock_ports'
|
4
|
+
require 'support/test_connection'
|
3
5
|
|
4
6
|
# For all tests, make sure mock I/O MIDI ports are used.
|
5
7
|
PM::PatchMaster.instance.use_midi = false
|
6
|
-
|
7
|
-
module PM
|
8
|
-
|
9
|
-
# To help with testing, we replace PM::MockInputPort#gets and
|
10
|
-
# PM::MockOutputPort#puts with versions that send what we want and save what
|
11
|
-
# is received.
|
12
|
-
class MockInputPort
|
13
|
-
|
14
|
-
attr_accessor :data_to_send
|
15
|
-
|
16
|
-
def initialize(arg)
|
17
|
-
@name = "MockInputPort #{arg}"
|
18
|
-
@t0 = (Time.now.to_f * 1000).to_i
|
19
|
-
end
|
20
|
-
|
21
|
-
def gets
|
22
|
-
retval = @data_to_send || []
|
23
|
-
@data_to_send = []
|
24
|
-
[{:data => retval, :timestamp => (Time.now.to_f * 1000).to_i - @t0}]
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
class MockOutputPort
|
29
|
-
|
30
|
-
attr_accessor :buffer
|
31
|
-
|
32
|
-
def initialize(port_num)
|
33
|
-
@name = "MockOutputPort #{port_num}"
|
34
|
-
@buffer = []
|
35
|
-
end
|
36
|
-
|
37
|
-
def puts(bytes)
|
38
|
-
@buffer += bytes
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
# A TestConnection records all bytes received and passes them straight
|
44
|
-
# through.
|
45
|
-
class TestConnection < PM::Connection
|
46
|
-
|
47
|
-
attr_accessor :bytes_received
|
48
|
-
|
49
|
-
def midi_in(bytes)
|
50
|
-
@bytes_received ||= []
|
51
|
-
@bytes_received += bytes
|
52
|
-
midi_out(bytes)
|
53
|
-
end
|
54
|
-
|
55
|
-
end
|
metadata
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: patchmaster
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jim Menard
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-09-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: midi-eye
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
description: |
|
@@ -36,6 +36,8 @@ files:
|
|
36
36
|
- bin/irb_init.rb
|
37
37
|
- bin/patchmaster
|
38
38
|
- lib/patchmaster.rb
|
39
|
+
- lib/patchmaster/code_chunk.rb
|
40
|
+
- lib/patchmaster/code_key.rb
|
39
41
|
- lib/patchmaster/connection.rb
|
40
42
|
- lib/patchmaster/consts.rb
|
41
43
|
- lib/patchmaster/curses/geometry.rb
|
@@ -68,6 +70,7 @@ files:
|
|
68
70
|
- lib/patchmaster/web/public/js/patchmaster.js
|
69
71
|
- lib/patchmaster/web/public/style.css
|
70
72
|
- lib/patchmaster/web/sinatra_app.rb
|
73
|
+
- test/support/test_connection.rb
|
71
74
|
- test/test_helper.rb
|
72
75
|
homepage: http://www.patchmaster.org/
|
73
76
|
licenses:
|
@@ -79,19 +82,20 @@ require_paths:
|
|
79
82
|
- lib
|
80
83
|
required_ruby_version: !ruby/object:Gem::Requirement
|
81
84
|
requirements:
|
82
|
-
- -
|
85
|
+
- - ">="
|
83
86
|
- !ruby/object:Gem::Version
|
84
87
|
version: '0'
|
85
88
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
89
|
requirements:
|
87
|
-
- -
|
90
|
+
- - ">="
|
88
91
|
- !ruby/object:Gem::Version
|
89
92
|
version: '0'
|
90
93
|
requirements: []
|
91
94
|
rubyforge_project:
|
92
|
-
rubygems_version: 2.
|
95
|
+
rubygems_version: 2.6.11
|
93
96
|
signing_key:
|
94
97
|
specification_version: 4
|
95
98
|
summary: Realtime MIDI setup configuration and MIDI filtering
|
96
99
|
test_files:
|
100
|
+
- test/support/test_connection.rb
|
97
101
|
- test/test_helper.rb
|