merge RTP branch
authorrjk@greenend.org.uk <>
Mon, 24 Sep 2007 18:03:30 +0000 (19:03 +0100)
committerrjk@greenend.org.uk <>
Mon, 24 Sep 2007 18:03:30 +0000 (19:03 +0100)
49 files changed:
.bzrignore
CHANGES
Doxyfile [new file with mode: 0644]
README
README.streams
clients/Makefile.am
clients/playrtp-log [new file with mode: 0755]
clients/playrtp.c [new file with mode: 0644]
configure.ac
disobedience/choose.c
doc/Makefile.am
doc/disorder-playrtp.1.in [new file with mode: 0644]
doc/disorder_config.5.in
lib/Makefile.am
lib/addr.c
lib/asprintf.c
lib/authhash.c
lib/basen.c
lib/cache.c
lib/charset.c
lib/configuration.c
lib/configuration.h
lib/defs.c
lib/defs.h
lib/eclient.h
lib/heap.h [new file with mode: 0644]
lib/hex.c
lib/log-impl.h
lib/log.c
lib/log.h
lib/mem.c
lib/rtp.h [new file with mode: 0644]
lib/speaker-protocol.c [moved from lib/speaker.c with 78% similarity]
lib/speaker-protocol.h [new file with mode: 0644]
lib/speaker.h [deleted file]
lib/test.c
lib/timeval.h [new file with mode: 0644]
lib/vector.h
server/Makefile.am
server/disorderd.c
server/play.c
server/server.c
server/server.h
server/speaker-alsa.c [new file with mode: 0644]
server/speaker-command.c [new file with mode: 0644]
server/speaker-network.c [new file with mode: 0644]
server/speaker.c
server/speaker.h [new file with mode: 0644]
server/uk.org.greenend.rjk.disorder.plist.in [new file with mode: 0644]

index 2ca293c..8704579 100644 (file)
@@ -93,3 +93,9 @@ TAGS
 ktrace.out
 tests/Makefile
 tests/testroot
+disorder.plist
+server/uk.org.greenend.rjk.disorder.plist
+doc/guts
+clients/disorder-playrtp
+doc/disorder-playrtp.1.html
+doc/disorder-playrtp.1
diff --git a/CHANGES b/CHANGES
index c95c17e..ab023a3 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -38,10 +38,10 @@ crash when random play was enabled has been fixed.
 A new configuration option 'queue_pad' allows the number of random
 tracks kept on the queue to be controlled.
 
-New configuration options 'speaker_command', 'sample_format' and
-'sox_generation' direct the speaker process to send audio data to a
-subcommand instead of straight to the sound card, for instance for
-broadcasting over a network.
+** Network Play
+
+DisOrder can broadcast audio over a network, allowing it to be played on
+multiple client machines.  See README.streams for details.
 
 ** disorderfm
 
diff --git a/Doxyfile b/Doxyfile
new file mode 100644 (file)
index 0000000..3c5bb5b
--- /dev/null
+++ b/Doxyfile
@@ -0,0 +1,1252 @@
+# Doxyfile 1.5.1
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project
+#
+# All text after a hash (#) is considered a comment and will be ignored
+# The format is:
+#       TAG = value [value, ...]
+# For lists items can also be appended using:
+#       TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (" ")
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded 
+# by quotes) that should identify the project.
+
+PROJECT_NAME           = DisOrder
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. 
+# This could be handy for archiving the generated documentation or 
+# if some version control system is used.
+
+PROJECT_NUMBER         = 
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) 
+# base path where the generated documentation will be put. 
+# If a relative path is entered, it will be relative to the location 
+# where doxygen was started. If left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       = doc/guts
+
+# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create 
+# 4096 sub-directories (in 2 levels) under the output directory of each output 
+# format and will distribute the generated files over these directories. 
+# Enabling this option can be useful when feeding doxygen a huge amount of 
+# source files, where putting all generated files in the same directory would 
+# otherwise cause performance problems for the file system.
+
+CREATE_SUBDIRS         = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all 
+# documentation generated by doxygen is written. Doxygen will use this 
+# information to generate all constant output in the proper language. 
+# The default language is English, other supported languages are: 
+# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, 
+# Croatian, Czech, Danish, Dutch, Finnish, French, German, Greek, Hungarian, 
+# Italian, Japanese, Japanese-en (Japanese with English messages), Korean, 
+# Korean-en, Lithuanian, Norwegian, Polish, Portuguese, Romanian, Russian, 
+# Serbian, Slovak, Slovene, Spanish, Swedish, and Ukrainian.
+
+OUTPUT_LANGUAGE        = English
+
+# This tag can be used to specify the encoding used in the generated output. 
+# The encoding is not always determined by the language that is chosen, 
+# but also whether or not the output is meant for Windows or non-Windows users. 
+# In case there is a difference, setting the USE_WINDOWS_ENCODING tag to YES 
+# forces the Windows encoding (this is the default for the Windows binary), 
+# whereas setting the tag to NO uses a Unix-style encoding (the default for 
+# all platforms other than Windows).
+
+USE_WINDOWS_ENCODING   = NO
+
+# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will 
+# include brief member descriptions after the members that are listed in 
+# the file and class documentation (similar to JavaDoc). 
+# Set to NO to disable this.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend 
+# the brief description of a member or function before the detailed description. 
+# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the 
+# brief descriptions will be completely suppressed.
+
+REPEAT_BRIEF           = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator 
+# that is used to form the text in various listings. Each string 
+# in this list, if found as the leading text of the brief description, will be 
+# stripped from the text and the result after processing the whole list, is 
+# used as the annotated text. Otherwise, the brief description is used as-is. 
+# If left blank, the following values are used ("$name" is automatically 
+# replaced with the name of the entity): "The $name class" "The $name widget" 
+# "The $name file" "is" "provides" "specifies" "contains" 
+# "represents" "a" "an" "the"
+
+ABBREVIATE_BRIEF       = 
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then 
+# Doxygen will generate a detailed section even if there is only a brief 
+# description.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all 
+# inherited members of a class in the documentation of that class as if those 
+# members were ordinary class members. Constructors, destructors and assignment 
+# operators of the base classes will not be shown.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full 
+# path before files name in the file list and in the header files. If set 
+# to NO the shortest path that makes the file name unique will be used.
+
+FULL_PATH_NAMES        = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag 
+# can be used to strip a user-defined part of the path. Stripping is 
+# only done if one of the specified strings matches the left-hand part of 
+# the path. The tag can be used to show relative paths in the file list. 
+# If left blank the directory from which doxygen is run is used as the 
+# path to strip.
+
+STRIP_FROM_PATH        = 
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of 
+# the path mentioned in the documentation of a class, which tells 
+# the reader which header file to include in order to use a class. 
+# If left blank only the name of the header file containing the class 
+# definition is used. Otherwise one should specify the include paths that 
+# are normally passed to the compiler using the -I flag.
+
+STRIP_FROM_INC_PATH    = 
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter 
+# (but less readable) file names. This can be useful is your file systems 
+# doesn't support long names like on DOS, Mac, or CD-ROM.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen 
+# will interpret the first line (until the first dot) of a JavaDoc-style 
+# comment as the brief description. If set to NO, the JavaDoc 
+# comments will behave just like the Qt-style comments (thus requiring an 
+# explicit @brief command for a brief description.
+
+JAVADOC_AUTOBRIEF      = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen 
+# treat a multi-line C++ special comment block (i.e. a block of //! or /// 
+# comments) as a brief description. This used to be the default behaviour. 
+# The new default is to treat a multi-line C++ comment block as a detailed 
+# description. Set this tag to YES if you prefer the old behaviour instead.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the DETAILS_AT_TOP tag is set to YES then Doxygen 
+# will output the detailed description near the top, like JavaDoc.
+# If set to NO, the detailed description appears after the member 
+# documentation.
+
+DETAILS_AT_TOP         = YES
+
+# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented 
+# member inherits the documentation from any documented member that it 
+# re-implements.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce 
+# a new page for each member. If set to NO, the documentation of a member will 
+# be part of the file/class/namespace that contains it.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. 
+# Doxygen uses this value to replace tabs by spaces in code fragments.
+
+TAB_SIZE               = 8
+
+# This tag can be used to specify a number of aliases that acts 
+# as commands in the documentation. An alias has the form "name=value". 
+# For example adding "sideeffect=\par Side Effects:\n" will allow you to 
+# put the command \sideeffect (or @sideeffect) in the documentation, which 
+# will result in a user-defined paragraph with heading "Side Effects:". 
+# You can put \n's in the value part of an alias to insert newlines.
+
+ALIASES                = 
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C 
+# sources only. Doxygen will then generate output that is more tailored for C. 
+# For instance, some of the names that are used will be different. The list 
+# of all members will be omitted, etc.
+
+OPTIMIZE_OUTPUT_FOR_C  = YES
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java 
+# sources only. Doxygen will then generate output that is more tailored for Java. 
+# For instance, namespaces will be presented as packages, qualified scopes 
+# will look different, etc.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want to 
+# include (a tag file for) the STL sources as input, then you should 
+# set this tag to YES in order to let doxygen match functions declarations and 
+# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. 
+# func(std::string) {}). This also make the inheritance and collaboration 
+# diagrams that involve STL classes more complete and accurate.
+
+BUILTIN_STL_SUPPORT    = NO
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC 
+# tag is set to YES, then doxygen will reuse the documentation of the first 
+# member in the group (if any) for the other members of the group. By default 
+# all members of a group must be documented explicitly.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# Set the SUBGROUPING tag to YES (the default) to allow class member groups of 
+# the same type (for instance a group of public functions) to be put as a 
+# subgroup of that type (e.g. under the Public Functions section). Set it to 
+# NO to prevent subgrouping. Alternatively, this can be done per class using 
+# the \nosubgrouping command.
+
+SUBGROUPING            = YES
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in 
+# documentation are documented, even if no documentation was available. 
+# Private class members and static file members will be hidden unless 
+# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES
+
+EXTRACT_ALL            = YES
+
+# If the EXTRACT_PRIVATE tag is set to YES all private members of a class 
+# will be included in the documentation.
+
+EXTRACT_PRIVATE        = YES
+
+# If the EXTRACT_STATIC tag is set to YES all static members of a file 
+# will be included in the documentation.
+
+EXTRACT_STATIC         = YES
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) 
+# defined locally in source files will be included in the documentation. 
+# If set to NO only classes defined in header files are included.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. When set to YES local 
+# methods, which are defined in the implementation section but not in 
+# the interface are included in the documentation. 
+# If set to NO (the default) only methods in the interface are included.
+
+EXTRACT_LOCAL_METHODS  = YES
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all 
+# undocumented members of documented classes, files or namespaces. 
+# If set to NO (the default) these members will be included in the 
+# various overviews, but no documentation section is generated. 
+# This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all 
+# undocumented classes that are normally visible in the class hierarchy. 
+# If set to NO (the default) these classes will be included in the various 
+# overviews. This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all 
+# friend (class|struct|union) declarations. 
+# If set to NO (the default) these declarations will be included in the 
+# documentation.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any 
+# documentation blocks found inside the body of a function. 
+# If set to NO (the default) these blocks will be appended to the 
+# function's detailed documentation block.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation 
+# that is typed after a \internal command is included. If the tag is set 
+# to NO (the default) then the documentation will be excluded. 
+# Set it to YES to include the internal documentation.
+
+INTERNAL_DOCS          = YES
+
+# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate 
+# file names in lower-case letters. If set to YES upper-case letters are also 
+# allowed. This is useful if you have classes or files whose names only differ 
+# in case and if your file system supports case sensitive file names. Windows 
+# and Mac users are advised to set this option to NO.
+
+CASE_SENSE_NAMES       = YES
+
+# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen 
+# will show members with their full class and namespace scopes in the 
+# documentation. If set to YES the scope will be hidden.
+
+HIDE_SCOPE_NAMES       = YES
+
+# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen 
+# will put a list of the files that are included by a file in the documentation 
+# of that file.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] 
+# is inserted in the documentation for inline members.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen 
+# will sort the (detailed) documentation of file and class members 
+# alphabetically by member name. If set to NO the members will appear in 
+# declaration order.
+
+SORT_MEMBER_DOCS       = NO
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the 
+# brief documentation of file, namespace and class members alphabetically 
+# by member name. If set to NO (the default) the members will appear in 
+# declaration order.
+
+SORT_BRIEF_DOCS        = YES
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be 
+# sorted by fully-qualified names, including namespaces. If set to 
+# NO (the default), the class list will be sorted only by class name, 
+# not including the namespace part. 
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the 
+# alphabetical list.
+
+SORT_BY_SCOPE_NAME     = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or 
+# disable (NO) the todo list. This list is created by putting \todo 
+# commands in the documentation.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or 
+# disable (NO) the test list. This list is created by putting \test 
+# commands in the documentation.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or 
+# disable (NO) the bug list. This list is created by putting \bug 
+# commands in the documentation.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or 
+# disable (NO) the deprecated list. This list is created by putting 
+# \deprecated commands in the documentation.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional 
+# documentation sections, marked by \if sectionname ... \endif.
+
+ENABLED_SECTIONS       = 
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines 
+# the initial value of a variable or define consists of for it to appear in 
+# the documentation. If the initializer consists of more lines than specified 
+# here it will be hidden. Use a value of 0 to hide initializers completely. 
+# The appearance of the initializer of individual variables and defines in the 
+# documentation can be controlled using \showinitializer or \hideinitializer 
+# command in the documentation regardless of this setting.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated 
+# at the bottom of the documentation of classes and structs. If set to YES the 
+# list will mention the files that were used to generate the documentation.
+
+SHOW_USED_FILES        = NO
+
+# If the sources in your project are distributed over multiple directories 
+# then setting the SHOW_DIRECTORIES tag to YES will show the directory hierarchy 
+# in the documentation. The default is NO.
+
+SHOW_DIRECTORIES       = NO
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that 
+# doxygen should invoke to get the current version for each file (typically from the 
+# version control system). Doxygen will invoke the program by executing (via 
+# popen()) the command <command> <input-file>, where <command> is the value of 
+# the FILE_VERSION_FILTER tag, and <input-file> is the name of an input file 
+# provided by doxygen. Whatever the program writes to standard output 
+# is used as the file version. See the manual for examples.
+
+FILE_VERSION_FILTER    = 
+
+#---------------------------------------------------------------------------
+# configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated 
+# by doxygen. Possible values are YES and NO. If left blank NO is used.
+
+QUIET                  = YES
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are 
+# generated by doxygen. Possible values are YES and NO. If left blank 
+# NO is used.
+
+WARNINGS               = YES
+
+# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings 
+# for undocumented members. If EXTRACT_ALL is set to YES then this flag will 
+# automatically be disabled.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for 
+# potential errors in the documentation, such as not documenting some 
+# parameters in a documented function, or documenting parameters that 
+# don't exist or using markup commands wrongly.
+
+WARN_IF_DOC_ERROR      = YES
+
+# This WARN_NO_PARAMDOC option can be abled to get warnings for 
+# functions that are documented, but have no documentation for their parameters 
+# or return value. If set to NO (the default) doxygen will only warn about 
+# wrong or incomplete parameter documentation, but not about the absence of 
+# documentation.
+
+WARN_NO_PARAMDOC       = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that 
+# doxygen can produce. The string should contain the $file, $line, and $text 
+# tags, which will be replaced by the file and line number from which the 
+# warning originated and the warning text. Optionally the format may contain 
+# $version, which will be replaced by the version of the file (if it could 
+# be obtained via FILE_VERSION_FILTER)
+
+WARN_FORMAT            = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning 
+# and error messages should be written. If left blank the output is written 
+# to stderr.
+
+WARN_LOGFILE           = 
+
+#---------------------------------------------------------------------------
+# configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag can be used to specify the files and/or directories that contain 
+# documented source files. You may enter file names like "myfile.cpp" or 
+# directories like "/usr/src/myproject". Separate the files or directories 
+# with spaces.
+
+INPUT                  = .
+
+# If the value of the INPUT tag contains directories, you can use the 
+# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp 
+# and *.h) to filter out the source-files in the directories. If left 
+# blank the following patterns are tested: 
+# *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx 
+# *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.py
+
+FILE_PATTERNS          = 
+
+# The RECURSIVE tag can be used to turn specify whether or not subdirectories 
+# should be searched for input files as well. Possible values are YES and NO. 
+# If left blank NO is used.
+
+RECURSIVE              = YES
+
+# The EXCLUDE tag can be used to specify files and/or directories that should 
+# excluded from the INPUT source files. This way you can easily exclude a 
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+
+EXCLUDE                = 
+
+# The EXCLUDE_SYMLINKS tag can be used select whether or not files or 
+# directories that are symbolic links (a Unix filesystem feature) are excluded 
+# from the input.
+
+EXCLUDE_SYMLINKS       = YES
+
+# If the value of the INPUT tag contains directories, you can use the 
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude 
+# certain files from those directories. Note that the wildcards are matched 
+# against the file with absolute path, so to exclude all test directories 
+# for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       = 
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or 
+# directories that contain example code fragments that are included (see 
+# the \include command).
+
+EXAMPLE_PATH           = 
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the 
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp 
+# and *.h) to filter out the source-files in the directories. If left 
+# blank all files are included.
+
+EXAMPLE_PATTERNS       = 
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be 
+# searched for input files to be used with the \include or \dontinclude 
+# commands irrespective of the value of the RECURSIVE tag. 
+# Possible values are YES and NO. If left blank NO is used.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or 
+# directories that contain image that are included in the documentation (see 
+# the \image command).
+
+IMAGE_PATH             = 
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should 
+# invoke to filter for each input file. Doxygen will invoke the filter program 
+# by executing (via popen()) the command <filter> <input-file>, where <filter> 
+# is the value of the INPUT_FILTER tag, and <input-file> is the name of an 
+# input file. Doxygen will then use the output that the filter program writes 
+# to standard output.  If FILTER_PATTERNS is specified, this tag will be 
+# ignored.
+
+INPUT_FILTER           = 
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern 
+# basis.  Doxygen will compare the file name with each pattern and apply the 
+# filter if there is a match.  The filters are a list of the form: 
+# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further 
+# info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER 
+# is applied to all files.
+
+FILTER_PATTERNS        = 
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using 
+# INPUT_FILTER) will be used to filter the input files when producing source 
+# files to browse (i.e. when SOURCE_BROWSER is set to YES).
+
+FILTER_SOURCE_FILES    = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will 
+# be generated. Documented entities will be cross-referenced with these sources. 
+# Note: To get rid of all source code in the generated output, make sure also 
+# VERBATIM_HEADERS is set to NO.
+
+SOURCE_BROWSER         = YES
+
+# Setting the INLINE_SOURCES tag to YES will include the body 
+# of functions and classes directly in the documentation.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct 
+# doxygen to hide any special comment blocks from generated source code 
+# fragments. Normal C and C++ comments will always remain visible.
+
+STRIP_CODE_COMMENTS    = NO
+
+# If the REFERENCED_BY_RELATION tag is set to YES (the default) 
+# then for each documented function all documented 
+# functions referencing it will be listed.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES (the default) 
+# then for each documented function all documented entities 
+# called/used by that function will be listed.
+
+REFERENCES_RELATION    = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES (the default)
+# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from
+# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will
+# link to the source code.  Otherwise they will link to the documentstion.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code 
+# will point to the HTML generated by the htags(1) tool instead of doxygen 
+# built-in source browser. The htags tool is part of GNU's global source 
+# tagging system (see http://www.gnu.org/software/global/global.html). You 
+# will need version 4.8.6 or higher.
+
+USE_HTAGS              = NO
+
+# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen 
+# will generate a verbatim copy of the header file for each class for 
+# which an include is specified. Set to NO to disable this.
+
+VERBATIM_HEADERS       = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index 
+# of all compounds will be generated. Enable this if the project 
+# contains a lot of classes, structs, unions or interfaces.
+
+ALPHABETICAL_INDEX     = NO
+
+# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then 
+# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns 
+# in which this list will be split (can be a number in the range [1..20])
+
+COLS_IN_ALPHA_INDEX    = 5
+
+# In case all classes in a project start with a common prefix, all 
+# classes will be put under the same header in the alphabetical index. 
+# The IGNORE_PREFIX tag can be used to specify one or more prefixes that 
+# should be ignored while generating the index headers.
+
+IGNORE_PREFIX          = 
+
+#---------------------------------------------------------------------------
+# configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES (the default) Doxygen will 
+# generate HTML output.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `html' will be used as the default path.
+
+HTML_OUTPUT            = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for 
+# each generated HTML page (for example: .htm,.php,.asp). If it is left blank 
+# doxygen will generate files with .html extension.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a personal HTML header for 
+# each generated HTML page. If it is left blank doxygen will generate a 
+# standard header.
+
+HTML_HEADER            = 
+
+# The HTML_FOOTER tag can be used to specify a personal HTML footer for 
+# each generated HTML page. If it is left blank doxygen will generate a 
+# standard footer.
+
+HTML_FOOTER            = 
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading 
+# style sheet that is used by each HTML page. It can be used to 
+# fine-tune the look of the HTML output. If the tag is left blank doxygen 
+# will generate a default style sheet. Note that doxygen will try to copy 
+# the style sheet file to the HTML output directory, so don't put your own 
+# stylesheet in the HTML output directory as well, or it will be erased!
+
+HTML_STYLESHEET        = 
+
+# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, 
+# files or namespaces will be aligned in HTML using tables. If set to 
+# NO a bullet list will be used.
+
+HTML_ALIGN_MEMBERS     = YES
+
+# If the GENERATE_HTMLHELP tag is set to YES, additional index files 
+# will be generated that can be used as input for tools like the 
+# Microsoft HTML help workshop to generate a compressed HTML help file (.chm) 
+# of the generated HTML documentation.
+
+GENERATE_HTMLHELP      = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can 
+# be used to specify the file name of the resulting .chm file. You 
+# can add a path in front of the file if the result should not be 
+# written to the html output directory.
+
+CHM_FILE               = 
+
+# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can 
+# be used to specify the location (absolute path including file name) of 
+# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run 
+# the HTML help compiler on the generated index.hhp.
+
+HHC_LOCATION           = 
+
+# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag 
+# controls if a separate .chi index file is generated (YES) or that 
+# it should be included in the master .chm file (NO).
+
+GENERATE_CHI           = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag 
+# controls whether a binary table of contents is generated (YES) or a 
+# normal table of contents (NO) in the .chm file.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members 
+# to the contents of the HTML help documentation and to the tree view.
+
+TOC_EXPAND             = NO
+
+# The DISABLE_INDEX tag can be used to turn on/off the condensed index at 
+# top of each HTML page. The value NO (the default) enables the index and 
+# the value YES disables it.
+
+DISABLE_INDEX          = NO
+
+# This tag can be used to set the number of enum values (range [1..20]) 
+# that doxygen will group on one line in the generated HTML documentation.
+
+ENUM_VALUES_PER_LINE   = 4
+
+# If the GENERATE_TREEVIEW tag is set to YES, a side panel will be
+# generated containing a tree-like index structure (just like the one that 
+# is generated for HTML Help). For this to work a browser that supports 
+# JavaScript, DHTML, CSS and frames is required (for instance Mozilla 1.0+, 
+# Netscape 6.0+, Internet explorer 5.0+, or Konqueror). Windows users are 
+# probably better off using the HTML help feature.
+
+GENERATE_TREEVIEW      = NO
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be 
+# used to set the initial width (in pixels) of the frame in which the tree 
+# is shown.
+
+TREEVIEW_WIDTH         = 250
+
+#---------------------------------------------------------------------------
+# configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will 
+# generate Latex output.
+
+GENERATE_LATEX         = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `latex' will be used as the default path.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be 
+# invoked. If left blank `latex' will be used as the default command name.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to 
+# generate index for LaTeX. If left blank `makeindex' will be used as the 
+# default command name.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact 
+# LaTeX documents. This may be useful for small projects and may help to 
+# save some trees in general.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used 
+# by the printer. Possible values are: a4, a4wide, letter, legal and 
+# executive. If left blank a4wide will be used.
+
+PAPER_TYPE             = a4wide
+
+# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX 
+# packages that should be included in the LaTeX output.
+
+EXTRA_PACKAGES         = 
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for 
+# the generated latex document. The header should contain everything until 
+# the first chapter. If it is left blank doxygen will generate a 
+# standard header. Notice: only use this tag if you know what you are doing!
+
+LATEX_HEADER           = 
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated 
+# is prepared for conversion to pdf (using ps2pdf). The pdf file will 
+# contain links (just like the HTML output) instead of page references 
+# This makes the output suitable for online browsing using a pdf viewer.
+
+PDF_HYPERLINKS         = NO
+
+# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of 
+# plain latex in the generated Makefile. Set this option to YES to get a 
+# higher quality PDF documentation.
+
+USE_PDFLATEX           = NO
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. 
+# command to the generated LaTeX files. This will instruct LaTeX to keep 
+# running if errors occur, instead of asking the user for help. 
+# This option is also used when generating formulas in HTML.
+
+LATEX_BATCHMODE        = NO
+
+# If LATEX_HIDE_INDICES is set to YES then doxygen will not 
+# include the index chapters (such as File Index, Compound Index, etc.) 
+# in the output.
+
+LATEX_HIDE_INDICES     = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output 
+# The RTF output is optimized for Word 97 and may not look very pretty with 
+# other RTF readers or editors.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `rtf' will be used as the default path.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES Doxygen generates more compact 
+# RTF documents. This may be useful for small projects and may help to 
+# save some trees in general.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated 
+# will contain hyperlink fields. The RTF file will 
+# contain links (just like the HTML output) instead of page references. 
+# This makes the output suitable for online browsing using WORD or other 
+# programs which support those fields. 
+# Note: wordpad (write) and others do not support links.
+
+RTF_HYPERLINKS         = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's 
+# config file, i.e. a series of assignments. You only have to provide 
+# replacements, missing definitions are set to their default value.
+
+RTF_STYLESHEET_FILE    = 
+
+# Set optional variables used in the generation of an rtf document. 
+# Syntax is similar to doxygen's config file.
+
+RTF_EXTENSIONS_FILE    = 
+
+#---------------------------------------------------------------------------
+# configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES (the default) Doxygen will 
+# generate man pages
+
+GENERATE_MAN           = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `man' will be used as the default path.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to 
+# the generated man pages (default is the subroutine's section .3)
+
+MAN_EXTENSION          = .3
+
+# If the MAN_LINKS tag is set to YES and Doxygen generates man output, 
+# then it will generate one additional man file for each entity 
+# documented in the real man page(s). These additional files 
+# only source the real man page, but without them the man command 
+# would be unable to find the correct page. The default is NO.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES Doxygen will 
+# generate an XML file that captures the structure of 
+# the code including all documentation.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `xml' will be used as the default path.
+
+XML_OUTPUT             = xml
+
+# The XML_SCHEMA tag can be used to specify an XML schema, 
+# which can be used by a validating XML parser to check the 
+# syntax of the XML files.
+
+XML_SCHEMA             = 
+
+# The XML_DTD tag can be used to specify an XML DTD, 
+# which can be used by a validating XML parser to check the 
+# syntax of the XML files.
+
+XML_DTD                = 
+
+# If the XML_PROGRAMLISTING tag is set to YES Doxygen will 
+# dump the program listings (including syntax highlighting 
+# and cross-referencing information) to the XML output. Note that 
+# enabling this will significantly increase the size of the XML output.
+
+XML_PROGRAMLISTING     = YES
+
+#---------------------------------------------------------------------------
+# configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will 
+# generate an AutoGen Definitions (see autogen.sf.net) file 
+# that captures the structure of the code including all 
+# documentation. Note that this feature is still experimental 
+# and incomplete at the moment.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES Doxygen will 
+# generate a Perl module file that captures the structure of 
+# the code including all documentation. Note that this 
+# feature is still experimental and incomplete at the 
+# moment.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES Doxygen will generate 
+# the necessary Makefile rules, Perl scripts and LaTeX code to be able 
+# to generate PDF and DVI output from the Perl module output.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be 
+# nicely formatted so it can be parsed by a human reader.  This is useful 
+# if you want to understand what is going on.  On the other hand, if this 
+# tag is set to NO the size of the Perl module output will be much smaller 
+# and Perl will parse it just the same.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file 
+# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. 
+# This is useful so different doxyrules.make files included by the same 
+# Makefile don't overwrite each other's variables.
+
+PERLMOD_MAKEVAR_PREFIX = 
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor   
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will 
+# evaluate all C-preprocessor directives found in the sources and include 
+# files.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro 
+# names in the source code. If set to NO (the default) only conditional 
+# compilation will be performed. Macro expansion can be done in a controlled 
+# way by setting EXPAND_ONLY_PREDEF to YES.
+
+MACRO_EXPANSION        = YES
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES 
+# then the macro expansion is limited to the macros specified with the 
+# PREDEFINED and EXPAND_AS_DEFINED tags.
+
+EXPAND_ONLY_PREDEF     = NO
+
+# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files 
+# in the INCLUDE_PATH (see below) will be search if a #include is found.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that 
+# contain include files that are not input files but should be processed by 
+# the preprocessor.
+
+INCLUDE_PATH           = 
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard 
+# patterns (like *.h and *.hpp) to filter out the header-files in the 
+# directories. If left blank, the patterns specified with FILE_PATTERNS will 
+# be used.
+
+INCLUDE_FILE_PATTERNS  = 
+
+# The PREDEFINED tag can be used to specify one or more macro names that 
+# are defined before the preprocessor is started (similar to the -D option of 
+# gcc). The argument of the tag is a list of macros of the form: name 
+# or name=definition (no spaces). If the definition and the = are 
+# omitted =1 is assumed. To prevent a macro definition from being 
+# undefined via #undef or recursively expanded use the := operator 
+# instead of the = operator.
+
+PREDEFINED             = 
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then 
+# this tag can be used to specify a list of macro names that should be expanded. 
+# The macro definition that is found in the sources will be used. 
+# Use the PREDEFINED tag if you want to use a different macro definition.
+
+EXPAND_AS_DEFINED      = 
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then 
+# doxygen's preprocessor will remove all function-like macros that are alone 
+# on a line, have an all uppercase name, and do not end with a semicolon. Such 
+# function macros are typically used for boiler-plate code, and will confuse 
+# the parser if not removed.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration::additions related to external references   
+#---------------------------------------------------------------------------
+
+# The TAGFILES option can be used to specify one or more tagfiles. 
+# Optionally an initial location of the external documentation 
+# can be added for each tagfile. The format of a tag file without 
+# this location is as follows: 
+#   TAGFILES = file1 file2 ... 
+# Adding location for the tag files is done as follows: 
+#   TAGFILES = file1=loc1 "file2 = loc2" ... 
+# where "loc1" and "loc2" can be relative or absolute paths or 
+# URLs. If a location is present for each tag, the installdox tool 
+# does not have to be run to correct the links.
+# Note that each tag file must have a unique name
+# (where the name does NOT include the path)
+# If a tag file is not located in the directory in which doxygen 
+# is run, you must also specify the path to the tagfile here.
+
+TAGFILES               = 
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create 
+# a tag file that is based on the input files it reads.
+
+GENERATE_TAGFILE       = 
+
+# If the ALLEXTERNALS tag is set to YES all external classes will be listed 
+# in the class index. If set to NO only the inherited external classes 
+# will be listed.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed 
+# in the modules index. If set to NO, only the current project's groups will 
+# be listed.
+
+EXTERNAL_GROUPS        = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script 
+# interpreter (i.e. the result of `which perl').
+
+PERL_PATH              = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool   
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will 
+# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base 
+# or super classes. Setting the tag to NO turns the diagrams off. Note that 
+# this option is superseded by the HAVE_DOT option below. This is only a 
+# fallback. It is recommended to install and use dot, since it yields more 
+# powerful graphs.
+
+CLASS_DIAGRAMS         = YES
+
+# If set to YES, the inheritance and collaboration graphs will hide 
+# inheritance and usage relations if the target is undocumented 
+# or is not a class.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is 
+# available from the path. This tool is part of Graphviz, a graph visualization 
+# toolkit from AT&T and Lucent Bell Labs. The other options in this section 
+# have no effect if this option is set to NO (the default)
+
+HAVE_DOT               = NO
+
+# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen 
+# will generate a graph for each documented class showing the direct and 
+# indirect inheritance relations. Setting this tag to YES will force the 
+# the CLASS_DIAGRAMS tag to NO.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen 
+# will generate a graph for each documented class showing the direct and 
+# indirect implementation dependencies (inheritance, containment, and 
+# class references variables) of the class with other documented classes.
+
+COLLABORATION_GRAPH    = YES
+
+# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen 
+# will generate a graph for groups, showing the direct groups dependencies
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES doxygen will generate inheritance and 
+# collaboration diagrams in a style similar to the OMG's Unified Modeling 
+# Language.
+
+UML_LOOK               = NO
+
+# If set to YES, the inheritance and collaboration graphs will show the 
+# relations between templates and their instances.
+
+TEMPLATE_RELATIONS     = NO
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT 
+# tags are set to YES then doxygen will generate a graph for each documented 
+# file showing the direct and indirect include dependencies of the file with 
+# other documented files.
+
+INCLUDE_GRAPH          = YES
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and 
+# HAVE_DOT tags are set to YES then doxygen will generate a graph for each 
+# documented header file showing the documented files that directly or 
+# indirectly include this file.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH and HAVE_DOT tags are set to YES then doxygen will 
+# generate a call dependency graph for every global function or class method. 
+# Note that enabling this option will significantly increase the time of a run. 
+# So in most cases it will be better to enable call graphs for selected 
+# functions only using the \callgraph command.
+
+CALL_GRAPH             = NO
+
+# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then doxygen will 
+# generate a caller dependency graph for every global function or class method. 
+# Note that enabling this option will significantly increase the time of a run. 
+# So in most cases it will be better to enable caller graphs for selected 
+# functions only using the \callergraph command.
+
+CALLER_GRAPH           = NO
+
+# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen 
+# will graphical hierarchy of all classes instead of a textual one.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES 
+# then doxygen will show the dependencies a directory has on other directories 
+# in a graphical way. The dependency relations are determined by the #include
+# relations between the files in the directories.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images 
+# generated by dot. Possible values are png, jpg, or gif
+# If left blank png will be used.
+
+DOT_IMAGE_FORMAT       = png
+
+# The tag DOT_PATH can be used to specify the path where the dot tool can be 
+# found. If left blank, it is assumed the dot tool can be found in the path.
+
+DOT_PATH               = 
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that 
+# contain dot files that are included in the documentation (see the 
+# \dotfile command).
+
+DOTFILE_DIRS           = 
+
+# The MAX_DOT_GRAPH_WIDTH tag can be used to set the maximum allowed width 
+# (in pixels) of the graphs generated by dot. If a graph becomes larger than 
+# this value, doxygen will try to truncate the graph, so that it fits within 
+# the specified constraint. Beware that most browsers cannot cope with very 
+# large images.
+
+MAX_DOT_GRAPH_WIDTH    = 1024
+
+# The MAX_DOT_GRAPH_HEIGHT tag can be used to set the maximum allows height 
+# (in pixels) of the graphs generated by dot. If a graph becomes larger than 
+# this value, doxygen will try to truncate the graph, so that it fits within 
+# the specified constraint. Beware that most browsers cannot cope with very 
+# large images.
+
+MAX_DOT_GRAPH_HEIGHT   = 1024
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the 
+# graphs generated by dot. A depth value of 3 means that only nodes reachable 
+# from the root by following a path via at most 3 edges will be shown. Nodes 
+# that lay further from the root node will be omitted. Note that setting this 
+# option to 1 or 2 may greatly reduce the computation time needed for large 
+# code bases. Also note that a graph may be further truncated if the graph's 
+# image dimensions are not sufficient to fit the graph (see MAX_DOT_GRAPH_WIDTH 
+# and MAX_DOT_GRAPH_HEIGHT). If 0 is used for the depth value (the default), 
+# the graph is not depth-constrained.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent 
+# background. This is disabled by default, which results in a white background. 
+# Warning: Depending on the platform used, enabling this option may lead to 
+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to 
+# read).
+
+DOT_TRANSPARENT        = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output 
+# files in one run (i.e. multiple -o and -T options on the command line). This 
+# makes dot run faster, but since only newer versions of dot (>1.8.10) 
+# support this, this feature is disabled by default.
+
+DOT_MULTI_TARGETS      = NO
+
+# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will 
+# generate a legend page explaining the meaning of the various boxes and 
+# arrows in the dot generated graphs.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will 
+# remove the intermediate dot files that are used to generate 
+# the various graphs.
+
+DOT_CLEANUP            = YES
+
+#---------------------------------------------------------------------------
+# Configuration::additions related to the search engine   
+#---------------------------------------------------------------------------
+
+# The SEARCHENGINE tag specifies whether or not a search engine should be 
+# used. If set to NO the values of all tags below this one will be ignored.
+
+SEARCHENGINE           = NO
diff --git a/README b/README
index 67c80e4..414e8f5 100644 (file)
--- a/README
+++ b/README
@@ -138,10 +138,20 @@ NOTE: If you are upgrading from an earlier version, see README.upgrades.
 
    See disorderd(8) and disorder_config(5) for more details.
 
