patchmaster 1.1.2 → 2.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.
- 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
|