package com.example.mediademo;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.provider.Settings;
import android.text.Editable;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import com.example.mediademo.util.AudioParamBean;
import com.example.mediademo.util.AudioPlayer;
import com.google.gson.Gson;
import com.itgsa.opensdk.common.OnConnectionSucceedListener;
import com.itgsa.opensdk.media.MediaClient;
import com.itgsa.opensdk.media.extensions.KtvSupportInfo;
import com.itgsa.opensdk.media.extensions.MediaClientUtil;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

//import android.support.v4.app.ActivityCompat;
//import android.support.v4.content.ContextCompat;

public class KTVActivity extends Activity implements
        OnClickListener,
        OnItemSelectedListener,
        OnSeekBarChangeListener {

    private static final String TAG = "KTVTest";
    private static final String SING_TAG = "KTVSING";

    //    private static final int MY_PERMISSIONS_REQUEST_RECORD_AUDIO = 101;
//    private static final int MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 102;
    private boolean isListGot = false;

    private static final int MSG_REPORT_RECORD_COST = 100;
    private static final int MSG_REPORT_READ_COST = 101;
    private static final int MSG_REPORT_TOTAL_COST = 102;
    private static final int MSG_REPORT_SING_START = 103;
    private static final int MSG_REPORT_SING_FINISH = 104;

    private static final int MSG_KTV_MODE_ENABLE = 200;
    private static final int MSG_RECORD_START = 201;
    private static final int MSG_PLAYBACK_START = 202;
    private static final int MSG_SLIENT_START = 203;

    private ToggleButton mKTVMode;
    private Button mBtnRecord;
    private Button mBtnRecPlay;
    private Button mBtnSelint;
    private Button mBtnSing;
    private Spinner mSpinnerMusicSelector;
    private Button mBtnStop;
    private TextView mReport;
    private Button mBtnDoSing;
    private Button mBtnSingPlay;

    private Context mContext;

    private Spinner mSpinnerPresetModeReverb;
    private Spinner mSpinnerPresetModeEq;
    private Spinner mSpinnerPresetModeTone;

    private ToggleButton mPlaySource;
    private TextView mTextVolMic;
    private SeekBar mVolMic;
    private RadioGroup mOutSelect;
    private RadioButton mHeadOut, mExtSpkr;
    private RadioGroup mRecSource;
    private RadioButton mRBMic, mRBMixed, mRBProcessMic;

    // default reverb type ( 0:无、1:KTV、2:剧场、3:音乐厅、4:录音棚 )
    private static final int DEFAULT_COUNT_REVERB = 4;
    // For VIVO extensions as bellow:
    // 10: 温暖, 11: 空灵, 12: 3D迷幻, 13: 老唱片
    private final String ReverbPresetMode[] = {"无", "KTV", "剧场", "音乐厅", "录音棚",
            "温暖", "空灵", "3D迷幻", "老唱片"};
    private final Map<String, Integer> mPresetModeMapReverb = new HashMap<>();

    private static final int DEFAULT_MAX_COUNT_REVERB_EQ = 5; // include no effect

    // default EQ type ( 0:无、1:标准、2:浑厚、3:清脆、4:明亮 )
    private static final int DEFAULT_COUNT_EQ = 4;
    // For VIVO extensions as bellow:
    // 10: 温暖, 11: 空灵, 12: 3D迷幻, 13: KTV
    private final String EqPresetMode[] = {"无", "标准", "浑厚", "清脆", "明亮",
            "温暖", "空灵", "3D迷幻", "KTV"};
    private final Map<String, Integer> mPresetModeMapEq = new HashMap<>();

    // for OPPO setToneMode to set magic voice [-12, 12]
    private static final int DEFAULT_COUNT_TONE = 0;
    private final String TonePresetMode[] = {"男（-12）", "男（-11）", "男（-10）", "男（-9）", "男（-8）",
            "男（-7）", "男（-6）", "男（-5）", "男（-4）", "男（-3）", "男（-2）", "男（-1）", "原声（0）",
            "女（1）", "女（2）", "女（3）", "女（4）", "女（5）", "女（6）", "女（7）", "女（8）", "女（9）", "女（10）",
            "女（11）", "女（12）"};
    private final Map<String, Integer> mPresetModeMapTone = new HashMap<>();

    private AudioManager mAM;
    private AudioPlayer mAudioPlayer = null;

    private AudioRecord mRecord = null;
    private final Object mRecordLock = new Object();
    private boolean isRecording = false;

    private static final String WORKPATH = "/storage/emulated/0/Android/data/com.example.mediademo/KTV/";
    private ArrayAdapter<String> mMusicListAdapter;
    private ArrayAdapter<String> mMusicPathAdapter;

    private static final int MSG_UPDATE_MIC_VOCAL = 100;

    private static final boolean DefaultKTVMode = false;
    private static final boolean DefaultPlaySource = false;

    private static final int DelaySlientStart = 0;
    private static final int DelayKTVEnable = 100;
    private static final int DelayRecordStart = 200;
    private static final int DelayPlayStart = 400;

    private static final int LatencyPlay = 560;

    private static final int RecBufSize = 2205;
    private static final int SampleRateInHz = 44100;
    //    private static final String AudioPCM = "/sdcard/KTV/ktvrec.pcm";
//    private static final String RecordWAV = "/sdcard/KTV/ktvrec.wav";
//    private static final String SINGWAV = "/sdcard/KTV/ktvsing.wav";
    private static final String AudioPCM = WORKPATH + "ktvrec.pcm";
    private static final String RecordWAV = WORKPATH + "ktvrec.wav";
    private static final String SINGWAV = WORKPATH + "ktvsing.wav";

    private final int STATE_IDLE = 0;
    private final int STATE_RECORDING = 1;
    private final int STATE_PLAYING = 2;
    private int mState = STATE_IDLE;

    private int mCurPresetReverb = 1;
    private int mCurPresetEq = 1;
    private int mCurPresetTone = 0;

    private long mRecStartTime;
    private int mSingDelay;
    private int mRecDelay;

    private MediaClient mMediaClient;
    @Nullable
    private KtvSupportInfo mKtvSupportInfo;

    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.ktv);

        mMediaClient = MediaClient.initialize(getApplicationContext());
        if (mMediaClient.isSupported()) {
            Log.d(TAG, "KTV is supported for the new architecture of KTV APIs, you can use them");
            Toast.makeText(this, "Anything you can do with KTV new API", Toast.LENGTH_SHORT).show();
            if (MediaClientUtil.isOsOppo()) {
                mMediaClient.addOnConnectionSucceedListener(new OnConnectionSucceedListener() {
                    @Override
                    public void onConnectionSucceed() {
                        Log.i(TAG, "connectionSucceed");
                        onKtvNewApiSupported();
                    }
                });
            } else {
                onKtvNewApiSupported();
            }
        } else {
            Log.w(TAG, "KTV is not supported for the new architecture of KTV APIs, " +
                    "you CAN NOT use them");
            // do something
            // Other alternatives or prompts for your APP
            Log.w(TAG, "new APIs of KTV is not supported for your APP!");
            Toast.makeText(this, "KTV is not supported for the new architecture of KTV APIs, " +
                    "you CAN NOT use them", Toast.LENGTH_LONG).show();
        }
    }

    private void onKtvNewApiSupported() {
        String ktvSupportInfoJson = mMediaClient.getKaraokeSupportParameters();
        Log.d(TAG, "KTV ktvSupportInfoJson: " + ktvSupportInfoJson);

        KtvSupportInfo supportInfo = new Gson().fromJson(ktvSupportInfoJson, KtvSupportInfo.class);
        mKtvSupportInfo = supportInfo;
        Log.d(TAG, "KTV isSupportListenRecordSame: " + supportInfo.isSupportListenRecordSame);
        Log.d(TAG, "KTV isSupportExtSpeakerParam: " + supportInfo.isSupportExtSpeakerParam);
        Log.d(TAG, "KTV isSupportToneMode: " + supportInfo.isSupportToneMode);

        KtvSupportInfo.TrackSupportInfo targetTrackInfo =
                new KtvSupportInfo.TrackSupportInfo("3", "48000", "2", "8");
        Log.d(TAG, "KTV targetTrackInfo: " + targetTrackInfo.toString());

        // Whether the parameters of target client AudioRecord or AudioTrack
        // using KTV new API compatibility supports
        if (!MediaClientUtil.isTargetAudioSupported(supportInfo, targetTrackInfo)) {
            Toast.makeText(this, "NOT Supported! Something you can do quietly without " +
                    "AudioTrack of KTV", Toast.LENGTH_SHORT).show();
            return;
        }
        // track equals

        KtvSupportInfo.RecordSupportInfo targetRecordInfo =
                new KtvSupportInfo.RecordSupportInfo("3", "48000", "2", "8", "1");
        Log.d(TAG, "KTV targetRecordInfo: " + targetRecordInfo.toString());

        if (!MediaClientUtil.isTargetAudioSupported(supportInfo, targetRecordInfo)) {
            Toast.makeText(this, "NOT Supported! Something you can do quietly without " +
                    "AudioRecord of KTV", Toast.LENGTH_SHORT).show();
            return;
        }
        // record equals


        mContext = this;

        mAM = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        mAudioPlayer = new AudioPlayer(null);


        mBtnRecord = (Button) findViewById(R.id.btn_record);
        mBtnRecord.setText(R.string.text_record);
        mBtnRecord.setOnClickListener(this);
        mBtnRecPlay = (Button) findViewById(R.id.btn_record_play);
        mBtnRecPlay.setOnClickListener(this);

        mBtnSelint = (Button) findViewById(R.id.btn_silent);
        mBtnSelint.setOnClickListener(this);
        mBtnSing = (Button) findViewById(R.id.btn_selected);
        mBtnSing.setText(R.string.text_sing);
        mBtnSing.setOnClickListener(this);
        mBtnStop = (Button) findViewById(R.id.btn_stop);
        mBtnStop.setOnClickListener(this);
        mBtnDoSing = (Button) findViewById(R.id.btn_sing);
        mBtnDoSing.setOnClickListener(this);
        mBtnSingPlay = (Button) findViewById(R.id.btn_sing_play);
        mBtnSingPlay.setOnClickListener(this);

        mSpinnerMusicSelector = (Spinner) findViewById(R.id.spinner_music);
        mMusicListAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item);
        mMusicListAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        mMusicPathAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item);
