Files
keyboard/keyBoard/Class/AiTalk/VM/AudioStreamPlayer.m

247 lines
7.2 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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