Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (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# - PCM 오디오 데이터를 연속(Streaming) 재생 (Windows Multimedia)

지난 글까지 해서 기본적인 사운드 파일 재생을 할 수 있게 되었는데요, 현실적으로 아직 해결되지 못한 사례가 하나 있습니다. 보통 짧은 음악 파일은 그냥 메모리에 들고 있으면 되지만, 가령 영화 정도 되는 분량의 데이터라면 단순히 메모리에 들고 있는 식으로 처리하는 것은 부담스러울 수밖에 없습니다.

즉, 소리 데이터를 적절하게 끊어서 재생해야 하는데요, 이에 대한 고민이 담긴 글이 바로 아래의 2개 내용입니다.

5.큰 파일 재생
; http://www.soen.kr/lecture/library/waveform/5.htm

6. 콜백
; http://www.soen.kr/lecture/library/waveform/6.htm

가볍게 한번 훑어보시면 저작자가 하는 이야기가 무엇인지 쉽게 이해가 되실 것입니다. 결국, 우리가 알아야 하는 방법은 7번 글에 나옵니다.

7.멀티 헤더
; http://www.soen.kr/lecture/library/waveform/7.htm




Windows Multimedia는 waveOutOpen으로 장치 핸들을 얻고, 하나의 WAVEHDR 구조체가 담고 있는 데이터를 waveOutPrepareHeader/waveOutWrite/waveOutUnprepareHeader로 출력을 하는 구조입니다.

따라서, 방법은 간단합니다. 그냥 연이어서 음이 끊기지 않을 정도만 WAVEHDR 구조체를 지속적으로 장치에 전달하기만 하면 되는 것입니다. 따라서, 그 정도의 WAVEHDR에 채울 데이터만 메모리에 읽어서 처리하면 굳이 음악 파일 하나를 통째로 로드할 필요가 없습니다.

그런데, C#에서 이 처리를 하려면 우선 해결해야 할 과제가 하나 있습니다. 지난 글에서 실습했을 때, 데이터 버퍼를 Native 영역에 전달하기 위해 fixed 영역을 지정했는데요, 이것을 없애야 합니다. 바로 이것을 위해 GCHandle을 사용할 수 있습니다.

try
{
    do
    {
        WaveHeader waveHeader = new WaveHeader();
        waveHeader.bufferLength = wf.PCM.Length;

        GCHandle handle = GCHandle.Alloc(wf.PCM, GCHandleType.Pinned);

        waveHeader.dataBuffer = new nint(ptr);

        result = NativeMethods.waveOutPrepareHeader(hWaveOut, &waveHeader, WaveHeader.Size);
        // ...[생략]...
        result = NativeMethods.waveOutWrite(hWaveOut, &waveHeader, WaveHeader.Size);
        // ...[생략]...
        result = NativeMethods.waveOutUnprepareHeader(hWaveOut, &waveHeader, WaveHeader.Size);
    } while (false);
}
finally
{
    NativeMethods.waveOutClose(hWaveOut);
}

물론, Free도 해줘야 합니다. 마침 Free를 위해 가장 좋은 위치가 있는데요, 바로 waveOutOpen에 전달했던 waveOutCallbackFunc입니다.

MmResult result = NativeMethods.waveOutOpen(out IntPtr hWaveOut, -1, &format, waveOutCallbackFunc,
    IntPtr.Zero, WaveInOutOpenFlags.CallbackFunction);

[UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvStdcall) })]
static void waveOutCallback(IntPtr hWaveOut, WaveMessage uMsg, IntPtr dwInstance, IntPtr wavhdr, IntPtr dwReserved)
{
    System.Console.WriteLine($"waveOutCallback: {uMsg}");
}

소리 재생이 완료되면 WaveMessage uMsg 인자에 WaveMessage.WaveOutDone이 담겨져 날아오게 되고, 따라서 이 시점에 4번째 IntPtr wavhdr로 넘어온 포인터를 사용하면 됩니다. 물론 현재의 WAVEHEADER로는 안 되고 GCHandle을 포함하도록 확장한 다음,

[StructLayout(LayoutKind.Sequential)]
public struct WaveHeader
{
    // ...[생략]...

    public GCHandle GcHandle;
}

