본문 바로가기
개발언어/NAudio

[NAuidio] 4. Changing Wave Formats

by 창용이랑 2021. 7. 8.
728x90

이번 4강에서는 wave format을 바꾸는 방법에 대해 다뤘다.

이 wave format이라는 단어가 자꾸 혼동의 여지가 있을 것 같은데, 이 format은 wav, mp3, aac 등의 확장자가 아니라 오디오 파일의 속성을 말한다.

대표적인 속성으로는 sample rate, bit depth, channel count가 있다.

 

이번 장에서 다루는 것은 uncompressed audio에 대해서 이런 속성들을 다루고 변환하는 것에 대한 내용이다.

중요한 내용도 많지만 사실 내 프로젝트에 필요한 내용은 좀 적어서, 불필요하다 싶은 부분은 아주 간략하게 언급하고 넘어간다. 필요하게 되면 그때 다시 찾아보고 정리하도록 하자.

 

그리고 wave format과는 별로 상관없지만, 강의 말미에 중요한 내용이 포함되어 있다. 좀 앞쪽으로 끌어오도록 하자.

 

 

NAudio Adapters

모든 라이브러리가 그렇듯이, NAudio에도 세 가지의 주요한 자료구조가 있다.

이 셋은 각각 클래스나 인터페이스로 되어서 수많은 child들을 가지고 있다. 지금까지 여러번 나왔고, 앞으로도 여러 번 나올 것이다.

다만 아래 내용은 어디에 설명된 것이 아니라 내가 눈치껏 이해한 것이라서, 실제와 다를 수 있다. 그러면 수정할 것이다.

 

WaveStream은 간단하게 말하자면 input 이다. 파일을 읽으면 이쪽이 된다.

IWaveProvider는 일반적인 PCM이라고 생각하자. 16bit거나 한다. signal chain에서 자주 이용된다.

ISampleProvider는 32bit IEEE float PCM으로 보인다. 역시 signal chain에서 주로 이용된다.

 

그들간에 변환할 일이 적잖이 생기는데, 몇몇 변환은 쉽게 멤버함수로 지원되고 어떤 변환은 직접 만들어야 하며(강의에 예제코드 있으니 필요하면 그때 찾아보자) 어떤 것은 허용되지 않는다.

 

 

기본적인 내용은 위와 같다. 다만 1.7버전이라서 현재는 이 외에도 조금씩 추가된 것은 있는 것 같다.

 

 

주요 Wave Format

이전에도 여러번 언급했고 위에서도 아래에서도 계속 다루겠지만, 혼동의 여지를 줄이기 위해 한번 더 쓰자.

 

sample rate

실제 '소리' 는 사인파이다. 그 파형은 곡선이지만 컴퓨터는 '곡선'을 다루기 너무 어렵다. 그래서 이신화(discretization) 과정이 필요해 진다.

즉 일정 간격으로 곡선을 점(sample)으로 표기하여 저장하는 방식인데, 이를 sampling 이라고 한다. '점을 얼마나 자주 찍을 것인가' 가 sample rate 이다. sample rate 가 높을수록 보다 많은 점을 찍게 되며, 당연히 원본과 비슷해지고 고품질의 소리를 낸다. 

가장 메이저한 sample rate는 44.1khz와 16khz 이다.

 

특기할 점이라면 직관적으론 잘 와닿지 않지만 sample rate가 낮으면 고주파수를 표현할 수 없게되는 문제가 있다. aliasing이라는 문제로, 자세한 것은 앞의 글을 보거나 검색하자.

 

 

Bit Depth

bit depth란 말은 내게는 잘 와닿지 않는 경향이 있다. 개인적으로는 sample size란 말이 어울린다고 생각한다.

이것은 하나의 sample을 몇 bit로 표시할 것인가를 나타낸다. 당연한 말이지만 보다 큰 bit로 표시할수록 더 큰 소리를 저장할 수 있고, bit depth가 낮으면 작은 소리밖에 표현하지 못한다.

sample의 크기는 소리의 크기이기 때문에, 표현 가능한 폭이 작아진다는 건 소리 크기 표현의 범위가 작아진다는 의미와 같다.

가장 메이저한 bit depth는 16bit(short), 24bit, IEEE float(32bit) 이다.

 

