diamond 0.5.1
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/LICENSE +13 -0
- data/README.md +126 -0
- data/lib/diamond.rb +35 -0
- data/lib/diamond/api.rb +102 -0
- data/lib/diamond/arpeggiator.rb +81 -0
- data/lib/diamond/clock.rb +88 -0
- data/lib/diamond/midi.rb +165 -0
- data/lib/diamond/osc.rb +102 -0
- data/lib/diamond/pattern.rb +95 -0
- data/lib/diamond/sequence.rb +178 -0
- data/lib/diamond/sequence_parameters.rb +164 -0
- data/test/api_test.rb +152 -0
- data/test/arpeggiator_test.rb +34 -0
- data/test/helper.rb +23 -0
- data/test/osc_test.rb +37 -0
- data/test/pattern_test.rb +63 -0
- data/test/sequence_parameters_test.rb +96 -0
- data/test/sequence_test.rb +36 -0
- metadata +222 -0
    
        data/lib/diamond/midi.rb
    ADDED
    
    | @@ -0,0 +1,165 @@ | |
| 1 | 
            +
            module Diamond
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              # Enable the instrument to use MIDI
         | 
| 4 | 
            +
              module MIDI
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                # Methods dealing with MIDI input
         | 
| 7 | 
            +
                module Input
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  def self.included(base)
         | 
| 10 | 
            +
                    base.send(:extend, Forwardable)
         | 
| 11 | 
            +
                    base.send(:def_delegators, :@midi,
         | 
| 12 | 
            +
                              :input,
         | 
| 13 | 
            +
                              :inputs,
         | 
| 14 | 
            +
                              :omni_on,
         | 
| 15 | 
            +
                              :rx_channel, 
         | 
| 16 | 
            +
                              :receive_channel,
         | 
| 17 | 
            +
                              :rx_channel=,
         | 
| 18 | 
            +
                              :receive_channel=)
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  # Add MIDI input notes 
         | 
| 22 | 
            +
                  # @param [Array<MIDIMessage>, MIDIMessage, *MIDIMessage] args
         | 
| 23 | 
            +
                  # @return [Array<MIDIMessage>]
         | 
| 24 | 
            +
                  def add(*args)
         | 
| 25 | 
            +
                    @midi.input << args
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                  alias_method :<<, :add
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  # Add note offs to cancel input
         | 
| 30 | 
            +
                  # @param [Array<MIDIMessage>, MIDIMessage, *MIDIMessage] args
         | 
| 31 | 
            +
                  # @return [Array<MIDIMessage>]
         | 
| 32 | 
            +
                  def remove(*args)
         | 
| 33 | 
            +
                    messages = MIDIInstrument::Message.to_note_offs(*args)
         | 
| 34 | 
            +
                    @midi.input.add(messages.compact)
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  # Initialize adding and removing MIDI notes from the sequence
         | 
| 38 | 
            +
                  # @param [Sequence] sequence
         | 
| 39 | 
            +
                  # @return [Boolean]
         | 
| 40 | 
            +
                  def enable_note_control(sequence)  
         | 
| 41 | 
            +
                    @midi.input.receive(:class => MIDIMessage::NoteOn) do |event|
         | 
| 42 | 
            +
                      message = event[:message]
         | 
| 43 | 
            +
                      if @midi.input.channel.nil? || @midi.input.channel == message.channel
         | 
| 44 | 
            +
                        puts "[DEBUG] MIDI: add note from input #{message.name} channel: #{message.channel}" if @debug
         | 
| 45 | 
            +
                        sequence.add(message)
         | 
| 46 | 
            +
                      end
         | 
| 47 | 
            +
                    end 
         | 
| 48 | 
            +
                    @midi.input.receive(:class => MIDIMessage::NoteOff) do |event| 
         | 
| 49 | 
            +
                      message = event[:message]
         | 
| 50 | 
            +
                      if @midi.input.channel.nil? || @midi.input.channel == message.channel
         | 
