Class: Musa::MIDIVoices::MIDIVoices

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

Overview

Note:

All durations are expressed as Rational numbers representing bars.

Note:

MIDI channels are zero-indexed (0-15), not 1-16.

High level helpers to drive one or more MIDI channels from a Sequencer::Sequencer.

A voice represents the state of a given MIDI channel (active notes, controllers, sustain pedal, etc.). MIDIVoices ties the life‑cycle of those voices to the sequencer clock so that note durations, waits and callbacks stay in the musical timeline even when running in fast-forward or quantized sessions.

Typical usage:

Examples:

Basic setup and playback

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

clock     = Musa::Clock::TimerClock.new bpm: 120
transport = Musa::Transport::Transport.new clock
output    = MIDICommunications::Output.all.first

voices = Musa::MIDIVoices::MIDIVoices.new(
  sequencer: transport.sequencer,
  output:    output,
  channels:  [0, 1] # also accepts ranges such as 0..7
)

voices.voices.first.note pitch: 64, velocity: 90, duration: 1r / 4

Playing chords

voice = voices.voices.first
voice.note pitch: [60, 64, 67], velocity: 90, duration: 1r

# A velocity per pitch is allowed, and they line up by position:
voice.note pitch: [60, 62, 64], velocity: [80, 90, 100], duration: 1r/4
# the three note-ons carry velocities 80, 90 and 100

A silence sends nothing at all

# A stand-in for a MIDI output: it just records what it is sent.
output = Class.new { def initialize = @sent = []
                     def puts(*m) = @sent.concat(m)
                     def sent = @sent.map(&:to_a)
                     def size = @sent.size }.new

sequencer = Musa::Sequencer::BaseSequencer.new(4, 24)
voices = Musa::MIDIVoices::MIDIVoices.new(sequencer: sequencer,
                                          output: output, channels: [0])
voice = voices.voices.first

voice.note pitch: :silence, duration: 1r/4
output.sent  # => []

# Not a note-on with velocity 0 -- nothing at all. The rest occupies its
# time in the sequencer and leaves the wire alone.

Using note controls with callbacks

voice = voices.voices.first
note_ctrl = voice.note pitch: 60, duration: nil  # indefinite
note_ctrl.on_stop { puts "Note ended!" }
# ... later:
note_ctrl.note_off

Fast-forward for silent catch-up

voices.fast_forward = true
# ... replay past events ...
voices.fast_forward = false  # resumes audible output

See Also:

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

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

Builds the voice container for one or many MIDI channels.

Parameters:

  • sequencer (Musa::Sequencer::Sequencer)

    sequencer that schedules waits and callbacks.

  • output (#puts, nil)

    anything responding to puts that accepts MIDIEvents::Events (typically a MIDICommunications output).

  • channels (Array<Numeric>, Range, Numeric)

    list of MIDI channels to control. Ranges are expanded automatically.

  • do_log (Boolean) (defaults to: nil)

    enables info level logs per emitted message.



137
138
139
140
141
142
143
144
145
146
# File 'lib/musa-dsl/midi/midi-voices.rb', line 137

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

  @sequencer = sequencer
  @output = output
  @channels = channels.arrayfy.explode_ranges
  @do_log = do_log

  reset
end

Instance Attribute Details

#do_logBoolean

Returns whether verbose logging is enabled.

Returns:

  • (Boolean)

    whether verbose logging is enabled.



127
128
129
# File 'lib/musa-dsl/midi/midi-voices.rb', line 127

def do_log
  @do_log
end

#voicesArray<MIDIVoice> (readonly)

Returns read-only list of per-channel voices.

Returns:



157
158
159
# File 'lib/musa-dsl/midi/midi-voices.rb', line 157

def voices
  @voices
end

Instance Method Details

#fast_forward=(enabled) ⇒ void

This method returns an undefined value.

Enables or disables the fast-forward mode on every voice.

When enabled, notes are registered internally but their MIDI messages are not emitted, allowing the sequencer to catch up silently (e.g. when loading a snapshot).

Parameters:

  • enabled (Boolean)

    true to enable fast-forward, false to disable.



167
168
169
# File 'lib/musa-dsl/midi/midi-voices.rb', line 167

def fast_forward=(enabled)
  @voices.each { |voice| voice.fast_forward = enabled }
end

#panic(reset: nil) ⇒ Object

sequencer = Musa::Sequencer::BaseSequencer.new(4, 24) voices = Musa::MIDIVoices::MIDIVoices.new(sequencer: sequencer, output: output, channels: [0]) voice = voices.voices.first

voices.panic
output.sent  # => [[176, 123, 0]]

# CC 123 -- all notes off -- once per channel. With `reset: true` an
# FF system-reset follows it:
voices.panic(reset: true)
output.sent  # => [[176, 123, 0], [176, 123, 0], [255]]


195
196
197
198
199
200
201
# File 'lib/musa-dsl/midi/midi-voices.rb', line 195

def panic(reset: nil)
  reset ||= false

  @voices.each(&:all_notes_off)

  @output.puts MIDIEvents::SystemRealtime.new(0xff) if reset
end

#resetvoid

This method returns an undefined value.

Resets the collection recreating every Musa::MIDIVoices::MIDIVoice. Useful when the MIDI output has changed or after a panic.



152
153
154
# File 'lib/musa-dsl/midi/midi-voices.rb', line 152

def reset
  @voices = @channels.collect { |channel| MIDIVoice.new(sequencer: @sequencer, output: @output, channel: channel, do_log: @do_log) }.freeze
end