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()