16bit는 일반 음질, 24bit는 DVD등의 고품질, float은 프로그래밍 시점에서의 연산용이 주요 목적이다.

 

 

Channel Count

이건 사실 너무 직관적이라 그다지 쓸 내용은 없다.

단순하게 모노 사운드, 스테레오 사운드, 멀티채널 사운드 등을 기록하기 위해서 있다.

1 = mono, 2 - stereo, 3~ - multi channel 이다.

 

 

Sample Rate Conversion (Resampling)

 

sample rate를 바꾸는 이유는 크게 3가지가 있다.

첫째는 재생(playback)을 위해서. 사운드카드는 동시에 다른 sample rate의 소리를 내지 못하므로, 여러개를 재생하기 위해서는 맞춰줘야 한다.

둘째는 Mixing을 위해서. 다른 sample rate의 파일은 당연히 섞을 수 없다. 배열 크기가 10인 배열과 20인 배열을 합하는 것은 불가능하다. 억지로 하자면 엉망진창이 되어버릴 것이다.

셋째는 Bandwidth를 위해서. 즉 용량 절약을 위해서이다. sample rate가 높다는 것은 다르게 말하면 용량이 크다는 것도 된다.

 

resampling의 가장 큰 문제 중 하나는 aliasing이다. 위에서도 잠깐 언급했지만 sample rate가 너무 낮아지면 고주파를 표현하지 못하는 문제가 생긴다. 정확히는 sample rate의 절반까지 (44.1khz면 22.05khz까지) 표현 가능하다고 한다.

그러므로 여기서 또다시 Low Pass Filter가 등장하게 된다. 표현 불가능해지는 고주파의 소리를 제거하는 것이다.

 

강의자료에선 'downsampling 전에' 와 'upsampling 후에' 라고 되어있는데, sample rate 를 높인 후에도 필요한가? 그 부분은 잘 모르겠다.

 

 

resampling의 기본적인 이론도 강의에 나오긴 하지만, 어차피 직접 짤 것은 아니므로(매우 고급 알고리즘이다!) 생략하도록 하겠다.

 

NAudio resamplers

다행이도, NAudio에서는 이걸 직접 짜라는 무식한 소리는 하지 않았다.

 

 

위 세 가지가 NAudio가 사용하는 resampler라고 할 수 있겠다.

물론(?) 이것들은 NAudio에서 작성한 건 아니고, Windows가 제공하는 기능을 가져온 것이다. 저자는 프로그래머지 신호 이론 전문가는 아닐 테니 당연한 일이라 할 수 있겠다.

 

DMO는 WasapiOut에 resampling 기능이 포함되지 않아서 그쪽을 사용할 때 자동으로 사용된다고 한다.

MFT는 다른 것들과 다르게 퀄리티를 조정할 수 있는데, 물론 퀄리티가 높아지면 시간은 오래 걸릴 것이다. 다만 디폴트는 best인 모양.

 

DMO는 왠지 없지만, 나머지 둘은 소스코드가 있어서 가져와 봤다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 public static void ResampleMediaFoundation()
 {
     using (var reader = new Mp3FileReader("demo.mp3"))
     {
         using (var resampler = new MediaFoundationResampler(reader, 16000))
         {
             // resampler.ResamplerQuality = 60;
             WaveFileWriter.CreateWaveFile("demo16000.wav", resampler);
         }
     }
 }


 public static void ResampleAcm()
 {
     using (var reader = new Mp3FileReader("demo.mp3"))
     {
         var resampledWaveFormat = new WaveFormat(16000,
             reader.WaveFormat.BitsPerSample,
             reader.WaveFormat.Channels);
         using (var resampler = new WaveFormatConversionStream(resampledWaveFormat, reader))
         {
             // resampler.ResamplerQuality = 60;
             WaveFileWriter.CreateWaveFile("demo16000acm.wav", resampler);
         }
     }
 }


Colored by Color Scripter
cs

 

위와 같다.

 

 

Bit Depth Conversion

 

sample 의 표현 방법을 바꾸는 내용이다. bit depth는 꽤 여러가지 있긴 하지만 이 강의에서는 세 가지 메이저 포맷만 다루었다.

