patchmaster 1.0.0 → 1.1.2

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3c68fa48a4f24f2e5b415950ca74445e84a63961
4
+ data.tar.gz: 2d359863417126a8e63eb401ccabe58727687b0e
5
+ SHA512:
6
+ metadata.gz: f79589d65c03ea1e0f6371ce149b0d5fff3c2d4b7f725f55afb9aa093f0813efefdd28a4276a370c263c39a618ddd7bb684fce49a76b0c6919a14f4904bb9c2a
7
+ data.tar.gz: 289d7ab401a04bf15cde5041cc4ce636e22893c6172d96142c116a5c655657cc0cf07df0f0d21a4a09823a0a15ada583c78143764d7e28bff728485428228cb8
@@ -0,0 +1,17 @@
1
+ # Set IRB prompt
2
+ IRB.conf[:PROMPT][:CUSTOM] = {
3
+ :PROMPT_I=>"PatchMaster:%03n:%i> ",
4
+ :PROMPT_N=>"PatchMaster:%03n:%i> ",
5
+ :PROMPT_S=>"PatchMaster:%03n:%i%l ",
6
+ :PROMPT_C=>"PatchMaster:%03n:%i* ",
7
+ :RETURN=>"=> %s\n"
8
+ }
9
+ IRB.conf[:PROMPT_MODE] = :CUSTOM
10
+
11
+ # Load ./.patchmasterrc or $HOME/.patchmasterrc
12
+ rc_file = File.join('.', '.patchmasterrc')
13
+ rc_file = File.join(ENV['HOME'], '.patchmasterrc') unless File.exist?(rc_file)
14
+ load(rc_file) if File.exist?(rc_file)
15
+
16
+ puts 'PatchMaster loaded'
17
+ puts 'Type "pm_help" for help'
@@ -1,28 +1,25 @@
1
1
  #!/usr/bin/env ruby
2
2
  #
3
- # usage: patchmaster [-n] [-i] [-w] [-p port] [-t] [-d] [pm_file]
3
+ # usage: patchmaster [-v] [-n] [-i] [-w] [-p port] [-d] [pm_file]
4
4
  #
5
5
  # Starts PatchMaster and optionally loads pm_file.
6
6
  #
7
+ # -v outputs the version number and exits.
8
+ #
7
9
  # The -n flag tells PatchMaster to not use MIDI. All MIDI errors such as not
8
10
  # being able to connect to the MIDI instruments specified in pm_file are
9
11
  # ignored, and no MIDI data is sent/received. That is useful if you want to
10
12
  # run PatchMaster without actually talking to any MIDI instruments.
11
13
  #
12
- # To run PatchMaster from within an IRB session use -i. See the
14
+ # To run PatchMaster from within an IRB session use -i. Reads
15
+ # ./.patchmasterrc if it exists, $HOME/.patchmasterrc if not. See the
13
16
  # documentation for details on the commands that are available.
14
17
  #
15
18
  # To run PatchMaster using a Web browser GUI use -w and point your browser
16
19
  # at http://localhost:4567. To change the port, use -p.
17
20
  #
18
- # To run PatchMaster without a GUI use -t. All output will go to the
19
- # console. The app will run until interrupted. (If you do this, you might
20
- # want to create a trigger that calls `panic', because you won't be able to
21
- # use the computer keyboard to do that.)
22
- #
23
- # The =-d= flag turns on debug mode. The app becomes slightly more verbose
24
- # and logs everything to `/tmp/pm_debug.txt'.
25
-
21
+ # The -d flag turns on debug mode. The app becomes slightly more verbose and
22
+ # logs everything to `/tmp/pm_debug.txt'.
26
23
 
27
24
  require 'optparse'
28
25
 
@@ -36,7 +33,12 @@ OptionParser.new do |opts|
36
33
  opts.on("-i", "--irb", "Use an IRB console") { gui = :irb }
37
34
  opts.on("-w", "--web", "Use a Web browser GUI") { gui = :web }
38
35
  opts.on("-p", "--port PORT", "Web browser GUI port number") { |opt| port = opt.to_i }
39
- opts.on("-t", "--text", "--nw", "--no-window", "No windows") { gui = :text }
36
+ opts.on("-v", "--version", "Show version number and exit") do
37
+ version_line = IO.readlines(File.join(File.dirname(__FILE__), '../Rakefile')).grep(/GEM_VERSION\s*=/).first
38
+ version_line =~ /(\d+\.\d+\.\d+)/
39
+ puts "patchmaster #{$1}"
40
+ exit 0
41
+ end
40
42
  opts.on_tail("-h", "-?", "--help", "Show this message") do
41
43
  puts opts
42
44
  exit 0
@@ -56,8 +58,8 @@ when :curses
56
58
  pm.gui = PM::Main.instance
57
59
  pm.run
58
60
  when :irb
59
- require 'patchmaster/irb'
60
- start_patchmaster_irb
61
+ require 'patchmaster/irb/irb'
62
+ start_patchmaster_irb(File.join(File.dirname(__FILE__), 'irb_init.rb'))
61
63
  when :web
62
64
  require 'patchmaster/web/sinatra_app'
63
65
  app = PM::SinatraApp.instance
