diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5709f9a --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2022 Jookia + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/evtone.c b/evtone.c new file mode 100644 index 0000000..51fc7c6 --- /dev/null +++ b/evtone.c @@ -0,0 +1,260 @@ +/* SPDX-License-Identifier: MIT */ +/* Copyright 2022 Jookia */ + +/* PROGRAM OPTIONS */ + +#define TONE_MAX 256 +#define TONE_MAX_STR "256" + +struct tone_cmd { + int tone_hz; + int duration_ms; +}; + +struct program_opts { + int dry_run; + int tone_count; + const char *device; + struct tone_cmd tones[TONE_MAX]; +}; + +/* OPTIONS PARSING */ + +#include +#include + +error_t argp_parser(int key, char *arg, struct argp_state *state); + +const char *argp_program_version = "evtone 1.0"; +const char argp_args_doc[] = "TONE [TONE...]"; +const char argp_doc[] = + "TONE is of the format HZ:MILLISECONDS\n" + "Up to " TONE_MAX_STR " TONEs may be specified at once.\v" + "Examples:\n" + " \"evtone 440:1500\" plays a tone of 440Hz for 1500 milliseconds.\n" + " \"evtone 440:1500 300:10\" plays a 440Hz tone for 1500 milliseconds\n" + " followed by a 300Hz tone for 10 milliseconds\n" + " \"evtone -d 440:1500 300:10\" will print what the program will do\n" + " instead of actually doing it\n" + " \"evtone -D /dev/input/event2 440:1000\" will play a 440Hz tone for\n" + " 1000 milliseconds using the /dev/input/event2 device"; + +const struct argp_option argp_options[] = { + {.name = "dry-run", .key = 'd', .doc = "Print what the program will do"}, + {.name = "device", + .key = 'D', + .arg = "FILE", + .doc = "/dev/input device to use to play tones\n" + "If not supplied the device will be guessed"}, + {0}}; + +const struct argp args = { + .options = argp_options, + .parser = argp_parser, + .args_doc = argp_args_doc, + .doc = argp_doc, +}; + +int parse_tone(char *arg, struct program_opts *opts) { + if (opts->tone_count >= TONE_MAX) { + fprintf(stderr, "Error: Too many TONEs specified at once\n"); + return 1; + } + struct tone_cmd *tone = &opts->tones[opts->tone_count++]; + char canary; + int rc = sscanf(arg, "%i:%i%c", &tone->tone_hz, &tone->duration_ms, &canary); + if (tone->tone_hz < 0 || tone->duration_ms < 0) { + fprintf(stderr, "Error: TONE HZ or DURATION must be over 0\n"); + return 1; + } + if (rc != 2) { + fprintf(stderr, "Error: Invalid TONE format\n"); + return 1; + } + return 0; +} + +error_t argp_parser(int key, char *arg, struct argp_state *state) { + struct program_opts *opts = state->input; + switch (key) { + case 'd': + opts->dry_run = 1; + break; + case 'D': + opts->device = arg; + break; + case ARGP_KEY_ARG: + if (parse_tone(arg, opts)) { + argp_usage(state); + } + break; + case ARGP_KEY_END: + if (state->arg_num < 1) { + argp_usage(state); + } + break; + default: + return ARGP_ERR_UNKNOWN; + }; + return 0; +} + +/* MAIN */ + +#include +#include + +struct program_opts *main_opts = 0; + +void cleanup_main_opts(void) { free(main_opts); } + +int play_tones(int dry_run, const char *device, struct tone_cmd *tones, + int tone_count); + +int main(int argc, char *argv[]) { + main_opts = calloc(sizeof(struct program_opts), 1); + if (main_opts == NULL) { + fprintf(stderr, + "Unable to allocate main program options. Out of memory?\n"); + return 1; + } + atexit(cleanup_main_opts); /* argp will exit instead of returning */ + error_t err = argp_parse(&args, argc, argv, 0, 0, main_opts); + if (err) { + fprintf(stderr, + "Unable to parse program arguments. Please report this bug.\n"); + return 1; + } + return play_tones(main_opts->dry_run, main_opts->device, main_opts->tones, + main_opts->tone_count); +} + +/* TONE PROCESSING */ + +#include + +static void cleanup_fd(int *fd) { + if (*fd >= 0) { + close(*fd); + } +} +#define _cleanup_fd_ __attribute__((cleanup(cleanup_fd))) + +int open_device(int dry_run, const char *device); +int play_tone(int dry_run, int device, int tone_hz); +int wait_ms(int dry_run, int duration_ms); +int stop_tone(int dry_run, int device); + +volatile int got_signal = 0; + +void handle_signal(int signal) { got_signal = signal; } + +int play_tones(int dry_run, const char *device, struct tone_cmd *tones, + int tone_count) { + _cleanup_fd_ int dev = open_device(dry_run, device); + signal(SIGINT, handle_signal); + signal(SIGTERM, handle_signal); + signal(SIGPIPE, handle_signal); + if (dev == -1) { + return 1; + } + for (int i = 0; i < tone_count; ++i) { + int tone_hz = tones[i].tone_hz; + int duration_ms = tones[i].duration_ms; + if (play_tone(dry_run, dev, tone_hz)) { + return 1; + } + if (wait_ms(dry_run, duration_ms)) { + return 1; + } + if (got_signal) { + fprintf(stdout, "Got signal %i, quitting\n", got_signal); + break; + } + } + if (stop_tone(dry_run, dev)) { + return 1; + } + return 0; +} + +/* SYSTEM SPECIFIC */ + +#include +#include +#include +#include +#include +#include + +const char *device_guesses[] = { + "/dev/input/by-path/platform-pcspkr-event-spkr", /* pcspkr on x86 */ + "/dev/input/by-path/platform-beeper-event", /* pwm-beeper */ + 0}; + +int try_open_device(int dry_run, const char *device) { + if (dry_run) { + fprintf(stdout, "Try opening device: %s\n", device); + return -2; + } + int dev = open(device, O_WRONLY); + return dev; +} + +int open_device(int dry_run, const char *device) { + int dev = -1; + if (device) { + int dev = try_open_device(dry_run, device); + if (dev == -1) { + fprintf(stdout, "Couldn't open %s: %s\n", device, strerror(errno)); + return -1; + } + } else { + const char **guess = device_guesses; + do { + dev = try_open_device(dry_run, *guess); + } while (*++guess && dev < 0); + if (dev == -1) { + fprintf(stdout, "Couldn't guess device\n"); + return -1; + } + } + return dev; +} + +int play_tone(int dry_run, int device, int tone_hz) { + if (dry_run) { + fprintf(stdout, "Send event EV_SND SND_TONE %i to device\n", tone_hz); + return 0; + } + struct input_event event = {0}; + event.type = EV_SND; + event.code = SND_TONE; + event.value = tone_hz; + if (write(device, &event, sizeof(struct input_event)) == -1) { + fprintf(stdout, "Couldn't write %iHz SND_TONE event to device: %s\n", + tone_hz, strerror(errno)); + return 1; + } + return 0; +} + +int stop_tone(int dry_run, int device) { return play_tone(dry_run, device, 0); } + +int wait_ms(int dry_run, int duration_ms) { + int sec = duration_ms / 1000; + int ns = (duration_ms % 1000) * 1000000; + if (dry_run) { + fprintf(stdout, "nanosleep for %i second %i nanoseconds\n", sec, ns); + return 0; + } + struct timespec time = {sec, ns}; + if (nanosleep(&time, NULL) == -1) { + if (!(errno == EINTR && got_signal)) { + fprintf(stdout, "Couldn't nanosleep for %i second %i nanoseconds: %s\n", + sec, ns, strerror(errno)); + } + return 1; + } + return 0; +}