Skip to content

Commit

Permalink
Merge d5376e7 into 95d14f5
Browse files Browse the repository at this point in the history
  • Loading branch information
albertms10 committed Mar 7, 2024
2 parents 95d14f5 + d5376e7 commit 83334f0
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 54 deletions.
28 changes: 28 additions & 0 deletions lib/src/notation_system.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/// The abstraction of a notation system.
abstract class NotationSystem<T> {
/// Creates a new [NotationSystem].
const NotationSystem();

/// The regular expression to match against a source.
RegExp get regExp;

/// The first match of [regExp] in [source].
RegExpMatch? match(String source) => regExp.firstMatch(source);

/// Parse [match] as a [T].
T parse(RegExpMatch match);
}

/// A notation system List extension.
extension NotationSystemListExtension<T> on List<NotationSystem<T>> {
/// Tries to parse [source] as a [T] in any [NotationSystem].
/// Otherwise, throws a [FormatException].
T parse(String source) {
for (final system in this) {
final match = system.match(source);
if (match != null) return system.parse(match);
}

throw FormatException('Invalid ${T.runtimeType}', source);
}
}
4 changes: 2 additions & 2 deletions lib/src/note/base_note.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ enum BaseNote implements Comparable<BaseNote> {
/// Creates a new [BaseNote] from [semitones].
const BaseNote(this.semitones);

/// Returns a [BaseNote] that matches with [semitones] as in [BaseNote],
/// otherwise returns `null`.
/// Returns a [BaseNote] that matches with [semitones] as in [BaseNote].
/// Otherwise, returns `null`.
///
/// Example:
/// ```dart
Expand Down
112 changes: 60 additions & 52 deletions lib/src/note/pitch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import '../harmony/chord_pattern.dart';
import '../interval/interval.dart';
import '../interval/size.dart';
import '../music.dart';
import '../notation_system.dart';
import '../scalable.dart';
import '../tuning/cent.dart';
import '../tuning/equal_temperament.dart';
Expand Down Expand Up @@ -36,23 +37,6 @@ final class Pitch extends Scalable<Pitch> implements Comparable<Pitch> {
/// Creates a new [Pitch] from [note] and [octave].
const Pitch(this.note, {required this.octave});

static const _superPrime = '′';
static const _superPrimeAlt = "'";
static const _subPrime = '͵';
static const _subPrimeAlt = ',';

static const _primeSymbols = [
_superPrime,
_superPrimeAlt,
_subPrime,
_subPrimeAlt,
];

static final _scientificNotationRegExp = RegExp(r'^(.+?)([-]?\d+)$');
static final _helmholtzNotationRegExp =
RegExp('(^[A-Ga-g${Accidental.symbols.join()}]+)'
'(${[for (final symbol in _primeSymbols) '$symbol+'].join('|')})?\$');

/// Parse [source] as a [Pitch] and return its value.
///
/// If the [source] string does not contain a valid [Pitch], a
Expand All @@ -64,38 +48,11 @@ final class Pitch extends Scalable<Pitch> implements Comparable<Pitch> {
/// Pitch.parse("c'") == Note.c.inOctave(4)
/// Pitch.parse('z') // throws a FormatException
/// ```
factory Pitch.parse(String source) {
final scientificNotationMatch =
_scientificNotationRegExp.firstMatch(source);
if (scientificNotationMatch != null) {
return Pitch(
Note.parse(scientificNotationMatch[1]!),
octave: int.parse(scientificNotationMatch[2]!),
);
}

final helmholtzNotationMatch = _helmholtzNotationRegExp.firstMatch(source);
if (helmholtzNotationMatch != null) {
const middleOctave = 3;
final notePart = helmholtzNotationMatch[1]!;
final primes = helmholtzNotationMatch[2]?.split('');
final octave = notePart[0].isUpperCase
? switch (primes?.first) {
'' || null => middleOctave - 1,
_subPrime || _subPrimeAlt => middleOctave - primes!.length - 1,
_ => throw FormatException('Invalid Pitch', source),
}
: switch (primes?.first) {
'' || null => middleOctave,
_superPrime || _superPrimeAlt => middleOctave + primes!.length,
_ => throw FormatException('Invalid Pitch', source),
};

return Pitch(Note.parse(notePart), octave: octave);
}

throw FormatException('Invalid Pitch', source);
}
factory Pitch.parse(
String source, {
List<PitchNotation> chain = PitchNotation._chain,
}) =>
chain.parse(source);

/// The [octave] that corresponds to the semitones from root height.
///
Expand Down Expand Up @@ -500,7 +457,7 @@ final class Pitch extends Scalable<Pitch> implements Comparable<Pitch> {

/// The abstraction for [Pitch] notation systems.
@immutable
abstract class PitchNotation {
abstract class PitchNotation extends NotationSystem<Pitch> {
/// Creates a new [PitchNotation].
const PitchNotation();

Expand All @@ -510,6 +467,11 @@ abstract class PitchNotation {
/// The Helmholtz [PitchNotation] system.
static const helmholtz = HelmholtzPitchNotation();

static const _chain = [
scientific,
helmholtz,
];

/// The string representation for [pitch].
String pitch(Pitch pitch);
}
Expand All @@ -523,6 +485,15 @@ final class ScientificPitchNotation extends PitchNotation {

@override
String pitch(Pitch pitch) => '${pitch.note}${pitch.octave}';

@override
RegExp get regExp => RegExp(r'^(.+?)([-]?\d+)$');

@override
Pitch parse(RegExpMatch match) => Pitch(
Note.parse(match[1]!),
octave: int.parse(match[2]!),
);
}

/// The Helmholtz [Pitch] notation system.
Expand All @@ -532,17 +503,54 @@ final class HelmholtzPitchNotation extends PitchNotation {
/// Creates a new [HelmholtzPitchNotation].
const HelmholtzPitchNotation();

static const _superPrime = '′';
static const _superPrimeAlt = "'";
static const _subPrime = '͵';
static const _subPrimeAlt = ',';

static const _primeSymbols = [
_superPrime,
_superPrimeAlt,
_subPrime,
_subPrimeAlt,
];

@override
String pitch(Pitch pitch) {
final accidental = pitch.note.accidental;
final accidentalSymbol = accidental.isNatural ? '' : accidental.symbol;

if (pitch.octave >= 3) {
return '${pitch.note.baseNote.name}$accidentalSymbol'
'${Pitch._superPrime * (pitch.octave - 3)}';
'${_superPrime * (pitch.octave - 3)}';
}

return '${pitch.note.baseNote.name.toUpperCase()}$accidentalSymbol'
'${Pitch._subPrime * (pitch.octave - 2).abs()}';
'${_subPrime * (pitch.octave - 2).abs()}';
}

@override
RegExp get regExp => RegExp('(^[A-Ga-g${Accidental.symbols.join()}]+)'
'(${[for (final symbol in _primeSymbols) '$symbol+'].join('|')})?\$');

@override
Pitch parse(RegExpMatch match) {
const middleOctave = 3;
final notePart = match[1]!;
final primesPart = match[2];
final primes = primesPart?.split('');
final octave = notePart[0].isUpperCase
? switch (primes?.first) {
'' || null => middleOctave - 1,
_subPrime || _subPrimeAlt => middleOctave - primes!.length - 1,
_ => throw FormatException('Invalid Pitch', primesPart),
}
: switch (primes?.first) {
'' || null => middleOctave,
_superPrime || _superPrimeAlt => middleOctave + primes!.length,
_ => throw FormatException('Invalid Pitch', primesPart),
};

return Pitch(Note.parse(notePart), octave: octave);
}
}
6 changes: 6 additions & 0 deletions test/src/note/pitch_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1280,4 +1280,10 @@ void main() {
final class _SubPitchNotation extends PitchNotation {
@override
String pitch(Pitch pitch) => throw UnimplementedError();

@override
Pitch parse(RegExpMatch match) => throw UnimplementedError();

@override
RegExp get regExp => throw UnimplementedError();
}

0 comments on commit 83334f0

Please sign in to comment.