@@ -18,7 +18,7 @@ class Connection
18
18
  # turned into 0-based channels for later use.
19
19
  def initialize(input, input_chan, output, output_chan, filter=nil, opts={})
20
20
  @input, @input_chan, @output, @output_chan, @filter = input, input_chan, output, output_chan, filter
21
- @pc_prog, @zone, @xpose = opts[:pc_prog], opts[:zone], opts[:xpose]
21
+ @bank, @pc_prog, @zone, @xpose = opts[:bank], opts[:pc_prog], opts[:zone], opts[:xpose]
22
22
 
23
23
  @input_chan -= 1 if @input_chan
24
24
  @output_chan -= 1 if @output_chan
@@ -27,7 +27,8 @@ class Connection
27
27
  def start(start_bytes=nil)
28
28
  bytes = []
29
29
  bytes += start_bytes if start_bytes
30
- bytes += [CC_BANK_SELECT + @output_chan, @bank] if @bank
30
+ # Bank select uses MSB if we're only sending one byte
31
+ bytes += [CONTROLLER + @output_chan, CC_BANK_SELECT+32, @bank] if @bank
31
32
  bytes += [PROGRAM_CHANGE + @output_chan, @pc_prog] if @pc_prog
32
33
  midi_out(bytes) unless bytes.empty?
33
34
  @input.add_connection(self)
@@ -50,21 +51,50 @@ class Connection
50
51
  @zone == nil || @zone.include?(note)
51
52
  end
52
53
 
54
+ # The workhorse. Ignore bytes that aren't from our input, or are outside
55
+ # the zone. Change to output channel. Filter.
56
+ #
57
+ # Note that running bytes are not handled, but unimidi doesn't seem to use
58
+ # them anyway.
59
+ #
60
+ # Finally, we go through gyrations to avoid duping bytes unless they are
61
+ # actually modified in some way.
53
62
  def midi_in(bytes)
54
63
  return unless accept_from_input?(bytes)
55
64
 
56
- # TODO handle running bytes if needed
65
+ bytes_duped = false
66
+
57
67
  high_nibble = bytes.high_nibble
58
68
  case high_nibble
59
69
  when NOTE_ON, NOTE_OFF, POLY_PRESSURE
60
70
  return unless inside_zone?(bytes[1])
71
+
72
+ if bytes[0] != high_nibble + @output_chan || (@xpose && @xpose != 0)
73
+ bytes = bytes.dup
74
+ bytes_duped = true
75
+ end
76
+
61
77
  bytes[0] = high_nibble + @output_chan
62
78
  bytes[1] = ((bytes[1] + @xpose) & 0xff) if @xpose
63
79
  when CONTROLLER, PROGRAM_CHANGE, CHANNEL_PRESSURE, PITCH_BEND
64
- bytes[0] = high_nibble + @output_chan
80
+ if bytes[0] != high_nibble + @output_chan
81
+ bytes = bytes.dup
82
+ bytes_duped = true
83
+ bytes[0] = high_nibble + @output_chan
84
+ end
85
+ end
86
+
87
+ # We can't tell if a filter will modify the bytes, so we have to assume
88
+ # they will be. If we didn't, we'd have to rely on the filter duping the
89
+ # bytes and returning the dupe.
90
+ if @filter
91
+ if !bytes_duped
92
+ bytes = bytes.dup
93
+ bytes_duped = true
94
+ end
95
+ bytes = @filter.call(self, bytes)
65
96
  end
66
97
 
67
- bytes = @filter.call(self, bytes) if @filter
68
98
  if bytes && bytes.size > 0
69
99
  midi_out(bytes)
70
100
  end
@@ -88,8 +88,8 @@ EOS
88
88
  SYSTEM_RESET = 0xFF
89
89
 
90
90
  # Controller numbers
91
- # = 0 - 31 = continuous, LSB
92
- # = 32 - 63 = continuous, MSB
91
+ # = 0 - 31 = continuous, MSB
92
+ # = 32 - 63 = continuous, LSB
93
93
  # = 64 - 97 = switches
94
94
  CC_BANK_SELECT = 0
95
95
  CC_MOD_WHEEL = 1
