본문 바로가기
Projects/Flutter 프로젝트 갈아엎기 - eqTrainer

Flutter 프로젝트 갈아엎기 2 - 오디오 신호 처리, coast_audio에서 답을 찾다

by potatosalad 2023. 5. 22.

중간고사 기간이 겹치는 것과 더불어, 실시간 오디오 필터링을 구현하기 위해서 처음 시도했던 방식이 실패하면서 2편을 쓰기까지 시간이 다소 걸렸습니다.

 

이전 내용을 읽어보고 싶으신 분들은 아래 글들을 참고하실 수 있습니다.

https://potatosalad775.tistory.com/6

 

eqTrainer 갈아엎기 1

eqTrainer? 노래 듣는 걸 좋아하는 저는 음향기기나 이론에도 관심이 좀 있어서, 그런 주제로 대화할 수 있는 커뮤니티 여러 곳을 자주 구경하곤 합니다. 그러한 커뮤니티를 둘러보면 여러 고민들

potatosalad775.tistory.com

https://potatosalad775.tistory.com/7

 

Flutter 프로젝트에서 Faust DSP 사용하기

얼마 전 eqTrainer 갈아엎기 - 1편을 작성한 이후로 Faust DSP를 플러터 프로젝트에 결합하는 과정을 시도해왔습니다. 몇 일 간 이리저리 시도한 끝에 해당 작업을 성공하기는 했는데, 기기에서 재생

potatosalad775.tistory.com

 


 

Faust DSP로는 기기에서 실행되는 오디오에 필터링이 불가능하다는 점을 확인했고, 이후 추가적인 조사를 진행하다가 pub.dev에 등록된 지 한달도 채 되지 않은 따끈따끈한 DSP 패키지를 발견할 수 있었습니다.

 

https://pub.dev/packages/coast_audio

 

coast_audio | Dart Package

A cross-platform audio processing library written in dart

pub.dev

https://github.com/SKKbySSK/coast_audio

 

GitHub - SKKbySSK/coast_audio: Real-time audio processing library written in dart with ffi

Real-time audio processing library written in dart with ffi - GitHub - SKKbySSK/coast_audio: Real-time audio processing library written in dart with ffi

github.com

 

coast_audio라고 하는 오디오 프로세싱 라이브러리가 그 주인공입니다.

해당 라이브러리는 다음과 같은 특징을 가지고 있습니다.

 

1. dart 언어로 작성되어 네이티브 코드 작성 없이 오디오 신호를 다룰 수 있음.

-> 이로 인해 하나의 코드베이스로 멀티플랫폼 지원이 가능함

2. 오디오 재생을 비롯한 추가적인 오디오 처리를 위해 miniaudio 라이브러리의 일부 기능을 사용할 수 있음.

 

다음과 같은 제약들도 존재합니다.

 

1. 오디오 신호를 처리하는 코어 기능이 준비되어 있기는 하지만, 복잡한 오디오 필터는 직접 구현이 필요함.

2. 처리된 오디오 신호를 재생할 수 있도록 세팅하는 과정이 매우 복잡함.

 

아무래도 오디오 신호 처리에 중점을 둔 패키지다보니, 간단한 오디오 플레이어를 구현하는 것부터가 상당히 복잡합니다.

패키지의 예시 프로젝트를 살펴보셔도 아시겠지만, 패키지 차원에서 오디오 재생을 위한 기능들은 제공하고 있지 않기 때문에 직접 구현이 필요합니다.

 

https://github.com/SKKbySSK/coast_audio/tree/main/examples/music_player

 

GitHub - SKKbySSK/coast_audio: Real-time audio processing library written in dart with ffi

Real-time audio processing library written in dart with ffi - GitHub - SKKbySSK/coast_audio: Real-time audio processing library written in dart with ffi

github.com

 

이거면 되겠는데?

 

위 패키지를 살펴본 뒤, 제가 원하는 eqTrainer의 핵심 기능 - 실시간 오디오 필터링 기능을 구현할 수 있겠다는 확신이 들어 실제로 해당 기능의 구현이 가능한지 실증을 거치기 위해, 패키지 레포지토리에서 제공되고 있는 뮤직 플레이어 예시 프로젝트에 파라메트릭 EQ 노드를 직접 구현하여 연결하는 과정을 거쳤습니다.

 

감사하게도 아주 오래전부터 이퀄라이저 구현을 위한 수학식을 미리 계산해두신 분이 계셨기에, 조금 헤매기는 했지만 노드를 구현하는 것 자체는 크게 어렵지 않았습니다. https://www.w3.org/TR/audio-eq-cookbook/

 

