@audio/note 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,40 @@
1
+ Kṛṣnized
2
+
3
+ This is a reminder that, in the absolute sense, all results of work belong to the Supreme.
4
+
5
+ While software and intellectual property (IP) require a formal license to define authorship,
6
+ responsibilities, or distribution limitations, these are temporary material designations.
7
+ The origin and destination of all ideas, as well as the ultimate owner of all results,
8
+ is the Supreme. The most meaningful action is to dedicate these results to the Supreme.
9
+ This process is known as Karma-Yoga and is described in BG 3.9, BG 12.10 and BG 18.46.
10
+
11
+ "Work done as a sacrifice for the Supreme must be performed;
12
+ otherwise, work binds one to the material world."
13
+
14
+ This license does not override or alter the terms specified by the material-level license below.
15
+
16
+
17
+
18
+ ---
19
+
20
+ MIT License
21
+
22
+ Copyright (c) Dmitry Iv
23
+
24
+ Permission is hereby granted, free of charge, to any person obtaining a copy
25
+ of this software and associated documentation files (the "Software"), to deal
26
+ in the Software without restriction, including without limitation the rights
27
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
28
+ copies of the Software, and to permit persons to whom the Software is
29
+ furnished to do so, subject to the following conditions:
30
+
31
+ The above copyright notice and this permission notice shall be included in all
32
+ copies or substantial portions of the Software.
33
+
34
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @audio/note
2
+
3
+ > Music-theory primitives: Hz ↔ MIDI ↔ note name, cents, scales, snapping.
4
+
5
+ ```js
6
+ import { hzToMidi, midiToHz, name, parse, cents, SCALES, snapMidi, snapHz } from '@audio/note'
7
+
8
+ name(69) // 'A4'
9
+ cents(443) // { midi: 69, name: 'A4', hz: 440, cents: 11.7 } — tuner readout
10
+ snapHz(450, { scale: 'major' }) // nearest major-scale frequency
11
+ ```
12
+
13
+ Hz/MIDI/name conversion + scale tables/snapping — one small, always-together substrate. Not a music-theory library (chords, keys, roman numerals): that's composition/notation tooling out of `@audio`'s audio-processing scope — see tonal.js/teoria.js for that. Used by `@audio/tune` (pitch correction), the tuner tool, and `@audio/midi`.
package/convert.js ADDED
@@ -0,0 +1,31 @@
1
+ // Note math — Hz ↔ MIDI ↔ note name, cents offset. 12-TET, MIDI convention (C4 = 60, A4 = 69).
2
+
3
+ export const NAMES_SHARP = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
4
+ export const NAMES_FLAT = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']
5
+
6
+ export const hzToMidi = (hz, a4 = 440) => 69 + 12 * Math.log2(hz / a4)
7
+ export const midiToHz = (midi, a4 = 440) => a4 * 2 ** ((midi - 69) / 12)
8
+
9
+ /** midi → 'C#4' (or 'Db4' with {flat: true}) */
10
+ export function name (midi, { flat = false } = {}) {
11
+ let m = Math.round(midi)
12
+ let pc = ((m % 12) + 12) % 12
13
+ let oct = Math.floor(m / 12) - 1
14
+ return (flat ? NAMES_FLAT : NAMES_SHARP)[pc] + oct
15
+ }
16
+
17
+ /** 'A4' | 'C#3' | 'Bb2' → midi number */
18
+ export function parse (str) {
19
+ let m = /^([A-Ga-g])([#b]?)(-?\d+)$/.exec(str.trim())
20
+ if (!m) throw new RangeError(`note: cannot parse "${str}"`)
21
+ const BASE = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 }
22
+ let pc = BASE[m[1].toUpperCase()] + (m[2] === '#' ? 1 : m[2] === 'b' ? -1 : 0)
23
+ return pc + (Number(m[3]) + 1) * 12
24
+ }
25
+
26
+ /** hz → { midi, name, hz (exact note), cents (offset from it) } — tuner readout */
27
+ export function cents (hz, { a4 = 440, flat = false } = {}) {
28
+ let midi = Math.round(hzToMidi(hz, a4))
29
+ let ref = midiToHz(midi, a4)
30
+ return { midi, name: name(midi, { flat }), hz: ref, cents: 1200 * Math.log2(hz / ref) }
31
+ }
package/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // @audio/note — Hz ↔ MIDI ↔ name/cents, scale tables, pitch snapping.
2
+
3
+ export * from './convert.js'
4
+ export * from './scale.js'
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@audio/note",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Music-theory primitives — Hz ↔ MIDI ↔ name, cents, scales, pitch snapping",
7
+ "main": "index.js",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "index.js",
14
+ "convert.js",
15
+ "scale.js"
16
+ ],
17
+ "scripts": {
18
+ "test": "node test.js"
19
+ },
20
+ "devDependencies": {
21
+ "tst": "^9.4.0"
22
+ },
23
+ "keywords": [
24
+ "audio",
25
+ "music",
26
+ "note",
27
+ "midi",
28
+ "scale",
29
+ "tuner"
30
+ ],
31
+ "license": "MIT",
32
+ "author": "Dmitry Iv. <dfcreative@gmail.com>",
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }
package/scale.js ADDED
@@ -0,0 +1,37 @@
1
+ // Scales and pitch snapping — the tune-snap / tuner substrate.
2
+
3
+ import { hzToMidi, midiToHz } from './convert.js'
4
+
5
+ export const SCALES = {
6
+ chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
7
+ major: [0, 2, 4, 5, 7, 9, 11],
8
+ minor: [0, 2, 3, 5, 7, 8, 10],
9
+ 'harmonic-minor': [0, 2, 3, 5, 7, 8, 11],
10
+ 'melodic-minor': [0, 2, 3, 5, 7, 9, 11],
11
+ 'pentatonic-major': [0, 2, 4, 7, 9],
12
+ 'pentatonic-minor': [0, 3, 5, 7, 10],
13
+ blues: [0, 3, 5, 6, 7, 10],
14
+ dorian: [0, 2, 3, 5, 7, 9, 10],
15
+ mixolydian: [0, 2, 4, 5, 7, 9, 10],
16
+ whole: [0, 2, 4, 6, 8, 10],
17
+ }
18
+
19
+ /** snap a (possibly fractional) midi value to the nearest scale degree */
20
+ export function snapMidi (midi, { scale = 'chromatic', root = 0 } = {}) {
21
+ let degrees = typeof scale === 'string' ? SCALES[scale] : scale
22
+ if (!degrees) throw new RangeError(`note: unknown scale "${scale}"`)
23
+ let best = 0, bestDist = Infinity
24
+ for (let oct = Math.floor(midi / 12) - 1; oct <= Math.floor(midi / 12) + 1; oct++) {
25
+ for (let d of degrees) {
26
+ let cand = oct * 12 + root + d
27
+ let dist = Math.abs(cand - midi)
28
+ if (dist < bestDist) { bestDist = dist; best = cand }
29
+ }
30
+ }
31
+ return best
32
+ }
33
+
34
+ /** snap a frequency to the nearest scale note frequency */
35
+ export function snapHz (hz, { scale = 'chromatic', root = 0, a4 = 440 } = {}) {
36
+ return midiToHz(snapMidi(hzToMidi(hz, a4), { scale, root }), a4)
37
+ }