安卓录音和播放 文件模式 字节流模式 声音播放

安卓界面刷新24帧/s 每一帧是16ms 主线程16ms的执行限制 主线耗时操作导致16ms执行不完 导致卡顿问题

文件模式开启录音耗时20-30ms 定制录音耗时30-50ms
字节流需要循环读写数据 必须再后台线程

主线程和后台线程状态同步

后台线程再循环中读状态值,主线程改变状态值让后台线程退出。
不需要synchronized 互斥访问
需要volatile保证主线程的修改后台线程可见

避免录音JNI函数闪退

JNI函数不能多线程调用
MediaRecorder : perpare() start() stop() reset() release()
AudioRecord : startRecording() read() stop() release()
以上都属于JNI函数

语音参数

文件模式

            //配置 MediaRecorder
            //从麦克风采集
            mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            //保存文件为mp4格式
            mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
            //采样频率
            mediaRecorder.setAudioSamplingRate(44100);
            //通用的AAC编码格式
            mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
            //音质比较好的编码频率
            mediaRecorder.setAudioEncodingBitRate(96000);
            //设置录音文件的位置
            mediaRecorder.setOutputFile(mAudioFile.getAbsolutePath());

setAudioSource
1.麦克风  MediaRecorder.AudioSource.MIC
2.语音识别 VOICE_RECOGNITION  
3.语音通话 VOICE_COMMUNICATION   如果系统支持 回音消除噪音抑制

setOutputFormat /setAudioEncoder
1.文件容器 MediaRecorder.OutputFormat.MPEG_4  文件头信息
2.声音编码 MediaRecorder.AudioEncoder.AAC 里面的数据编码格式

setAudioSamplingRate
1.说话声音是模拟信号 需要采样为数字信号(01)
2.采样频率越高(密集) 数据越大 音质越好
3.常用频率 8kHz 11.025kHz  22.05kHz  16kHz 37.8kHz  44.1kHz 48kHz 96kHz 192kHz 其中44.1安卓手机都支持

setAudioEncodingBitRate
1.声音编码,码率越大,压缩越小,音质越好
2.AAC HE(High Efficiency) : 32kbps~96kbps 码率越低 带宽越小 音质一般
3.AAC LC(Low Complexity): 96kbps~192kbps 平衡低码率 高音质

字节流模式

  /**
             * 配置AudioRecord
             */
            int audioSource = MediaRecorder.AudioSource.MIC;
            int sampleRate = 44100;
            //单声道输入
            int channelConfig = AudioFormat.CHANNEL_IN_MONO;
            //PCM 16 是所有安卓支持
            int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
            //获取缓冲区 计算Audiorecord 内部buffer最小大小
            int minBufferSize = AudioRecord.getMinBufferSize(sampleRate,channelConfig,audioFormat);

            //创建AudioRecord对象  buffer 不能小于最低要求 也不能小于我们每次读取的大小
            mAudioRecord = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat
                    , Math.max(minBufferSize, BUFFER_SIZE));


channelConfig
1.音频的采集和播放可以叠加
2.同时从多个音频源采集,分别输出到不同的扬声器
3.单声道(Mono)和双声道(Stereo)比较常见

audioFormat
1.量化精度:原始PCM数据,每个采样点的数据大小
2.4bit ,8bit,16bit,32bit... 位数越多,音质越好,数据越大
3.常用16bit 兼容所有安卓手机

声音播放

同样需要注意线程切换问题,状态值的原子性volatile。还有JNI函数调用对异常的处理。

文件模式

设置声音文件
mMediaPlayer.setDataSource(audioFile.getAbsolutePath())
配置音量 是否循环
mMediaPlayer.setVolume(1,1); 0~1 的范围
mMediaPlayer.setLooping(false); 支持循环播放
准备,开始
mMediaPlayer.prepare();
mMediaPlayer.start();

字节流模式

音乐类型 扬声器播放
int streamType = AudioManager.STREAM_MUSIC
录音时采用的采样频率,所以播放时使用同样的采样
int sampleRate = 44100
MONO表示单声道,录音输入单声道,播放用的时候保持一致
int channelConfig = Audioformat.CHANNEL_OUT_MONO
录音时使用16bit数据位宽 所以播放的时候使用同样的格式
int audioFormat = AudioFormat.ENCODING_PCM_16BIT
流模式
int mode =AudioTrack.MODE_STREAM

