tuning-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ import Fraction, { type FractionInput } from 'fraction.js';
2
+ import { Harmonic } from './Harmonic';
3
+ import { type HarmonicData, type SpectrumData, isHarmonicData } from '../lib';
4
+
5
+ /**
6
+ * Represents a spectrum as a collection of harmonics.
7
+ * Uses Map to ensure no duplicate frequencies.
8
+ * Key is the frequency as a string (e.g., "3/2") for exact matching.
9
+ *
10
+ * @example
11
+ * // Empty spectrum
12
+ * const s1 = new Spectrum();
13
+ *
14
+ * // Add harmonics using various representations
15
+ * s1.add(new Harmonic(3, 0.5, 0));
16
+ * s1.add(2, 0.3, Math.PI);
17
+ * s1.add({ frequency: '440', amplitude: 0.2, phase: Math.PI / 2 });
18
+ *
19
+ * @example
20
+ * // From SpectrumData
21
+ * const s2 = new Spectrum([
22
+ * { frequency: '110', amplitude: 0.5, phase: 0 },
23
+ * { frequency: '220', amplitude: 0.3, phase: Math.PI }
24
+ * ]);
25
+ */
26
+ export class Spectrum {
27
+ private harmonics: Map<string, Harmonic>;
28
+
29
+ constructor(data?: SpectrumData) {
30
+ this.harmonics = new Map();
31
+
32
+ if (data) {
33
+ for (const harmonicData of data) {
34
+ const harmonic = new Harmonic(harmonicData);
35
+ this.addHarmonic(harmonic);
36
+ }
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Number of harmonics in spectrum
42
+ */
43
+ get size(): number {
44
+ return this.harmonics.size;
45
+ }
46
+
47
+ /**
48
+ * Add a harmonic to the spectrum.
49
+ * If harmonic with same frequency exists, replaces it.
50
+ */
51
+ private addHarmonic(harmonic: Harmonic): this {
52
+ const key = harmonic.frequencyStr;
53
+ this.harmonics.set(key, harmonic);
54
+
55
+ return this;
56
+ }
57
+
58
+ /**
59
+ * Convenience method to add harmonic from various input types
60
+ */
61
+ add(harmonic: Harmonic): this;
62
+ add(data: HarmonicData): this;
63
+ add(frequency: FractionInput, amplitude?: number, phase?: number): this;
64
+ add(harmonicOrDataOrFreq: Harmonic | HarmonicData | FractionInput, amplitude?: number, phase?: number): this {
65
+ if (harmonicOrDataOrFreq instanceof Harmonic) {
66
+ // Harmonic case
67
+ this.addHarmonic(harmonicOrDataOrFreq);
68
+ } else if (isHarmonicData(harmonicOrDataOrFreq)) {
69
+ // HarmonicData case
70
+ const harmonic = new Harmonic(harmonicOrDataOrFreq);
71
+ this.addHarmonic(harmonic);
72
+ } else {
73
+ // FractionInput case: frequency, amplitude, phase
74
+ const harmonic = new Harmonic(harmonicOrDataOrFreq as FractionInput, amplitude ?? 1, phase ?? 0);
75
+ this.addHarmonic(harmonic);
76
+ }
77
+
78
+ return this;
79
+ }
80
+
81
+ /**
82
+ * Get harmonic by frequency (FractionInput) or Harmonic class
83
+ */
84
+ get(harmonicOrFrequency: Harmonic | FractionInput): Harmonic | undefined {
85
+ if (harmonicOrFrequency instanceof Harmonic) {
86
+ const key = harmonicOrFrequency.frequencyStr;
87
+ return this.harmonics.get(key);
88
+ } else {
89
+ const key = new Fraction(harmonicOrFrequency).toFraction();
90
+ return this.harmonics.get(key);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Check if spectrum contains a harmonic with by frequency (FractionInput) or Harmonic class
96
+ */
97
+ has(harmonicOrFrequency: Harmonic | FractionInput): boolean {
98
+ if (harmonicOrFrequency instanceof Harmonic) {
99
+ const key = harmonicOrFrequency.frequencyStr;
100
+ return this.harmonics.has(key);
101
+ } else {
102
+ const key = new Fraction(harmonicOrFrequency).toFraction();
103
+ return this.harmonics.has(key);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Remove harmonic by frequency (FractionInput) or Harmonic class
109
+ */
110
+ remove(harmonicOrFrequency: Harmonic | FractionInput): this {
111
+ if (harmonicOrFrequency instanceof Harmonic) {
112
+ const key = harmonicOrFrequency.frequencyStr;
113
+ this.harmonics.delete(key);
114
+ } else {
115
+ const key = new Fraction(harmonicOrFrequency).toFraction();
116
+ this.harmonics.delete(key);
117
+ }
118
+
119
+ return this;
120
+ }
121
+
122
+ /**
123
+ * Get harmonics sorted by frequency (ascending)
124
+ */
125
+ getHarmonics(): Harmonic[] {
126
+ return Array.from(this.harmonics.values()).sort((a, b) => a.compareFrequency(b));
127
+ }
128
+
129
+ /**
130
+ * Get all frequencies as an array
131
+ */
132
+ getFrequenciesAsNumbers(): number[] {
133
+ return this.getHarmonics().map(h => h.frequencyNum);
134
+ }
135
+
136
+ /**
137
+ * TODO: UNTESTED
138
+ * Get all keys (frequency strings) as an unsorted array.
139
+ * Useful for iterating over harmonics.
140
+ */
141
+ getKeys(): string[] {
142
+ return Array.from(this.harmonics.keys());
143
+ }
144
+
145
+ /**
146
+ * Check if spectrum is empty
147
+ */
148
+ isEmpty(): boolean {
149
+ return this.size === 0;
150
+ }
151
+
152
+ /**
153
+ * Clear all harmonics
154
+ */
155
+ clear(): this {
156
+ this.harmonics.clear();
157
+
158
+ return this;
159
+ }
160
+
161
+ /**
162
+ * Method to transpose a single harmonic in place
163
+ * Removes the old harmonic and adds the transposed one
164
+ */
165
+ transposeHarmonic(harmonicOrFrequency: Harmonic | FractionInput, ratio: FractionInput): this {
166
+ const oldHarmonic = this.get(harmonicOrFrequency);
167
+
168
+ if (!oldHarmonic) return this;
169
+
170
+ const oldKey = oldHarmonic.frequencyStr;
171
+ const newHarmonic = oldHarmonic.toTransposed(ratio);
172
+ const newKey = newHarmonic.frequencyStr;
173
+
174
+ if (oldKey === newKey) return this;
175
+
176
+ this.harmonics.delete(oldKey);
177
+ this.harmonics.set(newKey, newHarmonic);
178
+
179
+ return this
180
+ }
181
+
182
+ /**
183
+ * Transpose entire spectrum by a ratio in place
184
+ */
185
+ transpose(ratio: FractionInput): this {
186
+ const r = new Fraction(ratio);
187
+ const ratioValue = r.valueOf();
188
+
189
+ if (ratioValue === 1) return this;
190
+ if (ratioValue <= 0) throw new Error('Ratio must be greater than 0')
191
+
192
+ const allHarmonics = this.getHarmonics();
193
+
194
+ // If transposing up, start from highest frequency and go down
195
+ if (ratioValue > 1) {
196
+ for (let i = allHarmonics.length - 1; i >= 0; i--) {
197
+ this.transposeHarmonic(allHarmonics[i]!, r);
198
+ }
199
+ return this;
200
+ }
201
+
202
+ for (let i = 0; i < allHarmonics.length; i++) {
203
+ this.transposeHarmonic(allHarmonics[i]!, r);
204
+ }
205
+
206
+ return this;
207
+ }
208
+
209
+ toTransposed(ratio: FractionInput): Spectrum {
210
+ return this.clone().transpose(ratio);
211
+ }
212
+
213
+ /**
214
+ * Scale all amplitudes by a factor
215
+ */
216
+ scaleAmplitudes(factor: number): this {
217
+ for (const harmonic of this.harmonics.values()) {
218
+ harmonic.scale(factor);
219
+ }
220
+
221
+ return this
222
+ }
223
+
224
+ /**
225
+ * Get the lowest frequency harmonic
226
+ */
227
+ getLowestHarmonic(): Harmonic | undefined {
228
+ const sorted = this.getHarmonics();
229
+ return sorted[0];
230
+ }
231
+
232
+ /**
233
+ * Get the highest frequency harmonic
234
+ */
235
+ getHighestHarmonic(): Harmonic | undefined {
236
+ const sorted = this.getHarmonics();
237
+ return sorted[sorted.length - 1];
238
+ }
239
+
240
+ /**
241
+ * Calculate GCD of all frequency ratios
242
+ */
243
+ private getGCD(): Fraction | undefined {
244
+ const frequencies = this.getHarmonics().map(h => h.frequency);
245
+
246
+ if (frequencies.length === 0) return undefined;
247
+
248
+ let gcd = frequencies[0]!;
249
+
250
+ for (let i = 1; i < frequencies.length; i++) {
251
+ const freq = frequencies[i];
252
+ if (freq) gcd = gcd.gcd(freq);
253
+ }
254
+
255
+ return gcd;
256
+ }
257
+
258
+ /**
259
+ * Calculate period of the resulting wave
260
+ */
261
+ getPeriod(): Fraction | undefined {
262
+ const gcd = this.getGCD();
263
+
264
+ if (!gcd) return undefined;
265
+
266
+ return gcd.inverse();
267
+ }
268
+
269
+ /**
270
+ * Create a copy of this spectrum
271
+ */
272
+ clone(): Spectrum {
273
+ const copy = new Spectrum();
274
+
275
+ for (const harmonic of this.harmonics.values()) {
276
+ copy.addHarmonic(harmonic.clone());
277
+ }
278
+
279
+ return copy;
280
+ }
281
+
282
+ /**
283
+ * Iterate over harmonics
284
+ */
285
+ forEach(callback: (harmonic: Harmonic, key: string) => void): void {
286
+ this.harmonics.forEach(callback);
287
+ }
288
+
289
+ /**
290
+ * Serialize spectrum to JSON
291
+ */
292
+ toJSON(): SpectrumData {
293
+ const data: SpectrumData = [];
294
+
295
+ for (const harmonic of this.harmonics.values()) {
296
+ data.push(harmonic.toJSON());
297
+ }
298
+
299
+ return data;
300
+ }
301
+
302
+ /**
303
+ * Create harmonic spectrum (110 Hz, 220 Hz, 330 Hz, 440 Hz, ...)
304
+ */
305
+ static harmonic(count: number, fundamentalHz: FractionInput): Spectrum {
306
+ const spectrum = new Spectrum();
307
+
308
+ for (let i = 1; i <= count; i++) {
309
+ spectrum.add(new Fraction(fundamentalHz).mul(i), 1 / i);
310
+ }
311
+
312
+ return spectrum;
313
+ }
314
+
315
+ /**
316
+ * Create spectrum from arrays of arbitrary frequencies, amplitudes and phases
317
+ */
318
+ static fromFrequencies(
319
+ frequencies: FractionInput[],
320
+ amplitudes?: number[],
321
+ phases?: number[],
322
+ ): Spectrum {
323
+ const spectrum = new Spectrum();
324
+
325
+ frequencies.forEach((frequency, i) => {
326
+ const amp = amplitudes?.[i] ?? 1;
327
+ const phase = phases?.[i] ?? 0;
328
+ spectrum.add(frequency, amp, phase);
329
+ });
330
+
331
+ return spectrum;
332
+ }
333
+ }
@@ -0,0 +1,3 @@
1
+ export * from './Harmonic';
2
+ export * from './Spectrum';
3
+ export * from './IntervalSet';
package/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './classes';
2
+ export * from './lib';
package/lib/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './utils';
2
+ export * from './types';
package/lib/types.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Plain object for serialization/deserialization
3
+ */
4
+ export type HarmonicData = {
5
+ frequency: {
6
+ n: number;
7
+ d: number;
8
+ };
9
+ amplitude: number;
10
+ phase: number;
11
+ }
12
+
13
+ export type SpectrumData = HarmonicData[]
package/lib/utils.ts ADDED
@@ -0,0 +1,47 @@
1
+ import Fraction, { type FractionInput } from 'fraction.js';
2
+ import type { HarmonicData } from './types';
3
+
4
+ function round(num: number, places: number = 0): number {
5
+ return Math.round(num * Math.pow(10, places)) / Math.pow(10, places);
6
+ }
7
+
8
+ /**
9
+ * Convert a frequency ratio to cents.
10
+ * @param ratio - The ratio as FractionInput (number, string, array, object, or Fraction)
11
+ * @param places - Number of decimal places to round to (default: 0, no decimals)
12
+ * @returns The ratio in cents (1200 * log2(ratio)), rounded to the specified number of decimals
13
+ */
14
+ export function ratioToCents(ratio: FractionInput, places: number = 0): Fraction {
15
+ const val = new Fraction(ratio).valueOf();
16
+ const num = 1200 * Math.log2(val);
17
+ const rounded = round(num, places);
18
+
19
+ return new Fraction(rounded);
20
+ }
21
+
22
+ /**
23
+ * Convert cents to a frequency ratio.
24
+ * @param cents - The interval in cents as FractionInput (number, string, array, object, or Fraction)
25
+ * @param places - Number of decimal places to round to (default: 0, no decimals)
26
+ * @returns The ratio as a Fraction object (2^(cents/1200)), rounded to the specified number of decimals
27
+ */
28
+ export function centsToRatio(cents: FractionInput, places: number = 0): Fraction {
29
+ const val = new Fraction(cents).valueOf();
30
+ const num = Math.pow(2, val / 1200);
31
+ const rounded = round(num, places);
32
+
33
+ return new Fraction(rounded);
34
+ }
35
+
36
+ /**
37
+ * Type guard to check if a value is HarmonicData
38
+ */
39
+ export function isHarmonicData(value: unknown): value is HarmonicData {
40
+ return (
41
+ typeof value === 'object' &&
42
+ value !== null &&
43
+ 'frequency' in value &&
44
+ 'amplitude' in value &&
45
+ 'phase' in value
46
+ );
47
+ }
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "tuning-core",
3
+ "version": "0.1.0",
4
+ "module": "index.ts",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "npx tsc",
10
+ "publish": "npx tsc && bun publish --access public"
11
+ },
12
+ "devDependencies": {
13
+ "@types/bun": "latest"
14
+ },
15
+ "peerDependencies": {
16
+ "typescript": "^5"
17
+ },
18
+ "dependencies": {
19
+ "fraction.js": "^5.3.4"
20
+ }
21
+ }