musa-dsl 0.30.2 → 0.41.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 (158) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/Gemfile +0 -1
  6. data/README.md +227 -6
  7. data/docs/README.md +83 -0
  8. data/docs/api-reference.md +86 -0
  9. data/docs/getting-started/quick-start.md +93 -0
  10. data/docs/getting-started/tutorial.md +58 -0
  11. data/docs/subsystems/core-extensions.md +316 -0
  12. data/docs/subsystems/datasets.md +465 -0
  13. data/docs/subsystems/generative.md +290 -0
  14. data/docs/subsystems/matrix.md +63 -0
  15. data/docs/subsystems/midi.md +123 -0
  16. data/docs/subsystems/music.md +544 -0
  17. data/docs/subsystems/musicxml-builder.md +264 -0
  18. data/docs/subsystems/neumas.md +71 -0
  19. data/docs/subsystems/repl.md +135 -0
  20. data/docs/subsystems/sequencer.md +98 -0
  21. data/docs/subsystems/series.md +302 -0
  22. data/docs/subsystems/transcription.md +152 -0
  23. data/docs/subsystems/transport.md +177 -0
  24. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  25. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  26. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  27. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  28. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  29. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  30. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  31. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  32. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  33. data/lib/musa-dsl/core-ext/with.rb +114 -0
  34. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  35. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  36. data/lib/musa-dsl/datasets/e.rb +186 -2
  37. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  38. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  39. data/lib/musa-dsl/datasets/helper.rb +75 -0
  40. data/lib/musa-dsl/datasets/p.rb +177 -2
  41. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  42. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  43. data/lib/musa-dsl/datasets/ps.rb +134 -4
  44. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  45. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  48. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  49. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  50. data/lib/musa-dsl/datasets/score.rb +279 -0
  51. data/lib/musa-dsl/datasets/v.rb +88 -0
  52. data/lib/musa-dsl/generative/darwin.rb +215 -1
  53. data/lib/musa-dsl/generative/generative-grammar.rb +387 -0
  54. data/lib/musa-dsl/generative/markov.rb +135 -3
  55. data/lib/musa-dsl/generative/rules.rb +312 -4
  56. data/lib/musa-dsl/generative/variatio.rb +286 -2
  57. data/lib/musa-dsl/logger/logger.rb +267 -2
  58. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  59. data/lib/musa-dsl/midi/midi-recorder.rb +113 -2
  60. data/lib/musa-dsl/midi/midi-voices.rb +275 -4
  61. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  62. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  63. data/lib/musa-dsl/music/chords.rb +353 -2
  64. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -206
  65. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
  66. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
  67. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
  68. data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
  69. data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
  70. data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
  71. data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
  72. data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
  73. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
  74. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
  75. data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
  76. data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
  77. data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
  78. data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
  79. data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
  80. data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
  81. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
  82. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
  83. data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
  84. data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
  85. data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
  86. data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
  87. data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
  88. data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
  89. data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
  90. data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
  91. data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
  92. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
  93. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
  94. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
  95. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
  96. data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
  97. data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
  98. data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
  99. data/lib/musa-dsl/music/scales.rb +1384 -40
  100. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  101. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  102. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  103. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  104. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  105. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  106. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  107. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  108. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  109. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  110. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  111. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  112. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  113. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  114. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  115. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  116. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  117. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  118. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  119. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  120. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  121. data/lib/musa-dsl/repl/repl.rb +550 -0
  122. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  123. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  124. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  125. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  126. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  127. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  128. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  129. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  130. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  131. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  132. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  133. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  134. data/lib/musa-dsl/series/base-series.rb +843 -5
  135. data/lib/musa-dsl/series/buffer-serie.rb +54 -0
  136. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +64 -0
  137. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  138. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  139. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  140. data/lib/musa-dsl/series/quantizer-serie.rb +57 -7
  141. data/lib/musa-dsl/series/queue-serie.rb +78 -0
  142. data/lib/musa-dsl/series/series-composer.rb +701 -0
  143. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  144. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  145. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  146. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  147. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  148. data/lib/musa-dsl/transport/clock.rb +125 -0
  149. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  150. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  151. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  152. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  153. data/lib/musa-dsl/transport/timer.rb +83 -0
  154. data/lib/musa-dsl/transport/transport.rb +318 -0
  155. data/lib/musa-dsl/version.rb +2 -1
  156. data/lib/musa-dsl.rb +132 -25
  157. data/musa-dsl.gemspec +25 -18
  158. metadata +158 -16
@@ -1,8 +1,86 @@
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.
82
+ #
83
+ # @return [void]
6
84
  def initialize(sequencer)
