musa-dsl 0.30.2 → 0.40.0

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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/README.md +227 -6
  6. data/docs/README.md +83 -0
  7. data/docs/api-reference.md +86 -0
  8. data/docs/getting-started/quick-start.md +93 -0
  9. data/docs/getting-started/tutorial.md +58 -0
  10. data/docs/subsystems/core-extensions.md +316 -0
  11. data/docs/subsystems/datasets.md +465 -0
  12. data/docs/subsystems/generative.md +290 -0
  13. data/docs/subsystems/matrix.md +63 -0
  14. data/docs/subsystems/midi.md +123 -0
  15. data/docs/subsystems/music.md +233 -0
  16. data/docs/subsystems/musicxml-builder.md +264 -0
  17. data/docs/subsystems/neumas.md +71 -0
  18. data/docs/subsystems/repl.md +135 -0
  19. data/docs/subsystems/sequencer.md +98 -0
  20. data/docs/subsystems/series.md +302 -0
  21. data/docs/subsystems/transcription.md +152 -0
  22. data/docs/subsystems/transport.md +177 -0
  23. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  24. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  25. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  26. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  27. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  28. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  29. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  30. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  31. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  32. data/lib/musa-dsl/core-ext/with.rb +114 -0
  33. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  34. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  35. data/lib/musa-dsl/datasets/e.rb +186 -2
  36. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  37. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  38. data/lib/musa-dsl/datasets/helper.rb +75 -0
  39. data/lib/musa-dsl/datasets/p.rb +177 -2
  40. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  41. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  42. data/lib/musa-dsl/datasets/ps.rb +134 -4
  43. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  44. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  45. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  48. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  49. data/lib/musa-dsl/datasets/score.rb +279 -0
  50. data/lib/musa-dsl/datasets/v.rb +88 -0
  51. data/lib/musa-dsl/generative/darwin.rb +180 -1
  52. data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
  53. data/lib/musa-dsl/generative/markov.rb +133 -3
  54. data/lib/musa-dsl/generative/rules.rb +258 -4
  55. data/lib/musa-dsl/generative/variatio.rb +217 -2
  56. data/lib/musa-dsl/logger/logger.rb +267 -2
  57. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  58. data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
  59. data/lib/musa-dsl/midi/midi-voices.rb +265 -4
  60. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  61. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  62. data/lib/musa-dsl/music/chords.rb +308 -2
  63. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
  64. data/lib/musa-dsl/music/scales.rb +957 -40
  65. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  66. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  67. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  68. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  69. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  70. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  71. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  72. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  73. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  74. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  75. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  76. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  77. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  78. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  79. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  80. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  81. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  82. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  83. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  84. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  85. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  86. data/lib/musa-dsl/repl/repl.rb +550 -0
  87. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  88. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  89. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  90. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  91. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  92. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  93. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  94. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  95. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  96. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  97. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  98. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  99. data/lib/musa-dsl/series/base-series.rb +843 -5
  100. data/lib/musa-dsl/series/buffer-serie.rb +48 -0
  101. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
  102. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  103. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  104. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  105. data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
  106. data/lib/musa-dsl/series/queue-serie.rb +65 -0
  107. data/lib/musa-dsl/series/series-composer.rb +701 -0
  108. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  109. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  110. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  111. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  112. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  113. data/lib/musa-dsl/transport/clock.rb +125 -0
  114. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  115. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  116. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  117. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  118. data/lib/musa-dsl/transport/timer.rb +83 -0
  119. data/lib/musa-dsl/transport/transport.rb +318 -0
  120. data/lib/musa-dsl/version.rb +1 -1
  121. data/lib/musa-dsl.rb +132 -25
  122. data/musa-dsl.gemspec +12 -10
  123. metadata +87 -8
@@ -1,8 +1,84 @@
1
1
  require 'midi-parser'
2
2
 
3
3
  module Musa
