2020年6月13日 星期六

[ffmpeg] How to check a mp4 audio/video duration without re-encode

Condition:
Reecently implement a audio/video muxer with libffmpeg with 15 h264 fps video track and 8000khz aac audio track. THese two bitrate can not be devided by each each other. I find two solution:
1.find track info of each frame
2.concatenate clips with continuous recording, then watch the output to check
  • audio/video frame loss or not?
  • have the same duration ?
both we need: 
Step 0. download or build ffmpeg 
for Windows
please follow [[https://www.wikihow.com/Install-FFmpeg-on-Windows]]

Step 1.copy target clips to ffmpeg folder

For concatenate clips:
Step 2.
$ cd <ffmpeg path>
$ (for %i in (*.mp4) do @echo file '%i') > mylist.txt
$ ffmpeg -f concat -safe 0 -i mylist.txt -c copy output.mp4
For analyse frame informantion with ffprobe
for video info:
$ ffprobe -show_frames -select_streams v -of xml <mp4 clip name>.mp4 > video_<mp4 clip name>.info
for audio info:
$ ffprobe -show_frames -select_streams a -of xml <mp4 clip name>.mp4 > audio_<mp4 clip name>.info

2020年2月21日 星期五

Git: work flow of how a feature added to a branch

A workflow of how to add a feature in remote repository ?

full graph




STEP 0: PULL REMOTE TO LOCAL


STEP 1: NEW FEATURE BRANCH FROM LOCAL


STEP 2: EDIT YOUR FEATURE BRANCH (ADD, COMMIT)


STEP 3: LOCAL PULL REMOTE


STEP 4: FEATURE MERGE TO LOCAL, ALSO FIX CONFLICT


STEP 5: LOCAL SQUASH MERGE TO ONE COMMIT


STEP 6: PUSH COMMIT TO REMOTE



NOTE:
1. Difference between  fetch and pull

2. how to check commit history
git log --oneline

3. reset commit(s) from HEAD
git reset HEAD~<numberOfCommit>

4. change last git commit message
git commit --amend -m "reset message
>second line
>third line
"

reference:
https://backlog.com/git-tutorial/tw/stepup/stepup3_1.html
https://gitbook.tw/chapters/using-git/amend-commit1.html







2020年1月13日 星期一

【python】UI/ function code seperated(ISP), multithread TCP server receive multi client and send to specific one client

This example mention on:
1. ISP:pyqt5 Ui_form class and function code in different class 
2. Use ThreadingTcpServer class not QTcpSocket class
(if you don't need Graphic interface, this example can actually remove QT libraries)
3. Send by independent thread, received in ThreadingTcpServer handle
4. cross-threading communication use Queue


Graphic user interface




























1.explain each thread
main thread of this application
class TcpServer_Tool(QWidget, Ui_Form):

tcpserver thread

class client_handler(BaseRequestHandler):

thread to watch how many client connected?

class check_socket_sum(threading.Thread):

thread to send message to one client by ip
class client_sendBack(threading.Thread): 

2.explain each queue

client_msg_queue = queue.Queue()
client_add_addr_queue = queue.Queue()
client_remove_addr_queue =  queue.Queue()
client_recv_queue =queue.Queue()
client_send_msg_queue = queue.Queue()

client_socket_list = []


  • client_msg_queue :for client connect/ disconnect action signal
  • client_add_addr_queue/ client_remove_addr :for user interface to add client list
  • client_recv_queue:content received from client 
  • client_send_msg_queue: user send to specific clients from server

4.source code
Graphic User interface: tcpserver_tool.py 
(translated from PyQt5 .ui file format)


# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'tcpServer_tool.ui'
#
# Created by: PyQt5 UI code generator 5.14.1
#
# WARNING! All changes made in this file will be lost!


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(1078, 728)
        font = QtGui.QFont()
        font.setFamily("Century Gothic")
        font.setPointSize(11)
        Form.setFont(font)
        self.gridLayout = QtWidgets.QGridLayout(Form)
        self.gridLayout.setObjectName("gridLayout")
        self.groupBox = QtWidgets.QGroupBox(Form)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(2)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.groupBox.sizePolicy().hasHeightForWidth())
        self.groupBox.setSizePolicy(sizePolicy)
        self.groupBox.setObjectName("groupBox")
        self.gridLayout_2 = QtWidgets.QGridLayout(self.groupBox)
        self.gridLayout_2.setObjectName("gridLayout_2")
        self.label = QtWidgets.QLabel(self.groupBox)
        self.label.setObjectName("label")
        self.gridLayout_2.addWidget(self.label, 0, 0, 1, 1)
        self.s1_line_1 = QtWidgets.QLineEdit(self.groupBox)
        self.s1_line_1.setObjectName("s1_line_1")
        self.gridLayout_2.addWidget(self.s1_line_1, 0, 1, 1, 1)
        self.label_2 = QtWidgets.QLabel(self.groupBox)
        self.label_2.setObjectName("label_2")
        self.gridLayout_2.addWidget(self.label_2, 1, 0, 1, 1)
        self.s1_line_2 = QtWidgets.QLineEdit(self.groupBox)
        self.s1_line_2.setObjectName("s1_line_2")
        self.gridLayout_2.addWidget(self.s1_line_2, 1, 1, 1, 1)
        self.label_6 = QtWidgets.QLabel(self.groupBox)
        self.label_6.setObjectName("label_6")
        self.gridLayout_2.addWidget(self.label_6, 8, 0, 1, 1)
        self.s1_bt_2 = QtWidgets.QPushButton(self.groupBox)
        self.s1_bt_2.setObjectName("s1_bt_2")
        self.gridLayout_2.addWidget(self.s1_bt_2, 4, 1, 1, 1)
        self.label_3 = QtWidgets.QLabel(self.groupBox)
        self.label_3.setObjectName("label_3")
        self.gridLayout_2.addWidget(self.label_3, 2, 0, 1, 1)
        self.label_4 = QtWidgets.QLabel(self.groupBox)
        self.label_4.setObjectName("label_4")
        self.gridLayout_2.addWidget(self.label_4, 5, 0, 1, 1)
        self.s1_line_4 = QtWidgets.QLineEdit(self.groupBox)
        self.s1_line_4.setObjectName("s1_line_4")
        self.gridLayout_2.addWidget(self.s1_line_4, 8, 1, 1, 1)
        self.s1_line_3 = QtWidgets.QLineEdit(self.groupBox)
        self.s1_line_3.setObjectName("s1_line_3")
        self.gridLayout_2.addWidget(self.s1_line_3, 2, 1, 1, 1)
        self.label_5 = QtWidgets.QLabel(self.groupBox)
        self.label_5.setObjectName("label_5")
        self.gridLayout_2.addWidget(self.label_5, 6, 0, 1, 1)
        self.s1_line_5 = QtWidgets.QLineEdit(self.groupBox)
        self.s1_line_5.setObjectName("s1_line_5")
        self.gridLayout_2.addWidget(self.s1_line_5, 9, 1, 1, 1)
        self.s1_bt_1 = QtWidgets.QPushButton(self.groupBox)
        self.s1_bt_1.setObjectName("s1_bt_1")
        self.gridLayout_2.addWidget(self.s1_bt_1, 3, 1, 1, 1)
        self.s1_com_1 = QtWidgets.QComboBox(self.groupBox)
        self.s1_com_1.setObjectName("s1_com_1")
        self.gridLayout_2.addWidget(self.s1_com_1, 5, 1, 1, 1)
        self.label_7 = QtWidgets.QLabel(self.groupBox)
        self.label_7.setObjectName("label_7")
        self.gridLayout_2.addWidget(self.label_7, 9, 0, 1, 1)
        self.s1_infoBrowser = QtWidgets.QTextBrowser(self.groupBox)
        self.s1_infoBrowser.setObjectName("s1_infoBrowser")
        self.gridLayout_2.addWidget(self.s1_infoBrowser, 7, 0, 1, 2)
        self.gridLayout.addWidget(self.groupBox, 0, 0, 2, 1)
        self.groupBox_2 = QtWidgets.QGroupBox(Form)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.groupBox_2.sizePolicy().hasHeightForWidth())
        self.groupBox_2.setSizePolicy(sizePolicy)
        font = QtGui.QFont()
        font.setPointSize(11)
        self.groupBox_2.setFont(font)
        self.groupBox_2.setObjectName("groupBox_2")
        self.gridLayout_3 = QtWidgets.QGridLayout(self.groupBox_2)
        self.gridLayout_3.setObjectName("gridLayout_3")
        self.s3_com_1 = QtWidgets.QComboBox(self.groupBox_2)
        self.s3_com_1.setObjectName("s3_com_1")
        self.gridLayout_3.addWidget(self.s3_com_1, 0, 2, 1, 1)
        self.s3_check_1 = QtWidgets.QCheckBox(self.groupBox_2)
        self.s3_check_1.setObjectName("s3_check_1")
        self.gridLayout_3.addWidget(self.s3_check_1, 0, 0, 1, 1)
        self.s3_bt_1 = QtWidgets.QPushButton(self.groupBox_2)
        self.s3_bt_1.setObjectName("s3_bt_1")
        self.gridLayout_3.addWidget(self.s3_bt_1, 0, 3, 1, 1)
        self.label_8 = QtWidgets.QLabel(self.groupBox_2)
        self.label_8.setObjectName("label_8")
        self.gridLayout_3.addWidget(self.label_8, 0, 1, 1, 1)
        self.s3_line_1 = QtWidgets.QLineEdit(self.groupBox_2)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.s3_line_1.sizePolicy().hasHeightForWidth())
        self.s3_line_1.setSizePolicy(sizePolicy)
        self.s3_line_1.setObjectName("s3_line_1")
        self.gridLayout_3.addWidget(self.s3_line_1, 1, 0, 1, 4)
        self.gridLayout.addWidget(self.groupBox_2, 1, 1, 1, 1)
        self.tabWidget = QtWidgets.QTabWidget(Form)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(7)
        sizePolicy.setVerticalStretch(3)
        sizePolicy.setHeightForWidth(self.tabWidget.sizePolicy().hasHeightForWidth())
        self.tabWidget.setSizePolicy(sizePolicy)
        font = QtGui.QFont()
        font.setPointSize(13)
        self.tabWidget.setFont(font)
        self.tabWidget.setObjectName("tabWidget")
        self.tab = QtWidgets.QWidget()
        self.tab.setObjectName("tab")
        self.gridLayout_5 = QtWidgets.QGridLayout(self.tab)
        self.gridLayout_5.setObjectName("gridLayout_5")
        self.s2_browser_1 = QtWidgets.QTextBrowser(self.tab)
        self.s2_browser_1.setObjectName("s2_browser_1")
        self.gridLayout_5.addWidget(self.s2_browser_1, 0, 0, 1, 1)
        self.tabWidget.addTab(self.tab, "")
        self.tab_3 = QtWidgets.QWidget()
        self.tab_3.setObjectName("tab_3")
        self.gridLayout_4 = QtWidgets.QGridLayout(self.tab_3)
        self.gridLayout_4.setObjectName("gridLayout_4")
        self.label_9 = QtWidgets.QLabel(self.tab_3)
        self.label_9.setObjectName("label_9")
        self.gridLayout_4.addWidget(self.label_9, 0, 0, 1, 1)
        self.tabWidget.addTab(self.tab_3, "")
        self.gridLayout.addWidget(self.tabWidget, 0, 1, 1, 1)

        self.retranslateUi(Form)
        self.tabWidget.setCurrentIndex(0)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.groupBox.setTitle(_translate("Form", "TCP Server setting"))
        self.label.setText(_translate("Form", "host IP"))
        self.label_2.setText(_translate("Form", "Port"))
        self.label_6.setText(_translate("Form", "user"))
        self.s1_bt_2.setText(_translate("Form", "re-config"))
        self.label_3.setText(_translate("Form", "timeout"))
        self.label_4.setText(_translate("Form", "client"))
        self.label_5.setText(_translate("Form", "info "))
        self.s1_bt_1.setText(_translate("Form", "open"))
        self.label_7.setText(_translate("Form", "time"))
        self.groupBox_2.setTitle(_translate("Form", "Send"))
        self.s3_check_1.setText(_translate("Form", "send in hex"))
        self.s3_bt_1.setText(_translate("Form", "send"))
        self.label_8.setText(_translate("Form", "send to :"))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("Form", "general recv"))
        self.label_9.setText(_translate("Form", "power by Ingrid kuan  contact me at:agathakuannew@gmail.com"))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), _translate("Form", "info page"))