7
85
  @sequencer = sequencer
8
86
  @midi_parser = MIDIParser.new
@@ -10,10 +88,17 @@ module Musa
10
88
  clear
11
89
  end
12
90
 
91
+ # Clears all stored events.
92
+ #
93
+ # @return [void]
13
94
  def clear
14
95
  @messages = []
15
96
  end
16
97
 
98
+ # Records one MIDI packet.
99
+ #
100
+ # @param midi_bytes [String, Array<Integer>] bytes as provided by the MIDI driver.
101
+ # @return [void]
17
102
  def record(midi_bytes)
18
103
  m = @midi_parser.parse midi_bytes
19
104
  m = [m] unless m.is_a? Array
@@ -23,10 +108,24 @@ module Musa
23
108
  end
24
109
  end
25
110
 
111
+ # @return [Array<Message>] unprocessed recorded messages.
26
112
  def raw
27
113
  @messages
28
114
  end
29
115
 
116
+ # Converts the message buffer into a list of note hashes.
117
+ #
118
+ # Each note hash contains the keys :position, :channel, :pitch, :velocity
119
+ # and, when appropriate, :duration and :velocity_off. Silences (gaps between
120
+ # notes on the same channel) are expressed as `pitch: :silence`.
121
+ #
122
+ # @return [Array<Hash>] list of events suitable for Musa transcription pipelines.
123
+ # @example Output format
124
+ # [
125
+ # { position: Rational(0,1), channel: 0, pitch: 60, velocity: 100, duration: Rational(1,4), velocity_off: 64 },
126
+ # { position: Rational(1,4), channel: 0, pitch: :silence, duration: Rational(1,8) },
127
+ # { position: Rational(3,8), channel: 0, pitch: 62, velocity: 90, duration: Rational(1,4), velocity_off: 64 }
128
+ # ]
30
129
  def transcription
31
130
  note_on = {}
32
131
  last_note = {}
@@ -69,9 +168,21 @@ module Musa
69
168
  notes
70
169
  end
71
170
 
171
+ # Internal representation of a captured MIDI message linked to its sequencer position.
172
+ # @api private
72
173
  class Message
73
- attr_accessor :position, :message
74
-
174
+ # @return [Rational] sequencer position where the message was captured.
175
+ attr_reader :position
176
+
177
+ # @return [MIDIEvents::Event] parsed MIDI event.
178
+ attr_reader :message
179
+
180
+ # Creates a new message entry.
181
+ #
182
+ # @param position [Rational] sequencer position when captured.
183
+ # @param message [MIDIEvents::Event] parsed MIDI event.
184
+ #
185
+ # @return [void]
75
186
  def initialize(position, message)
76
187
  @position = position
77
188
  @message = message
@@ -5,13 +5,100 @@ 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.
100
+ #
101
+ # @return [void]
15
102
  def initialize(sequencer:, output:, channels:, do_log: nil)
16
103
  do_log ||= false
17
104
 
@@ -23,16 +110,33 @@ module Musa
23
110
  reset
24
111
  end
25
112
 
113
+ # Resets the collection recreating every {MIDIVoice}. Useful when the MIDI
114
+ # output has changed or after a panic.
115
+ #
116
+ # @return [void]
26
117
  def reset
27
118
  @voices = @channels.collect { |channel| MIDIVoice.new(sequencer: @sequencer, output: @output, channel: channel, do_log: @do_log) }.freeze
28
119
  end
29
120
 
121
+ # @return [Array<MIDIVoice>] read-only list of per-channel voices.
30
122
  attr_reader :voices
31
123
 
124
+ # Enables or disables the fast-forward mode on every voice.
125
+ #
126
+ # When enabled, notes are registered internally but their MIDI messages are
127
+ # not emitted, allowing the sequencer to catch up silently (e.g. when
128
+ # loading a snapshot).
129
+ #
130
+ # @param enabled [Boolean] true to enable fast-forward, false to disable.
131
+ # @return [void]
32
132
  def fast_forward=(enabled)
33
133
  @voices.each { |voice| voice.fast_forward = enabled }
34
134
  end
35
135
 
136
+ # Sends all-notes-off on every channel and (optionally) a MIDI reset.
137
+ #
138
+ # @param reset [Boolean] whether to emit an FF SystemRealtime (panic) message.
139
+ # @return [void]
36
140
  def panic(reset: nil)
37
141
  reset ||= false
38
142
 
@@ -44,10 +148,62 @@ module Musa
44
148
 
45
149
  private
46
150
 
