Pārlūkot izejas kodu

增加调音器音叉功能

Pq 2 gadi atpakaļ
vecāks
revīzija
52e2fbe767

+ 25 - 0
musictuner/src/main/java/com/cooleshow/musictuner/MusicTunerActivity.java

@@ -12,6 +12,7 @@ import be.tarsos.dsp.pitch.PitchProcessor;
 
 import android.Manifest;
 import android.content.Context;
+import android.content.DialogInterface;
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.LayoutInflater;
@@ -21,15 +22,18 @@ import com.cooleshow.base.ui.activity.BaseActivity;
 import com.cooleshow.base.utils.SizeUtils;
 import com.cooleshow.musictuner.bean.VoiceToneBean;
 import com.cooleshow.musictuner.databinding.ActivityMusicTunerLayoutBinding;
+import com.cooleshow.musictuner.utils.AudioTrackManager;
 import com.cooleshow.musictuner.utils.MusicTunerHelper;
 import com.cooleshow.musictuner.utils.VoiceDataUtils;
 import com.cooleshow.musictuner.widget.MusicTunerSettingDialog;
+import com.cooleshow.musictuner.widget.MusicTuningForkDialog;
 import com.tbruyelle.rxpermissions3.RxPermissions;
 
 public class MusicTunerActivity extends BaseActivity<ActivityMusicTunerLayoutBinding> implements View.OnClickListener {
 
     private MusicTunerHelper mMusicTunerHelper;
     private MusicTunerSettingDialog mTunerSettingDialog;
+    private MusicTuningForkDialog mTuningForkDialog;
 
     public static void start(Context context) {
         Intent intent = new Intent(context, MusicTunerActivity.class);
@@ -53,6 +57,7 @@ public class MusicTunerActivity extends BaseActivity<ActivityMusicTunerLayoutBin
         initMidTitleToolBar(viewBinding.toolbarInclude.toolbar, "调音器");
         viewBinding.ivHzAdd.setOnClickListener(this);
         viewBinding.ivHzReduce.setOnClickListener(this);
+        viewBinding.ivLeftBg.setOnClickListener(this);
         viewBinding.toolbarInclude.tvRightText.setOnClickListener(this);
         viewBinding.toolbarInclude.tvRightText.setText("设置");
         viewBinding.toolbarInclude.tvRightText.setCompoundDrawablePadding(SizeUtils.dp2px(5));
@@ -150,6 +155,26 @@ public class MusicTunerActivity extends BaseActivity<ActivityMusicTunerLayoutBin
             showSettingDialog();
             return;
         }
+
+        if (id == R.id.iv_left_bg) {
+            showTuningForkDialog();
+            return;
+        }
+    }
+
+    private void showTuningForkDialog() {
+        if (mTuningForkDialog == null) {
+            mTuningForkDialog = new MusicTuningForkDialog(this);
+            mTuningForkDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+                @Override
+                public void onDismiss(DialogInterface dialog) {
+                    AudioTrackManager.getInstance().stop();
+                }
+            });
+        }
+        if (!mTuningForkDialog.isShowing()) {
+            mTuningForkDialog.show();
+        }
     }
 
     private void updateCurrentHzStandardText() {

+ 42 - 0
musictuner/src/main/java/com/cooleshow/musictuner/adapter/MusicTuningForkAdapter.java

@@ -0,0 +1,42 @@
+package com.cooleshow.musictuner.adapter;
+
+import android.view.View;
+import android.widget.ImageView;
+
+import com.chad.library.adapter.base.BaseQuickAdapter;
+import com.chad.library.adapter.base.viewholder.BaseViewHolder;
+import com.cooleshow.musictuner.R;
+import com.cooleshow.musictuner.bean.MusicTuningForkBean;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Author by pq, Date on 2022/10/26.
+ */
+public class MusicTuningForkAdapter extends BaseQuickAdapter<MusicTuningForkBean, BaseViewHolder> {
+    private int currentSelectPos = -1;
+
+    public MusicTuningForkAdapter() {
+        super(R.layout.item_music_tuning_fork_layout);
+    }
+
+    @Override
+    protected void convert(@NonNull BaseViewHolder holder, MusicTuningForkBean item) {
+        holder.setText(R.id.tv_options, item.getTitle());
+        ImageView iv_options = holder.getView(R.id.iv_options);
+        if (currentSelectPos == holder.getLayoutPosition()) {
+            iv_options.setImageResource(R.drawable.icon_music_menu_select);
+        } else {
+            iv_options.setImageResource(R.drawable.icon_music_menu_normal);
+        }
+    }
+
+    public int getCurrentSelectPos() {
+        return currentSelectPos;
+    }
+
+    public void setCurrentSelectPos(int currentSelectPos) {
+        this.currentSelectPos = currentSelectPos;
+        notifyDataSetChanged();
+    }
+}

+ 27 - 0
musictuner/src/main/java/com/cooleshow/musictuner/bean/MusicTuningForkBean.java

@@ -0,0 +1,27 @@
+package com.cooleshow.musictuner.bean;
+
+/**
+ * Author by pq, Date on 2022/10/26.
+ */
+public class MusicTuningForkBean {
+    private String title;
+    private boolean isMatchParent;
+
+    public String getTitle() {
+        return title;
+    }
+
+    public boolean isMatchParent() {
+        return isMatchParent;
+    }
+
+    public void setMatchParent(boolean matchParent) {
+        isMatchParent = matchParent;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+
+}

+ 1 - 1
musictuner/src/main/java/com/cooleshow/musictuner/constants/MusicTunerConstants.java

@@ -9,5 +9,5 @@ public class MusicTunerConstants {
             "单簧管:降B大调", "双簧管:C大调", "竖笛:C调大调",
             "小号:降B大调", "长号:C大调", "圆号:F大调",
             "大号:降B大调", "上低音号:C大调", "上低音号:降B大调"};
-    public final static int[] TRANSPOSING_VALUES = new int[]{0, 0, -2, -9, -2, 0, 12, -2, 0, 5, -2, 0, -2};
+    public final static int[] TRANSPOSING_VALUES = new int[]{0, 0, -2, -9, -2, 0, -12, -2, 0, 5, -2, 0, -2};
 }

+ 119 - 0
musictuner/src/main/java/com/cooleshow/musictuner/utils/AudioTrackManager.java

@@ -0,0 +1,119 @@
+package com.cooleshow.musictuner.utils;
+
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.AudioTrack;
+import android.util.Log;
+
+/**
+ * Author by pq, Date on 2022/10/26.
+ */
+public class AudioTrackManager {
+    private Thread mRecordThread;
+    AudioTrack mAudioTrack;
+
+    /**
+     * 正弦波
+     **/
+    byte[] wave;
+    /**
+     * 总长度
+     **/
+    int length;
+    /**
+     * 是否循环播放
+     */
+    private boolean ISPLAYSOUND = true;
+    /**
+     * 正弦波的高度
+     **/
+    private static final int HEIGHT = 127;
+    /**
+     * 2PI
+     **/
+    private static final double TWOPI = 2 * Math.PI;
+    private volatile static AudioTrackManager mInstance;
+
+    Runnable recordRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (mAudioTrack != null) {
+                mAudioTrack.play();
+                while (ISPLAYSOUND) {
+                    mAudioTrack.write(wave, 0, length);
+                }
+            }
+        }
+    };
+
+    public static AudioTrackManager getInstance() {
+        if (mInstance == null) {
+            synchronized (AudioTrackManager.class) {
+                if (mInstance == null) {
+                    mInstance = new AudioTrackManager();
+                }
+            }
+        }
+        return mInstance;
+    }
+
+    public static final int RATE = 44100;
+
+    /**
+     * Play beep.
+     *
+     * @param
+     * @param frequency_hz the frequency hz
+     */
+    public void play(float frequency_hz) {
+        Log.i("AudioTrackManager", "play:" + frequency_hz);
+        stop();
+        if (frequency_hz > 0) {
+            float waveLen = RATE / frequency_hz;
+            length = (int) (waveLen * frequency_hz);
+            wave = new byte[RATE];
+            mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, RATE,
+                    AudioFormat.CHANNEL_OUT_MONO, // CHANNEL_CONFIGURATION_MONO,
+                    AudioFormat.ENCODING_PCM_8BIT, length, AudioTrack.MODE_STREAM);
+            wave = sin(wave, waveLen, length);
+        } else {
+            return;
+        }
+        ISPLAYSOUND = true;
+        mRecordThread = new Thread(recordRunnable);
+        mRecordThread.start();
+    }
+
+    public void stop() {
+        ISPLAYSOUND = false;
+        destroyThread();
+    }
+
+    private static byte[] sin(byte[] wave, float waveLen, int length) {
+        for (int i = 0; i < length; i++) {
+            wave[i] = (byte) (HEIGHT * (1 - Math.sin(TWOPI * ((i % waveLen) * 1.00 / waveLen))));
+        }
+        return wave;
+    }
+
+    private void destroyThread() {
+        try {
+            if (mAudioTrack != null) {
+                if (mAudioTrack.getState() == AudioRecord.STATE_INITIALIZED) {
+                    mAudioTrack.pause();
+                    mAudioTrack.flush();
+                }
+                mAudioTrack.release();
+            }
+            mRecordThread.interrupt();
+            mAudioTrack = null;
+            mRecordThread = null;
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            mRecordThread = null;
+            mAudioTrack = null;
+        }
+    }
+}

