一、服务器端程序设计-->server.py
python
import socket
import os
SAVE_DIR = "voice_uploads"
if not os.path.exists(SAVE_DIR):
os.makedirs(SAVE_DIR)
def start_server(host, port):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((host, port))
server_socket.listen(5)
print(f"服务已启动:{host}:{port}")
while True:
client_socket, addr = server_socket.accept()
print(f"\n手机连接:{addr}")
try:
name_len = int.from_bytes(client_socket.recv(2), 'big')
file_name = client_socket.recv(name_len).decode('utf-8')
file_size = int.from_bytes(client_socket.recv(4), 'big')
save_path = os.path.join(SAVE_DIR, file_name)
received = 0
with open(save_path, 'wb') as f:
while received < file_size:
buffer = client_socket.recv(min(4096, file_size - received))
if not buffer: break
f.write(buffer)
received += len(buffer)
print(f"接收完成:{save_path}")
except Exception as e:
print(f"错误:{e}")
finally:
client_socket.close()
if __name__ == "__main__":
start_server(host = "192.168.1.10", port=8092)
二、Android手机端程序设计
1、界面程序设计-->res/layout/activity_main.xml
XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="60dp"
android:text="语音数据采集APP"
android:textSize="12pt"
android:textStyle="bold"
android:textColor="#505050"
android:layout_gravity="center"
android:gravity="center_vertical">
</TextView>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="80dp"
android:layout_marginTop="30dp"
android:layout_gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="60dp"
android:text="服务器地址:"
android:textSize="25sp"
android:gravity="center_vertical">
</TextView>
<EditText
android:id="@+id/et_ip"
android:layout_width="240dp"
android:layout_height="60dp"
android:hint="请输入服务器ip地址"
android:text="192.168.1.10"
android:layout_marginLeft="5dp"
android:textSize="25sp"
android:textColor="#0000ff">
</EditText>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="80dp"
android:layout_marginTop="10dp"
android:layout_gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="60dp"
android:text="服务器端口:"
android:textSize="25sp"
android:gravity="center_vertical">
</TextView>
<EditText
android:id="@+id/et_port"
android:layout_width="240dp"
android:layout_height="60dp"
android:text="8092"
android:layout_marginLeft="5dp"
android:textSize="25sp"
android:textColor="#0000ff">
</EditText>
</LinearLayout>
<EditText
android:id="@+id/et_label"
android:layout_width="240dp"
android:layout_height="60dp"
android:hint="请输入数据标签"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:gravity="center"
android:textSize="30sp"
android:textColor="#00ff00">
</EditText>
<Button
android:id="@+id/btn_record"
android:layout_width="240dp"
android:layout_height="60dp"
android:text="长按录音"
android:textSize="25sp"
android:layout_marginTop="15dp"
android:layout_gravity="center"/>
<Button
android:id="@+id/btn_reset"
android:layout_width="240dp"
android:layout_height="60dp"
android:text="重录"
android:layout_marginTop="15dp"
android:textSize="25sp"
android:layout_gravity="center"/>
<Button
android:id="@+id/btn_upload"
android:layout_width="240dp"
android:layout_height="60dp"
android:text="确认上传"
android:layout_marginTop="15dp"
android:textSize="25sp"
android:layout_gravity="center"/>
<TextView
android:id="@+id/tv_status"
android:layout_marginTop="30dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="状态:等待操作"
android:textSize="25sp"
android:layout_gravity="center"/>
</LinearLayout>
2、Java程序设计-->java/com.example.voicecollection/MainActivity.java
java
package com.example.voicecollection;
import android.Manifest;
import android.content.pm.PackageManager;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.io.File;
import java.io.FileInputStream;
import java.net.Socket;
public class MainActivity extends AppCompatActivity {
private String SERVER_IP;
private int SERVER_PORT;
private String label;
private static final int REQUEST_AUDIO_PERMISSION = 1001;
private EditText etIP,etPort,etLabel;
private Button btnRecord, btnReset, btnUpload;
private TextView tvStatus;
private MediaRecorder mediaRecorder;
private String currentFilePath;
private boolean isRecording = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etIP = findViewById(R.id.et_ip);
etPort = findViewById(R.id.et_port);
etLabel = findViewById(R.id.et_label);
btnRecord = findViewById(R.id.btn_record);
btnReset = findViewById(R.id.btn_reset);
btnUpload = findViewById(R.id.btn_upload);
tvStatus = findViewById(R.id.tv_status);
requestAudioPermission();
// 长按录音,松开停止 ------ 严格加状态判断,防止重复触发
btnRecord.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (!isRecording) { // 只有没在录音,才开始
startRecord();
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
if (isRecording) { // 只有正在录音,才停止
stopRecord();
}
}
return true;
});
btnReset.setOnClickListener(v -> resetRecord());
btnUpload.setOnClickListener(v -> uploadFile());
}
private void requestAudioPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_AUDIO_PERMISSION);
}
}
private void startRecord() {
if (isRecording) return;
releaseRecorder(); // 先彻底释放之前的录音器
label = etLabel.getText().toString().trim();
if (label.isEmpty()) {
tvStatus.setText("请输入标签");
return;
}
try {
File labelFolder = getExternalFilesDir(null);
currentFilePath = labelFolder.getAbsolutePath() + "/"+label+"_" + System.currentTimeMillis() + ".wav";
mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
mediaRecorder.setAudioSamplingRate(16000);
mediaRecorder.setOutputFile(currentFilePath);
mediaRecorder.prepare();
mediaRecorder.start();
isRecording = true;
btnRecord.setText("松开结束");
tvStatus.setText("状态:录音中...");
} catch (Exception e) {
tvStatus.setText("录音失败");
releaseRecorder();
}
}
private void stopRecord() {
if (!isRecording || mediaRecorder == null) {
return;
}
// 先停止,再释放,顺序绝对不能乱
try {
mediaRecorder.stop();
} catch (Exception ignored) {}
releaseRecorder();
btnRecord.setText("长按录音");
tvStatus.setText("状态:录音完成,可上传或重录");
}
private void resetRecord() {
releaseRecorder();
if (currentFilePath != null) {
new File(currentFilePath).delete();
currentFilePath = null;
}
tvStatus.setText("状态:已重录,可以再次录音");
}
private void uploadFile() {
if (currentFilePath == null) {
tvStatus.setText("请先录音");
return;
}
SERVER_IP = etIP.getText().toString().trim();
SERVER_PORT = safeParseInt(etPort.getText().toString());
if (SERVER_IP.isEmpty() || SERVER_PORT <= 0) {
tvStatus.setText("请填写正确的IP和端口");
return;
}
new Thread(() -> {
try {
runOnUiThread(() -> tvStatus.setText("状态:上传中..."));
Socket socket = new Socket(SERVER_IP, SERVER_PORT);
File file = new File(currentFilePath);
String name = file.getName();
long size = file.length();
socket.getOutputStream().write(new byte[]{(byte) (name.length() >> 8), (byte) name.length()});
socket.getOutputStream().write(name.getBytes());
socket.getOutputStream().write(new byte[]{(byte) (size >> 24), (byte) (size >> 16), (byte) (size >> 8), (byte) size});
FileInputStream fis = new FileInputStream(file);
byte[] buffer = new byte[4096];
int len;
while ((len = fis.read(buffer)) != -1) {
socket.getOutputStream().write(buffer, 0, len);
}
fis.close();
socket.close();
runOnUiThread(() -> tvStatus.setText("状态:上传成功!"));
} catch (Exception e) {
runOnUiThread(() -> tvStatus.setText("上传失败"));
}
}).start();
}
private void releaseRecorder() {
if (mediaRecorder != null) {
try {
mediaRecorder.reset();
mediaRecorder.release();
} catch (Exception ignored) {}
mediaRecorder = null;
}
isRecording = false;
}
@Override
protected void onDestroy() {
super.onDestroy();
releaseRecorder();
}
private int safeParseInt(String str) {
try {
return Integer.parseInt(str.trim());
} catch (NumberFormatException e) {
return 0;
}
}
}
3、权限设置-->AndroidManifest.xml

java
<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-permission android:name="android.permission.INTERNET" />
4、编译生成APK文件,在手机端安装

三、测试
1、运行server.py启动服务器

2、打开手机APP采集音频数据

