성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - WAV 파일의 PCM 데이터 생성 및 출력</h1> <p> 지난 글에서,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - WAV 기본 파일 포맷 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/13596'>https://www.sysnet.pe.kr/2/0/13596</a> </pre> <br /> WAV 파일의 간단한 헤더 구조를 봤는데요, 그 덕분에 적절한 PCM 데이터만 있다면 금방 wav 파일을 만들 수 있습니다. 실제로 해볼까요? ^^<br /> <br /> 우선, 적당한 PCM 데이터를 생성해야 하는데요, 사실 PCM은 단순히 음의 크기를 나타내는 배열 형식의 자료 구조이기 때문에 간단하게 <a target='tab' href='https://www.sysnet.pe.kr/2/0/11622'>Octave</a>에서 다음과 같이 처리할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > # doremi.m # <a target='tab' href='https://docs.octave.org/v4.0.0/Two_002dDimensional-Plots.html'>https://docs.octave.org/v4.0.0/Two_002dDimensional-Plots.html</a> 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 </pre> <br /> 22Khz 샘플링으로 "도, 레, 미, 파, 솔, 라, 시, 도"에 해당하는 주파수의 데이터를 각각 1초씩 생성해 sound 변수에 넣었습니다. 일부 데이터를 plot으로 그려보면 단순히 -1~+1 사이에서 주파수에 따라 요동치는 간단한 형식입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > >> plot(t(1:200), sound(1:200)); </pre> <br /> <img alt='octave_cosine_wave_1.png' src='/SysWebRes/bbs/octave_cosine_wave_1.png' /><br /> <br /> 직접 소리를 내 확인하는 것도 가능한데요, <a target='tab' href='https://octave.sourceforge.io/octave/function/soundsc.html'>soundsc</a> 함수를 사용하면 ^^ 도레미... 음이 1초 간격으로 들릴 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > >> doremi; sound = [](0x0) >> <span style='color: blue; font-weight: bold'>soundsc(sound, sample_rate)</span> </pre> <br /> 자, 그럼 이 데이터를 binary 포맷으로 저장하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > >> <span style='color: blue; font-weight: bold'>save -binary test.pcm sound</span> </pre> <br /> <a target='tab' href='https://www.sysnet.pe.kr/2/0/13324'>지난 글에 소개한 Octave Reader</a>를 통해 C#에서 PCM 데이터 파일을 읽어들입니다. 단지, 현재 Octave가 출력한 PCM 데이터가 -1~+1 범위의 double 값이므로, wav 파일에서 PCM 데이터로 동작하기 위해서는 이 값을 정수 형태로 바꿔야 합니다. 이를 위해 다음과 같이 보조 메서드를 하나 추가해 줍니다. (아니면, 애당초 Octave 측에서 정수로 바뀐 데이터를 출력해도 됩니다.)<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // <a target='tab' href='https://www.sysnet.pe.kr/2/0/13324#src'>https://www.sysnet.pe.kr/2/0/13324#src</a> 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] * <span style='color: blue; font-weight: bold'>32767</span>); } return data; } } </pre> <br /> 자, 그럼 이 메서드로 다음과 같이 Octave가 생성한 PCM 데이터를 읽어내 <a target='tab' href='https://www.sysnet.pe.kr/2/0/13596#wav_create'>WAV 헤더 파일과 함께 출력해 주는 Create 메서드</a>에 전달하면 wav 파일이 만들어집니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > { <span style='color: blue; font-weight: bold'>BinaryOctaveFile octave = BinaryOctaveFile.Read(@"test.pcm"); short[] data = octave.PCMDataAsShorts();</span> 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 <span style='color: blue; font-weight: bold'>WaveFile.Create(@"C:\Temp\test.wav", 22050, 16, 1, buffer);</span> } </pre> <br /> 이렇게 저장한 파일이 정상적인지, 다시 WaveFile로 읽어들여 포맷을 체크할 수 있습니다. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > { 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"; <span style='color: blue; font-weight: bold'>WaveFile wf = new WaveFile(waveFilePath); Console.WriteLine(wf.Header);</span> } /* 출력 결과 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 */ </pre> <br /> 헤더 규약에 맞게 wav 파일을 생성했으니, 당연히 읽기 시에도 출력이 정상적으로 나오고, 게다가 Media Player 등을 이용해 test.wav 파일을 재생해 보면 ^^ 소리가 잘 들립니다.<br /> <br /> <hr style='width: 50%' /><br /> <a name='stereo_pcm'></a> <br /> 하는 김에, 스테레오용 PCM도 생성해 볼까요? 사실 Mono는 단일 음이 연속해서 Byte[] 버퍼에 채워지는 형식이지만,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [M1 M2 M3 ...] </pre> <br /> Stereo 형식이면 Left Channel과 Right Channel 음이 번갈아서 버퍼에 채워지게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [L1 R1 L2 R2 L3 R3 ...] </pre> <br /> 따라서, 이전의 도레미... 음을 다음과 같이 "<a target='tab' href='https://docs.octave.org/v6.3.0/Special-Utility-Matrices.html#XREFrepelem'>repelem</a>"을 이용해 (이번엔 44Khz의) 동일한 음으로 Left/Right Channel을 생성할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>sample_rate = 44100;</span> 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); <span style='color: blue; font-weight: bold'>s = repelem(s, 2);</span> sound = [sound s]; endfor </pre> <br /> 역시 이렇게 생성한 sound 데이터를 test_stereo.pcm 파일로 저장하고 wav 헤더를 추가해 주면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > { 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); } </pre> <br /> 동작하는 test_stereo.wav 음악 파일을 만들 수 있습니다.<br /> <br /> 하다 보니 재미있군요. ^^ 그럼 이번에는 Stereo지만, Right Channel 음을 무음으로 만들어 보겠습니다. 이를 위해 Octave에서는 2번째 데이터에 대해 0을 만들 수 있도록 아래와 같은 조작을 추가합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > sample_rate = 44100; frequency = [262 294 330 349 392 440 494 523]; sound = [] t = 0:1/sample_rate:1; <span style='color: blue; font-weight: bold'>even_pos = 2:2:size(t)(2)*2;</span> for doremi_freq = frequency s = cos(2*pi*doremi_freq*t); s = repelem(s, 2); <span style='color: blue; font-weight: bold'>s(even_pos) = 0;</span> sound = [sound s]; endfor </pre> <br /> 재생해 보면, 의도했던 대로 오른쪽 이어폰에서 소리가 발생하지 않습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 마지막으로, Octave의 PCM 데이터는 -1~+1 사이의 double 값을 가진, 어찌 보면 정규화된 형식이기 때문에 이 값의 bitsPerSample을 정하는 것은 읽어들이는 쪽에서 알아서 하면 됩니다. 앞선 예제 코드에서는 해당 데이터를 2바이트로 복원했었는데요, 당연히 곱해주는 값을 바꿔 4바이트로 하는 것도 가능합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System.Text; namespace Octave; public struct BinaryOctaveFile { // ...[생략]... <span style='color: blue; font-weight: bold'>public int[] PCMDataAsInt32s()</span> { int dataCount = Rows * Columns; int[] data = new int[dataCount]; for (int i = 0; i < dataCount; i++) { data[i] = (int)(Data[i] * <span style='color: blue; font-weight: bold'>2147483647</span>); } return data; } } </pre> <br /> 그럼 파일로 저장할 때도 다음과 같이 변경해야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > { 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, <span style='color: blue; font-weight: bold'>32</span>, 2, buffer); } </pre> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=2158&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그나저나, 지난 글에 소개만 했던 WaveFile.Create 메서드는 어쩌면 WAVE 헤더를 읽을 때 사용했던 <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.ptrtostructure'>Marshal.PtrToStructure</a> 호출에 대해,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > private WaveHeader structFromBytes(byte[] buffer) { GCHandle pData = GCHandle.Alloc(buffer, GCHandleType.Pinned); <span style='color: blue; font-weight: bold'>WaveHeader instance = Marshal.PtrToStructure<WaveHeader>(pData.AddrOfPinnedObject());</span> pData.Free(); return instance; } </pre> <br /> 그와 짝을 이루는 <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.structuretoptr'>Marshal.StructureToPtr</a>로 처리할 수도 있었는데요, 아쉽게도 예외 사항이 하나 있어 그렇게 처리할 수는 없었습니다. 왜냐하면 ChunkId와 같은 문자열 필드가,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)] public struct WaveHeader { [MarshalAs(UnmanagedType.ByValTStr, <span style='color: blue; font-weight: bold'>SizeConst = 4</span>)] public string ChunkId; // ...[생략]... } </pre> <br /> 크기는 4로 정해져 있지만, null을 포함하지 않는 크기이기 때문입니다. 그래서 저 필드에 "RIFF"가 들어 있는 것을 Marshal.StructureToPtr로 쓰게 되면 4바이트라는 제약에 null 처리까지 돼 "RIF\0"으로 직렬화되는 문제가 있습니다.<br /> <br /> 어쩔 수 없습니다. ^^ <a target='tab' href='https://www.sysnet.pe.kr/2/0/13204#limit'>Marshal.StructureToPtr에 대해서는 CustomMarshaler도 동작하지 않기</a> 때문에 직접 출력을 제어해야 합니다. 그런 이유로, 결국 일일이 헤더 필드 하나하나 쓰도록 만들게 된 것입니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1454
(왼쪽의 숫자를 입력해야 합니다.)