patchmaster 0.0.6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- use_gui = true
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("-t", "--text", "--nw", "--no-window", "No windows") { use_gui = false }
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
- app = use_gui ? PM::Main.instance : PM::PatchMaster.instance
28
- app.no_gui! if !use_gui
29
- app.no_midi! if !use_midi
30
- app.load(ARGV[0]) if ARGV[0]
31
- app.run
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
@@ -9,4 +9,3 @@ require 'patchmaster/instrument'
9
9
  require 'patchmaster/patchmaster'
10
10
  require 'patchmaster/trigger'
11
11
  require 'patchmaster/dsl'
12
- require 'patchmaster/app/main'
@@ -89,6 +89,7 @@ class Connection
89
89
  str << "; pc #@pc_prog" if pc?
90
90
  str << "; xpose #@xpose" if @xpose
91
91
  str << "; zone #{note_num_to_name(@zone.begin)}..#{note_num_to_name(@zone.end)}" if @zone
92
+ str
92
93
  end
93
94
  end
94
95
 
@@ -1,4 +1,4 @@
1
- require 'patchmaster/app/pm_window'
1
+ require 'patchmaster/curses/pm_window'
2
2
 
3
3
  module PM
4
4
  class ListWindow < PmWindow
@@ -1,6 +1,6 @@
1
1
  require 'curses'
2
2
  require 'singleton'
3
- %w(list patch info trigger prompt).each { |w| require "patchmaster/app/#{w}_window" }
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
@@ -1,4 +1,4 @@
1
- require 'patchmaster/app/pm_window'
1
+ require 'patchmaster/curses/pm_window'
2
2
 
3
3
  module PM
4
4
  class PatchWindow < PmWindow
@@ -7,18 +7,23 @@ class DSL
7
7
 
8
8
  include PM
9
9
 
10
- def initialize(no_midi=false)
11
- @no_midi = no_midi
10
+ def initialize
12
11
  @pm = PatchMaster.instance
12
+ init
13
13
  end
14
14
 
15
- def load(file)
16
- contents = IO.read(file)
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, @no_midi)
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 :in, :input
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, @no_midi)
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 !@pm.no_gui # TODO get rid of double negative
55
- PM::Main.instance.bind_message(name, key_or_sym)
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
- def connection(in_sym, in_chan, out_sym, out_chan)
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
- in_chan = nil if in_chan == :all || in_chan == :any
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
@@ -16,7 +16,7 @@ class Filter
16
16
  end
17
17
 
18
18
  def to_s
19
- @text.gsub(/\n\s*/, "; ")
19
+ (@text || '# no block text found').gsub(/\n\s*/, "; ")
20
20
  end
21
21
 
22
22
  end
@@ -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, no_midi=false)
26
- super(sym, name, port_num, input_port(port_num, no_midi))
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, no_midi=false)
65
- if no_midi
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, no_midi=false)
77
- super(sym, name, port_num, output_port(port_num, no_midi))
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, no_midi)
87
- if no_midi
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
- attr_reader :no_midi, :no_gui
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
- @no_midi = false
35
- @no_gui = false
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(@no_midi).load(file)
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(@no_midi).save(file)
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
- # 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.
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
- start(true)
113
- @inputs.each { |input| input.listener.join }
114
- stop
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?