Commit 53e9e6db47d8f13736fd21d0c7007ec822bd5615

Authored by 吴启风
1 parent 08a0f5a8

feat:绘本语音评测逻辑

android/app/src/main/assets/vad.0.1.bin 100755 → 100644
No preview for this file type
android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt
@@ -2,9 +2,13 @@ package com.kouyuxingqiu.wow_english.methodChannels @@ -2,9 +2,13 @@ package com.kouyuxingqiu.wow_english.methodChannels
2 2
3 import android.util.Log 3 import android.util.Log
4 import com.kouyuxingqiu.wow_english.singsound.SingEngineHelper 4 import com.kouyuxingqiu.wow_english.singsound.SingEngineHelper
  5 +import com.kouyuxingqiu.wow_english.singsound.SingEngineHelper.init
  6 +import com.kouyuxingqiu.wow_english.singsound.SingEngineLifecycles
  7 +import com.kouyuxingqiu.wow_english.util.GlobalHandler
5 import io.flutter.embedding.android.FlutterActivity 8 import io.flutter.embedding.android.FlutterActivity
6 import io.flutter.embedding.engine.FlutterEngine 9 import io.flutter.embedding.engine.FlutterEngine
7 import io.flutter.plugin.common.MethodChannel 10 import io.flutter.plugin.common.MethodChannel
  11 +import org.json.JSONObject
8 import java.lang.ref.WeakReference 12 import java.lang.ref.WeakReference
9 13
10 /** 14 /**
@@ -12,13 +16,13 @@ import java.lang.ref.WeakReference @@ -12,13 +16,13 @@ import java.lang.ref.WeakReference
12 * @date: 2023/6/27 00:32 16 * @date: 2023/6/27 00:32
13 * @description: 17 * @description:
14 */ 18 */
15 -class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: FlutterEngine) { 19 +class SingSoungMethodChannel(activity: FlutterActivity, flutterEngine: FlutterEngine): SingEngineLifecycles.OnSingEngineAdapter() {
16 private var methodChannel: MethodChannel? = null 20 private var methodChannel: MethodChannel? = null
17 21
18 companion object { 22 companion object {
19 var channel: WeakReference<SingSoungMethodChannel>? = null 23 var channel: WeakReference<SingSoungMethodChannel>? = null
20 24
21 - fun invokeMethod(method: String, arguments: Any) { 25 + fun invokeMethod(method: String, arguments: Any?) {
22 channel?.get()?.methodChannel?.invokeMethod(method, arguments) 26 channel?.get()?.methodChannel?.invokeMethod(method, arguments)
23 } 27 }
24 } 28 }
@@ -30,10 +34,11 @@ class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: F @@ -30,10 +34,11 @@ class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: F
30 flutterEngine.dartExecutor.binaryMessenger, 34 flutterEngine.dartExecutor.binaryMessenger,
31 "wow_english/sing_sound_method_channel" 35 "wow_english/sing_sound_method_channel"
32 ) 36 )
  37 + init(activity)
