Class: Musa::MIDIVoices::MIDIVoice Private

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

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

Individual MIDI channel voice with sequencer-synchronized note management.

Manages the state of a single MIDI channel including active notes, controller values, and sustain pedal. All note scheduling is tied to the sequencer clock, ensuring proper timing in fast-forward mode or during quantized playback.

Supports indefinite notes (manual note-off), automatic note-off scheduling, callbacks on note stop, and fast-forward mode for silent state updates.

Examples:

Playing notes

sent_before = output.size

voice.note pitch: 60, velocity: 90, duration: 1r/4
voice.note pitch: [60, 64, 67], velocity: 100, duration: 1r  # chord

# Four note-ons: the single note and the three of the chord.
output.size - sent_before  # => 4

Notes released by hand

# No duration: nothing is scheduled to end it, and it sounds until the
# control is asked to release it. A pedal tone, a drone, anything whose
# end depends on something other than a length decided in advance.
note_ctrl = voice.note pitch: 62, velocity: 90, duration: nil
ended = false
note_ctrl.on_stop { ended = true }
# ... later:
note_ctrl.note_off
ended  # => true

A note released before its scheduled end

# The control that `note` returns releases it whenever you want, and the
# note-off it had scheduled arrives to find the note already over: it
# does nothing, and on_stop fires once.
note_ctrl = voice.note pitch: 65, velocity: 90, duration: 100r
stops = 0
note_ctrl.on_stop { stops += 1 }
note_ctrl.note_off
note_ctrl.active?  # => false
stops              # => 1

A note whose pitch someone else is still holding

# A note has stopped sounding when its pitch has, and a pitch stops when
# the last note holding it lets go. Pitch 67 is still held by the chord
# above, so this one is still sounding after it has been released.
doubling = voice.note pitch: 67, velocity: 90, duration: 100r
doubling.note_off
doubling.active?  # => true

Controller and sustain pedal

voice.controller[:mod_wheel] = 64
voice.sustain_pedal = 127
voice.controller[:expression] = 100

