patchmaster 0.0.3 → 0.0.4
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 +5 -2
- data/lib/patchmaster/app/info_window_contents.txt +1 -1
- data/lib/patchmaster/app/main.rb +53 -8
- data/lib/patchmaster/app/trigger_window.rb +2 -2
- data/lib/patchmaster/dsl.rb +32 -30
- data/lib/patchmaster/instrument.rb +26 -11
- data/lib/patchmaster/patchmaster.rb +45 -32
- data/lib/patchmaster/predicates.rb +5 -0
- data/test/test_helper.rb +7 -17
- metadata +5 -5
    
        data/bin/patchmaster
    CHANGED
    
    | @@ -12,17 +12,20 @@ | |
| 12 12 | 
             
            require 'optparse'
         | 
| 13 13 |  | 
| 14 14 | 
             
            use_midi = true
         | 
| 15 | 
            +
            use_gui = true
         | 
| 15 16 | 
             
            OptionParser.new do |opts|
         | 
| 16 17 | 
             
              opts.banner = "usage: patchmaster [options] [pm_file]"
         | 
| 17 18 | 
             
              opts.on("-d", "--debug", "Turn on debug mode") { $DEBUG = true }
         | 
| 18 19 | 
             
              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 }
         | 
| 19 21 | 
             
            end.parse!(ARGV)
         | 
| 20 22 |  | 
| 21 23 | 
             
            # Must require patchmaster here, after handling options, because Singleton
         | 
| 22 | 
            -
            #  | 
| 24 | 
            +
            # initialize code checks $DEBUG.
         | 
| 23 25 | 
             
            require 'patchmaster'
         | 
| 24 26 |  | 
| 25 | 
            -
            app = PM::Main.instance
         | 
| 27 | 
            +
            app = use_gui ? PM::Main.instance : PM::PatchMaster.instance
         | 
| 28 | 
            +
            app.no_gui! if !use_gui
         | 
| 26 29 | 
             
            app.no_midi! if !use_midi
         | 
| 27 30 | 
             
            app.load(ARGV[0]) if ARGV[0]
         | 
| 28 31 | 
             
            app.run
         | 
    
        data/lib/patchmaster/app/main.rb
    CHANGED
    
    | @@ -9,18 +9,21 @@ class Main | |
| 9 9 | 
             
              include Singleton
         | 
| 10 10 | 
             
              include Curses
         | 
| 11 11 |  | 
| 12 | 
            +
              FUNCTION_KEY_SYMBOLS = {}
         | 
| 13 | 
            +
              12.times do |i|
         | 
| 14 | 
            +
                FUNCTION_KEY_SYMBOLS["f#{i+1}".to_sym] = Key::F1 + i
         | 
| 15 | 
            +
                FUNCTION_KEY_SYMBOLS["F#{i+1}".to_sym] = Key::F1 + i
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
             | 
| 12 18 | 
             
              def initialize
         | 
| 13 19 | 
             
                @pm = PatchMaster.instance
         | 
| 20 | 
            +
                @message_bindings = {}
         | 
| 14 21 | 
             
              end
         | 
| 15 22 |  | 
| 16 23 | 
             
              def no_midi!
         | 
| 17 24 | 
             
                @pm.no_midi!
         | 
| 18 25 | 
             
              end
         | 
| 19 26 |  | 
| 20 | 
            -
              def load(file)
         | 
| 21 | 
            -
                @pm.load(file)
         | 
| 22 | 
            -
              end
         | 
| 23 | 
            -
             | 
| 24 27 | 
             
              def run
         | 
| 25 28 | 
             
                @pm.start
         | 
| 26 29 | 
             
                begin
         | 
| @@ -49,8 +52,9 @@ class Main | |
| 49 52 | 
             
                        @pm.goto_song_list(name)
         | 
| 50 53 | 
             
                      when 'e'
         | 
| 51 54 | 
             
                        close_screen
         | 
| 52 | 
            -
                        @ | 
| 53 | 
            -
             | 
| 55 | 
            +
                        file = @loaded_file || PromptWindow.new('Edit', 'Edit file:').gets
         | 
| 56 | 
            +
                        edit(file)
         | 
| 57 | 
            +
                      when 'h'
         | 
| 54 58 | 
             
                        help
         | 
| 55 59 | 
             
                      when 27        # "\e" doesn't work here
         | 
| 56 60 | 
             
                        # Twice in a row sends individual note-off commands
         | 
| @@ -60,7 +64,7 @@ class Main | |
| 60 64 | 
             
                      when 'l'
         | 
| 61 65 | 
             
                        file = PromptWindow.new('Load', 'Load file:').gets
         | 
| 62 66 | 
             
                        begin
         | 
| 63 | 
            -
                           | 
| 67 | 
            +
                          load(file)
         | 
| 64 68 | 
             
                          message("Loaded #{file}")
         | 