강의 시점인 1.7 버전에서는 이들만 구현되어있던 모양이지만, 현재 버전인 1.10에서는 8bit, 16bit, 24bit, 32bit 모두를 다루고 있다.

강의 시점에서는 24bit이 덜 메이저했는지 이에 대한 지원도 적지만 1.10에서는 완벽하게 지원하고 있다.

 

16bit integer samples

int16 혹은 short로 표현된다.

범위는 -32758 ~ 32767 이다.

 

24bit integer samples

3byte int를 표시하는 타입은 없다.

범위는 -2^23 ~ 2^23-1 이다. (2~23 = 8388608)
 

32bit floating point IEEE samples

float(single) 로 표현된다.

-1.0f ~ 1.0 이다.

 

Bit Depth를 다루는 방법은 크게 2 가지로, signal chain 상에서 편집하는 방법과 byte array 로 받은 sample을 편집하는 두 가지 방법이 있다.

그러나 후자의 방법은 그다지 권장되는 방법은 아니고, 녹음 등 어쩔수 없는 상황에서 쓰이는 방법에 가깝다. 그렇기 때문에 후자의 방법은 매우 간략하게 넘어가도록 하겠다.

 

Converting in Signal Chains

signal chain은 대부분 IWaveProvider 인터페이스를 상속하는 값을 다룬다. 이전의 강의내용도 한번 보자.

그리고 이 클래스는 ISampleProvider로 바꿀 수 있는데, ToSampleProvider라는 함수를 지니고 있기 때문이다.

물론 반대 경우도 동작한다.

 

 

아래 예제 코드가 있다.

Mp3 파일을 읽어와 16bit PCM을 지닌 IWaveProvider 값을 받는다.

그걸 ToSampleProvider 함수를 써서 32bit float인 sampleProvider를 작성한다.

그리고 그걸 다시 TOWaveProvider 함수를 써서 되돌릴 수도 있음을 보여준다.

 

하나 유의할 점은, sampleProvider.ToWaveProvider() 함수도 존재하지만 이는 32bit int나 24bit int 를 리턴하는 게 아니라 32bit IEEE float을 리턴한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void ToFloatingPointAndBack()
{
    using (var reader = new Mp3FileReader("demo.mp3"))
    {
        // here we have 16 bit PCM
        Debug.Assert(reader.WaveFormat.BitsPerSample == 16);
        Debug.Assert(reader.WaveFormat.Encoding == WaveFormatEncoding.Pcm);
        
        // convert to 32 bit IEEE
        ISampleProvider sampleProvider = reader.ToSampleProvider();
        // ISampleProvider sampleProvider = new Pcm16BitToSampleProvider(reader);
        Debug.Assert(sampleProvider.WaveFormat.BitsPerSample == 32);
        Debug.Assert(sampleProvider.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat);                
        
        // get back to 16 bit PCM
        IWaveProvider backTo16Bit = sampleProvider.ToWaveProvider16();
        Debug.Assert(backTo16Bit.WaveFormat.BitsPerSample == 16);
        Debug.Assert(backTo16Bit.WaveFormat.Encoding == WaveFormatEncoding.Pcm);
    }
}


Colored by Color Scripter
cs

 

위 코드는 강의에 나오는 코드를 약간 수정한 것이다.

1.7에서는 제공되지 않았었지만 1.10에서는 제공되는 함수들이 있어서이다.

강의에서는 24bit를 지원하지 않는다고 말하지만, 실제로 1.10에서는 지원되고 있다.

 

아래 코드가 sample provider를 받아 24bit 로 변환, 저장하는 코드이다.

 

1
2
3
var waveProvider24Bit = new SampleToWaveProvider24(sampleProvider);
WaveFileWriter.CreateWaveFile("output.wav", waveProvider24Bit);


Colored by Color Scripter
cs

 

아래는 이론적으로 BitDepth를 어떻게 바꾸는가에 대한 이야기이다.

Byte array를 다루는 경우 직접 할 수도 있는데, 물론 굳이 할 필요는 없는 이야기이기도 하다.

그리고 아래를 보듯이 나누기와 곱하기가 다르기 때문에, 계속 반복하게 되면 약간씩 손상된다는 것도 알 수 있다. (애초에 int-float을 오가는 시점에서 손상이 생기기 쉽지만.)

 

 

 

