├── .gitignore ├── README.md ├── sig_dispatcher.h ├── Makefile.am ├── meta_handler.h ├── utils.h ├── gst_player.h ├── configure.ac ├── config_schema.xsd ├── scheduler.h ├── main.c ├── fsp_player.h ├── sig_dispatcher.c ├── scheduler.c ├── pls_handler.c ├── utils.c ├── media_loader.c ├── meta_handler.c ├── gst_player.c ├── cfg_handler.c ├── LICENSE └── fsp_player.c /.gitignore: -------------------------------------------------------------------------------- 1 | audio_scheduler 2 | 3 | # autocrap 4 | configure 5 | stamp* 6 | *.o 7 | Makefile 8 | Makefile.in 9 | build-aux 10 | autom4te.cache 11 | config.h* 12 | config.log 13 | config.status 14 | .deps 15 | aclocal.m4 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # audio-scheduler 2 | An audio clip scheduler for use in radio broadcasting 3 | 4 | 5 | ## Building 6 | 7 | This project uses autotools for building. It requires 8 | running autoreconf before configuring in order to generate 9 | the rest of the autotools build files that are not under 10 | revision control. 11 | 12 | ``` 13 | $ autoreconf -fi 14 | $ ./configure 15 | $ make 16 | ``` 17 | -------------------------------------------------------------------------------- /sig_dispatcher.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileType: SOURCE 3 | * 4 | * SPDX-FileCopyrightText: 2025 Nick Kossifidis 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #ifndef __SIG_DISPATCHER_H__ 10 | #define __SIG_DISPATCHER_H__ 11 | #include /* For sig_atomic_t */ 12 | 13 | enum sig_unit { 14 | SIG_UNIT_PLAYER, 15 | SIG_UNIT_META, 16 | SIG_UNIT_MAX 17 | }; 18 | 19 | typedef void (*sig_cb)(int signo, void *data); 20 | 21 | struct sig_handler { 22 | enum sig_unit unit; 23 | sig_cb cb; 24 | void *data; 25 | }; 26 | 27 | struct sig_dispatcher { 28 | int signal_fd; 29 | int epoll_fd; 30 | pthread_t thread; 31 | volatile sig_atomic_t running; 32 | 33 | /* Registered handlers */ 34 | struct sig_handler handlers[SIG_UNIT_MAX]; 35 | pthread_mutex_t handlers_mutex; 36 | }; 37 | 38 | void sig_dispatcher_cleanup(struct sig_dispatcher *sd); 39 | int sig_dispatcher_init(struct sig_dispatcher *sd); 40 | int sig_dispatcher_start(struct sig_dispatcher *sd); 41 | int sig_dispatcher_register(struct sig_dispatcher *sd, enum sig_unit unit, sig_cb cb, void *data); 42 | 43 | #endif /* __SIG_DISPATCHER_H__ */ -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | bin_PROGRAMS = audio_scheduler 2 | 3 | config_schema.o: config_schema.xsd 4 | $(LD) -r -b binary -o $@ $< 5 | 6 | cfg_handler.o: config_schema.o 7 | 8 | # Keep these out until we fix/update the gstreamer player backend 9 | #audio_scheduler_SOURCES = cfg_handler.c pls_handler.c meta_handler.c \ 10 | # media_loader.c gst_player.c utils.c scheduler.c main.c 11 | #audio_scheduler_LDADD = config_schema.o $(GStreamer_LIBS) $(LibXML2_LIBS) 12 | #audio_scheduler_CFLAGS = ${CFLAGS} ${GStreamer_CFLAGS} ${LibXML2_CFLAGS} \ 13 | # -Wall -fms-extensions 14 | 15 | audio_scheduler_SOURCES = utils.c cfg_handler.c pls_handler.c media_loader.c scheduler.c meta_handler.c fsp_player.c sig_dispatcher.c main.c 16 | audio_scheduler_LDADD = config_schema.o 17 | audio_scheduler_LDFLAGS = $(LibXML2_LIBS) $(AVFORMAT_LIBS) $(AVCODEC_LIBS) $(AVUTIL_LIBS) $(SWRESAMPLE_LIBS) $(JACK_LIBS) $(PIPEWIRE_LIBS) -lm -pthread 18 | audio_scheduler_CFLAGS = $(CFLAGS) $(LibXML2_CFLAGS) $(AVFORMAT_CFLAGS) $(AVCODEC_CFLAGS) $(AVUTIL_CFLAGS) $(SWRESAMPLE_CFLAGS) $(JACK_CFLAGS) $(PIPEWIRE_CFLAGS) \ 19 | -Wall -fms-extensions 20 | 21 | #Also clean up after autoconf 22 | distclean-local: 23 | -rm -rf autom4te.cache 24 | -rm -rf build-aux 25 | -rm aclocal.m4 26 | -rm configure 27 | -rm *.in 28 | -rm *~ 29 | 30 | #Do nothing 31 | test: 32 | -------------------------------------------------------------------------------- /meta_handler.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileType: SOURCE 3 | * 4 | * SPDX-FileCopyrightText: 2017 - 2025 Nick Kossifidis 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #ifndef __META_HANDLER_H__ 10 | #define __META_HANDLER_H__ 11 | 12 | #include /* For size_t */ 13 | #include /* For pthread stuff */ 14 | #include "scheduler.h" /* For audiofile_info and time_t (through time.h) */ 15 | #include "sig_dispatcher.h" /* For registering with signal dispatcher */ 16 | 17 | /* Callback to the player for updating current state */ 18 | typedef int (*mh_state_cb)(struct audiofile_info *cur, struct audiofile_info *next, 19 | uint32_t *elapsed_sec, void *player_data); 20 | 21 | struct meta_handler { 22 | int epoll_fd; 23 | int listen_fd; 24 | void *player_data; 25 | mh_state_cb state_cb; 26 | volatile int running; 27 | pthread_t thread; 28 | 29 | /* Cache last response */ 30 | char response[2048]; 31 | size_t response_len; 32 | time_t last_update; 33 | time_t next_update; 34 | pthread_mutex_t update_mutex; 35 | }; 36 | 37 | 38 | void mh_stop(struct meta_handler *mh); 39 | int mh_start(struct meta_handler *mh); 40 | void mh_cleanup(struct meta_handler *mh); 41 | int mh_init(struct meta_handler *mh, uint16_t port, const char* ip4addr, struct sig_dispatcher *sd); 42 | int mh_register_state_callback(struct meta_handler *mh, mh_state_cb cb, void *player_data); 43 | 44 | #endif /* __META_HANDLER_H__ */ 45 | -------------------------------------------------------------------------------- /utils.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileType: SOURCE 3 | * 4 | * SPDX-FileCopyrightText: 2016 Nick Kossifidis 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #ifndef __UTILS_H__ 10 | #define __UTILS_H__ 11 | 12 | #include /* For va_list handling */ 13 | #include /* For time_t */ 14 | #include "config.h" /* For DEBUG via autoconf */ 15 | 16 | enum facilities { 17 | NONE = 0, 18 | SCHED = 1 << 0, 19 | PLR = 1 << 1, 20 | CFG = 1 << 2, 21 | PLS = 1 << 3, 22 | LDR = 1 << 4, 23 | UTILS = 1 << 5, 24 | META = 1 << 6, 25 | SIGDISP = 1 << 7, 26 | SKIP = 1 << 8, 27 | }; 28 | 29 | enum log_levels { 30 | SILENT = 0, 31 | ERROR = 1, 32 | WARN = 2, 33 | INFO = 3, 34 | DBG = 4, 35 | }; 36 | 37 | /* Log configuration */ 38 | void utils_set_debug_mask(int debug_msk); 39 | int utils_is_debug_enabled(int facility); 40 | void utils_set_log_level(int log_lvl); 41 | 42 | /* Log output */ 43 | void utils_verr(int facility, const char* fmt, va_list args); 44 | void utils_vperr(int facility, const char* fmt, va_list args); 45 | void utils_vwrn(int facility, const char* fmt, va_list args); 46 | void utils_vpwrn(int facility, const char* fmt, va_list args); 47 | void utils_vinfo(int facility, const char* fmt, va_list args); 48 | void utils_vdbg(int facility, const char* fmt, va_list args); 49 | 50 | void utils_err(int facility, const char* fmt,...); 51 | void utils_perr(int facility, const char* fmt,...); 52 | void utils_wrn(int facility, const char* fmt,...); 53 | void utils_pwrn(int facility, const char* fmt,...); 54 | void utils_info(int facility, const char* fmt,...); 55 | void utils_dbg(int facility, const char* fmt,...); 56 | 57 | /* File operations */ 58 | time_t utils_get_mtime(char* filepath); 59 | int utils_is_regular_file(char* filepath); 60 | int utils_is_readable_file(char*filepath); 61 | 62 | /* Misc */ 63 | void utils_trim_string(char* string); 64 | unsigned int utils_get_random_uint(); 65 | int utils_compare_time(struct tm *tm1, struct tm* tm2, int no_date); 66 | 67 | #endif /* __UTILS_H__ */ 68 | -------------------------------------------------------------------------------- /gst_player.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Audio Scheduler - An audio clip scheduler for use in radio broadcasting 3 | * Crossfade-capable player 4 | * 5 | * Copyright (C) 2017 George Kiagiadakis 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | #ifndef __PLAYER_H__ 22 | #define __PLAYER_H__ 23 | 24 | #include "scheduler.h" 25 | #include "meta_handler.h" 26 | #include 27 | 28 | #define PLAY_QUEUE_SIZE 3 29 | 30 | struct player; 31 | 32 | struct play_queue_item 33 | { 34 | struct player *player; 35 | 36 | /* info we got from the scheduler */ 37 | gchar *file; 38 | struct fader_info fader; 39 | gchar *zone; 40 | 41 | /* info we discovered; rt = running time */ 42 | guint64 duration; 43 | GstClockTime start_rt; 44 | GstClockTime fadeout_rt; 45 | GstClockTime end_rt; 46 | 47 | /* operational variables */ 48 | GstElement *bin; 49 | GstPad *mixer_sink; 50 | 51 | struct play_queue_item *previous; 52 | struct play_queue_item *next; 53 | }; 54 | 55 | struct player 56 | { 57 | /* external objects */ 58 | struct scheduler *scheduler; 59 | struct meta_handler *mh; 60 | 61 | /* internal objects */ 62 | GMainLoop *loop; 63 | GstElement *pipeline; 64 | GstElement *mixer; 65 | 66 | struct play_queue_item *playlist; 67 | }; 68 | 69 | int gst_player_init (struct player* self, struct scheduler* scheduler, 70 | struct meta_handler *mh, const char *audiosink); 71 | void gst_player_cleanup (struct player* self); 72 | 73 | void gst_player_loop (struct player* self); 74 | void gst_player_loop_quit (struct player* self); 75 | 76 | #endif /* __PLAYER_H__ */ 77 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | #Prelude 2 | AC_INIT([audio-scheduler],[0.8],[radio-list@culture.uoc.gr]) 3 | AC_CONFIG_SRCDIR([main.c]) 4 | AC_CONFIG_AUX_DIR([build-aux]) 5 | AM_INIT_AUTOMAKE([foreign -Wall -Werror dist-bzip2]) 6 | 7 | # Check for programs 8 | AC_PROG_CC 9 | 10 | # Check for libraries 11 | PKG_CHECK_MODULES(LibXML2, 12 | [ 13 | libxml-2.0 >= 2.9.0 14 | ], 15 | [ 16 | AC_SUBST(LibXML2_CFLAGS) 17 | AC_SUBST(LibXML2_LIBS) 18 | ], 19 | [ 20 | AC_MSG_ERROR([Could not find libxml2]) 21 | ]) 22 | 23 | PKG_CHECK_MODULES(AVFORMAT, [ libavformat >= 5.0 ], 24 | [ 25 | AC_SUBST(AVFORMAT_CFLAGS) 26 | AC_SUBST(AVFORMAT_LIBS) 27 | ], 28 | [ AC_MSG_ERROR([libavformat not found]) ]) 29 | PKG_CHECK_MODULES(AVCODEC, [ libavcodec >= 5.0], 30 | [ 31 | AC_SUBST(AVCODEC_CFLAGS) 32 | AC_SUBST(AVCODEC_LIBS) 33 | ], 34 | [ AC_MSG_ERROR([libavcodec not found]) ]) 35 | PKG_CHECK_MODULES(AVUTIL, [ libavutil >= 5.0 ], 36 | [ 37 | AC_SUBST(AVUTIL_CFLAGS) 38 | AC_SUBST(AVUTIL_LIBS) 39 | ], 40 | [ AC_MSG_ERROR([libavutil not found]) ]) 41 | PKG_CHECK_MODULES(SWRESAMPLE, [ libswresample >= 4.0 ], 42 | [ 43 | AC_SUBST(SWRESAMPLE_CFLAGS) 44 | AC_SUBST(SWRESAMPLE_LIBS) 45 | ], 46 | [ AC_MSG_ERROR([libswresample not found]) ]) 47 | 48 | # Why on earth is it still 0.3 ? 49 | PKG_CHECK_MODULES(PIPEWIRE, [ libpipewire-0.3 >= 1.2 ], 50 | [ 51 | AC_SUBST(PIPEWIRE_CFLAGS) 52 | AC_SUBST(PIPEWIRE_LIBS) 53 | ], 54 | [ AC_MSG_ERROR([libpipewire not found]) ]) 55 | 56 | # We want that for the ringbuffer implementation 57 | # when pipewire with jack support is installed, this is 58 | # also there 59 | PKG_CHECK_MODULES(JACK, [ jack >= 1.9.17 ], 60 | [ 61 | AC_SUBST(JACK_CFLAGS) 62 | AC_SUBST(JACK_LIBS) 63 | ], 64 | [ AC_MSG_ERROR([jack (or Pipewire's jack API) not found]) ]) 65 | 66 | # Keep these out for now until we fix/update the gstreamer player backend 67 | #PKG_CHECK_MODULES(GStreamer, 68 | # [ 69 | # gstreamer-1.0 >= 1.0.0 70 | # gstreamer-base-1.0 >= 1.0.0 71 | # gstreamer-controller-1.0 >= 1.0.0 72 | # ], 73 | # [ 74 | # AC_SUBST(GStreamer_CFLAGS) 75 | # AC_SUBST(GStreamer_LIBS) 76 | # ], 77 | # [ 78 | # AC_MSG_ERROR([Could not find GStreamer 1.0 libraries]) 79 | # ]) 80 | # 81 | 82 | 83 | #Configuration / define macros 84 | AC_ARG_WITH([debug], 85 | AS_HELP_STRING([--with-debug], 86 | [Enable debug output]), 87 | [debug_enabled=$withval], 88 | [debug_enabled=no]) 89 | 90 | AS_IF([test "x$debug_enabled" = "xyes"], [ 91 | AC_DEFINE([DEBUG], [1], [Define this to enable debug output]) 92 | CFLAGS="$CFLAGS -g" # Append -g to CFLAGS 93 | ]) 94 | AC_SUBST([CFLAGS]) 95 | 96 | # Output files 97 | AC_CONFIG_HEADERS([config.h]) 98 | AC_CONFIG_FILES([Makefile]) 99 | AC_OUTPUT 100 | 101 | -------------------------------------------------------------------------------- /config_schema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /scheduler.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileType: SOURCE 3 | * 4 | * SPDX-FileCopyrightText: 2016 - 2025 Nick Kossifidis 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | #ifndef __SCHEDULER_H__ 9 | #define __SCHEDULER_H__ 10 | 11 | #include /* For time_t */ 12 | 13 | struct fader_info { 14 | int fadein_duration_secs; 15 | int fadeout_duration_secs; 16 | }; 17 | 18 | struct audiofile_info { 19 | const char* filepath; /* from pls->items[] */ 20 | 21 | char* artist; 22 | char* album; 23 | char* title; 24 | char* albumid; 25 | char* release_trackid; 26 | 27 | float album_gain; 28 | float album_peak; 29 | float track_gain; 30 | float track_peak; 31 | 32 | time_t duration_secs; 33 | 34 | /* zone->name of current zone */ 35 | const char* zone_name; 36 | /* playlist->fader of current playlist*/ 37 | const struct fader_info *fader_info; 38 | 39 | /* Marks a clone, where all string fields are copies, 40 | * and so should be freed. */ 41 | int is_copy; 42 | }; 43 | 44 | struct playlist { 45 | char* filepath; 46 | int num_items; 47 | char** items; 48 | int shuffle; 49 | time_t last_mtime; 50 | int curr_idx; 51 | struct fader_info *fader; 52 | }; 53 | 54 | struct intermediate_playlist { 55 | /* Anonymous struct as per C11 56 | * Note that this declaration is cleaner 57 | * and I prefer it but it needs -fms-extensions 58 | * to work on GCC, for more infos check out: 59 | * https://gcc.gnu.org/onlinedocs/gcc-5.1.0/gcc/Unnamed-Fields.html 60 | */ 61 | struct playlist; 62 | 63 | char* name; 64 | int sched_interval_mins; 65 | time_t last_scheduled; 66 | int num_sched_items; 67 | int sched_items_pending; 68 | }; 69 | 70 | struct zone { 71 | char* name; 72 | struct tm start_time; 73 | char* maintainer; 74 | char* description; 75 | char* comment; 76 | struct playlist *main_pls; 77 | struct playlist *fallback_pls; 78 | int num_others; 79 | struct intermediate_playlist **others; 80 | }; 81 | 82 | struct day_schedule { 83 | int num_zones; 84 | struct zone **zones; 85 | }; 86 | 87 | struct week_schedule { 88 | struct day_schedule *days[7]; 89 | }; 90 | 91 | struct config { 92 | char* filepath; 93 | char* schema_filepath; 94 | time_t last_mtime; 95 | struct week_schedule *ws; 96 | }; 97 | 98 | struct scheduler { 99 | struct config *cfg; 100 | int state_flags; 101 | }; 102 | 103 | enum state_flags { 104 | SCHED_FAILED = 2, 105 | SCHED_LOADING_NEW = 4, 106 | }; 107 | 108 | /* File handling */ 109 | void mldr_copy_audiofile(struct audiofile_info *dst, struct audiofile_info *src); 110 | void mldr_cleanup_audiofile(struct audiofile_info *info); 111 | int mldr_init_audiofile(char* filepath, const char* zone_name, const struct fader_info *fdr, struct audiofile_info *info, int strict); 112 | 113 | /* Playlist handling */ 114 | void pls_files_cleanup(struct playlist* pls); 115 | void pls_shuffle(struct playlist* pls); 116 | int pls_process(struct playlist* pls); 117 | int pls_reload_if_needed(struct playlist* pls); 118 | 119 | /* Config handling */ 120 | void cfg_cleanup(struct config *cfg); 121 | int cfg_process(struct config *cfg); 122 | int cfg_reload_if_needed(struct config *cfg); 123 | 124 | /* Scheduler entry points */ 125 | int sched_get_next(struct scheduler* sched, time_t sched_time, struct audiofile_info* next_info); 126 | int sched_init(struct scheduler* sched, char* config_filepath); 127 | void sched_cleanup(struct scheduler* sched); 128 | 129 | #endif /* __SCHEDULER_H__ */ 130 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileType: SOURCE 3 | * 4 | * SPDX-FileCopyrightText: 2016 - 2025 Nick Kossifidis 5 | * SPDX-FileCopyrightText: 2017 George Kiagiadakis 6 | * 7 | * SPDX-License-Identifier: GPL-3.0-or-later 8 | */ 9 | 10 | #include /* For getopt() */ 11 | #include /* For sig_atomic_t and signal handling */ 12 | #include /* For strtol() */ 13 | #include /* For perror() */ 14 | #include /* For av_log_set_level() */ 15 | #include "utils.h" 16 | #include "scheduler.h" 17 | //#include "gst_player.h" 18 | #include "fsp_player.h" 19 | #include "meta_handler.h" 20 | #include "sig_dispatcher.h" 21 | 22 | //static const char * usage_str = 23 | // "Usage: %s [-s audio_sink_bin] [-d debug_level] [-m debug_mask] [-p port] \n"; 24 | 25 | static const char * usage_str = 26 | "Usage: %s [-d debug_level] [-m debug_mask] [-p port] \n"; 27 | 28 | int 29 | main(int argc, char **argv) 30 | { 31 | struct scheduler sched = {0}; 32 | struct meta_handler mh = {0}; 33 | struct fsp_player fsp = {0}; 34 | struct sig_dispatcher sd = {0}; 35 | int ret = 0, opt, tmp; 36 | int dbg_lvl = INFO; 37 | int dbg_mask = PLR|SCHED|META; 38 | uint16_t port = 9670; 39 | //char *sink = NULL; 40 | 41 | while ((opt = getopt(argc, argv, "s:d:m:p:")) != -1) { 42 | switch (opt) { 43 | /* 44 | case 's': 45 | sink = optarg; 46 | break; 47 | */ 48 | case 'd': 49 | tmp = strtol(optarg, NULL, 10); 50 | if (errno != 0) 51 | perror("Failed to parse debug level"); 52 | else 53 | dbg_lvl = tmp; 54 | break; 55 | case 'm': 56 | tmp = strtol(optarg, NULL, 16); 57 | if (errno != 0) 58 | perror("Failed to parse debug mask"); 59 | else 60 | dbg_mask = tmp; 61 | break; 62 | case 'p': 63 | tmp = strtol(optarg, NULL, 10); 64 | if (errno != 0) 65 | perror("Failed to parse port number"); 66 | else 67 | port = tmp; 68 | break; 69 | default: 70 | printf(usage_str, argv[0]); 71 | return(0); 72 | } 73 | } 74 | 75 | if (optind >= argc) { 76 | printf(usage_str, argv[0]); 77 | return(0); 78 | } 79 | 80 | /* Configure log output */ 81 | utils_set_log_level(dbg_lvl); 82 | utils_set_debug_mask(dbg_mask); 83 | /* Prevent ffmpeg from spamming us, we report errors anyway */ 84 | av_log_set_level(AV_LOG_ERROR); 85 | 86 | ret = sig_dispatcher_init(&sd); 87 | if (ret < 0) { 88 | utils_err(NONE, "Unable to initialize signal dispatcher\n"); 89 | ret = -1; 90 | goto cleanup; 91 | } 92 | sig_dispatcher_start(&sd); 93 | 94 | ret = sched_init(&sched, argv[optind]); 95 | if (ret < 0) { 96 | utils_err(NONE, "Unable to initialize scheduler\n"); 97 | ret = -1; 98 | goto cleanup; 99 | } 100 | 101 | ret = mh_init(&mh, port, NULL, &sd); 102 | if (ret < 0) { 103 | utils_err(NONE, "Unable to initialize metadata request hanlder\n"); 104 | ret = -2; 105 | goto cleanup; 106 | } 107 | mh_start(&mh); 108 | 109 | #if 0 110 | ret = gst_player_init(&player, &sched, &mh, sink); 111 | if (ret < 0) { 112 | utils_err(NONE, "Unable to initialize player\n"); 113 | ret = -3; 114 | goto cleanup; 115 | } 116 | 117 | /* This will spawn a g_main_loop and block */ 118 | gst_player_loop(&player); 119 | #endif 120 | 121 | ret = fsp_init(&fsp, &sched, &mh, &sd); 122 | if (ret < 0) { 123 | utils_err(NONE, "Unable to initialize player\n"); 124 | ret = -3; 125 | goto cleanup; 126 | } 127 | 128 | /* This will spawn a pw_main_loop and block */ 129 | fsp_start(&fsp); 130 | 131 | utils_info(NONE, "Graceful exit...\n"); 132 | 133 | cleanup: 134 | mh_cleanup(&mh); 135 | fsp_cleanup(&fsp); 136 | // gst_player_cleanup(&player); 137 | sched_cleanup(&sched); 138 | sig_dispatcher_cleanup(&sd); 139 | return ret; 140 | } 141 | -------------------------------------------------------------------------------- /fsp_player.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileType: SOURCE 3 | * 4 | * SPDX-FileCopyrightText: 2025 Nick Kossifidis 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | #ifndef __PLAYER_H__ 9 | #define __PLAYER_H__ 10 | 11 | #include /* For pthread support */ 12 | #include /* For AVCodecContext / decoder */ 13 | #include /* For AVFormatContext / demuxer */ 14 | #include /* For SwrContext / resampler/converter */ 15 | #include /* For pipewire support */ 16 | #include /* For using JACK's ringbuffer API (also available through pipewire) */ 17 | #include "scheduler.h" 18 | #include "meta_handler.h" 19 | #include "sig_dispatcher.h" 20 | 21 | /* Configuration */ 22 | #define FSP_PERIOD_SIZE 2048 /* TODO: query pipewire for this */ 23 | #define FSP_OUTPUT_SAMPLE_RATE 48000 24 | #define FSP_OUTPUT_CHANNELS 2 25 | #define FSP_RING_BUFFER_SECONDS 4 26 | #define FSP_CACHE_LINE_SIZE 64 /* TODO: get that from the OS through sysconf() */ 27 | 28 | /* Slope for the 2sec gain curve, for fade in/out during pause/resume */ 29 | #define FSP_STATE_FADE_SLOPE (1.0f / (FSP_OUTPUT_SAMPLE_RATE * 2)) 30 | 31 | /* Player states */ 32 | enum fsp_state { 33 | FSP_STATE_STOPPED, 34 | FSP_STATE_PLAYING, 35 | FSP_STATE_PAUSING, /* Fading out before pause */ 36 | FSP_STATE_PAUSED, /* Fully paused */ 37 | FSP_STATE_RESUMING, /* Fading in from pause */ 38 | FSP_STATE_STOPPING, 39 | FSP_STATE_ERROR 40 | }; 41 | 42 | /* Structures */ 43 | struct fsp_decoder_state { 44 | AVFormatContext *fmt_ctx; 45 | AVCodecContext *codec_ctx; 46 | SwrContext *swr_ctx; 47 | AVFrame *decoded_avframe; 48 | AVFrame *resampled_avframe; 49 | AVPacket *stream_packet; 50 | int audio_stream_idx; 51 | size_t consumed_frames; 52 | size_t avail_frames; 53 | int eof_reached; 54 | volatile int started; 55 | }; 56 | 57 | struct fsp_af_fader_state { 58 | float fade_in_slope; 59 | float fade_out_slope; 60 | }; 61 | 62 | struct fsp_state_fader_state { 63 | float state_fade_slope; 64 | size_t state_fade_samples_tot; /* Total samples for state fade */ 65 | size_t state_fade_samples_out; /* Current position in state fade */ 66 | int state_fade_active; /* Whether we're in a state fade */ 67 | float state_fade_gain; /* Current gain during state fade */ 68 | }; 69 | 70 | struct fsp_replaygain_state { 71 | float replay_gain; 72 | float gain_limit; 73 | }; 74 | 75 | struct fsp_audiofile_ctx { 76 | struct fsp_decoder_state decoder; 77 | struct fsp_af_fader_state fader; 78 | struct fsp_replaygain_state replaygain; 79 | struct audiofile_info info; 80 | size_t total_samples; 81 | size_t samples_played; 82 | }; 83 | 84 | struct fsp_player { 85 | /* State */ 86 | volatile sig_atomic_t state; 87 | 88 | /* Current and next file info */ 89 | struct fsp_audiofile_ctx current; 90 | struct fsp_audiofile_ctx next; 91 | pthread_mutex_t file_mutex; 92 | 93 | /* Fader for state changes */ 94 | struct fsp_state_fader_state fader; 95 | 96 | /* Scheduler thread */ 97 | pthread_t scheduler_thread; 98 | pthread_mutex_t scheduler_mutex; 99 | pthread_cond_t scheduler_cond; 100 | struct scheduler *sched; 101 | 102 | /* Decoder thread */ 103 | pthread_t decoder_thread; 104 | pthread_mutex_t decoder_mutex; 105 | pthread_cond_t decoder_cond; 106 | 107 | /* Ring buffer */ 108 | jack_ringbuffer_t *ring; 109 | pthread_cond_t space_available; 110 | 111 | /* Pipewire specific members */ 112 | struct pw_main_loop *loop; 113 | struct pw_context *context; 114 | struct pw_core *core; 115 | struct pw_stream *stream; 116 | }; 117 | 118 | /* Functions */ 119 | 120 | void fsp_cleanup(struct fsp_player *player); 121 | int fsp_init(struct fsp_player *player, struct scheduler *sched, 122 | struct meta_handler *mh, struct sig_dispatcher *sd); 123 | int fsp_start(struct fsp_player *player); 124 | void fsp_stop(struct fsp_player *player); 125 | 126 | #endif /* __PLAYER_H__ */ -------------------------------------------------------------------------------- /sig_dispatcher.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileType: SOURCE 3 | * 4 | * SPDX-FileCopyrightText: 2025 Nick Kossifidis 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * This is the application's signal dispatcher. Since we have multiple 11 | * threads we need to control which thread receives signals, and also 12 | * be able to access shared data (the various state structs) which is 13 | * not safe to do from a normal signal handler. For this we use Linux's 14 | * signalfd mechanism and we epoll it on a thread, normal signal delivery 15 | * is blocked. Each component that spawns threads (currently the player 16 | * and the metadata handler) registers a callback to the dispatcher which 17 | * is called when a signal is delivered through signalfd, passing on its 18 | * state structure to it for convenience. 19 | */ 20 | 21 | #include /* For errno */ 22 | #include /* For memset */ 23 | #include /* For size_t */ 24 | #include /* For sigfillset/delset/strsignal etc */ 25 | #include /* For signalfd creation */ 26 | #include /* For epoll on signalfd */ 27 | #include /* For read() */ 28 | #include /* For pthread_create and mutexes */ 29 | #include "sig_dispatcher.h" 30 | #include "utils.h" 31 | 32 | /* Make sure this follows the enum */ 33 | static const char* unit_names[2] = {"PLAYER", "META"}; 34 | 35 | static void *sig_thread(void *arg) 36 | { 37 | struct sig_dispatcher *sd = arg; 38 | struct signalfd_siginfo si; 39 | struct epoll_event events[1]; 40 | int i; 41 | 42 | while (sd->running) { 43 | int n = epoll_wait(sd->epoll_fd, events, 1, -1); 44 | if (n < 0) { 45 | if (errno == EINTR) 46 | continue; 47 | break; 48 | } 49 | 50 | if (read(sd->signal_fd, &si, sizeof(si)) != sizeof(si)) 51 | continue; 52 | 53 | /* Dispatch signal to registered handlers */ 54 | pthread_mutex_lock(&sd->handlers_mutex); 55 | for (i = 0; i < SIG_UNIT_MAX; i++) { 56 | if (sd->handlers[i].cb) { 57 | utils_dbg(SIGDISP, "Sending %s, to %s\n", strsignal(si.ssi_signo), 58 | unit_names[i]); 59 | sd->handlers[i].cb(si.ssi_signo, sd->handlers[i].data); 60 | } 61 | } 62 | pthread_mutex_unlock(&sd->handlers_mutex); 63 | 64 | /* If we got a SIGINT/SIGTERM also terminate this thread */ 65 | if (si.ssi_signo == SIGINT || si.ssi_signo == SIGTERM) { 66 | utils_dbg(SIGDISP, "Stopped\n"); 67 | break; 68 | } 69 | } 70 | 71 | return NULL; 72 | } 73 | 74 | 75 | /**************\ 76 | * ENTRY POITNS * 77 | \**************/ 78 | 79 | int sig_dispatcher_start(struct sig_dispatcher *sd) 80 | { 81 | utils_dbg(SIGDISP, "Starting\n"); 82 | sd->running = 1; 83 | if (pthread_create(&sd->thread, NULL, sig_thread, sd) != 0) { 84 | utils_perr(SIGDISP, "Couldn't create sig_thread"); 85 | return -1; 86 | } 87 | 88 | return 0; 89 | } 90 | 91 | void sig_dispatcher_cleanup(struct sig_dispatcher *sd) 92 | { 93 | if (!sd) 94 | return; 95 | sd->running = 0; 96 | pthread_join(sd->thread, NULL); 97 | 98 | pthread_mutex_destroy(&sd->handlers_mutex); 99 | 100 | if (sd->signal_fd >= 0) 101 | close(sd->signal_fd); 102 | if (sd->epoll_fd >= 0) 103 | close(sd->epoll_fd); 104 | } 105 | 106 | int sig_dispatcher_init(struct sig_dispatcher *sd) 107 | {; 108 | int ret = 0; 109 | 110 | memset(sd, 0, sizeof(struct sig_dispatcher)); 111 | 112 | /* Block all incomming signals, except those that result 113 | * crashing, we'll handle them from the signal dispatcher 114 | * thread. Do this early on so that all threads inherit the 115 | * sigmask. */ 116 | sigset_t mask; 117 | sigfillset(&mask); 118 | sigdelset(&mask, SIGFPE); 119 | sigdelset(&mask, SIGILL); 120 | sigdelset(&mask, SIGSEGV); 121 | sigdelset(&mask, SIGBUS); 122 | sigdelset(&mask, SIGABRT); 123 | if (pthread_sigmask(SIG_BLOCK, &mask, NULL) < 0) { 124 | utils_perr(SIGDISP, "Couldn't block signals"); 125 | ret = -1; 126 | goto cleanup; 127 | } 128 | 129 | /* Create signalfd */ 130 | sd->signal_fd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC); 131 | if (sd->signal_fd < 0) { 132 | utils_perr(SIGDISP, "Could not create signalfd"); 133 | ret = -1; 134 | goto cleanup; 135 | } 136 | 137 | sd->epoll_fd = epoll_create1(EPOLL_CLOEXEC); 138 | if (sd->epoll_fd < 0) { 139 | utils_perr(SIGDISP, "Could not create epoll_fd"); 140 | ret = -1; 141 | goto cleanup; 142 | } 143 | 144 | pthread_mutexattr_t attr; 145 | pthread_mutexattr_init(&attr); 146 | pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); 147 | pthread_mutex_init(&sd->handlers_mutex, &attr); 148 | 149 | struct epoll_event ev; 150 | ev.events = EPOLLIN; 151 | ev.data.fd = sd->signal_fd; 152 | if (epoll_ctl(sd->epoll_fd, EPOLL_CTL_ADD, sd->signal_fd, &ev) < 0) { 153 | utils_perr(SIGDISP, "epoll_ctl failed"); 154 | ret = -1; 155 | goto cleanup; 156 | } 157 | 158 | return 0; 159 | 160 | cleanup: 161 | sig_dispatcher_cleanup(sd); 162 | return ret; 163 | } 164 | 165 | int sig_dispatcher_register(struct sig_dispatcher *sd, enum sig_unit unit, 166 | sig_cb cb, void *data) 167 | { 168 | if (!sd || unit >= SIG_UNIT_MAX || !cb) 169 | return -1; 170 | 171 | pthread_mutex_lock(&sd->handlers_mutex); 172 | sd->handlers[unit].cb = cb; 173 | sd->handlers[unit].data = data; 174 | pthread_mutex_unlock(&sd->handlers_mutex); 175 | 176 | return 0; 177 | } -------------------------------------------------------------------------------- /scheduler.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileType: SOURCE 3 | * 4 | * SPDX-FileCopyrightText: 2016 - 2025 Nick Kossifidis 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * This part is the scheduler core, based on the currently loaded config, it provides 11 | * a audiofile_info struct to the player, to be played at a provided time_t. This allows 12 | * the player to ask for songs to be played in the future, or at an updated time e.g. 13 | * after pause/resume. 14 | */ 15 | 16 | #include /* For malloc */ 17 | #include /* For memset */ 18 | #include "scheduler.h" 19 | #include "utils.h" 20 | 21 | /*********\ 22 | * HELPERS * 23 | \*********/ 24 | 25 | static int 26 | sched_is_ipls_ready(struct intermediate_playlist* ipls, time_t sched_time) 27 | { 28 | struct tm tm_curr = *localtime(&sched_time); 29 | struct tm ipls_rdy_tm = *localtime(&ipls->last_scheduled); 30 | int mins = ipls_rdy_tm.tm_min; 31 | int ret = 0; 32 | 33 | if (!ipls) 34 | return 0; 35 | 36 | /* Add interval to ipls ready time */ 37 | mins += ipls->sched_interval_mins; 38 | ipls_rdy_tm.tm_hour += mins / 60; 39 | ipls_rdy_tm.tm_min = mins % 60; 40 | 41 | ret = utils_compare_time(&tm_curr, &ipls_rdy_tm, 0); 42 | if(ret <= 0) 43 | return 0; 44 | 45 | utils_dbg(SCHED, "Intermediate playlist ready: %s\n", ipls->name); 46 | return 1; 47 | } 48 | 49 | static int 50 | sched_get_next_item(struct audiofile_info* next_info, struct playlist* pls, const char* zone_name) 51 | { 52 | int ret = 0; 53 | int idx = 0; 54 | char* next = NULL; 55 | 56 | if (!pls) 57 | return -1; 58 | 59 | /* Re-load playlist if needed */ 60 | ret = pls_reload_if_needed(pls); 61 | if(ret < 0) { 62 | utils_err(SCHED, "Re-loading playlist %s failed\n", pls->filepath); 63 | return -1; 64 | } 65 | 66 | /* We've played the whole list, reset index and 67 | * re-shuffle if needed */ 68 | if((pls->curr_idx + 1) >= pls->num_items) { 69 | pls->curr_idx = 0; 70 | if(pls->shuffle) { 71 | utils_dbg(SCHED, "Re-shuffling playlist\n"); 72 | pls_shuffle(pls); 73 | } 74 | } 75 | 76 | /* Check if next item is readable, if not 77 | * loop until we find a readable one. If we 78 | * don't find any readable file on the playlist 79 | * return NULL */ 80 | for(idx = pls->curr_idx; idx < pls->num_items; idx++) { 81 | next = pls->items[idx]; 82 | if(utils_is_readable_file(next)) { 83 | pls->curr_idx = idx + 1; 84 | ret = mldr_init_audiofile(next, zone_name, pls->fader, next_info, 1); 85 | if (!ret) 86 | return 0; 87 | else { 88 | utils_wrn(SCHED, "Failed to load file: %s\n", next); 89 | /* Non fatal */ 90 | continue; 91 | } 92 | } 93 | utils_wrn(SCHED, "File unreadable %s\n", next); 94 | } 95 | 96 | return -1; 97 | } 98 | 99 | 100 | /**************\ 101 | * ENTRY POINTS * 102 | \**************/ 103 | 104 | /* Note that failing to re-load config or get an item from a 105 | * playlist or intermediate playlist is not fatal. It 106 | * might be a temporary issue e.g. with network storage. 107 | * however if we can't get an item from any playlist 108 | * then we can't do anything about it. */ 109 | 110 | int 111 | sched_get_next(struct scheduler* sched, time_t sched_time, struct audiofile_info* next_info) 112 | { 113 | struct playlist *pls = NULL; 114 | struct intermediate_playlist *ipls = NULL; 115 | struct zone *zn = NULL; 116 | struct day_schedule *ds = NULL; 117 | struct week_schedule *ws = NULL; 118 | int i = 0; 119 | int ret = 0; 120 | struct tm tm = *localtime(&sched_time); 121 | char datestr[26]; 122 | 123 | if (!sched) 124 | return -1; 125 | 126 | /* format: Day DD Mon YYYY, HH:MM:SS */ 127 | strftime (datestr, 26, "%a %d %b %Y, %H:%M:%S", &tm); 128 | utils_info (SCHED, "Scheduling item for: %s\n", datestr); 129 | 130 | /* Reload config if needed */ 131 | ret = cfg_reload_if_needed(sched->cfg); 132 | if(ret < 0) { 133 | utils_wrn(SCHED, "Re-loading config failed\n"); 134 | return -1; 135 | } 136 | 137 | /* Get current day */ 138 | ws = sched->cfg->ws; 139 | ds = ws->days[tm.tm_wday]; 140 | 141 | /* Find a zone with a start time less 142 | * than the current time. In order to 143 | * get the latest one and since the zones 144 | * are stored in ascending order, do 145 | * the lookup backwards */ 146 | for(i = ds->num_zones - 1; i >= 0; i--) { 147 | zn = ds->zones[i]; 148 | ret = utils_compare_time(&tm, &zn->start_time, 1); 149 | 150 | if (utils_is_debug_enabled (SCHED)) { 151 | strftime (datestr, 26, "%H:%M:%S", &zn->start_time); 152 | utils_dbg (SCHED, "considering zone '%s' at: %s -> %i\n", 153 | zn->name, datestr, ret); 154 | } 155 | if(ret > 0) 156 | break; 157 | } 158 | 159 | if(i < 0) { 160 | utils_wrn(SCHED, "Nothing is scheduled for now "); 161 | utils_wrn(SCHED|SKIP, "using first zone of the day\n"); 162 | zn = ds->zones[0]; 163 | } 164 | 165 | /* Is it time to load an item from an intermediate 166 | * playlist ? Note: We assume here that intermediate 167 | * playlists are sorted in descending order from higher 168 | * to lower priority. */ 169 | for(i = 0; i < zn->num_others && zn->others; i++) { 170 | if(sched_is_ipls_ready(zn->others[i], sched_time)) { 171 | ipls = zn->others[i]; 172 | 173 | /* Only update last_scheduled after we've 174 | * scheduled num_sched_items */ 175 | if(ipls->sched_items_pending == -1) 176 | ipls->sched_items_pending = ipls->num_sched_items; 177 | else if(!ipls->sched_items_pending) { 178 | /* Done with this one, mark it as scheduled 179 | * and move on to the next */ 180 | ipls->sched_items_pending = -1; 181 | ipls->last_scheduled = sched_time; 182 | continue; 183 | } 184 | utils_dbg(SCHED, "Pending items: %i\n", 185 | ipls->sched_items_pending); 186 | 187 | pls = (struct playlist*) ipls; 188 | ipls->sched_items_pending--; 189 | break; 190 | } 191 | } 192 | 193 | ret = sched_get_next_item(next_info, pls, zn->name); 194 | if(!ret) { 195 | utils_dbg(SCHED, "Using intermediate playlist\n"); 196 | goto done; 197 | } 198 | 199 | /* Go for the main playlist */ 200 | pls = zn->main_pls; 201 | ret = sched_get_next_item(next_info, pls, zn->name); 202 | if(!ret) { 203 | utils_dbg(SCHED, "Using main playlist\n"); 204 | goto done; 205 | } 206 | 207 | /* Go for the fallback playlist */ 208 | pls = zn->fallback_pls; 209 | ret = sched_get_next_item(next_info, pls, zn->name); 210 | if(!ret) { 211 | utils_wrn(SCHED, "Using fallback playlist\n"); 212 | goto done; 213 | } 214 | 215 | done: 216 | if(!ret) { 217 | utils_info(SCHED, "Got next item from zone '%s': %s (fader: %s)\n", 218 | zn->name, next_info->filepath, pls->fader ? "true" : "false"); 219 | return 0; 220 | } 221 | 222 | /* Nothing we can do */ 223 | utils_err(SCHED, "could not find anything to schedule\n"); 224 | return -1; 225 | } 226 | 227 | int 228 | sched_init(struct scheduler* sched, char* config_filepath) 229 | { 230 | struct config *cfg = NULL; 231 | int ret = 0; 232 | 233 | memset(sched, 0, sizeof(struct scheduler)); 234 | 235 | cfg = (struct config*) malloc(sizeof(struct config)); 236 | if(!cfg) { 237 | utils_err(SCHED, "Could not allocate config structure\n"); 238 | return -1; 239 | } 240 | 241 | cfg->filepath = config_filepath; 242 | 243 | ret = cfg_process(cfg); 244 | if (ret < 0) 245 | return -1; 246 | 247 | sched->cfg = cfg; 248 | 249 | utils_dbg(SCHED, "Initialized\n"); 250 | return 0; 251 | } 252 | 253 | void 254 | sched_cleanup(struct scheduler* sched) 255 | { 256 | if(sched->cfg!=NULL) { 257 | cfg_cleanup(sched->cfg); 258 | free(sched->cfg); 259 | } 260 | sched->cfg=NULL; 261 | } 262 | -------------------------------------------------------------------------------- /pls_handler.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileType: SOURCE 3 | * 4 | * SPDX-FileCopyrightText: 2016 - 2025 Nick Kossifidis 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * This part parses a playlist file (m3u and pls are supported) and populates 11 | * a playlist struct, it also supports shuffling the playlist, and does some 12 | * basic checks to make sure each file exists and is readable. 13 | */ 14 | 15 | #include /* For malloc/realloc/free */ 16 | #include /* For strncmp() and strchr() */ 17 | #include /* For FILE handling/getline() */ 18 | #include /* For PATH_MAX */ 19 | #include "scheduler.h" 20 | #include "utils.h" 21 | 22 | enum pls_type { 23 | TYPE_PLS = 1, 24 | TYPE_M3U = 2, 25 | }; 26 | 27 | 28 | /*********\ 29 | * HELPERS * 30 | \*********/ 31 | 32 | static int 33 | pls_check_type(char* filepath) 34 | { 35 | int filepath_len = 0; 36 | int type = -1; 37 | char* ext = NULL; 38 | 39 | filepath_len = strnlen(filepath, PATH_MAX); 40 | ext = filepath + filepath_len + 1 - 4; 41 | if(!strncmp(ext, "pls", 4)) 42 | type = TYPE_PLS; 43 | else if(!strncmp(ext, "m3u", 4)) 44 | type = TYPE_M3U; 45 | else 46 | utils_err(PLS, "Unknown file type: %s\n", filepath); 47 | 48 | return type; 49 | } 50 | 51 | static void 52 | pls_files_cleanup_internal(char** files, int num_files) 53 | { 54 | int i = 0; 55 | 56 | if(!files) 57 | return; 58 | 59 | for(i = 0; i < num_files; i++) { 60 | if(files[i]) { 61 | free(files[i]); 62 | files[i] = NULL; 63 | } 64 | } 65 | 66 | free(files); 67 | } 68 | 69 | static int 70 | pls_add_file(char* filepath, char ***files, int *num_files) 71 | { 72 | int len = 0; 73 | int ret = 0; 74 | char* file = NULL; 75 | char** temp = NULL; 76 | 77 | utils_trim_string(filepath); 78 | 79 | /* Is it a file that we can read ? 80 | * Note that M3Us may also contain 81 | * folders, this is not supported here 82 | * for now */ 83 | if(!utils_is_readable_file(filepath)) 84 | return -1; 85 | 86 | /* Get size of the filepath string, including 87 | * null terminator */ 88 | len = strnlen(filepath, PATH_MAX) + 1; 89 | 90 | file = (char*) malloc(len); 91 | if(!file) { 92 | utils_err(PLS, "Could not allocate filename on files array\n"); 93 | return -1; 94 | } 95 | memcpy(file, filepath, len); 96 | 97 | temp = realloc(*files, ((*num_files) + 1) * sizeof(char*)); 98 | if(!temp) { 99 | utils_err(PLS, "Could not expand files array\n"); 100 | free(file); 101 | ret = -1; 102 | goto cleanup; 103 | } 104 | *files = temp; 105 | 106 | (*files)[(*num_files)] = file; 107 | utils_dbg(PLS, "Added file: %s\n", (*files)[(*num_files)]); 108 | (*num_files)++; 109 | 110 | cleanup: 111 | if(ret < 0) { 112 | pls_files_cleanup_internal(*files, (*num_files)); 113 | (*num_files) = 0; 114 | *files = NULL; 115 | } 116 | return ret; 117 | } 118 | 119 | 120 | /**********\ 121 | * SHUFFLER * 122 | \**********/ 123 | 124 | static inline void 125 | pls_file_swap(char** items, int x, int y) 126 | { 127 | char* tmp = items[x]; 128 | items[x] = items[y]; 129 | items[y] = tmp; 130 | } 131 | 132 | void 133 | pls_shuffle(struct playlist* pls) 134 | { 135 | unsigned int next_file_idx = 0; 136 | int target_slot = 0; 137 | 138 | /* Nothing to shuffle */ 139 | if(pls->num_items <= 1) 140 | return; 141 | 142 | /* Shuffle playlist using Durstenfeld's algorithm: 143 | * Pick a random number from the remaining ones, 144 | * and stack it up the end of the array. */ 145 | for (target_slot = pls->num_items-1; target_slot > 0; target_slot--) { 146 | next_file_idx = utils_get_random_uint() % target_slot; 147 | pls_file_swap(pls->items, next_file_idx, target_slot); 148 | } 149 | 150 | if(utils_is_debug_enabled(PLS)) { 151 | utils_dbg(PLS, "--== Shuffled list ==--\n"); 152 | int i = 0; 153 | for(i = 0; i < pls->num_items; i++) 154 | utils_dbg(PLS|SKIP, "%i %s\n", i, pls->items[i]); 155 | } 156 | 157 | return; 158 | } 159 | 160 | 161 | /**************\ 162 | * ENTRY POINTS * 163 | \**************/ 164 | 165 | void 166 | pls_files_cleanup(struct playlist* pls) 167 | { 168 | pls_files_cleanup_internal(pls->items, pls->num_items); 169 | } 170 | 171 | int 172 | pls_process(struct playlist* pls) 173 | { 174 | char* line = NULL; 175 | size_t line_size = 0; 176 | ssize_t line_len = 0; 177 | int line_num = 0; 178 | char* delim = NULL; 179 | FILE *pls_file = NULL; 180 | int type = 0; 181 | int ret = 0; 182 | 183 | /* Sanity checks */ 184 | if(pls->filepath == NULL) { 185 | utils_err(PLS, "Called with null argument\n"); 186 | ret = -1; 187 | goto cleanup; 188 | } 189 | 190 | ret = pls_check_type(pls->filepath); 191 | if(ret < 0) 192 | goto cleanup; 193 | type = ret; 194 | 195 | 196 | if(!utils_is_readable_file(pls->filepath)) { 197 | utils_err(PLS, "Could not read playlist: %s\n", pls->filepath); 198 | ret = -1; 199 | goto cleanup; 200 | } 201 | 202 | /* Store mtime for later checks */ 203 | pls->last_mtime = utils_get_mtime(pls->filepath); 204 | if(!pls->last_mtime) { 205 | ret = -1; 206 | goto cleanup; 207 | } 208 | 209 | /* Open playlist file and start parsing its contents */ 210 | pls_file = fopen(pls->filepath, "rb"); 211 | if (pls_file == NULL) { 212 | utils_perr(PLS, "Couldn't open file %s", pls->filepath); 213 | ret = -1; 214 | goto cleanup; 215 | } 216 | 217 | switch(type) { 218 | case TYPE_PLS: 219 | /* Grab the first line and see if it's the expected header */ 220 | line_len = getline(&line, &line_size, pls_file); 221 | if (line_len > 0) { 222 | utils_trim_string(line); 223 | if(strncmp(line, "[playlist]", 11)) { 224 | utils_err(PLS, "Invalid header on %s\n", 225 | pls->filepath); 226 | ret = -1; 227 | goto cleanup; 228 | } 229 | } 230 | 231 | line_num = 2; 232 | while ((line_len = getline(&line, &line_size, pls_file)) > 0) { 233 | /* Not a file */ 234 | if(strncmp(line, "File", 4)) 235 | continue; 236 | 237 | delim = strchr(line, '='); 238 | if(!delim){ 239 | utils_err(PLS, "malformed line %i in pls file: %s\n", 240 | line_num, pls->filepath); 241 | ret = -1; 242 | goto cleanup; 243 | } 244 | delim++; 245 | 246 | ret = pls_add_file(delim, &pls->items, &pls->num_items); 247 | if(ret < 0) { 248 | utils_wrn(PLS, "couldn't add file: %s\n", delim); 249 | /* Non-fatal */ 250 | ret = 0; 251 | } 252 | line_num++; 253 | } 254 | break; 255 | case TYPE_M3U: 256 | line_num = 0; 257 | while ((line_len = getline(&line, &line_size, pls_file)) > 0) { 258 | line_num++; 259 | /* EXTINF etc */ 260 | if(line[0] == '#') 261 | continue; 262 | 263 | ret = pls_add_file(line, &pls->items, &pls->num_items); 264 | if(ret < 0) { 265 | utils_wrn(PLS, "couldn't add file on line number %i: %s\n", 266 | line_num, pls->filepath); 267 | /* Non-fatal */ 268 | ret = 0; 269 | } 270 | } 271 | break; 272 | default: 273 | /* Shouldn't reach this */ 274 | ret = -1; 275 | goto cleanup; 276 | } 277 | 278 | if (!pls->num_items) { 279 | utils_err(PLS, "got empty playlist: %s\n", pls->filepath); 280 | ret = -1; 281 | goto cleanup; 282 | } 283 | 284 | /* Shuffle contents if needed */ 285 | if(pls->shuffle) 286 | pls_shuffle(pls); 287 | 288 | utils_dbg(PLS, "Got %i files from %s\n", pls->num_items, pls->filepath); 289 | 290 | cleanup: 291 | if(pls_file) 292 | fclose(pls_file); 293 | 294 | if (line) 295 | free(line); 296 | 297 | if(ret < 0) { 298 | if(pls->items) 299 | pls_files_cleanup_internal(pls->items, pls->num_items); 300 | pls->num_items = 0; 301 | } 302 | return ret; 303 | } 304 | 305 | int 306 | pls_reload_if_needed(struct playlist* pls) 307 | { 308 | time_t mtime = utils_get_mtime(pls->filepath); 309 | if(!mtime) { 310 | utils_err(PLS, "Unable to check mtime for %s\n", pls->filepath); 311 | return -1; 312 | } 313 | 314 | /* mtime didn't change, no need to reload */ 315 | if(mtime == pls->last_mtime) 316 | return 0; 317 | 318 | utils_info(PLS, "Got different mtime, reloading %s\n", pls->filepath); 319 | 320 | /* Re-load playlist */ 321 | pls_files_cleanup(pls); 322 | return pls_process(pls); 323 | } 324 | -------------------------------------------------------------------------------- /utils.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileType: SOURCE 3 | * 4 | * SPDX-FileCopyrightText: 2016 Nick Kossifidis 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * This part includes various utility functions for convinience 11 | */ 12 | 13 | #define _GNU_SOURCE /* Needed for vasprintf() */ 14 | #include "utils.h" 15 | #include /* For v/printf() */ 16 | #include /* For free()/random() */ 17 | #include /* For errno */ 18 | #include /* For strerror()/memmove() */ 19 | #include /* For PATH_MAX */ 20 | #include /* For struct stat */ 21 | #include /* For stat()/access()/syscall() */ 22 | 23 | /* For getrandom syscall available on linux 3.17+ */ 24 | #if defined(__linux__) && defined(__GLIBC__) 25 | #include 26 | #if defined(SYS_getrandom) 27 | #define GETRANDOM_DEFINED 28 | #endif 29 | #endif 30 | 31 | 32 | /* Some codes for prety output on the terminal */ 33 | #define NORMAL "\x1B[0m" 34 | #define BRIGHT "\x1B[1m" 35 | #define DIM "\x1B[2m" 36 | #define RED "\x1B[31m" 37 | #define GREEN "\x1B[32m" 38 | #define YELLOW "\x1B[33m" 39 | #define BLUE "\x1B[34m" 40 | #define MAGENTA "\x1B[35m" 41 | #define CYAN "\x1B[36m" 42 | #define WHITE "\x1B[37m" 43 | 44 | static int log_level = 0; 45 | 46 | #ifdef DEBUG 47 | static int debug_mask = 0; 48 | 49 | void 50 | utils_set_debug_mask(int debug_msk) 51 | { 52 | debug_mask = debug_msk; 53 | } 54 | 55 | int 56 | utils_is_debug_enabled(int facility) 57 | { 58 | return ((debug_mask & facility) == facility ? 1 : 0); 59 | } 60 | 61 | #else 62 | void utils_set_debug_mask(int debug_msk) {} 63 | int utils_is_debug_enabled(int facility) {return 0;} 64 | #endif 65 | 66 | void 67 | utils_set_log_level(int log_lvl) 68 | { 69 | log_level = log_lvl; 70 | } 71 | 72 | static const char* 73 | utils_get_facility_name(int facility) 74 | { 75 | if(facility & SKIP) 76 | return ""; 77 | 78 | switch(facility & 0xFF) { 79 | case NONE: 80 | return ""; 81 | case SCHED: 82 | return "[SCHED] "; 83 | case PLR: 84 | return "[PLR] "; 85 | case CFG: 86 | return "[CFG] "; 87 | case PLS: 88 | return "[PLS] "; 89 | case LDR: 90 | return "[LDR] "; 91 | case SIGDISP: 92 | return "[SIGDISP] "; 93 | case META: 94 | return "[META] "; 95 | case UTILS: 96 | return "[UTILS] "; 97 | default: 98 | return "[UNK] "; 99 | } 100 | } 101 | 102 | 103 | void 104 | utils_verr(int facility, const char* fmt, va_list args) 105 | { 106 | char *msg = NULL; 107 | int ret = 0; 108 | 109 | if(log_level < ERROR) 110 | return; 111 | 112 | ret = vasprintf(&msg, fmt, args); 113 | if(ret < 0) 114 | return; 115 | 116 | fprintf(stderr, RED"%s%s"NORMAL, 117 | utils_get_facility_name(facility), msg); 118 | fflush(stderr); 119 | free(msg); 120 | } 121 | 122 | void 123 | utils_err(int facility, const char* fmt,...) 124 | { 125 | va_list args; 126 | if(log_level < ERROR) 127 | return; 128 | va_start(args, fmt); 129 | utils_verr(facility, fmt, args); 130 | va_end(args); 131 | } 132 | 133 | 134 | void 135 | utils_vperr(int facility, const char* fmt, va_list args) 136 | { 137 | char *msg = NULL; 138 | int ret = 0; 139 | 140 | if(log_level < ERROR) 141 | return; 142 | 143 | ret = vasprintf(&msg, fmt, args); 144 | if(ret < 0) 145 | return; 146 | 147 | fprintf(stderr, RED"%s%s: %s"NORMAL"\n", 148 | utils_get_facility_name(facility), msg, strerror(errno)); 149 | fflush(stderr); 150 | free(msg); 151 | } 152 | 153 | void 154 | utils_perr(int facility, const char* fmt,...) 155 | { 156 | va_list args; 157 | if(log_level < ERROR) 158 | return; 159 | va_start(args, fmt); 160 | utils_vperr(facility, fmt, args); 161 | va_end(args); 162 | } 163 | 164 | 165 | void 166 | utils_vwrn(int facility, const char* fmt, va_list args) 167 | { 168 | char *msg = NULL; 169 | int ret = 0; 170 | 171 | if(log_level < WARN) 172 | return; 173 | 174 | ret = vasprintf(&msg, fmt, args); 175 | if(ret < 0) 176 | return; 177 | 178 | fprintf(stderr, YELLOW"%s%s"NORMAL, utils_get_facility_name(facility), msg); 179 | fflush(stderr); 180 | free(msg); 181 | } 182 | 183 | void 184 | utils_wrn(int facility, const char* fmt,...) 185 | { 186 | va_list args; 187 | if(log_level < WARN) 188 | return; 189 | va_start(args, fmt); 190 | utils_vwrn(facility, fmt, args); 191 | va_end(args); 192 | } 193 | 194 | 195 | void 196 | utils_vpwrn(int facility, const char* fmt, va_list args) 197 | { 198 | char *msg = NULL;; 199 | int ret = 0; 200 | 201 | if(log_level < WARN) 202 | return; 203 | 204 | ret = vasprintf(&msg, fmt, args); 205 | if(ret < 0) 206 | return; 207 | 208 | fprintf(stderr, YELLOW"%s%s: %s"NORMAL"\n", 209 | utils_get_facility_name(facility), msg, strerror(errno)); 210 | fflush(stderr); 211 | free(msg); 212 | } 213 | 214 | void 215 | utils_pwrn(int facility, const char* fmt,...) 216 | { 217 | va_list args; 218 | if(log_level < WARN) 219 | return; 220 | va_start(args, fmt); 221 | utils_vpwrn(facility, fmt, args); 222 | va_end(args); 223 | } 224 | 225 | 226 | void 227 | utils_vinfo(int facility, const char* fmt, va_list args) 228 | { 229 | char *msg = NULL; 230 | int ret = 0; 231 | 232 | if(log_level < INFO) 233 | return; 234 | 235 | ret = vasprintf(&msg, fmt, args); 236 | if(ret < 0) 237 | return; 238 | 239 | printf(CYAN"%s%s"NORMAL, utils_get_facility_name(facility), msg); 240 | fflush(stdout); 241 | free(msg); 242 | } 243 | 244 | void 245 | utils_info(int facility, const char* fmt,...) 246 | { 247 | va_list args; 248 | if(log_level < INFO) 249 | return; 250 | va_start(args, fmt); 251 | utils_vinfo(facility, fmt, args); 252 | va_end(args); 253 | } 254 | 255 | 256 | #ifdef DEBUG 257 | void 258 | utils_vdbg(int facility, const char* fmt, va_list args) 259 | { 260 | char *msg = NULL; 261 | int ret = 0; 262 | 263 | if(log_level < DBG) 264 | return; 265 | 266 | if(!(facility & debug_mask)) 267 | return; 268 | 269 | ret = vasprintf(&msg, fmt, args); 270 | if(ret < 0) 271 | return; 272 | 273 | fprintf(stderr, MAGENTA"%s%s"NORMAL, 274 | utils_get_facility_name(facility), msg); 275 | fflush(stderr); 276 | free(msg); 277 | } 278 | 279 | void 280 | utils_dbg(int facility, const char* fmt,...) 281 | { 282 | va_list args; 283 | if(log_level < DBG) 284 | return; 285 | va_start(args, fmt); 286 | utils_vdbg(facility, fmt, args); 287 | va_end(args); 288 | } 289 | #else 290 | void utils_vdbg(int facility, const char* fmt, va_list args) {} 291 | void utils_dbg(int facility, const char* fmt,...){} 292 | #endif 293 | 294 | 295 | void 296 | utils_trim_string(char* string) 297 | { 298 | char* start = NULL; 299 | char* end = NULL; 300 | int len = strnlen(string, PATH_MAX); 301 | int i = 0; 302 | 303 | /* Find start/end of actual string */ 304 | while(i < len) { 305 | start = string + i; 306 | if(((*start) != ' ') && ((*start) != '\n') && 307 | ((*start) != '\r')) 308 | break; 309 | i++; 310 | } 311 | 312 | i = len - 1; 313 | while(i >= 0) { 314 | end = string + i; 315 | if(((*end) != ' ') && ((*end) != '\n') && 316 | ((*end) != '\r') && ((*end) != '\0')) 317 | break; 318 | i--; 319 | } 320 | 321 | /* NULL-terminate it */ 322 | (*(end + 1)) = '\0'; 323 | 324 | /* Move it to the beginning of the buffer */ 325 | len = end - start + 1; 326 | memmove(string, start, len); 327 | } 328 | 329 | 330 | static int 331 | utils_platform_getrandom(void *buf, size_t len) 332 | { 333 | static int source_reported = 0; 334 | int ret = 0; 335 | FILE *file = NULL; 336 | 337 | #if defined(GETRANDOM_DEFINED) 338 | ret = syscall(SYS_getrandom, buf, len, 0); 339 | if (ret == len) { 340 | if(!source_reported) { 341 | utils_dbg(UTILS, "Got random data through syscall\n"); 342 | source_reported = 1; 343 | } 344 | return 0; 345 | } 346 | #endif 347 | /* Syscall failed, open urandom instead */ 348 | file = fopen("/dev/urandom", "rb"); 349 | if (file == NULL) 350 | return -1; 351 | 352 | ret = fread(buf, 1, len, file); 353 | if (ret != len) { 354 | fclose(file); 355 | return -1; 356 | } 357 | 358 | if(!source_reported) { 359 | utils_dbg(UTILS, "Got random data through /dev/urandom\n"); 360 | source_reported = 1; 361 | } 362 | fclose(file); 363 | return ret; 364 | } 365 | 366 | unsigned int 367 | utils_get_random_uint() 368 | { 369 | static int source_reported = 0; 370 | unsigned int value = 0; 371 | int ret = 0; 372 | 373 | /* Try to get a random number from the os, if it fails 374 | * fallback to libc's random() */ 375 | ret = utils_platform_getrandom(&value, sizeof(unsigned int)); 376 | if(ret < 0) { 377 | value = (unsigned int) random(); 378 | if(!source_reported) { 379 | utils_dbg(UTILS, "Got random data through random()\n"); 380 | source_reported = 1; 381 | } 382 | } 383 | 384 | return value; 385 | } 386 | 387 | time_t 388 | utils_get_mtime(char* filepath) 389 | { 390 | struct stat st; 391 | 392 | if (stat(filepath, &st) < 0) { 393 | utils_perr(UTILS, "Could not stat(%s)", filepath); 394 | return 0; 395 | } 396 | 397 | return st.st_mtime; 398 | } 399 | 400 | int 401 | utils_is_regular_file(char* filepath) 402 | { 403 | struct stat st; 404 | int ret = 1; 405 | 406 | ret = stat(filepath, &st); 407 | if (ret < 0) { 408 | utils_pwrn(UTILS, "Could not stat(%s)", filepath); 409 | ret = 0; 410 | } else if (!S_ISREG(st.st_mode)) { 411 | utils_wrn(UTILS, "Not a regular file: %s\n", filepath); 412 | ret = 0; 413 | } 414 | 415 | return 1; 416 | } 417 | 418 | int 419 | utils_is_readable_file(char*filepath) 420 | { 421 | #ifndef TEST 422 | int ret = 0; 423 | 424 | if(!utils_is_regular_file(filepath)) 425 | return 0; 426 | 427 | ret = access(filepath, R_OK); 428 | if(ret < 0) { 429 | utils_pwrn(UTILS, "access(%s) failed", filepath); 430 | return 0; 431 | } 432 | #endif 433 | return 1; 434 | } 435 | 436 | 437 | static void 438 | utils_tm_cleanup_date(struct tm *tm) 439 | { 440 | /* Zero-out the date part */ 441 | tm->tm_mday = 1; 442 | tm->tm_mon = 0; 443 | tm->tm_year = 70; 444 | tm->tm_wday = 0; 445 | tm->tm_yday = 0; 446 | tm->tm_isdst = -1; 447 | } 448 | 449 | int 450 | utils_compare_time(struct tm *tm1, struct tm* tm0, int no_date) 451 | { 452 | time_t t1 = 0; 453 | time_t t0 = 0; 454 | double diff = 0.0L; 455 | 456 | if(no_date) { 457 | utils_tm_cleanup_date(tm0); 458 | utils_tm_cleanup_date(tm1); 459 | } 460 | 461 | errno = 0; 462 | t1 = mktime(tm1); 463 | t0 = mktime(tm0); 464 | diff = difftime(t1, t0); 465 | 466 | if (errno != 0) 467 | utils_perr(UTILS, "compare_time"); 468 | 469 | if(diff > 0.0L) 470 | return 1; 471 | else if(diff < 0.0L) 472 | return -1; 473 | else 474 | return 0; 475 | } 476 | -------------------------------------------------------------------------------- /media_loader.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileType: SOURCE 3 | * 4 | * SPDX-FileCopyrightText: 2025 Nick Kossifidis 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * This part loads and pre-processes audio files before passing them on to the player. 11 | * It performs metadata parsing, integrity checking, and accurate duration calculation. 12 | */ 13 | 14 | #include /* For AVFormat / demuxer */ 15 | #include /* For AvDecoder / decoder */ 16 | #include /* For av_dict* */ 17 | #include "scheduler.h" 18 | #include "utils.h" 19 | 20 | 21 | /*********\ 22 | * HELPERS * 23 | \*********/ 24 | 25 | typedef enum { 26 | ARTIST, 27 | ALBUM, 28 | TITLE, 29 | ALBUM_GAIN, 30 | ALBUM_PEAK, 31 | ALBUM_ID, 32 | TRACK_GAIN, 33 | TRACK_PEAK, 34 | RELEASE_TID, 35 | } AudioTagType; 36 | 37 | /* Helper function to get ReplayGain tags both upper/lower case */ 38 | static char* 39 | mldr_get_tag(AVDictionary *metadata, AudioTagType tag_type) 40 | { 41 | char tag_name[128]; 42 | AVDictionaryEntry *tag = NULL; 43 | 44 | if (!metadata) 45 | return NULL; 46 | 47 | /* Note that av_dict_get is case insensitive by default */ 48 | switch (tag_type) { 49 | case ARTIST: 50 | snprintf(tag_name, sizeof(tag_name), "ARTIST"); 51 | tag = av_dict_get(metadata, tag_name, NULL, 0); 52 | break; 53 | case ALBUM: 54 | snprintf(tag_name, sizeof(tag_name), "ALBUM"); 55 | tag = av_dict_get(metadata, tag_name, NULL, 0); 56 | break; 57 | case TITLE: 58 | snprintf(tag_name, sizeof(tag_name), "TITLE"); 59 | tag = av_dict_get(metadata, tag_name, NULL, 0); 60 | break; 61 | case ALBUM_GAIN: 62 | snprintf(tag_name, sizeof(tag_name), "REPLAYGAIN_ALBUM_GAIN"); 63 | tag = av_dict_get(metadata, tag_name, NULL, 0); 64 | break; 65 | case ALBUM_PEAK: 66 | snprintf(tag_name, sizeof(tag_name), "REPLAYGAIN_ALBUM_PEAK"); 67 | tag = av_dict_get(metadata, tag_name, NULL, 0); 68 | break; 69 | case ALBUM_ID: 70 | snprintf(tag_name, sizeof(tag_name), "MUSICBRAINZ_ALBUMID"); 71 | tag = av_dict_get(metadata, tag_name, NULL, 0); 72 | if (!tag) { 73 | /* Try idv3 variant */ 74 | snprintf(tag_name, sizeof(tag_name), "MusicBrainz Album Id"); 75 | tag = av_dict_get(metadata, tag_name, NULL, 0); 76 | } 77 | break; 78 | case TRACK_GAIN: 79 | snprintf(tag_name, sizeof(tag_name), "REPLAYGAIN_TRACK_GAIN"); 80 | tag = av_dict_get(metadata, tag_name, NULL, 0); 81 | break; 82 | case TRACK_PEAK: 83 | snprintf(tag_name, sizeof(tag_name), "REPLAYGAIN_TRACK_PEAK"); 84 | tag = av_dict_get(metadata, tag_name, NULL, 0); 85 | break; 86 | case RELEASE_TID: 87 | snprintf(tag_name, sizeof(tag_name), "MUSICBRAINZ_RELEASETRACKID"); 88 | tag = av_dict_get(metadata, tag_name, NULL, 0); 89 | if (!tag) { 90 | snprintf(tag_name, sizeof(tag_name), "MusicBrainz Release Track Id"); 91 | tag = av_dict_get(metadata, tag_name, NULL, 0); 92 | } 93 | break; 94 | default: 95 | return NULL; 96 | } 97 | 98 | if (tag) 99 | return strdup(tag->value); 100 | 101 | return NULL; 102 | } 103 | 104 | static float 105 | mldr_get_replaygain_tag(AVDictionary *metadata, AudioTagType tag_type) 106 | { 107 | float db_val = 0.0f; 108 | char *str_val = mldr_get_tag(metadata, tag_type); 109 | 110 | if (!str_val) 111 | return 0.0f; 112 | 113 | if (sscanf(str_val, "%f", &db_val) != 1) { 114 | utils_wrn(LDR, "Invalid ReplayGain format: %s\n", str_val); 115 | db_val = 0.0f; 116 | } 117 | 118 | free(str_val); 119 | return db_val; 120 | } 121 | 122 | 123 | /**************\ 124 | * ENTRY POINTS * 125 | \**************/ 126 | 127 | void 128 | mldr_copy_audiofile(struct audiofile_info *dst, struct audiofile_info *src) 129 | { 130 | dst->filepath = src->filepath ? strdup(src->filepath) : NULL; 131 | dst->artist = src->artist ? strdup(src->artist) : NULL; 132 | dst->album = src->album ? strdup(src->album) : NULL; 133 | dst->title = src->title ? strdup(src->title) : NULL; 134 | dst->albumid = src->albumid ? strdup(src->albumid) : NULL; 135 | dst->release_trackid = src->release_trackid ? strdup(src->release_trackid) : NULL; 136 | dst->album_gain = src->album_gain; 137 | dst->album_peak = src->album_peak; 138 | dst->track_gain = src->track_gain; 139 | dst->track_peak = src->track_peak; 140 | dst->duration_secs = src->duration_secs; 141 | dst->zone_name = src->zone_name ? strdup(src->zone_name) : NULL; 142 | /* No need to cary this around outside the player */ 143 | dst->fader_info = NULL; 144 | dst->is_copy = 1; 145 | } 146 | 147 | void 148 | mldr_cleanup_audiofile(struct audiofile_info *info) 149 | { 150 | /* Note: const pointers come from pls/zone so don't free them 151 | * unless they are copies, or we'll corrupt pls/zone structs. */ 152 | 153 | if (info->is_copy && info->filepath) 154 | free((char*) info->filepath); 155 | info->filepath = NULL; 156 | if(info->artist) { 157 | free(info->artist); 158 | info->artist = NULL; 159 | } 160 | if(info->album) { 161 | free(info->album); 162 | info->album = NULL; 163 | } 164 | if(info->title) { 165 | free(info->title); 166 | info->title = NULL; 167 | } 168 | if(info->albumid) { 169 | free(info->albumid); 170 | info->albumid = NULL; 171 | } 172 | if(info->release_trackid) { 173 | free(info->release_trackid); 174 | info->release_trackid = NULL; 175 | } 176 | if (info->is_copy && info->zone_name) 177 | free((char*) info->zone_name); 178 | info->zone_name = NULL; 179 | info->fader_info = NULL; 180 | } 181 | 182 | int mldr_init_audiofile(char* filepath, const char* zone_name, const struct fader_info *fdr, struct audiofile_info *info, int strict) { 183 | AVFormatContext *format_ctx = NULL; 184 | AVCodecContext *codec_ctx = NULL; 185 | int ret = 0; 186 | 187 | memset(info, 0, sizeof(struct audiofile_info)); 188 | 189 | /* We 've already checked that this is a readable file */ 190 | info->filepath = filepath; 191 | 192 | /* Set zone_name and fader_info from the scheduler */ 193 | info->fader_info = fdr; 194 | info->zone_name = zone_name; 195 | 196 | if (avformat_open_input(&format_ctx, info->filepath, NULL, NULL) != 0) { 197 | utils_err(LDR, "Could not open file %s\n", info->filepath); 198 | ret = -1; 199 | goto cleanup; 200 | } 201 | 202 | /* Find the audio stream inside the file */ 203 | if (avformat_find_stream_info(format_ctx, NULL) < 0) { 204 | utils_err(LDR, "Could not get stream info for %s\n", info->filepath); 205 | ret = -1; 206 | goto cleanup; 207 | } 208 | 209 | int audio_stream_index = av_find_best_stream(format_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0); 210 | if (audio_stream_index < 0) { 211 | utils_err(LDR, "Could not find audio stream in %s\n", info->filepath); 212 | ret = -1; 213 | goto cleanup; 214 | } 215 | 216 | codec_ctx = avcodec_alloc_context3(NULL); 217 | if (!codec_ctx) { 218 | utils_err(LDR, "Could not allocate codec context for %s\n", info->filepath); 219 | ret = -1; 220 | goto cleanup; 221 | } 222 | 223 | if (avcodec_parameters_to_context(codec_ctx, format_ctx->streams[audio_stream_index]->codecpar) < 0) { 224 | utils_err(LDR, "Could not copy codec params to context for %s\n", info->filepath); 225 | ret = -1; 226 | goto cleanup; 227 | } 228 | 229 | if (avcodec_open2(codec_ctx, avcodec_find_decoder(codec_ctx->codec_id), NULL) < 0) { 230 | utils_err(LDR, "Could not open codec for %s\n", info->filepath); 231 | ret = -1; 232 | goto cleanup; 233 | } 234 | 235 | 236 | /* Grab metadata */ 237 | info->artist = mldr_get_tag(format_ctx->metadata, ARTIST); 238 | info->album = mldr_get_tag(format_ctx->metadata, ALBUM); 239 | info->title = mldr_get_tag(format_ctx->metadata, TITLE); 240 | info->albumid = mldr_get_tag(format_ctx->metadata, ALBUM_ID); 241 | info->release_trackid = mldr_get_tag(format_ctx->metadata, RELEASE_TID); 242 | 243 | info->album_gain = mldr_get_replaygain_tag(format_ctx->metadata, ALBUM_GAIN); 244 | info->album_peak = mldr_get_replaygain_tag(format_ctx->metadata, ALBUM_PEAK); 245 | info->track_gain = mldr_get_replaygain_tag(format_ctx->metadata, TRACK_GAIN); 246 | info->track_peak = mldr_get_replaygain_tag(format_ctx->metadata, TRACK_PEAK); 247 | 248 | 249 | /* If strict duration calculation and checking wasn't requested, skip this part 250 | * and use whatever values ffmpeg gave us, if it didn't, go for it. */ 251 | if (!strict) { 252 | if (format_ctx->duration != AV_NOPTS_VALUE) { 253 | info->duration_secs = format_ctx->duration / AV_TIME_BASE; 254 | ret = 0; 255 | goto cleanup; 256 | } 257 | } 258 | 259 | /* Determine duration in a reliable way, since metadata can't be trusted (especially 260 | * for VBR mp3s), this is done by decoding the file, so we can check for any decoding errors 261 | * while at it. Note that this also brings the file in the page cache, so when the player gets 262 | * it again it'll get it (or most of it) from the cache instead of readingit again, so consider 263 | * this also as a form of pre-buffering. */ 264 | info->duration_secs = 0; 265 | double duration_secs_frac = 0.0f; 266 | int frame_count = 0; 267 | int decode_errors = 0; 268 | AVFrame *frame = av_frame_alloc(); 269 | AVPacket packet; 270 | 271 | /* Get encoded packets, grab decoded frames */ 272 | while (av_read_frame(format_ctx, &packet) >= 0) { 273 | if (packet.stream_index == audio_stream_index) { 274 | 275 | /* Send the packet to the decoder */ 276 | ret = avcodec_send_packet(codec_ctx, &packet); 277 | if (ret < 0) { 278 | decode_errors++; 279 | utils_wrn(LDR, "Error sending packet to decoder: %s (frame %d)\n", 280 | av_err2str(ret), frame_count); 281 | av_packet_unref(&packet); 282 | break; 283 | } 284 | /* Safe to unref here, decoder has a copy */ 285 | av_packet_unref(&packet); 286 | 287 | /* Receive frames from the decoder */ 288 | while (ret >= 0) { 289 | ret = avcodec_receive_frame(codec_ctx, frame); 290 | /* No frame available yet, go for next packet */ 291 | if (ret == AVERROR(EAGAIN)) 292 | break; 293 | /* No more frames */ 294 | else if (ret == AVERROR_EOF) 295 | break; 296 | else if (ret < 0) { 297 | decode_errors++; 298 | utils_wrn(LDR, "Error receiving frame from decoder: %s (last frame %d)\n", 299 | av_err2str(ret), frame_count); 300 | break; 301 | } else { 302 | duration_secs_frac += (double)frame->nb_samples * av_q2d(codec_ctx->time_base); 303 | frame_count++; 304 | av_frame_unref(frame); 305 | } 306 | } 307 | } 308 | } 309 | ret = 0; 310 | av_frame_free(&frame); 311 | /* Round duration_secs_frac to the closest higher integer */ 312 | info->duration_secs = (time_t)(duration_secs_frac + 0.5f); 313 | 314 | if (decode_errors > 0) { 315 | utils_err(LDR, "File %s has %d decoding errors.\n", info->filepath, decode_errors); 316 | ret = -1; 317 | goto cleanup; 318 | } 319 | 320 | if (frame_count == 0) { 321 | utils_wrn(LDR, "File %s contains no audio frames.\n", info->filepath); 322 | ret = -1; 323 | } 324 | 325 | /* Compare calculated duration with metadata duration (if available) */ 326 | if (format_ctx->duration != AV_NOPTS_VALUE) { 327 | time_t metadata_duration_seconds = format_ctx->duration / AV_TIME_BASE; 328 | int difference = abs((int)(info->duration_secs - metadata_duration_seconds)); 329 | int tolerance_secs = 1; 330 | if (difference > tolerance_secs) { 331 | utils_wrn(LDR, "Duration mismatch in %s: Metadata: %lu seconds, Calculated: %lu seconds (tolerance: %i secs)\n", 332 | info->filepath, metadata_duration_seconds, info->duration_secs, tolerance_secs); 333 | } 334 | } else { 335 | utils_wrn(LDR, "No Duration Metadata in %s\n", info->filepath); 336 | } 337 | 338 | cleanup: 339 | if (codec_ctx) { 340 | avcodec_close(codec_ctx); 341 | avcodec_free_context(&codec_ctx); 342 | } 343 | if (format_ctx) { 344 | avformat_close_input(&format_ctx); 345 | } 346 | 347 | if (ret != 0) { 348 | utils_err(LDR, "Error initializing audio file.\n"); 349 | mldr_cleanup_audiofile(info); 350 | return ret; 351 | } 352 | 353 | if (utils_is_debug_enabled(LDR)) { 354 | utils_dbg(LDR, "File: %s\n", info->filepath); 355 | utils_dbg(LDR, "Artist: %s\n", info->artist ? info->artist : "N/A"); 356 | utils_dbg(LDR, "Album: %s\n", info->album ? info->album : "N/A"); 357 | utils_dbg(LDR, "Title: %s\n", info->title ? info->title : "N/A"); 358 | utils_dbg(LDR, "Album ID: %s\n", info->albumid ? info->albumid : "N/A"); 359 | utils_dbg(LDR, "Release Track ID: %s\n", info->release_trackid ? info->release_trackid : "N/A"); 360 | utils_dbg(LDR, "Album Gain: %f\n", info->album_gain); 361 | utils_dbg(LDR, "Album Peak: %f\n", info->album_peak); 362 | utils_dbg(LDR, "Track Gain: %f\n", info->track_gain); 363 | utils_dbg(LDR, "Track Peak: %f\n", info->track_peak); 364 | utils_dbg(LDR, "Duration: %u\n", info->duration_secs); 365 | } 366 | 367 | return ret; 368 | } -------------------------------------------------------------------------------- /meta_handler.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileType: SOURCE 3 | * 4 | * SPDX-FileCopyrightText: 2017 - 2025 Nick Kossifidis 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * This is a small and very simple HTTP server, that just replies with a JSON 11 | * representation of the current / next audiofile_info of the player, plus 12 | * the elapsed time in seconds of the current song. It's used mainly for 13 | * the station's website, or any other app that wants to know the player's 14 | * current state. 15 | */ 16 | 17 | #define _GNU_SOURCE /* For accept4, TEMP_FAILURE_RETRY */ 18 | #include /* For IP stuff (also brings in socket etc) */ 19 | #include /* For inet_aton */ 20 | #include /* For memset() */ 21 | #include /* For snprintf() */ 22 | #include /* For malloc()/free() */ 23 | #include /* For read/write */ 24 | #include /* For time() */ 25 | #include /* For errno and error codes */ 26 | #include /* For epoll, epoll_event etc */ 27 | #include /* For Socket and options */ 28 | #include /* For inet_ntoa etc */ 29 | #include /* For TCP flags */ 30 | 31 | #include "meta_handler.h" 32 | #include "utils.h" 33 | 34 | /****************\ 35 | * SIGNAL HANDLER * 36 | \****************/ 37 | 38 | static void mh_signal_handler(int signal_number, void *userdata) 39 | { 40 | struct meta_handler *mh = (struct meta_handler*) userdata; 41 | 42 | switch (signal_number) { 43 | case SIGINT: 44 | case SIGTERM: 45 | mh_stop(mh); 46 | break; 47 | default: 48 | break; 49 | } 50 | } 51 | 52 | /*****************\ 53 | * JSON FORMATTING * 54 | \*****************/ 55 | 56 | /* 57 | * We need to make sure that the strings we'll put in the JSON won't break 58 | * the parser, we know that albumid and release_track_id can be used as-is 59 | * since they are just hashes, but for filenames and album/artist/title anything 60 | * is possible except control characters. This unfortunately includes double 61 | * quotes and backslashes, which both break the parser, and although we can 62 | * replace double quotes with single quotes, for backslashes we'll need to 63 | * escape them. This means that we can't work in-place, and pre-allocating 64 | * PATH_MAX or something large in stack for both current and next songs etc 65 | * is an overkill, so we'll go for dynamic allocations. At least we know they 66 | * are properly terminated. 67 | */ 68 | 69 | static void mh_count_special_chars(const char *str, int *backslashes, int *dquotes) { 70 | *backslashes = 0; 71 | *dquotes = 0; 72 | const char *found; 73 | while ((found = strpbrk(str, "\\\"")) != NULL) { 74 | if (*found == '\\') 75 | (*backslashes)++; 76 | else 77 | (*dquotes)++; 78 | str = found + 1; 79 | } 80 | } 81 | 82 | static void mh_replace_inplace(char *str, char orig, char new) { 83 | char *ptr = str; 84 | while ((ptr = strchr(ptr, orig)) != NULL) { 85 | *ptr = new; 86 | ptr++; 87 | } 88 | } 89 | 90 | static char* mh_json_escape_string(char* str, int is_filename) 91 | { 92 | if (!str) 93 | return "(null)"; 94 | 95 | int backslashes = 0; 96 | int dquotes = 0; 97 | 98 | mh_count_special_chars(str, &backslashes, &dquotes); 99 | 100 | /* Nothing to do, use as-is */ 101 | if (!backslashes && !dquotes) 102 | return str; 103 | 104 | /* If string is a filename, we can't go with replacing stuff 105 | * since the filename from the JSON needs to be usable. No 106 | * easy way out of this. */ 107 | if (is_filename) 108 | goto no_replace; 109 | 110 | /* Low-hanging fruit: just replace double quotes 111 | * with single ones in-place */ 112 | if (dquotes) 113 | mh_replace_inplace(str, '"', '\''); 114 | 115 | /* Replace backslash with slash for album/artist */ 116 | if (backslashes) 117 | mh_replace_inplace(str, '\\', '/'); 118 | 119 | return str; 120 | 121 | no_replace: 122 | /* Calculate new length for the escaped string, for each 123 | * character to escape, we need an extra backslash. */ 124 | size_t new_len = strlen(str) + backslashes + dquotes + 1; 125 | char* new_str = malloc(new_len); 126 | if (!new_str) { 127 | utils_perr(META, "couldn't allocate buffer for escaping"); 128 | return "(null)"; 129 | } 130 | 131 | char *src = str; 132 | char *dst = new_str; 133 | char *found; 134 | 135 | /* Copy initial part */ 136 | while ((found = strpbrk(src, "\\\"")) != NULL) { 137 | /* Copy everything up to the special character */ 138 | size_t len = found - src; 139 | memcpy(dst, src, len); 140 | dst += len; 141 | 142 | /* Add escape character and special character */ 143 | *dst++ = '\\'; 144 | *dst++ = *found; 145 | 146 | /* Move src past the special character */ 147 | src = found + 1; 148 | } 149 | 150 | /* Copy remaining part including null terminator */ 151 | strcpy(dst, src); 152 | 153 | return new_str; 154 | } 155 | 156 | static size_t mh_format_json_response(char *buf, size_t bufsize, 157 | const struct audiofile_info *cur, 158 | const struct audiofile_info *next, 159 | uint32_t elapsed) 160 | { 161 | /* Fill the buffer with the response body */ 162 | 163 | /* We always need to operate on the original string or else we'll keep re-escaping */ 164 | char* escaped_curr_filepath = mh_json_escape_string((char*) cur->filepath, 1); 165 | char* escaped_next_filepath = mh_json_escape_string((char*) next->filepath, 1); 166 | 167 | int ret = snprintf(buf, bufsize, 168 | "HTTP/1.1 200 OK\r\n" 169 | "Content-Type: application/json\r\n" 170 | "Connection: close\r\n" 171 | "\r\n" 172 | "{" 173 | "\"current_song\": {" 174 | "\"Artist\": \"%s\"," 175 | "\"Album\": \"%s\"," 176 | "\"Title\": \"%s\"," 177 | "\"Path\": \"%s\"," 178 | "\"Duration\": \"%u\"," 179 | "\"Elapsed\": \"%u\"," 180 | "\"Zone\": \"%s\"," 181 | "\"MusicBrainz Album Id\": \"%s\"," 182 | "\"MusicBrainz Release Track Id\": \"%s\"" 183 | "}," 184 | "\"next_song\": {" 185 | "\"Artist\": \"%s\"," 186 | "\"Album\": \"%s\"," 187 | "\"Title\": \"%s\"," 188 | "\"Path\": \"%s\"," 189 | "\"Duration\": \"%u\"," 190 | "\"Zone\": \"%s\"," 191 | "\"MusicBrainz Album Id\": \"%s\"," 192 | "\"MusicBrainz Release Track Id\": \"%s\"" 193 | "}" 194 | "}\r\n", 195 | mh_json_escape_string(cur->artist, 0), 196 | mh_json_escape_string(cur->album, 0), 197 | mh_json_escape_string(cur->title, 0), 198 | escaped_curr_filepath, 199 | (uint32_t) cur->duration_secs, 200 | elapsed, 201 | cur->zone_name, 202 | cur->albumid, 203 | cur->release_trackid, 204 | mh_json_escape_string(next->artist, 0), 205 | mh_json_escape_string(next->album, 0), 206 | mh_json_escape_string(next->title, 0), 207 | escaped_next_filepath, 208 | (uint32_t) next->duration_secs, 209 | next->zone_name, 210 | next->albumid, 211 | next->release_trackid); 212 | 213 | if (escaped_curr_filepath != cur->filepath) 214 | free(escaped_curr_filepath); 215 | if (escaped_next_filepath != next->filepath) 216 | free(escaped_next_filepath); 217 | 218 | return ret; 219 | } 220 | 221 | 222 | /****************\ 223 | * SERVER ACTIONS * 224 | \****************/ 225 | 226 | static int 227 | mh_create_server_socket(uint16_t port, const char* ip4addr) 228 | { 229 | struct sockaddr_in name = {0}; 230 | int sockfd = 0; 231 | int opt = 1; 232 | int ret = 0; 233 | 234 | /* Create the socket. */ 235 | sockfd = socket(PF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); 236 | if (sockfd < 0) { 237 | utils_perr(META, "Could not create server socket"); 238 | return -errno; 239 | } 240 | 241 | /* Make sure we can re-bind imediately after a quick-restart */ 242 | setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); 243 | 244 | /* We'll only send a small responce so skip Nagle's buffering */ 245 | setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); 246 | 247 | /* We don't expect the client to send more stuff (we won't even 248 | * read them anyway), don't delay ACKs */ 249 | setsockopt(sockfd, IPPROTO_TCP, TCP_QUICKACK, &opt, sizeof(opt)); 250 | 251 | /* Check if it's an ipv4 address */ 252 | if(ip4addr != NULL) { 253 | ret = inet_aton(ip4addr, &name.sin_addr); 254 | if(ret != 0) 255 | return -EINVAL; 256 | } else 257 | name.sin_addr.s_addr = htonl(INADDR_ANY); 258 | 259 | /* Give the socket a name. */ 260 | name.sin_family = AF_INET; 261 | name.sin_port = htons(port); 262 | ret = bind(sockfd, (struct sockaddr *) &name, sizeof (name)); 263 | if (ret < 0) { 264 | utils_perr(META, "Could not bind server socket"); 265 | return -errno; 266 | } 267 | 268 | if (listen(sockfd, SOMAXCONN) < 0) { 269 | utils_perr(META, "Could not make socket passive"); 270 | close(sockfd); 271 | return -EIO; 272 | } 273 | 274 | return sockfd; 275 | } 276 | 277 | static int mh_update_response(struct meta_handler *mh) 278 | { 279 | static struct audiofile_info cur = {0}; 280 | static struct audiofile_info next = {0}; 281 | uint32_t elapsed; 282 | time_t now = time(NULL); 283 | int ret = 0; 284 | 285 | if (!mh->state_cb) 286 | return -1; 287 | 288 | /* Only update once per second */ 289 | if (now == mh->last_update) 290 | return 0; 291 | 292 | pthread_mutex_lock(&mh->update_mutex); 293 | 294 | /* Get elapsed first */ 295 | if (mh->state_cb(NULL, NULL, &elapsed, mh->player_data) < 0) { 296 | ret = -1; 297 | goto cleanup; 298 | } 299 | 300 | /* Get full info if needed */ 301 | if (now > mh->next_update) { 302 | mldr_cleanup_audiofile(&cur); 303 | mldr_cleanup_audiofile(&next); 304 | if (mh->state_cb(&cur, &next, NULL, mh->player_data) < 0) { 305 | ret = -1; 306 | goto cleanup; 307 | } 308 | mh->next_update = now + (cur.duration_secs - elapsed) + 1; 309 | } 310 | 311 | /* Format response */ 312 | mh->response_len = mh_format_json_response(mh->response, 313 | sizeof(mh->response), 314 | &cur, &next, elapsed); 315 | mh->last_update = now; 316 | 317 | cleanup: 318 | pthread_mutex_unlock(&mh->update_mutex); 319 | return ret; 320 | } 321 | 322 | static void mh_handle_client(struct meta_handler *mh, int client_fd) 323 | { 324 | int ret = 0; 325 | 326 | /* Update response if needed */ 327 | if (mh_update_response(mh) < 0) 328 | goto done; 329 | 330 | /* Send response directly, we don't care about the request */ 331 | ret = TEMP_FAILURE_RETRY(send(client_fd, mh->response, mh->response_len, MSG_NOSIGNAL)); 332 | if (ret < 0) { 333 | utils_perr(META, "write failed"); 334 | } else if (ret != mh->response_len) { 335 | utils_wrn(META, "wrote partial response (%i vs %i)\n", 336 | mh->response_len); 337 | } 338 | 339 | done: 340 | /* Send FIN to client, since we won't be 341 | * sending any more data. */ 342 | shutdown(client_fd, SHUT_WR); 343 | 344 | /* Close the connection but don't let unresponsive clients 345 | * keep the socket in TIMED_WAIT sstate for too long. Allow 346 | * them 5 secs and them force a reset. */ 347 | struct linger l = { 348 | .l_onoff = 1, 349 | .l_linger = 5 350 | }; 351 | setsockopt(client_fd, SOL_SOCKET, SO_LINGER, &l, sizeof(l)); 352 | close(client_fd); 353 | } 354 | 355 | static void mh_handle_new_connection(struct meta_handler *mh) 356 | { 357 | struct sockaddr_in clientname = {0}; 358 | socklen_t clientname_sz = sizeof(clientname); 359 | int client_fd; 360 | 361 | while (mh->running && 362 | (client_fd = accept4(mh->listen_fd, 363 | (struct sockaddr *) &clientname, 364 | &clientname_sz, SOCK_NONBLOCK)) > 0) { 365 | 366 | /* Leave this for debugging, under normal operation 367 | * we expect frequent connections so this would polute 368 | * the log otherwise */ 369 | utils_dbg(META, "Connection from host %s at port %i\n", 370 | inet_ntoa(clientname.sin_addr), 371 | ntohs(clientname.sin_port)); 372 | 373 | mh_handle_client(mh, client_fd); 374 | } 375 | } 376 | 377 | 378 | /***************\ 379 | * SERVER THREAD * 380 | \***************/ 381 | 382 | static void *mh_server_thread(void *arg) 383 | { 384 | struct meta_handler *mh = arg; 385 | struct epoll_event events[32]; 386 | int nfds; 387 | 388 | utils_info(META, "Waiting for connections...\n"); 389 | 390 | while (mh->running) { 391 | nfds = epoll_wait(mh->epoll_fd, events, 32, 1000); 392 | if (nfds < 0 && errno != EINTR) 393 | break; 394 | 395 | for (int i = 0; i < nfds; i++) { 396 | if (events[i].data.fd == mh->listen_fd) 397 | mh_handle_new_connection(mh); 398 | } 399 | } 400 | 401 | return NULL; 402 | } 403 | 404 | 405 | /**************\ 406 | * ENTRY POINTS * 407 | \**************/ 408 | 409 | void mh_stop(struct meta_handler *mh) 410 | { 411 | if (!mh) 412 | return; 413 | 414 | utils_dbg(META, "Stopping\n"); 415 | 416 | mh->running = 0; 417 | pthread_join(mh->thread, NULL); 418 | 419 | utils_dbg(META, "Stopped\n"); 420 | } 421 | 422 | void mh_cleanup(struct meta_handler *mh) 423 | { 424 | if (!mh) 425 | return; 426 | 427 | if (mh->listen_fd >= 0) 428 | close(mh->listen_fd); 429 | if (mh->epoll_fd >= 0) 430 | close(mh->epoll_fd); 431 | pthread_mutex_destroy(&mh->update_mutex); 432 | } 433 | 434 | int mh_init(struct meta_handler *mh, uint16_t port, const char* ip4addr, struct sig_dispatcher *sd) 435 | { 436 | struct epoll_event ev; 437 | 438 | memset(mh, 0, sizeof(struct meta_handler)); 439 | 440 | pthread_mutex_init(&mh->update_mutex, NULL); 441 | 442 | mh->epoll_fd = epoll_create1(0); 443 | if (mh->epoll_fd < 0) { 444 | utils_perr(META, "Could not create epoll_fd"); 445 | goto cleanup; 446 | } 447 | 448 | mh->listen_fd = mh_create_server_socket(port, ip4addr); 449 | if (mh->listen_fd < 0) 450 | goto cleanup; 451 | 452 | /* Add listening socket to epoll */ 453 | ev.events = EPOLLIN; 454 | ev.data.fd = mh->listen_fd; 455 | if (epoll_ctl(mh->epoll_fd, EPOLL_CTL_ADD, mh->listen_fd, &ev) < 0) { 456 | utils_perr(META, "epoll_ctl failed"); 457 | goto cleanup; 458 | } 459 | 460 | /* Register with the signal dispatcher */ 461 | sig_dispatcher_register(sd, SIG_UNIT_META, mh_signal_handler, mh); 462 | 463 | utils_dbg(META, "Initialized\n"); 464 | return 0; 465 | 466 | cleanup: 467 | mh_cleanup(mh); 468 | return -1; 469 | } 470 | 471 | int mh_start(struct meta_handler *mh) 472 | { 473 | utils_dbg(META, "Starting\n"); 474 | mh->running = 1; 475 | if (pthread_create(&mh->thread, NULL, mh_server_thread, mh) != 0) { 476 | utils_perr(META, "failed to create server thread\n"); 477 | mh->thread = 0; 478 | return -1; 479 | } 480 | return 0; 481 | } 482 | 483 | int mh_register_state_callback(struct meta_handler *mh, mh_state_cb cb, void *player_data) 484 | { 485 | if (!mh || !cb || !player_data) 486 | return -1; 487 | 488 | mh->state_cb = cb; 489 | mh->player_data = player_data; 490 | 491 | /* Force response update on next request */ 492 | mh->last_update = 0; 493 | 494 | return 0; 495 | } -------------------------------------------------------------------------------- /gst_player.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Audio Scheduler - An audio clip scheduler for use in radio broadcasting 3 | * Crossfade-capable player 4 | * 5 | * Copyright (C) 2017 George Kiagiadakis 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | #include "gst_player.h" 22 | #include "utils.h" 23 | #include 24 | #include 25 | #include /* for memset */ 26 | 27 | static void play_queue_item_set_fade (struct play_queue_item * item, 28 | GstClockTime start, gdouble start_value, GstClockTime end, 29 | gdouble end_value); 30 | static gboolean player_ensure_next (struct player * self); 31 | static gboolean player_recycle_item (struct play_queue_item * item); 32 | static gboolean player_handle_item_eos (struct play_queue_item * item); 33 | 34 | static GstPadProbeReturn 35 | itembin_srcpad_buffer_probe (GstPad * pad, GstPadProbeInfo * info, 36 | struct play_queue_item * item) 37 | { 38 | gint64 duration; 39 | GstEvent *event; 40 | const GstSegment *segment; 41 | GstClockTime fadeout, end; 42 | 43 | if (!gst_pad_query_duration (pad, GST_FORMAT_TIME, &duration) || 44 | duration <= 0) 45 | { 46 | /* try querying again every 20ms; break after 100ms or 47 | if the duration is calculated, whichever comes first */ 48 | gboolean got_duration = FALSE; 49 | gint64 now, start = g_get_monotonic_time (); 50 | do { 51 | g_usleep (20000); /* 20 ms */ 52 | got_duration = gst_pad_query_duration (pad, GST_FORMAT_TIME, &duration); 53 | now = g_get_monotonic_time (); 54 | } while ((!got_duration || duration <= 0) && (now - start) < 100000); 55 | 56 | if (!got_duration || duration <= 0) { 57 | utils_wrn (PLR, "item %p: unknown file duration, consider remuxing; " 58 | "skipping playback: %s\n", item, item->file); 59 | 60 | /* Here we unlink the pad from the audiomixer because letting the buffer 61 | * go in the GstAggregator (parent class of audiomixer) may cause some 62 | * locking on this thread, which will delay freeing this item and may block 63 | * the main thread for significant time. 64 | * As a side-effect, this causes an ERROR GstMessage, which gets posted 65 | * on the bus and we recycle the item from the handler of the message, 66 | * in a similar way we do when another error occurs (for example, when 67 | * a decoder is missing) */ 68 | gst_pad_unlink (pad, item->mixer_sink); 69 | 70 | /* and now get out of here */ 71 | GST_PAD_PROBE_INFO_FLOW_RETURN (info) = GST_FLOW_NOT_LINKED; 72 | return GST_PAD_PROBE_REMOVE; 73 | } 74 | } 75 | 76 | /* schedule fade in */ 77 | if (item->fader.fadein_duration_secs > 0) { 78 | play_queue_item_set_fade (item, 0, 0.0f, 79 | item->fader.fadein_duration_secs * GST_SECOND, 1.0f); 80 | } 81 | 82 | /* schedule fade out */ 83 | if (item->fader.fadeout_duration_secs > 0) { 84 | end = duration; 85 | fadeout = end - item->fader.fadeout_duration_secs * GST_SECOND; 86 | 87 | play_queue_item_set_fade (item, fadeout, 1.0f, 88 | end, 0.0f); 89 | } else { 90 | fadeout = end = duration; 91 | } 92 | 93 | event = gst_pad_get_sticky_event (item->mixer_sink, GST_EVENT_SEGMENT, 0); 94 | gst_event_parse_segment (event, &segment); 95 | 96 | item->duration = duration; 97 | item->fadeout_rt = gst_segment_to_running_time (segment, GST_FORMAT_TIME, 98 | gst_segment_position_from_stream_time (segment, GST_FORMAT_TIME, 99 | fadeout)); 100 | item->end_rt = gst_segment_to_running_time (segment, GST_FORMAT_TIME, 101 | gst_segment_position_from_stream_time (segment, GST_FORMAT_TIME, 102 | end)); 103 | gst_event_unref (event); 104 | 105 | utils_dbg (PLR, "item %p: duration is %" GST_TIME_FORMAT "\n", item, 106 | GST_TIME_ARGS (duration)); 107 | utils_dbg (PLR, "\tfadeout starts at running time: %" GST_TIME_FORMAT "\n", 108 | GST_TIME_ARGS (item->fadeout_rt)); 109 | utils_dbg (PLR, "\titem ends at running time: %" GST_TIME_FORMAT "\n", 110 | GST_TIME_ARGS (item->end_rt)); 111 | 112 | /* make sure we have enough items linked */ 113 | g_idle_add ((GSourceFunc) player_ensure_next, item->player); 114 | 115 | return GST_PAD_PROBE_REMOVE; 116 | } 117 | 118 | static GstPadProbeReturn 119 | mixer_sinkpad_event_probe (GstPad * pad, GstPadProbeInfo * info, 120 | struct play_queue_item * item) 121 | { 122 | GstEvent *event = gst_pad_probe_info_get_event (info); 123 | 124 | switch (GST_EVENT_TYPE (event)) { 125 | case GST_EVENT_EOS: 126 | g_idle_add ((GSourceFunc) player_handle_item_eos, item); 127 | return GST_PAD_PROBE_REMOVE; 128 | 129 | default: 130 | break; 131 | } 132 | 133 | return GST_PAD_PROBE_OK; 134 | } 135 | 136 | static void 137 | decodebin_pad_added (GstElement * decodebin, GstPad * src, GstPad * sink) 138 | { 139 | gst_pad_link (src, sink); 140 | } 141 | 142 | static time_t 143 | calculate_sched_time (GstClockTime start_rt, GstElement * pipeline) 144 | { 145 | gint64 sched_unix_time; 146 | GstClockTime base_time, now = 0; 147 | 148 | /* unless we are to start NOW... */ 149 | if (start_rt != 0) { 150 | GstClock *clock = gst_pipeline_get_clock (GST_PIPELINE (pipeline)); 151 | base_time = gst_element_get_base_time (pipeline); 152 | 153 | if (clock) { 154 | now = gst_clock_get_time (clock) - base_time; 155 | gst_object_unref (clock); 156 | } 157 | 158 | /* if the pipeline is not PLAYING yet and it's using the system clock, 159 | * gst_clock_get_time() will return an incredibly high value, while 160 | * base_time will be 0 and 'now' will end up weird. Just set it to 0 161 | * in this case, as this is the actual time (pipeline not started yet) */ 162 | if (now > start_rt) 163 | now = 0; 164 | } 165 | 166 | /* sched_unix_time is expressed in microseconds since the Epoch */ 167 | sched_unix_time = g_get_real_time () + GST_TIME_AS_USECONDS (start_rt - now); 168 | 169 | utils_dbg (PLR, "calc sched time: start_rt %" GST_TIME_FORMAT 170 | ", now %" GST_TIME_FORMAT ", sched time %" GST_TIME_FORMAT "\n", 171 | GST_TIME_ARGS (start_rt), GST_TIME_ARGS (now), 172 | GST_TIME_ARGS (sched_unix_time * GST_USECOND)); 173 | 174 | /* time_t is in seconds, sched_unix_time is in microseconds */ 175 | return gst_util_uint64_scale (sched_unix_time, GST_USECOND, GST_SECOND); 176 | } 177 | 178 | static struct play_queue_item * 179 | play_queue_item_new (struct player * self, struct play_queue_item * previous) 180 | { 181 | struct play_queue_item *item; 182 | const struct fader_info *fader; 183 | gchar *uri; 184 | GError *error = NULL; 185 | time_t sched_time; 186 | GstElement *decodebin; 187 | GstElement *audioconvert; 188 | GstPad *convert_src, *convert_sink; 189 | GstPad *ghost; 190 | GstClockTime offset = 0; 191 | struct audiofile_info next_info = {0}; 192 | 193 | /* ask for the item that would start exactly at the end of the previous item; 194 | * note that in reality this item may start earlier than the requested time, 195 | * if it has a fade in, but at this point we don't really care */ 196 | sched_time = calculate_sched_time (previous ? previous->end_rt : 0, 197 | self->pipeline); 198 | 199 | next: 200 | /* ask scheduler for the next item */ 201 | if (sched_get_next (self->scheduler, sched_time, &next_info) != 0) { 202 | utils_err (PLR, "No more files to play!!\n"); 203 | return NULL; 204 | } 205 | fader = next_info.fader_info; 206 | 207 | /* convert to file:// URI */ 208 | uri = gst_filename_to_uri (next_info.filepath, &error); 209 | if (error) { 210 | utils_wrn (PLR, "Failed to convert filename '%s' to URI: %s\n", next_info.filepath, 211 | error->message); 212 | g_clear_error (&error); 213 | goto next; 214 | } 215 | 216 | item = g_new0 (struct play_queue_item, 1); 217 | item->player = self; 218 | item->previous = previous; 219 | item->file = g_strdup (next_info.filepath); 220 | mldr_cleanup_audiofile(&next_info); 221 | 222 | utils_dbg (PLR, "item %p: scheduling to play '%s'\n", item, uri); 223 | 224 | /* configure fade properties */ 225 | if (fader) { 226 | item->fader = *fader; 227 | } else { 228 | item->fader.fadein_duration_secs = 0; 229 | item->fader.fadeout_duration_secs = 0; 230 | } 231 | 232 | item->bin = gst_bin_new (NULL); 233 | gst_bin_add (GST_BIN (self->pipeline), item->bin); 234 | 235 | /* create the decodebin and link it */ 236 | decodebin = gst_element_factory_make ("uridecodebin", NULL); 237 | gst_util_set_object_arg (G_OBJECT (decodebin), "caps", "audio/x-raw"); 238 | g_object_set (decodebin, 239 | "uri", uri, 240 | "use-buffering", TRUE, 241 | "buffer-size", 0, /* disable limiting the buffer by size */ 242 | "buffer-duration", 0, /* disable limiting the buffer by duration */ 243 | NULL); 244 | gst_bin_add (GST_BIN (item->bin), decodebin); 245 | 246 | /* plug audioconvert in between; 247 | * audiomixer cannot handle different formats on different sink pads */ 248 | audioconvert = gst_parse_bin_from_description ( 249 | "audioconvert ! audioresample ! rgvolume", TRUE, NULL); 250 | gst_bin_add (GST_BIN (item->bin), audioconvert); 251 | 252 | /* link the audioconvert bin's src pad to the audiomixer's sink */ 253 | item->mixer_sink = gst_element_request_pad_simple (self->mixer, "sink_%u"); 254 | 255 | convert_src = gst_element_get_static_pad (audioconvert, "src"); 256 | ghost = gst_ghost_pad_new ("src", convert_src); 257 | gst_pad_set_active (ghost, TRUE); 258 | gst_element_add_pad (item->bin, ghost); 259 | gst_pad_link (ghost, item->mixer_sink); 260 | gst_object_unref (convert_src); 261 | 262 | /* and the decodebin's src pad to the audioconvert bin's sink */ 263 | convert_sink = gst_element_get_static_pad (audioconvert, "sink"); 264 | g_signal_connect_object (decodebin, "pad-added", 265 | (GCallback) decodebin_pad_added, convert_sink, 0); 266 | gst_object_unref (convert_sink); 267 | 268 | /* add probes */ 269 | gst_pad_add_probe (ghost, 270 | GST_PAD_PROBE_TYPE_BUFFER | GST_PAD_PROBE_TYPE_BLOCK, 271 | (GstPadProbeCallback) itembin_srcpad_buffer_probe, item, NULL); 272 | gst_pad_add_probe (item->mixer_sink, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM, 273 | (GstPadProbeCallback) mixer_sinkpad_event_probe, item, NULL); 274 | 275 | /* start mixing this stream in the future; if there is a fade in, 276 | * start at the time the previous stream starts fading out, 277 | * otherwise start at the end of the previous stream */ 278 | if (item->previous && item->previous->end_rt > 0) { 279 | if (item->fader.fadein_duration_secs > 0) 280 | offset = item->previous->fadeout_rt; 281 | else 282 | offset = item->previous->end_rt; 283 | 284 | gst_pad_set_offset (item->mixer_sink, offset); 285 | } 286 | item->start_rt = offset; 287 | 288 | gst_element_sync_state_with_parent (item->bin); 289 | 290 | utils_dbg (PLR, "item %p: created, linked to %s:%s, start_rt: %" 291 | GST_TIME_FORMAT "\n", item, GST_DEBUG_PAD_NAME (item->mixer_sink), 292 | GST_TIME_ARGS (item->start_rt)); 293 | 294 | g_free (uri); 295 | return item; 296 | } 297 | 298 | static void 299 | play_queue_item_free (struct play_queue_item * item) 300 | { 301 | utils_dbg (PLR, "item %p: freeing item\n", item); 302 | 303 | g_free (item->file); 304 | 305 | gst_element_set_locked_state (item->bin, TRUE); 306 | gst_element_set_state (item->bin, GST_STATE_NULL); 307 | gst_bin_remove (GST_BIN (item->player->pipeline), item->bin); 308 | 309 | if (item->mixer_sink) { 310 | gst_element_release_request_pad (item->player->mixer, item->mixer_sink); 311 | gst_object_unref (item->mixer_sink); 312 | } 313 | 314 | g_free (item); 315 | } 316 | 317 | static void 318 | play_queue_item_set_fade (struct play_queue_item * item, 319 | GstClockTime start, gdouble start_value, GstClockTime end, 320 | gdouble end_value) 321 | { 322 | GstControlBinding *binding; 323 | GstControlSource *cs; 324 | GstTimedValueControlSource *tvcs; 325 | 326 | utils_dbg (PLR, "item %p: scheduling fade from %lf (@ %" GST_TIME_FORMAT ") " 327 | "to %lf (@ %" GST_TIME_FORMAT ")\n", item, 328 | start_value, GST_TIME_ARGS (start), end_value, GST_TIME_ARGS (end)); 329 | 330 | cs = gst_interpolation_control_source_new (); 331 | tvcs = GST_TIMED_VALUE_CONTROL_SOURCE (cs); 332 | g_object_set (cs, "mode", GST_INTERPOLATION_MODE_LINEAR, NULL); 333 | gst_timed_value_control_source_set (tvcs, start, start_value); 334 | gst_timed_value_control_source_set (tvcs, end, end_value); 335 | 336 | binding = gst_direct_control_binding_new_absolute ( 337 | GST_OBJECT_CAST (item->mixer_sink), "volume", cs); 338 | gst_object_add_control_binding (GST_OBJECT_CAST (item->mixer_sink), 339 | binding); 340 | gst_object_unref (cs); 341 | } 342 | 343 | static gboolean 344 | player_ensure_next (struct player * self) 345 | { 346 | if (!self->playlist->next) 347 | self->playlist->next = play_queue_item_new (self, self->playlist); 348 | return G_SOURCE_REMOVE; 349 | } 350 | 351 | static gboolean 352 | player_recycle_item (struct play_queue_item * item) 353 | { 354 | struct player * self = item->player; 355 | struct play_queue_item ** ptr; 356 | 357 | utils_dbg (PLR, "recycling item %p\n", item); 358 | 359 | /* this can happen when the very first loaded item fails to play 360 | * and we want to recycle it, otherwise normally it's the ->next 361 | * item that we recycle */ 362 | if (G_UNLIKELY (item == self->playlist)) 363 | ptr = &self->playlist; 364 | else 365 | ptr = &self->playlist->next; 366 | 367 | g_assert (*ptr == item); 368 | g_assert (item->next == NULL); 369 | 370 | *ptr = play_queue_item_new (self, item->previous); 371 | 372 | play_queue_item_free (item); 373 | return G_SOURCE_REMOVE; 374 | } 375 | 376 | static gboolean 377 | player_handle_item_eos (struct play_queue_item * item) 378 | { 379 | struct player * self = item->player; 380 | 381 | utils_dbg (PLR, "item %p EOS\n", item); 382 | 383 | if (G_UNLIKELY (item == self->playlist->next)) { 384 | utils_wrn (PLR, "next item finished before the current; corrupt file?\n"); 385 | player_recycle_item (item); 386 | return G_SOURCE_REMOVE; 387 | } 388 | 389 | g_assert (item == self->playlist); 390 | 391 | self->playlist = self->playlist->next; 392 | player_ensure_next (self); 393 | 394 | play_queue_item_free (item); 395 | return G_SOURCE_REMOVE; 396 | } 397 | 398 | static gboolean 399 | player_bus_watch (GstBus *bus, GstMessage *msg, struct player *self) 400 | { 401 | GError *error; 402 | gchar *debug = NULL; 403 | 404 | switch (GST_MESSAGE_TYPE (msg)) { 405 | case GST_MESSAGE_EOS: 406 | /* EOS can only be received when audiomixer receives GST_EVENT_EOS 407 | * on a sink and has no other sink with more data available at that 408 | * moment, which can only happen if the scheduler stopped giving 409 | * us new files to enqueue */ 410 | utils_info (PLR, "we got EOS, which means there is no file " 411 | "in the play queue; exiting...\n"); 412 | g_main_loop_quit (self->loop); 413 | break; 414 | 415 | case GST_MESSAGE_INFO: 416 | gst_message_parse_info (msg, &error, &debug); 417 | utils_info (PLR, "INFO from element %s: %s\n", 418 | GST_OBJECT_NAME (GST_MESSAGE_SRC (msg)), 419 | error->message); 420 | g_clear_error (&error); 421 | if (debug) { 422 | utils_info (PLR, "INFO debug message: %s\n", debug); 423 | g_free (debug); 424 | } 425 | break; 426 | 427 | case GST_MESSAGE_WARNING: 428 | gst_message_parse_warning (msg, &error, &debug); 429 | utils_wrn (PLR, "WARNING from element %s: %s\n", 430 | GST_OBJECT_NAME (GST_MESSAGE_SRC (msg)), 431 | error->message); 432 | g_clear_error (&error); 433 | if (debug) { 434 | utils_wrn (PLR, "WARNING debug message: %s\n", debug); 435 | g_free (debug); 436 | } 437 | break; 438 | 439 | case GST_MESSAGE_ERROR: 440 | { 441 | struct play_queue_item *item = self->playlist->next ? 442 | self->playlist->next : self->playlist; 443 | 444 | gst_message_parse_error (msg, &error, &debug); 445 | utils_wrn (PLR, "ERROR from element %s: %s\n", 446 | GST_OBJECT_NAME (GST_MESSAGE_SRC (msg)), 447 | error->message); 448 | g_clear_error (&error); 449 | if (debug) { 450 | utils_wrn (PLR, "ERROR debug message: %s\n", debug); 451 | g_free (debug); 452 | } 453 | 454 | /* check if the message came from an item's bin and attempt to recover; 455 | * it is possible to get an error there, in case of an unsupported 456 | * codec for example, or maybe a file read error... */ 457 | if (gst_object_has_as_ancestor (GST_MESSAGE_SRC (msg), 458 | GST_OBJECT (item->bin))) { 459 | /* 460 | * this is the last item in the queue; for this case we can recover 461 | * by calling the recycle function 462 | */ 463 | utils_info (PLR, "error message originated from the next " 464 | "item's bin; recycling item\n"); 465 | 466 | player_recycle_item (item); 467 | 468 | /* ensure the pipeline is PLAYING state; 469 | * error messages tamper with it */ 470 | gst_element_set_state (self->pipeline, GST_STATE_PLAYING); 471 | 472 | } else if (self->playlist->next && gst_object_has_as_ancestor ( 473 | GST_MESSAGE_SRC (msg), 474 | GST_OBJECT (self->playlist->bin))) { 475 | /* 476 | * this is the decodebin of the currently playing item, but we 477 | * have already linked the next item; no graceful recover here... 478 | * we need to get rid of the next item, then recycle the current one; 479 | * there *will* be an audio glitch here. 480 | */ 481 | utils_info (PLR, "error message originated from the current " 482 | "item's bin; recycling the whole playlist\n"); 483 | 484 | play_queue_item_free (self->playlist->next); 485 | self->playlist->next = NULL; 486 | player_recycle_item (self->playlist); 487 | 488 | /* ensure the pipeline is PLAYING state; 489 | * error messages tamper with it */ 490 | gst_element_set_state (self->pipeline, GST_STATE_PLAYING); 491 | 492 | } else if (!gst_object_has_as_ancestor (GST_MESSAGE_SRC (msg), 493 | GST_OBJECT (self->pipeline))) { 494 | /* 495 | * this is an element that we have already removed from the pipeline. 496 | * this can happen for example when a decodebin posts 2 errors in a row 497 | */ 498 | utils_info (PLR, "error message originated from already removed item; " 499 | "ignoring\n"); 500 | 501 | } else { 502 | utils_err (PLR, "error originated from a critical element; " 503 | "the pipeline cannot continue working, sorry!\n"); 504 | g_main_loop_quit (self->loop); 505 | } 506 | 507 | break; 508 | } 509 | default: 510 | break; 511 | } 512 | 513 | return G_SOURCE_CONTINUE; 514 | } 515 | 516 | /* this function replaces all occurencies of the double quote character (") 517 | * with a single quote character (') in order to allow the string to be used 518 | * as a JSON value without the need for escaping */ 519 | static inline void 520 | string_replace_quotes (char * str) 521 | { 522 | char *c; 523 | if (str != NULL) 524 | for (c = str; *c != '\0'; c++) 525 | if (*c == '"') 526 | *c = '\''; 527 | } 528 | 529 | static void 530 | populate_song_info (struct play_queue_item * item, struct song_info * song) 531 | { 532 | GstEvent *tag_event; 533 | GstTagList *taglist = NULL; 534 | gint64 pos = GST_CLOCK_TIME_NONE; 535 | 536 | /* cleanup song_info */ 537 | g_clear_pointer (&song->artist, g_free); 538 | g_clear_pointer (&song->album, g_free); 539 | g_clear_pointer (&song->title, g_free); 540 | g_clear_pointer (&song->path, g_free); 541 | song->duration_sec = song->elapsed_sec = 0; 542 | 543 | if (!item) 544 | return; 545 | 546 | song->path = g_strdup (item->file); 547 | string_replace_quotes (song->path); 548 | 549 | tag_event = gst_pad_get_sticky_event (item->mixer_sink, GST_EVENT_TAG, 0); 550 | if (tag_event) 551 | gst_event_parse_tag (tag_event, &taglist); 552 | 553 | if (taglist) { 554 | gst_tag_list_get_string (taglist, GST_TAG_ARTIST, &song->artist); 555 | string_replace_quotes (song->artist); 556 | gst_tag_list_get_string (taglist, GST_TAG_ALBUM, &song->album); 557 | string_replace_quotes (song->album); 558 | gst_tag_list_get_string (taglist, GST_TAG_TITLE, &song->title); 559 | string_replace_quotes (song->title); 560 | } 561 | 562 | if (gst_pad_peer_query_position (item->mixer_sink, GST_FORMAT_TIME, &pos)) 563 | song->elapsed_sec = 564 | (uint32_t) gst_util_uint64_scale_round (pos, 1, GST_SECOND); 565 | song->duration_sec = 566 | (uint32_t) gst_util_uint64_scale_round (item->duration, 1, GST_SECOND); 567 | 568 | if (tag_event) 569 | gst_event_unref (tag_event); 570 | } 571 | 572 | static gboolean 573 | refresh_metadata (struct player * self) 574 | { 575 | struct current_state *mstate; 576 | 577 | mstate = meta_get_state (self->mh); 578 | pthread_mutex_lock (&mstate->proc_mutex); 579 | 580 | populate_song_info (self->playlist, &mstate->current); 581 | populate_song_info (self->playlist->next, &mstate->next); 582 | 583 | if (self->playlist->next && 584 | self->playlist->next->fader.fadein_duration_secs > 0) 585 | mstate->overlap_sec = self->playlist->fader.fadeout_duration_secs; 586 | else 587 | mstate->overlap_sec = 0; 588 | 589 | pthread_mutex_unlock (&mstate->proc_mutex); 590 | 591 | return G_SOURCE_CONTINUE; 592 | } 593 | 594 | static void 595 | cleanup_metadata (struct meta_handler *mh) 596 | { 597 | struct current_state *mstate; 598 | 599 | mstate = meta_get_state (mh); 600 | pthread_mutex_lock (&mstate->proc_mutex); 601 | 602 | populate_song_info (NULL, &mstate->current); 603 | populate_song_info (NULL, &mstate->next); 604 | mstate->overlap_sec = 0; 605 | 606 | pthread_mutex_unlock (&mstate->proc_mutex); 607 | } 608 | 609 | int 610 | gst_player_init (struct player* self, struct scheduler* scheduler, 611 | struct meta_handler *mh, const char *audiosink) 612 | { 613 | GstElement *sink = NULL; 614 | GstElement *convert = NULL; 615 | 616 | gst_init (NULL, NULL); 617 | 618 | self->scheduler = scheduler; 619 | self->mh = mh; 620 | self->loop = g_main_loop_new (NULL, FALSE); 621 | self->pipeline = gst_pipeline_new ("player"); 622 | self->mixer = gst_element_factory_make ("audiomixer", NULL); 623 | convert = gst_element_factory_make ("audioconvert", NULL); 624 | if (audiosink) { 625 | GError *error = NULL; 626 | sink = gst_parse_bin_from_description (audiosink, TRUE, &error); 627 | if (error) 628 | utils_wrn (PLR, "Failed to parse audiosink description: %s\n", 629 | error->message); 630 | g_clear_error (&error); 631 | } 632 | 633 | if (!sink) 634 | sink = gst_element_factory_make ("autoaudiosink", NULL); 635 | 636 | if (!self->mixer || !convert || !sink) { 637 | utils_err (PLR, "Your GStreamer installation is missing required elements\n"); 638 | g_clear_object (&self->mixer); 639 | g_clear_object (&convert); 640 | g_clear_object (&sink); 641 | return -1; 642 | } 643 | 644 | gst_bin_add_many (GST_BIN (self->pipeline), self->mixer, convert, sink, NULL); 645 | if (!gst_element_link_many (self->mixer, convert, sink, NULL)) { 646 | utils_err (PLR, "Failed to link audiomixer to audio sink. Check caps\n"); 647 | return -1; 648 | } 649 | 650 | utils_dbg (PLR, "player initialized\n"); 651 | 652 | return 0; 653 | } 654 | 655 | void 656 | gst_player_cleanup (struct player* self) 657 | { 658 | g_clear_object (&self->pipeline); 659 | g_clear_pointer (&self->loop, g_main_loop_unref); 660 | 661 | memset (self, 0, sizeof (struct player)); 662 | 663 | utils_dbg (PLR, "player destroyed\n"); 664 | } 665 | 666 | void 667 | gst_player_loop (struct player* self) 668 | { 669 | GstBus *bus; 670 | guint timeout_id; 671 | 672 | self->playlist = play_queue_item_new (self, NULL); 673 | 674 | bus = gst_pipeline_get_bus (GST_PIPELINE (self->pipeline)); 675 | gst_bus_add_watch (bus, (GstBusFunc) player_bus_watch, self); 676 | 677 | timeout_id = g_timeout_add_seconds (1, (GSourceFunc) refresh_metadata, self); 678 | 679 | utils_dbg (PLR, "Beginning playback\n"); 680 | gst_element_set_state (self->pipeline, GST_STATE_PLAYING); 681 | 682 | g_main_loop_run (self->loop); 683 | 684 | gst_element_set_state (self->pipeline, GST_STATE_NULL); 685 | utils_dbg (PLR, "Playback stopped\n"); 686 | 687 | g_source_remove (timeout_id); 688 | cleanup_metadata (self->mh); 689 | 690 | gst_bus_remove_watch (bus); 691 | g_object_unref (bus); 692 | 693 | if (self->playlist->next) 694 | play_queue_item_free (self->playlist->next); 695 | play_queue_item_free (self->playlist); 696 | } 697 | 698 | void 699 | gst_player_loop_quit (struct player* self) 700 | { 701 | g_main_loop_quit (self->loop); 702 | } 703 | -------------------------------------------------------------------------------- /cfg_handler.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileType: SOURCE 3 | * 4 | * SPDX-FileCopyrightText: 2016 - 2025 Nick Kossifidis 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * Configuration data handler, this is the part that parses the XML schedule of 11 | * the week, that includes days -> zones -> playlists. 12 | */ 13 | 14 | #define _XOPEN_SOURCE /* Needed for strptime() */ 15 | #include /* For memset() / strncmp() */ 16 | #include /* For strptime() and time() */ 17 | #include /* For sig_atomic_t */ 18 | #include /* For strtof(), malloc/realloc/free() */ 19 | #include /* For parser context etc */ 20 | #include /* For grabbing stuff off the tree */ 21 | #include /* For validation context etc */ 22 | #include /* For schema context etc */ 23 | #include "scheduler.h" 24 | #include "utils.h" 25 | 26 | static volatile sig_atomic_t parser_failed = 0; 27 | 28 | 29 | /*********\ 30 | * HELPERS * 31 | \*********/ 32 | 33 | static char* 34 | cfg_get_string(xmlDocPtr config, xmlNodePtr element) 35 | { 36 | char* value = (char*) xmlNodeListGetString(config, 37 | element->xmlChildrenNode, 1); 38 | if(value == NULL) 39 | parser_failed = 1; 40 | 41 | utils_trim_string(value); 42 | 43 | utils_dbg(CFG, "Got string: %s\n", value); 44 | return value; 45 | } 46 | 47 | static int 48 | cfg_get_integer(xmlDocPtr config, xmlNodePtr element) 49 | { 50 | int ret = 0; 51 | char* value = cfg_get_string(config, element); 52 | if(parser_failed) 53 | return -1; 54 | 55 | ret = atoi(value); 56 | 57 | xmlFree(value); 58 | utils_dbg(CFG, "Got integer: %i\n", ret); 59 | return ret; 60 | } 61 | 62 | static int 63 | cfg_get_boolean(xmlDocPtr config, xmlNodePtr element) 64 | { 65 | int ret = 0; 66 | char* value = cfg_get_string(config, element); 67 | if(parser_failed) 68 | return -1; 69 | 70 | if (!strncmp(value, "true", 5)) 71 | ret = 1; 72 | else 73 | ret = 0; 74 | 75 | xmlFree(value); 76 | utils_dbg(CFG, "Got boolean: %s\n", ret ? "true" : "false"); 77 | return ret; 78 | } 79 | 80 | static char* 81 | cfg_get_str_attr(xmlNodePtr element, const char* attr) 82 | { 83 | char* value = (char*) xmlGetProp(element, (const xmlChar*) attr); 84 | if(value == NULL) { 85 | parser_failed = 1; 86 | return NULL; 87 | } 88 | utils_trim_string(value); 89 | return value; 90 | } 91 | 92 | static void 93 | cfg_get_start_attr(xmlDocPtr config, xmlNodePtr element, struct tm *time) 94 | { 95 | char* time_string = (char*) xmlGetProp(element, (const xmlChar*) "Start"); 96 | if(time_string == NULL) { 97 | parser_failed = 1; 98 | return; 99 | } 100 | utils_trim_string(time_string); 101 | strptime(time_string, "%T", time); 102 | utils_dbg(CFG, "Got start time: %s\n", time_string); 103 | xmlFree((xmlChar*) time_string); 104 | } 105 | 106 | 107 | /****************\ 108 | * FADER HANDLING * 109 | \****************/ 110 | 111 | static void 112 | cfg_free_fader(struct fader_info *fdr) 113 | { 114 | free(fdr); 115 | } 116 | 117 | static struct fader_info* 118 | cfg_get_fader(xmlDocPtr config, xmlNodePtr fdr_node) 119 | { 120 | struct fader_info *fdr = NULL; 121 | xmlNodePtr element = NULL; 122 | int failed = 0; 123 | 124 | if(parser_failed) 125 | return NULL; 126 | 127 | /* Allocate a new fader structure and 128 | * zero it out */ 129 | fdr = (struct fader_info*) malloc(sizeof(struct fader_info)); 130 | if (!fdr) { 131 | utils_err(CFG, "Unable to allocate fader structure !\n"); 132 | parser_failed = 1; 133 | return NULL; 134 | } 135 | memset(fdr, 0, sizeof(struct fader_info)); 136 | 137 | /* Fill it up */ 138 | element = fdr_node->xmlChildrenNode; 139 | while (element != NULL) { 140 | if(!strncmp((const char*) element->name, "FadeInDurationSecs", 19)) 141 | fdr->fadein_duration_secs = cfg_get_integer(config, element); 142 | else if(!strncmp((const char*) element->name, "FadeOutDurationSecs", 20)) 143 | fdr->fadeout_duration_secs = cfg_get_integer(config, element); 144 | if(parser_failed) { 145 | utils_err(CFG, "Parsing of fader element failed\n"); 146 | parser_failed = 1; 147 | goto cleanup; 148 | } 149 | element = element->next; 150 | } 151 | 152 | /* Sanity check, at least one duration field needs to be set, 153 | * note that fader is an optional element so failure here 154 | * should not be fatal */ 155 | if(!fdr->fadein_duration_secs || !fdr->fadeout_duration_secs) { 156 | utils_wrn(CFG, "Got empty fader element\n"); 157 | failed = 1; 158 | goto cleanup; 159 | } 160 | 161 | utils_dbg(CFG, "Got fader\n\tFade in duration (secs): %i\n\t" 162 | "Fade out duration (secs): %i\n\t", 163 | fdr->fadein_duration_secs, fdr->fadeout_duration_secs); 164 | 165 | cleanup: 166 | if(parser_failed || failed) { 167 | cfg_free_fader(fdr); 168 | fdr = NULL; 169 | } 170 | return fdr; 171 | } 172 | 173 | /*******************\ 174 | * PLAYLIST HANDLING * 175 | \*******************/ 176 | 177 | static void 178 | cfg_free_pls(struct playlist *pls) 179 | { 180 | if(!pls) 181 | return; 182 | 183 | if(pls->filepath) { 184 | xmlFree((xmlChar*) pls->filepath); 185 | pls->filepath = NULL; 186 | } 187 | 188 | if(pls->fader) { 189 | cfg_free_fader(pls->fader); 190 | pls->fader = NULL; 191 | } 192 | 193 | pls_files_cleanup(pls); 194 | 195 | free(pls); 196 | } 197 | 198 | static struct playlist* 199 | cfg_get_pls(xmlDocPtr config, xmlNodePtr pls_node) 200 | { 201 | struct playlist *pls = NULL; 202 | xmlNodePtr element = NULL; 203 | int ret = 0; 204 | 205 | if(parser_failed) 206 | return NULL; 207 | 208 | /* Allocate a new playlist structure and 209 | * zero it out */ 210 | pls = (struct playlist*) malloc(sizeof(struct playlist)); 211 | if (!pls) { 212 | utils_err(CFG, "Unable to allocate playlist structure !\n"); 213 | parser_failed = 1; 214 | return NULL; 215 | } 216 | memset(pls, 0, sizeof(struct playlist)); 217 | 218 | /* Fill it up */ 219 | element = pls_node->xmlChildrenNode; 220 | while (element != NULL) { 221 | if(!strncmp((const char*) element->name, "Path", 5)) 222 | pls->filepath = cfg_get_string(config, element); 223 | else if(!strncmp((const char*) element->name, "Shuffle", 8)) 224 | pls->shuffle = cfg_get_boolean(config, element); 225 | else if(!strncmp((const char*) element->name, "Fader", 5)) 226 | pls->fader = cfg_get_fader(config, element); 227 | if(parser_failed) { 228 | utils_err(CFG, "Parsing of playlist element failed\n"); 229 | goto cleanup; 230 | } 231 | element = element->next; 232 | } 233 | 234 | /* Sanity check, note that fade duration is optional */ 235 | if(!pls->filepath) { 236 | utils_err(CFG, "Filepath missing from playlist element\n"); 237 | parser_failed = 1; 238 | goto cleanup; 239 | } 240 | 241 | /* Fill up the items array */ 242 | ret = pls_process(pls); 243 | if(ret < 0) { 244 | utils_err(CFG, "Got empty/malformed playlist: %s\n", pls->filepath); 245 | parser_failed = 1; 246 | goto cleanup; 247 | } 248 | 249 | utils_dbg(CFG, "Got playlist: %s\n\tShuffle: %s\n\tFader: %s\n", 250 | pls->filepath, pls->shuffle ? "true" : "false", 251 | pls->fader ? "true" : "false"); 252 | 253 | cleanup: 254 | if(parser_failed) { 255 | cfg_free_pls(pls); 256 | pls = NULL; 257 | /* Not able to parse a fallback playlist is non-fatal */ 258 | if(!strncmp((const char*) pls_node->name, "Fallback", 9)) { 259 | utils_wrn(CFG, "ignoring empty/malformed fallback playlist\n"); 260 | parser_failed = 0; 261 | } 262 | } 263 | return pls; 264 | } 265 | 266 | 267 | /********************************\ 268 | * INTERMEDIATE PLAYLIST HANDLING * 269 | \********************************/ 270 | 271 | static void 272 | cfg_free_ipls(struct intermediate_playlist *ipls) 273 | { 274 | if(!ipls) 275 | return; 276 | 277 | if(ipls->name) { 278 | xmlFree((xmlChar*) ipls->name); 279 | ipls->name = NULL; 280 | } 281 | if(ipls->filepath) { 282 | xmlFree((xmlChar*) ipls->filepath); 283 | ipls->filepath = NULL; 284 | } 285 | if(ipls->fader) { 286 | cfg_free_fader(ipls->fader); 287 | ipls->fader = NULL; 288 | } 289 | 290 | pls_files_cleanup((struct playlist*) ipls); 291 | 292 | free(ipls); 293 | } 294 | 295 | static struct intermediate_playlist* 296 | cfg_get_ipls(xmlDocPtr config, xmlNodePtr ipls_node) 297 | { 298 | struct intermediate_playlist *ipls = NULL; 299 | time_t curr_time = time(NULL); 300 | xmlNodePtr element = NULL; 301 | int ret = 0; 302 | 303 | if(parser_failed) 304 | return NULL; 305 | 306 | /* Allocate a new intermediate playlist structure 307 | * and zero it out */ 308 | ipls = (struct intermediate_playlist*) 309 | malloc(sizeof(struct intermediate_playlist)); 310 | if (!ipls) { 311 | utils_err(CFG, "Unable to allocate intermediate playlist !\n"); 312 | parser_failed = 1; 313 | return NULL; 314 | } 315 | memset(ipls, 0, sizeof(struct intermediate_playlist)); 316 | 317 | /* Name attribute is mandatory */ 318 | ipls->name = cfg_get_str_attr(ipls_node, "Name"); 319 | if(parser_failed) { 320 | utils_err(CFG, "Could not get name atrribute" 321 | " for an intermediate playlist\n"); 322 | goto cleanup; 323 | } 324 | 325 | /* Fill it up */ 326 | element = ipls_node->xmlChildrenNode; 327 | while (element != NULL) { 328 | if(!strncmp((const char*) element->name, "Path", 5)) 329 | ipls->filepath = cfg_get_string(config, element); 330 | else if(!strncmp((const char*) element->name, "Shuffle", 8)) 331 | ipls->shuffle = cfg_get_boolean(config, element); 332 | else if(!strncmp((const char*) element->name, "Fader", 5)) 333 | ipls->fader = cfg_get_fader(config, element); 334 | else if(!strncmp((const char*) element->name, "SchedIntervalMins", 18)) 335 | ipls->sched_interval_mins = cfg_get_integer(config, element); 336 | else if(!strncmp((const char*) element->name, "NumSchedItems", 14)) 337 | ipls->num_sched_items = cfg_get_integer(config, element); 338 | if(parser_failed) { 339 | utils_err(CFG, "Parsing of intermediate playlist %s failed\n", 340 | ipls->name); 341 | goto cleanup; 342 | } 343 | element = element->next; 344 | } 345 | 346 | /* Sanity check, note that fade duration is optional */ 347 | if(!ipls->filepath) { 348 | utils_err(CFG, "Filepath missing from %s\n", ipls->name); 349 | parser_failed = 1; 350 | goto cleanup; 351 | } 352 | 353 | if(!ipls->sched_interval_mins) { 354 | utils_err(CFG, "No scheduling interval set for %s\n", ipls->name); 355 | parser_failed = 1; 356 | goto cleanup; 357 | } 358 | 359 | if(!ipls->num_sched_items) { 360 | utils_err(CFG, "Number of items to be scheduled set to 0 for %s\n", 361 | ipls->name); 362 | parser_failed = 1; 363 | goto cleanup; 364 | } 365 | 366 | /* Fill up the items array */ 367 | ret = pls_process((struct playlist*) ipls); 368 | if(ret < 0) { 369 | utils_err(CFG, "Got empty/malformed playlist: %s\n", ipls->filepath); 370 | parser_failed = 1; 371 | goto cleanup; 372 | } 373 | 374 | /* Initialize ipls by setting sched_items_pending and last_scheduled */ 375 | ipls->sched_items_pending = ipls->num_sched_items; 376 | ipls->last_scheduled = curr_time; 377 | 378 | utils_dbg(CFG, "Got intermediate playlist: %s\n\tFile:%s\n\tShuffle: %s\n\t", 379 | ipls->name, ipls->filepath, ipls->shuffle ? "true" : "false"); 380 | 381 | utils_dbg(CFG|SKIP, "Fader: %s\n\tScheduling interval: %i\n\t" 382 | "Items to schedule: %i\n", ipls->fader ? "true" : "false", 383 | ipls->sched_interval_mins, ipls->num_sched_items); 384 | 385 | cleanup: 386 | if(parser_failed) { 387 | cfg_free_ipls(ipls); 388 | ipls = NULL; 389 | /* Not able to parse an intermediate playlist is non-fatal */ 390 | utils_wrn(CFG, "ignoring empty/malformed intermediate playlist\n"); 391 | parser_failed = 0; 392 | } 393 | return ipls; 394 | } 395 | 396 | 397 | /***************\ 398 | * ZONE HANDLING * 399 | \***************/ 400 | 401 | static void 402 | cfg_free_zone(struct zone *zone) 403 | { 404 | int i = 0; 405 | 406 | if(!zone) 407 | return; 408 | 409 | if(zone->name) { 410 | xmlFree((xmlChar*) zone->name); 411 | zone->name = NULL; 412 | } 413 | if(zone->maintainer) { 414 | xmlFree((xmlChar*) zone->maintainer); 415 | zone->maintainer = NULL; 416 | } 417 | if(zone->description) { 418 | xmlFree((xmlChar*) zone->description); 419 | zone->description = NULL; 420 | } 421 | if(zone->comment) { 422 | xmlFree((xmlChar*) zone->comment); 423 | zone->comment = NULL; 424 | } 425 | if(zone->main_pls) { 426 | cfg_free_pls(zone->main_pls); 427 | zone->main_pls = NULL; 428 | } 429 | if(zone->fallback_pls) { 430 | cfg_free_pls(zone->fallback_pls); 431 | zone->fallback_pls = NULL; 432 | } 433 | if (zone->others) { 434 | for(i = 0; i < zone->num_others; i++) 435 | if(zone->others[i]) { 436 | cfg_free_ipls(zone->others[i]); 437 | zone->others[i] = NULL; 438 | } 439 | free(zone->others); 440 | zone->others = NULL; 441 | } 442 | free(zone); 443 | } 444 | 445 | static struct zone* 446 | cfg_get_zone(xmlDocPtr config, xmlNodePtr zone_node) 447 | { 448 | struct zone *zn = NULL; 449 | xmlNodePtr element = NULL; 450 | 451 | if(parser_failed) 452 | return NULL; 453 | 454 | /* Allocate a new zone structure and 455 | * zero it out */ 456 | zn = (struct zone*) malloc(sizeof(struct zone)); 457 | if (!zn) { 458 | utils_err(CFG, "Unable to allocate zone !\n"); 459 | parser_failed = 1; 460 | return zn; 461 | } 462 | memset(zn, 0, sizeof(struct zone)); 463 | 464 | /* Name and start time attributes are both 465 | * mandatory */ 466 | zn->name = cfg_get_str_attr(zone_node, "Name"); 467 | if(parser_failed) { 468 | utils_err(CFG, "Could not get name atrribute for a zone\n"); 469 | goto cleanup; 470 | } 471 | 472 | cfg_get_start_attr(config, zone_node, &zn->start_time); 473 | if(parser_failed) { 474 | utils_err(CFG, "Could not get start time attribute for zone %s\n", 475 | zn->name); 476 | goto cleanup; 477 | } 478 | 479 | /* Fill it up */ 480 | element = zone_node->xmlChildrenNode; 481 | while (element != NULL) { 482 | if(!strncmp((const char*) element->name, "Maintainer", 11)) 483 | zn->maintainer = cfg_get_string(config, element); 484 | else if(!strncmp((const char*) element->name, "Description", 12)) 485 | zn->description = cfg_get_string(config, element); 486 | else if(!strncmp((const char*) element->name, "Comment", 8)) 487 | zn->comment = cfg_get_string(config, element); 488 | else if(!strncmp((const char*) element->name, "Main", 5)) 489 | zn->main_pls = cfg_get_pls(config,element); 490 | else if(!strncmp((const char*) element->name, "Fallback", 9)) 491 | zn->fallback_pls = cfg_get_pls(config,element); 492 | else if(!strncmp((const char*) element->name, "Intermediate", 13)) { 493 | /* Expand the others array */ 494 | struct intermediate_playlist **new_ptr = realloc(zn->others, ((zn->num_others + 1) * 495 | (sizeof(struct intermediate_playlist*)))); 496 | if(!new_ptr) { 497 | utils_err(CFG, "Could not re-alloc zone->others!\n"); 498 | parser_failed = 1; 499 | goto cleanup; 500 | } 501 | zn->num_others++; 502 | zn->others = new_ptr; 503 | 504 | /* Grab and store the ipls */ 505 | zn->others[zn->num_others - 1] = cfg_get_ipls(config,element); 506 | 507 | /* We got an empty ipls, re-use that slot for the next one */ 508 | if (!zn->others[zn->num_others - 1]) 509 | zn->num_others--; 510 | } 511 | if(parser_failed) { 512 | utils_err(CFG, "Parsing of zone %s failed\n", zn->name); 513 | goto cleanup; 514 | } 515 | element = element->next; 516 | } 517 | 518 | /* Note: only Main playlist is mandatory */ 519 | if(!zn->main_pls) { 520 | utils_err(CFG, "Got zone with no main playlist: %s\n", zn->name); 521 | parser_failed = 1; 522 | goto cleanup; 523 | } 524 | 525 | utils_dbg(CFG, "Got zone: %s\n\tMaintainer: %s\n\tDescription: %s\n\t", 526 | zn->name, zn->maintainer, zn->description, zn->comment); 527 | utils_dbg(CFG|SKIP, "Comment: %s\n\tnum_others: %i\n", zn->comment, 528 | zn->num_others); 529 | 530 | cleanup: 531 | if(parser_failed) { 532 | cfg_free_zone(zn); 533 | zn = NULL; 534 | } 535 | return zn; 536 | } 537 | 538 | 539 | /***********************\ 540 | * DAY SCHEDULE HANDLING * 541 | \***********************/ 542 | 543 | static void 544 | cfg_free_day_schedule(struct day_schedule* ds) 545 | { 546 | int i = 0; 547 | 548 | if(!ds) 549 | return; 550 | 551 | if(ds->zones) { 552 | for(i = 0; i < ds->num_zones; i++) 553 | if(ds->zones[i] != NULL) { 554 | cfg_free_zone(ds->zones[i]); 555 | ds->zones[i] = NULL; 556 | } 557 | free(ds->zones); 558 | } 559 | 560 | free(ds); 561 | } 562 | 563 | static struct day_schedule* 564 | cfg_get_day_schedule(xmlDocPtr config, xmlNodePtr ds_node) 565 | { 566 | struct day_schedule *ds = NULL; 567 | struct zone *tmp_zn0 = NULL; 568 | struct zone *tmp_zn1 = NULL; 569 | struct tm *tmp_tm = NULL; 570 | xmlNodePtr element = NULL; 571 | int got_start_of_day = 0; 572 | int ret = 0; 573 | 574 | if(parser_failed) 575 | return NULL; 576 | 577 | /* Allocate a day schedule structure and 578 | * zero it out */ 579 | ds = (struct day_schedule*) malloc(sizeof(struct day_schedule)); 580 | if (!ds) { 581 | utils_err(CFG, "Unable to allocate day schedule !\n"); 582 | parser_failed = 1; 583 | return ds; 584 | } 585 | memset(ds, 0, sizeof(struct day_schedule)); 586 | 587 | /* Fill it up */ 588 | element = ds_node->xmlChildrenNode; 589 | while (element != NULL) { 590 | /* Only zones are expected */ 591 | if(strncmp((const char*) element->name, "Zone",5) != 0) { 592 | element = element->next; 593 | continue; 594 | } 595 | 596 | /* Expand the zones array */ 597 | struct zone **new_ptr = realloc(ds->zones, ((ds->num_zones + 1) * sizeof(struct zone*))); 598 | if(!new_ptr) { 599 | utils_err(CFG, "Could not re-alloc day schedule!\n"); 600 | parser_failed = 1; 601 | goto cleanup; 602 | } 603 | ds->num_zones++; 604 | ds->zones = new_ptr; 605 | 606 | ds->zones[ds->num_zones - 1] = cfg_get_zone(config,element); 607 | if((!ds->zones[ds->num_zones - 1]) || parser_failed){ 608 | utils_err(CFG, "Parsing of a day schedule failed\n"); 609 | goto cleanup; 610 | } 611 | 612 | /* Check if we got a zone with a start time of 00:00:00 */ 613 | tmp_tm = &ds->zones[ds->num_zones - 1]->start_time; 614 | if(tmp_tm->tm_hour == 0 && tmp_tm->tm_min == 0 && 615 | tmp_tm->tm_sec == 0) 616 | got_start_of_day = 1; 617 | 618 | /* Demand that zones are stored in ascending order 619 | * based on their start time. We do this to keep 620 | * the lookup code simple and efficient. */ 621 | if(ds->num_zones > 1) { 622 | tmp_zn0 = ds->zones[ds->num_zones - 2]; 623 | tmp_zn1 = ds->zones[ds->num_zones - 1]; 624 | ret = utils_compare_time(&tmp_zn1->start_time, 625 | &tmp_zn0->start_time, 1); 626 | if(ret < 0) { 627 | utils_err(CFG, "Zones stored in wrong order for %s\n", 628 | ds_node->name); 629 | parser_failed = 1; 630 | goto cleanup; 631 | } else if (!ret) { 632 | utils_err(CFG, "Overlapping zones on %s\n", 633 | ds_node->name); 634 | parser_failed = 1; 635 | goto cleanup; 636 | } 637 | } 638 | 639 | element = element->next; 640 | } 641 | 642 | /* At least a zone is needed */ 643 | if(!ds->num_zones) { 644 | utils_err(CFG, "Got empty day schedule element (%s)\n", 645 | ds_node->name); 646 | parser_failed = 1; 647 | goto cleanup; 648 | } 649 | 650 | if(!got_start_of_day) 651 | utils_wrn(CFG, "Nothing scheduled on 00:00:00 for %s\n", 652 | ds_node->name); 653 | 654 | utils_info(CFG, "Got day schedule for %s, num_zones: %i\n", 655 | ds_node->name, ds->num_zones); 656 | 657 | cleanup: 658 | if(parser_failed) { 659 | cfg_free_day_schedule(ds); 660 | ds = NULL; 661 | } 662 | return ds; 663 | } 664 | 665 | 666 | /************************\ 667 | * WEEK SCHEDULE HANDLING * 668 | \************************/ 669 | 670 | static void 671 | cfg_free_week_schedule(struct week_schedule *ws) 672 | { 673 | int i = 0; 674 | if(!ws) 675 | return; 676 | 677 | for(i = 0; i < 7; i++) 678 | if(ws->days[i] != NULL) { 679 | cfg_free_day_schedule(ws->days[i]); 680 | ws->days[i] = NULL; 681 | } 682 | 683 | free(ws); 684 | } 685 | 686 | static struct week_schedule* 687 | cfg_get_week_schedule(xmlDocPtr config, xmlNodePtr ws_node) 688 | { 689 | struct week_schedule *ws = NULL; 690 | xmlNodePtr element = NULL; 691 | int i = 0; 692 | 693 | /* Allocate a week schedule structure 694 | * and zero it out */ 695 | ws = (struct week_schedule*) malloc(sizeof(struct week_schedule)); 696 | if (!ws) { 697 | utils_err(CFG, "Unable to allocate week schedule !\n"); 698 | parser_failed = 1; 699 | return NULL; 700 | } 701 | memset(ws, 0, sizeof(struct week_schedule)); 702 | 703 | /* Fill it up */ 704 | element = ws_node->xmlChildrenNode; 705 | while (element != NULL) { 706 | /* Note: Match these ids with the mapping on struct tm 707 | * which means that Sunday = 0, Monday = 1 etc */ 708 | if(!strncmp((const char*) element->name, "Sun",4)) 709 | ws->days[0] = cfg_get_day_schedule(config,element); 710 | else if(!strncmp((const char*) element->name, "Mon",4)) 711 | ws->days[1] = cfg_get_day_schedule(config,element); 712 | else if(!strncmp((const char*) element->name, "Tue",4)) 713 | ws->days[2] = cfg_get_day_schedule(config,element); 714 | else if(!strncmp((const char*) element->name, "Wed",4)) 715 | ws->days[3] = cfg_get_day_schedule(config,element); 716 | else if(!strncmp((const char*) element->name, "Thu",4)) 717 | ws->days[4] = cfg_get_day_schedule(config,element); 718 | else if(!strncmp((const char*) element->name, "Fri",4)) 719 | ws->days[5] = cfg_get_day_schedule(config,element); 720 | else if(!strncmp((const char*) element->name, "Sat",4)) 721 | ws->days[6] = cfg_get_day_schedule(config,element); 722 | if(parser_failed) { 723 | utils_err(CFG, "Parsing of week schedule failed\n"); 724 | goto cleanup; 725 | } 726 | element = element->next; 727 | } 728 | 729 | /* All days of the week should be filled */ 730 | for(i = 0; i < 7; i++ ) { 731 | if(ws->days[i]) 732 | continue; 733 | utils_err(CFG, "Got empty/incomplete week schedule\n"); 734 | parser_failed = 1; 735 | goto cleanup; 736 | } 737 | 738 | utils_info(CFG, "Got week schedule\n"); 739 | 740 | cleanup: 741 | if(parser_failed) { 742 | cfg_free_week_schedule(ws); 743 | ws = NULL; 744 | } 745 | return ws; 746 | } 747 | 748 | 749 | /***********************\ 750 | * XML SCHEMA VALIDATION * 751 | \***********************/ 752 | 753 | static void 754 | cfg_print_validation_error_msg(void *ctx, const char *fmt, ...) 755 | { 756 | if (!fmt) 757 | return; 758 | va_list args; 759 | va_start(args, fmt); 760 | utils_err(CFG, "Config validation failed: "); 761 | utils_verr(NONE, fmt, args); 762 | va_end(args); 763 | } 764 | 765 | /* Linked-in config XSD schema file (config_schema.xsd)*/ 766 | extern const char _binary_config_schema_xsd_start; 767 | extern const char _binary_config_schema_xsd_end; 768 | 769 | static int 770 | cfg_validate_against_schema(xmlDocPtr config) 771 | { 772 | xmlSchemaParserCtxtPtr ctx = NULL; 773 | xmlSchemaPtr schema = NULL; 774 | xmlSchemaValidCtxtPtr validation_ctx = NULL; 775 | unsigned int len = 0; 776 | int ret = 0; 777 | 778 | /* Load XSD shema file from memory and create a parser context */ 779 | len = (unsigned int) (&_binary_config_schema_xsd_end - 780 | &_binary_config_schema_xsd_start); 781 | ctx = xmlSchemaNewMemParserCtxt(&_binary_config_schema_xsd_start, len); 782 | if (!ctx) { 783 | utils_err(CFG, "Could not create XSD schema parsing context.\n"); 784 | parser_failed = 1; 785 | goto cleanup; 786 | } 787 | 788 | /* Run the schema parser and put the result in memory */ 789 | schema = xmlSchemaParse(ctx); 790 | if (!schema) { 791 | utils_err(CFG, "Could not parse XSD schema.\n"); 792 | parser_failed = 1; 793 | goto cleanup; 794 | } 795 | 796 | /* Create a validation context */ 797 | validation_ctx = xmlSchemaNewValidCtxt(schema); 798 | if (!validation_ctx) { 799 | utils_err(CFG, "Could not create XSD schema validation context.\n"); 800 | parser_failed = 1; 801 | goto cleanup; 802 | } 803 | 804 | /* Register error printing callbacks */ 805 | xmlSetGenericErrorFunc(NULL, cfg_print_validation_error_msg); 806 | xmlThrDefSetGenericErrorFunc(NULL, cfg_print_validation_error_msg); 807 | 808 | /* Run validation */ 809 | ret = xmlSchemaValidateDoc(validation_ctx, config); 810 | if (ret != 0) 811 | parser_failed = 1; 812 | 813 | cleanup: 814 | if (ctx) 815 | xmlSchemaFreeParserCtxt(ctx); 816 | if (schema) 817 | xmlSchemaFree(schema); 818 | if (validation_ctx) 819 | xmlSchemaFreeValidCtxt(validation_ctx); 820 | 821 | return (parser_failed ? -1 : 0); 822 | } 823 | 824 | 825 | /**************\ 826 | * ENTRY POINTS * 827 | \**************/ 828 | 829 | void 830 | cfg_cleanup(struct config *cfg) 831 | { 832 | if(!cfg) 833 | return; 834 | if(cfg->ws != NULL) 835 | cfg_free_week_schedule(cfg->ws); 836 | cfg->ws = NULL; 837 | } 838 | 839 | int 840 | cfg_process(struct config *cfg) 841 | { 842 | xmlParserCtxtPtr ctx = NULL; 843 | xmlDocPtr config = NULL; 844 | xmlNodePtr root_node = NULL; 845 | int ret = 0; 846 | parser_failed = 0; 847 | 848 | /* Sanity checks */ 849 | if(cfg->filepath == NULL) { 850 | utils_err(CFG, "Called with null argument\n"); 851 | parser_failed = 1; 852 | goto cleanup; 853 | } 854 | 855 | if(!utils_is_readable_file(cfg->filepath)) { 856 | parser_failed = 1; 857 | goto cleanup; 858 | } 859 | 860 | /* Store mtime for later checks */ 861 | cfg->last_mtime = utils_get_mtime(cfg->filepath); 862 | if(!cfg->last_mtime) { 863 | parser_failed = 1; 864 | goto cleanup; 865 | } 866 | 867 | /* Initialize libxml2 and do version checks for ABI compatibility */ 868 | LIBXML_TEST_VERSION 869 | 870 | /* Create a parser context */ 871 | ctx = xmlNewParserCtxt(); 872 | if (!ctx) { 873 | utils_err(CFG, "Failed to allocate parser context\n"); 874 | parser_failed = 1; 875 | goto cleanup; 876 | } 877 | 878 | /* Parse config file and put result to memory */ 879 | config = xmlParseFile(cfg->filepath); 880 | if (!config) { 881 | utils_err(CFG, "Document not parsed successfully.\n"); 882 | parser_failed = 1; 883 | goto cleanup; 884 | } 885 | 886 | /* Grab the root node, should be a WeekSchedule element */ 887 | root_node = xmlDocGetRootElement(config); 888 | if (!root_node) { 889 | utils_err(CFG, "Empty config\n"); 890 | parser_failed = 1; 891 | goto cleanup; 892 | } 893 | if (strncmp((const char*) root_node->name, "WeekSchedule", 13)) { 894 | utils_err(CFG, "Root element is not a WeekSchedule\n"); 895 | parser_failed = 1; 896 | goto cleanup; 897 | } 898 | 899 | /* Validate configuration against the configuration schema */ 900 | ret = cfg_validate_against_schema(config); 901 | if (ret < 0) { 902 | utils_err(CFG, "Configuration did not pass shema validation\n"); 903 | parser_failed = 1; 904 | goto cleanup; 905 | } 906 | 907 | /* Fill the data to the config struct */ 908 | cfg->ws = cfg_get_week_schedule(config, root_node); 909 | 910 | cleanup: 911 | /* Cleanup the config and any leftovers from the parser */ 912 | if(config) 913 | xmlFreeDoc(config); 914 | if(ctx) 915 | xmlFreeParserCtxt(ctx); 916 | xmlCleanupParser(); 917 | if(parser_failed) { 918 | ret = -1; 919 | cfg_cleanup(cfg); 920 | } 921 | return ret; 922 | } 923 | 924 | int 925 | cfg_reload_if_needed(struct config *cfg) 926 | { 927 | time_t mtime = utils_get_mtime(cfg->filepath); 928 | if(!mtime) { 929 | utils_err(CFG, "Unable to check mtime for %s\n", cfg->filepath); 930 | return -1; 931 | } 932 | 933 | /* mtime didn't change, no need to reload */ 934 | if(mtime == cfg->last_mtime) 935 | return 0; 936 | 937 | utils_info(CFG, "Got different mtime, reloading %s\n", cfg->filepath); 938 | 939 | /* Re-load config */ 940 | cfg_cleanup(cfg); 941 | return cfg_process(cfg); 942 | } 943 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /fsp_player.c: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileType: SOURCE 3 | * 4 | * SPDX-FileCopyrightText: 2025 Nick Kossifidis 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * This is a player backend based on FFmpeg and Pipewire, in our case we compile FFmpeg 11 | * with libsoxr support, which is how this is intended to work (and how we use and test 12 | * it), but the built-in resampler should also work, since we use the swr API. 13 | */ 14 | 15 | #include /* For memset/memcpy */ 16 | #include /* For signal numbers */ 17 | #include /* For posix_memalign / malloc */ 18 | #include /* For av_opt_* */ 19 | #include /* For spa_pod_builder_* */ 20 | #include /* For spa_format_audio_raw_build */ 21 | #include "utils.h" 22 | #include "fsp_player.h" 23 | 24 | /****************\ 25 | * SIGNAL HANDLER * 26 | \****************/ 27 | 28 | static void fsp_signal_handler(int signal_number, void *userdata) 29 | { 30 | struct fsp_player *player = (struct fsp_player*) userdata; 31 | 32 | switch (signal_number) { 33 | case SIGINT: 34 | case SIGTERM: 35 | fsp_stop(player); 36 | break; 37 | case SIGUSR1: 38 | utils_info(PLR, "Pausing\n"); 39 | player->state = FSP_STATE_PAUSING; 40 | break; 41 | case SIGUSR2: 42 | utils_info(PLR, "Resuming\n"); 43 | player->state = FSP_STATE_RESUMING; 44 | break; 45 | default: 46 | break; 47 | } 48 | } 49 | 50 | 51 | /****************************\ 52 | * FADER / REPLAYGAIN HELPERS * 53 | \****************************/ 54 | 55 | static int fsp_replaygain_setup(struct fsp_replaygain_state *rgain, 56 | const struct audiofile_info *info) 57 | { 58 | /* Convert track gain from dB to linear */ 59 | if (info->track_gain) 60 | rgain->replay_gain = powf(10.0f, info->track_gain / 20.0f); 61 | else 62 | rgain->replay_gain = 1.0f; 63 | 64 | /* Calculate gain limit from peak (already in linear scale) */ 65 | if (info->track_peak) 66 | rgain->gain_limit = 1.0f / info->track_peak; 67 | else 68 | rgain->gain_limit = 1.0f; 69 | 70 | /* Limit replay gain by peak */ 71 | if (rgain->replay_gain > rgain->gain_limit) { 72 | utils_dbg(PLR, "Limiting replay gain to peak: %f\n", rgain->gain_limit); 73 | rgain->replay_gain = rgain->gain_limit; 74 | } 75 | 76 | return 0; 77 | } 78 | 79 | static int fsp_fader_setup(struct fsp_af_fader_state *fader, 80 | const struct audiofile_info *info) 81 | { 82 | fader->fade_in_slope = 0.0f; 83 | fader->fade_out_slope = 0.0f; 84 | 85 | if (!info->fader_info) 86 | return 0; 87 | 88 | const struct fader_info *fdr = info->fader_info; 89 | 90 | if (fdr->fadein_duration_secs > 0 && (fdr->fadein_duration_secs < info->duration_secs)) 91 | fader->fade_in_slope = 1.0f / (FSP_OUTPUT_SAMPLE_RATE * 92 | fdr->fadein_duration_secs); 93 | if (fdr->fadeout_duration_secs > 0 && (fdr->fadeout_duration_secs < info->duration_secs)) 94 | fader->fade_out_slope = 1.0f / (FSP_OUTPUT_SAMPLE_RATE * 95 | fdr->fadeout_duration_secs); 96 | 97 | return 0; 98 | } 99 | 100 | static void fsp_state_fader_setup(struct fsp_state_fader_state *fader) 101 | { 102 | fader->state_fade_samples_tot = FSP_OUTPUT_SAMPLE_RATE * 2; /* 2 seconds */ 103 | fader->state_fade_slope = 1.0f / fader->state_fade_samples_tot; 104 | fader->state_fade_samples_out = 0; 105 | fader->state_fade_active = 0; 106 | fader->state_fade_gain = 1.0f; 107 | } 108 | 109 | static void fsp_fader_state_fade_start(struct fsp_state_fader_state *fader, int fade_in) 110 | { 111 | fader->state_fade_samples_out = 0; 112 | fader->state_fade_active = 1; 113 | fader->state_fade_gain = fade_in ? 0.0f : 1.0f; 114 | } 115 | 116 | static float fsp_fader_state_fade_step(struct fsp_state_fader_state *fader, size_t frames, int fade_in) 117 | { 118 | if (!fader->state_fade_active) 119 | goto done; 120 | 121 | /* Check if fade is complete */ 122 | if (fader->state_fade_samples_out >= fader->state_fade_samples_tot) { 123 | fader->state_fade_active = 0; 124 | fader->state_fade_gain = fade_in ? 1.0f : 0.0f; 125 | goto done; 126 | } 127 | 128 | /* Calculate how many frames we can process in this step */ 129 | size_t frames_remaining = fader->state_fade_samples_tot - fader->state_fade_samples_out; 130 | 131 | /* Calculate new gain */ 132 | if (fade_in) { 133 | fader->state_fade_gain = (float)fader->state_fade_samples_out * fader->state_fade_slope; 134 | } else { 135 | fader->state_fade_gain = (float)frames_remaining * fader->state_fade_slope; 136 | } 137 | 138 | /* Update fade position */ 139 | fader->state_fade_samples_out += frames; 140 | 141 | done: 142 | return fader->state_fade_gain; 143 | } 144 | 145 | 146 | /**********************\ 147 | * DECODER INIT/CLEANUP * 148 | \**********************/ 149 | 150 | static void fsp_decoder_cleanup(struct fsp_decoder_state *dec) 151 | { 152 | if (!dec) 153 | return; 154 | 155 | /* Note: av_*_free functions also set their pointer 156 | * arg to NULL */ 157 | if (dec->decoded_avframe) 158 | av_frame_free(&dec->decoded_avframe); 159 | if (dec->resampled_avframe) 160 | av_frame_free(&dec->resampled_avframe); 161 | if (dec->stream_packet) 162 | av_packet_free(&dec->stream_packet); 163 | if (dec->codec_ctx) 164 | avcodec_free_context(&dec->codec_ctx); 165 | if (dec->fmt_ctx) 166 | avformat_close_input(&dec->fmt_ctx); 167 | if (dec->swr_ctx) 168 | swr_free(&dec->swr_ctx); 169 | } 170 | 171 | static int fsp_decoder_init(struct fsp_decoder_state *dec, const char *filepath) 172 | { 173 | int ret; 174 | 175 | /* Open input file */ 176 | ret = avformat_open_input(&dec->fmt_ctx, filepath, NULL, NULL); 177 | if (ret < 0) { 178 | utils_err(PLR, "Failed to open file: %s\n", av_err2str(ret)); 179 | return -1; 180 | } 181 | 182 | /* Find stream info */ 183 | ret = avformat_find_stream_info(dec->fmt_ctx, NULL); 184 | if (ret < 0) { 185 | utils_err(PLR, "Failed to find stream info: %s\n", av_err2str(ret)); 186 | goto cleanup; 187 | } 188 | 189 | /* Find audio stream */ 190 | dec->audio_stream_idx = av_find_best_stream(dec->fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0); 191 | if (dec->audio_stream_idx < 0) { 192 | utils_err(PLR, "Failed to find audio stream\n"); 193 | goto cleanup; 194 | } 195 | 196 | /* Get decoder */ 197 | int codec_id = dec->fmt_ctx->streams[dec->audio_stream_idx]->codecpar->codec_id; 198 | const AVCodec *codec = avcodec_find_decoder(codec_id); 199 | if (!codec) { 200 | utils_err(PLR, "Failed to find decoder\n"); 201 | goto cleanup; 202 | } 203 | 204 | /* Allocate codec context */ 205 | dec->codec_ctx = avcodec_alloc_context3(codec); 206 | if (!dec->codec_ctx) { 207 | utils_err(PLR, "Failed to allocate decoder context\n"); 208 | goto cleanup; 209 | } 210 | 211 | /* Copy codec parameters */ 212 | ret = avcodec_parameters_to_context(dec->codec_ctx, 213 | dec->fmt_ctx->streams[dec->audio_stream_idx]->codecpar); 214 | if (ret < 0) { 215 | utils_err(PLR, "Failed to copy codec params: %s\n", av_err2str(ret)); 216 | goto cleanup; 217 | } 218 | 219 | /* Set decoder output format to interleaved float */ 220 | ret = av_opt_set_int(dec->codec_ctx, "request_sample_fmt", AV_SAMPLE_FMT_FLT, 0); 221 | if (ret < 0) { 222 | utils_err(PLR, "Failed to set decoder output format: %s\n", av_err2str(ret)); 223 | goto cleanup; 224 | } 225 | 226 | /* Open the codec */ 227 | ret = avcodec_open2(dec->codec_ctx, NULL, NULL); 228 | if (ret < 0) { 229 | utils_err(PLR, "Failed to open codec: %s\n", av_err2str(ret)); 230 | goto cleanup; 231 | } 232 | 233 | /* Allocate decoded_avframe and stream_packet here so that we 234 | * don't allocate them on each loop. Note those are just 235 | * the structs, their content is managed by ffmpeg when we 236 | * request new packets/frames */ 237 | dec->decoded_avframe = av_frame_alloc(); 238 | dec->stream_packet = av_packet_alloc(); 239 | if (!dec->decoded_avframe || !dec->stream_packet) { 240 | utils_err(PLR, "Failed to allocate incoming frame/packet\n"); 241 | goto cleanup; 242 | } 243 | 244 | /* For ffmpeg to fill out resampled_avframe, we need to allocate 245 | * its buffers and set its properties here, since we give it to 246 | * ffmpeg (in contrast to decoded_avframe that ffmpeg gives to us) */ 247 | dec->resampled_avframe = av_frame_alloc(); 248 | if (!dec->resampled_avframe) { 249 | utils_err(PLR, "Failed to allocate outgoing frame\n"); 250 | goto cleanup; 251 | } 252 | 253 | /* Here nb_samples is samples per channel, so audio frames, 254 | * not raw samples (which is why we don't multiply it by 255 | * FSP_OUTPUT_CHANNELS). Set the output format (interleaved float), 256 | * channel layout, and sample rate. We'll pass the same values 257 | * to the resampler / converter initialization below. 258 | * 259 | * Note that the resampler may need more frames for output in 260 | * case it adds a delay, FSP_PERIOD_SIZE should be a safe start, but 261 | * we can query the codec context in case the decoder outputs a 262 | * fixed number of samples on the decoded_avframe for a more accurate 263 | * approach. No matter what we may still need to grow resampled_avframe 264 | * if needed. */ 265 | if (dec->codec_ctx->frame_size) { 266 | dec->resampled_avframe->nb_samples = av_rescale_rnd(dec->codec_ctx->frame_size, 267 | FSP_OUTPUT_SAMPLE_RATE, 268 | dec->codec_ctx->sample_rate, 269 | AV_ROUND_UP); 270 | } else 271 | dec->resampled_avframe->nb_samples = FSP_PERIOD_SIZE; 272 | av_channel_layout_copy(&dec->resampled_avframe->ch_layout, 273 | &(AVChannelLayout)AV_CHANNEL_LAYOUT_STEREO); 274 | dec->resampled_avframe->format = AV_SAMPLE_FMT_FLT; 275 | dec->resampled_avframe->sample_rate = FSP_OUTPUT_SAMPLE_RATE; 276 | 277 | ret = av_frame_get_buffer(dec->resampled_avframe, 0); 278 | if (ret < 0) { 279 | utils_err(PLR, "Failed to allocate resampled_avframe: %s\n", 280 | av_err2str(ret)); 281 | goto cleanup; 282 | } 283 | 284 | /* Initialize resampler / converter */ 285 | ret = swr_alloc_set_opts2(&dec->swr_ctx, 286 | &dec->resampled_avframe->ch_layout, dec->resampled_avframe->format, dec->resampled_avframe->sample_rate, 287 | &dec->codec_ctx->ch_layout, dec->codec_ctx->sample_fmt, dec->codec_ctx->sample_rate, 288 | 0, NULL); 289 | if (ret < 0) { 290 | utils_err(PLR, "Failed to allocate resampler context: %s\n", 291 | av_err2str(ret)); 292 | return -1; 293 | } 294 | 295 | /* Try using the SoXr backend if available */ 296 | #ifdef SWR_FLAG_RESAMPLE_SOXR 297 | av_opt_set (dec->swr_ctx, "resampler", "soxr", 0); 298 | av_opt_set (dec->swr_ctx, "precision", "28", 0); /* Very high quality */ 299 | #else 300 | /* Fallback to swr default backend */ 301 | av_opt_set_int(dec->swr_ctx, "dither_method", SWR_DITHER_TRIANGULAR_HIGHPASS, 0); 302 | av_opt_set_int(dec->swr_ctx, "filter_size", 64,0); 303 | #endif 304 | /* Full phase shift (linear responce) */ 305 | av_opt_set_double (dec->swr_ctx, "phase_shift", 1.0, 0); 306 | /* Full bandwidth (a bit reduced for anti-aliasing) */ 307 | av_opt_set_int(dec->swr_ctx, "cutoff", 0.98, 0); 308 | 309 | /* Initialize the resampling context */ 310 | ret = swr_init(dec->swr_ctx); 311 | if (ret < 0) { 312 | utils_err(PLR, "Failed to initialize resampler: %s\n", av_err2str(ret)); 313 | goto cleanup; 314 | } 315 | 316 | dec->consumed_frames = 0; 317 | dec->avail_frames = 0; 318 | dec->eof_reached = 0; 319 | 320 | return 0; 321 | 322 | cleanup: 323 | fsp_decoder_cleanup(dec); 324 | return -1; 325 | } 326 | 327 | 328 | /****************************\ 329 | * PLAYBACK/AUDIOFILE CONTEXT * 330 | \****************************/ 331 | 332 | static void fsp_audiofile_ctx_cleanup(struct fsp_audiofile_ctx *ctx) 333 | { 334 | if (!ctx) 335 | return; 336 | mldr_cleanup_audiofile(&ctx->info); 337 | fsp_decoder_cleanup(&ctx->decoder); 338 | } 339 | 340 | static int fsp_audiofile_ctx_init(struct fsp_audiofile_ctx *ctx, 341 | const struct audiofile_info *info) 342 | { 343 | int ret; 344 | 345 | /* Copy file info from fsp_load_next_file's stack to ctx */ 346 | memcpy(&ctx->info, info, sizeof(struct audiofile_info)); 347 | ctx->samples_played = 0; 348 | ctx->total_samples = info->duration_secs * FSP_OUTPUT_SAMPLE_RATE * FSP_OUTPUT_CHANNELS; 349 | 350 | /* Initialize decoder/resampler first */ 351 | ret = fsp_decoder_init(&ctx->decoder, info->filepath); 352 | if (ret < 0) { 353 | utils_err(PLR, "Failed to initialize decoder\n"); 354 | return ret; 355 | } 356 | 357 | /* Setup ReplayGain */ 358 | ret = fsp_replaygain_setup(&ctx->replaygain, info); 359 | if (ret < 0) { 360 | utils_err(PLR, "Failed to setup ReplayGain\n"); 361 | goto cleanup; 362 | } 363 | 364 | /* Setup fader */ 365 | ret = fsp_fader_setup(&ctx->fader, info); 366 | if (ret < 0) { 367 | utils_err(PLR, "Failed to setup fader\n"); 368 | goto cleanup; 369 | } 370 | 371 | return 0; 372 | 373 | cleanup: 374 | fsp_decoder_cleanup(&ctx->decoder); 375 | return ret; 376 | } 377 | 378 | 379 | /****************\ 380 | * DECODER THREAD * 381 | \****************/ 382 | 383 | /* Extracts audio frames from the provided fsp_audiofile_ctx, each audio frame 384 | * contains FSP_OUTPUT_CHANNELS audio samples. Since FFmpeg also uses the term 385 | * "frame" for the decoded audio chunks, I use "avframe" for those, don't let 386 | * the name of av_read_frame() confuse you, its for audio chunks (multiple audio 387 | * frames), not a single audio frame. */ 388 | static size_t 389 | fsp_extract_frames(struct fsp_player *player, struct fsp_audiofile_ctx *ctx, 390 | float *output, size_t frames_needed) 391 | { 392 | struct fsp_decoder_state *dec = &ctx->decoder; 393 | size_t frames_decoded = 0; 394 | int ret; 395 | 396 | /* Process until we have enough frames or hit EOF */ 397 | while (frames_decoded < frames_needed && !dec->eof_reached && player->state != FSP_STATE_STOPPING) { 398 | 399 | /* We consumed all frames from the last resampled 400 | * avframe, ask for a new one from the decoder. */ 401 | if (dec->consumed_frames >= dec->avail_frames) { 402 | dec->consumed_frames = 0; 403 | dec->avail_frames = 0; 404 | 405 | /* Try to get next avframe from the decoder, note that according 406 | * to docs, this will also unref decoded_frame before providing a new one.*/ 407 | ret = avcodec_receive_frame(dec->codec_ctx, dec->decoded_avframe); 408 | if (ret == AVERROR(EAGAIN)) { 409 | 410 | /* Out of data, grab the next packet from the demuxer that 411 | * handles the audio file. */ 412 | while ((ret = av_read_frame(dec->fmt_ctx, dec->stream_packet)) >= 0) { 413 | 414 | /* Check if it's an audio packet and send it to the decoder 415 | * According o the docs the packet is always fully consumed 416 | * and is owned by the caller, so we unref afterwards */ 417 | if (dec->stream_packet->stream_index == dec->audio_stream_idx) { 418 | if ((ret = avcodec_send_packet(dec->codec_ctx, dec->stream_packet)) < 0) { 419 | utils_err(PLR, "Error sending packet to decoder: %s\n", 420 | av_err2str(ret)); 421 | av_packet_unref(dec->stream_packet); 422 | goto cleanup; 423 | } 424 | av_packet_unref(dec->stream_packet); 425 | /* We should have decoded avframes now */ 426 | break; 427 | } 428 | 429 | /* Not an audio packet, unref and try next one */ 430 | av_packet_unref(dec->stream_packet); 431 | } 432 | 433 | if (ret < 0) { 434 | if (ret == AVERROR_EOF) { 435 | /* No more packets left on the stream, we need a new file 436 | * Flush any pending avframes out of the decoder and retry */ 437 | avcodec_send_packet(dec->codec_ctx, NULL); 438 | utils_dbg(PLR, "flushed decoder\n"); 439 | } else { 440 | utils_err(PLR, "Error reading packet: %s\n", av_err2str(ret)); 441 | goto cleanup; 442 | } 443 | } 444 | 445 | continue; 446 | } else if (ret == AVERROR_EOF) { 447 | /* No more avframes available on the decoder */ 448 | dec->eof_reached = 1; 449 | av_frame_unref(dec->decoded_avframe); 450 | } else if (ret < 0) { 451 | utils_err(PLR, "Error receiving frame from decoder: %s\n", 452 | av_err2str(ret)); 453 | goto cleanup; 454 | } 455 | 456 | /* Got a new avframe, resample / convert it to the requested output config 457 | * (including channel layout, so this will also downmix multi-channel files 458 | * and handle mono files). Note that we may have pending frames inside the 459 | * resampler from a previous run, in which case decoded_avframe will be empty 460 | * and we need to flush it the same way we flush the decoder. */ 461 | 462 | /* Check if the next call to swr_convert_frame will need extra frames 463 | * for delay compensation, or we have any pending frames to flush out of 464 | * the resampler. */ 465 | int64_t swr_delay = swr_get_delay(dec->swr_ctx, dec->codec_ctx->sample_rate); 466 | if (swr_delay < 0) { 467 | utils_err(PLR, "Error requesting delay from resampler: %s\n", 468 | av_err2str(ret)); 469 | goto cleanup; 470 | } 471 | 472 | /* Check if we have enough space on resampled_frame for the resampled output */ 473 | uint32_t required_samples = av_rescale_rnd(swr_delay + dec->decoded_avframe->nb_samples, 474 | FSP_OUTPUT_SAMPLE_RATE, dec->codec_ctx->sample_rate, 475 | AV_ROUND_UP); 476 | size_t required_bytes = required_samples * FSP_OUTPUT_CHANNELS * sizeof(float); 477 | size_t allocated_bytes = dec->resampled_avframe->buf[0]->size; 478 | if (required_bytes > allocated_bytes) { 479 | utils_dbg(PLR, "re-sizing resampled_avframe, required: %i, allocated: %i\n", 480 | required_bytes, allocated_bytes); 481 | 482 | /* Free up the current one */ 483 | av_frame_unref(dec->resampled_avframe); 484 | 485 | /* Initialize and allocate the updated one */ 486 | dec->resampled_avframe->nb_samples = required_samples; 487 | av_channel_layout_copy(&dec->resampled_avframe->ch_layout, 488 | &(AVChannelLayout)AV_CHANNEL_LAYOUT_STEREO); 489 | dec->resampled_avframe->format = AV_SAMPLE_FMT_FLT; 490 | dec->resampled_avframe->sample_rate = FSP_OUTPUT_SAMPLE_RATE; 491 | 492 | ret = av_frame_get_buffer(dec->resampled_avframe, 0); 493 | if (ret < 0) { 494 | utils_err(PLR, "Failed to allocate resampled_avframe: %s\n", 495 | av_err2str(ret)); 496 | goto cleanup; 497 | } 498 | } 499 | 500 | if (dec->decoded_avframe->nb_samples > 0) 501 | ret = swr_convert_frame(dec->swr_ctx, dec->resampled_avframe, dec->decoded_avframe); 502 | else { 503 | ret = swr_convert_frame(dec->swr_ctx, dec->resampled_avframe, NULL); 504 | utils_dbg(PLR, "flushed resampler\n"); 505 | } 506 | 507 | if (ret < 0) { 508 | utils_wrn(PLR, "Error during resampling: %s\n", 509 | av_err2str(ret)); 510 | goto cleanup; 511 | } 512 | 513 | /* Note that nb_samples is samples per channel, so audio frames */ 514 | dec->avail_frames = dec->resampled_avframe->nb_samples; 515 | } 516 | 517 | /* Copy frames from the last resampled_avframe to output */ 518 | if (dec->avail_frames > 0) { 519 | /* Since we use interleaved format (AV_SAMPLE_FMT_FLT) we have all samples from both channels on data[0] */ 520 | const float *src = (float *)dec->resampled_avframe->data[0] + (dec->consumed_frames * FSP_OUTPUT_CHANNELS); 521 | float *dst = output + (frames_decoded * FSP_OUTPUT_CHANNELS); 522 | 523 | size_t remaining_decoded_frames = frames_needed - frames_decoded; 524 | size_t remaining_ctx_samples = ctx->total_samples - ctx->samples_played; 525 | 526 | size_t frames_to_copy = dec->avail_frames - dec->consumed_frames; 527 | 528 | /* Decoder gave us more frames than we require, output up to remaining_decoded_frames 529 | * and leave the rest there for the next run to process. If it has less we'll return 530 | * a value less than frames_needed and the caller will handle calling us again with 531 | * a new ctx and an updated output pointer. */ 532 | if (frames_to_copy > remaining_decoded_frames) 533 | frames_to_copy = remaining_decoded_frames; 534 | 535 | /* Calculate fader gain */ 536 | float fader_gain = 1.0f; 537 | struct audiofile_info *info = &ctx->info; 538 | const struct fader_info *fdr = info->fader_info; 539 | 540 | /* Fade in on song start */ 541 | if (ctx->fader.fade_in_slope > 0 && 542 | ctx->samples_played < fdr->fadein_duration_secs * FSP_OUTPUT_SAMPLE_RATE) { 543 | fader_gain = ctx->fader.fade_in_slope * ctx->samples_played; 544 | /* Fade out on song end */ 545 | } else if (ctx->fader.fade_out_slope > 0 && 546 | remaining_ctx_samples < fdr->fadeout_duration_secs * FSP_OUTPUT_SAMPLE_RATE) { 547 | fader_gain = ctx->fader.fade_out_slope * remaining_ctx_samples; 548 | } 549 | 550 | /* Combine fader gain modifier with replaygain */ 551 | float gain_factor = fader_gain * ctx->replaygain.replay_gain; 552 | 553 | /* Apply combined gain and copy to output and fill the remaining buffer */ 554 | size_t samples_to_copy = frames_to_copy * FSP_OUTPUT_CHANNELS; 555 | for (size_t i = 0; i < samples_to_copy; i++) { 556 | dst[i] = src[i] * gain_factor; 557 | } 558 | 559 | dec->consumed_frames += frames_to_copy; 560 | frames_decoded += frames_to_copy; 561 | ctx->samples_played += samples_to_copy; 562 | } 563 | } 564 | 565 | cleanup: 566 | return frames_decoded; 567 | } 568 | 569 | struct fsp_decoder_thread_data { 570 | float *decode_buffer; 571 | size_t buffer_max_frames; 572 | size_t buffer_size; 573 | }; 574 | 575 | 576 | static void fsp_decoder_thread_data_cleanup(struct fsp_decoder_thread_data *data) 577 | { 578 | if (!data) 579 | return; 580 | free(data->decode_buffer); 581 | free(data); 582 | } 583 | 584 | static struct fsp_decoder_thread_data *fsp_decoder_thread_data_init(void) 585 | { 586 | struct fsp_decoder_thread_data *data; 587 | void *aligned_buf; 588 | int ret; 589 | 590 | data = malloc(sizeof(*data)); 591 | if (!data) { 592 | utils_err(PLR, "Failed to allocate decoder thread data\n"); 593 | return NULL; 594 | } 595 | 596 | /* The decoder thread outputs a period at a time to put to the ringbuffer 597 | * which holds up to FSP_RING_BUFFER_SECONDS of data. */ 598 | data->buffer_max_frames = FSP_PERIOD_SIZE; 599 | data->buffer_size = data->buffer_max_frames * FSP_OUTPUT_CHANNELS * sizeof(float); 600 | ret = posix_memalign(&aligned_buf, FSP_CACHE_LINE_SIZE, data->buffer_size); 601 | if (ret != 0) { 602 | utils_err(PLR, "Failed to allocate aligned decode buffer\n"); 603 | free(data); 604 | return NULL; 605 | } 606 | data->decode_buffer = aligned_buf; 607 | 608 | return data; 609 | } 610 | 611 | static void *fsp_decoder_thread(void *arg) 612 | { 613 | struct fsp_player *player = arg; 614 | struct fsp_decoder_thread_data *data; 615 | size_t frames_decoded = 0; 616 | 617 | utils_dbg(PLR, "Decoder thread started\n"); 618 | 619 | data = fsp_decoder_thread_data_init(); 620 | if (!data) { 621 | utils_err(PLR, "Failed to initialize decoder thread data\n"); 622 | return NULL; 623 | } 624 | 625 | /* Wait until we have at least one file to decode */ 626 | pthread_cond_wait(&player->decoder_cond, &player->decoder_mutex); 627 | 628 | while (player->state != FSP_STATE_STOPPING) { 629 | /* Wait if ring buffer doesn't have space for double buffer */ 630 | if (jack_ringbuffer_write_space(player->ring) < data->buffer_size) { 631 | pthread_cond_wait(&player->space_available, &player->decoder_mutex); 632 | continue; 633 | } 634 | 635 | /* Decode frames from file into the decode_buffer */ 636 | pthread_mutex_lock(&player->file_mutex); 637 | frames_decoded = fsp_extract_frames(player, &player->current, 638 | data->decode_buffer, 639 | data->buffer_max_frames); 640 | 641 | /* Got fewer frames than requested, so no more frames on the decoder 642 | * switch to the next file and keep filling the decode_buffer */ 643 | if (frames_decoded < data->buffer_max_frames && player->next.decoder.fmt_ctx) { 644 | utils_dbg(PLR, "Switching to next file\n"); 645 | 646 | /* Check if we missed any frames and warn the user, resampler may generate 647 | * extra samples but we shouldn't be very off (under normal cirumstances I 648 | * got 1 extra sample which is due to the round-up), this check is here 649 | * for debugging mostly, to make sure that if sometihng tottaly weird happens 650 | * we catch it and report it.*/ 651 | int diff = &player->current.total_samples - &player->current.samples_played; 652 | if (abs(diff) > 100) 653 | utils_wrn(PLR, "inconsistent playback diff: %i samples\n", diff); 654 | 655 | /* Cleanup current audiofile context */ 656 | fsp_audiofile_ctx_cleanup(&player->current); 657 | 658 | /* Move next to current */ 659 | memcpy(&player->current, &player->next, sizeof(struct fsp_audiofile_ctx)); 660 | memset(&player->next, 0, sizeof(struct fsp_audiofile_ctx)); 661 | 662 | /* Signal scheduler to load next file */ 663 | pthread_cond_signal(&player->scheduler_cond); 664 | 665 | /* Continue extracting frames with new current file 666 | * and any frames remaining on the last decoded_avframe */ 667 | size_t remaining = data->buffer_max_frames - frames_decoded; 668 | size_t buffer_offt = (frames_decoded * FSP_OUTPUT_CHANNELS); 669 | size_t extra_frames = fsp_extract_frames(player, &player->current, 670 | data->decode_buffer + buffer_offt, 671 | remaining); 672 | frames_decoded += extra_frames; 673 | } 674 | pthread_mutex_unlock(&player->file_mutex); 675 | 676 | if (frames_decoded > 0) { 677 | /* Write to ring buffer */ 678 | size_t samples_to_write = (frames_decoded * FSP_OUTPUT_CHANNELS); 679 | size_t bytes_to_write = samples_to_write * sizeof(float); 680 | size_t written = jack_ringbuffer_write(player->ring, 681 | (const char *)data->decode_buffer, 682 | bytes_to_write); 683 | 684 | if (written < bytes_to_write) 685 | utils_wrn(PLR, "Ring buffer overrun\n"); 686 | } 687 | 688 | /* Small sleep if no data */ 689 | if (frames_decoded == 0 && player->state != FSP_STATE_STOPPING) { 690 | struct timespec ts = {0, 1000000}; /* 1ms */ 691 | nanosleep(&ts, NULL); 692 | } 693 | } 694 | 695 | utils_dbg(PLR, "Decoder thread stopping\n"); 696 | fsp_decoder_thread_data_cleanup(data); 697 | fsp_stop(player); 698 | return NULL; 699 | } 700 | 701 | 702 | /******************\ 703 | * SCHEDULER THREAD * 704 | \******************/ 705 | 706 | static int fsp_load_next_file(struct fsp_player *player, time_t sched_time) 707 | { 708 | struct audiofile_info next_info; 709 | int ret; 710 | 711 | ret = sched_get_next(player->sched, sched_time, &next_info); 712 | if (ret < 0) { 713 | utils_err(PLR, "Failed to get next file from scheduler\n"); 714 | return -1; 715 | } 716 | 717 | utils_dbg(PLR, "Loading next file: %s\n", next_info.filepath); 718 | 719 | /* Initialize next audiofile context */ 720 | ret = fsp_audiofile_ctx_init(&player->next, &next_info); 721 | if (ret < 0) { 722 | utils_err(PLR, "Failed to initialize next audiofile context\n"); 723 | return -1; 724 | } 725 | 726 | return 0; 727 | } 728 | 729 | static void *fsp_scheduler_thread(void *arg) 730 | { 731 | struct fsp_player *player = arg; 732 | int ret; 733 | time_t now; 734 | time_t sched_time; 735 | time_t curr_duration; 736 | 737 | utils_dbg(PLR, "Scheduler thread started\n"); 738 | 739 | /* First run - get current song */ 740 | sched_time = time(NULL); 741 | ret = fsp_load_next_file(player, sched_time); 742 | if (ret < 0) { 743 | utils_err(PLR, "Failed to load initial file\n"); 744 | return NULL; 745 | } 746 | 747 | /* Move next to current */ 748 | pthread_mutex_lock(&player->file_mutex); 749 | memcpy(&player->current, &player->next, sizeof(struct fsp_audiofile_ctx)); 750 | memset(&player->next, 0, sizeof(struct fsp_audiofile_ctx)); 751 | pthread_mutex_unlock(&player->file_mutex); 752 | 753 | curr_duration = player->current.info.duration_secs; 754 | 755 | /* Immediately get next song */ 756 | sched_time += curr_duration; 757 | ret = fsp_load_next_file(player, sched_time); 758 | if (ret < 0) { 759 | utils_err(PLR, "Failed to load second file\n"); 760 | return NULL; 761 | } 762 | 763 | /* Signal decoder that we have files ready */ 764 | pthread_cond_signal(&player->decoder_cond); 765 | 766 | while (player->state != FSP_STATE_STOPPING) { 767 | /* Save curation of next song here, before decoder does 768 | * the switch and lose the information., so that when 769 | * we start playing it and becomes the current song, 770 | * we can correctly schedule the one after it. */ 771 | curr_duration = player->next.info.duration_secs; 772 | 773 | /* Wait for signal from decoder when it switches files */ 774 | pthread_cond_wait(&player->scheduler_cond, &player->scheduler_mutex); 775 | 776 | if (player->state == FSP_STATE_STOPPING) 777 | break; 778 | 779 | /* Calculate next schedule time based on current file */ 780 | now = time(NULL); 781 | if (utils_is_debug_enabled(PLR)) { 782 | char datestr[26]; 783 | struct tm tm = *localtime(&now); 784 | strftime (datestr, 26, "%a %d %b %Y, %H:%M:%S", &tm); 785 | utils_dbg (PLR, "Scheduler triggered at: %s\n", datestr); 786 | } 787 | sched_time = now + curr_duration; 788 | 789 | /* Load next file */ 790 | pthread_mutex_lock(&player->file_mutex); 791 | ret = fsp_load_next_file(player, sched_time); 792 | pthread_mutex_unlock(&player->file_mutex); 793 | if (ret < 0) { 794 | utils_err(PLR, "Failed to load next file\n"); 795 | break; /* Fatal error */ 796 | } 797 | 798 | pthread_cond_signal(&player->decoder_cond); 799 | } 800 | 801 | utils_dbg(PLR, "Scheduler thread stopping\n"); 802 | fsp_stop(player); 803 | return NULL; 804 | } 805 | 806 | 807 | /*****************\ 808 | * OUTPUT HANDLING * 809 | \*****************/ 810 | 811 | static void fsp_on_process(void *userdata) 812 | { 813 | struct fsp_player *player = userdata; 814 | struct pw_buffer *buf; 815 | struct spa_buffer *spa_buf; 816 | float *dest; 817 | size_t bytes_to_write; 818 | uint32_t n_frames; 819 | 820 | /* Should we terminate ? */ 821 | if (player->state == FSP_STATE_STOPPING) { 822 | fsp_stop(player); 823 | return; 824 | } 825 | 826 | /* Early return if no buffer available */ 827 | buf = pw_stream_dequeue_buffer(player->stream); 828 | if (!buf) { 829 | utils_wrn(PLR, "Pipewire output buffer overrun\n"); 830 | return; 831 | } 832 | 833 | spa_buf = buf->buffer; 834 | dest = spa_buf->datas[0].data; 835 | if (!dest) 836 | goto queue_buffer; 837 | 838 | 839 | size_t stride = sizeof(float) * FSP_OUTPUT_CHANNELS; 840 | n_frames = spa_buf->datas[0].maxsize / stride; 841 | if (buf->requested) 842 | n_frames = SPA_MIN((int)buf->requested, n_frames); 843 | bytes_to_write = n_frames * stride; 844 | 845 | /* Output silence if paused/stopped */ 846 | if (player->state == FSP_STATE_PAUSED || player->state == FSP_STATE_STOPPED) { 847 | memset(dest, 0, bytes_to_write); 848 | goto set_buffer_meta; 849 | } 850 | 851 | /* Handle state transitions */ 852 | if (player->state == FSP_STATE_PAUSING && !player->fader.state_fade_active) { 853 | utils_dbg(PLR, "Starting fade out for pause\n"); 854 | fsp_fader_state_fade_start(&player->fader, 0); 855 | } else if (player->state == FSP_STATE_RESUMING && !player->fader.state_fade_active) { 856 | utils_dbg(PLR, "Starting fade in for resume\n"); 857 | fsp_fader_state_fade_start(&player->fader, 1); 858 | } 859 | 860 | /* Check for data availability */ 861 | if (jack_ringbuffer_read_space(player->ring) < bytes_to_write) { 862 | memset(dest, 0, bytes_to_write); 863 | if (player->state == FSP_STATE_PLAYING) { 864 | utils_wrn(PLR, "Decoder ring buffer underrun: needed %u bytes, available %zu\n", 865 | bytes_to_write, jack_ringbuffer_read_space(player->ring)); 866 | } 867 | } else { 868 | /* Read data from ring buffer and notify the decoder about it */ 869 | jack_ringbuffer_read(player->ring, (char *)dest, bytes_to_write); 870 | pthread_cond_signal(&player->space_available); 871 | 872 | /* Apply state fade if active */ 873 | if (player->fader.state_fade_active) { 874 | float fade_gain = fsp_fader_state_fade_step(&player->fader, 875 | n_frames, 876 | player->state == FSP_STATE_RESUMING); 877 | 878 | /* Apply fade gain */ 879 | float *samples = dest; 880 | for (size_t i = 0; i < n_frames * FSP_OUTPUT_CHANNELS; i++) { 881 | samples[i] *= fade_gain; 882 | } 883 | 884 | /* Check if fade is complete */ 885 | if (!player->fader.state_fade_active) { 886 | if (player->state == FSP_STATE_PAUSING) { 887 | player->state = FSP_STATE_PAUSED; 888 | utils_dbg(PLR, "Fade out complete, now paused\n"); 889 | } else if (player->state == FSP_STATE_RESUMING) { 890 | player->state = FSP_STATE_PLAYING; 891 | utils_dbg(PLR, "Fade in complete, now playing\n"); 892 | } 893 | } 894 | } 895 | 896 | } 897 | 898 | set_buffer_meta: 899 | spa_buf->datas[0].chunk->offset = 0; 900 | spa_buf->datas[0].chunk->stride = stride; 901 | spa_buf->datas[0].chunk->size = bytes_to_write; 902 | 903 | queue_buffer: 904 | pw_stream_queue_buffer(player->stream, buf); 905 | } 906 | 907 | static const struct pw_stream_events stream_events = { 908 | PW_VERSION_STREAM_EVENTS, 909 | .process = fsp_on_process, 910 | }; 911 | 912 | static void fsp_stream_cleanup(struct fsp_player *player) 913 | { 914 | if (!player) 915 | return; 916 | 917 | if (player->stream) { 918 | pw_stream_destroy(player->stream); 919 | player->stream = NULL; 920 | } 921 | } 922 | 923 | static int fsp_stream_init(struct fsp_player *player) 924 | { 925 | struct pw_properties *props; 926 | uint8_t buffer[1024]; 927 | struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); 928 | const struct spa_pod *params[1]; 929 | 930 | props = pw_properties_new( 931 | PW_KEY_MEDIA_TYPE, "Audio", 932 | PW_KEY_MEDIA_CATEGORY, "Playback", 933 | PW_KEY_MEDIA_ROLE, "Music", 934 | PW_KEY_APP_NAME, "Audio Scheduler", 935 | PW_KEY_NODE_NAME, "audio_scheduler", 936 | PW_KEY_NODE_LATENCY, "1024/48000", 937 | PW_KEY_NODE_WANT_DRIVER, "false", 938 | NULL); 939 | 940 | if (!props) { 941 | utils_err(PLR, "Failed to create stream properties\n"); 942 | goto cleanup; 943 | } 944 | 945 | player->stream = pw_stream_new_simple(pw_main_loop_get_loop(player->loop), 946 | "audio-scheduler", props, 947 | &stream_events, player); 948 | 949 | if (!player->stream) { 950 | utils_err(PLR, "Failed to create stream\n"); 951 | goto cleanup; 952 | } 953 | 954 | /* Setup fixed audio format */ 955 | params[0] = spa_format_audio_raw_build(&b, 956 | SPA_PARAM_EnumFormat, 957 | &SPA_AUDIO_INFO_RAW_INIT( 958 | .format = SPA_AUDIO_FORMAT_F32, 959 | .channels = FSP_OUTPUT_CHANNELS, 960 | .rate = FSP_OUTPUT_SAMPLE_RATE, 961 | .position = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR } 962 | )); 963 | 964 | if (pw_stream_connect(player->stream, 965 | PW_DIRECTION_OUTPUT, 966 | PW_ID_ANY, 967 | PW_STREAM_FLAG_MAP_BUFFERS | 968 | PW_STREAM_FLAG_RT_PROCESS, 969 | params, 1) < 0) { 970 | utils_err(PLR, "Failed to connect stream\n"); 971 | goto cleanup; 972 | } 973 | 974 | return 0; 975 | 976 | cleanup: 977 | fsp_stream_cleanup(player); 978 | return -1; 979 | } 980 | 981 | 982 | /***********************\ 983 | * META HANDLER CALLBACK * 984 | \***********************/ 985 | 986 | int fsp_mh_cb(struct audiofile_info *cur, struct audiofile_info *next, 987 | uint32_t *elapsed_sec, void *player_data) 988 | { 989 | struct fsp_player *player = player_data; 990 | 991 | if(elapsed_sec) 992 | *elapsed_sec = player->current.samples_played / 993 | (FSP_OUTPUT_SAMPLE_RATE * FSP_OUTPUT_CHANNELS); 994 | 995 | if(!cur && !next) 996 | return 0; 997 | 998 | pthread_mutex_lock(&player->file_mutex); 999 | if(cur) 1000 | mldr_copy_audiofile(cur, &player->current.info); 1001 | if(next) 1002 | mldr_copy_audiofile(next, &player->next.info); 1003 | pthread_mutex_unlock(&player->file_mutex); 1004 | return 0; 1005 | } 1006 | 1007 | /**************\ 1008 | * ENTRY POINTS * 1009 | \**************/ 1010 | 1011 | void fsp_stop(struct fsp_player *player) 1012 | { 1013 | if (!player || player->state == FSP_STATE_STOPPED 1014 | || player->state == FSP_STATE_STOPPING) 1015 | return; 1016 | 1017 | player->state = FSP_STATE_STOPPING; 1018 | utils_dbg(PLR, "Stopping\n"); 1019 | 1020 | /* Stop pipewire loop */ 1021 | pw_main_loop_quit(player->loop); 1022 | 1023 | /* Signal threads to stop */ 1024 | pthread_cond_signal(&player->scheduler_cond); 1025 | pthread_cond_signal(&player->decoder_cond); 1026 | pthread_cond_signal(&player->space_available); 1027 | 1028 | /* Wait for threads */ 1029 | pthread_join(player->scheduler_thread, NULL); 1030 | pthread_join(player->decoder_thread, NULL); 1031 | 1032 | player->state = FSP_STATE_STOPPED; 1033 | utils_dbg(PLR, "Player stopped\n"); 1034 | } 1035 | 1036 | int fsp_start(struct fsp_player *player) 1037 | { 1038 | utils_dbg(PLR, "Starting\n"); 1039 | 1040 | if (player->state != FSP_STATE_STOPPED) { 1041 | utils_err(PLR, "Player not in stopped state\n"); 1042 | return -1; 1043 | } 1044 | 1045 | /* Reset state */ 1046 | player->state = FSP_STATE_RESUMING; 1047 | 1048 | /* Start scheduler thread */ 1049 | if (pthread_create(&player->scheduler_thread, NULL, fsp_scheduler_thread, player) != 0) { 1050 | utils_err(PLR, "Failed to create scheduler thread\n"); 1051 | return -1; 1052 | } 1053 | 1054 | /* Start decoder thread */ 1055 | if (pthread_create(&player->decoder_thread, NULL, fsp_decoder_thread, player) != 0) { 1056 | utils_err(PLR, "Failed to create decoder thread\n"); 1057 | player->state = FSP_STATE_STOPPING; 1058 | pthread_join(player->scheduler_thread, NULL); 1059 | return -1; 1060 | } 1061 | 1062 | /* Start loop */ 1063 | if (pw_main_loop_run(player->loop) < 0) { 1064 | utils_err(PLR, "Failed to start pipewire loop"); 1065 | player->state = FSP_STATE_STOPPING; 1066 | pthread_join(player->scheduler_thread, NULL); 1067 | pthread_join(player->decoder_thread, NULL); 1068 | return -1; 1069 | } 1070 | 1071 | utils_dbg(PLR, "Exit from pw_main_loop\n"); 1072 | fsp_stop(player); 1073 | return 0; 1074 | } 1075 | 1076 | void fsp_cleanup(struct fsp_player *player) 1077 | { 1078 | if (!player) 1079 | return; 1080 | 1081 | utils_dbg(PLR, "Destroying player\n"); 1082 | 1083 | /* Stop playback if running */ 1084 | fsp_stop(player); 1085 | 1086 | /* Cleanup files */ 1087 | fsp_audiofile_ctx_cleanup(&player->current); 1088 | fsp_audiofile_ctx_cleanup(&player->next); 1089 | 1090 | /* Cleanup ring buffer */ 1091 | if (player->ring) 1092 | jack_ringbuffer_free(player->ring); 1093 | 1094 | /* Cleanup pipewire */ 1095 | fsp_stream_cleanup(player); 1096 | if (player->loop) 1097 | pw_main_loop_destroy(player->loop); 1098 | 1099 | /* Cleanup synchronization primitives */ 1100 | pthread_mutex_destroy(&player->file_mutex); 1101 | pthread_mutex_destroy(&player->scheduler_mutex); 1102 | pthread_mutex_destroy(&player->decoder_mutex); 1103 | pthread_cond_destroy(&player->scheduler_cond); 1104 | pthread_cond_destroy(&player->decoder_cond); 1105 | pthread_cond_destroy(&player->space_available); 1106 | 1107 | pw_deinit(); 1108 | 1109 | utils_dbg(PLR, "Player destroyed\n"); 1110 | } 1111 | 1112 | int fsp_init(struct fsp_player *player, struct scheduler *sched, struct meta_handler *mh, struct sig_dispatcher *sd) 1113 | { 1114 | memset(player, 0, sizeof(struct fsp_player)); 1115 | 1116 | /* Initialize synchronization primitives */ 1117 | pthread_mutexattr_t attr; 1118 | pthread_mutexattr_init(&attr); 1119 | pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); 1120 | pthread_mutex_init(&player->file_mutex, &attr); 1121 | pthread_mutex_init(&player->scheduler_mutex, &attr); 1122 | pthread_mutex_init(&player->decoder_mutex, &attr); 1123 | pthread_cond_init(&player->scheduler_cond, NULL); 1124 | pthread_cond_init(&player->decoder_cond, NULL); 1125 | pthread_cond_init(&player->space_available, NULL); 1126 | 1127 | /* Create ring buffer */ 1128 | size_t ring_size = FSP_RING_BUFFER_SECONDS * FSP_OUTPUT_SAMPLE_RATE * 1129 | FSP_OUTPUT_CHANNELS * sizeof(float); 1130 | player->ring = jack_ringbuffer_create(ring_size); 1131 | if (!player->ring) { 1132 | utils_err(PLR, "Failed to create ring buffer\n"); 1133 | goto cleanup; 1134 | } 1135 | 1136 | /* Make ring buffer memory lock-resident */ 1137 | jack_ringbuffer_mlock(player->ring); 1138 | 1139 | player->sched = sched; 1140 | player->state = FSP_STATE_STOPPED; 1141 | 1142 | /* Register with media handler and signal dispatcher */ 1143 | mh_register_state_callback(mh, fsp_mh_cb, player); 1144 | sig_dispatcher_register(sd, SIG_UNIT_PLAYER, fsp_signal_handler, player); 1145 | 1146 | fsp_state_fader_setup(&player->fader); 1147 | 1148 | /* Initialize pipewire */ 1149 | pw_init(NULL, NULL); 1150 | 1151 | /* Create loop */ 1152 | player->loop = pw_main_loop_new(NULL); 1153 | if (!player->loop) { 1154 | utils_err(PLR, "Failed to create pipewire loop"); 1155 | goto cleanup; 1156 | } 1157 | 1158 | /* Create and connect stream */ 1159 | if (fsp_stream_init(player) < 0) 1160 | goto cleanup; 1161 | 1162 | utils_dbg(PLR, "Initialized\n"); 1163 | return 0; 1164 | 1165 | cleanup: 1166 | fsp_cleanup(player); 1167 | return -1; 1168 | } 1169 | --------------------------------------------------------------------------------