151
+ # Individual MIDI channel voice with sequencer-synchronized note management.
152
+ #
153
+ # Manages the state of a single MIDI channel including active notes, controller
154
+ # values, and sustain pedal. All note scheduling is tied to the sequencer clock,
155
+ # ensuring proper timing in fast-forward mode or during quantized playback.
156
+ #
157
+ # Supports indefinite notes (manual note-off), automatic note-off scheduling,
158
+ # callbacks on note stop, and fast-forward mode for silent state updates.
159
+ #
160
+ # @example Playing notes
161
+ # voice = voices.voices.first
162
+ # voice.note pitch: 60, velocity: 90, duration: 1r/4
163
+ # voice.note pitch: [60, 64, 67], velocity: 100, duration: 1r # chord
164
+ #
165
+ # @example Indefinite notes with manual control
166
+ # note_ctrl = voice.note pitch: 60, duration: nil
167
+ # note_ctrl.on_stop { puts "Note ended!" }
168
+ # # ... later:
169
+ # note_ctrl.note_off
170
+ #
171
+ # @example Controller and sustain pedal
172
+ # voice.controller[:mod_wheel] = 64
173
+ # voice.sustain_pedal = 127
174
+ # voice.controller[:expression] = 100
175
+ #
176
+ # @see MIDIVoices Parent voice collection
177
+ # @see NoteControl Note lifecycle controller
178
+ # @api private
47
179
  class MIDIVoice
48
- attr_accessor :name, :do_log
49
- attr_reader :sequencer, :output, :channel, :active_pitches, :tick_duration
180
+ # @return [String, nil] optional name used in log messages.
181
+ attr_accessor :name
50
182
 
183
+ # @return [Boolean] whether this voice logs every emitted message.
184
+ attr_accessor :do_log
185
+
186
+ # @return [Musa::Sequencer::Sequencer] sequencer driving this voice.
187
+ attr_reader :sequencer
188
+
189
+ # @return [#puts, nil] MIDI destination. When nil the voice becomes silent.
190
+ attr_reader :output
191
+
192
+ # @return [Integer] MIDI channel number (0-15).
193
+ attr_reader :channel
194
+
195
+ # @return [Array<Hash>] metadata for each of the 128 MIDI pitches. Mainly used internally.
196
+ attr_reader :active_pitches
197
+
198
+ # @return [Rational] duration (in bars) of a sequencer tick; used to schedule note offs.
199
+ attr_reader :tick_duration
200
+
201
+ # @param sequencer [Musa::Sequencer::Sequencer]
202
+ # @param output [#puts, nil]
203
+ # @param channel [Integer] MIDI channel number (0-15).
204
+ # @param name [String, nil] human friendly identifier.
205
+ #
206
+ # @return [void]
51
207
  def initialize(sequencer:, output:, channel:, name: nil, do_log: nil)
52
208
  do_log ||= false
53
209
 
@@ -69,6 +225,13 @@ module Musa
69
225
  self
70
226
  end
71
227
 
228
+ # Turns fast-forward on/off for this voice.
229
+ #
230
+ # When disabling it, pending notes that were held silently are sent again
231
+ # so the synth is in sync with the sequencer state.
232
+ #
233
+ # @param enabled [Boolean] true to enable fast-forward, false to disable.
234
+ # @return [void]
72
235
  def fast_forward=(enabled)
73
236
  if @fast_forward && !enabled
74
237
  (0..127).each do |pitch|
@@ -79,10 +242,21 @@ module Musa
79
242
  @fast_forward = enabled
80
243
  end
81
244
 
245
+ # @return [Boolean] true when in fast-forward mode (notes registered but not emitted).
82
246
  def fast_forward?
83
247
  @fast_forward
84
248
  end
85
249
 
250
+ # Plays one or several MIDI notes.
251
+ #
252
+ # @param pitchvalue [Numeric, Array<Numeric>, nil] optional shorthand for +pitch+.
253
+ # @param pitch [Numeric, Symbol, Array<Numeric, Symbol>] MIDI note numbers or :silence. Arrays/ranges expand to multiple notes.
254
+ # @param velocity [Numeric, Array<Numeric>] raw velocity (0-127). Defaults to 63.
255
+ # @param duration [Numeric, nil] musical duration in bars. When nil the note stays on until {NoteControl#note_off} is called manually.
256
+ # @param duration_offset [Numeric] offset applied when scheduling the note-off inside the sequencer.
257
+ # @param note_duration [Numeric, nil] alternative duration in bars for legato control.
258
+ # @param velocity_off [Numeric, Array<Numeric>] release velocity (defaults to 63).
259
+ # @return [NoteControl, nil] handler that can be used to attach callbacks.
86
260
  def note(pitchvalue = nil, pitch: nil, velocity: nil, duration: nil, duration_offset: nil, note_duration: nil, velocity_off: nil)