模式mode 就是java和native层数据传输模式
流模式:AudioTrack.MODE_STREAM
用流的形式一直从java层write写入native层
静态模式:AudioTrack.MODE_STATIC
在调用play之前一次性把数据写到native层

文件模式和字节流模式的主要代码
文件模式

package com.tencent.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.annotation.SuppressLint;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FileActivity extends AppCompatActivity {

    private static final String TAG = FileActivity.class.getSimpleName();
    private ExecutorService mExecutorService ;
    private MediaRecorder mediaRecorder;
    private File mAudioFile;
    private long mStartRecordTime , mStopRecordTime;
    private Handler mMainHandler ;
    private TextView tv_file;
    private Button btn_speech;
    private Button btn_play;
    //主线程和后台播放线程数据同步  使用volatile
    private volatile boolean mIsPlaying;

    private MediaPlayer mMediaPlayer;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_file);

        init();
    }

    @SuppressLint("ClickableViewAccessibility")
    private void init() {

        //主线程处理ui任务
        mMainHandler = new Handler(Looper.getMainLooper());


        //录音jni 函数 不是线程安全的 所以使用单线程处理任务
        mExecutorService = Executors.newSingleThreadExecutor();

        tv_file = findViewById(R.id.tv_file);

        btn_speech = findViewById(R.id.btn_speech);

        btn_play = findViewById(R.id.btn_play);

        tv_file.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        });

        btn_play.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //检查当前状态防止重复播放
                if (mAudioFile != null && !mIsPlaying) {
                    //设置当前播放状态
                    mIsPlaying = true;

                    //提交后台任务 开始播放
                    mExecutorService.submit(new Runnable() {
                        @Override
                        public void run() {
                            doPlay(mAudioFile);
                        }
                    });
                }

            }
        });

//        btn_speech.setOnClickListener(new View.OnClickListener() {
//            @Override
//            public void onClick(View v) {
//
//            }
//        });

        btn_speech.setOnTouchListener(new View.OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {

                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        startRecord();
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                        stopRecord();
                        break;
                    default:break;
                }

                return false;
            }
        });
    }



    /**
     * 停止执行录音逻辑
     */

    private void stopRecord() {
        //修改ui状态

        //提交后台任务
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                //执行停止录音逻辑 如果失败
                if (!doStop()){
                    recordFail();
                }

                //释放recorder
                releaseRecorder();
            }
        });
    }


    /**
     * 停止执行录音 执行失败返回false
     * @return
     */
    private boolean doStop() {


        try {
            //停止录音
            mediaRecorder.stop();

            //记录停止时间,统计时长
            mStopRecordTime = System.currentTimeMillis();


            //只接受指定时长的录音  这里是超过3秒
            int second = (int) ((mStopRecordTime - mStartRecordTime) / 1000);
            if (second > 3 ){
                //修改ui  在主线程执行
                runOnUiThread(new Runnable() {
                    @SuppressLint("SetTextI18n")
                    @Override
                    public void run() {
                        tv_file.setText(tv_file.getText() + "\n录音成功" + second +"秒");
                    }
                });

//                mMainHandler.post(new Runnable() {
//                    @Override
//                    public void run() {
//
//                    }
//                });

            }


        } catch (RuntimeException e) {
            e.printStackTrace();
            //捕获异常,避免闪退,返回false 提醒用户失败


            return false;
        }


        //停止成功
        return true;
    }

    /**
     * 开始执行录音逻辑
     *
     */
    private void startRecord() {
        //修改ui状态


        //提交后台任务
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                //释放之前录音的  recorder
                releaseRecorder();

                //执行录音逻辑  如果失败提示用户
                if (!doStart()){
                    recordFail();
                }
            }
        });
    }


    /**
     * 启动录音逻辑
     * 判断执行录音是否成功 执行失败返回false
     * @return
     */
    private boolean doStart() {

        try {
            //创建MediaRecorder
            mediaRecorder = new MediaRecorder();

            //创建录音文件
            mAudioFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/AudioDemo/" + System.currentTimeMillis() + ".m4a");
            mAudioFile.getParentFile().mkdirs();
            mAudioFile.createNewFile();
            Log.e(TAG, "doStart: mAudioFile ="+mAudioFile.getAbsolutePath());

            //配置 MediaRecorder
            //从麦克风采集
            mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            //保存文件为mp4格式
            mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
            //采样频率
            mediaRecorder.setAudioSamplingRate(44100);
            //通用的AAC编码格式
            mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
            //音质比较好的编码频率
            mediaRecorder.setAudioEncodingBitRate(96000);
            //设置录音文件的位置
            mediaRecorder.setOutputFile(mAudioFile.getAbsolutePath());


            //开始录音
            // prepare  start 都会抛出IllegalStateException 所以catch中添加runtimeException
            mediaRecorder.prepare();
            mediaRecorder.start();

            //记录开始录音的时间,用于统计时长

            mStartRecordTime = System.currentTimeMillis();


        } catch (IOException  | RuntimeException e) {
            e.printStackTrace();
            //捕获异常,避免闪退,返回false 提醒用户失败

            return false;
        }
        //启动成功
        return true;
    }

    /**
     * 录音错误处理
     * 执行失败 提示用户
     */
    private void recordFail() {
        mAudioFile = null;
        //todo 这里用handler 处理ui任务好 还是runOnUiThread 好???
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(FileActivity.this, "语音录制失败", Toast.LENGTH_SHORT).show();
            }
        });

