diff --git a/CHANGELOG.md b/CHANGELOG.md index f7845ec..f2fa7e4 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -538,3 +538,13 @@ November 4th, 2024 - Updated real-time mode. - Fixed an issue in real-time mode when no license plate validation templates were set. - Previously, the second most likely plate guess would be accepted, rather than the top guess. +- Updated audio output. + - Added support for multiple audio player back-ends. + - In addition to supporting the original `mpg321` back-end, Predator now also supports `mplayer`. + - Moved configuration values. + - The `sounds` configuration section has been renamed, moved, and re-organized. + - Added more sounds. + - `gps_connected` is played when the GPS gains a location fix. + - `gps_disconnected` is played when the GPS loses the location fix. + - `gps_fault` is played when the GPS encounters a problem. + - `dashcam_saved` is played when a dash-cam video segment is saved. diff --git a/assets/support/configdefault.json b/assets/support/configdefault.json index e0ab835..fd76a6f 100755 --- a/assets/support/configdefault.json +++ b/assets/support/configdefault.json @@ -55,6 +55,69 @@ "alpr_detection": "[U]&R=255&G=128&B=0", "dashcam_save": "[U]&R=0&G=0&B=255" } + }, + "audio": { + "enabled": false, + "player": { + "backend": "mplayer", + "mpg321": { + }, + "mplayer": { + "device": "alsa:device=hw=0.0" + } + }, + "sounds": { + "startup": { + "path": "./assets/sounds/startup.mp3", + "repeat": 0, + "delay": 0.3 + }, + "alpr_notification": { + "path": "./assets/sounds/platedetected.mp3", + "repeat": 1, + "delay": 0.3 + }, + "alpr_alert": { + "path": "./assets/sounds/alerthit.mp3", + "repeat": 1, + "delay": 2.5 + }, + "gps_connected": { + "path": "./assets/sounds/voice/gps_connected.mp3", + "repeat": 1, + "delay": 1 + }, + "gps_disconnected": { + "path": "./assets/sounds/voice/gps_disconnected.mp3", + "repeat": 1, + "delay": 1 + }, + "gps_fault": { + "path": "./assets/sounds/voice/gps_fault.mp3", + "repeat": 1, + "delay": 1 + }, + "dashcam_saved": { + "path": "./assets/sounds/voice/video_saved.mp3", + "repeat": 1, + "delay": 1 + }, + "message_notice": { + "path": "./assets/sounds/voice/notice.mp3", + "repeat": 1, + "delay": 1 + }, + "message_warning": { + "path": "./assets/sounds/voice/warning.mp3", + "repeat": 1, + "delay": 1 + }, + "message_error": { + "path": "./assets/sounds/voice/error.mp3", + "repeat": 1, + "delay": 1 + } + } } }, "management": { @@ -107,23 +170,6 @@ } } }, - "sounds": { - "startup_sound": { - "path": "./assets/sounds/startup.mp3", - "repeat": 0, - "delay": 0.3 - }, - "notification_sound": { - "path": "./assets/sounds/platedetected.mp3", - "repeat": 1, - "delay": 0.3 - }, - "alert_sound": { - "path": "./assets/sounds/alerthit.mp3", - "repeat": 1, - "delay": 2.5 - } - }, "saving": { "remote_alert_sources": { "enabled": true, diff --git a/assets/support/configoutline.json b/assets/support/configoutline.json index 2c79791..872b73a 100755 --- a/assets/support/configoutline.json +++ b/assets/support/configoutline.json @@ -51,6 +51,69 @@ "alpr_detection": "str", "dashcam_save": "str" } + }, + "audio": { + "enabled": "bool", + "player": { + "backend": ["mpg321", "mplayer"], + "mpg321": { + }, + "mplayer": { + "device": "str" + } + }, + "sounds": { + "startup": { + "path": "file", + "repeat": "+int", + "delay": "+float" + }, + "alpr_notification": { + "path": "file", + "repeat": "+int", + "delay": "+float" + }, + "alpr_alert": { + "path": "file", + "repeat": "+int", + "delay": "+float" + }, + "gps_connected": { + "path": "file", + "repeat": "+int", + "delay": "+float" + }, + "gps_disconnected": { + "path": "file", + "repeat": "+int", + "delay": "+float" + }, + "gps_fault": { + "path": "file", + "repeat": "+int", + "delay": "+float" + }, + "dashcam_saved": { + "path": "file", + "repeat": "+int", + "delay": "+float" + }, + "message_notice": { + "path": "file", + "repeat": "+int", + "delay": "+float" + }, + "message_warning": { + "path": "file", + "repeat": "+int", + "delay": "+float" + }, + "message_error": { + "path": "file", + "repeat": "+int", + "delay": "+float" + } + } } }, "management": { @@ -101,23 +164,6 @@ "devices": "dict" } }, - "sounds": { - "startup_sound": { - "path": "file", - "repeat": "+int", - "delay": "+float" - }, - "notification_sound": { - "path": "file", - "repeat": "+int", - "delay": "+float" - }, - "alert_sound": { - "path": "file", - "repeat": "+int", - "delay": "+float" - } - }, "saving": { "remote_alert_sources": { "enabled": "bool", diff --git a/docs/CONFIGURE.md b/docs/CONFIGURE.md index fe26538..fa34fd2 100755 --- a/docs/CONFIGURE.md +++ b/docs/CONFIGURE.md @@ -96,6 +96,35 @@ This document describes the configuration values found `config.json`. - The `alpr_detection` status is relevant in real-time mode, and used when Predator detects any valid license plate. - The `dashcam_save` status is relevant to dash-cam mode, and is triggered when a dashcam video file is saved using the dashcam lock trigger file. - The lighting will remain in the "dashcam_save" status until the segment has ended, and Predator returns to normal, unlocked recording. + - `audio` contains settings that control audio play-back. + - `enabled` is a boolean value that determines whether audio playback is enabled. + - `player` contains settings for controlling how sounds are played. + - `backend` sets the audio player back-end Predator will use. + - This value can only be set to one of these values: "mpg321", "mplayer" + - `mpg321` contains settings for the MPG321 player back-end (if it is selected). + - `mplayer` contains settings for the MPlayer player back-end (if it is selected). + - `device` is a string that specifies the audio device that will be used for audio playback. + - Leaving this value blank will use the default audio playback device. + - `sounds` contains the sound effects that Predator can play when certain events occur. + - Each entry in this section has 3 attributes. + - The `path` value should be set to the file path of the audio file you want to play. + - The `repeat` value should be set to how many times you want the sound effect to be repeated. + - To disable a sound from playing, set this to 0. + - Under normal circumstances, this value should just be "1", but there might be some cases in which you want to play a particular sound repeatedly. + - The `delay` value determines how long Predator will wait, in seconds, between repetitions, if `reptition` is set to more than 1. + - Note that this delay includes the time it takes for the previous instances of the sound effect to play. + - For example, if the audio clip you're repeating takes 2 seconds to play, and you want a 1 second delay between audio clips, this setting should be 3 seconds. + - If the delay is set to zero, then all of the repetitions will play over top of each-other. + - Each entry in this section corresponds to a sound effect. + - `startup` is the sound played just after Predator finishes loading. + - `alpr_notification` is the sound played when a valid plate is detected in real-time mode, and the plate is not in an alert database. + - `alpr_alert` is the sound played when a valid plate is detected, and the plate is in an alert database. + - `gps_connected` is played when the GPS finds a 2D or 3D location fix. + - `gps_disconnected` is played when the GPS loses the fix. + - `gps_fault` is played when the GPS encounters an error. + - `message_notice` is played when a "notice" level message is displayed. + - `message_warning` is played when a "warning" level message is displayed. + - `message_error` is played when a "error" level message is displayed. - `management` contains configuration values related to management mode. - `disk_statistics` is a boolean that enables and disables the disk statistics feature of management mode. - Setting this to `false` disables disk statistics, and eliminates the need for the 'psutil' Python package to be installed. @@ -146,23 +175,6 @@ This document describes the configuration values found `config.json`. - `camera` contains settings related to image capture. - `device` specifies the camera device that will be used to capture images. - Example: `"/dev/video0"` - - `sounds` contains the sound effects that Predator can play when certain events occur. - - Each entry in this section has 3 attributes. - - The `path` value should be set to the file path of the audio file you want to play. - - The `repeat` value should be set to how many times you want the sound effect to be repeated. - - To disable a sound from playing, set this to 0. - - Under normal circumstances, this value should just be "1", but there might be some cases in which you want to play a particular sound repeatedly. - - The `device` determines the name of the device that Predator will use to record audio. - - You can get a list of the names of the available sound inputs by running the `arecord -L` command. - - Leaving this value as a blank string will cause Predator to use the default audio input determined by `arecord`. - - The `delay` value determines how long Predator will wait, in seconds, between repetitions, if `reptition` is set to more than 1. - - Note that this delay includes the time it takes for the previous instances of the sound effect to play. - - For example, if the audio clip you're repeating takes 2 seconds to play, and you want a 1 second delay between audio clips, this setting should be 3 seconds. - - If the delay is set to zero, then all of the repetitions will play over top of each-other. - - Each entry in this section corresponds to a sound effect. - - `startup_sound` is the sound played just after Predator finishes loading. - - `notification_sound` is the sound played when a valid plate is detected in real-time mode, and the plate is not in an alert database. - - `alert_sound` is the sound played when a valid plate is detected, and the plate is in an alert database. - `saving` contains settings related to information logging while operating in real-time mode. - `license_plates` contains settings related to saving detected license plates. - `enabled` is a boolean value that determines whether license plate saving is enabled. @@ -246,7 +258,9 @@ This document describes the configuration values found `config.json`. - `audio` contains settings for configuring Predator's audio recording behavior. - `enabled` is a boolean that determines whether or not audio will be recorded during dash-cam operation. - `extension` sets the file extension that audio will be saved with. - - `device` specifies the device ID (as determined by `arecord --list-pcms`) that will be used to capture audio. + - `device` determines the name of the device that Predator will use to record audio. + - You can get a list of the names of the available sound inputs by running the `arecord -L` command. + - Leaving this value as a blank string will cause Predator to use the default audio input determined by `arecord`. - `format` specifies the format that will be used by the `arecord` process. - `merge` is a boolean that determines whether or not Predator will merge the separate audio and video files when each segment is done recording. - `record_as_user` specifies the user on the system that the audio recording process will be run as. diff --git a/main.py b/main.py index 1b4d48f..3a301d7 100755 --- a/main.py +++ b/main.py @@ -39,7 +39,6 @@ clear = utils.clear # Load the screen clearing function from the utils script. prompt = utils.prompt # Load the user input prompt function from the utils script. is_json = utils.is_json # Load the function used to determine if a given string is valid JSON. -play_sound = utils.play_sound # Load the function used to play sounds from the utils script. display_message = utils.display_message # Load the message display function from the utils script. process_gpx = utils.process_gpx # Load the GPX processing function from the utils script. save_to_file = utils.save_to_file # Load the file saving function from the utils script. @@ -204,7 +203,7 @@ if (config["general"]["display"]["startup_message"]!= ""): # Only display the line for the custom message if the user has defined one. print(config["general"]["display"]["startup_message"]) # Show the user's custom defined start-up message. -play_sound("startup") +utils.play_sound("startup") if (config["realtime"]["push_notifications"]["enabled"] == True): # Check to see if the user has push notifications enabled. debug_message("Issuing start-up push notification") @@ -239,9 +238,6 @@ config["prerecorded"]["image"]["processing"]["cropping"]["bottom_margin"] = 0 config["prerecorded"]["image"]["processing"]["cropping"]["top_margin"] = 0 -for device in config["realtime"]["image"]["camera"]["devices"]: # Iterate through each video device specified in the configuration. - if (os.path.exists(config["realtime"]["image"]["camera"]["devices"][device]) == False): # Check to make sure that a camera device points to a valid file. - display_message("The 'realtime>image>camera>devices>" + device + "' configuration value does not point to a valid file.", 3) @@ -1281,6 +1277,9 @@ elif (mode_selection == "2" and config["general"]["modes"]["enabled"]["realtime"] == True): # The user has set Predator to boot into real-time mode. debug_message("Started real-time mode") + for device in config["realtime"]["image"]["camera"]["devices"]: # Iterate through each video device specified in the configuration. + if (os.path.exists(config["realtime"]["image"]["camera"]["devices"][device]) == False): # Check to make sure that a camera device points to a valid file. + display_message("The 'realtime>image>camera>devices>" + device + "' configuration value does not point to a valid file.", 3) # Load the license plate history file. @@ -1450,7 +1449,7 @@ print("Displaying detected license plates...") for plate in new_plates_detected: - play_sound("notification") + utils.play_sound("alpr_notification") if (config["realtime"]["interface"]["display"]["output_level"] >= 2): # Only display this status message if the output level indicates to do so. print("Plates detected: " + str(len(new_plates_detected))) # Display the number of license plates detected this round. for plate in new_plates_detected: @@ -1553,7 +1552,7 @@ if (config["realtime"]["interface"]["display"]["shape_alerts"] == True): # Check to see if the user has enabled shape notifications. display_shape("triangle") # Display an ASCII triangle in the output. - play_sound("alert") # Play the alert sound, if configured to do so. + utils.play_sound("alpr_alert") # Play the alert sound, if configured to do so. if (config["realtime"]["interface"]["display"]["output_level"] >= 3): # Only display this status message if the output level indicates to do so. print("Done.\n----------") diff --git a/utils.py b/utils.py index aaf40fb..6d62bfc 100755 --- a/utils.py +++ b/utils.py @@ -379,22 +379,32 @@ def get_current_state(): else: # If the error file doesn't contain valid JSON data, then load a blank placeholder in it's place. error_log = json.loads("{}") # Load a blank placeholder dictionary. +last_message = {"notice": 0, "warning": 0, "error": 0} def display_message(message, level=1): if (level == 1): # Display the message as a notice. if (config["general"]["interface_directory"] != ""): # Check to see if the interface directory is enabled. error_log[time.time()] = {"msg": message, "type": "notice"} # Add this message to the log file, using the current time as the key. save_to_file(error_file_location, json.dumps(error_log)) # Save the modified error log to the disk as JSON data. + if (time.time() - last_message["notice"] > 10): + play_sound("message_notice") + last_message["notice"] = time.time() print("Notice: " + message) elif (level == 2): # Display the message as a warning. if (config["general"]["interface_directory"] != ""): # Check to see if the interface directory is enabled. error_log[time.time()] = {"msg": message, "type": "warn"} # Add this message to the log file, using the current time as the key. save_to_file(error_file_location, json.dumps(error_log)) # Save the modified error log to the disk as JSON data. + if (time.time() - last_message["warning"] > 10): + play_sound("message_warning") + last_message["warning"] = time.time() print(style.yellow + "Warning: " + message + style.end) prompt(style.faint + "Press enter to continue..." + style.end) elif (level == 3): # Display the message as an error. if (config["general"]["interface_directory"] != ""): # Check to see if the interface directory is enabled. error_log[time.time()] = {"msg": message, "type": "error"} # Add this message to the log file, using the current time as the key. save_to_file(error_file_location, json.dumps(error_log)) # Save the modified error log to the disk as JSON data. + if (time.time() - last_message["error"] > 10): + play_sound("message_error") + last_message["error"] = time.time() print(style.red + "Error: " + message + style.end) if (config["developer"]["hard_crash_on_error"] == True): global_variables.predator_running = False @@ -477,13 +487,21 @@ def prompt(message, optional=True, input_type=str, default=""): def play_sound(sound_id): - sound_key = sound_id + "_sound" - if (sound_key in config["realtime"]["sounds"]): # Check to make sure this sound ID actually exists in the configuration + if (sound_id in config["general"]["audio"]["sounds"]): # Check to make sure this sound ID actually exists in the configuration. debug_message("Playing '" + sound_id + "' sound") - if (int(config["realtime"]["sounds"][sound_key]["repeat"]) > 0): # Check to see if the user has audio alerts enabled. - for i in range(0, int(config["realtime"]["sounds"][sound_key]["repeat"])): # Repeat the sound several times, if the configuration says to. - os.system("mpg321 " + config["realtime"]["sounds"][sound_key]["path"] + " > /dev/null 2>&1 &") # Play the sound specified for this alert type in the configuration. - time.sleep(float(config["realtime"]["sounds"][sound_key]["delay"])) # Wait before playing the sound again. + if (config["general"]["audio"]["enabled"] == True): # Check if audio playback is enabled. + if (int(config["general"]["audio"]["sounds"][sound_id]["repeat"]) > 0): # Check to see if the user has audio alerts enabled. + for i in range(0, int(config["general"]["audio"]["sounds"][sound_id]["repeat"])): # Repeat the sound several times, if the configuration says to. + if (config["general"]["audio"]["player"]["backend"] == "mpg321"): + os.system("mpg321 \"" + config["general"]["audio"]["sounds"][sound_id]["path"] + "\" > /dev/null 2>&1 &") # Play the sound specified for this alert type in the configuration. + elif (config["general"]["audio"]["player"]["backend"] == "mplayer"): + if (len(config["general"]["audio"]["player"]["mplayer"]["device"]) == 0): + os.system("mplayer \"" + config["general"]["audio"]["sounds"][sound_id]["path"] + "\" -noconsolecontrols 2>&- 1>/dev/null &") # Play the sound specified for this alert type in the configuration. + else: + os.system("mplayer -ao " + config["general"]["audio"]["player"]["mplayer"]["device"] + " \"" + config["general"]["audio"]["sounds"][sound_id]["path"] + "\" -noconsolecontrols 2>&- 1>/dev/null &") # Play the sound specified for this alert type in the configuration. + else: + display_message("The configured audio player back-end is invalid.", 3) + time.sleep(float(config["general"]["audio"]["sounds"][sound_id]["delay"])) # Wait before playing the sound again. else: # No sound with this ID exists in the configuration database, and therefore the sound can't be played. display_message("No sound with the ID (" + str(sound_id) + ") exists in the configuration.", 3) @@ -634,6 +652,8 @@ def display_shape(shape): del demo_file_path elif (config["general"]["gps"]["enabled"] == True): # Check to see if GPS is enabled. gpsd.connect() # Connect to the GPS daemon. +last_gps_status = False # This will hold the state of the GPS during the previous location check. +last_gps_fault = 0 # This will hold a timestamp of the last time the GPS encountered a fault. def get_gps_location(): global gps_demo_gpx_data global current_state @@ -677,12 +697,26 @@ def get_gps_location(): global_time_offset = gps_time - time.time() display_message("The local system time differs significantly from the GPS time. Applied time offset of " + str(round(global_time_offset*10**3)/10**3) + " seconds.", 2) + if (position == [0,0]): + if (last_gps_status != False): + play_sound("gps_disconnected") + last_gps_status = False + else: + if (last_gps_status != True): + play_sound("gps_connected") + last_gps_status = True return position[0], position[1], speed, altitude, heading, satellites, gps_time except Exception as exception: display_message("A GPS error occurred: " + str(exception), 2) + if (time.time() - last_gps_fault > 60): + play_sound("gps_fault") + last_gps_fault = time.time() return 0.0000, 0.0000, 0.0, 0.0, 0.0, 0, 0 # Return a default placeholder location. else: # If GPS is disabled, then this function should never be called, but return a placeholder position regardless. - display_message("The `get_gps_location` function was called, even though GPS is disabled. This is a bug, and should never occur.", 2) + display_message("The `get_gps_location` function was called, even though GPS is disabled. This is a bug, and should never occur.", 3) + if (time.time() - last_gps_fault > 60): + play_sound("gps_fault") + last_gps_fault = time.time() return 0.0000, 0.0000, 0.0, 0.0, 0.0, 0, 0 # Return a default placeholder location. most_recent_gps_location = [0.0, 0.0, 0.0, 0.0, 0.0, 0, 0]