• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

albertms10 / music_notes / 17009263791

16 Aug 2025 02:06PM UTC coverage: 99.879% (-0.1%) from 100.0%
17009263791

push

github

web-flow
feat(pitch): ✨ allow using ASCII characters for `HelmholtzPitchNotation` (#626)

Signed-off-by: Albert Mañosa <26429103+albertms10@users.noreply.github.com>

11 of 13 new or added lines in 2 files covered. (84.62%)

1651 of 1653 relevant lines covered (99.88%)

2.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.67
/lib/src/note/note.dart
1
import 'package:meta/meta.dart' show immutable;
2
import 'package:music_notes/utils.dart';
3

4
import '../harmony/chord.dart';
5
import '../harmony/chord_pattern.dart';
6
import '../interval/interval.dart';
7
import '../key/key.dart';
8
import '../key/key_signature.dart';
9
import '../key/mode.dart';
10
import '../notation_system.dart';
11
import '../respellable.dart';
12
import '../scalable.dart';
13
import '../tuning/equal_temperament.dart';
14
import 'accidental.dart';
15
import 'base_note.dart';
16
import 'pitch.dart';
17

18
/// A musical note.
19
///
20
/// ---
21
/// See also:
22
/// * [BaseNote].
23
/// * [Accidental].
24
/// * [Pitch].
25
/// * [KeySignature].
26
/// * [Key].
27
@immutable
28
final class Note extends Scalable<Note>
29
    with RespellableScalable<Note>
30
    implements Comparable<Note> {
31
  /// The base note that defines this [Note].
32
  final BaseNote baseNote;
33

34
  /// The accidental that modifies the [baseNote].
35
  final Accidental accidental;
36

37
  /// Creates a new [Note] from [baseNote] and [accidental].
38
  const Note(this.baseNote, [this.accidental = Accidental.natural]);
5✔
39

40
  /// Note C.
41
  static const c = Note(BaseNote.c);
42

43
  /// Note D.
44
  static const d = Note(BaseNote.d);
45

46
  /// Note E.
47
  static const e = Note(BaseNote.e);
48

49
  /// Note F.
50
  static const f = Note(BaseNote.f);
51

52
  /// Note G.
53
  static const g = Note(BaseNote.g);
54

55
  /// Note A.
56
  static const a = Note(BaseNote.a);
57

58
  /// Note B.
59
  static const b = Note(BaseNote.b);
60

61
  /// Parse [source] as a [Note] and return its value.
62
  ///
63
  /// If the [source] string does not contain a valid [Note], a
64
  /// [FormatException] is thrown.
65
  ///
66
  /// Example:
67
  /// ```dart
68
  /// Note.parse('Bb') == Note.b.flat
69
  /// Note.parse('c') == Note.c
70
  /// Note.parse('z') // throws a FormatException
71
  /// ```
72
  factory Note.parse(
1✔
73
    String source, {
74
    List<Parser<Note>> chain = const [
75
      EnglishNoteNotation(),
76
      GermanNoteNotation(),
77
      RomanceNoteNotation(),
78
    ],
79
  }) => chain.parse(source);
1✔
80

81
  /// [Comparator] for [Note]s by fifths distance.
82
  static int compareByFifthsDistance(Note a, Note b) =>
1✔
83
      a.circleOfFifthsDistance.compareTo(b.circleOfFifthsDistance);
3✔
84

85
  /// [Comparator] for [Note]s by closest distance.
86
  static int compareByClosestDistance(Note a, Note b) => compareMultiple([
3✔
87
    () {
1✔
88
      final distance = (a.semitones - b.semitones).abs();
4✔
89

90
      return (distance <= chromaticDivisions - distance)
2✔
91
          ? a.semitones.compareTo(b.semitones)
3✔
92
          : b.semitones.compareTo(a.semitones);
3✔
93
    },
94
    ..._comparators(a, b),
1✔
95
  ]);
96

97
  static List<int Function()> _comparators(Note a, Note b) => [
2✔
98
    () => Scalable.compareEnharmonically(a, b),
2✔
99
    () => a.baseNote.semitones.compareTo(b.baseNote.semitones),
6✔
100
  ];
101

102
  /// The semitones distance of this [Note] relative to [Note.c].
103
  ///
104
  /// Example:
105
  /// ```dart
106
  /// Note.c.semitones == 0
107
  /// Note.d.semitones == 2
108
  /// Note.f.sharp.semitones == 6
109
  /// Note.b.sharp.semitones == 12
110
  /// Note.c.flat.semitones == -1
111
  /// ```
112
  @override
1✔
113
  int get semitones => baseNote.semitones + accidental.semitones;
5✔
114

115
  /// The difference in semitones between this [Note] and [other].
116
  ///
117
  /// Example:
118
  /// ```dart
119
  /// Note.c.difference(Note.d) == 2
120
  /// Note.a.difference(Note.g) == -2
121
  /// Note.e.flat.difference(Note.b.flat) == -5
122
  /// ```
123
  @override
1✔
124
  int difference(Note other) => super.difference(other);
1✔
125

126
  /// This [Note] sharpened by 1 semitone.
127
  ///
128
  /// Example:
129
  /// ```dart
130
  /// Note.c.sharp == const Note(BaseNote.c, Accidental.sharp)
131
  /// Note.a.sharp == const Note(BaseNote.a, Accidental.sharp)
132
  /// ```
133
  Note get sharp => Note(baseNote, accidental + 1);
5✔
134

135
  /// This [Note] flattened by 1 semitone.
136
  ///
137
  /// Example:
138
  /// ```dart
139
  /// Note.e.flat == const Note(BaseNote.e, Accidental.flat)
140
  /// Note.f.flat == const Note(BaseNote.f, Accidental.flat)
141
  /// ```
142
  Note get flat => Note(baseNote, accidental - 1);
5✔
143

144
  /// This [Note] without an accidental (natural).
145
  ///
146
  /// Example:
147
  /// ```dart
148
  /// Note.g.flat.natural == Note.g
149
  /// Note.c.sharp.sharp.natural == Note.c
150
  /// Note.a.natural == Note.a
151
  /// ```
152
  Note get natural => Note(baseNote);
3✔
153

154
  /// The [TonalMode.major] [Key] from this [Note].
155
  ///
156
  /// Example:
157
  /// ```dart
158
  /// Note.c.major == const Key(Note.c, TonalMode.major)
159
  /// Note.e.flat.major == Key(Note.e.flat, TonalMode.major)
160
  /// ```
161
  Key get major => Key(this, TonalMode.major);
2✔
162

163
  /// The [TonalMode.minor] [Key] from this [Note].
164
  ///
165
  /// Example:
166
  /// ```dart
167
  /// Note.d.minor == const Key(Note.d, TonalMode.minor)
168
  /// Note.g.sharp.minor == Key(Note.g.sharp, TonalMode.minor)
169
  /// ```
170
  Key get minor => Key(this, TonalMode.minor);
2✔
171

172
  /// The [ChordPattern.diminishedTriad] on this [Note].
173
  ///
174
  /// Example:
175
  /// ```dart
176
  /// Note.a.diminishedTriad == Chord([Note.a, Note.c, Note.e.flat])
177
  /// Note.b.diminishedTriad == Chord([Note.b, Note.d, Note.f])
178
  /// ```
179
  Chord<Note> get diminishedTriad => ChordPattern.diminishedTriad.on(this);
2✔
180

181
  /// The [ChordPattern.minorTriad] on this [Note].
182
  ///
183
  /// Example:
184
  /// ```dart
185
  /// Note.e.minorTriad == Chord([Note.e, Note.g, Note.b])
186
  /// Note.f.sharp.minorTriad == Chord([Note.f.sharp, Note.a, Note.c.sharp])
187
  /// ```
188
  Chord<Note> get minorTriad => ChordPattern.minorTriad.on(this);
2✔
189

190
  /// The [ChordPattern.majorTriad] on this [Note].
191
  ///
192
  /// Example:
193
  /// ```dart
194
  /// Note.d.majorTriad == Chord([Note.d, Note.f.sharp, Note.a])
195
  /// Note.a.flat.majorTriad == Chord([Note.a.flat, Note.c, Note.e.flat])
196
  /// ```
197
  Chord<Note> get majorTriad => ChordPattern.majorTriad.on(this);
2✔
198

199
  /// The [ChordPattern.augmentedTriad] on this [Note].
200
  ///
201
  /// Example:
202
  /// ```dart
203
  /// Note.d.flat.augmentedTriad == Chord([Note.d.flat, Note.f, Note.a])
204
  /// Note.g.augmentedTriad == Chord([Note.g, Note.b, Note.d.sharp])
205
  /// ```
206
  Chord<Note> get augmentedTriad => ChordPattern.augmentedTriad.on(this);
2✔
207

208
  /// This [Note] respelled by [baseNote] while keeping the same number of
209
  /// [semitones].
210
  ///
211
  /// Example:
212
  /// ```dart
213
  /// Note.c.sharp.respellByBaseNote(BaseNote.d) == Note.d.flat
214
  /// Note.f.respellByBaseNote(BaseNote.e) == Note.e.sharp
215
  /// Note.g.respellByBaseNote(BaseNote.a) == Note.a.flat.flat
216
  /// ```
217
  @override
1✔
218
  Note respellByBaseNote(BaseNote baseNote) {
219
    final rawSemitones = semitones - baseNote.semitones;
3✔
220
    final deltaSemitones =
221
        rawSemitones +
1✔
222
        (rawSemitones.abs() > (chromaticDivisions * 0.5)
3✔
223
            ? chromaticDivisions * -rawSemitones.sign
3✔
224
            : 0);
225

226
    return Note(baseNote, Accidental(deltaSemitones));
2✔
227
  }
228

229
  /// This [Note] respelled by [BaseNote.ordinal] distance while keeping the
230
  /// same number of [semitones].
231
  ///
232
  /// Example:
233
  /// ```dart
234
  /// Note.g.flat.respellByOrdinalDistance(-1) == Note.f.sharp
235
  /// Note.e.sharp.respellByOrdinalDistance(2) == Note.g.flat.flat
236
  /// ```
237
  @override
1✔
238
  Note respellByOrdinalDistance(int distance) =>
239
      respellByBaseNote(BaseNote.fromOrdinal(baseNote.ordinal + distance));
5✔
240

241
  /// This [Note] respelled upwards while keeping the same number of
242
  /// [semitones].
243
  ///
244
  /// Example:
245
  /// ```dart
246
  /// Note.g.sharp.respelledUpwards == Note.a.flat
247
  /// Note.e.sharp.respelledUpwards == Note.f
248
  /// ```
249
  @override
1✔
250
  Note get respelledUpwards => super.respelledUpwards;
1✔
251

252
  /// This [Note] respelled downwards while keeping the same number of
253
  /// [semitones].
254
  ///
255
  /// Example:
256
  /// ```dart
257
  /// Note.g.flat.respelledDownwards == Note.f.sharp
258
  /// Note.c.respelledDownwards == Note.b.sharp
259
  /// ```
260
  @override
1✔
261
  Note get respelledDownwards => super.respelledDownwards;
1✔
262

263
  /// This [Note] respelled by [accidental] while keeping the same number of
264
  /// [semitones].
265
  ///
266
  /// When no respelling is possible with [accidental], the next closest
267
  /// spelling is returned.
268
  ///
269
  /// Example:
270
  /// ```dart
271
  /// Note.e.flat.respellByAccidental(Accidental.sharp) == Note.d.sharp
272
  /// Note.b.respellByAccidental(Accidental.flat) == Note.c.flat
273
  /// Note.g.respellByAccidental(Accidental.sharp) == Note.f.sharp.sharp
274
  /// ```
275
  @override
1✔
276
  Note respellByAccidental(Accidental accidental) {
277
    final baseNote = BaseNote.fromSemitones(semitones - accidental.semitones);
4✔
278
    if (baseNote != null) return Note(baseNote, accidental);
1✔
279

280
    if (accidental.isNatural) {
1✔
281
      return respellByAccidental(Accidental(this.accidental.semitones.sign));
5✔
282
    }
283

284
    return respellByAccidental(accidental.incrementBy(1));
2✔
285
  }
286

287
  /// This [Note] with the simplest [Accidental] spelling while keeping the
288
  /// same number of [semitones].
289
  ///
290
  /// Example:
291
  /// ```dart
292
  /// Note.e.sharp.respelledSimple == Note.f
293
  /// Note.d.flat.flat.respelledSimple == Note.c
294
  /// Note.f.sharp.sharp.sharp.respelledSimple == Note.g.sharp
295
  /// ```
296
  @override
1✔
297
  Note get respelledSimple => super.respelledSimple;
1✔
298

299
  /// This [Note] positioned in the given [octave] as a [Pitch].
300
  ///
301
  /// Example:
302
  /// ```dart
303
  /// Note.c.inOctave(3) == const Pitch(Note.c, octave: 3)
304
  /// Note.a.flat.inOctave(2) == Pitch(Note.a.flat, octave: 2)
305
  /// ```
306
  Pitch inOctave(int octave) => Pitch(this, octave: octave);
2✔
307

308
  /// The circle of fifths starting from this [Note] split by sharps (`up`) and
309
  /// flats (`down`).
310
  ///
311
  /// Example:
312
  /// ```dart
313
  /// Note.c.splitCircleOfFifths.up.take(6).toList()
314
  ///   == [Note.g, Note.d, Note.a, Note.e, Note.b, Note.f.sharp]
315
  ///
316
  /// Note.c.splitCircleOfFifths.down.take(4).toList()
317
  ///   == [Note.f, Note.b.flat, Note.e.flat, Note.a.flat]
318
  ///
319
  /// Note.a.splitCircleOfFifths.up.take(4).toList()
320
  ///   == [Note.e, Note.b, Note.f.sharp, Note.c.sharp]
321
  /// ```
322
  /// ---
323
  /// See also:
324
  /// * [circleOfFifths] for a continuous list version of [splitCircleOfFifths].
325
  ({Iterable<Note> up, Iterable<Note> down}) get splitCircleOfFifths => (
1✔
326
    up: Interval.P5.circleFrom(this).skip(1),
2✔
327
    down: Interval.P4.circleFrom(this).skip(1),
2✔
328
  );
329

330
  /// The continuous circle of fifths up to [distance] including this [Note],
331
  /// from flats to sharps.
332
  ///
333
  /// Example:
334
  /// ```dart
335
  /// Note.c.circleOfFifths(distance: 3)
336
  ///   == [Note.e.flat, Note.b.flat, Note.f, Note.c, Note.g, Note.d, Note.a]
337
  ///
338
  /// Note.a.circleOfFifths(distance: 3)
339
  ///   == [Note.c, Note.g, Note.d, Note.a, Note.e, Note.b, Note.f.sharp]
340
  /// ```
341
  ///
342
  /// It is equivalent to sorting an array of the same [Note]s using the
343
  /// [compareByFifthsDistance] comparator:
344
  ///
345
  /// ```dart
346
  /// Note.c.circleOfFifths(distance: 3)
347
  ///   == ScalePattern.dorian.on(Note.c).degrees.skip(1)
348
  ///        .sorted(Note.compareByFifthsDistance)
349
  /// ```
350
  /// ---
351
  /// See also:
352
  /// * [splitCircleOfFifths] for a different representation of the same
353
  ///   circle of fifths.
354
  List<Note> circleOfFifths({int distance = chromaticDivisions ~/ 2}) {
1✔
355
    final (:down, :up) = splitCircleOfFifths;
1✔
356

357
    return [
1✔
358
      ...down.take(distance).toList(growable: false).reversed,
3✔
359
      this,
1✔
360
      ...up.take(distance),
1✔
361
    ];
362
  }
363

364
  /// The distance in relation to the circle of fifths.
365
  ///
366
  /// Example:
367
  /// ```dart
368
  /// Note.c.circleOfFifthsDistance == 0
369
  /// Note.d.circleOfFifthsDistance == 2
370
  /// Note.a.flat.circleOfFifthsDistance == -4
371
  /// ```
372
  int get circleOfFifthsDistance => Note.c.fifthsDistanceWith(this);
2✔
373

374
  /// The fifths distance between this [Note] and [other].
375
  ///
376
  /// Example:
377
  /// ```dart
378
  /// Note.c.fifthsDistanceWith(Note.e.flat) == -3
379
  /// Note.f.sharp.fifthsDistanceWith(Note.b) == -1
380
  /// Note.a.flat.fifthsDistanceWith(Note.c.sharp) == 11
381
  /// ```
382
  int fifthsDistanceWith(Note other) =>
1✔
383
      Interval.P5.circleDistance(from: this, to: other).$1;
1✔
384

385
  /// The [Interval] between this [Note] and [other].
386
  ///
387
  /// Example:
388
  /// ```dart
389
  /// Note.c.interval(Note.d) == Interval.m2
390
  /// Note.d.interval(Note.a.flat) == Interval.d5
391
  /// ```
392
  @override
1✔
393
  Interval interval(Note other) => Interval.fromSizeAndSemitones(
1✔
394
    baseNote.intervalSize(other.baseNote),
3✔
395
    difference(other) % chromaticDivisions,
2✔
396
  );
397

398
  /// Transposes this [Note] by [interval].
399
  ///
400
  /// Example:
401
  /// ```dart
402
  /// Note.c.transposeBy(Interval.tritone) == Note.f.sharp
403
  /// Note.a.transposeBy(-Interval.M2) == Note.g
404
  /// ```
405
  @override
1✔
406
  Note transposeBy(Interval interval) {
407
    final transposedBaseNote = baseNote.transposeBySize(interval.size);
3✔
408
    final positiveDifference = interval.isDescending
1✔
409
        ? transposedBaseNote.positiveDifference(baseNote)
2✔
410
        : baseNote.positiveDifference(transposedBaseNote);
2✔
411

412
    final accidentalSemitones =
413
        (accidental.semitones * interval.size.sign) +
6✔
414
        ((interval.semitones * interval.size.sign) - positiveDifference);
5✔
415
    final semitonesOctaveMod =
416
        accidentalSemitones -
1✔
417
        chromaticDivisions * ((interval.size.abs() - 1) ~/ 7);
5✔
418

419
    return Note(
1✔
420
      transposedBaseNote,
421
      Accidental(semitonesOctaveMod * interval.size.sign),
4✔
422
    );
423
  }
424

425
  /// The string representation of this [Note] based on [formatter].
426
  ///
427
  /// Example:
428
  /// ```dart
429
  /// Note.d.flat.toString() == 'D♭'
430
  /// Note.d.flat.toString(formatter: const RomanceNoteNotation()) == 'Re♭'
431
  /// Note.d.flat.toString(formatter: const GermanNoteNotation()) == 'Des'
432
  /// ```
433
  @override
1✔
434
  String toString({
435
    Formatter<Note> formatter = const EnglishNoteNotation(),
436
  }) => formatter.format(this);
1✔
437

438
  @override
1✔
439
  bool operator ==(Object other) =>
440
      other is Note &&
1✔
441
      baseNote == other.baseNote &&
3✔
442
      accidental == other.accidental;
3✔
443

444
  @override
1✔
445
  int get hashCode => Object.hash(baseNote, accidental);
3✔
446

447
  @override
1✔
448
  int compareTo(Note other) => compareMultiple(_comparators(this, other));
2✔
449
}
450

451
/// The [NotationSystem] interface for [Note].
452
abstract interface class NoteNotation extends NotationSystem<Note> {
453
  /// The [BaseNote] notation system used to format the [Note.baseNote].
454
  final NotationSystem<BaseNote> baseNoteNotation;
455

456
  /// The [Accidental] notation system used to format the [Note.accidental].
457
  final NotationSystem<Accidental> accidentalNotation;
458

459
  /// Creates a new [NoteNotation].
460
  const NoteNotation({
5✔
461
    this.baseNoteNotation = const EnglishBaseNoteNotation(),
462
    this.accidentalNotation = const SymbolAccidentalNotation(),
463
  });
464
}
465

466
/// The English notation system for [Note].
467
final class EnglishNoteNotation extends NoteNotation {
468
  /// Creates a new [EnglishNoteNotation].
469
  const EnglishNoteNotation({
3✔
470
    super.baseNoteNotation = const EnglishBaseNoteNotation(),
471
    super.accidentalNotation = const SymbolAccidentalNotation(
472
      showNatural: false,
473
    ),
474
  });
475

476
  /// The [EnglishNoteNotation] format variant that shows the
477
  /// [Accidental.natural] accidental.
478
  static const showNatural = EnglishNoteNotation(
479
    accidentalNotation: SymbolAccidentalNotation(),
480
  );
481

482
  /// Creates a new [EnglishNoteNotation] using ASCII characters.
483
  const EnglishNoteNotation.ascii({
1✔
484
    super.baseNoteNotation = const EnglishBaseNoteNotation(),
NEW
485
  }) : super(
×
486
         accidentalNotation: const SymbolAccidentalNotation.ascii(
487
           showNatural: false,
488
         ),
489
       );
490

491
  @override
1✔
492
  String format(Note note) =>
493
      note.baseNote.toString(formatter: baseNoteNotation) +
4✔
494
      note.accidental.toString(formatter: accidentalNotation);
3✔
495

496
  @override
1✔
497
  bool matches(String source) => source.isNotEmpty;
1✔
498

499
  @override
1✔
500
  Note parse(String source) {
501
    // First character is the base note
502
    if (!baseNoteNotation.matches(source[0])) {
3✔
503
      throw FormatException('Invalid BaseNote', source[0], 0);
2✔
504
    }
505
    final baseNote = baseNoteNotation.parse(source[0]);
3✔
506

507
    // Remaining characters are the accidental (if any)
508
    final accidentalSource = source.length > 1 ? source.substring(1) : '';
3✔
509
    final accidental = accidentalNotation.parse(accidentalSource);
2✔
510

511
    return Note(baseNote, accidental);
1✔
512
  }
513
}
514

515
/// The German alphabetic notation system for [Note].
516
///
517
/// See [Versetzungszeichen](https://de.wikipedia.org/wiki/Versetzungszeichen).
518
final class GermanNoteNotation extends NoteNotation {
519
  /// Creates a new [GermanNoteNotation].
520
  const GermanNoteNotation({
4✔
521
    super.baseNoteNotation = const GermanBaseNoteNotation(),
522
    super.accidentalNotation = const GermanAccidentalNotation(),
523
  });
524

525
  @override
1✔
526
  String format(Note note) => switch (note) {
527
    Note(baseNote: BaseNote.b, accidental: Accidental.flat) => 'B',
4✔
528

529
    Note(baseNote: BaseNote.a || BaseNote.e, :final accidental)
2✔
530
        when accidental.isFlat =>
1✔
531
      note.baseNote.toString(formatter: baseNoteNotation) +
4✔
532
          accidental.toString(formatter: accidentalNotation).substring(1),
3✔
533

534
    Note(:final baseNote, :final accidental) =>
535
      baseNote.toString(formatter: baseNoteNotation) +
3✔
536
          accidental.toString(formatter: accidentalNotation),
2✔
537
  };
538

539
  @override
1✔
540
  Note parse(String source) {
541
    if (source.isEmpty) throw FormatException('Invalid Note', source);
2✔
542
    if (source.toLowerCase() == 'b') {
2✔
543
      return const Note(BaseNote.b, Accidental.flat);
544
    }
545

546
    // For A and E with flats, the 'es' suffix is shortened to 's'
547
    if (source.length > 1) {
2✔
548
      final firstChar = source[0];
1✔
549
      if ((firstChar == 'A' ||
1✔
550
              firstChar == 'a' ||
1✔
551
              firstChar == 'E' ||
1✔
552
              firstChar == 'e') &&
1✔
553
          source.substring(1).startsWith('s') &&
2✔
554
          !source.substring(1).startsWith('es')) {
2✔
555
        final baseNote = baseNoteNotation.parse(firstChar);
2✔
556
        final accidental = accidentalNotation.parse('e${source.substring(1)}');
4✔
557
        return Note(baseNote, accidental);
1✔
558
      }
559
    }
560

561
    // Standard parsing: first character is base note, rest is accidental
562
    final baseNote = baseNoteNotation.parse(source[0]);
3✔
563
    final accidentalSource = source.length > 1 ? source.substring(1) : '';
3✔
564
    final accidental = accidentalNotation.parse(accidentalSource);
2✔
565

566
    return Note(baseNote, accidental);
1✔
567
  }
568
}
569

570
/// The Romance alphabetic notation system for [Note].
571
final class RomanceNoteNotation extends NoteNotation {
572
  /// Creates a new [RomanceNoteNotation].
573
  const RomanceNoteNotation({
4✔
574
    super.baseNoteNotation = const RomanceBaseNoteNotation(),
575
    super.accidentalNotation = const SymbolAccidentalNotation(
576
      showNatural: false,
577
    ),
578
  });
579

580
  /// The [RomanceNoteNotation] format variant that shows the
581
  /// [Accidental.natural] accidental.
582
  static const showNatural = RomanceNoteNotation(
583
    accidentalNotation: SymbolAccidentalNotation(),
584
  );
585

586
  /// Creates a new [RomanceNoteNotation] using ASCII characters.
587
  const RomanceNoteNotation.ascii({
1✔
588
    super.baseNoteNotation = const RomanceBaseNoteNotation(),
NEW
589
  }) : super(
×
590
         accidentalNotation: const SymbolAccidentalNotation.ascii(
591
           showNatural: false,
592
         ),
593
       );
594

595
  @override
1✔
596
  String format(Note note) =>
597
      note.baseNote.toString(formatter: baseNoteNotation) +
4✔
598
      note.accidental.toString(formatter: accidentalNotation);
3✔
599

600
  @override
1✔
601
  Note parse(String source) {
602
    if (source.isEmpty) throw FormatException('Invalid Note', source);
2✔
603

604
    // Find where the base note ends and accidental begins
605
    // Romance base notes can be 2-3 characters
606
    var baseNoteStr = '';
607
    var accidentalStr = '';
608

609
    const baseNotes = ['Do', 'Re', 'Mi', 'Fa', 'Sol', 'La', 'Si'];
610

611
    for (final baseNoteOption in baseNotes) {
2✔
612
      if (source.toLowerCase().startsWith(baseNoteOption.toLowerCase())) {
3✔
613
        baseNoteStr = source.substring(0, baseNoteOption.length);
2✔
614
        accidentalStr = source.substring(baseNoteOption.length);
2✔
615
        break;
616
      }
617
    }
618

619
    if (baseNoteStr.isEmpty) throw FormatException('Invalid Note', source);
2✔
620

621
    final baseNote = baseNoteNotation.parse(baseNoteStr);
2✔
622
    final accidental = accidentalNotation.parse(accidentalStr);
2✔
623

624
    return Note(baseNote, accidental);
1✔
625
  }
626
}
627

628
/// A list of notes extension.
629
extension Notes on List<Note> {
630
  /// Flattens all notes on this [List].
631
  List<Note> get flat => map((note) => note.flat).toList();
5✔
632

633
  /// Sharpens all notes on this [List].
634
  List<Note> get sharp => map((note) => note.sharp).toList();
5✔
635

636
  /// Makes all notes on this [List] natural.
637
  List<Note> get natural => map((note) => note.natural).toList();
5✔
638

639
  /// Creates a [Pitch] at [octave] for each [Note] in this list.
640
  ///
641
  /// Example:
642
  /// ```dart
643
  /// [Note.a, Note.c, Note.e].inOctave(4)
644
  ///   == [Note.a.inOctave(4), Note.c.inOctave(4), Note.e.inOctave(4)]
645
  /// ```
646
  List<Pitch> inOctave(int octave) =>
1✔
647
      map((note) => note.inOctave(octave)).toList();
4✔
648
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc