patchmaster 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/bin/patchmaster CHANGED
@@ -12,17 +12,20 @@
12
12
  require 'optparse'
13
13
 
14
14
  use_midi = true
15
+ use_gui = true
15
16
  OptionParser.new do |opts|
16
17
  opts.banner = "usage: patchmaster [options] [pm_file]"
17
18
  opts.on("-d", "--debug", "Turn on debug mode") { $DEBUG = true }
18
19
  opts.on("-n", "--no-midi", "Turn off MIDI processing") { use_midi = false }
20
+ opts.on("-t", "--text", "--nw", "--no-window", "No windows") { use_gui = false }
19
21
  end.parse!(ARGV)
20
22
 
21
23
  # Must require patchmaster here, after handling options, because Singleton
22
- # initialze code checks $DEBUG.
24
+ # initialize code checks $DEBUG.
23
25
  require 'patchmaster'
24
26
 
25
- app = PM::Main.instance
27
+ app = use_gui ? PM::Main.instance : PM::PatchMaster.instance
28
+ app.no_gui! if !use_gui
26
29
  app.no_midi! if !use_midi
27
30
  app.load(ARGV[0]) if ARGV[0]
28
31
  app.run
@@ -6,8 +6,8 @@ p, right - prev song
6
6
  g - goto song
7
7
  t - goto song list
8
8
 
9
+ h - help
9
10
  ESC - panic
10
- F1 - help
11
11
 
12
12
  l - load
13
13
  s - save
@@ -9,18 +9,21 @@ class Main
9
9
  include Singleton
10
10
  include Curses
11
11
 
12
+ FUNCTION_KEY_SYMBOLS = {}
13
+ 12.times do |i|
14
+ FUNCTION_KEY_SYMBOLS["f#{i+1}".to_sym] = Key::F1 + i
15
+ FUNCTION_KEY_SYMBOLS["F#{i+1}".to_sym] = Key::F1 + i
16
+ end
17
+
12
18
  def initialize
13
19
  @pm = PatchMaster.instance
20
+ @message_bindings = {}
14
21
  end
15
22
 
16
23
  def no_midi!
17
24
  @pm.no_midi!
18
25
  end
19
26
 
20
- def load(file)
21
- @pm.load(file)
22
- end
23
-
24
27
  def run
25
28
  @pm.start
26
29
  begin
@@ -49,8 +52,9 @@ class Main
49
52
  @pm.goto_song_list(name)
50
53
  when 'e'
51
54
  close_screen
52
- @pm.edit
53
- when Key::F1
55
+ file = @loaded_file || PromptWindow.new('Edit', 'Edit file:').gets
56
+ edit(file)
57
+ when 'h'
54
58
  help
55
59
  when 27 # "\e" doesn't work here
56
60
  # Twice in a row sends individual note-off commands
@@ -60,7 +64,7 @@ class Main
60
64
  when 'l'
61
65
  file = PromptWindow.new('Load', 'Load file:').gets
62
66
  begin
63
- @pm.load(file)
67
+ load(file)
64
68
  message("Loaded #{file}")
65
69
  rescue => ex
66
70
  message(ex.to_s)
@@ -68,7 +72,7 @@ class Main
68
72
  when 's'
69
73
  file = PromptWindow.new('Save', 'Save into file:').gets
70
74
  begin
71
- @pm.save(file)
75
+ save(file)
72
76
  message("Saved #{file}")
73
77
  rescue => ex
74
78
  message(ex.to_s)
@@ -88,6 +92,9 @@ class Main
88
92
  message(ex.to_s)
89
93
  @pm.debug caller.join("\n")
90
94
  end
95
+
96
+ msg_name = @message_bindings[ch]
97
+ @pm.send_message(msg_name) if msg_name
91
98
  end
92
99
  ensure
93
100
  clear
@@ -98,6 +105,10 @@ class Main
98
105
  end
99
106
  end
100
107
 
108
+ def bind_message(name, key_sym)
109
+ @message_bindings[FUNCTION_KEY_SYMBOLS[key_sym]] = name
110
+ end
111
+
101
112
  def config_curses
