Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: remove NoNote NoInterval and fix typo #412

Merged
merged 5 commits into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/flat-games-sneeze.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,12 @@ Example:
Chord.get("Cmaj7/B");
Chord.get("Eb/D");
```

### Feature: chord `notes`

Now `notes` property of a chord are always pitch classes, there's a new function to get the actual notes:

```js
Chord.notes("Cmaj7", "C4"); // => ['C4', 'E4', 'G4', 'B4']
Chord.notes("maj7", "D5"); // => ['D5', 'F#5', 'A5', 'C#6']
```
6 changes: 6 additions & 0 deletions .changeset/lovely-maps-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@tonaljs/interval": major
"tonal": major
---

Fix typo (breaking change): `substract` is now `subtract`
7 changes: 7 additions & 0 deletions .changeset/mean-dryers-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@tonaljs/pitch-interval": major
"tonal": major
"@tonaljs/core": major
---

Breaking change: remove `NoInterval` interface. Return `Interval` type (with `emtpy: true`) when parsing invalid intervals.
7 changes: 7 additions & 0 deletions .changeset/red-bags-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@tonaljs/pitch-note": major
"tonal": major
"@tonaljs/core": major
---

Breaking change: `NoNote` interface is removed. Always return `Note` type (with `empty: true`) when parsing invalid notes.
5 changes: 5 additions & 0 deletions .changeset/sweet-ants-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tonaljs/pitch": patch
---

Improve `isPitch` detection to accept `NaN` as values.
2 changes: 1 addition & 1 deletion docs/migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Lot of changes, but as a positive side effect, the library API surface is smalle
Now functions falls in two catergories:

- **Parsers**: takes a name of something (string) and return an object with properties. Examples of that functions are: note, interval, pcset, scaleType, chordType, scale, chord, mode. All of the returning objects has the properties `empty` (boolean) and name (string, "" indicating _no value_)
- **Operations**: takes one or more names and return a new name. It always work with strings (no objects). Invalid results are represented with empty strings "". Examples: transpose, distance, substract
- **Operations**: takes one or more names and return a new name. It always work with strings (no objects). Invalid results are represented with empty strings "". Examples: transpose, distance, subtract

### Utilility functions removed or made private

Expand Down
4 changes: 2 additions & 2 deletions packages/chord/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
all as chordTypes,
get as getChordType,
} from "@tonaljs/chord-type";
import { substract } from "@tonaljs/interval";
import { subtract } from "@tonaljs/interval";
import { isSubsetOf, isSupersetOf } from "@tonaljs/pcset";
import {
distance,
Expand Down Expand Up @@ -155,7 +155,7 @@ export function getChord(
intervals.shift();
}
} else if (hasBass) {
const ivl = substract(distance(tonic.pc, bass.pc), "8P");
const ivl = subtract(distance(tonic.pc, bass.pc), "8P");
if (ivl) intervals.unshift(ivl);
}

Expand Down
6 changes: 3 additions & 3 deletions packages/core/test/tonal.note.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ describe("note", () => {
expect(height("C-4 D-4 E-4 F-4 G-4")).toEqual([-36, -34, -32, -31, -29]);
expect(height("C D E F G")).toEqual([-1188, -1186, -1184, -1183, -1181]);
expect(height("Cb4 Cbb4 Cbbb4 B#4 B##4 B###4")).toEqual(
height("B3 Bb3 Bbb3 C5 C#5 C##5")
height("B3 Bb3 Bbb3 C5 C#5 C##5"),
);
expect(height("Cb Cbb Cbbb B# B## B###")).toEqual(
height("B Bb Bbb C C# C##")
height("B Bb Bbb C C# C##"),
);
});

Expand All @@ -63,7 +63,7 @@ describe("note", () => {
expect(note("F9").freq).toEqual(11175.303405856126);
expect(note("C-4").freq).toEqual(1.0219748644554634);
expect(note("C").freq).toEqual(null);
expect(note("x").freq).toEqual(undefined);
expect(note("x").freq).toEqual(null);
});
});

Expand Down
6 changes: 3 additions & 3 deletions packages/interval/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ Add two intervals:
Interval.add("3m", "5P"); // => "7m"
```

#### `substract(min: string, sub: string) => string`
#### `subtract(min: string, sub: string) => string`

Substract two intervals:

