patchmaster 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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