Browse Source

调整作品合成播放器,使用新版合成播放器去处理

Pq 9 months ago
parent
commit
19a491fcd3
24 changed files with 3762 additions and 106 deletions
  1. 12 0
      BaseLibrary/src/main/java/com/cooleshow/base/utils/TimeUtils.java
  2. 8 7
      accompany/src/main/java/com/daya/orchestra/accompany/web/AccompanyActivity.java
  3. 3 2
      accompany/src/main/java/com/daya/orchestra/accompany/web/AccompanyFragment.java
  4. 66 1
      ffmpegCmd/src/main/java/com/cooleshow/ffmpegcmd/util/FFmpegUtil.java
  5. 1 1
      musicMerge/src/main/AndroidManifest.xml
  6. 12 0
      musicMerge/src/main/java/com/cooleshow/musicmerge/bean/MusicMergeConfigBean.java
  7. 11 0
      musicMerge/src/main/java/com/cooleshow/musicmerge/constants/MergeConfig.java
  8. 1 0
      musicMerge/src/main/java/com/cooleshow/musicmerge/constants/MusicMergeConfig.java
  9. 311 24
      musicMerge/src/main/java/com/cooleshow/musicmerge/helper/MixHelper.java
  10. 67 0
      musicMerge/src/main/java/com/cooleshow/musicmerge/player/AddAudioMixer.java
  11. 58 0
      musicMerge/src/main/java/com/cooleshow/musicmerge/player/AverageAudioMixer.java
  12. 30 0
      musicMerge/src/main/java/com/cooleshow/musicmerge/player/ComposeInfo.java
  13. 51 34
      musicMerge/src/main/java/com/cooleshow/musicmerge/player/CustomPlayer.java
  14. 666 0
      musicMerge/src/main/java/com/cooleshow/musicmerge/player/MergeTrackManager.java
  15. 458 0
      musicMerge/src/main/java/com/cooleshow/musicmerge/player/MultiAudioMixer.java
  16. 72 0
      musicMerge/src/main/java/com/cooleshow/musicmerge/player/WeightAudioMixer.java
  17. 7 3
      musicMerge/src/main/java/com/cooleshow/musicmerge/ui/MusicHandleActivity.java
  18. 1674 0
      musicMerge/src/main/java/com/cooleshow/musicmerge/ui/MusicHandleActivity_.java
  19. 36 27
      musicMerge/src/main/java/com/cooleshow/musicmerge/ui/MusicHandleSettingFragment.java
  20. 187 0
      musicMerge/src/main/java/com/cooleshow/musicmerge/utils/AudioConverter.java
  21. 2 2
      musicMerge/src/main/java/com/cooleshow/musicmerge/widget/MusicFrequencyView.java
  22. 27 5
      musicMerge/src/main/res/layout/fg_music_handle_setting_layout.xml
  23. 1 0
      student/src/main/AndroidManifest.xml
  24. 1 0
      teacher/src/main/AndroidManifest.xml

+ 12 - 0
BaseLibrary/src/main/java/com/cooleshow/base/utils/TimeUtils.java

@@ -1674,6 +1674,18 @@ public final class TimeUtils {
         return secondToTime(ms / 1000);
     }
 
+    public static String msToTime(long ms) {
+        return secondToTime(ms / 1000);
+    }
+
+    public static String secondToTime(long second) {
+//        int hour = second / 3600; // 得到分钟数
+//        second = second % 3600;//剩余的秒数
+        long minute = second / 60;//得到分
+        second = second % 60;//剩余的秒
+        return String.format(Locale.getDefault(), "%02d:%02d", minute, second);
+    }
+
 
     public static boolean isLessThanTargetTime(String targetMinutes, String startTime) {
         try {

+ 8 - 7
accompany/src/main/java/com/daya/orchestra/accompany/web/AccompanyActivity.java

@@ -170,7 +170,7 @@ public class AccompanyActivity extends BaseActivity<ActivityAccompanyBinding> {
                             } else if (TextUtils.equals(cameraKitEvent.getType(), CameraKitEvent.TYPE_START_RECORD_EVENT)) {
                                 if (cameraKitEvent instanceof CameraStartRecord) {
                                     CameraStartRecord recordEvent = (CameraStartRecord) cameraKitEvent;
-                                    startRecordRealTime = recordEvent.getTime() + 500;//真正录制时间还要更大一点,无法确定准确值,增加300
+                                    startRecordRealTime = recordEvent.getTime() + 300;//真正录制时间还要更大一点,无法确定准确值,增加300
                                 }
                             }
                         }
@@ -285,8 +285,8 @@ public class AccompanyActivity extends BaseActivity<ActivityAccompanyBinding> {
             }
 
             @Override
-            public void openAdjustRecording(String recordId, String title, String coverImg) {
-                goAdjustMusic(recordId, title, coverImg);
+            public void openAdjustRecording(String recordId, String title, String coverImg,float speed) {
+                goAdjustMusic(recordId, title, coverImg,speed);
             }
 
             /**
@@ -366,22 +366,22 @@ public class AccompanyActivity extends BaseActivity<ActivityAccompanyBinding> {
 
     private int getVideoDelay() {
         if (startRecordRealTime != -1 && AccompanyPlayHelper.realPlayStartTime != -1) {
-            int time = (int) (startRecordRealTime - AccompanyPlayHelper.realPlayStartTime);
+            int time = (int) (AccompanyPlayHelper.realPlayStartTime - startRecordRealTime);
             LOG.i("pq", "视频录制消耗:" + time);
             return time;
         }
         return 0;
     }
 
-    private void goAdjustMusic(String recordId, String title, String coverImg) {
+    private void goAdjustMusic(String recordId, String title, String coverImg,float speed) {
         if (TextUtils.isEmpty(accompanimentUrl)) {
             ToastUtil.getInstance().showShort("无法找到伴奏文件");
             return;
         }
         //此字段用于评测播放缓冲延迟以及设备延迟总和
         int customCacheForInt = UserHelper.getCustomCacheForInt(AccompanyPlayHelper.DELAY_FOR_CURRENT_CACHE_KEY, 0);
-        customCacheForInt = -customCacheForInt;//这里要取反是因为录制在前 播放在后
-        int evaluateDelay =customCacheForInt;
+//        customCacheForInt = -customCacheForInt;//这里要取反是因为录制在前 播放在后
+        int evaluateDelay = customCacheForInt;
         String recordFilePath = MyFileUtils.getRecordFilePath();
         if (!TextUtils.isEmpty(filePath)) {
             recordFilePath = filePath;
@@ -397,6 +397,7 @@ public class AccompanyActivity extends BaseActivity<ActivityAccompanyBinding> {
                 .withInt("defaultDelay", customCacheForInt)
                 .withInt("evaluateDelay", evaluateDelay)//此字段给IOS使用
                 .withInt("c_orientation", ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
+                .withFloat("speedRate", speed)
                 .navigation(this, SAVE_PRACTICE_REQUEST_CODE);
     }
 

+ 3 - 2
accompany/src/main/java/com/daya/orchestra/accompany/web/AccompanyFragment.java

@@ -1471,7 +1471,7 @@ public class AccompanyFragment extends BaseMVPFragment<FragmentAccompanyV2Bindin
          */
         void onDownloadAccompaniment(String url);
 
-        void openAdjustRecording(String recordId, String title, String coverImg);
+        void openAdjustRecording(String recordId, String title, String coverImg,float speed);
     }
 
     public WebViewListener listener;
@@ -2147,8 +2147,9 @@ public class AccompanyFragment extends BaseMVPFragment<FragmentAccompanyV2Bindin
                 String recordId = contentJson.optString("recordId");
                 String title = contentJson.optString("title");
                 String coverImg = contentJson.optString("coverImg");
+                float speedRate = (float) contentJson.optDouble("speedRate", com.daya.orchestra.accompany.common.Constants.DEFAULT_PLAY_SPEED);
                 if (onAccompanyListener != null) {
-                    onAccompanyListener.openAdjustRecording(recordId, title, coverImg);
+                    onAccompanyListener.openAdjustRecording(recordId, title, coverImg, speedRate);
                 }
             }
         }

+ 66 - 1
ffmpegCmd/src/main/java/com/cooleshow/ffmpegcmd/util/FFmpegUtil.java

@@ -298,6 +298,26 @@ public class FFmpegUtil {
         return mediaMuxCmd.split(" ");
     }
 
