Module: Musa::Neumalang::Neumalang

Extended by:
Neumalang
Included in:
Neumalang
Defined in:
lib/musa-dsl/neumalang/neumalang.rb

Overview

Neumalang parser for parsing neuma text notation to structured objects.

Neumalang is a domain-specific language (DSL) for expressing musical notation in a compact, text-based format. The parser uses Citrus (PEG parser framework) to parse neuma notation strings into structured Ruby objects.

Architecture Overview

Parser Framework

The parser is built on Citrus, a Parsing Expression Grammar (PEG) framework:

  • Grammar defined in .citrus files (terminals, datatypes, neuma, vectors, process, neumalang)
  • Each grammar rule has a corresponding Ruby module for semantic actions
  • Modules transform parse tree into structured neuma objects

Grammar Files

  1. terminals.citrus - Basic tokens (numbers, names, symbols, whitespace)
  2. datatypes.citrus - Data types (strings, numbers, symbols, vectors)
  3. neuma.citrus - Neuma notation (grade, duration, velocity, modifiers)
  4. vectors.citrus - Vector notation (V, PackedV for musical data)
  5. process.citrus - Process notation (P for rhythmic processes)
  6. neumalang.citrus - Top-level grammar combining all elements

Parsing Pipeline

Text → Citrus Parser → Parse Tree → Semantic Modules → Neuma Objects → Series
"0 +2 +2"    ↓              ↓              ↓                ↓
          Grammar      AST nodes    Module.value()    Structured hashes

Neuma Object Structure

Parsed neumas are hashes with :kind key indicating type:

GDVD (Musical Event)

{
  kind: :gdvd,
  gdvd: {
    delta_grade: +2,
    factor_duration: 2,
    modifiers: { tr: true }
  }.extend(Musa::Datasets::GDVd)
}.extend(Musa::Neumas::Neuma)

Serie (Sequential)

{
  kind: :serie,
  serie: [neuma1, neuma2, ...]
}.extend(Musa::Neumas::Neuma::Serie)

Parallel (Polyphonic)

{
  kind: :parallel,
  parallel: [
    { kind: :serie, serie: [...] },
    { kind: :serie, serie: [...] }
  ]
}.extend(Musa::Neumas::Neuma::Parallel)

Commands & Variables

{ kind: :command, command: proc { ... } }
{ kind: :use_variable, use_variable: :@variable_name }
{ kind: :assign_to, assign_to: [:@var1], assign_value: ... }

Values

{ kind: :value, value: 42 }
{ kind: :value, value: :symbol }
{ kind: :value, value: "string" }

Vectors & Processes

{ kind: :v, v: [1, 2, 3].extend(Musa::Datasets::V) }
{ kind: :packed_v, packed_v: {a: 1, b: 2}.extend(Musa::Datasets::PackedV) }
{ kind: :p, p: [values...].extend(Musa::Datasets::P) }

Neumalang Syntax Features

  • Grade notation: 0, +2, -1, ^2 (octave up), v1 (octave down)
  • Duration notation: _, _2, _/2, _3/2 (dotted), _. (dots)
  • Velocity notation: p, pp, mp, mf, f, ff, fff
  • Modifiers: .tr, .mor, .turn, .st, .b (ornaments/articulations)
  • Appogiatura: (+1_/4)+2_ (grace note before main note)
  • Parallel: [0 +2 +4 | +7 +5 +7] (multiple voices)
  • Vectors: <1 2 3> (V), <a: 1 b: 2> (PackedV)
  • Process: << 1 _ _ 2 _ >> (rhythmic process)
  • Commands: { ruby code } (embedded Ruby)
  • Variables: @variable, @var = value
  • Events: event_name(params), event_name(key: value)

Usage

# Parse string
neumas = Musa::Neumalang::Neumalang.parse("0 +2 +2 -1 0")

# Parse with decoder (converts GDVD to GDV)
decoder = NeumaDecoder.new(scale)
gdvs = Musa::Neumalang::Neumalang.parse(
  "0 +2 +2 -1 0",
  decode_with: decoder
)

# Parse file
neumas = Musa::Neumalang::Neumalang.parse_file("melody.neuma")

# Debug parsing
Musa::Neumalang::Neumalang.parse(
  "0 +2 +2 -1 0",
  debug: true  # Dumps parse tree
)

Integration

  • Series: Parsed neumas generate Series for sequential playback
  • Neumas: Output extends Neuma modules for structural operations
  • Datasets: GDVd, V, PackedV, P extensions for musical data
  • Decoders: Optional decode_with parameter for immediate GDV conversion

Examples:

Basic parsing (simple melody)

neumas = Musa::Neumalang::Neumalang.parse("(0) (+2) (+2) (-1) (0)")
# Returns serie of GDVD neuma objects

# Access the series
neumas.i.to_a.size  # => 5
neumas.i.to_a[0][:gdvd][:abs_grade]  # => 0
neumas.i.to_a[1][:gdvd][:delta_grade]  # => 2