| 51 | 
            +
                        puts "[DEBUG] MIDI: remove note from input #{message.name} channel: #{message.channel}" if @debug
         | 
| 52 | 
            +
                        sequence.remove(message)
         | 
| 53 | 
            +
                      end
         | 
| 54 | 
            +
                    end
         | 
| 55 | 
            +
                    true
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  # Initialize a user-defined map of control change messages
         | 
| 59 | 
            +
                  # @param [SequenceParameters] parameters
         | 
| 60 | 
            +
                  # @param [Array<Hash>] map
         | 
| 61 | 
            +
                  # @return [Boolean]
         | 
| 62 | 
            +
                  def enable_parameter_control(parameters, map)
         | 
| 63 | 
            +
                    from_range = 0..127
         | 
| 64 | 
            +
                    @midi.input.receive(:class => MIDIMessage::ControlChange) do |event|
         | 
| 65 | 
            +
                      message = event[:message]
         | 
| 66 | 
            +
                      if @midi.input.channel.nil? || @midi.input.channel == message.channel
         | 
| 67 | 
            +
                        index = message.index
         | 
| 68 | 
            +
                        mapping = map.find { |mapping| mapping[:index] == index }
         | 
| 69 | 
            +
                        property = mapping[:property]
         | 
| 70 | 
            +
                        to_range = SequenceParameters::RANGE[property]
         | 
| 71 | 
            +
                        value = message.value
         | 
| 72 | 
            +
                        value = Scale.transform(value).from(from_range).to(to_range)
         | 
| 73 | 
            +
                        puts "[DEBUG] MIDI: #{property}= #{value} channel: #{message.channel}" if @debug
         | 
| 74 | 
            +
                        parameters.send("#{property}=", value)
         | 
| 75 | 
            +
                      end
         | 
| 76 | 
            +
                    end
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  private
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  # @param [Array<UniMIDI::Input>] inputs
         | 
| 82 | 
            +
                  # @param [Hash] options
         | 
| 83 | 
            +
                  # @option options [Fixnum] :channel The receive channel (also: :rx_channel)
         | 
| 84 | 
            +
                  def initialize_input(inputs, options = {})
         | 
| 85 | 
            +
                    @midi.input.devices.concat(inputs)
         | 
| 86 | 
            +
                    @midi.input.channel = options[:receive_channel]
         | 
| 87 | 
            +
                  end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                # Methods dealing with MIDI output
         | 
| 92 | 
            +
                module Output
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  def self.included(base)
         | 
| 95 | 
            +
                    base.send(:extend, Forwardable)
         | 
| 96 | 
            +
                    base.send(:def_delegators, :@midi,
         | 
| 97 | 
            +
                              :mute,
         | 
| 98 | 
            +
                              :mute=,
         | 
| 99 | 
            +
                              :output,
         | 
| 100 | 
            +
                              :outputs,
         | 
| 101 | 
            +
                              :toggle_mute,
         | 
| 102 | 
            +
                              :tx_channel, 
         | 
| 103 | 
            +
                              :transmit_channel,
         | 
| 104 | 
            +
                              :tx_channel=,
         | 
| 105 | 
            +
                              :transmit_channel=)
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  # Initialize MIDI output, enabling the sequencer to emit notes
         | 
| 109 | 
            +
                  # @param [Sequencer::Core] sequencer
         | 
| 110 | 
            +
                  # @return [Boolean]
         | 
| 111 | 
            +
                  def enable_output(sequencer)
         | 
| 112 | 
            +
                    sequencer.event.perform << proc do |bucket|
         | 
| 113 | 
            +
                      unless bucket.empty?
         | 
| 114 | 
            +
                        if @debug
         | 
| 115 | 
            +
                          bucket.each do |message|
         | 
| 116 | 
            +
                            puts "[DEBUG] MIDI: output #{message.name} channel: #{message.channel}"
         | 
| 117 | 
            +
                          end
         | 
| 118 | 
            +
                        end
         | 
