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.
@@ -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?