Microsoft MVP성태의 닷넷 이야기
닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력 [링크 복사], [링크+제목 복사],
조회: 9314
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 9개 있습니다.)
.NET Framework: 618. C# - NAudio를 이용한 MP3 파일 재생
; https://www.sysnet.pe.kr/2/0/11092

닷넷: 2236. C# - Audio 장치 열람 (Windows Multimedia, NAudio)
; https://www.sysnet.pe.kr/2/0/13594

닷넷: 2237. C# - Audio 장치 열기 (Windows Multimedia, NAudio)
; https://www.sysnet.pe.kr/2/0/13595

닷넷: 2238. C# - WAV 기본 파일 포맷
; https://www.sysnet.pe.kr/2/0/13596

닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력
; https://www.sysnet.pe.kr/2/0/13597

닷넷: 2240. C# - WAV 파일 포맷 + LIST 헤더
; https://www.sysnet.pe.kr/2/0/13598

닷넷: 2241. C# - WAV 파일의 PCM 사운드 재생(Windows Multimedia)
; https://www.sysnet.pe.kr/2/0/13599

닷넷: 2243. C# - PCM 사운드 재생(NAudio)
; https://www.sysnet.pe.kr/2/0/13601

닷넷: 2244. C# - PCM 오디오 데이터를 연속(Streaming) 재생 (Windows Multimedia)
; https://www.sysnet.pe.kr/2/0/13602




C# - WAV 파일의 PCM 데이터 생성 및 출력

지난 글에서,

C# - WAV 기본 파일 포맷
; https://www.sysnet.pe.kr/2/0/13596

WAV 파일의 간단한 헤더 구조를 봤는데요, 그 덕분에 적절한 PCM 데이터만 있다면 금방 wav 파일을 만들 수 있습니다. 실제로 해볼까요? ^^

우선, 적당한 PCM 데이터를 생성해야 하는데요, 사실 PCM은 단순히 음의 크기를 나타내는 배열 형식의 자료 구조이기 때문에 간단하게 Octave에서 다음과 같이 처리할 수 있습니다.

# doremi.m
# https://docs.octave.org/v4.0.0/Two_002dDimensional-Plots.html

sample_rate = 22050;

frequency = [262 294 330 349 392 440 494 523];  # 도레미...의 주파수
sound = []
t = 0:1/sample_rate:1;

for doremi_freq = frequency
  s = cos(2*pi*doremi_freq*t);
  sound = [sound s];
endfor

22Khz 샘플링으로 "도, 레, 미, 파, 솔, 라, 시, 도"에 해당하는 주파수의 데이터를 각각 1초씩 생성해 sound 변수에 넣었습니다. 일부 데이터를 plot으로 그려보면 단순히 -1~+1 사이에서 주파수에 따라 요동치는 간단한 형식입니다.

>> plot(t(1:200), sound(1:200));

octave_cosine_wave_1.png

직접 소리를 내 확인하는 것도 가능한데요, soundsc 함수를 사용하면 ^^ 도레미... 음이 1초 간격으로 들릴 것입니다.

>> doremi;
sound = [](0x0)

>> soundsc(sound, sample_rate)

자, 그럼 이 데이터를 binary 포맷으로 저장하고,

>> save -binary test.pcm sound

지난 글에 소개한 Octave Reader를 통해 C#에서 PCM 데이터 파일을 읽어들입니다. 단지, 현재 Octave가 출력한 PCM 데이터가 -1~+1 범위의 double 값이므로, wav 파일에서 PCM 데이터로 동작하기 위해서는 이 값을 정수 형태로 바꿔야 합니다. 이를 위해 다음과 같이 보조 메서드를 하나 추가해 줍니다. (아니면, 애당초 Octave 측에서 정수로 바뀐 데이터를 출력해도 됩니다.)

// https://www.sysnet.pe.kr/2/0/13324#src
public struct BinaryOctaveFile
{
    //  ...[생략]...

