From e8e98cb7e82223fcdf321fb5a8cc0ff45e225830 Mon Sep 17 00:00:00 2001 From: denizyesilirmak Date: Sun, 24 May 2026 21:55:51 +0300 Subject: [PATCH] Add temperament and adaptive frame size support to TunerEngine - Implement native methods for setting temperament and adaptive frame size in TunerEngine. - Update Kotlin module to handle new options for adaptive frame size and temperament. - Enhance AudioFrameDispatcher to manage adaptive frame size and apply stored settings. - Modify NoteMapper to support 'just' temperament and adjust pitch mapping accordingly. - Update API documentation to reflect new temperament options and their usage. --- android/src/main/cpp/TunerEngineJni.cpp | 16 +++++++ .../java/com/tunerengine/TunerEngineModule.kt | 8 +++- cpp/include/AudioFrameDispatcher.hpp | 17 +++++++ cpp/include/NoteMapper.hpp | 21 +++++++++ cpp/include/Pipeline.hpp | 1 + cpp/include/TunerEngine.hpp | 1 + cpp/src/AudioFrameDispatcher.cpp | 47 +++++++++++++++++-- cpp/src/NoteMapper.cpp | 16 ++++++- cpp/src/Pipeline.cpp | 4 ++ cpp/src/TunerEngine.cpp | 4 ++ documents/API.md | 7 +-- ios/TunerBridge.mm | 7 ++- src/TunerEngine.ts | 27 ++++++++++- 13 files changed, 164 insertions(+), 12 deletions(-) diff --git a/android/src/main/cpp/TunerEngineJni.cpp b/android/src/main/cpp/TunerEngineJni.cpp index 0d4af23..3c2dfb3 100644 --- a/android/src/main/cpp/TunerEngineJni.cpp +++ b/android/src/main/cpp/TunerEngineJni.cpp @@ -217,4 +217,20 @@ Java_com_tunerengine_TunerEngineModule_nativeSetOnsetDetection( if (gDispatcher) gDispatcher->setOnsetDetectionEnabled(static_cast(enabled)); } +JNIEXPORT void JNICALL +Java_com_tunerengine_TunerEngineModule_nativeSetTemperament( + JNIEnv* env, jobject /*thiz*/, jstring name +) { + const char* nameChars = env->GetStringUTFChars(name, nullptr); + if (gDispatcher && nameChars) gDispatcher->setTemperament(std::string(nameChars)); + env->ReleaseStringUTFChars(name, nameChars); +} + +JNIEXPORT void JNICALL +Java_com_tunerengine_TunerEngineModule_nativeSetAdaptiveFrameSize( + JNIEnv* /*env*/, jobject /*thiz*/, jboolean enabled +) { + if (gDispatcher) gDispatcher->setAdaptiveFrameSize(static_cast(enabled)); +} + } // extern "C" diff --git a/android/src/main/java/com/tunerengine/TunerEngineModule.kt b/android/src/main/java/com/tunerengine/TunerEngineModule.kt index 6899e52..176d2a8 100644 --- a/android/src/main/java/com/tunerengine/TunerEngineModule.kt +++ b/android/src/main/java/com/tunerengine/TunerEngineModule.kt @@ -57,6 +57,10 @@ class TunerEngineModule(reactContext: ReactApplicationContext) : nativeSetOnsetDetection(opts.getBoolean("onsetDetection")) } + if (opts?.hasKey("adaptiveFrameSize") == true) { + nativeSetAdaptiveFrameSize(opts.getBoolean("adaptiveFrameSize")) + } + promise.resolve(null) } catch (e: Exception) { promise.reject("CONFIGURE_ERROR", "configure failed: ${e.message}", e) @@ -96,7 +100,7 @@ class TunerEngineModule(reactContext: ReactApplicationContext) : } override fun setTemperament(name: String) { - // Temperament support added in M2 + nativeSetTemperament(name) } override fun requestPermission(promise: Promise) { @@ -196,6 +200,8 @@ class TunerEngineModule(reactContext: ReactApplicationContext) : private external fun nativeSetHpfCutoff(hz: Float) private external fun nativeSetPostProcessorConfig(emaAlpha: Float, hysteresisFrames: Int) private external fun nativeSetOnsetDetection(enabled: Boolean) + private external fun nativeSetTemperament(name: String) + private external fun nativeSetAdaptiveFrameSize(enabled: Boolean) private external fun nativeIsRunning(): Boolean companion object { diff --git a/cpp/include/AudioFrameDispatcher.hpp b/cpp/include/AudioFrameDispatcher.hpp index bda989a..213c924 100644 --- a/cpp/include/AudioFrameDispatcher.hpp +++ b/cpp/include/AudioFrameDispatcher.hpp @@ -48,6 +48,8 @@ class AudioFrameDispatcher { void setFrequencyRange(float minHz, float maxHz); void setInstrument(const std::string& name); void setTuning(const std::string& name); + void setTemperament(const std::string& name); + void setAdaptiveFrameSize(bool enabled); void setPostProcessorConfig(PostProcessor::Config cfg); void setHpfCutoff(float hz); void setOnsetDetectionEnabled(bool enabled); @@ -66,6 +68,7 @@ class AudioFrameDispatcher { private: void workerLoop(); void recomputeHopSize(); + void applyStoredSettings(); // re-apply cached settings after engine recreation static constexpr unsigned kRingCapacity = 32768u; // ~680ms at 48kHz — plenty of headroom @@ -73,6 +76,7 @@ class AudioFrameDispatcher { int hopSize_; float overlapRatio_; float sampleRate_; + bool adaptiveFrameSize_{true}; // auto-resize frame on setInstrument PitchCallback callback_; FloatRingBuffer ring_; @@ -82,6 +86,19 @@ class AudioFrameDispatcher { mutable std::mutex engineMutex_; // protects engine_ access across threads std::unique_ptr engine_; + // Cached settings — re-applied after engine recreation (setSampleRate / reconfigure) + std::string currentInstrument_; + std::string currentTuning_; + std::string currentTemperament_; + float currentA4_{440.0f}; + float currentNoiseGateDb_{-55.0f}; + float currentConfidenceThreshold_{0.75f}; + float currentMinHz_{60.0f}; + float currentMaxHz_{1200.0f}; + float currentHpfCutoff_{70.0f}; + bool currentOnsetEnabled_{false}; + PostProcessor::Config currentPostCfg_{}; + std::thread workerThread_; std::atomic running_{false}; }; diff --git a/cpp/include/NoteMapper.hpp b/cpp/include/NoteMapper.hpp index 869a6ba..7c47272 100644 --- a/cpp/include/NoteMapper.hpp +++ b/cpp/include/NoteMapper.hpp @@ -2,6 +2,8 @@ #include "PitchResult.hpp" +#include + class NoteMapper { public: explicit NoteMapper(float a4 = 440.0f); @@ -9,7 +11,26 @@ class NoteMapper { PitchResult map(float frequency, float confidence, float rmsDb) const; void setA4(float value); + void setTemperament(const std::string& name); // "equal" or "just" private: float a4_; + bool useJust_ = false; + + // Cents offset from equal temperament for each pitch class (C=0..B=11) + // using 5-limit just intonation with C as root. + static constexpr float kJustCentsOffset[12] = { + 0.0f, // C 1/1 + 11.73f, // C# 16/15 + 3.91f, // D 9/8 + 15.64f, // D# 6/5 + -13.69f, // E 5/4 + -1.96f, // F 4/3 + -9.78f, // F# 45/32 + 1.96f, // G 3/2 + 13.69f, // G# 8/5 + -15.64f, // A 5/3 + 17.60f, // A# 9/5 + -11.73f, // B 15/8 + }; }; \ No newline at end of file diff --git a/cpp/include/Pipeline.hpp b/cpp/include/Pipeline.hpp index 47ef86d..74e5468 100644 --- a/cpp/include/Pipeline.hpp +++ b/cpp/include/Pipeline.hpp @@ -27,6 +27,7 @@ class Pipeline { void setFrequencyRange(float minHz, float maxHz); void setInstrument(const std::string& name); void setTuning(const std::string& name); // e.g. "guitar_standard", "" to disable + void setTemperament(const std::string& name); // "equal" or "just" void setPostProcessorConfig(PostProcessor::Config cfg); void setHpfCutoff(float hz); void setOnsetDetectionEnabled(bool enabled); diff --git a/cpp/include/TunerEngine.hpp b/cpp/include/TunerEngine.hpp index 1bb30c2..f852fc9 100644 --- a/cpp/include/TunerEngine.hpp +++ b/cpp/include/TunerEngine.hpp @@ -22,6 +22,7 @@ class TunerEngine { void setFrequencyRange(float minFrequency, float maxFrequency); void setInstrument(const std::string& name); void setTuning(const std::string& name); + void setTemperament(const std::string& name); void setPostProcessorConfig(PostProcessor::Config cfg); void setHpfCutoff(float hz); void setOnsetDetectionEnabled(bool enabled); diff --git a/cpp/src/AudioFrameDispatcher.cpp b/cpp/src/AudioFrameDispatcher.cpp index 25324b8..73e90c7 100644 --- a/cpp/src/AudioFrameDispatcher.cpp +++ b/cpp/src/AudioFrameDispatcher.cpp @@ -51,56 +51,79 @@ void AudioFrameDispatcher::setSampleRate(float sampleRate) { std::lock_guard lock(engineMutex_); sampleRate_ = sampleRate; engine_ = std::make_unique(sampleRate, frameSize_); + applyStoredSettings(); } void AudioFrameDispatcher::setA4(float hz) { std::lock_guard lock(engineMutex_); + currentA4_ = hz; if (engine_) engine_->setA4(hz); } void AudioFrameDispatcher::setNoiseGateDb(float db) { std::lock_guard lock(engineMutex_); + currentNoiseGateDb_ = db; if (engine_) engine_->setNoiseGateDb(db); } void AudioFrameDispatcher::setConfidenceThreshold(float value) { std::lock_guard lock(engineMutex_); + currentConfidenceThreshold_ = value; if (engine_) engine_->setConfidenceThreshold(value); } void AudioFrameDispatcher::setFrequencyRange(float minHz, float maxHz) { std::lock_guard lock(engineMutex_); + currentMinHz_ = minHz; + currentMaxHz_ = maxHz; if (engine_) engine_->setFrequencyRange(minHz, maxHz); } void AudioFrameDispatcher::setInstrument(const std::string& name) { - // Check if the instrument's recommended frame size differs - const int recommended = instrumentRecommendedFrameSize(name); - if (recommended != frameSize_) { - reconfigure(recommended, sampleRate_); + // Auto-resize frame only if adaptive frame sizing is enabled + if (adaptiveFrameSize_) { + const int recommended = instrumentRecommendedFrameSize(name); + if (recommended != frameSize_) { + reconfigure(recommended, sampleRate_); + } } std::lock_guard lock(engineMutex_); + currentInstrument_ = name; if (engine_) engine_->setInstrument(name); } void AudioFrameDispatcher::setTuning(const std::string& name) { std::lock_guard lock(engineMutex_); + currentTuning_ = name; if (engine_) engine_->setTuning(name); } +void AudioFrameDispatcher::setTemperament(const std::string& name) { + std::lock_guard lock(engineMutex_); + currentTemperament_ = name; + if (engine_) engine_->setTemperament(name); +} + +void AudioFrameDispatcher::setAdaptiveFrameSize(bool enabled) { + adaptiveFrameSize_ = enabled; +} + void AudioFrameDispatcher::setPostProcessorConfig(PostProcessor::Config cfg) { std::lock_guard lock(engineMutex_); + currentPostCfg_ = cfg; if (engine_) engine_->setPostProcessorConfig(cfg); } void AudioFrameDispatcher::setHpfCutoff(float hz) { std::lock_guard lock(engineMutex_); + currentHpfCutoff_ = hz; if (engine_) engine_->setHpfCutoff(hz); } void AudioFrameDispatcher::setOnsetDetectionEnabled(bool enabled) { std::lock_guard lock(engineMutex_); + currentOnsetEnabled_ = enabled; if (engine_) engine_->setOnsetDetectionEnabled(enabled); } @@ -116,6 +139,21 @@ void AudioFrameDispatcher::setOverlapRatio(float ratio) { firstFrame_ = true; // reset sliding window state } +void AudioFrameDispatcher::applyStoredSettings() { + // Called with engineMutex_ already held after engine recreation + if (!engine_) return; + engine_->setA4(currentA4_); + engine_->setNoiseGateDb(currentNoiseGateDb_); + engine_->setConfidenceThreshold(currentConfidenceThreshold_); + engine_->setFrequencyRange(currentMinHz_, currentMaxHz_); + engine_->setHpfCutoff(currentHpfCutoff_); + engine_->setPostProcessorConfig(currentPostCfg_); + engine_->setOnsetDetectionEnabled(currentOnsetEnabled_); + if (!currentInstrument_.empty()) engine_->setInstrument(currentInstrument_); + if (!currentTuning_.empty()) engine_->setTuning(currentTuning_); + if (!currentTemperament_.empty()) engine_->setTemperament(currentTemperament_); +} + void AudioFrameDispatcher::reconfigure(int newFrameSize, float sampleRate) { const bool wasRunning = running_.load(); if (wasRunning) stop(); @@ -128,6 +166,7 @@ void AudioFrameDispatcher::reconfigure(int newFrameSize, float sampleRate) { frameBuffer_.assign(static_cast(frameSize_), 0.0f); firstFrame_ = true; engine_ = std::make_unique(sampleRate_, frameSize_); + applyStoredSettings(); } if (wasRunning) start(); diff --git a/cpp/src/NoteMapper.cpp b/cpp/src/NoteMapper.cpp index ef98336..02872b7 100644 --- a/cpp/src/NoteMapper.cpp +++ b/cpp/src/NoteMapper.cpp @@ -14,6 +14,10 @@ void NoteMapper::setA4(float value) { a4_ = value; } +void NoteMapper::setTemperament(const std::string& name) { + useJust_ = (name == "just"); +} + PitchResult NoteMapper::map(float frequency, float confidence, float rmsDb) const { PitchResult result; @@ -30,7 +34,15 @@ PitchResult NoteMapper::map(float frequency, float confidence, float rmsDb) cons std::round(69.0f + 12.0f * std::log2(frequency / a4_)) ); - const float target = a4_ * std::pow(2.0f, (midi - 69) / 12.0f); + // Equal-temperament target + float target = a4_ * std::pow(2.0f, (midi - 69) / 12.0f); + + // Apply just-intonation offset if enabled + if (useJust_) { + const int pitchClass = ((midi % 12) + 12) % 12; // 0=C .. 11=B + target *= std::pow(2.0f, kJustCentsOffset[pitchClass] / 1200.0f); + } + const float cents = 1200.0f * std::log2(frequency / target); result.hasPitch = true; @@ -38,7 +50,7 @@ PitchResult NoteMapper::map(float frequency, float confidence, float rmsDb) cons result.confidence = confidence; result.rmsDb = rmsDb; result.midiNote = midi; - result.noteName = names[midi % 12]; + result.noteName = names[((midi % 12) + 12) % 12]; result.octave = midi / 12 - 1; result.targetFrequency = target; result.cents = cents; diff --git a/cpp/src/Pipeline.cpp b/cpp/src/Pipeline.cpp index 06bd112..40e1ecc 100644 --- a/cpp/src/Pipeline.cpp +++ b/cpp/src/Pipeline.cpp @@ -103,6 +103,10 @@ void Pipeline::setTuning(const std::string& name) { stringMatcher_.setTuning(name.empty() ? nullptr : tuningPreset(name)); } +void Pipeline::setTemperament(const std::string& name) { + noteMapper_.setTemperament(name); +} + void Pipeline::setPostProcessorConfig(PostProcessor::Config cfg) { postProcessor_.setConfig(cfg); } diff --git a/cpp/src/TunerEngine.cpp b/cpp/src/TunerEngine.cpp index d30705b..71fb5cc 100644 --- a/cpp/src/TunerEngine.cpp +++ b/cpp/src/TunerEngine.cpp @@ -50,6 +50,10 @@ void TunerEngine::setTuning(const std::string& name) { pipeline_->setTuning(name); } +void TunerEngine::setTemperament(const std::string& name) { + pipeline_->setTemperament(name); +} + void TunerEngine::setPostProcessorConfig(PostProcessor::Config cfg) { pipeline_->setPostProcessorConfig(cfg); } diff --git a/documents/API.md b/documents/API.md index 78999f1..fe06df0 100644 --- a/documents/API.md +++ b/documents/API.md @@ -195,10 +195,10 @@ TunerEngine.setTuning('guitar_drop_d'); #### `setTemperament(name: Temperament): void` -Sets the temperament system (currently `'equal'` only; `'just'` planned). +Sets the temperament system. `'equal'` uses standard 12-TET; `'just'` uses 5-limit just intonation ratios (relative to C). ```typescript -TunerEngine.setTemperament('equal'); +TunerEngine.setTemperament('just'); ``` --- @@ -337,7 +337,8 @@ type TuningPreset = type Temperament = 'equal' | 'just'; ``` -Currently only `'equal'` is implemented. `'just'` intonation is planned for a future release. +- `'equal'` — Standard 12-tone equal temperament (default). +- `'just'` — 5-limit just intonation. Target frequencies are shifted by the pure-ratio cent offsets relative to C, so the tuner shows deviation from just intervals rather than equal-tempered ones. --- diff --git a/ios/TunerBridge.mm b/ios/TunerBridge.mm index 39e65a7..ab0d35d 100644 --- a/ios/TunerBridge.mm +++ b/ios/TunerBridge.mm @@ -66,6 +66,9 @@ - (void)buildDispatcherWithSampleRate:(float)sr frameSize:(int)fs overlapRatio:( if (opts[@"onsetDetection"]) { _dispatcher->setOnsetDetectionEnabled([opts[@"onsetDetection"] boolValue]); } + if (opts[@"adaptiveFrameSize"] != nil) { + _dispatcher->setAdaptiveFrameSize([opts[@"adaptiveFrameSize"] boolValue]); + } } - (void)startWithCompletion:(void(^)(NSError* _Nullable error))completion { @@ -124,7 +127,9 @@ - (void)setTuning:(NSString *)name { if (_dispatcher) _dispatcher->setTuning(std::string([name UTF8String])); } -- (void)setTemperament:(NSString *)name {} +- (void)setTemperament:(NSString *)name { + if (_dispatcher) _dispatcher->setTemperament(std::string([name UTF8String])); +} - (NSDictionary *)getStatus { return @{ diff --git a/src/TunerEngine.ts b/src/TunerEngine.ts index e81abb3..1018abe 100644 --- a/src/TunerEngine.ts +++ b/src/TunerEngine.ts @@ -4,6 +4,7 @@ import type { EngineStatus, Instrument, PitchEvent, + QualityPreset, Temperament, TunerConfig, TuningPreset, @@ -12,9 +13,33 @@ import type { type PitchCallback = (event: PitchEvent) => void; type Unsubscribe = () => void; +const QUALITY_PRESETS: Record = { + 'low-latency': { frameSize: 1024, overlapRatio: 0 }, + 'balanced': { frameSize: 2048, overlapRatio: 0.5 }, + 'high-accuracy': { frameSize: 4096, overlapRatio: 0.75 }, +}; + class TunerEngine { configure(opts: TunerConfig): Promise { - return NativeTunerEngine.configure(opts); + const { quality, adaptiveFrameSize, ...rest } = opts; + const resolved = { ...rest }; + + // quality preset overrides frameSize and overlapRatio + if (quality && QUALITY_PRESETS[quality]) { + const preset = QUALITY_PRESETS[quality]; + resolved.frameSize = preset.frameSize; + resolved.overlapRatio = preset.overlapRatio; + } + + // If user explicitly provided frameSize or quality, disable adaptive frame sizing + // so setInstrument won't override the chosen frame size. + if (resolved.frameSize !== undefined || quality !== undefined) { + (resolved as any).adaptiveFrameSize = false; + } else { + (resolved as any).adaptiveFrameSize = adaptiveFrameSize !== false; + } + + return NativeTunerEngine.configure(resolved); } start(): Promise {