4
+ # MIDI event recording and transcription utilities.
5
+ #
6
+ # Provides tools for capturing raw MIDI bytes alongside sequencer position
7
+ # timestamps and converting them into structured note events. Useful for
8
+ # recording phrases from external MIDI controllers synchronized with the
9
+ # sequencer timeline.
10
+ #
11
+ # The captured events can be transcribed into hashes suitable for Musa's
12
+ # transcription pipelines, including note on/off pairing, duration calculation,
13
+ # and silence detection.
14
+ #
15
+ # @example Basic recording workflow
16
+ # require 'musa-dsl'
17
+ # require 'midi-communications'
18
+ #
19
+ # sequencer = Musa::Sequencer::Sequencer.new(4, 24)
20
+ # recorder = Musa::MIDIRecorder::MIDIRecorder.new(sequencer)
21
+ #
22
+ # # Capture MIDI from controller
23
+ # input = MIDICommunications::Input.gets
24
+ # input.on_message { |bytes| recorder.record(bytes) }
25
+ #
26
+ # # After recording, get structured notes
27
+ # notes = recorder.transcription
28
+ # notes.each { |n| puts "#{n[:pitch]} at #{n[:position]} for #{n[:duration]}" }
29
+ #
30
+ # @see MIDIRecorder Main recorder class
31
+ # @see Musa::Sequencer::Sequencer Sequencer providing timeline context
32
+ # @see https://github.com/arirusso/midi-communications midi-communications gem
33
+ # @see https://github.com/javier-sy/midi-parser midi-parser gem
34
+ # @see https://github.com/javier-sy/midi-events midi-events gem
4
35
  module MIDIRecorder
36
+ # Collects raw MIDI bytes alongside the sequencer position and transforms
37
+ # them into note events. It is especially useful when capturing phrases from
38
+ # an external controller that is clocked by the sequencer timeline.
39
+ #
40
+ # The recorder uses the midi-parser gem to parse raw MIDI bytes into
41
+ # MIDIEvents objects, then pairs note-on/note-off events to calculate
42
+ # durations and detect silences.
43
+ #
44
+ # @example Complete recording and transcription workflow
45
+ # require 'musa-dsl'
46
+ # require 'midi-communications'
47
+ #
48
+ # sequencer = Musa::Sequencer::Sequencer.new(4, 24)
49
+ # recorder = Musa::MIDIRecorder::MIDIRecorder.new(sequencer)
50
+ #
51
+ # # Connect to MIDI input
52
+ # input = MIDICommunications::Input.gets
53
+ # input.on_message { |bytes| recorder.record(bytes) }
54
+ #
55
+ # # Start sequencer and record...
56
+ # # (play notes on MIDI controller)
57
+ #
58
+ # # After recording, get transcription:
59
+ # notes = recorder.transcription
60
+ # # => [
61
+ # # { position: 1r, channel: 0, pitch: 60, velocity: 100, duration: 1/4r, velocity_off: 64 },
62
+ # # { position: 5/4r, channel: 0, pitch: :silence, duration: 1/8r },
63
+ # # { position: 11/8r, channel: 0, pitch: 62, velocity: 90, duration: 1/4r, velocity_off: 64 }
64
+ # # ]
65
+ #
66
+ # notes.each do |note|
67
+ # if note[:pitch] == :silence
68
+ # puts "Rest at #{note[:position]} for #{note[:duration]} bars"
69
+ # else
70
+ # puts "Note #{note[:pitch]} at #{note[:position]} for #{note[:duration]} bars"
71
+ # end
72
+ # end
73
+ #
74
+ # # Clear for next recording:
75
+ # recorder.clear
76
+ #
77
+ # @see Musa::Sequencer::Sequencer
78
+ # @see https://github.com/javier-sy/midi-parser MIDIParser documentation
79
+ # @see https://github.com/javier-sy/midi-events MIDIEvents documentation
5
80
  class MIDIRecorder
81
+ # @param sequencer [Musa::Sequencer::Sequencer] provides the musical position for each recorded message.
6
82
  def initialize(sequencer)
7
83
  @sequencer = sequencer
8
84
  @midi_parser = MIDIParser.new
@@ -10,10 +86,17 @@ module Musa
10
86
  clear
11
87
  end
12
88
 
89
+ # Clears all stored events.
90
+ #
91
+ # @return [void]
13
92
  def clear
14
93
  @messages = []
15
94
  end
16
95
 
96
+ # Records one MIDI packet.
97
+ #
98
+ # @param midi_bytes [String, Array<Integer>] bytes as provided by the MIDI driver.
99
+ # @return [void]
17
100
  def record(midi_bytes)
18
101
  m = @midi_parser.parse midi_bytes
19
102
  m = [m] unless m.is_a? Array
@@ -23,10 +106,24 @@ module Musa
23
106
  end
24
107
  end
25
108
 
109
+ # @return [Array<Message>] unprocessed recorded messages.
26
110
  def raw
27
111
  @messages
28
112
  end
29
113
 
114
+ # Converts the message buffer into a list of note hashes.
115
+ #
116
+ # Each note hash contains the keys :position, :channel, :pitch, :velocity
117
+ # and, when appropriate, :duration and :velocity_off. Silences (gaps between
118
+ # notes on the same channel) are expressed as `pitch: :silence`.
119
+ #
120
+ # @return [Array<Hash>] list of events suitable for Musa transcription pipelines.
121
+ # @example Output format
122
+ # [
123
+ # { position: Rational(0,1), channel: 0, pitch: 60, velocity: 100, duration: Rational(1,4), velocity_off: 64 },
124
+ # { position: Rational(1,4), channel: 0, pitch: :silence, duration: Rational(1,8) },
125
+ # { position: Rational(3,8), channel: 0, pitch: 62, velocity: 90, duration: Rational(1,4), velocity_off: 64 }
126
+ # ]
30
127
  def transcription
31
128
  note_on = {}
32
129
  last_note = {}
@@ -69,9 +166,19 @@ module Musa
69
166
  notes
70
167
  end
71
168
 
169
+ # Internal representation of a captured MIDI message linked to its sequencer position.
170
+ # @api private
72
171
  class Message
73
- attr_accessor :position, :message
172
+ # @return [Rational] sequencer position where the message was captured.
173
+ attr_reader :position
74
174
 
175
+ # @return [MIDIEvents::Event] parsed MIDI event.
176
+ attr_reader :message
177
+
178
+ # Creates a new message entry.
179
+ #
180
+ # @param position [Rational] sequencer position when captured.
181
+ # @param message [MIDIEvents::Event] parsed MIDI event.
75
182
  def initialize(position, message)
76
183
  @position = position
77
184
  @message = message
@@ -5,13 +5,98 @@ require_relative '../core-ext/arrayfy'
5
5
  require_relative '../core-ext/array-explode-ranges'
6
6
 
7
7
  module Musa
8
+ # High-level MIDI channel management synchronized with sequencer timeline.
9
+ #
10
+ # Provides voice abstraction for controlling MIDI channels with sequencer-aware
11
+ # note scheduling, duration tracking, sustain pedal management, and fast-forward
12
+ # support for silent timeline catch-up.
13
+ #
14
+ # Each voice represents the state of a MIDI channel (active notes, controllers,
15
+ # sustain pedal) and ties all musical events to the sequencer clock. This ensures
16
+ # correct timing even when running in fast-forward mode or with quantization.
17
+ #
18
+ # @example Basic voice setup
19
+ # require 'musa-dsl'
20
+ # require 'midi-communications'
21
+ #
22
+ # clock = Musa::Clock::TimerClock.new bpm: 120
23
+ # transport = Musa::Transport::Transport.new clock
24
+ # output = MIDICommunications::Output.all.first
25
+ #
26
+ # voices = Musa::MIDIVoices::MIDIVoices.new(
27
+ # sequencer: transport.sequencer,
28
+ # output: output,
29
+ # channels: 0..3
30
+ # )
31
+ #
32
+ # voice = voices.voices.first
33
+ # voice.note pitch: 60, velocity: 90, duration: 1r/4
34
+ #
35
+ # @see MIDIVoices Voice collection manager
36
+ # @see Musa::Sequencer::Sequencer Timeline and scheduling
37
+ # @see https://github.com/arirusso/midi-communications midi-communications gem
38
+ # @see https://github.com/javier-sy/midi-events midi-events gem
8
39
  module MIDIVoices
9
40
  using Musa::Extension::Arrayfy
10
41
  using Musa::Extension::ExplodeRanges
11
42
 