그 외에도 byte array 타입으로 오디오 정보를 받아 다루는 내용은 별로 언급하지 않겠다.

 

 

Changing Channel Count

 

channel의 수를 변경할 일이 가끔은 존재한다.

경우의 수는 mono를 stereo로 바꾸거나, stereo를 mono로 바꾸는 게 일반적이지만 2개의 mono를 합쳐서 stereo로 만들거나 3개 이상의 multi channel로 만들 수도 있다.

 

다만 내가 필요한 경우는 mono를 복제하여 stereo로 만들거나, stereo에서 하나를 탈락시키는 경우 뿐이기에 그 외의 내용은 그다지 다루지 않겠다.

어느 정도는 언급할 거지만.

 

Mono To Stereo

1채널 사운드를 2채널 사운드로 바꾸는 방법(전략, strategies)은 4가지가 있다.

 

1. 같은 오디오를 두 채널에 전달

2. left channel 에만 출력

3. right channel 에만 출력

4. Panning

 

내가 가장 필요한 것은 1번인데 그에 관한 코드 예제만 없다.

찾아보니 MonoToStereoSampleProvider 란 클래스를 이용하여 signal chain에 포함하는 방법으로 할 수 있는 것 같다.

실행은 못 해봤지만서도.

 

아래 셋은 사실 전부 panning으로 볼 수 있는데, PanningSampleProvider 라는 클래스를 사용하여 signal chain의 한 부분으로 들어간다.

일단 1.0f 부터 -1.0f 까지의 범위가 존재하여 1.0일 경우 오른쪽만, -1.0일 경우 왼쪽에서만 소리가 나오는 형식이다.

그리고 그렇지 않을 경우, 즉 가운데 어딘가일 경우에도 몇 가지 전략들이 존재한다.

 

예를 들자면 음원이 가운데에 있으면 양쪽을 그냥 풀 볼륨으로 트는게 아니라 약간씩 소리를 줄여 약간 거리가 있는 것처럼 느껴지게하는 식이다.

이 부분은 크게 설명도 없었으니 쓰게 된다면 그때 찾아보는 걸로 하자.

 

Stereo To Mono

2채널 사운드를 1채널 사운드로 바꾸는 전략은 3가지이다.

1. right channel을 discard (left channel만 사용)

2. left channel을 discard (right channel만 사용)

3. mix channels

 

이들은 StereoToMonoProvider16 혹은 StereoToMonoSampleProvider 클래스에 의해서 진행된다.

양쪽 중 하나를 0.0으로 쳐 버리면 discard가 되는 것이고, 아니면 mixing이 되는 것이다.

 

1
2
3
4
5
6
7
8
 var reader = new Mp3FileReader("Demo.mp3");
 var mono  = new StereoToMonoProvider16(reader);
 mono.LeftVolume = 1.0f;
 mono.RightVolume = 0.0f;


 StereoToMonoSampleProvider sampleProvider = new StereoToMonoSampleProvider(reader.ToSampleProvider());
 sampleProvider.LeftVolume = 1.0f;
 sampleProvider.RightVolume = 0.0f;
Colored by Color Scripter
cs

 

위에서 보다시피 두 방법은 같이 사용되지만, 입출력이 IWaveProvider인지 ISampleProvider인지에 따라서 갈리게 된다.

24bit int 용은 없나보다. 역시 개발용으로는 부적합한 모양이다.

 

여러 개의 mono sound 합치기

두 개 이상의 mono sound를 합쳐서 stereo 혹은 그 이상의 multi channel을 만들 수 있다.

단순하게 생각해 보자면 각 샘플을 나란히 배치(interleaving)하기만 하면 된다.

 

아래 두 클래스를 이용해서 진행할 수 있다.

MultiplexingSampleProvider

MultiplexingWaveProvider

 

자세한 설명은 생략.

 

출처 : https://m.blog.naver.com/luku756/221895113633

'개발언어 > NAudio' 카테고리의 다른 글

[NAudio] 6. Recording Audio  (0) 2021.07.08
[NAudio] 5. Working With Codecs  (0) 2021.07.08
[NAuido] 3. Working With Files  (0) 2021.07.08
[NAudio] 2. Audio Playback  (0) 2021.07.08
[NAudio] 1. Introducing NAudio  (0) 2021.07.08