@@ -0,0 +1,53 @@
1
+ module PM
2
+
3
+ # Defines positions and sizes of windows. Rects contain [height, width, top,
4
+ # left], which is the order used by Curses::Window.new.
5
+ class Geometry
6
+
7
+ include Curses
8
+
9
+ def initialize
10
+ @top_height = (lines() - 1) * 2 / 3
11
+ @bot_height = (lines() - 1) - @top_height
12
+ @top_width = cols() / 3
13
+
14
+ @sls_height = @top_height / 3
15
+ @sl_height = @top_height - @sls_height
16
+
17
+ @info_width = cols() - (@top_width * 2)
18
+ @info_left = @top_width * 2
19
+ end
20
+
21
+ def song_list_rect
22
+ [@sl_height, @top_width, 0, 0]
23
+ end
24
+
25
+ def song_rect
26
+ [@sl_height, @top_width, 0, @top_width]
27
+ end
28
+
29
+ def song_lists_rect
30
+ [@sls_height, @top_width, @sl_height, 0]
31
+ end
32
+
33
+ def trigger_rect
34
+ [@sls_height, @top_width, @sl_height, @top_width]
35
+ end
36
+
37
+ def patch_rect
38
+ [@bot_height, cols(), @top_height, 0]
39
+ end
40
+
41
+ def message_rect
42
+ [1, cols(), lines()-1, 0]
43
+ end
44
+
45
+ def info_rect
46
+ [@top_height, @info_width, 0, @info_left]
47
+ end
48
+
49
+ def help_rect
50
+ [lines() - 6, cols() - 6, 3, 3]
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,29 @@
1
+ require 'curses'
2
+
3
+ module PM
4
+ class HelpWindow < PmWindow
5
+
6
+ CONTENTS = File.join(File.dirname(__FILE__), 'info_window_contents.txt')
7
+
8
+ include Curses
9
+
10
+ attr_reader :text
11
+
12
+ def initialize(rows, cols, row, col)
13
+ super(rows, cols, row, col, nil)
14
+ @text = IO.read(CONTENTS)
15
+ @title = 'PatchMaster Help'
16
+ end
17
+
18
+ def draw
19
+ super
20
+ i = 0
21
+ @text.each_line do |line|
22
+ @win.setpos(i+2, 3)
23
+ @win.addstr(make_fit(line.chomp))
24
+ i += 1
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -1,32 +1,39 @@
1
1
  require 'curses'
2
2
 
3
3
  module PM
4
- class InfoWindow
4
+ class InfoWindow < PmWindow
5
5
 
6
6
  CONTENTS = File.join(File.dirname(__FILE__), 'info_window_contents.txt')
7
7
 
8
8
  include Curses
9
9
 
10
- attr_reader :win, :text
11
-
12
- TITLE = ' PatchMaster '
10
+ attr_reader :text
13
11
 
14
12
  def initialize(rows, cols, row, col)
15
- @win = Window.new(rows, cols, row, col)
16
- @text = IO.read(CONTENTS)
13
+ super(rows, cols, row, col, nil)
14
+ @info_text = IO.read(CONTENTS)
15
+ @text = nil
17
16
  end
18
17
 
19
- def draw
20
- @win.setpos(0, (@win.maxx() - TITLE.length) / 2)
21
- @win.attron(A_REVERSE) {
22
- @win.addstr(TITLE)
23
- }
24
- @win.addstr("\n")
25
- @text.each_line { |line| @win.addstr(line) }
18
+ def text=(str)
19
+ if str
20
+ @text = str
21
+ @title = 'Song Notes'
22
+ else
23
+ @text = @info_text
24
+ @title = 'PatchMaster Help'
25
+ end
26
26
  end
27
27
 
28
- def refresh
29
- @win.refresh
28
+ def draw
29
+ super
30
+ i = 1
31
+ @text.each_line do |line|
32
+ break if i >= @win.maxy - 2
33
+ @win.setpos(i+1, 1)
34
+ @win.addstr(make_fit(" #{line.chomp}"))
35
+ i += 1
36
+ end
30
37
  end
31
38
 
32
39
  end
@@ -1,16 +1,17 @@
1
1
  j, down, space - next patch
2
2
  k, up - prev patch
3
- n, left - next song
4
- p, right - prev song
3
+ n, right - next song
4
+ p, left - prev song
5
5
 
6
- g - goto song
7
- t - goto song list
6
+ g - goto song
7
+ t - goto song list
8
8
 
9
- h - help
10
- ESC - panic
9
+ h, ? - help
10
+ ESC - panic
11
11
 
12
- l - load
13
- s - save
14
- e - edit
12
+ l - load
13
+ s - save
14
+ r - reload
15
+ e - edit
15
16
 
16
- q - quit
17
+ q - quit
@@ -5,7 +5,12 @@ class ListWindow < PmWindow
5
5
 
6
6
  attr_reader :list
7
7
 
8
- # +curr_item_method_sym+ is a method symbol that is sent to
8
+ def initialize(rows, cols, row, col, title_prefix)
9
+ super
10
+ @offset = 0
11
+ end
12
+
13
+ # +curr_item_method_sym+ is a method symbol that is sent to
9
14
  # PM::PatchMaster to obtain the current item so we can highlight it.
10
15
  def set_contents(title, list, curr_item_method_sym)
11
16
  @title, @list, @curr_item_method_sym = title, list, curr_item_method_sym
@@ -17,7 +22,15 @@ class ListWindow < PmWindow
17
22
  return unless @list
18
23
 
19
24
  curr_item = PM::PatchMaster.instance.send(@curr_item_method_sym)
20
- @list.each_with_index do |thing, i|
25
+ curr_index = @list.index(curr_item)
26
+
27
+ if curr_index < @offset
28
+ @offset = curr_index
29
+ elsif curr_index >= @offset + visible_height
30
+ @offset = curr_index - visible_height + 1
31
+ end
32
+
33
+ @list[@offset, visible_height].each_with_index do |thing, i|
21
34
  @win.setpos(i+1, 1)
22
35
  @win.attron(A_REVERSE) if thing == curr_item
23
36
  @win.addstr(make_fit(" #{thing.name} "))
