Microsoft MVP성태의 닷넷 이야기
닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력 [링크 복사], [링크+제목 복사],
조회: 10090
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 91  92  93  94  95  96  97  98  99  100  101  102  [103]  104  105  ...
NoWriterDateCnt.TitleFile(s)
11392정성태12/7/201718944개발 환경 구성: 341. openSUSE에 닷넷 코어 설치
11391정성태12/7/201721964개발 환경 구성: 340. WSL을 이용해 윈도우 PC 1대에서 openSUSE 응용 프로그램을 Visual Studio로 개발하는 방법 [1]
11390정성태12/7/201730859개발 환경 구성: 339. WSL을 이용해 윈도우 PC 1대에서 Linux 응용 프로그램을 Visual Studio로 개발하는 방법 [6]
11389정성태12/7/201719513오류 유형: 440. .NET Core 오류 - 0x80131620 Unable to load DLL 'libuv'
11388정성태12/6/201723326개발 환경 구성: 338. WSL 또는 Ubuntu에 닷넷 코어 설치 [3]
11387정성태12/6/201723283오류 유형: 439. 이벤트 로그 - Data Sharing Service 서비스의 %%3239247874 오류 메시지
11386정성태12/5/201719248오류 유형: 438. Hyper-V - '...' failed to add device 'Virtual CD/DVD Disk'
11385정성태12/5/201732396VC++: 121. DXGI를 이용한 윈도우 화면 캡처 소스 코드(Visual C++) [16]파일 다운로드1
11384정성태12/5/201721806오류 유형: 437. Visual C++ - Cannot open include file: 'SDKDDKVer.h'
11383정성태12/4/201724523디버깅 기술: 110. 비동기 코드 실행 중 예외로 인한 ASP.NET 프로세스 비정상 종료 현상 [1]
11382정성태12/4/201723191오류 유형: 436. System.Data.SqlClient.SqlException (0x80131904): Connection Timeout Expired 예외 발생 시 "[Pre-Login] initialization=48; handshake=1944;" 값의 의미
11381정성태11/30/201719802.NET Framework: 702. 한글이 포함된 바이트 배열을 나눈 경우 한글이 깨지지 않도록 다시 조합하는 방법(두 번째 이야기)파일 다운로드1
11380정성태11/30/201719858디버깅 기술: 109. windbg - (x64에서의 인자 값 추적을 이용한) Thread.Abort 시 대상이 되는 스레드를 식별하는 방법
11379정성태11/30/201719826오류 유형: 435. System.Web.HttpException - Session state has created a session id, but cannot save it because the response was already flushed by the application.
11378정성태11/29/201721673.NET Framework: 701. 한글이 포함된 바이트 배열을 나눈 경우 한글이 깨지지 않도록 다시 조합하는 방법 [1]파일 다운로드1
11377정성태11/29/201721228.NET Framework: 700. CommonOpenFileDialog 사용 시 사용자가 선택한 파일 목록을 구하는 방법 [3]파일 다운로드1
11376정성태11/28/201725821VS.NET IDE: 123. Visual Studio 편집기의 \r\n (crlf) 개행을 \n으로 폴더 단위로 설정하는 방법
11375정성태11/28/201719780오류 유형: 434. Visual Studio로 ASP.NET 디버깅 중 System.Web.HttpException - Could not load type 오류
11374정성태11/27/201725613사물인터넷: 14. 라즈베리 파이 - (윈도우의 NT 서비스처럼) 부팅 시 시작하는 프로그램 설정 [1]
11373정성태11/27/201724668오류 유형: 433. Raspberry Pi/Windows 다중 플랫폼 지원 컴파일 관련 오류 기록
11372정성태11/25/201727284사물인터넷: 13. 윈도우즈 사용자를 위한 라즈베리 파이 제로 W 모델을 설정하는 방법 [4]
11371정성태11/25/201721087오류 유형: 432. Hyper-V 가상 스위치 생성 시 Failed to connect Ethernet switch port 0x80070002 오류 발생
11370정성태11/25/201721257오류 유형: 431. Hyper-V의 Virtual Switch 생성 시 "External network" 목록에 특정 네트워크 어댑터 항목이 없는 경우
11369정성태11/25/201722999사물인터넷: 12. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 키보드 및 마우스로 쓰는 방법 (절대 좌표, 상대 좌표, 휠) [1]
11368정성태11/25/201728452.NET Framework: 699. UDP 브로드캐스트 주소 255.255.255.255와 192.168.0.255의 차이점과 이를 고려한 C# UDP 서버/클라이언트 예제 [2]파일 다운로드1
11367정성태11/25/201728937개발 환경 구성: 337. 윈도우 운영체제의 route 명령어 사용법
... 91  92  93  94  95  96  97  98  99  100  101  102  [103]  104  105  ...