이 글에서는 제가 오랫동안 사용해온 WAAPI 작업에 대한 다소 주관적인 접근 방식을 설명해드리려고 합니다. 이 접근 방식은 Python, 명령어 애드온(add-on), 그리고 작은 도우미 라이브러리로 구성되어 있으며, 이를 통해 기본 waapi-client
보다 새로운 WAAPI 스크립트를 조금 더 빠르게 작성할 수 있으며, 대부분의 경우 팀원들과의 공유도 원활합니다. 또한 이 글을 통해 WAAPI의 기본 작동 원리를 설명하고 실제로 유용한 WAAPI 스크립트가 어떻게 생겼는지 보여드리겠습니다. 따라서 이 글은 WAAPI를 처음 배우기 시작한 초보자나 오디오 팀 외부에서 동료들의 작업 과정 도구 및 오토메이션을 돕는 사람들에게도 유용합니다.
이 글을 위해 몇 가지 코드 예제를 제공해드리지만, 이 코드는 광범위하게 테스트되지 않았으니 주의해주세요. 코드를 실행하려면 PC 환경을 설정해야 하며, 자세한 내용은 부록의 설명을 참조하세요.
기본 개념
먼저 Wwise와 WAAPI에 대한 기본 정보를 살펴보는 것이 좋을 것 같습니다.
Wwise 프로젝트는 오브젝트 계층 구조로 구성되어 있습니다. 오브젝트는 여러 타입(Event, RandomSequenceContainer 등)으로 나뉘며, 이 타입은 오브젝트가 가진 속성을 설명합니다 (BusVolume, IsStreamingEnabled 등). 계층 구조에서의 상대적인 위치는 속성 값의 상속성을 정의합니다. 예를 들어 하위 오브젝트는 상위 오브젝트의 값을 이어 받죠. 이 모든 것들은 런타임 때 오디오 엔진에 대해 사운드 재생 규칙과 구성을 정의합니다.
WAAPI는 이러한 계층 구조를 조작하기 위한 클라이언트-서버 API입니다. 오브젝트의 이름을 변경하거나 제거하고, 속성을 변경하는 등의 작업을 수행할 수 있죠. 하지만 모든 것들에 접근할 수 있는 것은 아닙니다 (RTPC와 블렌드는 현재 작성 시점에 접근 불가). 하지만 여전히 수많은 작업을 수행할 수 있습니다. 따라서 Wwise 프로젝트가 어떻게 구성되어 있는지 이해하고 다양한 오브젝트 과 해당 속성을 아는 것이 중요합니다. 이를 위해 다음 참조 페이지를 북마크하여 빠르게 접근할 수 있도록 제공해드립니다.
- Wwise 오브젝트 레퍼런스 — 모든 오브젝트 및 해당 속성.
- Wwise Authoring API 참조 - 함수 — 쿼리하거나 변경할 수 있는 것들.
- Wwise Authoring API 참조 - 주제 — Wwise Authoring 도구가 전송할 수 있는 알림.
하지만 WAAPI는 또한 오브젝트 속성에 열거되지 않은 오브젝트에 대한 정보도 제공합니다. 예를 들어 sound:convertedWemFilePath, sound:originalWavFilePath 속성과, 혹은 심지어 AudioSource 객체의 TrimEnd와 TrimBegin 속성의 차이로 계산된 값을 제공하는 maxDurationSource도 있습니다. 이러한 속성들은 Wwise Object 참조에 열거되어 있지 않지만 WAAPI가 여전히 이들을 쿼리할 수 있죠 (아마도 프로젝트 데이터에 대한 특별 접근 권한이 있을 수 있습니다).
또한 고급 사용자들을 위해 %WWISEROOT%\Authoring\Data\Schemas 아래 있는 WAAPI와 Wwise Authoring 데이터(예: Work Unit)에 대한 JSON과 XML 스키마도 제공되며, 특정 상황에서 유용하게 사용될 수 있습니다.
고려 사항
WAAPI 스크립트가 유용하기 위해서 꼭 길고 복잡해야 하는 것은 아닙니다. 특히 번거로운 작업을 오토메이션할 때 그렇죠. 스크립트 자체 외에도 이상적으로는 다음 사항을 고려해야 합니다.
- 스크립트 실행 방법
- 팀원들 간에 스크립트를 배포하는 방법
이를 해결하기 위해 저희가 사용했던 한 가지 방법은 커스텀 Total Commander 빌드를 사용하는 것입니다. 이 빌드는 SVN에 저장되어 있으며, 다양한 기술적 오디오 요구에 맞는 다양한 휴대용 도구를 포함하고 있습니다. 이러한 유틸리티는 앱 상단의 메뉴/하위 메뉴 바를 통해 접근할 수 있습니다. 또한 이 빌드는 프로젝트 전용 및 WAAPI 스크립트를 포함하여 제가 설치하고 관리하는 패키지가 포함된 내장 Python 배포판도 포함되어 있습니다. 이를 통해 팀원들은 도구를 실행하기 위해 컴퓨터를 설정할 필요가 없으며, 저는 전체 환경을 쉽게 업데이트하고 다른 사람들과 동기화할 수 있죠. 참고로 Total Commander는 정말로 훌륭합니다!
제가 가끔 연습하는 또 다른 방법은 모든 Python 스크립트를 Wwise 프로젝트의 Scripts 디렉터리에 넣는 것입니다. 이렇게 하면 스크립트 제공 범위를 특정 프로젝트로 제한하죠.
Total Commander에 있는 버튼 외에도 저는 WAAPI 스크립트를 명령어 애드온을 통해 호출하는 것도 선호합니다. 이는 선택된 오브젝트의 GUID, Wwise 프로젝트의 경로 등 프로젝트별 인자를 사용하여 임의의 외부 도구를 실행하는 명령을 Authoring Tool에 전달합니다. 그리고 이러한 명령어는 Authoring Tool의 메뉴로 통합될 수 있으며, 이는 아마도 Wwise 관련 커스텀 도구를 사용하는 데 가장 방해받지 않는 방법일 것입니다.
작업 과정 개요
이 글의 목적에 맞게 간단하게 하기 위해 WAAPI 스크립트는 명령어 애드온을 통해서만 실행하고 Python 파일 자체는 Scripts 디렉터리 안에 배치하겠습니다. 또한 이 구성은 사용자가 PC 환경을 구성할 것을 전제로 합니다. 이에 대한 방법은 부록을 참고해 주세요.
추가 요구 사항으로 저는 모든 오류가 GUI에 표시되도록 하려고 합니다. 개인적인 경험상 이렇게 하면 사운드 디자이너에게 문제를 해결할 때 콘솔 창을 자세히 살펴보라고 하는 것보다 더 쉽게 처리할 수 있더군요.
Python 프로젝트 구성
다음 스크린샷은 Scripts 폴더 구조를 보여줍니다.
몇 가지 중요한 사항:
- 루트 폴더는 Wwise의 명령어 애드온에서 호출되는 스크립트만 직접 포함되어 있기 때문에 다른 Python 파일에서 가져와서는 안됩니다. 이 규칙에는 두 가지 예외 사항이 있습니다.
- __init__.py — Scripts 폴더를 모듈로 표시하는 파일로, 현재 작업 디렉토리가 Wwise 프로젝트 폴더로 구성된 경우 스크립트에서 상대적 가져오기를 사용할 수 있게 해줍니다.
- _template.py — 새로운 스크립트를 만들기 위한 템플릿 파일로, 상용구를 포함하며 구체적인 필요에 맞게 복제, 이름 변경, 변경 작업을 할 수 있습니다.
- 하위 모듈에는 크기가 보다 큰 코드나 여러 스크립트에서 재사용되는 함수가 포함됩니다.
- 이 스크립트는 Python 인터프리터에 경로를 인자로 전달하여 실행됩니다. 저는 모듈 이름(플래그
-m
)으로 스크립트를 실행하는 것을 선호하지만 작성 당시에는 현재 작업 디렉토리를 ${WwiseProjectRoot}로 설정할 때 명령 애드온을 작동시킬 수 없었기 때문에 여기서는 상대적 가져오기를 사용하지 않고 있습니다.1 - 보통 오류가 발생하거나 데이터가 잘못된 상태임을 감지할 경우 스크립트는
RuntimeError
예외 사항을 발생시켜야 합니다. 이러한 예외는 가장 바깥쪽 프레임에서 포착되며 해당 메시지는 대화 상자로 표시됩니다.
다음은 Python 템플릿 코드입니다. 길어 보일 수 있지만, 이 스크립트는 오류를 처리하고 GUI에 표시하여 공통 가져오기를 제공합니다. 저는 새로은 스크립트를 작성할 때 그냥 이 템플릿을 복사해넣습니다.
# 파일이 가져와지지 않도록 방지합니다.
if __name__ != '__main__':
print(f'error: {__file__} should not be imported, aborting script')
exit(1)
# tkinter는 경고 대화 상자 및 그 외 간단한 UI를 표시하는 데 사용됩니다.
import tkinter
from tkinter.messagebox import showinfo, showerror
from waapi import WaapiClient, CannotConnectToWaapiException
from waapi_helpers import *
from helpers import *
# 스크립트에서 사용하지 않는 가져오기(import)를 삭제하고 새로운 가져오기를 추가해보세요.
# Tk 위젯을 초기화하고 아이콘이 작업 표시줄에 나타나지 않도록 숨깁니다.
tk = tkinter.Tk()
tk.withdraw()
# 이 try-except 블록은 런타임 오류를 찾아내기 위해 사용됩니다.
# 그리고 이를 콘솔이 아닌 GUI 창 안에 사용자에게 표시해줍니다.
try:
with WaapiClient() as client:
# 여기서부터 스크립트가 시작됩니다.
pass
except CannotConnectToWaapiException:
# 실행 중인 Wwise Application 연결에 실패할 경우 '사용자 친화적' 메시지를 출력합니다.
showerror('Error', 'Could not establish the WAAPI connection. Is the Wwise Authoring Tool running?')
except RuntimeError as e:
# '예상된' 오류, 즉 스크립트에서 직접 생긴 오류.
showerror('Error', f'{e}')
except Exception as e:
# 예상치 못한 오류는 항상 스택 추적을 출력합니다.
import traceback
showerror('Error', f'{e}\n\n{traceback.format_exc()}')
finally:
# Tk 창 이벤트 반복 재생을 중지하려면 이를 호출해야 합니다.
# 그렇지 않으면 스크립트가 이 시점에서 중단됩니다.
tk.destroy()
명령어 애드온 구성하기
명령어 애드온 구성에 대해서 특별히 할 말은 없습니다. 이 과정은 공식 문서에서 잘 설명되어 있습니다. 저는 Wwise Project/Add-ons/Commands/ak_blog_addons.json
에 저장된 단일한 JSON 파일을 사용하여 모든 예시를 저장하겠습니다. 이 파일은 제공되는 아카이브(부록 참고)를 직접 다운로드하여 확인할 수 있습니다.
JSON 파일을 변경한 후에는 Wwise Authoring 도구에서 명령어 애드온을 다시 로드해야 합니다. 이 동작을 수행하는 단축키는 없지만, 검색란에 닫는 꺾쇠 괄호 뒤에 'command'를 입력하고 검색하여 실행할 수 있습니다.2:
WAAPI 스크립트 디버깅하기
Logs 창에는 WAAPI 탭이 있지만 모든 내용이 기본적으로 기록되지 않기 때문에 Logs Settings에서 추가 기록을 활성화하는 것을 고려해보세요. 또한 명령어 애드온 JSON에 있는 redirectOutputs 설정은 Wwise가 Python 스크립트의 콘솔 출력을 강제로 General 탭으로 리디렉션하도록 하며, 이는 기본적으로 비활성화되어 있습니다.
waapi_helpers에 대하여3
저는 이 글의 예시에서 전반적으로 사용하는 작은 라이브러리인 waapi_helpers를 작성했습니다. 이 라이브러리는 WaapiClient를 인자로 받아들이는 몇 가지 작은 상태 비저장 도우미로 구성되어 있어 기본 waapi-client
코드와 혼합될 수 있습니다. 모든 함수는 속성을 가져오는 것에 관한 규칙을 따르며, 속성이 존재하지 않을 경우 단순히 None 값으로 지정됩니다. 앞으로 나올 예시들에서 더 잘 볼 수 있으므로 여기서는 자세한 내용을 설명하지 않겠습니다.
예시
여기서 제시된 대부분의 예시와 실제로 제가 작성한 대부분의 WAAPI 스크립트는 대체로 동일한 구조를 따릅니다. Wwise 프로젝트를 탐색하고 정보를 수집한 다음 그 정보를 처리하거나 변환하고 Wwise 프로젝트에 변경 사항을 적용하죠.
예시 1: 선택한 오브젝트의 GUID를 클립보드로 복사하기
WAAPI를 필요로 하지 않으면서도 Wwise에 이미 존재하는 매우 유용한 명령어입니다. 또한 아주 간단하기 때문에 해당 명령어 애드온 JSON 전체와 Python 소스를 함께 제공하겠습니다.
copy_guid.py (딱히 특별한 내용은 없습니다. 템플릿의 대부분이 제거되었습니다).
if __name__ != '__main__':
print(f'error: {__file__} should not be imported, aborting script')
exit(1)
# 이 라이브러리는 서드파티 라이브러입니다.
import pyperclip
# argv의 인자를 목록으로 변환하는 간단한 함수입니다.
# '도우미' 하위 모듈이 없기 때문에 상대적인 가져오기를 사용합니다.
from helpers import get_selected_guids_list
guids = get_selected_guids_list()
pyperclip.copy(' '.join(guids))
JSON:
{
"version": 2,
"commands": [
{
"id": "waapi_article.copy_guid",
"displayName": "Copy GUID",
"program": "python",
"startMode": "MultipleSelectionSingleProcessSpaceSeparated",
"args": "\"${WwiseProjectRoot}/Scripts/copy_guid.py\" ${id}",
"redirectOutputs": false,
"contextMenu": {
"basePath": "WAAPI"
}
}
]
}
- id — 이 새로운 명령어의 고유 식별자입니다.
- displayName — 이 명령어의 사람이 읽을 수 있는 이름이며, 메뉴에 표시됩니다.
- program — 사용자가 이 명령어를 실행할 때 실행할 프로그램입니다. 스크립트 경로는 큰 따옴표로 이스케이프 처리되어 있습니다. 이는 Wwise가 해당 경로를 분할하지 않고 하나의 인자로서 처리하도록 도와주는 중요한 역할을 합니다.
- startMode — Wwise는 프로그램을 한 번 호출하고 공백으로 구분된 인자, 즉 이 예시의 경우 오브젝트 GUID를 전달합니다.
- args — Python에 전달되는 인자입니다.
- ${WwiseProjectRoot}/Scripts/copy_guid.py 는 경로에 공백이 포함된 경우 하나의 인자로 처리되도록 큰따옴표로 둘러싸인 스크립트 경로입니다.
- ${id} 는 Wwise가 선택된 오브젝트의 GUID로 대체할 특수 인자입니다.
- redirectOutputs — 스크립트의 stdout을 Wwise의 Logs 창으로 리디렉션하여 디버깅을 도와줍니다.
- contextMenu — 이 명령어를 Wwise 프로젝트 계층 구조의 모든 오브젝트에 대한 콘텍스트 메뉴에 표시되도록 구성합니다. 이 예시의 경우 WAAPI라는 하위 그룹이 있을 것입니다.
명령어 애드온을 새로 고치면 콘텍스트 메뉴에 항목에 나타납니다.
이를 클릭하면 다음 텍스트가 시스템 클립보드에 복사됩니다.
{2E9E3B71-C905-4BB0-9B30-06CFF26E0C5E} {3AD5C9DF-C0B5-4A78-B87A-2EE37D64BFCB} {8F7A715D-5704-4F09-9563-4172E250419B}
예시 2: 모든 이벤트의 이름 표시하기
전혀 실용적이지 않지만 walk_wproj
함수를 사용하여 계층 구조를 탐색하는 방법을 보여줍니다. 상용구는 생략되었습니다.
show_event_names.py:
events = []
with WaapiClient() as client:
for guid, name in walk_wproj(client,
start_guids_or_paths='\\Events',
properties=['id', 'name'],
types=['Event']):
events.append(name)
showinfo('Hi tutorial!', '\n'.join(events))
여기서 walk_project
함수는 \Events 경로에서부터 시작하여 계층 구조를 따라 각 오브젝트를 탐색하며, 찾아낸 각 Event 오브젝트의 id와 name 속성을 반환합니다. 이 함수가 바로 Wwise 계층 구조를 탐색하기 위해 XML 라이브러리가 제공하는 것과 유사한 방식으로 간단한 Pythonic 기반의 인터페이스를 원했던 저의 라이브러리의 시작이었습니다. 또한 저는 객체마다 스키마가 달라 작업 메모리에 항상 유지하기 어려운 JSON 객체를 생성하고 해제하는 것을 피하고 싶었습니다.
이름은 배열에 수집되어 반복이 끝난 후 함께 출력됩니다. 교육 목적만으로 사용되며 사실 아주 쓸모없는 예시이죠. 결과는 다음과 같습니다.
예시 3: 볼륨 페이더 재설정
가끔 믹싱 도중 계층 구조의 특정 부분에 있는 모든 페이더를 0로 설정하고 싶은 경우가 있습니다. 다음 스크립트는 선택한 오브젝트에서 노트에 @ignore 태그로 표시된 것을 제외한 모든 하위 오브젝트에 해당 작업을 수행합니다.
reset_faders.py:
with WaapiClient() as client:
num_reset_faders = 0
selected_guid = get_selected_guid()
for obj_id, obj_type, obj_notes in walk_wproj(client, selected_guid,
properties=['id', 'type', 'notes']):
if '@ignore' in obj_notes:
continue
# 오브젝트가 Actor-Mixer에 속하는지 혹은 Master Mixer 계층 구조에 속하는지에 따라
# 서로 다른 속성을 변경하려고 합니다.
prop_name = 'Volume'
if obj_type == 'Bus' or obj_type == 'AuxBus':
prop_name = 'BusVolume'
cur_volume = get_property_value(client, obj_id, prop_name)
if cur_volume is not None:
# 일반적으로 속성이 존재하지 않을 경우
# `get_property_value`는 None을 반환합니다.
# 이는 오브젝트에 볼륨 속성이 없을 경우
# `set_property_value` 호출을 건너뛰도록 해줍니다.
set_property_value(client, obj_id, prop_name, 0)
num_reset_faders += 1
showinfo('Info', f'{num_reset_faders} faders were reset')
결과:
예시 4: 유효하지 않은 이벤트 제거하기
Actor-Mixer Hierarchy에서 여러 레거시 오브젝트를 삭제했다고 해봅시다. 이로 인해 유효하지 않은 참조가 있는 여러 이벤트 동작이 남겨졌을 수도 있고, 일부 이벤트는 모든 동작이 더 이상 존재하지 않는 오브젝트를 참조하게 되어 아예 쓸모가 없게 되었을 수도 있죠. 그렇기 때문에 그러한 이벤트는 안전하게 삭제할 수 있다고 볼 수 있습니다. 이 작업은 WAAPI를 통해 모든 이벤트를 살펴보고 해당 동작이 더 이상 존재하지 않는 오브젝트를 참조하는지의 여부를 확인하여 수행할 수 있습니다. 만약 존재하지 않는 오브젝트를 참조할 경우 이벤트를 삭제 대상으로 표시할 수 있죠.
delete_invalid_events.py:
# 오브젝트를 참조하는 타입 식별자의 집합입니다.
# 자세한 내용은 Action 오브젝트 레퍼런스를 보세요.
action_types_to_check = {1, 2, 7, 9, 34, 37, 41}
events_to_delete = []
with WaapiClient() as client:
num_obj_visited = 0
for event_guid, in walk_wproj(client, '\\Events', properties=['id'], types=['Event']):
print(f'Visited: {num_obj_visited}, To delete: {len(events_to_delete)}', end='\r')
num_valid_actions = 0
for action_id, action_type, target in walk_wproj(client, event_guid,
properties=['id', 'ActionType', 'Target'],
types=['Action']):
if action_type in action_types_to_check:
if does_object_exist(client, target['id']):
num_valid_actions += 1
else:
num_valid_actions += 1
if num_valid_actions == 0:
events_to_delete.append(event_guid)
num_events_to_delete = len(events_to_delete)
if num_events_to_delete > 0 \
and askyesno('Confirm', f'{num_events_to_delete} events are going to be deleted. Proceed?'):
begin_undo_group(client)
for event_guid in events_to_delete:
delete_object(client, event_guid)
end_undo_group(client, 'Delete Invalid Events') # Wwise 규칙에 따라 대문자로 표기합니다.
showinfo('Success', f'{len(events_to_delete)} were deleted')
이 작업은 조금 더 복잡합니다.
먼저 오브젝트를 참조할 수 있는 동작 타입의 집합을 저장했다는 점을 주목하세요(예: Play, Stop, Set RTPC 등.) 존재하지 않는 오브젝트를 참조하는 모든 동작은 무효로 간주됩니다. 이 집합은 Action 오브젝트 문서(ActionType 속성 설명)를 기반으로 직접 작성되었습니다.
두 번째로, 사용자가 파괴적인 동작을 수행할 수 있는 권한을 요청하는 새로운 기능이 추가되었습니다. 사용자에게 특정 수의 이벤트를 삭제할지의 여부를 확인하는 대화 상자가 표시되죠.
마지막으로, 이 명령어는 WAAPI의 undo group(그룹 실행 취소) 기능을 사용합니다. 이를 통해 여러 WAAPI 호출로 수행된 변경 사항을 한 번의 작업 취소 동작으로 되돌릴 수 있으며, 이 동작 자체는Edit 에서 'Delete Invalid Events'라는 특별한 이름을 갖게 됩니다.
관련 스크린샷
예시 5: 긴 SFX에 스트리밍 설정하기
SFX에 스트리밍을 대량 설정하는 것은 간단하지 않을 수 있습니다. 왜냐하면 디자이너가 종종 음원을 잘라내기 때문에 Wave 파일 길이가 런타임에서 SFX 지속 시간과 크게 다를 수 있기 때문이죠. 이 글을 작성하는 시점에는 Wwise Queries나 WAQL을 통해 잘라낸 SFX의 지속 시간을 얻는 것이 불가능합니다.
다행히 WAAPI에는 trimmedDuration 속성이 있습니다. 하지만 한 걸음 더 나아가 도구가 설정할 스트리밍 매개 변수를 Wwise Authoring 도구에서 구성 가능하게 만들어보겠습니다. 이 예시에서는 \Actor-Mixer Hierarchy\Default Work Unit 노트 섹션에 구성을 넣고 Python의 configparser 구문을 사용해보겠습니다. 이 구문은 아주 간단하며 Python에 이미 내장되어 있습니다. 구성이 다음과 같다고 해봅시다.
[Enable_Streaming_For_SFX]
If_Longer_Than = 10
Non_Cachable = no
Zero_Latency = no
Prefetch_Length_Ms = 400
더 큰 프로젝트에서는 다양한 종류의 사운드나 심지어 플랫폼에 대해 스트리밍 설정을 더 세밀하게 제어하고 싶을 수 있습니다. 여기서 장점은 여러분이 구성 섹션을 추가하고 스크립트에서 각 특정 작업에 필요한 구성을 가져올 수 있다는 것입니다.
set_streaming_for_long_sfx.py:
with WaapiClient() as client:
dwu_notes = get_property_value(
client, '\\Actor-Mixer Hierarchy\\Default Work Unit', 'notes')
if dwu_notes is None:
raise RuntimeError('Could not fetch notes from Default Work Unit')
config = configparser.ConfigParser()
config.read_string(dwu_notes)
if 'Enable_Streaming_For_SFX' not in config:
raise RuntimeError('Could not find [Enable_Streaming_For_SFX] config section')
stream_config = config['Enable_Streaming_For_SFX']
objects_to_modify = []
for guid, name, max_dur_src in walk_wproj(client, '\\Actor-Mixer Hierarchy',
['id', 'name', 'maxDurationSource'], 'Sound'):
if max_dur_src is None:
continue
# trimmedDuration은 밀리초가 아닌 초 단위입니다.
is_long_sound = max_dur_src['trimmedDuration'] > stream_config.getfloat('If_Longer_Than')
if is_long_sound:
objects_to_modify.append(guid)
print(name)
break
if len(objects_to_modify) > 0 and \
askyesno('Confirm',
f'The tool is about to modify properties of {len(objects_to_modify)} objects. Proceed?'):
begin_undo_group(client)
for guid in objects_to_modify:
set_property_value(client, guid, 'IsStreamingEnabled', True)
set_property_value(client, guid, 'IsNonCachable', True) # stream_config.getboolean('Non_Cachable'))
set_property_value(client, guid, 'IsZeroLantency', stream_config.getboolean('Zero_Latency'))
set_property_value(client, guid, 'PreFetchLength', stream_config.getint('Prefetch_Length_Ms'))
end_undo_group(client, 'Bulk Set SFX Streaming')
showinfo('Success', f'{len(objects_to_modify)} objects were updated')
else:
showinfo('Success', f'No changes have been made')
구성은 다음과 같습니다.
예시 6: 컨테이너 그룹을 스위치로 리팩토링하기
때로는 A-M 계층 구조에서 여러 컨테이너를 하나의 스위치 컨테이너로 리팩토링해야 할 때가 있습니다. 이를 자동화하는 도구를 만들어볼 수 있습니다. 몇몇 오브젝트를 선택하고 버튼을 클릭하면 선택된 오브젝트가 스위치에 할당된 새로운 스위치 컨테이너가 나타나도록 해봅시다. 이 예시에서는 Wwise Adventure Game 프로젝트의 Surface_Type 스위치를 사용해보겠습니다.
refactor_into_switch_surface_type.py:
with WaapiClient() as client:
obj_names = [get_name_of_guid(client, guid)
for guid in selected_guids]
if None in obj_names:
raise RuntimeError('Could not get names of all selected objects')
switches = get_switches_for_group_surface_type(client)
if len(switches) == 0:
raise RuntimeError("Could not find switches for group 'Surface_Type'")
parent_obj = get_parent_guid(client, selected_guids[0])
if parent_obj is None:
raise RuntimeError(f'{selected_guids[0]} has no parent')
begin_undo_group(client)
switch_obj = create_objects(client, parent_obj, 'RENAME_ME', 'SwitchContainer')[0]
if switch_obj is not None:
set_reference(client, switch_obj, 'SwitchGroupOrStateGroup',
f'SwitchGroup:{SURFACE_TYPE_SWITCH_GROUP_NAME}')
else:
# 스크립트가 작업 도중 실패했을 경우 변경 사항을 다시 되돌립니다.
end_undo_group(client, 'Refactor Into Surface_Type Switch')
perform_undo(client)
raise RuntimeError('Could not create switch container under ' +
f'{get_name_of_guid(client, parent_obj)}. '
'All changes have been reverted.')
# 선택한 오브젝트를 다시 상위 항목으로 지정합니다.
for guid in selected_guids:
res = move_object(client, guid, switch_obj)
if res is None:
end_undo_group(client, 'Refactor Into Surface_Type Switch')
perform_undo(client)
raise RuntimeError(
f'Could not move object {guid} to parent {switch_obj}. '
'All changes have been reverted.')
obj_assignments = infer_obj_assignments(selected_guids, switches)
for obj_guid, sw_guid in obj_assignments:
client.call('ak.wwise.core.switchContainer.addAssignment',
{'child': obj_guid, 'stateOrSwitch': sw_guid})
end_undo_group(client, 'Refactor Into Surface_Type Switch')
이 코드는 이전 예시보다 조금 더 복잡합니다. 심지어 목록을 짧게 유지하기 위해 일부분을 두 개의 함수로 리팩토링해야 했죠. 첫 번째 함수인 get_switches_for_group_surface_type
은 모든 Surface_Type 스위치의 GUID와 이름을 가져오는 도우미 함수입니다. 두 번째 함수인 infer_obj_assignments
는 선택된 오브젝트의 이름을 비교하여 가장 유사한 스위치 이름을 선택하는 함수입니다 (thefuzz 라이브러리의 partial_ratio
함수).
그 외 참고할 사항:
- 이 코드는 실행 중에 다양한 데이터 유효성 검사를 실행하며 상태가 유효하지 않으면 RuntimeError 예외를 발생시킵니다. 이러한 예외는 사용자에게 오류 창으로 표시됩니다.
- 일부 오류 경로에서는 예외를 발생시키기 직전에 스크립트가 WAAPI를 통해 지금까지 수행된 모든 변경 사항을 되돌립니다.
- 또한 도우미 라이브러리에 스위치 컨테이너 할당 함수가 없기 때문에
waapi-client
에 직접 호출하는 것을 볼 수 있습니다. - 이 스크립트는 완벽하지 않습니다. 이상한 예외 상황이 있을 수도 있고 전혀 최적화되지 않았죠. 이 글을 작성하는 저녁에 재미로 한 작업임을 알아주세요. 이 점을 잊지마세요!
다음 스크린샷은 스크립트가 작동하는 방식을 보여줍니다.
예시 7: Originals 폴더에서 사용되지 않는 Wave 파일 삭제하기
시간이 지남에 따라 Wwise 프로젝트는 Originals 폴더에 참조되지 않는 Wave 파일이 쌓일 수 있습니다. 버튼을 누르면 스크립트가 확인을 요청하고 모든 파일이 삭제되었는지 아니면 일부 파일이 남아있는지 (예: 파일이 Audition에서 열려 있거나 다른 이유로 잠긴 경우) 알리도록 해봅시다. 팁: 이러한 작업 후에는 Integrity Report(무결성 보고서)를 실행하세요.
remove_unused_wavs.py:
with WaapiClient() as client:
default_wu_path, = get_object(client, '\\Actor-Mixer Hierarchy\\Default Work Unit', 'filePath')
# 이 함수는 .wproj 파일을 파싱하여 'Originals' 디렉터리의 위치를 결정합니다.
origs_dir = find_originals_dir(default_wu_path)
wavs_in_origs = set()
wavs_in_wproj = set()
# 'Plugins' 디렉터리는 건드리지 말아야 합니다.
for subdir in 'SFX', 'Voices':
for wav_path in glob(os.path.join(origs_dir, subdir, '**', '*.wav'),
recursive=True):
wavs_in_origs.add(normalize_path(wav_path))
# 단일한 walk_wproj가 여러 장소에서 하나의 계층 구조를
# 여러 번 순회할 수 있습니다.
for guid, wav_path in walk_wproj(
client,
start_guids_or_paths=['\\Actor-Mixer Hierarchy', '\\Interactive Music Hierarchy'],
properties=['id', 'originalWavFilePath'],
types=['AudioFileSource']
):
wavs_in_wproj.add(normalize_path(wav_path))
wavs_to_remove = wavs_in_origs.difference(wavs_in_wproj)
files_left = len(wavs_to_remove)
if files_left > 0 and askyesno(
'Confirm', f'You are about to delete {files_left} files. Proceed?'):
for wav_path in wavs_to_remove:
try:
os.remove(wav_path)
files_left -= 1
except PermissionError:
pass
if files_left == 0:
showinfo('Success',
f'{len(wavs_to_remove)} files were deleted')
else:
showwarning('Warning',
f'{files_left} files could not have been deleted. '
f'Are they open in some apps?')
이 코드는 이전 예시보다 간단해보일 수 있습니다. 하지만 여기서 WAAPI의 몇 가지 제약에 부딪혔고 Wwise 프로젝트 파일을 구문 분석하여 Originals 폴더의 경로를 알아내야 했죠. 처음에는 이 정보가 Project 오브젝트에 있다고 생각했지만 그렇지 않았고, 쿼리할 수 있는 다른 방법을 찾을 수 없었습니다.
또 다른 명확하지 않은 점은 바로 walk_wproj
에 AudioFileSource 의 모든 오브젝트를 검색하도록 요청했지만 이 Wwise Objects Reference 페이지에 없었다는 점입니다. 가장 가까운 타입은 AudioSource였는데, 이는 상위 타입일 수 있지만 확인할 수 없었죠. 저는 이전에 Work Unit XML 구문을 분석하고 생성하는 작업을 경험이 있고 Wave 파일이 <AudioFileSource/>
태그 안에 포함되어 있다는 것을 기억하고 있었기 때문에 이 선택이 직관적이라고 느꼈습니다. 또한 이 타입은 %WWISEROOT%\Authoring\Data\Schemas에 있는 XML 스키마 파일에 열거되어 있습니다.
스크린샷:
예시 8: Wave 파일 가져오기 오토메이션
많은 경우 게임 내 사운드는 여러 계층이나 구성 요소로 이루어진 복잡한 계층 구조로 표현됩니다. 이러한 사운드는 다양한 버스로 라우팅되거나 여러 RTPC에 의해 제어될 수 있죠. 이러한 시스템 중 일부는 슬롯(오디오 에셋이 이 시스템의 특정 사운드를 생성하기 위해 첨부되는 장소)이 있는 계층 구조로 모델링될 수 있습니다. 실제로는 기존의 계층 구조를 복제하고 적절한 '슬롯'에 에셋을 교체하는 방식으로 구현될 수 있죠.
물론 WAAPI를 사용하여 이를 수행할 수 있으며, 간단히 하기 위헤 이 작업 과정에서 하나의 사례만 구현해보겠습니다.
- 사용자가 템플릿 계층 구조를 구성하고 템플릿 이름, 슬롯, 슬롯 이름 등을 지정하여 주석을 달 수 있도록 해봅시다. 아래 이미지에서 템플릿 Gun에는 Shot, Tail_Indoor, Tail_Outdoor라는 슬롯이 있습니다.
- 사용자가 템플릿을 우클릭하고 버튼을 누르면 Wave 파일이 있는 디렉터리를 선택하라는 메시지가 표시됩니다. 선택 후 이 도구는 디렉터리를 스캔하여 이 특정 템플릿에 맞는 파일을 찾습니다.
- 사용자가 가져오기를 확인하면 템플릿이 복사되고 이름이 변경되며 슬롯이 Wave 파일로 채워집니다.
스크린샷에서 이 도구는 한 번에 M4와 M1911이라는 두 개의 사운드 오브젝트를 구현했습니다. 또한 각 총기의 가상 폴더에 템플릿 오브젝트 GUID를 기록하여 다른 스크립트가 필요할 때 이를 스캔할 수 있도록 했습니다.
작동 예시를 보려면 import_wavs.py 소스를 확인하세요. 이 코드는 여기에 완전히 열거하기에는 다른 코드보다 조금 더 복잡합니다.
마치는 말
이 글에서 저는 WAAPI와 Python을 사용하여 작업하는 과정을 소개하고 코드 구성 방법 및 오디오 팀과 스크립트를 공유하는 법을 설명해드렸습니다. 아이디어를 뒷받침하기 위해 작업 과정을 구현하는 몇 가지 도구 예시도 제공해드렸죠. 질문이나 제안 사항이 있으시다면 언제든지 연락주시거나 이 블로그에 댓글을 남겨 주세요.
개선할 수 있는 몇 가지 사항:
- WaapiClient를 인스턴스화할 때
allow_exception=True
를 사용할 것을 고려해보세요. 이렇게 하면 WAAPI 특정 예외를 포착하고 사용자에게 더 나은 오류 메시지를 제공할 수 있습니다. 이는 예를 들어 Wwise에서 대화 창이 열려 있을 때 유용합니다. - 이 Python 프로젝트 규칙에서는 모든 명령 추가 스크립트를 Scripts 폴더 바로 아래 배치합니다. 이를 통해 애드온 JSON 파일을 자동 생성할 수 있습니다.
- notes 섹션에 저장된 구성은 Jekyll과 같은 웹사이트 빌딩 프레임워크에서 널리 사용되는 YAML 프론트 매터와 유사한 방식으로 분리하여 개선할 수 있습니다. 이를 통해 일반적인 사용자 작성 노트와 함께 공존하도록 할 수 있죠.
- 예시 4에서는 무효 이벤트를 삭제하는 것 외에도 무효 동작을 삭제하는 것이 합리적일 수 있습니다. 현재 프로젝트에서 무효 동작은 그대로 남아 있어 Integrity Report(무결성 보고서)에서 오류를 일으킬 수 있습니다.
- 스위치 리팩토링 명령은 선택된 모든 오브젝트의 공통 부분 문자열을 가져오거나 AI 를 사용하여 텍스트를 요약하는 알고리즘을 사용하여 스위치에 가장 적합한 이름을 부여할 수 있습니다.
감사의 말
이 글은 DevGAMM Fall 20214에서 제가 발표한 짧은 강연을 바탕으로 작성되었습니다. 제시된 아이디어는 NetEase Games의 테크니컬 오디오 팀에서 드미트리 파트라코브(Dmitry Patrakov), 러슬랜 네스테루크(Ruslan Nesteruk), 빅터 에르마코브(Victor Ermakov)와 함께 작업하면서 생긴 아이디어입니다. DevGAMM 슬라이드에 대한 피드백과 Wave 파일 가져오기 아이디어를 제공해주신 데미안 캐스트바우어(Damian Kastbauer), 동료 검토 및 스크립트 디버깅 부분을 포함하도록 제안해 주신 베르나르 로드리그(Bernard Rodrigue), 교정 및 출판 과정에 도움을 주신 마샤 리트비나바(Masha Litvinava), '스위치 컨테이너' 예시 아이디어를 제공해주신 타이요마 마키브(Tyoma Makeev), 그리고 이 자료를 Audiokinetic 블로그에 실제로 게시하도록 격려해주신 드니스 즐로빈(Denis Zlobin)에게 특별히 감사의 말씀을 전합니다.
부록: 실행 예시
여기에 제시된 모든 예시는 이 글에 첨부된 아카이브로 제공되며, 여기에서 다운로드할 수 있습니다. 내용물을 Wwise 프로젝트에 압축 해제하면 바로 사용할 수 있지만 예시는 Wwise Adventure Game 프로젝트를 기반으로 작성되었음을 유의하세요.
필요한 기타 사항:
- Git
- Python 3: 최신 버전이면 충분하지만 설치할 때 Python 실행 파일을 PATH에 추가하고 pip 및 tcl/Tk 컴포넌트를 설치하는 옵션을 선택하세요.
- Wwise 2021: 설명된 그대로 따르려면 Wwise Adventure Game을 설치하세요.
- Wwise 프로젝트 설정에서 WAAPI를 활성화하세요.
시스템에 Python을 설치한 후 다음 Python 패키지를 설치해야 합니다.
pip install -U pyperclip waapi-client
pip install -U git+https://github.com/ech2/waapi_helpers.git
또한 예시 6에서는 퍼지 문자열 일치 알고리즘을 사용하기 때문에 이를 실행하려면 다음 패키지도 설치해야 합니다.
pip install -U thefuzz python-Levenshtein
-
이 버그는 redirectOutput 옵션이 사용자 cwd와 상호 작용하는 방식과 관련된 확인된 버그였습니다.
-
이 방법을 훨씬 더 일찍 알았더라면 좋았을 텐데. JSON을 업데이트할 때마다 Wwise Authoring 도구를 재시작하곤 했죠.
-
이 글이 홍보로 인식되지 않도록
waapi-client
위에 작성한 래퍼를 사용하는 것에 대해 약간의 우려가 있었습니다. 하지만 솔직히 말해서 저는 거의 순수한 WAAPI를 Python에서 사용하지 않으며, 일반적인 WAAPI 함수의 JSON 스키마를 잊어버리기 시작했습니다. 독자들에게 제 도구를 사용하라고 권장하는 것이 아니며, 이를 수도 코드로 읽기를 권장해드립니다. -
이 강연은 러시아어로 진행되었지만 슬라이드는 영어로 작성되었습니다. 강연은 YouTube에 올라올 예정입니다. 슬라이드와 코드 예시는 URL: ech2/DevGAMM_2021_Fall 에서 다운로드할 수 있습니다.
댓글