Module: Musa::Datasets::Score::ToMXML Private

Included in:
Musa::Datasets::Score
Defined in:
lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb,
lib/musa-dsl/datasets/score/to-mxml/process-ps.rb,
lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb,
lib/musa-dsl/datasets/score/to-mxml/process-time.rb

Overview

This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.

MusicXML export for scores.

ToMXML provides conversion of Musa::Datasets::Score objects to MusicXML format, suitable for import into notation software like MuseScore, Finale, or Sibelius.

Conversion Process

  1. Creates MusicXML structure with metadata (title, creators, etc.)

  2. Defines parts (instruments) with clefs and time signatures

  3. Divides score into measures (bars)

  4. Processes events in each measure:

    • PDV events → notes and rests
    • PS events → dynamics markings (crescendo, diminuendo)
  5. Fills gaps with rests

Event Types Supported

  • PDV (Pitch/Duration/Velocity): Converted to notes or rests
  • PS (Pitch Series): Converted to dynamics markings

Multi-part Scores

Scores can contain multiple instruments, differentiated by the :instrument attribute. Each part is rendered separately.

Time Representation

Positions and durations are counted in BARS, and they are 1-based: bar 1 starts at position 1r, bar 2 at 2r, and a duration of 1r lasts a whole bar whatever the meter. That is what the sequencer counts, and this follows it.

A figure's NAME, on the other hand, is a fraction of a whole note, which is a different thing: a bar measures beats_per_bar / beat_type whole notes. In 4/4 the two coincide -- a bar is a whole note -- which is why one number served for both for a long time and why nothing outside 4/4 came out right (issue #70). So:

  • in 4/4, duration: 1/4r is a quarter note;
  • in 3/4, duration: 1/4r is a quarter of a bar, which is written as a dotted eighth, and a quarter note is duration: 1/3r;
  • in 6/8, duration: 1/6r is a beat, which is written as an eighth.

The conversion happens here, at the only place that has to name figures. Nothing else in the library needs to know about whole notes.

Examples:

Basic single-part score

score = Musa::Datasets::Score.new
score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
score.at(2r, add: { pitch: 64, duration: 1.0 }.extend(Musa::Datasets::PDV))

# Two whole notes: each lasts a whole bar of 4/4.
mxml = score.to_mxml(
  4, 24,  # 4 beats per bar, 24 ticks per beat
  bpm: 120,
  title: 'My Song',
  creators: { composer: 'John Doe' },
  parts: { piano: { name: 'Piano', clefs: { g: 2 } } }
)

File.write('score.musicxml', mxml.to_xml.string)

Multi-part score

score = Musa::Datasets::Score.new
score.at(1r, add: { instrument: :violin, pitch: 67, duration: 1.0 }.extend(Musa::Datasets::PDV))
score.at(1r, add: { instrument: :cello, pitch: 48, duration: 1.0 }.extend(Musa::Datasets::PDV))

mxml = score.to_mxml(
  4, 24,
  parts: {
    violin: { name: 'Violin', clefs: { g: 2 } },
    cello: { name: 'Cello', clefs: { f: 4 } }
  }
)

With dynamics

score = Musa::Datasets::Score.new
score.at(1r, add: { pitch: 60, duration: 2.0 }.extend(Musa::Datasets::PDV))
score.at(1r, add: { type: :crescendo, duration: 2.0 }.extend(Musa::Datasets::PS))

See Also:

Instance Method Summary collapse

Instance Method Details

#to_mxml(beats_per_bar, ticks_per_beat, beat_type: nil, bpm: nil, title: nil, creators: nil, encoding_date: nil, parts:, logger: nil, do_log: nil) ⇒ Musa::MusicXML::Builder::ScorePartwise

Converts score to MusicXML.

Creates complete MusicXML document with metadata, parts, measures, notes, rests, and dynamics markings.

Examples:

Simple piano score

score = Musa::Datasets::Score.new
score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))

mxml = score.to_mxml(
  4, 24,
  bpm: 120,
  title: 'Invention',
  creators: { composer: 'J.S. Bach' },
  parts: { piano: { name: 'Piano', clefs: { g: 2, f: 4 } } }
)

String quartet

score = Musa::Datasets::Score.new
score.at(1r, add: { instrument: :vln1, pitch: 67, duration: 1.0 }.extend(Musa::Datasets::PDV))
score.at(1r, add: { instrument: :vln2, pitch: 64, duration: 1.0 }.extend(Musa::Datasets::PDV))
score.at(1r, add: { instrument: :vla, pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
score.at(1r, add: { instrument: :vc, pitch: 48, duration: 1.0 }.extend(Musa::Datasets::PDV))

mxml = score.to_mxml(
  4, 24,
  parts: {
    vln1: { name: 'Violin I', abbreviation: 'Vln. I', clefs: { g: 2 } },
    vln2: { name: 'Violin II', abbreviation: 'Vln. II', clefs: { g: 2 } },
    vla: { name: 'Viola', abbreviation: 'Vla.', clefs: { c: 3 } },
    vc: { name: 'Cello', abbreviation: 'Vc.', clefs: { f: 4 } }
  }
)

Export to file

score = Musa::Datasets::Score.new
score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))