```js
substract("5P", "3M"); // => '3m'
substract("3M", "5P"); // => '-3m'
subtract("5P", "3M"); // => '3m'
subtract("3M", "5P"); // => '-3m'
```
10 changes: 5 additions & 5 deletions packages/interval/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,12 @@ export const addTo = (interval: string) => (other: string) =>
* @function
* @param {string} minuendInterval
* @param {string} subtrahendInterval
* @return {string} the substracted interval name
* @return {string} the subtracted interval name
* @example
* Interval.substract('5P', '3M') // => '3m'
* Interval.substract('3M', '5P') // => '-3m'
* Interval.subtract('5P', '3M') // => '3m'
* Interval.subtract('3M', '5P') // => '-3m'
*/
export const substract = combinator((a, b) => [a[0] - b[0], a[1] - b[1]]);
export const subtract = combinator((a, b) => [a[0] - b[0], a[1] - b[1]]);

export function transposeFifths(
interval: IntervalName,
Expand All @@ -189,7 +189,7 @@ export default {
simplify,
add,
addTo,
substract,
subtract,
transposeFifths,
};

Expand Down
8 changes: 4 additions & 4 deletions packages/interval/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ describe("@tonaljs/interval", () => {
);
});

test("substract", () => {
expect(Interval.substract("5P", "3M")).toEqual("3m");
expect(Interval.substract("3M", "5P")).toEqual("-3m");
expect(Interval.names().map((n) => Interval.substract("5P", n))).toEqual(
test("subtract", () => {
expect(Interval.subtract("5P", "3M")).toEqual("3m");
expect(Interval.subtract("3M", "5P")).toEqual("-3m");
expect(Interval.names().map((n) => Interval.subtract("5P", n))).toEqual(
$("5P 4P 3m 2M 1P -2m -3m"),
);
});
Expand Down
28 changes: 18 additions & 10 deletions packages/pitch-interval/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,21 @@ export interface Interval extends Pitch, NamedPitch {
readonly oct: number;
}

export interface NoInterval extends Partial<Interval> {
readonly empty: true;
readonly name: "";
readonly acc: "";
}

const NoInterval: NoInterval = { empty: true, name: "", acc: "" };
const NoInterval: Interval = Object.freeze({
empty: true,
name: "",
num: NaN,
q: "" as Quality,
type: "" as Type,
step: NaN,
alt: NaN,
dir: NaN as Direction,
simple: NaN,
semitones: NaN,
chroma: NaN,
coord: [] as unknown as IntervalCoordinates,
oct: NaN,
});

// shorthand tonal notation (with quality after number)
const INTERVAL_TONAL_REGEX = "([-+]?\\d+)(d{1,4}|m|M|P|A{1,4})";
Expand All @@ -74,7 +82,7 @@ export function tokenizeInterval(str?: IntervalName): IntervalTokens {
return m[1] ? [m[1], m[2]] : [m[4], m[3]];
}

const cache: { [key in string]: Interval | NoInterval } = {};
const cache: { [key in string]: Interval } = {};

/**
* Get interval properties. It returns an object with:
Expand All @@ -96,7 +104,7 @@ const cache: { [key in string]: Interval | NoInterval } = {};
* interval('P5').semitones // => 7
* interval('m3').type // => 'majorable'
*/
export function interval(src: IntervalLiteral): Interval | NoInterval {
export function interval(src: IntervalLiteral): Interval {
return typeof src === "string"
? cache[src] || (cache[src] = parse(src))
: isPitch(src)
Expand All @@ -108,7 +116,7 @@ export function interval(src: IntervalLiteral): Interval | NoInterval {

const SIZES = [0, 2, 4, 5, 7, 9, 11];
const TYPES = "PMMPPMM";
function parse(str?: string): Interval | NoInterval {
function parse(str?: string): Interval {
const tokens = tokenizeInterval(str);
if (tokens[0] === "") {
return NoInterval;
Expand Down
29 changes: 18 additions & 11 deletions packages/pitch-note/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,22 @@ export interface Note extends Pitch, NamedPitch {
readonly freq: number | null;
}

export interface NoNote extends Partial<Note> {
empty: true;
name: "";
pc: "";
acc: "";
}
const NoNote: NoNote = { empty: true, name: "", pc: "", acc: "" };

const cache: Map<NoteLiteral | undefined, Note | NoNote> = new Map();
const NoNote: Note = Object.freeze({
empty: true,
name: "",
letter: "",
acc: "",
pc: "",
step: NaN,
alt: NaN,
chroma: NaN,
height: NaN,
coord: [] as unknown as PitchCoordinates,
midi: null,
freq: null,
});

const cache: Map<NoteLiteral | undefined, Note> = new Map();

export const stepToLetter = (step: number) => "CDEFGAB".charAt(step);
export const altToAcc = (alt: number): string =>
Expand All @@ -49,7 +56,7 @@ export const accToAlt = (acc: string): number =>
* @example
* note('Bb4') // => { name: "Bb4", midi: 70, chroma: 10, ... }
*/
export function note(src: NoteLiteral): Note | NoNote {
export function note(src: NoteLiteral): Note {
const stringSrc = JSON.stringify(src);

const cached = cache.get(stringSrc);
Expand Down Expand Up @@ -93,7 +100,7 @@ export function coordToNote(noteCoord: PitchCoordinates): Note {
const mod = (n: number, m: number) => ((n % m) + m) % m;

const SEMI = [0, 2, 4, 5, 7, 9, 11];
function parse(noteName: NoteName): Note | NoNote {
function parse(noteName: NoteName): Note {
const tokens = tokenizeNote(noteName);
if (tokens[0] === "" || tokens[3] !== "") {
return NoNote;
Expand Down
2 changes: 1 addition & 1 deletion packages/pitch-note/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe("note", () => {
expect(note("F9").freq).toEqual(11175.303405856126);
expect(note("C-4").freq).toEqual(1.0219748644554634);
expect(note("C").freq).toEqual(null);
expect(note("x").freq).toEqual(undefined);
expect(note("x").freq).toEqual(null);
});
});

Expand Down
4 changes: 3 additions & 1 deletion packages/pitch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ export function isPitch(pitch: unknown): pitch is Pitch {
"step" in pitch &&
typeof pitch.step === "number" &&
"alt" in pitch &&
typeof pitch.alt === "number"
typeof pitch.alt === "number" &&
!isNaN(pitch.step) &&
!isNaN(pitch.alt)
? true
: false;
}
Expand Down
9 changes: 9 additions & 0 deletions packages/pitch/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
coordinates,
height,
isNamedPitch,
isPitch,
midi,
pitch,
} from "./index";
Expand Down Expand Up @@ -60,4 +61,12 @@ describe("@tonaljs/pitch", () => {
expect(pitch([0])).toEqual(C);
expect(pitch([7])).toEqual(Cs);
});

test("isPitch", () => {
expect(isPitch({ step: 0, alt: 0 })).toBe(true);
expect(isPitch({ step: 0, alt: NaN })).toBe(false);
expect(isPitch({ step: NaN, alt: 0 })).toBe(false);
expect(isPitch(undefined)).toBe(false);
expect(isPitch("")).toBe(false);
});
});
2 changes: 1 addition & 1 deletion packages/tonal/__snapshots__/test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ exports[`tonal Modules exports functions 1`] = `
"quality",
"semitones",
"simplify",
"substract",
"subtract",
"transposeFifths",
],
"Key": [
Expand Down
2 changes: 1 addition & 1 deletion packages/tonal/browser/tonal.min.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions packages/tonal/browser/tonal.min.js.map

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions packages/voicing/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Chord from "@tonaljs/chord";
import Interval from "@tonaljs/interval";
import Note from "@tonaljs/note";
import Range from "@tonaljs/range";
import Interval from "@tonaljs/interval";
import VoicingDictionary from "@tonaljs/voicing-dictionary";
import VoiceLeading from "@tonaljs/voice-leading";
import VoicingDictionary from "@tonaljs/voicing-dictionary";

const defaultRange = ["C3", "C5"];
const defaultDictionary = VoicingDictionary.all;
Expand Down Expand Up @@ -44,7 +44,7 @@ function search(
return voicings.reduce((voiced: string[][], voicing: string[]) => {
// transpose intervals relative to first interval (e.g. 3m 5P > 1P 3M)
const relativeIntervals = voicing.map(
(interval) => Interval.substract(interval, voicing[0]) || "",
(interval) => Interval.subtract(interval, voicing[0]) || "",
);
// get enharmonic correct pitch class the bottom note
const bottomPitchClass = Note.transpose(tonic, voicing[0]);
Expand Down
Loading