patchmaster 0.0.6 → 1.0.0
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 +44 -8
- data/lib/patchmaster.rb +0 -1
- data/lib/patchmaster/connection.rb +1 -0
- data/lib/patchmaster/{app → curses}/info_window.rb +0 -0
- data/lib/patchmaster/{app → curses}/info_window_contents.txt +0 -0
- data/lib/patchmaster/{app → curses}/list_window.rb +1 -1
- data/lib/patchmaster/{app → curses}/main.rb +1 -5
- data/lib/patchmaster/{app → curses}/patch_window.rb +1 -1
- data/lib/patchmaster/{app → curses}/pm_window.rb +0 -0
- data/lib/patchmaster/{app → curses}/prompt_window.rb +0 -0
- data/lib/patchmaster/{app → curses}/trigger_window.rb +0 -0
- data/lib/patchmaster/dsl.rb +25 -14
- data/lib/patchmaster/filter.rb +1 -1
- data/lib/patchmaster/instrument.rb +12 -12
- data/lib/patchmaster/irb.rb +82 -0
- data/lib/patchmaster/patchmaster.rb +22 -18
- data/lib/patchmaster/trigger.rb +1 -1
- data/lib/patchmaster/web/public/index.html +65 -0
- data/lib/patchmaster/web/public/js/jquery-1.4.2.js +6240 -0
- data/lib/patchmaster/web/public/js/jquery.hotkeys.js +99 -0
- data/lib/patchmaster/web/public/js/patchmaster.coffee +81 -0
- data/lib/patchmaster/web/public/js/patchmaster.js +136 -0
- data/lib/patchmaster/web/public/style.css +79 -0
- data/lib/patchmaster/web/sinatra_app.rb +130 -0
- data/test/test_helper.rb +1 -2
- metadata +24 -18
data/bin/patchmaster
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
#
|
3
|
-
# usage: patchmaster [-n] [-d] [pm_file]
|
3
|
+
# usage: patchmaster [-n] [-i] [-w] [-p port] [-t] [-d] [pm_file]
|
4
4
|
#
|
5
5
|
# Starts PatchMaster and optionally loads pm_file.
|
6
6
|
#
|
@@ -8,24 +8,60 @@
|
|
8
8
|
# being able to connect to the MIDI instruments specified in pm_file are
|
9
9
|
# ignored, and no MIDI data is sent/received. That is useful if you want to
|
10
10
|
# run PatchMaster without actually talking to any MIDI instruments.
|
11
|
+
#
|
12
|
+
# To run PatchMaster from within an IRB session use -i. See the
|
13
|
+
# documentation for details on the commands that are available.
|
14
|
+
#
|
15
|
+
# To run PatchMaster using a Web browser GUI use -w and point your browser
|
16
|
+
# at http://localhost:4567. To change the port, use -p.
|
17
|
+
#
|
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
|
+
|
11
26
|
|
12
27
|
require 'optparse'
|
13
28
|
|
14
29
|
use_midi = true
|
15
|
-
|
30
|
+
gui = :curses
|
31
|
+
port = nil
|
16
32
|
OptionParser.new do |opts|
|
17
33
|
opts.banner = "usage: patchmaster [options] [pm_file]"
|
18
34
|
opts.on("-d", "--debug", "Turn on debug mode") { $DEBUG = true }
|
19
35
|
opts.on("-n", "--no-midi", "Turn off MIDI processing") { use_midi = false }
|
20
|
-
opts.on("-
|
36
|
+
opts.on("-i", "--irb", "Use an IRB console") { gui = :irb }
|
37
|
+
opts.on("-w", "--web", "Use a Web browser GUI") { gui = :web }
|
38
|
+
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 }
|
40
|
+
opts.on_tail("-h", "-?", "--help", "Show this message") do
|
41
|
+
puts opts
|
42
|
+
exit 0
|
43
|
+
end
|
21
44
|
end.parse!(ARGV)
|
22
45
|
|
23
46
|
# Must require patchmaster here, after handling options, because Singleton
|
24
47
|
# initialize code checks $DEBUG.
|
25
48
|
require 'patchmaster'
|
26
49
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
50
|
+
pm = PM::PatchMaster.instance
|
51
|
+
pm.use_midi = use_midi
|
52
|
+
pm.load(ARGV[0]) if ARGV[0]
|
53
|
+
case gui
|
54
|
+
when :curses
|
55
|
+
require 'patchmaster/curses/main'
|
56
|
+
pm.gui = PM::Main.instance
|
57
|
+
pm.run
|
58
|
+
when :irb
|
59
|
+
require 'patchmaster/irb'
|
60
|
+
start_patchmaster_irb
|
61
|
+
when :web
|
62
|
+
require 'patchmaster/web/sinatra_app'
|
63
|
+
app = PM::SinatraApp.instance
|
64
|
+
app.port = port if port
|
65
|
+
pm.gui = app
|
66
|
+
pm.run
|
67
|
+
end
|
data/lib/patchmaster.rb
CHANGED
File without changes
|
File without changes
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'curses'
|
2
2
|
require 'singleton'
|
3
|
-
%w(list patch info trigger prompt).each { |w| require "patchmaster/
|
3
|
+
%w(list patch info trigger prompt).each { |w| require "patchmaster/curses/#{w}_window" }
|
4
4
|
|
5
5
|
module PM
|
6
6
|
|
@@ -20,10 +20,6 @@ class Main
|
|
20
20
|
@message_bindings = {}
|
21
21
|
end
|
22
22
|
|
23
|
-
def no_midi!
|
24
|
-
@pm.no_midi!
|
25
|
-
end
|
26
|
-
|
27
23
|
def run
|
28
24
|
@pm.start
|
29
25
|
begin
|
File without changes
|
File without changes
|
File without changes
|
data/lib/patchmaster/dsl.rb
CHANGED
@@ -7,18 +7,23 @@ class DSL
|
|
7
7
|
|
8
8
|
include PM
|
9
9
|
|
10
|
-
def initialize
|
11
|
-
@no_midi = no_midi
|
10
|
+
def initialize
|
12
11
|
@pm = PatchMaster.instance
|
12
|
+
init
|
13
13
|
end
|
14
14
|
|
15
|
-
|
16
|
-
|
15
|
+
# Initialize state used for reading.
|
16
|
+
def init
|
17
17
|
@inputs = {}
|
18
18
|
@outputs = {}
|
19
19
|
@triggers = []
|
20
20
|
@filters = []
|
21
21
|
@songs = {} # key = name, value = song
|
22
|
+
end
|
23
|
+
|
24
|
+
def load(file)
|
25
|
+
contents = IO.read(file)
|
26
|
+
init
|
22
27
|
instance_eval(contents)
|
23
28
|
read_triggers(contents)
|
24
29
|
read_filters(contents)
|
@@ -27,22 +32,22 @@ class DSL
|
|
27
32
|
def input(port_num, sym, name=nil)
|
28
33
|
raise "input: two inputs can not have the same symbol (:#{sym})" if @inputs[sym]
|
29
34
|
|
30
|
-
input = InputInstrument.new(sym, name, port_num, @
|
35
|
+
input = InputInstrument.new(sym, name, port_num, @pm.use_midi?)
|
31
36
|
@inputs[sym] = input
|
32
37
|
@pm.inputs << input
|
33
38
|
rescue => ex
|
34
|
-
raise "input: error creating input instrument \"#{name}\" on input port #{port_num}: #{ex}"
|
39
|
+
raise "input: error creating input instrument \"#{name || sym}\" on input port #{port_num}: #{ex}"
|
35
40
|
end
|
36
|
-
alias_method :
|
41
|
+
alias_method :inp, :input
|
37
42
|
|
38
43
|
def output(port_num, sym, name=nil)
|
39
44
|
raise "output: two outputs can not have the same symbol (:#{sym})" if @outputs[sym]
|
40
45
|
|
41
|
-
output = OutputInstrument.new(sym, name, port_num, @
|
46
|
+
output = OutputInstrument.new(sym, name, port_num, @pm.use_midi?)
|
42
47
|
@outputs[sym] = output
|
43
48
|
@pm.outputs << output
|
44
49
|
rescue => ex
|
45
|
-
raise "output: error creating output instrument \"#{name}\" on output port #{port_num}: #{ex}"
|
50
|
+
raise "output: error creating output instrument \"#{name || sym}\" on output port #{port_num}: #{ex}"
|
46
51
|
end
|
47
52
|
alias_method :out, :output
|
48
53
|
|
@@ -51,8 +56,8 @@ class DSL
|
|
51
56
|
end
|
52
57
|
|
53
58
|
def message_key(name, key_or_sym)
|
54
|
-
if
|
55
|
-
|
59
|
+
if @pm.gui
|
60
|
+
@pm.gui.bind_message(name, key_or_sym)
|
56
61
|
end
|
57
62
|
end
|
58
63
|
|
@@ -84,9 +89,15 @@ class DSL
|
|
84
89
|
@patch.stop_bytes = bytes
|
85
90
|
end
|
86
91
|
|
87
|
-
|
92
|
+
# in_chan can be skipped, so "connection :foo, :bar, 1" is the same as
|
93
|
+
# "connection :foo, nil, :bar, 1".
|
94
|
+
def connection(in_sym, in_chan, out_sym, out_chan=nil)
|
88
95
|
input = @inputs[in_sym]
|
89
|
-
|
96
|
+
if in_chan.kind_of? Symbol
|
97
|
+
out_chan = out_sym
|
98
|
+
out_sym = in_chan
|
99
|
+
in_chan = nil
|
100
|
+
end
|
90
101
|
raise "can't find input instrument #{in_sym}" unless input
|
91
102
|
output = @outputs[out_sym]
|
92
103
|
raise "can't find outputput instrument #{out_sym}" unless output
|
@@ -266,7 +277,7 @@ class DSL
|
|
266
277
|
end
|
267
278
|
end
|
268
279
|
end
|
269
|
-
containers.each { |thing| thing.text.strip! }
|
280
|
+
containers.each { |thing| thing.text.strip! if thing.text }
|
270
281
|
end
|
271
282
|
|
272
283
|
end
|
data/lib/patchmaster/filter.rb
CHANGED
@@ -22,8 +22,8 @@ class InputInstrument < Instrument
|
|
22
22
|
attr_reader :listener
|
23
23
|
|
24
24
|
# If +port+ is nil (the normal case), creates either a real or a mock port
|
25
|
-
def initialize(sym, name, port_num,
|
26
|
-
super(sym, name, port_num, input_port(port_num,
|
25
|
+
def initialize(sym, name, port_num, use_midi=true)
|
26
|
+
super(sym, name, port_num, input_port(port_num, use_midi))
|
27
27
|
@connections = []
|
28
28
|
@triggers = []
|
29
29
|
end
|
@@ -61,11 +61,11 @@ class InputInstrument < Instrument
|
|
61
61
|
|
62
62
|
private
|
63
63
|
|
64
|
-
def input_port(port_num,
|
65
|
-
if
|
66
|
-
MockInputPort.new(port_num)
|
67
|
-
else
|
64
|
+
def input_port(port_num, use_midi=true)
|
65
|
+
if use_midi
|
68
66
|
UniMIDI::Input.all[port_num].open
|
67
|
+
else
|
68
|
+
MockInputPort.new(port_num)
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
@@ -73,8 +73,8 @@ end
|
|
73
73
|
|
74
74
|
class OutputInstrument < Instrument
|
75
75
|
|
76
|
-
def initialize(sym, name, port_num,
|
77
|
-
super(sym, name, port_num, output_port(port_num,
|
76
|
+
def initialize(sym, name, port_num, use_midi=true)
|
77
|
+
super(sym, name, port_num, output_port(port_num, use_midi))
|
78
78
|
end
|
79
79
|
|
80
80
|
def midi_out(bytes)
|
@@ -83,11 +83,11 @@ class OutputInstrument < Instrument
|
|
83
83
|
|
84
84
|
private
|
85
85
|
|
86
|
-
def output_port(port_num,
|
87
|
-
if
|
88
|
-
MockOutputPort.new(port_num)
|
89
|
-
else
|
86
|
+
def output_port(port_num, use_midi)
|
87
|
+
if use_midi
|
90
88
|
UniMIDI::Output.all[port_num].open
|
89
|
+
else
|
90
|
+
MockOutputPort.new(port_num)
|
91
91
|
end
|
92
92
|
end
|
93
93
|
end
|
@@ -0,0 +1,82 @@
|
|
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
|
@@ -22,7 +22,9 @@ class PatchMaster < SimpleDelegator
|
|
22
22
|
|
23
23
|
attr_reader :inputs, :outputs, :all_songs, :song_lists
|
24
24
|
attr_reader :messages
|
25
|
-
|
25
|
+
attr_accessor :use_midi
|
26
|
+
alias_method :use_midi?, :use_midi
|
27
|
+
attr_accessor :gui
|
26
28
|
|
27
29
|
# A Cursor to which we delegate incoming position methods (#song_list,
|
28
30
|
# #song, #patch, #next_song, #prev_patch, etc.)
|
@@ -31,8 +33,8 @@ class PatchMaster < SimpleDelegator
|
|
31
33
|
def initialize
|
32
34
|
@cursor = Cursor.new(self)
|
33
35
|
super(@cursor)
|
34
|
-
@
|
35
|
-
@
|
36
|
+
@use_midi = true
|
37
|
+
@gui = nil
|
36
38
|
|
37
39
|
if $DEBUG
|
38
40
|
@debug_file = File.open(DEBUG_FILE, 'a')
|
@@ -41,10 +43,6 @@ class PatchMaster < SimpleDelegator
|
|
41
43
|
init_data
|
42
44
|
end
|
43
45
|
|
44
|
-
def no_midi!
|
45
|
-
@no_midi = true
|
46
|
-
end
|
47
|
-
|
48
46
|
def no_gui!
|
49
47
|
@no_gui = true
|
50
48
|
end
|
@@ -57,7 +55,8 @@ class PatchMaster < SimpleDelegator
|
|
57
55
|
|
58
56
|
@cursor.mark
|
59
57
|
init_data
|
60
|
-
DSL.new
|
58
|
+
DSL.new.load(file)
|
59
|
+
@loaded_file = file
|
61
60
|
@cursor.restore
|
62
61
|
|
63
62
|
if restart
|
@@ -70,7 +69,8 @@ class PatchMaster < SimpleDelegator
|
|
70
69
|
end
|
71
70
|
|
72
71
|
def save(file)
|
73
|
-
DSL.new
|
72
|
+
DSL.new.save(file)
|
73
|
+
@loaded_file = file
|
74
74
|
rescue => ex
|
75
75
|
raise("error saving #{file}: #{ex}" + caller.join("\n"))
|
76
76
|
end
|
@@ -102,16 +102,20 @@ class PatchMaster < SimpleDelegator
|
|
102
102
|
@running = false
|
103
103
|
end
|
104
104
|
|
105
|
-
# Run PatchMaster without the GUI. Don't use this when using PM::Main.
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
#
|
110
|
-
#
|
105
|
+
# Run PatchMaster without the GUI. Don't use this when using PM::Main. If
|
106
|
+
# there is a GUI then forward this request to it. Otherwise, call #start,
|
107
|
+
# wait for inputs' MIDIEye listener threads to finish, then call #stop.
|
108
|
+
# Note that normally nothing stops those threads, so this is used as a way
|
109
|
+
# to make sure the script doesn't quit until killed by something like
|
110
|
+
# SIGINT.
|
111
111
|
def run
|
112
|
-
|
113
|
-
|
114
|
-
|
112
|
+
if @gui
|
113
|
+
@gui.run
|
114
|
+
else
|
115
|
+
start(true)
|
116
|
+
@inputs.each { |input| input.listener.join }
|
117
|
+
stop
|
118
|
+
end
|
115
119
|
end
|
116
120
|
|
117
121
|
def running?
|