43
+ # High level helpers to drive one or more MIDI channels from a {Musa::Sequencer::Sequencer}.
44
+ #
45
+ # A *voice* represents the state of a given MIDI channel (active notes,
46
+ # controllers, sustain pedal, etc.). {MIDIVoices} ties the life‑cycle of those
47
+ # voices to the sequencer clock so that note durations, waits and callbacks
48
+ # stay in the musical timeline even when running in fast-forward or quantized
49
+ # sessions.
50
+ #
51
+ # Typical usage:
52
+ #
53
+ # @example Basic setup and playback
54
+ # require 'musa-dsl'
55
+ # require 'midi-communications'
56
+ #
57
+ # clock = Musa::Clock::TimerClock.new bpm: 120
58
+ # transport = Musa::Transport::Transport.new clock
59
+ # output = MIDICommunications::Output.all.first
60
+ #
61
+ # voices = Musa::MIDIVoices::MIDIVoices.new(
62
+ # sequencer: transport.sequencer,
63
+ # output: output,
64
+ # channels: [0, 1] # also accepts ranges such as 0..7
65
+ # )
66
+ #
67
+ # voices.voices.first.note pitch: 64, velocity: 90, duration: 1r / 4
68
+ #
69
+ # @example Playing chords
70
+ # voice = voices.voices.first
71
+ # voice.note pitch: [60, 64, 67], velocity: 90, duration: 1r
72
+ #
73
+ # @example Using note controls with callbacks
74
+ # voice = voices.voices.first
75
+ # note_ctrl = voice.note pitch: 60, duration: nil # indefinite
76
+ # note_ctrl.on_stop { puts "Note ended!" }
77
+ # # ... later:
78
+ # note_ctrl.note_off
79
+ #
80
+ # @example Fast-forward for silent catch-up
81
+ # voices.fast_forward = true
82
+ # # ... replay past events ...
83
+ # voices.fast_forward = false # resumes audible output
84
+ #
85
+ # @see Musa::Sequencer::Sequencer
86
+ # @see https://github.com/arirusso/midi-communications MIDICommunications documentation
87
+ # @see https://github.com/javier-sy/midi-events MIDIEvents documentation
88
+ # @note All durations are expressed as Rational numbers representing bars.
89
+ # @note MIDI channels are zero-indexed (0-15), not 1-16.
12
90
  class MIDIVoices
91
+ # @return [Boolean] whether verbose logging is enabled.
13
92
  attr_accessor :do_log
14
93
 
94
+ # Builds the voice container for one or many MIDI channels.
95
+ #
96
+ # @param sequencer [Musa::Sequencer::Sequencer] sequencer that schedules waits and callbacks.
97
+ # @param output [#puts, nil] anything responding to `puts` that accepts `MIDIEvents::Event`s (typically a MIDICommunications output).
98
+ # @param channels [Array<Numeric>, Range, Numeric] list of MIDI channels to control. Ranges are expanded automatically.
99
+ # @param do_log [Boolean] enables info level logs per emitted message.
15
100
  def initialize(sequencer:, output:, channels:, do_log: nil)
16
101
  do_log ||= false
17
102
 
@@ -23,16 +108,33 @@ module Musa
23
108
  reset
24
109
  end
25
110
 
111
+ # Resets the collection recreating every {MIDIVoice}. Useful when the MIDI
112
+ # output has changed or after a panic.
113
+ #
114
+ # @return [void]
26
115
  def reset
27
116
  @voices = @channels.collect { |channel| MIDIVoice.new(sequencer: @sequencer, output: @output, channel: channel, do_log: @do_log) }.freeze
28
117
  end
29
118
 
119
+ # @return [Array<MIDIVoice>] read-only list of per-channel voices.
30
120
  attr_reader :voices
31
121
 
122
+ # Enables or disables the fast-forward mode on every voice.
123
+ #
124
+ # When enabled, notes are registered internally but their MIDI messages are
125
+ # not emitted, allowing the sequencer to catch up silently (e.g. when
126
+ # loading a snapshot).
127
+ #
128
+ # @param enabled [Boolean] true to enable fast-forward, false to disable.
129
+ # @return [void]
32
130
  def fast_forward=(enabled)
33
131
  @voices.each { |voice| voice.fast_forward = enabled }
34
132
  end
35
133
 
134
+ # Sends all-notes-off on every channel and (optionally) a MIDI reset.
135
+ #
136
+ # @param reset [Boolean] whether to emit an FF SystemRealtime (panic) message.
137
+ # @return [void]
36
138
  def panic(reset: nil)
37
139
  reset ||= false
38
140
 
@@ -44,10 +146,60 @@ module Musa
44
146
 
45
147
  private
46
148
 
