From 8021dcac6cd1bde3016baf22b43e3edcfed35803 Mon Sep 17 00:00:00 2001 From: Bludwarf Date: Fri, 15 Mar 2024 12:27:05 +0100 Subject: [PATCH] Accords + Patterns #6 --- src/app/fretboard/fretboard.component.html | 1 + src/app/fretboard/fretboard.component.scss | 2 +- src/app/fretboard/fretboard.component.ts | 3 + src/app/notes.spec.ts | 24 +++ src/app/notes.ts | 141 +++++++++++++++++- .../rythm-sandbox.component.html | 7 +- .../rythm-sandbox/rythm-sandbox.component.ts | 33 +++- .../structure/pattern/pattern-in-structure.ts | 7 + src/app/structure/pattern/pattern.ts | 22 ++- src/app/time.ts | 28 +++- src/polyfills.ts | 66 ++++++++ src/test.ts | 22 +++ tsconfig.spec.json | 20 +++ 13 files changed, 362 insertions(+), 14 deletions(-) create mode 100644 src/app/notes.spec.ts create mode 100644 src/polyfills.ts create mode 100644 src/test.ts create mode 100644 tsconfig.spec.json diff --git a/src/app/fretboard/fretboard.component.html b/src/app/fretboard/fretboard.component.html index 2fbc969..f2a6a83 100644 --- a/src/app/fretboard/fretboard.component.html +++ b/src/app/fretboard/fretboard.component.html @@ -7,6 +7,7 @@ [attr.data-fret]="line.fret" [attr.data-string]="note.string.name" [attr.data-note]="note.value" + [class.active]="currentNote?.equals(note)" [attr.data-degree]="key && note.degreeIn(key).value" [attr.data-mode]="key && note.modeIn(key).name" [attr.title]="key && diff --git a/src/app/fretboard/fretboard.component.scss b/src/app/fretboard/fretboard.component.scss index 2172381..942f038 100644 --- a/src/app/fretboard/fretboard.component.scss +++ b/src/app/fretboard/fretboard.component.scss @@ -36,7 +36,7 @@ /* box-shadow: 0 -2mm 0 0 #FFF;*/ /*}*/ -[data-note]:hover { +[data-note]:hover, [data-note].active { color: white; text-shadow: 0 0 1ex #000; border: solid 1pt #fff; diff --git a/src/app/fretboard/fretboard.component.ts b/src/app/fretboard/fretboard.component.ts index ed9ed9a..88d2a07 100644 --- a/src/app/fretboard/fretboard.component.ts +++ b/src/app/fretboard/fretboard.component.ts @@ -30,6 +30,9 @@ export class FretboardComponent implements OnInit, OnChanges { @Input() key? = new Key(Note.fromName('C'), Mode.fromName('I')) + @Input() + currentNote?: Note + fretboard?: Fretboard; ngOnInit(): void { diff --git a/src/app/notes.spec.ts b/src/app/notes.spec.ts new file mode 100644 index 0000000..61c62ba --- /dev/null +++ b/src/app/notes.spec.ts @@ -0,0 +1,24 @@ +import { Chord, Chords } from "./notes"; +import { Time } from "./time"; + +describe('Chords', () => { + + it('should create from | Gm | F | Eb | D |', () => { + const chords = Chords.fromAsciiChords('| Gm | F | Eb | D |') + expect(chords.length).toBe(4) + expect(chords.getChordAt(Time.fromValue('0:0'))).toEqual(new Chord('Gm')) + expect(chords.getChordAt(Time.fromValue('1:0'))).toEqual(new Chord('F')) + expect(chords.getChordAt(Time.fromValue('2:0'))).toEqual(new Chord('Eb')) + expect(chords.getChordAt(Time.fromValue('3:0'))).toEqual(new Chord('D')) + }); + + it('should create from | Gm F | Eb D |', () => { + const chords = Chords.fromAsciiChords('| Gm F | Eb D |') + expect(chords.length).toBe(4) + expect(chords.getChordAt(Time.fromValue('0:0'))).toEqual(new Chord('Gm')) + expect(chords.getChordAt(Time.fromValue('0:2'))).toEqual(new Chord('F')) + expect(chords.getChordAt(Time.fromValue('1:0'))).toEqual(new Chord('Eb')) + expect(chords.getChordAt(Time.fromValue('1:2'))).toEqual(new Chord('D')) + }); + +}); diff --git a/src/app/notes.ts b/src/app/notes.ts index fdbd32b..777f91a 100644 --- a/src/app/notes.ts +++ b/src/app/notes.ts @@ -1,3 +1,5 @@ +import { Time } from "./time"; + export const NOTE_NAMES = [ 'C', 'C#', @@ -26,9 +28,14 @@ class Mod12Value { if (value === -1) throw new Error('invalid name : ' + name); return value; } + + equals(note: Note): boolean { + return this.value === note.value; + } } export class Note extends Mod12Value { + constructor(value: number) { super(value); } @@ -56,6 +63,25 @@ export class Note extends Mod12Value { modeIn(key: Key): Mode { return new Mode(key.mode.value + this.value - key.note.value); } + + override toString(): string { + return NOTE_NAMES[this.value] + } +} + +export namespace Note { + export const C = Note.fromName('C') + export const Cs = Note.fromName('C#') + export const D = Note.fromName('D') + export const Eb = Note.fromName('Eb') + export const E = Note.fromName('E') + export const F = Note.fromName('F') + export const Fs = Note.fromName('F#') + export const G = Note.fromName('G') + export const Ab = Note.fromName('Ab') + export const A = Note.fromName('A') + export const Bb = Note.fromName('Bb') + export const B = Note.fromName('B') } export const MODE_NAMES = [ @@ -89,7 +115,7 @@ export class Mode extends Mod12Value { /** Tonalité */ export class Key { - constructor(readonly note: Note, readonly mode: Mode) {} + constructor(readonly note: Note, readonly mode: Mode) { } } @@ -97,4 +123,115 @@ export class Degree extends Mod12Value { constructor(value: number) { super(value); } -} \ No newline at end of file +} + +export class Chord { + + readonly root: Note + + constructor( + readonly name: string, // TODO pour l'instant on fait simple + root?: Note, + ) { + this.root = Chord.getRootFromName(name) + } + + static getRootFromName(name: string): Note { + // TODO faire une vraie détection + if (NOTE_NAMES.includes(name)) { + return Note.fromName(name) + } + // TODO gérer '#' -> 's' + switch (name) { + case 'Gm': + return Note.G + } + throw new Error('Cannot find root from ' + name) + } + + toString(): string { + return this.name + } + +} + +export namespace Chord { + export const Gm: Chord = new Chord("Gm", Note.G) +} + +export type AsciiChords = string + +export class Chords { + + constructor( + private readonly chordsByTime: [Time, Chord][] = [], // TODO trier par time asc + ) { } + + static fromAsciiChords(asciiChords: AsciiChords): Chords { + + console.log('asciiChords', asciiChords) + + const barGroups = asciiChords.split('|').slice(1, -1).map(x => x.trim()) + if (barGroups.length === 0) { + throw new Error('Cannot find bars in AsciiChords : ' + asciiChords) + } + + const chords = new Chords() + + let time = Time.fromValue(0) + barGroups.forEach(barAsciiChords => { + + const chordGroups = barAsciiChords.split(' ') + + if (chordGroups.length === 1) { + const duration = Time.fromValue('1m') + + const chord = new Chord(barAsciiChords) + console.log(time.toBarsBeatsSixteenths(), chord.name) + chords.setChordAt(time, chord) + + time = time.add(duration) + return + } + + if (chordGroups.length === 2) { + const duration = Time.fromValue('2n') // TODO valable uniquement en 4/4 + + chordGroups.forEach(chordGroup => { + const chord = new Chord(chordGroup) + console.log(time.toBarsBeatsSixteenths(), chord.name) + chords.setChordAt(time, chord) + + time = time.add(duration) + }) + + return + } + + throw new Error('Cannot split chords in bar : ' + barAsciiChords) + + }) + + return chords + } + + setChordAt(time: Time, chord: Chord) { + // TODO trier par time asc + this.chordsByTime.push([time, chord]) + } + + getChordAt(time: Time): Chord | undefined { + // TODO factoriser avec getCurrentPattern + const reversedChordsByTime = [... this.chordsByTime].reverse() + const chordAtTime = reversedChordsByTime.find(([chordTime]) => chordTime.isBeforeOrEquals(time)); + return chordAtTime?.[1] + } + + get length(): number { + return this.chordsByTime.length + } + + toString(): string { + return this.chordsByTime.map(([chordTime, chord]) => `${chordTime.toBarsBeatsSixteenths()} ${chord}`).join('\n') + } +} diff --git a/src/app/rythm-sandbox/rythm-sandbox.component.html b/src/app/rythm-sandbox/rythm-sandbox.component.html index a43f899..d6c994f 100644 --- a/src/app/rythm-sandbox/rythm-sandbox.component.html +++ b/src/app/rythm-sandbox/rythm-sandbox.component.html @@ -13,6 +13,7 @@
+ Gm @@ -25,7 +26,10 @@
- + + + {{currentChord}} +
{{currentPatternInStructure.pattern.name}} : | Gm F | Eb D | @@ -33,6 +37,7 @@ [lowestFret]="1" [fretsCount]="6" [key]="currentPatternInStructure.pattern.key" + [currentNote]="currentChord?.root" > = 0 } toBarsBeatsSixteenths(): string { @@ -55,4 +73,8 @@ export class Time { toSeconds(): number { return this._toneTime.toSeconds(); } + + toString(): string { + return this.toBarsBeatsSixteenths() + } } diff --git a/src/polyfills.ts b/src/polyfills.ts new file mode 100644 index 0000000..3fdc8ce --- /dev/null +++ b/src/polyfills.ts @@ -0,0 +1,66 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * IE11 requires the following for NgClass support on SVG elements + */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. +import 'reflect-metadata'; + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 0000000..1a4a47c --- /dev/null +++ b/src/test.ts @@ -0,0 +1,22 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import {getTestBed} from '@angular/core/testing'; +import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + keys(): string[]; + (id: string): T; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100644 index 0000000..91a35fb --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "resolveJsonModule": true, + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] + } + \ No newline at end of file