@@ -1,6 +1,7 @@
1
1
  require 'curses'
2
2
  require 'singleton'
3
- %w(list patch info trigger prompt).each { |w| require "patchmaster/curses/#{w}_window" }
3
+ require 'patchmaster/curses/geometry'
4
+ %w(list patch info trigger prompt help).each { |w| require "patchmaster/curses/#{w}_window" }
4
5
 
5
6
  module PM
6
7
 
@@ -36,21 +37,23 @@ class Main
36
37
  @pm.next_patch
37
38
  when 'k', Key::UP
38
39
  @pm.prev_patch
39
- when 'n', Key::LEFT
40
+ when 'n', Key::RIGHT
40
41
  @pm.next_song
41
- when 'p', Key::RIGHT
42
+ when 'p', Key::LEFT
42
43
  @pm.prev_song
43
44
  when 'g'
44
45
  name = PromptWindow.new('Go To Song', 'Go to song:').gets
45
- @pm.goto_song(name)
46
+ @pm.goto_song(name) if name.length > 0
46
47
  when 't'
47
48
  name = PromptWindow.new('Go To Song List', 'Go to Song List:').gets
48
- @pm.goto_song_list(name)
49
+ @pm.goto_song_list(name) if name.length > 0
49
50
  when 'e'
50
51
  close_screen
51
- file = @loaded_file || PromptWindow.new('Edit', 'Edit file:').gets
52
- edit(file)
53
- when 'h'
52
+ file = @pm.loaded_file || PromptWindow.new('Edit', 'Edit file:').gets
53
+ edit(file) if file.length > 0
54
+ when 'r'
55
+ load(@pm.loaded_file) if @pm.loaded_file && @pm.loaded_file.length > 0
56
+ when 'h', '?'
54
57
  help
55
58
  when 27 # "\e" doesn't work here
56
59
  # Twice in a row sends individual note-off commands
@@ -59,29 +62,28 @@ class Main
59
62
  message('Panic sent')
60
63
  when 'l'
61
64
  file = PromptWindow.new('Load', 'Load file:').gets
62
- begin
63
- load(file)
64
- message("Loaded #{file}")
65
- rescue => ex
66
- message(ex.to_s)
65
+ if file.length > 0
66
+ begin
67
+ load(file)
68
+ message("Loaded #{file}")
69
+ rescue => ex
70
+ message(ex.to_s)
71
+ end
67
72
  end
68
73
  when 's'
69
74
  file = PromptWindow.new('Save', 'Save into file:').gets
70
- begin
71
- save(file)
72
- message("Saved #{file}")
73
- rescue => ex
74
- message(ex.to_s)
75
- end
76
- when '?'
77
- if $DEBUG
78
- require 'pp'
79
- out = ''
80
- str = pp(@pm, out)
81
- message("pm = #{out}")
75
+ if file.length > 0
76
+ begin
77
+ save(file)
78
+ message("Saved #{file}")
79
+ rescue => ex
80
+ message(ex.to_s)
81
+ end
82
82
  end
83
83
  when 'q'
84
84
  break
85
+ when Key::RESIZE
86
+ resize_windows
85
87
  end
86
88
  @prev_cmd = ch
87
89
  rescue => ex
@@ -118,40 +120,40 @@ class Main
118
120
  end
119
121
 
120
122
  def create_windows
121
- top_height = (lines() - 1) * 2 / 3
122
- bot_height = (lines() - 1) - top_height
123
- top_width = cols() / 3
124
-
125
- sls_height = top_height / 3
126
- sl_height = top_height - sls_height
127
-
128
- @song_lists_win = ListWindow.new(sls_height, top_width, 0, 0, nil)
129
- @song_list_win = ListWindow.new(sl_height, top_width, sls_height, 0, 'Song List')
130
- @song_win = ListWindow.new(top_height, top_width, 0, top_width, 'Song')
131
- @patch_win = PatchWindow.new(bot_height, cols(), top_height, 0, 'Patch')
132
- @message_win = Window.new(1, cols(), lines()-1, 0)
133
- @message_win.scrollok(false)
123
+ g = PM::Geometry.new
134
124
 
135
- third_height = top_height / 3
136
- width = cols() - (top_width * 2) - 1
137
- left = top_width * 2 + 1
125
+ @song_lists_win = ListWindow.new(*g.song_lists_rect, nil)
126
+ @song_list_win = ListWindow.new(*g.song_list_rect, 'Song List')
127
+ @song_win = ListWindow.new(*g.song_rect, 'Song')
128
+ @patch_win = PatchWindow.new(*g.patch_rect, 'Patch')
129
+ @message_win = Window.new(*g.message_rect)
130
+ @trigger_win = TriggerWindow.new(*g.trigger_rect)
131
+ @info_win = InfoWindow.new(*g.info_rect)
138
132
 
139
- @trigger_win = TriggerWindow.new(third_height, width, third_height * 2, left)
140
- @trigger_win.draw
133
+ @message_win.scrollok(false)
134
+ end
141
135
 
142
- @info_win = InfoWindow.new(third_height * 2, width, 0, left)
143
- @info_win.draw
136
+ def resize_windows
137
+ g = PM::Geometry.new
144
138
 