//        Log.d(TAG, "find wav files START");
//        prepareList(Environment.getExternalStorageDirectory(), 0);
//        Log.d(TAG, "find wav files END");
        mSpinnerMusicSelector.setAdapter(mMusicListAdapter);
        mSpinnerMusicSelector.setOnItemSelectedListener(this);


        // Sets the type of sound effects for reverberation and gets additional types of reverb sound effects
        // init for reverb
        initReverb();

        // Sets the type of sound effects for EQ and gets additional types of EQ sound effects
        // init for EQ
        initEq();

        //init for OPPO magic voice
        initToneMode();

        mPlaySource = (ToggleButton) findViewById(R.id.tb_play_source);
        mPlaySource.setOnClickListener(this);
        mPlaySource.setChecked(DefaultPlaySource);

        mVolMic = (SeekBar) findViewById(R.id.skb_vol_mic);
        mVolMic.setMax(15);
        mVolMic.setProgress(mMediaClient.getMicVolParam());
        mVolMic.setOnSeekBarChangeListener(this);
        mTextVolMic = (TextView) findViewById(R.id.text_vol_mic);
        mTextVolMic.setText(Integer.toString(mMediaClient.getMicVolParam()));

        mOutSelect = (RadioGroup) findViewById(R.id.rg_ext_spkr);
        mHeadOut = (RadioButton) findViewById(R.id.rb_head_out);
        mExtSpkr = (RadioButton) findViewById(R.id.rb_ext_spkr);
        mOutSelect.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(RadioGroup group, int checkedId) {
                // TODO Auto-generated method stub
                if (checkedId == mHeadOut.getId()) {
                    setExtSpeakerParam(0);
                } else if (checkedId == mExtSpkr.getId()) {
                    setExtSpeakerParam(1);
                }
            }
        });

        mRecSource = (RadioGroup) findViewById(R.id.rg_rec_source);
        mRBMic = (RadioButton) findViewById(R.id.rb_mic_direct);
        mRBMixed = (RadioButton) findViewById(R.id.rb_mixed_out);
        mRBProcessMic = (RadioButton) findViewById(R.id.rb_process_mic);
        mRecSource.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(RadioGroup group, int checkedId) {
                // TODO Auto-generated method stub
                Log.d(TAG, "setListenRecordSame onCheckedChanged, called.");
                if (checkedId == mRBMic.getId()) {
                    // TODO NOTE: for oppo and vivo
                    setListenRecordSame(0);
                } else if (checkedId == mRBMixed.getId()) {
                    // TODO NOTE: only for oppo
                    if (!MediaClientUtil.isOsOppo()) {
                        Toast.makeText(KTVActivity.this, "Only support for oppo!", Toast.LENGTH_SHORT).show();
                        return;
                    }
                    setListenRecordSame(1);
                } else if (checkedId == mRBProcessMic.getId()) {
                    // TODO NOTE: only for vivo
                    if (!MediaClientUtil.isOsVivo()) {
                        Toast.makeText(KTVActivity.this, "Only support for vivo!", Toast.LENGTH_SHORT).show();
                        return;
                    }
                    setListenRecordSame(2);
                }
            }
        });

        // TODO NOTE: only working for oppo, not for vivo and xiaomi
        setToneMode(0);

        mReport = (TextView) findViewById(R.id.report);
        mReport.setText(mReport.getText(), TextView.BufferType.EDITABLE);

        requestPermissionIfNeed();

        mKTVMode = (ToggleButton) findViewById(R.id.tb_ktv_mode);
        mKTVMode.setOnClickListener(this);
        closeKTVDevice(true);
        // device status closed by default
        mKTVMode.setChecked(DefaultKTVMode);
    }

    @Override
    protected void onResume() {
        //mKTVMode.setChecked(isKTVMode());
        super.onResume();

//        requestPermissionIfNeed();

        if (mPlaySource != null) {
            mPlaySource.setChecked(mMediaClient.getPlayFeedbackParam() == 1);
        }
    }


    @Override
    protected void onStop() {
        super.onStop();
        Log.e(TAG, "onStop");
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.e(TAG, "onPause");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.e(TAG, "onDestroy");

        stopMusic();
        stopRecord();

        if (mAudioPlayer != null) {
            mAudioPlayer.stop();
            mAudioPlayer.release();
        }
        mAudioPlayer = null;
//        mSlientPlayer = null;

        // TODO NOTE: Maybe we should and MUST DO this if we do not use KTV functions!
        closeKTVDevice(true);
    }

