diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e76441..47e756d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -127,7 +127,7 @@ jobs: working-directory: tonalis/ts # Approach A: the repo keeps a local `file:` link to @tonalis/music-dsl for monorepo dev; # rewrite it to the published version range only in the publish artifact. - - run: npm pkg set 'dependencies[@tonalis/music-dsl]=^0.1.0' + - run: npm pkg set 'dependencies[@tonalis/music-dsl]=^0.1.1' working-directory: tonalis/ts - run: npm publish --access public working-directory: tonalis/ts diff --git a/README.md b/README.md index f6fc3ba..f70e836 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The same language is implemented three times — in **Python**, **TypeScript**, three pass two shared, language-agnostic conformance suites: the **lead-sheet** suite (255 cases under [`conformance/tonalis/`](conformance/tonalis/), specified in [`conformance/tonalis/SPEC.md`](conformance/tonalis/SPEC.md)) and the **music-DSL scale/encode** -suite (429 cases under [`conformance/music-dsl/`](conformance/music-dsl/), specified in +suite (433 cases under [`conformance/music-dsl/`](conformance/music-dsl/), specified in [`conformance/music-dsl/SPEC-scales.md`](conformance/music-dsl/SPEC-scales.md)). ## What it does diff --git a/conformance/music-dsl/cases/numeric/from_chord.json b/conformance/music-dsl/cases/numeric/from_chord.json new file mode 100644 index 0000000..0c0936b --- /dev/null +++ b/conformance/music-dsl/cases/numeric/from_chord.json @@ -0,0 +1,65 @@ +[ + { + "name": "numeric/from-chord/Csus-in-C", + "op": "numeric_from_chord", + "args": { + "key_root": "C", + "chord": "Csus", + "substitution": false + }, + "expect": { + "model": { + "numeric": { + "root": "I", + "triad": "sus", + "seventh": "", + "extensions": [], + "harmonic_function": "Tonic", + "substitution": false + } + } + } + }, + { + "name": "numeric/from-chord/Csus2-in-C", + "op": "numeric_from_chord", + "args": { + "key_root": "C", + "chord": "Csus2", + "substitution": false + }, + "expect": { + "model": { + "numeric": { + "root": "I", + "triad": "sus2", + "seventh": "", + "extensions": [], + "harmonic_function": "Tonic", + "substitution": false + } + } + } + }, + { + "name": "numeric/from-chord/Csus4-in-C", + "op": "numeric_from_chord", + "args": { + "key_root": "C", + "chord": "Csus4", + "substitution": false + }, + "expect": { + "model": { + "numeric": { + "root": "I", + "triad": "sus4", + "seventh": "", + "extensions": [], + "harmonic_function": "Tonic", + "substitution": false + } + } + } + } +] diff --git a/conformance/music-dsl/cases/numeric/parse.json b/conformance/music-dsl/cases/numeric/parse.json index 20f2b11..f0c22df 100644 --- a/conformance/music-dsl/cases/numeric/parse.json +++ b/conformance/music-dsl/cases/numeric/parse.json @@ -229,7 +229,7 @@ "model": { "numeric": { "numerator": { - "root": "bvii", + "root": "bVII", "triad": "sus", "seventh": "", "extensions": [], @@ -254,7 +254,9 @@ "root": "iv", "triad": "-", "seventh": "7", - "extensions": ["alt"], + "extensions": [ + "alt" + ], "harmonic_function": "Subdominant", "substitution": false }, @@ -273,7 +275,7 @@ "model": { "numeric": { "numerator": { - "root": "iii", + "root": "III", "triad": "sus2", "seventh": "^7", "extensions": [], @@ -284,5 +286,27 @@ } } } + }, + { + "name": "numeric/parse/mOrM-II-sus4", + "op": "parse_numeric", + "args": { + "input": "II7sus4" + }, + "expect": { + "model": { + "numeric": { + "numerator": { + "root": "II", + "triad": "sus4", + "seventh": "7", + "extensions": [], + "harmonic_function": "Tonic", + "substitution": false + }, + "denominator": null + } + } + } } ] diff --git a/conformance/music-dsl/runners/python/bless_numeric.py b/conformance/music-dsl/runners/python/bless_numeric.py index f36fdc2..e5cffe8 100644 --- a/conformance/music-dsl/runners/python/bless_numeric.py +++ b/conformance/music-dsl/runners/python/bless_numeric.py @@ -93,24 +93,24 @@ def _modulate_case(name: str, semitones: int, note: str) -> dict: def _is_diatonic_case(name: str, root: str, scale: str, chord: str) -> dict: """Build an is_diatonic case.""" - result = is_diatonic(Notes(root), Scales[scale], Chord(chord)) - return { - "name": name, - "op": "is_diatonic", - "args": {"root": root, "scale": scale, "chord": chord}, - "expect": {"value": result}, - } + args = {"root": root, "scale": scale, "chord": chord} + try: + result = is_diatonic(Notes(root), Scales[scale], Chord(chord)) + expect = {"value": result} + except InvalidChordStringException: + expect = {"error": True} + return {"name": name, "op": "is_diatonic", "args": args, "expect": expect} def _hf_in_key_case(name: str, key_root: str, key_is_minor: bool, chord: str) -> dict: """Build a harmonic_function_in_key case.""" - result = harmonic_function_in_key(Notes(key_root), key_is_minor, Chord(chord)).name - return { - "name": name, - "op": "harmonic_function_in_key", - "args": {"key_root": key_root, "key_is_minor": key_is_minor, "chord": chord}, - "expect": {"value": result}, - } + args = {"key_root": key_root, "key_is_minor": key_is_minor, "chord": chord} + try: + result = harmonic_function_in_key(Notes(key_root), key_is_minor, Chord(chord)).name + expect = {"value": result} + except InvalidChordStringException: + expect = {"error": True} + return {"name": name, "op": "harmonic_function_in_key", "args": args, "expect": expect} def _chord_in_key_case(name: str, numeric: str, key_root: str) -> dict: @@ -136,10 +136,24 @@ def _chord_in_key_case(name: str, numeric: str, key_root: str) -> dict: ("numeric/parse/bVI^7", "bVI^7"), ("numeric/parse/sharp-ivo7", "#ivo7"), ("numeric/parse/sV7", "sV7"), + ("numeric/parse/mOrM-bVI-minor", "bVI-7"), + ("numeric/parse/mOrM-II-minor", "II-7"), + ("numeric/parse/mOrM-sII-dim", "#IIo7"), + ("numeric/parse/mOrM-V-halfdim", "Vh^7"), + ("numeric/parse/mOrM-bVII-sus", "bVIIsus"), + ("numeric/parse/mOrM-IV-minor-alt", "IV-7alt"), + ("numeric/parse/mOrM-III-sus2", "IIIsus2^7"), + ("numeric/parse/mOrM-II-sus4", "II7sus4"), ] parse_cases = [_numeric_case(name, input_str) for name, input_str in parse_inputs] +from_chord_cases = [ + _numeric_from_chord_case("numeric/from-chord/Csus-in-C", "C", "Csus", False), + _numeric_from_chord_case("numeric/from-chord/Csus2-in-C", "C", "Csus2", False), + _numeric_from_chord_case("numeric/from-chord/Csus4-in-C", "C", "Csus4", False), +] + # --------------------------------------------------------------------------- # NUMERIC / SLASH # --------------------------------------------------------------------------- @@ -182,28 +196,28 @@ def _chord_in_key_case(name: str, numeric: str, key_root: str) -> dict: _ab_with_sub = next(c for c in substitution_cases if c["name"] == "substitution/Ab-m7-in-C-with-sub") assert _ab_no_sub["expect"].get("model", {}).get("numeric", {}).get("root") == \ _ab_with_sub["expect"].get("model", {}).get("numeric", {}).get("root"), \ - f"BLESS FAILED: Ab-7 sub=True root should equal sub=False (branch-c must not fire for A==4)" + "BLESS FAILED: Ab-7 sub=True root should equal sub=False (branch-c must not fire for A==4)" print("Substitution anchor cross-checks PASSED.") # --------------------------------------------------------------------------- # NUMERIC / ERRORS # --------------------------------------------------------------------------- -# IncorrectHarmonicFunctionException: chord whose harmonic function doesn't match -# the substitution branch's requirement. Branch (a) requires Subdominant; -# a Dominant-function chord at Tritone distance fires (a) and then raises. -# Actually branch (a) is: semitones(chord.root->key_root)==Tritone AND chord.hf != Subdominant -# Example: C7 in F# (F#->C ascending = 6 = Tritone; C7 is Dominant, not Subdominant) -> raises error_cases = [ - # IncorrectHarmonicFunctionException: C7 at tritone from key F# is Dominant, not Subdominant - _numeric_from_chord_case("numeric/errors/incorrect-hf-tritone-not-subdominant", "F#", "C7", True), - # InvalidChordStringException: empty numerator + _numeric_from_chord_case( + "numeric/errors/incorrect-hf-tritone-not-subdominant", "F#", "C7", True + ), _numeric_case("numeric/errors/empty-numerator", ""), + _numeric_case("numeric/errors/b9add9-merges-to-b99-invalid", "Vsusb9add9"), + _numeric_case( + "numeric/errors/b9add9-lowercase-root-invalid", "visusb9add9" + ), ] -# Verify they are indeed errors -for ec in error_cases: - assert "error" in ec["expect"], f"BLESS FAILED: {ec['name']} expected error but got: {ec['expect']}" +for error_case in error_cases: + assert "error" in error_case["expect"], ( + f"BLESS FAILED: {error_case['name']} expected error but got: {error_case['expect']}" + ) # --------------------------------------------------------------------------- # TRANSACTIONS / MODULATE @@ -246,6 +260,9 @@ def _chord_in_key_case(name: str, numeric: str, key_root: str) -> dict: _is_diatonic_case("transactions/is_diatonic/C-harm-minor-Bb^7-non-diatonic", "C", "HarmonicMinor", "Bb^7"), _is_diatonic_case("transactions/is_diatonic/G-major-D7-diatonic", "G", "Major", "D7"), _is_diatonic_case("transactions/is_diatonic/G-major-Db7-non-diatonic", "G", "Major", "Db7"), + _is_diatonic_case( + "is_diatonic-invalid-chord-error", "G", "Major", "Eb9#9add9" + ), ] # --------------------------------------------------------------------------- @@ -264,6 +281,12 @@ def _chord_in_key_case(name: str, numeric: str, key_root: str) -> dict: _hf_in_key_case("transactions/hf_in_key/C-major-subdominant-D-7", "C", False, "D-7"), # Different root: ii-7 in G = Subdominant _hf_in_key_case("transactions/hf_in_key/G-major-subdominant-A-7", "G", False, "A-7"), + _hf_in_key_case( + "transactions/hf_in_key/invalid-chord-error", + "C", + False, + "Eb9#9add9", + ), ] # --------------------------------------------------------------------------- @@ -295,6 +318,7 @@ def _chord_in_key_case(name: str, numeric: str, key_root: str) -> dict: files = [ (numeric_dir / "parse.json", parse_cases), + (numeric_dir / "from_chord.json", from_chord_cases), (numeric_dir / "slash.json", slash_cases), (numeric_dir / "substitution.json", substitution_cases), (numeric_dir / "errors.json", error_cases), diff --git a/conformance/music-dsl/runners/python/test_musicdsl_conformance.py b/conformance/music-dsl/runners/python/test_musicdsl_conformance.py index c486159..82f0f3c 100644 --- a/conformance/music-dsl/runners/python/test_musicdsl_conformance.py +++ b/conformance/music-dsl/runners/python/test_musicdsl_conformance.py @@ -5,7 +5,7 @@ RUN_PY = Path(__file__).resolve().parent / "run.py" CASES = Path(__file__).resolve().parents[2] / "cases" -EXPECTED_CASE_COUNT = 429 # 427 + 2 DRY-423 bebop-minor scale_value (BebopDorian, BebopMinor) +EXPECTED_CASE_COUNT = 433 # 429 baseline + 4 no-third suspension casing cases def test_musicdsl_conformance_passes(): diff --git a/conformance/music-dsl/runners/ts/run.ts b/conformance/music-dsl/runners/ts/run.ts index 2109ada..033f843 100644 --- a/conformance/music-dsl/runners/ts/run.ts +++ b/conformance/music-dsl/runners/ts/run.ts @@ -84,7 +84,7 @@ function discover(dir: string): Array<{ relpath: string; case: Case }> { return out; } -const EXPECTED_CASE_COUNT = 429; // keep in sync with the Python runner's frozen count +const EXPECTED_CASE_COUNT = 433; // keep in sync with the Python runner's frozen count const cases = discover(CASES_DIR); diff --git a/music-dsl/python/README.md b/music-dsl/python/README.md index bcd6b73..f8e9128 100644 --- a/music-dsl/python/README.md +++ b/music-dsl/python/README.md @@ -4,7 +4,7 @@ Pure music-theory domain library: notes, intervals, scale degrees, chord qualities, scales, and chord encoding. Zero runtime dependencies. This is the Python reference implementation; TypeScript (`@tonalis/music-dsl` on npm) and Rust (`tonalis-music-dsl` on crates.io) ports conform to the same spec and a shared -429-case conformance suite plus a seeded three-way differential fuzzer. +433-case conformance suite plus a seeded three-way differential fuzzer. `music-dsl` is the theory core underneath [`tonalis`](https://pypi.org/project/tonalis/), the format-agnostic lead-sheet DSL, and is fully usable on its own. diff --git a/music-dsl/python/music_dsl/helpers.py b/music-dsl/python/music_dsl/helpers.py index 5be8e03..ff6b212 100644 --- a/music-dsl/python/music_dsl/helpers.py +++ b/music-dsl/python/music_dsl/helpers.py @@ -18,9 +18,15 @@ def m_or_M_scaledegree(root: ScaleDegree, triad) -> ScaleDegree: - # Major-third triads (major, augmented) and sus4 keep the major scale degree; - # everything else (minor, dim, half-dim) lowers it. - if triad and triad not in (Triad.Major, Triad.Augmented, Triad.Sus4): + # Chords without a defining third retain an uppercase degree; all others that + # are not major-third triads lower it. + if triad and triad not in ( + Triad.Major, + Triad.Augmented, + Triad.Sus, + Triad.Sus2, + Triad.Sus4, + ): return root.to_minor() return root diff --git a/music-dsl/python/pyproject.toml b/music-dsl/python/pyproject.toml index 8350efc..3de32fe 100644 --- a/music-dsl/python/pyproject.toml +++ b/music-dsl/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tonalis-music-dsl" -version = "0.1.0" +version = "0.1.1" description = "Music-theory domain: notes, chords, intervals, scales, harmonic function." requires-python = ">=3.11" readme = "README.md" diff --git a/music-dsl/python/tests/unit/domain/chords/test_sus_shorthand.py b/music-dsl/python/tests/unit/domain/chords/test_sus_shorthand.py index b67b1fa..0c4c55b 100644 --- a/music-dsl/python/tests/unit/domain/chords/test_sus_shorthand.py +++ b/music-dsl/python/tests/unit/domain/chords/test_sus_shorthand.py @@ -17,6 +17,25 @@ from music_dsl.domain.chords.chord import Chord from music_dsl.domain.static import Seventh, Triad +from music_dsl.domain.chords.numeric_chord import NumericChord + + +@pytest.mark.parametrize( + "token", + ["Isus", "Isus2", "Isus4", "I7sus", "I7sus2", "I7sus4"], +) +def test_suspended_numeric_chords_keep_uppercase(token): + assert repr(NumericChord.from_chord_string(token)) == token + + +def test_bare_sus_is_the_sus4_pitch_set_but_sus2_is_distinct(): + bare = Chord("Csus") + two = Chord("Csus2") + four = Chord("Csus4") + assert bare.encoding == four.encoding + assert two.encoding != four.encoding + + @pytest.mark.parametrize("root", ["C", "G", "A", "D", "E", "F", "B"]) diff --git a/music-dsl/rust/Cargo.lock b/music-dsl/rust/Cargo.lock index f0ed51c..4d7dbd5 100644 --- a/music-dsl/rust/Cargo.lock +++ b/music-dsl/rust/Cargo.lock @@ -149,7 +149,7 @@ dependencies = [ [[package]] name = "tonalis-music-dsl" -version = "0.1.0" +version = "0.1.1" dependencies = [ "regex", "serde", diff --git a/music-dsl/rust/Cargo.toml b/music-dsl/rust/Cargo.toml index fcd1fba..2a6411c 100644 --- a/music-dsl/rust/Cargo.toml +++ b/music-dsl/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tonalis-music-dsl" -version = "0.1.0" +version = "0.1.1" edition = "2021" description = "MusicDSL — music-theory domain (notes, intervals, scale-degrees, chords, encoding). Port of the Python music_dsl reference." readme = "README.md" diff --git a/music-dsl/rust/README.md b/music-dsl/rust/README.md index 4f9ce31..c22abb8 100644 --- a/music-dsl/rust/README.md +++ b/music-dsl/rust/README.md @@ -3,7 +3,7 @@ Pure music-theory domain library: notes, intervals, scale degrees, chord qualities, scales, and chord encoding. No runtime dependencies. Port of the Python `music-dsl` reference implementation — all three ports (Python / -TypeScript / Rust) conform to the same spec via a shared 429-case conformance +TypeScript / Rust) conform to the same spec via a shared 433-case conformance suite plus a seeded three-way differential fuzzer. The crate is named `tonalis-music-dsl`; the library target (import path) is diff --git a/music-dsl/rust/src/numeric_chord.rs b/music-dsl/rust/src/numeric_chord.rs index 2631bc8..0cb3507 100644 --- a/music-dsl/rust/src/numeric_chord.rs +++ b/music-dsl/rust/src/numeric_chord.rs @@ -12,6 +12,17 @@ use regex::Regex; use serde::Serialize; use std::sync::OnceLock; +fn keeps_uppercase_degree(triad: Triad) -> bool { + matches!( + triad, + Triad::Major + | Triad::Augmented + | Triad::Sus + | Triad::Sus2 + | Triad::Sus4 + ) +} + // --------------------------------------------------------------------------- // Error type // --------------------------------------------------------------------------- @@ -176,11 +187,11 @@ fn parse_one(segment: &str, substitution: bool) -> Result root_degree, - _ => root_degree.to_minor(), + // Major-third triads and no-third suspensions keep the uppercase degree. + let root_final = if keeps_uppercase_degree(triad) { + root_degree + } else { + root_degree.to_minor() }; Ok(NumericChordAttrs { @@ -313,11 +324,10 @@ pub fn numeric_from_chord( base_degree }; - // m_or_M_scaledegree: minor triad quality → lowercase degree - let base_degree = if !matches!(triad, Triad::Major | Triad::Augmented | Triad::Sus4) { - base_degree.to_minor() - } else { + let base_degree = if keeps_uppercase_degree(triad) { base_degree + } else { + base_degree.to_minor() }; // Substitution branches diff --git a/music-dsl/rust/tests/conformance.rs b/music-dsl/rust/tests/conformance.rs index 049e220..646b89f 100644 --- a/music-dsl/rust/tests/conformance.rs +++ b/music-dsl/rust/tests/conformance.rs @@ -13,7 +13,7 @@ use serde_json::Value; use std::fs; use std::path::{Path, PathBuf}; -const EXPECTED_CASE_COUNT: usize = 429; // keep in sync with Python/TS runners +const EXPECTED_CASE_COUNT: usize = 433; // keep in sync with Python/TS runners fn cases_dir() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) diff --git a/music-dsl/rust/tests/unit.rs b/music-dsl/rust/tests/unit.rs index 803b0a2..640a17a 100644 --- a/music-dsl/rust/tests/unit.rs +++ b/music-dsl/rust/tests/unit.rs @@ -1,12 +1,29 @@ use music_dsl::{ chord_encoding, chord_in_key, chord_pitches, harmonic_function_in_key, interval_pitches, intervals_equal, interval_semitones, is_diatonic, midi_to_hz, modulate, note_index, - note_to_midi, notes_equal, parse_chord, parse_measure, scale_degree_pitch, - scale_degrees_equal, scale_pitches, encoding_value, scale_descriptor, scale_value, - semitones_apart_ascending, strip_left, strip_right, TimeSignature, + note_to_midi, notes_equal, numeric_from_chord, parse_chord, parse_measure, parse_numeric, + scale_degree_pitch, scale_degrees_equal, scale_pitches, encoding_value, scale_descriptor, + scale_value, semitones_apart_ascending, strip_left, strip_right, TimeSignature, }; use music_dsl::{contains, ScaleDescriptor}; + +#[test] +fn suspended_numeric_chords_keep_uppercase_degrees() { + for token in ["Isus", "Isus2", "Isus4", "I7sus", "I7sus2", "I7sus4"] { + let parsed = parse_numeric(token).expect("suspended numeral should parse"); + assert_eq!(parsed.numerator.root, "I", "{token}"); + } +} + +#[test] +fn suspended_absolute_chords_map_to_uppercase_degrees() { + for chord in ["Csus", "Csus2", "Csus4", "C7sus", "C7sus2", "C7sus4"] { + let numeric = numeric_from_chord("C", chord, false) + .expect("suspended chord should map to a numeral"); + assert_eq!(numeric.root, "I", "{chord}"); + } +} #[test] fn notes_enharmonic_equality() { assert!(notes_equal("C#", "Db")); diff --git a/music-dsl/ts/README.md b/music-dsl/ts/README.md index 5db133a..a8fcedb 100644 --- a/music-dsl/ts/README.md +++ b/music-dsl/ts/README.md @@ -4,7 +4,7 @@ Pure music-theory domain library: notes, intervals, scale degrees, chord qualities, scales, and chord encoding. Zero runtime dependencies; ships compiled ESM with type declarations. Port of the Python `music-dsl` reference implementation — all three ports (Python / TypeScript / Rust) conform to the -same spec via a shared 429-case conformance suite plus a seeded three-way +same spec via a shared 433-case conformance suite plus a seeded three-way differential fuzzer. `@tonalis/music-dsl` is the theory core underneath diff --git a/music-dsl/ts/package-lock.json b/music-dsl/ts/package-lock.json index edb3808..fe9cca5 100644 --- a/music-dsl/ts/package-lock.json +++ b/music-dsl/ts/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tonalis/music-dsl", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tonalis/music-dsl", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "devDependencies": { "@types/node": "^22.10.2", diff --git a/music-dsl/ts/package.json b/music-dsl/ts/package.json index 70f8404..14d7785 100644 --- a/music-dsl/ts/package.json +++ b/music-dsl/ts/package.json @@ -1,6 +1,6 @@ { "name": "@tonalis/music-dsl", - "version": "0.1.0", + "version": "0.1.1", "description": "MusicDSL — music-theory domain (notes, intervals, scale-degrees, chords, encoding). Port of the Python music_dsl reference. Zero runtime deps.", "type": "module", "main": "./dist/index.js", @@ -18,9 +18,16 @@ "test:watch": "vitest", "typecheck": "tsc --noEmit -p tsconfig.json" }, - "files": ["dist", "LICENSE"], + "files": [ + "dist", + "LICENSE" + ], "license": "MIT", - "repository": { "type": "git", "url": "git+https://github.com/drycode/tonalis.git", "directory": "music-dsl/ts" }, + "repository": { + "type": "git", + "url": "git+https://github.com/drycode/tonalis.git", + "directory": "music-dsl/ts" + }, "homepage": "https://github.com/drycode/tonalis#readme", "private": false, "devDependencies": { diff --git a/music-dsl/ts/src/numericChord.test.ts b/music-dsl/ts/src/numericChord.test.ts index 08dd8b5..a7a0f67 100644 --- a/music-dsl/ts/src/numericChord.test.ts +++ b/music-dsl/ts/src/numericChord.test.ts @@ -61,6 +61,14 @@ describe("fromChordString — basic", () => { }); }); +describe("fromChord — suspended numeral casing", () => { + for (const token of ["Isus", "Isus2", "Isus4", "I7sus", "I7sus2", "I7sus4"]) { + it(`${token} keeps its uppercase degree`, () => { + expect(serializeNumericChord(fromChordString(token)).numerator.root).toBe("I"); + }); + } +}); + // --------------------------------------------------------------------------- // fromChordString — slash chords // --------------------------------------------------------------------------- diff --git a/music-dsl/ts/src/numericChord.ts b/music-dsl/ts/src/numericChord.ts index b38a82d..e316392 100644 --- a/music-dsl/ts/src/numericChord.ts +++ b/music-dsl/ts/src/numericChord.ts @@ -148,12 +148,18 @@ function modulateDegree(semitones: number, degree: ScaleDegreeT): ScaleDegreeT { // --------------------------------------------------------------------------- // mOrMScaleDegree — mirror of Python m_or_M_scaledegree -// Major-third triads (Major, Augmented, Sus4) keep the major degree; -// everything else (Minor, Dim, HalfDim, Sus, Sus2) lowers it. +// Major-third triads and no-third suspensions keep the uppercase degree; +// everything else (Minor, Dim, HalfDim) lowers it. // --------------------------------------------------------------------------- function mOrMScaleDegree(degree: ScaleDegreeT, triad: TriadT): ScaleDegreeT { - if (triad && triad !== Triad.Augmented && triad !== Triad.Sus4) { + if ( + triad !== Triad.Major && + triad !== Triad.Augmented && + triad !== Triad.Sus && + triad !== Triad.Sus2 && + triad !== Triad.Sus4 + ) { return sdToMinor(degree) as ScaleDegreeT; } return degree; diff --git a/tonalis/python/pyproject.toml b/tonalis/python/pyproject.toml index e3e3d1f..42a29e8 100644 --- a/tonalis/python/pyproject.toml +++ b/tonalis/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tonalis" -version = "0.1.0" +version = "0.1.1" description = "Tonalis — a generic, format-agnostic music-harmony DSL (parse/lint/AST/JSON/text)." requires-python = ">=3.11" readme = "README.md" @@ -12,7 +12,7 @@ license = { file = "LICENSE" } classifiers = [ "License :: OSI Approved :: MIT License", ] -dependencies = ["tonalis-music-dsl==0.1.0"] +dependencies = ["tonalis-music-dsl==0.1.1"] [project.urls] Homepage = "https://github.com/drycode/tonalis" diff --git a/tonalis/rust/Cargo.lock b/tonalis/rust/Cargo.lock index 5b258ec..55ee6ef 100644 --- a/tonalis/rust/Cargo.lock +++ b/tonalis/rust/Cargo.lock @@ -149,7 +149,7 @@ dependencies = [ [[package]] name = "tonalis" -version = "0.1.0" +version = "0.1.1" dependencies = [ "regex", "serde", @@ -159,7 +159,7 @@ dependencies = [ [[package]] name = "tonalis-music-dsl" -version = "0.1.0" +version = "0.1.1" dependencies = [ "regex", "serde", diff --git a/tonalis/rust/Cargo.toml b/tonalis/rust/Cargo.toml index 93fbc8d..28545be 100644 --- a/tonalis/rust/Cargo.toml +++ b/tonalis/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tonalis" -version = "0.1.0" +version = "0.1.1" edition = "2021" description = "Tonalis — a generic, format-agnostic music-harmony DSL (parse/lint/AST/JSON/text)." readme = "README.md" @@ -12,7 +12,7 @@ repository = "https://github.com/drycode/tonalis" crate-type = ["rlib"] [dependencies] -music_dsl = { package = "tonalis-music-dsl", path = "../../music-dsl/rust", version = "0.1.0" } +music_dsl = { package = "tonalis-music-dsl", path = "../../music-dsl/rust", version = "0.1.1" } regex = "1" serde = { version = "1", features = ["derive"] } # preserve_order keeps serialized AST object keys in insertion order (matching the Python/TS diff --git a/tonalis/ts/package-lock.json b/tonalis/ts/package-lock.json index 8ceec8e..70236d6 100644 --- a/tonalis/ts/package-lock.json +++ b/tonalis/ts/package-lock.json @@ -1,12 +1,12 @@ { "name": "tonalis", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tonalis", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "dependencies": { "@tonalis/music-dsl": "file:../../music-dsl/ts" @@ -19,7 +19,7 @@ }, "../../music-dsl/ts": { "name": "@tonalis/music-dsl", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "devDependencies": { "@types/node": "^22.10.2", diff --git a/tonalis/ts/package.json b/tonalis/ts/package.json index f89a7cd..07f3199 100644 --- a/tonalis/ts/package.json +++ b/tonalis/ts/package.json @@ -1,6 +1,6 @@ { "name": "tonalis", - "version": "0.1.0", + "version": "0.1.1", "description": "Tonalis — a generic, format-agnostic music-harmony DSL (parse/lint/AST/JSON/text). Depends only on the pure @tonalis/music-dsl theory library; ships compiled ESM + type declarations, browser + Node safe.", "type": "module", "main": "./dist/index.js", @@ -23,7 +23,11 @@ "LICENSE" ], "license": "MIT", - "repository": { "type": "git", "url": "git+https://github.com/drycode/tonalis.git", "directory": "tonalis/ts" }, + "repository": { + "type": "git", + "url": "git+https://github.com/drycode/tonalis.git", + "directory": "tonalis/ts" + }, "homepage": "https://github.com/drycode/tonalis#readme", "private": false, "dependencies": {