-6. Make sure the server is started at boot time.  On many Linux systems,
-   examples/disorder.init should be more or less suitable; install it in
-   /etc/init.d, adapting it as necessary, and make appropriate links from
-   /etc/rc[0-6].d.  If you have a BSD style init then you are on your own.
+6. Make sure the server is started at boot time.
+
+   On many Linux systems, examples/disorder.init should be more or less
+   suitable; install it in /etc/init.d, adapting it as necessary, and make
+   appropriate links from /etc/rc[0-6].d.
+
+   For Mac OS X 10.4, a suitable plist file is automatically installed.  The
+   command:
+
+    sudo launchctl list
+
+   ...should show "uk.org.greenend.rjk.disorder" (among other things).
+
+   If you have a some other init system then you are on your own.
 
 7. Make sure the state directory (/var/disorder or /usr/local/var/disorder or
    as determined by configure) exists and is writable by the jukebox user.
@@ -149,10 +159,19 @@ NOTE: If you are upgrading from an earlier version, see README.upgrades.
      mkdir -m 755 /var/disorder
      chown disorder:root /var/disorder
 
-8. Start the server, for instance:
+   If you want to use some other directory you must put use the 'home' command
+   in the configuration file.
+
+8. Start the server.
+
+   On Linux systems with sysv-style init:
 
      /etc/init.d/disorder start
 
+   On Mac OS X 10.4:
+
+     sudo launchctl start uk.org.greenend.rjk.disorder
+
    By default disorderd logs to daemon.*; check your syslog.conf to see where
    this ends up and look for log messages from disorderd there.  If it didn't
    start up correctly there should be an error message.  Correct the problem
index 82226d6..aa4c26b 100644 (file)
@@ -26,8 +26,34 @@ interrupt the playing track, you have to scratch it manually.  Go back to
 normal play by scratching the stream.
 
 
-Creating A Stream Of DisOrder Output
-====================================
+RTP Streaming
+=============
+
+DisOrder is now capable to transmitting RTP streams over a suitable network.
+To enable this make sure that allplayers use the speaker process, i.e. execraw
+rather than exec (or it won't work properly) and configure the network speaker
+backend:
+
+    speaker_backend network
+    broadcast 172.17.207.255 9003
+    broadcast_from 172.17.207.2 9002
+
+broadcast_from is optional but may be convenient for some cases.
+
+To play, use the disorder-playrtp client.
+
+   disorder-playrtp 0.0.0.0 9003
+
+Currently only 16-bit 44100Hz stereo is supported, which requires about
+1.4Mbit/s.  Possibly other lower-quality but lower-bandwidth encodings will be
+supported in future.
+
+If you have a too-recent version of sox you may need to set the sox_generation
+option.
+
+
+Icecast Streaming
+=================
 
 This can be achieved using the speaker_command option and Icecast (see
 http://www.icecast.org/).  It will only work if you use the speaker process,
@@ -83,11 +109,8 @@ to pause a stream in any case.  I used IceCast 2.3.1 and Ices 2.0.1.  You can
 play the stream with XMMS or whatever.  The total setup seems to play rather
 behind the 'current' time, watch this space for a fix (or contribute one!)
 
-Other DisOrder options you can set are sox_generation which you should set to
-if you have a sufficently recent version of sox, due to an incompatile change
-in its command syntax; and sample_format which determines what format is
-supplied to the speaker_command.  See disorder_config(5) for further
-information.
+If you have a too-recent version of sox you may need to set the sox_generation
+option.
 
 
 Local Variables:
index b197b90..e2f5ec9 100644 (file)
@@ -18,7 +18,7 @@
 # USA
 #
 
-bin_PROGRAMS=disorder disorderfm
+bin_PROGRAMS=disorder disorderfm disorder-playrtp
 noinst_PROGRAMS=test-eclient filename-bytes
 
 AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
@@ -34,6 +34,11 @@ disorderfm_SOURCES=disorderfm.c \
 disorderfm_LDADD=$(LIBOBJS) ../lib/libdisorder.a $(LIBGC) $(LIBICONV)
 disorderfm_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.a
 
+disorder_playrtp_SOURCES=playrtp.c
+disorder_playrtp_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
+       $(LIBASOUND) $(COREAUDIO)
+disorder_playrtp_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.a
+
 filename_bytes_SOURCES=filename-bytes.c
 
 test_eclient_SOURCES=test-eclient.c \
@@ -52,6 +57,10 @@ check-help: all
        ./disorder --version > /dev/null
        ./disorder --help > /dev/null
        ./disorder --help-commands > /dev/null
+       ./disorderfm --version > /dev/null
+       ./disorderfm --help > /dev/null
+       ./disorder-playrtp --version > /dev/null
+       ./disorder-playrtp --help > /dev/null
 
 # check that the command completions are up to date
 check-completions:
diff --git a/clients/playrtp-log b/clients/playrtp-log
new file mode 100755 (executable)
index 0000000..52e79e2
--- /dev/null
@@ -0,0 +1,38 @@
+#! /usr/bin/perl -w
+use strict;
+
+our $last;
+my %start = ();
+my %end = ();
+while(<>) {
+    if(/sequence (\d+) timestamp (\w+) length (\w+) end (\w+)/) {
+       my $seq = $1;
+       my $timestamp = hex($2);
+       my $length = hex($3);
+       my $end = hex($4);
+
+       if(defined $last) {
+           if($seq < $last) {
+               print "$seq < $last\n";
+           } elsif($seq != $last + 1) {
+               printf "%u when %u expected, missed %d\n",
+               $seq, $last + 1, $seq - $last;
+           }
+       }
+       if(exists $start{$seq}) {
+           print "$seq: duplicate";
+       }
+       $start{$seq} = $timestamp;
+       $end{$seq} = $end;
+       if(exists $start{$seq-1}) {
+           if($end{$seq-1} != $start{$seq}) {
+               printf "%u ends at %x but %u starts at %x (delta=%d)\n",
+               $seq-1, $end{$seq-1}, $seq, $start{$seq},
+               $start{$seq}-$end{$seq-1};
+           }
+       }
+
+
+       $last = $seq;
+    }
+}
diff --git a/clients/playrtp.c b/clients/playrtp.c
new file mode 100644 (file)
index 0000000..d5668b7
--- /dev/null
@@ -0,0 +1,954 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file clients/playrtp.c
+ * @brief RTP player
+ *
+ * This player supports Linux (<a href="http://www.alsa-project.org/">ALSA</a>)
+ * and Apple Mac (<a
+ * href="http://developer.apple.com/audio/coreaudio.html">Core Audio</a>)
+ * systems.  There is no support for Microsoft Windows yet, and that will in
+ * fact probably an entirely separate program.
+ *
+ * The program runs (at least) three threads.  listen_thread() is responsible
+ * for reading RTP packets off the wire and adding them to the linked list @ref
+ * received_packets, assuming they are basically sound.  queue_thread() takes
+ * packets off this linked list and adds them to @ref packets (an operation
+ * which might be much slower due to contention for @ref lock).
+ *
+ * The main thread is responsible for actually playing audio.  In ALSA this
+ * means it waits until ALSA says it's ready for more audio which it then
+ * plays.
+ *
+ * InCore Audio the main thread is only responsible for starting and stopping
+ * play: the system does the actual playback in its own private thread, and
+ * calls adioproc() to fetch the audio data.
+ *
+ * Sometimes it happens that there is no audio available to play.  This may
+ * because the server went away, or a packet was dropped, or the server
+ * deliberately did not send any sound because it encountered a silence.
+ *
+ * Assumptions:
+ * - it is safe to read uint32_t values without a lock protecting them
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <pthread.h>
+#include <locale.h>
+#include <sys/uio.h>
+#include <string.h>
+
+#include "log.h"
+#include "mem.h"
+#include "configuration.h"
+#include "addr.h"
+#include "syscalls.h"
+#include "rtp.h"
+#include "defs.h"
+#include "vector.h"
+#include "heap.h"
+#include "timeval.h"
+
+#if HAVE_COREAUDIO_AUDIOHARDWARE_H
+# include <CoreAudio/AudioHardware.h>
+#endif
+#if API_ALSA
+#include <alsa/asoundlib.h>
+#endif
+
+#define readahead linux_headers_are_borked
+
+/** @brief RTP socket */
+static int rtpfd;
+
+/** @brief Log output */
+static FILE *logfp;
+
+/** @brief Output device */
+static const char *device;
+
+/** @brief Maximum samples per packet we'll support
+ *
+ * NB that two channels = two samples in this program.
+ */
+#define MAXSAMPLES 2048
+
+/** @brief Minimum low watermark
+ *
+ * We'll stop playing if there's only this many samples in the buffer. */
+static unsigned minbuffer = 2 * 44100 / 10;  /* 0.2 seconds */
+
+/** @brief Buffer high watermark
+ *
+ * We'll only start playing when this many samples are available. */
+static unsigned readahead = 2 * 2 * 44100;
+
+/** @brief Maximum buffer size
+ *
+ * We'll stop reading from the network if we have this many samples. */
+static unsigned maxbuffer;
+
+/** @brief Number of samples to infill by in one go
+ *
+ * This is an upper bound - in practice we expect the underlying audio API to
+ * only ask for a much smaller number of samples in any one go.
+ */
+#define INFILL_SAMPLES (44100 * 2)      /* 1s */
+
+/** @brief Received packet
+ *
+ * Received packets are kept in a binary heap (see @ref pheap) ordered by
+ * timestamp.
+ */
+struct packet {
+  /** @brief Next packet in @ref next_free_packet or @ref received_packets */
+  struct packet *next;
+  
+  /** @brief Number of samples in this packet */
+  uint32_t nsamples;
+
+  /** @brief Timestamp from RTP packet
+   *
+   * NB that "timestamps" are really sample counters.  Use lt() or lt_packet()
+   * to compare timestamps. 
+   */
+  uint32_t timestamp;
+
+  /** @brief Flags
+   *
+   * Valid values are:
+   * - @ref IDLE - the idle bit was set in the RTP packet
+   */
+  unsigned flags;
+/** @brief idle bit set in RTP packet*/
+#define IDLE 0x0001
+
+  /** @brief Raw sample data
+   *
+   * Only the first @p nsamples samples are defined; the rest is uninitialized
+   * data.
+   */
+  uint16_t samples_raw[MAXSAMPLES];
+};
+
+/** @brief Return true iff \f$a < b\f$ in sequence-space arithmetic
+ *
+ * Specifically it returns true if \f$(a-b) mod 2^{32} < 2^{31}\f$.
+ *
+ * See also lt_packet().
+ */
+static inline int lt(uint32_t a, uint32_t b) {
+  return (uint32_t)(a - b) & 0x80000000;
+}
+
+/** @brief Return true iff a >= b in sequence-space arithmetic */
+static inline int ge(uint32_t a, uint32_t b) {
+  return !lt(a, b);
+}
+
+/** @brief Return true iff a > b in sequence-space arithmetic */
+static inline int gt(uint32_t a, uint32_t b) {
+  return lt(b, a);
+}
+
+/** @brief Return true iff a <= b in sequence-space arithmetic */
+static inline int le(uint32_t a, uint32_t b) {
+  return !lt(b, a);
+}
+
+/** @brief Ordering for packets, used by @ref pheap */
+static inline int lt_packet(const struct packet *a, const struct packet *b) {
+  return lt(a->timestamp, b->timestamp);
+}
+
+/** @brief Received packets
+ * Protected by @ref receive_lock
+ *
+ * Received packets are added to this list, and queue_thread() picks them off
+ * it and adds them to @ref packets.  Whenever a packet is added to it, @ref
+ * receive_cond is signalled.
+ */
+static struct packet *received_packets;
+
+/** @brief Tail of @ref received_packets
+ * Protected by @ref receive_lock
+ */
+static struct packet **received_tail = &received_packets;
+
+/** @brief Lock protecting @ref received_packets 
+ *
+ * Only listen_thread() and queue_thread() ever hold this lock.  It is vital
+ * that queue_thread() not hold it any longer than it strictly has to. */
+static pthread_mutex_t receive_lock = PTHREAD_MUTEX_INITIALIZER;
+
+/** @brief Condition variable signalled when @ref received_packets is updated
+ *
+ * Used by listen_thread() to notify queue_thread() that it has added another
+ * packet to @ref received_packets. */
+static pthread_cond_t receive_cond = PTHREAD_COND_INITIALIZER;
+
+/** @brief Length of @ref received_packets */
+static uint32_t nreceived;
+
+/** @struct pheap 
+ * @brief Binary heap of packets ordered by timestamp */
+HEAP_TYPE(pheap, struct packet *, lt_packet);
+
+/** @brief Binary heap of received packets */
+static struct pheap packets;
+
+/** @brief Total number of samples available
+ *
+ * We make this volatile because we inspect it without a protecting lock,
+ * so the usual pthread_* guarantees aren't available.
+ */
+static volatile uint32_t nsamples;
+
+/** @brief Timestamp of next packet to play.
+ *
+ * This is set to the timestamp of the last packet, plus the number of
+ * samples it contained.  Only valid if @ref active is nonzero.
+ */
+static uint32_t next_timestamp;
+
+/** @brief True if actively playing
+ *
+ * This is true when playing and false when just buffering. */
+static int active;
+
+/** @brief Lock protecting @ref packets */
+static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
+
+/** @brief Condition variable signalled whenever @ref packets is changed */
+static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
+
+/** @brief Structure of free packet list */
+union free_packet {
+  struct packet p;
+  union free_packet *next;
+};
+
+/** @brief Linked list of free packets
+ *
+ * This is a linked list of formerly used packets.  For preference we re-use
+ * packets that have already been used rather than unused ones, to limit the
+ * size of the program's working set.  If there are no free packets in the list
+ * we try @ref next_free_packet instead.
+ *
+ * Must hold @ref lock when accessing this.
+ */
+static union free_packet *free_packets;
+
+/** @brief Array of new free packets 
+ *
+ * There are @ref count_free_packets ready to use at this address.  If there
+ * are none left we allocate more memory.
+ *
+ * Must hold @ref lock when accessing this.
+ */
+static union free_packet *next_free_packet;
+
+/** @brief Count of new free packets at @ref next_free_packet
+ *
+ * Must hold @ref lock when accessing this.
+ */
+static size_t count_free_packets;
+
+/** @brief Lock protecting packet allocator */
+static pthread_mutex_t mem_lock = PTHREAD_MUTEX_INITIALIZER;
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "debug", no_argument, 0, 'd' },
+  { "device", required_argument, 0, 'D' },
+  { "min", required_argument, 0, 'm' },
+  { "max", required_argument, 0, 'x' },
+  { "buffer", required_argument, 0, 'b' },
+  { "rcvbuf", required_argument, 0, 'R' },
+  { 0, 0, 0, 0 }
+};
+
+/** @brief Return a new packet */
+static struct packet *new_packet(void) {
+  struct packet *p;
+  
+  pthread_mutex_lock(&mem_lock);
+  if(free_packets) {
+    p = &free_packets->p;
+    free_packets = free_packets->next;
+  } else {
+    if(!count_free_packets) {
+      next_free_packet = xcalloc(1024, sizeof (union free_packet));
+      count_free_packets = 1024;
+    }
+    p = &(next_free_packet++)->p;
+    --count_free_packets;
+  }
+  pthread_mutex_unlock(&mem_lock);
+  return p;
+}
+
+/** @brief Free a packet */
+static void free_packet(struct packet *p) {
+  union free_packet *u = (union free_packet *)p;
+  pthread_mutex_lock(&mem_lock);
+  u->next = free_packets;
+  free_packets = u;
+  pthread_mutex_unlock(&mem_lock);
+}
+
+/** @brief Drop the first packet
+ *
+ * Assumes that @ref lock is held. 
+ */
+static void drop_first_packet(void) {
+  if(pheap_count(&packets)) {
+    struct packet *const p = pheap_remove(&packets);
+    nsamples -= p->nsamples;
+    free_packet(p);
+    pthread_cond_broadcast(&cond);
+  }
+}
+
+/** @brief Background thread adding packets to heap
+ *
+ * This just transfers packets from @ref received_packets to @ref packets.  It
+ * is important that it holds @ref receive_lock for as little time as possible,
+ * in order to minimize the interval between calls to read() in
+ * listen_thread().
+ */
+static void *queue_thread(void attribute((unused)) *arg) {
+  struct packet *p;
+
+  for(;;) {
+    /* Get the next packet */
+    pthread_mutex_lock(&receive_lock);
+    while(!received_packets)
+      pthread_cond_wait(&receive_cond, &receive_lock);
+    p = received_packets;
+    received_packets = p->next;
+    if(!received_packets)
+      received_tail = &received_packets;
+    --nreceived;
+    pthread_mutex_unlock(&receive_lock);
+    /* Add it to the heap */
+    pthread_mutex_lock(&lock);
+    pheap_insert(&packets, p);
+    nsamples += p->nsamples;
+    pthread_cond_broadcast(&cond);
+    pthread_mutex_unlock(&lock);
+  }
+}
+
+/** @brief Background thread collecting samples
+ *
+ * This function collects samples, perhaps converts them to the target format,
+ * and adds them to the packet list.
+ *
+ * It is crucial that the gap between successive calls to read() is as small as
+ * possible: otherwise packets will be dropped.
+ *
+ * We use a binary heap to ensure that the unavoidable effort is at worst
+ * logarithmic in the total number of packets - in fact if packets are mostly
+ * received in order then we will largely do constant work per packet since the
+ * newest packet will always be last.
+ *
+ * Of more concern is that we must acquire the lock on the heap to add a packet
+ * to it.  If this proves a problem in practice then the answer would be
+ * (probably doubly) linked list with new packets added the end and a second
+ * thread which reads packets off the list and adds them to the heap.
+ *
+ * We keep memory allocation (mostly) very fast by keeping pre-allocated
+ * packets around; see @ref new_packet().
+ */
+static void *listen_thread(void attribute((unused)) *arg) {
+  struct packet *p = 0;
+  int n;
+  struct rtp_header header;
+  uint16_t seq;
+  uint32_t timestamp;
+  struct iovec iov[2];
+
+  for(;;) {
+    if(!p)
+      p = new_packet();
+    iov[0].iov_base = &header;
+    iov[0].iov_len = sizeof header;
+    iov[1].iov_base = p->samples_raw;
+    iov[1].iov_len = sizeof p->samples_raw / sizeof *p->samples_raw;
+    n = readv(rtpfd, iov, 2);
+    if(n < 0) {
+      switch(errno) {
+      case EINTR:
+        continue;
+      default:
+        fatal(errno, "error reading from socket");
+      }
+    }
+    /* Ignore too-short packets */
+    if((size_t)n <= sizeof (struct rtp_header)) {
+      info("ignored a short packet");
+      continue;
+    }
+    timestamp = htonl(header.timestamp);
+    seq = htons(header.seq);
+    /* Ignore packets in the past */
+    if(active && lt(timestamp, next_timestamp)) {
+      info("dropping old packet, timestamp=%"PRIx32" < %"PRIx32,
+           timestamp, next_timestamp);
+      continue;
+    }
+    p->next = 0;
+    p->flags = 0;
+    p->timestamp = timestamp;
+    /* Convert to target format */
+    if(header.mpt & 0x80)
+      p->flags |= IDLE;
+    switch(header.mpt & 0x7F) {
+    case 10:
+      p->nsamples = (n - sizeof header) / sizeof(uint16_t);
+      break;
+      /* TODO support other RFC3551 media types (when the speaker does) */
+    default:
+      fatal(0, "unsupported RTP payload type %d",
+            header.mpt & 0x7F);
+    }
+    if(logfp)
+      fprintf(logfp, "sequence %u timestamp %"PRIx32" length %"PRIx32" end %"PRIx32"\n",
+              seq, timestamp, p->nsamples, timestamp + p->nsamples);
+    /* Stop reading if we've reached the maximum.
+     *
+     * This is rather unsatisfactory: it means that if packets get heavily
+     * out of order then we guarantee dropouts.  But for now... */
+    if(nsamples >= maxbuffer) {
+      pthread_mutex_lock(&lock);
+      while(nsamples >= maxbuffer)
+        pthread_cond_wait(&cond, &lock);
+      pthread_mutex_unlock(&lock);
+    }
+    /* Add the packet to the receive queue */
+    pthread_mutex_lock(&receive_lock);
+    *received_tail = p;
+    received_tail = &p->next;
+    ++nreceived;
+    pthread_cond_signal(&receive_cond);
+    pthread_mutex_unlock(&receive_lock);
+    /* We'll need a new packet */
+    p = 0;
+  }
+}
+
+/** @brief Return true if @p p contains @p timestamp
+ *
+ * Containment implies that a sample @p timestamp exists within the packet.
+ */
+static inline int contains(const struct packet *p, uint32_t timestamp) {
+  const uint32_t packet_start = p->timestamp;
+  const uint32_t packet_end = p->timestamp + p->nsamples;
+
+  return (ge(timestamp, packet_start)
+          && lt(timestamp, packet_end));
+}
+
+/** @brief Wait until the buffer is adequately full
+ *
+ * Must be called with @ref lock held.
+ */
+static void fill_buffer(void) {
+  while(nsamples)
+    drop_first_packet();
+  info("Buffering...");
+  while(nsamples < readahead)
+    pthread_cond_wait(&cond, &lock);
+  next_timestamp = pheap_first(&packets)->timestamp;
+  active = 1;
+}
+
+/** @brief Find next packet
+ * @return Packet to play or NULL if none found
+ *
+ * The return packet is merely guaranteed not to be in the past: it might be
+ * the first packet in the future rather than one that is actually suitable to
+ * play.
+ *
+ * Must be called with @ref lock held.
+ */
+static struct packet *next_packet(void) {
+  while(pheap_count(&packets)) {
+    struct packet *const p = pheap_first(&packets);
+    if(le(p->timestamp + p->nsamples, next_timestamp)) {
+      /* This packet is in the past.  Drop it and try another one. */
+      drop_first_packet();
+    } else
+      /* This packet is NOT in the past.  (It might be in the future
+       * however.) */
+      return p;
+  }
+  return 0;
+}
+
+#if HAVE_COREAUDIO_AUDIOHARDWARE_H
+/** @brief Callback from Core Audio */
+static OSStatus adioproc
+    (AudioDeviceID attribute((unused)) inDevice,
+     const AudioTimeStamp attribute((unused)) *inNow,
+     const AudioBufferList attribute((unused)) *inInputData,
+     const AudioTimeStamp attribute((unused)) *inInputTime,
+     AudioBufferList *outOutputData,
+     const AudioTimeStamp attribute((unused)) *inOutputTime,
+     void attribute((unused)) *inClientData) {
+  UInt32 nbuffers = outOutputData->mNumberBuffers;
+  AudioBuffer *ab = outOutputData->mBuffers;
+  uint32_t samples_available;
+
+  pthread_mutex_lock(&lock);
+  while(nbuffers > 0) {
+    float *samplesOut = ab->mData;
+    size_t samplesOutLeft = ab->mDataByteSize / sizeof (float);
+
+    while(samplesOutLeft > 0) {
+      const struct packet *p = next_packet();
+      if(p && contains(p, next_timestamp)) {
+        /* This packet is ready to play */
+        const uint32_t packet_end = p->timestamp + p->nsamples;
+        const uint32_t offset = next_timestamp - p->timestamp;
+        const uint16_t *ptr = (void *)(p->samples_raw + offset);
+
+        samples_available = packet_end - next_timestamp;
+        if(samples_available > samplesOutLeft)
+          samples_available = samplesOutLeft;
+        next_timestamp += samples_available;
+        samplesOutLeft -= samples_available;
+        while(samples_available-- > 0)
+          *samplesOut++ = (int16_t)ntohs(*ptr++) * (0.5 / 32767);
+        /* We don't bother junking the packet - that'll be dealt with next time
+         * round */
+      } else {
+        /* No packet is ready to play (and there might be no packet at all) */
+        samples_available = p ? p->timestamp - next_timestamp
+                              : samplesOutLeft;
+        if(samples_available > samplesOutLeft)
+          samples_available = samplesOutLeft;
+        //info("infill by %"PRIu32, samples_available);
+        /* Conveniently the buffer is 0 to start with */
+        next_timestamp += samples_available;
+        samplesOut += samples_available;
+        samplesOutLeft -= samples_available;
+      }
+    }
+    ++ab;
+    --nbuffers;
+  }
+  pthread_mutex_unlock(&lock);
+  return 0;
+}
+#endif
+
+
+#if API_ALSA
+/** @brief PCM handle */
+static snd_pcm_t *pcm;
+
+/** @brief True when @ref pcm is up and running */
+static int alsa_prepared = 1;
+
+/** @brief Initialize @ref pcm */
+static void setup_alsa(void) {
+  snd_pcm_hw_params_t *hwparams;
+  snd_pcm_sw_params_t *swparams;
+  /* Only support one format for now */
+  const int sample_format = SND_PCM_FORMAT_S16_BE;
+  unsigned rate = 44100;
+  const int channels = 2;
+  const int samplesize = channels * sizeof(uint16_t);
+  snd_pcm_uframes_t pcm_bufsize = MAXSAMPLES * samplesize * 3;
+  /* If we can write more than this many samples we'll get a wakeup */
+  const int avail_min = 256;
+  int err;
+  
+  /* Open ALSA */
+  if((err = snd_pcm_open(&pcm,
+                         device ? device : "default",
+                         SND_PCM_STREAM_PLAYBACK,
+                         SND_PCM_NONBLOCK)))
+    fatal(0, "error from snd_pcm_open: %d", err);
+  /* Set up 'hardware' parameters */
+  snd_pcm_hw_params_alloca(&hwparams);
+  if((err = snd_pcm_hw_params_any(pcm, hwparams)) < 0)
+    fatal(0, "error from snd_pcm_hw_params_any: %d", err);
+  if((err = snd_pcm_hw_params_set_access(pcm, hwparams,
+                                         SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
+    fatal(0, "error from snd_pcm_hw_params_set_access: %d", err);
+  if((err = snd_pcm_hw_params_set_format(pcm, hwparams,
+                                         sample_format)) < 0)
+    
+    fatal(0, "error from snd_pcm_hw_params_set_format (%d): %d",
+          sample_format, err);
+  if((err = snd_pcm_hw_params_set_rate_near(pcm, hwparams, &rate, 0)) < 0)
+    fatal(0, "error from snd_pcm_hw_params_set_rate (%d): %d",
+          rate, err);
+  if((err = snd_pcm_hw_params_set_channels(pcm, hwparams,
+                                           channels)) < 0)
+    fatal(0, "error from snd_pcm_hw_params_set_channels (%d): %d",
+          channels, err);
+  if((err = snd_pcm_hw_params_set_buffer_size_near(pcm, hwparams,
+                                                   &pcm_bufsize)) < 0)
+    fatal(0, "error from snd_pcm_hw_params_set_buffer_size (%d): %d",
+          MAXSAMPLES * samplesize * 3, err);
+  if((err = snd_pcm_hw_params(pcm, hwparams)) < 0)
+    fatal(0, "error calling snd_pcm_hw_params: %d", err);
+  /* Set up 'software' parameters */
+  snd_pcm_sw_params_alloca(&swparams);
+  if((err = snd_pcm_sw_params_current(pcm, swparams)) < 0)
+    fatal(0, "error calling snd_pcm_sw_params_current: %d", err);
+  if((err = snd_pcm_sw_params_set_avail_min(pcm, swparams, avail_min)) < 0)
+    fatal(0, "error calling snd_pcm_sw_params_set_avail_min %d: %d",
+          avail_min, err);
+  if((err = snd_pcm_sw_params(pcm, swparams)) < 0)
+    fatal(0, "error calling snd_pcm_sw_params: %d", err);
+}
+
+/** @brief Wait until ALSA wants some audio */
+static void wait_alsa(void) {
+  struct pollfd fds[64];
+  int nfds, err;
+  unsigned short events;
+
+  for(;;) {
+    do {
+      if((nfds = snd_pcm_poll_descriptors(pcm,
+                                          fds, sizeof fds / sizeof *fds)) < 0)
+        fatal(0, "error calling snd_pcm_poll_descriptors: %d", nfds);
+    } while(poll(fds, nfds, -1) < 0 && errno == EINTR);
+    if((err = snd_pcm_poll_descriptors_revents(pcm, fds, nfds, &events)))
+      fatal(0, "error calling snd_pcm_poll_descriptors_revents: %d", err);
+    if(events & POLLOUT)
+      return;
+  }
+}
+
+/** @brief Play some sound via ALSA
+ * @param s Pointer to sample data
+ * @param n Number of samples
+ * @return 0 on success, -1 on non-fatal error
+ */
+static int alsa_writei(const void *s, size_t n) {
+  /* Do the write */
+  const snd_pcm_sframes_t frames_written = snd_pcm_writei(pcm, s, n / 2);
+  if(frames_written < 0) {
+    /* Something went wrong */
+    switch(frames_written) {
+    case -EAGAIN:
+      return 0;
+    case -EPIPE:
+      error(0, "error calling snd_pcm_writei: %ld",
+            (long)frames_written);
+      return -1;
+    default:
+      fatal(0, "error calling snd_pcm_writei: %ld",
+            (long)frames_written);
+    }
+  } else {
+    /* Success */
+    next_timestamp += frames_written * 2;
+    return 0;
+  }
+}
+
+/** @brief Play the relevant part of a packet
+ * @param p Packet to play
+ * @return 0 on success, -1 on non-fatal error
+ */
+static int alsa_play(const struct packet *p) {
+  return alsa_writei(p->samples_raw + next_timestamp - p->timestamp,
+                     (p->timestamp + p->nsamples) - next_timestamp);
+}
+
+/** @brief Play some silence
+ * @param p Next packet or NULL
+ * @return 0 on success, -1 on non-fatal error
+ */
+static int alsa_infill(const struct packet *p) {
+  static const uint16_t zeros[INFILL_SAMPLES];
+  size_t samples_available = INFILL_SAMPLES;
+
+  if(p && samples_available > p->timestamp - next_timestamp)
+    samples_available = p->timestamp - next_timestamp;
+  return alsa_writei(zeros, samples_available);
+}
+
+/** @brief Reset ALSA state after we lost synchronization */
+static void alsa_reset(int hard_reset) {
+  int err;
+
+  if((err = snd_pcm_nonblock(pcm, 0)))
+    fatal(0, "error calling snd_pcm_nonblock: %d", err);
+  if(hard_reset) {
+    if((err = snd_pcm_drop(pcm)))
+      fatal(0, "error calling snd_pcm_drop: %d", err);
+  } else
+    if((err = snd_pcm_drain(pcm)))
+      fatal(0, "error calling snd_pcm_drain: %d", err);
+  if((err = snd_pcm_nonblock(pcm, 1)))
+    fatal(0, "error calling snd_pcm_nonblock: %d", err);
+  alsa_prepared = 0;
+}
+#endif
+
+/** @brief Play an RTP stream
+ *
+ * This is the guts of the program.  It is responsible for:
+ * - starting the listening thread
+ * - opening the audio device
+ * - reading ahead to build up a buffer
+ * - arranging for audio to be played
+ * - detecting when the buffer has got too small and re-buffering
+ */
+static void play_rtp(void) {
+  pthread_t ltid;
+
+  /* We receive and convert audio data in a background thread */
+  pthread_create(&ltid, 0, listen_thread, 0);
+  /* We have a second thread to add received packets to the queue */
+  pthread_create(&ltid, 0, queue_thread, 0);
+#if API_ALSA
+  {
+    struct packet *p;
+    int escape, err;
+
+    /* Open the sound device */
+    setup_alsa();
+    pthread_mutex_lock(&lock);
+    for(;;) {
+      /* Wait for the buffer to fill up a bit */
+      fill_buffer();
+      if(!alsa_prepared) {
+        if((err = snd_pcm_prepare(pcm)))
+          fatal(0, "error calling snd_pcm_prepare: %d", err);
+        alsa_prepared = 1;
+      }
+      escape = 0;
+      info("Playing...");
+      /* Keep playing until the buffer empties out, or ALSA tells us to get
+       * lost */
+      while((nsamples >= minbuffer
+             || (nsamples > 0
+                 && contains(pheap_first(&packets), next_timestamp)))
+            && !escape) {
+        /* Wait for ALSA to ask us for more data */
+        pthread_mutex_unlock(&lock);
+        wait_alsa();
+        pthread_mutex_lock(&lock);
+        /* ALSA is ready for more data, find something to play */
+        p = next_packet();
+        /* Play it or play some silence */
+        if(contains(p, next_timestamp))
+          escape = alsa_play(p);
+        else
+          escape = alsa_infill(p);
+      }
+      active = 0;
+      /* We stop playing for a bit until the buffer re-fills */
+      pthread_mutex_unlock(&lock);
+      alsa_reset(escape);
+      pthread_mutex_lock(&lock);
+    }
+
+  }
+#elif HAVE_COREAUDIO_AUDIOHARDWARE_H
+  {
+    OSStatus status;
+    UInt32 propertySize;
+    AudioDeviceID adid;
+    AudioStreamBasicDescription asbd;
+
+    /* If this looks suspiciously like libao's macosx driver there's an
+     * excellent reason for that... */
+
+    /* TODO report errors as strings not numbers */
+    propertySize = sizeof adid;
+    status = AudioHardwareGetProperty(kAudioHardwarePropertyDefaultOutputDevice,
+                                      &propertySize, &adid);
+    if(status)
+      fatal(0, "AudioHardwareGetProperty: %d", (int)status);
+    if(adid == kAudioDeviceUnknown)
+      fatal(0, "no output device");
+    propertySize = sizeof asbd;
+    status = AudioDeviceGetProperty(adid, 0, false,
+                                    kAudioDevicePropertyStreamFormat,
+                                    &propertySize, &asbd);
+    if(status)
+      fatal(0, "AudioHardwareGetProperty: %d", (int)status);
+    D(("mSampleRate       %f", asbd.mSampleRate));
+    D(("mFormatID         %08lx", asbd.mFormatID));
+    D(("mFormatFlags      %08lx", asbd.mFormatFlags));
+    D(("mBytesPerPacket   %08lx", asbd.mBytesPerPacket));
+    D(("mFramesPerPacket  %08lx", asbd.mFramesPerPacket));
+    D(("mBytesPerFrame    %08lx", asbd.mBytesPerFrame));
+    D(("mChannelsPerFrame %08lx", asbd.mChannelsPerFrame));
+    D(("mBitsPerChannel   %08lx", asbd.mBitsPerChannel));
+    D(("mReserved         %08lx", asbd.mReserved));
+    if(asbd.mFormatID != kAudioFormatLinearPCM)
+      fatal(0, "audio device does not support kAudioFormatLinearPCM");
+    status = AudioDeviceAddIOProc(adid, adioproc, 0);
+    if(status)
+      fatal(0, "AudioDeviceAddIOProc: %d", (int)status);
+    pthread_mutex_lock(&lock);
+    for(;;) {
+      /* Wait for the buffer to fill up a bit */
+      fill_buffer();
+      /* Start playing now */
+      info("Playing...");
+      next_timestamp = pheap_first(&packets)->timestamp;
+      active = 1;
+      status = AudioDeviceStart(adid, adioproc);
+      if(status)
+        fatal(0, "AudioDeviceStart: %d", (int)status);
+      /* Wait until the buffer empties out */
+      while(nsamples >= minbuffer
+            || (nsamples > 0
+                && contains(pheap_first(&packets), next_timestamp)))
+        pthread_cond_wait(&cond, &lock);
+      /* Stop playing for a bit until the buffer re-fills */
+      status = AudioDeviceStop(adid, adioproc);
+      if(status)
+        fatal(0, "AudioDeviceStop: %d", (int)status);
+      active = 0;
+      /* Go back round */
+    }
+  }
+#else
+# error No known audio API
+#endif
+}
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+         "  disorder-playrtp [OPTIONS] ADDRESS [PORT]\n"
+         "Options:\n"
+          "  --device, -D DEVICE     Output device\n"
+          "  --min, -m FRAMES        Buffer low water mark\n"
+          "  --buffer, -b FRAMES     Buffer high water mark\n"
+          "  --max, -x FRAMES        Buffer maximum size\n"
+          "  --rcvbuf, -R BYTES      Socket receive buffer size\n"
+         "  --help, -h              Display usage message\n"
+         "  --version, -V           Display version number\n"
+          );
+  xfclose(stdout);
+  exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+  xprintf("disorder-playrtp version %s\n", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+int main(int argc, char **argv) {
+  int n;
+  struct addrinfo *res;
+  struct stringlist sl;
+  char *sockname;
+  int rcvbuf, target_rcvbuf = 131072;
+  socklen_t len;
+
+  static const struct addrinfo prefs = {
+    AI_PASSIVE,
+    PF_INET,
+    SOCK_DGRAM,
+    IPPROTO_UDP,
+    0,
+    0,
+    0,
+    0
+  };
+
+  mem_init();
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  while((n = getopt_long(argc, argv, "hVdD:m:b:x:L:R:", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'd': debugging = 1; break;
+    case 'D': device = optarg; break;
+    case 'm': minbuffer = 2 * atol(optarg); break;
+    case 'b': readahead = 2 * atol(optarg); break;
+    case 'x': maxbuffer = 2 * atol(optarg); break;
+    case 'L': logfp = fopen(optarg, "w"); break;
+    case 'R': target_rcvbuf = atoi(optarg); break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  if(!maxbuffer)
+    maxbuffer = 4 * readahead;
+  argc -= optind;
+  argv += optind;
+  if(argc < 1 || argc > 2)
+    fatal(0, "usage: disorder-playrtp [OPTIONS] ADDRESS [PORT]");
+  sl.n = argc;
+  sl.s = argv;
+  /* Listen for inbound audio data */
+  if(!(res = get_address(&sl, &prefs, &sockname)))
+    exit(1);
+  if((rtpfd = socket(res->ai_family,
+                     res->ai_socktype,
+                     res->ai_protocol)) < 0)
+    fatal(errno, "error creating socket");
+  if(bind(rtpfd, res->ai_addr, res->ai_addrlen) < 0)
+    fatal(errno, "error binding socket to %s", sockname);
+  len = sizeof rcvbuf;
+  if(getsockopt(rtpfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &len) < 0)
+    fatal(errno, "error calling getsockopt SO_RCVBUF");
+  if(target_rcvbuf > rcvbuf) {
+    if(setsockopt(rtpfd, SOL_SOCKET, SO_RCVBUF,
+                  &target_rcvbuf, sizeof target_rcvbuf) < 0)
+      error(errno, "error calling setsockopt SO_RCVBUF %d", 
+            target_rcvbuf);
+      /* We try to carry on anyway */
+    else
+      info("changed socket receive buffer from %d to %d",
+           rcvbuf, target_rcvbuf);
+  } else
+    info("default socket receive buffer %d", rcvbuf);
+  if(logfp)
+    info("WARNING: -L option can impact performance");
+  play_rtp();
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index c152c92..0384893 100644 (file)
@@ -37,10 +37,15 @@ case "$host" in
 *linux* | *Linux* )
   want_alsa=yes
   ;;
+*-apple-darwin* )
+  want_alsa=no
+  COREAUDIO="-framework CoreAudio"
+  ;;
 * )
   want_alsa=no
   ;;
 esac
+AC_SUBST([COREAUDIO])
 
 # Checks for programs.
 AC_PROG_CC
@@ -100,12 +105,16 @@ if test "x$FINK" != xnone; then
   AC_CACHE_CHECK([fink install directory],[rjk_cv_finkprefix],[
     rjk_cv_finkprefix="`echo "$FINK" | sed 's,/bin/fink$,,'`"
   ])
+  finkbindir="${rjk_cv_finkprefix}/bin"
   CPPFLAGS="${CPPFLAGS} -I${rjk_cv_finkprefix}/include/gc -I${rjk_cv_finkprefix}/include"
   if test $want_server = yes; then
     CPPFLAGS="${CPPFLAGS} -I${rjk_cv_finkprefix}/include/db4"
   fi
   LDFLAGS="${LDFLAGS} -L${rjk_cv_finkprefix}/lib"
+else
+  finkbindir=""
 fi
+AC_SUBST([finkbindir])
 
 # Checks for libraries.
 # We save up a list of missing libraries that we can't do without
@@ -135,12 +144,12 @@ if test $want_server = yes; then
   AC_CHECK_LIB([ao], [ao_initialize],
               [AC_SUBST(LIBAO,[-lao])],
               [missing_libraries="$missing_libraries libao"])
-  if test $want_alsa = yes; then
-    AC_CHECK_LIB([asound], [snd_pcm_open],
-                [AC_SUBST(LIBASOUND,[-lasound])],
-                [missing_libraries="$missing_libraries libasound"])
-    AC_DEFINE([API_ALSA],[1],[define to use the ALSA API])
-  fi
+fi
+if test $want_alsa = yes; then
+  AC_CHECK_LIB([asound], [snd_pcm_open],
+               [AC_SUBST(LIBASOUND,[-lasound])],
+              [missing_libraries="$missing_libraries libasound"])
+  AC_DEFINE([API_ALSA],[1],[define to use the ALSA API])
 fi
 
 if test $want_gtk = yes; then
@@ -175,7 +184,7 @@ RJK_REQUIRE_PCRE_UTF8([-lpcre])
 
 # Checks for header files.
 RJK_FIND_GC_H
-AC_CHECK_HEADERS([inttypes.h])
+AC_CHECK_HEADERS([inttypes.h CoreAudio/AudioHardware.h])
 # Compilation will fail if any of these headers are missing, so we
 # check for them here and fail early.
 # We don't bother checking very standard stuff
index 24aa9f6..f13362a 100644 (file)
@@ -48,7 +48,8 @@ struct displaydata {
 };
 
 /* instantiate the node vector type */
-VECTOR_TYPE(nodevector, struct choosenode *, xrealloc)
+
+VECTOR_TYPE(nodevector, struct choosenode *, xrealloc);
 
 struct choosenode {
   struct choosenode *parent;            /* parent node */
index 8ce21d3..05c3544 100644 (file)
 
 SEDFILES=disorder.1 disorderd.8 disorder_config.5 \
        disorder-dump.8 disorder_protocol.5 disorder-deadlock.8 \
-       disorder-rescan.8 disobedience.1 disorderfm.1
+       disorder-rescan.8 disobedience.1 disorderfm.1 disorder-playrtp.1
 
 include ${top_srcdir}/scripts/sedfiles.make
 
 man_MANS=disorderd.8 disorder.1 disorder.3 disorder_config.5 disorder-dump.8 \
        disorder_protocol.5 tkdisorder.1 disorder-deadlock.8 \
-       disorder-rescan.8 disobedience.1 disorderfm.1 disorder-speaker.8
+       disorder-rescan.8 disobedience.1 disorderfm.1 disorder-speaker.8 \
+       disorder-playrtp.1
 
 HTMLMAN=$(foreach man,$(man_MANS),$(man).html)
 
@@ -41,6 +42,7 @@ pkgdata_DATA=$(HTMLMAN)
 EXTRA_DIST=disorderd.8.in disorder.1.in disorder_config.5.in \
           disorder.3 disorder-dump.8.in disorder_protocol.5.in \
           tkdisorder.1 disorder-deadlock.8.in disorder-rescan.8.in \
-          disobedience.1.in disorderfm.1.in disorder-speaker.8
+          disobedience.1.in disorderfm.1.in disorder-speaker.8 \
+          disorder-playrtp.1.in
 
 CLEANFILES=$(SEDFILES) $(HTMLMAN)
diff --git a/doc/disorder-playrtp.1.in b/doc/disorder-playrtp.1.in
new file mode 100644 (file)
index 0000000..18ca149
--- /dev/null
@@ -0,0 +1,64 @@
+.\"
+.\" Copyright (C) 2007 Richard Kettlewell
+.\"
+.\" This program is free software; you can redistribute it and/or modify
+.\" it under the terms of the GNU General Public License as published by
+.\" the Free Software Foundation; either version 2 of the License, or
+.\" (at your option) any later version.
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU General Public License
+.\" along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+.\" USA
+.\"
+.TH disorder-playrtp 1
+.SH NAME
+disorder-playrtp \- play DisOrder network broadcasts
+.SH SYNOPSIS
+.B disorder-playrtp
+.RI [ OPTIONS ]
+.RB [ -- ]
+.I ADDRESS
+.I PORT
+.SH DESCRIPTION
+\fBdisorder-playrtp\fR plays a network broadcast sent from the specified
+address.
+.SH OPTIONS
+.TP
+.B --device \fIDEVICE\fR, \fB-D \fIDEVICE\fR
+Specifies the audio device to use.  The exact meaning of this is
+platform-dependent; on Linux it is the ALSA device name.
+.TP
+.B --min \fIFRAMES\fR, \fB-m \fIFRAMES\fR
+Specifies the buffer low watermark in frames.  If the number of frames falls
+below this value then playing will be stopped until the buffer fills up.
+.TP
+.B --buffer \fIFRAMES\fR, \fB-b \fIFRAMES\fR
+Specifies the buffer high watermark in frames.  Once there are this many frames
+in the buffer, playing will be (re-)started.
+.TP
+.B --max \fIFRAMES\fR, \fB-x \fIFRAMES\fR
+Specifies the maximum buffer size in frames.  If there are this many frames in
+the buffer then reading from the network socket will be suspended.  The default
+is four times the \fB--buffer\fR value.
+.TP
+.B --rcvbuf \fIBYTES\fR, \fB-R \fIBYTES\fR
+Specifies socket receive buffer size.
+.TP
+.B --help\fR, \fB-h
+Display a usage message.
+.TP
+.B --version\fR, \fB-V
+Display version number.
+.SH "SEE ALSO"
+.BR disorder_config (5),
+.BR disorderd (8)
+.\" Local Variables:
+.\" mode:nroff
+.\" fill-column:79
+.\" End:
index 3ddeb47..cc8bd39 100644 (file)
@@ -142,6 +142,13 @@ automatically included, but should include the proper extension.
 .IP
 The default is \fB{/artist}{/album}{/title}{ext}\fR.
 .TP
+.B broadcast \fIADDRESS\fR \fIPORT\fR
+Transmit sound data to \fIADDRESS\fR using UDP port \fIPORT\fR.  This implies
+\fBspeaker_backend network\fR.
+.TP
+.B broadcast_from \fIADDRESS\fR \fIPORT\fR
+Sets the (local) source address used by \fBbroadcast\fR.
+.TP
 .B channel \fICHANNEL\fR
 The mixer channel that the volume control should use.  Valid names depend on
 your operating system and hardware, but some standard ones that might be useful
@@ -327,6 +334,27 @@ scratched.  The default is \fBSIGKILL\fR.
 Signals are specified by their full C name, i.e. \fBSIGINT\fR and not \fBINT\fR
 or \fBInterrupted\fR or whatever.
 .TP
+.B speaker_backend \fINAME\fR
+Selects the backend use by the speaker process.  The following options are
+available:
+.RS
+.TP
+.B alsa
+Use the ALSA API.  This is only available on Linux systems, on which it is the
+default.
+.TP
+.B command
+Execute a command.  This is the default if
+.B speaker_command
+is specified, or (currently) on non-Linux systems.
+.TP
+.B network
+Transmit audio over the network.  This is the default if
+\fBbroadcast\fR is specified.  You can use
+.BR disorder-playrtp (1)
+to receive and play the resulting stream on Linux and OS X.
+.RE
+.TP
 .B sox_generation \fB0\fR|\fB1
 Determines whether calls to \fBsox\fR(1) should use \fB-b\fR, \fB-x\fR, etc (if
 the generation is 0) or \fB-\fIbits\fR, \fB-L\fR etc (if it is 1).  The default
index ece2b3b..f79435b 100644 (file)
@@ -36,6 +36,7 @@ libdisorder_a_SOURCES=charset.c charset.h             \
        eventlog.c eventlog.h                           \
        filepart.c filepart.h                           \
        hash.c hash.h                                   \
+       heap.h                                          \
        hex.c hex.h                                     \
        inputline.c inputline.h                         \
        kvp.c kvp.h                                     \
@@ -49,14 +50,16 @@ libdisorder_a_SOURCES=charset.c charset.h           \
        asprintf.c fprintf.c snprintf.c                 \
        queue.c queue.h                                 \
        regsub.c regsub.h                               \
+       rtp.h                                           \
        selection.c selection.h                         \
        signame.c signame.h                             \
        sink.c sink.h                                   \
-       speaker.c speaker.h                             \
+       speaker-protocol.c speaker-protocol.h           \
        split.c split.h                                 \
        syscalls.c syscalls.h                           \
        types.h                                         \
        table.c table.h                                 \
+       timeval.h                                       \
        trackname.c trackname.h                         \
        user.h user.c                                   \
        utf8.h utf8.c                                   \
@@ -72,6 +75,9 @@ definitions.h: Makefile
        echo "#define PKGCONFDIR \"${sysconfdir}/\"PACKAGE" >> $@.new
        echo "#define PKGSTATEDIR \"${localstatedir}/\"PACKAGE" >> $@.new
        echo "#define PKGDATADIR \"${pkgdatadir}/\"" >> $@.new
+       echo "#define SBINDIR \"${sbindir}/\"" >> $@.new
+       echo "#define BINDIR \"${bindir}/\"" >> $@.new
+       echo "#define FINKBINDIR \"${finkbindir}/\"" >> $@.new
        mv $@.new $@
 defs.o: definitions.h
 defs.lo: definitions.h
@@ -80,7 +86,11 @@ test_SOURCES=test.c
 test_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV)
 test_DEPENDENCIES=libdisorder.a
 
-check: test
+check: test #test.i
        ./test
 
+%.i: %.c
+       $(CPP) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) -c $< > $@.new
+       mv $@.new $@
+
 CLEANFILES=definitions.h
index f994ac6..579fd0f 100644 (file)
@@ -17,6 +17,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/addr.c Socket address support */
 
 #include <config.h>
 #include "types.h"
 #include "configuration.h"
 #include "addr.h"
 
+/** @brief Convert a pair of strings to an address
+ * @param a Pointer to string list
+ * @param pref Hints structure for getaddrinfo, or NULL
+ * @param namep Where to store address description, or NULL
+ * @return Address info structure or NULL on error
+ *
+ * This converts one or two strings into an address specification suitable
+ * for passing to socket(), bind() etc.
+ *
+ * If there is only one string then it is assumed to be the service
+ * name (port number).  If there are two then the first is the host
+ * name and the second the service name.
+ *
+ * @p namep is used to return a description of the address suitable
+ * for use in log messages.
+ *
+ * If an error occurs a message is logged and a null pointer returned.
+ */
 struct addrinfo *get_address(const struct stringlist *a,
                             const struct addrinfo *pref,
                             char **namep) {
   struct addrinfo *res;
   char *name;
   int rc;
-  
-  if(a->n == 1) {
+
+  switch(a->n) {  
+  case 1:
     byte_xasprintf(&name, "host * service %s", a->s[0]);
     if((rc = getaddrinfo(0, a->s[0], pref, &res))) {
       error(0, "getaddrinfo %s: %s", a->s[0], gai_strerror(rc));
       return 0;
     }
-  } else {
+    break;
+  case 2:
     byte_xasprintf(&name, "host %s service %s", a->s[0], a->s[1]);
     if((rc = getaddrinfo(a->s[0], a->s[1], pref, &res))) {
       error(0, "getaddrinfo %s %s: %s", a->s[0], a->s[1], gai_strerror(rc));
       return 0;
     }
+    break;
+  default:
+    error(0, "invalid network address specification (n=%d)", a->n);
+    return 0;
   }
-  if(!res || res->ai_socktype != SOCK_STREAM) {
-    error(0, "getaddrinfo didn't give us a stream socket");
+  if(!res || (pref && res->ai_socktype != pref->ai_socktype)) {
+    error(0, "getaddrinfo didn't give us a suitable socket address");
     if(res)
       freeaddrinfo(res);
     return 0;
@@ -64,6 +89,10 @@ struct addrinfo *get_address(const struct stringlist *a,
   return res;
 }
 
+/** @brief Comparison function for address information
+ *
+ * Suitable for qsort().
+ */
 int addrinfocmp(const struct addrinfo *a,
                const struct addrinfo *b) {
   const struct sockaddr_in *ina, *inb;
index f590c0a..304304a 100644 (file)
@@ -17,6 +17,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/asprintf.c @brief printf() workalikes */
 
 #include <config.h>
 #include "types.h"
 #include "vector.h"
 #include "log.h"
 
+/** @brief vasprintf() workalike without encoding errors
+ *
+ * This acts like vasprintf() except that it does not throw an error
+ * if you use a string outside the current locale's encoding rules,
+ * and it uses the memory allocation calls from @ref mem.h.
+ */
 int byte_vasprintf(char **ptrp,
                   const char *fmt,
                   va_list ap) {
@@ -47,6 +54,12 @@ int byte_vasprintf(char **ptrp,
   return n;
 }
 
+/** @brief asprintf() workalike without encoding errors
+ *
+ * This acts like asprintf() except that it does not throw an error
+ * if you use a string outside the current locale's encoding rules,
+ * and it uses the memory allocation calls from @ref mem.h.
+ */
 int byte_asprintf(char **ptrp,
                  const char *fmt,
                  ...) {
@@ -59,6 +72,13 @@ int byte_asprintf(char **ptrp,
   return n;
 }
 
+/** @brief asprintf() workalike without encoding errors
+ *
+ * This acts like asprintf() except that it does not throw an error if
+ * you use a string outside the current locale's encoding rules; it
+ * uses the memory allocation calls from @ref mem.h; and it terminates
+ * the program on error.
+ */
 int byte_xasprintf(char **ptrp,
                   const char *fmt,
                   ...) {
@@ -71,6 +91,13 @@ int byte_xasprintf(char **ptrp,
   return n;
 }
 
+/** @brief vasprintf() workalike without encoding errors
+ *
+ * This acts like vasprintf() except that it does not throw an error
+ * if you use a string outside the current locale's encoding rules; it
+ * uses the memory allocation calls from @ref mem.h; and it terminates
+ * the program on error.
+ */
 int byte_xvasprintf(char **ptrp,
                    const char *fmt,
                    va_list ap) {
index a1012ba..b97204d 100644 (file)
@@ -17,6 +17,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/authhash.c @brief The authorization hash */
 
 #include <config.h>
 #include "types.h"
 #include "authhash.h"
 
 #ifndef AUTHHASH
+/** @brief Which hash function to use */
 # define AUTHHASH GCRY_MD_SHA1
 #endif
 
+/** @brief Perform the authorization hash function
+ * @param challenge Pointer to challange
+ * @param nchallenge Size of challenge
+ * @param password Password
+ *
+ * Computes H(challenge|password) and returns it as a newly allocated hex
+ * string.  Currently the hash function is SHA-1, but this may be changed in
+ * future versions; see @ref AUTHHASH.
+ */
 const char *authhash(const void *challenge, size_t nchallenge,
                     const char *password) {
   gcrypt_hash_handle h;
index 69ab273..c5644b4 100644 (file)
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/basen.c @brief Arbitrary base conversion 
+ *
+ * The functions in this file handle arbitrary-size non-negative integers,
+ * represented as a bigendian (MSW first) sequence of @c unsigned @c long
+ * words.  The words themselves use the native byte order.
+ */
 
 #include <config.h>
 #include "types.h"
 
 #include "basen.h"
 
-/* test whether v is 0 */
+/** @brief Test whether v is 0
+ * @param v Pointer to bigendian bignum
+ * @param nwords Length of bignum
+ * @return !v
+ */
 static int zero(const unsigned long *v, int nwords) {
   int n;
 
@@ -34,7 +44,14 @@ static int zero(const unsigned long *v, int nwords) {
   return n == nwords;
 }
 
-/* divide v by m returning the remainder */
+/** @brief Divide v by m returning the remainder.
+ * @param v Pointer to bigendian bignum
+ * @param nwords Length of bignum
+ * @param m Divisor (must not be 0)
+ * @return Remainder
+ *
+ * The quotient is stored in @p v.
+ */
 static unsigned divide(unsigned long *v, int nwords, unsigned long m) {
   unsigned long r = 0, a, b;
   int n;
@@ -54,6 +71,17 @@ static unsigned divide(unsigned long *v, int nwords, unsigned long m) {
   return r;
 }
 
+/** @brief Convert v to a chosen base
+ * @param v Pointer to bigendian bignum
+ * @param nwords Length of bignum
+ * @param buffer Output buffer
+ * @param bufsize Size of output buffer
+ * @param base Number base (2..62)
+ * @return 0 on success, -1 if the buffer is too small
+ *
+ * Converts @p v to a string in the given base using decimal digits, lower case
+ * letter sand upper case letters as digits.
+ */
 int basen(unsigned long *v,
          int nwords,
          char buffer[],
index c6624e8..bc6bc81 100644 (file)
@@ -17,6 +17,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/cache.c @brief Object caching */
 
 #include <config.h>
 #include "types.h"
 #include "log.h"
 #include "cache.h"
 
+/** @brief The global cache */
 static hash *h;
 
+/** @brief One cache entry */
 struct cache_entry {
+  /** @brief What type of object this is */
   const struct cache_type *type;
+
+  /** @brief Pointer to object value */
   const void *value;
+
+  /** @brief Time that object was inserted into cache */
   time_t birth;
 };
 
+/** @brief Return true if object @p c has expired */
 static int expired(const struct cache_entry *c, time_t now) {
   return now - c->birth > c->type->lifetime;
 }
 
+/** @brief Insert an object into the cache
+ * @param type Pointer to object type
+ * @param key Unique key
+ * @param value Pointer to value
+ */
 void cache_put(const struct cache_type *type,
                const char *key, const void *value) {
   struct cache_entry *c;
@@ -53,6 +67,11 @@ void cache_put(const struct cache_type *type,
   hash_add(h, key, c,  HASH_INSERT_OR_REPLACE);
 }
 
+/** @brief Look up an object in the cache
+ * @param type Pointer to object type
+ * @param key Unique key
+ * @return Pointer to object value or NULL if not found
+ */
 const void *cache_get(const struct cache_type *type, const char *key) {
   const struct cache_entry *c;
   
@@ -74,6 +93,9 @@ static int expiry_callback(const char *key, void *value, void *u) {
   return 0;
 }
 
+/** @brief Expire the cache
+ *
+ * Called from time to time to expire cache entries. */
 void cache_expire(void) {
   time_t now;
 
@@ -92,11 +114,20 @@ static int clean_callback(const char *key, void *value, void *u) {
   return 0;
 }
 
+/** @brief Clean the cache
+ * @param type Pointer to type to clean
+ *
+ * Removes all entries of type @p type from the cache.
+ */
 void cache_clean(const struct cache_type *type) {
   if(h)
     hash_foreach(h, clean_callback, (void *)type);
 }
 
+/** @brief Report cache size
+ *
+ * Returns the number of objects in the cache
+ */
 size_t cache_count(void) {
   return h ? hash_count(h) : 0;
 }
index f101ec2..2a38fbf 100644 (file)
@@ -17,6 +17,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/charset.c @brief Character set conversion */
 
 #include <config.h>
 #include "types.h"
 #include "utf8.h"
 #include "vector.h"
 
+/** @brief Low-level converstion routine
+ * @param from Source encoding
+ * @param to Destination encoding
+ * @param ptr First byte to convert
+ * @param n Number of bytes to convert
+ * @return Converted text, 0-terminated; or NULL on error.
+ */
 static void *convert(const char *from, const char *to,
                     const void *ptr, size_t n) {
   iconv_t i;
@@ -61,8 +69,14 @@ static void *convert(const char *from, const char *to,
   return buf;
 }
 
-/* not everybody's iconv supports UCS-4, and it's inconvenient to have to know
- * our endianness, and it's easy to convert it ourselves, so we do */
+/** @brief Convert UTF-8 to UCS-4
+ * @param mb Pointer to 0-terminated UTF-8 string
+ * @return Pointer to 0-terminated UCS-4 string
+ *
+ * Not everybody's iconv supports UCS-4, and it's inconvenient to have to know
+ * our endianness, and it's easy to convert it ourselves, so we do.  See also
+ * @ref ucs42utf8().
+ */ 
 uint32_t *utf82ucs4(const char *mb) {
   struct dynstr_ucs4 d;
   uint32_t c;
@@ -77,6 +91,12 @@ uint32_t *utf82ucs4(const char *mb) {
   return d.vec;
 }
 
+/** @brief Convert UCS-4 to UTF-8
+ * @param u Pointer to 0-terminated UCS-4 string
+ * @return Pointer to 0-terminated UTF-8 string
+ *
+ * See @ref utf82ucs4().
+ */
 char *ucs42utf8(const uint32_t *u) {
   struct dynstr d;
   uint32_t c;
@@ -106,23 +126,28 @@ char *ucs42utf8(const uint32_t *u) {
   return d.vec;
 }
 
+/** @brief Convert from the local multibyte encoding to UTF-8 */
 char *mb2utf8(const char *mb) {
   return convert(nl_langinfo(CODESET), "UTF-8", mb, strlen(mb) + 1);
 }
 
+/** @brief Convert from UTF-8 to the local multibyte encoding */
 char *utf82mb(const char *utf8) {
   return convert("UTF-8", nl_langinfo(CODESET), utf8, strlen(utf8) + 1);
 }
 
+/** @brief Convert from encoding @p from to UTF-8 */
 char *any2utf8(const char *from, const char *any) {
   return convert(from, "UTF-8", any, strlen(any) + 1);
 }
 
+/** @brief Convert from encoding @p from to the local multibyte encoding */
 char *any2mb(const char *from, const char *any) {
   if(from) return convert(from, nl_langinfo(CODESET), any, strlen(any) + 1);
   else return xstrdup(any);
 }
 
+/** @brief Convert from encoding @p from to encoding @p to */
 char *any2any(const char *from,
              const char *to,
              const char *any) {
@@ -130,6 +155,10 @@ char *any2any(const char *from,
   else return xstrdup(any);
 }
 
+/** @brief strlen workalike for UCS-4 strings
+ *
+ * We don't rely on the local @c wchar_t being UCS-4.
+ */
 int ucs4cmp(const uint32_t *a, const uint32_t *b) {
   while(*a && *b && *a == *b) ++a, ++b;
   if(*a > *b) return 1;
index 845c32a..60b3e0e 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ * Copyright (C) 2004, 2005, 2006, 2007 Richard Kettlewell
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 #include "regsub.h"
 #include "signame.h"
 
+/** @brief Path to config file 
+ *
+ * set_configfile() sets the deafult if it is null.
+ */
 char *configfile;
 
+/** @brief Config file parser state */
 struct config_state {
+  /** @brief Filename */
   const char *path;
+  /** @brief Line number */
   int line;
+  /** @brief Configuration object under construction */
   struct config *config;
 };
 
+/** @brief Current configuration */
 struct config *config;
 
+/** @brief One configuration item */
 struct conf {
+  /** @brief Name as it appears in the config file */
   const char *name;
+  /** @brief Offset in @ref config structure */
   size_t offset;
+  /** @brief Pointer to item type */
   const struct conftype *type;
+  /** @brief Pointer to item-specific validation routine */
   int (*validate)(const struct config_state *cs,
                  int nvec, char **vec);
 };
 
+/** @brief Type of a configuration item */
 struct conftype {
+  /** @brief Pointer to function to set item */
   int (*set)(const struct config_state *cs,
             const struct conf *whoami,
             int nvec, char **vec);
+  /** @brief Pointer to function to free item */
   void (*free)(struct config *c, const struct conf *whoami);
 };
 
+/** @brief Compute the address of an item */
 #define ADDRESS(C, TYPE) ((TYPE *)((char *)(C) + whoami->offset))
+/** @brief Return the value of an item */
 #define VALUE(C, TYPE) (*ADDRESS(C, TYPE))
 
 static int set_signal(const struct config_state *cs,
@@ -405,6 +424,36 @@ static int set_transform(const struct config_state *cs,
   return 0;
 }
 
+static int set_backend(const struct config_state *cs,
+                      const struct conf *whoami,
+                      int nvec, char **vec) {
+  int *const valuep = ADDRESS(cs->config, int);
+  
+  if(nvec != 1) {
+    error(0, "%s:%d: '%s' requires one argument",
+         cs->path, cs->line, whoami->name);
+    return -1;
+  }
+  if(!strcmp(vec[0], "alsa")) {
+#if API_ALSA
+    *valuep = BACKEND_ALSA;
+#else
+    error(0, "%s:%d: ALSA is not available on this platform",
+         cs->path, cs->line);
+    return -1;
+#endif
+  } else if(!strcmp(vec[0], "command"))
+    *valuep = BACKEND_COMMAND;
+  else if(!strcmp(vec[0], "network"))
+    *valuep = BACKEND_NETWORK;
+  else {
+    error(0, "%s:%d: invalid '%s' value '%s'",
+         cs->path, cs->line, whoami->name, vec[0]);
+    return -1;
+  }
+  return 0;
+}
+
 /* free functions */
 
 static void free_none(struct config attribute((unused)) *c,
@@ -502,7 +551,8 @@ static const struct conftype
   type_sample_format = { set_sample_format, free_none },
   type_restrict = { set_restrict, free_none },
   type_namepart = { set_namepart, free_namepartlist },
-  type_transform = { set_transform, free_transformlist };
+  type_transform = { set_transform, free_transformlist },
+  type_backend = { set_backend, free_none };
 
 /* specific validation routine */
 
@@ -720,24 +770,66 @@ static int validate_alias(const struct config_state *cs,
   return 0;
 }
 
-/* configuration table */
+static int validate_addrport(const struct config_state attribute((unused)) *cs,
+                            int nvec,
+                            char attribute((unused)) **vec) {
+  switch(nvec) {
+  case 0:
+    error(0, "%s:%d: missing address",
+         cs->path, cs->line);
+    return -1;
+  case 1:
+    error(0, "%s:%d: missing port name/number",
+         cs->path, cs->line);
+    return -1;
+  case 2:
+    return 0;
+  default:
+    error(0, "%s:%d: expected ADDRESS PORT",
+         cs->path, cs->line);
+    return -1;
+  }
+}
+
+static int validate_address(const struct config_state attribute((unused)) *cs,
+                        int nvec,
+                        char attribute((unused)) **vec) {
+  switch(nvec) {
+  case 0:
+    error(0, "%s:%d: missing address",
+         cs->path, cs->line);
+    return -1;
+  case 1:
+  case 2:
+    return 0;
+  default:
+    error(0, "%s:%d: expected ADDRESS PORT",
+         cs->path, cs->line);
+    return -1;
+  }
+}
 
+/** @brief Item name and and offset */
 #define C(x) #x, offsetof(struct config, x)
+/** @brief Item name and and offset */
 #define C2(x,y) #x, offsetof(struct config, y)
 
+/** @brief All configuration items */
 static const struct conf conf[] = {
   { C(alias),            &type_string,           validate_alias },
   { C(allow),            &type_stringlist_accum, validate_allow },
+  { C(broadcast),        &type_stringlist,       validate_addrport },
+  { C(broadcast_from),   &type_stringlist,       validate_address },
   { C(channel),          &type_string,           validate_channel },
   { C(checkpoint_kbyte), &type_integer,          validate_non_negative },
   { C(checkpoint_min),   &type_integer,          validate_non_negative },
   { C(collection),       &type_collections,      validate_any },
-  { C(connect),          &type_stringlist,       validate_any },
+  { C(connect),          &type_stringlist,       validate_addrport },
   { C(device),           &type_string,           validate_any },
   { C(gap),              &type_integer,          validate_non_negative },
   { C(history),          &type_integer,          validate_positive },
   { C(home),             &type_string,           validate_isdir },
-  { C(listen),           &type_stringlist,       validate_any },
+  { C(listen),           &type_stringlist,       validate_addrport },
   { C(lock),             &type_boolean,          validate_any },
   { C(mixer),            &type_string,           validate_ischr },
   { C(namepart),         &type_namepart,         validate_any },
@@ -756,6 +848,7 @@ static const struct conf conf[] = {
   { C(scratch),          &type_string_accum,     validate_isreg },
   { C(signal),           &type_signal,           validate_any },
   { C(sox_generation),   &type_integer,          validate_non_negative },
+  { C(speaker_backend),  &type_backend,          validate_any },
   { C(speaker_command),  &type_string,           validate_any },
   { C(stopword),         &type_string_accum,     validate_any },
   { C(templates),        &type_string_accum,     validate_isdir },
@@ -766,7 +859,7 @@ static const struct conf conf[] = {
   { C(username),         &type_string,           validate_any },
 };
 
-/* find a configuration item's definition by key */
+/** @brief Find a configuration item's definition by key */
 static const struct conf *find(const char *key) {
   int n;
 
@@ -775,7 +868,7 @@ static const struct conf *find(const char *key) {
   return &conf[n];
 }
 
-/* set a new configuration value */
+/** @brief Set a new configuration value */
 static int config_set(const struct config_state *cs,
                      int nvec, char **vec) {
   const struct conf *which;
@@ -796,7 +889,7 @@ static void config_error(const char *msg, void *u) {
   error(0, "%s:%d: %s", cs->path, cs->line, msg);
 }
 
-/* include a file by name */
+/** @brief Include a file by name */
 static int config_include(struct config *c, const char *path) {
   FILE *fp;
   char *buffer, *inputbuffer, **vec;
@@ -848,7 +941,7 @@ static int config_include(struct config *c, const char *path) {
   return ret;
 }
 
-/* make a new default config */
+/** @brief Make a new default configuration */
 static struct config *config_default(void) {
   struct config *c = xmalloc(sizeof *c);
   const char *logname;
@@ -875,6 +968,7 @@ static struct config *config_default(void) {
   c->sample_format.channels = 2;
   c->sample_format.byte_format = AO_FMT_NATIVE;
   c->queue_pad = 10;
+  c->speaker_backend = -1;
   return c;
 }
 
@@ -885,12 +979,13 @@ static char *get_file(struct config *c, const char *name) {
   return s;
 }
 
+/** @brief Set the default configuration file */
 static void set_configfile(void) {
   if(!configfile)
     byte_xasprintf(&configfile, "%s/config", pkgconfdir);
 }
 
-/* free the config file */
+/** @brief Free a configuration object */
 static void config_free(struct config *c) {
   int n;
 
@@ -904,6 +999,7 @@ static void config_free(struct config *c) {
   }
 }
 
+/** @brief Set post-parse defaults */
 static void config_postdefaults(struct config *c) {
   struct config_state cs;
   const struct conf *whoami;
@@ -940,9 +1036,26 @@ static void config_postdefaults(struct config *c) {
     for(n = 0; n < NTRANSFORM; ++n)
       set_transform(&cs, whoami, 5, (char **)transform[n]);
   }
+  if(c->speaker_backend == -1) {
+    if(c->speaker_command)
+      c->speaker_backend = BACKEND_COMMAND;
+    else if(c->broadcast.n)
+      c->speaker_backend = BACKEND_NETWORK;
+    else {
+#if API_ALSA
+      c->speaker_backend = BACKEND_ALSA;
+#else
+      c->speaker_backend = BACKEND_COMMAND;
+#endif
+    }
+  }
+  if(c->speaker_backend == BACKEND_COMMAND && !c->speaker_command)
+    fatal(0, "speaker_backend is command but speaker_command is not set");
+  if(c->speaker_backend == BACKEND_NETWORK && !c->broadcast.n)
+    fatal(0, "speaker_backend is network but broadcast is not set");
 }
 
-/* re-read the config file */
+/** @brief (Re-)read the config file */
 int config_read() {
   struct config *c;
   char *privconf;
@@ -980,6 +1093,7 @@ int config_read() {
   return 0;
 }
 
+/** @brief Return the path to the private configuration file */
 char *config_private(void) {
   char *s;
 
@@ -988,6 +1102,7 @@ char *config_private(void) {
   return s;
 }
 
+/** @brief Return the path to user's personal configuration file */
 char *config_userconf(const char *home, const struct passwd *pw) {
   char *s;
 
@@ -995,7 +1110,8 @@ char *config_userconf(const char *home, const struct passwd *pw) {
   return s;
 }
 
-char *config_usersysconf(const struct passwd *pw ) {
+/** @brief Return the path to user-specific system configuration */
+char *config_usersysconf(const struct passwd *pw) {
   char *s;
 
   set_configfile();
index c304921..c7b5b09 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ * Copyright (C) 2004, 2005, 2006, 2007 Richard Kettlewell
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -29,24 +29,37 @@ struct real_pcre;
  * is always pointed to by @config@.  Values in @config@ are UTF-8 encoded.
  */
 
+/** @brief A list of strings */
 struct stringlist {
+  /** @brief Number of strings */
   int n;
+  /** @brief Array of strings */
   char **s;
 };
 
+/** @brief A list of list of strings */
 struct stringlistlist {
+  /** @brief Number of string lists */
   int n;
+  /** @brief Array of string lists */
   struct stringlist *s;
 };
 
+/** @brief A collection of tracks */
 struct collection {
+  /** @brief Module that supports this collection */
   char *module;
+  /** @brief Filename encoding */
   char *encoding;
+  /** @brief Root directory */
   char *root;
 };
 
+/** @brief A list of collections */
 struct collectionlist {
+  /** @brief Number of collections */
   int n;
+  /** @brief Array of collections */
   struct collection *s;
 };
 
@@ -76,52 +89,137 @@ struct transformlist {
   struct transform *t;
 };
 
+/** @brief System configuration */
 struct config {
   /* server config */
-  struct stringlistlist player;                /* players */
-  struct stringlistlist allow;         /* allowed users */
-  struct stringlist scratch;           /* scratch tracks */
-  long gap;                            /* gap between tracks */
-  long history;                                /* length of history */
-  struct stringlist trust;             /* trusted users */
-  const char *user;                    /* user to run as */
-  long nice_rescan;                    /* rescan subprocess niceness */
-  struct stringlist plugins;           /* plugin path */
-  struct stringlist stopword;          /* stopwords for track search */
-  struct collectionlist collection;    /* track collections */
+
+  /** @brief All players */
+  struct stringlistlist player;
+
+  /** @brief Allowed users */
+  struct stringlistlist allow;
+
+  /** @brief Scratch tracks */
+  struct stringlist scratch;
+
+  /** @brief Gap between tracks in seconds */
+  long gap;
+
+  /** @brief Maximum number of recent tracks to record in history */
+  long history;
+
+  /** @brief Trusted users */
+  struct stringlist trust;
+
+  /** @brief User for server to run as */
+  const char *user;
+
+  /** @brief Nice value for rescan subprocess */
+  long nice_rescan;
+
+  /** @brief Paths to search for plugins */
+  struct stringlist plugins;
+
+  /** @brief List of stopwords */
+  struct stringlist stopword;
+
+  /** @brief List of collections */
+  struct collectionlist collection;
+
+  /** @brief Database checkpoint byte limit */
   long checkpoint_kbyte;
+
+  /** @brief Databsase checkpoint minimum */
   long checkpoint_min;
-  char *mixer;                         /* mixer device file */
-  char *channel;                       /* mixer channel */
-  long prefsync;                       /* preflog sync intreval */
-  struct stringlist listen;            /* secondary listen address */
-  const char *alias;                   /* alias format */
-  int lock;                            /* server takes a lock */
-  long nice_server;                    /* nice value for server */
-  long nice_speaker;                   /* nice value for speaker */
-  const char *speaker_command;         /* command for speaker to run */
-  ao_sample_format sample_format;      /* sample format to enforce */
-  long sox_generation;                 /* sox syntax generation */
-  /* shared client/server config */
-  const char *home;                    /* home directory for state files */
-  /* client config */
-  const char *username, *password;     /* our own username and password */
-  struct stringlist connect;           /* connect address */
-  /* web config */
-  struct stringlist templates;         /* template path */
-  const char *url;                     /* canonical URL */
-  long refresh;                                /* maximum refresh period */
+
+  /** @brief Path to mixer device */
+  char *mixer;
+
+  /** @brief Mixer channel to use */
+  char *channel;
+
+  long prefsync;                       /* preflog sync interval */
+
+  /** @brief Secondary listen address */
+  struct stringlist listen;
+
+  /** @brief Alias format string */
+  const char *alias;
+
+  /** @brief Enable server locking */
+  int lock;
+
+  /** @brief Nice value for server */
+  long nice_server;
+
+  /** @brief Nice value for speaker */
+  long nice_speaker;
+
+  /** @brief Command execute by speaker to play audio */
+  const char *speaker_command;
+
+  /** @brief Target sample format */
+  ao_sample_format sample_format;
+
+  /** @brief Sox syntax generation */
+  long sox_generation;
+
+  /** @brief Speaker backend
+   *
+   * Choices are @ref BACKEND_ALSA, @ref BACKEND_COMMAND or @ref
+   * BACKEND_NETWORK.
+   */
+  int speaker_backend;
+#define BACKEND_ALSA 0                 /**< Use ALSA (Linux only) */
+#define BACKEND_COMMAND 1              /**< Execute a command */
+#define BACKEND_NETWORK 2              /**< Transmit RTP  */
+
+  /** @brief Home directory for state files */
+  const char *home;
+
+  /** @brief Login username */
+  const char *username;
+
+  /** @brief Login password */
+  const char *password;
+
+  /** @brief Address to connect to */
+  struct stringlist connect;
+
+  /** @brief Directories to search for web templates */
+  struct stringlist templates;
+
+  /** @brief Canonical URL of web interface */
+  const char *url;
+
+  /** @brief Maximum refresh interval for web interface (seconds) */
+  long refresh;
+
+  /** @brief Facilities restricted to trusted users
+   *
+   * A bitmap of @ref RESTRICT_SCRATCH, @ref RESTRICT_REMOVE and @ref
+   * RESTRICT_MOVE.
+   */
   unsigned restrictions;               /* restrictions */
-  long queue_pad;                      /* how far to pad queue with
-                                        * random tracks */
-#define RESTRICT_SCRATCH 1
-#define RESTRICT_REMOVE 2
-#define RESTRICT_MOVE 4
+#define RESTRICT_SCRATCH 1             /**< Restrict scratching */
+#define RESTRICT_REMOVE 2              /**< Restrict removal */
+#define RESTRICT_MOVE 4                        /**< Restrict rearrangement */
+
+  /** @brief Target queue length */
+  long queue_pad;
+
   struct namepartlist namepart;                /* transformations */
-  int signal;                          /* termination signal */
-  const char *device;                  /* ALSA output device */
+
+  /** @brief Termination signal for subprocesses */
+  int signal;
+
+  /** @brief ALSA output device */
+  const char *device;
   struct transformlist transform;      /* path name transformations */
 
+  struct stringlist broadcast;         /* audio broadcast address */
+  struct stringlist broadcast_from;    /* audio broadcast source address */
+
   /* derived values: */
   int nparts;                          /* number of distinct name parts */
   char **parts;                                /* name part list  */
index 5e3795f..494597e 100644 (file)
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/defs.c @brief Definitions chosen by configure
+ *
+ * The binary directories are included so that they can be appended to the path
+ * (see fix_path()), not so that the path can be ignored.
+ */
 
 #include <config.h>
 #include "types.h"
 #include "defs.h"
 #include "definitions.h"
 
+/** @brief Software version number */
 const char disorder_version_string[] = VERSION;
+
+/** @brief Package library directory */
 const char pkglibdir[] = PKGLIBDIR;
+
+/** @brief Package configuration directory */
 const char pkgconfdir[] = PKGCONFDIR;
+
+/** @brief Package variable state directory */
 const char pkgstatedir[] = PKGSTATEDIR;
+
+/** @brief Package fixed data directory */
 const char pkgdatadir[] = PKGDATADIR;
 
+/** @brief Binary directory */
+const char bindir[] = BINDIR;
+
+/** @brief System binary directory */
+const char sbindir[] = SBINDIR;
+
+/** @brief Fink binary directory */
+const char finkbindir[] = FINKBINDIR;
+
 /*
 Local Variables:
 c-basic-offset:2
index 4f18012..c14bb9b 100644 (file)
@@ -26,6 +26,9 @@ extern const char pkglibdir[];
 extern const char pkgconfdir[];
 extern const char pkgstatedir[];
 extern const char pkgdatadir[];
+extern const char bindir[];
+extern const char sbindir[];
+extern const char finkbindir[];
 
 #endif /* DEFS_H */
 
index 8e95122..f9311e5 100644 (file)
@@ -21,7 +21,7 @@
 #ifndef ECLIENT_H
 #define ECLIENT_H
 
-/* Asynchronous client interface.  You must provide disorder_client_poll(). */
+/* Asynchronous client interface */
 
 typedef struct disorder_eclient disorder_eclient;
 
diff --git a/lib/heap.h b/lib/heap.h
new file mode 100644 (file)
index 0000000..56821bf
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file lib/heap.h @brief Binary heap template */
+
+#ifndef HEAP_H
+#define HEAP_H
+
+/** @brief Binary heap template.
+ * @param NAME name of type to define
+ * @param ETYPE element type
+ * @param LT comparison function
+ *
+ * Defines a heap type called @c struct @p NAME and a number of functions to
+ * operate on it.
+ *
+ * The element type of the heap will be @p ETYPE.
+ *
+ * @p LT will be called with two arguments of type @p ETYPE, and
+ * implements a less-than comparison.
+ *
+ * The functions defined are:
+ * - NAME_init(h) which initializes an empty heap at @p h
+ * - NAME_count(h) which returns the number of elements in the heap
+ * - NAME_insert(h, e) which inserts @p e into @p h
+ * - NAME_first(g) which returns the least element of @p h
+ * - NAME_remove(g) which removes and returns the least element of @p h
+ *
+ * The heap is implemented as a vector.  Element 0 is the root.  For any
+ * element \f$i\f$, its children are elements \f$2i+1\f$ and \f$2i+2\f$ and
+ * consequently its parent (if it is not the root) is
+ * \f$\lfloor(i-1)/2\rfloor\f$.
+ * 
+ * The insert and remove operations maintain two invariants: the @b
+ * shape property (all levels of the tree are fully filled except the
+ * deepest, and that is filled from the left), and the @b heap
+ * property, that every element compares less than or equal to its
+ * children.
+ *
+ * The shape property implies that the array representation has no gaps, which
+ * is convenient.  It is preserved by only adding or removing the final element
+ * of the array and otherwise only modifying the array by swapping pairs of
+ * elements.
+ *
+ * @b Insertion works by inserting the new element \f$N\f$ at the end and
+ * bubbling it up the tree until it is in the right order for its branch.
+ * - If, for its parent \f$P\f$, \f$P \le N\f$ then it is already in the right
+ * place and the insertion is complete.
+ * - Otherwise \f$P > N\f$ and so \f$P\f$ and \f$N\f$ are exchanged.  If
+ * \f$P\f$ has a second child, \f$C\f$, then \f$N < P < C\f$ so the heap
+ * property is now satisfied from \f$P\f$ down.
+ *
+ * @b Removal works by first swapping the root with the final element (and then
+ * removing it) and then bubbling the new root \f$N\f$ down the tree until it
+ * finds its proper place.  At each stage it is compared with its children
+ * \f$A\f$ and \f$B\f$.
+ * - If \f$N \le A\f$ and \f$N \le B\f$ then it is in the
+ * right place already.
+ * - Otherwise \f$N > A\f$ or \f$N > B\f$ (or both).  WLOG \f$A \le B\f$.
+ * \f$N\f$ and \f$A\f$ are exchanged, so now \f$A\f$ has children \f$N\f$ and
+ * \f$B\f$.  \f$A < N\f$ and \f$A \le B\f$.
+ */
+#define HEAP_TYPE(NAME, ETYPE, LT)                                      \
+  typedef ETYPE NAME##_element;                                         \
+  VECTOR_TYPE(NAME, NAME##_element, xrealloc);                          \
+                                                                        \
+  static inline int NAME##_count(struct NAME *heap) {                   \
+    return heap->nvec;                                                  \
+  }                                                                     \
+                                                                        \
+  static inline NAME##_element NAME##_first(struct NAME *heap) {        \
+    assert(heap->nvec > 0);                                             \
+    return heap->vec[0];                                                \
+  }                                                                     \
+                                                                        \
+  static void NAME##_insert(struct NAME *heap, NAME##_element elt) {    \
+    int n = heap->nvec;                                                 \
+    NAME##_append(heap, elt);                                           \
+    while(n > 0) {                                                      \
+      const int p = (n-1)/2;                                            \
+      if(!LT(heap->vec[n],heap->vec[p]))                                \
+        break;                                                          \
+      else {                                                            \
+        const NAME##_element t = heap->vec[n];                          \
+        heap->vec[n] = heap->vec[p];                                    \
+        heap->vec[p] = t;                                               \
+        n = p;                                                          \
+      }                                                                 \
+    }                                                                   \
+  }                                                                     \
+                                                                        \
+  static NAME##_element NAME##_remove(struct NAME *heap) {              \
+    int n = 0;                                                          \
+    NAME##_element r;                                                   \
+                                                                        \
+    assert(heap->nvec > 0);                                             \
+    r = heap->vec[0];                                                   \
+    heap->vec[0] = heap->vec[--heap->nvec];                             \
+    while(2 * n + 1 < heap->nvec) {                                     \
+      int a = 2 * n + 1;                                                \
+      int b = 2 * n + 2;                                                \
+                                                                        \
+      if(b < heap->nvec && LT(heap->vec[b],heap->vec[a])) {             \
+        ++a;                                                            \
+        --b;                                                            \
+      }                                                                 \
+      if(LT(heap->vec[a], heap->vec[n])) {                              \
+        const NAME##_element t = heap->vec[n];                          \
+        heap->vec[n] = heap->vec[a];                                    \
+        heap->vec[a] = t;                                               \
+        n = a;                                                          \
+      } else                                                            \
+        break;                                                          \
+    }                                                                   \
+    return r;                                                           \
+  }                                                                     \
+                                                                        \
+  struct heap_swallow_semicolon
+  
+
+#endif /* PQUEUE_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index bd4774a..42b15c0 100644 (file)
--- a/lib/hex.c
+++ b/lib/hex.c
@@ -17,6 +17,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/hex.c @brief Hexadecimal encoding and decoding */
 
 #include <config.h>
 #include "types.h"
 #include "mem.h"
 #include "log.h"
 
+/** @brief Convert a byte sequence to hex
+ * @param ptr Pointer to first byte
+ * @param n Number of bytes
+ * @return Allocated string containing hexdump
+ */
 char *hex(const uint8_t *ptr, size_t n) {
   char *buf = xmalloc_noptr(n * 2 + 1), *p = buf;
 
   while(n-- > 0)
     p += sprintf(p, "%02x", (unsigned)*ptr++);
+  *p = 0;
   return buf;
 }
 
+/** @brief Convert a character to its value as a hex digit
+ * @param c Character code
+ * @return Value has hex digit or -1
+ *
+ * The 'q' stands for 'quiet' - this function does not report errors.
+ */
 int unhexdigitq(int c) {
   switch(c) {
   case '0': return 0;
@@ -58,6 +71,13 @@ int unhexdigitq(int c) {
   }
 }
 
+/** @brief Convert a character to its value as a hex digit
+ * @param c Character code
+ * @return Value has hex digit or -1
+ *
+ * If the character is not a valid hex digit then an error is logged.
+ * See @ref unhexdigitq() if that is a problem.
+ */
 int unhexdigit(int c) {
   int d;
 
@@ -65,6 +85,17 @@ int unhexdigit(int c) {
   return d;
 }
 
+/** @brief Convert a hex string to bytes
+ * @param s Pointer to hex string
+ * @param np Where to store byte string length or NULL
+ * @return Allocated buffer, or 0
+ *
+ * @p s should point to a 0-terminated string containing an even number
+ * of hex digits.  They are converted to bytes and returned via the return
+ * value and optionally the length via @p np.
+ *
+ * On any error a message is logged and a null pointer is returned.
+ */
 uint8_t *unhex(const char *s, size_t *np) {
   size_t l;
   uint8_t *buf, *p;
index 51577df..b0147a6 100644 (file)
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/log-impl.h @brief Errors and logging */
 
+/** @brief Log an error and quit
+ *
+ * If @c ${DISORDER_FATAL_ABORT} is defined (as anything) then the process
+ * is aborted, so you can get a backtrace.
+ */
 void disorder_fatal(int errno_value, const char *msg, ...) {
   va_list ap;
 
@@ -28,6 +34,7 @@ void disorder_fatal(int errno_value, const char *msg, ...) {
   exitfn(EXIT_FAILURE);
 }
 
+/** @brief Log an error */
 void disorder_error(int errno_value, const char *msg, ...) {
   va_list ap;
 
@@ -36,6 +43,7 @@ void disorder_error(int errno_value, const char *msg, ...) {
   va_end(ap);
 }
 
+/** @brief Log an informational message */
 void disorder_info(const char *msg, ...) {
   va_list ap;
 
index 1705642..7b2ef3d 100644 (file)
--- a/lib/log.c
+++ b/lib/log.c
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/log.c @brief Errors and logging
+ *
+ * All messages are initially emitted by one of the four functions below.
+ * debug() is generally invoked via D() so that mostly you just do a test
+ * rather than a complete subroutine call.
+ *
+ * Messages are dispatched via @ref log_default.  This defaults to @ref
+ * log_stderr.  daemonize() will turn off @ref log_stderr and use @ref
+ * log_syslog instead.
+ *
+ * fatal() will call exitfn() with a nonzero status.  The default value is
+ * exit(), but it should be set to _exit() anywhere but the 'main line' of the
+ * program, to guarantee that exit() gets called at most once.
+ */
 
 #define NO_MEMORY_ALLOCATION
 /* because the memory allocation functions report errors */
 #include "disorder.h"
 #include "printf.h"
 
+/** @brief Definition of a log output */
 struct log_output {
+  /** @brief Function to call */
   void (*fn)(int pri, const char *msg, void *user);
+  /** @brief User data */
   void *user;
 };
 
+/** @brief Function to call on a fatal error
+ *
+ * This is normally @c exit() but in the presence of @c fork() it
+ * sometimes gets set to @c _exit(). */
 void (*exitfn)(int) attribute((noreturn)) = exit;
+
+/** @brief Debug flag */
 int debugging;
+
+/** @brief Program name */
 const char *progname;
+
+/** @brief Filename for debug messages */
 const char *debug_filename;
+
+/** @brief Line number for debug messages */
 int debug_lineno;
+
+/** @brief Pointer to chosen log output structure */
 struct log_output *log_default = &log_stderr;
 
+/** @brief Filename to debug for */
 static const char *debug_only;
 
-/* we might be receiving things in any old encoding, or binary rubbish in no
- * encoding at all, so escape anything we don't like the look of */
+/** @brief Construct log line, encoding special characters
+ *
+ * We might be receiving things in any old encoding, or binary rubbish
+ * in no encoding at all, so escape anything we don't like the look
+ * of.  We limit the log message to a kilobyte.
+ */
 static void format(char buffer[], size_t bufsize, const char *fmt, va_list ap) {
   char t[1024];
   const char *p;
@@ -74,7 +110,11 @@ static void format(char buffer[], size_t bufsize, const char *fmt, va_list ap) {
   buffer[n] = 0;
 }
 
-/* log to a file */
+/** @brief Log to a file
+ * @param pri Message priority (as per syslog)
+ * @param msg Messagge to log
+ * @param user The @c FILE @c * to log to or NULL for @c stderr
+ */
 static void logfp(int pri, const char *msg, void *user) {
   struct timeval tv;
   FILE *fp = user ? user : stderr;
@@ -106,7 +146,7 @@ static void logfp(int pri, const char *msg, void *user) {
   fputc('\n', fp);
 }
 
-/* log to syslog */
+/** @brief Log to syslog */
 static void logsyslog(int pri, const char *msg,
                      void attribute((unused)) *user) {
   if(pri < LOG_DEBUG)
@@ -115,10 +155,13 @@ static void logsyslog(int pri, const char *msg,
     syslog(pri, "%s:%d: %s", debug_filename, debug_lineno, msg);
 }
 
+/** @brief Log output that writes to @c stderr */
 struct log_output log_stderr = { logfp, 0 };
+
+/** @brief Log output that sends to syslog */
 struct log_output log_syslog = { logsyslog, 0 };
 
-/* log to all log outputs */
+/** @brief Format and log a message */
 static void vlogger(int pri, const char *fmt, va_list ap) {
   char buffer[1024];
 
@@ -126,7 +169,7 @@ static void vlogger(int pri, const char *fmt, va_list ap) {
   log_default->fn(pri, buffer, log_default->user);
 }
 
-/* wrapper for vlogger */
+/** @brief Format and log a message */
 static void logger(int pri, const char *fmt, ...) {
   va_list ap;
 
@@ -135,7 +178,12 @@ static void logger(int pri, const char *fmt, ...) {
   va_end(ap);
 }
 
-/* internals of fatal/error/info */
+/** @brief Format and log a message
+ * @param pri Message priority (as per syslog)
+ * @param fmt Format string
+ * @param errno_value Errno value to include as a string, or 0
+ * @param ap Argument list
+ */
 void elog(int pri, int errno_value, const char *fmt, va_list ap) {
   char buffer[1024];
 
@@ -156,6 +204,7 @@ void elog(int pri, int errno_value, const char *fmt, va_list ap) {
 /* shared implementation of vararg functions */
 #include "log-impl.h"
 
+/** @brief Log a debug message */
 void debug(const char *msg, ...) {
   va_list ap;
 
@@ -164,6 +213,7 @@ void debug(const char *msg, ...) {
   va_end(ap);
 }
 
+/** @brief Set the program name from @c argc */
 void set_progname(char **argv) {
   if((progname = strrchr(argv[0], '/')))
     ++progname;
index f23dcd7..85a6e05 100644 (file)
--- a/lib/log.h
+++ b/lib/log.h
 #ifndef LOG_H
 #define LOG_H
 
-/* All messages are initially emitted by one of the four functions below.
- * debug() is generally invoked via D() so that mostly you just do a test
- * rather than a complete subroutine call.
- *
- * Messages are dispatched via log_default.  This defaults to log_stderr.
- * daemonize() will turn off log_stderr and use log_syslog instead.
- *
- * fatal() will call exitfn() with a nonzero status.  The default value is
- * exit(), but it should be set to _exit() anywhere but the 'main line' of the
- * program, to guarantee that exit() gets called at most once.
- */
-
 #include <stdarg.h>
 
 struct log_output;
 
 void set_progname(char **argv);
-/* set progname from argv[0] */
 
 void elog(int pri, int errno_value, const char *fmt, va_list ap);
-/* internals of fatal/error/info/debug */
 
 void fatal(int errno_value, const char *msg, ...) attribute((noreturn))
   attribute((format (printf, 2, 3)));
index c2dd40c..6d531cc 100644 (file)
--- a/lib/mem.c
+++ b/lib/mem.c
@@ -17,6 +17,9 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/mem.c
+ * @brief Memory management
+ */
 
 #include <config.h>
 #include "types.h"
 
 #include "disorder.h"
 
+/** @brief Allocate and zero out
+ * @param n Number of bytes to allocate
+ * @return Pointer to allocated memory,  or 0
+ */
 static void *malloc_and_zero(size_t n) {
   void *ptr = malloc(n);
 
@@ -55,6 +62,12 @@ static void *(*do_malloc_atomic)(size_t) = malloc;
 static void (*do_free)(void *) = free;
 #endif
 
+/** @brief Initialize memory management
+ *
+ * Must be called by all programs that use garbage collection.  Define
+ * @c ${DISORDER_GC} to @c no to suppress use of the collector
+ * (e.g. for debugging purposes).
+ */
 void mem_init(void) {
 #if GC
   const char *e;
@@ -71,6 +84,13 @@ void mem_init(void) {
 #endif
 }
 
+/** @brief Allocate memory
+ * @param n Bytes to allocate
+ * @return Pointer to allocated memory
+ *
+ * Terminates the process on error.  The allocated memory is always
+ * 0-filled.
+ */
 void *xmalloc(size_t n) {
   void *ptr;
 
@@ -79,18 +99,42 @@ void *xmalloc(size_t n) {
   return ptr;
 }
 
+/** @brief Reallocate memory
+ * @param ptr Block to reallocated
+ * @param n Bytes to allocate
+ * @return Pointer to allocated memory
+ *
+ * Terminates the process on error.  It is NOT guaranteed that any
+ * additional memory allocated is 0-filled.
+ */
 void *xrealloc(void *ptr, size_t n) {
   if(!(ptr = do_realloc(ptr, n)) && n)
     fatal(errno, "error allocating memory");
   return ptr;
 }
 
+/** @brief Allocate memory
+ * @param count Number of objects to allocate
+ * @param size Size of one object
+ * @return Pointer to allocated memory
+ *
+ * Terminates the process on error.  The allocated memory is always
+ * 0-filled.
+ */
 void *xcalloc(size_t count, size_t size) {
   if(count > SIZE_MAX / size)
     fatal(0, "excessively large calloc");
   return xmalloc(count * size);
 }
 
+/** @brief Allocate memory
+ * @param n Bytes to allocate
+ * @return Pointer to allocated memory
+ *
+ * Terminates the process on error.  The allocated memory is not
+ * guaranteed to be 0-filled and is not suitable for storing pointers
+ * in.
+ */
 void *xmalloc_noptr(size_t n) {
   void *ptr;
 
@@ -99,6 +143,15 @@ void *xmalloc_noptr(size_t n) {
   return ptr;
 }
 
+/** @brief Reallocate memory
+ * @param ptr Block to reallocated
+ * @param n Bytes to allocate
+ * @return Pointer to allocated memory
+ *
+ * Terminates the processf on error.  It is NOT guaranteed that any
+ * additional memory allocated is 0-filled.  The block must have been
+ * allocated with xmalloc_noptr() (or xrealloc_noptr()) initially.
+ */
 void *xrealloc_noptr(void *ptr, size_t n) {
   if(ptr == 0)
     return xmalloc_noptr(n);
@@ -107,6 +160,12 @@ void *xrealloc_noptr(void *ptr, size_t n) {
   return ptr;
 }
 
+/** @brief Duplicate a string
+ * @param s String to copy
+ * @return New copy of string
+ *
+ * This uses the equivalent of xmalloc_noptr() to allocate the new string.
+ */
 char *xstrdup(const char *s) {
   char *t;
 
@@ -115,6 +174,14 @@ char *xstrdup(const char *s) {
   return strcpy(t, s);
 }
 
+/** @brief Duplicate a prefix of a string
+ * @param s String to copy
+ * @param n Prefix of string to copy
+ * @return New copy of string
+ *
+ * This uses the equivalent of xmalloc_noptr() to allocate the new string.
+ * @p n must not exceed the length of the string.
+ */
 char *xstrndup(const char *s, size_t n) {
   char *t;
 
@@ -125,6 +192,9 @@ char *xstrndup(const char *s, size_t n) {
   return t;
 }
 
+/** @brief Free memory
+ * @param ptr Block to free or 0
+ */
 void xfree(void *ptr) {
   do_free(ptr);
 }
diff --git a/lib/rtp.h b/lib/rtp.h
new file mode 100644 (file)
index 0000000..63ade9b
--- /dev/null
+++ b/lib/rtp.h
@@ -0,0 +1,42 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef RTP_H
+#define RTP_H
+
+/* RTP is defined in RFC1889 */
+struct attribute((packed)) rtp_header {
+  uint8_t vpxcc;
+  uint8_t mpt;
+  uint16_t seq;
+  uint32_t timestamp;
+  uint32_t ssrc;
+};
+
+#endif /* RTP_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
similarity index 78%
rename from lib/speaker.c
rename to lib/speaker-protocol.c
index 65bb87d..1c5199a 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2005 Richard Kettlewell
+ * Copyright (C) 2005, 2007 Richard Kettlewell
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -17,6 +17,9 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/speaker-protocol.c
+ * @brief Speaker/server protocol support
+ */
 
 #include <config.h>
 #include "types.h"
 #include <errno.h>
 #include <sys/uio.h>
 
-#include "speaker.h"
+#include "speaker-protocol.h"
 #include "log.h"
 
+/** @brief Send a speaker message
+ * @param fd File descriptor to send to
+ * @param sm Pointer to message
+ * @param datafd File descriptoxr to pass with message or -1
+ *
+ * @p datafd will be the output from some decoder.
+ */
 void speaker_send(int fd, const struct speaker_message *sm, int datafd) {
   struct msghdr m;
   struct iovec iov;
@@ -59,6 +69,15 @@ void speaker_send(int fd, const struct speaker_message *sm, int datafd) {
     fatal(errno, "sendmsg");
 }
 
+/** @brief Receive a speaker message
+ * @param fd File descriptor to read from
+ * @param sm Where to store received message
+ * @param datafd Where to store received file descriptor or NULL
+ * @return -ve on @c EAGAIN, 0 at EOF, +ve on success
+ *
+ * If @p datafd is NULL but a file descriptor is nonetheless received,
+ * the process is terminated with an error.
+ */
 int speaker_recv(int fd, struct speaker_message *sm, int *datafd) {
   struct msghdr m;
   struct iovec iov;
diff --git a/lib/speaker-protocol.h b/lib/speaker-protocol.h
new file mode 100644 (file)
index 0000000..eb2a1ae
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005, 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file lib/speaker-protocol.h
+ * @brief Speaker/server protocol support
+ *
+ * This file defines the protocol by which the main server and the speaker
+ * process communicate.
+ */
+
+#ifndef SPEAKER_PROTOCOL_H
+#define SPEAKER_PROTOCOL_H
+
+/** @brief A message from the main server to the speaker, or vica versa */
+struct speaker_message {
+  /** @brief Message type
+   *
+   * Messges from the main server:
+   * - @ref SM_PREPARE
+   * - @ref SM_PLAY
+   * - @ref SM_PAUSE
+   * - @ref SM_RESUME
+   * - @ref SM_CANCEL
+   * - @ref SM_RELOAD
+   *
+   * Messages from the speaker:
+   * - @ref SM_PAUSED
+   * - @ref SM_FINISHED
+   * - @ref SM_PLAYING
+   */
+  int type;
+
+  /** @brief Message-specific data */
+  long data;
+
+  /** @brief Track ID (including 0 terminator) */
+  char id[24];                          /* ID including terminator */
+};
+
+/* messages from the main DisOrder server */
+/** @brief Prepare track @c id
+ *
+ * This message will include a file descriptor.  The speaker starts buffering
+ * audio data read from this file against the time that it must be played.
+ */
+#define SM_PREPARE 0
+
+/** @brief Play track @c id
+ *
+ * The track must already have been prepared.
+ */
+#define SM_PLAY 1
+
+/** @brief Pause current track */
+#define SM_PAUSE 2
+
+/** @brief Resume current track */
+#define SM_RESUME 3
+
+/** @brief Cancel track @c id */
+#define SM_CANCEL 4
+
+/** @brief Reload configuration */
+#define SM_RELOAD 5
+
+/* messages from the speaker */
+/** @brief Paused track @c id, @c data seconds in
+ *
+ * There is no @c SM_RESUMED, instead @ref SM_PLAYING is sent after the track
+ * starts playing again.
+ */
+#define SM_PAUSED 128
+
+/** @brief Finished playing track @c id */
+#define SM_FINISHED 129
+
+/** @brief Currently track @c id, @c data seconds in
+ *
+ * This is sent from time to time while a track is playing.
+ */
+#define SM_PLAYING 131
+
+void speaker_send(int fd, const struct speaker_message *sm, int datafd);
+/* Send a message.  DATAFD is passed too if not -1.  Does not close DATAFD. */
+
+int speaker_recv(int fd, struct speaker_message *sm, int *datafd);
+/* Receive a message.  If DATAFD is not null then can receive an FD.  Return 0
+ * on EOF, +ve if a message is read, -1 on EAGAIN, terminates on any other
+ * error. */
+
+#endif /* SPEAKER_PROTOCOL_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/speaker.h b/lib/speaker.h
deleted file mode 100644 (file)
index a2da3a5..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * This file is part of DisOrder
- * Copyright (C) 2005 Richard Kettlewell
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
- * USA
- */
-
-#ifndef SPEAKER_H
-#define SPEAKER_H
-
-struct speaker_message {
-  int type;                             /* message type */
-  long data;                            /* whatever */
-  char id[24];                          /* ID including terminator */
-};
-
-/* messages from the main DisOrder server */
-#define SM_PREPARE 0                    /* prepare ID */
-#define SM_PLAY 1                       /* play ID */
-#define SM_PAUSE 2                      /* pause current track */
-#define SM_RESUME 3                     /* resume current track */
-#define SM_CANCEL 4                     /* cancel ID */
-#define SM_RELOAD 5                     /* reload configuration */
-
-/* messages from the speaker */
-#define SM_PAUSED 128                   /* paused ID, DATA seconds in */
-#define SM_FINISHED 129                 /* finished ID */
-#define SM_PLAYING 131                  /* playing ID, DATA seconds in */
-
-void speaker_send(int fd, const struct speaker_message *sm, int datafd);
-/* Send a message.  DATAFD is passed too if not -1.  Does not close DATAFD. */
-
-int speaker_recv(int fd, struct speaker_message *sm, int *datafd);
-/* Receive a message.  If DATAFD is not null then can receive an FD.  Return 0
- * on EOF, +ve if a message is read, -1 on EAGAIN, terminates on any other
- * error. */
-
-#endif /* SPEAKER_H */
-
-/*
-Local Variables:
-c-basic-offset:2
-comment-column:40
-fill-column:79
-indent-tabs-mode:nil
-End:
-*/
index 23d6366..ed90183 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2005 Richard Kettlewell
+ * Copyright (C) 2005, 2007 Richard Kettlewell
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -17,6 +17,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/test.c @brief Library tests */
 
 #include <config.h>
 #include "types.h"
@@ -26,6 +27,7 @@
 #include <stdlib.h>
 #include <errno.h>
 #include <ctype.h>
+#include <assert.h>
 
 #include "utf8.h"
 #include "mem.h"
 #include "mime.h"
 #include "hex.h"
 #include "words.h"
+#include "heap.h"
 
 static int tests, errors;
 
+/** @brief Checks that @p expr is nonzero */
 #define insist(expr) do {                              \
-  if(!expr) {                                          \
+  if(!(expr)) {                                                \
     ++errors;                                          \
     fprintf(stderr, "%s:%d: error checking %s\n",      \
             __FILE__, __LINE__, #expr);                        \
@@ -113,6 +117,8 @@ static void test_utf8(void) {
   insist(!strcmp(u8, CHARS));                  \
 } while(0)
 
+  fprintf(stderr, "test_utf8\n");
+
   /* empty string */
 
   U8("", "");
@@ -210,6 +216,8 @@ static void test_utf8(void) {
 static void test_mime(void) {
   char *t, *n, *v;
 
+  fprintf(stderr, "test_mime\n");
+
   t = n = v = 0;
   insist(!mime_content_type("text/plain", &t, &n, &v));
   insist(!strcmp(t, "text/plain"));
@@ -279,6 +287,8 @@ static void test_hex(void) {
   uint8_t *u;
   size_t ul;
 
+  fprintf(stderr, "test_hex\n");
+
   for(n = 0; n <= UCHAR_MAX; ++n) {
     if(!isxdigit(n))
       insist(unhexdigitq(n) == -1);
@@ -315,15 +325,18 @@ static void test_hex(void) {
   insist(memcmp(u, h, 4) == 0);
   u = unhex("", &ul);
   insist(ul == 0);
-  fprintf(stderr, "2 ERROR reports expected:\n");
+  fprintf(stderr, "2 ERROR reports expected {\n");
   insist(unhex("F", 0) == 0);
   insist(unhex("az", 0) == 0);
+  fprintf(stderr, "}\n");
 }
 
 static void test_casefold(void) {
   uint32_t c, l, u[2];
   const char *s, *ls;
 
+  fprintf(stderr, "test_casefold\n");
+
   for(c = 1; c < 256; ++c) {
     u[0] = c;
     u[1] = 0;
@@ -361,6 +374,34 @@ static void test_casefold(void) {
   check_string(casefold(""), "");
 }
 
+/** @brief Less-than comparison function for integer heap */
+static inline int int_lt(int a, int b) { return a < b; }
+
+/** @struct iheap
+ * @brief A heap with @c int elements */
+HEAP_TYPE(iheap, int, int_lt);
+
+/** @brief Tests for @ref heap.h */
+static void test_heap(void) {
+  struct iheap h[1];
+  int n;
+  int last = -1;
+
+  fprintf(stderr, "test_heap\n");
+
+  iheap_init(h);
+  for(n = 0; n < 1000; ++n)
+    iheap_insert(h, random() % 100);
+  for(n = 0; n < 1000; ++n) {
+    const int latest = iheap_remove(h);
+    if(last > latest)
+      fprintf(stderr, "should have %d <= %d\n", last, latest);
+    insist(last <= latest);
+    last = latest;
+  }
+  putchar('\n');
+}
+
 int main(void) {
   insist('\n' == 0x0A);
   insist('\r' == 0x0D);
@@ -380,6 +421,8 @@ int main(void) {
   /* configuration.c */
   /* event.c */
   /* fprintf.c */
+  /* heap.c */
+  test_heap();
   /* hex.c */
   test_hex();
   /* inputline.c */
diff --git a/lib/timeval.h b/lib/timeval.h
new file mode 100644 (file)
index 0000000..9ec8c31
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef TIMEVAL_H
+#define TIMEVAL_H
+
+static inline struct timeval tvsub(const struct timeval a,
+                                   const struct timeval b) {
+  struct timeval r;
+
+  r.tv_sec = a.tv_sec - b.tv_sec;
+  r.tv_usec = a.tv_usec - b.tv_usec;
+  if(r.tv_usec < 0) {
+    r.tv_usec += 1000000;
+    r.tv_sec--;
+  }
+  if(r.tv_usec > 999999) {
+    r.tv_usec -= 1000000;
+    r.tv_sec++;
+  }
+  return r;
+}
+
+static inline int64_t tvsub_us(const struct timeval a,
+                               const struct timeval b) {
+  return (((uint64_t)a.tv_sec * 1000000 + a.tv_usec)
+          - ((uint64_t)b.tv_sec * 1000000 + b.tv_usec));
+}
+
+#endif /* TIMEVAL_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index f40fe38..081a71d 100644 (file)
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/vector.h @brief Dynamic array template */
 
 #ifndef VECTOR_H
 #define VECTOR_H
 
+/** @brief Dynamic array template
+ * @param NAME type name
+ * @param ETYPE element type
+ * @param REALLOC realloc function
+ *
+ * Defines @c struct @p NAME as a dynamic array with element type @p
+ * ETYPE.  @p REALLOC should have the same signature as realloc() and
+ * will be used for all memory allocation.  Typically it would be
+ * xrealloc() for pointer-containing element types and
+ * xrealloc_noptr() for pointer-free element types.
+ *
+ * Clients are inspected to read the @p vec member of the structure,
+ * which points to the first element, and the @p nvec member, which is
+ * the number of elements.  It is safe to reduce @p nvec.  Do not
+ * touch any other members.
+ *
+ * The functions defined are:
+ * - NAME_init(struct NAME *v) which initializes @p v
+ * - NAME_append(struct NAME *v, ETYPE value) which appends @p value to @p v
+ * - NAME_terminate(struct NAME *v) which zeroes out the element beyond the last
+ */
 #define VECTOR_TYPE(NAME,ETYPE,REALLOC)                                \
                                                                \
 struct NAME {                                                  \
+  /** @brief Pointer to elements */                            \
   ETYPE *vec;                                                  \
-  int nvec, nslots;                                            \
+  /** @brief Number of elements */                             \
+  int nvec;                                                    \
+  /** @brief Number of slots */                                        \
+  int nslots;                                                  \
 };                                                             \
                                                                \
 static inline void NAME##_init(struct NAME *v) {               \
@@ -45,15 +71,23 @@ static inline void NAME##_terminate(struct NAME *v) {               \
     v->vec = REALLOC(v->vec, ++v->nslots * sizeof(ETYPE));     \
   memset(&v->vec[v->nvec], 0, sizeof (ETYPE));                 \
 }                                                              \
+                                                               \
+struct vector_swallow_semicolon
 
-VECTOR_TYPE(vector, char *, xrealloc)
-VECTOR_TYPE(dynstr, char, xrealloc_noptr)
-VECTOR_TYPE(dynstr_ucs4, uint32_t, xrealloc_noptr)
+/** @brief A dynamic array of pointers to strings */
+VECTOR_TYPE(vector, char *, xrealloc);
+/** @brief A dynamic string */
+VECTOR_TYPE(dynstr, char, xrealloc_noptr);
+/** @brief A dynamic unicode string */
+VECTOR_TYPE(dynstr_ucs4, uint32_t, xrealloc_noptr);
 
+/** @brief Append many strings to a @ref vector */
 void vector_append_many(struct vector *v, char **vec, int nvec);
 
+/** @brief Append @p n bytes to a @ref dynstr */
 void dynstr_append_bytes(struct dynstr *v, const char *ptr, size_t n);
 
+/** @brief Append a string to a @ref dynstr */
 static inline void dynstr_append_string(struct dynstr *v, const char *ptr) {
   dynstr_append_bytes(v, ptr, strlen(ptr));
 }
index 4b01707..92f3de2 100644 (file)
@@ -1,3 +1,4 @@
+
 #
 # This file is part of DisOrder.
 # Copyright (C) 2004, 2005, 2006, 2007 Richard Kettlewell
@@ -21,6 +22,7 @@
 sbin_PROGRAMS=disorderd disorder-deadlock disorder-rescan disorder-dump \
              disorder-speaker
 noinst_PROGRAMS=disorder.cgi trackname
+noinst_DATA=uk.org.greenend.rjk.disorder.plist
 
 AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
 
@@ -43,9 +45,12 @@ disorder_deadlock_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
        $(LIBDB) $(LIBPCRE) $(LIBICONV)
 disorder_deadlock_DEPENDENCIES=../lib/libdisorder.a
 
-disorder_speaker_SOURCES=speaker.c
+disorder_speaker_SOURCES=speaker.c speaker.h \
+                        speaker-command.c \
+                        speaker-network.c \
+                        speaker-alsa.c
 disorder_speaker_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
-       $(LIBASOUND) $(LIBPCRE) $(LIBICONV)
+       $(LIBASOUND) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT)
 disorder_speaker_DEPENDENCIES=../lib/libdisorder.a
 
 disorder_rescan_SOURCES=rescan.c                        \
@@ -90,3 +95,19 @@ check-help: all
        ./disorder-speaker --help > /dev/null
 
 cgi.o: ../lib/definitions.h
+
+# for Mac OS X >=10.4
+SEDFILES=uk.org.greenend.rjk.disorder.plist
+include ${top_srcdir}/scripts/sedfiles.make
+EXTRA_DIST=uk.org.greenend.rjk.disorder.plist.in
+LAUNCHD=/Library/LaunchDaemons
+
+install-data-hook:
+       @if [ -d ${LAUNCHD} ]; then \
+         echo $(INSTALL) -m 644 uk.org.greenend.rjk.disorder.plist ${LAUNCHD};\
+         $(INSTALL) -m 644 uk.org.greenend.rjk.disorder.plist ${LAUNCHD};\
+         echo launchctl unload ${LAUNCHD} \|\| true;\
+         launchctl unload ${LAUNCHD} || true;\
+         echo launchctl load ${LAUNCHD} \|\| true;\
+         launchctl load ${LAUNCHD} || true;\
+       fi
index 774ab6d..f2ebb9a 100644 (file)
@@ -53,6 +53,7 @@
 #include "user.h"
 #include "mixer.h"
 #include "eventlog.h"
+#include "printf.h"
 
 static ev_source *ev;
 
@@ -69,6 +70,8 @@ static const struct option options[] = {
   { "log", required_argument, 0, 'l' },
   { "pidfile", required_argument, 0, 'P' },
   { "no-initial-rescan", no_argument, 0, 'N' },
+  { "wide-open", no_argument, 0, 'w' },
+  { "syslog", no_argument, 0, 's' },
   { 0, 0, 0, 0 }
 };
 
@@ -82,6 +85,7 @@ static void help(void) {
          "  --config PATH, -c PATH   Set configuration file\n"
          "  --debug, -d              Turn on debugging\n"
          "  --foreground, -f         Do not become a daemon\n"
+         "  --syslog, -s             Log to syslog even with -f\n"
          "  --pidfile PATH, -P PATH  Leave a pidfile\n");
   xfclose(stdout);
   exit(0);
@@ -178,8 +182,27 @@ static void volumecheck_after(long offset) {
   ev_timeout(ev, 0, &w, volumecheck_again, 0);
 }
 
- int main(int argc, char **argv) {
-  int n, background = 1;
+/* We fix the path to include the bindir and sbindir we were installed into */
+static void fix_path(void) {
+  char *path = getenv("PATH");
+  char *newpath;
+
+  if(!path)
+    error(0, "PATH is not set at all!");
+
+  if(*finkbindir)
+    /* We appear to be a finkized mac; include fink on the path in case the
+     * tools we need are there. */
+    byte_xasprintf(&newpath, "PATH=%s:%s:%s:%s", 
+                  path, bindir, sbindir, finkbindir);
+  else
+    byte_xasprintf(&newpath, "PATH=%s:%s:%s", path, bindir, sbindir);
+  putenv(newpath);
+  info("%s", newpath); 
+}
+
+int main(int argc, char **argv) {
+  int n, background = 1, logsyslog = 0;
   const char *pidfile = 0;
   int initial_rescan = 1;
 
@@ -189,7 +212,7 @@ static void volumecheck_after(long offset) {
   /* garbage-collect PCRE's memory */
   pcre_malloc = xmalloc;
   pcre_free = xfree;
-  while((n = getopt_long(argc, argv, "hVc:dfP:N", options, 0)) >= 0) {
+  while((n = getopt_long(argc, argv, "hVc:dfP:Ns", options, 0)) >= 0) {
     switch(n) {
     case 'h': help();
     case 'V': version();
@@ -198,13 +221,22 @@ static void volumecheck_after(long offset) {
     case 'f': background = 0; break;
     case 'P': pidfile = optarg; break;
     case 'N': initial_rescan = 0; break;
+    case 's': logsyslog = 1; break;
+    case 'w': wideopen = 1; break;
     default: fatal(0, "invalid option");
     }
   }
   /* go into background if necessary */
   if(background)
     daemonize(progname, LOG_DAEMON, pidfile);
+  else if(logsyslog) {
+    /* If we're running under some kind of daemon supervisor then we may want
+     * to log to syslog but not to go into background */
+    openlog(progname, LOG_PID, LOG_DAEMON);
+    log_default = &log_syslog;
+  }
   info("process ID %lu", (unsigned long)getpid());
+  fix_path();
   srand(time(0));                      /* don't start the same every time */
   /* create event loop */
   ev = ev_new();
@@ -270,5 +302,6 @@ static void volumecheck_after(long offset) {
 Local Variables:
 c-basic-offset:2
 comment-column:40
+fill-column:79
 End:
 */
index e8df3e8..94567c4 100644 (file)
@@ -47,7 +47,7 @@
 #include "eventlog.h"
 #include "logfd.h"
 #include "syscalls.h"
-#include "speaker.h"
+#include "speaker-protocol.h"
 #include "disorder.h"
 #include "signame.h"
 #include "hash.h"
index 6e15115..5837e38 100644 (file)
 
 int volume_left, volume_right;         /* last known volume */
 
+/** @brief Accept all well-formed login attempts
+ *
+ * Used in debugging.
+ */
+int wideopen;
+
 struct listener {
   const char *name;
   int pf;
@@ -373,28 +379,29 @@ static int c_user(struct conn *c,
       sink_writes(ev_writer_sink(c->w), "530 authentication failure\n");
       return 1;
     }
-  }
+  } else
+    strcpy(host, "local");
   /* find the user */
   for(n = 0; n < config->allow.n
        && strcmp(config->allow.s[n].s[0], vec[0]); ++n)
     ;
   /* if it's a real user check whether the response is right */
-  if(n < config->allow.n) {
-    res = authhash(c->nonce, sizeof c->nonce, config->allow.s[n].s[1]);
-    if(res && !strcmp(res, vec[1])) {
-      c->who = vec[0];
-      /* currently we only bother logging remote connections */
-      if(c->l->pf != PF_UNIX)
-       info("S%x %s connected from %s", c->tag, vec[0], host);
-      sink_writes(ev_writer_sink(c->w), "230 OK\n");
-      return 1;
-    }
+  if(n >= config->allow.n) {
+    info("S%x unknown user '%s' from %s", c->tag, vec[0], host);
+    sink_writes(ev_writer_sink(c->w), "530 authentication failed\n");
+    return 1;
+  }
+  res = authhash(c->nonce, sizeof c->nonce, config->allow.s[n].s[1]);
+  if(wideopen || (res && !strcmp(res, vec[1]))) {
+    c->who = vec[0];
+    /* currently we only bother logging remote connections */
+    if(c->l->pf != PF_UNIX)
+      info("S%x %s connected from %s", c->tag, vec[0], host);
+    sink_writes(ev_writer_sink(c->w), "230 OK\n");
+    return 1;
   }
   /* oops, response was wrong */
-  if(c->l->pf != PF_UNIX)
-    info("S%x authentication failure for %s from %s", c->tag, vec[0], host);
-  else
-    info("S%x authentication failure for %s", c->tag, vec[0]);
+  info("S%x authentication failure for %s from %s", c->tag, vec[0], host);
   sink_writes(ev_writer_sink(c->w), "530 authentication failed\n");
   return 1;
 }
index f044120..a3a5beb 100644 (file)
@@ -36,6 +36,8 @@ int server_stop(ev_source *ev, int fd);
 
 extern int volume_left, volume_right;  /* last known volume */
 
+extern int wideopen;                   /* blindly accept all logins */
+
 #endif /* SERVER_H */
 
 /*
diff --git a/server/speaker-alsa.c b/server/speaker-alsa.c
new file mode 100644 (file)
index 0000000..fa9e6c3
--- /dev/null
@@ -0,0 +1,287 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005, 2006, 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file server/speaker-alsa.c
+ * @brief Support for @ref BACKEND_ALSA */
+
+#include <config.h>
+
+#if API_ALSA
+
+#include "types.h"
+
+#include <unistd.h>
+#include <poll.h>
+#include <alsa/asoundlib.h>
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "speaker-protocol.h"
+#include "speaker.h"
+
+/** @brief The current PCM handle */
+static snd_pcm_t *pcm;
+
+/** @brief Last seen buffer size */
+static snd_pcm_uframes_t last_pcm_bufsize;
+
+/** @brief ALSA backend initialization */
+static void alsa_init(void) {
+  info("selected ALSA backend");
+}
+
+/** @brief Log ALSA parameters */
+static void log_params(snd_pcm_hw_params_t *hwparams,
+                       snd_pcm_sw_params_t *swparams) {
+  snd_pcm_uframes_t f;
+  unsigned u;
+
+  return;                               /* too verbose */
+  if(hwparams) {
+    /* TODO */
+  }
+  if(swparams) {
+    snd_pcm_sw_params_get_silence_size(swparams, &f);
+    info("sw silence_size=%lu", (unsigned long)f);
+    snd_pcm_sw_params_get_silence_threshold(swparams, &f);
+    info("sw silence_threshold=%lu", (unsigned long)f);
+    snd_pcm_sw_params_get_sleep_min(swparams, &u);
+    info("sw sleep_min=%lu", (unsigned long)u);
+    snd_pcm_sw_params_get_start_threshold(swparams, &f);
+    info("sw start_threshold=%lu", (unsigned long)f);
+    snd_pcm_sw_params_get_stop_threshold(swparams, &f);
+    info("sw stop_threshold=%lu", (unsigned long)f);
+    snd_pcm_sw_params_get_xfer_align(swparams, &f);
+    info("sw xfer_align=%lu", (unsigned long)f);
+  }
+}
+
+/** @brief ALSA deactivation */
+static void alsa_deactivate(void) {
+  if(pcm) {
+    int err;
+    
+    if((err = snd_pcm_nonblock(pcm, 0)) < 0)
+      fatal(0, "error calling snd_pcm_nonblock: %d", err);
+    D(("draining pcm"));
+    snd_pcm_drain(pcm);
+    D(("closing pcm"));
+    snd_pcm_close(pcm);
+    pcm = 0;
+    device_state = device_closed;
+    D(("released audio device"));
+  }
+}
+
+/** @brief ALSA backend activation */
+static void alsa_activate(void) {
+  /* If we need to change format then close the current device. */
+  if(pcm && !formats_equal(&playing->format, &device_format))
+    alsa_deactivate();
+  /* Now if the sound device is open it must have the right format */
+  if(!pcm) {
+    snd_pcm_hw_params_t *hwparams;
+    snd_pcm_sw_params_t *swparams;
+    snd_pcm_uframes_t pcm_bufsize;
+    int err;
+    int sample_format = 0;
+    unsigned rate;
+
+    D(("snd_pcm_open"));
+    if((err = snd_pcm_open(&pcm,
+                           config->device,
+                           SND_PCM_STREAM_PLAYBACK,
+                           SND_PCM_NONBLOCK))) {
+      error(0, "error from snd_pcm_open: %d", err);
+      goto error;
+    }
+    snd_pcm_hw_params_alloca(&hwparams);
+    D(("set up hw params"));
+    if((err = snd_pcm_hw_params_any(pcm, hwparams)) < 0)
+      fatal(0, "error from snd_pcm_hw_params_any: %d", err);
+    if((err = snd_pcm_hw_params_set_access(pcm, hwparams,
+                                           SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
+      fatal(0, "error from snd_pcm_hw_params_set_access: %d", err);
+    switch(playing->format.bits) {
+    case 8:
+      sample_format = SND_PCM_FORMAT_S8;
+      break;
+    case 16:
+      switch(playing->format.byte_format) {
+      case AO_FMT_NATIVE: sample_format = SND_PCM_FORMAT_S16; break;
+      case AO_FMT_LITTLE: sample_format = SND_PCM_FORMAT_S16_LE; break;
+      case AO_FMT_BIG: sample_format = SND_PCM_FORMAT_S16_BE; break;
+        error(0, "unrecognized byte format %d", playing->format.byte_format);
+        goto fatal;
+      }
+      break;
+    default:
+      error(0, "unsupported sample size %d", playing->format.bits);
+      goto fatal;
+    }
+    if((err = snd_pcm_hw_params_set_format(pcm, hwparams,
+                                           sample_format)) < 0) {
+      error(0, "error from snd_pcm_hw_params_set_format (%d): %d",
+            sample_format, err);
+      goto fatal;
+    }
+    rate = playing->format.rate;
+    if((err = snd_pcm_hw_params_set_rate_near(pcm, hwparams, &rate, 0)) < 0) {
+      error(0, "error from snd_pcm_hw_params_set_rate (%d): %d",
+            playing->format.rate, err);
+      goto fatal;
+    }
+    if(rate != (unsigned)playing->format.rate)
+      info("want rate %d, got %u", playing->format.rate, rate);
+    if((err = snd_pcm_hw_params_set_channels(pcm, hwparams,
+                                             playing->format.channels)) < 0) {
+      error(0, "error from snd_pcm_hw_params_set_channels (%d): %d",
+            playing->format.channels, err);
+      goto fatal;
+    }
+    pcm_bufsize = 3 * FRAMES;
+    if((err = snd_pcm_hw_params_set_buffer_size_near(pcm, hwparams,
+                                                     &pcm_bufsize)) < 0)
+      fatal(0, "error from snd_pcm_hw_params_set_buffer_size (%d): %d",
+            3 * FRAMES, err);
+    if(pcm_bufsize != 3 * FRAMES && pcm_bufsize != last_pcm_bufsize)
+      info("asked for PCM buffer of %d frames, got %d",
+           3 * FRAMES, (int)pcm_bufsize);
+    last_pcm_bufsize = pcm_bufsize;
+    if((err = snd_pcm_hw_params(pcm, hwparams)) < 0)
+      fatal(0, "error calling snd_pcm_hw_params: %d", err);
+    D(("set up sw params"));
+    snd_pcm_sw_params_alloca(&swparams);
+    if((err = snd_pcm_sw_params_current(pcm, swparams)) < 0)
+      fatal(0, "error calling snd_pcm_sw_params_current: %d", err);
+    if((err = snd_pcm_sw_params_set_avail_min(pcm, swparams, FRAMES)) < 0)
+      fatal(0, "error calling snd_pcm_sw_params_set_avail_min %d: %d",
+            FRAMES, err);
+    if((err = snd_pcm_sw_params(pcm, swparams)) < 0)
+      fatal(0, "error calling snd_pcm_sw_params: %d", err);
+    device_format = playing->format;
+    D(("acquired audio device"));
+    log_params(hwparams, swparams);
+    device_state = device_open;
+  }
+  return;
+fatal:
+  abandon();
+error:
+  /* We assume the error is temporary and that we'll retry in a bit. */
+  if(pcm) {
+    snd_pcm_close(pcm);
+    pcm = 0;
+    device_state = device_error;
+  }
+  return;
+}
+
+/** @brief Play via ALSA */
+static size_t alsa_play(size_t frames) {
+  snd_pcm_sframes_t pcm_written_frames;
+  int err;
+  
+  pcm_written_frames = snd_pcm_writei(pcm,
+                                      playing->buffer + playing->start,
+                                      frames);
+  D(("actually play %zu frames, wrote %d",
+     frames, (int)pcm_written_frames));
+  if(pcm_written_frames < 0) {
+    switch(pcm_written_frames) {
+    case -EPIPE:                        /* underrun */
+      error(0, "snd_pcm_writei reports underrun");
+      if((err = snd_pcm_prepare(pcm)) < 0)
+        fatal(0, "error calling snd_pcm_prepare: %d", err);
+      return 0;
+    case -EAGAIN:
+      return 0;
+    default:
+      fatal(0, "error calling snd_pcm_writei: %d",
+            (int)pcm_written_frames);
+    }
+  } else
+    return pcm_written_frames;
+}
+
+static int alsa_slots, alsa_nslots = -1;
+
+/** @brief Fill in poll fd array for ALSA */
+static void alsa_beforepoll(void) {
+  /* We send sample data to ALSA as fast as it can accept it, relying on
+   * the fact that it has a relatively small buffer to minimize pause
+   * latency. */
+  int retry = 3, err;
+  
+  alsa_slots = fdno;
+  do {
+    retry = 0;
+    alsa_nslots = snd_pcm_poll_descriptors(pcm, &fds[fdno], NFDS - fdno);
+    if((alsa_nslots <= 0
+        || !(fds[alsa_slots].events & POLLOUT))
+       && snd_pcm_state(pcm) == SND_PCM_STATE_XRUN) {
+      error(0, "underrun detected after call to snd_pcm_poll_descriptors()");
+      if((err = snd_pcm_prepare(pcm)))
+        fatal(0, "error calling snd_pcm_prepare: %d", err);
+    } else
+      break;
+  } while(retry-- > 0);
+  if(alsa_nslots >= 0)
+    fdno += alsa_nslots;
+}
+
+/** @brief Process poll() results for ALSA */
+static int alsa_ready(void) {
+  int err;
+
+  unsigned short alsa_revents;
+  
+  if((err = snd_pcm_poll_descriptors_revents(pcm,
+                                             &fds[alsa_slots],
+                                             alsa_nslots,
+                                             &alsa_revents)) < 0)
+    fatal(0, "error calling snd_pcm_poll_descriptors_revents: %d", err);
+  if(alsa_revents & (POLLOUT | POLLERR))
+    return 1;
+  else
+    return 0;
+}
+
+const struct speaker_backend alsa_backend = {
+  BACKEND_ALSA,
+  0,
+  alsa_init,
+  alsa_activate,
+  alsa_play,
+  alsa_deactivate,
+  alsa_beforepoll,
+  alsa_ready
+};
+
+#endif
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/speaker-command.c b/server/speaker-command.c
new file mode 100644 (file)
index 0000000..8a8f63c
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005, 2006, 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file server/speaker-command.c
+ * @brief Support for @ref BACKEND_COMMAND */
+
+#include <config.h>
+#include "types.h"
+
+#include <unistd.h>
+#include <poll.h>
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "speaker-protocol.h"
+#include "speaker.h"
+
+/** @brief Pipe to subprocess
+ *
+ * This is the file descriptor to write to for @ref BACKEND_COMMAND.
+ */
+static int cmdfd = -1;
+
+/** @brief poll array slot for @ref cmdfd
+ *
+ * Set by command_beforepoll().
+ */
+static int cmdfd_slot;
+
+/** @brief Start the subprocess for @ref BACKEND_COMMAND */
+static void fork_cmd(void) {
+  pid_t cmdpid;
+  int pfd[2];
+  if(cmdfd != -1) close(cmdfd);
+  xpipe(pfd);
+  cmdpid = xfork();
+  if(!cmdpid) {
+    signal(SIGPIPE, SIG_DFL);
+    xdup2(pfd[0], 0);
+    close(pfd[0]);
+    close(pfd[1]);
+    execl("/bin/sh", "sh", "-c", config->speaker_command, (char *)0);
+    fatal(errno, "error execing /bin/sh");
+  }
+  close(pfd[0]);
+  cmdfd = pfd[1];
+  D(("forked cmd %d, fd = %d", cmdpid, cmdfd));
+}
+
+/** @brief Command backend initialization */
+static void command_init(void) {
+  info("selected command backend");
+  fork_cmd();
+}
+
+/** @brief Play to a subprocess */
+static size_t command_play(size_t frames) {
+  size_t bytes = frames * device_bpf;
+  int written_bytes;
+
+  written_bytes = write(cmdfd, playing->buffer + playing->start, bytes);
+  D(("actually play %zu bytes, wrote %d",
+     bytes, written_bytes));
+  if(written_bytes < 0) {
+    switch(errno) {
+    case EPIPE:
+      error(0, "hmm, command died; trying another");
+      fork_cmd();
+      return 0;
+    case EAGAIN:
+      return 0;
+    default:
+      fatal(errno, "error writing to subprocess");
+    }
+  } else
+    return written_bytes / device_bpf;
+}
+
+/** @brief Update poll array for writing to subprocess */
+static void command_beforepoll(void) {
+  /* We send sample data to the subprocess as fast as it can accept it.
+   * This isn't ideal as pause latency can be very high as a result. */
+  if(cmdfd >= 0)
+    cmdfd_slot = addfd(cmdfd, POLLOUT);
+}
+
+/** @brief Process poll() results for subprocess play */
+static int command_ready(void) {
+  if(fds[cmdfd_slot].revents & (POLLOUT | POLLERR))
+    return 1;
+  else
+    return 0;
+}
+
+const struct speaker_backend command_backend = {
+  BACKEND_COMMAND,
+  FIXED_FORMAT,
+  command_init,
+  0,                                    /* activate */
+  command_play,
+  0,                                    /* deactivate */
+  command_beforepoll,
+  command_ready
+};
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/speaker-network.c b/server/speaker-network.c
new file mode 100644 (file)
index 0000000..fab81a8
--- /dev/null
@@ -0,0 +1,314 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005, 2006, 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file server/speaker-network.c
+ * @brief Support for @ref BACKEND_NETWORK */
+
+#include <config.h>
+#include "types.h"
+
+#include <unistd.h>
+#include <poll.h>
+#include <netdb.h>
+#include <gcrypt.h>
+#include <sys/socket.h>
+#include <sys/uio.h>
+#include <assert.h>
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "addr.h"
+#include "timeval.h"
+#include "rtp.h"
+#include "speaker-protocol.h"
+#include "speaker.h"
+
+/** @brief Network socket
+ *
+ * This is the file descriptor to write to for @ref BACKEND_NETWORK.
+ */
+static int bfd = -1;
+
+/** @brief RTP timestamp
+ *
+ * This counts the number of samples played (NB not the number of frames
+ * played).
+ *
+ * The timestamp in the packet header is only 32 bits wide.  With 44100Hz
+ * stereo, that only gives about half a day before wrapping, which is not
+ * particularly convenient for certain debugging purposes.  Therefore the
+ * timestamp is maintained as a 64-bit integer, giving around six million years
+ * before wrapping, and truncated to 32 bits when transmitting.
+ */
+static uint64_t rtp_time;
+
+/** @brief RTP base timestamp
+ *
+ * This is the real time correspoding to an @ref rtp_time of 0.  It is used
+ * to recalculate the timestamp after idle periods.
+ */
+static struct timeval rtp_time_0;
+
+/** @brief RTP packet sequence number */
+static uint16_t rtp_seq;
+
+/** @brief RTP SSRC */
+static uint32_t rtp_id;
+
+/** @brief Error counter */
+static int audio_errors;
+
+/** @brief Network backend initialization */
+static void network_init(void) {
+  struct addrinfo *res, *sres;
+  static const struct addrinfo pref = {
+    0,
+    PF_INET,
+    SOCK_DGRAM,
+    IPPROTO_UDP,
+    0,
+    0,
+    0,
+    0
+  };
+  static const struct addrinfo prefbind = {
+    AI_PASSIVE,
+    PF_INET,
+    SOCK_DGRAM,
+    IPPROTO_UDP,
+    0,
+    0,
+    0,
+    0
+  };
+  static const int one = 1;
+  int sndbuf, target_sndbuf = 131072;
+  socklen_t len;
+  char *sockname, *ssockname;
+
+  /* Override sample format */
+  config->sample_format.rate = 44100;
+  config->sample_format.channels = 2;
+  config->sample_format.bits = 16;
+  config->sample_format.byte_format = AO_FMT_BIG;
+  res = get_address(&config->broadcast, &pref, &sockname);
+  if(!res) exit(-1);
+  if(config->broadcast_from.n) {
+    sres = get_address(&config->broadcast_from, &prefbind, &ssockname);
+    if(!sres) exit(-1);
+  } else
+    sres = 0;
+  if((bfd = socket(res->ai_family,
+                   res->ai_socktype,
+                   res->ai_protocol)) < 0)
+    fatal(errno, "error creating broadcast socket");
+  if(setsockopt(bfd, SOL_SOCKET, SO_BROADCAST, &one, sizeof one) < 0)
+    fatal(errno, "error setting SO_BROADCAST on broadcast socket");
+  len = sizeof sndbuf;
+  if(getsockopt(bfd, SOL_SOCKET, SO_SNDBUF,
+                &sndbuf, &len) < 0)
+    fatal(errno, "error getting SO_SNDBUF");
+  if(target_sndbuf > sndbuf) {
+    if(setsockopt(bfd, SOL_SOCKET, SO_SNDBUF,
+                  &target_sndbuf, sizeof target_sndbuf) < 0)
+      error(errno, "error setting SO_SNDBUF to %d", target_sndbuf);
+    else
+      info("changed socket send buffer size from %d to %d",
+           sndbuf, target_sndbuf);
+  } else
+    info("default socket send buffer is %d",
+         sndbuf);
+  /* We might well want to set additional broadcast- or multicast-related
+   * options here */
+  if(sres && bind(bfd, sres->ai_addr, sres->ai_addrlen) < 0)
+    fatal(errno, "error binding broadcast socket to %s", ssockname);
+  if(connect(bfd, res->ai_addr, res->ai_addrlen) < 0)
+    fatal(errno, "error connecting broadcast socket to %s", sockname);
+  /* Select an SSRC */
+  gcry_randomize(&rtp_id, sizeof rtp_id, GCRY_STRONG_RANDOM);
+  info("selected network backend, sending to %s", sockname);
+}
+
+/** @brief Play over the network */
+static size_t network_play(size_t frames) {
+  struct rtp_header header;
+  struct iovec vec[2];
+  size_t bytes = frames * device_bpf, written_frames;
+  int written_bytes;
+  /* We transmit using RTP (RFC3550) and attempt to conform to the internet
+   * AVT profile (RFC3551). */
+
+  if(idled) {
+    /* There may have been a gap.  Fix up the RTP time accordingly. */
+    struct timeval now;
+    uint64_t delta;
+    uint64_t target_rtp_time;
+
+    /* Find the current time */
+    xgettimeofday(&now, 0);
+    /* Find the number of microseconds elapsed since rtp_time=0 */
+    delta = tvsub_us(now, rtp_time_0);
+    assert(delta <= UINT64_MAX / 88200);
+    target_rtp_time = (delta * playing->format.rate
+                       * playing->format.channels) / 1000000;
+    /* Overflows at ~6 years uptime with 44100Hz stereo */
+
+    /* rtp_time is the number of samples we've played.  NB that we play
+     * RTP_AHEAD_MS ahead of ourselves, so it may legitimately be ahead of
+     * the value we deduce from time comparison.
+     *
+     * Suppose we have 1s track started at t=0, and another track begins to
+     * play at t=2s.  Suppose RTP_AHEAD_MS=1000 and 44100Hz stereo.  In that
+     * case we'll send 1s of audio as fast as we can, giving rtp_time=88200.
+     * rtp_time stops at this point.
+     *
+     * At t=2s we'll have calculated target_rtp_time=176400.  In this case we
+     * set rtp_time=176400 and the player can correctly conclude that it
+     * should leave 1s between the tracks.
+     *
+     * Suppose instead that the second track arrives at t=0.5s, and that
+     * we've managed to transmit the whole of the first track already.  We'll
+     * have target_rtp_time=44100.
+     *
+     * The desired behaviour is to play the second track back to back with
+     * first.  In this case therefore we do not modify rtp_time.
+     *
+     * Is it ever right to reduce rtp_time?  No; for that would imply
+     * transmitting packets with overlapping timestamp ranges, which does not
+     * make sense.
+     */
+    target_rtp_time &= ~(uint64_t)1;    /* stereo! */
+    if(target_rtp_time > rtp_time) {
+      /* More time has elapsed than we've transmitted samples.  That implies
+       * we've been 'sending' silence.  */
+      info("advancing rtp_time by %"PRIu64" samples",
+           target_rtp_time - rtp_time);
+      rtp_time = target_rtp_time;
+    } else if(target_rtp_time < rtp_time) {
+      const int64_t samples_ahead = ((uint64_t)RTP_AHEAD_MS
+                                     * config->sample_format.rate
+                                     * config->sample_format.channels
+                                     / 1000);
+        
+      if(target_rtp_time + samples_ahead < rtp_time) {
+        info("reversing rtp_time by %"PRIu64" samples",
+             rtp_time - target_rtp_time);
+      }
+    }
+  }
+  header.vpxcc = 2 << 6;              /* V=2, P=0, X=0, CC=0 */
+  header.seq = htons(rtp_seq++);
+  header.timestamp = htonl((uint32_t)rtp_time);
+  header.ssrc = rtp_id;
+  header.mpt = (idled ? 0x80 : 0x00) | 10;
+  /* 10 = L16 = 16-bit x 2 x 44100KHz.  We ought to deduce this value from
+   * the sample rate (in a library somewhere so that configuration.c can rule
+   * out invalid rates).
+   */
+  idled = 0;
+  if(bytes > NETWORK_BYTES - sizeof header) {
+    bytes = NETWORK_BYTES - sizeof header;
+    /* Always send a whole number of frames */
+    bytes -= bytes % device_bpf;
+  }
+  /* "The RTP clock rate used for generating the RTP timestamp is independent
+   * of the number of channels and the encoding; it equals the number of
+   * sampling periods per second.  For N-channel encodings, each sampling
+   * period (say, 1/8000 of a second) generates N samples. (This terminology
+   * is standard, but somewhat confusing, as the total number of samples
+   * generated per second is then the sampling rate times the channel
+   * count.)"
+   */
+  vec[0].iov_base = (void *)&header;
+  vec[0].iov_len = sizeof header;
+  vec[1].iov_base = playing->buffer + playing->start;
+  vec[1].iov_len = bytes;
+  do {
+    written_bytes = writev(bfd, vec, 2);
+  } while(written_bytes < 0 && errno == EINTR);
+  if(written_bytes < 0) {
+    error(errno, "error transmitting audio data");
+    ++audio_errors;
+    if(audio_errors == 10)
+      fatal(0, "too many audio errors");
+    return 0;
+  } else
+    audio_errors /= 2;
+  written_bytes -= sizeof (struct rtp_header);
+  written_frames = written_bytes / device_bpf;
+  /* Advance RTP's notion of the time */
+  rtp_time += written_frames * playing->format.channels;
+  return written_frames;
+}
+
+static int bfd_slot;
+
+/** @brief Set up poll array for network play */
+static void network_beforepoll(void) {
+  struct timeval now;
+  uint64_t target_us;
+  uint64_t target_rtp_time;
+  const int64_t samples_ahead = ((uint64_t)RTP_AHEAD_MS
+                                 * config->sample_format.rate
+                                 * config->sample_format.channels
+                                 / 1000);
+  
+  /* If we're starting then initialize the base time */
+  if(!rtp_time)
+    xgettimeofday(&rtp_time_0, 0);
+  /* We send audio data whenever we get RTP_AHEAD seconds or more
+   * behind */
+  xgettimeofday(&now, 0);
+  target_us = tvsub_us(now, rtp_time_0);
+  assert(target_us <= UINT64_MAX / 88200);
+  target_rtp_time = (target_us * config->sample_format.rate
+                               * config->sample_format.channels)
+                     / 1000000;
+  if((int64_t)(rtp_time - target_rtp_time) < samples_ahead)
+    bfd_slot = addfd(bfd, POLLOUT);
+}
+
+/** @brief Process poll() results for network play */
+static int network_ready(void) {
+  if(fds[bfd_slot].revents & (POLLOUT | POLLERR))
+    return 1;
+  else
+    return 0;
+}
+
+const struct speaker_backend network_backend = {
+  BACKEND_NETWORK,
+  FIXED_FORMAT,
+  network_init,
+  0,                                    /* activate */
+  network_play,
+  0,                                    /* deactivate */
+  network_beforepoll,
+  network_ready
+};
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index be783af..1dc90e4 100644 (file)
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
-
-/* This program deliberately does not use the garbage collector even though it
- * might be convenient to do so.  This is for two reasons.  Firstly some libao
- * drivers are implemented using threads and we do not want to have to deal
- * with potential interactions between threading and garbage collection.
- * Secondly this process needs to be able to respond quickly and this is not
- * compatible with the collector hanging the program even relatively
- * briefly. */
+/** @file server/speaker.c
+ * @brief Speaker process
+ *
+ * This program is responsible for transmitting a single coherent audio stream
+ * to its destination (over the network, to some sound API, to some
+ * subprocess).  It receives connections from decoders via file descriptor
+ * passing from the main server and plays them in the right order.
+ *
+ * @b Encodings.  For the <a href="http://www.alsa-project.org/">ALSA</a> API,
+ * 8- and 16- bit stereo and mono are supported, with any sample rate (within
+ * the limits that ALSA can deal with.)
+ *
+ * When communicating with a subprocess, <a
+ * href="http://sox.sourceforge.net/">sox</a> is invoked to convert the inbound
+ * data to a single consistent format.  The same applies for network (RTP)
+ * play, though in that case currently only 44.1KHz 16-bit stereo is supported.
+ *
+ * The inbound data starts with a structure defining the data format.  Note
+ * that this is NOT portable between different platforms or even necessarily
+ * between versions; the speaker is assumed to be built from the same source
+ * and run on the same host as the main server.
+ *
+ * @b Garbage @b Collection.  This program deliberately does not use the
+ * garbage collector even though it might be convenient to do so.  This is for
+ * two reasons.  Firstly some sound APIs use thread threads and we do not want
+ * to have to deal with potential interactions between threading and garbage
+ * collection.  Secondly this process needs to be able to respond quickly and
+ * this is not compatible with the collector hanging the program even
+ * relatively briefly.
+ *
+ * @b Units.  This program thinks at various times in three different units.
+ * Bytes are obvious.  A sample is a single sample on a single channel.  A
+ * frame is several samples on different channels at the same point in time.
+ * So (for instance) a 16-bit stereo frame is 4 bytes and consists of a pair of
+ * 2-byte samples.
+ */
 
 #include <config.h>
 #include "types.h"
 #include "log.h"
 #include "defs.h"
 #include "mem.h"
-#include "speaker.h"
+#include "speaker-protocol.h"
 #include "user.h"
+#include "speaker.h"
 
-#if API_ALSA
-#include <alsa/asoundlib.h>
-#endif
+/** @brief Linked list of all prepared tracks */
+struct track *tracks;
 
-#ifdef WORDS_BIGENDIAN
-# define MACHINE_AO_FMT AO_FMT_BIG
-#else
-# define MACHINE_AO_FMT AO_FMT_LITTLE
-#endif
+/** @brief Playing track, or NULL */
+struct track *playing;
 
-#define BUFFER_SECONDS 5                /* How many seconds of input to
-                                         * buffer. */
-
-#define FRAMES 4096                     /* Frame batch size */
-
-#define NFDS 256                        /* Max FDs to poll for */
-
-/* Known tracks are kept in a linked list.  We don't normally to have
- * more than two - maybe three at the outside. */
-static struct track {
-  struct track *next;                   /* next track */
-  int fd;                               /* input FD */
-  char id[24];                          /* ID */
-  size_t start, used;                   /* start + bytes used */
-  int eof;                              /* input is at EOF */
-  int got_format;                       /* got format yet? */
-  ao_sample_format format;              /* sample format */
-  unsigned long long played;            /* number of frames played */
-  char *buffer;                         /* sample buffer */
-  size_t size;                          /* sample buffer size */
-  int slot;                             /* poll array slot */
-} *tracks, *playing;                    /* all tracks + playing track */
+/** @brief Number of bytes pre frame */
+size_t device_bpf;
+
+/** @brief Array of file descriptors for poll() */
+struct pollfd fds[NFDS];
+
+/** @brief Next free slot in @ref fds */
+int fdno;
 
 static time_t last_report;              /* when we last reported */
 static int paused;                      /* pause status */
-static ao_sample_format pcm_format;     /* current format if aodev != 0 */
-static size_t bpf;                      /* bytes per frame */
-static struct pollfd fds[NFDS];         /* if we need more than that */
-static int fdno;                        /* fd number */
-static size_t bufsize;                  /* buffer size */
-#if API_ALSA
-static snd_pcm_t *pcm;                  /* current pcm handle */
-static snd_pcm_uframes_t last_pcm_bufsize; /* last seen buffer size */
-#endif
-static int ready;                       /* ready to send audio */
-static int forceplay;                   /* frames to force play */
-static int kidfd = -1;                  /* child process input */
+
+/** @brief The current device state */
+enum device_states device_state;
+
+/** @brief The current device sample format
+ *
+ * Only meaningful if @ref device_state = @ref device_open or perhaps @ref
+ * device_error.  For @ref FIXED_FORMAT backends, this should always match @c
+ * config->sample_format.
+ */
+ao_sample_format device_format;
+
+/** @brief Set when idled
+ *
+ * This is set when the sound device is deliberately closed by idle().
+ */
+int idled;
+
+/** @brief Selected backend */
+static const struct speaker_backend *backend;
 
 static const struct option options[] = {
   { "help", no_argument, 0, 'h' },
@@ -133,12 +152,12 @@ static void version(void) {
   exit(0);
 }
 
-/* Return the number of bytes per frame in FORMAT. */
+/** @brief Return the number of bytes per frame in @p format */
 static size_t bytes_per_frame(const ao_sample_format *format) {
   return format->channels * format->bits / 8;
 }
 
-/* Find track ID, maybe creating it if not found. */
+/** @brief Find track @p id, maybe creating it if not found */
 static struct track *findtrack(const char *id, int create) {
   struct track *t;
 
@@ -158,7 +177,7 @@ static struct track *findtrack(const char *id, int create) {
   return t;
 }
 
-/* Remove track ID (but do not destroy it). */
+/** @brief Remove track @p id (but do not destroy it) */
 static struct track *removetrack(const char *id) {
   struct track *t, **tt;
 
@@ -170,7 +189,7 @@ static struct track *removetrack(const char *id) {
   return t;
 }
 
-/* Destroy a track. */
+/** @brief Destroy a track */
 static void destroy(struct track *t) {
   D(("destroy %s", t->id));
   if(t->fd != -1) xclose(t->fd);
@@ -178,7 +197,7 @@ static void destroy(struct track *t) {
   free(t);
 }
 
-/* Notice a new FD. */
+/** @brief Notice a new connection */
 static void acquire(struct track *t, int fd) {
   D(("acquire %s %d", t->id, fd));
   if(t->fd != -1)
@@ -187,7 +206,104 @@ static void acquire(struct track *t, int fd) {
   nonblock(fd);
 }
 
-/* Read data into a sample buffer.  Return 0 on success, -1 on EOF. */
+/** @brief Return true if A and B denote identical libao formats, else false */
+int formats_equal(const ao_sample_format *a,
+                  const ao_sample_format *b) {
+  return (a->bits == b->bits
+          && a->rate == b->rate
+          && a->channels == b->channels
+          && a->byte_format == b->byte_format);
+}
+
+/** @brief Compute arguments to sox */
+static void soxargs(const char ***pp, char **qq, ao_sample_format *ao) {
+  int n;
+
+  *(*pp)++ = "-t.raw";
+  *(*pp)++ = "-s";
+  *(*pp)++ = *qq; n = sprintf(*qq, "-r%d", ao->rate); *qq += n + 1;
+  *(*pp)++ = *qq; n = sprintf(*qq, "-c%d", ao->channels); *qq += n + 1;
+  /* sox 12.17.9 insists on -b etc; CVS sox insists on -<n> etc; both are
+   * deployed! */
+  switch(config->sox_generation) {
+  case 0:
+    if(ao->bits != 8
+       && ao->byte_format != AO_FMT_NATIVE
+       && ao->byte_format != MACHINE_AO_FMT) {
+      *(*pp)++ = "-x";
+    }
+    switch(ao->bits) {
+    case 8: *(*pp)++ = "-b"; break;
+    case 16: *(*pp)++ = "-w"; break;
+    case 32: *(*pp)++ = "-l"; break;
+    case 64: *(*pp)++ = "-d"; break;
+    default: fatal(0, "cannot handle sample size %d", (int)ao->bits);
+    }
+    break;
+  case 1:
+    switch(ao->byte_format) {
+    case AO_FMT_NATIVE: break;
+    case AO_FMT_BIG: *(*pp)++ = "-B"; break;
+    case AO_FMT_LITTLE: *(*pp)++ = "-L"; break;
+    }
+    *(*pp)++ = *qq; n = sprintf(*qq, "-%d", ao->bits/8); *qq += n + 1;
+    break;
+  }
+}
+
+/** @brief Enable format translation
+ *
+ * If necessary, replaces a tracks inbound file descriptor with one connected
+ * to a sox invocation, which performs the required translation.
+ */
+static void enable_translation(struct track *t) {
+  if((backend->flags & FIXED_FORMAT)
+     && !formats_equal(&t->format, &config->sample_format)) {
+    char argbuf[1024], *q = argbuf;
+    const char *av[18], **pp = av;
+    int soxpipe[2];
+    pid_t soxkid;
+
+    *pp++ = "sox";
+    soxargs(&pp, &q, &t->format);
+    *pp++ = "-";
+    soxargs(&pp, &q, &config->sample_format);
+    *pp++ = "-";
+    *pp++ = 0;
+    if(debugging) {
+      for(pp = av; *pp; pp++)
+        D(("sox arg[%d] = %s", pp - av, *pp));
+      D(("end args"));
+    }
+    xpipe(soxpipe);
+    soxkid = xfork();
+    if(soxkid == 0) {
+      signal(SIGPIPE, SIG_DFL);
+      xdup2(t->fd, 0);
+      xdup2(soxpipe[1], 1);
+      fcntl(0, F_SETFL, fcntl(0, F_GETFL) & ~O_NONBLOCK);
+      close(soxpipe[0]);
+      close(soxpipe[1]);
+      close(t->fd);
+      execvp("sox", (char **)av);
+      _exit(1);
+    }
+    D(("forking sox for format conversion (kid = %d)", soxkid));
+    close(t->fd);
+    close(soxpipe[1]);
+    t->fd = soxpipe[0];
+    t->format = config->sample_format;
+  }
+}
+
+/** @brief Read data into a sample buffer
+ * @param t Pointer to track
+ * @return 0 on success, -1 on EOF
+ *
+ * This is effectively the read callback on @c t->fd.  It is called from the
+ * main loop whenever the track's file descriptor is readable, assuming the
+ * buffer has not reached the maximum allowed occupancy.
+ */
 static int fill(struct track *t) {
   size_t where, left;
   int n;
@@ -223,6 +339,8 @@ static int fill(struct track *t) {
       /* Check that our assumptions are met. */
       if(t->format.bits & 7)
         fatal(0, "bits per sample not a multiple of 8");
+      /* If the input format is unsuitable, arrange to translate it */
+      enable_translation(t);
       /* Make a new buffer for audio data. */
       t->size = bytes_per_frame(&t->format) * t->format.rate * BUFFER_SECONDS;
       t->buffer = xmalloc(t->size);
@@ -234,38 +352,23 @@ static int fill(struct track *t) {
   return 0;
 }
 
-/* Return true if A and B denote identical libao formats, else false. */
-static int formats_equal(const ao_sample_format *a,
-                         const ao_sample_format *b) {
-  return (a->bits == b->bits
-          && a->rate == b->rate
-          && a->channels == b->channels
-          && a->byte_format == b->byte_format);
-}
-
-/* Close the sound device. */
+/** @brief Close the sound device
+ *
+ * This is called to deactivate the output device when pausing, and also by the
+ * ALSA backend when changing encoding (in which case the sound device will be
+ * immediately reactivated).
+ */
 static void idle(void) {
   D(("idle"));
-#if API_ALSA
-  if(!config->speaker_command && pcm) {
-    int  err;
-
-    if((err = snd_pcm_nonblock(pcm, 0)) < 0)
-      fatal(0, "error calling snd_pcm_nonblock: %d", err);
-    D(("draining pcm"));
-    snd_pcm_drain(pcm);
-    D(("closing pcm"));
-    snd_pcm_close(pcm);
-    pcm = 0;
-    forceplay = 0;
-    D(("released audio device"));
-  }
-#endif
-  ready = 0;
+  if(backend->deactivate) 
+    backend->deactivate();
+  else
+    device_state = device_closed;
+  idled = 1;
 }
 
-/* Abandon the current track */
-static void abandon(void) {
+/** @brief Abandon the current track */
+void abandon(void) {
   struct speaker_message sm;
 
   D(("abandon"));
@@ -276,231 +379,39 @@ static void abandon(void) {
   removetrack(playing->id);
   destroy(playing);
   playing = 0;
-  forceplay = 0;
-}
-
-#if API_ALSA
-static void log_params(snd_pcm_hw_params_t *hwparams,
-                       snd_pcm_sw_params_t *swparams) {
-  snd_pcm_uframes_t f;
-  unsigned u;
-
-  return;                               /* too verbose */
-  if(hwparams) {
-    /* TODO */
-  }
-  if(swparams) {
-    snd_pcm_sw_params_get_silence_size(swparams, &f);
-    info("sw silence_size=%lu", (unsigned long)f);
-    snd_pcm_sw_params_get_silence_threshold(swparams, &f);
-    info("sw silence_threshold=%lu", (unsigned long)f);
-    snd_pcm_sw_params_get_sleep_min(swparams, &u);
-    info("sw sleep_min=%lu", (unsigned long)u);
-    snd_pcm_sw_params_get_start_threshold(swparams, &f);
-    info("sw start_threshold=%lu", (unsigned long)f);
-    snd_pcm_sw_params_get_stop_threshold(swparams, &f);
-    info("sw stop_threshold=%lu", (unsigned long)f);
-    snd_pcm_sw_params_get_xfer_align(swparams, &f);
-    info("sw xfer_align=%lu", (unsigned long)f);
-  }
 }
-#endif
-
-static void soxargs(const char ***pp, char **qq, ao_sample_format *ao) {
-  int n;
 
-  *(*pp)++ = "-t.raw";
-  *(*pp)++ = "-s";
-  *(*pp)++ = *qq; n = sprintf(*qq, "-r%d", ao->rate); *qq += n + 1;
-  *(*pp)++ = *qq; n = sprintf(*qq, "-c%d", ao->channels); *qq += n + 1;
-  /* sox 12.17.9 insists on -b etc; CVS sox insists on -<n> etc; both are
-   * deployed! */
-  switch(config->sox_generation) {
-  case 0:
-    if(ao->bits != 8
-       && ao->byte_format != AO_FMT_NATIVE
-       && ao->byte_format != MACHINE_AO_FMT) {
-      *(*pp)++ = "-x";
-    }
-    switch(ao->bits) {
-    case 8: *(*pp)++ = "-b"; break;
-    case 16: *(*pp)++ = "-w"; break;
-    case 32: *(*pp)++ = "-l"; break;
-    case 64: *(*pp)++ = "-d"; break;
-    default: fatal(0, "cannot handle sample size %d", (int)ao->bits);
-    }
-    break;
-  case 1:
-    switch(ao->byte_format) {
-    case AO_FMT_NATIVE: break;
-    case AO_FMT_BIG: *(*pp)++ = "-B"; break;
-    case AO_FMT_LITTLE: *(*pp)++ = "-L"; break;
-    }
-    *(*pp)++ = *qq; n = sprintf(*qq, "-%d", ao->bits/8); *qq += n + 1;
-    break;
-  }
-}
-
-/* Make sure the sound device is open and has the right sample format.  Return
- * 0 on success and -1 on error. */
-static int activate(void) {
+/** @brief Enable sound output
+ *
+ * Makes sure the sound device is open and has the right sample format.  Return
+ * 0 on success and -1 on error.
+ */
+static void activate(void) {
   /* If we don't know the format yet we cannot start. */
   if(!playing->got_format) {
     D((" - not got format for %s", playing->id));
-    return -1;
-  }
-  if(kidfd >= 0) {
-    if(!formats_equal(&playing->format, &config->sample_format)) {
-      char argbuf[1024], *q = argbuf;
-      const char *av[18], **pp = av;
-      int soxpipe[2];
-      pid_t soxkid;
-      *pp++ = "sox";
-      soxargs(&pp, &q, &playing->format);
-      *pp++ = "-";
-      soxargs(&pp, &q, &config->sample_format);
-      *pp++ = "-";
-      *pp++ = 0;
-      if(debugging) {
-        for(pp = av; *pp; pp++)
-          D(("sox arg[%d] = %s", pp - av, *pp));
-        D(("end args"));
-      }
-      xpipe(soxpipe);
-      soxkid = xfork();
-      if(soxkid == 0) {
-        xdup2(playing->fd, 0);
-        xdup2(soxpipe[1], 1);
-        fcntl(0, F_SETFL, fcntl(0, F_GETFL) & ~O_NONBLOCK);
-        close(soxpipe[0]);
-        close(soxpipe[1]);
-        close(playing->fd);
-        execvp("sox", (char **)av);
-        _exit(1);
-      }
-      D(("forking sox for format conversion (kid = %d)", soxkid));
-      close(playing->fd);
-      close(soxpipe[1]);
-      playing->fd = soxpipe[0];
-      playing->format = config->sample_format;
-      ready = 0;
-    }
-    if(!ready) {
-      pcm_format = config->sample_format;
-      bufsize = 3 * FRAMES;
-      bpf = bytes_per_frame(&config->sample_format);
-      D(("acquired audio device"));
-      ready = 1;
-    }
-    return 0;
-  }
-  if(config->speaker_command)
-    return -1;
-#if API_ALSA
-  /* If we need to change format then close the current device. */
-  if(pcm && !formats_equal(&playing->format, &pcm_format))
-     idle();
-  if(!pcm) {
-    snd_pcm_hw_params_t *hwparams;
-    snd_pcm_sw_params_t *swparams;
-    snd_pcm_uframes_t pcm_bufsize;
-    int err;
-    int sample_format = 0;
-    unsigned rate;
-
-    D(("snd_pcm_open"));
-    if((err = snd_pcm_open(&pcm,
-                           config->device,
-                           SND_PCM_STREAM_PLAYBACK,
-                           SND_PCM_NONBLOCK))) {
-      error(0, "error from snd_pcm_open: %d", err);
-      goto error;
-    }
-    snd_pcm_hw_params_alloca(&hwparams);
-    D(("set up hw params"));
-    if((err = snd_pcm_hw_params_any(pcm, hwparams)) < 0)
-      fatal(0, "error from snd_pcm_hw_params_any: %d", err);
-    if((err = snd_pcm_hw_params_set_access(pcm, hwparams,
-                                           SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
-      fatal(0, "error from snd_pcm_hw_params_set_access: %d", err);
-    switch(playing->format.bits) {
-    case 8:
-      sample_format = SND_PCM_FORMAT_S8;
-      break;
-    case 16:
-      switch(playing->format.byte_format) {
-      case AO_FMT_NATIVE: sample_format = SND_PCM_FORMAT_S16; break;
-      case AO_FMT_LITTLE: sample_format = SND_PCM_FORMAT_S16_LE; break;
-      case AO_FMT_BIG: sample_format = SND_PCM_FORMAT_S16_BE; break;
-        error(0, "unrecognized byte format %d", playing->format.byte_format);
-        goto fatal;
-      }
-      break;
-    default:
-      error(0, "unsupported sample size %d", playing->format.bits);
-      goto fatal;
-    }
-    if((err = snd_pcm_hw_params_set_format(pcm, hwparams,
-                                           sample_format)) < 0) {
-      error(0, "error from snd_pcm_hw_params_set_format (%d): %d",
-            sample_format, err);
-      goto fatal;
-    }
-    rate = playing->format.rate;
-    if((err = snd_pcm_hw_params_set_rate_near(pcm, hwparams, &rate, 0)) < 0) {
-      error(0, "error from snd_pcm_hw_params_set_rate (%d): %d",
-            playing->format.rate, err);
-      goto fatal;
-    }
-    if(rate != (unsigned)playing->format.rate)
-      info("want rate %d, got %u", playing->format.rate, rate);
-    if((err = snd_pcm_hw_params_set_channels(pcm, hwparams,
-                                             playing->format.channels)) < 0) {
-      error(0, "error from snd_pcm_hw_params_set_channels (%d): %d",
-            playing->format.channels, err);
-      goto fatal;
-    }
-    bufsize = 3 * FRAMES;
-    pcm_bufsize = bufsize;
-    if((err = snd_pcm_hw_params_set_buffer_size_near(pcm, hwparams,
-                                                     &pcm_bufsize)) < 0)
-      fatal(0, "error from snd_pcm_hw_params_set_buffer_size (%d): %d",
-            3 * FRAMES, err);
-    if(pcm_bufsize != 3 * FRAMES && pcm_bufsize != last_pcm_bufsize)
-      info("asked for PCM buffer of %d frames, got %d",
-           3 * FRAMES, (int)pcm_bufsize);
-    last_pcm_bufsize = pcm_bufsize;
-    if((err = snd_pcm_hw_params(pcm, hwparams)) < 0)
-      fatal(0, "error calling snd_pcm_hw_params: %d", err);
-    D(("set up sw params"));
-    snd_pcm_sw_params_alloca(&swparams);
-    if((err = snd_pcm_sw_params_current(pcm, swparams)) < 0)
-      fatal(0, "error calling snd_pcm_sw_params_current: %d", err);
-    if((err = snd_pcm_sw_params_set_avail_min(pcm, swparams, FRAMES)) < 0)
-      fatal(0, "error calling snd_pcm_sw_params_set_avail_min %d: %d",
-            FRAMES, err);
-    if((err = snd_pcm_sw_params(pcm, swparams)) < 0)
-      fatal(0, "error calling snd_pcm_sw_params: %d", err);
-    pcm_format = playing->format;
-    bpf = bytes_per_frame(&pcm_format);
-    D(("acquired audio device"));
-    log_params(hwparams, swparams);
-    ready = 1;
+    return;
   }
-  return 0;
-fatal:
-  abandon();
-error:
-  /* We assume the error is temporary and that we'll retry in a bit. */
-  if(pcm) {
-    snd_pcm_close(pcm);
-    pcm = 0;
+  if(backend->flags & FIXED_FORMAT)
+    device_format = config->sample_format;
+  if(backend->activate) {
+    backend->activate();
+  } else {
+    assert(backend->flags & FIXED_FORMAT);
+    /* ...otherwise device_format not set */
+    device_state = device_open;
   }
-#endif
-  return -1;
+  if(device_state == device_open)
+    device_bpf = bytes_per_frame(&device_format);
 }
 
-/* Check to see whether the current track has finished playing */
+/** @brief Check whether the current track has finished
+ *
+ * The current track is determined to have finished either if the input stream
+ * eded before the format could be determined (i.e. it is malformed) or the
+ * input is at end of file and there is less than a frame left unplayed.  (So
+ * it copes with decoders that crash mid-frame.)
+ */
 static void maybe_finished(void) {
   if(playing
      && playing->eof
@@ -509,108 +420,56 @@ static void maybe_finished(void) {
     abandon();
 }
 
-static void fork_kid(void) {
-  pid_t kid;
-  int pfd[2];
-  if(kidfd != -1) close(kidfd);
-  xpipe(pfd);
-  kid = xfork();
-  if(!kid) {
-    xdup2(pfd[0], 0);
-    close(pfd[0]);
-    close(pfd[1]);
-    execl("/bin/sh", "sh", "-c", config->speaker_command, (char *)0);
-    fatal(errno, "error execing /bin/sh");
-  }
-  close(pfd[0]);
-  kidfd = pfd[1];
-  D(("forked kid %d, fd = %d", kid, kidfd));
-}
-
+/** @brief Play up to @p frames frames of audio
+ *
+ * It is always safe to call this function.
+ * - If @ref playing is 0 then it will just return
+ * - If @ref paused is non-0 then it will just return
+ * - If @ref device_state != @ref device_open then it will call activate() and
+ * return if it it fails.
+ * - If there is not enough audio to play then it play what is available.
+ *
+ * If there are not enough frames to play then whatever is available is played
+ * instead.  It is up to mainloop() to ensure that play() is not called when
+ * unreasonably only an small amounts of data is available to play.
+ */
 static void play(size_t frames) {
-  size_t avail_bytes, written_frames;
+  size_t avail_frames, avail_bytes, written_frames;
   ssize_t written_bytes;
 
-  if(activate()) {
-    if(playing)
-      forceplay = frames;
-    else
-      forceplay = 0;                    /* Must have called abandon() */
+  /* Make sure there's a track to play and it is not pasued */
+  if(!playing || paused)
     return;
+  /* Make sure the output device is open and has the right sample format */
+  if(device_state != device_open
+     || !formats_equal(&device_format, &playing->format)) {
+    activate(); 
+    if(device_state != device_open)
+      return;
   }
-  D(("play: play %zu/%zu%s %dHz %db %dc",  frames, playing->used / bpf,
+  D(("play: play %zu/%zu%s %dHz %db %dc",  frames, playing->used / device_bpf,
      playing->eof ? " EOF" : "",
      playing->format.rate,
      playing->format.bits,
      playing->format.channels));
-  /* If we haven't got enough bytes yet wait until we have.  Exception: when
-   * we are at eof. */
-  if(playing->used < frames * bpf && !playing->eof) {
-    forceplay = frames;
-    return;
-  }
-  /* We have got enough data so don't force play again */
-  forceplay = 0;
   /* Figure out how many frames there are available to write */
   if(playing->start + playing->used > playing->size)
+    /* The ring buffer is currently wrapped, only play up to the wrap point */
     avail_bytes = playing->size - playing->start;
   else
+    /* The ring buffer is not wrapped, can play the lot */
     avail_bytes = playing->used;
-
-  if(!config->speaker_command) {
-#if API_ALSA
-    snd_pcm_sframes_t pcm_written_frames;
-    size_t avail_frames;
-    int err;
-
-    avail_frames = avail_bytes / bpf;
-    if(avail_frames > frames)
-      avail_frames = frames;
-    if(!avail_frames)
-      return;
-    pcm_written_frames = snd_pcm_writei(pcm,
-                                        playing->buffer + playing->start,
-                                        avail_frames);
-    D(("actually play %zu frames, wrote %d",
-       avail_frames, (int)pcm_written_frames));
-    if(pcm_written_frames < 0) {
-      switch(pcm_written_frames) {
-        case -EPIPE:                        /* underrun */
-          error(0, "snd_pcm_writei reports underrun");
-          if((err = snd_pcm_prepare(pcm)) < 0)
-            fatal(0, "error calling snd_pcm_prepare: %d", err);
-          return;
-        case -EAGAIN:
-          return;
-        default:
-          fatal(0, "error calling snd_pcm_writei: %d",
-                (int)pcm_written_frames);
-      }
-    }
-    written_frames = pcm_written_frames;
-    written_bytes = written_frames * bpf;
-#else
-    assert(!"reached");
-#endif
-  } else {
-    if(avail_bytes > frames * bpf)
-      avail_bytes = frames * bpf;
-    written_bytes = write(kidfd, playing->buffer + playing->start,
-                          avail_bytes);
-    D(("actually play %zu bytes, wrote %d",
-       avail_bytes, (int)written_bytes));
-    if(written_bytes < 0) {
-      switch(errno) {
-        case EPIPE:
-          error(0, "hmm, kid died; trying another");
-          fork_kid();
-          return;
-        case EAGAIN:
-          return;
-      }
-    }
-    written_frames = written_bytes / bpf; /* good enough */
-  }
+  avail_frames = avail_bytes / device_bpf;
+  /* Only play up to the requested amount */
+  if(avail_frames > frames)
+    avail_frames = frames;
+  if(!avail_frames)
+    return;
+  /* Play it, Sam */
+  written_frames = backend->play(avail_frames);
+  written_bytes = written_frames * device_bpf;
+  /* written_bytes and written_frames had better both be set and correct by
+   * this point */
   playing->start += written_bytes;
   playing->used -= written_bytes;
   playing->played += written_frames;
@@ -619,6 +478,7 @@ static void play(size_t frames) {
   if(!playing->used || playing->start == playing->size)
     playing->start = 0;
   frames -= written_frames;
+  return;
 }
 
 /* Notify the server what we're up to. */
@@ -636,16 +496,16 @@ static void report(void) {
 }
 
 static void reap(int __attribute__((unused)) sig) {
-  pid_t kid;
+  pid_t cmdpid;
   int st;
 
   do
-    kid = waitpid(-1, &st, WNOHANG);
-  while(kid > 0);
+    cmdpid = waitpid(-1, &st, WNOHANG);
+  while(cmdpid > 0);
   signal(SIGCHLD, reap);
 }
 
-static int addfd(int fd, int events) {
+int addfd(int fd, int events) {
   if(fdno < NFDS) {
     fds[fdno].fd = fd;
     fds[fdno].events = events;
@@ -654,95 +514,62 @@ static int addfd(int fd, int events) {
     return -1;
 }
 
-int main(int argc, char **argv) {
-  int n, fd, stdin_slot, alsa_slots, kid_slot;
-  struct track *t;
-  struct speaker_message sm;
+/** @brief Table of speaker backends */
+static const struct speaker_backend *backends[] = {
 #if API_ALSA
-  int alsa_nslots = -1, err;
+  &alsa_backend,
 #endif
+  &command_backend,
+  &network_backend,
+  0
+};
+
+/** @brief Return nonzero if we want to play some audio
+ *
+ * We want to play audio if there is a current track; and it is not paused; and
+ * there are at least @ref FRAMES frames of audio to play, or we are in sight
+ * of the end of the current track.
+ */
+static int playable(void) {
+  return playing
+         && !paused
+         && (playing->used >= FRAMES || playing->eof);
+}
+
+/** @brief Main event loop */
+static void mainloop(void) {
+  struct track *t;
+  struct speaker_message sm;
+  int n, fd, stdin_slot, timeout;
 
-  set_progname(argv);
-  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
-  while((n = getopt_long(argc, argv, "hVc:dD", options, 0)) >= 0) {
-    switch(n) {
-    case 'h': help();
-    case 'V': version();
-    case 'c': configfile = optarg; break;
-    case 'd': debugging = 1; break;
-    case 'D': debugging = 0; break;
-    default: fatal(0, "invalid option");
-    }
-  }
-  if(getenv("DISORDER_DEBUG_SPEAKER")) debugging = 1;
-  /* If stderr is a TTY then log there, otherwise to syslog. */
-  if(!isatty(2)) {
-    openlog(progname, LOG_PID, LOG_DAEMON);
-    log_default = &log_syslog;
-  }
-  if(config_read()) fatal(0, "cannot read configuration");
-  /* ignore SIGPIPE */
-  signal(SIGPIPE, SIG_IGN);
-  /* reap kids */
-  signal(SIGCHLD, reap);
-  /* set nice value */
-  xnice(config->nice_speaker);
-  /* change user */
-  become_mortal();
-  /* make sure we're not root, whatever the config says */
-  if(getuid() == 0 || geteuid() == 0) fatal(0, "do not run as root");
-  info("started");
-  if(config->speaker_command)
-    fork_kid();
-  else {
-#if API_ALSA
-    /* ok */
-#else
-    fatal(0, "invoked speaker but no speaker_command and no known sound API");
- #endif
-  }
   while(getppid() != 1) {
     fdno = 0;
+    /* By default we will wait up to a second before thinking about current
+     * state. */
+    timeout = 1000;
     /* Always ready for commands from the main server. */
     stdin_slot = addfd(0, POLLIN);
     /* Try to read sample data for the currently playing track if there is
      * buffer space. */
-    if(playing && !playing->eof && playing->used < playing->size) {
+    if(playing && !playing->eof && playing->used < playing->size)
       playing->slot = addfd(playing->fd, POLLIN);
-    else if(playing)
+    else if(playing)
       playing->slot = -1;
-    /* If forceplay is set then wait until it succeeds before waiting on the
-     * sound device. */
-    alsa_slots = -1;
-    kid_slot = -1;
-    if(ready && !forceplay) {
-      if(config->speaker_command) {
-        if(kidfd >= 0)
-          kid_slot = addfd(kidfd, POLLOUT);
-      } else {
-#if API_ALSA
-        int retry = 3;
-        
-        alsa_slots = fdno;
-        do {
-          retry = 0;
-          alsa_nslots = snd_pcm_poll_descriptors(pcm, &fds[fdno], NFDS - fdno);
-          if((alsa_nslots <= 0
-              || !(fds[alsa_slots].events & POLLOUT))
-             && snd_pcm_state(pcm) == SND_PCM_STATE_XRUN) {
-            error(0, "underrun detected after call to snd_pcm_poll_descriptors()");
-            if((err = snd_pcm_prepare(pcm)))
-              fatal(0, "error calling snd_pcm_prepare: %d", err);
-          } else
-            break;
-        } while(retry-- > 0);
-        if(alsa_nslots >= 0)
-          fdno += alsa_nslots;
-#endif
-      }
+    if(playable()) {
+      /* We want to play some audio.  If the device is closed then we attempt
+       * to open it. */
+      if(device_state == device_closed)
+        activate();
+      /* If the device is (now) open then we will wait up until it is ready for
+       * more.  If something went wrong then we should have device_error
+       * instead, but the post-poll code will cope even if it's
+       * device_closed. */
+      if(device_state == device_open)
+        backend->beforepoll();
     }
     /* If any other tracks don't have a full buffer, try to read sample data
-     * from them. */
+     * from them.  We do this last of all, so that if we run out of slots,
+     * nothing important can't be monitored. */
     for(t = tracks; t; t = t->next)
       if(t != playing) {
         if(!t->eof && t->used < t->size) {
@@ -750,37 +577,33 @@ int main(int argc, char **argv) {
         } else
           t->slot = -1;
       }
-    /* Wait up to a second before thinking about current state */
-    n = poll(fds, fdno, 1000);
+    /* Wait for something interesting to happen */
+    n = poll(fds, fdno, timeout);
     if(n < 0) {
       if(errno == EINTR) continue;
       fatal(errno, "error calling poll");
     }
     /* Play some sound before doing anything else */
-    if(alsa_slots != -1) {
-#if API_ALSA
-      unsigned short alsa_revents;
-      
-      if((err = snd_pcm_poll_descriptors_revents(pcm,
-                                                 &fds[alsa_slots],
-                                                 alsa_nslots,
-                                                 &alsa_revents)) < 0)
-        fatal(0, "error calling snd_pcm_poll_descriptors_revents: %d", err);
-      if(alsa_revents & (POLLOUT | POLLERR))
-        play(3 * FRAMES);
-#endif
-    } else if(kid_slot != -1) {
-      if(fds[kid_slot].revents & (POLLOUT | POLLERR))
+    if(playable()) {
+      /* We want to play some audio */
+      if(device_state == device_open) {
+        if(backend->ready())
+          play(3 * FRAMES);
+      } else {
+        /* We must be in _closed or _error, and it should be the latter, but we
+         * cope with either.
+         *
+         * We most likely timed out, so now is a good time to retry.  play()
+         * knows to re-activate the device if necessary.
+         */
         play(3 * FRAMES);
-    } else {
-      /* Some attempt to play must have failed */
-      if(playing && !paused)
-        play(forceplay);
-      else
-        forceplay = 0;                  /* just in case */
+      }
     }
     /* Perhaps we have a command to process */
     if(fds[stdin_slot].revents & POLLIN) {
+      /* There might (in theory) be several commands queued up, but in general
+       * this won't be the case, so we don't bother looping around to pick them
+       * all up. */ 
       n = speaker_recv(0, &sm, &fd);
       if(n > 0)
        switch(sm.type) {
@@ -796,7 +619,10 @@ int main(int argc, char **argv) {
          t = findtrack(sm.id, 1);
           if(fd != -1) acquire(t, fd);
           playing = t;
-          play(bufsize);
+          /* We attempt to play straight away rather than going round the loop.
+           * play() is clever enough to perform any activation that is
+           * required. */
+          play(3 * FRAMES);
           report();
          break;
        case SM_PAUSE:
@@ -808,8 +634,9 @@ int main(int argc, char **argv) {
           D(("SM_RESUME"));
           if(paused) {
             paused = 0;
+            /* As for SM_PLAY we attempt to play straight away. */
             if(playing)
-              play(bufsize);
+              play(3 * FRAMES);
           }
           report();
          break;
@@ -841,19 +668,60 @@ int main(int argc, char **argv) {
     for(t = tracks; t; t = t->next)
       if(t->slot != -1 && (fds[t->slot].revents & (POLLIN | POLLHUP)))
          fill(t);
-    /* We might be able to play now */
-    if(ready && forceplay && playing && !paused)
-      play(forceplay);
     /* Maybe we finished playing a track somewhere in the above */
     maybe_finished();
     /* If we don't need the sound device for now then close it for the benefit
      * of anyone else who wants it. */
-    if((!playing || paused) && ready)
+    if((!playing || paused) && device_state == device_open)
       idle();
     /* If we've not reported out state for a second do so now. */
     if(time(0) > last_report)
       report();
   }
+}
+
+int main(int argc, char **argv) {
+  int n;
+
+  set_progname(argv);
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  while((n = getopt_long(argc, argv, "hVc:dD", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'c': configfile = optarg; break;
+    case 'd': debugging = 1; break;
+    case 'D': debugging = 0; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  if(getenv("DISORDER_DEBUG_SPEAKER")) debugging = 1;
+  /* If stderr is a TTY then log there, otherwise to syslog. */
+  if(!isatty(2)) {
+    openlog(progname, LOG_PID, LOG_DAEMON);
+    log_default = &log_syslog;
+  }
+  if(config_read()) fatal(0, "cannot read configuration");
+  /* ignore SIGPIPE */
+  signal(SIGPIPE, SIG_IGN);
+  /* reap kids */
+  signal(SIGCHLD, reap);
+  /* set nice value */
+  xnice(config->nice_speaker);
+  /* change user */
+  become_mortal();
+  /* make sure we're not root, whatever the config says */
+  if(getuid() == 0 || geteuid() == 0) fatal(0, "do not run as root");
+  /* identify the backend used to play */
+  for(n = 0; backends[n]; ++n)
+    if(backends[n]->backend == config->speaker_backend)
+      break;
+  if(!backends[n])
+    fatal(0, "unsupported backend %d", config->speaker_backend);
+  backend = backends[n];
+  /* backend-specific initialization */
+  backend->init();
+  mainloop();
   info("stopped (parent terminated)");
   exit(0);
 }
diff --git a/server/speaker.h b/server/speaker.h
new file mode 100644 (file)
index 0000000..53230cb
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005, 2006, 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file server/speaker.h
+ * @brief Speaker process
+ */
+#ifndef SPEAKER_H
+#define SPEAKER_H
+
+#ifdef WORDS_BIGENDIAN
+# define MACHINE_AO_FMT AO_FMT_BIG
+#else
+# define MACHINE_AO_FMT AO_FMT_LITTLE
+#endif
+
+/** @brief How many seconds of input to buffer
+ *
+ * While any given connection has this much audio buffered, no more reads will
+ * be issued for that connection.  The decoder will have to wait.
+ */
+#define BUFFER_SECONDS 5
+
+/** @brief Minimum number of frames to try to play at once
+ *
+ * The main loop will only attempt to play any audio when this many
+ * frames are available (or the current track has reached the end).
+ * The actual number of frames it attempts to play will often be
+ * larger than this (up to three times).
+ *
+ * For ALSA we request a buffer of three times this size and set the low
+ * watermark to this amount.  The goal is then to keep between 1 and 3 times
+ * this many frames in play.
+ *
+ * For other we attempt to play up to three times this many frames per
+ * shot.  In practice we will often only send much less than this.
+ */
+#define FRAMES 4096
+
+/** @brief Bytes to send per network packet
+ *
+ * Don't make this too big or arithmetic will start to overflow.
+ */
+#define NETWORK_BYTES (1024+sizeof(struct rtp_header))
+
+/** @brief Maximum RTP playahead (ms) */
+#define RTP_AHEAD_MS 1000
+
+/** @brief Maximum number of FDs to poll for */
+#define NFDS 256
+
+/** @brief Track structure
+ *
+ * Known tracks are kept in a linked list.  Usually there will be at most two
+ * of these but rearranging the queue can cause there to be more.
+ */
+struct track {
+  struct track *next;                   /* next track */
+  int fd;                               /* input FD */
+  char id[24];                          /* ID */
+  size_t start, used;                   /* start + bytes used */
+  int eof;                              /* input is at EOF */
+  int got_format;                       /* got format yet? */
+  ao_sample_format format;              /* sample format */
+  unsigned long long played;            /* number of frames played */
+  char *buffer;                         /* sample buffer */
+  size_t size;                          /* sample buffer size */
+  int slot;                             /* poll array slot */
+};
+
+/** @brief Structure of a backend */
+struct speaker_backend {
+  /** @brief Which backend this is
+   *
+   * @c -1 terminates the list.
+   */
+  int backend;
+
+  /** @brief Flags
+   *
+   * Possible values
+   * - @ref FIXED_FORMAT
+   */
+  unsigned flags;
+/** @brief Lock to configured sample format */
+#define FIXED_FORMAT 0x0001
+  
+  /** @brief Initialization
+   *
+   * Called once at startup.  This is responsible for one-time setup
+   * operations, for instance opening a network socket to transmit to.
+   *
+   * When writing to a native sound API this might @b not imply opening the
+   * native sound device - that might be done by @c activate below.
+   */
+  void (*init)(void);
+
+  /** @brief Activation
+   * @return 0 on success, non-0 on error
+   *
+   * Called to activate the output device.
+   *
+   * On input @ref device_state may be anything.  If it is @ref
+   * device_open then the device is already open but might be using
+   * the wrong sample format.  The device should be reconfigured to
+   * use the right sample format.
+   *
+   * If it is @ref device_error then a retry is underway and an
+   * attempt to recover or re-open the device (with the right sample
+   * format) should be made.
+   *
+   * If it is @ref device_closed then the device should be opened with
+   * the right sample format.
+   *
+   * If the @ref FIXED_FORMAT flag is not set then @ref device_format
+   * must be set on success.
+   *
+   * Some devices are effectively always open and have no error state,
+   * in which case this callback can be NULL.  In this case @ref
+   * FIXED_FORMAT must be set.  Note that @ref device_state still
+   * switches between @ref device_open and @ref device_closd in this
+   * case.
+   */
+  void (*activate)(void);
+
+  /** @brief Play sound
+   * @param frames Number of frames to play
+   * @return Number of frames actually played
+   *
+   * If an error occurs (and it is not immediately recovered) this
+   * should set @ref device_state to @ref device_error.
+   */
+  size_t (*play)(size_t frames);
+  
+  /** @brief Deactivation
+   *
+   * Called to deactivate the sound device.  This is the inverse of @c
+   * activate above.
+   *
+   * For sound devices that are open all the time and have no error
+   * state, this callback can be NULL.  Note that @ref device_state
+   * still switches between @ref device_open and @ref device_closd in
+   * this case.
+   */
+  void (*deactivate)(void);
+
+  /** @brief Called before poll()
+   *
+   * Called before the call to poll().  Should call addfd() to update
+   * the FD array and stash the slot number somewhere safe.  This will
+   * only be called if @ref device_state = @ref device_open.
+   */
+  void (*beforepoll)(void);
+
+  /** @brief Called after poll()
+   * @return 1 if output device ready for play, 0 otherwise
+   *
+   * Called after the call to poll().  This will only be called if
+   * @ref device_state = @ref device_open.
+   *
+   * The return value should be 1 if the device was ready to play, or
+   * 0 if it was not.
+   */
+  int (*ready)(void);
+};
+
+/** @brief Possible device states */
+enum device_states {
+  /** @brief The device is closed */
+  device_closed,
+
+  /** @brief The device is open and ready to receive sound
+   *
+   * The current device sample format is potentially part of this state.
+   */
+  device_open,
+  
+  /** @brief An error has occurred on the device
+   *
+   * This state is used to ensure that a small interval is left
+   * between retrying the device.  If errors just set @ref
+   * device_closed then the main loop would busy-wait on broken output
+   * devices.
+   *
+   * The current device sample format is potentially part of this state.
+   */
+  device_error
+};
+
+extern enum device_states device_state;
+extern ao_sample_format device_format;
+extern struct track *tracks;
+extern struct track *playing;
+
+extern const struct speaker_backend network_backend;
+extern const struct speaker_backend alsa_backend;
+extern const struct speaker_backend command_backend;
+
+extern struct pollfd fds[NFDS];
+extern int fdno;
+extern size_t device_bpf;
+extern int idled;
+
+int addfd(int fd, int events);
+int formats_equal(const ao_sample_format *a,
+                  const ao_sample_format *b);
+void abandon(void);
+
+#endif /* SPEAKER_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/uk.org.greenend.rjk.disorder.plist.in b/server/uk.org.greenend.rjk.disorder.plist.in
new file mode 100644 (file)
index 0000000..7e878c0
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+       <key>EnvironmentVariables</key>
+       <dict>
+               <key>LANG</key>
+               <string>en_GB.UTF-8</string>
+               <key>LC_ALL</key>
+               <string>en_GB.UTF-8</string>
+       </dict>
+       <key>Label</key>
+       <string>uk.org.greenend.rjk.disorder</string>
+       <key>ProgramArguments</key>
+       <array>
+               <string>sbindir/disorderd</string>
+               <string>--foreground</string>
+               <string>--syslog</string>
+       </array>
+       <key>WorkingDirectory</key>
+       <string>/Users/jukebox</string>
+       <key>RunAtLoad</key>
+       <true/>
+</dict>
+</plist>
+