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 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