function code with main 


tcpserver_tool_main.py

#prototype@2020-01-13
#coding: utf-8

from socketserver import BaseRequestHandler, ThreadingTCPServer
import socket
import threading
import time
import sys
import datetime
import queue
import struct

from PyQt5 import QtWidgets
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtCore import QTimer,QTime,QDateTime,Qt,QSize

from  tcpServer_tool import Ui_Form
client_msg_queue = queue.Queue()
client_add_addr_queue = queue.Queue()
client_remove_addr_queue =  queue.Queue()
client_recv_queue =queue.Queue()
client_send_msg_queue = queue.Queue()

client_socket_list = []

class client_sendBack(threading.Thread):  # 另一个线程任务,用于计数,暂且叫他计数线程
    def __init__(self, conn, msg):
        super(client_sendBack, self).__init__()
        self.conn= conn
        self.addrAndPort = conn.getpeername()
        self.send_msg = msg
    def run(self):
        #https://stackoverflow.com/questions/41250805/how-do-i-print-the-local-and-remote-address-and-port-of-a-connected-socket
        self.conn.sendall(self.send_msg.encode('utf-8'))

class check_socket_sum(threading.Thread):
    def __init__(self, input_q):
        super(check_socket_sum, self).__init__()
        self.communicate_queue = input_q
        self.count = 0
    def run(self):
        global client_socket_list
        while True:
            self.communicate_queue.put(str(len(client_socket_list)))
            self.count = self.count+1
            time.sleep(1)