139
+ @song_lists_win.move_and_resize(g.song_lists_rect)
140
+ @song_list_win.move_and_resize(g.song_list_rect)
141
+ @song_win.move_and_resize(g.song_rect)
142
+ @patch_win.move_and_resize(g.patch_rect)
143
+ @trigger_win.move_and_resize(g.trigger_rect)
144
+ @info_win.move_and_resize(g.info_rect)
145
+
146
+ r = g.message_rect
147
+ @message_win.move(r[2], r[3])
148
+ @message_win.resize(r[0], r[1])
145
149
  end
146
150
 
147
151
  def load(file)
148
152
  @pm.load(file)
149
- @loaded_file = file
150
153
  end
151
154
 
152
155
  def save(file)
153
156
  @pm.save(file)
154
- @loaded_file = file
155
157
  end
156
158
 
157
159
  # Opens the most recently loaded/saved file name in an editor. After
@@ -167,7 +169,6 @@ class Main
167
169
  @pm.debug(cmd)
168
170
  system(cmd)
169
171
  load(file)
170
- @loaded_file = file
171
172
  end
172
173
 
173
174
  # Return the first legit command from $VISUAL, $EDITOR, vim, vi, and
@@ -179,7 +180,11 @@ class Main
179
180
  end
180
181
 
181
182
  def help
182
- message("Help: not yet implemented")
183
+ g = PM::Geometry.new
184
+ win = HelpWindow.new(*g.help_rect)
185
+ win.draw
186
+ win.refresh
187
+ getch # wait for key and eat it
183
188
  end
184
189
 
185
190
  def message(str)
@@ -193,11 +198,17 @@ class Main
193
198
  @pm.debug "#{Time.now} #{str}"
194
199
  end
195
200
 
201
+ # Public method callable by triggers
202
+ def refresh
203
+ refresh_all
204
+ end
205
+
196
206
  def refresh_all
197
207
  set_window_data
198
208
  wins = [@song_lists_win, @song_list_win, @song_win, @patch_win, @info_win, @trigger_win]
199
209
  wins.map(&:draw)
200
- ([stdscr] + wins).map(&:refresh)
210
+ ([stdscr] + wins).map(&:noutrefresh)
211
+ Curses.doupdate
201
212
  end
202
213
 
203
214
  def set_window_data
@@ -209,10 +220,12 @@ class Main
209
220
  song = @pm.song
210
221
  if song
211
222
  @song_win.set_contents(song.name, song.patches, :patch)
223
+ @info_win.text = song.notes
212
224
  patch = @pm.patch
213
225
  @patch_win.patch = patch
214
226
  else
215
227
  @song_win.set_contents(nil, nil, :patch)
228
+ @info_win.text = nil
216
229
  @patch_win.patch = nil
217
230
  end
218
231
  end
@@ -17,10 +17,9 @@ class PatchWindow < PmWindow
17
17
  draw_headers
18
18
  return unless @patch
19
19
 
20
- max_len = @win.maxx - 3 # minus 2 for borders
21
- @patch.connections.each_with_index do |connection, i|
20
+ @patch.connections[0, visible_height].each_with_index do |connection, i|
22
21
  @win.setpos(i+2, 1)
23
- draw_connection(connection, max_len)
22
+ draw_connection(connection)
24
23
  end
25
24
  end
26
25
 
@@ -32,7 +31,7 @@ class PatchWindow < PmWindow
32
31
  }
33
32
  end
34
33
 
35
- def draw_connection(connection, max_len)
34
+ def draw_connection(connection)
36
35
  str = " #{'%16s' % connection.input.name}"
37
36
  str << " #{connection.input_chan ? ('%2d' % (connection.input_chan+1)) : ' '} |"
38
37
  str << " #{'%16s' % connection.output.name}"
@@ -48,14 +47,18 @@ class PatchWindow < PmWindow
48
47
  else
49
48
  ' |'
50
49
  end
51
- str << if connection.xpose && connection.xpose > 0
52
- " #{'%2d' % connection.xpose.to_i} |"
50
+ str << if connection.xpose && connection.xpose != 0
51
+ " #{connection.xpose < 0 ? '' : ' '}#{'%2d' % connection.xpose.to_i} |"
53
52
  else
54
53
  " |"
55
54
  end
56
- str << " #{connection.filter}"
55
+ str << " #{filter_string(connection.filter)}"
57
56
  @win.addstr(make_fit(str))
58
57
  end
59
58
 
59
+ def filter_string(filter)
60
+ filter.to_s.gsub(/\s*#.*/, '').gsub(/\n\s*/, "; ")
61
+ end
62
+
60
63
  end
61
64
  end
@@ -1,7 +1,8 @@
1
1
  require 'curses'
2
+ require 'delegate'
2
3
 
3
4
  module PM
4
- class PmWindow
5
+ class PmWindow < SimpleDelegator
5
6
 
6
7
  include Curses
7
8
 
@@ -11,12 +12,15 @@ class PmWindow
11
12
  # If title is nil then list's name will be used
12
13
  def initialize(rows, cols, row, col, title_prefix)
13
14
  @win = Window.new(rows, cols, row, col)
15
+ super(@win)
14
16
  @title_prefix = title_prefix
15
- @max_contents_len = @win.maxx - 3 # 2 for borders
17
+ set_max_contents_len(cols)
16
18
  end
17
19
 
18
- def refresh
19
- @win.refresh
20
+ def move_and_resize(rect)
21
+ @win.move(rect[2], rect[3])
22
+ @win.resize(rect[0], rect[1])
23
+ set_max_contents_len(rect[1])
20
24
  end
21
25
 
22
26
  def draw
@@ -33,6 +37,15 @@ class PmWindow
33
37
  }
