react-native-tuner-engine 1.0.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 (224) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +188 -0
  3. package/TunerEngine.podspec +32 -0
  4. package/android/build.gradle +86 -0
  5. package/android/src/main/AndroidManifest.xml +3 -0
  6. package/android/src/main/cpp/CMakeLists.txt +31 -0
  7. package/android/src/main/cpp/OboeAudioSource.cpp +101 -0
  8. package/android/src/main/cpp/OboeAudioSource.h +41 -0
  9. package/android/src/main/cpp/TunerEngineJni.cpp +220 -0
  10. package/android/src/main/java/com/tunerengine/TunerEngineModule.kt +183 -0
  11. package/android/src/main/java/com/tunerengine/TunerEnginePackage.kt +31 -0
  12. package/cpp/CMakeLists.txt +25 -0
  13. package/cpp/build/CMakeCache.txt +347 -0
  14. package/cpp/build/CMakeFiles/4.3.2/CMakeCXXCompiler.cmake +102 -0
  15. package/cpp/build/CMakeFiles/4.3.2/CMakeDetermineCompilerABI_CXX.bin +0 -0
  16. package/cpp/build/CMakeFiles/4.3.2/CMakeSystem.cmake +15 -0
  17. package/cpp/build/CMakeFiles/4.3.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +949 -0
  18. package/cpp/build/CMakeFiles/4.3.2/CompilerIdCXX/a.out +0 -0
  19. package/cpp/build/CMakeFiles/4.3.2/CompilerIdCXX/apple-sdk.cpp +1 -0
  20. package/cpp/build/CMakeFiles/CMakeConfigureLog.yaml +1619 -0
  21. package/cpp/build/CMakeFiles/CMakeDirectoryInformation.cmake +16 -0
  22. package/cpp/build/CMakeFiles/InstallScripts.json +7 -0
  23. package/cpp/build/CMakeFiles/Makefile.cmake +118 -0
  24. package/cpp/build/CMakeFiles/Makefile2 +122 -0
  25. package/cpp/build/CMakeFiles/TargetDirectories.txt +3 -0
  26. package/cpp/build/CMakeFiles/cmake.check_cache +1 -0
  27. package/cpp/build/CMakeFiles/progress.marks +1 -0
  28. package/cpp/build/CMakeFiles/tuner_engine_core.dir/DependInfo.cmake +36 -0
  29. package/cpp/build/CMakeFiles/tuner_engine_core.dir/build.make +322 -0
  30. package/cpp/build/CMakeFiles/tuner_engine_core.dir/cmake_clean.cmake +37 -0
  31. package/cpp/build/CMakeFiles/tuner_engine_core.dir/cmake_clean_target.cmake +3 -0
  32. package/cpp/build/CMakeFiles/tuner_engine_core.dir/compiler_depend.make +2 -0
  33. package/cpp/build/CMakeFiles/tuner_engine_core.dir/compiler_depend.ts +2 -0
  34. package/cpp/build/CMakeFiles/tuner_engine_core.dir/depend.make +2 -0
  35. package/cpp/build/CMakeFiles/tuner_engine_core.dir/flags.make +12 -0
  36. package/cpp/build/CMakeFiles/tuner_engine_core.dir/link.txt +2 -0
  37. package/cpp/build/CMakeFiles/tuner_engine_core.dir/progress.make +16 -0
  38. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/AudioFrameDispatcher.cpp.o +0 -0
  39. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/AudioFrameDispatcher.cpp.o.d +814 -0
  40. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/BiquadHpf.cpp.o +0 -0
  41. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/BiquadHpf.cpp.o.d +206 -0
  42. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/CepstrumPitchDetector.cpp.o +0 -0
  43. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/CepstrumPitchDetector.cpp.o.d +797 -0
  44. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/EnsembleSelector.cpp.o +0 -0
  45. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/EnsembleSelector.cpp.o.d +755 -0
  46. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/NoteMapper.cpp.o +0 -0
  47. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/NoteMapper.cpp.o.d +669 -0
  48. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/OnsetDetector.cpp.o +0 -0
  49. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/OnsetDetector.cpp.o.d +648 -0
  50. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/Pipeline.cpp.o +0 -0
  51. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/Pipeline.cpp.o.d +765 -0
  52. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/PostProcessor.cpp.o +0 -0
  53. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/PostProcessor.cpp.o.d +648 -0
  54. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/PyinPitchDetector.cpp.o +0 -0
  55. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/PyinPitchDetector.cpp.o.d +755 -0
  56. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/SnrEstimator.cpp.o +0 -0
  57. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/SnrEstimator.cpp.o.d +648 -0
  58. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/StringMatcher.cpp.o +0 -0
  59. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/StringMatcher.cpp.o.d +665 -0
  60. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/TunerEngine.cpp.o +0 -0
  61. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/TunerEngine.cpp.o.d +811 -0
  62. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/Window.cpp.o +0 -0
  63. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/Window.cpp.o.d +754 -0
  64. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/YinPitchDetector.cpp.o +0 -0
  65. package/cpp/build/CMakeFiles/tuner_engine_core.dir/src/YinPitchDetector.cpp.o.d +755 -0
  66. package/cpp/build/Makefile +532 -0
  67. package/cpp/build/cmake_install.cmake +61 -0
  68. package/cpp/build/libtuner_engine_core.a +0 -0
  69. package/cpp/include/AudioFrameDispatcher.hpp +87 -0
  70. package/cpp/include/BiquadHpf.hpp +22 -0
  71. package/cpp/include/CepstrumPitchDetector.hpp +33 -0
  72. package/cpp/include/EnsembleSelector.hpp +25 -0
  73. package/cpp/include/Fft.hpp +44 -0
  74. package/cpp/include/IPitchDetector.hpp +21 -0
  75. package/cpp/include/InstrumentPresets.hpp +33 -0
  76. package/cpp/include/NoteMapper.hpp +15 -0
  77. package/cpp/include/OnsetDetector.hpp +37 -0
  78. package/cpp/include/Pipeline.hpp +55 -0
  79. package/cpp/include/PitchResult.hpp +24 -0
  80. package/cpp/include/PostProcessor.hpp +51 -0
  81. package/cpp/include/PyinPitchDetector.hpp +35 -0
  82. package/cpp/include/RingBuffer.hpp +72 -0
  83. package/cpp/include/SnrEstimator.hpp +21 -0
  84. package/cpp/include/StringMatcher.hpp +27 -0
  85. package/cpp/include/TunerEngine.hpp +32 -0
  86. package/cpp/include/TuningPresets.hpp +103 -0
  87. package/cpp/include/Window.hpp +13 -0
  88. package/cpp/include/YinPitchDetector.hpp +37 -0
  89. package/cpp/src/AudioFrameDispatcher.cpp +180 -0
  90. package/cpp/src/BiquadHpf.cpp +35 -0
  91. package/cpp/src/CepstrumPitchDetector.cpp +116 -0
  92. package/cpp/src/EnsembleSelector.cpp +91 -0
  93. package/cpp/src/NoteMapper.cpp +47 -0
  94. package/cpp/src/OnsetDetector.cpp +39 -0
  95. package/cpp/src/Pipeline.cpp +133 -0
  96. package/cpp/src/PostProcessor.cpp +111 -0
  97. package/cpp/src/PyinPitchDetector.cpp +134 -0
  98. package/cpp/src/SnrEstimator.cpp +33 -0
  99. package/cpp/src/StringMatcher.cpp +37 -0
  100. package/cpp/src/TunerEngine.cpp +67 -0
  101. package/cpp/src/Window.cpp +21 -0
  102. package/cpp/src/YinPitchDetector.cpp +118 -0
  103. package/cpp/tests/CMakeLists.txt +23 -0
  104. package/cpp/tests/bench.cpp +160 -0
  105. package/cpp/tests/build/CMakeCache.txt +356 -0
  106. package/cpp/tests/build/CMakeFiles/4.3.2/CMakeCXXCompiler.cmake +102 -0
  107. package/cpp/tests/build/CMakeFiles/4.3.2/CMakeDetermineCompilerABI_CXX.bin +0 -0
  108. package/cpp/tests/build/CMakeFiles/4.3.2/CMakeSystem.cmake +15 -0
  109. package/cpp/tests/build/CMakeFiles/4.3.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +949 -0
  110. package/cpp/tests/build/CMakeFiles/4.3.2/CompilerIdCXX/a.out +0 -0
  111. package/cpp/tests/build/CMakeFiles/4.3.2/CompilerIdCXX/apple-sdk.cpp +1 -0
  112. package/cpp/tests/build/CMakeFiles/CMakeConfigureLog.yaml +1619 -0
  113. package/cpp/tests/build/CMakeFiles/CMakeDirectoryInformation.cmake +16 -0
  114. package/cpp/tests/build/CMakeFiles/InstallScripts.json +8 -0
  115. package/cpp/tests/build/CMakeFiles/Makefile.cmake +122 -0
  116. package/cpp/tests/build/CMakeFiles/Makefile2 +211 -0
  117. package/cpp/tests/build/CMakeFiles/TargetDirectories.txt +9 -0
  118. package/cpp/tests/build/CMakeFiles/cmake.check_cache +1 -0
  119. package/cpp/tests/build/CMakeFiles/progress.marks +1 -0
  120. package/cpp/tests/build/CMakeFiles/tuner_engine_bench.dir/DependInfo.cmake +23 -0
  121. package/cpp/tests/build/CMakeFiles/tuner_engine_bench.dir/bench.cpp.o +0 -0
  122. package/cpp/tests/build/CMakeFiles/tuner_engine_bench.dir/bench.cpp.o.d +813 -0
  123. package/cpp/tests/build/CMakeFiles/tuner_engine_bench.dir/build.make +114 -0
  124. package/cpp/tests/build/CMakeFiles/tuner_engine_bench.dir/cmake_clean.cmake +11 -0
  125. package/cpp/tests/build/CMakeFiles/tuner_engine_bench.dir/compiler_depend.make +2 -0
  126. package/cpp/tests/build/CMakeFiles/tuner_engine_bench.dir/compiler_depend.ts +2 -0
  127. package/cpp/tests/build/CMakeFiles/tuner_engine_bench.dir/depend.make +2 -0
  128. package/cpp/tests/build/CMakeFiles/tuner_engine_bench.dir/flags.make +12 -0
  129. package/cpp/tests/build/CMakeFiles/tuner_engine_bench.dir/link.txt +1 -0
  130. package/cpp/tests/build/CMakeFiles/tuner_engine_bench.dir/progress.make +3 -0
  131. package/cpp/tests/build/CMakeFiles/tuner_engine_tests.dir/DependInfo.cmake +23 -0
  132. package/cpp/tests/build/CMakeFiles/tuner_engine_tests.dir/build.make +114 -0
  133. package/cpp/tests/build/CMakeFiles/tuner_engine_tests.dir/cmake_clean.cmake +11 -0
  134. package/cpp/tests/build/CMakeFiles/tuner_engine_tests.dir/compiler_depend.make +2 -0
  135. package/cpp/tests/build/CMakeFiles/tuner_engine_tests.dir/compiler_depend.ts +2 -0
  136. package/cpp/tests/build/CMakeFiles/tuner_engine_tests.dir/depend.make +2 -0
  137. package/cpp/tests/build/CMakeFiles/tuner_engine_tests.dir/flags.make +12 -0
  138. package/cpp/tests/build/CMakeFiles/tuner_engine_tests.dir/link.txt +1 -0
  139. package/cpp/tests/build/CMakeFiles/tuner_engine_tests.dir/main.cpp.o +0 -0
  140. package/cpp/tests/build/CMakeFiles/tuner_engine_tests.dir/main.cpp.o.d +823 -0
  141. package/cpp/tests/build/CMakeFiles/tuner_engine_tests.dir/progress.make +3 -0
  142. package/cpp/tests/build/CTestTestfile.cmake +9 -0
  143. package/cpp/tests/build/Makefile +247 -0
  144. package/cpp/tests/build/cmake_install.cmake +66 -0
  145. package/cpp/tests/build/tuner_engine_bench +0 -0
  146. package/cpp/tests/build/tuner_engine_core/CMakeFiles/CMakeDirectoryInformation.cmake +16 -0
  147. package/cpp/tests/build/tuner_engine_core/CMakeFiles/progress.marks +1 -0
  148. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/DependInfo.cmake +36 -0
  149. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/build.make +322 -0
  150. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/cmake_clean.cmake +37 -0
  151. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/cmake_clean_target.cmake +3 -0
  152. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/compiler_depend.make +2 -0
  153. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/compiler_depend.ts +2 -0
  154. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/depend.make +2 -0
  155. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/flags.make +12 -0
  156. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/link.txt +2 -0
  157. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/progress.make +16 -0
  158. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/AudioFrameDispatcher.cpp.o +0 -0
  159. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/AudioFrameDispatcher.cpp.o.d +814 -0
  160. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/BiquadHpf.cpp.o +0 -0
  161. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/BiquadHpf.cpp.o.d +206 -0
  162. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/CepstrumPitchDetector.cpp.o +0 -0
  163. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/CepstrumPitchDetector.cpp.o.d +797 -0
  164. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/EnsembleSelector.cpp.o +0 -0
  165. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/EnsembleSelector.cpp.o.d +755 -0
  166. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/NoteMapper.cpp.o +0 -0
  167. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/NoteMapper.cpp.o.d +669 -0
  168. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/OnsetDetector.cpp.o +0 -0
  169. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/OnsetDetector.cpp.o.d +648 -0
  170. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/Pipeline.cpp.o +0 -0
  171. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/Pipeline.cpp.o.d +765 -0
  172. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/PostProcessor.cpp.o +0 -0
  173. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/PostProcessor.cpp.o.d +648 -0
  174. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/PyinPitchDetector.cpp.o +0 -0
  175. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/PyinPitchDetector.cpp.o.d +755 -0
  176. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/SnrEstimator.cpp.o +0 -0
  177. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/SnrEstimator.cpp.o.d +648 -0
  178. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/StringMatcher.cpp.o +0 -0
  179. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/StringMatcher.cpp.o.d +665 -0
  180. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/TunerEngine.cpp.o +0 -0
  181. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/TunerEngine.cpp.o.d +811 -0
  182. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/Window.cpp.o +0 -0
  183. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/Window.cpp.o.d +754 -0
  184. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/YinPitchDetector.cpp.o +0 -0
  185. package/cpp/tests/build/tuner_engine_core/CMakeFiles/tuner_engine_core.dir/src/YinPitchDetector.cpp.o.d +755 -0
  186. package/cpp/tests/build/tuner_engine_core/Makefile +544 -0
  187. package/cpp/tests/build/tuner_engine_core/cmake_install.cmake +45 -0
  188. package/cpp/tests/build/tuner_engine_core/libtuner_engine_core.a +0 -0
  189. package/cpp/tests/build/tuner_engine_tests +0 -0
  190. package/cpp/tests/main.cpp +624 -0
  191. package/ios/IosAudioSource.h +23 -0
  192. package/ios/IosAudioSource.mm +174 -0
  193. package/ios/TunerBridge.h +24 -0
  194. package/ios/TunerBridge.mm +136 -0
  195. package/ios/TunerEngine.h +6 -0
  196. package/ios/TunerEngine.mm +144 -0
  197. package/lib/module/NativeTunerEngine.js +5 -0
  198. package/lib/module/NativeTunerEngine.js.map +1 -0
  199. package/lib/module/TunerEngine.js +43 -0
  200. package/lib/module/TunerEngine.js.map +1 -0
  201. package/lib/module/index.js +5 -0
  202. package/lib/module/index.js.map +1 -0
  203. package/lib/module/package.json +1 -0
  204. package/lib/module/types.js +2 -0
  205. package/lib/module/types.js.map +1 -0
  206. package/lib/module/useTuner.js +55 -0
  207. package/lib/module/useTuner.js.map +1 -0
  208. package/lib/typescript/package.json +1 -0
  209. package/lib/typescript/src/NativeTunerEngine.d.ts +17 -0
  210. package/lib/typescript/src/NativeTunerEngine.d.ts.map +1 -0
  211. package/lib/typescript/src/TunerEngine.d.ts +18 -0
  212. package/lib/typescript/src/TunerEngine.d.ts.map +1 -0
  213. package/lib/typescript/src/index.d.ts +4 -0
  214. package/lib/typescript/src/index.d.ts.map +1 -0
  215. package/lib/typescript/src/types.d.ts +86 -0
  216. package/lib/typescript/src/types.d.ts.map +1 -0
  217. package/lib/typescript/src/useTuner.d.ts +15 -0
  218. package/lib/typescript/src/useTuner.d.ts.map +1 -0
  219. package/package.json +141 -0
  220. package/src/NativeTunerEngine.ts +17 -0
  221. package/src/TunerEngine.ts +62 -0
  222. package/src/index.tsx +11 -0
  223. package/src/types.ts +109 -0
  224. package/src/useTuner.ts +67 -0
