Module: Musa::Datasets::GDV
Overview
Score-style musical events with scale degrees.
GDV (Grade/Duration/Velocity) represents musical events using score notation with scale degrees, octaves, and dynamics. Extends AbsD for duration support.
Purpose
GDV is the score representation layer of the dataset framework:
- Uses scale degrees (grade) instead of absolute pitches
- Uses dynamics markings (velocity -5 to +4) instead of MIDI velocity
- Human-readable and musically meaningful
- Independent of specific tuning or scale
Contrast with PDV which uses MIDI absolute pitches and velocities.
Natural Keys
- :grade: Scale degree (integer, 0-based)
- :sharps: Chromatic alteration (integer, positive = sharp, negative = flat)
- :octave: Octave offset (integer, 0 = base octave)
- :velocity: Dynamics (-3 to +4, where 0 = mp, 1 = mf)
- :silence: Indicates rest (boolean or symbol)
- :duration: Event duration (from AbsD)
- :note_duration, :forward_duration: Additional duration keys (from AbsD)
Pitch Representation
Pitches are specified as:
- grade: Position in scale (0 = first note, 1 = second note, etc.)
- octave: Octave offset (0 = base, 1 = up one octave, -1 = down one octave)
- sharps: Chromatic alteration (1 = sharp, -1 = flat, 2 = double sharp, etc.)
Example in C major scale:
- C4 = { grade: 0, octave: 0 }
- D4 = { grade: 1, octave: 0 }
- C5 = { grade: 0, octave: 1 }
- C#4 = { grade: 0, octave: 0, sharps: 1 }
Velocity (Dynamics)
Velocity represents musical dynamics in range -3 to +4:
-3: ppp (pianississimo)
-2: pp (pianissimo)
-1: p (piano)
0: mp (mezzo-piano)
+1: mf (mezzo-forte)
+2: f (forte)
+3: ff (fortissimo)
+4: fff (fortississimo)
Conversions
To PDV (MIDI)
Converts score notation to MIDI using a scale:
gdv = { grade: 0, octave: 0, duration: 1r, velocity: 0 }.extend(GDV)
scale = Musa::Scales::Scales.et12[440.0].major[60]
pdv = gdv.to_pdv(scale)
# => { pitch: 60, duration: 1r, velocity: 64 }
To GDVd (Delta Encoding)
Converts to movement-from-a-previous-event, which is what lets the same material be replayed from any other starting point:
gdv1 = { grade: 0, octave: 0, duration: 1r, velocity: 0 }.extend(GDV)
gdv2 = { grade: 2, octave: 0, duration: 1r, velocity: 1 }.extend(GDV)
[gdv1, gdv2].each { |g| g.base_duration = 1/4r }
gdvd = gdv2.to_gdvd(scale, previous: gdv1)
# => { delta_grade: 2, delta_sharps: 0, delta_velocity: 1 }
To Neuma Notation
Converts to Neuma string format for serialization:
gdv = { grade: 0, duration: 1r, velocity: 0 }.extend(GDV)
gdv.base_duration = 1/4r
gdv.to_neuma # => "(0 4 mp)"
MIDI Velocity Mapping
Dynamics are mapped to MIDI velocities using interpolation:
-3 (ppp) → 16
-2 (pp) → 33
-1 (p) → 49
0 (mp) → 64
+1 (mf) → 80
+2 (f) → 96
+3 (ff) → 112
+4 (fff) → 127
Constant Summary collapse
- NaturalKeys =
Natural keys for score events.
(NaturalKeys + [:grade, :sharps, :octave, :velocity, :silence]).freeze
- VELOCITY_MAP =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
MIDI velocity mapping for dynamics.
Maps dynamics values (-5 to +4) to MIDI velocities (0-127). Used for interpolation in #to_pdv.
TODO create a customizable MIDI velocity to score dynamics bidirectional conversor ppp = 16 ... fff = 127 (-5 ... 4) the standard used by Musescore 3 and others starts at ppp = 16
[1, 8, 16, 33, 49, 64, 80, 96, 112, 127].freeze
- VELOCITY_BANDS =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
The same mapping read the other way: which dynamic a MIDI velocity belongs to. Each band runs from just above the previous dynamic's velocity up to its own, so every value in VELOCITY_MAP falls in its own band.
DERIVED, NOT WRITTEN AGAIN. PDV#to_gdv used to carry its own copy of these ten ranges, and the copy disagreed with VELOCITY_MAP at exactly one edge: it closed p's band at 48 and started mp's at 49, so p's own velocity read back as mp and the dynamic did not survive a round trip (issue #86). One rule written twice is one rule that will be corrected once.
VELOCITY_MAP.each_with_index.collect do |top, i| (i.zero? ? top : VELOCITY_MAP[i - 1] + 1)..top end.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_gdvd(scale, previous: nil) ⇒ GDVd
Converts to GDVd (delta encoding).
-
#to_neuma ⇒ String
Converts to Neuma notation string.
-
#to_pdv(scale) ⇒ PDV
Converts to PDV (MIDI representation).
-
#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.
155 156 157 |
# File 'lib/musa-dsl/datasets/gdv.rb', line 155 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_gdvd(scale, previous: nil) ⇒ GDVd
Converts to GDVd (delta encoding).
Creates delta-encoded representation relative to a previous event. Only changed values are included, making the representation compact.
Without previous event (first in sequence):
- Uses abs_ keys for all values
With previous event:
- Uses delta_ keys for changed values
- Omits unchanged values
- Uses abs_ keys when changing from nil to value
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 |
# File 'lib/musa-dsl/datasets/gdv.rb', line 407 def to_gdvd(scale, previous: nil) gdvd = {}.extend GDVd gdvd.base_duration = @base_duration if previous if include?(:silence) gdvd[:abs_grade] = :silence elsif include?(:grade) && !previous.include?(:grade) gdvd[:abs_grade] = self[:grade] gdvd[:abs_sharps] = self[:sharps] elsif include?(:grade) && previous.include?(:grade) if self[:grade] != previous[:grade] || (self[:sharps] || 0) != (previous[:sharps] || 0) gdvd[:delta_grade] = scale[self[:grade]].at_octave(self[:octave]).wide_grade - scale[previous[:grade]].at_octave(previous[:octave]).wide_grade gdvd[:delta_sharps] = (self[:sharps] || 0) - (previous[:sharps] || 0) end elsif include?(:sharps) gdvd[:delta_sharps] = self[:sharps] - (previous[:sharps] || 0) end if self[:duration] && previous[:duration] && (self[:duration] != previous[:duration]) gdvd[:delta_duration] = (self[:duration] - previous[:duration]) end if self[:velocity] && previous[:velocity] && (self[:velocity] != previous[:velocity]) gdvd[:delta_velocity] = self[:velocity] - previous[:velocity] end else # The same order as the branch above, and for the same reason: a rest # asked about its grade answers with the grade it silences. Asking that # first turned a rest opening a delta-encoded sequence into an audible # note -- `(silence 4)` came out as `(0 4 mf)` (issue #80). if self[:silence] gdvd[:abs_grade] = :silence elsif self[:grade] gdvd[:abs_grade] = self[:grade] end gdvd[:abs_duration] = self[:duration] if self[:duration] gdvd[:abs_velocity] = self[:velocity] if self[:velocity] end (keys - NaturalKeys).each { |k| gdvd[k] = self[k] } gdvd end |
#to_neuma ⇒ String
The dynamics run ppp pp p mp mf f ff for velocities -3..3, so
velocity 0 is mp and not the middle of the range. And the duration must
be Rational: a Float duration reaches the notation as one, and
{ duration: 1r } renders "(0 4.0 mp)".
Converts to Neuma notation string.
Neuma is a compact text format for score notation. Format:
(grade[sharps] [octave] [duration] [velocity] [modifiers...])
- grade: Scale degree number (0, 1, 2...) or 'silence' for rests
- sharps: '#' for sharp, '_' for flat (e.g., "0#" = first degree sharp)
- octave: 'o' + number (e.g., "o1" = up one octave, "o-1" = down one)
- duration: Number of base_duration units
- velocity: Dynamics string (ppp, pp, p, mp, mf, f, ff, fff -- and any number of p's or f's beyond those, see Helper#velocity_of)
- modifiers: Additional key-value pairs (e.g., "staccato")
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 |
# File 'lib/musa-dsl/datasets/gdv.rb', line 329 def to_neuma @base_duration ||= Rational(1, 4) attributes = [] c = 0 if include?(:silence) attributes[c] = :silence elsif include?(:grade) attributes[c] = self[:grade].to_s if include?(:sharps) if self[:sharps] > 0 attributes[c] += '#' * self[:sharps] elsif self[:sharps] < 0 attributes[c] += '_' * self[:sharps].abs end end end attributes[c] = '.' if attributes[c].nil? || attributes[c].empty? attributes[c += 1] = 'o' + self[:octave].to_s if self[:octave] attributes[c += 1] = (self[:duration] / @base_duration).to_s if self[:duration] attributes[c += 1] = velocity_of(self[:velocity]) if self[:velocity] (keys - NaturalKeys).each do |k| attributes[c += 1] = modificator_string(k, self[k]) end '(' + attributes.join(' ') + ')' end |
#to_pdv(scale) ⇒ PDV
Converts to PDV (MIDI representation).
Translates score notation to MIDI using a scale:
- Scale degree → MIDI pitch (via scale lookup)
- Dynamics → MIDI velocity (via interpolation)
- Duration values copied
- Additional keys preserved
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 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
# File 'lib/musa-dsl/datasets/gdv.rb', line 225 def to_pdv(scale) pdv = {}.extend PDV pdv.base_duration = @base_duration # A rest is a rest whether or not it also carries the grade of the note # that would have sounded. Asking for the grade first left a silence with # no grade -- which is what a PDV rest converts into, since a PDV rest # carries no grade to recover -- with no :pitch at all, so it stopped # raising and started disappearing instead. if self[:silence] pdv[:pitch] = :silence elsif self[:grade] pdv[:pitch] = scale[self[:grade]].sharp(self[:sharps] || 0).at_octave(self[:octave] || 0).pitch end if self[:duration] pdv[:duration] = self[:duration] end if self[:note_duration] pdv[:note_duration] = self[:note_duration] end if self[:forward_duration] pdv[:forward_duration] = self[:forward_duration] end if self[:velocity] index = if (-5..4).cover?(self[:velocity]) self[:velocity] else self[:velocity] < -5 ? -5 : 4 end index_min = index.floor index_max = index.ceil velocity = VELOCITY_MAP[index_min + 5] + (VELOCITY_MAP[index_max + 5] - VELOCITY_MAP[index_min + 5]) * (self[:velocity] - index_min) pdv[:velocity] = velocity end (keys - NaturalKeys).each { |k| pdv[k] = self[k] } pdv 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.