react-native-tuner-engine 1.0.0 → 1.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.
Files changed (39) hide show
  1. package/android/src/main/cpp/TunerEngineJni.cpp +16 -0
  2. package/android/src/main/java/com/tunerengine/TunerEngineModule.kt +79 -47
  3. package/cpp/build/CMakeFiles/Makefile.cmake +0 -66
  4. package/cpp/build/CMakeFiles/tuner_engine_core.dir/compiler_depend.internal +9707 -0
  5. package/cpp/build/CMakeFiles/tuner_engine_core.dir/compiler_depend.make +11357 -2
  6. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/AudioFrameDispatcher.cpp.o +0 -0
  7. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/NoteMapper.cpp.o +0 -0
  8. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/Pipeline.cpp.o +0 -0
  9. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/TunerEngine.cpp.o +0 -0
  10. package/cpp/build/libtuner_engine_core.a +0 -0
  11. package/cpp/include/AudioFrameDispatcher.hpp +17 -0
  12. package/cpp/include/NoteMapper.hpp +21 -0
  13. package/cpp/include/Pipeline.hpp +1 -0
  14. package/cpp/include/TunerEngine.hpp +1 -0
  15. package/cpp/src/AudioFrameDispatcher.cpp +43 -4
  16. package/cpp/src/NoteMapper.cpp +14 -2
  17. package/cpp/src/Pipeline.cpp +4 -0
  18. package/cpp/src/TunerEngine.cpp +4 -0
  19. package/cpp/tests/build/CMakeFiles/Makefile.cmake +0 -66
  20. package/cpp/tests/build/CMakeFiles/tuner_engine_bench.dir/compiler_depend.internal +817 -0
  21. package/cpp/tests/build/CMakeFiles/tuner_engine_bench.dir/compiler_depend.make +2440 -2
  22. package/cpp/tests/build/CMakeFiles/tuner_engine_tests.dir/compiler_depend.internal +827 -0
  23. package/cpp/tests/build/CMakeFiles/tuner_engine_tests.dir/compiler_depend.make +2470 -2
  24. package/cpp/tests/build/CMakeFiles/tuner_engine_tests.dir/main.cpp.o +0 -0
  25. package/cpp/tests/build/tuner_engine_bench +0 -0
  26. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/compiler_depend.internal +9707 -0
  27. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/compiler_depend.make +11357 -2
  28. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/AudioFrameDispatcher.cpp.o +0 -0
  29. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/NoteMapper.cpp.o +0 -0
  30. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/Pipeline.cpp.o +0 -0
  31. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/TunerEngine.cpp.o +0 -0
  32. package/cpp/tests/build/tuner_engine_core/libtuner_engine_core.a +0 -0
  33. package/cpp/tests/build/tuner_engine_tests +0 -0
  34. package/ios/TunerBridge.mm +6 -1
  35. package/lib/module/TunerEngine.js +74 -2
  36. package/lib/module/TunerEngine.js.map +1 -1
  37. package/lib/typescript/src/TunerEngine.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/TunerEngine.ts +64 -2
Binary file
@@ -66,6 +66,9 @@
66
66
  if (opts[@"onsetDetection"]) {
67
67
  _dispatcher->setOnsetDetectionEnabled([opts[@"onsetDetection"] boolValue]);
68
68
  }
69
+ if (opts[@"adaptiveFrameSize"] != nil) {
70
+ _dispatcher->setAdaptiveFrameSize([opts[@"adaptiveFrameSize"] boolValue]);
71
+ }
69
72
  }
70
73
 
71
74
  - (void)startWithCompletion:(void(^)(NSError* _Nullable error))completion {
@@ -124,7 +127,9 @@
124
127
  if (_dispatcher) _dispatcher->setTuning(std::string([name UTF8String]));
125
128
  }
126
129
 
127
- - (void)setTemperament:(NSString *)name {}
130
+ - (void)setTemperament:(NSString *)name {
131
+ if (_dispatcher) _dispatcher->setTemperament(std::string([name UTF8String]));
132
+ }
128
133
 
