Просмотр исходного кода

完成录音克隆流程以及功能跳转

zk 2 лет назад
Родитель
Сommit
4532ac0e12
20 измененных файлов с 358 добавлено и 83 удалено
  1. 1 2
      app/src/main/java/com/atmob/voiceai/data/repositories/CloneRepository.java
  2. 10 1
      app/src/main/java/com/atmob/voiceai/data/repositories/VoiceAIRepository.java
  3. 0 2
      app/src/main/java/com/atmob/voiceai/dialog/CloneDeleteSureDialog.java
  4. 10 2
      app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneVoiceFragment.java
  5. 57 34
      app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneVoiceViewModel.java
  6. 13 6
      app/src/main/java/com/atmob/voiceai/module/cloning/VoiceCloningActivity.java
  7. 21 1
      app/src/main/java/com/atmob/voiceai/module/cloning/VoiceCloningViewModel.java
  8. 10 0
      app/src/main/java/com/atmob/voiceai/module/generating/VoiceGeneratingActivity.java
  9. 1 2
      app/src/main/java/com/atmob/voiceai/module/result/VoiceResultActivity.java
  10. 1 1
      app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIFragment.java
  11. 36 5
      app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIViewModel.java
  12. 2 2
      app/src/main/java/com/atmob/voiceai/utils/AudioRecorder.java
  13. 172 0
      app/src/main/java/com/atmob/voiceai/widget/AudioProgressBar.java
  14. 1 0
      app/src/main/java/com/atmob/voiceai/widget/AutoScrollTextView.java
  15. 0 15
      app/src/main/res/drawable/bg_progress_bar_horizontal_record_0_20.xml
  16. 1 1
      app/src/main/res/layout/activity_voice_clone.xml
  17. 8 9
      app/src/main/res/layout/fragment_clone_voice.xml
  18. 9 0
      app/src/main/res/values/attrs.xml
  19. 3 0
      app/src/main/res/values/colors.xml
  20. 2 0
      app/src/main/res/values/strings.xml

+ 1 - 2
app/src/main/java/com/atmob/voiceai/data/repositories/CloneRepository.java

@@ -106,11 +106,10 @@ public class CloneRepository {
         });
         });
     }
     }
 
 