| 65 69 | 
             
                        rescue => ex
         | 
| 66 70 | 
             
                          message(ex.to_s)
         | 
| @@ -68,7 +72,7 @@ class Main | |
| 68 72 | 
             
                      when 's'
         | 
| 69 73 | 
             
                        file = PromptWindow.new('Save', 'Save into file:').gets
         | 
| 70 74 | 
             
                        begin
         | 
| 71 | 
            -
                           | 
| 75 | 
            +
                          save(file)
         | 
| 72 76 | 
             
                          message("Saved #{file}")
         | 
| 73 77 | 
             
                        rescue => ex
         | 
| 74 78 | 
             
                          message(ex.to_s)
         | 
| @@ -88,6 +92,9 @@ class Main | |
| 88 92 | 
             
                      message(ex.to_s)
         | 
| 89 93 | 
             
                      @pm.debug caller.join("\n")
         | 
| 90 94 | 
             
                    end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                    msg_name = @message_bindings[ch]
         | 
| 97 | 
            +
                    @pm.send_message(msg_name) if msg_name
         | 
| 91 98 | 
             
                  end
         | 
| 92 99 | 
             
                ensure
         | 
| 93 100 | 
             
                  clear
         | 
| @@ -98,6 +105,10 @@ class Main | |
| 98 105 | 
             
                end
         | 
| 99 106 | 
             
              end
         | 
| 100 107 |  | 
| 108 | 
            +
              def bind_message(name, key_sym)
         | 
| 109 | 
            +
                @message_bindings[FUNCTION_KEY_SYMBOLS[key_sym]] = name
         | 
| 110 | 
            +
              end
         | 
| 111 | 
            +
             | 
| 101 112 | 
             
              def config_curses
         | 
| 102 113 | 
             
                init_screen
         | 
| 103 114 | 
             
                cbreak                      # unbuffered input
         | 
| @@ -133,6 +144,40 @@ class Main | |
| 133 144 |  | 
| 134 145 | 
             
              end
         | 
| 135 146 |  | 
| 147 | 
            +
              def load(file)
         | 
| 148 | 
            +
                @pm.load(file)
         | 
| 149 | 
            +
                @loaded_file = file
         | 
| 150 | 
            +
              end
         | 
| 151 | 
            +
             | 
| 152 | 
            +
              def save(file)
         | 
| 153 | 
            +
                @pm.save(file)
         | 
| 154 | 
            +
                @loaded_file = file
         | 
| 155 | 
            +
              end
         | 
| 156 | 
            +
             | 
| 157 | 
            +
              # Opens the most recently loaded/saved file name in an editor. After
         | 
| 158 | 
            +
              # editing, the file is re-loaded.
         | 
| 159 | 
            +
              def edit(file)
         | 
| 160 | 
            +
                editor_command = find_editor
         | 
| 161 | 
            +
                unless editor_command
         | 
| 162 | 
            +
                  message("Can not find $VISUAL, $EDITOR, vim, or vi on your path")
         | 
| 163 | 
            +
                  return
         | 
| 164 | 
            +
                end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                cmd = "#{editor_command} #{file}"
         | 
| 167 | 
            +
                @pm.debug(cmd)
         | 
| 168 | 
            +
                system(cmd)
         | 
| 169 | 
            +
                load(file)
         | 
| 170 | 
            +
                @loaded_file = file
         | 
| 171 | 
            +
              end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
              # Return the first legit command from $VISUAL, $EDITOR, vim, vi, and
         | 
| 174 | 
            +
              # notepad.exe.
         | 
| 175 | 
            +
              def find_editor
         | 
| 176 | 
            +
                @editor ||= [ENV['VISUAL'], ENV['EDITOR'], 'vim', 'vi', 'notepad.exe'].compact.detect do |cmd|
         | 
| 177 | 
            +
                  system('which', cmd) || File.exist?(cmd)
         | 
| 178 | 
            +
                end
         | 
| 179 | 
            +
              end
         | 
| 180 | 
            +
             | 
| 136 181 | 
             
              def help
         | 
| 137 182 | 
             
                message("Help: not yet implemented")
         | 
| 138 183 | 
             
              end
         | 
| @@ -14,10 +14,10 @@ class TriggerWindow < PmWindow | |
| 14 14 | 
             
                super
         | 
| 15 15 | 
             
                pm = PM::PatchMaster.instance
         | 
| 16 16 | 
             
                i = 0
         | 
| 17 | 
            -
                pm.inputs.each do | | 
| 17 | 
            +
                pm.inputs.each do |instrument|
         | 
| 18 18 | 
             
                  instrument.triggers.each do |trigger|
         | 
| 19 19 | 
             
                    @win.setpos(i+1, 1)
         | 
| 20 | 
            -
                    @win.addstr(make_fit(":#{sym} #{trigger.to_s}"))
         | 