Audio EQ Cookbook

Abstract Cookbook formulae for audio equalization biquad filter coefficients. Describes the most common analog biquad filter types and how to convert them to the digital filter equivalents. Adapted from Audio-EQ-Cookbook.txt, by Robert Bristow-Johnson, wit

www.w3.org

 

어라 왜 노이즈가 생기지?

 

문제는 노드를 연결한 다음이었습니다. 파라메트릭 EQ가 필터링된 오디오에서 잡음이 섞여 들리는 문제가 있었습니다.

 

제가 구현했던 파라메트릭 EQ 노드는, Raw 음악 데이터를 Float32 자료형 리스트의 형식으로 쪼개어 만든 '버퍼'를 제공받고, 빠르게 반복적으로 호출되는 Process 함수를 통해 이를 가공하는 방식으로 동작하고 있습니다.

 

당시 생각으로는 별 문제가 되지 않을 것으로 생각했었는데, process 함수 내부에서 coefficient를 반복적으로 정의하는데서 발생하는 레이턴시가 문제였습니다. 오디오 프로세싱이 생각보다도 레이턴시에 훨씬 민감하다는 점을 깨달았습니다. 그래서 대부분의 오디오 라이브러리가 C언어로 작성되어 있구나 하는 생각도 들었네요.

 

결국 노드에서 사용되는 coefficient는 전부 process 함수 바깥에서 정의하는 것으로 바꾸었고, 레이턴시로 인한 잡음 문제는 해결되었습니다.

 

더보기
import 'dart:math';
import 'package:coast_audio/coast_audio.dart';
import 'package:eq_trainer/main.dart';

class ParametricEQNode extends AutoFormatSingleInoutNode with ProcessorNodeMixin, BypassNodeMixin {
  ParametricEQNode({
    required this.format,
    required this.centerFreq,
    required this.dbGain,
    required this.qFactor,
  });

  final AudioFormat format;
  double centerFreq;
  double dbGain;
  double qFactor;

  double w0 = 0;
  double alpha = 0;
  double A = 0;
  double b0 = 0;
  double b1 = 0;
  double b2 = 0;
  double a0 = 0;
  double a1 = 0;
  double a2 = 0;
  double x = 0;
  double y = 0;
  double _x1 = 0;
  double _x2 = 0;
  double _y1 = 0;
  double _y2 = 0;

  @override
  List<SampleFormat> get supportedSampleFormats => const [SampleFormat.float32];

  @override
  int process(AudioBuffer buffer) {
    final inputData = buffer.asFloat32ListView();

    // Following Coefficients and Transfer Functions are adapted from Audio-EQ-Cookbook.
    // Learn more at https://www.w3.org/TR/audio-eq-cookbook/

    // Calculate filter coefficients
    w0 = 2 * pi * centerFreq / format.sampleRate;
    alpha = sin(w0) / (2 * qFactor);
    A = pow(10.0, (dbGain / 40)).toDouble();

    b0 = 1 + (alpha * A);
    b1 = -2 * cos(w0);
    b2 = 1 - (alpha * A);
    a0 = 1 + (alpha / A);
    a1 = b1;
    a2 = 1 - (alpha / A);

    // Process each sample
    for(int frame = 0; frame < buffer.sizeInFrames; frame++) {
      for (var channel = 0; format.channels > channel; channel++) {
        final inputBufferIndex = (frame * format.channels) + channel;

        x = inputData[inputBufferIndex];

        // Calculate output sample
        y = (b0/a0)*x + (b1/a0)*_x1 + (b2/a0)*_x2 - (a1/a0)*_y1 - (a2/a0)*_y2;

        // Update the state variables
        _x2 = _x1;
        _x1 = x;
        _y2 = _y1;
        _y1 = y;

        // Update the buffer data
        inputData[inputBufferIndex] = y;
      }
    }

    return buffer.sizeInFrames;
  }
}

 

생각보다 쉽지 않은 오디오 신호 처리

 

위 과정을 통해 네이티브 코드 작성 없이 파라메트릭 EQ 노드 구현 및 적용이 가능하다는 점을 확인했고, 지금은 eqTrainer 앱 개발이 진행 중인 상황입니다.

 

 

세삼 오디오 플레이어 패키지를 만들어주시는 분들께 참 감사하다는 느낌을 가지게 되는 순간입니다.

 

