IOS微软语音转文本,lame压缩音频

在IOS开发中,用微软进行语音转文本操作,并将录音文件压缩后返回

项目中遇到了利用微软SDK进行实时录音转文本操作,如果操作失败,那么就利用原始音频文件通过网络请求操作,最终这份文件上传到阿里云保存,考虑到传输速率,对文件压缩成mp3再上传

遇到的难点

  • 微软的示例中只能转文本,微软并不保存这份音频文件,需要自己实现从录音到推流,到获取结果
  • 项目是uniapp项目,非原生工程项目,录音管理器需要激活后才能使用
  • 关于压缩代码,采用Lame库压缩,网上大部分都是通过文件提取压缩再保存,直接录制音频压缩较少,记录下来以便后续使用

流程图

请添加图片描述

实现步骤

录音的实现

// 每个缓冲区的大小
#define kBufferSize 2048
// 缓冲区数量
#define kNumberBuffers 3// 定义结构体,里面保存录音队列ID,录音格式
typedef struct {AudioStreamBasicDescription dataFormat;AudioQueueRef               queue;AudioQueueBufferRef         buffers[kNumberBuffers];UInt32                      bufferByteSize;__unsafe_unretained id      selfRef;
} AQRecorderState;AQRecorderState recorderState = {0};- (instancetype)init {self = [super init];if (self) {// 设置音频格式recorderState.dataFormat.mFormatID = kAudioFormatLinearPCM;recorderState.dataFormat.mSampleRate = 16000.0;recorderState.dataFormat.mChannelsPerFrame = 1;recorderState.dataFormat.mBitsPerChannel = 16;recorderState.dataFormat.mBytesPerPacket = recorderState.dataFormat.mBytesPerFrame = recorderState.dataFormat.mChannelsPerFrame * sizeof(SInt16);recorderState.dataFormat.mFramesPerPacket = 1;recorderState.dataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;}return self;
}- (void)configureAudioSession {AVAudioSession *session = [AVAudioSession sharedInstance];NSError *error = nil;// 设置音频会话类别和模式[session setCategory:AVAudioSessionCategoryPlayAndRecord  error:&error];if (error) {NSLog(@"Error setting category: %@", error.localizedDescription);}// 激活音频会话[session setActive:YES error:&error];if (error) {NSLog(@"Error activating session: %@", error.localizedDescription);}
}
// 开始录音
- (void)startRecording{// 激活录音文件[self configureAudioSession];// 创建录音队列AudioQueueNewInput(&recorderState.dataFormat, HandleInputBuffer, &recorderState, NULL, kCFRunLoopCommonModes, 0, &recorderState.queue);// 设置录音增益AudioQueueSetParameter(recorderState.queue, kAudioQueueParam_Volume, 1.0);// 计算缓冲区大小DeriveBufferSize(recorderState.queue, &recorderState.dataFormat, 0.5, &recorderState.bufferByteSize);// 分配和分配缓冲区for (int i = 0; i < kNumberBuffers; i++) {AudioQueueAllocateBuffer(recorderState.queue, recorderState.bufferByteSize, &recorderState.buffers[i]);AudioQueueEnqueueBuffer(recorderState.queue, recorderState.buffers[i], 0, NULL);}OSStatus status = AudioQueueStart(recorderState.queue, NULL);if (status != noErr) {NSLog(@"AudioQueueNewInput failed with error: %d", (int)status);}
}
// 结束录音
- (void)stopRecording{// 停止录音AudioQueueStop(recorderState.queue, true);AudioQueueDispose(recorderState.queue, true);
};
// 计算缓冲区大小
void DeriveBufferSize(AudioQueueRef audioQueue, AudioStreamBasicDescription *ASBDesc, Float64 seconds, UInt32 *outBufferSize) {static const int maxBufferSize = 0x50000; // 限制缓冲区的最大值int maxPacketSize = ASBDesc->mBytesPerPacket;if (maxPacketSize == 0) {UInt32 maxVBRPacketSize = sizeof(maxPacketSize);AudioQueueGetProperty(audioQueue, kAudioQueueProperty_MaximumOutputPacketSize, &maxPacketSize, &maxVBRPacketSize);}Float64 numBytesForTime = ASBDesc->mSampleRate * maxPacketSize * seconds;*outBufferSize = (UInt32)(numBytesForTime < maxBufferSize ? numBytesForTime : maxBufferSize);
}
// 数据处理回调函数 这个里面有个录音回掉的PCM数据
void HandleInputBuffer(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, const AudioTimeStamp *inStartTime, UInt32 inNumPackets, const AudioStreamPacketDescription *inPacketDesc) {AQRecorderState *pAqData = (AQRecorderState *)aqData;// 如果有数据,就处理if (inNumPackets > 0) {// 创建NSData对象NSData *audioData = [NSData dataWithBytes:inBuffer->mAudioData length:inBuffer->mAudioDataByteSize];// 打印NSData对象内容NSLog(@"Audio Data: %@", audioData);// 这儿将进行保存文件// 编码文件// 推送数据到微软SDK}// 将缓冲区重新加入到队列中AudioQueueEnqueueBuffer(pAqData->queue, inBuffer, 0, NULL);
}

微软SDK初始化和推流

这个大部分和微软示例差不多,需要注意的是获取微软示例时候传入的是自定义的录音设置,并将自定义录音设置保存起来,在录音回掉中将数据推入流中

- (void)setUpKey:(NSString *)token service:(NSString *)service lang:(NSString *) lang { // 这里将通过token和区域初始化配置类,微软还有其他获取配置类的方法,其他方法示例化也可以SPXSpeechConfiguration *speechConfig = nil;speechConfig = [[SPXSpeechConfiguration alloc] initWithAuthorizationToken:token region:service];// 这个是通过token实例化配置类的方式
//    speechConfig = [[SPXSpeechConfiguration alloc] initWithSubscription:token region:service];// 设置语言 en-US格式[speechConfig setSpeechRecognitionLanguage:lang];// 设置微软接收到的数据的格式 16000HZ 16位深 单通道SPXAudioStreamFormat *audioFormat = [[SPXAudioStreamFormat alloc] initUsingPCMWithSampleRate: 16000 bitsPerSample:16 channels:1];// 获取推流的类,并保存起来,后面就通过它推送数据到SDKself.audioInputStream = [[SPXPushAudioInputStream alloc] initWithAudioFormat:audioFormat];// 获取录音配置SPXAudioConfiguration* audioConfig = [[SPXAudioConfiguration alloc] initWithStreamInput:self.audioInputStream];// 通过配置类和录音类信息获取微软识别器self.recognizer = [[SPXSpeechRecognizer alloc] initWithSpeechConfiguration:speechConfig audioConfiguration:audioConfig];// 定义已识别事件的处理函数[self.recognizer addRecognizedEventHandler:^(SPXSpeechRecognizer *recognizer, SPXSpeechRecognitionEventArgs *eventArgs) {NSString *recognizedText = eventArgs.result.text;NSLog(@"Final recognized text: %@", recognizedText);// 在这里处理最终识别结果[self.speechToTextResult appendFormat:recognizedText];}];// 定义识别中事件的处理函数[self.recognizer addRecognizingEventHandler:^(SPXSpeechRecognizer *recognizer, SPXSpeechRecognitionEventArgs *eventArgs) {NSString *intermediateText = eventArgs.result.text;NSLog(@"Intermediate recognized text: %@", intermediateText);// 在这里处理中间识别结果}];// 定义取消事件的处理函数[self.recognizer addCanceledEventHandler:^(SPXSpeechRecognizer *recognizer, SPXSpeechRecognitionCanceledEventArgs *eventArgs) {NSLog(@"Recognition canceled. Reason: %ld", (long)eventArgs.reason);if (eventArgs.errorDetails != nil) {NSLog(@"Error details: %@", eventArgs.errorDetails);}}];}
- (void)startRecording{ [self.recognizer startContinuousRecognition];
}
- (void)stopRecording{ [self.recognizer stopContinuousRecognition];
}
// 
// 数据处理回调函数
void HandleInputBuffer(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, const AudioTimeStamp *inStartTime, UInt32 inNumPackets, const AudioStreamPacketDescription *inPacketDesc) {// 在回掉函数中,将数据传递给微软,上面的回掉函数中就能拿到数据了[self.audioInputStream write:audioData];
}

保存文件

  • 保存文件相对简单,录音开始清除上一次的音频文件,创建新的音频文件
  • WAV文件需要添加头文件,才能正常播放

#import "SaveAudioFile.h"#define isValidString(string)               (string && [string isEqualToString:@""] == NO)
// WAV 文件头结构
typedef struct {char riff[4];UInt32 fileSize;char wave[4];char fmt[4];UInt32 fmtSize;UInt16 formatTag;UInt16 channels;UInt32 samplesPerSec;UInt32 avgBytesPerSec;UInt16 blockAlign;UInt16 bitsPerSample;char data[4];UInt32 dataSize;
} WAVHeader;@implementation SaveAudioFile
/*** 清理文件*/
- (void)cleanFile {if (isValidString(self.mp3Path)) {NSFileManager *fileManager = [NSFileManager defaultManager];BOOL isDir = FALSE;BOOL isDirExist = [fileManager fileExistsAtPath:self.mp3Path isDirectory:&isDir];if (isDirExist) {[fileManager removeItemAtPath:self.mp3Path error:nil];NSLog(@"  xxx.mp3  file   already delete");}}if (isValidString(self.wavPath)) {NSFileManager *fileManager = [NSFileManager defaultManager];BOOL isDir = FALSE;BOOL isDirExist = [fileManager fileExistsAtPath:self.wavPath isDirectory:&isDir];if (isDirExist) {[fileManager removeItemAtPath:self.wavPath error:nil];NSLog(@"  xxx.caf  file   already delete");}}
}
/***  取得录音文件保存路径**  @return 录音文件路径*/
-(NSURL *)getSavePath{//  在Documents目录下创建一个名为FileData的文件夹NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject] stringByAppendingPathComponent:@"AudioData"];NSLog(@"%@",path);NSFileManager *fileManager = [NSFileManager defaultManager];BOOL isDir = FALSE;BOOL isDirExist = [fileManager fileExistsAtPath:path isDirectory:&isDir];if(!(isDirExist && isDir)){BOOL bCreateDir = [fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];if(!bCreateDir){NSLog(@"创建文件夹失败!");}NSLog(@"创建文件夹成功,文件路径%@",path);}NSString *fileName = @"record";NSString *wavFileName = [NSString stringWithFormat:@"%@.wav", fileName];NSString *mp3FileName = [NSString stringWithFormat:@"%@.mp3", fileName];NSString *wavPath = [path stringByAppendingPathComponent:wavFileName];NSString *mp3Path = [path stringByAppendingPathComponent:mp3FileName];self.wavPath = wavPath;self.mp3Path = mp3Path;NSLog(@"file path:%@",mp3Path);NSURL *url=[NSURL fileURLWithPath:mp3Path];return url;
}-(void) startWritingHeaders {[self cleanFile];[self getSavePath];// 写入 WAV 头部WAVHeader header;memcpy(header.riff, "RIFF", 4);header.fileSize = 0;  // 将在录音结束时填充memcpy(header.wave, "WAVE", 4);memcpy(header.fmt, "fmt ", 4);header.fmtSize = 16;header.formatTag = 1;  // PCMheader.channels = 1;header.samplesPerSec = 16000;header.avgBytesPerSec = 16000 * 2;header.blockAlign = 2;header.bitsPerSample = 16;memcpy(header.data, "data", 4);header.dataSize = 0;  // 将在录音结束时填充// 创建 WAV 文件API[[NSFileManager defaultManager] createFileAtPath:self.wavPath contents:nil attributes:nil];self.audioFileHandle = [NSFileHandle fileHandleForWritingAtPath:self.wavPath];[self.audioFileHandle writeData:[NSData dataWithBytes:&header length:sizeof(header)]];// 创建 mp3 文件API[[NSFileManager defaultManager] createFileAtPath:self.mp3Path contents:nil attributes:nil];self.audioFileHandle2 = [NSFileHandle fileHandleForWritingAtPath:self.mp3Path];
}
- (void) saveAudioFile: (NSData *) data type:(NSString *) type{if([type isEqualToString:@"wav"]){// 写入音频数据到 WAV 文件[self.audioFileHandle writeData:data];}else{// 拿到编码过后的数据,保存到本地[self.audioFileHandle2 writeData:data];}
}
@end

利用Lame库编码PCM数据

  • 下载Lame库并导入项目中操作,参考网上文章https://www.cnblogs.com/XYQ-208910/p/7650759.html
  • lame库的使用主要分成3部分
    • 初始化Lame 并设置比特率,位深,通道数,压缩程度
    • 传入原始的音频数据,得到编码过后的mp3音频数据
    • 结束时刷新lame中还剩的数据,关闭Lame
//
//  LameEncoderMp3.m
//  SpeechUntil
//
//  Created by 肖鹏程 on 2024/7/25.
//#import "LameEncoderMp3.h"@implementation LameEncoderMp3- (void) settingFormat:(int)sampleRate channels:(int)channels{// 初始化lame编码器 设置格式self.lame = lame_init();lame_set_in_samplerate(self.lame, sampleRate);lame_set_num_channels(self.lame, channels);lame_set_brate(self.lame, 16); // 比特率128 kbpslame_set_mode(self.lame, channels == 1 ? MONO : STEREO);lame_set_quality(self.lame, 7); // 0 = 最高质量(最慢),9 = 最低质量(最快)lame_init_params(self.lame);self.channels = channels;};
- (NSData *)encodePCMToMP3:(NSData *)pcmData{// PCM数据的指针和长度const short *pcmBuffer = (const short *)[pcmData bytes];int pcmLength = (int)[pcmData length] / sizeof(short);NSLog(@"pcmLength %lu", [pcmData length]);// 分配MP3缓冲区int mp3BufferSize = (int)(1.25 * pcmLength) + 7200;unsigned char *mp3Buffer = (unsigned char *)malloc(mp3BufferSize);// 确保mp3Buffer分配成功if (mp3Buffer == NULL) {NSLog(@"Failed to allocate memory for MP3 buffer");return nil;}// PCM编码为MP3// 注意这个是单通道的方法,如果是双通道调用这个lame_encode_buffer_interleaved(//   lame,//   recordingData,//   numSamples / 2,  // 双声道//   mp3Buffer,//   mp3BufferSize);int mp3Length = lame_encode_buffer(self.lame, (short *)pcmBuffer, (short *)pcmBuffer,pcmLength, mp3Buffer, mp3BufferSize);if (mp3Length < 0) {NSLog(@"LAME encoding error: %d", mp3Length);free(mp3Buffer);return nil;}// 创建MP3数据NSData *mp3Data = [NSData dataWithBytes:mp3Buffer length:mp3Length];NSLog(@"mp3Length %lu", [mp3Data length]);// 清理free(mp3Buffer);return mp3Data;
}- (NSData *) closeLame{// 刷新LAME缓冲区unsigned char mp3Buffer[7200];int flushLength = lame_encode_flush(self.lame, mp3Buffer, sizeof(mp3Buffer));NSData *flushData;if (flushLength > 0) {// 将刷新后的数据追加到已有的MP3数据flushData = [NSData dataWithBytes:mp3Buffer length:flushLength];} else if (flushLength < 0) {NSLog(@"LAME flushing error: %d", flushLength);}// 关闭lame_close(self.lame);return flushData;
}@end

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/3266577.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

Find My电子书|苹果Find My技术与电子书结合,智能防丢,全球定位

电子书是指将文字、图片、声音、影像等讯息内容数字化的出版物和植入或下载数字化文字、图片、声音、影像等讯息内容的集存储和显示终端于一体的手持阅读器。代表人们所阅读的数字化出版物&#xff0c;区别于以纸张为载体的传统出版物。电子书通过数码方式记录在以光、电、磁为…

PostgreSQL性能优化之体系结构

本文介绍 PostgreSQL 数据库的体系结构&#xff0c;包括实例结构&#xff08;进程与内存&#xff09;、存储结构&#xff08;物理与逻辑&#xff09;以及插件式存储引擎。 实例与数据库聚簇 PostgreSQL 使用典型的客户端/服务器&#xff08;Client/Server&#xff09;架构&am…

基于Intel x86+FPGA+AI工业整机,支持医疗CT扫描仪应用

众所周知&#xff0c;医学影像设备是医疗器械行业中最具发展潜力的细分领域之一&#xff0c;诸如CT扫描仪&#xff08;简称&#xff1a;CT&#xff09;便是医生用于多种疾病临床检查和诊治时的理想选择。 医疗CT扫描仪的发展潜力及难点 CT&#xff0c;即电子计算机X线断层扫描&…

计算机网络:构建联结的基础

目录 1. 网络拓扑结构 1.1 星型拓扑 1.2 环型拓扑 1.3 总线型拓扑 1.4 网状拓扑 2. 传输介质 2.1 双绞线 2.2 同轴电缆 2.3 光纤 2.4 无线电波 3. 协议栈模型 3.1 OSI模型 3.2 TCP/IP模型 4. 网络设备 4.1 交换机 4.2 路由器 4.3 网关 4.4 防火墙 5. IP地址…

2024年数字化社会、信息科学与风险管理研讨会(ICDIR 2024,9月20-22)

2024年数字化社会、信息科学与风险管理研讨会&#xff08;ICDIR 2024&#xff09;作为2024年人工智能与数字管理国际学术会议&#xff08;ICAIDM 2024&#xff09;的分论坛&#xff0c;将携手主会齐于2024年9月22-24日在中国江苏省南京市隆重召开。 会议旨在为从事数字化社会、…

DjangoRF实战-2-apps-users

1、用户模块 创建一个用户模块子应用&#xff0c;用来管理用户&#xff0c;和认证和授权。 1.1根目录创建apps&#xff0c; 为了使用方便&#xff0c;还需要再pycharm中设置一下资源路径&#xff0c;就可以自动提示 1.2注册子应用 1.3添加应用根目录到环境变量path python导…

Java---后端文件上传详解

袁门才俊志高远&#xff0c; 震古烁今意决然。 风采翩翩才情显&#xff0c; 雄姿英发立世间。 目录 一&#xff0c;简单案例演示 二&#xff0c;服务器本地存储 三&#xff0c;配置单个文件上传大小限制 一&#xff0c;简单案例演示 首先简单编写一个前端网页&#xff1a; &l…

vue3+openLayers点击标记事件

<template><!--地图--><div class"distributeMap" id"distributeMap"></div> </template> <script lang"ts" setup> import { onMounted, reactive } from "vue"; import { Feature, Map, View }…

C++中的依赖注入

目录 1.概述 2.构造函数注入 3.setter方法注入 4.接口注入 5.依赖注入框架 6.依赖注入容器 7.依赖注入框架的工作原理 8.依赖注入的优势 9.总结 1.概述 依赖注入是一种设计模式&#xff0c;它允许我们在不直接创建对象的情况下为对象提供其依赖项&#xff1b;它通过将…

模拟信号介绍

定义&#xff1a; 模拟信号是指用连续变化的物理量表示的信息&#xff0c;其信号的幅度、频率或相位随时间作连续变化&#xff0c;或在一段连续的时间间隔内&#xff0c;其代表信息的特征量可以在任意瞬间呈现为任意数值的信号。我们通常又把模拟信号称为连续信号&#xff0c;它…

Pytorch使用教学6-张量的分割与合并

在使用PyTorch时&#xff0c;对张量的分割与合并是不可避免的操作&#xff0c;本节就带大家深刻理解张量的分割与合并。 在开始之前&#xff0c;我们先对张量的维度进行深入理解&#xff1a; t2 torch.zeros((3, 4)) # tensor([[0., 0., 0., 0.], # [0., 0., 0., 0.…

java发送https请求支持tls1.3

说明&#xff1a;java 8_u201及以下版本不支持tls1.3协议发送。最直接的方法是升级到该版本之上。 另外&#xff1a;需要修改一下代码强行使用tls1.3协议。如果只修改代码&#xff0c;不升级java版本会报错找不到该协议。

【基于PSINS】UKF/SSUKF对比的MATLAB程序

UKF与SSUKF UKF是&#xff1a;无迹卡尔滤波 SSUKF是&#xff1a;简化超球面无迹卡尔曼滤波 UKF 相较于传统的KF算法&#xff0c;UKF能够更好地处理非线性系统&#xff0c;并且具有更高的估计精度。它适用于多种应用场景&#xff0c;如机器人定位导航、目标跟踪、信号处理等。…

【解决方案】华普微基于CMT2189D的低功耗广域网解决方案

一、方案概述 随着物联网的快速发展&#xff0c;对于无线通信的需求越来越高。传统的通信技术可能无法满足物联网设备的特殊要求&#xff0c;如低功耗、长距离覆盖和大规模连接。LPWAN技术应运而生&#xff0c;旨在为物联网设备提供低成本、低功耗的远距离通信解决方案。ZETA作…

【Qt】Qt容器和STL容器的区别

1、简述 Qt容器和STL容器略有不同,作为一个Qter,应该知道它们之间的异同。 Qt容器官网介绍:https://doc.qt.io/qt-5/containers.html STL容器官网介绍:https://zh.cppreference.com/w/cpp/container 2、Qt容器和STL容器的对应关系 注意:QList 与 std::list 无关,QSet …

<数据集>铁路工人安全帽安全背心识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;3065张 标注数量(xml文件个数)&#xff1a;3065 标注数量(txt文件个数)&#xff1a;3065 标注类别数&#xff1a;3 标注类别名称&#xff1a;[vest, helmet, worker] 序号类别名称图片数框数1vest305978832helmet…

谷粒商城实战笔记-59-商品服务-API-品牌管理-使用逆向工程的前后端代码

文章目录 一&#xff0c; 使用逆向工程生成的代码二&#xff0c;生成品牌管理菜单三&#xff0c;几个小问题 在本次的技术实践中&#xff0c;我们利用逆向工程的方法成功地为后台管理系统增加了品牌管理功能。这种开发方式不仅能快速地构建起功能模块&#xff0c;还能在一定程度…

Go语言常见序列化协议全面对比

先说结论 从易用性、性能、内存占用、编码后大小等几个方面综合考虑 ProtoBuf 胜出。 Gob 从性能和 I/O 带宽占用上都和 ProtoBuf 差不多&#xff0c;唯一劣势是编解码时内存占用较多。考虑到不用再写 IDL 带来的易用性&#xff0c;如果整个系统内不存在使用除 Go 以外其他语言…

使用 Snorkel 和 MinIO 的以数据为中心的 AI

如今&#xff0c;业界都在谈论大型语言模型及其编码器、解码器、多头注意力层和数十亿&#xff08;即将数万亿&#xff09;的参数&#xff0c;人们很容易相信好的人工智能只是模型设计的结果。不幸的是&#xff0c;事实并非如此。好的人工智能需要的不仅仅是一个精心设计的模型…

医疗器械ce认证办理流程介绍

CE认证是的产品安全认证&#xff0c;所有进入市场的医疗器械都必须进行医疗器械CE认证&#xff0c;医疗器械CE认证为了解决成员国之间的贸易壁垒&#xff0c;组织逐步将自己建成一个统一的大市场&#xff0c;以确保人员、服务、资本和产品(如医疗器械)的自由流通。在医疗器械领…