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.
- checksums.yaml +4 -4
- data/.gitignore +5 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/Gemfile +0 -1
- 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 +544 -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 +215 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +387 -0
- data/lib/musa-dsl/generative/markov.rb +135 -3
- data/lib/musa-dsl/generative/rules.rb +312 -4
- data/lib/musa-dsl/generative/variatio.rb +286 -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 +113 -2
- data/lib/musa-dsl/midi/midi-voices.rb +275 -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 +353 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -206
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
- data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
- data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
- data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
- data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
- data/lib/musa-dsl/music/scales.rb +1384 -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 +54 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +64 -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 +57 -7
- data/lib/musa-dsl/series/queue-serie.rb +78 -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 +2 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +25 -18
- 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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
192
|
-
attr_reader :
|
|
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
|