1+ // @ts -ignore
2+ import { html , render } from 'https://unpkg.com/htm/preact/standalone.module.js' ;
3+ import { gcd } from "../lib/limit.js" ;
4+
5+ const edoInput = document . querySelector ( '#edo' ) as HTMLInputElement ;
6+ const scaleDiv = document . querySelector ( '#scale' ) ! ;
7+ const suggestionsDiv = document . querySelector ( '#suggestions' ) ! ;
8+ const prevModeButton = document . querySelector ( '#prevMode' ) ! ;
9+ const nextModeButton = document . querySelector ( '#nextMode' ) ! ;
10+
11+ const errorLimit = 18 ;
12+
13+ type Ratio = [ number , number ] ;
14+
15+ function ratios ( limit : number ) : Ratio [ ] {
16+ const s = new Set < number > ( ) ;
17+ const a = new Array < Ratio > ( ) ;
18+ for ( let n = 1 ; n <= limit ; n ++ ) {
19+ for ( let d = Math . ceil ( n / 2 ) ; d < n ; d ++ ) {
20+ const k = n / d ;
21+ if ( ! s . has ( k ) ) {
22+ s . add ( k ) ;
23+ a . push ( simplify ( [ n , d ] ) ) ;
24+ }
25+ }
26+ }
27+ return a ;
28+ }
29+
30+ const ratioPool = ratios ( 27 ) ;
31+ let edo = edoInput . valueAsNumber ;
32+
33+ function tenneyHeight ( r : Ratio ) : number {
34+ return Math . log2 ( r [ 0 ] * r [ 1 ] ) ;
35+ }
36+
37+ function bestMapping ( r : Ratio , et : number ) : number {
38+ return Math . round ( et * Math . log2 ( r [ 0 ] / r [ 1 ] ) / Math . log2 ( 2 ) ) ;
39+ }
40+
41+ function approximates ( n : number , et : number , r : Ratio ) : boolean {
42+ const justCents = 1200 * Math . log2 ( r [ 0 ] / r [ 1 ] ) / Math . log2 ( 2 ) ;
43+ const edoCents = 1200 * n / et ;
44+ return Math . abs ( justCents - edoCents ) < errorLimit ;
45+ }
46+
47+ function detemper ( n : number ) : Ratio | null {
48+ if ( n === 0 ) {
49+ return [ 1 , 1 ] ;
50+ }
51+ const matchedRatios = ratioPool
52+ . filter ( r => bestMapping ( r , edo ) == n && approximates ( n , edo , r ) ) ;
53+ matchedRatios . sort ( ( a , b ) => tenneyHeight ( a ) - tenneyHeight ( b ) ) ;
54+ return matchedRatios [ 0 ] ; // TODO
55+ }
56+
57+ function stepScore ( n : number ) : number {
58+ const r = detemper ( n ) ;
59+ if ( r ) {
60+ return tenneyHeight ( r ) ;
61+ } else {
62+ return 100 ;
63+ }
64+ }
65+
66+ let stepScores : number [ ] = [ ...Array ( edo ) . keys ( ) ] . map ( stepScore ) ;
67+ let scaleIntervals : number [ ] = [ 0 , bestMapping ( [ 3 , 2 ] , edo ) ] ;
68+
69+ // TODO: export to .scl
70+
71+ function simplify ( r : Ratio ) : Ratio {
72+ const f = gcd ( r ) ;
73+ return [ r [ 0 ] / f , r [ 1 ] / f ] ;
74+ }
75+
76+ function addInterval ( n : number ) {
77+ scaleIntervals . push ( n ) ;
78+ scaleIntervals . sort ( ( a , b ) => a - b ) ;
79+ renderScale ( ) ;
80+ updateDiagnostics ( ) ;
81+ }
82+
83+ function removeInterval ( n : number ) {
84+ if ( n == 0 ) return ;
85+ scaleIntervals = scaleIntervals . filter ( x => x != n ) ;
86+ renderScale ( ) ;
87+ updateDiagnostics ( ) ;
88+ }
89+
90+ function mean ( xs : number [ ] ) : number {
91+ return xs . reduce ( ( a , b ) => a + b , 0 ) / xs . length ;
92+ }
93+
94+ function complexity ( n : number , scale : number [ ] ) : number {
95+ const result = mean ( scale . map ( x => Math . pow ( stepScores [ Math . abs ( n - x ) ] , 2 ) ) ) ;
96+ return result ;
97+ }
98+
99+ function formatRatio ( r : Ratio | null ) : string {
100+ if ( r ) {
101+ return `${ r [ 0 ] } /${ r [ 1 ] } ` ;
102+ } else {
103+ return '?' ;
104+ }
105+ }
106+
107+ function renderScale ( ) {
108+ render ( html `${ scaleIntervals . map ( n => html `
109+ < button class ="note " onClick =${ ( ) => removeInterval ( n ) } >
110+ ${ n }
111+ < div class ="approx "> ${ formatRatio ( detemper ( n ) ) } </ div >
112+ </ button > ` )
113+ } `,
114+ scaleDiv ) ;
115+ const nonChordIntervals = [ ...Array ( edo ) . keys ( ) ]
116+ . filter ( x => ! scaleIntervals . includes ( x ) ) ;
117+ nonChordIntervals . sort ( ( a , b ) =>
118+ complexity ( a , scaleIntervals ) - complexity ( b , scaleIntervals ) ) ;
119+ render ( html `${ nonChordIntervals . map ( n => html `
120+ < button class ="note " onClick =${ ( ) => addInterval ( n ) } >
121+ ${ n }
122+ < div class ="approx "> ${ formatRatio ( detemper ( n ) ) } </ div >
123+ </ button > ` )
124+ } `,
125+ suggestionsDiv ) ;
126+ }
127+
128+ function updateDiagnostics ( ) {
129+ // TODO
130+ }
131+
132+ renderScale ( ) ;
133+ updateDiagnostics ( ) ;
134+
135+ edoInput . addEventListener ( 'change' , ( ) => {
136+ const newEdo = edoInput . valueAsNumber ;
137+ scaleIntervals = [ ...new Set ( scaleIntervals . map ( x => {
138+ const r = detemper ( x ) ;
139+ if ( r ) {
140+ return bestMapping ( r , newEdo ) ;
141+ } else {
142+ return 0 ;
143+ }
144+ } ) ) ] ;
145+ edo = newEdo ;
146+ stepScores = [ ...Array ( edo ) . keys ( ) ] . map ( stepScore ) ;
147+ renderScale ( ) ;
148+ } ) ;
149+
150+ prevModeButton . addEventListener ( 'click' , ( ) => {
151+ if ( scaleIntervals . length > 1 ) {
152+ const root = edo - scaleIntervals . pop ( ) ! ;
153+ scaleIntervals = scaleIntervals . map ( x => x + root ) ;
154+ scaleIntervals . unshift ( 0 ) ;
155+ renderScale ( ) ;
156+ }
157+ } ) ;
158+
159+ nextModeButton . addEventListener ( 'click' , ( ) => {
160+ if ( scaleIntervals . length > 1 ) {
161+ const root = scaleIntervals [ 1 ] ;
162+ scaleIntervals = scaleIntervals . map ( x => x - root ) ;
163+ scaleIntervals . push ( edo + scaleIntervals . shift ( ) ! ) ;
164+ renderScale ( ) ;
165+ }
166+ } ) ;
0 commit comments