| 119 | 
            +
                        @midi.output.puts(bucket)
         | 
| 120 | 
            +
                      end
         | 
| 121 | 
            +
                    end
         | 
| 122 | 
            +
                    sequencer.event.stop << proc { emit_pending_note_offs }
         | 
| 123 | 
            +
                    true
         | 
| 124 | 
            +
                  end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                  private
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                  # Initialize MIDI output
         | 
| 129 | 
            +
                  # @param [Array<UniMIDI::Output>] outputs
         | 
| 130 | 
            +
                  # @param [Hash] options
         | 
| 131 | 
            +
                  # @option options [Fixnum] :tx_channel The transmit channel
         | 
| 132 | 
            +
                  def initialize_output(outputs, options = {})
         | 
| 133 | 
            +
                    @midi.output.devices.concat(outputs)
         | 
| 134 | 
            +
                    @midi.output.channel = options[:transmit_channel]
         | 
| 135 | 
            +
                  end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                # An access point for dealing with all MIDI functionality for the instrument
         | 
| 140 | 
            +
                class Node
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                  include Input
         | 
| 143 | 
            +
                  include Output
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                  # Initialize MIDI input and output
         | 
| 146 | 
            +
                  # @param [Hash] devices
         | 
| 147 | 
            +
                  # @param [Hash] options
         | 
| 148 | 
            +
                  # @option options [Fixnum] :channel The receive channel (also: :rx_channel)
         | 
| 149 | 
            +
                  # @option options [Fixnum] :tx_channel The transmit channel
         | 
| 150 | 
            +
                  def initialize(devices, options = {})
         | 
| 151 | 
            +
                    @debug = options.fetch(:debug, false)
         | 
| 152 | 
            +
                    @midi = MIDIInstrument::Node.new
         | 
| 153 | 
            +
                    initialize_input(devices[:input], options)
         | 
| 154 | 
            +
                    initialize_output(devices[:output], options)
         | 
| 155 | 
            +
                  end
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                # Shortcut to Diamond::MIDI::Node.new
         | 
| 160 | 
            +
                def self.new(*args)
         | 
| 161 | 
            +
                  Node.new(*args)
         | 
| 162 | 
            +
                end
         | 
| 163 | 
            +
             | 
| 164 | 
            +
              end
         | 
| 165 | 
            +
            end
         | 
    
        data/lib/diamond/osc.rb
    ADDED
    
    | @@ -0,0 +1,102 @@ | |
| 1 | 
            +
            module Diamond
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              # Enable the instrument to use OSC
         | 
| 4 | 
            +
              module OSC
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                # An access point for dealing with all OSC functionality for the instrument
         | 
| 7 | 
            +
                class Node
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  # @param [Hash] options
         | 
| 10 | 
            +
                  # @option options [Boolean] :debug Whether to send debug output
         | 
| 11 | 
            +
                  # @option options [Fixnum] :server_port The port to listen on (default: 8000)
         | 
| 12 | 
            +
                  def initialize(options = {})
         | 
| 13 | 
            +
                    @debug = options.fetch(:debug, false)
         | 
| 14 | 
            +
                    port = options.fetch(:server_port, 8000)
         | 
| 15 | 
            +
                    @server = ::OSC::EMServer.new(port)
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  # Enable controlling the instrument via OSC
         | 
| 19 | 
            +
                  # @param [Object] subject The object to operate on when messages are received
         | 
| 20 | 
            +
                  # @param [Array<Hash>] map
         | 
| 21 | 
            +
                  # @return [Boolean]
         | 
| 22 | 
            +
                  def enable_parameter_control(subject, map)
         | 
| 23 | 
            +
                    start_server
         | 
| 24 | 
            +
                    maps = map.map do |item|
         | 
| 25 | 
            +
                      property = item[:property]
         | 
| 26 | 
            +
                      from_range = item[:value] || (0..1.0)
         | 
| 27 | 
            +
                      to_range = SequenceParameters::RANGE[property]
         | 
