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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3c68fa48a4f24f2e5b415950ca74445e84a63961
4
- data.tar.gz: 2d359863417126a8e63eb401ccabe58727687b0e
3
+ metadata.gz: c0a690b803a2cbb24400d24f16f576e77f528638
4
+ data.tar.gz: c88d291e8e2b4849f3202544a7c14fc1235037f7
5
5
  SHA512:
6
- metadata.gz: f79589d65c03ea1e0f6371ce149b0d5fff3c2d4b7f725f55afb9aa093f0813efefdd28a4276a370c263c39a618ddd7bb684fce49a76b0c6919a14f4904bb9c2a
7
- data.tar.gz: 289d7ab401a04bf15cde5041cc4ce636e22893c6172d96142c116a5c655657cc0cf07df0f0d21a4a09823a0a15ada583c78143764d7e28bff728485428228cb8
6
+ metadata.gz: a6c9c655d1c47ebbda7f687af55e2c51d717926a27e4942e1611bd0c599c426e3dbb3ca0b3ffd4b514daa92aedbccf61b04cebb03a6089d20719a6721b9cac8e
7
+ data.tar.gz: 962920f0541fda505f4264ed524d054a2d0bcbb9d63321353c131aefffa12f3a06f6ab2520d2c3f0a83ea61696b0c3410bc313e5ece3b0b85340349a720e1cb2
@@ -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
- require 'patchmaster'
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
- start_patchmaster_irb(File.join(File.dirname(__FILE__), 'irb_init.rb'))
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
@@ -8,4 +8,5 @@ require 'patchmaster/filter'
8
8
  require 'patchmaster/instrument'
9
9
  require 'patchmaster/patchmaster'
10
10
  require 'patchmaster/trigger'
11
+ require 'patchmaster/code_key'
11
12
  require 'patchmaster/dsl'
@@ -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.note? && bytes.channel == @input_chan
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
@@ -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
- CC_BANK_SELECT = 0
95
- CC_MOD_WHEEL = 1
96
- CC_BREATH_CONTROLLER = 2
97
- CC_FOOT_CONTROLLER = 4
98
- CC_PORTAMENTO_TIME = 5
99
- CC_DATA_ENTRY_MSB = 6
100
- CC_VOLUME = 7
101
- CC_BALANCE = 8
102
- CC_PAN = 10
103
- CC_EXPRESSION_CONTROLLER = 11
104
- CC_GEN_PURPOSE_1 = 16
105
- CC_GEN_PURPOSE_2 = 17
106
- CC_GEN_PURPOSE_3 = 18
107
- CC_GEN_PURPOSE_4 = 19
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
- CC_DATA_ENTRY_LSB = 38
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
- # Momentaries:
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
- "0",
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", "26", "27", "28", "29",
168
- "30", "31",
169
- "32", "33", "34", "35", "36", "37", "38", "39", "40", "41",
170
- "42", "43", "44", "45", "46", "47", "48", "49", "50", "51",
171
- "52", "53", "54", "55", "56", "57", "58", "59", "60", "61",
172
- "62", "63",
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
- curr_index = @list.index(curr_item)
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
@@ -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. A Cursor does not start/stop patches or manage connections.
5
+ # regexes.
6
6
  class Cursor
7
7
 
8
8
  attr_reader :song_list, :song, :patch
@@ -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(name, key_or_sym)
59
- if @pm.gui
60
- @pm.gui.bind_message(name, key_or_sym)
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}, #{quoted(instr.name)}"
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}, #{quoted(instr.name)}"
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 #{quoted(song.name)} do"
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 #{quoted(patch.name)} do"
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 #{quoted(sl.name)}, ["
282
+ f.puts "song_list #{sl.name.inspect}, ["
233
283
  @pm.all_songs.songs.each do |song|
234
- f.puts " #{quoted(song.name)},"
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].text = text + "\n"
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
- containers[i].text << line
353
+ chunk.text << line
285
354
  end
286
355
  in_block = false
287
356
  else
288
- containers[i].text << line
357
+ chunk.text << line
289
358
  end
290
359
  end
291
360
  end
292
- containers.each { |thing| thing.text.strip! if thing.text }
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
@@ -5,18 +5,18 @@ module PM
5
5
  # representation as well.
6
6
  class Filter
7
7
 
8
- attr_accessor :block, :text
8
+ attr_accessor :code_chunk
9
9
 
10
- def initialize(block, text=nil)
11
- @block, @text = block, text
10
+ def initialize(code_chunk)
11
+ @code_chunk = code_chunk
12
12
  end
13
13
 
14
14
  def call(conn, bytes)
15
- @block.call(conn, bytes)
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)
@@ -4,17 +4,28 @@ require 'tempfile'
4
4
 
5
5
  $dsl = nil
6
6
 
7
- # For bin/patchmaster. Does nothing.
8
- def run
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
- unless $dsl
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 $dsl. This version tells panic to send
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
@@ -11,7 +11,7 @@ class Integer
11
11
  end
12
12
 
13
13
  def channel?
14
- self >= PM::NOTE_ON && self < PM::SYSEX
14
+ self >= PM::NOTE_OFF && self < PM::SYSEX
15
15
  end
16
16
  alias_method :chan?, :channel?
17
17
 
@@ -1,6 +1,6 @@
1
1
  module PM
2
2
 
3
- # A Song is a named list of Patches with a cursor.
3
+ # A Song is a named list of Patches.
4
4
  class Song
5
5
 
6
6
  attr_accessor :name, :patches, :notes
@@ -1,6 +1,6 @@
1
1
  module PM
2
2
 
3
- # A SongList is a list of Songs with a cursor.
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. Does not
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 }
@@ -1,34 +1,33 @@
1
1
  module PM
2
2
 
3
- # A Trigger performs an action when it sees a particular array of bytes.
4
- # Instruments have zero or more triggers. The action is a symbol that gets
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, :block, :text
10
+ attr_accessor :bytes, :code_chunk
12
11
 
13
- def initialize(bytes, block)
14
- @bytes, @block = bytes, block
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 +@block+.
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.instance_eval &@block
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
- cycle_colors = () ->
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
- color_scheme_index = (color_scheme_index + 1) % COLOR_SCHEMES.length
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
- color_scheme = base_class
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
- return $('#patch').html(CONN_HEADERS + "\n" + rows.join("\n"));
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
- cycle_colors = function() {
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
- color_scheme_index = (color_scheme_index + 1) % COLOR_SCHEMES.length;
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
- return color_scheme = base_class;
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
@@ -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: 1.1.2
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: 2013-12-01 00:00:00.000000000 Z
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.1.10
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