编程的世界里,我们习惯于用代码构建各种奇妙的功能,但如果能用代码“演奏”出美妙的音乐,那岂不是更加有趣?今天,就让我们一起踏上这段奇妙的旅程,用 Lisp 的方言 Racket 来编写一个可以生成音调的图形界面程序,感受代码与音乐碰撞的魅力!
👋 初识 Racket
Racket 作为 Lisp 的一种方言,以其强大的跨平台 GUI 库而闻名。与用代码构建另一个计算器不同,我们将尝试构建一个可以生成音调的 GUI 界面。
在开始之前,我们需要先安装 Racket。好消息是,大多数 Linux 发行版的软件仓库中都包含 Racket,所以安装起来非常方便。安装完成后,我们就可以开始编写代码了。
#lang racket
(require racket/gui)
Racket 的一大优势是它拥有大量的内置库。在这里,我们将使用 racket/gui
库来构建我们的 GUI 界面。
; 主窗口
(define frame (new frame% [label "Bleep"]))
; 显示 GUI
(send frame show #t)
Racket 的 GUI 库是面向对象的。我们可以通过实例化 frame%
类来创建一个窗口。以百分号结尾的标识符是 Racket 中类的命名约定。通过调用窗口的 show
方法,我们可以将窗口显示出来。接下来,让我们在创建窗口和显示窗口之间添加一些其他的控件。
在 Racket 中,#t 是一个布尔值,表示 true。
🎚️ 滑动条与频率
首先,我们需要一个滑动条来让用户选择音调的频率。
(define slider (new slider% [label #f]
[min-value 20]
[max-value 20000]
[parent frame]
[init-value 440]
[style '(horizontal plain)]
[vert-margin 25]
[horiz-margin 10]))
这段代码创建了一个水平的滑动条,其取值范围为 20 到 20000 Hz,对应人类可听到的频率范围。我们将初始值设置为 440 Hz,也就是标准音高 A4 的频率。
然而,如果我们直接运行这段代码,会发现滑动条的初始位置几乎看不到变化:![线性刻度滑动条][]
这是因为 20 到 20000 的范围太大了,440 在这个范围内显得微不足道。为了解决这个问题,我们需要使用对数刻度来代替线性刻度。
通过参考 Stack Overflow 上的一个答案,我们可以将 JavaScript 代码移植到 Racket 中,实现对数刻度的滑动条:
; 滑动条使用的刻度
(define *min-position* 0)
(define *max-position* 2000)
; 频率范围
(define *min-frequency* 20)
(define *max-frequency* 20000)
; 频率的对数刻度(使中央 A [440] 大致位于中间)
; 改编自 https://stackoverflow.com/questions/846221/logarithmic-slider
(define min-freq (log *min-frequency*))
(define max-freq (log *max-frequency*))
(define frequency-scale (/ (- max-freq min-freq) (- *max-position* *min-position*)))
; 将滑块位置转换为频率
(define (position->frequency position)
(inexact->exact (round (exp (+ min-freq (* frequency-scale (- position *min-position*)))))))
; 将频率转换为滑块位置
(define (frequency->position freq)
(inexact->exact (round (/ (- (log freq) min-freq) (+ frequency-scale *min-position*)))))
这段代码定义了几个全局参数,并创建了两个函数:position->frequency
用于将滑动条上的位置转换为频率,frequency->position
用于将频率转换为滑动条上的位置。
在 Racket 语言中,
->
符号通常用于表示从一个值到另一个值的转换关系,特别是在函数命名中。具体来说,在你的代码中:
-
position->frequency
表示将滑块位置(position
)转换为频率(frequency
)。-
frequency->position
表示将频率转换为滑块位置。这种命名风格帮助开发者快速理解函数的用途和它们之间的关系。Racket 语言鼓励使用清晰的命名来提高代码的可读性。
现在,让我们修改 slider%
的代码,使用 frequency->position
函数将 init-value
转换为使用对数刻度的滑动条位置:
(define slider (new slider% [label #f]
[min-value *min-position*]
[max-value *max-position*]
[parent frame]
[init-value (frequency->position 440)]
[style '(horizontal plain)]
[vert-margin 25]
[horiz-margin 10]))
🎹 音调控制面板
在滑动条下方,我们将添加一个文本框来显示当前频率,并添加按钮来将频率增加或减少一个八度。
(define frequency-pane (new horizontal-pane% [parent frame]
[border 10]
[alignment '(center center)]))
(define lower-button (new button% [parent frequency-pane]
[label "<"]))
(define frequency-field (new text-field% [label #f]
[parent frequency-pane]
[init-value "440"]
[min-width 64]
[stretchable-width #f]))
(define frequency-label (new message% [parent frequency-pane] [label "Hz"]))
(define higher-button (new button% [parent frequency-pane]
[label ">"]))
horizontal-pane%
是一个不可见的控件,用于辅助布局。至此,我们已经拥有了一个看起来不错的界面,但它还不能做任何事情。如果我们点击按钮或滑动滑动条,什么也不会发生。
为了让界面动起来,我们需要为控件添加回调函数。例如,我们可以为滑动条添加一个回调函数,每当滑动条移动时,该函数就会被调用。
; 将滑块链接到文本字段以显示频率
(define (adjust-frequency widget event)
(send frequency-field set-value
(~a (position->frequency (send widget get-value)))))
(define (adjust-slider entry event) (define new-freq (string->number (send entry get-value)))
(send slider set-value
(frequency->position (if new-freq new-freq *min-frequency*))))
回调函数接受两个参数:第一个参数是调用它的对象的实例,第二个参数是事件类型。text-field%
需要一个字符串,因此我们必须使用 ~a
将 position->frequency
返回的数字转换为字符串。接下来,我们要做的就是将这些函数连接到控件上:
(define slider (new slider% [label #f]
...
[callback adjust-frequency]
...))
...
(define frequency-field (new text-field% [label #f]
...
[callback adjust-slider]
...))
我们将按钮连接到名为 decrease-octave
和 increase-octave
的回调函数。八度是指“两个音高之间的音程,其频率是另一个音高的两倍”。
; 设置频率滑块和显示
(define (set-frequency freq)
(send slider set-value (frequency->position freq))
(send frequency-field set-value (~a freq)))
; 按钮将频率增加和减少一个八度
(define (adjust-octave modifier)
(set-frequency (* (string->number (send frequency-field get-value)) modifier)))
(define (decrease-octave button event) (adjust-octave 0.5))
(define (increase-octave button event) (adjust-octave 2))
现在,如果我们滑动滑动条,文本框会相应更新。如果我们在文本框中输入一个数字,滑动条也会相应更新。### 🛡️ 自定义控件:更安全的输入
Racket 附带的控件非常基础,但我们可以扩展内置控件的类来创建自定义控件。让我们扩展 text-field%
类来创建一个新的 number-field%
类。这个类将有两个额外的初始化变量来指定 min-value
和 max-value
,并且只允许输入落在这个范围内的数字。
; 扩展 text-field% 类以在字段失去焦点时验证数据。
; 字段应仅包含允许范围内的数字。否则,设置为最小值。
(define number-field%
(class text-field%
; 添加初始化变量以定义允许范围
(init min-value max-value)
(define min-allowed min-value)
(define max-allowed max-value)
(super-new)
(define/override (on-focus on?)
(unless on?
(define current-value (string->number (send this get-value)))
(unless (and current-value
(>= current-value min-allowed)
(<= current-value max-allowed))
(send this set-value (~a min-allowed))
; 还重置滑块位置以确保它仍然与显示匹配
(send slider set-value (string->number (send frequency-field get-value))))))))
然后,我们可以用 number-field%
替换 text-field%
。
(define frequency-field (new number-field% [label #f]
[parent frequency-pane]
[min-value *min-frequency*]
[max-value *max-frequency*] [callback adjust-slider]
[init-value "440"]
[min-width 64]
[stretchable-width #f]))
让我们再次使用 number-field%
来创建一个字段,以毫秒为单位指定哔声的持续时间:
(define control-pane (new horizontal-pane% [parent frame]
[border 25]
[spacing 25]))
(define duration-pane (new horizontal-pane% [parent control-pane]))
(define duration-field (new number-field% [label "Duration "]
[parent duration-pane]
[min-value 1]
[max-value 600000] ; 10 minutes
[init-value "200"]
[min-width 120]))
🎼 音符选择
频率是一个比较抽象的概念。让我们也让用户能够选择一个音符。我们可以将 A4-G4 的对应频率存储在一个哈希表中。
; 音符 -> 频率(中央 A-G [A4-G4])
; http://pages.mtu.edu/~suits/notefreqs.html
(define notes (hash "A" 440.00
"B" 493.88
"C" 261.63
"D" 293.66
"E" 329.63
"F" 349.23
"G" 292.00))
我们将为用户提供一个下拉菜单。每当从下拉菜单中选择一个音符时,我们将在哈希表中查找频率,并使用我们为八度按钮创建的 set-frequency
辅助函数来设置它。
; 将频率设置为特定音符
(define (set-note choice event)
(set-frequency (hash-ref notes (send choice get-string-selection))))
(define note (new choice% [label "♪ "]
[choices '("A" "B" "C" "D" "E" "F" "G")]
[parent control-pane]
[callback set-note]))
🎧 让音乐响起
最后,让我们来制造一些噪音。
(require rsound)
; 使用 RSound 生成音调
; 明确设置 RSound 采样率,以防因平台/版本而异
(default-sample-rate 44100)
(define (generate-tone button event)
(play (make-tone (string->number (send frequency-field get-value))
0.5
; 以 44.1 kHz 的采样率表示的样本持续时间
(inexact->exact (* 44.1 (string->number (send duration-field get-value)))))))
我们将使用 Racket RSound 包来生成音调。这个包没有捆绑在 Racket 中,但你可以使用 Racket 附带的 raco
工具来安装它(raco pkg install rsound
)。将它连接到持续时间和音符选择器之间的按钮上,你就可以制造一些噪音了。
(define play-button (new button% [parent control-pane]
[label "Play"]
[callback generate-tone]))
🎉 结语
恭喜!我们已经成功地使用 Racket 构建了一个可以生成音调的图形界面程序。这段旅程不仅让我们体验了 Racket 图形界面编程的乐趣,更让我们感受到了代码与音乐碰撞的奇妙火花。
参考文献:
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于