149
+ # Individual MIDI channel voice with sequencer-synchronized note management.
150
+ #
151
+ # Manages the state of a single MIDI channel including active notes, controller
152
+ # values, and sustain pedal. All note scheduling is tied to the sequencer clock,
153
+ # ensuring proper timing in fast-forward mode or during quantized playback.
154
+ #
155
+ # Supports indefinite notes (manual note-off), automatic note-off scheduling,
156
+ # callbacks on note stop, and fast-forward mode for silent state updates.
157
+ #
158
+ # @example Playing notes
159
+ # voice = voices.voices.first
160
+ # voice.note pitch: 60, velocity: 90, duration: 1r/4
161
+ # voice.note pitch: [60, 64, 67], velocity: 100, duration: 1r # chord
162
+ #
163
+ # @example Indefinite notes with manual control
164
+ # note_ctrl = voice.note pitch: 60, duration: nil
165
+ # note_ctrl.on_stop { puts "Note ended!" }
166
+ # # ... later:
167
+ # note_ctrl.note_off
168
+ #
169
+ # @example Controller and sustain pedal
170
+ # voice.controller[:mod_wheel] = 64
171
+ # voice.sustain_pedal = 127
172
+ # voice.controller[:expression] = 100
173
+ #
174
+ # @see MIDIVoices Parent voice collection
175
+ # @see NoteControl Note lifecycle controller
176
+ # @api private
47
177
  class MIDIVoice
48
- attr_accessor :name, :do_log
49
- attr_reader :sequencer, :output, :channel, :active_pitches, :tick_duration
178
+ # @return [String, nil] optional name used in log messages.
179
+ attr_accessor :name
50
180
 
181
+ # @return [Boolean] whether this voice logs every emitted message.
182
+ attr_accessor :do_log
183
+
184
+ # @return [Musa::Sequencer::Sequencer] sequencer driving this voice.
185
+ attr_reader :sequencer
186
+
187
+ # @return [#puts, nil] MIDI destination. When nil the voice becomes silent.
188
+ attr_reader :output
189
+
190
+ # @return [Integer] MIDI channel number (0-15).
191
+ attr_reader :channel
192
+
193
+ # @return [Array<Hash>] metadata for each of the 128 MIDI pitches. Mainly used internally.
194
+ attr_reader :active_pitches
195
+
196
+ # @return [Rational] duration (in bars) of a sequencer tick; used to schedule note offs.
197
+ attr_reader :tick_duration
198
+
199
+ # @param sequencer [Musa::Sequencer::Sequencer]
200
+ # @param output [#puts, nil]
201
+ # @param channel [Integer] MIDI channel number (0-15).
202
+ # @param name [String, nil] human friendly identifier.
51
203
  def initialize(sequencer:, output:, channel:, name: nil, do_log: nil)
52
204
  do_log ||= false
53
205
 
@@ -69,6 +221,13 @@ module Musa
69
221
  self
70
222
  end
71
223
 
224
+ # Turns fast-forward on/off for this voice.
225
+ #
226
+ # When disabling it, pending notes that were held silently are sent again
227
+ # so the synth is in sync with the sequencer state.
228
+ #
229
+ # @param enabled [Boolean] true to enable fast-forward, false to disable.
230
+ # @return [void]
72
231
  def fast_forward=(enabled)
73
232
  if @fast_forward && !enabled
74
233
  (0..127).each do |pitch|
@@ -79,10 +238,21 @@ module Musa
79
238
  @fast_forward = enabled
80
239
  end
81
240
 
241
+ # @return [Boolean] true when in fast-forward mode (notes registered but not emitted).
82
242
  def fast_forward?
83
243
  @fast_forward
84
244
  end
85
245
 
246
+ # Plays one or several MIDI notes.
247
+ #
248
+ # @param pitchvalue [Numeric, Array<Numeric>, nil] optional shorthand for +pitch+.
249
+ # @param pitch [Numeric, Symbol, Array<Numeric, Symbol>] MIDI note numbers or :silence. Arrays/ranges expand to multiple notes.
250
+ # @param velocity [Numeric, Array<Numeric>] raw velocity (0-127). Defaults to 63.
251
+ # @param duration [Numeric, nil] musical duration in bars. When nil the note stays on until {NoteControl#note_off} is called manually.
252
+ # @param duration_offset [Numeric] offset applied when scheduling the note-off inside the sequencer.
253
+ # @param note_duration [Numeric, nil] alternative duration in bars for legato control.
254
+ # @param velocity_off [Numeric, Array<Numeric>] release velocity (defaults to 63).
255
+ # @return [NoteControl, nil] handler that can be used to attach callbacks.
86
256
  def note(pitchvalue = nil, pitch: nil, velocity: nil, duration: nil, duration_offset: nil, note_duration: nil, velocity_off: nil)