34
38
  end
35
39
 
40
+ # Visible height is height of window minus 2 for the borders.
41
+ def visible_height
42
+ @win.maxy - 2
43
+ end
44
+
45
+ def set_max_contents_len(cols)
46
+ @max_contents_len = cols - 3 # 2 for borders
47
+ end
48
+
36
49
  def make_fit(str)
37
50
  str = str[0..@max_contents_len] if str.length > @max_contents_len
38
51
  str
@@ -16,8 +16,10 @@ class TriggerWindow < PmWindow
16
16
  i = 0
17
17
  pm.inputs.each do |instrument|
18
18
  instrument.triggers.each do |trigger|
19
- @win.setpos(i+1, 1)
20
- @win.addstr(make_fit(":#{instrument.sym} #{trigger.to_s}"))
19
+ if i < visible_height
20
+ @win.setpos(i+1, 1)
21
+ @win.addstr(make_fit(":#{instrument.sym} #{trigger.to_s}"))
22
+ end
21
23
  i += 1
22
24
  end
23
25
  end
@@ -146,7 +146,6 @@ class Cursor
146
146
 
147
147
  str = str.downcase
148
148
  distances = list.collect { |item| dameraulevenshtein(str, item.name.downcase) }
149
- min_distance = distances.min
150
149
  list[distances.index(distances.min)]
151
150
  end
152
151
 
@@ -75,6 +75,10 @@ class DSL
75
75
  yield @song if block_given?
76
76
  end
77
77
 
78
+ def notes(txt)
79
+ @song.notes = txt
80
+ end
81
+
78
82
  def patch(name)
79
83
  @patch = Patch.new(name)
80
84
  @song << @patch
@@ -156,6 +160,14 @@ class DSL
156
160
  end
157
161
  end
158
162
 
163
+ def alias_input(new_sym, old_sym)
164
+ @inputs[new_sym] = @inputs[old_sym]
165
+ end
166
+
167
+ def alias_output(new_sym, old_sym)
168
+ @outputs[new_sym] = @outputs[old_sym]
169
+ end
170
+
159
171
  # ****************************************************************
160
172
 
161
173
  def save(file)
@@ -16,7 +16,7 @@ class Filter
16
16
  end
17
17
 
18
18
  def to_s
19
- (@text || '# no block text found').gsub(/\n\s*/, "; ")
19
+ @text || '# no block text found'
20
20
  end
21
21
 
22
22
  end
@@ -109,7 +109,7 @@ class MockInputPort
109
109
  def gets
110
110
  [{:data => [], :timestamp => 0}]
111
111
  end
112
-
112
+
113
113
  def poll
114
114
  yield gets
115
115
  end
@@ -118,7 +118,7 @@ class MockInputPort
118
118
  end
119
119
 
120
120
  # add this class to the Listener class' known input types
121
- MIDIEye::Listener.input_types << self
121
+ MIDIEye::Listener.input_types << self
122
122
 
123
123
  end
124
124
 
@@ -0,0 +1,61 @@
1
+ require 'patchmaster'
2
+ require 'irb'
3
+ require 'tempfile'
4
+
5
+ $dsl = nil
6
+
7
+ # For bin/patchmaster. Does nothing.
8
+ def run
9
+ end
10
+
11
+ def dsl
12
+ unless $dsl
13
+ $dsl = PM::DSL.new
14
+ $dsl.song("IRB Song")
15
+ $dsl.patch("IRB Patch")
16
+ end
17
+ $dsl
18
+ end
19
+
20
+ # Return the current (only) patch.
21
+ def patch
22
+ dsl.instance_variable_get(:@patch)
23
+ end
24
+
25
+ # Stop and delete all connections.
26
+ def clear
27
+ patch.stop
28
+ patch.connections = []
29
+ patch.start
30
+ end
31
+
32
+ def pm_help
33
+ puts IO.read(File.join(File.dirname(__FILE__), 'irb_help.txt'))
34
+ end
35
+
36
+ # The "panic" command is handled by $dsl. This version tells panic to send
37
+ # all all-notes-off messages.
38
+ def panic!
39
+ PM::PatchMaster.instance.panic(true)
40
+ end
41
+
42
+ def method_missing(sym, *args)
43
+ pm = PM::PatchMaster.instance
44
+ if dsl.respond_to?(sym)
45
+ patch.stop
46
+ dsl.send(sym, *args)
47
+ if sym == :input || sym == :inp
48
+ pm.inputs.last.start
49
+ end
50
+ patch.start
51
+ elsif pm.respond_to?(sym)
52
+ pm.send(sym, *args)
53
+ else
54
+ super
55
+ end
56
+ end
57
+
58
+ def start_patchmaster_irb(init_file=nil)
59
+ ENV['IRBRC'] = init_file if init_file
60
+ IRB.start
61
+ end
@@ -0,0 +1,18 @@
1
+ input num, :sym[, name] define an input instrument
2
+ output num, :sym[, name] define an output instrument
3
+ prog_chg [bank, ] prog send a program change
4
+ conn :in_sym, [chan|nil], :out_sym, chan create a connection
5
+ xpose num set transpose for connection
6
+ zone zone_def set zone for connection
7
+ filter { ... } set connection filter
8
+ clear remove all connections
9
+ panic panic
10
+ panic! panic plus note-offs
11
+
12
+ Alternate names:
13
+ input inp
14
+ output out
15
+ conn c, connection
16
+ prog_chg pc
17
+ xpose x, transpose
18
+ filter f
@@ -25,12 +25,14 @@ class PatchMaster < SimpleDelegator
25
25
  attr_accessor :use_midi
