1- import type { Instrument , InstrumentString , Stop , StopCalculationData } from './types.js'
1+ import type { Instrument , InstrumentString , Note , Stop , StopCalculationData } from './types.js'
2+ class NoteImpl implements Note {
3+ constructor (
4+ public name : Note [ 'name' ] ,
5+ public octave : Note [ 'octave' ] ,
6+ public alteration : Note [ 'alteration' ] ,
7+ public number : Note [ 'number' ]
8+ ) { }
9+ text ( ) {
10+ return this . name + this . alteration + ( this . octave - 1 )
11+ }
212
13+ abcnote ( ) : string {
14+ let noteName = this . name . toLowerCase ( )
15+ let alteration = ''
16+ switch ( this . alteration ) {
17+ case '#' : alteration = '^' ; break
18+ case 'b' : alteration = '_' ; break
19+ case 'x' : alteration = '^^' ; break
20+ case 'bb' : alteration = '__' ; break
21+ case '' : alteration = '' ; break
22+ }
23+ const octave = this . octave
24+ let ticks : number
25+ if ( octave <= 5 ) {
26+ noteName = noteName . toUpperCase ( )
27+ ticks = 5 - octave
28+ return `${ alteration } ${ noteName } ${ ',' . repeat ( ticks ) } `
29+ } else {
30+ ticks = octave - 6
31+ return `${ alteration } ${ noteName } ${ "'" . repeat ( ticks ) } `
32+ }
33+ }
34+ }
35+ export function tryParse ( noteText : string ) : Note | string {
36+ noteText = noteText . trim ( )
37+ if ( noteText . length < 2 ) return 'Note names must be at least two characters long'
38+ if ( noteText . length > 4 ) return 'Note names must be at most four characters long'
39+ const firstChar = noteText . charAt ( 0 ) . toUpperCase ( )
40+ const lastChar = noteText . charAt ( noteText . length - 1 )
41+ if ( firstChar < 'A' || firstChar > 'G' ) return 'First char must be A-G'
42+ if ( lastChar < '0' || lastChar > '9' ) return 'Last char must be a number'
43+ const octave = parseInt ( lastChar )
44+ if ( isNaN ( octave ) ) return 'Last char must be a number'
45+ const alteration = noteText . substring ( 1 , noteText . length - 1 )
46+ let alterationOffset = 0
47+ switch ( alteration ) {
48+ case 'bb' : alterationOffset = - 2 ; break
49+ case 'x' : alterationOffset = 1 ; break
50+ case 'b' : alterationOffset = - 1 ; break
51+ case '#' : alterationOffset = 1 ; break
52+ case '' : alterationOffset = 0 ; break
53+ default : return 'Second char must be #, b, x or bb. Text: ' + noteText + ', alteration: ' + alteration
54+ }
55+ const noteIndex = ( firstChar . charCodeAt ( 0 ) - 67 + 7 ) % 7
56+ const semitoneIndex = [ 0 , 2 , 4 , 5 , 7 , 9 , 11 ]
57+ const number =
58+ ( semitoneIndex [ noteIndex ] ?? NaN ) + alterationOffset + ( octave + 1 ) * 12
59+ return new NoteImpl (
60+ firstChar as Note [ 'name' ] ,
61+ octave + 1 ,
62+ alteration as Note [ 'alteration' ] ,
63+ number
64+ )
65+ }
66+ export function parse ( noteText : string ) : Note {
67+ const result = tryParse ( noteText )
68+ if ( typeof result === 'string' ) throw result
69+ return result
70+ }
371/**
472 * Gets the MIDI note number from a text representation (middle C is 'C4')
573 * @param noteName Can include sharp (#) and flat (b) alterations, like Db5 or F#2.
674 * @returns The MIDI note number or a string with an explanatory error message.
775 */
876export function noteNumber ( noteName : string ) : number | string {
9- noteName = noteName . trim ( )
10- if ( noteName . length < 2 ) return 'Note names must be at least two characters long'
11- let pointer = 0
12- const firstChar = noteName . charAt ( pointer ) . toUpperCase ( ) . charCodeAt ( 0 )
13- if ( firstChar < 65 || firstChar > 65 + 7 ) return 'First char must be ABCDEFG'
14-
15- const noteIndex = ( firstChar - 67 + 7 ) % 7
16- const semitoneIndex = [ 0 , 2 , 4 , 5 , 7 , 9 , 11 ]
17-
18- pointer ++
19- let alteration = 0
20- if ( noteName . charAt ( pointer ) === '#' ) {
21- alteration = 1
22- pointer ++
23- } else if ( noteName . charAt ( pointer ) === 'b' ) {
24- alteration = - 1
25- pointer ++
26- }
27- const octave = parseInt ( noteName . charAt ( pointer ) )
28- if ( isNaN ( octave ) ) return 'Last char must be a number'
29- pointer ++
30- if ( pointer !== noteName . length ) return 'Note name has extra characters'
31-
32- return ( semitoneIndex [ noteIndex ] ?? NaN ) + alteration + ( octave + 1 ) * 12
77+ const n = tryParse ( noteName )
78+ if ( typeof n === 'string' ) return n
79+ return n . number
3380}
3481
3582export function nn ( noteName : string ) {
@@ -50,19 +97,7 @@ export function noteName(noteNumber: number): string {
5097}
5198
5299export function abcnote ( noteNumber : number ) : string {
53- const noteIndex = noteNumber % 12
54- let noteName = [ 'c' , 'c' , 'd' , 'd' , 'e' , 'f' , 'f' , 'g' , 'g' , 'a' , 'a' , 'b' ] [ noteIndex ] ?? ""
55- const alteration = [ '' , '^' , '' , '^' , '' , '' , '^' , '' , '^' , '' , '^' , '' ] [ noteIndex ] ?? ""
56- const octave = Math . floor ( noteNumber / 12 )
57- let ticks : number
58- if ( octave <= 5 ) {
59- noteName = noteName . toUpperCase ( )
60- ticks = 5 - octave
61- return `${ alteration } ${ noteName } ${ ',' . repeat ( ticks ) } `
62- } else {
63- ticks = octave - 6
64- return `${ alteration } ${ noteName } ${ "'" . repeat ( ticks ) } `
65- }
100+ return parse ( noteName ( noteNumber ) ) . abcnote ( )
66101}
67102
68103function * stopsForString < TString extends InstrumentString > (
0 commit comments