33 methodChannel?.setMethodCallHandler { call, result -> 38 methodChannel?.setMethodCallHandler { call, result ->
34 when (call.method) { 39 when (call.method) {
35 "initVoiceSdk" -> { 40 "initVoiceSdk" -> {
36 - SingEngineHelper.init(activity) 41 +
37 } 42 }
38 "startVoice" -> { 43 "startVoice" -> {
39 val paramMap = call.arguments as HashMap<String, String> 44 val paramMap = call.arguments as HashMap<String, String>
@@ -43,6 +48,7 @@ class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: F @@ -43,6 +48,7 @@ class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: F
43 } 48 }
44 "stopVoice" -> { 49 "stopVoice" -> {
45 Log.d("WQF", "SingSoungMethodChannel stopVoice") 50 Log.d("WQF", "SingSoungMethodChannel stopVoice")
  51 + SingEngineHelper.stopRecord()
46 } 52 }
47 else -> { 53 else -> {
48 result.notImplemented() 54 result.notImplemented()
@@ -51,5 +57,26 @@ class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: F @@ -51,5 +57,26 @@ class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: F
51 57
52 } 58 }
53 channel = WeakReference(this) 59 channel = WeakReference(this)
  60 +
  61 + SingEngineHelper.addOnResultListener(this)
  62 + }
  63 +
  64 + override fun onResult(jsonObject: JSONObject, evalType: Int?) {
  65 + //先声回调在子线程,需要切换到主线程
  66 + GlobalHandler.runOnMainThread {
  67 + invokeMethod("voiceResult", jsonObject.toString())
  68 + }
  69 + }
  70 +
  71 + override fun onRecordBegin() {
  72 + GlobalHandler.runOnMainThread {
  73 + invokeMethod("voiceStart", null)
  74 + }
  75 + }
  76 +
  77 + override fun onRecordStop() {
  78 + GlobalHandler.runOnMainThread {
  79 + invokeMethod("voiceEnd", null)
  80 + }
54 } 81 }
55 } 82 }
56 \ No newline at end of file 83 \ No newline at end of file
android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt
@@ -73,7 +73,7 @@ object SingEngineHelper : @@ -73,7 +73,7 @@ object SingEngineHelper :
73 // 设置录音音频路径 73 // 设置录音音频路径
74 wavPath = AiUtil.getFilesDir(context).path + "/userdata/sound_record/" 74 wavPath = AiUtil.getFilesDir(context).path + "/userdata/sound_record/"
75 // 设置是否开启 VAD 功能 75 // 设置是否开启 VAD 功能
76 - setOpenVad(true, "vad.0.1.bin") 76 +// setOpenVad(true, "vad.0.1.bin")
77 //setOpenVad(false, null); 77 //setOpenVad(false, null);
78 // 设置 VAD 前置超时时间 78 // 设置 VAD 前置超时时间
79 setFrontVadTime(3000) 79 setFrontVadTime(3000)
@@ -383,6 +383,7 @@ object SingEngineHelper : @@ -383,6 +383,7 @@ object SingEngineHelper :
383 } 383 }
384 384
385 fun addOnResultListener(listener: SingEngineLifecycles) { 385 fun addOnResultListener(listener: SingEngineLifecycles) {
  386 + Log.i(TAG, "addOnResultListener")
386 if (mListeners?.contains(listener) == true) { 387 if (mListeners?.contains(listener) == true) {
387 return 388 return
388 } 389 }
@@ -390,6 +391,7 @@ object SingEngineHelper : @@ -390,6 +391,7 @@ object SingEngineHelper :
390 } 391 }
391 392
392 fun removeOnResultListener(listener: SingEngineLifecycles) { 393 fun removeOnResultListener(listener: SingEngineLifecycles) {
  394 + Log.i(TAG, "removeOnResultListener")
393 if (mListeners?.contains(listener) == true) { 395 if (mListeners?.contains(listener) == true) {
394 mListeners?.remove(listener) 396 mListeners?.remove(listener)
395 } 397 }
android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/BaseResultModel.kt
@@ -19,7 +19,9 @@ class BaseResultModel( @@ -19,7 +19,9 @@ class BaseResultModel(
19 constructor(parcel: Parcel) : this( 19 constructor(parcel: Parcel) : this(
20 parcel.readString(), 20 parcel.readString(),
21 parcel.readDouble(), 21 parcel.readDouble(),
22 - TODO("scores"), 22 + mutableListOf<SingleResultModel>().apply {
  23 + parcel.readTypedList(this, SingleResultModel.CREATOR)
  24 + },
23 parcel.readDouble(), 25 parcel.readDouble(),
24 parcel.readDouble() 26 parcel.readDouble()
25 ) { 27 ) {
@@ -59,6 +61,7 @@ class BaseResultModel( @@ -59,6 +61,7 @@ class BaseResultModel(
59 override fun writeToParcel(parcel: Parcel, flags: Int) { 61 override fun writeToParcel(parcel: Parcel, flags: Int) {
60 parcel.writeString(originText) 62 parcel.writeString(originText)
61 parcel.writeDouble(score) 63 parcel.writeDouble(score)
  64 + parcel.writeTypedList(scores)
62 parcel.writeDouble(pronounce) 65 parcel.writeDouble(pronounce)
63 parcel.writeDouble(fluency) 66 parcel.writeDouble(fluency)
64 } 67 }
android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/util/GlobalHandler.kt 0 → 100644
  1 +package com.kouyuxingqiu.wow_english.util
  2 +
  3 +import android.os.Handler
  4 +import android.os.Looper
  5 +
  6 +/**
  7 + * @author: stay
  8 + * @date: 2023/7/1 16:55
  9 + * @description: 全局Handler, 用于切线程等操作
  10 + */
  11 +object GlobalHandler {
  12 + private var handler: Handler? = null
  13 +
  14 + init {
  15 + handler = Handler(Looper.getMainLooper())
  16 + }
  17 +
  18 + fun runOnMainThread(runnable: Runnable?) {
  19 + runnable?.let {
  20 + handler?.post(it)
  21 + }
  22 + }
  23 +}
0 \ No newline at end of file 24 \ No newline at end of file
ios/Runner/XSMessageMehtodChannel.swift
@@ -13,7 +13,7 @@ class XSMessageMehtodChannel: NSObject,SSOralEvaluatingManagerDelegate { @@ -13,7 +13,7 @@ class XSMessageMehtodChannel: NSObject,SSOralEvaluatingManagerDelegate {
13 init(message:FlutterBinaryMessenger) { 13 init(message:FlutterBinaryMessenger) {
14 super.init() 14 super.init()
15 resultData = Dictionary() 15 resultData = Dictionary()
16 - messageChannel = FlutterMethodChannel.init(name: "wow_english/sing_sound_method_channely", binaryMessenger: message) 16 + messageChannel = FlutterMethodChannel.init(name: "wow_english/sing_sound_method_channel", binaryMessenger: message)
17 messageChannel!.setMethodCallHandler { call, result in 17 messageChannel!.setMethodCallHandler { call, result in
18 self.handle(call, result) 18 self.handle(call, result)
19 } 19 }
lib/models/course_process_entity.dart
@@ -34,6 +34,8 @@ class CourseProcessReadings { @@ -34,6 +34,8 @@ class CourseProcessReadings {
34 String? picUrl; 34 String? picUrl;
35 int? sortOrder; 35 int? sortOrder;
36 String? word; 36 String? word;
  37 + String? recordUrl;
  38 + String? recordScore;
37 39
38 CourseProcessReadings(); 40 CourseProcessReadings();
39 41
lib/pages/home/home_page.dart
@@ -116,6 +116,7 @@ class _HomePageView extends StatelessWidget { @@ -116,6 +116,7 @@ class _HomePageView extends StatelessWidget {
116 return; 116 return;
117 } 117 }
118 if (data.courseType == 4) {//绘本 118 if (data.courseType == 4) {//绘本
  119 + Navigator.of(context).pushNamed(AppRouteName.reading, arguments: {'courseLessonId':data.id!});
119 return; 120 return;
120 } 121 }
121 122
lib/pages/practice/bloc/topic_picture_bloc.dart
@@ -90,7 +90,7 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; { @@ -90,7 +90,7 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; {
90 add(VoicePlayStateChangeEvent()); 90 add(VoicePlayStateChangeEvent());
91 }); 91 });
92 92
93 - methodChannel = const MethodChannel('wow_english/sing_sound_method_channely'); 93 + methodChannel = const MethodChannel('wow_english/sing_sound_method_channel');
94 methodChannel.setMethodCallHandler((call) async { 94 methodChannel.setMethodCallHandler((call) async {
95 if (call.method == 'voiceResult') {//评测结果 95 if (call.method == 'voiceResult') {//评测结果
96 add(XSVoiceResultEvent(call.arguments)); 96 add(XSVoiceResultEvent(call.arguments));
@@ -181,7 +181,7 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; { @@ -181,7 +181,7 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; {
181 181
182 ///先声测试 182 ///先声测试
183 void _voiceXsTest(XSVoiceTestEvent event,Emitter<TopicPictureState> emitter) async { 183 void _voiceXsTest(XSVoiceTestEvent event,Emitter<TopicPictureState> emitter) async {
184 - audioPlayer.stop(); 184 + await audioPlayer.stop();
185 methodChannel.invokeMethod( 185 methodChannel.invokeMethod(
186 'startVoice', 186 'startVoice',
187 {'word':event.testWord,'type':event.type,'userId':event.userId.toString()} 187 {'word':event.testWord,'type':event.type,'userId':event.userId.toString()}
lib/pages/reading/bloc/reading_bloc.dart
@@ -10,13 +10,30 @@ import &#39;../../../common/request/dao/listen_dao.dart&#39;; @@ -10,13 +10,30 @@ import &#39;../../../common/request/dao/listen_dao.dart&#39;;
10 import '../../../common/request/exception.dart'; 10 import '../../../common/request/exception.dart';
11 import '../../../models/course_process_entity.dart'; 11 import '../../../models/course_process_entity.dart';
12 import '../../../utils/loading.dart'; 12 import '../../../utils/loading.dart';
  13 +import 'dart:convert';
13 14
14 part 'reading_event.dart'; 15 part 'reading_event.dart';
15 part 'reading_state.dart'; 16 part 'reading_state.dart';
16 17
  18 +enum VoicePlayState {
  19 + ///未知
  20 + unKnow,
  21 +
  22 + ///播放中
  23 + playing,
  24 +
  25 + ///播放完成
  26 + completed,
  27 +
  28 + ///播放终止
  29 + stop
  30 +}
  31 +
17 class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { 32 class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> {
18 final PageController pageController; 33 final PageController pageController;
19 34
  35 + final String courseLessonId;
  36 +
20 ///当前页索引 37 ///当前页索引
21 int _currentPage = 0; 38 int _currentPage = 0;
22 39
@@ -36,40 +53,98 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; { @@ -36,40 +53,98 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
36 53
37 bool get isRecording => _isRecording; 54 bool get isRecording => _isRecording;
38 55
  56 + ///原始音频是否正在播放
  57 + bool _isOriginAudioPlaying = false;
  58 +
  59 + bool get isOriginAudioPlaying => _isOriginAudioPlaying;
  60 +
  61 + ///录音音频是否正在播放
  62 + bool _isRecordAudioPlaying = false;
  63 +
  64 + bool get isRecordAudioPlaying => _isRecordAudioPlaying;
  65 +
  66 + ///正在播放音频状态
  67 + VoicePlayState _voicePlayState = VoicePlayState.unKnow;
  68 +
  69 + VoicePlayState get voicePlayState => _voicePlayState;
  70 +
39 late MethodChannel methodChannel; 71 late MethodChannel methodChannel;
40 72
41 late AudioPlayer audioPlayer; 73 late AudioPlayer audioPlayer;
42 74
43 - ReadingPageBloc(this.pageController) : super(ReadingPageInitial()) { 75 + ReadingPageBloc(this.pageController, this.courseLessonId)
  76 + : super(ReadingPageInitial()) {
44 on<CurrentPageIndexChangeEvent>(_pageControllerChange); 77 on<CurrentPageIndexChangeEvent>(_pageControllerChange);
45 - on<CurrentModeChangeEvent>(_selectItemLoad); 78 + on<CurrentModeChangeEvent>(_playModeChange);
46 // pageController.addListener(() { 79 // pageController.addListener(() {
47 // _currentPage = pageController.page!.round(); 80 // _currentPage = pageController.page!.round();
48 // }); 81 // });
49 on<RequestDataEvent>(_requestData); 82 on<RequestDataEvent>(_requestData);
50 - on<ReadingPageEvent>((event, emit) { 83 + on<InitBlocEvent>((event, emit) {
51 //音频播放器 84 //音频播放器
52 audioPlayer = AudioPlayer(); 85 audioPlayer = AudioPlayer();
53 - audioPlayer.onPlayerStateChanged.listen((event) { 86 + audioPlayer.onPlayerStateChanged.listen((event) async {
  87 + debugPrint('播放状态变化');
54 if (event == PlayerState.completed) { 88 if (event == PlayerState.completed) {
55 - if (kDebugMode) {  
56 - print('绘本播放完成'); 89 + debugPrint('播放完成');
  90 + _voicePlayState = VoicePlayState.completed;
  91 + }
  92 + if (event == PlayerState.stopped) {
  93 + debugPrint('播放结束');
  94 + _voicePlayState = VoicePlayState.stop;
  95 + }
57 96
58 - } 97 + if (event == PlayerState.playing) {
  98 + debugPrint('正在播放中');
  99 + _voicePlayState = VoicePlayState.playing;
  100 + }
  101 + if (isClosed) {
  102 + return;
59 } 103 }
  104 + add(VoicePlayStateChangeEvent());
60 }); 105 });
61 106
62 - methodChannel = const MethodChannel('sing_sound_method_channel');  
63 - methodChannel.invokeMethod('initVoiceSdk',{}); 107 + methodChannel =
  108 + const MethodChannel('wow_english/sing_sound_method_channel');
  109 + methodChannel.invokeMethod('initVoiceSdk', {}); //初始化评测
64 methodChannel.setMethodCallHandler((call) async { 110 methodChannel.setMethodCallHandler((call) async {
65 - if (call.method == 'voiceResult') {//评测结束  
66 - // add(XSVoiceResultEvent(call.arguments)); 111 + if (call.method == 'voiceResult') {
  112 + //评测结果
  113 + add(XSVoiceResultEvent(call.arguments));
  114 + return;
  115 + }
  116 +
  117 + if (call.method == 'voiceStart') {
  118 + //评测开始
  119 + if (kDebugMode) {
  120 + print('评测开始');
  121 + }
  122 + return;
  123 + }
  124 +
  125 + if (call.method == 'voiceEnd') {
  126 + //评测结束
  127 + if (kDebugMode) {
  128 + print('评测结束');
  129 + }
  130 + return;
  131 + }
  132 +
  133 + if (call.method == 'voiceFail') {
  134 + //评测失败
  135 + EasyLoading.showToast('评测失败');
  136 + return;
67 } 137 }
68 }); 138 });
69 }); 139 });
  140 + on<VoicePlayStateChangeEvent>(_voicePlayStateChange);
70 on<PlayOriginalAudioEvent>(_playOriginalAudio); 141 on<PlayOriginalAudioEvent>(_playOriginalAudio);
71 - on<XSVoiceTestEvent>(_voiceXsTest); 142 + on<XSVoiceInitEvent>(_initVoiceSdk);
  143 + on<XSVoiceStartEvent>(_voiceXsStart);
  144 + on<XSVoiceStopEvent>(_voiceXsStop);
72 on<XSVoiceResultEvent>(_voiceXsResult); 145 on<XSVoiceResultEvent>(_voiceXsResult);
  146 +
  147 + on<PlayRecordAudioEvent>(_playRecordAudio);
73 } 148 }
74 149
75 @override 150 @override
@@ -83,11 +158,11 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; { @@ -83,11 +158,11 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
83 void _pageControllerChange(CurrentPageIndexChangeEvent event, 158 void _pageControllerChange(CurrentPageIndexChangeEvent event,
84 Emitter<ReadingPageState> emitter) async { 159 Emitter<ReadingPageState> emitter) async {
85 _currentPage = event.pageIndex; 160 _currentPage = event.pageIndex;
86 - _playOriginVoice(null); 161 + _playOriginalAudioInner(null);
87 emitter(CurrentPageIndexState()); 162 emitter(CurrentPageIndexState());
88 } 163 }
89 164
90 - void _selectItemLoad( 165 + void _playModeChange(
91 CurrentModeChangeEvent event, Emitter<ReadingPageState> emitter) async { 166 CurrentModeChangeEvent event, Emitter<ReadingPageState> emitter) async {
92 if (_currentMode == ReadingModeType.auto) { 167 if (_currentMode == ReadingModeType.auto) {
93 _currentMode = ReadingModeType.manual; 168 _currentMode = ReadingModeType.manual;
@@ -102,7 +177,7 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; { @@ -102,7 +177,7 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
102 RequestDataEvent event, Emitter<ReadingPageState> emitter) async { 177 RequestDataEvent event, Emitter<ReadingPageState> emitter) async {
103 try { 178 try {
104 await loading(() async { 179 await loading(() async {
105 - _entity = await ListenDao.process('1'); 180 + _entity = await ListenDao.process(courseLessonId);
106 print("reading page entity: ${_entity!.toJson()}"); 181 print("reading page entity: ${_entity!.toJson()}");
107 emitter(RequestDataState()); 182 emitter(RequestDataState());
108 }); 183 });
@@ -113,20 +188,36 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; { @@ -113,20 +188,36 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
113 } 188 }
114 } 189 }
115 190
116 - void _playOriginalAudio(PlayOriginalAudioEvent event, Emitter<ReadingPageState> emitter) async {  
117 - print("_playOriginalAudio");  
118 - _playOriginVoice(event.url); 191 + /// 播放绘本原音
  192 + void _playOriginalAudio(
  193 + PlayOriginalAudioEvent event, Emitter<ReadingPageState> emitter) async {
  194 + _playOriginalAudioInner(event.url);
  195 + }
  196 +
  197 + void _playOriginalAudioInner(String? audioUrl) {
  198 + print("_playOriginalAudio url=$audioUrl");
  199 + audioUrl ??= currentPageData()?.audioUrl ?? '';
  200 + _playAudio(audioUrl);
  201 + _isOriginAudioPlaying = true;
119 } 202 }
120 203
121 - /// 播放绘本原音  
122 - void _playOriginVoice(String? audioUrl) async {  
123 - audioPlayer.stop();  
124 - final readingData = currentPageData();  
125 - if (readingData?.audioUrl != null) {  
126 - final urlStr = audioUrl ?? readingData?.audioUrl ?? '';  
127 - if (urlStr.isNotEmpty) {  
128 - audioPlayer.play(UrlSource(urlStr));  
129 - } 204 + /// 播放录音
  205 + void _playRecordAudio(
  206 + PlayRecordAudioEvent event, Emitter<ReadingPageState> emitter) async {
  207 + _playRecordAudioInner();
  208 + }
  209 +
  210 + void _playRecordAudioInner() {
  211 + final recordAudioUrl = currentPageData()?.recordUrl;
  212 + print("_playRecordAudioInner url=${currentPageData()?.recordUrl}");
  213 + _playAudio(recordAudioUrl);
  214 + _isRecordAudioPlaying = true;
  215 + }
  216 +
  217 + void _playAudio(String? audioUrl) async {
  218 + await audioPlayer.stop();
  219 + if (audioUrl!.isNotEmpty) {
  220 + await audioPlayer.play(UrlSource(audioUrl));
130 } 221 }
131 } 222 }
132 223
@@ -139,8 +230,16 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; { @@ -139,8 +230,16 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
139 return _entity?.readings?[_currentPage]; 230 return _entity?.readings?[_currentPage];
140 } 231 }
141 232
  233 + ///初始化SDK
  234 + _initVoiceSdk(
  235 + XSVoiceInitEvent event, Emitter<ReadingPageState> emitter) async {
  236 + methodChannel.invokeMethod('initVoiceSdk', event.data);
  237 + }
  238 +
142 ///先声测试 239 ///先声测试
143 - void _voiceXsTest(XSVoiceTestEvent event, Emitter<ReadingPageState> emitter) async { 240 + void _voiceXsStart(
  241 + XSVoiceStartEvent event, Emitter<ReadingPageState> emitter) async {
  242 + _stopAudio();
144 startRecord(event.content); 243 startRecord(event.content);
145 emitter(XSVoiceTestState()); 244 emitter(XSVoiceTestState());
146 } 245 }
@@ -149,24 +248,44 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; { @@ -149,24 +248,44 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
149 if (_isRecording == true) { 248 if (_isRecording == true) {
150 return; 249 return;
151 } 250 }
152 - EasyLoading.show(status: '录音中....');  
153 methodChannel.invokeMethod( 251 methodChannel.invokeMethod(
154 - 'startVoice',  
155 - {'word':'how old are you','type':'0','userId':'1'}  
156 - ); 252 + 'startVoice', {'word': 'how old are you', 'type': '0', 'userId': '1'});
157 _isRecording = true; 253 _isRecording = true;
158 } 254 }
159 255
160 - void _voiceXsResult(XSVoiceResultEvent event,Emitter<ReadingPageState> emitter) async {  
161 - final Map args = event.message as Map;  
162 - final result = args['result'] as String;  
163 - if (result == '1') {  
164 - final overall = args['overall'].toString();  
165 - EasyLoading.showToast('测评成功,分数是$overall',duration: const Duration(seconds: 10)); 256 + void _voiceXsResult(
  257 + XSVoiceResultEvent event, Emitter<ReadingPageState> emitter) async {
  258 + final Map args = json.decode(event.message);
  259 + final result = args['result'];
  260 + print("_voiceXsResult result=${result}");
  261 + if (result != null) {
  262 + final overall = result['overall'].toString();
  263 + EasyLoading.showToast('测评成功,分数是$overall',
  264 + duration: const Duration(seconds: 10));
  265 + currentPageData()?.recordScore = overall;
  266 + currentPageData()?.recordUrl = args['audioUrl'] + '.mp3';
  267 + _playRecordAudioInner();
166 } else { 268 } else {
167 - EasyLoading.showToast('测评失败',duration: const Duration(seconds: 10)); 269 + EasyLoading.showToast('测评失败', duration: const Duration(seconds: 10));
168 } 270 }
169 _isRecording = false; 271 _isRecording = false;
170 emitter(XSVoiceTestState()); 272 emitter(XSVoiceTestState());
171 } 273 }
  274 +
  275 + ///终止评测
  276 + void _voiceXsStop(
  277 + XSVoiceStopEvent event, Emitter<ReadingPageState> emitter) async {
  278 + methodChannel.invokeMethod('stopVoice');
  279 + }
  280 +
  281 + void _voicePlayStateChange(VoicePlayStateChangeEvent event,
  282 + Emitter<ReadingPageState> emitter) async {
  283 + emitter(VoicePlayStateChange());
  284 + }
  285 +
  286 + void _stopAudio() async {
  287 + await audioPlayer.stop();
  288 + _isOriginAudioPlaying = false;
  289 + _isRecordAudioPlaying = false;
  290 + }
172 } 291 }
lib/pages/reading/bloc/reading_event.dart
@@ -3,6 +3,9 @@ part of &#39;reading_bloc.dart&#39;; @@ -3,6 +3,9 @@ part of &#39;reading_bloc.dart&#39;;
3 @immutable 3 @immutable
4 abstract class ReadingPageEvent {} 4 abstract class ReadingPageEvent {}
5 5
  6 +///页面初始化
  7 +class InitBlocEvent extends ReadingPageEvent {}
  8 +
6 class CurrentPageIndexChangeEvent extends ReadingPageEvent { 9 class CurrentPageIndexChangeEvent extends ReadingPageEvent {
7 final int pageIndex; 10 final int pageIndex;
8 CurrentPageIndexChangeEvent(this.pageIndex); 11 CurrentPageIndexChangeEvent(this.pageIndex);
@@ -32,9 +35,18 @@ class XSVoiceResultEvent extends ReadingPageEvent { @@ -32,9 +35,18 @@ class XSVoiceResultEvent extends ReadingPageEvent {
32 } 35 }
33 36
34 ///先声测试 37 ///先声测试
35 -class XSVoiceTestEvent extends ReadingPageEvent { 38 +class XSVoiceStartEvent extends ReadingPageEvent {
36 final String content; 39 final String content;
37 final String type; 40 final String type;
38 final String userId; 41 final String userId;
39 - XSVoiceTestEvent(this.content,this.type,this.userId);  
40 -}  
41 \ No newline at end of file 42 \ No newline at end of file
  43 + XSVoiceStartEvent(this.content,this.type,this.userId);
  44 +}
  45 +
  46 +///先声评测停止
  47 +class XSVoiceStopEvent extends ReadingPageEvent {}
  48 +
  49 +///音频播放状态
  50 +class VoicePlayStateChangeEvent extends ReadingPageEvent {}
  51 +
  52 +///录音播放
  53 +class PlayRecordAudioEvent extends ReadingPageEvent {}
42 \ No newline at end of file 54 \ No newline at end of file
lib/pages/reading/bloc/reading_state.dart
@@ -13,3 +13,5 @@ class CurrentModeState extends ReadingPageState {} @@ -13,3 +13,5 @@ class CurrentModeState extends ReadingPageState {}
13 class RequestDataState extends ReadingPageState {} 13 class RequestDataState extends ReadingPageState {}
14 14
15 class XSVoiceTestState extends ReadingPageState {} 15 class XSVoiceTestState extends ReadingPageState {}
  16 +
  17 +class VoicePlayStateChange extends ReadingPageState {}
lib/pages/reading/reading_page.dart
@@ -9,12 +9,16 @@ import &#39;../../models/course_process_entity.dart&#39;; @@ -9,12 +9,16 @@ import &#39;../../models/course_process_entity.dart&#39;;
9 import 'bloc/reading_bloc.dart'; 9 import 'bloc/reading_bloc.dart';
10 10
11 class ReadingPage extends StatelessWidget { 11 class ReadingPage extends StatelessWidget {
12 - const ReadingPage({super.key}); 12 + const ReadingPage({super.key, this.courseLessonId});
  13 +
  14 + final String? courseLessonId;
13 15
14 @override 16 @override
15 Widget build(BuildContext context) { 17 Widget build(BuildContext context) {
16 return BlocProvider( 18 return BlocProvider(
17 - create: (_) => ReadingPageBloc(PageController())..add(RequestDataEvent()), 19 + create: (_) => ReadingPageBloc(PageController(), courseLessonId ?? '')
  20 + ..add(InitBlocEvent())
  21 + ..add(RequestDataEvent()),
18 child: _ReadingPage(), 22 child: _ReadingPage(),
19 ); 23 );
20 } 24 }
@@ -135,11 +139,13 @@ class _ReadingPage extends StatelessWidget { @@ -135,11 +139,13 @@ class _ReadingPage extends StatelessWidget {
135 if (bloc.isRecording) { 139 if (bloc.isRecording) {
136 return; 140 return;
137 } 141 }
138 - print("voice tap");  
139 bloc.add(PlayOriginalAudioEvent(null)); 142 bloc.add(PlayOriginalAudioEvent(null));
140 }, 143 },
141 child: Image.asset( 144 child: Image.asset(
142 - 'voice'.assetPng, 145 + bloc.voicePlayState == VoicePlayState.playing &&
  146 + bloc.isOriginAudioPlaying
  147 + ? 'reade_answer'.assetGif
  148 + : 'voice'.assetPng,
143 height: 40.h, 149 height: 40.h,
144 width: 45.w, 150 width: 45.w,
145 ), 151 ),
@@ -161,9 +167,13 @@ class _ReadingPage extends StatelessWidget { @@ -161,9 +167,13 @@ class _ReadingPage extends StatelessWidget {
161 GestureDetector( 167 GestureDetector(
162 onTap: () { 168 onTap: () {
163 if (bloc.isRecording) { 169 if (bloc.isRecording) {
164 - return; 170 + bloc.add(XSVoiceStopEvent());
  171 + } else {
  172 + bloc.add(XSVoiceStartEvent(
  173 + bloc.currentPageData()?.word ?? '',
  174 + '0',
  175 + UserUtil.getUser()!.id.toString()));
165 } 176 }
166 - bloc.add(XSVoiceTestEvent(bloc.currentPageData()?.word??'', '0',UserUtil.getUser()!.id.toString()));  
167 }, 177 },
168 child: Image.asset( 178 child: Image.asset(
169 'micro_phone'.assetPng, 179 'micro_phone'.assetPng,
@@ -173,18 +183,23 @@ class _ReadingPage extends StatelessWidget { @@ -173,18 +183,23 @@ class _ReadingPage extends StatelessWidget {
173 SizedBox( 183 SizedBox(
174 width: 10.w, 184 width: 10.w,
175 ), 185 ),
176 - Visibility(  
177 - visible: false,  
178 -  
179 - ///todo 依据是否录过音  
180 - child: Image.asset(  
181 - 'record_pause'.assetWebp,  
182 -  
183 - ///todo 根据播放状态切换图片  
184 - height: 33.h,  
185 - width: 33.w,  
186 - ),  
187 - ) 186 + GestureDetector(
  187 + onTap: () {
  188 + if (bloc.isRecording) {
  189 + return;
  190 + }
  191 + bloc.add(PlayRecordAudioEvent());
  192 + },
  193 + child: Visibility(
  194 + visible: bloc.currentPageData()?.recordUrl != null,
  195 + child: Image.asset(
  196 + bloc.isRecordAudioPlaying
  197 + ? 'record_pause'.assetWebp
  198 + : 'record_play'.assetWebp,
  199 + height: 33.h,
  200 + width: 33.w,
  201 + ),
  202 + )),
188 ], 203 ],
189 ), 204 ),
190 ), 205 ),
lib/route/route.dart
@@ -139,7 +139,11 @@ class AppRouter { @@ -139,7 +139,11 @@ class AppRouter {
139 pageBuilder: (_, __, ___) => const TabPage(), 139 pageBuilder: (_, __, ___) => const TabPage(),
140 transitionsBuilder: (_, __, ___, child) => child); 140 transitionsBuilder: (_, __, ___, child) => child);
141 case AppRouteName.reading: 141 case AppRouteName.reading:
142 - return CupertinoPageRoute(builder: (_) => const ReadingPage()); 142 + var courseLessonId = '';
  143 + if (settings.arguments != null) {
  144 + courseLessonId = (settings.arguments as Map)['courseLessonId'] as String??'';
  145 + }
  146 + return CupertinoPageRoute(builder: (_) => ReadingPage(courseLessonId: courseLessonId));
143 default: 147 default:
144 return CupertinoPageRoute( 148 return CupertinoPageRoute(
145 builder: (_) => Scaffold(body: Center(child: Text('No route defined for ${settings.name}')))); 149 builder: (_) => Scaffold(body: Center(child: Text('No route defined for ${settings.name}'))));