summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrian Cully <bjc@spork.org>2025-03-10 12:26:55 -0400
committerBrian Cully <bjc@spork.org>2025-03-10 12:26:55 -0400
commit33e376806164072f94cdf9a682262db19ff590d3 (patch)
tree1bc6da506fd4188557cba4de27139a87fe82b8a4
parentefae088a0f78eaa0e483c7709bb331809db9cbff (diff)
downloadchords-33e376806164072f94cdf9a682262db19ff590d3.tar.gz
chords-33e376806164072f94cdf9a682262db19ff590d3.zip
js: add ‘Note’ class
-rw-r--r--scale.mjs86
1 files changed, 86 insertions, 0 deletions
diff --git a/scale.mjs b/scale.mjs
index de25d14..9bec03b 100644
--- a/scale.mjs
+++ b/scale.mjs
@@ -8,6 +8,92 @@ function notesBetween(a, b) {
}
}
+export class Note {
+ static natural = '♮';
+ static sharp = '♯';
+ static flat = '♭';
+
+ static range = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
+ static get first() {
+ return this.range.at(0);
+ }
+ static get last() {
+ return this.range.at(-1);
+ }
+
+ static noSharps = ['B', 'E'];
+ static get noFlats() {
+ return this.noSharps.map(n => String.fromCharCode(n.charCodeAt()+1))
+ }
+
+ static fromString(string) {
+ const [root, accidental] = [string[0], string[1]];
+ switch (accidental) {
+ case this.sharp:
+ if (this.noSharps.includes(root)) {
+ return new this(String.fromCharCode(root.charCodeAt()+1))
+ } else {
+ return new this(root, true);
+ }
+ case this.flat:
+ if (this.noFlats.includes(root)) {
+ return new this(String.fromCharCode(root.charCodeAt()-1));
+ } else if (root === Note.first) {
+ // i don't want to make a ring buffer that accepts
+ // negative indices, ok???
+ return new this(Note.last, true);
+ } else {
+ const code = root.charCodeAt();
+ return new this(String.fromCharCode(code-1), true);
+ }
+ case undefined:
+ case this.natural:
+ return new this(root);
+ default:
+ throw new Error('wtf is that accidental', accidental);
+ }
+ }
+
+ constructor(root, isSharp=false) {
+ if (isSharp && Note.noSharps.includes(root)) {
+ throw new Error(`bad note: ${root}${Note.sharp}`);
+ }
+ this.root = root;
+ this.isSharp = isSharp;
+ }
+
+ toString() {
+ return this.root + (this.isSharp ? Note.sharp : '');
+ }
+
+ toAlternateString() {
+ if (this.isSharp) {
+ const next = (this.root === Note.last) ? Note.first : String.fromCharCode(this.root.charCodeAt()+1);
+ if (Note.noSharps.includes(this.root)) {
+ return next;
+ } else {
+ return next + Note.flat;
+ }
+ } else if (Note.noFlats.includes(this.root)) {
+ const prev = (this.root === Note.first) ? Note.last : String.fromCharCode(this.root.charCodeAt()-1);
+ return prev + Note.sharp;
+ } else {
+ return this.toString();
+ }
+ }
+
+ toRelativeString(other) {
+ const alternate = this.toAlternateString();
+ const otherStr = other.toString();
+ const delta = otherStr.charCodeAt() - alternate.charCodeAt();
+ if (delta === -1 || delta === 1) {
+ return alternate;
+ } else {
+ return this.toString();
+ }
+ }
+}
+
const ringHandler = {
get(target, prop) {
return target[prop % target.length] || Reflect.get(...arguments);