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

albertms10 / music_notes / 15889365663

25 Jun 2025 11:32PM UTC coverage: 97.841% (-2.2%) from 100.0%
15889365663

Pull #608

github

web-flow
Merge 542d1fa7c into 5122c56a3
Pull Request #608: refactor: ♻️ rename formatters to `*Notation`

152 of 181 new or added lines in 16 files covered. (83.98%)

3 existing lines in 1 file now uncovered.

1450 of 1482 relevant lines covered (97.84%)

2.03 hits per line

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

94.78
/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 '../music.dart';
11
import '../notation_system.dart';
12
import '../respellable.dart';
13
import '../scalable.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(String source) => Note(
2✔
73
    BaseNote.parse(source[0]),
2✔
74
    Accidental.parse(source.length > 1 ? source.substring(1) : ''),
4✔
75
  );
76

77
  /// [Comparator] for [Note]s by fifths distance.
78
  static int compareByFifthsDistance(Note a, Note b) =>
1✔
79
      a.circleOfFifthsDistance.compareTo(b.circleOfFifthsDistance);
3✔
80

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

86
      return (distance <= chromaticDivisions - distance)
2✔
87
          ? a.semitones.compareTo(b.semitones)
3✔
88
          : b.semitones.compareTo(a.semitones);
3✔
89
    },
90
    ..._comparators(a, b),
1✔
91
  ]);
92

93
  static List<int Function()> _comparators(Note a, Note b) => [
2✔
94
    () => Scalable.compareEnharmonically(a, b),
2✔
95
    () => a.baseNote.semitones.compareTo(b.baseNote.semitones),
6✔
96
  ];
97

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

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

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

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

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

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

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

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

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

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

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

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

222
    return Note(baseNote, Accidental(deltaSemitones));
2✔
223
  }
224

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

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

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

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

276
    if (accidental.isNatural) {
1✔
277
      return respellByAccidental(Accidental(this.accidental.semitones.sign));
5✔
278
    }
279

280
    return respellByAccidental(accidental.incrementBy(1));
2✔
281
  }
282

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

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

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

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

353
    return [
1✔
354
      ...down.take(distance).toList().reversed,
3✔
355
      this,
1✔
356
      ...up.take(distance),
1✔
357
    ];
358
  }
359

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

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

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

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

408
    final accidentalSemitones =
409
        (accidental.semitones * interval.size.sign) +
6✔
410
        ((interval.semitones * interval.size.sign) - positiveDifference);
5✔
411
    final semitonesOctaveMod =
412
        accidentalSemitones -
1✔
413
        chromaticDivisions * ((interval.size.abs() - 1) ~/ 7);
5✔
414

415
    return Note(
1✔
416
      transposedBaseNote,
417
      Accidental(semitonesOctaveMod * interval.size.sign),
4✔
418
    );
419
  }
420

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

434
  @override
1✔
435
  bool operator ==(Object other) =>
436
      other is Note &&
1✔
437
      baseNote == other.baseNote &&
3✔
438
      accidental == other.accidental;
3✔
439

440
  @override
1✔
441
  int get hashCode => Object.hash(baseNote, accidental);
3✔
442

443
  @override
1✔
444
  int compareTo(Note other) => compareMultiple(_comparators(this, other));
2✔
445
}
446

447
/// The English notation system for [Note
448
final class EnglishNoteNotation extends NotationSystem<Note> {
449
  /// The [EnglishBaseNoteNotation] used to format the [Note.baseNote].
450
  final EnglishBaseNoteNotation baseNoteNotation;
451

452
  /// The [SymbolAccidentalNotation] used to format the [Note.accidental].
453
  final SymbolAccidentalNotation accidentalNotation;
454

455
  /// The [EnglishNoteNotation] format variant that shows the
456
  /// [Accidental.natural] accidental.
457
  static const showNatural = EnglishNoteNotation(
458
    accidentalNotation: SymbolAccidentalNotation(),
459
  );
460

461
  /// Creates a new [EnglishNoteNotation].
462
  const EnglishNoteNotation({
3✔
463
    this.baseNoteNotation = const EnglishBaseNoteNotation(),
464
    this.accidentalNotation = const SymbolAccidentalNotation(
465
      showNatural: false,
466
    ),
467
  });
468

469
  @override
1✔
470
  String format(Note note) =>
471
      note.baseNote.toString(formatter: baseNoteNotation) +
4✔
472
      note.accidental.toString(formatter: accidentalNotation);
3✔
473

UNCOV
474
  @override
×
475
  Note parse(String source) {
NEW
476
    throw UnimplementedError();
×
477
  }
478
}
479

