【WIP】Synthesizer V 插件开发入门(1)

【WIP】Synthesizer V 插件开发入门(1)

图源:紺屋鴉江 - 《銀河》,Pid:94649749

写在前面

本文并非“零基础入门”型的文章。阅读此文章前,建议你已经:

  • 使用过 Synthesizer V Studio(即 SVR2,以下用此简称代替),了解过 VOCALOID,知道“插件”的作用是什么;
  • 已购买 Synthesizer V Studio Pro 版本,以便于插件的开发与调试;
  • 有一定的编程经验,使用过 Lua 语言;

Synthesizer V Studio Pro的正版授权可从 平行四界官方淘宝店 Dreamtonics官方淘宝店 获取。关于 Lua 编程的资料数不胜数,可从 菜鸟教程 等网站获取。

另外,阅读时,可以配合下面两个文档:

正文部分存在问答部分,可能与后文的知识关联。你可以按顺序阅读,也可以在阅读完其他内容后再阅读问答部分。

以下是文章正文。

脚本存放位置

SVR2 的软件数据默认保存在“我的文档”下,而脚本放置在其中的“scripts”子目录中。

以 Windows 系统为例,如果你没有移动过“我的文档”位置,那么路径应该是:

1
C:\Users\<你的用户名>\Documents\Dreamtonics\Synthesizer V Studio\scripts

你可以在其中创建子目录分类存放不同种类的脚本。SVR2 安装时附带的插件默认放置在“Utilities”子目录下。

image-20220125230306915

插件的基本结构

SVR2 Pro 支持两种插件格式:JavaScript 插件,使用嵌入式的 Duktape 解释器;以及 Lua 插件,使用嵌入式的 Lua 5.4解释器。这里选择 Lua 作为编程示例语言,以便有 VOCALIOID 插件开发经验的人快速转向 SVR2 的插件开发。

SVR2 插件在结构上主要有以下几个部分:

  • 插件信息函数:getClientInfo()
  • 本地化函数:getTranslations()
  • 入口点函数:main()

下面分别进行说明。同时注意,SVR2 软件在启动时会将所有的插件代码载入内存(但不会执行除 getClientInfo()getTranslations() 外的其他部分),因而对插件代码做出的改动需关闭 SVR2 再启动 SVR2 才可生效。

插件信息

插件信息由函数 getClientInfo() 控制。作用与 VOCALOID 的 JobPlugins 中 mainfest() 函数的作用类似,保存插件的名称、维护者、兼容性等信息。

其结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
--[[
@function getClientInfo 插件信息函数
@param nil 无参数
@return table 返回列表
@field name string 插件名字
@field author string 插件作者
@field versionNumber num 插件版本
@field minEditorVersion num 最低版本要求
--]]
function getClientInfo()
return {
name = '插件名字',
author = '作者信息',
versionNumber = 1,
minEditorVersion = 65540
}
end

其中:

  • name:字符串,为插件的名称。会作为 SVR2 软件中“脚本-启动脚本”菜单中的显示名称。可以使用本地化字符串(将在后文讲解)。

    image-20220126000251918

  • author:字符串,为插件的作者信息,仅起提示作用,不会显示在软件中。同样可以使用本地化字符串。

  • versionNumber:数字,为插件的版本号,仅起提示作用,目前在 SVR2 内不会产生任何动作。

  • minEditorVersion:可使用该插件的最低 SVR2 版本号。SVR2 的版本号为 6 位 16 进制数字。例如:1.4.0 版本对应的版本号为 0x010400。上述例子中的版本号 65540 转为 16 进制得 0x010004,对应 SVR2 版本 1.0.4。若该值大于当前使用的 SVR2 版本号,则会触发错误提示。

    image-20220126002614489

问:既然 SVR2 会在启动时执行 getClientInfo(),那么是否可以在这个函数中添加其他的语句实现“随 SVR2 启动而自动执行”?