class client_handler(BaseRequestHandler):
    ip = ''
    port = 0
    timeOut = 10*60
    def setup(self):
        global client_add_addr_queue,client_remove_addr_queue
        global client_msg_queue, client_socket_list
        self.ip = self.client_address[0].strip()  # 獲取客戶端的ip
        self.port = self.client_address[1]  # 獲取客戶端的port
        self.request.settimeout(self.timeOut)  # 對socket設定超時時間
        self.msg_queue = client_msg_queue
        client_socket_list.append(self.request)
        client_add_addr_queue.put(self.ip)
        self.disconnect_queue = client_remove_addr_queue

    def handle(self):
        global client_send_msg_queue
        address, pid = self.client_address
        print('%s connected!' % address)
        while True:
            data = self.request.recv(128)
            if len(data) > 0:
                response = '{}:{}'.format(self.ip, data)
                client_recv_queue.put(response)


    def finish(self):
        print("client is disconnect!")
        self.disconnect_queue.put(self.ip)
        try:
            client_socket_list.remove(self.server)
        except:
            pass

class TcpServer_Tool(QWidget, Ui_Form):
    def __init__(self):
        super(TcpServer_Tool, self).__init__()
        self.setupUi(self)
        self.setWindowTitle("TCP Server to multiple client")

        self.client_list = []
        self.client_num_queue = queue.Queue()

        self.infoInit()
        self.functionInit()
    def infoInit(self):
        self.s1_line_1.setText(str(self.get_host_ip()))
        self.s1_line_2.setText("8080")
        self.s1_line_3.setText("10")
        self.s1_line_4.setText("default user")

    def functionInit(self):
        self.sys_timer = QTimer(self)
        self.sys_timer.timeout.connect(self.update_time)
        self.sys_timer.start(500)
        self.print_update_timer = QTimer(self)
        self.print_update_timer.timeout.connect(self.update_ui)
        self.print_update_timer.start(100)

        watch_client_thread = check_socket_sum(self.client_num_queue)
        watch_client_thread.start()
        self.s1_bt_1.clicked.connect(self.server_open)
        self.s3_bt_1.clicked.connect(self.send_to_client)

        self.s1_browser_print(char="system start\r\n")
    def send_to_client(self):
        global client_socket_list
        ip = self.s3_com_1.currentText()
        msg = self.s3_line_1.text()
        send_msg = ip+","+msg
        self.s1_browser_print(char=send_msg)

        if ip == client_socket_list[self.s3_com_1.currentIndex()].getpeername()[0]:
            terminal_socket = client_socket_list[self.s3_com_1.currentIndex()]
            client_send_thread = client_sendBack(terminal_socket, msg)
            client_send_thread.start()


    def server_open(self):
        HOST = str(self.get_host_ip())
        PORT = int(self.s1_line_2.text())
        ADDR = (HOST, PORT)
        self.tcpserver = ThreadingTCPServer(ADDR, client_handler)  # 参数为监听地址和已建立连接的处理类
        self.tcpserver_thread = threading.Thread(target=self.tcpserver.serve_forever)  # 创建线程,线程用于TCP多线程

        self.tcpserver_thread.start()

        self.s1_line_1.setEnabled(False)
        self.s1_line_2.setEnabled(False)
        self.s1_line_3.setEnabled(False)
    def update_ui(self):
        global client_add_addr_queue, client_remove_addr_queue
        global client_recv_queue

        data = self.client_num_queue.get()
        data = "connected " + data + " " + "client"
        self.s1_browser_print(char=data)

        if client_add_addr_queue.empty() == False:
            self.updateClientList(func='add',arg=client_add_addr_queue.get())
        if client_remove_addr_queue.empty() == False:
            #https://stackoverflow.com/questions/53828161/pyqt-selected-combobox-item-should-remove-an-item-in-another-combobox
            idx = self.s3_com_1.findText(client_remove_addr_queue.get())
            self.updateClientList(func='remove', arg=idx)

        if client_recv_queue.empty() == False:
            self.s2_browser_print(char=client_recv_queue.get())

    def update_time(self):
        time_format = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.s1_line_5.setText(time_format)


    def updateClientList(self, func, arg):
        if func == 'add':
            self.s3_com_1.addItem(arg)
            self.s1_com_1.addItem(arg)
        if func == 'remove':
            self.s3_com_1.removeItem(arg)
            self.s1_com_1.removeItem(arg)

    def get_host_ip(self):
        #https://www.chenyudong.com/archives/python-get-local-ip-graceful.html#hostnameIP
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(('8.8.8.8', 80))
            ip = s.getsockname()[0]
        finally:
            s.close()
        return ip

    def s1_browser_print(self, char=" "):
        self.s1_infoBrowser.insertPlainText(char+'\r\n')
        textCursor = self.s1_infoBrowser.textCursor()
        textCursor.movePosition(textCursor.End)
        self.s1_infoBrowser.setTextCursor(textCursor)

    def s2_browser_print(self, char=" "):
        self.s2_browser_1.insertPlainText(char+'\r\n')
        textCursor = self.s2_browser_1.textCursor()
        textCursor.movePosition(textCursor.End)
        self.s2_browser_1.setTextCursor(textCursor)