+ 2 - 2
musictuner/src/main/java/com/cooleshow/musictuner/utils/MusicTunerHelper.java

@@ -23,7 +23,7 @@ public class MusicTunerHelper {
 
     public void start() {
         if (mDispatcher == null) {
-            mDispatcher = AudioDispatcherFactory.fromDefaultMicrophone(22050, 1024, 0);
+            mDispatcher = AudioDispatcherFactory.fromDefaultMicrophone(22050, 2048, 0);
         }
         PitchDetectionHandler pdh = new PitchDetectionHandler() {
             @Override
@@ -35,7 +35,7 @@ public class MusicTunerHelper {
                 }
             }
         };
-        AudioProcessor p = new PitchProcessor(PitchProcessor.PitchEstimationAlgorithm.FFT_YIN, 22050, 1024, pdh);
+        AudioProcessor p = new PitchProcessor(PitchProcessor.PitchEstimationAlgorithm.FFT_YIN, 22050, 2048, pdh);
         mDispatcher.addAudioProcessor(p);
         mThread = new Thread(mDispatcher, "Audio Dispatcher");
         mThread.start();

+ 34 - 3
musictuner/src/main/java/com/cooleshow/musictuner/utils/VoiceDataUtils.java

@@ -25,6 +25,9 @@ public class VoiceDataUtils {
     public static final int MIN_MUSIC_HZ_STANDARD_415_HZ = 415;//最小415Hz
     public static final int MAX_MUSIC_HZ_STANDARD_445_HZ = 445;//最大445Hz
     public static final int DEFAULT_MUSIC_HZ_STANDARD_440_HZ = 440;//默认440
+    public static final int MAX_MUSIC_POS = 8;//最大C8 D8这种
+    public static final int MIN_MUSIC_POS = 0;//最小C0 D0这种
+    public static final int DEFAULT_MUSIC_POS = 5;//默认
     public int currentMusicHzStandard = DEFAULT_MUSIC_HZ_STANDARD_440_HZ;
 
     private VoiceDataUtils() {
@@ -65,12 +68,12 @@ public class VoiceDataUtils {
             return null;
         }
         targetVoiceFrequencyValue = countWithMusicHzStandard(targetVoiceFrequencyValue);
-        if (targetVoiceFrequencyValue > ALL_VOICE_SAMPLES[ALL_VOICE_SAMPLES.length - 1]) {
+        if (targetVoiceFrequencyValue >= ALL_VOICE_SAMPLES[ALL_VOICE_SAMPLES.length - 1]) {
             VoiceToneBean voiceToneBean = buildBean(ALL_VOICE_SAMPLES.length - 2, ALL_VOICE_SAMPLES.length - 3, ALL_VOICE_SAMPLES.length - 1);
             voiceToneBean.difference = String.valueOf(targetVoiceFrequencyValue - ALL_VOICE_SAMPLES[ALL_VOICE_SAMPLES.length - 2]);
             return voiceToneBean;
         }
-        if (targetVoiceFrequencyValue < ALL_VOICE_SAMPLES[0]) {
+        if (targetVoiceFrequencyValue <= ALL_VOICE_SAMPLES[0]) {
             VoiceToneBean voiceToneBean = buildBean(1, 0, 2);
             voiceToneBean.difference = String.valueOf(targetVoiceFrequencyValue - ALL_VOICE_SAMPLES[1]);
             return voiceToneBean;
@@ -90,11 +93,25 @@ public class VoiceDataUtils {
         } else {
             resultPosition = end;
         }
-        VoiceToneBean voiceToneBean = buildBean(resultPosition, resultPosition - 1, resultPosition + 1);
+        VoiceToneBean voiceToneBean = buildBean(resultPosition, getBeforePosition(resultPosition - 1), getAfterPosition(resultPosition + 1));
         voiceToneBean.difference = String.valueOf(targetVoiceFrequencyValue - ALL_VOICE_SAMPLES[resultPosition]);
         return voiceToneBean;
     }
 
+    private int getBeforePosition(int pos) {
+        if (pos < 0) {
+            pos = 0;
+        }
+        return pos;
+    }
+
+    private int getAfterPosition(int pos) {
+        if (pos > ALL_VOICE_SAMPLES.length - 1) {
+            pos = ALL_VOICE_SAMPLES.length - 1;
+        }
+        return pos;
+    }
+
     private int currentTransposingValue = 0;
 
     private VoiceToneBean buildBean(int pos, int beforePos, int afterPos) {
@@ -185,9 +202,23 @@ public class VoiceDataUtils {
 
     /**
      * 设置移调值
+     *
      * @param value
      */
     public void setTransposingValue(int value) {
         this.currentTransposingValue = value;
     }
+
+    /**
+     * 参数1为VOICE_OF_TONE中的位置(比如对应0对应C,1对应C♯)
+     * 参数2为具体的音阶C1 C2 C3后面的1 2 3这种MIN_MUSIC_POS-MAX_MUSIC_POS之间
+     *
+     * @param posFromVoiceOfTone
+     * @param phase
+     * @return
+     */
+    public double getTargetFromSamples(int posFromVoiceOfTone, int phase) {
+        int pos = VOICE_OF_TONE.length * phase + posFromVoiceOfTone;
+        return countWithMusicHzStandard((float) ALL_VOICE_SAMPLES[pos]);
+    }
 }

+ 134 - 0
musictuner/src/main/java/com/cooleshow/musictuner/widget/MusicTuningForkDialog.java

@@ -0,0 +1,134 @@
+package com.cooleshow.musictuner.widget;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+import com.chad.library.adapter.base.BaseQuickAdapter;
+import com.chad.library.adapter.base.listener.OnItemClickListener;
+import com.cooleshow.musictuner.R;
+import com.cooleshow.musictuner.adapter.MusicTuningForkAdapter;
+import com.cooleshow.musictuner.bean.MusicTuningForkBean;
+import com.cooleshow.musictuner.utils.AudioTrackManager;
+import com.cooleshow.musictuner.utils.VoiceDataUtils;
+
+import java.util.ArrayList;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Author by pq, Date on 2022/10/26.
+ */
+public class MusicTuningForkDialog extends Dialog implements View.OnClickListener {
+
+    private RecyclerView mRecyclerView;
+    private ArrayList<MusicTuningForkBean> mList;
+    private MusicTuningForkAdapter mTuningForkAdapter;
+    private TextView mTvCurrentMusicPosition;
+    private int currentMusicPos = VoiceDataUtils.DEFAULT_MUSIC_POS;
+
+    public MusicTuningForkDialog(@NonNull Context context) {
+        super(context, com.cooleshow.base.R.style.DialogStyle);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.dialog_tuning_fork_layout);
+        buildList();
+        mRecyclerView = findViewById(R.id.recyclerView);
+        findViewById(R.id.view_add).setOnClickListener(this);
+        findViewById(R.id.view_reduce).setOnClickListener(this);
+        findViewById(R.id.tv_confirm).setOnClickListener(this);
+        mTvCurrentMusicPosition = findViewById(R.id.tv_current_music_position);
+        mTuningForkAdapter = new MusicTuningForkAdapter();
+        updateText();
+        GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), 2);
+        gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
+            @Override
+            public int getSpanSize(int position) {
+                MusicTuningForkBean musicTuningForkBean = mList.get(position);
+                return musicTuningForkBean.isMatchParent() ? 2 : 1;
+            }
+        });
+        mRecyclerView.setLayoutManager(gridLayoutManager);
+        mRecyclerView.setAdapter(mTuningForkAdapter);
+        mTuningForkAdapter.setList(mList);
+        initListener();
+    }
+
+    private void initListener() {
+        mTuningForkAdapter.setOnItemClickListener(new OnItemClickListener() {
+            @Override
+            public void onItemClick(@NonNull BaseQuickAdapter<?, ?> adapter, @NonNull View view, int position) {
+                mTuningForkAdapter.setCurrentSelectPos(position);
+                play();
+            }
+        });
+    }
+
+    private void buildList() {
+        mList = new ArrayList<>();
+        for (int i = 0; i < VoiceDataUtils.VOICE_OF_TONE.length; i++) {
+            MusicTuningForkBean musicTuningForkBean = new MusicTuningForkBean();
+            String s = VoiceDataUtils.VOICE_OF_TONE[i];
+            musicTuningForkBean.setTitle(s);
+            if (TextUtils.equals(s, "E") || TextUtils.equals(s, "B")) {
+                musicTuningForkBean.setMatchParent(true);
+            } else {
+                musicTuningForkBean.setMatchParent(false);
+            }
+            mList.add(musicTuningForkBean);
+        }
+    }
+
+    private void play() {
+        if (mTuningForkAdapter != null) {
+            int currentSelectPos = mTuningForkAdapter.getCurrentSelectPos();
+            if (currentSelectPos != -1) {
+                if (currentSelectPos < VoiceDataUtils.VOICE_OF_TONE.length) {
+                    double targetFromSamples = VoiceDataUtils.getInstance().getTargetFromSamples(currentSelectPos, currentMusicPos);
+                    AudioTrackManager.getInstance().play((int) targetFromSamples);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onClick(View v) {
+        int id = v.getId();
+        if (id == R.id.view_add) {
+            if (currentMusicPos < VoiceDataUtils.MAX_MUSIC_POS) {
+                currentMusicPos += 1;
+                play();
+                updateText();
+            }
+            return;
+        }
+        if (id == R.id.view_reduce) {
+            if (currentMusicPos > VoiceDataUtils.MIN_MUSIC_POS) {
+                currentMusicPos -= 1;
+                play();
+                updateText();
+            }
+            return;
+        }
+        if (id == R.id.tv_confirm) {
+            dismiss();
+            return;
+        }
+    }
+
+    private void updateText() {
+        if (mTvCurrentMusicPosition != null) {
+            mTvCurrentMusicPosition.setText(String.valueOf(currentMusicPos));
+        }
+    }
+}

BIN
musictuner/src/main/res/drawable-xhdpi/icon_music_hz_tip2.png


BIN
musictuner/src/main/res/drawable-xhdpi/icon_music_menu_normal.png


BIN
musictuner/src/main/res/drawable-xhdpi/icon_music_menu_select.png


BIN
musictuner/src/main/res/drawable-xxhdpi/icon_music_hz_tip2.png


BIN
musictuner/src/main/res/drawable-xxhdpi/icon_music_menu_normal.png


BIN
musictuner/src/main/res/drawable-xxhdpi/icon_music_menu_select.png


+ 115 - 0
musictuner/src/main/res/layout/dialog_tuning_fork_layout.xml

@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="271dp"
+    android:layout_height="wrap_content"
+    android:background="@drawable/bg_white_14dp"
+    android:paddingStart="18dp"
+    android:paddingEnd="18dp"
+    android:paddingBottom="19dp">
+
+    <TextView
+        android:id="@+id/tv_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:includeFontPadding="false"
+        android:paddingTop="14dp"
+        android:text="设置"
+        android:textColor="@color/color_333333"
+        android:textSize="@dimen/sp_17"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:id="@+id/tv_hz_tip"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:includeFontPadding="false"
+        android:text="A4=440Hz"
+        android:textColor="@color/color_333333"
+        android:textSize="@dimen/sp_15"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/tv_title" />
+
+    <ImageView
+        android:id="@+id/iv_hz_tip"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="5dp"
+        android:src="@drawable/icon_music_hz_tip2"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="@+id/tv_hz_tip"
+        app:layout_constraintLeft_toRightOf="@+id/tv_hz_tip"
+        app:layout_constraintTop_toTopOf="@+id/tv_hz_tip" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/recyclerView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:minHeight="256dp"
+        android:overScrollMode="never"
+        android:scrollbars="none"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/tv_hz_tip" />
+
+    <TextView
+        android:id="@+id/view_add"
+        android:layout_width="63dp"
+        android:layout_height="45dp"
+        android:layout_marginTop="13dp"
+        android:background="@drawable/shape_f1f1f1_6dp"
+        android:gravity="center"
+        android:text="+"
+        android:textColor="@color/color_2dc7aa"
+        android:textSize="@dimen/sp_25"
+        android:textStyle="bold"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/recyclerView" />
+
+
+    <TextView
+        android:id="@+id/view_reduce"
+        android:layout_width="63dp"
+        android:layout_height="45dp"
+        android:background="@drawable/shape_f1f1f1_6dp"
+        android:gravity="center"
+        android:text="-"
+        android:textColor="@color/color_2dc7aa"
+        android:textSize="@dimen/sp_25"
+        android:textStyle="bold"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/view_add" />
+
+    <TextView
+        android:id="@+id/tv_current_music_position"
+        android:layout_width="0dp"
+        android:layout_height="45dp"
+        android:layout_marginStart="9dp"
+        android:layout_marginEnd="9dp"
+        android:background="@drawable/shape_979797_border_6dp"
+        android:gravity="center"
+        android:text="6"
+        android:textColor="@color/color_333333"
+        android:textSize="@dimen/sp_14"
+        app:layout_constraintLeft_toRightOf="@+id/view_add"
+        app:layout_constraintRight_toLeftOf="@+id/view_reduce"
+        app:layout_constraintTop_toTopOf="@+id/view_add" />
+
+    <TextView
+        android:id="@+id/tv_confirm"
+        android:layout_width="0dp"
+        android:layout_height="40dp"
+        android:layout_marginTop="14dp"
+        android:background="@drawable/shape_2dc7aa_6dp"
+        android:gravity="center"
+        android:text="确认"
+        android:textColor="@color/white"
+        android:textSize="@dimen/sp_16"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/view_add" />
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 31 - 0
musictuner/src/main/res/layout/item_music_tuning_fork_layout.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingTop="9dp"
+    android:paddingBottom="9dp">
+
+    <ImageView
+        android:id="@+id/iv_options"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:src="@drawable/icon_music_menu_normal"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:gravity="center"
+        android:includeFontPadding="false"
+        android:id="@+id/tv_options"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingStart="10dp"
+        android:textColor="@color/color_333333"
+        android:textSize="@dimen/sp_14"
+        app:layout_constraintBottom_toBottomOf="@+id/iv_options"
+        app:layout_constraintLeft_toRightOf="@+id/iv_options"
+        app:layout_constraintTop_toTopOf="@+id/iv_options"
+        tools:text="C" />
+</androidx.constraintlayout.widget.ConstraintLayout>