diff --git a/CHANGELOG.md b/CHANGELOG.md
index 75b562c..2fc9490 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,36 @@
# Changelog
+## 2.1.6 (2025-09-12)
+
+* fix: Fix format rate error on iOS (#104) ([d8fe8b0](https://github.com/riderodd/react-native-vosk/commit/d8fe8b0)), closes [#104](https://github.com/riderodd/react-native-vosk/issues/104)
+* fix: ios record on simulator ([ff50aba](https://github.com/riderodd/react-native-vosk/commit/ff50aba))
+* fix: yarn lockfile (#155) ([507e67d](https://github.com/riderodd/react-native-vosk/commit/507e67d)), closes [#155](https://github.com/riderodd/react-native-vosk/issues/155)
+* #67: replace file:// in name path, have a more descriptive log (#79) ([4e756da](https://github.com/riderodd/react-native-vosk/commit/4e756da)), closes [#67](https://github.com/riderodd/react-native-vosk/issues/67) [#79](https://github.com/riderodd/react-native-vosk/issues/79)
+* Add Expo support for react-native-vosk (#151) ([fec6fb9](https://github.com/riderodd/react-native-vosk/commit/fec6fb9)), closes [#151](https://github.com/riderodd/react-native-vosk/issues/151)
+* Create dependabot.yml (#71) ([8634eba](https://github.com/riderodd/react-native-vosk/commit/8634eba)), closes [#71](https://github.com/riderodd/react-native-vosk/issues/71)
+* fix crashes (#143) ([cc9ea78](https://github.com/riderodd/react-native-vosk/commit/cc9ea78)), closes [#143](https://github.com/riderodd/react-native-vosk/issues/143)
+* Fix expo ios model path (#156) ([e2877d9](https://github.com/riderodd/react-native-vosk/commit/e2877d9)), closes [#156](https://github.com/riderodd/react-native-vosk/issues/156)
+* Fix Expo plugin configuration and dependencies (#152) ([3fa62f7](https://github.com/riderodd/react-native-vosk/commit/3fa62f7)), closes [#152](https://github.com/riderodd/react-native-vosk/issues/152)
+* Fix ios pcm bitrate (#157) ([948534e](https://github.com/riderodd/react-native-vosk/commit/948534e)), closes [#157](https://github.com/riderodd/react-native-vosk/issues/157)
+* Fix/expo ios (#154) ([cbdf9eb](https://github.com/riderodd/react-native-vosk/commit/cbdf9eb)), closes [#154](https://github.com/riderodd/react-native-vosk/issues/154)
+* Fix/update version (#106) ([eeebd52](https://github.com/riderodd/react-native-vosk/commit/eeebd52)), closes [#106](https://github.com/riderodd/react-native-vosk/issues/106)
+* Try to make expo plugin works (#153) ([b54f844](https://github.com/riderodd/react-native-vosk/commit/b54f844)), closes [#153](https://github.com/riderodd/react-native-vosk/issues/153)
+* Update dependabot.yml (#72) ([4013cf3](https://github.com/riderodd/react-native-vosk/commit/4013cf3)), closes [#72](https://github.com/riderodd/react-native-vosk/issues/72)
+* Update issue templates (#76) ([9e4ac88](https://github.com/riderodd/react-native-vosk/commit/9e4ac88)), closes [#76](https://github.com/riderodd/react-native-vosk/issues/76)
+* Update to TurboModules with breaking changes (#150) ([4869ff0](https://github.com/riderodd/react-native-vosk/commit/4869ff0)), closes [#150](https://github.com/riderodd/react-native-vosk/issues/150)
+* Update Vosk Android library and model loading method (#149) ([3248919](https://github.com/riderodd/react-native-vosk/commit/3248919)), closes [#149](https://github.com/riderodd/react-native-vosk/issues/149)
+* docs: add Copilot Coding Agent onboarding instructions (#148) ([1881c34](https://github.com/riderodd/react-native-vosk/commit/1881c34)), closes [#148](https://github.com/riderodd/react-native-vosk/issues/148)
+* chore: release 0.3.2 (#80) ([37ec42a](https://github.com/riderodd/react-native-vosk/commit/37ec42a)), closes [#80](https://github.com/riderodd/react-native-vosk/issues/80)
+* chore: updated release it deps (#105) ([b73429c](https://github.com/riderodd/react-native-vosk/commit/b73429c)), closes [#105](https://github.com/riderodd/react-native-vosk/issues/105)
+* chore(deps-dev): bump @react-native/metro-config from 0.76.1 to 0.79.2 (#118) ([a14e95a](https://github.com/riderodd/react-native-vosk/commit/a14e95a)), closes [#118](https://github.com/riderodd/react-native-vosk/issues/118)
+
+
+### BREAKING CHANGE
+
+* the model is not searched in the android project by default anymore
+
+* chore: release 1.0.0
+
## 2.1.5 (2025-09-12)
* fix: avoid crash on ios when multiple starts are thrown ([1aa44be](https://github.com/riderodd/react-native-vosk/commit/1aa44be))
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index a7bea9c..c635995 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1835,7 +1835,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - react-native-vosk (2.1.4):
+ - react-native-vosk (2.1.5):
- boost
- DoubleConversion
- fast_float
@@ -2643,7 +2643,7 @@ SPEC CHECKSUMS:
React-Mapbuffer: 91e0eab42a6ae7f3e34091a126d70fc53bd3823e
React-microtasksnativemodule: 1ead4fe154df3b1ba34b5a9e35ef3c4bdfa72ccb
react-native-safe-area-context: c6e2edd1c1da07bdce287fa9d9e60c5f7b514616
- react-native-vosk: 25bd5e5a7b3b1197ebf56c65e783c9852cc04fa8
+ react-native-vosk: 797d9009626d4bfa1b0522b7b1aa84def66c1f17
React-NativeModulesApple: eff2eba56030eb0d107b1642b8f853bc36a833ac
React-oscompat: b12c633e9c00f1f99467b1e0e0b8038895dae436
React-perflogger: 58d12c4e5df1403030c97b9c621375c312cca454
@@ -2679,4 +2679,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 605b934b5eb489405fbad7204a4924dc73596edb
-COCOAPODS: 1.15.2
+COCOAPODS: 1.16.2
diff --git a/ios/Vosk.mm b/ios/Vosk.mm
index f80a2b8..50e6762 100644
--- a/ios/Vosk.mm
+++ b/ios/Vosk.mm
@@ -22,6 +22,8 @@ @implementation Vosk {
BOOL _isRunning; // protects use of recognizer after stop
BOOL _isStarting; // prevents concurrent start
BOOL _tapInstalled; // track tap installation
+ BOOL _pendingTap; // indicates we are retrying tap installation
+ int _tapRetryCount; // retry counter
}
RCT_EXPORT_MODULE()
@@ -41,6 +43,8 @@ - (instancetype)init {
_isRunning = NO;
_isStarting = NO;
_tapInstalled = NO;
+ _pendingTap = NO;
+ _tapRetryCount = 0;
}
return self;
}
@@ -87,6 +91,8 @@ - (void)start:(JS::NativeVosk::VoskOptions *_Nullable)options
return;
}
_isStarting = YES;
+ _tapRetryCount = 0;
+ _pendingTap = NO;
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
// Extract options (grammar, timeout) from the codegen structure
@@ -153,9 +159,9 @@ - (void)start:(JS::NativeVosk::VoskOptions *_Nullable)options
}
self->_formatInput = [self->_inputNode inputFormatForBus:0];
- const double sampleRate = (self->_formatInput.sampleRate > 0) ? self->_formatInput.sampleRate : 16000.0;
- const AVAudioFrameCount bufferSize = (AVAudioFrameCount)(sampleRate / 10.0);
- self->_isRunning = YES;
+ const double sampleRate = (self->_formatInput.sampleRate > 0) ? self->_formatInput.sampleRate : 16000.0;
+ const AVAudioFrameCount bufferSize = (AVAudioFrameCount)(sampleRate / 10.0);
+ self->_isRunning = YES;
__weak __typeof(self) weakSelf = self;
dispatch_async(self->_processingQueue, ^{ // recognizer init off main
@@ -185,97 +191,19 @@ - (void)start:(JS::NativeVosk::VoskOptions *_Nullable)options
return; // promise already resolved later below or not? -> we won't resolve now
}
- dispatch_async(dispatch_get_main_queue(), ^{ // install tap + start engine
+ dispatch_async(dispatch_get_main_queue(), ^{ // start engine first
if (!self->_isRunning) { self->_isStarting = NO; return; }
- if (self->_tapInstalled) {
- NSLog(@"[Vosk] Tap already installed – skipping reinstall");
- self->_isStarting = NO;
- resolve(nil);
- return;
- }
- AVAudioFormat *tapFormat = [self->_inputNode inputFormatForBus:0];
- if (!tapFormat) {
- [self emitOnError:@"tapFormat is nil; cannot install tap"];
- [self stopInternalWithoutEvents:YES];
- self->_isStarting = NO;
- return;
- }
- BOOL invalidSR = tapFormat.sampleRate <= 0.0;
- BOOL invalidCh = tapFormat.channelCount == 0;
- if (invalidSR || invalidCh) {
- NSLog(@"[Vosk] Invalid input format (sr=%.1f ch=%u) -> using nil format", tapFormat.sampleRate, (unsigned int)tapFormat.channelCount);
- tapFormat = nil;
- }
- __weak __typeof(self) weakSelfTap = self;
- @try {
- [self->_inputNode installTapOnBus:0 bufferSize:bufferSize format:tapFormat block:^(AVAudioPCMBuffer *buffer, AVAudioTime *when) {
- __strong __typeof(self) self = weakSelfTap; if (!self) return; if (!self->_isRunning) return;
- dispatch_async(self->_processingQueue, ^{
- if (!self->_isRunning) return; VoskRecognizer *recognizer = self->_recognizer; if (!recognizer) return;
- AVAudioFrameCount frames = buffer.frameLength; if (frames == 0) return;
- int accepted = 0;
- if (buffer.int16ChannelData && buffer.int16ChannelData[0]) {
- int dataLen = (int)(frames * sizeof(int16_t));
- accepted = vosk_recognizer_accept_waveform(recognizer, (const char *)buffer.int16ChannelData[0], (int32_t)dataLen);
- } else if (buffer.floatChannelData && buffer.floatChannelData[0]) {
- float *const *floatChannels = buffer.floatChannelData; std::vector pcm16; pcm16.resize(frames); float *ch0 = floatChannels[0];
- for (AVAudioFrameCount i = 0; i < frames; ++i) { float s = ch0[i]; if (s > 1.f) s = 1.f; else if (s < -1.f) s = -1.f; pcm16[i] = (int16_t)lrintf(s * 32767.f); }
- int dataLen = (int)(frames * sizeof(int16_t));
- accepted = vosk_recognizer_accept_waveform(recognizer, (const char *)pcm16.data(), (int32_t)dataLen);
- } else { return; }
-
- const char *cstr = NULL;
- BOOL isFinal = NO;
- if (accepted) { // end of utterance recognized per Vosk API
- cstr = vosk_recognizer_result(recognizer);
- isFinal = YES;
- } else {
- cstr = vosk_recognizer_partial_result(recognizer);
- isFinal = NO;
- }
- NSString *json = cstr ? [NSString stringWithUTF8String:cstr] : nil;
- dispatch_async(dispatch_get_main_queue(), ^{
- if (!json) return;
- NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
- NSDictionary *parsed = data ? [NSJSONSerialization JSONObjectWithData:data options:0 error:nil] : nil;
- if (![parsed isKindOfClass:[NSDictionary class]]) {
- if (isFinal) { [self emitOnResult:json]; } else { [self emitOnPartialResult:json]; }
- return;
- }
- NSString *text = parsed[@"text"]; NSString *partial = parsed[@"partial"];
- if (isFinal) {
- if (text.length > 0) { [self emitOnResult:text]; }
- self->_lastPartial = nil; // reset after final
- } else {
- if (partial.length > 0 && (!self->_lastPartial || ![self->_lastPartial isEqualToString:partial])) {
- [self emitOnPartialResult:partial];
- }
- self->_lastPartial = partial ?: self->_lastPartial;
- }
- });
- });
- }];
- self->_tapInstalled = YES;
- } @catch (NSException *ex) {
- [self emitOnError:[NSString stringWithFormat:@"Failed to install tap: %@", ex.reason ?: @"unknown"]];
- [self stopInternalWithoutEvents:YES];
- self->_isStarting = NO;
- reject(@"start", @"Failed to install tap", nil);
- return;
- }
[self->_audioEngine prepare];
NSError *startErr = nil;
if (![self->_audioEngine startAndReturnError:&startErr]) {
NSString *msg = [NSString stringWithFormat:@"Failed to start audio engine: %@", startErr.localizedDescription];
[self emitOnError:msg];
- // clean
[self stopInternalWithoutEvents:YES];
- reject(@"start", msg, startErr); // still reject if not yet resolved
self->_isStarting = NO;
+ reject(@"start", msg, startErr);
return;
}
-
- // Timeout timer after engine started
+ // Timeout timer
if (timeoutMs >= 0) {
self->_timeoutSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self->_processingQueue);
if (self->_timeoutSource) {
@@ -286,18 +214,69 @@ - (void)start:(JS::NativeVosk::VoskOptions *_Nullable)options
dispatch_resume(self->_timeoutSource);
}
}
-
CFAbsoluteTime tEnd = CFAbsoluteTimeGetCurrent();
- NSLog(@"[Vosk] start() total after permission: %.2f ms", (tEnd - tStart) * 1000.0);
- // Resolve promise (success path)
- self->_isStarting = NO;
+ NSLog(@"[Vosk] start() engine running (tap pending): %.2f ms", (tEnd - tStart) * 1000.0);
+ // Resolve early; tap installation will be async via retry
resolve(nil);
+ self->_isStarting = NO;
+ self->_pendingTap = YES;
+ self->_tapRetryCount = 0;
+ [self scheduleTapInstallationWithBufferSize:bufferSize];
});
});
});
}];
}
+// Retry-based tap installer; called on main queue only
+- (void)scheduleTapInstallationWithBufferSize:(AVAudioFrameCount)bufferSize {
+ if (!_isRunning) { _pendingTap = NO; return; }
+ if (_tapInstalled) { _pendingTap = NO; return; }
+ if (!_recognizer) { _pendingTap = NO; return; }
+ AVAudioFormat *fmt = [_inputNode inputFormatForBus:0];
+ double sr = fmt ? fmt.sampleRate : 0.0;
+ AVAudioChannelCount ch = fmt ? fmt.channelCount : 0;
+ if (sr <= 0.0 || ch == 0) {
+ if (_tapRetryCount == 0) {
+ NSLog(@"[Vosk] Tap install waiting for valid format… (sr=%.1f ch=%u)", sr, (unsigned int)ch);
+ }
+ if (_tapRetryCount >= 25) { // ~2.5s at 100ms intervals
+ [self emitOnError:@"Unable to obtain valid audio input format (stabilization timeout)"]; _pendingTap = NO; return; }
+ _tapRetryCount++;
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self scheduleTapInstallationWithBufferSize:bufferSize]; });
+ return;
+ }
+ // Have a valid format now
+ NSLog(@"[Vosk] Tap install attempt with format sr=%.1f ch=%u retry=%d", sr, (unsigned int)ch, _tapRetryCount);
+ __weak __typeof(self) weakSelfTap = self;
+ @try {
+ [_inputNode installTapOnBus:0 bufferSize:bufferSize format:fmt block:^(AVAudioPCMBuffer *buffer, AVAudioTime *when) {
+ __strong __typeof(self) self = weakSelfTap; if (!self) return; if (!self->_isRunning) return;
+ dispatch_async(self->_processingQueue, ^{
+ if (!self->_isRunning) return; VoskRecognizer *recognizer = self->_recognizer; if (!recognizer) return;
+ AVAudioFrameCount frames = buffer.frameLength; if (frames == 0) return;
+ int accepted = 0;
+ if (buffer.int16ChannelData && buffer.int16ChannelData[0]) {
+ int dataLen = (int)(frames * sizeof(int16_t));
+ accepted = vosk_recognizer_accept_waveform(recognizer, (const char *)buffer.int16ChannelData[0], (int32_t)dataLen);
+ } else if (buffer.floatChannelData && buffer.floatChannelData[0]) {
+ float *const *floatChannels = buffer.floatChannelData; std::vector pcm16; pcm16.resize(frames); float *ch0 = floatChannels[0];
+ for (AVAudioFrameCount i = 0; i < frames; ++i) { float s = ch0[i]; if (s > 1.f) s = 1.f; else if (s < -1.f) s = -1.f; pcm16[i] = (int16_t)lrintf(s * 32767.f); }
+ int dataLen = (int)(frames * sizeof(int16_t));
+ accepted = vosk_recognizer_accept_waveform(recognizer, (const char *)pcm16.data(), (int32_t)dataLen);
+ } else { return; }
+ const char *cstr = NULL; BOOL isFinal = NO; if (accepted) { cstr = vosk_recognizer_result(recognizer); isFinal = YES; } else { cstr = vosk_recognizer_partial_result(recognizer); }
+ NSString *json = cstr ? [NSString stringWithUTF8String:cstr] : nil;
+ dispatch_async(dispatch_get_main_queue(), ^{ if (!json) return; NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *parsed = data ? [NSJSONSerialization JSONObjectWithData:data options:0 error:nil] : nil; if (![parsed isKindOfClass:[NSDictionary class]]) { if (isFinal) { [self emitOnResult:json]; } else { [self emitOnPartialResult:json]; } return; } NSString *text = parsed[@"text"]; NSString *partial = parsed[@"partial"]; if (isFinal) { if (text.length > 0) { [self emitOnResult:text]; } self->_lastPartial = nil; } else { if (partial.length > 0 && (!self->_lastPartial || ![self->_lastPartial isEqualToString:partial])) { [self emitOnPartialResult:partial]; } self->_lastPartial = partial ?: self->_lastPartial; } });
+ });
+ }];
+ _tapInstalled = YES; _pendingTap = NO; NSLog(@"[Vosk] Tap installed successfully after %d retries.", _tapRetryCount);
+ } @catch (NSException *ex) {
+ [self emitOnError:[NSString stringWithFormat:@"Failed to install tap (retry phase): %@", ex.reason ?: @"unknown"]];
+ _pendingTap = NO; [self stopInternalWithoutEvents:YES];
+ }
+}
+
- (void)stop {
if (!_isRunning)
return; // idempotent
@@ -335,6 +314,7 @@ - (void)stopInternalWithoutEvents:(BOOL)withoutEvents {
_isRunning = NO; // prevents new buffers from being processed
_isStarting = NO;
_tapInstalled = NO;
+ _pendingTap = NO;
if (_audioEngine.isRunning) {
[_audioEngine stop];
diff --git a/package.json b/package.json
index cc31733..0108ce4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-native-vosk",
- "version": "2.1.5",
+ "version": "2.1.6",
"description": "Speech recognition module for react native using Vosk library",
"types": "lib/typescript/src/index.d.ts",
"main": "lib/commonjs/index.js",