| 20 | 
            +
                    @win.addstr(make_fit(":#{instrument.sym} #{trigger.to_s}"))
         | 
| 21 21 | 
             
                    i += 1
         | 
| 22 22 | 
             
                  end
         | 
| 23 23 | 
             
                end
         | 
    
        data/lib/patchmaster/dsl.rb
    CHANGED
    
    | @@ -14,6 +14,8 @@ class DSL | |
| 14 14 |  | 
| 15 15 | 
             
              def load(file)
         | 
| 16 16 | 
             
                contents = IO.read(file)
         | 
| 17 | 
            +
                @inputs = {}
         | 
| 18 | 
            +
                @outputs = {}
         | 
| 17 19 | 
             
                @triggers = []
         | 
| 18 20 | 
             
                @filters = []
         | 
| 19 21 | 
             
                @songs = {}                 # key = name, value = song
         | 
| @@ -23,21 +25,39 @@ class DSL | |
| 23 25 | 
             
              end
         | 
| 24 26 |  | 
| 25 27 | 
             
              def input(port_num, sym, name=nil)
         | 
| 26 | 
            -
                 | 
| 28 | 
            +
                raise "input: two inputs can not have the same symbol (:#{sym})" if @inputs[sym]
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                input = InputInstrument.new(sym, name, port_num, @no_midi)
         | 
| 31 | 
            +
                @inputs[sym] = input
         | 
| 32 | 
            +
                @pm.inputs << input
         | 
| 27 33 | 
             
              rescue => ex
         | 
| 28 34 | 
             
                raise "input: error creating input instrument \"#{name}\" on input port #{port_num}: #{ex}"
         | 
| 29 35 | 
             
              end
         | 
| 30 36 | 
             
              alias_method :in, :input
         | 
| 31 37 |  | 
| 32 38 | 
             
              def output(port_num, sym, name=nil)
         | 
| 33 | 
            -
                 | 
| 39 | 
            +
                raise "output: two outputs can not have the same symbol (:#{sym})" if @outputs[sym]
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                output = OutputInstrument.new(sym, name, port_num, @no_midi)
         | 
| 42 | 
            +
                @outputs[sym] = output
         | 
| 43 | 
            +
                @pm.outputs << output
         | 
| 34 44 | 
             
              rescue => ex
         | 
| 35 45 | 
             
                raise "output: error creating output instrument \"#{name}\" on output port #{port_num}: #{ex}"
         | 
| 36 46 | 
             
              end
         | 
| 37 47 | 
             
              alias_method :out, :output
         | 
| 38 48 |  | 
| 49 | 
            +
              def message(name, bytes)
         | 
| 50 | 
            +
                @pm.messages[name] = bytes
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
              def message_key(name, key_sym)
         | 
| 54 | 
            +
                if !@pm.no_gui              # TODO get rid of double negative
         | 
| 55 | 
            +
                  PM::Main.instance.bind_message(name, key_sym)
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
             | 
| 39 59 | 
             
              def trigger(instrument_sym, bytes, &block)
         | 
| 40 | 
            -
                instrument = @ | 
| 60 | 
            +
                instrument = @inputs[instrument_sym]
         | 
| 41 61 | 
             
                raise "trigger: error finding instrument #{instrument_sym}" unless instrument
         | 
| 42 62 | 
             
                t = Trigger.new(bytes, block)
         | 
| 43 63 | 
             
                instrument.triggers << t
         | 
| @@ -65,10 +85,10 @@ class DSL | |
| 65 85 | 
             
              end
         | 
| 66 86 |  | 
| 67 87 | 
             
              def connection(in_sym, in_chan, out_sym, out_chan)
         | 
| 68 | 
            -
                input = @ | 
| 88 | 
            +
                input = @inputs[in_sym]
         | 
| 69 89 | 
             
                in_chan = nil if in_chan == :all || in_chan == :any
         | 
| 70 90 | 
             
                raise "can't find input instrument #{in_sym}" unless input
         | 
| 71 | 
            -
                output = @ | 
| 91 | 
            +
                output = @outputs[out_sym]
         | 
| 72 92 | 
             
                raise "can't find outputput instrument #{out_sym}" unless output
         | 
| 73 93 |  | 
| 74 94 | 
             
                @conn = Connection.new(input, in_chan, output, out_chan)
         | 
| @@ -130,19 +150,19 @@ class DSL | |
| 130 150 | 
             
              end
         | 
| 131 151 |  | 
| 132 152 | 
             
              def save_instruments(f)
         | 
| 133 | 
            -
                @pm.inputs.each do | | 
| 134 | 
            -
                  f.puts "input #{instr.port_num}, :#{sym}, #{quoted(instr.name)}"
         | 
| 153 | 
            +
                @pm.inputs.each do |instr|
         | 
| 154 | 
            +
                  f.puts "input #{instr.port_num}, :#{instr.sym}, #{quoted(instr.name)}"
         | 
| 135 155 | 
             
                end
         | 
| 136 | 
            -
                @pm.outputs.each do | | 
| 137 | 
            -
                  f.puts "output #{instr.port_num}, :#{sym}, #{quoted(instr.name)}"
         | 
| 156 | 
            +
                @pm.outputs.each do |instr|
         | 
| 157 | 
            +
                  f.puts "output #{instr.port_num}, :#{instr.sym}, #{quoted(instr.name)}"
         | 
| 138 158 | 
             
                end
         | 
| 139 159 | 
             
                f.puts
         | 
| 140 160 | 
             
              end
         | 
| 141 161 |  | 
| 142 162 | 
             
              def save_triggers(f)
         | 
| 143 | 
            -
                @pm.inputs.each do | | 
| 163 | 
            +
                @pm.inputs.each do |instrument|
         | 
| 144 164 | 
             
                  instrument.triggers.each do |trigger|
         | 
| 145 | 
            -
                    str = "trigger :#{sym}, #{trigger.bytes.inspect} #{trigger.text}"
         | 
| 165 | 
            +
                    str = "trigger :#{instrument.sym}, #{trigger.bytes.inspect} #{trigger.text}"
         | 
| 146 166 | 
             
                    f.puts str
         | 
| 147 167 | 
             
                  end
         | 
| 148 168 | 
             
                end
         | 
| @@ -166,11 +186,9 @@ class DSL | |
| 166 186 | 
             
              end
         | 
| 167 187 |  | 
| 168 188 | 
             
              def save_connection(f, conn)
         | 
| 169 | 
            -
                in_sym = @pm.inputs.key(conn.input)
         | 
| 170 189 | 
             
                in_chan = conn.input_chan ? conn.input_chan + 1 : 'nil'
         | 
| 171 | 
            -
                out_sym = @pm.outputs.key(conn.output)
         | 
| 172 190 | 
             
                out_chan = conn.output_chan + 1
         | 
| 173 | 
            -
                f.puts "    conn :#{ | 
| 191 | 
            +
                f.puts "    conn :#{conn.input.sym}, #{in_chan}, :#{conn.output.sym}, #{out_chan} do"
         | 
| 174 192 | 
             
                f.puts "      prog_chg #{conn.pc_prog}" if conn.pc?
         | 
| 175 193 | 
             
                f.puts "      zone #{conn.note_num_to_name(conn.zone.begin)}, #{conn.note_num_to_name(conn.zone.end)}" if conn.zone
         | 
| 176 194 | 
             
                f.puts "      xpose #{conn.xpose}" if conn.xpose
         | 
| @@ -197,22 +215,6 @@ class DSL | |
| 197 215 |  | 
| 198 216 | 
             
              private
         | 
| 199 217 |  | 
| 200 | 
            -
              def input_port(port)
         | 
| 201 | 
            -
                if @no_midi
         | 
| 202 | 
            -
                  MockInputPort.new
         | 
| 203 | 
            -
                else
         | 
| 204 | 
            -
                  UniMIDI::Input.all[port].open
         | 
| 205 | 
            -
                end
         | 
| 206 | 
            -
              end
         | 
| 207 | 
            -
             | 
| 208 | 
            -
              def output_port(port)
         | 
| 209 | 
            -
                if @no_midi
         | 
| 210 | 
            -
                  MockOutputPort.new
         | 
| 211 | 
            -
                else
         | 
| 212 | 
            -
                  UniMIDI::Output.all[port].open
         | 
| 213 | 
            -
                end
         | 
| 214 | 
            -
              end
         | 
| 215 | 
            -
             | 
| 216 218 | 
             
              def read_triggers(contents)
         | 
| 217 219 | 
             
                read_block_text('trigger', @triggers, contents)
         | 
| 218 220 | 
             
              end
         | 
| @@ -5,10 +5,11 @@ module PM | |
| 5 5 |  | 
| 6 6 | 
             
            class Instrument
         | 
| 7 7 |  | 
| 8 | 
            -
              attr_reader :name, :port_num, :port
         | 
| 8 | 
            +
              attr_reader :sym, :name, :port_num, :port
         | 
| 9 9 |  | 
| 10 | 
            -
              def initialize(name, port_num, port)
         | 
| 11 | 
            -
                @name, @port_num, @port = name, port_num, port
         | 
| 10 | 
            +
              def initialize(sym, name, port_num, port)
         | 
| 11 | 
            +
                @sym, @name, @port_num, @port = sym, name, port_num, port
         | 
| 12 | 
            +
                @name ||= @port.name if @port
         | 
| 12 13 | 
             
              end
         | 
| 13 14 |  | 
| 14 15 | 
             
            end
         | 
| @@ -21,8 +22,8 @@ class InputInstrument < Instrument | |
| 21 22 | 
             
              attr_reader :listener
         | 
