通过 ReaWwise 在 ReaScript (Lua) 中调用 WAAPI

音频编程 / Wwise 技巧和工具

ReaWwise 有个大家可能不太知道的功能,就是将原始 WAAPI 函数暴露给 REAPER。藉此,用户可在自己的 ReaScript 中使用这些函数。在本文中,我们将探讨如何使用 WAAPI 在 ReaScript Development Environment 中轻松实现一些与 Wwise 相关的基本功能。

前提要求

WAAPI 和 Lua

在阅读接下来的章节之前,建议先对 WAAPI、Lua 和 ReaScript 做一个基本的了解。这里有一些学习资源,感兴趣的话不妨看下:

ReaWwise

要在 ReaScript 内运行 WAAPI 命令,必须安装 ReaWwise。 

快速入门

The ReaScript Development Environment

要进入 ReaScript Development Environment,请先打开 REAPER。然后,在 REAPER 菜单中依次选择 Actions > Show action list...。接着,在 Actions 对话框右下角依次单击 New action... > New ReaScript...。这时会提示保存文件。对 ReaScript 文件进行命名,然后单击 Save。在保存之后,便会打开 ReaScript Development Environment。我们将在此编写代码。

Hello World

-- Waapi Hello World.lua

if(reaper.AK_Waapi_Connect("127.0.0.1", 8080)) then
    reaper.ShowConsoleMsg("Successfully connected to Wwise!")

    reaper.AK_Waapi_Disconnect()
end

在上面的代码段中,我们首先调用 AK_Waapi_Connect。此函数会获取 WAAPI 与 Wwise 通信所用的 IPPORT。它会返回一个布尔值:若连接成功,则返回 true;否则,返回 false。在 if 语句末尾,确保通过调用 AK_Waapi_Disconnect 来终止连接。若脚本运行成功,REAPER console output 窗口会弹出以下消息:

ReaScript console output 显示 "Successfully connected to Wwise!"

基本示例 (Get Selected Objects)

在以下示例中,我们来看看如何在 Wwise 中查询当前选定的对象。

-- Get Selected Objects.lua

if(reaper.AK_Waapi_Connect("127.0.0.1", 8080)) then
    local fieldsToReturn = reaper.AK_AkJson_Array()

    reaper.AK_AkJson_Array_Add(fieldsToReturn, reaper.AK_AkVariant_String("path"))

    local options = reaper.AK_AkJson_Map()
    reaper.AK_AkJson_Map_Set(options, "return", fieldsToReturn)

    local result = reaper.AK_Waapi_Call("ak.wwise.ui.getSelectedObjects",
      reaper.AK_AkJson_Map(), options)

    local status = reaper.AK_AkJson_GetStatus(result)

    if(status) then
      local objects = reaper.AK_AkJson_Map_Get(result, "objects")
      local numObjects = reaper.AK_AkJson_Array_Size(objects)

      for i=0, numObjects - 1 do
        local item = reaper.AK_AkJson_Array_Get(objects, i)
        local path = reaper.AK_AkJson_Map_Get(item, "path")
        local pathStr = reaper.AK_AkVariant_GetString(path)
        reaper.ShowConsoleMsg(pathStr .. "\n")
      end
    end

    reaper.AK_AkJson_ClearAll()
    reaper.AK_Waapi_Disconnect()
  end

AkJson

为了方便创建 JSON 对象,ReaWwise 导出了各种辅助函数。藉此,ReaScript 可根据脚本编写者的需求动态创建 JSON 对象。

调用 WAAPI 需要三个元素:参数选项命令字符串。在本例中,命名字符串为 ak.wwise.ui.getSelectedObjects。有关 WAAPI 命令的详尽列表,请参阅 WAAPI 参考

对于这个命令,WAAPI 会将空映射作为参数。在此,我们只需设置 options 映射即可。

我们要创建的 options 映射就跟下面一样:

{
    "return": [
        "path"
    ]
}

由于 REAPER 的 Lua 上下文和 ReaWwise 后端之间只能传递值类型和指针,所以需要分多个步骤来创建 options 映射。

首先,创建 fieldsToReturn 数组:

local fieldsToReturn = reaper.AK_AkJson_Array()

fieldsToReturn 变量会返回指向 ReaWwise 后端中数组的指针。利用该指针,我们可以对数组进行操作。

然后,将 path 字符串作为元素添加到 fieldsToReturn

reaper.AK_AkJson_Array_Add(fieldsToReturn, reaper
    .AK_AkVariant_String("path"))

最后,创建 options 映射并将 fieldsToReturn 数组添加为键值为 return 的值:

    local options = reaper.AK_AkJson_Map()
    reaper.AK_AkJson_Map_Set(options, "return", fieldsToReturn)

调用 WAAPI

在创建全部所需参数后,将其用作调用 WAAPI 时的输入:

local result = reaper.AK_Waapi_Call("ak.wwise.ui.getSelectedObjects",
    reaper.AK_AkJson_Map(), options)

然后,便可察看结果以确认对 WAAPI 的调用是否成功。注意,复杂的数据结构无法直接从 ReaWwise 后端传递到 Lua 上下文。在这里,结果变量是一个指针。

我们可以查询 status:

local status = reaper.AK_AkJson_GetStatus(result)

若 status 为 true,就可继续查询调用 WAAPI 时返回的实际数据:

local objects = reaper.AK_AkJson_Map_Get(result, "objects")
local numObjects = reaper.AK_AkJson_Array_Size(objects)

然后,遍历 numObjects 并提取所需的数据:

for i=0, numObjects - 1 do
    ...
end

每个对象都表示为映射。要访问对象的特定属性,可使用 ReaWwise 后端提供的辅助函数。

要从 objects 数组获取单个对象,需要使用 objects 指针和索引值: 

local item = reaper.AK_AkJson_Array_Get(objects, i)

变量 item 是指向映射对象的指针。该对象代表 objects 数组中的单个对象。然后,便可提取 path 变量:

local path = reaper.AK_AkJson_Map_Get(item, "path")

映射和数组值在内部存储为变体对象。要提取最终值,得知道该值代表什么数据。在本例中,我们知道应该获取字符串。为此,使用以下辅助函数从变体数据类型获取字符串:

local pathStr = reaper.AK_AkVariant_GetString(path)

变量 pathStr 是个常规 Lua 字符串,在后续的脚本代码中都可以使用。脚本会直接将 pathStr 输出到 REAPER 控制台。

reaper.ShowConsoleMsg(pathStr .. "\n")

由于 ReaWwise 后端需要追踪整个 Lua 代码中使用的引用类型对象,所以在不再需要这些对象时必须予以清除:

reaper.AK_AkJson_ClearAll()

Advanced Example (Import)

在下一示例中,我们会做一些更复杂的操作。比如,把音频文件从 REAPER 渲染目录传输到 Wwise 中。在此,每个音频文件都将作为 Sound SFX 导入到已在脚本中硬编码的导入目标下。此处要使用的底层 WAAPI 命令为 ak.wwise.core.audio.import

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
-- Import audio files in render directory.lua --
 
if(reaper.AK_Waapi_Connect("127.0.0.1", 8080)) then
    local windows = string.find(reaper.GetOS(), "Win") ~= nil
    local separator = windows and '\\' or '/'
 
    local importDestination = "\\Actor-Mixer Hierarchy\\Default Work Unit"
 
    -- 渲染目录 --
    local state, projectPath = reaper.EnumProjects(-1)
 
    local directory = projectPath:sub(1,string.len(projectPath) -
        string.reverse(projectPath):find(separator))
 
    -- import 命令 --
    local importCommand = "ak.wwise.core.audio.import"
 
    -- importOperation --
    local importOperation = reaper.AK_AkVariant_String("replaceExisting")
 
    -- default --
    local default = reaper.AK_AkJson_Map()
    local importLanguage = reaper.AK_AkVariant_String("SFX")
    reaper.AK_AkJson_Map_Set(default, "importLanguage", importLanguage)
 
    -- imports --
    local imports = reaper.AK_AkJson_Array()
 
    -- autoAddToSourceControl --
    local autoAddToSourceControl = reaper.AK_AkVariant_Bool(true)
 
    -- arguments --
    local arguments = reaper.AK_AkJson_Map()
    reaper.AK_AkJson_Map_Set(arguments, "importOperation", importOperation)
    reaper.AK_AkJson_Map_Set(arguments, "default", default)
    reaper.AK_AkJson_Map_Set(arguments, "imports", imports)
    reaper.AK_AkJson_Map_Set(arguments, "autoAddToSourceControl",
        autoAddToSourceControl)
 
    -- options --
    local options = reaper.AK_AkJson_Map()
 
    -- 构建 import 请求 --
    local currentFilePath = ""
    local fileIndex = 0;
 
    while(currentFilePath ~= nil) do
      currentFilePath = reaper.EnumerateFiles(directory, fileIndex)
 
      if currentFilePath ~= nil then
        local isWav = currentFilePath:find(".wav")
 
        if isWav then
          local importItem = reaper.AK_AkJson_Map()
          reaper.AK_AkJson_Map_Set(importItem, "audioFile",
            reaper.AK_AkVariant_String(directory .. separator .. currentFilePath))
 
          reaper.AK_AkJson_Map_Set(importItem, "objectPath",
            reaper.AK_AkVariant_String(importDestination .. "\\<Sound SFX>" ..
            currentFilePath))
 
          reaper.AK_AkJson_Map_Set(importItem, "originalsSubFolder",
            reaper.AK_AkVariant_String(""))
 
          reaper.AK_AkJson_Array_Add(imports, importItem)
        end
      end
 
      fileIndex = fileIndex + 1
    end
 
    local numFilesToImport = reaper.AK_AkJson_Array_Size(imports)
 
    if numFilesToImport > 0 then
      local result = reaper.AK_Waapi_Call(importCommand, arguments, options)
      local status = reaper.AK_AkJson_GetStatus(result)
 
      if status then
        reaper.ShowConsoleMsg("Successfully imported " .. numFilesToImport .. " audio
          files\n")
      else
        local errorMessage = reaper.AK_AkJson_Map_Get(result, "message")
        local errorMessageStr = reaper.AK_AkVariant_GetString(errorMessage)
 
        reaper.ShowConsoleMsg("Import failed: " .. errorMessageStr .. "\n")
 
        local details = reaper.AK_AkJson_Map_Get(result, "details")
        local log = reaper.AK_AkJson_Map_Get(details, "log")
        local logSize = reaper.AK_AkJson_Array_Size(log)
 
        for i=0, logSize - 1 do
          local logItem = reaper.AK_AkJson_Array_Get(log, i)
          local logItemMessage = reaper.AK_AkJson_Map_Get(logItem, "message")
          local logItemMessageStr = reaper.AK_AkVariant_GetString(logItemMessage)
          reaper.ShowConsoleMsg("["..i.."]" .. logItemMessageStr .. "\n")
        end
 
      end
    else
      reaper.ShowConsoleMsg("No audio files detected in render directory ...")
    end
 
  reaper.AK_AkJson_ClearAll()
  reaper.AK_Waapi_Disconnect()
end

对于这个示例,我不会详细阐释每行代码的作用,因为有关 API 的大部分概念已经在上一示例中说过了。不过,我会大致介绍一下各段代码的作用。

第 3-13 行:连接到 WAAPI,设置 importDestination,然后得出渲染目录。在此脚本中,我们假定渲染目录与工程文件的父目录相同。

第 15-41 行:构建所有要作为输入传给 AK_Waapi_Call 函数的 AkJson 结构。

第 44-70 行:遍历渲染目录并将遇到的所有 WAV 文件添加到要导入的音频文件列表。

第 74-101 行:执行 AK_WAAPI_Call 函数并在 REAPER 控制台中显示结果。这些代码行还包含在遇到错误时从结果中提取错误信息的逻辑。

结语

在本文中,我们查看了几个直接在 Lua ReaScript 中使用 WAAPI 的示例。从与 Wwise 的基本连接到查询 Wwise,再到导入音频文件等较为复杂的操作。希望此 API 能在工作流程当中给大家带来帮助。同时,期待看到各位在 ReaScript 中灵活运用 WAAPI。

安德鲁·科斯塔 (Andrew Costa)

软件研发

安德鲁·科斯塔 (Andrew Costa)

软件研发

在过去的8年中,Andrew一直作为软件开发人员为内容创作者建立工具。他对软件和音乐制作充满热情。他2021年加入Audiokinetic的研发团队,主要从事ReaWwise的开发工作。

评论

留下回复

您的电子邮件地址将不会被公布。

更多文章

关于音频复用和高效利用的几点经验分享

大家好,“如何对音频资源做有节制、更高效的利用”,应该是大家工作中都会考虑的问题。...

30.12.2019 - 作者:胡正伟

EBP管线概述

EBP是什么? 前不久Wwise 2019.2版本的UE4 Integration推出了一条新的管线,叫做Event-Based...

10.12.2020 - 作者:范润鹏

对白 | 基于Wwise与Unreal Engine的语音设计

22.12.2021 - 作者:杰克•盖米林 (Jake Gamelin)

浅析游戏中的音频GameObject管理

一、前言...

17.5.2022 - 作者:徐巍

WAQL 2.0

自 Wwise Authoring Query Language (WAQL) 的第一个版本发布以来已经有几年了。在此之后,几乎没什么改动。最大的改动就是把 WAQL 集成到了 Wwise...

10.8.2023 - 作者:伯纳德 罗德里格 (Bernard Rodrigue)

Wwise Spatial Audio 2023.1 新增功能 | Reverb Zone

Reverb Zone 简介 在 Wwise 23.1 中,我们为 Wwise Spatial Audio 增添了一个名为 Reverb Zone 的工具。Reverb Zone 本质上来说是...

10.1.2024 - 作者:托马斯•汉森 (Thomas Hansen)

更多文章

关于音频复用和高效利用的几点经验分享

大家好,“如何对音频资源做有节制、更高效的利用”,应该是大家工作中都会考虑的问题。...

EBP管线概述

EBP是什么? 前不久Wwise 2019.2版本的UE4 Integration推出了一条新的管线,叫做Event-Based...

对白 | 基于Wwise与Unreal Engine的语音设计