if __name__ == "__main__":
    print("system start")
    app = QApplication(sys.argv)
    ui = TcpServer_Tool()
    ui.show()
    sys.exit(app.exec_())




5.how it looks like ??


6. reference:

  1. https://www.chenyudong.com/archives/python-get-local-ip-graceful.html#hostnameIP
  2. https://stackoverflow.com/questions/53828161/pyqt-selected-combobox-item-should-remove-an-item-in-another-combobox          
  3. https://python3-cookbook.readthedocs.io/zh_CN/latest/c11/p02_creating_tcp_server.html
  4. https: // blog.csdn.net / u014453898 / article / details / 71440260
  5. https://www.itread01.com/content/1546500615.html
  6. https://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p03_communicating_between_threads.html




2019年10月30日 星期三

Android ble app: record .wav file which audio stream via ble (Mono, 16000Hz)

This is an example for android app supposed to:


use ble device: nordic thingy:52 
Audio Source: thingy:52 mic
(Mono, 16 bits ADC, sample rate:16000 Hz)



app flow chart:


In AndroidManifest.xml, require permission:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.android.bluetoothlegatt"
    android:versionCode="1"
    android:versionName="1.0">

    <!-- Min/target SDK versions (<uses-sdk>) managed by build.gradle -->
    
    <!-- Declare this required feature if you want to make the app available to BLE-capable
    devices only.  If you want to make your app available to devices that don't support BLE,
    you should omit this in the manifest.  Instead, determine BLE capability by using
    PackageManager.hasSystemFeature(FEATURE_BLUETOOTH_LE) -->

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.VIBRATE"/>
    <uses-permission android:name="android.permission.FLASHLIGHT"/>
    <uses-permission android:name="android.permission.RECORD_AUDIO" />

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />


    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>


    <application android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher3"
        android:roundIcon="@mipmap/ic_launcher3_round"
        android:supportsRtl="true"
        android:theme="@android:style/Theme.Holo.Light">
        <activity android:name=".DeviceScanActivity"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name=".DeviceControlActivity"/>
        <service android:name=".BluetoothLeService" android:enabled="true"/>
    </application>