| 22 23 |  | 
| 23 24 | 
             
              # If +port+ is nil (the normal case), creates either a real or a mock port
         | 
| 24 | 
            -
              def initialize(name, port_num, no_midi=false)
         | 
| 25 | 
            -
                super(name, port_num, input_port(port_num, no_midi))
         | 
| 25 | 
            +
              def initialize(sym, name, port_num, no_midi=false)
         | 
| 26 | 
            +
                super(sym, name, port_num, input_port(port_num, no_midi))
         | 
| 26 27 | 
             
                @connections = []
         | 
| 27 28 | 
             
                @triggers = []
         | 
| 28 29 | 
             
              end
         | 
| @@ -46,7 +47,10 @@ class InputInstrument < Instrument | |
| 46 47 | 
             
              def stop
         | 
| 47 48 | 
             
                PatchMaster.instance.debug("instrument #{name} stop")
         | 
| 48 49 | 
             
                @port.clear_buffer
         | 
| 49 | 
            -
                @listener | 
| 50 | 
            +
                if @listener
         | 
| 51 | 
            +
                  @listener.close
         | 
| 52 | 
            +
                  @listener = nil
         | 
| 53 | 
            +
                end
         | 
| 50 54 | 
             
              end
         | 
| 51 55 |  | 
| 52 56 | 
             
              # Passes MIDI bytes on to triggers and to each output connection.
         | 
| @@ -59,7 +63,7 @@ class InputInstrument < Instrument | |
| 59 63 |  | 
| 60 64 | 
             
              def input_port(port_num, no_midi=false)
         | 
| 61 65 | 
             
                if no_midi
         | 
| 62 | 
            -
                  MockInputPort.new
         | 
| 66 | 
            +
                  MockInputPort.new(port_num)
         | 
| 63 67 | 
             
                else
         | 
| 64 68 | 
             
                  UniMIDI::Input.all[port_num].open
         | 
| 65 69 | 
             
                end
         | 
| @@ -69,8 +73,8 @@ end | |
| 69 73 |  | 
| 70 74 | 
             
            class OutputInstrument < Instrument
         | 
| 71 75 |  | 
| 72 | 
            -
              def initialize(name, port_num, no_midi=false)
         | 
| 73 | 
            -
                super(name, port_num, output_port(port_num, no_midi))
         | 
| 76 | 
            +
              def initialize(sym, name, port_num, no_midi=false)
         | 
| 77 | 
            +
                super(sym, name, port_num, output_port(port_num, no_midi))
         | 
| 74 78 | 
             
              end
         | 
| 75 79 |  | 
| 76 80 | 
             
              def midi_out(bytes)
         | 
| @@ -81,7 +85,7 @@ class OutputInstrument < Instrument | |
| 81 85 |  | 
| 82 86 | 
             
              def output_port(port_num, no_midi)
         | 
| 83 87 | 
             
                if no_midi
         | 
| 84 | 
            -
                  MockOutputPort.new
         | 
| 88 | 
            +
                  MockOutputPort.new(port_num)
         | 
| 85 89 | 
             
                else
         | 
| 86 90 | 
             
                  UniMIDI::Output.all[port_num].open
         | 
| 87 91 | 
             
                end
         | 
| @@ -90,12 +94,16 @@ end | |
| 90 94 |  | 
| 91 95 | 
             
            class MockInputPort
         | 
| 92 96 |  | 
| 97 | 
            +
              attr_reader :name
         | 
| 98 | 
            +
             | 
| 93 99 | 
             
              # For MIDIEye::Listener
         | 
| 94 100 | 
             
              def self.is_compatible?(input)
         | 
| 95 101 | 
             
                true
         | 
| 96 102 | 
             
              end
         | 
| 97 103 |  | 
| 98 | 
            -
               | 
| 104 | 
            +
              # Constructor param is ignored; it's required by MIDIEye.
         | 
| 105 | 
            +
              def initialize(arg)
         | 
| 106 | 
            +
                @name = "MockInputPort #{arg}"
         | 
| 99 107 | 
             
              end
         | 
| 100 108 |  | 
| 101 109 | 
             
              def gets
         | 
| @@ -115,6 +123,13 @@ class MockInputPort | |
| 115 123 | 
             
            end
         | 
| 116 124 |  | 
| 117 125 | 
             
            class MockOutputPort
         | 
| 126 | 
            +
             | 
| 127 | 
            +
              attr_reader :name
         | 
| 128 | 
            +
             | 
| 129 | 
            +
              def initialize(port_num)
         | 
| 130 | 
            +
                @name = "MockOutputPort #{port_num}"
         | 
| 131 | 
            +
              end
         | 
| 132 | 
            +
             | 
| 118 133 | 
             
              def puts(data)
         | 
| 119 134 | 
             
              end
         | 
| 120 135 | 
             
            end
         | 