-    public void requestVoiceClone(int type, @NonNull Uri uri) {
+    public void requestVoiceClone(int type, @NonNull File file) {
         if (requestCloneFlag) {
         if (requestCloneFlag) {
             return;
             return;
         }
         }
-        File file = FileUtils.uriToFileApiQ(uri, ContextUtil.getContext());
         voiceClone(type, file).subscribe(new SingleObserver<VoiceCloneResponse>() {
         voiceClone(type, file).subscribe(new SingleObserver<VoiceCloneResponse>() {
             @Override
             @Override
             public void onSubscribe(@NonNull Disposable d) {
             public void onSubscribe(@NonNull Disposable d) {

+ 10 - 1
app/src/main/java/com/atmob/voiceai/data/repositories/VoiceAIRepository.java

@@ -57,6 +57,7 @@ public class VoiceAIRepository {
     private final MutableLiveData<ThreePair<Integer, String, UserVoiceBean>> textToSpeechState = new MutableLiveData<>();
     private final MutableLiveData<ThreePair<Integer, String, UserVoiceBean>> textToSpeechState = new MutableLiveData<>();
 
 
     private final SingleLiveEvent<VoiceListBean> recommendClickBean = new SingleLiveEvent<>();
     private final SingleLiveEvent<VoiceListBean> recommendClickBean = new SingleLiveEvent<>();
+    private final SingleLiveEvent<Integer> useClonedVoiceEvent = new SingleLiveEvent<>();
 
 
     private boolean isRequestFreeGenerate;
     private boolean isRequestFreeGenerate;
 
 
@@ -77,7 +78,15 @@ public class VoiceAIRepository {
         refreshVoiceInfo();
         refreshVoiceInfo();
     }
     }
 
 
-    public SingleLiveEvent<?> getCloneExecuteSuccess() {
+    public void setUseClonedVoiceEvent(int id) {
+        useClonedVoiceEvent.setValue(id);
+    }
+
+    public LiveData<Integer> getUseClonedVoiceEvent() {
+        return useClonedVoiceEvent;
+    }
+
+    public LiveData<?> getCloneExecuteSuccess() {
         return cloneExecuteSuccess;
         return cloneExecuteSuccess;
     }
     }
 
 

+ 0 - 2
app/src/main/java/com/atmob/voiceai/dialog/CloneDeleteSureDialog.java

@@ -21,8 +21,6 @@ public class CloneDeleteSureDialog extends BaseDialog<DialogCloneDeleteSureBindi
     public CloneDeleteSureDialog(@NonNull Context context) {
     public CloneDeleteSureDialog(@NonNull Context context) {
         super(context, R.style.Theme_Common_Dialog);
         super(context, R.style.Theme_Common_Dialog);
         binding.setCancelClick(v -> {
         binding.setCancelClick(v -> {
-            if (onSureClickListener != null)
-                onSureClickListener.onSureClick();
             dismiss();
             dismiss();
         });
         });
         binding.setDeleteClick(v -> {
         binding.setDeleteClick(v -> {

+ 10 - 2
app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneVoiceFragment.java

@@ -24,15 +24,18 @@ import androidx.media3.exoplayer.ExoPlayer;
 
 
 import com.atmob.app.lib.base.BaseFragment;
 import com.atmob.app.lib.base.BaseFragment;
 import com.atmob.common.logging.AtmobLog;
 import com.atmob.common.logging.AtmobLog;
+import com.atmob.common.runtime.ActivityUtil;
 import com.atmob.common.ui.SizeUtil;
 import com.atmob.common.ui.SizeUtil;
 import com.atmob.voiceai.R;
 import com.atmob.voiceai.R;
 import com.atmob.voiceai.databinding.FragmentCloneVoiceBinding;
 import com.atmob.voiceai.databinding.FragmentCloneVoiceBinding;
 import com.atmob.voiceai.dialog.CloneDeleteSureDialog;
 import com.atmob.voiceai.dialog.CloneDeleteSureDialog;
 import com.atmob.voiceai.dialog.RecordingInstructionsDialog;
 import com.atmob.voiceai.dialog.RecordingInstructionsDialog;
+import com.atmob.voiceai.module.cloning.VoiceCloningActivity;
 import com.atmob.voiceai.utils.ActivityForResultUtil;
 import com.atmob.voiceai.utils.ActivityForResultUtil;
 import com.atmob.voiceai.utils.AudioRecorder;
 import com.atmob.voiceai.utils.AudioRecorder;
 import com.atmob.voiceai.utils.FileUtils;
 import com.atmob.voiceai.utils.FileUtils;
 import com.atmob.voiceai.utils.ToastUtil;
 import com.atmob.voiceai.utils.ToastUtil;
+import com.atmob.voiceai.widget.AudioProgressBar;
 import com.gyf.immersionbar.ImmersionBar;
 import com.gyf.immersionbar.ImmersionBar;
 
 
 import java.io.IOException;
 import java.io.IOException;
@@ -62,6 +65,12 @@ public class CloneVoiceFragment extends BaseFragment<FragmentCloneVoiceBinding>
         initScrollView();
         initScrollView();
         initSeekBar();
         initSeekBar();
         initPlay();
         initPlay();
+        binding.progressRecordBar.setOnProgressColorChangeListener(new AudioProgressBar.OnProgressColorChangeListener() {
+            @Override
+            public void onProgressColorChange(int color) {
+                binding.tvRecordStateDesc.setTextColor(color);
+            }
+        });
     }
     }
 
 
 
 
@@ -80,7 +89,7 @@ public class CloneVoiceFragment extends BaseFragment<FragmentCloneVoiceBinding>
         binding.voiceSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
         binding.voiceSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
             @Override
             @Override
             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
-
+                cloneVoiceViewModel.setCurrentDuration(progress);
             }
             }
 
 
             @Override
             @Override
@@ -190,7 +199,6 @@ public class CloneVoiceFragment extends BaseFragment<FragmentCloneVoiceBinding>
         });
         });
         cloneVoiceViewModel.getRefreshAudioCurrentProgress().observe(getViewLifecycleOwner(), o -> {
         cloneVoiceViewModel.getRefreshAudioCurrentProgress().observe(getViewLifecycleOwner(), o -> {
             long currentPosition = player.getCurrentPosition();
             long currentPosition = player.getCurrentPosition();
-            cloneVoiceViewModel.setCurrentDuration(currentPosition);
             binding.voiceSeekBar.setProgress((int) currentPosition);
             binding.voiceSeekBar.setProgress((int) currentPosition);
         });
         });
         cloneVoiceViewModel.getUploadVoiceUri().observe(getViewLifecycleOwner(), uri -> {
         cloneVoiceViewModel.getUploadVoiceUri().observe(getViewLifecycleOwner(), uri -> {

+ 57 - 34
app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneVoiceViewModel.java

@@ -1,12 +1,8 @@
 package com.atmob.voiceai.module.clonevoice;
 package com.atmob.voiceai.module.clonevoice;
 
 
 import android.Manifest;
 import android.Manifest;
-import android.annotation.SuppressLint;
-import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.net.Uri;
 import android.os.CountDownTimer;
 import android.os.CountDownTimer;
-import android.os.Handler;
-import android.os.Looper;
 
 
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MediatorLiveData;
 import androidx.lifecycle.MediatorLiveData;
@@ -16,17 +12,19 @@ import androidx.lifecycle.Transformations;
 import com.atmob.app.lib.base.BaseViewModel;
 import com.atmob.app.lib.base.BaseViewModel;
 import com.atmob.app.lib.livedata.SingleLiveEvent;
 import com.atmob.app.lib.livedata.SingleLiveEvent;
 import com.atmob.common.data.KVUtils;
 import com.atmob.common.data.KVUtils;
-import com.atmob.common.logging.AtmobLog;
 import com.atmob.common.runtime.ActivityUtil;
 import com.atmob.common.runtime.ActivityUtil;
 import com.atmob.common.runtime.ContextUtil;
 import com.atmob.common.runtime.ContextUtil;
 import com.atmob.voiceai.R;
 import com.atmob.voiceai.R;
 import com.atmob.voiceai.data.api.bean.CloneVoiceListBean;
 import com.atmob.voiceai.data.api.bean.CloneVoiceListBean;
 import com.atmob.voiceai.data.repositories.CloneRepository;
 import com.atmob.voiceai.data.repositories.CloneRepository;
 import com.atmob.voiceai.data.repositories.MemberRepository;
 import com.atmob.voiceai.data.repositories.MemberRepository;
+import com.atmob.voiceai.data.repositories.VoiceAIRepository;
 import com.atmob.voiceai.helper.ErrorHelper;
 import com.atmob.voiceai.helper.ErrorHelper;
 import com.atmob.voiceai.module.cloning.VoiceCloningActivity;
 import com.atmob.voiceai.module.cloning.VoiceCloningActivity;
+import com.atmob.voiceai.module.main.MainActivity;
 import com.atmob.voiceai.module.setting.SettingActivity;
 import com.atmob.voiceai.module.setting.SettingActivity;
 import com.atmob.voiceai.module.subscription.SubscriptionPageActivity;
 import com.atmob.voiceai.module.subscription.SubscriptionPageActivity;
+import com.atmob.voiceai.module.voiceai.VoiceAIFragment;
 import com.atmob.voiceai.utils.DateUtil;
 import com.atmob.voiceai.utils.DateUtil;
 import com.atmob.voiceai.utils.FileUtils;
 import com.atmob.voiceai.utils.FileUtils;
 import com.atmob.voiceai.utils.PermissionUtil;
 import com.atmob.voiceai.utils.PermissionUtil;
@@ -58,11 +56,11 @@ public class CloneVoiceViewModel extends BaseViewModel {
     public static final int FILE_UPLOAD = 1;
     public static final int FILE_UPLOAD = 1;
     public static final int RECORD_UPLOAD = 2;
     public static final int RECORD_UPLOAD = 2;
 
 
-    private final LiveData<String> recordStateDesc;
+    private final MediatorLiveData<String> recordStateDesc = new MediatorLiveData<>();
 
 
     private final LiveData<String> recordTimeTxt;
     private final LiveData<String> recordTimeTxt;
     private final MutableLiveData<Integer> recordProgress = new MutableLiveData<>();
     private final MutableLiveData<Integer> recordProgress = new MutableLiveData<>();
-    private final MutableLiveData<Drawable> recordProgressDrawable = new MutableLiveData<>();
+    private final MutableLiveData<Integer> recordProgressColor = new MutableLiveData<>();
     private final MutableLiveData<Boolean> showStopRecord = new MutableLiveData<>();
     private final MutableLiveData<Boolean> showStopRecord = new MutableLiveData<>();
     private final MutableLiveData<String> uploadPreviewName = new MutableLiveData<>();
     private final MutableLiveData<String> uploadPreviewName = new MutableLiveData<>();
     private final MutableLiveData<String> uploadPreviewSize = new MutableLiveData<>();
     private final MutableLiveData<String> uploadPreviewSize = new MutableLiveData<>();
@@ -84,6 +82,7 @@ public class CloneVoiceViewModel extends BaseViewModel {
     private final MutableLiveData<Uri> uploadVoiceUri = new MutableLiveData<>();
     private final MutableLiveData<Uri> uploadVoiceUri = new MutableLiveData<>();
     private final MemberRepository memberRepository;
     private final MemberRepository memberRepository;
     private final CloneRepository cloneRepository;
     private final CloneRepository cloneRepository;
+    private final VoiceAIRepository voiceAIRepository;
 
 
     private boolean isSeekbarChanging;
     private boolean isSeekbarChanging;
     private Timer timer;
     private Timer timer;
@@ -91,22 +90,42 @@ public class CloneVoiceViewModel extends BaseViewModel {
     File recordFile;
     File recordFile;
     private CountDownTimer countDownTimer;
     private CountDownTimer countDownTimer;
 
 
+    private int lastColor;
+
+    private int color_0_20 = ContextUtil.getContext().getColor(R.color.voice_record_0_20);
+    private int color_20_30 = ContextUtil.getContext().getColor(R.color.voice_record_20_30);
+    private int color_exceed_30 = ContextUtil.getContext().getColor(R.color.voice_record_exceed_30);
+    private String record_can_not = ContextUtil.getContext().getString(R.string.record_can_not);
+    private String record_maybe_best = ContextUtil.getContext().getString(R.string.record_maybe_best);
+    private String record_best = ContextUtil.getContext().getString(R.string.record_best);
+
     @Inject
     @Inject
-    public CloneVoiceViewModel(MemberRepository memberRepository, CloneRepository cloneRepository) {
+    public CloneVoiceViewModel(MemberRepository memberRepository, CloneRepository cloneRepository, VoiceAIRepository voiceAIRepository) {
         this.memberRepository = memberRepository;
         this.memberRepository = memberRepository;
         this.cloneRepository = cloneRepository;
         this.cloneRepository = cloneRepository;
+        this.voiceAIRepository = voiceAIRepository;
         initCloneTipTxt();
         initCloneTipTxt();
         initCloneState();
         initCloneState();
-        recordStateDesc = Transformations.map(recordProgress, progress -> {
+        recordStateDesc.addSource(recordProgress, progress -> {
             if (progress == null) {
             if (progress == null) {
-                return "";
+                return;
             }
             }
-            if (progress >= 0 && progress <= 20) {
-                return ContextUtil.getContext().getString(R.string.record_can_not);
-            } else if (progress > 20 && progress < 30) {
-                return ContextUtil.getContext().getString(R.string.record_maybe_best);
+            int newColor;
+            String newText;
+            if (progress >= 0 && progress <= 20000) {
+                newColor = color_0_20;
+                newText = record_can_not;
+            } else if (progress > 20000 && progress <= 30000) {
+                newColor = color_20_30;
+                newText = record_maybe_best;
             } else {
             } else {
-                return ContextUtil.getContext().getString(R.string.record_best);
+                newColor = color_exceed_30;
+                newText = record_best;
+            }
+            if (newColor != lastColor) {
+                lastColor = newColor;
+                recordStateDesc.setValue(newText);
+                recordProgressColor.setValue(newColor);
             }
             }
         });
         });
         recordTimeTxt = Transformations.map(recordProgress, progress -> {
         recordTimeTxt = Transformations.map(recordProgress, progress -> {
@@ -130,8 +149,8 @@ public class CloneVoiceViewModel extends BaseViewModel {
         return recordProgress;
         return recordProgress;
     }
     }
 
 
-    public LiveData<Drawable> getRecordProgressDrawable() {
-        return recordProgressDrawable;
+    public LiveData<Integer> getRecordProgressColor() {
+        return recordProgressColor;
     }
     }
 
 
     public long getRecordMaxTime() {
     public long getRecordMaxTime() {
@@ -270,13 +289,9 @@ public class CloneVoiceViewModel extends BaseViewModel {
     }
     }
 
 
 
 
-    public void requestCloneVoice(int type) {
-        Uri value = uploadVoiceUri.getValue();
-        if (value == null) {
-            return;
-        }
+    public void requestCloneVoice(int type, File file) {
+        cloneRepository.requestVoiceClone(type, file);
         VoiceCloningActivity.start(ActivityUtil.getTopActivity());
         VoiceCloningActivity.start(ActivityUtil.getTopActivity());
-        cloneRepository.requestVoiceClone(type, value);
     }
     }
 
 
     public void deleteClone(int id) {
     public void deleteClone(int id) {
@@ -314,10 +329,10 @@ public class CloneVoiceViewModel extends BaseViewModel {
         //判断是否符合要求
         //判断是否符合要求
         //文件小于3M  时长大于20s且小于30s
         //文件小于3M  时长大于20s且小于30s
         long durationLong = FileUtils.getAudioDurationFromUri(ContextUtil.getContext(), uri);
         long durationLong = FileUtils.getAudioDurationFromUri(ContextUtil.getContext(), uri);
-//        if (durationLong < 20000 || durationLong > 30000) {
-//            ToastUtil.show(R.string.clone_voice_duration_error, ToastUtil.LENGTH_SHORT);
-//            return;
-//        }
+        if (durationLong < 20000 || durationLong > 30000) {
+            ToastUtil.show(R.string.clone_voice_duration_error, ToastUtil.LENGTH_SHORT);
+            return;
+        }
         long fileSize = FileUtils.getFileSizeFromUri(ContextUtil.getContext(), uri);
         long fileSize = FileUtils.getFileSizeFromUri(ContextUtil.getContext(), uri);
         if (fileSize > 3 * 1024 * 1024) {
         if (fileSize > 3 * 1024 * 1024) {
             ToastUtil.show(R.string.clone_voice_size_error, ToastUtil.LENGTH_SHORT);
             ToastUtil.show(R.string.clone_voice_size_error, ToastUtil.LENGTH_SHORT);
@@ -339,16 +354,26 @@ public class CloneVoiceViewModel extends BaseViewModel {
     }
     }
 
 
     public void onCloneVoiceClick(int state) {
     public void onCloneVoiceClick(int state) {
-        if (state == CloneState.UPLOAD_CHOICE_LOCAL) {
-            requestCloneVoice(FILE_UPLOAD);
+        Uri value = uploadVoiceUri.getValue();
+        if (value == null) {
+            return;
+        }
+        File file = FileUtils.uriToFileApiQ(value, ContextUtil.getContext());
+        if (state == CloneState.UPLOAD_CHOICE_RECORDING) {
+            requestCloneVoice(RECORD_UPLOAD, file);
         } else {
         } else {
-            requestCloneVoice(RECORD_UPLOAD);
+            requestCloneVoice(FILE_UPLOAD, file);
         }
         }
     }
     }
 
 
 
 
     public void useCloneVoiceClick() {
     public void useCloneVoiceClick() {
-
+        MainActivity.start(ActivityUtil.getTopActivity(), VoiceAIFragment.class);
+        List<CloneVoiceListBean> list = cloneRepository.getCloneVoiceList().getValue();
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+        voiceAIRepository.setUseClonedVoiceEvent(list.get(0).getId());
     }
     }
 
 
     public void onDeleteClick() {
     public void onDeleteClick() {
@@ -371,9 +396,7 @@ public class CloneVoiceViewModel extends BaseViewModel {
     }
     }
 
 
 
 
-    @SuppressLint("UseCompatLoadingForDrawables")
     public void startRecordAudio() {
     public void startRecordAudio() {
-        recordProgressDrawable.setValue(ContextUtil.getContext().getDrawable(R.drawable.bg_progress_bar_horizontal_record_0_20));
         startRecordAudioEvent.call();
         startRecordAudioEvent.call();
         recordProgress.setValue(0);
         recordProgress.setValue(0);
         cloneState.setValue(CloneState.RECORDING_VOICE);
         cloneState.setValue(CloneState.RECORDING_VOICE);
@@ -415,7 +438,7 @@ public class CloneVoiceViewModel extends BaseViewModel {
 
 
     public String getRecordAudioFilePath() {
     public String getRecordAudioFilePath() {
         File cacheFile = FileUtils.getVoiceDownloadSaveRootFile();
         File cacheFile = FileUtils.getVoiceDownloadSaveRootFile();
-        recordFile = new File(cacheFile, "record_audio");
+        recordFile = new File(cacheFile, "record_audio.mp3");
         return recordFile.getPath();
         return recordFile.getPath();
     }
     }
 
 

+ 13 - 6
app/src/main/java/com/atmob/voiceai/module/cloning/VoiceCloningActivity.java

@@ -8,6 +8,7 @@ import android.os.Bundle;
 import androidx.annotation.Nullable;
 import androidx.annotation.Nullable;
 
 
 import com.atmob.app.lib.base.BaseActivity;
 import com.atmob.app.lib.base.BaseActivity;
+import com.atmob.voiceai.data.repositories.CloneRepository;
 import com.atmob.voiceai.databinding.ActivityVoiceCloneBinding;
 import com.atmob.voiceai.databinding.ActivityVoiceCloneBinding;
 import com.tencent.qgame.animplayer.util.ScaleType;
 import com.tencent.qgame.animplayer.util.ScaleType;
 
 
@@ -40,12 +41,7 @@ public class VoiceCloningActivity extends BaseActivity<ActivityVoiceCloneBinding
     }
     }
 
 
     private void initObserver() {
     private void initObserver() {
-        voiceCloningViewModel.getCloneUploadState().observe(this, state -> {
-            if (state == null) {
-                return;
-            }
-            finish();
-        });
+        voiceCloningViewModel.getFinishEvent().observe(this, o -> finish());
     }
     }
 
 
     private void startAnim() {
     private void startAnim() {
@@ -54,6 +50,17 @@ public class VoiceCloningActivity extends BaseActivity<ActivityVoiceCloneBinding
         binding.animView.startPlay(this.getAssets(), "anim/voice_ai_clone_loading.mp4");
         binding.animView.startPlay(this.getAssets(), "anim/voice_ai_clone_loading.mp4");
     }
     }
 
 
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        stopAnim();
+    }
+
+    private void stopAnim() {
+        binding.animView.stopPlay();
+    }
+
     @Override
     @Override
     protected void initViewModel() {
     protected void initViewModel() {
         super.initViewModel();
         super.initViewModel();

+ 21 - 1
app/src/main/java/com/atmob/voiceai/module/cloning/VoiceCloningViewModel.java

@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData;
 import androidx.lifecycle.Observer;
 import androidx.lifecycle.Observer;
 
 
 import com.atmob.app.lib.base.BaseViewModel;
 import com.atmob.app.lib.base.BaseViewModel;
+import com.atmob.app.lib.livedata.SingleLiveEvent;
 import com.atmob.voiceai.data.repositories.CloneRepository;
 import com.atmob.voiceai.data.repositories.CloneRepository;
 
 
 import javax.inject.Inject;
 import javax.inject.Inject;
@@ -14,17 +15,36 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
 @HiltViewModel
 @HiltViewModel
 public class VoiceCloningViewModel extends BaseViewModel {
 public class VoiceCloningViewModel extends BaseViewModel {
 
 
+    private final SingleLiveEvent<?> finishEvent = new SingleLiveEvent<>();
     private final CloneRepository cloneRepository;
     private final CloneRepository cloneRepository;
 
 
+    private final Observer<Integer> cloneUploadStateObserver;
 
 
     @Inject
     @Inject
     public VoiceCloningViewModel(CloneRepository cloneRepository) {
     public VoiceCloningViewModel(CloneRepository cloneRepository) {
         this.cloneRepository = cloneRepository;
         this.cloneRepository = cloneRepository;
+        cloneUploadStateObserver = state -> {
+            if (state == null) {
+                return;
+            }
+            if (state == CloneRepository.UploadState.UPLOAD_SUCCESS || state == CloneRepository.UploadState.ERROR) {
+                finishEvent.call();
+            }
+        };
+        cloneRepository.getCloneUploadState().observeForever(cloneUploadStateObserver);
+    }
+
+    public LiveData<?> getFinishEvent() {
+        return finishEvent;
     }
     }
 
 
     public LiveData<Integer> getCloneUploadState() {
     public LiveData<Integer> getCloneUploadState() {
         return cloneRepository.getCloneUploadState();
         return cloneRepository.getCloneUploadState();
     }
     }
 
 
-
+    @Override
+    protected void onCleared() {
+        super.onCleared();
+        cloneRepository.getCloneUploadState().removeObserver(cloneUploadStateObserver);
+    }
 }
 }

+ 10 - 0
app/src/main/java/com/atmob/voiceai/module/generating/VoiceGeneratingActivity.java

@@ -55,6 +55,16 @@ public class VoiceGeneratingActivity extends BaseActivity<ActivityVoiceGeneratin
         binding.animView.startPlay(this.getAssets(), "anim/voice_ai_clone_loading.mp4");
         binding.animView.startPlay(this.getAssets(), "anim/voice_ai_clone_loading.mp4");
     }
     }
 
 
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        stopAnim();
+    }
+
+    private void stopAnim() {
+        binding.animView.stopPlay();
+    }
+
     private void initView() {
     private void initView() {
 
 
     }
     }

+ 1 - 2
app/src/main/java/com/atmob/voiceai/module/result/VoiceResultActivity.java

@@ -96,7 +96,7 @@ public class VoiceResultActivity extends BaseActivity<ActivityVoiceResultBinding
         binding.voiceSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
         binding.voiceSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
             @Override
             @Override
             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
-
+                voiceResultViewModel.setCurrentDuration(progress);
             }
             }
 
 
             @Override
             @Override
@@ -194,7 +194,6 @@ public class VoiceResultActivity extends BaseActivity<ActivityVoiceResultBinding
         voiceResultViewModel.getVoiceList().observe(this, list -> voiceRecommendAdapter.submit(list));
         voiceResultViewModel.getVoiceList().observe(this, list -> voiceRecommendAdapter.submit(list));
         voiceResultViewModel.getRefreshAudioCurrentProgress().observe(this, o -> {
         voiceResultViewModel.getRefreshAudioCurrentProgress().observe(this, o -> {
             long currentPosition = player.getCurrentPosition();
             long currentPosition = player.getCurrentPosition();
-            voiceResultViewModel.setCurrentDuration(currentPosition);
             binding.voiceSeekBar.setProgress((int) currentPosition);
             binding.voiceSeekBar.setProgress((int) currentPosition);
         });
         });
         voiceResultViewModel.getResultBean().observe(this, bean -> {
         voiceResultViewModel.getResultBean().observe(this, bean -> {

+ 1 - 1
app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIFragment.java

@@ -71,7 +71,7 @@ public class VoiceAIFragment extends BaseFragment<FragmentVoiceAiBinding> {
         });
         });
         voiceViewModel.getVoiceDetailList().observe(getViewLifecycleOwner(), list -> voiceAIListAdapter.submit(list.first, () -> {
         voiceViewModel.getVoiceDetailList().observe(getViewLifecycleOwner(), list -> voiceAIListAdapter.submit(list.first, () -> {
                     if (list.second != null) {
                     if (list.second != null) {
-                        voiceViewModel.scrollToListPosition(list.second);
+                        voiceViewModel.scrollToListPosition(list.second.getId());
                     } else {
                     } else {
                         binding.ryVoiceView.scrollToPosition(0);
                         binding.ryVoiceView.scrollToPosition(0);
                     }
                     }

+ 36 - 5
app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIViewModel.java

@@ -72,6 +72,7 @@ public class VoiceAIViewModel extends BaseViewModel {
 
 
     Observer<VoiceListBean> recommendObserver;
     Observer<VoiceListBean> recommendObserver;
     Observer<Object> cloneObserver;
     Observer<Object> cloneObserver;
+    Observer<Integer> useCloneObserver;
 
 
     @Inject
     @Inject
     public VoiceAIViewModel(MemberRepository memberRepository, VoiceAIRepository voiceAIRepository) {
     public VoiceAIViewModel(MemberRepository memberRepository, VoiceAIRepository voiceAIRepository) {
@@ -200,7 +201,7 @@ public class VoiceAIViewModel extends BaseViewModel {
             }
             }
             if (Objects.equals(this.currentTypeId, String.valueOf(bean.getTypeId())) || Objects.equals(String.valueOf(typeList.get(0).getId()), this.currentTypeId)) {
             if (Objects.equals(this.currentTypeId, String.valueOf(bean.getTypeId())) || Objects.equals(String.valueOf(typeList.get(0).getId()), this.currentTypeId)) {
                 //在当前列表项或当前列表项为第一个(一般为ALL)
                 //在当前列表项或当前列表项为第一个(一般为ALL)
-                scrollToListPosition(bean);
+                scrollToListPosition(bean.getId());
             } else {
             } else {
                 //将列表项设置为第一个
                 //将列表项设置为第一个
                 scrollToTabPosition.setValue(0);
                 scrollToTabPosition.setValue(0);
@@ -212,12 +213,35 @@ public class VoiceAIViewModel extends BaseViewModel {
             voiceAIRepository.getCloneExecuteSuccess().removeObserver(cloneObserver);
             voiceAIRepository.getCloneExecuteSuccess().removeObserver(cloneObserver);
         }
         }
         cloneObserver = o -> {
         cloneObserver = o -> {
-            refreshVoiceList(currentTypeId);
+            //判断当前选中tab是否第一个
+            List<TypeListBean> typeList = voiceTypeList.getValue();
+            if (typeList == null || typeList.isEmpty()) {
+                return;
+            }
+            if (Objects.equals(String.valueOf(typeList.get(0).getId()), currentTypeId)) {
+                requestVoiceList(currentTypeId, null);
+            }
         };
         };
-        voiceAIRepository.getCloneExecuteSuccess().observeForever(cloneObserver);
+        if (useCloneObserver != null) {
+            voiceAIRepository.getUseClonedVoiceEvent().removeObserver(useCloneObserver);
+        }
+        useCloneObserver = id -> {
+            List<TypeListBean> typeList = voiceTypeList.getValue();
+            if (typeList == null || typeList.isEmpty()) {
+                return;
+            }
+            int typeId = typeList.get(0).getId();
+            if (Objects.equals(String.valueOf(typeId), currentTypeId)) {
+                scrollToListPosition(BoxingUtil.boxing(id));
+            } else {
+                scrollToTabPosition.setValue(0);
+                refreshVoiceList(String.valueOf(typeId), null);
+            }
+        };
+        voiceAIRepository.getUseClonedVoiceEvent().observeForever(useCloneObserver);
     }
     }
 
 
-    public void scrollToListPosition(@NonNull VoiceListBean bean) {
+    public void scrollToListPosition(int id) {
         Pair<List<VoiceListBean>, VoiceListBean> voiceDetail = voiceDetailList.getValue();
         Pair<List<VoiceListBean>, VoiceListBean> voiceDetail = voiceDetailList.getValue();
         if (voiceDetail == null || voiceDetail.first == null) {
         if (voiceDetail == null || voiceDetail.first == null) {
             return;
             return;
@@ -226,7 +250,7 @@ public class VoiceAIViewModel extends BaseViewModel {
             if (TextUtils.isEmpty(voiceListBean.getVoiceUrl())) {
             if (TextUtils.isEmpty(voiceListBean.getVoiceUrl())) {
                 continue;
                 continue;
             }
             }
-            if (voiceListBean.getId() == bean.getId()) {
+            if (voiceListBean.getId() == id) {
                 scrollToBean.setValue(voiceListBean);
                 scrollToBean.setValue(voiceListBean);
                 setChoiceVoiceBean(voiceListBean, true);
                 setChoiceVoiceBean(voiceListBean, true);
                 notifyBean.setValue(voiceListBean);
                 notifyBean.setValue(voiceListBean);
@@ -269,6 +293,10 @@ public class VoiceAIViewModel extends BaseViewModel {
             return;
             return;
         }
         }
         this.currentTypeId = id;
         this.currentTypeId = id;
+        requestVoiceList(currentTypeId, bean);
+    }
+
+    private void requestVoiceList(String id, VoiceListBean bean) {
         if (voiceListDisposable != null && !voiceListDisposable.isDisposed()) {
         if (voiceListDisposable != null && !voiceListDisposable.isDisposed()) {
             voiceListDisposable.dispose();
             voiceListDisposable.dispose();
         }
         }
@@ -366,6 +394,9 @@ public class VoiceAIViewModel extends BaseViewModel {
         if (cloneObserver != null) {
         if (cloneObserver != null) {
             voiceAIRepository.getCloneExecuteSuccess().removeObserver(cloneObserver);
             voiceAIRepository.getCloneExecuteSuccess().removeObserver(cloneObserver);
         }
         }
+        if (useCloneObserver != null) {
+            voiceAIRepository.getUseClonedVoiceEvent().removeObserver(useCloneObserver);
+        }
     }
     }
 
 
 
 

+ 2 - 2
app/src/main/java/com/atmob/voiceai/utils/AudioRecorder.java

@@ -15,8 +15,8 @@ public class AudioRecorder {
     public AudioRecorder() {
     public AudioRecorder() {
         mediaRecorder = new MediaRecorder();
         mediaRecorder = new MediaRecorder();
         mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
         mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
-        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
-        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
+        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
+        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
     }
     }
 
 
     public void setupMediaRecorder(String outputPath) {
     public void setupMediaRecorder(String outputPath) {

+ 172 - 0
app/src/main/java/com/atmob/voiceai/widget/AudioProgressBar.java

@@ -0,0 +1,172 @@
+package com.atmob.voiceai.widget;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.atmob.voiceai.R;
+
+public class AudioProgressBar extends View {
+
+
+    private Paint progressPaint;
+    private int progressColor = 0xFFFF5656;
+    private int lastProgressColor = 0xFFFF5656;
+    private RectF progressRect;
+    private int max = 1;
+    private int currentProgress = 0;
+    private int width;
+    private long changeColorTime = 1000;
+    ValueAnimator animator;
+    private int startColor;
+
+    private OnProgressColorChangeListener onProgressColorChangeListener;
+
+
+    public AudioProgressBar(Context context) {
+        this(context, null);
+    }
+
+    public AudioProgressBar(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AudioProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AudioProgressBar, defStyleAttr, 0);
+        startColor = typedArray.getColor(R.styleable.AudioProgressBar_startColor, 0xFFFF5656);
+        typedArray.recycle();
+        initProgress();
+    }
+
+    public void setOnProgressColorChangeListener(OnProgressColorChangeListener onProgressColorChangeListener) {
+        this.onProgressColorChangeListener = onProgressColorChangeListener;
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        width = w;
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (animator != null) {
+            animator.cancel();
+        }
+    }
+
+    private void initProgress() {
+        progressPaint = new Paint();
+        progressPaint.setColor(progressColor);
+        progressRect = new RectF();
+        animator = ValueAnimator.ofFloat(0f, 1f);
+        animator.setDuration(changeColorTime);
+    }
+
+    public void setMax(int max) {
+        this.max = max;
+        int currentRectWidth = getCurrentRectWidth();
+        progressRect.set(0, 0, currentRectWidth, getHeight());
+        invalidate();
+    }
+
+    private int getCurrentRectWidth() {
+        return width * currentProgress / max;
+    }
+
+    public void setProgress(int progress) {
+        if (currentProgress > max) {
+            currentProgress = max;
+        } else {
+            this.currentProgress = progress;
+        }
+        if (currentProgress == 0) {
+            animator.cancel();
+            progressColor = startColor;
+            progressPaint.setColor(progressColor);
+            if (onProgressColorChangeListener != null)
+                onProgressColorChangeListener.onProgressColorChange(progressColor);
+        }
+        int currentRectWidth = getCurrentRectWidth();
+        progressRect.set(0, 0, currentRectWidth, getHeight());
+        invalidate();
+    }
+
+
+    public void setProgressColor(int color) {
+        lastProgressColor = progressColor;
+        progressColor = color;
+
+        if (animator.isRunning()) {
+            animator.cancel();
+        }
+        animator.addUpdateListener(animation -> {
+            float fraction = (float) animation.getAnimatedValue();
+            int evaluate = evaluate(fraction, lastProgressColor, progressColor);
+            progressPaint.setColor(evaluate);
+            if (onProgressColorChangeListener != null)
+                onProgressColorChangeListener.onProgressColorChange(evaluate);
+            invalidate();
+        });
+        animator.start();
+        Log.d("AudioProgressBar", "start");
+    }
+
+
+    @Override
+    protected void onDraw(@NonNull Canvas canvas) {
+        super.onDraw(canvas);
+        canvas.drawRect(progressRect, progressPaint);
+    }
+
+
+    public int evaluate(float fraction, int startColor, int endColor) {
+        float startA = ((startColor >> 24) & 0xff) / 255.0f;
+        float startR = ((startColor >> 16) & 0xff) / 255.0f;
+        float startG = ((startColor >> 8) & 0xff) / 255.0f;
+        float startB = (startColor & 0xff) / 255.0f;
+
+        float endA = ((endColor >> 24) & 0xff) / 255.0f;
+        float endR = ((endColor >> 16) & 0xff) / 255.0f;
+        float endG = ((endColor >> 8) & 0xff) / 255.0f;
+        float endB = (endColor & 0xff) / 255.0f;
+
+        // convert from sRGB to linear
+        startR = (float) Math.pow(startR, 2.2);
+        startG = (float) Math.pow(startG, 2.2);
+        startB = (float) Math.pow(startB, 2.2);
+
+        endR = (float) Math.pow(endR, 2.2);
+        endG = (float) Math.pow(endG, 2.2);
+        endB = (float) Math.pow(endB, 2.2);
+
+        // compute the interpolated color in linear space
+        float a = startA + fraction * (endA - startA);
+        float r = startR + fraction * (endR - startR);
+        float g = startG + fraction * (endG - startG);
+        float b = startB + fraction * (endB - startB);
+
+        // convert back to sRGB in the [0..255] range
+        a = a * 255.0f;
+        r = (float) Math.pow(r, 1.0 / 2.2) * 255.0f;
+        g = (float) Math.pow(g, 1.0 / 2.2) * 255.0f;
+        b = (float) Math.pow(b, 1.0 / 2.2) * 255.0f;
+
+        return Math.round(a) << 24 | Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b);
+    }
+
+    public interface OnProgressColorChangeListener {
+        void onProgressColorChange(int color);
+    }
+}

+ 1 - 0
app/src/main/java/com/atmob/voiceai/widget/AutoScrollTextView.java

@@ -53,6 +53,7 @@ public class AutoScrollTextView extends androidx.appcompat.widget.AppCompatTextV
             scrollTo(0, scrollY);
             scrollTo(0, scrollY);
             if (scrollY >= getLineHeight() * getLineCount() - getHeight()) {
             if (scrollY >= getLineHeight() * getLineCount() - getHeight()) {
 //                scrollTo(0, 0);
 //                scrollTo(0, 0);
+                stopScroll();
             }
             }
             postDelayed(this::postScroll, 50);
             postDelayed(this::postScroll, 50);
         });
         });

+ 0 - 15
app/src/main/res/drawable/bg_progress_bar_horizontal_record_0_20.xml

@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:id="@android:id/background">
-        <shape>
-            <solid android:color="@color/transparent" />
-        </shape>
-    </item>
-    <item android:id="@android:id/progress">
-        <scale android:scaleWidth="100%">
-            <shape>
-                <solid android:color="#FF5656" />
-            </shape>
-        </scale>
-    </item>
-</layer-list>

+ 1 - 1
app/src/main/res/layout/activity_voice_clone.xml

@@ -117,7 +117,7 @@
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintTop_toBottomOf="@+id/anim_view"
             app:layout_constraintTop_toBottomOf="@+id/anim_view"
             app:layout_constraintVertical_bias="0.5274825174825175"
             app:layout_constraintVertical_bias="0.5274825174825175"
-            tools:text="Generating ~ 15 sec" />
+            android:text="@string/cloning_state" />
 
 
     </androidx.constraintlayout.widget.ConstraintLayout>
     </androidx.constraintlayout.widget.ConstraintLayout>
 </layout>
 </layout>

+ 8 - 9
app/src/main/res/layout/fragment_clone_voice.xml

@@ -253,7 +253,7 @@
                     android:layout_height="wrap_content"
                     android:layout_height="wrap_content"
                     android:paddingBottom="@dimen/clone_voice_padding"
                     android:paddingBottom="@dimen/clone_voice_padding"
                     app:layout_constraintTop_toBottomOf="@+id/space4"
                     app:layout_constraintTop_toBottomOf="@+id/space4"
-                    tools:visibility="visible">
+                    tools:visibility="gone">
 
 
                     <View
                     <View
                         android:id="@+id/v_clone_upload_preview"
                         android:id="@+id/v_clone_upload_preview"
@@ -523,7 +523,7 @@
                     android:layout_height="wrap_content"
                     android:layout_height="wrap_content"
                     android:paddingBottom="@dimen/clone_voice_padding"
                     android:paddingBottom="@dimen/clone_voice_padding"
                     app:layout_constraintTop_toBottomOf="@+id/space4"
                     app:layout_constraintTop_toBottomOf="@+id/space4"
-                    tools:visibility="gone">
+                    tools:visibility="visible">
 
 
                     <View
                     <View
                         android:id="@+id/v_record_audio_preview"
                         android:id="@+id/v_record_audio_preview"
@@ -681,21 +681,20 @@
                         android:layout_width="wrap_content"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content" />
                         android:layout_height="wrap_content" />
 
 
-                    <ProgressBar
+                    <com.atmob.voiceai.widget.AudioProgressBar
                         android:id="@+id/progress_record_bar"
                         android:id="@+id/progress_record_bar"
-                        style="?android:attr/progressBarStyleHorizontal"
                         android:layout_width="0dp"
                         android:layout_width="0dp"
+                        app:startColor="@color/voice_record_0_20"
                         android:layout_height="4dp"
                         android:layout_height="4dp"
-                        android:progress="@{cloneVoiceViewModel.recordProgress}"
+                        progressColor="@{cloneVoiceViewModel.recordProgressColor}"
+                        progress="@{cloneVoiceViewModel.recordProgress}"
                         android:layout_marginHorizontal="16dp"
                         android:layout_marginHorizontal="16dp"
                         android:background="#7C7D88"
                         android:background="#7C7D88"
-                        android:max="@{(int)cloneVoiceViewModel.recordMaxTime}"
-                        android:progressDrawable="@{cloneVoiceViewModel.recordProgressDrawable}"
+                        max="@{(int)cloneVoiceViewModel.recordMaxTime}"
                         tools:progressDrawable="@drawable/bg_progress_bar_horizontal_record_0_20"
                         tools:progressDrawable="@drawable/bg_progress_bar_horizontal_record_0_20"
                         app:layout_constraintEnd_toEndOf="@+id/v_record_audio_preview"
                         app:layout_constraintEnd_toEndOf="@+id/v_record_audio_preview"
                         app:layout_constraintStart_toStartOf="@+id/v_record_audio_preview"
                         app:layout_constraintStart_toStartOf="@+id/v_record_audio_preview"
-                        app:layout_constraintTop_toBottomOf="@+id/space7"
-                        tools:progress="60" />
+                        app:layout_constraintTop_toBottomOf="@+id/space7" />
 
 
                     <Space
                     <Space
                         android:id="@+id/space5"
                         android:id="@+id/space5"

+ 9 - 0
app/src/main/res/values/attrs.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <declare-styleable name="AudioProgressBar">
+        <attr name="startColor" format="color" />
+    </declare-styleable>
+
+
+</resources>

+ 3 - 0
app/src/main/res/values/colors.xml

@@ -5,6 +5,9 @@
     <color name="transparent">#00FFFFFF</color>
     <color name="transparent">#00FFFFFF</color>
     <color name="common_txt_color">#202020</color>
     <color name="common_txt_color">#202020</color>
     <color name="voice_free_crux_txt_color">#FFEF5F</color>
     <color name="voice_free_crux_txt_color">#FFEF5F</color>
+    <color name="voice_record_0_20">#FF5656</color>
+    <color name="voice_record_20_30">#FFBB56</color>
+    <color name="voice_record_exceed_30">#06D6A0</color>
 
 
 
 
 </resources>
 </resources>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -19,6 +19,7 @@
     <string name="voice_play_error">Play Error</string>
     <string name="voice_play_error">Play Error</string>
     <string name="voice_generating_hint">PLEASE\nDO NOT CLOSE\nTHE APPLICATION</string>
     <string name="voice_generating_hint">PLEASE\nDO NOT CLOSE\nTHE APPLICATION</string>
     <string name="voice_generating_hint2">WHILE GENERATING VOICE</string>
     <string name="voice_generating_hint2">WHILE GENERATING VOICE</string>
+    <string name="voice_cloning_hint2">WHILE CLONING VOICE</string>
     <string name="generating_time">Generating ~ %d sec</string>
     <string name="generating_time">Generating ~ %d sec</string>
     <string name="generating_time_countdown_over">It\'ll be ready soon</string>
     <string name="generating_time_countdown_over">It\'ll be ready soon</string>
     <string name="generate_error">Generation timed out, please try again</string>
     <string name="generate_error">Generation timed out, please try again</string>
@@ -73,4 +74,5 @@
     <string name="record_can_not">Can\'t clone yet, Keep speaking loud and clear</string>
     <string name="record_can_not">Can\'t clone yet, Keep speaking loud and clear</string>
     <string name="record_maybe_best">Can be cloned, but results may not be accurate</string>
     <string name="record_maybe_best">Can be cloned, but results may not be accurate</string>
     <string name="record_best">Can be cloned! he results are more accurate</string>
     <string name="record_best">Can be cloned! he results are more accurate</string>
+    <string name="cloning_state">Cloning</string>
 </resources>
 </resources>