//    private boolean mCachedCurPlayFeedbackParam = false;
//    private boolean mCachedLastPlayFeedbackParam = false;
//    private int mCachedCurListenRecordSame = 0;
//    private int mCachedLastListenRecordSame = 0;
//    private int mCachedCurExtSpeakerParam = 0;
//    private int mCachedLastExtSpeakerParam = 0;


    /**
     * Close KTV
     */
    public void closeKTVDevice() {
        closeKTVDevice(false);
    }

    public void closeKTVDevice(boolean release) {
        if (mMediaClient.isSupported()) {
            Log.d(TAG, "KTV is supported for the new architecture of KTV APIs, you can use them");

            if (release) {
                // TODO NOTE: Maybe we should and MUST DO these after we do not use KTV functions!
                setPlayFeedbackParam(false);
                setListenRecordSame(0);
                setExtSpeakerParam(0);
                // TODO: 2022/8/2 FIXME????
//            setToneMode(0);
            }

            mMediaClient.closeKTVDevice();
        } else {
            Log.w(TAG, "KTV is not supported for the new architecture of KTV APIs, " +
                    "you CAN NOT use them");
            // do something
            // Other alternatives or prompts for your APP
            Log.w(TAG, "new APIs of KTV is not supported for your APP!");
        }
    }

    public void setExtSpeakerParam(int param) {
        if (!mMediaClient.isSupported()) {
            Log.w(TAG, "KTV is not supported for the new architecture of KTV APIs, " +
                    "you CAN NOT use them");
            // do something
            // Other alternatives or prompts for your APP
            Log.w(TAG, "new APIs of KTV is not supported for your APP!");
            return;
        }

        Log.d(TAG, "KTV is supported for the new architecture of KTV APIs, you can use them");
        // TODO NOTE: only working for vivo, not for xiaomi and oppo
        // TODO NOTE: Must use the field supported to check API!!! NOT directly use [MediaClientUtil.isOsVivo()]
        if (mKtvSupportInfo != null && mKtvSupportInfo.isSupportExtSpeakerParam) {
//            mCachedLastExtSpeakerParam = mCachedCurExtSpeakerParam;
            // Or use this API
//            mCachedLastExtSpeakerParam = mMediaClient.getExtSpeakerParam();

            mMediaClient.setExtSpeakerParam(param);

//            mCachedCurExtSpeakerParam = param;

            if (mHeadOut == null) {
                return;
            }
            if (param == 0 && !mHeadOut.isChecked()) {
                mHeadOut.setChecked(true);
                mExtSpkr.setChecked(false);
            } else if (param == 1 && !mExtSpkr.isChecked()) {
                mHeadOut.setChecked(false);
                mExtSpkr.setChecked(true);
            }
        } else {
            Log.d(TAG, "setExtSpeakerParam of KTV is NOT supported for this phone, " +
                    "only working for vivo and oppo, not for xiaomi");
            Toast.makeText(this, "setExtSpeakerParam of KTV is NOT supported for this phone, " +
                    "only working for vivo and oppo, not for xiaomi", Toast.LENGTH_SHORT).show();
        }
    }

    public void setListenRecordSame(int param) {
        if (!mMediaClient.isSupported()) {
            Log.w(TAG, "KTV is not supported for the new architecture of KTV APIs, " +
                    "you CAN NOT use them");
            // do something
            // Other alternatives or prompts for your APP
            Log.w(TAG, "new APIs of KTV is not supported for your APP!");
            return;
        }

        Log.d(TAG, "KTV is supported for the new architecture of KTV APIs, you can use them");
        // TODO NOTE: only working for vivo and oppo, not for xiaomi
        // TODO NOTE: Must use the field supported to check API!!! NOT directly use [MediaClientUtil.isOsXiaoMi()]
        if (mKtvSupportInfo != null && mKtvSupportInfo.isSupportListenRecordSame) {
//            Log.d(TAG, "setListenRecordSame START, mCachedLastListenRecordSame: "
//                    + mCachedLastListenRecordSame + ", mCachedCurListenRecordSame: " +
//                    mCachedCurListenRecordSame + ", param: " + param);
//            mCachedLastListenRecordSame = mCachedCurListenRecordSame;
            // Or use this API
//            mCachedLastListenRecordSame = mMediaClient.getListenRecordSame();

            mMediaClient.setListenRecordSame(param);

//            mCachedCurListenRecordSame = param;
//            Log.d(TAG, "setListenRecordSame END, mCachedLastListenRecordSame: "
//                    + mCachedLastListenRecordSame + ", mCachedCurListenRecordSame: " +
//                    mCachedCurListenRecordSame + ", param: " + param);

            if (mRBMic == null) {
                return;
            }
            if (param == 0 && !mRBMic.isChecked()) {
                mRBMic.setChecked(true);
                mRBMixed.setChecked(false);
                mRBProcessMic.setChecked(false);
            } else if (MediaClientUtil.isOsOppo() && param == 1 && !mRBMixed.isChecked()) {
                mRBMic.setChecked(false);
                mRBMixed.setChecked(true);
                mRBProcessMic.setChecked(false);
            } else if (MediaClientUtil.isOsVivo() && param == 2 && !mRBProcessMic.isChecked()) {
                mRBMic.setChecked(false);
                mRBMixed.setChecked(false);
                mRBProcessMic.setChecked(true);
            }
        } else {
            Log.d(TAG, "setListenRecordSame of KTV is NOT supported for this phone, " +
                    "only working for vivo and oppo, not for xiaomi");
            Toast.makeText(this, "setListenRecordSame of KTV is NOT supported for this phone, " +
                    "only working for vivo and oppo, not for xiaomi", Toast.LENGTH_SHORT).show();
        }
    }

    public void setToneMode(int toneValue) {
        if (!mMediaClient.isSupported()) {
            Log.w(TAG, "KTV is not supported for the new architecture of KTV APIs, " +
                    "you CAN NOT use them");
            // do something
            // Other alternatives or prompts for your APP
            Log.w(TAG, "new APIs of KTV is not supported for your APP!");
            return;
        }

        Log.d(TAG, "KTV is supported for the new architecture of KTV APIs, you can use them");
        // TODO NOTE: only working for oppo, not for vivo and xiaomi
        // TODO NOTE: Must use the field supported to check API!!! NOT directly use [MediaClientUtil.isOsOppo()]
        if (mKtvSupportInfo != null && mKtvSupportInfo.isSupportToneMode) {
            mMediaClient.setToneMode(toneValue);
        } else {
            Log.d(TAG, "setToneMode of KTV is NOT supported for this phone, " +
                    "only working for vivo and oppo, not for xiaomi");
            Toast.makeText(this, "setToneMode of KTV is NOT supported for this phone, " +
                    "only working for vivo and oppo, not for xiaomi", Toast.LENGTH_SHORT).show();
        }
    }

    private void presetExt(@NonNull Map<String, Integer> map, @NonNull String[] soundList,
                           @NonNull Supplier<int[]> provider) {
        int[] extSoundTypes = provider.get();
        if (extSoundTypes != null) {
            for (int extSoundType : extSoundTypes) {
                Log.d(TAG, "presetExt KTV extSoundTypes: " + extSoundType);
                String typeName = convertSoundEffectName(extSoundType, soundList);
                if (typeName == null) {
                    continue;
                }
                map.put(typeName, extSoundType);
                // You can use it
                // mMediaClient.setMixerSoundType(extSoundType);
                // or
                // mMediaClient.setEqualizerType(extSoundType);
            }
        }
    }

    private void presetDefaultSoundEffect(@NonNull Map<String, Integer> map,
                                          @NonNull SoundEffectConvert<Integer, String> sec) {
        for (int i = 0; i < DEFAULT_MAX_COUNT_REVERB_EQ; i++) {
//            String typeName = convertMixerSoundName(i);
            String typeName = sec.convert(i);
            if (typeName == null) {
                continue;
            }
            map.put(typeName, i);
        }
    }

    private void initSpinnerSoundEffectView(@NonNull Spinner spinner, @NonNull Map<String, Integer> soundList,
                                            int curPresetSoundEffect) {
        ArrayAdapter<String> reverbPresets = new ArrayAdapter<String>(this,
                R.layout.spinner_item);
        reverbPresets.setDropDownViewResource(R.layout.dropdown_style);
        soundList.forEach((k, v) -> {
            reverbPresets.add(k);
        });
        spinner.setAdapter(reverbPresets);
        spinner.setOnItemSelectedListener(this);
        spinner.setSelection(curPresetSoundEffect, true);
    }

    public interface SoundEffectConvert<In, Out> {
        Out convert(In in);
    }

    private void initReverb() {
        presetReverb();
    }

    private void presetReverb() {
        // reverb default type ( 0:无、1:KTV、2:剧场、3:音乐厅、4:录音棚 )
        // preset default to reverb sound effect mapping
        presetDefaultReverb();
        // For VIVO reverb extensions as bellow:
        // 10: 温暖, 11: 空灵, 12: 3D迷幻, 13: 老唱片
        presetExtReverb();

        // init reverb view
        initSpinnerReverb();
    }

    private void presetDefaultReverb() {
        presetDefaultSoundEffect(mPresetModeMapReverb, this::convertMixerSoundName);
    }

    private String convertMixerSoundName(int soundType) {
        return convertSoundEffectName(soundType, ReverbPresetMode);
    }

    private void presetExtReverb() {
        presetExt(mPresetModeMapReverb, ReverbPresetMode, () -> mMediaClient.getExtMixerSoundType());
    }

    private void initSpinnerReverb() {
        mSpinnerPresetModeReverb = (Spinner) findViewById(R.id.spinner_preset_reverb);
        initSpinnerSoundEffectView(mSpinnerPresetModeReverb, mPresetModeMapReverb, mCurPresetReverb);
    }

    private void setReverbSoundType(int soundIdx) {
        mMediaClient.setMixerSoundType(convertSoundTypeByIdx(soundIdx, ReverbPresetMode, mPresetModeMapReverb));
    }

    private void initEq() {
        presetEq();
    }

    private void presetEq() {
        // EQ default type ( 0:无、1:标准、2:浑厚、3:清脆、4:明亮 )
        // preset default to EQ sound effect mapping
        presetDefaultEq();

        // For VIVO EQ extensions as bellow:
        // 10: 温暖, 11: 空灵, 12: 3D迷幻, 13: KTV
        presetExtEq();

        // init reverb view
        initSpinnerEq();
    }

    private void presetDefaultEq() {
        presetDefaultSoundEffect(mPresetModeMapEq, this::convertEqName);
    }

    private String convertEqName(int soundType) {
        return convertSoundEffectName(soundType, EqPresetMode);
    }

    private void presetExtEq() {
        presetExt(mPresetModeMapEq, EqPresetMode, () -> mMediaClient.getExtEqualizerType());
    }

    private void initSpinnerEq() {
        mSpinnerPresetModeEq = (Spinner) findViewById(R.id.spinner_preset_eq);
        initSpinnerSoundEffectView(mSpinnerPresetModeEq, mPresetModeMapEq, mCurPresetEq);
    }

    private void setEqSoundType(int soundIdx) {
        mMediaClient.setMixerSoundType(convertSoundTypeByIdx(soundIdx, EqPresetMode, mPresetModeMapEq));
    }

    private String convertSoundEffectName(int soundType, @NonNull String[] soundList) {
        // reverb type ( 0:无、1:KTV、2:剧场、3:音乐厅、4:录音棚 )
        // For VIVO reverb extensions as bellow:
        // 10: 温暖, 11: 空灵, 12: 3D迷幻, 13: 老唱片

        // EQ type ( 0:无、1:标准、2:浑厚、3:清脆、4:明亮 )
        // preset default to sound effect mapping
        // For VIVO EQ extensions as bellow:
        // 10: 温暖, 11: 空灵, 12: 3D迷幻, 13: KTV

        String typeName = null;
        int soundIdx = -1;
        switch (soundType) {
            case 0:
            case 1:
            case 2:
            case 3:
            case 4:
            case 10:
            case 11:
            case 12:
            case 13:
                soundIdx = soundType;
                if (soundType > DEFAULT_MAX_COUNT_REVERB_EQ) {
                    soundIdx = soundType - 5;
                }
                // TODO check out of index ???
                typeName = soundList[soundIdx];
                break;

            default:
                break;
        }
        Log.d(TAG, "convertSoundEffectName: " + typeName + ", soundIdx: " + soundIdx +
                ", soundType: " + soundType);
        return typeName;
    }

    private int convertSoundTypeByIdx(int soundIdx, @NonNull String[] soundList, @NonNull Map<String, Integer> map) {
        if (soundIdx < 0 || soundIdx >= soundList.length) {
            return -1;
        }
        // TODO check out of index and NullException ???
        return map.get(soundList[soundIdx]);
//        return convertSoundEffectType(soundList[soundIdx], soundList);
    }

    @Deprecated
    private int convertSoundTypeByIdx_1(int soundIdx, @NonNull String[] soundList) {
        // TODO check out of index ???
        if (soundIdx < 0 || soundIdx >= soundList.length) {
            return -1;
        }
        return convertSoundEffectType(soundList[soundIdx], soundList);
    }

    @Deprecated
    private int convertSoundEffectType(String typeName, @NonNull String[] soundList) {
        // reverb type ( 0:无、1:KTV、2:剧场、3:音乐厅、4:录音棚 )
        // For VIVO reverb extensions as bellow:
        // 10: 温暖, 11: 空灵, 12: 3D迷幻, 13: 老唱片

        // EQ type ( 0:无、1:标准、2:浑厚、3:清脆、4:明亮 )
        // preset default to sound effect mapping
        // For VIVO EQ extensions as bellow:
        // 10: 温暖, 11: 空灵, 12: 3D迷幻, 13: KTV

        int soundType = -1;
        int soundIdx = -1;
        for (int i = 0; i < soundList.length; i++) {
            String mode = soundList[i];
            if (mode != null && mode.equals(typeName)) {
                soundIdx = i;
                break;
            }
        }

        if (soundIdx == -1) {
            return soundType;
        }

        soundType = soundIdx;
        if (soundIdx > DEFAULT_MAX_COUNT_REVERB_EQ) {
            soundType = soundIdx + 5;
        }
        Log.d(TAG, "convertSoundEffectType: " + typeName + ", soundIdx: " + soundIdx +
                ", soundType: " + soundType);
        return soundType;
    }

    // setToneMode, to set magic voice
    private void initToneMode() {
        presetToneMode();
    }

    private void presetToneMode() {
        // magic voice type [-12, 12]
        presetDefaultTone();

        // init magic voice view
        initSpinnerTone();
    }

    private void presetDefaultTone() {
        presetDefaultToneMode(mPresetModeMapTone, this::convertToneModeName);
    }

    private void presetDefaultToneMode(@NonNull Map<String, Integer> map,
                                       @NonNull SoundEffectConvert<Integer, String> sec) {
        for (int i = 0; i < 25; i++) {
//            String typeName = convertMixerSoundName(i);
            String typeName = sec.convert(i);
            if (typeName == null) {
                continue;
            }
            map.put(typeName, i);
        }
    }

    private String convertToneModeName(int toneMode) {
        return convertToneModeNames(toneMode, TonePresetMode);
    }

    private void initSpinnerTone() {
        mSpinnerPresetModeTone = (Spinner) findViewById(R.id.spinner_preset_tone);
        initSpinnerToneModeView(mSpinnerPresetModeTone, TonePresetMode, mCurPresetTone);
    }

    private void setMagicVoice(int toneMode) {
        int tone = convertSoundTypeByIdx(toneMode, TonePresetMode, mPresetModeMapTone) - 12;
        Log.d(TAG, "setToneMode " + tone);
        setToneMode(tone);
    }

    private void initSpinnerToneModeView(@NonNull Spinner spinner, @NonNull String[] soundList,
                                         int curPresetSoundEffect) {
        ArrayAdapter<String> reverbPresets = new ArrayAdapter<String>(this,
                R.layout.spinner_item);
        reverbPresets.setDropDownViewResource(R.layout.dropdown_style);
        for (String s : soundList) {
            reverbPresets.add(s);
        }
        spinner.setAdapter(reverbPresets);
        spinner.setOnItemSelectedListener(this);
        spinner.setSelection(curPresetSoundEffect, true);
    }

    private String convertToneModeNames(int soundType, @NonNull String[] soundList) {
        // magic voice type [-12,12]

        String typeName = null;
        int soundIdx = -1;
        switch (soundType - 12) {
            case -12:
            case -11:
            case -10:
            case -9:
            case -8:
            case -7:
            case -6:
            case -5:
            case -4:
            case -3:
            case -2:
            case -1:
            case 0:
            case 1:
            case 2:
            case 3:
            case 4:
            case 5:
            case 6:
            case 7:
            case 8:
            case 9:
            case 10:
            case 11:
            case 12:
                soundIdx = soundType;

                // TODO check out of index ???
                typeName = soundList[soundIdx];
                break;

            default:
                break;
        }
        Log.d(TAG, "convertSoundEffectName: " + typeName + ", soundIdx: " + soundIdx +
                ", soundType: " + soundType);
        return typeName;
    }

    @Override
    public void onClick(View arg0) {
        if (arg0 == mKTVMode) {
            enableKTVMode();
        } else if (arg0 == mBtnSelint) {
            Toast.makeText(this, "Silent audio data does not need to be played", Toast.LENGTH_SHORT).show();
            playSilent();
        } else if (arg0 == mBtnStop) {
            stopMusic();
        } else if (arg0 == mBtnSing) {
            if (isRecording) {
                Log.v(SING_TAG, "stop ktv sing");
                stopRecord();
                stopMusic();
                closeKTVDevice();

                mBtnSing.setText(R.string.text_sing);
            } else {
                if (mMusicPathAdapter.getCount() <= 0) {
                    Toast.makeText(this, "Music wav file under sdcard is empty", Toast.LENGTH_SHORT).show();
                    return;
                }
                Log.v(SING_TAG, "start ktv sing");
                sendMsg(mKTVHandler, MSG_SLIENT_START, 0, 0, DelaySlientStart);
                sendMsg(mKTVHandler, MSG_KTV_MODE_ENABLE, 0, 0, DelayKTVEnable);
                sendMsg(mKTVHandler, MSG_RECORD_START, 0, 0, DelayRecordStart);
                sendMsg(mKTVHandler, MSG_PLAYBACK_START, 0, 0, DelayPlayStart);

                mBtnSing.setText(R.string.text_stop_playing);
            }
        } else if (arg0 == mBtnRecord) {
            if (isRecording) {
                stopRecord();
                stopMusic();
                closeKTVDevice();

                mBtnRecord.setText(R.string.text_record);
            } else {
                if (!mAudioPlayer.isPlaying())
                    playSilent();
                startRecord();

                // TODO: 2022/7/20 NOTE: Maybe we DO NOT use this code to change the logical of function!
                //  This is a standalone feature test in here!
                // enableKTVMode(true);
                enableKTVMode();

                mBtnRecord.setText(R.string.text_stop_playing);
            }
        } else if (arg0 == mBtnRecPlay) {
            playMusic(RecordWAV);
        } else if (arg0 == mPlaySource) {
            setPlayFeedbackParam();
        } else if (arg0 == mBtnDoSing) {
            if (mMusicPathAdapter.getCount() <= 0) {
                Toast.makeText(this, "Music wav file under sdcard is empty", Toast.LENGTH_SHORT).show();
                return;
            }
            new Thread(new CompondSingThread()).start();
        } else if (arg0 == mBtnSingPlay) {
            playMusic(SINGWAV);
        }
    }

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress,
                                  boolean fromUser) {
        if (mVolMic == seekBar) {
            mMediaClient.setMicVolParam(progress);
            mTextVolMic.setText(Integer.toString(progress));
        }
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
        // TODO Auto-generated method stub

    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
        // TODO Auto-generated method stub

    }

    @Override
    public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2,
                               long arg3) {
        if (arg0 == mSpinnerPresetModeReverb) {
            if (mCurPresetReverb != arg2) {
                setReverbSoundType(arg2);
//                mMediaClient.setMixerSoundType(convertSoundTypeByIdx(arg2, ReverbPresetMode));
//                mMediaClient.setEqualizerType(arg2);
                mCurPresetReverb = arg2;
            }
        } else if (arg0 == mSpinnerPresetModeEq) {
            if (mCurPresetEq != arg2) {
//                mMediaClient.setMixerSoundType(arg2);
//                mMediaClient.setEqualizerType(arg2);
                setEqSoundType(arg2);
                mCurPresetEq = arg2;
            }
        } else if (arg0 == mSpinnerPresetModeTone) {
            if (mCurPresetTone != arg2) {
                setMagicVoice(arg2);
                mCurPresetTone = arg2;
            }
        }
    }

    @Override
    public void onNothingSelected(AdapterView<?> arg0) {
        // TODO Auto-generated method stub
    }

    @SuppressLint("HandlerLeak")
    private Handler mReportHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            Editable text = (Editable) mReport.getText();
            switch (msg.what) {
                case MSG_REPORT_RECORD_COST:
                    text.append("\nstartRecording cost ");
                    text.append(Integer.toString(msg.arg1));
                    text.append(" ms");
                    break;
                case MSG_REPORT_READ_COST:
                    text.append("\nRead first frame cost ");
                    text.append(Integer.toString(msg.arg1));
                    text.append(" ms");
                    break;
                case MSG_REPORT_TOTAL_COST:
                    text.append("\nTotal Record Start cost ");
                    text.append(Integer.toString(msg.arg1));
                    text.append(" ms");
                    break;
                case MSG_REPORT_SING_START:
                    text.append("\nSing Compound Start!");
                    break;
                case MSG_REPORT_SING_FINISH:
                    text.append("\nSing Compound Finished!");
                    break;
                default:
                    break;
            }
        }
    };

    @SuppressLint("HandlerLeak")
    private Handler mKTVHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_SLIENT_START:
                    Log.v(SING_TAG, "MSG_SLIENT_START");
                    playSilent();
                    break;
                case MSG_KTV_MODE_ENABLE:
                    Log.v(SING_TAG, "MSG_KTV_MODE_ENABLE");
                    // TODO: 2022/7/20 Here we must open if closed when user sing
                    enableKTVMode(true);
                    break;
                case MSG_RECORD_START:
                    Log.v(SING_TAG, "MSG_RECORD_START");
                    startRecord();
                    break;
                case MSG_PLAYBACK_START:
                    String path = mMusicPathAdapter.getItem(mSpinnerMusicSelector.getSelectedItemPosition());
                    Log.v(SING_TAG, "MSG_PLAYBACK_START: " + path);
                    playMusic(path);
                    break;
                default:
                    break;
            }
        }
    };

    private static void sendMsg(Handler handler, int what, int arg1, int arg2, int delay) {
        // FIXME: if here are multi clients, this line may remove the wrong msg.
        handler.removeMessages(what);
        handler.sendMessageDelayed(
                handler.obtainMessage(what, arg1, arg2), delay);
    }