| @@ -20,7 +20,9 @@ class PatchMaster < SimpleDelegator | |
| 20 20 |  | 
| 21 21 | 
             
              include Singleton
         | 
| 22 22 |  | 
| 23 | 
            -
              attr_reader :inputs, :outputs, :all_songs, :song_lists | 
| 23 | 
            +
              attr_reader :inputs, :outputs, :all_songs, :song_lists
         | 
| 24 | 
            +
              attr_reader :messages
         | 
| 25 | 
            +
              attr_reader :no_midi, :no_gui
         | 
| 24 26 |  | 
| 25 27 | 
             
              # A Cursor to which we delegate incoming position methods (#song_list,
         | 
| 26 28 | 
             
              # #song, #patch, #next_song, #prev_patch, etc.)
         | 
| @@ -29,11 +31,12 @@ class PatchMaster < SimpleDelegator | |
| 29 31 | 
             
              def initialize
         | 
| 30 32 | 
             
                @cursor = Cursor.new(self)
         | 
| 31 33 | 
             
                super(@cursor)
         | 
| 34 | 
            +
                @no_midi = false
         | 
| 35 | 
            +
                @no_gui = false
         | 
| 32 36 |  | 
| 33 37 | 
             
                if $DEBUG
         | 
| 34 38 | 
             
                  @debug_file = File.open(DEBUG_FILE, 'a')
         | 
| 35 39 | 
             
                end
         | 
| 36 | 
            -
                @no_midi = false
         | 
| 37 40 |  | 
| 38 41 | 
             
                init_data
         | 
| 39 42 | 
             
              end
         | 
| @@ -42,6 +45,10 @@ class PatchMaster < SimpleDelegator | |
| 42 45 | 
             
                @no_midi = true
         | 
| 43 46 | 
             
              end
         | 
| 44 47 |  | 
| 48 | 
            +
              def no_gui!
         | 
| 49 | 
            +
                @no_gui = true
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
             | 
| 45 52 | 
             
              # Loads +file+. Does its best to restore the current song list, song, and
         | 
| 46 53 | 
             
              # patch after loading.
         | 
| 47 54 | 
             
              def load(file)
         | 
| @@ -51,7 +58,6 @@ class PatchMaster < SimpleDelegator | |
| 51 58 | 
             
                @cursor.mark
         | 
| 52 59 | 
             
                init_data
         | 
| 53 60 | 
             
                DSL.new(@no_midi).load(file)
         | 
| 54 | 
            -
                @loaded_file = file
         | 
| 55 61 | 
             
                @cursor.restore
         | 
| 56 62 |  | 
| 57 63 | 
             
                if restart
         | 
| @@ -65,7 +71,6 @@ class PatchMaster < SimpleDelegator | |
| 65 71 |  | 
| 66 72 | 
             
              def save(file)
         | 
| 67 73 | 
             
                DSL.new(@no_midi).save(file)
         | 
| 68 | 
            -
                @loaded_file = file
         | 
| 69 74 | 
             
              rescue => ex
         | 
| 70 75 | 
             
                raise("error saving #{file}: #{ex}" + caller.join("\n"))
         | 
| 71 76 | 
             
              end
         | 
| @@ -73,11 +78,12 @@ class PatchMaster < SimpleDelegator | |
| 73 78 | 
             
              # Initializes the cursor and all data.
         | 
| 74 79 | 
             
              def init_data
         | 
| 75 80 | 
             
                @cursor.clear
         | 
| 76 | 
            -
                @inputs =  | 
| 77 | 
            -
                @outputs =  | 
| 81 | 
            +
                @inputs = []
         | 
| 82 | 
            +
                @outputs = []
         | 
| 78 83 | 
             
                @song_lists = []
         | 
| 79 84 | 
             
                @all_songs = SortedSongList.new('All Songs')
         | 
| 80 85 | 
             
                @song_lists << @all_songs
         | 
| 86 | 
            +
                @messages = {}
         | 
| 81 87 | 
             
              end
         | 
| 82 88 |  | 
| 83 89 | 
             
              # If +init_cursor+ is +true+ (the default), initializes current song list,
         | 
| @@ -86,25 +92,55 @@ class PatchMaster < SimpleDelegator | |
| 86 92 | 
             
                @cursor.init if init_cursor
         | 
| 87 93 | 
             
                @cursor.patch.start if @cursor.patch
         | 
| 88 94 | 
             
                @running = true
         | 
| 89 | 
            -
                @inputs. | 
| 95 | 
            +
                @inputs.map(&:start)
         | 
| 90 96 | 
             
              end
         | 
| 91 97 |  | 
| 98 | 
            +
              # Stop everything, including input instruments' MIDIEye listener threads.
         | 
| 92 99 | 
             
              def stop
         | 
| 93 100 | 
             
                @cursor.patch.stop if @cursor.patch
         | 