答:是可以的,但过程不能阻塞。阻塞会使 SVR2 卡在启动画面。来看一个有点超纲的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getClientInfo()
-- 定义窗口
local form = {
title = '自动启动了!'
}
-- 使用阻塞函数展示窗口
SV:showCustomDialog(form)
return {
name = 'Haha',
author = 'Test',
versionNumber = 2,
minEditorVersion = 0x010004
}
end

启动 SVR2 后的效果:

image-20220126003916238

image-20220126003950838

此处插件伴随 SVR2 的启动而自动启动了。关于插件是如何定义和显示 GUI 的,将在后文讲解。

本地化

SVR2 内置多种界面语言,而插件的本地化功能可以使插件可根据 SVR2 界面语言的不同使用不同的字符串。这样一来,插件的翻译和更新都变得更加容易。VOCALOID 无此功能,因此在分发 VOCALOID 插件时经常需要同时分发多个翻译版本。

image-20220126095018063

插件的本地化由函数 getTranslations() 控制。

其基本结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--[[
@function getTranslations 本地化函数
@param langCode string 语言代码
@return table 返回字符串列表
--]]
function getTranslations(langCode)
if langCode == 'zh-cn' then
return {
{'Hello', '你好'}
}
end
end

-- 使用本地化字符串
local testStr = SV:T('Hello')

其中:

  • en-us:字符串,为语言代码,由形参 langCode 给出。更多的语言代码可在 Language designators with regions 查看。常见的语言代码:
    • 中文简体:zh-cn
    • 中文繁体:zh-tw
    • 英语:en-us
    • 日语:ja-jp

image-20220126115306480

  • return:return的是一个二层嵌套的表。每一个子表都是如下形式:

    1
    {'默认语言下的字符串', '目标语言下的字符串'}
    • 默认语言下的字符串:当 getTranslations() 函数中对 langCode 的条件判断均不满足,无 return 时,将使用该字符串。本例中,“默认语言下的字符串”是英语“Hello”。当 SVR2 的语言是日语(ja-jp)时,字符串仍将使用“Hello”。
    • 目标语言下的字符串:当 getTranslations() 函数中对 langCode 的条件判断满足且 return 二层链表时,将使用该字符串。本例中,若 SVR2 的语言是中文(zh-cn)时,字符串将使用“你好”。
  • SV:T():使用本地化字符串。按照上述规则返回默认语言下的字符串或目标语言下的字符串。

问:既然 SVR2 也会在启动时执行 getTranslations(),那么是否也可以在这个函数中添加其他的语句实现“随 SVR2 启动而自动执行”?

答:是可以的,但过程依然不能阻塞。阻塞的过程会使 SVR2 卡在启动界面。

也需同时注意它与 getClientInfo() 间的区别:

  • 不能使用 SVR2 提供的同步的 GUI 相关函数,只能使用其异步版本。同步的 GUI 函数会使插件从插件列表中消失。(可能是Bug?)

    例子如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function getTranslations(langCode)
    local form = {
    title = 'Execute!'
    }
    -- 非阻塞的 GUI 函数
    SV:showCustomDialogAsync(form, nil)
    -- 照常 return 翻译
    if langCode == 'zh-cn' then
    return {
    {'Plugin Name', '插件名字'}
    }
    end
    end

    对同步和异步的 GUI 函数的细节将在后文描述。

    image-20220127115636980

  • 每次插件启动都会执行 getTranslations(),而 getClientInfo() 只会在 SVR2 启动时执行一次。搭配这两个函数可以实现复杂的插件自动启动过程。

    一个稍微超纲的例子如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function getTranslations(langCode)
    local form = {
    title = 'getTranslations() 执行了!'
    }
    SV:showCustomDialogAsync(form, nil)
    if langCode == 'zh-cn' then
    return {
    {'Plugin Name', '插件名字'}
    }
    end
    end

    -- 入口点
    function main()
    local form = {
    title = 'main() 执行了!'
    }
    SV:showCustomDialog(form)
    SV:finish()
    end

    从 SVR2 的“脚本”菜单中启动插件,效果如下:

    image-20220127125339516

    在顺序上,getTranslations() 优先于 main() 被执行。

  • getTranslations() 中不能使用 SV:T()使用 SV:T() 不会报错,但始终返回默认语言下的字符串。