위 사진은 현재 개발이 진행 중인 eqTrainer의 프로젝트 디렉터리인데, 여기에 포함되어 있는 dart 파일들 중 1/3이 단순 사운드 재생 및 제어를 위한 기능만을 담당하고 있습니다. 오디오 재생 관련 기능을 간소화해주는 패키지를 끌어들어 사용했었던 이전 eqTrainer 개발했을 당시에는 몰랐던 일입니다.

 

그마저도 제가 처음 작성했던 코드는 어째서인지 상태 관리 측면에서 잘 작동하지 않는 문제가 있었고, 결국에는 이를 해결하지 못해 coast_audio의 플레이어 예시 프로젝트를 기반으로 eqTrainer만의 기능들을 처음부터 다시 쌓아올리는 과정을 거쳐야 했습니다.

 

지금 다시 생각해보면 코드를 병렬화하여 실행시켜주는 isolate 구조의 이해가 부족하여 문제가 발생했던 것 같습니다.

 

다음 eqTrainer의 개발 목표

 

오디오 클립 편집, 파라메트릭 EQ, 주파수 응답 그래프 위젯 등 eqTrainer의 핵심 기능 구현은 거의 마무리되어 가는 단계입니다만, 새롭게 바닥부터 다시 개발하고 있는 eqTrainer의 개발 목표를 다시 한 번 곱씹어보는 것도 나쁘지 않을 것 같다는 생각이 들었습니다. 

 

제가 개인적으로 생각한 개발 목표는 다음과 같습니다.

 

1. 실시간 오디오 필터링 기능 구현 

이전 포스트에서도 설명드렸지만, ffmpeg를 통해 오디오 클립에 미리 필터를 입히는 방식은 안정적이고 간단하기는 했지만, 매번 세션이 진행될 때마다 오디오 클립을 편집하는 동안 사용자가 대기하는 시간이 필요했습니다.

 

필터가 입혀진 음악 파일과, 필터가 입혀지지 않은 음악 파일 두 개를 번갈아 가면서 듣는 것이 eqTrainer의 핵심 기능인데, 이를 위해 각각의 오디오 파일을 불러놓은 두 개의 오디오 플레이어 Object를 준비해놓고, 한 쪽은 Mute하고 다른 한 쪽은 Unmute하는 방식으로 구현을 시도하였으나 어쩔 수 없이 파일 전환 과정에서 확실하게 사람이 인지할 수 있을 정도의 끊김이나, 두 플레이어 간 재생 중인 Position이 맞지 않는 문제도 발생했습니다.

 

실시간 오디오 필터링의 장점은, 하나의 플레이어를 준비해놓고 해당 플레이어의 오디오 스트림이 통과하게 되는 파라메트릭 EQ를 껐다 켰다하는 것으로 오디오를 전환하는 효과를 낼 수 있기 때문에 오디오 끊김, 싱크 불일치 등의 문제를 근본적으로 해결할 것으로 기대하고 있습니다.

 

2. 무분별한 패키지 사용 자제

이전 eqTrainer는 GetX 패키지를 사용하여 아주 간단하게 다크모드 스위치, 스낵바 알림, 화면 전환 등을 구현할 수 있었습니다. 덕분에 프로그래밍 지식이 거의 전무하던 제가 프로젝트를 어찌저찌 완성하는데에는 성공할 수 있었지만, 그 과정에서 상태 관리의 개념을 비롯해서 정작 저 자신이 얻은 것이 거의 없었다는 생각이 들었습니다.

 

편의성과 간소화 하나 만큼은 강력한 장점을 지니고 있지만, 플러터 프레임워크를 관통하는 핵심적인 부분인 BuildContext를 사용하지 않는다는 점에서 GetX는 해외 플러터 커뮤니티에서도 뜨거운 감자인 것으로 알고 있습니다. 아무튼 뭐라도 좀 얻어보자는 생각에서 GetX는 사용하지 않겠다는 집념을 품고 새로운 eqTrainer 개발을 시작했고, 결과적으로 최소한 BuildContext가 뭐하는 녀석인지, 상태관리는 어떻게 돌아가는 것인지 쥐똥만큼이라도 알게 된 것 같습니다.

 

이렇게 결심한 데에는 또 다른 이유도 있습니다.

 