| 94 | 
            -
                @inputs. | 
| 101 | 
            +
                @inputs.map(&:stop)
         | 
| 95 102 | 
             
                @running = false
         | 
| 96 103 | 
             
              end
         | 
| 97 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.
         | 
| 111 | 
            +
              def run
         | 
| 112 | 
            +
                start(true)
         | 
| 113 | 
            +
                @inputs.each { |input| input.listener.join }
         | 
| 114 | 
            +
                stop
         | 
| 115 | 
            +
              end
         | 
| 116 | 
            +
             | 
| 98 117 | 
             
              def running?
         | 
| 99 118 | 
             
                @running
         | 
| 100 119 | 
             
              end
         | 
| 101 120 |  | 
| 121 | 
            +
              # Send the message with the given name to all outputs.
         | 
| 122 | 
            +
              def send_message(name)
         | 
| 123 | 
            +
                msg = @messages[name]
         | 
| 124 | 
            +
                if !msg
         | 
| 125 | 
            +
                  message("Message \"#{name}\" not found")
         | 
| 126 | 
            +
                  return
         | 
| 127 | 
            +
                end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                debug("Sending message \"#{name}\"")
         | 
| 130 | 
            +
                @outputs.each { |out| out.midi_out(msg) }
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                # If the user accidentally calls send_message in a filter at the end,
         | 
| 133 | 
            +
                # then the filter will return whatever this method returns. Just in
         | 
| 134 | 
            +
                # case, return nil instead of whatever the preceding code would return.
         | 
| 135 | 
            +
                nil
         | 
| 136 | 
            +
              end
         | 
| 137 | 
            +
             | 
| 102 138 | 
             
              # Sends the +CM_ALL_NOTES_OFF+ controller message to all output
         | 
| 103 139 | 
             
              # instruments on all 16 MIDI channels. If +individual_notes+ is +true+
         | 
| 104 140 | 
             
              # send individual +NOTE_OFF+ messages to all notes as well.
         | 
| 105 141 | 
             
              def panic(individual_notes=false)
         | 
| 106 142 | 
             
                debug("panic(#{individual_notes})")
         | 
| 107 | 
            -
                @outputs. | 
| 143 | 
            +
                @outputs.each do |out|
         | 
| 108 144 | 
             
                  MIDI_CHANNELS.times do |chan|
         | 
| 109 145 | 
             
                    out.midi_out([CONTROLLER + chan, CM_ALL_NOTES_OFF, 0])
         | 
| 110 146 | 
             
                    if individual_notes
         | 
| @@ -114,29 +150,6 @@ class PatchMaster < SimpleDelegator | |
| 114 150 | 
             
                end
         | 
| 115 151 | 
             
              end
         | 
| 116 152 |  | 
| 117 | 
            -
              # Opens the most recently loaded/saved file name in an editor. After
         | 
| 118 | 
            -
              # editing, the file is re-loaded.
         | 
| 119 | 
            -
              def edit
         | 
| 120 | 
            -
                editor_command = find_editor
         | 
| 121 | 
            -
                unless editor_command
         | 
| 122 | 
            -
                  message("Can not find $VISUAL, $EDITOR, vim, or vi on your path")
         | 
| 123 | 
            -
                  return
         | 
| 124 | 
            -
                end
         | 
| 125 | 
            -
             | 
| 126 | 
            -
                cmd = "#{editor_command} #{@loaded_file}"
         | 
| 127 | 
            -
                debug(cmd)
         | 
| 128 | 
            -
                system(cmd)
         | 
| 129 | 
            -
                load(@loaded_file)
         | 
| 130 | 
            -
              end
         | 
| 131 | 
            -
             | 
| 132 | 
            -
              # Return the first legit command from $VISUAL, $EDITOR, vim, vi, and
         | 
| 133 | 
            -
              # notepad.exe.
         | 
| 134 | 
            -
              def find_editor
         | 
| 135 | 
            -
                @editor ||= [ENV['VISUAL'], ENV['EDITOR'], 'vim', 'vi', 'notepad.exe'].compact.detect do |cmd|
         | 
| 136 | 
            -
                  system('which', cmd) || File.exist?(cmd)
         | 
| 137 | 
            -
                end
         | 
| 138 | 
            -
              end
         | 
| 139 | 
            -
             | 
| 140 153 | 
             
              # Output +str+ to @debug_file or $stderr.
         | 
| 141 154 | 
             
              def debug(str)
         | 
| 142 155 | 
             
                return unless $DEBUG
         | 
| @@ -62,6 +62,11 @@ class Integer | |
| 62 62 | 
             
              alias_method :rt?, :realtime?
         | 
| 63 63 | 
             
            end
         | 
| 64 64 |  | 
| 65 | 
            +
            # All the methods here delegate to the first byte in the array, so for
         | 
| 66 | 
            +
            # example the following two are equivalent:
         | 