See Also:

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(sequencer:, output:, channel:, name: nil, do_log: nil) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parameters:

  • sequencer (Musa::Sequencer::Sequencer)
  • output (#puts, nil)
  • channel (Integer)

    MIDI channel number (0-15).

  • name (String, nil) (defaults to: nil)

    human friendly identifier.



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/musa-dsl/midi/midi-voices.rb', line 248

def initialize(sequencer:, output:, channel:, name: nil, do_log: nil)
  do_log ||= false

  @sequencer = sequencer
  @output = output
  @channel = channel
  @name = name
  @do_log = do_log

  @tick_duration = Rational(1, @sequencer.ticks_per_bar)

  @controllers_control = ControllersControl.new(@output, @channel)

  @active_pitches = []
  fill_active_pitches @active_pitches

  @sequencer.logger.warn 'voice without output' unless @output

  self
end

Instance Attribute Details

#active_pitchesArray<Hash> (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns metadata for each of the 128 MIDI pitches. Mainly used internally.

Returns:

  • (Array<Hash>)

    metadata for each of the 128 MIDI pitches. Mainly used internally.



237
238
239
# File 'lib/musa-dsl/midi/midi-voices.rb', line 237

def active_pitches
  @active_pitches
end

#channelInteger (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns MIDI channel number (0-15).

Returns:

  • (Integer)

    MIDI channel number (0-15).



234
235
236
# File 'lib/musa-dsl/midi/midi-voices.rb', line 234

def channel
  @channel
end

#do_logBoolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns whether this voice logs every emitted message.

Returns:

  • (Boolean)

    whether this voice logs every emitted message.



225
226
227
# File 'lib/musa-dsl/midi/midi-voices.rb', line 225

def do_log
  @do_log
end

#nameString?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns optional name used in log messages.

Returns:

  • (String, nil)

    optional name used in log messages.



222
223
224
# File 'lib/musa-dsl/midi/midi-voices.rb', line 222

def name
  @name
end

#output#puts? (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns MIDI destination. When nil the voice becomes silent.

Returns:

  • (#puts, nil)

    MIDI destination. When nil the voice becomes silent.



231
232
233
# File 'lib/musa-dsl/midi/midi-voices.rb', line 231

def output
  @output
end

#sequencerMusa::Sequencer::Sequencer (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns sequencer driving this voice.

Returns:



228
229
230
# File 'lib/musa-dsl/midi/midi-voices.rb', line 228

def sequencer
  @sequencer
end

#tick_durationRational (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns duration (in bars) of a sequencer tick; used to schedule note offs.

Returns:

  • (Rational)

    duration (in bars) of a sequencer tick; used to schedule note offs.



240
241
242
# File 'lib/musa-dsl/midi/midi-voices.rb', line 240

def tick_duration
  @tick_duration
end

Instance Method Details

#all_notes_offvoid

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Sends immediate note-off messages for all active pitches and an all-notes-off message on this channel and resets internal state.



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/musa-dsl/midi/midi-voices.rb', line 349

def all_notes_off
  @output.puts MIDIEvents::ChannelMessage.new(0xb, @channel, 0x7b, 0)

  @active_pitches.each_with_index do |pitch_status, pitch|
    # Everything goes quiet here, including the notes that had let go of a
    # pitch someone else was holding and were waiting for it to fall to
    # zero. Clearing the table without telling them would leave them
    # sounding for ever as far as they know, and their callbacks unrun.
    waiting = pitch_status[:awaiting_silence]
    pitch_status[:awaiting_silence] = []
    waiting.each(&:_pitch_silenced)

    next if @fast_forward || pitch_status[:note_controls].empty?

    msg = MIDIEvents::NoteOff.new(@channel, pitch, 0)
    @output&.puts msg
  end

  @active_pitches.clear
  fill_active_pitches @active_pitches
end

#controllerControllersControl

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns MIDI CC manager for this voice.

Returns:

  • (ControllersControl)

    MIDI CC manager for this voice.



330
331
332
# File 'lib/musa-dsl/midi/midi-voices.rb', line 330

def controller
  @controllers_control
end

#fast_forward=(enabled) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Turns fast-forward on/off for this voice.

When disabling it, pending notes that were held silently are sent again so the synth is in sync with the sequencer state.

Parameters:

  • enabled (Boolean)

    true to enable fast-forward, false to disable.



276
277
278
279
280
281
282
283
284
# File 'lib/musa-dsl/midi/midi-voices.rb', line 276

def fast_forward=(enabled)
  if @fast_forward && !enabled
    (0..127).each do |pitch|
      @output.puts MIDIEvents::NoteOn.new(@channel, pitch, @active_pitches[pitch][:velocity]) unless @active_pitches[pitch][:note_controls].empty?
    end
  end

  @fast_forward = enabled
end

#fast_forward?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns true when in fast-forward mode (notes registered but not emitted).

Returns:

  • (Boolean)

    true when in fast-forward mode (notes registered but not emitted).



287
288
289
# File 'lib/musa-dsl/midi/midi-voices.rb', line 287

def fast_forward?
  @fast_forward
end

#log(msg) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Logs a message tagging the current voice.

Parameters:

  • msg (String)

    the message to log.



375
376
377
# File 'lib/musa-dsl/midi/midi-voices.rb', line 375

def log(msg)
  @sequencer.logger.info('MIDIVoice') { "voice #{name || @channel}: #{msg}" } if @do_log
end

#note(pitchvalue = nil, pitch: nil, velocity: nil, duration: nil, duration_offset: nil, note_duration: nil, velocity_off: nil) ⇒ NoteControl?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Plays one or several MIDI notes.

Parameters:

  • pitchvalue (Numeric, Array<Numeric>, nil) (defaults to: nil)

    optional shorthand for pitch.

  • pitch (Numeric, Symbol, Array<Numeric, Symbol>) (defaults to: nil)

    MIDI note numbers or :silence. Arrays/ranges expand to multiple notes.

  • velocity (Numeric, Array<Numeric>) (defaults to: nil)

    raw velocity (0-127). Defaults to 63.

  • duration (Numeric, nil) (defaults to: nil)

    musical duration in bars, or nil for a note that stays on until Musa::MIDIVoices::MIDIVoice::NoteControl#note_off is called by hand. Nothing is scheduled to end a note with no duration, so releasing it is the caller's job; the voice logs one at debug level, since a duration left out by mistake looks exactly like one left out on purpose.

  • duration_offset (Numeric) (defaults to: nil)

    offset applied when scheduling the note-off inside the sequencer.

  • note_duration (Numeric, nil) (defaults to: nil)

    alternative duration in bars for legato control.

  • velocity_off (Numeric, Array<Numeric>) (defaults to: nil)

    release velocity (defaults to 63).

Returns:

  • (NoteControl, nil)

    handler that can be used to attach callbacks.



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/musa-dsl/midi/midi-voices.rb', line 306

def note(pitchvalue = nil, pitch: nil, velocity: nil, duration: nil, duration_offset: nil, note_duration: nil, velocity_off: nil)
  pitch ||= pitchvalue

  if pitch
    velocity ||= 63

    duration_offset ||= -@tick_duration

    # Only when there is a duration to compute it from. Computing it
    # unconditionally raised on the nil that means "no end of its own",
    # which NoteControl one layer down goes out of its way to accept, so
    # the indefinite note could be implemented but not asked for
    # (issue #81). The keyword's own default value was unusable.
    note_duration ||= [0, duration + duration_offset].max if duration

    velocity_off ||= 63

    log "note #{pitch} has no duration: it will sound until note_off" unless note_duration

    NoteControl.new(self, pitch: pitch, velocity: velocity, duration: note_duration, velocity_off: velocity_off).note_on
  end
end

#sustain_pedalInteger?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns current sustain pedal value.

Returns:

  • (Integer, nil)

    current sustain pedal value.



342
343
344
# File 'lib/musa-dsl/midi/midi-voices.rb', line 342

def sustain_pedal
  @controllers_control[:sustain_pedal]
end

#sustain_pedal=(value) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Sets the sustain pedal state.

Parameters:

  • value (Integer)

    pedal value (0-127, typically 0 or 127).



337
338
339
# File 'lib/musa-dsl/midi/midi-voices.rb', line 337

def sustain_pedal=(value)
  @controllers_control[:sustain_pedal] = value
end

#to_sString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns human-readable voice description.

Returns:

  • (String)

    human-readable voice description.



380
381
382
# File 'lib/musa-dsl/midi/midi-voices.rb', line 380

def to_s
  "voice #{@name} output: #{@output} channel: #{@channel}"
end