添加语音websocket等,还没测试
This commit is contained in:
246
keyBoard/Class/AiTalk/VM/AudioStreamPlayer.m
Normal file
246
keyBoard/Class/AiTalk/VM/AudioStreamPlayer.m
Normal file
@@ -0,0 +1,246 @@
|
||||
//
|
||||
// AudioStreamPlayer.m
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Mac on 2026/1/15.
|
||||
//
|
||||
|
||||
#import "AudioStreamPlayer.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@interface AudioStreamPlayer ()
|
||||
|
||||
@property(nonatomic, strong) AVAudioEngine *audioEngine;
|
||||
@property(nonatomic, strong) AVAudioPlayerNode *playerNode;
|
||||
@property(nonatomic, strong) AVAudioFormat *playbackFormat;
|
||||
|
||||
// 片段跟踪
|
||||
@property(nonatomic, copy) NSString *currentSegmentId;
|
||||
@property(nonatomic, strong)
|
||||
NSMutableDictionary<NSString *, NSNumber *> *segmentDurations;
|
||||
@property(nonatomic, strong)
|
||||
NSMutableDictionary<NSString *, NSNumber *> *segmentStartTimes;
|
||||
@property(nonatomic, assign) NSUInteger scheduledSamples;
|
||||
@property(nonatomic, assign) NSUInteger playedSamples;
|
||||
|
||||
// 状态
|
||||
@property(nonatomic, assign) BOOL playing;
|
||||
@property(nonatomic, strong) dispatch_queue_t playerQueue;
|
||||
@property(nonatomic, strong) NSTimer *progressTimer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation AudioStreamPlayer
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_audioEngine = [[AVAudioEngine alloc] init];
|
||||
_playerNode = [[AVAudioPlayerNode alloc] init];
|
||||
_segmentDurations = [[NSMutableDictionary alloc] init];
|
||||
_segmentStartTimes = [[NSMutableDictionary alloc] init];
|
||||
_playerQueue = dispatch_queue_create("com.keyboard.aitalk.streamplayer",
|
||||
DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
// 默认播放格式:16kHz, Mono, Float32
|
||||
_playbackFormat =
|
||||
[[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
|
||||
sampleRate:16000
|
||||
channels:1
|
||||
interleaved:NO];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self stop];
|
||||
}
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
- (BOOL)start:(NSError **)error {
|
||||
if (self.playing) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// 连接节点
|
||||
[self.audioEngine attachNode:self.playerNode];
|
||||
[self.audioEngine connect:self.playerNode
|
||||
to:self.audioEngine.mainMixerNode
|
||||
format:self.playbackFormat];
|
||||
|
||||
// 启动引擎
|
||||
NSError *startError = nil;
|
||||
[self.audioEngine prepare];
|
||||
|
||||
if (![self.audioEngine startAndReturnError:&startError]) {
|
||||
if (error) {
|
||||
*error = startError;
|
||||
}
|
||||
NSLog(@"[AudioStreamPlayer] Failed to start engine: %@",
|
||||
startError.localizedDescription);
|
||||
return NO;
|
||||
}
|
||||
|
||||
[self.playerNode play];
|
||||
self.playing = YES;
|
||||
|
||||
// 启动进度更新定时器
|
||||
[self startProgressTimer];
|
||||
|
||||
NSLog(@"[AudioStreamPlayer] Started");
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)stop {
|
||||
dispatch_async(self.playerQueue, ^{
|
||||
[self stopProgressTimer];
|
||||
|
||||
[self.playerNode stop];
|
||||
[self.audioEngine stop];
|
||||
|
||||
self.playing = NO;
|
||||
self.currentSegmentId = nil;
|
||||
self.scheduledSamples = 0;
|
||||
self.playedSamples = 0;
|
||||
|
||||
[self.segmentDurations removeAllObjects];
|
||||
[self.segmentStartTimes removeAllObjects];
|
||||
|
||||
NSLog(@"[AudioStreamPlayer] Stopped");
|
||||
});
|
||||
}
|
||||
|
||||
- (void)enqueuePCMChunk:(NSData *)pcmData
|
||||
sampleRate:(double)sampleRate
|
||||
channels:(int)channels
|
||||
segmentId:(NSString *)segmentId {
|
||||
|
||||
if (!pcmData || pcmData.length == 0)
|
||||
return;
|
||||
|
||||
dispatch_async(self.playerQueue, ^{
|
||||
// 检查是否是新片段
|
||||
BOOL isNewSegment = ![segmentId isEqualToString:self.currentSegmentId];
|
||||
if (isNewSegment) {
|
||||
self.currentSegmentId = segmentId;
|
||||
self.scheduledSamples = 0;
|
||||
self.segmentStartTimes[segmentId] = @(CACurrentMediaTime());
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if ([self.delegate respondsToSelector:@selector
|
||||
(audioStreamPlayerDidStartSegment:)]) {
|
||||
[self.delegate audioStreamPlayerDidStartSegment:segmentId];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 转换 Int16 -> Float32
|
||||
NSUInteger sampleCount = pcmData.length / sizeof(int16_t);
|
||||
const int16_t *int16Samples = (const int16_t *)pcmData.bytes;
|
||||
|
||||
// 创建播放格式的 buffer
|
||||
AVAudioFormat *format =
|
||||
[[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
|
||||
sampleRate:sampleRate
|
||||
channels:channels
|
||||
interleaved:NO];
|
||||
|
||||
AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc]
|
||||
initWithPCMFormat:format
|
||||
frameCapacity:(AVAudioFrameCount)sampleCount];
|
||||
buffer.frameLength = (AVAudioFrameCount)sampleCount;
|
||||
|
||||
float *floatChannel = buffer.floatChannelData[0];
|
||||
for (NSUInteger i = 0; i < sampleCount; i++) {
|
||||
floatChannel[i] = (float)int16Samples[i] / 32768.0f;
|
||||
}
|
||||
|
||||
// 调度播放
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.playerNode scheduleBuffer:buffer
|
||||
completionHandler:^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf)
|
||||
return;
|
||||
|
||||
dispatch_async(strongSelf.playerQueue, ^{
|
||||
strongSelf.playedSamples += sampleCount;
|
||||
});
|
||||
}];
|
||||
|
||||
self.scheduledSamples += sampleCount;
|
||||
|
||||
// 更新时长
|
||||
NSTimeInterval chunkDuration = (double)sampleCount / sampleRate;
|
||||
NSNumber *currentDuration = self.segmentDurations[segmentId];
|
||||
self.segmentDurations[segmentId] =
|
||||
@(currentDuration.doubleValue + chunkDuration);
|
||||
});
|
||||
}
|
||||
|
||||
- (NSTimeInterval)playbackTimeForSegment:(NSString *)segmentId {
|
||||
if (![segmentId isEqualToString:self.currentSegmentId]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 基于已播放的采样数估算时间
|
||||
return (double)self.playedSamples / self.playbackFormat.sampleRate;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)durationForSegment:(NSString *)segmentId {
|
||||
NSNumber *duration = self.segmentDurations[segmentId];
|
||||
return duration ? duration.doubleValue : 0;
|
||||
}
|
||||
|
||||
#pragma mark - Progress Timer
|
||||
|
||||
- (void)startProgressTimer {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.progressTimer =
|
||||
[NSTimer scheduledTimerWithTimeInterval:1.0 / 30.0
|
||||
target:self
|
||||
selector:@selector(updateProgress)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)stopProgressTimer {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.progressTimer invalidate];
|
||||
self.progressTimer = nil;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)updateProgress {
|
||||
if (!self.playing || !self.currentSegmentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSTimeInterval currentTime =
|
||||
[self playbackTimeForSegment:self.currentSegmentId];
|
||||
NSString *segmentId = self.currentSegmentId;
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector
|
||||
(audioStreamPlayerDidUpdateTime:segmentId:)]) {
|
||||
[self.delegate audioStreamPlayerDidUpdateTime:currentTime
|
||||
segmentId:segmentId];
|
||||
}
|
||||
|
||||
// 检查是否播放完成
|
||||
NSTimeInterval duration = [self durationForSegment:segmentId];
|
||||
if (duration > 0 && currentTime >= duration - 0.1) {
|
||||
// 播放完成
|
||||
dispatch_async(self.playerQueue, ^{
|
||||
if ([self.delegate respondsToSelector:@selector
|
||||
(audioStreamPlayerDidFinishSegment:)]) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.delegate audioStreamPlayerDidFinishSegment:segmentId];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user