102
113
  init_screen
103
114
  cbreak # unbuffered input
@@ -133,6 +144,40 @@ class Main
133
144
 
134
145
  end
135
146
 
147
+ def load(file)
148
+ @pm.load(file)
149
+ @loaded_file = file
150
+ end
151
+
152
+ def save(file)
153
+ @pm.save(file)
154
+ @loaded_file = file
155
+ end
156
+
157
+ # Opens the most recently loaded/saved file name in an editor. After
158
+ # editing, the file is re-loaded.
159
+ def edit(file)
160
+ editor_command = find_editor
161
+ unless editor_command
162
+ message("Can not find $VISUAL, $EDITOR, vim, or vi on your path")
163
+ return
164
+ end
165
+
166
+ cmd = "#{editor_command} #{file}"
167
+ @pm.debug(cmd)
168
+ system(cmd)
169
+ load(file)
170
+ @loaded_file = file
171
+ end
172
+
173
+ # Return the first legit command from $VISUAL, $EDITOR, vim, vi, and
174
+ # notepad.exe.
175
+ def find_editor
176
+ @editor ||= [ENV['VISUAL'], ENV['EDITOR'], 'vim', 'vi', 'notepad.exe'].compact.detect do |cmd|
177
+ system('which', cmd) || File.exist?(cmd)
178
+ end
179
+ end
180
+
136
181
  def help
137
182
  message("Help: not yet implemented")
138
183
  end
@@ -14,10 +14,10 @@ class TriggerWindow < PmWindow
14
14
  super
15
15
  pm = PM::PatchMaster.instance
16
16
  i = 0
17
- pm.inputs.each do |sym, instrument|
17
+ pm.inputs.each do |instrument|
18
18
  instrument.triggers.each do |trigger|
19
19
  @win.setpos(i+1, 1)
20
- @win.addstr(make_fit(":#{sym} #{trigger.to_s}"))
20
+ @win.addstr(make_fit(":#{instrument.sym} #{trigger.to_s}"))
21
21
  i += 1
22
22
  end
23
23
  end
@@ -14,6 +14,8 @@ class DSL
14
14
 
15
15
  def load(file)
16
16
  contents = IO.read(file)
17
+ @inputs = {}
18
+ @outputs = {}
17
19
  @triggers = []
18
20
  @filters = []
19
21
  @songs = {} # key = name, value = song
@@ -23,21 +25,39 @@ class DSL
23
25
  end
24
26
 
25
27
  def input(port_num, sym, name=nil)
26
- @pm.inputs[sym] = InputInstrument.new(name || sym.to_s, port_num, @no_midi)
28
+ raise "input: two inputs can not have the same symbol (:#{sym})" if @inputs[sym]
29
+
30
+ input = InputInstrument.new(sym, name, port_num, @no_midi)
31
+ @inputs[sym] = input
32
+ @pm.inputs << input
27
33
  rescue => ex
28
34
  raise "input: error creating input instrument \"#{name}\" on input port #{port_num}: #{ex}"
29
35
  end
30
36
  alias_method :in, :input
31
37
 
32
38
  def output(port_num, sym, name=nil)
33
- @pm.outputs[sym] = OutputInstrument.new(name || sym.to_s, port_num, @no_midi)
39
+ raise "output: two outputs can not have the same symbol (:#{sym})" if @outputs[sym]
40
+
41
+ output = OutputInstrument.new(sym, name, port_num, @no_midi)
42
+ @outputs[sym] = output
43
+ @pm.outputs << output
34
44
  rescue => ex
35
45
  raise "output: error creating output instrument \"#{name}\" on output port #{port_num}: #{ex}"
36
46
  end
37
47
  alias_method :out, :output
38
48
 
49
+ def message(name, bytes)
50
+ @pm.messages[name] = bytes
51
+ end
52
+
53
+ def message_key(name, key_sym)
54
+ if !@pm.no_gui # TODO get rid of double negative
55
+ PM::Main.instance.bind_message(name, key_sym)
56
+ end
57
+ end
58
+
39
59
  def trigger(instrument_sym, bytes, &block)