| 28 | 
            +
                      @server.add_method(item[:address]) do |message|
         | 
| 29 | 
            +
                        value = message.to_a[0]
         | 
| 30 | 
            +
                        value = Scale.transform(value).from(from_range).to(to_range)
         | 
| 31 | 
            +
                        puts "[DEBUG]: OSC: #{property}= #{value}" if @debug
         | 
| 32 | 
            +
                        subject.send("#{property}=", value)
         | 
| 33 | 
            +
                        true
         | 
| 34 | 
            +
                      end
         | 
| 35 | 
            +
                      true
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
                    maps.any?     
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  private
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  # Start the server
         | 
| 43 | 
            +
                  # @return [Thread]
         | 
| 44 | 
            +
                  def start_server
         | 
| 45 | 
            +
                    @thread = Thread.new do
         | 
| 46 | 
            +
                      begin
         | 
| 47 | 
            +
                        EM.epoll
         | 
| 48 | 
            +
                        EM.run { @server.run }
         | 
| 49 | 
            +
                      rescue Exception => exception
         | 
| 50 | 
            +
                        Thread.main.raise(exception)
         | 
| 51 | 
            +
                      end
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
                    @thread.abort_on_exception = true
         | 
| 54 | 
            +
                    @thread
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                # Shortcut to Diamond::OSC::Node.new
         | 
| 60 | 
            +
                def self.new(*args)
         | 
| 61 | 
            +
                  Node.new(*args)
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
              end
         | 
| 65 | 
            +
            end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
            # Patch the OSC module
         | 
| 68 | 
            +
            #
         | 
| 69 | 
            +
            module OSC
         | 
| 70 | 
            +
              class EMServer
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def run
         | 
| 73 | 
            +
                  open
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                def open
         | 
| 77 | 
            +
                  EM::open_datagram_socket("0.0.0.0", @port, Connection)
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
            end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
            module EventMachine
         | 
| 84 | 
            +
              module WebSocket
         | 
| 85 | 
            +
                def self.start(options, &blk)
         | 
| 86 | 
            +
                  #EM.epoll
         | 
| 87 | 
            +
                  #EM.run {
         | 
| 88 | 
            +
                    trap("TERM") { stop }
         | 
| 89 | 
            +
                    trap("INT")  { stop }
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    run(options, &blk)
         | 
| 92 | 
            +
                  #}
         | 
| 93 | 
            +
                end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                def self.run(options)
         | 
| 96 | 
            +
                  host, port = options.values_at(:host, :port)
         | 
| 97 | 
            +
                  EM.start_server(host, port, Connection, options) do |c|
         | 
| 98 | 
            +
                    yield c
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
                end
         | 
| 101 | 
            +
              end
         | 
| 102 | 
            +
            end
         | 
| @@ -0,0 +1,95 @@ | |
| 1 | 
            +
            module Diamond
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              # Pattern that the sequence is derived from given the parameters and input
         | 
| 4 | 
            +
              class Pattern
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                module ClassMethods
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  # All patterns
         | 
| 9 | 
            +
                  # @return [Array<Pattern>]
         | 
| 10 | 
            +
                  def all
         | 
| 11 | 
            +
                    @patterns ||= []
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  # Find a pattern by its name (case insensitive)
         | 
| 15 | 
            +
                  # @param [String, Symbol] name
         | 
| 16 | 
            +
                  # @return [Pattern]
         | 
| 17 | 
            +
                  def find(name)
         | 
| 18 | 
            +
                    all.find { |pattern| pattern.name.to_s.downcase == name.to_s.downcase }
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  # Construct and add a pattern
         | 
| 22 | 
            +
                  # @param [Symbol, String] name
         | 
| 23 | 
            +
                  # @param [Proc] block
         | 
| 24 | 
            +
                  # @return [Array<Pattern>]
         | 
| 25 | 
            +
                  def add(*args, &block)
         | 
| 26 | 
            +
                    all << new(*args, &block)
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  # Add a pattern
         | 
