오디오 스트림 캐싱 개요

오디오 엔진에서 사용되는 스트림 캐싱 시스템을 살펴봅니다.

Choose your operating system:

Windows

macOS

Linux

오디오 스트림 캐싱(Audio stream caching) 은 오디오가 메모리에서 로드 및 릴리즈되는 방식을 크게 바꿉니다.

쿠킹 시간에 기능이 활성화되어 있는 경우 압축된 오디오 데이터의 거의 전부가 USoundWave 에셋에서 분리되어 .pak 파일 끝에 배치됩니다. 따라서 언제든지 오디오를 메모리에 로드하고, 최근에 사용하지 않은 경우에는 다시 릴리즈할 수 있습니다.

이 메모리 관리 방법은 특정 사용 사례에 필요한 실제 오디오를 미리 결정하기 어려운 오픈월드 게임에서 흔히 사용됩니다. 이 방법의 주요 단점은 `USoundWave`가 로드되었다고 하더라도 오디오가 즉시 재생되지 않을 수도 있다는 것입니다. 하지만 디자이너가 메모리 바운더리를 초과하지 않고 원하는 만큼 오디오 에셋을 참조할 수 있는 시스템이라는 장점이 훨씬 큽니다. 또한 오디오 엔지니어가 오디오 엔진에 의해 관리되는 상태에 의존하지 않고 압축된 오디오 청크를 로드 및 참조할 수 있다는 장점도 있습니다. 또한 오디오를 재생할 때 스트림 캐시를 사용하여 지연시간을 완화하는 것이 스트림 캐시 없이 메모리 문제를 완화하는 것보다 간단합니다.

적절한 캐시 크기 결정하기

최대 캐시 크기(Max Cache Size) 는 압축된 오디오 데이터에 의해 사용되는 메모리 하드 제한으로, 메모리에 유지되도록 명시적으로 마킹된 헤더 및 사운드는 제외됩니다. 캐시가 너무 작으면 사운드가 너무 빠르게 언로드되므로 사운드를 로드할 때 지연시간이 발생하게 됩니다.

극단적인 예시

캐시 제한이 8MB이지만 청크가 각각 최대 256KB인 경우 언제든지 캐시에 청크를 32개 가질 수 있습니다. 즉, 사운드 32개를 동시에 재생 중이라면 더 이상 청크를 로드할 수 없다는 의미입니다.

덜 극단적인 예시

캐시 제한이 16MB인 경우 캐시에 256KB 청크를 64개 가질 수 있습니다. 사운드 32개를 동시에 재생 중이더라도 여전히 32개를 더 준비할 수 있는 여유가 있습니다. 하지만 현재 재생 중인 32개 사운드 모두 여러 청크에 들어가기에 충분할 정도로 길다면 시퀀스의 다음 청크는 자동으로 캐시에 로드된 상태를 유지합니다. 즉, 이 시간 동안 준비해두려는 모든 사운드는 현재 재생 중인 사운드 32개에 대해 미리 로드된 32개 청크에 의해 스톰핑된다는 의미입니다.

일반적인 예시

캐시 제한이 32MB로 설정된 경우 각각 최대 256KB인 엘리먼트를 128개 포함할 수 있습니다. 사운드 32개를 동시에 재생 중이라고 해도 여전히 오디오 청크 96개를 캐시에 둬서 지연시간이 발생하는 것을 방지할 수 있습니다. 제한이 48MB인 경우에는 엘리먼트 192개의 공간이 있는 것이므로 사운드 32개를 동시에 재생하더라도 청크 160개를 준비해둘 수 있습니다.

캐시 크기 환경설정하기

  1. 프로젝트를 열고 메인 메뉴에서 편집(Edit) > 프로젝트 세팅(Project Settings) 을 선택합니다.

  2. 프로젝트(Project) 메뉴에서 플랫폼(Platforms) > 창(Windows) > 오디오(Audio) > 압축 오버라이드(Compression Overrides) > 쿠킹 오버라이드(CookOverrides) > 스트림 캐싱(Stream Caching) 으로 이동합니다.

  3. 최대 캐시 크기(KB)(Max Cache Size (KB)) 를 설정하여 캐시에 포함할 엘리먼트의 수를 결정합니다.

값이 0 으로 설정된 경우 디폴트 값은 32KB입니다. 최대 캐시 크기는 2,147,483,647MB지만 캐시 크기를 이렇게 크게 설정하면 머신의 메모리가 모두 소진될 수 있으므로 주의해야 합니다.

캐시 크기 환경설정하기

적절한 사운드 캐싱을 통한 지연시간 방지하기

이상적인 경우 사운드는 재생될 때 항상 메모리에 있습니다. 이는 다음과 같은 방법으로 달성할 수 있습니다.

사운드 재생 준비하기

사운드가 곧 재생될 것으로 예상되는 경우 블루프린트에서는 Prime Sound For Playback 을 호출하고 C++에서는 `UAudioMixerBlueprintLibrary::PrimeSoundForPlayback`을 호출할 수 있습니다.

Prime Sound For Playback

예를 들어 오픈월드 게임에서는 플레이어가 자동차 근처에 갔을 때 차량 사운드와 라디오 방송을 캐시에 미리 로드시켜 둡니다. 플레이어가 차에 탑승하지 않으면 준비된 사운드는 캐시에 남아 있다가 릴리즈됩니다.

사운드 큐(Sound Cue)도 준비해 둘 수 있지만 로드가 완료될 때 델리게이트가 발동하지 않습니다. 델리게이트(delegate) 란 오브젝트 인스턴스에 포인터 또는 참조를 래핑하는 클래스로, 해당 오브젝트 인스턴스에서 호출될 오브젝트 클래스의 멤버 메서드이자 해당 호출을 트리거하는 메서드를 제공합니다.

Prime Sound Cue for Playback

사운드에 디폴트 로딩 비헤이비어 설정하기

사운드가 로드되고 잠시 후에 재생되어야 한다면 사운드 웨이브의 로딩 비헤이비어를 Prime on Load 로 설정할 수 있습니다.

사운드 웨이브에 디폴트 로딩 비헤이비어 설정하기

또한 사운드 웨이브에 대한 사운드 클래스 로딩 비헤이비어도 Prime on Load 로 설정할 수 있습니다.

또한 `au.streamcache.DefaultSoundWaveLoadingBehavior`를 **2** 로 설정하여 모든 사운드웨이브가 로드 시 준비되도록 설정할 수도 있습니다.

메모리에 사운드 유지하기

어떤 상황에서든 지연시간이 전혀 없이 재생되어야 하는 사운드가 있는 경우 `USoundWave`의 수명 주기 내내 메모리에 유지할 수 있습니다.

메모리에 사운드 웨이브 강제 유지하기

사운드 웨이브에 대한 로딩 비헤이비어를 Retain on Load 로 설정하면 해당 사운드의 첫 오디오 청크가 캐시에 계속 유지되도록 설정할 수 있습니다.

또한 `au.streamcache.DefaultSoundWaveLoadingBehavior`를 *2* 로 설정하여 모든 사운드웨이브의 첫 번째 청크를 기본적으로 유지할 수도 있습니다.

메모리 트리밍

애플리케이션에 메모리가 더 필요한 경우에는 Trim Audio Cache 함수(UAudioMixerBlueprintLibrary::TrimAudioCache)로 사용되지 않은 오디오 청크를 캐시에서 릴리즈할 수 있습니다.

TrimmingMemory.png

Trim Audio Cache 함수는 In Megabytes To Free 실행인자에서 지정된 양에 도달할 때까지 캐시를 반복작업하고 사용하지 않은 청크를 해제합니다. 이 함수는 성공적으로 해제한 메모리를 반환합니다.

C++에서 호출되는 경우 이 함수는 스레드 세이프입니다. 하지만 이 함수는 캐시를 잠그며 잠재적으로 비용이 많이 들 수 있습니다. 즉, 함수가 실행 중인 동안에는 오디오 스트리밍이 언더런될 수 있습니다.

캐시 블로잉

캐시의 모든 엘리먼트가 사용 중인 동안(청크가 재생 중이든 디스크로부터 로드 중이든) 오디오 청크를 로드하거나 재생하려고 하면 캐시를 블로잉(blowing)하게 됩니다. 이 경우 `AudioStreamingCache.cpp`에서 히트됩니다.

BlowingCache.png

캐시가 계속 블로잉되는 경우 다섯 가지 옵션이 있습니다.

  • 캐시 크기를 늘립니다.

  • 유지 중인 사운드 웨이브의 양을 줄입니다.

  • 보이스 제한을 낮춥니다.

  • 캐시가 블로잉될 때 로드하려는 사운드의 양을 줄입니다.

  • 무시하고 청크 요청을 드롭합니다.

점점 늘어나는 최악의 캐시 활용 사례

짧은 사운드가 많은 경우 캐시는 아주 작은 청크로 채워지면서 캐시 크기 중에서 적은 일부분만 사용할 수 있게 됩니다.

예를 들어 캐시가 128개 청크이고 최대 청크 크기는 256KB인데 64KB 사운드를 여러 개 로드하면 캐시 제한은 32MB이지만 정작 8MB만 사용하게 됩니다.