87
261
  pitch ||= pitchvalue
88
262
 
@@ -98,18 +272,26 @@ module Musa
98
272
  end
99
273
  end
100
274
 
275
+ # @return [ControllersControl] MIDI CC manager for this voice.
101
276
  def controller
102
277
  @controllers_control
103
278
  end
104
279
 
280
+ # Sets the sustain pedal state.
281
+ #
282
+ # @param value [Integer] pedal value (0-127, typically 0 or 127).
105
283
  def sustain_pedal=(value)
106
284
  @controllers_control[:sustain_pedal] = value
107
285
  end
108
286
 
287
+ # @return [Integer, nil] current sustain pedal value.
109
288
  def sustain_pedal
110
289
  @controllers_control[:sustain_pedal]
111
290
  end
112
291
 
292
+ # Sends an immediate all-notes-off message on this channel and resets internal state.
293
+ #
294
+ # @return [void]
113
295
  def all_notes_off
114
296
  @active_pitches.clear
115
297
  fill_active_pitches @active_pitches
@@ -117,10 +299,15 @@ module Musa
117
299
  @output.puts MIDIEvents::ChannelMessage.new(0xb, @channel, 0x7b, 0)
118
300
  end
119
301
 
302
+ # Logs a message tagging the current voice.
303
+ #
304
+ # @param msg [String] the message to log.
305
+ # @return [void]
120
306
  def log(msg)
121
307
  @sequencer.logger.info('MIDIVoice') { "voice #{name || @channel}: #{msg}" } if @do_log
122
308
  end
123
309
 
310
+ # @return [String] human-readable voice description.
124
311
  def to_s
125
312
  "voice #{@name} output: #{@output} channel: #{@channel}"
126
313
  end
@@ -133,7 +320,29 @@ module Musa
133
320
  end
134
321
  end
135
322
 
323
+ # Manages MIDI Continuous Controller messages for a single channel.
324
+ #
325
+ # Provides a simple hash-like interface mapping controller numbers or
326
+ # symbolic names to values. All values are clamped to 0-127 automatically.
327
+ #
328
+ # @example Using symbolic controller names
329
+ # voice = voices.voices.first
330
+ # voice.controller[:mod_wheel] = 64 # Set modulation wheel
331
+ # voice.controller[:volume] = 100 # Set volume (CC 7)
332
+ # voice.controller[:expression] = 90 # Set expression (CC 11)
333
+ # current = voice.controller[:mod_wheel] # Get current value
334
+ #
335
+ # @example Using numeric controller numbers
336
+ # voice.controller[1] = 64 # Modulation wheel (CC 1)
337
+ # voice.controller[7] = 100 # Volume (CC 7)
338
+ # voice.controller[11] = 90 # Expression (CC 11)
339
+ #
340
+ # @see #sustain_pedal= Dedicated sustain pedal helper
136
341
  class ControllersControl
342
+ # @param output [#puts] MIDI output.
343
+ # @param channel [Integer] MIDI channel number.
344
+ #
345
+ # @return [void]
137
346
  def initialize(output, channel)
138
347
  @output = output
139
348
  @channel = channel
@@ -161,6 +370,12 @@ module Musa
161
370
  @controller = []
162
371
  end
163
372
 
373
+ # Sets a controller value, emitting the corresponding Control Change message.
374
+ #
375
+ # @param controller_number_or_symbol [Integer, Symbol] CC number or well-known alias (see +@controller_map+).
376
+ # @param value [Integer] byte value that will be clamped to 0-127.
377
+ #
378
+ # @return [Integer] clamped value
164
379
  def []=(controller_number_or_symbol, value)
165
380
  number = number_of(controller_number_or_symbol)
166
381
  value ||= 0
@@ -169,10 +384,16 @@ module Musa
169
384
  @output.puts MIDIEvents::ChannelMessage.new(0xb, @channel, number, @controller[number])
170
385
  end
171
386
 
387
+ # @return [Integer, nil] last value assigned to the controller.
172
388
  def [](controller_number_or_symbol)
173
389
  @controller[number_of(controller_number_or_symbol)]
174
390
  end
175
391
 