기존 eqTrainer를 개발하는 과정에서 Github에 apk 파일 형식으로 배포된 앱을 위한 자체 업데이트 기능을 구현하고자 하는 생각이 있었습니다. 이를 위한 'r_upgrade' 패키지를 pubspec.yaml에 포함만 시켜두고 해당 패키지의 기능을 사용하지는 않았던 상황이었는데, 이로 인해 앱이 구글 플레이스토어로부터 퇴출되는 일이 있었습니다.

 

이에 대해 이의를 제기하여 eqTrainer를 스토어로 복구하는데 성공했음에도 불구하고, 프로젝트를 열어보기 싫다? 귀찮다?는 감정에 이를 오랫동안 해결하지 않고 방치하는 바람에 동일한 이유로 앱이 구글 플레이스토어에서 퇴출되는 결과를 맞았습니다. ㅠㅜㅠ

 

3. 확실한 기능 및 클래스 분리

1학년 마치고 군대를 갔다 온 뒤 방학기간 동안 완성한 첫 eqTrainer의 코드를 살펴보면, 개판오분전이라고 해도 될 만큼의 혼란스러운 흔적들이 남아있습니다.

 

뭔가 기능을 구분하고 싶었던건지 다트 파일들을 Service, Page 등등 폴더를 나누어 집어넣어두기는 했는데,

SessionService.dart에는 주파수 응답 그래프를 표현하는 위젯과 해당 위젯을 감싸는 페이지가 들어가 있기도 하고,

SessionPageManager.dart에는 그래프 렌더링을 준비하는 기능과 오디오 재생을 준비하는 기능을 뭉쳐놔서 500줄이 넘어가는 코드는 읽기 어렵고,

충분히 재사용이 가능한 기능들을 여러 차례 재정의하는 경우도 있었습니다.

 

덕분에 주석을 아주 주렁주렁 달아놓았음에도 불구하고 제가 쓴 코드를 제가 이해하지 못하는 경우가 발생했고, 결국 제가 쓴 프로젝트를 다시 열어보는 것을 기피하게 되늘 결과를 낳았습니다.

 

이번에 eqTrainer를 구현할 때에는 이 문제를 의식하면서 그래도 가능한 최소화해보자... 하는 생각입니다.

 

Faust DSP vs Coast_audio

 

두 오디오 신호 처리 라이브러리를 사용해 본 입장에서 제 나름대로 두 라이브러리의 장단점을 정리해보았습니다.

  Faust DSP coast_audio
공통 장점 오디오 관련 기능이 빈약한 Dart/Flutter에 강력한 오디오 신호 처리 기능 제공

다양한 플랫폼에 하나의 코드베이스로 동일한 기능 제공 가능
공통 단점 오디오 신호 처리 이외의 기능은 제공하지 않아 직접 구현이 필요함.
단순 오디오 재생이 목적이라면 이에 특화된 just_audio 등 보다 단순한 패키지 사용이 권장됨.
장점 오디오 신호 생성, 가공을 위한 풍부한 기능들을 제공함. Dart 언어만을 사용하여 오디오 신호 처리가 가능한 유일한(?) 라이브러리
단점 Faust 언어를 사용하여 dsp 파일 작성 및 변환, cmake를 사용하여 이를 프로젝트에 연결하는 번거로운 과정이 필요함.

기기에서 재생되는 소리를 받아와 처리하기 위해서는 네이티브 코드 작성이 필요함.

다양한 플랫폼을 지원하기 위해서 추가적인 작업이 필요하나, 오래된 라이브러리라 관련된 정보가 오래된데다, 많지도 않음
나온지 한 달 남짓된 라이브러리라 정보가 많지 않음.

오디오 신호 처리를 위한 핵심 코어 기능만 제공하므로 오디오 필터를 비롯한 부가적인 기능 등은 직접 구현이 필요함.

arm64 기반 iOS, Android, macOS만 지원함

레이턴시에 극도로 민감한 오디오 + Dart 언어 사용으로 인해 프로젝트가 무거워지기 시작하면 노이즈, 스터터링을 비롯한 문제가 발생할 수 있음. 프로젝트 성능 관리에 신경을 써야 할 듯함

신호 처리의 '신'자도 모르는 꼬꼬마 친구는 이렇게 생각했구나~ 라고만 봐주시면 좋겠습니다.

 

여담

 

 

예전 eqTrainer의 디자인은 지금 봐도 예쁜 것 같아서 아마 이 디자인 그대로 유지되지 않을까 싶어요.

지친 마음에 대충 마무리했던 Session 화면 UI 정도만 조금 수정을 하게 되지 않을까 싶습니다.

 

지금 상태에서 완성도는 한 80% 되는 것 같네요~

댓글