26
26
  alias_method :use_midi?, :use_midi
27
27
  attr_accessor :gui
28
+ attr_reader :loaded_file
28
29
 
29
30
  # A Cursor to which we delegate incoming position methods (#song_list,
30
31
  # #song, #patch, #next_song, #prev_patch, etc.)
31
32
  attr_reader :cursor
32
33
 
33
34
  def initialize
35
+ @running = false
34
36
  @cursor = Cursor.new(self)
35
37
  super(@cursor)
36
38
  @use_midi = true
@@ -146,12 +148,14 @@ class PatchMaster < SimpleDelegator
146
148
  def panic(individual_notes=false)
147
149
  debug("panic(#{individual_notes})")
148
150
  @outputs.each do |out|
151
+ buf = []
149
152
  MIDI_CHANNELS.times do |chan|
150
- out.midi_out([CONTROLLER + chan, CM_ALL_NOTES_OFF, 0])
153
+ buf += [CONTROLLER + chan, CM_ALL_NOTES_OFF, 0]
151
154
  if individual_notes
152
- 128.times { |note| out.midi_out([NOTE_OFF + chan, note, 0]) }
155
+ buf += (0..127).collect { |note| [NOTE_OFF + chan, note, 0] }.flatten
153
156
  end
154
157
  end
158
+ out.midi_out(buf)
155
159
  end
156
160
  end
157
161
 
@@ -3,7 +3,7 @@ module PM
3
3
  # A Song is a named list of Patches with a cursor.
4
4
  class Song
5
5
 
6
- attr_accessor :name, :patches
6
+ attr_accessor :name, :patches, :notes
7
7
 
8
8
  def initialize(name)
9
9
  @name = name
@@ -21,7 +21,9 @@ class Trigger
21
21
  # If +bytes+ matches our +@bytes+ array then run +@block+.
22
22
  def signal(bytes)
23
23
  if bytes == @bytes
24
- block.call
24
+ pm = PM::PatchMaster.instance
25
+ pm.instance_eval &@block
26
+ pm.gui.refresh if pm.gui
25
27
  end
26
28
  end
27
29
 
@@ -17,7 +17,7 @@ def pm
17
17
  @pm ||= PM::SinatraApp.instance.pm
18
18
  end
19
19
 
20
- def return_status(opts = nil)
20
+ def return_status(opts={})
21
21
  pm = pm()
22
22
  status = {
23
23
  :lists => pm.song_lists.map(&:name),
@@ -51,9 +51,7 @@ def return_status(opts = nil)
51
51
  }
52
52
  end
53
53
  end
54
- status.merge(opts) if opts
55
-
56
- json status
54
+ json status.merge(opts)
57
55
  end
58
56
 
59
57
  # ================================================================
@@ -126,5 +124,10 @@ class SinatraApp
126
124
  @pm.stop
127
125
  @pm.close_debug_file
128
126
  end
127
+
128
+ # Public method callable by triggers
129
+ def refresh
130
+ # FIXME
131
+ end
129
132
  end
130
133
  end
metadata CHANGED
@@ -1,42 +1,45 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: patchmaster
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
5
- prerelease:
4
+ version: 1.1.2
6
5
  platform: ruby
7
6
  authors:
8
7
  - Jim Menard
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-02-21 00:00:00.000000000 Z
11
+ date: 2013-12-01 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: midi-eye
16
- requirement: &70108862796700 !ruby/object:Gem::Requirement
17
- none: false
15
+ requirement: !ruby/object:Gem::Requirement
18
16
  requirements:
19
- - - ! '>='
17
+ - - '>='
20
18
  - !ruby/object:Gem::Version
21
19
  version: '0'
22
20
  type: :runtime
23
21
  prerelease: false
24
- version_requirements: *70108862796700
25
- description: ! 'PatchMaster is a MIDI processing and patching system. It allows a
26
- musician to
27
-
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: |
28
+ PatchMaster is a MIDI processing and patching system. It allows a musician to
28
29
  reconfigure a MIDI setup instantaneously and modify the MIDI data in real time.
29
-
30
- '
31
30
  email: jim@jimmenard.com
32
31
  executables:
33
32
  - patchmaster
34
33
  extensions: []