이 같은 문제를 해결하려면 청크의 최대 수를 MaxCacheSize/MaxChunkSize 보다 크게 설정하여 현재 풀에 할당된 바이트 수를 카운터로 재면서 메모리 카운터가 캐시 크기에 도달하거나 최대 청크 수에 도달할 경우 가장 오래된 청크를 제거할 수 있습니다. 청크의 최대 수는 au.streamcaching.MinimumCacheUsage 콘솔 변수를 사용하여 결정할 수 있습니다.

au.streamcaching.MinimumCacheUsage 설정하기

au.streamcaching.MinimumCacheUsage`는 **0.0~1.00** 범위로 설정할 수 있습니다. IAudioStreamingManager`가 초기화되기 전에만 설정할 수 있으며, 게임플레이 중에 설정하면 아무런 효과도 나타나지 않습니다.

이 값을 높이면 캐시에 둘 수 있는 청크의 최대 수가 증가합니다. 예를 들어 au.streamcaching.MinimumCacheUsage`가 0.75이고 캐시 크기가 32MB인 경우 청크의 최대 숫자는 512가 됩니다. 64KB 오디오 에셋을 대량으로 로드한 경우에는 여전히 32MB만 사용할 수 있게 됩니다. 즉, au.streamcaching.MinimumCacheUsage`가 1에 가까울 수록 캐시를 완전히 활용하는 데 필요한 청크의 평균 크기는 작아집니다.

au.streamcaching.MinimumCacheUsage

캐시 크기(MB)

최대 청크 수

100% 활용을 위한 청크의 최소 크기(KB)

0.0

32

128

256

0.0

16

64

256

0.5

32

256

128

0.5

16

128

128

0.75

32

512

64

0.75

16

256

64

0.825

32

1024

32

0.825

16

512

32

개별 오디오 에셋은 어떤 크기든 100% 캐시 활용을 보장할 수 없는데, 이 경우 최대 청크 수가 무한대여야 하기 때문입니다.

`au.streamcaching.MinimumCacheUsage`를 높이면 다음과 같은 결과가 생깁니다.

  • 최악의 경우에는 캐시에 두는 오디오가 늘어나면서 보통 디스크 IO 읽기가 감소합니다.

  • 작은 청크를 더 많이 처리해야 할 수 있어서 보통 메모리 사용량이 증가합니다.

  • 청크 탐색에 들어가는 CPU 비용이 보통 증가합니다.

  • 청크 삽입/제거 비용은 동일하게 유지됩니다.

au.streamcaching.TrimCacheWhenOverBudget 사용하기

au.streamcaching.TrimCacheWhenOverBudget`의 디폴트 값은 **1** 입니다. 이는 스트리밍 캐시에서 au.streamcaching.MinimumCacheUsage`가 0보다 큰 값으로 설정되어 있는 경우 누수된 메모리의 잠재적인 벡터를 해결합니다. 이 같은 누수는 LRU 캐시에서 큰 에셋을 위해 작은 에셋을 제거할 때 발생합니다. 이러한 경우는 연속해서 여러 번 발생할 수 있기 때문에 지정된 최대 캐시 크기보다 훨씬 큰 캐시가 사용되는 결과를 초래합니다.

이러한 au.streamcaching.TrimCacheWhenOverBudget 사용 사례의 해결책은 예산 미만이 될 때까지 가장 오래 전에 사용된 청크부터 차례로 트리밍하는 것입니다. 단, 사운드를 준비하거나 재생하기 위한 호출로 인해 최근에 덜 사용된 사운드가 제거될 수 있다는 점에 유의해야 합니다.

오디오 읽기 우선순위 지정하기

오디오 청크는 텍스처 스트리밍 및 지오메트리 스트리밍 시스템과 유사하게 IAsyncReadRequest`를 사용하여 디스크에서 읽어 들입니다. 이로 인해 au.streamcaching.ReadRequestPriority` 콘솔 변수를 사용하여 오디오 청크 로드 우선순위를 다른 스트리밍 시스템보다 높이거나 낮출 수 있습니다. 이 값은 0~4로 설정할 수 있으며, 우선순위는 0이 가장 높고 4가 가장 낮습니다.

au.streamcaching.ReadRequestPriority

AsyncIOPriority

이 우선순위를 사용하는 다른 스트리밍 매니저

0

AIOP_High

애니메이션, 텍스처, 셰이더 코드

1

AIOP_Normal

비동기 Pak 읽기

2

AIOP_BelowNormal

지오메트리 스트리밍

3

AIOP_Low

4

AIOP_MIN

캐시된 오디오 스트리밍 요청의 디폴트 값은 2 입니다.