+    /**
+     * mux audio and video together
+     *
+     * @param videoFile the file of pure video
+     * @param audioFile the file of pure audio
+     * @param copy      copy codec
+     * @param muxFile   output file
+     * @return mux success or not
+     */
+    public static String[] mediaMux2(String videoFile, String audioFile, boolean copy, String muxFile, float videoDelay) {
+        String mediaMuxCmd;
+        if (copy) {
+            mediaMuxCmd = "ffmpeg -itsoffset %f -i %s -itsoffset 0 -i %s -codec copy -y %s";
+        } else {
+            mediaMuxCmd = "ffmpeg -itsoffset %f -i %s -itsoffset 0 -i %s -y %s";
+        }
+        mediaMuxCmd = String.format(Locale.getDefault(), mediaMuxCmd, videoDelay, videoFile, audioFile, muxFile);
+        return mediaMuxCmd.split(" ");
+    }
+
     public static String[] getVideoFrame(String input, String output) {
 //        String mediaMuxCmd = "ffmpeg -i %s -vf fps=1/1 %simg%03d.jpg";
         String mediaMuxCmd = "ffmpeg -i %s -vf fps=1/1 %s";
@@ -815,11 +835,56 @@ public class FFmpegUtil {
         if (speed > 100 || speed < 0.5) {
             throw new IllegalArgumentException("audio speed range is from 0.5 to 100");
         }
-        String speedCmd = "ffmpeg -i -filter_complex atempo=%.2f";
+        String speedCmd = "ffmpeg -i -filter_complex atempo=%.2f -c:a copy -b:a 192k";
         speedCmd = String.format(Locale.getDefault(), speedCmd, speed);
         return insert(speedCmd.split(" "), 2, inputPath, outputPath);
     }
 
+    public static String[] changeAudioSpeed2(String inputPath, String outputPath, float speed) {
+        // atempo range [0.5, 100.0]
+        if (speed > 100) {
+            throw new IllegalArgumentException("audio speed range is from 0.5 to 100");
+        }
+        if (speed < 0.5) {
+            String s = buildAtempoFilters(speed);
+            String command = "ffmpeg -i -filter:a %s -b:a 192k";
+            String format = String.format(Locale.getDefault(), command, s);
+            return insert(format.split(" "), 2, inputPath, outputPath);
+        }else{
+            String speedCmd = "ffmpeg -i -filter:a atempo=%.2f -b:a 192k";
+            speedCmd = String.format(Locale.getDefault(), speedCmd, speed);
+            return insert(speedCmd.split(" "), 2, inputPath, outputPath);
+        }
+    }
+
+    public static String[] changeAudioSpeed3(String inputPath, String outputPath, float speed) {
+        // atempo range [0.5, 100.0]
+        if (speed < 0.5) {
+            String s = buildAtempoFilters(speed);
+            String speedCmd = "ffmpeg -i -filter_complex [0:a]%s[a] -map [a]";
+            String format = String.format(Locale.getDefault(), speedCmd, s);
+            return insert(format.split(" "), 2, inputPath, outputPath);
+        }else{
+            String speedCmd = "ffmpeg -i -filter_complex [0:a]atempo=%.2f[a] -map [a]";
+            speedCmd = String.format(Locale.getDefault(), speedCmd, speed);
+            return insert(speedCmd.split(" "), 2, inputPath, outputPath);
+        }
+    }
+
+    private static String buildAtempoFilters(double speed) {
+        StringBuilder filters = new StringBuilder();
+        double tempSpeed = speed;
+
+        // 分解速度调整,FFmpeg的atempo值只能在0.5到2.0之间
+        while (tempSpeed < 0.5) {
+            filters.append("atempo=0.5,");
+            tempSpeed *= 2;
+        }
+        filters.append(String.format("atempo=%.2f", tempSpeed));
+        return filters.toString();
+    }
+
+
     /**
      * Insert the picture into the header of video, which as a thumbnail
      *

+ 1 - 1
musicMerge/src/main/AndroidManifest.xml

@@ -2,7 +2,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
     <application>
         <activity
-            android:name=".ui.MusicHandleActivity"
+            android:name=".ui.MusicHandleActivity_"
             android:configChanges="orientation|screenSize|keyboardHidden|fontScale|smallestScreenSize|screenLayout"
             android:screenOrientation="landscape"
             android:exported="false">

+ 12 - 0
musicMerge/src/main/java/com/cooleshow/musicmerge/bean/MusicMergeConfigBean.java

@@ -9,6 +9,18 @@ public class MusicMergeConfigBean {
     private int accompanyVolume;
     private int defaultDelay;
     private int evaluateDelay;
+    private float speedRate;
+
+    public float getSpeedRate() {
+        if (speedRate != 0) {
+            return speedRate;
+        }
+        return 1.0f;
+    }
+
+    public void setSpeedRate(float speedRate) {
+        this.speedRate = speedRate;
+    }
 
     public int getEvaluateDelay() {
         return evaluateDelay;

+ 11 - 0
musicMerge/src/main/java/com/cooleshow/musicmerge/constants/MergeConfig.java

@@ -0,0 +1,11 @@
+package com.cooleshow.musicmerge.constants;
+
+/**
+ * Author by pq, Date on 2024/6/6.
+ */
+public class MergeConfig {
+    public final static float MAX_PLAY_SPEED = 5.0f;
+    public final static float MIN_PLAY_SPEED = 0.3f;
+    public final static float DEFAULT_PLAY_SPEED = 1.0f;
+
+}

+ 1 - 0
musicMerge/src/main/java/com/cooleshow/musicmerge/constants/MusicMergeConfig.java

@@ -12,6 +12,7 @@ public class MusicMergeConfig {
     public static final String ACCOMPANYVOLUME_KEY="accompanyVolume";
     public static final String DEFAULTDELAY_KEY = "defaultDelay";
     public static final String EVALUATEDELAY_KEY = "evaluateDelay";
+    public static final String SPEEDRATE_KEY = "speedRate";
 
     public static final String DELAY_FOR_CURRENT_CACHE_KEY = "delayForCurrent";
     public static final int MAX_THUMBNAIL_COUNT = 10;

+ 311 - 24
musicMerge/src/main/java/com/cooleshow/musicmerge/helper/MixHelper.java

@@ -8,6 +8,7 @@ import com.cooleshow.base.utils.EncryptUtils;
 import com.cooleshow.base.utils.FileUtils;
 import com.cooleshow.base.utils.LOG;
 import com.cooleshow.base.utils.LogUtils;
+import com.cooleshow.base.utils.MyFileUtils;
 import com.cooleshow.base.utils.Utils;
 import com.cooleshow.ffmpegcmd.FFmpegCmd;
 import com.cooleshow.ffmpegcmd.listener.OnHandleListener;
@@ -39,6 +40,7 @@ public class MixHelper {
     public static final int min_offset_value = -300;
     private int currentOffsetValue = 0;
     public static final String TAG = "MixHelper";
+    //    public static final String SPEED_TAG = "_speed";
     public static final String BASE_PATH = FileUtils.getCacheDir(Utils.getApp(), "musicmerge");
 
     public static final String videoMp3Path = BASE_PATH + File.separator + "videoBgm.wav";
@@ -52,9 +54,10 @@ public class MixHelper {
     public static final String recordFileMp3Path = BASE_PATH + File.separator + "record_file.mp3";
 
     public static final String onlyVideoPath = BASE_PATH + File.separator + "onlyVideoPath.mp4";
+    public static final String handleVideoPath = BASE_PATH + File.separator + "handleVideoPath.mp4";
 
     public static String getVoicePath() {
-        return FileUtils.getCacheDir(Utils.getApp()) + File.separator + "wav-accompany" + ".wav";
+        return MyFileUtils.getRecordFilePath();
     }
 
     private MixHelper() {
@@ -125,6 +128,13 @@ public class MixHelper {
         return path;
     }
 
+    public String getDownloadSavePathForTag(String url, String tag, String fileEndSuffix) {
+        String fileName = EncryptUtils.encryptMD5ToString(url);
+        String path = BASE_PATH + File.separator + fileName + "_" + tag + fileEndSuffix;
+        LOG.i(TAG, "getDownloadSavePathForTag:" + path);
+        return path;
+    }
+
     public String getDownloadSavePathForMp4(String url) {
         String fileName = EncryptUtils.encryptMD5ToString(url);
         String path = BASE_PATH + File.separator + fileName + ".mp4";
@@ -137,6 +147,11 @@ public class MixHelper {
         return BASE_PATH + File.separator + fileName + "_temp" + fileEndSuffix;
     }
 
+    public String getDownloadChangeSpeedPath(String url, String speedTag, String fileEndSuffix) {
+        String fileName = EncryptUtils.encryptMD5ToString(url);
+        return BASE_PATH + File.separator + fileName + "_" + speedTag + fileEndSuffix;
+    }
+
     public void download(String url, String fileEndSuffix, ResultCallback<String> resultCallback) {
         Observable.create(new ObservableOnSubscribe<String>() {
                     @Override
@@ -180,6 +195,173 @@ public class MixHelper {
                 });
     }
 
+
+    public void downloadOriginalFile(String url, String fileEndSuffix, ResultCallback<String> resultCallback) {
+        Observable.create(new ObservableOnSubscribe<String>() {
+                    @Override
+                    public void subscribe(@NonNull ObservableEmitter<String> emitter) throws Throwable {
+                        deleteAllTempFile();
+                        String downloadSavePath = getDownloadSavePath(url, fileEndSuffix);
+                        File sourcefile = new File(downloadSavePath);
+                        if (!sourcefile.exists()) {
+                            String downloadSaveTempPath = getDownloadSaveTempPath(url, fileEndSuffix);
+                            FileUtils.createOrExistsFile(downloadSaveTempPath);
+                            File file = new File(downloadSaveTempPath);
+                            downloadAccompany(url, file, resultCallback);
+                        }
+                        String filePath = getDownloadSavePath(url, fileEndSuffix);
+                        if (MyFileUtils.isVideo(filePath)) {
+                            //截取视频中的音频
+                            String wavFileSavePath = getWavFileSavePath(url, MyFileUtils.WAV_FILE_SUFFIX);
+                            getVideoMp3_2(filePath, wavFileSavePath);
+                        }
+                        if (emitter != null) {
+                            emitter.onNext(filePath);
+                        }
+                    }
+                }).subscribeOn(Schedulers.newThread())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(new Observer<String>() {
+                    @Override
+                    public void onSubscribe(@NonNull Disposable d) {
+
+                    }
+
+                    @Override
+                    public void onNext(@NonNull String s) {
+                        if (resultCallback != null) {
+                            resultCallback.onSuccess(s);
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull Throwable e) {
+                        e.printStackTrace();
+                        if (resultCallback != null) {
+                            resultCallback.onFail(-1, "download error");
+                        }
+                    }
+
+                    @Override
+                    public void onComplete() {
+
+                    }
+                });
+    }
+
+    public String getWavFileSavePath(String url, String wavFileSuffix) {
+        String fileName = EncryptUtils.encryptMD5ToString(url);
+        String path = BASE_PATH + File.separator + fileName + "_vb" + wavFileSuffix;
+        LOG.i(TAG, "getWavFileSavePath:" + path);
+        return path;
+    }
+
+
+    public void downloadAccompany(String url, float speed, String fileEndSuffix, ResultCallback<String> resultCallback) {
+        Observable.create(new ObservableOnSubscribe<String>() {
+                    @Override
+                    public void subscribe(@NonNull ObservableEmitter<String> emitter) throws Throwable {
+                        deleteAllTempFile();
+                        String downloadSavePath = getDownloadSavePath(url, fileEndSuffix);
+                        File sourcefile = new File(downloadSavePath);
+                        if (!sourcefile.exists()) {
+                            String downloadSaveTempPath = getDownloadSaveTempPath(url, fileEndSuffix);
+                            File file = new File(downloadSaveTempPath);
+                            FileUtils.createOrExistsFile(downloadSaveTempPath);
+                            //下载
+                            downloadAccompany(url, file, resultCallback);
+                        }
+                        resultCallback.onProgress(100);
+                        //变速
+                        String downloadChangeSpeedTempPath = getDownloadChangeSpeedPath(url, String.valueOf(speed), fileEndSuffix);
+                        String s = checkChangeSpeedIfNeed(downloadSavePath, downloadChangeSpeedTempPath, speed);
+                        if (emitter != null) {
+                            emitter.onNext(s);
+                        }
+                    }
+                }).subscribeOn(Schedulers.newThread())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(new Observer<String>() {
+                    @Override
+                    public void onSubscribe(@NonNull Disposable d) {
+
+                    }
+
+                    @Override
+                    public void onNext(@NonNull String s) {
+                        if (resultCallback != null) {
+                            resultCallback.onSuccess(s);
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull Throwable e) {
+                        e.printStackTrace();
+                        if (resultCallback != null) {
+                            resultCallback.onFail(-1, "download error");
+                        }
+                    }
+
+                    @Override
+                    public void onComplete() {
+
+                    }
+                });
+    }
+
+    public void getWavFromMp4(String inputPath, String outPath, ResultCallback<String> resultCallback) {
+        Observable.create(new ObservableOnSubscribe<String>() {
+                    @Override
+                    public void subscribe(@NonNull ObservableEmitter<String> emitter) throws Throwable {
+                        FileUtils.delete(outPath);
+                        int result = getVideoMp3_2(inputPath, outPath);
+                        if (emitter != null) {
+                            emitter.onNext(result != 0 ? "" : outPath);
+                        }
+                    }
+                }).subscribeOn(Schedulers.newThread())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(new Observer<String>() {
+                    @Override
+                    public void onSubscribe(@NonNull Disposable d) {
+
+                    }
+
+                    @Override
+                    public void onNext(@NonNull String s) {
+                        if (resultCallback != null) {
+                            resultCallback.onSuccess(s);
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull Throwable e) {
+                        e.printStackTrace();
+                        if (resultCallback != null) {
+                            resultCallback.onFail(-1, "download error");
+                        }
+                    }
+
+                    @Override
+                    public void onComplete() {
+
+                    }
+                });
+    }
+
+    private String checkChangeSpeedIfNeed(String inputPath, String outPath, float speed) {
+        FileUtils.delete(outPath);
+        String[] commands = FFmpegUtil.changeAudioSpeed2(inputPath, outPath, speed);
+        if (commands != null) {
+            println(commands);
+            int i = FFmpegCmd.executeSync(commands);
+            if (i == 0) {
+                return outPath;
+            }
+        }
+        return inputPath;
+    }
+
     private void downloadAccompany(String url, File file, ResultCallback<String> resultCallback) {
         if (TextUtils.isEmpty(url)) {
             return;
@@ -232,6 +414,7 @@ public class MixHelper {
         FileUtils.delete(videoMp3Path);
         FileUtils.delete(onlyVideoPath);
         FileUtils.delete(accompanimentWAVPath);
+        FileUtils.delete(handleVideoPath);
 //        boolean b = FileUtils.deleteAllInDir(BASE_PATH);
 //        LogUtils.i(TAG, "deleteAllTempFile:" + b);
     }
@@ -302,6 +485,15 @@ public class MixHelper {
         return -1;
     }
 
+    private int cutVideo(String audioPath, float startTime, float duration, String outPath) {
+        String[] commands = FFmpegUtil.cutVideo(audioPath, startTime, duration, outPath);
+        if (commands != null) {
+            println(commands);
+            return FFmpegCmd.executeSync(commands);
+        }
+        return -1;
+    }
+
     public int getVideoMp3_2(String videoPath, String outPath) {
         //ffmpeg -i video.mp4 -vn audio.mp3
         Log.i(TAG, "getVideoMp3 start videoPath:" + videoPath + "\noutPath:" + outPath);
@@ -329,6 +521,17 @@ public class MixHelper {
     private int megreMp3AndMp4(String videoPath, String mp3Path, String outPath) {
         Log.i(TAG, "mergeMp4 start");
         String[] commands = FFmpegUtil.mediaMux(videoPath, mp3Path, true, outPath);
+        println(commands);
+        if (commands != null) {
+            return FFmpegCmd.executeSync(commands);
+        }
+        return -1;
+    }
+
+    private int megreMp3AndMp4V2(String videoPath, String mp3Path, String outPath, float videoDelay) {
+        Log.i(TAG, "mergeMp4 start");
+        String[] commands = FFmpegUtil.mediaMux2(videoPath, mp3Path, true, outPath, videoDelay);
+        println(commands);
         if (commands != null) {
             return FFmpegCmd.executeSync(commands);
         }
@@ -351,39 +554,64 @@ public class MixHelper {
                     @Override
                     public void subscribe(@NonNull ObservableEmitter<String> emitter) throws Throwable {
                         deleteAllTempFile();
-                        resetCondition(resultCallback, offsetValue < 0 ? 6 : 5);
+                        int totalStep;
+                        if (offsetValue > 0) {
+                            //获取视频中的MP3 MP3转换为wav 获取纯视频 剪裁音频 裁剪视频 合并音频 合并最终音视频
+                            totalStep = 7;
+                        } else {
+                            //获取视频中的MP3 MP3转换为wav 获取纯视频  补齐视频 合并音频 合并最终音视频
+                            //获取视频中的MP3 MP3转换为wav 获取纯视频  合并音频 合并最终音视频
+                            totalStep = offsetValue != 0 ? 5 : 5;
+                        }
+                        resetCondition(resultCallback, totalStep);
 
-                        Log.i(TAG, "转换开始");
+                        Log.i(TAG, "转换开始:" + offsetValue);
                         int getVideoMp3Result = getVideoMp3_2(recordFilePath, videoMp3Path);
                         completed_num++;
                         Log.i(TAG, "getVideoMp3 complete:" + getVideoMp3Result);
                         //mix原音和视频bgm mp3
 //                        String accompanyPath = accompanimentMp3Path;
+                        Log.i(TAG, "mp3ToWav start");
                         int i = mp3ToWav(accompanimentMp3Path, accompanimentWAVPath);
                         completed_num++;
                         Log.i(TAG, "mp3ToWav complete:" + i);
-                        String accompanyPath = accompanimentWAVPath;
+                        //获取纯视频
+                        Log.i(TAG, "getOnlyVideo start");
+                        int getOnlyVideoResult = getOnlyVideo(recordFilePath, onlyVideoPath);
+                        completed_num++;
+                        Log.i(TAG, "getOnlyVideo complete:" + getOnlyVideoResult);
+                        String recordLastPath = videoMp3Path;
+                        String recordVideoLastPath = onlyVideoPath;
                         int recordFileOffset = 0;
                         int accompanimentFileOffset = 0;
-                        if (offsetValue >= 0) {
-                            accompanimentFileOffset = offsetValue;
-                        } else {
+                        if (offsetValue > 0) {
                             float cutM = Math.abs(offsetValue) * 1.0f / 1000;
-                            cutAudio(accompanyPath, cutM, 1000000, cutPathForMP4);
+                            Log.i(TAG, "cutAudio start");
+                            int cutAudioResult = cutAudio(recordLastPath, cutM, 5000000, cutPath);
+                            completed_num++;
+                            Log.i(TAG, "cutAudio end:" + cutAudioResult);
+
+                            Log.i(TAG, "cutVideo start");
+                            int cutVideoResult = cutVideo(onlyVideoPath, cutM, 5000000, handleVideoPath);
                             completed_num++;
-                            accompanyPath = cutPathForMP4;
+                            Log.i(TAG, "cutVideo end:" + cutVideoResult);
+                            recordLastPath = cutPath;
+                            recordVideoLastPath = handleVideoPath;
+                        } else {
+                            recordFileOffset = Math.abs(offsetValue);
                         }
                         Log.i(TAG, "mixMp3 start");
-                        int mixMp3Result = mixMp3(videoMp3Path, accompanyPath, mergeMp3PathForMP4, recordFileOffset, accompanimentFileOffset, volume1, volume2);
+                        int mixMp3Result = mixMp3(recordLastPath, accompanimentWAVPath, mergeMp3PathForMP4, recordFileOffset, accompanimentFileOffset, volume1, volume2);
                         completed_num++;
                         Log.i(TAG, "mixMp3 complete:" + mixMp3Result);
-                        //获取纯视频
-                        Log.i(TAG, "getOnlyVideo start");
-                        int getOnlyVideoResult = getOnlyVideo(recordFilePath, onlyVideoPath);
-                        completed_num++;
-                        Log.i(TAG, "getOnlyVideo complete:" + getOnlyVideoResult);
                         String lastResultPath = getLastResultPath();
-                        int mergeResult = megreMp3AndMp4(onlyVideoPath, mergeMp3PathForMP4, lastResultPath);
+                        int mergeResult;
+                        if (offsetValue < 0) {
+                            float videoDelay = (recordFileOffset * 1.0f / 1000);
+                            mergeResult = megreMp3AndMp4V2(recordVideoLastPath, mergeMp3PathForMP4, lastResultPath, videoDelay);
+                        } else {
+                            mergeResult = megreMp3AndMp4(recordVideoLastPath, mergeMp3PathForMP4, lastResultPath);
+                        }
                         Log.i(TAG, "mergeMp4 complete:" + mergeResult);
                         Log.i(TAG, "转换完成");
                         resetCount();
@@ -445,26 +673,26 @@ public class MixHelper {
 //                        Log.i(TAG, "getVideoMp3 complete:" + getVideoMp3Result);
 //                        Log.i(TAG, "getMp3FromWav start");
                         deleteAllTempFile();
-                        resetCondition(resultCallback, offsetValue < 0 ? 3 : 2);
+                        resetCondition(resultCallback, offsetValue > 0 ? 3 : 2);
                         int mp3FromWav = getMp3FromWav(recordFilePath, recordFileMp3Path);
                         completed_num++;
                         Log.i(TAG, "getMp3FromWav complete:" + mp3FromWav);
-                        String accompanyPath = accompanimentMp3Path;
+                        String recordLastPath = recordFileMp3Path;
                         int recordFileOffset = 0;
                         int accompanimentFileOffset = 0;
-                        if (offsetValue >= 0) {
-                            accompanimentFileOffset = offsetValue;
-                        } else {
+                        if (offsetValue > 0) {
                             float cutM = Math.abs(offsetValue) * 1.0f / 1000;
                             Log.i(TAG, "cutAudio start");
-                            int cutAudioResult = cutAudio(accompanyPath, cutM, 1000000, cutPath);
+                            int cutAudioResult = cutAudio(recordLastPath, cutM, 5000000, cutPath);
                             completed_num++;
                             Log.i(TAG, "cutAudio end:" + cutAudioResult);
-                            accompanyPath = cutPath;
+                            recordLastPath = cutPath;
+                        } else {
+                            recordFileOffset = Math.abs(offsetValue);
                         }
                         //mix原音和视频bgm mp3
                         Log.i(TAG, "mixMp3 start");
-                        int mixMp3Result = mixMp3(recordFileMp3Path, accompanyPath, mergeMp3Path, recordFileOffset, accompanimentFileOffset, volume1, volume2);
+                        int mixMp3Result = mixMp3(recordLastPath, accompanimentMp3Path, mergeMp3Path, recordFileOffset, accompanimentFileOffset, volume1, volume2);
                         Log.i(TAG, "mixMp3 complete:" + mixMp3Result);
                         resetCount();
 
@@ -516,4 +744,63 @@ public class MixHelper {
         }
         return -1;
     }
+
+    public int downloadM3U8FileToMp4(String url, String encryption_key, String encryption_kid, String outPath) {
+        Log.i(TAG, "downloadM3U8FileToMp4 start url:" + url + "\noutPath:" + outPath);
+        String[] commands = new String[6];
+        commands[0] = "ffmpeg";
+        commands[1] = "-i";
+        commands[2] = url;
+        commands[3] = "-c";
+        commands[4] = "copy";
+//        commands[5] = "-encryption_scheme";
+//        commands[6] = "cenc-aes-ctr";
+//        commands[7] = "-encryption_key";
+//        commands[8] = encryption_key;
+//        commands[9] = "-encryption_kid";
+//        commands[10] = encryption_kid;
+        commands[5] = outPath;
+        if (commands != null) {
+            println(commands);
+            return FFmpegCmd.executeSync(commands);
+        }
+        return -1;
+    }
+
+    public int encryptionMp4(String input, String key, String id, String output) {
+        String[] commands = new String[12];
+        commands[0] = "ffmpeg";
+        commands[1] = "-i";
+        commands[2] = input;
+        commands[3] = "-c";
+        commands[4] = "-copy";
+        commands[5] = "-encryption_scheme";
+        commands[6] = "cenc-aes-ctr";
+        commands[7] = "-encryption_key";
+        commands[8] = key;
+        commands[9] = "-encryption_kid";
+        commands[10] = id;
+        commands[11] = output;
+        if (commands != null) {
+            println(commands);
+            return FFmpegCmd.executeSync(commands);
+        }
+        return -1;
+    }
+
+    public int decryptionMp4(String input, String key, String output) {
+        Log.i(TAG, "encryptionMp4 start input:" + input + "\noutPath:" + output);
+        String[] commands = new String[6];
+        commands[0] = "ffmpeg";
+        commands[1] = "-i";
+        commands[2] = input;
+        commands[3] = "-decryption_key";
+        commands[4] = key;
+        commands[5] = output;
+        if (commands != null) {
+            println(commands);
+            return FFmpegCmd.executeSync(commands);
+        }
+        return -1;
+    }
 }

+ 67 - 0
musicMerge/src/main/java/com/cooleshow/musicmerge/player/AddAudioMixer.java

@@ -0,0 +1,67 @@
+package com.cooleshow.musicmerge.player;
+
+import com.cooleshow.base.utils.LOG;
+
+/**
+ *  叠加合成器
+ * Author by pq, Date on 2024/6/14.
+ */
+public class AddAudioMixer extends MultiAudioMixer {
+
+    @Override
+    public byte[] mixRawAudioBytes(byte[][] bMulRoadAudioes) {
+
+        if (bMulRoadAudioes == null || bMulRoadAudioes.length == 0)
+            return null;
+
+        byte[] realMixAudio = bMulRoadAudioes[0];
+
+        if (bMulRoadAudioes.length == 1)
+            return realMixAudio;
+
+//            for (int rw = 0; rw < bMulRoadAudioes.length; ++rw) {
+//                if (bMulRoadAudioes[rw].length != realMixAudio.length) {
+//                    Log.e("app", "column of the road of audio + " + rw + " is diffrent.");
+//                    return null;
+//                }
+//            }
+
+        //row 代表参与合成的音频数量
+        //column 代表一段音频的采样点数,这里所有参与合成的音频的采样点数都是相同的
+        int row = bMulRoadAudioes.length;
+        int coloum = realMixAudio.length / 2;
+        LOG.i("pq", "row:" + row);
+        LOG.i("pq", "coloum:" + coloum);
+        short[][] sMulRoadAudioes = new short[row][coloum];
+
+        //PCM音频16位的存储是大端存储方式,即低位在前,高位在后,例如(X1Y1, X2Y2, X3Y3)数据,它代表的采样点数值就是((Y1 * 256 + X1), (Y2 * 256 + X2), (Y3 * 256 + X3))
+        for (int r = 0; r < row; ++r) {
+            for (int c = 0; c < coloum; ++c) {
+                sMulRoadAudioes[r][c] = (short) ((bMulRoadAudioes[r][c * 2] & 0xff) | (bMulRoadAudioes[r][c * 2 + 1] & 0xff) << 8);
+            }
+        }
+
+        short[] sMixAudio = new short[coloum];
+        int mixVal;
+        int sr = 0;
+        for (int sc = 0; sc < coloum; ++sc) {
+            mixVal = 0;
+            sr = 0;
+            //这里采取累加法
+            for (; sr < row; ++sr) {
+                mixVal += sMulRoadAudioes[sr][sc];
+            }
+            //最终值不能大于short最大值,因此可能出现溢出
+            sMixAudio[sc] = (short) (mixVal);
+        }
+
+        //short值转为大端存储的双字节序列
+        for (sr = 0; sr < coloum; ++sr) {
+            realMixAudio[sr * 2] = (byte) (sMixAudio[sr] & 0x00FF);
+            realMixAudio[sr * 2 + 1] = (byte) ((sMixAudio[sr] & 0xFF00) >> 8);
+        }
+
+        return realMixAudio;
+    }
+
+}

+ 58 - 0
musicMerge/src/main/java/com/cooleshow/musicmerge/player/AverageAudioMixer.java

@@ -0,0 +1,58 @@
+package com.cooleshow.musicmerge.player;
+
+import android.util.Log;
+
+/**
+ * 求平均值合成器
+ * Author by pq, Date on 2024/6/14.
+ */
+public class AverageAudioMixer extends MultiAudioMixer{
+
+    @Override
+    public byte[] mixRawAudioBytes(byte[][] bMulRoadAudioes) {
+
+        if (bMulRoadAudioes == null || bMulRoadAudioes.length == 0)
+            return null;
+
+        byte[] realMixAudio = bMulRoadAudioes[0];
+
+        if (bMulRoadAudioes.length == 1)
+            return realMixAudio;
+
+        for (int rw = 0; rw < bMulRoadAudioes.length; ++rw) {
+            if (bMulRoadAudioes[rw].length != realMixAudio.length) {
+                Log.e("app", "column of the road of audio + " + rw + " is diffrent.");
+                return null;
+            }
+        }
+
+        int row = bMulRoadAudioes.length;
+        int coloum = realMixAudio.length / 2;
+        short[][] sMulRoadAudioes = new short[row][coloum];
+
+        for (int r = 0; r < row; ++r) {
+            for (int c = 0; c < coloum; ++c) {
+                sMulRoadAudioes[r][c] = (short) ((bMulRoadAudioes[r][c * 2] & 0xff) | (bMulRoadAudioes[r][c * 2 + 1] & 0xff) << 8);
+            }
+        }
+
+        short[] sMixAudio = new short[coloum];
+        int mixVal;
+        int sr = 0;
+        for (int sc = 0; sc < coloum; ++sc) {
+            mixVal = 0;
+            sr = 0;
+            for (; sr < row; ++sr) {
+                mixVal += sMulRoadAudioes[sr][sc];
+            }
+            sMixAudio[sc] = (short) (mixVal / row);
+        }
+
+        for (sr = 0; sr < coloum; ++sr) {
+            realMixAudio[sr * 2] = (byte) (sMixAudio[sr] & 0x00FF);
+            realMixAudio[sr * 2 + 1] = (byte) ((sMixAudio[sr] & 0xFF00) >> 8);
+        }
+
+        return realMixAudio;
+    }
+}

+ 30 - 0
musicMerge/src/main/java/com/cooleshow/musicmerge/player/ComposeInfo.java

@@ -0,0 +1,30 @@
+package com.cooleshow.musicmerge.player;
+
+/**
+ * 音频合成信息
+ *
+ */
+public class ComposeInfo {
+
+    /**
+     * 音频文件路径
+     */
+    public String audioPath;
+
+    /**
+     * 音频解码后的pcm文件路径
+     */
+    public String pcmPath;
+
+    /**
+     * 音频开始播放的时间
+     */
+    public float offsetSeconds;
+
+    /**
+     * 参与合成的权重大小
+     */
+    public float weight;
+
+
+}

+ 51 - 34
musicMerge/src/main/java/com/cooleshow/musicmerge/player/CustomPlayer.java

@@ -14,8 +14,6 @@ import android.view.SurfaceHolder;
 
 import com.cooleshow.base.utils.LOG;
 
-import java.io.IOException;
-
 import androidx.annotation.NonNull;
 
 /**
@@ -51,7 +49,8 @@ public class CustomPlayer implements MediaPlayer.OnErrorListener, MediaPlayer.On
         @Override
         public void run() {
             if (mPlayer != null) {
-                int currentPosition = mPlayer.getCurrentPosition();
+                int currentPosition = getCu();
+                LOG.i(TAG, "getCu:" + currentPosition);
                 if (onEventListener != null) {
                     onEventListener.onProgress(currentPosition);
                 }
@@ -94,7 +93,7 @@ public class CustomPlayer implements MediaPlayer.OnErrorListener, MediaPlayer.On
             mPlayer.reset();
             mPlayer.setDataSource(context, dataSource); // 设置曲目资源
             mPlayer.prepareAsync(); // 异步的准备方法
-        } catch (IOException e) {
+        } catch (Exception e) {
             e.printStackTrace();
         }
     }
@@ -108,7 +107,7 @@ public class CustomPlayer implements MediaPlayer.OnErrorListener, MediaPlayer.On
             mPlayer.reset();
             mPlayer.setDataSource(path); // 设置曲目资源
             mPlayer.prepareAsync(); // 异步的准备方法
-        } catch (IOException e) {
+        } catch (Exception e) {
             e.printStackTrace();
         }
     }
@@ -138,7 +137,6 @@ public class CustomPlayer implements MediaPlayer.OnErrorListener, MediaPlayer.On
         if (isStoped) {
             return;
         }
-        // release()会释放player、将player置空,所以这里需要判断一下
         if (null != mPlayer && hasPrepared) {
             LOG.i(TAG, "start expectPosition:" + expectPosition);
             if (expectPosition < 0) {
@@ -146,17 +144,16 @@ public class CustomPlayer implements MediaPlayer.OnErrorListener, MediaPlayer.On
                 return;
             }
             mPlayer.start();
+            sendProgressMsg();
         }
-
-//        // release()会释放player、将player置空,所以这里需要判断一下
-//        if (null != mPlayer && hasPrepared) {
-//            mPlayer.start();
-//        }
     }
 
     public void setSpeed(float speed) {
-        if (speed < 0.5 || speed > 2.0f) {
-            return;
+        if (speed < 0.5) {
+            speed = 0.5f;
+        }
+        if (speed > 2.0) {
+            speed = 2.0f;
         }
         try {
             if (mPlayer != null) {
@@ -175,18 +172,17 @@ public class CustomPlayer implements MediaPlayer.OnErrorListener, MediaPlayer.On
     public void pause() {
         if (null != mPlayer && hasPrepared) {
             mPlayer.pause();
+            removeDelayPlayMsg();
+            removeProgressMsg();
         }
     }
 
+    private void removeDelayPlayMsg() {
+        mHandler.removeMessages(RESUME_PLAY_MSG_WHAT);
+    }
+
     public void resume() {
-        if (null != mPlayer && hasPrepared) {
-            LOG.i(TAG, "resume expectPosition:" + expectPosition);
-            if (expectPosition < 0) {
-                sendDelayPlayMsg(Math.abs(expectPosition));
-                return;
-            }
-            mPlayer.start();
-        }
+        start();
     }
 
     private int expectPosition = 0;
@@ -200,9 +196,7 @@ public class CustomPlayer implements MediaPlayer.OnErrorListener, MediaPlayer.On
     }
 
     public void resetToPrepare() {
-        if (expectPosition == 0) {
-            seekTo(0);
-        } else {
+        if (expectPosition != 0) {
             seekTo(expectPosition);
         }
     }
@@ -213,28 +207,34 @@ public class CustomPlayer implements MediaPlayer.OnErrorListener, MediaPlayer.On
         if (mPlayer == null) {
             return;
         }
+        boolean hasMessages = mHandler.hasMessages(RESUME_PLAY_MSG_WHAT);
         mHandler.removeMessages(RESUME_PLAY_MSG_WHAT);
-        LOG.i(TAG, "seekTo getTotal:" + getTotal());
-        if (position > getTotal()) {
-            position = getTotal();
+        int total = getTotal();
+        LOG.i(TAG, "seekTo getTotal:" + total);
+        if (total != 0 && position > total) {
+            position = total - 100;
         }
         LOG.i(TAG, "isPlaying:" + isPlaying());
         if (position < 0) {
             mPlayer.seekTo(0);
-            if (isPlaying()) {
+            if (isPlaying() || hasMessages) {
                 pause();
                 sendDelayPlayMsg(Math.abs(position));
             }
             return;
-        }
-        LOG.i(TAG, "seekTo:" + position + "--hasPrepared:" + hasPrepared);
-        if (null != mPlayer && hasPrepared) {
-            mPlayer.seekTo(position);
+        } else {
+            LOG.i(TAG, "seekTo:" + position + "--hasPrepared:" + hasPrepared);
+            if (null != mPlayer && hasPrepared) {
+                mPlayer.seekTo(position);
+                if (hasMessages) {
+                    sendDelayPlayMsg(0);
+                }
+            }
         }
     }
 
     private void sendDelayPlayMsg(int delay) {
-        LOG.i(TAG,"sendDelayPlayMsg:"+delay);
+        LOG.i(TAG, "sendDelayPlayMsg:" + delay);
         Message obtain = Message.obtain();
         obtain.what = RESUME_PLAY_MSG_WHAT;
         mHandler.removeMessages(RESUME_PLAY_MSG_WHAT);
@@ -280,7 +280,6 @@ public class CustomPlayer implements MediaPlayer.OnErrorListener, MediaPlayer.On
     @Override
     public void onPrepared(MediaPlayer mp) {
         hasPrepared = true; // 准备完成后回调到这里
-        sendProgressMsg();
         if (onEventListener != null) {
             onEventListener.onPrepared(getTotal());
         }
@@ -294,9 +293,14 @@ public class CustomPlayer implements MediaPlayer.OnErrorListener, MediaPlayer.On
     }
 
     private void sendProgressMsg() {
+        removeProgressMsg();
         mHandler.postDelayed(progressRunnable, 100);
     }
 
+    private void removeProgressMsg() {
+        mHandler.removeCallbacks(progressRunnable);
+    }
+
     private void removeAllMsg() {
         mHandler.removeCallbacksAndMessages(null);
     }
@@ -304,6 +308,11 @@ public class CustomPlayer implements MediaPlayer.OnErrorListener, MediaPlayer.On
     @Override
     public void onCompletion(MediaPlayer mp) {
 //        hasPrepared = false;
+        LOG.i(TAG, "onCompletion");
+        removeProgressMsg();
+        if (isPlaying()) {
+            pause();
+        }
         if (onEventListener != null) {
             onEventListener.onCompleted();
         }
@@ -312,6 +321,7 @@ public class CustomPlayer implements MediaPlayer.OnErrorListener, MediaPlayer.On
 
     @Override
     public boolean onError(MediaPlayer mp, int what, int extra) {
+        LOG.i(TAG, "onError:" + what);
         hasPrepared = false;
         if (onEventListener != null) {
             onEventListener.onError();
@@ -330,6 +340,13 @@ public class CustomPlayer implements MediaPlayer.OnErrorListener, MediaPlayer.On
         }
     }
 
+    public int getAudioSessionId() {
+        if (mPlayer != null) {
+            return mPlayer.getAudioSessionId();
+        }
+        return 0;
+    }
+
     public interface OnEventListener {
         void onProgress(int progress);
 

+ 666 - 0
musicMerge/src/main/java/com/cooleshow/musicmerge/player/MergeTrackManager.java

@@ -0,0 +1,666 @@
+package com.cooleshow.musicmerge.player;
+
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.AudioTrack;
+import android.media.PlaybackParams;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import com.cooleshow.base.utils.ConvertUtils;
+import com.cooleshow.base.utils.LOG;
+import com.cooleshow.musicmerge.utils.AudioConverter;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.util.Arrays;
+
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
+import io.reactivex.rxjava3.annotations.NonNull;
+import io.reactivex.rxjava3.core.Observable;
+import io.reactivex.rxjava3.core.ObservableEmitter;
+import io.reactivex.rxjava3.core.ObservableOnSubscribe;
+import io.reactivex.rxjava3.core.Observer;
+import io.reactivex.rxjava3.disposables.Disposable;
+import io.reactivex.rxjava3.schedulers.Schedulers;
+
+/**
+ * Author by pq, Date on 2022/10/26.
+ */
+public class MergeTrackManager {
+    public static final int PLAY_COMPLETED = 1001;
+    public static final int MAX_FILE_COUNT = 2;
+
+    public static final int WAV_HEADER_LENGTH = 44;
+    private static final int DEFAULT_STREAM_TYPE = AudioManager.STREAM_MUSIC;
+    private static final int DEFAULT_PLAY_MODE = AudioTrack.MODE_STREAM;
+    private static final int DEFAULT_CHANNEL = AudioFormat.CHANNEL_OUT_MONO;
+    private static final String TAG = "MergeTrackManager";
+    private OnEventListener eventListener;
+
+    AudioTrack mAudioTrack;
+
+    /**
+     * 总长度
+     **/
+    int length;
+    /**
+     * 是否循环播放
+     */
+    private boolean ISPLAYSOUND = false;
+
+    private float[] mWeghts;
+    private long[] positionOffset;
+
+    private long totalDuration = -1;
+
+    private long playProgress = -1;
+    private int minBufferSize;
+    private byte[][] mAllAudioData;
+    private byte[][] offsetAudioData;
+    public static final int RATE = 44100;
+
+    public static final float MAX_VOLUME = 1f;
+    public static final int CHANNEL_CONFIG = 2;
+    public static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
+
+    private int bytesReadPos = 0;
+
+    private Handler mHandler = new Handler(Looper.getMainLooper()) {
+        @Override
+        public void handleMessage(@androidx.annotation.NonNull Message msg) {
+            int what = msg.what;
+            if (what == PLAY_COMPLETED) {
+                if (eventListener != null) {
+                    eventListener.onProgress(getTotalDuration());
+                }
+                return;
+            }
+        }
+    };
+
+    private Runnable progressRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (mAudioTrack != null) {
+//                long currentPosition = getPlayProgress();
+//                LOG.i(TAG, "getCu:" + currentPosition);
+//                if (eventListener != null) {
+//                    eventListener.onProgress(currentPosition);
+//                }
+                sendProgressMsg();
+            }
+        }
+    };
+
+
+    public static MergeTrackManager createInstance() {
+        MergeTrackManager mergeTrackManager = new MergeTrackManager();
+        return mergeTrackManager;
+    }
+
+    public MergeTrackManager() {
+
+    }
+
+    public void init(OnEventListener onEventListener) {
+        minBufferSize = AudioTrack.getMinBufferSize(RATE, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
+        LOG.i(TAG, "bufferSize:" + minBufferSize);
+        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, RATE,
+                AudioFormat.CHANNEL_IN_STEREO, // CHANNEL_CONFIGURATION_MONO,
+                AUDIO_FORMAT, minBufferSize * 2, AudioTrack.MODE_STREAM);
+        LOG.i(TAG, "OnEventListener:" + onEventListener);
+        setOnEventListener(onEventListener);
+    }
+
+    private OnEventListener getEventListener() {
+        return eventListener;
+    }
+
+    public void setOnEventListener(OnEventListener onEventListener) {
+        this.eventListener = onEventListener;
+    }
+
+    private void sendProgressMsg() {
+        removeProgressMsg();
+        mHandler.postDelayed(progressRunnable, 100);
+    }
+
+    private void removeProgressMsg() {
+        mHandler.removeCallbacks(progressRunnable);
+    }
+
+    public void setWeight(int pos, float value) {
+        if (mWeghts == null) {
+            initWeight(MAX_FILE_COUNT);
+        }
+        if (value < 0f || value > 1.0f) {
+            return;
+        }
+        if (pos < mWeghts.length) {
+            LOG.i(TAG, "setWeight:" + pos + "--value:" + value);
+            mWeghts[pos] = value * MAX_VOLUME;
+        }
+    }
+
+    public void setPositionOffset(int pos, int timeOffset) {
+        LOG.i(TAG, "setPositionOffset pos:" + pos + "--value:" + timeOffset);
+        if (positionOffset == null) {
+            positionOffset = new long[2];
+        }
+        if (pos < positionOffset.length) {
+            positionOffset[pos] = timeOffset;
+            offsetAudioData(mAllAudioData);
+        }
+    }
+
+    public void setOnlyPlay(int pos) {
+        if (mWeghts == null) {
+            return;
+        }
+        if (pos < mWeghts.length) {
+            for (int i = 0; i < mWeghts.length; i++) {
+                mWeghts[i] = i == pos ? MAX_VOLUME : 0;
+            }
+        }
+    }
+
+    public void setPlaySpeed(float value) {
+        // 设置播放速度
+        if (value < 0.5f) {
+            value = 0.5f;
+        }
+        if (value > 2.5f) {
+            value = 2.5f;
+        }
+        if (mAudioTrack != null) {
+            PlaybackParams playbackParams = null;
+//            int i = mAudioTrack.setPlaybackRate((int) (value * RATE));
+//            LOG.i("setPlaybackRate:"+i);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                playbackParams = mAudioTrack.getPlaybackParams();
+                playbackParams.setSpeed(value);
+                LOG.i(TAG, "setPlaybackRate:" + value);
+                mAudioTrack.setPlaybackParams(playbackParams);
+            }
+        }
+    }
+
+    public void play2(String[] filePaths) {
+        if (filePaths.length > MAX_FILE_COUNT) {
+            return;
+        }
+        if (ISPLAYSOUND) {
+            return;
+        }
+        Observable.create(new ObservableOnSubscribe<Boolean>() {
+                    @Override
+                    public void subscribe(@NonNull ObservableEmitter<Boolean> emitter) throws Throwable {
+                        reset();
+                        if (mWeghts == null) {
+                            initWeight(MAX_FILE_COUNT);
+                        }
+                        //解析
+                        mAllAudioData = parseFile(filePaths);
+                        //对齐
+                        offsetAudioData(mAllAudioData);
+                        startPlay(emitter);
+//                        WeightAudioMixer weightAudioMixer = new WeightAudioMixer(mWeghts);
+//                        ISPLAYSOUND = true;
+//                        mAudioTrack.play();
+//                        emitter.onNext(true);
+//                        byte[][] allAudioData2 = new byte[filePaths.length][];
+//                        while (ISPLAYSOUND) {
+//                            int result = countBytesByPosition(bytesReadPos, allAudioData2);
+//                            bytesReadPos = result != -1 ? ++result : -1;
+//                            LOG.i("bytesRead:" + bytesReadPos);
+//                            byte[] audiodata = weightAudioMixer.mixRawAudioBytes(allAudioData2);
+//                            mAudioTrack.write(audiodata, 0, minBufferSize);
+//                            if (bytesReadPos == -1) {
+//                                LOG.i(TAG, "读取完成:" + getPlayProgress());
+//                                sendPlayCompletedMsg(audiodata.length);
+//                                break;
+//                            } else {
+//                                countPlayProgress();
+//                            }
+//                        }
+                    }
+                }).subscribeOn(Schedulers.newThread())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(new Observer<Boolean>() {
+                    @Override
+                    public void onSubscribe(@NonNull Disposable d) {
+
+                    }
+
+                    @Override
+                    public void onNext(@NonNull Boolean result) {
+                        if (result != null) {
+                            OnEventListener onEventListener = getEventListener();
+                            if (onEventListener != null) {
+                                onEventListener.onStartPlay();
+                            }
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull Throwable e) {
+                        e.printStackTrace();
+                    }
+
+                    @Override
+                    public void onComplete() {
+
+                    }
+                });
+    }
+
+    private void initWeight(int length) {
+        mWeghts = new float[length];
+        Arrays.fill(mWeghts, MAX_VOLUME);
+    }
+
+    private void reset() {
+        bytesReadPos = 0;
+        playProgress = 0;
+    }
+
+    private void countPlayProgress() {
+        if (bytesReadPos != -1) {
+            long bytePlayed = (long) bytesReadPos * minBufferSize;
+            playProgress = getTimeFromLength(bytePlayed);
+            LOG.i(TAG, "progress:" + playProgress);
+            if (eventListener != null) {
+                eventListener.onProgress(playProgress);
+            }
+        }
+    }
+
+    public int getAudioSessionId() {
+        if (mAudioTrack != null) {
+            return mAudioTrack.getAudioSessionId();
+        }
+        return -1;
+    }
+
+    private void sendPlayCompletedMsg(int lastLength) {
+        removeProgressMsg();
+        cancelPlayCompletedMsg();
+        long delay = getTimeFromLength(lastLength);
+        LOG.i(TAG, "lastLength:" + lastLength);
+        LOG.i(TAG, "delay:" + delay);
+        Message obtain = Message.obtain();
+        obtain.what = PLAY_COMPLETED;
+        mHandler.sendMessageDelayed(obtain, delay);
+    }
+
+    private int countBytesByPosition(int bytesReadStart, byte[][] allAudioData2) {
+        int result = 0;
+        boolean hasEnd = false;
+        for (int i = 0; i < offsetAudioData.length; i++) {
+            byte[] buffer = new byte[minBufferSize];
+            LOG.i(TAG, "source:" + offsetAudioData[i].length + "--bytesReadStart:" + bytesReadStart);
+            result = splitByteArray(offsetAudioData[i], bytesReadStart, buffer);
+            if (result == -1) {
+                hasEnd = true;
+            }
+            allAudioData2[i] = buffer;
+        }
+        return hasEnd ? -1 : result;
+    }
+
+    private void offsetAudioData(byte[][] allAudioData) {
+        if (allAudioData == null) {
+            return;
+        }
+        if (offsetAudioData == null) {
+            offsetAudioData = new byte[mAllAudioData.length][];
+        }
+        for (int i = 0; i < allAudioData.length; i++) {
+            byte[] afterChangeBytes = countFilePosFromTime(allAudioData[i], positionOffset[i]);
+            LOG.i(TAG, "offsetAudioData[i]" + afterChangeBytes.length);
+            offsetAudioData[i] = afterChangeBytes;
+        }
+    }
+
+    private byte[] countFilePosFromTime(byte[] source, long timeOffset) {
+        LOG.i(TAG, "timeOffset:" + timeOffset);
+        if (timeOffset != 0) {
+            int exceptAudioLength = getLengthFromDuration(timeOffset);
+            //因为采样率44100 双声道 16位的位深 需要偶数项
+            if (exceptAudioLength % 2 != 0) {
+                exceptAudioLength += 1;
+            }
+            LOG.i(TAG, "exceptAudioLength:" + exceptAudioLength);
+            byte[] lastLengthFromExcept = getLastLengthFromExcept(source, exceptAudioLength);
+            LOG.i(TAG, "lastLengthFromExcept:" + lastLengthFromExcept.length);
+            if (eventListener != null) {
+                eventListener.onOffset(source.length, lastLengthFromExcept.length);
+            }
+            return lastLengthFromExcept;
+        } else {
+            return source;
+        }
+    }
+
+    private int getLengthFromDuration(long timeOffset) {
+        float d = timeOffset / 1000f;
+        int exceptAudioLength = (int) (d * (RATE * CHANNEL_CONFIG * AUDIO_FORMAT));
+        return exceptAudioLength;
+    }
+
+    private long getTimeFromLength(long byteLength) {
+        return (long) ((byteLength * 1.0f / ((long) RATE * CHANNEL_CONFIG * AUDIO_FORMAT)) * 1000);
+    }
+
+    public byte[] getLastLengthFromExcept(byte[] originalAudio, int exceptLength) {
+        LOG.i(TAG, "originalAudio:" + originalAudio.length);
+        LOG.i(TAG, "exceptLength:" + exceptLength);
+        if (originalAudio.length == exceptLength) {
+            return originalAudio;
+        }
+        byte[] paddedBlock;
+        if (exceptLength > 0 && originalAudio.length > exceptLength) {
+//            paddedBlock = new byte[originalAudio.length - exceptLength];
+            paddedBlock = AudioConverter.trimWavFile(originalAudio, exceptLength, originalAudio.length);
+        } else {
+            int e = Math.abs(exceptLength);
+            paddedBlock = new byte[originalAudio.length + e];
+            //补齐
+            System.arraycopy(originalAudio, 0, paddedBlock, e, originalAudio.length);
+        }
+        return paddedBlock;
+    }
+
+    @androidx.annotation.NonNull
+    private byte[][] parseFile(String[] filePaths) {
+        byte[][] allAudioData = new byte[filePaths.length][];
+        for (int i = 0; i < filePaths.length; i++) {
+            String filePath = filePaths[i];
+            LOG.i(TAG, "filePath:" + filePath);
+            byte[] bytes = readAudioDataFromFile(filePath);
+            allAudioData[i] = bytes;
+        }
+        makeLengthConsistent(allAudioData);
+        int byteLength = allAudioData[0].length;
+        LOG.i(TAG, "allAudioData[0]:" + allAudioData[0].length);
+        LOG.i(TAG, "allAudioData[1]:" + allAudioData[1].length);
+        countPlayTotalTime(RATE, CHANNEL_CONFIG, AUDIO_FORMAT, byteLength);
+        return allAudioData;
+    }
+
+    private void countPlayTotalTime(int rate, int channelConfig, int audioFormat, int byteLength) {
+        //一帧音频的大小(字节) = 通道数 x 采样个数 x 采样位数
+        LOG.i(TAG, "countPlayTotalTime: rate:" + rate + "-channelConfig:" + channelConfig + "-audioFormat:" + audioFormat + "-byteLength:" + byteLength);
+        totalDuration = (long) ((byteLength * 1.0f / ((long) rate * channelConfig * audioFormat)) * 1000);
+        LOG.i(TAG, "totalDuration:" + totalDuration);
+        if (eventListener != null) {
+            eventListener.onPrepare(totalDuration);
+        }
+    }
+
+    private int splitByteArray(byte[] source, int startIndex, byte[] destination) {
+        if (startIndex * destination.length >= source.length) {
+            return -1;
+        }
+        int remainByteLength = source.length - startIndex * destination.length;
+        boolean isLast = remainByteLength <= destination.length;
+        if (isLast) {
+            System.arraycopy(source, startIndex * destination.length, destination, 0, remainByteLength);
+        } else {
+            if (startIndex == 0) {
+                //去掉头部44个字节
+                System.arraycopy(source, WAV_HEADER_LENGTH, destination, 0, destination.length - WAV_HEADER_LENGTH);
+            } else {
+                System.arraycopy(source, startIndex * destination.length, destination, 0, destination.length);
+            }
+        }
+        if (isLast) {
+            return -1;
+        }
+        return startIndex;
+    }
+
+    public long getTotalDuration() {
+        return totalDuration;
+    }
+
+    public void seekPercent(float percent) {
+        if (getTotalDuration() == -1) {
+            return;
+        }
+        if (percent < 0 || percent > 1.0f) {
+            return;
+        }
+        long totalDuration = getTotalDuration();
+        LOG.i(TAG, "seek percent:" + percent);
+        LOG.i(TAG, "seek percent:" + totalDuration);
+        int posTime = (int) (totalDuration * percent);
+        LOG.i(TAG, "seek posTime:" + posTime);
+        int seekToFrame = (int) (RATE * (posTime * 1.0f / 1000));  // 转换为对应的采样帧
+        LOG.i(TAG, "seek seekToFrame:" + seekToFrame);
+        setReadPos(posTime);
+    }
+
+    private void setReadPos(int exceptTimePos) {
+        //转换为期望的位置
+        LOG.i(TAG, "setReadPos:" + exceptTimePos);
+        int exceptBytePos = getLengthFromDuration(exceptTimePos);
+        LOG.i(TAG, "setReadPos exceptBytePos:" + exceptBytePos);
+        bytesReadPos = Math.round(exceptBytePos * 1.0f / minBufferSize);
+        LOG.i(TAG, "setReadPos pos:" + bytesReadPos);
+    }
+
+    private void seekTo(int seekToFrame) {
+        if (mAudioTrack != null) {
+            cancelPlayCompletedMsg();
+//            int currentFrame = mAudioTrack.getPlaybackHeadPosition();  // 获取当前播放头位置
+//            int targetFrame = seekToFrame;  // 计算目标播放头位置
+            LOG.i(TAG, "seekTo:" + seekToFrame);
+            int i = mAudioTrack.setPlaybackHeadPosition(seekToFrame);
+            LOG.i(TAG, "seekTo result:" + i);
+        }
+    }
+
+    private void cancelPlayCompletedMsg() {
+        mHandler.removeMessages(PLAY_COMPLETED);
+    }
+
+    public boolean isPlaying() {
+        if (mAudioTrack != null) {
+            return mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING;
+        }
+        return false;
+    }
+
+    public long getPlayProgress() {
+        if (mAudioTrack != null) {
+//            int currentFrame = mAudioTrack.getPlaybackHeadPosition();
+//            LOG.i(TAG, "currentFrame:" + currentFrame);
+//            float playTime = currentFrame * 1.0f / RATE;
+//            long currentPlayTimeMs = (long) (1000 * playTime);
+//            LOG.i(TAG, "currentPlayTimeMs=" + currentPlayTimeMs);
+            return playProgress;
+        }
+        return -1;
+    }
+
+    public void makeLengthConsistent(byte[][] dataBlocks) {
+        // 找出最长的数据块长度
+//        makeLengthForMax(dataBlocks);
+        makeLengthForMin(dataBlocks);
+    }
+
+    private void makeLengthForMax(byte[][] dataBlocks) {
+        int maxLength = 0;
+        for (byte[] block : dataBlocks) {
+            if (block.length > maxLength) {
+                maxLength = block.length;
+            }
+        }
+
+        // 填充数据块,使它们的长度一致
+        for (int i = 0; i < dataBlocks.length; i++) {
+            byte[] block = dataBlocks[i];
+            if (block.length < maxLength) {
+                byte[] paddedBlock = new byte[maxLength];
+                System.arraycopy(block, 0, paddedBlock, 0, block.length);
+                dataBlocks[i] = paddedBlock;
+            }
+        }
+    }
+
+    private void makeLengthForMin(byte[][] dataBlocks) {
+        int maxLength = 0;
+        for (byte[] block : dataBlocks) {
+            if (maxLength == 0) {
+                maxLength = block.length;
+            }
+            if (block.length < maxLength) {
+                maxLength = block.length;
+            }
+        }
+
+        // 填充数据块,使它们的长度一致
+        for (int i = 0; i < dataBlocks.length; i++) {
+            byte[] block = dataBlocks[i];
+            if (block.length > maxLength) {
+                byte[] paddedBlock = new byte[maxLength];
+                System.arraycopy(block, 0, paddedBlock, 0, paddedBlock.length);
+                dataBlocks[i] = paddedBlock;
+            }
+        }
+    }
+
+    private byte[] readAudioDataFromFile(String filePath) {
+        try {
+            File file = new File(filePath);
+            FileInputStream fileInputStream = new FileInputStream(file);
+            //声道判断
+            boolean isStereoWavFile = AudioConverter.isStereoWavFile(fileInputStream);
+            ByteArrayOutputStream byteArrayOutputStream = ConvertUtils.input2OutputStream(fileInputStream);
+            byte[] audiodata = byteArrayOutputStream.toByteArray();
+            if (!isStereoWavFile) {
+                audiodata = AudioConverter.convertMonoToStereo(audiodata);
+            }
+            byteArrayOutputStream.close();
+            return audiodata;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public void stop() {
+        ISPLAYSOUND = false;
+        if (mAudioTrack != null) {
+            if (mAudioTrack.getState() == AudioRecord.STATE_INITIALIZED) {
+                mAudioTrack.pause();
+                mAudioTrack.flush();
+            }
+        }
+        removeProgressMsg();
+    }
+
+    public void pause() {
+        ISPLAYSOUND = false;
+        if (mAudioTrack != null) {
+            mAudioTrack.pause();
+        }
+        removeProgressMsg();
+    }
+
+    public void resume() {
+        if (ISPLAYSOUND) {
+            return;
+        }
+        if (mAllAudioData == null || offsetAudioData == null) {
+            return;
+        }
+        Observable.create(new ObservableOnSubscribe<Boolean>() {
+                    @Override
+                    public void subscribe(@NonNull ObservableEmitter<Boolean> emitter) throws Throwable {
+                        startPlay(emitter);
+                    }
+                }).subscribeOn(Schedulers.newThread())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(new Observer<Boolean>() {
+                    @Override
+                    public void onSubscribe(@NonNull Disposable d) {
+
+                    }
+
+                    @Override
+                    public void onNext(@NonNull Boolean result) {
+                        if (result != null) {
+                            OnEventListener onEventListener = getEventListener();
+                            if (onEventListener != null) {
+                                onEventListener.onStartPlay();
+                            }
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull Throwable e) {
+                        e.printStackTrace();
+                    }
+
+                    @Override
+                    public void onComplete() {
+
+                    }
+                });
+    }
+
+    private void startPlay(ObservableEmitter<Boolean> emitter) {
+        ISPLAYSOUND = true;
+        mAudioTrack.play();
+        emitter.onNext(true);
+        WeightAudioMixer weightAudioMixer = new WeightAudioMixer(mWeghts);
+        byte[][] allAudioData2 = new byte[mAllAudioData.length][];
+        while (ISPLAYSOUND) {
+            int result = countBytesByPosition(bytesReadPos, allAudioData2);
+            bytesReadPos = result != -1 ? ++result : -1;
+            LOG.i(TAG, "bytesRead:" + bytesReadPos);
+            byte[] audiodata = weightAudioMixer.mixRawAudioBytes(allAudioData2);
+            mAudioTrack.write(audiodata, 0, minBufferSize);
+            if (bytesReadPos == -1) {
+                LOG.i(TAG, "读取完成:" + getPlayProgress());
+                sendPlayCompletedMsg(audiodata.length);
+                reset();
+                break;
+            } else {
+                countPlayProgress();
+            }
+        }
+    }
+
+    public interface OnEventListener {
+        void onStartPlay();
+
+        void onOffset(int oLength, int length);
+
+        void onPrepare(long totalDuration);
+
+        void onProgress(long currentPosition);
+
+    }
+
+    public void release() {
+        bytesReadPos = 0;
+        totalDuration = -1;
+        eventListener = null;
+        ISPLAYSOUND = false;
+        if (mHandler != null) {
+            mHandler.removeCallbacksAndMessages(null);
+        }
+        if (mAudioTrack != null) {
+            mAudioTrack.release();
+            mAudioTrack = null;
+        }
+    }
+}

+ 458 - 0
musicMerge/src/main/java/com/cooleshow/musicmerge/player/MultiAudioMixer.java

@@ -0,0 +1,458 @@
+package com.cooleshow.musicmerge.player;
+
+import android.util.Log;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+public abstract class MultiAudioMixer {
+	
+	private OnAudioMixListener mOnAudioMixListener;
+
+	/**
+	 * 创建默认的合成器
+	 * @return
+     */
+	public static MultiAudioMixer createDefaultAudioMixer(){
+		return createAddAudioMixer();
+	}
+
+	/**
+	 * 创建叠加合成器
+	 * @return
+	 */
+	public static MultiAudioMixer createAddAudioMixer(){
+		return new AddAudioMixer();
+	}
+
+	/**
+	 * 创建平均值合成器
+	 * @return
+     */
+	public static MultiAudioMixer createAverageAudioMixer(){
+		return new AverageAudioMixer();
+	}
+
+	/**
+	 * 创建权值合成器
+	 * @param weights
+	 * @return
+     */
+	public static MultiAudioMixer createWeightAudioMixer(float[] weights){
+		return new WeightAudioMixer(weights);
+	}
+
+	/**
+	 * 设置合成监听
+	 * @param l
+     */
+	public void setOnAudioMixListener(OnAudioMixListener l){
+		this.mOnAudioMixListener = l;
+	}
+	
+
+	/**
+	 * 合成音频
+	 *
+	 * @param rawAudioFiles 合成音频的列表
+     */
+	public void mixAudios(String[] rawAudioFiles){
+		
+		final int fileSize = rawAudioFiles.length;
+
+		FileInputStream[] audioFileStreams = new FileInputStream[fileSize];
+
+		FileInputStream inputStream;
+		byte[][] allAudioBytes = new byte[fileSize][];
+		boolean[] streamDoneArray = new boolean[fileSize];
+		final int bufferSize = 1024;
+		byte[] buffer = new byte[bufferSize];
+		int offset;
+		
+		try {
+			
+			for (int fileIndex = 0; fileIndex < fileSize; ++fileIndex) {
+				audioFileStreams[fileIndex] = new FileInputStream(rawAudioFiles[fileIndex]);
+			}
+
+			while(true){
+				
+				for(int streamIndex = 0 ; streamIndex < fileSize ; ++streamIndex){
+					
+					inputStream = audioFileStreams[streamIndex];
+					if(!streamDoneArray[streamIndex] && (offset = inputStream.read(buffer)) != -1){
+						allAudioBytes[streamIndex] = Arrays.copyOf(buffer,buffer.length);
+					}else{
+						streamDoneArray[streamIndex] = true;
+						allAudioBytes[streamIndex] = new byte[bufferSize];
+					}
+				}
+
+				byte[] mixBytes = mixRawAudioBytes(allAudioBytes);
+				if(mixBytes != null && mOnAudioMixListener != null){
+					mOnAudioMixListener.onMixing(mixBytes);
+				}
+				
+				boolean done = true;
+				for(boolean streamEnd : streamDoneArray){
+					if(!streamEnd){
+						done = false;
+					}
+				}
+				
+				if(done){
+					if(mOnAudioMixListener != null)
+						mOnAudioMixListener.onMixComplete();
+					break;
+				}
+			}
+			
+		} catch (IOException e) {
+			e.printStackTrace();
+			if(mOnAudioMixListener != null)
+				mOnAudioMixListener.onMixError(1);
+		}finally{
+			try {
+				for(FileInputStream in : audioFileStreams){
+					if(in != null)
+						in.close();
+				}
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}
+	}
+
+	/**
+	 * 合成音频, 可设置音频开始播放时间
+	 *
+	 * @param infoList 待合成的音频信息列表
+     */
+	public void mixAudios(List<ComposeInfo> infoList){
+
+		if(infoList == null || infoList.size() <= 0) return;
+
+		final int fileSize = infoList.size();
+
+		FileInputStream[] audioFileStreams = new FileInputStream[fileSize];
+
+		FileInputStream inputStream;
+		byte[][] allAudioBytes = new byte[fileSize][];
+		boolean[] streamDoneArray = new boolean[fileSize];
+		final int bufferSize = 1024;
+		byte[] buffer = new byte[bufferSize];
+		int offset;
+
+		int[] audioOffset = new int[fileSize];
+		for(int i=0; i<fileSize; i++){
+			audioOffset[i] = (int) (infoList.get(i).offsetSeconds * 16/8 * 2 * 44100);
+		}
+
+		try {
+
+			for (int fileIndex = 0; fileIndex < fileSize; ++fileIndex) {
+				audioFileStreams[fileIndex] = new FileInputStream(infoList.get(fileIndex).pcmPath);
+			}
+
+			while(true){
+
+				for(int streamIndex = 0 ; streamIndex < fileSize ; ++streamIndex){
+
+					inputStream = audioFileStreams[streamIndex];
+
+					//处理填充offset空白数据
+					int curOffset = audioOffset[streamIndex];
+					if(curOffset >= bufferSize){
+
+						//填充空白数据
+						allAudioBytes[streamIndex] = new byte[bufferSize];
+
+						audioOffset[streamIndex] = curOffset - bufferSize;
+
+						continue;
+
+					}else if(curOffset > 0 && curOffset < bufferSize){
+
+						//填充空白数据和读取的音频数据
+						byte[] data = new byte[bufferSize];
+
+						byte[] dataChild = new byte[bufferSize - curOffset];
+						inputStream.read(dataChild);
+
+						System.arraycopy(dataChild, 0, data, curOffset, dataChild.length);
+
+						allAudioBytes[streamIndex] = data;
+
+						audioOffset[streamIndex] = 0;
+
+						continue;
+
+					}
+
+					//处理文件流数据
+					if(!streamDoneArray[streamIndex] && (offset = inputStream.read(buffer)) != -1){
+						//填充音频数据
+						allAudioBytes[streamIndex] = Arrays.copyOf(buffer,buffer.length);
+					}else{
+						//填充空白数据
+						streamDoneArray[streamIndex] = true;
+						allAudioBytes[streamIndex] = new byte[bufferSize];
+					}
+				}
+
+				//合成数据
+				byte[] mixBytes = mixRawAudioBytes(allAudioBytes);
+
+				if(mixBytes != null && mOnAudioMixListener != null){
+					mOnAudioMixListener.onMixing(mixBytes);
+				}
+
+				boolean done = true;
+				for(boolean streamEnd : streamDoneArray){
+					if(!streamEnd){
+						done = false;
+					}
+				}
+
+				if(done){
+					//全部合成完成
+					if(mOnAudioMixListener != null)
+						mOnAudioMixListener.onMixComplete();
+					break;
+				}
+			}
+
+		} catch (Exception e) {
+			e.printStackTrace();
+
+			if(mOnAudioMixListener != null){
+				mOnAudioMixListener.onMixError(1);
+			}
+
+		}finally{
+			try {
+				for(FileInputStream in : audioFileStreams){
+					if(in != null)
+						in.close();
+				}
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}
+	}
+
+	/**
+	 * 合成音频数据
+	 * @param data
+	 * @return
+     */
+	public abstract byte[] mixRawAudioBytes(byte[][] data);
+
+	public interface OnAudioMixListener{
+		/**
+		 * 合成进行
+		 * @param mixBytes
+		 * @throws IOException
+         */
+		void onMixing(byte[] mixBytes) throws IOException;
+
+		/**
+		 * 合成错误
+		 * @param errorCode
+         */
+		void onMixError(int errorCode);
+		
+		/**
+		 * 合成完成
+		 */
+		void onMixComplete();
+	}
+	
+	/**
+	 * 叠加合成器
+	 * @author Darcy
+	 */
+	private static class AddAudioMixer extends MultiAudioMixer{
+
+		@Override
+		public byte[] mixRawAudioBytes(byte[][] bMulRoadAudioes) {
+			
+			if (bMulRoadAudioes == null || bMulRoadAudioes.length == 0)
+				return null;
+
+			byte[] realMixAudio = bMulRoadAudioes[0];
+			
+			if(bMulRoadAudioes.length == 1)
+				return realMixAudio;
+			
+			for(int rw = 0 ; rw < bMulRoadAudioes.length ; ++rw){
+				if(bMulRoadAudioes[rw].length != realMixAudio.length){
+					Log.e("app", "column of the road of audio + " + rw +" is diffrent.");
+					return null;
+				}
+			}
+
+			//row 代表参与合成的音频数量
+			//column 代表一段音频的采样点数,这里所有参与合成的音频的采样点数都是相同的
+			int row = bMulRoadAudioes.length;
+			int coloum = realMixAudio.length / 2;
+			short[][] sMulRoadAudioes = new short[row][coloum];
+
+			//PCM音频16位的存储是大端存储方式,即低位在前,高位在后,例如(X1Y1, X2Y2, X3Y3)数据,它代表的采样点数值就是((Y1 * 256 + X1), (Y2 * 256 + X2), (Y3 * 256 + X3))
+			for (int r = 0; r < row; ++r) {
+				for (int c = 0; c < coloum; ++c) {
+					sMulRoadAudioes[r][c] = (short) ((bMulRoadAudioes[r][c * 2] & 0xff) | (bMulRoadAudioes[r][c * 2 + 1] & 0xff) << 8);
+				}
+			}
+
+			short[] sMixAudio = new short[coloum];
+			int mixVal;
+			int sr = 0;
+			for (int sc = 0; sc < coloum; ++sc) {
+				mixVal = 0;
+				sr = 0;
+				//这里采取累加法
+				for (; sr < row; ++sr) {
+					mixVal += sMulRoadAudioes[sr][sc];
+				}
+				//最终值不能大于short最大值,因此可能出现溢出
+				sMixAudio[sc] = (short) (mixVal);
+			}
+
+			//short值转为大端存储的双字节序列
+			for (sr = 0; sr < coloum; ++sr) {
+				realMixAudio[sr * 2] = (byte) (sMixAudio[sr] & 0x00FF);
+				realMixAudio[sr * 2 + 1] = (byte) ((sMixAudio[sr] & 0xFF00) >> 8);
+			}
+
+			return realMixAudio;
+		}
+		
+	}
+
+	/**
+	 * 求平均值合成器
+	 * @author Darcy
+	 */
+	private static class AverageAudioMixer extends MultiAudioMixer{
+
+		@Override
+		public byte[] mixRawAudioBytes(byte[][] bMulRoadAudioes) {
+
+			if (bMulRoadAudioes == null || bMulRoadAudioes.length == 0)
+				return null;
+
+			byte[] realMixAudio = bMulRoadAudioes[0];
+
+			if(bMulRoadAudioes.length == 1)
+				return realMixAudio;
+
+			for(int rw = 0 ; rw < bMulRoadAudioes.length ; ++rw){
+				if(bMulRoadAudioes[rw].length != realMixAudio.length){
+					Log.e("app", "column of the road of audio + " + rw +" is diffrent.");
+					return null;
+				}
+			}
+
+			int row = bMulRoadAudioes.length;
+			int coloum = realMixAudio.length / 2;
+			short[][] sMulRoadAudioes = new short[row][coloum];
+
+			for (int r = 0; r < row; ++r) {
+				for (int c = 0; c < coloum; ++c) {
+					sMulRoadAudioes[r][c] = (short) ((bMulRoadAudioes[r][c * 2] & 0xff) | (bMulRoadAudioes[r][c * 2 + 1] & 0xff) << 8);
+				}
+			}
+
+			short[] sMixAudio = new short[coloum];
+			int mixVal;
+			int sr = 0;
+			for (int sc = 0; sc < coloum; ++sc) {
+				mixVal = 0;
+				sr = 0;
+				for (; sr < row; ++sr) {
+					mixVal += sMulRoadAudioes[sr][sc];
+				}
+				sMixAudio[sc] = (short) (mixVal / row);
+			}
+
+			for (sr = 0; sr < coloum; ++sr) {
+				realMixAudio[sr * 2] = (byte) (sMixAudio[sr] & 0x00FF);
+				realMixAudio[sr * 2 + 1] = (byte) ((sMixAudio[sr] & 0xFF00) >> 8);
+			}
+
+			return realMixAudio;
+		}
+
+	}
+
+	/**
+	 * 权重求值合成器
+	 * @author Darcy
+	 */
+	private static class WeightAudioMixer extends MultiAudioMixer{
+		private float[] weights;
+
+		public WeightAudioMixer(float[] weights){
+			this.weights = weights;
+		}
+
+		@Override
+		public byte[] mixRawAudioBytes(byte[][] bMulRoadAudioes) {
+
+			if (bMulRoadAudioes == null || bMulRoadAudioes.length == 0){
+				return null;
+			}
+
+			if(weights == null || weights.length != bMulRoadAudioes.length){
+				return null;
+			}
+
+			byte[] realMixAudio = bMulRoadAudioes[0];
+
+			if(bMulRoadAudioes.length == 1)
+				return realMixAudio;
+
+			for(int rw = 0 ; rw < bMulRoadAudioes.length ; ++rw){
+				if(bMulRoadAudioes[rw].length != realMixAudio.length){
+					Log.e("app", "column of the road of audio + " + rw +" is diffrent.");
+					return null;
+				}
+			}
+
+			int row = bMulRoadAudioes.length;
+			int coloum = realMixAudio.length / 2;
+			short[][] sMulRoadAudioes = new short[row][coloum];
+
+			for (int r = 0; r < row; ++r) {
+				for (int c = 0; c < coloum; ++c) {
+					sMulRoadAudioes[r][c] = (short) ((bMulRoadAudioes[r][c * 2] & 0xff) | (bMulRoadAudioes[r][c * 2 + 1] & 0xff) << 8);
+				}
+			}
+
+			short[] sMixAudio = new short[coloum];
+			int mixVal;
+			int sr = 0;
+			for (int sc = 0; sc < coloum; ++sc) {
+				mixVal = 0;
+				sr = 0;
+				for (; sr < row; ++sr) {
+					mixVal += sMulRoadAudioes[sr][sc] * weights[sr];
+				}
+				sMixAudio[sc] = (short) (mixVal);
+			}
+
+			for (sr = 0; sr < coloum; ++sr) {
+				realMixAudio[sr * 2] = (byte) (sMixAudio[sr] & 0x00FF);
+				realMixAudio[sr * 2 + 1] = (byte) ((sMixAudio[sr] & 0xFF00) >> 8);
+			}
+
+			return realMixAudio;
+		}
+
+	}
+
+}
+

+ 72 - 0
musicMerge/src/main/java/com/cooleshow/musicmerge/player/WeightAudioMixer.java

@@ -0,0 +1,72 @@
+package com.cooleshow.musicmerge.player;
+
+import android.util.Log;
+
+import com.cooleshow.base.utils.LOG;
+
+/**
+ * 权重求值合成器
+ * Author by pq, Date on 2024/6/14.
+ */
+public class WeightAudioMixer extends MultiAudioMixer{
+    private float[] weights;
+
+    public WeightAudioMixer(float[] weights) {
+        this.weights = weights;
+    }
+
+    @Override
+    public byte[] mixRawAudioBytes(byte[][] bMulRoadAudioes) {
+
+        if (bMulRoadAudioes == null || bMulRoadAudioes.length == 0) {
+            return null;
+        }
+        LOG.i("bMulRoadAudioes length:" + bMulRoadAudioes.length);
+        if (weights == null || weights.length != bMulRoadAudioes.length) {
+            return null;
+        }
+
+        byte[] realMixAudio = bMulRoadAudioes[0];
+
+        if (bMulRoadAudioes.length == 1)
+            return realMixAudio;
+
+        for (int rw = 0; rw < bMulRoadAudioes.length; ++rw) {
+            LOG.i("mixRawAudioBytes:" + bMulRoadAudioes[rw].length);
+            if (bMulRoadAudioes[rw].length != realMixAudio.length) {
+                Log.e("app", "column of the road of audio + " + rw + " is diffrent.");
+                return null;
+            }
+        }
+
+        int row = bMulRoadAudioes.length;
+        int coloum = realMixAudio.length / 2;
+        short[][] sMulRoadAudioes = new short[row][coloum];
+
+        for (int r = 0; r < row; ++r) {
+            for (int c = 0; c < coloum; ++c) {
+                sMulRoadAudioes[r][c] = (short) ((bMulRoadAudioes[r][c * 2] & 0xff) | (bMulRoadAudioes[r][c * 2 + 1] & 0xff) << 8);
+            }
+        }
+
+        short[] sMixAudio = new short[coloum];
+        int mixVal;
+        int sr = 0;
+        for (int sc = 0; sc < coloum; ++sc) {
+            mixVal = 0;
+            sr = 0;
+            for (; sr < row; ++sr) {
+                mixVal += sMulRoadAudioes[sr][sc] * weights[sr];
+            }
+//                sMixAudio[sc] = (short) (mixVal);
+            sMixAudio[sc] = (short) (mixVal / row);
+        }
+
+        for (sr = 0; sr < coloum; ++sr) {
+            realMixAudio[sr * 2] = (byte) (sMixAudio[sr] & 0x00FF);
+            realMixAudio[sr * 2 + 1] = (byte) ((sMixAudio[sr] & 0xFF00) >> 8);
+        }
+
+        return realMixAudio;
+    }
+}

+ 7 - 3
musicMerge/src/main/java/com/cooleshow/musicmerge/ui/MusicHandleActivity.java

@@ -55,6 +55,7 @@ import com.cooleshow.musicmerge.bean.MusicDataBean;
 import com.cooleshow.musicmerge.bean.MusicInfoBean;
 import com.cooleshow.musicmerge.bean.MusicMergeConfigBean;
 import com.cooleshow.musicmerge.callback.ResultCallback;
+import com.cooleshow.musicmerge.constants.MergeConfig;
 import com.cooleshow.musicmerge.contract.MusicFileHandleContract;
 import com.cooleshow.musicmerge.databinding.AcMusicHandleLayoutBinding;
 import com.cooleshow.musicmerge.helper.MixHelper;
@@ -78,7 +79,8 @@ import androidx.lifecycle.ViewModelProvider;
 /**
  * Author by pq, Date on 2023/8/28.
  */
-@Route(path = RouterPath.MusicTuner.MUSIC_MERGE_PAGE)
+@Deprecated
+//@Route(path = RouterPath.MusicTuner.MUSIC_MERGE_PAGE)
 public class MusicHandleActivity extends BaseMVPActivity<AcMusicHandleLayoutBinding, MusicFileHandlePresenter> implements View.OnClickListener, MusicFileHandleContract, TextureView.SurfaceTextureListener, UMShareListener {
     public static final int REQUEST_CODE_LOCAL = 0x19;
     public static final int REQUEST_CODE_LOCAL_VIDEO_COVER = 0x20;
@@ -120,6 +122,8 @@ public class MusicHandleActivity extends BaseMVPActivity<AcMusicHandleLayoutBind
     private int defaultDelay = 0;//此字段用于录音文件前面缓冲以及设备延迟造成的空白延迟字段
     private int evaluateDelay = 0;//此字段记录给IOS使用
 
+    private float accompanyPlaySpeed = MergeConfig.DEFAULT_PLAY_SPEED;
+
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -1070,7 +1074,7 @@ public class MusicHandleActivity extends BaseMVPActivity<AcMusicHandleLayoutBind
             return;
         }
         if (player1 != null && player1.getPlayer() != null && isPlay) {
-            viewBinding.musicFrequencyView.setMediaPlayer(player1.getPlayer());
+            viewBinding.musicFrequencyView.setMediaPlayer(player1.getPlayer().getAudioSessionId());
         }
         rotation(isPlay);
         rotationAlbum(isPlay);
@@ -1207,7 +1211,7 @@ public class MusicHandleActivity extends BaseMVPActivity<AcMusicHandleLayoutBind
                 if (!TextUtils.isEmpty(jsonConfig)) {
                     toApplyConfig(jsonConfig);
                 }
-                mSettingFragment.setAccompanyUrl(accompanyUrl);
+                mSettingFragment.setAccompanyUrl(accompanyUrl,accompanyPlaySpeed);
             }
             //这里为了兼容IOS录制的wav音频文件格式不正确 导致合成失败的问题Failed to read frame size: Could not seek to 1026.
             //取服务端存储的文件

+ 1674 - 0
musicMerge/src/main/java/com/cooleshow/musicmerge/ui/MusicHandleActivity_.java

@@ -0,0 +1,1674 @@
+package com.cooleshow.musicmerge.ui;
+
+import android.animation.ObjectAnimator;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.graphics.drawable.Drawable;
+import android.media.MediaPlayer;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+import android.view.animation.LinearInterpolator;
+import android.widget.FrameLayout;
+import android.widget.SeekBar;
+
+import com.alibaba.android.arouter.facade.annotation.Route;
+import com.alibaba.android.arouter.launcher.ARouter;
+import com.cooleshow.base.bean.ShareIntentBean;
+import com.cooleshow.base.common.WebConstants;
+import com.cooleshow.base.constanst.Constants;
+import com.cooleshow.base.constanst.ShareType;
+import com.cooleshow.base.constanst.UploadConstants;
+import com.cooleshow.base.router.RouterPath;
+import com.cooleshow.base.ui.activity.BaseMVPActivity;
+import com.cooleshow.base.utils.ClipboardUtils;
+import com.cooleshow.base.utils.FileUtils;
+import com.cooleshow.base.utils.GlideUtils;
+import com.cooleshow.base.utils.GsonUtils;
+import com.cooleshow.base.utils.JumpUtils;
+import com.cooleshow.base.utils.LOG;
+import com.cooleshow.base.utils.MyFileUtils;
+import com.cooleshow.base.utils.NumberUtils;
+import com.cooleshow.base.utils.SizeUtils;
+import com.cooleshow.base.utils.TimeUtils;
+import com.cooleshow.base.utils.ToastUtil;
+import com.cooleshow.base.utils.UrlUtils;
+import com.cooleshow.base.utils.helper.CommonShareHelper;
+import com.cooleshow.base.utils.helper.QMUIStatusBarHelper;
+import com.cooleshow.base.utils.helper.upload.UploadHelper;
+import com.cooleshow.base.widgets.dialog.CommonConfirmDialog2;
+import com.cooleshow.base.widgets.dialog.ShareDialog;
+import com.cooleshow.musicmerge.R;
+import com.cooleshow.musicmerge.bean.MusicDataBean;
+import com.cooleshow.musicmerge.bean.MusicInfoBean;
+import com.cooleshow.musicmerge.bean.MusicMergeConfigBean;
+import com.cooleshow.musicmerge.callback.ResultCallback;
+import com.cooleshow.musicmerge.constants.MergeConfig;
+import com.cooleshow.musicmerge.constants.MusicMergeConfig;
+import com.cooleshow.musicmerge.contract.MusicFileHandleContract;
+import com.cooleshow.musicmerge.databinding.AcMusicHandleLayoutBinding;
+import com.cooleshow.musicmerge.helper.MixHelper;
+import com.cooleshow.musicmerge.player.CustomPlayer;
+import com.cooleshow.musicmerge.player.MergeTrackManager;
+import com.cooleshow.musicmerge.presenter.MusicFileHandlePresenter;
+import com.cooleshow.musicmerge.viewmodel.MusicMergeViewModel;
+import com.cooleshow.musicmerge.widget.MergeLoadingTipDialog;
+import com.luck.picture.lib.PictureSelector;
+import com.luck.picture.lib.entity.LocalMedia;
+import com.umeng.socialize.UMShareAPI;
+import com.umeng.socialize.UMShareListener;
+import com.umeng.socialize.bean.SHARE_MEDIA;
+
+import java.io.File;
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.ViewModelProvider;
+
+/**
+ * Author by pq, Date on 2023/8/28.
+ */
+@Route(path = RouterPath.MusicTuner.MUSIC_MERGE_PAGE)
+public class MusicHandleActivity_ extends BaseMVPActivity<AcMusicHandleLayoutBinding, MusicFileHandlePresenter> implements View.OnClickListener, MusicFileHandleContract, TextureView.SurfaceTextureListener, UMShareListener {
+    public static final int REQUEST_CODE_LOCAL = 0x19;
+    public static final int REQUEST_CODE_LOCAL_VIDEO_COVER = 0x20;
+    public static final int REQUEST_CODE_VIDEO_COVER = 0x29;
+
+    public static final int MAX_ADJUSTMENT = 10;
+    private CustomPlayer player2;
+    private String accompanyUrl;
+    private String recordFilePath;
+    private String recordWavFilePath;
+    private MusicHandleSettingFragment mSettingFragment;
+    private boolean isVideo;
+    private TextureView mSurfaceView;
+    private SurfaceTexture mSurfaceTexture;
+    private int videoWidth;
+    private int videoHeight;
+    private Handler mHandler = new Handler(Looper.getMainLooper());
+    private String mRecordId;
+    private String worksId;
+    private String imgCover;
+    private String des;
+    private ObjectAnimator mRotateAnimation;
+    private ObjectAnimator mAlbumRotationAnimator;
+    private String originalFileUrl = "";
+    private String mTitle;
+    private MusicMergeViewModel mViewModel;
+
+    private boolean isNeedFinishPage = false;
+
+    private boolean isNeedResetScreenOrientation = true;
+    private MergeLoadingTipDialog mLoadingTipDialog;
+
+    private static int MAX_STEP = 3;
+    private static final int MAX_PROGRESS = 100;
+    private int currentStep = 0;
+
+    private long audioPlayDuration = 0;
+    private long videoPlayDuration = 0;
+
+    private int defaultDelay = 0;//此字段用于录音文件前面缓冲以及设备延迟造成的空白延迟字段
+    private int evaluateDelay = 0;//此字段记录给IOS使用
+
+    private float accompanyPlaySpeed = MergeConfig.DEFAULT_PLAY_SPEED;
+    private MergeTrackManager audioPlayer;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        QMUIStatusBarHelper.hideStatusBar(this);
+    }
+
+    @Override
+    protected void initView() {
+        mTitle = getIntent().getStringExtra("title");
+        viewBinding.tvTitle.setText(mTitle);
+        int c_orientation = getIntent().getIntExtra("c_orientation", ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+        isNeedResetScreenOrientation = c_orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+
+    }
+
+    private void initFragment() {
+        mSettingFragment = new MusicHandleSettingFragment();
+        Bundle bundle = new Bundle();
+        bundle.putString("accompanyUrl", accompanyUrl);
+        bundle.putFloat(MusicMergeConfig.SPEEDRATE_KEY, accompanyPlaySpeed);
+        bundle.putInt("defaultDelay", defaultDelay);
+        mSettingFragment.setArguments(bundle);
+        getSupportFragmentManager().beginTransaction().replace(R.id.fl_setting, mSettingFragment).commitAllowingStateLoss();
+    }
+
+    private void initSurfaceView() {
+        initVideoUIStyle();
+        mViewModel.getVideoFilePath().setValue(recordFilePath);
+        viewBinding.groupAudioView.setVisibility(View.GONE);
+        viewBinding.flSurface.setVisibility(View.VISIBLE);
+        viewBinding.viewVideoTopBg.setVisibility(View.VISIBLE);
+        viewBinding.viewVideoBottomBg.setVisibility(View.VISIBLE);
+        viewBinding.viewVideoBg.setVisibility(View.VISIBLE);
+        mSurfaceView = new TextureView(MusicHandleActivity_.this);
+        mSurfaceView.setSurfaceTextureListener(this);
+        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
+        layoutParams.gravity = Gravity.CENTER;
+        viewBinding.flSurface.addView(mSurfaceView, layoutParams);
+    }
+
+    private void initVideoUIStyle() {
+        viewBinding.tvTitle.setTextColor(Color.WHITE);
+        viewBinding.ivBack.setImageResource(com.cooleshow.base.R.drawable.ic_back_white);
+        viewBinding.ivPlay.setImageResource(R.drawable.icon_music_merge_play_white);
+        viewBinding.tvCurrentProgress.setTextColor(Color.WHITE);
+        viewBinding.tvTotalProgress.setTextColor(Color.WHITE);
+        viewBinding.seekPlay.setThumb(getResources().getDrawable(R.drawable.shape_volume_progreesbar_thumb2));
+        setSeekBarProgressDrawable(viewBinding.seekPlay, getResources().getDrawable(R.drawable.shape_play_progress_seekbar_bg2));
+
+        viewBinding.tvTotalProgress2.setTextColor(Color.WHITE);
+        viewBinding.seekPlay2.setThumb(getResources().getDrawable(R.drawable.shape_volume_progreesbar_thumb2));
+        viewBinding.tvCurrentProgress2.setTextColor(Color.WHITE);
+        setSeekBarProgressDrawable(viewBinding.seekPlay2, getResources().getDrawable(R.drawable.shape_play_progress_seekbar_bg2));
+    }
+
+    private void setSeekBarProgressDrawable(SeekBar seekBarProgress, Drawable drawable) {
+        Rect bounds = seekBarProgress.getProgressDrawable().getBounds();
+        seekBarProgress.setProgressDrawable(drawable);
+        seekBarProgress.getProgressDrawable().setBounds(bounds);
+    }
+
+    @Override
+    public void initData() {
+        super.initData();
+        mRecordId = getIntent().getStringExtra("recordId");
+        worksId = getIntent().getStringExtra("worksId");
+        imgCover = getIntent().getStringExtra("coverImg");
+        defaultDelay = Math.abs(getIntent().getIntExtra("defaultDelay", 0));
+        evaluateDelay = Math.abs(getIntent().getIntExtra("evaluateDelay", 0));
+        accompanyPlaySpeed = getIntent().getFloatExtra(MusicMergeConfig.SPEEDRATE_KEY, MergeConfig.DEFAULT_PLAY_SPEED);
+        loadCover();
+        if (TextUtils.isEmpty(mRecordId)) {
+            ToastUtil.getInstance().showShort("作品生成失败");
+            finish();
+            return;
+        }
+        accompanyUrl = getIntent().getStringExtra("accompanyUrl");
+        recordFilePath = getIntent().getStringExtra("recordFilePath");
+        initLoadingDialog();
+        initViewModel();
+        initFragment();
+        initListener();
+        initPlayer();
+        LOG.i("recordFilePath:" + recordFilePath);
+        LOG.i("accompanyUrl:" + accompanyUrl);
+        if (!checkRecordFile()) {
+            if (!TextUtils.isEmpty(worksId)) {
+                presenter.getDetail(worksId);
+            } else {
+                ToastUtil.getInstance().showShort("作品生成失败");
+                finish();
+            }
+        } else {
+            preLoad();
+        }
+    }
+
+    private void initViewModel() {
+        ViewModelProvider.AndroidViewModelFactory instance =
+                ViewModelProvider.AndroidViewModelFactory
+                        .getInstance(getApplication());
+        mViewModel = new ViewModelProvider(this, instance)
+                .get(MusicMergeViewModel.class);
+        mViewModel.getWorksId().setValue(worksId);
+        refreshMusicInfo(imgCover);
+    }
+
+    private void preLoad() {
+        isVideo = MyFileUtils.isVideo(recordFilePath);
+        if (isVideo) {
+            if (TextUtils.isEmpty(recordWavFilePath)) {
+                //本地视频的模式
+                String wavFileSavePath = MixHelper.getInstance().getWavFileSavePath(recordFilePath, MyFileUtils.WAV_FILE_SUFFIX);
+                File videoAudioFile = new File(wavFileSavePath);
+                if (!videoAudioFile.exists()) {
+                    toCreateWavFile();
+                    return;
+                }
+                recordWavFilePath = videoAudioFile.getAbsolutePath();
+            }
+        } else {
+            recordWavFilePath = recordFilePath;
+        }
+        LOG.i("isVideo:" + isVideo);
+        mViewModel.getIsVideoFile().setValue(isVideo);
+        boolean b = checkAccompanimentMp3File();
+        if (b) {
+            preparePlay();
+        }
+    }
+
+    private void toCreateWavFile() {
+        setLoadingCancelable(false);
+        showLoading("草稿处理中");
+        String wavFileSavePath = MixHelper.getInstance().getWavFileSavePath(recordFilePath, MyFileUtils.WAV_FILE_SUFFIX);
+        MixHelper.getInstance().getWavFromMp4(recordFilePath, wavFileSavePath, new ResultCallback<String>() {
+            @Override
+            public void onSuccess(String s) {
+                if (!checkActivityExist()) {
+                    return;
+                }
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        hideLoading();
+                        if (!TextUtils.isEmpty(s)) {
+                            recordWavFilePath = s;
+                            preLoad();
+                        } else {
+                            ToastUtil.getInstance().showShort("草稿文件处理失败,请退出重试");
+                        }
+                    }
+                });
+            }
+
+            @Override
+            public void onProgress(int progressPercent) {
+
+            }
+
+            @Override
+            public void onFail(int errorCode, String errorStr) {
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        hideLoading();
+                        ToastUtil.getInstance().showShort("草稿文件处理失败,请退出重试");
+                    }
+                });
+            }
+        });
+    }
+
+    private void preparePlay() {
+        if (isVideo) {
+            initSurfaceView();
+        } else {
+            toPlay(getAccompanyPath());
+        }
+    }
+
+    private void loadCover() {
+        GlideUtils.INSTANCE.loadImage(this, imgCover, viewBinding.ivCover, R.drawable.icon_default_music_song_cover);
+    }
+
+    @Override
+    protected MusicFileHandlePresenter createPresenter() {
+        return new MusicFileHandlePresenter();
+    }
+
+    private void initListener() {
+        viewBinding.ivBack.setOnClickListener(this);
+        viewBinding.ivPlay.setOnClickListener(this);
+        viewBinding.ivUnfoldSentting.setOnClickListener(this);
+
+        mSettingFragment.setEventListener(new MusicHandleSettingFragment.OnEventListener() {
+            @Override
+            public void onVolumeChange(int type, float value) {
+                if (type == MusicHandleSettingFragment.ACCOMPANY_TYPE) {
+                    getAudioPlayer().setWeight(0, value);
+                } else {
+                    getAudioPlayer().setWeight(1, value);
+                }
+            }
+
+            @Override
+            public void onOffsetValueChange(int value) {
+                aligningAccompany(value);
+            }
+
+            @Override
+            public void toMix(int offsetValue, float volume1, float volume2, boolean isNeedNotify) {
+                MAX_STEP = 3;
+                checkCoverToUpload();
+                if (isVideo) {
+                    startMixForMp4(getAccompanyPath(), recordFilePath, offsetValue, volume1, volume2, isNeedNotify);
+                } else {
+                    startMix(getAccompanyPath(), recordFilePath, offsetValue, volume1, volume2, isNeedNotify);
+                }
+            }
+
+            @Override
+            public void hideSetting() {
+                handleSettingVisibility();
+            }
+
+            @Override
+            public void saveDraft() {
+                //上传原始视频 以及 相关调节参数
+                toSaveDraft();
+            }
+        });
+        viewBinding.seekPlay.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                if (fromUser) {
+                }
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+                handleSeekEvent();
+            }
+        });
+    }
+
+    private long getAudioTotalDuration() {
+        return getAudioPlayer().getTotalDuration();
+//        if (player2 != null) {
+//            int total = player2.getTotal();
+//            LOG.i("total:" + total);
+//            if (total <= 0) {
+//                return 0;
+//            }
+//            int delayValue = getDelayValue();
+//            int lastResult;
+//            if (delayValue >= 0) {
+//                lastResult = total - delayValue;
+//            } else {
+//                lastResult = total + Math.abs(delayValue);
+//            }
+//            LOG.i("lastResult:" + lastResult);
+//            return lastResult;
+//        }
+//        return 0;
+    }
+
+    private int getDelayValue() {
+        int currentOffsetValue = mSettingFragment.getCurrentOffsetValue();
+        LOG.i("currentOffsetValue:" + currentOffsetValue);
+        return currentOffsetValue;
+    }
+
+    private void checkCoverToUpload() {
+        MusicInfoBean value = mViewModel.getMusicInfoLiveData().getValue();
+        if (value != null) {
+            String preCover = value.getPreCover();
+            String videoCover = value.getVideoCover();
+            LOG.i("preCover:" + preCover);
+            LOG.i("videoCover:" + videoCover);
+            if (!TextUtils.isEmpty(preCover)) {
+                presenter.upLoadImage(null, preCover, false);
+            }
+            if (!TextUtils.isEmpty(videoCover)) {
+                presenter.upLoadImage(null, videoCover, true);
+            }
+        }
+    }
+
+    private void toSaveDraft() {
+        if (!TextUtils.isEmpty(recordFilePath)) {
+            if (!TextUtils.isEmpty(originalFileUrl)) {
+                MAX_STEP = 1;
+                toShowLoading(0, getString(R.string.save_draft_tip));
+                toNotifyDraft(originalFileUrl);
+            } else {
+                MAX_STEP = 2;
+                uploadDraft(recordFilePath);
+            }
+        }
+    }
+
+    private void uploadDraft(String filePath) {
+        currentStep = 0;
+        File file = new File(filePath);
+        if (!file.exists()) {
+            return;
+        }
+        toSetLoadingCancelable(false);
+        toShowLoading(getCurrentProgress(0), getString(R.string.updload_draft_tip));
+        UploadHelper uploadHelper = new UploadHelper(null, UploadConstants.UPLOAD_TYPE_HOMEWORK);
+        uploadHelper.setUpLoadCallBack(new UploadHelper.UpLoadCallBack() {
+            @Override
+            protected void onSuccess(String url) {
+                toUpdateLoadingText(getCurrentProgress(100), getString(R.string.updload_draft_tip));
+                toNotifyDraft(url);
+            }
+
+            @Override
+            protected void onFailure() {
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        hideLoading();
+                        ToastUtil.getInstance().showShort("上传失败,请重试");
+                    }
+                });
+            }
+
+            @Override
+            public void onUploadProgress(double v) {
+                Log.i("pq", "onUploadProgress" + v);
+//                UiUtils.convertDouble(v)
+                toUpdateLoadingText(getCurrentProgress((int) v), getString(R.string.updload_draft_tip));
+            }
+        });
+        uploadHelper.setLoadingTip(getString(R.string.updload_draft_tip));
+
+        uploadHelper.uploadFile(file);
+    }
+
+    private void upload(String filePath) {
+        currentStep = 1;
+        File file = new File(filePath);
+        if (!file.exists()) {
+            return;
+        }
+        toUpdateLoadingText(getCurrentProgress(0), getString(R.string.upload_works_tip));
+        UploadHelper uploadHelper = new UploadHelper(null, UploadConstants.UPLOAD_TYPE_HOMEWORK);
+        uploadHelper.setUpLoadCallBack(new UploadHelper.UpLoadCallBack() {
+            @Override
+            protected void onSuccess(String url) {
+                toUpdateLoadingText(getCurrentProgress(100), getString(R.string.upload_works_tip));
+                toNotify(url);
+            }
+
+            @Override
+            protected void onFailure() {
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        hideLoading();
+                        ToastUtil.getInstance().showShort("上传失败,请重试");
+                    }
+                });
+            }
+
+            @Override
+            public void onUploadProgress(double v) {
+                toUpdateLoadingText(getCurrentProgress((int) v), getString(R.string.upload_works_tip));
+            }
+        });
+        uploadHelper.setLoadingTip(getString(R.string.upload_works_tip));
+
+        uploadHelper.uploadFile(file);
+    }
+
+    private void toNotify(String url) {
+        String configJson = mSettingFragment.getConfigJson(defaultDelay, evaluateDelay);
+        MusicInfoBean value = mViewModel.getMusicInfoLiveData().getValue();
+        String videoCover = "";
+        String cover = imgCover;
+        if (value != null) {
+            des = value.getDes();
+            String videoCoverResult = value.getVideoCover();
+            if (UrlUtils.isValidUrl(videoCoverResult)) {
+                videoCover = videoCoverResult;
+            }
+            String cover1 = value.getCover();
+            if (!TextUtils.isEmpty(cover1)) {
+                cover = cover1;
+            }
+        }
+        presenter.save(mRecordId, url, cover, videoCover, des, configJson);
+    }
+
+    private void toNotifyDraft(String url) {
+        if (mSettingFragment != null) {
+            String configJson = mSettingFragment.getConfigJson(defaultDelay, evaluateDelay);
+            presenter.saveDraft(mRecordId, url, accompanyUrl, imgCover, configJson);
+        }
+    }
+
+    public void startMixForMp4(String accompanimentMp3Path, String recordFilePath, int offsetValue, float recordFileVolume, float accompanyFileVolume, boolean isNeedNotify) {
+        currentStep = 0;
+        toSetLoadingCancelable(false);
+        toShowLoading(getCurrentProgress(0), getString(R.string.video_merge_tip));
+        MixHelper.getInstance().startMixForMp4(accompanimentMp3Path, recordFilePath, offsetValue, recordFileVolume, accompanyFileVolume, new ResultCallback<String>() {
+            @Override
+            public void onSuccess(String s) {
+                if (!checkActivityExist()) {
+                    return;
+                }
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        toUpdateLoadingText(getCurrentProgress(100), getString(R.string.video_merge_tip));
+                        if (!TextUtils.isEmpty(s)) {
+                            if (isNeedNotify) {
+                                upload(s);
+                            } else {
+                                FileUtils.notifySystemToScan(s);
+                                ToastUtil.getInstance().showShort("保存成功");
+                            }
+                        } else {
+                            hideLoading();
+                            ToastUtil.getInstance().showShort("mix onFail");
+                        }
+                    }
+                });
+            }
+
+            @Override
+            public void onProgress(int progressPercent) {
+                if (!checkActivityExist()) {
+                    return;
+                }
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (currentStep == 0) {//这个进度有延迟,可能不准确,如果到了下一个步骤了就不再触发更新
+                            LOG.i("progressPercent:" + progressPercent);
+                            toUpdateLoadingText(getCurrentProgress(progressPercent), getString(R.string.video_merge_tip));
+                        }
+                    }
+                });
+            }
+
+            @Override
+            public void onFail(int errorCode, String errorStr) {
+                if (!checkActivityExist()) {
+                    return;
+                }
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        hideLoading();
+                        ToastUtil.getInstance().showShort("mix onFail:" + errorCode + "--reason:" + errorStr);
+                    }
+                });
+            }
+        });
+    }
+
+    private void startMix(String accompanimentMp3Path, String recordFilePath, int offsetValue, float recordFileVolume, float accompanyFileVolume, boolean isNeedNotify) {
+        currentStep = 0;
+        toSetLoadingCancelable(false);
+        toShowLoading(getCurrentProgress(0), getString(R.string.audio_merge_tip));
+        MixHelper.getInstance().startMix(accompanimentMp3Path, recordFilePath, offsetValue, recordFileVolume, accompanyFileVolume, new ResultCallback<String>() {
+            @Override
+            public void onSuccess(String s) {
+                if (!checkActivityExist()) {
+                    return;
+                }
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        toUpdateLoadingText(getCurrentProgress(100), getString(R.string.audio_merge_tip));
+                        if (!TextUtils.isEmpty(s)) {
+                            if (isNeedNotify) {
+                                upload(s);
+                            } else {
+                                FileUtils.notifySystemToScan(s);
+                                ToastUtil.getInstance().showShort("保存成功");
+                            }
+                        } else {
+                            hideLoading();
+                            ToastUtil.getInstance().showShort("mix onFail");
+                        }
+                    }
+                });
+            }
+
+            @Override
+            public void onProgress(int progressPercent) {
+                if (!checkActivityExist()) {
+                    return;
+                }
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (currentStep == 0) {
+                            LOG.i("progressPercent:" + progressPercent);
+                            toUpdateLoadingText(getCurrentProgress(progressPercent), getString(R.string.audio_merge_tip));
+                        }
+                    }
+                });
+            }
+
+            @Override
+            public void onFail(int errorCode, String errorStr) {
+                if (!checkActivityExist()) {
+                    return;
+                }
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        hideLoading();
+                        ToastUtil.getInstance().showShort("mix onFail:" + errorCode + "--reason:" + errorStr);
+                    }
+                });
+            }
+        });
+    }
+
+    private void aligningAccompany(int value) {
+        long cu = getAudioPlayer().getPlayProgress();
+        LOG.i("pq", "cu:" + cu);
+        LOG.i("pq", "currentOffsetValue:" + value);
+        int seekResult2 = countAccompanyPosition(cu);
+        LOG.i("pq", "seekResult2:" + seekResult2);
+        player2.seekTo(seekResult2);
+        getAudioPlayer().setPositionOffset(1, getDelayValue());
+    }
+
+    private void handleSeekEvent() {
+        int progress = viewBinding.seekPlay.getProgress();
+        float percent = progress * 1.0f / viewBinding.seekPlay.getMax();
+        int seekResult = (int) (getAudioTotalDuration() * percent);
+        LOG.i("pq", "seekResult:" + seekResult);
+        getAudioPlayer().seekPercent(percent);
+
+        if (isVideo) {
+            int seekResult2 = countAccompanyPosition(seekResult);
+            LOG.i("pq", "seekResult2:" + seekResult2);
+            player2.seekTo(seekResult2);
+        }
+    }
+
+    private int countAccompanyPosition(long recordPosition) {
+        int delayValue = getDelayValue();
+        LOG.i("pq", "currentOffsetValue:" + delayValue);
+        int exceptPosition = (int) (recordPosition + delayValue);
+        return exceptPosition;
+    }
+
+
+    private void initPlayer() {
+        audioPlayer = new MergeTrackManager();
+        audioPlayer.init(new MergeTrackManager.OnEventListener() {
+            @Override
+            public void onStartPlay() {
+                updatePlayStatus();
+            }
+
+            @Override
+            public void onOffset(int i, int length) {
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (!checkActivityExist()) {
+                            return;
+                        }
+                        viewBinding.tvDelayText.setText("延迟值:" + getDelayValue() + "\n" + "处理前的长度为:" + i + "\n" + "处理后的长度为:" + length);
+                    }
+                });
+            }
+
+            @Override
+            public void onPrepare(long totalDuration) {
+                audioPlayDuration = totalDuration;
+                setDurationText();
+                if (isVideo) {
+                    toRealPlayVideo();
+                }
+            }
+
+            @Override
+            public void onProgress(long currentPosition) {
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (!checkActivityExist()) {
+                            return;
+                        }
+                        setRecordProgress(currentPosition);
+                        if (currentPosition >= getAudioTotalDuration()) {
+                            handleCompletedStatus();
+                        }
+                    }
+                });
+            }
+        });
+        player2 = new CustomPlayer("luzhi");
+        //演奏播放器
+        player2.setOnEventListener(new CustomPlayer.OnEventListener() {
+            @Override
+            public void onProgress(int progress) {
+                if (!checkActivityExist()) {
+                    return;
+                }
+                toShowDelay();
+            }
+
+            @Override
+            public void onPrepared(int duration) {
+                videoPlayDuration = duration;
+                toRealPlayVideo();
+            }
+
+            @Override
+            public void onCompleted() {
+//                handleCompletedStatus();
+            }
+
+            @Override
+            public void onError() {
+
+            }
+
+            @Override
+            public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
+                MusicHandleActivity_.this.videoWidth = width;
+                MusicHandleActivity_.this.videoHeight = height;
+                resetVideoSize(width, height);
+            }
+        });
+    }
+
+    private void handleCompletedStatus() {
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                setRecordProgress(0);
+                viewBinding.seekPlay.setProgress(0);
+                getAudioPlayer().stop();
+                if (isVideo) {
+                    player2.pause();
+                    player2.seekTo(0);
+                }
+                updatePlayStatus();
+            }
+        }, 100);
+    }
+
+    private void setRecordProgress(long progress) {
+//        LOG.i("pq", "setRecordProgress:" + progress);
+        String s = TimeUtils.msToTime(progress);
+        viewBinding.tvCurrentProgress.setText(s);
+        int maxProgress = viewBinding.seekPlay.getMax();
+        int percent = (int) ((progress * 1.0f / getAudioTotalDuration()) * maxProgress);
+//        LOG.i("pq", "setRecordProgress percent:" + percent);
+        viewBinding.seekPlay.setProgress(percent);
+    }
+
+    private void setDurationText() {
+        long recordTotalDuration = getAudioTotalDuration();
+        if (recordTotalDuration > 0) {
+            String s = TimeUtils.msToTime(recordTotalDuration);
+            viewBinding.tvTotalProgress.setText(s);
+        }
+    }
+
+    /**
+     * 比较进度,进行校准
+     */
+    private void toShowDelay() {
+        if (!isVideo) {
+            return;
+        }
+        if (!getAudioPlayer().isPlaying()) {
+            return;
+        }
+        long cu = getAudioPlayer().getPlayProgress();
+        long cu1 = player2.getCu();
+        int delayValue = getDelayValue();
+        long expectPlayer2Delay = delayValue > 0 ? cu1 : cu1 - getDelayValue();
+        long dif = cu - expectPlayer2Delay;
+        String text = "演奏进度:" + cu + "\n视频进度:" + expectPlayer2Delay + "\n差值:" + dif;
+        viewBinding.tvDelayText.setText(text);
+        LOG.i("pq", "toShowDelay:" + text);
+        LOG.i("pq", "expectDelay:" + dif);
+        long difAbs = Math.abs(dif);
+        LOG.i("pq", "difAbs:" + difAbs);
+        float defaultSpeed = MergeConfig.DEFAULT_PLAY_SPEED;
+        if (difAbs > (MAX_ADJUSTMENT)) {
+            //为了不调整伴奏,防止伴奏声音出现缓慢或者快进的问题,听感不适,并且缩小单个调速的速率值从1000f->800f
+            float s = difAbs / 800f;
+            if (dif > 0) {
+//                player1.setSpeed(defaultSpeed + s);
+                player2.setSpeed(defaultSpeed + s);
+            } else {
+//                player1.setSpeed(defaultSpeed - s);
+                player2.setSpeed(defaultSpeed - s);
+            }
+        } else {
+//            player1.setSpeed(defaultSpeed);
+            player2.setSpeed(defaultSpeed);
+        }
+    }
+
+    private void toRealPlayVideo() {
+        if (audioPlayDuration != 0 && videoPlayDuration != 0) {
+            player2.resetToPrepare();
+            player2.start();
+            player2.setVolume(0);
+        }
+    }
+
+    private void setVolume() {
+        if (mSettingFragment != null) {
+            float originalVolume = mSettingFragment.getOriginalVolume();
+            float accompanyVolume = mSettingFragment.getAccompanyVolume();
+            getAudioPlayer().setWeight(0, accompanyVolume);
+            getAudioPlayer().setWeight(1, originalVolume);
+        }
+    }
+
+    private MergeTrackManager getAudioPlayer() {
+        return audioPlayer;
+    }
+
+    private void resetVideoSize(int width, int height) {
+        int surfaceWidth = viewBinding.flSurface.getWidth();
+        int surfaceHeight = viewBinding.flSurface.getHeight();
+        LOG.i("resetVideoSize", "surfaceWidth:" + surfaceWidth);
+        LOG.i("resetVideoSize", "surfaceHeight:" + surfaceHeight);
+        float containerAspectRatio = surfaceWidth * 1.0f / surfaceHeight;
+        float videoAspectRatio = width * 1.0f / height;
+        LOG.i("resetVideoSize", "containerAspectRatio:" + containerAspectRatio);
+        LOG.i("resetVideoSize", "videoAspectRatio:" + videoAspectRatio);
+        boolean b = NumberUtils.compareResult(containerAspectRatio, videoAspectRatio);
+        LOG.i("resetVideoSize", "compareResult:" + b);
+        if (b) {
+            return;
+        }
+        LOG.i("resetVideoSize", "videoWidth:" + width);
+        LOG.i("resetVideoSize", "videoHeight:" + height);
+        int w;
+        int h;
+        if (containerAspectRatio > videoAspectRatio) {
+            // 容器更宽,视频高度适应容器高度
+            h = surfaceHeight;
+            w = (int) (surfaceHeight * videoAspectRatio);
+        } else {
+            // 容器更高,视频宽度适应容器宽度
+            w = surfaceWidth;
+            h = (int) (surfaceWidth / videoAspectRatio);
+        }
+
+        LOG.i("resetVideoSize", "w:" + w);
+        LOG.i("resetVideoSize", "h:" + h);
+        if (w > surfaceWidth) {
+            w = surfaceWidth;
+        }
+        if (h > surfaceHeight) {
+            h = surfaceHeight;
+        }
+        LOG.i("pq", "w:" + w);
+        LOG.i("pq", "h:" + h);
+        FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mSurfaceView.getLayoutParams();
+        layoutParams.width = w;
+        layoutParams.height = h;
+        mSurfaceView.setLayoutParams(layoutParams);
+//        mSurfaceView.getHolder().setFixedSize(w, h);
+    }
+
+    private boolean checkRecordFile() {
+        if (TextUtils.isEmpty(recordFilePath)) {
+            return false;
+        }
+        File file = new File(recordFilePath);
+        if (!file.exists()) {
+            return false;
+        }
+        return true;
+    }
+
+    private void toPlay(String accompanyPath) {
+
+        String[] files = new String[]{accompanyPath, recordWavFilePath};
+        getAudioPlayer().play2(files);
+        if (isVideo) {
+            player2.play(recordFilePath);
+        }
+    }
+
+    private boolean checkRecordFile(String recordUrl) {
+        String fileEndSuffix = MyFileUtils.getWAVOrMp4FileSuffix(recordUrl);
+        String wavFileSavePath = MixHelper.getInstance().getWavFileSavePath(recordUrl, MyFileUtils.WAV_FILE_SUFFIX);
+        String recordFileDownloadPath = getRecordFileDownloadPath(recordUrl, fileEndSuffix);
+        File file = new File(recordFileDownloadPath);
+        File wavFile = new File(wavFileSavePath);
+        if (file.exists() && wavFile.exists()) {
+            recordFilePath = recordFileDownloadPath;
+            recordWavFilePath = wavFileSavePath;
+            LOG.i("pq", "checkRecordFile:" + recordFilePath);
+            return true;
+        }
+        boolean validDownloadUrl = UrlUtils.isValidDownloadUrl(recordUrl);
+        if (!validDownloadUrl) {
+            ToastUtil.getInstance().showShort("未找到演奏文件,请退出重试");
+            return false;
+        }
+        LOG.i("pq", "下载草稿");
+        setLoadingCancelable(false);
+        showLoading("草稿下载中");
+        MixHelper.getInstance().downloadOriginalFile(recordUrl, fileEndSuffix, new ResultCallback<String>() {
+            @Override
+            public void onSuccess(String s) {
+                if (!checkActivityExist()) {
+                    return;
+                }
+                recordFilePath = s;
+                if (MyFileUtils.isVideoFromUrl(recordUrl)) {
+                    recordWavFilePath = MixHelper.getInstance().getWavFileSavePath(recordUrl, MyFileUtils.WAV_FILE_SUFFIX);
+                } else {
+                    recordWavFilePath = recordFilePath;
+                }
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        hideLoading();
+                        preLoad();
+                    }
+                });
+            }
+
+            @Override
+            public void onProgress(int progressPercent) {
+                if (!checkActivityExist()) {
+                    return;
+                }
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        updateLoadingText("草稿下载中" + progressPercent + "%");
+                    }
+                });
+            }
+
+            @Override
+            public void onFail(int errorCode, String errorStr) {
+                if (!checkActivityExist()) {
+                    return;
+                }
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        ToastUtil.getInstance().showShort("草稿下载失败,请重试");
+                        hideLoading();
+                    }
+                });
+            }
+        });
+        return false;
+    }
+
+    private boolean checkAccompanimentMp3File() {
+        if (TextUtils.isEmpty(accompanyUrl)) {
+            ToastUtil.getInstance().showShort("未找到伴奏文件,请退出重试");
+            return false;
+        }
+        String accompanyPath = getAccompanyPath();
+        File file = new File(accompanyPath);
+        if (file.exists()) {
+            return true;
+        }
+        boolean validDownloadUrl = UrlUtils.isValidDownloadUrl(accompanyUrl);
+        if (!validDownloadUrl) {
+            ToastUtil.getInstance().showShort("未找到伴奏文件,请退出重试");
+            return false;
+        }
+        setLoadingCancelable(false);
+        showLoading("伴奏下载中");
+        MixHelper.getInstance().downloadAccompany(accompanyUrl, accompanyPlaySpeed, MyFileUtils.WAV_FILE_SUFFIX, new ResultCallback<String>() {
+            @Override
+            public void onSuccess(String s) {
+                if (!checkActivityExist()) {
+                    return;
+                }
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        preparePlay();
+                        hideLoading();
+                    }
+                });
+            }
+
+            @Override
+            public void onProgress(int progressPercent) {
+                if (!checkActivityExist()) {
+                    return;
+                }
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        updateLoadingText("伴奏下载中" + progressPercent + "%");
+                    }
+                });
+            }
+
+            @Override
+            public void onFail(int errorCode, String errorStr) {
+                if (!checkActivityExist()) {
+                    return;
+                }
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        ToastUtil.getInstance().showShort("伴奏下载失败,请重试");
+                        hideLoading();
+                    }
+                });
+            }
+        });
+        return false;
+    }
+
+
+    @Override
+    public void hideLoading() {
+        setLoadingCancelable(true);
+        toSetLoadingCancelable(true);
+        if (mLoadingTipDialog != null) {
+            mLoadingTipDialog.hide();
+        }
+        super.hideLoading();
+    }
+
+    private String getAccompanyPath() {
+        String accompanyPath = MixHelper.getInstance().getDownloadSavePathForTag(accompanyUrl, String.valueOf(accompanyPlaySpeed), MyFileUtils.WAV_FILE_SUFFIX);
+        return accompanyPath;
+    }
+
+    private String getRecordFileDownloadPath(String recordFileUrl, String fileEndSuffix) {
+        String recordFilePath;
+        if (MyFileUtils.isVideoFromUrl(recordFileUrl)) {
+            recordFilePath = MixHelper.getInstance().getDownloadSavePathForMp4(recordFileUrl);
+        } else {
+            recordFilePath = MixHelper.getInstance().getDownloadSavePath(recordFileUrl, fileEndSuffix);
+        }
+        return recordFilePath;
+    }
+
+    @Override
+    protected AcMusicHandleLayoutBinding getLayoutView() {
+        return AcMusicHandleLayoutBinding.inflate(getLayoutInflater());
+    }
+
+    @Override
+    public void onClick(View v) {
+        int id = v.getId();
+        if (id == R.id.iv_play) {
+            if (isPlaying()) {
+                pausePlay();
+            } else {
+                resumePlay();
+            }
+            return;
+        }
+        if (id == R.id.iv_unfold_sentting) {
+            handleSettingVisibility();
+            return;
+        }
+        if (id == R.id.iv_back) {
+            checkTipToFinish();
+            return;
+        }
+    }
+
+    private void resumePlay() {
+        getAudioPlayer().resume();
+        if (player2 != null) {
+            player2.resume();
+        }
+    }
+
+    private boolean isPlaying() {
+        boolean playing = getAudioPlayer().isPlaying();
+        return playing;
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        pausePlay();
+    }
+
+    private void pausePlay() {
+        if (player2.isPlaying()) {
+            player2.pause();
+        }
+        getAudioPlayer().pause();
+        updatePlayStatus();
+    }
+
+    private void checkTipToFinish() {
+        boolean hasUpdate = mViewModel.isHasUpdate();
+        if (hasUpdate) {
+            showSaveTipDialog();
+        } else {
+            finish();
+        }
+    }
+
+    private void showSaveTipDialog() {
+        CommonConfirmDialog2 commonConfirmDialog = new CommonConfirmDialog2(this);
+        commonConfirmDialog.setWidth(SizeUtils.dp2px(387));
+        commonConfirmDialog.show();
+        commonConfirmDialog.setTitle("提示");
+        commonConfirmDialog.setContent("是否将本次录制的作品保存为草稿?");
+        commonConfirmDialog.setOnConfirmClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                commonConfirmDialog.dismiss();
+                isNeedFinishPage = true;
+                toSaveDraft();
+            }
+        });
+        commonConfirmDialog.setOnCancelClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                commonConfirmDialog.dismiss();
+                finish();
+            }
+        });
+    }
+
+    /**
+     * 监听返回键
+     *
+     * @param keyCode
+     * @param event
+     * @return
+     */
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_BACK) {
+            checkTipToFinish();
+            return true;
+
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
+    private void handleSettingVisibility() {
+        int visibility = viewBinding.flSetting.getVisibility();
+        if (visibility == View.VISIBLE) {
+            viewBinding.groupSetting.setVisibility(View.GONE);
+            viewBinding.ivUnfoldSentting.setVisibility(View.VISIBLE);
+        } else {
+            viewBinding.groupSetting.setVisibility(View.VISIBLE);
+            viewBinding.ivUnfoldSentting.setVisibility(View.GONE);
+        }
+
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                resetVideoSize(videoWidth, videoHeight);
+            }
+        }, 50);
+    }
+
+    private void updatePlayStatus() {
+        if (isPlaying()) {
+            viewBinding.ivPlayPointer.setRotation(0);
+            handleAnim(true);
+            viewBinding.ivPlay.setImageResource(isVideo ? R.drawable.icon_music_merge_pause_white : R.drawable.icon_music_merge_pause);
+        } else {
+            handleAnim(false);
+            viewBinding.ivPlayPointer.setRotation(92);
+            viewBinding.ivPlay.setImageResource(isVideo ? R.drawable.icon_music_merge_play_white : R.drawable.icon_music_merge_play);
+        }
+    }
+
+
+    private void handleAnim(boolean isPlay) {
+        if (isVideo) {
+            return;
+        }
+        if (isPlay) {
+            viewBinding.musicFrequencyView.setMediaPlayer(getAudioPlayer().getAudioSessionId());
+        }
+        rotation(isPlay);
+        rotationAlbum(isPlay);
+    }
+
+    private void rotation(boolean isPlay) {
+        float from = isPlay ? 92 : 0;
+        float to = isPlay ? 0 : 92;
+        viewBinding.ivPlayPointer.clearAnimation();
+        float rotation = viewBinding.ivPlayPointer.getRotation();
+        LOG.i("rotation:" + rotation);
+        if (mRotateAnimation == null) {
+            mRotateAnimation = ObjectAnimator.ofFloat(viewBinding.ivPlayPointer, "rotation", from, to);
+            mRotateAnimation.setInterpolator(new LinearInterpolator());//不停顿
+            mRotateAnimation.setDuration(300);
+            mRotateAnimation.setRepeatCount(0);
+        }
+        mRotateAnimation.setFloatValues(from, to);
+        mRotateAnimation.start();
+    }
+
+    private void rotationAlbum(boolean isPlay) {
+        float rotation = viewBinding.flAblum.getRotation();
+        if (mAlbumRotationAnimator == null) {
+            mAlbumRotationAnimator = ObjectAnimator.ofFloat(viewBinding.flAblum, "rotation", 0f, 360f);
+            mAlbumRotationAnimator.setDuration(5000); // 设置动画持续时间为1秒
+            mAlbumRotationAnimator.setRepeatCount(ObjectAnimator.INFINITE); // 设置无限循环
+            mAlbumRotationAnimator.setInterpolator(new LinearInterpolator()); // 设置动画插值器,这里使用线性插值器
+        }
+        if (isPlay) {
+            if (mAlbumRotationAnimator.isPaused()) {
+                mAlbumRotationAnimator.resume();
+            } else {
+                mAlbumRotationAnimator.start(); // 启动动画
+            }
+        } else {
+            mAlbumRotationAnimator.pause();//
+        }
+    }
+
+
+    @Override
+    public void saveWorksSuccess(Object data, String des, String imgCover) {
+        if (!checkActivityExist()) {
+            return;
+        }
+        currentStep = 2;
+        String worksId = null;
+        if (data instanceof String) {
+            worksId = (String) data;
+        }
+        if (!TextUtils.isEmpty(worksId)) {
+            ShareIntentBean shareIntentBean = buildShareData(worksId, des, imgCover);
+            toShowShareDialog(shareIntentBean, true);
+        } else {
+            showToastViewAndFinish(getString(R.string.publish_success), true);
+        }
+    }
+
+    private ShareIntentBean buildShareData(String worksId, String shareDes, String imgCover) {
+        ShareIntentBean bean = new ShareIntentBean();
+        String url = String.format(WebConstants.MY_WORKS_SHARE, worksId);
+        bean.setLinkUrl(url);
+        bean.setTitle(getString(R.string.share_works_title));
+        bean.setDes(shareDes);
+        bean.setThumb(imgCover);
+        return bean;
+    }
+
+    private void toShowShareDialog(ShareIntentBean intentBean, boolean isReCallBack) {
+        toUpdateLoadingText(getCurrentProgress(100), getString(R.string.publish_success));
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                hideLoading();
+                showShareDialog(intentBean, isReCallBack);
+            }
+        }, 1500);
+    }
+
+    private void showShareDialog(ShareIntentBean intentBean, boolean isReCallBack) {
+        ShareDialog shareDialog = new ShareDialog(this);
+        shareDialog.setOnEventListener(new ShareDialog.OnEventListener() {
+            @Override
+            public void toShare(ShareType shareType) {
+                goShare(intentBean, shareType);
+            }
+        });
+        shareDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+            @Override
+            public void onDismiss(DialogInterface dialog) {
+                toFinish(isReCallBack);
+            }
+        });
+        shareDialog.show();
+    }
+
+
+    private void goShare(ShareIntentBean intentBean, ShareType shareType) {
+        if (shareType == ShareType.COPY_LINK) {
+            ClipboardUtils.copyText(intentBean.getLinkUrl());
+            ToastUtil.getInstance().showShort("复制成功");
+            return;
+        } else if (shareType == ShareType.WEIXIN) {
+            intentBean.setShare_media(SHARE_MEDIA.WEIXIN);
+        } else if (shareType == ShareType.WEIXIN_CIRCLE) {
+            intentBean.setShare_media(SHARE_MEDIA.WEIXIN_CIRCLE);
+        }
+        if (intentBean.getShare_media() != null) {
+            CommonShareHelper.toShareUrl(MusicHandleActivity_.this, intentBean, this);
+        }
+    }
+
+    private void toFinish(boolean isReCallBack) {
+        Intent intent = new Intent();
+        if (isReCallBack) {
+            intent.putExtra("saveWorksStatus", 1);
+        }
+        setResult(RESULT_OK, intent);
+        finish();
+    }
+
+
+    @Override
+    public void getDetailSuccess(MusicDataBean data) {
+        if (!checkActivityExist()) {
+            return;
+        }
+        if (data != null) {
+            this.originalFileUrl = data.getVideoUrl();
+            String jsonConfig = data.getJsonConfig();
+            accompanyUrl = data.getAccompanyUrl();
+            if (mSettingFragment != null) {
+                if (!TextUtils.isEmpty(jsonConfig)) {
+                    toApplyConfig(jsonConfig);
+                }
+                mSettingFragment.setAccompanyUrl(accompanyUrl, accompanyPlaySpeed);
+            }
+            //这里为了兼容IOS录制的wav音频文件格式不正确 导致合成失败的问题Failed to read frame size: Could not seek to 1026.
+            //取服务端存储的文件
+            boolean isVideo = MyFileUtils.isVideoFromUrl(data.getVideoUrl());
+            String fileUrl = isVideo ? data.getVideoUrl() : data.getRecordFilePath();
+            boolean b = checkRecordFile(fileUrl);
+            if (b) {
+                preLoad();
+            }
+
+        }
+    }
+
+    private void toApplyConfig(String jsonConfig) {
+        try {
+            MusicMergeConfigBean musicMergeConfigBean = GsonUtils.fromJson(jsonConfig, MusicMergeConfigBean.class);
+            defaultDelay = Math.abs(musicMergeConfigBean.getDefaultDelay());
+            evaluateDelay = Math.abs(musicMergeConfigBean.getEvaluateDelay());
+            mSettingFragment.applyConfig(musicMergeConfigBean);
+            accompanyPlaySpeed = musicMergeConfigBean.getSpeedRate();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public void saveWorksDraftSuccess() {
+        if (!checkActivityExist()) {
+            return;
+        }
+        currentStep = 1;
+        mViewModel.getUpdateEvent().setValue(false);
+        if (isNeedFinishPage) {
+            showToastViewAndFinish("保存成功", false);
+        } else {
+            toUpdateLoadingText(getCurrentProgress(100), "保存成功");
+            mHandler.postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    hideLoading();
+                    if (isNeedTip()) {
+                        showOtherTipDialog();
+                    }
+                }
+            }, 500);
+        }
+    }
+
+    private boolean isNeedTip() {
+        //云教练进来的需要提示 作品草稿过来的不需要
+        return TextUtils.isEmpty(worksId);
+    }
+
+    private void showOtherTipDialog() {
+        CommonConfirmDialog2 commonConfirmDialog = new CommonConfirmDialog2(this);
+        commonConfirmDialog.setWidth(SizeUtils.dp2px(387));
+        commonConfirmDialog.show();
+        commonConfirmDialog.setTitle("提示");
+        commonConfirmDialog.setContent("已成功保存到草稿,草稿7天未发布\n将自动删除。");
+        commonConfirmDialog.setCancelText("确认");
+        commonConfirmDialog.setConfirmText("查看草稿");
+        commonConfirmDialog.setOnCancelClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                commonConfirmDialog.dismiss();
+            }
+        });
+        commonConfirmDialog.setOnConfirmClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                commonConfirmDialog.dismiss();
+                //从首页个人中心跳转我的作品
+                JumpUtils.jumpMain(4);
+                goMyWorks();
+            }
+        });
+
+    }
+
+    private void goMyWorks() {
+        ARouter.getInstance().build(RouterPath.Homework.MY_WORK)
+                .withInt(Constants.MAIN_PAGE_SELECT_POTION_KEY, 1)
+                .navigation();
+        finish();
+    }
+
+    private void showToastViewAndFinish(String tip, boolean isReCallBack) {
+        //测试lyr提出提示要在当前面提示,所以给出延迟finish
+//        viewBinding.tvToastView.setText(tip);
+//        viewBinding.tvToastView.setVisibility(View.VISIBLE);
+        toUpdateLoadingText(getCurrentProgress(100), tip);
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                hideLoading();
+                toFinish(isReCallBack);
+            }
+        }, 1500);
+    }
+
+    @Override
+    public void upLoadImageSuccess(String url, boolean isVideoCover) {
+        if (!checkActivityExist()) {
+            return;
+        }
+        if (mViewModel != null) {
+            if (isVideoCover) {
+                mViewModel.refreshMusicVideoCover(url);
+            } else {
+                mViewModel.refreshMusicWorksCover(url);
+            }
+        }
+    }
+
+    private void refreshMusicInfoPreCover(String imgCover) {
+        mViewModel.refreshMusicPreCover(imgCover);
+    }
+
+    private void refreshMusicInfoVideoCover(String imgCover) {
+        mViewModel.refreshMusicVideoCover(imgCover);
+    }
+
+
+    private void refreshMusicInfo(String imgCover) {
+        MusicInfoBean bean = new MusicInfoBean();
+        bean.setCover(imgCover);
+        bean.setMusicTitle(mTitle);
+        mViewModel.getMusicInfoLiveData().setValue(bean);
+    }
+
+    private int getCurrentProgress(int progress) {
+        float singleStepMaxProgress = MAX_PROGRESS * 1.0f / MAX_STEP;
+        float progressPercent = progress * 1.0f / 100;
+        int result = (int) (singleStepMaxProgress * currentStep + singleStepMaxProgress * progressPercent);
+        LOG.i("progress:" + progress + "--currentStep:" + currentStep + "--currentStepProgress" + singleStepMaxProgress * currentStep + "--progressPercent:" + progressPercent + "--result:" + result);
+        return result;
+    }
+
+    @Override
+    public void upLoadImageFailure() {
+        ToastUtil.getInstance().showShort("封面上传失败");
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        UMShareAPI.get(MusicHandleActivity_.this).onActivityResult(requestCode, resultCode, data);
+        if (resultCode == RESULT_OK) {
+            if (requestCode == REQUEST_CODE_LOCAL || requestCode == REQUEST_CODE_LOCAL_VIDEO_COVER) {
+                if (data != null) {
+                    // 图片、视频、音频选择结果回调
+                    List<LocalMedia> selectList = PictureSelector.obtainMultipleResult(data);
+                    String v_path = null;
+                    if (selectList != null && selectList.size() > 0) {
+                        v_path = selectList.get(0).getCompressPath();
+                    }
+                    if (!TextUtils.isEmpty(v_path)) {
+                        boolean isImg = MyFileUtils.isImg(v_path);
+                        if (isImg) {
+                            if (requestCode == REQUEST_CODE_LOCAL_VIDEO_COVER) {
+                                refreshMusicInfoVideoCover(v_path);
+                            } else {
+                                refreshMusicInfoPreCover(v_path);
+                            }
+                        } else {
+                            ToastUtil.getInstance().showShort("请选择图片类型文件");
+                        }
+                    }
+                }
+            }
+            if (requestCode == REQUEST_CODE_VIDEO_COVER) {
+                if (data != null) {
+                    String imgPath = data.getStringExtra(Constants.COMMON_EXTRA_KEY);
+                    refreshMusicInfoVideoCover(imgPath);
+                    return;
+                }
+            }
+        }
+    }
+
+    private void toShowLoading(int progress, String text) {
+        if (mLoadingTipDialog != null) {
+            mLoadingTipDialog.showLoading(progress, text);
+        }
+    }
+
+    private void toSetLoadingCancelable(boolean flag) {
+        if (mLoadingTipDialog != null) {
+            mLoadingTipDialog.setLoadingCancelable(flag);
+        }
+    }
+
+    private void toUpdateLoadingText(int progress, String text) {
+        if (mLoadingTipDialog != null) {
+            mLoadingTipDialog.updateLoadingText(progress, text);
+        }
+    }
+
+    private void initLoadingDialog() {
+        if (mLoadingTipDialog == null) {
+            mLoadingTipDialog = new MergeLoadingTipDialog(this);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        releaseAnim();
+        if (player2 != null) {
+            player2.release();
+        }
+        if (getAudioPlayer() != null) {
+            getAudioPlayer().release();
+        }
+        super.onDestroy();
+        UMShareAPI.get(MusicHandleActivity_.this).release();
+        if (mHandler != null) {
+            mHandler.removeCallbacksAndMessages(null);
+        }
+        if (mSurfaceView != null && mSurfaceView.getSurfaceTexture() != null) {
+            mSurfaceView.getSurfaceTexture().release();
+        }
+    }
+
+    private void releaseAnim() {
+        if (viewBinding != null && viewBinding.musicFrequencyView != null) {
+            viewBinding.musicFrequencyView.release();
+        }
+        if (mRotateAnimation != null) {
+            mRotateAnimation.cancel();
+            mRotateAnimation = null;
+        }
+        if (mAlbumRotationAnimator != null) {
+            mAlbumRotationAnimator.cancel();
+            mAlbumRotationAnimator = null;
+        }
+    }
+
+    @Override
+    public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
+        if (mSurfaceTexture == null) {
+            mSurfaceTexture = surface;
+        } else {
+            mSurfaceView.setSurfaceTexture(mSurfaceTexture);
+        }
+        if (player2 != null) {
+            toPlay(getAccompanyPath());
+            player2.setSurface(new Surface(mSurfaceTexture));
+        }
+    }
+
+    @Override
+    public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, int height) {
+
+    }
+
+    @Override
+    public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
+        return false;
+    }
+
+    @Override
+    public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {
+
+    }
+
+    @Override
+    public void finish() {
+        //为了适配华为mata40曲面屏,此页面是横屏,前一页面是竖屏,返回回去的时候会有UI闪动,所以这样处理
+        //观察发现横屏模式时候没有达到曲面最大效果(竖屏可以达到),猜测横屏模式就不是曲面模式了,这个时候回到前面(竖屏)页面,屏幕的最外层的容器可能会变化导致UI闪动
+        checkScreenOrientation();
+        super.finish();
+    }
+
+    @Override
+    public void onBackPressed() {
+        //为了适配华为mata40曲面屏,此页面是横屏,前一页面是竖屏,返回回去的时候会有UI闪动,所以这样处理
+        //观察发现横屏模式时候没有达到曲面最大效果(竖屏可以达到),猜测横屏模式就不是曲面模式了,这个时候回到前面(竖屏)页面,屏幕的最外层的容器可能会变化导致UI闪动
+        checkScreenOrientation();
+        super.onBackPressed();
+    }
+
+    private void checkScreenOrientation() {
+        LOG.i("isNeedResetScreenOrientation:" + isNeedResetScreenOrientation);
+        if (isNeedResetScreenOrientation) {
+            if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
+                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+            }
+        }
+    }
+
+    @Override
+    public void onStart(SHARE_MEDIA shareMedia) {
+
+    }
+
+    @Override
+    public void onResult(SHARE_MEDIA shareMedia) {
+        ToastUtil.getInstance().showShort("分享成功啦");
+    }
+
+    @Override
+    public void onError(SHARE_MEDIA shareMedia, Throwable throwable) {
+        ToastUtil.getInstance().showShort("分享失败啦");
+    }
+
+    @Override
+    public void onCancel(SHARE_MEDIA shareMedia) {
+        ToastUtil.getInstance().showShort("分享取消了");
+    }
+}