129
134
  - (NSDictionary *)getStatus {
130
135
  return @{
@@ -1,10 +1,47 @@
1
1
  "use strict";
2
2
 
3
- import { DeviceEventEmitter } from 'react-native';
3
+ import { Platform, DeviceEventEmitter } from 'react-native';
4
4
  import NativeTunerEngine from "./NativeTunerEngine.js";
5
+ const QUALITY_PRESETS = {
6
+ 'low-latency': {
7
+ frameSize: 1024,
8
+ overlapRatio: 0
9
+ },
10
+ 'balanced': {
11
+ frameSize: 2048,
12
+ overlapRatio: 0.5
13
+ },
14
+ 'high-accuracy': {
15
+ frameSize: 4096,
16
+ overlapRatio: 0.75
17
+ }
18
+ };
5
19
  class TunerEngine {
6
20
  configure(opts) {
7
- return NativeTunerEngine.configure(opts);
21
+ const {
22
+ quality,
23
+ adaptiveFrameSize,
24
+ ...rest
25
+ } = opts;
26
+ const resolved = {
27
+ ...rest
28
+ };
29
+
30
+ // quality preset overrides frameSize and overlapRatio
31
+ if (quality && QUALITY_PRESETS[quality]) {
32
+ const preset = QUALITY_PRESETS[quality];
33
+ resolved.frameSize = preset.frameSize;
34
+ resolved.overlapRatio = preset.overlapRatio;
35
+ }
36
+
37
+ // If user explicitly provided frameSize or quality, disable adaptive frame sizing
38
+ // so setInstrument won't override the chosen frame size.
39
+ if (resolved.frameSize !== undefined || quality !== undefined) {
40
+ resolved.adaptiveFrameSize = false;
41
+ } else {
42
+ resolved.adaptiveFrameSize = adaptiveFrameSize !== false;
43
+ }
44
+ return NativeTunerEngine.configure(resolved);
8
45
  }
9
46
  start() {
10
47
  return NativeTunerEngine.start();
@@ -31,6 +68,41 @@ class TunerEngine {
31
68
  return NativeTunerEngine.getStatus();
32
69
  }
33
70
  onPitch(callback) {
71
+ if (Platform.OS === 'android') {
72
+ // Bridgeless mode on Android doesn't deliver RCTDeviceEventEmitter events.
73
+ // Poll getStatus() which includes latest pitch via requestAnimationFrame.
74
+ let lastSeq = -1;
75
+ let rafId;
76
+ let stopped = false;
77
+ const poll = () => {
78
+ if (stopped) return;
79
+ try {
80
+ const s = NativeTunerEngine.getStatus();
81
+ if (s.seq !== lastSeq) {
82
+ lastSeq = s.seq;
83
+ callback({
84
+ hasPitch: s.hasPitch,
85
+ frequency: s.frequency,
86
+ confidence: s.confidence,
87
+ rmsDb: s.rmsDb,
88
+ noteName: s.noteName,
89
+ octave: s.octave,
90
+ cents: s.cents,
91
+ nearestString: s.nearestString,
92
+ stringDeviation: s.stringDeviation
93
+ });
94
+ }
95
+ } catch (_) {}
96
+ rafId = requestAnimationFrame(poll);
97
+ };
98
+ rafId = requestAnimationFrame(poll);
99
+ return () => {
100
+ stopped = true;
101
+ cancelAnimationFrame(rafId);
102
+ };
103
+ }
104
+
105
+ // iOS: JSI direct callback via global + DeviceEventEmitter fallback
34
106
  globalThis.__tunerEngineOnPitch = callback;
35
107
  const sub = DeviceEventEmitter.addListener('onPitch', callback);
36
108
  return () => {
@@ -1 +1 @@
1
- {"version":3,"names":["DeviceEventEmitter","NativeTunerEngine","TunerEngine","configure","opts","start","stop","requestPermission","setA4","hz","setInstrument","name","setTemperament","setTuning","getStatus","onPitch","callback","globalThis","__tunerEngineOnPitch","sub","addListener","undefined","remove"],"sourceRoot":"../../src","sources":["TunerEngine.ts"],"mappings":";;AAAA,SAASA,kBAAkB,QAAQ,cAAc;AACjD,OAAOC,iBAAiB,MAAM,wBAAqB;AAanD,MAAMC,WAAW,CAAC;EAChBC,SAASA,CAACC,IAAiB,EAAiB;IAC1C,OAAOH,iBAAiB,CAACE,SAAS,CAACC,IAAI,CAAC;EAC1C;EAEAC,KAAKA,CAAA,EAAkB;IACrB,OAAOJ,iBAAiB,CAACI,KAAK,CAAC,CAAC;EAClC;EAEAC,IAAIA,CAAA,EAAkB;IACpB,OAAOL,iBAAiB,CAACK,IAAI,CAAC,CAAC;EACjC;EAEAC,iBAAiBA,CAAA,EAAqB;IACpC,OAAON,iBAAiB,CAACM,iBAAiB,CAAC,CAAC;EAC9C;EAEAC,KAAKA,CAACC,EAAU,EAAQ;IACtBR,iBAAiB,CAACO,KAAK,CAACC,EAAE,CAAC;EAC7B;EAEAC,aAAaA,CAACC,IAAgB,EAAQ;IACpCV,iBAAiB,CAACS,aAAa,CAACC,IAAI,CAAC;EACvC;EAEAC,cAAcA,CAACD,IAAiB,EAAQ;IACtCV,iBAAiB,CAACW,cAAc,CAACD,IAAI,CAAC;EACxC;EAEAE,SAASA,CAACF,IAAuB,EAAQ;IACvCV,iBAAiB,CAACY,SAAS,CAACF,IAAI,CAAC;EACnC;EAEAG,SAASA,CAAA,EAAiB;IACxB,OAAOb,iBAAiB,CAACa,SAAS,CAAC,CAAC;EACtC;EAEAC,OAAOA,CAACC,QAAuB,EAAe;IAC3CC,UAAU,CAASC,oBAAoB,GAAGF,QAAQ;IACnD,MAAMG,GAAG,GAAGnB,kBAAkB,CAACoB,WAAW,CAAC,SAAS,EAAEJ,QAAQ,CAAC;IAC/D,OAAO,MAAM;MACVC,UAAU,CAASC,oBAAoB,GAAGG,SAAS;MACpDF,GAAG,CAACG,MAAM,CAAC,CAAC;IACd,CAAC;EACH;AACF;AAEA,eAAe,IAAIpB,WAAW,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["Platform","DeviceEventEmitter","NativeTunerEngine","QUALITY_PRESETS","frameSize","overlapRatio","TunerEngine","configure","opts","quality","adaptiveFrameSize","rest","resolved","preset","undefined","start","stop","requestPermission","setA4","hz","setInstrument","name","setTemperament","setTuning","getStatus","onPitch","callback","OS","lastSeq","rafId","stopped","poll","s","seq","hasPitch","frequency","confidence","rmsDb","noteName","octave","cents","nearestString","stringDeviation","_","requestAnimationFrame","cancelAnimationFrame","globalThis","__tunerEngineOnPitch","sub","addListener","remove"],"sourceRoot":"../../src","sources":["TunerEngine.ts"],"mappings":"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","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"TunerEngine.d.ts","sourceRoot":"","sources":["../../../src/TunerEngine.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,UAAU,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACb,MAAM,SAAS,CAAC;AAEjB,KAAK,aAAa,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AACjD,KAAK,WAAW,GAAG,MAAM,IAAI,CAAC;AAE9B,cAAM,WAAW;IACf,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC;IAIrC,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAIvB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IAIrC,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAIvC,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,EAAE,GAAG,IAAI;IAIxC,SAAS,IAAI,YAAY;IAIzB,OAAO,CAAC,QAAQ,EAAE,aAAa,GAAG,WAAW;CAQ9C;;AAED,wBAAiC"}
1
+ {"version":3,"file":"TunerEngine.d.ts","sourceRoot":"","sources":["../../../src/TunerEngine.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,UAAU,EAEV,WAAW,EACX,WAAW,EACX,YAAY,EACb,MAAM,SAAS,CAAC;AAEjB,KAAK,aAAa,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AACjD,KAAK,WAAW,GAAG,MAAM,IAAI,CAAC;AAQ9B,cAAM,WAAW;IACf,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB3C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC;IAIrC,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAIvB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IAIrC,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAIvC,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,EAAE,GAAG,IAAI;IAIxC,SAAS,IAAI,YAAY;IAIzB,OAAO,CAAC,QAAQ,EAAE,aAAa,GAAG,WAAW;CA6C9C;;AAED,wBAAiC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-tuner-engine",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A high-performance React Native Turbo Module for real-time instrument tuning with native audio processing and pitch detection.",
5
5
  "main": "./lib/module/index.js",
6
6
  "source": "./src/index.tsx",
@@ -1,9 +1,10 @@
1
- import { DeviceEventEmitter } from 'react-native';
1
+ import { Platform, DeviceEventEmitter } from 'react-native';
2
2
  import NativeTunerEngine from './NativeTunerEngine';
3
3
  import type {
4
4
  EngineStatus,
5
5
  Instrument,
6
6
  PitchEvent,
7
+ QualityPreset,
7
8
  Temperament,
8
9
  TunerConfig,
9
10
  TuningPreset,
@@ -12,9 +13,33 @@ import type {
12
13
  type PitchCallback = (event: PitchEvent) => void;
13
14
  type Unsubscribe = () => void;
14
15
 
16
+ const QUALITY_PRESETS: Record<QualityPreset, { frameSize: number; overlapRatio: number }> = {
17
+ 'low-latency': { frameSize: 1024, overlapRatio: 0 },
18
+ 'balanced': { frameSize: 2048, overlapRatio: 0.5 },
19
+ 'high-accuracy': { frameSize: 4096, overlapRatio: 0.75 },
20
+ };
21
+
15
22
  class TunerEngine {
16
23
  configure(opts: TunerConfig): Promise<void> {
17
- return NativeTunerEngine.configure(opts);
24
+ const { quality, adaptiveFrameSize, ...rest } = opts;
25
+ const resolved = { ...rest };
26
+
27
+ // quality preset overrides frameSize and overlapRatio
28
+ if (quality && QUALITY_PRESETS[quality]) {
29
+ const preset = QUALITY_PRESETS[quality];
30
+ resolved.frameSize = preset.frameSize;
31
+ resolved.overlapRatio = preset.overlapRatio;
32
+ }
33
+
34
+ // If user explicitly provided frameSize or quality, disable adaptive frame sizing
35
+ // so setInstrument won't override the chosen frame size.
36
+ if (resolved.frameSize !== undefined || quality !== undefined) {
37
+ (resolved as any).adaptiveFrameSize = false;
38
+ } else {
39
+ (resolved as any).adaptiveFrameSize = adaptiveFrameSize !== false;
40
+ }
41
+
42
+ return NativeTunerEngine.configure(resolved);
18
43
  }
19
44
 
20
45
  start(): Promise<void> {
@@ -50,6 +75,43 @@ class TunerEngine {
50
75
  }
51
76
 
52
77
  onPitch(callback: PitchCallback): Unsubscribe {
78
+ if (Platform.OS === 'android') {
79
+ // Bridgeless mode on Android doesn't deliver RCTDeviceEventEmitter events.
80
+ // Poll getStatus() which includes latest pitch via requestAnimationFrame.
81
+ let lastSeq = -1;
82
+ let rafId: number;
83
+ let stopped = false;
84
+
85
+ const poll = () => {
86
+ if (stopped) return;
87
+ try {
88
+ const s = NativeTunerEngine.getStatus() as any;
89
+ if (s.seq !== lastSeq) {
90
+ lastSeq = s.seq;
91
+ callback({
92
+ hasPitch: s.hasPitch,
93
+ frequency: s.frequency,
94
+ confidence: s.confidence,
95
+ rmsDb: s.rmsDb,
96
+ noteName: s.noteName,
97
+ octave: s.octave,
98
+ cents: s.cents,
99
+ nearestString: s.nearestString,
100
+ stringDeviation: s.stringDeviation,
101
+ } as PitchEvent);
102
+ }
103
+ } catch (_) {}
104
+ rafId = requestAnimationFrame(poll);
105
+ };
106
+ rafId = requestAnimationFrame(poll);
107
+
108
+ return () => {
109
+ stopped = true;
110
+ cancelAnimationFrame(rafId);
111
+ };
112
+ }
113
+
114
+ // iOS: JSI direct callback via global + DeviceEventEmitter fallback
53
115
  (globalThis as any).__tunerEngineOnPitch = callback;
54
116
  const sub = DeviceEventEmitter.addListener('onPitch', callback);
55
117
  return () => {