392
+ # Resolves a controller reference to its MIDI CC number.
393
+ #
394
+ # @param controller_number_or_symbol [Integer, Symbol] CC number or alias.
395
+ # @return [Integer] MIDI CC number (0-127).
396
+ # @raise [ArgumentError] if the parameter is neither Numeric nor Symbol.
176
397
  def number_of(controller_number_or_symbol)
177
398
  case controller_number_or_symbol
178
399
  when Numeric
@@ -188,9 +409,36 @@ module Musa
188
409
  private_constant :ControllersControl
189
410
 
190
411
  class NoteControl
191
- attr_reader :voice, :pitch, :velocity, :velocity_off, :duration
192
- attr_reader :start_position, :end_position
412
+ # @return [MIDIVoice] voice that scheduled this control.
413
+ attr_reader :voice
414
+
415
+ # @return [Array<Numeric>, Symbol] collection of MIDI note numbers (or :silence entries) handled by the control.
416
+ attr_reader :pitch
417
+
418
+ # @return [Array<Numeric>] per-note on velocities.
419
+ attr_reader :velocity
420
+
421
+ # @return [Array<Numeric>] per-note off velocities.
422
+ attr_reader :velocity_off
423
+
424
+ # @return [Numeric, nil] duration in bars or nil for indefinite notes.
425
+ attr_reader :duration
426
+
427
+ # @return [Rational, nil] sequencer position at which the note began.
428
+ attr_reader :start_position
429
+
430
+ # @return [Rational, nil] sequencer position of the note-off, if already executed.
431
+ attr_reader :end_position
193
432
 
433
+ # Wraps the state of pedal or note events scheduled by {MIDIVoice#note}.
434
+ #
435
+ # @param voice [MIDIVoice] owning voice.
436
+ # @param pitch [Array<Numeric>, Numeric, Symbol] notes or :silence.
437
+ # @param velocity [Numeric, Array<Numeric>] on velocity (can be per-note).
438
+ # @param duration [Numeric, nil] duration in bars or nil for infinite.
439
+ # @param velocity_off [Numeric, Array<Numeric>] release velocity.
440
+ #
441
+ # @return [void]
194
442
  def initialize(voice, pitch:, velocity: nil, duration: nil, velocity_off: nil)
195
443
  raise ArgumentError, "MIDIVoice: note duration should be nil or Numeric: #{duration} (#{duration.class})" unless duration.nil? || duration.is_a?(Numeric)
196
444
 
@@ -209,6 +457,9 @@ module Musa
209
457
  @start_position = @end_position = nil
210
458
  end
211
459
 
460
+ # Emits the NoteOn messages and schedules the note-off when applicable.
461
+ #
462
+ # @return [NoteControl]
212
463
  def note_on
213
464
  @start_position = @voice.sequencer.position
214
465
  @end_position = nil
@@ -239,6 +490,10 @@ module Musa
239
490
  self
240
491
  end
241
492
 
493
+ # Stops the note, sending the proper NoteOffs and executing registered callbacks.
494
+ #
495
+ # @param velocity [Numeric, Array<Numeric>] optional override for the release velocity.
496
+ # @return [void]
242
497
  def note_off(velocity: nil)
243
498
  velocity ||= @velocity_off
244
499
 
@@ -272,15 +527,29 @@ module Musa
272
527
  nil
273
528
  end
274
529
 
530
+ # @return [Boolean] true while the note is sounding (NoteOn sent, NoteOff pending).
275
531
  def active?
276
532
  @start_position && !@end_position
277
533
  end
278
534
 
535
+ # Registers a block to be executed when the note stops.
536
+ #
537
+ # @yield Block to execute when note-off occurs
538
+ # @yieldparam sequencer [Musa::Sequencer::Sequencer]
539
+ # @return [void]
279
540
  def on_stop(&block)
280
541
  @do_on_stop << block
281
542
  nil
282
543
  end
283
544
 
545
+ # Registers a block to be executed a number of bars after the note has ended.
546
+ #
547
+ # Useful for scheduling continuations or cleanup logic once the note fully
548
+ # decays in the musical timeline.
549
+ #
550
+ # @param bars [Numeric] delay in bars (can be rational). Defaults to 0.
551
+ # @yieldparam sequencer [Musa::Sequencer::Sequencer]
552
+ # @return [void]
284
553
  def after(bars = 0, &block)
285
554
  @do_after << { bars: bars.rationalize, block: block }
286
555
  nil
@@ -288,6 +557,8 @@ module Musa
288
557
 
289
558
  private
290
559
 
560
+ # @return [Boolean] true if the pitch represents a rest/gap.
561
+ # @api private
291
562
  def silence?(pitch)
292
563
  pitch.nil? || pitch == :silence
293
564
  end