</manifest>

But also need to ask again in activity:
In DeviceControlActivity.java:

 mStartRecord.setOnClickListener(new Button.OnClickListener(){
            @Override
            public void onClick(View v){
                mRecordInfo.setText("Press start record");
                if(checkStoragePermission2())
                {
                    mRecordInfo.setText("start record...");
                    Intent intent = new Intent(getApplicationContext(), BluetoothLeService.class);
                    intent.setAction(BluetoothLeService.ACTION_START_RECORDING_SERVICE);
                    startService(intent);
                }
            }
        });
public boolean checkStoragePermission2(){
        if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(
                        new String[]{
                                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                                Manifest.permission.READ_EXTERNAL_STORAGE},
                        REQ_CODE_READ_EXTERNAL_STORAGE_IMPORT);
                return false;
            }
        }
        return true;
    }

in BluetoothLeService.java, a handler to request mtu(ble Max Transmit Unit) to 276 bytes(default 23 bytes):

  public final static UUID UUID_HEART_RATE_MEASUREMENT =
            UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);
    public final static UUID THINGY_MICROPHONE_CHARACTERISTIC =
            UUID.fromString(SampleGattAttributes.THINGY_MICROPHONE_CHARACTERISTIC);

    private int mMtu;
    private int mtu = 276;
    private final Handler mMtuHandler;