| 30 | 
            +
                  # @param [Pattern] pattern
         | 
| 31 | 
            +
                  # @return [Array<Pattern>]
         | 
| 32 | 
            +
                  def <<(pattern)
         | 
| 33 | 
            +
                    all << pattern
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  # @return [Pattern]
         | 
| 37 | 
            +
                  def first
         | 
| 38 | 
            +
                    all.first
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  # @return [Pattern]
         | 
| 42 | 
            +
                  def last
         | 
| 43 | 
            +
                    all.last
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                  
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                extend ClassMethods
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                attr_reader :name
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                # @param [String, Symbol] name A name to identify the pattern by eg "up/down"
         | 
| 53 | 
            +
                # @param [Proc] block The pattern procedure, which should return an array of scale degree numbers.  
         | 
| 54 | 
            +
                #                     For example, given the arguments (3, 7) the "Up" pattern will produce [0, 7, 14, 21]
         | 
| 55 | 
            +
                def initialize(name, &block)
         | 
| 56 | 
            +
                  @name = name
         | 
| 57 | 
            +
                  @proc = block
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                # Compute scale degrees using the pattern with the given range and interval
         | 
| 61 | 
            +
                # @param [Fixnum] range
         | 
| 62 | 
            +
                # @param [Interval] interval
         | 
| 63 | 
            +
                # @return [Array<Fixnum>]
         | 
| 64 | 
            +
                def compute(range, interval)
         | 
| 65 | 
            +
                  @proc.call(range, interval)
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                # Standard preset patterns
         | 
| 69 | 
            +
                module Presets
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  Pattern << Pattern.new("Up") do |range, interval|
         | 
| 72 | 
            +
                    0.upto(range).map { |num| num * interval }
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  Pattern << Pattern.new("Down") do |range, interval|
         | 
| 76 | 
            +
                    range.downto(0).map { |num| num * interval }
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  Pattern << Pattern.new("UpDown") do |range, interval|
         | 
| 80 | 
            +
                    up = 0.upto(range).map { |num| num * interval }
         | 
| 81 | 
            +
                    down = [(range - 1), 0].max.downto(0).map { |num| num * interval }
         | 
| 82 | 
            +
                    up + down
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  Pattern << Pattern.new("DownUp") do |range, interval|
         | 
| 86 | 
            +
                    down = range.downto(0).map { |num| num * interval }
         | 
| 87 | 
            +
                    up = 1.upto(range).map { |num| num * interval }
         | 
| 88 | 
            +
                    down + up
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
              end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
            end
         | 
| @@ -0,0 +1,178 @@ | |
| 1 | 
            +
            module Diamond
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              # The note event sequence from where the arpeggiator output is derived
         | 
| 4 | 
            +
              class Sequence
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                extend Forwardable
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def_delegators :@sequence, :each, :first, :last, :length
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def initialize 
         | 
| 11 | 
            +
                  @parameter = nil
         | 
| 12 | 
            +
                  # realtime
         | 
| 13 | 
            +
                  @changed = false
         | 
| 14 | 
            +
                  @input_queue = []
         | 
| 15 | 
            +
                  @queue = []
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                # The bucket of messages for the given pointer
         | 
| 19 | 
            +
                # @param [Fixnum] pointer
         | 
| 20 | 
            +
                # @return [Array<MIDIMessage>]
         | 
| 21 | 
            +
                def at(pointer)
         | 
| 22 | 
            +
                  if changed? && (pointer % @parameter.rate == 0)
         | 
| 23 | 
            +
                    update
         | 
| 24 | 
            +
                    @changed = false
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                  enqueue_next(pointer)
         | 
| 27 | 
            +
                  messages = @queue.shift || []
         | 
| 28 | 
            +
                  messages
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                # Has the sequence changed since the last update?
         | 
| 32 | 
            +
                # @return [Boolean]
         | 
| 33 | 
            +
                def changed?
         | 