mxml = score.to_mxml(4, 24, parts: { piano: { name: 'Piano' } })
File.write('output.musicxml', mxml.to_xml.string)

Parameters:

  • beats_per_bar (Integer)

    time signature numerator (e.g., 4 for 4/4)

  • ticks_per_beat (Integer)

    resolution per beat (typically 24)

  • beat_type (Integer) (defaults to: nil)

    time signature denominator: what figure the beat is (4 for a quarter, 8 for an eighth). Defaults to 4. The sequencer does not know this and does not need to -- it counts bars and ticks -- but the notation cannot be written without it: it is what makes 6/8 a 6/8 and not a 6/4, and what names every figure.

  • bpm (Integer) (defaults to: nil)

    tempo in beats per minute (default: 90)

  • title (String) (defaults to: nil)

    work title (default: 'Untitled')

  • creators (Hash{Symbol => String}) (defaults to: nil)

    creator roles and names (default: { composer: 'Unknown' })

  • encoding_date (DateTime, nil) (defaults to: nil)

    encoding date for metadata

  • parts (Hash{Symbol => Hash})

    part definitions Each part: { name: String, abbreviation: String, clefs: Hash } Clefs: { clef_sign: line_number } (e.g., { g: 2, f: 4 } for piano)

  • logger (Musa::Logger::Logger, nil) (defaults to: nil)

    logger for debugging

  • do_log (Boolean, nil) (defaults to: nil)

    enable logging output

Returns:

Raises:

  • (ArgumentError)


161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
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
250
251
252
253
# File 'lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb', line 161

def to_mxml(beats_per_bar, ticks_per_beat,
            beat_type: nil,
            bpm: nil,
            title: nil,
            creators: nil,
            encoding_date: nil,
            parts:,
            logger: nil,
            do_log: nil)

  beat_type ||= 4
  bpm ||= 90

  # THE TWO UNITS. A duration in this library is a fraction of a BAR --
  # `1r` is a bar, and that is what the sequencer counts. The name a
  # duration is written with is a fraction of a WHOLE NOTE. A bar measures
  # `beats_per_bar / beat_type` whole notes, and the two readings coincide
  # only in 4/4, which is why they were the same number for so long and
  # why nothing outside 4/4 came out right (issue #70).
  #
  # Everything about time here is in bars; `bar_duration` is what the
  # notation side multiplies by, and nothing else.
  bar_duration = Rational(beats_per_bar, beat_type)

  # A beat is `4 / beat_type` quarters, so a quarter holds this many of the
  # sequencer's ticks. It has to be a whole number of them: MusicXML has no
  # way to say "half a division".
  divisions_per_quarter = Rational(ticks_per_beat * beat_type, 4)

  raise ArgumentError,
        "A quarter note is #{divisions_per_quarter} ticks in #{beats_per_bar}/#{beat_type} " \
        "with #{ticks_per_beat} ticks per beat, and MusicXML divisions must be whole. " \
        "Use a ticks_per_beat that is a multiple of #{Rational(4, beat_type).numerator}." \
    unless divisions_per_quarter.denominator == 1

  divisions_per_quarter = divisions_per_quarter.to_i
  title ||= 'Untitled'
  creators ||= { composer: 'Unknown' }

  if logger.nil?
    logger = Musa::Logger::Logger.new
    logger.debug! if do_log
  end

  do_log ||= nil

  mxml = Musa::MusicXML::Builder::ScorePartwise.new do |_|
    _.work_title title
    _.creators **creators
    _.encoding_date encoding_date if encoding_date

    parts.each_pair do |id, part_info|
      _.part id,
             name: part_info&.[](:name),
             abbreviation: part_info&.[](:abbreviation) do |_|

        _.measure do |_|
          _.attributes do |_|
            # MusicXML counts divisions per QUARTER NOTE, and a beat is a
            # quarter only when beat_type is 4. In 6/8 with 24 ticks to the
            # beat there are 48 of them in a quarter, not 24.
            _.divisions divisions_per_quarter

            i = 0
            (part_info&.[](:clefs) || { g: 2 }).each_pair do |clef, line|
              i += 1
              _.clef i, sign: clef.upcase, line: line
              _.time i, beats: beats_per_bar, beat_type: beat_type
            end
          end

          _.metronome placement: 'above', beat_unit: 'quarter', per_minute: bpm
        end
      end
    end
  end

  if do_log
    logger.debug ""
    logger.debug"score.to_mxml log:"
    logger.debug"------------------"
  end

  parts.each_key do |part_id|
    fill_part mxml.parts[part_id],
              beats_per_bar * ticks_per_beat,
              bar_duration,
              (parts.size > 1 ? part_id : nil),
              logger, do_log
  end

  mxml
end