after connected:

  @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            String intentAction;
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                intentAction = ACTION_GATT_CONNECTED;
                mConnectionState = STATE_CONNECTED;
                broadcastUpdate(intentAction);
                Log.i(TAG, "Connected to GATT server.");
                // Attempts to discover services after successful connection.
                Log.i(TAG, "Attempting to start service discovery:" +
                        mBluetoothGatt.discoverServices());
                mMtuHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        Log.i(TAG, "requestMtu:"+mMtu);
                        if (mBluetoothGatt != null) {
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
                            {
                                while(mMtu!=276)
                                {
                                    Log.i(TAG, "mMtu length"+mMtu);
                                    if (mMtu < mtu) {
                                        boolean isMtuRequestSuccess = mBluetoothGatt.requestMtu(276);
                                        Log.i(TAG, "mMtu length in request="+isMtuRequestSuccess);
                                    }
                                    try {
                                        Thread.sleep(1000);
                                    } catch (InterruptedException e){
                                        e.printStackTrace();
                                    }
                                }
                            } else {
                                mMtu = 23;
                                Log.i(TAG, "mMtu length in else"+mMtu);
                            }
                        }
                    }
                }, 1000);

            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                intentAction = ACTION_GATT_DISCONNECTED;
                mConnectionState = STATE_DISCONNECTED;
                Log.i(TAG, "Disconnected from GATT server.");
                broadcastUpdate(intentAction);

            }
        }

