After studying the source code of the program beep
, I came up with a minimal C program that rings the bell.
It uses the PC speaker, which the Linux kernel makes available as an evdev device usually named /dev/input/by-path/platform-pcspkr-event-spkr
(a symbolic link to an actual location).
There is one event to turn the speaker on (i.e. to start producing sound) at a given frequency, and another event to turn the speaker off (i.e. to stop the sound). We simply send the first event, then sleep for the required duration, then send the second event.
This requires write access to the device, which can be set up via an udev rule, the setuid bit for the program, or by running the program as root, as described in the documentation of beep
. Moreover, this cannot control the sound volume. Somehow, the xset
program (from the X server suite) and terminal emulators do not have these restrictions.
In 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;
}
(For conciseness and clarity, the source code above skips all safety checks (check that the file exists, check that open
succeeds, check that the file descriptor is a character device, check that the frequency and duration are integers within the allowed range, check that write
succeeds, check that nanosleep
succeeds…). In an actual application, this should be addressed.)
In a shell script
The C program above can be used to observe the shape of the events; then, the same events can be written from a shell script. Unfortunately, the binary format is most probably platform-specific. On my x86-64 machine with the Linux kernel 5.2, the sound event has the following structure:
- 16 bytes: zero
- 2 bytes, little-endian: 0x12 (field
type
with value EV_SND
)
- 2 bytes, little-endian: 0x2 (field
code
with value SND_TONE
)
- 4 bytes, little-endian: the frequency in Hz, or 0 to stop the sound (field
value
)
This binary structure can be written using Perl with the function pack
. Binary integers can also be output using pure Bash, but this is more verbose.
# 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"
}
echo
to send the binary data. – dirkt Jul 21 '19 at 17:45getfacl
on either the symbolic link or its target confirm this. – Maëlan Aug 12 '19 at 09:12beep
program is rather complicated; what happens if you write a small C program just to write the needed couple of bytes, usingstruct input_event
from the kernel headers? "Illegal argument" may mean that you encode them somehow in the wrong way. – dirkt Aug 12 '19 at 17:51