    public short[] PCMDataAsShorts()
    {
        int dataCount = Rows * Columns;
        short[] data = new short[dataCount];

        for (int i = 0; i < dataCount; i ++)
        {
            data[i] = (short)(Data[i] * 32767);
        }

        return data;
    }
}

자, 그럼 이 메서드로 다음과 같이 Octave가 생성한 PCM 데이터를 읽어내 WAV 헤더 파일과 함께 출력해 주는 Create 메서드에 전달하면 wav 파일이 만들어집니다.

{
    BinaryOctaveFile octave = BinaryOctaveFile.Read(@"test.pcm");
    short[] data = octave.PCMDataAsShorts();

    byte[] buffer = new byte[data.Length * 2];
    Buffer.BlockCopy(data, 0, buffer, 0, buffer.Length);

    // Octave에서 생성한 데이터 유형: sample_rate = 22050
    //                              bits_per_sample = 16
    //                              channels = 1
    WaveFile.Create(@"C:\Temp\test.wav", 22050, 16, 1, buffer);
}

이렇게 저장한 파일이 정상적인지, 다시 WaveFile로 읽어들여 포맷을 체크할 수 있습니다. ^^

{
    BinaryOctaveFile octave = BinaryOctaveFile.Read(@"test.pcm");
    short[] data = octave.PCMDataAsShorts();

    byte[] buffer = new byte[data.Length * 2];
    Buffer.BlockCopy(data, 0, buffer, 0, buffer.Length);

    WaveFile.Create(@"C:\Temp\test.wav", 22050, 16, 1, buffer);
}

{
    string waveFilePath = @"C:\Temp\test.wav";
    WaveFile wf = new WaveFile(waveFilePath);

    Console.WriteLine(wf.Header);
}

/* 출력 결과
ChunkId: RIFF, FileSize: 352852, TypeHeader: WAVE, FormatMarker: fmt , SubChunkSize: 16, AudioFormat: 1, Channels: 1, SampleRate: 22050, ByteRate: 44100, BlockAlign: 2, BitsPerSample: 16, DataChunkHeader: data, DataSize: 352816
*/

헤더 규약에 맞게 wav 파일을 생성했으니, 당연히 읽기 시에도 출력이 정상적으로 나오고, 게다가 Media Player 등을 이용해 test.wav 파일을 재생해 보면 ^^ 소리가 잘 들립니다.




하는 김에, 스테레오용 PCM도 생성해 볼까요? 사실 Mono는 단일 음이 연속해서 Byte[] 버퍼에 채워지는 형식이지만,

[M1 M2 M3 ...]

Stereo 형식이면 Left Channel과 Right Channel 음이 번갈아서 버퍼에 채워지게 됩니다.

[L1 R1 L2 R2 L3 R3 ...]

따라서, 이전의 도레미... 음을 다음과 같이 "repelem"을 이용해 (이번엔 44Khz의) 동일한 음으로 Left/Right Channel을 생성할 수 있습니다.

sample_rate = 44100;

frequency = [262 294 330 349 392 440 494 523];
sound = []
t = 0:1/sample_rate:1;

for doremi_freq = frequency
  s = cos(2*pi*doremi_freq*t);

  s = repelem(s, 2);
  sound = [sound s];
endfor

역시 이렇게 생성한 sound 데이터를 test_stereo.pcm 파일로 저장하고 wav 헤더를 추가해 주면,

{
    BinaryOctaveFile octave = BinaryOctaveFile.Read(@"test_stereo.pcm");
    short[] data = octave.PCMDataAsShorts();

    byte[] buffer = new byte[data.Length * 2];
    Buffer.BlockCopy(data, 0, buffer, 0, buffer.Length);

    // Octave에서 생성한 데이터 유형: sample_rate = 44100
    //                              channels = 2
    // 데이터를 2바이트로 변환:       bits_per_sample = 16
    WaveFile.Create(@"C:\Temp\test_stereo.wav", 44100, 16, 2, buffer);
}

동작하는 test_stereo.wav 음악 파일을 만들 수 있습니다.

