Skip to content

Commit c9d3b6c

Browse files
authored
Add microphone demo about VAD+ASR for HarmonyOS (#1581)
1 parent 299f239 commit c9d3b6c

File tree

7 files changed

+276
-23
lines changed

7 files changed

+276
-23
lines changed

harmony-os/SherpaOnnxVadAsr/entry/src/main/ets/pages/Index.ets

Lines changed: 161 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,59 +3,154 @@ import worker, { MessageEvents } from '@ohos.worker';
33
import { BusinessError } from '@kit.BasicServicesKit';
44
import { picker } from '@kit.CoreFileKit';
55

6+
import { Permissions } from '@kit.AbilityKit';
7+
import { allAllowed, requestPermissions } from './Permission';
8+
import { audio } from '@kit.AudioKit';
9+
610

711
@Entry
812
@Component
913
struct Index {
1014
@State currentIndex: number = 0;
11-
@State resultFromFile: string = '';
15+
@State resultForFile: string = '';
1216
@State progressForFile: number = 0;
1317
@State selectFileBtnEnabled: boolean = false;
14-
@State message: string = 'To be implemented';
1518
@State lang: string = 'English';
19+
@State resultForMic: string = '';
20+
@State micStarted: boolean = false;
21+
@State message: string = 'Start recording';
22+
@State micInitDone: boolean = false;
1623
private controller: TabsController = new TabsController();
1724
private workerInstance?: worker.ThreadWorker
1825
private readonly scriptURL: string = 'entry/ets/workers/NonStreamingAsrWithVadWorker.ets'
26+
private mic?: audio.AudioCapturer;
27+
private sampleList: Float32Array[] = []
28+
29+
flatten(samples: Float32Array[]): Float32Array {
30+
let n = 0;
31+
for (let i = 0; i < samples.length; ++i) {
32+
n += samples[i].length;
33+
}
34+
35+
const ans: Float32Array = new Float32Array(n);
36+
let offset: number = 0;
37+
for (let i = 0; i < samples.length; ++i) {
38+
ans.set(samples[i], offset);
39+
offset += samples[i].length;
40+
}
41+
42+
return ans;
43+
}
44+
45+
async initMic() {
46+
const permissions: Permissions[] = ["ohos.permission.MICROPHONE"];
47+
let allowed: boolean = await allAllowed(permissions);
48+
if (!allowed) {
49+
requestPermissions(permissions);
50+
console.log("request to access the microphone");
51+
52+
allowed = await allAllowed(permissions);
53+
if (!allowed) {
54+
console.error('failed to get microphone permission');
55+
this.resultForMic = "Failed to get microphone permission. Please retry";
56+
return;
57+
}
58+
} else {
59+
console.log("allowed to access microphone");
60+
}
1961

20-
aboutToAppear(): void {
62+
const audioStreamInfo: audio.AudioStreamInfo = {
63+
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000,
64+
channels: audio.AudioChannel.CHANNEL_1,
65+
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
66+
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW,
67+
};
68+
69+
const audioCapturerInfo: audio.AudioCapturerInfo = {
70+
source: audio.SourceType.SOURCE_TYPE_MIC,
71+
capturerFlags: 0
72+
};
73+
74+
const audioCapturerOptions: audio.AudioCapturerOptions = {
75+
streamInfo: audioStreamInfo,
76+
capturerInfo: audioCapturerInfo
77+
78+
};
79+
audio.createAudioCapturer(audioCapturerOptions, (err, data) => {
80+
if (err) {
81+
console.error(`error code is ${err.code}, error message is ${err.message}`);
82+
this.resultForMic = 'Failed to init microphone';
83+
} else {
84+
console.info(`init mic successfully`);
85+
this.mic = data;
86+
this.mic.on('readData', this.micCallback);
87+
88+
if (this.workerInstance) {
89+
this.workerInstance.postMessage({ msgType: 'init-vad-mic', context: getContext() });
90+
}
91+
}
92+
});
93+
}
94+
95+
async aboutToAppear() {
2196
this.workerInstance = new worker.ThreadWorker(this.scriptURL, {
2297
name: 'NonStreaming ASR worker'
2398
});
2499

25100
this.workerInstance.onmessage = (e: MessageEvents) => {
26101
const msgType = e.data['msgType'] as string;
27-
console.log(`received data ${msgType}`);
102+
console.log(`received msg from worker: ${msgType}`);
103+
104+
if (msgType == 'init-vad-mic-done') {
105+
this.micInitDone = true;
106+
}
28107

29108
if (msgType == 'init-non-streaming-asr-done') {
30109
this.selectFileBtnEnabled = true;
110+
this.resultForFile = `Initializing done.\n\nPlease select a wave file of 16kHz in language ${this.lang}`;
31111
}
32112

33113
if (msgType == 'non-streaming-asr-vad-decode-done') {
34-
this.resultFromFile = e.data['text'] as string + '\n';
114+
this.resultForFile = e.data['text'] as string + '\n';
35115
}
36116

37117
if (msgType == 'non-streaming-asr-vad-decode-partial') {
38-
if (this.resultFromFile == '') {
39-
this.resultFromFile = e.data['text'] as string;
118+
if (this.resultForFile == '') {
119+
this.resultForFile = e.data['text'] as string;
40120
} else {
41-
this.resultFromFile += '\n\n' + e.data['text'] as string;
121+
this.resultForFile += '\n\n' + e.data['text'] as string;
42122
}
43123
}
44124

45125
if (msgType == 'non-streaming-asr-vad-decode-error') {
46-
this.resultFromFile = e.data['text'] as string;
126+
this.resultForFile = e.data['text'] as string;
47127
}
48128

49129
if (msgType == 'non-streaming-asr-vad-decode-progress') {
50130
this.progressForFile = e.data['progress'] as number;
51131

52132
this.selectFileBtnEnabled = this.progressForFile >= 100;
53133
}
134+
135+
if (msgType == 'non-streaming-asr-vad-mic-partial') {
136+
if (this.resultForMic == '') {
137+
this.resultForMic = e.data['text'] as string;
138+
} else {
139+
this.resultForMic += '\n\n' + e.data['text'] as string;
140+
}
141+
}
142+
143+
if (msgType == 'non-streaming-asr-vad-mic-error') {
144+
this.resultForMic = e.data['text'] as string;
145+
}
54146
}
55147

56148
const context = getContext();
149+
this.resultForFile = 'Initializing models';
57150
this.workerInstance.postMessage({ msgType: 'init-vad', context });
58151
this.workerInstance.postMessage({ msgType: 'init-non-streaming-asr', context });
152+
153+
await this.initMic();
59154
}
60155

61156
@Builder
@@ -86,13 +181,13 @@ struct Index {
86181
.lineHeight(41)
87182
.fontWeight(500)
88183

89-
Button('Select .wav file ')
184+
Button('Select .wav file (16kHz) ')
90185
.enabled(this.selectFileBtnEnabled)
91186
.fontSize(13)
92187
.width(296)
93188
.height(60)
94189
.onClick(() => {
95-
this.resultFromFile = '';
190+
this.resultForFile = '';
96191
this.progressForFile = 0;
97192

98193
const documentSelectOptions = new picker.DocumentSelectOptions();
@@ -103,7 +198,7 @@ struct Index {
103198
console.log(`Result: ${result}`);
104199

105200
if (!result[0]) {
106-
this.resultFromFile = 'Please select a file to decode';
201+
this.resultForFile = 'Please select a file to decode';
107202
this.selectFileBtnEnabled = true;
108203
return;
109204
}
@@ -135,7 +230,7 @@ struct Index {
135230
}.width('100%').justifyContent(FlexAlign.Center)
136231
}
137232

138-
TextArea({ text: this.resultFromFile }).width('100%').lineSpacing({ value: 10, unit: LengthUnit.VP });
233+
TextArea({ text: this.resultForFile }).width('100%').lineSpacing({ value: 10, unit: LengthUnit.VP });
139234

140235
}
141236
.alignItems(HorizontalAlign.Center)
@@ -144,10 +239,50 @@ struct Index {
144239

145240
TabContent() {
146241
Column() {
147-
Text(this.message)
148-
.fontSize(50)
149-
.fontWeight(FontWeight.Bold);
242+
Button(this.message)
243+
.enabled(this.micInitDone)
244+
.onClick(() => {
245+
console.log('clicked mic button');
246+
this.resultForMic = '';
247+
if (this.mic) {
248+
if (this.micStarted) {
249+
this.mic.stop();
250+
this.message = "Start recording";
251+
this.micStarted = false;
252+
console.log('mic stopped');
253+
254+
const samples = this.flatten(this.sampleList);
255+
let s = 0;
256+
for (let i = 0; i < samples.length; ++i) {
257+
s += samples[i];
258+
}
259+
console.log(`samples ${samples.length}, sum: ${s}`);
260+
261+
if (this.workerInstance) {
262+
console.log('decode mic');
263+
this.workerInstance.postMessage({
264+
msgType: 'non-streaming-asr-vad-mic',
265+
samples,
266+
});
267+
} else {
268+
console.log(`this worker instance is undefined ${this.workerInstance}`);
269+
}
270+
} else {
271+
this.sampleList = [];
272+
this.mic.start();
273+
this.message = "Stop recording";
274+
this.micStarted = true;
275+
console.log('mic started');
276+
}
277+
}
278+
});
279+
280+
Text(`Supported languages: ${this.lang}`)
281+
282+
TextArea({ text: this.resultForMic }).width('100%').lineSpacing({ value: 10, unit: LengthUnit.VP });
150283
}
284+
.alignItems(HorizontalAlign.Center)
285+
.justifyContent(FlexAlign.Start)
151286
}
152287
.tabBar(this.TabBuilder('From mic', 1, $r('app.media.ic_public_input_voice'),
153288
$r('app.media.ic_public_input_voice_default')))
@@ -170,4 +305,14 @@ struct Index {
170305
.width('100%')
171306
.justifyContent(FlexAlign.Start)
172307
}
308+
309+
private micCallback = (buffer: ArrayBuffer) => {
310+
const view: Int16Array = new Int16Array(buffer);
311+
312+
const samplesFloat: Float32Array = new Float32Array(view.length);
313+
for (let i = 0; i < view.length; ++i) {
314+
samplesFloat[i] = view[i] / 32768.0;
315+
}
316+
this.sampleList.push(samplesFloat);
317+
}
173318
}

harmony-os/SherpaOnnxVadAsr/entry/src/main/ets/pages/NonStreamingAsrModels.ets

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,10 @@ export function getOfflineModelConfig(type: number): OfflineModelConfig {
229229

230230
break;
231231
}
232+
default: {
233+
console.log(`Please specify a supported type. Given type ${type}`);
234+
}
232235
}
233236

234-
console.log(`Please specify a supported type. Given type ${type}`);
235-
236237
return c;
237238
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// This file is modified from
2+
// https://gitee.com/ukSir/hmchat2/blob/master/entry/src/main/ets/utils/permissionMananger.ets
3+
import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit';
4+
5+
export function allAllowed(permissions: Permissions[]): boolean {
6+
if (permissions.length == 0) {
7+
return false;
8+
}
9+
10+
const mgr: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
11+
12+
const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
13+
14+
let tokenID: number = bundleInfo.appInfo.accessTokenId;
15+
16+
return permissions.every(permission => abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED ==
17+
mgr.checkAccessTokenSync(tokenID, permission));
18+
}
19+
20+
export async function requestPermissions(permissions: Permissions[]): Promise<boolean> {
21+
const mgr: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
22+
const context: Context = getContext() as common.UIAbilityContext;
23+
24+
const result = await mgr.requestPermissionsFromUser(context, permissions);
25+
return result.authResults.length > 0 && result.authResults.every(authResults => authResults == 0);
26+
}

0 commit comments

Comments
 (0)