값을 설정하는 코드도 추가하면,

try
{
    do
    {
        WaveHeader waveHeader = new WaveHeader();
        waveHeader.bufferLength = wf.PCM.Length;

        GCHandle handle = GCHandle.Alloc(wf.PCM, GCHandleType.Pinned);
        waveHeader.GcHandle = handle;

        waveHeader.dataBuffer = new nint(ptr);

        result = NativeMethods.waveOutPrepareHeader(hWaveOut, &waveHeader, WaveHeader.Size);
        // ...[생략]...
        result = NativeMethods.waveOutWrite(hWaveOut, &waveHeader, WaveHeader.Size);
        // ...[생략]...
        result = NativeMethods.waveOutUnprepareHeader(hWaveOut, &waveHeader, WaveHeader.Size);
    } while (false);
}
finally
{
    NativeMethods.waveOutClose(hWaveOut);
}

이제 콜백 함수에서 안전하게 Free 처리를 할 수 있습니다.

[UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvStdcall) })]
static void waveOutCallback(IntPtr hWaveOut, WaveMessage uMsg, IntPtr dwInstance, IntPtr wavhdr, IntPtr dwReserved)
{
    if (uMsg == WaveMessage.WaveOutDone)
    {
        WaveHeader header = Marshal.PtrToStructure<WaveHeader>(wavhdr);
        header.GcHandle.Free();
    }
        
    System.Console.WriteLine($"waveOutCallback: {uMsg}");
}

그런데, 저 단계에서 waveOutUnprepareHeader를 해서 WAVEHEADER도 해제하는 것이 좋지 않을까요? 이 작업은, 문서의 경고에 따라 하지 않는 것이 좋습니다.

waveOutProc callback function
; https://learn.microsoft.com/en-us/previous-versions/dd743869(v=vs.85)

Applications should not call any system-defined functions from inside a callback function, except for EnterCriticalSection, LeaveCriticalSection, midiOutLongMsg, midiOutShortMsg, OutputDebugString, PostMessage, PostThreadMessage, SetEvent, timeGetSystemTime, timeGetTime, timeKillEvent, and timeSetEvent. Calling other wave functions will cause deadlock.


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




자, 그럼 완료 처리를 대행하는 것과, 일정량의 소리 데이터를 (해제는 어차피 callback에서 할 것이므로) waveOutPrepareHeader/waveOutWrite 단계로 밀어 넣어주기만 하면 됩니다.

그런데, 어느 정도로 소리 데이터를 보내주는 것이 좋을까요?

아마도, 2초 정도의 분량만 항상 마련이 되어 있으면 좋지 않을까 싶습니다. 즉 1초씩 끊어서 2번의 waveOutPrepareHeader/waveOutWrite로 넣어주고, Done callback이 발생하게 되면 첫 번째 1초의 WAVEHEADER 데이터가 모두 연주된 것이고 현재 두 번째 1초 데이터가 연주를 시작했을 것이므로 곧바로 waveOutPrepareHeader/waveOutWrite를 이용해 추가로 1초 분량의 WAVEHEADER를 추가해 주는 식일 것입니다.

반면, 컴퓨터 세계에서 2초는 너무 길 수도 있습니다. 테스트에 따라 100ms 정도로 2번 정도의 예비 데이터만 보내도 아마 잘 동작하는 경우도 많을 것입니다. 그런 부분은 적절하게 환경에 맞게, 경험으로 조정하면 될 듯합니다. (혹시, 현업에 계신 분들은 어떤 식으로 처리하는지 덧글로 조언 부탁드립니다. ^^)

NAudio를 사용해 위와 같은 방식의 재생을 구현하려면 아마도 다음의 코드를 참고하면 될 듯합니다.

https://github.com/teetow/teevegas/blob/master/src/NAudio/Wave/WaveProviders/BufferedWaveProvider.cs

https://github.com/naudio/NAudio

https://markheath.net/post/how-to-play-back-streaming-mp3-using




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







[최초 등록일: ]
[최종 수정일: 5/2/2024]

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

비밀번호

댓글 작성자
 




... 61  62  63  64  65  66  67  68  69  70  71  72  [73]  74  75  ...
NoWriterDateCnt.TitleFile(s)
12112정성태1/12/202016832오류 유형: 589. PowerShell - 원격 Invoke-Command 실행 시 "WinRM cannot complete the operation" 오류 발생
12111정성태1/12/202020627디버깅 기술: 155. C# - KernelMemoryIO 드라이버를 이용해 실행 프로그램을 숨기는 방법(DKOM: Direct Kernel Object Modification) [16]파일 다운로드1
12110정성태1/11/202020033디버깅 기술: 154. Patch Guard로 인해 블루 스크린(BSOD)가 발생하는 사례 [5]파일 다운로드1
12109정성태1/10/202016711오류 유형: 588. Driver 프로젝트 빌드 오류 - Inf2Cat error -2: "Inf2Cat, signability test failed."
12108정성태1/10/202017530오류 유형: 587. Kernel Driver 시작 시 127(The specified procedure could not be found.) 오류 메시지 발생
12107정성태1/10/202018748.NET Framework: 877. C# - 프로세스의 모든 핸들을 열람 - 두 번째 이야기
12106정성태1/8/202019744VC++: 136. C++ - OSR Driver Loader와 같은 Legacy 커널 드라이버 설치 프로그램 제작 [1]
12105정성태1/8/202018223디버깅 기술: 153. C# - PEB를 조작해 로드된 DLL을 숨기는 방법
12104정성태1/7/202019477DDK: 9. 커널 메모리를 읽고 쓰는 NT Legacy driver와 C# 클라이언트 프로그램 [4]
12103정성태1/7/202022610DDK: 8. Visual Studio 2019 + WDK Legacy Driver 제작- Hello World 예제 [1]파일 다운로드2
12102정성태1/6/202018886디버깅 기술: 152. User 권한(Ring 3)의 프로그램에서 _ETHREAD 주소(및 커널 메모리를 읽을 수 있다면 _EPROCESS 주소) 구하는 방법
12101정성태1/5/202019251.NET Framework: 876. C# - PEB(Process Environment Block)를 통해 로드된 모듈 목록 열람
12100정성태1/3/202016684.NET Framework: 875. .NET 3.5 이하에서 IntPtr.Add 사용
12099정성태1/3/202019565디버깅 기술: 151. Windows 10 - Process Explorer로 확인한 Handle 정보를 windbg에서 조회 [1]
12098정성태1/2/202019317.NET Framework: 874. C# - 커널 구조체의 Offset 값을 하드 코딩하지 않고 사용하는 방법 [3]
12097정성태1/2/202017431디버깅 기술: 150. windbg - Wow64, x86, x64에서의 커널 구조체(예: TEB) 구조체 확인
12096정성태12/30/201919990디버깅 기술: 149. C# - DbgEng.dll을 이용한 간단한 디버거 제작 [1]
12095정성태12/27/201921736VC++: 135. C++ - string_view의 동작 방식
12094정성태12/26/201919456.NET Framework: 873. C# - 코드를 통해 PDB 심벌 파일 다운로드 방법
12093정성태12/26/201919079.NET Framework: 872. C# - 로딩된 Native DLL의 export 함수 목록 출력파일 다운로드1
12092정성태12/25/201917730디버깅 기술: 148. cdb.exe를 이용해 (ntdll.dll 등에 정의된) 커널 구조체 출력하는 방법
12091정성태12/25/201920146디버깅 기술: 147. pdb 파일을 다운로드하기 위한 symchk.exe 실행에 필요한 최소 파일 [1]
12090정성태12/24/201920191.NET Framework: 871. .NET AnyCPU로 빌드된 PE 헤더의 로딩 전/후 차이점 [1]파일 다운로드1
12089정성태12/23/201919116디버깅 기술: 146. gflags와 _CrtIsMemoryBlock을 이용한 Heap 메모리 손상 여부 체크
12088정성태12/23/201918109Linux: 28. Linux - 윈도우의 "Run as different user" 기능을 shell에서 실행하는 방법
12087정성태12/21/201918568디버깅 기술: 145. windbg/sos - Dictionary의 entries 배열 내용을 모두 덤프하는 방법 (do_hashtable.py) [1]
... 61  62  63  64  65  66  67  68  69  70  71  72  [73]  74  75  ...