react-native-simple-note-pitch-detector 0.7.4 → 0.7.6
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.
|
@@ -4,6 +4,71 @@ import Pitchy
|
|
|
4
4
|
import AVFoundation
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
// Beethoven's built-in InputSignalTracker runs a second AVCaptureSession
|
|
8
|
+
// just to read audio levels. On iOS 26 that returns -inf, so the tap's
|
|
9
|
+
// `averageLevel > threshold` check is always false and pitch detection
|
|
10
|
+
// never sees a single buffer. This replacement uses ONE AVAudioEngine and
|
|
11
|
+
// computes the level directly from each buffer.
|
|
12
|
+
final class BufferLevelSignalTracker: NSObject, SignalTracker {
|
|
13
|
+
weak var delegate: SignalTrackerDelegate?
|
|
14
|
+
var levelThreshold: Float?
|
|
15
|
+
|
|
16
|
+
private let bufferSize: AVAudioFrameCount
|
|
17
|
+
private var audioEngine: AVAudioEngine?
|
|
18
|
+
private var lastLevel: Float = -160.0
|
|
19
|
+
private let bus = 0
|
|
20
|
+
|
|
21
|
+
var mode: SignalTrackerMode { .record }
|
|
22
|
+
var averageLevel: Float? { lastLevel }
|
|
23
|
+
var peakLevel: Float? { lastLevel }
|
|
24
|
+
|
|
25
|
+
init(bufferSize: AVAudioFrameCount = 8192, delegate: SignalTrackerDelegate? = nil) {
|
|
26
|
+
self.bufferSize = bufferSize
|
|
27
|
+
self.delegate = delegate
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func start() throws {
|
|
31
|
+
let engine = AVAudioEngine()
|
|
32
|
+
audioEngine = engine
|
|
33
|
+
|
|
34
|
+
let inputNode = engine.inputNode
|
|
35
|
+
let format = inputNode.outputFormat(forBus: bus)
|
|
36
|
+
|
|
37
|
+
inputNode.installTap(onBus: bus, bufferSize: bufferSize, format: format) { [weak self] buffer, time in
|
|
38
|
+
guard let self = self else { return }
|
|
39
|
+
guard let channelData = buffer.floatChannelData?[0] else { return }
|
|
40
|
+
|
|
41
|
+
let frameLength = Int(buffer.frameLength)
|
|
42
|
+
var sumSquares: Float = 0
|
|
43
|
+
for i in 0..<frameLength {
|
|
44
|
+
let sample = channelData[i]
|
|
45
|
+
sumSquares += sample * sample
|
|
46
|
+
}
|
|
47
|
+
let rms = sqrt(sumSquares / Float(frameLength))
|
|
48
|
+
let level = 20 * log10f(max(rms, 1e-10))
|
|
49
|
+
self.lastLevel = level
|
|
50
|
+
|
|
51
|
+
let threshold = self.levelThreshold ?? -160.0
|
|
52
|
+
DispatchQueue.main.async {
|
|
53
|
+
if level > threshold {
|
|
54
|
+
self.delegate?.signalTracker(self, didReceiveBuffer: buffer, atTime: time)
|
|
55
|
+
} else {
|
|
56
|
+
self.delegate?.signalTrackerWentBelowLevelThreshold(self)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try engine.start()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func stop() {
|
|
65
|
+
audioEngine?.inputNode.removeTap(onBus: bus)
|
|
66
|
+
audioEngine?.stop()
|
|
67
|
+
audioEngine = nil
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
7
72
|
public class ReactNativeSimpleNotePitchDetectorModule: Module {
|
|
8
73
|
|
|
9
74
|
// Configurable buffer size - can be changed from JS
|
|
@@ -12,6 +77,7 @@ public class ReactNativeSimpleNotePitchDetectorModule: Module {
|
|
|
12
77
|
private var bufferSize: UInt32 = 8192
|
|
13
78
|
private var estimationStrategy: EstimationStrategy = .yin
|
|
14
79
|
private var _pitchEngine: PitchEngine?
|
|
80
|
+
private var belowThresholdLogged = false
|
|
15
81
|
|
|
16
82
|
private func sendStatus(_ level: String, _ message: String) {
|
|
17
83
|
self.sendEvent("onStatus", [
|
|
@@ -25,11 +91,13 @@ public class ReactNativeSimpleNotePitchDetectorModule: Module {
|
|
|
25
91
|
do {
|
|
26
92
|
try session.setCategory(
|
|
27
93
|
.playAndRecord,
|
|
28
|
-
|
|
29
|
-
options: [.defaultToSpeaker, .allowBluetooth]
|
|
94
|
+
options: [.defaultToSpeaker, .allowBluetoothHFP]
|
|
30
95
|
)
|
|
31
96
|
try session.setActive(true, options: .notifyOthersOnDeactivation)
|
|
32
|
-
self.sendStatus(
|
|
97
|
+
self.sendStatus(
|
|
98
|
+
"debug",
|
|
99
|
+
"AVAudioSession configured: category=playAndRecord, sampleRate=\(session.sampleRate), inputChannels=\(session.inputNumberOfChannels), inputAvailable=\(session.isInputAvailable)"
|
|
100
|
+
)
|
|
33
101
|
} catch {
|
|
34
102
|
self.sendStatus("error", "Failed to configure AVAudioSession: \(error.localizedDescription)")
|
|
35
103
|
}
|
|
@@ -138,9 +206,10 @@ public class ReactNativeSimpleNotePitchDetectorModule: Module {
|
|
|
138
206
|
return engine
|
|
139
207
|
}
|
|
140
208
|
let config = Config(bufferSize: bufferSize, estimationStrategy: estimationStrategy)
|
|
141
|
-
let
|
|
209
|
+
let signalTracker = BufferLevelSignalTracker(bufferSize: bufferSize)
|
|
210
|
+
let engine = PitchEngine(config: config, signalTracker: signalTracker, delegate: self)
|
|
142
211
|
// Default threshold - can be adjusted from JS via setLevelThreshold()
|
|
143
|
-
engine.levelThreshold = -
|
|
212
|
+
engine.levelThreshold = -60
|
|
144
213
|
_pitchEngine = engine
|
|
145
214
|
return engine
|
|
146
215
|
}
|
|
@@ -180,6 +249,10 @@ extension ReactNativeSimpleNotePitchDetectorModule: PitchEngineDelegate {
|
|
|
180
249
|
}
|
|
181
250
|
|
|
182
251
|
public func pitchEngineWentBelowLevelThreshold(_ pitchEngine: PitchEngine) {
|
|
183
|
-
//
|
|
252
|
+
// Log once per session to avoid spamming
|
|
253
|
+
if !belowThresholdLogged {
|
|
254
|
+
self.sendStatus("debug", "Audio level below threshold (audio is reaching Beethoven but too quiet)")
|
|
255
|
+
belowThresholdLogged = true
|
|
256
|
+
}
|
|
184
257
|
}
|
|
185
258
|
}
|
package/package.json
CHANGED