성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Roll A Lisp In C - Reading ; https...
[정성태] 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...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
글쓰기
제목
이름
암호
전자우편
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 파일 포맷 + LIST 헤더</h1> <p> <a target='tab' href='https://www.sysnet.pe.kr/2/0/1210'>ffmpeg를 이용</a>하면 미디어 파일의 포맷을 다양하게 바꿀 수 있습니다. 일례로 오디오 파일의 경우, 다채널 오디오에서 특정 채널의 음을 제거하는 것도 가능한데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Manipulating audio channels with ffmpeg ; <a target='tab' href='https://img.sauf.ca/pictures/2017-11-08/dba6fb7be0442f4a76742978fa21766a.pdf'>https://img.sauf.ca/pictures/2017-11-08/dba6fb7be0442f4a76742978fa21766a.pdf</a> Manipulating audio channels ; <a target='tab' href='https://trac.ffmpeg.org/wiki/AudioChannelManipulation'>https://trac.ffmpeg.org/wiki/AudioChannelManipulation</a> </pre> <br /> 가령 이전에 소개한 글에서 <a target='tab' href='https://www.sysnet.pe.kr/2/0/13597#stereo_pcm'>Octave를 이용해 생성한 Stereo wav 파일</a>을 다음과 같은 명령으로 한쪽 채널의 음을 소거할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // Left 채널 음 소거 ffmpeg -i test_stereo.wav -map_channel -1 -map_channel <span style='color: blue; font-weight: bold'>0.0.1</span> left_mute.wav </pre> <br /> 그런데 Right 채널 음 소거가 어렵군요. ^^; 단순히 map_channel 인자로는 잘 안되고 pan 인자로 했더니 만들어집니다. (혹시 이유를 아시는 분은 덧글 부탁드립니다. ^^)<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 이렇게 하면 안 되고, ffmpeg -i test_stereo.wav -map_channel -1 -map_channel <span style='color: blue; font-weight: bold'>0.0.0</span> right_mute.wav // 이렇게 해야 Stereo로 Right 채널 음 소거가 된 출력 파일이 만들어집니다. ffmpeg -i test_stereo.wav -af pan="stereo|c0=c0" right_mute.wav </pre> <br /> 암튼 이렇게 ffmpeg로 출력한 wav 파일을 <a target='tab' href='https://www.sysnet.pe.kr/2/0/13596'>지난번에 만든 프로그램</a>으로 출력해 보면 데이터가 비정상적으로 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ChunkId: RIFF, FileSize: 1411302, TypeHeader: WAVE, FormatMarker: fmt , SubChunkSize: 16, AudioFormat: 1, Channels: 2, SampleRate: 44100, ByteRate: 176400, BlockAlign: 4, BitsPerSample: 16, <span style='color: blue; font-weight: bold'>DataChunkHeader: LIST, DataSize: 26</span> </pre> <br /> 생뚱맞게 LIST chunk가 나오는 바람에 이하 DataSize가 틀린 값이 나오는데요, 이에 대해 검색해 보면 다음과 같은 글이 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > List chunk (of a RIFF file) ; <a target='tab' href='https://www.recordingblogs.com/wiki/list-chunk-of-a-wave-file'>https://www.recordingblogs.com/wiki/list-chunk-of-a-wave-file</a> Wave file format ; <a target='tab' href='https://www.recordingblogs.com/wiki/wave-file-format'>https://www.recordingblogs.com/wiki/wave-file-format</a> </pre> <br /> 그러니까, 그 간단한 wav 포맷이 fmt, data 이외에도 Silent, Wave, list, Fact, Cue, Playlist, List, Sample, Instrument와 같은 서브 헤더들의 확장이 있었던 것입니다. ^^;<br /> <br /> 자, 그러면 정리해 볼까요? 우선 RIFF 헤더는 "Resource Interchange File Format"라는 말에서 의미하는 것처럼 Wave 파일을 위한 헤더는 아닙니다. 즉, Wave 이외에 다른 리소스도 포함할 수 있는 것인데, 이번 글에서는 RIFF 하위에 Wave만 있는 파일을 가정하고 설명하는 것입니다.<br /> <br /> 그리고 Wave 하위에는 일반적으로 fmt, data 헤더가 있지만 기타 다른 헤더들도 나올 수 있습니다. 따라서 다음과 같이 구조가 이뤄집니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // WAVE 하위의 포맷은 순서 없음 RIFF - WAVE - fmt + data + Silent + Wave list + Fact + Cue + Playlist + List + Sample - Instrument </pre> <br /> 이번 글에서는 저 모든 포맷을 해석할 수는 없고 이전에 해석한 fmt, data와 함께 List 헤더 해석을 추가하고 기타 헤더들은 포함됐어도 해석하지 않고 넘어가도록 만들겠습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 위의 설명을 이해했다면, 이제 더 이상 <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.ptrtostructure?view=net-8.0'>Marshal.PtrToStructure</a>을 이용한 파일 읽기가 불가능하다는 것을 알 수 있을 것입니다.<br /> <br /> 그래서 지난 글에서 작성한 읽기 및 클래스 구조를 모두 뜯어고쳐야 하는데요, 우선, RIFF/WAVE 구조에 맞게 WAVE에 포함되는 하위 헤더 중 우리가 해석할 List, Format, Data에 대해 각각 필드로 포함하도록 만듭니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public class WaveFile { ListChunk _list; FormatChunk _format; DataChunk _data; // ...[생략]... } </pre> <br /> 이후, 헤더의 Id에 따라 각각 하위 헤더에 읽기를 맡기는 식으로, 그리고 필요 없는 헤더는 그냥 넘어가는 식으로 해석하면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public class WaveFile { // ...[생략]... public static WaveFile Read(string filePath) { WaveFile wave = new WaveFile(); using (FileStream fs = File.OpenRead(filePath)) using (BinaryReader br = new BinaryReader(fs, Encoding.ASCII)) { string type = Encoding.ASCII.GetString(br.ReadBytes(4)); if (type != "RIFF") { throw new ApplicationException("RIFF signature not found"); } int remainSize = br.ReadInt32(); while (remainSize > 0) { int chunkReadSize = 0; string? chunkTypeId = br.ForwardAnsi4(ref chunkReadSize)?.ToLower(); switch (chunkTypeId) { case "wave": break; case "fmt ": wave._format = FormatChunk.Read(br, ref chunkReadSize); break; case "data": wave._data = DataChunk.Read(br, ref chunkReadSize); break; case "list": wave._list = ListChunk.Read(br, ref chunkReadSize); break; case "slnt": // Silent case "wavl": // Wave list case "fact": case "cue ": case "plst": // Playlist case "smpl": // sample case "inst": // Instrument { int chunkSize = br.ForwardInt32(ref chunkReadSize); br.ForwardBytes(chunkSize, ref chunkReadSize); } break; default: throw new ApplicationException($"Not supported sub chunk: {chunkTypeId}"); } remainSize -= chunkReadSize; } } return wave; } } </pre> <br /> 남은 작업은, 자신의 헤더에 맞는 데이터를 읽어들이는 코드를 작성하면 됩니다. 가령 "fmt "에 해당하는 헤더는 이렇게 구현합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > class FormatChunk { public CompressionCode CompressionCode; /* 1 is PCM */ public short Channels; public int SampleRate; public int ByteRate; /* (Sample Rate * BitsPerSample * Channels) / 8 */ public short BlockAlign; /* (BitsPerSample * Channels) / 8 */ public short BitsPerSample; public static FormatChunk Read(BinaryReader br, ref int totalRead) { FormatChunk format = new FormatChunk(); int chunkSize = br.ForwardInt32(ref totalRead); format.CompressionCode = (CompressionCode)br.ForwardUInt16(ref totalRead); if (format.CompressionCode != CompressionCode.Microsoft_PCM) { throw new Exception("Only supported for Microsoft PCM"); } format.Channels = br.ForwardInt16(ref totalRead); format.SampleRate = br.ForwardInt32(ref totalRead); format.ByteRate = br.ForwardInt32(ref totalRead); format.BlockAlign = br.ForwardInt16(ref totalRead); format.BitsPerSample = br.ForwardInt16(ref totalRead); if (chunkSize > 16) { short extraFormatBytes = br.ForwardInt16(ref totalRead); br.ForwardBytes(extraFormatBytes, ref totalRead); } return format; } public override string ToString() { return $"{CompressionCode}, {SampleRate} Hz, {Channels} channels, {BitsPerSample}"; } } </pre> <br /> 어렵지 않죠? ^^ 헤더 파싱 중에 주의해야 할 점이 있다면 LIST 헤더의 경우 key=value 쌍에서 value는 반드시 word-aligned라는 점입니다. 만약 "kevin"이라는 값을 줬으면 5바이트를 차지하지만 word-aligned이어야 하므로 6바이트로 길이를 맞춰야 합니다. (그 외 "data", "list" 헤더를 읽어들이는 코드는 첨부 파일을 참고하세요.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 자, 이렇게 개선한 Read를 이용하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > { string waveFilePath = @"C:\temp\right_mute.wav"; WaveFile? wf = WaveFile.Read(waveFilePath); Console.WriteLine(wf); } /* 출력 결과 Metadata: ISFT Lavf58.76.100 Microsoft_PCM, 44100 Hz, 2 channels, 16, duration: 00:00:08.00, 1411 kb/s */ </pre> <br /> 보는 바와 같이 ffmpeg가 만든 파일을 잘 해석하고 있습니다. 물론, 쓰기 기능도 INFO 헤더를 쓰는 것도 구현했으므로 다음과 같이 작성하면,<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); Dictionary<string, string> info = new Dictionary<string, string>() { { InfoKeys.ISFT, "sysnet" }, { InfoKeys.ICOP, "Copyright sysnet 2023" }, }; WaveFile.Write(@"C:\Temp\test_stereo.wav", 44100, 16, 2, buffer, info); } </pre> <br /> 이제 저렇게 생성한 파일을 ffprobe로 다음과 같이 확인할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > c:\temp> <span style='color: blue; font-weight: bold'>ffprobe -hide_banner test_stereo.wav</span> Input #0, wav, from 'test_stereo.wav': Metadata: <span style='color: blue; font-weight: bold'>encoder : sysnet copyright : Copyright sysnet 2023</span> Duration: 00:00:08.00, bitrate: 1411 kb/s Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 44100 Hz, 2 channels, s16, 1411 kb/s </pre> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=2159&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 위의 Sub Chunk 헤더 분석으로 "C:\Windows\Media\Alarm09.wav" 파일을 해보면 "cdif"라는 헤더가 나옵니다. 검색해 보면, 듣보잡 헤더인지 정보를 찾을 수가 없습니다. <br /> <br /> 아래의 문서에 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Resource Interchange File Format (RIFF) ; <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/xaudio2/resource-interchange-file-format--riff-'>https://learn.microsoft.com/en-us/windows/win32/xaudio2/resource-interchange-file-format--riff-</a> </pre> <br /> sub chunk의 규격이 "chunkID, chunkSize, data" 식으로 나온다고 하므로, chunkTypeId에 따른 코드를 단순히 default 처리해 넘어가도록 처리하는 것이 나을 수도 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > switch (chunkTypeId) { case "wave": break; case "fmt ": wave._format = FormatChunk.Read(br, ref chunkReadSize); break; case "data": wave._data = DataChunk.Read(br, ref chunkReadSize, wave._loadAllData); break; case "list": wave._list = ListChunk.Read(br, ref chunkReadSize); break; default: // "cdif", "slnt", "wavl", "fact", "cue ", "plst", "smpl", "inst" { int chunkSize = br.ForwardInt32(ref chunkReadSize); br.ForwardBytes(chunkSize, ref chunkReadSize); } break; } </pre> <br /> <hr style='width: 50%' /><br /> <br /> 가지고 있던 flac 파일을 ffmpeg를 이용해 wav 파일로 변환했더니 Microsoft_PCM이 아닌 포맷이 나옵니다. 바로 Extensible 포맷인데요, 다행히 아래의 문서를 보면 어렵지 않게 지원을 추가할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Extensible Wave-Format Descriptors ; <a target='tab' href='https://learn.microsoft.com/en-us/windows-hardware/drivers/audio/extensible-wave-format-descriptors'>https://learn.microsoft.com/en-us/windows-hardware/drivers/audio/extensible-wave-format-descriptors</a> </pre> <br /> 문서에 포함된 한 장의 그림에서 잘 설명하고 있는데,<br /> <br /> <img alt='waveformt_def_1.png' src='/SysWebRes/bbs/waveformt_def_1.png' /><br /> <br /> WAVEFORMATEXTENSIBLE은 기존 WAVEFORMATEX에서 3개의 필드가 더 추가된 유형입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > /* WAVEFORMATEXTENSIBLE structure (ksmedia.h) ; <a target='tab' href='https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ksmedia/ns-ksmedia-waveformatextensible'>https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ksmedia/ns-ksmedia-waveformatextensible</a> */ typedef struct { WAVEFORMATEX Format; union { WORD wValidBitsPerSample; WORD wSamplesPerBlock; WORD wReserved; } Samples; DWORD dwChannelMask; GUID SubFormat; } WAVEFORMATEXTENSIBLE, *PWAVEFORMATEXTENSIBLE; </pre> <br /> 단지, 여기서 만들고 있는 것은 PCM 데이터로 제약이 있으므로 SubFormat 필드를 체크해,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Media Type Identifiers for the Windows Media Format SDK ; <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/wmformat/media-type-identifiers'>https://learn.microsoft.com/en-us/windows/win32/wmformat/media-type-identifiers</a> </pre> <br /> "00000001-0000-0010-8000-00AA00389B71" (WMMEDIASUBTYPE_PCM) 값 유형에 대해서만 재생할 수 있도록 해야 합니다. 따라서 이를 반영해 다음과 같이 FormatChunk.Read 코드를 완성할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public static FormatChunk Read(BinaryReader br, ref int totalRead) { FormatChunk format = new FormatChunk(); // ...[생략]... if (chunkSize > 16) { format.cbSize = br.ForwardInt16(ref totalRead); <span style='color: blue; font-weight: bold'>if (format.CompressionCode == CompressionCode.Extensible) { format.SamplesValidBitsPerSample = br.ForwardInt16(ref totalRead); format.ChannelMask = br.ForwardInt32(ref totalRead); format.SubFormat = br.ForwardGuid(ref totalRead); if (format.SubFormat != WMMEDIASUBTYPE_PCM) { throw new ApplicationException("Extensible format PCM is supported only"); } }</span> else { br.ForwardBytes(format.cbSize, ref totalRead); } } return format; } </pre> <br /> <a target='tab' href='https://www.sysnet.pe.kr/2/0/13596'>지난 글</a>을 다룰 때만 해도 WAV 파일의 포맷이 우스워 보였는데... ^^;<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
3422
(왼쪽의 숫자를 입력해야 합니다.)