Search code examples
linuxassemblygnugnu-assemblerlow-level

Make a speaker vibrate at a defined frequency in Ubuntu Linux, with GNU Assembly


I am new to assembly language

Our computer makes sound all the time (mp3, wav, mp4, ogg, etc).

How could I directly communicate to a speaker with assembly language? In C, you can use "Beep()" and pass duration in milliseconds and hertz of the sound to the speaker. How could I do this in GNU assembly language?

My Computer Specs: Dell Optiplex 960; Ubuntu 23.04 LTS x86-64 architecture.

Could somebody leave and example of how this could be done, I.E. pasting some source code? Leaving comments on relatively what each instruction is doing would also be very helpful.

I have tried to find some tutorials, but am kind of lost. Most tutorials that were in NASM (Not GNU like I use), would use the systems default sound, I.E. the bell sound on windows, or the notification sound on Linux Ubuntu.


Solution

  • Update: According to the online service manual for the Dell Optiplex 960, the internal speaker is an optional device. Therefore, your computer might not have an internal speaker connected and the comments to the question and this answer about using the PC speaker interface will not help much. This answer mainly explains about using the internal speaker, but has a few remarks on PCM audio at the end.

    As I have been maintaining the common Linux beep utility https://github.com/spkr-beep/beep for a few years, I would recommend to use the proper API for the PC speaker on Linux without the need for privileged access.

    This amounts to a program which does

    • open(2) the well-known device file /dev/input/by-path/platform-pcspkr-event-spkr and save the file descriptor if successful, abort the program otherwise
    • prepare a struct input_event (defined in linux/input.h) with e.type = EV_SND, e.code = SND_TONE, e.value = frequency_in_Hz (a frequency of 0 will silence the PC speaker) (can be done statically)
    • write(2) that 24 byte struct to the file descriptor
    • eventually close(2) the file descriptor

    So basically, you just need to write a program which writes a few bytes into a file.

    If you want your program to start a tone, then wait, and then stop that tone, your program also needs to wait for a defined time. The nanosleep(2) syscall with a struct timespec from time.h can help you there.

    This leaves you with just needing to figure out how to execute the syscalls open(2), write(2) and close(2). OK, the close(2) is not really needed if you exit(2) afterwards anyway, but nanosleep(2) is probably needed as well.

    Any Linux "hello world" program in assembly should be a good starting point: It will already have implemented write(2) and exit(2), and that should be a good place to start from when adding the other syscalls.

    Starting with a simple hello world program like https://cs.lmu.edu/~ray/notes/gasexamples/ (first hit in search engine for "linux gnu assembly hello world") with some information from https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/ (first hit in search engine for "x86-64 abi") with a few of my own adornments (.size, foo_size = . - foo, .balign and comments, and .global for debugging) gives the following program which starts a beep. Stopping the beep is left as an exercise to the reader, or for running beep on the shell.

    # gas-beep.s - do a PC speaker beep with GNU assembler for Linux x86-64
    
        .global _start
        .text
        .global main_program
    main_program:
    _start:
        # open(speaker_device, O_WRONLY)
        mov $2, %rax        # sys_open
        mov $speaker_device, %rdi   # filename = speaker_device
        mov $1, %rsi        # flags = O_WRONLY
        xor %rdx, %rdx      # mode = 0
        syscall
    
        cmp $0, %rax
        jl  error_exit_open
    
        mov %rax, fd
    
        # write(fd, &beepcmd_on, sizeof(beepcmd_on))
        mov $1, %rax        # sys_write
        mov fd, %rdi        # fd = fd
        mov $beepcmd_on, %rsi   # buf = beepcmd_on
        mov $beepcmd_on_size, %rdx  # count = sizeof(beepcmd_on)
        syscall
    
    # TODO: nanosleep(2) with a struct timeval aka two .quad values
    # TODO: turn off sound using write(2) (beepcmd_off with frequency 0)
    # TODO: print a few enlightening words to stdout with write(2)
    
        # close(fd)
        mov $3, %rax        # sys_close
        mov fd, %rdi        # fd = fd
        syscall
    
        # exit(0)
        mov $60, %rax       # sys_exit
        xor %rdi, %rdi      # error_code = 0
        syscall
    
    error_exit_open:
        # write(1, errmsg_open, sizeof(errmsg_open))
        mov $1, %rax        # sys_write
        mov $2, %rdi        # fd = STDERR_FILENO
        mov $errmsg_open, %rsi  # buf = errmsg_open
        mov $errmsg_open_size, %rdx # count = sizeof(errmsg_open)
        syscall
    
        # exit(2)
        mov $60, %rax       # sys_exit
        xor $2, %rdi        # error_code = 2
        syscall
    
        .size   main_program, . - main_program
    
        .balign 8
        .global beepcmd_on
        # struct input_event {.type=EV_SND, .code=SND_TONE, .value=880}
    beepcmd_on:
        .quad   0
        .quad   0
        .short  0x12            # .type = EV_SND
        .short  0x02            # .code = SND_TONE
        .int    880         # .value = frequency
    beepcmd_on_size = . - beepcmd_on    # required for write(2)
        .size   beepcmd_on, beepcmd_on_size
    
        .global speaker_device
    speaker_device:
        .asciz  "/dev/input/by-path/platform-pcspkr-event-spkr"
        .size   speaker_device, . - speaker_device
    
        .global errmsg_open
    errmsg_open:
        .ascii  "Error: Could not open(2) the device.\n"
    errmsg_open_size = . - errmsg_open  # required for write(2)
        .size   errmsg_open, errmsg_open_size
    
        .bss
    
        .global fd
        .lcomm  fd, 8           # uninitialized file descriptor variable
    

    To make building this and understanding it easier, use the following GNUmakefile which builds you a symbol list, a map file, a disassembled view, etc. of the whole program:

    .PHONY: all
    all: all-local
    
    TARGETS += gas-beep.lss
    %.lss: %
        objdump -h -S $< > $@
    
    TARGETS += gas-beep.syms
    %.syms: %
        objdump --syms $< > $@
    
    TARGETS += gas-beep.nm
    %.nm: %
        nm $< > $@
    
    TARGETS += gas-beep.map
    %.map: %
    
    TARGETS += gas-beep
    gas-beep: gas-beep.o
        ld -g -Map=gas-beep.map -o $@ $^
    
    TARGETS += gas-beep.lst
    %.lst: %.o
    
    TARGETS += gas-beep.stripped
    %.stripped: %
        strip -s -o $@ $<
    
    TARGETS += gas-beep.readelf-a
    %.readelf-a: %
        readelf -a $< > $@
    
    %.o: %.s
        gcc -Wall -Werror -g -Wa,-adhlns=$*.lst -c -o $@ $<
    
    .PHONY: all-local
    all-local: $(TARGETS)
    
    .PHONY: clean
    clean:
        rm -f *.{lss,lst,map,nm,o,readelf-a,stripped,syms} gas-beep
    

    Note that you might need to explicitly install and load the pcspkr.ko kernel module, and make sure your computer actually has a physical PC speaker connected (speaker or piezo buzzer). Installing your operating system's beep package could save you some manual sysadmin work.

    So much for the PC speaker.

    If you want to produce sound via PCM audio instead of using the PC speaker, the API will probably be a lot more complex than just writing a few bytes to a file. Twentry years ago, preparing a simple memory buffer for PCM audio with a square wave to write(2) to an open(2)ed PCM audio device /dev/dsp was of comparable complexity. These days however, even just finding a device to open is a lot more complex, and we are not even talking about buffers running out and refilling buffers, and there is more than one PCM sound API.