| 67 | 
            +
            #
         | 
| 68 | 
            +
            #   my_array.note_on?
         | 
| 69 | 
            +
            #   my_array[0].note_on?
         | 
| 65 70 | 
             
            class Array
         | 
| 66 71 |  | 
| 67 72 | 
             
              def high_nibble
         | 
    
        data/test/test_helper.rb
    CHANGED
    
    | @@ -7,19 +7,15 @@ PM::PatchMaster.instance.no_midi! | |
| 7 7 |  | 
| 8 8 | 
             
            module PM
         | 
| 9 9 |  | 
| 10 | 
            -
            # To help with testing, we replace MockInputPort#gets and
         | 
| 11 | 
            -
            # MockOutputPort#puts with versions that send what we want and save what | 
| 12 | 
            -
            # received.
         | 
| 10 | 
            +
            # To help with testing, we replace PM::MockInputPort#gets and
         | 
| 11 | 
            +
            # PM::MockOutputPort#puts with versions that send what we want and save what
         | 
| 12 | 
            +
            # is received.
         | 
| 13 13 | 
             
            class MockInputPort
         | 
| 14 14 |  | 
| 15 15 | 
             
              attr_accessor :data_to_send
         | 
| 16 | 
            -
                
         | 
| 17 | 
            -
              # For MIDIEye::Listener
         | 
| 18 | 
            -
              def self.is_compatible?(input)
         | 
| 19 | 
            -
                true
         | 
| 20 | 
            -
              end
         | 
| 21 16 |  | 
| 22 | 
            -
              def initialize( | 
| 17 | 
            +
              def initialize(arg)
         | 
| 18 | 
            +
                @name = "MockInputPort #{arg}"
         | 
| 23 19 | 
             
                @t0 = (Time.now.to_f * 1000).to_i
         | 
| 24 20 | 
             
              end
         | 
| 25 21 |  | 
| @@ -28,20 +24,14 @@ class MockInputPort | |
| 28 24 | 
             
                @data_to_send = []
         | 
| 29 25 | 
             
                [{:data => retval, :timestamp => (Time.now.to_f * 1000).to_i - @t0}]
         | 
| 30 26 | 
             
              end
         | 
| 31 | 
            -
                
         | 
| 32 | 
            -
              def poll
         | 
| 33 | 
            -
                yield gets
         | 
| 34 | 
            -
              end
         | 
| 35 | 
            -
             | 
| 36 | 
            -
              # add this class to the Listener class' known input types
         | 
| 37 | 
            -
              MIDIEye::Listener.input_types << self 
         | 
| 38 27 | 
             
            end
         | 
| 39 28 |  | 
| 40 29 | 
             
            class MockOutputPort
         | 
| 41 30 |  | 
| 42 31 | 
             
              attr_accessor :buffer
         | 
| 43 32 |  | 
| 44 | 
            -
              def initialize
         | 
| 33 | 
            +
              def initialize(port_num)
         | 
| 34 | 
            +
                @name = "MockOutputPort #{port_num}"
         | 
| 45 35 | 
             
                @buffer = []
         | 
| 46 36 | 
             
              end
         | 
| 47 37 |  | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: patchmaster
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.0. | 
| 4 | 
            +
              version: 0.0.4
         | 
| 5 5 | 
             
              prerelease: 
         | 
| 6 6 | 
             
            platform: ruby
         | 
| 7 7 | 
             
            authors:
         | 
| @@ -9,11 +9,11 @@ authors: | |
| 9 9 | 
             
            autorequire: 
         | 
| 10 10 | 
             
            bindir: bin
         | 
| 11 11 | 
             
            cert_chain: []
         | 
| 12 | 
            -
            date: 2012-04- | 
| 12 | 
            +
            date: 2012-04-24 00:00:00.000000000 Z
         | 
| 13 13 | 
             
            dependencies:
         | 
| 14 14 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 15 | 
            -
              name:  | 
| 16 | 
            -
              requirement: & | 
| 15 | 
            +
              name: midi-eye
         | 
| 16 | 
            +
              requirement: &2156041300 !ruby/object:Gem::Requirement
         | 
| 17 17 | 
             
                none: false
         | 
| 18 18 | 
             
                requirements:
         | 
| 19 19 | 
             
                - - ! '>='
         | 
| @@ -21,7 +21,7 @@ dependencies: | |
| 21 21 | 
             
                    version: '0'
         | 
| 22 22 | 
             
              type: :runtime
         | 
| 23 23 | 
             
              prerelease: false
         | 
| 24 | 
            -
              version_requirements: * | 
| 24 | 
            +
              version_requirements: *2156041300
         | 
| 25 25 | 
             
            description: ! 'PatchMaster is realtime MIDI performance software that alloweds a
         | 
| 26 26 | 
             
              musician
         | 
| 27 27 |  |