diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7ea96b7 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,60 @@ +cmake_minimum_required(VERSION 2.8.3) +project(audio_file_player) + +find_package(catkin REQUIRED COMPONENTS + rospy + actionlib_msgs +) + +################################################ +## Declare ROS messages, services and actions ## +################################################ + + +# Generate actions in the 'action' folder +add_action_files( + FILES + AudioFilePlay.action +) + +# Generate added messages and services with any dependencies listed here +generate_messages( + DEPENDENCIES + actionlib_msgs +) + + +################################### +## catkin specific configuration ## +################################### +catkin_package( + CATKIN_DEPENDS actionlib_msgs +) + +########### +## Build ## +########### + +## Specify additional locations of header files +## Your package locations should be listed before other locations +# include_directories(include) +include_directories( + ${catkin_INCLUDE_DIRS} +) + +############# +## Install ## +############# + +# Mark executable scripts (Python etc.) for installation +# in contrast to setup.py, you can choose the destination +install(PROGRAMS + scripts/play_file_server.py + DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) + +# Mark other files for installation (e.g. launch and bag files, etc.) +foreach (dir launch assets) + install(DIRECTORY ${dir}/ + DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/${dir}) +endforeach(dir) diff --git a/README.md b/README.md new file mode 100644 index 0000000..db8eec2 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# audio_file_player +audio_file_player offers a simple way to play audio files through +an action server interface and a topic interface. + +By default it uses sox's `play` to play the audio files (as it plays more file types than for example `aplay`). +You may need to install: +```` +sudo apt-get install sox libsox-fmt-all +```` + +# Example usage + +You can find an example mp3 file in this package in the folder assets, called `tada.mp3`. + +Launch the node: + + roslaunch audio_file_player audio_file_player.launch + +Try the topic interface: + + rostopic pub /audio_file_player/play std_msgs/String `rospack find audio_file_player`/assets/tada.mp3 + +Try the actionlib interface: + + rosrun actionlib axclient.py /audio_file_player + +# Use your own playing script +If you want to use your own command for playing the files (or even use this node for some other +purpose) just modify the launch file to use the command and flags that you want: + +```` + + + + + + + + +```` diff --git a/action/AudioFilePlay.action b/action/AudioFilePlay.action new file mode 100644 index 0000000..ee56bdd --- /dev/null +++ b/action/AudioFilePlay.action @@ -0,0 +1,13 @@ +# Path to the audio file +string filepath +--- +# Result information +# If it was successful +bool success +# If it wasn't reason why it wasn't +string reason +# Total time the file was playing +time total_time +--- +# Feedback about the amount of time the audio has been played +time elapsed_played_time diff --git a/assets/tada.mp3 b/assets/tada.mp3 new file mode 100644 index 0000000..6aa04a1 Binary files /dev/null and b/assets/tada.mp3 differ diff --git a/launch/audio_file_player.launch b/launch/audio_file_player.launch new file mode 100644 index 0000000..f2c9e1f --- /dev/null +++ b/launch/audio_file_player.launch @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/launch/audio_file_player_aplay.launch b/launch/audio_file_player_aplay.launch new file mode 100644 index 0000000..f4820d5 --- /dev/null +++ b/launch/audio_file_player_aplay.launch @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/package.xml b/package.xml new file mode 100644 index 0000000..379fdb4 --- /dev/null +++ b/package.xml @@ -0,0 +1,19 @@ + + + audio_file_player + 0.0.1 + The audio_file_player package contains a node to play audio files + through an action server or a topic interface. + + Sammy Pfeiffer + Sammy Pfeiffer + + BSD + + catkin + rospy + actionlib_msgs + rospy + actionlib_msgs + + \ No newline at end of file diff --git a/scripts/play_file_server.py b/scripts/play_file_server.py new file mode 100755 index 0000000..bb040c5 --- /dev/null +++ b/scripts/play_file_server.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python + +import subprocess +import tempfile +import os +import signal +import time +import rospy +from actionlib import SimpleActionServer +from audio_file_player.msg import AudioFilePlayAction, AudioFilePlayGoal, AudioFilePlayResult, AudioFilePlayFeedback +from std_msgs.msg import String + + +class ShellCmd: + """Helpful class to spawn commands and keep track of them""" + + def __init__(self, cmd): + self.retcode = None + self.outf = tempfile.NamedTemporaryFile(mode="w") + self.errf = tempfile.NamedTemporaryFile(mode="w") + self.inf = tempfile.NamedTemporaryFile(mode="r") + self.process = subprocess.Popen(cmd, shell=True, stdin=self.inf, + stdout=self.outf, stderr=self.errf, + preexec_fn=os.setsid, close_fds=True) + + def __del__(self): + if not self.is_done(): + self.kill() + self.outf.close() + self.errf.close() + self.inf.close() + + def get_stdout(self): + with open(self.outf.name, "r") as f: + return f.read() + + def get_stderr(self): + with open(self.errf.name, "r") as f: + return f.read() + + def get_retcode(self): + """Get retcode or None if still running""" + if self.retcode is None: + self.retcode = self.process.poll() + return self.retcode + + def is_done(self): + return self.get_retcode() is not None + + def is_succeeded(self): + """Check if the process ended with success state (retcode 0) + If the process hasn't finished yet this will be False.""" + return self.get_retcode() == 0 + + def kill(self): + self.retcode = -1 + os.killpg(self.process.pid, signal.SIGTERM) + self.process.wait() + + +class AudioFilePlayer(object): + def __init__(self): + rospy.loginfo("Initializing AudioFilePlayer...") + self.current_playing_process = None + self.afp_as = SimpleActionServer(rospy.get_name(), AudioFilePlayAction, + self.as_cb, auto_start=False) + self.afp_sub = rospy.Subscriber('~play', String, self.topic_cb, + queue_size=1) + # By default this node plays files using the aplay command + # Feel free to use any other command or flags + # by using the params provided + self.command = rospy.get_param('~/command', 'play') + self.flags = rospy.get_param('~/flags', '') + self.feedback_rate = rospy.get_param('~/feedback_rate', 10) + self.afp_as.start() + # Needs to be done after start + self.afp_as.register_preempt_callback(self.as_preempt_cb) + + rospy.loginfo( + "Done, playing files from action server or topic interface.") + + def as_preempt_cb(self): + if self.current_playing_process: + self.current_playing_process.kill() + # Put the AS as cancelled/preempted + res = AudioFilePlayResult() + res.success = False + res.reason = "Got a cancel request." + self.afp_as.set_preempted(res, text="Cancel requested.") + + def as_cb(self, goal): + initial_time = time.time() + self.play_audio_file(goal.filepath) + r = rospy.Rate(self.feedback_rate) + while not rospy.is_shutdown() and not self.current_playing_process.is_done(): + feedback = AudioFilePlayFeedback() + curr_time = time.time() + feedback.elapsed_played_time = rospy.Duration( + curr_time - initial_time) + self.afp_as.publish_feedback(feedback) + r.sleep() + + final_time = time.time() + res = AudioFilePlayResult() + if self.current_playing_process.is_succeeded(): + res.success = True + res.total_time = rospy.Duration(final_time) + self.afp_as.set_succeeded(res) + else: + if self.afp_as.is_preempt_requested(): + return + res.success = False + reason = "stderr: " + self.current_playing_process.get_stderr() + reason += "\nstdout: " + self.current_playing_process.get_stdout() + res.reason = reason + self.afp_as.set_aborted(res) + + def topic_cb(self, data): + if self.current_playing_process: + if not self.current_playing_process.is_done(): + self.current_playing_process.kill() + self.play_audio_file(data.data) + + def play_audio_file(self, audio_file_path): + # Replace any ' or " characters with emptyness to avoid bad usage + audio_file_path = audio_file_path.replace("'", "") + audio_file_path = audio_file_path.replace('"', '') + full_command = self.command + " " + self.flags + " '" + audio_file_path + "'" + rospy.loginfo("Playing audio file: " + str(audio_file_path) + + " with command: " + str(full_command)) + self.current_playing_process = ShellCmd(full_command) + + +if __name__ == '__main__': + rospy.init_node('audio_file_player') + afp = AudioFilePlayer() + rospy.spin()