40
- instrument = @pm.inputs[instrument_sym]
60
+ instrument = @inputs[instrument_sym]
41
61
  raise "trigger: error finding instrument #{instrument_sym}" unless instrument
42
62
  t = Trigger.new(bytes, block)
43
63
  instrument.triggers << t
@@ -65,10 +85,10 @@ class DSL
65
85
  end
66
86
 
67
87
  def connection(in_sym, in_chan, out_sym, out_chan)
68
- input = @pm.inputs[in_sym]
88
+ input = @inputs[in_sym]
69
89
  in_chan = nil if in_chan == :all || in_chan == :any
70
90
  raise "can't find input instrument #{in_sym}" unless input
71
- output = @pm.outputs[out_sym]
91
+ output = @outputs[out_sym]
72
92
  raise "can't find outputput instrument #{out_sym}" unless output
73
93
 
74
94
  @conn = Connection.new(input, in_chan, output, out_chan)
@@ -130,19 +150,19 @@ class DSL
130
150
  end
131
151
 
132
152
  def save_instruments(f)
133
- @pm.inputs.each do |sym, instr|
134
- f.puts "input #{instr.port_num}, :#{sym}, #{quoted(instr.name)}"
153
+ @pm.inputs.each do |instr|
154
+ f.puts "input #{instr.port_num}, :#{instr.sym}, #{quoted(instr.name)}"
135
155
  end
136
- @pm.outputs.each do |sym, instr|
137
- f.puts "output #{instr.port_num}, :#{sym}, #{quoted(instr.name)}"
156
+ @pm.outputs.each do |instr|
157
+ f.puts "output #{instr.port_num}, :#{instr.sym}, #{quoted(instr.name)}"
138
158
  end
139
159
  f.puts
140
160
  end
141
161
 
142
162
  def save_triggers(f)
143
- @pm.inputs.each do |sym, instrument|
163
+ @pm.inputs.each do |instrument|
144
164
  instrument.triggers.each do |trigger|
145
- str = "trigger :#{sym}, #{trigger.bytes.inspect} #{trigger.text}"
165
+ str = "trigger :#{instrument.sym}, #{trigger.bytes.inspect} #{trigger.text}"
146
166
  f.puts str
147
167
  end
148
168
  end
@@ -166,11 +186,9 @@ class DSL
166
186
  end
167
187
 
168
188
  def save_connection(f, conn)
169
- in_sym = @pm.inputs.key(conn.input)
170
189
  in_chan = conn.input_chan ? conn.input_chan + 1 : 'nil'
171
- out_sym = @pm.outputs.key(conn.output)
172
190
  out_chan = conn.output_chan + 1
173
- f.puts " conn :#{in_sym}, #{in_chan}, :#{out_sym}, #{out_chan} do"
191
+ f.puts " conn :#{conn.input.sym}, #{in_chan}, :#{conn.output.sym}, #{out_chan} do"
174
192
  f.puts " prog_chg #{conn.pc_prog}" if conn.pc?
175
193
  f.puts " zone #{conn.note_num_to_name(conn.zone.begin)}, #{conn.note_num_to_name(conn.zone.end)}" if conn.zone
176
194
  f.puts " xpose #{conn.xpose}" if conn.xpose
@@ -197,22 +215,6 @@ class DSL
197
215
 
198
216
  private
199
217
 
200
- def input_port(port)
201
- if @no_midi
202
- MockInputPort.new
203
- else
204
- UniMIDI::Input.all[port].open
205
- end
206
- end
207
-
208
- def output_port(port)
209
- if @no_midi
210
- MockOutputPort.new
211
- else
212
- UniMIDI::Output.all[port].open
213
- end
214
- end
215
-
216
218
  def read_triggers(contents)
217
219
  read_block_text('trigger', @triggers, contents)
218
220
  end
@@ -5,10 +5,11 @@ module PM
5
5
 
6
6
  class Instrument
7
7
 