find mtu changed(onMtuChanged):
  @Override
        public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
            super.onMtuChanged(gatt, mtu, status);
            if (status == BluetoothGatt.GATT_SUCCESS){
                Log.i(TAG, "onMtuChanged() " + mtu + " Status: " + status);
                mMtu = mtu;
            }
            else
            {
                Log.i(TAG, "MTU configuration failed with error:"+ status);
            }
        }


after find sound service, notify Microphone characteristic,new ADCDecoder, file Stream:

public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
                                              boolean enabled) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }
        mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);

        // This is specific to Heart Rate Measurement.
        if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
            BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
                    UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            mBluetoothGatt.writeDescriptor(descriptor);
        }
        else if(THINGY_MICROPHONE_CHARACTERISTIC.equals(characteristic.getUuid()))
        {
            final BluetoothGattDescriptor microphoneDescriptor = characteristic.getDescriptor( UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
            microphoneDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            mBluetoothGatt.writeDescriptor(microphoneDescriptor);

            enableAdpcmMode(true);
        }
    }



enableAdpcmMode:

private void enableAdpcmMode(final boolean enable)
    {        
        mAdpcmDecoder = new ADPCMDecoder(mContext,false);       
    }


in DeviceControlActivity.java press 3 buttons to send intent to BluetoothLeService:

(the above members in BluetoothLeServie.java )

    private ADPCMDecoder mAdpcmDecoder;
    private final Handler mRecordHandler;
    //记录播放状态
    private boolean isRecording = false;
    private boolean isRecordUnsave = false;
    //数字信号数组
    private byte [] noteArray;
    //PCM文件
    private File pcmFile;
    //WAV文件
    private File wavFile;
    //文件输出流
    private OutputStream os;
    //文件根目录
    private String basePath = Environment.getExternalStorageDirectory().getAbsolutePath()+"/thingy_voice";
    //wav文件目录
    SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
    Date date = new Date(System.currentTimeMillis());
    private String outFileName = basePath+"/agatha_"+format.format(date)+".wav";
    //pcm文件目录
    private String inFileName = basePath+"/yinfu.pcm";

Buttons in Activity:
 mStartRecord = (Button) findViewById(R.id.startRecord_bt);
        mStopRecord = (Button)findViewById(R.id.stopRecord_bt);
        mTransWav_bt = (Button)findViewById(R.id.transWav_bt);
        mRecordInfo = (TextView)findViewById(R.id.recordInfo);
        mMicStreamInfo = (TextView)findViewById(R.id.streamInfo);


        mStartRecord.setOnClickListener(new Button.OnClickListener(){
            @Override
            public void onClick(View v){
                mRecordInfo.setText("Press start record");
                if(checkStoragePermission2())
                {
                    mRecordInfo.setText("start record...");
                    Intent intent = new Intent(getApplicationContext(), BluetoothLeService.class);
                    intent.setAction(BluetoothLeService.ACTION_START_RECORDING_SERVICE);
                    startService(intent);
                }
            }
        });
        mStopRecord.setOnClickListener(new Button.OnClickListener(){
            @Override
            public void onClick(View v){
                mRecordInfo.setText("Press stop record");
                Intent intent = new Intent(getApplicationContext(), BluetoothLeService.class);
                intent.setAction(BluetoothLeService.ACTION_STOP_RECORDING_SERVICE);
                startService(intent);
            }
        });
        mTransWav_bt.setOnClickListener(new Button.OnClickListener(){
            @Override
            public void onClick(View v){
                mRecordInfo.setText("Press save as WAV file");
                Intent intent = new Intent(getApplicationContext(), BluetoothLeService.class);
                intent.setAction(BluetoothLeService.ACTION_START_TRANS_WAV);
                startService(intent);
            }

        });

