Pitch.fromString constructor

Pitch.fromString(
  1. String notation
)

Constructs a Pitch from a string (e.g. "C4", "F#5", "Bb3", "C-1").

Accidentals are repeatable ("C##4", "Ebb3", "C###4") and the octave may be negative, so the lowest MIDI note "C-1" round-trips correctly.

Implementation

factory Pitch.fromString(String notation) {
  if (notation.isEmpty) {
    throw ArgumentError('Notation cannot be empty');
  }

  // Extract the base note (first letter)
  final step = notation[0].toUpperCase();
  if (!isValidStep(step)) {
    throw ArgumentError('Invalid note step: $step');
  }

  // Find where the octave number begins
  int octaveStart = notation.length;
  double alter = 0.0;

  // Process accidentals; stop at the first octave character
  // ('-' introduces a negative octave such as "C-1").
  for (int i = 1; i < notation.length; i++) {
    final char = notation[i];
    if (char == '#') {
      alter += 1.0;
    } else if (char == 'b') {
      alter -= 1.0;
    } else if (char == '-' ||
        (char.codeUnitAt(0) >= 0x30 && char.codeUnitAt(0) <= 0x39)) {
      octaveStart = i;
      break;
    } else {
      throw ArgumentError('Invalid character "$char" in notation: $notation');
    }
  }

  // Extract the octave
  if (octaveStart >= notation.length) {
    throw ArgumentError('Missing octave number in notation: $notation');
  }

  final octaveString = notation.substring(octaveStart);
  final octave = int.tryParse(octaveString);
  if (octave == null) {
    throw ArgumentError('Invalid octave number: $octaveString');
  }

  return Pitch.validated(
    step: step,
    octave: octave,
    alter: alter,
    accidentalType: _accidentalTypeForAlter(alter),
  );
}