1

I know how to ring the bell with echo -ne '\a' (or, even better if you don’t mind relying on an external command: tput bel). However, what it actually does is sending some special code to the output; then, if that output happens to be a terminal, the terminal interprets the code and rings the bell.

My issue is that I want to emit warning noises from a background script, whose output is not read by any terminal. I might play some harmonious .wav file with paplay or similar, but I prefer that plain, strident beeeep. And I would like not to depend on external programs.

So, questions:

  1. Is the bell sound, as emitted by terminals, special in some way (for example using a dedicated hardware), or is it a sound file like any other?
  2. How to reproduce that sound?
Maëlan
  • 426
  • 2
  • 16
  • 1
    Assuming you mean the PC speaker, see this answer. If you pick a frequency, you can use echo to send the binary data. – dirkt Jul 21 '19 at 17:45
  • Thanks, maybe I was simply missing the “PC speaker” keyword… Unlike in the answer you point at and even with proper permissions set up, I cannot manage to write to that device with usual shell commands, maybe because it is a character-special device. However, my research led me to discover the notorious beep program, I will study its source code to see how it does. – Maëlan Aug 11 '19 at 21:59
  • What error do you get when you write? Did you check the permissions? – dirkt Aug 12 '19 at 03:22
  • “Invalid argument” (whether I am writing with perl, echo, or dd). Yes, as the user currently logged in in the virtual console, I have write permission set up via udev, as described there. getfacl on either the symbolic link or its target confirm this. – Maëlan Aug 12 '19 at 09:12
  • The beep program is rather complicated; what happens if you write a small C program just to write the needed couple of bytes, using struct 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

2 Answers2

4

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"
}
Maëlan
  • 426
  • 2
  • 16
1

If your script is running as root, you could simply do:

echo -ne '\a' >/dev/console
telcoM
  • 96,466
  • Interesting. It doesn’t allow any customization (pitch, duration, volume) but it is dead simple and may be tried as a last resort. – Maëlan Aug 30 '19 at 23:59