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.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/README.md +227 -6
- data/docs/README.md +83 -0
- data/docs/api-reference.md +86 -0
- data/docs/getting-started/quick-start.md +93 -0
- data/docs/getting-started/tutorial.md +58 -0
- data/docs/subsystems/core-extensions.md +316 -0
- data/docs/subsystems/datasets.md +465 -0
- data/docs/subsystems/generative.md +290 -0
- data/docs/subsystems/matrix.md +63 -0
- data/docs/subsystems/midi.md +123 -0
- data/docs/subsystems/music.md +233 -0
- data/docs/subsystems/musicxml-builder.md +264 -0
- data/docs/subsystems/neumas.md +71 -0
- data/docs/subsystems/repl.md +135 -0
- data/docs/subsystems/sequencer.md +98 -0
- data/docs/subsystems/series.md +302 -0
- data/docs/subsystems/transcription.md +152 -0
- data/docs/subsystems/transport.md +177 -0
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
- data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
- data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
- data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
- data/lib/musa-dsl/core-ext/extension.rb +53 -0
- data/lib/musa-dsl/core-ext/hashify.rb +162 -1
- data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
- data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
- data/lib/musa-dsl/core-ext/with.rb +114 -0
- data/lib/musa-dsl/datasets/dataset.rb +109 -0
- data/lib/musa-dsl/datasets/delta-d.rb +78 -0
- data/lib/musa-dsl/datasets/e.rb +186 -2
- data/lib/musa-dsl/datasets/gdv.rb +279 -2
- data/lib/musa-dsl/datasets/gdvd.rb +201 -0
- data/lib/musa-dsl/datasets/helper.rb +75 -0
- data/lib/musa-dsl/datasets/p.rb +177 -2
- data/lib/musa-dsl/datasets/packed-v.rb +91 -0
- data/lib/musa-dsl/datasets/pdv.rb +136 -1
- data/lib/musa-dsl/datasets/ps.rb +134 -4
- data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
- data/lib/musa-dsl/datasets/score/render.rb +105 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
- data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
- data/lib/musa-dsl/datasets/score.rb +279 -0
- data/lib/musa-dsl/datasets/v.rb +88 -0
- data/lib/musa-dsl/generative/darwin.rb +180 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
- data/lib/musa-dsl/generative/markov.rb +133 -3
- data/lib/musa-dsl/generative/rules.rb +258 -4
- data/lib/musa-dsl/generative/variatio.rb +217 -2
- data/lib/musa-dsl/logger/logger.rb +267 -2
- data/lib/musa-dsl/matrix/matrix.rb +256 -10
- data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
- data/lib/musa-dsl/midi/midi-voices.rb +265 -4
- data/lib/musa-dsl/music/chord-definition.rb +233 -1
- data/lib/musa-dsl/music/chord-definitions.rb +33 -6
- data/lib/musa-dsl/music/chords.rb +308 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
- data/lib/musa-dsl/music/scales.rb +957 -40
- data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
- data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
- data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
- data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
- data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
- data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
- data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
- data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
- data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
- data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
- data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
- data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
- data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
- data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
- data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
- data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
- data/lib/musa-dsl/neumas/neumas.rb +67 -0
- data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
- data/lib/musa-dsl/repl/repl.rb +550 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
- data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
- data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
- data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
- data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
- data/lib/musa-dsl/series/array-to-serie.rb +37 -1
- data/lib/musa-dsl/series/base-series.rb +843 -5
- data/lib/musa-dsl/series/buffer-serie.rb +48 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
- data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
- data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
- data/lib/musa-dsl/series/proxy-serie.rb +67 -0
- data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
- data/lib/musa-dsl/series/queue-serie.rb +65 -0
- data/lib/musa-dsl/series/series-composer.rb +701 -0
- data/lib/musa-dsl/series/timed-serie.rb +473 -28
- data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
- data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
- data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
- data/lib/musa-dsl/transcription/transcription.rb +265 -0
- data/lib/musa-dsl/transport/clock.rb +125 -0
- data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
- data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
- data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
- data/lib/musa-dsl/transport/timer-clock.rb +183 -1
- data/lib/musa-dsl/transport/timer.rb +83 -0
- data/lib/musa-dsl/transport/transport.rb +318 -0
- data/lib/musa-dsl/version.rb +1 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +12 -10
- 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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
192
|
-
attr_reader :
|
|
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
|