//        mMainHandler.post(new Runnable() {
//            @Override
//            public void run() {
//                Toast.makeText(FileActivity.this, "语音录制失败", Toast.LENGTH_SHORT).show();
//
//            }
//        });

    }

    /**
     * 释放recorder
     */
    private void releaseRecorder() {
        //检查MediaRecorder 不为null
        if (mediaRecorder != null){
            mediaRecorder.release();
            mediaRecorder = null;
        }
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        //停止后台任务 防止内存泄漏
        mExecutorService.shutdownNow();

        releaseRecorder();

        stopPlay();
    }
    /**
     * 实际播放的逻辑
     * @param mAudioFile
     */

    private void doPlay(File mAudioFile) {
        //配置播放器 MediaPlayer
        mMediaPlayer = new MediaPlayer();
        try {

            //设置声音文件  告诉播放器播放什么
            mMediaPlayer.setDataSource(mAudioFile.getAbsolutePath());

            //设置监听回调  播放问题?

            //完成播放的监听配置
            mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mp) {
                    //播放结束 释放播放器
                    stopPlay();
                }
            });
            //播放出错的监听配置
            mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
                @Override
                public boolean onError(MediaPlayer mp, int what, int extra) {
                    //提示用户
                    playFail();
                    //释放播放器
                    stopPlay();
                    //错误已经处理返回true
                    return true;
                }
            });

            //配置音量 是否循环
            mMediaPlayer.setVolume(1,1);
            mMediaPlayer.setLooping(false); //不循环
            //准备  开始

            mMediaPlayer.prepare();
            mMediaPlayer.start();

        }catch (RuntimeException  | IOException e){
            //异常处理 防止闪退
            e.printStackTrace();
            //提示用户
            playFail();

            //释放播放器
            stopPlay();
        }



    }

    /**
     * 提醒用户播放失败
     */
    private void playFail() {
        //主线程 toast 提示
        mMainHandler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(FileActivity.this, "播放失败", Toast.LENGTH_SHORT).show();
            }
        });

    }

    /**
     * 停止播放的逻辑
     */
    private void stopPlay() {
        //重置播放状态
        mIsPlaying = false;
        //释放播放器
        if (mMediaPlayer != null){
            //重置监听器 防止内存泄漏
            mMediaPlayer.setOnCompletionListener( null);
            mMediaPlayer.setOnErrorListener(null);


            mMediaPlayer.stop();
            mMediaPlayer.reset();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }

    }
}

字节流模式

package com.tencent.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.annotation.SuppressLint;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class StreamActivity extends AppCompatActivity {

    private static final String TAG = StreamActivity.class.getSimpleName();
    private TextView tv_stream;
    private Button btn_stream;

    //录音状态  volatile 保证多线程内存同步
    private volatile boolean mIsRecording;
    //播放状态
    private volatile boolean mIsPlaying;
    private ExecutorService mExecutorService;

    private Handler mMainTheadHandler;
    //buffer 不能太大 避免oom  2k
    private static final int BUFFER_SIZE = 2048;
    //读取字节数据
    private byte[] mBuffer;
    private File mAudioFile;
    private long mStartRecordTime,mStopRecordTime;
    private FileOutputStream mFileOutputStream;
    private AudioRecord mAudioRecord;
    private Button btn_stream_speech;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_stream);

        init();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mExecutorService.shutdownNow();
    }

    private void init() {

        mBuffer = new byte[BUFFER_SIZE];

        //录音JNI函数 不具备线程安全性 所以用单线程
        mExecutorService = Executors.newSingleThreadExecutor();
        mMainTheadHandler = new Handler(Looper.getMainLooper());

        btn_stream = findViewById(R.id.btn_stream);

        tv_stream = findViewById(R.id.tv_stream);

        btn_stream_speech = findViewById(R.id.btn_stream_speech);

        btn_stream.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //根据当前状态,改变ui 执行开始或者定制录音
                if(mIsRecording){
                    //改变ui状态
                   btn_stream.setText("开始");
                   //改变录音状态
                    mIsRecording = false;

//                    //提交后台任务,执行停止逻辑
//                    mExecutorService.submit(new Runnable() {
//                        @Override
//                        public void run() {
//                            //执行开停止录音逻辑  失败提示用户
//                        }
//                    });
                }else{
                    //改变ui状态
                    btn_stream.setText("停止");
                    //改变录音状态
                    mIsRecording = true;
                    //提交后台任务,执行录音逻辑
                    mExecutorService.submit(new Runnable() {
                        @Override
                        public void run() {
                            //执行开始录音逻辑  失败提示用户
                            if (!startRecord()){
                                recordFail();
                            }

                        }
                    });
                }

            }
        });

        btn_stream_speech.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //检查播放状态 防止重复播放
                if (mAudioFile != null && !mIsPlaying){
                    //设置当前为播放状态
                    mIsPlaying = true;

                    //在后台线程提交播放任务
                    mExecutorService.submit(new Runnable() {
                        @Override
                        public void run() {
                            doPlay(mAudioFile);
                        }
                    });
                }
            }
        });

    }


    /**
     * 播放录音文件逻辑
     * @param mAudioFile
     */
    private void doPlay(File mAudioFile) {
        //配置播放器
        //音乐播放类型 扬声器播放
        int streamType = AudioManager.STREAM_MUSIC;
        //录音时采用的采样频率,所以播放的时候使用同样的采样频率
        int sampleRate = 44100;
        //录音用输入单声道  播放用输出单声道
        int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
        //录音的时候使用的16bit 所以播放的时候也要采用同样的格式
        int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
        //流模式
        int mode = AudioTrack.MODE_STREAM;
        //计算最小 buffer大小
        int minBufferSize =  AudioTrack.getMinBufferSize(sampleRate,channelConfig,audioFormat);

        //构造AudioTrack
        AudioTrack audioTrack = new AudioTrack(streamType,sampleRate,channelConfig,audioFormat,
        //不能小于AudioTrack的最低要求 也不能小于我们每次读的大小
        Math.max(minBufferSize,BUFFER_SIZE),mode);


        FileInputStream inputStream = null;
        try {
            //从文件流读数据
            inputStream =  new FileInputStream(mAudioFile);
            //循环读取数据 写到播放器去播放
            int read;
            while ((read = inputStream.read(mBuffer))> 0){
                int ret = audioTrack.write(mBuffer,0,read);
            //检查 write返回值 错误处理
                switch (ret){
                    case AudioTrack.ERROR_INVALID_OPERATION:
                    case AudioTrack.ERROR_BAD_VALUE:
                    case AudioManager.ERROR_DEAD_OBJECT:
                        playFail();
                        return;
                    default:
                        break;

                }

            }

        }catch (RuntimeException | IOException e){
            e.printStackTrace();

        }finally {
            //重置播放状态
            mIsPlaying = false;

            //关闭文件输入流

            if (inputStream != null){
                coloseQuietly(inputStream);
            }

            //播放器释放
            resetQuietly(audioTrack);
        }


        //循环读数据写到播放器去播放

        //错误处理防止闪退
    }


    //错误处理
    private void playFail() {
        mAudioFile = null;

        //toast 提示用户
        mMainTheadHandler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(StreamActivity.this, "播放失败", Toast.LENGTH_SHORT).show();
            }
        });

    }

    private void resetQuietly(AudioTrack audioTrack) {
       try {
           audioTrack.stop();
           audioTrack.release();
       }catch (RuntimeException e){
           e.printStackTrace();
       }
    }

    private void coloseQuietly(FileInputStream inputStream) {
        try {
            inputStream.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    /**
     * 录音错误的处理
     */
    private void recordFail() {
        mMainTheadHandler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(StreamActivity.this,"录音失败",Toast.LENGTH_SHORT).show();
                //重置录音状态,修改ui
                mIsRecording = false;
                btn_stream.setText("开始");
            }
        });
    }

    /**
     * 启动录音逻辑
     * @return
     */
    private boolean startRecord() {


        try {
            //创建录音文件
            mAudioFile = new File(getExternalCacheDir().getPath() +"/AudioDemo/" +
                    System.currentTimeMillis() +".pcm");

            mAudioFile.getParentFile().mkdirs();
            mAudioFile.createNewFile();
//            if (!mAudioFile.exists()){
//                boolean mkdirs = mAudioFile.mkdirs();
//                Log.e(TAG, "startRecord: mkdirs="+mkdirs );
//                if (!mkdirs){
//                    try {
//                        throw new IOException("file is not create");
//                    } catch(IOException e){
//                        e.printStackTrace();
//                    }
//
//                }
//            }

            //创建文件输出流

            mFileOutputStream = new FileOutputStream(mAudioFile);

            /**
             * 配置AudioRecord
             */
            int audioSource = MediaRecorder.AudioSource.MIC;
            int sampleRate = 44100;
            //单声道输入
            int channelConfig = AudioFormat.CHANNEL_IN_MONO;
            //PCM 16 是所有安卓支持
            int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
            //获取缓冲区 计算Audiorecord 内部buffer最小大小
            int minBufferSize = AudioRecord.getMinBufferSize(sampleRate,channelConfig,audioFormat);

            //创建AudioRecord对象  buffer 不能小于最低要求 也不能小于我们每次读取的大小
            mAudioRecord = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat
                    , Math.max(minBufferSize, BUFFER_SIZE));

            //开始录音
            mAudioRecord.startRecording();
            //记录开始录音时间 统计时长

            mStartRecordTime = System.currentTimeMillis();

            //循环读取数据,写到输出流中

            while (mIsRecording){
                //只要还在录音状态,就一直读取数据
                int read = mAudioRecord.read(mBuffer,0,BUFFER_SIZE);
                if (read > 0){
                    //读取成功 写到文件中
                    mFileOutputStream.write(mBuffer,0,read);

                }else{
                    //读取失败 返回false 提示用户

                    return false;
                }


            }


            //退出循环 通过mIsRecording判断  停止录音 释放资源

            return stopRecord();

        } catch (IOException | RuntimeException e) {
            e.printStackTrace();
            //捕获异常 避免闪退 返回false 提示用户
            Log.e(TAG, "startRecord: exception = "+e.toString());
            return false;
        } finally {
            //释放 AudioRecord资源
            if (mAudioRecord != null){
                mAudioRecord.release();
            }
        }



//        return true;
    }


    /**
     * 结束录音逻辑
     * @return
     */
    private boolean stopRecord() {


        try {

            //停止录音 关闭文件输出流
            mAudioRecord.stop();
            mAudioRecord.release();
            mAudioRecord = null;
            mFileOutputStream.close();
            //记录结束时间,统计录音时长
            mStopRecordTime = System.currentTimeMillis();
            //大于3秒成功  改变ui

            int second = (int)((mStopRecordTime - mStartRecordTime) /1000);
            if (second > 3){
                mMainTheadHandler.post(new Runnable() {
                    @SuppressLint("SetTextI18n")
                    @Override
                    public void run() {
                        tv_stream.setText(tv_stream.getText() + "\n录音成功" + second + " 秒 ");
                    }
                });
            }else {
                mMainTheadHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(StreamActivity.this,"当前录制时间不足3秒",Toast.LENGTH_SHORT).show();
                    }
                });
            }

        } catch (IOException e) {
            e.printStackTrace();
            //捕获异常 避免闪退 返回false 提示用户

            return false;
        }
        return true;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章