1 /**
2 
3    In log scale, 0 is C5, -3 is A4 (440Hz)
4 
5 */
6 
7 module soundtab.ui.noteutil;
8 
9 import std..string : format;
10 import std.math : exp, log, exp2, log2, floor;
11 import dlangui.core.logger;
12 
13 immutable double HALF_TONE = 1.05946309435929530980;
14 immutable double QUARTER_TONE = 1.02930223664349207440;
15 
16 immutable double BASE_FREQUENCY = 440.0;
17 
18 /// Convert frequency to log scale, 0 = BASE_FREQUENCY (440hz + 3halftones), +1 == + half tone, +12 == + octave (880Hz), -12 == -octave (220hz)
19 double toLogScale(double freq) {
20     if (freq < 8)
21         return -6 * 12;
22     return log2(freq / BASE_FREQUENCY) * 12 - 3;
23 }
24 
25 /// Convert from note (log scale) to frequency (-3 -> 440, 9 -> 880, -15 -> 220)
26 double fromLogScale(double note) {
27     return exp2((note + 3) / 12) * BASE_FREQUENCY;
28 }
29 
30 /// returns index 0..11 of nearest note (0=A, 1=A#, 2=B, .. 11=G#)
31 int getNoteIndex(double note) {
32     int nn = getNearestNote(note);
33     while (nn < 0)
34         nn += 12;
35     nn %= 12;
36     return nn;
37 }
38 
39 /// returns nearest int note
40 int getNearestNote(double note) {
41     return cast(int)floor(note + 0.5);
42 }
43 
44 int getNoteOctave(double note) {
45     int n = getNearestNote(note);
46     return (n + 12*5) / 12;
47 }
48 
49 immutable dstring[10] OCTAVE_NAMES = [
50     "0", "1", "2",  "3",  "4", "5",  "6", "7",  "8", "9"
51 ];
52 
53 dstring getNoteOctaveName(double note) {
54     int oct = getNoteOctave(note);
55     if (oct < 0 || oct > 8)
56         return " ";
57     return OCTAVE_NAMES[oct];
58 }
59 
60 // 0  1  2  3  4  5  6  7  8  9  10 11
61 // C  C# D  D# E  F  F# G  G# A  A# B
62 immutable static bool[12] BLACK_NOTES = [
63     false, // C
64     true,  // C#
65     false, // D
66     true,  // D#
67     false, // E
68     false, // F
69     true,  // F#
70     false, // G
71     true,  // G#
72     false, // A
73     true,  // A#
74     false, // B
75 ];
76 
77 /// returns true for "black" - sharp note, e.g. for A#, G#
78 bool isBlackNote(double note) {
79     int nn = getNoteIndex(note);
80     return BLACK_NOTES[nn];
81 }
82 
83 /// returns true for "black" - sharp note, e.g. for A#, G#
84 bool isBlackNote(int note) {
85     return BLACK_NOTES[(note + 12*8) % 12];
86 }
87 
88 immutable dstring[12] NOTE_NAMES = [
89     "C",  "C#", "D",  "D#", "E",  "F",  "F#", "G", "G#", "A", "A#", "B",
90 ];
91 
92 dstring getNoteName(double n) {
93     return NOTE_NAMES[getNoteIndex(n)];
94 }
95 
96 dstring noteToFullName(double note) {
97     return getNoteName(note) ~ getNoteOctaveName(note);
98 }
99 
100 dstring noteToFullName(int note) {
101     return getNoteName(note) ~ getNoteOctaveName(note);
102 }
103 
104 int fullNameToNote(dstring noteName) {
105     dstring note = noteName[0 .. $ - 1];
106     int octaveNumber = cast(int)(noteName[$ - 1] - '0');
107     foreach(idx, name; NOTE_NAMES) {
108         if (name == note) {
109             return cast(int)((octaveNumber - 5) * 12 + idx);
110         }
111     }
112     return 0;
113 }
114 
115 int whiteNotesInRange(int start, int end) {
116     start += 12*8;
117     end += 12*8;
118     int res = 0;
119     for (int i = start; i <= end; ) {
120         int idx = i % 12;
121         if (idx == 0 && i + 12 <= end) {
122             res += 7;
123             i += 12;
124         } else {
125             if (!BLACK_NOTES[idx])
126                 res++;
127             i++;
128         }
129     }
130     return res;
131 }
132 
133 struct PitchCorrector {
134     private int _amount;
135     @property int amount() {
136         return _amount;
137     }
138     @property void amount(int v) {
139         if (v < 0)
140             v = 0;
141         else if (v > 1000)
142             v = 1000;
143         _amount = v;
144     }
145     double correctNote(double note) {
146         import std.math : round, pow;
147         if (_amount <= 0)
148             return note;
149         if (_amount >= 1000) {
150             return round(note);
151         }
152         double v1 = note;
153         double v2 = round(note);
154         double diff = v1 - v2; // -0.5 .. 0.5
155         double diffsgn = diff > 0 ? 0.5 : -0.5; // -0.5 or 0.5
156         double diffabs = diff > 0 ? 2 * diff : - 2 * diff; // 0 .. 1
157         double p = ((_amount / 1000.0) + 1) * 1.5;
158         diffabs = pow(diffabs, p);
159         // correct diffabs
160         return v2 + diffabs * diffsgn;
161         //return v2 + diff * (1000 - _amount) / 1000;
162     }
163 
164     double correctPitch(double pitch) {
165         return fromLogScale(correctNote(toLogScale(pitch)));
166     }
167 }
168 
169 void buildNoteConversionTable() {
170     char[] buf;
171     buf.assumeSafeAppend;
172 
173     double baseFreq = fromLogScale(0);
174     buf ~= "\nstatic const float SEMITONE_FRAC[256] = {\n";
175     for (int i = 0; i < 256; i++) {
176         if ((i % 8) == 0)
177             buf ~= "    ";
178         double n = i / 256.0;
179         double freq = fromLogScale(n);
180         double frac = freq / baseFreq;
181         buf ~= "%.7ff,".format(frac);
182         if ((i % 8) == 7)
183             buf ~= "\n";
184         else
185             buf ~= " ";
186     }
187     buf ~= "};\n";
188     buf ~= "\nstatic const float NOTE_FREQ[120] = {\n";
189     for (int i = 0; i < 120; i++) {
190         if ((i % 6) == 0)
191             buf ~= "    ";
192         double n = i - 57 - 3;
193         double freq = fromLogScale(n);
194         buf ~= "%.7ff,".format(freq);
195         if ((i % 6) == 5) {
196             if (i % 12 == 5) {
197                 buf ~= " // C%d..F%d".format(i / 12, i / 12);
198             }
199             if (i % 12 == 11) {
200                 buf ~= " // F#%d..B%d".format(i / 12, i / 12);
201             }
202             buf ~= "\n";
203         } else
204             buf ~= " ";
205     }
206     buf ~= "};\n";
207 
208 
209     Log.d(buf);
210 }