With decoder

scale = Musa::Scales::Scales.et12[440.0].major[60]
decoder = Musa::Neumas::Decoders::NeumaDecoder.new(
  scale,
  base_duration: 1/4r
)

gdvs = Musa::Neumalang::Neumalang.parse(
  "(0) (+2) (+2) (-1) (0)",
  decode_with: decoder
)
# Returns serie of GDV events

Complex notation

neumas = Musa::Neumalang::Neumalang.parse(
  "(0) (+2 .tr) (+4 _) (+5 _2) ((^1 _/4) +7 _) (+5) (+4) (+2) (0)"
)

Parallel voices

neumas = Musa::Neumalang::Neumalang.parse(
  "[(0) (+2) (+4) | (+7) (+5) (+7)]"
)

With variables and commands

neumas = Musa::Neumalang::Neumalang.parse(
  "@melody = (0) (+2) (+2) (-1) (0)
   @melody { |gdv| gdv[:duration] *= 2 }"
)

See Also:

Defined Under Namespace

Modules: Parser

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.parse(string_or_file, decode_with: nil, debug: nil) ⇒ Serie, Array

Parses Neumalang notation string or file to structured neuma objects.

Main parsing method. Uses Citrus parser to transform text notation into structured neuma series. Optionally decodes GDVD to GDV using provided decoder.

Parsing Process

  1. Parse text with Citrus grammar
  2. Apply semantic action modules to build neuma structures
  3. Optionally decode GDVD events to GDV with decoder
  4. Return serie of neuma objects

Decoder Integration

If decode_with parameter provided:

  • GDVD events decoded to GDV (absolute format)
  • Requires NeumaDecoder or compatible decoder
  • Useful for immediate conversion to playable events

Debug Mode

If debug: true:

  • Dumps parse tree to stdout
  • Shows grammar rule matches
  • Useful for understanding parsing or debugging grammar

Examples:

Parse simple notation

neumas = Musa::Neumalang::Neumalang.parse("(0) (+2) (+2) (-1) (0)")
# => Serie of GDVD neuma objects

Parse with decoder (immediate GDV conversion)

scale = Musa::Scales::Scales.et12[440.0].major[60]
decoder = Musa::Neumas::Decoders::NeumaDecoder.new(
  scale,
  base_duration: 1/4r
)

gdvs = Musa::Neumalang::Neumalang.parse(
  "(0) (+2) (+2) (-1) (0)",
  decode_with: decoder
)
# => Serie of GDV events ready for playback

Parse file

file = File.open("melody.neuma")
neumas = Musa::Neumalang::Neumalang.parse(file)

Debug parsing

neumas = Musa::Neumalang::Neumalang.parse(
  "(0) (+2) (+2)",
  debug: true
)
# Prints parse tree to stdout

Complex notation

neumas = Musa::Neumalang::Neumalang.parse(
  "[(0) (+2 .tr) (+4 _) | (+7) (+5 .mor) (+7 _)] (+9 _2)"
)
# Parallel voices followed by longer note

Parameters:

  • string_or_file (String, File)

    neuma notation to parse

  • decode_with (Decoder, nil) (defaults to: nil)

    optional decoder for GDVD→GDV conversion

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

    enable parse tree debugging output

Returns:

  • (Serie, Array)

    parsed neuma serie or array of neumas

Raises:

  • (ArgumentError)

    if string_or_file is not String or File

  • (Citrus::ParseError)

    if notation has syntax errors



972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
# File 'lib/musa-dsl/neumalang/neumalang.rb', line 972

def parse(string_or_file, decode_with: nil, debug: nil)
  case string_or_file
  when String
    match = Parser::Grammar::Grammar.parse string_or_file

  when File
    match = Parser::Grammar::Grammar.parse string_or_file.read

  else
    raise ArgumentError, 'Only String or File allowed to be parsed'
  end

  match.dump if debug

  serie = match.value

  if decode_with
    serie.eval do |e|
      if e[:kind] == :gdvd
        decode_with.decode(e[:gdvd])
      else
        raise ArgumentError, "Don't know how to convert #{e} to neumas"
      end
    end
  else
    serie
  end
end

.parse_file(filename, decode_with: nil, debug: nil) ⇒ Serie, Array

Parses Neumalang notation file to structured neuma objects.

Convenience method for parsing files. Opens file and calls parse.

Examples:

Parse neuma file

neumas = Musa::Neumalang::Neumalang.parse_file("melodies/theme.neuma")

Parse with decoder

scale = Musa::Scales::Scales.et12[440.0].major[60]
decoder = Musa::Neumas::Decoders::NeumaDecoder.new(scale)
gdvs = Musa::Neumalang::Neumalang.parse_file(
  "melodies/theme.neuma",
  decode_with: decoder
)

Parameters:

  • filename (String)

    path to neuma notation file

  • decode_with (Decoder, nil) (defaults to: nil)

    optional decoder for GDVD→GDV conversion

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

    enable parse tree debugging output

