我司和我同項目的Android小夥伴分享了技術文章, 咱大iOS也不能夠落後, 整理了一下關於音頻處理的一些內容, 但願對你們有所幫助.php
Talk is cheap, show you the code WaveDemohtml
##轉碼前奏 我所在項目, 須要將音頻上傳至服務器, iOS原生的錄音產生的PCM文件過大, 爲了統一三端, 咱們決定使用mp3
格式. iOS錄音的輸出參數默認不支持mp3(那個字段沒用...), 因此咱們須要使用lame進行轉碼. 網上能夠找到的lame.a
可能是iOS6, 7, 而且有的不支持bitcode, 容我作個悲傷的表情🤣 我在github上找到了一個編譯lame源碼的庫build-lame-for-iOS, 支持bitcode, 而且可修改最低支持版本, 自行修改sh文件node
tip: lipo
操做可操做libXXX.a文件, 增刪平臺依賴.ios
##轉碼進行時 搞完lame的問題, 咱們開始進行編碼, 最開始我使用Google大法, 找到了使用lame的方式, 核心代碼以下:git
@try {
int read, write;
FILE *pcm = fopen([cafFilePath cStringUsingEncoding:1], "rb"); //source 被轉換的音頻文件位置
fseek(pcm, 4*1024, SEEK_CUR); //skip file header
FILE *mp3 = fopen([mp3FilePath cStringUsingEncoding:1], "wb"); //output 輸出生成的Mp3文件位置
const int PCM_SIZE = 8192;
const int MP3_SIZE = 8192;
short int pcm_buffer[PCM_SIZE*2];
unsigned char mp3_buffer[MP3_SIZE];
lame_t lame = lame_init();
lame_set_in_samplerate(lame, 22050.0);
lame_set_VBR(lame, vbr_default);
lame_init_params(lame);
do {
read = fread(pcm_buffer, 2*sizeof(short int), PCM_SIZE, pcm);
if (read == 0)
write = lame_encode_flush(lame, mp3_buffer, MP3_SIZE);
else
write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE);
fwrite(mp3_buffer, write, 1, mp3);
} while (read != 0);
lame_close(lame);
fclose(mp3);
fclose(pcm);
}
@catch (NSException *exception) {
NSLog(@"%@",[exception description]);
}
@finally {
return mp3FilePath;
}
複製代碼
咱們完成了第一步, 錄製完成後將PCM轉爲mp3
讓我先檢驗一下音頻能否播放等問題, 而後問題就來了 用mac自帶的iTunes播放, 獲取的總時長不正確.不用問, 確定是轉碼出了問題, 查找了一些資料得知, lame_set_VBR
的參數vbr_default
是變碼率vbr
形式的, 默認的播放器AVPlayer是使用cbr
均碼率的形式識別播放, 致使時長不正確, 因此這裏調整上面的一行代碼:github
lame_set_VBR(lame, vbr_off);
複製代碼
注意模擬器和真機的採樣率有些許不一樣, 如遇到播放雜音的情況可調整爲objective-c
lame_set_in_samplerate(lame, 44100);
複製代碼
##優化轉碼 其實就是邊錄邊轉了, 這裏我查看了一些文章, 大多基於AVAudioRecorder實現的方式比較粗暴, 不想使用. 我還查到了基於AVAudioQueue的, 不過api多C語言. 想起來前一陣看的Apple的session中有關於AVAudioEngine的介紹, 使用起來更加oc, 本着折騰就是學習的心, 使用AVAudioEngine進行轉碼. 這裏我就很少作介紹了,能夠查看文章iOS AVAudioEngine 使用AVAudioEngine能夠拿到時時的音頻buffer, 對其進行轉碼便可, 將轉碼後的data
進行append
(可本身改造, 使用AFNetworking進行流上傳). 核心代碼以下:api
- 轉碼準備工做,建立engine,並初始化lame
private func initLame() {
engine = AVAudioEngine()
guard let engine = engine,
let input = engine.inputNode else {
return
}
let format = input.inputFormat(forBus: 0)
let sampleRate = Int32(format.sampleRate) / 2
lame = lame_init()
lame_set_in_samplerate(lame, sampleRate);
lame_set_VBR_mean_bitrate_kbps(lame, 96);
lame_set_VBR(lame, vbr_off);
lame_init_params(lame);
}
複製代碼
設置AVAudioSession,設置偏好的
採樣率
和獲取buffer的io頻率
bash
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(AVAudioSessionCategoryPlayAndRecord)
try session.setPreferredSampleRate(44100)
try session.setPreferredIOBufferDuration(0.1)
try session.setActive(true)
initLame()
} catch {
print("seesion設置")
return
}
複製代碼
計算音量 使用
Accelerate
庫進行高效計算, 詳情查看 Level Metering with AVAudioEngine服務器
let levelLowpassTrig: Float = 0.5
var avgValue: Float32 = 0
vDSP_meamgv(buf, 1, &avgValue, vDSP_Length(frameLength))
this.averagePowerForChannel0 = (levelLowpassTrig * ((avgValue==0) ? -100 : 20.0 * log10f(avgValue))) + ((1-levelLowpassTrig) * this.averagePowerForChannel0)
let volume = min((this.averagePowerForChannel0 + Float(55))/55.0, 1.0)
this.minLevel = min(this.minLevel, volume)
this.maxLevel = max(this.maxLevel, volume)
// 切回去, 更新UI
DispatchQueue.main.async {
this.delegate?.record(this, voluem: volume)
}
複製代碼
結束操做
public func stop() {
engine?.inputNode?.removeTap(onBus: 0)
engine = nil
do {
var url = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let name = String(CACurrentMediaTime()).appending(".mp3")
url.appendPathComponent(name)
if !data.isEmpty {
try data.write(to: url)
}
else {
print("空文件")
}
} catch {
print("文件操做")
}
data.removeAll()
}
複製代碼
幾個須要注意的點
lame_set_bWriteVbrTag(_lame, 0);
複製代碼
在input的回調中, 咱們修改了 使用這樣的方法不夠優雅,因此經過設置session的ioduration來達到相似的目的。buffer
的frameLength
, 由於默認的input的回調頻率是0.375s, 咱們能夠經過修改frameLength
來達到修改頻率的目的.
錄製過程當中沒有辦法取得當前錄製時長, 請自行使用NSDate, NSTimer等方式進行計算.
在模擬器上一切ok, 在個人小6上, 錄製的音頻播放出來是快進的🤣,debug了很久發現, 在真機上, 若是連續播放錄製, 有時AVAudioEngine的input上的format拿到的採樣率sampleRate
不是預期的44100而是16000. 解決辦法有兩種
AVAudioSession *sessionInstance = [AVAudioSession sharedInstance];
[sessionInstance setPreferredSampleRate:kPreferredSampleRate error:&error]
複製代碼
##同步Android波形圖 由於重構以上部分的內容, 致使業務上拉後Android小夥伴, 他已經完成了波形圖的繪製, 能夠查看效果圖.
Android自繪動畫實現與一些優化思考——以智課批改App錄音波形動畫爲例 Android小夥伴詳細介紹瞭如何繪製該圖形, 在他的幫助下, 我在iOS端也實現了該效果.
基本流程
優化手段大致相同
核心代碼以下:
CGFloat reduction[kPointNumber];
CGFloat perVolume;
NSInteger count;
@property (nonatomic, assign) CGFloat targetVolue;
@property (nonatomic, copy) NSArray<NSNumber *> *amplitudes;
@property (nonatomic, copy) NSArray<CAShapeLayer *> *shapeLayers;
@property (nonatomic, copy) NSArray<UIBezierPath *> *paths;
- (void)doSomeInit {
perVolume = 0.15;
count = 0;
self.amplitudes = @[@0.6, @0.35, @0.1, @-0.1];
self.shapeLayers = [self.amplitudes bk_map:^id(id obj) {
CAShapeLayer *layer = [self creatLayer];
[self.layer addSublayer:layer];
return layer;
}];
self.shapeLayers.firstObject.lineWidth = 2;
self.paths = [self.amplitudes bk_map:^id(id obj) {
return [UIBezierPath bezierPath];
}];
for (int i = 0; i < kPointNumber; i++) {
reduction[i] = self.height / 2.0 * 4 / (4 + pow((i/(CGFloat)kPointNumber - 0.5) * 3, 4));
}
}
- (CAShapeLayer *)creatLayer {
CAShapeLayer *layer = [CAShapeLayer layer];
layer.fillColor = [UIColor clearColor].CGColor;
layer.strokeColor = [UIColor defaultColor].CGColor;
layer.lineWidth = 0.2;
return layer;
}
// 用來忽略變化較小的波動
- (void)setTargetVolue:(CGFloat)targetVolue {
if (ABS(_targetVolue - targetVolue) > perVolume) {
_targetVolue = targetVolue;
}
}
// 在每一個CADisplayLink週期中, 平滑調整音量.
- (void)softerChangeVolume {
CGFloat target = self.targetVolue;
if (volume < target - perVolume) {
volume += perVolume;
} else if (volume > target + perVolume) {
if (volume < perVolume * 2) {
volume = perVolume * 2;
} else {
volume -= perVolume;
}
} else {
volume = target;
}
}
- (void)updatePaths:(CADisplayLink *)sender {
// 座標軸取[-3,3], 屏幕取像素點64份
NSInteger xLen = 64;
count++;
[self softerChangeVolume];
for (int i = 0; i < xLen; i++) {
CGFloat left = i/(CGFloat)xLen * self.width;
CGFloat x = (i/(CGFloat)xLen - 0.5) * 3;
tmpY = volume * reduction[i] * sin(M_PI*x - count*0.2);
for (int j = 0; j < self.amplitudes.count ; j++) {
CGPoint point = CGPointMake(left, tmpY * [self.amplitudes[j] doubleValue] + self.height/2);
UIBezierPath *path = self.paths[j];
if (i == 0) {
[path moveToPoint:point];
} else {
[path addLineToPoint:point];
}
}
}
for (int i = 0; i < self.paths.count; i++) {
self.shapeLayers[i].path = self.paths[i].CGPath;
}
[self.paths bk_each:^(UIBezierPath *obj) {
[obj removeAllPoints];
}];
}
複製代碼
固然也有一些小小的不足, 在改變path的時候, 我發現cpu佔用率打到了15%, 不知道有沒有辦法繼續優化, 你們集思廣益😜
update: 波形圖採用了另外一種實現方式,上面滿是對Android同窗的致敬🙆🏻♂️
Demo完成, 用Swift寫完發現和oc的效果不同, 作了一些調整... 連接以下: WaveDemo
若是幫助到了你, 能夠點一波關注, 走一波魚丸 不對不對, 給文章點個喜歡
, 做者點個關注
就好了😘
iOS - 錄音文件lame轉換MP3相關配置 build-lame-for-iOS iOS中使用lame將PCM文件轉換成MP3(邊錄邊轉) IOS 實現錄音PCM轉MP3格式(邊錄音邊轉碼) iOS AVAudioEngine Android自繪動畫實現與一些優化思考——以智課批改App錄音波形動畫爲例