问: 既然 getClientInfo()getTranslations() 都会在启动时被执行,那么它们的先后顺序是什么?

答:getTranslations(langCode) 优先于 getClientInfo() 被执行。

入口点

就像C语言程序需要 int main() 作为入口点才能编译为独立的直接运行的程序一样,插件也有类似的入口点函数 main()

其结构如下:

1
2
3
4
5
6
7
8
--[[
@function main 入口点函数
@param nil 无参数
@return nil 无返回值
--]]
function main()
-- 此处放置代码
end

该函数只会在插件从“脚本”菜单中启动时被执行。

插件界面

界面是插件的一个重要组成部分。用户通过界面了解插件的作用、通过界面提供和调整插件所需的参数、通过界面获取插件的各种提示信息等。

SVR2 提供了两套显示 GUI 的函数:同步的函数和异步的函数。所有函数均是宿主对象 SV 的方法。

宿主对象

对象 SV 就是宿主对象(Host Object)。宿主对象保存了当前宿主(即正在运行的 SVR2 软件)内的部分数据(如软件信息、播放状态、音符状态等)。SVR2 为了方便开发者,还在 SV 对象的父类中定义了许多的工具函数(如频率与音名互相转换、时间单位互相转换等,在后文详细说明)。

宿主对象的父类会在 SVR2 启动时被实例化。同一时间运行的两个 SVR2 中的宿主对象相互独立。

界面类型

  • 输入框

image-20220127180641314

由函数 SV:showInputBox()(同步)及函数 SV:showInputBoxAsync() (异步)定义。提供输入框的窗口标题、输入框上方的提示信息、输入框的默认值(异步函数还需提供回调函数的函数名)作为函数参数,用户的输入内容作为返回值。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- 同步版本
--[[
@param title string 标题
@param message string 提示消息
@param defaultText string 输入框的默认值
@return string 用户的输入结果
--]]
local userInput = SV:showInputBox(title, message, defaultText)

-- 异步版本
--[[
@function callback 回调函数
@param userInput string 用户的输入结果
@return nil 无返回值
--]]
function callback(userInput)
-- 此处加入代码
end

--[[
@param callback function 回调函数名
@return nil 无返回值
--]]
SV:showInputBoxAsync(title, message, defaultText, callback)
  • 消息框

image-20220127182848514

由函数 SV:showMessageBox() (同步)及函数 SV:showMessageBoxAsync() (异步)定义。提供消息框的窗口标题和消息内容(异步函数还需提供回调函数的函数名)作为参数,无返回值。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 同步版本
--[[
@param title string 标题
@param message string 消息内容
@return nil 无返回值
--]]
SV:showMessageBox(title, message)

-- 异步版本
--[[
@function callback 回调函数
@param nil 无参数
@return nil 无返回值
--]]
function callback()
-- 此处加入代码
end

--[[
@param callback function 回调函数名
@return nil 无返回值
--]]
SV:showMessageBoxAsync(title, message, callback)
  • “确定-取消”框

image-20220127203946674

由函数 SV:showOkCancelBox() (同步)及函数 SV:showOkCancelBoxAsync() (异步)定义。名称来自于窗口中的两个按键“确定”和“取消”。提供“确定-取消”框的窗口标题和窗口内容(异步函数还需提供回调函数的函数名)作为参数。当用户按下“确定”时返回布尔值“true”,按下“取消”时返回布尔值“false”。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 同步版本
--[[
@param title string 标题
@param message string 窗口内容
@return boolean 用户确认或取消
--]]
local userChoice = SV:showOkCancelBox(title, message)

