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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Changelog

## <small>2.1.6 (2025-09-12)</small>

* 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

## <small>2.1.5 (2025-09-12)</small>

* fix: avoid crash on ios when multiple starts are thrown ([1aa44be](https://github.com/riderodd/react-native-vosk/commit/1aa44be))
Expand Down
6 changes: 3 additions & 3 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2679,4 +2679,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 605b934b5eb489405fbad7204a4924dc73596edb

COCOAPODS: 1.15.2
COCOAPODS: 1.16.2
156 changes: 68 additions & 88 deletions ios/Vosk.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -41,6 +43,8 @@ - (instancetype)init {
_isRunning = NO;
_isStarting = NO;
_tapInstalled = NO;
_pendingTap = NO;
_tapRetryCount = 0;
}
return self;
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<int16_t> 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) {
Expand All @@ -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<int16_t> 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); }

Copilot AI Sep 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines contain extremely long single-line statements that are difficult to read and maintain. The original multi-line formatting should be preserved for better code readability and debugging.

Suggested change
const char *cstr = NULL; BOOL isFinal = NO; if (accepted) { cstr = vosk_recognizer_result(recognizer); isFinal = YES; } else { cstr = vosk_recognizer_partial_result(recognizer); }
const char *cstr = NULL;
BOOL isFinal = NO;
if (accepted) {
cstr = vosk_recognizer_result(recognizer);
isFinal = YES;
} else {
cstr = vosk_recognizer_partial_result(recognizer);
}

Copilot uses AI. Check for mistakes.
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; } });

Copilot AI Sep 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines contain extremely long single-line statements that are difficult to read and maintain. The original multi-line formatting should be preserved for better code readability and debugging.

Suggested change
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; } });
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;
}
});

Copilot uses AI. Check for mistakes.
});
}];
_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
Expand Down Expand Up @@ -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];
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading