Microsoft MVP성태의 닷넷 이야기
닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력 [링크 복사], [링크+제목 복사],
조회: 9338
글쓴 사람
정성태 (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)
999정성태2/28/201146589개발 환경 구성: 108. RemoteFX - Windows 7 가상 머신에서 DirectX 9c 환경을 제공 [5]
998정성태2/27/201120289Team Foundation Server: 42. TFS Application-Tier만 재설치
996정성태2/12/201137739디버깅 기술: 35. windbg - 분석 예: 시작하자마자 비정상 종료하는 프로세스 - NullReferenceException
995정성태2/11/201156258.NET Framework: 205. 코드(C#)를 통한 풀 덤프 만드는 방법 [4]
994정성태2/10/201136217디버깅 기술: 34. Windbg - ERROR: Unable to load DLL mscordacwks_x86_x86_2.0.50727.4200.dll, Win32 error 0n2 [1]
993정성태2/10/201128526개발 환경 구성: 107. 하나의 WPF 프로젝트를 WinExe / Library로 빌드하는 방법
992정성태10/15/201129382개발 환경 구성: 106. VSS(Volume Shadow Service)를 이용한 Hyper-V VM 백업/복원 [2]
991정성태2/6/201148857개발 환경 구성: 105. 풀 덤프 파일을 남기는 방법 [4]
990정성태2/2/201134003개발 환경 구성: 104. Visual C++ Custom Build Tool 사용예 [1]파일 다운로드1
989정성태2/1/201130518개발 환경 구성: 103. DOS batch - 동기 방식으로 원격 서비스 제어
988정성태1/30/201126708개발 환경 구성: 102. MSBuild - DefineConstants에 다중 전처리 값 설정
987정성태1/29/201139863디버깅 기술: 33. PDB Symbol 로드 오류 - Cannot find or open the PDB file. [2]
986정성태1/26/201131316.NET Framework: 204. 분리된 ThreadPool 사용 - Smart Thread Pool
985정성태1/25/201127951디버깅 기술: 32. 인증서로 서명된 닷넷 어셈블리의 실행 지연 현상
984정성태1/25/201122660개발 환경 구성: 101. SharePoint 2010 - Form Design
983정성태1/23/201127725제니퍼 .NET: 15. 눈으로 확인하는 maxWorkerThreads, minFreeThreads 설정값 [1]
982정성태1/22/201125019개발 환경 구성: 100. SharePoint 2010 - iPad 친화적인 게시판 만들기 (사용자 지정 목록) [1]
981정성태1/19/201120890개발 환경 구성: 99. SharePoint 2010 - 웹 애플리케이션 생성 시 고려해야 할 점. [1]
980정성태1/19/201132401개발 환경 구성: 98. SharePoint 2010 - Office Web Apps 설치
979정성태1/18/201125006개발 환경 구성: 97. SharePoint 2010 팀 사이트 구성
978정성태1/16/201131972.NET Framework: 203. VPN 자동 연결 및 Router 설정 추가
977정성태1/12/201131310개발 환경 구성: 96. SharePoint 2010 설치 [5]
976정성태1/11/201153957오류 유형: 111. IIS - 500.19 오류 (0x8007000d)
975정성태1/10/201128220.NET Framework: 202. CLR JIT 컴파일러가 생성한 기계어 코드 확인하는 방법 [3]파일 다운로드1
974정성태1/8/201126989.NET Framework: 201. 윈폼 TreeView - Bold 폰트 설정 후 텍스트가 잘리는 문제 [1]파일 다운로드1
973정성태1/7/201126321.NET Framework: 200. IIS Metabase와 ServerManager 개체 활용파일 다운로드1
... 151  152  153  154  155  156  157  158  159  160  161  [162]  163  164  165  ...