480
/// The German alphabetic notation system for [Note].
481
///
482
/// See [Versetzungszeichen](https://de.wikipedia.org/wiki/Versetzungszeichen).
483
final class GermanNoteNotation extends NotationSystem<Note> {
484
  /// The [GermanBaseNoteNotation] used to format the [Note.baseNote].
485
  final GermanBaseNoteNotation baseNoteNotation;
486

487
  /// The [GermanAccidentalNotation] used to format the [Note.accidental].
488
  final GermanAccidentalNotation accidentalNotation;
489

490
  /// Creates a new [GermanNoteNotation].
491
  const GermanNoteNotation({
3✔
492
    this.baseNoteNotation = const GermanBaseNoteNotation(),
493
    this.accidentalNotation = const GermanAccidentalNotation(),
494
  });
495

496
  @override
1✔
497
  String format(Note note) => switch (note) {
498
    Note(baseNote: BaseNote.b, accidental: Accidental.flat) => 'B',
4✔
499

500
    Note(baseNote: BaseNote.a || BaseNote.e, :final accidental)
2✔
501
        when accidental.isFlat =>
1✔
502
      note.baseNote.toString(formatter: baseNoteNotation) +
4✔
503
          accidental.toString(formatter: accidentalNotation).substring(1),
3✔
504

505
    Note(:final baseNote, :final accidental) =>
506
      baseNote.toString(formatter: baseNoteNotation) +
3✔
507
          accidental.toString(formatter: accidentalNotation),
2✔
508
  };
509

UNCOV
510
  @override
×
511
  Note parse(String source) {
NEW
512
    throw UnimplementedError();
×
513
  }
514
}
515

516
/// The Romance alphabetic notation system for [Note].
517
final class RomanceNoteNotation extends NotationSystem<Note> {
518
  /// The [RomanceBaseNoteNotation] used to format the [Note.baseNote].
519
  final RomanceBaseNoteNotation baseNoteNotation;
520

521
  /// The [SymbolAccidentalNotation] used to format the [Note.accidental].
522
  final SymbolAccidentalNotation accidentalNotation;
523

524
  /// The [RomanceNoteNotation] format variant that shows the
525
  /// [Accidental.natural] accidental.
526
  static const showNatural = RomanceNoteNotation(
527
    accidentalNotation: SymbolAccidentalNotation(),
528
  );
529

530
  /// Creates a new [RomanceNoteNotation].
531
  const RomanceNoteNotation({
4✔
532
    this.baseNoteNotation = const RomanceBaseNoteNotation(),
533
    this.accidentalNotation = const SymbolAccidentalNotation(
534
      showNatural: false,
535
    ),
536
  });
537

538
  @override
1✔
539
  String format(Note note) =>
540
      note.baseNote.toString(formatter: baseNoteNotation) +
4✔
541
      note.accidental.toString(formatter: accidentalNotation);
3✔
542

UNCOV
543
  @override
×
544
  Note parse(String source) {
NEW
545
    throw UnimplementedError();
×
546
  }
547
}
548

549
/// A list of notes extension.
550
extension Notes on List<Note> {
551
  /// Flattens all notes on this [List].
552
  List<Note> get flat => map((note) => note.flat).toList();
5✔
553

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

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

560
  /// Creates a [Pitch] at [octave] for each [Note] in this list.
561
  ///
562
  /// Example:
563
  /// ```dart
564
  /// [Note.a, Note.c, Note.e].inOctave(4)
565
  ///   == [Note.a.inOctave(4), Note.c.inOctave(4), Note.e.inOctave(4)]
566
  /// ```
567
  List<Pitch> inOctave(int octave) =>
1✔
568
      map((note) => note.inOctave(octave)).toList();
4✔
569
}
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