+ 36 - 27
musicMerge/src/main/java/com/cooleshow/musicmerge/ui/MusicHandleSettingFragment.java

@@ -7,9 +7,7 @@ import android.os.Handler;
 import android.text.TextUtils;
 import android.view.Gravity;
 import android.view.View;
-import android.widget.ImageView;
 import android.widget.SeekBar;
-import android.widget.TextView;
 
 import com.alibaba.android.arouter.launcher.ARouter;
 import com.cooleshow.base.router.RouterPath;
@@ -17,21 +15,19 @@ import com.cooleshow.base.ui.fragment.BaseFragment;
 import com.cooleshow.base.utils.FileUtils;
 import com.cooleshow.base.utils.LOG;
 import com.cooleshow.base.utils.MyFileUtils;
-import com.cooleshow.base.utils.PermissionUtils;
 import com.cooleshow.base.utils.ThreadUtils;
 import com.cooleshow.base.utils.ToastUtil;
 import com.cooleshow.base.utils.UiUtils;
 import com.cooleshow.base.utils.helper.GlideEngine;
 import com.cooleshow.base.utils.helper.PermissionTipHelper;
-import com.cooleshow.base.widgets.DialogUtil;
 import com.cooleshow.musicmerge.R;
 import com.cooleshow.musicmerge.bean.MusicMergeConfigBean;