| 34 | 
            +
                  @changed
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                # Add inputted note_messages
         | 
| 38 | 
            +
                # @param [Array<MIDIMessage::NoteOn>, MIDIMessage::NoteOn, *MIDIMessage::NoteOn] note_messages
         | 
| 39 | 
            +
                # @return [Boolean]
         | 
| 40 | 
            +
                def add(*note_messages)
         | 
| 41 | 
            +
                  messages = [note_messages].flatten.compact
         | 
| 42 | 
            +
                  @input_queue.concat(messages)
         | 
| 43 | 
            +
                  mark_changed
         | 
| 44 | 
            +
                  true
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                # Remove input note messages with the same note value
         | 
| 48 | 
            +
                # @param [Array<MIDIMessage::NoteOn, MIDIMessage::NoteOff>, MIDIMessage::NoteOff, MIDIMessage::NoteOn, *MIDIMessage::NoteOff, *MIDIMessage::NoteOn] note_messages
         | 
| 49 | 
            +
                # @return [Boolean]
         | 
| 50 | 
            +
                def remove(*note_messages)
         | 
| 51 | 
            +
                  messages = [note_messages].flatten
         | 
| 52 | 
            +
                  deletion_queue = messages.map(&:note)
         | 
| 53 | 
            +
                  @input_queue.delete_if { |message| deletion_queue.include?(message.note) }
         | 
| 54 | 
            +
                  mark_changed
         | 
| 55 | 
            +
                  true
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                # Remove all input note messages
         | 
| 59 | 
            +
                # @return [Boolean]
         | 
| 60 | 
            +
                def remove_all
         | 
| 61 | 
            +
                  @input_queue.clear
         | 
| 62 | 
            +
                  mark_changed
         | 
| 63 | 
            +
                  true
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                # All NoteOff messages in the queue
         | 
| 67 | 
            +
                # @return [Array<MIDIMessage::NoteOff>]
         | 
| 68 | 
            +
                def pending_note_offs
         | 
| 69 | 
            +
                  messages = @queue.map do |bucket|
         | 
| 70 | 
            +
                    unless bucket.nil?
         | 
| 71 | 
            +
                      bucket.select { |m| m.class == MIDIMessage::NoteOff }       
         | 
| 72 | 
            +
                    end
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
                  messages.flatten.compact
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                # Mark the sequence as changed
         | 
| 78 | 
            +
                # @return [Boolean]
         | 
| 79 | 
            +
                def mark_changed
         | 
| 80 | 
            +
                  @changed = true
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                protected
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                # Apply the given parameters object
         | 
| 86 | 
            +
                # @param [SequenceParameters] parameters
         | 
| 87 | 
            +
                # @return [SequenceParameters]
         | 
| 88 | 
            +
                def use_parameters(parameters)
         | 
| 89 | 
            +
                  @parameter = parameters
         | 
| 90 | 
            +
                  update
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                private
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                # Enqueue next bucket for the given pointer
         | 
| 96 | 
            +
                # @param [Fixnum] pointer
         | 
| 97 | 
            +
                # @return [Array<NoteEvent>]
         | 
| 98 | 
            +
                def enqueue_next(pointer)
         | 
| 99 | 
            +
                  bucket = @sequence[pointer]
         | 
| 100 | 
            +
                  enqueue(bucket) unless bucket.nil?
         | 
| 101 | 
            +
                  bucket
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                # Prepare the given event bucket for performance, moving note messages to the queue
         | 
| 105 | 
            +
                # @param [Array<NoteEvent>] bucket
         | 
| 106 | 
            +
                # @return [Array<NoteEvent>]
         | 
| 107 | 
            +
                def enqueue(bucket)
         | 
| 108 | 
            +
                  bucket.map do |event|
         | 
| 109 | 
            +
                    @queue[0] ||= []
         | 
| 110 | 
            +
                    @queue[0] << event.start 
         | 
| 111 | 
            +
                    float_length = (event.length.to_f / 100) * @parameter.duration.to_f
         | 
