247 lines
7.2 KiB
Objective-C
247 lines
7.2 KiB
Objective-C
//
|
||
// 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
|