Class: Musa::MIDIRecorder::MIDIRecorder

Inherits:
Object
  • Object
show all
Defined in:
lib/musa-dsl/midi/midi-recorder.rb

Overview

Collects raw MIDI bytes alongside the sequencer position and transforms them into note events. It is especially useful when capturing phrases from an external controller that is clocked by the sequencer timeline.

The recorder uses the midi-parser gem to parse raw MIDI bytes into MIDIEvents objects, then pairs note-on/note-off events to calculate durations and detect silences.

Examples:

Complete recording and transcription workflow

require 'musa-dsl'
require 'midi-communications'

sequencer = Musa::Sequencer::Sequencer.new(4, 24)
recorder = Musa::MIDIRecorder::MIDIRecorder.new(sequencer)

# Connect to MIDI input
input = MIDICommunications::Input.gets
input.on_message { |bytes| recorder.record(bytes) }

# Start sequencer and record...
# (play notes on MIDI controller)

# After recording, get transcription:
notes = recorder.transcription
# => [
#   { position: 1r, channel: 0, pitch: 60, velocity: 100, duration: 1/4r, velocity_off: 64 },
#   { position: 5/4r, channel: 0, pitch: :silence, duration: 1/8r },
#   { position: 11/8r, channel: 0, pitch: 62, velocity: 90, duration: 1/4r, velocity_off: 64 }
# ]

notes.each do |note|
  if note[:pitch] == :silence
    puts "Rest at #{note[:position]} for #{note[:duration]} bars"
  else
    puts "Note #{note[:pitch]} at #{note[:position]} for #{note[:duration]} bars"
  end
end

# Clear for next recording:
recorder.clear

See Also:

Instance Method Summary collapse

Constructor Details

#initialize(sequencer) ⇒ void

Parameters:



84
85
86
87
88
89
# File 'lib/musa-dsl/midi/midi-recorder.rb', line 84

def initialize(sequencer)
  @sequencer = sequencer
  @midi_parser = MIDIParser.new

  clear
end

Instance Method Details

#clearvoid

This method returns an undefined value.

Clears all stored events.



94
95
96
# File 'lib/musa-dsl/midi/midi-recorder.rb', line 94

def clear
  @messages = []
end

#rawArray<Message>

Returns unprocessed recorded messages.

Returns:

  • (Array<Message>)

    unprocessed recorded messages.



112
113
114
# File 'lib/musa-dsl/midi/midi-recorder.rb', line 112

def raw
  @messages
end

#record(midi_bytes) ⇒ void

This method returns an undefined value.

Records one MIDI packet.

Parameters:

  • midi_bytes (String, Array<Integer>)

    bytes as provided by the MIDI driver.



102
103
104
105
106
107
108
109
# File 'lib/musa-dsl/midi/midi-recorder.rb', line 102

def record(midi_bytes)
  m = @midi_parser.parse midi_bytes
  m = [m] unless m.is_a? Array

  m.each do |mm|
    @messages << Message.new(@sequencer.position, mm)
  end
end

#transcriptionArray<Hash>

Converts the message buffer into a list of note hashes.

Each note hash contains the keys :position, :channel, :pitch, :velocity and, when appropriate, :duration and :velocity_off. Silences (gaps between notes on the same channel) are expressed as pitch: :silence.

Both ways of releasing a note are read as a release: an explicit NoteOff and a NoteOn of velocity 0, which is the running-status form and what most keyboards and sequencers emit.

Examples:

Output format

[
  { position: Rational(0,1), channel: 0, pitch: 60, velocity: 100, duration: Rational(1,4), velocity_off: 64 },
  { position: Rational(1,4), channel: 0, pitch: :silence, duration: Rational(1,8) },
  { position: Rational(3,8), channel: 0, pitch: 62, velocity: 90, duration: Rational(1,4), velocity_off: 64 }
]

Returns:

  • (Array<Hash>)

    list of events suitable for Musa transcription pipelines.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/musa-dsl/midi/midi-recorder.rb', line 133

def transcription
  note_on = {}
  last_note = {}

  notes = []

  release = lambda do |mm, position|
    note_on[mm.channel] ||= {}

    note = note_on[mm.channel][mm.note]

    if note
      note_on[mm.channel].delete mm.note

      note[:duration] = position - note[:position]
      note[:velocity_off] = mm.velocity
    end

    last_note[mm.channel] = position
  end

  @messages.each do |m|
    mm = m.message

    case mm
    when MIDIEvents::NoteOn
      # A NoteOn of velocity 0 IS a NoteOff -- the running-status form, and
      # what most keyboards and sequencers actually emit. Taking it as a
      # note recorded a phantom note nobody played AND left the note it was
      # releasing without its :duration, since only a NoteOff closed one
      # (issue #89). Its velocity, 0, is the release velocity, which is all
      # the message says about how the note ended.
      if mm.velocity.zero?
        release.call(mm, m.position)
        next
      end

      if last_note[mm.channel]
        notes << { position: last_note[mm.channel], channel: mm.channel, pitch: :silence, duration: m.position - last_note[mm.channel] }
        last_note.delete mm.channel
      end

      note = { position: m.position, channel: mm.channel, pitch: mm.note, velocity: mm.velocity }

      note_on[mm.channel] ||= {}
      note_on[mm.channel][mm.note] = note

      notes << note

    when MIDIEvents::NoteOff
      release.call(mm, m.position)
    end
  end

  notes
end