Returns:

  • (Serie, Array)

    parsed neuma serie or array

Raises:

  • (Errno::ENOENT)

    if file not found

  • (Citrus::ParseError)

    if notation has syntax errors



1026
1027
1028
1029
1030
# File 'lib/musa-dsl/neumalang/neumalang.rb', line 1026

def parse_file(filename, decode_with: nil, debug: nil)
  File.open filename do |file|
    parse file, decode_with: decode_with, debug: debug
  end
end

Instance Method Details

#parse(string_or_file, decode_with: nil, debug: nil) ⇒ Serie, Array

Parses Neumalang notation string or file to structured neuma objects.

Main parsing method. Uses Citrus parser to transform text notation into structured neuma series. Optionally decodes GDVD to GDV using provided decoder.

Parsing Process

  1. Parse text with Citrus grammar
  2. Apply semantic action modules to build neuma structures
  3. Optionally decode GDVD events to GDV with decoder
  4. Return serie of neuma objects

Decoder Integration

If decode_with parameter provided:

  • GDVD events decoded to GDV (absolute format)
  • Requires NeumaDecoder or compatible decoder
  • Useful for immediate conversion to playable events

Debug Mode

If debug: true:

  • Dumps parse tree to stdout
  • Shows grammar rule matches
  • Useful for understanding parsing or debugging grammar

Examples:

Parse simple notation

neumas = Musa::Neumalang::Neumalang.parse("(0) (+2) (+2) (-1) (0)")
# => Serie of GDVD neuma objects

Parse with decoder (immediate GDV conversion)

scale = Musa::Scales::Scales.et12[440.0].major[60]
decoder = Musa::Neumas::Decoders::NeumaDecoder.new(
  scale,
  base_duration: 1/4r
)

gdvs = Musa::Neumalang::Neumalang.parse(
  "(0) (+2) (+2) (-1) (0)",
  decode_with: decoder
)
# => Serie of GDV events ready for playback

Parse file

file = File.open("melody.neuma")
neumas = Musa::Neumalang::Neumalang.parse(file)

Debug parsing

neumas = Musa::Neumalang::Neumalang.parse(
  "(0) (+2) (+2)",
  debug: true
)
# Prints parse tree to stdout

Complex notation

neumas = Musa::Neumalang::Neumalang.parse(
  "[(0) (+2 .tr) (+4 _) | (+7) (+5 .mor) (+7 _)] (+9 _2)"
)
# Parallel voices followed by longer note

Parameters:

  • string_or_file (String, File)

    neuma notation to parse

  • decode_with (Decoder, nil) (defaults to: nil)

    optional decoder for GDVD→GDV conversion

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

    enable parse tree debugging output

Returns:

  • (Serie, Array)

    parsed neuma serie or array of neumas

Raises:

  • (ArgumentError)

    if string_or_file is not String or File

  • (Citrus::ParseError)

    if notation has syntax errors



972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
# File 'lib/musa-dsl/neumalang/neumalang.rb', line 972

def parse(string_or_file, decode_with: nil, debug: nil)
  case string_or_file
  when String
    match = Parser::Grammar::Grammar.parse string_or_file

  when File
    match = Parser::Grammar::Grammar.parse string_or_file.read

  else
    raise ArgumentError, 'Only String or File allowed to be parsed'
  end

  match.dump if debug

  serie = match.value

  if decode_with
    serie.eval do |e|
      if e[:kind] == :gdvd
        decode_with.decode(e[:gdvd])
      else
        raise ArgumentError, "Don't know how to convert #{e} to neumas"
      end
    end
  else
    serie
  end
end

#parse_file(filename, decode_with: nil, debug: nil) ⇒ Serie, Array

Parses Neumalang notation file to structured neuma objects.

Convenience method for parsing files. Opens file and calls parse.

Examples:

Parse neuma file

neumas = Musa::Neumalang::Neumalang.parse_file("melodies/theme.neuma")

Parse with decoder

scale = Musa::Scales::Scales.et12[440.0].major[60]
decoder = Musa::Neumas::Decoders::NeumaDecoder.new(scale)
gdvs = Musa::Neumalang::Neumalang.parse_file(
  "melodies/theme.neuma",
  decode_with: decoder
)

Parameters:

  • filename (String)

    path to neuma notation file

  • decode_with (Decoder, nil) (defaults to: nil)

    optional decoder for GDVD→GDV conversion

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

    enable parse tree debugging output

Returns:

  • (Serie, Array)

    parsed neuma serie or array

Raises:

  • (Errno::ENOENT)

    if file not found

  • (Citrus::ParseError)

    if notation has syntax errors



1026
1027
1028
1029
1030
# File 'lib/musa-dsl/neumalang/neumalang.rb', line 1026

def parse_file(filename, decode_with: nil, debug: nil)
  File.open filename do |file|
    parse file, decode_with: decode_with, debug: debug
  end
end