+import com.cooleshow.musicmerge.constants.MergeConfig;
 import com.cooleshow.musicmerge.constants.MusicMergeConfig;
 import com.cooleshow.musicmerge.databinding.FgMusicHandleSettingLayoutBinding;
 import com.cooleshow.musicmerge.helper.MixHelper;
 import com.cooleshow.musicmerge.viewmodel.MusicMergeViewModel;
 import com.cooleshow.musicmerge.widget.UploadCoverTipDialog;
-import com.cooleshow.usercenter.helper.UserHelper;
 import com.luck.picture.lib.PictureSelector;
 import com.luck.picture.lib.config.PictureConfig;
 import com.luck.picture.lib.config.PictureMimeType;
@@ -47,19 +43,22 @@ import androidx.lifecycle.ViewModelProvider;
  * Author by pq, Date on 2023/8/28.
  */
 public class MusicHandleSettingFragment extends BaseFragment<FgMusicHandleSettingLayoutBinding> implements View.OnClickListener {
-    public static final int MAX_OFFSET_SECTION = 1200;
+    public static final int MAX_OFFSET_SECTION = 2400;
+
     public static final int MIN_OFFSET_SECTION = MAX_OFFSET_SECTION / 2 / 6 / 10;//最好是10的整数倍
     public static final int ACCOMPANY_TYPE = 1;
     public static final int RECORD_TYPE = 2;
     private String accompanyUrl;
+
+    private float accompanySpeed = MergeConfig.DEFAULT_PLAY_SPEED;
     private String worksId;
 
     private OnEventListener mEventListener;
     private MusicMergeViewModel mViewModel;
+
     private int defaultDelay = 0;
     private int evaluateDelay = 0;
 
-
     private Runnable mRunnable = new Runnable() {
         @Override
         public void run() {
@@ -87,16 +86,16 @@ public class MusicHandleSettingFragment extends BaseFragment<FgMusicHandleSettin
     protected void initView(View rootView) {
         mViewBinding.seekBarOffset.setMax(MAX_OFFSET_SECTION);
         mViewBinding.seekBarOffset.setProgress(MAX_OFFSET_SECTION / 2);
-        formatProgress();
     }
 
     @Override
     protected void initData() {
         accompanyUrl = getArguments().getString("accompanyUrl");
         defaultDelay = getArguments().getInt("defaultDelay", 0);
+        accompanySpeed = getArguments().getFloat(MusicMergeConfig.SPEEDRATE_KEY, MergeConfig.DEFAULT_PLAY_SPEED);
         initViewModel();
         initListener();
-        formatProgress();
+        formatProgress(true);
     }
 
     private void initViewModel() {
@@ -180,7 +179,7 @@ public class MusicHandleSettingFragment extends BaseFragment<FgMusicHandleSettin
             @Override
             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                 if (roundOffProgress(progress)) {
-                    formatProgress();
+                    formatProgress(!fromUser);
                 }
                 if (fromUser) {
                     setUpdateStatus();
@@ -194,7 +193,8 @@ public class MusicHandleSettingFragment extends BaseFragment<FgMusicHandleSettin
 
             @Override
             public void onStopTrackingTouch(SeekBar seekBar) {
-
+                LOG.i("onStopTrackingTouch");
+                formatProgress(true);
             }
         });
     }
@@ -272,7 +272,7 @@ public class MusicHandleSettingFragment extends BaseFragment<FgMusicHandleSettin
                     if (permission) {
                         checkDownload(isNeedNotify);
                     } else {
-                        UiUtils.showPermissionTipDialog(MusicHandleSettingFragment.this.getChildFragmentManager(),getContext(),"提示","请开储存访问权限");
+                        UiUtils.showPermissionTipDialog(MusicHandleSettingFragment.this.getChildFragmentManager(), getContext(), "提示", "请开储存访问权限");
                     }
                 });
     }