Action in BluetoothLeService.java:

 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent != null) {
            String action = intent.getAction();
            if (action != null && !action.isEmpty())
                switch (action){
                    case ACTION_START_RECORDING_SERVICE:
                        Log.i(TAG,"ACTION_START_RECORDING_SERVICE");
                        createFile();
                        startRecord();
                        break;
                    case ACTION_STOP_RECORDING_SERVICE:
                        Log.i(TAG,"ACTION_STOP_RECORDING_SERVICE");
                        stopRecord();
                        break;
                    case ACTION_START_TRANS_WAV:
                        Log.i(TAG,"ACTION_START_TRANS_WAV");
                        convertWaveFile();
                        break;

                }
            }
        return super.onStartCommand(intent, flags, startId);
    }



 public void createFile(){
        File baseFile = new File(basePath);

        if(!baseFile.exists())
            baseFile.mkdirs();
        pcmFile = new File(basePath+"/yinfu.pcm");
        wavFile = new File(basePath+"/agatha.wav");
        if(pcmFile.exists()){
            pcmFile.delete();
        }
        if(wavFile.exists()){
            wavFile.delete();
        }
        try{
            boolean i = pcmFile.createNewFile();
            Log.i(TAG,"create pcmfile:"+i);
            boolean j =wavFile.createNewFile();
            Log.i(TAG,"create pcmfile:"+j+",base"+basePath);
            os = new BufferedOutputStream(new FileOutputStream(pcmFile));
        }catch(IOException e){
            Log.i(TAG,e.toString());
        }
    }

    public void startRecord(){
        isRecording = true;
    }

    public void stopRecord(){
        isRecording = false;
    }

    public void convertWaveFile(){
        FileInputStream in = null;
        FileOutputStream out = null;
        long totalAudioLen = 0;
        long totalDataLen = totalAudioLen + 36;
        long longSampleRate = 16000;
        int channels = 1;
        long byteRate = 16 *longSampleRate* channels / 8;
        byte[] data = new byte[512];
        try{
            in = new FileInputStream(inFileName);

            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date date = new Date(System.currentTimeMillis());
            String outFileName_new = basePath+"/agatha_"+format.format(date)+".wav";
            out = new FileOutputStream(outFileName_new);
            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();
        }
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = new Date(System.currentTimeMillis());
        String outFileName_new = basePath+"/agatha_"+format.format(date)+".wav";
        final Intent intent = new Intent(ACTION_FINISH_WAV);
        intent.putExtra(EXTRA_DATA,outFileName_new);
        sendBroadcast(intent);

    }
    //https://blog.csdn.net/chezi008/article/details/53064604
    //https://blog.csdn.net/tong5956/article/details/82687001
    //https://blog.xuite.net/john75310/wretch/137622947-%5BAndroid%5D+AudioRecord+%E9%8C%84%E8%A3%BD+Wav+File
    private void WriteWaveFileHeader(FileOutputStream out, long totalAudioLen, long totalDataLen, long longSampleRate,
                                     int channels, long byteRate) throws IOException {
        byte[] header = new byte[44];
        header[0] = 'R'; // RIFF
        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';//WAVE
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        //FMT Chunk
        header[12] = 'f'; // 'fmt '
        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;
        //编码方式 10H为PCM编码格式
        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);
        //音频数据传送速率,采样率*通道数*采样深度/8
        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) (1 * 16 / 8);
        header[33] = 0;
        //每个样本的数据位数
        header[34] = 16;
        header[35] = 0;
        //Data chunk
        header[36] = 'd';//data
        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, 44);
    }
how to use:

the file will save in /storage0 

     //文件根目录
    private String basePath = Environment.getExternalStorageDirectory().getAbsolutePath()+"/thingy_voice";
    //wav文件目录
    SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
    Date date = new Date(System.currentTimeMillis());
    private String outFileName = basePath+"/agatha_"+format.format(date)+".wav";
    //pcm文件目录
    private String inFileName = basePath+"/yinfu.pcm";



Refereance: