1 Audacity插件开发:文件对话框使用指南
本教程介绍了如何在Nyquist插件中创建和使用文件按钮组件,并提供了相关示例。文件按钮组件提供了一种通过图形化文件浏览器选择一个或多个文件的方式。
1.1.1 概述
对于某些插件,需要读取文件或写入文件。为了实现这一点,必须精确指定所需的文件、文件位置,以及是需要读取权限还是写入权限(对于读取权限,文件必须存在;而对于写入权限,文件不一定需要预先存在)。
在文件按钮组件出现之前,文件名可能会硬编码到Nyquist脚本中,或者为用户提供一个文本框来输入文件名。硬编码的文件路径缺乏灵活性,因为它们特定于某些机器(例如,以“C:\”开头的路径在Mac或Linux上无法使用)。虽然文本框比硬编码文件路径有所改进,但它仍然不方便,并且容易出现用户错误,尤其是对于较长的文件路径。Audacity 2.3.0中引入文件按钮组件就是为了解决这些问题,它通过图形化的“文件浏览器窗口”提供访问权限,类似于其他应用程序中的“文件”菜单>“打开”或“文件”菜单>“保存”功能。
1.1.2 语法与外观
文件按钮组件(如上图所示)有一个可编辑的文本输入字段,允许输入(或粘贴)文件路径。文本输入字段后面是一个按钮,点击该按钮会启动文件浏览器。以下是Windows 10上常见文件浏览器窗口的示例。
注意:在文件浏览器中选择文件并不会打开该文件。当在文件浏览器窗口中选择文件时,所选文件的完整名称和路径将作为文件按钮组件变量的值传递给Nyquist脚本。
1.1.3 组件参数
创建文件按钮组件的语法与所有其他Nyquist插件组件类似:
;control variable-name "text-left" file "button-text" "default-file-path" "wildcard-filters" "flags"
- ;control:这是头部语句的起始部分。开头的分号“;”(美元符号“$”会告诉Nyquist将这一行视为注释并忽略它),“control”关键字会告知Audacity创建一个Nyquist脚本将使用的图形用户界面组件。
- variable-name:[符号] 要设置的变量名。
- text-left:[字符串] 显示在组件左侧的文本。
- file:[关键字] 声明这是一个“文件”类型的组件。
- button-text:[字符串] 按钮上显示的文本。通常是两个双引号(即空字符串),其默认文本为“Select a file”(选择文件) 。
- default-file-path:[字符串] 文件浏览器的默认文件路径。支持跨平台路径的关键字。
- wildcard-filters:[字符串] 这是一个特殊的“通配符”字符串,语法与wxFileDialog一致。该字符串由成对的“描述”、竖线符号(“|”)、“文件扩展名”组成。可以列出多个文件扩展名,用分号(“;”)分隔。
- flags:[字符串] 这是一个特殊字符串,用于为文件浏览器设置选项,语法与wxFileDialog一致。
1.1.4 特殊字符串
最后三个参数,即default-file-path
(默认文件路径)、wildcard-filters
(通配符过滤器)和flags
(标志),使用特殊关键字来定义文件按钮组件和相关文件浏览器的行为。
- 注意,与Nyquist符号不同,这些关键字区分大小写。
- 以下的“Windows”、“macOS”和“Linux”示例指的是现代操作系统的标准文件路径,不过在某些机器上可能会有所不同。
<username>
是计算机用户账户的名称(登录名)。
1.1.5 默认文件路径
“默认文件路径”支持以下关键字:
*home*
:当前用户的“主”目录。- Windows:
C:\Users\<username>\
- macOS:
/Users/<username>/
- Linux:
/home/<username>/
- Windows:
*~*
:等同于*home*
。*default*
:用户的“文档”路径。- Windows:
C:\Users\<username>\Documents\Audacity
- macOS:
/Users/<username>/Documents/
- Linux:
/home/<username>/Documents/
- Windows:
*export*
:用户的“导出”路径。- Windows:
C:\Users\<username>\Desktop\
- macOS:
/Users/<username>/Documents/
- Linux:
/home/<username>/Documents/
- Windows:
*save*
:默认“保存”路径。- Windows:
C:\Users\<username>\Desktop\
- macOS:
/Users/<username>/Documents/
- Linux:
/home/<username>/Documents/
- Windows:
*config*
:用户配置文件目录。- Windows:
C:\Users\<username>\AppData\Roaming\audacity\
- macOS:
/Users/<username>/Library/Application Support/audacity/
- Linux:
/home/<username>/.audacity-data/
- Windows:
这些关键字可以与文件名结合使用,以指定要打开的默认文件。例如,如果你希望“文档”路径下的文件名为“sample-data.txt”,可以将默认文件路径参数写为:*default*/sample-data.txt
。
注意:文件路径应该用双引号括起来,否则文件名中的空格将导致错误。如果未提供文件路径,默认值为“default”。如果未提供文件名,默认文件名是“untitled”。默认文件扩展名取自通配符过滤器。
1.1.6 通配符过滤器
“通配符过滤器”决定文件浏览器中显示哪些文件类型。空字符串将默认为所有文件类型。
这个“特殊字符串”的语法与wxFileDialog一致。该字符串由成对的“描述”和“文件扩展名”组成,用竖线字符(“|”)分隔。可以列出多个文件扩展名,用分号(“;”)分隔。
示例:
"Text files|*.txt;*.TXT|All files|*.*;*"
在这个示例中,我们有两对:
- Text files|.txt;.TXT:描述:“Text files” ,文件扩展名“.txt”匹配任何以“.txt”结尾的文件,“.TXT”匹配任何以“.TXT”结尾的文件。
- All files|.;*::描述:“All files” ,文件扩展名“.”匹配任何文件。
1.1.7 标志
特殊“标志”字符串类似于wxFileDialog中的“样式”选项。标志可以是空字符串、单个标志,或用逗号分隔的标志列表。
可用关键字包括:
- open:这是一个“文件打开”对话框。通常这意味着对话框默认按钮的标签是“Open”。不能与“save”结合使用。
- save:这是一个“文件保存”对话框。通常这意味着对话框默认按钮的标签是“Save”。不能与“open”结合使用。
- overwrite_prompt:对于保存对话框:如果文件将被覆盖,仅提示用户确认。
- exists:对于打开对话框:仅允许选择实际存在的文件。
- multiple:对于打开对话框:允许多选文件。
示例:
- open:打开一个或多个必须存在的文件的对话框。
"open,exists,multiple"
- open,exists,multiple,overwrite_prompt:如果文件存在,打开一个或多个文件的对话框,并在文件将被覆盖时提示。
"open,exists,multiple,overwrite_prompt"
- save,overwrite_prompt:保存文件对话框。
"save,overwrite_prompt"
1.1.8 返回值
如果用户在文件浏览器中选择文件,组件会创建一个有效的文件路径字符串,并将其赋值给变量名符号。
如果使用文件浏览器选择了文件,文本框将更新为显示所选文件的完整路径。
注意:如果选择了多个文件(需要设置“multiple”标志),每个文件路径都用双引号括起来。
注意:文件路径列表不是LISP列表,它仍然是一个字符串。有关如何处理多个文件路径的示例,请参见下文。
如果文件路径文本框为空,则组件变量符号将设置为默认路径。
如果文件路径文本框仅包含文件名(或不包含路径的任何字符串),则会在默认路径前加上该文件名。
1.1.9 错误消息
在程序运行或用户错误期间,文件按钮组件可能会返回错误消息。理解这些消息对于调试新插件很有帮助。
<path> is not a valid file path.
:此错误表示返回的文件路径无效,例如文件目录不存在。<file> does not exist
:此错误在用户手动输入文件路径且文件实际上不存在时发生。Mismatched quotes in "<string>"
:文件浏览器返回一个带引号的字符串列表作为文件路径。此错误是由于用户手动编辑文件路径文本框并遗漏一个或多个引号字符导致的。Invalid wildcard string in 'wildcard' control. Using empty string instead.
:此错误表示通配符特殊字符串格式错误,这是编程错误。
1.1.10 示例
- 简单“打开文件”示例
;control var "Select file to open" file "" "" "open"
在这个示例中,仅明确设置了“var”和“text-left”(“Select file to open”)参数。空字符串传递给其他参数,因此它们将采用默认值。
相反顺序:默认标志为“open”,默认通配符过滤器为“所有文件”,默认文件路径为“default”,默认文件名为“untitled”,默认按钮文本为“Select a file” 。
- 简单“保存文件”示例
;control var "Select file to save" file "" "" "save"
与上面的简单“打开文件”示例非常相似,只是这是用于选择要写入的文件。
- 高级“打开文件”示例
;control var "Select file" file "open,exists" *export*/sample-data.txt "Text files|*.txt;*.TXT|All files|*.*;*"
与前面的示例不同,此示例中所有参数都已明确定义。默认文件名为“sample-data.txt”,文件浏览器将显示仅以“.txt”或“.TXT”结尾的文件列表。
注意:“exists”标志仅与允许用户输入文件名的文件浏览器相关。对于纯粹的图形化文件浏览器,无法选择不存在的文件。
还要注意,“exists”标志仅影响文件浏览器,它不会阻止用户输入不存在的文件名。
- 高级“保存文件”示例
;control var "Export data to" file "save,overwrite_prompt" *export*/sample-data.txt "Text files|*.txt;*.TXT|CSV files|*.csv;*.CSV|HTML files|*.html;*.HTML;*.htm;*.HTM|All files|*.*;*"
在这个示例中,所有参数都已明确定义:
- variable-name:
var
- text-left:
Export data to
- button-text:
Select file
- default-file-path:
*export*/sample-data.txt
- wildcard-filters:
Text files|*.txt;*.TXT
CSV files|*.csv;*.CSV
HTML files|*.html;*.HTML;*.htm;*.HTM
All files|*.*;*
- flags:
save,overwrite_prompt
“flags”参数在文件浏览器中提供选项,以显示以“.txt”或“.TXT”结尾的文本文件(默认)、以“.csv”或“.CSV”结尾的CSV文件,或以“.html”、“.HTML”、“.htm”或“.HTM”结尾的HTML文件,或任何文件名称。
注意:对于跨平台可移植性,不建议使用长文件名,但在现代计算机文件系统中通常是可以的。
1.1.11 打开多个文件
;control var "Select one or more files" file "Select" *default* "Text file|*.txt;*.TXT|All files|*.*;*" "open,multiple"
在这个示例中,变量将被设置为var
,默认目录为*default*
,默认过滤器为文本文件,但也可以选择所有文件。与之前的版本不同,此文件浏览器可用于选择多个文件(需要Audacity 2.3.1或更高版本)。
注意:如果用户使用文件浏览器选择一个或多个文件,每个文件路径将用双引号括起来。但是,用户也可以输入单个文件的路径而不用引号,或者在这种情况下,默认值可以是未加引号的单个文件路径,所以我们应该检查并支持这两种情况。
为了从返回的字符串中提取所有路径,我们首先需要将其转换为更有用的形式,例如LISP列表:
(setf path-string
(format nil "(list ~s )" (string-trim "\"" var)))
在这里,我们去掉了外层双引号(如果存在),然后将其格式化为描述LISP列表的字符串。例如,如果所选文件是“C:\first.txt”和“C:\second.txt”,那么var
的值将是"C:\first.txt""C:\second.txt"
,而path-string
的值将是"(list \"C:\\first.txt\" \"C:\\second.txt\")"
。
重要提示:注意这仍然只是一个字符串值,而不是LISP列表。
要将这个字符串转换为LISP列表,我们需要将该字符串作为代码进行求值。幸运的是,在Audacity 2.3.1及更高版本中,使用EVAL-STRING
函数可以轻松实现:
(setf paths (eval-string path-string))
paths
现在是一个有效的字符串LISP列表,我们可以像这样遍历它:
(dolist (p paths)
(print p))
可以在Nyquist提示符中运行的完整示例:
;version 4
;debugflags trace
;control var "Select one or more files" file "Select" *default* "Text file|*.txt;*.TXT|All files|*.*;*" "open,multiple"
(setf path-string (format nil "(list ~s )" (string-trim "\"" var)))
(setf paths (eval-string path-string))
(dolist (p paths "")
(print p))
1.2 应用示例
这些应用示例可以在Nyquist提示符中运行,也可以通过添加完整的插件头部转换为插件。
注意:这些代码示例添加了大量注释以解释其功能。对于生产代码,注释应简洁明了,但这里是为了学习目的,所以包含了额外的解释性注释。
1.2.1 写入文件
在此示例中,我们使用文件按钮组件指定要写入的文件。需要注意的是,选择文件并不会写入文件,它只是捕获文件路径和文件名,然后我们在脚本中写入这些信息。插件将获取有关所选音频的一些信息,并将其写入文件。
首先,我们添加一些头部来设置语法版本和插件类型:
;version 4
;type analyze
接下来是我们的文件按钮组件。注意,它带有“save”标志,因为我们要选择一个文件进行写入:
;control filename "Export to" file "" "data.txt" "Text file|*.txt;*.TXT|All files|*.*;*" "save"
接下来的三行代码收集我们要写入文件的原始信息:
;; 获取数据:
(setf tname (get '*track* 'name))
(setf peak (get '*selection* 'peak))
(setf rms (get '*selection* 'rms))
当我们使用Nyquist写入文件时,文件会被我们写入的数据覆盖。如果我们想将新数据追加到文件而不是覆盖它,我们必须首先读取文件中的现有文本并将其存储在变量中。然后,我们可以将旧数据和新数据一起写回文件。下面是一个函数,如果文件存在,它将读取文件内容并将数据返回给调用者:
(defun read-file(fname)
;; 如果文件存在,复制其内容。
;; 返回数据,或空字符串。
(setf data "")
(setf fp (open fname))
(when fp
(do ((line (read-line fp) (read-line fp)))
((not (read))
(setf data (format nil "~a~%" data line)))
(close fp))
data)
由于此代码设计用于处理文本文件,我们可以检查文件名是否以“.txt”结尾。
注意:文件扩展名不是测试文件类型的可靠方法。在任何涉及安全问题的情况下,都不能依赖文件扩展名来指示文件类型。
- 首先,我们将一个空字符串赋值给变量
ext
。 - 如果文件名至少有4个字符长,我们提取最后4个字符并将其赋值给
ext
。 - 然后,我们可以应用不区分大小写的字符串比较。如果
ext
等于.txt
,那么我们假设它是一个纯文本文件。
;; 检查文件扩展名。
(setf ext "")
(when (>= (length filename) 4)
(setf ext (subseq filename (- (length filename) 4))))
(if (string-equal ext ".txt")
...
现在我们可以运行主程序,它在一个progn
块中:
- 程序块首先将局部变量
data
绑定到由上面定义的read-file
函数返回的数据。 - 然后,我们打开文件进行写入。
- 读取或写入文件后,应再次关闭文件。
- 最后,我们返回一条确认消息。
;; 看起来像一个文本文件,所以让我们继续。
(prog ((data (read-file filename)))
;; 打开文件进行写入。
(setf fp (open filename :direction :output))
;; 使用'format'命令将数据写入文件指针'fp'。
(format fp "~a~%Track name: ~s~%Peak level: ~a~%RMS level: ~a~%"
data tname peak rms)
;; 关闭文件。
(close fp)
(format nil "Data exported to:~%~s" filename))
如果文件名不以.txt
结尾,我们返回一条错误消息:
;; 看起来不像一个文本文件,所以抛出一个错误。
(error "~%Unsupported file type.")
完整代码
;version 4
;type analyze
;control filename "Export to" file "" "data.txt" "Text file|*.txt;*.TXT|All files|*.*;*" "save"
;; 获取数据:
(setf tname (get '*track* 'name))
(setf peak (get '*selection* 'peak))
(setf rms (get '*selection* 'rms))
(defun read-file(fname)
;; 如果文件存在,复制其内容。
;; 返回数据,或空字符串。
(setf data "")
(setf fp (open fname))
(when fp
(do ((line (read-line fp) (read-line fp)))
((not (read))
(setf data (format nil "~a~%" data line)))
(close fp))
data)
;; 检查文件扩展名。
(setf ext "")
(when (>= (length filename) 3)
(setf ext (subseq filename (- (length filename) 4))))
(if (string-equal ext ".txt")
(prog ((data (read-file filename)))
;; 打开文件进行写入。
(setf fp (open filename :direction :output))
;; 使用'format'命令将数据写入文件指针'fp'。
(format fp "~a~%Track name: ~s~%Peak level: ~a~%RMS level: ~a~%"
data tname peak rms)
;; 关闭文件。
(close fp)
(format nil "Data exported to:~%~s" filename))
(error "~%Unsupported file type."))
1.2.2 读取多个文件
这是一个高级示例,展示了如何使用文件按钮组件从一个或多个文本文件中读取数据。文件处理并不是Nyquist的强项(Nyquist主要设计用于处理音频),因此代码可能会有点复杂。
在此示例中,我们将从一个或多个文本文件中导入标签。
注意:为简单起见,我们仅处理基本格式的标签,不支持SpectralSelections
格式。
对于此插件,我们将检查标签数据文件是否每行有一个标签定义,每个标签定义包含开始时间(秒)、结束时间(秒)和可选的标签文本。数据应采用纯文本格式,扩展名为“.txt”。
- 示例数据
39.742984 62.429744 Hello
79.524610 79.524610 Hi there
- 完整代码
;version 4
;type analyze
;control filepaths "Select one or more files" file "Select" *default*/Label_Track.txt "Text file|*.txt;*.TXT|All files|*.*;*" "open,multiple"
(defun Convert-listing-of-file-paths-to-LISP-list
;; 将文件路径列表转换为LISP列表。
(let* ((txt (string-trim "\"" filepaths))
(txt (format nil "(list ~s )" txt))
(txt (eval-string txt)))
txt))
(defun 'label-from-line(text)
;; 从文件的一行中获取标签。它必须格式化为:
;; number number string
;; 每个标签编号必须是整数列表。
(let ((newtxt "")
(index 0)
(ch "")
(next "")
(char1 "")
(char2 "")
(newlabel "")
(labeltxt "")
(start "")
(end "")
(number nil)
(label nil))
;; 确保文本被引用。
(if (not (stringp text))
(setf text (format nil "\"~a\"" text)))
;; 确保标签文本区域有文本,所以只需添加字符。
(setf newtxt (strcat text " a"))
;; 处理标签文本区域,所以只需添加字符。
(setf next (subseq newtxt 1))
(do ((ch (char newtxt 0) (char next 0))
(next (subseq newtxt 1) (subseq next 1))
(index 0 (1+ index)))
((= index (length newtxt)))
(and
;; 确保字符是数字。
(or (digit-char-p ch) (and (char= ch #\.) (digit-char-p (char next 0))))
(setf number (append number (list ch))))
(and
;; 确保字符不是数字。
(not (digit-char-p ch))
(setf newlabel (append newlabel (list (read-from-string (format nil "~a" number)))))
(setf number nil))
;; 非空格字符在两个数字之间。
(if (not (string= ch " "))
(setf labeltxt (strcat labeltxt ch)))
;; 如果下一个存在,关闭双引号。
(if (label-next-exists-p newtxt next)
(setf labeltxt (strcat labeltxt "\"")))
(setf newtxt next))
;; 如果添加了空字符串。
(if (string= labeltxt "")
(setf labeltxt (strcat newtxt "\"")))
(setf label (list (first newlabel) (second newlabel) labeltxt))
label))
(defun label-next-exists-p(txt ntxt)
;; 如果下一个字符存在,则为真。
(if (>= (length ntxt) 1)
t
nil))
(defun Convert-data-to-a-list
;; 将数据转换为列表。
(let* ((txt (string-trim "\"" filepaths))
(txt (format nil "(list ~s )" txt))
(data (eval-string txt)))
data))
(defun check-data(data)
;; 检查数据。文件数据必须每行包含一个标签。
(if (not (every #'(lambda (x) (= (length x) 3)) data))
(throw 'error "Invalid data. Each line must contain one label."))
(if (not (every #'(lambda (x) (numberp (first x))) data))
(throw 'error "Invalid data. First value on each line must be a number."))
(if (not (every #'(lambda (x) (numberp (second x))) data))
(throw 'error "Invalid data. Second value on each line must be a number."))
(if (not (every #'(lambda (x) (stringp (third x))) data))
(throw 'error "Invalid data. Third value on each line must be a string."))
data)
(defun open-file(filename)
;; 打开文件并检查错误。
(let ((fp nil))
;; 获取文件指针。
(setf fp (open filename))
(if (not fp)
(throw 'err (format nil "error:~%~s could not be opened." filename))
fp)))
(defun Process-data-from-file(fp)
;; 从文件指针读取数据并为每行创建一个标签。
(let ((labels ()))
(do ((txt (read-line fp nil 'eof) (read-line fp nil 'eof)))
((eq txt 'eof) labels)
(setf labels (append labels (list (label-from-line txt)))))))
(defun process()
;; 主函数。
;; 从filepaths中的文件获取标签并连接标签列表。
(let ((paths (Convert-listing-of-file-paths-to-LISP-list filepaths))
(labels ()))
(dolist (path paths)
(setf fp (open-file path))
(setf labels (append labels (Process-data-from-file fp)))
(close fp))
(setf labels))
(catch 'err
(process)))
作者声明:本文用于记录和分享作者的学习心得,可能有部分文字或示例来自AI平台,如:豆包、DeepSeek(硅基流动)(注册链接)等,由于本人水平有限,难免存在表达错误,欢迎留言交流和指教!
Copyright © 2022~2025 All rights reserved.