35
34
  extra_rdoc_files: []
36
35
  files:
36
+ - bin/irb_init.rb
37
37
  - bin/patchmaster
38
+ - lib/patchmaster.rb
38
39
  - lib/patchmaster/connection.rb
39
40
  - lib/patchmaster/consts.rb
41
+ - lib/patchmaster/curses/geometry.rb
42
+ - lib/patchmaster/curses/help_window.rb
40
43
  - lib/patchmaster/curses/info_window.rb
41
44
  - lib/patchmaster/curses/info_window_contents.txt
42
45
  - lib/patchmaster/curses/list_window.rb
@@ -49,7 +52,8 @@ files:
49
52
  - lib/patchmaster/dsl.rb
50
53
  - lib/patchmaster/filter.rb
51
54
  - lib/patchmaster/instrument.rb
52
- - lib/patchmaster/irb.rb
55
+ - lib/patchmaster/irb/irb.rb
56
+ - lib/patchmaster/irb/irb_help.txt
53
57
  - lib/patchmaster/patch.rb
54
58
  - lib/patchmaster/patchmaster.rb
55
59
  - lib/patchmaster/predicates.rb
@@ -64,32 +68,30 @@ files:
64
68
  - lib/patchmaster/web/public/js/patchmaster.js
65
69
  - lib/patchmaster/web/public/style.css
66
70
  - lib/patchmaster/web/sinatra_app.rb
67
- - lib/patchmaster.rb
68
71
  - test/test_helper.rb
69
72
  homepage: http://www.patchmaster.org/
70
73
  licenses:
71
74
  - Ruby
75
+ metadata: {}
72
76
  post_install_message:
73
77
  rdoc_options: []
74
78
  require_paths:
75
79
  - lib
76
80
  required_ruby_version: !ruby/object:Gem::Requirement
77
- none: false
78
81
  requirements:
79
- - - ! '>='
82
+ - - '>='
80
83
  - !ruby/object:Gem::Version
81
84
  version: '0'
82
85
  required_rubygems_version: !ruby/object:Gem::Requirement
83
- none: false
84
86
  requirements:
85
- - - ! '>='
87
+ - - '>='
86
88
  - !ruby/object:Gem::Version
87
89
  version: '0'
88
90
  requirements: []
89
91
  rubyforge_project:
90
- rubygems_version: 1.8.12
92
+ rubygems_version: 2.1.10
91
93
  signing_key:
92
- specification_version: 3
94
+ specification_version: 4
93
95
  summary: Realtime MIDI setup configuration and MIDI filtering
94
96
  test_files:
95
97
  - test/test_helper.rb
@@ -1,82 +0,0 @@
1
- require 'patchmaster'
2
- require 'irb'
3
- require 'tempfile'
4
-
5
- $dsl = nil
6
-
7
- # For bin/patchmaster. Does nothing
8
- def run
9
- end
10
-
11
- def dsl
12
- unless $dsl
13
- $dsl = PM::DSL.new
14
- $dsl.song("IRB Song")
15
- $dsl.patch("IRB Patch")
16
- end
17
- $dsl
18
- end
19
-
20
- def patch
21
- dsl.instance_variable_get(:@patch)
22
- end
23
-
24
- def clear
25
- patch.stop
26
- patch.connections = []
27
- patch.start
28
- end
29
-
30
- def pm_help
31
- puts <<EOS
32
- input num, :sym[, name] define an input instrument
33
- output num, :sym[, name] define an output instrument
34
- conn :in_sym, [chan|nil], :out_sym, [chan|nil] create a connection
35
- xpose num set transpose for conn
36
- zone zone_def set zone for conn
37
- clear remove all connections
38
- panic panic
39
- panic! panic plus note-offs
40
- EOS
41
- end
42
-
43
- def panic!
44
- PM::PatchMaster.instance.panic(true)
45
- end
46
-
47
- def method_missing(sym, *args)
48
- pm = PM::PatchMaster.instance
49
- if dsl.respond_to?(sym)
50
- patch.stop
51
- dsl.send(sym, *args)
52
- if sym == :input || sym == :inp
53
- pm.inputs.last.start
54
- end
55
- patch.start
56
- elsif pm.respond_to?(sym)
57
- pm.send(sym, *args)
58
- else
59
- super
60
- end
61
- end
62
-
63
- def start_patchmaster_irb
64
- f = Tempfile.new('patchmaster')
65
- f.write <<EOS
66
- IRB.conf[:PROMPT][:CUSTOM] = {
67
- :PROMPT_I=>"PatchMaster:%03n:%i> ",
68
- :PROMPT_N=>"PatchMaster:%03n:%i> ",
69
- :PROMPT_S=>"PatchMaster:%03n:%i%l ",
70
- :PROMPT_C=>"PatchMaster:%03n:%i* ",
71
- :RETURN=>"=> %s\n"
72
- }
73
- IRB.conf[:PROMPT_MODE] = :CUSTOM
74
-
75
- puts 'PatchMaster loaded'
76
- puts 'Type "pm_help" for help'
77
- EOS
78
- f.close
79
- ENV['IRBRC'] = f.path
80
- IRB.start
81
- f.unlink
82
- end