87
257
  pitch ||= pitchvalue
88
258
 
@@ -98,18 +268,26 @@ module Musa
98
268
  end
99
269
  end
100
270
 
271
+ # @return [ControllersControl] MIDI CC manager for this voice.
101
272
  def controller
102
273
  @controllers_control
103
274
  end
104
275
 
276
+ # Sets the sustain pedal state.
277
+ #
278
+ # @param value [Integer] pedal value (0-127, typically 0 or 127).
105
279
  def sustain_pedal=(value)
106
280
  @controllers_control[:sustain_pedal] = value
107
281
  end
108
282
 
283
+ # @return [Integer, nil] current sustain pedal value.
109
284
  def sustain_pedal
110
285
  @controllers_control[:sustain_pedal]
111
286
  end
112
287
 
288
+ # Sends an immediate all-notes-off message on this channel and resets internal state.
289
+ #
290
+ # @return [void]
113
291
  def all_notes_off
114
292
  @active_pitches.clear
115
293
  fill_active_pitches @active_pitches
@@ -117,10 +295,15 @@ module Musa
117
295
  @output.puts MIDIEvents::ChannelMessage.new(0xb, @channel, 0x7b, 0)
118
296
  end
119
297
 
298
+ # Logs a message tagging the current voice.
299
+ #
300
+ # @param msg [String] the message to log.
301
+ # @return [void]
120
302
  def log(msg)
121
303
  @sequencer.logger.info('MIDIVoice') { "voice #{name || @channel}: #{msg}" } if @do_log
122
304
  end
123
305
 
306
+ # @return [String] human-readable voice description.
124
307
  def to_s
125
308
  "voice #{@name} output: #{@output} channel: #{@channel}"
126
309
  end
@@ -133,7 +316,27 @@ module Musa
133
316
  end
134
317
  end
135
318
 
319
+ # Manages MIDI Continuous Controller messages for a single channel.
320
+ #
321
+ # Provides a simple hash-like interface mapping controller numbers or
322
+ # symbolic names to values. All values are clamped to 0-127 automatically.
323
+ #
324
+ # @example Using symbolic controller names
325
+ # voice = voices.voices.first
326
+ # voice.controller[:mod_wheel] = 64 # Set modulation wheel
327
+ # voice.controller[:volume] = 100 # Set volume (CC 7)
328
+ # voice.controller[:expression] = 90 # Set expression (CC 11)
329
+ # current = voice.controller[:mod_wheel] # Get current value
330
+ #
331
+ # @example Using numeric controller numbers
332
+ # voice.controller[1] = 64 # Modulation wheel (CC 1)
333
+ # voice.controller[7] = 100 # Volume (CC 7)
334
+ # voice.controller[11] = 90 # Expression (CC 11)
335
+ #
336
+ # @see #sustain_pedal= Dedicated sustain pedal helper
136
337
  class ControllersControl
338
+ # @param output [#puts] MIDI output.
339
+ # @param channel [Integer] MIDI channel number.
137
340
  def initialize(output, channel)
138
341
  @output = output
139
342
  @channel = channel
@@ -161,6 +364,10 @@ module Musa
161
364
  @controller = []
162
365
  end
163
366
 
367
+ # Sets a controller value, emitting the corresponding Control Change message.
368
+ #
369
+ # @param controller_number_or_symbol [Integer, Symbol] CC number or well-known alias (see +@controller_map+).
370
+ # @param value [Integer] byte value that will be clamped to 0-127.
164
371
  def []=(controller_number_or_symbol, value)
165
372
  number = number_of(controller_number_or_symbol)
166
373
  value ||= 0
@@ -169,10 +376,16 @@ module Musa
169
376
  @output.puts MIDIEvents::ChannelMessage.new(0xb, @channel, number, @controller[number])
170
377
  end
171
378
 
379
+ # @return [Integer, nil] last value assigned to the controller.
172
380
  def [](controller_number_or_symbol)
173
381
  @controller[number_of(controller_number_or_symbol)]
174
382
  end
175
383
 
384
+ # Resolves a controller reference to its MIDI CC number.
385
+ #
386
+ # @param controller_number_or_symbol [Integer, Symbol] CC number or alias.
387
+ # @return [Integer] MIDI CC number (0-127).
388
+ # @raise [ArgumentError] if the parameter is neither Numeric nor Symbol.
176
389
  def number_of(controller_number_or_symbol)
