diff --git a/README.md b/README.md index bb9ca78d..feab5e56 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ The API documentation lives inside README.md file of each module #### Time, rhythm +- [@tonaljs/time-signature](/packages/time-signature): Parse time signatures - [@tonaljs/duration-value](/packages/duration-value): Note duration values #### Utilities diff --git a/package.json b/package.json index f32c9d7a..efeb7529 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ ".(ts|tsx)": "ts-jest" }, "testEnvironment": "node", - "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", + "testRegex": "test.ts$", "moduleFileExtensions": [ "ts", "tsx", diff --git a/packages/time-signature/LICENSE b/packages/time-signature/LICENSE new file mode 100644 index 00000000..77ac35ab --- /dev/null +++ b/packages/time-signature/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 danigb + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/packages/time-signature/README.md b/packages/time-signature/README.md new file mode 100644 index 00000000..c878544e --- /dev/null +++ b/packages/time-signature/README.md @@ -0,0 +1,78 @@ +# @tonaljs/time-signature ![tonal](https://img.shields.io/badge/@tonaljs-time_signature-yellow.svg?style=flat-square) [![npm version](https://img.shields.io/npm/v/@tonaljs/time-signature.svg?style=flat-square)](https://www.npmjs.com/package/@tonaljs/time-signature) + +> Functions to parse time signatures + +## Usage + +ES6: + +```js +import { TimeSignature } from "@tonaljs/tonal"; +``` + +node: + +```js +const { TimeSignature } = require("@tonaljs/tonal"); +``` + +single module: + +```js +import TimeSignature from "@tonaljs/time-signature"; +``` + +## API + +#### `TimeSignature.names() => string[]` + +Return a list of most most frequently-used time signatures: + +```js +TimeSignature.names(); +``` + +#### `TimeSignature.get(name: string | [number, number]) // => object` + +Get a time signature: + +```js +TimeSignature.get("3/4"); // => +// { +// empty: false, +// name: "3/4", +// upper: 3, +// lower: 4, +// type: "simple", +// additive: [] +// }; +``` + +`type` can be `simple`, `compound` or `regular` + +Additive signatures are accepted: + +```js +TimeSignature.get("3+2+3/8"); // => +// { +// empty: false, +// name: '3+2+3/8', +// type: 'irregular', +// upper: 8, +// lower: 8, +// additive: [ 3, 2, 3 ] +// } +``` + +Arrays can be passed as arguments: + +```js +TimeSignature.get([3, 4]); +TimeSignature.get(["3", "4"]); +TimeSignature.get(["3+2+3", "8"]); +``` + +## References + +- https://en.wikipedia.org/wiki/Time_signature +- https://en.wikipedia.org/wiki/Metre_(music) diff --git a/packages/time-signature/index.ts b/packages/time-signature/index.ts new file mode 100644 index 00000000..8ffe0a50 --- /dev/null +++ b/packages/time-signature/index.ts @@ -0,0 +1,102 @@ +// TYPES: PARSING +export type TimeSignatureLiteral = string | [number, number] | [string, string]; +type ParsedTimeSignature = [number | number[], number]; + +// TYPES: PROPERTIES +export type ValidTimeSignature = { + readonly empty: false; + readonly name: string; + readonly upper: number | number[]; + readonly lower: number; + readonly type: "simple" | "compound" | "irregular"; + readonly additive: number[]; +}; + +export type InvalidTimeSignature = { + readonly empty: true; + readonly name: ""; + readonly upper: undefined; + readonly lower: undefined; + readonly type: undefined; + readonly additive: []; +}; + +export type TimeSignature = ValidTimeSignature | InvalidTimeSignature; + +// CONSTANTS +const NONE: InvalidTimeSignature = { + empty: true, + name: "", + upper: undefined, + lower: undefined, + type: undefined, + additive: [] +}; + +const NAMES = ["4/4", "3/4", "2/4", "2/2", "12/8", "9/8", "6/8", "3/8"]; + +// PUBLIC API + +export function names() { + return NAMES.slice(); +} + +const REGEX = /^(\d?\d(?:\+\d)*)\/(\d)$/; +const CACHE = new Map(); + +export function get(literal: TimeSignatureLiteral): TimeSignature { + const cached = CACHE.get(literal); + if (cached) { + return cached; + } + + const ts = build(parse(literal)); + CACHE.set(literal, ts); + return ts; +} + +export function parse(literal: TimeSignatureLiteral): ParsedTimeSignature { + if (typeof literal === "string") { + const [_, up, low] = REGEX.exec(literal) || []; + return parse([up, low]); + } + + const [up, down] = literal; + const denominator = +down; + if (typeof up === "number") { + return [up, denominator]; + } + + const list = up.split("+").map(n => +n); + return list.length === 1 ? [list[0], denominator] : [list, denominator]; +} + +export default { names, parse, get }; + +// PRIVATE + +function build([up, down]: ParsedTimeSignature): TimeSignature { + const upper = Array.isArray(up) ? up.reduce((a, b) => a + b, 0) : up; + const lower = down; + if (upper === 0 || lower === 0) { + return NONE; + } + + const name = Array.isArray(up) ? `${up.join("+")}/${down}` : `${up}/${down}`; + const additive = Array.isArray(up) ? up : []; + const type = + lower === 4 || lower === 2 + ? "simple" + : lower === 8 && upper % 3 === 0 + ? "compound" + : "irregular"; + + return { + empty: false, + name, + type, + upper, + lower, + additive + }; +} diff --git a/packages/time-signature/package.json b/packages/time-signature/package.json new file mode 100644 index 00000000..8c2c5007 --- /dev/null +++ b/packages/time-signature/package.json @@ -0,0 +1,26 @@ +{ + "name": "@tonaljs/time-signature", + "version": "3.6.0", + "description": "Musical time signatures", + "scripts": { + "build": "rollup -c=../../rollup.config.js" + }, + "keywords": [ + "time signature", + "time meter", + "metre", + "music", + "theory" + ], + "main": "dist/index.es5.js", + "module": "dist/index.esnext.js", + "files": [ + "dist" + ], + "types": "dist/time-signature/index.d.ts", + "author": "danigb@gmail.com", + "license": "MIT", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/time-signature/test.ts b/packages/time-signature/test.ts new file mode 100644 index 00000000..e827e912 --- /dev/null +++ b/packages/time-signature/test.ts @@ -0,0 +1,49 @@ +import TimeSignature from "./index"; + +describe("time-signature", () => { + test("get", () => { + expect(TimeSignature.get("4/4")).toEqual({ + empty: false, + name: "4/4", + type: "simple", + upper: 4, + lower: 4, + additive: [] + }); + }); + + test("get invalid", () => { + expect(TimeSignature.get("0/0").empty).toBe(true); + }); + + test("simple", () => { + expect(TimeSignature.get("4/4").type).toEqual("simple"); + expect(TimeSignature.get("3/4").type).toEqual("simple"); + expect(TimeSignature.get("2/4").type).toEqual("simple"); + expect(TimeSignature.get("2/2").type).toEqual("simple"); + }); + test("compound", () => { + expect(TimeSignature.get("3/8").type).toEqual("compound"); + expect(TimeSignature.get("6/8").type).toEqual("compound"); + expect(TimeSignature.get("9/8").type).toEqual("compound"); + expect(TimeSignature.get("12/8").type).toEqual("compound"); + }); + + test("irregular", () => { + expect(TimeSignature.get("2+3+3/8").type).toEqual("irregular"); + expect(TimeSignature.get("3+2+2/8").type).toEqual("irregular"); + }); + + test("names", () => { + expect(TimeSignature.names()).toEqual([ + "4/4", + "3/4", + "2/4", + "2/2", + "12/8", + "9/8", + "6/8", + "3/8" + ]); + }); +}); diff --git a/packages/tonal/index.ts b/packages/tonal/index.ts index d821d4f7..3bcd5e38 100644 --- a/packages/tonal/index.ts +++ b/packages/tonal/index.ts @@ -16,6 +16,7 @@ import Range from "@tonaljs/range"; import RomanNumeral from "@tonaljs/roman-numeral"; import Scale from "@tonaljs/scale"; import ScaleType from "@tonaljs/scale-type"; +import TimeSignature from "@tonaljs/time-signature"; export * from "@tonaljs/core"; @@ -44,7 +45,8 @@ export { RomanNumeral, Scale, ScaleType, - // backwards + TimeSignature, + // backwards API compatibility (3.0) Tonal, PcSet, ChordDictionary, diff --git a/packages/tonal/package.json b/packages/tonal/package.json index f17b3b33..cb3d5da8 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -34,7 +34,8 @@ "@tonaljs/range": "^3.5.0", "@tonaljs/roman-numeral": "^3.5.0", "@tonaljs/scale": "^3.5.1", - "@tonaljs/scale-type": "^3.5.0" + "@tonaljs/scale-type": "^3.5.0", + "@tonaljs/time-signature": "^3.5.0" }, "author": "danigb@gmail.com", "license": "MIT", diff --git a/packages/tonal/tonal.test.ts b/packages/tonal/tonal.test.ts index a19cc713..aece69e4 100644 --- a/packages/tonal/tonal.test.ts +++ b/packages/tonal/tonal.test.ts @@ -25,6 +25,7 @@ describe("@tonaljs/tonal", () => { "Scale", "ScaleDictionary", "ScaleType", + "TimeSignature", "Tonal", "accToAlt", "altToAcc", diff --git a/rollup.config.js b/rollup.config.js index ca5ddb49..6b2550a2 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -7,11 +7,14 @@ const INPUT_FILE = path.join(PACKAGE_ROOT_PATH, "index.ts"); const OUTPUT_DIR = path.join(PACKAGE_ROOT_PATH, "dist"); const PKG_JSON = require(path.join(PACKAGE_ROOT_PATH, "package.json")); -const formats = [{ dist: "umd", ts: "es5" }, { dist: "es", ts: "esnext" }]; +const OUTPUTS = [ + { format: "umd", target: "es5" }, + { format: "es", target: "esnext" } +]; const name = getUmdName(PKG_JSON.name); -export default formats.map(format => ({ +export default OUTPUTS.map(format => ({ input: INPUT_FILE, external: [ ...Object.keys(PKG_JSON.dependencies || {}), @@ -19,14 +22,14 @@ export default formats.map(format => ({ ], output: { name, - file: path.join(OUTPUT_DIR, `index.${format.ts}.js`), - format: format.dist, + file: path.join(OUTPUT_DIR, `index.${format.target}.js`), + format: format.format, sourcemap: true }, plugins: [ typescript({ tsconfig: `tsconfig.json`, - tsconfigOverride: { compilerOptions: { target: format.ts } } + tsconfigOverride: { compilerOptions: { target: format.target } } }) ] })); @@ -38,6 +41,6 @@ function getUmdName(packageName) { packageName ); } - const sufix = packageName.slice("@tonaljs/".length); - return _.startCase(sufix).replace(" ", ""); + const suffix = packageName.slice("@tonaljs/".length); + return _.startCase(suffix).replace(" ", ""); } diff --git a/tsconfig.json b/tsconfig.json index 8808002e..19294507 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "moduleResolution": "node" }, "include": ["packages/**/*.ts", "../../packages/**/*"], - "exclude": ["node_modules", "packages/**/*.test.ts", "**/dist/**/*"] + "exclude": ["node_modules", "packages/**/*test.ts", "**/dist/**/*"] } diff --git a/tslint.json b/tslint.json index d21cfaae..d28ded25 100644 --- a/tslint.json +++ b/tslint.json @@ -5,6 +5,7 @@ "no-implicit-dependencies": true, // Disable recommended rules + "interface-over-type-literal": false, "interface-name": false, "object-literal-sort-keys": false, "no-shadowed-variable": false