@@ -0,0 +1,624 @@
1
+ #include "NoteMapper.hpp"
2
+ #include "OnsetDetector.hpp"
3
+ #include "TunerEngine.hpp"
4
+ #include "YinPitchDetector.hpp"
5
+ #include "PyinPitchDetector.hpp"
6
+ #include "CepstrumPitchDetector.hpp"
7
+ #include "EnsembleSelector.hpp"
8
+ #include "BiquadHpf.hpp"
9
+ #include "AudioFrameDispatcher.hpp"
10
+ #include "InstrumentPresets.hpp"
11
+
12
+ #include <cassert>
13
+ #include <cmath>
14
+ #include <chrono>
15
+ #include <iostream>
16
+ #include <thread>
17
+ #include <vector>
18
+
19
+ static constexpr float PI = 3.14159265358979323846f;
20
+
21
+ static void assertNear(float actual, float expected, float tolerance) {
22
+ assert(std::fabs(actual - expected) <= tolerance);
23
+ }
24
+
25
+ static std::vector<float> generateSine(
26
+ float frequency,
27
+ float sampleRate,
28
+ int frameSize,
29
+ float amplitude = 0.8f
30
+ ) {
31
+ std::vector<float> buffer(frameSize);
32
+
33
+ for (int i = 0; i < frameSize; ++i) {
34
+ buffer[i] = amplitude * std::sin(2.0f * PI * frequency * i / sampleRate);
35
+ }
36
+
37
+ return buffer;
38
+ }
39
+
40
+ static std::vector<float> generateSilence(int frameSize) {
41
+ return std::vector<float>(frameSize, 0.0f);
42
+ }
43
+
44
+ static void testNoteMapper() {
45
+ NoteMapper mapper(440.0f);
46
+
47
+ {
48
+ auto result = mapper.map(82.41f, 1.0f, -12.0f);
49
+ assert(result.hasPitch);
50
+ assert(result.noteName == "E");
51
+ assert(result.octave == 2);
52
+ assertNear(result.cents, 0.0f, 1.0f);
53
+ }
54
+
55
+ {
56
+ auto result = mapper.map(110.0f, 1.0f, -12.0f);
57
+ assert(result.hasPitch);
58
+ assert(result.noteName == "A");
59
+ assert(result.octave == 2);
60
+ assertNear(result.cents, 0.0f, 1.0f);
61
+ }
62
+
63
+ {
64
+ auto result = mapper.map(329.63f, 1.0f, -12.0f);
65
+ assert(result.hasPitch);
66
+ assert(result.noteName == "E");
67
+ assert(result.octave == 4);
68
+ assertNear(result.cents, 0.0f, 1.0f);
69
+ }
70
+
71
+ {
72
+ auto result = mapper.map(440.0f, 1.0f, -12.0f);
73
+ assert(result.hasPitch);
74
+ assert(result.noteName == "A");
75
+ assert(result.octave == 4);
76
+ assertNear(result.cents, 0.0f, 1.0f);
77
+ }
78
+ }
79
+
80
+ static void testYin(float inputFrequency) {
81
+ constexpr float sampleRate = 48000.0f;
82
+ constexpr int frameSize = 4096;
83
+
84
+ auto buffer = generateSine(inputFrequency, sampleRate, frameSize);
85
+
86
+ YinPitchDetector detector(sampleRate, frameSize);
87
+ auto result = detector.detect(buffer.data(), static_cast<int>(buffer.size()));
88
+
89
+ std::cout
90
+ << "yin input: " << inputFrequency
91
+ << " detected: " << result.frequency
92
+ << " confidence: " << result.confidence
93
+ << std::endl;
94
+
95
+ assert(result.hasPitch);
96
+ assertNear(result.frequency, inputFrequency, 0.5f);
97
+ assert(result.confidence > 0.80f);
98
+ }
99
+
100
+ static void testTunerEngine(
101
+ float inputFrequency,
102
+ const std::string& expectedNote,
103
+ int expectedOctave
104
+ ) {
105
+ constexpr float sampleRate = 48000.0f;
106
+ constexpr int frameSize = 4096;
107
+
108
+ auto buffer = generateSine(inputFrequency, sampleRate, frameSize);
109
+
110
+ TunerEngine engine(sampleRate, frameSize);
111
+ auto result = engine.process(buffer.data(), static_cast<int>(buffer.size()));
112
+
113
+ std::cout
114
+ << "engine input: " << inputFrequency
115
+ << " detected: " << result.frequency
116
+ << " note: " << result.noteName << result.octave
117
+ << " cents: " << result.cents
118
+ << " confidence: " << result.confidence
119
+ << " rms: " << result.rmsDb
120
+ << std::endl;
121
+
122
+ assert(result.hasPitch);
123
+ assertNear(result.frequency, inputFrequency, 0.5f);
124
+ assert(result.noteName == expectedNote);
125
+ assert(result.octave == expectedOctave);
126
+ assert(std::fabs(result.cents) < 5.0f);
127
+ assert(result.confidence > 0.75f);
128
+ }
129
+
130
+ static void testTunerEngineSilence() {
131
+ constexpr float sampleRate = 48000.0f;
132
+ constexpr int frameSize = 4096;
133
+
134
+ auto buffer = generateSilence(frameSize);
135
+
136
+ TunerEngine engine(sampleRate, frameSize);
137
+ auto result = engine.process(buffer.data(), static_cast<int>(buffer.size()));
138
+
139
+ std::cout
140
+ << "silence hasPitch: " << result.hasPitch
141
+ << " rms: " << result.rmsDb
142
+ << std::endl;
143
+
144
+ assert(!result.hasPitch);
145
+ assert(result.rmsDb <= -100.0f);
146
+ }
147
+
148
+ static void testTunerEngineQuietSignal() {
149
+ constexpr float sampleRate = 48000.0f;
150
+ constexpr int frameSize = 4096;
151
+
152
+ auto buffer = generateSine(110.0f, sampleRate, frameSize, 0.0001f);
153
+
154
+ TunerEngine engine(sampleRate, frameSize);
155
+ engine.setNoiseGateDb(-55.0f);
156
+
157
+ auto result = engine.process(buffer.data(), static_cast<int>(buffer.size()));
158
+
159
+ std::cout
160
+ << "quiet signal hasPitch: " << result.hasPitch
161
+ << " rms: " << result.rmsDb
162
+ << std::endl;
163
+
164
+ assert(!result.hasPitch);
165
+ }
166
+
167
+ // --- M2: Pipeline / DSP hardening tests ---
168
+
169
+ static void testPipelineCleanSine(float inputFrequency, const std::string& expectedNote, int expectedOctave) {
170
+ constexpr float sampleRate = 48000.0f;
171
+ constexpr int frameSize = 4096;
172
+ constexpr int numFrames = 6;
173
+
174
+ // Generate a single continuous sine (no phase discontinuity between frames)
175
+ // so the HPF state transitions correctly across frame boundaries.
176
+ auto continuous = generateSine(inputFrequency, sampleRate, frameSize * numFrames);
177
+
178
+ TunerEngine engine(sampleRate, frameSize);
179
+ PitchResult result;
180
+ for (int f = 0; f < numFrames; ++f) {
181
+ result = engine.process(continuous.data() + f * frameSize, frameSize);
182
+ }
183
+
184
+ std::cout
185
+ << "pipeline sine " << inputFrequency << " Hz"
186
+ << " → " << result.noteName << result.octave
187
+ << " cents=" << result.cents
188
+ << " conf=" << result.confidence << "\n";
189
+
190
+ assert(result.hasPitch);
191
+ assert(result.noteName == expectedNote);
192
+ assert(result.octave == expectedOctave);
193
+ assertNear(result.cents, 0.0f, 5.0f);
194
+ }
195
+
196
+ static void testPipelineNoisySignal() {
197
+ // Pure noise should NOT produce a stable pitch result
198
+ constexpr float sampleRate = 48000.0f;
199
+ constexpr int frameSize = 4096;
200
+
201
+ TunerEngine engine(sampleRate, frameSize);
202
+ std::vector<float> buffer(frameSize);
203
+
204
+ // Pink-ish noise via simple LFSR-style scramble
205
+ uint32_t seed = 0xDEADBEEF;
206
+ for (int i = 0; i < frameSize; ++i) {
207
+ seed ^= seed << 13; seed ^= seed >> 17; seed ^= seed << 5;
208
+ buffer[i] = static_cast<float>(static_cast<int32_t>(seed)) / 2147483648.0f * 0.3f;
209
+ }
210
+
211
+ PitchResult result;
212
+ for (int f = 0; f < 6; ++f) {
213
+ result = engine.process(buffer.data(), static_cast<int>(buffer.size()));
214
+ }
215
+
216
+ std::cout
217
+ << "noisy signal hasPitch=" << result.hasPitch
218
+ << " conf=" << result.confidence << "\n";
219
+
220
+ // Noisy signal should have low confidence / no stable pitch
221
+ assert(!result.hasPitch || result.confidence < 0.50f);
222
+ }
223
+
224
+ static void testPipelineHysteresis() {
225
+ // Play A4 for several frames, then switch to B4 — verify debounce
226
+ constexpr float sampleRate = 48000.0f;
227
+ constexpr int frameSize = 4096;
228
+ constexpr float freqA4 = 440.0f;
229
+ constexpr float freqB4 = 493.88f;
230
+
231
+ TunerEngine engine(sampleRate, frameSize);
232
+ PitchResult result;
233
+
234
+ // Stabilise on A4 — continuous sine
235
+ auto contA4 = generateSine(freqA4, sampleRate, frameSize * 6);
236
+ for (int f = 0; f < 6; ++f) {
237
+ result = engine.process(contA4.data() + f * frameSize, frameSize);
238
+ }
239
+ assert(result.hasPitch);
240
+ assert(result.noteName == "A");
241
+
242
+ // Switch to B4 — continuous sine.
243
+ // Median buffer (5) fills with B4 at frame 3 of the new note.
244
+ // Then hysteresis counter (3) confirms → lock at frame 5 (3 frames where median == B4).
245
+ // Feed 8 frames total to be comfortably past the switch.
246
+ auto contB4 = generateSine(freqB4, sampleRate, frameSize * 8);
247
+
248
+ result = engine.process(contB4.data(), frameSize);
249
+ std::cout
250
+ << "hysteresis after 1 B4 frame: note=" << result.noteName << "\n";
251
+
252
+ for (int f = 1; f < 8; ++f) {
253
+ result = engine.process(contB4.data() + f * frameSize, frameSize);
254
+ }
255
+ std::cout
256
+ << "hysteresis after 8 B4 frames: note=" << result.noteName << "\n";
257
+ assert(result.hasPitch);
258
+ assert(result.noteName == "B");
259
+ }
260
+
261
+ static void testInstrumentPreset() {
262
+ TunerEngine engine(48000.0f, 4096);
263
+ engine.setInstrument("guitar");
264
+
265
+ // E2 (82 Hz) — within guitar range, continuous sine
266
+ PitchResult result;
267
+ auto contE2 = generateSine(82.41f, 48000.0f, 4096 * 6);
268
+ for (int f = 0; f < 6; ++f) {
269
+ result = engine.process(contE2.data() + f * 4096, 4096);
270
+ }
271
+ std::cout << "guitar preset E2: hasPitch=" << result.hasPitch
272
+ << " note=" << result.noteName << result.octave << "\n";
273
+ assert(result.hasPitch);
274
+
275
+ // Switch to ukulele — G4 should work, E2 might not (too low)
276
+ engine.setInstrument("ukulele");
277
+ auto contG4 = generateSine(392.0f, 48000.0f, 4096 * 6);
278
+ for (int f = 0; f < 6; ++f) {
279
+ result = engine.process(contG4.data() + f * 4096, 4096);
280
+ }
281
+ std::cout << "ukulele preset G4: hasPitch=" << result.hasPitch
282
+ << " note=" << result.noteName << result.octave << "\n";
283
+ assert(result.hasPitch);
284
+ assert(result.noteName == "G");
285
+ }
286
+
287
+ // --- M3: per-detector and ensemble tests ---
288
+
289
+ static void testDetectorOnSine(float freq, const std::string& label) {
290
+ constexpr float sr = 48000.0f;
291
+ constexpr int n = 4096;
292
+ auto buf = generateSine(freq, sr, n);
293
+
294
+ BiquadHpf hpf(sr, 70.0f);
295
+ hpf.process(buf.data(), n);
296
+
297
+ YinPitchDetector yin(sr, n);
298
+ PyinPitchDetector pyin(sr, n);
299
+
300
+ auto ry = yin.detect(buf.data(), n, sr);
301
+ auto rpy = pyin.detect(buf.data(), n, sr);
302
+
303
+ std::cout << "detector " << label << " @ " << freq << " Hz:\n"
304
+ << " YIN freq=" << ry.frequency << " conf=" << ry.confidence << "\n"
305
+ << " PYIN freq=" << rpy.frequency << " conf=" << rpy.confidence << "\n";
306
+
307
+ assert(ry.voiced);
308
+ assertNear(ry.frequency, freq, freq * 0.01f); // within 1%
309
+
310
+ assert(rpy.voiced);
311
+ assertNear(rpy.frequency, freq, freq * 0.01f); // PYIN must agree with YIN
312
+ }
313
+
314
+ static void testEnsembleAgreementBonus() {
315
+ // When YIN and PYIN agree, the ensemble should produce confidence > either detector alone.
316
+ constexpr float sr = 48000.0f;
317
+ constexpr int n = 4096;
318
+ constexpr float f0 = 196.0f; // G3
319
+
320
+ auto buf = generateSine(f0, sr, n * 6); // 6 continuous frames for HPF warmup
321
+
322
+ std::vector<std::unique_ptr<IPitchDetector>> detectors;
323
+ detectors.push_back(std::make_unique<YinPitchDetector>(sr, n));
324
+ detectors.push_back(std::make_unique<PyinPitchDetector>(sr, n));
325
+ detectors.push_back(std::make_unique<CepstrumPitchDetector>(sr, n));
326
+ EnsembleSelector ensemble(std::move(detectors));
327
+
328
+ BiquadHpf hpf(sr, 70.0f);
329
+
330
+ DetectorResult result;
331
+ for (int f = 0; f < 6; ++f) {
332
+ std::vector<float> frame(buf.begin() + f * n, buf.begin() + (f + 1) * n);
333
+ hpf.process(frame.data(), n);
334
+ result = ensemble.detect(frame.data(), n, sr);
335
+ }
336
+
337
+ std::cout << "ensemble G3: voiced=" << result.voiced
338
+ << " freq=" << result.frequency
339
+ << " conf=" << result.confidence << "\n";
340
+
341
+ assert(result.voiced);
342
+ assertNear(result.frequency, f0, f0 * 0.01f);
343
+ assert(result.confidence > 0.85f); // agreement bonus applied
344
+ }
345
+
346
+ static void testEnsembleOctaveSafety() {
347
+ // D3 (146.83 Hz): tau0 and 2*tau0 both fit in the search range.
348
+ // The ensemble must return the fundamental, not the sub-octave.
349
+ constexpr float sr = 48000.0f;
350
+ constexpr int n = 4096;
351
+
352
+ TunerEngine engine(sr, n);
353
+ auto buf = generateSine(146.83f, sr, n * 6);
354
+
355
+ PitchResult result;
356
+ for (int f = 0; f < 6; ++f) {
357
+ result = engine.process(buf.data() + f * n, n);
358
+ }
359
+
360
+ std::cout << "ensemble octave safety D3: "
361
+ << result.noteName << result.octave
362
+ << " freq=" << result.frequency << "\n";
363
+
364
+ assert(result.hasPitch);
365
+ assert(result.noteName == "D");
366
+ assert(result.octave == 3);
367
+ }
368
+
369
+ static void testOnsetDetector() {
370
+ OnsetDetector::Config cfg;
371
+ cfg.thresholdDb = 6.0f;
372
+ cfg.envelopeAlpha = 0.15f;
373
+ cfg.cooldownFrames = 4;
374
+
375
+ OnsetDetector det(cfg);
376
+
377
+ // When disabled, never fires
378
+ assert(!det.detect(-30.0f));
379
+ assert(!det.detect(0.0f)); // 30dB jump — still no onset because disabled
380
+
381
+ // Enable
382
+ det.setEnabled(true);
383
+ det.reset();
384
+
385
+ // Feed quiet frames to establish envelope
386
+ for (int i = 0; i < 10; ++i) {
387
+ det.detect(-40.0f);
388
+ }
389
+
390
+ // Sudden jump — should trigger onset
391
+ bool onset = det.detect(-20.0f); // +20dB rise >> 6dB threshold
392
+ std::cout << "onset on energy spike: " << onset << "\n";
393
+ assert(onset);
394
+
395
+ // Cooldown — should NOT fire even with another jump
396
+ bool duringCooldown = det.detect(-10.0f);
397
+ std::cout << "onset during cooldown: " << duringCooldown << "\n";
398
+ assert(!duringCooldown);
399
+
400
+ // After cooldown expires, can fire again
401
+ for (int i = 0; i < 4; ++i) {
402
+ det.detect(-40.0f);
403
+ }
404
+ bool afterCooldown = det.detect(-20.0f);
405
+ std::cout << "onset after cooldown: " << afterCooldown << "\n";
406
+ assert(afterCooldown);
407
+ }
408
+
409
+ // --- M8: Adaptive Frame Size & Overlap tests ---
410
+
411
+ static void testSlidingWindowOverlap() {
412
+ // With 75% overlap on a 2048 frame, hopSize should be 512.
413
+ // Feed a continuous 440 Hz sine and verify we get results at hop rate.
414
+ constexpr float sr = 48000.0f;
415
+ constexpr int frameSize = 2048;
416
+ constexpr float overlap = 0.75f;
417
+
418
+ int callbackCount = 0;
419
+ PitchResult lastResult;
420
+
421
+ AudioFrameDispatcher dispatcher(frameSize, sr,
422
+ [&](const PitchResult& r) {
423
+ callbackCount++;
424
+ lastResult = r;
425
+ },
426
+ overlap
427
+ );
428
+
429
+ assert(dispatcher.hopSize() == 512); // 2048 * (1 - 0.75) = 512
430
+ assert(dispatcher.frameSize() == 2048);
431
+
432
+ // Generate enough audio to fill multiple hops
433
+ // First frame needs 2048, then each subsequent needs 512
434
+ constexpr int totalSamples = 2048 + 512 * 7; // 1 first + 7 hops = 8 callbacks
435
+ auto signal = generateSine(440.0f, sr, totalSamples);
436
+
437
+ dispatcher.start();
438
+ dispatcher.push(signal.data(), totalSamples);
439
+
440
+ // Wait for processing
441
+ std::this_thread::sleep_for(std::chrono::milliseconds(50));
442
+ dispatcher.stop();
443
+
444
+ std::cout << "sliding window overlap 75%: callbacks=" << callbackCount
445
+ << " lastFreq=" << lastResult.frequency << "\n";
446
+
447
+ // Should have received multiple callbacks (first frame + hops)
448
+ assert(callbackCount >= 4); // at least some overlap callbacks fired
449
+ }
450
+
451
+ static void testSlidingWindowNoOverlap() {
452
+ // With 0% overlap, behavior should be identical to the old implementation
453
+ constexpr float sr = 48000.0f;
454
+ constexpr int frameSize = 2048;
455
+
456
+ int callbackCount = 0;
457
+
458
+ AudioFrameDispatcher dispatcher(frameSize, sr,
459
+ [&](const PitchResult& r) {
460
+ callbackCount++;
461
+ },
462
+ 0.0f
463
+ );
464
+
465
+ assert(dispatcher.hopSize() == 2048); // no overlap → hop = frame
466
+
467
+ auto signal = generateSine(440.0f, sr, 2048 * 4);
468
+
469
+ dispatcher.start();
470
+ dispatcher.push(signal.data(), 2048 * 4);
471
+
472
+ std::this_thread::sleep_for(std::chrono::milliseconds(50));
473
+ dispatcher.stop();
474
+
475
+ std::cout << "sliding window no overlap: callbacks=" << callbackCount << "\n";
476
+ assert(callbackCount == 4); // exactly 4 full frames
477
+ }
478
+
479
+ static void testAdaptiveFrameSizeBass() {
480
+ // Bass instrument should use 4096 frame for E1 (41 Hz) detection
481
+ constexpr float sr = 48000.0f;
482
+ constexpr int frameSize = 4096;
483
+ constexpr float e1Freq = 41.2f;
484
+
485
+ auto signal = generateSine(e1Freq, sr, frameSize * 8);
486
+
487
+ TunerEngine engine(sr, frameSize);
488
+ engine.setInstrument("bass");
489
+
490
+ PitchResult result;
491
+ for (int f = 0; f < 8; ++f) {
492
+ result = engine.process(signal.data() + f * frameSize, frameSize);
493
+ }
494
+
495
+ std::cout << "adaptive bass E1 (41.2 Hz): hasPitch=" << result.hasPitch
496
+ << " freq=" << result.frequency
497
+ << " note=" << result.noteName << result.octave
498
+ << " conf=" << result.confidence << "\n";
499
+
500
+ assert(result.hasPitch);
501
+ assertNear(result.frequency, e1Freq, e1Freq * 0.02f); // within 2%
502
+ }
503
+
504
+ static void testInstrumentRecommendedFrameSize() {
505
+ assert(instrumentRecommendedFrameSize("bass") == 4096);
506
+ assert(instrumentRecommendedFrameSize("cello") == 4096);
507
+ assert(instrumentRecommendedFrameSize("guitar") == 2048);
508
+ assert(instrumentRecommendedFrameSize("ukulele") == 2048);
509
+ assert(instrumentRecommendedFrameSize("violin") == 2048);
510
+ assert(instrumentRecommendedFrameSize("chromatic") == 2048);
511
+ assert(instrumentRecommendedFrameSize("unknown") == 2048);
512
+ std::cout << "instrumentRecommendedFrameSize: all correct\n";
513
+ }
514
+
515
+ static void testDispatcherReconfigure() {
516
+ constexpr float sr = 48000.0f;
517
+ int callbackCount = 0;
518
+
519
+ AudioFrameDispatcher dispatcher(2048, sr,
520
+ [&](const PitchResult& r) {
521
+ callbackCount++;
522
+ },
523
+ 0.0f
524
+ );
525
+
526
+ assert(dispatcher.frameSize() == 2048);
527
+
528
+ // Reconfigure to 4096
529
+ dispatcher.reconfigure(4096, sr);
530
+ assert(dispatcher.frameSize() == 4096);
531
+ assert(dispatcher.hopSize() == 4096); // overlap still 0
532
+
533
+ // Push audio and verify it processes with new frame size
534
+ auto signal = generateSine(82.41f, sr, 4096 * 2);
535
+ dispatcher.start();
536
+ dispatcher.push(signal.data(), 4096 * 2);
537
+
538
+ std::this_thread::sleep_for(std::chrono::milliseconds(50));
539
+ dispatcher.stop();
540
+
541
+ std::cout << "dispatcher reconfigure 2048→4096: callbacks=" << callbackCount << "\n";
542
+ assert(callbackCount == 2);
543
+ }
544
+
545
+ static void testSetOverlapRatio() {
546
+ constexpr float sr = 48000.0f;
547
+
548
+ AudioFrameDispatcher dispatcher(2048, sr,
549
+ [](const PitchResult&) {},
550
+ 0.0f
551
+ );
552
+
553
+ assert(dispatcher.hopSize() == 2048);
554
+
555
+ dispatcher.setOverlapRatio(0.5f);
556
+ assert(dispatcher.hopSize() == 1024);
557
+
558
+ dispatcher.setOverlapRatio(0.75f);
559
+ assert(dispatcher.hopSize() == 512);
560
+
561
+ // Clamp to max 0.75
562
+ dispatcher.setOverlapRatio(0.9f);
563
+ assert(dispatcher.hopSize() == 512); // clamped to 0.75
564
+
565
+ // Clamp to min 0.0
566
+ dispatcher.setOverlapRatio(-0.5f);
567
+ assert(dispatcher.hopSize() == 2048); // clamped to 0.0
568
+
569
+ std::cout << "setOverlapRatio: all correct\n";
570
+ }
571
+
572
+ int main() {
573
+ testNoteMapper();
574
+
575
+ testYin(82.41f);
576
+ testYin(110.0f);
577
+ testYin(146.83f);
578
+ testYin(196.0f);
579
+ testYin(246.94f);
580
+ testYin(329.63f);
581
+
582
+ testTunerEngine(82.41f, "E", 2);
583
+ testTunerEngine(110.0f, "A", 2);
584
+ testTunerEngine(146.83f, "D", 3);
585
+ testTunerEngine(196.0f, "G", 3);
586
+ testTunerEngine(246.94f, "B", 3);
587
+ testTunerEngine(329.63f, "E", 4);
588
+
589
+ testTunerEngineSilence();
590
+ testTunerEngineQuietSignal();
591
+
592
+ // M3 per-detector and ensemble tests
593
+ testDetectorOnSine(82.41f, "E2");
594
+ testDetectorOnSine(110.0f, "A2");
595
+ testDetectorOnSine(146.83f, "D3");
596
+ testDetectorOnSine(196.0f, "G3");
597
+ testDetectorOnSine(329.63f, "E4");
598
+ testDetectorOnSine(440.0f, "A4");
599
+ testEnsembleAgreementBonus();
600
+ testEnsembleOctaveSafety();
601
+
602
+ // M2 DSP hardening tests
603
+ testPipelineCleanSine(82.41f, "E", 2);
604
+ testPipelineCleanSine(110.0f, "A", 2);
605
+ testPipelineCleanSine(196.0f, "G", 3);
606
+ testPipelineCleanSine(329.63f, "E", 4);
607
+ testPipelineCleanSine(440.0f, "A", 4);
608
+ testPipelineNoisySignal();
609
+ testPipelineHysteresis();
610
+ testInstrumentPreset();
611
+ testOnsetDetector();
612
+
613
+ // M8 Adaptive Frame Size & Overlap tests
614
+ testInstrumentRecommendedFrameSize();
615
+ testSetOverlapRatio();
616
+ testSlidingWindowNoOverlap();
617
+ testSlidingWindowOverlap();
618
+ testDispatcherReconfigure();
619
+ testAdaptiveFrameSizeBass();
620
+
621
+ std::cout << "all tuner engine tests passed." << std::endl;
622
+
623
+ return 0;
624
+ }
@@ -0,0 +1,23 @@
1
+ #pragma once
2
+
3
+ #import <AVFoundation/AVFoundation.h>
4
+ #import <Foundation/Foundation.h>
5
+
6
+ NS_ASSUME_NONNULL_BEGIN
7
+
8
+ typedef void (^AudioSamplesCallback)(const float* samples, int count, float sampleRate);
9
+
10
+ // Manages AVAudioSession + AVAudioEngine mic capture.
11
+ // Delivers interleaved float32 mono samples to the callback on AVAudioEngine's real-time thread.
12
+ // The callback must not block or allocate.
13
+ @interface IosAudioSource : NSObject
14
+
15
+ @property(nonatomic, copy, nullable) AudioSamplesCallback onSamples;
16
+
17
+ - (BOOL)startWithError:(NSError **)error;
18
+ - (void)stop;
19
+ - (float)sampleRate;
20
+
21
+ @end
22
+
23
+ NS_ASSUME_NONNULL_END