//	public static final int DEVICE_OUT_WIRED_HEADSET = 0x4;
//	private boolean isHeadsetHasMic()
//	{
//		AudioManager localAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
//		int type =  localAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC);
//		Log.v(TAG, "KTV isHeadsetHasMic: type = " + type);
//		return (type == AudioManager.DEVICE_OUT_WIRED_HEADSET);
//	}

    private boolean isHeadsetPlugIn() {
        AudioManager localAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        return localAudioManager.isWiredHeadsetOn();
    }

    private void playSilent() {
        // TODO Silent data does not need to be played
//        if (mSlientPlayer == null)
//            mSlientPlayer = new SlientPlayer();
//
//        if (!mSlientPlayer.isPlaying())
//            mSlientPlayer.play();
    }

    private void playMusic(String path) {
        Log.d(TAG, "playMusic path: " + path);
        if (mAudioPlayer.isPlaying()) {
            mAudioPlayer.pause();
        }

        try {
            // channel 3 flag for AudioFormat.CHANNEL_CONFIGURATION_STEREO
            // format 2 flag for 16bit
            AudioParamBean audioParam = new AudioParamBean(44100, 3, 2);
            mAudioPlayer.setAudioParam(audioParam);

            // 读取PCM文件内容
            byte[] data = null;
            try {
//                InputStream inputStream = getResources().openRawResource(R.raw.rainy_city);
                data = toByteArray(new FileInputStream(path), path.endsWith(".wav"));
            } catch (Exception e) {
                e.printStackTrace();
            }

            mAudioPlayer.setDataSource(data);
            mAudioPlayer.prepare();
            mAudioPlayer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /***
     * 将inputStream转换为byte[]
     */
    public static byte[] toByteArray(InputStream input, boolean skipWAVHeader) throws Exception {
        Log.d(TAG, "playMusic toByteArray skipWAVHeader: " + skipWAVHeader);
        if (skipWAVHeader) {
            // only skip once
            long actSkipLength = input.skip(WAVE_HEADER_NUM);
            Log.d(TAG, "playMusic toByteArray actSkipLength: " + actSkipLength);
            if (actSkipLength != WAVE_HEADER_NUM) {
                Log.w(TAG, "Warning! playMusic toByteArray actSkipLength is not 44 !");
            }
        }
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024 * 4];
        int n;
        while (-1 != (n = input.read(buffer))) {
            output.write(buffer, 0, n);
        }
        return output.toByteArray();
    }

    private void stopMusic() {
        if (mAudioPlayer.isPlaying()) {
            mAudioPlayer.pause();
        }

//        if (mSlientPlayer != null
//                && mSlientPlayer.isPlaying())
//            mSlientPlayer.stop();
    }

    private void startRecord() {
        Log.v(TAG, "startRecord getListenRecordSame mode: " + mMediaClient.getListenRecordSame());

        mRecStartTime = System.currentTimeMillis();
        mRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SampleRateInHz,
                AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT, RecBufSize * 2 * 2);
//		mRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
//								SampleRateInHz,
//								AudioFormat.CHANNEL_IN_STEREO,
//								AudioFormat.ENCODING_PCM_16BIT,
//								RecBufSize*2*2);

        mRecord.startRecording();
        isRecording = true;

        int duration = (int) (System.currentTimeMillis() - mRecStartTime);

        sendMsg(mReportHandler, MSG_REPORT_RECORD_COST, duration, 0, 0);
        mSingDelay = 0;

        new Thread(new AudioRecordThread()).start();
    }

    private void stopRecord() {
        Log.v(TAG, "stopRecord");

        isRecording = false;

        synchronized (mRecordLock) {
            if (mRecord != null) {
                mRecord.stop();
                mRecord.release();
                mRecord = null;
            }
        }
    }

    class AudioRecordThread implements Runnable {

        @Override
        public void run() {
            writeDateTOFile();
            copyWaveFile(AudioPCM, RecordWAV);
            Log.v(TAG, "copyWaveFile over!");
			
            /*
            try {  
	            File file = new File(AudioPCM);  
	            if (file.exists()) {  
	                file.delete();  
	            }
	        } catch (Exception e) {  
	            e.printStackTrace();  
	        } 
	        */
        }
    }

    private void writeDateTOFile() {
        byte[] recorddata = new byte[RecBufSize * 4];
        FileOutputStream fos = null;
        int readsize = 0;
        int serial = 0;

        try {
            File file = new File(AudioPCM);
            Log.d(SING_TAG, "AudioRecordThread run 111 AudioPCM file: " + file);
            if (file.exists()) {
                Log.d(SING_TAG, "AudioRecordThread run 222 AudioPCM file: " + file);
                file.delete();
            }
            Log.d(SING_TAG, "AudioRecordThread run 333 AudioPCM file: " + file);
            file.createNewFile();
            Log.d(SING_TAG, "AudioRecordThread run 444 AudioPCM file: " + file);
            fos = new FileOutputStream(file);
        } catch (Exception e) {
            Log.e(SING_TAG, "AudioRecordThread run err: " + e.getMessage());
            e.printStackTrace();
        }

        Log.v(SING_TAG, "AudioRecordThread run");

        while (isRecording) {
            synchronized (mRecordLock) {
                if (mRecord != null) {
                    long startTimeMs = System.currentTimeMillis();
                    readsize = mRecord.read(recorddata, 0, RecBufSize * 4);

                    int duration = (int) (System.currentTimeMillis() - startTimeMs);
                    if (serial == 0) {
                        serial++;
                        sendMsg(mReportHandler, MSG_REPORT_READ_COST, duration, 0, 0);
                        int total = (int) (System.currentTimeMillis() - mRecStartTime);
                        mRecDelay = total;
                        sendMsg(mReportHandler, MSG_REPORT_TOTAL_COST, total, 0, 0);
                    }
                }
            }

            if (AudioRecord.ERROR_INVALID_OPERATION != readsize) {
                try {
                    fos.write(recorddata);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void copyWaveFile(String inFilename, String outFilename) {
        FileInputStream in = null;
        FileOutputStream out = null;
        long totalAudioLen = 0;
        long totalDataLen = totalAudioLen + 36;
        long longSampleRate = SampleRateInHz;
        int channels = 2;
        long byteRate = 16 * SampleRateInHz * channels / 8;
        byte[] data = new byte[256];

        try {
            in = new FileInputStream(inFilename);
            out = new FileOutputStream(outFilename);
            totalAudioLen = in.getChannel().size();
            totalDataLen = totalAudioLen + 36;
            WriteWaveFileHeader(out, totalAudioLen, totalDataLen,
                    longSampleRate, channels, byteRate);
            while (in.read(data) != -1) {
                out.write(data);
            }
            in.close();
            out.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 复制单个文件
     *
     * @param oldpath string 原文件路径 如：c:/fqf.txt
     * @param newpath string 复制后路径 如：f:/fqf.txt
     * @return boolean
     */
    public void copyfile(String oldpath, String newpath) {
        try {
            File old = new File(oldpath);
            if (old.exists()) { //文件存在时
                copyfile(new FileInputStream(oldpath), newpath); //读入原文件
            } else {
                Log.d(TAG, "复制单个文件操作出错");
            }
        } catch (Exception e) {
            Log.d(TAG, "复制单个文件操作出错, err: " + e.getMessage());
            e.printStackTrace();
        }
    }

    public void copyfile(InputStream instream, String newpath) {
        try {
            File file = new File(newpath);
            if (file.exists()) {
                file.delete();
            }
            file.createNewFile();

            int bytesum = 0;
            int byteread = 0;
            if (instream != null) { //文件流存在时
                FileOutputStream fs = new FileOutputStream(newpath);
                byte[] buffer = new byte[1444];
                int length;
                while ((byteread = instream.read(buffer)) != -1) {
                    bytesum += byteread; //字节数 文件大小
//                    Log.d(TAG, "bytesum: " + bytesum);
                    fs.write(buffer, 0, byteread);
                }
                instream.close();
                Log.d(TAG, "copyfile bytesum: " + bytesum);
            }
        } catch (Exception e) {
            Log.d(TAG, "复制单个文件操作出错, err: " + e.getMessage());
            e.printStackTrace();
        }
    }

    private static final int WAVE_HEADER_NUM = 44;

    private void WriteWaveFileHeader(FileOutputStream out, long totalAudioLen,
                                     long totalDataLen, long longSampleRate, int channels, long byteRate)
            throws IOException {

        byte[] header = new byte[WAVE_HEADER_NUM];
        header[0] = 'R'; // RIFF/WAVE header   
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        header[8] = 'W';
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        header[12] = 'f'; // 'fmt ' chunk   
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';
        header[16] = 16; // 4 bytes: size of 'fmt ' chunk   
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        header[20] = 1; // format = 1   
        header[21] = 0;
        header[22] = (byte) channels;
        header[23] = 0;
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        header[32] = (byte) (2 * 16 / 8); // block align   
        header[33] = 0;
        header[34] = 16; // bits per sample   
        header[35] = 0;
        header[36] = 'd';
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);

        out.write(header, 0, WAVE_HEADER_NUM);
    }

    private void prepareList(File sddir, int dirlevel) {
        try {
            prepareList_1(sddir, dirlevel);
        } catch (Exception ex) {
            Log.d(TAG, "find wav err: " + ex.getMessage());
            ex.printStackTrace();
        }
    }

    private void prepareList_1(File sddir, int dirlevel) {
        File[] files = sddir.listFiles();
        Log.d(TAG, "prepareList_1 DIR getPath: " + sddir.getPath());
        Log.d(TAG, "prepareList_1 DIR getName: " + sddir.getName());
        if (files != null) {
            for (File file : files) {
                if (file.isFile()) {
                    if (file.getName().endsWith(".pcm") || file.getName().endsWith(".wav")) {
                        Log.d(TAG, "find wav files getName: " + file.getName());
                        Log.d(TAG, "find wav files getPath: " + file.getPath());
                        mMusicPathAdapter.add(file.getPath());
                        mMusicListAdapter.add(file.getName());
                    }
                } else if (file.isDirectory() && dirlevel > 0) {
                    prepareList(file, dirlevel - 1);
                }
            }
        }
    }

    // TODO: 2022/7/20 FATAL NOTE:
    //  This method must be called after opening record when your app must be foreground!
    private void setPlayFeedbackParam() {
        if (mPlaySource == null) {
            return;
        }
        setPlayFeedbackParam(mPlaySource.isChecked());
    }

    private void setPlayFeedbackParam(boolean doOpen) {
//        mCachedLastPlayFeedbackParam = mCachedCurPlayFeedbackParam;
        // Or use them!
//        mCachedLastPlayFeedbackParam = mPlaySource.isChecked();
//        mCachedLastPlayFeedbackParam = mMediaClient.getPlayFeedbackParam() == 1;

        if (mPlaySource == null) {
            Log.d(TAG, "setPlayFeedbackParam, mPlaySource=null, doOpen: " + doOpen);
        } else {
            Log.d(TAG, "setPlayFeedbackParam, doOpen: " + doOpen +
                    ", setPlayFeedback.isChecked: " + mPlaySource.isChecked());
        }

        // KTV playback feedback mode opened if checked
        mMediaClient.setPlayFeedbackParam(doOpen ? 1 : 0);
//        mCachedCurPlayFeedbackParam = doOpen;

        if (mPlaySource == null) {
            return;
        }
        if (doOpen != mPlaySource.isChecked()) {
            mPlaySource.setChecked(doOpen);
        }
    }

    private void enableKTVMode() {
        enableKTVMode(mKTVMode.isChecked());
    }

    private void enableKTVMode(boolean doOpen) {
        Log.d(TAG, "enableKTVMode (true for open)mKTVMode.isChecked: " +
                mKTVMode.isChecked() + ", doOpen: " + doOpen);
        if (!mKTVMode.isChecked() && doOpen) {
            mKTVMode.setChecked(true);
        }
        // KTV mode opened if checked
        if (mKTVMode.isChecked()) {
            openKTVDevice();
        } else {
            closeKTVDevice();
        }
    }

    private void openKTVDevice() {
        // redundant, garbage, meaningless calls for demo ...
//        if (mCachedLastPlayFeedbackParam != mCachedCurPlayFeedbackParam) {
//            Log.d(TAG, "enableKTVMode, fallback to previous status mCachedLastPlayFeedbackParam: "
//                    + mCachedLastPlayFeedbackParam);
//            // fallback to previous status
//            setPlayFeedbackParam(mCachedLastPlayFeedbackParam);
//        } else {
//            setPlayFeedbackParam();
//        }
//        if (mCachedLastListenRecordSame != mCachedCurListenRecordSame) {
//            Log.d(TAG, "enableKTVMode, fallback to previous status mCachedLastListenRecordSame: "
//                    + mCachedLastListenRecordSame);
//            setListenRecordSame(mCachedLastListenRecordSame);
//        }
//        if (mCachedLastExtSpeakerParam != mCachedCurExtSpeakerParam) {
//            Log.d(TAG, "enableKTVMode, fallback to previous status mCachedLastExtSpeakerParam: "
//                    + mCachedLastExtSpeakerParam);
//            setExtSpeakerParam(mCachedLastExtSpeakerParam);
//        }

        setPlayFeedbackParam();
        mMediaClient.setMixerSoundType(mSpinnerPresetModeReverb.getSelectedItemPosition());
        mMediaClient.setEqualizerType(mSpinnerPresetModeEq.getSelectedItemPosition());
        mMediaClient.openKTVDevice();
    }

    private void doSingConvert(String musicFilename,
                               String recFilename, String singFilename, int aligncount) {
        long totalAudioLen = 0;
        long totalDataLen = totalAudioLen + 36;
        long longSampleRate = SampleRateInHz;
        int channels = 2;
        long byteRate = 16 * SampleRateInHz * channels / 8;

        FileInputStream music = null;
        FileInputStream rec = null;
        FileOutputStream sing = null;

        int bufcount = 4000;
        byte[] wavhead = new byte[WAVE_HEADER_NUM];
        byte[] musicbuf = new byte[bufcount * 2 * 2];
        byte[] recbuf = new byte[bufcount * 2 * 2];
        byte[] singbuf = null;

        try {
            music = new FileInputStream(musicFilename);
            rec = new FileInputStream(recFilename);
            sing = new FileOutputStream(singFilename);

            totalAudioLen = music.getChannel().size();
            totalDataLen = totalAudioLen + 36;
            WriteWaveFileHeader(sing, totalAudioLen, totalDataLen,
                    longSampleRate, channels, byteRate);

            int musicready = music.read(wavhead);
            int recready = rec.read(wavhead);
            musicready = music.read(musicbuf);
            if (aligncount > 0) {
                Log.v(SING_TAG, "forward count: " + aligncount);
                byte[] forwardskip = new byte[aligncount * 2 * 2];
                recready = rec.read(forwardskip);
                recready = rec.read(recbuf);
            } else {
                int afterwordcount = -aligncount;
                Log.v(SING_TAG, "afterword count: " + afterwordcount);
                byte[] alignbuf = new byte[(bufcount - afterwordcount) * 2 * 2];
                recready = rec.read(alignbuf);
                for (int i = 0; i < alignbuf.length; i++) {
                    recbuf[afterwordcount + i] = alignbuf[i];
                }
            }

            while (musicready != -1 && recready != -1) {
                singbuf = doOverlap(musicbuf, recbuf);
                Log.v(TAG, "doSingConvert " + musicready / 2 + " samples");
                sing.write(singbuf);
                musicready = music.read(musicbuf);
                recready = rec.read(recbuf);
            }
            music.close();
            rec.close();
            sing.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private byte[] doOverlap(byte[] musicbyte, byte[] recbyte) {
        short[] musicshort = new short[musicbyte.length / 2];
        ByteBuffer.wrap(musicbyte).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(musicshort);

        short[] recshort = new short[recbyte.length / 2];
        ByteBuffer.wrap(recbyte).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(recshort);

        int temp = 0;
        short[] singshort = new short[musicshort.length];
        for (int i = 0; i < musicshort.length; i++) {
            temp = (int) musicshort[i] + (int) recshort[i];

            if (temp > 32767)
                temp = 32767;
            else if (temp < -32768)
                temp = -32768;

            singshort[i] = (short) temp;
        }

        byte[] singbyte = new byte[singshort.length * 2];
        ByteBuffer.wrap(singbyte).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(singshort);

        return singbyte;
    }

    class CompondSingThread implements Runnable {

        @Override
        public void run() {
            sendMsg(mReportHandler, MSG_REPORT_SING_START, 0, 0, 0);

            String musicpath = mMusicPathAdapter.getItem(mSpinnerMusicSelector.getSelectedItemPosition());
            Log.v(TAG, "doSingConvert start: musicpath " + musicpath);
            mSingDelay = (DelayPlayStart - DelayRecordStart + LatencyPlay) - (mRecDelay - (RecBufSize * 1000) / SampleRateInHz);
            Log.v(SING_TAG, "doSingConvert start: mSingDelay " + mSingDelay);
            int aligncount = (mSingDelay * SampleRateInHz) / 1000;
            doSingConvert(musicpath, RecordWAV, SINGWAV, aligncount);
            Log.v(TAG, "doSingConvert finish");

            sendMsg(mReportHandler, MSG_REPORT_SING_FINISH, 0, 0, 0);
        }
    }

    // =================================== check permissions START ================================= /

    private void requestPermissionIfNeed() {
        requestPermission(this, () -> {
            Log.v(TAG, "requestPermissionIfNeed WRITE_EXTERNAL_STORAGE permission was granted");
            permissionsPassed();
        });
    }

    private void permissionsPassed() {
        File appRootDir = new File("/storage/emulated/0/Android/data/com.example.mediademo/");
        File cacheDir = getExternalCacheDir();
        Log.d(TAG, "prepareList_1 cacheDir getPath: " + cacheDir.getPath());
        Log.d(TAG, "prepareList_1 appRootDir getPath: " + appRootDir.getPath());

//        File work_path = getExternalFilesDir(Environment.DIRECTORY_MUSIC);
        File work_path = new File(WORKPATH);
        Log.v(TAG, "permissionsPassed called, isListGot: " + isListGot +
                ", work_path.exists: " + work_path.exists());
        if (!work_path.exists()) {
            Log.v(TAG, "create work path!");
            work_path.mkdirs();
        }

        if (!isListGot) {
            copyAllRawFilesToWorkPath();

            isListGot = true;
            // TODO FATAL NOTE: XiaoMi can not access to this directory...
//            prepareList(Environment.getExternalStorageDirectory(), 1);
            prepareList(appRootDir, 1);
            mMusicListAdapter.notifyDataSetChanged();
        }
    }

    private void copyAllRawFilesToWorkPath() {
        // 从assets根目录开始遍历
        traverseAssets("", "");

//        traverseRaws();
    }

    private void traverseRaws() {
//            InputStream is = getResources().openRawResource(R.raw.langyabang_44100_2ch_16bit_14s);
//            copyfile(is, WORKPATH + "langyabang_44100_2ch_16bit_14s.wav");

//        try {
//            Field[] fields = R.raw.class.getDeclaredFields();
//            for (Field field : fields) {
//                // rawName: langyabang_44100_2ch_16bit_14s
//                String rawName = field.getName();
//                int rawId = field.getInt(R.raw.class);
//
//                Log.d(TAG, "copyAllRawFilesToWorkPath rawName: " + rawName);
////                Log.d(TAG, "copyAllRawFilesToWorkPath rawId: " + rawId);
////                Log.d(TAG, "copyAllRawFilesToWorkPath R.raw.langyabang_44100_2ch_16bit_14s: " + R.raw.langyabang_44100_2ch_16bit_14s);
//                InputStream is = getResources().openRawResource(rawId);
//                copyfile(is, WORKPATH + rawName);
//            }
//            // rawName就是文件名称，如果想要id的话可以通过下面的代码拿到，希望被采纳~
//            // rawId = fields[i].getInt(R.raw.class);
//        } catch (Exception e) {
//            e.printStackTrace();
//            Log.e(TAG, "copyAllRawFilesToWorkPath ex err: " + e.getMessage());
//        }
    }

    /**
     * 遍历assets文件夹
     *
     * @param tab  格式化显示占位符
     * @param path 在assets中的路径
     */
    private void traverseAssets(String tab, String path) {
        AssetManager assetManager = getAssets();

        try {
            // 获取path目录下，全部的文件、文件夹
            String[] list = assetManager.list(path);

            if (list == null || list.length <= 0) {
                // 当前为文件时，或者当前目录下为空
                return;
            }

            for (String fileName : list) {
                Log.d(TAG, "copyAllRawFilesToWorkPath traverseAssets fileName: " + fileName);
                if (fileName.endsWith(".wav") || fileName.endsWith(".pcm")) {
                    InputStream is = getAssets().open(fileName);
                    copyfile(is, WORKPATH + fileName);
                }
            }
//
//            for (int i = 0; i < list.length; i++) {
//                System.out.println(tab + list[i]);
//
//                String subPath;
//                if ("".equals(path)) {
//                    // 如果当前是根目录
//                    subPath = list[i];
//                } else {
//                    // 如果当前不是根目录
//                    subPath = path + "/" + list[i];
//                }
//
//                traverseAssets(tab.concat("___"), subPath);
//            }
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TAG, "traverseAssets ex err: " + e.getMessage());
        }
    }

    private static final int REQUEST_CODE = 1024;

    private static final boolean skipDrawOverlays = true;

    private void requestPermission(Activity con, Runnable run) {
        Log.d(TAG, "requestPermission: called");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            Log.d(TAG, "requestPermission: Build.VERSION_CODES.R");
            // 先判断有没有权限
//            if (Environment.isExternalStorageManager()) {
//            if (ActivityCompat.checkSelfPermission(con, Manifest.permission.ACTION_MANAGE_OVERLAY_PERMISSIO ==
//                    PackageManager.PERMISSION_GRANTED) {
            if (commonROMPermissionCheck(con)) {
                Log.d(TAG, "requestPermission: has permission");
                // success
//                run.run();
                Log.e(TAG, "requestPermission: Build.VERSION_CODES.M");
                // 先判断有没有权限
                if (ActivityCompat.checkSelfPermission(con, Manifest.permission.READ_EXTERNAL_STORAGE) ==
                        PackageManager.PERMISSION_GRANTED &&
                        ContextCompat.checkSelfPermission(con, Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
                                PackageManager.PERMISSION_GRANTED) {
                    Log.e(TAG, "requestPermission: WRITE_READ_EXTERNAL_STORAGE has permissions");
                    // success
                    run.run();
                } else {
                    ActivityCompat.requestPermissions(con, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
                            Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
                }
            } else {
                Log.d(TAG, "requestPermission: has no permission");

                if (skipDrawOverlays) {
                    // permission was not granted, we should request the permission.
                    ActivityCompat.requestPermissions(this,
                            new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE,
                                    Manifest.permission.WRITE_EXTERNAL_STORAGE},
                            REQUEST_CODE);
                    return;
                }

//                Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
                Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
                intent.setData(Uri.parse("package:" + con.getPackageName()));
                con.startActivityForResult(intent, REQUEST_CODE);
            }
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            Log.e(TAG, "requestPermission: Build.VERSION_CODES.M");
            // 先判断有没有权限
            if (ActivityCompat.checkSelfPermission(con, Manifest.permission.READ_EXTERNAL_STORAGE) ==
                    PackageManager.PERMISSION_GRANTED &&
                    ContextCompat.checkSelfPermission(con, Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
                            PackageManager.PERMISSION_GRANTED) {
                // success
                run.run();
            } else {
                ActivityCompat.requestPermissions(con, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
            }
        } else {
            // success
            run.run();
        }
    }

    /**
     * 判断悬浮窗权限权限
     */
    private boolean commonROMPermissionCheck(Context context) {
        boolean result = true;
        if (skipDrawOverlays) {
            if (ContextCompat.checkSelfPermission(this,
                    Manifest.permission.RECORD_AUDIO)
                    == PackageManager.PERMISSION_GRANTED) {
                Log.v(TAG, "RECORD_AUDIO permission was granted");
                result = true;
            } else {
                Log.v(TAG, "RECORD_AUDIO permission was not granted, request!");
//                // permission was not granted, we should request the permission.
//                ActivityCompat.requestPermissions(this,
//                        new String[]{Manifest.permission.RECORD_AUDIO},
//                        MY_PERMISSIONS_REQUEST_RECORD_AUDIO);
                result = false;
            }
            return result;
        }
        if (Build.VERSION.SDK_INT >= 23) {
            try {
                Class clazz = Settings.class;
                Method canDrawOverlays =
                        clazz.getDeclaredMethod("canDrawOverlays", Context.class);
                result = (boolean) canDrawOverlays.invoke(null, context);
            } catch (Exception e) {
                Log.e(TAG, Log.getStackTraceString(e));
            }
        }
        Log.e(TAG, "commonROMPermissionCheck: canDrawOverlays=" + result);
        return result;
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
//            if (Environment.isExternalStorageManager()) {
            if (commonROMPermissionCheck(this) && ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) ==
                    PackageManager.PERMISSION_GRANTED &&
                    ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
                            PackageManager.PERMISSION_GRANTED) {
                Log.v(TAG, "onActivityResult READ_WRITE_EXTERNAL_STORAGE permission was granted");
                permissionsPassed();
            } else {
                Log.w(TAG, "onActivityResult READ_WRITE_EXTERNAL_STORAGE permission denied!");
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
//            case MY_PERMISSIONS_REQUEST_RECORD_AUDIO:
//                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//                    Log.v(TAG, "RECORD_AUDIO permission was granted");
//                } else {
//                    Log.w(TAG, "RECORD_AUDIO permission denied!");
//                }
//                break;

            case REQUEST_CODE:
                if (commonROMPermissionCheck(this) && ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) ==
                        PackageManager.PERMISSION_GRANTED &&
                        ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
                                PackageManager.PERMISSION_GRANTED) {
                    Log.v(TAG, "onRequestPermissionsResult READ_WRITE_EXTERNAL_STORAGE permission was granted");
                    permissionsPassed();
                } else {
                    Log.w(TAG, "onRequestPermissionsResult READ_WRITE_EXTERNAL_STORAGE permission denied!");
                }
                break;
        }
    }
    // =================================== check permissions END ================================= /
}
