Core Audio I/O File Recording

本文介绍如何使用CoreAudio库在iOS应用中实现音频录制功能,包括初始化音频单元、设置输入回调、调整音频格式等关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转自:http://pastebin.com/92Fyjaye

//
//  ViewController.m
//  Mic Recording
//
//  Created by Thorsten Wolfer on 03.02.12.
//  Copyright (c) 2012 malivo. All rights reserved.
//

#import "ViewController.h"
#import <CoreAudio/CoreAudioTypes.h>

#define kOutputBus 0
#define kInputBus 1
#define kSampleRate 44100

#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>
#import <AVFoundation/AVAudioSession.h>
#import "AudioUnit/AudioUnit.h"

@interface ViewController () {
    
    AudioComponentInstance      mAudioUnit;
    ExtAudioFileRef             mAudioFileRef;
}

-(void) initializeOutputUnit;
static OSStatus recordingCallback       (void *                            inRefCon,
                                         AudioUnitRenderActionFlags *      ioActionFlags,
                                         const AudioTimeStamp *            inTimeStamp,
                                         UInt32                            inBusNumber,
                                         UInt32                            inNumberFrames,
                                         AudioBufferList *                 ioData);
-(void)stopRecording:(NSTimer*)theTimer;

@end

@implementation ViewController

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Release any cached data, images, etc that aren't in use.
}

#pragma mark Core Audio

static void CheckError(OSStatus error, const char *operation) 
{
    if (error == noErr) return;
    
    char errorString[20];
    
    // See if it appears to be a 4-char-code *(UInt32 *)(errorString + 1) =
    CFSwapInt32HostToBig(error);
    if (isprint(errorString[1]) && isprint(errorString[2]) &&
        isprint(errorString[3]) && isprint(errorString[4])) { 
        
        errorString[0] = errorString[5] = '\''; errorString[6] = '\0';
    } 
    else {
        
        // No, format it as an integer sprintf(errorString, "%d", (int)error);
        fprintf(stderr, "Error: %s (%s)\n", operation, errorString);
    }
}

- (void)stopRecording:(NSTimer*)theTimer
{
    printf("\nstopRecording\n");
    AudioOutputUnitStop(mAudioUnit);
    AudioUnitUninitialize(mAudioUnit);
    
    OSStatus status = ExtAudioFileDispose(mAudioFileRef);
    printf("OSStatus(ExtAudioFileDispose): %ld\n", status);
}

- (void) initializeOutputUnit
{
    OSStatus status;
    
    // Describe audio component
    AudioComponentDescription desc;
    desc.componentType = kAudioUnitType_Output;
    desc.componentSubType = kAudioUnitSubType_RemoteIO;
    desc.componentFlags = 0;
    desc.componentFlagsMask = 0;
    desc.componentManufacturer = kAudioUnitManufacturer_Apple;
    
    // Get component
    AudioComponent inputComponent = AudioComponentFindNext(NULL, &desc);
    
    // Get audio units
    status = AudioComponentInstanceNew(inputComponent, &mAudioUnit);
    
    // Enable IO for recording
    UInt32 flag = 1;
    status = AudioUnitSetProperty(mAudioUnit, 
                                  kAudioOutputUnitProperty_EnableIO, 
                                  kAudioUnitScope_Input, 
                                  kInputBus,
                                  &flag, 
                                  sizeof(flag));
    
    // Enable IO for playback
    status = AudioUnitSetProperty(mAudioUnit, 
                                  kAudioOutputUnitProperty_EnableIO, 
                                  kAudioUnitScope_Output, 
                                  kOutputBus,
                                  &flag, 
                                  sizeof(flag));
    
    // Describe format
    AudioStreamBasicDescription audioFormat={0};
    audioFormat.mSampleRate         = kSampleRate;
    audioFormat.mFormatID           = kAudioFormatLinearPCM;
    audioFormat.mFormatFlags        = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
    audioFormat.mFramesPerPacket    = 1;
    audioFormat.mChannelsPerFrame   = 1;
    audioFormat.mBitsPerChannel     = 16;
    audioFormat.mBytesPerPacket     = 2;
    audioFormat.mBytesPerFrame      = 2;
    
    // Apply format
    status = AudioUnitSetProperty(mAudioUnit, 
                                  kAudioUnitProperty_StreamFormat, 
                                  kAudioUnitScope_Output, 
                                  kInputBus, 
                                  &audioFormat, 
                                  sizeof(audioFormat));
    status = AudioUnitSetProperty(mAudioUnit, 
                                  kAudioUnitProperty_StreamFormat, 
                                  kAudioUnitScope_Input, 
                                  kOutputBus, 
                                  &audioFormat, 
                                  sizeof(audioFormat));
    
    
    // Set input callback
    AURenderCallbackStruct callbackStruct;
    callbackStruct.inputProc = recordingCallback;
    callbackStruct.inputProcRefCon = (__bridge void *)self;
    
    status = AudioUnitSetProperty(mAudioUnit, 
                                  kAudioOutputUnitProperty_SetInputCallback, 
                                  kAudioUnitScope_Global, 
                                  kInputBus, 
                                  &callbackStruct, 
                                  sizeof(callbackStruct));
        
    // Disable buffer allocation for the recorder (optional - do this if we want to pass in our own)
    flag = 0;
    status = AudioUnitSetProperty(mAudioUnit, 
                                  kAudioUnitProperty_ShouldAllocateBuffer,
                                  kAudioUnitScope_Output, 
                                  kInputBus,
                                  &flag, 
                                  sizeof(flag));
        
    // On initialise le fichier audio
    NSArray  *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *destinationFilePath = [[NSString alloc] initWithFormat: @"%@/output.caf", documentsDirectory];
    NSLog(@">>> %@\n", destinationFilePath);
    
    CFURLRef destinationURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (__bridge CFStringRef)destinationFilePath, kCFURLPOSIXPathStyle, false);
    
    OSStatus setupErr = ExtAudioFileCreateWithURL(destinationURL, kAudioFileCAFType, &audioFormat, NULL, kAudioFileFlags_EraseFile, &mAudioFileRef);  
    CFRelease(destinationURL);
    NSAssert(setupErr == noErr, @"Couldn't create file for writing");
    
    setupErr = ExtAudioFileSetProperty(mAudioFileRef, kExtAudioFileProperty_ClientDataFormat, sizeof(AudioStreamBasicDescription), &audioFormat);
    NSAssert(setupErr == noErr, @"Couldn't create file for format");
    
    setupErr =  ExtAudioFileWriteAsync(mAudioFileRef, 0, NULL);
    NSAssert(setupErr == noErr, @"Couldn't initialize write buffers for audio file");
    
    CheckError(AudioUnitInitialize(mAudioUnit), "AudioUnitInitialize");
    CheckError(AudioOutputUnitStart(mAudioUnit), "AudioOutputUnitStart");
    
    [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(stopRecording:) userInfo:nil repeats:NO];
}

static void printAudioUnitRenderActionFlags(AudioUnitRenderActionFlags * ioActionFlags)
{
    if (*ioActionFlags == 0) {
        
        printf("AudioUnitRenderActionFlags(%lu) ", *ioActionFlags);
        return;
    }
    printf("AudioUnitRenderActionFlags(%lu): ", *ioActionFlags);
    if (*ioActionFlags & kAudioUnitRenderAction_PreRender)              printf("kAudioUnitRenderAction_PreRender ");
    if (*ioActionFlags & kAudioUnitRenderAction_PostRender)             printf("kAudioUnitRenderAction_PostRender ");
    if (*ioActionFlags & kAudioUnitRenderAction_OutputIsSilence)        printf("kAudioUnitRenderAction_OutputIsSilence ");
    if (*ioActionFlags & kAudioOfflineUnitRenderAction_Preflight)       printf("kAudioOfflineUnitRenderAction_Prefli ght ");
    if (*ioActionFlags & kAudioOfflineUnitRenderAction_Render)          printf("kAudioOfflineUnitRenderAction_Render");
    if (*ioActionFlags & kAudioOfflineUnitRenderAction_Complete)        printf("kAudioOfflineUnitRenderAction_Complete ");
    if (*ioActionFlags & kAudioUnitRenderAction_PostRenderError)        printf("kAudioUnitRenderAction_PostRenderError ");
    if (*ioActionFlags & kAudioUnitRenderAction_DoNotCheckRenderArgs)   printf("kAudioUnitRenderAction_DoNotCheckRenderArgs ");
}

static OSStatus recordingCallback       (void *                            inRefCon,
                                         AudioUnitRenderActionFlags *      ioActionFlags,
                                         const AudioTimeStamp *            inTimeStamp,
                                         UInt32                            inBusNumber,
                                         UInt32                            inNumberFrames,
                                         AudioBufferList *                 ioData) 
{
    double timeInSeconds = inTimeStamp->mSampleTime / kSampleRate;
    printf("\n%fs inBusNumber: %lu inNumberFrames: %lu ", timeInSeconds, inBusNumber, inNumberFrames);
    printAudioUnitRenderActionFlags(ioActionFlags);

    AudioBufferList bufferList;
    
    SInt16 samples[inNumberFrames]; // A large enough size to not have to worry about buffer overrun
    memset (&samples, 0, sizeof (samples));
    
    bufferList.mNumberBuffers = 1;
    bufferList.mBuffers[0].mData = samples;
    bufferList.mBuffers[0].mNumberChannels = 1;
    bufferList.mBuffers[0].mDataByteSize = inNumberFrames*sizeof(SInt16);

    ViewController* THIS = THIS = (__bridge ViewController *)inRefCon;
    
    OSStatus status;
    status = AudioUnitRender(THIS->mAudioUnit,     
                             ioActionFlags, 
                             inTimeStamp, 
                             kInputBus, 
                             inNumberFrames, 
                             &bufferList);
    
    if (noErr != status) {
        
        printf("AudioUnitRender error: %ld", status); 
        return noErr;
    }
    
    // Now, we have the samples we just read sitting in buffers in bufferList
    ExtAudioFileWriteAsync(THIS->mAudioFileRef, inNumberFrames, &bufferList);
        
    return noErr;     
}

#pragma mark - View lifecycle