8
- attr_reader :name, :port_num, :port
8
+ attr_reader :sym, :name, :port_num, :port
9
9
 
10
- def initialize(name, port_num, port)
11
- @name, @port_num, @port = name, port_num, port
10
+ def initialize(sym, name, port_num, port)
11
+ @sym, @name, @port_num, @port = sym, name, port_num, port
12
+ @name ||= @port.name if @port
12
13
  end
13
14
 
14
15
  end
@@ -21,8 +22,8 @@ class InputInstrument < Instrument
21
22
  attr_reader :listener
22
23
 
23
24
  # If +port+ is nil (the normal case), creates either a real or a mock port
24
- def initialize(name, port_num, no_midi=false)
25
- super(name, port_num, input_port(port_num, no_midi))
25
+ def initialize(sym, name, port_num, no_midi=false)
26
+ super(sym, name, port_num, input_port(port_num, no_midi))
26
27
  @connections = []
27
28
  @triggers = []
28
29
  end
@@ -46,7 +47,10 @@ class InputInstrument < Instrument
46
47
  def stop
47
48
  PatchMaster.instance.debug("instrument #{name} stop")
48
49
  @port.clear_buffer
49
- @listener.close
50
+ if @listener
51
+ @listener.close
52
+ @listener = nil
53
+ end
50
54
  end
51
55
 
52
56
  # Passes MIDI bytes on to triggers and to each output connection.
@@ -59,7 +63,7 @@ class InputInstrument < Instrument
59
63
 
60
64
  def input_port(port_num, no_midi=false)
61
65
  if no_midi
62
- MockInputPort.new
66
+ MockInputPort.new(port_num)
63
67
  else
64
68
  UniMIDI::Input.all[port_num].open
65
69
  end
@@ -69,8 +73,8 @@ end
69
73
 
70
74
  class OutputInstrument < Instrument
71
75
 
72
- def initialize(name, port_num, no_midi=false)
73
- super(name, port_num, output_port(port_num, no_midi))
76
+ def initialize(sym, name, port_num, no_midi=false)
77
+ super(sym, name, port_num, output_port(port_num, no_midi))
74
78
  end
75
79
 
76
80
  def midi_out(bytes)
@@ -81,7 +85,7 @@ class OutputInstrument < Instrument
81
85
 
82
86
  def output_port(port_num, no_midi)
83
87
  if no_midi
84
- MockOutputPort.new
88
+ MockOutputPort.new(port_num)
85
89
  else
86
90
  UniMIDI::Output.all[port_num].open
87
91
  end
@@ -90,12 +94,16 @@ end
90
94
 
91
95
  class MockInputPort
92
96
 
97
+ attr_reader :name
98
+
93
99
  # For MIDIEye::Listener
94
100
  def self.is_compatible?(input)
95
101
  true
96
102
  end
97
103
 
98
- def initialize(_=nil)
104
+ # Constructor param is ignored; it's required by MIDIEye.
105
+ def initialize(arg)
106
+ @name = "MockInputPort #{arg}"
99
107
  end
100
108
 
101
109
  def gets
@@ -115,6 +123,13 @@ class MockInputPort
115
123
  end
116
124
 
117
125
  class MockOutputPort
126
+
127
+ attr_reader :name
128
+
129
+ def initialize(port_num)
130
+ @name = "MockOutputPort #{port_num}"
131
+ end
132
+
118
133
  def puts(data)
119
134
  end
120
135
  end
@@ -20,7 +20,9 @@ class PatchMaster < SimpleDelegator
20
20
 
21
21
  include Singleton
22
22
 
23
- attr_reader :inputs, :outputs, :all_songs, :song_lists, :no_midi
23
+ attr_reader :inputs, :outputs, :all_songs, :song_lists
24
+ attr_reader :messages
25
+ attr_reader :no_midi, :no_gui
24
26
 
25
27
  # A Cursor to which we delegate incoming position methods (#song_list,
26
28
  # #song, #patch, #next_song, #prev_patch, etc.)
@@ -29,11 +31,12 @@ class PatchMaster < SimpleDelegator
29
31
  def initialize
30
32
  @cursor = Cursor.new(self)
31
33
  super(@cursor)
34
+ @no_midi = false
35
+ @no_gui = false
32
36
 
33
37
  if $DEBUG
34
38
  @debug_file = File.open(DEBUG_FILE, 'a')
35
39
  end
36
- @no_midi = false
37
40
 
38
41
  init_data
39
42
  end
@@ -42,6 +45,10 @@ class PatchMaster < SimpleDelegator
42
45
  @no_midi = true
43
46
  end
44
47
 
48
+ def no_gui!
49
+ @no_gui = true
50
+ end
51
+
45
52
  # Loads +file+. Does its best to restore the current song list, song, and
46
53
  # patch after loading.
47
54
  def load(file)
@@ -51,7 +58,6 @@ class PatchMaster < SimpleDelegator
51
58
  @cursor.mark
52
59
  init_data
53
60
  DSL.new(@no_midi).load(file)
54
- @loaded_file = file
55
61
  @cursor.restore
56
62
 
57
63
  if restart
@@ -65,7 +71,6 @@ class PatchMaster < SimpleDelegator
65
71
 
66
72
  def save(file)
67
73
  DSL.new(@no_midi).save(file)
68
- @loaded_file = file
69
74
  rescue => ex
70
75
  raise("error saving #{file}: #{ex}" + caller.join("\n"))
71
76
  end
@@ -73,11 +78,12 @@ class PatchMaster < SimpleDelegator
73
78
  # Initializes the cursor and all data.
74
79
  def init_data
75
80
  @cursor.clear
76
- @inputs = {}
77
- @outputs = {}
81
+ @inputs = []
82
+ @outputs = []
78
83
  @song_lists = []
79
84
  @all_songs = SortedSongList.new('All Songs')
80
85
  @song_lists << @all_songs
86
+ @messages = {}
81
87
  end
82
88
 
83
89
  # If +init_cursor+ is +true+ (the default), initializes current song list,
@@ -86,25 +92,55 @@ class PatchMaster < SimpleDelegator
86
92
  @cursor.init if init_cursor
87
93
  @cursor.patch.start if @cursor.patch
88
94
  @running = true
89
- @inputs.values.map(&:start)
95
+ @inputs.map(&:start)
90
96
  end
91
97
 
98
+ # Stop everything, including input instruments' MIDIEye listener threads.
92
99
  def stop
93
100
  @cursor.patch.stop if @cursor.patch
94
- @inputs.values.map(&:stop)
101
+ @inputs.map(&:stop)
95
102
  @running = false
96
103
  end
97
104
 
105
+ # Run PatchMaster without the GUI. Don't use this when using PM::Main.
106
+ #
107
+ # Call #start, wait for inputs' MIDIEye listener threads to finish, then
108
+ # call #stop. Note that normally nothing stops those threads, so this is
109
+ # used as a way to make sure the script doesn't quit until killed by
110
+ # something like SIGINT.
111
+ def run
112
+ start(true)
113
+ @inputs.each { |input| input.listener.join }
114
+ stop
115
+ end
116
+
98
117
  def running?
99
118
  @running
100
119
  end
101
120
 
121
+ # Send the message with the given name to all outputs.
122
+ def send_message(name)
123
+ msg = @messages[name]
124
+ if !msg
125
+ message("Message \"#{name}\" not found")
126
+ return
127
+ end
128
+
129
+ debug("Sending message \"#{name}\"")
130
+ @outputs.each { |out| out.midi_out(msg) }
131
+
132
+ # If the user accidentally calls send_message in a filter at the end,
133
+ # then the filter will return whatever this method returns. Just in
134
+ # case, return nil instead of whatever the preceding code would return.
135
+ nil
136
+ end
137
+
102
138
  # Sends the +CM_ALL_NOTES_OFF+ controller message to all output
103
139
  # instruments on all 16 MIDI channels. If +individual_notes+ is +true+
104
140
  # send individual +NOTE_OFF+ messages to all notes as well.
