Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions android/src/main/cpp/TunerEngineJni.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,20 @@ Java_com_tunerengine_TunerEngineModule_nativeSetOnsetDetection(
if (gDispatcher) gDispatcher->setOnsetDetectionEnabled(static_cast<bool>(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<bool>(enabled));
}

} // extern "C"
8 changes: 7 additions & 1 deletion android/src/main/java/com/tunerengine/TunerEngineModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down
17 changes: 17 additions & 0 deletions cpp/include/AudioFrameDispatcher.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -66,13 +68,15 @@ 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

int frameSize_;
int hopSize_;
float overlapRatio_;
float sampleRate_;
bool adaptiveFrameSize_{true}; // auto-resize frame on setInstrument
PitchCallback callback_;

FloatRingBuffer<kRingCapacity> ring_;
Expand All @@ -82,6 +86,19 @@ class AudioFrameDispatcher {
mutable std::mutex engineMutex_; // protects engine_ access across threads
std::unique_ptr<TunerEngine> 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<bool> running_{false};
};
21 changes: 21 additions & 0 deletions cpp/include/NoteMapper.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,35 @@

#include "PitchResult.hpp"

#include <string>

class NoteMapper {
public:
explicit NoteMapper(float a4 = 440.0f);

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
};
};
1 change: 1 addition & 0 deletions cpp/include/Pipeline.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions cpp/include/TunerEngine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
47 changes: 43 additions & 4 deletions cpp/src/AudioFrameDispatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,56 +51,79 @@ void AudioFrameDispatcher::setSampleRate(float sampleRate) {
std::lock_guard<std::mutex> lock(engineMutex_);
sampleRate_ = sampleRate;
engine_ = std::make_unique<TunerEngine>(sampleRate, frameSize_);
applyStoredSettings();
}

void AudioFrameDispatcher::setA4(float hz) {
std::lock_guard<std::mutex> lock(engineMutex_);
currentA4_ = hz;
if (engine_) engine_->setA4(hz);
}

void AudioFrameDispatcher::setNoiseGateDb(float db) {
std::lock_guard<std::mutex> lock(engineMutex_);
currentNoiseGateDb_ = db;
if (engine_) engine_->setNoiseGateDb(db);
}

void AudioFrameDispatcher::setConfidenceThreshold(float value) {
std::lock_guard<std::mutex> lock(engineMutex_);
currentConfidenceThreshold_ = value;
if (engine_) engine_->setConfidenceThreshold(value);
}

void AudioFrameDispatcher::setFrequencyRange(float minHz, float maxHz) {
std::lock_guard<std::mutex> 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<std::mutex> lock(engineMutex_);
currentInstrument_ = name;
if (engine_) engine_->setInstrument(name);
}

void AudioFrameDispatcher::setTuning(const std::string& name) {
std::lock_guard<std::mutex> lock(engineMutex_);
currentTuning_ = name;
if (engine_) engine_->setTuning(name);
}

void AudioFrameDispatcher::setTemperament(const std::string& name) {
std::lock_guard<std::mutex> 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<std::mutex> lock(engineMutex_);
currentPostCfg_ = cfg;
if (engine_) engine_->setPostProcessorConfig(cfg);
}

void AudioFrameDispatcher::setHpfCutoff(float hz) {
std::lock_guard<std::mutex> lock(engineMutex_);
currentHpfCutoff_ = hz;
if (engine_) engine_->setHpfCutoff(hz);
}

void AudioFrameDispatcher::setOnsetDetectionEnabled(bool enabled) {
std::lock_guard<std::mutex> lock(engineMutex_);
currentOnsetEnabled_ = enabled;
if (engine_) engine_->setOnsetDetectionEnabled(enabled);
}

Expand All @@ -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();
Expand All @@ -128,6 +166,7 @@ void AudioFrameDispatcher::reconfigure(int newFrameSize, float sampleRate) {
frameBuffer_.assign(static_cast<size_t>(frameSize_), 0.0f);
firstFrame_ = true;
engine_ = std::make_unique<TunerEngine>(sampleRate_, frameSize_);
applyStoredSettings();
}

if (wasRunning) start();
Expand Down
16 changes: 14 additions & 2 deletions cpp/src/NoteMapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -30,15 +34,23 @@ 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;
result.frequency = frequency;
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;
Expand Down
4 changes: 4 additions & 0 deletions cpp/src/Pipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
4 changes: 4 additions & 0 deletions cpp/src/TunerEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
7 changes: 4 additions & 3 deletions documents/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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');
```

---
Expand Down Expand Up @@ -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.

---

Expand Down
7 changes: 6 additions & 1 deletion ios/TunerBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 @{
Expand Down
Loading
Loading