| 112 | 
            +
                    length = float_length.to_i
         | 
| 113 | 
            +
                    @queue[length] ||= []
         | 
| 114 | 
            +
                    @queue[length] << event.finish
         | 
| 115 | 
            +
                    event
         | 
| 116 | 
            +
                  end
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                # Commit changes to the sequence
         | 
| 120 | 
            +
                # @return [ArpeggiatorSequence]
         | 
| 121 | 
            +
                def update
         | 
| 122 | 
            +
                  notes = get_note_sequence
         | 
| 123 | 
            +
                  initialize_sequence(notes.length)
         | 
| 124 | 
            +
                  populate_sequence(notes) unless notes.empty?
         | 
| 125 | 
            +
                  @sequence
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                # (Re)initialize the sequence with the given length
         | 
| 129 | 
            +
                # @param [Fixnum] length
         | 
| 130 | 
            +
                # @return [Array]
         | 
| 131 | 
            +
                def initialize_sequence(length)
         | 
| 132 | 
            +
                  sequence_length_in_ticks = length * @parameter.duration
         | 
| 133 | 
            +
                  @sequence = Array.new(sequence_length_in_ticks, [])
         | 
| 134 | 
            +
                end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                # Populate the sequence with the given notes
         | 
| 137 | 
            +
                # @param [Array<MIDIMessage::NoteOn>] notes
         | 
| 138 | 
            +
                # @return [Array<Array<NoteEvent>>]
         | 
| 139 | 
            +
                def populate_sequence(notes)
         | 
| 140 | 
            +
                  @parameter.pattern_offset.times { notes.push(notes.shift) }
         | 
| 141 | 
            +
                  notes.each_with_index do |note, i| 
         | 
| 142 | 
            +
                    index = i * @parameter.duration
         | 
| 143 | 
            +
                    populate_bucket(index, note) unless @sequence[index].nil?
         | 
| 144 | 
            +
                  end
         | 
| 145 | 
            +
                  @sequence
         | 
| 146 | 
            +
                end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                # Populate the bucket for index with the given note message
         | 
| 149 | 
            +
                # @param [Fixnum] index
         | 
| 150 | 
            +
                # @param [MIDIMessage::NoteOn] note_message
         | 
| 151 | 
            +
                # @return [Array<NoteEvent>]
         | 
| 152 | 
            +
                def populate_bucket(index, note_message)
         | 
| 153 | 
            +
                  @sequence[index] = create_bucket(note_message)
         | 
| 154 | 
            +
                end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                # Create a bucket/note event for the given note message
         | 
| 157 | 
            +
                # @param [MIDIMessage::NoteOn] note_message
         | 
| 158 | 
            +
                # @return [Array<NoteEvent>]
         | 
| 159 | 
            +
                def create_bucket(note_message)
         | 
| 160 | 
            +
                  event = MIDIInstrument::NoteEvent.new(note_message, @parameter.gate)
         | 
| 161 | 
            +
                  [event]
         | 
| 162 | 
            +
                end
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                # The input queue as note messages
         | 
| 165 | 
            +
                # @return [Array<MIDIMessage::NoteOn>]
         | 
| 166 | 
            +
                def get_note_sequence
         | 
| 167 | 
            +
                  notes = @parameter.computed_pattern.map do |degree|
         | 
| 168 | 
            +
                    @input_queue.map do |message| 
         | 
| 169 | 
            +
                      note = message.note + degree + @parameter.transpose
         | 
| 170 | 
            +
                      MIDIMessage::NoteOn.new(message.channel, note, message.velocity)
         | 
| 171 | 
            +
                    end
         | 
| 172 | 
            +
                  end
         | 
| 173 | 
            +
                  notes.flatten.compact
         | 
| 174 | 
            +
                end 
         | 
| 175 | 
            +
             | 
| 176 | 
            +
              end
         | 
| 177 | 
            +
             | 
| 178 | 
            +
            end
         |