-- 异步版本
--[[
@function callback 回调函数
@param userChoice boolean 用户确认或取消
@return nil 无返回值
]]
function callback(userChoice)
-- 此处加入代码
end

--[[
@param callback function 回调函数
@return nil 无返回值
--]]
SV:showOkCancelBoxAsync(title, message, callback)
  • “是-否-取消”框

image-20220128114727086

由函数 SV:showYesNoCancelBox() (同步)及函数 SV:showYesNoCancelBoxAsync() (异步)定义。名称来自于窗口中的三个案件“是”、“否”与“取消”。提供“是-否-取消”框的窗口标题和窗口内容(异步函数还需提供回调函数的函数名)作为参数。返回字符串“yes”、“no”和“cancel”,分别对应用户按下“是”、“否”和“取消”按键。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 同步版本
--[[
@param title string 标题
@param message string 窗口内容
@return string 用户选择
--]]
local userChoice = SV:showYesNoCancelBox(title, message)

-- 异步版本
--[[
@function callback 回调函数
@param userChoice string 用户选择
@return nil 无返回值
--]]
function callback(userChoice)
-- 此处加入代码
end

--[[
@param callback function 回调函数名
@return nil 无返回值
]]
SV:showYesNoCancelBoxAsync(title, message, userChoice)

同步函数与异步函数

每个 GUI 函数均有同步的和异步的两个版本。在函数定义上,异步函数以“Async”结尾且均没有返回值,形参较同步函数多一个回调函数名称。

与JavaScript中的Async/Await、Python中的yield一样,异步函数不会阻塞脚本的执行,而同步函数会阻塞脚本的执行,直到窗口关闭(得到返回值)。

image-20220131154531567

自定义界面

预定义的几种界面类型只提供了有限的输入类型(输入框的字符串、带按钮的窗口框的按钮等),很难满足功能复杂的插件的要求。因此 SVR2 提供了绘制自定义界面的函数:SV:showCustomDialog() (同步)与 SV:showCustomDialogAsync() (异步)。

其基本结构如下:

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
--[[
@variable exampleForm table 窗口结构变量
@field title string 窗口标题
@field message string 窗口提示信息
@field buttons string 按钮类型,将在后文详解
@field widgets array 窗口工具,将在后文详解
--]]
local exampleForm = {
title = '示例窗口',
message = '这是一个示例提示信息',
buttons = 'OkCancel',
widgets = {
-- 此处内容将在后文详解
}
}

-- 同步版本
--[[
@param exampleForm table 窗口结构
@return object 返回记录窗口输入信息的对象,将在后文详解
--]]
local dialogResult = SV:showCustomDialog(exampleForm)

-- 异步版本
--[[
@function callback 回调函数
@param dialogResult object 记录窗口输入信息的对象
--]]
function callback(dialogResult)
-- 此处加入代码
end

--[[
@param callback function 回调函数名
@return nil 无返回值
--]]
SV:showCustomDialog(exampleForm, callback)

按钮类型

按钮类型共有两种:

  • “OkCancel”:对应着界面类型中的“确认-取消框”。
  • “YesNoCancel”:对应着界面类型中的“是-否-取消”框。
  • “Message”:对应着界面类型中的消息框。(注:可能是翻译问题,按钮上的文字是“取消”而不是“确定”,后续可能修正)

提示信息

提示信息直接显示在标题的下方,可以用来概述插件用途、指示用户操作或显示插件作者信息等。支持使用转义字符。

举例如下:

1
2
3
4
local form = {
title = '标题',
message = '提示信息第一行!\n第二行!'
}

【WIP】Synthesizer V 插件开发入门(1)

https://www.zhouweitong.site/post/021-synthv-plugin-intro/

作者

ObjectNotFound

发布于

2022-01-27

更新于

2023-02-13

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×