@@ -326,7 +326,7 @@ public class MusicHandleSettingFragment extends BaseFragment<FgMusicHandleSettin
                     if (granted) {
                         goAlbum(aspect_ratio_x, aspect_ratio_y, requestCode);
                     } else {
-                        UiUtils.showPermissionTipDialog(getChildFragmentManager(),getContext(),"","请打开存储和相机权限!");
+                        ToastUtil.getInstance().show(getContext(), "请打开存储和相机权限!");
                     }
                 });
     }
@@ -347,7 +347,7 @@ public class MusicHandleSettingFragment extends BaseFragment<FgMusicHandleSettin
     }
 
     private void checkDownload(boolean isNeedNotify) {
-        String downloadSavePath = MixHelper.getInstance().getDownloadSavePath(accompanyUrl, MyFileUtils.MP3_FILE_SUFFIX);
+        String downloadSavePath = MixHelper.getInstance().getDownloadSavePathForTag(accompanyUrl, String.valueOf(accompanySpeed), MyFileUtils.WAV_FILE_SUFFIX);
         boolean fileExists = FileUtils.isFileExists(downloadSavePath);
         if (!fileExists) {
             return;
@@ -364,7 +364,7 @@ public class MusicHandleSettingFragment extends BaseFragment<FgMusicHandleSettin
     }
 
     private void toMix(boolean isNeedNotify) {
-        int i = -getCurrentOffsetValue();//取负值是因为合成的时候需要针对伴奏文件做处理
+        int i = getCurrentOffsetValue();
         float recordFileVolume = getSeekVolume();
         float accompanyFileVolume = getAccompanySeekVolume();
         if (mEventListener != null) {
@@ -372,16 +372,16 @@ public class MusicHandleSettingFragment extends BaseFragment<FgMusicHandleSettin
         }
     }
 
-    private int formatProgress() {
+    private int formatProgress(boolean isNotify) {
         int lastValue = getOffsetValue();
         if (lastValue > 0) {
-            mViewBinding.tvOffsetResult.setText("已为您对齐演奏伴奏,演奏延后" + lastValue + "毫秒");
+            mViewBinding.tvOffsetResult.setText("演奏提前" + lastValue + "毫秒");
         } else if (lastValue < 0) {
-            mViewBinding.tvOffsetResult.setText("已为您对齐演奏伴奏,演奏提前" + Math.abs(lastValue) + "毫秒");
+            mViewBinding.tvOffsetResult.setText("演奏延后" + Math.abs(lastValue) + "毫秒");
         } else {
-            mViewBinding.tvOffsetResult.setText("演奏伴奏没有对齐?试试调整这里");
+            mViewBinding.tvOffsetResult.setText("拖动手柄移动演奏音频");
         }
-        if (mEventListener != null) {
+        if (isNotify && mEventListener != null) {
             mEventListener.onOffsetValueChange(lastValue);
         }
         return lastValue;
@@ -411,8 +411,17 @@ public class MusicHandleSettingFragment extends BaseFragment<FgMusicHandleSettin
         return false;
     }
 
+    private int roundTarget(int progress) {
+        int minSection = MIN_OFFSET_SECTION;
+        int result = progress % minSection;
+        if (result == 0) {
+            return progress;
+        }
+        return progress - result;
+    }
+
     public int getCurrentOffsetValue() {
-        LOG.i("pq","getCurrentOffsetValue:"+getOffsetValue()+"--defaultDelay:"+defaultDelay);
+        LOG.i("pq", "getCurrentOffsetValue:" + getOffsetValue() + "--defaultDelay:" + defaultDelay);
         return getOffsetValue() + defaultDelay;
     }
 
@@ -426,7 +435,7 @@ public class MusicHandleSettingFragment extends BaseFragment<FgMusicHandleSettin
         return value;
     }
 
-    public String getConfigJson(int defaultDelay,int evaluateDelay) {
+    public String getConfigJson(int defaultDelay, int evaluateDelay) {
         if (isDetached()) {
             return "";
         }
@@ -443,6 +452,7 @@ public class MusicHandleSettingFragment extends BaseFragment<FgMusicHandleSettin
             jsonObject.put(MusicMergeConfig.ACCOMPANYVOLUME_KEY, accompanyVolume);
             jsonObject.put(MusicMergeConfig.DEFAULTDELAY_KEY, defaultDelay);
             jsonObject.put(MusicMergeConfig.EVALUATEDELAY_KEY, evaluateDelay);
+            jsonObject.put(MusicMergeConfig.SPEEDRATE_KEY, accompanySpeed);
             return jsonObject.toString();
         } catch (JSONException e) {
             e.printStackTrace();
@@ -462,10 +472,8 @@ public class MusicHandleSettingFragment extends BaseFragment<FgMusicHandleSettin
             int originalVolume = configBean.getOriginalVolume();
             int accompanyVolume = configBean.getAccompanyVolume();
             int progressFromOffsetValue = getProgressFromOffsetValue(offset);
-            mViewBinding.seekBarOffset.setProgress(progressFromOffsetValue);
-            if(offset == 0){
-                formatProgress();//当前seekbar进度为0不会触发onProgressChanged,所以手动触发
-            }
+            mViewBinding.seekBarOffset.setProgress(roundTarget(progressFromOffsetValue));
+            formatProgress(true);
             mViewBinding.seekVolume.setProgress(originalVolume);
             mViewBinding.seekVolume2.setProgress(accompanyVolume);
 
@@ -475,8 +483,9 @@ public class MusicHandleSettingFragment extends BaseFragment<FgMusicHandleSettin
         }
     }
 
-    public void setAccompanyUrl(String accompanyUrl) {
+    public void setAccompanyUrl(String accompanyUrl, float accompanySpeed) {
         this.accompanyUrl = accompanyUrl;
+        this.accompanySpeed = accompanySpeed;
     }
 
     public interface OnEventListener {

+ 187 - 0
musicMerge/src/main/java/com/cooleshow/musicmerge/utils/AudioConverter.java

@@ -0,0 +1,187 @@
+package com.cooleshow.musicmerge.utils;
+
+import com.cooleshow.base.utils.FileUtils;
+import com.cooleshow.base.utils.Utils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Author by pq, Date on 2024/6/14.
+ */
+public class AudioConverter {
+    private static final int HEADER_SIZE = 44;
+
+    /**
+     * 将单声道音频数据转换为双声道音频数据
+     *
+     * @param monoData 单声道音频数据
+     * @return 双声道音频数据
+     */
+    public static byte[] convertMonoToStereo(byte[] monoData) {
+//        int monoSampleCount = monoData.length / 2; // 每个样本2字节
+//        byte[] stereoData = new byte[monoData.length * 2];
+//
+//        for (int i = 0; i < monoSampleCount; i++) {
+//            // 读取单声道样本值
+//            byte left1 = monoData[i * 2];
+//            byte left2 = monoData[i * 2 + 1];
+//
+//            // 将相同的样本值写入左右声道
+//            stereoData[i * 4] = left1;       // 左声道低字节
+//            stereoData[i * 4 + 1] = left2;   // 左声道高字节
+//            stereoData[i * 4 + 2] = left1;   // 右声道低字节
+//            stereoData[i * 4 + 3] = left2;   // 右声道高字节
+//        }
+//
+//        return stereoData;
+        if (monoData == null || monoData.length % 2 != 0) {
+            throw new IllegalArgumentException("Mono data must be a non-null array with even length.");
+        }
+        int monoSampleCount = monoData.length / 2;
+        byte[] stereoData = new byte[monoSampleCount * 4]; // 每个样本4个字节(双声道)
+
+        for (int i = 0, j = 0; i < monoSampleCount; i++, j += 4) {
+            // 获取单声道样本的两个字节
+            short sample = (short) ((monoData[i * 2] & 0xFF) | ((monoData[i * 2 + 1] & 0xFF) << 8));
+
+            // 将样本复制到左声道
+            stereoData[j] = (byte) (sample & 0xFF);
+            stereoData[j + 1] = (byte) ((sample >> 8) & 0xFF);
+
+            // 将样本复制到右声道
+            stereoData[j + 2] = stereoData[j];
+            stereoData[j + 3] = stereoData[j + 1];
+        }
+
+        return stereoData;
+    }
+
+    /**
+     * 检查WAV文件是否为单声道
+     *
+     * @param filePath 文件路径
+     * @return 如果是单声道则返回true,否则返回false
+     * @throws IOException 读取文件时可能抛出的异常
+     */
+    public static boolean isMonoWavFile(String filePath) throws IOException {
+        FileInputStream fis = new FileInputStream(filePath);
+        byte[] header = new byte[44];
+
+        // 读取WAV文件的前44个字节(WAV文件头)
+        if (fis.read(header, 0, 44) != 44) {
+            fis.close();
+            throw new IOException("文件格式错误或文件过短");
+        }
+
+        fis.close();
+
+        // WAV文件头中,第23和24字节表示通道数 (16-bit little-endian)
+        int channels = ((header[23] & 0xff) << 8) | (header[22] & 0xff);
+
+        return channels == 1;
+    }
+
+    /**
+     * 检查WAV文件是否为双声道
+     *
+     * @param filePath 文件路径
+     * @return 如果是双声道则返回true,否则返回false
+     * @throws IOException 读取文件时可能抛出的异常
+     */
+    public static boolean isStereoWavFile(String filePath) throws IOException {
+        FileInputStream fis = new FileInputStream(filePath);
+        byte[] header = new byte[44];
+
+        // 读取WAV文件的前44个字节(WAV文件头)
+        if (fis.read(header, 0, 44) != 44) {
+            fis.close();
+            throw new IOException("文件格式错误或文件过短");
+        }
+
+        fis.close();
+
+        // WAV文件头中,第23和24字节表示通道数 (16-bit little-endian)
+        int channels = ((header[23] & 0xff) << 8) | (header[22] & 0xff);
+
+        return channels == 2;
+    }
+
+    /**
+     * 检查WAV文件是否为双声道
+     *
+     * @param
+     * @return 如果是双声道则返回true,否则返回false
+     * @throws IOException 读取文件时可能抛出的异常
+     */
+    public static boolean isStereoWavFile(FileInputStream fis) throws IOException {
+        byte[] header = new byte[44];
+
+        // 读取WAV文件的前44个字节(WAV文件头)
+        if (fis.read(header, 0, 44) != 44) {
+            fis.close();
+            throw new IOException("文件格式错误或文件过短");
+        }
+        // WAV文件头中,第23和24字节表示通道数 (16-bit little-endian)
+        int channels = ((header[23] & 0xff) << 8) | (header[22] & 0xff);
+
+        return channels == 2;
+    }
+
+
+    /**
+     * 裁剪WAV文件
+     *
+     * @param startByte 开始时间(毫秒)
+     * @param endByte   结束时间(毫秒)
+     * @throws IOException 读取或写入文件时可能抛出的异常
+     */
+    public static byte[] trimWavFile(byte[] wavBytes, int startByte, int endByte) {
+        if (startByte % 2 != 0) {
+            startByte += 1;
+        }
+//        write(wavBytes, "original");
+
+        // 检查边界
+        if (startByte < HEADER_SIZE) startByte = HEADER_SIZE;
+        if (endByte > wavBytes.length) endByte = wavBytes.length;
+
+        // 创建新的WAV数据
+        int newWavDataSize = endByte - startByte;
+        byte[] newWavBytes = new byte[newWavDataSize + HEADER_SIZE];
+
+//        // 复制文件头
+        System.arraycopy(wavBytes, 0, newWavBytes, 0, HEADER_SIZE);
+//
+        // 更新数据部分大小(第41-44字节)
+        newWavBytes[40] = (byte) (newWavDataSize & 0xff);
+        newWavBytes[41] = (byte) ((newWavDataSize >> 8) & 0xff);
+        newWavBytes[42] = (byte) ((newWavDataSize >> 16) & 0xff);
+        newWavBytes[43] = (byte) ((newWavDataSize >> 24) & 0xff);
+
+        // 更新文件总大小(第5-8字节)
+        newWavBytes[4] = (byte) (newWavDataSize & 0xff);
+        newWavBytes[5] = (byte) ((newWavDataSize >> 8) & 0xff);
+        newWavBytes[6] = (byte) ((newWavDataSize >> 16) & 0xff);
+        newWavBytes[7] = (byte) ((newWavDataSize >> 24) & 0xff);
+
+        // 复制音频数据
+        System.arraycopy(wavBytes, startByte, newWavBytes, HEADER_SIZE, newWavDataSize);
+//        write(newWavBytes, "test");
+        return newWavBytes;
+    }
+
+    public static void write(byte[] wavBytes, String name) {
+        try {
+            File file = new File(FileUtils.getCacheDir(Utils.getApp(), "test"), name + ".wav");
+            file.delete();
+            FileOutputStream outputStream = new FileOutputStream(file);
+            outputStream.write(wavBytes);
+            outputStream.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 2 - 2
musicMerge/src/main/java/com/cooleshow/musicmerge/widget/MusicFrequencyView.java

@@ -144,10 +144,10 @@ public class MusicFrequencyView extends View {
      *
      * @param mediaPlayer
      */
-    public void setMediaPlayer(final MediaPlayer mediaPlayer) {
+    public void setMediaPlayer(final int audioSessionId) {
         try{
             if (visualizer == null) {
-                visualizer = new Visualizer(mediaPlayer.getAudioSessionId());
+                visualizer = new Visualizer(audioSessionId);
 
                 int captureSize = Visualizer.getCaptureSizeRange()[1];
                 int captureRate = Visualizer.getMaxCaptureRate() * 3 / 4;

+ 27 - 5
musicMerge/src/main/res/layout/fg_music_handle_setting_layout.xml

@@ -43,8 +43,8 @@
         app:layout_constraintTop_toBottomOf="@+id/tv_title"/>
 
         <View
-            android:layout_marginStart="6dp"
-            android:layout_marginEnd="6dp"
+            android:layout_marginStart="9dp"
+            android:layout_marginEnd="9dp"
             android:id="@+id/view_line"
             app:layout_constraintRight_toRightOf="@+id/seek_volume"
             app:layout_constraintLeft_toLeftOf="@+id/seek_volume"
@@ -125,8 +125,8 @@
         app:layout_constraintTop_toBottomOf="@+id/tv_title3" />
 
     <View
-        android:layout_marginStart="6dp"
-        android:layout_marginEnd="6dp"
+        android:layout_marginStart="9dp"
+        android:layout_marginEnd="9dp"
         android:id="@+id/view_line2"
         app:layout_constraintRight_toRightOf="@+id/seek_volume2"
         app:layout_constraintLeft_toLeftOf="@+id/seek_volume2"
@@ -225,7 +225,29 @@
         app:layout_constraintTop_toBottomOf="@+id/iv_reduce"
         android:textColor="@color/color_8f8f8f"
         android:textSize="@dimen/sp_11"
-        tools:text="已为您对齐演奏伴奏,演奏提前10毫秒"
+        tools:text="拖动手柄移动演奏音频"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+    <TextView
+        android:id="@+id/tv_offset_tip"
+        app:layout_constraintRight_toRightOf="@+id/iv_reduce"
+        app:layout_constraintLeft_toLeftOf="@+id/iv_reduce"
+        app:layout_constraintTop_toTopOf="@+id/tv_offset_result"
+        android:textColor="@color/color_8f8f8f"
+        android:textSize="@dimen/sp_11"
+        android:text="演奏延后"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+    <TextView
+        android:id="@+id/tv_offset_tip2"
+        app:layout_constraintRight_toRightOf="@+id/iv_add"
+        app:layout_constraintLeft_toLeftOf="@+id/iv_add"
+        app:layout_constraintTop_toTopOf="@+id/tv_offset_result"
+        android:textColor="@color/color_8f8f8f"
+        android:textSize="@dimen/sp_11"
+        android:text="演奏提前"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"/>
 

+ 1 - 0
student/src/main/AndroidManifest.xml

@@ -29,6 +29,7 @@
         android:requestLegacyExternalStorage="true"
         android:supportsRtl="true"
         android:theme="@style/AppTheme"
+        android:largeHeap="true"
         tools:replace="android:allowBackup,label">
         <meta-data
             android:name="design_width_in_dp"

+ 1 - 0
teacher/src/main/AndroidManifest.xml

@@ -39,6 +39,7 @@
         android:requestLegacyExternalStorage="true"
         android:supportsRtl="true"
         android:theme="@style/AppTheme"
+        android:largeHeap="true"
         tools:replace="android:allowBackup,label">
 
         <meta-data