Dans cet article, j'aimerais décrire une approche de travail avec WAAPI un peu particulière, approche que j'utilise depuis un certain temps déjà. Composée de Python, de compléments de commande et d'une petite bibliothèque d'aide, cette approche me permet d'écrire de nouveaux scripts WAAPI un peu plus rapidement qu'en utilisant la version brute de waapi-client
, et de les partager avec des collègues, dans la plupart des cas sans problème. Je vais également aborder les bases du fonctionnement de WAAPI et j'espère montrer à quoi cela ressemble de travailler avec des scripts WAAPI — ainsi, cet article pourrait également s'avérer utile aux débutants qui viennent de commencer à apprendre WAAPI, ou faire office en quelques sortes de guide de « démarrage » pour les personnes en dehors de l'équipe audio qui se retrouvent à aider leurs collègues avec des outils de travail et d'automatisation.
J'ai écrit quelques exemples de code uniquement destinés à cet article, gardez donc en mémoire qu'ils n'ont pas été testés de manière approfondie. Vous devrez configurer votre environnement PC afin de les exécuter, veuillez vous référer pour cela aux instructions de l'Annexe.
Concepts de base
Je pense que, dans un premier temps, il peut être utile de rappeler quelques informations de base concernant Wwise et WAAPI.
Les projets Wwise sont organisés en hiérarchies d'objets, les objets peuvent être de plusieurs types (Event, RandomSequenceContainer, etc.), les types d'objets décrivent quelles propriétés vont avoir ces objets (BusVolume, IsStreamingEnabled, etc.). La position relative dans la hiérarchie définit la manière dont les valeurs des propriétés sont héritées, c'est-à-dire que les objets enfants héritent des valeurs de leurs parents. Tout cela définit les règles de lecture du son ainsi que les configurations du moteur audio au moment de l'exécution.
WAAPI est une API client-serveur permettant de manipuler ces hiérarchies : renommer ou supprimer des objets, modifier leurs propriétés, etc. — cependant, tout n'est pas accessible, comme par exemple les RTPCs ou les mix résultant des Blends Containers, du moins au moment où cet article est écrit, mais beaucoup de choses restent néanmoins possibles. Ainsi, il est important de comprendre comment les projets Wwise sont structurés, de connaître les différents types d'objets, leurs propriétés, etc. Pour cela, j'ai mis les pages de référence suivantes dans mes favoris pour un accès rapide :
- Wwise Objects Reference — tous les types d'objets et leurs propriétés
- Wwise Authoring API Reference - Functions — les choses pouvant être interrogées ou modifiées.
- Wwise Authoring API Reference - Topics — les notifications que l'outil Wwise Authoring peut vous envoyer.
Cependant, WAAPI fournit également des informations d'objets qui ne sont pas répertoriées parmi les propriétés d'objets. Par exemple, les propriétés sound:convertedWemFilePath, sound:originalWavFilePath, ou même maxDurationSource qui fournit une valeur calculée depuis la différence des propriétés TrimEnd et TrimBegin de l'objet AudioSource, — celles-ci ne sont pas listées comme propriétés d'objet dans la référence d'objet Wwise, mais WAAPI peut malgré tout les interroger (peut-être dispose-t-il d'un accès spécial aux données du projet).
De plus, pour les utilisateurs avancés, il existe des schémas JSON et XML pour les données de WAAPI et de Wwise Authoring semblables aux Work Units, se trouvant dans le répertoire %WWISEROOT%\Authoring\Data\Schemas, et qui peuvent se révéler très pratiques dans certains cas.
Considérations
Les scripts WAAPI n'ont pas besoin d'être longs et complexes pour être utiles, notamment lorsqu'il s'agit d'automatiser des tâches laborieuses. En dehors des scripts eux-mêmes, les éléments suivants doivent idéalement être pris en compte :
- comment exécuter les scripts ;
- comment les distribuer entre les membres de l'équipe.
L'une des manières possibles que nous avons utilisée pour cela consiste à faire appel à une compilation personnalisée de Total Commander. Celle-ci est stockée sous SVN et comprend de nombreux utilitaires portables pour répondre à une grande variété de besoins techniques audio, et accessibles par des barres de menu / sous-menu en haut de l'application. Elle comprend également une distribution Python intégrée incluant des packages installés et que je garde à jour, y compris des scripts spécifiques au projet et des scripts WAAPI. De cette façon, les membres de l'équipe n'ont pas besoin de configurer leurs ordinateurs pour exécuter nos outils, et je dispose d'un moyen facile pour mettre à jour l'environnement de travail au complet et le synchroniser avec tous les autres. Au passage, Total Commander est vraiment excellent !
Une autre technique que j'utilise parfois consiste simplement à mettre tous les scripts Python dans un répertoire Scripts, directement dans le projet Wwise. Bien évidemment, cela limite la réserve de scripts disponibles à un projet spécifique.
En dehors des boutons accessibles dans Total Commander, j'aime aussi invoquer des scripts WAAPI par le biais de compléments de commande, qui définissent, dans l'outil de création, des commandes exécutant des outils externes arbitraires avec des arguments spécifiques au projet, comme le GUID d'un objet sélectionné, un chemin vers un projet Wwise, etc. Ces commandes peuvent être intégrées dans les menus de l'outil de création, ce qui est probablement le moyen le plus simple d'utiliser des outils personnalisés liés à Wwise.
Aperçu du flux d'exécution (workflow)
Afin de garder les choses simples pour les besoins de cet article, les scripts WAAPI seront exécutés exclusivement par le biais de compléments de commande, et les fichiers Python eux-mêmes seront placés dans le répertoire Scripts. Ainsi, cette configuration attend d'un utilisateur qu'il configure son environnement PC. Veuillez vous référer à l'Annexe pour les instructions.
Comme exigence supplémentaire, j'aimerais que toutes les erreurs soient affichées dans une interface graphique GUI - d'après mon expérience, cela facilite le traitement des problèmes que peuvent rencontrer les spécialistes sonores, plutôt que de leur demander de chercher dans une fenêtre de console.
Organisation du projet Python
La capture d'écran ci-dessous montre la structure du dossier Scripts :
Quelques points importants :
- Le dossier racine ne contient directement que les scripts qui sont invoqués par Wwise à partir de compléments de commande, ils ne doivent donc pas être importés par d'autres fichiers Python. Cette règle comprend les deux exceptions suivantes :
- __init__.py — un fichier qui marque le dossier Scripts comme module, ce qui nous permet d'utiliser les importations relatives de nos scripts lorsque le répertoire de travail courant est défini comme le dossier du projet Wwise ;
- _template.py — un fichier modèle pour réaliser de nouveaux scripts ; il contient du contenu boilerplate destiné à être dupliqué, renommé et modifié pour répondre à des besoins concrets.
- Les sous-modules contiennent du code dont la taille est plus que triviale ou des fonctions qui sont réutilisées dans plusieurs scripts.
- Les scripts sont exécutés en communiquant leurs chemins comme arguments à l'interpréteur Python. J'aurais préféré exécuter les scripts par nom de module (flag
-m
), mais au moment d'écrire ces lignes, je n'ai pas réussi à faire fonctionner les compléments de commande lorsque le répertoire de travail actuel est défini à ${WwiseProjectRoot}, donc sans utiliser des importations relatives pour le moment.1 - Par convention, les scripts qui rencontrent une erreur ou détectent que leurs données sont dans un état invalide doivent appeler des exceptions
RuntimeError
. De telles exceptions sont capturées dans la zone la plus externe, et leurs messages d'erreur sont affichés dans des boîtes de dialogue.
Le code du modèle Python est indiqué ci-dessous. Il peut sembler long, mais n'oubliez pas qu'il gère les erreurs et les affiche dans une interface graphique, et qu'il fournit des importations courantes. Je me contente simplement de copier et coller ce modèle lors de l'écriture d'un nouveau script.
# ceci empêche ce fichier d'être importé
if __name__ != '__main__':
print(f'error: {__file__} should not be imported, aborting script')
exit(1)
# tkinter est utilisé pour afficher des boîtes de dialogue d'alerte et d'autres IU simples
import tkinter
from tkinter.messagebox import showinfo, showerror
from waapi import WaapiClient, CannotConnectToWaapiException
from waapi_helpers import *
from helpers import *
# n'hésitez pas à supprimer les importations inutilisées par votre script et/ou à en ajouter de nouvelles
# initialise les widgets Tk et cache l'icône pour qu'elle n'apparaisse pas dans la barre des tâches
tk = tkinter.Tk()
tk.withdraw()
# le but de ce bloc de processus try-except est de détecter les erreurs d'exécution
# et de les afficher à un-e utilisateur-ice dans une fenêtre GUI et non dans la console
try:
with WaapiClient() as client:
# notre script commence ici
pass
except CannotConnectToWaapiException:
# imprime un message 'compréhensible' en cas d'échec de connexion à une application Wwise en cours d'exécution
showerror('Error', 'Could not establish the WAAPI connection. Is the Wwise Authoring Tool running?')
except RuntimeError as e:
# erreurs 'attendues', càd résultant directement des scripts
showerror('Error', f'{e}')
except Exception as e:
# les erreurs inattendues affichent toujours des traces d'appels
import traceback
showerror('Error', f'{e}\n\n{traceback.format_exc()}')
finally:
# nous devons appeler ceci afin d'arrêter la boucle d'événements de la fenêtre Tk,
# sans quoi le script se bloquera à cet endroit
tk.destroy()
Configuration des compléments de commande
Il n'y a pas grand-chose à mentionner concernant la configuration des compléments de commande, car ce processus est assez bien expliqué dans la documentation officielle. Je vais utiliser un seul fichier JSON localisé dans le répertoire Wwise Project/Add-ons/Commands/ak_blog_addons.json
pour contenir tous les exemples, vous pouvez consulter ce fichier vous-même en téléchargeant le fichier archive fourni (voir l'Annexe).
Attention, après avoir modifié les fichiers JSON, vous devrez recharger les compléments de commande dans l'outil de création Wwise. Il n'y a pas de touche de raccourci pour effectuer cette action, mais vous pouvez la rechercher et l'exécuter en tapant le symbole « Supérieur » suivi de « command » dans le champ de recherche2 :
Debug des scripts WAAPI
Bien qu'il y ait un onglet WAAPI dans la fenêtre Logs, tout n'y est pas indiqué par défaut, et vous pouvez décider d'activer des éléments supplémentaires dans les paramètres Log Settings. De plus, le paramètre redirectOutputs d'un complément de commande JSON forcera Wwise à rediriger la sortie console des scripts Python vers l'onglet General, ce qui est désactivé par défaut.
À propos de waapi_helpers3
J'ai écrit une petite bibliothèque waapi_helpers que j'utilise dans les exemples tout au long de cet article. Elle consiste en quelques petites aides indépendantes qui acceptent WaapiClient comme argument, de sorte qu'elles peuvent être mélangées avec le code waapi-client
brut. Toutes les fonctions suivent la convention concernant l'obtention des propriétés, de manière que si une propriété n'existe pas, la valeur doit être None, purement et simplement. Je ne vais pas entrer dans les détails ici, car les exemples ci-dessous vont montrer cela plus clairement.
Exemples
La plupart des exemples présentés ici, et en fait la plupart de mes propres scripts WAAPI, suivent plus ou moins la même structure : parcourir un projet Wwise et collecter des informations, faire quelque chose avec ou transformer les informations, appliquer les changements au projet Wwise.
E1 : copier dans le presse-papiers le GUID des objets sélectionnés
Une commande étonnamment utile qui ne nécessite pas WAAPI et qui existe déjà dans Wwise. Elle est également tellement simple que je vais transcrire intégralement le JSON de son complément de commande avec la source Python.
copy_guid.py (vraiment rien de spécial, la majeure partie du modèle a été nettoyée) :
if __name__ != '__main__':
print(f'error: {__file__} should not be imported, aborting script')
exit(1)
# ceci est une librairie tierce partie
import pyperclip
# une fonction simple qui retourne les arguments dans argv en tant que liste
# notez l'utilisation d'un import relatif car il y a un sous-module 'helpers'
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 — un identifiant unique pour cette nouvelle commande.
- displayName — un nom lisible par n'importe qui pour afficher le nom de cette commande dans les menus.
- program — un programme à exécuter lorsqu'un-e utilisateur-ice exécute cette commande ; notez que le chemin du script est écrit avec des escape quotes — ceci est important afin d'aider Wwise à traiter ce chemin comme un seul argument et à ne pas le diviser.
- startMode — Wwise appellera le programme une fois et passera les arguments séparés par des espaces, soit des GUIDs d'objets dans notre cas.
- args — arguments passés à Python :
- ${WwiseProjectRoot}/Scripts/copy_guid.py est un chemin de script encadré d'escape quotes afin d'être traité comme un seul argument lorsque le chemin contient des espaces ;
- ${id} est un argument spécial qui sera substitué par Wwise avec les GUIDs des objets sélectionnés.
- redirectOutputs — aide pour le debug des scripts en redirigeant leur stdout vers la fenêtre Logs de Wwise ; désactivé par défaut.
- contextMenu — configure cette commande pour qu'elle apparaisse dans les menus contextuels de tous les objets dans les hiérarchies du projet Wwise, dans notre cas, il y aura un sous-menu appelé WAAPI.
Après avoir rafraîchi les compléments de commande, un élément apparaîtra dans un menu contextuel :
En cliquant dessus, le texte suivant sera copié dans le presse-papiers du système :
{2E9E3B71-C905-4BB0-9B30-06CFF26E0C5E} {3AD5C9DF-C0B5-4A78-B87A-2EE37D64BFCB} {8F7A715D-5704-4F09-9563-4172E250419B}
E2 : montrer les noms de tous les Events
Cela n'est pas du tout pratique, mais montre comment utiliser la fonction walk_wproj
pour traverser les hiérarchies. Le contenu boilerplate est omis.
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))
Ici, la fonction walk_project
parcourt chaque objet tout au long de la hiérarchie en commençant par le chemin \Events, et rapporte les propriétés id et name de chaque objet Event qu'elle rencontre. C'est avec ces fonctions que j'ai commencé ma bibliothèque, car je voulais une interface Python simple permettant d'itérer et de parcourir les hiérarchies Wwise facilement, comme le proposent les bibliothèques XML. Je voulais également éviter de créer et extraire des objets JSON, car leurs schémas diffèrent selon les commandes, ce qui est difficile à garder en mémoire lorsque l'on travaille.
Les noms sont rassemblés dans un tableau de valeurs et exportés ensemble une fois l'itération terminée. C'est inutile, mais et sert à des fins d'apprentissage. Le résultat ressemblera à quelque chose comme ceci :
E3 : réinitialiser les faders de volume
Parfois, peut-être pendant le mixage, je veux remettre à zéro tous les faders d'une certaine partie de la hiérarchie. Le script suivant le fera pour tous les enfants d'un objet sélectionné, sauf ceux marqués d'un tag @ignore dans leurs notes.
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
# remarque, nous voulons changer différentes propriétés selon que
# l'objet appartient à l'Actor-Mixer Hierarchy ou au Master Mixer Hierarchy
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:
# par convention, si une propriété n'existe pas,
# `get_property_value` retournera la valeur None, ce qui
# permet de ne pas appeler `set_property_value`
# lorsqu'il n'y a pas de propriété de volume sur l'objet
set_property_value(client, obj_id, prop_name, 0)
num_reset_faders += 1
showinfo('Info', f'{num_reset_faders} faders were reset')
Résultats :
E4 : supprimer des Events invalides
Imaginons que nous venons de supprimer un certain nombre d'objets obsolètes dans l'Actor-Mixer Hierarchy. Cela peut avoir laissé beaucoup de références invalides dans des actions d'Events, certains Events pouvant même être devenus inutiles à cause de cela, car toutes leurs actions font maintenant référence à des objets inexistants. Dans ces conditions, nous pouvons supposer que ces Events peuvent être supprimés en toute sécurité. Nous pouvons le faire avec WAAPI en parcourant tous les Events et en vérifiant si toutes leurs actions font référence à des objets inexistants. Si oui, nous identifions l'Event comme devant être supprimé.
delete_invalid_events.py:
# un ensemble d'identifiants de types d'action qui font référence à des objets,
# voir la page de référence des actions d'objets pour plus de détails
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') # avec des majuscules conformément à la convention Wwise
showinfo('Success', f'{len(events_to_delete)} were deleted')
Celui-ci est un peu plus complexe.
Notez tout d'abord que nous avons sauvegardé un ensemble de types d'actions qui peuvent potentiellement faire référence à des objets (par exemple Play, Stop, Set RTPC, etc.) - chacune d'entre elles faisant référence à un objet inexistant est considérée comme invalide. Cet ensemble a été peuplé à la main sur la base de la documentation de l'objet Action (voir la description de la propriété ActionType).
Une autre chose nouvelle pour nous est que nous demandons à l'usager la permission d'effectuer une action destructive, — on lui montrera une boîte de dialogue demandant de confirmer si un certain nombre d'Events doivent être supprimés ou non.
Et enfin, la commande utilise la fonctionnalité undo group de WAAPI. Elle nous permet d'annuler des changements effectués par plusieurs appels WAAPI en une seule action d'annulation, qui aura elle-même un nom spécial dans le menu Edit : « Delete Invalid Events » (Supprimer les événements non valides).
Captures d'écran correspondantes :
E5 : activer le streaming pour les SFX de longue durée
La configuration groupée de la fonction de streaming des SFX peut s'avérer pertinente, car les spécialistes sonores peuvent être amenés à ajuster la longueur des sources sonores, de sorte que la durée des fichiers Wave peut différer sensiblement de celle des SFX au moment de l'exécution. Au moment de la rédaction de cet article, il est impossible d'obtenir la durée des SFX ajustés, ni par Wwise Queries, ni par WAQL.
Heureusement, WAAPI dispose de la propriété trimmedDuration. Mais allons plus loin et faisons en sorte qu'il soit possible de configurer depuis l'application Wwise Authoring les paramètres de streaming que notre outil va définir. Dans cet exemple, je vais placer la configuration dans la section notes de \Actor-Mixer Hierarchy\Default Work Unit et utiliser la syntaxe configparser de Python, très simple et déjà intégrée dans Python. Notre configuration ressemblerait à ceci :
[Enable_Streaming_For_SFX]
If_Longer_Than = 10
Non_Cachable = no
Zero_Latency = no
Prefetch_Length_Ms = 400
Pour un projet plus important, vous voudriez peut-être avoir un contrôle plus granulaire sur les paramètres de streaming pour différents types de sons, ou même selon les plateformes. Le bon côté est que vous pouvez ajouter plusieurs sections de configuration et récupérer dans le script celle dont vous avez besoin pour chaque tâche particulière.
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 est en secondes, non en millisecondes
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')
Et voici à quoi ressemble la configuration :
E6 : reconfigurer un groupe de Containers en un Switch
Parfois, nous avons besoin de reconfigurer plusieurs Containers de l'Actor-Mixer Hierarchy en un Switch Container. Nous pouvons essayer de faire un outil qui automatise cela, avec un processus utilisateur ressemblant à ceci : sélectionner quelques objets, cliquer sur un bouton, un nouveau Switch Container apparaît ayant les objets sélectionnés assignés aux Switches. Pour cet exemple, je vais utiliser un Switch Surface_Type provenant du projet Wwise Adventure Game.
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:
# annule les changements si l'exécution du script échoue en cours d'opération
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.')
# reconfigure les objets sélectionnés
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')
Ce code est un peu plus complexe que les exemples précédents, j'ai même dû reconfigurer certaines parties en deux fonctions afin de garder le code plus court. La première fonction, get_switches_for_group_surface_type
, est une aide pour obtenir les GUIDs et les noms de tous les Switches Surface_Type. La seconde, infer_obj_assignments
, tente de faire correspondre les objets sélectionnés aux Switches en comparant leurs noms par paires, et en sélectionnant le nom du Switch ayant le plus de similarités (fonction partial_ratio
venant de la bibliothèque thefuzz).
Autres points à noter :
- Le code effectue différentes validations de données tout au long de l'exécution et rapporte des exceptions RuntimeError si l'état est invalide. Ces exceptions sont affichées à l'usager sous forme de fenêtres d'erreur.
- Sur certains chemins d'erreur, juste avant de rapporter une exception, le script annule toutes les modifications apportées jusqu'à présent par WAAPI en effectuant une opération d'annulation.
- Vous pouvez également voir qu'il y a un appel direct à
waapi-client
car il n'y a pas de fonction d'assignation pour les Switch Containers dans la bibliothèque d'aide. - Ce script est loin d'être parfait, il ne couvre probablement pas certaines situations spécifiques, et n'est pas du tout optimisé, — c'était juste une chose amusante à faire le soir en écrivant cet article. Je vous aurais prévenus.
Les captures d'écran ci-dessous montrent comment il fonctionne.
E7 : supprimer les fichiers Wave inutilisés du dossier Originals
Au fil du temps, les projets Wwise peuvent accumuler des fichiers Wave dans le dossier Originals qui ne sont référencés nulle part et ne font que gaspiller de l'espace disque. Le processus utilisateur est simple : appuyer sur un bouton, le script devrait demander une confirmation, puis notifier si tout a été supprimé ou s'il reste quelque chose, par exemple si un fichier est ouvert dans Audition ou est verrouillé d'une autre façon. Un conseil : lancez un Integrity Report (rapport d'intégrité) après de telles opérations.
remove_unused_wavs.py:
with WaapiClient() as client:
default_wu_path, = get_object(client, '\\Actor-Mixer Hierarchy\\Default Work Unit', 'filePath')
# Cette fonction analyse le fichier .wproj pour déterminer où se trouve le répertoire 'Originals'
origs_dir = find_originals_dir(default_wu_path)
wavs_in_origs = set()
wavs_in_wproj = set()
# nous ne voulons pas toucher au répertoire '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))
# note, un seul walk_wproj peut traverser une hiérarchie
# plusieurs fois depuis différents endroits
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?')
Même si ce code semble plus simple que dans l'exemple précédent, nous avons rencontré ici certaines limitations de WAAPI et avons dû analyser le fichier de projet Wwise afin d'obtenir le chemin d'accès au dossier Originals. Initialement, je pensais que cette information était stockée dans l'objet Projet, mais ce n'est apparemment pas le cas, et je n'ai pas été capable de trouver une autre façon de l'identifier.
Une autre chose peu évidente est que j'ai demandé à walk_wproj
de récupérer tous les objets de type AudioFileSource — mais ce type n'est pas répertorié sur la page Wwise Objects Reference ! Le type qui s'en rapproche le plus est AudioSource, lequel pourrait être un type parent, mais je ne peux pas le confirmer. J'ai probablement essayé cela parce que j'ai travaillé avec l'analyse et la génération de Work Unit XML à un moment donné, et je me suis souvenu que les fichiers Wave sont encapsulés dans des tags <AudioFileSource/>
, donc ce choix semblait intuitif, croyez-le ou non. De plus, ce type est répertorié dans le fichier de schéma XML qui se trouve dans l'emplacement %WWISEROOT%\Authoring\Data\Schemas.
Captures d'écran indispensables :
E8 : Automatisation de l'importation de fichiers Wave
Dans de nombreux cas, les sons du jeu sont représentés par des hiérarchies spécifiques, avec éventuellement plusieurs couches (ou composants) de sons pouvant être acheminées vers différents bus, contrôlés par différents RTPC, etc. Certains de ces systèmes peuvent être modélisés comme des hiérarchies avec des « slots », c'est-à-dire des emplacements, où des ressources audio sont attachées afin de créer un son en particulier au sein de ce système. En pratique, cela revient en quelques sortes à dupliquer une hiérarchie existante et à remplacer des assets situés dans les « slots » appropriés.
Nous pouvons bien évidemment faire cela avec WAAPI, et par souci de simplicité, je n'implémenterai qu'un seul processus utilisateur de ce workflow.
- Un usager configure une hiérarchie de modèles et annote ses parties, en spécifiant le nom du modèle, les slots, leurs noms, etc. Dans l'image ci-dessous, le modèle Gun contient des slots Shot, Tail_Indoor et Tail_Outdoor.
- L'usager fait un clic-droit sur le modèle, appuie sur un bouton et est invité à sélectionner un répertoire contenant des fichiers Wave formatés par nom. Après la sélection, l'outil analyse le répertoire et trouve les fichiers correspondant à la nomenclature utilisée dans ce modèle en particulier.
- Une fois que l'usager a confirmé l'importation, le modèle est copié, renommé et les slots sont remplis avec les fichiers Wave.
Sur les captures d'écran, l'outil a implémenté deux objets Sound, M4 et M1911, en une seule fois. De plus, il a enregistré le GUID de l'objet modèle dans les notes du dossier virtuel de chaque Gun, afin que d'autres scripts puissent les parcourir si nécessaire.
Veuillez consulter le code source du fichier import_wavs.py pour un exemple pratique. Ce code est un peu plus complexe que les autres pour être entièrement exposé ici.
Conclusions
Dans cet article, j'ai présenté un workflow que j'utilise pour travailler avec WAAPI et Python, ainsi que des manières d'organiser le code, et de partager les scripts avec l'équipe audio. Pour appuyer mes idées, j'ai donné quelques exemples d'outils qui utilisent l'implémentation de ce workflow. Surtout n'hésitez pas à me joindre ou à laisser un commentaire sur cet article si vous avez des questions, des suggestions, etc.
Voici certaines (des nombreuses) choses qui peuvent être améliorées :
- Envisager d'utiliser
allow_exception=True
lors de l'instanciation de WaapiClient — de cette façon, vous pouvez identifier les exceptions spécifiques à WAAPI et donner de meilleurs messages d'erreur à l'utilisateur-ice, par exemple lorsqu'une certaine boîte de dialogue est ouverte dans Wwise. - Dans notre convention de projet Python, tous les scripts de compléments de commande sont placés directement dans le dossier Scripts. Ceci peut être utilisé pour générer automatiquement les fichiers JSON des modules complémentaires.
- Les configurations stockées dans la section notes peuvent être améliorées en les séparant d'une manière similaire à YAML front matter, qui est largement utilisé par les frameworks de développement de sites web comme Jekyll. Cela leur permettrait de coexister avec des notes ordinaires rédigées par des utilisateurs-ices.
- Dans l'exemple 4, en plus de supprimer les Events invalides, il pourrait être judicieux de supprimer également les Actions invalides, car elles sont actuellement laissées intactes dans le projet, ce qui peut provoquer des erreurs dans l'Integrity Report.
- La commande de reconfiguration des Switches peut utiliser un algorithme qui essaye de donner le meilleur nom à un Switch, par exemple en obtenant une sous-chaîne de caractères commune à tous les objets sélectionnés, ou... en utilisant une IA pour résumer du texte !
Remerciements
Cet article est basé sur une courte conférence que j'ai donnée au DevGAMM Fall 20214. Les idées présentées ont été élaborées alors que je travaillais au sein d'une équipe technique audio chez NetEase Games avec les personnes suivantes (par ordre alphabétique) : Dmitry Patrakov, Ruslan Nesteruk, Victor Ermakov. J'aimerais remercier en particulier Damian Kastbauer pour ses commentaires sur ma présentation au DevGAMM et pour l'idée de l'importateur de fichiers Wave ; à Bernard Rodrigue pour son examen par les pairs et sa suggestion d'inclure une partie de debug de script ; à Masha Litvinava pour sa relecture et son aide dans le processus de publication ; à Tyoma Makeev pour son idée d'exemple de « Switch Container », à Denis Zlobin pour son encouragement à publier réellement ces documents sur le blog Audiokinetic.
Annexe : exemples pratiques
Tous les exemples présentés ici sont regroupés dans une archive, que vous pouvez télécharger ici. Il suffit de décompresser son contenu dans n'importe quel projet Wwise et vous êtes prêt à démarrer, mais notez que les exemples ont été réalisés en travaillant avec le projet Wwise Adventure Game.
Voici certaines choses additionnelles dont vous aurez besoin :
- Git
- Python 3, n'importe quelle version devrait fonctionner, mais lors de l'installation, assurez-vous de cocher les cases qui ajoutent l'exécutable Python à PATH, ainsi que d'installer les composants pip et tcl/Tk.
- Wwise 2021, installez le projet Wwise Adventure Game si vous voulez reproduire exactement les exemples.
- Dans les paramètres du projet Wwise, assurez-vous que WAAPI soit activé.
Une fois Python installé, vous devrez ajouter certains packages Python :
pip install -U pyperclip waapi-client
pip install -U git+https://github.com/ech2/waapi_helpers.git
De plus, comme l'exemple 6 utilise l'algorithme de correspondance de chaînes de caractères floues, vous devrez également installer les packages suivants si vous souhaitez l'exécuter :
pip install -U thefuzz python-Levenshtein
-
Il s'agissait d'un bug valide lié à la façon dont l'option redirectOutput interagit avec un cwd personnalisé. ↩
-
J'aurais aimé apprendre cette astuce bien plus tôt, car j'avais l'habitude de redémarrer l'outil Wwise Authoring après chaque mise à jour JSON. ↩
-
J'avais quelques réserves quant à l'utilisation d'un wrapper que j'avais écrit en plus de
waapi-client
pour éviter que cet article soit perçu comme biaisé. Mais pour être honnête, je n'utilise presque jamais la version brute de WAAPI en Python, et j'ai déjà commencé à oublier le schéma JSON des fonctions WAAPI habituelles. Je n'encourage en aucun cas les lecteurs à utiliser mon outil, et recommande de le lire plutôt comme un pseudo code. ↩ -
La conférence a été donnée en russe, mais les images de la présentation sont en anglais. La conférence devrait éventuellement apparaître sur YouTube. Les images et les exemples de code peuvent être téléchargés à l'adresse URL : ech2/DevGAMM_2021_Fall ↩
Commentaires