- (void)viewDidLoad
{
    [super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.
    
    [self initializeOutputUnit];
}

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated
{
	[super viewWillDisappear:animated];
}

- (void)viewDidDisappear:(BOOL)animated
{
	[super viewDidDisappear:animated];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // Return YES for supported orientations
    return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}

@end


package com.example.demoapplication; // 定义应用程序的主包名 import android.Manifest; // 导入权限相关类 import android.content.pm.PackageManager; // 导入包管理器类 import android.media.AudioFormat; // 导入音频格式类 import android.media.AudioManager; // 导入音频管理类 import android.media.AudioRecord; // 导入音频录制类 import android.media.AudioTrack; // 导入音频播放类 import android.media.MediaRecorder; // 导入媒体录制类 import android.os.Bundle; // 导入Bundle类用于保存实例状态 import android.os.Handler; // 导入Handler类用于线程间通信 import android.os.Looper; // 导入Looper类用于消息循环 import android.os.Message; // 导入Message类用于传递消息 import android.speech.tts.TextToSpeech; // 导入文本转语音类 import android.util.Base64; // 导入Base64编码解码工具类 import android.util.Log; // 导入日志工具类 import android.widget.Button; // 导入按钮控件 import android.widget.Toast; // 导入提示信息显示类 import androidx.annotation.NonNull; // 导入非空注解支持 import androidx.appcompat.app.AppCompatActivity; // 导入AppCompatActivity基类 import androidx.core.app.ActivityCompat; // 导入兼容库中的Activity权限请求类 import androidx.core.content.ContextCompat; // 导入兼容库中的Context权限检查类 import org.json.JSONException; // 导入JSON异常类 import org.json.JSONObject; // 导入JSON对象类 import java.io.BufferedReader; // 导入缓冲读取流类 import java.io.BufferedWriter; // 导入缓冲写入流类 import java.io.ByteArrayInputStream; // 导入字节数组输入流类 import java.io.IOException; // 导入IO异常类 import java.io.InputStreamReader; // 导入输入流读取器 import java.io.OutputStreamWriter; // 导入输出流写入器 import java.net.ServerSocket; // 导入服务器端Socket类 import java.net.Socket; // 导入Socket网络连接类 import java.util.LinkedList; // 导入链表实现的队列 import java.util.Locale; // 导入本地化语言环境类 import java.util.Queue; // 导入队列接口 import java.util.concurrent.ExecutorService; // 导入线程池服务接口 import java.util.concurrent.Executors; // 导入线程池工厂类 import java.util.concurrent.ScheduledExecutorService; // 导入定时任务执行服务 import java.util.concurrent.TimeUnit; // 导入时间单位枚举 import java.util.concurrent.atomic.AtomicBoolean; // 导入原子布尔值类 public class MainActivity extends AppCompatActivity implements TextToSpeech.OnInitListener { // 主活动类继承自AppCompatActivity并实现TTS初始化监听器 private static final String TAG = "AudioRecorder"; // 日志标签,用于调试输出 private Button startRecordButton, stopRecordButton; // 录音开始和停止按钮 private Button playSoundButton, pauseSoundButton, stopSoundButton, resumeSoundButton, clearSoundsButton; // 播放控制按钮 private AudioRecord audioRecord; // 音频录制对象 private static final int SAMPLE_RATE = 16000; // 音频采样率定义为16kHz private static final int BUFFER_SIZE; // 缓冲区大小定义 static { int minBufferSize = AudioRecord.getMinBufferSize( // 获取最小缓冲区大小 SAMPLE_RATE, // 使用预设的采样率 AudioFormat.CHANNEL_IN_MONO, // 单声道输入 AudioFormat.ENCODING_PCM_16BIT // 16位PCM编码 ); BUFFER_SIZE = Math.max(minBufferSize, 4096); // 设置缓冲区大小为计算出的最小值或4096中较大的一个 } private ScheduledExecutorService scheduler; // 定时任务调度器 private AtomicBoolean isRecording = new AtomicBoolean(false); // 原子布尔值表示是否正在录音 private static final int PERMISSION_REQUEST_CODE = 1; // 权限请求码 private final ExecutorService executorService = Executors.newCachedThreadPool(); // 线程池用于异步任务执行 private ServerSocket serverSocket; // 服务器Socket用于接收连接 private volatile boolean isServerRunning = true; // 表示服务器是否运行 private volatile Socket clientSocket; // 当前客户端连接Socket private volatile BufferedWriter socketWriter; // Socket数据写入器 private TextToSpeech ttsEngine; // 文本转语音引擎 private boolean isTtsInitialized = false; // TTS引擎初始化标志 private AudioTrack audioTrack; // 音频播放轨道 private final Queue<byte[]> recordingQueue = new LinkedList<>(); // 存储录音数据的队列 private final Queue<byte[]> pausedQueue = new LinkedList<>(); // 暂停时存储录音数据的队列 private final Queue<byte[]> playbackQueue = new LinkedList<>(); // 存储待播放音频数据的队列 private final AtomicBoolean isPlaying = new AtomicBoolean(false); // 是否正在播放音频 private final AtomicBoolean isPaused = new AtomicBoolean(false); // 是否暂停播放 private volatile boolean isPlaybackThreadActive = false; // 播放线程是否活跃 private final Object audioTrackLock = new Object(); // 同步锁用于保护audioTrack资源 private final Object playbackQueueLock = new Object(); // 同步锁用于保护播放队列 private final Object recordingQueueLock = new Object(); // 同步锁用于保护录音队列 private final Handler handler = new Handler(Looper.getMainLooper()) { // 主线程的消息处理器 @Override public void handleMessage(@NonNull Message msg) { // 处理消息 switch (msg.what) { // 根据消息类型处理不同操作 case 0x11: // 客户端连接事件 Toast.makeText(MainActivity.this, "客户端已连接", Toast.LENGTH_SHORT).show(); // 显示Toast通知 break; case 0x12: // 开始录音事件 Toast.makeText(MainActivity.this, "开始录音", Toast.LENGTH_SHORT).show(); // 显示开始录音提示 sendJsonPacket("startRecorder", null); // 发送JSON消息通知客户端 playTts("开始录音"); // 使用TTS播报开始录音 break; case 0x14: // 停止录音事件 Toast.makeText(MainActivity.this, "停止录音", Toast.LENGTH_SHORT).show(); // 显示停止录音提示 sendJsonPacket("stopRecorder", null); // 发送JSON消息通知客户端 playTts("停止录音"); // 使用TTS播报停止录音 break; case 0x16: // 错误消息处理 Toast.makeText(MainActivity.this, "错误: " + msg.obj, Toast.LENGTH_LONG).show(); // 显示错误信息 break; case 0x17: // 播放完成事件 Toast.makeText(MainActivity.this, "播放完成", Toast.LENGTH_SHORT).show(); // 显示播放完成提示 isPlaying.set(false); // 设置播放状态为未播放 isPlaybackThreadActive = false; // 设置播放线程不活跃 updatePlayButtonsState(); // 更新播放按钮状态 break; case 0x18: // 添加到播放队列事件 Toast.makeText(MainActivity.this, "已添加到播放队列", Toast.LENGTH_SHORT).show(); // 提示用户已添加 break; case 0x19: // 播放按钮状态更新事件 updatePlayButtonsState(); // 更新播放按钮状态 break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { // Activity创建方法 super.onCreate(savedInstanceState); // 调用父类onCreate方法 setContentView(R.layout.activity_main); // 设置界面布局文件 ttsEngine = new TextToSpeech(this, this); // 初始化TTS引擎 initViews(); // 初始化视图组件 setupClickListeners(); // 设置点击事件监听器 checkPermissions(); // 检查应用所需权限 startServer(30000); // 启动服务器监听在30000端口 startSocketListener(); // 启动Socket监听器 } private void initViews() { // 初始化UI组件 startRecordButton = findViewById(R.id.startRecordButton); // 绑定开始录音按钮 stopRecordButton = findViewById(R.id.stopRecordButton); // 绑定停止录音按钮 playSoundButton = findViewById(R.id.playSoundButton); // 绑定播放声音按钮 pauseSoundButton = findViewById(R.id.pauseSoundButton); // 绑定暂停播放按钮 stopSoundButton = findViewById(R.id.stopSoundButton); // 绑定停止播放按钮 resumeSoundButton = findViewById(R.id.resumeSoundButton); // 绑定恢复播放按钮 clearSoundsButton = findViewById(R.id.clearSoundsButton); // 绑定清除所有录音按钮 stopRecordButton.setEnabled(false); // 默认禁用停止录音按钮 pauseSoundButton.setEnabled(false); // 默认禁用暂停播放按钮 stopSoundButton.setEnabled(false); // 默认禁用停止播放按钮 resumeSoundButton.setEnabled(false); // 默认禁用恢复播放按钮 } private void setupClickListeners() { // 设置按钮点击事件 startRecordButton.setOnClickListener(v -> startRecording()); // 开始录音按钮点击事件 stopRecordButton.setOnClickListener(v -> stopRecording()); // 停止录音按钮点击事件 // 播放控制按钮功能已注释 playSoundButton.setOnClickListener(v -> playNextInQueue()); pauseSoundButton.setOnClickListener(v -> pausePlayback()); stopSoundButton.setOnClickListener(v -> stopPlayback()); resumeSoundButton.setOnClickListener(v -> resumePlayback()); clearSoundsButton.setOnClickListener(v -> { clearPlaybackQueue(); handler.sendEmptyMessage(0x19); }); } // ==================== 录音功能实现 ==================== private void startRecording() { // 开始录音方法 if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) // 检查录音权限 != PackageManager.PERMISSION_GRANTED) { sendErrorMessage("没有录音权限"); // 如果没有权限发送错误消息 return; } if (isRecording.get()) { // 如果已经在录音则释放现有资源 releaseAudioResources(); } try { audioRecord = new AudioRecord( // 创建新的AudioRecord实例 MediaRecorder.AudioSource.MIC, // 使用麦克风作为音频源 SAMPLE_RATE, // 设置采样率为16kHz AudioFormat.CHANNEL_IN_MONO, // 设置单声道输入 AudioFormat.ENCODING_PCM_16BIT, // 设置16位PCM编码 BUFFER_SIZE // 使用预先计算好的缓冲区大小 ); if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { // 检查是否初始化成功 throw new IllegalStateException("AudioRecord 初始化失败"); } audioRecord.startRecording(); // 开始录音 isRecording.set(true); // 设置录音状态为true startRecordButton.setEnabled(false); // 禁用开始录音按钮 stopRecordButton.setEnabled(true); // 启用停止录音按钮 updatePlayButtonsState(); // 更新播放按钮状态 if (scheduler != null && !scheduler.isShutdown()) { // 如果已有调度器则关闭 scheduler.shutdownNow(); } scheduler = Executors.newSingleThreadScheduledExecutor(); // 创建新的单线程调度器 scheduler.scheduleAtFixedRate(this::captureAudioData, 0, 100, TimeUnit.MILLISECONDS); // 定期采集音频数据 handler.sendEmptyMessage(0x12); // 发送开始录音消息 } catch (Exception e) { Log.e(TAG, "录音启动失败", e); // 记录异常日志 sendErrorMessage("录音启动失败: " + e.getMessage()); // 发送错误消息 releaseAudioResources(); // 释放音频资源 } } private void stopRecording() { // 停止录音方法 if (!isRecording.get()) return; // 如果没有录音直接返回 isRecording.set(false); // 设置录音状态为false releaseAudioResources(); // 释放音频资源 stopRecordButton.setEnabled(false); // 禁用停止录音按钮 startRecordButton.setEnabled(true); // 启用开始录音按钮 updatePlayButtonsState(); // 更新播放按钮状态 handler.sendEmptyMessage(0x14); // 发送停止录音消息 } private void captureAudioData() { // 捕获音频数据方法 if (!isRecording.get() || audioRecord == null) return; // 如果不在录音或没有录音器返回 byte[] buffer = new byte[BUFFER_SIZE]; // 创建缓冲区 try { int bytesRead = audioRecord.read(buffer, 0, BUFFER_SIZE); // 从录音设备读取数据 if (bytesRead > 0) { // 如果有数据读取 synchronized (recordingQueueLock) { // 加锁保护录音队列 recordingQueue.offer(buffer.clone()); // 将数据复制后加入队列 } String base64Data = Base64.encodeToString(buffer, Base64.DEFAULT); // 数据进行Base64编码 sendJsonPacket("recording", base64Data); // 发送JSON数据包 } } catch (Exception e) { Log.e(TAG, "音频采集失败", e); // 记录异常日志 } } // ==================== 录音功能结束 ==================== // ==================== 辅助方法 ==================== private void updatePlayButtonsState() { // 更新播放按钮状态 runOnUiThread(() -> { // 在主线程中执行 boolean hasRecordings = !recordingQueue.isEmpty() || !pausedQueue.isEmpty(); // 是否有录音数据 boolean isPlayingState = isPlaying.get() && !isPaused.get(); // 是否正在播放且未暂停 playSoundButton.setEnabled(hasRecordings && !isPlayingState); // 设置播放按钮是否可用 pauseSoundButton.setEnabled(isPlayingState); // 设置暂停按钮是否可用 stopSoundButton.setEnabled(isPlaying.get() || isPaused.get()); // 设置停止按钮是否可用 resumeSoundButton.setEnabled(!playbackQueue.isEmpty() && isPaused.get()); // 设置恢复按钮是否可用 clearSoundsButton.setEnabled(hasRecordings); // 设置清除按钮是否可用 }); } private void playTts(String text) { // 播放TTS文本 if (isTtsInitialized) { // 如果TTS已初始化 ttsEngine.speak(text, TextToSpeech.QUEUE_FLUSH, null); // 清除之前的队列并播放新文本 } } private void releaseAudioResources() { // 释放音频资源 if (audioRecord != null) { // 如果存在录音器 try { if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { // 如果正在录音 audioRecord.stop(); // 停止录音 } audioRecord.release(); // 释放资源 } catch (IllegalStateException e) { Log.e(TAG, "停止录音失败", e); // 记录异常日志 } finally { audioRecord = null; // 设置为null } } if (scheduler != null) { // 如果存在调度器 try { scheduler.shutdownNow(); // 关闭调度器 if (!scheduler.awaitTermination(500, TimeUnit.MILLISECONDS)) { // 等待关闭 Log.w(TAG, "录音线程池未正常关闭"); // 记录警告日志 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 中断当前线程 } finally { scheduler = null; // 设置为null } } } private void sendJsonPacket(String type, Object data) { // 发送JSON数据包 if (clientSocket == null || clientSocket.isClosed() || socketWriter == null) { // 如果Socket无效 return; // 直接返回 } try { JSONObject packet = new JSONObject(); // 创建JSON对象 packet.put("type", type); // 添加类型字段 if (data != null) { // 如果有数据 packet.put("data", data); // 添加数据字段 } synchronized (this) { // 加锁确保线程安全 if (socketWriter != null) { // 如果写入器有效 socketWriter.write(packet.toString()); // 写入JSON字符串 socketWriter.write("\n\n"); // 写入换行符作为分隔符 socketWriter.flush(); // 刷新缓冲区 } } } catch (Exception e) { Log.e(TAG, "发送数据包失败: " + type, e); // 记录异常日志 } } private void sendErrorMessage(String message) { // 发送错误消息 handler.obtainMessage(0x16, message).sendToTarget(); // 获取消息并发送 } private void checkPermissions() { // 检查权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) // 检查录音权限 != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, // 请求权限 new String[]{Manifest.permission.RECORD_AUDIO}, // 权限数组 PERMISSION_REQUEST_CODE); // 请求码 } } private void startServer(int port) { // 启动服务器 executorService.execute(() -> { // 异步执行 try { serverSocket = new ServerSocket(port); // 创建服务器Socket Log.i(TAG, "服务器启动: " + port); // 记录启动日志 while (isServerRunning) { // 循环等待连接 try { Socket socket = serverSocket.accept(); // 接受连接 clientSocket = socket; // 保存客户端Socket synchronized (this) { // 加锁同步 socketWriter = new BufferedWriter( // 创建缓冲写入器 new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); // UTF-8编码 } handler.sendEmptyMessage(0x11); // 发送客户端连接消息 } catch (IOException e) { if (isServerRunning) Log.e(TAG, "接受连接失败", e); // 记录异常日志 } } } catch (IOException e) { Log.e(TAG, "服务器启动失败", e); // 记录异常日志 runOnUiThread(() -> Toast.makeText(this, // 在主线程显示Toast "服务器启动失败: " + e.getMessage(), Toast.LENGTH_LONG).show()); } finally { closeServerSocket(); // 关闭服务器Socket } }); } private void startSocketListener() { // 启动Socket监听 executorService.execute(() -> { // 异步执行 while (true) { // 持续监听 if (clientSocket != null && !clientSocket.isClosed()) { // 如果有客户端连接 try { BufferedReader reader = new BufferedReader( // 创建缓冲读取器 new InputStreamReader(clientSocket.getInputStream(), "UTF-8")); // UTF-8编码 StringBuilder packetBuilder = new StringBuilder(); // 构建数据包 String line; // 临时变量存储每行数据 while ((line = reader.readLine()) != null) { // 逐行读取 if (line.isEmpty()) { // 如果是空行 if (packetBuilder.length() > 0) { // 如果有数据包 String packet = packetBuilder.toString(); // 转换为字符串 Log.d(TAG, "收到数据包: " + packet); // 记录收到的数据包 try { JSONObject command = new JSONObject(packet); // 解析JSON String type = command.getString("type"); // 获取类型 switch (type) { case "playSound": String base64Sound = command.optString("data"); if (base64Sound != null && !base64Sound.isEmpty()) { byte[] soundData = Base64.decode(base64Sound, Base64.DEFAULT); addSoundToQueue(soundData); handler.sendEmptyMessage(0x18); } break; case "pauseSound": pausePlayback(); break; case "stopSound": stopPlayback(); break; case "resumeSound": resumePlayback(); break; case "clearSounds": clearPlaybackQueue(); handler.sendEmptyMessage(0x19); break; } } catch (JSONException e) { Log.e(TAG, "JSON解析失败", e); // 记录异常日志 } packetBuilder.setLength(0); // 清空构建器 } } else { packetBuilder.append(line); // 添加当前行 } } } catch (IOException e) { Log.e(TAG, "Socket读取失败", e); // 记录异常日志 } } else { try { Thread.sleep(500); // 如果没有连接休眠500毫秒 } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 中断线程 break; } } } }); } private void clearPlaybackQueue() { synchronized (playbackQueueLock) { playbackQueue.clear(); } } private void addSoundToQueue(byte[] soundData) { synchronized (playbackQueueLock) { playbackQueue.offer(soundData); } if (!isPlaying.get() && !isPlaybackThreadActive) { handler.post(this::playNextInQueue); } } private void playNextInQueue() { if (isPlaybackThreadActive) { return; } isPlaybackThreadActive = true; new Thread(() -> { try { while (!playbackQueue.isEmpty() && !isPaused.get()) { byte[] soundData; synchronized (playbackQueueLock) { soundData = playbackQueue.poll(); } if (soundData != null) { playSoundDirectly(soundData); } } if (!isPaused.get()) { isPlaying.set(false); isPlaybackThreadActive = false; handler.sendEmptyMessage(0x17); } } catch (Exception e) { Log.e(TAG, "播放队列处理失败", e); sendErrorMessage("播放失败: " + e.getMessage()); } }).start(); } private void playSoundDirectly(byte[] soundData) { if (soundData == null || soundData.length == 0) { return; } try { isPlaying.set(true); isPaused.set(false); synchronized (audioTrackLock) { int bufferSize = AudioTrack.getMinBufferSize( SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT ); if (audioTrack != null) { audioTrack.release(); } audioTrack = new AudioTrack( AudioManager.STREAM_MUSIC, SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM ); if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) { throw new IllegalStateException("AudioTrack 初始化失败"); } audioTrack.play(); ByteArrayInputStream inputStream = new ByteArrayInputStream(soundData); byte[] buffer = new byte[bufferSize]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1 && !isPaused.get()) { audioTrack.write(buffer, 0, bytesRead); } audioTrack.stop(); audioTrack.release(); audioTrack = null; if (!isPaused.get()) { runOnUiThread(() -> { updatePlayButtonsState(); }); } } } catch (Exception e) { Log.e(TAG, "音频播放失败", e); sendErrorMessage("播放失败: " + e.getMessage()); } } private void pausePlayback() { if (!isPlaying.get() || isPaused.get()) { return; } isPaused.set(true); synchronized (audioTrackLock) { if (audioTrack != null) { audioTrack.pause(); } } // 将当前播放的数据移到暂停队列 byte[] currentData = null; synchronized (playbackQueueLock) { if (!playbackQueue.isEmpty()) { currentData = playbackQueue.poll(); } } if (currentData != null) { synchronized (playbackQueueLock) { pausedQueue.offer(currentData); } } updatePlayButtonsState(); } private void resumePlayback() { if (!isPaused.get()) { return; } isPaused.set(false); // 将暂停的数据移回播放队列 byte[] pausedData = null; synchronized (playbackQueueLock) { if (!pausedQueue.isEmpty()) { pausedData = pausedQueue.poll(); } } if (pausedData != null) { synchronized (playbackQueueLock) { playbackQueue.offer(pausedData); } } synchronized (audioTrackLock) { if (audioTrack != null && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PAUSED) { audioTrack.play(); } } if (!isPlaybackThreadActive) { new Thread(() -> { isPlaybackThreadActive = true; try { while (!playbackQueue.isEmpty() && !isPaused.get()) { byte[] soundData; synchronized (playbackQueueLock) { soundData = playbackQueue.poll(); } if (soundData != null) { playSoundDirectly(soundData); } } if (!isPaused.get()) { isPlaying.set(false); isPlaybackThreadActive = false; handler.sendEmptyMessage(0x17); } } catch (Exception e) { Log.e(TAG, "恢复播放失败", e); sendErrorMessage("恢复播放失败: " + e.getMessage()); } }).start(); } updatePlayButtonsState(); } private void stopPlayback() { if (!isPlaying.get() && !isPaused.get()) { return; } isPlaying.set(false); isPaused.set(false); synchronized (playbackQueueLock) { playbackQueue.clear(); if (!pausedQueue.isEmpty()) { pausedQueue.clear(); } } synchronized (audioTrackLock) { if (audioTrack != null) { try { audioTrack.stop(); audioTrack.release(); } catch (Exception e) { Log.e(TAG, "停止音频播放失败", e); } finally { audioTrack = null; } } } updatePlayButtonsState(); handler.sendEmptyMessage(0x19); } private void closeServerSocket() { // 关闭服务器Socket try { if (serverSocket != null && !serverSocket.isClosed()) { // 如果Socket有效 serverSocket.close(); // 关闭Socket } } catch (IOException e) { Log.w(TAG, "关闭ServerSocket失败", e); // 记录异常日志 } } @Override public void onInit(int status) { // TTS初始化回调 if (status == TextToSpeech.SUCCESS) { // 如果初始化成功 int result = ttsEngine.setLanguage(Locale.CHINESE); // 设置中文语言 if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) { // 如果不支持中文 Log.e(TAG, "TTS语言不支持中文"); // 记录异常日志 } else { isTtsInitialized = true; // 设置为已初始化 } } } @Override protected void onDestroy() { // Activity销毁方法 super.onDestroy(); // 调用父类方法 isServerRunning = false; // 设置服务器不再运行 if (ttsEngine != null) { // 如果TTS引擎存在 ttsEngine.stop(); // 停止TTS ttsEngine.shutdown(); // 关闭TTS } closeServerSocket(); // 关闭服务器Socket closeSocket(clientSocket); // 关闭客户端Socket stopRecording(); // 停止录音 executorService.shutdown(); // 关闭线程池 try { if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) { // 等待关闭 executorService.shutdownNow(); // 立即关闭 } } catch (InterruptedException e) { executorService.shutdownNow(); // 立即关闭 Thread.currentThread().interrupt(); // 中断当前线程 } releaseAudioResources(); // 释放音频资源 } private void closeSocket(Socket socket) { // 关闭Socket try { if (socket != null && !socket.isClosed()) { // 如果Socket有效 socket.close(); // 关闭Socket } } catch (IOException e) { Log.w(TAG, "关闭Socket失败", e); // 记录异常日志 } if (socket == clientSocket) { // 如果是客户端Socket clientSocket = null; // 设置为null synchronized (this) { socketWriter = null; // 设置写入器为null } } } } 生成python脚本
07-05
package com.example.demoapplication; import android.Manifest; // 导入录音权限类 import android.content.pm.PackageManager; // 导入包管理器权限类 import android.media.AudioFormat; // 导入音频格式类 import android.media.AudioManager; // 导入音频管理类 import android.media.AudioRecord; // 导入音频录制类 import android.media.MediaPlayer; // 导入媒体播放器类 import android.media.MediaRecorder; // 导入媒体录制器类 import android.os.Bundle; // 导入Android数据包类 import android.os.Handler; // 导入消息处理类 import android.os.Looper; // 导入线程循环类 import android.os.Message; // 导入消息传递类 import android.speech.tts.TextToSpeech; // 导入文本转语音接口 import android.util.Log; // 导入日志工具类 import android.widget.Button; // 导入按钮组件 import android.widget.Toast; // 导入提示框类 import androidx.annotation.NonNull; // 导入非空注解 import androidx.appcompat.app.AppCompatActivity; // 导入AppCompatActivity基类 import androidx.core.app.ActivityCompat; // 导入兼容库活动权限工具类 import androidx.core.content.ContextCompat; // 导入兼容库内容上下文工具类 import org.json.JSONObject; // 导入JSON对象类 import java.io.BufferedReader; // 导入缓冲字符读取流 import java.io.BufferedWriter; // 导入缓冲字符写入流 import java.io.File; // 导入文件操作类 import java.io.FileOutputStream; // 导入文件输出流 import java.io.IOException; // 导入输入输出异常类 import java.io.InputStreamReader; // 导入字符输入流包装器 import java.io.OutputStreamWriter; // 导入字符输出流包装器 import java.net.ServerSocket; // 导入服务器套接字 import java.net.Socket; // 导入网络连接套接字 import java.util.LinkedList; // 导入链表队列结构 import java.util.Locale; // 导入语言环境类 import java.util.Queue; // 导入队列接口 import java.util.concurrent.ExecutorService; // 导入执行服务线程池 import java.util.concurrent.Executors; // 导入线程池工厂类 import java.util.concurrent.ScheduledExecutorService; // 导入定时任务执行服务 import java.util.concurrent.TimeUnit; // 导入时间单位枚举 import java.util.concurrent.atomic.AtomicBoolean; // 导入原子布尔值 public class MainActivity extends AppCompatActivity implements TextToSpeech.OnInitListener { private static final String TAG = "AudioRecorder"; // 日志标签 private static final int PERMISSION_REQUEST_CODE = 1; // 权限请求代码 private static final int SAMPLE_RATE = 16000; // 音频采样率 private static final int BUFFER_SIZE; // 缓冲区大小 static { // 计算最小缓冲区大小 int minBufferSize = AudioRecord.getMinBufferSize( SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, // 单声道采集 AudioFormat.ENCODING_PCM_16BIT // 16位PCM编码 ); BUFFER_SIZE = Math.max(minBufferSize, 4096); // 取最大值作为最终缓冲区大小 } // UI组件声明 private Button startRecordButton, stopRecordButton; // 录音控制按钮 private Button playSoundButton, pauseSoundButton, stopSoundButton, resumeSoundButton, clearSoundsButton; // 播放控制按钮 // 音频录制相关变量 private AudioRecord audioRecord; // 音频录制对象 private ScheduledExecutorService scheduler; // 定时任务调度器 private AtomicBoolean isRecording = new AtomicBoolean(false); // 是否正在录音标志 private final Queue<byte[]> recordingQueue = new LinkedList<>(); // 录制数据队列 // 网络通信相关变量 private final ExecutorService executorService = Executors.newCachedThreadPool(); // 弹性线程池 private ServerSocket serverSocket; // 服务器端监听套接字 private volatile boolean isServerRunning = true; // 服务器运行状态标志 private volatile Socket clientSocket; // 客户端连接套接字 private volatile BufferedWriter socketWriter; // 套接字写入器 // 文本转语音相关变量 private TextToSpeech ttsEngine; // TTS引擎实例 private boolean isTtsInitialized = false; // TTS初始化状态标志 // MP3播放相关变量 private MediaPlayer mediaPlayer; // 媒体播放器实例 private final Queue<byte[]> mp3PlaybackQueue = new LinkedList<>(); // 播放队列 private final AtomicBoolean isPlaying = new AtomicBoolean(false); // 是否正在播放 private final AtomicBoolean isPaused = new AtomicBoolean(false); // 是否暂停 private volatile boolean isPlaybackThreadActive = false; // 播放线程活跃状态 // 同步锁对象 private final Object playbackQueueLock = new Object(); // 播放队列锁 private final Object recordingQueueLock = new Object(); // 录制队列锁 private final Object mediaPlayerLock = new Object(); // 媒体播放器锁 // 主线程消息处理器 private final Handler handler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(@NonNull Message msg) { switch (msg.what) { case 0x11: // 客户端连接事件 Toast.makeText(MainActivity.this, "客户端已连接", Toast.LENGTH_SHORT).show(); break; case 0x12: // 开始录音事件 Toast.makeText(MainActivity.this, "开始录音", Toast.LENGTH_SHORT).show(); sendJsonPacket("startRecorder", null); playTts("开始录音"); break; case 0x14: // 停止录音事件 Toast.makeText(MainActivity.this, "停止录音", Toast.LENGTH_SHORT).show(); sendJsonPacket("stopRecorder", null); playTts("停止录音"); break; case 0x16: // 错误消息事件 Toast.makeText(MainActivity.this, "错误: " + msg.obj, Toast.LENGTH_LONG).show(); break; case 0x17: // 播放完成事件 Toast.makeText(MainActivity.this, "播放完成", Toast.LENGTH_SHORT).show(); isPlaying.set(false); isPlaybackThreadActive = false; updatePlayButtonsState(); break; case 0x18: // 添加到播放队列 Toast.makeText(MainActivity.this, "已添加到播放队列", Toast.LENGTH_SHORT).show(); break; case 0x19: // 更新播放按钮状态 updatePlayButtonsState(); break; case 0x20: // 开始播放MP3 Toast.makeText(MainActivity.this, "开始播放MP3", Toast.LENGTH_SHORT).show(); break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 设置界面布局 // 初始化文本转语音功能 ttsEngine = new TextToSpeech(this, this); // 初始化视图组件 initViews(); setupClickListeners(); // 绑定点击事件 // 检查必要权限 checkPermissions(); // 启动服务器并开始监听 startServer(30000); startSocketListener(); } private void initViews() { // 绑定UI控件 startRecordButton = findViewById(R.id.startRecordButton); stopRecordButton = findViewById(R.id.stopRecordButton); playSoundButton = findViewById(R.id.playSoundButton); pauseSoundButton = findViewById(R.id.pauseSoundButton); stopSoundButton = findViewById(R.id.stopSoundButton); resumeSoundButton = findViewById(R.id.resumeSoundButton); clearSoundsButton = findViewById(R.id.clearSoundsButton); // 设置初始按钮状态 stopRecordButton.setEnabled(false); pauseSoundButton.setEnabled(false); stopSoundButton.setEnabled(false); resumeSoundButton.setEnabled(false); } private void setupClickListeners() { // 设置录音按钮点击事件 startRecordButton.setOnClickListener(v -> startRecording()); stopRecordButton.setOnClickListener(v -> stopRecording()); // 设置播放控制按钮点击事件 playSoundButton.setOnClickListener(v -> playNextInQueue()); pauseSoundButton.setOnClickListener(v -> pausePlayback()); stopSoundButton.setOnClickListener(v -> stopPlayback()); resumeSoundButton.setOnClickListener(v -> resumePlayback()); clearSoundsButton.setOnClickListener(v -> { clearPlaybackQueue(); // 清空播放队列 handler.sendEmptyMessage(0x19); // 发送更新消息 }); } // ==================== 录音功能模块 ==================== private void startRecording() { // 检查录音权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { sendErrorMessage("没有录音权限"); return; } // 如果已经在录音则先释放资源 if (isRecording.get()) { releaseAudioResources(); } try { // 创建音频录制对象 audioRecord = new AudioRecord( MediaRecorder.AudioSource.MIC, // 使用麦克风输入 SAMPLE_RATE, // 采样率 AudioFormat.CHANNEL_IN_MONO, // 单声道 AudioFormat.ENCODING_PCM_16BIT, // 16位PCM编码 BUFFER_SIZE // 缓冲区大小 ); // 检查初始化状态 if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { throw new IllegalStateException("AudioRecord 初始化失败"); } // 开始录音 audioRecord.startRecording(); isRecording.set(true); startRecordButton.setEnabled(false); stopRecordButton.setEnabled(true); updatePlayButtonsState(); // 更新播放按钮状态 // 关闭旧的调度器 if (scheduler != null && !scheduler.isShutdown()) { scheduler.shutdownNow(); } // 创建新的单线程调度器 scheduler = Executors.newSingleThreadScheduledExecutor(); // 定期采集音频数据(每100ms) scheduler.scheduleAtFixedRate(this::captureAudioData, 0, 100, TimeUnit.MILLISECONDS); handler.sendEmptyMessage(0x12); // 发送开始录音通知 } catch (Exception e) { Log.e(TAG, "录音启动失败", e); sendErrorMessage("录音启动失败: " + e.getMessage()); releaseAudioResources(); } } private void stopRecording() { if (!isRecording.get()) return; isRecording.set(false); releaseAudioResources(); // 释放音频资源 stopRecordButton.setEnabled(false); startRecordButton.setEnabled(true); updatePlayButtonsState(); // 更新播放按钮状态 handler.sendEmptyMessage(0x14); // 发送停止录音通知 } private void captureAudioData() { if (!isRecording.get() || audioRecord == null) return; byte[] buffer = new byte[BUFFER_SIZE]; // 创建缓冲区 try { // 从音频设备读取数据 int bytesRead = audioRecord.read(buffer, 0, BUFFER_SIZE); if (bytesRead > 0) { synchronized (recordingQueueLock) { recordingQueue.offer(buffer.clone()); // 存储音频数据 } sendJsonPacket("recording", buffer); // 发送数据包 } } catch (Exception e) { Log.e(TAG, "音频采集失败", e); } } // ==================== MP3播放功能模块 ==================== private void addMp3ToQueue(byte[] mp3Data) { synchronized (playbackQueueLock) { mp3PlaybackQueue.offer(mp3Data); // 将MP3数据加入播放队列 } handler.sendEmptyMessage(0x18); // 发送加入队列通知 // 如果没有播放且线程不活跃,开始播放 if (!isPlaying.get() && !isPlaybackThreadActive) { playNextInQueue(); } } private void playNextInQueue() { if (isPlaybackThreadActive || mp3PlaybackQueue.isEmpty()) return; isPlaybackThreadActive = true; executorService.execute(() -> { // 在线程池中执行 try { while (!mp3PlaybackQueue.isEmpty() && !isPaused.get()) { byte[] mp3Data; synchronized (playbackQueueLock) { mp3Data = mp3PlaybackQueue.poll(); // 取出队列中的MP3数据 } if (mp3Data != null) { playMp3Data(mp3Data); // 播放MP3数据 } } if (!isPaused.get()) { isPlaying.set(false); isPlaybackThreadActive = false; handler.sendEmptyMessage(0x17); // 发送播放完成通知 } } catch (Exception e) { Log.e(TAG, "MP3播放失败", e); sendErrorMessage("MP3播放失败: " + e.getMessage()); } }); } private void playMp3Data(final byte[] mp3Data) { try { // 创建临时文件 File tempFile = File.createTempFile("audio_", ".mp3", getCacheDir()); try (FileOutputStream fos = new FileOutputStream(tempFile)) { fos.write(mp3Data); // 写入文件 } runOnUiThread(() -> { synchronized (mediaPlayerLock) { try { releaseMediaPlayer(); // 释放之前的播放器资源 mediaPlayer = new MediaPlayer(); // 创建新播放器 mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); // 设置音频流类型 mediaPlayer.setDataSource(tempFile.getAbsolutePath()); // 设置数据源 // 设置准备监听器 mediaPlayer.setOnPreparedListener(mp -> { mediaPlayer.start(); // 开始播放 isPlaying.set(true); // 标记为正在播放 isPaused.set(false); // 标记为未暂停 handler.sendEmptyMessage(0x20); // 发送开始播放通知 updatePlayButtonsState(); // 更新按钮状态 }); // 设置完成监听器 mediaPlayer.setOnCompletionListener(mp -> { // 删除临时文件 tempFile.delete(); // 播放下一个文件 if (!playNextInQueueImmediately()) { isPlaying.set(false); isPlaybackThreadActive = false; handler.sendEmptyMessage(0x17); } }); // 设置错误监听器 mediaPlayer.setOnErrorListener((mp, what, extra) -> { Log.e(TAG, "MediaPlayer error: " + what + ", " + extra); sendErrorMessage("播放错误: " + what); tempFile.delete(); // 删除临时文件 return false; }); mediaPlayer.prepareAsync(); // 准备播放 } catch (IOException e) { Log.e(TAG, "MediaPlayer初始化失败", e); tempFile.delete(); // 删除临时文件 } } }); } catch (IOException e) { Log.e(TAG, "创建临时文件失败", e); } } private boolean playNextInQueueImmediately() { if (!mp3PlaybackQueue.isEmpty()) { byte[] nextData; synchronized (playbackQueueLock) { nextData = mp3PlaybackQueue.poll(); // 取出下一个播放项 } if (nextData != null) { playMp3Data(nextData); // 直接播放 return true; } } return false; } // ==================== 播放控制 ==================== private void pausePlayback() { synchronized (mediaPlayerLock) { if (mediaPlayer != null && mediaPlayer.isPlaying()) { mediaPlayer.pause(); // 暂停播放 isPaused.set(true); // 设置暂停标志 updatePlayButtonsState(); // 更新按钮状态 } } } private void resumePlayback() { synchronized (mediaPlayerLock) { if (mediaPlayer != null && isPaused.get()) { mediaPlayer.start(); // 恢复播放 isPaused.set(false); // 取消暂停标志 updatePlayButtonsState(); // 更新按钮状态 } } } private void stopPlayback() { synchronized (mediaPlayerLock) { if (mediaPlayer != null) { try { mediaPlayer.stop(); // 停止播放 } catch (IllegalStateException e) { Log.e(TAG, "停止播放失败", e); } } } clearPlaybackQueue(); // 清空播放队列 isPlaying.set(false); // 设置未播放状态 isPaused.set(false); // 取消暂停状态 updatePlayButtonsState(); // 更新按钮状态 } private void clearPlaybackQueue() { synchronized (playbackQueueLock) { mp3PlaybackQueue.clear(); // 清空播放队列 } } private void releaseMediaPlayer() { synchronized (mediaPlayerLock) { if (mediaPlayer != null) { try { if (mediaPlayer.isPlaying()) { mediaPlayer.stop(); // 停止播放 } mediaPlayer.release(); // 释放资源 } catch (Exception e) { Log.e(TAG, "释放MediaPlayer失败", e); } finally { mediaPlayer = null; // 清空引用 } } } } // ==================== 辅助方法 ==================== private void updatePlayButtonsState() { runOnUiThread(() -> { boolean hasRecordings = !mp3PlaybackQueue.isEmpty(); // 是否有录音 boolean isPlayingState = isPlaying.get() && !isPaused.get(); // 是否正在播放 boolean isPausedState = isPaused.get(); // 是否暂停 // 设置按钮启用状态 playSoundButton.setEnabled(hasRecordings && !isPlayingState); pauseSoundButton.setEnabled(isPlayingState); stopSoundButton.setEnabled(isPlaying.get() || isPausedState); resumeSoundButton.setEnabled(isPausedState); clearSoundsButton.setEnabled(hasRecordings); }); } private void playTts(String text) { if (isTtsInitialized) { ttsEngine.speak(text, TextToSpeech.QUEUE_FLUSH, null); // 播放TTS } } private void releaseAudioResources() { if (audioRecord != null) { try { // 如果正在录音则停止 if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { audioRecord.stop(); } audioRecord.release(); // 释放资源 } catch (IllegalStateException e) { Log.e(TAG, "停止录音失败", e); } finally { audioRecord = null; // 清除引用 } } if (scheduler != null) { try { scheduler.shutdownNow(); // 关闭调度器 // 等待关闭完成 if (!scheduler.awaitTermination(500, TimeUnit.MILLISECONDS)) { Log.w(TAG, "录音线程池未正常关闭"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 重新设置中断标志 } finally { scheduler = null; // 清除引用 } } } private void sendJsonPacket(String type, Object data) { if (clientSocket == null || clientSocket.isClosed() || socketWriter == null) { return; // 如果连接无效则返回 } try { JSONObject packet = new JSONObject(); // 创建JSON包 packet.put("type", type); // 设置类型 if (data != null) { packet.put("data", data); // 添加数据 } synchronized (this) { if (socketWriter != null) { socketWriter.write(packet.toString()); // 写入JSON字符串 socketWriter.write("\n\n"); // 添加分隔符 socketWriter.flush(); // 刷新缓冲区 } } } catch (Exception e) { Log.e(TAG, "发送数据包失败: " + type, e); } } private void sendErrorMessage(String message) { handler.obtainMessage(0x16, message).sendToTarget(); // 发送错误消息 } private void checkPermissions() { // 检查录音权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions( this, new String[]{Manifest.permission.RECORD_AUDIO}, PERMISSION_REQUEST_CODE ); } // 检查存储权限(用于临时文件) if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions( this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE + 1 ); } } // ==================== 网络通信 ==================== private void startServer(int port) { executorService.execute(() -> { // 在线程池中执行 try { serverSocket = new ServerSocket(port); // 创建服务器套接字 Log.i(TAG, "服务器启动: " + port); // 打印日志 while (isServerRunning) { // 服务器运行循环 try { Socket socket = serverSocket.accept(); // 接受连接 clientSocket = socket; // 保存客户端套接字 synchronized (this) { // 创建套接字写入器 socketWriter = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream(), "UTF-8") ); } handler.sendEmptyMessage(0x11); // 发送连接成功通知 } catch (IOException e) { if (isServerRunning) Log.e(TAG, "接受连接失败", e); } } } catch (IOException e) { Log.e(TAG, "服务器启动失败", e); runOnUiThread(() -> Toast.makeText( this, "服务器启动失败: " + e.getMessage(), Toast.LENGTH_LONG ).show()); } finally { closeServerSocket(); // 关闭服务器套接字 } }); } private void startSocketListener() { executorService.execute(() -> { // 在线程池中执行 while (isServerRunning) { if (clientSocket != null && !clientSocket.isClosed()) { try { BufferedReader reader = new BufferedReader( new InputStreamReader(clientSocket.getInputStream(), "UTF-8") ); // 创建输入流读取器 // 先读取数据长度 String lengthLine = reader.readLine(); if (lengthLine == null) break; int dataLength = Integer.parseInt(lengthLine.trim()); // 解析数据长度 byte[] mp3Data = new byte[dataLength]; // 创建数据缓冲区 // 读取二进制数据 int bytesRead = 0; while (bytesRead < dataLength) { int count = clientSocket.getInputStream().read( mp3Data, bytesRead, dataLength - bytesRead); if (count == -1) break; bytesRead += count; } if (bytesRead == dataLength) { addMp3ToQueue(mp3Data); // 添加到播放队列 } else { Log.w(TAG, "数据不完整: " + bytesRead + "/" + dataLength); } } catch (IOException | NumberFormatException e) { Log.e(TAG, "Socket读取失败", e); closeSocket(clientSocket); // 关闭套接字 } } else { try { Thread.sleep(500); // 等待500毫秒 } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 重新设置中断标志 break; } } } }); } private void closeServerSocket() { try { if (serverSocket != null && !serverSocket.isClosed()) { serverSocket.close(); // 关闭服务器套接字 } } catch (IOException e) { Log.w(TAG, "关闭ServerSocket失败", e); } } private void closeSocket(Socket socket) { try { if (socket != null && !socket.isClosed()) { socket.close(); // 关闭套接字 } } catch (IOException e) { Log.w(TAG, "关闭Socket失败", e); } if (socket == clientSocket) { clientSocket = null; // 清除客户端套接字引用 synchronized (this) { socketWriter = null; // 清除写入器引用 } } } // ==================== 文本转语音 ==================== @Override public void onInit(int status) { if (status == TextToSpeech.SUCCESS) { int result = ttsEngine.setLanguage(Locale.CHINESE); // 设置中文语言 if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) { Log.e(TAG, "TTS语言不支持中文"); } else { isTtsInitialized = true; // 设置初始化完成标志 } } } // ==================== 生命周期 ==================== @Override protected void onDestroy() { super.onDestroy(); isServerRunning = false; // 停止服务器 // 释放TTS资源 if (ttsEngine != null) { ttsEngine.stop(); ttsEngine.shutdown(); } // 释放网络资源 closeServerSocket(); closeSocket(clientSocket); // 释放音频资源 stopRecording(); releaseMediaPlayer(); // 关闭线程池 executorService.shutdown(); try { // 等待关闭完成 if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) { executorService.shutdownNow(); // 强制关闭 } } catch (InterruptedException e) { executorService.shutdownNow(); // 强制关闭 Thread.currentThread().interrupt(); // 重新设置中断标志 } } } 我这个安卓端口是多少
最新发布
07-08
import socket import subprocess import websocket import time import os import threading import json import pyaudio import requests import hashlib import base64 from audioplayer import AudioPlayer import numpy as np from runner import set_global_var, get_global_var device_status = {} def listen_devices(): try: # 检测设备连接状态 result = subprocess.check_output("adb devices", shell=True).decode() current_devices = set(line.split('\t')[0] for line in result.splitlines()[1:] if line) # 检测新连接设备 for dev in current_devices - set(device_status.keys()): print(f"[设备已连接] {dev}") device_status[dev] = "connected" # 检测断开设备 for dev in set(device_status.keys()) - current_devices: print(f"[设备已断开连接] {dev}") del device_status[dev] time.sleep(1) except Exception as e: print(f"设备监控错误: {e}") def pcm_to_utf8(pcm_data: bytearray) -> str: """将16位PCM音频数据转为UTF-8字符串""" def validate_pcm(data: bytearray) -> bool: """验证PCM数据有效性""" return len(data) % 2 == 0 # 16位PCM需为偶数长度 if not validate_pcm(pcm_data): raise ValueError("无效的PCM数据长度,16位PCM需为偶数长度") try: # 转为16位有符号整数数组(小端序) samples = np.frombuffer(pcm_data, dtype='<i2') # 标准化到0-255范围 normalized = ((samples - samples.min()) * (255 / (samples.max() - samples.min()))).astype(np.uint8) # 转换为UTF-8字符串 return bytes(normalized).decode('utf-8', errors='replace') except Exception as e: raise RuntimeError(f"转换失败: {str(e)}") # 打印前32字节的十六进制表示 def parse_packets(buffer): """解析接收到的数据包""" # 解析数据包 end_marker = b'\n\n' while buffer.find(end_marker) != -1: packet_bytes = buffer[:buffer.find(end_marker) + len(end_marker)] buffer = buffer[buffer.find(end_marker) + len(end_marker):] try: json_bytes = packet_bytes[:-len(end_marker)] json_str = json_bytes.decode('utf-8') packet = json.loads(json_str) # 处理数据包 packet_type = packet.get("type") if packet_type == "recording": audio_data = base64.b64decode(packet.get("data", "")) print('audio_data ', audio_data) return audio_data elif packet_type in ["startRecorder", "stopRecord"]: pass # command_callback(packet_type) else: print(f"未知数据包类型: {packet_type}") except json.JSONDecodeError as e: print(f"JSON解析错误: {e}") except Exception as e: print(f"数据包处理错误: {e}") def start_server(port=35000): adb_path = "adb.exe" os.system(f"adb forward tcp:{port} tcp:30000") with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect(('localhost', port)) #s.bind(('0.0.0.0', port)) #s.listen(5) print(f"服务器已启动,正在监听端口 {port}...") while True: threading.Thread(target=listen_devices).start() #client_socket, addr = s.accept() #print(f"接收到来自 {addr} 的连接") buffer = bytearray() try: while True: data = s.recv(4096) if data == b'': pass else: print('data', data) #buffer.extend(data) if not data: print("连接断开") break buffer.extend(data) hex_preview = parse_packets(buffer) handle_audio_chunk(hex_preview) print('hex_preview',hex_preview) '''if data==b'': pass else: if len(data) > 0: hex_preview = ' '.join(f'{b:02x}' for b in data[:32]) print(f"前32字节: {hex_preview}...") #handle_audio_chunk(hex_preview) # 调试用:将PCM转为UTF-8 if len(data) < 1024: try: utf8_data = pcm_to_utf8(data) print(f"UTF-8预览: {utf8_data[:30]}...") #handle_audio_chunk(utf8_data) except: pass''' except Exception as e: print(f"接收音频数据异常: {e}") # 全局配置信息 # 播放时是否停止收音 stop_recording_when_playing = True # 打断播放的语音指令 stop_playing_words = ["别说了", "停止", "停下"] # 说话人id voice_speaker_id = 5199 # 语音活动检测-静音时间长度,超过这个时间视为停止说话 vad_silent_time = 1.5 # 语音合成参数 tts_params = {"lan": "zh", "cuid": "test-1234", "ctp": 1, "pdt":993, "spd":5, "pit": 5,"aue": 3} # 语音识别开始指令参数 asr_params = { "type": "START", "data": { "dev_pid": 1912, "dev_key": "com.baidu.open", "format": "pcm", "sample": 16000, "cuid": "my_test_dev", "type": 1, "asr_type": 1, "need_mid": False, "need_session_finish": True } } # 全局状态变量 ws_running = False ws_object = None recorder_running = False sound_play_list = [] current_audio_player = None chat_running = False current_query = '' last_asr_time = 0 def ws_send_start_command(ws): message = json.dumps(asr_params) ws.send_text(message) def ws_send_stop_command(ws): # 发送数据 msg_data = { "type": "FINISH", } message = json.dumps(msg_data) ws.send_text(message) def on_ws_message(ws, message): global current_query, last_asr_time data = json.loads(message) cmd_type = data.get("type") if cmd_type == 'MID_TEXT': mid_text = data.get("result") set_global_var("voicebot.asr.mid_text", mid_text) last_asr_time = time.time() # print("voicebot.asr.mid_text:", mid_text) elif cmd_type == "FIN_TEXT": query = data.get("result") # print("asr result:", query) set_global_var("voicebot.asr.result", query) last_asr_time = time.time() if query and len(query) > 0: current_query += query set_global_var("voicebot.chat.query", current_query) if ws_running == False: ws.close() def on_ws_close(ws, close_status_code, close_msg): print("websocket closed:", close_status_code, close_msg) def on_ws_error(ws, error): print(f"websocket Error: {error}") ws.close() def on_ws_open(ws): print("websocket connection opened:", ws) ws_send_start_command(ws) def check_chat(query:str): # for word in stop_playing_words: # if word in query: # stop_sound_player() # return False # if query in stop_playing_words: # stop_sound_player() # return False if is_playing_or_chatting(): return False return True def stop_sound_player(): global chat_running if current_audio_player: current_audio_player.stop() if len(sound_play_list) > 0: sound_play_list.clear() chat_running = False def run_chat(query:str): global chat_running chat_running = True set_global_var("voicebot.chat.query", query) params = {"query": query} params['username'] = get_global_var("voicebot.username") params['password'] = get_global_var("voicebot.password") response = requests.post("http://127.0.0.1:8010/chat", json=params, stream=True) total_reply = '' buffer = '' for line in response.iter_lines(): if line and chat_running: text = line.decode('utf-8') data = json.loads(text[5:]) content = data.get("content") buffer += content buffer = extract_play_text(buffer) total_reply += content set_global_var("voicebot.chat.reply", total_reply) # print(content, end='', flush=True) chat_running = False buffer = buffer.strip() if len(buffer) > 0: add_play_text(buffer) time.sleep(1) set_global_var("voicebot.chat.query", None) set_global_var("voicebot.chat.reply", None) #提取播放文本 def extract_play_text(total_text:str): separators = ",;。!?:,.!?\n" last_start_pos = 0 min_sentence_length = 4 for i in range(0, len(total_text)): if total_text[i] in separators and i - last_start_pos >= min_sentence_length: text = total_text[last_start_pos: i + 1] last_start_pos = i + 1 add_play_text(text.strip()) return total_text[last_start_pos:] #添加播放文本 def add_play_text(text:str): # print("add play text:", text) if len(text) > 1: sound_play_list.append({"text": text, "mp3_file": None}) # 语音合成 下载声音文件 def download_sound_file(text:str, speaker:int=None): if speaker is None: speaker = voice_speaker_id # print("tts create:", text) mp3_path = "sounds/" + str(speaker) if not os.path.exists(mp3_path): os.mkdir(mp3_path) mp3_file = mp3_path + "/" + hashlib.md5(text.encode('utf-8')).hexdigest() + ".mp3" if os.path.exists(mp3_file): return mp3_file params = tts_params params['per'] = speaker params['text'] = text url = "http://25.83.75.1:8088/Others/tts/text2audio/json" response = requests.post(url, json=params) data = response.json() if data['success'] == False: binary_array = json.loads(data['message']['message']) binary_data = bytes(binary_array) string_data = binary_data.decode('utf-8', errors='replace') data = json.loads(string_data) return "sounds/tts-failed.mp3" else: b64_string = data['result'].get('data') mp3_data = base64.b64decode(b64_string) with open(mp3_file, 'wb') as file: file.write(mp3_data) return mp3_file #开始聊天 def is_playing_or_chatting(): return len(sound_play_list) > 0 #播放下一个声音 def play_next_sound(): global sound_play_list, current_audio_player item = sound_play_list[0] mp3_file = item.get("mp3_file") if mp3_file: player = AudioPlayer(mp3_file) current_audio_player = player try: player.play(block=True) except Exception as e: print("player exception:" + e) current_audio_player = None # print("remained sound:", len(sound_play_list)) if len(sound_play_list) > 0: sound_play_list.pop(0) #运行websocket def run_websocket(): global ws_running, ws_object ws_running = True uri = "ws://25.83.75.1:8088/Others/asr/realtime_asr?sn=voicebot" ws = websocket.WebSocketApp(uri, on_message=on_ws_message, on_close=on_ws_close, on_error=on_ws_error) ws_object = ws ws.on_open = on_ws_open ws.run_forever() ws_running = False # print("websocket end") #开始记录 def start_recorder(chuck_size:int=2560): audio = pyaudio.PyAudio() try: stream = audio.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True, frames_per_buffer=chuck_size) return audio, stream except: print("打开麦克风失败") return None, None #获得不发音的时间 def get_silent_chunk(duration:float=0.16): sample_rate = 16000 # 采样率 num_samples = int(sample_rate * duration) # 计算样本数 silent_data = np.zeros(num_samples, dtype=np.int16) silent_bytes = silent_data.tobytes() return silent_bytes #处理音频块 def handle_audio_chunk(chunk_data:bytes): # 接受外部是否收音的要求 recording = get_global_var("voicebot.recording") if ws_object and ws_object.sock and ws_object.sock.connected: if recording == False or (stop_recording_when_playing and is_playing_or_chatting()): # print("ignor audio chunk:", sound_play_list, chat_running) ws_object.send_bytes(get_silent_chunk()) else: ws_object.send_bytes(chunk_data) #运行录音机 def run_recorder(audio=None, stream=None, chuck_size=2560): global recorder_running recorder_running = True set_global_var("voicebot.recording", True) while recorder_running: chunk_data = stream.read(chuck_size) print('chunk_data)',chunk_data) handle_audio_chunk(chunk_data) stream.stop_stream() stream.close() audio.terminate() # print("recorder end") #运行检查 def run_check(): global ws_running, recorder_running, current_query set_global_var("voicebot.running", True) while ws_running and recorder_running: time.sleep(1) if get_global_var("voicebot.running") == False: break if len(current_query) > 0 and last_asr_time > 0 and time.time() - last_asr_time > vad_silent_time: t = threading.Thread(target=run_chat, args=(current_query,)) t.start() current_query = '' ws_running = recorder_running = False set_global_var("voicebot.running", False) # print("语音助手已经停止") #运行播放机 def run_player(): while ws_running and recorder_running: time.sleep(0.1) if len(sound_play_list) > 0: play_next_sound() def run_tts(): while ws_running and recorder_running: time.sleep(0.1) for item in sound_play_list: if item.get("mp3_file") is None: item['mp3_file'] = download_sound_file(item['text']) def run(): active_threads = threading.enumerate() # 打印每个活跃线程的信息 for t in active_threads: if t.name == 'voicebot-runner': return "语音助手已经在运行中了" audio, stream = start_recorder() if audio is None or stream is None: return {"error": "语音助手开启失败,无法访问麦克风"} t = threading.Thread(target=run_websocket) t.daemon = True t.start() t=threading.Thread(target=start_server()) t.daemon = True t.start() t = threading.Thread(target=run_check, name='voicebot-runner') t.daemon = True t.start() t = threading.Thread(target=run_tts) t.daemon = True t.start() t = threading.Thread(target=run_player) t.daemon = True t.start() return "执行成功" if __name__ == "__main__": #run() start_server() 把这个TTS的功能融入到第一个脚本里面生成新脚本,并且修改安卓的代码package com.example.demoapplication; import android.Manifest; import android.content.pm.PackageManager; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaRecorder; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.speech.tts.TextToSpeech; import android.util.Base64; import android.util.Log; import android.widget.Button; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; public class MainActivity extends AppCompatActivity implements TextToSpeech.OnInitListener { private static final String TAG = "AudioRecorder"; private Button startRecordButton; private Button stopRecordButton; private Button uploadButton; // 音频录制相关 private AudioRecord audioRecord; private static final int SAMPLE_RATE = 44100; // 音频采样率 private static final int BUFFER_SIZE; // 静态代码块用于初始化缓冲区大小 static { int minBufferSize = 0; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) { minBufferSize = AudioRecord.getMinBufferSize( SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT ); } // 确保缓冲区大小有效 BUFFER_SIZE = Math.max(minBufferSize, 4096); } // 多线程任务调度器 private ScheduledExecutorService scheduler; private AtomicBoolean isRecording = new AtomicBoolean(false); // 录音状态标志 private static final int PERMISSION_REQUEST_CODE = 1; // 权限请求码 // 线程池服务 private final ExecutorService executorService = Executors.newCachedThreadPool(); // 网络服务器相关 private ServerSocket serverSocket; private volatile boolean isServerRunning = true; // 服务器运行状态 private volatile Socket clientSocket; // 客户端Socket连接 private volatile BufferedWriter socketWriter; // Socket写入流 // 文本转语音(TTS)相关变量 private TextToSpeech ttsEngine; private boolean isTtsInitialized = false; // 主线程消息处理器,用于UI更新 private final Handler handler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(@NonNull Message msg) { switch (msg.what) { case 0x11: // 客户端连接成功 Toast.makeText(MainActivity.this, "客户端已连接", Toast.LENGTH_SHORT).show(); break; case 0x12: // 开始录音 Toast.makeText(MainActivity.this, "开始录音", Toast.LENGTH_SHORT).show(); break; case 0x13: // 数据发送成功 // 减少Toast频率,避免刷屏 if (Math.random() < 0.1) { // 10%概率显示 Toast.makeText(MainActivity.this, "录音数据已发送", Toast.LENGTH_SHORT).show(); } break; case 0x14: // 停止录音 Toast.makeText(MainActivity.this, "停止录音", Toast.LENGTH_SHORT).show(); break; case 0x15: // 控制指令 Toast.makeText(MainActivity.this, "收到控制指令:" + msg.obj.toString(), Toast.LENGTH_SHORT).show(); break; case 0x16: // 错误消息 Toast.makeText(MainActivity.this, "错误: " + msg.obj.toString(), Toast.LENGTH_LONG).show(); break; case 0x17: // 网络状态 Toast.makeText(MainActivity.this, "网络: " + msg.obj.toString(), Toast.LENGTH_SHORT).show(); break; } } }; /** * Activity创建时调用,进行初始化操作。 * @param savedInstanceState 保存的状态数据 */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 初始化TTS引擎 ttsEngine = new TextToSpeech(this, this); initViews(); // 初始化视图组件 setupClickListeners(); // 设置点击事件监听器 checkPermissions(); // 检查权限 startServer(30000); // 启动服务器,端口30000 } /** * 初始化UI视图组件 */ private void initViews() { startRecordButton = findViewById(R.id.startRecordButton); stopRecordButton = findViewById(R.id.stopRecordButton); uploadButton = findViewById(R.id.uploadButton); stopRecordButton.setEnabled(false); uploadButton.setEnabled(false); } /** * 设置按钮点击事件监听器 */ private void setupClickListeners() { startRecordButton.setOnClickListener(v -> startRecording()); stopRecordButton.setOnClickListener(v -> stopRecording()); uploadButton.setOnClickListener(v -> uploadRecording()); } /** * 检查录音权限并请求必要权限 */ private void checkPermissions() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, PERMISSION_REQUEST_CODE); } } /** * 开始录音操作 */ private void startRecording() { // 检查权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { sendErrorMessage("没有录音权限"); return; } // 检查是否正在录音 if (isRecording.get() || audioRecord != null) { sendErrorMessage("录音已在进行中"); return; } // 检查网络连接 if (clientSocket == null || clientSocket.isClosed() || socketWriter == null) { sendErrorMessage("客户端未连接,无法录音"); return; } try { // 初始化 AudioRecord audioRecord = new AudioRecord( MediaRecorder.AudioSource.MIC, SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, BUFFER_SIZE ); // 检查初始化状态 if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { throw new IllegalStateException("AudioRecord 初始化失败"); } // 开始录音 audioRecord.startRecording(); isRecording.set(true); // 更新按钮状态 startRecordButton.setEnabled(false); stopRecordButton.setEnabled(true); uploadButton.setEnabled(false); // 创建定时任务发送音频数据 scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(this::uploadAudioData, 0, 100, TimeUnit.MILLISECONDS); // 提高发送频率 handler.sendEmptyMessage(0x12); // 发送开始录音的消息 // 发送开始录音控制指令 sendControlPacket("startRecorder"); // 播放TTS提示音 playTts("开始录音"); } catch (Exception e) { Log.e(TAG, "录音启动失败", e); sendErrorMessage("录音启动失败: " + e.getMessage()); releaseAudioResources(); } } /** * 停止录音操作 */ private void stopRecording() { if (!isRecording.get()) return; isRecording.set(false); releaseAudioResources(); // 更新按钮状态 stopRecordButton.setEnabled(false); uploadButton.setEnabled(true); handler.sendEmptyMessage(0x14); // 发送停止录音的消息 // 发送停止录音控制指令 sendControlPacket("stopRecor"); // 播放TTS提示音 playTts("停止录音"); } /** * 使用TTS播放指定文本 * @param text 要播放的文本内容 */ private void playTts(String text) { if (isTtsInitialized) { // 使用系统TTS播放 ttsEngine.speak(text, TextToSpeech.QUEUE_FLUSH, null); Log.i(TAG, "播放TTS: " + text); } else { Log.w(TAG, "TTS未初始化,无法播放: " + text); } } /** * 释放音频资源 */ private void releaseAudioResources() { if (audioRecord != null) { try { if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { audioRecord.stop(); } } catch (IllegalStateException e) { Log.e(TAG, "停止录音失败", e); } audioRecord.release(); audioRecord = null; } if (scheduler != null) { scheduler.shutdownNow(); scheduler = null; } } /** * 上传音频数据到服务器 */ private void uploadAudioData() { if (!isRecording.get() || clientSocket == null || clientSocket.isClosed() || socketWriter == null) { Log.w(TAG, "无法发送音频数据: 录音未进行或客户端未连接"); return; } byte[] buffer = new byte[BUFFER_SIZE]; try { int bytesRead = audioRecord.read(buffer, 0, BUFFER_SIZE); if (bytesRead > 0) { // 创建JSON数据包 JSONObject json = new JSONObject(); json.put("type", "recording"); json.put("data", Base64.encodeToString(buffer, 0, bytesRead, Base64.NO_WRAP)); // 使用NO_WRAP避免换行符 // 发送数据 synchronized (this) { if (socketWriter != null) { socketWriter.write(json.toString()); socketWriter.write("\n\n"); // 添加双换行作为结束标识 socketWriter.flush(); } } handler.sendEmptyMessage(0x13); // 发送录音数据的消息 } } catch (Exception e) { Log.e(TAG, "发送音频数据失败", e); sendErrorMessage("发送音频数据失败: " + e.getMessage()); } } /** * TTS初始化回调方法 * @param status 初始化状态 */ @Override public void onInit(int status) { if (status == TextToSpeech.SUCCESS) { // 设置默认语言为中文 int result = ttsEngine.setLanguage(Locale.CHINESE); if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) { Log.e(TAG, "TTS语言不支持中文"); } else { isTtsInitialized = true; Log.i(TAG, "TTS初始化成功,语言设置为中文"); } } else { Log.e(TAG, "TTS初始化失败"); } } /** * 发送控制指令包 * @param type 控制指令类型 */ private void sendControlPacket(String type) { if (clientSocket == null || clientSocket.isClosed() || socketWriter == null) { sendErrorMessage("无法发送控制指令: 客户端未连接"); return; } try { JSONObject packet = new JSONObject(); packet.put("type", type); packet.put("data", JSONObject.NULL); synchronized (this) { if (socketWriter != null) { socketWriter.write(packet.toString()); socketWriter.write("\n\n"); // 双换行作为结束标识 socketWriter.flush(); } } Log.i(TAG, "控制指令发送成功: " + type); } catch (Exception e) { Log.e(TAG, "发送控制指令失败", e); sendErrorMessage("发送控制指令失败: " + e.getMessage()); } } /** * 发送错误消息 * @param message 错误信息 */ private void sendErrorMessage(String message) { Message msg = handler.obtainMessage(0x16, message); handler.sendMessage(msg); } /** * 发送网络状态消息 * @param message 网络状态信息 */ private void sendNetworkMessage(String message) { Message msg = handler.obtainMessage(0x17, message); handler.sendMessage(msg); } /** * 上传录音文件(当前模式下无实际作用) */ private void uploadRecording() { Toast.makeText(this, "该模式下无需上传文件,已实时发送", Toast.LENGTH_SHORT).show(); } /** * 启动服务器监听 * @param port 监听端口号 */ private void startServer(int port) { executorService.execute(() -> { try { serverSocket = new ServerSocket(port); Log.i(TAG, "服务器启动,监听端口: " + port); sendNetworkMessage("服务器启动"); while (isServerRunning) { try { Socket socket = serverSocket.accept(); clientSocket = socket; // 创建输出流 synchronized (this) { socketWriter = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); } handler.sendEmptyMessage(0x11); // 发送客户端连接成功的消息 Log.i(TAG, "客户端已连接: " + socket.getInetAddress()); sendNetworkMessage("客户端已连接"); // 启动双向通信处理 executorService.execute(() -> startCommunication(socket)); } catch (IOException e) { if (isServerRunning) { Log.e(TAG, "接受连接失败", e); sendErrorMessage("接受连接失败: " + e.getMessage()); } } } } catch (IOException e) { Log.e(TAG, "服务器启动失败", e); runOnUiThread(() -> Toast.makeText(MainActivity.this, "服务器启动失败: " + e.getMessage(), Toast.LENGTH_LONG).show()); } finally { closeServerSocket(); } }); } /** * 开始与客户端的通信 * @param socket 客户端Socket连接 */ private void startCommunication(Socket socket) { try (java.io.BufferedReader reader = new java.io.BufferedReader( new java.io.InputStreamReader(socket.getInputStream(), "UTF-8"))) { StringBuilder packetBuilder = new StringBuilder(); int c; while ((c = reader.read()) != -1 && isServerRunning) { char ch = (char) c; packetBuilder.append(ch); // 检测到连续两个换行符,表示一个完整的数据包结束 if (packetBuilder.length() >= 2 && packetBuilder.charAt(packetBuilder.length() - 2) == '\n' && packetBuilder.charAt(packetBuilder.length() - 1) == '\n') { String packet = packetBuilder.toString().trim(); packetBuilder.setLength(0); // 清空构建器 if (!packet.isEmpty()) { try { JSONObject jsonObject = new JSONObject(packet); handleReceivedPacket(jsonObject); } catch (JSONException e) { Log.w(TAG, "JSON解析失败: " + packet, e); } } } } } catch (IOException e) { if (isServerRunning) { Log.e(TAG, "通信中断", e); runOnUiThread(() -> Toast.makeText(MainActivity.this, "通信中断: " + e.getMessage(), Toast.LENGTH_SHORT).show()); } } finally { closeSocket(socket); } } /** * 处理接收到的数据包 * @param jsonObject 接收到的JSON数据包 */ private void handleReceivedPacket(JSONObject jsonObject) { try { String type = jsonObject.getString("type"); Object data = jsonObject.opt("data"); // 发送消息到主线程进行显示 Message msg = handler.obtainMessage(0x15, type + ": " + data); handler.sendMessage(msg); Log.i(TAG, "收到控制指令: " + type); // 根据不同类型执行不同操作 switch (type) { case "start_recording": runOnUiThread(this::startRecording); break; case "stop_recording": runOnUiThread(this::stopRecording); break; case "ping": sendResponse("pong"); break; } } catch (JSONException e) { Log.e(TAG, "处理数据包失败", e); } } /** * 发送响应给客户端 * @param responseType 响应类型 */ private void sendResponse(String responseType) { if (clientSocket == null || clientSocket.isClosed() || socketWriter == null) return; try { JSONObject response = new JSONObject(); response.put("type", responseType); response.put("data", ""); synchronized (this) { if (socketWriter != null) { socketWriter.write(response.toString()); socketWriter.write("\n\n"); socketWriter.flush(); } } Log.i(TAG, "发送响应: " + responseType); } catch (Exception e) { Log.e(TAG, "发送响应失败", e); } } /** * 关闭指定的Socket连接 * @param socket 要关闭的Socket */ private void closeSocket(Socket socket) { try { if (socket != null && !socket.isClosed()) { socket.close(); } } catch (IOException e) { Log.w(TAG, "关闭Socket失败", e); } // 如果是当前客户端Socket,重置引用 if (socket == clientSocket) { clientSocket = null; synchronized (this) { socketWriter = null; } sendNetworkMessage("客户端断开连接"); } } /** * 关闭服务器Socket */ private void closeServerSocket() { try { if (serverSocket != null && !serverSocket.isClosed()) { serverSocket.close(); } } catch (IOException e) { Log.w(TAG, "关闭ServerSocket失败", e); } } /** * Activity销毁时调用,释放所有资源 */ @Override protected void onDestroy() { super.onDestroy(); isServerRunning = false; // 关闭TTS引擎 if (ttsEngine != null) { ttsEngine.stop(); ttsEngine.shutdown(); } // 关闭所有资源 closeServerSocket(); closeSocket(clientSocket); executorService.shutdownNow(); releaseAudioResources(); Log.i(TAG, "应用已销毁"); sendNetworkMessage("服务已停止"); } /** * 权限请求结果回调 * @param requestCode 请求码 * @param permissions 请求的权限数组 * @param grantResults 权限授予结果 */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_CODE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Toast.makeText(this, "录音权限已授予", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "录音权限被拒绝", Toast.LENGTH_SHORT).show(); } } } }
07-01
package com.example.demoapplication; import android.Manifest; // 录音权限相关 import android.content.pm.PackageManager; // 权限检查相关 import android.media.AudioFormat; // 音频格式定义 import android.media.AudioRecord; // 音频录制功能 import android.media.MediaRecorder; // 媒体录制配置 import android.os.Bundle; // Activity生命周期数据 import android.os.Handler; // 线程通信机制 import android.os.Looper; // 主线程消息循环 import android.os.Message; // 消息传递对象 import android.widget.Button; // UI按钮控件 import android.widget.Toast; // 短时提示信息 import androidx.annotation.NonNull; // 非空注解 import androidx.appcompat.app.AppCompatActivity; // 兼容Activity基类 import androidx.core.app.ActivityCompat; // 动态权限请求 import androidx.core.content.ContextCompat; // 权限状态查询 import java.io.DataInputStream; // 客户端数据输入流 import java.io.DataOutputStream; // 客户端数据输出流 import java.io.File; // 文件操作 import java.io.FileOutputStream; // 文件写入流 import java.io.IOException; // IO异常处理 import java.net.ServerSocket; // 服务端监听套接字 import java.net.Socket; // 客户端连接套接字 import java.util.concurrent.ExecutorService; // 线程池管理 import java.util.concurrent.Executors; // 线程池工厂 import java.util.concurrent.ScheduledExecutorService; // 定时任务调度 import java.util.concurrent.TimeUnit; // 时间单位定义 public class MainActivity extends AppCompatActivity { private Button startRecordButton; // 开始录音按钮 private Button stopRecordButton; // 停止录音按钮 private Button uploadButton; // 上传文件按钮 private AudioRecord audioRecord; // 音频录制对象 private static final int SAMPLE_RATE = 44100; // 采样率:44.1kHz private static final int BUFFER_SIZE = AudioRecord.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); // 缓冲区大小 private ScheduledExecutorService scheduler; // 录音定时器 private boolean isRecording = false; // 录音状态标志 private static final int PERMISSION_REQUEST_CODE = 1; // 权限请求码 private final ExecutorService executorService = Executors.newCachedThreadPool(); // 通用线程池 private ServerSocket serverSocket; // TCP服务端Socket private volatile boolean isServerRunning = true; // 服务运行状态 private Socket clientSocket; // 当前客户端连接 // 主线程消息处理器 private Handler handler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(@NonNull Message msg) { switch (msg.what) { case 0x11: // 客户端连接成功 Toast.makeText(MainActivity.this, "客户端已连接", Toast.LENGTH_SHORT).show(); break; case 0x12: // 数据接收完成 Toast.makeText(MainActivity.this, "上传完成", Toast.LENGTH_SHORT).show(); break; case 0x13: // 上传错误 Toast.makeText(MainActivity.this, "上传失败: " + msg.obj, Toast.LENGTH_SHORT).show(); break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initViews(); // 初始化视图组件 checkPermissions(); // 检查权限状态 setupClickListeners(); // 设置点击事件监听 startServer(30000); // 启动TCP服务器,监听30000端口 } /** * 视图初始化方法 * 绑定布局中的UI组件并设置初始状态 */ private void initViews() { startRecordButton = findViewById(R.id.startRecordButton); stopRecordButton = findViewById(R.id.stopRecordButton); uploadButton = findViewById(R.id.uploadButton); stopRecordButton.setEnabled(false); // 初始禁用停止按钮 uploadButton.setEnabled(false); // 初始禁用上传按钮 } /** * 权限检查方法 * 如果未授予录音权限则发起请求 */ private void checkPermissions() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, PERMISSION_REQUEST_CODE); } } /** * 按钮点击事件绑定 * 设置各按钮的响应逻辑 */ private void setupClickListeners() { startRecordButton.setOnClickListener(v -> startRecording()); // 开始录音 stopRecordButton.setOnClickListener(v -> stopRecording()); // 停止录音 uploadButton.setOnClickListener(v -> uploadRecording()); // 上传录音 } /** * 开始录音方法 * 初始化AudioRecord并启动录制 */ private void startRecording() { audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, BUFFER_SIZE); audioRecord.startRecording(); isRecording = true; startRecordButton.setEnabled(false); stopRecordButton.setEnabled(true); uploadButton.setEnabled(false); // 创建定时任务发送音频数据 scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(this::uploadAudioData, 0, 160, TimeUnit.MILLISECONDS); Toast.makeText(this, "开始录音", Toast.LENGTH_SHORT).show(); } /** * 停止录音方法 * 释放录音资源并清理状态 */ private void stopRecording() { isRecording = false; if (audioRecord != null) { audioRecord.stop(); audioRecord.release(); audioRecord = null; } if (scheduler != null) { scheduler.shutdownNow(); } stopRecordButton.setEnabled(false); uploadButton.setEnabled(true); Toast.makeText(this, "录音已停止", Toast.LENGTH_SHORT).show(); } /** * 实时上传音频数据 * 将当前缓冲区数据通过Socket发送给客户端 */ private void uploadAudioData() { if (!isRecording || clientSocket == null || clientSocket.isClosed()) return; byte[] buffer = new byte[BUFFER_SIZE]; int bytesRead = audioRecord.read(buffer, 0, BUFFER_SIZE); if (bytesRead > 0) { try { DataOutputStream dataOut = new DataOutputStream(clientSocket.getOutputStream()); dataOut.writeInt(bytesRead); dataOut.write(buffer, 0, bytesRead); dataOut.flush(); } catch (IOException e) { Message msg = handler.obtainMessage(0x13, e.getMessage()); handler.sendMessage(msg); } } } /** * 上传完整录音文件(当前未使用) * 提示该模式下为实时传输无需手动上传 */ private void uploadRecording() { // 可选:上传录音完整文件逻辑,如有需要可添加实现 Toast.makeText(this, "该模式下无需上传文件,已实时发送", Toast.LENGTH_SHORT).show(); } /** * 启动TCP服务器 * 在指定端口监听客户端连接 * * @param port 监听端口号 */ private void startServer(int port) { executorService.execute(() -> { try { serverSocket = new ServerSocket(port); while (isServerRunning) { Socket socket = serverSocket.accept(); clientSocket = socket; handler.sendEmptyMessage(0x11); executorService.execute(() -> handleClientUpload(socket)); } } catch (IOException e) { e.printStackTrace(); } }); } /** * 处理客户端上传 * 接收音频数据并保存到本地 * * @param socket 客户端Socket连接 */ private void handleClientUpload(Socket socket) { try (DataInputStream in = new DataInputStream(socket.getInputStream())) { while (true) { int length; try { length = in.readInt(); } catch (IOException e) { break; } if (length <= 0) break; byte[] buffer = new byte[length]; in.readFully(buffer); File file = new File(getExternalCacheDir(), "live_stream.pcm"); try (FileOutputStream out = new FileOutputStream(file, true)) { out.write(buffer); } handler.sendEmptyMessage(0x12); } } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException ignored) {} } } @Override protected void onDestroy() { super.onDestroy(); isServerRunning = false; try { if (serverSocket != null) serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } executorService.shutdownNow(); if (audioRecord != null) { audioRecord.release(); audioRecord = null; } if (scheduler != null) { scheduler.shutdownNow(); } try { if (clientSocket != null && !clientSocket.isClosed()) clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PERMISSION_REQUEST_CODE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Toast.makeText(this, "权限已授予", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "录音权限被拒绝", Toast.LENGTH_SHORT).show(); } } super.onRequestPermissionsResult(requestCode, permissions, grantResults); } } 通信格式 电脑与安卓设备之间的通信数据,统一使用utf-8文本编码,以json格式组织数据。 统一的json内容格式为:{“type”: xxx, “data”: xxxx}。 每个数据包必须包含一个type属性,表示数据包的类型或者指令,data属性表示具体的数据,可能为空,也可能是一个字符串、数字、对象或者数组,根据不同的类型而定。 每个json数据包发送之后,再发送连续两个换行符(\n\n)表示数据结尾。 电脑和安卓程序都需要发送数据给彼此,也需要接收彼此发送过来的数据。接收数据的时候,当收到连续两个换行符的时候,将之前的所有数据作为一个包,转换成字符串,再解析为json格式,然后进行具体的逻辑处理。 注意:对于双向持续数据流的通信方式,每个完整的数据包务必要有一个数据包的结束标志。 安卓发给电脑的数据包 开始录音 {“type”: “startRecorder”, “data”: null} 录音数据 {“type”: “recording”, “data”: “音频数据的base64编码字符串”} 停止录音 {“type”: “stopRecord”, “data”: null} 把以上的代码按照上述通信格式修改安卓代码
06-25
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值