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: 1.0, velocity: 0 }.extend(GDV)
scale = Musa::Scales::Scales.et12[440.0].major[60]
pdv = gdv.to_pdv(scale)
# => { pitch: 60, duration: 1.0, velocity: 64 }
To GDVd (Delta Encoding)
Converts to delta encoding for efficient storage:
gdv1 = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
gdv2 = { grade: 2, octave: 0, duration: 1.0, velocity: 1 }.extend(GDV)
gdvd = gdv2.to_gdvd(scale, previous: gdv1)
# => { delta_grade: 2, delta_velocity: 1 }
To Neuma Notation
Converts to Neuma string format for serialization:
gdv = { grade: 0, duration: 1.0, 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)
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
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.
151 152 153 |
# File 'lib/musa-dsl/datasets/gdv.rb', line 151 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.
#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
384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 |
# File 'lib/musa-dsl/datasets/gdv.rb', line 384 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]].octave(self[:octave]).wide_grade - scale[previous[:grade]].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 gdvd[:abs_grade] = self[:grade] if self[:grade] 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
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)
- modifiers: Additional key-value pairs (e.g., "staccato")
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 |
# File 'lib/musa-dsl/datasets/gdv.rb', line 289 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
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 |
# File 'lib/musa-dsl/datasets/gdv.rb', line 199 def to_pdv(scale) pdv = {}.extend PDV pdv.base_duration = @base_duration if self[:grade] pdv[:pitch] = if self[:silence] :silence else scale[self[:grade]].sharp(self[:sharps] || 0).octave(self[:octave] || 0).pitch end 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.