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.
- checksums.yaml +7 -0
- data/bin/irb_init.rb +17 -0
- data/bin/patchmaster +15 -13
- data/lib/patchmaster/connection.rb +35 -5
- data/lib/patchmaster/consts.rb +2 -2
- data/lib/patchmaster/curses/geometry.rb +53 -0
- data/lib/patchmaster/curses/help_window.rb +29 -0
- data/lib/patchmaster/curses/info_window.rb +22 -15
- data/lib/patchmaster/curses/info_window_contents.txt +11 -10
- data/lib/patchmaster/curses/list_window.rb +15 -2
- data/lib/patchmaster/curses/main.rb +63 -50
- data/lib/patchmaster/curses/patch_window.rb +10 -7
- data/lib/patchmaster/curses/pm_window.rb +17 -4
- data/lib/patchmaster/curses/trigger_window.rb +4 -2
- data/lib/patchmaster/cursor.rb +0 -1
- data/lib/patchmaster/dsl.rb +12 -0
- data/lib/patchmaster/filter.rb +1 -1
- data/lib/patchmaster/instrument.rb +2 -2
- data/lib/patchmaster/irb/irb.rb +61 -0
- data/lib/patchmaster/irb/irb_help.txt +18 -0
- data/lib/patchmaster/patchmaster.rb +6 -2
- data/lib/patchmaster/song.rb +1 -1
- data/lib/patchmaster/trigger.rb +3 -1
- data/lib/patchmaster/web/sinatra_app.rb +7 -4
- metadata +22 -20
- data/lib/patchmaster/irb.rb +0 -82
checksums.yaml
ADDED
@@ -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
|
data/bin/irb_init.rb
ADDED
@@ -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'
|
data/bin/patchmaster
CHANGED
@@ -1,28 +1,25 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
#
|
3
|
-
# usage: patchmaster [-n] [-i] [-w] [-p port] [-
|
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.
|
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
|
-
#
|
19
|
-
#
|
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("-
|
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
|
-
|
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
|
-
|
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]
|
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
|
data/lib/patchmaster/consts.rb
CHANGED
@@ -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 :
|
11
|
-
|
12
|
-
TITLE = ' PatchMaster '
|
10
|
+
attr_reader :text
|
13
11
|
|
14
12
|
def initialize(rows, cols, row, col)
|
15
|
-
|
16
|
-
@
|
13
|
+
super(rows, cols, row, col, nil)
|
14
|
+
@info_text = IO.read(CONTENTS)
|
15
|
+
@text = nil
|
17
16
|
end
|
18
17
|
|
19
|
-
def
|
20
|
-
|
21
|
-
|
22
|
-
@
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
29
|
-
|
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,
|
4
|
-
p,
|
3
|
+
n, right - next song
|
4
|
+
p, left - prev song
|
5
5
|
|
6
|
-
g
|
7
|
-
t
|
6
|
+
g - goto song
|
7
|
+
t - goto song list
|
8
8
|
|
9
|
-
h
|
10
|
-
ESC
|
9
|
+
h, ? - help
|
10
|
+
ESC - panic
|
11
11
|
|
12
|
-
l
|
13
|
-
s
|
14
|
-
|
12
|
+
l - load
|
13
|
+
s - save
|
14
|
+
r - reload
|
15
|
+
e - edit
|
15
16
|
|
16
|
-
q
|
17
|
+
q - quit
|
@@ -5,7 +5,12 @@ class ListWindow < PmWindow
|
|
5
5
|
|
6
6
|
attr_reader :list
|
7
7
|
|
8
|
-
|
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.
|
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
|
-
|
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::
|
40
|
+
when 'n', Key::RIGHT
|
40
41
|
@pm.next_song
|
41
|
-
when 'p', Key::
|
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 '
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
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
|
-
|
136
|
-
|
137
|
-
|
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
|
-
@
|
140
|
-
|
133
|
+
@message_win.scrollok(false)
|
134
|
+
end
|
141
135
|
|
142
|
-
|
143
|
-
|
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
|
-
|
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(&:
|
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
|
-
|
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
|
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
|
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
|
52
|
-
"
|
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
|
-
|
17
|
+
set_max_contents_len(cols)
|
16
18
|
end
|
17
19
|
|
18
|
-
def
|
19
|
-
@win.
|
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
|
-
|
20
|
-
|
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
|
data/lib/patchmaster/cursor.rb
CHANGED
data/lib/patchmaster/dsl.rb
CHANGED
@@ -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)
|
data/lib/patchmaster/filter.rb
CHANGED
@@ -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
|
-
|
153
|
+
buf += [CONTROLLER + chan, CM_ALL_NOTES_OFF, 0]
|
151
154
|
if individual_notes
|
152
|
-
|
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
|
|
data/lib/patchmaster/song.rb
CHANGED
data/lib/patchmaster/trigger.rb
CHANGED
@@ -17,7 +17,7 @@ def pm
|
|
17
17
|
@pm ||= PM::SinatraApp.instance.pm
|
18
18
|
end
|
19
19
|
|
20
|
-
def return_status(opts
|
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)
|
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.
|
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-
|
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:
|
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:
|
25
|
-
|
26
|
-
|
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.
|
92
|
+
rubygems_version: 2.1.10
|
91
93
|
signing_key:
|
92
|
-
specification_version:
|
94
|
+
specification_version: 4
|
93
95
|
summary: Realtime MIDI setup configuration and MIDI filtering
|
94
96
|
test_files:
|
95
97
|
- test/test_helper.rb
|
data/lib/patchmaster/irb.rb
DELETED
@@ -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
|