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://www.mzph.cn/web/48924.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MongoDB教程(十五):MongoDB原子操作

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; 文章目录 引言一、MongoD…

11 深度推荐模型演化中的“范式替换“灵活组合

上一课时&#xff0c;我们介绍了 DIEN 模型添加了 RNN中 的 GRU&#xff0c;使模型获得了对序列数据的建模能力&#xff1b;而 DSIN 模型不仅使用了 RNN 中的 bi-LSTM&#xff0c;还使用了 Transformer 组件。 由此可见&#xff0c;在新的演化模型中&#xff0c;根据场景和数据…

Lua脚本简单理解

目录 1.安装 2.语法 2.1Lua数据类型 2.2变量 2.3lua循环 2.4流程控制 2.5函数 2.6运算符 2.7关系运算符 3.lua脚本在redis中的使用 3.1lua脚本再redis简单编写 3.2普通锁Lua脚本 3.3可重入锁lua脚本 1.安装 centos安装 安装指令&#xff1a; yum -y update yum i…

本地部署VMware ESXi服务实现无公网IP远程访问管理服务器

文章目录 前言1. 下载安装ESXi2. 安装Cpolar工具3. 配置ESXi公网地址4. 远程访问ESXi5. 固定ESXi公网地址 前言 在虚拟化技术日益成熟的今天&#xff0c;VMware ESXi以其卓越的性能和稳定性&#xff0c;成为了众多企业构建虚拟化环境的首选。然而&#xff0c;随着远程办公和跨…

CCS光源的高输出TH2系列平面光源

光源在机器视觉系统中起着重要作用&#xff0c;不同环境、场景及应用合适光源都不一样&#xff0c;今天我们来看看高输出TH2系列平面光源。它可以对应高速化的生产线&#xff0c;为提高生产效率做出贡献。 TH2系列光源的特点&#xff1a; 1、实现了更高一级的高亮度 实现了更…

谷粒商城实战笔记-56~57-商品服务-API-三级分类-修改-拖拽功能完成

文章目录 一&#xff0c;56-商品服务-API-三级分类-修改-拖拽功能完成二&#xff0c;57-商品服务-API-三级分类-修改-批量拖拽效果1&#xff0c;增加按钮2&#xff0c;多次拖拽一次保存完整代码 在构建商品服务API中的三级分类修改功能时&#xff0c;拖拽排序是一个直观且高效的…

Java | Leetcode Java题解之第260题只出现一次的数字III

题目&#xff1a; 题解&#xff1a; class Solution {public int[] singleNumber(int[] nums) {int xorsum 0;for (int num : nums) {xorsum ^ num;}// 防止溢出int lsb (xorsum Integer.MIN_VALUE ? xorsum : xorsum & (-xorsum));int type1 0, type2 0;for (int n…

Prometheus配置alertmanager告警

1、拉取镜像并运行 1、配置docker镜像源 [rootlocalhost ~]# vim /etc/docker/daemon.json {"registry-mirrors": ["https://dfaad.mirror.aliyuncs.com"] } [rootlocalhost ~]# systemctl daemon-reload [rootlocalhost ~]# systemctl restart docker2、…

刷题了: 151.翻转字符串里的单词 |卡码网:55.右旋转字符串

151.翻转字符串里的单词 题目链接:https://leetcode.cn/problems/reverse-words-in-a-string/description/ 文章讲解:https://programmercarl.com/0151.%E7%BF%BB%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2%E9%87%8C%E7%9A%84%E5%8D%95%E8%AF%8D.html 视频讲解:https://www.bilibi…

vue2之jessibuca视频插件使用教程

vue2之jessibuca视频插件使用教程 jessibuca简介前期准备下载相关jsvue index.html文件引入 组件封装使用小知识 引入iconfont jessibuca简介 Jessibuca是一款开源的纯H5直播流播放器&#xff0c;通过Emscripten将音视频解码库编译成Js&#xff08;ams.js/wasm)运行于浏览器之中…

