iOS端音訊邊錄邊轉和聲波圖的實現

iOSleep發表於2017-12-21

我司和我同專案的Android小夥伴分享了技術文章, 我們大iOS也不可以落後, 整理了一下關於音訊處理的一些內容, 希望對大家有所幫助.

Talk is cheap, show you the code WaveDemo

##轉碼前奏 我所在專案, 需要將音訊上傳至伺服器, iOS原生的錄音產生的PCM檔案過大, 為了統一三端, 我們決定使用mp3格式. iOS錄音的輸出引數預設不支援mp3(那個欄位沒用...), 所以我們需要使用lame進行轉碼. 網上可以找到的lame.a多是iOS6, 7, 並且有的不支援bitcode, 容我做個悲傷的表情? 我在github上找到了一個編譯lame原始碼的庫build-lame-for-iOS, 支援bitcode, 並且可修改最低支援版本, 自行修改sh檔案

tip: lipo操作可操作libXXX.a檔案, 增刪平臺依賴.

##轉碼進行時 搞完lame的問題, 我們開始進行編碼, 最開始我使用Google大法, 找到了使用lame的方式, 核心程式碼如下:

@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均位元速率的形式識別播放, 導致時長不正確, 所以這裡調整上面的一行程式碼:

lame_set_VBR(lame, vbr_off);
複製程式碼

注意模擬器和真機的取樣率有些許不同, 如遇到播放雜音的狀況可調整為

lame_set_in_samplerate(lame, 44100);
複製程式碼

##優化轉碼 其實就是邊錄邊轉了, 這裡我檢視了一些文章, 大多基於AVAudioRecorder實現的方式比較粗暴, 不想使用. 我還查到了基於AVAudioQueue的, 不過api多C語言. 想起來前一陣看的Apple的session中有關於AVAudioEngine的介紹, 使用起來更加oc, 本著折騰就是學習的心, 使用AVAudioEngine進行轉碼. 這裡我就不多做介紹了,可以檢視文章iOS AVAudioEngine 使用AVAudioEngine可以拿到時時的音訊buffer, 對其進行轉碼即可, 將轉碼後的data進行append(可自己改造, 使用AFNetworking進行流上傳). 核心程式碼如下:

  • 轉碼準備工作,建立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頻率

 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()
  }

複製程式碼

幾個需要注意的點

  1. 參考Lame使用VBR模式編碼MP3檔案,播放總時間長度不對的解決方法LAME 是一個開源的MP3解碼編碼工具, 我沒有回溯檔案, 所以清空了vbr的檔案頭, 其實使用的是cbr的形式
lame_set_bWriteVbrTag(_lame, 0);
複製程式碼
  1. 在input的回撥中, 我們修改了bufferframeLength, 因為預設的input的回撥頻率是0.375s, 我們可以通過修改frameLength來達到修改頻率的目的. 使用這樣的方法不夠優雅,所以通過設定session的ioduration來達到類似的目的。

  2. 錄製過程中沒有辦法取得當前錄製時長, 請自行使用NSDate, NSTimer等方式進行計算.

  3. 在模擬器上一切ok, 在我的小6上, 錄製的音訊播放出來是快進的?,debug了好久發現, 在真機上, 如果連續播放錄製, 有時AVAudioEngine的input上的format拿到的取樣率sampleRate不是預期的44100而是16000. 解決辦法有兩種

  • 設定AudioSession的preferdSampleRate
AVAudioSession *sessionInstance = [AVAudioSession sharedInstance];
[sessionInstance setPreferredSampleRate:kPreferredSampleRate error:&error]
複製程式碼
  • 或者使用mixnode的方式對取樣率進行處理, 可參考這個帖子

##同步Android波形圖 因為重構以上部分的內容, 導致業務上拉後Android小夥伴, 他已經完成了波形圖的繪製, 可以檢視效果圖.

wave.gif

Android自繪動畫實現與一些優化思考——以智課批改App錄音波形動畫為例 Android小夥伴詳細介紹瞭如何繪製該圖形, 在他的幫助下, 我在iOS端也實現了該效果.

基本流程

  1. 計算曲線點的位置和對稱衰減函式
  2. 根據計算繪製點, 使用CAShapeLayer和UIBezierPath
  3. 根據時間, 改邊Φ值, 實現曲線位移效果
  4. 根據音量volume和衰減函式, 改變振幅, 實現上下波動.

優化手段大體相同

  • 降低繪製密度
  • 減少重複實時計算量
  • 複用, 減少物件建立銷燬

核心程式碼如下:

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錄音波形動畫為例

相關文章