177
390
  case controller_number_or_symbol
178
391
  when Numeric
@@ -188,9 +401,34 @@ module Musa
188
401
  private_constant :ControllersControl
189
402
 
190
403
  class NoteControl
191
- attr_reader :voice, :pitch, :velocity, :velocity_off, :duration
192
- attr_reader :start_position, :end_position
404
+ # @return [MIDIVoice] voice that scheduled this control.
405
+ attr_reader :voice
406
+
407
+ # @return [Array<Numeric>, Symbol] collection of MIDI note numbers (or :silence entries) handled by the control.
408
+ attr_reader :pitch
409
+
410
+ # @return [Array<Numeric>] per-note on velocities.
411
+ attr_reader :velocity
412
+
413
+ # @return [Array<Numeric>] per-note off velocities.
414
+ attr_reader :velocity_off
415
+
416
+ # @return [Numeric, nil] duration in bars or nil for indefinite notes.
417
+ attr_reader :duration
418
+
419
+ # @return [Rational, nil] sequencer position at which the note began.
420
+ attr_reader :start_position
421
+
422
+ # @return [Rational, nil] sequencer position of the note-off, if already executed.
423
+ attr_reader :end_position
193
424
 
425
+ # Wraps the state of pedal or note events scheduled by {MIDIVoice#note}.
426
+ #
427
+ # @param voice [MIDIVoice] owning voice.
428
+ # @param pitch [Array<Numeric>, Numeric, Symbol] notes or :silence.
429
+ # @param velocity [Numeric, Array<Numeric>] on velocity (can be per-note).
430
+ # @param duration [Numeric, nil] duration in bars or nil for infinite.
431
+ # @param velocity_off [Numeric, Array<Numeric>] release velocity.
194
432
  def initialize(voice, pitch:, velocity: nil, duration: nil, velocity_off: nil)
195
433
  raise ArgumentError, "MIDIVoice: note duration should be nil or Numeric: #{duration} (#{duration.class})" unless duration.nil? || duration.is_a?(Numeric)
196
434
 
@@ -209,6 +447,9 @@ module Musa
209
447
  @start_position = @end_position = nil
210
448
  end
211
449
 
450
+ # Emits the NoteOn messages and schedules the note-off when applicable.
451
+ #
452
+ # @return [NoteControl]
212
453
  def note_on
213
454
  @start_position = @voice.sequencer.position
214
455
  @end_position = nil
@@ -239,6 +480,10 @@ module Musa
239
480
  self
240
481
  end
241
482
 
483
+ # Stops the note, sending the proper NoteOffs and executing registered callbacks.
484
+ #
485
+ # @param velocity [Numeric, Array<Numeric>] optional override for the release velocity.
486
+ # @return [void]
242
487
  def note_off(velocity: nil)
243
488
  velocity ||= @velocity_off
244
489
 
@@ -272,15 +517,29 @@ module Musa
272
517
  nil
273
518
  end
274
519
 
520
+ # @return [Boolean] true while the note is sounding (NoteOn sent, NoteOff pending).
275
521
  def active?
276
522
  @start_position && !@end_position
277
523
  end
278
524
 
525
+ # Registers a block to be executed when the note stops.
526
+ #
527
+ # @yield Block to execute when note-off occurs
528
+ # @yieldparam sequencer [Musa::Sequencer::Sequencer]
529
+ # @return [void]
279
530
  def on_stop(&block)
280
531
  @do_on_stop << block
281
532
  nil
282
533
  end
283
534
 
535
+ # Registers a block to be executed a number of bars after the note has ended.
536
+ #
537
+ # Useful for scheduling continuations or cleanup logic once the note fully
538
+ # decays in the musical timeline.
539
+ #
540
+ # @param bars [Numeric] delay in bars (can be rational). Defaults to 0.
541
+ # @yieldparam sequencer [Musa::Sequencer::Sequencer]
542
+ # @return [void]
284
543
  def after(bars = 0, &block)
285
544
  @do_after << { bars: bars.rationalize, block: block }
286
545
  nil
@@ -288,6 +547,8 @@ module Musa
288
547
 
289
548
  private
290
549
 
550
+ # @return [Boolean] true if the pitch represents a rest/gap.
551
+ # @api private
291
552
  def silence?(pitch)
292
553
  pitch.nil? || pitch == :silence
293
554
  end