하다 보니 재미있군요. ^^ 그럼 이번에는 Stereo지만, Right Channel 음을 무음으로 만들어 보겠습니다. 이를 위해 Octave에서는 2번째 데이터에 대해 0을 만들 수 있도록 아래와 같은 조작을 추가합니다.

sample_rate = 44100;

frequency = [262 294 330 349 392 440 494 523];
sound = []
t = 0:1/sample_rate:1;
even_pos = 2:2:size(t)(2)*2;

for doremi_freq = frequency
  s = cos(2*pi*doremi_freq*t);

  s = repelem(s, 2);
  s(even_pos) = 0;
  sound = [sound s];
endfor

재생해 보면, 의도했던 대로 오른쪽 이어폰에서 소리가 발생하지 않습니다.




마지막으로, Octave의 PCM 데이터는 -1~+1 사이의 double 값을 가진, 어찌 보면 정규화된 형식이기 때문에 이 값의 bitsPerSample을 정하는 것은 읽어들이는 쪽에서 알아서 하면 됩니다. 앞선 예제 코드에서는 해당 데이터를 2바이트로 복원했었는데요, 당연히 곱해주는 값을 바꿔 4바이트로 하는 것도 가능합니다.

using System.Text;

namespace Octave;

public struct BinaryOctaveFile
{
    // ...[생략]...

    public int[] PCMDataAsInt32s()
    {
        int dataCount = Rows * Columns;
        int[] data = new int[dataCount];

        for (int i = 0; i < dataCount; i++)
        {
            data[i] = (int)(Data[i] * 2147483647);
        }

        return data;
    }
}

그럼 파일로 저장할 때도 다음과 같이 변경해야 합니다.

{
    BinaryOctaveFile octave = BinaryOctaveFile.Read(@"test_half_stereo.pcm");
    int[] data = octave.PCMDataAsInt32s();

    byte[] buffer = new byte[data.Length * 4];
    Buffer.BlockCopy(data, 0, buffer, 0, buffer.Length);

    // Octave에서 생성한 데이터 유형: sample_rate = 44100
    //                              channels = 2
    // 데이터를 4바이트로 변환:       bits_per_sample = 32
    WaveFile.Create(@"C:\Temp\test_half_stereo.wav", 44100, 32, 2, buffer);
}

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




그나저나, 지난 글에 소개만 했던 WaveFile.Create 메서드는 어쩌면 WAVE 헤더를 읽을 때 사용했던 Marshal.PtrToStructure 호출에 대해,

private WaveHeader structFromBytes(byte[] buffer)
{
    GCHandle pData = GCHandle.Alloc(buffer, GCHandleType.Pinned);
    WaveHeader instance = Marshal.PtrToStructure<WaveHeader>(pData.AddrOfPinnedObject());
    pData.Free();

    return instance;
}

그와 짝을 이루는 Marshal.StructureToPtr로 처리할 수도 있었는데요, 아쉽게도 예외 사항이 하나 있어 그렇게 처리할 수는 없었습니다. 왜냐하면 ChunkId와 같은 문자열 필드가,

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
public struct WaveHeader
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
    public string ChunkId;

    // ...[생략]...
}

크기는 4로 정해져 있지만, null을 포함하지 않는 크기이기 때문입니다. 그래서 저 필드에 "RIFF"가 들어 있는 것을 Marshal.StructureToPtr로 쓰게 되면 4바이트라는 제약에 null 처리까지 돼 "RIF\0"으로 직렬화되는 문제가 있습니다.