基于PyCharm在Windows系统上远程连接Linux服务器中Docker容器进行Python项目开发与部署

文章目录 摘要项目结构项目开发项目上线参考文章 摘要 本文介绍了如何在Windows 10系统上使用PyCharm专业版2024.1&#xff0c;通过Docker容器在阿里云CentOS 7.9服务器上进行Python项目的开发和生产部署。文章详细阐述了项目结构的搭建、PyCharm的使用技巧、以及如何将开发项…

12.Spring事务和事务传播机制

文章目录 1.为什么需要事务2.Spring 中事务的实现2.1 MySQL 中的事务使⽤2.2 Spring 编程式事务2.3 Spring 声明式事务&#xff08;自动&#xff09;2.3.1 Transactional 作⽤范围2.3.2 Transactional 参数说明2.3.3 注意事项2.3.4 Transactional ⼯作原理 3.事务隔离级别3.1 事…

vue+element的table合并单元格(竖着合并行)及合计行添加并计算

1 效果: 代码分析: 1 表格头配置: 2 懒得写的:自己复制吧 <el-table:data"tableData"style"width: 98%":height"height"v-loading"isLoading"stripe"false" :span-method"objectSpanMethod"show-summary:summ…

视图、存储过程、触发器

一、视图 视图是从一个或者几个基本表&#xff08;或视图&#xff09;导出的表。它与基 本表不同&#xff0c;是一个虚表&#xff0c;视图只能用来从查询&#xff0c;不能做增删改(虚拟的表) 1.创建视图 创建视图的语法&#xff1a; create view 视图名【view_xxx / v_xxx】 a…

深入理解MySQL锁机制与性能优化:详解记录锁、间隙锁、临键锁及慢SQL查询分析

1. 事务隔离和锁机制详解 记录锁 第一种情况,当我们对于唯一性的索引(包括唯一索引和主键索引)使用等值查询,精准匹配到一条记录的时候,这个时候使用的就是记录锁。 比如 where id = 1 4 7 10。 间隙锁 第二种情况,当我们查询的记录不存在,无论是用等值查询还是范围…

Thinkphp开发文档二次整理版

基础部分 安装 环境要求 ​ *php>7.1.0 命令下载 通过Composer进行下载&#xff0c;操作步骤下载软件 phpstudy --->点击软件管理 --->安装Composer --->再点击网站 --->点击管理 --->点击Composer --->复制如下命令代码&#xff1a; ​ 稳定版&…

国际化技术参考

一、概述 国际化就是用户可以选择对应的语言,页面展示成对应的语言; 一个系统的国际化按照信息的所在位置,可以分为三种国际化信息: 前端页面信息后端提示信息数据库的字典类信息二、前端页面国际化 使用i18n库实现国际化 i18n国际化库思路:通过jquery或者dom操作拿到需…

推荐4款简单高效的视频转文字工具。

最近我要将很多的以前的培训视频转换成笔记&#xff0c;觉得很麻烦&#xff0c;于是就搜索有没有什么工具可以帮助。结果就真的找到了很多将视频转换成文字的软件和网站。解决了一个大工程&#xff0c;后来发现其实很多人都会碰到像我这样的问题&#xff0c;于是在这里将我使用…

类和对象:完结

1.再深构造函数 • 之前我们实现构造函数时&#xff0c;初始化成员变量主要使⽤函数体内赋值&#xff0c;构造函数初始化还有⼀种⽅ 式&#xff0c;就是初始化列表&#xff0c;初始化列表的使⽤⽅式是以⼀个冒号开始&#xff0c;接着是⼀个以逗号分隔的数据成 员列表&#xf…

通信原理-思科实验三:无线局域网实验

实验三 无线局域网实验 一&#xff1a;无线局域网基础服务集 实验步骤&#xff1a; 进入物理工作区&#xff0c;导航选择 城市家园; 选择设备 AP0&#xff0c;并分别选择Laptop0、Laptop1放在APO范围外区域 修改笔记本的网卡&#xff0c;从以太网卡切换到无线网卡WPC300N 切…