Module: Musa::Datasets::PDV
Overview
MIDI-style musical events with absolute pitches.
PDV (Pitch/Duration/Velocity) represents musical events using MIDI-like absolute pitch numbers. Extends AbsD for duration support.
Purpose
PDV is the MIDI representation layer of the dataset framework:
- Uses absolute MIDI pitch numbers (0-127)
- Uses MIDI velocity values (0-127)
- Direct mapping to MIDI messages
- Machine-oriented (not human-readable)
Contrast with GDV which uses score notation (scale degrees, dynamics).
Natural Keys
- :pitch: MIDI pitch number (0-127) or :silence for rests
- :velocity: MIDI velocity (0-127)
- :duration: Event duration (from AbsD)
- :note_duration, :forward_duration: Additional duration keys (from AbsD)
Conversions
To GDV (Score Notation)
Converts MIDI pitches to scale degrees using a scale reference:
pdv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(PDV)
scale = Musa::Scales::Scales.et12[440.0].major[60]
gdv = pdv.to_gdv(scale)
# => { grade: 0, octave: 0, duration: 1.0, velocity: 0 }
- Pitch → Grade: Finds closest scale degree
- Chromatic notes: Represented as grade + sharps
- Velocity: Maps MIDI 1-127 to dynamics -5 to +4 (ppppp to fff)
Velocity Mapping
MIDI velocities are mapped to musical dynamics:
MIDI 0 → not a dynamic at all: no sound, which becomes a rest
MIDI 1-1 → velocity -5 (ppppp)
MIDI 2-8 → velocity -4 (pppp)
MIDI 9-16 → velocity -3 (ppp)
MIDI 17-33 → velocity -2 (pp)
MIDI 34-49 → velocity -1 (p)
MIDI 50-64 → velocity 0 (mp)
MIDI 65-80 → velocity +1 (mf)
MIDI 81-96 → velocity +2 (f)
MIDI 97-112 → velocity +3 (ff)
MIDI 113-127 → velocity +4 (fff)
Zero is left out on purpose. A pianissimo, however faint, is a sound; zero amplitude is not one, so it is not the bottom of this scale but off it, and #to_gdv writes it as a rest. Anything above 127 or between 0 and 1 is clamped into the table.
The names come from Helper#velocity_of, and zero is mp, not mf. The table
used to name them two steps towards the soft end, which is where the
velocity 0 == mf misreading kept coming back from (issue #74).
The bands themselves are GDV::VELOCITY_BANDS, derived from GDV::VELOCITY_MAP so that each dynamic's own MIDI velocity falls inside its own band. They used to be written out here a second time, and disagreed with the map at MIDI 49 (issue #86).
Base Duration
The base_duration attribute defines the unit for duration values,
typically 1/4r, which is a quarter of a bar -- a quarter note in 4/4 and
not in other meters; see AbsD.
Constant Summary collapse
- NaturalKeys =
Natural keys for MIDI events.
(NaturalKeys + [:pitch, :velocity]).freeze
Instance Attribute Summary collapse
-
#base_duration ⇒ Rational
Base duration for time calculations.
Instance Method Summary collapse
-
#duration ⇒ Numeric
included
from AbsD
Returns event duration.
-
#forward_duration ⇒ Numeric
included
from AbsD
Returns forward duration (time until next event).
-
#note_duration ⇒ Numeric
included
from AbsD
Returns actual note duration.
-
#to_gdv(scale) ⇒ GDV
Converts to GDV (score notation).
-
#valid? ⇒ Boolean
included
from E
Checks if event is valid.
-
#validate! ⇒ void
included
from E
Validates event, raising if invalid.
Instance Attribute Details
#base_duration ⇒ Rational
Base duration for time calculations.
138 139 140 |
# File 'lib/musa-dsl/datasets/pdv.rb', line 138 def base_duration @base_duration end |
Instance Method Details
#duration ⇒ Numeric Originally defined in module AbsD
Returns event duration.
#forward_duration ⇒ Numeric Originally defined in module AbsD
Returns forward duration (time until next event).
Defaults to :duration if :forward_duration not specified. This is the
value play waits on in :wait mode, so it is what advances a serie.
#note_duration ⇒ Numeric Originally defined in module AbsD
Returns actual note duration.
Defaults to :duration if :note_duration not specified.
#to_gdv(scale) ⇒ GDV
Converts to GDV (score notation).
Translates MIDI representation to score notation using a scale:
- MIDI pitch → scale degree (grade + octave + sharps)
- MIDI velocity → dynamics (-5 to +4)
- Duration values copied
- Additional keys preserved
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/musa-dsl/datasets/pdv.rb', line 188 def to_gdv(scale) gdv = {}.extend GDV gdv.base_duration = @base_duration if self[:pitch] if self[:pitch] == :silence # The :silence KEY, which is what everything else reads and a declared # natural key of GDV. It used to write `grade: :silence`, which nothing # reads: the GDV came back malformed by the framework's own convention # and could not be converted again (issue #80). It looked right only # because to_neuma falls through to printing the grade, and the # symbol's name happens to be the word the notation uses. gdv[:silence] = true else note = scale.note_of_pitch(self[:pitch], allow_chromatic: true) if background_note = note.background_note gdv[:grade] = background_note.grade gdv[:octave] = background_note.octave gdv[:sharps] = note.background_sharps else gdv[:grade] = note.grade gdv[:octave] = note.octave end end end gdv[:duration] = self[:duration] if self[:duration] if self[:velocity] # TODO create a customizable MIDI velocity to score dynamics bidirectional conversor if self[:velocity] <= 0 # No sound, and that is not the same as the softest sound. A # pianissimo, however faint, is something being played; zero amplitude # is not. MIDI's own velocity 0 means "note off" at the wire, but PDV # is a dataset and not a protocol, so here it can only mean what it # says. # # What the score layer writes for "this does not sound" is a rest, and # GDV already has the representation: the grade of the note that would # have sounded plus the :silence key, which is exactly the shape the # neuma decoder produces for `(silence 4)`. So the pitch survives the # crossing; it is `GDV#to_pdv` that normalises it back to a plain # `pitch: :silence`. # # It used to raise: the bands start at 1, so nothing covered 0, `index` # returned nil and `nil - 5` blew up with a message naming neither the # velocity nor the dataset (issue #85). gdv[:silence] = true else # Any positive velocity is a sound. Clamped because MIDI says nothing # outside 1..127, and because a velocity between 0 and 1 -- what a # continuous curve gives before it is rounded -- is a sound too, and # fell through the same hole. gdv[:velocity] = GDV::VELOCITY_BANDS.index { |band| band.cover?(self[:velocity].clamp(1, 127)) } - 5 end end (keys - NaturalKeys).each { |k| gdv[k] = self[k] } gdv end |
#valid? ⇒ Boolean Originally defined in module E
Checks if event is valid.
Base implementation always returns true. Subclasses should override to implement specific validation logic.
#validate! ⇒ void Originally defined in module E
This method returns an undefined value.
Validates event, raising if invalid.