어쩔 수 없습니다. ^^ Marshal.StructureToPtr에 대해서는 CustomMarshaler도 동작하지 않기 때문에 직접 출력을 제어해야 합니다. 그런 이유로, 결국 일일이 헤더 필드 하나하나 쓰도록 만들게 된 것입니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 4/16/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 151  152  153  [154]  155  156  157  158  159  160  161  162  163  164  165  ...
NoWriterDateCnt.TitleFile(s)
1203정성태12/21/201125634제니퍼 .NET: 18. MEF가 적용된 ASP.NET 웹 사이트를 제니퍼 닷넷으로 모니터링 해본 결과! [6]
1202정성태12/21/201126107오류 유형: 144. The database '...' cannot be opened because it is version 661.
1201정성태12/14/201141151디버깅 기술: 47. .NET Reflector를 이용한 "소스 코드가 없는" 어셈블리 디버깅 [4]
1200정성태12/11/201127042디버깅 기술: 46. Windbg 확장 DLL 만들기 (2) - Debugger Extension API 사용파일 다운로드1
1199정성태12/11/201128378VC++: 55. JNI DLL 컴파일 시 x86과 x64의 Export된 함수의 이름이 왜 다를까요? [2]파일 다운로드1
1198정성태12/9/201132208디버깅 기술: 45. Windbg 확장 DLL 만들기 (1) - 스레드를 강제 종료시키는 명령어 [2]파일 다운로드1
1197정성태12/9/201129990.NET Framework: 282. Shader 강좌와 함께 배워보는 XNA Framework (2) - RenderMonkey의 Shader/Model 파일 연동파일 다운로드2
1196정성태12/9/201133172.NET Framework: 281. Shader 강좌와 함께 배워보는 XNA Framework (1) - 기초 프로그램 구조 [3]파일 다운로드2
1195정성태12/8/201147812오류 유형: 143. DXSDK_Jun10.exe 설치 시 "Error Code: S1023" 오류 해결하는 방법 [4]
1194정성태12/8/201135602개발 환경 구성: 137. Visual C++ 런타임 구성요소에 대한 디버그 버전 설치하는 방법
1193정성태12/8/201122697오류 유형: 142. Windows Phone SDK 7.1 설치 시 Expression Blend 제거를 요구하는 경우
1192정성태12/8/201125736개발 환경 구성: 136. Windows 7 SP1의 IIS에서 사용자 프로파일을 로드하는 방법
1191정성태12/6/201126838.NET Framework: 280. MVC3에서 JavaScriptSerializer 재정의하는 방법파일 다운로드1
1190정성태12/6/201130046오류 유형: 141. Visual C++ 컴파일 오류 - error C2275: 'xxxxx' : illegal use of this type as an expression [1]
1189정성태12/6/201137082VS.NET IDE: 70. Visual Studio에서 프로젝트 로드가 안된다면?
1188정성태12/3/201126186개발 환경 구성: 135. 마이크로소프트 TFS 호스팅 서비스 - Preview [3]
1187정성태12/2/201130839개발 환경 구성: 134. Robocopy 오류 및 종료 코드
1186정성태12/1/201132724.NET Framework: 279. WPF - 그리기 성능 및 Blurring 문제파일 다운로드1
1185정성태11/29/201123405.NET Framework: 278. WPF - Content의 Changed 이벤트에 해당하는게 뭔가요?파일 다운로드1
1184정성태11/29/201126216.NET Framework: 277. F#과 WPF가 어울리지 못하는 근본적인 이유 [2]
1183정성태11/26/201121737오류 유형: 140. Visual Studio 2010 - Floating된 에디트 윈도우가 사라지지 않는 경우 [2]
1182정성태11/25/201157439.NET Framework: 276. 중복 없는 숫자를 랜덤으로 배열하는 방법 [5]파일 다운로드1
1181정성태11/24/201127928디버깅 기술: 44. windbg의 mscordacwks DLL 로드 문제
1180정성태11/23/201137713.NET Framework: 275. 레지스트리 등록 및 Interop DLL 없이 COM 개체 사용하는 방법 [2]파일 다운로드1
1179정성태11/22/201128303.NET Framework: 274. ReaderWriterLockSlim은 언제 쓰는 걸까요? [4]파일 다운로드1
1178정성태11/19/201124828.NET Framework: 273. 설치된 .NET 버전에 민감한 코드를 포함하는 경우, 다중으로 어셈블리를 만들어야 할까요?파일 다운로드1
... 151  152  153  [154]  155  156  157  158  159  160  161  162  163  164  165  ...