105
141
  def panic(individual_notes=false)
106
142
  debug("panic(#{individual_notes})")
107
- @outputs.values.each do |out|
143
+ @outputs.each do |out|
108
144
  MIDI_CHANNELS.times do |chan|
109
145
  out.midi_out([CONTROLLER + chan, CM_ALL_NOTES_OFF, 0])
110
146
  if individual_notes
@@ -114,29 +150,6 @@ class PatchMaster < SimpleDelegator
114
150
  end
115
151
  end
116
152
 
117
- # Opens the most recently loaded/saved file name in an editor. After
118
- # editing, the file is re-loaded.
119
- def edit
120
- editor_command = find_editor
121
- unless editor_command
122
- message("Can not find $VISUAL, $EDITOR, vim, or vi on your path")
123
- return
124
- end
125
-
126
- cmd = "#{editor_command} #{@loaded_file}"
127
- debug(cmd)
128
- system(cmd)
129
- load(@loaded_file)
130
- end
131
-
132
- # Return the first legit command from $VISUAL, $EDITOR, vim, vi, and
133
- # notepad.exe.
134
- def find_editor
135
- @editor ||= [ENV['VISUAL'], ENV['EDITOR'], 'vim', 'vi', 'notepad.exe'].compact.detect do |cmd|
136
- system('which', cmd) || File.exist?(cmd)
137
- end
138
- end
139
-
140
153
  # Output +str+ to @debug_file or $stderr.
141
154
  def debug(str)
142
155
  return unless $DEBUG
@@ -62,6 +62,11 @@ class Integer
62
62
  alias_method :rt?, :realtime?
63
63
  end
64
64
 
65
+ # All the methods here delegate to the first byte in the array, so for
66
+ # example the following two are equivalent:
67
+ #
68
+ # my_array.note_on?
69
+ # my_array[0].note_on?
65
70
  class Array
66
71
 
67
72
  def high_nibble
data/test/test_helper.rb CHANGED
@@ -7,19 +7,15 @@ PM::PatchMaster.instance.no_midi!
7
7
 
8
8
  module PM
9
9
 
10
- # To help with testing, we replace MockInputPort#gets and
11
- # MockOutputPort#puts with versions that send what we want and save what is
12
- # received.
10
+ # To help with testing, we replace PM::MockInputPort#gets and
11
+ # PM::MockOutputPort#puts with versions that send what we want and save what
12
+ # is received.
13
13
  class MockInputPort
14
14
 
15
15
  attr_accessor :data_to_send
16
-
17
- # For MIDIEye::Listener
18
- def self.is_compatible?(input)
19
- true
20
- end
21
16
 
22
- def initialize(_=nil)
17
+ def initialize(arg)
18
+ @name = "MockInputPort #{arg}"
23
19
  @t0 = (Time.now.to_f * 1000).to_i
24
20
  end
25
21
 
@@ -28,20 +24,14 @@ class MockInputPort
28
24
  @data_to_send = []
29
25
  [{:data => retval, :timestamp => (Time.now.to_f * 1000).to_i - @t0}]
30
26
  end
31
-
32
- def poll
33
- yield gets
34
- end
35
-
36
- # add this class to the Listener class' known input types
37
- MIDIEye::Listener.input_types << self
38
27
  end
39
28
 
40
29
  class MockOutputPort
41
30
 
42
31
  attr_accessor :buffer
43
32
 
44
- def initialize
33
+ def initialize(port_num)
34
+ @name = "MockOutputPort #{port_num}"
45
35
  @buffer = []
46
36
  end
47
37
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: patchmaster
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-04-09 00:00:00.000000000 Z
12
+ date: 2012-04-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: unimidi
16
- requirement: &2156042600 !ruby/object:Gem::Requirement
15
+ name: midi-eye
16
+ requirement: &2156041300 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,7 +21,7 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2156042600
24
+ version_requirements: *2156041300
25
25
  description: ! 'PatchMaster is realtime MIDI performance software that alloweds a
26
26
  musician
27
27