我知道如何按响铃echo -ne '\a'
(或者,如果您不介意依赖外部命令,那就更好了tput bel
:)。然而,它实际上所做的是将一些特殊代码发送到输出;然后,如果该输出恰好是一个终端,终端解释代码并响铃。
我的问题是我想从后台脚本发出警告噪音,任何终端都不会读取其输出。我可能会播放一些和谐的.wav
文件paplay
或类似的文件,但我更喜欢那种简单、刺耳的蜂鸣声。我不想依赖外部程序。
那么,问题:
- 终端发出的铃声在某种程度上是否特殊(例如使用专用硬件),或者它是像其他声音文件一样的声音文件吗?
- 如何再现那个声音?
答案1
研究完程序源码后beep
,我想出了一个敲响警钟的最小 C 程序。
它使用 PC 扬声器,Linux 内核将其作为通常命名的 evdev 设备/dev/input/by-path/platform-pcspkr-event-spkr
(指向实际位置的符号链接)提供。
有一个事件可以打开扬声器(IE以给定频率开始产生声音),以及另一个关闭扬声器的事件(IE停止声音)。我们只需发送第一个事件,然后休眠所需的持续时间,然后发送第二个事件。
这需要对设备进行写访问,可以通过 udev 规则、程序的 setuid 位或通过以 root 身份运行程序来设置,如beep
.而且,这无法控制音量。不知何故,xset
程序(来自 X 服务器套件)和终端仿真器没有这些限制。
在C中
#include <linux/input.h> // struct input_event
#include <fcntl.h> // open
#include <unistd.h> // write, close
#include <time.h> // nanosleep
/* frequency (Hz): */
static unsigned int const default_frequency = 440;
/* duration (ms): */
static unsigned int const default_duration = 200;
/* file path of PC speaker: */
static char const * const default_device = "/dev/input/by-path/platform-pcspkr-event-spkr";
void start_beep(int fd, int freq)
{
struct input_event e = { 0 };
e.type = EV_SND;
e.code = SND_TONE;
e.value = freq;
write(fd, &e, sizeof(e));
}
void stop_beep(int fd)
{
start_beep(fd, 0);
}
void sleep_ms(unsigned int ms)
{
struct timespec duration =
{ .tv_sec = ms / 1000U,
.tv_nsec = (long)(ms % 1000UL * 1000UL * 1000UL) };
nanosleep(&duration, NULL);
}
int main(void)
{
int fd = open(default_device, O_WRONLY);
start_beep(fd, default_frequency);
sleep_ms(default_duration);
stop_beep(fd);
close(fd);
return 0;
}
(为了简洁和清楚起见,上面的源代码跳过了所有安全检查(检查文件是否存在、检查是否open
成功、检查文件描述符是否为字符设备、检查频率和持续时间是否为允许范围内的整数、检查是否write
成功,检查是否nanosleep
成功……)。在实际应用中,应该解决这个问题。)
在 shell 脚本中
上面的C程序可以用来观察事件的形状;然后,可以从 shell 脚本编写相同的事件。不幸的是,二进制格式很可能是特定于平台的。在我的带有 Linux 内核 5.2 的 x86-64 机器上,声音事件具有以下结构:
- 16 字节:零
- 2 个字节,小尾数:0x12(
type
值为 的字段EV_SND
) - 2 个字节,小尾数:0x2(
code
值为 的字段SND_TONE
) - 4 个字节,little-endian:以 Hz 为单位的频率,或 0 停止声音(字段
value
)
这个二进制结构可以使用 Perl 的函数编写pack
。也可以输出二进制整数使用纯 Bash,但这更详细。
# frequency (Hz):
default_frequency=440
# duration (ms):
default_duration=200
# file path of PC speaker:
default_device='/dev/input/by-path/platform-pcspkr-event-spkr'
function start_beep()
{
declare -i freq="$1"
perl -e 'print pack("qqssl", 0, 0, 0x12, 2, '$freq')'
}
function stop_beep()
{
start_beep 0
}
# USAGE: beep [FREQUENCY [DURATION [DEVICE]]]
function beep()
{
declare -i freq="${1-$default_frequency}"
declare -i dur="${2-$default_duration}"
declare dev="${3-$default_device}"
# convert milliseconds to seconds
declare dur_sec=$( printf '%u.%03u' $(( dur / 1000 )) $(( dur % 1000 )) )
# write the sound events
{
start_beep $freq
sleep $dur_sec
stop_beep
} >> "$dev"
}
答案2
如果您的脚本以 root 身份运行,您可以简单地执行以下操作:
echo -ne '\a' >/dev/console