Import cmus_2.8.0.orig.tar.gz
authorRyan Kavanagh <rak@debian.org>
Sat, 17 Aug 2019 18:51:27 +0000 (19:51 +0100)
committerRyan Kavanagh <rak@debian.org>
Sat, 17 Aug 2019 18:51:27 +0000 (19:51 +0100)
[dgit import orig cmus_2.8.0.orig.tar.gz]

206 files changed:
.github/ISSUE_TEMPLATE.md [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.travis.yml [new file with mode: 0644]
AUTHORS [new file with mode: 0644]
COPYING [new file with mode: 0644]
Doc/.gitignore [new file with mode: 0644]
Doc/cmus-remote.txt [new file with mode: 0644]
Doc/cmus-tutorial.txt [new file with mode: 0644]
Doc/cmus.txt [new file with mode: 0644]
Doc/ttman.c [new file with mode: 0644]
Makefile [new file with mode: 0644]
README.md [new file with mode: 0644]
ape.c [new file with mode: 0644]
ape.h [new file with mode: 0644]
browser.c [new file with mode: 0644]
browser.h [new file with mode: 0644]
buffer.c [new file with mode: 0644]
buffer.h [new file with mode: 0644]
cache.c [new file with mode: 0644]
cache.h [new file with mode: 0644]
channelmap.c [new file with mode: 0644]
channelmap.h [new file with mode: 0644]
cmdline.c [new file with mode: 0644]
cmdline.h [new file with mode: 0644]
cmus-status-display [new file with mode: 0755]
cmus.c [new file with mode: 0644]
cmus.h [new file with mode: 0644]
command_mode.c [new file with mode: 0644]
command_mode.h [new file with mode: 0644]
comment.c [new file with mode: 0644]
comment.h [new file with mode: 0644]
compiler.h [new file with mode: 0644]
configure [new file with mode: 0755]
contrib/README [new file with mode: 0644]
contrib/_cmus [new file with mode: 0644]
contrib/cmus-updategaim.py [new file with mode: 0644]
contrib/cmus-updatepidgin.py [new file with mode: 0644]
contrib/cmus.bash-completion [new file with mode: 0644]
convert.c [new file with mode: 0644]
convert.h [new file with mode: 0644]
cue.c [new file with mode: 0644]
cue.h [new file with mode: 0644]
cue_utils.c [new file with mode: 0644]
cue_utils.h [new file with mode: 0644]
data/cyan.theme [new file with mode: 0644]
data/default.theme [new file with mode: 0644]
data/dracula.theme [new file with mode: 0644]
data/gray-88.theme [new file with mode: 0644]
data/green-mono-88.theme [new file with mode: 0644]
data/green.theme [new file with mode: 0644]
data/gruvbox-alt.theme [new file with mode: 0644]
data/gruvbox.theme [new file with mode: 0644]
data/jellybeans.theme [new file with mode: 0644]
data/night.theme [new file with mode: 0644]
data/rc [new file with mode: 0644]
data/solarized-dark.theme [new file with mode: 0644]
data/solarized-light.theme [new file with mode: 0644]
data/xterm-white.theme [new file with mode: 0644]
data/zenburn.theme [new file with mode: 0644]
debug.c [new file with mode: 0644]
debug.h [new file with mode: 0644]
discid.c [new file with mode: 0644]
discid.h [new file with mode: 0644]
editable.c [new file with mode: 0644]
editable.h [new file with mode: 0644]
expr.c [new file with mode: 0644]
expr.h [new file with mode: 0644]
file.c [new file with mode: 0644]
file.h [new file with mode: 0644]
filters.c [new file with mode: 0644]
filters.h [new file with mode: 0644]
format_print.c [new file with mode: 0644]
format_print.h [new file with mode: 0644]
gbuf.c [new file with mode: 0644]
gbuf.h [new file with mode: 0644]
glob.c [new file with mode: 0644]
glob.h [new file with mode: 0644]
help.c [new file with mode: 0644]
help.h [new file with mode: 0644]
history.c [new file with mode: 0644]
history.h [new file with mode: 0644]
http.c [new file with mode: 0644]
http.h [new file with mode: 0644]
id3.c [new file with mode: 0644]
id3.h [new file with mode: 0644]
input.c [new file with mode: 0644]
input.h [new file with mode: 0644]
ip.h [new file with mode: 0644]
ip/aac.c [new file with mode: 0644]
ip/aac.h [new file with mode: 0644]
ip/bass.c [new file with mode: 0644]
ip/cdio.c [new file with mode: 0644]
ip/cue.c [new file with mode: 0644]
ip/ffmpeg.c [new file with mode: 0644]
ip/flac.c [new file with mode: 0644]
ip/mad.c [new file with mode: 0644]
ip/mikmod.c [new file with mode: 0644]
ip/modplug.c [new file with mode: 0644]
ip/mp4.c [new file with mode: 0644]
ip/mpc.c [new file with mode: 0644]
ip/nomad.c [new file with mode: 0644]
ip/nomad.h [new file with mode: 0644]
ip/opus.c [new file with mode: 0644]
ip/vorbis.c [new file with mode: 0644]
ip/vtx.c [new file with mode: 0644]
ip/wav.c [new file with mode: 0644]
ip/wavpack.c [new file with mode: 0644]
iter.h [new file with mode: 0644]
job.c [new file with mode: 0644]
job.h [new file with mode: 0644]
keys.c [new file with mode: 0644]
keys.h [new file with mode: 0644]
keyval.c [new file with mode: 0644]
keyval.h [new file with mode: 0644]
lib.c [new file with mode: 0644]
lib.h [new file with mode: 0644]
list.h [new file with mode: 0644]
load_dir.c [new file with mode: 0644]
load_dir.h [new file with mode: 0644]
locking.c [new file with mode: 0644]
locking.h [new file with mode: 0644]
main.c [new file with mode: 0644]
mergesort.c [new file with mode: 0644]
mergesort.h [new file with mode: 0644]
misc.c [new file with mode: 0644]
misc.h [new file with mode: 0644]
mixer.h [new file with mode: 0644]
mpris.c [new file with mode: 0644]
mpris.h [new file with mode: 0644]
op.h [new file with mode: 0644]
op/alsa.c [new file with mode: 0644]
op/ao.c [new file with mode: 0644]
op/arts.c [new file with mode: 0644]
op/coreaudio.c [new file with mode: 0644]
op/jack.c [new file with mode: 0644]
op/mixer_alsa.c [new file with mode: 0644]
op/mixer_oss.c [new file with mode: 0644]
op/mixer_sun.c [new file with mode: 0644]
op/oss.c [new file with mode: 0644]
op/pulse.c [new file with mode: 0644]
op/roar.c [new file with mode: 0644]
op/sndio.c [new file with mode: 0644]
op/sun.c [new file with mode: 0644]
op/waveout.c [new file with mode: 0644]
options.c [new file with mode: 0644]
options.h [new file with mode: 0644]
output.c [new file with mode: 0644]
output.h [new file with mode: 0644]
path.c [new file with mode: 0644]
path.h [new file with mode: 0644]
pcm.c [new file with mode: 0644]
pcm.h [new file with mode: 0644]
pl.c [new file with mode: 0644]
pl.h [new file with mode: 0644]
play_queue.c [new file with mode: 0644]
play_queue.h [new file with mode: 0644]
player.c [new file with mode: 0644]
player.h [new file with mode: 0644]
prog.c [new file with mode: 0644]
prog.h [new file with mode: 0644]
rbtree.c [new file with mode: 0644]
rbtree.h [new file with mode: 0644]
read_wrapper.c [new file with mode: 0644]
read_wrapper.h [new file with mode: 0644]
scripts/checks.sh [new file with mode: 0644]
scripts/configure.sh [new file with mode: 0644]
scripts/ffmpeg_test.sh [new file with mode: 0755]
scripts/gen_decomp.py [new file with mode: 0755]
scripts/install [new file with mode: 0755]
scripts/lib.mk [new file with mode: 0644]
scripts/uninstall [new file with mode: 0755]
scripts/utils.sh [new file with mode: 0644]
search.c [new file with mode: 0644]
search.h [new file with mode: 0644]
search_mode.c [new file with mode: 0644]
search_mode.h [new file with mode: 0644]
server.c [new file with mode: 0644]
server.h [new file with mode: 0644]
sf.h [new file with mode: 0644]
spawn.c [new file with mode: 0644]
spawn.h [new file with mode: 0644]
tabexp.c [new file with mode: 0644]
tabexp.h [new file with mode: 0644]
tabexp_file.c [new file with mode: 0644]
tabexp_file.h [new file with mode: 0644]
track.c [new file with mode: 0644]
track.h [new file with mode: 0644]
track_info.c [new file with mode: 0644]
track_info.h [new file with mode: 0644]
tree.c [new file with mode: 0644]
u_collate.c [new file with mode: 0644]
u_collate.h [new file with mode: 0644]
uchar.c [new file with mode: 0644]
uchar.h [new file with mode: 0644]
ui_curses.c [new file with mode: 0644]
ui_curses.h [new file with mode: 0644]
unidecomp.h [new file with mode: 0644]
utils.h [new file with mode: 0644]
window.c [new file with mode: 0644]
window.h [new file with mode: 0644]
worker.c [new file with mode: 0644]
worker.h [new file with mode: 0644]
xmalloc.c [new file with mode: 0644]
xmalloc.h [new file with mode: 0644]
xstrjoin.c [new file with mode: 0644]
xstrjoin.h [new file with mode: 0644]

diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644 (file)
index 0000000..1af835f
--- /dev/null
@@ -0,0 +1,17 @@
+<!-- Please fill the following if you have found an issue, delete if otherwise. -->
+---
+
+1) `uname -a`:
+```
+(paste output here)
+```
+
+2) `cmus --version`:
+```
+(paste output here)
+```
+
+3) `cmus --plugins`:
+```
+(paste output here)
+```
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..31e4bfa
--- /dev/null
@@ -0,0 +1,24 @@
+# normal ignores
+.*
+*~
+*.[ao]
+*.lo
+*.so
+tags
+cscope.out
+!.gitignore
+!.travis.yml
+!.github
+
+# top-level ignores
+/*.spec
+/config
+/config.mk
+/cmus
+/cmus-remote
+
+# Cygwin stuff
+*.exe
+/cmus.base
+/cmus.def
+/cmus.exp
diff --git a/.travis.yml b/.travis.yml
new file mode 100644 (file)
index 0000000..b289f9a
--- /dev/null
@@ -0,0 +1,52 @@
+dist: trusty
+
+language: c
+
+os:
+    - linux
+    - osx
+
+compiler:
+    - clang
+    - gcc
+
+addons:
+    apt:
+        sources:
+            - ubuntu-toolchain-r-test
+        packages:
+            - g++-5
+            - libncursesw5-dev
+            - libpulse-dev
+            - libasound2-dev
+            - libroar-dev
+            - libao-dev
+            - libjack-dev
+            - libsamplerate0-dev
+            - libcdio-dev
+            - libcdio-cdda-dev
+            - libcddb2-dev
+            # - libopus-dev
+            - libflac-dev
+            - libvorbis-dev
+            - libmpcdec-dev
+            - libwavpack-dev
+            - libmad0-dev
+            - libavformat-dev
+            - libavcodec-dev
+            - libfaad-dev
+            - libmp4v2-dev
+            - libmodplug-dev
+            - libmikmod2-dev
+    homebrew:
+        packages:
+            - libmad
+            - libcddb
+script: 
+    - if [ ${TRAVIS_OS_NAME} == "linux" ] && [ ${TRAVIS_COMPILER} == "gcc" ] ; then
+        ./configure CC=/usr/bin/gcc-5 ; 
+      else
+        ./configure ;
+      fi
+    - make
+    
diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..875047b
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,69 @@
+Maintainers
+-----------
+Gregory Petrosyan <gregory.petrosyan@gmail.com>
+Jason Woofenden <jason@jasonwoof.com>
+
+Original Author
+---------------
+Timo Hirvonen <tihirvon@gmail.com>
+
+NOTE: This list is not complete.  Especially small changes/bug fixes may
+      not be listed here.  See the git repository for full list of
+      contributors.
+
+Credits
+-------
+original help window code and mad.charset option by Sergey Kuleshov
+<svyatogor@gentoo.org>
+
+artist/album mode idea and "display artist/album as a tree instead of two
+windows" idea from divxero <divxero@gmx.net>
+
+play queue idea and other misc ideas from Martin Stubenschrott
+<stubenschrott@gmx.net>
+
+original RPM spec file by Eugene Vlasov <eugene@ikz.ru>
+
+Claes Nästen <pekdon@gmail.com>
+  :seek command
+  --volume option for cmus-remote
+
+Frank Terbeck <ft@bewatermyfriend.org>
+  dynamic keybindings patch
+
+alex <pukpuk@gmx.de>
+  Sun output plugin
+  Tremor support for vorbis plugin
+  NetBSD and OpenBSD port
+  Various bug fixes
+
+Chun-Yu Shei <cshei@cs.indiana.edu>
+  mpc plugin
+  gapless MP3 playback
+
+Johannes Weißl <jargon@molb.org>
+  ao plugin
+
+Gregory Petrosyan <gregory.petrosyan@gmail.com>
+  PulseAudio output plugin
+
+Philipp 'ph3-der-loewe' Schafft <lion@lion.leolix.org>
+  RoarAudio output plugin
+
+Jason Woofenden <jason@jasonwoof.com>
+  Tutorial
+  cmus-unofficial patch-commiter
+
+Niko Efthymiou <nefthy-cmus@nefthy.de>
+  Jack plugin
+
+Tuncer Ayaz <tuncer.ayaz@gmail.com>
+  Opus input plugin
+
+Boris Timofeev <mashin87@gmail.com>
+  vtx plugin
+
+Yue Wang <yuleopen@gmail.com>
+  CoreAudio plugin
+
+Google Inc.
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..d159169
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/Doc/.gitignore b/Doc/.gitignore
new file mode 100644 (file)
index 0000000..9b1572a
--- /dev/null
@@ -0,0 +1,4 @@
+.*
+*.1
+*.7
+ttman
diff --git a/Doc/cmus-remote.txt b/Doc/cmus-remote.txt
new file mode 100644 (file)
index 0000000..f11927f
--- /dev/null
@@ -0,0 +1,150 @@
+@title CMUS-REMOTE 1 05/11/2006 cmus
+
+@h1 NAME
+
+cmus-remote - control cmus
+
+
+@h1 SYNOPSIS
+
+cmus-remote [*OPTION*]... [`FILE`|`DIR`|`PLAYLIST`]...@br
+cmus-remote *-C* `COMMAND`...@br
+cmus-remote
+
+
+@h1 DESCRIPTION
+
+Add `FILE/DIR/PLAYLIST` to playlist, library (*-l*) or play queue (*-q*).
+
+If no arguments are given cmus-remote reads raw commands from stdin (one
+command per line).  Raw commands are cmus' command mode commands.  These same
+commands are used in configuration files and key bindings.  *cmus*(1) contains
+full list of commands.  For consistency also searching is supported:
+*-C /text*.
+
+If *-C* is given, all command line arguments are treated as raw commands.
+
+@h1 OPTIONS
+
+--server SOCKET
+       Connect using socket *SOCKET* instead of `$XDG_RUNTIME_DIR/cmus-socket`.
+
+--help
+       Display usage information and exit.
+
+--version
+       Display version information and exit.
+
+-p, --play
+       Start playing.
+
+-u, --pause
+       Toggle pause.
+
+-U, --pause-playback
+       Pause if currently playing.
+
+-s, --stop
+       Stop playing.
+
+-n, --next
+       Skip forward in playlist.
+
+-r, --prev
+       Skip backward in playlist.
+
+-R, --repeat
+       Toggle repeat.
+
+-S, --shuffle
+       Toggle shuffle.
+
+-v, --volume VOL
+       Change volume. See *vol* command in *cmus*(1).
+
+-k, --seek SEEK
+       Seek. See *seek* command in *cmus*(1).
+
+-Q
+       Get player status information.  Same as *-C status*.  Note that
+       *status* is a special command only available to cmus-remote.
+
+-l, --library
+       Modify library instead of playlist.
+
+-P, --playlist
+       Modify playlist (default).
+
+-q, --queue
+       Modify play queue instead of playlist.
+
+-c, --clear
+       Clear playlist, library (*-l*) or play queue (*-q*).
+
+-C, --raw
+       Treat arguments (instead of stdin) as raw commands.
+
+@h1 REMOTE COMMANDS
+
+Special commands only available to cmus-remote.
+
+status
+       Print information about currently playing track.
+
+format_print
+       Print arguments as `Format Strings`. Each argument starts a new line.
+
+@h1 EXAMPLES
+
+Add playlists/files/directories/URLs to library view (1 & 2):
+
+       @pre
+       $ cmus-remote -l music.m3u \\
+       http://live.urn1350.net:8080/urn_high.ogg
+       @endpre
+
+Load (clear and add) playlist to playlist view (3):
+
+       @pre
+       $ cmus-remote -c music.m3u
+       @endpre
+
+Three different ways to toggle repeat:
+
+       @pre
+       $ cmus-remote -R
+       $ cmus-remote -C "toggle repeat"
+       $ cmus-remote
+       toggle repeat
+       ^D
+       @endpre
+
+Query settings or key bindings:
+
+       @pre
+       $ cmus-remote -C "set repeat?"
+       setting: 'repeat=false'
+       $ cmus-remote -C "showbind common a"
+       bind common a win-add-l
+       @endpre
+
+Dump the playlist to stdout:
+
+       @pre
+       $ cmus-remote -C "save -p -"
+       [...]
+       @endpre
+
+Search works too:
+
+       @pre
+       $ cmus-remote -C /beatles
+       @endpre
+
+@h1 SEE ALSO
+
+*cmus*(1)
+
+@h1 AUTHOR
+
+Written by Timo Hirvonen <tihirvon\@gmail.com>
diff --git a/Doc/cmus-tutorial.txt b/Doc/cmus-tutorial.txt
new file mode 100644 (file)
index 0000000..bfbde48
--- /dev/null
@@ -0,0 +1,264 @@
+@title cmus-tutorial 7 14/02/2010 cmus
+
+
+@h1 NAME
+
+cmus - C\* Music Player tutorial
+
+
+@h1 CONTENTS
+
+Step 1: Starting Cmus
+
+Step 2: Adding Music
+
+Step 3: Playing Tracks From The Library
+
+Step 4: Managing The Queue
+
+Step 5: The Playlists
+
+Step 6: Find that track
+
+Step 7: Customization
+
+Step 8: Quit
+
+Step 9: Further Reading
+
+
+@h1 Step 1: Starting Cmus
+
+When you first launch cmus (just type `cmus` in a terminal and press Enter) it
+will open to the album/artist view, which looks something like this:
+
+@pre
++---------------------------------------------------------------------+
+| Artist / Album             Track                            Library |
+|                          |                                          |
+|                          |                                          |
+|                          |                                          |
+|                          |                                          |
+|                          |                                          |
+|                          |                                          |
+|                          |                                          |
+|                                                                     |
+| . 00:00 - 00:00 vol: 100                     all from library | C   |
+|                                                                     |
++---------------------------------------------------------------------+
+@endpre
+
+This is the view where your artists and albums will be displayed.
+
+
+@h1 Step 2: Adding Music
+
+Press *5* to switch to the file-browser view so we can add some music. You
+should see something like this:
+
+@pre
++---------------------------------------------------------------------+
+| Browser - /home/jasonwoof                                           |
+| ../                                                                 |
+| Desktop/                                                            |
+| MySqueak/                                                           |
+| audio-projects/                                                     |
+| audio/                                                              |
+| bin/                                                                |
+| config/                                                             |
+|                                                                     |
+| . 00:00 - 00:00 vol: 100                     all from library | C   |
+|                                                                     |
++---------------------------------------------------------------------+
+@endpre
+
+Now, use the arrow keys, Enter and Backspace to navigate to where you have
+audio files stored. To add music to your cmus library, use the arrow keys to
+highlight a file or folder, and press *a*. When you press *a* cmus will move you
+to the next line down (so that it is easy to add a bunch of files/folders in a
+row) and start adding the file/folder you pressed *a* on to your library. This
+can take a while if you added a folder with a lot in it. As files are added,
+you will see the second time in the bottom right go up. This is the total
+duration of all the music in the cmus library.
+
+Note: cmus does not move, duplicate or change your files. It just remembers
+where they are and caches the metadata (duration, artist, etc.)
+
+Just to be on the safe side, lets save. Type *:save* and press Enter.
+
+Note: Cmus automatically saves your settings and library and everything when
+you quit, so you probably won't use the save command much.
+
+
+@h1 Step 3: Playing Tracks From The Library
+
+Press *2* to go to the simple library view. You should see something like
+this:
+
+@pre
++---------------------------------------------------------------------+
+| Library ~/.config/cmus/lib.pl - 31 tracks sorted by artist album di |
+| Flying Lizards         . Money (That's What I Want)           02:31 |
+| Jason Woofenden        . VoR Theme                       2009 01:20 |
+| Keali'i Reichel      06. Wanting Memories                1994 04:28 |
+| Molly Lewis            . Tom Cruise Crazy                     03:13 |
+| NonMemory              . pista1                          2009 03:18 |
+| NonMemory            01. pista1                    2009-04-21 04:13 |
+| Ray Charles          06. Halleluja I Love Her So              02:33 |
+|                                                                     |
+| . 00:00 - 2:16:25 vol: 100                   all from library | C   |
+|                                                                     |
++---------------------------------------------------------------------+
+@endpre
+
+Use the up and down arrow keys to select a track you'd like to hear, and press
+Enter to play it. Here are some keys to control playback:
+
+Press *c* to pause/unpause
+Press right/left to seek by 10 seconds
+Press *<*/*>* seek by one minute
+Press *z* to play the previous track and *b* to play the next track
+
+cmus has some great options to control what plays next (if anything) when the
+track ends. The state of these settings are shown in the bottom right corner.
+The first of these shows what collection of tracks (currently "all from
+library") we are playing. Press *m* to cycle through the different options for
+this setting. To the right of that (past the "|") cmus shows the state of four
+toggles. Only toggles which are "on" are shown, so now we only see the *C*.
+Here are the toggles:
+
+[C]ontinue
+
+    If this is off, cmus will always stop at the end of the track. You can
+toggle this setting by pressing *shift-C*.
+
+[R]epeat
+
+    If this is on (and continue is on), when cmus reaches the end of the group
+of tracks you're playing (selected with the *m* key) it will start again from
+the beginning. Press *r* to toggle this setting.
+
+[S]huffle
+
+    If this is on, cmus will choose a random order to play all the tracks
+once. Press *s* to toggle this option.
+
+[F]ollow
+
+    If this is on, cmus will select the currently playing track on track change.
+Press *f* to toggle this option.
+
+
+@h1 Step 4: Managing The Queue
+
+Lets say you're listening to a song, and you want to select which song will
+play next, without interrupting the currently playing song. No problem! Just go
+to the song you want to hear next (in any of the views) and press *e*. The
+queue is FIFO, meaning if you queue up another track, it will play after the
+one you already had queued up.
+
+Note: The queue is not affected by the "shuffle" option described above.
+
+Press *4* to view/edit the queue. This view works and looks a lot like the
+simple library view. The main difference is that you can change the order of
+the tracks with the *p* and *P* keys. You can press *shift-D* to remove a track
+from the queue.
+
+When cmus is ready to play another track (it's reached the end of a track and
+the "continue" setting is on) it will remove the top entry from the queue and
+start playing it.
+
+
+@h1 Step 5: The Playlists
+
+The playlists work like another set of libraries (like view *2*) except that
+(like the queue) you manually set the order of the tracks. This can be quite
+useful if you want to create a mixes of specific tracks or if you want to
+listen to an audio book without having the chapters play when you're playing
+"all from library".
+
+The playlists are on view *3*. But before we go there, lets add some tracks.
+Press *2* to go to the simple library view, go to a track you want and press
+*y* to add it to a playlist. The only visual feedback you'll get that anything
+happened is that the highlight will move down one row. Add a few more so you
+have something to work with.
+
+Now press *3* to go to the playlist. You should see something like this:
+
+@pre
++---------------------------------------------------------------------+
+| Playlist                   Track                              11:32 |
+| * default                | Flying Lizards     . Money (Th...  02:31 |
+|                          | Jason Woofenden    . VoR T... 2009 01:20 |
+|                          | Keali'i Reichel  06. Wanti... 1994 04:28 |
+|                          | Molly Lewis        . Tom Cruis...  03:13 |
+|                          |                                          |
+|                          |                                          |
+|                          |                                          |
+|                                                                     |
+| . 00:00 - 00:00 vol: 100                     all from library | C   |
+|                                                                     |
++---------------------------------------------------------------------+
+@endpre
+
+Just like the queue, you can use the *p*, *P* and *D* keys to move and delete
+tracks from the playlist.
+
+Note: Changing the view (e.g. by pressing *3*) does not affect what cmus will
+play next. To put cmus into "play from the playlist" mode, press Enter on one
+of the tracks in the playlist. To switch modes without interrupting the
+currently-playing song, you can press *shift-M*.
+
+
+@h1 Step 6: Find that track
+
+This step shows various ways you can find track(s) you're looking for.
+
+Search: Press *2* to be sure you're on the simple library view, then press */*
+to start a search. Type a word or two from the track you're looking for. cmus
+will search for tracks that have all those words in them. Press enter to get
+the keyboard out of the search command, and *n* to find the next match.
+
+Tree View: Press *1* to select the tree view. Scroll to the artist, press
+*space* to show their albums, scroll to the album you want, then press tab so
+the keyboard controls the right column. Press tab again to get back to the left
+column.
+
+Filters: See the reference manual (see Further Reading below) for a detailed
+description on how to quickly (and temporarily) hide most of your music.
+
+
+@h1 Step 7: Customization
+
+Cmus has some very cool settings you can tweak, like changing the way tracks
+are displayed (e.g. to display disk numbers), enabling replaygain support or
+changing the keybindings.
+
+Press *7* for a quick overview of the current keybindings and settings.
+
+To change a setting or keybind, just select it (up/down keys) and press enter.
+This will put the command for the current setting in the command now (bottom
+left of your screen), which you can edit to put in a new value/key.
+
+Please see the reference manual (see Further Reading below) for a detailed
+description of all the commands and settings available.
+
+
+@h1 Step 8: Quit
+
+When you're done, type *:q* and press Enter to quit. This will save your
+settings, library, playlist and queue.
+
+
+@h1 Step 9: Further Reading
+
+Cmus comes with a great reference manual. Now that you've got the basics down
+it should be intelligible. Try *man cmus* in a terminal. If that's not
+installed, try opening up `cmus.txt` from the `Doc` directory, or read the latest
+version online:
+
+`https://github.com/cmus/cmus/blob/master/Doc/cmus.txt`
+
+There are more commands and features not covered here like loading and saving
+playlists, controlling cmus remotely with `cmus-remote`, etc.
+
diff --git a/Doc/cmus.txt b/Doc/cmus.txt
new file mode 100644 (file)
index 0000000..46a57c6
--- /dev/null
@@ -0,0 +1,1584 @@
+@title CMUS 1 31/01/2010 cmus
+
+@h1 NAME
+
+cmus - C\* Music Player
+
+
+@h1 SYNOPSIS
+
+cmus [*options*]
+
+
+@h1 DESCRIPTION
+
+cmus is a small ncurses based music player.  It supports various output
+methods by output-plugins. cmus has completely configurable keybindings and
+can be controlled from the outside via *cmus-remote*(1).
+
+@h1 OPTIONS
+
+--listen ADDR
+       Listen to ADDR (UNIX socket) instead of `$CMUS_SOCKET` or
+       `$XDG_RUNTIME_DIR/cmus-socket`.
+       ADDR is either a UNIX socket or host[:port].
+
+       *WARNING*: Using host[:port] is insecure even with password!
+       It might be useful though in LAN if you want multiple local users to
+       able to control cmus.  Never make cmus listen to the internet.
+
+       NOTE: Don't use this option to run multiple instances as same user.
+       That would corrupt the track metadata cache.
+
+--passwd PASSWD
+       Set the password for TCP/IP connections.  Must be set if you are listening
+       from host[:port].  Used in conjunction with --listen.
+
+--plugins
+       List available plugins and exit.
+
+--show-cursor
+       Keep cursor always visible.  This is useful for screen readers.
+
+--help
+       Display usage information and exit.
+
+--version
+       Display version information and exit.
+
+
+@h1 VIEWS
+
+There are 7 views in cmus.  Press keys 1-7 to change active view.
+
+Library view (1)
+       Display all tracks in so-called *library*. Tracks are sorted
+       artist/album tree.  Artist sorting is done alphabetically. Albums are
+       sorted by year.
+
+Sorted library view (2)
+       Displays same content as view 1, but as a simple list which is
+       automatically sorted by user criteria.
+
+Playlist view (3)
+       Displays editable playlists with optional sorting.
+
+Play Queue view (4)
+       Displays queue of tracks which are played next. These tracks are
+       played before anything else (i.e. the playlist or library).
+
+Browser (5)
+       Directory browser.  In this view, music can be added to either the
+       library, marked playlist or queue from the filesystem.
+
+Filters view (6)
+       Lists user defined filters.
+
+Settings view (7)
+       Lists keybindings, unbound commands and options.  Remove bindings with
+       *D* or *del*, change bindings and variables with *enter* and toggle
+       variables with *space*.
+
+@h1 COMMAND LINE
+
+Everything in cmus is implemented as commands which can be typed at command
+line or bound to a key.  To enter command mode type *:*.  To execute a command
+press *ENTER* or to cancel press *ESC* or *CTRL-C*.  Use up/down arrows to
+browse command history.  Use *TAB* to complete commands and parameters, you can
+tab complete almost anything.  You don't need to type full command names if the
+command is unambiguous (no other commands starting with the same characters).
+
+Examples:
+
+       @pre
+       # add files, short for ':add ~/music'
+       :a ~/music
+
+       # change output plugin
+       :set output_plugin=oss
+
+       # start playing
+       # you could just press 'x' which is the default
+       # binding for this command
+       :player-play
+
+       # clear current view (library, playlist or play queue)
+       :clear
+       @endpre
+
+
+@h1 SEARCHING
+
+Search mode works like the command mode, to enter search mode press */* and
+then type the search words and press *ENTER*.  Press *n* to search next or *N*
+to search previous match using the same search words.  Type *?* to search
+backwards.
+
+In views 1-4 words are compared to artist, album and title tags. Type
+*//WORDS* or *??WORDS* to search only artists/albums in view 1 or titles in
+views 2-4. If the file doesn't have tags words are compared to filename
+without path.
+
+Searching works in views 5-7 too and its logic should be pretty obvious.
+
+
+@h1 PLAYLIST EDITING
+
+@h2 Selecting Tracks
+
+Editing commands affect the currently marked tracks or if there are no marked
+tracks the currently selected track (or selected artist/album in view 1).
+
+Mark selected track by pressing *SPACE*.  Marked tracks appear with a gray
+background.  You can only mark tracks in the list views (2-4).
+
+@h2 Copying Tracks Between Views
+
+You can copy marked or selected tracks from views 1-5.
+
+@li *a*
+copy tracks to the library (1-2)
+
+@li *y*
+copy tracks to the marked playlist (3)
+
+@li *e*
+append tracks to the play queue (4)
+
+@li *E*
+prepend tracks to the play queue (4)
+
+@h2 Moving Tracks
+
+In views 2-4 you can move tracks within the list. Note that moving is
+disabled if the view is auto-sorted (see *lib_sort* and *pl_sort* options).
+
+Pressing *p* moves marked tracks to the position immediately after the
+selected track.  *P* moves them to the position immediately before the
+selected track.  If there are no marked tracks then the selected track is
+moved down (*p*) or up (*P*).
+
+NOTE: Changing active filters reloads view 2 so it isn't a good idea to
+manually order tracks in the view.
+
+@h2 Removing Tracks
+
+Press *D* or *delete* to remove marked or selected tracks in the current view
+(1-4).  The tracks will be removed immediately from the view without asking
+for confirmation.  In the browser and filters views the same keys are used to
+remove a file or filter (will ask for confirmation).
+
+
+@h1 STATUS LINE
+
+Right hand side of the status line (second row from the bottom, black text on
+a grey background) consists of the following fields:
+
+@pre
+aaa_mode & play_sorted & play_library | continue follow repeat shuffle
+@endpre
+
+NOTE: *aaa_mode* and *play_sorted* will be displayed only if *play_library* is
+*true* because these are meaningless when playing the playlists (view 3).
+
+Pressing *m*, *o*, *M*, *C*, *r* and *s* keys should make it easier to
+understand what all those fields mean.
+
+See CONFIGURATION OPTIONS section for more information about these options.
+
+
+@h1 KEYBINDINGS
+
+Here's list of default keybindings.  See *unbind* and *bind* commands in the
+COMMANDS section.
+
+
+@h2 Common Context
+@pre
+q                           quit -i
+^C                          echo Type :quit<enter> to exit cmus.
+I                           echo {}
+b                           player-next
+c                           player-pause
+x                           player-play
+z                           player-prev
+v                           player-stop
+^L                          refresh
+/                           search-start
+?                           search-b-start
+n                           search-next
+N                           search-prev
+.                           seek +1m
+l, right                    seek +5
+,                           seek -1m
+h, left                     seek -5
+m                           toggle aaa_mode
+C                           toggle continue
+M                           toggle play_library
+o                           toggle play_sorted
+r                           toggle repeat
+^R                          toggle repeat_current
+t                           toggle show_remaining_time
+s                           toggle shuffle
+f                           toggle follow
+F                           push filter<space>
+L                           push live-filter<space>
+u                           update-cache
+1                           view tree
+2                           view sorted
+3                           view playlist
+4                           view queue
+5                           view browser
+6                           view filters
+7                           view settings
+!                           push shell<space>
+]                           vol +0 +1
+[                           vol +1 +0
++, =                        vol +10%
+}                           vol -0 -1
+{                           vol -1 -0
+-                           vol -10%
+enter, mlb_click_selected   win-activate
+E                           win-add-Q
+a                           win-add-l
+y                           win-add-p
+e                           win-add-q
+G, end                      win-bottom
+down, j, mouse_scroll_down  win-down
+p                           win-mv-after
+P                           win-mv-before
+tab                         win-next
+^F, page_down               win-page-down
+^B, page_up                 win-page-up
+^E                          win-scroll-down
+^Y                          win-scroll-up
+D, delete                   win-remove
+i                           win-sel-cur
+space                       win-toggle
+g, home                     win-top
+k, up, mouse_scroll_up      win-up
+@endpre
+
+@h2 Browser Context
+@pre
+space          win-activate
+backspace      browser-up
+i              toggle show_hidden
+u              win-update
+@endpre
+
+
+@h1 LIBRARY VIEW SORTING
+
+The library view (the tree-like one; not the sorted library view, for which
+the sorting is controlled by the user by setting lib_sort - see `CONFIGURATION
+OPTIONS`), is sorted automatically by cmus using the information found in the
+tagging information provided by the audio files.
+
+Generally, in the library view cmus uses three levels of sorting: the first
+level would be the artist name, the second one the album and finally the
+actual track.
+
+At first, cmus checks if the "filename" looks like an URL, if it does, the
+item is given the special artist and album name *<Stream>*.
+
+If it is a file, it is checked if the artist and album tags are set. If not,
+cmus assigns the special name *<No Name>* for the unset tag.
+
+As the first level, cmus sorts alphanumerically by the value of the artist
+tag. (<Stream> and <No Name> will be used as if they where normal names.)
+If a special sorting tag is available, it's value will be used instead.
+
+For album names, alphanumerical sorting is not the primary method, though.
+To decide, how the second level should be sorted, cmus looks at the date of
+the first track of each album. Sorting is done from young to old. Of course,
+if one artist happens to have more then one album from one year,
+alphanumerical sorting will be used after sorting by date.
+
+If the date header is not set, the album will be placed on top of the list (in
+fact, the internal integer value for unset album tags is -1).
+
+The method for third sorting level (the track) is very similar to album
+sorting. First two numerical values are checked (discnumber and tracknumber).
+If sorting is still ambiguous, sorting will be done alphanumerically by the
+value of the track's `filename` (not track name!).
+
+For simple albums, that is it. There is a special case, though. Albums, that
+feature various artists, also known as samplers or compilations.
+
+If a track belongs to a compilation is again decided by the existence and
+value of special tagging information. First, it is checked if cmus should use
+a special artist name (e.g.: `'Fatboy Slim'` for a DJ set). If so, that one
+will be used instead of the real artist name.
+
+If that special name tag is not set, cmus checks if another tag is
+set. If that is the case, the album will be given the special artist
+name *<Various Artists>*. Albums filed under *<Various Artists>* are sorted
+alphanumerically by album name instead of by date.
+
+That way, you do not end up with compilation tracks scattered around your
+library view.
+
+The problem with compilation tagging is, that there is no generic tag or
+method, that can be regarded as a standard across all different formats,
+supported by cmus.
+
+For mp3, the special-name tag would be the id3v2 *TPE2* frame. The
+mark-as-compilation tag is the *TCMP* frame (which is a user defined id3v2.3
+frame, used at least by amarok and apple's iTunes[tm]).
+
+For vorbis style tags (for example in ogg vorbis and flac files), the
+special-name tag is *ALBUMARTIST* and the mark-as-compilation tag is
+*COMPILATION*. Vorbis tags names are case insensitive.
+
+
+@h1 COMMANDS
+
+This section describes cmus' commands.  You can bind a key to any of these
+commands, put these commands to configuration files and execute them in
+command mode.  Also cmus-remote uses these commands in its protocol.
+
+Optional parameters are in brackets, obligatory parameters in angle brackets
+and default key bindings in parenthesis.
+
+add [-l] [-p] [-q] [-Q] <file|dir|url|playlist>
+       Add file/dir/url/playlist to the specified view or the current view.
+
+       @li -l
+       add to library
+
+       @li -p
+       add to playlist
+
+       @li -q
+       add play queue
+
+       @li -Q
+       prepend to play queue
+
+       URL is a Shoutcast stream (http://...) or a CDDA URL (cdda://...)
+       (see *PLAYING AUDIO DISCS*).
+
+       Supported playlist: plain, .m3u, .pls.
+
+bind [-f] <context> <key> <command>
+       Add a key binding.
+
+       @li -f
+       overwrite existing binding
+
+       Use tab to expand contexts, keys and commands.  Command is any command
+       listed in this section.
+
+       Valid key contexts
+               common, library (1-2), playlist (3), queue (4), browser (5),
+               filters (6)
+
+       There's one context for each view.  Common is a special context on
+       which bound keys work in every view.
+
+       You can override specific keys in common context for a view.  For
+       example *i* selects the current track in views 1-3 but in browser it
+       is overridden to toggle showing of hidden files.
+
+browser-up (*backspace*)
+       Change to parent directory in browser view (5). This command only
+       makes sense to be bound to the *browser* key context although it's
+       possible to use this even if browser view is not active.
+
+cd [directory]
+       Changes the current working directory.  Changes the directory
+       displayed in browser view too.
+
+clear [-l] [-p] [-q]
+       Remove all tracks from the specified view or the current view.
+
+       @li -l
+       clear library
+
+       @li -p
+       clear playlist
+
+       @li -q
+       clear play queue
+
+colorscheme <name>
+       Change color scheme.  Color schemes are found in `/usr/share/cmus/` or
+       `$XDG_CONFIG_HOME/cmus/` and have .theme filename extension.
+
+echo <arg>...
+       Display arguments on the command line.
+
+       If the arguments contain *{}* it is replaced with file name of the
+       first selected track.
+
+       NOTE: unlike with *run* the *{}* is replaced with only the first
+       selected filename.
+
+       Default bindings:
+
+               @pre
+               common  I   echo {}
+               common  ^C  echo Type :quit<enter> to exit cmus.
+               @endpre
+
+factivate <user-defined-filter>...
+       Select and activate the given user defined filters (displayed in the
+       filters view).  Filter names are separated by spaces.  This command is
+       mostly useful when bound to a key, to change active filters very
+       quickly.  If no arguments given then all filters are unactivated.
+
+       If you prefix a filter name with "!" then the filter value is negated
+       before activation.
+
+filter <filter-expression>
+       Use this command when you want to temporarily filter contents of the
+       library views without having separately define (fset) and activate the
+       filter.  The filter is not saved.
+
+fset <name>=<filter-expression>
+       Define (or replace existing) filter and add it to filters view (6).
+
+invert
+       Invert the marking of tracks in playlist and queue views. See *mark*
+       and *unmark*.
+
+live-filter <simple-filter-expression|short-filter-expression>
+       Use this command when you want to temporarily filter contents of the
+       library views without having separately define (fset) and activate the
+       filter.  The filter is not saved.
+
+load [-l] [-p] <playlist>
+       Load a playlist to the specified view or to the current view.
+
+       @li -l
+       load to library views
+
+       @li -p
+       load to playlist view
+
+lqueue [NUM]
+       Queue NUM (default 1) random albums from the library. See also
+       *tqueue*.
+
+mark <filter-expression>
+       Mark tracks in playlist and queue view by using a filter expression.
+
+pl-create <name>
+       Creates a new playlist.
+
+pl-export <filename>
+       Exports the currently selected playlist.
+
+pl-import [filename]
+       Imports a playlist into the playlist view. The argument can be omitted in
+       the browser view.
+
+pl-rename <name>
+       Renames the selected playlist.
+
+player-next (*b*)
+       Skip to the next track.
+
+player-pause (*c*)
+       Toggle pause.
+
+player-pause-playback
+       Pause if currently playing.
+
+player-play [filename] (*x*)
+       Play the given track, or, if none is specified, [re]play the current
+       track from the beginning.
+
+player-prev (*z*)
+       Skip to the previous track.
+
+player-stop (*v*)
+       Stop playback.
+
+prev-view
+       Go to previously used view.
+
+left-view
+       Go to view "to the left" of current one, e.g. view 4 -> view 3.
+
+right-view
+       Go to view "to the right" of current one, e.g. view 3 -> view 4.
+
+push <text>
+       Enter command mode with the command line pre-set to text. Example:
+
+               bind common w push filter artist=
+
+       Text can contain spaces and even trailing spaces will be honored.
+       This command can only be bound to a key but not used in the command
+       line directly.
+
+pwd
+       Prints the current working directory.
+
+quit [-i] (*q*, *:wq*)
+       Exit cmus.
+
+       @li -i
+       ask before exiting
+
+raise-vte
+       Raise the virtual terminal emulator window.  Works only in X session.
+
+rand
+       Randomizes (shuffles) the tracks in the library, playlist or queue view.
+
+refresh (*^L*)
+       Redraw the terminal window.
+
+run <command>
+       Run command for the marked tracks OR the selected one if none marked.
+
+       By default file names are appended to the command.  If the command
+       contains *{}* it is replaced with list of filenames.
+
+       NOTE: In view 1 you can run a command for all files in the selected
+       album or artist.
+
+save [-e]  [-l] [-L] [-p] [-q]  [file]  (*:w*)
+       Save the specified view's or the current view's contents to a playlist
+       file. In extended mode (-e), also save metadata.
+
+       @li -l
+       save library views
+
+       @li -L
+       save filtered library views
+
+       @li -p
+       save playlist view
+
+       @li -q
+       save queue view
+
+       If no filename given the old filename is used. "-" outputs to stdout
+       (works only remotely).
+
+search-next (*n*)
+       If a search pattern has been entered before, search forward for the
+       next match in the current view.  See *SEARCHING* above.
+
+search-prev (*N*)
+       If a search pattern has been entered before, search backwards for the
+       previous match in the current view.  See *SEARCHING* above.
+
+seek [+-](<num>[mh] | [HH:]MM:SS)
+       Seek to absolute or relative position.  Position can be given in
+       seconds, minutes (m), hours (h) or HH:MM:SS format where HH: is
+       optional.
+
+       Seek 1 minute backward
+               :seek -1m
+
+       Seek 5 seconds forward
+               :seek +5
+
+       Seek to absolute position 1h
+               :seek 1h
+
+       Seek 90 seconds forward
+               :seek +1:30
+
+       Default bindings:
+
+               @pre
+               common  ,      :seek -1m
+               common  .      :seek +1m
+               common  l      :seek +5
+               common  h      :seek -5
+               common  right  :seek +5
+               common  left   :seek -5
+               @endpre
+
+set <option>=<value>
+       Set value of an option. See *OPTIONS*.
+
+set <option>
+       Display option value.  Vim compatible *set <option>?* is also
+       supported.
+
+shell <command>
+       Execute a command via /bin/sh.
+
+showbind <context> <key>
+       Show key binding.
+
+shuffle
+       Reshuffle the shuffle lists for both library and playlist views.
+
+source <filename>
+       Read and execute commands from <filename>.
+
+toggle <option>
+       Toggle value of a toggle-able option (all booleans and tristate
+       *aaa_mode*).
+
+tqueue [NUM]
+       Queue NUM (default 1) random tracks from the library. See also
+       *lqueue*.
+
+unbind [-f] <context> <key>
+       Remove a key binding. Use tab to cycle through bound keys.
+
+       -f
+               Don't throw an error if the binding is not known
+
+unmark
+       Unmark all tracks (see *mark*).
+
+update-cache [-f]
+       Update track metadata cache ($XDG_CONFIG_HOME/cmus/cache). Only files
+       with changed modification time or removed files are considered.
+
+       -f
+               Update all files. Same as quit, rm -f $XDG_CONFIG_HOME/cmus/cache, start cmus.
+
+version
+       Print the version information.
+
+view <name or 1-7>
+       Switches active view.
+
+vol [+-]NUM[%] [[+-]NUM[%]]
+       Set, increase or decrease volume.
+
+       If you give *vol* just one argument it changes both channels.  Two
+       values make it possible to change the left and right channel
+       independently.
+
+       To increase or decrease volume prefix the value with *-* or *+*,
+       otherwise value is treated as absolute volume.
+
+       Both absolute and relative values can be given as percentage units
+       (suffixed with *%*) or as internal values (hardware may have volume in
+       range 0-31 for example).
+
+       Default bindings:
+
+               @pre
+               common  =  :vol +10%
+               common  +  :vol +10%
+               common  -  :vol -10%
+               common  [  :vol +1% +0%
+               common  ]  :vol +0% +1%
+               common  {  :vol -1% -0%
+               common  }  :vol -0% -1%
+               @endpre
+
+win-activate (*enter*)
+       In views 1-3 start playing the selected track.  In view 5 start
+       playing the selected track or change to the selected directory.  In
+       view 6 activate the selected filters.  In settings view (7) change
+       binding or variable.
+
+win-add-l (*a*)
+       Add the currently marked or selected track(s) (views 3-4), or the
+       currently selected file or directory (view 5) to the library.
+
+       Analogous to *:add -l*
+
+win-add-p (*y*)
+       Add the currently marked or selected track(s) (views 1-2, 4), or the
+       currently selected file or directory (view 5) to the marked playlist.
+
+       Analogous to *:add -p*
+
+win-add-Q (*E*)
+       Prepend the currently marked or selected track(s) (views 1-3), or the
+       currently selected file or directory (view 5) to the play queue.
+
+       Analogous to *:add -Q*
+
+win-add-q (*e*)
+       Add the currently marked or selected track(s) (views 1-3), or the
+       currently selected file or directory (view 5) to the play queue.
+
+       Analogous to *:add -q*
+
+win-bottom (*G*, *end*)
+       Goto bottom of the current window.
+
+win-down [NUM] (*j*, *down*)
+       Goto down NUM (default 1) rows in the current window.
+
+win-half-page-down (*^D*)
+       Goto down half a page in the current window.
+
+win-half-page-up (*^U*)
+       Goto up half a page in the current window.
+
+win-mv-after (*p*)
+       If no tracks are marked, move the selected track down one row. If any
+       tracks are marked, move the marked tracks after the currently selected
+       track.  This command works in unsorted playlist and play queue view.
+
+win-mv-before (*P*)
+       If no tracks are marked, move the selected track up one row. If any
+       tracks are marked, move the marked tracks before the currently
+       selected track.  This command works in unsorted playlist and play
+       queue view.
+
+win-next (*tab*)
+       Activate next window.  Only relevant in view 1.
+
+win-page-bottom
+       Goto the bottom of the visible part of the current window.
+
+win-page-down (*^F*, *page_down*)
+       Goto down one page in the current window.
+
+win-page-middle
+       Goto the middle of the visible part of the current window.
+
+win-page-top
+       Goto the top of the visible part of the current window.
+
+win-page-up (*^B*, *page_up*)
+       Go up one page in the current window.
+
+win-remove (*D*, *delete*)
+       Remove the selected entry.  For tracks no confirmations are made.  For
+       playlists (view 3), files (view 5), filters (view 6) and bindings (view 7)
+       user has to confirm the action.
+
+win-scroll-down (*^E*)
+       Scroll current window one row downwards.
+
+win-scroll-up (*^Y*)
+       Scroll current window one row upwards.
+
+win-sel-cur (*i*)
+       Select the current track (position in library or playlist, not
+       necessarily same as the currently playing track).  Works only in views
+       1-3, does nothing in other views.
+
+win-toggle (*space*)
+       Expand albums in library view (1), mark tracks in views 2-4, set the marked
+       playlist in view 3, toggle selection of a filter in view 6, toggle variable
+       value in view 7.
+
+win-top (*g*, *home*)
+       Goto top of the current window.
+
+win-up [NUM] (*k*, *up*)
+       Goto up NUM (default 1) rows in the current window.
+
+win-update (*u*)
+       Checks the modification time of the files in the library, and updates
+       metadata for changed files.  Removes non-existent files from the
+       library.
+
+       Reloads contents of directory in the browser view.
+
+       Only works in views 1-2 and 5, does nothing in other views.
+
+win-update-cache [-f]
+       Same as *update-cache*, but only for marked / selected tracks.
+       Only works in views 1-2, does nothing in other views.
+
+
+@h1 CONFIGURATION OPTIONS
+
+This section describes cmus' options that can be altered with the *set* and
+*toggle* commands. Default values are in parenthesis, possible values in
+brackets.
+
+auto_expand_albums_follow, auto_expand_albums_search, auto_expand_albums_selcur (true)
+       If enabled, cmus will always open an artist and select the album when
+       following the currently played track or performing actions such as "search"
+       or "go to current track". This option is tightly coupled to the
+       show_all_tracks option. Any "auto_expand_albums_\* = false" implies
+       "show_all_tracks = true".
+
+auto_reshuffle (true)
+       Reshuffle playlist when end of shuffle list is reached.
+
+aaa_mode (all) [all, artist, album]
+       Defines what tracks should be played in the library view.  Not used in
+       the other views. For example if set to *artist* the player behaves
+       like there were only the files of the currently playing artist in the
+       library.
+
+altformat_current [`Format String`]
+       Alternative format string for the line displaying currently playing
+       track.
+
+       NOTE: if empty, *format_current* is used instead.
+
+altformat_playlist [`Format String`]
+       Alternative format string for the list views (2-4).
+
+       NOTE: if empty, *format_playlist* is used instead.
+
+altformat_title [`Format String`]
+       Alternative format string for terminal title.
+
+       NOTE: not all terminals support changing window title.
+
+       NOTE: if empty, *format_title* is used instead.
+
+altformat_trackwin [`Format String`]
+       Alternative format string for the tree view's (1) track window.
+
+       NOTE: if empty, *format_trackwin* is used instead.
+
+buffer_seconds (10) [1-300]
+       Size of player buffer in seconds.
+
+color_cmdline_bg (default) [`Color`]
+       Command line background color.
+
+color_cmdline_fg (default) [`Color`]
+       Command line foreground color.
+
+color_cmdline_attr (default) [`Attributes`]
+       Command line attributes.
+
+color_error (lightred) [`Color`]
+       Color of error messages displayed on the command line.
+
+color_info (lightyellow) [`Color`]
+       Color of informational messages displayed on the command line.
+
+color_separator (blue) [`Color`]
+       Color of the separator line between windows in view (1).
+
+color_statusline_bg (gray) [`Color`]
+       Status line background color.
+
+color_statusline_fg (black) [`Color`]
+       Status line foreground color.
+
+color_statusline_attr (default) [`Attributes`]
+       Status line attributes.
+
+color_titleline_bg (blue) [`Color`]
+       Background color of the line displaying currently playing track.
+
+color_titleline_fg (white) [`Color`]
+       Foreground color of the line displaying currently playing track.
+
+color_titleline_attr (default) [`Attributes`]
+       Attributes of the line displaying currently playing track.
+
+color_win_bg (default) [`Color`]
+       Window background color.
+
+color_win_cur (lightyellow) [`Color`]
+       Color of currently playing track.
+
+color_win_cur_attr (default) [`Attributes`]
+       Currently playing track attributes.
+
+color_win_cur_sel_bg (blue) [`Color`]
+       Background color of the selected row which is also the currently
+       playing track in active window.
+
+color_win_cur_sel_fg (lightyellow) [`Color`]
+       Foreground color of the selected row which is also the currently
+       playing track in active window.
+
+color_win_cur_sel_attr (default) [`Attributes`]
+       Attributes of the selected row which is also the currently
+       playing track in active window.
+
+color_win_dir (lightblue) [`Color`]
+       Color of directories in browser.
+
+color_win_fg (default) [`Color`]
+       Window foreground color.
+
+color_win_attr (default) [`Attributes`]
+       Window attributes.
+
+color_win_inactive_cur_sel_bg (gray) [`Color`]
+       Background color of the selected row which is also the currently
+       playing track in inactive window.
+
+color_win_inactive_cur_sel_fg (lightyellow) [`Color`]
+       Foreground color of the selected row which is also the currently
+       playing track in inactive window.
+
+color_win_inactive_cur_sel_attr (default) [`Attributes`]
+       Attributes of the selected row which is also the currently
+       playing track in inactive window.
+
+color_win_inactive_sel_bg (gray) [`Color`]
+       Background color of selected row in inactive window.
+
+color_win_inactive_sel_fg (black) [`Color`]
+       Foreground color of selected row in inactive window.
+
+color_win_inactive_sel_attr (default) [`Attributes`]
+       Attributes of selected row in inactive window.
+
+color_win_sel_bg (blue) [`Color`]
+       Background color of selected row in active window.
+
+color_win_sel_fg (white) [`Color`]
+       Foreground color of selected row in active window.
+
+color_win_sel_attr (default) [`Attributes`]
+       Attributes of selected row in active window.
+
+color_win_title_bg (blue) [`Color`]
+       Background color of window titles (topmost line of the screen).
+
+color_win_title_fg (white) [`Color`]
+       Foreground color of window titles (topmost line of the screen).
+
+color_win_title_attr (default) [`Attributes`]
+       Attributes of window titles (topmost line of the screen).
+
+color_trackwin_album_bg (default) [`Color`]
+       Background color of the album row shown in the track window.
+
+color_trackwin_album_fg (default) [`Color`]
+       Foreground color of the album row shown in the track window.
+
+color_trackwin_album_attr (bold) [`Attributes`]
+       Attributes of the album row shown in the track window.
+
+confirm_run (true)
+       Ask for confirmation before executing *:run*
+
+continue (true)
+       Continue playing after current track finishes.
+
+device (/dev/cdrom)
+       CDDA device file.
+
+display_artist_sort_name (false)
+       If enabled, always displays artist names used for sorting instead of
+       regular ones in tree view (e.g. "Artist, The" instead of "The Artist"),
+       so that artists column looks alphabetically sorted.
+
+follow (false)
+       If enabled, always select the currently playing track on track change.
+
+format_current [`Format String`]
+       Format string for the line displaying currently playing track.
+
+format_playlist [`Format String`]
+       Format string for the list views (2-4).
+
+format_playlist_va [`Format String`]
+       Format string for the list views (2-4), if a track is assumed to be a
+       part of compilation (see `LIBRARY VIEW SORTING` for details).
+
+       NOTE: if empty, *format_playlist* is used instead.
+
+format_statusline [`Format String`]
+       Format string for status line.
+
+format_title [`Format String`]
+       Format string for terminal title.
+
+       NOTE: not all terminals support changing window title.
+
+format_trackwin [`Format String`]
+       Format string for the tree view's (1) track window.
+
+format_trackwin_album [`Format String`]
+       Format string for albums in tree view's (1) track window.
+
+format_trackwin_va [`Format String`]
+       Format string for the tree view's (1) track window, if a track
+       is assumed to be a part of compilation (see `LIBRARY VIEW SORTING`
+       for details).
+
+       NOTE: if empty, *format_trackwin* is used instead.
+
+format_treewin [`Format String`]
+       Format string for the tree view's (1) tree window.
+
+format_treewin_artist [`Format String`]
+       Format string for artists in tree view's (1) tree window.
+
+smart_artist_sort (true)
+       If enabled, makes tree view sorting ignore "The" in front of artist
+       names, preventing artists starting with "The" from clumping together.
+       Real `artistsort` tags override this option, if present.
+
+id3_default_charset (ISO-8859-1)
+       Default character set to use for ID3v1 and broken ID3v2 tags.
+
+       NOTE:  This is used only if the tag is not valid UTF-8.
+
+icecast_default_charset (ISO-8859-1)
+       Default character set to use for non-UTF-8 icecast stream metadata.
+
+       NOTE:  This is used only if the metadata is not valid UTF-8.
+
+lib_add_filter (`Filter`)
+       Apply filter when adding files to the library. See *FILTERS*.
+
+lib_sort (artist album discnumber tracknumber title filename) [`Sort Keys`]
+       Sort keys for the sorted library view (2).
+
+mouse (false)
+       Enable mouse support.
+
+       NOTE: Mouse wheel scrolling can lag if cmus is compiled with
+       old version of Ncurses.
+
+mpris (true)
+       Enable MPRIS support. See *D-Bus Interface (MPRIS)*
+
+       NOTE: This flag has no effect if cmus was compiled without MPRIS support.
+
+output_plugin [roar, pulse, alsa, arts, oss, sndio, sun, coreaudio]
+       Name of output plugin.
+
+pl_sort () [`Sort Keys`]
+       Sort keys for the playlist view (3).  Empty value disables sorting and
+       enables manually moving tracks.
+
+play_library (true)
+       Play tracks from the library instead of playlist.
+
+play_sorted (false)
+       Play tracks from the library in the sorted view (2) order instead of
+       tree view (1) order.  Used only when play_library is true.
+
+repeat (false)
+       Repeat after all tracks played.
+
+repeat_current (false)
+       Repeat current track forever.
+
+replaygain (disabled)
+       Enable Replay Gain. Default is "disabled".
+       Set to "track", "album", "track-preferred" or "album-preferred".
+
+replaygain_limit (true)
+       Use replay gain limiting when clipping.
+
+replaygain_preamp (0.0)
+       Replay gain preamplification in decibels.
+
+resume (false)
+       Resume playback on startup.
+
+rewind_offset (5) [-1-9999]
+       If the position of the current track is smaller than rewind_offset,
+       player_prev jumps to the previous track. Otherwise, player_prev jumps
+       to the beginning of the current track. If rewind_offset=-1, player_prev
+       always jumps to the previous track.
+
+scroll_offset (2) [0-9999]
+       Minimal number of screen lines to keep above and below the cursor.
+
+show_all_tracks (true)
+       Display all tracks of the artist when the artist is selected in the tree
+       view. This option is tightly coupled to the auto_expand_albums_\* options.
+       "show_all_tracks = false" implies "auto_expand_albums_\* = true".
+
+show_hidden (false)
+       Display hidden files in browser.
+
+show_current_bitrate (false)
+       Display current bitrate in the status lines.
+
+show_playback_position (true)
+       Display elapsed (or remaining) time in the status line. Can be disabled
+       to e.g. not trigger tmux's activity monitor.
+
+show_remaining_time (false)
+       Display remaining time instead of elapsed time.
+
+shuffle (false)
+       Play in shuffled order.  Shuffle works in the library views (1-2) and
+       playlist view (3).
+
+skip_track_info (false)
+       Don't load metadata when adding tracks. Useful when using network file system
+       and having huge amount of files.
+       Tags can be loaded using 'update-cache' or 'win-update-cache' commands.
+
+softvol (false)
+       Use software volume control.
+
+       NOTE: You should probably set this to false when using *ao* as
+       *output_plugin* to output to wav files.
+
+softvol_state (100 100)
+       Used to save left and right channel values for software volume
+       control.  Two integers in range 0..100 separated by a space.  This
+       option is not usually changed directly since *vol* command does same
+       thing if *softvol* is true.
+
+start_view (tree) [tree, sorted, playlist, queue, browser, filters, settings]
+       Specify the view that gets shown when cmus starts.
+
+status_display_program () [command]
+       This command, if not empty, is run every time cmus' status changes.
+       It can be used to display currently playing track on desktop
+       background or panel for example.  See
+       `/usr/share/doc/cmus/examples/cmus-status-display`.
+
+time_show_leading_zero (true)
+       Pad durations of less than 10 minutes with a leading 0
+
+wrap_search (true)
+       Controls whether the search wraps around the end.
+
+@h2 Colors
+
+Color is integer in range -1..255.
+
+The following color names are recognized:
+
+Terminal's default color, -1
+       default
+
+Fg & bg, 0..7
+       black, red, green, yellow, blue, magenta, cyan, gray
+
+Fg, 8..15
+       darkgray, lightred, lightgreen, lightyellow, lightblue, lightmagenta,
+       lightcyan, white
+
+@h2 Attributes
+
+Attributes is a set of names "standout|bold":
+
+`default` does nothing, if you put it with other attributes
+the other attributes will be used.
+
+`standout` makes the text standout.
+
+`bold` makes the text bold.
+
+`reverse` reverses the text colors.
+
+`underline` underlines the text.
+
+`blink` makes the text blink.
+
+@h2 Format Strings
+
+Format strings control display of tracks in library, playlist and play queue
+views.
+
+NOTE: *altformat_\** options are used when there are no tags available.
+
+Special Keys:
+
+       %a  %{artist}           @br
+       %A  %{albumartist}      @br
+       %l  %{album}            @br
+       %D  %{discnumber}       @br
+       %n  %{tracknumber}      @br
+       %X  %{play_count}       @br
+       %t  %{title}            @br
+       %g  %{genre}            @br
+       %c  %{comment}          @br
+       %y  %{date}             @br
+       %d  %{duration}         @br
+       %f  %{path}             @br
+       %F  %{filename}         @br
+           %{albumduration}    @br
+           %{originaldate}     @br
+           %{maxdate}          @br
+           %{bpm}              @br
+           %{bitrate}          @br
+           %{codec}            @br
+           %{codec_profile}    @br
+           %{rg_track_gain}    @br
+           %{rg_track_peak}    @br
+           %{rg_album_gain}    @br
+           %{rg_album_peak}    @br
+           %{arranger}         @br
+           %{composer}         @br
+           %{conductor}        @br
+           %{lyricist}         @br
+           %{performer}        @br
+           %{remixer}          @br
+           %{label}            @br
+           %{publisher}        @br
+           %{work}             @br
+           %{opus}             @br
+           %{partnumber}       @br
+           %{part}             @br
+           %{subtitle}         @br
+           %{media}            @br
+       %=
+               start align right (use at most once)
+       %%
+               literal *%*
+       %?
+               literal *?*
+
+You can use printf style formatting (width, alignment, padding). As an
+extension, the width can have a %-suffix, to specify a percentage of the
+terminal width. @br
+To see current value of an option type *:set option=<TAB>*.
+
+Note: With %{bitrate}, you'll have to append the unit yourself, as mentioned
+in the example below.
+
+You can use conditional operator *%{?CONDITION?A[?B]}*.
+CONDITION has the same syntax as `filters`, except for unsupported short and
+simple expressions and supported keys comparison (e.g. artist=albumartist).
+Its keys are:
+       format strings' special keys @br
+       configuration options @br
+       keyword *stream* [boolean] (returns true if track is a stream) @br
+       keyword *va* [boolean] (returns true if track's album is compilation)
+Else part can be skipped. A and B can contain string literals in *"* or *'*.
+A and B can be empty.
+
+Examples:
+
+       @pre
+       :set format_trackwin= %02n. %t %{?y?(%y)}%= %d
+       :set format_current= %n. %-30t %40F (%y)%= %d
+       :set format_current= %a - %l - %02n. %t%= %{bitrate}Kbps %g %y
+       :set format_playlist= %f%= %6{rg_track_gain} dB  %8{rg_track_peak}
+       :set format_playlist= %-25%a %-15%l %3n. %t%= %y %d
+       @endpre
+
+@h2 Sort Keys
+
+Sort option (lib_sort, pl_sort) value is space separated list of the following
+sort keys:
+
+       artist, album, title, tracknumber, play_count, discnumber, date,
+       originaldate, genre, comment, albumartist, filename, filemtime, bpm,
+       bitrate, codec, media, codec_profile, rg_track_gain, rg_track_peak,
+       rg_album_gain, rg_album_peak
+
+Note: Adding a '-' in front of the key will sort in reverse order
+
+
+@h1 PLUGIN OPTIONS
+
+dsp.alsa.device
+       PCM device for ALSA plugin, usually "default".
+
+mixer.alsa.channel
+       Mixer channel for ALSA Plugin, usually "pcm", "master" or "headphone".
+       To see all possible values run "alsamixer" or "amixer".
+
+mixer.alsa.device
+       Mixer device for ALSA plugin, usually "default".
+
+mixer.pulse.restore_volume
+       Restore the volume at startup using PulseAudio. Otherwise, cmus sets
+       the volume to 100%, which does not mix well with "flat volumes"
+       feature of PA. Defaults to "1"; set to "0" to turn off.
+
+dsp.ao.buffer_size
+       Buffer size, default is 16kB (but you may want to try bigger values if
+       you experience buffer under-runs).
+
+dsp.ao.device_interface
+       Device interface for libao plugin to request a specific playback
+       device/sink/output.  This name will be in a format determined by the
+       specific driver backend.
+
+dsp.ao.driver
+       Output driver for libao plugin.  Example values: "alsa09", "esd",
+       "irix", "oss", "sun", "aixs", "wav".
+
+       NOTE: of the file output drivers only "wav" is supported.
+
+dsp.ao.wav_counter
+       Counter used for making filename.  Used only if *dsp.ao.driver* is
+       "wav".  For example if this is 1 and *dsp.ao.wav_dir* is "/home/user"
+       then PCM data is outputted to "/home/user/01.wav".  This counter is
+       incremented every time playback is stopped.
+
+       NOTE: you probably want to set *continue* to *false* (press *C*),
+       otherwise playback is not stopped between tracks and all PCM data is
+       outputted to one wav file (useful if you want to join files).  Also
+       unsetting shuffle and repeat might be good idea.
+
+dsp.ao.wav_dir
+       Output directory for libao plugin, default is your home directory.
+       Used only if *dsp.ao.driver* is "wav".
+
+dsp.coreaudio.device
+       Device for Core Audio output. Leave empty for default.
+
+dsp.coreaudio.enable_hog_mode
+       Set true to set the device to hog mode. The default value is false.
+
+dsp.coreaudio.sync_sample_rate
+       Synchonize the device sample rate with the player, so no interpolation
+       will be applied to the stream.
+
+dsp.jack.server_name
+       Connect to jackd with this name. Leave empty for default.
+
+dsp.jack.resampling_quality
+       Sets the quality of the re-sampling. 0 is low quality but fast, 1 is
+       medium quality, 2 (default) is high quality but more CPU intensiv. This
+       option is only available if cmus was compiled with libsamplerate
+       support.
+
+input.cdio.cddb_url
+       CDDB URL (default: freedb.freedb.org:8880). Use HTTP protocol if prefixed
+       with "http://" (e.g.: http://freedb.musicbrainz.org:80/~cddb/cddb.cgi).
+       Set to an empty string to disable CDDB lookup completely.
+
+input.\*.priority
+       Sets the priority of the input plugin. If multiple plugins can play a
+       file, the plugin with the higher priority is chosen. If the priority is
+       0, the plugin is disabled.
+
+dsp.oss.device
+       PCM device for OSS plugin, usually /dev/dsp.
+
+mixer.oss.channel
+       Mixer channel for OSS Plugin, "pcm" or "master".
+
+mixer.oss.device
+       Mixer device for OSS plugin, usually /dev/mixer.
+
+dsp.roar.server
+       Address of RoarAudio server. Defaults to internal defaults.
+       Can be UNIX, TCP/IP or DECnet address.
+
+dsp.roar.role [music, background_music, ...]
+       Role for stream. May be used by the server to apply additional
+       defaults.
+
+dsp.sun.device
+       PCM device for Sun plugin, usually /dev/audio.
+
+mixer.sun.channel
+       Mixer channel for Sun Plugin, usually "master".
+
+mixer.sun.device
+       Mixer device for Sun plugin, usually /dev/mixer.
+
+@h1 PLAYING AUDIO DISCS
+
+With the cdio input plugin enabled, it is possible to play Audio CDs and CD
+images. Just set the *device* option to either a device file (e.g. /dev/cdrom)
+or an image file (e.g. ~/cd.cue). Then add a new track using the CDDA URL
+scheme, e.g.:
+
+       :add cdda://2
+
+To add the whole disc, use cdda:// (without track number). This is currently
+only working for audio discs, not images. Adding track ranges is also possible
+(cdda://1-3). To add images without changing the device option, it is possible
+to include the image path in the URL, e.g.:
+
+       :add cdda:///path/to/cd.cue/2-5
+
+The metadata will be read from CD-Text, and if not available, looked up from
+a CDDB server (see *input.cdio.cddb_url*).
+
+
+@h1 FILTERS
+
+Filters are used mostly for filtering contents of library views (1 & 2).
+Filters do not change the actual library content, i.e. *:save* command will
+still save all tracks to playlist file whether they are visible or not.
+
+@h2 Types
+
+There are three types of filter expressions, each offering more
+expressiveness:
+
+       @li *simple*
+       `e.g.` beatles
+
+       @li *short*
+       `e.g.` ~a beatles (!~y1960-1965 | ~d>600)
+
+       @li *long*
+       `e.g.` artist="\*beatles\*"&album="R\*"
+
+Simple expressions are only available using *live-filter*. For other
+filter commands the type is auto-detected, so both short and long
+expressions can be used.
+
+Long expressions are lists of built-in filters or user defined filters
+separated with *&* (and) or *|* (or).  Parenthesis can be used group
+subexpressions and *!* negates result of the expression following it.
+Same is true for short expressions, but they can only be made of
+built-in filters. Also (and)-grouping is done implicitly.
+
+@h2 Strings
+
+@li long
+*filename*, *artist*, *albumartist*, *album*, *title*, *genre*, *comment*,
+*codec*, *codec_profile*, *media*
+@br
+Comparators: *=* and *!=* (not equal)
+
+@li short
+*~f*, *~a*, *~A*, *~l*, *~t*, *~g*, *~c*
+@br
+Comparators: none
+
+@h2 Integers
+
+@li long
+*discnumber*, *tracknumber*, *date* (year), *originaldate* (year), *duration*
+(seconds), *bitrate*
+@br
+Comparators: *<*, *<=*, *=*, *>=*, *>*, *!=*
+
+@li short
+*~D*, *~n*, *~y*, *~d*
+@br
+Comparators: *<*, *>*
+@br
+Ranges: *a-b* (>=a&<=b), *-b* (<=b), *a-* (>=a)
+
+@h2 Booleans
+
+*tag* (true if track has tags), *stream* (true if track is a stream)
+@br
+For short expressions: *~T* and *~s*
+
+@h2 Defining Filters
+
+Filters can be defined with *fset* command.  User defined filters appear in
+the filters view (6).
+
+Create a new filter which name is *ogg* and value *filename="\*.ogg"*
+       :fset ogg=filename="\*.ogg"
+
+Filter ogg and mp3 files from the 90s. Note the use of user defined filter
+*ogg*
+       :fset 90s-ogg-mp3=date>=1990&date<2000&(ogg|filename="\*.mp3")
+
+@h2 Activating Filters
+
+*factivate* changes visible contents of the library (views 1-2).
+
+Activate user defined filters *ogg* and *missing-tags*
+       :factivate ogg missing-tags
+
+Like above but negate value of *ogg* filter.
+       :factivate !ogg missing-tags
+
+Alternatively you can select the filters by pressing *space* in view 6 and
+then activate the selected filters by pressing *enter*.
+
+@h2 Throw-away Filters
+
+*live-filter* and *filter* commands are useful when you want to use a filter only
+once and not save it.  It changes visible contents of the library (views 1-2).
+*filter* unactivates all filters in the filters view, while *live-filter* is
+applied in addition to all currently activated filters.
+
+Filter all rock (anything with *rock* in genre tag) music from 80s-
+       :filter date>=1980&genre="\*rock\*"
+       @br
+       :filter ~y1980-~grock
+
+Filter all artists/albums/titles containing "sleepwalking"
+       :live-filter sleepwalking
+
+@h2 Selecting Tracks Matching a Filter
+
+Mark (select) all tracks with duration less than 2 minutes
+       :mark duration<120
+
+Mark (select) all tracks which have been played at least once
+       :mark play_count>=1
+
+These commands work in views 2-4.
+
+
+@h1 FILES
+
+cmus reads its configuration from 3 different places.
+
+`$XDG_CONFIG_HOME/cmus/autosave`
+       This is the first file cmus loads.  cmus saves its state on exit to
+       this file so you shouldn't edit it.
+
+`/usr/share/cmus/rc`
+       If the autosave file didn't exist, this file is read instead.
+
+`$XDG_CONFIG_HOME/cmus/rc`
+       Static config file. This file is read immediately after the autosave
+       file, and is never modified by cmus.  You can override auto-saved
+       settings in this file.  This file is not limited to options, it can
+       contain other commands too.
+
+@h2 Color Schemes
+
+There are some color schemes (\*.theme) in `/usr/share/cmus`.  You can switch
+them using the *:colorscheme* command.  You can put your own color schemes to
+$XDG_CONFIG_HOME/cmus.
+
+@h2 Examples
+
+Example status display script (See *status_display_program* option) can be
+found in `/usr/share/doc/cmus/examples`.
+
+
+@h1 ENVIRONMENT
+
+CMUS_CHARSET
+       Override cmus character set (default: \`locale charmap\`).
+
+CMUS_HOME
+       Override cmus config directory (default: $XDG_CONFIG_HOME/cmus).
+
+CMUS_SOCKET
+       Override cmus socket path (default: $XDG_RUNTIME_DIR/cmus-socket).
+
+HOME
+       Full path of the user's home directory.
+
+http_proxy
+       URI of the proxy to use for HTTP requests.
+
+USER
+       Name of the user running cmus.
+
+USERNAME
+       Fallback for *USER*.
+
+@h1 D-Bus Interface (MPRIS)
+
+cmus provides a D-Bus interface following the Media Player Remote Interfacing Specification (MPRIS) v2:
+       https://www.freedesktop.org/wiki/Specifications/mpris-spec/
+
+It exposes the `/org/mpris/MediaPlayer2` object path with the interfaces
+`org.mpris.MediaPlayer2` and `org.mpris.MediaPlayer2.Player`.
+The unique bus name is `org.mpris.MediaPlayer2.cmus`.
+
+Metadata fields follow the naming convention of the specification:
+       https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/
+Additionally, the `cmus:stream_title` field is exposed if appropriate.
+
+@h1 BUGS
+
+After a crash last lines of `~/cmus-debug.txt` might
+contain useful information.  The file exists only if you configured cmus with
+maximum debug level (*./configure DEBUG=2*).
+
+Feature requests and bug reports should go to the github issue tracker:
+       https://github.com/cmus/cmus/issues
+
+
+@h1 SEE ALSO
+
+*cmus-tutorial*(7), *cmus-remote*(1)
+
+
+@h1 AUTHORS
+
+cmus was mainly written by Timo Hirvonen <tihirvon\@gmail.com>.  Other
+contributers are listed in the `AUTHORS` file.
+
+This man page was written by Frank Terbeck <ft\@bewatermyfriend.org>,
+Timo Hirvonen <tihirvon\@gmail.com>, and Clay Barnes <clay.barnes\@gmail.com>.
diff --git a/Doc/ttman.c b/Doc/ttman.c
new file mode 100644 (file)
index 0000000..0de643f
--- /dev/null
@@ -0,0 +1,883 @@
+/*
+ * ttman - text to man converter
+ *
+ * Copyright 2006 Timo Hirvonen <tihirvon@gmail.com>
+ *
+ * This file is licensed under the GPLv2.
+ */
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+struct token {
+       struct token *next;
+       struct token *prev;
+       enum {
+               TOK_TEXT,       // max one line w/o \n
+               TOK_NL,         // \n
+               TOK_ITALIC,     // `
+               TOK_BOLD,       // *
+               TOK_INDENT,     // \t
+
+               // keywords (@...)
+               TOK_H1,
+               TOK_H2,
+               TOK_LI,
+               TOK_BR,
+               TOK_PRE,
+               TOK_ENDPRE,     // must be after TOK_PRE
+               TOK_RAW,
+               TOK_ENDRAW,     // must be after TOK_RAW
+               TOK_TITLE,      // WRITE 2 2001-12-13 "Linux 2.0.32" "Linux Programmer's Manual"
+       } type;
+       int line;
+
+       // not NUL-terminated
+       const char *text;
+       // length of text
+       int len;
+};
+
+static const char *program;
+static const char *filename;
+static char tmp_file[1024];
+static FILE *outfile;
+static int cur_line = 1;
+static struct token head = { &head, &head, TOK_TEXT, 0, NULL, 0 };
+
+#define CONST_STR(str) { str, sizeof(str) - 1 }
+static const struct {
+       const char *str;
+       int len;
+} token_names[] = {
+       CONST_STR("text"),
+       CONST_STR("nl"),
+       CONST_STR("italic"),
+       CONST_STR("bold"),
+       CONST_STR("indent"),
+
+       // keywords
+       CONST_STR("h1"),
+       CONST_STR("h2"),
+       CONST_STR("li"),
+       CONST_STR("br"),
+       CONST_STR("pre"),
+       CONST_STR("endpre"),
+       CONST_STR("raw"),
+       CONST_STR("endraw"),
+       CONST_STR("title")
+};
+#define NR_TOKEN_NAMES (sizeof(token_names) / sizeof(token_names[0]))
+#define BUG() die("BUG in %s\n", __FUNCTION__)
+
+#ifdef __GNUC__
+#define CMUS_NORETURN  __attribute__((__noreturn__))
+#else
+#define CMUS_NORETURN
+#endif
+
+static CMUS_NORETURN void quit(void)
+{
+       if (tmp_file[0])
+               unlink(tmp_file);
+       exit(1);
+}
+
+static CMUS_NORETURN void die(const char *format, ...)
+{
+       va_list ap;
+
+       fprintf(stderr, "%s: ", program);
+       va_start(ap, format);
+       vfprintf(stderr, format, ap);
+       va_end(ap);
+       quit();
+}
+
+static CMUS_NORETURN void syntax(int line, const char *format, ...)
+{
+       va_list ap;
+
+       fprintf(stderr, "%s:%d: error: ", filename, line);
+       va_start(ap, format);
+       vfprintf(stderr, format, ap);
+       va_end(ap);
+       quit();
+}
+
+static inline const char *keyword_name(int type)
+{
+       if (type < TOK_H1 || type > TOK_TITLE)
+               die("BUG: no keyword name for type %d\n", type);
+       return token_names[type].str;
+}
+
+static void *xmalloc(size_t size)
+{
+       void *ret = malloc(size);
+
+       if (!ret)
+               die("OOM when allocating %ul bytes\n", size);
+       return ret;
+}
+
+static char *memdup(const char *str, int len)
+{
+       char *s = xmalloc(len + 1);
+       memcpy(s, str, len);
+       s[len] = 0;
+       return s;
+}
+
+static struct token *new_token(int type)
+{
+       struct token *tok = xmalloc(sizeof(struct token));
+
+       tok->prev = NULL;
+       tok->next = NULL;
+       tok->type = type;
+       tok->line = cur_line;
+       return tok;
+}
+
+static void free_token(struct token *tok)
+{
+       struct token *prev = tok->prev;
+       struct token *next = tok->next;
+
+       if (tok == &head)
+               BUG();
+
+       prev->next = next;
+       next->prev = prev;
+       free(tok);
+}
+
+static void emit_token(struct token *tok)
+{
+       tok->prev = head.prev;
+       tok->next = &head;
+       head.prev->next = tok;
+       head.prev = tok;
+}
+
+static void emit(int type)
+{
+       struct token *tok = new_token(type);
+       tok->len = 0;
+       tok->text = NULL;
+       emit_token(tok);
+}
+
+static int emit_keyword(const char *buf, int size)
+{
+       int i, len;
+
+       for (len = 0; len < size; len++) {
+               if (!isalnum((unsigned char)buf[len]))
+                       break;
+       }
+
+       if (!len)
+               syntax(cur_line, "keyword expected\n");
+
+       for (i = TOK_H1; i < NR_TOKEN_NAMES; i++) {
+               if (len != token_names[i].len)
+                       continue;
+               if (!strncmp(buf, token_names[i].str, len)) {
+                       emit(i);
+                       return len;
+               }
+       }
+       syntax(cur_line, "invalid keyword '@%s'\n", memdup(buf, len));
+}
+
+static int emit_text(const char *buf, int size)
+{
+       struct token *tok;
+       int i;
+
+       for (i = 0; i < size; i++) {
+               int c = buf[i];
+               if (c == '@' || c == '`' || c == '*' || c == '\n' || c == '\\' || c == '\t')
+                       break;
+       }
+       tok = new_token(TOK_TEXT);
+       tok->text = buf;
+       tok->len = i;
+       emit_token(tok);
+       return i;
+}
+
+static void tokenize(const char *buf, int size)
+{
+       int pos = 0;
+
+       while (pos < size) {
+               struct token *tok;
+               int ch;
+
+               ch = buf[pos++];
+               switch (ch) {
+               case '@':
+                       pos += emit_keyword(buf + pos, size - pos);
+                       break;
+               case '`':
+                       emit(TOK_ITALIC);
+                       break;
+               case '*':
+                       emit(TOK_BOLD);
+                       break;
+               case '\n':
+                       emit(TOK_NL);
+                       cur_line++;
+                       break;
+               case '\t':
+                       emit(TOK_INDENT);
+                       break;
+               case '\\':
+                       tok = new_token(TOK_TEXT);
+                       tok->text = buf + pos;
+                       tok->len = 1;
+                       pos++;
+                       if (pos == size || buf[pos] == '\n') {
+                               // just one '\\'
+                               tok->text--;
+                       }
+
+                       if (tok->text[0] == '\\') {
+                               tok->text = "\\\\";
+                               tok->len = 2;
+                       }
+
+                       emit_token(tok);
+                       break;
+               default:
+                       pos--;
+                       pos += emit_text(buf + pos, size - pos);
+                       break;
+               }
+       }
+}
+
+static int is_empty_line(const struct token *tok)
+{
+       while (tok != &head) {
+               int i;
+
+               switch (tok->type) {
+               case TOK_TEXT:
+                       for (i = 0; i < tok->len; i++) {
+                               if (tok->text[i] != ' ')
+                                       return 0;
+                       }
+                       break;
+               case TOK_INDENT:
+                       break;
+               case TOK_NL:
+                       return 1;
+               default:
+                       return 0;
+               }
+               tok = tok->next;
+       }
+       return 1;
+}
+
+static struct token *remove_line(struct token *tok)
+{
+       while (tok != &head) {
+               struct token *next = tok->next;
+               int type = tok->type;
+
+               free_token(tok);
+               tok = next;
+               if (type == TOK_NL)
+                       break;
+       }
+       return tok;
+}
+
+static struct token *skip_after(struct token *tok, int type)
+{
+       struct token *save = tok;
+
+       while (tok != &head) {
+               if (tok->type == type) {
+                       tok = tok->next;
+                       if (tok->type != TOK_NL)
+                               syntax(tok->line, "newline expected after @%s\n",
+                                               keyword_name(type));
+                       return tok->next;
+               }
+               if (tok->type >= TOK_H1)
+                       syntax(tok->line, "keywords not allowed betweed @%s and @%s\n",
+                                       keyword_name(type-1), keyword_name(type));
+               tok = tok->next;
+       }
+       syntax(save->prev->line, "missing @%s\n", keyword_name(type));
+}
+
+static struct token *get_next_line(struct token *tok)
+{
+       while (tok != &head) {
+               int type = tok->type;
+
+               tok = tok->next;
+               if (type == TOK_NL)
+                       break;
+       }
+       return tok;
+}
+
+static struct token *get_indent(struct token *tok, int *ip)
+{
+       int i = 0;
+
+       while (tok != &head && tok->type == TOK_INDENT) {
+               tok = tok->next;
+               i++;
+       }
+       *ip = i;
+       return tok;
+}
+
+// line must be non-empty
+static struct token *check_line(struct token *tok, int *ip)
+{
+       struct token *start;
+       int tok_type;
+
+       start = tok = get_indent(tok, ip);
+
+       tok_type = tok->type;
+       switch (tok_type) {
+       case TOK_TEXT:
+       case TOK_BOLD:
+       case TOK_ITALIC:
+       case TOK_BR:
+               tok = tok->next;
+               while (tok != &head) {
+                       switch (tok->type) {
+                       case TOK_TEXT:
+                       case TOK_BOLD:
+                       case TOK_ITALIC:
+                       case TOK_BR:
+                       case TOK_INDENT:
+                               break;
+                       case TOK_NL:
+                               return start;
+                       default:
+                               syntax(tok->line, "@%s not allowed inside paragraph\n",
+                                               keyword_name(tok->type));
+                       }
+                       tok = tok->next;
+               }
+               break;
+       case TOK_H1:
+       case TOK_H2:
+       case TOK_TITLE:
+               if (*ip)
+                       goto indentation;
+
+               // check arguments
+               tok = tok->next;
+               while (tok != &head) {
+                       switch (tok->type) {
+                       case TOK_TEXT:
+                       case TOK_INDENT:
+                               break;
+                       case TOK_NL:
+                               return start;
+                       default:
+                               syntax(tok->line, "@%s can contain only text\n",
+                                               keyword_name(tok_type));
+                       }
+                       tok = tok->next;
+               }
+               break;
+       case TOK_LI:
+               // check arguments
+               tok = tok->next;
+               while (tok != &head) {
+                       switch (tok->type) {
+                       case TOK_TEXT:
+                       case TOK_BOLD:
+                       case TOK_ITALIC:
+                       case TOK_INDENT:
+                               break;
+                       case TOK_NL:
+                               return start;
+                       default:
+                               syntax(tok->line, "@%s not allowed inside @li\n",
+                                               keyword_name(tok->type));
+                       }
+                       tok = tok->next;
+               }
+               break;
+       case TOK_PRE:
+               // checked later
+               break;
+       case TOK_RAW:
+               if (*ip)
+                       goto indentation;
+               // checked later
+               break;
+       case TOK_ENDPRE:
+       case TOK_ENDRAW:
+               syntax(tok->line, "@%s not expected\n", keyword_name(tok->type));
+               break;
+       case TOK_NL:
+       case TOK_INDENT:
+               BUG();
+               break;
+       }
+       return start;
+indentation:
+       syntax(tok->line, "indentation before @%s\n", keyword_name(tok->type));
+}
+
+static void insert_nl_before(struct token *next)
+{
+       struct token *prev = next->prev;
+       struct token *new = new_token(TOK_NL);
+
+       new->prev = prev;
+       new->next = next;
+       prev->next = new;
+       next->prev = new;
+}
+
+static void normalize(void)
+{
+       struct token *tok = head.next;
+       /*
+        * >= 0 if previous line was text (== amount of indent)
+        *   -1 if previous block was @pre (amount of indent doesn't matter)
+        *   -2 otherwise (@h1 etc., indent was 0)
+        */
+       int prev_indent = -2;
+
+       while (tok != &head) {
+               struct token *start;
+               int i, new_para = 0;
+
+               // remove empty lines
+               while (is_empty_line(tok)) {
+                       tok = remove_line(tok);
+                       new_para = 1;
+                       if (tok == &head)
+                               return;
+               }
+
+               // skips indent
+               start = tok;
+               tok = check_line(tok, &i);
+
+               switch (tok->type) {
+               case TOK_TEXT:
+               case TOK_ITALIC:
+               case TOK_BOLD:
+               case TOK_BR:
+                       // normal text
+                       if (new_para && prev_indent >= -1) {
+                               // previous line/block was text or @pre
+                               // and there was a empty line after it
+                               insert_nl_before(start);
+                       }
+
+                       if (!new_para && prev_indent == i) {
+                               // join with previous line
+                               struct token *nl = start->prev;
+
+                               if (nl->type != TOK_NL)
+                                       BUG();
+
+                               if ((nl->prev != &head && nl->prev->type == TOK_BR) ||
+                                               tok->type == TOK_BR) {
+                                       // don't convert \n after/before @br to ' '
+                                       free_token(nl);
+                               } else {
+                                       // convert "\n" to " "
+                                       nl->type = TOK_TEXT;
+                                       nl->text = " ";
+                                       nl->len = 1;
+                               }
+
+                               // remove indent
+                               while (start->type == TOK_INDENT) {
+                                       struct token *next = start->next;
+                                       free_token(start);
+                                       start = next;
+                               }
+                       }
+
+                       prev_indent = i;
+                       tok = get_next_line(tok);
+                       break;
+               case TOK_PRE:
+               case TOK_RAW:
+                       // these can be directly after normal text
+                       // but not joined with the previous line
+                       if (new_para && prev_indent >= -1) {
+                               // previous line/block was text or @pre
+                               // and there was a empty line after it
+                               insert_nl_before(start);
+                       }
+                       tok = skip_after(tok->next, tok->type + 1);
+                       prev_indent = -1;
+                       break;
+               case TOK_H1:
+               case TOK_H2:
+               case TOK_LI:
+               case TOK_TITLE:
+                       // remove white space after H1, H2, L1 and TITLE
+                       tok = tok->next;
+                       while (tok != &head) {
+                               int type = tok->type;
+                               struct token *next;
+
+                               if (type == TOK_TEXT) {
+                                       while (tok->len && *tok->text == ' ') {
+                                               tok->text++;
+                                               tok->len--;
+                                       }
+                                       if (tok->len)
+                                               break;
+                               }
+                               if (type != TOK_INDENT)
+                                       break;
+
+                               // empty TOK_TEXT or TOK_INDENT
+                               next = tok->next;
+                               free_token(tok);
+                               tok = next;
+                       }
+                       // not normal text. can't be joined
+                       prev_indent = -2;
+                       tok = get_next_line(tok);
+                       break;
+               case TOK_NL:
+               case TOK_INDENT:
+               case TOK_ENDPRE:
+               case TOK_ENDRAW:
+                       BUG();
+                       break;
+               }
+       }
+}
+
+#define output(...) fprintf(outfile, __VA_ARGS__)
+
+static void output_buf(const char *buf, int len)
+{
+       fwrite(buf, 1, len, outfile);
+}
+
+static void output_text(struct token *tok)
+{
+       char buf[1024];
+       const char *str = tok->text;
+       int len = tok->len;
+       int pos = 0;
+
+       while (len) {
+               int c = *str++;
+
+               if (pos >= sizeof(buf) - 1) {
+                       output_buf(buf, pos);
+                       pos = 0;
+               }
+               if (c == '-')
+                       buf[pos++] = '\\';
+               buf[pos++] = c;
+               len--;
+       }
+
+       if (pos)
+               output_buf(buf, pos);
+}
+
+static int bold = 0;
+static int italic = 0;
+static int indent = 0;
+
+static struct token *output_pre(struct token *tok)
+{
+       int bol = 1;
+
+       if (tok->type != TOK_NL)
+               syntax(tok->line, "newline expected after @pre\n");
+
+       output(".nf\n");
+       tok = tok->next;
+       while (tok != &head) {
+               if (bol) {
+                       int i;
+
+                       tok = get_indent(tok, &i);
+                       if (i != indent && tok->type != TOK_NL)
+                               syntax(tok->line, "indent changed in @pre\n");
+               }
+
+               switch (tok->type) {
+               case TOK_TEXT:
+                       if (bol && tok->len && tok->text[0] == '.')
+                               output("\\&");
+                       output_text(tok);
+                       break;
+               case TOK_NL:
+                       output("\n");
+                       bol = 1;
+                       tok = tok->next;
+                       continue;
+               case TOK_ITALIC:
+                       output("`");
+                       break;
+               case TOK_BOLD:
+                       output("*");
+                       break;
+               case TOK_INDENT:
+                       // FIXME: warn
+                       output(" ");
+                       break;
+               case TOK_ENDPRE:
+                       output(".fi\n");
+                       tok = tok->next;
+                       if (tok != &head && tok->type == TOK_NL)
+                               tok = tok->next;
+                       return tok;
+               default:
+                       BUG();
+                       break;
+               }
+               bol = 0;
+               tok = tok->next;
+       }
+       return tok;
+}
+
+static struct token *output_raw(struct token *tok)
+{
+       if (tok->type != TOK_NL)
+               syntax(tok->line, "newline expected after @raw\n");
+
+       tok = tok->next;
+       while (tok != &head) {
+               switch (tok->type) {
+               case TOK_TEXT:
+                       if (tok->len == 2 && !strncmp(tok->text, "\\\\", 2)) {
+                               /* ugly special case
+                                * "\\" (\) was converted to "\\\\" (\\) because
+                                * nroff does escaping too.
+                                */
+                               output("\\");
+                       } else {
+                               output_buf(tok->text, tok->len);
+                       }
+                       break;
+               case TOK_NL:
+                       output("\n");
+                       break;
+               case TOK_ITALIC:
+                       output("`");
+                       break;
+               case TOK_BOLD:
+                       output("*");
+                       break;
+               case TOK_INDENT:
+                       output("\t");
+                       break;
+               case TOK_ENDRAW:
+                       tok = tok->next;
+                       if (tok != &head && tok->type == TOK_NL)
+                               tok = tok->next;
+                       return tok;
+               default:
+                       BUG();
+                       break;
+               }
+               tok = tok->next;
+       }
+       return tok;
+}
+
+static struct token *output_para(struct token *tok)
+{
+       int bol = 1;
+
+       while (tok != &head) {
+               switch (tok->type) {
+               case TOK_TEXT:
+                       output_text(tok);
+                       break;
+               case TOK_ITALIC:
+                       italic ^= 1;
+                       if (italic) {
+                               output("\\fI");
+                       } else {
+                               output("\\fR");
+                       }
+                       break;
+               case TOK_BOLD:
+                       bold ^= 1;
+                       if (bold) {
+                               output("\\fB");
+                       } else {
+                               output("\\fR");
+                       }
+                       break;
+               case TOK_BR:
+                       if (bol) {
+                               output(".br\n");
+                       } else {
+                               output("\n.br\n");
+                       }
+                       bol = 1;
+                       tok = tok->next;
+                       continue;
+               case TOK_NL:
+                       output("\n");
+                       return tok->next;
+               case TOK_INDENT:
+                       output(" ");
+                       break;
+               default:
+                       BUG();
+                       break;
+               }
+               bol = 0;
+               tok = tok->next;
+       }
+       return tok;
+}
+
+static struct token *title(struct token *tok, const char *cmd)
+{
+       output("%s", cmd);
+       return output_para(tok->next);
+}
+
+static struct token *dump_one(struct token *tok)
+{
+       int i;
+
+       tok = get_indent(tok, &i);
+       if (tok->type != TOK_RAW) {
+               while (indent < i) {
+                       output(".RS\n");
+                       indent++;
+               }
+               while (indent > i) {
+                       output(".RE\n");
+                       indent--;
+               }
+       }
+
+       switch (tok->type) {
+       case TOK_TEXT:
+       case TOK_ITALIC:
+       case TOK_BOLD:
+       case TOK_BR:
+               if (tok->type == TOK_TEXT && tok->len && tok->text[0] == '.')
+                       output("\\&");
+               tok = output_para(tok);
+               break;
+       case TOK_H1:
+               tok = title(tok, ".SH ");
+               break;
+       case TOK_H2:
+               tok = title(tok, ".SS ");
+               break;
+       case TOK_LI:
+               tok = title(tok, ".TP\n");
+               break;
+       case TOK_PRE:
+               tok = output_pre(tok->next);
+               break;
+       case TOK_RAW:
+               tok = output_raw(tok->next);
+               break;
+       case TOK_TITLE:
+               tok = title(tok, ".TH ");
+               // must be after .TH
+               // no hyphenation, adjust left
+               output(".nh\n.ad l\n");
+               break;
+       case TOK_NL:
+               output("\n");
+               tok = tok->next;
+               break;
+       case TOK_ENDPRE:
+       case TOK_ENDRAW:
+       case TOK_INDENT:
+               BUG();
+               break;
+       }
+       return tok;
+}
+
+static void dump(void)
+{
+       struct token *tok = head.next;
+
+       while (tok != &head)
+               tok = dump_one(tok);
+}
+
+static void process(void)
+{
+       struct stat s = {};
+       const char *buf;
+       int fd;
+
+       fd = open(filename, O_RDONLY);
+       if (fd == -1)
+               die("opening `%s' for reading: %s\n", filename, strerror(errno));
+       fstat(fd, &s);
+       if (s.st_size) {
+               buf = mmap(NULL, s.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+               if (buf == MAP_FAILED)
+                       die("mmap: %s\n", strerror(errno));
+
+               tokenize(buf, s.st_size);
+               normalize();
+       }
+       close(fd);
+       dump();
+}
+
+int main(int argc, char *argv[])
+{
+       const char *dest;
+       int fd;
+
+       program = argv[0];
+       if (argc != 3) {
+               fprintf(stderr, "Usage: %s <in> <out>\n", program);
+               return 1;
+       }
+       filename = argv[1];
+       dest = argv[2];
+
+       snprintf(tmp_file, sizeof(tmp_file), "%s.XXXXXX", dest);
+       fd = mkstemp(tmp_file);
+       if (fd < 0)
+               die("creating %s: %s\n", tmp_file, strerror(errno));
+       outfile = fdopen(fd, "w");
+       if (!outfile)
+               die("opening %s: %s\n", tmp_file, strerror(errno));
+
+       process();
+       if (rename(tmp_file, dest))
+               die("renaming %s to %s: %s\n", tmp_file, dest, strerror(errno));
+       return 0;
+}
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..7e447cc
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,304 @@
+REV    = HEAD
+
+# version from an annotated tag
+_ver0  = $(shell git describe $(REV) 2>/dev/null)
+# version from a plain tag
+_ver1  = $(shell git describe --tags $(REV) 2>/dev/null)
+# SHA1
+_ver2  = $(shell git rev-parse --verify --short $(REV) 2>/dev/null)
+# hand-made
+_ver3  = v2.8.0
+
+VERSION        = $(or $(_ver0),$(_ver1),$(_ver2),$(_ver3))
+
+all: main plugins man
+
+-include config.mk
+include scripts/lib.mk
+
+CFLAGS += -D_FILE_OFFSET_BITS=64
+
+FFMPEG_CFLAGS += $(shell pkg-config --cflags libswresample)
+FFMPEG_LIBS += $(shell pkg-config --libs libswresample)
+
+CMUS_LIBS = $(PTHREAD_LIBS) $(NCURSES_LIBS) $(ICONV_LIBS) $(DL_LIBS) $(DISCID_LIBS) \
+                       -lm $(COMPAT_LIBS) $(LIBSYSTEMD_LIBS)
+
+command_mode.o input.o main.o ui_curses.o op/pulse.lo: .version
+command_mode.o input.o main.o ui_curses.o op/pulse.lo: CFLAGS += -DVERSION=\"$(VERSION)\"
+main.o server.o: CFLAGS += -DDEFAULT_PORT=3000
+discid.o: CFLAGS += $(DISCID_CFLAGS)
+mpris.o: CFLAGS += $(LIBSYSTEMD_CFLAGS)
+
+.version: Makefile
+       @test "`cat $@ 2> /dev/null`" = "$(VERSION)" && exit 0; \
+       echo "   GEN    $@"; echo $(VERSION) > $@
+
+# programs {{{
+cmus-y := \
+       ape.o browser.o buffer.o cache.o channelmap.o cmdline.o cmus.o command_mode.o \
+       comment.o convert.lo cue.o cue_utils.o debug.o discid.o editable.o expr.o \
+       filters.o format_print.o gbuf.o glob.o help.o history.o http.o id3.o input.o \
+       job.o keys.o keyval.o lib.o load_dir.o locking.o mergesort.o misc.o options.o \
+       output.o pcm.o player.o play_queue.o pl.o rbtree.o read_wrapper.o search_mode.o \
+       search.o server.o spawn.o tabexp_file.o tabexp.o track_info.o track.o tree.o \
+       uchar.o u_collate.o ui_curses.o window.o worker.o xstrjoin.o
+
+cmus-$(CONFIG_MPRIS) += mpris.o
+
+$(cmus-y): CFLAGS += $(PTHREAD_CFLAGS) $(NCURSES_CFLAGS) $(ICONV_CFLAGS) $(DL_CFLAGS)
+
+cmus: $(cmus-y) file.o path.o prog.o xmalloc.o
+       $(call cmd,ld,$(CMUS_LIBS))
+
+cmus-remote: main.o file.o misc.o path.o prog.o xmalloc.o xstrjoin.o
+       $(call cmd,ld,$(COMPAT_LIBS))
+
+# cygwin compat
+DLLTOOL=dlltool
+
+libcmus-$(CONFIG_CYGWIN) := libcmus.a
+
+libcmus.a: $(cmus-y) file.o path.o prog.o xmalloc.o
+       $(LD) -shared -o cmus.exe -Wl,--out-implib=libcmus.a -Wl,--base-file,cmus.base \
+               -Wl,--export-all-symbols -Wl,--no-whole-archive $^ $(CMUS_LIBS)
+       $(DLLTOOL) --output-def cmus.def --dllname cmus.exe --export-all-symbols $^
+       $(DLLTOOL) --base-file cmus.base --dllname cmus.exe --input-def cmus.def --output-exp cmus.exp
+       $(LD) -o cmus.exe -Wl,cmus.exp $^ $(CMUS_LIBS)
+
+# }}}
+
+# input plugins {{{
+cdio-objs              := ip/cdio.lo
+flac-objs              := ip/flac.lo
+mad-objs               := ip/mad.lo ip/nomad.lo
+mikmod-objs            := ip/mikmod.lo
+modplug-objs           := ip/modplug.lo
+bass-objs              := ip/bass.lo
+mpc-objs               := ip/mpc.lo
+vorbis-objs            := ip/vorbis.lo
+opus-objs              := ip/opus.lo
+wavpack-objs           := ip/wavpack.lo
+wav-objs               := ip/wav.lo
+mp4-objs               := ip/mp4.lo
+aac-objs               := ip/aac.lo
+ffmpeg-objs            := ip/ffmpeg.lo
+cue-objs               := ip/cue.lo
+vtx-objs               := ip/vtx.lo
+
+ip-$(CONFIG_CDIO)      += ip/cdio.so
+ip-$(CONFIG_FLAC)      += ip/flac.so
+ip-$(CONFIG_MAD)       += ip/mad.so
+ip-$(CONFIG_MIKMOD)    += ip/mikmod.so
+ip-$(CONFIG_MODPLUG)   += ip/modplug.so
+ip-$(CONFIG_BASS)      += ip/bass.so
+ip-$(CONFIG_MPC)       += ip/mpc.so
+ip-$(CONFIG_VORBIS)    += ip/vorbis.so
+ip-$(CONFIG_OPUS)      += ip/opus.so
+ip-$(CONFIG_WAVPACK)   += ip/wavpack.so
+ip-$(CONFIG_WAV)       += ip/wav.so
+ip-$(CONFIG_MP4)       += ip/mp4.so
+ip-$(CONFIG_AAC)       += ip/aac.so
+ip-$(CONFIG_FFMPEG)    += ip/ffmpeg.so
+ip-$(CONFIG_CUE)       += ip/cue.so
+ip-$(CONFIG_VTX)       += ip/vtx.so
+
+$(cdio-objs):          CFLAGS += $(CDIO_CFLAGS) $(CDDB_CFLAGS)
+$(flac-objs):          CFLAGS += $(FLAC_CFLAGS)
+$(mad-objs):           CFLAGS += $(MAD_CFLAGS)
+$(mikmod-objs):                CFLAGS += $(MIKMOD_CFLAGS)
+$(modplug-objs):       CFLAGS += $(MODPLUG_CFLAGS)
+$(bass-objs):          CFLAGS += $(BASS_CFLAGS)
+$(mpc-objs):           CFLAGS += $(MPC_CFLAGS)
+$(vorbis-objs):                CFLAGS += $(VORBIS_CFLAGS)
+$(opus-objs):          CFLAGS += $(OPUS_CFLAGS)
+$(wavpack-objs):       CFLAGS += $(WAVPACK_CFLAGS)
+$(mp4-objs):           CFLAGS += $(MP4_CFLAGS)
+$(aac-objs):           CFLAGS += $(AAC_CFLAGS)
+$(ffmpeg-objs):                CFLAGS += $(FFMPEG_CFLAGS)
+$(vtx-objs):           CFLAGS += $(VTX_CFLAGS)
+
+ip/cdio.so: $(cdio-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(CDIO_LIBS) $(CDDB_LIBS))
+
+ip/flac.so: $(flac-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(FLAC_LIBS))
+
+ip/mad.so: $(mad-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(MAD_LIBS) $(ICONV_LIBS))
+
+ip/mikmod.so: $(mikmod-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(MIKMOD_LIBS))
+
+ip/modplug.so: $(modplug-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(MODPLUG_LIBS))
+
+ip/bass.so: $(bass-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(BASS_LIBS))
+
+ip/mpc.so: $(mpc-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(MPC_LIBS))
+
+ip/vorbis.so: $(vorbis-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(VORBIS_LIBS))
+
+ip/opus.so: $(opus-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(OPUS_LIBS))
+
+ip/wavpack.so: $(wavpack-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(WAVPACK_LIBS))
+
+ip/wav.so: $(wav-objs) $(libcmus-y)
+       $(call cmd,ld_dl,)
+
+ip/mp4.so: $(mp4-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(MP4_LIBS))
+
+ip/aac.so: $(aac-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(AAC_LIBS))
+
+ip/ffmpeg.so: $(ffmpeg-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(FFMPEG_LIBS))
+
+ip/cue.so: $(cue-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(CUE_LIBS))
+
+ip/vtx.so: $(vtx-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(VTX_LIBS))
+
+# }}}
+
+# output plugins {{{
+pulse-objs             := op/pulse.lo
+alsa-objs              := op/alsa.lo op/mixer_alsa.lo
+jack-objs              := op/jack.lo
+arts-objs              := op/arts.lo
+oss-objs               := op/oss.lo op/mixer_oss.lo
+sun-objs               := op/sun.lo op/mixer_sun.lo
+sndio-objs             := op/sndio.lo
+ao-objs                        := op/ao.lo
+coreaudio-objs         := op/coreaudio.lo
+waveout-objs           := op/waveout.lo
+roar-objs               := op/roar.lo
+
+op-$(CONFIG_PULSE)     += op/pulse.so
+op-$(CONFIG_ALSA)      += op/alsa.so
+op-$(CONFIG_JACK)      += op/jack.so
+op-$(CONFIG_ARTS)      += op/arts.so
+op-$(CONFIG_OSS)       += op/oss.so
+op-$(CONFIG_SNDIO)     += op/sndio.so
+op-$(CONFIG_SUN)       += op/sun.so
+op-$(CONFIG_COREAUDIO) += op/coreaudio.so
+op-$(CONFIG_AO)                += op/ao.so
+op-$(CONFIG_WAVEOUT)   += op/waveout.so
+op-$(CONFIG_ROAR)       += op/roar.so
+
+$(pulse-objs): CFLAGS          += $(PULSE_CFLAGS)
+$(alsa-objs): CFLAGS           += $(ALSA_CFLAGS)
+$(jack-objs): CFLAGS           += $(JACK_CFLAGS) $(SAMPLERATE_CFLAGS)
+$(arts-objs): CFLAGS           += $(ARTS_CFLAGS)
+$(oss-objs):  CFLAGS           += $(OSS_CFLAGS)
+$(sndio-objs): CFLAGS          += $(SNDIO_CFLAGS)
+$(sun-objs):  CFLAGS           += $(SUN_CFLAGS)
+$(ao-objs):   CFLAGS           += $(AO_CFLAGS)
+$(coreaudio-objs): CFLAGS      += $(COREAUDIO_CFLAGS)
+$(waveout-objs): CFLAGS        += $(WAVEOUT_CFLAGS)
+$(roar-objs): CFLAGS           += $(ROAR_CFLAGS)
+
+op/pulse.so: $(pulse-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(PULSE_LIBS))
+
+op/alsa.so: $(alsa-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(ALSA_LIBS))
+
+op/jack.so: $(jack-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(JACK_LIBS) $(SAMPLERATE_LIBS))
+
+op/arts.so: $(arts-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(ARTS_LIBS))
+
+op/oss.so: $(oss-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(OSS_LIBS))
+
+op/sndio.so: $(sndio-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(SNDIO_LIBS))
+
+op/sun.so: $(sun-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(SUN_LIBS))
+
+op/ao.so: $(ao-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(AO_LIBS))
+
+op/coreaudio.so: $(coreaudio-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(COREAUDIO_LIBS))
+
+op/waveout.so: $(waveout-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(WAVEOUT_LIBS))
+
+op/roar.so: $(roar-objs) $(libcmus-y)
+       $(call cmd,ld_dl,$(ROAR_LIBS))
+# }}}
+
+# man {{{
+man1   := Doc/cmus.1 Doc/cmus-remote.1
+man7   := Doc/cmus-tutorial.7
+
+$(man1): Doc/ttman
+$(man7): Doc/ttman
+
+%.1: %.txt
+       $(call cmd,ttman)
+
+%.7: %.txt
+       $(call cmd,ttman)
+
+Doc/ttman.o: Doc/ttman.c
+       $(call cmd,hostcc,)
+
+Doc/ttman: Doc/ttman.o
+       $(call cmd,hostld,)
+
+quiet_cmd_ttman = MAN    $@
+      cmd_ttman = Doc/ttman $< $@
+# }}}
+
+data           = $(wildcard data/*)
+
+clean          += *.o ip/*.lo op/*.lo ip/*.so op/*.so cmus libcmus.a cmus.def cmus.base cmus.exp cmus-remote Doc/*.o Doc/ttman Doc/*.1 Doc/*.7 .install.log
+distclean      += .version config.mk config/*.h tags
+
+main: cmus cmus-remote
+plugins: $(ip-y) $(op-y)
+man: $(man1) $(man7)
+
+install-main: main
+       $(INSTALL) -m755 $(bindir) cmus cmus-remote
+
+install-plugins: plugins
+       $(INSTALL) -m755 $(libdir)/cmus/ip $(ip-y)
+       $(INSTALL) -m755 $(libdir)/cmus/op $(op-y)
+
+install-data: man
+       $(INSTALL) -m644 $(datadir)/cmus $(data)
+       $(INSTALL) -m644 $(mandir)/man1 $(man1)
+       $(INSTALL) -m644 $(mandir)/man7 $(man7)
+       $(INSTALL) -m755 $(exampledir) cmus-status-display
+
+install: all install-main install-plugins install-data
+
+tags:
+       exuberant-ctags *.[ch]
+
+# generating tarball using GIT {{{
+TARNAME        = cmus-$(VERSION)
+
+dist:
+       @tarname=$(TARNAME);                                            \
+       test "$(_ver2)" || { echo "No such revision $(REV)"; exit 1; }; \
+       echo "   DIST   $$tarname.tar.bz2";                             \
+       git archive --format=tar --prefix=$$tarname/ $(REV)^{tree} | bzip2 -c -9 > $$tarname.tar.bz2
+
+# }}}
+
+.PHONY: all main plugins man dist tags
+.PHONY: install install-main install-plugins install-man
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..8346db3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,121 @@
+*Warning: cmus is not actively maintained. For details, please see [#856](https://github.com/cmus/cmus/issues/856)*
+
+cmus — C\* Music Player
+=======================
+
+https://cmus.github.io/
+
+[![Build Status](https://travis-ci.org/cmus/cmus.svg?branch=master)](https://travis-ci.org/cmus/cmus)
+
+Copyright © 2004-2008 Timo Hirvonen <tihirvon@gmail.com>
+
+Copyright © 2008-2017 Various Authors
+
+
+Configuration
+-------------
+
+List available optional features
+
+    $ ./configure --help
+
+Auto-detect everything
+
+    $ ./configure
+
+To disable some feature, arts for example, and install to `$HOME` run
+
+    $ ./configure prefix=$HOME CONFIG_ARTS=n
+
+After running configure you can see from the generated `config.mk` file
+what features have been configured in (see the `CONFIG_*` options).
+
+*Note*: For some distributions you need to install development versions
+of the dependencies.  For example if you want to use 'mad' input plugin
+(mp3) you need to install `libmad0-dev` (Debian) or `libmad-devel` (RPM)
+package. After installing dependencies you need to run `./configure`
+again, of course.
+
+If you want to use the Tremor library as alternative for decoding
+Ogg/Vorbis files you have to pass `CONFIG_TREMOR=y` to the configure
+script:
+
+    $ ./configure CONFIG_VORBIS=y CONFIG_TREMOR=y
+
+The Tremor library is supposed to be used on hardware that has no FPU.
+
+
+Building
+--------
+
+    $ make
+
+Or on some BSD systems you need to explicitly use GNU make:
+
+    $ gmake
+
+
+Installation
+------------
+
+    $ make install
+
+Or to install to a temporary directory:
+
+    $ make install DESTDIR=~/tmp/cmus
+
+This is useful when creating binary packages.
+
+Remember to replace `make` with `gmake` if needed.
+
+
+Manuals
+-------
+
+    $ man cmus-tutorial
+
+And
+
+    $ man cmus
+
+
+Mailing List
+------------
+
+To subscribe to cmus-devel@lists.sourceforge.net or view the archive visit
+http://lists.sourceforge.net/lists/listinfo/cmus-devel.
+
+The mailing list now serves as an archive for old releases and issues.
+Please use the github [issues](https://github.com/cmus/cmus/issues)
+page for any problems, suggestions, or bug reports.
+
+
+Reporting Bugs
+--------------
+
+Bugs should be reported using the Github [issue tracker](https://github.com/cmus/cmus/issues).
+When creating a new issue, a template will be shown containing instructions on how to collect
+the necessary information.
+
+Additional debug information can be found in `~/cmus-debug.txt` if you configured cmus with
+maximum debug level (`./configure DEBUG=2`). In case of a crash the last lines may be helpful.
+
+
+Git Repository
+--------------
+
+https://github.com/cmus/cmus
+
+    $ git clone https://github.com/cmus/cmus.git
+
+
+Hacking
+-------
+
+cmus uses the [Linux kernel coding style](https://www.kernel.org/doc/html/latest/process/coding-style.html).
+Use hard tabs.  Tabs are _always_ 8 characters wide.  Keep the style consistent with rest of the
+code.
+
+Bug fixes and implementations of new features should be suggested as a
+[pull request](https://github.com/cmus/cmus/pulls) directly on Github.
+
diff --git a/ape.c b/ape.c
new file mode 100644 (file)
index 0000000..1f620c1
--- /dev/null
+++ b/ape.c
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 Chun-Yu Shei <cshei AT cs.indiana.edu>
+ *
+ * Cleaned up by Timo Hirvonen <tihirvon@gmail.com>
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ape.h"
+#include "file.h"
+#include "xmalloc.h"
+#include "utils.h"
+
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <strings.h>
+
+/* http://www.personal.uni-jena.de/~pfk/mpp/sv8/apetag.html */
+
+#define PREAMBLE_SIZE (8)
+static const char preamble[PREAMBLE_SIZE] = { 'A', 'P', 'E', 'T', 'A', 'G', 'E', 'X' };
+
+/* NOTE: not sizeof(struct ape_header)! */
+#define HEADER_SIZE (32)
+
+/* returns position of APE header or -1 if not found */
+static int find_ape_tag_slow(int fd)
+{
+       char buf[4096];
+       int match = 0;
+       int pos = 0;
+
+       /* seek to start of file */
+       if (lseek(fd, pos, SEEK_SET) == -1)
+               return -1;
+
+       while (1) {
+               int i, got = read(fd, buf, sizeof(buf));
+
+               if (got == -1) {
+                       if (errno == EAGAIN || errno == EINTR)
+                               continue;
+                       break;
+               }
+               if (got == 0)
+                       break;
+
+               for (i = 0; i < got; i++) {
+                       if (buf[i] != preamble[match]) {
+                               match = 0;
+                               continue;
+                       }
+
+                       match++;
+                       if (match == PREAMBLE_SIZE)
+                               return pos + i + 1 - PREAMBLE_SIZE;
+               }
+               pos += got;
+       }
+       return -1;
+}
+
+static int ape_parse_header(const char *buf, struct ape_header *h)
+{
+       if (memcmp(buf, preamble, PREAMBLE_SIZE))
+               return 0;
+
+       h->version = read_le32(buf + 8);
+       h->size = read_le32(buf + 12);
+       h->count = read_le32(buf + 16);
+       h->flags = read_le32(buf + 20);
+       return 1;
+}
+
+static int read_header(int fd, struct ape_header *h)
+{
+       char buf[HEADER_SIZE];
+
+       if (read_all(fd, buf, sizeof(buf)) != sizeof(buf))
+               return 0;
+
+       return ape_parse_header(buf, h);
+}
+
+/* sets fd right after the header and returns 1 if found,
+ * otherwise returns 0
+ */
+static int find_ape_tag(int fd, struct ape_header *h, int slow)
+{
+       int pos;
+
+       if (lseek(fd, -HEADER_SIZE, SEEK_END) == -1)
+               return 0;
+       if (read_header(fd, h))
+               return 1;
+
+       /* try to skip ID3v1 tag at the end of the file */
+       if (lseek(fd, -(HEADER_SIZE + 128), SEEK_END) == -1)
+               return 0;
+       if (read_header(fd, h))
+               return 1;
+
+       if (!slow)
+               return 0;
+
+       pos = find_ape_tag_slow(fd);
+       if (pos == -1)
+               return 0;
+       if (lseek(fd, pos, SEEK_SET) == -1)
+               return 0;
+       return read_header(fd, h);
+}
+
+/*
+ * All keys are ASCII and length is 2..255
+ *
+ * UTF-8:      Artist, Album, Title, Genre
+ * Integer:    Track (N or N/M)
+ * Date:       Year (release), "Record Date"
+ *
+ * UTF-8 strings are NOT zero terminated.
+ *
+ * Also support "discnumber" (vorbis) and "disc" (non-standard)
+ */
+static int ape_parse_one(const char *buf, int size, char **keyp, char **valp)
+{
+       int pos = 0;
+
+       while (size - pos > 8) {
+               uint32_t val_len, flags;
+               char *key, *val;
+               int64_t max_key_len, key_len;
+
+               val_len = read_le32(buf + pos); pos += 4;
+               flags = read_le32(buf + pos); pos += 4;
+
+               max_key_len = size - pos - (int64_t)val_len - 1;
+               if (max_key_len < 0) {
+                       /* corrupt */
+                       break;
+               }
+
+               for (key_len = 0; key_len < max_key_len && buf[pos + key_len]; key_len++)
+                       ; /* nothing */
+               if (buf[pos + key_len]) {
+                       /* corrupt */
+                       break;
+               }
+
+               if (!AF_IS_UTF8(flags)) {
+                       /* ignore binary data */
+                       pos += key_len + 1 + val_len;
+                       continue;
+               }
+
+               key = xstrdup(buf + pos);
+               pos += key_len + 1;
+
+               /* should not be NUL-terminated */
+               val = xstrndup(buf + pos, val_len);
+               pos += val_len;
+
+               /* could be moved to comment.c but I don't think anyone else would use it */
+               if (!strcasecmp(key, "record date") || !strcasecmp(key, "year")) {
+                       free(key);
+                       key = xstrdup("date");
+               }
+
+               if (!strcasecmp(key, "date")) {
+                       /* Date format
+                        *
+                        * 1999-08-11 12:34:56
+                        * 1999-08-11 12:34
+                        * 1999-08-11
+                        * 1999-08
+                        * 1999
+                        * 1999-W34     (week 34, totally crazy)
+                        *
+                        * convert to year, pl.c supports only years anyways
+                        *
+                        * FIXME: which one is the most common tag (year or record date)?
+                        */
+                       if (strlen(val) > 4)
+                               val[4] = 0;
+               }
+
+               *keyp = key;
+               *valp = val;
+               return pos;
+       }
+       return -1;
+}
+
+/* return the number of comments, or -1 */
+int ape_read_tags(struct apetag *ape, int fd, int slow)
+{
+       struct ape_header *h = &ape->header;
+       int rc = -1;
+       off_t old_pos;
+
+       /* save position */
+       old_pos = lseek(fd, 0, SEEK_CUR);
+
+       if (!find_ape_tag(fd, h, slow))
+               goto fail;
+
+       if (AF_IS_FOOTER(h->flags)) {
+               /* seek back right after the header */
+               if (lseek(fd, -((int)h->size), SEEK_CUR) == -1)
+                       goto fail;
+       }
+
+       /* ignore insane tags */
+       if (h->size > 1024 * 1024)
+               goto fail;
+
+       ape->buf = xnew(char, h->size);
+       if (read_all(fd, ape->buf, h->size) != h->size)
+               goto fail;
+
+       rc = h->count;
+
+fail:
+       lseek(fd, old_pos, SEEK_SET);
+       return rc;
+}
+
+/* returned key-name must be free'd */
+char *ape_get_comment(struct apetag *ape, char **val)
+{
+       struct ape_header *h = &ape->header;
+       char *key;
+       int rc;
+
+       if (ape->pos >= h->size)
+               return NULL;
+
+       rc = ape_parse_one(ape->buf + ape->pos, h->size - ape->pos, &key, val);
+       if (rc < 0)
+               return NULL;
+       ape->pos += rc;
+
+       return key;
+}
diff --git a/ape.h b/ape.h
new file mode 100644 (file)
index 0000000..58522ef
--- /dev/null
+++ b/ape.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2007 Johannes Weißl
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_APE_H
+#define CMUS_APE_H
+
+#include <stdint.h>
+#include <stdlib.h>
+
+struct ape_header {
+       /* 1000 or 2000 (1.0, 2.0) */
+       uint32_t version;
+
+       /* tag size (header + tags, excluding footer) */
+       uint32_t size;
+
+       /* number of items */
+       uint32_t count;
+
+       /* global flags for each tag
+        * there are also private flags for every tag
+        * NOTE: 0 for version 1.0 (1000)
+        */
+       uint32_t flags;
+};
+
+/* ape flags */
+#define AF_IS_UTF8(f)          (((f) & 6) == 0)
+#define AF_IS_FOOTER(f)                (((f) & (1 << 29)) == 0)
+
+struct apetag {
+       char *buf;
+       int pos;
+       struct ape_header header;
+};
+
+#define APETAG(name) struct apetag name = { .buf = NULL, .pos = 0, }
+
+int ape_read_tags(struct apetag *ape, int fd, int slow);
+char *ape_get_comment(struct apetag *ape, char **val);
+
+static inline void ape_free(struct apetag *ape)
+{
+       free(ape->buf);
+}
+
+#endif
diff --git a/browser.c b/browser.c
new file mode 100644 (file)
index 0000000..4ecffe6
--- /dev/null
+++ b/browser.c
@@ -0,0 +1,499 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "browser.h"
+#include "load_dir.h"
+#include "cmus.h"
+#include "xmalloc.h"
+#include "ui_curses.h"
+#include "file.h"
+#include "misc.h"
+#include "options.h"
+#include "uchar.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/mman.h>
+
+struct window *browser_win;
+struct searchable *browser_searchable;
+char *browser_dir;
+
+static LIST_HEAD(browser_head);
+
+static inline void browser_entry_to_iter(struct browser_entry *e, struct iter *iter)
+{
+       iter->data0 = &browser_head;
+       iter->data1 = e;
+       iter->data2 = NULL;
+}
+
+/* filter out names starting with '.' except '..' */
+static int normal_filter(const char *name, const struct stat *s)
+{
+       if (name[0] == '.') {
+               if (name[1] == '.' && name[2] == 0)
+                       return 1;
+               return 0;
+       }
+       if (S_ISDIR(s->st_mode))
+               return 1;
+       return cmus_is_supported(name);
+}
+
+/* filter out '.' */
+static int hidden_filter(const char *name, const struct stat *s)
+{
+       if (name[0] == '.' && name[1] == 0)
+               return 0;
+       return 1;
+}
+
+/* only works for BROWSER_ENTRY_DIR and BROWSER_ENTRY_FILE */
+static int entry_cmp(const struct browser_entry *a, const struct browser_entry *b)
+{
+       if (a->type == BROWSER_ENTRY_DIR) {
+               if (b->type == BROWSER_ENTRY_FILE)
+                       return -1;
+               if (!strcmp(a->name, "../"))
+                       return -1;
+               if (!strcmp(b->name, "../"))
+                       return 1;
+               return strcmp(a->name, b->name);
+       }
+       if (b->type == BROWSER_ENTRY_DIR)
+               return 1;
+       return strcmp(a->name, b->name);
+}
+
+static char *fullname(const char *path, const char *name)
+{
+       int l1, l2;
+       char *full;
+
+       l1 = strlen(path);
+       l2 = strlen(name);
+       if (path[l1 - 1] == '/')
+               l1--;
+       full = xnew(char, l1 + 1 + l2 + 1);
+       memcpy(full, path, l1);
+       full[l1] = '/';
+       memcpy(full + l1 + 1, name, l2 + 1);
+       return full;
+}
+
+static void free_browser_list(void)
+{
+       struct list_head *item;
+
+       item = browser_head.next;
+       while (item != &browser_head) {
+               struct list_head *next = item->next;
+               struct browser_entry *entry;
+
+               entry = list_entry(item, struct browser_entry, node);
+               free(entry);
+               item = next;
+       }
+       list_init(&browser_head);
+}
+
+static int add_pl_line(void *data, const char *line)
+{
+       struct browser_entry *e;
+       int name_size = strlen(line) + 1;
+
+       e = xmalloc(sizeof(struct browser_entry) + name_size);
+       memcpy(e->name, line, name_size);
+       e->type = BROWSER_ENTRY_PLLINE;
+       list_add_tail(&e->node, &browser_head);
+       return 0;
+}
+
+static int do_browser_load(const char *name)
+{
+       struct stat st;
+
+       if (stat(name, &st))
+               return -1;
+
+       if (S_ISREG(st.st_mode) && cmus_is_playlist(name)) {
+               char *buf;
+               ssize_t size;
+
+               buf = mmap_file(name, &size);
+               if (size == -1)
+                       return -1;
+
+               free_browser_list();
+
+               if (buf) {
+                       struct browser_entry *parent_dir_e = xmalloc(sizeof(struct browser_entry) + 4);
+                       strcpy(parent_dir_e->name, "../");
+                       parent_dir_e->type = BROWSER_ENTRY_DIR;
+                       list_add_tail(&parent_dir_e->node, &browser_head);
+
+                       cmus_playlist_for_each(buf, size, 0, add_pl_line, NULL);
+                       munmap(buf, size);
+               }
+       } else if (S_ISDIR(st.st_mode)) {
+               int (*filter)(const char *, const struct stat *) = normal_filter;
+               struct directory dir;
+               const char *str;
+               int root = !strcmp(name, "/");
+
+               if (show_hidden)
+                       filter = hidden_filter;
+
+               if (dir_open(&dir, name))
+                       return -1;
+
+               free_browser_list();
+               while ((str = dir_read(&dir))) {
+                       struct browser_entry *e;
+                       struct list_head *item;
+                       int len;
+
+                       if (!filter(str, &dir.st))
+                               continue;
+
+                       /* ignore .. if we are in the root dir */
+                       if (root && !strcmp(str, ".."))
+                               continue;
+
+                       len = strlen(str);
+                       e = xmalloc(sizeof(struct browser_entry) + len + 2);
+                       e->type = BROWSER_ENTRY_FILE;
+                       memcpy(e->name, str, len);
+                       if (S_ISDIR(dir.st.st_mode)) {
+                               e->type = BROWSER_ENTRY_DIR;
+                               e->name[len++] = '/';
+                       }
+                       e->name[len] = 0;
+
+                       item = browser_head.prev;
+                       while (item != &browser_head) {
+                               struct browser_entry *other;
+
+                               other = container_of(item, struct browser_entry, node);
+                               if (entry_cmp(e, other) >= 0)
+                                       break;
+                               item = item->prev;
+                       }
+                       /* add after item */
+                       list_add(&e->node, item);
+               }
+               dir_close(&dir);
+
+               /* try to update currect working directory */
+               if (chdir(name))
+                       return -1;
+       } else {
+               errno = ENOTDIR;
+               return -1;
+       }
+       return 0;
+}
+
+static int browser_load(const char *name)
+{
+       int rc;
+
+       rc = do_browser_load(name);
+       if (rc)
+               return rc;
+
+       window_set_contents(browser_win, &browser_head);
+       free(browser_dir);
+       browser_dir = xstrdup(name);
+       return 0;
+}
+
+static GENERIC_ITER_PREV(browser_get_prev, struct browser_entry, node)
+static GENERIC_ITER_NEXT(browser_get_next, struct browser_entry, node)
+
+static int browser_search_get_current(void *data, struct iter *iter)
+{
+       return window_get_sel(browser_win, iter);
+}
+
+static int browser_search_matches(void *data, struct iter *iter, const char *text)
+{
+       char **words = get_words(text);
+       int matched = 0;
+
+       if (words[0] != NULL) {
+               struct browser_entry *e;
+               int i;
+
+               e = iter_to_browser_entry(iter);
+               for (i = 0; ; i++) {
+                       if (words[i] == NULL) {
+                               window_set_sel(browser_win, iter);
+                               matched = 1;
+                               break;
+                       }
+                       if (u_strcasestr_filename(e->name, words[i]) == NULL)
+                               break;
+               }
+       }
+       free_str_array(words);
+       return matched;
+}
+
+static const struct searchable_ops browser_search_ops = {
+       .get_prev = browser_get_prev,
+       .get_next = browser_get_next,
+       .get_current = browser_search_get_current,
+       .matches = browser_search_matches
+};
+
+void browser_init(void)
+{
+       struct iter iter;
+       char cwd[1024];
+       char *dir;
+
+       if (getcwd(cwd, sizeof(cwd)) == NULL) {
+               dir = xstrdup("/");
+       } else {
+               dir = xstrdup(cwd);
+       }
+       if (do_browser_load(dir)) {
+               free(dir);
+               do_browser_load("/");
+               browser_dir = xstrdup("/");
+       } else {
+               browser_dir = dir;
+       }
+
+       browser_win = window_new(browser_get_prev, browser_get_next);
+       window_set_contents(browser_win, &browser_head);
+       window_changed(browser_win);
+
+       iter.data0 = &browser_head;
+       iter.data1 = NULL;
+       iter.data2 = NULL;
+       browser_searchable = searchable_new(NULL, &iter, &browser_search_ops);
+}
+
+void browser_exit(void)
+{
+       searchable_free(browser_searchable);
+       free_browser_list();
+       window_free(browser_win);
+       free(browser_dir);
+}
+
+int browser_chdir(const char *dir)
+{
+       if (browser_load(dir)) {
+       }
+       return 0;
+}
+
+void browser_up(void)
+{
+       char *new, *ptr, *pos;
+       struct browser_entry *e;
+       int len;
+
+       if (strcmp(browser_dir, "/") == 0)
+               return;
+
+       ptr = strrchr(browser_dir, '/');
+       if (ptr == browser_dir) {
+               new = xstrdup("/");
+       } else {
+               new = xstrndup(browser_dir, ptr - browser_dir);
+       }
+
+       /* remember old position */
+       ptr++;
+       len = strlen(ptr);
+       pos = xstrdup(ptr);
+
+       errno = 0;
+       if (browser_load(new)) {
+               if (errno == ENOENT) {
+                       free(pos);
+                       free(browser_dir);
+                       browser_dir = new;
+                       browser_up();
+                       return;
+               }
+               error_msg("could not open directory '%s': %s\n", new, strerror(errno));
+               free(new);
+               free(pos);
+               return;
+       }
+       free(new);
+
+       /* select old position */
+       list_for_each_entry(e, &browser_head, node) {
+               if (strncmp(e->name, pos, len) == 0 &&
+                   (e->name[len] == '/' || e->name[len] == '\0')) {
+                       struct iter iter;
+
+                       browser_entry_to_iter(e, &iter);
+                       window_set_sel(browser_win, &iter);
+                       break;
+               }
+       }
+       free(pos);
+}
+
+static void browser_cd(const char *dir)
+{
+       char *new;
+       int len;
+
+       if (strcmp(dir, "../") == 0) {
+               browser_up();
+               return;
+       }
+
+       new = fullname(browser_dir, dir);
+       len = strlen(new);
+       if (new[len - 1] == '/')
+               new[len - 1] = 0;
+       if (browser_load(new))
+               error_msg("could not open directory '%s': %s\n", dir, strerror(errno));
+       free(new);
+}
+
+static void browser_cd_playlist(const char *filename)
+{
+       if (browser_load(filename))
+               error_msg("could not read playlist '%s': %s\n", filename, strerror(errno));
+}
+
+void browser_enter(void)
+{
+       struct browser_entry *e;
+       struct iter sel;
+       int len;
+
+       if (!window_get_sel(browser_win, &sel))
+               return;
+       e = iter_to_browser_entry(&sel);
+       len = strlen(e->name);
+       if (len == 0)
+               return;
+       if (e->type == BROWSER_ENTRY_DIR) {
+               browser_cd(e->name);
+       } else {
+               if (e->type == BROWSER_ENTRY_PLLINE) {
+                       cmus_play_file(e->name);
+               } else {
+                       char *filename;
+
+                       filename = fullname(browser_dir, e->name);
+                       if (cmus_is_playlist(filename)) {
+                               browser_cd_playlist(filename);
+                       } else {
+                               cmus_play_file(filename);
+                       }
+                       free(filename);
+               }
+       }
+}
+
+char *browser_get_sel(void)
+{
+       struct browser_entry *e;
+       struct iter sel;
+
+       if (!window_get_sel(browser_win, &sel))
+               return NULL;
+
+       e = iter_to_browser_entry(&sel);
+       if (e->type == BROWSER_ENTRY_PLLINE)
+               return xstrdup(e->name);
+
+       return fullname(browser_dir, e->name);
+}
+
+void browser_delete(void)
+{
+       struct browser_entry *e;
+       struct iter sel;
+       int len;
+
+       if (!window_get_sel(browser_win, &sel))
+               return;
+       e = iter_to_browser_entry(&sel);
+       len = strlen(e->name);
+       if (len == 0)
+               return;
+       if (e->type == BROWSER_ENTRY_FILE) {
+               char *name;
+
+               name = fullname(browser_dir, e->name);
+               if (yes_no_query("Delete file '%s'? [y/N]", e->name)) {
+                       if (unlink(name) == -1) {
+                               error_msg("deleting '%s': %s", e->name, strerror(errno));
+                       } else {
+                               window_row_vanishes(browser_win, &sel);
+                               list_del(&e->node);
+                               free(e);
+                       }
+               }
+               free(name);
+       }
+}
+
+void browser_reload(void)
+{
+       char *tmp = xstrdup(browser_dir);
+       char *sel = NULL;
+       struct iter iter;
+       struct browser_entry *e;
+
+       /* remember selection */
+       if (window_get_sel(browser_win, &iter)) {
+               e = iter_to_browser_entry(&iter);
+               sel = xstrdup(e->name);
+       }
+
+       /* have to use tmp  */
+       if (browser_load(tmp)) {
+               error_msg("could not update contents '%s': %s\n", tmp, strerror(errno));
+               free(tmp);
+               free(sel);
+               return;
+       }
+
+       if (sel) {
+               /* set selection */
+               list_for_each_entry(e, &browser_head, node) {
+                       if (strcmp(e->name, sel) == 0) {
+                               browser_entry_to_iter(e, &iter);
+                               window_set_sel(browser_win, &iter);
+                               break;
+                       }
+               }
+       }
+
+       free(tmp);
+       free(sel);
+}
diff --git a/browser.h b/browser.h
new file mode 100644 (file)
index 0000000..95ac61e
--- /dev/null
+++ b/browser.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_BROWSER_H
+#define CMUS_BROWSER_H
+
+#include "list.h"
+#include "window.h"
+#include "search.h"
+
+struct browser_entry {
+       struct list_head node;
+
+       enum { BROWSER_ENTRY_DIR, BROWSER_ENTRY_FILE, BROWSER_ENTRY_PLLINE } type;
+       char name[];
+};
+
+static inline struct browser_entry *iter_to_browser_entry(struct iter *iter)
+{
+       return iter->data1;
+}
+
+extern struct window *browser_win;
+extern char *browser_dir;
+extern struct searchable *browser_searchable;
+
+void browser_init(void);
+void browser_exit(void);
+int browser_chdir(const char *dir);
+char *browser_get_sel(void);
+void browser_up(void);
+void browser_enter(void);
+void browser_delete(void);
+void browser_reload(void);
+void browser_toggle_show_hidden(void);
+
+#endif
diff --git a/buffer.c b/buffer.c
new file mode 100644 (file)
index 0000000..dd40f68
--- /dev/null
+++ b/buffer.c
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "buffer.h"
+#include "xmalloc.h"
+#include "locking.h"
+#include "debug.h"
+
+/*
+ * chunk can be accessed by either consumer OR producer, not both at same time
+ * -> no need to lock
+ */
+struct chunk {
+       char data[CHUNK_SIZE];
+
+       /* index to data, first filled byte */
+       unsigned int l;
+
+       /* index to data, last filled byte + 1
+        *
+        * there are h - l bytes available (filled)
+        */
+       unsigned int h : 31;
+
+       /* if chunk is marked filled it can only be accessed by consumer
+        * otherwise only producer is allowed to access the chunk
+        */
+       unsigned int filled : 1;
+};
+
+unsigned int buffer_nr_chunks;
+
+static pthread_mutex_t buffer_mutex = CMUS_MUTEX_INITIALIZER;
+static struct chunk *buffer_chunks = NULL;
+static unsigned int buffer_ridx;
+static unsigned int buffer_widx;
+
+void buffer_init(void)
+{
+       free(buffer_chunks);
+       buffer_chunks = xnew(struct chunk, buffer_nr_chunks);
+       buffer_reset();
+}
+
+void buffer_free(void)
+{
+       free(buffer_chunks);
+}
+
+/*
+ * @pos: returned pointer to available data
+ *
+ * Returns number of bytes available at @pos
+ *
+ * After reading bytes mark them consumed calling buffer_consume().
+ */
+int buffer_get_rpos(char **pos)
+{
+       struct chunk *c;
+       int size = 0;
+
+       cmus_mutex_lock(&buffer_mutex);
+       c = &buffer_chunks[buffer_ridx];
+       if (c->filled) {
+               size = c->h - c->l;
+               *pos = c->data + c->l;
+       }
+       cmus_mutex_unlock(&buffer_mutex);
+
+       return size;
+}
+
+/*
+ * @pos: pointer to buffer position where data can be written
+ *
+ * Returns number of bytes can be written to @pos.  If the return value is
+ * non-zero it is guaranteed to be >= 1024.
+ *
+ * After writing bytes mark them filled calling buffer_fill().
+ */
+int buffer_get_wpos(char **pos)
+{
+       struct chunk *c;
+       int size = 0;
+
+       cmus_mutex_lock(&buffer_mutex);
+       c = &buffer_chunks[buffer_widx];
+       if (!c->filled) {
+               size = CHUNK_SIZE - c->h;
+               *pos = c->data + c->h;
+       }
+       cmus_mutex_unlock(&buffer_mutex);
+
+       return size;
+}
+
+void buffer_consume(int count)
+{
+       struct chunk *c;
+
+       BUG_ON(count < 0);
+       cmus_mutex_lock(&buffer_mutex);
+       c = &buffer_chunks[buffer_ridx];
+       BUG_ON(!c->filled);
+       c->l += count;
+       if (c->l == c->h) {
+               c->l = 0;
+               c->h = 0;
+               c->filled = 0;
+               buffer_ridx++;
+               buffer_ridx %= buffer_nr_chunks;
+       }
+       cmus_mutex_unlock(&buffer_mutex);
+}
+
+/* chunk is marked filled if free bytes < 1024 or count == 0 */
+int buffer_fill(int count)
+{
+       struct chunk *c;
+       int filled = 0;
+
+       cmus_mutex_lock(&buffer_mutex);
+       c = &buffer_chunks[buffer_widx];
+       BUG_ON(c->filled);
+       c->h += count;
+
+       if (CHUNK_SIZE - c->h < 1024 || (count == 0 && c->h > 0)) {
+               c->filled = 1;
+               buffer_widx++;
+               buffer_widx %= buffer_nr_chunks;
+               filled = 1;
+       }
+
+       cmus_mutex_unlock(&buffer_mutex);
+       return filled;
+}
+
+void buffer_reset(void)
+{
+       int i;
+
+       cmus_mutex_lock(&buffer_mutex);
+       buffer_ridx = 0;
+       buffer_widx = 0;
+       for (i = 0; i < buffer_nr_chunks; i++) {
+               buffer_chunks[i].l = 0;
+               buffer_chunks[i].h = 0;
+               buffer_chunks[i].filled = 0;
+       }
+       cmus_mutex_unlock(&buffer_mutex);
+}
+
+int buffer_get_filled_chunks(void)
+{
+       int c;
+
+       cmus_mutex_lock(&buffer_mutex);
+       if (buffer_ridx < buffer_widx) {
+               /*
+                * |__##########____|
+                *    r         w
+                *
+                * |############____|
+                *  r           w
+                */
+               c = buffer_widx - buffer_ridx;
+       } else if (buffer_ridx > buffer_widx) {
+               /*
+                * |#######______###|
+                *         w     r
+                *
+                * |_____________###|
+                *  w            r
+                */
+               c = buffer_nr_chunks - buffer_ridx + buffer_widx;
+       } else {
+               /*
+                * |################|
+                *     r
+                *     w
+                *
+                * |________________|
+                *     r
+                *     w
+                */
+               if (buffer_chunks[buffer_ridx].filled) {
+                       c = buffer_nr_chunks;
+               } else {
+                       c = 0;
+               }
+       }
+       cmus_mutex_unlock(&buffer_mutex);
+       return c;
+}
diff --git a/buffer.h b/buffer.h
new file mode 100644 (file)
index 0000000..b21d57e
--- /dev/null
+++ b/buffer.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_BUFFER_H
+#define CMUS_BUFFER_H
+
+/* must be a multiple of any supported frame size */
+#define CHUNK_SIZE (60 * 1024)
+
+extern unsigned int buffer_nr_chunks;
+
+void buffer_init(void);
+void buffer_free(void);
+int buffer_get_rpos(char **pos);
+int buffer_get_wpos(char **pos);
+void buffer_consume(int count);
+int buffer_fill(int count);
+void buffer_reset(void);
+int buffer_get_filled_chunks(void);
+
+#endif
diff --git a/cache.c b/cache.c
new file mode 100644 (file)
index 0000000..d2a79cd
--- /dev/null
+++ b/cache.c
@@ -0,0 +1,541 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "cache.h"
+#include "misc.h"
+#include "file.h"
+#include "input.h"
+#include "track_info.h"
+#include "utils.h"
+#include "xmalloc.h"
+#include "xstrjoin.h"
+#include "gbuf.h"
+#include "options.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/mman.h>
+
+#define CACHE_VERSION   0x0d
+
+#define CACHE_64_BIT   0x01
+#define CACHE_BE       0x02
+
+#define CACHE_RESERVED_PATTERN         0xff
+
+#define CACHE_ENTRY_USED_SIZE          28
+#define CACHE_ENTRY_RESERVED_SIZE      52
+#define CACHE_ENTRY_TOTAL_SIZE (CACHE_ENTRY_RESERVED_SIZE + CACHE_ENTRY_USED_SIZE)
+
+// Cmus Track Cache version X + 4 bytes flags
+static char cache_header[8] = "CTC\0\0\0\0\0";
+
+// host byte order
+// mtime is either 32 or 64 bits
+struct cache_entry {
+       // size of this struct including size itself
+       uint32_t size;
+
+       int32_t play_count;
+       int64_t mtime;
+       int32_t duration;
+       int32_t bitrate;
+       int32_t bpm;
+
+       // when introducing new fields decrease the reserved space accordingly
+       uint8_t _reserved[CACHE_ENTRY_RESERVED_SIZE];
+
+       // filename, codec, codec_profile and N * (key, val)
+       char strings[];
+};
+
+// make sure our mmap/sizeof-based code works
+STATIC_ASSERT(CACHE_ENTRY_TOTAL_SIZE == sizeof(struct cache_entry));
+STATIC_ASSERT(CACHE_ENTRY_TOTAL_SIZE == offsetof(struct cache_entry, strings));
+
+
+#define ALIGN(size) (((size) + sizeof(long) - 1) & ~(sizeof(long) - 1))
+#define HASH_SIZE 1023
+
+static struct track_info *hash_table[HASH_SIZE];
+static char *cache_filename;
+static int total;
+
+struct fifo_mutex cache_mutex = FIFO_MUTEX_INITIALIZER;
+
+
+static void add_ti(struct track_info *ti, unsigned int hash)
+{
+       unsigned int pos = hash % HASH_SIZE;
+       struct track_info *next = hash_table[pos];
+
+       ti->next = next;
+       hash_table[pos] = ti;
+       total++;
+}
+
+static int valid_cache_entry(const struct cache_entry *e, unsigned int avail)
+{
+       unsigned int min_size = sizeof(*e);
+       unsigned int str_size;
+       int i, count;
+
+       if (avail < min_size)
+               return 0;
+
+       if (e->size < min_size || e->size > avail)
+               return 0;
+
+       str_size = e->size - min_size;
+       count = 0;
+       for (i = 0; i < str_size; i++) {
+               if (!e->strings[i])
+                       count++;
+       }
+       if (count % 2 == 0)
+               return 0;
+       if (e->strings[str_size - 1])
+               return 0;
+       return 1;
+}
+
+static struct track_info *cache_entry_to_ti(struct cache_entry *e)
+{
+       const char *strings = e->strings;
+       struct track_info *ti = track_info_new(strings);
+       struct keyval *kv;
+       int str_size = e->size - sizeof(*e);
+       int pos, i, count;
+
+       ti->duration = e->duration;
+       ti->bitrate = e->bitrate;
+       ti->mtime = e->mtime;
+       ti->play_count = e->play_count;
+       ti->bpm = e->bpm;
+
+       // count strings (filename + codec + codec_profile + key/val pairs)
+       count = 0;
+       for (i = 0; i < str_size; i++) {
+               if (!strings[i])
+                       count++;
+       }
+       count = (count - 3) / 2;
+
+       // NOTE: filename already copied by track_info_new()
+       pos = strlen(strings) + 1;
+       ti->codec = strings[pos] ? xstrdup(strings + pos) : NULL;
+       pos += strlen(strings + pos) + 1;
+       ti->codec_profile = strings[pos] ? xstrdup(strings + pos) : NULL;
+       pos += strlen(strings + pos) + 1;
+       kv = xnew(struct keyval, count + 1);
+       for (i = 0; i < count; i++) {
+               int size;
+
+               size = strlen(strings + pos) + 1;
+               kv[i].key = xstrdup(strings + pos);
+               pos += size;
+
+               size = strlen(strings + pos) + 1;
+               kv[i].val = xstrdup(strings + pos);
+               pos += size;
+       }
+       kv[i].key = NULL;
+       kv[i].val = NULL;
+       track_info_set_comments(ti, kv);
+       return ti;
+}
+
+struct track_info *lookup_cache_entry(const char *filename, unsigned int hash)
+{
+       struct track_info *ti = hash_table[hash % HASH_SIZE];
+
+       while (ti) {
+               if (!strcmp(filename, ti->filename))
+                       return ti;
+               ti = ti->next;
+       }
+       return NULL;
+}
+
+static void do_cache_remove_ti(struct track_info *ti, unsigned int hash)
+{
+       unsigned int pos = hash % HASH_SIZE;
+       struct track_info *t = hash_table[pos];
+       struct track_info *next, *prev = NULL;
+
+       while (t) {
+               next = t->next;
+               if (t == ti) {
+                       if (prev) {
+                               prev->next = next;
+                       } else {
+                               hash_table[pos] = next;
+                       }
+                       total--;
+                       track_info_unref(ti);
+                       return;
+               }
+               prev = t;
+               t = next;
+       }
+}
+
+void cache_remove_ti(struct track_info *ti)
+{
+       do_cache_remove_ti(ti, hash_str(ti->filename));
+}
+
+static int read_cache(void)
+{
+       unsigned int size, offset = 0;
+       struct stat st = {};
+       char *buf;
+       int fd;
+
+       fd = open(cache_filename, O_RDONLY);
+       if (fd < 0) {
+               if (errno == ENOENT)
+                       return 0;
+               return -1;
+       }
+       fstat(fd, &st);
+       if (st.st_size < sizeof(cache_header))
+               goto close;
+       size = st.st_size;
+
+       buf = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
+       if (buf == MAP_FAILED) {
+               close(fd);
+               return -1;
+       }
+
+       if (memcmp(buf, cache_header, sizeof(cache_header)))
+               goto corrupt;
+
+       offset = sizeof(cache_header);
+       while (offset < size) {
+               struct cache_entry *e = (void *)(buf + offset);
+               struct track_info *ti;
+
+               if (!valid_cache_entry(e, size - offset))
+                       goto corrupt;
+
+               ti = cache_entry_to_ti(e);
+               add_ti(ti, hash_str(ti->filename));
+               offset += ALIGN(e->size);
+       }
+       munmap(buf, size);
+       close(fd);
+       return 0;
+corrupt:
+       munmap(buf, size);
+close:
+       close(fd);
+       // corrupt
+       return -2;
+}
+
+int cache_init(void)
+{
+       unsigned int flags = 0;
+
+#ifdef WORDS_BIGENDIAN
+       flags |= CACHE_BE;
+#endif
+       if (sizeof(long) == 8)
+               flags |= CACHE_64_BIT;
+
+       cache_header[7] = flags & 0xff; flags >>= 8;
+       cache_header[6] = flags & 0xff; flags >>= 8;
+       cache_header[5] = flags & 0xff; flags >>= 8;
+       cache_header[4] = flags & 0xff;
+
+       /* assumed version */
+       cache_header[3] = CACHE_VERSION;
+
+       cache_filename = xstrjoin(cmus_config_dir, "/cache");
+       return read_cache();
+}
+
+static int ti_filename_cmp(const void *a, const void *b)
+{
+       const struct track_info *ai = *(const struct track_info **)a;
+       const struct track_info *bi = *(const struct track_info **)b;
+
+       return strcmp(ai->filename, bi->filename);
+}
+
+static struct track_info **get_track_infos(bool reference)
+{
+       struct track_info **tis;
+       int i, c;
+
+       tis = xnew(struct track_info *, total);
+       c = 0;
+       for (i = 0; i < HASH_SIZE; i++) {
+               struct track_info *ti = hash_table[i];
+
+               while (ti) {
+                       if (reference)
+                               track_info_ref(ti);
+                       tis[c++] = ti;
+                       ti = ti->next;
+               }
+       }
+       qsort(tis, total, sizeof(struct track_info *), ti_filename_cmp);
+       return tis;
+}
+
+static void flush_buffer(int fd, struct gbuf *buf)
+{
+       if (buf->len) {
+               write_all(fd, buf->buffer, buf->len);
+               gbuf_clear(buf);
+       }
+}
+
+static void write_ti(int fd, struct gbuf *buf, struct track_info *ti, unsigned int *offsetp)
+{
+       const struct keyval *kv = ti->comments;
+       unsigned int offset = *offsetp;
+       unsigned int pad;
+       struct cache_entry e;
+       int *len, alloc = 64, count, i;
+
+       memset(e._reserved, CACHE_RESERVED_PATTERN, sizeof(e._reserved));
+
+       count = 0;
+       len = xnew(int, alloc);
+       e.size = sizeof(e);
+       e.duration = ti->duration;
+       e.bitrate = ti->bitrate;
+       e.mtime = ti->mtime;
+       e.play_count = ti->play_count;
+       e.bpm = ti->bpm;
+       len[count] = strlen(ti->filename) + 1;
+       e.size += len[count++];
+       len[count] = (ti->codec ? strlen(ti->codec) : 0) + 1;
+       e.size += len[count++];
+       len[count] = (ti->codec_profile ? strlen(ti->codec_profile) : 0) + 1;
+       e.size += len[count++];
+       for (i = 0; kv[i].key; i++) {
+               if (count + 2 > alloc) {
+                       alloc *= 2;
+                       len = xrenew(int, len, alloc);
+               }
+               len[count] = strlen(kv[i].key) + 1;
+               e.size += len[count++];
+               len[count] = strlen(kv[i].val) + 1;
+               e.size += len[count++];
+       }
+
+       pad = ALIGN(offset) - offset;
+       if (gbuf_avail(buf) < pad + e.size)
+               flush_buffer(fd, buf);
+
+       count = 0;
+       if (pad)
+               gbuf_set(buf, 0, pad);
+       gbuf_add_bytes(buf, &e, sizeof(e));
+       gbuf_add_bytes(buf, ti->filename, len[count++]);
+       gbuf_add_bytes(buf, ti->codec ? ti->codec : "", len[count++]);
+       gbuf_add_bytes(buf, ti->codec_profile ? ti->codec_profile : "", len[count++]);
+       for (i = 0; kv[i].key; i++) {
+               gbuf_add_bytes(buf, kv[i].key, len[count++]);
+               gbuf_add_bytes(buf, kv[i].val, len[count++]);
+       }
+
+       free(len);
+       *offsetp = offset + pad + e.size;
+}
+
+int cache_close(void)
+{
+       GBUF(buf);
+       struct track_info **tis;
+       unsigned int offset;
+       int i, fd, rc;
+       char *tmp;
+
+       tmp = xstrjoin(cmus_config_dir, "/cache.tmp");
+       fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0666);
+       if (fd < 0) {
+               free(tmp);
+               return -1;
+       }
+
+       tis = get_track_infos(false);
+
+       gbuf_grow(&buf, 64 * 1024 - 1);
+       gbuf_add_bytes(&buf, cache_header, sizeof(cache_header));
+       offset = sizeof(cache_header);
+       for (i = 0; i < total; i++)
+               write_ti(fd, &buf, tis[i], &offset);
+       flush_buffer(fd, &buf);
+       gbuf_free(&buf);
+       free(tis);
+
+       close(fd);
+       rc = rename(tmp, cache_filename);
+       free(tmp);
+       return rc;
+}
+
+static struct track_info *ip_get_ti(const char *filename)
+{
+       struct track_info *ti = NULL;
+       struct input_plugin *ip;
+       struct keyval *comments;
+       int rc;
+
+       ip = ip_new(filename);
+       rc = ip_open(ip);
+       if (rc) {
+               ip_delete(ip);
+               return NULL;
+       }
+
+       rc = ip_read_comments(ip, &comments);
+       if (!rc) {
+               ti = track_info_new(filename);
+               track_info_set_comments(ti, comments);
+               ti->duration = ip_duration(ip);
+               ti->bitrate = ip_bitrate(ip);
+               ti->codec = ip_codec(ip);
+               ti->codec_profile = ip_codec_profile(ip);
+               ti->mtime = ip_is_remote(ip) ? -1 : file_get_mtime(filename);
+       }
+       ip_delete(ip);
+       return ti;
+}
+
+struct track_info *cache_get_ti(const char *filename, int force)
+{
+       unsigned int hash = hash_str(filename);
+       struct track_info *ti;
+       int reload = 0;
+
+       ti = lookup_cache_entry(filename, hash);
+       if (ti) {
+               if ((!skip_track_info && ti->duration == 0 && !is_http_url(filename)) || force){
+                       do_cache_remove_ti(ti, hash);
+                       ti = NULL;
+                       reload = 1;
+               }
+       }
+       if (!ti) {
+               if (skip_track_info && !reload && !force) {
+                       struct growing_keyvals c = {NULL, 0, 0};
+
+                       ti = track_info_new(filename);
+
+                       keyvals_terminate(&c);
+                       track_info_set_comments(ti, c.keyvals);
+
+                       ti->duration = 0;
+               } else {
+                       ti = ip_get_ti(filename);
+               }
+               if (!ti)
+                       return NULL;
+               add_ti(ti, hash);
+       }
+       track_info_ref(ti);
+       return ti;
+}
+
+struct track_info **cache_refresh(int *count, int force)
+{
+       struct track_info **tis = get_track_infos(true);
+       int i, n = total;
+
+       for (i = 0; i < n; i++) {
+               unsigned int hash;
+               struct track_info *ti = tis[i];
+               struct stat st;
+               int rc = 0;
+
+               cache_yield();
+
+               /*
+                * If no-one else has reference to tis[i] then it is set to NULL
+                * otherwise:
+                *
+                * unchanged: tis[i] = NULL
+                * deleted:   tis[i]->next = NULL
+                * changed:   tis[i]->next = new
+                */
+
+               if (!is_url(ti->filename)) {
+                       rc = stat(ti->filename, &st);
+                       if (!rc && !force && ti->mtime == st.st_mtime) {
+                               // unchanged
+                               track_info_unref(ti);
+                               tis[i] = NULL;
+                               continue;
+                       }
+               }
+
+               hash = hash_str(ti->filename);
+               do_cache_remove_ti(ti, hash);
+
+               if (!rc) {
+                       // changed
+                       struct track_info *new_ti;
+
+                       // clear cache-only entries
+                       if (force && track_info_unique_ref(ti)) {
+                               track_info_unref(ti);
+                               tis[i] = NULL;
+                               continue;
+                       }
+
+                       new_ti = ip_get_ti(ti->filename);
+                       if (new_ti) {
+                               add_ti(new_ti, hash);
+
+                               if (track_info_unique_ref(ti)) {
+                                       track_info_unref(ti);
+                                       tis[i] = NULL;
+                               } else {
+                                       track_info_ref(new_ti);
+                                       ti->next = new_ti;
+                               }
+                               continue;
+                       }
+                       // treat as deleted
+               }
+
+               // deleted
+               if (track_info_unique_ref(ti)) {
+                       track_info_unref(ti);
+                       tis[i] = NULL;
+               } else {
+                       ti->next = NULL;
+               }
+       }
+       *count = n;
+       return tis;
+}
diff --git a/cache.h b/cache.h
new file mode 100644 (file)
index 0000000..d4bf331
--- /dev/null
+++ b/cache.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_CACHE_H
+#define CMUS_CACHE_H
+
+#include "track_info.h"
+#include "locking.h"
+
+extern struct fifo_mutex cache_mutex;
+
+#define cache_lock() fifo_mutex_lock(&cache_mutex)
+#define cache_yield() fifo_mutex_yield(&cache_mutex)
+#define cache_unlock() fifo_mutex_unlock(&cache_mutex)
+
+int cache_init(void);
+int cache_close(void);
+struct track_info *cache_get_ti(const char *filename, int force);
+void cache_remove_ti(struct track_info *ti);
+struct track_info **cache_refresh(int *count, int force);
+struct track_info *lookup_cache_entry(const char *filename, unsigned int hash);
+
+#endif
diff --git a/channelmap.c b/channelmap.c
new file mode 100644 (file)
index 0000000..c2f24a0
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2011-2013 Various Authors
+ * Copyright 2011 Johannes Weißl
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "channelmap.h"
+#include "utils.h"
+
+void channel_map_init_waveex(int channels, unsigned int mask, channel_position_t *map)
+{
+       /* http://www.microsoft.com/whdc/device/audio/multichaud.mspx#EMLAC */
+       const channel_position_t channel_map_waveex[] = {
+               CHANNEL_POSITION_FRONT_LEFT,
+               CHANNEL_POSITION_FRONT_RIGHT,
+               CHANNEL_POSITION_FRONT_CENTER,
+               CHANNEL_POSITION_LFE,
+               CHANNEL_POSITION_REAR_LEFT,
+               CHANNEL_POSITION_REAR_RIGHT,
+               CHANNEL_POSITION_FRONT_LEFT_OF_CENTER,
+               CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER,
+               CHANNEL_POSITION_REAR_CENTER,
+               CHANNEL_POSITION_SIDE_LEFT,
+               CHANNEL_POSITION_SIDE_RIGHT,
+               CHANNEL_POSITION_TOP_CENTER,
+               CHANNEL_POSITION_TOP_FRONT_LEFT,
+               CHANNEL_POSITION_TOP_FRONT_CENTER,
+               CHANNEL_POSITION_TOP_FRONT_RIGHT,
+               CHANNEL_POSITION_TOP_REAR_LEFT,
+               CHANNEL_POSITION_TOP_REAR_CENTER,
+               CHANNEL_POSITION_TOP_REAR_RIGHT
+       };
+
+       if (channels == 1) {
+               map[0] = CHANNEL_POSITION_MONO;
+       } else if (channels > 1 && channels < N_ELEMENTS(channel_map_waveex)) {
+               int i, j = 0;
+
+               if (!mask)
+                       mask = (1 << channels) - 1;
+
+               for (i = 0; i < N_ELEMENTS(channel_map_waveex); i++) {
+                       if (mask & (1 << i))
+                               map[j++] = channel_map_waveex[i];
+               }
+               if (j != channels)
+                       map[0] = CHANNEL_POSITION_INVALID;
+       } else {
+               map[0] = CHANNEL_POSITION_INVALID;
+       }
+}
diff --git a/channelmap.h b/channelmap.h
new file mode 100644 (file)
index 0000000..403a34e
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2011-2013 Various Authors
+ * Copyright 2011 Johannes Weißl
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_CHANNELMAP_H
+#define CMUS_CHANNELMAP_H
+
+#include <string.h>
+
+#define CHANNELS_MAX 32
+
+/* Modelled after PulseAudio */
+enum channel_position {
+       CHANNEL_POSITION_INVALID = -1,
+       CHANNEL_POSITION_MONO = 0,
+       CHANNEL_POSITION_FRONT_LEFT,
+       CHANNEL_POSITION_FRONT_RIGHT,
+       CHANNEL_POSITION_FRONT_CENTER,
+
+       CHANNEL_POSITION_LEFT = CHANNEL_POSITION_FRONT_LEFT,
+       CHANNEL_POSITION_RIGHT = CHANNEL_POSITION_FRONT_RIGHT,
+       CHANNEL_POSITION_CENTER = CHANNEL_POSITION_FRONT_CENTER,
+
+       CHANNEL_POSITION_REAR_CENTER,
+       CHANNEL_POSITION_REAR_LEFT,
+       CHANNEL_POSITION_REAR_RIGHT,
+
+       CHANNEL_POSITION_LFE,
+       CHANNEL_POSITION_SUBWOOFER = CHANNEL_POSITION_LFE,
+
+       CHANNEL_POSITION_FRONT_LEFT_OF_CENTER,
+       CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER,
+
+       CHANNEL_POSITION_SIDE_LEFT,
+       CHANNEL_POSITION_SIDE_RIGHT,
+
+       CHANNEL_POSITION_TOP_CENTER,
+
+       CHANNEL_POSITION_TOP_FRONT_LEFT,
+       CHANNEL_POSITION_TOP_FRONT_RIGHT,
+       CHANNEL_POSITION_TOP_FRONT_CENTER,
+
+       CHANNEL_POSITION_TOP_REAR_LEFT,
+       CHANNEL_POSITION_TOP_REAR_RIGHT,
+       CHANNEL_POSITION_TOP_REAR_CENTER,
+
+       CHANNEL_POSITION_MAX
+};
+
+typedef enum channel_position  channel_position_t;
+
+#define CHANNEL_MAP_INIT       { CHANNEL_POSITION_INVALID }
+
+#define CHANNEL_MAP(name) \
+       channel_position_t name[CHANNELS_MAX] = CHANNEL_MAP_INIT
+
+static inline int channel_map_valid(const channel_position_t *map)
+{
+       return map[0] != CHANNEL_POSITION_INVALID;
+}
+
+static inline int channel_map_equal(const channel_position_t *a, const channel_position_t *b, int channels)
+{
+       return memcmp(a, b, sizeof(*a) * channels) == 0;
+}
+
+static inline channel_position_t *channel_map_copy(channel_position_t *dst, const channel_position_t *src)
+{
+       return memcpy(dst, src, sizeof(*dst) * CHANNELS_MAX);
+}
+
+static inline void channel_map_init_stereo(channel_position_t *map)
+{
+       map[0] = CHANNEL_POSITION_LEFT;
+       map[1] = CHANNEL_POSITION_RIGHT;
+}
+
+void channel_map_init_waveex(int channels, unsigned int mask, channel_position_t *map);
+
+#endif
diff --git a/cmdline.c b/cmdline.c
new file mode 100644 (file)
index 0000000..92afae2
--- /dev/null
+++ b/cmdline.c
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "cmdline.h"
+#include "uchar.h"
+#include "xmalloc.h"
+
+struct cmdline cmdline;
+
+const char cmdline_word_delimiters[]     = " ";
+const char cmdline_filename_delimiters[] = "/";
+
+void cmdline_init(void)
+{
+       cmdline.blen = 0;
+       cmdline.clen = 0;
+       cmdline.bpos = 0;
+       cmdline.cpos = 0;
+       cmdline.size = 128;
+       cmdline.line = xnew(char, cmdline.size);
+       cmdline.line[0] = 0;
+}
+
+void cmdline_insert_ch(uchar ch)
+{
+       int size;
+
+       size = u_char_size(ch);
+       if (cmdline.blen + size > cmdline.size) {
+               cmdline.size *= 2;
+               cmdline.line = xrenew(char, cmdline.line, cmdline.size);
+       }
+       memmove(cmdline.line + cmdline.bpos + size,
+               cmdline.line + cmdline.bpos,
+               cmdline.blen - cmdline.bpos + 1);
+       u_set_char_raw(cmdline.line, &cmdline.bpos, ch);
+       cmdline.cpos++;
+       cmdline.blen += size;
+       cmdline.clen++;
+}
+
+void cmdline_backspace(void)
+{
+       int bpos, size;
+
+       if (cmdline.bpos == 0)
+               return;
+
+       bpos = cmdline.bpos;
+       u_prev_char_pos(cmdline.line, &bpos);
+       size = cmdline.bpos - bpos;
+
+       memmove(cmdline.line + bpos,
+               cmdline.line + cmdline.bpos,
+               cmdline.blen - cmdline.bpos + 1);
+       cmdline.bpos -= size;
+       cmdline.cpos--;
+       cmdline.blen -= size;
+       cmdline.clen--;
+}
+
+void cmdline_backspace_to_bol(void)
+{
+       while (cmdline.bpos)
+               cmdline_backspace();
+}
+
+void cmdline_delete_ch(void)
+{
+       uchar ch;
+       int size, bpos;
+
+       if (cmdline.bpos == cmdline.blen)
+               return;
+       bpos = cmdline.bpos;
+       ch = u_get_char(cmdline.line, &bpos);
+       size = u_char_size(ch);
+       cmdline.blen -= size;
+       cmdline.clen--;
+       memmove(cmdline.line + cmdline.bpos,
+               cmdline.line + cmdline.bpos + size,
+               cmdline.blen - cmdline.bpos + 1);
+}
+
+void cmdline_set_text(const char *text)
+{
+       int len = strlen(text);
+
+       if (len >= cmdline.size) {
+               while (len >= cmdline.size)
+                       cmdline.size *= 2;
+               cmdline.line = xrenew(char, cmdline.line, cmdline.size);
+       }
+       memcpy(cmdline.line, text, len + 1);
+       cmdline.cpos = u_strlen_safe(cmdline.line);
+       cmdline.bpos = len;
+       cmdline.clen = cmdline.cpos;
+       cmdline.blen = len;
+}
+
+void cmdline_clear(void)
+{
+       cmdline.blen = 0;
+       cmdline.clen = 0;
+       cmdline.bpos = 0;
+       cmdline.cpos = 0;
+       cmdline.line[0] = 0;
+}
+
+void cmdline_clear_end(void)
+{
+       cmdline.line[cmdline.bpos] = 0;
+
+       cmdline.clen = u_strlen_safe(cmdline.line);
+       cmdline.blen = strlen(cmdline.line);
+}
+
+void cmdline_move_left(void)
+{
+       if (cmdline.bpos > 0) {
+               cmdline.cpos--;
+               u_prev_char_pos(cmdline.line, &cmdline.bpos);
+       }
+}
+
+void cmdline_move_right(void)
+{
+       if (cmdline.bpos < cmdline.blen) {
+               u_get_char(cmdline.line, &cmdline.bpos);
+               cmdline.cpos++;
+       }
+}
+
+void cmdline_move_home(void)
+{
+       cmdline.cpos = 0;
+       cmdline.bpos = 0;
+}
+
+void cmdline_move_end(void)
+{
+       cmdline.cpos = cmdline.clen;
+       cmdline.bpos = cmdline.blen;
+}
+
+static int next_word(const char *str, int bpos, int *cdiff, const char *delim, int direction)
+{
+       int skip_delim = 1;
+       while ((direction > 0) ? str[bpos] : (bpos > 0)) {
+               uchar ch;
+               int oldp = bpos;
+
+               if (direction > 0) {
+                       ch = u_get_char(str, &bpos);
+               } else {
+                       u_prev_char_pos(str, &bpos);
+                       oldp = bpos;
+                       ch = u_get_char(str, &oldp);
+               }
+
+               if (u_strchr(delim, ch)) {
+                       if (!skip_delim) {
+                               bpos -= bpos - oldp;
+                               break;
+                       }
+               } else
+                       skip_delim = 0;
+
+               *cdiff += direction;
+       }
+       return bpos;
+}
+
+void cmdline_forward_word(const char *delim)
+{
+       cmdline.bpos = next_word(cmdline.line, cmdline.bpos, &cmdline.cpos, delim, +1);
+}
+
+void cmdline_backward_word(const char *delim)
+{
+       cmdline.bpos = next_word(cmdline.line, cmdline.bpos, &cmdline.cpos, delim, -1);
+}
+
+void cmdline_delete_word(const char *delim)
+{
+       int bpos, cdiff = 0;
+
+       bpos = next_word(cmdline.line, cmdline.bpos, &cdiff, delim, +1);
+
+       memmove(cmdline.line + cmdline.bpos,
+               cmdline.line + bpos,
+               cmdline.blen - bpos + 1);
+       cmdline.blen -= bpos - cmdline.bpos;
+       cmdline.clen -= cdiff;
+}
+
+void cmdline_backward_delete_word(const char *delim)
+{
+       int bpos, cdiff = 0;
+
+       bpos = next_word(cmdline.line, cmdline.bpos, &cdiff, delim, -1);
+
+       cmdline.blen += bpos - cmdline.bpos;
+       memmove(cmdline.line + bpos,
+               cmdline.line + cmdline.bpos,
+               cmdline.blen - bpos + 1);
+       cmdline.bpos = bpos;
+       cmdline.clen += cdiff;
+       cmdline.cpos += cdiff;
+}
diff --git a/cmdline.h b/cmdline.h
new file mode 100644 (file)
index 0000000..36778ed
--- /dev/null
+++ b/cmdline.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMDLINE_H
+#define CMDLINE_H
+
+#include "uchar.h"
+
+struct cmdline {
+       /* length in bytes */
+       int blen;
+
+       /* length in characters */
+       int clen;
+
+       /* pos in bytes */
+       int bpos;
+
+       /* pos in characters */
+       int cpos;
+
+       /* allocated size */
+       int size;
+
+       char *line;
+};
+
+extern struct cmdline cmdline;
+
+extern const char cmdline_word_delimiters[];
+extern const char cmdline_filename_delimiters[];
+
+void cmdline_init(void);
+void cmdline_insert_ch(uchar ch);
+void cmdline_backspace(void);
+void cmdline_backspace_to_bol(void);
+void cmdline_delete_ch(void);
+void cmdline_set_text(const char *text);
+void cmdline_clear(void);
+void cmdline_clear_end(void);
+void cmdline_move_left(void);
+void cmdline_move_right(void);
+void cmdline_move_home(void);
+void cmdline_move_end(void);
+
+void cmdline_forward_word(const char *delim);
+void cmdline_backward_word(const char *delim);
+void cmdline_delete_word(const char *delim);
+void cmdline_backward_delete_word(const char *delim);
+
+#endif
diff --git a/cmus-status-display b/cmus-status-display
new file mode 100755 (executable)
index 0000000..bfa5a90
--- /dev/null
@@ -0,0 +1,50 @@
+#!/bin/sh
+#
+# cmus-status-display
+#
+# Usage:
+#   in cmus command ":set status_display_program=cmus-status-display"
+#
+# This scripts is executed by cmus when status changes:
+#   cmus-status-display key1 val1 key2 val2 ...
+#
+# All keys contain only chars a-z. Values are UTF-8 strings.
+#
+# Keys: status file url artist album discnumber tracknumber title date
+#   - status (stopped, playing, paused) is always given
+#   - file or url is given only if track is 'loaded' in cmus
+#   - other keys/values are given only if they are available
+#  
+
+output()
+{
+       # write status to ~/cmus-status.txt (not very useful though)
+       echo "$*" >> ~/cmus-status.txt 2>&1
+
+       # WMI (http://wmi.modprobe.de/)
+       #wmiremote -t "$*" &> /dev/null
+}
+
+while test $# -ge 2
+do
+       eval _$1='$2'
+       shift
+       shift
+done
+
+if test -n "$_file"
+then
+       h=$(($_duration / 3600))
+       m=$(($_duration % 3600))
+
+       duration=""
+       test $h -gt 0 && dur="$h:"
+       duration="$dur$(printf '%02d:%02d' $(($m / 60)) $(($m % 60)))"
+
+       output "[$_status] $_artist - $_album - $_title ($_date) $duration"
+elif test -n "$_url"
+then
+       output "[$_status] $_url - $_title"
+else
+       output "[$_status]"
+fi
diff --git a/cmus.c b/cmus.c
new file mode 100644 (file)
index 0000000..1823575
--- /dev/null
+++ b/cmus.c
@@ -0,0 +1,505 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "cmus.h"
+#include "job.h"
+#include "lib.h"
+#include "pl.h"
+#include "player.h"
+#include "input.h"
+#include "play_queue.h"
+#include "cache.h"
+#include "misc.h"
+#include "file.h"
+#include "utils.h"
+#include "path.h"
+#include "options.h"
+#include "xmalloc.h"
+#include "debug.h"
+#include "load_dir.h"
+#include "ui_curses.h"
+#include "cache.h"
+#include "gbuf.h"
+#include "discid.h"
+#include "locking.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <dlfcn.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <strings.h>
+
+/* save_playlist_cb, save_ext_playlist_cb */
+typedef int (*save_tracks_cb)(void *data, struct track_info *ti);
+
+static char **playable_exts;
+static const char * const playlist_exts[] = { "m3u", "pl", "pls", NULL };
+
+int cmus_next_track_request_fd;
+static int cmus_next_track_request_fd_priv;
+static pthread_mutex_t cmus_next_file_mutex = CMUS_MUTEX_INITIALIZER;
+static pthread_cond_t cmus_next_file_cond = CMUS_COND_INITIALIZER;
+static int cmus_next_file_provided;
+static struct track_info *cmus_next_file;
+
+static int x11_init_done = 0;
+static void *(*x11_open)(void *) = NULL;
+static int (*x11_raise)(void *, int) = NULL;
+static int (*x11_close)(void *) = NULL;
+
+int cmus_init(void)
+{
+       playable_exts = ip_get_supported_extensions();
+       cache_init();
+       job_init();
+       play_queue_init();
+       return 0;
+}
+
+void cmus_exit(void)
+{
+       job_exit();
+       if (cache_close())
+               d_print("error: %s\n", strerror(errno));
+}
+
+void cmus_next(void)
+{
+       struct track_info *info = cmus_get_next_track();
+       if (info)
+               player_set_file(info);
+}
+
+void cmus_prev(void)
+{
+       struct track_info *info;
+
+       if (play_library) {
+               info = lib_goto_prev();
+       } else {
+               info = pl_goto_prev();
+       }
+
+       if (info)
+               player_set_file(info);
+}
+
+void cmus_play_file(const char *filename)
+{
+       struct track_info *ti;
+
+       cache_lock();
+       ti = cache_get_ti(filename, 0);
+       cache_unlock();
+       if (!ti) {
+               error_msg("Couldn't get file information for %s\n", filename);
+               return;
+       }
+
+       player_play_file(ti);
+}
+
+enum file_type cmus_detect_ft(const char *name, char **ret)
+{
+       char *absolute;
+       struct stat st;
+
+       if (is_http_url(name) || is_cue_url(name)) {
+               *ret = xstrdup(name);
+               return FILE_TYPE_URL;
+       }
+
+       if (is_cdda_url(name)) {
+               *ret = complete_cdda_url(cdda_device, name);
+               return FILE_TYPE_CDDA;
+       }
+
+       *ret = NULL;
+       absolute = path_absolute(name);
+       if (absolute == NULL)
+               return FILE_TYPE_INVALID;
+
+       /* stat follows symlinks, lstat does not */
+       if (stat(absolute, &st) == -1) {
+               free(absolute);
+               return FILE_TYPE_INVALID;
+       }
+
+       if (S_ISDIR(st.st_mode)) {
+               *ret = absolute;
+               return FILE_TYPE_DIR;
+       }
+       if (!S_ISREG(st.st_mode)) {
+               free(absolute);
+               errno = EINVAL;
+               return FILE_TYPE_INVALID;
+       }
+
+       *ret = absolute;
+       if (cmus_is_playlist(absolute))
+               return FILE_TYPE_PL;
+
+       /* NOTE: it could be FILE_TYPE_PL too! */
+       return FILE_TYPE_FILE;
+}
+
+void cmus_add(add_ti_cb add, const char *name, enum file_type ft, int jt, int force,
+               void *opaque)
+{
+       struct add_data *data = xnew(struct add_data, 1);
+
+       data->add = add;
+       data->name = xstrdup(name);
+       data->type = ft;
+       data->force = force;
+       data->opaque = opaque;
+
+       job_schedule_add(jt, data);
+}
+
+static int save_ext_playlist_cb(void *data, struct track_info *ti)
+{
+       GBUF(buf);
+       int fd = *(int *)data;
+       int i, rc;
+
+       gbuf_addf(&buf, "file %s\n", escape(ti->filename));
+       gbuf_addf(&buf, "duration %d\n", ti->duration);
+       gbuf_addf(&buf, "codec %s\n", ti->codec);
+       gbuf_addf(&buf, "bitrate %ld\n", ti->bitrate);
+       for (i = 0; ti->comments[i].key; i++)
+               gbuf_addf(&buf, "tag %s %s\n",
+                               ti->comments[i].key,
+                               escape(ti->comments[i].val));
+
+       rc = write_all(fd, buf.buffer, buf.len);
+       gbuf_free(&buf);
+
+       if (rc == -1)
+               return -1;
+       return 0;
+}
+
+static int save_playlist_cb(void *data, struct track_info *ti)
+{
+       int fd = *(int *)data;
+       const char nl = '\n';
+       int rc;
+
+       rc = write_all(fd, ti->filename, strlen(ti->filename));
+       if (rc == -1)
+               return -1;
+       rc = write_all(fd, &nl, 1);
+       if (rc == -1)
+               return -1;
+       return 0;
+}
+
+static int do_cmus_save(for_each_ti_cb for_each_ti, const char *filename,
+               save_tracks_cb save_tracks, void *opaque)
+{
+       int fd, rc;
+
+       if (strcmp(filename, "-") == 0) {
+               if (get_client_fd() == -1) {
+                       error_msg("saving to stdout works only remotely");
+                       return 0;
+               }
+               fd = dup(get_client_fd());
+       } else
+               fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
+       if (fd == -1)
+               return -1;
+       rc = for_each_ti(save_tracks, &fd, opaque);
+       close(fd);
+       return rc;
+}
+
+int cmus_save(for_each_ti_cb for_each_ti, const char *filename, void *opaque)
+{
+       return do_cmus_save(for_each_ti, filename, save_playlist_cb, opaque);
+}
+
+int cmus_save_ext(for_each_ti_cb for_each_ti, const char *filename,
+               void *opaque)
+{
+       return do_cmus_save(for_each_ti, filename, save_ext_playlist_cb,
+                       opaque);
+}
+
+static int update_cb(void *data, struct track_info *ti)
+{
+       struct update_data *d = data;
+
+       if (d->size == d->used) {
+               if (d->size == 0)
+                       d->size = 16;
+               d->size *= 2;
+               d->ti = xrenew(struct track_info *, d->ti, d->size);
+       }
+       track_info_ref(ti);
+       d->ti[d->used++] = ti;
+       return 0;
+}
+
+void cmus_update_cache(int force)
+{
+       struct update_cache_data *data;
+
+       data = xnew(struct update_cache_data, 1);
+       data->force = force;
+
+       job_schedule_update_cache(JOB_TYPE_LIB, data);
+}
+
+void cmus_update_lib(void)
+{
+       struct update_data *data;
+
+       data = xnew0(struct update_data, 1);
+
+       lib_for_each(update_cb, data, NULL);
+
+       job_schedule_update(data);
+}
+
+void cmus_update_tis(struct track_info **tis, int nr, int force)
+{
+       struct update_data *data;
+
+       data = xnew(struct update_data, 1);
+       data->size = nr;
+       data->used = nr;
+       data->ti = tis;
+       data->force = force;
+
+       job_schedule_update(data);
+}
+
+static const char *get_ext(const char *filename)
+{
+       const char *ext = strrchr(filename, '.');
+
+       if (ext)
+               ext++;
+       return ext;
+}
+
+static int str_in_array(const char *str, const char * const *array)
+{
+       int i;
+
+       for (i = 0; array[i]; i++) {
+               if (strcasecmp(str, array[i]) == 0)
+                       return 1;
+       }
+       return 0;
+}
+
+int cmus_is_playlist(const char *filename)
+{
+       const char *ext = get_ext(filename);
+
+       return ext && str_in_array(ext, playlist_exts);
+}
+
+int cmus_is_playable(const char *filename)
+{
+       const char *ext = get_ext(filename);
+
+       return ext && str_in_array(ext, (const char * const *)playable_exts);
+}
+
+int cmus_is_supported(const char *filename)
+{
+       const char *ext = get_ext(filename);
+
+       return ext && (str_in_array(ext, (const char * const *)playable_exts) ||
+                       str_in_array(ext, playlist_exts));
+}
+
+struct pl_data {
+       int (*cb)(void *data, const char *line);
+       void *data;
+};
+
+static int pl_handle_line(void *data, const char *line)
+{
+       struct pl_data *d = data;
+       int i = 0;
+
+       while (isspace((unsigned char)line[i]))
+               i++;
+       if (line[i] == 0)
+               return 0;
+
+       if (line[i] == '#')
+               return 0;
+
+       return d->cb(d->data, line);
+}
+
+static int pls_handle_line(void *data, const char *line)
+{
+       struct pl_data *d = data;
+
+       if (strncasecmp(line, "file", 4))
+               return 0;
+       line = strchr(line, '=');
+       if (line == NULL)
+               return 0;
+       return d->cb(d->data, line + 1);
+}
+
+int cmus_playlist_for_each(const char *buf, int size, int reverse,
+               int (*cb)(void *data, const char *line),
+               void *data)
+{
+       struct pl_data d = { cb, data };
+       int (*handler)(void *, const char *);
+
+       handler = pl_handle_line;
+       if (size >= 10 && strncasecmp(buf, "[playlist]", 10) == 0)
+               handler = pls_handle_line;
+
+       if (reverse) {
+               buffer_for_each_line_reverse(buf, size, handler, &d);
+       } else {
+               buffer_for_each_line(buf, size, handler, &d);
+       }
+       return 0;
+}
+
+/* multi-threaded next track requests */
+
+#define cmus_next_file_lock() cmus_mutex_lock(&cmus_next_file_mutex)
+#define cmus_next_file_unlock() cmus_mutex_unlock(&cmus_next_file_mutex)
+
+static struct track_info *cmus_get_next_from_main_thread(void)
+{
+       struct track_info *ti = play_queue_remove();
+       if (!ti)
+               ti = play_library ? lib_goto_next() : pl_goto_next();
+       return ti;
+}
+
+static struct track_info *cmus_get_next_from_other_thread(void)
+{
+       static pthread_mutex_t mutex = CMUS_MUTEX_INITIALIZER;
+       cmus_mutex_lock(&mutex);
+
+       /* only one thread may request a track at a time */
+
+       notify_via_pipe(cmus_next_track_request_fd_priv);
+
+       cmus_next_file_lock();
+       while (!cmus_next_file_provided)
+               pthread_cond_wait(&cmus_next_file_cond, &cmus_next_file_mutex);
+       struct track_info *ti = cmus_next_file;
+       cmus_next_file_provided = 0;
+       cmus_next_file_unlock();
+
+       cmus_mutex_unlock(&mutex);
+
+       return ti;
+}
+
+struct track_info *cmus_get_next_track(void)
+{
+       pthread_t this_thread = pthread_self();
+       if (pthread_equal(this_thread, main_thread))
+               return cmus_get_next_from_main_thread();
+       return cmus_get_next_from_other_thread();
+}
+
+void cmus_provide_next_track(void)
+{
+       clear_pipe(cmus_next_track_request_fd, 1);
+
+       cmus_next_file_lock();
+       cmus_next_file = cmus_get_next_from_main_thread();
+       cmus_next_file_provided = 1;
+       cmus_next_file_unlock();
+
+       pthread_cond_broadcast(&cmus_next_file_cond);
+}
+
+void cmus_track_request_init(void)
+{
+       init_pipes(&cmus_next_track_request_fd, &cmus_next_track_request_fd_priv);
+}
+
+static int cmus_can_raise_vte_x11(void)
+{
+       return getenv("DISPLAY") && getenv("WINDOWID");
+}
+
+int cmus_can_raise_vte(void)
+{
+       return cmus_can_raise_vte_x11();
+}
+
+static int cmus_raise_vte_x11_error(void)
+{
+       return 0;
+}
+
+void cmus_raise_vte(void)
+{
+       if (cmus_can_raise_vte_x11()) {
+               if (!x11_init_done) {
+                       void *x11;
+
+                       x11_init_done = 1;
+                       x11 = dlopen("libX11.so", RTLD_LAZY);
+
+                       if (x11) {
+                               int (*x11_error)(void *);
+
+                               x11_error = dlsym(x11, "XSetErrorHandler");
+                               x11_open = dlsym(x11, "XOpenDisplay");
+                               x11_raise = dlsym(x11, "XRaiseWindow");
+                               x11_close = dlsym(x11, "XCloseDisplay");
+
+                               if (x11_error) {
+                                       x11_error(cmus_raise_vte_x11_error);
+                               }
+                       }
+               }
+
+               if (x11_open && x11_raise && x11_close) {
+                       char *xid_str;
+                       long int xid = 0;
+
+                       xid_str = getenv("WINDOWID");
+                       if (!str_to_int(xid_str, &xid) && xid != 0) {
+                               void *display;
+
+                               display = x11_open(NULL);
+                               if (display) {
+                                       x11_raise(display, (int) xid);
+                                       x11_close(display);
+                               }
+                       }
+               }
+       }
+}
diff --git a/cmus.h b/cmus.h
new file mode 100644 (file)
index 0000000..e182ca8
--- /dev/null
+++ b/cmus.h
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_API_H
+#define CMUS_API_H
+
+#include "track_info.h"
+
+enum file_type {
+       /* not found, device file... */
+       FILE_TYPE_INVALID,
+
+       FILE_TYPE_URL,
+       FILE_TYPE_PL,
+       FILE_TYPE_DIR,
+       FILE_TYPE_FILE,
+       FILE_TYPE_CDDA
+};
+
+typedef int (*track_info_cb)(void *data, struct track_info *ti);
+
+/* lib_for_each, lib_for_each_filtered, pl_for_each, play_queue_for_each */
+typedef int (*for_each_ti_cb)(track_info_cb cb, void *data, void *opaque);
+
+/* lib_for_each_sel, pl_for_each_sel, play_queue_for_each_sel */
+typedef int (*for_each_sel_ti_cb)(track_info_cb cb, void *data, int reverse);
+
+/* lib_add_track, pl_add_track, play_queue_append, play_queue_prepend */
+typedef void (*add_ti_cb)(struct track_info *, void *opaque);
+
+/* cmus_save, cmus_save_ext */
+typedef int (*save_ti_cb)(for_each_ti_cb for_each_ti, const char *filename,
+               void *opaque);
+
+int cmus_init(void);
+void cmus_exit(void);
+void cmus_play_file(const char *filename);
+
+/* detect file type, returns absolute path or url in @ret */
+enum file_type cmus_detect_ft(const char *name, char **ret);
+
+/* add to library, playlist or queue view
+ *
+ * @add   callback that does the actual adding
+ * @name  playlist, directory, file, URL
+ * @ft    detected FILE_TYPE_*
+ * @jt    JOB_TYPE_{LIB,PL,QUEUE}
+ *
+ * returns immediately, actual work is done in the worker thread.
+ */
+void cmus_add(add_ti_cb, const char *name, enum file_type ft, int jt,
+               int force, void *opaque);
+
+int cmus_save(for_each_ti_cb for_each_ti, const char *filename, void *opaque);
+int cmus_save_ext(for_each_ti_cb for_each_ti, const char *filename,
+               void *opaque);
+
+void cmus_update_cache(int force);
+void cmus_update_lib(void);
+void cmus_update_tis(struct track_info **tis, int nr, int force);
+
+int cmus_is_playlist(const char *filename);
+int cmus_is_playable(const char *filename);
+int cmus_is_supported(const char *filename);
+
+int cmus_playlist_for_each(const char *buf, int size, int reverse,
+               int (*cb)(void *data, const char *line),
+               void *data);
+
+void cmus_next(void);
+void cmus_prev(void);
+
+extern int cmus_next_track_request_fd;
+struct track_info *cmus_get_next_track(void);
+void cmus_provide_next_track(void);
+void cmus_track_request_init(void);
+
+int cmus_can_raise_vte(void);
+void cmus_raise_vte(void);
+
+#endif
diff --git a/command_mode.c b/command_mode.c
new file mode 100644 (file)
index 0000000..af54ef0
--- /dev/null
@@ -0,0 +1,3090 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "command_mode.h"
+#include "search_mode.h"
+#include "cmdline.h"
+#include "options.h"
+#include "ui_curses.h"
+#include "history.h"
+#include "tabexp.h"
+#include "tabexp_file.h"
+#include "browser.h"
+#include "filters.h"
+#include "player.h"
+#include "output.h"
+#include "editable.h"
+#include "lib.h"
+#include "pl.h"
+#include "play_queue.h"
+#include "cmus.h"
+#include "worker.h"
+#include "keys.h"
+#include "xmalloc.h"
+#include "xstrjoin.h"
+#include "misc.h"
+#include "path.h"
+#include "spawn.h"
+#include "utils.h"
+#include "list.h"
+#include "debug.h"
+#include "load_dir.h"
+#include "help.h"
+#include "op.h"
+#include "mpris.h"
+#include "job.h"
+
+#include <stdlib.h>
+#include <ctype.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <dirent.h>
+
+static struct history cmd_history;
+static char *cmd_history_filename;
+static char *history_search_text = NULL;
+static int arg_expand_cmd = -1;
+
+/* view {{{ */
+
+void view_clear(int view)
+{
+       switch (view) {
+       case TREE_VIEW:
+       case SORTED_VIEW:
+               worker_remove_jobs_by_type(JOB_TYPE_LIB);
+               editable_clear(&lib_editable);
+
+               /* FIXME: make this optional? */
+               lib_clear_store();
+               break;
+       case PLAYLIST_VIEW:
+               pl_clear();
+               break;
+       case QUEUE_VIEW:
+               worker_remove_jobs_by_type(JOB_TYPE_QUEUE);
+               editable_clear(&pq_editable);
+               break;
+       default:
+               info_msg(":clear only works in views 1-4");
+       }
+}
+
+void view_add(int view, char *arg, int prepend)
+{
+       char *tmp, *name;
+       enum file_type ft;
+
+       tmp = expand_filename(arg);
+       ft = cmus_detect_ft(tmp, &name);
+       if (ft == FILE_TYPE_INVALID) {
+               error_msg("adding '%s': %s", tmp, strerror(errno));
+               free(tmp);
+               return;
+       }
+       free(tmp);
+
+       switch (view) {
+       case TREE_VIEW:
+       case SORTED_VIEW:
+               cmus_add(lib_add_track, name, ft, JOB_TYPE_LIB, 0, NULL);
+               break;
+       case PLAYLIST_VIEW:
+               pl_add_file_to_marked_pl(name);
+               break;
+       case QUEUE_VIEW:
+               if (prepend) {
+                       cmus_add(play_queue_prepend, name, ft, JOB_TYPE_QUEUE,
+                                       0, NULL);
+               } else {
+                       cmus_add(play_queue_append, name, ft, JOB_TYPE_QUEUE, 0,
+                                       NULL);
+               }
+               break;
+       default:
+               info_msg(":add only works in views 1-4");
+       }
+       free(name);
+}
+
+static char *view_load_prepare(char *arg)
+{
+       char *name, *tmp = expand_filename(arg);
+       enum file_type ft = cmus_detect_ft(tmp, &name);
+       if (ft == FILE_TYPE_INVALID) {
+               error_msg("loading '%s': %s", tmp, strerror(errno));
+               free(tmp);
+               return NULL;
+       }
+       free(tmp);
+
+       if (ft == FILE_TYPE_FILE)
+               ft = FILE_TYPE_PL;
+       if (ft != FILE_TYPE_PL) {
+               error_msg("loading '%s': not a playlist file", name);
+               free(name);
+               return NULL;
+       }
+       return name;
+}
+
+void view_load(int view, char *arg)
+{
+       char *name = view_load_prepare(arg);
+       if (!name)
+               return;
+
+       switch (view) {
+       case TREE_VIEW:
+       case SORTED_VIEW:
+               worker_remove_jobs_by_type(JOB_TYPE_LIB);
+               editable_clear(&lib_editable);
+               cmus_add(lib_add_track, name, FILE_TYPE_PL, JOB_TYPE_LIB, 0,
+                               NULL);
+               free(lib_filename);
+               lib_filename = name;
+               break;
+       default:
+               info_msg(":load only works in views 1-2");
+               free(name);
+       }
+}
+
+static void do_save(for_each_ti_cb for_each_ti, const char *arg, char **filenamep,
+               save_ti_cb save_ti)
+{
+       char *filename = *filenamep;
+
+       if (arg) {
+               if (strcmp(arg, "-") == 0) {
+                       filename = (char *) arg;
+               } else {
+                       free(filename);
+                       filename = xstrdup(arg);
+                       *filenamep = filename;
+               }
+       } else if (!filename) {
+               error_msg("need a file as argument, no default stored yet");
+               return;
+       }
+
+       if (save_ti(for_each_ti, filename, NULL) == -1)
+               error_msg("saving '%s': %s", filename, strerror(errno));
+}
+
+void view_save(int view, char *arg, int to_stdout, int filtered, int extended)
+{
+       char **dest;
+       save_ti_cb     save_ti         = extended ? cmus_save_ext         : cmus_save;
+       for_each_ti_cb lib_for_each_ti = filtered ? lib_for_each_filtered : lib_for_each;
+
+       if (arg) {
+               if (to_stdout) {
+                       arg = xstrdup(arg);
+               } else {
+                       char *tmp = expand_filename(arg);
+                       arg = path_absolute(tmp);
+                       free(tmp);
+               }
+       }
+
+       switch (view) {
+       case TREE_VIEW:
+       case SORTED_VIEW:
+               if (worker_has_job_by_type(JOB_TYPE_LIB))
+                       goto worker_running;
+               dest = extended ? &lib_ext_filename : &lib_filename;
+               do_save(lib_for_each_ti, arg, dest, save_ti);
+               break;
+       case PLAYLIST_VIEW:
+               pl_save();
+               break;
+       case QUEUE_VIEW:
+               if (worker_has_job_by_type(JOB_TYPE_QUEUE))
+                       goto worker_running;
+               dest = extended ? &play_queue_ext_filename : &play_queue_filename;
+               do_save(play_queue_for_each, arg, dest, save_ti);
+               break;
+       default:
+               info_msg(":save only works in views 1 - 4");
+       }
+       free(arg);
+       return;
+worker_running:
+       error_msg("can't save when tracks are being added");
+       free(arg);
+}
+
+/* }}} */
+
+/* if only_last != 0, only return the last flag */
+static int do_parse_flags(const char **strp, const char *flags, int only_last)
+{
+       const char *str = *strp;
+       int flag = 0;
+
+       if (str == NULL)
+               return flag;
+
+       while (*str && (only_last || !flag)) {
+               if (*str != '-')
+                       break;
+
+               // "-"
+               if (str[1] == 0)
+                       break;
+
+               // "--" or "-- "
+               if (str[1] == '-' && (str[2] == 0 || str[2] == ' ')) {
+                       str += 2;
+                       break;
+               }
+
+               // not "-?" or "-? "
+               if (str[2] && str[2] != ' ')
+                       break;
+
+               flag = str[1];
+               if (!strchr(flags, flag)) {
+                       error_msg("invalid option -%c", flag);
+                       return -1;
+               }
+
+               str += 2;
+
+               while (*str == ' ')
+                       str++;
+       }
+       while (*str == ' ')
+               str++;
+       if (*str == 0)
+               str = NULL;
+       *strp = str;
+       return flag;
+}
+
+static int parse_flags(const char **strp, const char *flags)
+{
+       return do_parse_flags(strp, flags, 1);
+}
+
+static int parse_one_flag(const char **strp, const char *flags)
+{
+       return do_parse_flags(strp, flags, 0);
+}
+
+/* is str == "...-", but not "...-- -" ? copied from do_parse_flags() */
+static int is_stdout_filename(const char *str)
+{
+       if (!str)
+               return 0;
+
+       while (*str) {
+               if (*str != '-')
+                       return 0;
+               // "-"
+               if (str[1] == 0)
+                       return 1;
+               // "--" or "-- "
+               if (str[1] == '-' && (str[2] == 0 || str[2] == ' '))
+                       return 0;
+               // not "-?" or "-? "
+               if (str[2] && str[2] != ' ')
+                       return 0;
+               str += 2;
+               while (*str == ' ')
+                       str++;
+       }
+
+       return 0;
+}
+
+static int flag_to_view(int flag)
+{
+       switch (flag) {
+       case 'l':
+       case 'L':
+               return TREE_VIEW;
+       case 'p':
+               return PLAYLIST_VIEW;
+       case 'q':
+       case 'Q':
+               return QUEUE_VIEW;
+       default:
+               return cur_view;
+       }
+}
+
+struct window *current_win(void)
+{
+       switch (cur_view) {
+       case TREE_VIEW:
+               return lib_cur_win;
+       case SORTED_VIEW:
+               return lib_editable.shared->win;
+       case PLAYLIST_VIEW:
+               return pl_cursor_win();
+       case QUEUE_VIEW:
+               return pq_editable.shared->win;
+       case BROWSER_VIEW:
+               return browser_win;
+       case HELP_VIEW:
+               return help_win;
+       case FILTERS_VIEW:
+       default:
+               return filters_win;
+       }
+}
+
+static void cmd_add(char *arg)
+{
+       int flag = parse_flags((const char **)&arg, "lpqQ");
+
+       if (flag == -1)
+               return;
+       if (arg == NULL) {
+               error_msg("not enough arguments\n");
+               return;
+       }
+       view_add(flag_to_view(flag), arg, flag == 'Q');
+}
+
+static void cmd_clear(char *arg)
+{
+       int flag = parse_flags((const char **)&arg, "lpq");
+
+       if (flag == -1)
+               return;
+       if (arg) {
+               error_msg("too many arguments\n");
+               return;
+       }
+       view_clear(flag_to_view(flag));
+}
+
+static void cmd_load(char *arg)
+{
+       int flag = parse_flags((const char **)&arg, "lp");
+
+       if (flag == -1)
+               return;
+       if (arg == NULL) {
+               error_msg("not enough arguments\n");
+               return;
+       }
+       view_load(flag_to_view(flag), arg);
+}
+
+static void cmd_save(char *arg)
+{
+       int to_stdout = is_stdout_filename(arg);
+       int flag = 0, f, extended = 0;
+
+       do {
+               f = parse_one_flag((const char **)&arg, "eLlpq");
+               if (f == 'e')
+                       extended = 1;
+               else if (f)
+                       flag = f;
+       } while (f > 0);
+
+       if (flag == -1)
+               return;
+       view_save(flag_to_view(flag), arg, to_stdout, flag == 'L', extended);
+}
+
+static void cmd_set(char *arg)
+{
+       char *value = NULL;
+       int i;
+
+       for (i = 0; arg[i]; i++) {
+               if (arg[i] == '=') {
+                       arg[i] = 0;
+                       value = &arg[i + 1];
+                       break;
+               }
+       }
+       if (value) {
+               option_set(arg, value);
+               help_win->changed = 1;
+               if (cur_view == TREE_VIEW) {
+                       lib_track_win->changed = 1;
+                       lib_tree_win->changed = 1;
+               } else if (cur_view == PLAYLIST_VIEW) {
+                       pl_mark_for_redraw();
+               } else {
+                       current_win()->changed = 1;
+               }
+               update_titleline();
+               update_statusline();
+       } else {
+               struct cmus_opt *opt;
+               char buf[OPTION_MAX_SIZE];
+
+               /* support "set <option>?" */
+               i--;
+               if (arg[i] == '?')
+                       arg[i] = 0;
+
+               opt = option_find(arg);
+               if (opt) {
+                       opt->get(opt->data, buf, OPTION_MAX_SIZE);
+                       info_msg("setting: '%s=%s'", arg, buf);
+               }
+       }
+}
+
+static void cmd_toggle(char *arg)
+{
+       struct cmus_opt *opt = option_find(arg);
+
+       if (opt == NULL)
+               return;
+
+       if (opt->toggle == NULL) {
+               error_msg("%s is not toggle option", opt->name);
+               return;
+       }
+       opt->toggle(opt->data);
+       help_win->changed = 1;
+       if (cur_view == TREE_VIEW) {
+               lib_track_win->changed = 1;
+               lib_tree_win->changed = 1;
+       } else if (cur_view == PLAYLIST_VIEW) {
+               pl_mark_for_redraw();
+       } else {
+               current_win()->changed = 1;
+       }
+       update_titleline();
+       update_statusline();
+}
+
+static int get_number(char *str, char **end)
+{
+       int val = 0;
+
+       while (*str >= '0' && *str <= '9') {
+               val *= 10;
+               val += *str++ - '0';
+       }
+       *end = str;
+       return val;
+}
+
+static void cmd_seek(char *arg)
+{
+       int relative = 0;
+       int seek = 0, sign = 1, count;
+
+       switch (*arg) {
+       case '-':
+               sign = -1;
+               /* fallthrough */
+       case '+':
+               relative = 1;
+               arg++;
+               break;
+       }
+
+       count = 0;
+       goto inside;
+
+       do {
+               int num;
+               char *end;
+
+               if (*arg != ':')
+                       break;
+               arg++;
+inside:
+               num = get_number(arg, &end);
+               if (arg == end)
+                       break;
+               arg = end;
+               seek = seek * 60 + num;
+       } while (++count < 3);
+
+       seek *= sign;
+       if (!count)
+               goto err;
+
+       if (count == 1) {
+               switch (tolower((unsigned char)*arg)) {
+               case 'h':
+                       seek *= 60;
+                       /* fallthrough */
+               case 'm':
+                       seek *= 60;
+                       /* fallthrough */
+               case 's':
+                       arg++;
+                       break;
+               }
+       }
+
+       if (!*arg) {
+               player_seek(seek, relative, 0);
+               return;
+       }
+err:
+       error_msg("expecting one argument: [+-]INTEGER[mh] or [+-]H:MM:SS");
+}
+
+static void cmd_factivate(char *arg)
+{
+       filters_activate_names(arg);
+}
+
+static void cmd_live_filter(char *arg)
+{
+       filters_set_live(arg);
+}
+
+static void cmd_filter(char *arg)
+{
+       filters_set_anonymous(arg);
+}
+
+static void cmd_fset(char *arg)
+{
+       filters_set_filter(arg);
+}
+
+static void cmd_help(char *arg)
+{
+       info_msg("To get started with cmus, read cmus-tutorial(7) and cmus(1) man pages");
+}
+
+static void cmd_invert(char *arg)
+{
+       switch (cur_view) {
+       case SORTED_VIEW:
+               editable_invert_marks(&lib_editable);
+               break;
+       case PLAYLIST_VIEW:
+               pl_invert_marks();
+               break;
+       case QUEUE_VIEW:
+               editable_invert_marks(&pq_editable);
+               break;
+       default:
+               info_msg(":invert only works in views 2-4");
+       }
+}
+
+static void cmd_mark(char *arg)
+{
+       switch (cur_view) {
+       case SORTED_VIEW:
+               editable_mark(&lib_editable, arg);
+               break;
+       case PLAYLIST_VIEW:
+               pl_mark(arg);
+               break;
+       case QUEUE_VIEW:
+               editable_mark(&pq_editable, arg);
+               break;
+       default:
+               info_msg(":mark only works in views 2-4");
+       }
+}
+
+static void cmd_unmark(char *arg)
+{
+       switch (cur_view) {
+       case SORTED_VIEW:
+               editable_unmark(&lib_editable);
+               break;
+       case PLAYLIST_VIEW:
+               pl_unmark();
+               break;
+       case QUEUE_VIEW:
+               editable_unmark(&pq_editable);
+               break;
+       default:
+               info_msg(":unmark only works in views 2-4");
+       }
+}
+
+static void cmd_update_cache(char *arg)
+{
+       int flag = parse_flags((const char **)&arg, "f");
+       cmus_update_cache(flag == 'f');
+}
+
+static void cmd_cd(char *arg)
+{
+       if (arg) {
+               char *dir, *absolute;
+
+               dir = expand_filename(arg);
+               absolute = path_absolute(dir);
+               if (chdir(dir) == -1) {
+                       error_msg("could not cd to '%s': %s", dir, strerror(errno));
+               } else {
+                       browser_chdir(absolute);
+               }
+               free(absolute);
+               free(dir);
+       } else {
+               if (chdir(home_dir) == -1) {
+                       error_msg("could not cd to '%s': %s", home_dir, strerror(errno));
+               } else {
+                       browser_chdir(home_dir);
+               }
+       }
+}
+
+static void cmd_bind(char *arg)
+{
+       int flag = parse_flags((const char **)&arg, "f");
+       char *key, *func;
+
+       if (flag == -1)
+               return;
+
+       if (arg == NULL)
+               goto err;
+
+       key = strchr(arg, ' ');
+       if (key == NULL)
+               goto err;
+       *key++ = 0;
+       while (*key == ' ')
+               key++;
+
+       func = strchr(key, ' ');
+       if (func == NULL)
+               goto err;
+       *func++ = 0;
+       while (*func == ' ')
+               func++;
+       if (*func == 0)
+               goto err;
+
+       key_bind(arg, key, func, flag == 'f');
+       if (cur_view == HELP_VIEW)
+               window_changed(help_win);
+       return;
+err:
+       error_msg("expecting 3 arguments (context, key and function)\n");
+}
+
+static void cmd_unbind(char *arg)
+{
+       int flag = parse_flags((const char **)&arg, "f");
+       char *key;
+
+       if (flag == -1)
+               return;
+
+       if (arg == NULL)
+               goto err;
+
+       key = strchr(arg, ' ');
+       if (key == NULL)
+               goto err;
+       *key++ = 0;
+       while (*key == ' ')
+               key++;
+       if (*key == 0)
+               goto err;
+
+       strip_trailing_spaces(key);
+
+       key_unbind(arg, key, flag == 'f');
+       return;
+err:
+       error_msg("expecting 2 arguments (context and key)\n");
+}
+
+static void cmd_showbind(char *arg)
+{
+       char *key;
+
+       key = strchr(arg, ' ');
+       if (key == NULL)
+               goto err;
+       *key++ = 0;
+       while (*key == ' ')
+               key++;
+       if (*key == 0)
+               goto err;
+
+       strip_trailing_spaces(key);
+
+       show_binding(arg, key);
+       return;
+err:
+       error_msg("expecting 2 arguments (context and key)\n");
+}
+
+static void cmd_quit(char *arg)
+{
+       int flag = parse_flags((const char **)&arg, "i");
+       if (!worker_has_job_by_type(JOB_TYPE_ANY)) {
+               if (flag != 'i' || yes_no_query("Quit cmus? [y/N]"))
+                       cmus_running = 0;
+       } else {
+               if (yes_no_query("Tracks are being added. Quit and truncate playlist(s)? [y/N]"))
+                       cmus_running = 0;
+       }
+}
+
+static void cmd_reshuffle(char *arg)
+{
+       lib_reshuffle();
+       pl_reshuffle();
+}
+
+static void cmd_source(char *arg)
+{
+       char *filename = expand_filename(arg);
+
+       if (source_file(filename) == -1)
+               error_msg("sourcing %s: %s", filename, strerror(errno));
+       free(filename);
+}
+
+static void cmd_colorscheme(char *arg)
+{
+       char filename[512];
+
+       snprintf(filename, sizeof(filename), "%s/%s.theme", cmus_config_dir, arg);
+       if (source_file(filename) == -1) {
+               snprintf(filename, sizeof(filename), "%s/%s.theme", cmus_data_dir, arg);
+               if (source_file(filename) == -1)
+                       error_msg("sourcing %s: %s", filename, strerror(errno));
+       }
+}
+
+/*
+ * \" inside double-quotes becomes "
+ * \\ inside double-quotes becomes \
+ */
+static char *parse_quoted(const char **strp)
+{
+       const char *str = *strp;
+       const char *start;
+       char *ret, *dst;
+
+       str++;
+       start = str;
+       while (1) {
+               int c = *str++;
+
+               if (c == 0)
+                       goto error;
+               if (c == '"')
+                       break;
+               if (c == '\\') {
+                       if (*str++ == 0)
+                               goto error;
+               }
+       }
+       *strp = str;
+       ret = xnew(char, str - start);
+       str = start;
+       dst = ret;
+       while (1) {
+               int c = *str++;
+
+               if (c == '"')
+                       break;
+               if (c == '\\') {
+                       c = *str++;
+                       if (c != '"' && c != '\\')
+                               *dst++ = '\\';
+               }
+               *dst++ = c;
+       }
+       *dst = 0;
+       return ret;
+error:
+       error_msg("`\"' expected");
+       return NULL;
+}
+
+static char *parse_escaped(const char **strp)
+{
+       const char *str = *strp;
+       const char *start;
+       char *ret, *dst;
+
+       start = str;
+       while (1) {
+               int c = *str;
+
+               if (c == 0 || c == ' ' || c == '\'' || c == '"')
+                       break;
+
+               str++;
+               if (c == '\\') {
+                       c = *str;
+                       if (c == 0)
+                               break;
+                       str++;
+               }
+       }
+       *strp = str;
+       ret = xnew(char, str - start + 1);
+       str = start;
+       dst = ret;
+       while (1) {
+               int c = *str;
+
+               if (c == 0 || c == ' ' || c == '\'' || c == '"')
+                       break;
+
+               str++;
+               if (c == '\\') {
+                       c = *str;
+                       if (c == 0) {
+                               *dst++ = '\\';
+                               break;
+                       }
+                       str++;
+               }
+               *dst++ = c;
+       }
+       *dst = 0;
+       return ret;
+}
+
+static char *parse_one(const char **strp)
+{
+       const char *str = *strp;
+       char *ret = NULL;
+
+       while (1) {
+               char *part = NULL;
+               int c = *str;
+
+               if (!c || c == ' ')
+                       break;
+               if (c == '"') {
+                       part = parse_quoted(&str);
+                       if (part == NULL)
+                               goto error;
+               } else if (c == '\'') {
+                       /* backslashes are normal chars inside single-quotes */
+                       const char *end;
+
+                       str++;
+                       end = strchr(str, '\'');
+                       if (end == NULL)
+                               goto sq_missing;
+                       part = xstrndup(str, end - str);
+                       str = end + 1;
+               } else {
+                       part = parse_escaped(&str);
+               }
+
+               if (ret == NULL) {
+                       ret = xstrdup(part);
+               } else {
+                       char *tmp = xstrjoin(ret, part);
+                       free(ret);
+                       ret = tmp;
+               }
+               free(part);
+       }
+       *strp = str;
+       return ret;
+sq_missing:
+       error_msg("`'' expected");
+error:
+       free(ret);
+       return NULL;
+}
+
+char **parse_cmd(const char *cmd, int *args_idx, int *ac)
+{
+       char **av = NULL;
+       int nr = 0;
+       int alloc = 0;
+
+       while (*cmd) {
+               char *arg;
+
+               /* there can't be spaces at start of command
+                * and there is at least one argument */
+               if (cmd[0] == '{' && cmd[1] == '}' && (cmd[2] == ' ' || cmd[2] == 0)) {
+                       /* {} is replaced with file arguments */
+                       if (*args_idx != -1)
+                               goto only_once_please;
+                       *args_idx = nr;
+                       cmd += 2;
+                       goto skip_spaces;
+               } else {
+                       arg = parse_one(&cmd);
+                       if (arg == NULL)
+                               goto error;
+               }
+
+               if (nr == alloc) {
+                       alloc = alloc ? alloc * 2 : 4;
+                       av = xrenew(char *, av, alloc + 1);
+               }
+               av[nr++] = arg;
+skip_spaces:
+               while (*cmd == ' ')
+                       cmd++;
+       }
+       av[nr] = NULL;
+       *ac = nr;
+       return av;
+only_once_please:
+       error_msg("{} can be used only once");
+error:
+       while (nr > 0)
+               free(av[--nr]);
+       free(av);
+       return NULL;
+}
+
+struct track_info_selection {
+       struct track_info **tis;
+       int tis_alloc;
+       int tis_nr;
+};
+
+static int add_ti(void *data, struct track_info *ti)
+{
+       struct track_info_selection *sel = data;
+       if (sel->tis_nr == sel->tis_alloc) {
+               sel->tis_alloc = sel->tis_alloc ? sel->tis_alloc * 2 : 8;
+               sel->tis = xrenew(struct track_info *, sel->tis, sel->tis_alloc);
+       }
+       track_info_ref(ti);
+       sel->tis[sel->tis_nr++] = ti;
+       return 0;
+}
+
+static void cmd_run(char *arg)
+{
+       char **av, **argv;
+       int ac, argc, i, run, files_idx = -1;
+       struct track_info_selection sel = { .tis = NULL };
+
+       if (cur_view > QUEUE_VIEW) {
+               info_msg("Command execution is supported only in views 1-4");
+               return;
+       }
+
+       av = parse_cmd(arg, &files_idx, &ac);
+       if (av == NULL) {
+               return;
+       }
+
+       /* collect selected files (struct track_info) */
+       switch (cur_view) {
+       case TREE_VIEW:
+               _tree_for_each_sel(add_ti, &sel, 0);
+               break;
+       case SORTED_VIEW:
+               _editable_for_each_sel(&lib_editable, add_ti, &sel, 0);
+               break;
+       case PLAYLIST_VIEW:
+               _pl_for_each_sel(add_ti, &sel, 0);
+               break;
+       case QUEUE_VIEW:
+               _editable_for_each_sel(&pq_editable, add_ti, &sel, 0);
+               break;
+       }
+
+       if (sel.tis_nr == 0) {
+               /* no files selected, do nothing */
+               free_str_array(av);
+               return;
+       }
+       sel.tis[sel.tis_nr] = NULL;
+
+       /* build argv */
+       argv = xnew(char *, ac + sel.tis_nr + 1);
+       argc = 0;
+       if (files_idx == -1) {
+               /* add selected files after rest of the args */
+               for (i = 0; i < ac; i++)
+                       argv[argc++] = av[i];
+               for (i = 0; i < sel.tis_nr; i++)
+                       argv[argc++] = sel.tis[i]->filename;
+       } else {
+               for (i = 0; i < files_idx; i++)
+                       argv[argc++] = av[i];
+               for (i = 0; i < sel.tis_nr; i++)
+                       argv[argc++] = sel.tis[i]->filename;
+               for (i = files_idx; i < ac; i++)
+                       argv[argc++] = av[i];
+       }
+       argv[argc] = NULL;
+
+       for (i = 0; argv[i]; i++)
+               d_print("ARG: '%s'\n", argv[i]);
+
+       run = 1;
+       if (confirm_run && (sel.tis_nr > 1 || strcmp(argv[0], "rm") == 0)) {
+               if (!yes_no_query("Execute %s for the %d selected files? [y/N]", arg, sel.tis_nr)) {
+                       info_msg("Aborted");
+                       run = 0;
+               }
+       }
+       if (run) {
+               int status;
+
+               if (spawn(argv, &status, 1)) {
+                       error_msg("executing %s: %s", argv[0], strerror(errno));
+               } else {
+                       if (WIFEXITED(status)) {
+                               int rc = WEXITSTATUS(status);
+
+                               if (rc)
+                                       error_msg("%s returned %d", argv[0], rc);
+                       }
+                       if (WIFSIGNALED(status))
+                               error_msg("%s received signal %d", argv[0], WTERMSIG(status));
+
+                       switch (cur_view) {
+                       case TREE_VIEW:
+                       case SORTED_VIEW:
+                               /* this must be done before sel.tis are unreffed */
+                               free_str_array(av);
+                               free(argv);
+
+                               /* remove non-existed files, update tags for changed files */
+                               cmus_update_tis(sel.tis, sel.tis_nr, 0);
+
+                               /* we don't own sel.tis anymore! */
+                               return;
+                       }
+               }
+       }
+       free_str_array(av);
+       free(argv);
+       for (i = 0; sel.tis[i]; i++)
+               track_info_unref(sel.tis[i]);
+       free(sel.tis);
+}
+
+static void cmd_shell(char *arg)
+{
+       const char * const argv[] = { "sh", "-c", arg, NULL };
+
+       if (spawn((char **) argv, NULL, 0))
+               error_msg("executing '%s': %s", arg, strerror(errno));
+}
+
+static int get_one_ti(void *data, struct track_info *ti)
+{
+       struct track_info **sel_ti = data;
+
+       track_info_ref(ti);
+       *sel_ti = ti;
+       /* stop the for each loop, we need only the first selected track */
+       return 1;
+}
+
+static void cmd_echo(char *arg)
+{
+       struct track_info *sel_ti;
+       char *ptr = arg;
+
+       while (1) {
+               ptr = strchr(ptr, '{');
+               if (ptr == NULL)
+                       break;
+               if (ptr[1] == '}')
+                       break;
+               ptr++;
+       }
+
+       if (ptr == NULL) {
+               info_msg("%s", arg);
+               return;
+       }
+
+       if (cur_view > QUEUE_VIEW) {
+               info_msg("echo with {} in its arguments is supported only in views 1-4");
+               return;
+       }
+
+       *ptr = 0;
+       ptr += 2;
+
+       /* get only the first selected track */
+       sel_ti = NULL;
+
+       switch (cur_view) {
+       case TREE_VIEW:
+               _tree_for_each_sel(get_one_ti, &sel_ti, 0);
+               break;
+       case SORTED_VIEW:
+               _editable_for_each_sel(&lib_editable, get_one_ti, &sel_ti, 0);
+               break;
+       case PLAYLIST_VIEW:
+               _pl_for_each_sel(get_one_ti, &sel_ti, 0);
+               break;
+       case QUEUE_VIEW:
+               _editable_for_each_sel(&pq_editable, get_one_ti, &sel_ti, 0);
+               break;
+       }
+
+       if (sel_ti == NULL)
+               return;
+
+       info_msg("%s%s%s", arg, sel_ti->filename, ptr);
+       track_info_unref(sel_ti);
+}
+
+static int parse_vol_arg(const char *arg, int *value, unsigned int *flags)
+{
+       unsigned int f = 0;
+       int ch, val = 0, digits = 0, sign = 1;
+
+       if (*arg == '-') {
+               arg++;
+               f |= VF_RELATIVE;
+               sign = -1;
+       } else if (*arg == '+') {
+               arg++;
+               f |= VF_RELATIVE;
+       }
+
+       while (1) {
+               ch = *arg++;
+               if (ch < '0' || ch > '9')
+                       break;
+               val *= 10;
+               val += ch - '0';
+               digits++;
+       }
+       if (digits == 0)
+               goto err;
+
+       if (ch == '%') {
+               f |= VF_PERCENTAGE;
+               ch = *arg;
+       }
+       if (ch)
+               goto err;
+
+       *value = sign * val;
+       *flags = f;
+       return 0;
+err:
+       return -1;
+}
+
+/*
+ * :vol value [value]
+ *
+ * where value is [-+]?[0-9]+%?
+ */
+static void cmd_vol(char *arg)
+{
+       char **values = get_words(arg);
+       unsigned int lf, rf;
+       int l, r;
+
+       if (values[1] && values[2])
+               goto err;
+
+       if (parse_vol_arg(values[0], &l, &lf))
+               goto err;
+
+       r = l;
+       rf = lf;
+       if (values[1] && parse_vol_arg(values[1], &r, &rf))
+               goto err;
+
+       free_str_array(values);
+
+       int rc = player_set_vol(l, lf, r, rf);
+       if (rc != OP_ERROR_SUCCESS) {
+               char *msg = op_get_error_msg(rc, "can't change volume");
+               error_msg("%s", msg);
+               free(msg);
+       } else {
+               mpris_volume_changed();
+       }
+       update_statusline();
+       return;
+err:
+       free_str_array(values);
+       error_msg("expecting 1 or 2 arguments (total or L and R volumes [+-]INTEGER[%%])\n");
+}
+
+static void cmd_prev_view(char *arg)
+{
+       if (prev_view >= 0) {
+               set_view(prev_view);
+       }
+}
+
+static void cmd_left_view(char *arg)
+{
+       if (cur_view == TREE_VIEW) {
+               set_view(HELP_VIEW);
+       } else {
+               set_view(cur_view - 1);
+       }
+}
+
+static void cmd_right_view(char *arg)
+{
+       if (cur_view == HELP_VIEW) {
+               set_view(TREE_VIEW);
+       } else {
+               set_view(cur_view + 1);
+       }
+}
+
+static void cmd_pl_create(char *arg)
+{
+       pl_create(arg);
+}
+
+static void cmd_pl_export(char *arg)
+{
+       if (cur_view == PLAYLIST_VIEW)
+               pl_export_selected_pl(arg);
+       else
+               info_msg(":pl-export only works in view 3");
+}
+
+static char *get_browser_add_file(void)
+{
+       char *sel = browser_get_sel();
+
+       if (sel && (ends_with(sel, "/../") || ends_with(sel, "/.."))) {
+               info_msg("For convenience, you can not add \"..\" directory from the browser view");
+               free(sel);
+               sel = NULL;
+       }
+
+       return sel;
+}
+
+static void cmd_pl_import(char *arg)
+{
+       char *name = NULL;
+
+       if (arg)
+               name = view_load_prepare(arg);
+       else if (cur_view == BROWSER_VIEW)
+               name = get_browser_add_file();
+       else
+               error_msg("not enough arguments");
+
+       if (name) {
+               pl_import(name);
+               free(name);
+       }
+}
+
+static void cmd_pl_rename(char *arg)
+{
+       if (cur_view == PLAYLIST_VIEW)
+               pl_rename_selected_pl(arg);
+       else
+               info_msg(":pl-rename only works in view 3");
+}
+
+static void cmd_version(char *arg)
+{
+       info_msg(VERSION);
+}
+
+static void cmd_view(char *arg)
+{
+       int view;
+
+       if (parse_enum(arg, 1, NR_VIEWS, view_names, &view) && (view - 1) != cur_view) {
+               set_view(view - 1);
+       }
+}
+
+static void cmd_push(char *arg)
+{
+       cmdline_set_text(arg);
+       enter_command_mode();
+}
+
+static void cmd_p_next(char *arg)
+{
+       cmus_next();
+}
+
+static void cmd_p_pause(char *arg)
+{
+       player_pause();
+}
+
+static void cmd_p_pause_playback(char *arg)
+{
+       player_pause_playback();
+}
+
+static void cmd_p_play(char *arg)
+{
+       if (arg) {
+               char *tmp = expand_filename(arg);
+               cmus_play_file(tmp);
+               free(tmp);
+       } else {
+               player_play();
+       }
+}
+
+static void cmd_p_prev(char *arg)
+{
+       if (rewind_offset < 0 || player_info.pos < rewind_offset) {
+               cmus_prev();
+       } else {
+               player_play();
+       }
+}
+
+static void cmd_p_stop(char *arg)
+{
+       player_stop();
+}
+
+static void cmd_pwd(char *arg)
+{
+       char buf[4096];
+       if (getcwd(buf, sizeof buf)) {
+               info_msg("%s", buf);
+       }
+}
+
+static void cmd_raise_vte(char *arg)
+{
+       cmus_raise_vte();
+}
+
+static void cmd_rand(char *arg)
+{
+       switch (cur_view) {
+       case TREE_VIEW:
+               break;
+       case SORTED_VIEW:
+               editable_rand(&lib_editable);
+               break;
+       case PLAYLIST_VIEW:
+               pl_rand();
+               break;
+       case QUEUE_VIEW:
+               editable_rand(&pq_editable);
+               break;
+       }
+}
+
+static void cmd_search_next(char *arg)
+{
+       if (search_str) {
+               if (!search_next(searchable, search_str, search_direction))
+                       search_not_found();
+       }
+}
+
+static void cmd_search_prev(char *arg)
+{
+       if (search_str) {
+               if (!search_next(searchable, search_str, !search_direction))
+                       search_not_found();
+       }
+}
+
+static void cmd_search_start(char *arg)
+{
+       enter_search_mode();
+}
+
+static void cmd_search_b_start(char *arg)
+{
+       enter_search_backward_mode();
+}
+
+static int sorted_for_each_sel(track_info_cb cb, void *data, int reverse)
+{
+       return editable_for_each_sel(&lib_editable, cb, data, reverse);
+}
+
+static int pq_for_each_sel(track_info_cb cb, void *data, int reverse)
+{
+       return editable_for_each_sel(&pq_editable, cb, data, reverse);
+}
+
+static for_each_sel_ti_cb view_for_each_sel[4] = {
+       tree_for_each_sel,
+       sorted_for_each_sel,
+       pl_for_each_sel,
+       pq_for_each_sel
+};
+
+/* wrapper for add_ti_cb, (void *) can't store function pointers */
+struct wrapper_cb_data {
+       add_ti_cb cb;
+};
+
+/* wrapper for void lib_add_track(struct track_info *) etc. */
+static int wrapper_cb(void *data, struct track_info *ti)
+{
+       struct wrapper_cb_data *add = data;
+
+       add->cb(ti, NULL);
+       return 0;
+}
+
+static void add_from_browser(add_ti_cb add, int job_type)
+{
+       char *sel = get_browser_add_file();
+
+       if (sel) {
+               enum file_type ft;
+               char *ret;
+
+               ft = cmus_detect_ft(sel, &ret);
+               if (ft != FILE_TYPE_INVALID) {
+                       cmus_add(add, ret, ft, job_type, 0, NULL);
+                       window_down(browser_win, 1);
+               }
+               free(ret);
+               free(sel);
+       }
+}
+
+static void cmd_win_add_l(char *arg)
+{
+       if (cur_view == TREE_VIEW || cur_view == SORTED_VIEW)
+               return;
+
+       if (cur_view <= QUEUE_VIEW) {
+               struct wrapper_cb_data add = { lib_add_track };
+               view_for_each_sel[cur_view](wrapper_cb, &add, 0);
+       } else if (cur_view == BROWSER_VIEW) {
+               add_from_browser(lib_add_track, JOB_TYPE_LIB);
+       }
+}
+
+static void cmd_win_add_p(char *arg)
+{
+       if (cur_view == PLAYLIST_VIEW && pl_visible_is_marked())
+               return;
+
+       if (cur_view <= QUEUE_VIEW) {
+               struct wrapper_cb_data add = { pl_add_track_to_marked_pl2 };
+               view_for_each_sel[cur_view](wrapper_cb, &add, 0);
+       } else if (cur_view == BROWSER_VIEW) {
+               char *sel = get_browser_add_file();
+               if (sel) {
+                       if (pl_add_file_to_marked_pl(sel))
+                               window_down(browser_win, 1);
+                       free(sel);
+               }
+       }
+}
+
+static void cmd_win_add_Q(char *arg)
+{
+       if (cur_view == QUEUE_VIEW)
+               return;
+
+       if (cur_view <= QUEUE_VIEW) {
+               struct wrapper_cb_data add = { play_queue_prepend };
+               view_for_each_sel[cur_view](wrapper_cb, &add, 1);
+       } else if (cur_view == BROWSER_VIEW) {
+               add_from_browser(play_queue_prepend, JOB_TYPE_QUEUE);
+       }
+}
+
+static void cmd_win_add_q(char *arg)
+{
+       if (cur_view == QUEUE_VIEW)
+               return;
+
+       if (cur_view <= QUEUE_VIEW) {
+               struct wrapper_cb_data add = { play_queue_append };
+               view_for_each_sel[cur_view](wrapper_cb, &add, 0);
+       } else if (cur_view == BROWSER_VIEW) {
+               add_from_browser(play_queue_append, JOB_TYPE_QUEUE);
+       }
+}
+
+static void cmd_win_activate(char *arg)
+{
+       struct track_info *info = NULL;
+       struct shuffle_track *previous = NULL, *next = NULL;
+       struct rb_root *shuffle_root = NULL;
+
+       if (cur_view == TREE_VIEW || cur_view == SORTED_VIEW) {
+               if (lib_cur_track)
+                       previous = &lib_cur_track->shuffle_track;
+               shuffle_root = &lib_shuffle_root;
+       }
+
+       switch (cur_view) {
+       case TREE_VIEW:
+               info = tree_activate_selected();
+               next = &lib_cur_track->shuffle_track;
+               break;
+       case SORTED_VIEW:
+               info = sorted_activate_selected();
+               next = &lib_cur_track->shuffle_track;
+               break;
+       case PLAYLIST_VIEW:
+               info = pl_play_selected_row();
+               break;
+       case QUEUE_VIEW:
+               break;
+       case BROWSER_VIEW:
+               browser_enter();
+               break;
+       case FILTERS_VIEW:
+               filters_activate(1);
+               break;
+       case HELP_VIEW:
+               help_select();
+               break;
+       }
+
+       if (info) {
+               if (shuffle && next)
+                       shuffle_insert(shuffle_root, previous, next);
+               /* update lib/pl mode */
+               if (cur_view < 2)
+                       play_library = 1;
+               if (cur_view == 2)
+                       play_library = 0;
+
+               player_play_file(info);
+       }
+}
+
+static void cmd_win_mv_after(char *arg)
+{
+       switch (cur_view) {
+       case SORTED_VIEW:
+               editable_move_after(&lib_editable);
+               break;
+       case PLAYLIST_VIEW:
+               pl_win_mv_after();
+               break;
+       case QUEUE_VIEW:
+               editable_move_after(&pq_editable);
+               break;
+       }
+}
+
+static void cmd_win_mv_before(char *arg)
+{
+       switch (cur_view) {
+       case SORTED_VIEW:
+               editable_move_before(&lib_editable);
+               break;
+       case PLAYLIST_VIEW:
+               pl_win_mv_before();
+               break;
+       case QUEUE_VIEW:
+               editable_move_before(&pq_editable);
+               break;
+       }
+}
+
+static void cmd_win_remove(char *arg)
+{
+       switch (cur_view) {
+       case TREE_VIEW:
+               tree_remove_sel();
+               break;
+       case SORTED_VIEW:
+               editable_remove_sel(&lib_editable);
+               break;
+       case PLAYLIST_VIEW:
+               pl_win_remove();
+               break;
+       case QUEUE_VIEW:
+               editable_remove_sel(&pq_editable);
+               break;
+       case BROWSER_VIEW:
+               browser_delete();
+               break;
+       case FILTERS_VIEW:
+               filters_delete_filter();
+               break;
+       case HELP_VIEW:
+               help_remove();
+               break;
+       }
+}
+
+static void cmd_win_sel_cur(char *arg)
+{
+       switch (cur_view) {
+       case TREE_VIEW:
+               tree_sel_current(auto_expand_albums_selcur);
+               break;
+       case SORTED_VIEW:
+               sorted_sel_current();
+               break;
+       case PLAYLIST_VIEW:
+               pl_select_playing_track();
+               break;
+       }
+}
+
+static void cmd_win_toggle(char *arg)
+{
+       switch (cur_view) {
+       case TREE_VIEW:
+               tree_toggle_expand_artist();
+               break;
+       case SORTED_VIEW:
+               editable_toggle_mark(&lib_editable);
+               break;
+       case PLAYLIST_VIEW:
+               pl_win_toggle();
+               break;
+       case QUEUE_VIEW:
+               editable_toggle_mark(&pq_editable);
+               break;
+       case FILTERS_VIEW:
+               filters_toggle_filter();
+               break;
+       case HELP_VIEW:
+               help_toggle();
+               break;
+       }
+}
+
+static void cmd_win_scroll_down(char *arg)
+{
+       window_scroll_down(current_win());
+}
+
+static void cmd_win_scroll_up(char *arg)
+{
+       window_scroll_up(current_win());
+}
+
+static void cmd_win_bottom(char *arg)
+{
+       window_goto_bottom(current_win());
+}
+
+static void cmd_win_down(char *arg)
+{
+       unsigned num_rows = 1;
+       char *end;
+
+       if (arg) {
+               if ((num_rows = get_number(arg, &end)) == 0 || *end) {
+                       error_msg("invalid argument\n");
+                       return;
+               }
+       }
+
+       window_down(current_win(), num_rows);
+}
+
+static void cmd_win_next(char *arg)
+{
+       if (cur_view == TREE_VIEW)
+               tree_toggle_active_window();
+       else if (cur_view == PLAYLIST_VIEW)
+               pl_win_next();
+}
+
+static void cmd_win_pg_down(char *arg)
+{
+       window_page_down(current_win());
+}
+
+static void cmd_win_pg_up(char *arg)
+{
+       window_page_up(current_win());
+}
+
+static void cmd_win_hf_pg_down(char *arg)
+{
+       window_half_page_down(current_win());
+}
+
+static void cmd_win_hf_pg_up(char *arg)
+{
+       window_half_page_up(current_win());
+}
+
+static void cmd_win_pg_top(char *arg)
+{
+       window_page_top(current_win());
+}
+
+static void cmd_win_pg_bottom(char *arg)
+{
+       window_page_bottom(current_win());
+}
+
+static void cmd_win_pg_middle(char *arg)
+{
+       window_page_middle(current_win());
+}
+
+static void cmd_win_update_cache(char *arg)
+{
+       struct track_info_selection sel = { .tis = NULL };
+       int flag = parse_flags((const char **)&arg, "f");
+
+       if (cur_view != TREE_VIEW && cur_view != SORTED_VIEW)
+               return;
+
+       view_for_each_sel[cur_view](add_ti, &sel, 0);
+       if (sel.tis_nr == 0)
+               return;
+       sel.tis[sel.tis_nr] = NULL;
+       cmus_update_tis(sel.tis, sel.tis_nr, flag == 'f');
+}
+
+static void cmd_win_top(char *arg)
+{
+       window_goto_top(current_win());
+}
+
+static void cmd_win_up(char *arg)
+{
+       unsigned num_rows = 1;
+       char *end;
+
+       if (arg) {
+               if ((num_rows = get_number(arg, &end)) == 0 || *end) {
+                       error_msg("invalid argument\n");
+                       return;
+               }
+       }
+
+       window_up(current_win(), num_rows);
+}
+
+static void cmd_win_update(char *arg)
+{
+       switch (cur_view) {
+       case TREE_VIEW:
+       case SORTED_VIEW:
+               cmus_update_lib();
+               break;
+       case PLAYLIST_VIEW:
+               pl_win_update();
+               break;
+       case BROWSER_VIEW:
+               browser_reload();
+               break;
+       }
+}
+
+static void cmd_browser_up(char *arg)
+{
+       browser_up();
+}
+
+static void cmd_refresh(char *arg)
+{
+       clearok(curscr, TRUE);
+       refresh();
+}
+
+static int cmp_intp(const void *ap, const void *bp)
+{
+       int a = *(int *)ap;
+       int b = *(int *)bp;
+       return a - b;
+}
+
+static int *rand_array(int size, int nmax)
+{
+       int *r = xnew(int, size + 1);
+       int i, offset = 0;
+       int count = size;
+
+       if (count > nmax / 2) {
+               /*
+                * Imagine that there are 1000 tracks in library and we want to
+                * add 998 random tracks to queue.  After we have added 997
+                * random numbers to the array it would be quite hard to find a
+                * random number that isn't already in the array (3/1000
+                * probability).
+                *
+                * So we invert the logic:
+                *
+                * Find two (1000 - 998) random numbers in 0..999 range and put
+                * them at end of the array.  Sort the numbers and then fill
+                * the array starting at index 0 with incrementing values that
+                * are not in the set of random numbers.
+                */
+               count = nmax - count;
+               offset = size - count;
+       }
+
+       for (i = 0; i < count; ) {
+               int v, j;
+found:
+               v = rand() % nmax;
+               for (j = 0; j < i; j++) {
+                       if (r[offset + j] == v)
+                               goto found;
+               }
+               r[offset + i++] = v;
+       }
+       qsort(r + offset, count, sizeof(*r), cmp_intp);
+
+       if (offset) {
+               int j, n;
+
+               /* simplifies next loop */
+               r[size] = nmax;
+
+               /* convert the indexes we don't want to those we want */
+               i = 0;
+               j = offset;
+               n = 0;
+               do {
+                       while (n < r[j])
+                               r[i++] = n++;
+                       j++;
+                       n++;
+               } while (i < size);
+       }
+       return r;
+}
+
+static int count_albums(void)
+{
+       struct artist *artist;
+       struct rb_node *tmp1, *tmp2;
+       int count = 0;
+
+       rb_for_each_entry(artist, tmp1, &lib_artist_root, tree_node) {
+               rb_for_each(tmp2, &artist->album_root)
+                       count++;
+       }
+       return count;
+}
+
+struct album_list {
+       struct list_head node;
+       const struct album *album;
+};
+
+static void cmd_lqueue(char *arg)
+{
+       LIST_HEAD(head);
+       const struct list_head *item;
+       const struct album *album;
+       int count = 1, nmax, i, pos;
+       int *r;
+
+       if (arg) {
+               long int val;
+
+               if (str_to_int(arg, &val) || val <= 0) {
+                       error_msg("argument must be positive integer");
+                       return;
+               }
+               count = val;
+       }
+       nmax = count_albums();
+       if (count > nmax)
+               count = nmax;
+       if (!count)
+               return;
+
+       r = rand_array(count, nmax);
+       album = to_album(rb_first(&to_artist(rb_first(&lib_artist_root))->album_root));
+       pos = 0;
+       for (i = 0; i < count; i++) {
+               struct album_list *a;
+
+               while (pos < r[i]) {
+                       struct artist *artist = album->artist;
+                       if (!rb_next(&album->tree_node)) {
+                               artist = to_artist(rb_next(&artist->tree_node));
+                               album = to_album(rb_first(&artist->album_root));
+                       } else {
+                               album = to_album(rb_next(&album->tree_node));
+                       }
+                       pos++;
+               }
+               a = xnew(struct album_list, 1);
+               a->album = album;
+               list_add_rand(&head, &a->node, i);
+       }
+       free(r);
+
+       item = head.next;
+       do {
+               struct list_head *next = item->next;
+               struct album_list *a = container_of(item, struct album_list, node);
+               struct tree_track *t;
+               struct rb_node *tmp;
+
+               rb_for_each_entry(t, tmp, &a->album->track_root, tree_node)
+                       play_queue_append(tree_track_info(t), NULL);
+               free(a);
+               item = next;
+       } while (item != &head);
+}
+
+struct track_list {
+       struct list_head node;
+       const struct simple_track *track;
+};
+
+static void cmd_tqueue(char *arg)
+{
+       LIST_HEAD(head);
+       struct list_head *item;
+       int count = 1, i, pos;
+       int *r;
+
+       if (arg) {
+               long int val;
+
+               if (str_to_int(arg, &val) || val <= 0) {
+                       error_msg("argument must be positive integer");
+                       return;
+               }
+               count = val;
+       }
+       if (count > lib_editable.nr_tracks)
+               count = lib_editable.nr_tracks;
+       if (!count)
+               return;
+
+       r = rand_array(count, lib_editable.nr_tracks);
+       item = lib_editable.head.next;
+       pos = 0;
+       for (i = 0; i < count; i++) {
+               struct track_list *t;
+
+               while (pos < r[i]) {
+                       item = item->next;
+                       pos++;
+               }
+               t = xnew(struct track_list, 1);
+               t->track = to_simple_track(item);
+               list_add_rand(&head, &t->node, i);
+       }
+       free(r);
+
+       item = head.next;
+       do {
+               struct list_head *next = item->next;
+               struct track_list *t = container_of(item, struct track_list, node);
+               play_queue_append(t->track->info, NULL);
+               free(t);
+               item = next;
+       } while (item != &head);
+}
+
+/* tab exp {{{
+ *
+ * these functions fill tabexp struct, which is resetted beforehand
+ */
+
+/* buffer used for tab expansion */
+static char expbuf[512];
+
+static int filter_directories(const char *name, const struct stat *s)
+{
+       return S_ISDIR(s->st_mode);
+}
+
+static int filter_executable_files(const char *name, const struct stat *s)
+{
+       return S_ISREG(s->st_mode) && (s->st_mode & 0111);
+}
+
+static int filter_any(const char *name, const struct stat *s)
+{
+       return 1;
+}
+
+static int filter_playable(const char *name, const struct stat *s)
+{
+       return S_ISDIR(s->st_mode) || cmus_is_playable(name);
+}
+
+static int filter_playlist(const char *name, const struct stat *s)
+{
+       return S_ISDIR(s->st_mode) || cmus_is_playlist(name);
+}
+
+static int filter_supported(const char *name, const struct stat *s)
+{
+       return S_ISDIR(s->st_mode) || cmus_is_supported(name);
+}
+
+static void expand_files(const char *str)
+{
+       expand_files_and_dirs(str, filter_any);
+}
+
+static void expand_directories(const char *str)
+{
+       expand_files_and_dirs(str, filter_directories);
+}
+
+static void expand_playable(const char *str)
+{
+       expand_files_and_dirs(str, filter_playable);
+}
+
+static void expand_playlist(const char *str)
+{
+       expand_files_and_dirs(str, filter_playlist);
+}
+
+static void expand_supported(const char *str)
+{
+       expand_files_and_dirs(str, filter_supported);
+}
+
+static void expand_add(const char *str)
+{
+       int flag = parse_flags(&str, "lpqQ");
+
+       if (flag == -1)
+               return;
+       if (str == NULL)
+               str = "";
+       expand_supported(str);
+
+       if (tabexp.head && flag) {
+               snprintf(expbuf, sizeof(expbuf), "-%c %s", flag, tabexp.head);
+               free(tabexp.head);
+               tabexp.head = xstrdup(expbuf);
+       }
+}
+
+static void expand_program_paths(const char *str)
+{
+       if (str == NULL)
+               str = "";
+       if (str[0] == '~' || strchr(str, '/'))
+               expand_files(str);
+       else
+               expand_env_path(str, filter_executable_files);
+}
+
+static void expand_program_paths_option(const char *str, const char *opt)
+{
+       expand_program_paths(str);
+
+       if (tabexp.head && opt) {
+               snprintf(expbuf, sizeof(expbuf), "%s=%s", opt, tabexp.head);
+               free(tabexp.head);
+               tabexp.head = xstrdup(expbuf);
+       }
+}
+
+static void expand_load_save(const char *str)
+{
+       int flag = parse_flags(&str, "lp");
+
+       if (flag == -1)
+               return;
+       if (str == NULL)
+               str = "";
+       expand_playlist(str);
+
+       if (tabexp.head && flag) {
+               snprintf(expbuf, sizeof(expbuf), "-%c %s", flag, tabexp.head);
+               free(tabexp.head);
+               tabexp.head = xstrdup(expbuf);
+       }
+}
+
+static void expand_key_context(const char *str, const char *force)
+{
+       int pos, i, len = strlen(str);
+       char **tails;
+
+       tails = xnew(char *, NR_CTXS);
+       pos = 0;
+       for (i = 0; key_context_names[i]; i++) {
+               int cmp = strncmp(str, key_context_names[i], len);
+               if (cmp > 0)
+                       continue;
+               if (cmp < 0)
+                       break;
+               tails[pos++] = xstrdup(key_context_names[i] + len);
+       }
+
+       if (pos == 0) {
+               free(tails);
+               return;
+       }
+       if (pos == 1) {
+               char *tmp = xstrjoin(tails[0], " ");
+               free(tails[0]);
+               tails[0] = tmp;
+       }
+       snprintf(expbuf, sizeof(expbuf), "%s%s", force, str);
+       tabexp.head = xstrdup(expbuf);
+       tabexp.tails = tails;
+       tabexp.count = pos;
+}
+
+static int get_context(const char *str, int len)
+{
+       int i, c = -1, count = 0;
+
+       for (i = 0; key_context_names[i]; i++) {
+               if (strncmp(str, key_context_names[i], len) == 0) {
+                       if (key_context_names[i][len] == 0) {
+                               /* exact */
+                               return i;
+                       }
+                       c = i;
+                       count++;
+               }
+       }
+       if (count == 1)
+               return c;
+       return -1;
+}
+
+static void expand_command_line(const char *str);
+
+static void expand_bind_args(const char *str)
+{
+       /* :bind context key function
+        *
+        * possible values for str:
+        *   c
+        *   context k
+        *   context key f
+        *
+        * you need to know context before you can expand function
+        */
+       /* start and end pointers for context, key and function */
+       const char *cs, *ce, *ks, *ke, *fs;
+       int i, c, k, count;
+       int flag = parse_flags((const char **)&str, "f");
+       const char *force = "";
+
+       if (flag == -1)
+               return;
+       if (str == NULL)
+               str = "";
+
+       if (flag == 'f')
+               force = "-f ";
+
+       cs = str;
+       ce = strchr(cs, ' ');
+       if (ce == NULL) {
+               expand_key_context(cs, force);
+               return;
+       }
+
+       /* context must be expandable */
+       c = get_context(cs, ce - cs);
+       if (c == -1) {
+               /* context is ambiguous or invalid */
+               return;
+       }
+
+       ks = ce;
+       while (*ks == ' ')
+               ks++;
+       ke = strchr(ks, ' ');
+       if (ke == NULL) {
+               /* expand key */
+               int len = strlen(ks);
+               PTR_ARRAY(array);
+
+               for (i = 0; key_table[i].name; i++) {
+                       int cmp = strncmp(ks, key_table[i].name, len);
+                       if (cmp > 0)
+                               continue;
+                       if (cmp < 0)
+                               break;
+                       ptr_array_add(&array, xstrdup(key_table[i].name + len));
+               }
+
+               if (!array.count)
+                       return;
+
+               if (array.count == 1) {
+                       char **ptrs = array.ptrs;
+                       char *tmp = xstrjoin(ptrs[0], " ");
+                       free(ptrs[0]);
+                       ptrs[0] = tmp;
+               }
+
+               snprintf(expbuf, sizeof(expbuf), "%s%s %s", force, key_context_names[c], ks);
+
+               tabexp.head = xstrdup(expbuf);
+               tabexp.tails = array.ptrs;
+               tabexp.count = array.count;
+               return;
+       }
+
+       /* key must be expandable */
+       k = -1;
+       count = 0;
+       for (i = 0; key_table[i].name; i++) {
+               if (strncmp(ks, key_table[i].name, ke - ks) == 0) {
+                       if (key_table[i].name[ke - ks] == 0) {
+                               /* exact */
+                               k = i;
+                               count = 1;
+                               break;
+                       }
+                       k = i;
+                       count++;
+               }
+       }
+       if (count != 1) {
+               /* key is ambiguous or invalid */
+               return;
+       }
+
+       fs = ke;
+       while (*fs == ' ')
+               fs++;
+
+       if (*fs == ':')
+               fs++;
+
+       /* expand com [arg...] */
+       expand_command_line(fs);
+       if (tabexp.head == NULL) {
+               /* command expand failed */
+               return;
+       }
+
+       /*
+        * tabexp.head is now "com"
+        * tabexp.tails is [ mand1 mand2 ... ]
+        *
+        * need to change tabexp.head to "context key com"
+        */
+
+       snprintf(expbuf, sizeof(expbuf), "%s%s %s %s", force, key_context_names[c],
+                       key_table[k].name, tabexp.head);
+       free(tabexp.head);
+       tabexp.head = xstrdup(expbuf);
+}
+
+static void expand_unbind_args(const char *str)
+{
+       /* :unbind context key */
+       /* start and end pointers for context and key */
+       const char *cs, *ce, *ks;
+       const struct binding *b;
+       PTR_ARRAY(array);
+       int c, len;
+
+       cs = str;
+       ce = strchr(cs, ' ');
+       if (ce == NULL) {
+               expand_key_context(cs, "");
+               return;
+       }
+
+       /* context must be expandable */
+       c = get_context(cs, ce - cs);
+       if (c == -1) {
+               /* context is ambiguous or invalid */
+               return;
+       }
+
+       ks = ce;
+       while (*ks == ' ')
+               ks++;
+
+       /* expand key */
+       len = strlen(ks);
+       b = key_bindings[c];
+       while (b) {
+               if (!strncmp(ks, b->key->name, len))
+                       ptr_array_add(&array, xstrdup(b->key->name + len));
+               b = b->next;
+       }
+       if (!array.count)
+               return;
+
+       snprintf(expbuf, sizeof(expbuf), "%s %s", key_context_names[c], ks);
+
+       tabexp.head = xstrdup(expbuf);
+       tabexp.tails = array.ptrs;
+       tabexp.count = array.count;
+}
+
+static void expand_factivate(const char *str)
+{
+       /* "name1 name2 name3", expand only name3 */
+       struct filter_entry *e;
+       const char *name;
+       PTR_ARRAY(array);
+       int str_len, len, i;
+
+       str_len = strlen(str);
+       i = str_len;
+       while (i > 0) {
+               if (str[i - 1] == ' ')
+                       break;
+               i--;
+       }
+       len = str_len - i;
+       name = str + i;
+
+       list_for_each_entry(e, &filters_head, node) {
+               if (!strncmp(name, e->name, len))
+                       ptr_array_add(&array, xstrdup(e->name + len));
+       }
+       if (!array.count)
+               return;
+
+       tabexp.head = xstrdup(str);
+       tabexp.tails = array.ptrs;
+       tabexp.count = array.count;
+}
+
+static void expand_fset(const char *str)
+{
+       struct filter_entry *e;
+       PTR_ARRAY(array);
+
+       list_for_each_entry(e, &filters_head, node) {
+               char *line = xnew(char, strlen(e->name) + strlen(e->filter) + 2);
+               sprintf(line, "%s=%s", e->name, e->filter);
+               if (!strncmp(str, line, strlen(str)))
+                       ptr_array_add(&array, xstrdup(line + strlen(str)));
+               free(line);
+       }
+       if (!array.count)
+               return;
+
+       tabexp.head = xstrdup(str);
+       tabexp.tails = array.ptrs;
+       tabexp.count = array.count;
+}
+
+static void expand_options(const char *str)
+{
+       struct cmus_opt *opt;
+       int len;
+       char **tails, *sep;
+
+       /* tabexp is resetted */
+       len = strlen(str);
+       sep = strchr(str, '=');
+       if (len > 1 && sep) {
+               /* expand value */
+               char *var = xstrndup(str, sep - str);
+
+               list_for_each_entry(opt, &option_head, node) {
+                       if (strcmp(var, opt->name) == 0) {
+                               if (str[len - 1] == '=') {
+                                       char buf[OPTION_MAX_SIZE];
+
+                                       tails = xnew(char *, 1);
+
+                                       buf[0] = 0;
+                                       opt->get(opt->data, buf, OPTION_MAX_SIZE);
+                                       tails[0] = xstrdup(buf);
+
+                                       tabexp.head = xstrdup(str);
+                                       tabexp.tails = tails;
+                                       tabexp.count = 1;
+                               } else if (opt->flags & OPT_PROGRAM_PATH) {
+                                       expand_program_paths_option(sep + 1, var);
+                               }
+                               break;
+                       }
+               }
+               free(var);
+       } else {
+               /* expand variable */
+               int pos;
+
+               tails = xnew(char *, nr_options);
+               pos = 0;
+               list_for_each_entry(opt, &option_head, node) {
+                       if (strncmp(str, opt->name, len) == 0)
+                               tails[pos++] = xstrdup(opt->name + len);
+               }
+               if (pos > 0) {
+                       if (pos == 1) {
+                               /* only one variable matches, add '=' */
+                               char *tmp = xstrjoin(tails[0], "=");
+
+                               free(tails[0]);
+                               tails[0] = tmp;
+                       }
+
+                       tabexp.head = xstrdup(str);
+                       tabexp.tails = tails;
+                       tabexp.count = pos;
+               } else {
+                       free(tails);
+               }
+       }
+}
+
+static void expand_toptions(const char *str)
+{
+       struct cmus_opt *opt;
+       int len, pos;
+       char **tails;
+
+       tails = xnew(char *, nr_options);
+       len = strlen(str);
+       pos = 0;
+       list_for_each_entry(opt, &option_head, node) {
+               if (opt->toggle == NULL)
+                       continue;
+               if (strncmp(str, opt->name, len) == 0)
+                       tails[pos++] = xstrdup(opt->name + len);
+       }
+       if (pos > 0) {
+               tabexp.head = xstrdup(str);
+               tabexp.tails = tails;
+               tabexp.count = pos;
+       } else {
+               free(tails);
+       }
+}
+
+static void load_themes(const char *dirname, const char *str, struct ptr_array *array)
+{
+       struct directory dir;
+       const char *name, *dot;
+       int len = strlen(str);
+
+       if (dir_open(&dir, dirname))
+               return;
+
+       while ((name = dir_read(&dir))) {
+               if (!S_ISREG(dir.st.st_mode))
+                       continue;
+               if (strncmp(name, str, len))
+                       continue;
+               dot = strrchr(name, '.');
+               if (dot == NULL || strcmp(dot, ".theme"))
+                       continue;
+               if (dot - name < len)
+                       /* str is  "foo.th"
+                        * matches "foo.theme"
+                        * which also ends with ".theme"
+                        */
+                       continue;
+               ptr_array_add(array, xstrndup(name + len, dot - name - len));
+       }
+       dir_close(&dir);
+}
+
+static void expand_colorscheme(const char *str)
+{
+       PTR_ARRAY(array);
+
+       load_themes(cmus_config_dir, str, &array);
+       load_themes(cmus_data_dir, str, &array);
+
+       if (array.count) {
+               ptr_array_sort(&array, strptrcmp);
+
+               tabexp.head = xstrdup(str);
+               tabexp.tails = array.ptrs;
+               tabexp.count = array.count;
+       }
+}
+
+static void expand_commands(const char *str);
+
+/* tab exp }}} */
+
+/* sort by name */
+struct command commands[] = {
+       { "add",                   cmd_add,              1, 1,  expand_add,           0, 0          },
+       { "bind",                  cmd_bind,             1, 1,  expand_bind_args,     0, CMD_UNSAFE },
+       { "browser-up",            cmd_browser_up,       0, 0,  NULL,                 0, 0          },
+       { "cd",                    cmd_cd,               0, 1,  expand_directories,   0, 0          },
+       { "clear",                 cmd_clear,            0, 1,  NULL,                 0, 0          },
+       { "colorscheme",           cmd_colorscheme,      1, 1,  expand_colorscheme,   0, 0          },
+       { "echo",                  cmd_echo,             1, -1, NULL,                 0, 0          },
+       { "factivate",             cmd_factivate,        0, 1,  expand_factivate,     0, 0          },
+       { "filter",                cmd_filter,           0, 1,  NULL,                 0, 0          },
+       { "fset",                  cmd_fset,             1, 1,  expand_fset,          0, 0          },
+       { "help",                  cmd_help,             0, 0,  NULL,                 0, 0          },
+       { "invert",                cmd_invert,           0, 0,  NULL,                 0, 0          },
+       { "live-filter",           cmd_live_filter,      0, 1,  NULL,                 0, CMD_LIVE   },
+       { "load",                  cmd_load,             1, 1,  expand_load_save,     0, 0          },
+       { "lqueue",                cmd_lqueue,           0, 1,  NULL,                 0, 0          },
+       { "mark",                  cmd_mark,             0, 1,  NULL,                 0, 0          },
+       { "player-next",           cmd_p_next,           0, 0,  NULL,                 0, 0          },
+       { "player-pause",          cmd_p_pause,          0, 0,  NULL,                 0, 0          },
+       { "player-pause-playback", cmd_p_pause_playback, 0, 0,  NULL,                 0, 0          },
+       { "player-play",           cmd_p_play,           0, 1,  expand_playable,      0, 0          },
+       { "player-prev",           cmd_p_prev,           0, 0,  NULL,                 0, 0          },
+       { "player-stop",           cmd_p_stop,           0, 0,  NULL,                 0, 0          },
+       { "prev-view",             cmd_prev_view,        0, 0,  NULL,                 0, 0          },
+       { "left-view",             cmd_left_view,        0, 0,  NULL,                 0, 0          },
+       { "right-view",            cmd_right_view,       0, 0,  NULL,                 0, 0          },
+       { "pl-create",             cmd_pl_create,        1, -1, NULL,                 0, 0          },
+       { "pl-export",             cmd_pl_export,        1, -1, NULL,                 0, 0          },
+       { "pl-import",             cmd_pl_import,        0, -1, NULL,                 0, 0          },
+       { "pl-rename",             cmd_pl_rename,        1, -1, NULL,                 0, 0          },
+       { "push",                  cmd_push,             1, -1, expand_commands,      0, 0          },
+       { "pwd",                   cmd_pwd,              0, 0,  NULL,                 0, 0          },
+       { "raise-vte",             cmd_raise_vte,        0, 0,  NULL,                 0, 0          },
+       { "rand",                  cmd_rand,             0, 0,  NULL,                 0, 0          },
+       { "quit",                  cmd_quit,             0, 1,  NULL,                 0, 0          },
+       { "refresh",               cmd_refresh,          0, 0,  NULL,                 0, 0          },
+       { "run",                   cmd_run,              1, -1, expand_program_paths, 0, CMD_UNSAFE },
+       { "save",                  cmd_save,             0, 1,  expand_load_save,     0, CMD_UNSAFE },
+       { "search-b-start",        cmd_search_b_start,   0, 0,  NULL,                 0, 0          },
+       { "search-next",           cmd_search_next,      0, 0,  NULL,                 0, 0          },
+       { "search-prev",           cmd_search_prev,      0, 0,  NULL,                 0, 0          },
+       { "search-start",          cmd_search_start,     0, 0,  NULL,                 0, 0          },
+       { "seek",                  cmd_seek,             1, 1,  NULL,                 0, 0          },
+       { "set",                   cmd_set,              1, 1,  expand_options,       0, 0          },
+       { "shell",                 cmd_shell,            1, -1, expand_program_paths, 0, CMD_UNSAFE },
+       { "showbind",              cmd_showbind,         1, 1,  expand_unbind_args,   0, 0          },
+       { "shuffle",               cmd_reshuffle,        0, 0,  NULL,                 0, 0          },
+       { "source",                cmd_source,           1, 1,  expand_files,         0, CMD_UNSAFE },
+       { "toggle",                cmd_toggle,           1, 1,  expand_toptions,      0, 0          },
+       { "tqueue",                cmd_tqueue,           0, 1,  NULL,                 0, 0          },
+       { "unbind",                cmd_unbind,           1, 1,  expand_unbind_args,   0, 0          },
+       { "unmark",                cmd_unmark,           0, 0,  NULL,                 0, 0          },
+       { "update-cache",          cmd_update_cache,     0, 1,  NULL,                 0, 0          },
+       { "version",               cmd_version,          0, 0,  NULL,                 0, 0          },
+       { "view",                  cmd_view,             1, 1,  NULL,                 0, 0          },
+       { "vol",                   cmd_vol,              1, 2,  NULL,                 0, 0          },
+       { "w",                     cmd_save,             0, 1,  expand_load_save,     0, CMD_UNSAFE },
+       { "win-activate",          cmd_win_activate,     0, 0,  NULL,                 0, 0          },
+       { "win-add-l",             cmd_win_add_l,        0, 0,  NULL,                 0, 0          },
+       { "win-add-p",             cmd_win_add_p,        0, 0,  NULL,                 0, 0          },
+       { "win-add-Q",             cmd_win_add_Q,        0, 0,  NULL,                 0, 0          },
+       { "win-add-q",             cmd_win_add_q,        0, 0,  NULL,                 0, 0          },
+       { "win-bottom",            cmd_win_bottom,       0, 0,  NULL,                 0, 0          },
+       { "win-down",              cmd_win_down,         0, 1,  NULL,                 0, 0          },
+       { "win-half-page-down",    cmd_win_hf_pg_down,   0, 0,  NULL,                 0, 0          },
+       { "win-half-page-up",      cmd_win_hf_pg_up,     0, 0,  NULL,                 0, 0          },
+       { "win-mv-after",          cmd_win_mv_after,     0, 0,  NULL,                 0, 0          },
+       { "win-mv-before",         cmd_win_mv_before,    0, 0,  NULL,                 0, 0          },
+       { "win-next",              cmd_win_next,         0, 0,  NULL,                 0, 0          },
+       { "win-page-bottom",       cmd_win_pg_bottom,    0, 0,  NULL,                 0, 0          },
+       { "win-page-down",         cmd_win_pg_down,      0, 0,  NULL,                 0, 0          },
+       { "win-page-middle",       cmd_win_pg_middle,    0, 0,  NULL,                 0, 0          },
+       { "win-page-top",          cmd_win_pg_top,       0, 0,  NULL,                 0, 0          },
+       { "win-page-up",           cmd_win_pg_up,        0, 0,  NULL,                 0, 0          },
+       { "win-remove",            cmd_win_remove,       0, 0,  NULL,                 0, CMD_UNSAFE },
+       { "win-scroll-down",       cmd_win_scroll_down,  0, 0,  NULL,                 0, 0          },
+       { "win-scroll-up",         cmd_win_scroll_up,    0, 0,  NULL,                 0, 0          },
+       { "win-sel-cur",           cmd_win_sel_cur,      0, 0,  NULL,                 0, 0          },
+       { "win-toggle",            cmd_win_toggle,       0, 0,  NULL,                 0, 0          },
+       { "win-top",               cmd_win_top,          0, 0,  NULL,                 0, 0          },
+       { "win-up",                cmd_win_up,           0, 1,  NULL,                 0, 0          },
+       { "win-update",            cmd_win_update,       0, 0,  NULL,                 0, 0          },
+       { "win-update-cache",      cmd_win_update_cache, 0, 1,  NULL,                 0, 0          },
+       { "wq",                    cmd_quit,             0, 1,  NULL,                 0, 0          },
+       { NULL,                    NULL,                 0, 0,  0,                    0, 0          }
+};
+
+/* fills tabexp struct */
+static void expand_commands(const char *str)
+{
+       int i, len, pos;
+       char **tails;
+
+       /* tabexp is resetted */
+       tails = xnew(char *, N_ELEMENTS(commands) - 1);
+       len = strlen(str);
+       pos = 0;
+       for (i = 0; commands[i].name; i++) {
+               if (strncmp(str, commands[i].name, len) == 0)
+                       tails[pos++] = xstrdup(commands[i].name + len);
+       }
+       if (pos > 0) {
+               if (pos == 1) {
+                       /* only one command matches, add ' ' */
+                       char *tmp = xstrjoin(tails[0], " ");
+
+                       free(tails[0]);
+                       tails[0] = tmp;
+               }
+               tabexp.head = xstrdup(str);
+               tabexp.tails = tails;
+               tabexp.count = pos;
+       } else {
+               free(tails);
+       }
+}
+
+struct command *get_command(const char *str)
+{
+       int i, len;
+
+       while (*str == ' ')
+               str++;
+       for (len = 0; str[len] && str[len] != ' '; len++)
+               ;
+
+       for (i = 0; commands[i].name; i++) {
+               if (strncmp(str, commands[i].name, len))
+                       continue;
+
+               if (commands[i].name[len] == 0) {
+                       /* exact */
+                       return &commands[i];
+               }
+
+               if (commands[i + 1].name && strncmp(str, commands[i + 1].name, len) == 0) {
+                       /* ambiguous */
+                       return NULL;
+               }
+               return &commands[i];
+       }
+       return NULL;
+}
+
+/* fills tabexp struct */
+static void expand_command_line(const char *str)
+{
+       /* :command [arg]...
+        *
+        * examples:
+        *
+        * str      expanded value (tabexp.head)
+        * -------------------------------------
+        *   fs     fset
+        *   b c    bind common
+        *   se     se          (tabexp.tails = [ ek t ])
+        */
+       /* command start/end, argument start */
+       const char *cs, *ce, *as;
+       const struct command *cmd;
+
+       cs = str;
+       ce = strchr(cs, ' ');
+       if (ce == NULL) {
+               /* expand command */
+               expand_commands(cs);
+               return;
+       }
+
+       /* command must be expandable */
+       cmd = get_command(cs);
+       if (cmd == NULL) {
+               /* command ambiguous or invalid */
+               return;
+       }
+
+       if (cmd->expand == NULL) {
+               /* can't expand argument */
+               return;
+       }
+
+       as = ce;
+       while (*as == ' ')
+               as++;
+
+       /* expand argument */
+       cmd->expand(as);
+       if (tabexp.head == NULL) {
+               /* argument expansion failed */
+               return;
+       }
+
+       /* tabexp.head is now start of the argument string */
+       snprintf(expbuf, sizeof(expbuf), "%s %s", cmd->name, tabexp.head);
+       free(tabexp.head);
+       tabexp.head = xstrdup(expbuf);
+}
+
+static void tab_expand(int direction)
+{
+       char *s1, *s2, *tmp;
+       int pos;
+
+       /* strip white space */
+       pos = 0;
+       while (cmdline.line[pos] == ' ' && pos < cmdline.bpos)
+               pos++;
+
+       /* string to expand */
+       s1 = xstrndup(cmdline.line + pos, cmdline.bpos - pos);
+
+       /* tail */
+       s2 = xstrdup(cmdline.line + cmdline.bpos);
+
+       tmp = tabexp_expand(s1, expand_command_line, direction);
+       if (tmp) {
+               /* tmp.s2 */
+               int l1, l2;
+
+               l1 = strlen(tmp);
+               l2 = strlen(s2);
+               cmdline.blen = l1 + l2;
+               if (cmdline.blen >= cmdline.size) {
+                       while (cmdline.blen >= cmdline.size)
+                               cmdline.size *= 2;
+                       cmdline.line = xrenew(char, cmdline.line, cmdline.size);
+               }
+               sprintf(cmdline.line, "%s%s", tmp, s2);
+               cmdline.bpos = l1;
+               cmdline.cpos = u_strlen_safe(tmp);
+               cmdline.clen = u_strlen_safe(cmdline.line);
+               free(tmp);
+       }
+       free(s1);
+       free(s2);
+}
+
+static void reset_tab_expansion(void)
+{
+       tabexp_reset();
+       arg_expand_cmd = -1;
+}
+
+static void cmdline_modified(void)
+{
+       char *cmd, *arg;
+       struct command *c;
+
+       if (!parse_command(cmdline.line, &cmd, &arg))
+               return;
+
+       c = get_command(cmd);
+       if (!c)
+               goto end;
+
+       if (c->flags & CMD_LIVE)
+               run_parsed_command(cmd, arg);
+
+end:
+       free(cmd);
+       free(arg);
+}
+
+int parse_command(const char *buf, char **cmdp, char **argp)
+{
+       int cmd_start, cmd_end, cmd_len;
+       int arg_start, arg_end;
+       int i;
+
+       i = 0;
+       while (buf[i] && buf[i] == ' ')
+               i++;
+
+       if (buf[i] == '#')
+               return 0;
+
+       cmd_start = i;
+       while (buf[i] && buf[i] != ' ')
+               i++;
+       cmd_end = i;
+       while (buf[i] && buf[i] == ' ')
+               i++;
+       arg_start = i;
+       while (buf[i])
+               i++;
+       arg_end = i;
+
+       cmd_len = cmd_end - cmd_start;
+       if (cmd_len == 0)
+               return 0;
+
+       *cmdp = xstrndup(buf + cmd_start, cmd_len);
+       if (arg_start == arg_end) {
+               *argp = NULL;
+       } else {
+               *argp = xstrndup(buf + arg_start, arg_end - arg_start);
+       }
+       return 1;
+}
+
+int run_only_safe_commands;
+
+void run_parsed_command(char *cmd, char *arg)
+{
+       int cmd_len = strlen(cmd);
+       int i = 0;
+
+       while (1) {
+               const struct command *c = &commands[i];
+
+               if (c->name == NULL) {
+                       error_msg("unknown command\n");
+                       break;
+               }
+               if (strncmp(cmd, c->name, cmd_len) == 0) {
+                       const char *next = commands[i + 1].name;
+                       int exact = c->name[cmd_len] == 0;
+
+                       if (!exact && next && strncmp(cmd, next, cmd_len) == 0) {
+                               error_msg("ambiguous command\n");
+                               break;
+                       }
+                       if (c->min_args > 0 && arg == NULL) {
+                               error_msg("not enough arguments\n");
+                               break;
+                       }
+                       if (c->max_args == 0 && arg) {
+                               error_msg("too many arguments\n");
+                               break;
+                       }
+                       if (run_only_safe_commands && (c->flags & CMD_UNSAFE)) {
+                               if (c->func != cmd_save || !is_stdout_filename(arg)) {
+                                       d_print("trying to execute unsafe command over net\n");
+                                       break;
+                               }
+                       }
+                       c->func(arg);
+                       break;
+               }
+               i++;
+       }
+}
+
+void run_command(const char *buf)
+{
+       char *cmd, *arg;
+
+       if (!parse_command(buf, &cmd, &arg))
+               return;
+
+       run_parsed_command(cmd, arg);
+       free(arg);
+       free(cmd);
+}
+
+static void reset_history_search(void)
+{
+       history_reset_search(&cmd_history);
+       free(history_search_text);
+       history_search_text = NULL;
+}
+
+static void backspace(void)
+{
+       if (cmdline.clen > 0) {
+               cmdline_backspace();
+       } else {
+               input_mode = NORMAL_MODE;
+       }
+}
+
+void command_mode_ch(uchar ch)
+{
+       switch (ch) {
+       case 0x01: // ^A
+               cmdline_move_home();
+               break;
+       case 0x02: // ^B
+               cmdline_move_left();
+               break;
+       case 0x04: // ^D
+               cmdline_delete_ch();
+               cmdline_modified();
+               break;
+       case 0x05: // ^E
+               cmdline_move_end();
+               break;
+       case 0x06: // ^F
+               cmdline_move_right();
+               break;
+       case 0x03: // ^C
+       case 0x07: // ^G
+       case 0x1B: // ESC
+               if (cmdline.blen) {
+                       history_add_line(&cmd_history, cmdline.line);
+                       cmdline_clear();
+               }
+               input_mode = NORMAL_MODE;
+               break;
+       case 0x10: // ^P
+               command_mode_key(KEY_UP);
+               return;
+       case 0xE: // ^N
+               command_mode_key(KEY_DOWN);
+               return;
+       case 0x0A:
+               if (cmdline.blen) {
+                       run_command(cmdline.line);
+                       history_add_line(&cmd_history, cmdline.line);
+                       cmdline_clear();
+               }
+               input_mode = NORMAL_MODE;
+               break;
+       case 0x0B:
+               cmdline_clear_end();
+               cmdline_modified();
+               break;
+       case 0x09:
+               tab_expand(1);
+               break;
+       case 0x15:
+               cmdline_backspace_to_bol();
+               cmdline_modified();
+               break;
+       case 0x17: // ^W
+               cmdline_backward_delete_word(cmdline_word_delimiters);
+               cmdline_modified();
+               break;
+       case 0x08: // ^H
+       case 127:
+               backspace();
+               cmdline_modified();
+               break;
+       default:
+               cmdline_insert_ch(ch);
+               cmdline_modified();
+       }
+       reset_history_search();
+       if (ch != 0x09)
+               reset_tab_expansion();
+}
+
+void command_mode_escape(int c)
+{
+       switch (c) {
+       case 98:
+               cmdline_backward_word(cmdline_filename_delimiters);
+               break;
+       case 100:
+               cmdline_delete_word(cmdline_filename_delimiters);
+               cmdline_modified();
+               break;
+       case 102:
+               cmdline_forward_word(cmdline_filename_delimiters);
+               break;
+       case 127:
+       case KEY_BACKSPACE:
+               cmdline_backward_delete_word(cmdline_filename_delimiters);
+               cmdline_modified();
+               break;
+       }
+       reset_history_search();
+}
+
+void command_mode_key(int key)
+{
+       if (key != KEY_BTAB)
+               reset_tab_expansion();
+       switch (key) {
+       case KEY_DC:
+               cmdline_delete_ch();
+               cmdline_modified();
+               break;
+       case KEY_BACKSPACE:
+               backspace();
+               cmdline_modified();
+               break;
+       case KEY_LEFT:
+               cmdline_move_left();
+               return;
+       case KEY_RIGHT:
+               cmdline_move_right();
+               return;
+       case KEY_HOME:
+               cmdline_move_home();
+               return;
+       case KEY_END:
+               cmdline_move_end();
+               return;
+       case KEY_UP:
+               {
+                       const char *s;
+
+                       if (history_search_text == NULL)
+                               history_search_text = xstrdup(cmdline.line);
+                       s = history_search_forward(&cmd_history, history_search_text);
+                       if (s)
+                               cmdline_set_text(s);
+               }
+               return;
+       case KEY_DOWN:
+               if (history_search_text) {
+                       const char *s;
+
+                       s = history_search_backward(&cmd_history, history_search_text);
+                       if (s) {
+                               cmdline_set_text(s);
+                       } else {
+                               cmdline_set_text(history_search_text);
+                       }
+               }
+               return;
+       case KEY_BTAB:
+               tab_expand(-1);
+               break;
+       default:
+               d_print("key = %c (%d)\n", key, key);
+       }
+       reset_history_search();
+}
+
+void command_mode_mouse(MEVENT *event)
+{
+       if ((event->bstate & BUTTON1_PRESSED) || (event->bstate & BUTTON3_PRESSED)) {
+               if (event->y <= window_get_nr_rows(current_win()) + 2) {
+                       if (cmdline.blen) {
+                               history_add_line(&cmd_history, cmdline.line);
+                               cmdline_clear();
+                       }
+                       input_mode = NORMAL_MODE;
+                       normal_mode_mouse(event);
+                       return;
+               }
+               if (event->x == 0)
+                       return;
+               int i = event->x > cmdline.clen ? cmdline.clen : event->x - 1;
+               while (i < cmdline.cpos)
+                       cmdline_move_left();
+               while (i > cmdline.cpos)
+                       cmdline_move_right();
+       } else if (event->bstate & BUTTON4_PRESSED) {
+               command_mode_key(KEY_UP);
+       } else if (event->bstate & BUTTON5_PRESSED) {
+               command_mode_key(KEY_DOWN);
+       }
+}
+
+void commands_init(void)
+{
+       cmd_history_filename = xstrjoin(cmus_config_dir, "/command-history");
+       history_load(&cmd_history, cmd_history_filename, 2000);
+}
+
+void commands_exit(void)
+{
+       view_clear(TREE_VIEW);
+       view_clear(SORTED_VIEW);
+       view_clear(PLAYLIST_VIEW);
+       view_clear(QUEUE_VIEW);
+       history_save(&cmd_history);
+       history_free(&cmd_history);
+       free(cmd_history_filename);
+       tabexp_reset();
+}
diff --git a/command_mode.h b/command_mode.h
new file mode 100644 (file)
index 0000000..9beb9f2
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_COMMAND_MODE_H
+#define CMUS_COMMAND_MODE_H
+
+#include "uchar.h"
+
+#if defined(__sun__)
+#include <ncurses.h>
+#else
+#include <curses.h>
+#endif
+
+enum {
+       /* executing command is disabled over net */
+       CMD_UNSAFE      = 1 << 0,
+       /* execute command after every typed/deleted character */
+       CMD_LIVE        = 1 << 1,
+};
+
+struct command {
+       const char *name;
+       void (*func)(char *arg);
+
+       /* min/max number of arguments */
+       int min_args;
+       int max_args;
+
+       void (*expand)(const char *str);
+
+       /* bind count (0 means: unbound) */
+       int bc;
+
+       /* CMD_* */
+       unsigned int flags;
+};
+
+extern struct command commands[];
+extern int run_only_safe_commands;
+
+void command_mode_ch(uchar ch);
+void command_mode_escape(int c);
+void command_mode_key(int key);
+void command_mode_mouse(MEVENT *event);
+void commands_init(void);
+void commands_exit(void);
+int parse_command(const char *buf, char **cmdp, char **argp);
+char **parse_cmd(const char *cmd, int *args_idx, int *ac);
+void run_parsed_command(char *cmd, char *arg);
+void run_command(const char *buf);
+
+struct command *get_command(const char *str);
+
+void view_clear(int view);
+void view_add(int view, char *arg, int prepend);
+void view_load(int view, char *arg);
+void view_save(int view, char *arg, int to_stdout, int filtered, int extended);
+
+struct window *current_win(void);
+
+#endif
diff --git a/comment.c b/comment.c
new file mode 100644 (file)
index 0000000..13ba7f0
--- /dev/null
+++ b/comment.c
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2007 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "comment.h"
+#include "xmalloc.h"
+#include "utils.h"
+#include "uchar.h"
+
+#include <string.h>
+#include <strings.h>
+
+static int is_various_artists(const char *a)
+{
+       return strcasecmp(a, "Various Artists") == 0 ||
+              strcasecmp(a, "Various")         == 0 ||
+              strcasecmp(a, "VA")              == 0 ||
+              strcasecmp(a, "V/A")             == 0;
+}
+
+int track_is_compilation(const struct keyval *comments)
+{
+       const char *c, *a, *aa;
+
+       c = keyvals_get_val(comments, "compilation");
+       if (c && is_freeform_true(c))
+               return 1;
+
+       c = keyvals_get_val(comments, "partofacompilation");
+       if (c && is_freeform_true(c))
+               return 1;
+
+       aa = keyvals_get_val(comments, "albumartist");
+       if (aa && is_various_artists(aa))
+               return 1;
+
+       a = keyvals_get_val(comments, "artist");
+       if (a && is_various_artists(a))
+               return 1;
+
+       if (aa && a && !u_strcase_equal(aa, a))
+               return 1;
+
+       return 0;
+}
+
+int track_is_va_compilation(const struct keyval *comments)
+{
+       const char *c, *a, *aa;
+
+       aa = keyvals_get_val(comments, "albumartist");
+       if (aa)
+               return is_various_artists(aa);
+
+       a = keyvals_get_val(comments, "artist");
+       if (a && is_various_artists(a))
+               return 1;
+
+       c = keyvals_get_val(comments, "compilation");
+       if (c && is_freeform_true(c))
+               return 1;
+
+       c = keyvals_get_val(comments, "partofacompilation");
+       if (c && is_freeform_true(c))
+               return 1;
+
+       return 0;
+}
+
+const char *comments_get_albumartist(const struct keyval *comments)
+{
+       const char *val = keyvals_get_val(comments, "albumartist");
+
+       if (!val || strcmp(val, "") == 0)
+               val = keyvals_get_val(comments, "artist");
+
+       return val;
+}
+
+const char *comments_get_artistsort(const struct keyval *comments)
+{
+       const char *val;
+
+       if (track_is_va_compilation(comments))
+               return NULL;
+
+       val = keyvals_get_val(comments, "albumartistsort");
+       if (!track_is_compilation(comments)) {
+               if (!val || strcmp(val, "") == 0)
+                       val = keyvals_get_val(comments, "artistsort");
+       }
+
+       if (!val || strcmp(val, "") == 0)
+               return NULL;
+
+       return val;
+}
+
+int comments_get_int(const struct keyval *comments, const char *key)
+{
+       const char *val;
+       long int ival;
+
+       val = keyvals_get_val(comments, key);
+       if (val == NULL)
+               return -1;
+       while (*val && !(*val >= '0' && *val <= '9'))
+               val++;
+       if (str_to_int(val, &ival) == -1)
+               return -1;
+       return ival;
+}
+
+double comments_get_double(const struct keyval *comments, const char *key)
+{
+       const char *val;
+       char *end;
+       double d;
+
+       val = keyvals_get_val(comments, key);
+       if (!val || strcmp(val, "") == 0)
+               goto error;
+
+       d = strtod(val, &end);
+       if (val == end)
+               goto error;
+
+       return d;
+
+error:
+       return strtod("NAN", NULL);
+}
+
+/* Return date as an integer in the form YYYYMMDD, for sorting purposes.
+ * This function is not year 10000 compliant. */
+int comments_get_date(const struct keyval *comments, const char *key)
+{
+       const char *val;
+       char *endptr;
+       int year, month, day;
+       long int ival;
+
+       val = keyvals_get_val(comments, key);
+       if (val == NULL)
+               return -1;
+
+       year = strtol(val, &endptr, 10);
+       /* Looking for a four-digit number */
+       if (year < 1000 || year > 9999)
+               return -1;
+       ival = year * 10000;
+
+       if (*endptr == '-' || *endptr == ' ' || *endptr == '/') {
+               month = strtol(endptr+1, &endptr, 10);
+               if (month < 1 || month > 12)
+                       return ival;
+               ival += month * 100;
+       }
+
+       if (*endptr == '-' || *endptr == ' ' || *endptr == '/') {
+               day = strtol(endptr+1, &endptr, 10);
+               if (day < 1 || day > 31)
+                       return ival;
+               ival += day;
+       }
+
+
+       return ival;
+}
+
+static const char *interesting[] = {
+       "artist", "album", "title", "tracknumber", "discnumber", "genre",
+       "date", "compilation", "partofacompilation", "albumartist", "artistsort", "albumartistsort",
+       "albumsort",
+       "originaldate",
+       "replaygain_track_gain",
+       "replaygain_track_peak",
+       "replaygain_album_gain",
+       "replaygain_album_peak",
+       "musicbrainz_trackid",
+       "comment",
+       "bpm",
+       "arranger", "composer", "conductor", "lyricist", "performer",
+       "remixer", "label", "publisher", "work", "opus", "partnumber", "part",
+       "subtitle", "media",
+       NULL
+};
+
+static struct {
+       const char *old;
+       const char *new;
+} key_map[] = {
+       { "album_artist", "albumartist" },
+       { "album artist", "albumartist" },
+       { "disc", "discnumber" },
+       { "tempo", "bpm" },
+       { "track", "tracknumber" },
+       { "WM/Year", "date" },
+       { "WM/ArtistSortOrder", "artistsort" },
+       { "WM/AlbumArtistSortOrder", "albumartistsort" },
+       { "WM/AlbumSortOrder", "albumsort" },
+       { "WM/OriginalReleaseYear", "originaldate" },
+       { "WM/Media", "media" },
+       { "sourcemedia", "media" },
+       { "MusicBrainz Track Id", "musicbrainz_trackid" },
+       { "version", "subtitle" },
+       { NULL, NULL }
+};
+
+static const char *fix_key(const char *key)
+{
+       int i;
+
+       for (i = 0; interesting[i]; i++) {
+               if (!strcasecmp(key, interesting[i]))
+                       return interesting[i];
+       }
+       for (i = 0; key_map[i].old; i++) {
+               if (!strcasecmp(key, key_map[i].old))
+                       return key_map[i].new;
+       }
+       return NULL;
+}
+
+int comments_add(struct growing_keyvals *c, const char *key, char *val)
+{
+       if (!strcasecmp(key, "songwriter")) {
+               int r = comments_add_const(c, "lyricist", val);
+               return comments_add(c, "composer", val) && r;
+       }
+
+       key = fix_key(key);
+       if (!key) {
+               free(val);
+               return 0;
+       }
+
+       if (!strcmp(key, "tracknumber") || !strcmp(key, "discnumber")) {
+               char *slash = strchr(val, '/');
+               if (slash)
+                       *slash = 0;
+       }
+
+       /* don't add duplicates */
+       if (keyvals_get_val_growing(c, key)) {
+               free(val);
+               return 0;
+       }
+
+       keyvals_add(c, key, val);
+       return 1;
+}
+
+int comments_add_const(struct growing_keyvals *c, const char *key, const char *val)
+{
+       return comments_add(c, key, xstrdup(val));
+}
diff --git a/comment.h b/comment.h
new file mode 100644 (file)
index 0000000..ecc407d
--- /dev/null
+++ b/comment.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_COMMENT_H
+#define CMUS_COMMENT_H
+
+#include "keyval.h"
+
+int track_is_compilation(const struct keyval *comments);
+int track_is_va_compilation(const struct keyval *comments);
+
+const char *comments_get_albumartist(const struct keyval *comments);
+const char *comments_get_artistsort(const struct keyval *comments); /* can return NULL */
+
+int comments_get_int(const struct keyval *comments, const char *key);
+double comments_get_double(const struct keyval *comments, const char *key);
+int comments_get_date(const struct keyval *comments, const char *key);
+
+int comments_add(struct growing_keyvals *c, const char *key, char *val);
+int comments_add_const(struct growing_keyvals *c, const char *key, const char *val);
+
+#endif
diff --git a/compiler.h b/compiler.h
new file mode 100644 (file)
index 0000000..f31ae29
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_COMPILER_H
+#define CMUS_COMPILER_H
+
+#include <stddef.h>
+
+/*
+ * GCC 2.96 or compatible required
+ */
+#if defined(__GNUC__)
+
+#if __GNUC__ > 3
+#undef offsetof
+#define offsetof(type, member) __builtin_offsetof(type, member)
+#endif
+
+/* Optimization: Condition @x is likely */
+#define likely(x)      __builtin_expect(!!(x), 1)
+
+/* Optimization: Condition @x is unlikely */
+#define unlikely(x)    __builtin_expect(!!(x), 0)
+
+#ifndef UNUSED
+#define UNUSED __attribute__((unused))
+#endif
+
+#else
+
+#define likely(x)      (x)
+#define unlikely(x)    (x)
+#define UNUSED
+
+#endif
+
+/* Optimization: Function never returns */
+#define CMUS_NORETURN  __attribute__((__noreturn__))
+
+/* Argument at index @fmt_idx is printf compatible format string and
+ * argument at index @first_idx is the first format argument */
+#define CMUS_FORMAT(fmt_idx, first_idx) __attribute__((format(printf, (fmt_idx), (first_idx))))
+
+#if defined(__GNUC__) && (__GNUC__ >= 3)
+
+/* Optimization: Pointer returned can't alias other pointers */
+#define CMUS_MALLOC    __attribute__((__malloc__))
+
+#else
+
+#define CMUS_MALLOC
+
+#endif
+
+
+/**
+ * container_of - cast a member of a structure out to the containing structure
+ *
+ * @ptr:       the pointer to the member.
+ * @type:      the type of the container struct this is embedded in.
+ * @member:    the name of the member within the struct.
+ *
+ */
+#define container_of_portable(ptr, type, member) \
+       ((type *)(void *)( (char *)(ptr) - offsetof(type,member) ))
+#undef container_of
+#if defined(__GNUC__)
+#define container_of(ptr, type, member) __extension__ ({               \
+       const __typeof__( ((type *)0)->member ) *_mptr = (ptr); \
+       container_of_portable(_mptr, type, member);})
+#else
+#define container_of(ptr, type, member) container_of_portable(ptr, type, member)
+#endif
+
+#endif
diff --git a/configure b/configure
new file mode 100755 (executable)
index 0000000..e76acdd
--- /dev/null
+++ b/configure
@@ -0,0 +1,648 @@
+#!/bin/sh
+
+. scripts/configure.sh || exit 1
+
+c11_code="
+#include <stdatomic.h>
+
+int main(void)
+{
+#ifdef __STDC_NO_ATOMICS__
+#error No C11 atomics
+#endif
+       _Atomic int res = ATOMIC_VAR_INIT(0);
+
+       return res;
+}
+"
+
+check_c11()
+{
+       msg_checking "for C11 (with atomics support)"
+
+       for flag in -std=gnu11 -std=c11 ""
+       do
+               if try_compile_link "$c11_code" $flag
+               then
+                       EXTRA_CFLAGS="$EXTRA_CFLAGS $flag"
+                       msg_result yes
+                       working_c11=y
+                       break
+               fi
+       done
+
+       if test -z "$working_c11"
+       then
+               msg_result no
+               return 1
+       fi
+
+       return 0
+}
+
+check_cflags()
+{
+       check_cc_flag -pipe -Wall -Wshadow -Wcast-align -Wpointer-arith \
+               -Wwrite-strings -Wundef -Wmissing-prototypes -Wredundant-decls \
+               -Wextra -Wno-sign-compare -Wformat-security
+
+       for i in -Wold-style-definition \
+               -Wno-pointer-sign \
+               -Werror-implicit-function-declaration \
+               -Wno-unused-parameter \
+               -Wno-missing-field-initializers
+       do
+               check_cc_flag $i
+       done
+       return 0
+}
+
+check_sndio()
+{
+       check_library SNDIO "" "-lsndio"
+       return $?
+}
+
+check_coreaudio()
+{
+       case `uname -s` in
+       Darwin)
+               check_library COREAUDIO "" "-framework CoreAudio -framework AudioUnit"
+               return $?
+       esac
+       return 1
+}
+
+
+check_compat()
+{
+       COMPAT_LIBS=
+       case `uname -s` in
+       SunOS)
+               # connect() etc.
+               try_link -lsocket && COMPAT_LIBS="$COMPAT_LIBS -lsocket"
+
+               # gethostbyname()
+               try_link -lnsl && COMPAT_LIBS="$COMPAT_LIBS -lnsl"
+
+               # nanosleep()
+               if try_link -lrt
+               then
+                       COMPAT_LIBS="$COMPAT_LIBS -lrt"
+               elif try_link -lposix4
+               then
+                       COMPAT_LIBS="$COMPAT_LIBS -lposix4"
+               fi
+               ;;
+       CYGWIN*)
+               CONFIG_CYGWIN=y
+               makefile_vars CONFIG_CYGWIN
+       esac
+       makefile_vars COMPAT_LIBS
+}
+
+rtsched_code="
+#include <pthread.h>
+
+int main(int argc, char *argv[])
+{
+       pthread_attr_t attr;
+       struct sched_param param;
+
+       pthread_attr_init(&attr);
+       pthread_attr_setschedpolicy(&attr, SCHED_RR);
+       param.sched_priority = sched_get_priority_max(SCHED_RR);
+       pthread_attr_setschedparam(&attr, &param);
+       return 0;
+}
+"
+
+check_rtsched()
+{
+       msg_checking "for realtime scheduling"
+       if try_compile_link "$rtsched_code" $PTHREAD_CFLAGS $PTHREAD_LIBS
+       then
+               msg_result yes
+               EXTRA_CFLAGS="$EXTRA_CFLAGS -DREALTIME_SCHEDULING"
+       else
+               msg_result no
+       fi
+       return 0
+}
+
+ncurses_code="
+#if defined(__sun__) || defined(__CYGWIN__)
+#include <termios.h>
+#include <ncurses.h>
+#else
+#include <curses.h>
+#endif
+
+int main(void)
+{
+       initscr();
+       endwin();
+       return 0;
+}
+"
+
+check_ncurses()
+{
+       if pkg_config NCURSES "ncursesw" "" "-lncursesw"
+       then
+               widechars=y
+       elif pkg_config NCURSES "ncurses" "" "-lncurses" || pkg_config NCURSES "curses" "" "-lcurses"
+       then
+               widechars=n
+               msg_error "Your ncurses does not support wide characters!"
+               msg_error "Install ncursesw if you need wide character support,"
+               msg_error "you can ignore this warning otherwise."
+       fi
+       test -z "$widechars" && return 1
+
+       msg_checking "for working ncurses setup"
+       for flag in "" "-I/usr/include/ncurses" "-I/usr/include/ncursesw"
+       do
+               if try_compile_link "$ncurses_code" $flag $NCURSES_LIBS
+               then
+                       NCURSES_CFLAGS="$NCURSES_CFLAGS $flag"
+                       msg_result yes
+                       working_curses=y
+                       break
+               fi
+       done
+       if test -z "$working_curses"
+       then
+               msg_result no
+               return 1
+       fi
+
+       check_function "resizeterm" $NCURSES_CFLAGS $NCURSES_LIBS
+       HAVE_RESIZETERM=`test $? -ne 0 ; echo $?`
+
+       check_function "use_default_colors" $NCURSES_CFLAGS $NCURSES_LIBS
+       HAVE_USE_DEFAULT_COLORS=`test $? -ne 0 ; echo $?`
+
+       return 0
+}
+
+check_discid()
+{
+       HAVE_DISCID=n
+       pkg_config DISCID "libdiscid" "" "-ldiscid" && HAVE_DISCID=y
+       return $?
+}
+
+check_mpc()
+{
+       MPC_SV8=0
+       if check_header mpc/mpcdec.h
+       then
+               MPC_SV8=1
+       else
+               check_header mpcdec/mpcdec.h || return $?
+       fi
+       check_library MPC "" "-lmpcdec -lm"
+       return $?
+}
+
+check_cddb()
+{
+       pkg_config CDDB "libcddb" "" "-lcddb" && HAVE_CDDB=y
+       return $?
+}
+
+check_cdio()
+{
+       pkg_config CDIO "libcdio_cdda" "" "-lcdio_cdio -lcdio -lm"
+       return $?
+}
+
+check_flac()
+{
+       pkg_config FLAC "flac" "" "-lFLAC -lm" || return $?
+
+       # Make sure the FLAC_CFLAGS value is sane, strip trailing '/FLAC'.
+       FLAC_CFLAGS=`echo $FLAC_CFLAGS | sed "s/FLAC$//"`
+       return 0
+}
+
+check_mad()
+{
+       pkg_config MAD "mad" "" "-lmad -lm"
+       return $?
+}
+
+mikmod_code="
+#include <mikmod.h>
+int main() {
+       MikMod_RegisterAllDrivers();
+       return 0;
+}
+"
+check_mikmod()
+{
+       # mikmod is linked against pthread
+       app_config MIKMOD libmikmod-config || \
+               check_library MIKMOD "$PTHREAD_CFLAGS" "-lmikmod $PTHREAD_LIBS" || \
+               return 1
+       try_compile_link "$mikmod_code" $MIKMOD_CFLAGS $MIKMOD_LIBS
+       return $?
+}
+
+check_modplug()
+{
+       pkg_config MODPLUG "libmodplug" "-I/usr/include/libmodplug" "-lmodplug -lstdc++ -lm" || return $?
+       MODPLUG_API_8=0
+       if check_function "ModPlug_GetModuleType" $MODPLUG_CFLAGS $MODPLUG_LIBS
+       then
+               MODPLUG_API_8=1
+       fi
+       return 0
+}
+
+check_bass()
+{
+    check_header bass.h &&
+    check_library BASS "" "-lbass"
+    return $?
+}
+
+check_vtx()
+{
+       check_header ayemu.h &&
+       check_library VTX "" "-layemu"
+       return $?
+}
+
+check_vorbis()
+{
+       if test "$CONFIG_TREMOR" = y
+       then
+               pkg_config VORBIS "vorbisidec" "" "-lvorbisidec -lm"
+               return $?
+       else
+               pkg_config VORBIS "vorbisfile" "" "-lvorbisfile -lvorbis -lm -logg"
+               return $?
+       fi
+}
+
+check_libsystemd()
+{
+       pkg_config LIBSYSTEMD "libsystemd"
+       return $?
+}
+
+check_opus()
+{
+       pkg_config OPUS "opusfile"
+       return $?
+}
+
+check_wavpack()
+{
+       pkg_config WAVPACK "wavpack >= 4.40" "" "-lwavpack"
+       return $?
+}
+
+check_pulse()
+{
+       pkg_config PULSE "libpulse >= 0.9.19"
+       return $?
+}
+
+check_alsa()
+{
+       # the alsa.pc file should be always available
+       pkg_config ALSA "alsa >= 1.0.11"
+       return $?
+}
+
+check_jack()
+{
+       pkg_config JACK "jack"
+       return $?
+}
+
+check_samplerate()
+{
+       pkg_config SAMPLERATE "samplerate" && HAVE_SAMPLERATE=y
+       return $?
+}
+
+check_ao()
+{
+       pkg_config AO "ao" "" "-lao"
+       return $?
+}
+
+arts_code="
+#include <artsc.h>
+int main() {
+       return arts_init();
+}
+"
+check_arts()
+{
+       app_config ARTS artsc-config || return 1
+       try_compile_link "$arts_code" $ARTS_CFLAGS $ARTS_LIBS
+       return $?
+}
+
+check_oss()
+{
+       case `uname -s` in
+               Linux|*FreeBSD)
+                       ;;
+               *BSD)
+                       check_library OSS "" "-lossaudio"
+                       return $?
+                       ;;
+               *)
+                       # unknown
+                       ;;
+       esac
+
+       OSS_CFLAGS=""
+       OSS_LIBS=""
+       msg_checking "for header <sys/soundcard.h>"
+       if test -f /usr/include/sys/soundcard.h
+       then
+               msg_result "yes"
+               makefile_vars OSS_CFLAGS OSS_LIBS
+               return 0
+       else
+               msg_result "no"
+       fi
+       return 1
+}
+
+check_sun()
+{
+       msg_checking "for header <sys/audioio.h>"
+       if test -f /usr/include/sys/audioio.h
+       then
+               msg_result "yes"
+               return 0
+       else
+               msg_result "no"
+               return 1
+       fi
+}
+
+check_waveout()
+{
+       case `uname -s` in
+       CYGWIN*)
+               check_library WAVEOUT "" "-lwinmm"
+               return $?
+       esac
+       return 1
+}
+
+check_roar()
+{
+       pkg_config ROAR "libroar >= 0.4.5"
+       return $?
+}
+
+check_mp4()
+{
+       USE_MPEG4IP=1
+       if check_header mp4v2/mp4v2.h
+       then
+               USE_MPEG4IP=0
+       else
+               check_header mp4.h || return $?
+       fi
+       check_header neaacdec.h &&
+       check_library MP4 "" "-lmp4v2 -lfaad -lm"
+       return $?
+}
+
+check_aac()
+{
+       check_header neaacdec.h &&
+       check_library AAC "" "-lfaad -lm"
+       return $?
+}
+
+check_ffmpeg()
+{
+       HAVE_FFMPEG_AVCODEC_H=y
+       pkg_config FFMPEG "libavformat libavcodec" || return $?
+       if check_header "libavcodec/avcodec.h" $FFMPEG_CFLAGS
+       then
+               HAVE_FFMPEG_AVCODEC_H=n
+       else
+               check_header "ffmpeg/avcodec.h" $FFMPEG_CFLAGS || return $?
+       fi
+       # ffmpeg api changes so frequently that it is best to compile the module
+       libs="$LDDLFLAGS $FFMPEG_LIBS"
+       cflags="$SOFLAGS $FFMPEG_CFLAGS"
+       if test "$HAVE_FFMPEG_AVCODEC_H" = y
+       then
+               cflags="$cflags -DHAVE_FFMPEG_AVCODEC_H"
+       fi
+       topdir=`dirname "$0"`
+       ffmpeg_code=`cat "$topdir"/ip/ffmpeg.c | sed 's/\\\n//g'`
+       msg_checking "for successful build of ffmpeg.c"
+       if try_compile_link "$ffmpeg_code" $cflags $libs
+       then
+               msg_result yes
+               return 0
+       fi
+       msg_result no
+       return 1
+}
+
+check_string_function()
+{
+       msg_checking "for function $1"
+       string_function_code="
+#include <string.h>
+int main() {
+       return $1;
+}
+"
+       if try_compile_link "$string_function_code"
+       then
+               msg_result yes
+               return 0
+       fi
+       msg_result no
+       return 1
+}
+
+
+# defaults
+prefix=/usr/local
+DEBUG=1
+HAVE_CDDB=n
+CONFIG_TREMOR=n
+CONFIG_MIKMOD=n
+CONFIG_BASS=n
+USE_FALLBACK_IP=n
+HAVE_BYTESWAP_H=n
+HAVE_STRDUP=n
+HAVE_STRNDUP=n
+HAVE_SAMPLERATE=n
+# unset CONFIG_* variables: if check succeeds 'y', otherwise 'n'
+
+USAGE="
+Options:
+  prefix          Installation prefix    [$prefix]
+  bindir          User executables       [\$prefix/bin]
+  datadir         Read-only data         [\$prefix/share]
+  libdir          Libraries              [\$prefix/lib]
+  mandir          Man pages              [\$datadir/man]
+  exampledir      Examples               [\$datadir/doc/cmus/examples]
+  DEBUG           Debugging level (0-2)  [$DEBUG]
+
+Optional Features: y/n
+  CONFIG_AAC            AAC (.aac, audio/aac, audio/aacp)               [auto]
+  CONFIG_ALSA           ALSA                                            [auto]
+  CONFIG_AO             Libao cross-platform audio library              [auto]
+  CONFIG_ARTS           ARTS                                            [auto]
+  CONFIG_CDDB           libcddb CDDA identification                     [auto]
+  CONFIG_CDIO           libcdio CDDA input                              [auto]
+  CONFIG_COREAUDIO      CoreAudio                                       [auto]
+  CONFIG_CUE            CUE sheets (.cue)                               [y]
+  CONFIG_DISCID         libdiscid CDDA identification                   [auto]
+  CONFIG_FFMPEG         FFMPEG (.shn, .wma)                             [auto]
+  CONFIG_FLAC           Free Lossless Audio Codec (.flac, .fla)         [auto]
+  CONFIG_JACK           JACK                                            [auto]
+  CONFIG_MAD            MPEG Audio Decoder (.mp3, .mp2, streams)        [auto]
+  CONFIG_MIKMOD         libmikmod (.mod, .x3m, ...)                     [n]
+  CONFIG_BASS           libbass (.mod, .x3m, ...)                       [n]
+  CONFIG_MODPLUG        libmodplug (.mod, .x3m, ...)                    [auto]
+  CONFIG_MP4            MPEG-4 AAC (.mp4, .m4a, .m4b)                   [auto]
+  CONFIG_MPC            libmpcdec (Musepack .mpc, .mpp, .mp+)           [auto]
+  CONFIG_MPRIS          MPRIS                                           [auto]
+  CONFIG_OPUS           Opus (.opus)                                    [auto]
+  CONFIG_OSS            Open Sound System                               [auto]
+  CONFIG_PULSE          native PulseAudio output                        [auto]
+  CONFIG_ROAR           native RoarAudio output                         [auto]
+  CONFIG_SAMPLERATE     Use libsamplerate to resample to JACK's rate    [auto]
+  CONFIG_SNDIO          Sndio                                           [auto]
+  CONFIG_SUN            Sun Audio                                       [auto]
+  CONFIG_TREMOR         Use Tremor as Ogg/Vorbis input plugin           [n]
+  CONFIG_VORBIS         Ogg/Vorbis (.ogg, application/ogg, audio/x-ogg) [auto]
+  CONFIG_VTX            libayemu (.vtx)                                 [auto]
+  CONFIG_WAVEOUT        Windows Wave Out                                [auto]
+  CONFIG_WAVPACK        WavPack (.wv, audio/x-wavpack)                  [auto]
+  CONFIG_WAV            WAV                                             [y]
+  USE_FALLBACK_IP       Use a specific IP for every unrecognized        [n]
+                        input format. Currently set to FFMPEG.
+
+Also many standard variables like CC, LD, CFLAGS, LDFLAGS are recognized.
+Cross compiling is supported via CROSS=target-prefix-
+    optionally set HOSTCC=this-machine-gcc, HOSTLD, HOST_CFLAGS, HOST_LDFLAGS."
+
+parse_command_line "$@"
+
+case $DEBUG in
+[0-2])
+       ;;
+*)
+       die "DEBUG must be 0-2"
+       ;;
+esac
+
+var_default bindir "${prefix}/bin"
+var_default datadir "${prefix}/share"
+var_default libdir "${prefix}/lib"
+var_default mandir "${datadir}/man"
+var_default exampledir "${datadir}/doc/cmus/examples"
+
+check check_cc
+check check_host_cc
+check check_c11
+check check_cflags
+check check_cc_depgen
+check check_endianness
+check check_compat
+check check_dl
+check check_pthread
+check check_rtsched
+check check_ncurses
+check check_iconv
+check_header byteswap.h && HAVE_BYTESWAP_H=y
+check_string_function "strdup" && HAVE_STRDUP=y
+check_string_function "strndup" && HAVE_STRNDUP=y
+
+check check_cddb       CONFIG_CDDB
+check check_cdio       CONFIG_CDIO
+check check_flac       CONFIG_FLAC
+check check_mad        CONFIG_MAD
+check check_mikmod     CONFIG_MIKMOD
+check check_modplug    CONFIG_MODPLUG
+check check_bass       CONFIG_BASS
+check check_mpc        CONFIG_MPC
+check check_vorbis     CONFIG_VORBIS
+check check_opus       CONFIG_OPUS
+check check_libsystemd CONFIG_MPRIS
+check check_wavpack    CONFIG_WAVPACK
+check check_mp4        CONFIG_MP4
+check check_aac        CONFIG_AAC
+check check_ffmpeg     CONFIG_FFMPEG
+check check_vtx        CONFIG_VTX
+# nothing to check, just validate the variable values
+check true             CONFIG_TREMOR
+check true             CONFIG_WAV
+check true             CONFIG_CUE
+check check_pulse      CONFIG_PULSE
+check check_alsa       CONFIG_ALSA
+check check_jack       CONFIG_JACK
+check check_samplerate CONFIG_SAMPLERATE
+check check_ao         CONFIG_AO
+check check_coreaudio  CONFIG_COREAUDIO
+check check_arts       CONFIG_ARTS
+check check_oss        CONFIG_OSS
+check check_sndio      CONFIG_SNDIO
+check check_sun        CONFIG_SUN
+check check_waveout    CONFIG_WAVEOUT
+check check_roar       CONFIG_ROAR
+
+# discid is only needed if at least one cdda plugin is active
+test -z "$CONFIG_DISCID" && CONFIG_DISCID=a
+if test "$CONFIG_DISCID" = a
+then
+       test "$CONFIG_CDIO" = n && CONFIG_DISCID=n
+fi
+check check_discid  CONFIG_DISCID
+
+test "$WORDS_BIGENDIAN" = y && CFLAGS="${CFLAGS} -DWORDS_BIGENDIAN"
+test "$HAVE_DISCID" = y && CFLAGS="${CFLAGS} -DHAVE_DISCID"
+
+DATADIR="$datadir"
+LIBDIR="$libdir"
+
+config_header config/cdio.h HAVE_CDDB
+config_header config/mpris.h CONFIG_MPRIS
+config_header config/datadir.h DATADIR
+config_header config/libdir.h LIBDIR
+config_header config/debug.h DEBUG
+config_header config/tremor.h CONFIG_TREMOR
+config_header config/modplug.h MODPLUG_API_8
+config_header config/mpc.h MPC_SV8
+config_header config/mp4.h USE_MPEG4IP
+config_header config/curses.h HAVE_RESIZETERM HAVE_USE_DEFAULT_COLORS
+config_header config/ffmpeg.h HAVE_FFMPEG_AVCODEC_H USE_FALLBACK_IP
+config_header config/utils.h HAVE_BYTESWAP_H
+config_header config/iconv.h HAVE_ICONV
+config_header config/samplerate.h HAVE_SAMPLERATE
+config_header config/xmalloc.h HAVE_STRDUP HAVE_STRNDUP
+
+CFLAGS="${CFLAGS} -DHAVE_CONFIG"
+
+makefile_vars bindir datadir libdir mandir exampledir
+makefile_vars \
+       CONFIG_AAC CONFIG_ALSA CONFIG_AO CONFIG_ARTS CONFIG_CDIO \
+       CONFIG_COREAUDIO CONFIG_CUE CONFIG_FFMPEG CONFIG_FLAC CONFIG_JACK \
+       CONFIG_MAD CONFIG_MIKMOD CONFIG_MODPLUG CONFIG_MP4 CONFIG_MPC \
+       CONFIG_MPRIS CONFIG_OPUS CONFIG_OSS CONFIG_PULSE CONFIG_ROAR \
+       CONFIG_SAMPLERATE CONFIG_SNDIO CONFIG_SUN CONFIG_VORBIS CONFIG_VTX \
+       CONFIG_WAV CONFIG_WAVEOUT CONFIG_WAVPACK CONFIG_BASS
+
+generate_config_mk
diff --git a/contrib/README b/contrib/README
new file mode 100644 (file)
index 0000000..6cb0240
--- /dev/null
@@ -0,0 +1,10 @@
+_cmus (originally _cmus_remote)
+       zsh completion for cmus-remote by Christian Schneider strcat AT gmx DOT net
+       extended for the main cmus binary by Frank Terbeck.
+
+cmus-updategaim.py
+        Joshua Kwan <joshk@triplehelix.org>
+
+cmus-updatepidgin.py
+       Based on Joshua's cmus-updategaim.py script.
+       David Thiel <lx@redundancy.redundancy.org>
diff --git a/contrib/_cmus b/contrib/_cmus
new file mode 100644 (file)
index 0000000..7940474
--- /dev/null
@@ -0,0 +1,50 @@
+#compdef cmus cmus-remote
+
+local expl cmus_commands
+
+cmus_commands=(
+  add bind browser-up cd clear colorscheme echo factivate
+  filter fset invert load mark player-next player-pause
+  player-play quit refresh run save search-next search-prev
+  seek set showbind shuffle source toggle unbind unmark view
+  vol win-activate win-add-l win-add-p win-add-Q win-add-q
+  win-bottom win-down win-mv-after win-mv-before win-next
+  win-page-down win-page-up win-remove win-sel-cur
+  win-toggle win-top win-up win-update
+)
+_cmus_volume() {
+  local expl
+  compset -P '[-+]'
+  _wanted list expl volume compadd $expl - {0..100}
+}
+
+case $service in
+  (cmus-remote)
+    _arguments -C -s\
+      '--server[connect using socket SOCKET]:socket:_files' \
+      '--help[display this help and exit]:' \
+      '--version[Display version information and exit.]:' \
+      '(--play -p)'{--play,-p}'[Start playing.]:' \
+      '(--pause -u)'{--pause,-u}'[Toggle pause.]:' \
+      '(--stop -s)'{--stop,-s}'[Stop playing.]:' \
+      '(--next -n)'{--next,-n}'[Skip forward in playlist.]:' \
+      '(--prev -r)'{--prev,-r}'[Skip backward in playlist.]:' \
+      '(--repeat -R)'{--repeat,-R}'[Toggle repeat.]:' \
+      '(--shuffle -S)'{--shuffle,-S}'[Toggle shuffle.]:' \
+      '(--volume -v)'{--volume,-V+}'[Change volume. See vol command in cmus(1).]:volume:_cmus_volume' \
+      '(--seek -k)'{--seek,-k+}'[Seek. See seek command in cmus(1).]:seek [+-]<num>[m/h]'\
+      '(--library -l)'{--library,-l+}'[Modify library instead of playlist.]:playlists/files/directories/URLs:_files'\
+      '(--playlist -P)'{--playlist,-P}'[Modify playlist (default).]::Playlist:_files'\
+      '(--queue -q)'{--queue,-q}'[Modify play queue instead of playlist.]:'\
+      '(--clear -c)'{--clear,-c}'[Clear playlist, library (-l) or play queue (-q).]:playlist'\
+      '(--raw -C)'{--raw,-C+}'[Treat arguments (instead of stdin) as raw commands.]:command:(${cmus_commands[@]}):'\
+    ;;
+  (cmus)
+    _arguments \
+      '--listen[listen on ADDR instead of $CMUS_SOCKET or $XDG_RUNTIME_DIR/cmus-socket]:socket:_files' \
+      '--plugins[list available plugins and exit]'                        \
+      '--help[display this help and exit]'                                \
+      '--version[display version information]'
+    ;;
+esac
+
diff --git a/contrib/cmus-updategaim.py b/contrib/cmus-updategaim.py
new file mode 100644 (file)
index 0000000..515b4e2
--- /dev/null
@@ -0,0 +1,20 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import dbus
+import sys
+
+args = {}
+
+for n in range(1, len(sys.argv) - 1, 2):
+       args[sys.argv[n]] = sys.argv[n + 1]
+
+obj = dbus.SessionBus().get_object("net.sf.gaim.GaimService", "/net/sf/gaim/GaimObject")
+gaim = dbus.Interface(obj, "net.sf.gaim.GaimInterface")
+
+current = gaim.GaimSavedstatusGetCurrent()
+status_type = gaim.GaimSavedstatusGetType(current)
+saved = gaim.GaimSavedstatusNew("", status_type)
+gaim.GaimSavedstatusSetMessage(saved, "♪ %s - %s" % (args["artist"], args["title"]))
+gaim.GaimSavedstatusActivate(saved)
+
diff --git a/contrib/cmus-updatepidgin.py b/contrib/cmus-updatepidgin.py
new file mode 100644 (file)
index 0000000..7d1c7ac
--- /dev/null
@@ -0,0 +1,25 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import dbus
+import sys
+
+args = {}
+
+for n in range(1, len(sys.argv) - 1, 2):
+       args[sys.argv[n]] = sys.argv[n + 1]
+
+print args
+
+bus = dbus.SessionBus()
+
+obj = bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject")
+pidgin = dbus.Interface(obj, "im.pidgin.purple.PurpleInterface")
+
+current = pidgin.PurpleSavedstatusGetCurrent()
+status_type = pidgin.PurpleSavedstatusGetType(current)
+saved = pidgin.PurpleSavedstatusNew("", status_type)
+pidgin.PurpleSavedstatusSetMessage(saved, "♪ %s - %s" % (args["artist"], args["title"]))
+pidgin.PurpleSavedstatusActivate(saved)
+
+
diff --git a/contrib/cmus.bash-completion b/contrib/cmus.bash-completion
new file mode 100644 (file)
index 0000000..08b0a7f
--- /dev/null
@@ -0,0 +1,75 @@
+# bash completion for cmus-remote and cmus
+
+_cmus-remote()
+{
+       local cur prev longopts shortopts
+       cur="${COMP_WORDS[COMP_CWORD]}"
+       prev="${COMP_WORDS[COMP_CWORD-1]}"
+       # maybe we'll differentiate between $cur starting with - or --
+       longopts="--server --passwd --help --version --play --pause --stop
+               --next --prev --file --repeat --shuffle --volume --seek
+               --library --playlist --queue --clear --raw"
+       shortopts="-p -u -s -n -r -f -R -S -v -k -Q -l -P -q -c -C"
+
+       COMPREPLY=()
+
+       case "${prev}" in
+               --server) # can be a hostname[:port] or a filename
+                       compopt -o nospace
+                       _known_hosts_real -c "${cur}"
+                       ;&
+               --file|-f)
+                       _filedir
+                       return 0
+                       ;;
+               --passwd) # do not attempt to complete anything
+                       ;&
+               --volume|-v)
+                       ;&
+               --seek|-k)
+                       ;&
+               --raw|-C)
+               # supporting completion for raw commands would be nice (TODO)
+                       return 0
+                       ;;
+               *)
+                       ;;
+       esac
+
+       if [[ ${cur} == -* ]]; then
+               COMPREPLY=(
+                       $(compgen -W "${shortopts[*]} ${longopts[*]}" -- ${cur})
+               )
+       else
+               _filedir
+       fi
+}
+
+_cmus()
+{
+       local cur prev opts
+       cur="${COMP_WORDS[COMP_CWORD]}"
+       prev="${COMP_WORDS[COMP_CWORD-1]}"
+       opts="--listen --plugins --show-cursor --help --version"
+
+       COMPREPLY=()
+
+       case "${prev}" in
+               --listen)
+                       compopt -o nospace
+                       _ip_addresses
+                       _filedir
+                       return 0;
+                       ;;
+               --plugins|--help|--version)
+                       return 0;
+                       ;;
+               *)
+                       ;;
+       esac
+
+       COMPREPLY=($(compgen -W "${opts[*]}" -- ${cur}))
+}
+
+complete -F _cmus-remote cmus-remote
+complete -F _cmus cmus
diff --git a/convert.c b/convert.c
new file mode 100644 (file)
index 0000000..b8de4ed
--- /dev/null
+++ b/convert.c
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "convert.h"
+#include "xmalloc.h"
+#include "uchar.h"
+#ifdef HAVE_CONFIG
+#include "config/iconv.h"
+#endif
+
+#ifdef HAVE_ICONV
+#include <iconv.h>
+#endif
+#include <string.h>
+#include <errno.h>
+
+ssize_t convert(const char *inbuf, ssize_t inbuf_size,
+               char **outbuf, ssize_t outbuf_estimate,
+               const char *tocode, const char *fromcode)
+{
+#ifdef HAVE_ICONV
+       const char *in;
+       char *out;
+       size_t rc, outbuf_size, inbytesleft, outbytesleft;
+       iconv_t cd;
+       int finished = 0, err_save;
+
+       cd = iconv_open(tocode, fromcode);
+       if (cd == (iconv_t) -1)
+               return -1;
+
+       if (inbuf_size < 0)
+               inbuf_size = strlen(inbuf);
+       inbytesleft = inbuf_size;
+
+       if (outbuf_estimate < 0)
+               outbuf_size = inbuf_size;
+       else
+               outbuf_size = outbuf_estimate;
+       outbytesleft = outbuf_size;
+
+       in = inbuf;
+       out = *outbuf = xnew(char, outbuf_size + 1);
+
+       while (!finished) {
+               finished = 1;
+               rc = iconv(cd, (char **)&in, &inbytesleft, &out, &outbytesleft);
+               if (rc == (size_t) -1) {
+                       if (errno == E2BIG) {
+                               size_t used = out - *outbuf;
+                               outbytesleft += outbuf_size;
+                               outbuf_size *= 2;
+                               *outbuf = xrenew(char, *outbuf, outbuf_size + 1);
+                               out = *outbuf + used;
+                               continue;
+                       } else if (errno != EINVAL)
+                               goto error;
+               }
+       }
+       /* NUL-terminate for safety reasons */
+       *out = '\0';
+       iconv_close(cd);
+       return outbuf_size - outbytesleft;
+
+error:
+       err_save = errno;
+       free(*outbuf);
+       *outbuf = NULL;
+       iconv_close(cd);
+       errno = err_save;
+       return -1;
+
+#else
+       if (inbuf_size < 0)
+               inbuf_size = strlen(inbuf);
+       *outbuf = xnew(char, inbuf_size + 1);
+       memcpy(*outbuf, inbuf, inbuf_size);
+       (*outbuf)[inbuf_size] = '\0';
+       return inbuf_size;
+#endif
+}
+
+int utf8_encode(const char *inbuf, const char *encoding, char **outbuf)
+{
+       size_t inbuf_size, outbuf_size, i;
+       int rc;
+
+       inbuf_size = strlen(inbuf);
+       outbuf_size = inbuf_size;
+       for (i = 0; i < inbuf_size; i++) {
+               unsigned char ch;
+
+               ch = inbuf[i];
+               if (ch > 127)
+                       outbuf_size++;
+       }
+
+       rc = convert(inbuf, inbuf_size, outbuf, outbuf_size, "UTF-8", encoding);
+
+       return rc < 0 ? -1 : 0;
+}
+
+char *to_utf8(const char *str, const char *enc)
+{
+       char *outbuf = NULL;
+       int rc;
+
+       if (u_is_valid(str)) {
+               return xstrdup(str);
+       } else {
+               rc = utf8_encode(str, enc, &outbuf);
+               return rc < 0 ? xstrdup(str) : outbuf;
+       }
+}
diff --git a/convert.h b/convert.h
new file mode 100644 (file)
index 0000000..8c8c7c8
--- /dev/null
+++ b/convert.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_CONVERT_H
+#define CMUS_CONVERT_H
+
+#include <sys/types.h> /* ssize_t */
+
+/* Returns length of *outbuf in bytes (without closing '\0'), -1 on error. */
+ssize_t convert(const char *inbuf, ssize_t inbuf_size,
+               char **outbuf, ssize_t outbuf_estimate,
+               const char *tocode, const char *fromcode);
+
+int utf8_encode(const char *inbuf, const char *encoding, char **outbuf);
+
+char *to_utf8(const char *str, const char *enc);
+
+#endif
diff --git a/cue.c b/cue.c
new file mode 100644 (file)
index 0000000..a04bd77
--- /dev/null
+++ b/cue.c
@@ -0,0 +1,507 @@
+/*
+ * Copyright 2016 Various Authors
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <ctype.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "cue.h"
+#include "xmalloc.h"
+#include "file.h"
+#include "list.h"
+
+#define ASCII_LOWER_TO_UPPER(c) ((c) & ~0x20)
+
+struct cue_track_proto {
+       struct list_head node;
+
+       uint32_t nr;
+       int32_t pregap;
+       int32_t postgap;
+       int32_t index0;
+       int32_t index1;
+
+       struct cue_meta meta;
+};
+
+struct cue_parser {
+       const char *src;
+       size_t len;
+       bool err;
+
+       char *file;
+       struct list_head tracks;
+       size_t num_tracks;
+
+       struct cue_meta meta;
+};
+
+struct cue_switch {
+       const char *cmd;
+       void (*parser)(struct cue_parser *p);
+};
+
+static struct cue_track_proto *cue_proto_from_node(struct list_head *n)
+{
+       return container_of(n, struct cue_track_proto, node);
+}
+
+static struct cue_track_proto *cue_last_proto(struct cue_parser *p)
+{
+       if (p->num_tracks == 0)
+               return NULL;
+       return cue_proto_from_node(p->tracks.prev);
+}
+
+static inline void cue_consume(struct cue_parser *p)
+{
+       p->len--;
+       p->src++;
+}
+
+static void cue_set_err(struct cue_parser *p)
+{
+       p->err = true;
+}
+
+static bool cue_str_eq(const char *a, size_t a_len, const char *b, size_t b_len)
+{
+       if (a_len != b_len)
+               return false;
+       for (size_t i = 0; i < a_len; i++) {
+               if (ASCII_LOWER_TO_UPPER(a[i]) != ASCII_LOWER_TO_UPPER(b[i]))
+                       return false;
+       }
+       return true;
+}
+
+static void cue_skip_spaces(struct cue_parser *p)
+{
+       while (p->len > 0 && (*p->src == ' ' || *p->src == '\t'))
+               cue_consume(p);
+}
+
+static size_t cue_extract_token(struct cue_parser *p, const char **start)
+{
+       cue_skip_spaces(p);
+
+       bool quoted = p->len > 0 && *p->src == '"';
+       if (quoted)
+               cue_consume(p);
+
+       *start = p->src;
+
+       while (p->len > 0) {
+               char c = *p->src;
+               if (c == '\n' || c == '\r')
+                       break;
+               if (quoted) {
+                       if (c == '"')
+                               break;
+               } else {
+                       if (c == ' ' || c == '\t')
+                               break;
+               }
+               cue_consume(p);
+       }
+
+       if (quoted) {
+               size_t len = p->src - *start;
+               if (p->len > 0 && *p->src == '"')
+                       cue_consume(p);
+               return len;
+       }
+
+       return p->src - *start;
+}
+
+static void cue_skip_line(struct cue_parser *p)
+{
+       while (p->len > 0 && *p->src != '\n' && *p->src != '\r')
+               cue_consume(p);
+
+       if (p->len > 0) {
+               char c = *p->src;
+               cue_consume(p);
+               if (p->len > 0 && c == '\r' && *p->src == '\n')
+                       cue_consume(p);
+       }
+}
+
+static char *cue_strdup(const char *start, size_t len)
+{
+       char *s = xnew(char, len + 1);
+       s[len] = 0;
+       memcpy(s, start, len);
+       return s;
+}
+
+static uint32_t cue_parse_int(struct cue_parser *p, const char *start, size_t len)
+{
+       uint32_t val = 0;
+       for (size_t i = 0; i < len; i++) {
+               if (!isdigit(start[i])) {
+                       cue_set_err(p);
+                       return 0;
+               }
+               val = val * 10 + start[i] - '0';
+       }
+       return val;
+}
+
+static void cue_parse_str(struct cue_parser *p, char **dst)
+{
+       const char *start;
+       size_t len = cue_extract_token(p, &start);
+       if (!*dst)
+               *dst = cue_strdup(start, len);
+}
+
+#define CUE_PARSE_STR(field) \
+       static void cue_parse_##field(struct cue_parser *p) \
+       { \
+               struct cue_track_proto *t = cue_last_proto(p); \
+               if (t) \
+                       cue_parse_str(p, &t->meta.field); \
+               else \
+                       cue_parse_str(p, &p->meta.field); \
+       }
+
+CUE_PARSE_STR(performer)
+CUE_PARSE_STR(songwriter)
+CUE_PARSE_STR(title)
+CUE_PARSE_STR(genre)
+CUE_PARSE_STR(date)
+CUE_PARSE_STR(comment)
+CUE_PARSE_STR(compilation);
+
+static void cue_parse_file(struct cue_parser *p)
+{
+       cue_parse_str(p, &p->file);
+}
+
+static void cue_parse_track(struct cue_parser *p)
+{
+       const char *nr;
+       size_t len = cue_extract_token(p, &nr);
+
+       uint32_t d = cue_parse_int(p, nr, len);
+       if (p->err)
+               return;
+
+       struct cue_track_proto *t = xnew(struct cue_track_proto, 1);
+       *t = (struct cue_track_proto) {
+               .nr = d,
+               .pregap = -1,
+               .postgap = -1,
+               .index0 = -1,
+               .index1 = -1,
+       };
+
+       list_add_tail(&t->node, &p->tracks);
+       p->num_tracks++;
+}
+
+static uint32_t cue_parse_time(struct cue_parser *p, const char *start, size_t len)
+{
+       uint32_t vals[] = { 0, 0, 0 };
+       uint32_t *val = vals;
+       for (size_t i = 0; i < len; i++) {
+               if (start[i] == ':') {
+                       if (val != &vals[2]) {
+                               val++;
+                               continue;
+                       }
+                       break;
+               }
+               if (!isdigit(start[i])) {
+                       cue_set_err(p);
+                       return 0;
+               }
+               *val = *val * 10 + start[i] - '0';
+       }
+       return (vals[0] * 60 + vals[1]) * 75 + vals[2];
+}
+
+static void cue_parse_index(struct cue_parser *p)
+{
+       const char *nr;
+       size_t nr_len = cue_extract_token(p, &nr);
+
+       uint32_t d = cue_parse_int(p, nr, nr_len);
+       if (p->err || d > 1)
+               return;
+
+       const char *offset_str;
+       size_t offset_len = cue_extract_token(p, &offset_str);
+
+       uint32_t offset = cue_parse_time(p, offset_str, offset_len);
+       if (p->err)
+               return;
+
+       struct cue_track_proto *last = cue_last_proto(p);
+       if (!last)
+               return;
+
+       if (d == 0)
+               last->index0 = offset;
+       else
+               last->index1 = offset;
+}
+
+static void cue_parse_cmd(struct cue_parser *p, struct cue_switch *s)
+{
+       const char *start;
+       size_t len = cue_extract_token(p, &start);
+
+       while (s->cmd) {
+               if (cue_str_eq(start, len, s->cmd, strlen(s->cmd))) {
+                       s->parser(p);
+                       return;
+               }
+               s++;
+       }
+}
+
+static void cue_parse_rem(struct cue_parser *p)
+{
+       struct cue_switch cmds[] = {
+               { "DATE",        cue_parse_date        },
+               { "GENRE",       cue_parse_genre       },
+               { "COMMENT",     cue_parse_comment     },
+               { "COMPILATION", cue_parse_compilation },
+               { 0 },
+       };
+
+       cue_parse_cmd(p, cmds);
+}
+
+static void cue_parse_gap(struct cue_parser *p, bool post)
+{
+       const char *gap_str;
+       size_t gap_len = cue_extract_token(p, &gap_str);
+
+       uint32_t gap = cue_parse_time(p, gap_str, gap_len);
+       if (p->err)
+               return;
+
+       struct cue_track_proto *last = cue_last_proto(p);
+       if (!last)
+               return;
+
+       if (post)
+               last->postgap = gap;
+       else
+               last->pregap = gap;
+}
+
+static void cue_parse_pregap(struct cue_parser *p)
+{
+       cue_parse_gap(p, false);
+}
+
+static void cue_parse_postgap(struct cue_parser *p)
+{
+       cue_parse_gap(p, true);
+}
+
+static void cue_parse_line(struct cue_parser *p)
+{
+       struct cue_switch cmds[] = {
+               { "FILE",       cue_parse_file       },
+               { "PERFORMER",  cue_parse_performer  },
+               { "SONGWRITER", cue_parse_songwriter },
+               { "TITLE",      cue_parse_title      },
+               { "TRACK",      cue_parse_track      },
+               { "INDEX",      cue_parse_index      },
+               { "REM",        cue_parse_rem        },
+               { "PREGAP",     cue_parse_pregap     },
+               { "POSTGAP",    cue_parse_postgap    },
+               { 0 },
+       };
+
+       cue_parse_cmd(p, cmds);
+       cue_skip_line(p);
+}
+
+static void cue_post_process(struct cue_parser *p)
+{
+       if (!p->file || p->num_tracks == 0) {
+               cue_set_err(p);
+               return;
+       }
+
+       struct cue_track_proto *t;
+
+       int32_t last = -1;
+
+       list_for_each_entry(t, &p->tracks, node) {
+               if (last != -1 && t->nr != last + 1) {
+                       cue_set_err(p);
+                       return;
+               }
+               last = t->nr;
+       }
+
+       last = -1;
+
+       list_for_each_entry(t, &p->tracks, node) {
+               if (t->index0 == -1 && t->index1 == -1) {
+                       cue_set_err(p);
+                       return;
+               }
+               if (t->index0 == -1 || t->index1 == -1) {
+                       int32_t pregap = t->pregap != -1 ? t->pregap : 0;
+                       if (t->index1 != -1)
+                               t->index0 = t->index1 - pregap;
+                       else
+                               t->index1 = t->index0 + pregap;
+               }
+               if (last != -1 && t->index0 < last) {
+                       cue_set_err(p);
+                       return;
+               }
+               int32_t postgap = t->postgap != -1 ? t->postgap : 0;
+               last = t->index1 + postgap;
+       }
+}
+
+static void cue_meta_move(struct cue_meta *l, struct cue_meta *r)
+{
+       *l = *r;
+       *r = (struct cue_meta) { 0 };
+}
+
+static struct cue_sheet *cue_parser_to_sheet(struct cue_parser *p)
+{
+       struct cue_sheet *s = xnew(struct cue_sheet, 1);
+
+       s->file = p->file;
+       p->file = NULL;
+
+       s->tracks = xnew(struct cue_track, p->num_tracks);
+       s->num_tracks = p->num_tracks;
+       s->track_base = cue_last_proto(p)->nr + 1 - p->num_tracks;
+
+       cue_meta_move(&s->meta, &p->meta);
+
+       size_t idx = 0;
+       struct cue_track_proto *t, *prev;
+       list_for_each_entry(t, &p->tracks, node) {
+               s->tracks[idx].offset = t->index1 / 75.0;
+               s->tracks[idx].length = -1;
+
+               if (idx > 0) {
+                       int32_t postgap = prev->postgap != -1 ? prev->postgap : 0;
+                       s->tracks[idx - 1].length =
+                               (t->index0 - prev->index1 - postgap) / 75.0;
+               }
+
+               cue_meta_move(&s->tracks[idx].meta, &t->meta);
+
+               prev = t;
+               idx++;
+       }
+
+       return s;
+}
+
+static void cue_meta_free(struct cue_meta *m)
+{
+       free(m->performer);
+       free(m->songwriter);
+       free(m->title);
+       free(m->genre);
+       free(m->date);
+       free(m->comment);
+       free(m->compilation);
+}
+
+static void cue_parser_free(struct cue_parser *p)
+{
+       struct cue_track_proto *t, *next;
+       list_for_each_entry_safe(t, next, &p->tracks, node) {
+               cue_meta_free(&t->meta);
+               free(t);
+       }
+
+       free(p->file);
+       cue_meta_free(&p->meta);
+}
+
+struct cue_sheet *cue_parse(const char *src, size_t len)
+{
+       struct cue_sheet *res = NULL;
+
+       struct cue_parser p = {
+               .src = src,
+               .len = len,
+       };
+       list_init(&p.tracks);
+
+       while (p.len > 0 && !p.err)
+               cue_parse_line(&p);
+
+       if (p.err)
+               goto out;
+
+       cue_post_process(&p);
+
+       if (p.err)
+               goto out;
+
+       res = cue_parser_to_sheet(&p);
+
+out:
+       cue_parser_free(&p);
+       return res;
+}
+
+struct cue_sheet *cue_from_file(const char *file)
+{
+       ssize_t size;
+       char *buf = mmap_file(file, &size);
+       if (size == -1)
+               return NULL;
+       struct cue_sheet *rv;
+
+       // Check for UTF-8 BOM, and skip ahead if found
+       if (size >= 3 && memcmp(buf, "\xEF\xBB\xBF", 3) == 0) {
+               rv = cue_parse(buf + 3, size - 3);
+       } else {
+               rv = cue_parse(buf, size);
+       }
+
+       munmap(buf, size);
+       return rv;
+}
+
+void cue_free(struct cue_sheet *s)
+{
+       free(s->file);
+
+       for (size_t i = 0; i < s->num_tracks; i++)
+               cue_meta_free(&s->tracks[i].meta);
+       free(s->tracks);
+
+       cue_meta_free(&s->meta);
+       free(s);
+}
diff --git a/cue.h b/cue.h
new file mode 100644 (file)
index 0000000..da20002
--- /dev/null
+++ b/cue.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2016 Various Authors
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_CUE_H
+#define CMUS_CUE_H
+
+#include <stdint.h>
+
+struct cue_meta {
+       char *performer;
+       char *songwriter;
+       char *title;
+       char *genre;
+       char *date;
+       char *comment;
+       char *compilation;
+};
+
+struct cue_track {
+       double offset;
+       double length;
+       struct cue_meta meta;
+};
+
+struct cue_sheet {
+       char *file;
+
+       struct cue_track *tracks;
+       size_t num_tracks;
+       size_t track_base;
+
+       struct cue_meta meta;
+};
+
+struct cue_sheet *cue_parse(const char *src, size_t len);
+struct cue_sheet *cue_from_file(const char *file);
+void cue_free(struct cue_sheet *s);
+
+static inline struct cue_track *cue_get_track(struct cue_sheet *s, size_t n)
+{
+       size_t offset = n - s->track_base;
+       if (n < s->track_base || offset > s->num_tracks)
+               return NULL;
+       return &s->tracks[offset];
+}
+
+#endif
diff --git a/cue_utils.c b/cue_utils.c
new file mode 100644 (file)
index 0000000..afc2641
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2008-2013 Various Authors
+ * Copyright (C) 2011 Gregory Petrosyan
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "path.h"
+#include "utils.h"
+#include "cue_utils.h"
+#include "xmalloc.h"
+#include "cue.h"
+
+#include <stdio.h>
+
+char *associated_cue(const char *filename)
+{
+       FILE *fp;
+       const char *ext;
+       char buf[4096] = {0};
+       const char *dot;
+
+       ext = get_extension(filename);
+       if (ext != NULL && strcmp(ext, "cue") == 0)
+               return NULL;
+
+       dot = strrchr(filename, '.');
+       if (dot == NULL)
+               return NULL;
+
+       snprintf(buf, sizeof buf, "%.*s.cue", (int) (dot - filename), filename);
+       fp = fopen(buf, "r");
+       if (!fp)
+               snprintf(buf, sizeof buf, "%s.cue", filename);
+       else
+               fclose(fp);
+
+       return xstrdup(buf);
+}
+
+
+int cue_get_ntracks(const char *filename)
+{
+       struct cue_sheet *cd = cue_from_file(filename);
+       if (!cd)
+               return -1;
+       size_t n = cd->num_tracks;
+       cue_free(cd);
+       return n;
+}
+
+
+char *construct_cue_url(const char *cue_filename, int track_n)
+{
+       char buf[4096] = {0};
+
+       snprintf(buf, sizeof buf, "cue://%s/%d", cue_filename, track_n);
+
+       return xstrdup(buf);
+}
diff --git a/cue_utils.h b/cue_utils.h
new file mode 100644 (file)
index 0000000..c7ba0e1
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2008-2013 Various Authors
+ * Copyright (C) 2011 Gregory Petrosyan
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_CUE_UTILS_H
+#define CMUS_CUE_UTILS_H
+
+#include <stdio.h>
+
+char *associated_cue(const char *filename);
+int cue_get_ntracks(const char *filename);
+char *construct_cue_url(const char *cue_filename, int track_n);
+
+
+#endif
diff --git a/data/cyan.theme b/data/cyan.theme
new file mode 100644 (file)
index 0000000..5675d68
--- /dev/null
@@ -0,0 +1,23 @@
+set color_cmdline_bg=default
+set color_cmdline_fg=cyan
+set color_error=lightred
+set color_info=lightyellow
+set color_separator=cyan
+set color_statusline_bg=default
+set color_statusline_fg=cyan
+set color_titleline_bg=blue
+set color_titleline_fg=white
+set color_win_bg=default
+set color_win_cur=green
+set color_win_cur_sel_bg=blue
+set color_win_cur_sel_fg=lightyellow
+set color_win_dir=lightblue
+set color_win_fg=cyan
+set color_win_inactive_cur_sel_bg=cyan
+set color_win_inactive_cur_sel_fg=black
+set color_win_inactive_sel_bg=gray
+set color_win_inactive_sel_fg=black
+set color_win_sel_bg=red
+set color_win_sel_fg=gray
+set color_win_title_bg=blue
+set color_win_title_fg=white
diff --git a/data/default.theme b/data/default.theme
new file mode 100644 (file)
index 0000000..1325cf6
--- /dev/null
@@ -0,0 +1,23 @@
+set color_cmdline_bg=default
+set color_cmdline_fg=default
+set color_error=lightred
+set color_info=lightyellow
+set color_separator=blue
+set color_statusline_bg=gray
+set color_statusline_fg=black
+set color_titleline_bg=blue
+set color_titleline_fg=white
+set color_win_bg=default
+set color_win_cur=lightyellow
+set color_win_cur_sel_bg=blue
+set color_win_cur_sel_fg=lightyellow
+set color_win_dir=lightblue
+set color_win_fg=default
+set color_win_inactive_cur_sel_bg=gray
+set color_win_inactive_cur_sel_fg=lightyellow
+set color_win_inactive_sel_bg=gray
+set color_win_inactive_sel_fg=black
+set color_win_sel_bg=blue
+set color_win_sel_fg=white
+set color_win_title_bg=blue
+set color_win_title_fg=white
diff --git a/data/dracula.theme b/data/dracula.theme
new file mode 100644 (file)
index 0000000..8df7d5a
--- /dev/null
@@ -0,0 +1,24 @@
+### 'Dracula' theme for CMus (for 255 color terms)
+set color_cmdline_bg=default
+set color_cmdline_fg=231
+set color_error=009
+set color_info=231
+set color_separator=004
+set color_statusline_bg=default
+set color_statusline_fg=231
+set color_titleline_bg=010
+set color_titleline_fg=236
+set color_win_bg=default
+set color_win_cur=010
+set color_win_cur_sel_bg=010
+set color_win_cur_sel_fg=016
+set color_win_dir=004
+set color_win_fg=004
+set color_win_inactive_cur_sel_bg=default
+set color_win_inactive_cur_sel_fg=014
+set color_win_inactive_sel_bg=default
+set color_win_inactive_sel_fg=014
+set color_win_sel_bg=004
+set color_win_sel_fg=236
+set color_win_title_bg=default
+set color_win_title_fg=014
diff --git a/data/gray-88.theme b/data/gray-88.theme
new file mode 100644 (file)
index 0000000..93c905c
--- /dev/null
@@ -0,0 +1,24 @@
+# 88 color gray theme for urxvt
+set color_cmdline_bg=80
+set color_cmdline_fg=85
+set color_error=lightgreen
+set color_info=green
+set color_separator=darkgray
+set color_statusline_bg=80
+set color_statusline_fg=85
+set color_titleline_bg=darkgray
+set color_titleline_fg=85
+set color_win_bg=80
+set color_win_cur=lightgreen
+set color_win_cur_sel_bg=82
+set color_win_cur_sel_fg=lightyellow
+set color_win_dir=83
+set color_win_fg=85
+set color_win_inactive_cur_sel_bg=darkgray
+set color_win_inactive_cur_sel_fg=lightgreen
+set color_win_inactive_sel_bg=darkgray
+set color_win_inactive_sel_fg=black
+set color_win_sel_bg=82
+set color_win_sel_fg=black
+set color_win_title_bg=darkgray
+set color_win_title_fg=85
diff --git a/data/green-mono-88.theme b/data/green-mono-88.theme
new file mode 100644 (file)
index 0000000..020bfbd
--- /dev/null
@@ -0,0 +1,62 @@
+# Clay's Green Monochromatic Color Scheme 1.0 and example file.
+#
+# This theme is designed to be 1) Very readable on high-resolution or blurry
+# displays (dying CRTs and TVs, for example).  2) Be more accessable to color-
+# blind people, by removing chroma cues in favor of brightness and color
+# inversion.  3) Show how to incorperate named and numbered colors into a
+# single theme.  4) Provide a stating point for creating other mono-chromatic
+# themes.
+#
+# Questions, comments (just a heads-up that you use/like this, especially),
+# and such should be directed to clay.barnes@gmail.com
+
+##### Common User Interface Components #########################################
+# Default text color
+set color_win_fg=green
+
+# Overall background color
+set color_win_bg=black
+
+# Command-line colors
+set color_cmdline_bg=black
+set color_cmdline_fg=green
+set color_error=lightgreen
+set color_info=green
+set color_separator=green
+
+# Bottom status line
+set color_statusline_bg=black
+set color_statusline_fg=green
+
+# Bottom title line
+set color_titleline_bg=green
+set color_titleline_fg=black
+
+# Top title area
+set color_win_title_bg=green
+set color_win_title_fg=black
+##### Playing File Colors ######################################################
+# Unselected currently playing track's text
+set color_win_cur=lightgreen
+
+# Active selection for currently playing track
+set color_win_cur_sel_bg=28
+set color_win_cur_sel_fg=darkgray
+
+# Inactive selection for currently playing track
+set color_win_inactive_cur_sel_bg=20
+set color_win_inactive_cur_sel_fg=lightgreen
+
+##### Non-Playing File Colors ##################################################
+# Active selection
+set color_win_sel_bg=green
+set color_win_sel_fg=black
+
+# Inactive selection
+set color_win_inactive_sel_bg=20
+set color_win_inactive_sel_fg=black
+
+##### File Browser View Colors #################################################
+# Directory listing color
+set color_win_dir=lightgreen
+
diff --git a/data/green.theme b/data/green.theme
new file mode 100644 (file)
index 0000000..3ef19bb
--- /dev/null
@@ -0,0 +1,65 @@
+# Another Green Theme for cmus
+#
+# Change this to a red or blue theme by doing the following in vim:
+#
+#  :%s/lightgreen/light<color>/g
+#  :%s/green/<color>/g
+#
+# - roobert@gmail.com
+
+# Directory colors
+set color_win_dir=default
+
+# Normal text
+set color_win_fg=default
+
+# Window background color.
+set color_win_bg=default
+
+# Command line color.
+set color_cmdline_bg=default
+set color_cmdline_fg=default
+
+# Color of error messages displayed on the command line.
+set color_error=lightred
+
+# Color of informational messages displayed on the command line.
+set color_info=lightgreen
+
+# Color of currently playing track.
+set color_win_cur=lightgreen
+
+# Color of the separator line between windows in view (1).
+set color_separator=black
+
+# Color of window titles (topmost line of the screen).
+set color_win_title_bg=default
+set color_win_title_fg=green
+
+# Status line color.
+set color_statusline_bg=default
+set color_statusline_fg=gray
+
+# Color of the line displaying currently playing track.
+set color_titleline_bg=default
+set color_titleline_fg=green
+
+# Color of the selected row which is also the currently playing track in active window.
+set color_win_cur_sel_bg=default
+set color_win_cur_sel_fg=white
+
+# Color of the selected row which is also the currently playing track in inactive window.
+set color_win_inactive_cur_sel_bg=default
+set color_win_inactive_cur_sel_fg=lightgreen
+
+# Color of selected row in inactive window.
+set color_win_inactive_sel_bg=default
+set color_win_inactive_sel_fg=default
+
+# Color of selected row in active window.
+set color_win_sel_bg=default
+set color_win_sel_fg=green
+
+# Command line color.
+set color_cmdline_bg=default
+set color_cmdline_fg=default
diff --git a/data/gruvbox-alt.theme b/data/gruvbox-alt.theme
new file mode 100644 (file)
index 0000000..d7e0ff7
--- /dev/null
@@ -0,0 +1,53 @@
+# colors from gruvbox: https://github.com/morhetz/gruvbox
+
+# default text color: fg
+set color_win_fg=223
+
+# overall background color: bg
+set color_win_bg=235
+
+# top title area: bg2/light-blue
+set color_win_title_bg=239
+set color_win_title_fg=109
+
+# separator line between windows in view (1): dark-gray
+set color_separator=245
+
+# bottom title line: dark-gray/bg0
+set color_titleline_bg=245
+set color_titleline_fg=235
+
+# bottom status line: bg0_s/dark-aqua
+set color_statusline_bg=236
+set color_statusline_fg=72
+
+# command-line colors: bg/orange/light-red/light-yellow
+set color_cmdline_bg=235
+set color_cmdline_fg=208
+set color_error=167
+set color_info=214
+
+##### playing file colors ######################################################
+# unselected currently playing track's text: light-green
+set color_win_cur=142
+
+# active selection for currently playing track: bg1/light-green
+set color_win_cur_sel_bg=237
+set color_win_cur_sel_fg=142
+
+# inactive selection for currently playing track: bg/light-green
+set color_win_inactive_cur_sel_bg=235
+set color_win_inactive_cur_sel_fg=142
+
+##### non-playing file colors ##################################################
+# active selection: bg1/fg0
+set color_win_sel_bg=237
+set color_win_sel_fg=229
+
+# inactive selection: bg/fg0
+set color_win_inactive_sel_bg=235
+set color_win_inactive_sel_fg=229
+
+##### file browser view colors #################################################
+# directory listing color: fg
+set color_win_dir=223
diff --git a/data/gruvbox.theme b/data/gruvbox.theme
new file mode 100644 (file)
index 0000000..3e35407
--- /dev/null
@@ -0,0 +1,49 @@
+# colors from gruvbox: https://github.com/morhetz/gruvbox
+# default text color
+set color_win_fg=default
+
+# overall background color
+set color_win_bg=default
+
+# command-line colors
+set color_cmdline_bg=default
+set color_cmdline_fg=default
+set color_error=124
+set color_info=172
+set color_separator=246
+
+# bottom status line
+set color_statusline_bg=237
+set color_statusline_fg=72
+
+# bottom title line
+set color_titleline_bg=236
+set color_titleline_fg=142
+
+# top title area
+set color_win_title_bg=246
+set color_win_title_fg=235
+##### playing file colors ######################################################
+# unselected currently playing track's text
+set color_win_cur=175
+
+# active selection for currently playing track
+set color_win_cur_sel_bg=237
+set color_win_cur_sel_fg=175
+
+# inactive selection for currently playing track
+set color_win_inactive_cur_sel_bg=236
+set color_win_inactive_cur_sel_fg=175
+
+##### non-playing file colors ##################################################
+# active selection
+set color_win_sel_bg=237
+set color_win_sel_fg=229
+
+# inactive selection
+set color_win_inactive_sel_bg=236
+set color_win_inactive_sel_fg=229
+
+##### file browser view colors #################################################
+# directory listing color
+set color_win_dir=229
diff --git a/data/jellybeans.theme b/data/jellybeans.theme
new file mode 100644 (file)
index 0000000..27e11b6
--- /dev/null
@@ -0,0 +1,53 @@
+# Jellybeans colorscheme
+# by: Daniel Rivas (https://github.com/DanielRS)
+# based on: https://github.com/nanotech/jellybeans.vim
+# term codes from: http://vim.wikia.com/wiki/Xterm256_color_names_for_console_Vim
+
+# Window
+set color_cmdline_bg=233
+set color_cmdline_fg=188
+set color_cmdline_attr=default
+
+set color_win_title_bg=237
+set color_win_title_fg=110
+set color_win_title_attr=default
+
+set color_win_fg=188
+set color_win_bg=233
+set color_win_attr=default
+
+set color_separator=107
+
+# Bottom status
+set color_titleline_bg=186
+set color_titleline_fg=237
+set color_titleline_attr=default
+
+set color_statusline_bg=237
+set color_statusline_fg=145
+set color_statusline_attr=default
+
+# Text
+set color_win_cur=183
+set color_win_dir=186
+
+# Menu
+set color_win_sel_bg=254
+set color_win_sel_fg=237
+set color_win_sel_attr=default
+
+set color_win_inactive_sel_bg=237
+set color_win_inactive_sel_fg=145
+set color_win_inactive_sel_attr=default
+
+set color_win_cur_sel_bg=186
+set color_win_cur_sel_fg=237
+set color_win_cur_sel_attr=default
+
+set color_win_inactive_cur_sel_bg=242
+set color_win_inactive_cur_sel_fg=186
+set color_win_inactive_cur_sel_attr=default
+
+# Messages
+set color_error=167
+set color_info=186
diff --git a/data/night.theme b/data/night.theme
new file mode 100644 (file)
index 0000000..39b64d6
--- /dev/null
@@ -0,0 +1,27 @@
+### 'Night' theme for CMus (for 255 color terms)
+### (C) 2013 Mladen Pejaković <pejakm@autistici.org>
+### {{{
+set color_cmdline_bg=default
+set color_cmdline_fg=255
+set color_error=196
+set color_info=220
+set color_separator=002
+set color_statusline_bg=234
+set color_statusline_fg=045
+set color_titleline_bg=default
+set color_titleline_fg=046
+set color_win_bg=default
+set color_win_cur=046
+set color_win_cur_sel_bg=235
+set color_win_cur_sel_fg=118
+set color_win_dir=250
+set color_win_fg=255
+set color_win_inactive_cur_sel_bg=233
+set color_win_inactive_cur_sel_fg=046
+set color_win_inactive_sel_bg=234
+set color_win_inactive_sel_fg=002
+set color_win_sel_bg=235
+set color_win_sel_fg=045
+set color_win_title_bg=234
+set color_win_title_fg=045
+### }}}
\ No newline at end of file
diff --git a/data/rc b/data/rc
new file mode 100644 (file)
index 0000000..8061992
--- /dev/null
+++ b/data/rc
@@ -0,0 +1,89 @@
+bind browser backspace browser-up
+bind browser space win-activate
+bind browser i toggle show_hidden
+bind browser u win-update
+bind common mouse_scroll_up win-up
+bind common mouse_scroll_down win-down
+bind common mlb_click_selected win-activate
+bind common ! push shell 
+bind common + vol +10%
+bind common , seek -1m
+bind common - vol -10%
+bind common . seek +1m
+bind common / search-start
+bind common 1 view tree
+bind common 2 view sorted
+bind common 3 view playlist
+bind common 4 view queue
+bind common 5 view browser
+bind common 6 view filters
+bind common 7 view settings
+bind common = vol +10%
+bind common ? search-b-start
+bind common C toggle continue
+bind common D win-remove
+bind common E win-add-Q
+bind common F push filter 
+bind common G win-bottom
+bind common I echo {}
+bind common L push live-filter 
+bind common M toggle play_library
+bind common N search-prev
+bind common P win-mv-before
+bind common [ vol +1% +0
+bind common ] vol +0 +1%
+bind common ^B win-page-up
+bind common ^U win-half-page-up
+bind common ^C echo Type :quit<enter> to exit cmus.
+bind common ^E win-scroll-down
+bind common ^F win-page-down
+bind common ^D win-half-page-down
+bind common ^L refresh
+bind common ^R toggle repeat_current
+bind common ^Y win-scroll-up
+bind common a win-add-l
+bind common b player-next
+bind common c player-pause
+bind common delete win-remove
+bind common down win-down
+bind common e win-add-q
+bind common end win-bottom
+bind common enter win-activate
+bind common f toggle follow
+bind common g win-top
+bind common h seek -5
+bind common home win-top
+bind common i win-sel-cur
+bind common j win-down
+bind common k win-up
+bind common l seek +5
+bind common left seek -5
+bind common m toggle aaa_mode
+bind common n search-next
+bind common o toggle play_sorted
+bind common p win-mv-after
+bind common q quit -i
+bind common page_down win-page-down
+bind common page_up win-page-up
+bind common r toggle repeat
+bind common right seek +5
+bind common s toggle shuffle
+bind common space win-toggle
+bind common t toggle show_remaining_time
+bind common tab win-next
+bind common u update-cache
+bind common U win-update-cache
+bind common up win-up
+bind common v player-stop
+bind common x player-play
+bind common y win-add-p
+bind common z player-prev
+bind common { vol -1% -0
+bind common } vol -0 -1%
+fset 90s=date>=1990&date<2000
+fset classical=genre="Classical"
+fset unheard=play_count=0
+fset missing-tag=!stream&(artist=""|album=""|title=""|tracknumber=-1|date=-1)
+fset mp3=filename="*.mp3"
+fset ogg=filename="*.ogg"
+fset ogg-or-mp3=ogg|mp3
diff --git a/data/solarized-dark.theme b/data/solarized-dark.theme
new file mode 100644 (file)
index 0000000..95041ef
--- /dev/null
@@ -0,0 +1,49 @@
+# colors from solarized: http://ethanschoonover.com/solarized
+# default text color
+set color_win_fg=default
+
+# overall background color
+set color_win_bg=default
+
+# command-line colors
+set color_cmdline_bg=default
+set color_cmdline_fg=default
+set color_error=160
+set color_info=136
+set color_separator=240
+
+# bottom status line
+set color_statusline_bg=0
+set color_statusline_fg=37
+
+# bottom title line
+set color_titleline_bg=0
+set color_titleline_fg=136
+
+# top title area
+set color_win_title_bg=0
+set color_win_title_fg=64
+##### playing file colors ######################################################
+# unselected currently playing track's text
+set color_win_cur=33
+
+# active selection for currently playing track
+set color_win_cur_sel_bg=0
+set color_win_cur_sel_fg=33
+
+# inactive selection for currently playing track
+set color_win_inactive_cur_sel_bg=default
+set color_win_inactive_cur_sel_fg=125
+
+##### non-playing file colors ##################################################
+# active selection
+set color_win_sel_bg=0
+set color_win_sel_fg=166
+
+# inactive selection
+set color_win_inactive_sel_bg=default
+set color_win_inactive_sel_fg=125
+
+##### file browser view colors #################################################
+# directory listing color
+set color_win_dir=33
diff --git a/data/solarized-light.theme b/data/solarized-light.theme
new file mode 100644 (file)
index 0000000..8d8aaac
--- /dev/null
@@ -0,0 +1,49 @@
+# colors from solarized: http://ethanschoonover.com/solarized
+# default text color
+set color_win_fg=default
+
+# overall background color
+set color_win_bg=default
+
+# command-line colors
+set color_cmdline_bg=default
+set color_cmdline_fg=default
+set color_error=160
+set color_info=136
+set color_separator=245
+
+# bottom status line
+set color_statusline_bg=0
+set color_statusline_fg=37
+
+# bottom title line
+set color_titleline_bg=0
+set color_titleline_fg=136
+
+# top title area
+set color_win_title_bg=0
+set color_win_title_fg=64
+##### playing file colors ######################################################
+# unselected currently playing track's text
+set color_win_cur=33
+
+# active selection for currently playing track
+set color_win_cur_sel_bg=0
+set color_win_cur_sel_fg=33
+
+# inactive selection for currently playing track
+set color_win_inactive_cur_sel_bg=default
+set color_win_inactive_cur_sel_fg=125
+
+##### non-playing file colors ##################################################
+# active selection
+set color_win_sel_bg=0
+set color_win_sel_fg=166
+
+# inactive selection
+set color_win_inactive_sel_bg=default
+set color_win_inactive_sel_fg=125
+
+##### file browser view colors #################################################
+# directory listing color
+set color_win_dir=33
diff --git a/data/xterm-white.theme b/data/xterm-white.theme
new file mode 100644 (file)
index 0000000..fc475a7
--- /dev/null
@@ -0,0 +1,24 @@
+# looks good with xterm using default settings (white bg)
+set color_cmdline_bg=default
+set color_cmdline_fg=default
+set color_error=lightred
+set color_info=lightblue
+set color_separator=default
+set color_statusline_bg=default
+set color_statusline_fg=default
+set color_titleline_bg=gray
+set color_titleline_fg=default
+set color_win_bg=default
+set color_win_cur=lightblue
+set color_win_cur_sel_bg=green
+set color_win_cur_sel_fg=lightblue
+set color_win_dir=blue
+set color_win_fg=default
+set color_win_inactive_cur_sel_bg=gray
+set color_win_inactive_cur_sel_fg=lightblue
+set color_win_inactive_sel_bg=gray
+set color_win_inactive_sel_fg=default
+set color_win_sel_bg=green
+set color_win_sel_fg=default
+set color_win_title_bg=gray
+set color_win_title_fg=black
diff --git a/data/zenburn.theme b/data/zenburn.theme
new file mode 100644 (file)
index 0000000..a7481b0
--- /dev/null
@@ -0,0 +1,52 @@
+# Directory colors
+set color_win_dir=108
+
+# Normal text
+set color_win_fg=188
+
+# Window background color.
+set color_win_bg=237
+
+# Command line color.
+set color_cmdline_bg=237
+set color_cmdline_fg=108
+
+# Color of error messages displayed on the command line.
+set color_error=lightred
+
+# Color of informational messages displayed on the command line.
+set color_info=lightgreen
+
+# Color of the separator line between windows in view (1).
+set color_separator=246
+
+# Color of window titles (topmost line of the screen).
+set color_win_title_bg=235
+set color_win_title_fg=174
+
+# Status line color.
+set color_statusline_bg=237
+set color_statusline_fg=142
+
+# Color of currently playing track.
+set color_win_cur=172
+
+# Color of the line displaying currently playing track.
+set color_titleline_bg=235
+set color_titleline_fg=144
+
+# Color of the selected row which is also the currently playing track in active window.
+set color_win_cur_sel_bg=235
+set color_win_cur_sel_fg=223
+
+# Color of the selected row which is also the currently playing track in inactive window.
+set color_win_inactive_cur_sel_bg=238
+set color_win_inactive_cur_sel_fg=116
+
+# Color of selected row in active window.
+set color_win_sel_bg=235
+set color_win_sel_fg=223
+
+# Color of selected row in inactive window.
+set color_win_inactive_sel_bg=238
+set color_win_inactive_sel_fg=116
diff --git a/debug.c b/debug.c
new file mode 100644 (file)
index 0000000..6e114b6
--- /dev/null
+++ b/debug.c
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "debug.h"
+#include "prog.h"
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <sys/time.h>
+
+#if DEBUG > 1
+static FILE *debug_stream = NULL;
+#endif
+
+void debug_init(void)
+{
+#if DEBUG > 1
+       char filename[512];
+       const char *dir = getenv("CMUS_HOME");
+
+       if (!dir || !dir[0]) {
+               dir = getenv("HOME");
+               if (!dir)
+                       die("error: environment variable HOME not set\n");
+       }
+       snprintf(filename, sizeof(filename), "%s/cmus-debug.txt", dir);
+
+       debug_stream = fopen(filename, "w");
+       if (debug_stream == NULL)
+               die_errno("error opening `%s' for writing", filename);
+#endif
+}
+
+/* This function must be defined even if debugging is disabled in the program
+ * because debugging might still be enabled in some plugin.
+ */
+void _debug_bug(const char *function, const char *fmt, ...)
+{
+       const char *format = "\n%s: BUG: ";
+       va_list ap;
+
+       /* debug_stream exists only if debugging is enabled */
+#if DEBUG > 1
+       fprintf(debug_stream, format, function);
+       va_start(ap, fmt);
+       vfprintf(debug_stream, fmt, ap);
+       va_end(ap);
+#endif
+
+       /* always print bug message to stderr */
+       fprintf(stderr, format, function);
+       va_start(ap, fmt);
+       vfprintf(stderr, fmt, ap);
+       va_end(ap);
+       exit(127);
+}
+
+void _debug_print(const char *function, const char *fmt, ...)
+{
+#if DEBUG > 1
+       va_list ap;
+
+       fprintf(debug_stream, "%s: ", function);
+       va_start(ap, fmt);
+       vfprintf(debug_stream, fmt, ap);
+       va_end(ap);
+       fflush(debug_stream);
+#endif
+}
+
+uint64_t timer_get(void)
+{
+#if DEBUG > 1
+       struct timeval tv;
+
+       gettimeofday(&tv, NULL);
+       return tv.tv_sec * 1e6L + tv.tv_usec;
+#else
+       return 0;
+#endif
+}
+
+void timer_print(const char *what, uint64_t usec)
+{
+#if DEBUG > 1
+       uint64_t a = usec / 1e6;
+       uint64_t b = usec - a * 1e6;
+
+       _debug_print("TIMER", "%s: %11u.%06u\n", what, (unsigned int)a, (unsigned int)b);
+#endif
+}
diff --git a/debug.h b/debug.h
new file mode 100644 (file)
index 0000000..9509f8a
--- /dev/null
+++ b/debug.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_DEBUG_H
+#define CMUS_DEBUG_H
+
+#include "compiler.h"
+#ifdef HAVE_CONFIG
+#include "config/debug.h"
+#endif
+
+#include <errno.h>
+#include <stdint.h>
+
+void debug_init(void);
+void _debug_bug(const char *function, const char *fmt, ...) CMUS_FORMAT(2, 3) CMUS_NORETURN;
+void _debug_print(const char *function, const char *fmt, ...) CMUS_FORMAT(2, 3);
+
+uint64_t timer_get(void);
+void timer_print(const char *what, uint64_t usec);
+
+#define BUG(...) _debug_bug(__FUNCTION__, __VA_ARGS__)
+
+#define CMUS_STR(a) #a
+
+#define BUG_ON(a)                      \
+do {                                   \
+       if (unlikely(a))                \
+               BUG("%s\n", CMUS_STR(a));       \
+} while (0)
+
+#define d_print(...) _debug_print(__FUNCTION__, __VA_ARGS__)
+
+#endif
diff --git a/discid.c b/discid.c
new file mode 100644 (file)
index 0000000..7cb1146
--- /dev/null
+++ b/discid.c
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2011-2013 Various Authors
+ * Copyright 2011 Johannes Weißl
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "discid.h"
+#include "xmalloc.h"
+#include "path.h"
+#include "utils.h"
+#include "debug.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <limits.h>
+
+#ifdef HAVE_DISCID
+#include <discid/discid.h>
+#endif
+
+char *get_default_cdda_device(void)
+{
+       const char *dev = NULL;
+#ifdef HAVE_DISCID
+       dev = discid_get_default_device();
+#endif
+       if (!dev)
+               dev = "/dev/cdrom";
+       return xstrdup(dev);
+}
+
+int parse_cdda_url(const char *url, char **disc_id, int *start_track, int *end_track)
+{
+       char *slash, *dash;
+       long int t;
+
+       if (!is_cdda_url(url))
+               return 0;
+       url += 7;
+
+       slash = strrchr(url, '/');
+       if (slash) {
+               *disc_id = xstrndup(url, slash - url);
+               url = slash + 1;
+       }
+       dash = strchr(url, '-');
+       if (dash) {
+               char *tmp = xstrndup(url, dash - url);
+               if (str_to_int(tmp, &t) == 0)
+                       *start_track = t;
+               if (end_track) {
+                       if (str_to_int(dash + 1, &t) == 0)
+                               *end_track = t;
+                       else
+                               *end_track = INT_MAX;
+               }
+               free(tmp);
+       } else {
+               if (str_to_int(url, &t) == 0)
+                       *start_track = t;
+       }
+
+       return 1;
+}
+
+char *gen_cdda_url(const char *disc_id, int start_track, int end_track)
+{
+       char buf[256];
+       if (end_track != -1)
+               snprintf(buf, sizeof(buf), "cdda://%s/%d-%d", disc_id, start_track, end_track);
+       else
+               snprintf(buf, sizeof(buf), "cdda://%s/%d", disc_id, start_track);
+       return xstrdup(buf);
+}
+
+char *complete_cdda_url(const char *device, const char *url)
+{
+       char *new_url, *url_disc_id = NULL, *disc_id = NULL;
+       int is_range, start_track = -1, end_track = -1, num_tracks = -1;
+
+       parse_cdda_url(url, &url_disc_id, &start_track, &end_track);
+       is_range = (start_track == -1 && end_track == -1) || end_track == INT_MAX;
+       if (!url_disc_id || is_range) {
+               if (url_disc_id && strchr(url_disc_id, '/'))
+                       device = url_disc_id;
+               get_disc_id(device, &disc_id, &num_tracks);
+               if (is_range)
+                       end_track = num_tracks;
+               if (!url_disc_id)
+                       url_disc_id = disc_id;
+       }
+       if (start_track == -1)
+               start_track = 1;
+
+       new_url = gen_cdda_url(url_disc_id, start_track, end_track);
+       free(disc_id);
+
+       return new_url;
+}
+
+static int get_device_disc_id(const char *device, char **disc_id, int *num_tracks)
+{
+#ifdef HAVE_DISCID
+       DiscId *disc = discid_new();
+       if (!disc)
+               return 0;
+
+       if (!discid_read(disc, device)) {
+               d_print("%s\n", discid_get_error_msg(disc));
+               discid_free(disc);
+               return 0;
+       }
+
+       *disc_id = xstrdup(discid_get_id(disc));
+       if (num_tracks)
+               *num_tracks = discid_get_last_track_num(disc);
+
+       discid_free(disc);
+       return 1;
+#else
+       return 0;
+#endif
+}
+
+int get_disc_id(const char *device, char **disc_id, int *num_tracks)
+{
+       struct stat st;
+
+       if (stat(device, &st) == -1)
+               return 0;
+
+       if (S_ISBLK(st.st_mode))
+               return get_device_disc_id(device, disc_id, num_tracks);
+
+       *disc_id = path_absolute(device);
+       return 1;
+}
diff --git a/discid.h b/discid.h
new file mode 100644 (file)
index 0000000..40e9f6d
--- /dev/null
+++ b/discid.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2011-2013 Various Authors
+ * Copyright 2011 Johannes Weißl
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_DISCID_H
+#define CMUS_DISCID_H
+
+char *get_default_cdda_device(void);
+int parse_cdda_url(const char *url, char **disc_id, int *start_track, int *end_track);
+char *gen_cdda_url(const char *disc_id, int start_track, int end_track);
+char *complete_cdda_url(const char *device, const char *url);
+int get_disc_id(const char *device, char **disc_id, int *num_tracks);
+
+#endif
diff --git a/editable.c b/editable.c
new file mode 100644 (file)
index 0000000..8b0a41a
--- /dev/null
@@ -0,0 +1,466 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "editable.h"
+#include "search.h"
+#include "track.h"
+#include "track_info.h"
+#include "expr.h"
+#include "filters.h"
+#include "locking.h"
+#include "mergesort.h"
+#include "xmalloc.h"
+
+static const struct searchable_ops simple_search_ops = {
+       .get_prev = simple_track_get_prev,
+       .get_next = simple_track_get_next,
+       .get_current = simple_track_search_get_current,
+       .matches = simple_track_search_matches
+};
+
+static struct simple_track *get_selected(struct editable *e)
+{
+       struct iter sel;
+
+       if (window_get_sel(e->shared->win, &sel))
+               return iter_to_simple_track(&sel);
+       return NULL;
+}
+
+void editable_shared_init(struct editable_shared *shared,
+               editable_free_track free_track)
+{
+       shared->win = window_new(simple_track_get_prev, simple_track_get_next);
+       shared->sort_keys = xnew(sort_key_t, 1);
+       shared->sort_keys[0] = SORT_INVALID;
+       shared->sort_str[0] = 0;
+       shared->free_track = free_track;
+       shared->owner = NULL;
+
+       struct iter iter = { 0 };
+       shared->searchable = searchable_new(shared->win, &iter,
+                       &simple_search_ops);
+}
+
+void editable_init(struct editable *e, struct editable_shared *shared,
+               int take_ownership)
+{
+       list_init(&e->head);
+       e->tree_root = RB_ROOT;
+       e->nr_tracks = 0;
+       e->nr_marked = 0;
+       e->total_time = 0;
+       e->shared = shared;
+
+
+       if (take_ownership)
+               editable_take_ownership(e);
+}
+
+static int editable_owns_shared(struct editable *e)
+{
+       return e->shared->owner == e;
+}
+
+void editable_take_ownership(struct editable *e)
+{
+       if (!editable_owns_shared(e)) {
+               e->shared->owner = e;
+               window_set_contents(e->shared->win, &e->head);
+               e->shared->win->changed = 1;
+
+               struct iter iter = { .data0 = &e->head };
+               searchable_set_head(e->shared->searchable, &iter);
+       }
+}
+
+static void do_editable_add(struct editable *e, struct simple_track *track, int tiebreak)
+{
+       sorted_list_add_track(&e->head, &e->tree_root, track,
+                       e->shared->sort_keys, tiebreak);
+       e->nr_tracks++;
+       if (track->info->duration != -1)
+               e->total_time += track->info->duration;
+       if (editable_owns_shared(e))
+               window_changed(e->shared->win);
+}
+
+void editable_add(struct editable *e, struct simple_track *track)
+{
+       do_editable_add(e, track, +1);
+}
+
+void editable_add_before(struct editable *e, struct simple_track *track)
+{
+       do_editable_add(e, track, -1);
+}
+
+void editable_remove_track(struct editable *e, struct simple_track *track)
+{
+       struct track_info *ti = track->info;
+       struct iter iter;
+
+       editable_track_to_iter(e, track, &iter);
+       if (editable_owns_shared(e))
+               window_row_vanishes(e->shared->win, &iter);
+
+       e->nr_tracks--;
+       e->nr_marked -= track->marked;
+       if (ti->duration != -1)
+               e->total_time -= ti->duration;
+
+       sorted_list_remove_track(&e->head, &e->tree_root, track);
+       e->shared->free_track(e, &track->node);
+}
+
+void editable_remove_sel(struct editable *e)
+{
+       struct simple_track *t;
+
+       if (e->nr_marked) {
+               /* treat marked tracks as selected */
+               struct list_head *next, *item = e->head.next;
+
+               while (item != &e->head) {
+                       next = item->next;
+                       t = to_simple_track(item);
+                       if (t->marked)
+                               editable_remove_track(e, t);
+                       item = next;
+               }
+       } else {
+               t = get_selected(e);
+               if (t)
+                       editable_remove_track(e, t);
+       }
+}
+
+void editable_sort(struct editable *e)
+{
+       if (e->nr_tracks <= 1)
+               return;
+       sorted_list_rebuild(&e->head, &e->tree_root, e->shared->sort_keys);
+
+       if (editable_owns_shared(e)) {
+               window_changed(e->shared->win);
+               window_goto_top(e->shared->win);
+       }
+}
+
+void editable_shared_set_sort_keys(struct editable_shared *shared,
+               sort_key_t *keys)
+{
+       free(shared->sort_keys);
+       shared->sort_keys = keys;
+}
+
+void editable_toggle_mark(struct editable *e)
+{
+       struct simple_track *t;
+
+       t = get_selected(e);
+       if (t) {
+               e->nr_marked -= t->marked;
+               t->marked ^= 1;
+               e->nr_marked += t->marked;
+               if (editable_owns_shared(e)) {
+                       e->shared->win->changed = 1;
+                       window_down(e->shared->win, 1);
+               }
+       }
+}
+
+static void move_item(struct editable *e, struct list_head *head, struct list_head *item)
+{
+       struct simple_track *t = to_simple_track(item);
+       struct iter iter;
+
+       editable_track_to_iter(e, t, &iter);
+       if (editable_owns_shared(e))
+               window_row_vanishes(e->shared->win, &iter);
+
+       list_del(item);
+       list_add(item, head);
+}
+
+static void reset_tree(struct editable *e)
+{
+       struct simple_track *old, *first_track;
+
+       old = tree_node_to_simple_track(rb_first(&e->tree_root));
+       first_track = to_simple_track(e->head.next);
+       if (old != first_track) {
+               rb_replace_node(&old->tree_node, &first_track->tree_node, &e->tree_root);
+               RB_CLEAR_NODE(&old->tree_node);
+       }
+}
+
+static void move_sel(struct editable *e, struct list_head *after)
+{
+       struct simple_track *t;
+       struct list_head *item, *next;
+       struct iter iter;
+       LIST_HEAD(tmp_head);
+
+       if (e->nr_marked) {
+               /* collect marked */
+               item = e->head.next;
+               while (item != &e->head) {
+                       t = to_simple_track(item);
+                       next = item->next;
+                       if (t->marked)
+                               move_item(e, &tmp_head, item);
+                       item = next;
+               }
+       } else {
+               /* collect the selected track */
+               t = get_selected(e);
+               if (t)
+                       move_item(e, &tmp_head, &t->node);
+       }
+
+       /* put them back to the list after @after */
+       item = tmp_head.next;
+       while (item != &tmp_head) {
+               next = item->next;
+               list_add(item, after);
+               item = next;
+       }
+       reset_tree(e);
+
+       /* select top-most of the moved tracks */
+       editable_track_to_iter(e, to_simple_track(after->next), &iter);
+
+       if (editable_owns_shared(e)) {
+               window_set_sel(e->shared->win, &iter);
+               window_changed(e->shared->win);
+       }
+}
+
+static struct list_head *find_insert_after_point(struct editable *e, struct list_head *item)
+{
+       if (e->nr_marked == 0) {
+               /* move the selected track down one row */
+               return item->next;
+       }
+
+       /* move marked after the selected
+        *
+        * if the selected track itself is marked we find the first unmarked
+        * track (or head) before the selected one
+        */
+       while (item != &e->head) {
+               struct simple_track *t = to_simple_track(item);
+
+               if (!t->marked)
+                       break;
+               item = item->prev;
+       }
+       return item;
+}
+
+static struct list_head *find_insert_before_point(struct editable *e, struct list_head *item)
+{
+       item = item->prev;
+       if (e->nr_marked == 0) {
+               /* move the selected track up one row */
+               return item->prev;
+       }
+
+       /* move marked before the selected
+        *
+        * if the selected track itself is marked we find the first unmarked
+        * track (or head) before the selected one
+        */
+       while (item != &e->head) {
+               struct simple_track *t = to_simple_track(item);
+
+               if (!t->marked)
+                       break;
+               item = item->prev;
+       }
+       return item;
+}
+
+void editable_move_after(struct editable *e)
+{
+       struct simple_track *sel;
+
+       if (e->nr_tracks <= 1 || e->shared->sort_keys[0] != SORT_INVALID)
+               return;
+
+       sel = get_selected(e);
+       if (sel)
+               move_sel(e, find_insert_after_point(e, &sel->node));
+}
+
+void editable_move_before(struct editable *e)
+{
+       struct simple_track *sel;
+
+       if (e->nr_tracks <= 1 || e->shared->sort_keys[0] != SORT_INVALID)
+               return;
+
+       sel = get_selected(e);
+       if (sel)
+               move_sel(e, find_insert_before_point(e, &sel->node));
+}
+
+void editable_clear(struct editable *e)
+{
+       struct list_head *item, *tmp;
+
+       list_for_each_safe(item, tmp, &e->head)
+               editable_remove_track(e, to_simple_track(item));
+}
+
+void editable_remove_matching_tracks(struct editable *e,
+               int (*cb)(void *data, struct track_info *ti), void *data)
+{
+       struct list_head *item, *tmp;
+
+       list_for_each_safe(item, tmp, &e->head) {
+               struct simple_track *t = to_simple_track(item);
+               if (cb(data, t->info))
+                       editable_remove_track(e, t);
+       }
+}
+
+void editable_mark(struct editable *e, const char *filter)
+{
+       struct expr *expr = NULL;
+       struct simple_track *t;
+
+       if (filter) {
+               expr = parse_filter(filter);
+               if (expr == NULL)
+                       return;
+       }
+
+       list_for_each_entry(t, &e->head, node) {
+               e->nr_marked -= t->marked;
+               t->marked = 0;
+               if (expr == NULL || expr_eval(expr, t->info)) {
+                       t->marked = 1;
+                       e->nr_marked++;
+               }
+       }
+
+       if (editable_owns_shared(e))
+               e->shared->win->changed = 1;
+}
+
+void editable_unmark(struct editable *e)
+{
+       struct simple_track *t;
+
+       list_for_each_entry(t, &e->head, node) {
+               e->nr_marked -= t->marked;
+               t->marked = 0;
+       }
+
+       if (editable_owns_shared(e))
+               e->shared->win->changed = 1;
+}
+
+void editable_invert_marks(struct editable *e)
+{
+       struct simple_track *t;
+
+       list_for_each_entry(t, &e->head, node) {
+               e->nr_marked -= t->marked;
+               t->marked ^= 1;
+               e->nr_marked += t->marked;
+       }
+
+       if (editable_owns_shared(e))
+               e->shared->win->changed = 1;
+}
+
+int _editable_for_each_sel(struct editable *e, track_info_cb cb, void *data,
+               int reverse)
+{
+       int rc = 0;
+
+       if (e->nr_marked) {
+               /* treat marked tracks as selected */
+               rc = simple_list_for_each_marked(&e->head, cb, data, reverse);
+       } else {
+               struct simple_track *t = get_selected(e);
+
+               if (t)
+                       rc = cb(data, t->info);
+       }
+       return rc;
+}
+
+int editable_for_each_sel(struct editable *e, track_info_cb cb, void *data,
+               int reverse)
+{
+       int rc;
+
+       rc = _editable_for_each_sel(e, cb, data, reverse);
+       if (e->nr_marked == 0 && editable_owns_shared(e))
+               window_down(e->shared->win, 1);
+       return rc;
+}
+
+int editable_for_each(struct editable *e, track_info_cb cb, void *data,
+               int reverse)
+{
+       return simple_list_for_each(&e->head, cb, data, reverse);
+}
+
+void editable_update_track(struct editable *e, struct track_info *old, struct track_info *new)
+{
+       struct list_head *item, *tmp;
+       int changed = 0;
+
+       list_for_each_safe(item, tmp, &e->head) {
+               struct simple_track *track = to_simple_track(item);
+               if (track->info == old) {
+                       if (new) {
+                               track_info_unref(old);
+                               track_info_ref(new);
+                               track->info = new;
+                       } else {
+                               editable_remove_track(e, track);
+                       }
+                       changed = 1;
+               }
+       }
+       if (editable_owns_shared(e))
+               e->shared->win->changed |= changed;
+}
+
+void editable_rand(struct editable *e)
+{
+       if (e->nr_tracks <=1)
+               return;
+       rand_list_rebuild(&e->head, &e->tree_root);
+
+       if (editable_owns_shared(e)) {
+               window_changed(e->shared->win);
+               window_goto_top(e->shared->win);
+       }
+}
+
+int editable_empty(struct editable *e)
+{
+       return list_empty(&e->head);
+}
diff --git a/editable.h b/editable.h
new file mode 100644 (file)
index 0000000..ab515c6
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_EDITABLE_H
+#define CMUS_EDITABLE_H
+
+#include "window.h"
+#include "list.h"
+#include "rbtree.h"
+#include "track.h"
+#include "locking.h"
+#include "cmus.h"
+
+struct editable;
+
+typedef void (*editable_free_track)(struct editable *e, struct list_head *head);
+
+struct editable_shared {
+       struct editable *owner;
+
+       struct window *win;
+       sort_key_t *sort_keys;
+       char sort_str[128];
+       editable_free_track free_track;
+       struct searchable *searchable;
+};
+
+struct editable {
+       struct list_head head;
+       struct rb_root tree_root;
+       unsigned int nr_tracks;
+       unsigned int nr_marked;
+       unsigned int total_time;
+       struct editable_shared *shared;
+};
+
+void editable_shared_init(struct editable_shared *shared,
+               editable_free_track free_track);
+void editable_shared_set_sort_keys(struct editable_shared *shared,
+               sort_key_t *keys);
+
+void editable_init(struct editable *e, struct editable_shared *shared,
+               int take_ownership);
+void editable_take_ownership(struct editable *e);
+void editable_add(struct editable *e, struct simple_track *track);
+void editable_add_before(struct editable *e, struct simple_track *track);
+void editable_remove_track(struct editable *e, struct simple_track *track);
+void editable_remove_sel(struct editable *e);
+void editable_sort(struct editable *e);
+void editable_rand(struct editable *e);
+void editable_toggle_mark(struct editable *e);
+void editable_move_after(struct editable *e);
+void editable_move_before(struct editable *e);
+void editable_clear(struct editable *e);
+void editable_remove_matching_tracks(struct editable *e,
+               int (*cb)(void *data, struct track_info *ti), void *data);
+void editable_mark(struct editable *e, const char *filter);
+void editable_unmark(struct editable *e);
+void editable_invert_marks(struct editable *e);
+int _editable_for_each_sel(struct editable *e, track_info_cb cb, void *data,
+               int reverse);
+int editable_for_each_sel(struct editable *e, track_info_cb cb, void *data,
+               int reverse);
+int editable_for_each(struct editable *e, track_info_cb cb, void *data,
+               int reverse);
+void editable_update_track(struct editable *e, struct track_info *old, struct track_info *new);
+int editable_empty(struct editable *e);
+
+static inline void editable_track_to_iter(struct editable *e, struct simple_track *track, struct iter *iter)
+{
+       iter->data0 = &e->head;
+       iter->data1 = track;
+       iter->data2 = NULL;
+}
+
+#endif
diff --git a/expr.c b/expr.c
new file mode 100644 (file)
index 0000000..b8f7513
--- /dev/null
+++ b/expr.c
@@ -0,0 +1,1054 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "expr.h"
+#include "glob.h"
+#include "uchar.h"
+#include "track_info.h"
+#include "comment.h"
+#include "xmalloc.h"
+#include "utils.h"
+#include "debug.h"
+#include "list.h"
+#include "ui_curses.h" /* using_utf8, charset */
+#include "convert.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <stdarg.h>
+#include <limits.h>
+
+enum token_type {
+       /* speacial chars */
+       TOK_NOT,
+       TOK_LT,
+       TOK_GT,
+
+#define NR_COMBINATIONS TOK_EQ
+
+       /* speacial chars */
+       TOK_EQ,
+       TOK_AND,
+       TOK_OR,
+       TOK_LPAREN,
+       TOK_RPAREN,
+
+#define NR_SPECIALS TOK_NE
+#define COMB_BASE TOK_NE
+
+       /* same as the first 3 + '=' */
+       TOK_NE,
+       TOK_LE,
+       TOK_GE,
+
+       TOK_KEY,
+       TOK_INT_OR_KEY,
+       TOK_STR
+};
+#define NR_TOKS (TOK_STR + 1)
+
+struct token {
+       struct list_head node;
+       enum token_type type;
+       /* for TOK_KEY, TOK_INT_OR_KEY and TOK_STR */
+       char str[];
+};
+
+/* same order as TOK_* */
+static const char specials[NR_SPECIALS] = "!<>=&|()";
+
+static const int tok_to_op[NR_TOKS] = {
+       -1, OP_LT, OP_GT, OP_EQ, -1, -1, -1, -1, OP_NE, OP_LE, OP_GE, -1, -1, -1
+};
+
+static const char * const op_names[NR_OPS] = { "<", "<=", "=", ">=", ">", "!=" };
+static const char * const expr_names[NR_EXPRS] = {
+       "&", "|", "!", "a string", "an integer", "a boolean"
+};
+
+static char error_buf[64] = { 0, };
+
+
+static void set_error(const char *format, ...)
+{
+       va_list ap;
+
+       va_start(ap, format);
+       vsnprintf(error_buf, sizeof(error_buf), format, ap);
+       va_end(ap);
+}
+
+static struct token *get_str(const char *str, int *idxp)
+{
+       struct token *tok;
+       int s = *idxp + 1;
+       int e = s;
+
+       /* can't remove all backslashes here => don't remove any */
+       while (str[e] != '"') {
+               int c = str[e];
+
+               if (c == 0)
+                       goto err;
+               if (c == '\\') {
+                       if (str[e + 1] == 0)
+                               goto err;
+                       e += 2;
+                       continue;
+               }
+               e++;
+       }
+
+       tok = xmalloc(sizeof(struct token) + e - s + 1);
+       memcpy(tok->str, str + s, e - s);
+       tok->str[e - s] = 0;
+       tok->type = TOK_STR;
+       *idxp = e + 1;
+       return tok;
+err:
+       set_error("end of expression at middle of string");
+       return NULL;
+}
+
+static struct token *get_int_or_key(const char *str, int *idxp)
+{
+       int s = *idxp;
+       int e = s;
+       int digits_only = 1;
+       struct token *tok;
+
+       if (str[e] == '-')
+               e++;
+       while (str[e]) {
+               int i, c = str[e];
+
+               if (isspace(c))
+                       goto out;
+               for (i = 0; i < NR_SPECIALS; i++) {
+                       if (c == specials[i])
+                               goto out;
+               }
+               if (c < '0' || c > '9') {
+                       digits_only = 0;
+                       if (!isalpha(c) && c != '_' && c != '-') {
+                               set_error("unexpected '%c'", c);
+                               return NULL;
+                       }
+               }
+               e++;
+       }
+out:
+       tok = xmalloc(sizeof(struct token) + e - s + 1);
+       memcpy(tok->str, str + s, e - s);
+       tok->str[e - s] = 0;
+       tok->type = TOK_KEY;
+       if (digits_only)
+               tok->type = TOK_INT_OR_KEY;
+       *idxp = e;
+       return tok;
+}
+
+static struct token *get_token(const char *str, int *idxp)
+{
+       int idx = *idxp;
+       int c, i;
+
+       c = str[idx];
+       for (i = 0; i < NR_SPECIALS; i++) {
+               struct token *tok;
+
+               if (c != specials[i])
+                       continue;
+
+               idx++;
+               tok = xnew(struct token, 1);
+               tok->type = i;
+               if (i < NR_COMBINATIONS && str[idx] == '=') {
+                       tok->type = COMB_BASE + i;
+                       idx++;
+               }
+               *idxp = idx;
+               return tok;
+       }
+       if (c == '"')
+               return get_str(str, idxp);
+       return get_int_or_key(str, idxp);
+}
+
+static void free_tokens(struct list_head *head)
+{
+       struct list_head *item = head->next;
+
+       while (item != head) {
+               struct list_head *next = item->next;
+               struct token *tok = container_of(item, struct token, node);
+
+               free(tok);
+               item = next;
+       }
+}
+
+static int tokenize(struct list_head *head, const char *str)
+{
+       struct token *tok;
+       int idx = 0;
+
+       while (1) {
+               while (isspace(str[idx]))
+                       ++idx;
+               if (str[idx] == 0)
+                       break;
+               tok = get_token(str, &idx);
+               if (tok == NULL) {
+                       free_tokens(head);
+                       return -1;
+               }
+               list_add_tail(&tok->node, head);
+       }
+       return 0;
+}
+
+static struct expr *expr_new(int type)
+{
+       struct expr *new = xnew0(struct expr, 1);
+
+       new->type = type;
+
+       return new;
+}
+
+static int parse(struct expr **rootp, struct list_head *head, struct list_head **itemp, int level);
+
+static int parse_one(struct expr **exprp, struct list_head *head, struct list_head **itemp)
+{
+       struct list_head *item = *itemp;
+       struct token *tok;
+       enum token_type type;
+       int rc;
+
+       *exprp = NULL;
+       if (item == head) {
+               set_error("expression expected");
+               return -1;
+       }
+
+       tok = container_of(item, struct token, node);
+       type = tok->type;
+       if (type == TOK_NOT) {
+               struct expr *new, *tmp;
+
+               *itemp = item->next;
+               rc = parse_one(&tmp, head, itemp);
+               if (rc)
+                       return rc;
+               new = expr_new(EXPR_NOT);
+               new->left = tmp;
+               *exprp = new;
+               return 0;
+       } else if (type == TOK_LPAREN) {
+               *itemp = item->next;
+               *exprp = NULL;
+               return parse(exprp, head, itemp, 1);
+               /* ')' already eaten */
+       } else if (type == TOK_KEY || type == TOK_INT_OR_KEY) {
+               const char *key = tok->str;
+               struct expr *new;
+               int op = -1;
+
+               item = item->next;
+               if (item != head) {
+                       tok = container_of(item, struct token, node);
+                       op = tok_to_op[tok->type];
+               }
+               if (item == head || op == -1) {
+                       /* must be a bool */
+                       new = expr_new(EXPR_BOOL);
+                       new->key = xstrdup(key);
+                       *itemp = item;
+                       *exprp = new;
+                       return 0;
+               }
+               item = item->next;
+               if (item == head) {
+                       set_error("right side of expression expected");
+                       return -1;
+               }
+               tok = container_of(item, struct token, node);
+               type = tok->type;
+               *itemp = item->next;
+               if (type == TOK_STR) {
+                       if (op != OP_EQ && op != OP_NE) {
+                               set_error("invalid string operator '%s'", op_names[op]);
+                               return -1;
+                       }
+                       new = expr_new(EXPR_STR);
+                       new->key = xstrdup(key);
+                       glob_compile(&new->estr.glob_head, tok->str);
+                       new->estr.op = op;
+                       *exprp = new;
+                       return 0;
+               } else if (type == TOK_INT_OR_KEY) {
+                       long int val = 0;
+
+                       if (str_to_int(tok->str, &val)) {
+                       }
+                       new = expr_new(EXPR_INT);
+                       new->key = xstrdup(key);
+                       new->eint.val = val;
+                       new->eint.op = op;
+                       *exprp = new;
+                       return 0;
+               } else if (type == TOK_KEY) {
+                       new = expr_new(EXPR_ID);
+                       new->key = xstrdup(key);
+                       new->eid.key = xstrdup(tok->str);
+                       new->eid.op = op;
+                       *exprp = new;
+                       return 0;
+               }
+               if (op == OP_EQ || op == OP_NE) {
+                       set_error("integer or string expected");
+               } else {
+                       set_error("integer expected");
+               }
+               return -1;
+       }
+       set_error("key expected");
+       return -1;
+}
+
+static void add(struct expr **rootp, struct expr *expr)
+{
+       struct expr *tmp, *root = *rootp;
+
+       if (root == NULL) {
+               *rootp = expr;
+               return;
+       }
+
+       tmp = root;
+       while (tmp->right)
+               tmp = tmp->right;
+       if (tmp->type <= EXPR_OR) {
+               /* tmp is binary, tree is incomplete */
+               tmp->right = expr;
+               expr->parent = tmp;
+               return;
+       }
+
+       /* tmp is unary, tree is complete
+        * expr must be a binary operator */
+       BUG_ON(expr->type > EXPR_OR);
+
+       expr->left = root;
+       root->parent = expr;
+       *rootp = expr;
+}
+
+static int parse(struct expr **rootp, struct list_head *head, struct list_head **itemp, int level)
+{
+       struct list_head *item = *itemp;
+
+       while (1) {
+               struct token *tok;
+               struct expr *expr;
+               int rc, type;
+
+               rc = parse_one(&expr, head, &item);
+               if (rc)
+                       return rc;
+               add(rootp, expr);
+               if (item == head) {
+                       if (level > 0) {
+                               set_error("')' expected");
+                               return -1;
+                       }
+                       *itemp = item;
+                       return 0;
+               }
+               tok = container_of(item, struct token, node);
+               if (tok->type == TOK_RPAREN) {
+                       if (level == 0) {
+                               set_error("unexpected ')'");
+                               return -1;
+                       }
+                       *itemp = item->next;
+                       return 0;
+               }
+
+               if (tok->type == TOK_AND) {
+                       type = EXPR_AND;
+               } else if (tok->type == TOK_OR) {
+                       type = EXPR_OR;
+               } else {
+                       set_error("'&' or '|' expected");
+                       return -1;
+               }
+               expr = expr_new(type);
+               add(rootp, expr);
+               item = item->next;
+       }
+}
+
+static const struct {
+       char short_key;
+       const char *long_key;
+} map_short2long[] = {
+       { 'A',  "albumartist"   },
+       { 'D',  "discnumber"    },
+       { 'T',  "tag",          },
+       { 'a',  "artist"        },
+       { 'c',  "comment"       },
+       { 'd',  "duration"      },
+       { 'f',  "filename"      },
+       { 'g',  "genre"         },
+       { 'l',  "album"         },
+       { 'n',  "tracknumber"   },
+       { 'X',  "play_count"    },
+       { 's',  "stream"        },
+       { 't',  "title"         },
+       { 'y',  "date"          },
+       { '\0', NULL            },
+};
+
+static const struct {
+       const char *key;
+       enum expr_type type;
+} builtin[] = {
+       { "album",      EXPR_STR        },
+       { "albumartist",EXPR_STR        },
+       { "artist",     EXPR_STR        },
+       { "bitrate",    EXPR_INT        },
+       { "bpm",        EXPR_INT        },
+       { "codec",      EXPR_STR        },
+       { "codec_profile",EXPR_STR      },
+       { "comment",    EXPR_STR        },
+       { "date",       EXPR_INT        },
+       { "discnumber", EXPR_INT        },
+       { "duration",   EXPR_INT        },
+       { "filename",   EXPR_STR        },
+       { "genre",      EXPR_STR        },
+       { "media",      EXPR_STR        },
+       { "originaldate",EXPR_INT       },
+       { "play_count", EXPR_INT        },
+       { "stream",     EXPR_BOOL       },
+       { "tag",        EXPR_BOOL       },
+       { "title",      EXPR_STR        },
+       { "tracknumber",EXPR_INT        },
+       { NULL,         -1              },
+};
+
+static const char *lookup_long_key(char c)
+{
+       int i;
+       for (i = 0; map_short2long[i].short_key; i++) {
+               if (map_short2long[i].short_key == c)
+                       return map_short2long[i].long_key;
+       }
+       return NULL;
+}
+
+static enum expr_type lookup_key_type(const char *key)
+{
+       int i;
+       for (i = 0; builtin[i].key; i++) {
+               int cmp = strcmp(key, builtin[i].key);
+               if (cmp == 0)
+                       return builtin[i].type;
+               if (cmp < 0)
+                       break;
+       }
+       return -1;
+}
+
+static unsigned long stack4_new(void)
+{
+       return 0;
+}
+static void stack4_push(unsigned long *s, unsigned long e)
+{
+       *s = (*s << 4) | e;
+}
+static void stack4_pop(unsigned long *s)
+{
+       *s = *s >> 4;
+}
+static unsigned long stack4_top(unsigned long s)
+{
+       return s & 0xf;
+}
+static void stack4_replace_top(unsigned long *s, unsigned long e)
+{
+       *s = (*s & ~0xf) | e;
+}
+
+static char *expand_short_expr(const char *expr_short)
+{
+       /* state space, can contain maximal 15 states */
+       enum state_type {
+               ST_SKIP_SPACE = 1,
+               ST_TOP,
+               ST_EXPECT_KEY,
+               ST_EXPECT_OP,
+               ST_EXPECT_INT,
+               ST_IN_INT,
+               ST_MEM_INT,
+               ST_IN_2ND_INT,
+               ST_EXPECT_STR,
+               ST_IN_QUOTE_STR,
+               ST_IN_STR,
+       };
+
+       size_t len_expr_short = strlen(expr_short);
+       /* worst case blowup of expr_short is 31/5 (e.g. ~n1-2), so take x7:
+        * strlen("~n1-2") == 5
+        * strlen("(tracknumber>=1&tracknumber<=2)") == 31
+        */
+       char *out = xnew(char, len_expr_short * 7);
+       char *num = NULL;
+       size_t i, i_num = 0, k = 0;
+       const char *key = NULL;
+       int level = 0;
+       enum expr_type etype;
+       /* used as state-stack, can contain at least 32/4 = 8 states */
+       unsigned long state_stack = stack4_new();
+       stack4_push(&state_stack, ST_TOP);
+       stack4_push(&state_stack, ST_SKIP_SPACE);
+
+       /* include terminal '\0' to recognize end of string */
+       for (i = 0; i <= len_expr_short; i++) {
+               unsigned char c = expr_short[i];
+               switch (stack4_top(state_stack)) {
+               case ST_SKIP_SPACE:
+                       if (c != ' ') {
+                               stack4_pop(&state_stack);
+                               i--;
+                       }
+                       break;
+               case ST_TOP:
+                       switch (c) {
+                       case '~':
+                               stack4_push(&state_stack, ST_EXPECT_OP);
+                               stack4_push(&state_stack, ST_SKIP_SPACE);
+                               stack4_push(&state_stack, ST_EXPECT_KEY);
+                               break;
+                       case '(':
+                               level++;
+                       /* Fall through */
+                       case '!':
+                       case '|':
+                               out[k++] = c;
+                               stack4_push(&state_stack, ST_SKIP_SPACE);
+                               break;
+                       case ')':
+                               level--;
+                               out[k++] = c;
+                               stack4_push(&state_stack, ST_EXPECT_OP);
+                               stack4_push(&state_stack, ST_SKIP_SPACE);
+                               break;
+                       case '\0':
+                               if (level > 0) {
+                                       set_error("')' expected");
+                                       goto error_exit;
+                               }
+                               out[k++] = c;
+                               break;
+                       default:
+                               set_error("unexpected '%c'", c);
+                               goto error_exit;
+                       }
+                       break;
+               case ST_EXPECT_KEY:
+                       stack4_pop(&state_stack);
+                       key = lookup_long_key(c);
+                       if (!key) {
+                               set_error("unknown short key %c", c);
+                               goto error_exit;
+                       }
+                       etype = lookup_key_type(key);
+                       if (etype == EXPR_INT) {
+                               stack4_push(&state_stack, ST_EXPECT_INT);
+                               out[k++] = '(';
+                       } else if (etype == EXPR_STR) {
+                               stack4_push(&state_stack, ST_EXPECT_STR);
+                       } else if (etype != EXPR_BOOL) {
+                               BUG("wrong etype: %d\n", etype);
+                       }
+                       strcpy(out+k, key);
+                       k += strlen(key);
+                       stack4_push(&state_stack, ST_SKIP_SPACE);
+                       break;
+               case ST_EXPECT_OP:
+                       if (c == '~' || c == '(' || c == '!')
+                               out[k++] = '&';
+                       i--;
+                       stack4_replace_top(&state_stack, ST_SKIP_SPACE);
+                       break;
+               case ST_EXPECT_INT:
+                       if (c == '<' || c == '>') {
+                               out[k++] = c;
+                               stack4_replace_top(&state_stack, ST_IN_INT);
+                       } else if (c == '-') {
+                               out[k++] = '<';
+                               out[k++] = '=';
+                               stack4_replace_top(&state_stack, ST_IN_INT);
+                       } else if (isdigit(c)) {
+                               if (!num)
+                                       num = xnew(char, len_expr_short);
+                               num[i_num++] = c;
+                               stack4_replace_top(&state_stack, ST_MEM_INT);
+                       } else {
+                               set_error("integer expected", expr_short);
+                               goto error_exit;
+                       }
+                       break;
+               case ST_IN_INT:
+                       if (isdigit(c)) {
+                               out[k++] = c;
+                       } else {
+                               i -= 1;
+                               stack4_pop(&state_stack);
+                               out[k++] = ')';
+                       }
+                       break;
+               case ST_MEM_INT:
+                       if (isdigit(c)) {
+                               num[i_num++] = c;
+                       } else {
+                               if (c == '-') {
+                                       out[k++] = '>';
+                                       out[k++] = '=';
+                                       stack4_replace_top(&state_stack, ST_IN_2ND_INT);
+                               } else {
+                                       out[k++] = '=';
+                                       i--;
+                                       stack4_pop(&state_stack);
+                               }
+                               strncpy(out+k, num, i_num);
+                               k += i_num;
+                               i_num = 0;
+                               if (c != '-')
+                                       out[k++] = ')';
+                       }
+                       break;
+               case ST_IN_2ND_INT:
+                       if (isdigit(c)) {
+                               num[i_num++] = c;
+                       } else {
+                               i--;
+                               stack4_pop(&state_stack);
+                               if (i_num > 0) {
+                                       out[k++] = '&';
+                                       strcpy(out+k, key);
+                                       k += strlen(key);
+                                       out[k++] = '<';
+                                       out[k++] = '=';
+                                       strncpy(out+k, num, i_num);
+                                       k += i_num;
+                               }
+                               out[k++] = ')';
+                       }
+                       break;
+               case ST_EXPECT_STR:
+                       out[k++] = '=';
+                       if (c == '"') {
+                               stack4_replace_top(&state_stack, ST_IN_QUOTE_STR);
+                               out[k++] = c;
+                       } else {
+                               stack4_replace_top(&state_stack, ST_IN_STR);
+                               out[k++] = '"';
+                               out[k++] = '*';
+                               out[k++] = c;
+                       }
+                       break;
+               case ST_IN_QUOTE_STR:
+                       if (c == '"' && expr_short[i-1] != '\\') {
+                               stack4_pop(&state_stack);
+                       }
+                       out[k++] = c;
+                       break;
+               case ST_IN_STR:
+                       /* isalnum() doesn't work for multi-byte characters */
+                       if (c != '~' && c != '!' && c != '|' &&
+                                       c != '(' && c != ')' && c != '\0') {
+                               out[k++] = c;
+                       } else {
+                               while (k > 0 && out[k-1] == ' ')
+                                       k--;
+                               out[k++] = '*';
+                               out[k++] = '"';
+                               i--;
+                               stack4_pop(&state_stack);
+                       }
+                       break;
+               default:
+                       BUG("state %ld not covered", stack4_top(state_stack));
+                       break;
+               }
+       }
+
+       if (num)
+               free(num);
+
+       d_print("expanded \"%s\" to \"%s\"\n", expr_short, out);
+
+       return out;
+
+error_exit:
+       if (num)
+               free(num);
+       free(out);
+       return NULL;
+}
+
+int expr_is_short(const char *str)
+{
+       int i;
+       for (i = 0; str[i]; i++) {
+               if (str[i] == '~')
+                       return 1;
+               if (str[i] != '!' && str[i] != '(' && str[i] != ' ')
+                       return 0;
+       }
+       return 0;
+}
+
+struct expr *expr_parse(const char *str)
+{
+       return expr_parse_i(str, "filter contains control characters", 1);
+}
+
+struct expr *expr_parse_i(const char *str, const char *err_msg, int check_short)
+{
+       LIST_HEAD(head);
+       struct expr *root = NULL;
+       struct list_head *item;
+       char *long_str = NULL, *u_str = NULL;
+       int i;
+
+       for (i = 0; str[i]; i++) {
+               unsigned char c = str[i];
+               if (c < 0x20) {
+                       set_error(err_msg);
+                       goto out;
+               }
+       }
+       if (!using_utf8 && utf8_encode(str, charset, &u_str) == 0) {
+               str = u_str;
+       }
+       if (!u_is_valid(str)) {
+               set_error("invalid UTF-8");
+               goto out;
+       }
+
+       if (check_short && expr_is_short(str)) {
+               str = long_str = expand_short_expr(str);
+               if (!str)
+                       goto out;
+       }
+
+       if (tokenize(&head, str))
+               goto out;
+
+       item = head.next;
+       if (parse(&root, &head, &item, 0))
+               root = NULL;
+       free_tokens(&head);
+
+out:
+       free(u_str);
+       free(long_str);
+       return root;
+}
+
+int expr_check_leaves(struct expr **exprp, const char *(*get_filter)(const char *name))
+{
+       struct expr *expr = *exprp;
+       struct expr *e;
+       const char *filter;
+       int i, rc;
+
+       if (expr->left) {
+               if (expr_check_leaves(&expr->left, get_filter))
+                       return -1;
+               if (expr->right)
+                       return expr_check_leaves(&expr->right, get_filter);
+               return 0;
+       }
+
+       for (i = 0; builtin[i].key; i++) {
+               int cmp = strcmp(expr->key, builtin[i].key);
+
+               if (cmp > 0)
+                       continue;
+               if (cmp < 0)
+                       break;
+
+               if (builtin[i].type != expr->type) {
+                       /* type mismatch */
+                       set_error("%s is %s", builtin[i].key, expr_names[builtin[i].type]);
+                       return -1;
+               }
+               return 0;
+       }
+
+       if (expr->type != EXPR_BOOL) {
+               /* unknown key */
+               set_error("unknown key %s", expr->key);
+               return -1;
+       }
+
+       /* user defined filter */
+       filter = get_filter(expr->key);
+       if (filter == NULL) {
+               set_error("unknown filter or boolean %s", expr->key);
+               return -1;
+       }
+       e = expr_parse(filter);
+       if (e == NULL) {
+               return -1;
+       }
+       rc = expr_check_leaves(&e, get_filter);
+       if (rc) {
+               expr_free(e);
+               return rc;
+       }
+
+       /* replace */
+       e->parent = expr->parent;
+       expr_free(expr);
+
+       /* this sets parents left pointer */
+       *exprp = e;
+       return 0;
+}
+
+unsigned int expr_get_match_type(struct expr *expr)
+{
+       const char *key;
+
+       if (expr->left) {
+               unsigned int left = expr_get_match_type(expr->left);
+               if (expr->type == EXPR_AND || expr->type == EXPR_OR)
+                       return left | expr_get_match_type(expr->right);
+               return left;
+       }
+
+       key = expr->key;
+       if (strcmp(key, "artist") == 0 || strcmp(key, "albumartist") == 0)
+               return TI_MATCH_ARTIST;
+       if (strcmp(key, "album") == 0 || strcmp(key, "discnumber") == 0)
+               return TI_MATCH_ALBUM;
+       if (strcmp(key, "title") == 0 || strcmp(key, "tracknumber") == 0)
+               return TI_MATCH_TITLE;
+
+       return 0;
+}
+
+int expr_is_harmless(const struct expr *expr)
+{
+       switch (expr->type) {
+       case EXPR_OR:
+       case EXPR_NOT:
+               return 0;
+       case EXPR_AND:
+               expr = expr->right;
+       default:
+               break;
+       }
+       if (expr->type == EXPR_INT) {
+               switch (expr->eint.op) {
+               case IOP_LT:
+               case IOP_EQ:
+               case IOP_LE:
+                       return 0;
+               default:
+                       return 1;
+               }
+       }
+       if (expr->type == EXPR_ID)
+               return 0;
+       return 1;
+}
+
+static const char *str_val(const char *key, struct track_info *ti, char **need_free)
+{
+       const char *val;
+       *need_free = NULL;
+       if (strcmp(key, "filename") == 0) {
+               val = ti->filename;
+               if (!using_utf8 && utf8_encode(val, charset, need_free) == 0) {
+                       val = *need_free;
+               }
+       } else if (strcmp(key, "codec") == 0) {
+               val = ti->codec;
+       } else if (strcmp(key, "codec_profile") == 0) {
+               val = ti->codec_profile;
+       } else {
+               val = keyvals_get_val(ti->comments, key);
+       }
+       return val;
+}
+
+static int int_val(const char *key, struct track_info *ti)
+{
+       int val;
+       if (strcmp(key, "duration") == 0) {
+               val = ti->duration;
+               /* duration of a stream is infinite (well, almost) */
+               if (is_http_url(ti->filename))
+                       val = INT_MAX;
+       } else if (strcmp(key, "date") == 0) {
+               val = (ti->date >= 0) ? (ti->date / 10000) : -1;
+       } else if (strcmp(key, "originaldate") == 0) {
+               val = (ti->originaldate >= 0) ? (ti->originaldate / 10000) : -1;
+       } else if (strcmp(key, "bitrate") == 0) {
+               val = (ti->bitrate >= 0) ? (int) (ti->bitrate / 1000. + 0.5) : -1;
+       } else if (strcmp(key, "play_count") == 0) {
+               val = ti->play_count;
+       } else if (strcmp(key, "bpm") == 0) {
+               val = ti->bpm;
+       } else {
+               val = comments_get_int(ti->comments, key);
+       }
+       return val;
+}
+
+int expr_op_to_bool(int res, int op)
+{
+       switch (op) {
+       case OP_LT:
+               return res < 0;
+       case OP_LE:
+               return res <= 0;
+       case OP_EQ:
+               return res == 0;
+       case OP_GE:
+               return res >= 0;
+       case OP_GT:
+               return res > 0;
+       case OP_NE:
+               return res != 0;
+       default:
+               return 0;
+       }
+}
+
+int expr_eval(struct expr *expr, struct track_info *ti)
+{
+       enum expr_type type = expr->type;
+       const char *key;
+
+       if (expr->left) {
+               int left = expr_eval(expr->left, ti);
+
+               if (type == EXPR_AND)
+                       return left && expr_eval(expr->right, ti);
+               if (type == EXPR_OR)
+                       return left || expr_eval(expr->right, ti);
+               /* EXPR_NOT */
+               return !left;
+       }
+
+       key = expr->key;
+       if (type == EXPR_STR) {
+               int res;
+               char *need_free;
+               const char *val = str_val(key, ti, &need_free);
+               if (!val)
+                       val = "";
+               res = glob_match(&expr->estr.glob_head, val);
+               free(need_free);
+               if (expr->estr.op == SOP_EQ)
+                       return res;
+               return !res;
+       } else if (type == EXPR_INT) {
+               int val = int_val(key, ti);
+               int res;
+               if (expr->eint.val == -1) {
+                       /* -1 is "not set"
+                        * doesn't make sense to do 123 < "not set"
+                        * but it makes sense to do date=-1 (date is not set)
+                        */
+                       if (expr->eint.op == IOP_EQ)
+                               return val == -1;
+                       if (expr->eint.op == IOP_NE)
+                               return val != -1;
+               }
+               if (val == -1) {
+                       /* tag not set, can't compare */
+                       return 0;
+               }
+               res = val - expr->eint.val;
+               return expr_op_to_bool(res, expr->eint.op);
+       } else if (type == EXPR_ID) {
+               int a = 0, b = 0;
+               const char *sa, *sb;
+               char *fa, *fb;
+               int res = 0;
+               if ((sa = str_val(key, ti, &fa))) {
+                       if ((sb = str_val(expr->eid.key, ti, &fb))) {
+                               res = strcmp(sa, sb);
+                               free(fa);
+                               free(fb);
+                               return expr_op_to_bool(res, expr->eid.op);
+                       }
+                       free(fa);
+               } else {
+                       a = int_val(key, ti);
+                       b = int_val(expr->eid.key, ti);
+                       res = a - b;
+                       if (a == -1 || b == -1) {
+                               switch (expr->eid.op) {
+                               case KOP_EQ:
+                                       return res == 0;
+                               case KOP_NE:
+                                       return res != 0;
+                               default:
+                                       return 0;
+                               }
+                       }
+                       return expr_op_to_bool(res, expr->eid.op);
+               }
+               return res;
+       }
+       if (strcmp(key, "stream") == 0)
+               return is_http_url(ti->filename);
+       return track_info_has_tag(ti);
+}
+
+void expr_free(struct expr *expr)
+{
+       if (expr->left) {
+               expr_free(expr->left);
+               if (expr->right)
+                       expr_free(expr->right);
+       }
+       free(expr->key);
+       if (expr->type == EXPR_STR)
+               glob_free(&expr->estr.glob_head);
+       else if (expr->type == EXPR_ID)
+               free(expr->eid.key);
+       free(expr);
+}
+
+const char *expr_error(void)
+{
+       return error_buf;
+}
diff --git a/expr.h b/expr.h
new file mode 100644 (file)
index 0000000..674201b
--- /dev/null
+++ b/expr.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_EXPR_H
+#define CMUS_EXPR_H
+
+#include "track_info.h"
+#include "list.h"
+
+enum { OP_LT, OP_LE, OP_EQ, OP_GE, OP_GT, OP_NE };
+#define NR_OPS (OP_NE + 1)
+
+enum expr_type {
+       EXPR_AND,
+       EXPR_OR,
+
+       EXPR_NOT,
+
+       EXPR_STR,
+       EXPR_INT,
+       EXPR_ID,
+       EXPR_BOOL
+};
+#define NR_EXPRS (EXPR_BOOL + 1)
+
+struct expr {
+       struct expr *left, *right, *parent;
+       enum expr_type type;
+       char *key;
+       union {
+               struct {
+                       struct list_head glob_head;
+                       enum {
+                               SOP_EQ = OP_EQ,
+                               SOP_NE = OP_NE
+                       } op;
+               } estr;
+               struct {
+                       int val;
+                       enum {
+                               IOP_LT = OP_LT,
+                               IOP_LE = OP_LE,
+                               IOP_EQ = OP_EQ,
+                               IOP_GE = OP_GE,
+                               IOP_GT = OP_GT,
+                               IOP_NE = OP_NE
+                       } op;
+               } eint;
+               struct {
+                       char* key;
+                       enum {
+                               KOP_LT = OP_LT,
+                               KOP_LE = OP_LE,
+                               KOP_EQ = OP_EQ,
+                               KOP_GE = OP_GE,
+                               KOP_GT = OP_GT,
+                               KOP_NE = OP_NE
+                       } op;
+               } eid;
+       };
+};
+
+struct expr *expr_parse(const char *str);
+struct expr* expr_parse_i(const char *str, const char *err_msg, int check_short);
+int expr_check_leaves(struct expr **exprp, const char *(*get_filter)(const char *name));
+int expr_op_to_bool(int res, int op);
+int expr_eval(struct expr *expr, struct track_info *ti);
+void expr_free(struct expr *expr);
+const char *expr_error(void);
+int expr_is_short(const char *str);
+
+unsigned int expr_get_match_type(struct expr *expr);
+/* "harmless" expressions will reduce filter results when adding characters at the beginning/end */
+int expr_is_harmless(const struct expr *expr);
+
+#endif
diff --git a/file.c b/file.c
new file mode 100644 (file)
index 0000000..3886ef2
--- /dev/null
+++ b/file.c
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "file.h"
+#include "xmalloc.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+
+ssize_t read_all(int fd, void *buf, size_t count)
+{
+       char *buffer = buf;
+       ssize_t pos = 0;
+
+       do {
+               ssize_t rc;
+
+               rc = read(fd, buffer + pos, count - pos);
+               if (rc == -1) {
+                       if (errno == EINTR || errno == EAGAIN)
+                               continue;
+                       return -1;
+               }
+               if (rc == 0) {
+                       /* eof */
+                       break;
+               }
+               pos += rc;
+       } while (count - pos > 0);
+       return pos;
+}
+
+ssize_t write_all(int fd, const void *buf, size_t count)
+{
+       const char *buffer = buf;
+       int count_save = count;
+
+       do {
+               int rc;
+
+               rc = write(fd, buffer, count);
+               if (rc == -1) {
+                       if (errno == EINTR || errno == EAGAIN)
+                               continue;
+                       return -1;
+               }
+               buffer += rc;
+               count -= rc;
+       } while (count > 0);
+       return count_save;
+}
+
+char *mmap_file(const char *filename, ssize_t *size)
+{
+       struct stat st;
+       char *buf;
+       int fd;
+
+       fd = open(filename, O_RDONLY);
+       if (fd == -1)
+               goto err;
+
+       if (fstat(fd, &st) == -1)
+               goto close_err;
+
+       /* can't mmap empty files */
+       buf = NULL;
+       if (st.st_size) {
+               buf = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+               if (buf == MAP_FAILED)
+                       goto close_err;
+       }
+
+       close(fd);
+       *size = st.st_size;
+       return buf;
+
+close_err:
+       close(fd);
+err:
+       *size = -1;
+       return NULL;
+}
+
+void buffer_for_each_line(const char *buf, int size,
+               int (*cb)(void *data, const char *line),
+               void *data)
+{
+       char *line = NULL;
+       int line_size = 0, pos = 0;
+
+       while (pos < size) {
+               int end, len;
+
+               end = pos;
+               while (end < size && buf[end] != '\n')
+                       end++;
+
+               len = end - pos;
+               if (end > pos && buf[end - 1] == '\r')
+                       len--;
+
+               if (len >= line_size) {
+                       line_size = len + 1;
+                       line = xrenew(char, line, line_size);
+               }
+               memcpy(line, buf + pos, len);
+               line[len] = 0;
+               pos = end + 1;
+
+               if (cb(data, line))
+                       break;
+       }
+       free(line);
+}
+
+void buffer_for_each_line_reverse(const char *buf, int size,
+               int (*cb)(void *data, const char *line),
+               void *data)
+{
+       char *line = NULL;
+       int line_size = 0, end = size - 1;
+
+       while (end >= 0) {
+               int pos, len;
+
+               if (end > 1 && buf[end] == '\n' && buf[end - 1] == '\r')
+                       end--;
+
+               pos = end;
+               while (pos > 0 && buf[pos - 1] != '\n')
+                       pos--;
+
+               len = end - pos;
+               if (len >= line_size) {
+                       line_size = len + 1;
+                       line = xrenew(char, line, line_size);
+               }
+               memcpy(line, buf + pos, len);
+               line[len] = 0;
+               end = pos - 1;
+
+               if (cb(data, line))
+                       break;
+       }
+       free(line);
+}
+
+int file_for_each_line(const char *filename,
+               int (*cb)(void *data, const char *line),
+               void *data)
+{
+       char *buf;
+       ssize_t size;
+
+       buf = mmap_file(filename, &size);
+       if (size == -1)
+               return -1;
+
+       if (buf) {
+               buffer_for_each_line(buf, size, cb, data);
+               munmap(buf, size);
+       }
+       return 0;
+}
diff --git a/file.h b/file.h
new file mode 100644 (file)
index 0000000..0408559
--- /dev/null
+++ b/file.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_FILE_H
+#define CMUS_FILE_H
+
+#include <stddef.h> /* size_t */
+#include <sys/types.h> /* ssize_t */
+
+ssize_t read_all(int fd, void *buf, size_t count);
+ssize_t write_all(int fd, const void *buf, size_t count);
+
+/* @filename  file to mmap for reading
+ * @size      returned size of the file or -1 if failed
+ *
+ * returns buffer or NULL if empty file or failed
+ */
+char *mmap_file(const char *filename, ssize_t *size);
+
+void buffer_for_each_line(const char *buf, int size,
+               int (*cb)(void *data, const char *line),
+               void *data);
+void buffer_for_each_line_reverse(const char *buf, int size,
+               int (*cb)(void *data, const char *line),
+               void *data);
+int file_for_each_line(const char *filename,
+               int (*cb)(void *data, const char *line),
+               void *data);
+
+#endif
diff --git a/filters.c b/filters.c
new file mode 100644 (file)
index 0000000..b89912c
--- /dev/null
+++ b/filters.c
@@ -0,0 +1,474 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "filters.h"
+#include "cmdline.h"
+#include "expr.h"
+#include "window.h"
+#include "search.h"
+#include "uchar.h"
+#include "lib.h"
+#include "misc.h"
+#include "file.h"
+#include "ui_curses.h"
+#include "xmalloc.h"
+
+#include <stdio.h>
+#include <ctype.h>
+
+struct window *filters_win;
+struct searchable *filters_searchable;
+LIST_HEAD(filters_head);
+
+static const char *recursive_filter;
+
+static inline void filter_entry_to_iter(struct filter_entry *e, struct iter *iter)
+{
+       iter->data0 = &filters_head;
+       iter->data1 = e;
+       iter->data2 = NULL;
+}
+
+static GENERIC_ITER_PREV(filters_get_prev, struct filter_entry, node)
+static GENERIC_ITER_NEXT(filters_get_next, struct filter_entry, node)
+
+static int filters_search_get_current(void *data, struct iter *iter)
+{
+       return window_get_sel(filters_win, iter);
+}
+
+static int filters_search_matches(void *data, struct iter *iter, const char *text)
+{
+       char **words = get_words(text);
+       int matched = 0;
+
+       if (words[0] != NULL) {
+               struct filter_entry *e;
+               int i;
+
+               e = iter_to_filter_entry(iter);
+               for (i = 0; ; i++) {
+                       if (words[i] == NULL) {
+                               window_set_sel(filters_win, iter);
+                               matched = 1;
+                               break;
+                       }
+                       if (u_strcasestr(e->name, words[i]) == NULL)
+                               break;
+               }
+       }
+       free_str_array(words);
+       return matched;
+}
+
+static const struct searchable_ops filters_search_ops = {
+       .get_prev = filters_get_prev,
+       .get_next = filters_get_next,
+       .get_current = filters_search_get_current,
+       .matches = filters_search_matches
+};
+
+static void free_filter(struct filter_entry *e)
+{
+       free(e->name);
+       free(e->filter);
+       free(e);
+}
+
+static struct filter_entry *find_filter(const char *name)
+{
+       struct filter_entry *e;
+
+       list_for_each_entry(e, &filters_head, node) {
+               if (strcmp(e->name, name) == 0)
+                       return e;
+       }
+       return NULL;
+}
+
+static const char *get_filter(const char *name)
+{
+       struct filter_entry *e = find_filter(name);
+
+       if (e) {
+               if (e->visited) {
+                       recursive_filter = e->name;
+                       return NULL;
+               }
+               e->visited = 1;
+               return e->filter;
+       }
+       return NULL;
+}
+
+static void edit_sel_filter(void)
+{
+       struct iter sel;
+       struct filter_entry *e;
+       char buf[512];
+
+       if (!window_get_sel(filters_win, &sel))
+               return;
+
+       e = iter_to_filter_entry(&sel);
+       snprintf(buf, sizeof(buf), "fset %s=%s", e->name, e->filter);
+       cmdline_set_text(buf);
+       enter_command_mode();
+}
+
+void filters_activate(int win_activate)
+{
+       struct filter_entry *f;
+       struct expr *e, *expr = NULL;
+       int unchanged = 1;
+
+       /* if no pending selection is to apply, edit currently select filter */
+       list_for_each_entry(f, &filters_head, node) {
+               if (f->act_stat != f->sel_stat)
+                       unchanged = 0;
+       }
+
+       if (unchanged) {
+               if (win_activate)
+                       edit_sel_filter();
+               else
+                       return;
+       }
+
+       /* mark visited and AND together all selected filters
+        * mark any other filters unvisited */
+       list_for_each_entry(f, &filters_head, node) {
+               f->visited = 0;
+               if (f->sel_stat == FS_IGNORE)
+                       continue;
+
+               f->visited = 1;
+               e = expr_parse(f->filter);
+               if (e == NULL) {
+                       error_msg("error parsing filter %s: %s", f->name, expr_error());
+                       if (expr)
+                               expr_free(expr);
+                       return;
+               }
+
+               if (f->sel_stat == FS_NO) {
+                       /* add ! */
+                       struct expr *not = xnew(struct expr, 1);
+
+                       not->type = EXPR_NOT;
+                       not->key = NULL;
+                       not->left = e;
+                       not->right = NULL;
+                       e = not;
+               }
+               if (expr == NULL) {
+                       expr = e;
+               } else {
+                       struct expr *and = xnew(struct expr, 1);
+
+                       and->type = EXPR_AND;
+                       and->key = NULL;
+                       and->left = expr;
+                       and->right = e;
+                       expr->parent = and;
+                       e->parent = and;
+                       expr = and;
+               }
+       }
+
+       recursive_filter = NULL;
+       if (expr && expr_check_leaves(&expr, get_filter)) {
+               if (recursive_filter) {
+                       error_msg("recursion detected in filter %s", recursive_filter);
+               } else {
+                       error_msg("error parsing filter: %s", expr_error());
+               }
+               expr_free(expr);
+               return;
+       }
+
+       /* update active flag */
+       list_for_each_entry(f, &filters_head, node) {
+               f->act_stat = f->sel_stat;
+       }
+       lib_set_filter(expr);
+       filters_win->changed = 1;
+}
+
+static int for_each_name(const char *str, int (*cb)(const char *name, int sel_stat))
+{
+       char buf[64];
+       int s, e, len;
+
+       e = 0;
+       do {
+               int sel_stat = FS_YES;
+
+               s = e;
+               while (str[s] == ' ')
+                       s++;
+               if (str[s] == '!') {
+                       sel_stat = FS_NO;
+                       s++;
+               }
+               e = s;
+               while (str[e] && str[e] != ' ')
+                       e++;
+
+               len = e - s;
+               if (len == 0)
+                       return 0;
+               if (len >= sizeof(buf)) {
+                       error_msg("filter name too long");
+                       return -1;
+               }
+
+               memcpy(buf, str + s, len);
+               buf[len] = 0;
+
+               if (cb(buf, sel_stat))
+                       return -1;
+       } while (1);
+}
+
+static int ensure_filter_name(const char *name, int sel_stat)
+{
+       if (find_filter(name) == NULL) {
+               error_msg("no such filter %s", name);
+               return -1;
+       }
+       return 0;
+}
+
+static int select_filter(const char *name, int sel_stat)
+{
+       struct filter_entry *e = find_filter(name);
+
+       e->sel_stat = sel_stat;
+       return 0;
+}
+
+void filters_activate_names(const char *str)
+{
+       struct filter_entry *f;
+
+       /* first validate all filter names */
+       if (str && for_each_name(str, ensure_filter_name))
+               return;
+
+       /* mark all filters unselected  */
+       list_for_each_entry(f, &filters_head, node)
+               f->sel_stat = FS_IGNORE;
+
+       /* select the filters */
+       if (str)
+               for_each_name(str, select_filter);
+
+       /* activate selected */
+       filters_activate(0);
+}
+
+void filters_toggle_filter(void)
+{
+       struct iter iter;
+
+       if (window_get_sel(filters_win, &iter)) {
+               struct filter_entry *e;
+
+               e = iter_to_filter_entry(&iter);
+               e->sel_stat = (e->sel_stat + 1) % 3;
+               filters_win->changed = 1;
+       }
+}
+
+void filters_delete_filter(void)
+{
+       struct iter iter;
+
+       if (window_get_sel(filters_win, &iter)) {
+               struct filter_entry *e;
+
+               e = iter_to_filter_entry(&iter);
+               if (yes_no_query("Delete filter '%s'? [y/N]", e->name)) {
+                       window_row_vanishes(filters_win, &iter);
+                       list_del(&e->node);
+                       free_filter(e);
+               }
+       }
+}
+
+static int validate_filter_name(const char *name)
+{
+       int i;
+
+       for (i = 0; name[i]; i++) {
+               if (isalnum((unsigned char)name[i]))
+                       continue;
+               if (name[i] == '_' || name[i] == '-')
+                       continue;
+               return 0;
+       }
+       return i != 0;
+}
+
+static void do_filters_set_filter(const char *keyval)
+{
+       const char *eq = strchr(keyval, '=');
+       char *key, *val;
+       struct expr *expr;
+       struct filter_entry *new;
+       struct list_head *item;
+
+       if (eq == NULL) {
+               if (ui_initialized)
+                       error_msg("invalid argument ('key=value' expected)");
+               return;
+       }
+       key = xstrndup(keyval, eq - keyval);
+       val = xstrdup(eq + 1);
+       if (!validate_filter_name(key)) {
+               if (ui_initialized)
+                       error_msg("invalid filter name (can only contain 'a-zA-Z0-9_-' characters)");
+               free(key);
+               free(val);
+               return;
+       }
+       expr = expr_parse(val);
+       if (expr == NULL) {
+               if (ui_initialized)
+                       error_msg("error parsing filter %s: %s", val, expr_error());
+               free(key);
+               free(val);
+               return;
+       }
+       expr_free(expr);
+
+       new = xnew(struct filter_entry, 1);
+       new->name = key;
+       new->filter = val;
+       new->act_stat = FS_IGNORE;
+       new->sel_stat = FS_IGNORE;
+
+       /* add or replace filter */
+       list_for_each(item, &filters_head) {
+               struct filter_entry *e = container_of(item, struct filter_entry, node);
+               int res = strcmp(key, e->name);
+
+               if (res < 0)
+                       break;
+               if (res == 0) {
+                       /* replace */
+                       struct iter iter;
+
+                       new->sel_stat = e->sel_stat;
+                       if (ui_initialized) {
+                               filter_entry_to_iter(e, &iter);
+                               window_row_vanishes(filters_win, &iter);
+                       }
+                       item = item->next;
+                       list_del(&e->node);
+                       free_filter(e);
+                       break;
+               }
+       }
+       /* add before item */
+       list_add_tail(&new->node, item);
+       if (ui_initialized)
+               window_changed(filters_win);
+}
+
+void filters_init(void)
+{
+       struct iter iter;
+
+       filters_win = window_new(filters_get_prev, filters_get_next);
+       window_set_contents(filters_win, &filters_head);
+       window_changed(filters_win);
+
+       iter.data0 = &filters_head;
+       iter.data1 = NULL;
+       iter.data2 = NULL;
+       filters_searchable = searchable_new(NULL, &iter, &filters_search_ops);
+}
+
+void filters_exit(void)
+{
+       searchable_free(filters_searchable);
+       window_free(filters_win);
+}
+
+void filters_set_filter(const char *keyval)
+{
+       do_filters_set_filter(keyval);
+}
+
+struct expr *parse_filter(const char *val)
+{
+       struct expr *e = NULL;
+       struct filter_entry *f;
+
+       if (val) {
+               e = expr_parse(val);
+               if (e == NULL) {
+                       error_msg("error parsing filter %s: %s", val, expr_error());
+                       return NULL;
+               }
+       }
+
+       /* mark all unvisited so that we can check recursion */
+       list_for_each_entry(f, &filters_head, node)
+               f->visited = 0;
+
+       recursive_filter = NULL;
+       if (e && expr_check_leaves(&e, get_filter)) {
+               if (recursive_filter) {
+                       error_msg("recursion detected in filter %s", recursive_filter);
+               } else {
+                       error_msg("error parsing filter: %s", expr_error());
+               }
+               expr_free(e);
+               return NULL;
+       }
+       return e;
+}
+
+void filters_set_anonymous(const char *val)
+{
+       struct filter_entry *f;
+       struct expr *e = NULL;
+
+       if (val) {
+               e = parse_filter(val);
+               if (e == NULL)
+                       return;
+       }
+
+       /* deactive all filters */
+       list_for_each_entry(f, &filters_head, node)
+               f->act_stat = FS_IGNORE;
+
+       lib_set_filter(e);
+
+       filters_win->changed = 1;
+}
+
+void filters_set_live(const char *val)
+{
+       lib_set_live_filter(val);
+       update_filterline();
+}
diff --git a/filters.h b/filters.h
new file mode 100644 (file)
index 0000000..8fe0c16
--- /dev/null
+++ b/filters.h
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_FILTERS_H
+#define CMUS_FILTERS_H
+
+#include "list.h"
+#include "window.h"
+#include "search.h"
+
+/* factivate foo !bar
+ *
+ * foo: FS_YES
+ * bar: FS_NO
+ * baz: FS_IGNORE
+ */
+enum {
+       /* [ ] filter not selected */
+       FS_IGNORE,
+       /* [*] filter selected */
+       FS_YES,
+       /* [!] filter selected and inverted */
+       FS_NO,
+};
+
+struct filter_entry {
+       struct list_head node;
+
+       char *name;
+       char *filter;
+       unsigned visited : 1;
+
+       /* selected and activated status (FS_* enum) */
+       unsigned sel_stat : 2;
+       unsigned act_stat : 2;
+};
+
+static inline struct filter_entry *iter_to_filter_entry(struct iter *iter)
+{
+       return iter->data1;
+}
+
+extern struct window *filters_win;
+extern struct searchable *filters_searchable;
+extern struct list_head filters_head;
+
+void filters_init(void);
+void filters_exit(void);
+
+/* parse filter and expand sub filters */
+struct expr *parse_filter(const char *val);
+
+/* add filter to filter list (replaces old filter with same name)
+ *
+ * @keyval  "name=value" where value is filter
+ */
+void filters_set_filter(const char *keyval);
+
+/* set throwaway filter (not saved to the filter list)
+ *
+ * @val   filter or NULL to disable filtering
+ */
+void filters_set_anonymous(const char *val);
+
+/* set live filter (not saved to the filter list)
+ *
+ * @val   filter or NULL to disable filtering
+ */
+void filters_set_live(const char *val);
+
+void filters_activate_names(const char *str);
+
+void filters_activate(int win_activate);
+void filters_toggle_filter(void);
+void filters_delete_filter(void);
+
+#endif
diff --git a/format_print.c b/format_print.c
new file mode 100644 (file)
index 0000000..883eae5
--- /dev/null
@@ -0,0 +1,748 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "format_print.h"
+#include "expr.h"
+#include "glob.h"
+#include "utils.h"
+#include "options.h"
+#include "uchar.h"
+#include "xmalloc.h"
+#include "debug.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+
+static int width;
+static int align_left;
+static int pad;
+
+static struct gbuf cond_buffer = {0, 64, 0};
+static struct gbuf l_str = {0, 256, 0};
+static struct gbuf r_str = {0, 256, 0};
+static struct fp_len str_len = {0, 0};
+static int *len = &str_len.llen;
+static struct gbuf* str = &l_str;
+
+static void stack_print(char *stack, int stack_len)
+{
+       int i = 0;
+
+       gbuf_grow(str, width ? width : stack_len);
+       char* buf = str->buffer + str->len;
+
+       if (width) {
+               if (align_left) {
+                       while (i < width && stack_len)
+                               buf[i++] = stack[--stack_len];
+                       while (i < width)
+                               buf[i++] = pad;
+               } else {
+                       int pad_len;
+
+                       if (stack_len > width)
+                               stack_len = width;
+                       pad_len = width - stack_len;
+                       while (i < pad_len)
+                               buf[i++] = pad;
+                       while (i < width)
+                               buf[i++] = stack[--stack_len];
+               }
+       } else {
+               while (stack_len)
+                       buf[i++] = stack[--stack_len];
+       }
+       str->len += i;
+       *len += i;
+}
+
+static void print_num(int num)
+{
+       char stack[20];
+       int i, p;
+
+       if (num < 0) {
+               if (width == 0)
+                       width = 1;
+               for (i = 0; i < width; i++)
+                       gbuf_add_ch(str, '?');
+               len += width;
+               return;
+       }
+       p = 0;
+       do {
+               stack[p++] = num % 10 + '0';
+               num /= 10;
+       } while (num);
+
+       stack_print(stack, p);
+}
+
+#define DBL_MAX_LEN (20)
+
+static int format_double(char *buf, int buflen, double num)
+{
+       int l = snprintf(buf, buflen, "%f", num);
+       /* skip trailing zeros */
+       while (l > 0 && buf[l-1] == '0')
+               l--;
+       return l;
+}
+
+static void print_double(double num)
+{
+       char stack[DBL_MAX_LEN], b[DBL_MAX_LEN];
+       int i, p = 0;
+
+       i = format_double(b, DBL_MAX_LEN, num) - 1;
+       while (i >= 0) {
+               stack[p++] = b[i];
+               i--;
+       }
+       stack_print(stack, p);
+}
+
+/* print '{,-}{h:,}mm:ss' */
+static void print_time(int t)
+{
+       int h, m, s;
+       char stack[32];
+       int neg = 0;
+       int p = 0;
+
+       if (t < 0) {
+               neg = 1;
+               t *= -1;
+       }
+       h = t / 3600;
+       t = t % 3600;
+       m = t / 60;
+       s = t % 60;
+
+       /* put all chars to stack in reverse order ;) */
+       stack[p++] = s % 10 + '0';
+       stack[p++] = s / 10 + '0';
+       stack[p++] = ':';
+       stack[p++] = m % 10 + '0';
+       if (m / 10 || h || time_show_leading_zero)
+               stack[p++] = m / 10 + '0';
+       if (h) {
+               stack[p++] = ':';
+               do {
+                       stack[p++] = h % 10 + '0';
+                       h /= 10;
+               } while (h);
+       }
+       if (neg)
+               stack[p++] = '-';
+
+       stack_print(stack, p);
+}
+
+static void print_str(const char *src)
+{
+       int str_width = u_str_width(src);
+       gbuf_grow(str, (width ? width : str_width) * 4);
+       *len += (width ? width : str_width);
+
+       if (width) {
+               int ws_len;
+               int i = 0;
+
+               if (align_left) {
+                       i = width;
+                       str->len += u_copy_chars(str->buffer + str->len, src, &i);
+
+                       ws_len = width - i;
+                       memset(str->buffer + str->len, ' ', ws_len);
+                       str->len += ws_len;
+               } else {
+                       int s = 0;
+
+                       ws_len = width - str_width;
+
+                       if (ws_len > 0) {
+                               memset(str->buffer + str->len, ' ', ws_len);
+                               str->len += ws_len;
+                               i += ws_len;
+                       }
+
+                       if (ws_len < 0) {
+                               int w, c = -ws_len;
+                               uchar u;
+
+                               while (c > 0) {
+                                       u = u_get_char(src, &s);
+                                       w = u_char_width(u);
+                                       c -= w;
+                               }
+                               if (c < 0) {
+                                       /* gaah, skipped too much */
+                                       if (u_char_width(u) == 2) {
+                                               /* double-byte */
+                                               str->buffer[str->len++] = ' ';
+                                       } else {
+                                               /* <xx> */
+                                               if (c == -3)
+                                                       str->buffer[str->len++] = hex_tab[(u >> 4) & 0xf];
+                                               if (c <= -2)
+                                                       str->buffer[str->len++] = hex_tab[u & 0xf];
+                                               str->buffer[str->len++] = '>';
+                                       }
+                               }
+                       }
+
+                       if (width - i > 0) {
+                               int w = width - i;
+
+                               str->len += u_copy_chars(str->buffer + str->len, src + s, &w);
+                       }
+
+               }
+       } else {
+               int s = 0, d = 0;
+               uchar u;
+
+               while (1) {
+                       u = u_get_char(src, &s);
+                       if (u == 0)
+                               break;
+                       u_set_char(str->buffer + str->len, &d, u);
+               }
+
+               str->len += d;
+       }
+}
+
+static inline int strnequal(const char *a, const char *b, size_t b_len)
+{
+       return a && (strlen(a) == b_len) && (memcmp(a, b, b_len) == 0);
+}
+
+static const struct format_option *find_fopt(const struct format_option *fopts, const char *key)
+{
+       const struct format_option *fo;
+       char ch = strlen(key) == 1 ? *key : 0;
+       for (fo = fopts; fo->type != 0; fo++) {
+               if ((ch != 0 && fo->ch == ch) || strnequal(fo->str, key, strlen(key))) {
+                       return fo;
+               }
+       }
+       return NULL;
+}
+
+static const char *str_val(const char *key, const struct format_option *fopts, char *buf)
+{
+       const struct format_option *fo;
+       const struct cmus_opt *opt;
+       const char *val = NULL;
+
+       fo = find_fopt(fopts, key);
+       if (fo && !fo->empty) {
+               if (fo->type == FO_STR)
+                       val = fo->fo_str;
+       } else {
+               opt = option_find_silent(key);
+               if (opt) {
+                       opt->get(opt->data, buf, OPTION_MAX_SIZE);
+                       val = buf;
+               }
+       }
+       return val;
+}
+
+static int int_val(const char *key, const struct format_option *fopts, char *buf)
+{
+       const struct format_option *fo;
+       int val = -1;
+
+       fo = find_fopt(fopts, key);
+       if (fo && !fo->empty) {
+               if (fo->type == FO_INT)
+                       val = fo->fo_int;
+       }
+       return val;
+}
+
+static int format_eval_cond(struct expr* expr, const struct format_option *fopts)
+{
+       if (!expr)
+               return -1;
+       enum expr_type type = expr->type;
+       const char *key;
+       const struct format_option *fo;
+       const struct cmus_opt *opt;
+       char buf[OPTION_MAX_SIZE];
+
+       if (expr->left) {
+               int left = format_eval_cond(expr->left, fopts);
+
+               if (type == EXPR_AND)
+                       return left && format_eval_cond(expr->right, fopts);
+               if (type == EXPR_OR)
+                       return left || format_eval_cond(expr->right, fopts);
+               /* EXPR_NOT */
+               return !left;
+       }
+
+       key = expr->key;
+       if (type == EXPR_STR) {
+               const char *val = str_val(key, fopts, buf);
+               int res;
+
+               if (!val)
+                       val = "";
+               res = glob_match(&expr->estr.glob_head, val);
+               if (expr->estr.op == SOP_EQ)
+                       return res;
+               return !res;
+       } else if (type == EXPR_INT) {
+               int val = int_val(key, fopts, buf);
+               int res = val - expr->eint.val;
+               if (val == -1 || expr->eint.val == -1) {
+                       switch (expr->eid.op) {
+                       case KOP_EQ:
+                               return res == 0;
+                       case KOP_NE:
+                               return res != 0;
+                       default:
+                               return 0;
+                       }
+               }
+               return expr_op_to_bool(res, expr->eint.op);
+       } else if (type == EXPR_ID) {
+               int a = 0, b = 0;
+               const char *sa, *sb;
+               int res = 0;
+               if ((sa = str_val(key, fopts, buf)) && (sb = str_val(expr->eid.key, fopts, buf))) {
+                       res = strcmp(sa, sb);
+                       return expr_op_to_bool(res, expr->eid.op);
+               } else {
+                       a = int_val(key, fopts, buf);
+                       b = int_val(expr->eid.key, fopts, buf);
+                       res = a - b;
+                       if (a == -1 || b == -1) {
+                               switch (expr->eid.op) {
+                               case KOP_EQ:
+                                       return res == 0;
+                               case KOP_NE:
+                                       return res != 0;
+                               default:
+                                       return 0;
+                               }
+                       }
+                       return expr_op_to_bool(res, expr->eid.op);
+               }
+               return res;
+       }
+       if (strcmp(key, "stream") == 0) {
+               fo = find_fopt(fopts, "filename");
+               return fo && is_http_url(fo->fo_str);
+       }
+       fo = find_fopt(fopts, key);
+       if (fo)
+               return !fo->empty;
+       opt = option_find_silent(key);
+       if (opt) {
+               opt->get(opt->data, buf, OPTION_MAX_SIZE);
+               if (strcmp(buf, "false") != 0 && strlen(buf) != 0)
+                       return 1;
+       }
+       return 0;
+}
+
+static struct expr *format_parse_cond(const char* format, int size)
+{
+       if (!cond_buffer.buffer)
+               cond_buffer.buffer = xmalloc(cond_buffer.alloc);
+       cond_buffer.len = 0;
+       gbuf_add_bytes(&cond_buffer, format, size);
+       return expr_parse_i(cond_buffer.buffer, "condition contains control characters", 0);
+}
+
+static uchar format_skip_cond_expr(const char *format, int *s)
+{
+       uchar r = 0;
+       while (format[*s]) {
+               uchar u = u_get_char(format, s);
+               if (u == '}' || u == '?') {
+                       return u;
+               }
+               if (u != '%') {
+                       continue;
+               }
+               u = u_get_char(format, s);
+               if (u == '%' || u == '?' || u == '=') {
+                       continue;
+               }
+               if (u == '-') {
+                       u = u_get_char(format, s);
+               }
+               while (isdigit(u)) {
+                       u = u_get_char(format, s);
+               }
+               if (u == '{') {
+                       unsigned level = 1;
+                       while (level) {
+                               u = u_get_char(format, s);
+                               if (u == 0)
+                                       return 0;
+                               if (u == '}')
+                                       --level;
+                               if (u != '%')
+                                       continue;
+                               u = u_get_char(format, s);
+                               if (u == '{')
+                                       ++level;
+                       }
+               }
+       }
+       return r;
+}
+
+static int format_read_cond(const char *format, int *s, int *a, int *b, int *end)
+{
+       uchar t = format_skip_cond_expr(format, s);
+       if (t != '?')
+               return 1;
+       *a = *s - 1;
+       t = format_skip_cond_expr(format, s);
+       if (t == 0)
+               return 1;
+       if (t == '?') {
+               *b = *s - 1;
+               t = format_skip_cond_expr(format, s);
+               if (t != '}')
+                       return 1;
+       }
+       *end = *s - 1;
+       return 0;
+}
+
+static void format_parse(int str_width, const char *format, const struct format_option *fopts, int f_size);
+
+static void format_parse_if(int str_width, const char *format, const struct format_option *fopts, int *s)
+{
+       int cond_pos = *s, then_pos = -1, else_pos = -1, end_pos = -1, cond_res = -1;
+       BUG_ON(format_read_cond(format, s, &then_pos, &else_pos, &end_pos) != 0);
+
+       struct expr *cond = format_parse_cond(format + cond_pos, then_pos - cond_pos);
+       cond_res = format_eval_cond(cond, fopts);
+       if (cond)
+               expr_free(cond);
+
+       BUG_ON(cond_res < 0);
+       if (cond_res) {
+               format_parse(str_width, format + then_pos + 1, fopts,
+                               (else_pos > 0 ? else_pos : end_pos) - then_pos - 1);
+       } else if (else_pos > 0) {
+               format_parse(str_width, format + else_pos + 1, fopts, end_pos - else_pos - 1);
+       }
+
+       *s = end_pos + 1;
+}
+
+static void format_parse(int str_width, const char *format, const struct format_option *fopts, int f_size)
+{
+       int s = 0;
+
+       while (s < f_size) {
+               const struct format_option *fo;
+               int long_len = 0;
+               const char *long_begin = NULL;
+               uchar u;
+
+               u = u_get_char(format, &s);
+               if (u != '%') {
+                       gbuf_grow(str, 4);
+                       u_set_char(str->buffer, (int *)&str->len, u);
+                       (*len) += u_char_width(u);
+                       continue;
+               }
+               u = u_get_char(format, &s);
+               if (u == '%' || u == '?') {
+                       gbuf_add_ch(str, u);
+                       ++(*len);
+                       continue;
+               }
+               if (u == '=') {
+                       /* right aligned text starts */
+                       str = &r_str;
+                       len = &str_len.rlen;
+                       continue;
+               }
+               align_left = 0;
+               if (u == '-') {
+                       align_left = 1;
+                       u = u_get_char(format, &s);
+               }
+               pad = ' ';
+               if (u == '0') {
+                       pad = '0';
+                       u = u_get_char(format, &s);
+               }
+               width = 0;
+               while (isdigit(u)) {
+                       /* minimum length of this field */
+                       width *= 10;
+                       width += u - '0';
+                       u = u_get_char(format, &s);
+               }
+               if (u == '%') {
+                       width = (width * str_width) / 100.0 + 0.5;
+                       u = u_get_char(format, &s);
+               }
+               if (u == '{') {
+                       long_begin = format + s;
+                       if (*long_begin == '?') {
+                               ++s;
+                               format_parse_if(str_width, format, fopts, &s);
+                               BUG_ON(s > f_size);
+                               continue;
+                       }
+                       while (1) {
+                               BUG_ON(s >= f_size);
+                               u = u_get_char(format, &s);
+                               if (u == '}')
+                                       break;
+                               long_len++;
+                       }
+               }
+               for (fo = fopts; ; fo++) {
+                       BUG_ON(fo->type == 0);
+                       if (long_len ? strnequal(fo->str, long_begin, long_len)
+                                    : (fo->ch == u)) {
+
+                               int type = fo->type;
+
+                               if (fo->empty) {
+                                       gbuf_grow(str, width);
+                                       memset(str->buffer + str->len, ' ', width);
+                                       str->len += width;
+                                       *len += width;
+                               } else if (type == FO_STR) {
+                                       print_str(fo->fo_str);
+                               } else if (type == FO_INT) {
+                                       print_num(fo->fo_int);
+                               } else if (type == FO_TIME) {
+                                       print_time(fo->fo_time);
+                               } else if (type == FO_DOUBLE) {
+                                       print_double(fo->fo_double);
+                               }
+                               break;
+                       }
+               }
+       }
+}
+
+static void format_read(int str_width, const char *format, const struct format_option *fopts)
+{
+       if (!l_str.buffer)
+               l_str.buffer = xmalloc(l_str.alloc);
+       if (!r_str.buffer)
+               r_str.buffer = xmalloc(r_str.alloc);
+       str_len.llen = 0;
+       str_len.rlen = 0;
+       str = &l_str;
+       len = &str_len.llen;
+       l_str.len = 0;
+       r_str.len = 0;
+       *l_str.buffer = 0;
+       *r_str.buffer = 0;
+       format_parse(str_width, format, fopts, strlen(format));
+
+       l_str.buffer[l_str.len] = 0;
+       r_str.buffer[r_str.len] = 0;
+}
+
+static void format_write(char *buf, int str_width)
+{
+       if (str_width == 0)
+               str_width = str_len.llen + str_len.rlen + (str_len.rlen > 0);
+
+       /* NOTE: any invalid UTF-8 bytes have already been converted to <xx>
+        *       (ASCII) where x is hex digit
+        */
+
+       if (str_len.llen + str_len.rlen <= str_width) {
+               /* both fit */
+               int ws_len = str_width - str_len.llen - str_len.rlen;
+               int pos = 0;
+
+               /* I would use strcpy if it returned anything useful */
+               while (l_str.buffer[pos]) {
+                       buf[pos] = l_str.buffer[pos];
+                       pos++;
+               }
+               memset(buf + pos, ' ', ws_len);
+               strcpy(buf + pos + ws_len, r_str.buffer);
+       } else {
+               int l_space = str_width - str_len.rlen;
+               int pos = 0;
+               int idx = 0;
+
+               if (l_space > 0)
+                       pos = u_copy_chars(buf, l_str.buffer, &l_space);
+               if (l_space < 0) {
+                       int w = -l_space;
+
+                       idx = u_skip_chars(r_str.buffer, &w);
+                       if (w != -l_space)
+                               buf[pos++] = ' ';
+               }
+               strcpy(buf + pos, r_str.buffer + idx);
+       }
+}
+
+struct fp_len format_print(char *buf, int str_width, const char *format, const struct format_option *fopts)
+{
+       format_read(str_width, format, fopts);
+
+#if DEBUG > 1
+       if (str_len.llen > 0) {
+               int ul = u_str_width(l_str.buffer);
+               if (ul != str_len.llen)
+                       d_print("L %d != %d: size=%zu '%s'\n", ul, str_len.llen, l_str.len, l_str.buffer);
+       }
+
+       if (str_len.rlen > 0) {
+               int ul = u_str_width(r_str.buffer);
+               if (ul != str_len.rlen)
+                       d_print("R %d != %d: size=%zu '%s'\n", ul, str_len.rlen, r_str.len, r_str.buffer);
+       }
+#endif
+
+       format_write(buf, str_width);
+       return str_len;
+}
+
+struct fp_len format_print_gbuf(struct gbuf *buf, int str_width, const char *format, const struct format_option *fopts)
+{
+       format_read(str_width, format, fopts);
+       int ws_len = str_width - str_len.llen - str_len.rlen;
+       gbuf_grow(buf, l_str.len + (ws_len > 0 ? ws_len : 0) + r_str.len);
+
+#if DEBUG > 1
+       if (str_len.llen > 0) {
+               int ul = u_str_width(l_str.buffer);
+               if (ul != str_len.llen)
+                       d_print("L %d != %d: size=%zu '%s'\n", ul, str_len.llen, l_str.len, l_str.buffer);
+       }
+
+       if (str_len.rlen > 0) {
+               int ul = u_str_width(r_str.buffer);
+               if (ul != str_len.rlen)
+                       d_print("R %d != %d: size=%zu '%s'\n", ul, str_len.rlen, r_str.len, r_str.buffer);
+       }
+#endif
+
+       format_write(buf->buffer + buf->len, str_width);
+       buf->len = strlen(buf->buffer);
+       return str_len;
+}
+
+static int format_valid_sub(const char *format, const struct format_option *fopts, int f_size);
+
+static int format_valid_if(const char *format, const struct format_option *fopts, int *s)
+{
+       int cond_pos = *s, then_pos = -1, else_pos = -1, end_pos = -1;
+       if (format_read_cond(format, s, &then_pos, &else_pos, &end_pos) != 0)
+               return 0;
+
+       struct expr *cond = format_parse_cond(format + cond_pos, then_pos - cond_pos);
+       if (cond == NULL)
+               return 0;
+       expr_free(cond);
+
+       if (!format_valid_sub(format + then_pos + 1, fopts,
+                               (else_pos > 0 ? else_pos : end_pos) - then_pos - 1))
+               return 0;
+       if (else_pos > 0)
+               if (!format_valid_sub(format + else_pos + 1, fopts, end_pos - else_pos - 1))
+                       return 0;
+
+       *s = end_pos + 1;
+       return 1;
+}
+
+static int format_valid_sub(const char *format, const struct format_option *fopts, int f_size)
+{
+       int s = 0;
+
+       while (s < f_size) {
+               uchar u;
+
+               u = u_get_char(format, &s);
+               if (u == '%') {
+                       int pad_zero = 0, long_len = 0;
+                       const struct format_option *fo;
+                       const char *long_begin = NULL;
+
+                       u = u_get_char(format, &s);
+                       if (u == '%' || u == '=' || u == '?')
+                               continue;
+                       if (u == '-')
+                               u = u_get_char(format, &s);
+                       if (u == '0') {
+                               pad_zero = 1;
+                               u = u_get_char(format, &s);
+                       }
+                       while (isdigit(u))
+                               u = u_get_char(format, &s);
+                       if (u == '%')
+                               u = u_get_char(format, &s);
+                       if (u == '{') {
+                               long_begin = format + s;
+                               if (*long_begin == '?') {
+                                       ++s;
+                                       if (!format_valid_if(format, fopts, &s))
+                                               return 0;
+                                       if (s > f_size)
+                                               return 0;
+                                       continue;
+                               }
+
+                               while (1) {
+                                       if (s >= f_size)
+                                               return 0;
+                                       u = u_get_char(format, &s);
+                                       if (u == '}')
+                                               break;
+                                       long_len++;
+                               }
+                       }
+                       for (fo = fopts; fo->type; fo++) {
+                               if (long_len ? strnequal(fo->str, long_begin, long_len)
+                                            : (fo->ch == u)) {
+                                       if (pad_zero && !fo->pad_zero)
+                                               return 0;
+                                       break;
+                               }
+                       }
+                       if (! fo->type)
+                               return 0;
+               }
+       }
+       return 1;
+}
+
+int format_valid(const char *format, const struct format_option *fopts)
+{
+       return format_valid_sub(format, fopts, strlen(format));
+}
diff --git a/format_print.h b/format_print.h
new file mode 100644 (file)
index 0000000..ffb5474
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_FORMAT_PRINT_H
+#define CMUS_FORMAT_PRINT_H
+
+#include "gbuf.h"
+
+struct format_option {
+       union {
+               /* NULL is treated like "" */
+               const char *fo_str;
+               int fo_int;
+               /* [h:]mm:ss. can be negative */
+               int fo_time;
+               double fo_double;
+       };
+       /* set to 1 if you want to disable printing */
+       unsigned int empty : 1;
+       /* set to 1 if zero padding is allowed */
+       unsigned int pad_zero : 1;
+       enum { FO_STR = 1, FO_INT, FO_TIME, FO_DOUBLE } type;
+       char ch;
+       const char *str;
+};
+
+/* gcc < 4.6 and icc < 12.0 can't properly initialize anonymous unions */
+#if (defined(__GNUC__) && defined(__GNUC_MINOR__) && (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 6))) || \
+       (defined(__INTEL_COMPILER) && __INTEL_COMPILER < 1200)
+#define UNION_INIT(f, v) { .f = v }
+#else
+#define UNION_INIT(f, v) .f = v
+#endif
+
+#define DEF_FO_STR(c, s, z)    { UNION_INIT(fo_str,  ""),   .type = FO_STR,    .pad_zero = z, .ch = c, .str = s }
+#define DEF_FO_INT(c, s, z)    { UNION_INIT(fo_int,  0),    .type = FO_INT,    .pad_zero = z, .ch = c, .str = s }
+#define DEF_FO_TIME(c, s, z)   { UNION_INIT(fo_time, 0),    .type = FO_TIME,   .pad_zero = z, .ch = c, .str = s }
+#define DEF_FO_DOUBLE(c, s, z) { UNION_INIT(fo_double, 0.), .type = FO_DOUBLE, .pad_zero = z, .ch = c, .str = s }
+#define DEF_FO_END             { .type = 0 }
+
+struct fp_len {
+       int llen;
+       int rlen;
+};
+
+struct fp_len format_print(char *buf, int str_width, const char *format, const struct format_option *fopts);
+struct fp_len format_print_gbuf(struct gbuf *buf, int str_width, const char *format, const struct format_option *fopts);
+int format_valid(const char *format, const struct format_option *fopts);
+
+#endif
diff --git a/gbuf.c b/gbuf.c
new file mode 100644 (file)
index 0000000..f69719b
--- /dev/null
+++ b/gbuf.c
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2008 Timo Hirvonen
+ *
+ * This code is largely based on strbuf in the GIT version control system.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "gbuf.h"
+#include "xmalloc.h"
+
+#include <stdio.h>
+#include <stdarg.h>
+
+char gbuf_empty_buffer[1];
+
+static inline void gbuf_init(struct gbuf *buf)
+{
+       buf->buffer = gbuf_empty_buffer;
+       buf->alloc = 0;
+       buf->len = 0;
+}
+
+void gbuf_grow(struct gbuf *buf, size_t more)
+{
+       size_t align = 16 - 1;
+       size_t alloc = (buf->len + more + 1 + align) & ~align;
+
+       if (alloc > buf->alloc) {
+               if (!buf->alloc)
+                       buf->buffer = NULL;
+               buf->alloc = alloc;
+               buf->buffer = xrealloc(buf->buffer, buf->alloc);
+               // gbuf is not NUL terminated if this was first alloc
+               buf->buffer[buf->len] = 0;
+       }
+}
+
+void gbuf_free(struct gbuf *buf)
+{
+       if (buf->alloc)
+               free(buf->buffer);
+       gbuf_init(buf);
+}
+
+void gbuf_add_ch(struct gbuf *buf, char ch)
+{
+       gbuf_grow(buf, 1);
+       buf->buffer[buf->len++] = ch;
+       buf->buffer[buf->len] = 0;
+}
+
+void gbuf_add_bytes(struct gbuf *buf, const void *data, size_t len)
+{
+       gbuf_grow(buf, len);
+       memcpy(buf->buffer + buf->len, data, len);
+       buf->len += len;
+       buf->buffer[buf->len] = 0;
+}
+
+void gbuf_add_str(struct gbuf *buf, const char *str)
+{
+       int len = strlen(str);
+
+       if (!len)
+               return;
+       gbuf_grow(buf, len);
+       memcpy(buf->buffer + buf->len, str, len + 1);
+       buf->len += len;
+}
+
+void gbuf_addf(struct gbuf *buf, const char *fmt, ...)
+{
+       va_list ap;
+       int slen;
+
+       va_start(ap, fmt);
+       slen = vsnprintf(buf->buffer + buf->len, buf->alloc - buf->len, fmt, ap);
+       va_end(ap);
+
+       if (slen > gbuf_avail(buf)) {
+               gbuf_grow(buf, slen + 1);
+
+               va_start(ap, fmt);
+               slen = vsnprintf(buf->buffer + buf->len, buf->alloc - buf->len, fmt, ap);
+               va_end(ap);
+       }
+
+       buf->len += slen;
+}
+
+void gbuf_set(struct gbuf *buf, int c, size_t count)
+{
+       gbuf_grow(buf, count);
+       memset(buf->buffer + buf->len, c, count);
+       buf->len += count;
+       buf->buffer[buf->len] = 0;
+}
+
+char *gbuf_steal(struct gbuf *buf)
+{
+       char *b = buf->buffer;
+       if (!buf->alloc)
+               b = xnew0(char, 1);
+       gbuf_init(buf);
+       return b;
+}
diff --git a/gbuf.h b/gbuf.h
new file mode 100644 (file)
index 0000000..baacd2b
--- /dev/null
+++ b/gbuf.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2008 Timo Hirvonen
+ *
+ * This code is largely based on strbuf in the GIT version control system.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_GBUF_H
+#define CMUS_GBUF_H
+
+#include "compiler.h"
+
+#include <stddef.h> /* size_t */
+
+struct gbuf {
+       char *buffer;
+       size_t alloc;
+       size_t len;
+};
+
+extern char gbuf_empty_buffer[];
+
+#define GBUF(name) struct gbuf name = { gbuf_empty_buffer, 0, 0 }
+
+static inline void gbuf_clear(struct gbuf *buf)
+{
+       buf->len = 0;
+       buf->buffer[0] = 0;
+}
+
+static inline size_t gbuf_avail(struct gbuf *buf)
+{
+       if (buf->alloc)
+               return buf->alloc - buf->len - 1;
+       return 0;
+}
+
+void gbuf_grow(struct gbuf *buf, size_t more);
+void gbuf_free(struct gbuf *buf);
+void gbuf_add_ch(struct gbuf *buf, char ch);
+void gbuf_add_bytes(struct gbuf *buf, const void *data, size_t len);
+void gbuf_add_str(struct gbuf *buf, const char *str);
+void gbuf_addf(struct gbuf *buf, const char *fmt, ...) CMUS_FORMAT(2, 3);
+void gbuf_set(struct gbuf *buf, int c, size_t count);
+char *gbuf_steal(struct gbuf *buf);
+
+#endif
diff --git a/glob.c b/glob.c
new file mode 100644 (file)
index 0000000..0a1e848
--- /dev/null
+++ b/glob.c
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "glob.h"
+#include "uchar.h"
+#include "list.h"
+#include "xmalloc.h"
+#include "debug.h"
+
+#include <string.h>
+
+struct glob_item {
+       struct list_head node;
+       enum {
+               GLOB_STAR,
+               GLOB_QMARK,
+               GLOB_TEXT
+       } type;
+       char text[];
+};
+
+/* simplification:
+ * 
+ *   ??*? => ???*
+ *   *?*  => ?*
+ *   *?   => ?*
+ *   ...
+ */
+static void simplify(struct list_head *head)
+{
+       struct list_head *item;
+
+       item = head->next;
+       while (item != head) {
+               struct list_head *i, *next;
+               int qcount = 0;
+               int scount = 0;
+
+               i = item;
+               do {
+                       struct glob_item *gi;
+
+                       gi = container_of(i, struct glob_item, node);
+                       if (gi->type == GLOB_STAR) {
+                               scount++;
+                       } else if (gi->type == GLOB_QMARK) {
+                               qcount++;
+                       } else {
+                               i = i->next;
+                               break;
+                       }
+                       i = i->next;
+               } while (i != head);
+
+               next = i;
+
+               if (scount) {
+                       /* move all qmarks to front and
+                        * if there are >1 stars remove all but the last */
+                       struct list_head *insert_after = item->prev;
+
+                       i = item;
+                       while (qcount) {
+                               struct glob_item *gi;
+
+                               gi = container_of(i, struct glob_item, node);
+                               i = i->next;
+                               if (gi->type == GLOB_QMARK) {
+                                       list_del(&gi->node);
+                                       list_add(&gi->node, insert_after);
+                                       qcount--;
+                               }
+                       }
+
+                       i = item;
+                       while (scount > 1) {
+                               struct glob_item *gi;
+
+                               gi = container_of(i, struct glob_item, node);
+                               i = i->next;
+                               if (gi->type == GLOB_STAR) {
+                                       list_del(&gi->node);
+                                       free(gi);
+                                       scount--;
+                               }
+                       }
+               }
+
+               item = next;
+       }
+}
+
+void glob_compile(struct list_head *head, const char *pattern)
+{
+       int i = 0;
+
+       list_init(head);
+       while (pattern[i]) {
+               struct glob_item *item;
+
+               if (pattern[i] == '*') {
+                       item = xnew(struct glob_item, 1);
+                       item->type = GLOB_STAR;
+                       i++;
+               } else if (pattern[i] == '?') {
+                       item = xnew(struct glob_item, 1);
+                       item->type = GLOB_QMARK;
+                       i++;
+               } else {
+                       int start, len, j;
+                       char *str;
+
+                       start = i;
+                       len = 0;
+                       while (pattern[i]) {
+                               if (pattern[i] == '\\') {
+                                       i++;
+                                       len++;
+                                       if (pattern[i])
+                                               i++;
+                               } else if (pattern[i] == '*') {
+                                       break;
+                               } else if (pattern[i] == '?') {
+                                       break;
+                               } else {
+                                       i++;
+                                       len++;
+                               }
+                       }
+
+                       item = xmalloc(sizeof(struct glob_item) + len + 1);
+                       item->type = GLOB_TEXT;
+
+                       str = item->text;
+                       i = start;
+                       j = 0;
+                       while (j < len) {
+                               if (pattern[i] == '\\') {
+                                       i++;
+                                       if (pattern[i]) {
+                                               str[j++] = pattern[i++];
+                                       } else {
+                                               str[j++] = '\\';
+                                       }
+                               } else {
+                                       str[j++] = pattern[i++];
+                               }
+                       }
+                       str[j] = 0;
+               }
+               list_add_tail(&item->node, head);
+       }
+       simplify(head);
+}
+
+void glob_free(struct list_head *head)
+{
+       struct list_head *item = head->next;
+
+       while (item != head) {
+               struct glob_item *gi;
+               struct list_head *next = item->next;
+
+               gi = container_of(item, struct glob_item, node);
+               free(gi);
+               item = next;
+       }
+}
+
+static int do_glob_match(struct list_head *head, struct list_head *first, const char *text)
+{
+       struct list_head *item = first;
+
+       while (item != head) {
+               struct glob_item *gitem;
+
+               gitem = container_of(item, struct glob_item, node);
+               if (gitem->type == GLOB_TEXT) {
+                       int len = u_strlen(gitem->text);
+
+                       if (!u_strncase_equal_base(gitem->text, text, len))
+                               return 0;
+                       text += strlen(gitem->text);
+               } else if (gitem->type == GLOB_QMARK) {
+                       uchar u;
+                       int idx = 0;
+
+                       u = u_get_char(text, &idx);
+                       if (u == 0)
+                               return 0;
+                       text += idx;
+               } else if (gitem->type == GLOB_STAR) {
+                       /* after star there MUST be normal text (or nothing),
+                        * question marks have been moved before this star and
+                        * other stars have been sripped (see simplify)
+                        */
+                       struct list_head *next;
+                       struct glob_item *next_gi;
+                       const char *t;
+                       int tlen;
+
+                       next = item->next;
+                       if (next == head) {
+                               /* this star was the last item => matched */
+                               return 1;
+                       }
+                       next_gi = container_of(next, struct glob_item, node);
+                       BUG_ON(next_gi->type != GLOB_TEXT);
+                       t = next_gi->text;
+                       tlen = strlen(t);
+                       while (1) {
+                               const char *pos;
+
+                               pos = u_strcasestr_base(text, t);
+                               if (pos == NULL)
+                                       return 0;
+                               if (do_glob_match(head, next->next, pos + tlen))
+                                       return 1;
+                               text = pos + 1;
+                       }
+               }
+               item = item->next;
+       }
+       return text[0] == 0;
+}
+
+int glob_match(struct list_head *head, const char *text)
+{
+       return do_glob_match(head, head->next, text);
+}
diff --git a/glob.h b/glob.h
new file mode 100644 (file)
index 0000000..e08fd62
--- /dev/null
+++ b/glob.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_GLOB_H
+#define CMUS_GLOB_H
+
+#include "list.h"
+
+void glob_compile(struct list_head *head, const char *pattern);
+void glob_free(struct list_head *head);
+int glob_match(struct list_head *head, const char *text);
+
+#endif
diff --git a/help.c b/help.c
new file mode 100644 (file)
index 0000000..13248db
--- /dev/null
+++ b/help.c
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 <ft@bewatermyfriend.org>
+ *
+ * heavily based on filters.c
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "help.h"
+#include "window.h"
+#include "search.h"
+#include "misc.h"
+#include "xmalloc.h"
+#include "keys.h"
+#include "command_mode.h"
+#include "ui_curses.h"
+#include "options.h"
+#include "cmdline.h"
+
+#include <stdio.h>
+
+struct window *help_win;
+struct searchable *help_searchable;
+
+static LIST_HEAD(help_head);
+static struct list_head *bound_head;
+static struct list_head *bound_tail;
+static struct list_head *unbound_head;
+static struct list_head *unbound_tail;
+
+static inline void help_entry_to_iter(struct help_entry *e, struct iter *iter)
+{
+       iter->data0 = &help_head;
+       iter->data1 = e;
+       iter->data2 = NULL;
+}
+
+static GENERIC_ITER_PREV(help_get_prev, struct help_entry, node)
+static GENERIC_ITER_NEXT(help_get_next, struct help_entry, node)
+
+static int help_search_get_current(void *data, struct iter *iter)
+{
+       return window_get_sel(help_win, iter);
+}
+
+static int help_search_matches(void *data, struct iter *iter, const char *text)
+{
+       int matched = 0;
+       char **words = get_words(text);
+
+       if (words[0] != NULL) {
+               struct help_entry *ent;
+               int i;
+
+               ent = iter_to_help_entry(iter);
+               for (i = 0; ; i++) {
+                       if (words[i] == NULL) {
+                               window_set_sel(help_win, iter);
+                               matched = 1;
+                               break;
+                       }
+                       if (ent->type == HE_TEXT) {
+                               if (!u_strcasestr(ent->text, words[i]))
+                                       break;
+                       } else if (ent->type == HE_BOUND) {
+                               if (!u_strcasestr(ent->binding->cmd, words[i]) &&
+                                       !u_strcasestr(ent->binding->key->name, words[i]))
+                                       break;
+                       } else if (ent->type == HE_UNBOUND) {
+                               if (!u_strcasestr(ent->command->name, words[i]))
+                                       break;
+                       } else if (ent->type == HE_OPTION) {
+                               if (!u_strcasestr(ent->option->name, words[i]))
+                                       break;
+                       }
+               }
+       }
+       free_str_array(words);
+       return matched;
+}
+
+static const struct searchable_ops help_search_ops = {
+       .get_prev = help_get_prev,
+       .get_next = help_get_next,
+       .get_current = help_search_get_current,
+       .matches = help_search_matches
+};
+
+static void help_add_text(const char *s)
+{
+       struct help_entry *ent;
+       ent = xnew(struct help_entry, 1);
+       ent->type = HE_TEXT;
+       ent->text = s;
+       list_add_tail(&ent->node, &help_head);
+}
+
+static void help_add_defaults(void)
+{
+       struct cmus_opt *opt;
+
+       help_add_text("Keybindings");
+       help_add_text("-----------");
+       bound_head = help_head.prev;
+       help_add_text("");
+       help_add_text("Unbound Commands");
+       help_add_text("----------------");
+       unbound_head = help_head.prev;
+       help_add_text("");
+       help_add_text("Options");
+       help_add_text("-------");
+
+       list_for_each_entry(opt, &option_head, node) {
+               struct help_entry *ent = xnew(struct help_entry, 1);
+
+               ent->type = HE_OPTION;
+               ent->option = opt;
+               list_add_tail(&ent->node, &help_head);
+       }
+
+       bound_tail = bound_head->next;
+       unbound_tail = unbound_head->next;
+}
+
+void help_remove_unbound(struct command *cmd)
+{
+       struct help_entry *ent;
+       struct iter i;
+       list_for_each_entry(ent, &help_head, node) {
+               if (ent->type != HE_UNBOUND)
+                       continue;
+               if (ent->command == cmd) {
+                       help_entry_to_iter(ent, &i);
+                       window_row_vanishes(help_win, &i);
+                       list_del(&ent->node);
+                       free(ent);
+                       return;
+               }
+       }
+}
+
+static void list_add_sorted(struct list_head *new, struct list_head *head,
+               struct list_head *tail,
+               int (*cmp)(struct list_head *, struct list_head *))
+{
+       struct list_head *item = tail->prev;
+
+       while (item != head) {
+               if (cmp(new, item) >= 0)
+                       break;
+               item = item->prev;
+       }
+       /* add after item */
+       list_add(new, item);
+}
+
+static int bound_cmp(struct list_head *ai, struct list_head *bi)
+{
+       struct help_entry *a = container_of(ai, struct help_entry, node);
+       struct help_entry *b = container_of(bi, struct help_entry, node);
+       int ret = a->binding->ctx - b->binding->ctx;
+
+       if (!ret)
+               ret = strcmp(a->binding->key->name, b->binding->key->name);
+       return ret;
+}
+
+static int unbound_cmp(struct list_head *ai, struct list_head *bi)
+{
+       struct help_entry *a = container_of(ai, struct help_entry, node);
+       struct help_entry *b = container_of(bi, struct help_entry, node);
+
+       return strcmp(a->command->name, b->command->name);
+}
+
+void help_add_unbound(struct command *cmd)
+{
+       struct help_entry *ent;
+
+       ent = xnew(struct help_entry, 1);
+       ent->type = HE_UNBOUND;
+       ent->command = cmd;
+       list_add_sorted(&ent->node, unbound_head, unbound_tail, unbound_cmp);
+}
+
+void help_add_all_unbound(void)
+{
+       int i;
+       for (i = 0; commands[i].name; ++i)
+               if (!commands[i].bc)
+                       help_add_unbound(&commands[i]);
+}
+
+void help_select(void)
+{
+       struct iter sel;
+       struct help_entry *ent;
+       char buf[OPTION_MAX_SIZE];
+
+       if (!window_get_sel(help_win, &sel))
+               return;
+
+       ent = iter_to_help_entry(&sel);
+       switch (ent->type) {
+       case HE_BOUND:
+               snprintf(buf, sizeof(buf), "bind -f %s %s %s",
+                               key_context_names[ent->binding->ctx],
+                               ent->binding->key->name,
+                               ent->binding->cmd);
+               cmdline_set_text(buf);
+               enter_command_mode();
+               break;
+       case HE_UNBOUND:
+               snprintf(buf, sizeof(buf), "bind common <key> %s",
+                               ent->command->name);
+               cmdline_set_text(buf);
+               enter_command_mode();
+               break;
+       case HE_OPTION:
+               snprintf(buf, sizeof(buf), "set %s=", ent->option->name);
+               size_t len = strlen(buf);
+               ent->option->get(ent->option->data, buf + len, sizeof(buf) - len);
+               cmdline_set_text(buf);
+               enter_command_mode();
+               break;
+       default:
+               break;
+       }
+}
+
+void help_toggle(void)
+{
+       struct iter sel;
+       struct help_entry *ent;
+
+       if (!window_get_sel(help_win, &sel))
+               return;
+
+       ent = iter_to_help_entry(&sel);
+       switch (ent->type) {
+       case HE_OPTION:
+               if (ent->option->toggle) {
+                       ent->option->toggle(ent->option->data);
+                       help_win->changed = 1;
+               }
+               break;
+       default:
+               break;
+       }
+}
+
+void help_remove(void)
+{
+       struct iter sel;
+       struct help_entry *ent;
+
+       if (!window_get_sel(help_win, &sel))
+               return;
+
+       ent = iter_to_help_entry(&sel);
+       switch (ent->type) {
+       case HE_BOUND:
+               if (yes_no_query("Remove selected binding? [y/N]"))
+                       key_unbind(key_context_names[ent->binding->ctx],
+                                       ent->binding->key->name, 0);
+               break;
+       default:
+               break;
+       }
+}
+
+void help_add_bound(const struct binding *bind)
+{
+       struct help_entry *ent;
+       ent = xnew(struct help_entry, 1);
+       ent->type = HE_BOUND;
+       ent->binding = bind;
+       list_add_sorted(&ent->node, bound_head, bound_tail, bound_cmp);
+}
+
+void help_remove_bound(const struct binding *bind)
+{
+       struct help_entry *ent;
+       struct iter i;
+       list_for_each_entry(ent, &help_head, node) {
+               if (ent->binding == bind) {
+                       help_entry_to_iter(ent, &i);
+                       window_row_vanishes(help_win, &i);
+                       list_del(&ent->node);
+                       free(ent);
+                       return;
+               }
+       }
+}
+
+void help_init(void)
+{
+       struct iter iter;
+
+       help_win = window_new(help_get_prev, help_get_next);
+       window_set_contents(help_win, &help_head);
+       window_changed(help_win);
+       help_add_defaults();
+
+       iter.data0 = &help_head;
+       iter.data1 = NULL;
+       iter.data2 = NULL;
+       help_searchable = searchable_new(NULL, &iter, &help_search_ops);
+}
+
+void help_exit(void)
+{
+       searchable_free(help_searchable);
+       window_free(help_win);
+}
diff --git a/help.h b/help.h
new file mode 100644 (file)
index 0000000..6beeb7e
--- /dev/null
+++ b/help.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 <ft@bewatermyfriend.org>
+ *
+ * heavily based on filters.h
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_HELP_H
+#define CMUS_HELP_H
+
+#include "list.h"
+#include "window.h"
+#include "search.h"
+#include "keys.h"
+
+struct help_entry {
+       struct list_head node;
+       enum {
+               HE_TEXT,                /* text entries         */
+               HE_BOUND,               /* bound keys           */
+               HE_UNBOUND,             /* unbound commands     */
+               HE_OPTION,
+       } type;
+       union {
+               const char *text;                       /* HE_TEXT      */
+               const struct binding *binding;          /* HE_BOUND     */
+               const struct command *command;          /* HE_UNBOUND   */
+               const struct cmus_opt *option;
+       };
+};
+
+static inline struct help_entry *iter_to_help_entry(struct iter *iter)
+{
+       return iter->data1;
+}
+
+extern struct window *help_win;
+extern struct searchable *help_searchable;
+
+void help_select(void);
+void help_toggle(void);
+void help_remove(void);
+
+void help_add_bound(const struct binding *bind);
+void help_remove_bound(const struct binding *bind);
+void help_remove_unbound(struct command *cmd);
+void help_add_unbound(struct command *cmd);
+void help_add_all_unbound(void);
+
+void help_init(void);
+void help_exit(void);
+
+#endif /* HELP_H */
diff --git a/history.c b/history.c
new file mode 100644 (file)
index 0000000..1d7195e
--- /dev/null
+++ b/history.c
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "history.h"
+#include "xmalloc.h"
+#include "file.h"
+#include "uchar.h"
+#include "list.h"
+#include "prog.h"
+
+#include <sys/types.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+
+struct history_entry {
+       struct list_head node;
+       char *text;
+};
+
+static struct history_entry *history_entry_new(const char *text)
+{
+       struct history_entry *new;
+       new = xnew(struct history_entry, 1);
+       new->text = xstrdup(text);
+       return new;
+}
+
+static void history_entry_free(struct history_entry *history)
+{
+       free(history->text);
+       free(history);
+}
+
+void history_free(struct history *history)
+{
+       struct list_head *item, *temp;
+       list_for_each_safe(item, temp, &history->head) {
+               struct history_entry *history_entry;
+               history_entry = list_entry(item, struct history_entry, node);
+               history_entry_free(history_entry);
+       }
+}
+
+static int history_add_tail(void *data, const char *line)
+{
+       struct history *history = data;
+
+       if (history->lines < history->max_lines) {
+               struct history_entry *new;
+
+               new = history_entry_new(line);
+               list_add_tail(&new->node, &history->head);
+               history->lines++;
+       }
+       return 0;
+}
+
+void history_load(struct history *history, char *filename, int max_lines)
+{
+       list_init(&history->head);
+       history->max_lines = max_lines;
+       history->lines = 0;
+       history->search_pos = NULL;
+       history->filename = filename;
+       file_for_each_line(filename, history_add_tail, history);
+}
+
+void history_save(struct history *history)
+{
+       char filename_tmp[512];
+       struct list_head *item;
+       int fd;
+       ssize_t rc;
+
+       snprintf(filename_tmp, sizeof(filename_tmp), "%s.tmp", history->filename);
+       fd = open(filename_tmp, O_CREAT | O_WRONLY | O_TRUNC, 0666);
+       if (fd == -1)
+               return;
+       list_for_each(item, &history->head) {
+               struct history_entry *history_entry;
+               const char nl = '\n';
+
+               history_entry = list_entry(item, struct history_entry, node);
+
+               rc = write(fd, history_entry->text, strlen(history_entry->text));
+               if (rc == -1)
+                       goto out;
+
+               rc = write(fd, &nl, 1);
+               if (rc == -1)
+                       goto out;
+       }
+out:
+       close(fd);
+
+       rc = rename(filename_tmp, history->filename);
+       if (rc)
+               warn_errno("renaming %s to %s", filename_tmp, history->filename);
+}
+
+void history_add_line(struct history *history, const char *line)
+{
+       struct history_entry *new;
+       struct list_head *item;
+
+       new = history_entry_new(line);
+       list_add(&new->node, &history->head);
+       history->lines++;
+
+       /* remove identical */
+       item = history->head.next->next;
+       while (item != &history->head) {
+               struct list_head *next = item->next;
+               struct history_entry *hentry;
+
+               hentry = container_of(item, struct history_entry, node);
+               if (strcmp(hentry->text, new->text) == 0) {
+                       list_del(item);
+                       history_entry_free(hentry);
+                       history->lines--;
+               }
+               item = next;
+       }
+
+       /* remove oldest if history is 'full' */
+       if (history->lines > history->max_lines) {
+               struct list_head *node;
+               struct history_entry *hentry;
+
+               node = history->head.prev;
+               list_del(node);
+               hentry = list_entry(node, struct history_entry, node);
+               history_entry_free(hentry);
+               history->lines--;
+       }
+}
+
+void history_reset_search(struct history *history)
+{
+       history->search_pos = NULL;
+}
+
+const char *history_search_forward(struct history *history, const char *text)
+{
+       struct list_head *item;
+       int search_len;
+
+       if (history->search_pos == NULL) {
+               /* first time to search. set search */
+               item = history->head.next;
+       } else {
+               item = history->search_pos->next;
+       }
+       search_len = strlen(text);
+       while (item != &history->head) {
+               struct history_entry *hentry;
+
+               hentry = list_entry(item, struct history_entry, node);
+               if (strncmp(text, hentry->text, search_len) == 0) {
+                       history->search_pos = item;
+                       return hentry->text;
+               }
+               item = item->next;
+       }
+       return NULL;
+}
+
+const char *history_search_backward(struct history *history, const char *text)
+{
+       struct list_head *item;
+       int search_len;
+
+       if (history->search_pos == NULL)
+               return NULL;
+       item = history->search_pos->prev;
+       search_len = strlen(text);
+       while (item != &history->head) {
+               struct history_entry *hentry;
+
+               hentry = list_entry(item, struct history_entry, node);
+               if (strncmp(text, hentry->text, search_len) == 0) {
+                       history->search_pos = item;
+                       return hentry->text;
+               }
+               item = item->prev;
+       }
+       history->search_pos = NULL;
+       return NULL;
+}
diff --git a/history.h b/history.h
new file mode 100644 (file)
index 0000000..d06eb69
--- /dev/null
+++ b/history.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_HISTORY_H
+#define CMUS_HISTORY_H
+
+#include "list.h"
+
+struct history {
+       struct list_head head;
+       struct list_head *search_pos;
+       char *filename;
+       int max_lines;
+       int lines;
+};
+
+void history_load(struct history *history, char *filename, int max_lines);
+void history_save(struct history *history);
+void history_free(struct history *history);
+void history_add_line(struct history *history, const char *line);
+void history_reset_search(struct history *history);
+const char *history_search_forward(struct history *history, const char *text);
+const char *history_search_backward(struct history *history, const char *text);
+
+#endif
diff --git a/http.c b/http.c
new file mode 100644 (file)
index 0000000..3c1ff68
--- /dev/null
+++ b/http.c
@@ -0,0 +1,516 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "http.h"
+#include "file.h"
+#include "debug.h"
+#include "xmalloc.h"
+#include "gbuf.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/time.h>
+#include <sys/socket.h>
+#include <sys/select.h>
+#include <netinet/in.h>
+#include <netdb.h>
+#include <arpa/inet.h>
+#include <string.h>
+#include <errno.h>
+
+/*
+ * @uri is http://[user[:pass]@]host[:port][/path][?query]
+ *
+ * uri(7): If the URL supplies a user  name  but no  password, and the remote
+ * server requests a password, the program interpreting the URL should request
+ * one from the user.
+ */
+int http_parse_uri(const char *uri, struct http_uri *u)
+{
+       const char *str, *colon, *at, *slash, *host_start;
+
+       /* initialize all fields */
+       u->uri  = xstrdup(uri);
+       u->user = NULL;
+       u->pass = NULL;
+       u->host = NULL;
+       u->path = NULL;
+       u->port = 80;
+
+       if (strncmp(uri, "http://", 7))
+               return -1;
+       str = uri + 7;
+       host_start = str;
+
+       /* [/path] */
+       slash = strchr(str, '/');
+       if (slash) {
+               u->path = xstrdup(slash);
+       } else {
+               u->path = xstrdup("/");
+       }
+
+       /* [user[:pass]@] */
+       at = strchr(str, '@');
+       if (at && (!slash || at < slash)) {
+               /* user[:pass]@ */
+               host_start = at + 1;
+               colon = strchr(str, ':');
+               if (colon == NULL || colon > at) {
+                       /* user */
+                       u->user = xstrndup(str, at - str);
+               } else {
+                       /* user:pass */
+                       u->user = xstrndup(str, colon - str);
+                       u->pass = xstrndup(colon + 1, at - (colon + 1));
+               }
+       }
+
+       /* host[:port] */
+       colon = strchr(host_start, ':');
+       if (colon && (!slash || colon < slash)) {
+               /* host:port */
+               const char *start;
+               int port;
+
+               u->host = xstrndup(host_start, colon - host_start);
+               colon++;
+               start = colon;
+
+               port = 0;
+               while (*colon >= '0' && *colon <= '9') {
+                       port *= 10;
+                       port += *colon - '0';
+                       colon++;
+               }
+               u->port = port;
+
+               if (colon == start || (*colon != 0 && *colon != '/')) {
+                       http_free_uri(u);
+                       return -1;
+               }
+       } else {
+               /* host */
+               if (slash) {
+                       u->host = xstrndup(host_start, slash - host_start);
+               } else {
+                       u->host = xstrdup(host_start);
+               }
+       }
+       return 0;
+}
+
+void http_free_uri(struct http_uri *u)
+{
+       free(u->uri);
+       free(u->user);
+       free(u->pass);
+       free(u->host);
+       free(u->path);
+
+       u->uri  = NULL;
+       u->user = NULL;
+       u->pass = NULL;
+       u->host = NULL;
+       u->path = NULL;
+}
+
+int http_open(struct http_get *hg, int timeout_ms)
+{
+       const struct addrinfo hints = {
+               .ai_socktype = SOCK_STREAM
+       };
+       struct addrinfo *result;
+       union {
+               struct sockaddr sa;
+               struct sockaddr_storage sas;
+       } addr;
+       size_t addrlen;
+       struct timeval tv;
+       int save, flags, rc;
+       char port[16];
+
+       char *proxy = getenv("http_proxy");
+       if (proxy) {
+               hg->proxy = xnew(struct http_uri, 1);
+               if (http_parse_uri(proxy, hg->proxy)) {
+                       d_print("Failed to parse HTTP proxy URI '%s'\n", proxy);
+                       return -1;
+               }
+       } else {
+               hg->proxy = NULL;
+       }
+
+       snprintf(port, sizeof(port), "%d", hg->proxy ? hg->proxy->port : hg->uri.port);
+       rc = getaddrinfo(hg->proxy ? hg->proxy->host : hg->uri.host, port, &hints, &result);
+       if (rc != 0) {
+               d_print("getaddrinfo: %s\n", gai_strerror(rc));
+               return -1;
+       }
+       memcpy(&addr.sa, result->ai_addr, result->ai_addrlen);
+       addrlen = result->ai_addrlen;
+       freeaddrinfo(result);
+
+       hg->fd = socket(addr.sa.sa_family, SOCK_STREAM, 0);
+       if (hg->fd == -1)
+               return -1;
+
+       flags = fcntl(hg->fd, F_GETFL);
+       if (fcntl(hg->fd, F_SETFL, O_NONBLOCK) == -1)
+               goto close_exit;
+
+       tv.tv_sec = timeout_ms / 1000;
+       tv.tv_usec = (timeout_ms % 1000) * 1000;
+       while (1) {
+               fd_set wfds;
+
+               d_print("connecting. timeout=%lld s %lld us\n", (long long)tv.tv_sec, (long long)tv.tv_usec);
+               if (connect(hg->fd, &addr.sa, addrlen) == 0)
+                       break;
+               if (errno == EISCONN)
+                       break;
+               if (errno != EAGAIN && errno != EINPROGRESS)
+                       goto close_exit;
+
+               FD_ZERO(&wfds);
+               FD_SET(hg->fd, &wfds);
+               while (1) {
+                       rc = select(hg->fd + 1, NULL, &wfds, NULL, &tv);
+                       if (rc == -1) {
+                               if (errno != EINTR)
+                                       goto close_exit;
+                               /* signalled */
+                               continue;
+                       }
+                       if (rc == 1) {
+                               /* socket ready */
+                               break;
+                       }
+                       if (tv.tv_sec == 0 && tv.tv_usec == 0) {
+                               errno = ETIMEDOUT;
+                               goto close_exit;
+                       }
+               }
+       }
+
+       /* restore old flags */
+       if (fcntl(hg->fd, F_SETFL, flags) == -1)
+               goto close_exit;
+       return 0;
+close_exit:
+       save = errno;
+       close(hg->fd);
+       errno = save;
+       return -1;
+}
+
+static int http_write(int fd, const char *buf, int count, int timeout_ms)
+{
+       struct timeval tv;
+       int pos = 0;
+
+       tv.tv_sec = timeout_ms / 1000;
+       tv.tv_usec = (timeout_ms % 1000) * 1000;
+       while (1) {
+               fd_set wfds;
+               int rc;
+
+               d_print("timeout=%lld s %lld us\n", (long long)tv.tv_sec, (long long)tv.tv_usec);
+
+               FD_ZERO(&wfds);
+               FD_SET(fd, &wfds);
+               rc = select(fd + 1, NULL, &wfds, NULL, &tv);
+               if (rc == -1) {
+                       if (errno != EINTR)
+                               return -1;
+                       /* signalled */
+                       continue;
+               }
+               if (rc == 1) {
+                       rc = write(fd, buf + pos, count - pos);
+                       if (rc == -1) {
+                               if (errno == EINTR || errno == EAGAIN)
+                                       continue;
+                               return -1;
+                       }
+                       pos += rc;
+                       if (pos == count)
+                               return 0;
+               } else if (tv.tv_sec == 0 && tv.tv_usec == 0) {
+                       errno = ETIMEDOUT;
+                       return -1;
+               }
+       }
+}
+
+static int read_timeout(int fd, int timeout_ms)
+{
+       struct timeval tv;
+
+       tv.tv_sec = timeout_ms / 1000;
+       tv.tv_usec = (timeout_ms % 1000) * 1000;
+       while (1) {
+               fd_set rfds;
+               int rc;
+
+               FD_ZERO(&rfds);
+               FD_SET(fd, &rfds);
+               rc = select(fd + 1, &rfds, NULL, NULL, &tv);
+               if (rc == -1) {
+                       if (errno != EINTR)
+                               return -1;
+                       /* signalled */
+                       continue;
+               }
+               if (rc == 1)
+                       return 0;
+               if (tv.tv_sec == 0 && tv.tv_usec == 0) {
+                       errno = ETIMEDOUT;
+                       return -1;
+               }
+       }
+}
+
+/* reads response, ignores fscking carriage returns */
+static int http_read_response(int fd, struct gbuf *buf, int timeout_ms)
+{
+       char prev = 0;
+
+       if (read_timeout(fd, timeout_ms))
+               return -1;
+       while (1) {
+               int rc;
+               char ch;
+
+               rc = read(fd, &ch, 1);
+               if (rc == -1) {
+                       return -1;
+               }
+               if (rc == 0) {
+                       return -2;
+               }
+               if (ch == '\r')
+                       continue;
+               if (ch == '\n' && prev == '\n')
+                       return 0;
+               gbuf_add_ch(buf, ch);
+               prev = ch;
+       }
+}
+
+static int http_parse_response(char *str, struct http_get *hg)
+{
+       /* str is 0 terminated buffer of lines
+        * every line ends with '\n'
+        * no carriage returns
+        * no empty lines
+        */
+       GROWING_KEYVALS(h);
+       char *end;
+
+       if (strncmp(str, "HTTP/", 5) == 0) {
+               str += 5;
+               while (*str != ' ') {
+                       if (*str == '\n') {
+                               return -2;
+                       }
+                       str++;
+               }
+       } else if (strncmp(str, "ICY", 3) == 0) {
+               str += 3;
+       } else {
+               return -2;
+       }
+       while (*str == ' ')
+               str++;
+
+       hg->code = 0;
+       while (*str >= '0' && *str <= '9') {
+               hg->code *= 10;
+               hg->code += *str - '0';
+               str++;
+       }
+       if (!hg->code)
+               return -2;
+       while (*str == ' ')
+               str++;
+
+       end = strchr(str, '\n');
+       hg->reason = xstrndup(str, end - str);
+       str = end + 1;
+
+       /* headers */
+       while (*str) {
+               char *ptr;
+
+               end = strchr(str, '\n');
+               ptr = strchr(str, ':');
+               if (ptr == NULL || ptr > end) {
+                       free(hg->reason);
+                       hg->reason = NULL;
+                       keyvals_terminate(&h);
+                       keyvals_free(h.keyvals);
+                       return -2;
+               }
+
+               *ptr++ = 0;
+               while (*ptr == ' ')
+                       ptr++;
+
+               keyvals_add(&h, str, xstrndup(ptr, end - ptr));
+               str = end + 1;
+       }
+       keyvals_terminate(&h);
+       hg->headers = h.keyvals;
+       return 0;
+}
+
+int http_get(struct http_get *hg, struct keyval *headers, int timeout_ms)
+{
+       GBUF(buf);
+       int i, rc, save;
+
+       gbuf_add_str(&buf, "GET ");
+       gbuf_add_str(&buf, hg->proxy ? hg->uri.uri : hg->uri.path);
+       gbuf_add_str(&buf, " HTTP/1.0\r\n");
+       for (i = 0; headers[i].key; i++) {
+               gbuf_add_str(&buf, headers[i].key);
+               gbuf_add_str(&buf, ": ");
+               gbuf_add_str(&buf, headers[i].val);
+               gbuf_add_str(&buf, "\r\n");
+       }
+       gbuf_add_str(&buf, "\r\n");
+
+       rc = http_write(hg->fd, buf.buffer, buf.len, timeout_ms);
+       if (rc)
+               goto out;
+
+       gbuf_clear(&buf);
+       rc = http_read_response(hg->fd, &buf, timeout_ms);
+       if (rc)
+               goto out;
+
+       rc = http_parse_response(buf.buffer, hg);
+out:
+       save = errno;
+       gbuf_free(&buf);
+       errno = save;
+       return rc;
+}
+
+char *http_read_body(int fd, size_t *size, int timeout_ms)
+{
+       GBUF(buf);
+
+       if (read_timeout(fd, timeout_ms))
+               return NULL;
+       while (1) {
+               int count = 1023;
+               int rc;
+
+               gbuf_grow(&buf, count);
+               rc = read_all(fd, buf.buffer + buf.len, count);
+               if (rc == -1) {
+                       gbuf_free(&buf);
+                       return NULL;
+               }
+               buf.len += rc;
+               if (rc == 0) {
+                       *size = buf.len;
+                       return gbuf_steal(&buf);
+               }
+       }
+}
+
+void http_get_free(struct http_get *hg)
+{
+       http_free_uri(&hg->uri);
+       if (hg->proxy) {
+               http_free_uri(hg->proxy);
+               free(hg->proxy);
+       }
+       if (hg->headers)
+               keyvals_free(hg->headers);
+       free(hg->reason);
+}
+
+char *base64_encode(const char *str)
+{
+       static const char t[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+       int str_len, buf_len, i, s, d;
+       char *buf;
+       unsigned char b0, b1, b2;
+
+       str_len = strlen(str);
+       buf_len = (str_len + 2) / 3 * 4 + 1;
+       buf = xnew(char, buf_len);
+       s = 0;
+       d = 0;
+       for (i = 0; i < str_len / 3; i++) {
+               b0 = str[s++];
+               b1 = str[s++];
+               b2 = str[s++];
+
+               /* 6 ms bits of b0 */
+               buf[d++] = t[b0 >> 2];
+
+               /* 2 ls bits of b0 . 4 ms bits of b1 */
+               buf[d++] = t[((b0 << 4) | (b1 >> 4)) & 0x3f];
+
+               /* 4 ls bits of b1 . 2 ms bits of b2 */
+               buf[d++] = t[((b1 << 2) | (b2 >> 6)) & 0x3f];
+
+               /* 6 ls bits of b2 */
+               buf[d++] = t[b2 & 0x3f];
+       }
+       switch (str_len % 3) {
+       case 2:
+               b0 = str[s++];
+               b1 = str[s++];
+
+               /* 6 ms bits of b0 */
+               buf[d++] = t[b0 >> 2];
+
+               /* 2 ls bits of b0 . 4 ms bits of b1 */
+               buf[d++] = t[((b0 << 4) | (b1 >> 4)) & 0x3f];
+
+               /* 4 ls bits of b1 */
+               buf[d++] = t[(b1 << 2) & 0x3f];
+
+               buf[d++] = '=';
+               break;
+       case 1:
+               b0 = str[s++];
+
+               /* 6 ms bits of b0 */
+               buf[d++] = t[b0 >> 2];
+
+               /* 2 ls bits of b0 */
+               buf[d++] = t[(b0 << 4) & 0x3f];
+
+               buf[d++] = '=';
+               buf[d++] = '=';
+               break;
+       case 0:
+               break;
+       }
+       buf[d] = 0;
+       return buf;
+}
diff --git a/http.h b/http.h
new file mode 100644 (file)
index 0000000..35d07f0
--- /dev/null
+++ b/http.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_HTTP_H
+#define CMUS_HTTP_H
+
+#include "keyval.h"
+
+#include <stddef.h> /* size_t */
+
+/*
+ * 1xx indicates an informational message only
+ * 2xx indicates success of some kind
+ * 3xx redirects the client to another URL
+ * 4xx indicates an error on the client's part
+ * 5xx indicates an error on the server's part
+ */
+
+struct http_uri {
+       char *uri;
+       char *user;
+       char *pass;
+       char *host;
+       char *path;
+       int port;
+};
+
+struct http_get {
+       struct http_uri uri;
+       struct http_uri *proxy;
+       int fd;
+       struct keyval *headers;
+       char *reason;
+       int code;
+};
+
+int http_parse_uri(const char *uri, struct http_uri *u);
+
+/* frees contents of @u, not @u itself */
+void http_free_uri(struct http_uri *u);
+
+int http_open(struct http_get *hg, int timeout_ms);
+
+/*
+ * returns:  0 success
+ *          -1 check errno
+ *          -2 parse error
+ */
+int http_get(struct http_get *hg, struct keyval *headers, int timeout_ms);
+void http_get_free(struct http_get *hg);
+
+char *http_read_body(int fd, size_t *size, int timeout_ms);
+char *base64_encode(const char *str);
+
+#endif
diff --git a/id3.c b/id3.c
new file mode 100644 (file)
index 0000000..d505ba9
--- /dev/null
+++ b/id3.c
@@ -0,0 +1,1294 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "id3.h"
+#include "xmalloc.h"
+#include "convert.h"
+#include "uchar.h"
+#include "options.h"
+#include "debug.h"
+#include "utils.h"
+#include "file.h"
+
+#include <unistd.h>
+#include <stdint.h>
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <limits.h>
+
+enum {
+       ID3_ENCODING_ISO_8859_1 = 0x00,
+       ID3_ENCODING_UTF_16     = 0x01,
+       ID3_ENCODING_UTF_16_BE  = 0x02,
+       ID3_ENCODING_UTF_8      = 0x03,
+
+       ID3_ENCODING_MAX        = 0x03
+};
+
+/*
+ * position:
+ *
+ *    0 "ID3"
+ *  -10 "3DI"
+ * -128 "TAG"
+ * -138 "3DI"
+ *
+ * if v2 is at beginning _and_ at end then there must be a seek tag at beginning
+ */
+
+struct v2_header {
+       unsigned char ver_major;
+       unsigned char ver_minor;
+       unsigned char flags;
+       uint32_t size;
+};
+
+struct v2_extended_header {
+       uint32_t size;
+};
+
+struct v2_frame_header {
+       char id[4];
+       uint32_t size;
+       uint16_t flags;
+};
+
+#define V2_HEADER_UNSYNC       (1 << 7)
+#define V2_HEADER_EXTENDED     (1 << 6)
+#define V2_HEADER_EXPERIMENTAL (1 << 5)
+#define V2_HEADER_FOOTER       (1 << 4)
+
+#define V2_FRAME_COMPRESSED    (1 << 3) /* great idea!!1 */
+#define V2_FRAME_ENCRYPTHED    (1 << 2) /* wow, this is very neat! */
+#define V2_FRAME_UNSYNC                (1 << 1)
+#define V2_FRAME_LEN_INDICATOR (1 << 0)
+
+#define NR_GENRES 148
+/* genres {{{ */
+static const char *genres[NR_GENRES] = {
+       "Blues",
+       "Classic Rock",
+       "Country",
+       "Dance",
+       "Disco",
+       "Funk",
+       "Grunge",
+       "Hip-Hop",
+       "Jazz",
+       "Metal",
+       "New Age",
+       "Oldies",
+       "Other",
+       "Pop",
+       "R&B",
+       "Rap",
+       "Reggae",
+       "Rock",
+       "Techno",
+       "Industrial",
+       "Alternative",
+       "Ska",
+       "Death Metal",
+       "Pranks",
+       "Soundtrack",
+       "Euro-Techno",
+       "Ambient",
+       "Trip-Hop",
+       "Vocal",
+       "Jazz+Funk",
+       "Fusion",
+       "Trance",
+       "Classical",
+       "Instrumental",
+       "Acid",
+       "House",
+       "Game",
+       "Sound Clip",
+       "Gospel",
+       "Noise",
+       "Alt",
+       "Bass",
+       "Soul",
+       "Punk",
+       "Space",
+       "Meditative",
+       "Instrumental Pop",
+       "Instrumental Rock",
+       "Ethnic",
+       "Gothic",
+       "Darkwave",
+       "Techno-Industrial",
+       "Electronic",
+       "Pop-Folk",
+       "Eurodance",
+       "Dream",
+       "Southern Rock",
+       "Comedy",
+       "Cult",
+       "Gangsta Rap",
+       "Top 40",
+       "Christian Rap",
+       "Pop/Funk",
+       "Jungle",
+       "Native American",
+       "Cabaret",
+       "New Wave",
+       "Psychedelic",
+       "Rave",
+       "Showtunes",
+       "Trailer",
+       "Lo-Fi",
+       "Tribal",
+       "Acid Punk",
+       "Acid Jazz",
+       "Polka",
+       "Retro",
+       "Musical",
+       "Rock & Roll",
+       "Hard Rock",
+       "Folk",
+       "Folk/Rock",
+       "National Folk",
+       "Swing",
+       "Fast-Fusion",
+       "Bebob",
+       "Latin",
+       "Revival",
+       "Celtic",
+       "Bluegrass",
+       "Avantgarde",
+       "Gothic Rock",
+       "Progressive Rock",
+       "Psychedelic Rock",
+       "Symphonic Rock",
+       "Slow Rock",
+       "Big Band",
+       "Chorus",
+       "Easy Listening",
+       "Acoustic",
+       "Humour",
+       "Speech",
+       "Chanson",
+       "Opera",
+       "Chamber Music",
+       "Sonata",
+       "Symphony",
+       "Booty Bass",
+       "Primus",
+       "Porn Groove",
+       "Satire",
+       "Slow Jam",
+       "Club",
+       "Tango",
+       "Samba",
+       "Folklore",
+       "Ballad",
+       "Power Ballad",
+       "Rhythmic Soul",
+       "Freestyle",
+       "Duet",
+       "Punk Rock",
+       "Drum Solo",
+       "A Cappella",
+       "Euro-House",
+       "Dance Hall",
+       "Goa",
+       "Drum & Bass",
+       "Club-House",
+       "Hardcore",
+       "Terror",
+       "Indie",
+       "BritPop",
+       "Negerpunk",
+       "Polsk Punk",
+       "Beat",
+       "Christian Gangsta Rap",
+       "Heavy Metal",
+       "Black Metal",
+       "Crossover",
+       "Contemporary Christian",
+       "Christian Rock",
+       "Merengue",
+       "Salsa",
+       "Thrash Metal",
+       "Anime",
+       "JPop",
+       "Synthpop"
+};
+/* }}} */
+
+#define id3_debug(...) d_print(__VA_ARGS__)
+
+const char * const id3_key_names[NUM_ID3_KEYS] = {
+       "artist",
+       "album",
+       "title",
+       "date",
+       "originaldate",
+       "genre",
+       "discnumber",
+       "tracknumber",
+       "albumartist",
+       "artistsort",
+       "albumartistsort",
+       "albumsort",
+       "compilation",
+       "replaygain_track_gain",
+       "replaygain_track_peak",
+       "replaygain_album_gain",
+       "replaygain_album_peak",
+       "composer",
+       "conductor",
+       "lyricist",
+       "remixer",
+       "label",
+       "publisher",
+       "subtitle",
+       "comment",
+       "musicbrainz_trackid",
+       "media",
+       "bpm",
+};
+
+static int utf16_is_lsurrogate(uchar uch)
+{
+       return 0xdc00 <= uch && 0xdfff >= uch;
+}
+
+static int utf16_is_hsurrogate(uchar uch)
+{
+       return 0xd800 <= uch && 0xdbff >= uch;
+}
+
+static int utf16_is_bom(uchar uch)
+{
+       return uch == 0xfeff;
+}
+
+static int utf16_is_special(uchar uch)
+{
+       return utf16_is_hsurrogate(uch) || utf16_is_lsurrogate(uch) || utf16_is_bom(uch);
+}
+
+static char *utf16_to_utf8(const unsigned char *buf, int buf_size)
+{
+       char *out;
+       int i, idx;
+       int little_endian = 0;
+
+       if (buf_size < 2)
+               return NULL;
+
+       if (buf[0] == 0xff && buf[1] == 0xfe)
+               little_endian = 1;
+
+       out = xnew(char, (buf_size / 2) * 4 + 1);
+       i = idx = 0;
+
+       while (buf_size - i >= 2) {
+               uchar u;
+
+               if (little_endian)
+                       u = buf[i] + (buf[i + 1] << 8);
+               else
+                       u = buf[i + 1] + (buf[i] << 8);
+
+               if (u_is_unicode(u)) {
+                       if (!utf16_is_special(u))
+                               u_set_char(out, &idx, u);
+               } else {
+                       free(out);
+                       return NULL;
+               }
+
+               if (u == 0)
+                       return out;
+
+               i += 2;
+       }
+
+       u_set_char(out, &idx, 0);
+       return out;
+}
+
+static int is_v1(const char *buf)
+{
+       return buf[0] == 'T' && buf[1] == 'A' && buf[2] == 'G';
+}
+
+static int u32_unsync(const unsigned char *buf, uint32_t *up)
+{
+       uint32_t b, u = 0;
+       int i;
+
+       for (i = 0; i < 4; i++) {
+               b = buf[i];
+               if (b >= 0x80)
+                       return 0;
+               u <<= 7;
+               u |= b;
+       }
+       *up = u;
+       return 1;
+}
+
+static void get_u32(const unsigned char *buf, uint32_t *up)
+{
+       uint32_t b, u = 0;
+       int i;
+
+       for (i = 0; i < 4; i++) {
+               b = buf[i];
+               u <<= 8;
+               u |= b;
+       }
+       *up = u;
+}
+
+static void get_u24(const unsigned char *buf, uint32_t *up)
+{
+       uint32_t b, u = 0;
+       int i;
+
+       for (i = 0; i < 3; i++) {
+               b = buf[i];
+               u <<= 8;
+               u |= b;
+       }
+       *up = u;
+}
+
+static void get_i16(const unsigned char *buf, int16_t *ip)
+{
+       uint16_t b, u = 0;
+       int i;
+
+       for (i = 0; i < 2; i++) {
+               b = buf[i];
+               u <<= 8;
+               u |= b;
+       }
+       *ip = u;
+}
+
+static int v2_header_footer_parse(struct v2_header *header, const char *buf)
+{
+       const unsigned char *b = (const unsigned char *)buf;
+
+       header->ver_major = b[3];
+       header->ver_minor = b[4];
+       header->flags = b[5];
+       if (header->ver_major == 0xff || header->ver_minor == 0xff)
+               return 0;
+       return u32_unsync(b + 6, &header->size);
+}
+
+static int v2_header_parse(struct v2_header *header, const char *buf)
+{
+       if (buf[0] != 'I' || buf[1] != 'D' || buf[2] != '3')
+               return 0;
+       return v2_header_footer_parse(header, buf);
+}
+
+static int v2_footer_parse(struct v2_header *header, const char *buf)
+{
+       if (buf[0] != '3' || buf[1] != 'D' || buf[2] != 'I')
+               return 0;
+       return v2_header_footer_parse(header, buf);
+}
+
+static int v2_extended_header_parse(struct v2_extended_header *header, const char *buf)
+{
+       return u32_unsync((const unsigned char *)buf, &header->size);
+}
+
+static int is_frame_id_char(char ch)
+{
+       return (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9');
+}
+
+/* XXXYYY
+ *
+ * X = [A-Z0-9]
+ * Y = byte
+ *
+ * XXX is frame
+ * YYY is frame size excluding this 6 byte header
+ */
+static int v2_2_0_frame_header_parse(struct v2_frame_header *header, const char *buf)
+{
+       int i;
+
+       for (i = 0; i < 3; i++) {
+               if (!is_frame_id_char(buf[i]))
+                       return 0;
+               header->id[i] = buf[i];
+       }
+       header->id[3] = 0;
+       get_u24((const unsigned char *)(buf + 3), &header->size);
+       header->flags = 0;
+       if (header->size == 0)
+               return 0;
+       id3_debug("%c%c%c %d\n", header->id[0], header->id[1], header->id[2], header->size);
+       return 1;
+}
+
+/* XXXXYYYYZZ
+ *
+ * X = [A-Z0-9]
+ * Y = byte
+ * Z = byte
+ *
+ * XXXX is frame
+ * YYYY is frame size excluding this 10 byte header
+ * ZZ   is flags
+ */
+static int v2_3_0_frame_header_parse(struct v2_frame_header *header, const char *buf)
+{
+       int i;
+
+       for (i = 0; i < 4; i++) {
+               if (!(is_frame_id_char(buf[i]) || (i == 3 && buf[i] == '\0')))
+                       return 0;
+               header->id[i] = buf[i];
+       }
+       get_u32((const unsigned char *)(buf + 4), &header->size);
+       header->flags = (buf[8] << 8) | buf[9];
+       if (header->size == 0)
+               return 0;
+       id3_debug("%c%c%c%c %d\n", header->id[0], header->id[1], header->id[2],
+                       header->id[3], header->size);
+       return 1;
+}
+
+/* same as 2.3 but header size is sync safe */
+static int v2_4_0_frame_header_parse(struct v2_frame_header *header, const char *buf)
+{
+       int i;
+
+       for (i = 0; i < 4; i++) {
+               if (!(is_frame_id_char(buf[i]) || (i == 3 && buf[i] == '\0')))
+                       return 0;
+               header->id[i] = buf[i];
+       }
+       if (!u32_unsync((const unsigned char *)(buf + 4), &header->size))
+               return 0;
+       header->flags = (buf[8] << 8) | buf[9];
+       if (header->size == 0)
+               return 0;
+       id3_debug("%c%c%c%c %d\n", header->id[0], header->id[1], header->id[2],
+                       header->id[3], header->size);
+       return 1;
+}
+
+static char *parse_genre(const char *str)
+{
+       int parenthesis = 0;
+       long int idx;
+       char *end;
+
+       if (strncasecmp(str, "(RX", 3) == 0)
+               return xstrdup("Remix");
+
+       if (strncasecmp(str, "(CR", 3) == 0)
+               return xstrdup("Cover");
+
+       if (*str == '(') {
+               parenthesis = 1;
+               str++;
+       }
+
+       idx = strtol(str, &end, 10);
+       if (str != end) {
+               /* Number parsed but there may be some crap after the number.
+                * I don't care, ID3v2 by definition contains crap.
+                */
+               if (idx >= 0 && idx < NR_GENRES)
+                       return xstrdup(genres[idx]);
+       }
+
+       if (parenthesis) {
+               const char *ptr = strchr(str, ')');
+
+               if (ptr && ptr[1]) {
+                       /* genre name after random crap in parenthesis,
+                        * return the genre name */
+                       return xstrdup(ptr + 1);
+               }
+               str--;
+       }
+
+       /* random crap, just return it and wait for a bug report */
+       return xstrdup(str);
+}
+
+/* http://www.id3.org/id3v2.4.0-structure.txt */
+static struct {
+       const char name[8];
+       enum id3_key key;
+} frame_tab[] = {
+       /* 2.4.0 */
+       { "TDRC", ID3_DATE }, // recording date
+       { "TDRL", ID3_DATE }, // release date
+       { "TDOR", ID3_ORIGINALDATE }, // original release date
+       { "TSOP", ID3_ARTISTSORT },
+       { "TSOA", ID3_ALBUMSORT },
+
+       /* >= 2.3.0 */
+       { "TPE1", ID3_ARTIST },
+       { "TALB", ID3_ALBUM },
+       { "TIT2", ID3_TITLE },
+       { "TYER", ID3_DATE },
+       { "TCON", ID3_GENRE },
+       { "TPOS", ID3_DISC },
+       { "TRCK", ID3_TRACK },
+       { "TPE2", ID3_ALBUMARTIST },
+       { "TSO2", ID3_ALBUMARTISTSORT },
+       { "XSOP", ID3_ARTISTSORT }, // obsolete
+       { "XSOA", ID3_ALBUMSORT }, // obsolete
+       { "TCMP", ID3_COMPILATION },
+       { "TORY", ID3_ORIGINALDATE },
+       { "TCOM", ID3_COMPOSER },
+       { "TPE3", ID3_CONDUCTOR },
+       { "TEXT", ID3_LYRICIST },
+       { "TPE4", ID3_REMIXER },
+       { "TPUB", ID3_PUBLISHER }, // TPUB can be both publisher or label
+       { "TIT3", ID3_SUBTITLE },
+       { "TMED", ID3_MEDIA },
+       { "TBPM", ID3_BPM},
+
+       /* obsolete frames (2.2.0) */
+       { "TP1",  ID3_ARTIST },
+       { "TP2",  ID3_ALBUMARTIST },
+       { "TAL",  ID3_ALBUM },
+       { "TT2",  ID3_TITLE },
+       { "TYE",  ID3_DATE },
+       { "TCO",  ID3_GENRE },
+       { "TPA",  ID3_DISC },
+       { "TRK",  ID3_TRACK },
+       { "TSP",  ID3_ARTISTSORT },
+       { "TS2",  ID3_ALBUMARTISTSORT },
+       { "TSA",  ID3_ALBUMSORT },
+       { "TCP",  ID3_COMPILATION },
+       { "TBP",  ID3_BPM },
+};
+
+static int frame_tab_index(const char *id)
+{
+       int i = 0;
+
+       while (i < N_ELEMENTS(frame_tab)) {
+               if (!strncmp(id, frame_tab[i].name, 4))
+                       return i;
+               i++;
+       }
+       return -1;
+}
+
+static int check_date_format(const char *buf)
+{
+       int i, ch;
+
+       /* year */
+       for (i = 0; i < 4; i++) {
+               ch = *buf++;
+               if (ch < '0' || ch > '9')
+                       return 0;
+       }
+       ch = *buf++;
+       if (!ch)
+               return 4;
+       if (ch != '-')
+               return 0;
+
+       /* month */
+       for (i = 0; i < 2; i++) {
+               ch = *buf++;
+               if (ch < '0' || ch > '9')
+                       return 0;
+       }
+       ch = *buf++;
+       if (!ch)
+               return 7;
+       if (ch != '-')
+               return 0;
+
+       /* day */
+       for (i = 0; i < 2; i++) {
+               ch = *buf++;
+               if (ch < '0' || ch > '9')
+                       return 0;
+       }
+       ch = *buf;
+       if (!ch || (ch >= '0' && ch <= '9'))
+               return 10;
+       return 0;
+}
+
+static void fix_date(char *buf)
+{
+       const char *ptr = buf;
+       int ch, len = 0;
+
+       do {
+               ch = *ptr++;
+               if (ch >= '0' && ch <= '9') {
+                       len++;
+                       continue;
+               }
+               if (len == 4) {
+                       // number which length is 4, must be year
+                       memmove(buf, ptr - 5, 4);
+                       buf[4] = 0;
+                       return;
+               }
+               len = 0;
+       } while (ch);
+       *buf = 0;
+}
+
+static char *decode_str(const char *buf, int len, int encoding)
+{
+       char *in, *out = NULL;
+
+       switch (encoding) {
+       case ID3_ENCODING_ISO_8859_1:
+               in = xstrndup(buf, len);
+               utf8_encode(in, id3_default_charset, &out);
+               free(in);
+               break;
+       case ID3_ENCODING_UTF_8:
+               in = xstrndup(buf, len);
+               if (u_is_valid(in)) {
+                       out = in;
+               } else {
+                       utf8_encode(in, id3_default_charset, &out);
+                       free(in);
+               }
+               break;
+       case ID3_ENCODING_UTF_16:
+       case ID3_ENCODING_UTF_16_BE:
+               out = utf16_to_utf8((const unsigned char *)buf, len);
+               break;
+       }
+       return out;
+}
+
+static void add_v2(struct id3tag *id3, enum id3_key key, char *value)
+{
+       free(id3->v2[key]);
+       id3->v2[key] = value;
+       id3->has_v2 = 1;
+}
+
+static void decode_normal(struct id3tag *id3, const char *buf, int len, int encoding, enum id3_key key)
+{
+       char *out = decode_str(buf, len, encoding);
+
+       if (!out)
+               return;
+
+       if (key == ID3_GENRE) {
+               char *tmp;
+
+               id3_debug("genre before: '%s'\n", out);
+               tmp = parse_genre(out);
+               free(out);
+               out = tmp;
+       } else if (key == ID3_DATE || key == ID3_ORIGINALDATE) {
+               int date_len = check_date_format(out);
+               id3_debug("date before: '%s'\n", out);
+               if (date_len)
+                       out[date_len] = '\0';
+               else
+                       fix_date(out);
+               if (!*out) {
+                       id3_debug("date parsing failed\n");
+                       free(out);
+                       return;
+               }
+       } else if (key == ID3_ALBUMARTIST) {
+               /*
+                * This must be TPE2 frame; ignore it if ID3_ALBUMARTIST is
+                * already present
+                */
+               if (id3->v2[key]) {
+                       free(out);
+                       return;
+               }
+       } else if (key == ID3_PUBLISHER) {
+                add_v2(id3, ID3_LABEL, strdup(out));
+       }
+
+       add_v2(id3, key, out);
+}
+
+static size_t id3_skiplen(const char *buf, size_t len, int encoding)
+{
+       if (encoding == ID3_ENCODING_ISO_8859_1 || encoding == ID3_ENCODING_UTF_8) {
+               return strlen(buf) + 1;
+       } else {
+               int i = 0;
+               while (i + 1 < len) {
+                       if (buf[i] == '\0' && buf[i + 1] == '\0')
+                               return i + 2;
+
+                       /* Assume every character is exactly 2 bytes */
+                       i += 2;
+               }
+
+               return len;
+       }
+}
+
+static void decode_txxx(struct id3tag *id3, const char *buf, int len, int encoding)
+{
+       const char ql_prefix[] = "QuodLibet::";
+       enum id3_key key = NUM_ID3_KEYS;
+       int size;
+       char *out, *out_mem;
+
+       out = decode_str(buf, len, encoding);
+       if (!out)
+               return;
+
+       id3_debug("TXXX, key = '%s'\n", out);
+
+       out_mem = out;
+
+       /* skip braindead QuodLibet TXXX frame prefix */
+       if (!strncmp(out, ql_prefix, sizeof(ql_prefix) - 1))
+               out += sizeof(ql_prefix) - 1;
+
+       if (!strcasecmp(out, "replaygain_track_gain"))
+               key = ID3_RG_TRACK_GAIN;
+       else if (!strcasecmp(out, "replaygain_track_peak"))
+               key = ID3_RG_TRACK_PEAK;
+       else if (!strcasecmp(out, "replaygain_album_gain"))
+               key = ID3_RG_ALBUM_GAIN;
+       else if (!strcasecmp(out, "replaygain_album_peak"))
+               key = ID3_RG_ALBUM_PEAK;
+       else if (!strcasecmp(out, "album artist"))
+               key = ID3_ALBUMARTIST;
+       else if (!strcasecmp(out, "albumartist"))
+               key = ID3_ALBUMARTIST;
+       else if (!strcasecmp(out, "albumartistsort"))
+               key = ID3_ALBUMARTISTSORT;
+       else if (!strcasecmp(out, "albumsort"))
+               key = ID3_ALBUMSORT;
+       else if (!strcasecmp(out, "compilation"))
+               key = ID3_COMPILATION;
+
+       size = id3_skiplen(buf, len, encoding);
+       free(out_mem);
+
+       if (key == NUM_ID3_KEYS)
+               return;
+
+       buf += size;
+       len -= size;
+       if (len <= 0)
+               return;
+
+       out = decode_str(buf, len, encoding);
+       if (!out)
+               return;
+
+       add_v2(id3, key, out);
+}
+
+static void decode_comment(struct id3tag *id3, const char *buf, int len, int encoding)
+{
+       int slen;
+       char *out;
+       int valid_description;
+
+       if (len <= 3)
+               return;
+
+       /* skip language */
+       buf += 3;
+       len -= 3;
+
+       /* "Short content description" part of COMM frame */
+       out = decode_str(buf, len, encoding);
+       if (!out)
+               return;
+
+       valid_description = strcmp(out, "") == 0 || strcmp(out, "description") == 0;
+       free(out);
+
+       if (!valid_description)
+               return;
+
+       slen = id3_skiplen(buf, len, encoding);
+       if (slen >= len)
+               return;
+
+       buf += slen;
+       len -= slen;
+
+       out = decode_str(buf, len, encoding);
+       if (!out)
+               return;
+
+       add_v2(id3, ID3_COMMENT, out);
+}
+
+/*
+ * From http://id3.org/id3v2.4.0-frames:
+ *
+ * The volume adjustment is encoded as a fixed point decibel value, 16 bit signed
+ * integer representing (adjustment*512), giving +/- 64 dB with a precision of
+ * 0.001953125 dB. E.g. +2 dB is stored as $04 00 and -2 dB is $FC 00. There may
+ * be more than one "RVA2" frame in each tag, but only one with the same
+ * identification string.
+ *
+ *     <Header for 'Relative volume adjustment (2)', ID: "RVA2">
+ *     Identification          <text string> $00
+ *
+ * The 'identification' string is used to identify the situation and/or device
+ * where this adjustment should apply. The following is then repeated for every
+ * channel
+ *
+ *     Type of channel         $xx
+ *     Volume adjustment       $xx xx
+ *     Bits representing peak  $xx
+ *     Peak volume             $xx (xx ...)
+ *
+ * Type of channel:    $00 Other
+ *                     $01 Master volume
+ *                     $02 Front right
+ *                     $03 Front left
+ *                     $04 Back right
+ *                     $05 Back left
+ *                     $06 Front centre
+ *                     $07 Back centre
+ *                     $08 Subwoofer
+ *
+ * Bits representing peak can be any number between 0 and 255. 0 means that there
+ * is no peak volume field. The peak volume field is always padded to whole
+ * bytes, setting the most significant bits to zero.
+ */
+static void decode_rva2(struct id3tag *id3, const char *buf, int len)
+{
+       const int rva2_min_len  = 6 + 1 + 2 + 1;
+
+       int audiophile_rg       = 0;
+       int channel             = 0;
+       int16_t volume_adj      = 0;
+       int peak_bits           = 0;
+       int peak_bytes          = 0;
+       int peak_shift          = 0;
+       uint32_t peak           = 0;
+
+       char *gain_str          = NULL;
+       char *peak_str          = NULL;
+
+       int i;
+
+       if (len < rva2_min_len) {
+               id3_debug("frame length %d too small\n", len);
+               return;
+       }
+
+       if (!strcasecmp(buf, "album")) {
+               audiophile_rg = 1;
+       } else if (strcasecmp(buf, "track")) {
+               id3_debug("unsupported identifier: %s\n", buf);
+               return;
+       }
+
+       buf += 6;
+
+       channel = *buf++;
+       if (channel != 0x1) {
+               id3_debug("unsupported channel: %d\n", channel);
+               return;
+       }
+
+       get_i16((unsigned char *)buf, &volume_adj);
+       buf += 2;
+
+       peak_bits = *buf++;
+
+       if (peak_bits == 0)
+               id3_debug("no peak data\n");
+
+       /*
+        * This crazy code comes from Mutagen
+        */
+       peak_bytes = min_i(4, (peak_bits + 7) >> 3);
+       peak_shift = ((8 - (peak_bits & 7)) & 7) + (4 - peak_bytes) * 8;
+
+       if (len < rva2_min_len + peak_bytes) {
+               id3_debug("peak data %d does not fit frame with length %d\n", peak_bytes, len);
+               return;
+       }
+
+       for (i = 0; i < peak_bytes; ++i) {
+               peak <<= 8;
+               peak |= (unsigned char)*buf++;
+       }
+
+       gain_str = xnew(char, 32);
+       snprintf(gain_str, 32, "%lf dB", volume_adj / 512.0);
+
+       add_v2(id3, audiophile_rg ? ID3_RG_ALBUM_GAIN : ID3_RG_TRACK_GAIN, gain_str);
+
+       if (peak_bytes) {
+               peak_str = xnew(char, 32);
+               snprintf(peak_str, 32, "%lf", ((double)peak * (1 << peak_shift)) / INT_MAX);
+
+               add_v2(id3, audiophile_rg ? ID3_RG_ALBUM_PEAK : ID3_RG_TRACK_PEAK, peak_str);
+       }
+
+       id3_debug("gain %s, peak %s\n", gain_str, peak_str ? peak_str : "none");
+}
+
+static void decode_ufid(struct id3tag *id3, const char *buf, int len)
+{
+       char *ufid;
+       int ufid_len = len - 22 - 1;
+
+       if (ufid_len < 0 || strcmp(buf, "http://musicbrainz.org") != 0)
+               return;
+
+       ufid = xnew(char, ufid_len + 1);
+       memcpy(ufid, buf + len - ufid_len, ufid_len);
+       ufid[ufid_len] = '\0';
+
+       id3_debug("%s: %s\n", buf, ufid);
+       add_v2(id3, ID3_MUSICBRAINZ_TRACKID, ufid);
+}
+
+
+static void v2_add_frame(struct id3tag *id3, struct v2_frame_header *fh, const char *buf)
+{
+       int encoding;
+       int len;
+       int idx;
+
+       if (!strncmp(fh->id, "RVA2", 4)) {
+               decode_rva2(id3, buf, fh->size);
+               return;
+       } else if (!strncmp(fh->id, "UFID", 4)) {
+               decode_ufid(id3, buf, fh->size);
+               return;
+       }
+
+       encoding = *buf++;
+       len = fh->size - 1;
+
+       if (encoding > ID3_ENCODING_MAX)
+               return;
+
+       idx = frame_tab_index(fh->id);
+       if (idx >= 0) {
+               decode_normal(id3, buf, len, encoding, frame_tab[idx].key);
+       } else if (!strncmp(fh->id, "TXXX", 4)) {
+               decode_txxx(id3, buf, len, encoding);
+       } else if (!strncmp(fh->id, "COMM", 4)) {
+               decode_comment(id3, buf, len, encoding);
+       } else if (!strncmp(fh->id, "COM", 3)) {
+               decode_comment(id3, buf, len, encoding);
+       }
+}
+
+static void unsync(unsigned char *buf, int *lenp)
+{
+       int len = *lenp;
+       int s, d;
+
+       s = d = 0;
+       while (s < len - 1) {
+               if (buf[s] == 0xff && buf[s + 1] == 0x00) {
+                       /* 0xff 0x00 -> 0xff */
+                       buf[d++] = 0xff;
+                       s += 2;
+
+                       if (s < len - 2 && buf[s] == 0x00) {
+                               /* 0xff 0x00 0x00 -> 0xff 0x00 */
+                               buf[d++] = 0x00;
+                               s++;
+                       }
+                       continue;
+               }
+               buf[d++] = buf[s++];
+       }
+       if (s < len)
+               buf[d++] = buf[s++];
+
+       d_print("unsyncronization removed %d bytes\n", s - d);
+       *lenp = d;
+}
+
+static int v2_read(struct id3tag *id3, int fd, const struct v2_header *header)
+{
+       char *buf;
+       int rc, buf_size;
+       int frame_start, i;
+       int frame_header_size;
+
+       buf_size = header->size;
+       buf = xnew(char, buf_size);
+       rc = read_all(fd, buf, buf_size);
+       if (rc == -1) {
+               free(buf);
+               return rc;
+       }
+
+       frame_start = 0;
+       if (header->flags & V2_HEADER_EXTENDED) {
+               struct v2_extended_header ext;
+
+               if (!v2_extended_header_parse(&ext, buf) || ext.size > buf_size) {
+                       id3_debug("extended header corrupted\n");
+                       free(buf);
+                       return -2;
+               }
+               frame_start = ext.size;
+               /* should check if update flag is set */
+       }
+
+       frame_header_size = 10;
+       if (header->ver_major == 2)
+               frame_header_size = 6;
+
+       i = frame_start;
+       while (i < buf_size - frame_header_size) {
+               struct v2_frame_header fh;
+               int len_unsync;
+
+               if (header->ver_major == 2) {
+                       if (!v2_2_0_frame_header_parse(&fh, buf + i))
+                               break;
+               } else if (header->ver_major == 3) {
+                       if (!v2_3_0_frame_header_parse(&fh, buf + i))
+                               break;
+               } else {
+                       /* assume v2.4 */
+                       if (!v2_4_0_frame_header_parse(&fh, buf + i))
+                               break;
+               }
+
+               i += frame_header_size;
+
+               if (fh.size > buf_size - i) {
+                       id3_debug("frame too big\n");
+                       break;
+               }
+
+               if (fh.flags & V2_FRAME_LEN_INDICATOR) {
+                       /*
+                        * Ignore the frame length 4-byte field
+                        */
+                       i       += 4;
+                       fh.size -= 4;
+               }
+
+               len_unsync = fh.size;
+
+               if ((fh.flags & V2_FRAME_UNSYNC) || (header->flags & V2_HEADER_UNSYNC))
+                       unsync((unsigned char *)(buf + i), (int *)&fh.size);
+
+               v2_add_frame(id3, &fh, buf + i);
+
+               i += len_unsync;
+       }
+
+       free(buf);
+       return 0;
+}
+
+int id3_tag_size(const char *buf, int buf_size)
+{
+       struct v2_header header;
+
+       if (buf_size < 10)
+               return 0;
+       if (v2_header_parse(&header, buf)) {
+               if (header.flags & V2_HEADER_FOOTER) {
+                       /* header + data + footer */
+                       id3_debug("v2.%d.%d with footer\n", header.ver_major, header.ver_minor);
+                       return 10 + header.size + 10;
+               }
+               /* header */
+               id3_debug("v2.%d.%d\n", header.ver_major, header.ver_minor);
+               return 10 + header.size;
+       }
+       if (buf_size >= 3 && is_v1(buf)) {
+               id3_debug("v1\n");
+               return 128;
+       }
+       return 0;
+}
+
+void id3_init(struct id3tag *id3)
+{
+       const struct id3tag t = { .has_v1 = 0, .has_v2 = 0 };
+       *id3 = t;
+}
+
+void id3_free(struct id3tag *id3)
+{
+       int i;
+
+       for (i = 0; i < NUM_ID3_KEYS; i++)
+               free(id3->v2[i]);
+}
+
+int id3_read_tags(struct id3tag *id3, int fd, unsigned int flags)
+{
+       off_t off;
+       int rc;
+
+       if (flags & ID3_V2) {
+               struct v2_header header;
+               char buf[138];
+
+               rc = read_all(fd, buf, 10);
+               if (rc == -1)
+                       goto rc_error;
+               if (v2_header_parse(&header, buf)) {
+                       rc = v2_read(id3, fd, &header);
+                       if (rc)
+                               goto rc_error;
+                       /* get v1 if needed */
+               } else {
+                       /* get v2 from end and optionally v1 */
+
+                       off = lseek(fd, -138, SEEK_END);
+                       if (off == -1)
+                               goto error;
+                       rc = read_all(fd, buf, 138);
+                       if (rc == -1)
+                               goto rc_error;
+
+                       if (is_v1(buf + 10)) {
+                               if (flags & ID3_V1) {
+                                       memcpy(id3->v1, buf + 10, 128);
+                                       id3->has_v1 = 1;
+                               }
+                               if (v2_footer_parse(&header, buf)) {
+                                       /* footer at end of file - 128 */
+                                       off = lseek(fd, -((off_t) header.size + 138), SEEK_END);
+                                       if (off == -1)
+                                               goto error;
+                                       rc = v2_read(id3, fd, &header);
+                                       if (rc)
+                                               goto rc_error;
+                               }
+                       } else if (v2_footer_parse(&header, buf + 128)) {
+                               /* footer at end of file */
+                               off = lseek(fd, -((off_t) header.size + 10), SEEK_END);
+                               if (off == -1)
+                                       goto error;
+                               rc = v2_read(id3, fd, &header);
+                               if (rc)
+                                       goto rc_error;
+                       }
+                       return 0;
+               }
+       }
+       if (flags & ID3_V1) {
+               off = lseek(fd, -128, SEEK_END);
+               if (off == -1)
+                       goto error;
+               rc = read_all(fd, id3->v1, 128);
+               if (rc == -1)
+                       goto rc_error;
+               id3->has_v1 = is_v1(id3->v1);
+       }
+       return 0;
+error:
+       rc = -1;
+rc_error:
+       return rc;
+}
+
+static char *v1_get_str(const char *buf, int len)
+{
+       char in[32];
+       char *out;
+       int i;
+
+       for (i = len - 1; i >= 0; i--) {
+               if (buf[i] != 0 && buf[i] != ' ')
+                       break;
+       }
+       if (i == -1)
+               return NULL;
+       memcpy(in, buf, i + 1);
+       in[i + 1] = 0;
+       if (u_is_valid(in))
+               return xstrdup(in);
+       if (utf8_encode(in, id3_default_charset, &out))
+               return NULL;
+       return out;
+}
+
+char *id3_get_comment(struct id3tag *id3, enum id3_key key)
+{
+       if (id3->has_v2) {
+               if (id3->v2[key])
+                       return xstrdup(id3->v2[key]);
+       }
+       if (id3->has_v1) {
+               switch (key) {
+               case ID3_ARTIST:
+                       return v1_get_str(id3->v1 + 33, 30);
+               case ID3_ALBUM:
+                       return v1_get_str(id3->v1 + 63, 30);
+               case ID3_TITLE:
+                       return v1_get_str(id3->v1 + 3, 30);
+               case ID3_DATE:
+                       return v1_get_str(id3->v1 + 93, 4);
+               case ID3_GENRE:
+                       {
+                               unsigned char idx = id3->v1[127];
+
+                               if (idx >= NR_GENRES)
+                                       return NULL;
+                               return xstrdup(genres[idx]);
+                       }
+               case ID3_TRACK:
+                       {
+                               char *t;
+
+                               if (id3->v1[125] != 0)
+                                       return NULL;
+                               t = xnew(char, 4);
+                               snprintf(t, 4, "%d", ((unsigned char *)id3->v1)[126]);
+                               return t;
+                       }
+               default:
+                       return NULL;
+               }
+       }
+       return NULL;
+}
+
+char const *id3_get_genre(uint16_t id)
+{
+       if (id >= NR_GENRES)
+               return NULL;
+       return genres[id];
+}
diff --git a/id3.h b/id3.h
new file mode 100644 (file)
index 0000000..9eb1613
--- /dev/null
+++ b/id3.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_ID3_H
+#define CMUS_ID3_H
+
+#include <stdint.h>
+
+/* flags for id3_read_tags */
+#define ID3_V1 (1 << 0)
+#define ID3_V2 (1 << 1)
+
+enum id3_key {
+       ID3_ARTIST,
+       ID3_ALBUM,
+       ID3_TITLE,
+       ID3_DATE,
+       ID3_ORIGINALDATE,
+       ID3_GENRE,
+       ID3_DISC,
+       ID3_TRACK,
+       ID3_ALBUMARTIST,
+       ID3_ARTISTSORT,
+       ID3_ALBUMARTISTSORT,
+       ID3_ALBUMSORT,
+       ID3_COMPILATION,
+       ID3_RG_TRACK_GAIN,
+       ID3_RG_TRACK_PEAK,
+       ID3_RG_ALBUM_GAIN,
+       ID3_RG_ALBUM_PEAK,
+       ID3_COMPOSER,
+       ID3_CONDUCTOR,
+       ID3_LYRICIST,
+       ID3_REMIXER,
+       ID3_LABEL,
+       ID3_PUBLISHER,
+       ID3_SUBTITLE,
+       ID3_COMMENT,
+       ID3_MUSICBRAINZ_TRACKID,
+       ID3_MEDIA,
+       ID3_BPM,
+
+       NUM_ID3_KEYS
+};
+
+struct id3tag {
+       char v1[128];
+       char *v2[NUM_ID3_KEYS];
+
+       unsigned int has_v1 : 1;
+       unsigned int has_v2 : 1;
+};
+
+extern const char * const id3_key_names[NUM_ID3_KEYS];
+
+int id3_tag_size(const char *buf, int buf_size);
+
+void id3_init(struct id3tag *id3);
+void id3_free(struct id3tag *id3);
+
+int id3_read_tags(struct id3tag *id3, int fd, unsigned int flags);
+char *id3_get_comment(struct id3tag *id3, enum id3_key key);
+
+char const *id3_get_genre(uint16_t id);
+
+#endif
diff --git a/input.c b/input.c
new file mode 100644 (file)
index 0000000..e544e17
--- /dev/null
+++ b/input.c
@@ -0,0 +1,1049 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "convert.h"
+#include "input.h"
+#include "ip.h"
+#include "pcm.h"
+#include "http.h"
+#include "xmalloc.h"
+#include "file.h"
+#include "path.h"
+#include "utils.h"
+#include "cmus.h"
+#include "options.h"
+#include "list.h"
+#include "mergesort.h"
+#include "misc.h"
+#include "debug.h"
+#include "ui_curses.h"
+#include "locking.h"
+#include "xstrjoin.h"
+
+#include <unistd.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <sys/select.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <dirent.h>
+#include <dlfcn.h>
+#include <strings.h>
+
+struct input_plugin {
+       const struct input_plugin_ops *ops;
+       struct input_plugin_data data;
+       unsigned int open : 1;
+       unsigned int eof : 1;
+       int http_code;
+       char *http_reason;
+
+       /* cached duration, -1 = unset */
+       int duration;
+       /* cached bitrate, -1 = unset */
+       long bitrate;
+       /* cached codec, NULL = unset */
+       char *codec;
+       /* cached codec_profile, NULL = unset */
+       char *codec_profile;
+
+       /*
+        * pcm is converted to 16-bit signed little-endian stereo
+        * NOTE: no conversion is done if channels > 2 or bits > 16
+        */
+       void (*pcm_convert)(void *, const void *, int);
+       void (*pcm_convert_in_place)(void *, int);
+       /*
+        * 4  if 8-bit mono
+        * 2  if 8-bit stereo or 16-bit mono
+        * 1  otherwise
+        */
+       int pcm_convert_scale;
+};
+
+struct ip {
+       struct list_head node;
+       char *name;
+       void *handle;
+
+       int priority;
+       const char * const *extensions;
+       const char * const *mime_types;
+       const struct input_plugin_ops *ops;
+       const struct input_plugin_opt *options;
+};
+
+static const char *plugin_dir;
+static LIST_HEAD(ip_head);
+
+/* protects ip->priority and ip_head */
+static pthread_rwlock_t ip_lock = CMUS_RWLOCK_INITIALIZER;
+
+#define ip_rdlock() cmus_rwlock_rdlock(&ip_lock)
+#define ip_wrlock() cmus_rwlock_wrlock(&ip_lock)
+#define ip_unlock() cmus_rwlock_unlock(&ip_lock)
+
+/* timeouts (ms) */
+static int http_connection_timeout = 5e3;
+static int http_read_timeout = 5e3;
+
+static const char *pl_mime_types[] = {
+       "audio/m3u",
+       "audio/x-scpls",
+       "audio/x-mpegurl"
+};
+
+static const struct input_plugin_ops *
+get_ops_by_extension_locked(const char *ext, struct list_head **headp)
+{
+       struct list_head *node = *headp;
+
+       for (node = node->next; node != &ip_head; node = node->next) {
+               struct ip *ip = list_entry(node, struct ip, node);
+               const char * const *exts = ip->extensions;
+               int i;
+
+               if (ip->priority <= 0) {
+                       break;
+               }
+
+               for (i = 0; exts[i]; i++) {
+                       if (strcasecmp(ext, exts[i]) == 0 || strcmp("*", exts[i]) == 0) {
+                               *headp = node;
+                               return ip->ops;
+                       }
+               }
+       }
+       return NULL;
+}
+
+static const struct input_plugin_ops *
+get_ops_by_extension(const char *ext, struct list_head **headp)
+{
+       ip_rdlock();
+       const struct input_plugin_ops *rv = get_ops_by_extension_locked(ext,
+                       headp);
+       ip_unlock();
+       return rv;
+}
+
+static const struct input_plugin_ops *
+get_ops_by_mime_type_locked(const char *mime_type)
+{
+       struct ip *ip;
+
+       list_for_each_entry(ip, &ip_head, node) {
+               const char * const *types = ip->mime_types;
+               int i;
+
+               if (ip->priority <= 0) {
+                       break;
+               }
+
+               for (i = 0; types[i]; i++) {
+                       if (strcasecmp(mime_type, types[i]) == 0)
+                               return ip->ops;
+               }
+       }
+       return NULL;
+}
+
+static const struct input_plugin_ops *
+get_ops_by_mime_type(const char *mime_type)
+{
+       ip_rdlock();
+       const struct input_plugin_ops *rv =
+               get_ops_by_mime_type_locked(mime_type);
+       ip_unlock();
+       return rv;
+}
+
+static void keyvals_add_basic_auth(struct growing_keyvals *c,
+                                  const char *user,
+                                  const char *pass,
+                                  const char *header)
+{
+       char buf[256];
+       char *encoded;
+
+       snprintf(buf, sizeof(buf), "%s:%s", user, pass);
+       encoded = base64_encode(buf);
+       if (encoded == NULL) {
+               d_print("couldn't base64 encode '%s'\n", buf);
+       } else {
+               snprintf(buf, sizeof(buf), "Basic %s", encoded);
+               free(encoded);
+               keyvals_add(c, header, xstrdup(buf));
+       }
+}
+
+static int do_http_get(struct http_get *hg, const char *uri, int redirections)
+{
+       GROWING_KEYVALS(h);
+       int i, rc;
+       const char *val;
+       char *redirloc;
+
+       d_print("%s\n", uri);
+
+       hg->headers = NULL;
+       hg->reason = NULL;
+       hg->proxy = NULL;
+       hg->code = -1;
+       hg->fd = -1;
+       if (http_parse_uri(uri, &hg->uri))
+               return -IP_ERROR_INVALID_URI;
+
+       if (http_open(hg, http_connection_timeout))
+               return -IP_ERROR_ERRNO;
+
+       keyvals_add(&h, "Host", xstrdup(hg->uri.host));
+       if (hg->proxy && hg->proxy->user && hg->proxy->pass)
+               keyvals_add_basic_auth(&h, hg->proxy->user, hg->proxy->pass, "Proxy-Authorization");
+       keyvals_add(&h, "User-Agent", xstrdup("cmus/" VERSION));
+       keyvals_add(&h, "Icy-MetaData", xstrdup("1"));
+       if (hg->uri.user && hg->uri.pass)
+               keyvals_add_basic_auth(&h, hg->uri.user, hg->uri.pass, "Authorization");
+       keyvals_terminate(&h);
+
+       rc = http_get(hg, h.keyvals, http_read_timeout);
+       keyvals_free(h.keyvals);
+       switch (rc) {
+       case -1:
+               return -IP_ERROR_ERRNO;
+       case -2:
+               return -IP_ERROR_HTTP_RESPONSE;
+       }
+
+       d_print("HTTP response: %d %s\n", hg->code, hg->reason);
+       for (i = 0; hg->headers[i].key != NULL; i++)
+               d_print("  %s: %s\n", hg->headers[i].key, hg->headers[i].val);
+
+       switch (hg->code) {
+       case 200: /* OK */
+               return 0;
+       /*
+        * 3xx Codes (Redirections)
+        *     unhandled: 300 Multiple Choices
+        */
+       case 301: /* Moved Permanently */
+       case 302: /* Found */
+       case 303: /* See Other */
+       case 307: /* Temporary Redirect */
+               val = keyvals_get_val(hg->headers, "location");
+               if (!val)
+                       return -IP_ERROR_HTTP_RESPONSE;
+
+               redirections++;
+               if (redirections > 2)
+                       return -IP_ERROR_HTTP_REDIRECT_LIMIT;
+
+               redirloc = xstrdup(val);
+               http_get_free(hg);
+               close(hg->fd);
+
+               rc = do_http_get(hg, redirloc, redirections);
+
+               free(redirloc);
+               return rc;
+       default:
+               return -IP_ERROR_HTTP_STATUS;
+       }
+}
+
+static int setup_remote(struct input_plugin *ip, const struct keyval *headers, int sock)
+{
+       const char *val;
+
+       val = keyvals_get_val(headers, "Content-Type");
+       if (val) {
+               d_print("Content-Type: %s\n", val);
+               ip->ops = get_ops_by_mime_type(val);
+               if (ip->ops == NULL) {
+                       d_print("unsupported content type: %s\n", val);
+                       close(sock);
+                       return -IP_ERROR_FILE_FORMAT;
+               }
+       } else {
+               const char *type = "audio/mpeg";
+
+               d_print("assuming %s content type\n", type);
+               ip->ops = get_ops_by_mime_type(type);
+               if (ip->ops == NULL) {
+                       d_print("unsupported content type: %s\n", type);
+                       close(sock);
+                       return -IP_ERROR_FILE_FORMAT;
+               }
+       }
+
+       ip->data.fd = sock;
+       ip->data.metadata = xnew(char, 16 * 255 + 1);
+
+       val = keyvals_get_val(headers, "icy-metaint");
+       if (val) {
+               long int lint;
+
+               if (str_to_int(val, &lint) == 0 && lint >= 0) {
+                       ip->data.metaint = lint;
+                       d_print("metaint: %d\n", ip->data.metaint);
+               }
+       }
+
+       val = keyvals_get_val(headers, "icy-name");
+       if (val)
+               ip->data.icy_name = to_utf8(val, icecast_default_charset);
+
+       val = keyvals_get_val(headers, "icy-genre");
+       if (val)
+               ip->data.icy_genre = to_utf8(val, icecast_default_charset);
+
+       val = keyvals_get_val(headers, "icy-url");
+       if (val)
+               ip->data.icy_url = to_utf8(val, icecast_default_charset);
+
+       return 0;
+}
+
+struct read_playlist_data {
+       struct input_plugin *ip;
+       int rc;
+       int count;
+};
+
+static int handle_line(void *data, const char *uri)
+{
+       struct read_playlist_data *rpd = data;
+       struct http_get hg;
+
+       rpd->count++;
+       rpd->rc = do_http_get(&hg, uri, 0);
+       if (rpd->rc) {
+               rpd->ip->http_code = hg.code;
+               rpd->ip->http_reason = hg.reason;
+               if (hg.fd >= 0)
+                       close(hg.fd);
+
+               hg.reason = NULL;
+               http_get_free(&hg);
+               return 0;
+       }
+
+       rpd->rc = setup_remote(rpd->ip, hg.headers, hg.fd);
+       http_get_free(&hg);
+       return 1;
+}
+
+static int read_playlist(struct input_plugin *ip, int sock)
+{
+       struct read_playlist_data rpd = { ip, 0, 0 };
+       char *body;
+       size_t size;
+
+       body = http_read_body(sock, &size, http_read_timeout);
+       close(sock);
+       if (!body)
+               return -IP_ERROR_ERRNO;
+
+       cmus_playlist_for_each(body, size, 0, handle_line, &rpd);
+       free(body);
+       if (!rpd.count) {
+               d_print("empty playlist\n");
+               rpd.rc = -IP_ERROR_HTTP_RESPONSE;
+       }
+       return rpd.rc;
+}
+
+static int open_remote(struct input_plugin *ip)
+{
+       struct input_plugin_data *d = &ip->data;
+       struct http_get hg;
+       const char *val;
+       int rc;
+
+       rc = do_http_get(&hg, d->filename, 0);
+       if (rc) {
+               ip->http_code = hg.code;
+               ip->http_reason = hg.reason;
+               hg.reason = NULL;
+               http_get_free(&hg);
+               return rc;
+       }
+
+       val = keyvals_get_val(hg.headers, "Content-Type");
+       if (val) {
+               int i;
+
+               for (i = 0; i < N_ELEMENTS(pl_mime_types); i++) {
+                       if (!strcasecmp(val, pl_mime_types[i])) {
+                               d_print("Content-Type: %s\n", val);
+                               http_get_free(&hg);
+                               return read_playlist(ip, hg.fd);
+                       }
+               }
+       }
+
+       rc = setup_remote(ip, hg.headers, hg.fd);
+       http_get_free(&hg);
+       return rc;
+}
+
+static void ip_init(struct input_plugin *ip, char *filename)
+{
+       const struct input_plugin t = {
+               .http_code          = -1,
+               .pcm_convert_scale  = -1,
+               .duration           = -1,
+               .bitrate            = -1,
+               .data = {
+                       .fd         = -1,
+                       .filename   = filename,
+                       .remote     = is_http_url(filename),
+                       .channel_map = CHANNEL_MAP_INIT
+               }
+       };
+       *ip = t;
+}
+
+static void ip_reset(struct input_plugin *ip, int close_fd)
+{
+       int fd = ip->data.fd;
+       free(ip->data.metadata);
+       ip_init(ip, ip->data.filename);
+       if (fd != -1) {
+               if (close_fd)
+                       close(fd);
+               else {
+                       lseek(fd, 0, SEEK_SET);
+                       ip->data.fd = fd;
+               }
+       }
+}
+
+static int open_file_locked(struct input_plugin *ip)
+{
+       const struct input_plugin_ops *ops;
+       struct list_head *head = &ip_head;
+       const char *ext;
+       int rc = 0;
+
+       ext = get_extension(ip->data.filename);
+       if (!ext)
+               return -IP_ERROR_UNRECOGNIZED_FILE_TYPE;
+
+       ops = get_ops_by_extension(ext, &head);
+       if (!ops)
+               return -IP_ERROR_UNRECOGNIZED_FILE_TYPE;
+
+       ip->data.fd = open(ip->data.filename, O_RDONLY);
+       if (ip->data.fd == -1)
+               return -IP_ERROR_ERRNO;
+
+       while (1) {
+               ip->ops = ops;
+               rc = ip->ops->open(&ip->data);
+               if (rc != -IP_ERROR_UNSUPPORTED_FILE_TYPE)
+                       break;
+
+               ops = get_ops_by_extension(ext, &head);
+               if (!ops)
+                       break;
+
+               ip_reset(ip, 0);
+               d_print("fallback: try next plugin for `%s'\n", ip->data.filename);
+       }
+
+       return rc;
+}
+
+static int open_file(struct input_plugin *ip)
+{
+       ip_rdlock();
+       int rv = open_file_locked(ip);
+       ip_unlock();
+       return rv;
+}
+
+static int sort_ip(const struct list_head *a_, const struct list_head *b_)
+{
+       const struct ip *a = list_entry(a_, struct ip, node);
+       const struct ip *b = list_entry(b_, struct ip, node);
+       return b->priority - a->priority;
+}
+
+void ip_load_plugins(void)
+{
+       DIR *dir;
+       struct dirent *d;
+
+       plugin_dir = xstrjoin(cmus_lib_dir, "/ip");
+       dir = opendir(plugin_dir);
+       if (dir == NULL) {
+               error_msg("couldn't open directory `%s': %s", plugin_dir, strerror(errno));
+               return;
+       }
+
+       ip_wrlock();
+       while ((d = (struct dirent *) readdir(dir)) != NULL) {
+               char filename[256];
+               struct ip *ip;
+               void *so;
+               char *ext;
+               const int *priority_ptr;
+               const unsigned *abi_version_ptr;
+               bool err = false;
+
+               if (d->d_name[0] == '.')
+                       continue;
+               ext = strrchr(d->d_name, '.');
+               if (ext == NULL)
+                       continue;
+               if (strcmp(ext, ".so"))
+                       continue;
+
+               snprintf(filename, sizeof(filename), "%s/%s", plugin_dir, d->d_name);
+
+               so = dlopen(filename, RTLD_NOW);
+               if (so == NULL) {
+                       d_print("%s: %s\n", filename, dlerror());
+                       continue;
+               }
+
+               ip = xnew(struct ip, 1);
+
+               abi_version_ptr = dlsym(so, "ip_abi_version");
+               priority_ptr = dlsym(so, "ip_priority");
+               ip->extensions = dlsym(so, "ip_extensions");
+               ip->mime_types = dlsym(so, "ip_mime_types");
+               ip->ops = dlsym(so, "ip_ops");
+               ip->options = dlsym(so, "ip_options");
+               if (!priority_ptr || !ip->extensions || !ip->mime_types || !ip->ops || !ip->options) {
+                       error_msg("%s: missing symbol", filename);
+                       err = true;
+               }
+               if (!abi_version_ptr || *abi_version_ptr != IP_ABI_VERSION) {
+                       error_msg("%s: incompatible plugin version", filename);
+                       err = true;
+               }
+               if (err) {
+                       free(ip);
+                       dlclose(so);
+                       continue;
+               }
+               ip->priority = *priority_ptr;
+
+               ip->name = xstrndup(d->d_name, ext - d->d_name);
+               ip->handle = so;
+
+               list_add_tail(&ip->node, &ip_head);
+       }
+       list_mergesort(&ip_head, sort_ip);
+       closedir(dir);
+       ip_unlock();
+}
+
+struct input_plugin *ip_new(const char *filename)
+{
+       struct input_plugin *ip = xnew(struct input_plugin, 1);
+
+       ip_init(ip, xstrdup(filename));
+       return ip;
+}
+
+void ip_delete(struct input_plugin *ip)
+{
+       if (ip->open)
+               ip_close(ip);
+       free(ip->data.filename);
+       free(ip);
+}
+
+int ip_open(struct input_plugin *ip)
+{
+       int rc;
+
+       BUG_ON(ip->open);
+
+       /* set fd and ops, call ops->open */
+       if (ip->data.remote) {
+               rc = open_remote(ip);
+               if (rc == 0)
+                       rc = ip->ops->open(&ip->data);
+       } else {
+               if (is_cdda_url(ip->data.filename)) {
+                       ip->ops = get_ops_by_mime_type("x-content/audio-cdda");
+                       rc = ip->ops ? ip->ops->open(&ip->data) : 1;
+               } else if (is_cue_url(ip->data.filename)) {
+                       ip->ops = get_ops_by_mime_type("application/x-cue");
+                       rc = ip->ops ? ip->ops->open(&ip->data) : 1;
+               } else
+                       rc = open_file(ip);
+       }
+
+       if (rc) {
+               d_print("opening `%s' failed: %d %s\n", ip->data.filename, rc,
+                               rc == -1 ? strerror(errno) : "");
+               ip_reset(ip, 1);
+               return rc;
+       }
+       ip->open = 1;
+       return 0;
+}
+
+void ip_setup(struct input_plugin *ip)
+{
+       unsigned int bits, is_signed, channels;
+       sample_format_t sf = ip->data.sf;
+
+       bits = sf_get_bits(sf);
+       is_signed = sf_get_signed(sf);
+       channels = sf_get_channels(sf);
+
+       ip->pcm_convert_scale = 1;
+       ip->pcm_convert = NULL;
+       ip->pcm_convert_in_place = NULL;
+
+       if (bits <= 16 && channels <= 2) {
+               unsigned int mask = ((bits >> 2) & 4) | (is_signed << 1);
+
+               ip->pcm_convert = pcm_conv[mask | (channels - 1)];
+               ip->pcm_convert_in_place = pcm_conv_in_place[mask | sf_get_bigendian(sf)];
+
+               ip->pcm_convert_scale = (3 - channels) * (3 - bits / 8);
+       }
+
+       d_print("pcm convert: scale=%d convert=%d convert_in_place=%d\n",
+                       ip->pcm_convert_scale,
+                       ip->pcm_convert != NULL,
+                       ip->pcm_convert_in_place != NULL);
+}
+
+int ip_close(struct input_plugin *ip)
+{
+       int rc;
+
+       rc = ip->ops->close(&ip->data);
+       BUG_ON(ip->data.private);
+       if (ip->data.fd != -1)
+               close(ip->data.fd);
+       free(ip->data.metadata);
+       free(ip->data.icy_name);
+       free(ip->data.icy_genre);
+       free(ip->data.icy_url);
+       free(ip->http_reason);
+
+       ip_init(ip, ip->data.filename);
+       return rc;
+}
+
+int ip_read(struct input_plugin *ip, char *buffer, int count)
+{
+       struct timeval tv;
+       fd_set readfds;
+       /* 4608 seems to be optimal for mp3s, 4096 for oggs */
+       char tmp[8 * 1024];
+       char *buf;
+       int sample_size;
+       int rc;
+
+       BUG_ON(count <= 0);
+
+       FD_ZERO(&readfds);
+       FD_SET(ip->data.fd, &readfds);
+       /* zero timeout -> return immediately */
+       tv.tv_sec = 0;
+       tv.tv_usec = 50e3;
+       rc = select(ip->data.fd + 1, &readfds, NULL, NULL, &tv);
+       if (rc == -1) {
+               if (errno == EINTR)
+                       errno = EAGAIN;
+               return -1;
+       }
+       if (rc == 0) {
+               errno = EAGAIN;
+               return -1;
+       }
+
+       buf = buffer;
+       if (ip->pcm_convert_scale > 1) {
+               /* use tmp buffer for 16-bit mono and 8-bit */
+               buf = tmp;
+               count /= ip->pcm_convert_scale;
+               if (count > sizeof(tmp))
+                       count = sizeof(tmp);
+       }
+
+       rc = ip->ops->read(&ip->data, buf, count);
+       if (rc == -1 && (errno == EAGAIN || errno == EINTR)) {
+               errno = EAGAIN;
+               return -1;
+       }
+       if (rc <= 0) {
+               ip->eof = 1;
+               return rc;
+       }
+
+       BUG_ON(rc % sf_get_frame_size(ip->data.sf) != 0);
+
+       sample_size = sf_get_sample_size(ip->data.sf);
+       if (ip->pcm_convert_in_place != NULL)
+               ip->pcm_convert_in_place(buf, rc / sample_size);
+       if (ip->pcm_convert != NULL)
+               ip->pcm_convert(buffer, tmp, rc / sample_size);
+       return rc * ip->pcm_convert_scale;
+}
+
+int ip_seek(struct input_plugin *ip, double offset)
+{
+       int rc;
+
+       if (ip->data.remote)
+               return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+       rc = ip->ops->seek(&ip->data, offset);
+       if (rc == 0)
+               ip->eof = 0;
+       return rc;
+}
+
+int ip_read_comments(struct input_plugin *ip, struct keyval **comments)
+{
+       struct keyval *kv = NULL;
+       int rc;
+
+       rc = ip->ops->read_comments(&ip->data, &kv);
+
+       if (ip->data.remote) {
+               GROWING_KEYVALS(c);
+
+               if (kv) {
+                       keyvals_init(&c, kv);
+                       keyvals_free(kv);
+               }
+
+               if (ip->data.icy_name && !keyvals_get_val_growing(&c, "title"))
+                       keyvals_add(&c, "title", xstrdup(ip->data.icy_name));
+
+               if (ip->data.icy_genre && !keyvals_get_val_growing(&c, "genre"))
+                       keyvals_add(&c, "genre", xstrdup(ip->data.icy_genre));
+
+               if (ip->data.icy_url && !keyvals_get_val_growing(&c, "comment"))
+                       keyvals_add(&c, "comment", xstrdup(ip->data.icy_url));
+
+               keyvals_terminate(&c);
+
+               kv = c.keyvals;
+       }
+
+       *comments = kv;
+
+       return ip->data.remote ? 0 : rc;
+}
+
+int ip_duration(struct input_plugin *ip)
+{
+       if (ip->data.remote)
+               return -1;
+       if (ip->duration == -1)
+               ip->duration = ip->ops->duration(&ip->data);
+       if (ip->duration < 0)
+               return -1;
+       return ip->duration;
+}
+
+int ip_bitrate(struct input_plugin *ip)
+{
+       if (ip->data.remote)
+               return -1;
+       if (ip->bitrate == -1)
+               ip->bitrate = ip->ops->bitrate(&ip->data);
+       if (ip->bitrate < 0)
+               return -1;
+       return ip->bitrate;
+}
+
+int ip_current_bitrate(struct input_plugin *ip)
+{
+       return ip->ops->bitrate_current(&ip->data);
+}
+
+char *ip_codec(struct input_plugin *ip)
+{
+       if (ip->data.remote)
+               return NULL;
+       if (!ip->codec)
+               ip->codec = ip->ops->codec(&ip->data);
+       return ip->codec;
+}
+
+char *ip_codec_profile(struct input_plugin *ip)
+{
+       if (ip->data.remote)
+               return NULL;
+       if (!ip->codec_profile)
+               ip->codec_profile = ip->ops->codec_profile(&ip->data);
+       return ip->codec_profile;
+}
+
+sample_format_t ip_get_sf(struct input_plugin *ip)
+{
+       BUG_ON(!ip->open);
+       return ip->data.sf;
+}
+
+void ip_get_channel_map(struct input_plugin *ip, channel_position_t *channel_map)
+{
+       BUG_ON(!ip->open);
+       channel_map_copy(channel_map, ip->data.channel_map);
+}
+
+const char *ip_get_filename(struct input_plugin *ip)
+{
+       return ip->data.filename;
+}
+
+const char *ip_get_metadata(struct input_plugin *ip)
+{
+       BUG_ON(!ip->open);
+       return ip->data.metadata;
+}
+
+int ip_is_remote(struct input_plugin *ip)
+{
+       return ip->data.remote;
+}
+
+int ip_metadata_changed(struct input_plugin *ip)
+{
+       int ret = ip->data.metadata_changed;
+
+       BUG_ON(!ip->open);
+       ip->data.metadata_changed = 0;
+       return ret;
+}
+
+int ip_eof(struct input_plugin *ip)
+{
+       BUG_ON(!ip->open);
+       return ip->eof;
+}
+
+static void option_error(int rc)
+{
+       char *msg = ip_get_error_msg(NULL, rc, "setting option");
+       error_msg("%s", msg);
+       free(msg);
+}
+
+static void set_ip_option(void *data, const char *val)
+{
+       const struct input_plugin_opt *ipo = data;
+       int rc;
+
+       rc = ipo->set(val);
+       if (rc)
+               option_error(rc);
+}
+
+static void get_ip_option(void *data, char *buf, size_t size)
+{
+       const struct input_plugin_opt *ipo = data;
+       char *val = NULL;
+
+       ipo->get(&val);
+       if (val) {
+               strscpy(buf, val, size);
+               free(val);
+       }
+}
+
+static void set_ip_priority(void *data, const char *val)
+{
+       /* warn only once during the lifetime of the program. */
+       static bool warned = false;
+       long tmp;
+       struct ip *ip = data;
+
+       if (str_to_int(val, &tmp) == -1 || tmp < 0 || (long)(int)tmp != tmp) {
+               error_msg("non-negative integer expected");
+               return;
+       }
+       if (ui_initialized) {
+               if (!warned) {
+                       static const char *msg =
+                               "Metadata might become inconsistent "
+                               "after this change. Continue? [y/N]";
+                       if (!yes_no_query("%s", msg)) {
+                               info_msg("Aborted");
+                               return;
+                       }
+                       warned = true;
+               }
+               info_msg("Run \":update-cache -f\" to refresh the metadata.");
+       }
+
+       ip_wrlock();
+       ip->priority = (int)tmp;
+       list_mergesort(&ip_head, sort_ip);
+       ip_unlock();
+}
+
+static void get_ip_priority(void *data, char *val, size_t size)
+{
+       const struct ip *ip = data;
+       ip_rdlock();
+       snprintf(val, size, "%d", ip->priority);
+       ip_unlock();
+}
+
+void ip_add_options(void)
+{
+       struct ip *ip;
+       const struct input_plugin_opt *ipo;
+       char key[64];
+
+       ip_rdlock();
+       list_for_each_entry(ip, &ip_head, node) {
+               for (ipo = ip->options; ipo->name; ipo++) {
+                       snprintf(key, sizeof(key), "input.%s.%s", ip->name,
+                                       ipo->name);
+                       option_add(xstrdup(key), ipo, get_ip_option,
+                                       set_ip_option, NULL, 0);
+               }
+               snprintf(key, sizeof(key), "input.%s.priority", ip->name);
+               option_add(xstrdup(key), ip, get_ip_priority, set_ip_priority, NULL, 0);
+       }
+       ip_unlock();
+}
+
+char *ip_get_error_msg(struct input_plugin *ip, int rc, const char *arg)
+{
+       char buffer[1024];
+
+       switch (-rc) {
+       case IP_ERROR_ERRNO:
+               snprintf(buffer, sizeof(buffer), "%s: %s", arg, strerror(errno));
+               break;
+       case IP_ERROR_UNRECOGNIZED_FILE_TYPE:
+               snprintf(buffer, sizeof(buffer),
+                               "%s: unrecognized filename extension", arg);
+               break;
+       case IP_ERROR_UNSUPPORTED_FILE_TYPE:
+               snprintf(buffer, sizeof(buffer),
+                               "%s: unsupported file format", arg);
+               break;
+       case IP_ERROR_FUNCTION_NOT_SUPPORTED:
+               snprintf(buffer, sizeof(buffer),
+                               "%s: function not supported", arg);
+               break;
+       case IP_ERROR_FILE_FORMAT:
+               snprintf(buffer, sizeof(buffer),
+                               "%s: file format not supported or corrupted file",
+                               arg);
+               break;
+       case IP_ERROR_INVALID_URI:
+               snprintf(buffer, sizeof(buffer), "%s: invalid URI", arg);
+               break;
+       case IP_ERROR_SAMPLE_FORMAT:
+               snprintf(buffer, sizeof(buffer),
+                               "%s: input plugin doesn't support the sample format",
+                               arg);
+               break;
+       case IP_ERROR_WRONG_DISC:
+               snprintf(buffer, sizeof(buffer), "%s: wrong disc inserted, aborting!", arg);
+               break;
+       case IP_ERROR_NO_DISC:
+               snprintf(buffer, sizeof(buffer), "%s: could not read disc", arg);
+               break;
+       case IP_ERROR_HTTP_RESPONSE:
+               snprintf(buffer, sizeof(buffer), "%s: invalid HTTP response", arg);
+               break;
+       case IP_ERROR_HTTP_STATUS:
+               snprintf(buffer, sizeof(buffer), "%s: %d %s", arg, ip->http_code, ip->http_reason);
+               free(ip->http_reason);
+               ip->http_reason = NULL;
+               ip->http_code = -1;
+               break;
+       case IP_ERROR_HTTP_REDIRECT_LIMIT:
+               snprintf(buffer, sizeof(buffer), "%s: too many HTTP redirections", arg);
+               break;
+       case IP_ERROR_NOT_OPTION:
+               snprintf(buffer, sizeof(buffer),
+                               "%s: no such option", arg);
+               break;
+       case IP_ERROR_INTERNAL:
+               snprintf(buffer, sizeof(buffer), "%s: internal error", arg);
+               break;
+       case IP_ERROR_SUCCESS:
+       default:
+               snprintf(buffer, sizeof(buffer),
+                               "%s: this is not an error (%d), this is a bug",
+                               arg, rc);
+               break;
+       }
+       return xstrdup(buffer);
+}
+
+char **ip_get_supported_extensions(void)
+{
+       struct ip *ip;
+       char **exts;
+       int i, size;
+       int count = 0;
+
+       size = 8;
+       exts = xnew(char *, size);
+       ip_rdlock();
+       list_for_each_entry(ip, &ip_head, node) {
+               const char * const *e = ip->extensions;
+
+               for (i = 0; e[i]; i++) {
+                       if (count == size - 1) {
+                               size *= 2;
+                               exts = xrenew(char *, exts, size);
+                       }
+                       exts[count++] = xstrdup(e[i]);
+               }
+       }
+       ip_unlock();
+       exts[count] = NULL;
+       qsort(exts, count, sizeof(char *), strptrcmp);
+       return exts;
+}
+
+void ip_dump_plugins(void)
+{
+       struct ip *ip;
+       int i;
+
+       printf("Input Plugins: %s\n", plugin_dir);
+       ip_rdlock();
+       list_for_each_entry(ip, &ip_head, node) {
+               printf("  %s:\n    Priority: %d\n    File Types:", ip->name, ip->priority);
+               for (i = 0; ip->extensions[i]; i++)
+                       printf(" %s", ip->extensions[i]);
+               printf("\n    MIME Types:");
+               for (i = 0; ip->mime_types[i]; i++)
+                       printf(" %s", ip->mime_types[i]);
+               printf("\n");
+       }
+       ip_unlock();
+}
diff --git a/input.h b/input.h
new file mode 100644 (file)
index 0000000..8536856
--- /dev/null
+++ b/input.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_INPUT_H
+#define CMUS_INPUT_H
+
+#include "keyval.h"
+#include "sf.h"
+#include "channelmap.h"
+
+struct input_plugin;
+
+void ip_load_plugins(void);
+
+/*
+ * allocates new struct input_plugin.
+ * never fails. does not check if the file is really playable
+ */
+struct input_plugin *ip_new(const char *filename);
+
+/*
+ * frees struct input_plugin closing it first if necessary
+ */
+void ip_delete(struct input_plugin *ip);
+
+/*
+ * errors: IP_ERROR_{ERRNO, FILE_FORMAT, SAMPLE_FORMAT}
+ */
+int ip_open(struct input_plugin *ip);
+
+void ip_setup(struct input_plugin *ip);
+
+/*
+ * errors: none?
+ */
+int ip_close(struct input_plugin *ip);
+
+/*
+ * errors: IP_ERROR_{ERRNO, FILE_FORMAT}
+ */
+int ip_read(struct input_plugin *ip, char *buffer, int count);
+
+/*
+ * errors: IP_ERROR_{FUNCTION_NOT_SUPPORTED}
+ */
+int ip_seek(struct input_plugin *ip, double offset);
+
+/*
+ * errors: IP_ERROR_{ERRNO}
+ */
+int ip_read_comments(struct input_plugin *ip, struct keyval **comments);
+
+int ip_duration(struct input_plugin *ip);
+int ip_bitrate(struct input_plugin *ip);
+int ip_current_bitrate(struct input_plugin *ip);
+char *ip_codec(struct input_plugin *ip);
+char *ip_codec_profile(struct input_plugin *ip);
+
+sample_format_t ip_get_sf(struct input_plugin *ip);
+void ip_get_channel_map(struct input_plugin *ip, channel_position_t *channel_map);
+const char *ip_get_filename(struct input_plugin *ip);
+const char *ip_get_metadata(struct input_plugin *ip);
+int ip_is_remote(struct input_plugin *ip);
+int ip_metadata_changed(struct input_plugin *ip);
+int ip_eof(struct input_plugin *ip);
+void ip_add_options(void);
+char *ip_get_error_msg(struct input_plugin *ip, int rc, const char *arg);
+char **ip_get_supported_extensions(void);
+void ip_dump_plugins(void);
+
+#endif
diff --git a/ip.h b/ip.h
new file mode 100644 (file)
index 0000000..56de9d6
--- /dev/null
+++ b/ip.h
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_IP_H
+#define CMUS_IP_H
+
+#include "keyval.h"
+#include "sf.h"
+#include "channelmap.h"
+
+#ifndef __GNUC__
+#include <fcntl.h>
+#include <unistd.h>
+#endif
+
+#define IP_ABI_VERSION 1
+
+enum {
+       /* no error */
+       IP_ERROR_SUCCESS,
+       /* system error (error code in errno) */
+       IP_ERROR_ERRNO,
+       /* file type not recognized */
+       IP_ERROR_UNRECOGNIZED_FILE_TYPE,
+       /* file type recognized, but not supported */
+       IP_ERROR_UNSUPPORTED_FILE_TYPE,
+       /* function not supported (usually seek) */
+       IP_ERROR_FUNCTION_NOT_SUPPORTED,
+       /* input plugin detected corrupted file */
+       IP_ERROR_FILE_FORMAT,
+       /* malformed uri */
+       IP_ERROR_INVALID_URI,
+       /* sample format not supported */
+       IP_ERROR_SAMPLE_FORMAT,
+       /* wrong disc inserted */
+       IP_ERROR_WRONG_DISC,
+       /* could not read disc */
+       IP_ERROR_NO_DISC,
+       /* error parsing response line / headers */
+       IP_ERROR_HTTP_RESPONSE,
+       /* usually 404 */
+       IP_ERROR_HTTP_STATUS,
+       /* too many redirections */
+       IP_ERROR_HTTP_REDIRECT_LIMIT,
+       /* plugin does not have this option */
+       IP_ERROR_NOT_OPTION,
+       /*  */
+       IP_ERROR_INTERNAL
+};
+
+struct input_plugin_data {
+       /* filled by ip-layer */
+       char *filename;
+       int fd;
+
+       unsigned int remote : 1;
+       unsigned int metadata_changed : 1;
+
+       /* shoutcast */
+       int counter;
+       int metaint;
+       char *metadata;
+       char *icy_name;
+       char *icy_genre;
+       char *icy_url;
+
+       /* filled by plugin */
+       sample_format_t sf;
+       channel_position_t channel_map[CHANNELS_MAX];
+       void *private;
+};
+
+struct input_plugin_ops {
+       int (*open)(struct input_plugin_data *ip_data);
+       int (*close)(struct input_plugin_data *ip_data);
+       int (*read)(struct input_plugin_data *ip_data, char *buffer, int count);
+       int (*seek)(struct input_plugin_data *ip_data, double offset);
+       int (*read_comments)(struct input_plugin_data *ip_data,
+                       struct keyval **comments);
+       int (*duration)(struct input_plugin_data *ip_data);
+       long (*bitrate)(struct input_plugin_data *ip_data);
+       long (*bitrate_current)(struct input_plugin_data *ip_data);
+       char *(*codec)(struct input_plugin_data *ip_data);
+       char *(*codec_profile)(struct input_plugin_data *ip_data);
+};
+
+struct input_plugin_opt {
+       const char *name;
+       int (*set)(const char *val);
+       int (*get)(char **val);
+};
+
+/* symbols exported by plugin */
+extern const struct input_plugin_ops ip_ops;
+extern const int ip_priority;
+extern const char * const ip_extensions[];
+extern const char * const ip_mime_types[];
+extern const struct input_plugin_opt ip_options[];
+extern const unsigned ip_abi_version;
+
+#endif
diff --git a/ip/aac.c b/ip/aac.c
new file mode 100644 (file)
index 0000000..d71527d
--- /dev/null
+++ b/ip/aac.c
@@ -0,0 +1,538 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 dnk <dnk@bjum.net>
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "xmalloc.h"
+#include "debug.h"
+#include "id3.h"
+#include "comment.h"
+#include "read_wrapper.h"
+#include "aac.h"
+
+#include <neaacdec.h>
+
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+/* FAAD_MIN_STREAMSIZE == 768, 6 == # of channels */
+#define BUFFER_SIZE    (FAAD_MIN_STREAMSIZE * 6 * 4)
+
+struct aac_private {
+       char rbuf[BUFFER_SIZE];
+       int rbuf_len;
+       int rbuf_pos;
+
+       unsigned char channels;
+       unsigned long sample_rate;
+       long bitrate;
+       int object_type;
+
+       struct {
+               unsigned long samples;
+               unsigned long bytes;
+       } current;
+
+       char *overflow_buf;
+       int overflow_buf_len;
+
+       NeAACDecHandle decoder; /* typedef void * */
+};
+
+static inline int buffer_length(const struct input_plugin_data *ip_data)
+{
+       struct aac_private *priv = ip_data->private;
+
+       return priv->rbuf_len - priv->rbuf_pos;
+}
+
+static inline void *buffer_data(const struct input_plugin_data *ip_data)
+{
+       struct aac_private *priv = ip_data->private;
+
+       return priv->rbuf + priv->rbuf_pos;
+}
+
+static int buffer_fill(struct input_plugin_data *ip_data)
+{
+       struct aac_private *priv = ip_data->private;
+       int32_t n;
+
+       if (priv->rbuf_pos > 0) {
+               priv->rbuf_len = buffer_length(ip_data);
+               memmove(priv->rbuf, priv->rbuf + priv->rbuf_pos, priv->rbuf_len);
+               priv->rbuf_pos = 0;
+       }
+
+       if (priv->rbuf_len == BUFFER_SIZE)
+               return 1;
+
+       n = read_wrapper(ip_data, priv->rbuf + priv->rbuf_len, BUFFER_SIZE - priv->rbuf_len);
+       if (n == -1)
+               return -1;
+       if (n == 0)
+               return 0;
+
+       priv->rbuf_len += n;
+       return 1;
+}
+
+static inline void buffer_consume(struct input_plugin_data *ip_data, int n)
+{
+       struct aac_private *priv = ip_data->private;
+
+       BUG_ON(n > buffer_length(ip_data));
+
+       priv->rbuf_pos += n;
+}
+
+static int buffer_fill_min(struct input_plugin_data *ip_data, int len)
+{
+       int rc;
+
+       BUG_ON(len > BUFFER_SIZE);
+
+       while (buffer_length(ip_data) < len) {
+               rc = buffer_fill(ip_data);
+               if (rc <= 0)
+                       return rc;
+       }
+       return 1;
+}
+
+/* 'data' must point to at least 6 bytes of data */
+static inline int parse_frame(const unsigned char data[6])
+{
+       int len;
+
+       /* http://www.audiocoding.com/modules/wiki/?page=ADTS */
+
+       /* first 12 bits must be set */
+       if (data[0] != 0xFF)
+               return 0;
+       if ((data[1] & 0xF0) != 0xF0)
+               return 0;
+
+       /* layer is always '00' */
+       if ((data[1] & 0x06) != 0x00)
+               return 0;
+
+       /* frame length is stored in 13 bits */
+       len  = data[3] << 11;   /* ..1100000000000 */
+       len |= data[4] << 3;    /* ..xx11111111xxx */
+       len |= data[5] >> 5;    /* ..xxxxxxxxxx111 */
+       len &= 0x1FFF;          /* 13 bits */
+       return len;
+}
+
+/* scans forward to the next aac frame and makes sure
+ * the entire frame is in the buffer.
+ */
+static int buffer_fill_frame(struct input_plugin_data *ip_data)
+{
+       unsigned char *data;
+       int rc, n, len;
+       int max = 32768;
+
+       while (1) {
+               /* need at least 6 bytes of data */
+               rc = buffer_fill_min(ip_data, 6);
+               if (rc <= 0)
+                       return rc;
+
+               len = buffer_length(ip_data);
+               data = buffer_data(ip_data);
+
+               /* scan for a frame */
+               for (n = 0; n < len - 5; n++) {
+                       /* give up after 32KB */
+                       if (max-- == 0) {
+                               d_print("no frame found!\n");
+                               /* FIXME: set errno? */
+                               return -1;
+                       }
+
+                       /* see if there's a frame at this location */
+                       rc = parse_frame(data + n);
+                       if (rc == 0)
+                               continue;
+
+                       /* found a frame, consume all data up to the frame */
+                       buffer_consume(ip_data, n);
+
+                       /* rc == frame length */
+                       rc = buffer_fill_min(ip_data, rc);
+                       if (rc <= 0)
+                               return rc;
+
+                       return 1;
+               }
+
+               /* consume what we used */
+               buffer_consume(ip_data, n);
+       }
+       /* not reached */
+}
+
+static void aac_get_channel_map(struct input_plugin_data *ip_data)
+{
+       struct aac_private *priv = ip_data->private;
+       NeAACDecFrameInfo frame_info;
+       void *buf;
+       int i;
+
+       ip_data->channel_map[0] = CHANNEL_POSITION_INVALID;
+
+       if (buffer_fill_frame(ip_data) <= 0)
+               return;
+
+       buf = NeAACDecDecode(priv->decoder, &frame_info, buffer_data(ip_data), buffer_length(ip_data));
+       if (!buf || frame_info.error != 0 || frame_info.bytesconsumed <= 0
+                       || frame_info.channels > CHANNELS_MAX)
+               return;
+
+       for (i = 0; i < frame_info.channels; i++)
+               ip_data->channel_map[i] = channel_position_aac(frame_info.channel_position[i]);
+}
+
+static int aac_open(struct input_plugin_data *ip_data)
+{
+       struct aac_private *priv;
+       NeAACDecConfigurationPtr neaac_cfg;
+       int ret, n;
+
+       /* init private struct */
+       const struct aac_private priv_init = {
+               .decoder = NeAACDecOpen(),
+               .bitrate = -1,
+               .object_type = -1
+       };
+       priv = xnew(struct aac_private, 1);
+       *priv = priv_init;
+       ip_data->private = priv;
+
+       /* set decoder config */
+       neaac_cfg = NeAACDecGetCurrentConfiguration(priv->decoder);
+       neaac_cfg->outputFormat = FAAD_FMT_16BIT;       /* force 16 bit audio */
+       neaac_cfg->downMatrix = 0;                      /* NOT 5.1 -> stereo */
+       neaac_cfg->dontUpSampleImplicitSBR = 0;         /* upsample, please! */
+       NeAACDecSetConfiguration(priv->decoder, neaac_cfg);
+
+       /* find a frame */
+       if (buffer_fill_frame(ip_data) <= 0) {
+               ret = -IP_ERROR_FILE_FORMAT;
+               goto out;
+       }
+
+       /* in case of a bug, make sure there is at least some data
+        * in the buffer for NeAACDecInit() to work with.
+        */
+       if (buffer_fill_min(ip_data, 256) <= 0) {
+               d_print("not enough data\n");
+               ret = -IP_ERROR_FILE_FORMAT;
+               goto out;
+       }
+
+       /* init decoder, returns the length of the header (if any) */
+       n = NeAACDecInit(priv->decoder, buffer_data(ip_data), buffer_length(ip_data),
+               &priv->sample_rate, &priv->channels);
+       if (n < 0) {
+               d_print("NeAACDecInit failed\n");
+               ret = -IP_ERROR_FILE_FORMAT;
+               goto out;
+       }
+
+       d_print("sample rate %luhz, channels %u\n", priv->sample_rate, priv->channels);
+       if (!priv->sample_rate || !priv->channels) {
+               ret = -IP_ERROR_FILE_FORMAT;
+               goto out;
+       }
+
+       /* skip the header */
+       d_print("skipping header (%d bytes)\n", n);
+
+       buffer_consume(ip_data, n);
+
+       /*NeAACDecInitDRM(priv->decoder, priv->sample_rate, priv->channels);*/
+
+       ip_data->sf = sf_rate(priv->sample_rate) | sf_channels(priv->channels) | sf_bits(16) | sf_signed(1);
+       ip_data->sf |= sf_host_endian();
+       aac_get_channel_map(ip_data);
+
+       return 0;
+out:
+       NeAACDecClose(priv->decoder);
+       free(priv);
+       return ret;
+}
+
+static int aac_close(struct input_plugin_data *ip_data)
+{
+       struct aac_private *priv = ip_data->private;
+
+       NeAACDecClose(priv->decoder);
+       free(priv);
+       ip_data->private = NULL;
+       return 0;
+}
+
+/* returns -1 on fatal errors
+ * returns -2 on non-fatal errors
+ * 0 on eof
+ * number of bytes put in 'buffer' on success */
+static int decode_one_frame(struct input_plugin_data *ip_data, void *buffer, int count)
+{
+       struct aac_private *priv = ip_data->private;
+       unsigned char *aac_data;
+       unsigned int aac_data_size;
+       NeAACDecFrameInfo frame_info;
+       char *sample_buf;
+       int bytes, rc;
+
+       rc = buffer_fill_frame(ip_data);
+       if (rc <= 0)
+               return rc;
+
+       aac_data = buffer_data(ip_data);
+       aac_data_size = buffer_length(ip_data);
+
+       /* aac data -> raw pcm */
+       sample_buf = NeAACDecDecode(priv->decoder, &frame_info, aac_data, aac_data_size);
+       if (frame_info.error == 0 && frame_info.samples > 0) {
+               priv->current.samples += frame_info.samples;
+               priv->current.bytes += frame_info.bytesconsumed;
+       }
+
+       buffer_consume(ip_data, frame_info.bytesconsumed);
+
+       if (!sample_buf || frame_info.bytesconsumed <= 0) {
+               d_print("fatal error: %s\n", NeAACDecGetErrorMessage(frame_info.error));
+               errno = EINVAL;
+               return -1;
+       }
+
+       if (frame_info.error != 0) {
+               d_print("frame error: %s\n", NeAACDecGetErrorMessage(frame_info.error));
+               return -2;
+       }
+
+       if (frame_info.samples <= 0)
+               return -2;
+
+       if (frame_info.channels != priv->channels || frame_info.samplerate != priv->sample_rate) {
+               d_print("invalid channel or sample_rate count\n");
+               return -2;
+       }
+
+       /* 16-bit samples */
+       bytes = frame_info.samples * 2;
+
+       if (bytes > count) {
+               /* decoded too much, keep overflow */
+               priv->overflow_buf = sample_buf + count;
+               priv->overflow_buf_len = bytes - count;
+               memcpy(buffer, sample_buf, count);
+               return count;
+       } else {
+               memcpy(buffer, sample_buf, bytes);
+       }
+       return bytes;
+}
+
+static int aac_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       struct aac_private *priv = ip_data->private;
+       int rc;
+
+       /* use overflow from previous call (if any) */
+       if (priv->overflow_buf_len) {
+               int len = priv->overflow_buf_len;
+
+               if (len > count)
+                       len = count;
+
+               memcpy(buffer, priv->overflow_buf, len);
+               priv->overflow_buf += len;
+               priv->overflow_buf_len -= len;
+               return len;
+       }
+
+       do {
+               rc = decode_one_frame(ip_data, buffer, count);
+       } while (rc == -2);
+       return rc;
+}
+
+static int aac_seek(struct input_plugin_data *ip_data, double offset)
+{
+       return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+}
+
+static int aac_read_comments(struct input_plugin_data *ip_data,
+               struct keyval **comments)
+{
+       GROWING_KEYVALS(c);
+       struct id3tag id3;
+       int rc, fd, i;
+
+       fd = open(ip_data->filename, O_RDONLY);
+       if (fd == -1)
+               return -1;
+
+       id3_init(&id3);
+       rc = id3_read_tags(&id3, fd, ID3_V1 | ID3_V2);
+       if (rc == -1) {
+               d_print("error: %s\n", strerror(errno));
+               goto out;
+       }
+
+       for (i = 0; i < NUM_ID3_KEYS; i++) {
+               char *val = id3_get_comment(&id3, i);
+
+               if (val)
+                       comments_add(&c, id3_key_names[i], val);
+       }
+out:
+       close(fd);
+       id3_free(&id3);
+       keyvals_terminate(&c);
+       *comments = c.keyvals;
+       return 0;
+}
+
+static int aac_duration(struct input_plugin_data *ip_data)
+{
+       struct aac_private *priv = ip_data->private;
+       NeAACDecFrameInfo frame_info;
+       int samples = 0, bytes = 0, frames = 0;
+       off_t file_size;
+
+       file_size = lseek(ip_data->fd, 0, SEEK_END);
+       if (file_size == -1)
+               return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+
+       /* Seek to the middle of the file. There is almost always silence at
+        * the beginning, which gives wrong results. */
+       if (lseek(ip_data->fd, file_size/2, SEEK_SET) == -1)
+               return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+
+       priv->rbuf_pos = 0;
+       priv->rbuf_len = 0;
+
+       /* guess track length by decoding the first 10 frames */
+       while (frames < 10) {
+               if (buffer_fill_frame(ip_data) <= 0)
+                       break;
+
+               NeAACDecDecode(priv->decoder, &frame_info,
+                       buffer_data(ip_data), buffer_length(ip_data));
+               if (frame_info.error == 0 && frame_info.samples > 0) {
+                       samples += frame_info.samples;
+                       bytes += frame_info.bytesconsumed;
+                       frames++;
+               }
+               if (frame_info.bytesconsumed == 0)
+                       break;
+
+               buffer_consume(ip_data, frame_info.bytesconsumed);
+       }
+
+       if (frames == 0)
+               return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+
+       samples /= frames;
+       samples /= priv->channels;
+       bytes /= frames;
+
+       /*  8 * file_size / duration */
+       priv->bitrate = (8 * bytes * priv->sample_rate) / samples;
+
+       priv->object_type = frame_info.object_type;
+
+       return ((file_size / bytes) * samples) / priv->sample_rate;
+}
+
+static long aac_bitrate(struct input_plugin_data *ip_data)
+{
+       struct aac_private *priv = ip_data->private;
+       return priv->bitrate != -1 ? priv->bitrate : -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+}
+
+static long aac_current_bitrate(struct input_plugin_data *ip_data)
+{
+       struct aac_private *priv = ip_data->private;
+       long bitrate = -1;
+       if (priv->current.samples > 0) {
+               priv->current.samples /= priv->channels;
+               bitrate = (8 * priv->current.bytes * priv->sample_rate) / priv->current.samples;
+               priv->current.samples = 0;
+               priv->current.bytes = 0;
+       }
+       return bitrate;
+}
+
+static char *aac_codec(struct input_plugin_data *ip_data)
+{
+       return xstrdup("aac");
+}
+
+static const char *object_type_to_str(int object_type)
+{
+       switch (object_type) {
+       case MAIN:      return "Main";
+       case LC:        return "LC";
+       case SSR:       return "SSR";
+       case LTP:       return "LTP";
+       case HE_AAC:    return "HE";
+       case ER_LC:     return "ER-LD";
+       case ER_LTP:    return "ER-LTP";
+       case LD:        return "LD";
+       case DRM_ER_LC: return "DRM-ER-LC";
+       }
+       return NULL;
+}
+
+static char *aac_codec_profile(struct input_plugin_data *ip_data)
+{
+       struct aac_private *priv = ip_data->private;
+       const char *profile = object_type_to_str(priv->object_type);
+
+       return profile ? xstrdup(profile) : NULL;
+}
+
+const struct input_plugin_ops ip_ops = {
+       .open = aac_open,
+       .close = aac_close,
+       .read = aac_read,
+       .seek = aac_seek,
+       .read_comments = aac_read_comments,
+       .duration = aac_duration,
+       .bitrate = aac_bitrate,
+       .bitrate_current = aac_current_bitrate,
+       .codec = aac_codec,
+       .codec_profile = aac_codec_profile
+};
+
+const int ip_priority = 50;
+const char * const ip_extensions[] = { "aac", NULL };
+const char * const ip_mime_types[] = { "audio/aac", "audio/aacp", NULL };
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/ip/aac.h b/ip/aac.h
new file mode 100644 (file)
index 0000000..9430de6
--- /dev/null
+++ b/ip/aac.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2011-2013 Various Authors
+ * Copyright 2011 Johannes Weißl
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_AAC_H
+#define CMUS_AAC_H
+
+#include "channelmap.h"
+#include <neaacdec.h>
+
+static inline channel_position_t channel_position_aac(unsigned char c)
+{
+       switch (c) {
+       case FRONT_CHANNEL_CENTER:      return CHANNEL_POSITION_FRONT_CENTER;
+       case FRONT_CHANNEL_LEFT:        return CHANNEL_POSITION_FRONT_LEFT;
+       case FRONT_CHANNEL_RIGHT:       return CHANNEL_POSITION_FRONT_RIGHT;
+       case SIDE_CHANNEL_LEFT:         return CHANNEL_POSITION_SIDE_LEFT;
+       case SIDE_CHANNEL_RIGHT:        return CHANNEL_POSITION_SIDE_RIGHT;
+       case BACK_CHANNEL_LEFT:         return CHANNEL_POSITION_REAR_LEFT;
+       case BACK_CHANNEL_RIGHT:        return CHANNEL_POSITION_REAR_RIGHT;
+       case BACK_CHANNEL_CENTER:       return CHANNEL_POSITION_REAR_CENTER;
+       case LFE_CHANNEL:               return CHANNEL_POSITION_LFE;
+       default:                        return CHANNEL_POSITION_INVALID;
+       }
+}
+
+#endif
diff --git a/ip/bass.c b/ip/bass.c
new file mode 100644 (file)
index 0000000..5adff64
--- /dev/null
+++ b/ip/bass.c
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2016 Nic Soudée
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "xmalloc.h"
+#include "comment.h"
+#include "bass.h"
+#include "uchar.h"
+
+#define BITS (16)
+#define FREQ (44100)
+#define CHANS (2)
+
+struct bass_private {
+       DWORD chan;
+};
+
+static int bass_init(void)
+{
+       static int inited = 0;
+
+       if (inited)
+               return 1;
+
+       if (!BASS_Init(0, FREQ, 0, 0, NULL))
+               return 0;
+
+       inited = 1;
+       return 1;
+}
+
+static int bass_open(struct input_plugin_data *ip_data)
+{
+       struct bass_private *priv;
+       DWORD chan;
+       DWORD flags;
+
+       if (!bass_init())
+               return -IP_ERROR_INTERNAL;
+
+       flags = BASS_MUSIC_DECODE;
+       flags |= BASS_MUSIC_RAMP;
+       flags |= BASS_MUSIC_PRESCAN;
+       flags |= BASS_MUSIC_STOPBACK;
+
+       chan = BASS_MusicLoad(FALSE, ip_data->filename, 0, 0, flags, 0);
+
+       if (!chan) {
+               return -IP_ERROR_ERRNO;
+       }
+
+       priv = xnew(struct bass_private, 1);
+       priv->chan = chan;
+       ip_data->private = priv;
+       ip_data->sf = sf_bits(BITS) | sf_rate(FREQ) | sf_channels(CHANS) | sf_signed(1);
+       ip_data->sf |= sf_host_endian();
+       channel_map_init_stereo(ip_data->channel_map);
+       return 0;
+}
+
+static int bass_close(struct input_plugin_data *ip_data)
+{
+       struct bass_private *priv = ip_data->private;
+
+       BASS_MusicFree(priv->chan);
+       free(priv);
+       ip_data->private = NULL;
+       return 0;
+}
+
+static int bass_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       int length;
+       struct bass_private *priv = ip_data->private;
+       length = BASS_ChannelGetData(priv->chan, buffer, count);
+       if (length < 0) {
+               return 0;
+       }
+       return length;
+}
+
+static int bass_seek(struct input_plugin_data *ip_data, double offset)
+{
+       struct bass_private *priv = ip_data->private;
+       QWORD pos = (QWORD)(offset * (FREQ * CHANS * (BITS / 8)) + 0.5);
+       QWORD flags = BASS_POS_BYTE | BASS_POS_DECODE;
+
+       if (!BASS_ChannelSetPosition(priv->chan, pos, flags)) {
+               return -IP_ERROR_INTERNAL;
+       }
+       return 0;
+}
+
+static unsigned char *encode_ascii_string(const char *str)
+{
+       unsigned char *ret;
+       int n;
+
+       ret = malloc(strlen(str) + 1);
+       n = u_to_ascii(ret, str, strlen(str));
+       ret[n] = '\0';
+       return ret;
+}
+
+static int bass_read_comments(struct input_plugin_data *ip_data,
+                               struct keyval **comments)
+{
+       struct bass_private *priv = ip_data->private;
+       GROWING_KEYVALS(c);
+       const char *val;
+
+       val = BASS_ChannelGetTags(priv->chan, BASS_TAG_MUSIC_NAME);
+       if (val && val[0])
+               comments_add_const(&c, "title", encode_ascii_string(val));
+       keyvals_terminate(&c);
+       *comments = c.keyvals;
+       return 0;
+}
+
+static int bass_duration(struct input_plugin_data *ip_data)
+{
+       static float length = 0;
+       int pos;
+       struct bass_private *priv = ip_data->private;
+
+       pos = BASS_ChannelGetLength(priv->chan, BASS_POS_BYTE);
+       if (pos && pos != -1) {
+               length = BASS_ChannelBytes2Seconds(priv->chan, pos);
+       }
+       else {
+               length = -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+       }
+       return length;
+}
+
+static long bass_bitrate(struct input_plugin_data *ip_data)
+{
+       return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+}
+
+static const char *bass_type_to_string(int type)
+{
+       /* from <bass.h> */
+       switch (type) {
+               case 0x20000: return "mod";
+               case 0x20001: return "mtm";
+               case 0x20002: return "s3m";
+               case 0x20003: return "xm";
+               case 0x20004: return "it";
+       }
+       return NULL;
+}
+
+static char *bass_codec(struct input_plugin_data *ip_data)
+{
+       const char *codec;
+       int type;
+       BASS_CHANNELINFO info;
+       struct bass_private *priv = ip_data->private;
+
+       if (!(BASS_ChannelGetInfo(priv->chan, &info))) {
+               return NULL;
+       }
+       type = info.ctype;
+       codec = bass_type_to_string(type);
+       return codec ? xstrdup(codec) : NULL;
+}
+
+static char *bass_codec_profile(struct input_plugin_data *ip_data)
+{
+       return NULL;
+}
+
+const struct input_plugin_ops ip_ops = {
+       .open = bass_open,
+       .close = bass_close,
+       .read = bass_read,
+       .seek = bass_seek,
+       .read_comments = bass_read_comments,
+       .duration = bass_duration,
+       .bitrate = bass_bitrate,
+       .bitrate_current = bass_bitrate,
+       .codec = bass_codec,
+       .codec_profile = bass_codec_profile
+};
+
+const int ip_priority = 60;
+const char * const ip_extensions[] = {
+       "xm", "it", "s3m", "mod", "mtm", "umx", NULL
+};
+
+const char * const ip_mime_types[] = { NULL };
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
+
diff --git a/ip/cdio.c b/ip/cdio.c
new file mode 100644 (file)
index 0000000..8a16685
--- /dev/null
+++ b/ip/cdio.c
@@ -0,0 +1,547 @@
+/*
+ * Copyright 2011-2013 Various Authors
+ * Copyright 2011 Johannes Weißl
+ *
+ * Based on cdda.c from XMMS2.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "file.h"
+#include "xmalloc.h"
+#include "debug.h"
+#include "utils.h"
+#include "options.h"
+#include "comment.h"
+#include "discid.h"
+
+#include <cdio/cdio.h>
+#include <cdio/logging.h>
+#if LIBCDIO_VERSION_NUM >= 90
+#include <cdio/paranoia/cdda.h>
+#else
+#include <cdio/cdda.h>
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#undef HAVE_CDDB
+
+#ifdef HAVE_CONFIG
+#include "config/cdio.h"
+#endif
+
+#ifdef HAVE_CDDB
+#include "http.h"
+#include "xstrjoin.h"
+#include <cddb/cddb.h>
+#endif
+
+#ifdef HAVE_CDDB
+static char *cddb_url = NULL;
+#endif
+
+static struct {
+       CdIo_t *cdio;
+       cdrom_drive_t *drive;
+       const char *disc_id;
+       const char *device;
+} cached;
+
+struct cdda_private {
+       CdIo_t *cdio;
+       cdrom_drive_t *drive;
+       char *disc_id;
+       char *device;
+       track_t track;
+       lsn_t first_lsn;
+       lsn_t last_lsn;
+       lsn_t current_lsn;
+       int first_read;
+
+       char read_buf[CDIO_CD_FRAMESIZE_RAW];
+       unsigned long buf_used;
+};
+
+static void libcdio_log(cdio_log_level_t level, const char *message)
+{
+       const char *level_names[] = { "DEBUG", "INFO", "WARN", "ERROR", "ASSERT" };
+       int len = strlen(message);
+       if (len > 0 && message[len-1] == '\n')
+               len--;
+       if (len > 0) {
+               level = clamp(level, 1, N_ELEMENTS(level_names));
+               d_print("%s: %.*s\n", level_names[level-1], len, message);
+       }
+}
+
+static int libcdio_open(struct input_plugin_data *ip_data)
+{
+       struct cdda_private *priv, priv_init = {
+               .first_read = 1,
+               .buf_used = CDIO_CD_FRAMESIZE_RAW
+       };
+       CdIo_t *cdio = NULL;
+       cdrom_drive_t *drive = NULL;
+       const char *device = cdda_device;
+       lsn_t first_lsn;
+       int track = -1;
+       char *disc_id = NULL;
+       char *msg = NULL;
+       int rc = 0, save = 0;
+
+       if (!parse_cdda_url(ip_data->filename, &disc_id, &track, NULL)) {
+               rc = -IP_ERROR_INVALID_URI;
+               goto end;
+       }
+
+       if (track == -1) {
+               d_print("invalid or missing track number, aborting!\n");
+               rc = -IP_ERROR_INVALID_URI;
+               goto end;
+       }
+
+       /* In case of cue/toc/nrg, take filename (= disc_id) as device.
+        * A real disc_id is base64 encoded and never contains a slash */
+       if (strchr(disc_id, '/'))
+               device = disc_id;
+
+       ip_data->fd = open(device, O_RDONLY);
+       if (ip_data->fd == -1) {
+               save = errno;
+               d_print("could not open device %s\n", device);
+               rc = -IP_ERROR_ERRNO;
+               goto end;
+       }
+
+       if (cached.cdio && strcmp(disc_id, cached.disc_id) == 0 && strcmp(device, cached.device) == 0) {
+               cdio = cached.cdio;
+               drive = cached.drive;
+       } else {
+               cdio_log_set_handler(libcdio_log);
+               cdio = cdio_open(device, DRIVER_UNKNOWN);
+               if (!cdio) {
+                       d_print("failed to open device %s\n", device);
+                       rc = -IP_ERROR_NO_DISC;
+                       goto end;
+               }
+               cdio_set_speed(cdio, 1);
+
+               drive = cdio_cddap_identify_cdio(cdio, CDDA_MESSAGE_LOGIT, &msg);
+               if (!drive) {
+                       d_print("failed to identify drive, aborting!\n");
+                       rc = -IP_ERROR_NO_DISC;
+                       goto end;
+               }
+               d_print("%s", msg);
+               cdio_cddap_verbose_set(drive, CDDA_MESSAGE_LOGIT, CDDA_MESSAGE_LOGIT);
+               drive->b_swap_bytes = 1;
+
+               if (cdio_cddap_open(drive)) {
+                       d_print("unable to open disc, aborting!\n");
+                       rc = -IP_ERROR_NO_DISC;
+                       goto end;
+               }
+       }
+
+       first_lsn = cdio_cddap_track_firstsector(drive, track);
+       if (first_lsn == -1) {
+               d_print("no such track: %d, aborting!\n", track);
+               rc = -IP_ERROR_INVALID_URI;
+               goto end;
+       }
+
+       priv = xnew(struct cdda_private, 1);
+       *priv = priv_init;
+       priv->cdio = cdio;
+       priv->drive = drive;
+       priv->disc_id = xstrdup(disc_id);
+       priv->device = xstrdup(device);
+       priv->track = track;
+       priv->first_lsn = first_lsn;
+       priv->last_lsn = cdio_cddap_track_lastsector(drive, priv->track);
+       priv->current_lsn = first_lsn;
+
+       cached.cdio = priv->cdio;
+       cached.drive = priv->drive;
+       cached.disc_id = priv->disc_id;
+       cached.device = priv->device;
+
+       ip_data->private = priv;
+       ip_data->sf = sf_bits(16) | sf_rate(44100) | sf_channels(2) | sf_signed(1);
+       ip_data->sf |= sf_host_endian();
+
+end:
+       free(disc_id);
+
+       if (rc < 0) {
+               if (ip_data->fd != -1)
+                       close(ip_data->fd);
+               ip_data->fd = -1;
+       }
+
+       if (rc == -IP_ERROR_ERRNO)
+               errno = save;
+       return rc;
+}
+
+static int libcdio_close(struct input_plugin_data *ip_data)
+{
+       struct cdda_private *priv = ip_data->private;
+
+       if (ip_data->fd != -1)
+               close(ip_data->fd);
+       ip_data->fd = -1;
+
+       if (strcmp(priv->disc_id, cached.disc_id) != 0 || strcmp(priv->device, cached.device) != 0) {
+               cdio_cddap_close_no_free_cdio(priv->drive);
+               cdio_destroy(priv->cdio);
+               free(priv->disc_id);
+               free(priv->device);
+       }
+
+       free(priv);
+       ip_data->private = NULL;
+       return 0;
+}
+
+static int libcdio_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       struct cdda_private *priv = ip_data->private;
+       int rc = 0;
+
+       if (priv->first_read || cdio_get_media_changed(priv->cdio)) {
+               char *disc_id;
+               priv->first_read = 0;
+               if (!get_disc_id(priv->device, &disc_id, NULL))
+                       return -IP_ERROR_NO_DISC;
+               if (strcmp(disc_id, priv->disc_id) != 0) {
+                       free(disc_id);
+                       return -IP_ERROR_WRONG_DISC;
+               }
+               free(disc_id);
+       }
+
+       if (priv->current_lsn >= priv->last_lsn)
+               return 0;
+
+       if (priv->buf_used == CDIO_CD_FRAMESIZE_RAW) {
+               cdio_cddap_read(priv->drive, priv->read_buf, priv->current_lsn, 1);
+               priv->current_lsn++;
+               priv->buf_used = 0;
+       }
+
+       if (count >= CDIO_CD_FRAMESIZE_RAW) {
+               rc = CDIO_CD_FRAMESIZE_RAW - priv->buf_used;
+               memcpy(buffer, priv->read_buf + priv->buf_used, rc);
+       } else {
+               unsigned long buf_left = CDIO_CD_FRAMESIZE_RAW - priv->buf_used;
+
+               if (buf_left < count) {
+                       memcpy(buffer, priv->read_buf + priv->buf_used, buf_left);
+                       rc = buf_left;
+               } else {
+                       memcpy(buffer, priv->read_buf + priv->buf_used, count);
+                       rc = count;
+               }
+       }
+       priv->buf_used += rc;
+
+       return rc;
+}
+
+static int libcdio_seek(struct input_plugin_data *ip_data, double offset)
+{
+       struct cdda_private *priv = ip_data->private;
+       lsn_t new_lsn;
+       int64_t samples = offset * 44100;
+
+       /* Magic number 42... really should think of a better way to do this but
+        * it seemed that the lsn is off by about 42 everytime...
+        */
+       new_lsn = samples / 441.0 * CDIO_CD_FRAMES_PER_SEC / 100 + 42;
+
+       if ((priv->first_lsn + new_lsn) > priv->last_lsn) {
+               d_print("trying to seek past the end of track.\n");
+               return -1;
+       }
+
+       priv->current_lsn = priv->first_lsn + new_lsn;
+
+       return 0;
+}
+
+#ifdef HAVE_CDDB
+static int parse_cddb_url(const char *url, struct http_uri *http_uri, int *use_http)
+{
+       char *full_url;
+       int rc;
+
+       if (is_http_url(url)) {
+               *use_http = 1;
+               full_url = xstrdup(url);
+       } else {
+               *use_http = 0;
+               full_url = xstrjoin("http://", url);
+       }
+
+       rc = http_parse_uri(full_url, http_uri);
+       free(full_url);
+       return rc == 0;
+}
+
+static void setup_cddb_conn(cddb_conn_t *cddb_conn)
+{
+       struct http_uri http_uri, http_proxy_uri;
+       const char *proxy;
+       int use_http;
+
+       parse_cddb_url(cddb_url, &http_uri, &use_http);
+
+       proxy = getenv("http_proxy");
+       if (proxy && http_parse_uri(proxy, &http_proxy_uri) == 0) {
+               cddb_http_proxy_enable(cddb_conn);
+               cddb_set_http_proxy_server_name(cddb_conn, http_proxy_uri.host);
+               cddb_set_http_proxy_server_port(cddb_conn, http_proxy_uri.port);
+               if (http_proxy_uri.user)
+                       cddb_set_http_proxy_username(cddb_conn, http_proxy_uri.user);
+               if (http_proxy_uri.pass)
+                       cddb_set_http_proxy_password(cddb_conn, http_proxy_uri.pass);
+               http_free_uri(&http_proxy_uri);
+       } else
+               cddb_http_proxy_disable(cddb_conn);
+
+       if (use_http)
+               cddb_http_enable(cddb_conn);
+       else
+               cddb_http_disable(cddb_conn);
+
+       cddb_set_server_name(cddb_conn, http_uri.host);
+       cddb_set_email_address(cddb_conn, "me@home");
+       cddb_set_server_port(cddb_conn, http_uri.port);
+       if (strcmp(http_uri.path, "/") != 0)
+               cddb_set_http_path_query(cddb_conn, http_uri.path);
+#ifdef DEBUG_CDDB
+       cddb_cache_disable(cddb_conn);
+#endif
+
+       http_free_uri(&http_uri);
+}
+#endif
+
+
+#define add_comment(c, x)      do { if (x) comments_add_const(c, #x, x); } while (0)
+
+static int libcdio_read_comments(struct input_plugin_data *ip_data, struct keyval **comments)
+{
+       struct cdda_private *priv = ip_data->private;
+       GROWING_KEYVALS(c);
+       const char *artist = NULL, *albumartist = NULL, *album = NULL,
+               *title = NULL, *genre = NULL, *comment = NULL;
+       int track_comments_found = 0;
+       const cdtext_t *cdt;
+#ifdef HAVE_CDDB
+       cddb_conn_t *cddb_conn = NULL;
+       cddb_disc_t *cddb_disc = NULL;
+#endif
+       char buf[64];
+
+#if LIBCDIO_VERSION_NUM >= 90
+       cdt = cdio_get_cdtext(priv->cdio);
+       if (cdt) {
+               artist = cdtext_get(cdt, CDTEXT_FIELD_PERFORMER, priv->track);
+               title = cdtext_get(cdt, CDTEXT_FIELD_TITLE, priv->track);
+               genre = cdtext_get(cdt, CDTEXT_FIELD_GENRE, priv->track);
+               comment = cdtext_get(cdt, CDTEXT_FIELD_MESSAGE, priv->track);
+
+               if (title)
+                       track_comments_found = 1;
+
+               album = cdtext_get(cdt, CDTEXT_FIELD_TITLE, 0);
+               albumartist = cdtext_get(cdt, CDTEXT_FIELD_PERFORMER, 0);
+               if (!artist)
+                       artist = albumartist;
+               if (!genre)
+                       genre = cdtext_get(cdt, CDTEXT_FIELD_GENRE, 0);
+               if (!comment)
+                       comment = cdtext_get(cdt, CDTEXT_FIELD_MESSAGE, 0);
+       }
+#else
+       cdt = cdio_get_cdtext(priv->cdio, priv->track);
+       if (cdt) {
+               char * const *field = cdt->field;
+               track_comments_found = 1;
+               artist = field[CDTEXT_PERFORMER];
+               title = field[CDTEXT_TITLE];
+               genre = field[CDTEXT_GENRE];
+               comment = field[CDTEXT_MESSAGE];
+       }
+       cdt = cdio_get_cdtext(priv->cdio, 0);
+       if (cdt) {
+               char * const *field = cdt->field;
+               album = field[CDTEXT_TITLE];
+               albumartist = field[CDTEXT_PERFORMER];
+               if (!artist)
+                       artist = field[CDTEXT_PERFORMER];
+               if (!genre)
+                       genre = field[CDTEXT_GENRE];
+               if (!comment)
+                       comment = field[CDTEXT_MESSAGE];
+       }
+#endif
+
+#ifdef HAVE_CDDB
+       if (!track_comments_found && cddb_url && cddb_url[0]) {
+               cddb_track_t *cddb_track;
+               track_t i_tracks = cdio_get_num_tracks(priv->cdio);
+               track_t i_first_track = cdio_get_first_track_num(priv->cdio);
+               unsigned int year;
+               int i;
+
+               cddb_conn = cddb_new();
+               if (!cddb_conn)
+                       malloc_fail();
+
+               setup_cddb_conn(cddb_conn);
+
+               cddb_disc = cddb_disc_new();
+               if (!cddb_disc)
+                       malloc_fail();
+               for (i = 0; i < i_tracks; i++) {
+                       cddb_track = cddb_track_new();
+                       if (!cddb_track)
+                               malloc_fail();
+                       cddb_track_set_frame_offset(cddb_track,
+                                       cdio_get_track_lba(priv->cdio, i+i_first_track));
+                       cddb_disc_add_track(cddb_disc, cddb_track);
+               }
+
+               cddb_disc_set_length(cddb_disc, cdio_get_track_lba(priv->cdio,
+                                       CDIO_CDROM_LEADOUT_TRACK) / CDIO_CD_FRAMES_PER_SEC);
+               if (cddb_query(cddb_conn, cddb_disc) == 1 && cddb_read(cddb_conn, cddb_disc)) {
+                       albumartist = cddb_disc_get_artist(cddb_disc);
+                       album = cddb_disc_get_title(cddb_disc);
+                       genre = cddb_disc_get_genre(cddb_disc);
+                       year = cddb_disc_get_year(cddb_disc);
+                       if (year) {
+                               sprintf(buf, "%u", year);
+                               comments_add_const(&c, "date", buf);
+                       }
+                       cddb_track = cddb_disc_get_track(cddb_disc, priv->track - 1);
+                       artist = cddb_track_get_artist(cddb_track);
+                       if (!artist)
+                               artist = albumartist;
+                       title = cddb_track_get_title(cddb_track);
+               }
+       }
+#endif
+
+       add_comment(&c, artist);
+       add_comment(&c, albumartist);
+       add_comment(&c, album);
+       add_comment(&c, title);
+       add_comment(&c, genre);
+       add_comment(&c, comment);
+
+       sprintf(buf, "%02d", priv->track);
+       comments_add_const(&c, "tracknumber", buf);
+
+#ifdef HAVE_CDDB
+       if (cddb_disc)
+               cddb_disc_destroy(cddb_disc);
+       if (cddb_conn)
+               cddb_destroy(cddb_conn);
+#endif
+
+       keyvals_terminate(&c);
+       *comments = c.keyvals;
+       return 0;
+}
+
+static int libcdio_duration(struct input_plugin_data *ip_data)
+{
+       struct cdda_private *priv = ip_data->private;
+
+       return (priv->last_lsn - priv->first_lsn) / CDIO_CD_FRAMES_PER_SEC;
+}
+
+static long libcdio_bitrate(struct input_plugin_data *ip_data)
+{
+       return 44100 * 16 * 2;
+}
+
+static char *libcdio_codec(struct input_plugin_data *ip_data)
+{
+       return xstrdup("cdda");
+}
+
+static char *libcdio_codec_profile(struct input_plugin_data *ip_data)
+{
+       struct cdda_private *priv = ip_data->private;
+       discmode_t cd_discmode = cdio_get_discmode(priv->cdio);
+
+       return xstrdup(discmode2str[cd_discmode]);
+}
+
+#ifdef HAVE_CDDB
+static int libcdio_set_cddb_url(const char *val)
+{
+       struct http_uri http_uri;
+       int use_http;
+       if (!parse_cddb_url(val, &http_uri, &use_http))
+               return -IP_ERROR_INVALID_URI;
+       http_free_uri(&http_uri);
+       free(cddb_url);
+       cddb_url = xstrdup(val);
+       return 0;
+}
+
+static int libcdio_get_cddb_url(char **val)
+{
+       if (!cddb_url)
+               cddb_url = xstrdup("freedb.freedb.org:8880");
+       *val = xstrdup(cddb_url);
+       return 0;
+}
+#endif
+
+const struct input_plugin_ops ip_ops = {
+       .open = libcdio_open,
+       .close = libcdio_close,
+       .read = libcdio_read,
+       .seek = libcdio_seek,
+       .read_comments = libcdio_read_comments,
+       .duration = libcdio_duration,
+       .bitrate = libcdio_bitrate,
+       .codec = libcdio_codec,
+       .codec_profile = libcdio_codec_profile,
+};
+
+const struct input_plugin_opt ip_options[] = {
+#ifdef HAVE_CDDB
+       { "cddb_url", libcdio_set_cddb_url, libcdio_get_cddb_url },
+#endif
+       { NULL },
+};
+
+const int ip_priority = 50;
+const char * const ip_extensions[] = { NULL };
+const char * const ip_mime_types[] = { "x-content/audio-cdda", NULL };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/ip/cue.c b/ip/cue.c
new file mode 100644 (file)
index 0000000..93991c9
--- /dev/null
+++ b/ip/cue.c
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2008-2013 Various Authors
+ * Copyright (C) 2011 Gregory Petrosyan
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "debug.h"
+#include "input.h"
+#include "utils.h"
+#include "comment.h"
+#include "xmalloc.h"
+#include "cue_utils.h"
+#include "cue.h"
+
+#include <stdio.h>
+#include <fcntl.h>
+#include <math.h>
+
+struct cue_private {
+       struct input_plugin *child;
+
+       char *cue_filename;
+       int track_n;
+
+       double start_offset;
+       double current_offset;
+       double end_offset;
+};
+
+
+static int _parse_cue_url(const char *url, char **filename, int *track_n)
+{
+       const char *slash;
+       long n;
+
+       if (!is_cue_url(url))
+               return 1;
+
+       url += 6;
+
+       slash = strrchr(url, '/');
+       if (!slash)
+               return 1;
+
+       if (str_to_int(slash + 1, &n) != 0)
+               return 1;
+
+       *filename = xstrndup(url, slash - url);
+       *track_n = n;
+       return 0;
+}
+
+
+static char *_make_absolute_path(const char *abs_filename, const char *rel_filename)
+{
+       char *s;
+       const char *slash;
+       char buf[4096] = {0};
+
+       slash = strrchr(abs_filename, '/');
+       if (slash == NULL)
+               return xstrdup(rel_filename);
+
+       s = xstrndup(abs_filename, slash - abs_filename);
+       snprintf(buf, sizeof buf, "%s/%s", s, rel_filename);
+
+       free(s);
+       return xstrdup(buf);
+}
+
+
+static int cue_open(struct input_plugin_data *ip_data)
+{
+       int rc;
+       char *child_filename;
+       struct cue_sheet *cd;
+       struct cue_track *t;
+       struct cue_private *priv;
+
+       priv = xnew(struct cue_private, 1);
+
+       rc = _parse_cue_url(ip_data->filename, &priv->cue_filename, &priv->track_n);
+       if (rc) {
+               rc = -IP_ERROR_INVALID_URI;
+               goto url_parse_failed;
+       }
+
+       cd = cue_from_file(priv->cue_filename);
+       if (cd == NULL) {
+               rc = -IP_ERROR_FILE_FORMAT;
+               goto cue_parse_failed;
+       }
+
+       t = cue_get_track(cd, priv->track_n);
+       if (!t) {
+               rc = -IP_ERROR_FILE_FORMAT;
+               goto cue_read_failed;
+       }
+
+       child_filename = _make_absolute_path(priv->cue_filename, cd->file);
+       priv->child = ip_new(child_filename);
+       free(child_filename);
+
+       rc = ip_open(priv->child);
+       if (rc)
+               goto ip_open_failed;
+
+       ip_setup(priv->child);
+
+       priv->start_offset = t->offset;
+       priv->current_offset = t->offset;
+
+       rc = ip_seek(priv->child, priv->start_offset);
+       if (rc)
+               goto ip_open_failed;
+
+       if (t->length >= 0)
+               priv->end_offset = priv->start_offset + t->length;
+       else
+               priv->end_offset = ip_duration(priv->child);
+
+       ip_data->fd = open(ip_get_filename(priv->child), O_RDONLY);
+       if (ip_data->fd == -1)
+               goto ip_open_failed;
+
+       ip_data->private = priv;
+       ip_data->sf = ip_get_sf(priv->child);
+       ip_get_channel_map(priv->child, ip_data->channel_map);
+
+       cue_free(cd);
+       return 0;
+
+ip_open_failed:
+       ip_delete(priv->child);
+
+cue_read_failed:
+       cue_free(cd);
+
+cue_parse_failed:
+       free(priv->cue_filename);
+
+url_parse_failed:
+       free(priv);
+
+       return rc;
+}
+
+
+static int cue_close(struct input_plugin_data *ip_data)
+{
+       struct cue_private *priv = ip_data->private;
+
+       close(ip_data->fd);
+       ip_data->fd = -1;
+
+       ip_delete(priv->child);
+       free(priv->cue_filename);
+
+       free(priv);
+       ip_data->private = NULL;
+
+       return 0;
+}
+
+
+static int cue_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       int rc;
+       sample_format_t sf;
+       double len;
+       double rem_len;
+       struct cue_private *priv = ip_data->private;
+
+       if (priv->current_offset >= priv->end_offset)
+               return 0;
+
+       rc = ip_read(priv->child, buffer, count);
+       if (rc <= 0)
+               return rc;
+
+       sf = ip_get_sf(priv->child);
+       len = (double)rc / sf_get_second_size(sf);
+
+       rem_len = priv->end_offset - priv->current_offset;
+       priv->current_offset += len;
+
+       if (priv->current_offset >= priv->end_offset)
+               rc = lround(rem_len * sf_get_rate(sf)) * sf_get_frame_size(sf);
+
+       return rc;
+}
+
+
+static int cue_seek(struct input_plugin_data *ip_data, double offset)
+{
+       struct cue_private *priv = ip_data->private;
+       double new_offset = priv->start_offset + offset;
+
+       if (new_offset > priv->end_offset)
+               new_offset = priv->end_offset;
+
+       priv->current_offset = new_offset;
+
+       return ip_seek(priv->child, new_offset);
+}
+
+
+static int cue_read_comments(struct input_plugin_data *ip_data, struct keyval **comments)
+{
+       struct cue_private *priv = ip_data->private;
+       struct cue_sheet *cd = cue_from_file(priv->cue_filename);
+       struct cue_track *t;
+       int rc;
+       char buf[32] = { 0 };
+       GROWING_KEYVALS(c);
+
+       if (cd == NULL) {
+               rc = -IP_ERROR_FILE_FORMAT;
+               goto cue_parse_failed;
+       }
+
+       t = cue_get_track(cd, priv->track_n);
+       if (!t) {
+               rc = -IP_ERROR_FILE_FORMAT;
+               goto get_track_failed;
+       }
+
+       snprintf(buf, sizeof buf, "%d", priv->track_n);
+       comments_add_const(&c, "tracknumber", buf);
+
+       if (t->meta.title)
+               comments_add_const(&c, "title", t->meta.title);
+       if (cd->meta.title)
+               comments_add_const(&c, "album", cd->meta.title);
+       if (t->meta.performer)
+               comments_add_const(&c, "artist", t->meta.performer);
+       if (cd->meta.performer)
+               comments_add_const(&c, "albumartist", cd->meta.performer);
+       if (t->meta.date)
+               comments_add_const(&c, "date", t->meta.date);
+       else if (cd->meta.date)
+               comments_add_const(&c, "date", cd->meta.date);
+       if (cd->meta.compilation)
+               comments_add_const(&c, "compilation", cd->meta.compilation);
+
+       /*
+        * TODO:
+        * - replaygain REMs
+        * - genre?
+        */
+
+       keyvals_terminate(&c);
+       *comments = c.keyvals;
+
+       cue_free(cd);
+       return 0;
+
+get_track_failed:
+       cue_free(cd);
+
+cue_parse_failed:
+       return rc;
+}
+
+
+static int cue_duration(struct input_plugin_data *ip_data)
+{
+       struct cue_private *priv = ip_data->private;
+
+       return priv->end_offset - priv->start_offset;
+}
+
+
+static long cue_bitrate(struct input_plugin_data *ip_data)
+{
+       struct cue_private *priv = ip_data->private;
+
+       return ip_bitrate(priv->child);
+}
+
+
+static long cue_current_bitrate(struct input_plugin_data *ip_data)
+{
+       struct cue_private *priv = ip_data->private;
+
+       return ip_current_bitrate(priv->child);
+}
+
+
+static char *cue_codec(struct input_plugin_data *ip_data)
+{
+       struct cue_private *priv = ip_data->private;
+
+       return ip_codec(priv->child);
+}
+
+
+static char *cue_codec_profile(struct input_plugin_data *ip_data)
+{
+       struct cue_private *priv = ip_data->private;
+
+       return ip_codec_profile(priv->child);
+}
+
+
+const struct input_plugin_ops ip_ops = {
+       .open            = cue_open,
+       .close           = cue_close,
+       .read            = cue_read,
+       .seek            = cue_seek,
+       .read_comments   = cue_read_comments,
+       .duration        = cue_duration,
+       .bitrate         = cue_bitrate,
+       .bitrate_current = cue_current_bitrate,
+       .codec           = cue_codec,
+       .codec_profile   = cue_codec_profile,
+};
+
+const int ip_priority = 50;
+const char * const ip_extensions[] = { NULL };
+const char * const ip_mime_types[] = { "application/x-cue", NULL };
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/ip/ffmpeg.c b/ip/ffmpeg.c
new file mode 100644 (file)
index 0000000..eaad5c4
--- /dev/null
@@ -0,0 +1,488 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2007 Kevin Ko <kevin.s.ko@gmail.com>
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "xmalloc.h"
+#include "debug.h"
+#include "utils.h"
+#include "comment.h"
+#ifdef HAVE_CONFIG
+#include "config/ffmpeg.h"
+#endif
+
+#include <stdio.h>
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libavformat/avio.h>
+#include <libswresample/swresample.h>
+#include <libavutil/opt.h>
+#include <libavutil/channel_layout.h>
+#ifndef AVUTIL_MATHEMATICS_H
+#include <libavutil/mathematics.h>
+#endif
+
+#ifndef AVCODEC_MAX_AUDIO_FRAME_SIZE
+#define AVCODEC_MAX_AUDIO_FRAME_SIZE 192000
+#endif
+
+struct ffmpeg_input {
+       AVPacket pkt;
+       int curr_pkt_size;
+       uint8_t *curr_pkt_buf;
+       int stream_index;
+
+       unsigned long curr_size;
+       unsigned long curr_duration;
+};
+
+struct ffmpeg_output {
+       uint8_t *buffer;
+       uint8_t *buffer_malloc;
+       uint8_t *buffer_pos;    /* current buffer position */
+       int buffer_used_len;
+};
+
+struct ffmpeg_private {
+       AVCodecContext *codec_context;
+       AVFormatContext *input_context;
+       AVCodec *codec;
+       SwrContext *swr;
+
+       struct ffmpeg_input *input;
+       struct ffmpeg_output *output;
+};
+
+static struct ffmpeg_input *ffmpeg_input_create(void)
+{
+       struct ffmpeg_input *input = xnew(struct ffmpeg_input, 1);
+
+       if (av_new_packet(&input->pkt, 0) != 0) {
+               free(input);
+               return NULL;
+       }
+       input->curr_pkt_size = 0;
+       input->curr_pkt_buf = input->pkt.data;
+       return input;
+}
+
+static void ffmpeg_input_free(struct ffmpeg_input *input)
+{
+#if LIBAVCODEC_VERSION_MAJOR >= 56
+       av_packet_unref(&input->pkt);
+#else
+       av_free_packet(&input->pkt);
+#endif
+       free(input);
+}
+
+static struct ffmpeg_output *ffmpeg_output_create(void)
+{
+       struct ffmpeg_output *output = xnew(struct ffmpeg_output, 1);
+
+       output->buffer_malloc = xnew(uint8_t, AVCODEC_MAX_AUDIO_FRAME_SIZE + 15);
+       output->buffer = output->buffer_malloc;
+       /* align to 16 bytes so avcodec can SSE/Altivec/etc */
+       while ((intptr_t) output->buffer % 16)
+               output->buffer += 1;
+       output->buffer_pos = output->buffer;
+       output->buffer_used_len = 0;
+       return output;
+}
+
+static void ffmpeg_output_free(struct ffmpeg_output *output)
+{
+       free(output->buffer_malloc);
+       output->buffer_malloc = NULL;
+       output->buffer = NULL;
+       free(output);
+}
+
+static inline void ffmpeg_buffer_flush(struct ffmpeg_output *output)
+{
+       output->buffer_pos = output->buffer;
+       output->buffer_used_len = 0;
+}
+
+static void ffmpeg_init(void)
+{
+       static int inited = 0;
+
+       if (inited != 0)
+               return;
+       inited = 1;
+
+       av_log_set_level(AV_LOG_QUIET);
+
+       /* We could register decoders explicitly to save memory, but we have to
+        * be careful about compatibility. */
+       av_register_all();
+}
+
+static int ffmpeg_open(struct input_plugin_data *ip_data)
+{
+       struct ffmpeg_private *priv;
+       int err = 0;
+       int i;
+       int stream_index = -1;
+       int64_t channel_layout = 0;
+       AVCodec *codec;
+       AVCodecContext *cc = NULL;
+       AVFormatContext *ic = NULL;
+       SwrContext *swr = NULL;
+
+       ffmpeg_init();
+
+       err = avformat_open_input(&ic, ip_data->filename, NULL, NULL);
+       if (err < 0) {
+               d_print("av_open failed: %d\n", err);
+               return -IP_ERROR_FILE_FORMAT;
+       }
+
+       do {
+               err = avformat_find_stream_info(ic, NULL);
+               if (err < 0) {
+                       d_print("unable to find stream info: %d\n", err);
+                       err = -IP_ERROR_FILE_FORMAT;
+                       break;
+               }
+
+               for (i = 0; i < ic->nb_streams; i++) {
+                       cc = ic->streams[i]->codec;
+                       if (cc->codec_type == AVMEDIA_TYPE_AUDIO) {
+                               stream_index = i;
+                               break;
+                       }
+               }
+
+               if (stream_index == -1) {
+                       d_print("could not find audio stream\n");
+                       err = -IP_ERROR_FILE_FORMAT;
+                       break;
+               }
+
+               codec = avcodec_find_decoder(cc->codec_id);
+               if (!codec) {
+                       d_print("codec not found: %d, %s\n", cc->codec_id, avcodec_get_name(cc->codec_id));
+                       err = -IP_ERROR_UNSUPPORTED_FILE_TYPE;
+                       break;
+               }
+
+               if (codec->capabilities & AV_CODEC_CAP_TRUNCATED)
+                       cc->flags |= AV_CODEC_FLAG_TRUNCATED;
+
+               if (avcodec_open2(cc, codec, NULL) < 0) {
+                       d_print("could not open codec: %d, %s\n", cc->codec_id, avcodec_get_name(cc->codec_id));
+                       err = -IP_ERROR_UNSUPPORTED_FILE_TYPE;
+                       break;
+               }
+
+               /* We assume below that no more errors follow. */
+       } while (0);
+
+       if (err < 0) {
+               /* Clean up.  cc is never opened at this point.  (See above assumption.) */
+               avformat_close_input(&ic);
+               return err;
+       }
+
+       priv = xnew(struct ffmpeg_private, 1);
+       priv->codec_context = cc;
+       priv->input_context = ic;
+       priv->codec = codec;
+       priv->input = ffmpeg_input_create();
+       if (priv->input == NULL) {
+               avcodec_close(cc);
+               avformat_close_input(&ic);
+               free(priv);
+               return -IP_ERROR_INTERNAL;
+       }
+       priv->input->stream_index = stream_index;
+       priv->output = ffmpeg_output_create();
+
+       /* Prepare for resampling. */
+       swr = swr_alloc();
+       av_opt_set_int(swr, "in_channel_layout",  av_get_default_channel_layout(cc->channels), 0);
+       av_opt_set_int(swr, "out_channel_layout", av_get_default_channel_layout(cc->channels), 0);
+       av_opt_set_int(swr, "in_sample_rate",     cc->sample_rate, 0);
+       av_opt_set_int(swr, "out_sample_rate",    cc->sample_rate, 0);
+       av_opt_set_sample_fmt(swr, "in_sample_fmt",  cc->sample_fmt, 0);
+       priv->swr = swr;
+
+       ip_data->private = priv;
+       ip_data->sf = sf_rate(cc->sample_rate) | sf_channels(cc->channels);
+       switch (cc->sample_fmt) {
+       case AV_SAMPLE_FMT_U8:
+               ip_data->sf |= sf_bits(8) | sf_signed(0);
+               av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_U8,  0);
+               break;
+       case AV_SAMPLE_FMT_S32:
+               ip_data->sf |= sf_bits(32) | sf_signed(1);
+               av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_S32,  0);
+               break;
+       /* AV_SAMPLE_FMT_S16 */
+       default:
+               ip_data->sf |= sf_bits(16) | sf_signed(1);
+               av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_S16,  0);
+               break;
+       }
+       swr_init(swr);
+       ip_data->sf |= sf_host_endian();
+       channel_layout = cc->channel_layout;
+       channel_map_init_waveex(cc->channels, channel_layout, ip_data->channel_map);
+       return 0;
+}
+
+static int ffmpeg_close(struct input_plugin_data *ip_data)
+{
+       struct ffmpeg_private *priv = ip_data->private;
+
+       avcodec_close(priv->codec_context);
+       avformat_close_input(&priv->input_context);
+       swr_free(&priv->swr);
+       ffmpeg_input_free(priv->input);
+       ffmpeg_output_free(priv->output);
+       free(priv);
+       ip_data->private = NULL;
+       return 0;
+}
+
+/*
+ * This returns the number of bytes added to the buffer.
+ * It returns < 0 on error.  0 on EOF.
+ */
+static int ffmpeg_fill_buffer(AVFormatContext *ic, AVCodecContext *cc, struct ffmpeg_input *input,
+                             struct ffmpeg_output *output, SwrContext *swr)
+{
+#if LIBAVCODEC_VERSION_MAJOR >= 56
+       AVFrame *frame = av_frame_alloc();
+#else
+       AVFrame *frame = avcodec_alloc_frame();
+#endif
+       int got_frame;
+       while (1) {
+               int len;
+
+               if (input->curr_pkt_size <= 0) {
+#if LIBAVCODEC_VERSION_MAJOR >= 56
+                       av_packet_unref(&input->pkt);
+#else
+                       av_free_packet(&input->pkt);
+#endif
+                       if (av_read_frame(ic, &input->pkt) < 0) {
+                               /* Force EOF once we can read no longer. */
+#if LIBAVCODEC_VERSION_MAJOR >= 56
+                               av_frame_free(&frame);
+#else
+                               avcodec_free_frame(&frame);
+#endif
+                               return 0;
+                       }
+                       if (input->pkt.stream_index == input->stream_index) {
+                               input->curr_pkt_size = input->pkt.size;
+                               input->curr_pkt_buf = input->pkt.data;
+                               input->curr_size += input->pkt.size;
+                               input->curr_duration += input->pkt.duration;
+                       }
+                       continue;
+               }
+
+               {
+                       AVPacket avpkt;
+                       av_new_packet(&avpkt, input->curr_pkt_size);
+                       memcpy(avpkt.data, input->curr_pkt_buf, input->curr_pkt_size);
+                       len = avcodec_decode_audio4(cc, frame, &got_frame, &avpkt);
+#if LIBAVCODEC_VERSION_MAJOR >= 56
+                       av_packet_unref(&avpkt);
+#else
+                       av_free_packet(&avpkt);
+#endif
+               }
+               if (len < 0) {
+                       /* this is often reached when seeking, not sure why */
+                       input->curr_pkt_size = 0;
+                       continue;
+               }
+               input->curr_pkt_size -= len;
+               input->curr_pkt_buf += len;
+               if (got_frame) {
+                       int res = swr_convert(swr,
+                                       &output->buffer,
+                                       frame->nb_samples,
+                                       (const uint8_t **)frame->extended_data,
+                                       frame->nb_samples);
+                       if (res < 0)
+                               res = 0;
+                       output->buffer_pos = output->buffer;
+                       output->buffer_used_len = res * cc->channels * sizeof(int16_t);
+#if LIBAVCODEC_VERSION_MAJOR >= 56
+                       av_frame_free(&frame);
+#else
+                       avcodec_free_frame(&frame);
+#endif
+                       return output->buffer_used_len;
+               }
+       }
+       /* This should never get here. */
+       return -IP_ERROR_INTERNAL;
+}
+
+static int ffmpeg_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       struct ffmpeg_private *priv = ip_data->private;
+       struct ffmpeg_output *output = priv->output;
+       int rc;
+       int out_size;
+
+       if (output->buffer_used_len == 0) {
+               rc = ffmpeg_fill_buffer(priv->input_context, priv->codec_context,
+                               priv->input, priv->output, priv->swr);
+               if (rc <= 0) {
+                       return rc;
+               }
+       }
+       out_size = min_i(output->buffer_used_len, count);
+       memcpy(buffer, output->buffer_pos, out_size);
+       output->buffer_used_len -= out_size;
+       output->buffer_pos += out_size;
+       return out_size;
+}
+
+static int ffmpeg_seek(struct input_plugin_data *ip_data, double offset)
+{
+       struct ffmpeg_private *priv = ip_data->private;
+       AVStream *st = priv->input_context->streams[priv->input->stream_index];
+       int ret;
+
+       int64_t pts = av_rescale_q(offset * AV_TIME_BASE, AV_TIME_BASE_Q, st->time_base);
+
+       avcodec_flush_buffers(priv->codec_context);
+       /* Force reading a new packet in next ffmpeg_fill_buffer(). */
+       priv->input->curr_pkt_size = 0;
+
+       ret = av_seek_frame(priv->input_context, priv->input->stream_index, pts, 0);
+
+       if (ret < 0) {
+               return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+       } else {
+               ffmpeg_buffer_flush(priv->output);
+               return 0;
+       }
+}
+
+static void ffmpeg_read_metadata(struct growing_keyvals *c, AVDictionary *metadata)
+{
+       AVDictionaryEntry *tag = NULL;
+
+       while ((tag = av_dict_get(metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) {
+               if (tag->value[0])
+                       comments_add_const(c, tag->key, tag->value);
+       }
+}
+
+static int ffmpeg_read_comments(struct input_plugin_data *ip_data, struct keyval **comments)
+{
+       struct ffmpeg_private *priv = ip_data->private;
+       AVFormatContext *ic = priv->input_context;
+
+       GROWING_KEYVALS(c);
+
+       ffmpeg_read_metadata(&c, ic->metadata);
+       for (unsigned i = 0; i < ic->nb_streams; i++) {
+               ffmpeg_read_metadata(&c, ic->streams[i]->metadata);
+       }
+
+       keyvals_terminate(&c);
+       *comments = c.keyvals;
+
+       return 0;
+}
+
+static int ffmpeg_duration(struct input_plugin_data *ip_data)
+{
+       struct ffmpeg_private *priv = ip_data->private;
+       return priv->input_context->duration / AV_TIME_BASE;
+}
+
+static long ffmpeg_bitrate(struct input_plugin_data *ip_data)
+{
+       struct ffmpeg_private *priv = ip_data->private;
+       long bitrate = priv->input_context->bit_rate;
+       return bitrate ? bitrate : -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+}
+
+static long ffmpeg_current_bitrate(struct input_plugin_data *ip_data)
+{
+       struct ffmpeg_private *priv = ip_data->private;
+       AVStream *st = priv->input_context->streams[priv->input->stream_index];
+       long bitrate = -1;
+       /* ape codec returns silly numbers */
+#if LIBAVCODEC_VERSION_MAJOR >= 55
+       if (priv->codec->id == AV_CODEC_ID_APE)
+#else
+       if (priv->codec->id == CODEC_ID_APE)
+#endif
+               return -1;
+       if (priv->input->curr_duration > 0) {
+               double seconds = priv->input->curr_duration * av_q2d(st->time_base);
+               bitrate = (8 * priv->input->curr_size) / seconds;
+               priv->input->curr_size = 0;
+               priv->input->curr_duration = 0;
+       }
+       return bitrate;
+}
+
+static char *ffmpeg_codec(struct input_plugin_data *ip_data)
+{
+       struct ffmpeg_private *priv = ip_data->private;
+       return xstrdup(priv->codec->name);
+}
+
+static char *ffmpeg_codec_profile(struct input_plugin_data *ip_data)
+{
+       struct ffmpeg_private *priv = ip_data->private;
+       const char *profile;
+       profile = av_get_profile_name(priv->codec, priv->codec_context->profile);
+       return profile ? xstrdup(profile) : NULL;
+}
+
+const struct input_plugin_ops ip_ops = {
+       .open = ffmpeg_open,
+       .close = ffmpeg_close,
+       .read = ffmpeg_read,
+       .seek = ffmpeg_seek,
+       .read_comments = ffmpeg_read_comments,
+       .duration = ffmpeg_duration,
+       .bitrate = ffmpeg_bitrate,
+       .bitrate_current = ffmpeg_current_bitrate,
+       .codec = ffmpeg_codec,
+       .codec_profile = ffmpeg_codec_profile
+};
+
+const int ip_priority = 30;
+const char *const ip_extensions[] = {
+       "aa", "aac", "ac3", "aif", "aifc", "aiff", "ape", "au", "fla", "flac",
+       "m4a", "m4b", "mka", "mkv", "mp+", "mp2", "mp3", "mp4", "mpc", "mpp",
+       "ogg", "shn", "tak", "tta", "wav", "webm", "wma", "wv",
+#ifdef USE_FALLBACK_IP
+       "*",
+#endif
+       NULL
+};
+const char *const ip_mime_types[] = { NULL };
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/ip/flac.c b/ip/flac.c
new file mode 100644 (file)
index 0000000..90a97b6
--- /dev/null
+++ b/ip/flac.c
@@ -0,0 +1,482 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "comment.h"
+#include "xmalloc.h"
+#include "debug.h"
+#include "utils.h"
+
+#include <FLAC/export.h>
+#include <FLAC/stream_decoder.h>
+#include <FLAC/metadata.h>
+#include <stdint.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <errno.h>
+
+#ifndef UINT64_MAX
+#define UINT64_MAX ((uint64_t)-1)
+#endif
+
+/* Reduce typing.  Namespaces are nice but FLAC API is fscking ridiculous.  */
+
+/* functions, types, enums */
+#define F(s) FLAC__stream_decoder_ ## s
+#define T(s) FLAC__StreamDecoder ## s
+#define Dec FLAC__StreamDecoder
+#define E(s) FLAC__STREAM_DECODER_ ## s
+
+struct flac_private {
+       /* file/stream position and length */
+       uint64_t pos;
+       uint64_t len;
+
+       Dec *dec;
+
+       /* PCM data */
+       char *buf;
+       unsigned int buf_size;
+       unsigned int buf_wpos;
+       unsigned int buf_rpos;
+
+       struct keyval *comments;
+       double duration;
+       long bitrate;
+};
+
+static T(ReadStatus) read_cb(const Dec *dec, unsigned char *buf, size_t *size, void *data)
+{
+       struct input_plugin_data *ip_data = data;
+       struct flac_private *priv = ip_data->private;
+       int rc;
+
+       if (priv->pos == priv->len) {
+               *size = 0;
+               return E(READ_STATUS_END_OF_STREAM);
+       }
+       if (*size == 0)
+               return E(READ_STATUS_CONTINUE);
+
+       rc = read(ip_data->fd, buf, *size);
+       if (rc == -1) {
+               *size = 0;
+               if (errno == EINTR || errno == EAGAIN) {
+                       /* FIXME: not sure how the flac decoder handles this */
+                       d_print("interrupted\n");
+                       return E(READ_STATUS_CONTINUE);
+               }
+               return E(READ_STATUS_ABORT);
+       }
+
+       priv->pos += rc;
+       *size = rc;
+       if (rc == 0) {
+               /* should not happen */
+               return E(READ_STATUS_END_OF_STREAM);
+       }
+       return E(READ_STATUS_CONTINUE);
+}
+
+static T(SeekStatus) seek_cb(const Dec *dec, uint64_t offset, void *data)
+{
+       struct input_plugin_data *ip_data = data;
+       struct flac_private *priv = ip_data->private;
+       off_t off;
+
+       if (priv->len == UINT64_MAX)
+               return E(SEEK_STATUS_ERROR);
+       off = lseek(ip_data->fd, offset, SEEK_SET);
+       if (off == -1) {
+               return E(SEEK_STATUS_ERROR);
+       }
+       priv->pos = off;
+       return E(SEEK_STATUS_OK);
+}
+
+static T(TellStatus) tell_cb(const Dec *dec, uint64_t *offset, void *data)
+{
+       struct input_plugin_data *ip_data = data;
+       struct flac_private *priv = ip_data->private;
+
+       d_print("\n");
+       *offset = priv->pos;
+       return E(TELL_STATUS_OK);
+}
+
+static T(LengthStatus) length_cb(const Dec *dec, uint64_t *len, void *data)
+{
+       struct input_plugin_data *ip_data = data;
+       struct flac_private *priv = ip_data->private;
+
+       d_print("\n");
+       if (ip_data->remote) {
+               return E(LENGTH_STATUS_ERROR);
+       }
+       *len = priv->len;
+       return E(LENGTH_STATUS_OK);
+}
+
+static int eof_cb(const Dec *dec, void *data)
+{
+       struct input_plugin_data *ip_data = data;
+       struct flac_private *priv = ip_data->private;
+
+       return priv->pos == priv->len;;
+}
+
+#if defined(WORDS_BIGENDIAN)
+
+#define LE32(x) swap_uint32(x)
+
+#else
+
+#define LE32(x)        (x)
+
+#endif
+
+static FLAC__StreamDecoderWriteStatus write_cb(const Dec *dec, const FLAC__Frame *frame,
+               const int32_t * const *buf, void *data)
+{
+       struct input_plugin_data *ip_data = data;
+       struct flac_private *priv = ip_data->private;
+       int frames, bytes, size, channels, bits, depth;
+       int ch, nch, i = 0;
+       char *dest; int32_t src;
+
+       frames = frame->header.blocksize;
+       channels = sf_get_channels(ip_data->sf);
+       bits = sf_get_bits(ip_data->sf);
+       bytes = frames * bits / 8 * channels;
+       size = priv->buf_size;
+
+       if (size - priv->buf_wpos < bytes) {
+               if (size < bytes)
+                       size = bytes;
+               size *= 2;
+               priv->buf = xrenew(char, priv->buf, size);
+               priv->buf_size = size;
+       }
+
+       depth = frame->header.bits_per_sample;
+       if (!depth)
+               depth = bits;
+       nch = frame->header.channels;
+       dest = priv->buf + priv->buf_wpos;
+       for (i = 0; i < frames; i++) {
+               for (ch = 0; ch < channels; ch++) {
+                       src = LE32(buf[ch % nch][i] << (bits -depth));
+                       memcpy(dest, &src, bits / 8);
+                       dest += bits / 8;
+               }
+       }
+
+       priv->buf_wpos += bytes;
+       return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
+}
+
+/* You should make a copy of metadata with FLAC__metadata_object_clone() if you will
+ * need it elsewhere. Since metadata blocks can potentially be large, by
+ * default the decoder only calls the metadata callback for the STREAMINFO
+ * block; you can instruct the decoder to pass or filter other blocks with
+ * FLAC__stream_decoder_set_metadata_*() calls.
+ */
+static void metadata_cb(const Dec *dec, const FLAC__StreamMetadata *metadata, void *data)
+{
+       struct input_plugin_data *ip_data = data;
+       struct flac_private *priv = ip_data->private;
+
+       switch (metadata->type) {
+       case FLAC__METADATA_TYPE_STREAMINFO:
+               {
+                       const FLAC__StreamMetadata_StreamInfo *si = &metadata->data.stream_info;
+                       int bits = 0;
+
+                       switch (si->bits_per_sample) {
+                       case 8:
+                               bits = 8;
+                               break;
+                       case 12:
+                       case 16:
+                               bits = 16;
+                               break;
+                       case 20:
+                       case 24:
+                               bits = 24;
+                               break;
+                       case 32:
+                               bits = 32;
+                               break;
+                       }
+
+                       ip_data->sf = sf_rate(si->sample_rate) |
+                               sf_bits(bits) |
+                               sf_signed(1) |
+                               sf_channels(si->channels);
+                       if (!ip_data->remote && si->total_samples) {
+                               priv->duration = (double) si->total_samples / si->sample_rate;
+                               if (priv->duration >= 1 && priv->len >= 1)
+                                       priv->bitrate = priv->len * 8 / priv->duration;
+                       }
+               }
+               break;
+       case FLAC__METADATA_TYPE_VORBIS_COMMENT:
+               d_print("VORBISCOMMENT\n");
+               if (priv->comments) {
+                       d_print("Ignoring\n");
+               } else {
+                       GROWING_KEYVALS(c);
+                       int i, nr;
+
+                       nr = metadata->data.vorbis_comment.num_comments;
+                       for (i = 0; i < nr; i++) {
+                               const char *str = (const char *)metadata->data.vorbis_comment.comments[i].entry;
+                               char *key, *val;
+
+                               val = strchr(str, '=');
+                               if (!val)
+                                       continue;
+                               key = xstrndup(str, val - str);
+                               val = xstrdup(val + 1);
+                               comments_add(&c, key, val);
+                               free(key);
+                       }
+                       keyvals_terminate(&c);
+                       priv->comments = c.keyvals;
+               }
+               break;
+       default:
+               d_print("something else\n");
+               break;
+       }
+}
+
+static void error_cb(const Dec *dec, FLAC__StreamDecoderErrorStatus status, void *data)
+{
+       d_print("FLAC error: %s\n", FLAC__StreamDecoderErrorStatusString[status]);
+}
+
+static void free_priv(struct input_plugin_data *ip_data)
+{
+       struct flac_private *priv = ip_data->private;
+       int save = errno;
+
+       F(finish)(priv->dec);
+       F(delete)(priv->dec);
+       if (priv->comments)
+               keyvals_free(priv->comments);
+       free(priv->buf);
+       free(priv);
+       ip_data->private = NULL;
+       errno = save;
+}
+
+/* http://flac.sourceforge.net/format.html#frame_header */
+static void channel_map_init_flac(int channels, channel_position_t *map)
+{
+       unsigned int mask = 0;
+       if (channels == 4)
+               mask = 0x33; // 0b110011, without center and lfe
+       else if (channels == 5)
+               mask = 0x37; // 0b110111, without lfe
+       channel_map_init_waveex(channels, mask, map);
+}
+
+static int flac_open(struct input_plugin_data *ip_data)
+{
+       struct flac_private *priv;
+
+       Dec *dec = F(new)();
+
+       const struct flac_private priv_init = {
+               .dec      = dec,
+               .duration = -1,
+               .bitrate  = -1
+       };
+
+       if (!dec)
+               return -IP_ERROR_INTERNAL;
+
+       priv = xnew(struct flac_private, 1);
+       *priv = priv_init;
+       if (ip_data->remote) {
+               priv->len = UINT64_MAX;
+       } else {
+               off_t off = lseek(ip_data->fd, 0, SEEK_END);
+
+               if (off == -1 || lseek(ip_data->fd, 0, SEEK_SET) == -1) {
+                       int save = errno;
+
+                       F(delete)(dec);
+                       free(priv);
+                       errno = save;
+                       return -IP_ERROR_ERRNO;
+               }
+               priv->len = off;
+       }
+       ip_data->private = priv;
+
+       FLAC__stream_decoder_set_metadata_respond_all(dec);
+       if (FLAC__stream_decoder_init_stream(dec, read_cb, seek_cb, tell_cb,
+                               length_cb, eof_cb, write_cb, metadata_cb,
+                               error_cb, ip_data) != E(INIT_STATUS_OK)) {
+               int save = errno;
+
+               d_print("init failed\n");
+               F(delete)(priv->dec);
+               free(priv);
+               ip_data->private = NULL;
+               errno = save;
+               return -IP_ERROR_ERRNO;
+       }
+
+       ip_data->sf = 0;
+       if (!F(process_until_end_of_metadata)(priv->dec)) {
+               free_priv(ip_data);
+               return -IP_ERROR_ERRNO;
+       }
+
+       if (!ip_data->sf) {
+               free_priv(ip_data);
+               return -IP_ERROR_FILE_FORMAT;
+       }
+       if (!sf_get_bits(ip_data->sf)) {
+               free_priv(ip_data);
+               return -IP_ERROR_SAMPLE_FORMAT;
+       }
+
+       channel_map_init_flac(sf_get_channels(ip_data->sf), ip_data->channel_map);
+       d_print("sr: %d, ch: %d, bits: %d\n",
+                       sf_get_rate(ip_data->sf),
+                       sf_get_channels(ip_data->sf),
+                       sf_get_bits(ip_data->sf));
+       return 0;
+}
+
+static int flac_close(struct input_plugin_data *ip_data)
+{
+       free_priv(ip_data);
+       return 0;
+}
+
+static int flac_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       struct flac_private *priv = ip_data->private;
+       int avail;
+
+       while (1) {
+               avail = priv->buf_wpos - priv->buf_rpos;
+               BUG_ON(avail < 0);
+               if (avail > 0)
+                       break;
+               FLAC__bool internal_error = !F(process_single)(priv->dec);
+               FLAC__StreamDecoderState state = F(get_state)(priv->dec);
+               if (state == E(END_OF_STREAM))
+                       return 0;
+               if (state == E(ABORTED) || state == E(OGG_ERROR) || internal_error) {
+                       d_print("process_single failed\n");
+                       return -1;
+               }
+       }
+       if (count > avail)
+               count = avail;
+       memcpy(buffer, priv->buf + priv->buf_rpos, count);
+       priv->buf_rpos += count;
+       BUG_ON(priv->buf_rpos > priv->buf_wpos);
+       if (priv->buf_rpos == priv->buf_wpos) {
+               priv->buf_rpos = 0;
+               priv->buf_wpos = 0;
+       }
+       return count;
+}
+
+/* Flush the input and seek to an absolute sample. Decoding will resume at the
+ * given sample.
+ */
+static int flac_seek(struct input_plugin_data *ip_data, double offset)
+{
+       struct flac_private *priv = ip_data->private;
+       priv->buf_rpos = 0;
+       priv->buf_wpos = 0;
+       uint64_t sample;
+
+       sample = (uint64_t)(offset * (double)sf_get_rate(ip_data->sf) + 0.5);
+       if (!F(seek_absolute)(priv->dec, sample)) {
+               if (F(get_state(priv->dec)) == FLAC__STREAM_DECODER_SEEK_ERROR) {
+                       if (!F(flush)(priv->dec))
+                               d_print("failed to flush\n");
+               }
+               return -IP_ERROR_ERRNO;
+       }
+       return 0;
+}
+
+static int flac_read_comments(struct input_plugin_data *ip_data, struct keyval **comments)
+{
+       struct flac_private *priv = ip_data->private;
+
+       if (priv->comments) {
+               *comments = keyvals_dup(priv->comments);
+       } else {
+               *comments = keyvals_new(0);
+       }
+       return 0;
+}
+
+static int flac_duration(struct input_plugin_data *ip_data)
+{
+       struct flac_private *priv = ip_data->private;
+
+       return priv->duration;
+}
+
+static long flac_bitrate(struct input_plugin_data *ip_data)
+{
+       struct flac_private *priv = ip_data->private;
+       return priv->bitrate;
+}
+
+static char *flac_codec(struct input_plugin_data *ip_data)
+{
+       return xstrdup("flac");
+}
+
+static char *flac_codec_profile(struct input_plugin_data *ip_data)
+{
+       /* maybe identify compression-level over min/max blocksize/framesize */
+       return NULL;
+}
+
+const struct input_plugin_ops ip_ops = {
+       .open = flac_open,
+       .close = flac_close,
+       .read = flac_read,
+       .seek = flac_seek,
+       .read_comments = flac_read_comments,
+       .duration = flac_duration,
+       .bitrate = flac_bitrate,
+       .bitrate_current = flac_bitrate,
+       .codec = flac_codec,
+       .codec_profile = flac_codec_profile
+};
+
+const int ip_priority = 50;
+const char * const ip_extensions[] = { "flac", "fla", NULL };
+const char * const ip_mime_types[] = { NULL };
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/ip/mad.c b/ip/mad.c
new file mode 100644 (file)
index 0000000..b7b92ae
--- /dev/null
+++ b/ip/mad.c
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "nomad.h"
+#include "id3.h"
+#include "ape.h"
+#include "xmalloc.h"
+#include "read_wrapper.h"
+#include "debug.h"
+#include "utils.h"
+#include "comment.h"
+
+#include <stdio.h>
+#include <math.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+/* ------------------------------------------------------------------------- */
+
+static ssize_t read_func(void *datasource, void *buffer, size_t count)
+{
+       struct input_plugin_data *ip_data = datasource;
+
+       return read_wrapper(ip_data, buffer, count);
+}
+
+static off_t lseek_func(void *datasource, off_t offset, int whence)
+{
+       struct input_plugin_data *ip_data = datasource;
+
+       return lseek(ip_data->fd, offset, whence);
+}
+
+static int close_func(void *datasource)
+{
+       struct input_plugin_data *ip_data = datasource;
+
+       return close(ip_data->fd);
+}
+
+static struct nomad_callbacks callbacks = {
+       .read = read_func,
+       .lseek = lseek_func,
+       .close = close_func
+};
+
+/* ------------------------------------------------------------------------- */
+
+static int mad_open(struct input_plugin_data *ip_data)
+{
+       struct nomad *nomad;
+       const struct nomad_info *info;
+       int rc;
+
+       rc = nomad_open_callbacks(&nomad, ip_data, &callbacks);
+       switch (rc) {
+       case -NOMAD_ERROR_ERRNO:
+               return -1;
+       case -NOMAD_ERROR_FILE_FORMAT:
+               return -IP_ERROR_FILE_FORMAT;
+       }
+       ip_data->private = nomad;
+
+       info = nomad_info(nomad);
+
+       /* always 16-bit signed little-endian */
+       ip_data->sf = sf_rate(info->sample_rate) | sf_channels(info->channels) |
+               sf_bits(16) | sf_signed(1);
+       channel_map_init_waveex(info->channels, 0, ip_data->channel_map);
+       return 0;
+}
+
+static int mad_close(struct input_plugin_data *ip_data)
+{
+       struct nomad *nomad;
+
+       nomad = ip_data->private;
+       nomad_close(nomad);
+       ip_data->fd = -1;
+       ip_data->private = NULL;
+       return 0;
+}
+
+static int mad_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       struct nomad *nomad;
+
+       nomad = ip_data->private;
+       return nomad_read(nomad, buffer, count);
+}
+
+static int mad_seek(struct input_plugin_data *ip_data, double offset)
+{
+       struct nomad *nomad;
+
+       nomad = ip_data->private;
+       return nomad_time_seek(nomad, offset);
+}
+
+static int mad_read_comments(struct input_plugin_data *ip_data,
+               struct keyval **comments)
+{
+       struct nomad *nomad = ip_data->private;
+       const struct nomad_lame *lame = nomad_lame(nomad);
+       struct id3tag id3;
+       int fd, rc, save, i;
+       APETAG(ape);
+       GROWING_KEYVALS(c);
+
+       fd = open(ip_data->filename, O_RDONLY);
+       if (fd == -1) {
+               return -1;
+       }
+       d_print("filename: %s\n", ip_data->filename);
+
+       id3_init(&id3);
+       rc = id3_read_tags(&id3, fd, ID3_V1 | ID3_V2);
+       save = errno;
+       close(fd);
+       errno = save;
+       if (rc) {
+               if (rc == -1) {
+                       d_print("error: %s\n", strerror(errno));
+                       return -1;
+               }
+               d_print("corrupted tag?\n");
+               goto next;
+       }
+
+       for (i = 0; i < NUM_ID3_KEYS; i++) {
+               char *val = id3_get_comment(&id3, i);
+
+               if (val)
+                       comments_add(&c, id3_key_names[i], val);
+       }
+
+next:
+       id3_free(&id3);
+
+       rc = ape_read_tags(&ape, ip_data->fd, 0);
+       if (rc < 0)
+               goto out;
+
+       for (i = 0; i < rc; i++) {
+               char *k, *v;
+               k = ape_get_comment(&ape, &v);
+               if (!k)
+                       break;
+               comments_add(&c, k, v);
+               free(k);
+       }
+
+out:
+       ape_free(&ape);
+
+       /* add last so the other tags get preference */
+       if (lame && !isnan(lame->trackGain)) {
+               char buf[64];
+
+               if (!isnan(lame->peak)) {
+                       sprintf(buf, "%f", lame->peak);
+                       comments_add_const(&c, "replaygain_track_peak", buf);
+               }
+               sprintf(buf, "%+.1f dB", lame->trackGain);
+               comments_add_const(&c, "replaygain_track_gain", buf);
+       }
+
+       keyvals_terminate(&c);
+       *comments = c.keyvals;
+       return 0;
+}
+
+static int mad_duration(struct input_plugin_data *ip_data)
+{
+       struct nomad *nomad = ip_data->private;
+
+       return nomad_info(nomad)->duration;
+}
+
+static long mad_bitrate(struct input_plugin_data *ip_data)
+{
+       struct nomad *nomad = ip_data->private;
+       long bitrate = nomad_info(nomad)->avg_bitrate;
+
+       return bitrate != -1 ? bitrate : -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+}
+
+static long mad_current_bitrate(struct input_plugin_data *ip_data)
+{
+       struct nomad *nomad = ip_data->private;
+       return nomad_current_bitrate(nomad);
+}
+
+static char *mad_codec(struct input_plugin_data *ip_data)
+{
+       struct nomad *nomad = ip_data->private;
+
+       switch (nomad_info(nomad)->layer) {
+       case 3:
+               return xstrdup("mp3");
+       case 2:
+               return xstrdup("mp2");
+       case 1:
+               return xstrdup("mp1");
+       }
+       return NULL;
+}
+
+static char *mad_codec_profile(struct input_plugin_data *ip_data)
+{
+       struct nomad *nomad = ip_data->private;
+       const struct nomad_lame *lame = nomad_lame(nomad);
+       const char *mode = nomad_info(nomad)->vbr ? "VBR" : "CBR";
+
+       if (lame) {
+               /* LAME:
+                * 0: unknown
+                * 1: cbr
+                * 2: abr
+                * 3: vbr rh (--vbr-old)
+                * 4: vbr mtrh (--vbr-new)
+                * 5: vbr mt (obsolete)
+                */
+               int method = lame->vbr_method;
+               if (method == 2)
+                       mode = "ABR";
+               else if (method >= 3 && method <= 5) {
+                       const struct nomad_xing *xing = nomad_xing(nomad);
+
+                       if (xing && xing->flags & XING_SCALE && xing->scale && xing->scale <= 100) {
+                               char buf[16];
+                               int v = 10 - (xing->scale + 9) / 10;
+                               /* quality (-q): 10 - (xing->scale - ((9 - v) * 10)) */
+
+                               sprintf(buf, "VBR V%d", v);
+                               return xstrdup(buf);
+
+                       }
+               }
+       }
+
+       return xstrdup(mode);
+}
+
+const struct input_plugin_ops ip_ops = {
+       .open = mad_open,
+       .close = mad_close,
+       .read = mad_read,
+       .seek = mad_seek,
+       .read_comments = mad_read_comments,
+       .duration = mad_duration,
+       .bitrate = mad_bitrate,
+       .bitrate_current = mad_current_bitrate,
+       .codec = mad_codec,
+       .codec_profile = mad_codec_profile
+};
+
+const int ip_priority = 55;
+const char * const ip_extensions[] = { "mp3", "mp2", NULL };
+const char * const ip_mime_types[] = {
+       "audio/mpeg", "audio/x-mp3", "audio/x-mpeg", NULL
+};
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/ip/mikmod.c b/ip/mikmod.c
new file mode 100644 (file)
index 0000000..4ff4479
--- /dev/null
@@ -0,0 +1,176 @@
+/*
+ * Adapted from AlsaPlayer 0.99.76
+ *
+ * mikmod_engine.c
+ * Copyright (C) 1999 Paul N. Fisher <rao@gnu.org>
+ * Copyright (C) 2002 Andy Lo A Foe <andy@alsaplayer.org>
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "xmalloc.h"
+#include <mikmod.h>
+#include "debug.h"
+#include "comment.h"
+
+struct mik_private {
+       MODULE *file;
+};
+
+static int mikmod_init(void)
+{
+       static int inited = 0;
+
+       if (inited)
+               return 1;
+
+       MikMod_RegisterAllDrivers();
+       MikMod_RegisterAllLoaders();
+
+       md_reverb = 0;
+       /* we should let the user decide which one is better... */
+       md_mode = DMODE_SOFT_MUSIC | DMODE_SOFT_SNDFX | DMODE_STEREO |
+               DMODE_16BITS | DMODE_INTERP;
+
+       if (MikMod_Init(NULL)) {
+               d_print("Could not initialize mikmod, reason: %s\n",
+                               MikMod_strerror(MikMod_errno));
+               return 0;
+       }
+
+       inited = 1;
+       return 1;
+}
+
+static int mik_open(struct input_plugin_data *ip_data)
+{
+       MODULE *mf = NULL;
+       struct mik_private *priv;
+       int mi = mikmod_init();
+
+       if (!mi)
+               return -IP_ERROR_INTERNAL;
+
+       mf = Player_Load(ip_data->filename, 255, 0);
+
+       if (!mf)
+               return -IP_ERROR_ERRNO;
+
+       priv = xnew(struct mik_private, 1);
+       priv->file = mf;
+
+       ip_data->private = priv;
+       ip_data->sf = sf_bits(16) | sf_rate(44100) | sf_channels(2) | sf_signed(1);
+       ip_data->sf |= sf_host_endian();
+       channel_map_init_stereo(ip_data->channel_map);
+       return 0;
+}
+
+static int mik_close(struct input_plugin_data *ip_data)
+{
+       struct mik_private *priv = ip_data->private;
+
+       Player_Stop();
+       Player_Free(priv->file);
+       free(ip_data->private);
+       ip_data->private = NULL;
+       return 0;
+}
+
+static int mik_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       int length;
+       struct mik_private *priv = ip_data->private;
+
+       if (!Player_Active())
+               Player_Start(priv->file);
+
+       if (!Player_Active())
+               return 0;
+
+       length = VC_WriteBytes(buffer, count);
+
+       return length;
+}
+
+static int mik_seek(struct input_plugin_data *ip_data, double offset)
+{
+       /* cannot seek in modules properly */
+       return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+}
+
+static int mik_read_comments(struct input_plugin_data *ip_data, struct keyval **comments)
+{
+       struct mik_private *priv = ip_data->private;
+       GROWING_KEYVALS(c);
+       const char *val;
+
+       val = priv->file->songname;
+       if (val && val[0])
+               comments_add_const(&c, "title", val);
+
+       val = priv->file->comment;
+       if (val && val[0])
+               comments_add_const(&c, "comment", val);
+
+       keyvals_terminate(&c);
+       *comments = c.keyvals;
+       return 0;
+}
+
+static int mik_duration(struct input_plugin_data *ip_data)
+{
+       return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+}
+
+static long mik_bitrate(struct input_plugin_data *ip_data)
+{
+       return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+}
+
+static char *mik_codec(struct input_plugin_data *ip_data)
+{
+       struct mik_private *priv = ip_data->private;
+       const char *codec = priv->file->modtype;
+       return (codec && codec[0]) ? xstrdup(codec) : NULL;
+}
+
+static char *mik_codec_profile(struct input_plugin_data *ip_data)
+{
+       return NULL;
+}
+
+const struct input_plugin_ops ip_ops = {
+       .open = mik_open,
+       .close = mik_close,
+       .read = mik_read,
+       .seek = mik_seek,
+       .read_comments = mik_read_comments,
+       .duration = mik_duration,
+       .bitrate = mik_bitrate,
+       .bitrate_current = mik_bitrate,
+       .codec = mik_codec,
+       .codec_profile = mik_codec_profile
+};
+
+const int ip_priority = 40;
+const char * const ip_extensions[] = {
+       "mod", "s3m", "xm", "it", "669", "amf", "dsm",
+       "far", "med", "mtm", "stm", "ult",
+       NULL
+};
+const char * const ip_mime_types[] = { NULL };
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/ip/modplug.c b/ip/modplug.c
new file mode 100644 (file)
index 0000000..be0bf31
--- /dev/null
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "file.h"
+#include "xmalloc.h"
+#include "comment.h"
+#ifdef HAVE_CONFIG
+#include "config/modplug.h"
+#endif
+
+#include <libmodplug/modplug.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+
+struct mod_private {
+       ModPlugFile *file;
+};
+
+static int mod_open(struct input_plugin_data *ip_data)
+{
+       struct mod_private *priv;
+       char *contents;
+       off_t size;
+       ssize_t rc;
+       ModPlugFile *file;
+       ModPlug_Settings settings;
+
+       size = lseek(ip_data->fd, 0, SEEK_END);
+       if (size == -1)
+               return -IP_ERROR_ERRNO;
+       if (lseek(ip_data->fd, 0, SEEK_SET) == -1)
+               return -IP_ERROR_ERRNO;
+
+       contents = xnew(char, size);
+       rc = read_all(ip_data->fd, contents, size);
+       if (rc == -1) {
+               int save = errno;
+
+               free(contents);
+               errno = save;
+               return -IP_ERROR_ERRNO;
+       }
+       if (rc != size) {
+               free(contents);
+               return -IP_ERROR_FILE_FORMAT;
+       }
+       errno = 0;
+       file = ModPlug_Load(contents, size);
+       if (file == NULL) {
+               int save = errno;
+
+               free(contents);
+               errno = save;
+               if (errno == 0) {
+                       /* libmodplug never sets errno? */
+                       return -IP_ERROR_FILE_FORMAT;
+               }
+               return -IP_ERROR_ERRNO;
+       }
+       free(contents);
+
+       priv = xnew(struct mod_private, 1);
+       priv->file = file;
+
+       ModPlug_GetSettings(&settings);
+       settings.mFlags = MODPLUG_ENABLE_OVERSAMPLING | MODPLUG_ENABLE_NOISE_REDUCTION;
+/*     settings.mFlags |= MODPLUG_ENABLE_REVERB; */
+/*     settings.mFlags |= MODPLUG_ENABLE_MEGABASS; */
+/*     settings.mFlags |= MODPLUG_ENABLE_SURROUND; */
+       settings.mChannels = 2;
+       settings.mBits = 16;
+       settings.mFrequency = 44100;
+       settings.mResamplingMode = MODPLUG_RESAMPLE_FIR;
+       ModPlug_SetSettings(&settings);
+
+       ip_data->private = priv;
+       ip_data->sf = sf_bits(16) | sf_rate(44100) | sf_channels(2) | sf_signed(1);
+       ip_data->sf |= sf_host_endian();
+       channel_map_init_stereo(ip_data->channel_map);
+       return 0;
+}
+
+static int mod_close(struct input_plugin_data *ip_data)
+{
+       struct mod_private *priv = ip_data->private;
+
+       ModPlug_Unload(priv->file);
+       free(priv);
+       ip_data->private = NULL;
+       return 0;
+}
+
+static int mod_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       struct mod_private *priv = ip_data->private;
+       int rc;
+
+       errno = 0;
+       rc = ModPlug_Read(priv->file, buffer, count);
+       if (rc < 0) {
+               if (errno == 0)
+                       return -IP_ERROR_INTERNAL;
+               return -IP_ERROR_ERRNO;
+       }
+       return rc;
+}
+
+static int mod_seek(struct input_plugin_data *ip_data, double offset)
+{
+       struct mod_private *priv = ip_data->private;
+       int ms = (int)(offset * 1000.0 + 0.5);
+
+       /* void */
+       ModPlug_Seek(priv->file, ms);
+       return 0;
+}
+
+static int mod_read_comments(struct input_plugin_data *ip_data, struct keyval **comments)
+{
+       struct mod_private *priv = ip_data->private;
+       GROWING_KEYVALS(c);
+       const char *val;
+
+       val = ModPlug_GetName(priv->file);
+       if (val && val[0])
+               comments_add_const(&c, "title", val);
+
+#if MODPLUG_API_8
+       val = ModPlug_GetMessage(priv->file);
+       if (val && val[0])
+               comments_add_const(&c, "comment", val);
+#endif
+
+       keyvals_terminate(&c);
+       *comments = c.keyvals;
+       return 0;
+}
+
+static int mod_duration(struct input_plugin_data *ip_data)
+{
+       struct mod_private *priv = ip_data->private;
+
+       return (ModPlug_GetLength(priv->file) + 500) / 1000;
+}
+
+static long mod_bitrate(struct input_plugin_data *ip_data)
+{
+       return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+}
+
+#if MODPLUG_API_8
+static const char *mod_type_to_string(int type)
+{
+       /* from <libmodplug/sndfile.h>, which is C++ */
+       switch (type) {
+       case 0x01:      return "mod";
+       case 0x02:      return "s3m";
+       case 0x04:      return "xm";
+       case 0x08:      return "med";
+       case 0x10:      return "mtm";
+       case 0x20:      return "it";
+       case 0x40:      return "699";
+       case 0x80:      return "ult";
+       case 0x100:     return "stm";
+       case 0x200:     return "far";
+       case 0x800:     return "amf";
+       case 0x1000:    return "ams";
+       case 0x2000:    return "dsm";
+       case 0x4000:    return "mdl";
+       case 0x8000:    return "okt";
+       case 0x10000:   return "midi";
+       case 0x20000:   return "dmf";
+       case 0x40000:   return "ptm";
+       case 0x80000:   return "dbm";
+       case 0x100000:  return "mt2";
+       case 0x200000:  return "amf0";
+       case 0x400000:  return "psm";
+       case 0x80000000:return "umx";
+       }
+       return NULL;
+}
+#endif
+
+static char *mod_codec(struct input_plugin_data *ip_data)
+{
+#if MODPLUG_API_8
+       struct mod_private *priv = ip_data->private;
+       const char *codec;
+       int type;
+
+       type = ModPlug_GetModuleType(priv->file);
+       codec = mod_type_to_string(type);
+
+       return codec ? xstrdup(codec) : NULL;
+#else
+       return NULL;
+#endif
+}
+
+static char *mod_codec_profile(struct input_plugin_data *ip_data)
+{
+       return NULL;
+}
+
+const struct input_plugin_ops ip_ops = {
+       .open = mod_open,
+       .close = mod_close,
+       .read = mod_read,
+       .seek = mod_seek,
+       .read_comments = mod_read_comments,
+       .duration = mod_duration,
+       .bitrate = mod_bitrate,
+       .bitrate_current = mod_bitrate,
+       .codec = mod_codec,
+       .codec_profile = mod_codec_profile
+};
+
+const int ip_priority = 50;
+const char * const ip_extensions[] = {
+       "mod", "s3m", "xm", "it", "669", "amf", "ams", "dbm", "dmf", "dsm",
+       "far", "mdl", "med", "mtm", "okt", "ptm", "stm", "ult", "umx", "mt2",
+       "psm", NULL
+};
+const char * const ip_mime_types[] = { NULL };
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/ip/mp4.c b/ip/mp4.c
new file mode 100644 (file)
index 0000000..d25a24b
--- /dev/null
+++ b/ip/mp4.c
@@ -0,0 +1,604 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 dnk <dnk@bjum.net>
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "xmalloc.h"
+#include "debug.h"
+#include "id3.h"
+#include "file.h"
+#ifdef HAVE_CONFIG
+#include "config/mp4.h"
+#endif
+#include "comment.h"
+#include "aac.h"
+
+#if USE_MPEG4IP
+#include <mp4.h>
+#else
+#include <mp4v2/mp4v2.h>
+#endif
+
+#include <neaacdec.h>
+
+#include <errno.h>
+#include <string.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <strings.h>
+
+struct mp4_private {
+       char *overflow_buf;
+       int overflow_buf_len;
+
+       unsigned char channels;
+       unsigned long sample_rate;
+
+       NeAACDecHandle decoder;         /* typedef void * */
+
+       struct {
+               MP4FileHandle handle;   /* typedef void * */
+
+               MP4TrackId track;
+               MP4SampleId sample;
+               MP4SampleId num_samples;
+       } mp4;
+
+       struct {
+               unsigned long samples;
+               unsigned long bytes;
+       } current;
+};
+
+
+static MP4TrackId mp4_get_track(MP4FileHandle *handle)
+{
+       MP4TrackId num_tracks;
+       const char *track_type;
+       uint8_t obj_type;
+       MP4TrackId i;
+
+       num_tracks = MP4GetNumberOfTracks(handle, NULL, 0);
+
+       for (i = 1; i <= num_tracks; i++) {
+               track_type = MP4GetTrackType(handle, i);
+               if (!track_type)
+                       continue;
+
+               if (!MP4_IS_AUDIO_TRACK_TYPE(track_type))
+                       continue;
+
+               /* MP4GetTrackAudioType */
+               obj_type = MP4GetTrackEsdsObjectTypeId(handle, i);
+               if (obj_type == MP4_INVALID_AUDIO_TYPE)
+                       continue;
+
+               if (obj_type == MP4_MPEG4_AUDIO_TYPE) {
+                       obj_type = MP4GetTrackAudioMpeg4Type(handle, i);
+
+                       if (MP4_IS_MPEG4_AAC_AUDIO_TYPE(obj_type))
+                               return i;
+               } else {
+                       if (MP4_IS_AAC_AUDIO_TYPE(obj_type))
+                               return i;
+               }
+       }
+
+       return MP4_INVALID_TRACK_ID;
+}
+
+static void mp4_get_channel_map(struct input_plugin_data *ip_data)
+{
+       struct mp4_private *priv = ip_data->private;
+       unsigned char *aac_data = NULL;
+       unsigned int aac_data_len = 0;
+       NeAACDecFrameInfo frame_info;
+       int i;
+
+       ip_data->channel_map[0] = CHANNEL_POSITION_INVALID;
+
+       if (MP4ReadSample(priv->mp4.handle, priv->mp4.track, priv->mp4.sample,
+                       &aac_data, &aac_data_len, NULL, NULL, NULL, NULL) == 0)
+               return;
+
+       if (!aac_data)
+               return;
+
+       NeAACDecDecode(priv->decoder, &frame_info, aac_data, aac_data_len);
+       free(aac_data);
+
+       if (frame_info.error != 0 || frame_info.bytesconsumed <= 0
+                       || frame_info.channels > CHANNELS_MAX)
+               return;
+
+       for (i = 0; i < frame_info.channels; i++)
+               ip_data->channel_map[i] = channel_position_aac(frame_info.channel_position[i]);
+}
+
+static void mp4_close_handle(MP4FileHandle handle)
+{
+#ifdef MP4_CLOSE_DO_NOT_COMPUTE_BITRATE
+       MP4Close(handle, 0);
+#else
+       MP4Close(handle);
+#endif
+}
+
+static int mp4_open(struct input_plugin_data *ip_data)
+{
+       struct mp4_private *priv;
+       NeAACDecConfigurationPtr neaac_cfg;
+       unsigned char *buf;
+       unsigned int buf_size;
+       int rc = -IP_ERROR_FILE_FORMAT;
+
+       const struct mp4_private priv_init = {
+               .decoder = NULL
+       };
+
+       /* http://sourceforge.net/forum/message.php?msg_id=3578887 */
+       if (ip_data->remote)
+               return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+
+       /* kindly ask mp4v2 to not spam stderr */
+       MP4LogSetLevel(MP4_LOG_NONE);
+
+       /* init private struct */
+       priv = xnew(struct mp4_private, 1);
+       *priv = priv_init;
+       ip_data->private = priv;
+
+       priv->decoder = NeAACDecOpen();
+
+       /* set decoder config */
+       neaac_cfg = NeAACDecGetCurrentConfiguration(priv->decoder);
+       neaac_cfg->outputFormat = FAAD_FMT_16BIT;       /* force 16 bit audio */
+       neaac_cfg->downMatrix = 0;                      /* NOT 5.1 -> stereo */
+       NeAACDecSetConfiguration(priv->decoder, neaac_cfg);
+
+       /* open mpeg-4 file */
+#ifdef MP4_DETAILS_ALL
+       priv->mp4.handle = MP4Read(ip_data->filename, 0);
+#else
+       priv->mp4.handle = MP4Read(ip_data->filename);
+#endif
+       if (!priv->mp4.handle) {
+               d_print("MP4Read failed\n");
+               goto out;
+       }
+
+       /* find aac audio track */
+       priv->mp4.track = mp4_get_track(priv->mp4.handle);
+       if (priv->mp4.track == MP4_INVALID_TRACK_ID) {
+               d_print("MP4FindTrackId failed\n");
+               if (MP4GetNumberOfTracks(priv->mp4.handle, MP4_AUDIO_TRACK_TYPE, 0) > 0)
+                       rc = -IP_ERROR_UNSUPPORTED_FILE_TYPE;
+               goto out;
+       }
+
+       priv->mp4.num_samples = MP4GetTrackNumberOfSamples(priv->mp4.handle, priv->mp4.track);
+
+       priv->mp4.sample = 1;
+
+       buf = NULL;
+       buf_size = 0;
+       if (!MP4GetTrackESConfiguration(priv->mp4.handle, priv->mp4.track, &buf, &buf_size)) {
+               /* failed to get mpeg-4 audio config... this is ok.
+                * NeAACDecInit2() will simply use default values instead.
+                */
+               buf = NULL;
+               buf_size = 0;
+       }
+
+       /* init decoder according to mpeg-4 audio config */
+       if (NeAACDecInit2(priv->decoder, buf, buf_size, &priv->sample_rate, &priv->channels) < 0) {
+               free(buf);
+               goto out;
+       }
+
+       free(buf);
+
+       d_print("sample rate %luhz, channels %u\n", priv->sample_rate, priv->channels);
+
+       ip_data->sf = sf_rate(priv->sample_rate) | sf_channels(priv->channels) | sf_bits(16) | sf_signed(1);
+       ip_data->sf |= sf_host_endian();
+       mp4_get_channel_map(ip_data);
+
+       return 0;
+
+out:
+       if (priv->mp4.handle)
+               mp4_close_handle(priv->mp4.handle);
+       if (priv->decoder)
+               NeAACDecClose(priv->decoder);
+       free(priv);
+       return rc;
+}
+
+static int mp4_close(struct input_plugin_data *ip_data)
+{
+       struct mp4_private *priv;
+
+       priv = ip_data->private;
+
+       if (priv->mp4.handle)
+               mp4_close_handle(priv->mp4.handle);
+
+       if (priv->decoder)
+               NeAACDecClose(priv->decoder);
+
+       free(priv);
+       ip_data->private = NULL;
+
+       return 0;
+}
+
+/* returns -1 on fatal errors
+ * returns -2 on non-fatal errors
+ * 0 on eof
+ * number of bytes put in 'buffer' on success */
+static int decode_one_frame(struct input_plugin_data *ip_data, void *buffer, int count)
+{
+       struct mp4_private *priv;
+       unsigned char *aac_data = NULL;
+       unsigned int aac_data_len = 0;
+       NeAACDecFrameInfo frame_info;
+       char *sample_buf;
+       int bytes;
+
+       priv = ip_data->private;
+
+       BUG_ON(priv->overflow_buf_len);
+
+       if (priv->mp4.sample > priv->mp4.num_samples)
+               return 0; /* EOF */
+
+       if (MP4ReadSample(priv->mp4.handle, priv->mp4.track, priv->mp4.sample,
+               &aac_data, &aac_data_len, NULL, NULL, NULL, NULL) == 0) {
+               d_print("error reading mp4 sample %d\n", priv->mp4.sample);
+               errno = EINVAL;
+               return -1;
+       }
+
+       priv->mp4.sample++;
+
+       if (!aac_data) {
+               d_print("aac_data == NULL\n");
+               errno = EINVAL;
+               return -1;
+       }
+
+       sample_buf = NeAACDecDecode(priv->decoder, &frame_info, aac_data, aac_data_len);
+       if (frame_info.error == 0 && frame_info.samples > 0) {
+               priv->current.samples += frame_info.samples;
+               priv->current.bytes += frame_info.bytesconsumed;
+       }
+
+       free(aac_data);
+
+       if (!sample_buf || frame_info.bytesconsumed <= 0) {
+               d_print("fatal error: %s\n", NeAACDecGetErrorMessage(frame_info.error));
+               errno = EINVAL;
+               return -1;
+       }
+
+       if (frame_info.error != 0) {
+               d_print("frame error: %s\n", NeAACDecGetErrorMessage(frame_info.error));
+               return -2;
+       }
+
+       if (frame_info.samples <= 0)
+               return -2;
+
+       if (frame_info.channels != priv->channels || frame_info.samplerate != priv->sample_rate) {
+               d_print("invalid channel or sample_rate count\n");
+               return -2;
+       }
+
+       /* 16-bit samples */
+       bytes = frame_info.samples * 2;
+
+       if (bytes > count) {
+               /* decoded too much; keep overflow. */
+               priv->overflow_buf = sample_buf + count;
+               priv->overflow_buf_len = bytes - count;
+               memcpy(buffer, sample_buf, count);
+               return count;
+       } else {
+               memcpy(buffer, sample_buf, bytes);
+       }
+
+       return bytes;
+}
+
+static int mp4_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       struct mp4_private *priv;
+       int rc;
+
+       priv = ip_data->private;
+
+       /* use overflow from previous call (if any) */
+       if (priv->overflow_buf_len > 0) {
+               int len = priv->overflow_buf_len;
+
+               if (len > count)
+                       len = count;
+
+               memcpy(buffer, priv->overflow_buf, len);
+               priv->overflow_buf += len;
+               priv->overflow_buf_len -= len;
+
+               return len;
+       }
+
+       do {
+               rc = decode_one_frame(ip_data, buffer, count);
+       } while (rc == -2);
+
+       return rc;
+}
+
+static int mp4_seek(struct input_plugin_data *ip_data, double offset)
+{
+       struct mp4_private *priv;
+       MP4SampleId sample;
+       uint32_t scale;
+
+       priv = ip_data->private;
+
+       scale = MP4GetTrackTimeScale(priv->mp4.handle, priv->mp4.track);
+       if (scale == 0)
+               return -IP_ERROR_INTERNAL;
+
+       sample = MP4GetSampleIdFromTime(priv->mp4.handle, priv->mp4.track,
+               (MP4Timestamp)(offset * (double)scale), 0);
+       if (sample == MP4_INVALID_SAMPLE_ID)
+               return -IP_ERROR_INTERNAL;
+
+       priv->mp4.sample = sample;
+
+       d_print("seeking to sample %d\n", sample);
+
+       return 0;
+}
+
+static int mp4_read_comments(struct input_plugin_data *ip_data,
+               struct keyval **comments)
+{
+       struct mp4_private *priv;
+#if USE_MPEG4IP
+       uint16_t meta_num, meta_total;
+       uint8_t val;
+       char *str;
+       /*uint8_t *ustr;
+       uint32_t size;*/
+#else
+       const MP4Tags *tags;
+       MP4ItmfItemList* itmf_list;
+#endif
+       GROWING_KEYVALS(c);
+
+       priv = ip_data->private;
+
+#if USE_MPEG4IP
+       /* MP4GetMetadata* provides malloced pointers, and the data
+        * is in UTF-8 (or at least it should be). */
+       if (MP4GetMetadataArtist(priv->mp4.handle, &str))
+               comments_add(&c, "artist", str);
+       if (MP4GetMetadataAlbum(priv->mp4.handle, &str))
+               comments_add(&c, "album", str);
+       if (MP4GetMetadataName(priv->mp4.handle, &str))
+               comments_add(&c, "title", str);
+       if (MP4GetMetadataGenre(priv->mp4.handle, &str))
+               comments_add(&c, "genre", str);
+       if (MP4GetMetadataYear(priv->mp4.handle, &str))
+               comments_add(&c, "date", str);
+
+       if (MP4GetMetadataCompilation(priv->mp4.handle, &val))
+               comments_add_const(&c, "compilation", val ? "yes" : "no");
+#if 0
+       if (MP4GetBytesProperty(priv->mp4.handle, "moov.udta.meta.ilst.aART.data", &ustr, &size)) {
+               char *xstr;
+
+               /* What's this?
+                * This is the result from lack of documentation.
+                * It's supposed to return just a string, but it
+                * returns an additional 16 bytes of junk at the
+                * beginning. Could be a bug. Could be intentional.
+                * Hopefully this works around it:
+                */
+               if (ustr[0] == 0 && size > 16) {
+                       ustr += 16;
+                       size -= 16;
+               }
+               xstr = xnew(char, size + 1);
+               memcpy(xstr, ustr, size);
+               xstr[size] = 0;
+               comments_add(&c, "albumartist", xstr);
+               free(xstr);
+       }
+#endif
+       if (MP4GetMetadataTrack(priv->mp4.handle, &meta_num, &meta_total)) {
+               char buf[6];
+               snprintf(buf, 6, "%u", meta_num);
+               comments_add_const(&c, "tracknumber", buf);
+       }
+       if (MP4GetMetadataDisk(priv->mp4.handle, &meta_num, &meta_total)) {
+               char buf[6];
+               snprintf(buf, 6, "%u", meta_num);
+               comments_add_const(&c, "discnumber", buf);
+       }
+
+#else /* !USE_MPEG4IP, new interface */
+
+       tags = MP4TagsAlloc();
+
+       MP4TagsFetch(tags, priv->mp4.handle);
+
+       if (tags->artist)
+               comments_add_const(&c, "artist", tags->artist);
+       if (tags->albumArtist)
+               comments_add_const(&c, "albumartist", tags->albumArtist);
+       if (tags->sortArtist)
+               comments_add_const(&c, "artistsort", tags->sortArtist);
+       if (tags->sortAlbumArtist)
+               comments_add_const(&c, "albumartistsort", tags->sortAlbumArtist);
+       if (tags->sortAlbum)
+               comments_add_const(&c, "albumsort", tags->sortAlbum);
+       if (tags->album)
+               comments_add_const(&c, "album", tags->album);
+       if (tags->name)
+               comments_add_const(&c, "title", tags->name);
+       if (tags->genre) {
+               comments_add_const(&c, "genre", tags->genre);
+       } else if (tags->genreType) {
+               char const *genre = id3_get_genre(*tags->genreType - 1);
+               if (genre)
+                       comments_add_const(&c, "genre", genre);
+       }
+       if (tags->releaseDate && strcmp(tags->releaseDate, "0") != 0)
+               comments_add_const(&c, "date", tags->releaseDate);
+       if (tags->compilation)
+               comments_add_const(&c, "compilation", *tags->compilation ? "yes" : "no");
+       if (tags->track) {
+               char buf[6];
+               snprintf(buf, 6, "%u", tags->track->index);
+               comments_add_const(&c, "tracknumber", buf);
+       }
+       if (tags->disk) {
+               char buf[6];
+               snprintf(buf, 6, "%u", tags->disk->index);
+               comments_add_const(&c, "discnumber", buf);
+       }
+
+       MP4TagsFree(tags);
+
+       itmf_list = MP4ItmfGetItemsByMeaning(priv->mp4.handle, "com.apple.iTunes", NULL);
+       if (itmf_list) {
+               int i;
+               for (i = 0; i < itmf_list->size; i++) {
+                       MP4ItmfItem* item = &itmf_list->elements[i];
+                       if (item->dataList.size < 1)
+                               continue;
+                       if (item->dataList.size > 1)
+                               d_print("ignore multiple values for tag %s\n", item->name);
+                       else {
+                               MP4ItmfData* data = &item->dataList.elements[0];
+                               char *val = xstrndup(data->value, data->valueSize);
+                               comments_add(&c, item->name, val);
+                       }
+               }
+               MP4ItmfItemListFree(itmf_list);
+       }
+#endif
+
+       keyvals_terminate(&c);
+       *comments = c.keyvals;
+       return 0;
+}
+
+static int mp4_duration(struct input_plugin_data *ip_data)
+{
+       struct mp4_private *priv;
+       uint32_t scale;
+       uint64_t duration;
+
+       priv = ip_data->private;
+
+       scale = MP4GetTrackTimeScale(priv->mp4.handle, priv->mp4.track);
+       if (scale == 0)
+               return 0;
+
+       duration = MP4GetTrackDuration(priv->mp4.handle, priv->mp4.track);
+
+       return duration / scale;
+}
+
+static long mp4_bitrate(struct input_plugin_data *ip_data)
+{
+       struct mp4_private *priv = ip_data->private;
+       long bitrate = MP4GetTrackBitRate(priv->mp4.handle, priv->mp4.track);
+       return bitrate ? bitrate : -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+}
+
+static long mp4_current_bitrate(struct input_plugin_data *ip_data)
+{
+       struct mp4_private *priv = ip_data->private;
+       long bitrate = -1;
+       if (priv->current.samples > 0) {
+               priv->current.samples /= priv->channels;
+               bitrate = (8 * priv->current.bytes * priv->sample_rate) / priv->current.samples;
+               priv->current.samples = 0;
+               priv->current.bytes = 0;
+       }
+       return bitrate;
+}
+
+static char *mp4_codec(struct input_plugin_data *ip_data)
+{
+       return xstrdup("aac");
+}
+
+static const char *object_type_to_str(uint8_t obj_type)
+{
+       switch (obj_type) {
+       case MP4_MPEG4_AAC_MAIN_AUDIO_TYPE:     return "Main";
+       case MP4_MPEG4_AAC_LC_AUDIO_TYPE:       return "LC";
+       case MP4_MPEG4_AAC_SSR_AUDIO_TYPE:      return "SSR";
+       case MP4_MPEG4_AAC_LTP_AUDIO_TYPE:      return "LTP";
+#ifdef MP4_MPEG4_AAC_HE_AUDIO_TYPE
+       case MP4_MPEG4_AAC_HE_AUDIO_TYPE:       return "HE";
+#endif
+       case MP4_MPEG4_AAC_SCALABLE_AUDIO_TYPE: return "Scalable";
+       }
+       return NULL;
+}
+
+static char *mp4_codec_profile(struct input_plugin_data *ip_data)
+{
+       struct mp4_private *priv = ip_data->private;
+       const char *profile;
+       uint8_t obj_type;
+
+       obj_type = MP4GetTrackEsdsObjectTypeId(priv->mp4.handle, priv->mp4.track);
+       if (obj_type == MP4_MPEG4_AUDIO_TYPE)
+               obj_type = MP4GetTrackAudioMpeg4Type(priv->mp4.handle, priv->mp4.track);
+
+       profile = object_type_to_str(obj_type);
+
+       return profile ? xstrdup(profile) : NULL;
+}
+
+const struct input_plugin_ops ip_ops = {
+       .open = mp4_open,
+       .close = mp4_close,
+       .read = mp4_read,
+       .seek = mp4_seek,
+       .read_comments = mp4_read_comments,
+       .duration = mp4_duration,
+       .bitrate = mp4_bitrate,
+       .bitrate_current = mp4_current_bitrate,
+       .codec = mp4_codec,
+       .codec_profile = mp4_codec_profile
+};
+
+const int ip_priority = 50;
+const char * const ip_extensions[] = { "mp4", "m4a", "m4b", NULL };
+const char * const ip_mime_types[] = { /*"audio/mp4", "audio/mp4a-latm",*/ NULL };
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/ip/mpc.c b/ip/mpc.c
new file mode 100644 (file)
index 0000000..77361c4
--- /dev/null
+++ b/ip/mpc.c
@@ -0,0 +1,464 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 Chun-Yu Shei <cshei AT cs.indiana.edu>
+ *
+ * Cleaned up by Timo Hirvonen <tihirvon@gmail.com>
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "ape.h"
+#include "comment.h"
+#include "file.h"
+#include "xmalloc.h"
+#include "read_wrapper.h"
+
+#ifdef HAVE_CONFIG
+#include "config/mpc.h"
+#endif
+
+#if MPC_SV8
+#include <mpc/mpcdec.h>
+#define callback_t mpc_reader
+#define get_ip_data(d) (d)->data
+#else
+#include <mpcdec/mpcdec.h>
+#define MPC_FALSE FALSE
+#define MPC_TRUE TRUE
+#define callback_t void
+#define get_ip_data(d) (d)
+#endif
+
+#include <stdint.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <errno.h>
+
+struct mpc_private {
+#if MPC_SV8
+       mpc_demux *decoder;
+#else
+       mpc_decoder decoder;
+#endif
+       mpc_reader reader;
+       mpc_streaminfo info;
+
+       off_t file_size;
+
+       int samples_pos;
+       int samples_avail;
+
+       /* mpcdec/mpcdec.h
+        *
+        * the api doc says this is pcm samples per mpc frame
+        * but it's really pcm _frames_ per mpc frame
+        *     MPC_FRAME_LENGTH = 36 * 32 (1152)
+        *
+        * this is wrong, it should be 2 * MPC_FRAME_LENGTH (2304)
+        *     MPC_DECODER_BUFFER_LENGTH = 4 * MPC_FRAME_LENGTH (4608)
+        *
+        * use MPC_DECODER_BUFFER_LENGTH just to be sure it works
+        */
+       MPC_SAMPLE_FORMAT samples[MPC_DECODER_BUFFER_LENGTH];
+
+       struct {
+               unsigned long samples;
+               unsigned long bits;
+       } current;
+};
+
+/* callbacks */
+static mpc_int32_t read_impl(callback_t *data, void *ptr, mpc_int32_t size)
+{
+       struct input_plugin_data *ip_data = get_ip_data(data);
+       int rc;
+
+       rc = read_wrapper(ip_data, ptr, size);
+       if (rc == -1)
+               return -1;
+       if (rc == 0) {
+               errno = 0;
+               return 0;
+       }
+       return rc;
+}
+
+static mpc_bool_t seek_impl(callback_t *data, mpc_int32_t offset)
+{
+       struct input_plugin_data *ip_data = get_ip_data(data);
+
+       if (lseek(ip_data->fd, offset, SEEK_SET) == -1)
+               return MPC_FALSE;
+       return MPC_TRUE;
+}
+
+static mpc_int32_t tell_impl(callback_t *data)
+{
+       struct input_plugin_data *ip_data = get_ip_data(data);
+
+       return lseek(ip_data->fd, 0, SEEK_CUR);
+}
+
+static mpc_int32_t get_size_impl(callback_t *data)
+{
+       struct input_plugin_data *ip_data = get_ip_data(data);
+       struct mpc_private *priv = ip_data->private;
+
+       return priv->file_size;
+}
+
+static mpc_bool_t canseek_impl(callback_t *data)
+{
+       struct input_plugin_data *ip_data = get_ip_data(data);
+
+       return !ip_data->remote;
+}
+
+static int mpc_open(struct input_plugin_data *ip_data)
+{
+       struct mpc_private *priv;
+
+       const struct mpc_private priv_init = {
+               .file_size = -1,
+               /* set up an mpc_reader linked to our function implementations */
+               .reader = {
+                       .read     = read_impl,
+                       .seek     = seek_impl,
+                       .tell     = tell_impl,
+                       .get_size = get_size_impl,
+                       .canseek  = canseek_impl,
+                       .data     = ip_data
+               }
+       };
+
+       priv = xnew(struct mpc_private, 1);
+       *priv = priv_init;
+
+       if (!ip_data->remote) {
+               priv->file_size = lseek(ip_data->fd, 0, SEEK_END);
+               lseek(ip_data->fd, 0, SEEK_SET);
+       }
+
+       /* must be before mpc_streaminfo_read() */
+       ip_data->private = priv;
+
+       /* read file's streaminfo data */
+#if MPC_SV8
+       priv->decoder = mpc_demux_init(&priv->reader);
+       if (!priv->decoder) {
+#else
+       mpc_streaminfo_init(&priv->info);
+       if (mpc_streaminfo_read(&priv->info, &priv->reader) != ERROR_CODE_OK) {
+#endif
+               free(priv);
+               return -IP_ERROR_FILE_FORMAT;
+       }
+
+#if MPC_SV8
+       mpc_demux_get_info(priv->decoder, &priv->info);
+#else
+       /* instantiate a decoder with our file reader */
+       mpc_decoder_setup(&priv->decoder, &priv->reader);
+       if (!mpc_decoder_initialize(&priv->decoder, &priv->info)) {
+               free(priv);
+               return -IP_ERROR_FILE_FORMAT;
+       }
+#endif
+
+       priv->samples_avail = 0;
+       priv->samples_pos = 0;
+
+       ip_data->sf = sf_rate(priv->info.sample_freq) | sf_channels(priv->info.channels) |
+               sf_bits(16) | sf_signed(1);
+       channel_map_init_waveex(priv->info.channels, 0, ip_data->channel_map);
+       return 0;
+}
+
+static int mpc_close(struct input_plugin_data *ip_data)
+{
+       struct mpc_private *priv = ip_data->private;
+
+#if MPC_SV8
+       mpc_demux_exit(priv->decoder);
+#endif
+       free(priv);
+       ip_data->private = NULL;
+       return 0;
+}
+
+static int scale(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       struct mpc_private *priv = ip_data->private;
+       const MPC_SAMPLE_FORMAT *samples;
+       const int clip_min = (unsigned)-1 << (16 - 1);
+       const int clip_max = (1 << (16 - 1)) - 1;
+       const int float_scale = 1 << (16 - 1);
+       int i, sample_count;
+
+       /* number of bytes to 16-bit samples */
+       sample_count = count / 2;
+       if (sample_count > priv->samples_avail)
+               sample_count = priv->samples_avail;
+
+       /* scale 32-bit samples to 16-bit */
+       samples = priv->samples + priv->samples_pos;
+       for (i = 0; i < sample_count; i++) {
+               int val;
+
+               val = samples[i] * float_scale;
+               if (val < clip_min) {
+                       val = clip_min;
+               } else if (val > clip_max) {
+                       val = clip_max;
+               }
+
+               buffer[i * 2 + 0] = val & 0xff;
+               buffer[i * 2 + 1] = val >> 8;
+       }
+
+       priv->samples_pos += sample_count;
+       priv->samples_avail -= sample_count;
+       if (priv->samples_avail == 0)
+               priv->samples_pos = 0;
+
+       /* number of 16-bit samples to bytes */
+       return sample_count * 2;
+}
+
+static int mpc_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       struct mpc_private *priv = ip_data->private;
+
+#if MPC_SV8
+       mpc_status status;
+       mpc_frame_info frame;
+       int samples;
+
+       frame.buffer = priv->samples;
+
+       while (priv->samples_avail == 0) {
+               status = mpc_demux_decode(priv->decoder, &frame);
+
+               if (status != MPC_STATUS_OK) {
+                       return -IP_ERROR_ERRNO;
+               }
+               if (frame.bits == -1) {
+                       /* EOF */
+                       return 0;
+               }
+
+               samples = frame.samples;
+               priv->samples_avail = samples * priv->info.channels;
+
+               priv->current.samples += frame.samples;
+               priv->current.bits += frame.bits;
+       }
+#else
+
+       if (priv->samples_avail == 0) {
+               uint32_t acc = 0, bits = 0;
+               uint32_t status = mpc_decoder_decode(&priv->decoder, priv->samples, &acc, &bits);
+
+               if (status == (uint32_t)(-1)) {
+                       /* right ret val? */
+                       return -IP_ERROR_ERRNO;
+               }
+               if (status == 0) {
+                       /* EOF */
+                       return 0;
+               }
+
+               /* status seems to be number of _frames_
+                * the api documentation is wrong
+                */
+               priv->samples_avail = status * priv->info.channels;
+
+               priv->current.samples += status;
+               priv->current.bits += bits;
+       }
+#endif
+
+       return scale(ip_data, buffer, count);
+}
+
+static int mpc_seek(struct input_plugin_data *ip_data, double offset)
+{
+       struct mpc_private *priv = ip_data->private;
+
+       priv->samples_pos = 0;
+       priv->samples_avail = 0;
+
+#if MPC_SV8
+       if (mpc_demux_seek_second(priv->decoder, offset) == MPC_STATUS_OK)
+#else
+       if (mpc_decoder_seek_seconds(&priv->decoder, offset))
+#endif
+               return 0;
+       return -1;
+}
+
+static const char *gain_to_str(int gain)
+{
+       static char buf[16];
+#if MPC_SV8
+       float g = MPC_OLD_GAIN_REF - gain / 256.f;
+       sprintf(buf, "%.2f", g);
+#else
+       int b, a = gain / 100;
+
+       if (gain < 0) {
+               b = -gain % 100;
+       } else {
+               b = gain % 100;
+       }
+       sprintf(buf, "%d.%02d", a, b);
+#endif
+       return buf;
+}
+
+static const char *peak_to_str(unsigned int peak)
+{
+       static char buf[16];
+#if MPC_SV8
+       sprintf(buf, "%.5f", peak / 256.f / 100.f);
+#else
+       sprintf(buf, "%d.%05d", peak / 32767, peak % 32767);
+#endif
+       return buf;
+}
+
+static int mpc_read_comments(struct input_plugin_data *ip_data, struct keyval **comments)
+{
+       struct mpc_private *priv = ip_data->private;
+       GROWING_KEYVALS(c);
+       int count, i;
+       APETAG(ape);
+
+       count = ape_read_tags(&ape, ip_data->fd, 1);
+       if (count < 0)
+               goto out;
+
+       for (i = 0; i < count; i++) {
+               char *k, *v;
+               k = ape_get_comment(&ape, &v);
+               if (!k)
+                       break;
+               comments_add(&c, k, v);
+               free(k);
+       }
+
+out:
+       if (priv->info.gain_title && priv->info.peak_title) {
+               comments_add_const(&c, "replaygain_track_gain", gain_to_str(priv->info.gain_title));
+               comments_add_const(&c, "replaygain_track_peak", peak_to_str(priv->info.peak_title));
+       }
+       if (priv->info.gain_album && priv->info.peak_album) {
+               comments_add_const(&c, "replaygain_album_gain", gain_to_str(priv->info.gain_album));
+               comments_add_const(&c, "replaygain_album_peak", peak_to_str(priv->info.peak_album));
+       }
+       keyvals_terminate(&c);
+
+       *comments = c.keyvals;
+       ape_free(&ape);
+       return 0;
+}
+
+static int mpc_duration(struct input_plugin_data *ip_data)
+{
+       struct mpc_private *priv = ip_data->private;
+
+       /* priv->info.pcm_samples seems to be number of frames
+        * priv->info.frames is _not_ pcm frames
+        */
+#if MPC_SV8
+       return mpc_streaminfo_get_length(&priv->info);
+#else
+       return priv->info.pcm_samples / priv->info.sample_freq;
+#endif
+}
+
+static long mpc_bitrate(struct input_plugin_data *ip_data)
+{
+       struct mpc_private *priv = ip_data->private;
+       if (priv->info.average_bitrate)
+               return (long) (priv->info.average_bitrate + 0.5);
+       if (priv->info.bitrate)
+               return priv->info.bitrate;
+       return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+}
+
+static long mpc_current_bitrate(struct input_plugin_data *ip_data)
+{
+       struct mpc_private *priv = ip_data->private;
+       long bitrate = -1;
+       if (priv->current.samples > 0) {
+               bitrate = (priv->info.sample_freq * priv->current.bits) / priv->current.samples;
+               priv->current.samples = 0;
+               priv->current.bits = 0;
+       }
+       return bitrate;
+
+}
+
+static char *mpc_codec(struct input_plugin_data *ip_data)
+{
+       struct mpc_private *priv = ip_data->private;
+       switch (priv->info.stream_version) {
+       case 7:
+               return xstrdup("mpc7");
+       case 8:
+               return xstrdup("mpc8");
+       }
+       return NULL;
+}
+
+static char *mpc_codec_profile(struct input_plugin_data *ip_data)
+{
+       struct mpc_private *priv = ip_data->private;
+       const char *profile = priv->info.profile_name;
+       char *s = NULL;
+
+       if (profile && profile[0]) {
+               int i;
+
+               /* remove single quotes */
+               while (*profile == '\'')
+                       profile++;
+               s = xstrdup(profile);
+               for (i = strlen(s) - 1; s[i] == '\'' && i >= 0; i--)
+                       s[i] = '\0';
+       }
+
+       return s;
+}
+
+const struct input_plugin_ops ip_ops = {
+       .open = mpc_open,
+       .close = mpc_close,
+       .read = mpc_read,
+       .seek = mpc_seek,
+       .read_comments = mpc_read_comments,
+       .duration = mpc_duration,
+       .bitrate = mpc_bitrate,
+       .bitrate_current = mpc_current_bitrate,
+       .codec = mpc_codec,
+       .codec_profile = mpc_codec_profile
+};
+
+const int ip_priority = 50;
+const char *const ip_extensions[] = { "mpc", "mpp", "mp+", NULL };
+const char *const ip_mime_types[] = { "audio/x-musepack", NULL };
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/ip/nomad.c b/ip/nomad.c
new file mode 100644 (file)
index 0000000..f284d64
--- /dev/null
@@ -0,0 +1,871 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * Gapless decoding added by Chun-Yu Shei <cshei AT cs.indiana.edu>
+ */
+
+/*
+ * Xing code copied from xmms-mad plugin.
+ * Lame code copied from mpd
+ */
+
+#include "nomad.h"
+#include "id3.h"
+#include "xmalloc.h"
+#include "debug.h"
+#include "misc.h"
+
+#include <mad.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+
+#define INPUT_BUFFER_SIZE      (5 * 8192)
+#define SEEK_IDX_INTERVAL      15
+
+/* the number of samples of silence the decoder inserts at start */
+#define DECODERDELAY           529
+
+#define XING_MAGIC (('X' << 24) | ('i' << 16) | ('n' << 8) | 'g')
+#define INFO_MAGIC (('I' << 24) | ('n' << 16) | ('f' << 8) | 'o')
+
+struct seek_idx_entry {
+       off_t offset;
+       mad_timer_t timer;
+};
+
+struct nomad {
+       struct mad_stream stream;
+       struct mad_frame frame;
+       struct mad_synth synth;
+       mad_timer_t timer;
+       unsigned long cur_frame;
+       off_t input_offset;
+       /* MAD_BUFFER_GUARD zeros are required at the end of the stream to decode the last frame
+          ref: http://www.mars.org/mailman/public/mad-dev/2001-May/000262.html */
+       unsigned char input_buffer[INPUT_BUFFER_SIZE + MAD_BUFFER_GUARD];
+       int i;
+       unsigned int has_xing : 1;
+       unsigned int has_lame : 1;
+       unsigned int seen_first_frame : 1;
+       unsigned int readEOF : 1;
+       int start_drop_frames;
+       int start_drop_samples;
+       int end_drop_samples;
+       int end_drop_frames;
+
+       struct nomad_xing xing;
+       struct nomad_lame lame;
+
+       struct {
+               int size;
+               struct seek_idx_entry *table;
+       } seek_idx;
+
+       struct {
+               unsigned long long int bitrate_sum;
+               unsigned long nr_frames;
+       } current;
+
+       struct nomad_info info;
+       void *datasource;
+       int datasource_fd;
+       struct nomad_callbacks cbs;
+};
+
+static inline int scale(mad_fixed_t sample)
+{
+       sample += 1L << (MAD_F_FRACBITS - 16);
+       if (sample >= MAD_F_ONE) {
+               sample = MAD_F_ONE - 1;
+       } else if (sample < -MAD_F_ONE) {
+               sample = -MAD_F_ONE;
+       }
+       return sample >> (MAD_F_FRACBITS - 15);
+}
+
+static inline double timer_to_seconds(mad_timer_t timer)
+{
+       signed long ms;
+
+       ms = mad_timer_count(timer, MAD_UNITS_MILLISECONDS);
+       return (double)ms / 1000.0;
+}
+
+static int parse_lame(struct nomad *nomad, struct mad_bitptr ptr, int bitlen)
+{
+       int i, adj = 0;
+       unsigned int version_major, version_minor;
+       float val;
+
+       /* Unlike the xing header, the lame tag has a fixed length.  Fail if
+        * not all 36 bytes (288 bits) are there. */
+       if (bitlen < 288) return 0;
+
+       for (i = 0; i < 9; i++) nomad->lame.encoder[i] = (char)mad_bit_read(&ptr, 8);
+       nomad->lame.encoder[9] = '\0';
+
+       /* This is technically incorrect, since the encoder might not be lame.
+        * But there's no other way to determine if this is a lame tag, and we
+        * wouldn't want to go reading a tag that's not there. */
+       if (strncmp(nomad->lame.encoder, "LAME", 4) != 0) return 0;
+
+       if (sscanf(nomad->lame.encoder + 4, "%u.%u", &version_major, &version_minor) != 2)
+               return 0;
+
+#if defined(DEBUG_LAME)
+       d_print("detected LAME version %s\n", nomad->lame.encoder + 4);
+#endif
+
+       i = mad_bit_read(&ptr, 4);
+#if defined(DEBUG_LAME)
+       d_print("LAME tag revision: %d\n", i);
+#endif
+       nomad->lame.vbr_method = mad_bit_read(&ptr, 4);
+
+       /* ReplayGain in LAME tag was added in 3.94 */
+       if (version_major > 3 || (version_major == 3 && version_minor >= 94)) {
+               /* lowpass */
+               mad_bit_read(&ptr, 8);
+
+               /* The reference volume was changed from the 83dB used in the
+                * ReplayGain spec to 89dB in lame 3.95.1.  Bump the gain for older
+                * versions, since everyone else uses 89dB instead of 83dB.
+                * Unfortunately, lame didn't differentiate between 3.95 and 3.95.1, so
+                * it's impossible to make the proper adjustment for 3.95.
+                * Fortunately, 3.95 was only out for about a day before 3.95.1 was
+                * released. -- tmz */
+               if (version_major < 3 || (version_major == 3 && version_minor < 95))
+                       adj = 6;
+
+               val = mad_bit_read(&ptr, 32) / (float) (1 << 23);
+               /* peak value of 0.0 means lame didn't calculate the peak at all
+                * (--replaygain-fast), even silence has a value > 0.0 */
+               if (val)
+                       nomad->lame.peak = val;
+               for (i = 0; i < 2; i++) {
+                       int gain, gain_type;
+                       gain_type = replaygain_decode(mad_bit_read(&ptr, 16), &gain);
+                       val = gain / 10.f + adj;
+                       if (gain_type == 1)
+                               nomad->lame.trackGain = val;
+                       /* LAME currently doesn't store any album gain!
+                       else if (gain_type == 2)
+                               nomad->lame.albumGain = val;
+                       */
+               }
+
+               /*
+                * 4 encoding flags
+                * 4 ATH type
+                * 8 minimal bitrate (if ABR -> specified bitrate)
+                */
+               mad_bit_read(&ptr, 16);
+       } else
+               mad_bit_read(&ptr, 88);
+
+       nomad->lame.encoderDelay = mad_bit_read(&ptr, 12);
+       nomad->lame.encoderPadding = mad_bit_read(&ptr, 12);
+#if defined(DEBUG_LAME)
+       if (adj > 0)
+               d_print("adjusted gains by %+d dB (old LAME)\n", adj);
+       if (!isnan(nomad->lame.peak))
+               d_print("peak: %f\n", nomad->lame.peak);
+       if (!isnan(nomad->lame.trackGain))
+               d_print("trackGain: %+.1f dB\n", nomad->lame.trackGain);
+       if (!isnan(nomad->lame.albumGain))
+               d_print("albumGain: %+.1f dB\n", nomad->lame.albumGain);
+       d_print("encoderDelay: %d, encoderPadding: %d\n", nomad->lame.encoderDelay, nomad->lame.encoderPadding);
+#endif
+
+       mad_bit_read(&ptr, 96);
+
+       nomad->start_drop_frames = 1;   /* XING/LAME header is an empty frame */
+       nomad->start_drop_samples = nomad->lame.encoderDelay + DECODERDELAY;
+       nomad->end_drop_samples = nomad->lame.encoderPadding - DECODERDELAY;
+
+       nomad->has_lame = 1;
+
+       return 1;
+}
+
+/*
+ * format:
+ *
+ *   4 "Xing"
+ *   4 flags
+ *   4 frames (optional)
+ *   4 bytes  (optional)
+ * 100 TOC    (optional)
+ *   4 scale  (optional)
+ */
+static int xing_parse(struct nomad *nomad)
+{
+       struct mad_bitptr ptr = nomad->stream.anc_ptr;
+       struct mad_bitptr start = ptr;
+       int oldbitlen = nomad->stream.anc_bitlen;
+       int bitlen = nomad->stream.anc_bitlen;
+       int bitsleft;
+       unsigned xing_id;
+
+       nomad->has_xing = 0;
+       nomad->has_lame = 0;
+       if (bitlen < 64)
+               return -1;
+       xing_id = mad_bit_read(&ptr, 32);
+       if (xing_id != XING_MAGIC && xing_id != INFO_MAGIC) {
+               /*
+                * Due to an unfortunate historical accident, a Xing VBR tag
+                * may be misplaced in a stream with CRC protection. We check
+                * for this by assuming the tag began two octets prior and the
+                * high bits of the following flags field are always zero.
+                */
+               if (xing_id != (((XING_MAGIC+0UL) << 16) & 0xffffffffL) &&
+                               xing_id != (((INFO_MAGIC+0UL) << 16) & 0xffffffffL))
+                       return -1;
+               xing_id >>= 16;
+               ptr = start;
+               mad_bit_skip(&ptr, 16);
+               bitlen += 16;
+       }
+       nomad->xing.is_info = ((xing_id & 0x0000ffffL) == (INFO_MAGIC & 0x0000ffffL));
+       nomad->xing.flags = mad_bit_read(&ptr, 32);
+       bitlen -= 64;
+       if (nomad->xing.flags & XING_FRAMES) {
+               if (bitlen < 32)
+                       return -1;
+               nomad->xing.nr_frames = mad_bit_read(&ptr, 32);
+               bitlen -= 32;
+       }
+       if (nomad->xing.flags & XING_BYTES) {
+               if (bitlen < 32)
+                       return -1;
+               nomad->xing.bytes = mad_bit_read(&ptr, 32);
+               bitlen -= 32;
+       }
+       if (nomad->xing.flags & XING_TOC) {
+               int i;
+
+               if (bitlen < 800)
+                       return -1;
+               for (i = 0; i < 100; i++)
+                       nomad->xing.toc[i] = mad_bit_read(&ptr, 8);
+               bitlen -= 800;
+       }
+       if (nomad->xing.flags & XING_SCALE) {
+               if (bitlen < 32)
+                       return -1;
+               nomad->xing.scale = mad_bit_read(&ptr, 32);
+               bitlen -= 32;
+       }
+
+       /* Make sure we consume no less than 120 bytes (960 bits) in hopes that
+        * the LAME tag is found there, and not right after the Xing header */
+       bitsleft = 960 - (oldbitlen - bitlen);
+       if (bitsleft < 0) return -1;
+       else if (bitsleft > 0) {
+               mad_bit_read(&ptr, bitsleft);
+               bitlen -= bitsleft;
+       }
+
+       nomad->has_xing = 1;
+#if defined(DEBUG_XING)
+       if (nomad->xing.flags & XING_FRAMES)
+               d_print("frames: %d (xing)\n", nomad->xing.nr_frames);
+#endif
+
+       parse_lame(nomad, ptr, bitlen);
+
+       return 0;
+}
+
+/*
+ * returns:
+ *    0: eof
+ *   -1: error
+ *   >0: ok
+ */
+static int fill_buffer(struct nomad *nomad)
+{
+       if (nomad->stream.buffer == NULL || nomad->stream.error == MAD_ERROR_BUFLEN) {
+               ssize_t read_size, remaining, len;
+               unsigned char *read_start;
+
+               if (nomad->stream.next_frame != NULL) {
+                       remaining = nomad->stream.bufend - nomad->stream.next_frame;
+                       memmove(nomad->input_buffer, nomad->stream.next_frame, remaining);
+                       read_start = nomad->input_buffer + remaining;
+                       read_size = INPUT_BUFFER_SIZE - remaining;
+               } else {
+                       read_size = INPUT_BUFFER_SIZE;
+                       read_start = nomad->input_buffer;
+                       remaining = 0;
+               }
+               read_size = nomad->cbs.read(nomad->datasource, read_start, read_size);
+               if (read_size == -1) {
+                       if (errno != EAGAIN)
+                               d_print("read error on bitstream (%d:%s)\n", errno, strerror(errno));
+                       return -1;
+               }
+               if (read_size == 0) {
+                       if (!nomad->readEOF) {
+                               memset(nomad->input_buffer + remaining, 0, MAD_BUFFER_GUARD);
+                               remaining += MAD_BUFFER_GUARD;
+                               d_print("hit end of stream, appended MAD_BUFFER_GUARD zeros\n");
+                               nomad->readEOF = 1;
+                       }
+                       else return 0;
+               }
+
+               len = read_size + remaining;
+
+               nomad->input_offset += read_size;
+
+               mad_stream_buffer(&nomad->stream, nomad->input_buffer, len);
+               nomad->stream.error = 0;
+       }
+       return 1;
+}
+
+static void handle_lost_sync(struct nomad *nomad)
+{
+       unsigned long frame;
+       int size;
+
+       frame = nomad->cur_frame;
+       if (frame == 0) {
+               /* cur_frame is not set when scanning file */
+               frame = nomad->info.nr_frames;
+       }
+
+       size = id3_tag_size((const char *)nomad->stream.this_frame,
+                       nomad->stream.bufend - nomad->stream.this_frame);
+       if (size > 0) {
+               d_print("frame %ld, skipping ID3 tag (%d bytes)\n", frame, size);
+               mad_stream_skip(&nomad->stream, size);
+       } else {
+               d_print("frame %ld\n", frame);
+       }
+}
+
+
+/* Builds a seek index as the file is decoded
+ * NOTE: increases nomad->timer (current position)
+ */
+static void build_seek_index(struct nomad *nomad)
+{
+       mad_timer_t timer_now = nomad->timer;
+       off_t offset;
+       int idx;
+
+       mad_timer_add(&nomad->timer, nomad->frame.header.duration);
+
+       if (nomad->has_xing)
+               return;
+
+       if (nomad->timer.seconds < (nomad->seek_idx.size + 1) * SEEK_IDX_INTERVAL)
+               return;
+
+       /* offset = ftell() */
+       offset = nomad->input_offset;
+       /* subtract by buffer length to get offset to start of buffer */
+       offset -= (nomad->stream.bufend - nomad->input_buffer);
+       /* then add offset to the current frame */
+       offset += (nomad->stream.this_frame - nomad->input_buffer);
+
+       idx = nomad->seek_idx.size;
+
+       nomad->seek_idx.table = xrenew(struct seek_idx_entry, nomad->seek_idx.table, idx + 1);
+       nomad->seek_idx.table[idx].offset = offset;
+       nomad->seek_idx.table[idx].timer = timer_now;
+
+       nomad->seek_idx.size++;
+}
+
+static void calc_frames_fast(struct nomad *nomad)
+{
+       if (nomad->has_xing && (nomad->xing.flags & XING_FRAMES) && nomad->xing.nr_frames) {
+               nomad->info.nr_frames = nomad->xing.nr_frames;
+               mad_timer_multiply(&nomad->timer, nomad->info.nr_frames);
+       } else {
+               nomad->info.nr_frames = nomad->info.filesize /
+                       (nomad->stream.next_frame - nomad->stream.this_frame);
+               mad_timer_multiply(&nomad->timer, nomad->info.nr_frames);
+       }
+}
+
+static void calc_bitrate_fast(struct nomad *nomad)
+{
+       nomad->info.vbr = nomad->has_xing ? !nomad->xing.is_info : 0;
+
+       if (nomad->has_lame && nomad->lame.vbr_method == 1)
+               nomad->info.vbr = 0;
+
+       if (nomad->has_xing && (nomad->xing.flags & XING_BYTES) && nomad->xing.bytes)
+               nomad->info.avg_bitrate = (nomad->xing.bytes * 8.0) / nomad->info.duration;
+       else
+               nomad->info.avg_bitrate = nomad->frame.header.bitrate;
+}
+
+/*
+ * fields
+ *     nomad->info.avg_bitrate and
+ *     nomad->info.vbr
+ * are only estimated
+ */
+static int scan(struct nomad *nomad)
+{
+       struct mad_header *header = &nomad->frame.header;
+
+       while (1) {
+               int rc;
+
+               rc = fill_buffer(nomad);
+               if (rc == -1)
+                       return -1;
+               if (rc == 0)
+                       break;
+
+               if (mad_frame_decode(&nomad->frame, &nomad->stream) == -1) {
+                       if (nomad->stream.error == MAD_ERROR_BUFLEN)
+                               continue;
+                       if (!MAD_RECOVERABLE(nomad->stream.error)) {
+                               d_print("unrecoverable frame level error.\n");
+                               return -1;
+                       }
+                       if (nomad->stream.error == MAD_ERROR_LOSTSYNC)
+                               handle_lost_sync(nomad);
+                       continue;
+               }
+
+               build_seek_index(nomad);
+
+               // first valid frame
+               nomad->info.sample_rate = header->samplerate;
+               nomad->info.channels = MAD_NCHANNELS(header);
+               nomad->info.layer = header->layer;
+               nomad->info.dual_channel = header->mode == MAD_MODE_DUAL_CHANNEL;
+               nomad->info.joint_stereo = header->mode == MAD_MODE_JOINT_STEREO;
+
+               xing_parse(nomad);
+               calc_frames_fast(nomad);
+               break;
+       }
+       if (nomad->info.nr_frames == 0) {
+               d_print("error: not an mp3 file!\n");
+               return -NOMAD_ERROR_FILE_FORMAT;
+       }
+       nomad->info.duration = timer_to_seconds(nomad->timer);
+       calc_bitrate_fast(nomad);
+       nomad->cur_frame = 0;
+       nomad->cbs.lseek(nomad->datasource, 0, SEEK_SET);
+       nomad->input_offset = 0;
+       return 0;
+}
+
+static int decode(struct nomad *nomad)
+{
+       int rc;
+
+start:
+       rc = fill_buffer(nomad);
+       if (rc == -1)
+               return -1;
+       if (rc == 0)
+               return 1;
+
+       if (mad_frame_decode(&nomad->frame, &nomad->stream)) {
+               if (nomad->stream.error == MAD_ERROR_BUFLEN)
+                       goto start;
+               if (!MAD_RECOVERABLE(nomad->stream.error)) {
+                       d_print("unrecoverable frame level error.\n");
+                       return -1;
+               }
+               if (nomad->stream.error == MAD_ERROR_LOSTSYNC)
+                       handle_lost_sync(nomad);
+               goto start;
+       }
+       nomad->cur_frame++;
+       nomad->current.bitrate_sum += nomad->frame.header.bitrate;
+       nomad->current.nr_frames++;
+       if (nomad->info.filesize != -1) {
+               build_seek_index(nomad);
+       } else {
+               mad_timer_add(&nomad->timer, nomad->frame.header.duration);
+       }
+       mad_synth_frame(&nomad->synth, &nomad->frame);
+       return 0;
+}
+
+static void init_mad(struct nomad *nomad)
+{
+       mad_stream_init(&nomad->stream);
+       nomad->stream.options |= MAD_OPTION_IGNORECRC;
+       mad_frame_init(&nomad->frame);
+       mad_synth_init(&nomad->synth);
+       mad_timer_reset(&nomad->timer);
+       nomad->cur_frame = 0;
+       nomad->i = -1;
+       nomad->input_offset = 0;
+       nomad->seen_first_frame = 0;
+       nomad->readEOF = 0;
+}
+
+static void free_mad(struct nomad *nomad)
+{
+       mad_stream_finish(&nomad->stream);
+       mad_frame_finish(&nomad->frame);
+       mad_synth_finish(nomad->synth);
+}
+
+static int do_open(struct nomad *nomad)
+{
+       int rc;
+
+       init_mad(nomad);
+       nomad->info.filesize = nomad->cbs.lseek(nomad->datasource, 0, SEEK_END);
+       if (nomad->info.filesize != -1)
+               nomad->cbs.lseek(nomad->datasource, 0, SEEK_SET);
+       if (nomad->info.filesize == -1) {
+               rc = decode(nomad);
+               if (rc < 0)
+                       goto error;
+               if (rc == 1)
+                       goto eof;
+               nomad->info.sample_rate = nomad->frame.header.samplerate;
+               nomad->info.channels = MAD_NCHANNELS(&nomad->frame.header);
+               nomad->info.layer = nomad->frame.header.layer;
+               nomad->info.dual_channel = nomad->frame.header.mode == MAD_MODE_DUAL_CHANNEL;
+               nomad->info.joint_stereo = nomad->frame.header.mode == MAD_MODE_JOINT_STEREO;
+
+               /* unknown */
+               nomad->info.duration = -1.0;
+               nomad->info.nr_frames = -1;
+               nomad->info.vbr = -1;
+               nomad->info.avg_bitrate = -1;
+       } else {
+               rc = scan(nomad);
+               if (rc < 0)
+                       goto error;
+               if (rc == 1)
+                       goto eof;
+               free_mad(nomad);
+               init_mad(nomad);
+       }
+       d_print("\n  frames: %d, br: %d b/s, sr: %d Hz, ch: %d, layer: %d, joint stereo: %d\n"
+               "  dual channel: %d, vbr: %d, duration: %g s, xing: %d\n",
+                       nomad->info.nr_frames, nomad->info.avg_bitrate,
+                       nomad->info.sample_rate, nomad->info.channels,
+                       nomad->info.layer, nomad->info.joint_stereo,
+                       nomad->info.dual_channel, nomad->info.vbr,
+                       nomad->info.duration,
+                       nomad->has_xing);
+#if defined(DEBUG_XING)
+       if (nomad->has_xing)
+               d_print("xing: flags: 0x%x, frames: %d, bytes: %d, scale: %d\n",
+                       nomad->xing.flags,
+                       nomad->xing.nr_frames,
+                       nomad->xing.bytes,
+                       nomad->xing.scale);
+#endif
+       return 0;
+error:
+       nomad_close(nomad);
+       return rc;
+eof:
+       nomad_close(nomad);
+       return -NOMAD_ERROR_FILE_FORMAT;
+}
+
+int nomad_open_callbacks(struct nomad **nomadp, void *datasource, struct nomad_callbacks *cbs)
+{
+       struct nomad *nomad;
+
+       const struct nomad nomad_init = {
+               .datasource = datasource,
+               .cbs = {
+                       .read  = cbs->read,
+                       .lseek = cbs->lseek,
+                       .close = cbs->close
+               }
+       };
+
+       nomad = xnew(struct nomad, 1);
+       *nomad = nomad_init;
+       nomad->lame.peak = nomad->lame.trackGain = nomad->lame.albumGain = strtof("NAN", NULL);
+       *nomadp = nomad;
+       /* on error do_open calls nomad_close */
+       return do_open(nomad);
+}
+
+void nomad_close(struct nomad *nomad)
+{
+       free_mad(nomad);
+       nomad->cbs.close(nomad->datasource);
+       free(nomad->seek_idx.table);
+       free(nomad);
+}
+
+int nomad_read(struct nomad *nomad, char *buffer, int count)
+{
+       int i, j, size, psize, to;
+
+       if (nomad->i == -1) {
+               int rc;
+
+next_frame:
+               rc = decode(nomad);
+               if (rc < 0)
+                       return rc;
+               if (rc == 1)
+                       return 0;
+               nomad->i = 0;
+       }
+
+       if (nomad->has_lame) {
+               /* skip samples at start for gapless playback */
+               if (nomad->start_drop_frames) {
+                       nomad->start_drop_frames--;
+                       /* XING header is an empty frame we want to skip */
+                       if (!nomad->seen_first_frame) {
+                               nomad->cur_frame--;
+                               nomad->seen_first_frame = 1;
+                       }
+#if defined(DEBUG_LAME)
+                       d_print("skipped a frame at start\n");
+#endif
+                       goto next_frame;
+               }
+               if (nomad->start_drop_samples) {
+                       if (nomad->start_drop_samples < nomad->synth.pcm.length) {
+                               nomad->i += nomad->start_drop_samples;
+                               nomad->start_drop_samples = 0;
+                               /* Take advantage of the fact that this block is only executed once per file, and
+                                  calculate the # of samples/frames to skip at the end.  Note that synth.pcm.length
+                                  is needed for the calculation. */
+                               nomad->end_drop_frames = nomad->end_drop_samples / nomad->synth.pcm.length;
+                               nomad->end_drop_samples = nomad->end_drop_samples % nomad->synth.pcm.length;
+#if defined(DEBUG_LAME)
+                               d_print("skipped %d samples at start\n", nomad->i);
+                               d_print("will skip %d samples and %d frame(s) at end\n",
+                                       nomad->end_drop_samples, nomad->end_drop_frames);
+#endif
+                       }
+                       else {
+                               nomad->start_drop_samples -= nomad->synth.pcm.length;
+#if defined(DEBUG_LAME)
+                               d_print("skipped %d samples at start and moving to next frame\n", nomad->synth.pcm.length);
+#endif
+                               goto next_frame;
+                       }
+               }
+               /* skip samples/frames at end for gapless playback */
+               if (nomad->cur_frame == (nomad->xing.nr_frames + 1 - nomad->end_drop_frames)) {
+#if defined(DEBUG_LAME)
+                               d_print("skipped %d frame(s) at end\n", nomad->end_drop_frames);
+#endif
+                       return 0;
+               }
+       }
+
+       psize = nomad->info.channels * 16 / 8;
+       size = (nomad->synth.pcm.length - nomad->i) * psize;
+
+       if (size > count) {
+               to = nomad->i + count / psize;
+       } else {
+               to = nomad->synth.pcm.length;
+       }
+       j = 0;
+       for (i = nomad->i; i < to; i++) {
+               short sample;
+
+               /* skip samples/frames at end for gapless playback */
+               if (nomad->has_lame
+                   && nomad->end_drop_samples
+                   && (nomad->cur_frame == (nomad->xing.nr_frames - nomad->end_drop_frames))
+                   && i == (nomad->synth.pcm.length - nomad->end_drop_samples)) {
+                       nomad->i = -1;
+#if defined(DEBUG_LAME)
+                       d_print("skipped %d samples at end of frame %d\n", nomad->end_drop_samples, (int)nomad->cur_frame);
+#endif
+                       return j;
+               }
+               sample = scale(nomad->synth.pcm.samples[0][i]);
+               buffer[j++] = (sample >> 0) & 0xff;
+               buffer[j++] = (sample >> 8) & 0xff;
+
+               if (nomad->info.channels == 2) {
+                       sample = scale(nomad->synth.pcm.samples[1][i]);
+                       buffer[j++] = (sample >> 0) & 0xff;
+                       buffer[j++] = (sample >> 8) & 0xff;
+               }
+       }
+       if (to != nomad->synth.pcm.length) {
+               nomad->i = i;
+       } else {
+               nomad->i = -1;
+       }
+       return j;
+}
+
+static int nomad_time_seek_accurate(struct nomad *nomad, double pos)
+{
+       int rc;
+
+       /* seek to beginning of file and search frame-by-frame */
+       if (nomad->cbs.lseek(nomad->datasource, 0, SEEK_SET) == -1)
+               return -1;
+
+       /* XING header should NOT be counted - if we're here, we know it's present */
+       nomad->cur_frame = -1;
+
+       while (timer_to_seconds(nomad->timer) < pos) {
+               rc = fill_buffer(nomad);
+               if (rc == -1)
+                       return -1;
+               if (rc == 0)
+                       return 1;
+
+               if (mad_header_decode(&nomad->frame.header, &nomad->stream)) {
+                       if (nomad->stream.error == MAD_ERROR_BUFLEN)
+                               continue;
+                       if (!MAD_RECOVERABLE(nomad->stream.error)) {
+                               d_print("unrecoverable frame level error.\n");
+                               return -1;
+                       }
+                       if (nomad->stream.error == MAD_ERROR_LOSTSYNC)
+                               handle_lost_sync(nomad);
+                       continue;
+               }
+               nomad->cur_frame++;
+               mad_timer_add(&nomad->timer, nomad->frame.header.duration);
+       }
+#if defined(DEBUG_LAME)
+               d_print("seeked to %g = %g\n", pos, timer_to_seconds(nomad->timer));
+#endif
+       return 0;
+}
+
+int nomad_time_seek(struct nomad *nomad, double pos)
+{
+       off_t offset = 0;
+
+       if (pos < 0.0 || pos > nomad->info.duration) {
+               errno = EINVAL;
+               return -1;
+       }
+       if (nomad->info.filesize == -1) {
+               errno = ESPIPE;
+               return -1;
+       }
+       free_mad(nomad);
+       init_mad(nomad);
+
+       /* if file has a LAME header, perform frame-accurate seek for gapless playback */
+       if (nomad->has_lame) {
+               return nomad_time_seek_accurate(nomad, pos);
+       } else if (nomad->has_xing) {
+               /* calculate seek offset */
+               /* seek to truncate(pos / duration * 100) / 100 * duration */
+               double k, tmp_pos;
+               int ki;
+
+               k = pos / nomad->info.duration * 100.0;
+               ki = k;
+               tmp_pos = ((double)ki) / 100.0 * nomad->info.duration;
+               nomad->timer.seconds = (signed int)tmp_pos;
+               nomad->timer.fraction = (tmp_pos - (double)nomad->timer.seconds) * MAD_TIMER_RESOLUTION;
+#if defined(DEBUG_XING)
+               d_print("seeking to %g = %g %d%%\n",
+                               pos,
+                               timer_to_seconds(nomad->timer),
+                               ki);
+#endif
+               offset = ((unsigned long long)nomad->xing.toc[ki] * nomad->xing.bytes) / 256;
+       } else if (nomad->seek_idx.size > 0) {
+               int idx = (int)(pos / SEEK_IDX_INTERVAL) - 1;
+
+               if (idx > nomad->seek_idx.size - 1)
+                       idx = nomad->seek_idx.size - 1;
+
+               if (idx >= 0) {
+                       offset = nomad->seek_idx.table[idx].offset;
+                       nomad->timer = nomad->seek_idx.table[idx].timer;
+               }
+       }
+       if (nomad->cbs.lseek(nomad->datasource, offset, SEEK_SET) == -1)
+               return -1;
+
+       nomad->input_offset = offset;
+       while (timer_to_seconds(nomad->timer) < pos) {
+               int rc;
+
+               rc = fill_buffer(nomad);
+               if (rc == -1)
+                       return -1;
+               if (rc == 0)
+                       return 0;
+
+               if (mad_header_decode(&nomad->frame.header, &nomad->stream) == 0) {
+                       build_seek_index(nomad);
+               } else {
+                       if (!MAD_RECOVERABLE(nomad->stream.error) && nomad->stream.error != MAD_ERROR_BUFLEN) {
+                               d_print("unrecoverable frame level error.\n");
+                               return -1;
+                       }
+                       if (nomad->stream.error == MAD_ERROR_LOSTSYNC)
+                               handle_lost_sync(nomad);
+               }
+       }
+#if defined(DEBUG_XING)
+       if (nomad->has_xing)
+               d_print("seeked to %g = %g\n", pos, timer_to_seconds(nomad->timer));
+#endif
+       return 0;
+}
+
+const struct nomad_xing *nomad_xing(struct nomad *nomad)
+{
+       return nomad->has_xing ? &nomad->xing : NULL;
+}
+
+const struct nomad_lame *nomad_lame(struct nomad *nomad)
+{
+       return nomad->has_lame ? &nomad->lame : NULL;
+}
+
+const struct nomad_info *nomad_info(struct nomad *nomad)
+{
+       return &nomad->info;
+}
+
+long nomad_current_bitrate(struct nomad *nomad)
+{
+       long bitrate = -1;
+       if (nomad->current.nr_frames > 0) {
+               bitrate = nomad->current.bitrate_sum / nomad->current.nr_frames;
+               nomad->current.bitrate_sum = 0;
+               nomad->current.nr_frames = 0;
+       }
+       return bitrate;
+}
diff --git a/ip/nomad.h b/ip/nomad.h
new file mode 100644 (file)
index 0000000..a4c91c1
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_NOMAD_H
+#define CMUS_NOMAD_H
+
+#include <sys/types.h>
+
+#ifndef __GNUC__
+#include <unistd.h>
+#endif
+
+/* default callbacks use read, lseek, close */
+struct nomad_callbacks {
+       ssize_t (*read)(void *datasource, void *buffer, size_t count);
+       off_t (*lseek)(void *datasource, off_t offset, int whence);
+       int (*close)(void *datasource);
+};
+
+enum {
+       XING_FRAMES = 0x00000001L,
+       XING_BYTES  = 0x00000002L,
+       XING_TOC    = 0x00000004L,
+       XING_SCALE  = 0x00000008L
+};
+
+struct nomad_xing {
+       unsigned int is_info : 1;
+       unsigned int flags;
+       unsigned int nr_frames;
+       unsigned int bytes;
+       unsigned int scale;
+       unsigned char toc[100];
+};
+
+struct nomad_lame {
+       char encoder[10];   /* 9 byte encoder name/version ("LAME3.97b") */
+       int vbr_method;     /* VBR method */
+       float peak;         /* replaygain peak */
+       float trackGain;    /* replaygain track gain */
+       float albumGain;    /* replaygain album gain */
+       int encoderDelay;   /* # of added samples at start of mp3 */
+       int encoderPadding; /* # of added samples at end of mp3 */
+};
+
+/* always 16-bit signed little-endian */
+struct nomad_info {
+       double duration;
+       int sample_rate;
+       int channels;
+       int nr_frames;
+       int layer;
+       /* guessed */
+       int vbr;
+       /* guessed */
+       int avg_bitrate;
+       /* -1 if file not seekable */
+       off_t filesize;
+       unsigned int joint_stereo : 1;
+       unsigned int dual_channel : 1;
+};
+
+enum {
+       NOMAD_ERROR_SUCCESS,
+       NOMAD_ERROR_ERRNO,
+       NOMAD_ERROR_FILE_FORMAT
+};
+
+struct nomad;
+
+/* -NOMAD_ERROR_ERRNO -NOMAD_ERROR_FILE_FORMAT */
+int nomad_open_callbacks(struct nomad **nomadp, void *datasource,
+               struct nomad_callbacks *cbs);
+
+void nomad_close(struct nomad *nomad);
+
+/* -NOMAD_ERROR_ERRNO */
+int nomad_read(struct nomad *nomad, char *buffer, int count);
+
+/* -NOMAD_ERROR_ERRNO */
+int nomad_time_seek(struct nomad *nomad, double pos);
+
+const struct nomad_xing *nomad_xing(struct nomad *nomad);
+const struct nomad_lame *nomad_lame(struct nomad *nomad);
+const struct nomad_info *nomad_info(struct nomad *nomad);
+long nomad_current_bitrate(struct nomad *nomad);
+
+#endif
diff --git a/ip/opus.c b/ip/opus.c
new file mode 100644 (file)
index 0000000..4216531
--- /dev/null
+++ b/ip/opus.c
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2008-2014 Various Authors
+ * Copyright 2012 Tuncer Ayaz
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "xmalloc.h"
+#include "read_wrapper.h"
+#include "debug.h"
+#include "comment.h"
+
+#include <opusfile.h>
+
+#include <errno.h>
+#include <string.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#define SAMPLING_RATE 48000
+#define CHANNELS 2
+
+struct opus_private {
+       OggOpusFile *of;
+       int current_link;
+};
+
+static int read_func(void *datasource, unsigned char *ptr, int size)
+{
+       struct input_plugin_data *ip_data = datasource;
+       return read_wrapper(ip_data, ptr, size);
+}
+
+static int seek_func(void *datasource, opus_int64 offset, int whence)
+{
+       struct input_plugin_data *ip_data = datasource;
+       return lseek(ip_data->fd, offset, whence);
+}
+
+static int close_func(void *datasource)
+{
+       struct input_plugin_data *ip_data;
+       int rc;
+
+       ip_data = datasource;
+       rc = close(ip_data->fd);
+       ip_data->fd = -1;
+       return rc;
+}
+
+static opus_int64 tell_func(void *datasource)
+{
+       struct input_plugin_data *ip_data = datasource;
+       return lseek(ip_data->fd, 0, SEEK_CUR);
+}
+
+static OpusFileCallbacks callbacks = {
+       .read = read_func,
+       .seek = seek_func,
+       .tell = tell_func,
+       .close = close_func
+};
+
+static int opus_open(struct input_plugin_data *ip_data)
+{
+       struct opus_private *priv;
+       int rc;
+       void *source;
+
+       priv = xnew(struct opus_private, 1);
+       priv->current_link = -1;
+       priv->of = NULL;
+
+       source = op_fdopen(&callbacks, ip_data->fd, "r");
+       if (source == NULL) {
+               free(priv);
+               return -IP_ERROR_INTERNAL;
+       }
+
+       priv->of = op_open_callbacks(source, &callbacks, NULL, 0, &rc);
+       if (rc != 0) {
+               d_print("op_open_callbacks failed: %d:%s\n", rc, strerror(rc));
+               free(priv);
+               /* ogg is a container format, so it is likely to contain
+                * something else if it isn't opus */
+               return -IP_ERROR_UNSUPPORTED_FILE_TYPE;
+       }
+       ip_data->private = priv;
+
+       ip_data->sf = sf_rate(SAMPLING_RATE)
+               | sf_channels(CHANNELS)
+               | sf_bits(16)
+               | sf_signed(1);
+       ip_data->sf |= sf_host_endian();
+       return 0;
+}
+
+static int opus_close(struct input_plugin_data *ip_data)
+{
+       struct opus_private *priv = ip_data->private;
+       /* this closes ip_data->fd! */
+       op_free(priv->of);
+       ip_data->fd = -1;
+       free(priv);
+       ip_data->private = NULL;
+       return 0;
+}
+
+/*
+ * -n
+ *     indicates error
+ * 0
+ *     indicates EOF
+ * n
+ *     indicates actual number of bytes read
+ */
+static int opus_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       struct opus_private *priv;
+       int samples, current_link, rc;
+
+       priv = ip_data->private;
+
+       /* samples = number of samples read per channel */
+       samples = op_read_stereo(priv->of, (void*)buffer,
+                                                        count / sizeof(opus_int16));
+       if (samples < 0) {
+               switch (samples) {
+               case OP_HOLE:
+                       errno = EAGAIN;
+                       rc = -1;
+                       break;
+               case OP_EREAD:
+                       errno = EINVAL;
+                       rc = -1;
+                       break;
+               case OP_EFAULT:
+                       errno = EINVAL;
+                       rc = -1;
+                       break;
+               case OP_EIMPL:
+                       rc = -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+                       break;
+               case OP_EINVAL:
+                       errno = EINVAL;
+                       rc = -1;
+                       break;
+               case OP_ENOTFORMAT:
+                       rc = -IP_ERROR_FILE_FORMAT;
+                       break;
+               case OP_EBADHEADER:
+                       rc = -IP_ERROR_FILE_FORMAT;
+                       break;
+               case OP_EVERSION:
+                       rc = -IP_ERROR_FILE_FORMAT;
+                       break;
+               case OP_EBADPACKET:
+                       errno = EINVAL;
+                       rc = -1;
+                       break;
+               case OP_EBADLINK:
+                       errno = EINVAL;
+                       rc = -1;
+                       break;
+               case OP_EBADTIMESTAMP:
+                       rc = -IP_ERROR_FILE_FORMAT;
+                       break;
+               default:
+                       d_print("error: %d\n", samples);
+                       rc = -IP_ERROR_FILE_FORMAT;
+               }
+       } else if (samples == 0) {
+               /* EOF or buffer too small */
+               rc = 0;
+       } else {
+               current_link = op_current_link(priv->of);
+               if (current_link < 0) {
+                       d_print("error: %d\n", current_link);
+                       rc = -1;
+               } else {
+                       if (ip_data->remote && current_link != priv->current_link) {
+                               ip_data->metadata_changed = 1;
+                               priv->current_link = current_link;
+                       }
+
+                       /* bytes = samples * channels * sample_size */
+                       rc = samples * CHANNELS * sizeof(opus_int16);
+               }
+       }
+
+       return rc;
+}
+
+static int opus_seek(struct input_plugin_data *ip_data, double offset)
+{
+       struct opus_private *priv;
+       int rc;
+
+       priv = ip_data->private;
+
+       rc = op_pcm_seek(priv->of, offset * SAMPLING_RATE);
+       switch (rc) {
+       case OP_ENOSEEK:
+               return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+       case OP_EINVAL:
+               return -IP_ERROR_INTERNAL;
+       case OP_EREAD:
+               return -IP_ERROR_INTERNAL;
+       case OP_EFAULT:
+               return -IP_ERROR_INTERNAL;
+       case OP_EBADLINK:
+               return -IP_ERROR_INTERNAL;
+       }
+       return 0;
+}
+
+static int opus_read_comments(struct input_plugin_data *ip_data,
+                                                         struct keyval **comments)
+{
+       GROWING_KEYVALS(c);
+       struct opus_private *priv;
+       const OpusTags *ot;
+       int i;
+
+       priv = ip_data->private;
+
+       ot = op_tags(priv->of, -1);
+       if (ot == NULL) {
+               d_print("ot == NULL\n");
+               *comments = keyvals_new(0);
+               return 0;
+       }
+
+       for (i = 0; i < ot->comments; i++) {
+               const char *str = ot->user_comments[i];
+               const char *eq = strchr(str, '=');
+               char *key;
+
+               if (!eq) {
+                       d_print("invalid comment: '%s' ('=' expected)\n", str);
+                       continue;
+               }
+
+               key = xstrndup(str, eq - str);
+               comments_add_const(&c, key, eq + 1);
+               free(key);
+       }
+       keyvals_terminate(&c);
+       *comments = c.keyvals;
+       return 0;
+}
+
+static int opus_duration(struct input_plugin_data *ip_data)
+{
+       struct opus_private *priv;
+       ogg_int64_t samples;
+
+       priv = ip_data->private;
+
+       samples = op_pcm_total(priv->of, -1);
+       if (samples < 0)
+               return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+
+       return samples / SAMPLING_RATE;
+}
+
+static long opus_bitrate(struct input_plugin_data *ip_data)
+{
+       struct opus_private *priv;
+       opus_int32 bitrate;
+
+       priv = ip_data->private;
+
+       bitrate = op_bitrate(priv->of, -1);
+       if (bitrate < 0)
+               return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+       else
+               return bitrate;
+}
+
+static long opus_current_bitrate(struct input_plugin_data *ip_data)
+{
+       struct opus_private *priv;
+       opus_int32 bitrate;
+
+       priv = ip_data->private;
+
+       bitrate = op_bitrate_instant(priv->of);
+       if (bitrate < 0)
+               return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+       else
+               return bitrate;
+}
+
+static char *opus_codec(struct input_plugin_data *ip_data)
+{
+       return xstrdup("opus");
+}
+
+static char *opus_codec_profile(struct input_plugin_data *ip_data)
+{
+       return NULL;
+}
+
+const struct input_plugin_ops ip_ops = {
+       .open = opus_open,
+       .close = opus_close,
+       .read = opus_read,
+       .seek = opus_seek,
+       .read_comments = opus_read_comments,
+       .duration = opus_duration,
+       .bitrate = opus_bitrate,
+       .bitrate_current = opus_current_bitrate,
+       .codec = opus_codec,
+       .codec_profile = opus_codec_profile
+};
+
+const int ip_priority = 50;
+const char * const ip_extensions[] = { "opus", NULL };
+const char * const ip_mime_types[] = { NULL };
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/ip/vorbis.c b/ip/vorbis.c
new file mode 100644 (file)
index 0000000..e78e405
--- /dev/null
@@ -0,0 +1,414 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "xmalloc.h"
+#include "read_wrapper.h"
+#include "debug.h"
+#ifdef HAVE_CONFIG
+#include "config/tremor.h"
+#endif
+#include "comment.h"
+
+#ifdef CONFIG_TREMOR
+#include <tremor/ivorbisfile.h>
+#else
+#include <vorbis/vorbisfile.h>
+#endif
+
+#include <errno.h>
+#include <string.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <math.h>
+
+struct vorbis_private {
+       OggVorbis_File vf;
+       int current_section;
+};
+
+/* http://www.xiph.org/vorbis/doc/vorbisfile/callbacks.html */
+
+static size_t read_func(void *ptr, size_t size, size_t nmemb, void *datasource)
+{
+       struct input_plugin_data *ip_data = datasource;
+       int rc;
+
+       rc = read_wrapper(ip_data, ptr, size * nmemb);
+       if (rc == -1) {
+               d_print("error: %s\n", strerror(errno));
+               return 0;
+       }
+       if (rc == 0) {
+               errno = 0;
+               return 0;
+       }
+       return rc / size;
+}
+
+static int seek_func(void *datasource, ogg_int64_t offset, int whence)
+{
+       struct input_plugin_data *ip_data = datasource;
+
+       if (lseek(ip_data->fd, offset, whence) == -1)
+               return -1;
+       return 0;
+}
+
+static int close_func(void *datasource)
+{
+       struct input_plugin_data *ip_data = datasource;
+       int rc;
+
+       rc = close(ip_data->fd);
+       ip_data->fd = -1;
+       return rc;
+}
+
+static long tell_func(void *datasource)
+{
+       struct input_plugin_data *ip_data = datasource;
+       off_t off;
+
+       off = lseek(ip_data->fd, 0, SEEK_CUR);
+       return (off == -1) ? -1 : off;
+}
+
+/*
+ * typedef struct {
+ *   size_t (*read_func)  (void *ptr, size_t size, size_t nmemb, void *datasource);
+ *   int    (*seek_func)  (void *datasource, ogg_int64_t offset, int whence);
+ *   int    (*close_func) (void *datasource);
+ *   long   (*tell_func)  (void *datasource);
+ * } ov_callbacks;
+ */
+static ov_callbacks callbacks = {
+       .read_func = read_func,
+       .seek_func = seek_func,
+       .close_func = close_func,
+       .tell_func = tell_func
+};
+
+/* http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-800004.3.9 */
+static void channel_map_init_vorbis(int channels, channel_position_t *map)
+{
+       switch (channels) {
+       case 8:
+               channel_map_init_vorbis(7, map);
+               map[5] = CHANNEL_POSITION_REAR_LEFT;
+               map[6] = CHANNEL_POSITION_REAR_RIGHT;
+               map[7] = CHANNEL_POSITION_LFE;
+               break;
+       case 7:
+               channel_map_init_vorbis(3, map);
+               map[3] = CHANNEL_POSITION_SIDE_LEFT;
+               map[4] = CHANNEL_POSITION_SIDE_RIGHT;
+               map[5] = CHANNEL_POSITION_REAR_CENTER;
+               map[6] = CHANNEL_POSITION_LFE;
+               break;
+       case 6:
+               map[5] = CHANNEL_POSITION_LFE;
+               /* Fall through */
+       case 5:
+               map[3] = CHANNEL_POSITION_REAR_LEFT;
+               map[4] = CHANNEL_POSITION_REAR_RIGHT;
+               /* Fall through */
+       case 3:
+               map[0] = CHANNEL_POSITION_FRONT_LEFT;
+               map[1] = CHANNEL_POSITION_CENTER;
+               map[2] = CHANNEL_POSITION_FRONT_RIGHT;
+               break;
+       case 4:
+               map[2] = CHANNEL_POSITION_REAR_LEFT;
+               map[3] = CHANNEL_POSITION_REAR_RIGHT;
+               /* Fall through */
+       case 2:
+               map[0] = CHANNEL_POSITION_FRONT_LEFT;
+               map[1] = CHANNEL_POSITION_FRONT_RIGHT;
+               break;
+       case 1:
+               map[0] = CHANNEL_POSITION_MONO;
+               break;
+       default:
+               map[0] = CHANNEL_POSITION_INVALID;
+               break;
+       }
+}
+
+static int vorbis_open(struct input_plugin_data *ip_data)
+{
+       struct vorbis_private *priv;
+       vorbis_info *vi;
+       int rc;
+
+       priv = xnew(struct vorbis_private, 1);
+       priv->current_section = -1;
+       memset(&priv->vf, 0, sizeof(priv->vf));
+
+       rc = ov_open_callbacks(ip_data, &priv->vf, NULL, 0, callbacks);
+       if (rc != 0) {
+               d_print("ov_open failed: %d\n", rc);
+               free(priv);
+               /* ogg is a container format, so it is likely to contain
+                * something else if it isn't vorbis */
+               return -IP_ERROR_UNSUPPORTED_FILE_TYPE;
+       }
+       ip_data->private = priv;
+
+       vi = ov_info(&priv->vf, -1);
+       ip_data->sf = sf_rate(vi->rate) | sf_channels(vi->channels) | sf_bits(16) | sf_signed(1);
+       ip_data->sf |= sf_host_endian();
+       channel_map_init_vorbis(vi->channels, ip_data->channel_map);
+       return 0;
+}
+
+static int vorbis_close(struct input_plugin_data *ip_data)
+{
+       struct vorbis_private *priv;
+       int rc;
+
+       priv = ip_data->private;
+       /* this closes ip_data->fd! */
+       rc = ov_clear(&priv->vf);
+       ip_data->fd = -1;
+       if (rc)
+               d_print("ov_clear returned %d\n", rc);
+       free(priv);
+       ip_data->private = NULL;
+       return 0;
+}
+
+static inline int vorbis_endian(void)
+{
+#ifdef WORDS_BIGENDIAN
+       return 1;
+#else
+       return 0;
+#endif
+}
+
+/*
+ * OV_HOLE
+ *     indicates there was an interruption in the data.
+ *     (one of: garbage between pages, loss of sync followed by recapture,
+ *     or a corrupt page)
+ * OV_EBADLINK
+ *     indicates that an invalid stream section was supplied to libvorbisfile,
+ *     or the requested link is corrupt.
+ * 0
+ *     indicates EOF
+ * n
+ *     indicates actual number of bytes read. ov_read() will decode at most
+ *     one vorbis packet per invocation, so the value returned will generally
+ *     be less than length.
+ */
+static int vorbis_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       struct vorbis_private *priv;
+       int rc;
+       int current_section;
+
+       priv = ip_data->private;
+#ifdef CONFIG_TREMOR
+       /* Tremor can only handle signed 16 bit data */
+       rc = ov_read(&priv->vf, buffer, count, &current_section);
+#else
+       rc = ov_read(&priv->vf, buffer, count, vorbis_endian(), 2, 1, &current_section);
+#endif
+
+       if (ip_data->remote && current_section != priv->current_section) {
+               ip_data->metadata_changed = 1;
+               priv->current_section = current_section;
+       }
+
+       switch (rc) {
+       case OV_HOLE:
+               errno = EAGAIN;
+               return -1;
+       case OV_EBADLINK:
+               errno = EINVAL;
+               return -1;
+       case OV_EINVAL:
+               errno = EINVAL;
+               return -1;
+       case 0:
+               if (errno) {
+                       d_print("error: %s\n", strerror(errno));
+                       return -1;
+/*                     return -IP_ERROR_INTERNAL; */
+               }
+               /* EOF */
+               return 0;
+       default:
+               if (rc < 0) {
+                       d_print("error: %d\n", rc);
+                       rc = -IP_ERROR_FILE_FORMAT;
+               }
+               return rc;
+       }
+}
+
+static int vorbis_seek(struct input_plugin_data *ip_data, double offset)
+{
+       struct vorbis_private *priv;
+       int rc;
+
+       priv = ip_data->private;
+
+#ifdef CONFIG_TREMOR
+       rc = ov_time_seek(&priv->vf, offset * 1000);
+#else
+       rc = ov_time_seek(&priv->vf, offset);
+#endif
+       switch (rc) {
+       case OV_ENOSEEK:
+               return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+       case OV_EINVAL:
+               return -IP_ERROR_INTERNAL;
+       case OV_EREAD:
+               return -IP_ERROR_INTERNAL;
+       case OV_EFAULT:
+               return -IP_ERROR_INTERNAL;
+       case OV_EBADLINK:
+               return -IP_ERROR_INTERNAL;
+       }
+       return 0;
+}
+
+static int vorbis_read_comments(struct input_plugin_data *ip_data,
+               struct keyval **comments)
+{
+       GROWING_KEYVALS(c);
+       struct vorbis_private *priv;
+       vorbis_comment *vc;
+       int i;
+
+       priv = ip_data->private;
+       vc = ov_comment(&priv->vf, -1);
+       if (vc == NULL) {
+               d_print("vc == NULL\n");
+               *comments = keyvals_new(0);
+               return 0;
+       }
+       for (i = 0; i < vc->comments; i++) {
+               const char *str = vc->user_comments[i];
+               const char *eq = strchr(str, '=');
+               char *key;
+
+               if (!eq) {
+                       d_print("invalid comment: '%s' ('=' expected)\n", str);
+                       continue;
+               }
+
+               key = xstrndup(str, eq - str);
+               comments_add_const(&c, key, eq + 1);
+               free(key);
+       }
+       keyvals_terminate(&c);
+       *comments = c.keyvals;
+       return 0;
+}
+
+static int vorbis_duration(struct input_plugin_data *ip_data)
+{
+       struct vorbis_private *priv;
+       int duration;
+
+       priv = ip_data->private;
+       duration = ov_time_total(&priv->vf, -1);
+       if (duration == OV_EINVAL)
+               return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+#ifdef CONFIG_TREMOR
+       duration = (duration + 500) / 1000;
+#endif
+       return duration;
+}
+
+static long vorbis_bitrate(struct input_plugin_data *ip_data)
+{
+       struct vorbis_private *priv = ip_data->private;
+       long bitrate = ov_bitrate(&priv->vf, -1);
+       if (bitrate == OV_EINVAL || bitrate == OV_FALSE)
+               return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+       return bitrate;
+}
+
+static long vorbis_current_bitrate(struct input_plugin_data *ip_data)
+{
+       struct vorbis_private *priv = ip_data->private;
+       return ov_bitrate_instant(&priv->vf);
+}
+
+static char *vorbis_codec(struct input_plugin_data *ip_data)
+{
+       return xstrdup("vorbis");
+}
+
+static const long rate_mapping_44[2][12] = {
+       { 32000, 48000, 60000, 70000,  80000,  86000,  96000, 110000, 120000, 140000, 160000, 239920 },
+       { 45000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000, 499821 }
+};
+
+static char *vorbis_codec_profile(struct input_plugin_data *ip_data)
+{
+       struct vorbis_private *priv = ip_data->private;
+       vorbis_info *vi = ov_info(&priv->vf, -1);
+       long b = vi->bitrate_nominal;
+       char buf[64];
+
+       if (b <= 0)
+               return NULL;
+
+       if (vi->channels > 2 || vi->rate < 44100) {
+               sprintf(buf, "%ldkbps", b / 1000);
+       } else {
+               const long *map = rate_mapping_44[vi->channels - 1];
+               float q;
+               int i;
+
+               for (i = 0; i < 12-1; i++) {
+                       if (b >= map[i] && b < map[i+1])
+                               break;
+               }
+               /* This is used even if upper / lower bitrate are set
+                * because it gives a good approximation. */
+               q = (i - 1) + (float) (b - map[i]) / (map[i+1] - map[i]);
+               sprintf(buf, "q%g", roundf(q * 100.f) / 100.f);
+       }
+
+       return xstrdup(buf);
+}
+
+const struct input_plugin_ops ip_ops = {
+       .open = vorbis_open,
+       .close = vorbis_close,
+       .read = vorbis_read,
+       .seek = vorbis_seek,
+       .read_comments = vorbis_read_comments,
+       .duration = vorbis_duration,
+       .bitrate = vorbis_bitrate,
+       .bitrate_current = vorbis_current_bitrate,
+       .codec = vorbis_codec,
+       .codec_profile = vorbis_codec_profile
+};
+
+const int ip_priority = 50;
+const char * const ip_extensions[] = { "ogg", "oga", "ogx", NULL };
+const char * const ip_mime_types[] = { "application/ogg", "audio/x-ogg", NULL };
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/ip/vtx.c b/ip/vtx.c
new file mode 100644 (file)
index 0000000..9e92e16
--- /dev/null
+++ b/ip/vtx.c
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2014 Boris Timofeev <mashin87@gmail.com>
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "comment.h"
+#include "debug.h"
+#include "ip.h"
+#include "utils.h"
+#include "xmalloc.h"
+#include <ayemu.h>
+#include <string.h>
+
+struct vtx_private {
+       ayemu_ay_t ay;
+       ayemu_ay_reg_frame_t regs;
+       ayemu_vtx_t *vtx;
+       int pos;
+       int left;
+};
+
+static const int sample_rate = 44100;
+static const int channels = 2;
+static const int bits = 16;
+
+static int vtx_open(struct input_plugin_data *ip_data)
+{
+       struct vtx_private *priv;
+       priv = xnew(struct vtx_private, 1);
+       ip_data->private = priv;
+
+       priv->vtx = ayemu_vtx_load_from_file(ip_data->filename);
+       if (!priv->vtx) {
+               d_print("error: failed to open file %s\n", ip_data->filename);
+               free(priv);
+               return -IP_ERROR_INTERNAL;
+       }
+
+       ayemu_init(&priv->ay);
+       ayemu_set_sound_format(&priv->ay, sample_rate, channels, bits);
+       ayemu_set_chip_type(&priv->ay, priv->vtx->chiptype, NULL);
+       ayemu_set_chip_freq(&priv->ay, priv->vtx->chipFreq);
+       ayemu_set_stereo(&priv->ay, priv->vtx->stereo, NULL);
+
+       ip_data->sf = sf_bits(bits) | sf_rate(sample_rate) | sf_channels(channels) | sf_signed(1);
+       ip_data->sf |= sf_host_endian();
+       channel_map_init_stereo(ip_data->channel_map);
+
+       priv->pos = 0;
+       priv->left = 0;
+
+       return 0;
+}
+
+static int vtx_close(struct input_plugin_data *ip_data)
+{
+       struct vtx_private *priv = ip_data->private;
+
+       ayemu_vtx_free(priv->vtx);
+       free(priv);
+       ip_data->private = NULL;
+       return 0;
+}
+
+static int vtx_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       struct vtx_private *priv = ip_data->private;
+       int need = count;
+       int donow = 0;
+
+       while (need > 0) {
+               if (priv->left > 0) {
+                       donow = min_i(need, priv->left);
+                       buffer = ayemu_gen_sound(&priv->ay, (char *)buffer, donow);
+                       priv->left -= donow;
+                       need -= donow;
+               } else {
+                       if (priv->pos >= priv->vtx->frames)
+                               return 0;
+                       ayemu_vtx_getframe(priv->vtx, priv->pos++, priv->regs);
+                       ayemu_set_regs(&priv->ay, priv->regs);
+                       priv->left = (sample_rate / priv->vtx->playerFreq) * (channels * bits / 8);
+               }
+       }
+
+       return count;
+}
+
+static int vtx_seek(struct input_plugin_data *ip_data, double offset)
+{
+       struct vtx_private *priv = ip_data->private;
+       int sample = sample_rate * offset;
+       int samples_per_frame = sample_rate / priv->vtx->playerFreq;
+       priv->pos = sample / samples_per_frame;
+       if (priv->pos >= priv->vtx->frames) {
+               return 0;
+       }
+       ayemu_vtx_getframe(priv->vtx, priv->pos, priv->regs);
+       priv->left = samples_per_frame - (sample % samples_per_frame);
+       return 0;
+}
+
+static int vtx_read_comments(struct input_plugin_data *ip_data, struct keyval **comments)
+{
+       struct vtx_private *priv = ip_data->private;
+       GROWING_KEYVALS(c);
+       const char *str;
+
+       str = priv->vtx->author;
+       if (str && str[0])
+               comments_add_const(&c, "artist", str);
+       str = priv->vtx->from;
+       if (str && str[0])
+               comments_add_const(&c, "album", str);
+       str = priv->vtx->title;
+       if (str && str[0])
+               comments_add_const(&c, "title", str);
+       int year = priv->vtx->year;
+       if (year > 0) {
+               char buf[16] = {0};
+               snprintf(buf, sizeof buf, "%d", year);
+               comments_add_const(&c, "date", buf);
+       }
+       str = priv->vtx->comment;
+       if (str && str[0])
+               comments_add_const(&c, "comment", str);
+
+       keyvals_terminate(&c);
+       *comments = c.keyvals;
+       return 0;
+}
+
+static int vtx_duration(struct input_plugin_data *ip_data)
+{
+       struct vtx_private *priv = ip_data->private;
+
+       return (int)(priv->vtx->frames / priv->vtx->playerFreq);
+}
+
+static long vtx_bitrate(struct input_plugin_data *ip_data)
+{
+       return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+}
+
+static long vtx_current_bitrate(struct input_plugin_data *ip_data)
+{
+       return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+}
+
+static char *vtx_codec(struct input_plugin_data *ip_data)
+{
+       return xstrdup("vtx");
+}
+
+static char *vtx_codec_profile(struct input_plugin_data *ip_data)
+{
+       return NULL;
+}
+
+const struct input_plugin_ops ip_ops = {
+       .open = vtx_open,
+       .close = vtx_close,
+       .read = vtx_read,
+       .seek = vtx_seek,
+       .read_comments = vtx_read_comments,
+       .duration = vtx_duration,
+       .bitrate = vtx_bitrate,
+       .bitrate_current = vtx_current_bitrate,
+       .codec = vtx_codec,
+       .codec_profile = vtx_codec_profile
+};
+
+const int ip_priority = 50;
+const char * const ip_extensions[] = {"vtx", NULL};
+const char * const ip_mime_types[] = { NULL };
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/ip/wav.c b/ip/wav.c
new file mode 100644 (file)
index 0000000..b25db61
--- /dev/null
+++ b/ip/wav.c
@@ -0,0 +1,417 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "file.h"
+#include "xmalloc.h"
+#include "debug.h"
+#include "utils.h"
+#include "comment.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <errno.h>
+
+#include <sys/types.h>
+#include <unistd.h>
+
+#define WAVE_FORMAT_PCM        0x0001U
+#define WAVE_FORMAT_EXTENSIBLE 0xfffeU
+
+#define WAVE_WRONG_HEADER 1
+
+struct wav_private {
+       off_t pcm_start;
+       unsigned int pcm_size;
+       unsigned int pos;
+
+       /* size of one second of data */
+       unsigned int sec_size;
+
+       unsigned int frame_size;
+};
+
+static int read_chunk_header(int fd, char *name, unsigned int *size)
+{
+       int rc;
+       char buf[8];
+
+       rc = read_all(fd, buf, 8);
+       if (rc == -1)
+               return -IP_ERROR_ERRNO;
+       if (rc != 8)
+               return -IP_ERROR_FILE_FORMAT;
+       *size = read_le32(buf + 4);
+       memmove(name, buf, 4);
+       return 0;
+}
+
+static int read_named_chunk_header(int fd, const char *name, unsigned int *size)
+{
+       int rc;
+       char buf[4];
+
+       rc = read_chunk_header(fd, buf, size);
+       if (rc)
+               return rc;
+       if (memcmp(buf, name, 4))
+               return WAVE_WRONG_HEADER;
+       return 0;
+}
+
+static int find_chunk(int fd, const char *name, unsigned int *size)
+{
+       int rc;
+
+       do {
+               rc = read_named_chunk_header(fd, name, size);
+               if (rc != WAVE_WRONG_HEADER)
+                       return rc;
+               d_print("seeking %u\n", *size);
+               if (lseek(fd, *size, SEEK_CUR) == -1) {
+                       d_print("seek failed\n");
+                       return -IP_ERROR_ERRNO;
+               }
+       } while (1);
+}
+
+static int wav_open(struct input_plugin_data *ip_data)
+{
+       struct wav_private *priv;
+       char buf[4];
+       char *fmt;
+       int rc;
+       unsigned int riff_size, fmt_size;
+       int save;
+
+       d_print("file: %s\n", ip_data->filename);
+       priv = xnew(struct wav_private, 1);
+       ip_data->private = priv;
+       rc = read_named_chunk_header(ip_data->fd, "RIFF", &riff_size);
+       if (rc == WAVE_WRONG_HEADER)
+               rc = -IP_ERROR_FILE_FORMAT;
+       if (rc)
+               goto error_exit;
+       rc = read_all(ip_data->fd, buf, 4);
+       if (rc == -1) {
+               rc = -IP_ERROR_ERRNO;
+               goto error_exit;
+       }
+       if (rc != 4 || memcmp(buf, "WAVE", 4) != 0) {
+               rc = -IP_ERROR_FILE_FORMAT;
+               goto error_exit;
+       }
+
+       rc = find_chunk(ip_data->fd, "fmt ", &fmt_size);
+       if (rc)
+               goto error_exit;
+       if (fmt_size < 16) {
+               d_print("size of \"fmt \" chunk is invalid (%u)\n", fmt_size);
+               rc = -IP_ERROR_FILE_FORMAT;
+               goto error_exit;
+       }
+       fmt = xnew(char, fmt_size);
+       rc = read_all(ip_data->fd, fmt, fmt_size);
+       if (rc == -1) {
+               save = errno;
+               free(fmt);
+               errno = save;
+               rc = -IP_ERROR_ERRNO;
+               goto error_exit;
+       }
+       if (rc != fmt_size) {
+               save = errno;
+               free(fmt);
+               errno = save;
+               rc = -IP_ERROR_FILE_FORMAT;
+               goto error_exit;
+       }
+       {
+               unsigned int format_tag, channels, rate, bits, channel_mask = 0;
+
+               format_tag = read_le16(fmt + 0);
+               channels = read_le16(fmt + 2);
+               rate = read_le32(fmt + 4);
+               /* 4 bytes, bytes per second */
+               /* 2 bytes, bytes per sample */
+               bits = read_le16(fmt + 14);
+               if (format_tag == WAVE_FORMAT_EXTENSIBLE) {
+                       unsigned int ext_size, valid_bits;
+                       if (fmt_size < 18) {
+                               free(fmt);
+                               d_print("size of \"fmt \" chunk is invalid (%u)\n", fmt_size);
+                               rc = -IP_ERROR_FILE_FORMAT;
+                               goto error_exit;
+                       }
+                       ext_size = read_le16(fmt + 16);
+                       if (ext_size < 22) {
+                               free(fmt);
+                               d_print("size of \"fmt \" chunk extension is invalid (%u)\n", ext_size);
+                               rc = -IP_ERROR_FILE_FORMAT;
+                               goto error_exit;
+                       }
+                       valid_bits = read_le16(fmt + 18);
+                       if (valid_bits != bits) {
+                               free(fmt);
+                               d_print("padded samples are not supported (%u != %u)\n", bits, valid_bits);
+                               rc = -IP_ERROR_FILE_FORMAT;
+                               goto error_exit;
+                       }
+                       channel_mask = read_le32(fmt + 20);
+                       format_tag = read_le16(fmt + 24);
+                       /* ignore rest of extension tag */
+               }
+               free(fmt);
+
+               if (format_tag != WAVE_FORMAT_PCM) {
+                       d_print("unsupported format tag %u, should be 1\n", format_tag);
+                       rc = -IP_ERROR_UNSUPPORTED_FILE_TYPE;
+                       goto error_exit;
+               }
+               if ((bits != 8 && bits != 16 && bits != 24 && bits != 32) || channels < 1) {
+                       rc = -IP_ERROR_SAMPLE_FORMAT;
+                       goto error_exit;
+               }
+               ip_data->sf = sf_channels(channels) | sf_rate(rate) | sf_bits(bits) |
+                       sf_signed(bits > 8);
+               channel_map_init_waveex(channels, channel_mask, ip_data->channel_map);
+       }
+
+       rc = find_chunk(ip_data->fd, "data", &priv->pcm_size);
+       if (rc)
+               goto error_exit;
+       priv->pcm_start = lseek(ip_data->fd, 0, SEEK_CUR);
+       if (priv->pcm_start == -1) {
+               rc = -IP_ERROR_ERRNO;
+               goto error_exit;
+       }
+
+       priv->sec_size = sf_get_second_size(ip_data->sf);
+       priv->frame_size = sf_get_frame_size(ip_data->sf);
+       priv->pos = 0;
+
+       d_print("pcm start: %u\n", (unsigned int)priv->pcm_start);
+       d_print("pcm size: %u\n", priv->pcm_size);
+       d_print("\n");
+       d_print("sr: %d, ch: %d, bits: %d, signed: %d\n", sf_get_rate(ip_data->sf),
+                       sf_get_channels(ip_data->sf), sf_get_bits(ip_data->sf),
+                       sf_get_signed(ip_data->sf));
+
+       /* clamp pcm_size to full frames (file might be corrupt or truncated) */
+       priv->pcm_size -= priv->pcm_size % sf_get_frame_size(ip_data->sf);
+       return 0;
+error_exit:
+       save = errno;
+       free(priv);
+       errno = save;
+       return rc;
+}
+
+static int wav_close(struct input_plugin_data *ip_data)
+{
+       struct wav_private *priv;
+
+       priv = ip_data->private;
+       free(priv);
+       ip_data->private = NULL;
+       return 0;
+}
+
+static int wav_read(struct input_plugin_data *ip_data, char *buffer, int _count)
+{
+       struct wav_private *priv = ip_data->private;
+       unsigned int count = _count;
+       int rc;
+
+       if (priv->pos == priv->pcm_size) {
+               /* eof */
+               return 0;
+       }
+       if (count > priv->pcm_size - priv->pos)
+               count = priv->pcm_size - priv->pos;
+       rc = read(ip_data->fd, buffer, count);
+       if (rc == -1) {
+               d_print("read error\n");
+               return -IP_ERROR_ERRNO;
+       }
+       if (rc == 0) {
+               d_print("eof\n");
+               return 0;
+       }
+       priv->pos += rc;
+       return rc;
+}
+
+static int wav_seek(struct input_plugin_data *ip_data, double _offset)
+{
+       struct wav_private *priv = ip_data->private;
+       unsigned int offset;
+
+       offset = (unsigned int)(_offset * (double)priv->sec_size + 0.5);
+       /* align to frame size */
+       offset -= offset % priv->frame_size;
+       priv->pos = offset;
+       if (lseek(ip_data->fd, priv->pcm_start + offset, SEEK_SET) == -1)
+               return -1;
+       return 0;
+}
+
+static struct {
+       const char *old;
+       const char *new;
+} key_map[] = {
+       { "IART", "artist" },
+       { "ICMT", "comment" },
+       { "ICOP", "copyright" },
+       { "ICRD", "date" },
+       { "IGNR", "genre" },
+       { "INAM", "title" },
+       { "IPRD", "album" },
+       { "IPRT", "tracknumber" },
+       { "ISFT", "software" },
+       { NULL, NULL }
+};
+
+static const char *lookup_key(const char *key)
+{
+       int i;
+       for (i = 0; key_map[i].old; i++) {
+               if (!strcasecmp(key, key_map[i].old))
+                       return key_map[i].new;
+       }
+       return NULL;
+}
+
+static int wav_read_comments(struct input_plugin_data *ip_data,
+               struct keyval **comments)
+{
+       GROWING_KEYVALS(c);
+       struct wav_private *priv;
+       unsigned int size;
+       char id[4+1];
+       int rc = 0;
+
+       priv = ip_data->private;
+       id[4] = '\0';
+
+       if (lseek(ip_data->fd, 12, SEEK_SET) == -1) {
+               rc = -1;
+               goto out;
+       }
+
+       while (1) {
+               rc = read_chunk_header(ip_data->fd, id, &size);
+               if (rc)
+                       break;
+               if (strcmp(id, "data") == 0) {
+                       rc = 0;
+                       break;
+               } else if (strcmp(id, "LIST") == 0) {
+                       char buf[4];
+                       rc = read_all(ip_data->fd, buf, 4);
+                       if (rc == -1)
+                               break;
+                       if (memcmp(buf, "INFO", 4) == 0)
+                               continue;
+                       size -= 4;
+               } else {
+                       const char *key = lookup_key(id);
+                       if (key) {
+                               char *val = xnew(char, size + 1);
+                               rc = read_all(ip_data->fd, val, size);
+                               if (rc == -1) {
+                                       free(val);
+                                       break;
+                               }
+                               val[rc] = '\0';
+                               comments_add(&c, key, val);
+                               continue;
+                       }
+               }
+
+               if (lseek(ip_data->fd, size, SEEK_CUR) == -1) {
+                       rc = -1;
+                       break;
+               }
+       }
+
+out:
+       lseek(ip_data->fd, priv->pcm_start, SEEK_SET);
+
+       keyvals_terminate(&c);
+
+       if (rc && c.count == 0) {
+               keyvals_free(c.keyvals);
+               return -1;
+       }
+
+       *comments = c.keyvals;
+       return 0;
+}
+
+static int wav_duration(struct input_plugin_data *ip_data)
+{
+       struct wav_private *priv;
+       int duration;
+
+       priv = ip_data->private;
+       duration = priv->pcm_size / priv->sec_size;
+       return duration;
+}
+
+static long wav_bitrate(struct input_plugin_data *ip_data)
+{
+       sample_format_t sf = ip_data->sf;
+       return sf_get_bits(sf) * sf_get_rate(sf) * sf_get_channels(sf);
+}
+
+static char *wav_codec(struct input_plugin_data *ip_data)
+{
+       char buf[16];
+       snprintf(buf, 16, "pcm_%c%u%s",
+                       sf_get_signed(ip_data->sf) ? 's' : 'u',
+                       sf_get_bits(ip_data->sf),
+                       sf_get_bigendian(ip_data->sf) ? "be" : "le");
+
+       return xstrdup(buf);
+}
+
+static char *wav_codec_profile(struct input_plugin_data *ip_data)
+{
+       return NULL;
+}
+
+const struct input_plugin_ops ip_ops = {
+       .open = wav_open,
+       .close = wav_close,
+       .read = wav_read,
+       .seek = wav_seek,
+       .read_comments = wav_read_comments,
+       .duration = wav_duration,
+       .bitrate = wav_bitrate,
+       .bitrate_current = wav_bitrate,
+       .codec = wav_codec,
+       .codec_profile = wav_codec_profile
+};
+
+const int ip_priority = 50;
+const char * const ip_extensions[] = { "wav", NULL };
+const char * const ip_mime_types[] = { NULL };
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/ip/wavpack.c b/ip/wavpack.c
new file mode 100644 (file)
index 0000000..f30912a
--- /dev/null
@@ -0,0 +1,432 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2007 Johannes Weißl
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ip.h"
+#include "ape.h"
+#include "id3.h"
+#include "xmalloc.h"
+#include "read_wrapper.h"
+#include "debug.h"
+#include "buffer.h"
+#include "comment.h"
+
+#include <wavpack/wavpack.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#define WV_CHANNEL_MAX 2
+
+struct wavpack_file {
+       int fd;
+       off_t len;
+       int push_back_byte;
+};
+
+struct wavpack_private {
+       WavpackContext *wpc;
+       int32_t samples[CHUNK_SIZE * WV_CHANNEL_MAX];
+       struct wavpack_file wv_file;
+       struct wavpack_file wvc_file;
+       unsigned int has_wvc : 1;
+};
+
+/* http://www.wavpack.com/lib_use.txt */
+
+static int32_t read_bytes(void *data, void *ptr, int32_t count)
+{
+       struct wavpack_file *file = data;
+       int32_t rc, n = 0;
+
+       if (file->push_back_byte != EOF) {
+               char *p = ptr;
+               *p = (char) file->push_back_byte;
+               ptr = p + 1;
+               file->push_back_byte = EOF;
+               count--;
+               n++;
+       }
+
+       rc = read(file->fd, ptr, count);
+       if (rc == -1) {
+               d_print("error: %s\n", strerror(errno));
+               return 0;
+       }
+       if (rc == 0) {
+               errno = 0;
+               return 0;
+       }
+       return rc + n;
+}
+
+static uint32_t get_pos(void *data)
+{
+       struct wavpack_file *file = data;
+
+       return lseek(file->fd, 0, SEEK_CUR);
+}
+
+static int set_pos_rel(void *data, int32_t delta, int mode)
+{
+       struct wavpack_file *file = data;
+
+       if (lseek(file->fd, delta, mode) == -1)
+               return -1;
+
+       file->push_back_byte = EOF;
+       return 0;
+}
+
+static int set_pos_abs(void *data, uint32_t pos)
+{
+       return set_pos_rel(data, pos, SEEK_SET);
+}
+
+static int push_back_byte(void *data, int c)
+{
+       struct wavpack_file *file = data;
+
+       if (file->push_back_byte != EOF) {
+               d_print("error: only one byte push back possible!\n");
+               return EOF;
+       }
+       file->push_back_byte = c;
+       return c;
+}
+
+static uint32_t get_length(void *data)
+{
+       struct wavpack_file *file = data;
+       return file->len;
+}
+
+static int can_seek(void *data)
+{
+       struct wavpack_file *file = data;
+       return file->len != -1;
+}
+
+static int32_t write_bytes(void *data, void *ptr, int32_t count)
+{
+       /* we shall not write any bytes */
+       return 0;
+}
+
+
+/*
+ * typedef struct {
+ *     int32_t (*read_bytes)(void *id, void *data, int32_t bcount);
+ *     uint32_t (*get_pos)(void *id);
+ *     int (*set_pos_abs)(void *id, uint32_t pos);
+ *     int (*set_pos_rel)(void *id, int32_t delta, int mode);
+ *     int (*push_back_byte)(void *id, int c);
+ *     uint32_t (*get_length)(void *id);
+ *     int (*can_seek)(void *id);
+ *
+ *     // this callback is for writing edited tags only
+ *     int32_t (*write_bytes)(void *id, void *data, int32_t bcount);
+ * } WavpackStreamReader;
+ */
+static WavpackStreamReader callbacks = {
+       .read_bytes = read_bytes,
+       .get_pos = get_pos,
+       .set_pos_abs = set_pos_abs,
+       .set_pos_rel = set_pos_rel,
+       .push_back_byte = push_back_byte,
+       .get_length = get_length,
+       .can_seek = can_seek,
+       .write_bytes = write_bytes
+};
+
+static int wavpack_open(struct input_plugin_data *ip_data)
+{
+       struct wavpack_private *priv;
+       struct stat st;
+       char msg[80];
+       int channel_mask = 0;
+
+       const struct wavpack_private priv_init = {
+               .wv_file = {
+                       .fd = ip_data->fd,
+                       .push_back_byte = EOF
+               }
+       };
+
+       priv = xnew(struct wavpack_private, 1);
+       *priv = priv_init;
+       if (!ip_data->remote && fstat(ip_data->fd, &st) == 0) {
+               char *filename_wvc;
+
+               priv->wv_file.len = st.st_size;
+
+               filename_wvc = xnew(char, strlen(ip_data->filename) + 2);
+               sprintf(filename_wvc, "%sc", ip_data->filename);
+               if (stat(filename_wvc, &st) == 0) {
+                       priv->wvc_file.fd = open(filename_wvc, O_RDONLY);
+                       if (priv->wvc_file.fd != -1) {
+                               priv->wvc_file.len = st.st_size;
+                               priv->wvc_file.push_back_byte = EOF;
+                               priv->has_wvc = 1;
+                               d_print("use correction file: %s\n", filename_wvc);
+                       }
+               }
+               free(filename_wvc);
+       } else
+               priv->wv_file.len = -1;
+       ip_data->private = priv;
+
+       *msg = '\0';
+
+       priv->wpc = WavpackOpenFileInputEx(&callbacks, &priv->wv_file,
+                       priv->has_wvc ? &priv->wvc_file : NULL, msg,
+                       OPEN_NORMALIZE, 0);
+
+       if (!priv->wpc) {
+               d_print("WavpackOpenFileInputEx failed: %s\n", msg);
+               free(priv);
+               return -IP_ERROR_FILE_FORMAT;
+       }
+
+       ip_data->sf = sf_rate(WavpackGetSampleRate(priv->wpc))
+               | sf_channels(WavpackGetReducedChannels(priv->wpc))
+               | sf_bits(WavpackGetBitsPerSample(priv->wpc))
+               | sf_signed(1);
+       channel_mask = WavpackGetChannelMask(priv->wpc);
+       channel_map_init_waveex(sf_get_channels(ip_data->sf), channel_mask, ip_data->channel_map);
+       return 0;
+}
+
+static int wavpack_close(struct input_plugin_data *ip_data)
+{
+       struct wavpack_private *priv;
+
+       priv = ip_data->private;
+       priv->wpc = WavpackCloseFile(priv->wpc);
+       if (priv->has_wvc)
+               close(priv->wvc_file.fd);
+       free(priv);
+       ip_data->private = NULL;
+       return 0;
+}
+
+/* from wv_engine.cpp (C) 2006 by Peter Lemenkov <lemenkov@newmail.ru> */
+static char *format_samples(int bps, char *dst, int32_t *src, uint32_t count)
+{
+       int32_t temp;
+
+       switch (bps) {
+       case 1:
+               while (count--)
+                       *dst++ = *src++ + 128;
+               break;
+       case 2:
+               while (count--) {
+                       *dst++ = (char) (temp = *src++);
+                       *dst++ = (char) (temp >> 8);
+               }
+               break;
+       case 3:
+               while (count--) {
+                       *dst++ = (char) (temp = *src++);
+                       *dst++ = (char) (temp >> 8);
+                       *dst++ = (char) (temp >> 16);
+               }
+               break;
+       case 4:
+               while (count--) {
+                       *dst++ = (char) (temp = *src++);
+                       *dst++ = (char) (temp >> 8);
+                       *dst++ = (char) (temp >> 16);
+                       *dst++ = (char) (temp >> 24);
+               }
+               break;
+       }
+
+       return dst;
+}
+
+static int wavpack_read(struct input_plugin_data *ip_data, char *buffer, int count)
+{
+       struct wavpack_private *priv;
+       int rc, bps, sample_count, channels;
+
+       priv = ip_data->private;
+       channels = sf_get_channels(ip_data->sf);
+       bps = WavpackGetBytesPerSample(priv->wpc);
+
+       sample_count = count / bps;
+
+       rc = WavpackUnpackSamples(priv->wpc, priv->samples, sample_count / channels);
+       format_samples(bps, buffer, priv->samples, rc * channels);
+       return rc * channels * bps;
+}
+
+static int wavpack_seek(struct input_plugin_data *ip_data, double offset)
+{
+       struct wavpack_private *priv = ip_data->private;
+
+       if (!WavpackSeekSample(priv->wpc, WavpackGetSampleRate(priv->wpc) * offset))
+               return -IP_ERROR_INTERNAL;
+       return 0;
+}
+
+static int wavpack_read_comments(struct input_plugin_data *ip_data,
+               struct keyval **comments)
+{
+       struct id3tag id3;
+       APETAG(ape);
+       GROWING_KEYVALS(c);
+       int fd, rc, save, i;
+
+       fd = open(ip_data->filename, O_RDONLY);
+       if (fd == -1)
+               return -1;
+       d_print("filename: %s\n", ip_data->filename);
+
+       id3_init(&id3);
+       rc = id3_read_tags(&id3, fd, ID3_V1);
+       save = errno;
+       close(fd);
+       errno = save;
+       if (rc) {
+               if (rc == -1) {
+                       d_print("error: %s\n", strerror(errno));
+                       return -1;
+               }
+               d_print("corrupted tag?\n");
+               goto next;
+       }
+
+       for (i = 0; i < NUM_ID3_KEYS; i++) {
+               char *val = id3_get_comment(&id3, i);
+               if (val)
+                       comments_add(&c, id3_key_names[i], val);
+       }
+
+next:
+       id3_free(&id3);
+
+       rc = ape_read_tags(&ape, ip_data->fd, 1);
+       if (rc < 0)
+               goto out;
+
+       for (i = 0; i < rc; i++) {
+               char *k, *v;
+               k = ape_get_comment(&ape, &v);
+               if (!k)
+                       break;
+               comments_add(&c, k, v);
+               free(k);
+       }
+
+out:
+       ape_free(&ape);
+
+       keyvals_terminate(&c);
+       *comments = c.keyvals;
+       return 0;
+}
+
+static int wavpack_duration(struct input_plugin_data *ip_data)
+{
+       struct wavpack_private *priv;
+       int duration;
+
+       priv = ip_data->private;
+       duration = WavpackGetNumSamples(priv->wpc) /
+               WavpackGetSampleRate(priv->wpc);
+
+       return duration;
+}
+
+static long wavpack_bitrate(struct input_plugin_data *ip_data)
+{
+       struct wavpack_private *priv = ip_data->private;
+       double bitrate = WavpackGetAverageBitrate(priv->wpc, 1);
+       if (!bitrate)
+               return -IP_ERROR_FUNCTION_NOT_SUPPORTED;
+       return (long) (bitrate + 0.5);
+}
+
+static long wavpack_current_bitrate(struct input_plugin_data *ip_data)
+{
+       struct wavpack_private *priv = ip_data->private;
+       return WavpackGetInstantBitrate(priv->wpc);
+}
+
+static char *wavpack_codec(struct input_plugin_data *ip_data)
+{
+       return xstrdup("wavpack");
+}
+
+static char *wavpack_codec_profile(struct input_plugin_data *ip_data)
+{
+       struct wavpack_private *priv = ip_data->private;
+       int m = WavpackGetMode(priv->wpc);
+       char buf[32];
+
+       buf[0] = '\0';
+
+       if (m & MODE_FAST)
+               strcat(buf, "fast");
+#ifdef MODE_VERY_HIGH
+       else if (m & MODE_VERY_HIGH)
+               strcat(buf, "very high");
+#endif
+       else if (m & MODE_HIGH)
+               strcat(buf, "high");
+       else
+               strcat(buf, "normal");
+
+       if (m & MODE_HYBRID)
+               strcat(buf, " hybrid");
+
+#ifdef MODE_XMODE
+       if ((m & MODE_EXTRA) && (m & MODE_XMODE)) {
+               char xmode[] = " x0";
+               xmode[2] = ((m & MODE_XMODE) >> 12) + '0';
+               strcat(buf, xmode);
+       }
+#endif
+
+       return xstrdup(buf);
+}
+
+const struct input_plugin_ops ip_ops = {
+       .open = wavpack_open,
+       .close = wavpack_close,
+       .read = wavpack_read,
+       .seek = wavpack_seek,
+       .read_comments = wavpack_read_comments,
+       .duration = wavpack_duration,
+       .bitrate = wavpack_bitrate,
+       .bitrate_current = wavpack_current_bitrate,
+       .codec = wavpack_codec,
+       .codec_profile = wavpack_codec_profile
+};
+
+const int ip_priority = 50;
+const char * const ip_extensions[] = { "wv", NULL };
+const char * const ip_mime_types[] = { "audio/x-wavpack", NULL };
+const struct input_plugin_opt ip_options[] = { { NULL } };
+const unsigned ip_abi_version = IP_ABI_VERSION;
diff --git a/iter.h b/iter.h
new file mode 100644 (file)
index 0000000..654a4c2
--- /dev/null
+++ b/iter.h
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_ITER_H
+#define CMUS_ITER_H
+
+#include <stddef.h> /* NULL */
+
+struct iter {
+       /* this usually points to the list head */
+       void *data0;
+
+       /* these point to the list item, for simple lists data2 is usually NULL */
+       void *data1;
+       void *data2;
+};
+
+static inline void iter_init(struct iter *iter)
+{
+       iter->data0 = NULL;
+       iter->data1 = NULL;
+       iter->data2 = NULL;
+}
+
+static inline void iter_head(struct iter *iter)
+{
+       iter->data1 = NULL;
+       iter->data2 = NULL;
+}
+
+static inline int iters_equal(struct iter *a, struct iter *b)
+{
+       return a->data0 == b->data0 &&
+               a->data1 == b->data1 &&
+               a->data2 == b->data2;
+}
+
+static inline int iter_is_head(struct iter *iter)
+{
+       return iter->data0 != NULL &&
+               iter->data1 == NULL &&
+               iter->data2 == NULL;
+}
+
+static inline int iter_is_null(struct iter *iter)
+{
+       return iter->data0 == NULL &&
+               iter->data1 == NULL &&
+               iter->data2 == NULL;
+}
+
+static inline int iter_is_empty(struct iter *iter)
+{
+       return iter->data0 == NULL || (iter->data1 == NULL && iter->data2 == NULL);
+}
+
+#define GENERIC_ITER_PREV(FUNC, TYPE, MEMBER)                          \
+int FUNC(struct iter *iter)                                            \
+{                                                                      \
+       struct list_head *head = iter->data0;                           \
+       TYPE *e = iter->data1;                                          \
+                                                                       \
+       if (head == NULL)                                               \
+               return 0;                                               \
+       if (e == NULL) {                                                \
+               /* head, get last */                                    \
+               if (head->prev == head) {                               \
+                       /* empty, iter points to the head already */    \
+                       return 0;                                       \
+               }                                                       \
+               iter->data1 = container_of(head->prev, TYPE, MEMBER);   \
+               return 1;                                               \
+       }                                                               \
+       if (e->MEMBER.prev == head) {                                   \
+               iter->data1 = NULL;                                     \
+               return 0;                                               \
+       }                                                               \
+       iter->data1 = container_of(e->MEMBER.prev, TYPE, MEMBER);       \
+       return 1;                                                       \
+}
+
+#define GENERIC_ITER_NEXT(FUNC, TYPE, MEMBER)                          \
+int FUNC(struct iter *iter)                                            \
+{                                                                      \
+       struct list_head *head = iter->data0;                           \
+       TYPE *e = iter->data1;                                          \
+                                                                       \
+       if (head == NULL)                                               \
+               return 0;                                               \
+       if (e == NULL) {                                                \
+               /* head, get first */                                   \
+               if (head->next == head) {                               \
+                       /* empty, iter points to the head already */    \
+                       return 0;                                       \
+               }                                                       \
+               iter->data1 = container_of(head->next, TYPE, MEMBER);   \
+               return 1;                                               \
+       }                                                               \
+       if (e->MEMBER.next == head) {                                   \
+               iter->data1 = NULL;                                     \
+               return 0;                                               \
+       }                                                               \
+       iter->data1 = container_of(e->MEMBER.next, TYPE, MEMBER);       \
+       return 1;                                                       \
+}
+
+#define GENERIC_TREE_ITER_PREV(FUNC, TYPE, MEMBER)                             \
+int FUNC(struct iter *iter)                                                    \
+{                                                                              \
+       struct rb_root *root = iter->data0;                                     \
+       TYPE *e = iter->data1;                                                  \
+                                                                               \
+       if (root == NULL)                                                       \
+               return 0;                                                       \
+       if (e == NULL) {                                                        \
+               /* head, get last */                                            \
+               if (rb_root_empty(root)) {                                      \
+                       /* empty, iter points to the head already */            \
+                       return 0;                                               \
+               }                                                               \
+               iter->data1 = container_of(rb_last(root), TYPE, MEMBER);        \
+               return 1;                                                       \
+       }                                                                       \
+       if (rb_prev(&e->MEMBER) == NULL) {                                      \
+               iter->data1 = NULL;                                             \
+               return 0;                                                       \
+       }                                                                       \
+       iter->data1 = container_of(rb_prev(&e->MEMBER), TYPE, MEMBER);          \
+       return 1;                                                               \
+}
+
+#define GENERIC_TREE_ITER_NEXT(FUNC, TYPE, MEMBER)                             \
+int FUNC(struct iter *iter)                                                    \
+{                                                                              \
+       struct rb_root *root = iter->data0;                                     \
+       TYPE *e = iter->data1;                                                  \
+                                                                               \
+       if (root == NULL)                                                       \
+               return 0;                                                       \
+       if (e == NULL) {                                                        \
+               /* head, get first */                                           \
+               if (rb_root_empty(root)) {                                      \
+                       /* empty, iter points to the head already */            \
+                       return 0;                                               \
+               }                                                               \
+               iter->data1 = container_of(rb_first(root), TYPE, MEMBER);       \
+               return 1;                                                       \
+       }                                                                       \
+       if (rb_next(&e->MEMBER) == NULL) {                                      \
+               iter->data1 = NULL;                                             \
+               return 0;                                                       \
+       }                                                                       \
+       iter->data1 = container_of(rb_next(&e->MEMBER), TYPE, MEMBER);          \
+       return 1;                                                               \
+}
+
+#endif
diff --git a/job.c b/job.c
new file mode 100644 (file)
index 0000000..cd32e0b
--- /dev/null
+++ b/job.c
@@ -0,0 +1,627 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2008 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "utils.h"
+#include "job.h"
+#include "worker.h"
+#include "cache.h"
+#include "xmalloc.h"
+#include "debug.h"
+#include "load_dir.h"
+#include "path.h"
+#include "editable.h"
+#include "pl.h"
+#include "play_queue.h"
+#include "lib.h"
+#include "utils.h"
+#include "file.h"
+#include "cache.h"
+#include "player.h"
+#include "discid.h"
+#include "xstrjoin.h"
+#include "ui_curses.h"
+#include "cue_utils.h"
+
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/mman.h>
+
+enum job_result_var {
+       JOB_RES_ADD,
+       JOB_RES_UPDATE,
+       JOB_RES_UPDATE_CACHE,
+       JOB_RES_PL_DELETE,
+};
+
+enum update_kind {
+       UPDATE_NONE = 0,
+       UPDATE_REMOVE = 1,
+       UPDATE_MTIME_CHANGED = 2,
+};
+
+struct job_result {
+       struct list_head node;
+
+       enum job_result_var var;
+       union {
+               struct {
+                       add_ti_cb add_cb;
+                       size_t add_num;
+                       struct track_info **add_ti;
+                       void *add_opaque;
+               };
+               struct {
+                       size_t update_num;
+                       struct track_info **update_ti;
+                       enum update_kind *update_kind;
+               };
+               struct {
+                       size_t update_cache_num;
+                       struct track_info **update_cache_ti;
+               };
+               struct {
+                       void (*pl_delete_cb)(struct playlist *);
+                       struct playlist *pl_delete_pl;
+               };
+       };
+};
+
+int job_fd;
+static int job_fd_priv;
+
+static LIST_HEAD(job_result_head);
+static pthread_mutex_t job_mutex = CMUS_MUTEX_INITIALIZER;
+
+#define TI_CAP 32
+static struct track_info **ti_buffer;
+static size_t ti_buffer_fill;
+static struct add_data *jd;
+
+#define job_lock() cmus_mutex_lock(&job_mutex)
+#define job_unlock() cmus_mutex_unlock(&job_mutex)
+
+void job_init(void)
+{
+       init_pipes(&job_fd, &job_fd_priv);
+
+       worker_init();
+}
+
+void job_exit(void)
+{
+       worker_remove_jobs_by_type(JOB_TYPE_ANY);
+       worker_exit();
+
+       close(job_fd);
+       close(job_fd_priv);
+}
+
+static void job_push_result(struct job_result *res)
+{
+       job_lock();
+       list_add_tail(&res->node, &job_result_head);
+       job_unlock();
+
+       notify_via_pipe(job_fd_priv);
+}
+
+static struct job_result *job_pop_result(void)
+{
+       struct job_result *res = NULL;
+
+       job_lock();
+       if (!list_empty(&job_result_head)) {
+               struct list_head *item = job_result_head.next;
+               list_del(item);
+               res = container_of(item, struct job_result, node);
+       }
+       job_unlock();
+
+       return res;
+}
+
+static void flush_ti_buffer(void)
+{
+       struct job_result *res = xnew(struct job_result, 1);
+
+       res->var = JOB_RES_ADD;
+       res->add_cb = jd->add;
+       res->add_num = ti_buffer_fill;
+       res->add_ti = ti_buffer;
+       res->add_opaque = jd->opaque;
+
+       job_push_result(res);
+
+       ti_buffer_fill = 0;
+       ti_buffer = NULL;
+}
+
+static void add_ti(struct track_info *ti)
+{
+       if (ti_buffer_fill == TI_CAP)
+               flush_ti_buffer();
+       if (!ti_buffer)
+               ti_buffer = xnew(struct track_info *, TI_CAP);
+       ti_buffer[ti_buffer_fill++] = ti;
+}
+
+static int add_file_cue(const char *filename);
+
+static void add_file(const char *filename, int force)
+{
+       struct track_info *ti;
+
+       if (!is_cue_url(filename)) {
+               if (force || lookup_cache_entry(filename, hash_str(filename)) == NULL) {
+                       int done = add_file_cue(filename);
+                       if (done)
+                               return;
+               }
+       }
+
+       cache_lock();
+       ti = cache_get_ti(filename, force);
+       cache_unlock();
+
+       if (ti)
+               add_ti(ti);
+}
+
+static int add_file_cue(const char *filename)
+{
+       int n_tracks;
+       char *url;
+       char *cue_filename;
+
+       cue_filename = associated_cue(filename);
+       if (cue_filename == NULL)
+               return 0;
+
+       n_tracks = cue_get_ntracks(cue_filename);
+       if (n_tracks <= 0) {
+               free(cue_filename);
+               return 0;
+       }
+
+       for (int i = 1; i <= n_tracks; ++i) {
+               url = construct_cue_url(cue_filename, i);
+               add_file(url, 0);
+               free(url);
+       }
+
+       free(cue_filename);
+       return 1;
+}
+
+static void add_url(const char *url)
+{
+       add_file(url, 0);
+}
+
+static void add_cdda(const char *url)
+{
+       char *disc_id = NULL;
+       int start_track = 1, end_track = -1;
+
+       parse_cdda_url(url, &disc_id, &start_track, &end_track);
+
+       if (end_track != -1) {
+               int i;
+               for (i = start_track; i <= end_track; i++) {
+                       char *new_url = gen_cdda_url(disc_id, i, -1);
+                       add_file(new_url, 0);
+                       free(new_url);
+               }
+       } else
+               add_file(url, 0);
+       free(disc_id);
+}
+
+static int dir_entry_cmp(const void *ap, const void *bp)
+{
+       struct dir_entry *a = *(struct dir_entry **)ap;
+       struct dir_entry *b = *(struct dir_entry **)bp;
+
+       return strcmp(a->name, b->name);
+}
+
+static int dir_entry_cmp_reverse(const void *ap, const void *bp)
+{
+       struct dir_entry *a = *(struct dir_entry **)ap;
+       struct dir_entry *b = *(struct dir_entry **)bp;
+
+       return strcmp(b->name, a->name);
+}
+
+static int points_within_and_visible(const char *target, const char *root)
+{
+       int tlen = strlen(target);
+       int rlen = strlen(root);
+
+       if (rlen > tlen)
+               return 0;
+       if (strncmp(target, root, rlen))
+               return 0;
+       if (target[rlen] != '/' && target[rlen] != '\0')
+               return 0;
+       /* assume the path is normalized */
+       if (strstr(target + rlen, "/."))
+               return 0;
+
+       return 1;
+}
+
+static void add_dir(const char *dirname, const char *root)
+{
+       struct directory dir;
+       struct dir_entry **ents;
+       const char *name;
+       PTR_ARRAY(array);
+       int i;
+
+       if (dir_open(&dir, dirname)) {
+               d_print("error: opening %s: %s\n", dirname, strerror(errno));
+               return;
+       }
+       while ((name = dir_read(&dir))) {
+               struct dir_entry *ent;
+               int size;
+
+               if (name[0] == '.')
+                       continue;
+
+               if (dir.is_link) {
+                       char buf[1024];
+                       char *target;
+                       int rc = readlink(dir.path, buf, sizeof(buf));
+
+                       if (rc < 0 || rc == sizeof(buf))
+                               continue;
+                       buf[rc] = 0;
+                       target = path_absolute_cwd(buf, dirname);
+                       if (points_within_and_visible(target, root)) {
+                               d_print("%s -> %s points within %s. ignoring\n",
+                                               dir.path, target, root);
+                               free(target);
+                               continue;
+                       }
+                       free(target);
+               }
+
+               size = strlen(name) + 1;
+               ent = xmalloc(sizeof(struct dir_entry) + size);
+               ent->mode = dir.st.st_mode;
+               memcpy(ent->name, name, size);
+               ptr_array_add(&array, ent);
+       }
+       dir_close(&dir);
+
+       if (jd->add == play_queue_prepend) {
+               ptr_array_sort(&array, dir_entry_cmp_reverse);
+       } else {
+               ptr_array_sort(&array, dir_entry_cmp);
+       }
+       ents = array.ptrs;
+       for (i = 0; i < array.count; i++) {
+               if (!worker_cancelling()) {
+                       /* abuse dir.path because
+                        *  - it already contains dirname + '/'
+                        *  - it is guaranteed to be large enough
+                        */
+                       int len = strlen(ents[i]->name);
+
+                       memcpy(dir.path + dir.len, ents[i]->name, len + 1);
+                       if (S_ISDIR(ents[i]->mode)) {
+                               add_dir(dir.path, root);
+                       } else {
+                               add_file(dir.path, 0);
+                       }
+               }
+               free(ents[i]);
+       }
+       free(ents);
+}
+
+static int handle_line(void *data, const char *line)
+{
+       if (worker_cancelling())
+               return 1;
+
+       if (is_http_url(line) || is_cue_url(line)) {
+               add_url(line);
+       } else {
+               char *absolute = path_absolute_cwd(line, data);
+               add_file(absolute, 0);
+               free(absolute);
+       }
+
+       return 0;
+}
+
+static void add_pl(const char *filename)
+{
+       char *buf;
+       ssize_t size;
+       int reverse;
+
+       buf = mmap_file(filename, &size);
+       if (size == -1)
+               return;
+
+       if (buf) {
+               char *cwd = xstrjoin(filename, "/..");
+               /* beautiful hack */
+               reverse = jd->add == play_queue_prepend;
+
+               cmus_playlist_for_each(buf, size, reverse, handle_line, cwd);
+               free(cwd);
+               munmap(buf, size);
+       }
+}
+
+static void do_add_job(void *data)
+{
+       jd = data;
+       switch (jd->type) {
+       case FILE_TYPE_URL:
+               add_url(jd->name);
+               break;
+       case FILE_TYPE_CDDA:
+               add_cdda(jd->name);
+               break;
+       case FILE_TYPE_PL:
+               add_pl(jd->name);
+               break;
+       case FILE_TYPE_DIR:
+               add_dir(jd->name, jd->name);
+               break;
+       case FILE_TYPE_FILE:
+               add_file(jd->name, jd->force);
+               break;
+       case FILE_TYPE_INVALID:
+               break;
+       }
+       if (ti_buffer)
+               flush_ti_buffer();
+       jd = NULL;
+}
+
+static void free_add_job(void *data)
+{
+       struct add_data *d = data;
+       free(d->name);
+       free(d);
+}
+
+static void job_handle_add_result(struct job_result *res)
+{
+       for (size_t i = 0; i < res->add_num; i++) {
+               res->add_cb(res->add_ti[i], res->add_opaque);
+               track_info_unref(res->add_ti[i]);
+       }
+
+       free(res->add_ti);
+}
+
+void job_schedule_add(int type, struct add_data *data)
+{
+       worker_add_job(type | JOB_TYPE_ADD, do_add_job, free_add_job, data);
+}
+
+static void do_update_job(void *data)
+{
+       struct update_data *d = data;
+       int i;
+       enum update_kind *kind = xnew(enum update_kind, d->used);
+       struct job_result *res;
+
+       for (i = 0; i < d->used; i++) {
+               struct track_info *ti = d->ti[i];
+               struct stat s;
+               int rc;
+
+               rc = stat(ti->filename, &s);
+               if (rc || d->force || ti->mtime != s.st_mtime || ti->duration == 0) {
+                       kind[i] = UPDATE_NONE;
+                       if (!is_cue_url(ti->filename) && !is_http_url(ti->filename) && rc)
+                               kind[i] |= UPDATE_REMOVE;
+                       else if (ti->mtime != s.st_mtime)
+                               kind[i] |= UPDATE_MTIME_CHANGED;
+               } else {
+                       track_info_unref(ti);
+                       d->ti[i] = NULL;
+               }
+       }
+
+       res = xnew(struct job_result, 1);
+
+       res->var = JOB_RES_UPDATE;
+       res->update_num = d->used;
+       res->update_ti = d->ti;
+       res->update_kind = kind;
+
+       job_push_result(res);
+
+       d->ti = NULL;
+}
+
+static void free_update_job(void *data)
+{
+       struct update_data *d = data;
+
+       if (d->ti) {
+               for (size_t i = 0; i < d->used; i++)
+                       track_info_unref(d->ti[i]);
+               free(d->ti);
+       }
+       free(d);
+}
+
+static void job_handle_update_result(struct job_result *res)
+{
+       for (size_t i = 0; i < res->update_num; i++) {
+               struct track_info *ti = res->update_ti[i];
+               int force;
+
+               if (!ti)
+                       continue;
+
+               lib_remove(ti);
+
+               cache_lock();
+               cache_remove_ti(ti);
+               cache_unlock();
+
+               if (res->update_kind[i] & UPDATE_REMOVE) {
+                       d_print("removing dead file %s\n", ti->filename);
+               } else {
+                       if (res->update_kind[i] & UPDATE_MTIME_CHANGED)
+                               d_print("mtime changed: %s\n", ti->filename);
+                       force = ti->duration == 0;
+                       cmus_add(lib_add_track, ti->filename, FILE_TYPE_FILE,
+                                       JOB_TYPE_LIB, force, NULL);
+               }
+
+               track_info_unref(ti);
+       }
+
+       free(res->update_kind);
+       free(res->update_ti);
+}
+
+void job_schedule_update(struct update_data *data)
+{
+       worker_add_job(JOB_TYPE_LIB | JOB_TYPE_UPDATE, do_update_job,
+                       free_update_job, data);
+}
+
+static void do_update_cache_job(void *data)
+{
+       struct update_cache_data *d = data;
+       int count;
+       struct track_info **tis;
+       struct job_result *res;
+
+       cache_lock();
+       tis = cache_refresh(&count, d->force);
+       cache_unlock();
+
+       res = xnew(struct job_result, 1);
+       res->var = JOB_RES_UPDATE_CACHE;
+       res->update_cache_ti = tis;
+       res->update_cache_num = count;
+       job_push_result(res);
+}
+
+static void free_update_cache_job(void *data)
+{
+       free(data);
+}
+
+static void job_handle_update_cache_result(struct job_result *res)
+{
+       for (size_t i = 0; i < res->update_cache_num; i++) {
+               struct track_info *new, *old = res->update_cache_ti[i];
+
+               if (!old)
+                       continue;
+
+               new = old->next;
+               if (lib_remove(old) && new)
+                       lib_add_track(new, NULL);
+               pl_update_track(old, new);
+               editable_update_track(&pq_editable, old, new);
+               if (player_info.ti == old && new) {
+                       track_info_ref(new);
+                       player_file_changed(new);
+               }
+
+               track_info_unref(old);
+               if (new)
+                       track_info_unref(new);
+       }
+       free(res->update_cache_ti);
+}
+
+void job_schedule_update_cache(int type, struct update_cache_data *data)
+{
+       worker_add_job(type | JOB_TYPE_UPDATE_CACHE, do_update_cache_job,
+                       free_update_cache_job, data);
+}
+
+static void do_pl_delete_job(void *data)
+{
+       /*
+        * If PL jobs are canceled this function won't run. Hence we push the
+        * result in the free function.
+        */
+}
+
+static void free_pl_delete_job(void *data)
+{
+       struct pl_delete_data *pdd = data;
+       struct job_result *res;
+
+       res = xnew(struct job_result, 1);
+       res->var = JOB_RES_PL_DELETE;
+       res->pl_delete_cb = pdd->cb;
+       res->pl_delete_pl = pdd->pl;
+       job_push_result(res);
+
+       free(pdd);
+}
+
+static void job_handle_pl_delete_result(struct job_result *res)
+{
+       res->pl_delete_cb(res->pl_delete_pl);
+}
+
+void job_schedule_pl_delete(struct pl_delete_data *data)
+{
+       worker_add_job(JOB_TYPE_PL | JOB_TYPE_DELETE, do_pl_delete_job,
+                       free_pl_delete_job, data);
+}
+
+static void job_handle_result(struct job_result *res)
+{
+       switch (res->var) {
+       case JOB_RES_ADD:
+               job_handle_add_result(res);
+               break;
+       case JOB_RES_UPDATE:
+               job_handle_update_result(res);
+               break;
+       case JOB_RES_UPDATE_CACHE:
+               job_handle_update_cache_result(res);
+               break;
+       case JOB_RES_PL_DELETE:
+               job_handle_pl_delete_result(res);
+               break;
+       }
+       free(res);
+}
+
+void job_handle(void)
+{
+       clear_pipe(job_fd, -1);
+
+       struct job_result *res;
+       while ((res = job_pop_result()))
+               job_handle_result(res);
+}
diff --git a/job.h b/job.h
new file mode 100644 (file)
index 0000000..a043982
--- /dev/null
+++ b/job.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2008 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_JOB_H
+#define CMUS_JOB_H
+
+#include "cmus.h"
+
+#define JOB_TYPE_LIB   1 << 0
+#define JOB_TYPE_PL    1 << 1
+#define JOB_TYPE_QUEUE 1 << 2
+
+#define JOB_TYPE_ADD          1 << 16
+#define JOB_TYPE_UPDATE       1 << 17
+#define JOB_TYPE_UPDATE_CACHE 1 << 18
+#define JOB_TYPE_DELETE       1 << 19
+
+struct add_data {
+       enum file_type type;
+       char *name;
+       add_ti_cb add;
+       void *opaque;
+       unsigned int force : 1;
+};
+
+struct update_data {
+       size_t size;
+       size_t used;
+       struct track_info **ti;
+       unsigned int force : 1;
+};
+
+struct update_cache_data {
+       unsigned int force : 1;
+};
+
+struct pl_delete_data {
+       struct playlist *pl;
+       void (*cb)(struct playlist *);
+};
+
+extern int job_fd;
+
+void job_init(void);
+void job_exit(void);
+void job_schedule_add(int type, struct add_data *data);
+void job_schedule_update(struct update_data *data);
+void job_schedule_update_cache(int type, struct update_cache_data *data);
+void job_schedule_pl_delete(struct pl_delete_data *data);
+void job_handle(void);
+
+#endif
diff --git a/keys.c b/keys.c
new file mode 100644 (file)
index 0000000..b7256a7
--- /dev/null
+++ b/keys.c
@@ -0,0 +1,769 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2006 Timo Hirvonen
+ *
+ * keys.[ch] by Frank Terbeck <ft@bewatermyfriend.org>
+ * heavily modified by Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "keys.h"
+#include "help.h"
+#include "ui_curses.h"
+#include "command_mode.h"
+#include "xmalloc.h"
+
+#include "window.h"
+#include "options.h"
+#include "editable.h"
+#include "lib.h"
+
+const char * const key_context_names[NR_CTXS + 1] = {
+       "browser",
+       "common",
+       "filters",
+       "library",
+       "playlist",
+       "queue",
+       "settings",
+       NULL
+};
+
+struct binding *key_bindings[NR_CTXS] = { NULL, };
+
+static const enum key_context view_to_context[] = {
+       CTX_LIBRARY,
+       CTX_LIBRARY,
+       CTX_PLAYLIST,
+       CTX_QUEUE,
+       CTX_BROWSER,
+       CTX_FILTERS,
+       CTX_SETTINGS,
+};
+
+#define KEY_IS_CHAR -255
+
+#define KEY_MLB_CLICK                  0
+#define KEY_MLB_CLICK_SEL              1
+#define KEY_MRB_CLICK                  2
+#define KEY_MRB_CLICK_SEL              3
+#define KEY_MSCRL_UP                   4
+#define KEY_MSCRL_DOWN                 5
+
+/* key_table {{{
+ *
+ * key: KEY_IS_CHAR, not a key
+ * ch:  0, not a char
+ * key: KEY_MOUSE, neither key, nor char
+ */
+const struct key key_table[] = {
+       { "!",                  KEY_IS_CHAR,            33      },
+       { "\"",                 KEY_IS_CHAR,            34      },
+       { "#",                  KEY_IS_CHAR,            35      },
+       { "$",                  KEY_IS_CHAR,            36      },
+       { "%",                  KEY_IS_CHAR,            37      },
+       { "&",                  KEY_IS_CHAR,            38      },
+       { "'",                  KEY_IS_CHAR,            39      },
+       { "(",                  KEY_IS_CHAR,            40      },
+       { ")",                  KEY_IS_CHAR,            41      },
+       { "*",                  KEY_IS_CHAR,            42      },
+       { "+",                  KEY_IS_CHAR,            43      },
+       { ",",                  KEY_IS_CHAR,            44      },
+       { "-",                  KEY_IS_CHAR,            45      },
+       { ".",                  KEY_IS_CHAR,            46      },
+       { "/",                  KEY_IS_CHAR,            47      },
+       { "0",                  KEY_IS_CHAR,            48      },
+       { "1",                  KEY_IS_CHAR,            49      },
+       { "2",                  KEY_IS_CHAR,            50      },
+       { "3",                  KEY_IS_CHAR,            51      },
+       { "4",                  KEY_IS_CHAR,            52      },
+       { "5",                  KEY_IS_CHAR,            53      },
+       { "6",                  KEY_IS_CHAR,            54      },
+       { "7",                  KEY_IS_CHAR,            55      },
+       { "8",                  KEY_IS_CHAR,            56      },
+       { "9",                  KEY_IS_CHAR,            57      },
+       { ":",                  KEY_IS_CHAR,            58      },
+       { ";",                  KEY_IS_CHAR,            59      },
+       { "<",                  KEY_IS_CHAR,            60      },
+       { "=",                  KEY_IS_CHAR,            61      },
+       { ">",                  KEY_IS_CHAR,            62      },
+       { "?",                  KEY_IS_CHAR,            63      },
+       { "@",                  KEY_IS_CHAR,            64      },
+       { "A",                  KEY_IS_CHAR,            65      },
+       { "B",                  KEY_IS_CHAR,            66      },
+       { "C",                  KEY_IS_CHAR,            67      },
+       { "D",                  KEY_IS_CHAR,            68      },
+       { "E",                  KEY_IS_CHAR,            69      },
+       { "F",                  KEY_IS_CHAR,            70      },
+       { "F1",                 KEY_F(1),               0       },
+       { "F10",                KEY_F(10),              0       },
+       { "F11",                KEY_F(11),              0       },
+       { "F12",                KEY_F(12),              0       },
+       { "F2",                 KEY_F(2),               0       },
+       { "F3",                 KEY_F(3),               0       },
+       { "F4",                 KEY_F(4),               0       },
+       { "F5",                 KEY_F(5),               0       },
+       { "F6",                 KEY_F(6),               0       },
+       { "F7",                 KEY_F(7),               0       },
+       { "F8",                 KEY_F(8),               0       },
+       { "F9",                 KEY_F(9),               0       },
+       { "G",                  KEY_IS_CHAR,            71      },
+       { "H",                  KEY_IS_CHAR,            72      },
+       { "I",                  KEY_IS_CHAR,            73      },
+       { "J",                  KEY_IS_CHAR,            74      },
+       { "K",                  KEY_IS_CHAR,            75      },
+       { "KP_center",          KEY_B2,                 0       },
+       { "KP_lower_left",      KEY_C1,                 0       },
+       { "KP_lower_right",     KEY_C3,                 0       },
+       { "KP_upper_left",      KEY_A1,                 0       },
+       { "KP_upper_right",     KEY_A3,                 0       },
+       { "L",                  KEY_IS_CHAR,            76      },
+       { "M",                  KEY_IS_CHAR,            77      },
+       { "M-!",                KEY_IS_CHAR,            161     },
+       { "M-\"",               KEY_IS_CHAR,            162     },
+       { "M-#",                KEY_IS_CHAR,            163     },
+       { "M-$",                KEY_IS_CHAR,            164     },
+       { "M-%",                KEY_IS_CHAR,            165     },
+       { "M-&",                KEY_IS_CHAR,            166     },
+       { "M-'",                KEY_IS_CHAR,            167     },
+       { "M-(",                KEY_IS_CHAR,            168     },
+       { "M-)",                KEY_IS_CHAR,            169     },
+       { "M-*",                KEY_IS_CHAR,            170     },
+       { "M-+",                KEY_IS_CHAR,            171     },
+       { "M-,",                KEY_IS_CHAR,            172     },
+       { "M--",                KEY_IS_CHAR,            173     },
+       { "M-.",                KEY_IS_CHAR,            174     },
+       { "M-/",                KEY_IS_CHAR,            175     },
+       { "M-0",                KEY_IS_CHAR,            176     },
+       { "M-1",                KEY_IS_CHAR,            177     },
+       { "M-2",                KEY_IS_CHAR,            178     },
+       { "M-3",                KEY_IS_CHAR,            179     },
+       { "M-4",                KEY_IS_CHAR,            180     },
+       { "M-5",                KEY_IS_CHAR,            181     },
+       { "M-6",                KEY_IS_CHAR,            182     },
+       { "M-7",                KEY_IS_CHAR,            183     },
+       { "M-8",                KEY_IS_CHAR,            184     },
+       { "M-9",                KEY_IS_CHAR,            185     },
+       { "M-:",                KEY_IS_CHAR,            186     },
+       { "M-;",                KEY_IS_CHAR,            187     },
+       { "M-<",                KEY_IS_CHAR,            188     },
+       { "M-=",                KEY_IS_CHAR,            189     },
+       { "M->",                KEY_IS_CHAR,            190     },
+       { "M-?",                KEY_IS_CHAR,            191     },
+       { "M-@",                KEY_IS_CHAR,            192     },
+       { "M-A",                KEY_IS_CHAR,            193     },
+       { "M-B",                KEY_IS_CHAR,            194     },
+       { "M-C",                KEY_IS_CHAR,            195     },
+       { "M-D",                KEY_IS_CHAR,            196     },
+       { "M-E",                KEY_IS_CHAR,            197     },
+       { "M-F",                KEY_IS_CHAR,            198     },
+       { "M-G",                KEY_IS_CHAR,            199     },
+       { "M-H",                KEY_IS_CHAR,            200     },
+       { "M-I",                KEY_IS_CHAR,            201     },
+       { "M-J",                KEY_IS_CHAR,            202     },
+       { "M-K",                KEY_IS_CHAR,            203     },
+       { "M-L",                KEY_IS_CHAR,            204     },
+       { "M-M",                KEY_IS_CHAR,            205     },
+       { "M-N",                KEY_IS_CHAR,            206     },
+       { "M-O",                KEY_IS_CHAR,            207     },
+       { "M-P",                KEY_IS_CHAR,            208     },
+       { "M-Q",                KEY_IS_CHAR,            209     },
+       { "M-R",                KEY_IS_CHAR,            210     },
+       { "M-S",                KEY_IS_CHAR,            211     },
+       { "M-T",                KEY_IS_CHAR,            212     },
+       { "M-U",                KEY_IS_CHAR,            213     },
+       { "M-V",                KEY_IS_CHAR,            214     },
+       { "M-W",                KEY_IS_CHAR,            215     },
+       { "M-X",                KEY_IS_CHAR,            216     },
+       { "M-Y",                KEY_IS_CHAR,            217     },
+       { "M-Z",                KEY_IS_CHAR,            218     },
+       { "M-[",                KEY_IS_CHAR,            219     },
+       { "M-\\",               KEY_IS_CHAR,            220     },
+       { "M-]",                KEY_IS_CHAR,            221     },
+       { "M-^",                KEY_IS_CHAR,            222     },
+       { "M-^?",               KEY_IS_CHAR,            255     },
+       { "M-^@",               KEY_IS_CHAR,            128     },
+       { "M-^A",               KEY_IS_CHAR,            129     },
+       { "M-^B",               KEY_IS_CHAR,            130     },
+       { "M-^C",               KEY_IS_CHAR,            131     },
+       { "M-^D",               KEY_IS_CHAR,            132     },
+       { "M-^E",               KEY_IS_CHAR,            133     },
+       { "M-^F",               KEY_IS_CHAR,            134     },
+       { "M-^G",               KEY_IS_CHAR,            135     },
+       { "M-^H",               KEY_IS_CHAR,            136     },
+       { "M-^I",               KEY_IS_CHAR,            137     },
+       { "M-^J",               KEY_IS_CHAR,            138     },
+       { "M-^K",               KEY_IS_CHAR,            139     },
+       { "M-^L",               KEY_IS_CHAR,            140     },
+       { "M-^M",               KEY_IS_CHAR,            141     },
+       { "M-^N",               KEY_IS_CHAR,            142     },
+       { "M-^O",               KEY_IS_CHAR,            143     },
+       { "M-^P",               KEY_IS_CHAR,            144     },
+       { "M-^Q",               KEY_IS_CHAR,            145     },
+       { "M-^R",               KEY_IS_CHAR,            146     },
+       { "M-^S",               KEY_IS_CHAR,            147     },
+       { "M-^T",               KEY_IS_CHAR,            148     },
+       { "M-^U",               KEY_IS_CHAR,            149     },
+       { "M-^V",               KEY_IS_CHAR,            150     },
+       { "M-^W",               KEY_IS_CHAR,            151     },
+       { "M-^X",               KEY_IS_CHAR,            152     },
+       { "M-^Y",               KEY_IS_CHAR,            153     },
+       { "M-^Z",               KEY_IS_CHAR,            154     },
+       { "M-^[",               KEY_IS_CHAR,            155     },
+       { "M-^\\",              KEY_IS_CHAR,            156     },
+       { "M-^]",               KEY_IS_CHAR,            157     },
+       { "M-^^",               KEY_IS_CHAR,            158     },
+       { "M-^_",               KEY_IS_CHAR,            159     },
+       { "M-_",                KEY_IS_CHAR,            223     },
+       { "M-`",                KEY_IS_CHAR,            224     },
+       { "M-a",                KEY_IS_CHAR,            225     },
+       { "M-b",                KEY_IS_CHAR,            226     },
+       { "M-c",                KEY_IS_CHAR,            227     },
+       { "M-d",                KEY_IS_CHAR,            228     },
+       { "M-e",                KEY_IS_CHAR,            229     },
+       { "M-f",                KEY_IS_CHAR,            230     },
+       { "M-g",                KEY_IS_CHAR,            231     },
+       { "M-h",                KEY_IS_CHAR,            232     },
+       { "M-i",                KEY_IS_CHAR,            233     },
+       { "M-j",                KEY_IS_CHAR,            234     },
+       { "M-k",                KEY_IS_CHAR,            235     },
+       { "M-l",                KEY_IS_CHAR,            236     },
+       { "M-m",                KEY_IS_CHAR,            237     },
+       { "M-n",                KEY_IS_CHAR,            238     },
+       { "M-o",                KEY_IS_CHAR,            239     },
+       { "M-p",                KEY_IS_CHAR,            240     },
+       { "M-q",                KEY_IS_CHAR,            241     },
+       { "M-r",                KEY_IS_CHAR,            242     },
+       { "M-s",                KEY_IS_CHAR,            243     },
+       { "M-space",            KEY_IS_CHAR,            160     },
+       { "M-t",                KEY_IS_CHAR,            244     },
+       { "M-u",                KEY_IS_CHAR,            245     },
+       { "M-v",                KEY_IS_CHAR,            246     },
+       { "M-w",                KEY_IS_CHAR,            247     },
+       { "M-x",                KEY_IS_CHAR,            248     },
+       { "M-y",                KEY_IS_CHAR,            249     },
+       { "M-z",                KEY_IS_CHAR,            250     },
+       { "M-{",                KEY_IS_CHAR,            251     },
+       { "M-|",                KEY_IS_CHAR,            252     },
+       { "M-}",                KEY_IS_CHAR,            253     },
+       { "M-~",                KEY_IS_CHAR,            254     },
+       { "N",                  KEY_IS_CHAR,            78      },
+       { "O",                  KEY_IS_CHAR,            79      },
+       { "P",                  KEY_IS_CHAR,            80      },
+       { "Q",                  KEY_IS_CHAR,            81      },
+       { "R",                  KEY_IS_CHAR,            82      },
+       { "S",                  KEY_IS_CHAR,            83      },
+       { "S-begin",            KEY_SBEG,               0       },
+       { "S-cancel",           KEY_SCANCEL,            0       },
+       { "S-command",          KEY_SCOMMAND,           0       },
+       { "S-copy",             KEY_SCOPY,              0       },
+       { "S-create",           KEY_SCREATE,            0       },
+       { "S-del_line",         KEY_SDL,                0       },
+       { "S-delete",           KEY_SDC,                0       },
+       { "S-eol",              KEY_SEOL,               0       },
+       { "S-exit",             KEY_SEXIT,              0       },
+       { "S-find",             KEY_SFIND,              0       },
+       { "S-help",             KEY_SHELP,              0       },
+       { "S-home",             KEY_SHOME,              0       },
+       { "S-insert",           KEY_SIC,                0       },
+       { "S-left",             KEY_SLEFT,              0       },
+       { "S-message",          KEY_SMESSAGE,           0       },
+       { "S-move",             KEY_SMOVE,              0       },
+       { "S-next",             KEY_SNEXT,              0       },
+       { "S-options",          KEY_SOPTIONS,           0       },
+       { "S-previous",         KEY_SPREVIOUS,          0       },
+       { "S-print",            KEY_SPRINT,             0       },
+       { "S-redo",             KEY_SREDO,              0       },
+       { "S-replace",          KEY_SREPLACE,           0       },
+       { "S-resume",           KEY_SRSUME,             0       },
+       { "S-right",            KEY_SRIGHT,             0       },
+       { "S-save",             KEY_SSAVE,              0       },
+       { "S-suspend",          KEY_SSUSPEND,           0       },
+       { "S-undo",             KEY_SUNDO,              0       },
+       { "T",                  KEY_IS_CHAR,            84      },
+       { "U",                  KEY_IS_CHAR,            85      },
+       { "V",                  KEY_IS_CHAR,            86      },
+       { "W",                  KEY_IS_CHAR,            87      },
+       { "X",                  KEY_IS_CHAR,            88      },
+       { "Y",                  KEY_IS_CHAR,            89      },
+       { "Z",                  KEY_IS_CHAR,            90      },
+       { "[",                  KEY_IS_CHAR,            91      },
+       { "\\",                 KEY_IS_CHAR,            92      },
+       { "]",                  KEY_IS_CHAR,            93      },
+       { "^",                  KEY_IS_CHAR,            94      },
+       { "^A",                 KEY_IS_CHAR,            1       },
+       { "^B",                 KEY_IS_CHAR,            2       },
+       { "^C",                 KEY_IS_CHAR,            3       },
+       { "^D",                 KEY_IS_CHAR,            4       },
+       { "^E",                 KEY_IS_CHAR,            5       },
+       { "^F",                 KEY_IS_CHAR,            6       },
+       { "^G",                 KEY_IS_CHAR,            7       },
+       { "^H",                 KEY_IS_CHAR,            8       },
+       { "^K",                 KEY_IS_CHAR,            11      },
+       { "^L",                 KEY_IS_CHAR,            12      },
+       { "^M",                 KEY_IS_CHAR,            13      },
+       { "^N",                 KEY_IS_CHAR,            14      },
+       { "^O",                 KEY_IS_CHAR,            15      },
+       { "^P",                 KEY_IS_CHAR,            16      },
+       { "^Q",                 KEY_IS_CHAR,            17      },
+       { "^R",                 KEY_IS_CHAR,            18      },
+       { "^S",                 KEY_IS_CHAR,            19      },
+       { "^T",                 KEY_IS_CHAR,            20      },
+       { "^U",                 KEY_IS_CHAR,            21      },
+       { "^V",                 KEY_IS_CHAR,            22      },
+       { "^W",                 KEY_IS_CHAR,            23      },
+       { "^X",                 KEY_IS_CHAR,            24      },
+       { "^Y",                 KEY_IS_CHAR,            25      },
+       { "^Z",                 KEY_IS_CHAR,            26      },
+       { "^\\",                KEY_IS_CHAR,            28      },
+       { "^]",                 KEY_IS_CHAR,            29      },
+       { "^^",                 KEY_IS_CHAR,            30      },
+       { "^_",                 KEY_IS_CHAR,            31      },
+       { "_",                  KEY_IS_CHAR,            95      },
+       { "`",                  KEY_IS_CHAR,            96      },
+       { "a",                  KEY_IS_CHAR,            97      },
+       { "b",                  KEY_IS_CHAR,            98      },
+       { "back_tab",           KEY_BTAB,               0       },
+       { "backspace",          KEY_BACKSPACE,          127     }, /* NOTE: both key and ch */
+       { "begin",              KEY_BEG,                0       },
+       { "c",                  KEY_IS_CHAR,            99      },
+       { "cancel",             KEY_CANCEL,             0       },
+       { "clear",              KEY_CLEAR,              0       },
+       { "clear_all_tabs",     KEY_CATAB,              0       },
+       { "clear_tab",          KEY_CTAB,               0       },
+       { "close",              KEY_CLOSE,              0       },
+       { "command",            KEY_COMMAND,            0       },
+       { "copy",               KEY_COPY,               0       },
+       { "create",             KEY_CREATE,             0       },
+       { "d",                  KEY_IS_CHAR,            100     },
+       { "del_line",           KEY_DL,                 0       },
+       { "delete",             KEY_DC,                 0       },
+       { "down",               KEY_DOWN,               0       },
+       { "e",                  KEY_IS_CHAR,            101     },
+       { "eic",                KEY_EIC,                0       },
+       { "end",                KEY_END,                0       },
+       { "enter",              KEY_IS_CHAR,            10      },
+       { "eol",                KEY_EOL,                0       },
+       { "eos",                KEY_EOS,                0       },
+       { "exit",               KEY_EXIT,               0       },
+       { "f",                  KEY_IS_CHAR,            102     },
+       { "find",               KEY_FIND,               0       },
+       { "g",                  KEY_IS_CHAR,            103     },
+       { "h",                  KEY_IS_CHAR,            104     },
+       { "help",               KEY_HELP,               0       },
+       { "home",               KEY_HOME,               0       },
+       { "i",                  KEY_IS_CHAR,            105     },
+       { "ins_line",           KEY_IL,                 0       },
+       { "insert",             KEY_IC,                 0       },
+       { "j",                  KEY_IS_CHAR,            106     },
+       { "k",                  KEY_IS_CHAR,            107     },
+       { "l",                  KEY_IS_CHAR,            108     },
+       { "left",               KEY_LEFT,               0       },
+       { "lower_left",         KEY_LL,                 0       },
+       { "m",                  KEY_IS_CHAR,            109     },
+       { "mark",               KEY_MARK,               0       },
+       { "message",            KEY_MESSAGE,            0       },
+       { "move",               KEY_MOVE,               0       },
+       { "n",                  KEY_IS_CHAR,            110     },
+       { "next",               KEY_NEXT,               0       },
+       { "o",                  KEY_IS_CHAR,            111     },
+       { "open",               KEY_OPEN,               0       },
+       { "options",            KEY_OPTIONS,            0       },
+       { "p",                  KEY_IS_CHAR,            112     },
+       { "page_down",          KEY_NPAGE,              0       },
+       { "page_up",            KEY_PPAGE,              0       },
+       { "previous",           KEY_PREVIOUS,           0       },
+       { "print",              KEY_PRINT,              0       },
+       { "q",                  KEY_IS_CHAR,            113     },
+       { "r",                  KEY_IS_CHAR,            114     },
+       { "redo",               KEY_REDO,               0       },
+       { "reference",          KEY_REFERENCE,          0       },
+       { "refresh",            KEY_REFRESH,            0       },
+       { "replace",            KEY_REPLACE,            0       },
+       { "restart",            KEY_RESTART,            0       },
+       { "resume",             KEY_RESUME,             0       },
+       { "right",              KEY_RIGHT,              0       },
+       { "s",                  KEY_IS_CHAR,            115     },
+       { "save",               KEY_SAVE,               0       },
+       { "scroll_b",           KEY_SR,                 0       },
+       { "scroll_f",           KEY_SF,                 0       },
+       { "select",             KEY_SELECT,             0       },
+       { "send",               KEY_SEND,               0       },
+       { "set_tab",            KEY_STAB,               0       },
+       { "space",              KEY_IS_CHAR,            32      },
+       { "suspend",            KEY_SUSPEND,            0       },
+       { "t",                  KEY_IS_CHAR,            116     },
+       { "tab",                KEY_IS_CHAR,            9       },
+       { "u",                  KEY_IS_CHAR,            117     },
+       { "undo",               KEY_UNDO,               0       },
+       { "up",                 KEY_UP,                 0       },
+       { "v",                  KEY_IS_CHAR,            118     },
+       { "w",                  KEY_IS_CHAR,            119     },
+       { "x",                  KEY_IS_CHAR,            120     },
+       { "y",                  KEY_IS_CHAR,            121     },
+       { "z",                  KEY_IS_CHAR,            122     },
+       { "{",                  KEY_IS_CHAR,            123     },
+       { "|",                  KEY_IS_CHAR,            124     },
+       { "}",                  KEY_IS_CHAR,            125     },
+       { "~",                  KEY_IS_CHAR,            126     },
+       { "mlb_click",          KEY_MOUSE,              KEY_MLB_CLICK           },
+       { "mlb_click_selected", KEY_MOUSE,              KEY_MLB_CLICK_SEL       },
+       { "mrb_click",          KEY_MOUSE,              KEY_MRB_CLICK           },
+       { "mrb_click_selected", KEY_MOUSE,              KEY_MRB_CLICK_SEL       },
+       { "mouse_scroll_up",    KEY_MOUSE,              KEY_MSCRL_UP            },
+       { "mouse_scroll_down",  KEY_MOUSE,              KEY_MSCRL_DOWN          },
+       { NULL,                 0,                      0       }
+};
+/* }}} */
+
+static int find_context(const char *name)
+{
+       int i;
+
+       for (i = 0; i < NR_CTXS; i++) {
+               if (strcmp(name, key_context_names[i]) == 0)
+                       return i;
+       }
+       error_msg("invalid context '%s'", name);
+       return -1;
+}
+
+static const struct key *find_key(const char *name)
+{
+       int i;
+
+       for (i = 0; key_table[i].name; i++) {
+               if (strcmp(name, key_table[i].name) == 0)
+                       return &key_table[i];
+       }
+       error_msg("invalid key '%s'", name);
+       return NULL;
+}
+
+static struct binding *find_binding(enum key_context c, const struct key *k)
+{
+       struct binding *b = key_bindings[c];
+
+       while (b) {
+               if (b->key == k)
+                       break;
+               b = b->next;
+       }
+       return b;
+}
+
+void show_binding(const char *context, const char *key)
+{
+       const struct key *k;
+       const struct binding *b;
+       int c;
+
+       c = find_context(context);
+       if (c < 0)
+               return;
+
+       k = find_key(key);
+       if (k == NULL)
+               return;
+
+       b = find_binding(c, k);
+       if (b == NULL) {
+               info_msg("No such binding");
+       } else {
+               info_msg("bind %s %s %s", context, key, b->cmd);
+       }
+}
+
+int key_bind(const char *context, const char *key, const char *cmd, int force)
+{
+       const struct key *k;
+       struct binding *b, *ptr, *prev;
+       struct command *command;
+       int c, size;
+
+       c = find_context(context);
+       if (c < 0)
+               return -1;
+
+       k = find_key(key);
+       if (k == NULL)
+               return -1;
+
+       /* check if already bound */
+       b = find_binding(c, k);
+       if (b) {
+               if (!force)
+                       goto bound;
+               key_unbind(context, key, 0);
+       }
+
+       if (*cmd == ':')
+               cmd++;
+       size = strlen(cmd) + 1;
+
+       b = xmalloc(sizeof(struct binding) + size);
+       b->key = k;
+       b->ctx = c;
+       memcpy(b->cmd, cmd, size);
+
+       /* insert keeping sorted by key */
+       prev = NULL;
+       ptr = key_bindings[c];
+       while (ptr) {
+               if (strcmp(b->key->name, ptr->key->name) < 0)
+                       break;
+               prev = ptr;
+               ptr = ptr->next;
+       }
+       b->next = ptr;
+       if (prev) {
+               prev->next = b;
+       } else {
+               key_bindings[c] = b;
+       }
+       command = get_command(cmd);
+       if (command && !command->bc++)
+               help_remove_unbound(command);
+       help_add_bound(b);
+       return 0;
+bound:
+       error_msg("key %s already bound in context %s", key, key_context_names[c]);
+       return -1;
+}
+
+int key_unbind(const char *context, const char *key, int force)
+{
+       int c;
+       const struct key *k;
+       struct binding *b, *prev;
+       struct command *command;
+
+       c = find_context(context);
+       if (c < 0)
+               return -1;
+
+       k = find_key(key);
+       if (k == NULL)
+               return -1;
+
+       prev = NULL;
+       b = key_bindings[c];
+       while (b) {
+               if (b->key == k) {
+                       if (prev) {
+                               prev->next = b->next;
+                       } else {
+                               key_bindings[c] = b->next;
+                       }
+                       command = get_command(b->cmd);
+                       if (command && !--command->bc)
+                               help_add_unbound(command);
+                       help_remove_bound(b);
+                       free(b);
+                       return 0;
+               }
+               prev = b;
+               b = b->next;
+       }
+       if (!force) {
+               error_msg("key %s not bound in context %s", key, context);
+               return -1;
+       }
+       return 0;
+}
+
+static int handle_key(const struct binding *b, const struct key *k)
+{
+       while (b) {
+               if (b->key == k) {
+                       run_command(b->cmd);
+                       return 1;
+               }
+               b = b->next;
+       }
+       return 0;
+}
+
+static const struct key *ch_to_key(uchar ch)
+{
+       int i;
+
+       for (i = 0; key_table[i].name; i++) {
+               if (key_table[i].key != KEY_MOUSE && key_table[i].ch == ch)
+                       return &key_table[i];
+       }
+       return NULL;
+}
+
+static const struct key *keycode_to_key(int key)
+{
+       int i;
+
+       for (i = 0; key_table[i].name; i++) {
+               if (key_table[i].key != KEY_IS_CHAR &&
+                       key_table[i].key != KEY_MOUSE && key_table[i].key == key)
+                       return &key_table[i];
+       }
+       return NULL;
+}
+
+#define DEF_ME_START if (event->bstate == 0) { return NULL; }
+#define DEF_ME_KEY(s, k) else if (event->bstate & s) { key = k + is_sel; }
+#define DEF_ME_END else { return NULL; }
+
+static const struct key *mevent_to_key(MEVENT *event, int is_sel)
+{
+       int i, key = -255;
+
+       DEF_ME_START
+       DEF_ME_KEY(BUTTON1_CLICKED, KEY_MLB_CLICK)
+       DEF_ME_KEY(BUTTON1_PRESSED, KEY_MLB_CLICK)
+       DEF_ME_KEY(BUTTON3_CLICKED, KEY_MRB_CLICK)
+       DEF_ME_KEY(BUTTON3_PRESSED, KEY_MRB_CLICK)
+       DEF_ME_KEY(BUTTON4_PRESSED, KEY_MSCRL_UP)
+       DEF_ME_KEY(BUTTON5_PRESSED, KEY_MSCRL_DOWN)
+       DEF_ME_END
+
+       for (i = 0; key_table[i].name; i++) {
+               if (key_table[i].key == KEY_MOUSE && key_table[i].ch == key)
+                       return &key_table[i];
+       }
+       return NULL;
+}
+
+#undef DEF_ME_START
+#undef DEF_ME_KEY
+#undef DEF_ME_END
+
+void normal_mode_ch(uchar ch)
+{
+       enum key_context c;
+       const struct key *k;
+
+       c = view_to_context[cur_view];
+       k = ch_to_key(ch);
+
+       if (k == NULL) {
+               return;
+       }
+
+       /* view-specific ch */
+       if (handle_key(key_bindings[c], k))
+               return;
+
+       /* common ch */
+       if (handle_key(key_bindings[CTX_COMMON], k))
+               return;
+
+       /* these can be overridden but have default magic */
+       switch (ch) {
+       case ':':
+               enter_command_mode();
+               return;
+       case '/':
+               enter_search_mode();
+               return;
+       case '?':
+               enter_search_backward_mode();
+               return;
+       }
+}
+
+void normal_mode_key(int key)
+{
+       enum key_context c = view_to_context[cur_view];
+       const struct key *k = keycode_to_key(key);
+
+       if (k == NULL) {
+               return;
+       }
+
+       /* view-specific key */
+       if (handle_key(key_bindings[c], k))
+               return;
+
+       /* common key */
+       handle_key(key_bindings[CTX_COMMON], k);
+}
+
+static const struct key *normal_mode_mouse_handle(MEVENT* event)
+{
+       int track_win_x = get_track_win_x(), i = event->y - 1, need_sel, is_sel;
+       struct window* win = NULL;
+       struct iter it, sel;
+
+       if (cur_view == TREE_VIEW) {
+               if (event->x >= track_win_x)
+                       win = lib_track_win;
+               else if (event->x < track_win_x - 1)
+                       win = lib_tree_win;
+               else
+                       return NULL;
+               is_sel = (lib_cur_win == win);
+       } else {
+               win = current_win();
+               is_sel = 1;
+       }
+
+       if ((event->bstate & BUTTON4_PRESSED) || (event->bstate & BUTTON5_PRESSED)) {
+               if (event->y < 1 || event->y >= LINES - 3)
+                       return NULL;
+               is_sel = 0;
+               need_sel = 0;
+               if (cur_view == TREE_VIEW && lib_cur_win != win)
+                       tree_toggle_active_window();
+       } else {
+               if (event->y < 1 || event->y > window_get_nr_rows(win))
+                       return NULL;
+               if (cur_view == TREE_VIEW && lib_cur_win != win)
+                       tree_toggle_active_window();
+               if (!window_get_top(win, &it) || !window_get_sel(win, &sel))
+                       return NULL;
+               while (i-- > 0)
+                       if (!window_get_next(win, &it))
+                               return NULL;
+               while (win->selectable && !win->selectable(&it))
+                       if (!window_get_next(win, &it))
+                               return NULL;
+
+               is_sel = is_sel && iters_equal(&sel, &it);
+               need_sel = !iters_equal(&sel, &it);
+       }
+
+       const struct key *k = mevent_to_key(event, is_sel);
+       if (k == NULL)
+               return NULL;
+
+       if (need_sel)
+               window_set_sel(win, &it);
+
+       return k;
+}
+
+void normal_mode_mouse(MEVENT *event)
+{
+       enum key_context c = view_to_context[cur_view];
+       const struct key *k = normal_mode_mouse_handle(event);
+
+       if (k == NULL)
+               return;
+
+       /* view-specific key */
+       if (handle_key(key_bindings[c], k))
+               return;
+
+       /* common key */
+       handle_key(key_bindings[CTX_COMMON], k);
+}
diff --git a/keys.h b/keys.h
new file mode 100644 (file)
index 0000000..845e7f5
--- /dev/null
+++ b/keys.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2006 Timo Hirvonen
+ *
+ * keys.[ch] by Frank Terbeck <ft@bewatermyfriend.org>
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_KEYS_H
+#define CMUS_KEYS_H
+
+#include "uchar.h"
+
+#if defined(__sun__) || defined(__CYGWIN__)
+/* TIOCGWINSZ */
+#include <termios.h>
+#include <ncurses.h>
+#else
+#include <curses.h>
+#endif
+
+enum key_context {
+       CTX_BROWSER,
+       CTX_COMMON,
+       CTX_FILTERS,
+       CTX_LIBRARY,
+       CTX_PLAYLIST,
+       CTX_QUEUE,
+       CTX_SETTINGS,
+};
+#define NR_CTXS (CTX_SETTINGS + 1)
+
+#if NCURSES_MOUSE_VERSION <= 1
+#define BUTTON5_PRESSED ((REPORT_MOUSE_POSITION) | (BUTTON2_PRESSED))
+#endif
+
+struct key {
+       const char *name;
+       int key;
+       uchar ch;
+};
+
+struct binding {
+       struct binding *next;
+       const struct key *key;
+       enum key_context ctx;
+       char cmd[];
+};
+
+extern const char * const key_context_names[NR_CTXS + 1];
+extern const struct key key_table[];
+extern struct binding *key_bindings[NR_CTXS];
+
+void show_binding(const char *context, const char *key);
+int key_bind(const char *context, const char *key, const char *cmd, int force);
+int key_unbind(const char *context, const char *key, int force);
+
+void normal_mode_ch(uchar ch);
+void normal_mode_key(int key);
+void normal_mode_mouse(MEVENT *event);
+
+#endif
diff --git a/keyval.c b/keyval.c
new file mode 100644 (file)
index 0000000..23b4669
--- /dev/null
+++ b/keyval.c
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2008 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "debug.h"
+#include "keyval.h"
+#include "xmalloc.h"
+
+#include <strings.h>
+
+struct keyval *keyvals_new(int num)
+{
+       struct keyval *c = xnew(struct keyval, num + 1);
+       int i;
+
+       for (i = 0; i <= num; i++) {
+               c[i].key = NULL;
+               c[i].val = NULL;
+       }
+       return c;
+}
+
+struct keyval *keyvals_dup(const struct keyval *keyvals)
+{
+       struct keyval *c;
+       int i;
+
+       for (i = 0; keyvals[i].key; i++)
+               ; /* nothing */
+       c = xnew(struct keyval, i + 1);
+       for (i = 0; keyvals[i].key; i++) {
+               c[i].key = xstrdup(keyvals[i].key);
+               c[i].val = xstrdup(keyvals[i].val);
+       }
+       c[i].key = NULL;
+       c[i].val = NULL;
+       return c;
+}
+
+void keyvals_free(struct keyval *keyvals)
+{
+       int i;
+
+       for (i = 0; keyvals[i].key; i++) {
+               free(keyvals[i].key);
+               free(keyvals[i].val);
+       }
+       free(keyvals);
+}
+
+const char *keyvals_get_val(const struct keyval *keyvals, const char *key)
+{
+       int i;
+
+       for (i = 0; keyvals[i].key; i++) {
+               if (strcasecmp(keyvals[i].key, key) == 0)
+                       return keyvals[i].val;
+       }
+       return NULL;
+}
+
+void keyvals_init(struct growing_keyvals *c, const struct keyval *keyvals)
+{
+       int i;
+
+       BUG_ON(c->keyvals);
+
+       for (i = 0; keyvals[i].key; i++)
+               ; /* nothing */
+
+       c->keyvals = keyvals_dup(keyvals);
+       c->alloc = i;
+       c->count = i;
+}
+
+void keyvals_add(struct growing_keyvals *c, const char *key, char *val)
+{
+       int n = c->count + 1;
+
+       if (n > c->alloc) {
+               n = (n + 3) & ~3;
+               c->keyvals = xrenew(struct keyval, c->keyvals, n);
+               c->alloc = n;
+       }
+
+       c->keyvals[c->count].key = xstrdup(key);
+       c->keyvals[c->count].val = val;
+       c->count++;
+}
+
+const char *keyvals_get_val_growing(const struct growing_keyvals *c, const char *key)
+{
+       int i;
+
+       for (i = 0; i < c->count; ++i)
+               if (strcasecmp(c->keyvals[i].key, key) == 0)
+                       return c->keyvals[i].val;
+
+       return NULL;
+}
+
+void keyvals_terminate(struct growing_keyvals *c)
+{
+       int alloc = c->count + 1;
+
+       if (alloc > c->alloc) {
+               c->keyvals = xrenew(struct keyval, c->keyvals, alloc);
+               c->alloc = alloc;
+       }
+       c->keyvals[c->count].key = NULL;
+       c->keyvals[c->count].val = NULL;
+}
diff --git a/keyval.h b/keyval.h
new file mode 100644 (file)
index 0000000..9ac8de4
--- /dev/null
+++ b/keyval.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2008 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_KEYVAL_H
+#define CMUS_KEYVAL_H
+
+struct keyval {
+       char *key;
+       char *val;
+};
+
+struct growing_keyvals {
+       struct keyval *keyvals;
+       int alloc;
+       int count;
+};
+
+#define GROWING_KEYVALS(name) struct growing_keyvals name = { NULL, 0, 0 }
+
+struct keyval *keyvals_new(int num);
+void keyvals_init(struct growing_keyvals *c, const struct keyval *keyvals);
+void keyvals_add(struct growing_keyvals *c, const char *key, char *val);
+const char *keyvals_get_val_growing(const struct growing_keyvals *c, const char *key);
+void keyvals_terminate(struct growing_keyvals *c);
+void keyvals_free(struct keyval *keyvals);
+struct keyval *keyvals_dup(const struct keyval *keyvals);
+const char *keyvals_get_val(const struct keyval *keyvals, const char *key);
+
+#endif
diff --git a/lib.c b/lib.c
new file mode 100644 (file)
index 0000000..a0b98b9
--- /dev/null
+++ b/lib.c
@@ -0,0 +1,714 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "lib.h"
+#include "editable.h"
+#include "track_info.h"
+#include "options.h"
+#include "xmalloc.h"
+#include "rbtree.h"
+#include "debug.h"
+#include "utils.h"
+#include "ui_curses.h" /* cur_view */
+
+#include <pthread.h>
+#include <string.h>
+
+struct editable lib_editable;
+static struct editable_shared lib_editable_shared;
+
+struct tree_track *lib_cur_track = NULL;
+unsigned int play_sorted = 0;
+enum aaa_mode aaa_mode = AAA_MODE_ALL;
+/* used in ui_curses.c for status display */
+char *lib_live_filter = NULL;
+
+struct rb_root lib_shuffle_root;
+static struct expr *filter = NULL;
+static struct expr *add_filter = NULL;
+static int remove_from_hash = 1;
+
+static struct expr *live_filter_expr = NULL;
+static struct track_info *cur_track_ti = NULL;
+static struct track_info *sel_track_ti = NULL;
+
+const char *artist_sort_name(const struct artist *a)
+{
+       if (a->sort_name)
+               return a->sort_name;
+
+       if (smart_artist_sort && a->auto_sort_name)
+               return a->auto_sort_name;
+
+       return a->name;
+}
+
+static inline void sorted_track_to_iter(struct tree_track *track, struct iter *iter)
+{
+       iter->data0 = &lib_editable.head;
+       iter->data1 = track;
+       iter->data2 = NULL;
+}
+
+static void all_wins_changed(void)
+{
+       lib_tree_win->changed = 1;
+       lib_track_win->changed = 1;
+       lib_editable.shared->win->changed = 1;
+}
+
+static void shuffle_add(struct tree_track *track)
+{
+       shuffle_list_add(&track->shuffle_track, &lib_shuffle_root);
+}
+
+static void views_add_track(struct track_info *ti)
+{
+       struct tree_track *track = xnew(struct tree_track, 1);
+
+       /* NOTE: does not ref ti */
+       simple_track_init((struct simple_track *)track, ti);
+
+       /* both the hash table and views have refs */
+       track_info_ref(ti);
+
+       tree_add_track(track);
+       shuffle_add(track);
+       editable_add(&lib_editable, (struct simple_track *)track);
+}
+
+struct fh_entry {
+       struct fh_entry *next;
+
+       /* ref count is increased when added to this hash */
+       struct track_info *ti;
+};
+
+#define FH_SIZE (1024)
+static struct fh_entry *ti_hash[FH_SIZE] = { NULL, };
+
+static int hash_insert(struct track_info *ti)
+{
+       const char *filename = ti->filename;
+       unsigned int pos = hash_str(filename) % FH_SIZE;
+       struct fh_entry **entryp;
+       struct fh_entry *e;
+
+       entryp = &ti_hash[pos];
+       e = *entryp;
+       while (e) {
+               if (strcmp(e->ti->filename, filename) == 0) {
+                       /* found, don't insert */
+                       return 0;
+               }
+               e = e->next;
+       }
+
+       e = xnew(struct fh_entry, 1);
+       track_info_ref(ti);
+       e->ti = ti;
+       e->next = *entryp;
+       *entryp = e;
+       return 1;
+}
+
+static void hash_remove(struct track_info *ti)
+{
+       const char *filename = ti->filename;
+       unsigned int pos = hash_str(filename) % FH_SIZE;
+       struct fh_entry **entryp;
+
+       entryp = &ti_hash[pos];
+       while (1) {
+               struct fh_entry *e = *entryp;
+
+               BUG_ON(e == NULL);
+               if (strcmp(e->ti->filename, filename) == 0) {
+                       *entryp = e->next;
+                       track_info_unref(e->ti);
+                       free(e);
+                       break;
+               }
+               entryp = &e->next;
+       }
+}
+
+static int is_filtered(struct track_info *ti)
+{
+       if (live_filter_expr && !expr_eval(live_filter_expr, ti))
+               return 1;
+       if (!live_filter_expr && lib_live_filter && !track_info_matches(ti, lib_live_filter, TI_MATCH_ALL))
+               return 1;
+       if (filter && !expr_eval(filter, ti))
+               return 1;
+       return 0;
+}
+
+void lib_add_track(struct track_info *ti, void *opaque)
+{
+       if (add_filter && !expr_eval(add_filter, ti)) {
+               /* filter any files exluded by lib_add_filter */
+               return;
+       }
+
+       if (!hash_insert(ti)) {
+               /* duplicate files not allowed */
+               return;
+       }
+
+       if (!is_filtered(ti))
+               views_add_track(ti);
+}
+
+static struct tree_track *album_first_track(const struct album *album)
+{
+       return to_tree_track(rb_first(&album->track_root));
+}
+
+static struct tree_track *artist_first_track(const struct artist *artist)
+{
+       return album_first_track(to_album(rb_first(&artist->album_root)));
+}
+
+static struct tree_track *normal_get_first(void)
+{
+       return artist_first_track(to_artist(rb_first(&lib_artist_root)));
+}
+
+static struct tree_track *album_last_track(const struct album *album)
+{
+       return to_tree_track(rb_last(&album->track_root));
+}
+
+static struct tree_track *artist_last_track(const struct artist *artist)
+{
+       return album_last_track(to_album(rb_last(&artist->album_root)));
+}
+
+static int aaa_mode_filter(const struct simple_track *track)
+{
+       const struct album *album = ((struct tree_track *)track)->album;
+
+       if (aaa_mode == AAA_MODE_ALBUM)
+               return CUR_ALBUM == album;
+
+       if (aaa_mode == AAA_MODE_ARTIST)
+               return CUR_ARTIST == album->artist;
+
+       /* AAA_MODE_ALL */
+       return 1;
+}
+
+/* set next/prev (tree) {{{ */
+
+static struct tree_track *normal_get_next(void)
+{
+       if (lib_cur_track == NULL)
+               return normal_get_first();
+
+       /* not last track of the album? */
+       if (rb_next(&lib_cur_track->tree_node)) {
+               /* next track of the current album */
+               return to_tree_track(rb_next(&lib_cur_track->tree_node));
+       }
+
+       if (aaa_mode == AAA_MODE_ALBUM) {
+               if (!repeat)
+                       return NULL;
+               /* first track of the current album */
+               return album_first_track(CUR_ALBUM);
+       }
+
+       /* not last album of the artist? */
+       if (rb_next(&CUR_ALBUM->tree_node) != NULL) {
+               /* first track of the next album */
+               return album_first_track(to_album(rb_next(&CUR_ALBUM->tree_node)));
+       }
+
+       if (aaa_mode == AAA_MODE_ARTIST) {
+               if (!repeat)
+                       return NULL;
+               /* first track of the first album of the current artist */
+               return artist_first_track(CUR_ARTIST);
+       }
+
+       /* not last artist of the library? */
+       if (rb_next(&CUR_ARTIST->tree_node) != NULL) {
+               /* first track of the next artist */
+               return artist_first_track(to_artist(rb_next(&CUR_ARTIST->tree_node)));
+       }
+
+       if (!repeat)
+               return NULL;
+
+       /* first track */
+       return normal_get_first();
+}
+
+static struct tree_track *normal_get_prev(void)
+{
+       if (lib_cur_track == NULL)
+               return normal_get_first();
+
+       /* not first track of the album? */
+       if (rb_prev(&lib_cur_track->tree_node)) {
+               /* prev track of the album */
+               return to_tree_track(rb_prev(&lib_cur_track->tree_node));
+       }
+
+       if (aaa_mode == AAA_MODE_ALBUM) {
+               if (!repeat)
+                       return NULL;
+               /* last track of the album */
+               return album_last_track(CUR_ALBUM);
+       }
+
+       /* not first album of the artist? */
+       if (rb_prev(&CUR_ALBUM->tree_node) != NULL) {
+               /* last track of the prev album of the artist */
+               return album_last_track(to_album(rb_prev(&CUR_ALBUM->tree_node)));
+       }
+
+       if (aaa_mode == AAA_MODE_ARTIST) {
+               if (!repeat)
+                       return NULL;
+               /* last track of the last album of the artist */
+               return album_last_track(to_album(rb_last(&CUR_ARTIST->album_root)));
+       }
+
+       /* not first artist of the library? */
+       if (rb_prev(&CUR_ARTIST->tree_node) != NULL) {
+               /* last track of the last album of the prev artist */
+               return artist_last_track(to_artist(rb_prev(&CUR_ARTIST->tree_node)));
+       }
+
+       if (!repeat)
+               return NULL;
+
+       /* last track */
+       return artist_last_track(to_artist(rb_last(&lib_artist_root)));
+}
+
+/* set next/prev (tree) }}} */
+
+void lib_reshuffle(void)
+{
+       shuffle_list_reshuffle(&lib_shuffle_root);
+}
+
+static void free_lib_track(struct editable *e, struct list_head *item)
+{
+       struct tree_track *track = (struct tree_track *)to_simple_track(item);
+       struct track_info *ti = tree_track_info(track);
+
+       if (track == lib_cur_track)
+               lib_cur_track = NULL;
+
+       if (remove_from_hash)
+               hash_remove(ti);
+
+       rb_erase(&track->shuffle_track.tree_node, &lib_shuffle_root);
+       tree_remove(track);
+
+       track_info_unref(ti);
+       free(track);
+}
+
+void lib_init(void)
+{
+       editable_shared_init(&lib_editable_shared, free_lib_track);
+       editable_init(&lib_editable, &lib_editable_shared, 1);
+       tree_init();
+       srand(time(NULL));
+}
+
+struct track_info *lib_set_track(struct tree_track *track)
+{
+       struct track_info *ti = NULL;
+
+       if (track) {
+               lib_cur_track = track;
+               ti = tree_track_info(track);
+               track_info_ref(ti);
+               if (follow) {
+                       tree_sel_current(auto_expand_albums_follow);
+                       sorted_sel_current();
+               }
+               all_wins_changed();
+       }
+       return ti;
+}
+
+struct track_info *lib_goto_next(void)
+{
+       struct tree_track *track;
+
+       if (rb_root_empty(&lib_artist_root)) {
+               BUG_ON(lib_cur_track != NULL);
+               return NULL;
+       }
+       if (shuffle) {
+               track = (struct tree_track *)shuffle_list_get_next(&lib_shuffle_root,
+                               (struct shuffle_track *)lib_cur_track, aaa_mode_filter);
+       } else if (play_sorted) {
+               track = (struct tree_track *)simple_list_get_next(&lib_editable.head,
+                               (struct simple_track *)lib_cur_track, aaa_mode_filter);
+       } else {
+               track = normal_get_next();
+       }
+       return lib_set_track(track);
+}
+
+struct track_info *lib_goto_prev(void)
+{
+       struct tree_track *track;
+
+       if (rb_root_empty(&lib_artist_root)) {
+               BUG_ON(lib_cur_track != NULL);
+               return NULL;
+       }
+       if (shuffle) {
+               track = (struct tree_track *)shuffle_list_get_prev(&lib_shuffle_root,
+                               (struct shuffle_track *)lib_cur_track, aaa_mode_filter);
+       } else if (play_sorted) {
+               track = (struct tree_track *)simple_list_get_prev(&lib_editable.head,
+                               (struct simple_track *)lib_cur_track, aaa_mode_filter);
+       } else {
+               track = normal_get_prev();
+       }
+       return lib_set_track(track);
+}
+
+static struct tree_track *sorted_get_selected(void)
+{
+       struct iter sel;
+
+       if (list_empty(&lib_editable.head))
+               return NULL;
+
+       window_get_sel(lib_editable.shared->win, &sel);
+       return iter_to_sorted_track(&sel);
+}
+
+struct track_info *sorted_activate_selected(void)
+{
+       return lib_set_track(sorted_get_selected());
+}
+
+static void hash_add_to_views(void)
+{
+       int i;
+       for (i = 0; i < FH_SIZE; i++) {
+               struct fh_entry *e;
+
+               e = ti_hash[i];
+               while (e) {
+                       struct track_info *ti = e->ti;
+
+                       if (!is_filtered(ti))
+                               views_add_track(ti);
+                       e = e->next;
+               }
+       }
+}
+
+struct tree_track *lib_find_track(struct track_info *ti)
+{
+       struct simple_track *track;
+
+       list_for_each_entry(track, &lib_editable.head, node) {
+               if (strcmp(track->info->filename, ti->filename) == 0) {
+                       struct tree_track *tt = (struct tree_track *)track;
+                       return tt;
+               }
+       }
+       return NULL;
+}
+
+void lib_store_cur_track(struct track_info *ti)
+{
+       if (cur_track_ti)
+               track_info_unref(cur_track_ti);
+       cur_track_ti = ti;
+       track_info_ref(cur_track_ti);
+}
+
+struct track_info *lib_get_cur_stored_track(void)
+{
+       if (cur_track_ti && lib_find_track(cur_track_ti))
+               return cur_track_ti;
+       return NULL;
+}
+
+static void restore_cur_track(struct track_info *ti)
+{
+       struct tree_track *tt = lib_find_track(ti);
+       if (tt)
+               lib_cur_track = tt;
+}
+
+static int is_filtered_cb(void *data, struct track_info *ti)
+{
+       return is_filtered(ti);
+}
+
+static void do_lib_filter(int clear_before)
+{
+       /* try to save cur_track */
+       if (lib_cur_track)
+               lib_store_cur_track(tree_track_info(lib_cur_track));
+
+       if (clear_before)
+               d_print("filter results could grow, clear tracks and re-add (slow)\n");
+
+       remove_from_hash = 0;
+       if (clear_before) {
+               editable_clear(&lib_editable);
+               hash_add_to_views();
+       } else
+               editable_remove_matching_tracks(&lib_editable, is_filtered_cb, NULL);
+       remove_from_hash = 1;
+
+       window_changed(lib_editable.shared->win);
+       window_goto_top(lib_editable.shared->win);
+       lib_cur_win = lib_tree_win;
+       window_goto_top(lib_tree_win);
+
+       /* restore cur_track */
+       if (cur_track_ti && !lib_cur_track)
+               restore_cur_track(cur_track_ti);
+}
+
+static void unset_live_filter(void)
+{
+       free(lib_live_filter);
+       lib_live_filter = NULL;
+       free(live_filter_expr);
+       live_filter_expr = NULL;
+}
+
+void lib_set_filter(struct expr *expr)
+{
+       int clear_before = lib_live_filter || filter;
+       unset_live_filter();
+       if (filter)
+               expr_free(filter);
+       filter = expr;
+       do_lib_filter(clear_before);
+}
+
+void lib_set_add_filter(struct expr *expr)
+{
+       if (add_filter)
+               expr_free(add_filter);
+       add_filter = expr;
+}
+
+static struct tree_track *get_sel_track(void)
+{
+       switch (cur_view) {
+       case TREE_VIEW:
+               return tree_get_selected();
+       case SORTED_VIEW:
+               return sorted_get_selected();
+       }
+       return NULL;
+}
+
+static void set_sel_track(struct tree_track *tt)
+{
+       struct iter iter;
+
+       switch (cur_view) {
+       case TREE_VIEW:
+               tree_sel_track(tt, auto_expand_albums_selcur);
+               break;
+       case SORTED_VIEW:
+               sorted_track_to_iter(tt, &iter);
+               window_set_sel(lib_editable.shared->win, &iter);
+               break;
+       }
+}
+
+static void store_sel_track(void)
+{
+       struct tree_track *tt = get_sel_track();
+       if (tt) {
+               sel_track_ti = tree_track_info(tt);
+               track_info_ref(sel_track_ti);
+       }
+}
+
+static void restore_sel_track(void)
+{
+       if (sel_track_ti) {
+               struct tree_track *tt = lib_find_track(sel_track_ti);
+               if (tt) {
+                       set_sel_track(tt);
+                       track_info_unref(sel_track_ti);
+                       sel_track_ti = NULL;
+               }
+       }
+}
+
+/* determine if filter results could grow, in which case all tracks must be cleared and re-added */
+static int do_clear_before(const char *str, struct expr *expr)
+{
+       if (!lib_live_filter)
+               return 0;
+       if (!str)
+               return 1;
+       if ((!expr && live_filter_expr) || (expr && !live_filter_expr))
+               return 1;
+       if (!expr || expr_is_harmless(expr))
+               return !strstr(str, lib_live_filter);
+       return 1;
+}
+
+void lib_set_live_filter(const char *str)
+{
+       int clear_before;
+       struct expr *expr = NULL;
+
+       if (strcmp0(str, lib_live_filter) == 0)
+               return;
+
+       if (str && expr_is_short(str)) {
+               expr = expr_parse(str);
+               if (!expr)
+                       return;
+       }
+
+       clear_before = do_clear_before(str, expr);
+
+       if (!str)
+               store_sel_track();
+
+       unset_live_filter();
+       lib_live_filter = str ? xstrdup(str) : NULL;
+       live_filter_expr = expr;
+       do_lib_filter(clear_before);
+
+       if (expr) {
+               unsigned int match_type = expr_get_match_type(expr);
+               if (match_type & TI_MATCH_ALBUM)
+                       tree_expand_all();
+               if (match_type & TI_MATCH_TITLE)
+                       tree_sel_first();
+       } else if (str)
+               tree_expand_matching(str);
+
+       if (!str)
+               restore_sel_track();
+}
+
+int lib_remove(struct track_info *ti)
+{
+       struct simple_track *track;
+
+       list_for_each_entry(track, &lib_editable.head, node) {
+               if (track->info == ti) {
+                       editable_remove_track(&lib_editable, track);
+                       return 1;
+               }
+       }
+       return 0;
+}
+
+void lib_clear_store(void)
+{
+       int i;
+
+       for (i = 0; i < FH_SIZE; i++) {
+               struct fh_entry *e, *next;
+
+               e = ti_hash[i];
+               while (e) {
+                       next = e->next;
+                       track_info_unref(e->ti);
+                       free(e);
+                       e = next;
+               }
+               ti_hash[i] = NULL;
+       }
+}
+
+void sorted_sel_current(void)
+{
+       if (lib_cur_track) {
+               struct iter iter;
+
+               sorted_track_to_iter(lib_cur_track, &iter);
+               window_set_sel(lib_editable.shared->win, &iter);
+       }
+}
+
+static int ti_cmp(const void *a, const void *b)
+{
+       const struct track_info *ai = *(const struct track_info **)a;
+       const struct track_info *bi = *(const struct track_info **)b;
+
+       return track_info_cmp(ai, bi, lib_editable.shared->sort_keys);
+}
+
+static int do_lib_for_each(int (*cb)(void *data, struct track_info *ti), void *data, int filtered)
+{
+       int i, rc = 0, count = 0, size = 1024;
+       struct track_info **tis;
+
+       tis = xnew(struct track_info *, size);
+
+       /* collect all track_infos */
+       for (i = 0; i < FH_SIZE; i++) {
+               struct fh_entry *e;
+
+               e = ti_hash[i];
+               while (e) {
+                       if (count == size) {
+                               size *= 2;
+                               tis = xrenew(struct track_info *, tis, size);
+                       }
+                       if (!filtered || !filter || expr_eval(filter, e->ti))
+                               tis[count++] = e->ti;
+                       e = e->next;
+               }
+       }
+
+       /* sort to speed up playlist loading */
+       qsort(tis, count, sizeof(struct track_info *), ti_cmp);
+       for (i = 0; i < count; i++) {
+               rc = cb(data, tis[i]);
+               if (rc)
+                       break;
+       }
+
+       free(tis);
+       return rc;
+}
+
+int lib_for_each(int (*cb)(void *data, struct track_info *ti), void *data,
+               void *opaque)
+{
+       return do_lib_for_each(cb, data, 0);
+}
+
+int lib_for_each_filtered(int (*cb)(void *data, struct track_info *ti),
+               void *data, void *opaque)
+{
+       return do_lib_for_each(cb, data, 1);
+}
diff --git a/lib.h b/lib.h
new file mode 100644 (file)
index 0000000..ddaad66
--- /dev/null
+++ b/lib.h
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_LIB_H
+#define CMUS_LIB_H
+
+#include "editable.h"
+#include "search.h"
+#include "track_info.h"
+#include "expr.h"
+#include "rbtree.h"
+
+struct tree_track {
+       struct shuffle_track shuffle_track;
+
+       /* position in track search tree */
+       struct rb_node tree_node;
+
+       struct album *album;
+};
+
+static inline struct track_info *tree_track_info(const struct tree_track *track)
+{
+       return ((struct simple_track *)track)->info;
+}
+
+static inline struct tree_track *to_tree_track(const struct rb_node *node)
+{
+       return container_of(node, struct tree_track, tree_node);
+}
+
+
+struct album {
+       /* position in album search tree */
+       struct rb_node tree_node;
+
+       /* root of track tree */
+       struct rb_root track_root;
+
+       struct artist *artist;
+       char *name;
+       char *sort_name;
+       char *collkey_name;
+       char *collkey_sort_name;
+       /* max date of the tracks added to this album */
+       int date;
+       /* min date of the tracks added to this album */
+       int min_date;
+};
+
+struct artist {
+       /* position in artist search tree */
+       struct rb_node tree_node;
+
+       /* root of album tree */
+       struct rb_root album_root;
+
+       char *name;
+       char *sort_name;
+       char *auto_sort_name;
+       char *collkey_name;
+       char *collkey_sort_name;
+       char *collkey_auto_sort_name;
+
+       /* albums visible for this artist in the tree_win? */
+       unsigned int expanded : 1;
+       unsigned int is_compilation : 1;
+};
+
+const char *artist_sort_name(const struct artist *);
+
+enum aaa_mode {
+       AAA_MODE_ALL,
+       AAA_MODE_ARTIST,
+       AAA_MODE_ALBUM
+};
+
+extern struct editable lib_editable;
+extern struct tree_track *lib_cur_track;
+extern struct rb_root lib_shuffle_root;
+extern enum aaa_mode aaa_mode;
+extern unsigned int play_sorted;
+extern char *lib_live_filter;
+
+extern struct searchable *tree_searchable;
+extern struct window *lib_tree_win;
+extern struct window *lib_track_win;
+extern struct window *lib_cur_win;
+extern struct rb_root lib_artist_root;
+
+#define CUR_ALBUM      (lib_cur_track->album)
+#define CUR_ARTIST     (lib_cur_track->album->artist)
+
+void lib_init(void);
+void tree_init(void);
+struct track_info *lib_goto_next(void);
+struct track_info *lib_goto_prev(void);
+void lib_add_track(struct track_info *track_info, void *opaque);
+void lib_set_filter(struct expr *expr);
+void lib_set_live_filter(const char *str);
+void lib_set_add_filter(struct expr *expr);
+int lib_remove(struct track_info *ti);
+void lib_clear_store(void);
+void lib_reshuffle(void);
+void lib_set_view(int view);
+int lib_for_each(int (*cb)(void *data, struct track_info *ti), void *data,
+               void *opaque);
+int lib_for_each_filtered(int (*cb)(void *data, struct track_info *ti),
+               void *data, void *opaque);
+
+struct tree_track *lib_find_track(struct track_info *ti);
+struct track_info *lib_set_track(struct tree_track *track);
+void lib_store_cur_track(struct track_info *ti);
+struct track_info *lib_get_cur_stored_track(void);
+
+struct tree_track *tree_get_selected(void);
+struct track_info *tree_activate_selected(void);
+void tree_sort_artists(void);
+void tree_add_track(struct tree_track *track);
+void tree_remove(struct tree_track *track);
+void tree_remove_sel(void);
+void tree_toggle_active_window(void);
+void tree_toggle_expand_artist(void);
+void tree_expand_matching(const char *text);
+void tree_expand_all(void);
+void tree_sel_update(int changed);
+void tree_sel_current(int auto_expand_albums);
+void tree_sel_first(void);
+void tree_sel_track(struct tree_track *t, int auto_expand_albums );
+int tree_for_each_sel(int (*cb)(void *data, struct track_info *ti), void *data, int reverse);
+int _tree_for_each_sel(int (*cb)(void *data, struct track_info *ti), void *data, int reverse);
+
+struct track_info *sorted_activate_selected(void);
+void sorted_sel_current(void);
+
+static inline struct tree_track *iter_to_sorted_track(const struct iter *iter)
+{
+       return iter->data1;
+}
+
+static inline struct artist *iter_to_artist(const struct iter *iter)
+{
+       return iter->data1;
+}
+
+static inline struct album *iter_to_album(const struct iter *iter)
+{
+       return iter->data2;
+}
+
+static inline struct tree_track *iter_to_tree_track(const struct iter *iter)
+{
+       return iter->data1;
+}
+
+static inline struct artist *to_artist(const struct rb_node *node)
+{
+       return container_of(node, struct artist, tree_node);
+}
+
+static inline struct album *to_album(const struct rb_node *node)
+{
+       return container_of(node, struct album, tree_node);
+}
+
+#endif
diff --git a/list.h b/list.h
new file mode 100644 (file)
index 0000000..60a9332
--- /dev/null
+++ b/list.h
@@ -0,0 +1,348 @@
+/*
+ * Stolen from Linux 2.6.7
+ *
+ * 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; version 2 of the
+ * License.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_LIST_H
+#define CMUS_LIST_H
+
+#include "compiler.h" /* container_of */
+
+static inline void prefetch(const void *x)
+{
+}
+
+/*
+ * These are non-NULL pointers that will result in page faults
+ * under normal circumstances, used to verify that nobody uses
+ * non-initialized list entries.
+ */
+#define LIST_POISON1  ((void *) 0x00100100)
+#define LIST_POISON2  ((void *) 0x00200200)
+
+/*
+ * Simple doubly linked list implementation.
+ *
+ * Some of the internal functions ("_xxx") are useful when
+ * manipulating whole lists rather than single entries, as
+ * sometimes we already know the next/prev entries and we can
+ * generate better code by using them directly rather than
+ * using the generic single-entry routines.
+ */
+
+struct list_head {
+       struct list_head *next, *prev;
+};
+
+#define LIST_HEAD_INIT(name) { &(name), &(name) }
+
+#define LIST_HEAD(name) \
+       struct list_head name = LIST_HEAD_INIT(name)
+
+static inline void list_init(struct list_head *head)
+{
+       head->next = head;
+       head->prev = head;
+}
+
+/*
+ * Insert a new entry between two known consecutive entries.
+ *
+ * This is only for internal list manipulation where we know
+ * the prev/next entries already!
+ */
+static inline void _list_add(struct list_head *new,
+                             struct list_head *prev,
+                             struct list_head *next)
+{
+       next->prev = new;
+       new->next = next;
+       new->prev = prev;
+       prev->next = new;
+}
+
+static inline struct list_head *list_prev(struct list_head *list)
+{
+       return list->prev;
+}
+
+static inline struct list_head *list_next(struct list_head *list)
+{
+       return list->next;
+}
+
+/**
+ * list_add - add a new entry
+ * @new: new entry to be added
+ * @head: list head to add it after
+ *
+ * Insert a new entry after the specified head.
+ * This is good for implementing stacks.
+ */
+static inline void list_add(struct list_head *new, struct list_head *head)
+{
+       _list_add(new, head, head->next);
+}
+
+/**
+ * list_add_tail - add a new entry
+ * @new: new entry to be added
+ * @head: list head to add it before
+ *
+ * Insert a new entry before the specified head.
+ * This is useful for implementing queues.
+ */
+static inline void list_add_tail(struct list_head *new, struct list_head *head)
+{
+       _list_add(new, head->prev, head);
+}
+
+/*
+ * Delete a list entry by making the prev/next entries
+ * point to each other.
+ *
+ * This is only for internal list manipulation where we know
+ * the prev/next entries already!
+ */
+static inline void _list_del(struct list_head *prev, struct list_head *next)
+{
+       next->prev = prev;
+       prev->next = next;
+}
+
+/**
+ * list_del - deletes entry from list.
+ * @entry: the element to delete from the list.
+ * Note: list_empty on entry does not return true after this, the entry is
+ * in an undefined state.
+ */
+static inline void list_del(struct list_head *entry)
+{
+       _list_del(entry->prev, entry->next);
+       entry->next = LIST_POISON1;
+       entry->prev = LIST_POISON2;
+}
+
+/**
+ * list_del_init - deletes entry from list and reinitialize it.
+ * @entry: the element to delete from the list.
+ */
+static inline void list_del_init(struct list_head *entry)
+{
+       _list_del(entry->prev, entry->next);
+       list_init(entry);
+}
+
+/**
+ * list_move - delete from one list and add as another's head
+ * @list: the entry to move
+ * @head: the head that will precede our entry
+ */
+static inline void list_move(struct list_head *list, struct list_head *head)
+{
+       _list_del(list->prev, list->next);
+       list_add(list, head);
+}
+
+/**
+ * list_move_tail - delete from one list and add as another's tail
+ * @list: the entry to move
+ * @head: the head that will follow our entry
+ */
+static inline void list_move_tail(struct list_head *list,
+                                 struct list_head *head)
+{
+       _list_del(list->prev, list->next);
+       list_add_tail(list, head);
+}
+
+/**
+ * list_empty - tests whether a list is empty
+ * @head: the list to test.
+ */
+static inline int list_empty(const struct list_head *head)
+{
+       return head->next == head;
+}
+
+static inline void _list_splice(struct list_head *list,
+                                struct list_head *head)
+{
+       struct list_head *first = list->next;
+       struct list_head *last = list->prev;
+       struct list_head *at = head->next;
+
+       first->prev = head;
+       head->next = first;
+
+       last->next = at;
+       at->prev = last;
+}
+
+/**
+ * list_splice - join two lists
+ * @list: the new list to add.
+ * @head: the place to add it in the first list.
+ */
+static inline void list_splice(struct list_head *list, struct list_head *head)
+{
+       if (!list_empty(list))
+               _list_splice(list, head);
+}
+
+/**
+ * list_splice_init - join two lists and reinitialise the emptied list.
+ * @list: the new list to add.
+ * @head: the place to add it in the first list.
+ *
+ * The list at @list is reinitialised
+ */
+static inline void list_splice_init(struct list_head *list,
+                                   struct list_head *head)
+{
+       if (!list_empty(list)) {
+               _list_splice(list, head);
+               list_init(list);
+       }
+}
+
+/**
+ * list_entry - get the struct for this entry
+ * @ptr:       the &struct list_head pointer.
+ * @type:      the type of the struct this is embedded in.
+ * @member:    the name of the list_struct within the struct.
+ */
+#define list_entry(ptr, type, member) \
+       container_of(ptr, type, member)
+
+/**
+ * list_for_each       -       iterate over a list
+ * @pos:       the &struct list_head to use as a loop counter.
+ * @head:      the head for your list.
+ */
+#define list_for_each(pos, head) \
+       for (pos = (head)->next, prefetch(pos->next); pos != (head); \
+               pos = pos->next, prefetch(pos->next))
+
+/**
+ * _list_for_each      -       iterate over a list
+ * @pos:       the &struct list_head to use as a loop counter.
+ * @head:      the head for your list.
+ *
+ * This variant differs from list_for_each() in that it's the
+ * simplest possible list iteration code, no prefetching is done.
+ * Use this for code that knows the list to be very short (empty
+ * or 1 entry) most of the time.
+ */
+#define _list_for_each(pos, head) \
+       for (pos = (head)->next; pos != (head); pos = pos->next)
+
+/**
+ * list_for_each_prev  -       iterate over a list backwards
+ * @pos:       the &struct list_head to use as a loop counter.
+ * @head:      the head for your list.
+ */
+#define list_for_each_prev(pos, head) \
+       for (pos = (head)->prev, prefetch(pos->prev); pos != (head); \
+               pos = pos->prev, prefetch(pos->prev))
+
+/**
+ * list_for_each_safe  -       iterate over a list safe against removal of list entry
+ * @pos:       the &struct list_head to use as a loop counter.
+ * @n:         another &struct list_head to use as temporary storage
+ * @head:      the head for your list.
+ */
+#define list_for_each_safe(pos, n, head) \
+       for (pos = (head)->next, n = pos->next; pos != (head); \
+               pos = n, n = pos->next)
+
+/**
+ * list_for_each_entry -       iterate over list of given type
+ * @pos:       the type * to use as a loop counter.
+ * @head:      the head for your list.
+ * @member:    the name of the list_struct within the struct.
+ */
+#define list_for_each_entry(pos, head, member)                         \
+       for (pos = list_entry((head)->next, __typeof__(*pos), member),  \
+                    prefetch(pos->member.next);                        \
+            &pos->member != (head);                                    \
+            pos = list_entry(pos->member.next, __typeof__(*pos), member),      \
+                    prefetch(pos->member.next))
+
+/**
+ * list_for_each_entry_reverse - iterate backwards over list of given type.
+ * @pos:       the type * to use as a loop counter.
+ * @head:      the head for your list.
+ * @member:    the name of the list_struct within the struct.
+ */
+#define list_for_each_entry_reverse(pos, head, member)                 \
+       for (pos = list_entry((head)->prev, __typeof__(*pos), member),  \
+                    prefetch(pos->member.prev);                        \
+            &pos->member != (head);                                    \
+            pos = list_entry(pos->member.prev, __typeof__(*pos), member),      \
+                    prefetch(pos->member.prev))
+
+/**
+ * list_prepare_entry - prepare a pos entry for use as a start point in
+ *                     list_for_each_entry_continue
+ * @pos:       the type * to use as a start point
+ * @head:      the head of the list
+ * @member:    the name of the list_struct within the struct.
+ */
+#define list_prepare_entry(pos, head, member) \
+       ((pos) ? : list_entry(head, __typeof__(*pos), member))
+
+/**
+ * list_for_each_entry_continue -      iterate over list of given type
+ *                     continuing after existing point
+ * @pos:       the type * to use as a loop counter.
+ * @head:      the head for your list.
+ * @member:    the name of the list_struct within the struct.
+ */
+#define list_for_each_entry_continue(pos, head, member)                \
+       for (pos = list_entry(pos->member.next, __typeof__(*pos), member),      \
+                    prefetch(pos->member.next);                        \
+            &pos->member != (head);                                    \
+            pos = list_entry(pos->member.next, __typeof__(*pos), member),      \
+                    prefetch(pos->member.next))
+
+/**
+ * list_for_each_entry_safe - iterate over list of given type safe against removal of list entry
+ * @pos:       the type * to use as a loop counter.
+ * @n:         another type * to use as temporary storage
+ * @head:      the head for your list.
+ * @member:    the name of the list_struct within the struct.
+ */
+#define list_for_each_entry_safe(pos, n, head, member)                 \
+       for (pos = list_entry((head)->next, __typeof__(*pos), member),  \
+               n = list_entry(pos->member.next, __typeof__(*pos), member);     \
+            &pos->member != (head);                                    \
+            pos = n, n = list_entry(n->member.next, __typeof__(*n), member))
+
+
+/**
+ * list_len - get the length of a list
+ * @list: the list to measure
+ */
+static inline size_t list_len(struct list_head *list)
+{
+       size_t len = 0;
+       struct list_head *pos;
+       list_for_each(pos, list)
+               len++;
+       return len;
+}
+
+#endif
diff --git a/load_dir.c b/load_dir.c
new file mode 100644 (file)
index 0000000..007ecfb
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "load_dir.h"
+#include "xmalloc.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <errno.h>
+
+int dir_open(struct directory *dir, const char *name)
+{
+       int len = strlen(name);
+
+       if (len >= sizeof(dir->path) - 2) {
+               errno = ENAMETOOLONG;
+               return -1;
+       }
+
+       dir->d = opendir(name);
+       if (!dir->d)
+               return -1;
+
+       memcpy(dir->path, name, len);
+       dir->path[len++] = '/';
+       dir->path[len] = 0;
+       dir->len = len;
+       return 0;
+}
+
+void dir_close(struct directory *dir)
+{
+       closedir(dir->d);
+}
+
+const char *dir_read(struct directory *dir)
+{
+       DIR *d = dir->d;
+       int len = dir->len;
+       char *full = dir->path;
+       struct dirent *de;
+
+#if defined(__CYGWIN__)
+       /* Fix for cygwin "hang" bug when browsing /
+        * Windows treats // as a network path.
+        */
+       if (strcmp(full, "//") == 0)
+               full++;
+#endif
+
+       while ((de = (struct dirent *) readdir(d))) {
+               const char *name = de->d_name;
+               int nlen = strlen(name);
+
+               /* just ignore too long paths
+                * + 2 -> space for \0 or / and \0
+                */
+               if (len + nlen + 2 >= sizeof(dir->path))
+                       continue;
+
+               memcpy(full + len, name, nlen + 1);
+               if (lstat(full, &dir->st))
+                       continue;
+
+               dir->is_link = 0;
+               if (S_ISLNK(dir->st.st_mode)) {
+                       /* argh. must stat the target */
+                       if (stat(full, &dir->st))
+                               continue;
+                       dir->is_link = 1;
+               }
+
+               return full + len;
+       }
+       return NULL;
+}
+
+void ptr_array_add(struct ptr_array *array, void *ptr)
+{
+       void **ptrs = array->ptrs;
+       int alloc = array->alloc;
+
+       if (alloc == array->count) {
+               alloc = alloc * 3 / 2 + 16;
+               ptrs = xrealloc(ptrs, alloc * sizeof(void *));
+               array->ptrs = ptrs;
+               array->alloc = alloc;
+       }
+       ptrs[array->count++] = ptr;
+}
diff --git a/load_dir.h b/load_dir.h
new file mode 100644 (file)
index 0000000..eb5cd09
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_LOAD_DIR_H
+#define CMUS_LOAD_DIR_H
+
+#include <sys/stat.h>
+#include <stdlib.h>
+#include <dirent.h>
+
+struct directory {
+       DIR *d;
+       int len;
+       /* we need stat information for symlink targets */
+       int is_link;
+       /* stat() information. ie. for the symlink target */
+       struct stat st;
+       char path[1024];
+};
+
+int dir_open(struct directory *dir, const char *name);
+void dir_close(struct directory *dir);
+const char *dir_read(struct directory *dir);
+
+
+struct ptr_array {
+       /* allocated with malloc(). contains pointers */
+       void *ptrs;
+       int alloc;
+       int count;
+};
+
+/* ptr_array.ptrs is either char ** or struct dir_entry ** */
+struct dir_entry {
+       mode_t mode;
+       char name[];
+};
+
+#define PTR_ARRAY(name) struct ptr_array name = { NULL, 0, 0 }
+
+void ptr_array_add(struct ptr_array *array, void *ptr);
+
+static inline void ptr_array_plug(struct ptr_array *array)
+{
+       ptr_array_add(array, NULL);
+       array->count--;
+}
+
+static inline void ptr_array_sort(struct ptr_array *array,
+               int (*cmp)(const void *a, const void *b))
+{
+       int count = array->count;
+       if (count)
+               qsort(array->ptrs, count, sizeof(void *), cmp);
+}
+
+static inline void ptr_array_unique(struct ptr_array *array,
+               int (*cmp)(const void *a, const void *b))
+{
+       void **ptrs = array->ptrs;
+       int i, j = 0;
+
+       for (i = 1; i < array->count; i++) {
+               if (cmp(&ptrs[i-1], &ptrs[i]) != 0)
+                       ptrs[j++] = ptrs[i];
+       }
+       array->count = j;
+}
+
+#endif
diff --git a/locking.c b/locking.c
new file mode 100644 (file)
index 0000000..b57d69b
--- /dev/null
+++ b/locking.c
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "locking.h"
+#include "debug.h"
+
+#include <string.h>
+#include <stdatomic.h>
+#include <stdbool.h>
+
+struct fifo_waiter {
+       struct fifo_waiter * _Atomic next;
+       pthread_cond_t cond;
+};
+
+pthread_t main_thread;
+
+void cmus_mutex_lock(pthread_mutex_t *mutex)
+{
+       int rc = pthread_mutex_lock(mutex);
+       if (unlikely(rc))
+               BUG("error locking mutex: %s\n", strerror(rc));
+}
+
+void cmus_mutex_unlock(pthread_mutex_t *mutex)
+{
+       int rc = pthread_mutex_unlock(mutex);
+       if (unlikely(rc))
+               BUG("error unlocking mutex: %s\n", strerror(rc));
+}
+
+void cmus_rwlock_rdlock(pthread_rwlock_t *lock)
+{
+       int rc = pthread_rwlock_rdlock(lock);
+       if (unlikely(rc))
+               BUG("error locking mutex: %s\n", strerror(rc));
+}
+
+void cmus_rwlock_wrlock(pthread_rwlock_t *lock)
+{
+       int rc = pthread_rwlock_wrlock(lock);
+       if (unlikely(rc))
+               BUG("error locking mutex: %s\n", strerror(rc));
+}
+
+void cmus_rwlock_unlock(pthread_rwlock_t *lock)
+{
+       int rc = pthread_rwlock_unlock(lock);
+       if (unlikely(rc))
+               BUG("error unlocking mutex: %s\n", strerror(rc));
+}
+
+void fifo_mutex_lock(struct fifo_mutex *fifo)
+{
+       struct fifo_waiter self = {
+               .cond = PTHREAD_COND_INITIALIZER,
+               .next = ATOMIC_VAR_INIT(NULL),
+       };
+
+       struct fifo_waiter *old_tail = atomic_exchange_explicit(&fifo->tail, &self,
+                       memory_order_relaxed);
+       if (old_tail)
+               atomic_store_explicit(&old_tail->next, &self, memory_order_release);
+
+       cmus_mutex_lock(&fifo->mutex);
+       if (old_tail) {
+               while (fifo->head != &self)
+                       pthread_cond_wait(&self.cond, &fifo->mutex);
+               pthread_cond_destroy(&self.cond);
+       }
+
+       struct fifo_waiter *self_addr = &self;
+       bool was_tail = atomic_compare_exchange_strong_explicit(&fifo->tail, &self_addr,
+                       NULL, memory_order_relaxed, memory_order_relaxed);
+       struct fifo_waiter *next = NULL;
+       if (!was_tail) {
+               while (!(next = atomic_load_explicit(&self.next, memory_order_consume)))
+                       ;
+       }
+       fifo->head = next;
+}
+
+void fifo_mutex_unlock(struct fifo_mutex *fifo)
+{
+       if (fifo->head)
+               pthread_cond_signal(&fifo->head->cond);
+       cmus_mutex_unlock(&fifo->mutex);
+}
+
+void fifo_mutex_yield(struct fifo_mutex *fifo)
+{
+       if (fifo->head || atomic_load_explicit(&fifo->tail, memory_order_relaxed)) {
+               fifo_mutex_unlock(fifo);
+               fifo_mutex_lock(fifo);
+       }
+}
diff --git a/locking.h b/locking.h
new file mode 100644 (file)
index 0000000..cae3b57
--- /dev/null
+++ b/locking.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_LOCKING_H
+#define CMUS_LOCKING_H
+
+#include <pthread.h>
+#include <stdatomic.h>
+
+struct fifo_mutex {
+       struct fifo_waiter * _Atomic tail;
+       struct fifo_waiter *head;
+       pthread_mutex_t mutex;
+};
+
+extern pthread_t main_thread;
+
+#define CMUS_MUTEX_INITIALIZER PTHREAD_MUTEX_INITIALIZER
+#define CMUS_COND_INITIALIZER PTHREAD_COND_INITIALIZER
+#define CMUS_RWLOCK_INITIALIZER PTHREAD_RWLOCK_INITIALIZER
+
+#define FIFO_MUTEX_INITIALIZER { \
+               .mutex = PTHREAD_MUTEX_INITIALIZER, \
+               .tail = ATOMIC_VAR_INIT(NULL), \
+       }
+
+void cmus_mutex_lock(pthread_mutex_t *mutex);
+void cmus_mutex_unlock(pthread_mutex_t *mutex);
+void cmus_rwlock_rdlock(pthread_rwlock_t *lock);
+void cmus_rwlock_wrlock(pthread_rwlock_t *lock);
+void cmus_rwlock_unlock(pthread_rwlock_t *lock);
+
+void fifo_mutex_lock(struct fifo_mutex *fifo);
+void fifo_mutex_unlock(struct fifo_mutex *fifo);
+void fifo_mutex_yield(struct fifo_mutex *fifo);
+
+#endif
diff --git a/main.c b/main.c
new file mode 100644 (file)
index 0000000..7a1fe58
--- /dev/null
+++ b/main.c
@@ -0,0 +1,406 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005-2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "misc.h"
+#include "prog.h"
+#include "file.h"
+#include "path.h"
+#include "xmalloc.h"
+#include "utils.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <errno.h>
+
+static int sock;
+static int raw_args = 0;
+static char *passwd;
+
+static int read_answer(void)
+{
+       char buf[8192];
+       int got_nl = 0;
+       int len = 0;
+
+       while (1) {
+               int rc = read(sock, buf, sizeof(buf));
+
+               if (rc < 0) {
+                       warn_errno("read");
+                       break;
+               }
+               if (!rc)
+                       die("unexpected EOF\n");
+
+               len += rc;
+
+               // Last line should be empty (i.e. read "\n" or "...\n\n").
+               // Write everything but the last \n to stdout.
+               if (got_nl && buf[0] == '\n')
+                       break;
+               if (len == 1 && buf[0] == '\n')
+                       break;
+               if (rc > 1 && buf[rc - 1] == '\n' && buf[rc - 2] == '\n') {
+                       write_all(1, buf, rc - 1);
+                       break;
+               }
+               got_nl = buf[rc - 1] == '\n';
+               write_all(1, buf, rc);
+       }
+       return len;
+}
+
+static int write_line(const char *line)
+{
+       if (write_all(sock, line, strlen(line)) == -1)
+               die_errno("write");
+
+       return read_answer();
+}
+
+static int send_cmd(const char *format, ...)
+{
+       char buf[512];
+       va_list ap;
+
+       va_start(ap, format);
+       vsnprintf(buf, sizeof(buf), format, ap);
+       va_end(ap);
+
+       return write_line(buf);
+}
+
+static int remote_connect(const char *address)
+{
+       union {
+               struct sockaddr sa;
+               struct sockaddr_un un;
+               struct sockaddr_storage sas;
+       } addr;
+       size_t addrlen;
+
+       if (strchr(address, '/')) {
+               if (passwd)
+                       warn("password ignored for unix connections\n");
+               passwd = NULL;
+
+               addr.sa.sa_family = AF_UNIX;
+               strncpy(addr.un.sun_path, address, sizeof(addr.un.sun_path) - 1);
+
+               addrlen = sizeof(addr.un);
+       } else {
+               const struct addrinfo hints = {
+                       .ai_socktype = SOCK_STREAM
+               };
+               const char *port = STRINGIZE(DEFAULT_PORT);
+               struct addrinfo *result;
+               char *s = strrchr(address, ':');
+               int rc;
+
+               if (!passwd)
+                       die("password required for tcp/ip connection\n");
+
+               if (s) {
+                       *s++ = 0;
+                       port = s;
+               }
+
+               rc = getaddrinfo(address, port, &hints, &result);
+               if (rc != 0)
+                       die("getaddrinfo: %s\n", gai_strerror(rc));
+               memcpy(&addr.sa, result->ai_addr, result->ai_addrlen);
+               addrlen = result->ai_addrlen;
+               freeaddrinfo(result);
+       }
+
+       sock = socket(addr.sa.sa_family, SOCK_STREAM, 0);
+       if (sock == -1)
+               die_errno("socket");
+
+       if (connect(sock, &addr.sa, addrlen)) {
+               if (errno == ENOENT || errno == ECONNREFUSED) {
+                       /* "cmus-remote -C" can be used to check if cmus is running */
+                       if (!raw_args)
+                               warn("cmus is not running\n");
+                       exit(1);
+               }
+               die_errno("connect");
+       }
+
+       if (passwd)
+               return send_cmd("passwd %s\n", passwd) == 1;
+       return 1;
+}
+
+static char *file_url_absolute(const char *str)
+{
+       char *absolute;
+
+       if (strncmp(str, "http://", 7) == 0)
+               return xstrdup(str);
+
+       absolute = path_absolute(str);
+       if (absolute == NULL)
+               die_errno("get_current_dir_name");
+       return absolute;
+}
+
+enum flags {
+       FLAG_SERVER,
+       FLAG_PASSWD,
+       FLAG_HELP,
+       FLAG_VERSION,
+
+       FLAG_PLAY,
+       FLAG_PAUSE,
+       FLAG_PAUSE_PLAYBACK,
+       FLAG_STOP,
+       FLAG_NEXT,
+       FLAG_PREV,
+       FLAG_FILE,
+       FLAG_REPEAT,
+       FLAG_SHUFFLE,
+       FLAG_VOLUME,
+       FLAG_SEEK,
+       FLAG_QUERY,
+
+       FLAG_LIBRARY,
+       FLAG_PLAYLIST,
+       FLAG_QUEUE,
+       FLAG_CLEAR,
+
+       FLAG_RAW
+#define NR_FLAGS (FLAG_RAW + 1)
+};
+
+static struct option options[NR_FLAGS + 1] = {
+       { 0, "server", 1 },
+       { 0, "passwd", 1 },
+       { 0, "help", 0 },
+       { 0, "version", 0 },
+
+       { 'p', "play", 0 },
+       { 'u', "pause", 0 },
+       { 'U', "pause-playback", 0 },
+       { 's', "stop", 0 },
+       { 'n', "next", 0 },
+       { 'r', "prev", 0 },
+       { 'f', "file", 1 },
+       { 'R', "repeat", 0 },
+       { 'S', "shuffle", 0 },
+       { 'v', "volume", 1 },
+       { 'k', "seek", 1 },
+       { 'Q', "query", 0 },
+
+       { 'l', "library", 0 },
+       { 'P', "playlist", 0 },
+       { 'q', "queue", 0 },
+       { 'c', "clear", 0 },
+
+       { 'C', "raw", 0 },
+       { 0, NULL, 0 }
+};
+
+static int flags[NR_FLAGS] = { 0, };
+
+static const char *usage =
+"Usage: %s [OPTION]... [FILE|DIR|PLAYLIST]...\n"
+"   or: %s -C COMMAND...\n"
+"   or: %s\n"
+"Control cmus through socket.\n"
+"\n"
+"      --server ADDR    connect using ADDR instead of $CMUS_SOCKET or $XDG_RUNTIME_DIR/cmus-socket\n"
+"                       ADDR is either a UNIX socket or host[:port]\n"
+"                       WARNING: using TCP/IP is insecure!\n"
+"      --passwd PASSWD  password to use for TCP/IP connection\n"
+"      --help           display this help and exit\n"
+"      --version        " VERSION "\n"
+"\n"
+"Cooked mode:\n"
+"  -p, --play           player-play\n"
+"  -u, --pause          player-pause\n"
+"  -U, --pause-playback player-pause-playback\n"
+"  -s, --stop           player-stop\n"
+"  -n, --next           player-next\n"
+"  -r, --prev           player-prev\n"
+"  -f, --file           player-play FILE\n"
+"  -R, --repeat         toggle repeat\n"
+"  -S, --shuffle        toggle shuffle\n"
+"  -v, --volume VOL     vol VOL\n"
+"  -k, --seek SEEK      seek SEEK\n"
+"  -Q, --query          get player status (same as -C status)\n"
+"\n"
+"  -l, --library        modify library instead of playlist\n"
+"  -P, --playlist       modify playlist (default)\n"
+"  -q, --queue          modify play queue instead of playlist\n"
+"  -c, --clear          clear playlist, library (-l) or play queue (-q)\n"
+"\n"
+"  Add FILE/DIR/PLAYLIST to playlist, library (-l) or play queue (-q).\n"
+"\n"
+"Raw mode:\n"
+"  -C, --raw            treat arguments (instead of stdin) as raw commands\n"
+"\n"
+"  By default cmus-remote reads raw commands from stdin (one command per line).\n"
+"\n"
+"Report bugs to <cmus-devel@lists.sourceforge.net>.\n";
+
+int main(int argc, char *argv[])
+{
+       char *server = NULL;
+       char *play_file = NULL;
+       char *volume = NULL;
+       char *seek = NULL;
+       int query = 0;
+       int i, nr_cmds = 0;
+       int context = 'p';
+
+       program_name = argv[0];
+       argv++;
+       while (1) {
+               int idx;
+               char *arg;
+
+               idx = get_option(&argv, options, &arg);
+               if (idx < 0)
+                       break;
+
+               flags[idx] = 1;
+               switch ((enum flags)idx) {
+               case FLAG_HELP:
+                       printf(usage, program_name, program_name, program_name);
+                       return 0;
+               case FLAG_VERSION:
+                       printf("cmus " VERSION
+                              "\nCopyright 2004-2006 Timo Hirvonen"
+                              "\nCopyright 2008-2016 Various Authors\n");
+                       return 0;
+               case FLAG_SERVER:
+                       server = arg;
+                       break;
+               case FLAG_PASSWD:
+                       passwd = arg;
+                       break;
+               case FLAG_VOLUME:
+                       volume = arg;
+                       nr_cmds++;
+                       break;
+               case FLAG_SEEK:
+                       seek = arg;
+                       nr_cmds++;
+                       break;
+               case FLAG_QUERY:
+                       query = 1;
+                       nr_cmds++;
+                       break;
+               case FLAG_FILE:
+                       play_file = arg;
+                       nr_cmds++;
+                       break;
+               case FLAG_LIBRARY:
+                       context = 'l';
+                       break;
+               case FLAG_PLAYLIST:
+                       context = 'p';
+                       break;
+               case FLAG_QUEUE:
+                       context = 'q';
+                       break;
+               case FLAG_PLAY:
+               case FLAG_PAUSE:
+               case FLAG_PAUSE_PLAYBACK:
+               case FLAG_STOP:
+               case FLAG_NEXT:
+               case FLAG_PREV:
+               case FLAG_REPEAT:
+               case FLAG_SHUFFLE:
+               case FLAG_CLEAR:
+                       nr_cmds++;
+                       break;
+               case FLAG_RAW:
+                       raw_args = 1;
+                       break;
+               }
+       }
+
+       if (nr_cmds && raw_args)
+               die("don't mix raw and cooked stuff\n");
+
+       misc_init();
+       if (server == NULL)
+               server = xstrdup(cmus_socket_path);
+
+       if (!remote_connect(server))
+               return 1;
+
+       if (raw_args) {
+               while (*argv)
+                       send_cmd("%s\n", *argv++);
+               return 0;
+       }
+
+       if (nr_cmds == 0 && argv[0] == NULL) {
+               char line[512];
+
+               while (fgets(line, sizeof(line), stdin))
+                       write_line(line);
+               return 0;
+       }
+
+       if (flags[FLAG_CLEAR])
+               send_cmd("clear -%c\n", context);
+       for (i = 0; argv[i]; i++) {
+               char *filename = file_url_absolute(argv[i]);
+
+               send_cmd("add -%c %s\n", context, filename);
+               free(filename);
+       }
+       if (flags[FLAG_REPEAT])
+               send_cmd("toggle repeat\n");
+       if (flags[FLAG_SHUFFLE])
+               send_cmd("toggle shuffle\n");
+       if (flags[FLAG_STOP])
+               send_cmd("player-stop\n");
+       if (flags[FLAG_NEXT])
+               send_cmd("player-next\n");
+       if (flags[FLAG_PREV])
+               send_cmd("player-prev\n");
+       if (flags[FLAG_PLAY])
+               send_cmd("player-play\n");
+       if (flags[FLAG_PAUSE])
+               send_cmd("player-pause\n");
+       if (flags[FLAG_PAUSE_PLAYBACK])
+               send_cmd("player-pause-playback\n");
+       if (flags[FLAG_FILE])
+               send_cmd("player-play %s\n", file_url_absolute(play_file));
+       if (volume)
+               send_cmd("vol %s\n", volume);
+       if (seek)
+               send_cmd("seek %s\n", seek);
+       if (query)
+               send_cmd("status\n");
+       return 0;
+}
diff --git a/mergesort.c b/mergesort.c
new file mode 100644 (file)
index 0000000..decf9bf
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "mergesort.h"
+#include "list.h"
+
+void list_mergesort(struct list_head *head,
+       int (*compare)(const struct list_head *, const struct list_head *))
+{
+       LIST_HEAD(empty);
+       struct list_head *unsorted_head, *sorted_head, *p, *q, *tmp;
+       int psize, qsize, K, count;
+
+       if (list_empty(head))
+               return;
+
+       unsorted_head = head;
+       sorted_head = &empty;
+       K = 1;
+       while (1) {
+               p = unsorted_head->next;
+               count = 0;
+               do {
+                       q = p;
+                       psize = 0;
+                       while (psize < K) {
+                               if (q == unsorted_head)
+                                       break;
+                               psize++;
+                               q = q->next;
+                       }
+                       qsize = K;
+                       while (1) {
+                               struct list_head *e;
+
+                               if (q == unsorted_head)
+                                       qsize = 0;
+                               if (psize == 0 && qsize == 0)
+                                       break;
+                               if (!psize || (qsize && compare(p, q) > 0)) {
+                                       e = q;
+                                       q = q->next;
+                                       qsize--;
+                               } else {
+                                       e = p;
+                                       p = p->next;
+                                       psize--;
+                               }
+                               list_del(e);
+                               list_add_tail(e, sorted_head);
+                       }
+                       count++;
+                       p = q;
+               } while (p != unsorted_head);
+
+               if (count == 1) {
+                       head->next = sorted_head->next;
+                       head->prev = sorted_head->prev;
+                       sorted_head->prev->next = head;
+                       sorted_head->next->prev = head;
+                       return;
+               }
+               tmp = unsorted_head;
+               unsorted_head = sorted_head;
+               sorted_head = tmp;
+               K *= 2;
+       }
+}
diff --git a/mergesort.h b/mergesort.h
new file mode 100644 (file)
index 0000000..aff0bb6
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_MERGESORT_H
+#define CMUS_MERGESORT_H
+
+#include "list.h"
+
+void list_mergesort(struct list_head *head,
+       int (*compare)(const struct list_head *, const struct list_head *));
+
+#endif
diff --git a/misc.c b/misc.c
new file mode 100644 (file)
index 0000000..d8d4021
--- /dev/null
+++ b/misc.c
@@ -0,0 +1,371 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "misc.h"
+#include "prog.h"
+#include "xmalloc.h"
+#include "xstrjoin.h"
+#include "ui_curses.h"
+#include "config/libdir.h"
+#include "config/datadir.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <stdarg.h>
+#include <pwd.h>
+
+const char *cmus_config_dir = NULL;
+const char *cmus_playlist_dir = NULL;
+const char *cmus_socket_path = NULL;
+const char *cmus_data_dir = NULL;
+const char *cmus_lib_dir = NULL;
+const char *home_dir = NULL;
+const char *user_name = NULL;
+
+char **get_words(const char *text)
+{
+       char **words;
+       int i, j, count;
+
+       while (*text == ' ')
+               text++;
+
+       count = 0;
+       i = 0;
+       while (text[i]) {
+               count++;
+               while (text[i] && text[i] != ' ')
+                       i++;
+               while (text[i] == ' ')
+                       i++;
+       }
+       words = xnew(char *, count + 1);
+
+       i = 0;
+       j = 0;
+       while (text[i]) {
+               int start = i;
+
+               while (text[i] && text[i] != ' ')
+                       i++;
+               words[j++] = xstrndup(text + start, i - start);
+               while (text[i] == ' ')
+                       i++;
+       }
+       words[j] = NULL;
+       return words;
+}
+
+int strptrcmp(const void *a, const void *b)
+{
+       const char *as = *(char **)a;
+       const char *bs = *(char **)b;
+
+       return strcmp(as, bs);
+}
+
+int strptrcoll(const void *a, const void *b)
+{
+       const char *as = *(char **)a;
+       const char *bs = *(char **)b;
+
+       return strcoll(as, bs);
+}
+
+const char *escape(const char *str)
+{
+       static char *buf = NULL;
+       static size_t alloc = 0;
+       size_t len = strlen(str);
+       size_t need = len * 2 + 1;
+       int s, d;
+
+       if (need > alloc) {
+               alloc = (need + 16) & ~(16 - 1);
+               buf = xrealloc(buf, alloc);
+       }
+
+       d = 0;
+       for (s = 0; str[s]; s++) {
+               if (str[s] == '\\') {
+                       buf[d++] = '\\';
+                       buf[d++] = '\\';
+                       continue;
+               }
+               if (str[s] == '\n') {
+                       buf[d++] = '\\';
+                       buf[d++] = 'n';
+                       continue;
+               }
+               buf[d++] = str[s];
+       }
+       buf[d] = 0;
+       return buf;
+}
+
+const char *unescape(const char *str)
+{
+       static char *buf = NULL;
+       static size_t alloc = 0;
+       size_t need = strlen(str) + 1;
+       int do_escape = 0;
+       int s, d;
+
+       if (need > alloc) {
+               alloc = (need + 16) & ~(16 - 1);
+               buf = xrealloc(buf, alloc);
+       }
+
+       d = 0;
+       for (s = 0; str[s]; s++) {
+               if (!do_escape && str[s] == '\\')
+                       do_escape = 1;
+               else {
+                       buf[d++] = (do_escape && str[s] == 'n') ? '\n' : str[s];
+                       do_escape = 0;
+               }
+       }
+       buf[d] = 0;
+       return buf;
+}
+
+static int dir_exists(const char *dirname)
+{
+       DIR *dir;
+
+       dir = opendir(dirname);
+       if (dir == NULL) {
+               if (errno == ENOENT)
+                       return 0;
+               return -1;
+       }
+       closedir(dir);
+       return 1;
+}
+
+static void make_dir(const char *dirname)
+{
+       int rc;
+
+       rc = dir_exists(dirname);
+       if (rc == 1)
+               return;
+       if (rc == -1)
+               die_errno("error: opening `%s'", dirname);
+       rc = mkdir(dirname, 0700);
+       if (rc == -1)
+               die_errno("error: creating directory `%s'", dirname);
+}
+
+static char *get_non_empty_env(const char *name)
+{
+       const char *val;
+
+       val = getenv(name);
+       if (val == NULL || val[0] == 0)
+               return NULL;
+       return xstrdup(val);
+}
+
+const char *get_filename(const char *path)
+{
+       const char *file = strrchr(path, '/');
+       if (!file)
+               file = path;
+       else
+               file++;
+       if (!*file)
+               return NULL;
+       return file;
+}
+
+static void move_old_playlist(void)
+{
+       char *default_playlist = xstrjoin(cmus_playlist_dir, "/default");
+       char *old_playlist = xstrjoin(cmus_config_dir, "/playlist.pl");
+       int rc = rename(old_playlist, default_playlist);
+       if (rc && errno != ENOENT)
+               die_errno("error: unable to move %s to playlist directory",
+                               old_playlist);
+       free(default_playlist);
+       free(old_playlist);
+}
+
+int misc_init(void)
+{
+       char *xdg_runtime_dir = get_non_empty_env("XDG_RUNTIME_DIR");
+
+       home_dir = get_non_empty_env("HOME");
+       if (home_dir == NULL)
+               die("error: environment variable HOME not set\n");
+
+       user_name = get_non_empty_env("USER");
+       if (user_name == NULL) {
+               user_name = get_non_empty_env("USERNAME");
+               if (user_name == NULL)
+                       die("error: neither USER or USERNAME environment variable set\n");
+       }
+
+       cmus_config_dir = get_non_empty_env("CMUS_HOME");
+       if (cmus_config_dir == NULL) {
+               char *cmus_home = xstrjoin(home_dir, "/.cmus");
+               int cmus_home_exists = dir_exists(cmus_home);
+
+               if (cmus_home_exists == 1) {
+                       cmus_config_dir = xstrdup(cmus_home);
+               } else if (cmus_home_exists == -1) {
+                       die_errno("error: opening `%s'", cmus_home);
+               } else {
+                       char *xdg_config_home = get_non_empty_env("XDG_CONFIG_HOME");
+                       if (xdg_config_home == NULL) {
+                               xdg_config_home = xstrjoin(home_dir, "/.config");
+                       }
+
+                       make_dir(xdg_config_home);
+                       cmus_config_dir = xstrjoin(xdg_config_home, "/cmus");
+
+                       free(xdg_config_home);
+               }
+
+               free(cmus_home);
+       }
+       make_dir(cmus_config_dir);
+
+       cmus_playlist_dir = get_non_empty_env("CMUS_PLAYLIST_DIR");
+       if (!cmus_playlist_dir)
+               cmus_playlist_dir = xstrjoin(cmus_config_dir, "/playlists");
+
+       int playlist_dir_is_new = dir_exists(cmus_playlist_dir) == 0;
+       make_dir(cmus_playlist_dir);
+       if (playlist_dir_is_new)
+               move_old_playlist();
+
+       cmus_socket_path = get_non_empty_env("CMUS_SOCKET");
+       if (cmus_socket_path == NULL) {
+               if (xdg_runtime_dir == NULL) {
+                       cmus_socket_path = xstrjoin(cmus_config_dir, "/socket");
+               } else {
+                       cmus_socket_path = xstrjoin(xdg_runtime_dir, "/cmus-socket");
+               }
+       }
+
+       cmus_lib_dir = getenv("CMUS_LIB_DIR");
+       if (!cmus_lib_dir)
+               cmus_lib_dir = LIBDIR "/cmus";
+
+       cmus_data_dir = getenv("CMUS_DATA_DIR");
+       if (!cmus_data_dir)
+               cmus_data_dir = DATADIR "/cmus";
+
+       free(xdg_runtime_dir);
+       return 0;
+}
+
+int replaygain_decode(unsigned int field, int *gain)
+{
+       unsigned int name_code, originator_code, sign_bit, val;
+
+       name_code = (field >> 13) & 0x7;
+       if (!name_code || name_code > 2)
+               return 0;
+       originator_code = (field >> 10) & 0x7;
+       if (!originator_code)
+               return 0;
+       sign_bit = (field >> 9) & 0x1;
+       val = field & 0x1ff;
+       if (sign_bit && !val)
+               return 0;
+       *gain = (sign_bit ? -1 : 1) * val;
+       return name_code;
+}
+
+static char *get_home_dir(const char *username)
+{
+       struct passwd *passwd;
+
+       if (username == NULL)
+               return xstrdup(home_dir);
+       passwd = getpwnam(username);
+       if (passwd == NULL)
+               return NULL;
+       /* don't free passwd */
+       return xstrdup(passwd->pw_dir);
+}
+
+char *expand_filename(const char *name)
+{
+       if (name[0] == '~') {
+               char *slash;
+
+               slash = strchr(name, '/');
+               if (slash) {
+                       char *username, *home;
+
+                       if (slash - name - 1 > 0) {
+                               /* ~user/... */
+                               username = xstrndup(name + 1, slash - name - 1);
+                       } else {
+                               /* ~/... */
+                               username = NULL;
+                       }
+                       home = get_home_dir(username);
+                       free(username);
+                       if (home) {
+                               char *expanded;
+
+                               expanded = xstrjoin(home, slash);
+                               free(home);
+                               return expanded;
+                       } else {
+                               return xstrdup(name);
+                       }
+               } else {
+                       if (name[1] == 0) {
+                               return xstrdup(home_dir);
+                       } else {
+                               char *home;
+
+                               home = get_home_dir(name + 1);
+                               if (home)
+                                       return home;
+                               return xstrdup(name);
+                       }
+               }
+       } else {
+               return xstrdup(name);
+       }
+}
+
+void shuffle_array(void *array, size_t n, size_t size) 
+{
+       char tmp[size];
+       char *arr = array;
+       for (ssize_t i = 0; i < (ssize_t)n - 1; ++i) {
+               size_t rnd = (size_t) rand();
+               size_t j = i + rnd / (RAND_MAX / (n - i) + 1);
+               memcpy(tmp, arr + j * size, size);
+               memcpy(arr + j * size, arr + i * size, size);
+               memcpy(arr + i * size, tmp, size);
+       }
+}
diff --git a/misc.h b/misc.h
new file mode 100644 (file)
index 0000000..48ec69e
--- /dev/null
+++ b/misc.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_MISC_H
+#define CMUS_MISC_H
+
+#include <stddef.h>
+
+extern const char *cmus_config_dir;
+extern const char *cmus_playlist_dir;
+extern const char *cmus_socket_path;
+extern const char *cmus_data_dir;
+extern const char *cmus_lib_dir;
+extern const char *home_dir;
+extern const char *user_name;
+
+char **get_words(const char *text);
+int strptrcmp(const void *a, const void *b);
+int strptrcoll(const void *a, const void *b);
+int misc_init(void);
+const char *escape(const char *str);
+const char *unescape(const char *str);
+const char *get_filename(const char *path);
+
+/*
+ * @field   contains Replay Gain data format in bit representation
+ * @gain    pointer where to store gain value * 10
+ *
+ * Returns 0 if @field doesn't contain a valid gain value,
+ *         1 for track (= radio) adjustment
+ *         2 for album (= audiophile) adjustment
+ *
+ * http://replaygain.hydrogenaudio.org/rg_data_format.html
+ */
+int replaygain_decode(unsigned int field, int *gain);
+
+char *expand_filename(const char *name);
+void shuffle_array(void *array, size_t n, size_t size);
+
+#endif
diff --git a/mixer.h b/mixer.h
new file mode 100644 (file)
index 0000000..20f7229
--- /dev/null
+++ b/mixer.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_MIXER_H
+#define CMUS_MIXER_H
+
+#ifndef __GNUC__
+#include <fcntl.h>
+#endif
+
+#define NR_MIXER_FDS 4
+
+struct mixer_plugin_ops {
+       int (*init)(void);
+       int (*exit)(void);
+       int (*open)(int *volume_max);
+       int (*close)(void);
+       int (*get_fds)(int *fds);
+       int (*set_volume)(int l, int r);
+       int (*get_volume)(int *l, int *r);
+};
+
+struct mixer_plugin_opt {
+       const char *name;
+       int (*set)(const char *val);
+       int (*get)(char **val);
+};
+
+/* symbols exported by plugin */
+extern const struct mixer_plugin_ops op_mixer_ops;
+extern const struct mixer_plugin_opt op_mixer_options[];
+
+#endif
diff --git a/mpris.c b/mpris.c
new file mode 100644 (file)
index 0000000..27a8c69
--- /dev/null
+++ b/mpris.c
@@ -0,0 +1,560 @@
+/*
+ * Copyright 2016 Various Authors
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <systemd/sd-bus.h>
+
+#include "mpris.h"
+#include "ui_curses.h"
+#include "cmus.h"
+#include "player.h"
+#include "options.h"
+#include "output.h"
+#include "track_info.h"
+#include "utils.h"
+
+#define CK(v) \
+do { \
+       int tmp = (v); \
+       if (tmp < 0) \
+               return tmp; \
+} while (0)
+
+static sd_bus *bus;
+int mpris_fd = -1;
+
+static int mpris_msg_ignore(sd_bus_message *m, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       return sd_bus_reply_method_return(m, "");
+}
+
+static int mpris_read_false(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *reply, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       uint32_t b = 0;
+       return sd_bus_message_append_basic(reply, 'b', &b);
+}
+
+static int mpris_read_true(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *reply, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       uint32_t b = 1;
+       return sd_bus_message_append_basic(reply, 'b', &b);
+}
+
+static int mpris_write_ignore(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *value, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       return sd_bus_reply_method_return(value, "");
+}
+
+static int mpris_raise_vte(sd_bus_message *m, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       cmus_raise_vte();
+       return sd_bus_reply_method_return(m, "");
+}
+
+static int mpris_can_raise_vte(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *reply, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       uint32_t b = cmus_can_raise_vte();
+       return sd_bus_message_append_basic(reply, 'b', &b);
+}
+
+static int mpris_identity(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *reply, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       const char *id = "cmus";
+       return sd_bus_message_append_basic(reply, 's', id);
+}
+
+static int mpris_uri_schemes(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *reply, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       static const char * const schemes[] = { "file", "http", NULL };
+       return sd_bus_message_append_strv(reply, (char **)schemes);
+}
+
+static int mpris_mime_types(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *reply, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       static const char * const types[] = { NULL };
+       return sd_bus_message_append_strv(reply, (char **)types);
+}
+
+static int mpris_next(sd_bus_message *m, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       cmus_next();
+       return sd_bus_reply_method_return(m, "");
+}
+
+static int mpris_prev(sd_bus_message *m, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       cmus_prev();
+       return sd_bus_reply_method_return(m, "");
+}
+
+static int mpris_pause(sd_bus_message *m, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       player_pause_playback();
+       return sd_bus_reply_method_return(m, "");
+}
+
+static int mpris_toggle_pause(sd_bus_message *m, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       player_pause();
+       return sd_bus_reply_method_return(m, "");
+}
+
+static int mpris_stop(sd_bus_message *m, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       player_stop();
+       return sd_bus_reply_method_return(m, "");
+}
+
+static int mpris_play(sd_bus_message *m, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       player_play();
+       return sd_bus_reply_method_return(m, "");
+}
+
+static int mpris_seek(sd_bus_message *m, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       int64_t val = 0;
+       CK(sd_bus_message_read_basic(m, 'x', &val));
+       player_seek(val / (1000 * 1000), 1, 0);
+       return sd_bus_reply_method_return(m, "");
+}
+
+static int mpris_seek_abs(sd_bus_message *m, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       char buf[] = "/1122334455667788";
+       if (player_info.ti)
+               sprintf(buf, "/%"PRIx64, player_info.ti->uid);
+       else
+               sprintf(buf, "/");
+
+       const char *path = NULL;
+       int64_t val = 0;
+       CK(sd_bus_message_read_basic(m, 'o', &path));
+       CK(sd_bus_message_read_basic(m, 'x', &val));
+
+       if (strcmp(buf, path) == 0)
+               player_seek(val / (1000 * 1000), 0, 0);
+       return sd_bus_reply_method_return(m, "");
+}
+
+static int mpris_play_file(sd_bus_message *m, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       const char *path = NULL;
+       CK(sd_bus_message_read_basic(m, 's', &path));
+       cmus_play_file(path);
+       return sd_bus_reply_method_return(m, "");
+}
+
+static int mpris_playback_status(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *reply, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       const char *ss[] = { "Stopped", "Playing", "Paused" };
+       const char *s = ss[player_info.status];
+       return sd_bus_message_append_basic(reply, 's', s);
+}
+
+static int mpris_loop_status(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *reply, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       const char *t = "None";
+       if (player_repeat_current)
+               t = "Track";
+       else if (repeat)
+               t = "Playlist";
+       return sd_bus_message_append_basic(reply, 's', t);
+}
+
+static int mpris_set_loop_status(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *value, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       const char *t = NULL;
+       CK(sd_bus_message_read_basic(value, 's', &t));
+       if (strcmp(t, "None") == 0) {
+               player_repeat_current = 0;
+               repeat = 0;
+       } else if (strcmp(t, "Track") == 0) {
+               player_repeat_current = 1;
+       } else if (strcmp(t, "Playlist") == 0) {
+               player_repeat_current = 0;
+               repeat = 1;
+       }
+       update_statusline();
+       return sd_bus_reply_method_return(value, "");
+}
+
+static int mpris_rate(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *reply, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       static const double d = 1.0;
+       return sd_bus_message_append_basic(reply, 'd', &d);
+}
+
+static int mpris_shuffle(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *reply, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       const uint32_t s = shuffle;
+       return sd_bus_message_append_basic(reply, 'b', &s);
+}
+
+static int mpris_set_shuffle(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *value, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       uint32_t s = 0;
+       CK(sd_bus_message_read_basic(value, 'b', &s));
+       shuffle = s;
+       update_statusline();
+       return sd_bus_reply_method_return(value, "");
+}
+
+static int mpris_volume(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *reply, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       double vol;
+       if (soft_vol) {
+               vol = (soft_vol_l + soft_vol_r) / 200.0;
+       } else if (volume_max && volume_l >= 0 && volume_r >= 0) {
+               int vol_left = scale_to_percentage(volume_l, volume_max);
+               int vol_right = scale_to_percentage(volume_r, volume_max);
+               vol = (vol_left + vol_right) / 200.0;
+       }
+       return sd_bus_message_append_basic(reply, 'd', &vol);
+}
+
+static int mpris_set_volume(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *value, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       double vol;
+       CK(sd_bus_message_read_basic(value, 'd', &vol));
+       if (vol < 0.0)
+               vol = 0.0;
+       else if (vol > 1.0)
+               vol = 1.0;
+       int ivol = vol * 100;
+       player_set_vol(ivol, VF_PERCENTAGE, ivol, VF_PERCENTAGE);
+       update_statusline();
+       return sd_bus_reply_method_return(value, "");
+}
+
+static int mpris_position(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *reply, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       int64_t pos = player_info.pos;
+       pos *= 1000 * 1000;
+       return sd_bus_message_append_basic(reply, 'x', &pos);
+}
+
+static int mpris_msg_append_simple_dict(sd_bus_message *m, const char *tag,
+               char type, const void *val)
+{
+       const char s[] = { type, 0 };
+       CK(sd_bus_message_open_container(m, 'e', "sv"));
+       CK(sd_bus_message_append_basic(m, 's', tag));
+       CK(sd_bus_message_open_container(m, 'v', s));
+       CK(sd_bus_message_append_basic(m, type, val));
+       CK(sd_bus_message_close_container(m));
+       CK(sd_bus_message_close_container(m));
+       return 0;
+}
+
+static int mpris_msg_append_si_dict(sd_bus_message *m, const char *a,
+               int32_t i)
+{
+       return mpris_msg_append_simple_dict(m, a, 'i', &i);
+}
+
+static int mpris_msg_append_sx_dict(sd_bus_message *m, const char *a,
+               int64_t i)
+{
+       return mpris_msg_append_simple_dict(m, a, 'x', &i);
+}
+
+static int mpris_msg_append_ss_dict(sd_bus_message *m, const char *a,
+               const char *b)
+{
+       return mpris_msg_append_simple_dict(m, a, 's', b);
+}
+
+static int mpris_msg_append_so_dict(sd_bus_message *m, const char *a,
+               const char *b)
+{
+       return mpris_msg_append_simple_dict(m, a, 'o', b);
+}
+
+static int mpris_msg_append_sas_dict(sd_bus_message *m, const char *a,
+               const char *b)
+{
+       CK(sd_bus_message_open_container(m, 'e', "sv"));
+       CK(sd_bus_message_append_basic(m, 's', a));
+       CK(sd_bus_message_open_container(m, 'v', "as"));
+       CK(sd_bus_message_open_container(m, 'a', "s"));
+       CK(sd_bus_message_append_basic(m, 's', b));
+       CK(sd_bus_message_close_container(m));
+       CK(sd_bus_message_close_container(m));
+       CK(sd_bus_message_close_container(m));
+       return 0;
+}
+
+static int mpris_metadata(sd_bus *_bus, const char *_path,
+               const char *_interface, const char *_property,
+               sd_bus_message *reply, void *_userdata,
+               sd_bus_error *_ret_error)
+{
+       CK(sd_bus_message_open_container(reply, 'a', "{sv}"));
+
+       struct track_info *ti = player_info.ti;
+       if (ti) {
+               char buf[] = "/1122334455667788";
+               sprintf(buf, "/%"PRIx64, ti->uid);
+               CK(mpris_msg_append_so_dict(reply, "mpris:trackid", buf));
+
+               int64_t dur = ti->duration;
+               dur *= 1000 * 1000;
+               CK(mpris_msg_append_sx_dict(reply, "mpris:length", dur));
+
+               if (ti->artist)
+                       CK(mpris_msg_append_sas_dict(reply, "xesam:artist",
+                                               ti->artist));
+               if (ti->title)
+                       CK(mpris_msg_append_ss_dict(reply, "xesam:title",
+                                               ti->title));
+               if (ti->album)
+                       CK(mpris_msg_append_ss_dict(reply, "xesam:album",
+                                               ti->album));
+               if (ti->albumartist)
+                       CK(mpris_msg_append_sas_dict(reply, "xesam:albumArtist",
+                                               ti->albumartist));
+               if (ti->genre)
+                       CK(mpris_msg_append_sas_dict(reply, "xesam:genre",
+                                               ti->genre));
+               if (ti->comment)
+                       CK(mpris_msg_append_sas_dict(reply, "xesam:comment",
+                                               ti->comment));
+               if (ti->bpm != -1)
+                       CK(mpris_msg_append_si_dict(reply, "xesam:audioBPM",
+                                               ti->bpm));
+               if (ti->tracknumber != -1)
+                       CK(mpris_msg_append_si_dict(reply, "xesam:trackNumber",
+                                               ti->tracknumber));
+               if (ti->discnumber != -1)
+                       CK(mpris_msg_append_si_dict(reply, "xesam:discNumber",
+                                               ti->discnumber));
+               if (is_http_url(ti->filename))
+                       CK(mpris_msg_append_ss_dict(reply, "cmus:stream_title",
+                                               get_stream_title()));
+       }
+
+       CK(sd_bus_message_close_container(reply));
+       return 0;
+}
+
+#define MPRIS_PROP(name, type, read) \
+       SD_BUS_PROPERTY(name, type, read, 0, \
+                       SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE)
+
+#define MPRIS_WPROP(name, type, read, write) \
+       SD_BUS_WRITABLE_PROPERTY(name, type, read, write, 0, \
+                       SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE)
+
+static const sd_bus_vtable media_player2_vt[] = {
+       SD_BUS_VTABLE_START(0),
+       SD_BUS_METHOD("Raise", "", "", mpris_raise_vte, 0),
+       SD_BUS_METHOD("Quit", "", "", mpris_msg_ignore, 0),
+       MPRIS_PROP("CanQuit", "b", mpris_read_false),
+       MPRIS_WPROP("Fullscreen", "b", mpris_read_false, mpris_write_ignore),
+       MPRIS_PROP("CanSetFullscreen", "b", mpris_read_false),
+       MPRIS_PROP("CanRaise", "b", mpris_can_raise_vte),
+       MPRIS_PROP("HasTrackList", "b", mpris_read_false),
+       MPRIS_PROP("Identity", "s", mpris_identity),
+       MPRIS_PROP("SupportedUriSchemes", "as", mpris_uri_schemes),
+       MPRIS_PROP("SupportedMimeTypes", "as", mpris_mime_types),
+       SD_BUS_VTABLE_END,
+};
+
+static const sd_bus_vtable media_player2_player_vt[] = {
+       SD_BUS_VTABLE_START(0),
+       SD_BUS_METHOD("Next", "", "", mpris_next, 0),
+       SD_BUS_METHOD("Previous", "", "", mpris_prev, 0),
+       SD_BUS_METHOD("Pause", "", "", mpris_pause, 0),
+       SD_BUS_METHOD("PlayPause", "", "", mpris_toggle_pause, 0),
+       SD_BUS_METHOD("Stop", "", "", mpris_stop, 0),
+       SD_BUS_METHOD("Play", "", "", mpris_play, 0),
+       SD_BUS_METHOD("Seek", "x", "", mpris_seek, 0),
+       SD_BUS_METHOD("SetPosition", "ox", "", mpris_seek_abs, 0),
+       SD_BUS_METHOD("OpenUri", "s", "", mpris_play_file, 0),
+       MPRIS_PROP("PlaybackStatus", "s", mpris_playback_status),
+       MPRIS_WPROP("LoopStatus", "s", mpris_loop_status, mpris_set_loop_status),
+       MPRIS_WPROP("Rate", "d", mpris_rate, mpris_write_ignore),
+       MPRIS_WPROP("Shuffle", "b", mpris_shuffle, mpris_set_shuffle),
+       MPRIS_WPROP("Volume", "d", mpris_volume, mpris_set_volume),
+       SD_BUS_PROPERTY("Position", "x", mpris_position, 0, 0),
+       MPRIS_PROP("MinimumRate", "d", mpris_rate),
+       MPRIS_PROP("MaximumRate", "d", mpris_rate),
+       MPRIS_PROP("CanGoNext", "b", mpris_read_true),
+       MPRIS_PROP("CanGoPrevious", "b", mpris_read_true),
+       MPRIS_PROP("CanPlay", "b", mpris_read_true),
+       MPRIS_PROP("CanPause", "b", mpris_read_true),
+       MPRIS_PROP("CanSeek", "b", mpris_read_true),
+       SD_BUS_PROPERTY("CanControl", "b", mpris_read_true, 0, 0),
+       MPRIS_PROP("Metadata", "a{sv}", mpris_metadata),
+       SD_BUS_SIGNAL("Seeked", "x", 0),
+       SD_BUS_VTABLE_END,
+};
+
+void mpris_init(void)
+{
+       int res = 0;
+
+       res = sd_bus_default_user(&bus);
+       if (res < 0)
+               goto out;
+       res = sd_bus_add_object_vtable(bus, NULL, "/org/mpris/MediaPlayer2",
+                       "org.mpris.MediaPlayer2", media_player2_vt, NULL);
+       if (res < 0)
+               goto out;
+       res = sd_bus_add_object_vtable(bus, NULL, "/org/mpris/MediaPlayer2",
+                       "org.mpris.MediaPlayer2.Player",
+                       media_player2_player_vt, NULL);
+       if (res < 0)
+               goto out;
+       res = sd_bus_request_name(bus, "org.mpris.MediaPlayer2.cmus", 0);
+       mpris_fd = sd_bus_get_fd(bus);
+
+out:
+       if (res < 0) {
+               sd_bus_unref(bus);
+               bus = NULL;
+               mpris_fd = -1;
+
+               const char *msg = "an error occured while initializing "
+                                 "MPRIS: %s. MPRIS will be disabled.";
+
+               error_msg(msg, strerror(-res));
+       }
+}
+
+void mpris_process(void)
+{
+       if (bus) {
+               while (sd_bus_process(bus, NULL) > 0)
+                       ;
+       }
+}
+
+void mpris_free(void)
+{
+       sd_bus_unref(bus);
+       bus = NULL;
+       mpris_fd = -1;
+}
+
+static void mpris_player_property_changed(const char *name)
+{
+       const char * const strv[] = { name, NULL };
+       if (bus) {
+               sd_bus_emit_properties_changed_strv(bus,
+                               "/org/mpris/MediaPlayer2",
+                               "org.mpris.MediaPlayer2.Player", (char **)strv);
+               sd_bus_flush(bus);
+       }
+}
+
+void mpris_playback_status_changed(void)
+{
+       mpris_player_property_changed("PlaybackStatus");
+}
+
+void mpris_loop_status_changed(void)
+{
+       mpris_player_property_changed("LoopStatus");
+}
+
+void mpris_shuffle_changed(void)
+{
+       mpris_player_property_changed("Shuffle");
+}
+
+void mpris_volume_changed(void)
+{
+       mpris_player_property_changed("Volume");
+}
+
+void mpris_metadata_changed(void)
+{
+       mpris_player_property_changed("Metadata");
+       // the following is not necessary according to the spec but some
+       // applications seem to disregard the spec and expect this to happen
+       mpris_seeked();
+}
+
+void mpris_seeked(void)
+{
+       if (!bus)
+               return;
+       int64_t pos = player_info.pos;
+       pos *= 1000 * 1000;
+       sd_bus_emit_signal(bus, "/org/mpris/MediaPlayer2",
+                       "org.mpris.MediaPlayer2.Player", "Seeked", "x", pos);
+}
diff --git a/mpris.h b/mpris.h
new file mode 100644 (file)
index 0000000..644f6a9
--- /dev/null
+++ b/mpris.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_MPRIS_H
+#define CMUS_MPRIS_H
+
+#include "config/mpris.h"
+
+#ifdef CONFIG_MPRIS
+
+extern int mpris_fd;
+void mpris_init(void);
+void mpris_process(void);
+void mpris_free(void);
+void mpris_playback_status_changed(void);
+void mpris_loop_status_changed(void);
+void mpris_shuffle_changed(void);
+void mpris_volume_changed(void);
+void mpris_metadata_changed(void);
+void mpris_seeked(void);
+
+#else
+
+#define mpris_fd -1
+#define mpris_init() { }
+#define mpris_process() { }
+#define mpris_free() { }
+#define mpris_playback_status_changed() { }
+#define mpris_loop_status_changed() { }
+#define mpris_shuffle_changed() { }
+#define mpris_volume_changed() { }
+#define mpris_metadata_changed() { }
+#define mpris_seeked() { }
+
+#endif
+
+#endif
diff --git a/op.h b/op.h
new file mode 100644 (file)
index 0000000..b79bae8
--- /dev/null
+++ b/op.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_OP_H
+#define CMUS_OP_H
+
+#include "sf.h"
+#include "channelmap.h"
+
+#ifndef __GNUC__
+#include <fcntl.h>
+#endif
+
+#define OP_ABI_VERSION 1
+
+enum {
+       /* no error */
+       OP_ERROR_SUCCESS,
+       /* system error (error code in errno) */
+       OP_ERROR_ERRNO,
+       /* no such plugin */
+       OP_ERROR_NO_PLUGIN,
+       /* plugin not initialized */
+       OP_ERROR_NOT_INITIALIZED,
+       /* function not supported */
+       OP_ERROR_NOT_SUPPORTED,
+       /* mixer not open */
+       OP_ERROR_NOT_OPEN,
+       /* plugin does not support the sample format */
+       OP_ERROR_SAMPLE_FORMAT,
+       /* plugin does not have this option */
+       OP_ERROR_NOT_OPTION,
+       /*  */
+       OP_ERROR_INTERNAL
+};
+
+struct output_plugin_ops {
+       int (*init)(void);
+       int (*exit)(void);
+       int (*open)(sample_format_t sf, const channel_position_t *channel_map);
+       int (*close)(void);
+       int (*drop)(void);
+       int (*write)(const char *buffer, int count);
+       int (*buffer_space)(void);
+
+       /* these can be NULL */
+       int (*pause)(void);
+       int (*unpause)(void);
+
+};
+
+#define OPT(prefix, name) { #name, prefix ## _set_ ## name, \
+       prefix ## _get_ ## name }
+
+struct output_plugin_opt {
+       const char *name;
+       int (*set)(const char *val);
+       int (*get)(char **val);
+};
+
+/* symbols exported by plugin */
+extern const struct output_plugin_ops op_pcm_ops;
+extern const struct output_plugin_opt op_pcm_options[];
+extern const int op_priority;
+extern const unsigned op_abi_version;
+
+#endif
diff --git a/op/alsa.c b/op/alsa.c
new file mode 100644 (file)
index 0000000..7d0e60a
--- /dev/null
+++ b/op/alsa.c
@@ -0,0 +1,380 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2008 Jonathan Kleinehellefort
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * snd_pcm_state_t:
+ *
+ * Open
+ * SND_PCM_STATE_OPEN = 0,
+ *
+ * Setup installed
+ * SND_PCM_STATE_SETUP = 1,
+ *
+ * Ready to start
+ * SND_PCM_STATE_PREPARED = 2,
+ *
+ * Running
+ * SND_PCM_STATE_RUNNING = 3,
+ *
+ * Stopped: underrun (playback) or overrun (capture) detected
+ * SND_PCM_STATE_XRUN = 4,
+ *
+ * Draining: running (playback) or stopped (capture)
+ * SND_PCM_STATE_DRAINING = 5,
+ *
+ * Paused
+ * SND_PCM_STATE_PAUSED = 6,
+ *
+ * Hardware is suspended
+ * SND_PCM_STATE_SUSPENDED = 7,
+ *
+ * Hardware is disconnected
+ * SND_PCM_STATE_DISCONNECTED = 8,
+ */
+
+#include "op.h"
+#include "utils.h"
+#include "xmalloc.h"
+#include "sf.h"
+#include "debug.h"
+
+#define ALSA_PCM_NEW_HW_PARAMS_API
+#define ALSA_PCM_NEW_SW_PARAMS_API
+
+#include <alsa/asoundlib.h>
+
+static sample_format_t alsa_sf;
+static snd_pcm_t *alsa_handle;
+static snd_pcm_format_t alsa_fmt;
+static int alsa_can_pause;
+static snd_pcm_status_t *status;
+
+/* bytes (bits * channels / 8) */
+static int alsa_frame_size;
+
+/* configuration */
+static char *alsa_dsp_device = NULL;
+
+#if 0
+#define debug_ret(func, ret) \
+       d_print("%s returned %d %s\n", func, ret, ret < 0 ? snd_strerror(ret) : "")
+#else
+#define debug_ret(func, ret) do { } while (0)
+#endif
+
+static int alsa_error_to_op_error(int err)
+{
+       if (!err)
+               return OP_ERROR_SUCCESS;
+       err = -err;
+       if (err < SND_ERROR_BEGIN) {
+               errno = err;
+               return -OP_ERROR_ERRNO;
+       }
+       return -OP_ERROR_INTERNAL;
+}
+
+/* we don't want error messages to stderr */
+static void error_handler(const char *file, int line, const char *function, int err, const char *fmt, ...)
+{
+}
+
+static int op_alsa_init(void)
+{
+       int rc;
+
+       snd_lib_error_set_handler(error_handler);
+
+       if (alsa_dsp_device == NULL)
+               alsa_dsp_device = xstrdup("default");
+       rc = snd_pcm_status_malloc(&status);
+       if (rc < 0) {
+               free(alsa_dsp_device);
+               alsa_dsp_device = NULL;
+               errno = ENOMEM;
+               return -OP_ERROR_ERRNO;
+       }
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_alsa_exit(void)
+{
+       snd_pcm_status_free(status);
+       free(alsa_dsp_device);
+       alsa_dsp_device = NULL;
+       return OP_ERROR_SUCCESS;
+}
+
+/* randomize hw params */
+static int alsa_set_hw_params(void)
+{
+       snd_pcm_hw_params_t *hwparams = NULL;
+       unsigned int buffer_time_max = 300 * 1000; /* us */
+       const char *cmd;
+       unsigned int rate;
+       int rc, dir;
+
+       snd_pcm_hw_params_malloc(&hwparams);
+
+       cmd = "snd_pcm_hw_params_any";
+       rc = snd_pcm_hw_params_any(alsa_handle, hwparams);
+       if (rc < 0)
+               goto error;
+
+       cmd = "snd_pcm_hw_params_set_buffer_time_max";
+       rc = snd_pcm_hw_params_set_buffer_time_max(alsa_handle, hwparams,
+                                                  &buffer_time_max, &dir);
+       if (rc < 0)
+               goto error;
+
+       alsa_can_pause = snd_pcm_hw_params_can_pause(hwparams);
+       d_print("can pause = %d\n", alsa_can_pause);
+
+       cmd = "snd_pcm_hw_params_set_access";
+       rc = snd_pcm_hw_params_set_access(alsa_handle, hwparams,
+                       SND_PCM_ACCESS_RW_INTERLEAVED);
+       if (rc < 0)
+               goto error;
+
+       alsa_fmt = snd_pcm_build_linear_format(sf_get_bits(alsa_sf), sf_get_bits(alsa_sf),
+                       sf_get_signed(alsa_sf) ? 0 : 1,
+                       sf_get_bigendian(alsa_sf));
+       cmd = "snd_pcm_hw_params_set_format";
+       rc = snd_pcm_hw_params_set_format(alsa_handle, hwparams, alsa_fmt);
+       if (rc < 0)
+               goto error;
+
+       cmd = "snd_pcm_hw_params_set_channels";
+       rc = snd_pcm_hw_params_set_channels(alsa_handle, hwparams, sf_get_channels(alsa_sf));
+       if (rc < 0)
+               goto error;
+
+       cmd = "snd_pcm_hw_params_set_rate";
+       rate = sf_get_rate(alsa_sf);
+       dir = 0;
+       rc = snd_pcm_hw_params_set_rate_near(alsa_handle, hwparams, &rate, &dir);
+       if (rc < 0)
+               goto error;
+       d_print("rate=%d\n", rate);
+
+       cmd = "snd_pcm_hw_params";
+       rc = snd_pcm_hw_params(alsa_handle, hwparams);
+       if (rc < 0)
+               goto error;
+       goto out;
+error:
+       d_print("%s: error: %s\n", cmd, snd_strerror(rc));
+out:
+       snd_pcm_hw_params_free(hwparams);
+       return rc;
+}
+
+static int op_alsa_open(sample_format_t sf, const channel_position_t *channel_map)
+{
+       int rc;
+
+       alsa_sf = sf;
+       alsa_frame_size = sf_get_frame_size(alsa_sf);
+
+       rc = snd_pcm_open(&alsa_handle, alsa_dsp_device, SND_PCM_STREAM_PLAYBACK, 0);
+       if (rc < 0)
+               goto error;
+
+       rc = alsa_set_hw_params();
+       if (rc)
+               goto close_error;
+
+       rc = snd_pcm_prepare(alsa_handle);
+       if (rc < 0)
+               goto close_error;
+       return OP_ERROR_SUCCESS;
+close_error:
+       snd_pcm_close(alsa_handle);
+error:
+       return alsa_error_to_op_error(rc);
+}
+
+static int op_alsa_write(const char *buffer, int count);
+
+static int op_alsa_close(void)
+{
+       int rc;
+
+       rc = snd_pcm_drain(alsa_handle);
+       debug_ret("snd_pcm_drain", rc);
+
+       rc = snd_pcm_close(alsa_handle);
+       debug_ret("snd_pcm_close", rc);
+       return alsa_error_to_op_error(rc);
+}
+
+static int op_alsa_drop(void)
+{
+       int rc;
+
+       rc = snd_pcm_drop(alsa_handle);
+       debug_ret("snd_pcm_drop", rc);
+
+       rc = snd_pcm_prepare(alsa_handle);
+       debug_ret("snd_pcm_prepare", rc);
+
+       /* drop set state to SETUP
+        * prepare set state to PREPARED
+        *
+        * so if old state was PAUSED we can't UNPAUSE (see op_alsa_unpause)
+        */
+       return alsa_error_to_op_error(rc);
+}
+
+static int op_alsa_write(const char *buffer, int count)
+{
+       int rc, len;
+       int recovered = 0;
+
+       len = count / alsa_frame_size;
+again:
+       rc = snd_pcm_writei(alsa_handle, buffer, len);
+       if (rc < 0) {
+               // rc _should_ be either -EBADFD, -EPIPE or -ESTRPIPE
+               if (!recovered && (rc == -EINTR || rc == -EPIPE || rc == -ESTRPIPE)) {
+                       d_print("snd_pcm_writei failed: %s, trying to recover\n",
+                                       snd_strerror(rc));
+                       recovered++;
+                       // this handles -EINTR, -EPIPE and -ESTRPIPE
+                       // for other errors it just returns the error code
+                       rc = snd_pcm_recover(alsa_handle, rc, 1);
+                       if (!rc)
+                               goto again;
+               }
+
+               /* this handles EAGAIN too which is not critical error */
+               return alsa_error_to_op_error(rc);
+       }
+
+       rc *= alsa_frame_size;
+       return rc;
+}
+
+static int op_alsa_buffer_space(void)
+{
+       int rc;
+       snd_pcm_sframes_t f;
+
+       f = snd_pcm_avail_update(alsa_handle);
+       while (f < 0) {
+               d_print("snd_pcm_avail_update failed: %s, trying to recover\n",
+                       snd_strerror(f));
+               rc = snd_pcm_recover(alsa_handle, f, 1);
+               if (rc < 0) {
+                       d_print("recovery failed: %s\n", snd_strerror(rc));
+                       return alsa_error_to_op_error(rc);
+               }
+               f = snd_pcm_avail_update(alsa_handle);
+       }
+
+       return f * alsa_frame_size;
+}
+
+static int op_alsa_pause(void)
+{
+       int rc = 0;
+       if (alsa_can_pause) {
+               snd_pcm_state_t state = snd_pcm_state(alsa_handle);
+               if (state == SND_PCM_STATE_PREPARED) {
+                       // state is PREPARED -> no need to pause
+               } else if (state == SND_PCM_STATE_RUNNING) {
+                       // state is RUNNING - > pause
+
+                       // infinite timeout
+                       rc = snd_pcm_wait(alsa_handle, -1);
+                       debug_ret("snd_pcm_wait", rc);
+
+                       rc = snd_pcm_pause(alsa_handle, 1);
+                       debug_ret("snd_pcm_pause", rc);
+               } else {
+                       d_print("error: state is not RUNNING or PREPARED\n");
+                       rc = -OP_ERROR_INTERNAL;
+               }
+       } else {
+               rc = snd_pcm_drop(alsa_handle);
+               debug_ret("snd_pcm_drop", rc);
+       }
+       return alsa_error_to_op_error(rc);
+}
+
+static int op_alsa_unpause(void)
+{
+       int rc = 0;
+       if (alsa_can_pause) {
+               snd_pcm_state_t state = snd_pcm_state(alsa_handle);
+               if (state == SND_PCM_STATE_PREPARED) {
+                       // state is PREPARED -> no need to unpause
+               } else if (state == SND_PCM_STATE_PAUSED) {
+                       // state is PAUSED -> unpause
+
+                       // infinite timeout
+                       rc = snd_pcm_wait(alsa_handle, -1);
+                       debug_ret("snd_pcm_wait", rc);
+
+                       rc = snd_pcm_pause(alsa_handle, 0);
+                       debug_ret("snd_pcm_pause", rc);
+               } else {
+                       d_print("error: state is not PAUSED nor PREPARED\n");
+                       rc = -OP_ERROR_INTERNAL;
+               }
+       } else {
+               rc = snd_pcm_prepare(alsa_handle);
+               debug_ret("snd_pcm_prepare", rc);
+       }
+       return alsa_error_to_op_error(rc);
+}
+
+static int op_alsa_set_device(const char *val)
+{
+       free(alsa_dsp_device);
+       alsa_dsp_device = xstrdup(val);
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_alsa_get_device(char **val)
+{
+       if (alsa_dsp_device)
+               *val = xstrdup(alsa_dsp_device);
+       return OP_ERROR_SUCCESS;
+}
+
+const struct output_plugin_ops op_pcm_ops = {
+       .init = op_alsa_init,
+       .exit = op_alsa_exit,
+       .open = op_alsa_open,
+       .close = op_alsa_close,
+       .drop = op_alsa_drop,
+       .write = op_alsa_write,
+       .buffer_space = op_alsa_buffer_space,
+       .pause = op_alsa_pause,
+       .unpause = op_alsa_unpause,
+};
+
+const struct output_plugin_opt op_pcm_options[] = {
+       OPT(op_alsa, device),
+       { NULL },
+};
+
+const int op_priority = 0;
+const unsigned op_abi_version = OP_ABI_VERSION;
diff --git a/op/ao.c b/op/ao.c
new file mode 100644 (file)
index 0000000..2187b87
--- /dev/null
+++ b/op/ao.c
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 Johannes Weißl
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "op.h"
+#include "xmalloc.h"
+#include "utils.h"
+#include "misc.h"
+#include "debug.h"
+
+/*
+ * <ao/ao.h> uses FILE but doesn't include stdio.h.
+ * Also we use snprintf().
+ */
+#include <stdio.h>
+#include <strings.h>
+#include <ao/ao.h>
+
+#ifdef AO_EBADFORMAT
+#define AO_API_1
+#endif
+
+static ao_device *libao_device;
+static char *wav_dir = NULL;
+static int wav_counter = 1;
+static int is_wav = 0;
+
+/* configuration */
+static char *libao_driver = NULL;
+static int libao_buffer_space = 16384;
+static int libao_cur_buffer_space = 0;
+static char *libao_device_interface = NULL;
+
+
+static int op_ao_init(void)
+{
+       /* ignore config value */
+       wav_counter = 1;
+
+       ao_initialize();
+       return 0;
+}
+
+static int op_ao_exit(void)
+{
+       free(libao_driver);
+       ao_shutdown();
+       return 0;
+}
+
+/* http://www.xiph.org/ao/doc/ao_sample_format.html */
+static const struct {
+       channel_position_t pos;
+       const char *str;
+} ao_channel_mapping[] = {
+       { CHANNEL_POSITION_LEFT,                        "L" },
+       { CHANNEL_POSITION_RIGHT,                       "R" },
+       { CHANNEL_POSITION_CENTER,                      "C" },
+       { CHANNEL_POSITION_MONO,                        "M" },
+       { CHANNEL_POSITION_FRONT_LEFT_OF_CENTER,        "CL" },
+       { CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER,       "CR" },
+       { CHANNEL_POSITION_REAR_LEFT,                   "BL" },
+       { CHANNEL_POSITION_REAR_RIGHT,                  "BR" },
+       { CHANNEL_POSITION_REAR_CENTER,                 "BC" },
+       { CHANNEL_POSITION_SIDE_LEFT,                   "SL" },
+       { CHANNEL_POSITION_SIDE_RIGHT,                  "SR" },
+       { CHANNEL_POSITION_LFE,                         "LFE" },
+       { CHANNEL_POSITION_INVALID,                     "X" },
+};
+
+#ifdef AO_API_1
+static char *ao_channel_matrix(int channels, const channel_position_t *map)
+{
+       int i, j;
+       char buf[256] = "";
+
+       if (!map || !channel_map_valid(map))
+               return NULL;
+
+       for (i = 0; i < channels; i++) {
+               const channel_position_t pos = map[i];
+               int found = 0;
+               for (j = 0; j < N_ELEMENTS(ao_channel_mapping); j++) {
+                       if (pos == ao_channel_mapping[j].pos) {
+                               strcat(buf, ao_channel_mapping[j].str);
+                               strcat(buf, ",");
+                               found = 1;
+                               break;
+                       }
+               }
+               if (!found)
+                       strcat(buf, "M,");
+       }
+       buf[strlen(buf)-1] = '\0';
+
+       return xstrdup(buf);
+}
+#endif
+
+static int op_ao_open(sample_format_t sf, const channel_position_t *channel_map)
+{
+       ao_sample_format format = {
+               .bits        = sf_get_bits(sf),
+               .rate        = sf_get_rate(sf),
+               .channels    = sf_get_channels(sf),
+               .byte_format = sf_get_bigendian(sf) ? AO_FMT_BIG : AO_FMT_LITTLE,
+#ifdef AO_API_1
+               .matrix      = ao_channel_matrix(sf_get_channels(sf), channel_map)
+#endif
+       };
+       int driver;
+
+       if (libao_driver == NULL) {
+               driver = ao_default_driver_id();
+       } else {
+               driver = ao_driver_id(libao_driver);
+               is_wav = strcasecmp(libao_driver, "wav") == 0;
+       }
+       if (driver == -1) {
+               errno = ENODEV;
+               return -OP_ERROR_ERRNO;
+       }
+
+       if (is_wav) {
+               char file[512];
+
+               if (wav_dir == NULL)
+                       wav_dir = xstrdup(home_dir);
+               snprintf(file, sizeof(file), "%s/%02d.wav", wav_dir, wav_counter);
+               libao_device = ao_open_file(driver, file, 0, &format, NULL);
+       } else {
+               ao_option *ao_options = NULL;
+               if (libao_device_interface) {
+                       ao_append_option(&ao_options, "dev", libao_device_interface);
+               }
+               libao_device = ao_open_live(driver, &format, ao_options);
+       }
+
+       if (libao_device == NULL) {
+               switch (errno) {
+               case AO_ENODRIVER:
+               case AO_ENOTFILE:
+               case AO_ENOTLIVE:
+               case AO_EOPENDEVICE:
+                       errno = ENODEV;
+                       return -OP_ERROR_ERRNO;
+               case AO_EBADOPTION:
+                       errno = EINVAL;
+                       return -OP_ERROR_ERRNO;
+               case AO_EOPENFILE:
+                       errno = EACCES;
+                       return -OP_ERROR_ERRNO;
+               case AO_EFILEEXISTS:
+                       errno = EEXIST;
+                       return -OP_ERROR_ERRNO;
+               case AO_EFAIL:
+               default:
+                       return -OP_ERROR_INTERNAL;
+               }
+       }
+
+       /* ensure that the buffer size is a multiple of the frame size */
+       libao_cur_buffer_space = is_wav ? 128 * 1024 : libao_buffer_space;
+       libao_cur_buffer_space -= libao_cur_buffer_space % sf_get_frame_size(sf);
+
+#ifdef AO_API_1
+       d_print("channel matrix: %s\n", format.matrix ? format.matrix : "default");
+#endif
+       return 0;
+}
+
+static int op_ao_close(void)
+{
+       ao_close(libao_device);
+       if (is_wav)
+               wav_counter++;
+       return 0;
+}
+
+static int op_ao_write(const char *buffer, int count)
+{
+       if (ao_play(libao_device, (void *)buffer, count) == 0)
+               return -1;
+       return count;
+}
+
+static int op_ao_buffer_space(void)
+{
+       return libao_cur_buffer_space;
+}
+
+static int op_ao_set_buffer_size(const char *val)
+{
+       long int ival;
+       if (str_to_int(val, &ival) || ival < 4096) {
+               errno = EINVAL;
+               return -OP_ERROR_ERRNO;
+       }
+       libao_buffer_space = ival;
+       return 0;
+}
+
+static int op_ao_get_buffer_size(char **val)
+{
+       *val = xnew(char, 22);
+       snprintf(*val, 22, "%d", libao_buffer_space);
+       return 0;
+}
+
+static int op_ao_set_driver(const char *val)
+{
+       free(libao_driver);
+       libao_driver = NULL;
+       if (val[0])
+               libao_driver = xstrdup(val);
+       return 0;
+}
+
+static int op_ao_get_driver(char **val)
+{
+       if (libao_driver)
+               *val = xstrdup(libao_driver);
+       return 0;
+}
+
+static int op_ao_set_wav_counter(const char *val)
+{
+       long int ival;
+       if (str_to_int(val, &ival)) {
+               errno = EINVAL;
+               return -OP_ERROR_ERRNO;
+       }
+       wav_counter = ival;
+       return 0;
+}
+
+static int op_ao_get_wav_counter(char **val)
+{
+       *val = xnew(char, 22);
+       snprintf(*val, 22, "%d", wav_counter);
+       return 0;
+}
+
+static int op_ao_set_wav_dir(const char *val)
+{
+       free(wav_dir);
+       wav_dir = xstrdup(val);
+       return 0;
+}
+
+static int op_ao_get_wav_dir(char **val)
+{
+       if (wav_dir == NULL)
+               wav_dir = xstrdup(home_dir);
+       *val = expand_filename(wav_dir);
+       return 0;
+}
+
+static int op_ao_set_device_interface(const char *val)
+{
+       free(libao_device_interface);
+       libao_device_interface = NULL;
+       if (val[0])
+               libao_device_interface = xstrdup(val);
+       return 0;
+}
+
+static int op_ao_get_device_interface(char **val)
+{
+       if (libao_device_interface)
+               *val = xstrdup(libao_device_interface);
+       return 0;
+}
+
+const struct output_plugin_ops op_pcm_ops = {
+       .init = op_ao_init,
+       .exit = op_ao_exit,
+       .open = op_ao_open,
+       .close = op_ao_close,
+       .write = op_ao_write,
+       .buffer_space = op_ao_buffer_space,
+};
+
+const struct output_plugin_opt op_pcm_options[] = {
+       OPT(op_ao, buffer_size),
+       OPT(op_ao, driver),
+       OPT(op_ao, wav_counter),
+       OPT(op_ao, wav_dir),
+       OPT(op_ao, device_interface),
+       { NULL },
+};
+
+const int op_priority = 3;
+const unsigned op_abi_version = OP_ABI_VERSION;
diff --git a/op/arts.c b/op/arts.c
new file mode 100644 (file)
index 0000000..d8eb693
--- /dev/null
+++ b/op/arts.c
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "op.h"
+#include "xmalloc.h"
+#include "debug.h"
+
+#include <artsc.h>
+
+static arts_stream_t arts_stream;
+static sample_format_t arts_sf;
+static int arts_buffer_size;
+
+static int op_arts_init(void)
+{
+       int rc;
+
+       rc = arts_init();
+       if (rc < 0) {
+               return -1;
+       }
+       return 0;
+}
+
+static int op_arts_exit(void)
+{
+       arts_free();
+       return 0;
+}
+
+static int op_arts_open(sample_format_t sf, const channel_position_t *channel_map)
+{
+       int buffer_time, server_latency, total_latency;
+       int blocking;
+
+       arts_sf = sf;
+       arts_stream = arts_play_stream(sf_get_rate(arts_sf), sf_get_bits(arts_sf),
+                       sf_get_channels(arts_sf), "cmus");
+       blocking = arts_stream_set(arts_stream, ARTS_P_BLOCKING, 0);
+       if (blocking) {
+       }
+       arts_buffer_size = arts_stream_get(arts_stream, ARTS_P_BUFFER_SIZE);
+       if (arts_buffer_size < 0) {
+       }
+       buffer_time = arts_stream_get(arts_stream, ARTS_P_BUFFER_TIME);
+       server_latency = arts_stream_get(arts_stream, ARTS_P_SERVER_LATENCY);
+       total_latency = arts_stream_get(arts_stream, ARTS_P_TOTAL_LATENCY);
+       d_print("buffer_time: %d\n", buffer_time);
+       d_print("server_latency: %d\n", server_latency);
+       d_print("total_latency: %d\n", total_latency);
+       return 0;
+}
+
+static int op_arts_close(void)
+{
+       arts_close_stream(arts_stream);
+       return 0;
+}
+
+static int op_arts_write(const char *buffer, int count)
+{
+       int rc;
+
+       rc = arts_write(arts_stream, buffer, count);
+       if (rc < 0) {
+               d_print("rc = %d, count = %d\n", rc, count);
+               return -1;
+       }
+       return rc;
+}
+
+static int op_arts_pause(void)
+{
+       return 0;
+}
+
+static int op_arts_unpause(void)
+{
+       return 0;
+}
+
+static int op_arts_buffer_space(void)
+{
+       int space;
+
+       space = arts_stream_get(arts_stream, ARTS_P_BUFFER_SPACE);
+       if (space < 0)
+               return -1;
+       return space;
+}
+
+const struct output_plugin_ops op_pcm_ops = {
+       .init = op_arts_init,
+       .exit = op_arts_exit,
+       .open = op_arts_open,
+       .close = op_arts_close,
+       .write = op_arts_write,
+       .pause = op_arts_pause,
+       .unpause = op_arts_unpause,
+       .buffer_space = op_arts_buffer_space,
+};
+
+const struct output_plugin_opt op_pcm_options[] = {
+       { NULL },
+};
+
+const int op_priority = 4;
+const unsigned op_abi_version = OP_ABI_VERSION;
diff --git a/op/coreaudio.c b/op/coreaudio.c
new file mode 100644 (file)
index 0000000..f968eed
--- /dev/null
@@ -0,0 +1,976 @@
+/*
+ * Copyright (C) 2015 Yue Wang <yuleopen@gmail.com>
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <sys/types.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdatomic.h>
+#include <AudioUnit/AudioUnit.h>
+#include <CoreAudio/CoreAudio.h>
+
+#include "debug.h"
+#include "op.h"
+#include "mixer.h"
+#include "sf.h"
+#include "utils.h"
+#include "xmalloc.h"
+
+
+/////////////////////////////////////////////////////////////////////////////
+// Ring buffer utility from the PortAudio project.
+// Original licence information is listed below.
+
+/*
+ * Portable Audio I/O Library
+ * Ring Buffer utility.
+ *
+ * Author: Phil Burk, http://www.softsynth.com
+ * modified for SMP safety on Mac OS X by Bjorn Roche
+ * modified for SMP safety on Linux by Leland Lucius
+ * also, allowed for const where possible
+ * Note that this is safe only for a single-thread reader and a
+ * single-thread writer.
+ *
+ * This program uses the PortAudio Portable Audio Library.
+ * For more information see: http://www.portaudio.com
+ * Copyright (c) 1999-2000 Ross Bencina and Phil Burk
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files
+ * (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge,
+ * publish, distribute, sublicense, and/or sell copies of the Software,
+ * and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * The text above constitutes the entire PortAudio license; however, 
+ * the PortAudio community also makes the following non-binding requests:
+ *
+ * Any person wishing to distribute modifications to the Software is
+ * requested to send the modifications to the original developer so that
+ * they can be incorporated into the canonical version. It is also 
+ * requested that these non-binding requests be included along with the 
+ * license above.
+ */
+
+typedef struct coreaudio_ring_buffer_t {
+       size_t buffer_size;
+       size_t write_index;
+       size_t read_index;
+       size_t big_mask;
+       size_t small_mask;
+       char *buffer;
+} coreaudio_ring_buffer_t;
+
+static int coreaudio_ring_buffer_init(coreaudio_ring_buffer_t *rbuf, size_t num_of_bytes);
+static void coreaudio_ring_buffer_destroy(coreaudio_ring_buffer_t *rbuf);
+static void coreaudio_ring_buffer_flush(coreaudio_ring_buffer_t *rbuf);
+static size_t coreaudio_ring_buffer_write_available(coreaudio_ring_buffer_t *rbuf);
+static size_t coreaudio_ring_buffer_read_available(coreaudio_ring_buffer_t *rbuf);
+static size_t coreaudio_ring_buffer_write(coreaudio_ring_buffer_t *rbuf, const char *data, size_t num_of_bytes);
+static size_t coreaudio_ring_buffer_read(coreaudio_ring_buffer_t *rbuf, char *data, size_t num_of_bytes);
+static size_t coreaudio_ring_buffer_write_regions(coreaudio_ring_buffer_t *rbuf, size_t num_of_bytes, char **data_ptr1, size_t *size_ptr1, char **data_ptr2, size_t *size_ptr2);
+static size_t coreaudio_ring_buffer_advance_write_index(coreaudio_ring_buffer_t *rbuf, size_t num_of_bytes);
+static size_t coreaudio_ring_buffer_read_regions(coreaudio_ring_buffer_t *rbuf, size_t num_of_bytes, char **data_ptr1, size_t *size_ptr1, char **data_ptr2, size_t *size_ptr2);
+static size_t coreaudio_ring_buffer_advance_read_index(coreaudio_ring_buffer_t *rbuf, size_t num_of_bytes);
+
+static int coreaudio_ring_buffer_init(coreaudio_ring_buffer_t *rbuf, size_t num_of_bytes)
+{
+       if (((num_of_bytes - 1) & num_of_bytes) != 0)
+               return -1;                              /*Not Power of two. */
+       rbuf->buffer_size = num_of_bytes;
+       rbuf->buffer = (char *)malloc(num_of_bytes);
+       coreaudio_ring_buffer_flush(rbuf);
+       rbuf->big_mask = (num_of_bytes *2) - 1;
+       rbuf->small_mask = (num_of_bytes) - 1;
+       return 0;
+}
+
+static void coreaudio_ring_buffer_destroy(coreaudio_ring_buffer_t *rbuf)
+{
+       if (rbuf->buffer)
+               free(rbuf->buffer);
+       rbuf->buffer = NULL;
+       rbuf->buffer_size = 0;
+       rbuf->write_index = 0;
+       rbuf->read_index = 0;
+       rbuf->big_mask = 0;
+       rbuf->small_mask = 0;
+}
+
+static size_t coreaudio_ring_buffer_read_available(coreaudio_ring_buffer_t *rbuf)
+{
+       atomic_thread_fence(memory_order_seq_cst);
+       return ((rbuf->write_index - rbuf->read_index) & rbuf->big_mask);
+}
+
+static size_t coreaudio_ring_buffer_write_available(coreaudio_ring_buffer_t *rbuf)
+{
+       return (rbuf->buffer_size - coreaudio_ring_buffer_read_available(rbuf));
+}
+
+static void coreaudio_ring_buffer_flush(coreaudio_ring_buffer_t *rbuf)
+{
+       rbuf->write_index = rbuf->read_index = 0;
+}
+
+static size_t coreaudio_ring_buffer_write_regions(coreaudio_ring_buffer_t *rbuf, size_t num_of_bytes, char **data_ptr1, size_t *size_ptr1, char **data_ptr2, size_t *size_ptr2)
+{
+       size_t index;
+       size_t available = coreaudio_ring_buffer_write_available(rbuf);
+       if (num_of_bytes > available)
+               num_of_bytes = available;
+       index = rbuf->write_index & rbuf->small_mask;
+       if ((index + num_of_bytes) > rbuf->buffer_size) {
+               size_t first_half = rbuf->buffer_size - index;
+               *data_ptr1 = &rbuf->buffer[index];
+               *size_ptr1 = first_half;
+               *data_ptr2 = &rbuf->buffer[0];
+               *size_ptr2 = num_of_bytes - first_half;
+       } else {
+               *data_ptr1 = &rbuf->buffer[index];
+               *size_ptr1 = num_of_bytes;
+               *data_ptr2 = NULL;
+               *size_ptr2 = 0;
+       }
+       return num_of_bytes;
+}
+
+static size_t coreaudio_ring_buffer_advance_write_index(coreaudio_ring_buffer_t *rbuf, size_t num_of_bytes)
+{
+       atomic_thread_fence(memory_order_seq_cst);
+       return rbuf->write_index = (rbuf->write_index + num_of_bytes) & rbuf->big_mask;
+}
+
+static size_t coreaudio_ring_buffer_read_regions(coreaudio_ring_buffer_t *rbuf, size_t num_of_bytes, char **data_ptr1, size_t *size_ptr1, char **data_ptr2, size_t *size_ptr2)
+{
+       size_t index;
+       size_t available = coreaudio_ring_buffer_read_available(rbuf);
+       if (num_of_bytes > available)
+               num_of_bytes = available;
+       index = rbuf->read_index & rbuf->small_mask;
+       if ((index + num_of_bytes) > rbuf->buffer_size) {
+               size_t first_half = rbuf->buffer_size - index;
+               *data_ptr1 = &rbuf->buffer[index];
+               *size_ptr1 = first_half;
+               *data_ptr2 = &rbuf->buffer[0];
+               *size_ptr2 = num_of_bytes - first_half;
+       } else {
+               *data_ptr1 = &rbuf->buffer[index];
+               *size_ptr1 = num_of_bytes;
+               *data_ptr2 = NULL;
+               *size_ptr2 = 0;
+       }
+       return num_of_bytes;
+}
+
+static size_t coreaudio_ring_buffer_advance_read_index(coreaudio_ring_buffer_t *rbuf, size_t num_of_bytes)
+{
+       atomic_thread_fence(memory_order_seq_cst);
+       return rbuf->read_index = (rbuf->read_index + num_of_bytes) & rbuf->big_mask;
+}
+
+static size_t coreaudio_ring_buffer_write(coreaudio_ring_buffer_t *rbuf, const char *data, size_t num_of_bytes)
+{
+       size_t size1, size2, num_write;
+       char *data1, *data2;
+       num_write = coreaudio_ring_buffer_write_regions(rbuf, num_of_bytes, &data1, &size1, &data2, &size2);
+       if (size2 > 0) {
+               memcpy(data1, data, size1);
+               data = ((char *) data) + size1;
+               memcpy(data2, data, size2);
+       } else {
+               memcpy(data1, data, size1);
+       }
+       coreaudio_ring_buffer_advance_write_index(rbuf, num_write);
+       return num_write;
+}
+
+static size_t coreaudio_ring_buffer_read(coreaudio_ring_buffer_t *rbuf, char *data, size_t num_of_bytes)
+{
+       size_t size1, size2, num_read;
+       char *data1, *data2;
+       num_read = coreaudio_ring_buffer_read_regions(rbuf, num_of_bytes, &data1, &size1, &data2, &size2);
+       if (size2 > 0) {
+               memcpy(data, data1, size1);
+               data = ((char *) data) + size1;
+               memcpy(data, data2, size2);
+       } else {
+               memcpy(data, data1, size1);
+       }
+       coreaudio_ring_buffer_advance_read_index(rbuf, num_read);
+       return num_read;
+}
+
+// End of ring buffer utility from the PortAudio project.
+/////////////////////////////////////////////////////////////////////////////
+
+static char *coreaudio_opt_device_name     = NULL;
+static bool  coreaudio_opt_enable_hog_mode = false;
+static bool  coreaudio_opt_sync_rate       = false;
+
+static int coreaudio_max_volume = 100;
+static AudioDeviceID coreaudio_device_id = kAudioDeviceUnknown;
+static AudioStreamBasicDescription coreaudio_format_description;
+static AudioUnit coreaudio_audio_unit = NULL;
+static UInt32 coreaudio_buffer_frame_size = 1;
+static coreaudio_ring_buffer_t coreaudio_ring_buffer = {0, 0, 0, 0, 0, NULL};
+static UInt32 coreaudio_stero_channels[2];
+static int coreaudio_mixer_pipe_in = 0;
+static int coreaudio_mixer_pipe_out = 0;
+
+static OSStatus coreaudio_device_volume_change_listener(AudioObjectID inObjectID,
+                                                       UInt32 inNumberAddresses,
+                                                       const AudioObjectPropertyAddress inAddresses[],
+                                                       void *inClientData)
+{
+       notify_via_pipe(coreaudio_mixer_pipe_in);
+       return noErr;
+}
+
+static OSStatus coreaudio_play_callback(void *user_data,
+                                       AudioUnitRenderActionFlags *flags,
+                                       const AudioTimeStamp *ts,
+                                       UInt32 busnum,
+                                       UInt32 nframes,
+                                       AudioBufferList *buflist)
+{
+       int count = nframes * coreaudio_format_description.mBytesPerFrame;
+       buflist->mBuffers[0].mDataByteSize = coreaudio_ring_buffer_read(&coreaudio_ring_buffer,
+                                                                       buflist->mBuffers[0].mData,
+                                                                       count);
+       return noErr;
+}
+
+static AudioDeviceID coreaudio_get_default_device()
+{
+       AudioObjectPropertyAddress aopa = {
+               kAudioHardwarePropertyDefaultOutputDevice,
+               kAudioObjectPropertyScopeOutput,
+               kAudioObjectPropertyElementMaster
+       };
+
+       AudioDeviceID dev_id = kAudioDeviceUnknown;
+       UInt32 dev_id_size = sizeof(dev_id);
+       AudioObjectGetPropertyData(kAudioObjectSystemObject,
+                                  &aopa,
+                                  0,
+                                  NULL,
+                                  &dev_id_size,
+                                  &dev_id);
+       return dev_id;
+}
+
+static AudioDeviceID coreaudio_find_device(const char *dev_name)
+{
+       AudioObjectPropertyAddress aopa = {
+               kAudioHardwarePropertyDevices,
+               kAudioObjectPropertyScopeOutput,
+               kAudioObjectPropertyElementMaster
+       };
+
+       UInt32 property_size = 0;
+       OSStatus err = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject,
+                                                     &aopa,
+                                                     0,
+                                                     NULL,
+                                                     &property_size);
+       if (err != noErr)
+               return kAudioDeviceUnknown;
+
+       aopa.mSelector = kAudioHardwarePropertyDevices;
+       int device_count = property_size / sizeof(AudioDeviceID);
+       AudioDeviceID devices[device_count];
+       property_size = sizeof(devices);
+
+       err = AudioObjectGetPropertyData(kAudioObjectSystemObject,
+                                        &aopa,
+                                        0,
+                                        NULL, 
+                                        &property_size,
+                                        devices);
+       if (err != noErr)
+               return kAudioDeviceUnknown;
+
+       aopa.mSelector = kAudioDevicePropertyDeviceName;
+       for (int i = 0; i < device_count; i++) {
+               char name[256] = {0};
+               property_size = sizeof(name);
+               err = AudioObjectGetPropertyData(devices[i],
+                                                &aopa,
+                                                0,
+                                                NULL,
+                                                &property_size,
+                                                name);
+               if (err == noErr && strcmp(name, dev_name) == 0) {
+                       return devices[i];
+               }
+       }
+
+       return kAudioDeviceUnknown;
+}
+
+static const struct {
+       channel_position_t pos;
+       const AudioChannelLabel label;
+} coreaudio_channel_mapping[] = {
+       { CHANNEL_POSITION_LEFT,                        kAudioChannelLabel_Left },
+       { CHANNEL_POSITION_RIGHT,                       kAudioChannelLabel_Right },
+       { CHANNEL_POSITION_CENTER,                      kAudioChannelLabel_Center },
+       { CHANNEL_POSITION_LFE,                         kAudioChannelLabel_LFEScreen },
+       { CHANNEL_POSITION_SIDE_LEFT,                   kAudioChannelLabel_LeftSurround },
+       { CHANNEL_POSITION_SIDE_RIGHT,                  kAudioChannelLabel_RightSurround },
+       { CHANNEL_POSITION_MONO,                        kAudioChannelLabel_Mono },
+       { CHANNEL_POSITION_FRONT_LEFT_OF_CENTER,        kAudioChannelLabel_LeftCenter },
+       { CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER,       kAudioChannelLabel_RightCenter },
+       { CHANNEL_POSITION_REAR_LEFT,                   kAudioChannelLabel_LeftSurroundDirect },
+       { CHANNEL_POSITION_REAR_RIGHT,                  kAudioChannelLabel_RightSurroundDirect },
+       { CHANNEL_POSITION_REAR_CENTER,                 kAudioChannelLabel_CenterSurround },
+       { CHANNEL_POSITION_INVALID,                     kAudioChannelLabel_Unknown },
+};
+
+static void coreaudio_set_channel_position(AudioDeviceID dev_id,
+                                           int channels,
+                                           const channel_position_t *map)
+{
+       AudioObjectPropertyAddress aopa = {
+               kAudioDevicePropertyPreferredChannelLayout,
+               kAudioObjectPropertyScopeOutput,
+               kAudioObjectPropertyElementMaster
+       };
+       AudioChannelLayout *layout = NULL;
+       size_t layout_size = (size_t) &layout->mChannelDescriptions[channels];
+       layout = (AudioChannelLayout*)malloc(layout_size);
+       layout->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions ;
+       layout->mChannelBitmap = 0;
+       layout->mNumberChannelDescriptions = channels;
+       AudioChannelDescription *descriptions = layout->mChannelDescriptions;
+       for (int i = 0; i < channels; i++) {
+               const channel_position_t pos = map[i];
+               AudioChannelLabel label = kAudioChannelLabel_Mono;
+               for (int j = 0; j < N_ELEMENTS(coreaudio_channel_mapping); j++) {
+                       if (pos == coreaudio_channel_mapping[j].pos) {
+                               label = coreaudio_channel_mapping[j].label;
+                               break;
+                       }
+               }
+               descriptions[channels - 1 - i].mChannelLabel = label;
+               descriptions[i].mChannelFlags = kAudioChannelFlags_AllOff;
+               descriptions[i].mCoordinates[0] = 0;
+               descriptions[i].mCoordinates[1] = 0;
+               descriptions[i].mCoordinates[2] = 0;
+       }
+       OSStatus err =
+               AudioObjectSetPropertyData(dev_id,
+                               &aopa,
+                        0, NULL, layout_size, layout);
+       if (err != noErr)
+               d_print("Cannot set the channel layout successfully.\n");
+       free(layout);
+}
+
+
+static AudioStreamBasicDescription coreaudio_fill_format_description(sample_format_t sf)
+{
+       AudioStreamBasicDescription desc = {
+               .mSampleRate       = (Float64)sf_get_rate(sf),
+               .mFormatID         = kAudioFormatLinearPCM,
+               .mFormatFlags      = kAudioFormatFlagIsPacked,
+               .mBytesPerPacket   = sf_get_frame_size(sf),
+               .mFramesPerPacket  = 1,
+               .mChannelsPerFrame = sf_get_channels(sf),
+               .mBitsPerChannel   = sf_get_bits(sf),
+               .mBytesPerFrame    = sf_get_frame_size(sf),
+       };
+
+       d_print("Bits:%d\n", sf_get_bits(sf));
+       if (sf_get_bigendian(sf))
+               desc.mFormatFlags |= kAudioFormatFlagIsBigEndian;
+       if (sf_get_signed(sf))
+               desc.mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger;
+
+       return desc;
+}
+
+static void coreaudio_sync_device_sample_rate(AudioDeviceID dev_id, AudioStreamBasicDescription desc)
+{
+       AudioObjectPropertyAddress aopa = {
+               kAudioDevicePropertyAvailableNominalSampleRates,
+               kAudioObjectPropertyScopeOutput,
+               kAudioObjectPropertyElementMaster
+       };
+
+       UInt32 property_size;
+       OSStatus err = AudioObjectGetPropertyDataSize(dev_id,
+                                                     &aopa,
+                                                     0,
+                                                     NULL,
+                                                     &property_size);
+
+       int count = property_size/sizeof(AudioValueRange);
+       AudioValueRange ranges[count];
+       property_size = sizeof(ranges);
+       err = AudioObjectGetPropertyData(dev_id,
+                                        &aopa,
+                                        0,
+                                        NULL,
+                                        &property_size,
+                                        &ranges);
+       // Get the maximum sample rate as fallback.
+       Float64 sample_rate = .0;
+       for (int i = 0; i < count; i++) {
+               if (ranges[i].mMaximum > sample_rate)
+                       sample_rate = ranges[i].mMaximum;
+       }
+
+       // Now try to see if the device support our format sample rate.
+       // For some high quality media samples, the frame rate may exceed
+       // device capability. In this case, we let CoreAudio downsample
+       // by decimation with an integer factor ranging from 1 to 4.
+       for (int f = 4; f > 0; f--) {
+               Float64 rate = desc.mSampleRate / f;
+               for (int i = 0; i < count; i++) {
+                       if (ranges[i].mMinimum <= rate
+                          && rate <= ranges[i].mMaximum) {
+                               sample_rate = rate;
+                               break;
+                       }
+               }
+       }
+
+       aopa.mSelector = kAudioDevicePropertyNominalSampleRate,
+
+       err = AudioObjectSetPropertyData(dev_id,
+                                        &aopa,
+                                        0,
+                                        NULL,
+                                        sizeof(&desc.mSampleRate),
+                                        &sample_rate);
+       if (err != noErr)
+               d_print("Failed to synchronize the sample rate: %d\n", err);
+}
+
+static void coreaudio_hog_device(AudioDeviceID dev_id, bool hog)
+{
+       pid_t hog_pid;
+       AudioObjectPropertyAddress aopa = {
+               kAudioDevicePropertyHogMode,
+               kAudioObjectPropertyScopeOutput,
+               kAudioObjectPropertyElementMaster
+       };
+       UInt32 size = sizeof(hog_pid);
+       OSStatus err = AudioObjectGetPropertyData(dev_id,
+                                                 &aopa,
+                                                 0,
+                                                 NULL,
+                                                 &size,
+                                                 &hog_pid);
+       if (err != noErr) {
+               d_print("Cannot get hog information: %d\n", err);
+               return;
+       }
+       if (hog) {
+               if (hog_pid != -1) {
+                       d_print("Device is already hogged.\n");
+                       return;
+               }
+       } else {
+               if (hog_pid != getpid()) {
+                       d_print("Device is not owned by this process.\n");
+                       return;
+               }
+       }
+       hog_pid = hog ? getpid() : -1;
+       size = sizeof(hog_pid);
+       err = AudioObjectSetPropertyData(dev_id,
+                                        &aopa,
+                                        0,
+                                        NULL,
+                                        size,
+                                        &hog_pid);
+       if (err != noErr)
+               d_print("Cannot hog the device: %d\n", err);
+}
+
+static OSStatus coreaudio_set_buffer_size(AudioUnit au, AudioStreamBasicDescription desc, int *frame_size)
+{
+       AudioValueRange value_range = {0, 0};
+       UInt32 property_size = sizeof(AudioValueRange);
+       OSStatus err = AudioUnitGetProperty(au,
+                                           kAudioDevicePropertyBufferFrameSizeRange,
+                                           kAudioUnitScope_Global,
+                                           0,
+                                           &value_range,
+                                           &property_size);
+       if (err != noErr)
+               return err;
+
+       UInt32 buffer_frame_size = value_range.mMaximum;
+       err = AudioUnitSetProperty(au,
+                                  kAudioDevicePropertyBufferFrameSize,
+                                  kAudioUnitScope_Global,
+                                  0,
+                                  &buffer_frame_size,
+                                  sizeof(buffer_frame_size));
+       if (err != noErr)
+               d_print("Failed to set maximum buffer size: %d\n", err);
+
+       property_size = sizeof(buffer_frame_size);
+       err = AudioUnitGetProperty(au,
+                                  kAudioDevicePropertyBufferFrameSize,
+                                  kAudioUnitScope_Global,
+                                  0,
+                                  &buffer_frame_size,
+                                  &property_size);
+       if (err != noErr) {
+               d_print("Cannot get the buffer frame size: %d\n", err);
+               return err;
+       }
+
+       buffer_frame_size *= desc.mBytesPerFrame;
+
+       // We set the frame size to a power of two integer that
+       // is larger than buffer_frame_size.
+       while (*frame_size < buffer_frame_size + 1) {
+               *frame_size <<= 1;
+       }
+
+       return noErr;
+}
+
+static OSStatus coreaudio_init_audio_unit(AudioUnit *au,
+                                         OSType os_type,
+                                         AudioDeviceID dev_id)
+{
+       OSStatus err;   
+       AudioComponentDescription comp_desc = {
+               kAudioUnitType_Output,
+               os_type,
+               kAudioUnitManufacturer_Apple,
+               0,
+               0
+       };
+
+       AudioComponent comp = AudioComponentFindNext(0, &comp_desc);
+       if (!comp) {
+               return -1;
+       }
+
+       err = AudioComponentInstanceNew(comp, au);
+       if (err != noErr)
+               return err;
+
+       if (os_type == kAudioUnitSubType_HALOutput) {
+               err = AudioUnitSetProperty(*au,
+                                          kAudioOutputUnitProperty_CurrentDevice,
+                                          kAudioUnitScope_Global,
+                                          0,
+                                          &dev_id,
+                                          sizeof(dev_id));
+               if (err != noErr)
+                       return err;
+       }
+
+       return err;
+}
+
+static OSStatus coreaudio_start_audio_unit(AudioUnit *au,
+                                          int *frame_size,
+                                          AudioStreamBasicDescription desc)
+{
+       
+       OSStatus err;
+       err = AudioUnitSetProperty(*au,
+                                  kAudioUnitProperty_StreamFormat,
+                                  kAudioUnitScope_Input,
+                                  0,
+                                  &desc,
+                                  sizeof(desc));
+       if (err != noErr)
+               return err;
+
+       AURenderCallbackStruct cb = {
+               .inputProc = coreaudio_play_callback,
+               .inputProcRefCon = NULL,
+       };
+       err = AudioUnitSetProperty(*au,
+                                  kAudioUnitProperty_SetRenderCallback,
+                                  kAudioUnitScope_Input,
+                                  0,
+                                  &cb,
+                                  sizeof(cb));
+       if (err != noErr)
+               return err;
+
+       err = AudioUnitInitialize(*au);
+       if (err != noErr)
+               return err;
+
+       err = coreaudio_set_buffer_size(*au, desc, frame_size);
+       if (err != noErr)
+               return err;
+
+       return AudioOutputUnitStart(*au);
+}
+
+static int coreaudio_init(void)
+{
+       AudioDeviceID default_dev_id = coreaudio_get_default_device();
+       if (default_dev_id == kAudioDeviceUnknown) {
+               errno = ENODEV;
+               return -OP_ERROR_ERRNO;
+       }
+
+       AudioDeviceID named_dev_id = kAudioDeviceUnknown;
+       if (coreaudio_opt_device_name)
+               named_dev_id = coreaudio_find_device(coreaudio_opt_device_name);
+
+       coreaudio_device_id = named_dev_id != kAudioDeviceUnknown ? named_dev_id : default_dev_id;
+
+       if (named_dev_id != kAudioDeviceUnknown && coreaudio_opt_enable_hog_mode)
+               coreaudio_hog_device(coreaudio_device_id, true);
+
+       OSType unit_subtype = named_dev_id != kAudioDeviceUnknown ?
+                                       kAudioUnitSubType_HALOutput :
+                                       kAudioUnitSubType_DefaultOutput;
+       OSStatus err = coreaudio_init_audio_unit(&coreaudio_audio_unit,
+                                                unit_subtype,
+                                                coreaudio_device_id);
+       if (err != noErr) {
+               errno = ENODEV;
+               return -OP_ERROR_ERRNO;
+       }
+       return OP_ERROR_SUCCESS;
+}
+
+static int coreaudio_exit(void)
+{
+       AudioComponentInstanceDispose(coreaudio_audio_unit);
+       coreaudio_audio_unit = NULL;
+       coreaudio_hog_device(coreaudio_device_id, false);
+       AudioHardwareUnload();
+       coreaudio_device_id = kAudioDeviceUnknown;
+       return OP_ERROR_SUCCESS;
+}
+
+static int coreaudio_open(sample_format_t sf, const channel_position_t *channel_map)
+{
+
+       coreaudio_format_description = coreaudio_fill_format_description(sf);
+       if (coreaudio_opt_sync_rate)
+               coreaudio_sync_device_sample_rate(coreaudio_device_id, coreaudio_format_description);
+       if (channel_map)
+               coreaudio_set_channel_position(coreaudio_device_id,
+                                              coreaudio_format_description.mChannelsPerFrame,
+                                              channel_map);
+       OSStatus err = coreaudio_start_audio_unit(&coreaudio_audio_unit,
+                                                 &coreaudio_buffer_frame_size,
+                                                 coreaudio_format_description);
+       if (err)
+               return -OP_ERROR_SAMPLE_FORMAT;
+       coreaudio_ring_buffer_init(&coreaudio_ring_buffer, coreaudio_buffer_frame_size);
+       return OP_ERROR_SUCCESS;
+}
+
+static int coreaudio_close(void)
+{
+       AudioOutputUnitStop(coreaudio_audio_unit);
+       AudioUnitUninitialize(coreaudio_audio_unit);
+       coreaudio_ring_buffer_destroy(&coreaudio_ring_buffer);
+       return OP_ERROR_SUCCESS;
+}
+
+static int coreaudio_drop(void)
+{
+       coreaudio_ring_buffer_flush(&coreaudio_ring_buffer);
+       return OP_ERROR_SUCCESS;
+}
+
+static int coreaudio_write(const char *buf, int cnt)
+{
+       return coreaudio_ring_buffer_write(&coreaudio_ring_buffer, buf, cnt);
+}
+
+static OSStatus coreaudio_get_device_stereo_channels(AudioDeviceID dev_id, UInt32 *channels) {
+       AudioObjectPropertyAddress aopa = {
+               kAudioDevicePropertyPreferredChannelsForStereo,
+               kAudioObjectPropertyScopeOutput,
+               kAudioObjectPropertyElementMaster
+       };
+       UInt32 size = sizeof(UInt32[2]);
+       OSStatus err = AudioObjectGetPropertyData(dev_id,
+                                                 &aopa,
+                                                 0,
+                                                 NULL,
+                                                 &size,
+                                                 channels);
+       return err;
+} 
+
+static int coreaudio_mixer_set_volume(int l, int r)
+{
+       Float32 vol[2];
+       OSStatus err = 0;
+       for (int i = 0; i < 2; i++) {
+               vol[i]  = (i == 0 ? l : r) * 1.0f / coreaudio_max_volume;
+               if (vol[i] > 1.0f)
+                       vol[i] = 1.0f;
+               if (vol[i] < 0.0f)
+                       vol[i] = 0.0f;
+               AudioObjectPropertyAddress aopa = {
+                       .mSelector      = kAudioDevicePropertyVolumeScalar,
+                       .mScope         = kAudioObjectPropertyScopeOutput,
+                       .mElement       = coreaudio_stero_channels[i]
+               };
+
+               UInt32 size = sizeof(vol[i]);
+               err |= AudioObjectSetPropertyData(coreaudio_device_id,
+                                                 &aopa,
+                                                 0,
+                                                 NULL,
+                                                 size,
+                                                 vol + i);
+       }
+       if (err != noErr) {
+               errno = ENODEV;
+               return -OP_ERROR_ERRNO;
+       }
+       return OP_ERROR_SUCCESS;
+}
+
+static int coreaudio_mixer_get_volume(int *l, int *r)
+{
+       clear_pipe(coreaudio_mixer_pipe_out, -1);
+       Float32 vol[2] = {.0, .0};
+       OSStatus err = 0;
+       for (int i = 0; i < 2; i++) {
+               AudioObjectPropertyAddress aopa = {
+                       .mSelector      = kAudioDevicePropertyVolumeScalar,
+                       .mScope         = kAudioObjectPropertyScopeOutput,
+                       .mElement       = coreaudio_stero_channels[i]
+               };
+               UInt32 size = sizeof(vol[i]);
+               err |= AudioObjectGetPropertyData(coreaudio_device_id,
+                                                 &aopa,
+                                                 0,
+                                                 NULL,
+                                                 &size,
+                                                 vol + i);
+               int volume = vol[i] * coreaudio_max_volume;
+               if (volume > coreaudio_max_volume)
+                       volume = coreaudio_max_volume;
+               if (volume < 0)
+                       volume = 0;
+               if (i == 0) {
+                       *l = volume;
+               } else {
+                       *r = volume;
+               }
+       }
+       if (err != noErr) {
+               errno = ENODEV;
+               return -OP_ERROR_ERRNO;
+       }
+       return OP_ERROR_SUCCESS;
+}
+
+static int coreaudio_mixer_open(int *volume_max)
+{
+       *volume_max = coreaudio_max_volume;
+       OSStatus err = coreaudio_get_device_stereo_channels(coreaudio_device_id, coreaudio_stero_channels);
+       if (err != noErr) {
+               d_print("Cannot get channel information: %d\n", err);
+               errno = ENODEV;
+               return -OP_ERROR_ERRNO;
+       }
+       for (int i = 0; i < 2; i++) {
+               AudioObjectPropertyAddress aopa = {
+                       .mSelector      = kAudioDevicePropertyVolumeScalar,
+                       .mScope         = kAudioObjectPropertyScopeOutput,
+                       .mElement       = coreaudio_stero_channels[i]
+               };
+               err |= AudioObjectAddPropertyListener(coreaudio_device_id,
+                                                     &aopa,
+                                                     coreaudio_device_volume_change_listener,
+                                                     NULL);
+       }
+       if (err != noErr) {
+               d_print("Cannot add property listener: %d\n", err);
+               errno = ENODEV;
+               return -OP_ERROR_ERRNO;
+       }
+       init_pipes(&coreaudio_mixer_pipe_out, &coreaudio_mixer_pipe_in);
+       return OP_ERROR_SUCCESS;
+}
+
+static int coreaudio_mixer_close(void)
+{
+       OSStatus err = noErr;
+       for (int i = 0; i < 2; i++) {
+               AudioObjectPropertyAddress aopa = {
+                       .mSelector      = kAudioDevicePropertyVolumeScalar,
+                       .mScope         = kAudioObjectPropertyScopeOutput,
+                       .mElement       = coreaudio_stero_channels[i]
+               };
+       
+               err |= AudioObjectRemovePropertyListener(coreaudio_device_id,
+                                                        &aopa,
+                                                        coreaudio_device_volume_change_listener,
+                                                        NULL);
+       }
+       if (err != noErr) {
+               d_print("Cannot remove property listener: %d\n", err);
+               errno = ENODEV;
+               return -OP_ERROR_ERRNO;
+       }
+       close(coreaudio_mixer_pipe_out);
+       close(coreaudio_mixer_pipe_in);
+       return OP_ERROR_SUCCESS;
+}
+
+static int coreaudio_mixer_dummy(void)
+{
+       return OP_ERROR_SUCCESS;
+}
+
+static int coreaudio_mixer_get_fds(int *fds)
+{
+       fds[0] = coreaudio_mixer_pipe_out;
+       return 1;
+}
+
+static int coreaudio_pause(void)
+{
+       OSStatus err = AudioOutputUnitStop(coreaudio_audio_unit);
+       if (err != noErr) {
+               errno = ENODEV;
+               return -OP_ERROR_ERRNO;
+       }
+       return OP_ERROR_SUCCESS;
+}
+
+static int coreaudio_unpause(void)
+{
+       OSStatus err = AudioOutputUnitStart(coreaudio_audio_unit);
+       if (err != noErr) {
+               errno = ENODEV;
+               return -OP_ERROR_ERRNO;
+       }
+       return OP_ERROR_SUCCESS;
+}
+
+static int coreaudio_buffer_space(void)
+{
+       return coreaudio_ring_buffer_write_available(&coreaudio_ring_buffer);
+}
+
+static int coreaudio_set_sync_sample_rate(const char *val)
+{
+       coreaudio_opt_sync_rate = strcmp(val, "true") ? false : true;
+       if (coreaudio_opt_sync_rate)
+               coreaudio_sync_device_sample_rate(coreaudio_device_id, coreaudio_format_description);
+       return 0;
+}
+
+static int coreaudio_get_sync_sample_rate(char **val)
+{
+       *val = xstrdup(coreaudio_opt_sync_rate ? "true" : "false");
+       return 0;
+}
+
+static int coreaudio_set_enable_hog_mode(const char *val)
+{
+       coreaudio_opt_enable_hog_mode = strcmp(val, "true") ? false : true;
+       coreaudio_hog_device(coreaudio_device_id, coreaudio_opt_enable_hog_mode);
+       return 0;
+}
+
+static int coreaudio_get_enable_hog_mode(char **val)
+{
+       *val = xstrdup(coreaudio_opt_enable_hog_mode ? "true" : "false");
+       return 0;
+}
+
+static int coreaudio_set_device(const char *val)
+{
+       free(coreaudio_opt_device_name);
+       coreaudio_opt_device_name = NULL;
+       if (val[0])
+               coreaudio_opt_device_name = xstrdup(val);
+       return 0;
+}
+
+static int coreaudio_get_device(char **val)
+{
+       if (coreaudio_opt_device_name)
+               *val = xstrdup(coreaudio_opt_device_name);
+       return 0;
+}
+
+const struct output_plugin_ops op_pcm_ops = {
+       .init         = coreaudio_init,
+       .exit         = coreaudio_exit,
+       .open         = coreaudio_open,
+       .close        = coreaudio_close,
+       .drop         = coreaudio_drop,
+       .write        = coreaudio_write,
+       .pause        = coreaudio_pause,
+       .unpause      = coreaudio_unpause,
+       .buffer_space = coreaudio_buffer_space,
+};
+
+
+const struct mixer_plugin_ops op_mixer_ops = {
+       .init       = coreaudio_mixer_dummy,
+       .exit       = coreaudio_mixer_dummy,
+       .open       = coreaudio_mixer_open,
+       .close      = coreaudio_mixer_close,
+       .get_fds    = coreaudio_mixer_get_fds,
+       .set_volume = coreaudio_mixer_set_volume,
+       .get_volume = coreaudio_mixer_get_volume,
+};
+
+const struct output_plugin_opt op_pcm_options[] = {
+       OPT(coreaudio, device),
+       OPT(coreaudio, enable_hog_mode),
+       OPT(coreaudio, sync_sample_rate),
+       { NULL },
+};
+
+const struct mixer_plugin_opt op_mixer_options[] = {
+       { NULL },
+};
+
+const int op_priority = 1;
+const unsigned op_abi_version = OP_ABI_VERSION;
diff --git a/op/jack.c b/op/jack.c
new file mode 100644 (file)
index 0000000..7237724
--- /dev/null
+++ b/op/jack.c
@@ -0,0 +1,649 @@
+/*
+ * Copyright 2014 Niko Efthymiou <nefthy-cmus@nefthy.de>
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+/* TODO
+ *
+ * - configurable maping of channels to ports
+ */
+
+#include <jack/jack.h>
+#include <jack/ringbuffer.h>
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <stdbool.h>
+
+#if HAVE_CONFIG
+#include "config/samplerate.h"
+#ifdef HAVE_SAMPLERATE
+#include <samplerate.h>
+#endif
+#endif
+
+#include "op.h"
+#include "utils.h"
+#include "channelmap.h"
+#include "xmalloc.h"
+#include "debug.h"
+
+#define CHANNELS 2
+#define BUFFER_MULTIPLYER (sizeof(jack_default_audio_sample_t) * 16)
+#define BUFFER_SIZE_MIN 16384
+
+static char               *server_name;
+
+static jack_client_t      *client;
+static jack_port_t        *output_ports[CHANNELS];
+static jack_ringbuffer_t  *ringbuffer[CHANNELS];
+
+static jack_nframes_t     jack_sample_rate;
+
+#ifdef HAVE_SAMPLERATE
+static SRC_STATE*         src_state[CHANNELS];
+static int                src_quality = SRC_SINC_BEST_QUALITY;
+static float              resample_ratio = 1.0f;
+#endif
+
+static size_t                    buffer_size;
+static sample_format_t           sample_format;
+static unsigned int              sample_bytes;
+static const channel_position_t  *channel_map;
+static volatile bool             paused = true;
+static volatile bool             drop = false;
+static volatile bool             drop_done = true;
+
+/* fail on the next call */
+static int fail;
+
+/* function pointer to appropriate read function */
+static float (*read_sample) (const char *buff);
+
+static int op_jack_init(void);
+static int op_jack_exit(void);
+static int op_jack_open(sample_format_t sf, const channel_position_t* cm);
+static int op_jack_close(void);
+static int op_jack_write(const char *buffer, int count);
+static int op_jack_drop(void);
+static int op_jack_buffer_space(void);
+static int op_jack_pause(void);
+static int op_jack_unpause(void);
+
+/* read functions for various sample formats */
+
+static jack_default_audio_sample_t read_sample_le16(const char *buffer)
+{
+       int16_t s = (int16_t)read_le16(buffer);
+       uint16_t upper_bound = (uint16_t)INT16_MAX + (s <= 0);
+       return (jack_default_audio_sample_t)s / (jack_default_audio_sample_t)upper_bound;
+}
+
+static jack_default_audio_sample_t read_sample_le24(const char *buffer)
+{
+       int32_t s = read_le24i(buffer);
+       uint32_t upper_bound = 0x7FFFFF + (s < 0);
+       return (jack_default_audio_sample_t) s / (jack_default_audio_sample_t)upper_bound;
+}
+
+static jack_default_audio_sample_t read_sample_le32(const char *buffer)
+{
+       int32_t s = (int32_t)read_le32(buffer);
+       uint32_t upper_bound = (uint32_t)INT32_MAX + (s <= 0);
+       return (jack_default_audio_sample_t)s / (jack_default_audio_sample_t)upper_bound;
+}
+
+static jack_default_audio_sample_t read_sample_le16u(const char *buffer)
+{
+       uint32_t u = read_le16(buffer);
+       return (((jack_default_audio_sample_t) u)
+               / ((jack_default_audio_sample_t) UINT16_MAX)) * 2.0 - 2.0;
+}
+
+static jack_default_audio_sample_t read_sample_le24u(const char *buffer)
+{
+       uint32_t u = read_le24(buffer);
+       return (((jack_default_audio_sample_t) u)
+               / ((jack_default_audio_sample_t) 0xFFFFFFU)) * 2.0 - 2.0;
+}
+
+static jack_default_audio_sample_t read_sample_le32u(const char *buffer)
+{
+       uint32_t u = read_le32(buffer);
+       return (((jack_default_audio_sample_t) u)
+               / ((jack_default_audio_sample_t) UINT32_MAX)) * 2.0 - 2.0;
+}
+
+#ifdef HAVE_SAMPLERATE
+static void op_jack_reset_src(void) {
+       for (int c = 0; c < CHANNELS; c++) {
+               src_reset(src_state[c]);
+       }
+}
+#endif
+
+/* jack callbacks */
+static void op_jack_error_cb(const char *msg) {
+       d_print("jackd error: %s\n", msg);
+       fail = 1;
+}
+
+static int op_jack_cb(jack_nframes_t frames, void *arg)
+{
+       size_t bytes_want = frames * sizeof(jack_default_audio_sample_t);
+
+       if (drop) {
+               for (int i = 0; i < CHANNELS; i++) {
+                       jack_ringbuffer_reset(ringbuffer[i]);
+               }
+               drop = false;
+               drop_done = true;
+       }
+
+       size_t bytes_min = SIZE_MAX;
+       for (int i = 0; i < CHANNELS; i++) {
+               size_t bytes_available = jack_ringbuffer_read_space(ringbuffer[i]);
+               if (bytes_available < bytes_min) {
+                       bytes_min = bytes_available;
+               }
+       }
+
+       /* if there is less than frames awaylable play silence */
+       if (paused || bytes_min < bytes_want) {
+               for (int i = 0; i < CHANNELS; i++) {
+                       jack_default_audio_sample_t* jack_buf = jack_port_get_buffer(output_ports[i], frames);
+                       memset(jack_buf, 0, bytes_want);
+               }
+               return 0;
+       }
+
+       for (int i = 0; i < CHANNELS; i++) {
+               jack_default_audio_sample_t* jack_buf = jack_port_get_buffer(output_ports[i], frames);
+               size_t bytes_read = jack_ringbuffer_read(ringbuffer[i], (char*) jack_buf, bytes_want);
+
+               if (bytes_read < bytes_want) {
+                       /* This should not happen[TM] - just in case set fail = 1 */
+                       d_print("underrun! wanted %zu only got %zu bytes\n", bytes_want, bytes_read);
+                       fail = 1;
+               }
+       }
+
+       return 0;
+}
+
+/* init or resize buffers if needed */
+static int op_jack_buffer_init(jack_nframes_t samples, void *arg)
+{
+       if (buffer_size > samples * BUFFER_MULTIPLYER) {
+               /* we just don't shrink buffers, since this could result
+                * in gaps and they won't get that big anyway
+                */
+               return 0;
+       }
+
+       buffer_size = samples * BUFFER_MULTIPLYER;
+       if (buffer_size < BUFFER_SIZE_MIN) {
+               buffer_size = BUFFER_SIZE_MIN;
+       }
+       d_print("new buffer size %zu\n", buffer_size);
+
+       char *tmp = xmalloc(buffer_size);
+
+       for (int i = 0; i < CHANNELS; i++) {
+               jack_ringbuffer_t *new_buffer = jack_ringbuffer_create(buffer_size);
+
+               if (!new_buffer) {
+                       d_print("ringbuffer alloc failed\n");
+                       free(tmp);
+                       fail = 1;
+                       op_jack_exit();
+                       return 1;
+               }
+               if (ringbuffer[i] != NULL) {
+                       size_t length = jack_ringbuffer_read_space(ringbuffer[i]);
+
+                       /* actualy this could both read/write less than length.
+                        * In that case, which should not happen[TM], there will
+                        * be a gap in playback.
+                        */
+                       jack_ringbuffer_read(ringbuffer[i], tmp, length);
+                       jack_ringbuffer_write(new_buffer, tmp, length);
+
+                       jack_ringbuffer_free(ringbuffer[i]);
+               }
+
+               ringbuffer[i] = new_buffer;
+       }
+
+       free(tmp);
+       return 0;
+}
+
+static int op_jack_sample_rate_cb(jack_nframes_t samples, void *arg)
+{
+#ifdef HAVE_SAMPLERATE
+       resample_ratio = (float) sf_get_rate(sample_format) / (float) samples;
+#else
+       if (jack_sample_rate != samples) {
+               /* this cannot be handled */
+               fail = 1;
+               return 1;
+       }
+#endif
+       return 0;
+}
+
+static void op_jack_shutdown_cb(void *arg)
+{
+       d_print("jackd went away");
+
+       /* calling op_jack_exit() will cause a segfault if op_jack_write or
+        * anything else is in the middle of something...
+        * the fail flag is checked by op_jack_write and op_jack_buffer_space
+        *
+        * op_jack_open will try to reinitialize jack
+        */
+       fail = 1;
+}
+
+/* cmus callbacks */
+
+static int op_jack_init(void)
+{
+#ifdef HAVE_SAMPLERATE
+       for (int i = 0; i < CHANNELS; i++) {
+               src_state[i] = src_new(src_quality, 1, NULL);
+               if (src_state[i] == NULL) {
+                       d_print("src_new failed");
+                       for (i = i - 1; i >= 0; i--) {
+                               src_delete(src_state[i]);
+                       }
+                       return -OP_ERROR_INTERNAL;
+               }
+       }
+#endif
+       jack_set_error_function(op_jack_error_cb);
+
+       jack_options_t options = JackNullOption;
+       if (fail) {
+               /* since jackd failed, it will not be autostarted. Either jackd
+                * was killed intentionaly or it died by heartattack.
+                * Until it is restarted, init will happily fail again
+                * and again and again..
+                */
+               options |= JackNoStartServer;
+       }
+
+       jack_status_t status;
+       client = jack_client_open("cmus", options, &status, server_name);
+       if (client == NULL) {
+               d_print("jack_client_new failed\n");
+               return -OP_ERROR_INTERNAL;
+       }
+
+       if (status & JackServerStarted) {
+               d_print("jackd started\n");
+       }
+
+       jack_nframes_t jack_buffer_size = jack_get_buffer_size(client);
+       jack_sample_rate = jack_get_sample_rate(client);
+       op_jack_buffer_init(jack_buffer_size, NULL);
+
+       jack_set_process_callback(client, op_jack_cb, NULL);
+       jack_set_sample_rate_callback(client, op_jack_sample_rate_cb, NULL);
+       jack_set_buffer_size_callback(client, op_jack_buffer_init, NULL);
+       jack_on_shutdown(client, op_jack_shutdown_cb, NULL);
+
+       for (int i = 0; i < CHANNELS; i++) {
+               char port_name[20];
+               snprintf(port_name, sizeof(port_name)-1, "output %d", i);
+
+               output_ports[i] = jack_port_register(
+                       client,
+                       port_name,
+                       JACK_DEFAULT_AUDIO_TYPE,
+                       JackPortIsOutput,
+                       0
+               );
+               if (output_ports[i] == NULL) {
+                       d_print("no jack ports available\n");
+                       return -OP_ERROR_INTERNAL;
+               }
+       }
+
+       if (jack_activate(client)) {
+               d_print("jack_client_activate failed\n");
+               return -OP_ERROR_INTERNAL;
+       }
+
+       const char **ports = jack_get_ports(client, NULL, NULL, JackPortIsPhysical | JackPortIsInput);
+       if (ports == NULL) {
+               d_print("cannot get playback ports\n");
+               return -OP_ERROR_INTERNAL;
+       }
+
+       for (int i = 0; i < CHANNELS; i++) {
+               if (ports[i] == NULL) {
+                       d_print("could not connect output %d. too few ports.\n", i);
+                       break;
+               }
+               if (jack_connect(client, jack_port_name(output_ports[i]), ports[i])) {
+                       d_print("connot connect port %s\n", ports[i]);
+                       jack_free(ports);
+                       return -OP_ERROR_INTERNAL;
+               }
+       }
+
+       jack_free(ports);
+       fail = 0;
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_jack_exit(void)
+{
+       if (client != NULL) {
+               jack_deactivate(client);
+               for (int i = 0; i < CHANNELS; i++) {
+                       if (output_ports[i] != NULL) {
+                               jack_port_unregister(client, output_ports[i]);
+                       }
+               }
+               jack_client_close(client);
+       }
+
+       for (int i = 0; i < CHANNELS; i++) {
+               if (ringbuffer[i] != NULL) {
+                       jack_ringbuffer_free(ringbuffer[i]);
+               }
+               ringbuffer[i] = NULL;
+       }
+
+       buffer_size = 0;
+       client = NULL;
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_jack_open(sample_format_t sf, const channel_position_t *cm)
+{
+       sample_format = sf;
+
+       if (fail) {
+               /* jack went away so lets see if we can recover */
+               if (client != NULL) {
+                       op_jack_exit();
+               }
+               if (op_jack_init() != OP_ERROR_SUCCESS) {
+                       return -OP_ERROR_INTERNAL;
+               }
+       }
+
+       if (cm == NULL) {
+               d_print("no channel_map\n");
+               return -OP_ERROR_NOT_SUPPORTED;
+       }
+       channel_map = cm;
+
+#ifdef HAVE_SAMPLERATE
+       op_jack_reset_src();
+       resample_ratio = (float) jack_sample_rate / (float) sf_get_rate(sf);
+#else
+       if (jack_sample_rate != sf_get_rate(sf)) {
+               d_print("jack sample rate of %d does not match %d\n", jack_get_sample_rate(client), sf_get_rate(sf));
+               return -OP_ERROR_SAMPLE_FORMAT;
+       }
+#endif
+
+       if (sf_get_channels(sf) < CHANNELS) {
+               d_print("%d channels not supported\n", sf_get_channels(sf));
+               return -OP_ERROR_SAMPLE_FORMAT;
+       }
+
+       int bits = sf_get_bits(sf);
+
+       if (bits == 16) {
+               sample_bytes = 2;
+               read_sample = sf_get_signed(sf) ? &read_sample_le16 : &read_sample_le16u;
+       } else if (bits == 24) {
+               sample_bytes = 3;
+               read_sample = sf_get_signed(sf) ? &read_sample_le24 : &read_sample_le24u;
+       } else if (bits == 32) {
+               sample_bytes = 4;
+               read_sample = sf_get_signed(sf) ? &read_sample_le32 : &read_sample_le32u;
+       } else {
+               d_print("%d bits not supported\n", sf_get_bits(sf));
+               return -OP_ERROR_SAMPLE_FORMAT;
+       }
+
+       paused = false;
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_jack_close(void)
+{
+       paused = true;
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_jack_drop(void)
+{
+       if (!drop_done) {
+               return OP_ERROR_SUCCESS;
+       }
+       drop_done = false;
+       drop = true;
+       while (!drop_done) {
+               /* wait till op_jack_cb is done with dropping */
+               usleep(1000);
+       }
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_jack_write(const char *buffer, int count)
+{
+       if (fail) {
+               op_jack_exit();
+               return -OP_ERROR_INTERNAL;
+       }
+
+       if (!drop_done) {
+               return 0;
+       }
+
+       int frame_size = sf_get_frame_size(sample_format);
+       int channels = sf_get_channels(sample_format);
+       size_t frames = count / frame_size;
+
+       /* since this is the only place where the ringbuffers get
+        * written, available space will only grow, therefore frames_min
+        * is safe.
+        */
+       size_t frames_min = SIZE_MAX;
+       for (int c = 0; c < CHANNELS; c++) {
+               size_t frames_available = jack_ringbuffer_write_space(ringbuffer[c]) / sizeof(jack_default_audio_sample_t);
+               if (frames_available < frames_min) {
+                       frames_min = frames_available;
+               }
+       }
+
+       if (frames > frames_min) {
+               frames = frames_min;
+       }
+
+       jack_default_audio_sample_t buf[CHANNELS][buffer_size];
+
+       /* demux and convert to float */
+       for (int pos = 0; pos < count; ) {
+               int frame = pos / frame_size;
+               for (int c = 0; c < channels; c++) {
+                       int idx = pos + c * sample_bytes;
+                       /* for now, only 2 channels and mono are supported */
+                       if (channel_map[c] == CHANNEL_POSITION_LEFT || channel_map[c] == CHANNEL_POSITION_MONO) {
+                               buf[0][frame] = read_sample(&buffer[idx]);
+                       } else if (channel_map[c] == CHANNEL_POSITION_RIGHT || channel_map[c] == CHANNEL_POSITION_MONO) {
+                               buf[1][frame] = read_sample(&buffer[idx]);
+                       }
+               }
+               pos += frame_size;
+       }
+
+#ifdef HAVE_SAMPLERATE
+       if (resample_ratio > 1.01f || resample_ratio < 0.99) {
+               jack_default_audio_sample_t converted[buffer_size];
+               SRC_DATA src_data;
+               for (int c = 0; c < CHANNELS; c++) {
+                       src_data.data_in = buf[c];
+                       src_data.data_out = converted;
+                       src_data.input_frames = frames;
+                       src_data.output_frames = frames_min;
+                       src_data.src_ratio = resample_ratio;
+                       src_data.end_of_input = 0;
+
+                       int err = src_process(src_state[c], &src_data);
+                       if (err) {
+                               d_print("libsamplerate err %s\n", src_strerror(err));
+                       }
+
+                       int byte_length = src_data.output_frames_gen * sizeof(jack_default_audio_sample_t);
+                       jack_ringbuffer_write(ringbuffer[c], (const char*) converted, byte_length);
+               }
+               return src_data.input_frames_used * frame_size;
+       } else {
+#endif
+               int byte_length = frames * sizeof(jack_default_audio_sample_t);
+               for (int c = 0; c < CHANNELS; c++) {
+                       jack_ringbuffer_write(ringbuffer[c], (const char*) buf[c], byte_length);
+               }
+
+               return frames * frame_size;
+#ifdef HAVE_SAMPLERATE
+       }
+#endif
+}
+
+static int op_jack_buffer_space(void)
+{
+       if (fail) {
+               op_jack_exit();
+               return -OP_ERROR_INTERNAL;
+       }
+
+       int bytes = jack_ringbuffer_write_space(ringbuffer[0]);
+       for (int c = 1; c < CHANNELS; c++) {
+               int tmp = jack_ringbuffer_write_space(ringbuffer[0]);
+               if (bytes > tmp) {
+                       bytes = tmp;
+               }
+       }
+
+       int frames = bytes / sizeof(jack_default_audio_sample_t);
+       int frame_size = sf_get_frame_size(sample_format);
+
+#ifdef HAVE_SAMPLERATE
+       return (int) ((float) (frames) / resample_ratio) * frame_size;
+#else
+       return frames * frame_size;
+#endif
+}
+
+static int op_jack_pause(void)
+{
+       paused = true;
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_jack_unpause(void)
+{
+#ifdef HAVE_SAMPLERATE
+       op_jack_reset_src();
+#endif
+       paused = false;
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_jack_set_server_name(const char *val)
+{
+       free(server_name);
+       server_name = val[0] != '\0' ? xstrdup(val) : NULL;
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_jack_get_server_name(char **val)
+{
+       *val = xstrdup(server_name != NULL ? server_name : "");
+       return OP_ERROR_SUCCESS;
+}
+
+#ifdef HAVE_SAMPLERATE
+static int op_jack_set_resampling_quality(const char *val)
+{
+       if (strlen(val) != 1) {
+               return -OP_ERROR_NOT_SUPPORTED;
+       }
+       switch (val[0]) {
+       default:
+       case '2':
+               src_quality = SRC_SINC_BEST_QUALITY;
+               break;
+       case '1':
+               src_quality = SRC_SINC_MEDIUM_QUALITY;
+               break;
+       case '0':
+               src_quality = SRC_SINC_FASTEST;
+               break;
+       }
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_jack_get_resampling_quality(char **val)
+{
+       switch (src_quality) {
+       case SRC_SINC_BEST_QUALITY:
+               *val = xstrdup("2");
+               break;
+       case SRC_SINC_MEDIUM_QUALITY:
+               *val = xstrdup("1");
+               break;
+       case SRC_SINC_FASTEST:
+               *val = xstrdup("0");
+               break;
+       }
+       return OP_ERROR_SUCCESS;
+}
+#endif
+
+const struct output_plugin_ops op_pcm_ops = {
+       .init         = op_jack_init,
+       .exit         = op_jack_exit,
+       .open         = op_jack_open,
+       .close        = op_jack_close,
+       .drop         = op_jack_drop,
+       .write        = op_jack_write,
+       .buffer_space = op_jack_buffer_space,
+       .pause        = op_jack_pause,
+       .unpause      = op_jack_unpause,
+};
+
+const struct output_plugin_opt op_pcm_options[] = {
+       OPT(op_jack, server_name),
+#ifdef HAVE_SAMPLERATE
+       OPT(op_jack, resampling_quality),
+#endif
+       { NULL },
+};
+
+const int op_priority = 2;
+const unsigned op_abi_version = OP_ABI_VERSION;
diff --git a/op/mixer_alsa.c b/op/mixer_alsa.c
new file mode 100644 (file)
index 0000000..d91b413
--- /dev/null
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "mixer.h"
+#include "op.h"
+#include "xmalloc.h"
+#include "debug.h"
+
+#include <strings.h>
+
+#define ALSA_PCM_NEW_HW_PARAMS_API
+#define ALSA_PCM_NEW_SW_PARAMS_API
+
+#include <alsa/asoundlib.h>
+
+static snd_mixer_t *alsa_mixer_handle;
+static snd_mixer_elem_t *mixer_elem = NULL;
+static long mixer_vol_min, mixer_vol_max;
+
+/* configuration */
+static char *alsa_mixer_device = NULL;
+static char *alsa_mixer_element = NULL;
+
+static int alsa_mixer_init(void)
+{
+       if (alsa_mixer_device == NULL)
+               alsa_mixer_device = xstrdup("default");
+       if (alsa_mixer_element == NULL)
+               alsa_mixer_element = xstrdup("PCM");
+       /* FIXME: check device */
+       return 0;
+}
+
+static int alsa_mixer_exit(void)
+{
+       free(alsa_mixer_device);
+       alsa_mixer_device = NULL;
+       free(alsa_mixer_element);
+       alsa_mixer_element = NULL;
+       return 0;
+}
+
+static snd_mixer_elem_t *find_mixer_elem_by_name(const char *goal_name)
+{
+       snd_mixer_elem_t *elem;
+       snd_mixer_selem_id_t *sid = NULL;
+
+       snd_mixer_selem_id_malloc(&sid);
+
+       for (elem = snd_mixer_first_elem(alsa_mixer_handle); elem;
+                elem = snd_mixer_elem_next(elem)) {
+
+               const char *name;
+
+               snd_mixer_selem_get_id(elem, sid);
+               name = snd_mixer_selem_id_get_name(sid);
+               d_print("name = %s\n", name);
+               d_print("has playback volume = %d\n", snd_mixer_selem_has_playback_volume(elem));
+               d_print("has playback switch = %d\n", snd_mixer_selem_has_playback_switch(elem));
+
+               if (strcasecmp(name, goal_name) == 0) {
+                       if (!snd_mixer_selem_has_playback_volume(elem)) {
+                               d_print("mixer element `%s' does not have playback volume\n", name);
+                               elem = NULL;
+                       }
+                       break;
+               }
+       }
+
+       snd_mixer_selem_id_free(sid);
+       return elem;
+}
+
+static int alsa_mixer_open(int *volume_max)
+{
+       snd_mixer_elem_t *elem;
+       int count;
+       int rc;
+
+       rc = snd_mixer_open(&alsa_mixer_handle, 0);
+       if (rc < 0)
+               goto error;
+       rc = snd_mixer_attach(alsa_mixer_handle, alsa_mixer_device);
+       if (rc < 0)
+               goto error;
+       rc = snd_mixer_selem_register(alsa_mixer_handle, NULL, NULL);
+       if (rc < 0)
+               goto error;
+       rc = snd_mixer_load(alsa_mixer_handle);
+       if (rc < 0)
+               goto error;
+       count = snd_mixer_get_count(alsa_mixer_handle);
+       if (count == 0) {
+               d_print("error: mixer does not have elements\n");
+               return -2;
+       }
+
+       elem = find_mixer_elem_by_name(alsa_mixer_element);
+       if (!elem) {
+               d_print("mixer element `%s' not found, trying `Master'\n", alsa_mixer_element);
+               elem = find_mixer_elem_by_name("Master");
+               if (!elem) {
+                       d_print("error: cannot find suitable mixer element\n");
+                       return -2;
+               }
+       }
+       snd_mixer_selem_get_playback_volume_range(elem, &mixer_vol_min, &mixer_vol_max);
+       /* FIXME: get number of channels */
+       mixer_elem = elem;
+       *volume_max = mixer_vol_max - mixer_vol_min;
+
+       return 0;
+
+error:
+       d_print("error: %s\n", snd_strerror(rc));
+       return -1;
+}
+
+static int alsa_mixer_close(void)
+{
+       snd_mixer_close(alsa_mixer_handle);
+       return 0;
+}
+
+static int alsa_mixer_get_fds(int *fds)
+{
+       struct pollfd pfd[NR_MIXER_FDS];
+       int count, i;
+
+       count = snd_mixer_poll_descriptors(alsa_mixer_handle, pfd, NR_MIXER_FDS);
+       for (i = 0; i < count; i++)
+               fds[i] = pfd[i].fd;
+       return count;
+}
+
+static int alsa_mixer_set_volume(int l, int r)
+{
+       if (mixer_elem == NULL) {
+               return -1;
+       }
+       l += mixer_vol_min;
+       r += mixer_vol_min;
+       if (l > mixer_vol_max)
+               d_print("error: left volume too high (%d > %ld)\n",
+                               l, mixer_vol_max);
+       if (r > mixer_vol_max)
+               d_print("error: right volume too high (%d > %ld)\n",
+                               r, mixer_vol_max);
+       snd_mixer_selem_set_playback_volume(mixer_elem, SND_MIXER_SCHN_FRONT_LEFT, l);
+       snd_mixer_selem_set_playback_volume(mixer_elem, SND_MIXER_SCHN_FRONT_RIGHT, r);
+       return 0;
+}
+
+static int alsa_mixer_get_volume(int *l, int *r)
+{
+       long lv, rv;
+
+       if (mixer_elem == NULL)
+               return -1;
+       snd_mixer_handle_events(alsa_mixer_handle);
+       snd_mixer_selem_get_playback_volume(mixer_elem, SND_MIXER_SCHN_FRONT_LEFT, &lv);
+       snd_mixer_selem_get_playback_volume(mixer_elem, SND_MIXER_SCHN_FRONT_RIGHT, &rv);
+       *l = lv - mixer_vol_min;
+       *r = rv - mixer_vol_min;
+       return 0;
+}
+
+static int alsa_mixer_set_channel(const char *val)
+{
+       free(alsa_mixer_element);
+       alsa_mixer_element = xstrdup(val);
+       return 0;
+}
+
+static int alsa_mixer_get_channel(char **val)
+{
+       if (alsa_mixer_element)
+               *val = xstrdup(alsa_mixer_element);
+       return 0;
+}
+
+static int alsa_mixer_set_device(const char *val)
+{
+       free(alsa_mixer_device);
+       alsa_mixer_device = xstrdup(val);
+       return 0;
+}
+
+static int alsa_mixer_get_device(char **val)
+{
+       if (alsa_mixer_device)
+               *val = xstrdup(alsa_mixer_device);
+       return 0;
+}
+
+const struct mixer_plugin_ops op_mixer_ops = {
+       .init = alsa_mixer_init,
+       .exit = alsa_mixer_exit,
+       .open = alsa_mixer_open,
+       .close = alsa_mixer_close,
+       .get_fds = alsa_mixer_get_fds,
+       .set_volume = alsa_mixer_set_volume,
+       .get_volume = alsa_mixer_get_volume,
+};
+
+const struct mixer_plugin_opt op_mixer_options[] = {
+       OPT(alsa_mixer, channel),
+       OPT(alsa_mixer, device),
+       { NULL },
+};
diff --git a/op/mixer_oss.c b/op/mixer_oss.c
new file mode 100644 (file)
index 0000000..bc292da
--- /dev/null
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "mixer.h"
+#include "op.h"
+#include "utils.h"
+#include "xmalloc.h"
+#include "debug.h"
+
+#include <strings.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#if defined(__OpenBSD__)
+#include <soundcard.h>
+#else
+#include <sys/soundcard.h>
+#endif
+
+enum {
+       OSS_MIXER_CHANNEL_VOLUME,
+       OSS_MIXER_CHANNEL_BASS,
+       OSS_MIXER_CHANNEL_TREBLE,
+       OSS_MIXER_CHANNEL_SYNTH,
+       OSS_MIXER_CHANNEL_PCM,
+       OSS_MIXER_CHANNEL_SPEAKER,
+       OSS_MIXER_CHANNEL_LINE,
+       OSS_MIXER_CHANNEL_MIC,
+       OSS_MIXER_CHANNEL_CD,
+       OSS_MIXER_CHANNEL_IMIX,
+       OSS_MIXER_CHANNEL_ALTPCM,
+       OSS_MIXER_CHANNEL_RECLEV,
+       OSS_MIXER_CHANNEL_IGAIN,
+       OSS_MIXER_CHANNEL_OGAIN,
+       OSS_MIXER_CHANNEL_LINE1,
+       OSS_MIXER_CHANNEL_LINE2,
+       OSS_MIXER_CHANNEL_LINE3,
+       OSS_MIXER_CHANNEL_DIGITAL1,
+       OSS_MIXER_CHANNEL_DIGITAL2,
+       OSS_MIXER_CHANNEL_DIGITAL3,
+       OSS_MIXER_CHANNEL_PHONEIN,
+       OSS_MIXER_CHANNEL_PHONEOUT,
+       OSS_MIXER_CHANNEL_VIDEO,
+       OSS_MIXER_CHANNEL_RADIO,
+       OSS_MIXER_CHANNEL_MONITOR,
+       OSS_MIXER_CHANNEL_MAX
+};
+
+static int mixer_fd = -1;
+static int mixer_devmask;
+/* static int mixer_recmask; */
+/* static int mixer_recsrc; */
+/* static int mixer_stereodevs; */
+static char mixer_channels[OSS_MIXER_CHANNEL_MAX];
+
+/* configuration */
+static char *oss_mixer_device = NULL;
+static int oss_volume_controls_pcm = 1;
+
+static int mixer_open(const char *device)
+{
+       int i;
+
+       mixer_fd = open(device, O_RDWR);
+       if (mixer_fd == -1)
+               return -1;
+       ioctl(mixer_fd, SOUND_MIXER_READ_DEVMASK, &mixer_devmask);
+/*     ioctl(mixer_fd, SOUND_MIXER_READ_RECMASK, &mixer_recmask); */
+/*     ioctl(mixer_fd, SOUND_MIXER_READ_RECSRC, &mixer_recsrc); */
+/*     ioctl(mixer_fd, SOUND_MIXER_READ_STEREODEVS, &mixer_stereodevs); */
+       i = 0;
+       while (i < min_i(SOUND_MIXER_NRDEVICES, OSS_MIXER_CHANNEL_MAX)) {
+               mixer_channels[i] = (mixer_devmask >> i) & 1;
+               i++;
+       }
+       while (i < OSS_MIXER_CHANNEL_MAX)
+               mixer_channels[i++] = 0;
+       return 0;
+}
+
+static int mixer_set_level(int channel, int l, int r)
+{
+       int tmp;
+
+       tmp = (l & 0x7f) + ((r & 0x7f) << 8);
+       if (ioctl(mixer_fd, MIXER_WRITE(channel), &tmp) == -1)
+               return -1;
+       return 0;
+}
+
+static int mixer_get_level(int channel, int *l, int *r)
+{
+       int tmp;
+
+       if (ioctl(mixer_fd, MIXER_READ(channel), &tmp) == -1)
+               return -1;
+       *l = tmp & 0x7f;
+       *r = (tmp >> 8) & 0x7f;
+       return 0;
+}
+
+static int oss_device_exists(const char *device)
+{
+       struct stat s;
+
+       if (stat(device, &s) == 0) {
+               d_print("device %s exists\n", device);
+               return 1;
+       }
+       d_print("device %s does not exist\n", device);
+       return 0;
+}
+
+static int oss_mixer_init(void)
+{
+       const char *new_mixer_dev = "/dev/sound/mixer";
+       const char *mixer_dev = "/dev/mixer";
+
+       if (oss_mixer_device) {
+               if (oss_device_exists(oss_mixer_device))
+                       return 0;
+               free(oss_mixer_device);
+               oss_mixer_device = NULL;
+               return -1;
+       }
+       if (oss_device_exists(new_mixer_dev)) {
+               oss_mixer_device = xstrdup(new_mixer_dev);
+               return 0;
+       }
+       if (oss_device_exists(mixer_dev)) {
+               oss_mixer_device = xstrdup(mixer_dev);
+               return 0;
+       }
+       return -1;
+}
+
+static int oss_mixer_exit(void)
+{
+       if (oss_mixer_device) {
+               free(oss_mixer_device);
+               oss_mixer_device = NULL;
+       }
+       return 0;
+}
+
+static int oss_mixer_open(int *volume_max)
+{
+       *volume_max = 100;
+       if (mixer_open(oss_mixer_device) == 0)
+               return 0;
+       return -1;
+}
+
+static int oss_mixer_close(void)
+{
+       close(mixer_fd);
+       mixer_fd = -1;
+       return 0;
+}
+
+static int oss_mixer_set_volume(int l, int r)
+{
+       if (oss_volume_controls_pcm) {
+               return mixer_set_level(OSS_MIXER_CHANNEL_PCM, l, r);
+       } else {
+               return mixer_set_level(OSS_MIXER_CHANNEL_VOLUME, l, r);
+       }
+}
+
+static int oss_mixer_get_volume(int *l, int *r)
+{
+       if (oss_volume_controls_pcm) {
+               return mixer_get_level(OSS_MIXER_CHANNEL_PCM, l, r);
+       } else {
+               return mixer_get_level(OSS_MIXER_CHANNEL_VOLUME, l, r);
+       }
+}
+
+static int oss_mixer_set_channel(const char *val)
+{
+       if (strcasecmp(val, "pcm") == 0) {
+               oss_volume_controls_pcm = 1;
+       } else if (strcasecmp(val, "master") == 0) {
+               oss_volume_controls_pcm = 0;
+       } else {
+               errno = EINVAL;
+               return -OP_ERROR_ERRNO;
+       }
+       return 0;
+}
+
+static int oss_mixer_get_channel(char **val)
+{
+       if (oss_volume_controls_pcm) {
+               *val = xstrdup("PCM");
+       } else {
+               *val = xstrdup("Master");
+       }
+       return 0;
+}
+
+static int oss_mixer_set_device(const char *val)
+{
+       free(oss_mixer_device);
+       oss_mixer_device = xstrdup(val);
+       return 0;
+}
+
+static int oss_mixer_get_device(char **val)
+{
+       if (oss_mixer_device)
+               *val = xstrdup(oss_mixer_device);
+       return 0;
+}
+
+const struct mixer_plugin_ops op_mixer_ops = {
+       .init = oss_mixer_init,
+       .exit = oss_mixer_exit,
+       .open = oss_mixer_open,
+       .close = oss_mixer_close,
+       .set_volume = oss_mixer_set_volume,
+       .get_volume = oss_mixer_get_volume,
+};
+
+const struct mixer_plugin_opt op_mixer_options[] = {
+       OPT(oss_mixer, channel),
+       OPT(oss_mixer, device),
+       { NULL },
+};
diff --git a/op/mixer_sun.c b/op/mixer_sun.c
new file mode 100644 (file)
index 0000000..48c689b
--- /dev/null
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * mixer_sun.c by alex <pukpuk@gmx.de>
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <sys/types.h>
+#include <sys/ioctl.h>
+#include <sys/audioio.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "debug.h"
+#include "mixer.h"
+#include "op.h"
+#include "sf.h"
+#include "xmalloc.h"
+
+static int sun_mixer_device_id = -1;
+static int sun_mixer_channels = -1;
+static int sun_mixer_volume_delta = -1;
+static int mixer_fd = -1;
+
+static char *sun_mixer_device = NULL;
+static char *sun_mixer_channel = NULL;
+
+static int mixer_open(const char *);
+static int min_delta(int, int, int);
+static int sun_device_exists(const char *);
+static int sun_mixer_init(void);
+static int sun_mixer_exit(void);
+static int sun_mixer_open(int *);
+static int sun_mixer_close(void);
+static int sun_mixer_set_volume(int, int);
+static int sun_mixer_get_volume(int *, int *);
+
+static int mixer_open(const char *dev)
+{
+       struct mixer_devinfo minf;
+       int output_class;
+
+       mixer_fd = open(dev, O_RDWR);
+       if (mixer_fd == -1)
+               return -1;
+
+       output_class = -1;
+       sun_mixer_device_id = -1;
+       /* determine output class */
+       minf.index = 0;
+       while (ioctl(mixer_fd, AUDIO_MIXER_DEVINFO, &minf) != -1) {
+               if (minf.type == AUDIO_MIXER_CLASS) {
+                       if (strcmp(minf.label.name, AudioCoutputs) == 0)
+                               output_class = minf.index;
+               }
+               ++minf.index;
+       }
+       /* no output class found?? something must be wrong */
+       if (output_class == -1)
+               return -1;
+
+       minf.index = 0;
+       /* query all mixer devices and try choose the correct one */
+       while (ioctl(mixer_fd, AUDIO_MIXER_DEVINFO, &minf) != -1) {
+               /* only scan output channels */
+               if (minf.type == AUDIO_MIXER_VALUE && minf.prev == AUDIO_MIXER_LAST &&
+                   minf.mixer_class == output_class) {
+                       if (strcasecmp(minf.label.name, sun_mixer_channel) == 0) {
+                               sun_mixer_volume_delta = minf.un.v.delta;
+                               sun_mixer_device_id = minf.index;
+                               sun_mixer_channels = minf.un.v.num_channels;
+                       }
+               }
+               ++minf.index;
+       }
+
+       if (sun_mixer_device_id == -1)
+               return -1;
+
+       d_print("sun: found mixer-channel: %s, devid: %d, delta: %d, channels: %d\n", sun_mixer_channel,
+           sun_mixer_device_id, sun_mixer_volume_delta, sun_mixer_channels);
+
+       if (sun_mixer_volume_delta == 0)
+               sun_mixer_volume_delta = 1;
+
+       return 0;
+
+}
+
+static int min_delta(int oval, int nval, int delta)
+{
+       if (oval > nval && oval - nval < delta)
+               nval -= delta;
+       else if (oval < nval && nval - oval < delta)
+               nval += delta;
+
+       nval = (nval < 0) ? 0 : nval;
+       nval = (nval > AUDIO_MAX_GAIN) ? AUDIO_MAX_GAIN : nval;
+
+       return nval;
+}
+
+static int sun_device_exists(const char *dev)
+{
+       struct stat s;
+
+       if (stat(dev, &s) == 0) {
+               d_print("device %s exists\n", dev);
+               return 1;
+       }
+       d_print("device %s does not exist\n", dev);
+
+       return 0;
+}
+
+static int sun_mixer_init(void)
+{
+       const char *mixer_dev = "/dev/mixer";
+
+       if (sun_mixer_device != NULL) {
+               if (sun_device_exists(sun_mixer_device))
+                       return 0;
+               free(sun_mixer_device);
+               sun_mixer_device = NULL;
+               return -1;
+       }
+       if (sun_device_exists(mixer_dev)) {
+               sun_mixer_device = xstrdup(mixer_dev);
+               return 0;
+       }
+
+       return -1;
+}
+
+static int sun_mixer_exit(void)
+{
+       if (sun_mixer_device != NULL) {
+               free(sun_mixer_device);
+               sun_mixer_device = NULL;
+       }
+       if (sun_mixer_channel != NULL) {
+               free(sun_mixer_channel);
+               sun_mixer_channel = NULL;
+       }
+
+       return 0;
+}
+
+static int sun_mixer_open(int *vol_max)
+{
+       const char *mixer_channel = "master";
+
+       /* set default mixer channel */
+       if (sun_mixer_channel == NULL)
+               sun_mixer_channel = xstrdup(mixer_channel);
+
+       if (mixer_open(sun_mixer_device) == 0) {
+               *vol_max = AUDIO_MAX_GAIN;
+               return 0;
+       }
+
+       return -1;
+}
+
+static int sun_mixer_close(void)
+{
+       if (mixer_fd != -1) {
+               close(mixer_fd);
+               mixer_fd = -1;
+       }
+
+       return 0;
+}
+
+static int sun_mixer_set_volume(int l, int r)
+{
+       struct mixer_ctrl minf;
+       int ovall, ovalr;
+
+       if (sun_mixer_get_volume(&ovall, &ovalr) == -1)
+               return -1;
+
+       /* OpenBSD mixer values are `discrete' */
+       l = min_delta(ovall, l, sun_mixer_volume_delta);
+       r = min_delta(ovalr, r, sun_mixer_volume_delta);
+
+       minf.type = AUDIO_MIXER_VALUE;
+       minf.dev = sun_mixer_device_id;
+
+       if (sun_mixer_channels == 1)
+               minf.un.value.level[AUDIO_MIXER_LEVEL_MONO] = l;
+       else {
+               minf.un.value.level[AUDIO_MIXER_LEVEL_LEFT] = l;
+               minf.un.value.level[AUDIO_MIXER_LEVEL_RIGHT] = r;
+       }
+       minf.un.value.num_channels = sun_mixer_channels;
+
+       if (ioctl(mixer_fd, AUDIO_MIXER_WRITE, &minf) == -1)
+               return -1;
+
+       return 0;
+}
+
+static int sun_mixer_get_volume(int *l, int *r)
+{
+       struct mixer_ctrl minf;
+
+       minf.dev = sun_mixer_device_id;
+       minf.type = AUDIO_MIXER_VALUE;
+       minf.un.value.num_channels = sun_mixer_channels;
+
+       if (ioctl(mixer_fd, AUDIO_MIXER_READ, &minf) == -1)
+               return -1;
+
+       if (sun_mixer_channels == 1) {
+               *l = minf.un.value.level[AUDIO_MIXER_LEVEL_MONO];
+               *r = *l;
+       } else {
+               *l = minf.un.value.level[AUDIO_MIXER_LEVEL_LEFT];
+               *r = minf.un.value.level[AUDIO_MIXER_LEVEL_RIGHT];
+       }
+
+       return 0;
+}
+
+static int sun_mixer_set_channel(const char *val)
+{
+       if (sun_mixer_channel != NULL)
+               free(sun_mixer_channel);
+       sun_mixer_channel = xstrdup(val);
+       return 0;
+}
+
+static int sun_mixer_get_channel(char **val)
+{
+       if (sun_mixer_channel)
+               *val = xstrdup(sun_mixer_channel);
+       return 0;
+}
+
+static int sun_mixer_set_device(const char *val)
+{
+       free(sun_mixer_device);
+       sun_mixer_device = xstrdup(val);
+       return 0;
+}
+
+static int sun_mixer_get_device(char **val)
+{
+       if (sun_mixer_device)
+               *val = xstrdup(sun_mixer_device);
+       return 0;
+}
+
+const struct mixer_plugin_ops op_mixer_ops = {
+       .init = sun_mixer_init,
+       .exit = sun_mixer_exit,
+       .open = sun_mixer_open,
+       .close = sun_mixer_close,
+       .set_volume = sun_mixer_set_volume,
+       .get_volume = sun_mixer_get_volume,
+};
+
+const struct mixer_plugin_opt op_mixer_options[] = {
+       OPT(sun_mixer, channel),
+       OPT(sun_mixer, device),
+       { NULL },
+};
diff --git a/op/oss.c b/op/oss.c
new file mode 100644 (file)
index 0000000..9d59614
--- /dev/null
+++ b/op/oss.c
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "op.h"
+#include "sf.h"
+#include "xmalloc.h"
+#include "debug.h"
+#include "utils.h"
+
+#if defined(__OpenBSD__)
+#include <soundcard.h>
+#else
+#include <sys/soundcard.h>
+#endif
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+static sample_format_t oss_sf;
+static int oss_fd = -1;
+
+/* configuration */
+static char *oss_dsp_device = NULL;
+
+static int oss_close(void);
+
+static int oss_reset(void)
+{
+       if (fcntl(oss_fd, SNDCTL_DSP_RESET, 0) == -1) {
+               return -1;
+       }
+       return 0;
+}
+
+#if defined(__linux__)
+/* defined only in OSSv4, but seem to work in OSSv3 (Linux) */
+#ifndef AFMT_S32_LE
+#define AFMT_S32_LE    0x00001000
+#endif
+#ifndef AFMT_S32_BE
+#define AFMT_S32_BE    0x00002000
+#endif
+#ifndef AFMT_S24_PACKED
+#define AFMT_S24_PACKED        0x00040000
+#endif
+#endif
+
+struct oss_fmt {
+       int fmt, bits, sig, be;
+};
+static struct oss_fmt oss_fmts[] = {
+       { AFMT_S16_BE, 16, 1, 1 },
+       { AFMT_S16_LE, 16, 1, 0 },
+#ifdef AFMT_S24_PACKED
+       { AFMT_S24_PACKED, 24, 1, 0 },
+#endif
+#ifdef AFMT_S24_BE
+       { AFMT_S24_BE, 24, 1, 1 },
+#endif
+#ifdef AFMT_S24_LE
+       { AFMT_S24_LE, 24, 1, 0 },
+#endif
+#ifdef AFMT_S32_BE
+       { AFMT_S32_BE, 32, 1, 1 },
+#endif
+#ifdef AFMT_S32_LE
+       { AFMT_S32_LE, 32, 1, 0 },
+#endif
+
+       { AFMT_U16_BE, 16, 0, 1 },
+       { AFMT_U16_LE, 16, 0, 0 },
+#ifdef AFMT_U24_BE
+       { AFMT_U24_BE, 24, 0, 1 },
+#endif
+#ifdef AFMT_U24_LE
+       { AFMT_U24_LE, 24, 0, 0 },
+#endif
+#ifdef AFMT_U32_BE
+       { AFMT_U32_BE, 32, 0, 1 },
+#endif
+#ifdef AFMT_U32_LE
+       { AFMT_U32_LE, 32, 0, 0 },
+#endif         
+       { AFMT_S8, 8, 1, 0 },
+       { AFMT_S8, 8, 1, 1 },
+       { AFMT_U8, 8, 0, 0 },
+       { AFMT_U8, 8, 0, 1 },
+};
+
+static int oss_set_sf(sample_format_t sf)
+{
+       int found, tmp, log2_fragment_size, nr_fragments, bytes_per_second;
+       size_t i;
+
+       oss_reset();
+       oss_sf = sf;
+
+#ifdef SNDCTL_DSP_CHANNELS
+       tmp = sf_get_channels(oss_sf);
+       if (ioctl(oss_fd, SNDCTL_DSP_CHANNELS, &tmp) == -1)
+               return -1;
+#else
+       tmp = sf_get_channels(oss_sf) - 1;
+       if (ioctl(oss_fd, SNDCTL_DSP_STEREO, &tmp) == -1)
+               return -1;
+#endif
+
+       found = 0;
+       for (i = 0; i < N_ELEMENTS(oss_fmts); i++) {
+               if (sf_get_bits(oss_sf) == oss_fmts[i].bits &&
+                   sf_get_signed(oss_sf) == oss_fmts[i].sig &&
+                   sf_get_bigendian(oss_sf) == oss_fmts[i].be) {
+                       found = 1;
+                       tmp = oss_fmts[i].fmt;
+                       break;
+               }
+       }
+       if (!found) {
+               d_print("unsupported sample format: %c%u_%s\n",
+                       sf_get_signed(oss_sf) ? 'S' : 'U', sf_get_bits(oss_sf),
+                       sf_get_bigendian(oss_sf) ? "BE" : "LE");
+               return -1;
+       }
+       if (ioctl(oss_fd, SNDCTL_DSP_SAMPLESIZE, &tmp) == -1)
+               return -1;
+
+       tmp = sf_get_rate(oss_sf);
+       if (ioctl(oss_fd, SNDCTL_DSP_SPEED, &tmp) == -1)
+               return -1;
+
+       bytes_per_second = sf_get_second_size(oss_sf);
+       log2_fragment_size = 0;
+       while (1 << log2_fragment_size < bytes_per_second / 25)
+               log2_fragment_size++;
+       log2_fragment_size--;
+       nr_fragments = 32;
+
+       /* bits 0..15 = size of fragment, 16..31 = log2(number of fragments) */
+       tmp = (nr_fragments << 16) + log2_fragment_size;
+       if (ioctl(oss_fd, SNDCTL_DSP_SETFRAGMENT, &tmp) == -1)
+               return -1;
+       return 0;
+}
+
+static int oss_device_exists(const char *device)
+{
+       struct stat s;
+
+       if (stat(device, &s))
+               return 0;
+       return 1;
+}
+
+static int oss_init(void)
+{
+       const char *new_dsp_dev = "/dev/sound/dsp";
+       const char *dsp_dev = "/dev/dsp";
+
+       if (oss_dsp_device) {
+               if (oss_device_exists(oss_dsp_device))
+                       return 0;
+               free(oss_dsp_device);
+               oss_dsp_device = NULL;
+               return -1;
+       }
+       if (oss_device_exists(new_dsp_dev)) {
+               oss_dsp_device = xstrdup(new_dsp_dev);
+               return 0;
+       }
+       if (oss_device_exists(dsp_dev)) {
+               oss_dsp_device = xstrdup(dsp_dev);
+               return 0;
+       }
+       return -1;
+}
+
+static int oss_exit(void)
+{
+       free(oss_dsp_device);
+       oss_dsp_device = NULL;
+       return 0;
+}
+
+static int oss_open(sample_format_t sf, const channel_position_t *channel_map)
+{
+       int oss_version = 0;
+       oss_fd = open(oss_dsp_device, O_WRONLY);
+       if (oss_fd == -1)
+               return -1;
+       ioctl(oss_fd, OSS_GETVERSION, &oss_version);
+       d_print("oss version: %#08x\n", oss_version);
+       if (oss_set_sf(sf) == -1) {
+               oss_close();
+               return -1;
+       }
+       return 0;
+}
+
+static int oss_close(void)
+{
+       close(oss_fd);
+       oss_fd = -1;
+       return 0;
+}
+
+static int oss_write(const char *buffer, int count)
+{
+       int rc;
+
+       count -= count % sf_get_frame_size(oss_sf);
+       rc = write(oss_fd, buffer, count);
+       return rc;
+}
+
+static int oss_pause(void)
+{
+       if (ioctl(oss_fd, SNDCTL_DSP_POST, NULL) == -1)
+               return -1;
+       return 0;
+}
+
+static int oss_unpause(void)
+{
+       return 0;
+}
+
+static int oss_buffer_space(void)
+{
+       audio_buf_info info;
+       int space;
+
+       if (ioctl(oss_fd, SNDCTL_DSP_GETOSPACE, &info) == -1)
+               return -1;
+       space = (info.fragments - 1) * info.fragsize;
+       return space;
+}
+
+static int op_oss_set_device(const char *val)
+{
+       free(oss_dsp_device);
+       oss_dsp_device = xstrdup(val);
+       return 0;
+}
+
+static int op_oss_get_device(char **val)
+{
+       if (oss_dsp_device)
+               *val = xstrdup(oss_dsp_device);
+       return 0;
+}
+
+const struct output_plugin_ops op_pcm_ops = {
+       .init = oss_init,
+       .exit = oss_exit,
+       .open = oss_open,
+       .close = oss_close,
+       .write = oss_write,
+       .pause = oss_pause,
+       .unpause = oss_unpause,
+       .buffer_space = oss_buffer_space,
+};
+
+const struct output_plugin_opt op_pcm_options[] = {
+       OPT(op_oss, device),
+       { NULL },
+};
+
+const int op_priority = 1;
+const unsigned op_abi_version = OP_ABI_VERSION;
diff --git a/op/pulse.c b/op/pulse.c
new file mode 100644 (file)
index 0000000..d753506
--- /dev/null
@@ -0,0 +1,619 @@
+/*
+ * Copyright (C) 2008-2013 Various Authors
+ * Copyright (C) 2009 Gregory Petrosyan
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "op.h"
+#include "mixer.h"
+#include "debug.h"
+#include "utils.h"
+#include "xmalloc.h"
+
+static pa_threaded_mainloop    *pa_ml;
+static pa_context              *pa_ctx;
+static pa_stream               *pa_s;
+static pa_channel_map           pa_cmap;
+static pa_cvolume               pa_vol;
+static pa_sample_spec           pa_ss;
+
+static int                      mixer_notify_in;
+static int                      mixer_notify_out;
+
+/* configuration */
+static int pa_restore_volume = 1;
+
+#define ret_pa_error(err)                                              \
+       do {                                                            \
+               d_print("PulseAudio error: %s\n", pa_strerror(err));    \
+               return -OP_ERROR_INTERNAL;                              \
+       } while (0)
+
+#define ret_pa_last_error() ret_pa_error(pa_context_errno(pa_ctx))
+
+static pa_proplist *_create_app_proplist(void)
+{
+       pa_proplist     *pl;
+       int              rc;
+
+       pl = pa_proplist_new();
+       BUG_ON(!pl);
+
+       rc = pa_proplist_sets(pl, PA_PROP_APPLICATION_NAME, "C* Music Player");
+       BUG_ON(rc);
+
+       rc = pa_proplist_sets(pl, PA_PROP_APPLICATION_VERSION, VERSION);
+       BUG_ON(rc);
+
+       return pl;
+}
+
+static pa_proplist *_create_stream_proplist(void)
+{
+       pa_proplist     *pl;
+       int              rc;
+
+       pl = pa_proplist_new();
+       BUG_ON(!pl);
+
+       rc = pa_proplist_sets(pl, PA_PROP_MEDIA_ROLE, "music");
+       BUG_ON(rc);
+
+       rc = pa_proplist_sets(pl, PA_PROP_MEDIA_ICON_NAME, "audio-x-generic");
+       BUG_ON(rc);
+
+       return pl;
+}
+
+static const char *_pa_context_state_str(pa_context_state_t s)
+{
+       switch (s) {
+       case PA_CONTEXT_AUTHORIZING:
+               return "PA_CONTEXT_AUTHORIZING";
+       case PA_CONTEXT_CONNECTING:
+               return "PA_CONTEXT_CONNECTING";
+       case PA_CONTEXT_FAILED:
+               return "PA_CONTEXT_FAILED";
+       case PA_CONTEXT_READY:
+               return "PA_CONTEXT_READY";
+       case PA_CONTEXT_SETTING_NAME:
+               return "PA_CONTEXT_SETTING_NAME";
+       case PA_CONTEXT_TERMINATED:
+               return "PA_CONTEXT_TERMINATED";
+       case PA_CONTEXT_UNCONNECTED:
+               return "PA_CONTEXT_UNCONNECTED";
+       }
+
+       return "unknown";
+}
+
+static void _pa_context_running_cb(pa_context *c, void *data)
+{
+       const pa_context_state_t cs = pa_context_get_state(c);
+
+       d_print("pulse: context state has changed to %s\n", _pa_context_state_str(cs));
+
+       switch (cs) {
+       case PA_CONTEXT_READY:
+       case PA_CONTEXT_FAILED:
+       case PA_CONTEXT_TERMINATED:
+               pa_threaded_mainloop_signal(pa_ml, 0);
+       default:
+               return;
+       }
+}
+
+static const char *_pa_stream_state_str(pa_stream_state_t s)
+{
+       switch (s) {
+       case PA_STREAM_CREATING:
+               return "PA_STREAM_CREATING";
+       case PA_STREAM_FAILED:
+               return "PA_STREAM_FAILED";
+       case PA_STREAM_READY:
+               return "PA_STREAM_READY";
+       case PA_STREAM_TERMINATED:
+               return "PA_STREAM_TERMINATED";
+       case PA_STREAM_UNCONNECTED:
+               return "PA_STREAM_UNCONNECTED";
+       }
+
+       return "unknown";
+}
+
+static void _pa_stream_running_cb(pa_stream *s, void *data)
+{
+       const pa_stream_state_t ss = pa_stream_get_state(s);
+
+       d_print("pulse: stream state has changed to %s\n", _pa_stream_state_str(ss));
+
+       switch (ss) {
+       case PA_STREAM_READY:
+       case PA_STREAM_FAILED:
+       case PA_STREAM_TERMINATED:
+               pa_threaded_mainloop_signal(pa_ml, 0);
+       default:
+               return;
+       }
+}
+
+static void _pa_sink_input_info_cb(pa_context *c,
+                                  const pa_sink_input_info *i,
+                                  int eol,
+                                  void *data)
+{
+       if (i) {
+               memcpy(&pa_vol, &i->volume, sizeof(pa_vol));
+               notify_via_pipe(mixer_notify_in);
+       }
+}
+
+static void _pa_stream_success_cb(pa_stream *s, int success, void *data)
+{
+       pa_threaded_mainloop_signal(pa_ml, 0);
+}
+
+static pa_sample_format_t _pa_sample_format(sample_format_t sf)
+{
+       const int signed_       = sf_get_signed(sf);
+       const int big_endian    = sf_get_bigendian(sf);
+       const int sample_size   = sf_get_sample_size(sf) * 8;
+
+       if (!signed_ && sample_size == 8)
+               return PA_SAMPLE_U8;
+
+       if (signed_) {
+               switch (sample_size) {
+               case 16:
+                       return big_endian ? PA_SAMPLE_S16BE : PA_SAMPLE_S16LE;
+               case 24:
+                       return big_endian ? PA_SAMPLE_S24BE : PA_SAMPLE_S24LE;
+               case 32:
+                       return big_endian ? PA_SAMPLE_S32BE : PA_SAMPLE_S32LE;
+               }
+       }
+
+       return PA_SAMPLE_INVALID;
+}
+
+static int _pa_wait_unlock(pa_operation *o)
+{
+       pa_operation_state_t state;
+
+       if (!o) {
+               pa_threaded_mainloop_unlock(pa_ml);
+               ret_pa_last_error();
+       }
+
+       while ((state = pa_operation_get_state(o)) == PA_OPERATION_RUNNING)
+               pa_threaded_mainloop_wait(pa_ml);
+
+       pa_operation_unref(o);
+       pa_threaded_mainloop_unlock(pa_ml);
+
+       if (state == PA_OPERATION_DONE)
+               return OP_ERROR_SUCCESS;
+       else
+               ret_pa_last_error();
+}
+
+static int _pa_nowait_unlock(pa_operation *o)
+{
+       if (!o) {
+               pa_threaded_mainloop_unlock(pa_ml);
+               ret_pa_last_error();
+       }
+
+       pa_operation_unref(o);
+       pa_threaded_mainloop_unlock(pa_ml);
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int _pa_stream_cork(int pause_)
+{
+       pa_threaded_mainloop_lock(pa_ml);
+
+       return _pa_wait_unlock(pa_stream_cork(pa_s, pause_, _pa_stream_success_cb, NULL));
+}
+
+static int _pa_stream_drain(void)
+{
+       pa_threaded_mainloop_lock(pa_ml);
+
+       return _pa_wait_unlock(pa_stream_drain(pa_s, _pa_stream_success_cb, NULL));
+}
+
+static void _pa_ctx_subscription_cb(pa_context *ctx, pa_subscription_event_type_t t,
+               uint32_t idx, void *userdata)
+{
+       pa_subscription_event_type_t type = t & PA_SUBSCRIPTION_EVENT_TYPE_MASK;
+       if (type != PA_SUBSCRIPTION_EVENT_CHANGE)
+               return;
+
+       if (pa_s && idx == pa_stream_get_index(pa_s))
+               pa_context_get_sink_input_info(ctx, idx, _pa_sink_input_info_cb, NULL);
+}
+
+static int _pa_create_context(void)
+{
+       pa_mainloop_api *api;
+       pa_proplist     *pl;
+       int              rc;
+
+       pl = _create_app_proplist();
+
+       api = pa_threaded_mainloop_get_api(pa_ml);
+       BUG_ON(!api);
+
+       pa_threaded_mainloop_lock(pa_ml);
+
+       pa_ctx = pa_context_new_with_proplist(api, "C* Music Player", pl);
+       BUG_ON(!pa_ctx);
+       pa_proplist_free(pl);
+
+       pa_context_set_state_callback(pa_ctx, _pa_context_running_cb, NULL);
+
+       rc = pa_context_connect(pa_ctx, NULL, PA_CONTEXT_NOFLAGS, NULL);
+       if (rc)
+               goto out_fail;
+
+       for (;;) {
+               pa_context_state_t state;
+               state = pa_context_get_state(pa_ctx);
+               if (state == PA_CONTEXT_READY)
+                       break;
+               if (!PA_CONTEXT_IS_GOOD(state))
+                       goto out_fail_connected;
+               pa_threaded_mainloop_wait(pa_ml);
+       }
+
+       pa_context_set_subscribe_callback(pa_ctx, _pa_ctx_subscription_cb, NULL);
+       pa_operation *op = pa_context_subscribe(pa_ctx, PA_SUBSCRIPTION_MASK_SINK_INPUT,
+                       NULL, NULL);
+       if (!op)
+               goto out_fail_connected;
+       pa_operation_unref(op);
+
+       pa_threaded_mainloop_unlock(pa_ml);
+
+       return OP_ERROR_SUCCESS;
+
+out_fail_connected:
+       pa_context_disconnect(pa_ctx);
+
+out_fail:
+       pa_context_unref(pa_ctx);
+       pa_ctx = NULL;
+
+       pa_threaded_mainloop_unlock(pa_ml);
+
+       ret_pa_last_error();
+}
+
+static int op_pulse_init(void)
+{
+       int rc;
+
+       pa_ml = pa_threaded_mainloop_new();
+       BUG_ON(!pa_ml);
+
+       rc = pa_threaded_mainloop_start(pa_ml);
+       if (rc) {
+               pa_threaded_mainloop_free(pa_ml);
+               ret_pa_error(rc);
+       }
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_pulse_exit(void)
+{
+       if (pa_ml) {
+               pa_threaded_mainloop_stop(pa_ml);
+               pa_threaded_mainloop_free(pa_ml);
+               pa_ml = NULL;
+       }
+
+       return OP_ERROR_SUCCESS;
+}
+
+#define RET_IF(x) case CHANNEL_POSITION_ ## x: return PA_CHANNEL_POSITION_ ## x
+
+static pa_channel_position_t pulse_channel_position(channel_position_t p)
+{
+       switch (p) {
+       RET_IF(MONO);
+       RET_IF(FRONT_LEFT); RET_IF(FRONT_RIGHT); RET_IF(FRONT_CENTER);
+       RET_IF(REAR_CENTER); RET_IF(REAR_LEFT); RET_IF(REAR_RIGHT);
+       RET_IF(LFE);
+       RET_IF(FRONT_LEFT_OF_CENTER); RET_IF(FRONT_RIGHT_OF_CENTER);
+       RET_IF(SIDE_LEFT); RET_IF(SIDE_RIGHT);
+       RET_IF(TOP_CENTER);
+       RET_IF(TOP_FRONT_LEFT); RET_IF(TOP_FRONT_RIGHT); RET_IF(TOP_FRONT_CENTER);
+       RET_IF(TOP_REAR_LEFT); RET_IF(TOP_REAR_RIGHT); RET_IF(TOP_REAR_CENTER);
+       default:
+               return PA_CHANNEL_POSITION_INVALID;
+       }
+}
+
+static int op_pulse_open(sample_format_t sf, const channel_position_t *channel_map)
+{
+       pa_proplist     *pl;
+       int              rc, i;
+
+       const pa_sample_spec ss = {
+               .format         = _pa_sample_format(sf),
+               .rate           = sf_get_rate(sf),
+               .channels       = sf_get_channels(sf)
+       };
+
+       if (!pa_sample_spec_valid(&ss))
+               return -OP_ERROR_SAMPLE_FORMAT;
+
+       pa_ss = ss;
+       if (channel_map && channel_map_valid(channel_map)) {
+               pa_channel_map_init(&pa_cmap);
+               pa_cmap.channels = ss.channels;
+               for (i = 0; i < pa_cmap.channels; i++)
+                       pa_cmap.map[i] = pulse_channel_position(channel_map[i]);
+       } else
+               pa_channel_map_init_auto(&pa_cmap, ss.channels, PA_CHANNEL_MAP_ALSA);
+
+       rc = _pa_create_context();
+       if (rc)
+               return rc;
+
+       pl = _create_stream_proplist();
+
+       pa_threaded_mainloop_lock(pa_ml);
+
+       pa_s = pa_stream_new_with_proplist(pa_ctx, "playback", &ss, &pa_cmap, pl);
+       pa_proplist_free(pl);
+       if (!pa_s) {
+               pa_threaded_mainloop_unlock(pa_ml);
+               ret_pa_last_error();
+       }
+
+       pa_stream_set_state_callback(pa_s, _pa_stream_running_cb, NULL);
+
+       rc = pa_stream_connect_playback(pa_s,
+                                       NULL,
+                                       NULL,
+                                       PA_STREAM_NOFLAGS,
+                                       pa_restore_volume ? NULL : &pa_vol,
+                                       NULL);
+       if (rc)
+               goto out_fail;
+
+       pa_threaded_mainloop_wait(pa_ml);
+
+       if (pa_stream_get_state(pa_s) != PA_STREAM_READY)
+               goto out_fail;
+
+       pa_context_get_sink_input_info(pa_ctx, pa_stream_get_index(pa_s),
+                       _pa_sink_input_info_cb, NULL);
+
+       pa_threaded_mainloop_unlock(pa_ml);
+
+       return OP_ERROR_SUCCESS;
+
+out_fail:
+       pa_stream_unref(pa_s);
+
+       pa_threaded_mainloop_unlock(pa_ml);
+
+       ret_pa_last_error();
+}
+
+static int op_pulse_close(void)
+{
+       /*
+        * If this _pa_stream_drain() will be moved below following
+        * pa_threaded_mainloop_lock(), PulseAudio 0.9.19 will hang.
+        */
+       if (pa_s)
+               _pa_stream_drain();
+
+       pa_threaded_mainloop_lock(pa_ml);
+
+       if (pa_s) {
+               pa_stream_disconnect(pa_s);
+               pa_stream_unref(pa_s);
+               pa_s = NULL;
+       }
+
+       if (pa_ctx) {
+               pa_context_disconnect(pa_ctx);
+               pa_context_unref(pa_ctx);
+               pa_ctx = NULL;
+       }
+
+       pa_threaded_mainloop_unlock(pa_ml);
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_pulse_drop(void)
+{
+       pa_threaded_mainloop_lock(pa_ml);
+
+       return _pa_wait_unlock(pa_stream_flush(pa_s, _pa_stream_success_cb, NULL));
+}
+
+static int op_pulse_write(const char *buf, int count)
+{
+       int rc;
+
+       pa_threaded_mainloop_lock(pa_ml);
+       rc = pa_stream_write(pa_s, buf, count, NULL, 0, PA_SEEK_RELATIVE);
+       pa_threaded_mainloop_unlock(pa_ml);
+
+       if (rc)
+               ret_pa_error(rc);
+       else
+               return count;
+}
+
+static int op_pulse_buffer_space(void)
+{
+       int s;
+
+       pa_threaded_mainloop_lock(pa_ml);
+       s = (int)pa_stream_writable_size(pa_s);
+       pa_threaded_mainloop_unlock(pa_ml);
+
+       return s;
+}
+
+static int op_pulse_pause(void)
+{
+       return _pa_stream_cork(1);
+}
+
+static int op_pulse_unpause(void)
+{
+       return _pa_stream_cork(0);
+}
+
+static int op_pulse_mixer_init(void)
+{
+       if (!pa_channel_map_init_stereo(&pa_cmap))
+               ret_pa_last_error();
+
+       pa_cvolume_reset(&pa_vol, 2);
+
+       init_pipes(&mixer_notify_out, &mixer_notify_in);
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_pulse_mixer_exit(void)
+{
+       close(mixer_notify_out);
+       close(mixer_notify_in);
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_pulse_mixer_open(int *volume_max)
+{
+       *volume_max = PA_VOLUME_NORM;
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_pulse_mixer_close(void)
+{
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_pulse_mixer_get_fds(int *fds)
+{
+       fds[0] = mixer_notify_out;
+       return 1;
+}
+
+static int op_pulse_mixer_set_volume(int l, int r)
+{
+       if (!pa_s && pa_restore_volume)
+               return -OP_ERROR_NOT_OPEN;
+
+       pa_cvolume_set(&pa_vol, pa_ss.channels, (pa_volume_t) ((l + r) / 2));
+       pa_cvolume_set_position(&pa_vol,
+                               &pa_cmap,
+                               PA_CHANNEL_POSITION_FRONT_LEFT,
+                               (pa_volume_t)l);
+
+       pa_cvolume_set_position(&pa_vol,
+                               &pa_cmap,
+                               PA_CHANNEL_POSITION_FRONT_RIGHT,
+                               (pa_volume_t)r);
+
+       if (!pa_s) {
+               return OP_ERROR_SUCCESS;
+       } else {
+               pa_threaded_mainloop_lock(pa_ml);
+
+               return _pa_nowait_unlock(pa_context_set_sink_input_volume(pa_ctx,
+                                                                         pa_stream_get_index(pa_s),
+                                                                         &pa_vol,
+                                                                         NULL,
+                                                                         NULL));
+       }
+}
+
+static int op_pulse_mixer_get_volume(int *l, int *r)
+{
+       clear_pipe(mixer_notify_out, -1);
+
+       if (!pa_s && pa_restore_volume)
+               return -OP_ERROR_NOT_OPEN;
+
+       *l = pa_cvolume_get_position(&pa_vol, &pa_cmap, PA_CHANNEL_POSITION_FRONT_LEFT);
+       *r = pa_cvolume_get_position(&pa_vol, &pa_cmap, PA_CHANNEL_POSITION_FRONT_RIGHT);
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int op_pulse_set_restore_volume(const char *val)
+{
+       pa_restore_volume = is_freeform_true(val);
+       return 0;
+}
+
+static int op_pulse_get_restore_volume(char **val)
+{
+       *val = xstrdup(pa_restore_volume ? "1" : "0");
+       return 0;
+}
+
+const struct output_plugin_ops op_pcm_ops = {
+       .init           = op_pulse_init,
+       .exit           = op_pulse_exit,
+       .open           = op_pulse_open,
+       .close          = op_pulse_close,
+       .drop           = op_pulse_drop,
+       .write          = op_pulse_write,
+       .buffer_space   = op_pulse_buffer_space,
+       .pause          = op_pulse_pause,
+       .unpause        = op_pulse_unpause,
+};
+
+const struct mixer_plugin_ops op_mixer_ops = {
+       .init           = op_pulse_mixer_init,
+       .exit           = op_pulse_mixer_exit,
+       .open           = op_pulse_mixer_open,
+       .close          = op_pulse_mixer_close,
+       .get_fds        = op_pulse_mixer_get_fds,
+       .set_volume     = op_pulse_mixer_set_volume,
+       .get_volume     = op_pulse_mixer_get_volume,
+};
+
+const struct output_plugin_opt op_pcm_options[] = {
+       { NULL },
+};
+
+const struct mixer_plugin_opt op_mixer_options[] = {
+       OPT(op_pulse, restore_volume),
+       { NULL },
+};
+
+const int op_priority = -2;
+const unsigned op_abi_version = OP_ABI_VERSION;
diff --git a/op/roar.c b/op/roar.c
new file mode 100644 (file)
index 0000000..06ae87d
--- /dev/null
+++ b/op/roar.c
@@ -0,0 +1,369 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2010-2011 Philipp 'ph3-der-loewe' Schafft
+ * Copyright 2006 Johannes Weißl
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <roaraudio.h>
+
+#include "op.h"
+#include "mixer.h"
+#include "xmalloc.h"
+#include "utils.h"
+#include "misc.h"
+#include "debug.h"
+
+// we do not use native 2^16-1 here as they use signed ints with 16 bit
+// so we use 2^(16-1)-1 here.
+#define MIXER_BASE_VOLUME 32767
+
+static struct roar_connection con;
+static roar_vs_t *vss = NULL;
+static int err;
+static sample_format_t format;
+
+/* configuration */
+static char *host = NULL;
+static char *role = NULL;
+
+static inline void _err_to_errno(void)
+{
+       roar_err_set(err);
+       roar_err_to_errno();
+}
+
+static int op_roar_dummy(void)
+{
+       return 0;
+}
+
+#if DEBUG > 1
+static ssize_t op_roar_debug_write(struct roar_vio_calls *vio, void *buf_, size_t count)
+{
+       char *buf = (char *) buf_;
+       int len = count;
+       if (len > 0 && buf[len-1] == '\n')
+               len--;
+       if (len > 0)
+               d_print("%*s\n", len, buf);
+       return count;
+}
+
+static struct roar_vio_calls op_roar_debug_cbs = {
+       .write = op_roar_debug_write
+};
+#endif
+
+static int op_roar_init(void)
+{
+#if DEBUG > 1
+       roar_debug_set_stderr_mode(ROAR_DEBUG_MODE_VIO);
+       roar_debug_set_stderr_vio(&op_roar_debug_cbs);
+#else
+       roar_debug_set_stderr_mode(ROAR_DEBUG_MODE_SYSLOG);
+#endif
+       return 0;
+}
+
+static int op_roar_exit(void)
+{
+       free(host);
+       free(role);
+
+       return 0;
+}
+
+static int _set_role(void)
+{
+       int roleid = ROAR_ROLE_UNKNOWN;
+
+       if (role == NULL)
+               return 0;
+
+       roleid = roar_str2role(role);
+
+       if (roleid == ROAR_ROLE_UNKNOWN) {
+               // TODO: warn if role is invalid.
+               return 0;
+       }
+
+       if (roar_vs_role(vss, roleid, &err) == -1) {
+               return -OP_ERROR_ERRNO;
+       }
+
+       return 0;
+}
+
+static int op_roar_open(sample_format_t sf, const channel_position_t *channel_map)
+{
+       struct roar_audio_info info;
+       int ret;
+
+       memset(&info, 0, sizeof(info));
+
+       ROAR_DBG("op_roar_open(*) = ?");
+
+       format = sf;
+
+       info.rate = sf_get_rate(sf);
+       info.channels = sf_get_channels(sf);
+       info.bits = sf_get_bits(sf);
+
+       if (sf_get_bigendian(sf)) {
+               if (sf_get_signed(sf)) {
+                       info.codec = ROAR_CODEC_PCM_S_BE;
+               } else {
+                       info.codec = ROAR_CODEC_PCM_U_BE;
+               }
+       } else {
+               if (sf_get_signed(sf)) {
+                       info.codec = ROAR_CODEC_PCM_S_LE;
+               } else {
+                       info.codec = ROAR_CODEC_PCM_U_LE;
+               }
+       }
+
+       ROAR_DBG("op_roar_open(*) = ?");
+
+       if (roar_libroar_set_server(host) == -1) {
+               ROAR_DBG("op_roar_open(*) = ?");
+
+               roar_err_to_errno();
+               return -OP_ERROR_ERRNO;
+       }
+
+       if ( roar_simple_connect2(&con, NULL, "C* Music Player (cmus)", ROAR_ENUM_FLAG_NONBLOCK, 0) == -1 ) {
+               ROAR_DBG("op_roar_open(*) = ?");
+
+               roar_err_to_errno();
+               return -OP_ERROR_ERRNO;
+       }
+
+       vss = roar_vs_new_from_con(&con, &err);
+       if (vss == NULL) {
+               ROAR_DBG("op_roar_open(*) = ?");
+
+               roar_disconnect(&con);
+
+               _err_to_errno();
+               return -OP_ERROR_ERRNO;
+       }
+
+       if (roar_vs_stream(vss, &info, ROAR_DIR_PLAY, &err) == -1) {
+               ROAR_DBG("op_roar_open(*) = ?");
+
+               roar_disconnect(&con);
+
+               _err_to_errno();
+               return -OP_ERROR_ERRNO;
+       }
+
+       ROAR_DBG("op_roar_open(*) = ?");
+
+       if (roar_vs_buffer(vss, 2048*8, &err) == -1) {
+               roar_vs_close(vss, ROAR_VS_TRUE, NULL);
+               roar_disconnect(&con);
+               _err_to_errno();
+               return -OP_ERROR_ERRNO;
+       }
+
+       ROAR_DBG("op_roar_open(*) = ?");
+
+       ret = _set_role();
+       if (ret != 0) {
+               roar_vs_close(vss, ROAR_VS_TRUE, NULL);
+               roar_disconnect(&con);
+               _err_to_errno();
+               return ret;
+       }
+
+       ROAR_DBG("op_roar_open(*) = ?");
+
+       if (roar_vs_blocking(vss, ROAR_VS_FALSE, &err) == -1) {
+               /* FIXME: handle this error */
+       }
+
+       ROAR_DBG("op_roar_open(*) = 0");
+
+       return 0;
+}
+
+static int op_roar_close(void)
+{
+       roar_vs_close(vss, ROAR_VS_FALSE, &err);
+       roar_disconnect(&con);
+       return 0;
+}
+
+static int op_roar_drop(void)
+{
+       if (roar_vs_reset_buffer(vss, ROAR_VS_TRUE, ROAR_VS_TRUE, &err) == -1) {
+               /* FIXME: handle this error
+                * FIXME: I'm deprecated */
+       }
+       return 0;
+}
+
+static int op_roar_write(const char *buffer, int count)
+{
+       int ret;
+       int i;
+       ret = roar_vs_write(vss, buffer, count, &err);
+       for (i = 0; i < 8; i++)
+               roar_vs_iterate(vss, ROAR_VS_NOWAIT, NULL);
+       return ret;
+}
+
+static int op_roar_buffer_space(void)
+{
+       ssize_t ret;
+       int i;
+       int fs = sf_get_frame_size(format);
+
+       for (i = 0; i < 8; i++)
+               roar_vs_iterate(vss, ROAR_VS_NOWAIT, NULL);
+
+       ret = roar_vs_get_avail_write(vss, &err);
+
+       ret -= ret % fs;
+
+       return ret;
+}
+
+static int op_roar_pause(void) {
+       if (roar_vs_pause(vss, ROAR_VS_TRUE, &err) == -1) {
+               _err_to_errno();
+               return -OP_ERROR_ERRNO;
+       }
+       return 0;
+}
+static int op_roar_unpause(void) {
+       if (roar_vs_pause(vss, ROAR_VS_FALSE, &err) == -1) {
+               _err_to_errno();
+               return -OP_ERROR_ERRNO;
+       }
+       return 0;
+}
+
+
+static int op_roar_mixer_open(int *volume_max)
+{
+       *volume_max = MIXER_BASE_VOLUME;
+       return 0;
+}
+
+static int op_roar_mixer_set_volume(int l, int r)
+{
+       float lf, rf;
+
+       if (vss == NULL)
+               return -OP_ERROR_NOT_OPEN;
+
+       lf = (float)l / (float)MIXER_BASE_VOLUME;
+       rf = (float)r / (float)MIXER_BASE_VOLUME;
+
+       if (roar_vs_volume_stereo(vss, lf, rf, &err) == -1) {
+               _err_to_errno();
+               return -OP_ERROR_ERRNO;
+       }
+
+       return 0;
+}
+
+static int op_roar_mixer_get_volume(int *l, int *r)
+{
+       float lf, rf;
+
+       if (vss == NULL)
+               return -OP_ERROR_NOT_OPEN;
+
+       if (roar_vs_volume_get(vss, &lf, &rf, &err) == -1) {
+               _err_to_errno();
+               return -OP_ERROR_ERRNO;
+       }
+
+       lf *= (float)MIXER_BASE_VOLUME;
+       rf *= (float)MIXER_BASE_VOLUME;
+
+       *l = lf;
+       *r = rf;
+
+       return 0;
+}
+
+static int op_roar_set_server(const char *val)
+{
+       free(host);
+       host = xstrdup(val);
+       return 0;
+}
+
+static int op_roar_get_server(char **val)
+{
+       if (host != NULL)
+               *val = xstrdup(host);
+       return 0;
+}
+
+
+static int op_roar_set_role(const char *val)
+{
+       free(host);
+       host = xstrdup(val);
+       return 0;
+}
+
+static int op_roar_get_role(char **val)
+{
+       if (role != NULL)
+               *val = xstrdup(role);
+       return 0;
+}
+
+const struct output_plugin_ops op_pcm_ops = {
+       .init = op_roar_init,
+       .exit = op_roar_exit,
+       .open = op_roar_open,
+       .close = op_roar_close,
+       .drop = op_roar_drop,
+       .write = op_roar_write,
+       .buffer_space = op_roar_buffer_space,
+       .pause = op_roar_pause,
+       .unpause = op_roar_unpause,
+};
+
+const struct output_plugin_opt op_pcm_options[] = {
+       OPT(op_roar, server),
+       OPT(op_roar, role),
+       { NULL },
+};
+
+const struct mixer_plugin_ops op_mixer_ops = {
+       .init = op_roar_dummy,
+       .exit = op_roar_dummy,
+       .open = op_roar_mixer_open,
+       .close = op_roar_dummy,
+       .get_fds = NULL,
+       .set_volume = op_roar_mixer_set_volume,
+       .get_volume = op_roar_mixer_get_volume,
+};
+
+const struct mixer_plugin_opt op_mixer_options[] = {
+       { NULL },
+};
+
+const int op_priority = -1;
+const unsigned op_abi_version = OP_ABI_VERSION;
diff --git a/op/sndio.c b/op/sndio.c
new file mode 100644 (file)
index 0000000..c2b504f
--- /dev/null
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2011 Donovan "Tsomi" Watteau <tsoomi@gmail.com>
+ *
+ * Based on Thomas Pfaff's work for XMMS, and some suggestions from
+ * Alexandre Ratchov.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <sys/types.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+#include <sndio.h>
+
+#include "op.h"
+#include "mixer.h"
+#include "sf.h"
+#include "xmalloc.h"
+
+static sample_format_t sndio_sf;
+static struct sio_par par;
+static struct sio_hdl *hdl;
+static int sndio_volume = SIO_MAXVOL;
+static int sndio_paused;
+
+static int sndio_mixer_set_volume(int l, int r)
+{
+       sndio_volume = l > r ? l : r;
+
+       if (hdl == NULL)
+               return -OP_ERROR_NOT_INITIALIZED;
+
+       if (!sio_setvol(hdl, sndio_volume))
+               return -OP_ERROR_INTERNAL;
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int sndio_mixer_get_volume(int *l, int *r)
+{
+       *l = *r = sndio_volume;
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int sndio_set_sf(sample_format_t sf)
+{
+       struct sio_par apar;
+
+       sndio_sf = sf;
+
+       sio_initpar(&par);
+
+       par.pchan = sf_get_channels(sndio_sf);
+       par.rate = sf_get_rate(sndio_sf);
+       sndio_paused = 0;
+
+       if (sf_get_signed(sndio_sf))
+               par.sig = 1;
+       else
+               par.sig = 0;
+
+       if (sf_get_bigendian(sndio_sf))
+               par.le = 0;
+       else
+               par.le = 1;
+
+       switch (sf_get_bits(sndio_sf)) {
+       case 32:
+               par.bits = 32;
+               break;
+       case 24:
+               par.bits = 24;
+               par.bps = 3;
+               break;
+       case 16:
+               par.bits = 16;
+               break;
+       case 8:
+               par.bits = 8;
+               break;
+       default:
+               return -OP_ERROR_SAMPLE_FORMAT;
+       }
+
+       par.appbufsz = par.rate * 300 / 1000;
+       apar = par;
+
+       if (!sio_setpar(hdl, &par))
+               return -OP_ERROR_INTERNAL;
+
+       if (!sio_getpar(hdl, &par))
+               return -OP_ERROR_INTERNAL;
+
+       if (apar.rate != par.rate || apar.pchan != par.pchan ||
+           apar.bits != par.bits || (par.bits > 8 && apar.le != par.le) ||
+           apar.sig != par.sig)
+               return -OP_ERROR_INTERNAL;
+
+       sndio_mixer_set_volume(sndio_volume, sndio_volume);
+
+       if (!sio_start(hdl))
+               return -OP_ERROR_INTERNAL;
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int sndio_init(void)
+{
+       return OP_ERROR_SUCCESS;
+}
+
+static int sndio_exit(void)
+{
+       return OP_ERROR_SUCCESS;
+}
+
+static int sndio_close(void)
+{
+       if (hdl != NULL) {
+               sio_close(hdl);
+               hdl = NULL;
+       }
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int sndio_open(sample_format_t sf, const channel_position_t *channel_map)
+{
+       int ret = 0;
+
+       hdl = sio_open(NULL, SIO_PLAY, 0);
+       if (hdl == NULL)
+               return -OP_ERROR_INTERNAL;
+
+       if ((ret = sndio_set_sf(sf)) < 0) {
+               sndio_close();
+               return ret;
+       }
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int sndio_write(const char *buf, int cnt)
+{
+       size_t rc;
+
+       rc = sio_write(hdl, buf, cnt);
+       if (rc == 0)
+               return -OP_ERROR_INTERNAL;
+
+       return rc;
+}
+
+static int op_sndio_set_option(int key, const char *val)
+{
+       return -OP_ERROR_NOT_OPTION;
+}
+
+static int op_sndio_get_option(int key, char **val)
+{
+       return -OP_ERROR_NOT_OPTION;
+}
+
+static int sndio_pause(void)
+{
+       if (!sndio_paused) {
+               if (!sio_stop(hdl))
+                       return -OP_ERROR_INTERNAL;
+               sndio_paused = 1;
+       }
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int sndio_unpause(void)
+{
+       if (sndio_paused) {
+               if (!sio_start(hdl))
+                       return -OP_ERROR_INTERNAL;
+               sndio_paused = 0;
+       }
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int sndio_buffer_space(void)
+{
+       /*
+        * Do as if there's always some space and let sio_write() block.
+        */
+       return par.bufsz * par.bps * par.pchan;
+}
+
+static int sndio_mixer_init(void)
+{
+       return OP_ERROR_SUCCESS;
+}
+
+static int sndio_mixer_exit(void)
+{
+       return OP_ERROR_SUCCESS;
+}
+
+static int sndio_mixer_open(int *volume_max)
+{
+       *volume_max = SIO_MAXVOL;
+
+       return OP_ERROR_SUCCESS;
+}
+
+static int sndio_mixer_close(void)
+{
+       return OP_ERROR_SUCCESS;
+}
+
+const struct output_plugin_ops op_pcm_ops = {
+       .init = sndio_init,
+       .exit = sndio_exit,
+       .open = sndio_open,
+       .close = sndio_close,
+       .write = sndio_write,
+       .pause = sndio_pause,
+       .unpause = sndio_unpause,
+       .buffer_space = sndio_buffer_space,
+};
+
+const struct mixer_plugin_ops op_mixer_ops = {
+       .init = sndio_mixer_init,
+       .exit = sndio_mixer_exit,
+       .open = sndio_mixer_open,
+       .close = sndio_mixer_close,
+       .set_volume = sndio_mixer_set_volume,
+       .get_volume = sndio_mixer_get_volume,
+};
+
+const struct output_plugin_opt op_pcm_options[] = {
+       { NULL },
+};
+
+const struct mixer_plugin_opt op_mixer_options[] = {
+       { NULL },
+};
+
+const int op_priority = 2;
+const unsigned op_abi_version = OP_ABI_VERSION;
diff --git a/op/sun.c b/op/sun.c
new file mode 100644 (file)
index 0000000..6c9b5bd
--- /dev/null
+++ b/op/sun.c
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * sun.c by alex <pukpuk@gmx.de>
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <sys/types.h>
+#include <sys/ioctl.h>
+#include <sys/audioio.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "op.h"
+#include "sf.h"
+#include "xmalloc.h"
+
+static sample_format_t sun_sf;
+static int sun_fd = -1;
+
+static char *sun_audio_device = NULL;
+
+static int sun_reset(void)
+{
+       if (ioctl(sun_fd, AUDIO_FLUSH, NULL) == -1)
+               return -1;
+
+       return 0;
+}
+
+static int sun_set_sf(sample_format_t sf)
+{
+       struct audio_info ainf;
+
+       AUDIO_INITINFO(&ainf);
+
+       sun_reset();
+       sun_sf = sf;
+
+       ainf.play.channels = sf_get_channels(sun_sf);
+       ainf.play.sample_rate = sf_get_rate(sun_sf);
+       ainf.play.pause = 0;
+       ainf.mode = AUMODE_PLAY;
+
+       switch (sf_get_bits(sun_sf)) {
+       case 16:
+               ainf.play.precision = 16;
+               if (sf_get_signed(sun_sf)) {
+                       if (sf_get_bigendian(sun_sf))
+                               ainf.play.encoding = AUDIO_ENCODING_SLINEAR_BE;
+                       else
+                               ainf.play.encoding = AUDIO_ENCODING_SLINEAR_LE;
+               } else {
+                       if (sf_get_bigendian(sun_sf))
+                               ainf.play.encoding = AUDIO_ENCODING_ULINEAR_BE;
+                       else
+                               ainf.play.encoding = AUDIO_ENCODING_ULINEAR_LE;
+               }
+               break;
+       case 8:
+               ainf.play.precision = 8;
+               if (sf_get_signed(sun_sf))
+                       ainf.play.encoding = AUDIO_ENCODING_SLINEAR;
+               else
+                       ainf.play.encoding = AUDIO_ENCODING_ULINEAR;
+               break;
+       default:
+               return -1;
+       }
+
+       if (ioctl(sun_fd, AUDIO_SETINFO, &ainf) == -1)
+               return -1;
+
+       if (ioctl(sun_fd, AUDIO_GETINFO, &ainf) == -1)
+               return -1;
+
+       /* FIXME: check if sample rate is supported */
+       return 0;
+}
+
+static int sun_device_exists(const char *dev)
+{
+       struct stat s;
+
+       if (stat(dev, &s))
+               return 0;
+       return 1;
+}
+
+static int sun_init(void)
+{
+       const char *audio_dev = "/dev/audio";
+
+       if (sun_audio_device != NULL) {
+               if (sun_device_exists(sun_audio_device))
+                       return 0;
+               free(sun_audio_device);
+               sun_audio_device = NULL;
+               return -1;
+       }
+       if (sun_device_exists(audio_dev)) {
+               sun_audio_device = xstrdup(audio_dev);
+               return 0;
+       }
+
+       return -1;
+}
+
+static int sun_exit(void)
+{
+       if (sun_audio_device != NULL) {
+               free(sun_audio_device);
+               sun_audio_device = NULL;
+       }
+
+       return 0;
+}
+
+static int sun_close(void)
+{
+       if (sun_fd != -1) {
+               close(sun_fd);
+               sun_fd = -1;
+       }
+
+       return 0;
+}
+
+static int sun_open(sample_format_t sf, const channel_position_t *channel_map)
+{
+       sun_fd = open(sun_audio_device, O_WRONLY);
+       if (sun_fd == -1)
+               return -1;
+       if (sun_set_sf(sf) == -1) {
+               sun_close();
+               return -1;
+       }
+
+       return 0;
+}
+
+static int sun_write(const char *buf, int cnt)
+{
+       const char *t;
+
+       cnt /= 4;
+       cnt *= 4;
+       t = buf;
+       while (cnt > 0) {
+               int rc = write(sun_fd, buf, cnt);
+               if (rc == -1) {
+                       if (errno == EINTR)
+                               continue;
+                       else
+                               return rc;
+               }
+               buf += rc;
+               cnt -= rc;
+       }
+
+       return (buf - t);
+}
+
+static int sun_pause(void)
+{
+       struct audio_info ainf;
+
+       AUDIO_INITINFO(&ainf);
+
+       ainf.play.pause = 1;
+       if (ioctl(sun_fd, AUDIO_SETINFO, &ainf) == -1)
+               return -1;
+
+       return 0;
+}
+
+static int sun_unpause(void)
+{
+       struct audio_info ainf;
+
+       AUDIO_INITINFO(&ainf);
+
+       ainf.play.pause = 0;
+       if (ioctl(sun_fd, AUDIO_SETINFO, &ainf) == -1)
+               return -1;
+
+       return 0;
+}
+
+static int sun_buffer_space(void)
+{
+       struct audio_info ainf;
+       int sp;
+
+       AUDIO_INITINFO(&ainf);
+
+       if (ioctl(sun_fd, AUDIO_GETINFO, &ainf) == -1)
+               return -1;
+       sp = ainf.play.buffer_size;
+
+       return sp;
+}
+
+static int op_sun_set_device(const char *val)
+{
+       free(sun_audio_device);
+       sun_audio_device = xstrdup(val);
+       return 0;
+}
+
+static int op_sun_get_device(char **val)
+{
+       if (sun_audio_device)
+               *val = xstrdup(sun_audio_device);
+       return 0;
+}
+
+const struct output_plugin_ops op_pcm_ops = {
+       .init = sun_init,
+       .exit = sun_exit,
+       .open = sun_open,
+       .close = sun_close,
+       .write = sun_write,
+       .pause = sun_pause,
+       .unpause = sun_unpause,
+       .buffer_space = sun_buffer_space,
+};
+
+const struct output_plugin_opt op_pcm_options[] = {
+       OPT(op_sun, device),
+       { NULL },
+};
+
+const int op_priority = 0;
+const unsigned op_abi_version = OP_ABI_VERSION;
diff --git a/op/waveout.c b/op/waveout.c
new file mode 100644 (file)
index 0000000..cf92a1b
--- /dev/null
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2007 dnk <dnk@bjum.net>
+ *
+ * Based on oss.c
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "op.h"
+#include "sf.h"
+#include "utils.h"
+#include "xmalloc.h"
+#include "debug.h"
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <mmsystem.h>
+#include <stdio.h>
+
+static HWAVEOUT wave_out;
+static sample_format_t waveout_sf;
+static int buffer_size = 4096;
+static int buffer_count = 12;
+static WAVEHDR *buffers;
+static int buffer_idx;
+static int buffers_free;
+
+#define FRAME_SIZE_ALIGN(x) \
+       (((x) / sf_get_frame_size(waveout_sf)) * sf_get_frame_size(waveout_sf))
+
+static void waveout_error(const char *name, int rc)
+{
+       const char *errstr = "UNKNOWN";
+
+       switch (rc) {
+       case MMSYSERR_ALLOCATED:        errstr = "MMSYSERR_ALLOCATED"; break;
+       case MMSYSERR_INVALHANDLE:      errstr = "MMSYSERR_INVALHANDLE"; break;
+       case MMSYSERR_NODRIVER:         errstr = "MMSYSERR_NODRIVER"; break;
+       case MMSYSERR_BADDEVICEID:      errstr = "MMSYSERR_BADDEVICEID"; break;
+       case MMSYSERR_NOMEM:            errstr = "MMSYSERR_NOMEM"; break;
+       case MMSYSERR_NOTSUPPORTED:     errstr = "MMSYSERR_NOTSUPPORTED"; break;
+       case WAVERR_STILLPLAYING:       errstr = "WAVERR_STILLPLAYING"; break;
+       case WAVERR_UNPREPARED:         errstr = "WAVERR_UNPREPARED"; break;
+       case WAVERR_BADFORMAT:          errstr = "WAVERR_BADFORMAT"; break;
+       case WAVERR_SYNC:               errstr = "WAVERR_SYNC"; break;
+       }
+
+       d_print("%s returned error %s (%d)\n", name, errstr, rc);
+}
+
+static void clean_buffers(void)
+{
+       int i;
+
+       /* mark used buffers clean */
+       for (i = 0; i < buffer_count; i++) {
+               WAVEHDR *hdr = &buffers[(buffer_idx + i) % buffer_count];
+
+               if (!(hdr->dwFlags & WHDR_DONE))
+                       break;
+               buffers_free++;
+               waveOutUnprepareHeader(wave_out, hdr, sizeof(WAVEHDR));
+               hdr->dwFlags = 0;
+       }
+}
+
+static int waveout_open(sample_format_t sf, const channel_position_t *channel_map)
+{
+       WAVEFORMATEX format = {
+               .cbSize          = sizeof(format),
+               .wFormatTag      = WAVE_FORMAT_PCM,
+               .nChannels       = sf_get_channels(sf),
+               .nSamplesPerSec  = sf_get_rate(sf),
+               .wBitsPerSample  = sf_get_bits(sf),
+               .nAvgBytesPerSec = sf_get_second_size(sf),
+               .nBlockAlign     = sf_get_frame_size(sf)
+       };
+       int rc, i;
+
+       /* WAVEFORMATEX does not support channels > 2, waveOutWrite() wants little endian signed PCM */
+       if (sf_get_bigendian(sf) || !sf_get_signed(sf) || sf_get_channels(sf) > 2) {
+               return -OP_ERROR_SAMPLE_FORMAT;
+       }
+
+       rc = waveOutOpen(&wave_out, WAVE_MAPPER, &format, 0, 0, CALLBACK_NULL);
+       if (rc != MMSYSERR_NOERROR) {
+               switch (rc) {
+               case MMSYSERR_ALLOCATED:
+                       errno = EBUSY;
+                       return -OP_ERROR_ERRNO;
+               case MMSYSERR_BADDEVICEID:
+               case MMSYSERR_NODRIVER:
+                       errno = ENODEV;
+                       return -OP_ERROR_ERRNO;
+               case MMSYSERR_NOMEM:
+                       errno = ENOMEM;
+                       return -OP_ERROR_ERRNO;
+               case WAVERR_BADFORMAT:
+                       return -OP_ERROR_SAMPLE_FORMAT;
+               }
+               return -OP_ERROR_INTERNAL;
+       }
+
+       /* reset buffers */
+       for (i = 0; i < buffer_count; i++) {
+               buffers[i].dwFlags = 0;
+       }
+       buffer_idx = 0;
+       buffers_free = buffer_count;
+
+       waveout_sf = sf;
+
+       return 0;
+}
+
+static int waveout_close(void)
+{
+       int rc;
+
+       waveOutReset(wave_out);
+
+       clean_buffers();
+
+       rc = waveOutClose(wave_out);
+       if (rc != MMSYSERR_NOERROR) {
+               waveout_error("waveOutClose", rc);
+               return -1;
+       }
+       wave_out = NULL;
+
+       return 0;
+}
+
+static int waveout_init(void)
+{
+       WAVEHDR *hdr;
+       int i;
+
+       /* create buffers */
+       buffers = xnew(WAVEHDR, buffer_count);
+       for (i = 0; i < buffer_count; i++) {
+               hdr = &buffers[i];
+
+               memset(hdr, 0, sizeof(WAVEHDR));
+               hdr->lpData = xmalloc(buffer_size);
+       }
+       return 0;
+}
+
+static int waveout_exit(void)
+{
+       int i;
+
+       for (i = 0; i < buffer_count; i++) {
+               free(buffers[i].lpData);
+       }
+       free(buffers);
+       buffers = NULL;
+
+       return 0;
+}
+
+static int waveout_write(const char *buffer, int count)
+{
+       int written = 0;
+       int len, rc;
+
+       count = FRAME_SIZE_ALIGN(count);
+
+       clean_buffers();
+
+       while (count > 0) {
+               WAVEHDR *hdr = &buffers[buffer_idx];
+
+               if (hdr->dwFlags != 0) {
+                       /* no free buffers */
+                       break;
+               }
+
+               len = FRAME_SIZE_ALIGN(min_i(count, buffer_size));
+               hdr->dwBufferLength = len;
+               memcpy(hdr->lpData, buffer + written, len);
+
+               rc = waveOutPrepareHeader(wave_out, hdr, sizeof(WAVEHDR));
+               if (rc != MMSYSERR_NOERROR) {
+                       waveout_error("waveOutPrepareHeader", rc);
+                       break;
+               }
+
+               rc = waveOutWrite(wave_out, hdr, sizeof(WAVEHDR));
+               if (rc != MMSYSERR_NOERROR) {
+                       waveOutUnprepareHeader(wave_out, hdr, sizeof(WAVEHDR));
+                       hdr->dwFlags = 0;
+                       waveout_error("waveOutWrite", rc);
+                       break;
+               }
+
+               written += len;
+               count -= len;
+               buffer_idx = (buffer_idx + 1) % buffer_count;
+               buffers_free--;
+       }
+
+       return written;
+}
+
+static int waveout_pause(void)
+{
+       if (waveOutPause(wave_out) != MMSYSERR_NOERROR)
+               return -1;
+       return 0;
+}
+
+static int waveout_unpause(void)
+{
+       if (waveOutRestart(wave_out) != MMSYSERR_NOERROR)
+               return -1;
+       return 0;
+}
+
+static int waveout_buffer_space(void)
+{
+       clean_buffers();
+
+       return buffers_free * FRAME_SIZE_ALIGN(buffer_size);
+}
+
+static int waveout_set_buffer_common(long int val, long int *dst)
+{
+       int reinit = 0;
+
+       if (buffers) {
+               waveout_exit();
+               reinit = 1;
+       }
+       *dst = val;
+
+       if (reinit) {
+               waveout_init();
+       }
+
+       return 0;
+}
+
+static int waveout_set_buffer_size(const char *val)
+{
+       long int ival;
+       if (str_to_int(val, &ival) || ival < 4096 || ival > 65536) {
+               errno = EINVAL;
+               return -OP_ERROR_ERRNO;
+       }
+       return waveout_set_buffer_common(ival, &buffer_size);
+}
+
+static int waveout_set_buffer_count(const char *val)
+{
+       long int ival;
+       if (str_to_int(val, &ival) || ival < 2 || ival > 64) {
+               errno = EINVAL;
+               return -OP_ERROR_ERRNO;
+       }
+       return waveout_set_buffer_common(ival, &buffer_count);
+}
+
+static int waveout_get_buffer_size(char **val)
+{
+       *val = xnew(char, 22);
+       snprintf(*val, 22, "%d", buffer_size);
+       return 0;
+}
+
+static int waveout_get_buffer_count(char **val)
+{
+       *val = xnew(char, 22);
+       snprintf(*val, 22, "%d", buffer_count);
+       return 0;
+}
+
+const struct output_plugin_ops op_pcm_ops = {
+       .init = waveout_init,
+       .exit = waveout_exit,
+       .open = waveout_open,
+       .close = waveout_close,
+       .write = waveout_write,
+       .pause = waveout_pause,
+       .unpause = waveout_unpause,
+       .buffer_space = waveout_buffer_space,
+};
+
+const struct output_plugin_opt op_pcm_options[] = {
+       OPT(waveout, buffer_size),
+       OPT(waveout, buffer_count),
+       { NULL },
+};
+
+const int op_priority = 0;
+const unsigned op_abi_version = OP_ABI_VERSION;
diff --git a/options.c b/options.c
new file mode 100644 (file)
index 0000000..1b6b88b
--- /dev/null
+++ b/options.c
@@ -0,0 +1,1746 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "options.h"
+#include "list.h"
+#include "utils.h"
+#include "xmalloc.h"
+#include "player.h"
+#include "buffer.h"
+#include "ui_curses.h"
+#include "cmus.h"
+#include "misc.h"
+#include "lib.h"
+#include "pl.h"
+#include "browser.h"
+#include "keys.h"
+#include "filters.h"
+#include "command_mode.h"
+#include "file.h"
+#include "prog.h"
+#include "output.h"
+#include "input.h"
+#include "xstrjoin.h"
+#include "track_info.h"
+#include "cache.h"
+#include "debug.h"
+#include "discid.h"
+#include "mpris.h"
+
+#include <stdio.h>
+#include <errno.h>
+#include <strings.h>
+
+#if defined(__sun__)
+#include <ncurses.h>
+#else
+#include <curses.h>
+#endif
+
+/* initialized option variables */
+
+char *cdda_device = NULL;
+char *output_plugin = NULL;
+char *status_display_program = NULL;
+char *server_password;
+int auto_reshuffle = 1;
+int confirm_run = 1;
+int resume_cmus = 0;
+int show_hidden = 0;
+int show_current_bitrate = 0;
+int show_playback_position = 1;
+int show_remaining_time = 0;
+int set_term_title = 1;
+int wrap_search = 1;
+int play_library = 1;
+int repeat = 0;
+int shuffle = 0;
+int follow = 0;
+int display_artist_sort_name;
+int smart_artist_sort = 1;
+int scroll_offset = 2;
+int rewind_offset = 5;
+int skip_track_info = 0;
+int auto_expand_albums_follow = 1;
+int auto_expand_albums_search = 1;
+int auto_expand_albums_selcur = 1;
+int show_all_tracks = 1;
+int mouse = 0;
+int mpris = 1;
+int time_show_leading_zero = 1;
+int start_view = TREE_VIEW;
+
+int colors[NR_COLORS] = {
+       -1,
+       -1,
+       COLOR_RED | BRIGHT,
+       COLOR_YELLOW | BRIGHT,
+
+       COLOR_BLUE,
+       COLOR_WHITE,
+       COLOR_BLACK,
+       COLOR_BLUE,
+
+       COLOR_WHITE | BRIGHT,
+       -1,
+       COLOR_YELLOW | BRIGHT,
+       COLOR_BLUE,
+
+       COLOR_YELLOW | BRIGHT,
+       COLOR_BLUE | BRIGHT,
+       -1,
+       COLOR_WHITE,
+
+       COLOR_YELLOW | BRIGHT,
+       COLOR_WHITE,
+       COLOR_BLACK,
+       COLOR_BLUE,
+
+       COLOR_WHITE | BRIGHT,
+       COLOR_BLUE,
+       COLOR_WHITE | BRIGHT,
+       -1,
+
+       -1,
+};
+
+int attrs[NR_ATTRS] = {
+       A_NORMAL,
+       A_NORMAL,
+       A_NORMAL,
+       A_NORMAL,
+       A_NORMAL,
+       A_NORMAL,
+       A_NORMAL,
+       A_NORMAL,
+       A_NORMAL,
+       A_NORMAL,
+       A_BOLD,
+       A_NORMAL,
+};
+
+/* uninitialized option variables */
+char *tree_win_format = NULL;
+char *tree_win_artist_format = NULL;
+char *track_win_album_format = NULL;
+char *track_win_format = NULL;
+char *track_win_format_va = NULL;
+char *track_win_alt_format = NULL;
+char *list_win_format = NULL;
+char *list_win_format_va = NULL;
+char *list_win_alt_format = NULL;
+char *current_format = NULL;
+char *current_alt_format = NULL;
+char *statusline_format = NULL;
+char *window_title_format = NULL;
+char *window_title_alt_format = NULL;
+char *id3_default_charset = NULL;
+char *icecast_default_charset = NULL;
+char *lib_add_filter = NULL;
+
+static void buf_int(char *buf, int val, size_t size)
+{
+       snprintf(buf, size, "%d", val);
+}
+
+static int parse_int(const char *buf, int minval, int maxval, int *val)
+{
+       long int tmp;
+
+       if (str_to_int(buf, &tmp) == -1 || tmp < minval || tmp > maxval) {
+               error_msg("integer in range %d..%d expected", minval, maxval);
+               return 0;
+       }
+       *val = tmp;
+       return 1;
+}
+
+int parse_enum(const char *buf, int minval, int maxval, const char * const names[], int *val)
+{
+       long int tmp;
+       int i;
+
+       if (str_to_int(buf, &tmp) == 0) {
+               if (tmp < minval || tmp > maxval)
+                       goto err;
+               *val = tmp;
+               return 1;
+       }
+
+       for (i = 0; names[i]; i++) {
+               if (strcasecmp(buf, names[i]) == 0) {
+                       *val = i + minval;
+                       return 1;
+               }
+       }
+err:
+       error_msg("name or integer in range %d..%d expected", minval, maxval);
+       return 0;
+}
+
+static const char * const bool_names[] = {
+       "false", "true", NULL
+};
+
+static int parse_bool(const char *buf, int *val)
+{
+       return parse_enum(buf, 0, 1, bool_names, val);
+}
+
+/* this is used as id in struct cmus_opt */
+enum format_id {
+       FMT_CURRENT,
+       FMT_CURRENT_ALT,
+       FMT_STATUSLINE,
+       FMT_PLAYLIST,
+       FMT_PLAYLIST_ALT,
+       FMT_PLAYLIST_VA,
+       FMT_TITLE,
+       FMT_TITLE_ALT,
+       FMT_TRACKWIN,
+       FMT_TRACKWIN_ALBUM,
+       FMT_TRACKWIN_ALT,
+       FMT_TRACKWIN_VA,
+       FMT_TREEWIN,
+       FMT_TREEWIN_ARTIST,
+
+       NR_FMTS
+};
+
+/* default values for the variables which we must initialize but
+ * can't do it statically */
+static const struct {
+       const char *name;
+       const char *value;
+} str_defaults[] = {
+       [FMT_CURRENT_ALT]       = { "altformat_current"         , " %F "                                                },
+       [FMT_CURRENT]           = { "format_current"            , " %a - %l -%3n. %t%= %y "                             },
+       [FMT_STATUSLINE]        = { "format_statusline"         ,
+               " %{status} %{?show_playback_position?%{position} %{?duration?/ %{duration} }?%{?duration?%{duration} }}"
+               "- %{total} %{?bpm>0?at %{bpm} BPM }"
+               "%{?volume>=0?vol: %{?lvolume!=rvolume?%{lvolume},%{rvolume} ?%{volume} }}"
+               "%{?stream?buf: %{buffer} }"
+               "%{?show_current_bitrate & bitrate>=0? %{bitrate} kbps }"
+               "%="
+               "%{?repeat_current?repeat current?%{?play_library?%{playlist_mode} from %{?play_sorted?sorted }library?playlist}}"
+               " | %1{continue}%1{follow}%1{repeat}%1{shuffle} "
+       },
+       [FMT_PLAYLIST_ALT]      = { "altformat_playlist"        , " %f%= %d "                                           },
+       [FMT_PLAYLIST]          = { "format_playlist"           , " %-21%a %3n. %t%= %y %d %{?X!=0?%3X ?    }"          },
+       [FMT_PLAYLIST_VA]       = { "format_playlist_va"        , " %-21%A %3n. %t (%a)%= %y %d %{?X!=0?%3X ?    }"     },
+       [FMT_TITLE_ALT]         = { "altformat_title"           , "%f"                                                  },
+       [FMT_TITLE]             = { "format_title"              , "%a - %l - %t (%y)"                                   },
+       [FMT_TRACKWIN_ALBUM]    = { "format_trackwin_album"     , " %l %= %{albumduration} "                            },
+       [FMT_TRACKWIN_ALT]      = { "altformat_trackwin"        , " %f%= %d "                                           },
+       [FMT_TRACKWIN]          = { "format_trackwin"           , "%3n. %t%= %y %d "                                    },
+       [FMT_TRACKWIN_VA]       = { "format_trackwin_va"        , "%3n. %t (%a)%= %y %d "                               },
+       [FMT_TREEWIN]           = { "format_treewin"            , "  %l"                                                },
+       [FMT_TREEWIN_ARTIST]    = { "format_treewin_artist"     , "%a"                                                  },
+
+       [NR_FMTS] =
+
+       { "lib_sort", "albumartist date album discnumber tracknumber title filename play_count" },
+       { "pl_sort", "" },
+       { "id3_default_charset", "ISO-8859-1" },
+       { "icecast_default_charset", "ISO-8859-1" },
+       { NULL, NULL }
+};
+
+/* callbacks for normal options {{{ */
+
+static void get_device(void *data, char *buf, size_t size)
+{
+       strscpy(buf, cdda_device, size);
+}
+
+static void set_device(void *data, const char *buf)
+{
+       free(cdda_device);
+       cdda_device = expand_filename(buf);
+}
+
+#define SECOND_SIZE (44100 * 16 / 8 * 2)
+static void get_buffer_seconds(void *data, char *buf, size_t size)
+{
+       int val = (player_get_buffer_chunks() * CHUNK_SIZE + SECOND_SIZE / 2) /
+               SECOND_SIZE;
+       buf_int(buf, val, size);
+}
+
+static void set_buffer_seconds(void *data, const char *buf)
+{
+       int sec;
+
+       if (parse_int(buf, 1, 300, &sec))
+               player_set_buffer_chunks((sec * SECOND_SIZE + CHUNK_SIZE / 2) / CHUNK_SIZE);
+}
+
+static void get_scroll_offset(void *data, char *buf, size_t size)
+{
+       buf_int(buf, scroll_offset, size);
+}
+
+static void set_scroll_offset(void *data, const char *buf)
+{
+       int offset;
+
+       if (parse_int(buf, 0, 9999, &offset))
+               scroll_offset = offset;
+}
+
+static void get_rewind_offset(void *data, char *buf, size_t size)
+{
+       buf_int(buf, rewind_offset, size);
+}
+
+static void set_rewind_offset(void *data, const char *buf)
+{
+       int offset;
+
+       if (parse_int(buf, -1, 9999, &offset))
+               rewind_offset = offset;
+}
+
+static void get_id3_default_charset(void *data, char *buf, size_t size)
+{
+       strscpy(buf, id3_default_charset, size);
+}
+
+static void get_icecast_default_charset(void *data, char *buf, size_t size)
+{
+       strscpy(buf, icecast_default_charset, size);
+}
+
+static void set_id3_default_charset(void *data, const char *buf)
+{
+       free(id3_default_charset);
+       id3_default_charset = xstrdup(buf);
+}
+
+static void set_icecast_default_charset(void *data, const char *buf)
+{
+       free(icecast_default_charset);
+       icecast_default_charset = xstrdup(buf);
+}
+
+static void get_lib_sort(void *data, char *buf, size_t size)
+{
+       strscpy(buf, lib_editable.shared->sort_str, size);
+}
+
+static void set_lib_sort(void *data, const char *buf)
+{
+       sort_key_t *keys = parse_sort_keys(buf);
+
+       if (keys) {
+               editable_shared_set_sort_keys(lib_editable.shared, keys);
+               editable_sort(&lib_editable);
+               sort_keys_to_str(keys, lib_editable.shared->sort_str,
+                               sizeof(lib_editable.shared->sort_str));
+       }
+}
+
+static void get_pl_sort(void *data, char *buf, size_t size)
+{
+       pl_get_sort_str(buf, size);
+}
+
+static void set_pl_sort(void *data, const char *buf)
+{
+       pl_set_sort_str(buf);
+}
+
+static void get_output_plugin(void *data, char *buf, size_t size)
+{
+       const char *value = op_get_current();
+
+       if (value)
+               strscpy(buf, value, size);
+}
+
+static void set_output_plugin(void *data, const char *buf)
+{
+       if (ui_initialized) {
+               if (!soft_vol)
+                       mixer_close();
+               player_set_op(buf);
+               if (!soft_vol)
+                       mixer_open();
+       } else {
+               /* must set it later manually */
+               output_plugin = xstrdup(buf);
+       }
+}
+
+static void get_passwd(void *data, char *buf, size_t size)
+{
+       if (server_password)
+               strscpy(buf, server_password, size);
+}
+
+static void set_passwd(void *data, const char *buf)
+{
+       int len = strlen(buf);
+
+       if (len == 0) {
+               free(server_password);
+               server_password = NULL;
+       } else if (len < 6) {
+               error_msg("unsafe password");
+       } else {
+               free(server_password);
+               server_password = xstrdup(buf);
+       }
+}
+
+static void get_replaygain_preamp(void *data, char *buf, size_t size)
+{
+       snprintf(buf, size, "%f", replaygain_preamp);
+}
+
+static void set_replaygain_preamp(void *data, const char *buf)
+{
+       double val;
+       char *end;
+
+       val = strtod(buf, &end);
+       if (end == buf) {
+               error_msg("floating point number expected (dB)");
+               return;
+       }
+       player_set_rg_preamp(val);
+}
+
+static void get_softvol_state(void *data, char *buf, size_t size)
+{
+       snprintf(buf, size, "%d %d", soft_vol_l, soft_vol_r);
+}
+
+static void set_softvol_state(void *data, const char *buf)
+{
+       char buffer[OPTION_MAX_SIZE];
+       char *ptr;
+       long int l, r;
+
+       strscpy(buffer, buf, sizeof(buffer));
+       ptr = strchr(buffer, ' ');
+       if (!ptr)
+               goto err;
+       while (*ptr == ' ')
+               *ptr++ = 0;
+
+       if (str_to_int(buffer, &l) == -1 || l < 0 || l > 100)
+               goto err;
+       if (str_to_int(ptr, &r) == -1 || r < 0 || r > 100)
+               goto err;
+
+       player_set_soft_volume(l, r);
+       return;
+err:
+       error_msg("two integers in range 0..100 expected");
+}
+
+static void get_status_display_program(void *data, char *buf, size_t size)
+{
+       if (status_display_program)
+               strscpy(buf, status_display_program, size);
+}
+
+static void set_status_display_program(void *data, const char *buf)
+{
+       free(status_display_program);
+       status_display_program = NULL;
+       if (buf[0])
+               status_display_program = expand_filename(buf);
+}
+
+/* }}} */
+
+/* callbacks for toggle options {{{ */
+
+static void get_auto_reshuffle(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[auto_reshuffle], size);
+}
+
+static void set_auto_reshuffle(void *data, const char *buf)
+{
+       parse_bool(buf, &auto_reshuffle);
+}
+
+static void toggle_auto_reshuffle(void *data)
+{
+       auto_reshuffle ^= 1;
+}
+
+static void get_follow(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[follow], size);
+}
+
+static void set_follow(void *data, const char *buf)
+{
+       if (!parse_bool(buf, &follow))
+               return;
+       update_statusline();
+}
+
+static void toggle_follow(void *data)
+{
+       follow ^= 1;
+       update_statusline();
+}
+
+static void get_continue(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[player_cont], size);
+}
+
+static void set_continue(void *data, const char *buf)
+{
+       if (!parse_bool(buf, &player_cont))
+               return;
+       update_statusline();
+}
+
+static void toggle_continue(void *data)
+{
+       player_cont ^= 1;
+       update_statusline();
+}
+
+static void get_repeat_current(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[player_repeat_current], size);
+}
+
+static void set_repeat_current(void *data, const char *buf)
+{
+       int old = player_repeat_current;
+       if (!parse_bool(buf, &player_repeat_current))
+               return;
+       if (old != player_repeat_current)
+               mpris_loop_status_changed();
+       update_statusline();
+}
+
+static void toggle_repeat_current(void *data)
+{
+       player_repeat_current ^= 1;
+       mpris_loop_status_changed();
+       update_statusline();
+}
+
+static void get_confirm_run(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[confirm_run], size);
+}
+
+static void set_confirm_run(void *data, const char *buf)
+{
+       parse_bool(buf, &confirm_run);
+}
+
+static void toggle_confirm_run(void *data)
+{
+       confirm_run ^= 1;
+}
+
+const char * const view_names[NR_VIEWS + 1] = {
+       "tree", "sorted", "playlist", "queue", "browser", "filters", "settings", NULL
+};
+
+static void get_play_library(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[play_library], size);
+}
+
+static void set_play_library(void *data, const char *buf)
+{
+       if (!parse_bool(buf, &play_library))
+               return;
+       update_statusline();
+}
+
+static void toggle_play_library(void *data)
+{
+       play_library ^= 1;
+       update_statusline();
+}
+
+static void get_play_sorted(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[play_sorted], size);
+}
+
+static void set_play_sorted(void *data, const char *buf)
+{
+       int tmp;
+
+       if (!parse_bool(buf, &tmp))
+               return;
+
+       play_sorted = tmp;
+
+       update_statusline();
+}
+
+static void toggle_play_sorted(void *data)
+{
+       play_sorted = play_sorted ^ 1;
+
+       /* shuffle would override play_sorted... */
+       if (play_sorted) {
+               /* play_sorted makes no sense in playlist */
+               play_library = 1;
+               shuffle = 0;
+       }
+
+       update_statusline();
+}
+
+static void get_smart_artist_sort(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[smart_artist_sort], size);
+}
+
+static void set_smart_artist_sort(void *data, const char *buf)
+{
+       if (parse_bool(buf, &smart_artist_sort))
+               tree_sort_artists();
+}
+
+static void toggle_smart_artist_sort(void *data)
+{
+       smart_artist_sort ^= 1;
+       tree_sort_artists();
+}
+
+static void get_display_artist_sort_name(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[display_artist_sort_name], size);
+}
+
+static void set_display_artist_sort_name(void *data, const char *buf)
+{
+       parse_bool(buf, &display_artist_sort_name);
+       lib_tree_win->changed = 1;
+}
+
+static void toggle_display_artist_sort_name(void *data)
+{
+       display_artist_sort_name ^= 1;
+       lib_tree_win->changed = 1;
+}
+
+const char * const aaa_mode_names[] = {
+       "all", "artist", "album", NULL
+};
+
+static void get_aaa_mode(void *data, char *buf, size_t size)
+{
+       strscpy(buf, aaa_mode_names[aaa_mode], size);
+}
+
+static void set_aaa_mode(void *data, const char *buf)
+{
+       int tmp;
+
+       if (!parse_enum(buf, 0, 2, aaa_mode_names, &tmp))
+               return;
+
+       aaa_mode = tmp;
+       update_statusline();
+}
+
+static void toggle_aaa_mode(void *data)
+{
+       /* aaa mode makes no sense in playlist */
+       play_library = 1;
+
+       aaa_mode++;
+       aaa_mode %= 3;
+       update_statusline();
+}
+
+static void get_repeat(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[repeat], size);
+}
+
+static void set_repeat(void *data, const char *buf)
+{
+       int old = repeat;
+       if (!parse_bool(buf, &repeat))
+               return;
+       if (!player_repeat_current && old != repeat)
+               mpris_loop_status_changed();
+       update_statusline();
+}
+
+static void toggle_repeat(void *data)
+{
+       repeat ^= 1;
+       if (!player_repeat_current)
+               mpris_loop_status_changed();
+       update_statusline();
+}
+
+static const char * const replaygain_names[] = {
+       "disabled", "track", "album", "track-preferred", "album-preferred", NULL
+};
+
+static void get_replaygain(void *data, char *buf, size_t size)
+{
+       strscpy(buf, replaygain_names[replaygain], size);
+}
+
+static void set_replaygain(void *data, const char *buf)
+{
+       int tmp;
+
+       if (!parse_enum(buf, 0, 4, replaygain_names, &tmp))
+               return;
+       player_set_rg(tmp);
+}
+
+static void toggle_replaygain(void *data)
+{
+       player_set_rg((replaygain + 1) % 5);
+}
+
+static void get_replaygain_limit(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[replaygain_limit], size);
+}
+
+static void set_replaygain_limit(void *data, const char *buf)
+{
+       int tmp;
+
+       if (!parse_bool(buf, &tmp))
+               return;
+       player_set_rg_limit(tmp);
+}
+
+static void toggle_replaygain_limit(void *data)
+{
+       player_set_rg_limit(replaygain_limit ^ 1);
+}
+
+static void get_resume(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[resume_cmus], size);
+}
+
+static void set_resume(void *data, const char *buf)
+{
+       parse_bool(buf, &resume_cmus);
+}
+
+static void toggle_resume(void *data)
+{
+       resume_cmus ^= 1;
+}
+
+static void get_show_hidden(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[show_hidden], size);
+}
+
+static void set_show_hidden(void *data, const char *buf)
+{
+       if (!parse_bool(buf, &show_hidden))
+               return;
+       browser_reload();
+}
+
+static void toggle_show_hidden(void *data)
+{
+       show_hidden ^= 1;
+       browser_reload();
+}
+
+static void set_show_all_tracks_int(int); /* defined below */
+
+static void get_auto_expand_albums_follow(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[auto_expand_albums_follow], size);
+}
+
+static void set_auto_expand_albums_follow_int(int value)
+{
+       auto_expand_albums_follow = !!value;
+       if (!auto_expand_albums_follow && !show_all_tracks)
+               set_show_all_tracks_int(1);
+}
+
+static void set_auto_expand_albums_follow(void *data, const char *buf)
+{
+       int tmp = 0;
+       parse_bool(buf, &tmp);
+       set_auto_expand_albums_follow_int(tmp);
+}
+
+static void toggle_auto_expand_albums_follow(void *data)
+{
+       set_auto_expand_albums_follow_int(!auto_expand_albums_follow);
+}
+
+static void get_auto_expand_albums_search(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[auto_expand_albums_search], size);
+}
+
+static void set_auto_expand_albums_search_int(int value)
+{
+       auto_expand_albums_search = !!value;
+       if (!auto_expand_albums_search && !show_all_tracks)
+               set_show_all_tracks_int(1);
+}
+
+static void set_auto_expand_albums_search(void *data, const char *buf)
+{
+       int tmp = 0;
+       parse_bool(buf, &tmp);
+       set_auto_expand_albums_search_int(tmp);
+}
+
+static void toggle_auto_expand_albums_search(void *data)
+{
+       set_auto_expand_albums_search_int(!auto_expand_albums_search);
+}
+
+static void get_auto_expand_albums_selcur(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[auto_expand_albums_selcur], size);
+}
+
+static void set_auto_expand_albums_selcur_int(int value)
+{
+       auto_expand_albums_selcur = !!value;
+       if (!auto_expand_albums_selcur && !show_all_tracks)
+               set_show_all_tracks_int(1);
+}
+
+static void set_auto_expand_albums_selcur(void *data, const char *buf)
+{
+       int tmp = 0;
+       parse_bool(buf, &tmp);
+       set_auto_expand_albums_selcur_int(tmp);
+}
+
+static void toggle_auto_expand_albums_selcur(void *data)
+{
+       set_auto_expand_albums_selcur_int(!auto_expand_albums_selcur);
+}
+
+
+static void get_show_all_tracks(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[show_all_tracks], size);
+}
+
+static void set_show_all_tracks_int(int value)
+{
+       value = !!value;
+       if (show_all_tracks == value)
+               return;
+       show_all_tracks = value;
+       if (!show_all_tracks) {
+               if  (!auto_expand_albums_follow)
+                       set_auto_expand_albums_follow_int(1);
+               if  (!auto_expand_albums_search)
+                       set_auto_expand_albums_search_int(1);
+               if  (!auto_expand_albums_selcur)
+                       set_auto_expand_albums_selcur_int(1);
+       }
+       tree_sel_update(0);
+}
+
+static void set_show_all_tracks(void *data, const char *buf)
+{
+       int tmp = 0;
+       parse_bool(buf, &tmp);
+       set_show_all_tracks_int(tmp);
+}
+
+static void toggle_show_all_tracks(void *data)
+{
+       set_show_all_tracks_int(!show_all_tracks);
+}
+
+static void get_show_current_bitrate(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[show_current_bitrate], size);
+}
+
+static void set_show_current_bitrate(void *data, const char *buf)
+{
+       if (parse_bool(buf, &show_current_bitrate))
+               update_statusline();
+}
+
+static void toggle_show_current_bitrate(void *data)
+{
+       show_current_bitrate ^= 1;
+       update_statusline();
+}
+
+static void get_show_playback_position(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[show_playback_position], size);
+}
+
+static void set_show_playback_position(void *data, const char *buf)
+{
+       if (!parse_bool(buf, &show_playback_position))
+               return;
+       update_statusline();
+}
+
+static void toggle_show_playback_position(void *data)
+{
+       show_playback_position ^= 1;
+       update_statusline();
+}
+
+static void get_show_remaining_time(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[show_remaining_time], size);
+}
+
+static void set_show_remaining_time(void *data, const char *buf)
+{
+       if (!parse_bool(buf, &show_remaining_time))
+               return;
+       update_statusline();
+}
+
+static void toggle_show_remaining_time(void *data)
+{
+       show_remaining_time ^= 1;
+       update_statusline();
+}
+
+static void get_set_term_title(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[set_term_title], size);
+}
+
+static void set_set_term_title(void *data, const char *buf)
+{
+       parse_bool(buf, &set_term_title);
+}
+
+static void toggle_set_term_title(void *data)
+{
+       set_term_title ^= 1;
+}
+
+static void get_shuffle(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[shuffle], size);
+}
+
+static void set_shuffle(void *data, const char *buf)
+{
+       int old = shuffle;
+       if (!parse_bool(buf, &shuffle))
+               return;
+       if (old != shuffle)
+               mpris_shuffle_changed();
+       update_statusline();
+}
+
+static void toggle_shuffle(void *data)
+{
+       shuffle ^= 1;
+       mpris_shuffle_changed();
+       update_statusline();
+}
+
+static void get_softvol(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[soft_vol], size);
+}
+
+static void do_set_softvol(int soft)
+{
+       if (!soft_vol)
+               mixer_close();
+       player_set_soft_vol(soft);
+       if (!soft_vol)
+               mixer_open();
+       update_statusline();
+}
+
+static void set_softvol(void *data, const char *buf)
+{
+       int soft;
+
+       if (!parse_bool(buf, &soft))
+               return;
+       do_set_softvol(soft);
+}
+
+static void toggle_softvol(void *data)
+{
+       do_set_softvol(soft_vol ^ 1);
+}
+
+static void get_wrap_search(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[wrap_search], size);
+}
+
+static void set_wrap_search(void *data, const char *buf)
+{
+       parse_bool(buf, &wrap_search);
+}
+
+static void toggle_wrap_search(void *data)
+{
+       wrap_search ^= 1;
+}
+
+static void get_skip_track_info(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[skip_track_info], size);
+}
+
+static void set_skip_track_info(void *data, const char *buf)
+{
+       parse_bool(buf, &skip_track_info);
+}
+
+static void toggle_skip_track_info(void *data)
+{
+       skip_track_info ^= 1;
+}
+
+void update_mouse(void)
+{
+       if (mouse) {
+               mouseinterval(25);
+               mousemask(BUTTON_CTRL | BUTTON_ALT
+                 | BUTTON1_PRESSED | BUTTON1_RELEASED | BUTTON1_CLICKED
+                 | BUTTON1_DOUBLE_CLICKED | BUTTON1_TRIPLE_CLICKED
+                 | BUTTON2_PRESSED | BUTTON2_RELEASED | BUTTON2_CLICKED
+                 | BUTTON3_PRESSED | BUTTON3_RELEASED | BUTTON3_CLICKED
+                 | BUTTON3_DOUBLE_CLICKED | BUTTON3_TRIPLE_CLICKED
+                 | BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED
+                 | BUTTON4_DOUBLE_CLICKED | BUTTON4_TRIPLE_CLICKED
+#if NCURSES_MOUSE_VERSION >= 2
+                 | BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED
+                 | BUTTON5_DOUBLE_CLICKED | BUTTON5_TRIPLE_CLICKED
+#endif
+                 , NULL);
+       } else {
+               mousemask(0, NULL);
+       }
+}
+
+static void get_mouse(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[mouse], size);
+}
+
+static void set_mouse(void *data, const char *buf)
+{
+       parse_bool(buf, &mouse);
+       update_mouse();
+}
+
+static void toggle_mouse(void *data)
+{
+       mouse ^= 1;
+       update_mouse();
+}
+
+static void get_mpris(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[mpris], size);
+}
+
+static void set_mpris(void *data, const char *buf)
+{
+       parse_bool(buf, &mpris);
+}
+
+static void toggle_mpris(void *data)
+{
+       mpris ^= 1;
+}
+
+static void get_time_show_leading_zero(void *data, char *buf, size_t size)
+{
+       strscpy(buf, bool_names[time_show_leading_zero], size);
+}
+
+static void set_time_show_leading_zero(void *data, const char *buf)
+{
+       if (!parse_bool(buf, &time_show_leading_zero))
+               return;
+       update_statusline();
+}
+
+static void toggle_time_show_leading_zero(void *data)
+{
+       time_show_leading_zero ^= 1;
+       update_statusline();
+}
+
+static void get_lib_add_filter(void *data, char *buf, size_t size)
+{
+       strscpy(buf, lib_add_filter ? lib_add_filter : "", size);
+}
+
+static void set_lib_add_filter(void *data, const char *buf)
+{
+       struct expr *expr = NULL;
+
+       if (strlen(buf) != 0) {
+               /* parse expression if non-empty string given */
+               expr = expr_parse(buf);
+
+               if (!expr)
+                       return;
+       }
+
+       free(lib_add_filter);
+       lib_add_filter = xstrdup(buf);
+
+       lib_set_add_filter(expr);
+}
+
+/* }}} */
+
+/* special callbacks (id set) {{{ */
+
+static const char * const color_enum_names[1 + 8 * 2 + 1] = {
+       "default",
+       "black", "red", "green", "yellow", "blue", "magenta", "cyan", "gray",
+       "darkgray", "lightred", "lightgreen", "lightyellow", "lightblue", "lightmagenta", "lightcyan", "white",
+       NULL
+};
+
+static void get_color(void *data, char *buf, size_t size)
+{
+       int val = *(int *)data;
+       if (val < 16) {
+               strscpy(buf, color_enum_names[val + 1], size);
+       } else {
+               buf_int(buf, val, size);
+       }
+}
+
+static void set_color(void *data, const char *buf)
+{
+       int color;
+
+       if (!parse_enum(buf, -1, 255, color_enum_names, &color))
+               return;
+
+       *(int *)data = color;
+       update_colors();
+       update_full();
+}
+
+static void get_start_view(void *data, char *buf, size_t size)
+{
+       strscpy(buf, view_names[start_view], size);
+}
+
+static void set_start_view(void *data, const char *buf)
+{
+       int view;
+
+       if (parse_enum(buf, 0, NR_VIEWS - 1, view_names, &view)) {
+               start_view = view;
+       }
+}
+
+static void get_attr(void *data, char *buf, size_t size)
+{
+       int attr = *(int *)data;
+
+       if (attr == 0) {
+               strscpy(buf, "default", size);
+               return;
+       }
+
+       const char *standout = "";
+       const char *underline = "";
+       const char *reverse = "";
+       const char *blink = "";
+       const char *bold = "";
+
+       if (attr & A_STANDOUT)
+               standout = "standout|";
+       if (attr & A_UNDERLINE)
+               underline = "underline|";
+       if (attr & A_REVERSE)
+               reverse = "reverse|";
+       if (attr & A_BLINK)
+               blink = "blink|";
+       if (attr & A_BOLD)
+               bold = "bold|";
+
+       size_t len = snprintf(buf, size, "%s%s%s%s%s", standout, underline, reverse,
+                       blink, bold);
+
+       if (0 < len && len < size)
+               buf[len - 1] = 0;
+}
+
+static void set_attr(void *data, const char *buf)
+{
+       int attr = 0;
+       size_t i = 0;
+       size_t offset = 0;
+       size_t length = 0;
+       char*  current;
+
+       do {
+               if (buf[i] == '|' || buf[i] == '\0') {
+                       current = xstrndup(&buf[offset], length);
+
+                       if (strcmp(current, "default") == 0)
+                               attr |= A_NORMAL;
+                       else if (strcmp(current, "standout") == 0)
+                               attr |= A_STANDOUT;
+                       else if (strcmp(current, "underline") == 0)
+                               attr |= A_UNDERLINE;
+                       else if (strcmp(current, "reverse") == 0)
+                               attr |= A_REVERSE;
+                       else if (strcmp(current, "blink") == 0)
+                               attr |= A_BLINK;
+                       else if (strcmp(current, "bold") == 0)
+                               attr |= A_BOLD;
+
+                       free(current);
+
+                       offset = i;
+                       length = -1;
+               }
+
+               i++;
+               length++;
+       } while (buf[i - 1] != '\0');
+
+       *(int *)data = attr;
+       update_colors();
+       update_full();
+}
+
+static char **id_to_fmt(enum format_id id)
+{
+       switch (id) {
+       case FMT_CURRENT_ALT:
+               return &current_alt_format;
+       case FMT_PLAYLIST_ALT:
+               return &list_win_alt_format;
+       case FMT_TITLE_ALT:
+               return &window_title_alt_format;
+       case FMT_TRACKWIN_ALT:
+               return &track_win_alt_format;
+       case FMT_CURRENT:
+               return &current_format;
+       case FMT_PLAYLIST:
+               return &list_win_format;
+       case FMT_PLAYLIST_VA:
+               return &list_win_format_va;
+       case FMT_TITLE:
+               return &window_title_format;
+       case FMT_TRACKWIN:
+               return &track_win_format;
+       case FMT_TRACKWIN_ALBUM:
+               return &track_win_album_format;
+       case FMT_TRACKWIN_VA:
+               return &track_win_format_va;
+       case FMT_TREEWIN:
+               return &tree_win_format;
+       case FMT_TREEWIN_ARTIST:
+               return &tree_win_artist_format;
+       case FMT_STATUSLINE:
+               return &statusline_format;
+       default:
+               die("unhandled format code: %d\n", id);
+       }
+       return NULL;
+}
+
+static void get_format(void *data, char *buf, size_t size)
+{
+       char **fmtp = data;
+
+       strscpy(buf, *fmtp, size);
+}
+
+static void set_format(void *data, const char *buf)
+{
+       char **fmtp = data;
+
+       if (!track_format_valid(buf)) {
+               error_msg("invalid format string");
+               return;
+       }
+       free(*fmtp);
+       *fmtp = xstrdup(buf);
+
+       update_full();
+}
+
+/* }}} */
+
+#define DN(name) { #name, get_ ## name, set_ ## name, NULL, 0 },
+#define DN_FLAGS(name, flags) { #name, get_ ## name, set_ ## name, NULL, flags },
+#define DT(name) { #name, get_ ## name, set_ ## name, toggle_ ## name, 0 },
+
+static const struct {
+       const char *name;
+       opt_get_cb get;
+       opt_set_cb set;
+       opt_toggle_cb toggle;
+       unsigned int flags;
+} simple_options[] = {
+       DT(aaa_mode)
+       DT(auto_reshuffle)
+       DN_FLAGS(device, OPT_PROGRAM_PATH)
+       DN(buffer_seconds)
+       DN(scroll_offset)
+       DN(rewind_offset)
+       DT(confirm_run)
+       DT(continue)
+       DT(smart_artist_sort)
+       DN(id3_default_charset)
+       DN(icecast_default_charset)
+       DN(lib_sort)
+       DN(output_plugin)
+       DN(passwd)
+       DN(pl_sort)
+       DT(play_library)
+       DT(play_sorted)
+       DT(display_artist_sort_name)
+       DT(repeat)
+       DT(repeat_current)
+       DT(replaygain)
+       DT(replaygain_limit)
+       DN(replaygain_preamp)
+       DT(resume)
+       DT(show_hidden)
+       DT(auto_expand_albums_follow)
+       DT(auto_expand_albums_search)
+       DT(auto_expand_albums_selcur)
+       DT(show_all_tracks)
+       DT(show_current_bitrate)
+       DT(show_playback_position)
+       DT(show_remaining_time)
+       DT(set_term_title)
+       DT(shuffle)
+       DT(follow)
+       DT(softvol)
+       DN(softvol_state)
+       DN_FLAGS(status_display_program, OPT_PROGRAM_PATH)
+       DT(wrap_search)
+       DT(skip_track_info)
+       DT(mouse)
+       DT(mpris)
+       DT(time_show_leading_zero)
+       DN(lib_add_filter)
+       DN(start_view)
+       { NULL, NULL, NULL, NULL, 0 }
+};
+
+static const char * const color_names[NR_COLORS] = {
+       "color_cmdline_bg",
+       "color_cmdline_fg",
+       "color_error",
+       "color_info",
+       "color_separator",
+       "color_statusline_bg",
+       "color_statusline_fg",
+       "color_titleline_bg",
+       "color_titleline_fg",
+       "color_win_bg",
+       "color_win_cur",
+       "color_win_cur_sel_bg",
+       "color_win_cur_sel_fg",
+       "color_win_dir",
+       "color_win_fg",
+       "color_win_inactive_cur_sel_bg",
+       "color_win_inactive_cur_sel_fg",
+       "color_win_inactive_sel_bg",
+       "color_win_inactive_sel_fg",
+       "color_win_sel_bg",
+       "color_win_sel_fg",
+       "color_win_title_bg",
+       "color_win_title_fg",
+       "color_trackwin_album_bg",
+       "color_trackwin_album_fg",
+};
+
+static const char * const attr_names[NR_ATTRS] = {
+       "color_cmdline_attr",
+       "color_statusline_attr",
+       "color_titleline_attr",
+       "color_win_attr",
+       "color_win_cur_sel_attr",
+       "color_cur_sel_attr",
+       "color_win_inactive_cur_sel_attr",
+       "color_win_inactive_sel_attr",
+       "color_win_sel_attr",
+       "color_win_title_attr",
+       "color_trackwin_album_attr",
+       "color_win_cur_attr",
+};
+
+LIST_HEAD(option_head);
+int nr_options = 0;
+
+void option_add(const char *name, const void *data, opt_get_cb get,
+               opt_set_cb set, opt_toggle_cb toggle, unsigned int flags)
+{
+       struct cmus_opt *opt = xnew(struct cmus_opt, 1);
+       struct list_head *item;
+
+       opt->name = name;
+       opt->data = (void *)data;
+       opt->get = get;
+       opt->set = set;
+       opt->toggle = toggle;
+       opt->flags = flags;
+
+       item = option_head.next;
+       while (item != &option_head) {
+               struct cmus_opt *o = container_of(item, struct cmus_opt, node);
+
+               if (strcmp(name, o->name) < 0)
+                       break;
+               item = item->next;
+       }
+       /* add before item */
+       list_add_tail(&opt->node, item);
+       nr_options++;
+}
+
+struct cmus_opt *option_find(const char *name)
+{
+       struct cmus_opt *opt = option_find_silent(name);
+       if (opt == NULL)
+               error_msg("no such option %s", name);
+       return opt;
+}
+
+struct cmus_opt *option_find_silent(const char *name)
+{
+       struct cmus_opt *opt;
+
+       list_for_each_entry(opt, &option_head, node) {
+               if (strcmp(name, opt->name) == 0)
+                       return opt;
+       }
+       return NULL;
+}
+
+void option_set(const char *name, const char *value)
+{
+       struct cmus_opt *opt = option_find(name);
+
+       if (opt)
+               opt->set(opt->data, value);
+}
+
+void options_add(void)
+{
+       int i;
+
+       for (i = 0; simple_options[i].name; i++)
+               option_add(simple_options[i].name, NULL, simple_options[i].get,
+                               simple_options[i].set, simple_options[i].toggle,
+                               simple_options[i].flags);
+
+       for (i = 0; i < NR_FMTS; i++)
+               option_add(str_defaults[i].name, id_to_fmt(i), get_format,
+                               set_format, NULL, 0);
+
+       for (i = 0; i < NR_COLORS; i++)
+               option_add(color_names[i], &colors[i], get_color, set_color,
+                               NULL, 0);
+
+       for (i = 0; i < NR_ATTRS; i++)
+               option_add(attr_names[i], &attrs[i], get_attr, set_attr, NULL,
+                               0);
+
+       ip_add_options();
+       op_add_options();
+}
+
+static int handle_line(void *data, const char *line)
+{
+       run_command(line);
+       return 0;
+}
+
+int source_file(const char *filename)
+{
+       return file_for_each_line(filename, handle_line, NULL);
+}
+
+void options_load(void)
+{
+       char filename[512];
+       int i;
+
+       /* initialize those that can't be statically initialized */
+       cdda_device = get_default_cdda_device();
+       for (i = 0; str_defaults[i].name; i++)
+               option_set(str_defaults[i].name, str_defaults[i].value);
+
+       /* load autosave config */
+       snprintf(filename, sizeof(filename), "%s/autosave", cmus_config_dir);
+       if (source_file(filename) == -1) {
+               char *def = xstrjoin(cmus_data_dir, "/rc");
+
+               if (errno != ENOENT)
+                       error_msg("loading %s: %s", filename, strerror(errno));
+
+               /* load defaults */
+               if (source_file(def) == -1)
+                       die_errno("loading %s", def);
+
+               free(def);
+       }
+
+       /* load optional static config */
+       snprintf(filename, sizeof(filename), "%s/rc", cmus_config_dir);
+       if (source_file(filename) == -1) {
+               if (errno != ENOENT)
+                       error_msg("loading %s: %s", filename, strerror(errno));
+       }
+}
+
+void options_exit(void)
+{
+       struct cmus_opt *opt;
+       struct filter_entry *filt;
+       char filename_tmp[512];
+       char filename[512];
+       FILE *f;
+       int i;
+
+       snprintf(filename_tmp, sizeof(filename_tmp), "%s/autosave.tmp", cmus_config_dir);
+       f = fopen(filename_tmp, "w");
+       if (f == NULL) {
+               warn_errno("creating %s", filename_tmp);
+               return;
+       }
+
+       /* save options */
+       list_for_each_entry(opt, &option_head, node) {
+               char buf[OPTION_MAX_SIZE];
+
+               buf[0] = 0;
+               opt->get(opt->data, buf, OPTION_MAX_SIZE);
+               fprintf(f, "set %s=%s\n", opt->name, buf);
+       }
+
+       /* save key bindings */
+       for (i = 0; i < NR_CTXS; i++) {
+               struct binding *b = key_bindings[i];
+
+               while (b) {
+                       fprintf(f, "bind %s %s %s\n", key_context_names[i], b->key->name, b->cmd);
+                       b = b->next;
+               }
+       }
+
+       /* save filters */
+       list_for_each_entry(filt, &filters_head, node)
+               fprintf(f, "fset %s=%s\n", filt->name, filt->filter);
+       fprintf(f, "factivate");
+       list_for_each_entry(filt, &filters_head, node) {
+               switch (filt->act_stat) {
+               case FS_YES:
+                       fprintf(f, " %s", filt->name);
+                       break;
+               case FS_NO:
+                       fprintf(f, " !%s", filt->name);
+                       break;
+               }
+       }
+       fprintf(f, "\n");
+
+       fclose(f);
+
+       snprintf(filename, sizeof(filename), "%s/autosave", cmus_config_dir);
+       i = rename(filename_tmp, filename);
+       if (i)
+               warn_errno("renaming %s to %s", filename_tmp, filename);
+}
+
+struct resume {
+       enum player_status status;
+       char *filename;
+       long int position;
+       char *lib_filename;
+       int view;
+       char *live_filter;
+       char *browser_dir;
+       char *marked_pl;
+};
+
+static int handle_resume_line(void *data, const char *line)
+{
+       struct resume *resume = data;
+       char *cmd, *arg;
+
+       if (!parse_command(line, &cmd, &arg))
+               return 0;
+       if (!arg)
+               goto out;
+
+       if (strcmp(cmd, "status") == 0) {
+               parse_enum(arg, 0, NR_PLAYER_STATUS, player_status_names, (int *) &resume->status);
+       } else if (strcmp(cmd, "file") == 0) {
+               free(resume->filename);
+               resume->filename = xstrdup(unescape(arg));
+       } else if (strcmp(cmd, "position") == 0) {
+               str_to_int(arg, &resume->position);
+       } else if (strcmp(cmd, "lib_file") == 0) {
+               free(resume->lib_filename);
+               resume->lib_filename = xstrdup(unescape(arg));
+       } else if (strcmp(cmd, "view") == 0) {
+               parse_enum(arg, 0, NR_VIEWS, view_names, &resume->view);
+       } else if (strcmp(cmd, "live-filter") == 0) {
+               free(resume->live_filter);
+               resume->live_filter = xstrdup(unescape(arg));
+       } else if (strcmp(cmd, "browser-dir") == 0) {
+               free(resume->browser_dir);
+               resume->browser_dir = xstrdup(unescape(arg));
+       } else if (strcmp(cmd, "marked-pl") == 0) {
+               free(resume->marked_pl);
+               resume->marked_pl = xstrdup(unescape(arg));
+       }
+
+       free(arg);
+out:
+       free(cmd);
+       return 0;
+}
+
+void resume_load(void)
+{
+       char filename[512];
+       struct track_info *ti, *old;
+       struct resume resume = { .status = PLAYER_STATUS_STOPPED, .view = -1 };
+
+       snprintf(filename, sizeof(filename), "%s/resume", cmus_config_dir);
+       if (file_for_each_line(filename, handle_resume_line, &resume) == -1) {
+               if (errno != ENOENT)
+                       error_msg("loading %s: %s", filename, strerror(errno));
+               return;
+       }
+       if (resume.view >= 0 && resume.view != cur_view)
+               set_view(resume.view);
+       if (resume.lib_filename) {
+               cache_lock();
+               ti = old = cache_get_ti(resume.lib_filename, 0);
+               cache_unlock();
+               if (ti) {
+                       lib_add_track(ti, NULL);
+                       track_info_unref(ti);
+                       lib_store_cur_track(ti);
+                       track_info_unref(ti);
+                       ti = lib_set_track(lib_find_track(ti));
+                       if (ti) {
+                               BUG_ON(ti != old);
+                               track_info_unref(ti);
+                               tree_sel_current(auto_expand_albums_follow);
+                               sorted_sel_current();
+                       }
+               }
+               free(resume.lib_filename);
+       }
+       if (resume.filename) {
+               cache_lock();
+               ti = cache_get_ti(resume.filename, 0);
+               cache_unlock();
+               if (ti) {
+                       player_set_file(ti);
+                       if (resume.status != PLAYER_STATUS_STOPPED)
+                               player_seek(resume.position, 0, resume.status == PLAYER_STATUS_PLAYING);
+               }
+               free(resume.filename);
+       }
+       if (resume.live_filter) {
+               filters_set_live(resume.live_filter);
+               free(resume.live_filter);
+       }
+       if (resume.browser_dir) {
+               browser_chdir(resume.browser_dir);
+               free(resume.browser_dir);
+       }
+       if (resume.marked_pl) {
+               pl_set_marked_pl_by_name(resume.marked_pl);
+               free(resume.marked_pl);
+       }
+}
+
+void resume_exit(void)
+{
+       char filename_tmp[512];
+       char filename[512];
+       struct track_info *ti;
+       FILE *f;
+       int rc;
+
+       snprintf(filename_tmp, sizeof(filename_tmp), "%s/resume.tmp", cmus_config_dir);
+       f = fopen(filename_tmp, "w");
+       if (!f) {
+               warn_errno("creating %s", filename_tmp);
+               return;
+       }
+
+       fprintf(f, "status %s\n", player_status_names[player_info.status]);
+       ti = player_info.ti;
+       if (ti) {
+               fprintf(f, "file %s\n", escape(ti->filename));
+               fprintf(f, "position %d\n", player_info.pos);
+       }
+       if (lib_cur_track)
+               ti = tree_track_info(lib_cur_track);
+       else
+               ti = lib_get_cur_stored_track();
+       if (ti)
+               fprintf(f, "lib_file %s\n", escape(ti->filename));
+       fprintf(f, "view %s\n", view_names[cur_view]);
+       if (lib_live_filter)
+               fprintf(f, "live-filter %s\n", escape(lib_live_filter));
+       fprintf(f, "browser-dir %s\n", escape(browser_dir));
+
+       fprintf(f, "marked-pl %s\n", escape(pl_marked_pl_name()));
+
+       fclose(f);
+
+       snprintf(filename, sizeof(filename), "%s/resume", cmus_config_dir);
+       rc = rename(filename_tmp, filename);
+       if (rc)
+               warn_errno("renaming %s to %s", filename_tmp, filename);
+}
diff --git a/options.h b/options.h
new file mode 100644 (file)
index 0000000..ad16e43
--- /dev/null
+++ b/options.h
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_OPTIONS_H
+#define CMUS_OPTIONS_H
+
+#include "list.h"
+
+#define OPTION_MAX_SIZE        4096
+
+typedef void (*opt_get_cb)(void *data, char *buf, size_t size);
+typedef void (*opt_set_cb)(void *data, const char *buf);
+typedef void (*opt_toggle_cb)(void *data);
+
+enum {
+       OPT_PROGRAM_PATH = 1 << 0,
+};
+
+struct cmus_opt {
+       struct list_head node;
+
+       const char *name;
+
+       /* If there are many similar options you should write generic get(),
+        * set() and optionally toggle() and distinguish the concrete option
+        * via this pointer.
+        */
+       void *data;
+
+       opt_get_cb get;
+       opt_set_cb set;
+
+       /* NULL if not toggle-able */
+       opt_toggle_cb toggle;
+
+       unsigned int flags;
+};
+
+extern struct list_head option_head;
+extern int nr_options;
+
+enum {
+       TREE_VIEW,
+       SORTED_VIEW,
+       PLAYLIST_VIEW,
+       QUEUE_VIEW,
+       BROWSER_VIEW,
+       FILTERS_VIEW,
+       HELP_VIEW,
+       NR_VIEWS
+};
+
+enum {
+       COLOR_CMDLINE_BG,
+       COLOR_CMDLINE_FG,
+       COLOR_ERROR,
+       COLOR_INFO,
+
+       COLOR_SEPARATOR,
+       COLOR_STATUSLINE_BG,
+       COLOR_STATUSLINE_FG,
+       COLOR_TITLELINE_BG,
+
+       COLOR_TITLELINE_FG,
+       COLOR_WIN_BG,
+       COLOR_WIN_CUR,
+       COLOR_WIN_CUR_SEL_BG,
+
+       COLOR_WIN_CUR_SEL_FG,
+       COLOR_WIN_DIR,
+       COLOR_WIN_FG,
+       COLOR_WIN_INACTIVE_CUR_SEL_BG,
+
+       COLOR_WIN_INACTIVE_CUR_SEL_FG,
+       COLOR_WIN_INACTIVE_SEL_BG,
+       COLOR_WIN_INACTIVE_SEL_FG,
+       COLOR_WIN_SEL_BG,
+
+       COLOR_WIN_SEL_FG,
+       COLOR_WIN_TITLE_BG,
+       COLOR_WIN_TITLE_FG,
+       COLOR_TRACKWIN_ALBUM_BG,
+
+       COLOR_TRACKWIN_ALBUM_FG,
+       NR_COLORS
+};
+
+enum {
+       COLOR_CMDLINE_ATTR,
+       COLOR_STATUSLINE_ATTR,
+       COLOR_TITLELINE_ATTR,
+       COLOR_WIN_ATTR,
+       COLOR_WIN_CUR_SEL_ATTR,
+       COLOR_CUR_SEL_ATTR,
+       COLOR_WIN_INACTIVE_CUR_SEL_ATTR,
+       COLOR_WIN_INACTIVE_SEL_ATTR,
+       COLOR_WIN_SEL_ATTR,
+       COLOR_WIN_TITLE_ATTR,
+       COLOR_TRACKWIN_ALBUM_ATTR,
+       COLOR_WIN_CUR_ATTR,
+       NR_ATTRS
+};
+
+#define BRIGHT (1 << 3)
+
+extern char *cdda_device;
+extern char *output_plugin;
+extern char *status_display_program;
+extern char *server_password;
+extern int auto_expand_albums_follow;
+extern int auto_expand_albums_search;
+extern int auto_expand_albums_selcur;
+extern int show_all_tracks;
+extern int auto_reshuffle;
+extern int confirm_run;
+extern int resume_cmus;
+extern int show_hidden;
+extern int show_current_bitrate;
+extern int show_playback_position;
+extern int show_remaining_time;
+extern int set_term_title;
+extern int wrap_search;
+extern int play_library;
+extern int repeat;
+extern int shuffle;
+extern int follow;
+extern int display_artist_sort_name;
+extern int smart_artist_sort;
+extern int scroll_offset;
+extern int rewind_offset;
+extern int skip_track_info;
+extern int mouse;
+extern int mpris;
+extern int time_show_leading_zero;
+extern int start_view;
+
+extern const char * const aaa_mode_names[];
+extern const char * const view_names[NR_VIEWS + 1];
+
+extern int colors[NR_COLORS];
+extern int attrs[NR_ATTRS];
+
+/* format string for tree window (tree view) */
+extern char *tree_win_format;
+extern char *tree_win_artist_format;
+
+/* format string for track window (tree view) */
+extern char *track_win_album_format;
+extern char *track_win_format;
+extern char *track_win_format_va;
+extern char *track_win_alt_format;
+
+/* format string for shuffle, sorted and play queue views */
+extern char *list_win_format;
+extern char *list_win_format_va;
+extern char *list_win_alt_format;
+
+/* format string for currently playing track */
+extern char *current_format;
+extern char *current_alt_format;
+
+/* format string for status line */
+extern char *statusline_format;
+
+/* format string for window title */
+extern char *window_title_format;
+extern char *window_title_alt_format;
+
+extern char *id3_default_charset;
+extern char *icecast_default_charset;
+
+/* build option list */
+void options_add(void);
+
+/* load options from the config file */
+void options_load(void);
+
+int source_file(const char *filename);
+
+/* save options */
+void options_exit(void);
+
+/* load resume file */
+void resume_load(void);
+/* save resume file */
+void resume_exit(void);
+
+void option_add(const char *name, const void *data, opt_get_cb get,
+               opt_set_cb set, opt_toggle_cb toggle, unsigned int flags);
+struct cmus_opt *option_find(const char *name);
+struct cmus_opt *option_find_silent(const char *name);
+void option_set(const char *name, const char *value);
+int parse_enum(const char *buf, int minval, int maxval, const char * const names[], int *val);
+
+void update_mouse(void);
+
+#endif
diff --git a/output.c b/output.c
new file mode 100644 (file)
index 0000000..56f9d86
--- /dev/null
+++ b/output.c
@@ -0,0 +1,486 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "output.h"
+#include "op.h"
+#include "mixer.h"
+#include "sf.h"
+#include "utils.h"
+#include "xmalloc.h"
+#include "list.h"
+#include "debug.h"
+#include "ui_curses.h"
+#include "options.h"
+#include "xstrjoin.h"
+#include "misc.h"
+
+#include <string.h>
+#include <strings.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <dlfcn.h>
+
+struct output_plugin {
+       struct list_head node;
+       char *name;
+       void *handle;
+
+       const struct output_plugin_ops *pcm_ops;
+       const struct mixer_plugin_ops *mixer_ops;
+       const struct output_plugin_opt *pcm_options;
+       const struct mixer_plugin_opt *mixer_options;
+       int priority;
+
+       unsigned int pcm_initialized : 1;
+       unsigned int mixer_initialized : 1;
+       unsigned int mixer_open : 1;
+};
+
+static const char *plugin_dir;
+static LIST_HEAD(op_head);
+static struct output_plugin *op = NULL;
+
+/* volume is between 0 and volume_max */
+int volume_max = 0;
+int volume_l = -1;
+int volume_r = -1;
+
+static void add_plugin(struct output_plugin *plugin)
+{
+       struct list_head *item = op_head.next;
+
+       while (item != &op_head) {
+               struct output_plugin *o = container_of(item, struct output_plugin, node);
+
+               if (plugin->priority < o->priority)
+                       break;
+               item = item->next;
+       }
+
+       /* add before item */
+       list_add_tail(&plugin->node, item);
+}
+
+void op_load_plugins(void)
+{
+       DIR *dir;
+       struct dirent *d;
+
+       plugin_dir = xstrjoin(cmus_lib_dir, "/op");
+       dir = opendir(plugin_dir);
+       if (dir == NULL) {
+               error_msg("couldn't open directory `%s': %s", plugin_dir, strerror(errno));
+               return;
+       }
+       while ((d = (struct dirent *) readdir(dir)) != NULL) {
+               char filename[256];
+               struct output_plugin *plug;
+               void *so, *symptr;
+               char *ext;
+               const unsigned *abi_version_ptr;
+               bool err = false;
+
+               if (d->d_name[0] == '.')
+                       continue;
+               ext = strrchr(d->d_name, '.');
+               if (ext == NULL)
+                       continue;
+               if (strcmp(ext, ".so"))
+                       continue;
+
+               snprintf(filename, sizeof(filename), "%s/%s", plugin_dir, d->d_name);
+
+               so = dlopen(filename, RTLD_NOW);
+               if (so == NULL) {
+                       d_print("%s: %s\n", filename, dlerror());
+                       continue;
+               }
+
+               plug = xnew(struct output_plugin, 1);
+
+               plug->pcm_ops = dlsym(so, "op_pcm_ops");
+               plug->pcm_options = dlsym(so, "op_pcm_options");
+               symptr = dlsym(so, "op_priority");
+               abi_version_ptr = dlsym(so, "op_abi_version");
+               if (!plug->pcm_ops || !plug->pcm_options || !symptr) {
+                       error_msg("%s: missing symbol", filename);
+                       err = true;
+               }
+               if (!abi_version_ptr || *abi_version_ptr != OP_ABI_VERSION) {
+                       error_msg("%s: incompatible plugin version", filename);
+                       err = true;
+               }
+               if (err) {
+                       free(plug);
+                       dlclose(so);
+                       continue;
+               }
+               plug->priority = *(int *)symptr;
+
+               plug->mixer_ops = dlsym(so, "op_mixer_ops");
+               plug->mixer_options = dlsym(so, "op_mixer_options");
+               if (plug->mixer_ops == NULL || plug->mixer_options == NULL) {
+                       plug->mixer_ops = NULL;
+                       plug->mixer_options = NULL;
+               }
+
+               plug->name = xstrndup(d->d_name, ext - d->d_name);
+               plug->handle = so;
+               plug->pcm_initialized = 0;
+               plug->mixer_initialized = 0;
+               plug->mixer_open = 0;
+
+               add_plugin(plug);
+       }
+       closedir(dir);
+}
+
+static void init_plugin(struct output_plugin *o)
+{
+       if (!o->mixer_initialized && o->mixer_ops) {
+               if (o->mixer_ops->init() == 0) {
+                       d_print("initialized mixer for %s\n", o->name);
+                       o->mixer_initialized = 1;
+               } else {
+                       d_print("could not initialize mixer `%s'\n", o->name);
+               }
+       }
+       if (!o->pcm_initialized) {
+               if (o->pcm_ops->init() == 0) {
+                       d_print("initialized pcm for %s\n", o->name);
+                       o->pcm_initialized = 1;
+               } else {
+                       d_print("could not initialize pcm `%s'\n", o->name);
+               }
+       }
+}
+
+void op_exit_plugins(void)
+{
+       struct output_plugin *o;
+
+       list_for_each_entry(o, &op_head, node) {
+               if (o->mixer_initialized && o->mixer_ops)
+                       o->mixer_ops->exit();
+               if (o->pcm_initialized)
+                       o->pcm_ops->exit();
+       }
+}
+
+void mixer_close(void)
+{
+       volume_max = 0;
+       if (op && op->mixer_open) {
+               BUG_ON(op->mixer_ops == NULL);
+               op->mixer_ops->close();
+               op->mixer_open = 0;
+       }
+}
+
+void mixer_open(void)
+{
+       if (op == NULL)
+               return;
+
+       BUG_ON(op->mixer_open);
+       if (op->mixer_ops && op->mixer_initialized) {
+               int rc;
+
+               rc = op->mixer_ops->open(&volume_max);
+               if (rc == 0) {
+                       op->mixer_open = 1;
+                       mixer_read_volume();
+               } else {
+                       volume_max = 0;
+               }
+       }
+}
+
+static int select_plugin(struct output_plugin *o)
+{
+       /* try to initialize if not initialized yet */
+       init_plugin(o);
+
+       if (!o->pcm_initialized)
+               return -OP_ERROR_NOT_INITIALIZED;
+       op = o;
+       return 0;
+}
+
+int op_select(const char *name)
+{
+       struct output_plugin *o;
+
+       list_for_each_entry(o, &op_head, node) {
+               if (strcasecmp(name, o->name) == 0)
+                       return select_plugin(o);
+       }
+       return -OP_ERROR_NO_PLUGIN;
+}
+
+int op_select_any(void)
+{
+       struct output_plugin *o;
+       int rc = -OP_ERROR_NO_PLUGIN;
+       sample_format_t sf = sf_channels(2) | sf_rate(44100) | sf_bits(16) | sf_signed(1);
+
+       list_for_each_entry(o, &op_head, node) {
+               rc = select_plugin(o);
+               if (rc != 0)
+                       continue;
+               rc = o->pcm_ops->open(sf, NULL);
+               if (rc == 0) {
+                       o->pcm_ops->close();
+                       break;
+               }
+       }
+       return rc;
+}
+
+int op_open(sample_format_t sf, const channel_position_t *channel_map)
+{
+       if (op == NULL)
+               return -OP_ERROR_NOT_INITIALIZED;
+       return op->pcm_ops->open(sf, channel_map);
+}
+
+int op_drop(void)
+{
+       if (op->pcm_ops->drop == NULL)
+               return -OP_ERROR_NOT_SUPPORTED;
+       return op->pcm_ops->drop();
+}
+
+int op_close(void)
+{
+       return op->pcm_ops->close();
+}
+
+int op_write(const char *buffer, int count)
+{
+       return op->pcm_ops->write(buffer, count);
+}
+
+int op_pause(void)
+{
+       if (op->pcm_ops->pause == NULL)
+               return 0;
+       return op->pcm_ops->pause();
+}
+
+int op_unpause(void)
+{
+       if (op->pcm_ops->unpause == NULL)
+               return 0;
+       return op->pcm_ops->unpause();
+}
+
+int op_buffer_space(void)
+{
+       return op->pcm_ops->buffer_space();
+}
+
+int mixer_set_volume(int left, int right)
+{
+       if (op == NULL)
+               return -OP_ERROR_NOT_INITIALIZED;
+       if (!op->mixer_open)
+               return -OP_ERROR_NOT_OPEN;
+       return op->mixer_ops->set_volume(left, right);
+}
+
+int mixer_read_volume(void)
+{
+       if (op == NULL)
+               return -OP_ERROR_NOT_INITIALIZED;
+       if (!op->mixer_open)
+               return -OP_ERROR_NOT_OPEN;
+       return op->mixer_ops->get_volume(&volume_l, &volume_r);
+}
+
+int mixer_get_fds(int *fds)
+{
+       if (op == NULL)
+               return -OP_ERROR_NOT_INITIALIZED;
+       if (!op->mixer_open)
+               return -OP_ERROR_NOT_OPEN;
+       if (!op->mixer_ops->get_fds)
+               return -OP_ERROR_NOT_SUPPORTED;
+       return op->mixer_ops->get_fds(fds);
+}
+
+extern int soft_vol;
+
+static void option_error(int rc)
+{
+       char *msg = op_get_error_msg(rc, "setting option");
+       error_msg("%s", msg);
+       free(msg);
+}
+
+static void set_dsp_option(void *data, const char *val)
+{
+       const struct output_plugin_opt *o = data;
+       int rc;
+
+       rc = o->set(val);
+       if (rc)
+               option_error(rc);
+}
+
+static bool option_of_current_mixer(const struct mixer_plugin_opt *opt)
+{
+       const struct mixer_plugin_opt *mpo;
+
+       if (!op)
+               return false;
+       for (mpo = op->mixer_options; mpo && mpo->name; mpo++) {
+               if (mpo == opt)
+                       return true;
+       }
+       return false;
+}
+
+static void set_mixer_option(void *data, const char *val)
+{
+       const struct mixer_plugin_opt *o = data;
+       int rc;
+
+       rc = o->set(val);
+       if (rc) {
+               option_error(rc);
+       } else if (option_of_current_mixer(o)) {
+               /* option of the current op was set
+                * try to reopen the mixer */
+               mixer_close();
+               if (!soft_vol)
+                       mixer_open();
+       }
+}
+
+static void get_dsp_option(void *data, char *buf, size_t size)
+{
+       const struct output_plugin_opt *o = data;
+       char *val = NULL;
+
+       o->get(&val);
+       if (val) {
+               strscpy(buf, val, size);
+               free(val);
+       }
+}
+
+static void get_mixer_option(void *data, char *buf, size_t size)
+{
+       const struct mixer_plugin_opt *o = data;
+       char *val = NULL;
+
+       o->get(&val);
+       if (val) {
+               strscpy(buf, val, size);
+               free(val);
+       }
+}
+
+void op_add_options(void)
+{
+       struct output_plugin *o;
+       const struct output_plugin_opt *opo;
+       const struct mixer_plugin_opt *mpo;
+       char key[64];
+
+       list_for_each_entry(o, &op_head, node) {
+               for (opo = o->pcm_options; opo->name; opo++) {
+                       snprintf(key, sizeof(key), "dsp.%s.%s", o->name,
+                                       opo->name);
+                       option_add(xstrdup(key), opo, get_dsp_option,
+                                       set_dsp_option, NULL, 0);
+               }
+               for (mpo = o->mixer_options; mpo && mpo->name; mpo++) {
+                       snprintf(key, sizeof(key), "mixer.%s.%s", o->name,
+                                       mpo->name);
+                       option_add(xstrdup(key), mpo, get_mixer_option,
+                                       set_mixer_option, NULL, 0);
+               }
+       }
+}
+
+char *op_get_error_msg(int rc, const char *arg)
+{
+       char buffer[1024];
+
+       switch (-rc) {
+       case OP_ERROR_ERRNO:
+               snprintf(buffer, sizeof(buffer), "%s: %s", arg, strerror(errno));
+               break;
+       case OP_ERROR_NO_PLUGIN:
+               snprintf(buffer, sizeof(buffer),
+                               "%s: no such plugin", arg);
+               break;
+       case OP_ERROR_NOT_INITIALIZED:
+               snprintf(buffer, sizeof(buffer),
+                               "%s: couldn't initialize required output plugin", arg);
+               break;
+       case OP_ERROR_NOT_SUPPORTED:
+               snprintf(buffer, sizeof(buffer),
+                               "%s: function not supported", arg);
+               break;
+       case OP_ERROR_NOT_OPEN:
+               snprintf(buffer, sizeof(buffer),
+                               "%s: mixer is not open", arg);
+               break;
+       case OP_ERROR_SAMPLE_FORMAT:
+               snprintf(buffer, sizeof(buffer),
+                               "%s: sample format not supported", arg);
+               break;
+       case OP_ERROR_NOT_OPTION:
+               snprintf(buffer, sizeof(buffer),
+                               "%s: no such option", arg);
+               break;
+       case OP_ERROR_INTERNAL:
+               snprintf(buffer, sizeof(buffer), "%s: internal error", arg);
+               break;
+       case OP_ERROR_SUCCESS:
+       default:
+               snprintf(buffer, sizeof(buffer),
+                               "%s: this is not an error (%d), this is a bug",
+                               arg, rc);
+               break;
+       }
+       return xstrdup(buffer);
+}
+
+void op_dump_plugins(void)
+{
+       struct output_plugin *o;
+
+       printf("\nOutput Plugins: %s\n", plugin_dir);
+       list_for_each_entry(o, &op_head, node) {
+               printf("  %s\n", o->name);
+       }
+}
+
+const char *op_get_current(void)
+{
+       if (op)
+               return op->name;
+       return NULL;
+}
diff --git a/output.h b/output.h
new file mode 100644 (file)
index 0000000..6cf26a3
--- /dev/null
+++ b/output.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_OUTPUT_H
+#define CMUS_OUTPUT_H
+
+#include "sf.h"
+#include "channelmap.h"
+
+extern int volume_max;
+extern int volume_l;
+extern int volume_r;
+
+void op_load_plugins(void);
+void op_exit_plugins(void);
+
+/*
+ * select output plugin and open its mixer
+ *
+ * errors: OP_ERROR_{ERRNO, NO_PLUGIN}
+ */
+int op_select(const char *name);
+int op_select_any(void);
+
+/*
+ * open selected plugin
+ *
+ * errors: OP_ERROR_{}
+ */
+int op_open(sample_format_t sf, const channel_position_t *channel_map);
+
+/*
+ * drop pcm data
+ *
+ * errors: OP_ERROR_{ERRNO}
+ */
+int op_drop(void);
+
+/*
+ * close plugin
+ *
+ * errors: OP_ERROR_{}
+ */
+int op_close(void);
+
+/*
+ * returns bytes written or error
+ *
+ * errors: OP_ERROR_{ERRNO}
+ */
+int op_write(const char *buffer, int count);
+
+/*
+ * errors: OP_ERROR_{}
+ */
+int op_pause(void);
+int op_unpause(void);
+
+/*
+ * returns space in output buffer in bytes or -1 if busy
+ */
+int op_buffer_space(void);
+
+/*
+ * errors: OP_ERROR_{}
+ */
+int op_reset(void);
+
+void mixer_open(void);
+void mixer_close(void);
+int mixer_set_volume(int left, int right);
+int mixer_read_volume(void);
+int mixer_get_fds(int *fds);
+
+void op_add_options(void);
+char *op_get_error_msg(int rc, const char *arg);
+void op_dump_plugins(void);
+const char *op_get_current(void);
+
+#endif
diff --git a/path.c b/path.c
new file mode 100644 (file)
index 0000000..8ec7236
--- /dev/null
+++ b/path.c
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "path.h"
+#include "xmalloc.h"
+
+#include <stdlib.h>
+#include <unistd.h>
+
+const char *get_extension(const char *filename)
+{
+       const char *ext;
+
+       ext = filename + strlen(filename) - 1;
+       while (ext >= filename && *ext != '/') {
+               if (*ext == '.') {
+                       ext++;
+                       return ext;
+               }
+               ext--;
+       }
+       return NULL;
+}
+
+const char *path_basename(const char *path)
+{
+       const char *f;
+
+       f = strrchr(path, '/');
+
+       return f ? f + 1 : path;
+}
+
+void path_strip(char *str)
+{
+       int i, s, d;
+
+       i = 0;
+       if (str[0] == '/')
+               i = 1;
+       while (str[i]) {
+               if (str[i] == '/') {
+                       d = i;
+                       s = i + 1;
+                       while (str[s] && str[s] == '/')
+                               s++;
+                       s--;
+                       do {
+                               str[d++] = str[++s];
+                       } while (str[s]);
+               } else if (i && str[i] == '.') {
+                       if (str[i + 1] == '/') {
+                               d = i;
+                               s = i + 1;
+                               do {
+                                       str[d++] = str[++s];
+                               } while (str[s]);
+                       } else if (str[i + 1] == 0) {
+                               str[i] = 0;
+                               break;
+                       } else if (str[i + 1] == '.' &&
+                                 (str[i + 2] == '/' || str[i + 2] == 0)) {
+                               /* aaa/bbb/../ccc */
+                               /* aaa/ccc */
+                               if (str[i + 2]) {
+                                       s = i + 3; /* ccc */
+                               } else {
+                                       s = i + 2;
+                               }
+                               d = i - 1; /* /../ccc */
+                               do {
+                                       if (d == 0)
+                                               break;
+                                       d--;
+                               } while (str[d] != '/');
+                               d++;
+                               /* std[d] is bbb/../ccc */
+                               i = d;
+                               s--;
+                               do {
+                                       str[d++] = str[++s];
+                               } while (str[s]);
+                       } else {
+                               while (str[i] && str[i] != '/')
+                                       i++;
+                               if (str[i])
+                                       i++;
+                       }
+               } else {
+                       while (str[i] && str[i] != '/')
+                               i++;
+                       if (str[i])
+                               i++;
+               }
+       }
+       if (i > 1 && str[i - 1] == '/')
+               str[i - 1] = 0;
+}
+
+char *path_absolute_cwd(const char *src, const char *cwd)
+{
+       char *str;
+
+       if (src[0] == '/') {
+               /* already absolute */
+               str = xstrdup(src);
+       } else {
+               int src_len;
+               int cwd_len;
+
+               src_len = strlen(src);
+               cwd_len = strlen(cwd);
+               str = xnew(char, cwd_len + 1 + src_len + 1);
+               memcpy(str, cwd, cwd_len);
+               str[cwd_len] = '/';
+               memcpy(str + cwd_len + 1, src, src_len + 1);
+       }
+       path_strip(str);
+       return str;
+}
+
+char *path_absolute(const char *src)
+{
+       char cwd[1024];
+
+       if (!getcwd(cwd, sizeof(cwd))) {
+               cwd[0] = '/';
+               cwd[1] = 0;
+       }
+       return path_absolute_cwd(src, cwd);
+}
diff --git a/path.h b/path.h
new file mode 100644 (file)
index 0000000..4c84745
--- /dev/null
+++ b/path.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_PATH_H
+#define CMUS_PATH_H
+
+const char *get_extension(const char *filename);
+const char *path_basename(const char *path);
+void path_strip(char *str);
+char *path_absolute_cwd(const char *src, const char *cwd);
+char *path_absolute(const char *src);
+
+#endif
diff --git a/pcm.c b/pcm.c
new file mode 100644 (file)
index 0000000..cb20728
--- /dev/null
+++ b/pcm.c
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "pcm.h"
+#include "utils.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+/*
+ * Functions to convert PCM to 16-bit signed little-endian stereo
+ *
+ * Conversion for 8-bit PCM):
+ *   1. phase
+ *      unsigned -> signed
+ *      mono -> stereo
+ *      8 -> 16
+ *
+ * Conversion for 16-bit PCM:
+ *   1. phase
+ *      be -> le
+ *      unsigned -> signed
+ *
+ *   2. phase
+ *      mono -> stereo
+ *
+ * There's no reason to split 8-bit conversion to 2 phases because we need to
+ * use separate buffer for 8->16 conversion anyway.
+ *
+ * Conversions for 16-bit stereo can be done in place. 16-bit mono needs to be
+ * converted to stereo so it's worthwhile to split the conversion to 2 phases.
+ */
+
+static void convert_u8_1ch_to_s16_2ch(void *dst, const void *src, int count)
+{
+       int16_t *d = dst;
+       const uint8_t *s = src;
+       int i, j = 0;
+
+       for (i = 0; i < count; i++) {
+               int16_t sample = s[i] << 8;
+               sample -= 32768;
+               d[j++] = sample;
+               d[j++] = sample;
+       }
+}
+
+static void convert_s8_1ch_to_s16_2ch(void *dst, const void *src, int count)
+{
+       int16_t *d = dst;
+       const int8_t *s = src;
+       int i, j = 0;
+
+       for (i = 0; i < count; i++) {
+               int16_t sample = s[i] << 8;
+               d[j++] = sample;
+               d[j++] = sample;
+       }
+}
+
+static void convert_u8_2ch_to_s16_2ch(void *dst, const void *src, int count)
+{
+       int16_t *d = dst;
+       const int8_t *s = src;
+       int i;
+
+       for (i = 0; i < count; i++) {
+               int16_t sample = s[i] << 8;
+               sample -= 32768;
+               d[i] = sample;
+       }
+}
+
+static void convert_s8_2ch_to_s16_2ch(void *dst, const void *src, int count)
+{
+       int16_t *d = dst;
+       const int8_t *s = src;
+       int i;
+
+       for (i = 0; i < count; i++) {
+               int16_t sample = s[i] << 8;
+               d[i] = sample;
+       }
+}
+
+static void convert_u16_le_to_s16_le(void *buf, int count)
+{
+       int16_t *b = buf;
+       int i;
+
+       for (i = 0; i < count; i++) {
+               int sample = (uint16_t)b[i];
+               sample -= 32768;
+               b[i] = sample;
+       }
+}
+
+static void convert_u16_be_to_s16_le(void *buf, int count)
+{
+       int16_t *b = buf;
+       int i;
+
+       for (i = 0; i < count; i++) {
+               uint16_t u = b[i];
+               int sample;
+
+               u = swap_uint16(u);
+               sample = (int)u - 32768;
+               b[i] = sample;
+       }
+}
+
+static void swap_s16_byte_order(void *buf, int count)
+{
+       int16_t *b = buf;
+       int i;
+
+       for (i = 0; i < count; i++)
+               b[i] = swap_uint16(b[i]);
+}
+
+static void convert_16_1ch_to_16_2ch(void *dst, const void *src, int count)
+{
+       int16_t *d = dst;
+       const int16_t *s = src;
+       int i, j = 0;
+
+       for (i = 0; i < count; i++) {
+               d[j++] = s[i];
+               d[j++] = s[i];
+       }
+}
+
+/* index is ((bits >> 2) & 4) | (is_signed << 1) | (channels - 1) */
+pcm_conv_func pcm_conv[8] = {
+       convert_u8_1ch_to_s16_2ch,
+       convert_u8_2ch_to_s16_2ch,
+       convert_s8_1ch_to_s16_2ch,
+       convert_s8_2ch_to_s16_2ch,
+
+       convert_16_1ch_to_16_2ch,
+       NULL,
+       convert_16_1ch_to_16_2ch,
+       NULL
+};
+
+/* index is ((bits >> 2) & 4) | (is_signed << 1) | bigendian */
+pcm_conv_in_place_func pcm_conv_in_place[8] = {
+       NULL,
+       NULL,
+       NULL,
+       NULL,
+
+       convert_u16_le_to_s16_le,
+       convert_u16_be_to_s16_le,
+
+#ifdef WORDS_BIGENDIAN
+       swap_s16_byte_order,
+       NULL,
+#else
+       NULL,
+       swap_s16_byte_order,
+#endif
+};
diff --git a/pcm.h b/pcm.h
new file mode 100644 (file)
index 0000000..e5d0e21
--- /dev/null
+++ b/pcm.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_PCM_H
+#define CMUS_PCM_H
+
+typedef void (*pcm_conv_func)(void *dst, const void *src, int count);
+typedef void (*pcm_conv_in_place_func)(void *buf, int count);
+
+extern pcm_conv_func pcm_conv[8];
+extern pcm_conv_in_place_func pcm_conv_in_place[8];
+
+#endif
diff --git a/pl.c b/pl.c
new file mode 100644 (file)
index 0000000..66119aa
--- /dev/null
+++ b/pl.c
@@ -0,0 +1,933 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "pl.h"
+#include "prog.h"
+#include "editable.h"
+#include "options.h"
+#include "xmalloc.h"
+#include "load_dir.h"
+#include "list.h"
+#include "job.h"
+#include "misc.h"
+#include "ui_curses.h"
+#include "xstrjoin.h"
+#include "worker.h"
+#include "uchar.h"
+#include "mergesort.h"
+
+#include <unistd.h>
+#include <stdio.h>
+#include <signal.h>
+
+struct playlist {
+       struct list_head node;
+
+       char *name;
+       struct editable editable;
+       struct rb_root shuffle_root;
+       struct simple_track *cur_track;
+};
+
+static struct playlist *pl_visible; /* never NULL */
+static struct playlist *pl_marked; /* never NULL */
+static struct window *pl_list_win;
+
+/* pl_playing_track shares its track_info reference with the playlist it's in.
+ * pl_playing_track and pl_playing might be null but pl_playing_track != NULL
+ * implies pl_playing != NULL and pl_playing_track is in pl_playing.
+ */
+static struct simple_track *pl_playing_track;
+static struct playlist *pl_playing;
+
+static int pl_cursor_in_track_window;
+static struct editable_shared pl_editable_shared;
+static LIST_HEAD(pl_head); /* never empty */
+
+static struct searchable *pl_searchable;
+
+static char *pl_name_to_pl_file(const char *name)
+{
+       return xstrjoin(cmus_playlist_dir, "/", name);
+}
+
+static void pl_to_iter(struct playlist *pl, struct iter *iter)
+{
+       *iter = (struct iter) {
+               .data0 = &pl_head,
+               .data1 = &pl->node
+       };
+}
+
+static struct playlist *pl_from_list(const struct list_head *list)
+{
+       return container_of(list, struct playlist, node);
+}
+
+static struct playlist *pl_from_editable(const struct editable *editable)
+{
+       return container_of(editable, struct playlist, editable);
+}
+
+static int pl_search_get_generic(struct iter *iter,
+               struct list_head *(*list_step)(struct list_head *list),
+               int (*iter_step)(struct iter *iter))
+{
+       struct list_head *pl_node = iter->data2;
+       struct playlist *pl;
+
+       if (!pl_node)
+               pl_node = &pl_head;
+
+       if (iter_step(iter))
+               return 1;
+
+       pl_node = list_step(pl_node);
+       if (pl_node == &pl_head)
+               return 0;
+
+       pl = pl_from_list(pl_node);
+       iter->data0 = &pl->editable.head;
+       iter->data1 = NULL;
+       iter->data2 = pl_node;
+       return 1;
+}
+
+static int pl_search_get_prev(struct iter *iter)
+{
+       return pl_search_get_generic(iter, list_prev, simple_track_get_prev);
+}
+
+static int pl_search_get_next(struct iter *iter)
+{
+       return pl_search_get_generic(iter, list_next, simple_track_get_next);
+}
+
+static int pl_search_get_current(void *data, struct iter *iter)
+{
+       window_get_sel(pl_editable_shared.win, iter);
+       iter->data2 = &pl_visible->node;
+       return 1;
+}
+
+static int pl_search_matches(void *data, struct iter *iter, const char *text)
+{
+       struct playlist *pl = pl_from_list(iter->data2);
+       int matched = 0;
+
+       char **words = get_words(text);
+       for (size_t i = 0; words[i]; i++) {
+               if (u_strcasestr_base(pl->name, words[i])) {
+                       matched = 1;
+                       break;
+               }
+       }
+       free_str_array(words);
+
+       if (!matched && iter->data1)
+               matched = _simple_track_search_matches(iter, text);
+
+       if (matched) {
+               struct iter list_iter;
+               pl_to_iter(pl, &list_iter);
+               window_set_sel(pl_list_win, &list_iter);
+
+               editable_take_ownership(&pl->editable);
+
+               if (iter->data1) {
+                       struct iter track_iter = *iter;
+                       track_iter.data2 = NULL;
+                       window_set_sel(pl_editable_shared.win, &track_iter);
+               }
+
+               pl_cursor_in_track_window = !!iter->data1;
+       }
+
+       return matched;
+}
+
+static const struct searchable_ops pl_searchable_ops = {
+       .get_prev = pl_search_get_prev,
+       .get_next = pl_search_get_next,
+       .get_current = pl_search_get_current,
+       .matches = pl_search_matches,
+};
+
+static void pl_free_track(struct editable *e, struct list_head *item)
+{
+       struct playlist *pl = pl_from_editable(e);
+       struct simple_track *track = to_simple_track(item);
+       struct shuffle_track *shuffle_track =
+               simple_track_to_shuffle_track(track);
+
+       if (track == pl->cur_track)
+               pl->cur_track = NULL;
+
+       rb_erase(&shuffle_track->tree_node, &pl->shuffle_root);
+       track_info_unref(track->info);
+       free(track);
+}
+
+static struct playlist *pl_new(const char *name)
+{
+       struct playlist *pl = xnew0(struct playlist, 1);
+       pl->name = xstrdup(name);
+       editable_init(&pl->editable, &pl_editable_shared, 0);
+       return pl;
+}
+
+static void pl_free(struct playlist *pl)
+{
+       editable_clear(&pl->editable);
+       free(pl->name);
+       free(pl);
+}
+
+static void pl_add_track(struct playlist *pl, struct track_info *ti)
+{
+       struct shuffle_track *track = xnew(struct shuffle_track, 1);
+
+       track_info_ref(ti);
+       simple_track_init(&track->simple_track, ti);
+       shuffle_list_add(track, &pl->shuffle_root);
+       editable_add(&pl->editable, &track->simple_track);
+}
+
+static void pl_add_cb(struct track_info *ti, void *opaque)
+{
+       pl_add_track(opaque, ti);
+}
+
+int pl_add_file_to_marked_pl(const char *file)
+{
+       char *full = NULL;
+       enum file_type type = cmus_detect_ft(file, &full);
+       int not_invalid = type != FILE_TYPE_INVALID;
+       if (not_invalid)
+               cmus_add(pl_add_cb, full, type, JOB_TYPE_PL, 0, pl_marked);
+       free(full);
+       return not_invalid;
+}
+
+void pl_add_track_to_marked_pl(struct track_info *ti)
+{
+       pl_add_track(pl_marked, ti);
+}
+
+static int pl_list_compare(const struct list_head *l, const struct list_head *r)
+{
+       struct playlist *pl = pl_from_list(l);
+       struct playlist *pr = pl_from_list(r);
+       return strcmp(pl->name, pr->name);
+}
+
+static void pl_sort_all(void)
+{
+       list_mergesort(&pl_head, pl_list_compare);
+}
+
+static void pl_load_one(const char *file)
+{
+       char *full = pl_name_to_pl_file(file);
+
+       struct playlist *pl = pl_new(file);
+       cmus_add(pl_add_cb, full, FILE_TYPE_PL, JOB_TYPE_PL, 0, pl);
+       list_add_tail(&pl->node, &pl_head);
+
+       free(full);
+}
+
+static void pl_load_all(void)
+{
+       struct directory dir;
+       if (dir_open(&dir, cmus_playlist_dir))
+               die_errno("error: cannot open playlist directory %s", cmus_playlist_dir);
+       const char *file;
+       while ((file = dir_read(&dir))) {
+               if (strcmp(file, ".") == 0 || strcmp(file, "..") == 0)
+                       continue;
+               if (!S_ISREG(dir.st.st_mode)) {
+                       error_msg("error: %s in %s is not a regular file", file,
+                                       cmus_playlist_dir);
+                       continue;
+               }
+               pl_load_one(file);
+       }
+       dir_close(&dir);
+}
+
+static void pl_create_default(void)
+{
+       struct playlist *pl = pl_new("default");
+       list_add_tail(&pl->node, &pl_head);
+}
+
+static GENERIC_ITER_PREV(pl_list_get_prev, struct playlist, node);
+static GENERIC_ITER_NEXT(pl_list_get_next, struct playlist, node);
+
+static void pl_list_sel_changed(void)
+{
+       struct list_head *list = pl_list_win->sel.data1;
+       struct playlist *pl = pl_from_list(list);
+       pl_visible = pl;
+       editable_take_ownership(&pl_visible->editable);
+}
+
+static int pl_dummy_filter(const struct simple_track *track)
+{
+       return 1;
+}
+
+static int pl_empty(struct playlist *pl)
+{
+       return editable_empty(&pl->editable);
+}
+
+static struct simple_track *pl_get_selected_track(void)
+{
+       /* pl_visible is not empty */
+
+       struct iter sel = pl_editable_shared.win->sel;
+       return iter_to_simple_track(&sel);
+}
+
+static struct simple_track *pl_get_first_track(struct playlist *pl)
+{
+       /* pl is not empty */
+
+       return to_simple_track(pl->editable.head.next);
+}
+
+static struct track_info *pl_play_track(struct playlist *pl, struct simple_track *t)
+{
+       /* t is a track in pl */
+
+       if (pl != pl_playing)
+               pl_list_win->changed = 1;
+
+       pl_playing_track = t;
+       pl_playing = pl;
+       pl_editable_shared.win->changed = 1;
+
+       if (follow)
+               pl_select_playing_track();
+
+       /* reference owned by the caller */
+       track_info_ref(pl_playing_track->info);
+
+       return pl_playing_track->info;
+}
+
+static struct track_info *pl_play_selected_track(void)
+{
+       if (pl_empty(pl_visible))
+               return NULL;
+
+       return pl_play_track(pl_visible, pl_get_selected_track());
+}
+
+static struct track_info *pl_play_first_in_pl_playing(void)
+{
+       if (!pl_playing)
+               pl_playing = pl_visible;
+
+       if (pl_empty(pl_playing)) {
+               pl_playing = NULL;
+               return NULL;
+       }
+
+       return pl_play_track(pl_playing, pl_get_first_track(pl_playing));
+}
+
+static struct simple_track *pl_get_next(struct playlist *pl, struct simple_track *cur)
+{
+       return simple_list_get_next(&pl->editable.head, cur, pl_dummy_filter);
+}
+
+static struct simple_track *pl_get_next_shuffled(struct playlist *pl,
+               struct simple_track *cur)
+{
+       struct shuffle_track *st = simple_track_to_shuffle_track(cur);
+       st = shuffle_list_get_next(&pl->shuffle_root, st, pl_dummy_filter);
+       return &st->simple_track;
+}
+
+static struct simple_track *pl_get_prev(struct playlist *pl,
+               struct simple_track *cur)
+{
+       return simple_list_get_prev(&pl->editable.head, cur, pl_dummy_filter);
+}
+
+static struct simple_track *pl_get_prev_shuffled(struct playlist *pl,
+               struct simple_track *cur)
+{
+       struct shuffle_track *st = simple_track_to_shuffle_track(cur);
+       st = shuffle_list_get_prev(&pl->shuffle_root, st, pl_dummy_filter);
+       return &st->simple_track;
+}
+
+static int pl_match_add_job(uint32_t type, void *job_data, void *opaque)
+{
+       uint32_t pat = JOB_TYPE_PL | JOB_TYPE_ADD;
+       if (type != pat)
+               return 0;
+
+       struct add_data *add_data= job_data;
+       return add_data->opaque == opaque;
+}
+
+static void pl_cancel_add_jobs(struct playlist *pl)
+{
+       worker_remove_jobs_by_cb(pl_match_add_job, pl);
+}
+
+static int pl_save_cb(track_info_cb cb, void *data, void *opaque)
+{
+       struct playlist *pl = opaque;
+       return editable_for_each(&pl->editable, cb, data, 0);
+}
+
+static void pl_save_one(struct playlist *pl)
+{
+       char *path = pl_name_to_pl_file(pl->name);
+       cmus_save(pl_save_cb, path, pl);
+       free(path);
+}
+
+static void pl_save_all(void)
+{
+       struct playlist *pl;
+       list_for_each_entry(pl, &pl_head, node)
+               pl_save_one(pl);
+}
+
+static void pl_delete_selected_pl(void)
+{
+       if (list_len(&pl_head) == 1) {
+               error_msg("cannot delete the last playlist");
+               return;
+       }
+
+       if (!yes_no_query("Delete selected playlist? [y/N]"))
+               return;
+
+       struct playlist *pl = pl_visible;
+
+       struct iter iter;
+       pl_to_iter(pl, &iter);
+       window_row_vanishes(pl_list_win, &iter);
+
+       list_del(&pl->node);
+
+       if (pl == pl_marked)
+               pl_marked = pl_visible;
+       if (pl == pl_playing) {
+               pl_playing = NULL;
+               pl_playing_track = NULL;
+       }
+
+       char *path = pl_name_to_pl_file(pl->name);
+       unlink(path);
+       free(path);
+
+       pl_cancel_add_jobs(pl);
+
+       /* can't free the pl now because the worker thread might hold a
+        * reference to it. instead free it once all running jobs are done.
+        */
+       struct pl_delete_data *pdd = xnew(struct pl_delete_data, 1);
+       pdd->cb = pl_free;
+       pdd->pl = pl;
+       job_schedule_pl_delete(pdd);
+}
+
+static void pl_mark_selected_pl(void)
+{
+       pl_marked = pl_visible;
+       pl_list_win->changed = 1;
+}
+
+typedef struct simple_track *(*pl_shuffled_move)(struct playlist *pl,
+               struct simple_track *cur);
+typedef struct simple_track *(*pl_normal_move)(struct playlist *pl,
+               struct simple_track *cur);
+
+static struct track_info *pl_goto_generic(pl_shuffled_move shuffled,
+               pl_normal_move normal)
+{
+       if (!pl_playing_track)
+               return pl_play_first_in_pl_playing();
+
+       struct simple_track *track;
+
+       if (shuffle)
+               track = shuffled(pl_playing, pl_playing_track);
+       else
+               track = normal(pl_playing, pl_playing_track);
+
+       if (track)
+               return pl_play_track(pl_playing, track);
+       return NULL;
+}
+
+static void pl_clear_visible_pl(void)
+{
+       if (pl_cursor_in_track_window)
+               pl_win_next();
+       if (pl_visible == pl_playing)
+               pl_playing_track = NULL;
+       editable_clear(&pl_visible->editable);
+       pl_cancel_add_jobs(pl_visible);
+}
+
+static int pl_name_exists(const char *name)
+{
+       struct playlist *pl;
+       list_for_each_entry(pl, &pl_head, node) {
+               if (strcmp(pl->name, name) == 0)
+                       return 1;
+       }
+       return 0;
+}
+
+static int pl_check_new_pl_name(const char *name)
+{
+       if (strchr(name, '/')) {
+               error_msg("playlists cannot contain the '/' character");
+               return 0;
+       }
+
+       if (pl_name_exists(name)) {
+               error_msg("another playlist named %s already exists", name);
+               return 0;
+       }
+
+       return 1;
+}
+
+static char *pl_create_name(const char *file)
+{
+       size_t file_len = strlen(file);
+
+       char *name = xnew(char, file_len + 10);
+       strcpy(name, file);
+
+       for (int i = 1; pl_name_exists(name); i++) {
+               if (i == 100) {
+                       free(name);
+                       return NULL;
+               }
+               sprintf(name + file_len, ".%d", i);
+       }
+
+       return name;
+}
+
+static void pl_delete_selected_track(void)
+{
+       /* pl_cursor_in_track_window == true */
+
+       if (pl_get_selected_track() == pl_playing_track)
+               pl_playing_track = NULL;
+       editable_remove_sel(&pl_visible->editable);
+       if (pl_empty(pl_visible))
+               pl_win_next();
+}
+
+void pl_init(void)
+{
+       editable_shared_init(&pl_editable_shared, pl_free_track);
+
+       pl_load_all();
+       if (list_empty(&pl_head))
+               pl_create_default();
+
+       pl_sort_all();
+
+       pl_list_win = window_new(pl_list_get_prev, pl_list_get_next);
+       pl_list_win->sel_changed = pl_list_sel_changed;
+       window_set_contents(pl_list_win, &pl_head);
+       window_changed(pl_list_win);
+
+       /* pl_visible set by window_set_contents */
+       pl_marked = pl_visible;
+
+       struct iter iter = { 0 };
+       pl_searchable = searchable_new(NULL, &iter, &pl_searchable_ops);
+}
+
+void pl_exit(void)
+{
+       pl_save_all();
+}
+
+void pl_save(void)
+{
+       pl_save_all();
+}
+
+void pl_import(const char *path)
+{
+       const char *file = get_filename(path);
+       if (!file) {
+               error_msg("\"%s\" is not a valid path", path);
+               return;
+       }
+
+       char *name = pl_create_name(file);
+       if (!name) {
+               error_msg("a playlist named \"%s\" already exists ", file);
+               return;
+       }
+
+       if (strcmp(name, file) != 0)
+               info_msg("adding \"%s\" as \"%s\"", file, name);
+
+       struct playlist *pl = pl_new(name);
+       cmus_add(pl_add_cb, path, FILE_TYPE_PL, JOB_TYPE_PL, 0, pl);
+       list_add_tail(&pl->node, &pl_head);
+       pl_list_win->changed = 1;
+
+       free(name);
+}
+
+void pl_export_selected_pl(const char *path)
+{
+       char *tmp = expand_filename(path);
+       if (access(tmp, F_OK) != 0 || yes_no_query("File exists. Overwrite? [y/N]"))
+               cmus_save(pl_save_cb, tmp, pl_visible);
+       free(tmp);
+}
+
+struct searchable *pl_get_searchable(void)
+{
+       return pl_searchable;
+}
+
+struct track_info *pl_goto_next(void)
+{
+       return pl_goto_generic(pl_get_next_shuffled, pl_get_next);
+}
+
+struct track_info *pl_goto_prev(void)
+{
+       return pl_goto_generic(pl_get_prev_shuffled, pl_get_prev);
+}
+
+struct track_info *pl_play_selected_row(void)
+{
+       /* a bit tricky because we want to insert the selected track at the
+        * current position in the shuffle list. but we must be careful not to
+        * insert a track into a foreign shuffle list.
+        */
+
+       int was_in_track_window = pl_cursor_in_track_window;
+       if (!pl_cursor_in_track_window)
+               window_goto_top(pl_editable_shared.win);
+
+       struct playlist *prev_pl = pl_playing;
+       struct simple_track *prev_track = pl_playing_track;
+
+       struct track_info *rv = pl_play_selected_track();
+
+       if (shuffle && rv && (pl_playing == prev_pl) && prev_track) {
+               struct shuffle_track *prev_st = simple_track_to_shuffle_track(prev_track);
+               struct shuffle_track *cur_st =
+                       simple_track_to_shuffle_track(pl_playing_track);
+               shuffle_insert(&pl_playing->shuffle_root, prev_st, cur_st);
+       }
+
+       pl_cursor_in_track_window = was_in_track_window;
+
+       return rv;
+}
+
+void pl_select_playing_track(void)
+{
+       if (!pl_playing_track)
+               return;
+
+       struct iter iter;
+
+       editable_take_ownership(&pl_playing->editable);
+
+       editable_track_to_iter(&pl_playing->editable, pl_playing_track, &iter);
+       window_set_sel(pl_editable_shared.win, &iter);
+
+       pl_to_iter(pl_playing, &iter);
+       window_set_sel(pl_list_win, &iter);
+
+       if (!pl_cursor_in_track_window)
+               pl_mark_for_redraw();
+
+       pl_cursor_in_track_window = 1;
+}
+
+void pl_reshuffle(void)
+{
+       if (pl_playing)
+               shuffle_list_reshuffle(&pl_playing->shuffle_root);
+}
+
+void pl_get_sort_str(char *buf, size_t size)
+{
+       strscpy(buf, pl_editable_shared.sort_str, size);
+}
+
+void pl_set_sort_str(const char *buf)
+{
+       sort_key_t *keys = parse_sort_keys(buf);
+
+       if (!keys)
+               return;
+
+       editable_shared_set_sort_keys(&pl_editable_shared, keys);
+       sort_keys_to_str(keys, pl_editable_shared.sort_str,
+                       sizeof(pl_editable_shared.sort_str));
+
+       struct playlist *pl;
+       list_for_each_entry(pl, &pl_head, node)
+               editable_sort(&pl->editable);
+}
+
+void pl_rename_selected_pl(const char *name)
+{
+       if (strcmp(pl_visible->name, name) == 0)
+               return;
+
+       if (!pl_check_new_pl_name(name))
+               return;
+
+       char *full_cur = pl_name_to_pl_file(pl_visible->name);
+       char *full_new = pl_name_to_pl_file(name);
+       rename(full_cur, full_new);
+       free(full_cur);
+       free(full_new);
+
+       free(pl_visible->name);
+       pl_visible->name = xstrdup(name);
+
+       pl_mark_for_redraw();
+}
+
+void pl_clear(void)
+{
+       if (!pl_cursor_in_track_window)
+               return;
+
+       pl_clear_visible_pl();
+}
+
+void pl_mark_for_redraw(void)
+{
+       pl_list_win->changed = 1;
+       pl_editable_shared.win->changed = 1;
+}
+
+int pl_needs_redraw(void)
+{
+       return pl_list_win->changed || pl_editable_shared.win->changed;
+}
+
+struct window *pl_cursor_win(void)
+{
+       if (pl_cursor_in_track_window)
+               return pl_editable_shared.win;
+       else
+               return pl_list_win;
+}
+
+int _pl_for_each_sel(track_info_cb cb, void *data, int reverse)
+{
+       if (pl_cursor_in_track_window)
+               return _editable_for_each_sel(&pl_visible->editable, cb, data, reverse);
+       else
+               return editable_for_each(&pl_visible->editable, cb, data, reverse);
+}
+
+int pl_for_each_sel(track_info_cb cb, void *data, int reverse)
+{
+       if (pl_cursor_in_track_window)
+               return editable_for_each_sel(&pl_visible->editable, cb, data, reverse);
+       else
+               return editable_for_each(&pl_visible->editable, cb, data, reverse);
+}
+
+#define pl_tw_only(cmd) if (!pl_cursor_in_track_window) { \
+       info_msg(":%s only works in the track window", cmd); \
+} else
+
+void pl_invert_marks(void)
+{
+       pl_tw_only("invert")
+               editable_invert_marks(&pl_visible->editable);
+}
+
+void pl_mark(char *arg)
+{
+       pl_tw_only("mark")
+               editable_invert_marks(&pl_visible->editable);
+}
+
+void pl_unmark(void)
+{
+       pl_tw_only("unmark")
+               editable_unmark(&pl_visible->editable);
+}
+
+void pl_rand(void)
+{
+       pl_tw_only("rand")
+               editable_rand(&pl_visible->editable);
+}
+
+void pl_win_mv_after(void)
+{
+       if (pl_cursor_in_track_window)
+               editable_move_after(&pl_visible->editable);
+}
+
+void pl_win_mv_before(void)
+{
+       if (pl_cursor_in_track_window)
+               editable_move_before(&pl_visible->editable);
+}
+
+void pl_win_remove(void)
+{
+       if (pl_cursor_in_track_window)
+               pl_delete_selected_track();
+       else
+               pl_delete_selected_pl();
+}
+
+void pl_win_toggle(void)
+{
+       if (pl_cursor_in_track_window)
+               editable_toggle_mark(&pl_visible->editable);
+       else
+               pl_mark_selected_pl();
+}
+
+void pl_win_update(void)
+{
+       if (!yes_no_query("Reload this playlist? [y/N]"))
+               return;
+
+       pl_clear_visible_pl();
+
+       char *full = pl_name_to_pl_file(pl_visible->name);
+       cmus_add(pl_add_cb, full, FILE_TYPE_PL, JOB_TYPE_PL, 0, pl_visible);
+       free(full);
+}
+
+void pl_win_next(void)
+{
+       pl_cursor_in_track_window ^= 1;
+       if (pl_empty(pl_visible))
+               pl_cursor_in_track_window = 0;
+       pl_mark_for_redraw();
+}
+
+void pl_set_nr_rows(int h)
+{
+       window_set_nr_rows(pl_list_win, h);
+       window_set_nr_rows(pl_editable_shared.win, h);
+}
+
+unsigned int pl_visible_total_time(void)
+{
+       return pl_visible->editable.total_time;
+}
+
+unsigned int pl_playing_total_time(void)
+{
+       if (pl_playing)
+               return pl_playing->editable.total_time;
+       return 0;
+}
+
+void pl_list_iter_to_info(struct iter *iter, struct pl_list_info *info)
+{
+       struct playlist *pl = pl_from_list(iter->data1);
+
+       info->name = pl->name;
+       info->marked = pl == pl_marked;
+       info->active = !pl_cursor_in_track_window;
+       info->selected = pl == pl_visible;
+       info->current = pl == pl_playing;
+}
+
+void pl_draw(void (*list)(struct window *win),
+               void (*tracks)(struct window *win), int full)
+{
+       if (full || pl_list_win->changed)
+               list(pl_list_win);
+       if (full || pl_editable_shared.win->changed)
+               tracks(pl_editable_shared.win);
+       pl_list_win->changed = 0;
+       pl_editable_shared.win->changed = 0;
+}
+
+struct simple_track *pl_get_playing_track(void)
+{
+       return pl_playing_track;
+}
+
+void pl_update_track(struct track_info *old, struct track_info *new)
+{
+       struct playlist *pl;
+       list_for_each_entry(pl, &pl_head, node)
+               editable_update_track(&pl->editable, old, new);
+}
+
+int pl_get_cursor_in_track_window(void)
+{
+       return pl_cursor_in_track_window;
+}
+
+void pl_create(const char *name)
+{
+       if (!pl_check_new_pl_name(name))
+               return;
+
+       struct playlist *pl = pl_new(name);
+       list_add_tail(&pl->node, &pl_head);
+       pl_list_win->changed = 1;
+}
+
+int pl_visible_is_marked(void)
+{
+       return pl_visible == pl_marked;
+}
+
+const char *pl_marked_pl_name(void)
+{
+       return pl_marked->name;
+}
+
+void pl_set_marked_pl_by_name(const char *name)
+{
+       struct playlist *pl;
+       list_for_each_entry(pl, &pl_head, node) {
+               if (strcmp(pl->name, name) == 0) {
+                       pl_marked = pl;
+                       return;
+               }
+       }
+}
diff --git a/pl.h b/pl.h
new file mode 100644 (file)
index 0000000..a43faef
--- /dev/null
+++ b/pl.h
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_PL_H
+#define CMUS_PL_H
+
+#include "track_info.h"
+#include "window.h"
+#include "editable.h"
+#include "cmus.h"
+
+struct pl_list_info {
+       const char *name;
+       unsigned marked : 1;
+       unsigned active : 1;
+       unsigned selected : 1;
+       unsigned current : 1;
+};
+
+void pl_init(void);
+void pl_exit(void);
+void pl_save(void);
+void pl_import(const char *path);
+void pl_export_selected_pl(const char *path);
+struct searchable *pl_get_searchable(void);
+int pl_add_file_to_marked_pl(const char *file);
+void pl_add_track_to_marked_pl(struct track_info *ti);
+void pl_rename_selected_pl(const char *name);
+void pl_create(const char *name);
+void pl_get_sort_str(char *buf, size_t size);
+void pl_set_sort_str(const char *buf);
+void pl_clear(void);
+struct track_info *pl_goto_next(void);
+struct track_info *pl_goto_prev(void);
+struct track_info *pl_play_selected_row(void);
+void pl_select_playing_track(void);
+void pl_reshuffle(void);
+int _pl_for_each_sel(track_info_cb cb, void *data, int reverse);
+int pl_for_each_sel(track_info_cb cb, void *data, int reverse);
+void pl_reload_visible(void);
+struct window *pl_cursor_win(void);
+void pl_set_nr_rows(int h);
+unsigned int pl_visible_total_time(void);
+unsigned int pl_playing_total_time(void);
+struct simple_track *pl_get_playing_track(void);
+void pl_update_track(struct track_info *old, struct track_info *new);
+int pl_get_cursor_in_track_window(void);
+int pl_visible_is_marked(void);
+const char *pl_marked_pl_name(void);
+void pl_set_marked_pl_by_name(const char *name);
+
+void pl_mark_for_redraw(void);
+int pl_needs_redraw(void);
+void pl_draw(void (*list)(struct window *win),
+               void (*tracks)(struct window *win), int full);
+void pl_list_iter_to_info(struct iter *iter, struct pl_list_info *info);
+
+static inline void pl_add_track_to_marked_pl2(struct track_info *ti,
+               void *opaque)
+{
+       pl_add_track_to_marked_pl(ti);
+}
+
+/* cmd wrappers */
+
+void pl_invert_marks(void);
+void pl_mark(char *arg);
+void pl_unmark(void);
+void pl_rand(void);
+void pl_win_mv_after(void);
+void pl_win_mv_before(void);
+void pl_win_remove(void);
+void pl_win_toggle(void);
+void pl_win_update(void);
+void pl_win_next(void);
+
+#endif
diff --git a/play_queue.c b/play_queue.c
new file mode 100644 (file)
index 0000000..83dd7d2
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005-2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "play_queue.h"
+#include "editable.h"
+#include "track.h"
+#include "xmalloc.h"
+
+struct editable pq_editable;
+static struct editable_shared pq_editable_shared;
+
+static void pq_free_track(struct editable *e, struct list_head *item)
+{
+       struct simple_track *track = to_simple_track(item);
+
+       track_info_unref(track->info);
+       free(track);
+}
+
+void play_queue_init(void)
+{
+       editable_shared_init(&pq_editable_shared, pq_free_track);
+       editable_init(&pq_editable, &pq_editable_shared, 1);
+}
+
+void play_queue_append(struct track_info *ti, void *opaque)
+{
+       struct simple_track *t = simple_track_new(ti);
+
+       editable_add(&pq_editable, t);
+}
+
+void play_queue_prepend(struct track_info *ti, void *opaque)
+{
+       struct simple_track *t = simple_track_new(ti);
+
+       editable_add_before(&pq_editable, t);
+}
+
+struct track_info *play_queue_remove(void)
+{
+       struct track_info *info = NULL;
+
+       if (!list_empty(&pq_editable.head)) {
+               struct simple_track *t = to_simple_track(pq_editable.head.next);
+               info = t->info;
+               track_info_ref(info);
+               editable_remove_track(&pq_editable, t);
+       }
+
+       return info;
+}
+
+int play_queue_for_each(int (*cb)(void *data, struct track_info *ti),
+               void *data, void *opaque)
+{
+       struct simple_track *track;
+       int rc = 0;
+
+       list_for_each_entry(track, &pq_editable.head, node) {
+               rc = cb(data, track->info);
+               if (rc)
+                       break;
+       }
+       return rc;
+}
diff --git a/play_queue.h b/play_queue.h
new file mode 100644 (file)
index 0000000..426f180
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005-2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_PLAY_QUEUE_H
+#define CMUS_PLAY_QUEUE_H
+
+#include "editable.h"
+#include "track_info.h"
+
+extern struct editable pq_editable;
+
+void play_queue_init(void);
+void play_queue_append(struct track_info *ti, void *opaque);
+void play_queue_prepend(struct track_info *ti, void *opaque);
+struct track_info *play_queue_remove(void);
+int play_queue_for_each(int (*cb)(void *data, struct track_info *ti),
+               void *data, void *opaque);
+
+#endif
diff --git a/player.c b/player.c
new file mode 100644 (file)
index 0000000..07b3219
--- /dev/null
+++ b/player.c
@@ -0,0 +1,1485 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "player.h"
+#include "buffer.h"
+#include "input.h"
+#include "output.h"
+#include "sf.h"
+#include "op.h"
+#include "utils.h"
+#include "xmalloc.h"
+#include "debug.h"
+#include "compiler.h"
+#include "options.h"
+#include "mpris.h"
+#include "cmus.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <errno.h>
+#include <string.h>
+#include <sys/time.h>
+#include <stdarg.h>
+#include <math.h>
+
+const char * const player_status_names[] = {
+       "stopped", "playing", "paused", NULL
+};
+
+enum producer_status {
+       PS_UNLOADED,
+       PS_STOPPED,
+       PS_PLAYING,
+       PS_PAUSED
+};
+
+enum consumer_status {
+       CS_STOPPED,
+       CS_PLAYING,
+       CS_PAUSED
+};
+
+/* protects player_info_priv and player_metadata */
+static pthread_mutex_t player_info_mutex = CMUS_MUTEX_INITIALIZER;
+struct player_info player_info;
+char player_metadata[255 * 16 + 1];
+static struct player_info player_info_priv = {
+       .ti = NULL,
+       .status = PLAYER_STATUS_STOPPED,
+       .pos = 0,
+       .current_bitrate = -1,
+       .buffer_fill = 0,
+       .buffer_size = 0,
+       .error_msg = NULL,
+       .file_changed = 0,
+       .metadata_changed = 0,
+       .status_changed = 0,
+       .position_changed = 0,
+       .buffer_fill_changed = 0,
+};
+
+/* continue playing after track is finished? */
+int player_cont = 1;
+
+/* repeat current track forever? */
+int player_repeat_current;
+
+enum replaygain replaygain;
+int replaygain_limit = 1;
+double replaygain_preamp = 0.0;
+
+int soft_vol;
+int soft_vol_l;
+int soft_vol_r;
+
+static sample_format_t buffer_sf;
+static CHANNEL_MAP(buffer_channel_map);
+
+static pthread_t producer_thread;
+static pthread_mutex_t producer_mutex = CMUS_MUTEX_INITIALIZER;
+static pthread_cond_t producer_playing = CMUS_COND_INITIALIZER;
+static int producer_running = 1;
+static enum producer_status producer_status = PS_UNLOADED;
+static struct input_plugin *ip = NULL;
+
+static pthread_t consumer_thread;
+static pthread_mutex_t consumer_mutex = CMUS_MUTEX_INITIALIZER;
+static pthread_cond_t consumer_playing = CMUS_COND_INITIALIZER;
+static int consumer_running = 1;
+static enum consumer_status consumer_status = CS_STOPPED;
+static unsigned long consumer_pos = 0;
+
+/* for replay gain and soft vol
+ * usually same as consumer_pos, sometimes more than consumer_pos
+ */
+static unsigned long scale_pos;
+static double replaygain_scale = 1.0;
+
+/* locking {{{ */
+
+#define player_info_priv_lock() cmus_mutex_lock(&player_info_mutex)
+#define player_info_priv_unlock() cmus_mutex_unlock(&player_info_mutex)
+
+#define producer_lock() cmus_mutex_lock(&producer_mutex)
+#define producer_unlock() cmus_mutex_unlock(&producer_mutex)
+
+#define consumer_lock() cmus_mutex_lock(&consumer_mutex)
+#define consumer_unlock() cmus_mutex_unlock(&consumer_mutex)
+
+#define player_lock() \
+       do { \
+               consumer_lock(); \
+               producer_lock(); \
+       } while (0)
+
+#define player_unlock() \
+       do { \
+               producer_unlock(); \
+               consumer_unlock(); \
+       } while (0)
+
+/* locking }}} */
+
+static void reset_buffer(void)
+{
+       buffer_reset();
+       consumer_pos = 0;
+       scale_pos = 0;
+       pthread_cond_broadcast(&producer_playing);
+}
+
+static void set_buffer_sf(void)
+{
+       buffer_sf = ip_get_sf(ip);
+       ip_get_channel_map(ip, buffer_channel_map);
+
+       /* ip_read converts samples to this format */
+       if (sf_get_channels(buffer_sf) <= 2 && sf_get_bits(buffer_sf) <= 16) {
+               buffer_sf &= SF_RATE_MASK;
+               buffer_sf |= sf_channels(2) | sf_bits(16) | sf_signed(1);
+               buffer_sf |= sf_host_endian();
+               channel_map_init_stereo(buffer_channel_map);
+       }
+}
+
+#define SOFT_VOL_SCALE 65536
+
+/* coefficients for volumes 0..99, for 100 65536 is used
+ * data copied from alsa-lib src/pcm/pcm_softvol.c
+ */
+static const unsigned short soft_vol_db[100] = {
+       0x0000, 0x0110, 0x011c, 0x012f, 0x013d, 0x0152, 0x0161, 0x0179,
+       0x018a, 0x01a5, 0x01c1, 0x01d5, 0x01f5, 0x020b, 0x022e, 0x0247,
+       0x026e, 0x028a, 0x02b6, 0x02d5, 0x0306, 0x033a, 0x035f, 0x0399,
+       0x03c2, 0x0403, 0x0431, 0x0479, 0x04ac, 0x04fd, 0x0553, 0x058f,
+       0x05ef, 0x0633, 0x069e, 0x06ea, 0x0761, 0x07b5, 0x083a, 0x0898,
+       0x092c, 0x09cb, 0x0a3a, 0x0aeb, 0x0b67, 0x0c2c, 0x0cb6, 0x0d92,
+       0x0e2d, 0x0f21, 0x1027, 0x10de, 0x1202, 0x12cf, 0x1414, 0x14f8,
+       0x1662, 0x1761, 0x18f5, 0x1a11, 0x1bd3, 0x1db4, 0x1f06, 0x211d,
+       0x2297, 0x24ec, 0x2690, 0x292a, 0x2aff, 0x2de5, 0x30fe, 0x332b,
+       0x369f, 0x390d, 0x3ce6, 0x3f9b, 0x43e6, 0x46eb, 0x4bb3, 0x4f11,
+       0x5466, 0x5a18, 0x5e19, 0x6472, 0x68ea, 0x6ffd, 0x74f8, 0x7cdc,
+       0x826a, 0x8b35, 0x9499, 0x9b35, 0xa5ad, 0xad0b, 0xb8b7, 0xc0ee,
+       0xcdf1, 0xd71a, 0xe59c, 0xefd3
+};
+
+static inline void scale_sample_int16_t(int16_t *buf, int i, int vol, int swap)
+{
+       int32_t sample = swap ? (int16_t)swap_uint16(buf[i]) : buf[i];
+
+       if (sample < 0) {
+               sample = (sample * vol - SOFT_VOL_SCALE / 2) / SOFT_VOL_SCALE;
+               if (sample < INT16_MIN)
+                       sample = INT16_MIN;
+       } else {
+               sample = (sample * vol + SOFT_VOL_SCALE / 2) / SOFT_VOL_SCALE;
+               if (sample > INT16_MAX)
+                       sample = INT16_MAX;
+       }
+       buf[i] = swap ? swap_uint16(sample) : sample;
+}
+
+static inline int32_t scale_sample_s24le(int32_t s, int vol)
+{
+       int64_t sample = s;
+       if (sample < 0) {
+               sample = (sample * vol - SOFT_VOL_SCALE / 2) / SOFT_VOL_SCALE;
+               if (sample < -0x800000)
+                       sample = -0x800000;
+       } else {
+               sample = (sample * vol + SOFT_VOL_SCALE / 2) / SOFT_VOL_SCALE;
+               if (sample > 0x7fffff)
+                       sample = 0x7fffff;
+       }
+       return sample;
+}
+
+static inline void scale_sample_int32_t(int32_t *buf, int i, int vol, int swap)
+{
+       int64_t sample = swap ? (int32_t)swap_uint32(buf[i]) : buf[i];
+
+       if (sample < 0) {
+               sample = (sample * vol - SOFT_VOL_SCALE / 2) / SOFT_VOL_SCALE;
+               if (sample < INT32_MIN)
+                       sample = INT32_MIN;
+       } else {
+               sample = (sample * vol + SOFT_VOL_SCALE / 2) / SOFT_VOL_SCALE;
+               if (sample > INT32_MAX)
+                       sample = INT32_MAX;
+       }
+       buf[i] = swap ? swap_uint32(sample) : sample;
+}
+
+static inline int sf_need_swap(sample_format_t sf)
+{
+#ifdef WORDS_BIGENDIAN
+       return !sf_get_bigendian(sf);
+#else
+       return sf_get_bigendian(sf);
+#endif
+}
+
+#define SCALE_SAMPLES(TYPE, buffer, count, l, r, swap)                         \
+{                                                                              \
+       const int frames = count / sizeof(TYPE) / 2;                            \
+       TYPE *buf = (void *) buffer;                                            \
+       int i;                                                                  \
+       /* avoid underflowing -32768 to 32767 when scale is 65536 */            \
+       if (l != SOFT_VOL_SCALE && r != SOFT_VOL_SCALE) {                       \
+               for (i = 0; i < frames; i++) {                                  \
+                       scale_sample_##TYPE(buf, i * 2, l, swap);               \
+                       scale_sample_##TYPE(buf, i * 2 + 1, r, swap);           \
+               }                                                               \
+       } else if (l != SOFT_VOL_SCALE) {                                       \
+               for (i = 0; i < frames; i++)                                    \
+                       scale_sample_##TYPE(buf, i * 2, l, swap);               \
+       } else if (r != SOFT_VOL_SCALE) {                                       \
+               for (i = 0; i < frames; i++)                                    \
+                       scale_sample_##TYPE(buf, i * 2 + 1, r, swap);           \
+       }                                                                       \
+}
+
+static inline int32_t read_s24le(const char *buf)
+{
+       const unsigned char *b = (const unsigned char *) buf;
+       return b[0] | (b[1] << 8) | (((const signed char *) buf)[2] << 16);
+}
+
+static inline void write_s24le(char *buf, int32_t x)
+{
+       unsigned char *b = (unsigned char *) buf;
+       b[0] = x;
+       b[1] = x >> 8;
+       b[2] = x >> 16;
+}
+
+static void scale_samples_s24le(char *buf, unsigned int count, int l, int r)
+{
+       int frames = count / 3 / 2;
+       if (l != SOFT_VOL_SCALE && r != SOFT_VOL_SCALE) {
+               while (frames--) {
+                       write_s24le(buf, scale_sample_s24le(read_s24le(buf), l));
+                       buf += 3;
+                       write_s24le(buf, scale_sample_s24le(read_s24le(buf), r));
+                       buf += 3;
+               }
+       } else if (l != SOFT_VOL_SCALE) {
+               while (frames--) {
+                       write_s24le(buf, scale_sample_s24le(read_s24le(buf), l));
+                       buf += 3 * 2;
+               }
+       } else if (r != SOFT_VOL_SCALE) {
+               buf += 3;
+               while (frames--) {
+                       write_s24le(buf, scale_sample_s24le(read_s24le(buf), r));
+                       buf += 3 * 2;
+               }
+       }
+}
+
+static void scale_samples(char *buffer, unsigned int *countp)
+{
+       unsigned int count = *countp;
+       int ch, bits, l, r;
+
+       BUG_ON(scale_pos < consumer_pos);
+
+       if (consumer_pos != scale_pos) {
+               unsigned int offs = scale_pos - consumer_pos;
+
+               if (offs >= count)
+                       return;
+               buffer += offs;
+               count -= offs;
+       }
+       scale_pos += count;
+
+       if (replaygain_scale == 1.0 && soft_vol_l == 100 && soft_vol_r == 100)
+               return;
+
+       ch = sf_get_channels(buffer_sf);
+       bits = sf_get_bits(buffer_sf);
+       if (ch != 2 || (bits != 16 && bits != 24 && bits != 32))
+               return;
+
+       l = SOFT_VOL_SCALE;
+       r = SOFT_VOL_SCALE;
+       if (soft_vol && soft_vol_l != 100)
+               l = soft_vol_db[soft_vol_l];
+       if (soft_vol && soft_vol_r != 100)
+               r = soft_vol_db[soft_vol_r];
+
+       l *= replaygain_scale;
+       r *= replaygain_scale;
+
+       switch (bits) {
+       case 16:
+               SCALE_SAMPLES(int16_t, buffer, count, l, r, sf_need_swap(buffer_sf));
+               break;
+       case 24:
+               if (likely(!sf_get_bigendian(buffer_sf)))
+                       scale_samples_s24le(buffer, count, l, r);
+               break;
+       case 32:
+               SCALE_SAMPLES(int32_t, buffer, count, l, r, sf_need_swap(buffer_sf));
+               break;
+       }
+}
+
+static void update_rg_scale(void)
+{
+       double gain, peak, db, scale, limit;
+
+       replaygain_scale = 1.0;
+       if (!player_info_priv.ti || !replaygain)
+               return;
+
+       if (replaygain == RG_TRACK || replaygain == RG_TRACK_PREFERRED) {
+               gain = player_info_priv.ti->rg_track_gain;
+               peak = player_info_priv.ti->rg_track_peak;
+       } else {
+               gain = player_info_priv.ti->rg_album_gain;
+               peak = player_info_priv.ti->rg_album_peak;
+       }
+
+       if (isnan(gain)) {
+               if (replaygain == RG_TRACK_PREFERRED) {
+                       gain = player_info_priv.ti->rg_album_gain;
+                       peak = player_info_priv.ti->rg_album_peak;
+               } else if (replaygain == RG_ALBUM_PREFERRED) {
+                       gain = player_info_priv.ti->rg_track_gain;
+                       peak = player_info_priv.ti->rg_track_peak;
+               }
+       }
+
+       if (isnan(gain)) {
+               d_print("gain not available\n");
+               return;
+       }
+       if (isnan(peak)) {
+               d_print("peak not available, defaulting to 1\n");
+               peak = 1;
+       }
+       if (peak < 0.05) {
+               d_print("peak (%g) is too small\n", peak);
+               return;
+       }
+
+       db = replaygain_preamp + gain;
+
+       scale = pow(10.0, db / 20.0);
+       replaygain_scale = scale;
+       limit = 1.0 / peak;
+       if (replaygain_limit && !isnan(peak)) {
+               if (replaygain_scale > limit)
+                       replaygain_scale = limit;
+       }
+
+       d_print("gain = %f, peak = %f, db = %f, scale = %f, limit = %f, replaygain_scale = %f\n",
+                       gain, peak, db, scale, limit, replaygain_scale);
+}
+
+static inline unsigned int buffer_second_size(void)
+{
+       return sf_get_second_size(buffer_sf);
+}
+
+/* updating player status {{{ */
+
+static inline void _file_changed(struct track_info *ti)
+{
+       player_info_priv_lock();
+       if (player_info_priv.ti)
+               track_info_unref(player_info_priv.ti);
+
+       player_info_priv.ti = ti;
+       update_rg_scale();
+       player_metadata[0] = 0;
+       player_info_priv.file_changed = 1;
+       player_info_priv_unlock();
+}
+
+static inline void file_changed(struct track_info *ti)
+{
+       if (ti) {
+               d_print("file: %s\n", ti->filename);
+       } else {
+               d_print("unloaded\n");
+       }
+       _file_changed(ti);
+}
+
+static inline void metadata_changed(void)
+{
+       struct keyval *comments;
+       int rc;
+
+       player_info_priv_lock();
+       if (ip_get_metadata(ip)) {
+               d_print("metadata changed: %s\n", ip_get_metadata(ip));
+               memcpy(player_metadata, ip_get_metadata(ip), 255 * 16 + 1);
+       }
+
+       rc = ip_read_comments(ip, &comments);
+       if (!rc) {
+               if (player_info_priv.ti->comments)
+                       keyvals_free(player_info_priv.ti->comments);
+               track_info_set_comments(player_info_priv.ti, comments);
+       }
+
+       player_info_priv.metadata_changed = 1;
+       player_info_priv_unlock();
+}
+
+static void player_error(const char *msg)
+{
+       player_info_priv_lock();
+       player_info_priv.status = (enum player_status)consumer_status;
+       player_info_priv.pos = 0;
+       player_info_priv.current_bitrate = -1;
+       player_info_priv.buffer_fill = buffer_get_filled_chunks();
+       player_info_priv.buffer_size = buffer_nr_chunks;
+       player_info_priv.status_changed = 1;
+
+       free(player_info_priv.error_msg);
+       player_info_priv.error_msg = xstrdup(msg);
+       player_info_priv_unlock();
+
+       d_print("ERROR: '%s'\n", msg);
+}
+
+static void CMUS_FORMAT(2, 3) player_ip_error(int rc, const char *format, ...)
+{
+       char buffer[1024];
+       va_list ap;
+       char *msg;
+       int save = errno;
+
+       va_start(ap, format);
+       vsnprintf(buffer, sizeof(buffer), format, ap);
+       va_end(ap);
+
+       errno = save;
+       msg = ip_get_error_msg(ip, rc, buffer);
+       player_error(msg);
+       free(msg);
+}
+
+static void CMUS_FORMAT(2, 3) player_op_error(int rc, const char *format, ...)
+{
+       char buffer[1024];
+       va_list ap;
+       char *msg;
+       int save = errno;
+
+       va_start(ap, format);
+       vsnprintf(buffer, sizeof(buffer), format, ap);
+       va_end(ap);
+
+       errno = save;
+       msg = op_get_error_msg(rc, buffer);
+       player_error(msg);
+       free(msg);
+}
+
+/*
+ * buffer-fill changed
+ */
+static void _producer_buffer_fill_update(void)
+{
+       int fill;
+
+       player_info_priv_lock();
+       fill = buffer_get_filled_chunks();
+       if (fill != player_info_priv.buffer_fill) {
+/*             d_print("\n"); */
+               player_info_priv.buffer_fill = fill;
+               player_info_priv.buffer_fill_changed = 1;
+       }
+       player_info_priv_unlock();
+}
+
+/*
+ * playing position changed
+ */
+static void _consumer_position_update(void)
+{
+       static unsigned int old_pos = -1;
+       unsigned int pos = 0;
+       long bitrate;
+
+       if (consumer_status == CS_PLAYING || consumer_status == CS_PAUSED)
+               pos = consumer_pos / buffer_second_size();
+       if (pos != old_pos) {
+               old_pos = pos;
+
+               player_info_priv_lock();
+               player_info_priv.pos = pos;
+
+               if (show_current_bitrate) {
+                       bitrate = ip_current_bitrate(ip);
+                       if (bitrate != -1)
+                               player_info_priv.current_bitrate = bitrate;
+               }
+               player_info_priv.position_changed = 1;
+               player_info_priv_unlock();
+       }
+}
+
+/*
+ * something big happened (stopped/paused/unpaused...)
+ */
+static void _player_status_changed(void)
+{
+       unsigned int pos = 0;
+
+/*     d_print("\n"); */
+       if (consumer_status == CS_PLAYING || consumer_status == CS_PAUSED)
+               pos = consumer_pos / buffer_second_size();
+
+       player_info_priv_lock();
+       player_info_priv.status = (enum player_status)consumer_status;
+       player_info_priv.pos = pos;
+       player_info_priv.current_bitrate = -1;
+       player_info_priv.buffer_fill = buffer_get_filled_chunks();
+       player_info_priv.buffer_size = buffer_nr_chunks;
+       player_info_priv.status_changed = 1;
+       player_info_priv_unlock();
+}
+
+/* updating player status }}} */
+
+static void _prebuffer(void)
+{
+       int limit_chunks;
+
+       BUG_ON(producer_status != PS_PLAYING);
+       if (ip_is_remote(ip)) {
+               limit_chunks = buffer_nr_chunks;
+       } else {
+               int limit_ms, limit_size;
+
+               limit_ms = 250;
+               limit_size = limit_ms * buffer_second_size() / 1000;
+               limit_chunks = limit_size / CHUNK_SIZE;
+               if (limit_chunks < 1)
+                       limit_chunks = 1;
+       }
+       while (1) {
+               int nr_read, size, filled;
+               char *wpos;
+
+               filled = buffer_get_filled_chunks();
+/*             d_print("PREBUF: %2d / %2d\n", filled, limit_chunks); */
+
+               /* not fatal */
+               //BUG_ON(filled > limit_chunks);
+
+               if (filled >= limit_chunks)
+                       break;
+
+               size = buffer_get_wpos(&wpos);
+               nr_read = ip_read(ip, wpos, size);
+               if (nr_read < 0) {
+                       if (nr_read == -1 && errno == EAGAIN)
+                               continue;
+                       player_ip_error(nr_read, "reading file %s", ip_get_filename(ip));
+                       /* ip_read sets eof */
+                       nr_read = 0;
+               }
+               if (ip_metadata_changed(ip))
+                       metadata_changed();
+
+               /* buffer_fill with 0 count marks current chunk filled */
+               buffer_fill(nr_read);
+
+               _producer_buffer_fill_update();
+               if (nr_read == 0) {
+                       /* EOF */
+                       break;
+               }
+       }
+}
+
+/* setting producer status {{{ */
+
+static void _producer_status_update(enum producer_status status)
+{
+       producer_status =  status;
+       pthread_cond_broadcast(&producer_playing);
+}
+
+static void _producer_play(void)
+{
+       if (producer_status == PS_UNLOADED) {
+               struct track_info *ti;
+
+               if ((ti = cmus_get_next_track())) {
+                       int rc;
+
+                       ip = ip_new(ti->filename);
+                       rc = ip_open(ip);
+                       if (rc) {
+                               player_ip_error(rc, "opening file `%s'", ti->filename);
+                               ip_delete(ip);
+                               track_info_unref(ti);
+                               file_changed(NULL);
+                       } else {
+                               ip_setup(ip);
+                               _producer_status_update(PS_PLAYING);
+                               file_changed(ti);
+                       }
+               }
+       } else if (producer_status == PS_PLAYING) {
+               if (ip_seek(ip, 0.0) == 0) {
+                       reset_buffer();
+               }
+       } else if (producer_status == PS_STOPPED) {
+               int rc;
+
+               rc = ip_open(ip);
+               if (rc) {
+                       player_ip_error(rc, "opening file `%s'", ip_get_filename(ip));
+                       ip_delete(ip);
+                       _producer_status_update(PS_UNLOADED);
+               } else {
+                       ip_setup(ip);
+                       _producer_status_update(PS_PLAYING);
+               }
+       } else if (producer_status == PS_PAUSED) {
+               _producer_status_update(PS_PLAYING);
+       }
+}
+
+static void _producer_stop(void)
+{
+       if (producer_status == PS_PLAYING || producer_status == PS_PAUSED) {
+               ip_close(ip);
+               _producer_status_update(PS_STOPPED);
+               reset_buffer();
+       }
+}
+
+static void _producer_unload(void)
+{
+       _producer_stop();
+       if (producer_status == PS_STOPPED) {
+               ip_delete(ip);
+               _producer_status_update(PS_UNLOADED);
+       }
+}
+
+static void _producer_pause(void)
+{
+       if (producer_status == PS_PLAYING) {
+               _producer_status_update(PS_PAUSED);
+       } else if (producer_status == PS_PAUSED) {
+               _producer_status_update(PS_PLAYING);
+       }
+}
+
+static void _producer_set_file(struct track_info *ti)
+{
+       _producer_unload();
+       ip = ip_new(ti->filename);
+       _producer_status_update(PS_STOPPED);
+       file_changed(ti);
+}
+
+/* setting producer status }}} */
+
+/* setting consumer status {{{ */
+
+static void _consumer_status_update(enum consumer_status status)
+{
+       consumer_status = status;
+       pthread_cond_broadcast(&consumer_playing);
+}
+
+static void _consumer_play(void)
+{
+       if (consumer_status == CS_PLAYING) {
+               op_drop();
+       } else if (consumer_status == CS_STOPPED) {
+               int rc;
+
+               set_buffer_sf();
+               rc = op_open(buffer_sf, buffer_channel_map);
+               if (rc) {
+                       player_op_error(rc, "opening audio device");
+               } else {
+                       _consumer_status_update(CS_PLAYING);
+               }
+       } else if (consumer_status == CS_PAUSED) {
+               op_unpause();
+               _consumer_status_update(CS_PLAYING);
+       }
+}
+
+static void _consumer_drain_and_stop(void)
+{
+       if (consumer_status == CS_PLAYING || consumer_status == CS_PAUSED) {
+               op_close();
+               _consumer_status_update(CS_STOPPED);
+       }
+}
+
+static void _consumer_stop(void)
+{
+       if (consumer_status == CS_PLAYING || consumer_status == CS_PAUSED) {
+               op_drop();
+               op_close();
+               _consumer_status_update(CS_STOPPED);
+       }
+}
+
+static void _consumer_pause(void)
+{
+       if (consumer_status == CS_PLAYING) {
+               op_pause();
+               _consumer_status_update(CS_PAUSED);
+       } else if (consumer_status == CS_PAUSED) {
+               op_unpause();
+               _consumer_status_update(CS_PLAYING);
+       }
+}
+
+/* setting consumer status }}} */
+
+static int change_sf(int drop)
+{
+       int old_sf = buffer_sf;
+       CHANNEL_MAP(old_channel_map);
+       channel_map_copy(old_channel_map, buffer_channel_map);
+
+       set_buffer_sf();
+       if (buffer_sf != old_sf || !channel_map_equal(buffer_channel_map, old_channel_map, sf_get_channels(buffer_sf))) {
+               /* reopen */
+               int rc;
+
+               if (drop)
+                       op_drop();
+               op_close();
+               rc = op_open(buffer_sf, buffer_channel_map);
+               if (rc) {
+                       player_op_error(rc, "opening audio device");
+                       _consumer_status_update(CS_STOPPED);
+                       _producer_stop();
+                       return rc;
+               }
+       } else if (consumer_status == CS_PAUSED) {
+               op_drop();
+               op_unpause();
+       }
+       _consumer_status_update(CS_PLAYING);
+       return 0;
+}
+
+static void _consumer_handle_eof(void)
+{
+       struct track_info *ti;
+
+       if (ip_is_remote(ip)) {
+               _producer_stop();
+               _consumer_drain_and_stop();
+               player_error("lost connection");
+               return;
+       }
+
+       if (player_info_priv.ti)
+               player_info_priv.ti->play_count++;
+
+       if (player_repeat_current) {
+               if (player_cont) {
+                       ip_seek(ip, 0);
+                       reset_buffer();
+               } else {
+                       _producer_stop();
+                       _consumer_drain_and_stop();
+               }
+               _player_status_changed();
+               return;
+       }
+
+       if ((ti = cmus_get_next_track())) {
+               _producer_unload();
+               ip = ip_new(ti->filename);
+               _producer_status_update(PS_STOPPED);
+               /* PS_STOPPED, CS_PLAYING */
+               if (player_cont) {
+                       _producer_play();
+                       if (producer_status == PS_UNLOADED) {
+                               _consumer_stop();
+                               track_info_unref(ti);
+                               file_changed(NULL);
+                       } else {
+                               /* PS_PLAYING */
+                               file_changed(ti);
+                               if (!change_sf(0))
+                                       _prebuffer();
+                       }
+               } else {
+                       _consumer_drain_and_stop();
+                       file_changed(ti);
+               }
+       } else {
+               _producer_unload();
+               _consumer_drain_and_stop();
+               file_changed(NULL);
+       }
+       _player_status_changed();
+}
+
+static void *consumer_loop(void *arg)
+{
+       while (1) {
+               int rc, space;
+               int size;
+               char *rpos;
+
+               consumer_lock();
+               if (!consumer_running)
+                       break;
+
+               if (consumer_status == CS_PAUSED || consumer_status == CS_STOPPED) {
+                       pthread_cond_wait(&consumer_playing, &consumer_mutex);
+                       consumer_unlock();
+                       continue;
+               }
+               space = op_buffer_space();
+               if (space < 0) {
+                       d_print("op_buffer_space returned %d %s\n", space,
+                                       space == -1 ? strerror(errno) : "");
+
+                       /* try to reopen */
+                       op_close();
+                       _consumer_status_update(CS_STOPPED);
+                       _consumer_play();
+
+                       consumer_unlock();
+                       continue;
+               }
+/*             d_print("BS: %6d %3d\n", space, space * 1000 / (44100 * 2 * 2)); */
+
+               while (1) {
+                       if (space == 0) {
+                               _consumer_position_update();
+                               consumer_unlock();
+                               ms_sleep(25);
+                               break;
+                       }
+                       size = buffer_get_rpos(&rpos);
+                       if (size == 0) {
+                               producer_lock();
+                               if (producer_status != PS_PLAYING) {
+                                       producer_unlock();
+                                       consumer_unlock();
+                                       break;
+                               }
+                               /* must recheck rpos */
+                               size = buffer_get_rpos(&rpos);
+                               if (size == 0) {
+                                       /* OK. now it's safe to check if we are at EOF */
+                                       if (ip_eof(ip)) {
+                                               /* EOF */
+                                               _consumer_handle_eof();
+                                               producer_unlock();
+                                               consumer_unlock();
+                                               break;
+                                       } else {
+                                               /* possible underrun */
+                                               producer_unlock();
+                                               _consumer_position_update();
+                                               consumer_unlock();
+/*                                             d_print("possible underrun\n"); */
+                                               ms_sleep(10);
+                                               break;
+                                       }
+                               }
+
+                               /* player_buffer and ip.eof were inconsistent */
+                               producer_unlock();
+                       }
+                       if (size > space)
+                               size = space;
+                       if (soft_vol || replaygain)
+                               scale_samples(rpos, (unsigned int *)&size);
+                       rc = op_write(rpos, size);
+                       if (rc < 0) {
+                               d_print("op_write returned %d %s\n", rc,
+                                               rc == -1 ? strerror(errno) : "");
+
+                               /* try to reopen */
+                               op_close();
+                               _consumer_status_update(CS_STOPPED);
+                               _consumer_play();
+
+                               consumer_unlock();
+                               break;
+                       }
+                       buffer_consume(rc);
+                       consumer_pos += rc;
+                       space -= rc;
+               }
+       }
+       _consumer_stop();
+       consumer_unlock();
+       return NULL;
+}
+
+static void *producer_loop(void *arg)
+{
+       while (1) {
+               /* number of chunks to fill
+                * too big   => seeking is slow
+                * too small => underruns?
+                */
+               const int chunks = 1;
+               int size, nr_read, i;
+               char *wpos;
+
+               producer_lock();
+               if (!producer_running)
+                       break;
+
+               if (producer_status == PS_UNLOADED ||
+                   producer_status == PS_PAUSED ||
+                   producer_status == PS_STOPPED || ip_eof(ip)) {
+                       pthread_cond_wait(&producer_playing, &producer_mutex);
+                       producer_unlock();
+                       continue;
+               }
+               for (i = 0; ; i++) {
+                       size = buffer_get_wpos(&wpos);
+                       if (size == 0) {
+                               /* buffer is full */
+                               producer_unlock();
+                               ms_sleep(50);
+                               break;
+                       }
+                       nr_read = ip_read(ip, wpos, size);
+                       if (nr_read < 0) {
+                               if (nr_read != -1 || errno != EAGAIN) {
+                                       player_ip_error(nr_read, "reading file %s",
+                                                       ip_get_filename(ip));
+                                       /* ip_read sets eof */
+                                       nr_read = 0;
+                               } else {
+                                       producer_unlock();
+                                       ms_sleep(50);
+                                       break;
+                               }
+                       }
+                       if (ip_metadata_changed(ip))
+                               metadata_changed();
+
+                       /* buffer_fill with 0 count marks current chunk filled */
+                       buffer_fill(nr_read);
+                       if (nr_read == 0) {
+                               /* consumer handles EOF */
+                               producer_unlock();
+                               ms_sleep(50);
+                               break;
+                       }
+                       if (i == chunks) {
+                               producer_unlock();
+                               /* don't sleep! */
+                               break;
+                       }
+               }
+               _producer_buffer_fill_update();
+       }
+       _producer_unload();
+       producer_unlock();
+       return NULL;
+}
+
+void player_init(void)
+{
+       int rc;
+#ifdef REALTIME_SCHEDULING
+       pthread_attr_t attr;
+#endif
+       pthread_attr_t *attrp = NULL;
+
+       /*  1 s is 176400 B (0.168 MB)
+        * 10 s is 1.68 MB
+        */
+       buffer_nr_chunks = 10 * 44100 * 16 / 8 * 2 / CHUNK_SIZE;
+       buffer_init();
+
+#ifdef REALTIME_SCHEDULING
+       rc = pthread_attr_init(&attr);
+       BUG_ON(rc);
+       rc = pthread_attr_setschedpolicy(&attr, SCHED_RR);
+       if (rc) {
+               d_print("could not set real-time scheduling priority: %s\n", strerror(rc));
+       } else {
+               struct sched_param param;
+
+               d_print("using real-time scheduling\n");
+               param.sched_priority = sched_get_priority_max(SCHED_RR);
+               d_print("setting priority to %d\n", param.sched_priority);
+               rc = pthread_attr_setschedparam(&attr, &param);
+               BUG_ON(rc);
+               attrp = &attr;
+       }
+#endif
+
+       rc = pthread_create(&producer_thread, NULL, producer_loop, NULL);
+       BUG_ON(rc);
+
+       rc = pthread_create(&consumer_thread, attrp, consumer_loop, NULL);
+       if (rc && attrp) {
+               d_print("could not create thread using real-time scheduling: %s\n", strerror(rc));
+               rc = pthread_create(&consumer_thread, NULL, consumer_loop, NULL);
+       }
+       BUG_ON(rc);
+
+       /* update player_info_priv.cont etc. */
+       player_lock();
+       _player_status_changed();
+       player_unlock();
+}
+
+void player_exit(void)
+{
+       int rc;
+
+       player_lock();
+       consumer_running = 0;
+       pthread_cond_broadcast(&consumer_playing);
+       producer_running = 0;
+       pthread_cond_broadcast(&producer_playing);
+       player_unlock();
+
+       rc = pthread_join(consumer_thread, NULL);
+       BUG_ON(rc);
+       rc = pthread_join(producer_thread, NULL);
+       BUG_ON(rc);
+       buffer_free();
+}
+
+void player_stop(void)
+{
+       player_lock();
+       _consumer_stop();
+       _producer_stop();
+       _player_status_changed();
+       player_unlock();
+}
+
+void player_play(void)
+{
+       int prebuffer;
+
+       player_lock();
+       if (producer_status == PS_PLAYING && ip_is_remote(ip)) {
+               /* seeking not allowed */
+               player_unlock();
+               return;
+       }
+       prebuffer = consumer_status == CS_STOPPED;
+       _producer_play();
+       if (producer_status == PS_PLAYING) {
+               _consumer_play();
+               if (consumer_status != CS_PLAYING)
+                       _producer_stop();
+       } else {
+               _consumer_stop();
+       }
+       _player_status_changed();
+       if (consumer_status == CS_PLAYING && prebuffer)
+               _prebuffer();
+       player_unlock();
+}
+
+void player_pause(void)
+{
+       if (ip && ip_is_remote(ip) && consumer_status == CS_PLAYING) {
+               /* pausing not allowed */
+               player_stop();
+               return;
+       }
+       player_lock();
+
+       if (consumer_status == CS_STOPPED) {
+               _producer_play();
+               if (producer_status == PS_PLAYING) {
+                       _consumer_play();
+                       if (consumer_status != CS_PLAYING)
+                               _producer_stop();
+               }
+               _player_status_changed();
+               if (consumer_status == CS_PLAYING)
+                       _prebuffer();
+               player_unlock();
+               return;
+       }
+
+       _producer_pause();
+       _consumer_pause();
+       _player_status_changed();
+       player_unlock();
+}
+
+void player_pause_playback(void)
+{
+       if (consumer_status == CS_PLAYING)
+               player_pause();
+}
+
+void player_set_file(struct track_info *ti)
+{
+       player_lock();
+       _producer_set_file(ti);
+       if (producer_status == PS_UNLOADED) {
+               _consumer_stop();
+               goto out;
+       }
+
+       /* PS_STOPPED */
+       if (consumer_status == CS_PLAYING || consumer_status == CS_PAUSED) {
+               op_drop();
+               _producer_play();
+               if (producer_status == PS_UNLOADED) {
+                       _consumer_stop();
+                       goto out;
+               }
+               change_sf(1);
+       }
+out:
+       _player_status_changed();
+       if (producer_status == PS_PLAYING)
+               _prebuffer();
+       player_unlock();
+}
+
+void player_play_file(struct track_info *ti)
+{
+       player_lock();
+       _producer_set_file(ti);
+       if (producer_status == PS_UNLOADED) {
+               _consumer_stop();
+               goto out;
+       }
+
+       /* PS_STOPPED */
+       _producer_play();
+
+       /* PS_UNLOADED,PS_PLAYING */
+       if (producer_status == PS_UNLOADED) {
+               _consumer_stop();
+               goto out;
+       }
+
+       /* PS_PLAYING */
+       if (consumer_status == CS_STOPPED) {
+               _consumer_play();
+               if (consumer_status == CS_STOPPED)
+                       _producer_stop();
+       } else {
+               op_drop();
+               change_sf(1);
+       }
+out:
+       _player_status_changed();
+       if (producer_status == PS_PLAYING)
+               _prebuffer();
+       player_unlock();
+}
+
+void player_file_changed(struct track_info *ti)
+{
+       _file_changed(ti);
+}
+
+void player_seek(double offset, int relative, int start_playing)
+{
+       int stopped = 0;
+       player_lock();
+       if (consumer_status == CS_STOPPED) {
+               stopped = 1;
+               _producer_play();
+               if (producer_status == PS_PLAYING) {
+                       _consumer_play();
+                       if (consumer_status != CS_PLAYING) {
+                               _producer_stop();
+                               player_unlock();
+                               return;
+                       } else
+                               _player_status_changed();
+               }
+       }
+       if (consumer_status == CS_PLAYING || consumer_status == CS_PAUSED) {
+               double pos, duration, new_pos;
+               int rc;
+
+               pos = (double)consumer_pos / (double)buffer_second_size();
+               duration = ip_duration(ip);
+               if (duration < 0) {
+                       /* can't seek */
+                       d_print("can't seek\n");
+                       player_unlock();
+                       return;
+               }
+               if (relative) {
+                       new_pos = pos + offset;
+                       if (new_pos < 0.0)
+                               new_pos = 0.0;
+                       if (offset > 0.0) {
+                               /* seeking forward */
+                               if (new_pos > duration) {
+                                       player_unlock();
+                                       cmus_next();
+                                       return;
+                               }
+                               if (new_pos < 0.0)
+                                       new_pos = 0.0;
+                               if (new_pos < pos - 0.5) {
+                                       /* must seek at least 0.5s */
+                                       d_print("must seek at least 0.5s\n");
+                                       player_unlock();
+                                       return;
+                               }
+                       }
+               } else {
+                       new_pos = offset;
+                       if (new_pos < 0.0) {
+                               d_print("seek offset negative\n");
+                               player_unlock();
+                               return;
+                       }
+                       if (new_pos > duration - 5.0) {
+                               new_pos = duration - 5.0;
+                               if (new_pos < 0.0)
+                                       new_pos = 0.0;
+                       }
+               }
+/*             d_print("seeking %g/%g (%g from eof)\n", new_pos, duration, duration - new_pos); */
+               rc = ip_seek(ip, new_pos);
+               if (rc == 0) {
+                       d_print("doing op_drop after seek\n");
+                       op_drop();
+                       reset_buffer();
+                       consumer_pos = new_pos * buffer_second_size();
+                       scale_pos = consumer_pos;
+                       _consumer_position_update();
+                       if (stopped && !start_playing) {
+                               _producer_pause();
+                               _consumer_pause();
+                               _player_status_changed();
+                       }
+               } else {
+                       player_ip_error(rc, "seeking in file %s", ip_get_filename(ip));
+                       d_print("error: ip_seek returned %d\n", rc);
+               }
+       }
+       mpris_seeked();
+       player_unlock();
+}
+
+/*
+ * change output plugin without stopping playback
+ */
+void player_set_op(const char *name)
+{
+       int rc;
+
+       player_lock();
+
+       /* drop needed because close drains the buffer */
+       if (consumer_status == CS_PAUSED)
+               op_drop();
+
+       if (consumer_status == CS_PLAYING || consumer_status == CS_PAUSED)
+               op_close();
+
+       if (name) {
+               d_print("setting op to '%s'\n", name);
+               rc = op_select(name);
+       } else {
+               /* first initialized plugin */
+               d_print("selecting first initialized op\n");
+               rc = op_select_any();
+       }
+       if (rc) {
+               _consumer_status_update(CS_STOPPED);
+
+               _producer_stop();
+               if (name)
+                       player_op_error(rc, "selecting output plugin '%s'", name);
+               else
+                       player_op_error(rc, "selecting any output plugin");
+               player_unlock();
+               return;
+       }
+
+       if (consumer_status == CS_PLAYING || consumer_status == CS_PAUSED) {
+               set_buffer_sf();
+               rc = op_open(buffer_sf, buffer_channel_map);
+               if (rc) {
+                       _consumer_status_update(CS_STOPPED);
+                       _producer_stop();
+                       player_op_error(rc, "opening audio device");
+                       player_unlock();
+                       return;
+               }
+               if (consumer_status == CS_PAUSED)
+                       op_pause();
+       }
+
+       player_unlock();
+}
+
+void player_set_buffer_chunks(unsigned int nr_chunks)
+{
+       player_lock();
+       _producer_stop();
+       _consumer_stop();
+
+       buffer_nr_chunks = nr_chunks;
+       buffer_init();
+
+       _player_status_changed();
+       player_unlock();
+}
+
+int player_get_buffer_chunks(void)
+{
+       return buffer_nr_chunks;
+}
+
+void player_set_soft_volume(int l, int r)
+{
+       consumer_lock();
+       soft_vol_l = l;
+       soft_vol_r = r;
+       consumer_unlock();
+}
+
+void player_set_soft_vol(int soft)
+{
+       consumer_lock();
+       /* don't mess with scale_pos if soft_vol or replaygain is already enabled */
+       if (!soft_vol && !replaygain)
+               scale_pos = consumer_pos;
+       soft_vol = soft;
+       consumer_unlock();
+}
+
+static int calc_vol(int val, int old, int max_vol, unsigned int flags)
+{
+       if (flags & VF_RELATIVE) {
+               if (flags & VF_PERCENTAGE)
+                       val = scale_from_percentage(val, max_vol);
+               val += old;
+       } else if (flags & VF_PERCENTAGE) {
+               val = scale_from_percentage(val, max_vol);
+       }
+       return clamp(val, 0, max_vol);
+}
+
+int player_set_vol(int l, int lf, int r, int rf)
+{
+       int rc = OP_ERROR_SUCCESS;
+       if (soft_vol) {
+               l = calc_vol(l, soft_vol_l, 100, lf);
+               r = calc_vol(r, soft_vol_r, 100, rf);
+               player_set_soft_volume(l, r);
+       } else {
+               mixer_read_volume();
+               l = calc_vol(l, volume_l, volume_max, lf);
+               r = calc_vol(r, volume_r, volume_max, rf);
+               rc = mixer_set_volume(l, r);
+               mixer_read_volume();
+       }
+       return rc;
+}
+
+void player_set_rg(enum replaygain rg)
+{
+       player_lock();
+       /* don't mess with scale_pos if soft_vol or replaygain is already enabled */
+       if (!soft_vol && !replaygain)
+               scale_pos = consumer_pos;
+       replaygain = rg;
+
+       player_info_priv_lock();
+       update_rg_scale();
+       player_info_priv_unlock();
+
+       player_unlock();
+}
+
+void player_set_rg_limit(int limit)
+{
+       player_lock();
+       replaygain_limit = limit;
+
+       player_info_priv_lock();
+       update_rg_scale();
+       player_info_priv_unlock();
+
+       player_unlock();
+}
+
+void player_set_rg_preamp(double db)
+{
+       player_lock();
+       replaygain_preamp = db;
+
+       player_info_priv_lock();
+       update_rg_scale();
+       player_info_priv_unlock();
+
+       player_unlock();
+}
+
+void player_info_snapshot(void)
+{
+       player_info_priv_lock();
+
+       free(player_info.error_msg);
+       if (player_info.ti)
+               track_info_unref(player_info.ti);
+       memcpy(&player_info, &player_info_priv, sizeof(player_info));
+       if (player_info.ti)
+               track_info_ref(player_info.ti);
+
+       player_info_priv.file_changed = 0;
+       player_info_priv.metadata_changed = 0;
+       player_info_priv.status_changed = 0;
+       player_info_priv.position_changed = 0;
+       player_info_priv.buffer_fill_changed = 0;
+       player_info_priv.error_msg = NULL;
+
+       player_info_priv_unlock();
+}
+
+void player_metadata_lock(void)
+{
+       cmus_mutex_lock(&player_info_mutex);
+}
+
+void player_metadata_unlock(void)
+{
+       cmus_mutex_unlock(&player_info_mutex);
+}
diff --git a/player.h b/player.h
new file mode 100644 (file)
index 0000000..7315827
--- /dev/null
+++ b/player.h
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_PLAYER_H
+#define CMUS_PLAYER_H
+
+#include "locking.h"
+#include "track_info.h"
+
+#include <pthread.h>
+
+enum {
+       /* no error */
+       PLAYER_ERROR_SUCCESS,
+       /* system error (error code in errno) */
+       PLAYER_ERROR_ERRNO,
+       /* function not supported */
+       PLAYER_ERROR_NOT_SUPPORTED
+};
+
+extern const char * const player_status_names[];
+enum player_status {
+       PLAYER_STATUS_STOPPED,
+       PLAYER_STATUS_PLAYING,
+       PLAYER_STATUS_PAUSED,
+       NR_PLAYER_STATUS
+};
+
+enum replaygain {
+       RG_DISABLED,
+       RG_TRACK,
+       RG_ALBUM,
+       RG_TRACK_PREFERRED,
+       RG_ALBUM_PREFERRED
+};
+
+struct player_info {
+       /* current track */
+       struct track_info *ti;
+
+       /* status */
+       enum player_status status;
+       int pos;
+       int current_bitrate;
+
+       int buffer_fill;
+       int buffer_size;
+
+       /* display this if not NULL */
+       char *error_msg;
+
+       unsigned int file_changed : 1;
+       unsigned int metadata_changed : 1;
+       unsigned int status_changed : 1;
+       unsigned int position_changed : 1;
+       unsigned int buffer_fill_changed : 1;
+};
+
+extern char player_metadata[255 * 16 + 1];
+extern struct player_info player_info;
+extern int player_cont;
+extern int player_repeat_current;
+extern enum replaygain replaygain;
+extern int replaygain_limit;
+extern double replaygain_preamp;
+extern int soft_vol;
+extern int soft_vol_l;
+extern int soft_vol_r;
+
+void player_init(void);
+void player_exit(void);
+
+/* set current file */
+void player_set_file(struct track_info *ti);
+
+/* set current file and start playing */
+void player_play_file(struct track_info *ti);
+
+/* update track info */
+void player_file_changed(struct track_info *ti);
+
+void player_play(void);
+void player_stop(void);
+void player_pause(void);
+void player_pause_playback(void);
+void player_seek(double offset, int relative, int start_playing);
+void player_set_op(const char *name);
+void player_set_buffer_chunks(unsigned int nr_chunks);
+int player_get_buffer_chunks(void);
+void player_info_snapshot(void);
+
+void player_set_soft_volume(int l, int r);
+void player_set_soft_vol(int soft);
+void player_set_rg(enum replaygain rg);
+void player_set_rg_limit(int limit);
+void player_set_rg_preamp(double db);
+
+#define VF_RELATIVE    0x01
+#define VF_PERCENTAGE  0x02
+int player_set_vol(int l, int lf, int r, int rf);
+
+void player_metadata_lock(void);
+void player_metadata_unlock(void);
+
+#endif
diff --git a/prog.c b/prog.c
new file mode 100644 (file)
index 0000000..cefd12f
--- /dev/null
+++ b/prog.c
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "prog.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdarg.h>
+#include <errno.h>
+
+char *program_name = NULL;
+
+void warn(const char *format, ...)
+{
+       va_list ap;
+
+       fprintf(stderr, "%s: ", program_name);
+       va_start(ap, format);
+       vfprintf(stderr, format, ap);
+       va_end(ap);
+}
+
+void warn_errno(const char *format, ...)
+{
+       int e = errno;
+       va_list ap;
+
+       fprintf(stderr, "%s: ", program_name);
+       va_start(ap, format);
+       vfprintf(stderr, format, ap);
+       va_end(ap);
+       fprintf(stderr, ": %s\n", strerror(e));
+}
+
+void CMUS_NORETURN die(const char *format, ...)
+{
+       va_list ap;
+
+       fprintf(stderr, "%s: ", program_name);
+       va_start(ap, format);
+       vfprintf(stderr, format, ap);
+       va_end(ap);
+       exit(1);
+}
+
+void CMUS_NORETURN die_errno(const char *format, ...)
+{
+       int e = errno;
+       va_list ap;
+
+       fprintf(stderr, "%s: ", program_name);
+       va_start(ap, format);
+       vfprintf(stderr, format, ap);
+       va_end(ap);
+       fprintf(stderr, ": %s\n", strerror(e));
+       exit(1);
+}
+
+static int short_option(int ch, const struct option *options)
+{
+       int i;
+
+       for (i = 0; ; i++) {
+               if (!options[i].short_opt) {
+                       if (!options[i].long_opt)
+                               die("unrecognized option `-%c'\n", ch);
+                       continue;
+               }
+               if (options[i].short_opt != ch)
+                       continue;
+               return i;
+       }
+}
+
+static int long_option(const char *opt, const struct option *options)
+{
+       int len, i, idx, num;
+
+       len = strlen(opt);
+       idx = -1;
+       num = 0;
+       for (i = 0; options[i].short_opt || options[i].long_opt; i++) {
+               if (options[i].long_opt && strncmp(opt, options[i].long_opt, len) == 0) {
+                       idx = i;
+                       num++;
+                       if (options[i].long_opt[len] == 0) {
+                               /* exact */
+                               num = 1;
+                               break;
+                       }
+               }
+       }
+       if (num > 1)
+               die("option `--%s' is ambiguous\n", opt);
+       if (num == 0)
+               die("unrecognized option `--%s'\n", opt);
+       return idx;
+}
+
+int get_option(char **argvp[], const struct option *options, char **arg)
+{
+       char **argv = *argvp;
+       const char *opt = *argv;
+       int i;
+
+       *arg = NULL;
+       if (opt == NULL || opt[0] != '-' || opt[1] == 0)
+               return -1;
+
+       if (opt[1] == '-') {
+               if (opt[2] == 0) {
+                       /* '--' => no more options */
+                       *argvp = argv + 1;
+                       return -1;
+               }
+               i = long_option(opt + 2, options);
+       } else if (opt[2]) {
+               return -1;
+       } else {
+               i = short_option(opt[1], options);
+       }
+       argv++;
+       if (options[i].has_arg) {
+               if (*argv == NULL)
+                       die("option `%s' requires an argument\n", opt);
+               *arg = *argv++;
+       }
+       *argvp = argv;
+       return i;
+}
diff --git a/prog.h b/prog.h
new file mode 100644 (file)
index 0000000..17acb84
--- /dev/null
+++ b/prog.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_PROG_H
+#define CMUS_PROG_H
+
+#include "compiler.h"
+
+/* set in beginning of main */
+extern char *program_name;
+
+struct option {
+       /* short option or 0 */
+       int short_opt;
+
+       /* long option or NULL */
+       const char *long_opt;
+
+       /* does option have an argument */
+       int has_arg;
+};
+
+/*
+ * arg: returned argument if .has_arg is 1
+ *
+ * returns: index to options array or -1 of no more options
+ */
+int get_option(char **argv[], const struct option *options, char **arg);
+
+void warn(const char *format, ...) CMUS_FORMAT(1, 2);
+void warn_errno(const char *format, ...) CMUS_FORMAT(1, 2);
+void die(const char *format, ...) CMUS_FORMAT(1, 2) CMUS_NORETURN;
+void die_errno(const char *format, ...) CMUS_FORMAT(1, 2) CMUS_NORETURN;
+
+#endif
diff --git a/rbtree.c b/rbtree.c
new file mode 100644 (file)
index 0000000..bfdd53c
--- /dev/null
+++ b/rbtree.c
@@ -0,0 +1,384 @@
+/* Stolen from Linux 2.6.34 */
+
+/*
+  Red Black Trees
+  (C) 1999  Andrea Arcangeli <andrea@suse.de>
+  (C) 2002  David Woodhouse <dwmw2@infradead.org>
+
+  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, see <http://www.gnu.org/licenses/>.
+
+  linux/lib/rbtree.c
+*/
+
+#include "rbtree.h"
+
+static void _rb_rotate_left(struct rb_node *node, struct rb_root *root)
+{
+       struct rb_node *right = node->rb_right;
+       struct rb_node *parent = rb_parent(node);
+
+       if ((node->rb_right = right->rb_left))
+               rb_set_parent(right->rb_left, node);
+       right->rb_left = node;
+
+       rb_set_parent(right, parent);
+
+       if (parent)
+       {
+               if (node == parent->rb_left)
+                       parent->rb_left = right;
+               else
+                       parent->rb_right = right;
+       }
+       else
+               root->rb_node = right;
+       rb_set_parent(node, right);
+}
+
+static void _rb_rotate_right(struct rb_node *node, struct rb_root *root)
+{
+       struct rb_node *left = node->rb_left;
+       struct rb_node *parent = rb_parent(node);
+
+       if ((node->rb_left = left->rb_right))
+               rb_set_parent(left->rb_right, node);
+       left->rb_right = node;
+
+       rb_set_parent(left, parent);
+
+       if (parent)
+       {
+               if (node == parent->rb_right)
+                       parent->rb_right = left;
+               else
+                       parent->rb_left = left;
+       }
+       else
+               root->rb_node = left;
+       rb_set_parent(node, left);
+}
+
+void rb_insert_color(struct rb_node *node, struct rb_root *root)
+{
+       struct rb_node *parent, *gparent;
+
+       while ((parent = rb_parent(node)) && rb_is_red(parent))
+       {
+               gparent = rb_parent(parent);
+
+               if (parent == gparent->rb_left)
+               {
+                       {
+                               register struct rb_node *uncle = gparent->rb_right;
+                               if (uncle && rb_is_red(uncle))
+                               {
+                                       rb_set_black(uncle);
+                                       rb_set_black(parent);
+                                       rb_set_red(gparent);
+                                       node = gparent;
+                                       continue;
+                               }
+                       }
+
+                       if (parent->rb_right == node)
+                       {
+                               register struct rb_node *tmp;
+                               _rb_rotate_left(parent, root);
+                               tmp = parent;
+                               parent = node;
+                               node = tmp;
+                       }
+
+                       rb_set_black(parent);
+                       rb_set_red(gparent);
+                       _rb_rotate_right(gparent, root);
+               } else {
+                       {
+                               register struct rb_node *uncle = gparent->rb_left;
+                               if (uncle && rb_is_red(uncle))
+                               {
+                                       rb_set_black(uncle);
+                                       rb_set_black(parent);
+                                       rb_set_red(gparent);
+                                       node = gparent;
+                                       continue;
+                               }
+                       }
+
+                       if (parent->rb_left == node)
+                       {
+                               register struct rb_node *tmp;
+                               _rb_rotate_right(parent, root);
+                               tmp = parent;
+                               parent = node;
+                               node = tmp;
+                       }
+
+                       rb_set_black(parent);
+                       rb_set_red(gparent);
+                       _rb_rotate_left(gparent, root);
+               }
+       }
+
+       rb_set_black(root->rb_node);
+}
+
+static void _rb_erase_color(struct rb_node *node, struct rb_node *parent,
+                            struct rb_root *root)
+{
+       struct rb_node *other;
+
+       while ((!node || rb_is_black(node)) && node != root->rb_node)
+       {
+               if (parent->rb_left == node)
+               {
+                       other = parent->rb_right;
+                       if (rb_is_red(other))
+                       {
+                               rb_set_black(other);
+                               rb_set_red(parent);
+                               _rb_rotate_left(parent, root);
+                               other = parent->rb_right;
+                       }
+                       if ((!other->rb_left || rb_is_black(other->rb_left)) &&
+                           (!other->rb_right || rb_is_black(other->rb_right)))
+                       {
+                               rb_set_red(other);
+                               node = parent;
+                               parent = rb_parent(node);
+                       }
+                       else
+                       {
+                               if (!other->rb_right || rb_is_black(other->rb_right))
+                               {
+                                       rb_set_black(other->rb_left);
+                                       rb_set_red(other);
+                                       _rb_rotate_right(other, root);
+                                       other = parent->rb_right;
+                               }
+                               rb_set_color(other, rb_color(parent));
+                               rb_set_black(parent);
+                               rb_set_black(other->rb_right);
+                               _rb_rotate_left(parent, root);
+                               node = root->rb_node;
+                               break;
+                       }
+               }
+               else
+               {
+                       other = parent->rb_left;
+                       if (rb_is_red(other))
+                       {
+                               rb_set_black(other);
+                               rb_set_red(parent);
+                               _rb_rotate_right(parent, root);
+                               other = parent->rb_left;
+                       }
+                       if ((!other->rb_left || rb_is_black(other->rb_left)) &&
+                           (!other->rb_right || rb_is_black(other->rb_right)))
+                       {
+                               rb_set_red(other);
+                               node = parent;
+                               parent = rb_parent(node);
+                       }
+                       else
+                       {
+                               if (!other->rb_left || rb_is_black(other->rb_left))
+                               {
+                                       rb_set_black(other->rb_right);
+                                       rb_set_red(other);
+                                       _rb_rotate_left(other, root);
+                                       other = parent->rb_left;
+                               }
+                               rb_set_color(other, rb_color(parent));
+                               rb_set_black(parent);
+                               rb_set_black(other->rb_left);
+                               _rb_rotate_right(parent, root);
+                               node = root->rb_node;
+                               break;
+                       }
+               }
+       }
+       if (node)
+               rb_set_black(node);
+}
+
+void rb_erase(struct rb_node *node, struct rb_root *root)
+{
+       struct rb_node *child, *parent;
+       int color;
+
+       if (!node->rb_left)
+               child = node->rb_right;
+       else if (!node->rb_right)
+               child = node->rb_left;
+       else
+       {
+               struct rb_node *old = node, *left;
+
+               node = node->rb_right;
+               while ((left = node->rb_left) != NULL)
+                       node = left;
+
+               if (rb_parent(old)) {
+                       if (rb_parent(old)->rb_left == old)
+                               rb_parent(old)->rb_left = node;
+                       else
+                               rb_parent(old)->rb_right = node;
+               } else
+                       root->rb_node = node;
+
+               child = node->rb_right;
+               parent = rb_parent(node);
+               color = rb_color(node);
+
+               if (parent == old) {
+                       parent = node;
+               } else {
+                       if (child)
+                               rb_set_parent(child, parent);
+                       parent->rb_left = child;
+
+                       node->rb_right = old->rb_right;
+                       rb_set_parent(old->rb_right, node);
+               }
+
+               node->rb_parent_color = old->rb_parent_color;
+               node->rb_left = old->rb_left;
+               rb_set_parent(old->rb_left, node);
+
+               goto color;
+       }
+
+       parent = rb_parent(node);
+       color = rb_color(node);
+
+       if (child)
+               rb_set_parent(child, parent);
+       if (parent)
+       {
+               if (parent->rb_left == node)
+                       parent->rb_left = child;
+               else
+                       parent->rb_right = child;
+       }
+       else
+               root->rb_node = child;
+
+ color:
+       if (color == RB_BLACK)
+               _rb_erase_color(child, parent, root);
+}
+
+/*
+ * This function returns the first node (in sort order) of the tree.
+ */
+struct rb_node *rb_first(const struct rb_root *root)
+{
+       struct rb_node  *n;
+
+       n = root->rb_node;
+       if (!n)
+               return NULL;
+       while (n->rb_left)
+               n = n->rb_left;
+       return n;
+}
+
+struct rb_node *rb_last(const struct rb_root *root)
+{
+       struct rb_node  *n;
+
+       n = root->rb_node;
+       if (!n)
+               return NULL;
+       while (n->rb_right)
+               n = n->rb_right;
+       return n;
+}
+
+struct rb_node *rb_next(const struct rb_node *node)
+{
+       struct rb_node *parent;
+
+       if (rb_parent(node) == node)
+               return NULL;
+
+       /* If we have a right-hand child, go down and then left as far
+          as we can. */
+       if (node->rb_right) {
+               node = node->rb_right;
+               while (node->rb_left)
+                       node=node->rb_left;
+               return (struct rb_node *)node;
+       }
+
+       /* No right-hand children.  Everything down and left is
+          smaller than us, so any 'next' node must be in the general
+          direction of our parent. Go up the tree; any time the
+          ancestor is a right-hand child of its parent, keep going
+          up. First time it's a left-hand child of its parent, said
+          parent is our 'next' node. */
+       while ((parent = rb_parent(node)) && node == parent->rb_right)
+               node = parent;
+
+       return parent;
+}
+
+struct rb_node *rb_prev(const struct rb_node *node)
+{
+       struct rb_node *parent;
+
+       if (rb_parent(node) == node)
+               return NULL;
+
+       /* If we have a left-hand child, go down and then right as far
+          as we can. */
+       if (node->rb_left) {
+               node = node->rb_left;
+               while (node->rb_right)
+                       node=node->rb_right;
+               return (struct rb_node *)node;
+       }
+
+       /* No left-hand children. Go up till we find an ancestor which
+          is a right-hand child of its parent */
+       while ((parent = rb_parent(node)) && node == parent->rb_left)
+               node = parent;
+
+       return parent;
+}
+
+void rb_replace_node(struct rb_node *victim, struct rb_node *new,
+                    struct rb_root *root)
+{
+       struct rb_node *parent = rb_parent(victim);
+
+       /* Set the surrounding nodes to point to the replacement */
+       if (parent) {
+               if (victim == parent->rb_left)
+                       parent->rb_left = new;
+               else
+                       parent->rb_right = new;
+       } else {
+               root->rb_node = new;
+       }
+       if (victim->rb_left)
+               rb_set_parent(victim->rb_left, new);
+       if (victim->rb_right)
+               rb_set_parent(victim->rb_right, new);
+
+       /* Copy the pointers/colour from the victim to the replacement */
+       *new = *victim;
+}
diff --git a/rbtree.h b/rbtree.h
new file mode 100644 (file)
index 0000000..15faa37
--- /dev/null
+++ b/rbtree.h
@@ -0,0 +1,223 @@
+/* Stolen from Linux 2.6.34 */
+
+/*
+  Red Black Trees
+  (C) 1999  Andrea Arcangeli <andrea@suse.de>
+
+  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, see <http://www.gnu.org/licenses/>.
+
+  linux/include/linux/rbtree.h
+
+  To use rbtrees you'll have to implement your own insert and search cores.
+  This will avoid us to use callbacks and to drop drammatically performances.
+  I know it's not the cleaner way,  but in C (not in C++) to get
+  performances and genericity...
+
+  Some example of insert and search follows here. The search is a plain
+  normal search over an ordered tree. The insert instead must be implemented
+  int two steps: as first thing the code must insert the element in
+  order as a red leaf in the tree, then the support library function
+  rb_insert_color() must be called. Such function will do the
+  not trivial work to rebalance the rbtree if necessary.
+
+-----------------------------------------------------------------------
+static inline struct page * rb_search_page_cache(struct inode * inode,
+                                                unsigned long offset)
+{
+       struct rb_node * n = inode->i_rb_page_cache.rb_node;
+       struct page * page;
+
+       while (n)
+       {
+               page = rb_entry(n, struct page, rb_page_cache);
+
+               if (offset < page->offset)
+                       n = n->rb_left;
+               else if (offset > page->offset)
+                       n = n->rb_right;
+               else
+                       return page;
+       }
+       return NULL;
+}
+
+static inline struct page * _rb_insert_page_cache(struct inode * inode,
+                                                  unsigned long offset,
+                                                  struct rb_node * node)
+{
+       struct rb_node ** p = &inode->i_rb_page_cache.rb_node;
+       struct rb_node * parent = NULL;
+       struct page * page;
+
+       while (*p)
+       {
+               parent = *p;
+               page = rb_entry(parent, struct page, rb_page_cache);
+
+               if (offset < page->offset)
+                       p = &(*p)->rb_left;
+               else if (offset > page->offset)
+                       p = &(*p)->rb_right;
+               else
+                       return page;
+       }
+
+       rb_link_node(node, parent, p);
+
+       return NULL;
+}
+
+static inline struct page * rb_insert_page_cache(struct inode * inode,
+                                                unsigned long offset,
+                                                struct rb_node * node)
+{
+       struct page * ret;
+       if ((ret = _rb_insert_page_cache(inode, offset, node)))
+               goto out;
+       rb_insert_color(node, &inode->i_rb_page_cache);
+ out:
+       return ret;
+}
+-----------------------------------------------------------------------
+*/
+
+#ifndef CMUS_RBTREE_H
+#define CMUS_RBTREE_H
+
+#include "compiler.h" /* container_of */
+#include <stddef.h>
+
+struct rb_node
+{
+       unsigned long  rb_parent_color;
+#define        RB_RED          0
+#define        RB_BLACK        1
+       struct rb_node *rb_right;
+       struct rb_node *rb_left;
+} __attribute__((aligned(sizeof(long))));
+    /* The alignment might seem pointless, but allegedly CRIS needs it */
+
+struct rb_root
+{
+       struct rb_node *rb_node;
+};
+
+
+#define rb_parent(r)   ((struct rb_node *)((r)->rb_parent_color & ~3))
+#define rb_color(r)   ((r)->rb_parent_color & 1)
+#define rb_is_red(r)   (!rb_color(r))
+#define rb_is_black(r) rb_color(r)
+#define rb_set_red(r)  do { (r)->rb_parent_color &= ~1; } while (0)
+#define rb_set_black(r)  do { (r)->rb_parent_color |= 1; } while (0)
+
+static inline void rb_set_parent(struct rb_node *rb, struct rb_node *p)
+{
+       rb->rb_parent_color = (rb->rb_parent_color & 3) | (unsigned long)p;
+}
+static inline void rb_set_color(struct rb_node *rb, int color)
+{
+       rb->rb_parent_color = (rb->rb_parent_color & ~1) | color;
+}
+
+#define RB_ROOT        (struct rb_root) { NULL, }
+#define        rb_entry(ptr, type, member) container_of(ptr, type, member)
+
+#define RB_EMPTY_ROOT(root)    ((root)->rb_node == NULL)
+#define RB_EMPTY_NODE(node)    (rb_parent(node) == node)
+#define RB_CLEAR_NODE(node)    (rb_set_parent(node, node))
+
+void rb_insert_color(struct rb_node *, struct rb_root *);
+void rb_erase(struct rb_node *, struct rb_root *);
+
+/* Find logical next and previous nodes in a tree */
+struct rb_node *rb_next(const struct rb_node *);
+struct rb_node *rb_prev(const struct rb_node *);
+struct rb_node *rb_first(const struct rb_root *);
+struct rb_node *rb_last(const struct rb_root *);
+
+/* Fast replacement of a single node without remove/rebalance/add/rebalance */
+void rb_replace_node(struct rb_node *victim, struct rb_node *new,
+                       struct rb_root *root);
+
+static inline void rb_link_node(struct rb_node * node, struct rb_node * parent,
+                               struct rb_node ** rb_link)
+{
+       node->rb_parent_color = (unsigned long )parent;
+       node->rb_left = node->rb_right = NULL;
+
+       *rb_link = node;
+}
+
+
+/* Cmus extensions */
+
+static inline void rb_root_init(struct rb_root *root)
+{
+       root->rb_node = NULL;
+}
+
+static inline int rb_root_empty(struct rb_root *root)
+{
+       return RB_EMPTY_ROOT(root);
+}
+
+/**
+ * rb_for_each        -       iterate over a rbtree
+ * @pos:        the &struct rb_node to use as a loop counter.
+ * @root:       the root for your rbtree.
+ */
+#define rb_for_each(pos, root) \
+       for (pos = rb_first(root); pos; pos = rb_next(pos))
+
+/**
+ * rb_for_each_prev   -       iterate over a rbtree backwards
+ * @pos:        the &struct rb_node to use as a loop counter.
+ * @root:       the root for your rbtree.
+ */
+#define rb_for_each_prev(pos, root) \
+       for (pos = rb_last(root); pos; pos = rb_prev(pos))
+
+/**
+ * rb_for_each_safe   -       iterate over a rbtree safe against removal of rbtree node
+ * @pos:        the &struct rb_node to use as a loop counter.
+ * @n:          another &struct rb_node to use as temporary storage
+ * @root:       the root for your rbtree.
+ */
+#define rb_for_each_safe(pos, n, root) \
+       for (pos = rb_first(root), n = pos ? rb_next(pos) : NULL; pos; \
+               pos = n, n = pos ? rb_next(pos) : NULL)
+
+/**
+ * rb_for_each_entry        -       iterate over a rbtree of given type
+ * @pos:        the &struct rb_node to use as a loop counter.
+ * @t:          the type * to use as a loop counter.
+ * @root:       the root for your rbtree.
+ * @member:    the name of the rb_node-struct within the struct.
+ */
+#define rb_for_each_entry(t, pos, root, member) \
+       for (pos = rb_first(root), t = pos ? rb_entry(pos, __typeof__(*t), member) : NULL; \
+               pos; pos = rb_next(pos), t = pos ? rb_entry(pos, __typeof__(*t), member) : NULL)
+
+/**
+ * rb_for_each_entry_reverse        -       iterate backwards over a rbtree of given type
+ * @pos:        the &struct rb_node to use as a loop counter.
+ * @t:          the type * to use as a loop counter.
+ * @root:       the root for your rbtree.
+ * @member:    the name of the rb_node-struct within the struct.
+ */
+#define rb_for_each_entry_reverse(t, pos, root, member) \
+       for (pos = rb_last(root), t = pos ? rb_entry(pos, __typeof__(*t), member) : NULL; \
+               pos; pos = rb_prev(pos), t = pos ? rb_entry(pos, __typeof__(*t), member) : NULL)
+
+#endif /* _LINUX_RBTREE_H */
diff --git a/read_wrapper.c b/read_wrapper.c
new file mode 100644 (file)
index 0000000..7d28c1c
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "read_wrapper.h"
+#include "ip.h"
+#include "file.h"
+
+#include <unistd.h>
+
+ssize_t read_wrapper(struct input_plugin_data *ip_data, void *buffer, size_t count)
+{
+       int rc;
+
+       if (ip_data->metaint == 0) {
+               /* no metadata in the stream */
+               return read(ip_data->fd, buffer, count);
+       }
+
+       if (ip_data->counter == ip_data->metaint) {
+               /* read metadata */
+               unsigned char byte;
+               int len;
+
+               rc = read(ip_data->fd, &byte, 1);
+               if (rc == -1)
+                       return -1;
+               if (rc == 0)
+                       return 0;
+               if (byte != 0) {
+                       len = ((int)byte) * 16;
+                       ip_data->metadata[0] = 0;
+                       rc = read_all(ip_data->fd, ip_data->metadata, len);
+                       if (rc == -1)
+                               return -1;
+                       if (rc < len) {
+                               ip_data->metadata[0] = 0;
+                               return 0;
+                       }
+                       ip_data->metadata[len] = 0;
+                       ip_data->metadata_changed = 1;
+               }
+               ip_data->counter = 0;
+       }
+       if (count + ip_data->counter > ip_data->metaint)
+               count = ip_data->metaint - ip_data->counter;
+       rc = read(ip_data->fd, buffer, count);
+       if (rc > 0)
+               ip_data->counter += rc;
+       return rc;
+}
diff --git a/read_wrapper.h b/read_wrapper.h
new file mode 100644 (file)
index 0000000..f41608c
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_READ_WRAPPER_H
+#define CMUS_READ_WRAPPER_H
+
+#include "ip.h"
+
+#include <stddef.h> /* size_t */
+#include <sys/types.h> /* ssize_t */
+
+ssize_t read_wrapper(struct input_plugin_data *ip_data, void *buffer, size_t count);
+
+#endif
diff --git a/scripts/checks.sh b/scripts/checks.sh
new file mode 100644 (file)
index 0000000..41c9f3e
--- /dev/null
@@ -0,0 +1,708 @@
+#!/bin/sh
+#
+# Copyright 2005-2006 Timo Hirvonen
+#
+# This file is licensed under the GPLv2.
+
+# C compiler
+# ----------
+# CC          default gcc
+# LD          default $CC
+# LDFLAGS     common linker flags for CC
+#
+# C++ Compiler
+# ------------
+# CXX         default g++
+# CXXLD       default $CXX
+# CXXLDFLAGS  common linker flags for CXX
+#
+# Common for C and C++
+# --------------------
+# SOFLAGS     flags for compiling position independent code (-fPIC)
+# LDSOFLAGS   flags for linking shared libraries
+# LDDLFLAGS   flags for linking dynamically loadable modules
+
+msg_checking()
+{
+       printf "checking $@... "
+}
+
+msg_result()
+{
+       echo "$@"
+}
+
+msg_error()
+{
+       echo "*** $@"
+}
+
+# @program: program to check
+# @name:    name of variable where to store the full program name (optional)
+#
+# returns 0 on success and 1 on failure
+check_program()
+{
+       argc check_program $# 1 2
+       msg_checking "for program $1"
+       __cp_file=`path_find "$1"`
+       if test $? -eq 0
+       then
+               msg_result $__cp_file
+               test $# -eq 2 && set_var $2 "$__cp_file"
+               return 0
+       else
+               msg_result "no"
+               return 1
+       fi
+}
+
+cc_supports()
+{
+       $CC $CFLAGS "$@" -S -o /dev/null -x c /dev/null 2> /dev/null
+       return $?
+}
+
+cxx_supports()
+{
+       $CXX $CXXFLAGS "$@" -S -o /dev/null -x c /dev/null 2> /dev/null
+       return $?
+}
+
+# @flag: option flag(s) to check
+#
+# add @flag to EXTRA_CFLAGS if CC accepts it
+# EXTRA_CFLAGS are added to CFLAGS in the end of configuration
+check_cc_flag()
+{
+       argc check_cc_flag $# 1
+
+       test -z "$CC" && die "check_cc_flag: CC not set"
+       msg_checking "for CFLAGS $*"
+       if cc_supports $*
+       then
+               EXTRA_CFLAGS="$EXTRA_CFLAGS $*"
+               msg_result "yes"
+               return 0
+       else
+               msg_result "no"
+               return 1
+       fi
+}
+
+# @flag: option flag(s) to check
+#
+# add @flag to EXTRA_CXXFLAGS if CXX accepts it
+# EXTRA_CXXFLAGS are added to CXXFLAGS in the end of configuration
+check_cxx_flag()
+{
+       argc check_cxx_flag $# 1
+
+       test -z "$CXX" && die "check_cxx_flag: CXX not set"
+       msg_checking "for CXXFLAGS $*"
+       if cxx_supports $*
+       then
+               EXTRA_CXXFLAGS="$EXTRA_CXXFLAGS $*"
+               msg_result "yes"
+               return 0
+       else
+               msg_result "no"
+               return 1
+       fi
+}
+
+cc_cxx_common()
+{
+       test "$cc_cxx_common_done" && return 0
+       cc_cxx_common_done=yes
+
+       var_default SOFLAGS "-fPIC"
+       var_default LDSOFLAGS "-shared"
+       var_default LDDLFLAGS "-shared"
+
+       common_cf=
+       common_lf=
+
+       case `uname -s` in
+       *BSD)
+               common_cf="$common_cf -I/usr/local/include"
+               common_lf="$common_lf -L/usr/local/lib"
+               ;;
+       Darwin)
+               # fink
+               if test -d /sw/lib
+               then
+                       common_cf="$common_cf -I/sw/include"
+                       common_lf="$common_lf -L/sw/lib"
+               fi
+               # darwinports
+               if test -d /opt/local/lib
+               then
+                       common_cf="$common_cf -I/opt/local/include"
+                       common_lf="$common_lf -L/opt/local/lib"
+               fi
+               LDSOFLAGS="-dynamic"
+               case ${MACOSX_DEPLOYMENT_TARGET} in
+               10.[012])
+                       LDDLFLAGS="-bundle -flat_namespace -undefined suppress"
+                       ;;
+               10.*)
+                       LDDLFLAGS="-bundle -undefined dynamic_lookup"
+                       ;;
+               *)
+                       LDDLFLAGS="-bundle -flat_namespace -undefined suppress"
+                       ;;
+               esac
+               ;;
+       SunOS)
+               common_cf="$common_cf -D__EXTENSIONS__ -I/usr/local/include"
+               common_lf="$common_lf -R/usr/local/lib -L/usr/local/lib"
+               ;;
+       esac
+       makefile_vars SOFLAGS LDSOFLAGS LDDLFLAGS
+}
+
+# CC, LD, CFLAGS, LDFLAGS, SOFLAGS, LDSOFLAGS, LDDLFLAGS
+check_cc()
+{
+       var_default CC ${CROSS}gcc
+       var_default LD $CC
+       var_default CFLAGS "-g -O2 -Wall"
+       var_default LDFLAGS ""
+       check_program $CC || return 1
+
+       cc_cxx_common
+       CFLAGS="$CFLAGS -I$(pwd) $common_cf"
+       LDFLAGS="$LDFLAGS $common_lf"
+
+       makefile_vars CC LD CFLAGS LDFLAGS
+       __check_lang=c
+       return 0
+}
+
+# HOSTCC, HOSTLD, HOST_CFLAGS, HOST_LDFLAGS
+check_host_cc()
+{
+       var_default HOSTCC gcc
+       var_default HOSTLD $HOSTCC
+       var_default HOST_CFLAGS "-g -O2 -Wall"
+       var_default HOST_LDFLAGS ""
+       check_program $HOSTCC || return 1
+       makefile_vars HOSTCC HOSTLD HOST_CFLAGS HOST_LDFLAGS
+       __check_lang=c
+       return 0
+}
+
+# CXX, CXXLD, CXXFLAGS, CXXLDFLAGS, SOFLAGS, LDSOFLAGS, LDDLFLAGS
+check_cxx()
+{
+       var_default CXX ${CROSS}g++
+       var_default CXXLD $CXX
+       var_default CXXFLAGS "-g -O2 -Wall"
+       var_default CXXLDFLAGS ""
+       check_program $CXX || return 1
+
+       cc_cxx_common
+       CXXFLAGS="$CXXFLAGS $common_cf"
+       CXXLDFLAGS="$CXXLDFLAGS $common_lf"
+
+       makefile_vars CXX CXXLD CXXFLAGS CXXLDFLAGS
+       __check_lang=cxx
+       return 0
+}
+
+# check if CC can generate dependencies (.dep-*.o files)
+# always succeeds
+check_cc_depgen()
+{
+       msg_checking "if CC can generate dependency information"
+       if cc_supports -MMD -MP -MF /dev/null
+       then
+               EXTRA_CFLAGS="$EXTRA_CFLAGS -MMD -MP -MF .dep-\$(subst /,-,\$@)"
+               msg_result yes
+       else
+               msg_result no
+       fi
+       return 0
+}
+
+# check if CXX can generate dependencies (.dep-*.o files)
+# always succeeds
+check_cxx_depgen()
+{
+       msg_checking "if CXX can generate dependency information"
+       if cxx_supports -MMD -MP -MF /dev/null
+       then
+               EXTRA_CXXFLAGS="$EXTRA_CXXFLAGS -MMD -MP -MF .dep-\$(subst /,-,\$@)"
+               msg_result yes
+       else
+               msg_result no
+       fi
+       return 0
+}
+
+# adds AR to config.mk
+check_ar()
+{
+       var_default AR ${CROSS}ar
+       var_default ARFLAGS "-cr"
+       if check_program $AR
+       then
+               makefile_vars AR ARFLAGS
+               return 0
+       fi
+       return 1
+}
+
+# adds AS to config.mk
+check_as()
+{
+       var_default AS ${CROSS}gcc
+       if check_program $AS
+       then
+               makefile_vars AS
+               return 0
+       fi
+       return 1
+}
+
+check_pkgconfig()
+{
+       if test -z "$PKG_CONFIG"
+       then
+               if check_program pkg-config PKG_CONFIG
+               then
+                       makefile_vars PKG_CONFIG
+               else
+                       # don't check again
+                       PKG_CONFIG="no"
+                       return 1
+               fi
+       fi
+       return 0
+}
+
+# check for library FOO and add FOO_CFLAGS and FOO_LIBS to config.mk
+#
+# @name:    variable prefix (e.g. CURSES -> CURSES_CFLAGS, CURSES_LIBS)
+# @cflags:  CFLAGS for the lib
+# @libs:    LIBS to check
+#
+# adds @name_CFLAGS and @name_LIBS to config.mk
+# CFLAGS are not checked, they are assumed to be correct
+check_library()
+{
+       argc check_library $# 3 3
+       msg_checking "for ${1}_LIBS ($3)"
+       if try_link $3
+       then
+               msg_result yes
+               makefile_var ${1}_CFLAGS "$2"
+               makefile_var ${1}_LIBS "$3"
+               return 0
+       else
+               msg_result no
+               return 1
+       fi
+}
+
+# run pkg-config
+#
+# @prefix:  variable prefix (e.g. GLIB -> GLIB_CFLAGS, GLIB_LIBS)
+# @modules: the argument for pkg-config
+# @cflags:  CFLAGS to use if pkg-config failed (optional)
+# @libs:    LIBS to use if pkg-config failed (optional)
+#
+# if pkg-config fails and @libs are given check_library is called
+#
+# example:
+#   ---
+#   check_glib()
+#   {
+#           pkg_config GLIB "glib-2.0 >= 2.2"
+#           return $?
+#   }
+#
+#   check check_cc
+#   check check_glib
+#   ---
+#   GLIB_CFLAGS and GLIB_LIBS are automatically added to Makefile
+pkg_config()
+{
+       argc pkg_config $# 2 4
+
+       # optional
+       __pc_cflags="$3"
+       __pc_libs="$4"
+
+       check_pkgconfig
+       msg_checking "for ${1}_LIBS (pkg-config)"
+       if test "$PKG_CONFIG" != "no" && $PKG_CONFIG --exists "$2" >/dev/null 2>&1
+       then
+               # pkg-config is installed and the .pc file exists
+               __pc_libs="`$PKG_CONFIG --libs ""$2""`"
+               msg_result "$__pc_libs"
+
+               msg_checking "for ${1}_CFLAGS (pkg-config)"
+               __pc_cflags="`$PKG_CONFIG --cflags ""$2""`"
+               msg_result "$__pc_cflags"
+
+               makefile_var ${1}_CFLAGS "$__pc_cflags"
+               makefile_var ${1}_LIBS "$__pc_libs"
+               return 0
+       fi
+
+       # no pkg-config or .pc file
+       msg_result "no"
+
+       if test -z "$__pc_libs"
+       then
+               if test "$PKG_CONFIG" = "no"
+               then
+                       # pkg-config not installed and no libs to check were given
+                       msg_error "pkg-config required for $1"
+               else
+                       # pkg-config is installed but the required .pc file wasn't found
+                       $PKG_CONFIG --errors-to-stdout --print-errors "$2" | sed 's:^:*** :'
+               fi
+               return 1
+       fi
+
+       check_library "$1" "$__pc_cflags" "$__pc_libs"
+       return $?
+}
+
+# old name
+pkg_check_modules()
+{
+       pkg_config "$@"
+}
+
+# run *-config
+#
+# @prefix:  variable prefix (e.g. ARTS -> ARTS_CFLAGS, ARTS_LIBS)
+# @program: the -config program
+#
+# example:
+#   ---
+#   check_arts()
+#   {
+#           app_config ARTS artsc-config
+#           return $?
+#   }
+#
+#   check check_cc
+#   check check_arts
+#   ---
+#   ARTS_CFLAGS and ARTS_LIBS are automatically added to config.mk
+app_config()
+{
+       argc app_config $# 2 2
+       check_program $2 || return 1
+
+       msg_checking "for ${1}_CFLAGS"
+       __ac_cflags="`$2 --cflags`"
+       msg_result "$__ac_cflags"
+
+       msg_checking "for ${1}_LIBS"
+       __ac_libs="`$2 --libs`"
+       msg_result "$__ac_libs"
+
+       makefile_var ${1}_CFLAGS "$__ac_cflags"
+       makefile_var ${1}_LIBS "$__ac_libs"
+       return 0
+}
+
+# @contents:  file contents to compile
+# @cflags:    extra cflags (optional)
+try_compile()
+{
+       argc try_compile $# 1
+       case $__check_lang in
+       c)
+               __src=`tmp_file prog.c`
+               __obj=`tmp_file prog.o`
+               echo "$1" > $__src || exit 1
+               shift
+               __cmd="$CC -c $CFLAGS $@ $__src -o $__obj"
+               $CC -c $CFLAGS "$@" $__src -o $__obj 2>/dev/null
+               ;;
+       cxx)
+               __src=`tmp_file prog.cc`
+               __obj=`tmp_file prog.o`
+               echo "$1" > $__src || exit 1
+               shift
+               __cmd="$CXX -c $CXXFLAGS $@ $__src -o $__obj"
+               $CXX -c $CXXFLAGS "$@" $__src -o $__obj 2>/dev/null
+               ;;
+       esac
+       return $?
+}
+
+# @contents:  file contents to compile and link
+# @flags:     extra flags (optional)
+try_compile_link()
+{
+       argc try_compile $# 1
+       case $__check_lang in
+       c)
+               __src=`tmp_file prog.c`
+               __exe=`tmp_file prog`
+               echo "$1" > $__src || exit 1
+               shift
+               __cmd="$CC $__src -o $__exe $CFLAGS $LDFLAGS $@"
+               $CC $__src -o $__exe $CFLAGS $LDFLAGS "$@" 2>/dev/null
+               ;;
+       cxx)
+               __src=`tmp_file prog.cc`
+               __exe=`tmp_file prog`
+               echo "$1" > $__src || exit 1
+               shift
+               __cmd="$CXX $__src -o $__exe $CXXFLAGS $CXXLDFLAGS $@"
+               $CXX $__src -o $__exe $CXXFLAGS $CXXLDFLAGS "$@" 2>/dev/null
+               ;;
+       esac
+       return $?
+}
+
+# optionally used after try_compile or try_compile_link
+__compile_failed()
+{
+       warn
+       warn "Failed to compile simple program:"
+       warn "---"
+       cat $__src >&2
+       warn "---"
+       warn "Command: $__cmd"
+       case $__check_lang in
+       c)
+               warn "Make sure your CC and CFLAGS are sane."
+               ;;
+       cxx)
+               warn "Make sure your CXX and CXXFLAGS are sane."
+               ;;
+       esac
+       exit 1
+}
+
+# tries to link against a lib
+# 
+# @function:  some function
+# @flags:     extra flags (optional)
+check_function()
+{
+       argc check_function $# 1
+       __func="$1"
+       shift
+       msg_checking "for function $__func"
+       if try_compile_link "char $__func(); int main(int argc, char *argv[]) { return $__func; }" "$@"
+       then
+               msg_result yes
+               return 0
+       fi
+
+       msg_result no
+       return 1
+}
+
+# tries to link against a lib
+#
+# @ldadd:  something like -L/usr/X11R6/lib -lX11
+try_link()
+{
+       try_compile_link "int main(int argc, char *argv[]) { return 0; }" "$@"
+       return $?
+}
+
+# compile and run
+#
+# @code:  simple program code to run
+run_code()
+{
+       if test $CROSS
+       then
+               msg_error "cannot run code when cross compiling"
+               exit 1
+       fi
+       try_compile_link "$1" || __compile_failed
+       ./$__exe
+       return $?
+}
+
+# check if the architecture is big-endian
+# parts are from autoconf 2.67
+#
+# defines WORDS_BIGENDIAN=y/n
+check_endianness()
+{
+       msg_checking "byte order"
+       WORDS_BIGENDIAN=n
+       # See if sys/param.h defines the BYTE_ORDER macro.
+       if try_compile_link "
+#include <sys/types.h>
+#include <sys/param.h>
+int main() {
+#if ! (defined BYTE_ORDER && defined BIG_ENDIAN \
+               && defined LITTLE_ENDIAN && BYTE_ORDER && BIG_ENDIAN \
+               && LITTLE_ENDIAN)
+       bogus endian macros
+#endif
+       return 0;
+}"
+       then
+               # It does; now see whether it defined to BIG_ENDIAN or not.
+               if try_compile_link "
+#include <sys/types.h>
+#include <sys/param.h>
+int main() {
+#if BYTE_ORDER != BIG_ENDIAN
+       not big endian
+#endif
+       return 0;
+}"
+               then
+                       WORDS_BIGENDIAN=y
+               fi
+       # See if <limits.h> defines _LITTLE_ENDIAN or _BIG_ENDIAN (e.g., Solaris).
+       elif try_compile_link "
+#include <limits.h>
+int main() {
+#if ! (defined _LITTLE_ENDIAN || defined _BIG_ENDIAN)
+       bogus endian macros
+#endif
+       return 0;
+}"
+       then
+               # It does; now see whether it defined to _BIG_ENDIAN or not.
+               if try_compile_link "
+#include <limits.h>
+int main() {
+#ifndef _BIG_ENDIAN
+       not big endian
+#endif
+       return 0;
+}"
+               then
+                       WORDS_BIGENDIAN=y
+               fi
+       elif run_code "
+int main(int argc, char *argv[])
+{
+       unsigned int i = 1;
+       return *(char *)&i;
+}"
+       then
+               WORDS_BIGENDIAN=y
+       fi
+       if test "$WORDS_BIGENDIAN" = y
+       then
+               msg_result "big-endian"
+       else
+               msg_result "little-endian"
+       fi
+       return 0
+}
+
+# check if @header can be included
+#
+# @header
+# @cflags   -I/some/path (optional)
+check_header()
+{
+       argc check_header $# 1
+       __header="$1"
+       shift
+       msg_checking "for header <$__header>"
+       if try_compile "#include <$__header>" "$@"
+       then
+               msg_result yes
+               return 0
+       fi
+       msg_result no
+       return 1
+}
+
+# check X11 libs
+#
+# adds X11_LIBS (and empty X11_CFLAGS) to config.mk
+check_x11()
+{
+       for __libs in "-lX11" "-L/usr/X11R6/lib -lX11"
+       do
+               check_library X11 "" "$__libs" && return 0
+       done
+       return 1
+}
+
+# check posix threads
+#
+# adds PTHREAD_CFLAGS and PTHREAD_LIBS to config.mk
+check_pthread()
+{
+       for __libs in "$PTHREAD_LIBS" -lpthread -lc_r -lkse
+       do
+               test -z "$__libs" && continue
+               check_library PTHREAD "-D_REENTRANT" "$__libs" && return 0
+       done
+       echo "using -pthread gcc option"
+       makefile_var PTHREAD_CFLAGS "-pthread -D_THREAD_SAFE"
+       makefile_var PTHREAD_LIBS "-pthread"
+       return 0
+}
+
+# check dynamic linking loader
+#
+# adds DL_LIBS to config.mk
+check_dl()
+{
+       for DL_LIBS in "-ldl -Wl,--export-dynamic" "-ldl -rdynamic" "-Wl,--export-dynamic" "-rdynamic" "-ldl"
+       do
+               check_library DL "" "$DL_LIBS" && return 0
+       done
+       echo "assuming -ldl is not needed"
+       DL_LIBS=
+       makefile_vars DL_LIBS DL_CFLAGS
+       return 0
+}
+
+# check for iconv
+#
+# adds ICONV_CFLAGS and ICONV_LIBS to config.mk
+check_iconv()
+{
+       HAVE_ICONV=n
+       if check_library ICONV "" "-liconv"
+       then
+               echo "taking iconv from libiconv"
+       else
+               echo "assuming libc contains iconv"
+               makefile_var ICONV_CFLAGS ""
+               makefile_var ICONV_LIBS ""
+       fi
+       msg_checking "for working iconv"
+       if try_compile_link '
+#include <stdio.h>
+#include <string.h>
+#include <iconv.h>
+int main(int argc, char *argv[]) {
+       char buf[128], *out = buf, *in = argv[1];
+       size_t outleft = 127, inleft = strlen(in);
+       iconv_t cd = iconv_open("UTF-8", "ISO-8859-1");
+       iconv(cd, &in, &inleft, &out, &outleft);
+       *out = 0;
+       printf("%s", buf);
+       iconv_close(cd);
+       return 0;
+}' $ICONV_CFLAGS $ICONV_LIBS
+       then
+               msg_result "yes"
+               HAVE_ICONV=y
+       else
+               msg_result "no"
+               msg_error "Your system doesn't have iconv!"
+               msg_error "This means that no charset conversion can be done, so all"
+               msg_error "your tracks need to be encoded in your system charset!"
+       fi
+
+       return 0
+}
diff --git a/scripts/configure.sh b/scripts/configure.sh
new file mode 100644 (file)
index 0000000..0284d6d
--- /dev/null
@@ -0,0 +1,222 @@
+#!/bin/sh
+#
+# Copyright 2005 Timo Hirvonen
+#
+# This file is licensed under the GPLv2.
+
+. scripts/utils.sh || exit 1
+. scripts/checks.sh || exit 1
+
+# Usage: parse_command_line "$@"
+# USAGE string must be defined in configure (used for --help)
+parse_command_line()
+{
+       while test $# -gt 0
+       do
+               case $1 in
+               --help)
+                       show_usage
+                       ;;
+               -f)
+                       shift
+                       test $# -eq 0 && die "-f requires an argument"
+                       . "$1"
+                       ;;
+               -*)
+                       die "unrecognized option \`$1'"
+                       ;;
+               *=*)
+                       _var=`echo "$1" | sed "s/=.*//"`
+                       _val=`echo "$1" | sed "s/${_var}=//"`
+                       set_var "$_var" "$_val"
+                       ;;
+               *)
+                       die "unrecognized argument \`$1'"
+                       ;;
+               esac
+               shift
+       done
+}
+
+# check function [variable]
+#
+# Example:
+# check check_cc
+# check check_vorbis CONFIG_VORBIS
+check()
+{
+       argc check $# 1 2
+       if test $# -eq 1
+       then
+               $1 || die "configure failed."
+               return
+       fi
+
+       # optional feature
+       case `get_var $2` in
+       n)
+               ;;
+       y)
+               $1 || die "configure failed."
+               ;;
+       a|'')
+               if $1
+               then
+                       set_var $2 y
+               else
+                       set_var $2 n
+               fi
+               ;;
+       *)
+               die "invalid value for $2. 'y', 'n', 'a' or '' expected"
+               ;;
+       esac
+}
+
+# Set and register variable to be added to config.mk
+#
+# @name   name of the variable
+# @value  value of the variable
+makefile_var()
+{
+       argc makefile_var $# 2 2
+       set_var $1 "$2"
+       makefile_vars $1
+}
+
+# Register variables to be added to config.mk
+makefile_vars()
+{
+       makefile_variables="$makefile_variables $*"
+}
+
+# generate config.mk
+generate_config_mk()
+{
+       CFLAGS="$CFLAGS $EXTRA_CFLAGS"
+       CXXFLAGS="$CXXFLAGS $EXTRA_CXXFLAGS"
+       if test -z "$GINSTALL"
+       then
+               GINSTALL=`path_find ginstall`
+               test "$GINSTALL" || GINSTALL=install
+       fi
+       # $PWD is useless!
+       topdir=`pwd`
+       makefile_vars GINSTALL topdir
+
+       __tmp=`tmp_file config.mk`
+       for __i in $makefile_variables
+       do
+               echo "$__i = `get_var $__i`"
+       done > $__tmp
+       update_file $__tmp config.mk
+}
+
+# -----------------------------------------------------------------------------
+# Config header generation
+
+# Simple interface
+#
+# Guesses variable types:
+#   y or n        -> bool
+#   [0-9]+        -> int
+#   anything else -> str
+#
+# Example:
+#   CONFIG_FOO=y  # bool
+#   VERSION=2.0.1 # string
+#   DEBUG=1       # int
+#   config_header config.h CONFIG_FOO VERSION DEBUG
+config_header()
+{
+       argc config_header $# 2
+       config_header_begin "$1"
+       shift
+       while test $# -gt 0
+       do
+               __var=`get_var $1`
+               case "$__var" in
+               [yn])
+                       config_bool $1
+                       ;;
+               *)
+                       if test "$__var" && test "$__var" = "`echo $__var | sed 's/[^0-9]//g'`"
+                       then
+                               config_int $1
+                       else
+                               config_str $1
+                       fi
+                       ;;
+               esac
+               shift
+       done
+       config_header_end
+}
+
+# Low-level interface
+#
+# Example:
+#   config_header_begin config.h
+#   config_str PACKAGE VERSION
+#   config_bool CONFIG_ALSA
+#   config_header_end
+
+config_header_begin()
+{
+       argc config_header_begin $# 1 1
+       config_header_file="$1"
+       config_header_tmp=`tmp_file config_header`
+
+       __def=`echo $config_header_file | to_upper | sed 's/[-\.\/]/_/g'`
+       cat <<EOF > "$config_header_tmp"
+#ifndef $__def
+#define $__def
+
+EOF
+}
+
+config_str()
+{
+       while test $# -gt 0
+       do
+               echo "#define $1 \"`get_var $1`\"" >> "$config_header_tmp"
+               shift
+       done
+}
+
+config_int()
+{
+       while test $# -gt 0
+       do
+               echo "#define $1 `get_var $1`" >> "$config_header_tmp"
+               shift
+       done
+}
+
+config_bool()
+{
+       while test $# -gt 0
+       do
+               case "`get_var $1`" in
+                       n)
+                               echo "/* #define $1 */" >> "$config_header_tmp"
+                               ;;
+                       y)
+                               echo "#define $1 1" >> "$config_header_tmp"
+                               ;;
+                       *)
+                               die "bool '$1' has invalid value '`get_var $1`'"
+                               ;;
+               esac
+               shift
+       done
+}
+
+config_header_end()
+{
+       argc config_header_end $# 0 0
+       echo "" >> "$config_header_tmp"
+       echo "#endif" >> "$config_header_tmp"
+       mkdir -p `dirname "$config_header_file"`
+       update_file "$config_header_tmp" "$config_header_file"
+}
diff --git a/scripts/ffmpeg_test.sh b/scripts/ffmpeg_test.sh
new file mode 100755 (executable)
index 0000000..c585238
--- /dev/null
@@ -0,0 +1,220 @@
+#!/bin/bash
+# vim: set expandtab shiftwidth=4:
+#
+# Copyright 2010-2013 Various Authors
+# Copyright 2012 Johannes Weißl
+#
+# 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, see <http://www.gnu.org/licenses/>.
+
+# many (!) FFmpeg versions will be installed here, at least 25GB
+FFMPEG_BUILD_DIR=$HOME/cmus_ffmpeg_test/ffmpeg_builds
+
+# ffmpeg/libav source will be cloned into this directory
+FFMPEG_SRC_DIR=$HOME/cmus_ffmpeg_test/ffmpeg_src
+
+# source code of cmus is expected here
+CMUS_SRC_DIR=$HOME/cmus_ffmpeg_test/cmus_src
+
+# cmus versions will be installed here
+CMUS_BUILD_DIR=$HOME/cmus_ffmpeg_test/cmus_builds
+
+FFMPEG_CLONE_URL=git://source.ffmpeg.org/ffmpeg.git
+LIBAV_CLONE_URL=git://git.libav.org/libav.git
+
+# headers of ffmpeg that are relevant to cmus compilation
+HEADERS="avcodec.h avformat.h avio.h mathematics.h version.h"
+
+# argument to make -j
+MAKE_J=$(grep -c "^processor" /proc/cpuinfo 2>/dev/null || echo 1)
+
+print_usage () {
+    echo "Usage: $progname build_ffmpeg | build_libav | build_cmus | test_cmus"
+    echo
+    echo "build_{ffmpeg,libav}:"
+    echo " 1. clone/pull source into $FFMPEG_SRC_DIR/{ffmpeg,libav}"
+    echo " 2. build and install (necessary) revisions into $FFMPEG_BUILD_DIR"
+    echo "    can take days and needs up to 25 GB hard disk (!)"
+    echo "    you can use ctrl-c to stop the script and run it later to continue"
+    echo
+    echo "build_cmus:"
+    echo " 1. expects cmus source in $CMUS_SRC_DIR"
+    echo " 2. build cmus for every revision in $FFMPEG_BUILD_DIR and install"
+    echo "    to $CMUS_BUILD_DIR"
+    echo
+    echo "test_cmus:"
+    echo " test ffmpeg plugin of every cmus build in $CMUS_BUILD_DIR"
+}
+
+function get_commits () {
+    for name in "$@" ; do
+        find -type f -name "$name" -exec git log --follow --pretty=format:"%H%n" {} \;
+    done
+    for tag in $(git tag) ; do
+        git show "$tag" | sed -n "s/^commit //p"
+    done
+}
+
+function uniq_stable () {
+    nl -ba | sort -suk2 | sort -n | cut -f2-
+}
+
+DONE=
+trap 'DONE=1' SIGINT
+
+function build_to_prefix () {
+    prefix=$1
+    cur=$2
+    all=$3
+    cur_name=$4
+    build_cmd=$5
+    echo -n "[$((cur*100/all))%] "
+    if [ -e "$prefix.broken" ] ; then
+        echo "skip $cur_name, broken"
+    elif [ -e "$prefix.part" ] ; then
+        echo "skip $cur_name, is being build"
+    else
+        if [ -e "$prefix" ] ; then
+            echo "skip $cur_name, already built"
+        else
+            echo -n "build and install to $prefix: "
+            echo $build_cmd >"$prefix.log"
+            (mkdir -p "$prefix.part" && eval $build_cmd && mv "$prefix.part/$prefix" "$prefix" && rm -rf "$prefix.part") >>"$prefix.log" 2>&1 && echo "ok" ||
+                    (touch "$prefix.broken" ; echo "FAILED:" ; echo $build_cmd)
+        fi
+    fi
+    [ -n "$DONE" ] && rm -rvf "$prefix" "$prefix".part "$prefix".broken
+}
+
+function build_revisions () {
+    name=$1
+    url=$2
+    mkdir -p "$FFMPEG_SRC_DIR" "$FFMPEG_BUILD_DIR"
+    FFMPEG_SRC_DIR=$FFMPEG_SRC_DIR/$name
+    if [ -e "$FFMPEG_SRC_DIR" ] ; then
+        echo "pull $url in $FFMPEG_SRC_DIR"
+        pushd "$FFMPEG_SRC_DIR" >/dev/null
+        git reset --hard origin/master >/dev/null
+        git clean -fxd >/dev/null
+        git pull >/dev/null
+    else
+        echo "clone $url in $FFMPEG_SRC_DIR"
+        git clone "$url" "$FFMPEG_SRC_DIR" >/dev/null
+        pushd "$FFMPEG_SRC_DIR" >/dev/null
+    fi
+    commits=$(get_commits $HEADERS | uniq_stable)
+    commits_count=$(echo $commits | wc -w)
+    i=0
+    for c in $commits ; do
+        i=$((i+1))
+        git reset --hard "$c" >/dev/null
+        git clean -fxd >/dev/null
+        prefix="$FFMPEG_BUILD_DIR/$c"
+        build_to_prefix "$prefix" "$i" "$commits_count" "$c" \
+            "./configure --prefix=\"$prefix.part\" --enable-shared --disable-static && make -j$MAKE_J && make install"
+        [ -n "$DONE" ] && break
+    done
+    popd >/dev/null
+}
+
+build_cmus () {
+    pushd "$CMUS_SRC_DIR" >/dev/null
+    mkdir -p "$CMUS_BUILD_DIR"
+    revdirs=$(find "$FFMPEG_BUILD_DIR" -mindepth 1 -maxdepth 1 -type d ! -name "*.part")
+    revdirs_count=$(echo $revdirs | wc -w)
+    i=0
+    for revdir in $revdirs ; do
+        i=$((i+1))
+        rev=$(basename "$revdir")
+        prefix="$CMUS_BUILD_DIR/$rev"
+        make distclean >/dev/null 2>&1
+        build_to_prefix "$prefix" "$i" "$revdirs_count" "$rev" \
+            "CFLAGS=\"-I$revdir/include\" LDFLAGS=\"-L$revdir/lib\" ./configure prefix=\"$prefix\" CONFIG_FFMPEG=y DEBUG=2 && make -j$MAKE_J && make install DESTDIR=\"$prefix.part\""
+        [ -n "$DONE" ] && break
+    done
+    popd >/dev/null
+}
+
+test_cmus () {
+    mkdir -p "$CMUS_BUILD_DIR"
+    revdirs=$(find "$CMUS_BUILD_DIR" -mindepth 1 -maxdepth 1 -type d ! -name "*.part")
+    revdirs_count=$(echo $revdirs | wc -w)
+    i=0
+    for revdir in $revdirs ; do
+        i=$((i+1))
+        rev=$(basename "$revdir")
+        tmpdir=$(mktemp -d)
+        lib_prefix=$FFMPEG_BUILD_DIR/$rev
+        echo -n "[$((i*100/revdirs_count))%] test $revdir: "
+        if CMUS_HOME=$tmpdir LD_LIBRARY_PATH=$lib_prefix/lib:$LD_LIBRARY_PATH "$revdir"/bin/cmus --plugins | grep -q "^ *ffmpeg" ; then
+            echo "working"
+        else
+            echo "not working: "
+            echo "CMUS_HOME=$tmpdir LD_LIBRARY_PATH=$lib_prefix/lib:$LD_LIBRARY_PATH \"$revdir\"/bin/cmus --plugins"
+            cat $tmpdir/cmus-debug.txt
+        fi
+        rm "$tmpdir"/cmus-debug.txt
+        rmdir "$tmpdir"
+        [ -n "$DONE" ] && break
+    done
+}
+
+progname=$(basename "$0")
+
+while [ $# -gt 0 ] ; do
+    case "$1" in
+        -h | --help)
+            print_usage
+            exit 0
+            ;;
+         --)
+            shift ; break
+            ;;
+        -*)
+            echo >&2 "$progname: unrecognized option \`$1'"
+            echo >&2 "Try \`$0 --help' for more information."
+            exit 1
+            ;;
+        *)
+            break
+            ;;
+    esac
+done
+
+if [ $# -eq 0 ] ; then
+    print_usage
+    exit 0
+elif [ $# -gt 1 ] ; then
+    echo >&2 "$progname: too many arguments"
+    echo >&2 "Try \`$0 --help' for more information."
+    exit 1
+fi
+
+case "$1" in
+    build_ffmpeg)
+        build_revisions ffmpeg "$FFMPEG_CLONE_URL"
+        ;;
+    build_libav)
+        build_revisions libav "$LIBAV_CLONE_URL"
+        ;;
+    build_cmus)
+        build_cmus
+        ;;
+    test_cmus)
+        test_cmus
+        ;;
+    *)
+        echo >&2 "$progname: unrecognized command \`$1'"
+        echo >&2 "Try \`$0 --help' for more information."
+        exit 1
+esac
diff --git a/scripts/gen_decomp.py b/scripts/gen_decomp.py
new file mode 100755 (executable)
index 0000000..48dc3dc
--- /dev/null
@@ -0,0 +1,174 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2010-2013 Various Authors
+# Copyright 2010 Johannes Weißl
+#
+# 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, see <http://www.gnu.org/licenses/>.
+
+import sys
+import re
+import os.path
+import urllib2
+from optparse import OptionParser
+
+# Some letters don't have a decomposition, but can't be composed on all
+# keyboards. This dictionary maps them to an ASCII character which
+# *looks* similar.
+special_decompositions = {
+    u'Æ': u'A',
+    u'Ð': u'D',
+    u'×': u'x',
+    u'Ø': u'O',
+    u'Þ': u'P',
+    u'ß': u'B',
+    u'æ': u'a',
+    u'ð': u'd',
+    u'ø': u'o',
+    u'þ': u'p',
+# Various punctation/quotation characters
+    u'‐': u'-',
+    u'‒': u'-',
+    u'–': u'-',
+    u'−': u'-',
+    u'—': u'-',
+    u'―': u'-',
+    u'‘': u"'",
+    u'’': u"'",
+    u'′': u"'",
+    u'“': u'"',
+    u'”': u'"',
+    u'″': u'"',
+    u'〃': u'"',
+    u'…': u'.',
+}
+
+def parse_unidata(f):
+    u = {}
+    for line in f:
+        d = line.rstrip('\n').split(';')
+        cp = int(d[0], 16)
+        u[cp] = {}
+        u[cp]['name'] = d[1]
+        decomp = d[5]
+        if decomp:
+            m = re.match(r'<.*> (.*)', decomp)
+            u[cp]['compat'] = bool(m)
+            if m:
+                decomp = m.group(1)
+            u[cp]['decomp'] = [int(x, 16) for x in decomp.split(' ')]
+        else:
+            u[cp]['decomp'] = []
+    return u
+
+def unidata_expand_decomp(unidata):
+    def recurse(k):
+        if k not in unidata or not unidata[k]['decomp']:
+            return [k]
+        exp = []
+        for d in unidata[k]['decomp']:
+            exp += recurse(d)
+        return exp
+    for k in unidata.keys():
+        exp = recurse(k)
+        if exp != [k]:
+            unidata[k]['decomp'] = exp
+
+def unidata_add_mapping(unidata, mapping):
+    for k, v in mapping.items():
+        unidata[ord(k)]['decomp'] = [ord(v)]
+
+def is_diacritical_mark(c):
+    return c >= 0x0300 and c <= 0x036F
+
+def filter_unidata(unidata, include):
+    for k, v in unidata.items():
+        if k in include:
+            continue
+        if not v['decomp']:
+            del unidata[k]
+            continue
+        b = v['decomp'][0]
+        if unichr(b) == u' ' or is_diacritical_mark(b):
+            del unidata[k]
+            continue
+        has_accents = False
+        for d in v['decomp'][1:]:
+            if is_diacritical_mark(d):
+                has_accents = True
+                break
+        if not has_accents:
+            del unidata[k]
+
+def output(unidata, f):
+    buf = '''/* This file is automatically generated. DO NOT EDIT!
+Instead, edit %s and re-run. */
+
+static struct {
+       uchar composed;
+       uchar base;
+} unidecomp_map[] = {
+''' % os.path.basename(sys.argv[0])
+    for k in sorted(unidata.keys()):
+        b = unidata[k]['decomp'][0]
+        buf += '\t{ %#6x, %#6x },\t// %s -> %s,\t%s\n' % \
+            (k, b,
+            unichr(k).encode('utf-8'),
+            unichr(b).encode('utf-8'),
+            ', '.join([' %s (%x)' %
+                (unichr(d).encode('utf-8'), d)
+                    for d in unidata[k]['decomp'][1:]]))
+    buf += '};'
+    f.write(buf+'\n')
+
+def main(argv=None):
+
+    if not argv:
+        argv = sys.argv
+
+    parser = OptionParser(usage='usage: %prog [-w] [-o unidecomp.h]')
+    parser.add_option('-w', '--wget', action='store_true',
+        help='get unicode data from unicode.org')
+    parser.add_option('-o', '--output',
+        help='output file, default stdout')
+    (options, args) = parser.parse_args(argv[1:])
+
+    urlbase = 'http://unicode.org/Public/UNIDATA/'
+    unidata_filename = 'UnicodeData.txt'
+
+    if not os.path.exists(unidata_filename) and not options.wget:
+        parser.error('''need %s in the current directory, download
+from unicode.org or use `--wget' option.''' % unidata_filename)
+
+    if options.wget:
+        unidata_file = urllib2.urlopen(urlbase+unidata_filename)
+    else:
+        unidata_file = open(unidata_filename, 'rb')
+
+    unidata = parse_unidata(unidata_file)
+    unidata_file.close()
+
+    unidata_add_mapping(unidata, special_decompositions)
+    unidata_expand_decomp(unidata)
+    filter_unidata(unidata, [ord(x) for x in special_decompositions])
+
+    outfile = sys.stdout
+    if options.output:
+        outfile = open(options.output, 'wb')
+    output(unidata, outfile)
+    if options.output:
+        outfile.close()
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/scripts/install b/scripts/install
new file mode 100755 (executable)
index 0000000..8c453b7
--- /dev/null
@@ -0,0 +1,32 @@
+#!/bin/sh
+#
+# Copyright 2005-2006 Timo Hirvonen
+#
+# This file is licensed under the GPLv2.
+
+flags=""
+while test $# -gt 0
+do
+       case $1 in
+               -*)
+                       flags="$flags $1"
+                       ;;
+               *)
+                       break
+                       ;;
+       esac
+       shift
+done
+
+test $# -lt 2 && exit 0
+
+to="${DESTDIR}${1}"
+shift
+$GINSTALL -d -m755 "${to}"
+for i in "$@"
+do
+       dest="${to}/`basename ${i}`"
+       test "$INSTALL_LOG" && echo "$dest" >> "$INSTALL_LOG"
+       echo "INSTALL ${dest}"
+       $GINSTALL $flags "$i" "${to}"
+done
diff --git a/scripts/lib.mk b/scripts/lib.mk
new file mode 100644 (file)
index 0000000..2e412a9
--- /dev/null
@@ -0,0 +1,148 @@
+#
+# Copyright 2005-2006 Timo Hirvonen
+#
+# This file is licensed under the GPLv2.
+#
+# cmd macro copied from kbuild (Linux kernel build system)
+
+# Build verbosity:
+#   make V=0    silent
+#   make V=1    clean (default)
+#   make V=2    verbose
+
+# build verbosity (0-2), default is 1
+ifneq ($(origin V),command line)
+  V := 1
+endif
+ifneq ($(findstring s,$(MAKEFLAGS)),)
+  V := 0
+endif
+
+ifeq ($(V),2)
+  quiet =
+  Q =
+else
+  ifeq ($(V),1)
+    quiet = quiet_
+    Q = @
+  else
+    quiet = silent_
+    Q = @
+  endif
+endif
+
+# simple wrapper around install(1)
+#
+#  - creates directories automatically
+#  - adds $(DESTDIR) to front of files
+INSTALL                := @$(topdir)/scripts/install
+INSTALL_LOG    := $(topdir)/.install.log
+
+dependencies   := $(wildcard .dep-*)
+clean          := $(dependencies)
+distclean      :=
+
+LC_ALL         := C
+LANG           := C
+
+export INSTALL_LOG LC_ALL LANG GINSTALL
+
+# remove files generated by make
+clean:
+       rm -f $(clean)
+
+# remove files generated by make and configure
+distclean: clean
+       rm -f $(distclean)
+
+uninstall:
+       @$(topdir)/scripts/uninstall
+
+%.o: %.S
+       $(call cmd,as)
+
+# object files for programs and static libs
+%.o: %.c
+       $(call cmd,cc)
+
+%.o: %.cc
+       $(call cmd,cxx)
+
+%.o: %.cpp
+       $(call cmd,cxx)
+
+# object files for shared libs
+%.lo: %.c
+       $(call cmd,cc_lo)
+
+%.lo: %.cc
+       $(call cmd,cxx_lo)
+
+%.lo: %.cpp
+       $(call cmd,cxx_lo)
+
+# CC for program object files (.o)
+quiet_cmd_cc    = CC     $@
+      cmd_cc    = $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $<
+
+# HOSTCC for program object files (.o)
+quiet_cmd_hostcc    = HOSTCC     $@
+      cmd_hostcc    = $(HOSTCC) -c $(HOST_CFLAGS) -o $@ $<
+
+# CC for shared library and dynamically loadable module objects (.lo)
+quiet_cmd_cc_lo = CC     $@
+      cmd_cc_lo = $(CC) -c $(CPPFLAGS) $(CFLAGS) $(SOFLAGS) -o $@ $<
+
+# LD for programs, optional parameter: libraries
+quiet_cmd_ld = LD     $@
+      cmd_ld = $(LD) $(LDFLAGS) -o $@ $^ $(1)
+
+# HOSTLD for programs, optional parameter: libraries
+quiet_cmd_hostld = HOSTLD     $@
+      cmd_hostld = $(HOSTLD) $(HOST_LDFLAGS) -o $@ $^ $(1)
+
+# LD for shared libraries, optional parameter: libraries
+quiet_cmd_ld_so = LD     $@
+      cmd_ld_so = $(LD) $(LDSOFLAGS) $(LDFLAGS) -o $@ $^ $(1)
+
+# LD for dynamically loadable modules, optional parameter: libraries
+quiet_cmd_ld_dl = LD     $@
+      cmd_ld_dl = $(LD) $(LDDLFLAGS) $(LDFLAGS) -o $@ $^ $(1)
+
+# CXX for program object files (.o)
+quiet_cmd_cxx    = CXX    $@
+      cmd_cxx    = $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) -o $@ $<
+
+# CXX for shared library and dynamically loadable module objects (.lo)
+quiet_cmd_cxx_lo = CXX    $@
+      cmd_cxx_lo = $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $(SOFLAGS) -o $@ $<
+
+# CXXLD for programs, optional parameter: libraries
+quiet_cmd_cxxld = CXXLD  $@
+      cmd_cxxld = $(CXXLD) $(CXXLDFLAGS) -o $@ $^ $(1)
+
+# CXXLD for shared libraries, optional parameter: libraries
+quiet_cmd_cxxld_so = CXXLD  $@
+      cmd_cxxld_so = $(CXXLD) $(LDSOFLAGS) $(CXXLDFLAGS) -o $@ $^ $(1)
+
+# CXXLD for dynamically loadable modules, optional parameter: libraries
+quiet_cmd_cxxld_dl = CXXLD  $@
+      cmd_cxxld_dl = $(CXXLD) $(LDDLFLAGS) $(CXXLDFLAGS) -o $@ $^ $(1)
+
+# create archive
+quiet_cmd_ar = AR     $@
+      cmd_ar = $(AR) $(ARFLAGS) $@ $^
+
+# assembler
+quiet_cmd_as = AS     $@
+      cmd_as = $(AS) -c $(ASFLAGS) -o $@ $<
+
+cmd = @$(if $($(quiet)cmd_$(1)),echo '   $(call $(quiet)cmd_$(1),$(2))' &&) $(call cmd_$(1),$(2))
+
+ifneq ($(dependencies),)
+-include $(dependencies)
+endif
+
+.SECONDARY:
+
+.PHONY: clean distclean uninstall
diff --git a/scripts/uninstall b/scripts/uninstall
new file mode 100755 (executable)
index 0000000..9c2c82f
--- /dev/null
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+test -f "$INSTALL_LOG" || exit 0
+
+sort "$INSTALL_LOG" | uniq | \
+while read file
+do
+       echo "RM $file"
+       rm -f "$file"
+       rmdir -p "`dirname $file`" 2>/dev/null
+done
+rm -f "$INSTALL_LOG"
diff --git a/scripts/utils.sh b/scripts/utils.sh
new file mode 100644 (file)
index 0000000..ec74f9f
--- /dev/null
@@ -0,0 +1,201 @@
+#!/bin/sh
+#
+# Copyright 2005 Timo Hirvonen
+#
+# This file is licensed under the GPLv2.
+
+# initialization {{{
+
+LC_ALL=C
+LANG=C
+
+export LC_ALL LANG
+
+if test "$CDPATH"
+then
+       echo "Exporting CDPATH is dangerous and unnecessary!"
+       echo
+fi
+unset CDPATH
+
+__cleanup()
+{
+       if test "$DEBUG_CONFIGURE" = y
+       then
+               echo
+               echo "DEBUG_CONFIGURE=y, not removing temporary files"
+               ls .tmp-[0-9]*-*
+       else
+               rm -f .tmp-[0-9]*-*
+       fi
+}
+
+__abort()
+{
+       # can't use "die" because stderr is often redirected to /dev/null
+       # (stdout could also be redirected but it's not so common)
+       echo
+       echo
+       echo "Aborting. configure failed."
+       # this executes __cleanup automatically
+       exit 1
+}
+
+# clean temporary files on exit
+trap '__cleanup' 0
+
+# clean temporary files and die with error message if interrupted
+trap '__abort' 1 2 3 13 15
+
+# }}}
+
+# config.mk variable names
+makefile_variables=""
+
+# cross compilation, prefix for CC, LD etc.
+# CROSS=
+
+# argc function_name $# min [max]
+argc()
+{
+       if test $# -lt 3 || test $# -gt 4
+       then
+               die "argc: expecting 3-4 arguments (got $*)"
+       fi
+
+       if test $# -eq 3
+       then
+               if test $2 -lt $3
+               then
+                       die "$1: expecting at least $3 arguments"
+               fi
+       else
+               if test $2 -lt $3 || test $2 -gt $4
+               then
+                       die "$1: expecting $3-$4 arguments"
+               fi
+       fi
+}
+
+# print warning message (all parameters)
+warn()
+{
+       echo "$@" >&2
+}
+
+# print error message (all parameters) and exit
+die()
+{
+       warn "$@"
+       exit 1
+}
+
+# usage: 'tmp_file .c'
+# get filename for temporary file
+tmp_file()
+{
+       if test -z "$__tmp_file_counter"
+       then
+               __tmp_file_counter=0
+       fi
+
+       while true
+       do
+               __tmp_filename=.tmp-${__tmp_file_counter}-${1}
+               __tmp_file_counter=`expr $__tmp_file_counter + 1`
+               test -f "$__tmp_filename" || break
+       done
+       echo "$__tmp_filename"
+}
+
+# get variable value
+#
+# @name:  name of the variable
+get_var()
+{
+       eval echo '"$'${1}'"'
+}
+
+# set variable by name
+#
+# @name:  name of the variable
+# @value: value of the variable
+set_var()
+{
+       eval $1='$2'
+}
+
+# set variable @name to @default IF NOT SET OR EMPTY
+#
+# @name:  name of the variable
+# @value: value of the variable
+var_default()
+{
+       test "`get_var $1`" || set_var $1 "$2"
+}
+
+# usage: echo $foo | to_upper
+to_upper()
+{
+       # stupid solaris tr doesn't understand ranges
+       tr abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ
+}
+
+# portable which command
+path_find()
+{
+       case $1 in
+       */*)
+               if test -x "$1"
+               then
+                       echo "$1"
+                       return 0
+               fi
+               return 1
+               ;;
+       esac
+
+       _ifs="$IFS"
+       IFS=:
+       for __pf_i in $PATH
+       do
+               if test -x "$__pf_i/$1"
+               then
+                       IFS="$_ifs"
+                       echo "$__pf_i/$1"
+                       return 0
+               fi
+       done
+       IFS="$_ifs"
+       return 1
+}
+
+show_usage()
+{
+       cat <<EOF
+Usage ./configure [-f FILE] [OPTION=VALUE]...
+
+  -f FILE         Read OPTION=VALUE list from FILE (sh script)
+$USAGE
+EOF
+       exit 0
+}
+
+# @tmpfile: temporary file
+# @file:    file to update
+#
+# replace @file with @tmpfile if their contents differ
+update_file()
+{
+       if test -f "$2"
+       then
+               if cmp "$2" "$1" 2>/dev/null 1>&2
+               then
+                       return 0
+               fi
+               echo "updating $2"
+       else
+               echo "creating $2"
+       fi
+       mv -f "$1" "$2"
+}
diff --git a/search.c b/search.c
new file mode 100644 (file)
index 0000000..6c4ea6f
--- /dev/null
+++ b/search.c
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "search.h"
+#include "editable.h"
+#include "xmalloc.h"
+#include "ui_curses.h"
+#include "convert.h"
+#include "options.h"
+
+struct searchable {
+       void *data;
+       struct iter head;
+       struct searchable_ops ops;
+};
+
+static int advance(struct searchable *s, struct iter *iter,
+               enum search_direction dir, int *wrapped)
+{
+       if (dir == SEARCH_FORWARD) {
+               if (!s->ops.get_next(iter)) {
+                       if (!wrap_search)
+                               return 0;
+                       *iter = s->head;
+                       if (!s->ops.get_next(iter))
+                               return 0;
+                       *wrapped = 1;
+               }
+       } else {
+               if (!s->ops.get_prev(iter)) {
+                       if (!wrap_search)
+                               return 0;
+                       *iter = s->head;
+                       if (!s->ops.get_prev(iter))
+                               return 0;
+                       *wrapped = 1;
+               }
+       }
+       return 1;
+}
+
+/* returns next matching item or NULL if not found
+ * result can be the current item unless skip_current is set */
+static int do_u_search(struct searchable *s, struct iter *iter, const char *text,
+               enum search_direction dir, int skip_current)
+{
+       struct iter start;
+       int wrapped = 0;
+
+       if (skip_current && !advance(s, iter, dir, &wrapped))
+               return 0;
+
+       start = *iter;
+       while (1) {
+               if (s->ops.matches(s->data, iter, text)) {
+                       if (wrapped)
+                               info_msg(dir == SEARCH_FORWARD ?
+                                        "search hit BOTTOM, continuing at TOP" :
+                                        "search hit TOP, continuing at BOTTOM");
+                       return 1;
+               }
+               if (!advance(s, iter, dir, &wrapped) || iters_equal(iter, &start))
+                       return 0;
+       }
+}
+
+static int do_search(struct searchable *s, struct iter *iter, const char *text,
+               enum search_direction dir, int skip_current)
+{
+       char *u_text = NULL;
+       int r;
+
+       /* search text is always in locale encoding (because cmdline is) */
+       if (!using_utf8 && utf8_encode(text, charset, &u_text) == 0)
+               text = u_text;
+
+       r = do_u_search(s, iter, text, dir, skip_current);
+
+       free(u_text);
+       return r;
+}
+
+struct searchable *searchable_new(void *data, const struct iter *head, const struct searchable_ops *ops)
+{
+       struct searchable *s;
+
+       s = xnew(struct searchable, 1);
+       s->data = data;
+       s->head = *head;
+       s->ops = *ops;
+       return s;
+}
+
+void searchable_free(struct searchable *s)
+{
+       free(s);
+}
+
+void searchable_set_head(struct searchable *s, const struct iter *head)
+{
+       s->head = *head;
+}
+
+int search(struct searchable *s, const char *text, enum search_direction dir, int beginning)
+{
+       struct iter iter;
+       int ret;
+
+       if (beginning) {
+               /* first or last item */
+               iter = s->head;
+               if (dir == SEARCH_FORWARD) {
+                       ret = s->ops.get_next(&iter);
+               } else {
+                       ret = s->ops.get_prev(&iter);
+               }
+       } else {
+               /* selected item */
+               ret = s->ops.get_current(s->data, &iter);
+       }
+       if (ret)
+               ret = do_search(s, &iter, text, dir, 0);
+       return ret;
+}
+
+int search_next(struct searchable *s, const char *text, enum search_direction dir)
+{
+       struct iter iter;
+       int ret;
+
+       if (!s->ops.get_current(s->data, &iter)) {
+               return 0;
+       }
+       ret = do_search(s, &iter, text, dir, 1);
+       return ret;
+}
diff --git a/search.h b/search.h
new file mode 100644 (file)
index 0000000..2a3ebba
--- /dev/null
+++ b/search.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_SEARCH_H
+#define CMUS_SEARCH_H
+
+#include "iter.h"
+
+enum search_direction { SEARCH_FORWARD, SEARCH_BACKWARD };
+
+struct searchable_ops {
+       int (*get_prev)(struct iter *iter);
+       int (*get_next)(struct iter *iter);
+       int (*get_current)(void *data, struct iter *iter);
+       int (*matches)(void *data, struct iter *iter, const char *text);
+};
+
+struct searchable;
+
+struct searchable *searchable_new(void *data, const struct iter *head, const struct searchable_ops *ops);
+void searchable_free(struct searchable *s);
+void searchable_set_head(struct searchable *s, const struct iter *head);
+
+int search(struct searchable *s, const char *text, enum search_direction dir, int beginning);
+int search_next(struct searchable *s, const char *text, enum search_direction dir);
+
+#endif
diff --git a/search_mode.c b/search_mode.c
new file mode 100644 (file)
index 0000000..0d43523
--- /dev/null
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "search_mode.h"
+#include "cmdline.h"
+#include "history.h"
+#include "ui_curses.h"
+#include "search.h"
+#include "xmalloc.h"
+#include "xstrjoin.h"
+#include "misc.h"
+#include "lib.h"
+#include "command_mode.h"
+#include "keys.h"
+
+#include <ctype.h>
+
+/* this is set in ui_curses.c */
+enum search_direction search_direction = SEARCH_FORWARD;
+
+/* current search string, this is set _only_ when user presses enter
+ * this string is used when 'n' or 'N' is pressed
+ * incremental search does not use this, it uses cmdline.line directly
+ */
+char *search_str = NULL;
+int search_restricted = 0;
+
+static struct history search_history;
+static char *search_history_filename;
+static char *history_search_text = NULL;
+
+static void update_search_line(const char *text, int restricted)
+{
+       int len = strlen(text);
+       char ch = search_direction == SEARCH_FORWARD ? '/' : '?';
+       char *buf, *ptr;
+
+       buf = xnew(char, len + 2);
+       ptr = buf;
+       if (restricted)
+               *ptr++ = ch;
+       memcpy(ptr, text, len + 1);
+       cmdline_set_text(buf);
+       free(buf);
+}
+
+static int search_line_empty(void)
+{
+       char ch;
+
+       if (cmdline.clen == 0)
+               return 1;
+       if (cmdline.clen > 1)
+               return 0;
+       ch = search_direction == SEARCH_FORWARD ? '/' : '?';
+       return cmdline.line[0] == ch;
+}
+
+static void parse_line(const char **text, int *restricted)
+{
+       char ch = search_direction == SEARCH_FORWARD ? '/' : '?';
+       int r = 0;
+
+       if (cmdline.line[0] == ch) {
+               /* //WORDS or ??WORDS */
+               r = 1;
+       }
+       *text = cmdline.line + r;
+       *restricted = r;
+}
+
+static void reset_history_search(void)
+{
+       history_reset_search(&search_history);
+       free(history_search_text);
+       history_search_text = NULL;
+}
+
+static void backspace(void)
+{
+       if (cmdline.clen > 0) {
+               cmdline_backspace();
+       } else {
+               input_mode = NORMAL_MODE;
+       }
+}
+
+static void delete(void)
+{
+       /* save old value */
+       int restricted = search_restricted;
+       const char *text;
+
+       cmdline_delete_ch();
+       parse_line(&text, &search_restricted);
+       if (text[0])
+               search(searchable, text, search_direction, 0);
+
+       /* restore old value */
+       search_restricted = restricted;
+}
+
+void search_text(const char *text, int restricted, int beginning)
+{
+       if (text[0] == 0) {
+               /* cmdline is "/", "?", "//" or "??" */
+               if (search_str) {
+                       /* use old search string */
+                       search_restricted = restricted;
+                       if (!search_next(searchable, search_str, search_direction))
+                               search_not_found();
+               }
+       } else {
+               /* set new search string and add it to the history */
+               free(search_str);
+               search_str = xstrdup(text);
+               history_add_line(&search_history, text);
+
+               /* search not yet done if up or down arrow was pressed */
+               search_restricted = restricted;
+               if (!search(searchable, search_str, search_direction, beginning))
+                       search_not_found();
+       }
+}
+
+void search_mode_ch(uchar ch)
+{
+       const char *text;
+       int restricted;
+
+       switch (ch) {
+       case 0x01: // ^A
+               cmdline_move_home();
+               break;
+       case 0x02: // ^B
+               cmdline_move_left();
+               break;
+       case 0x04: // ^D
+               delete();
+               break;
+       case 0x05: // ^E
+               cmdline_move_end();
+               break;
+       case 0x06: // ^F
+               cmdline_move_right();
+               break;
+       case 0x03: // ^C
+       case 0x07: // ^G
+       case 0x1B: // ESC
+               parse_line(&text, &restricted);
+               if (text[0]) {
+                       history_add_line(&search_history, text);
+                       cmdline_clear();
+               }
+               input_mode = NORMAL_MODE;
+               break;
+       case 0x0A:
+               parse_line(&text, &restricted);
+               search_text(text, restricted, 0);
+               cmdline_clear();
+               input_mode = NORMAL_MODE;
+               break;
+       case 0x0B:
+               cmdline_clear_end();
+               break;
+       case 0x10: // ^P
+               search_mode_key(KEY_UP);
+               return;
+       case 0xE: // ^N
+               search_mode_key(KEY_DOWN);
+               return;
+       case 0x15:
+               cmdline_backspace_to_bol();
+               break;
+       case 0x17: // ^W
+               cmdline_backward_delete_word(cmdline_word_delimiters);
+               break;
+       case 0x08: // ^H
+       case 127:
+               backspace();
+               break;
+       default:
+               if (ch < 0x20) {
+                       return;
+               } else {
+                       /* start from beginning if this is first char */
+                       int beginning = search_line_empty();
+
+                       /* save old value
+                        *
+                        * don't set search_{str,restricted} here because
+                        * search can be cancelled by pressing ESC
+                        */
+                       restricted = search_restricted;
+
+                       cmdline_insert_ch(ch);
+                       parse_line(&text, &search_restricted);
+                       search(searchable, text, search_direction, beginning);
+
+                       /* restore old value */
+                       search_restricted = restricted;
+               }
+               break;
+       }
+       reset_history_search();
+}
+
+void search_mode_escape(int c)
+{
+       switch (c) {
+       case 98:
+               cmdline_backward_word(cmdline_filename_delimiters);
+               break;
+       case 100:
+               cmdline_delete_word(cmdline_filename_delimiters);
+               break;
+       case 102:
+               cmdline_forward_word(cmdline_filename_delimiters);
+               break;
+       case 127:
+       case KEY_BACKSPACE:
+               cmdline_backward_delete_word(cmdline_filename_delimiters);
+               break;
+       }
+       reset_history_search();
+}
+
+void search_mode_key(int key)
+{
+       const char *text;
+       int restricted;
+
+       switch (key) {
+       case KEY_DC:
+               delete();
+               break;
+       case KEY_BACKSPACE:
+               backspace();
+               break;
+       case KEY_LEFT:
+               cmdline_move_left();
+               return;
+       case KEY_RIGHT:
+               cmdline_move_right();
+               return;
+       case KEY_HOME:
+               cmdline_move_home();
+               return;
+       case KEY_END:
+               cmdline_move_end();
+               return;
+       case KEY_UP:
+               parse_line(&text, &restricted);
+               if (history_search_text == NULL)
+                       history_search_text = xstrdup(text);
+               text = history_search_forward(&search_history, history_search_text);
+               if (text)
+                       update_search_line(text, restricted);
+               return;
+       case KEY_DOWN:
+               if (history_search_text) {
+                       parse_line(&text, &restricted);
+                       text = history_search_backward(&search_history, history_search_text);
+                       if (text) {
+                               update_search_line(text, restricted);
+                       } else {
+                               update_search_line(history_search_text, restricted);
+                       }
+               }
+               return;
+       default:
+               return;
+       }
+       reset_history_search();
+}
+
+void search_mode_mouse(MEVENT *event)
+{
+       if ((event->bstate & BUTTON1_PRESSED) || (event->bstate & BUTTON3_PRESSED)) {
+               const char *text;
+               int restricted;
+               if (event->y <= window_get_nr_rows(current_win()) + 2) {
+                       parse_line(&text, &restricted);
+                       if (text[0]) {
+                               history_add_line(&search_history, text);
+                               cmdline_clear();
+                       }
+                       input_mode = NORMAL_MODE;
+                       normal_mode_mouse(event);
+                       return;
+               }
+               if (event->x == 0)
+                       return;
+               int i = event->x > cmdline.clen ? cmdline.clen : event->x - 1;
+               while (i < cmdline.cpos)
+                       cmdline_move_left();
+               while (i > cmdline.cpos)
+                       cmdline_move_right();
+       } else if (event->bstate & BUTTON4_PRESSED) {
+               search_mode_key(KEY_UP);
+       } else if (event->bstate & BUTTON5_PRESSED) {
+               search_mode_key(KEY_DOWN);
+       }
+}
+
+void search_mode_init(void)
+{
+       search_history_filename = xstrjoin(cmus_config_dir, "/search-history");
+       history_load(&search_history, search_history_filename, 100);
+}
+
+void search_mode_exit(void)
+{
+       history_save(&search_history);
+       history_free(&search_history);
+       free(search_history_filename);
+}
diff --git a/search_mode.h b/search_mode.h
new file mode 100644 (file)
index 0000000..9b55874
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_SEARCH_MODE_H
+#define CMUS_SEARCH_MODE_H
+
+#include "search.h"
+#include "uchar.h"
+
+#if defined(__sun__)
+#include <ncurses.h>
+#else
+#include <curses.h>
+#endif
+
+extern char *search_str;
+extern enum search_direction search_direction;
+
+/* //WORDS or ??WORDS search mode */
+extern int search_restricted;
+
+void search_mode_ch(uchar ch);
+void search_mode_escape(int c);
+void search_mode_key(int key);
+void search_mode_mouse(MEVENT *event);
+void search_mode_init(void);
+void search_mode_exit(void);
+
+void search_text(const char *text, int restricted, int beginning);
+
+#endif
diff --git a/server.c b/server.c
new file mode 100644 (file)
index 0000000..d9a8133
--- /dev/null
+++ b/server.c
@@ -0,0 +1,412 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "server.h"
+#include "prog.h"
+#include "command_mode.h"
+#include "search_mode.h"
+#include "options.h"
+#include "output.h"
+#include "utils.h"
+#include "xmalloc.h"
+#include "player.h"
+#include "file.h"
+#include "compiler.h"
+#include "debug.h"
+#include "gbuf.h"
+#include "ui_curses.h"
+#include "misc.h"
+#include "keyval.h"
+#include "convert.h"
+#include "format_print.h"
+
+#include <stdarg.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <ctype.h>
+
+int server_socket;
+LIST_HEAD(client_head);
+
+static char *title_buf = NULL;
+
+static union {
+       struct sockaddr sa;
+       struct sockaddr_un un;
+       struct sockaddr_storage sas;
+} addr;
+
+#define MAX_CLIENTS 10
+
+static int cmd_status(struct client *client)
+{
+       const char *export_options[] = {
+               "aaa_mode",
+               "continue",
+               "play_library",
+               "play_sorted",
+               "replaygain",
+               "replaygain_limit",
+               "replaygain_preamp",
+               "repeat",
+               "repeat_current",
+               "shuffle",
+               "softvol",
+               NULL
+       };
+       const struct track_info *ti;
+       struct cmus_opt *opt;
+       char optbuf[OPTION_MAX_SIZE];
+       GBUF(buf);
+       int vol_left, vol_right;
+       int i, ret;
+       enum player_status status;
+
+       gbuf_addf(&buf, "status %s\n", player_status_names[player_info.status]);
+       ti = player_info.ti;
+       if (ti) {
+               gbuf_addf(&buf, "file %s\n", escape(ti->filename));
+               gbuf_addf(&buf, "duration %d\n", ti->duration);
+               gbuf_addf(&buf, "position %d\n", player_info.pos);
+               for (i = 0; ti->comments[i].key; i++)
+                       gbuf_addf(&buf, "tag %s %s\n",
+                                       ti->comments[i].key,
+                                       escape(ti->comments[i].val));
+       }
+
+       /* add track metadata to cmus-status */
+       status = player_info.status;
+       if (status == PLAYER_STATUS_PLAYING && ti && is_http_url(player_info.ti->filename)) {
+       const char *title = get_stream_title();
+               if (title != NULL) {
+                       free(title_buf);
+                       title_buf = to_utf8(title, icecast_default_charset);
+                       // we have a stream title (probably artist/track/album info)
+                       gbuf_addf(&buf, "stream %s\n", escape(title_buf));
+               } else if (ti->comment != NULL) {
+                       // fallback to the radio station name
+                       gbuf_addf(&buf, "stream %s\n", escape(ti->comment));
+               }
+       }
+
+       /* output options */
+       for (i = 0; export_options[i]; i++) {
+               opt = option_find(export_options[i]);
+               if (opt) {
+                       opt->get(opt->data, optbuf, OPTION_MAX_SIZE);
+                       gbuf_addf(&buf, "set %s %s\n", opt->name, optbuf);
+               }
+       }
+
+       /* get volume (copied from ui_curses.c) */
+       if (soft_vol) {
+               vol_left = soft_vol_l;
+               vol_right = soft_vol_r;
+       } else if (!volume_max) {
+               vol_left = vol_right = -1;
+       } else {
+               vol_left = scale_to_percentage(volume_l, volume_max);
+               vol_right = scale_to_percentage(volume_r, volume_max);
+       }
+
+       /* output volume */
+       gbuf_addf(&buf, "set vol_left %d\n", vol_left);
+       gbuf_addf(&buf, "set vol_right %d\n", vol_right);
+
+       gbuf_add_str(&buf, "\n");
+
+       ret = write_all(client->fd, buf.buffer, buf.len);
+       gbuf_free(&buf);
+       return ret;
+}
+
+static int cmd_format_print(struct client *client, char *arg)
+{
+       if (run_only_safe_commands) {
+               d_print("trying to execute unsafe command over net\n");
+               return write_all(client->fd, "\n", strlen("\n"));
+       }
+
+       int args_idx, ac, i, ret;
+       char **args = NULL;
+
+       if (arg)
+               args = parse_cmd(arg, &args_idx, &ac);
+
+       if (args == NULL) {
+               error_msg("not enough arguments\n");
+               return write_all(client->fd, "\n", strlen("\n"));
+       }
+
+       GBUF(buf);
+
+       const struct format_option *fopts = get_global_fopts();
+       for (i = 0; i < ac; ++i) {
+               if (format_valid(args[i], fopts))
+                       format_print_gbuf(&buf, 0, args[i], fopts);
+               gbuf_add_ch(&buf, '\n');
+               free(args[i]);
+       }
+       gbuf_add_ch(&buf, '\n');
+
+       ret = write_all(client->fd, buf.buffer, buf.len);
+       gbuf_free(&buf);
+       free(args);
+       return ret;
+}
+
+static ssize_t send_answer(int fd, const char *format, ...)
+{
+       char buf[512];
+       va_list ap;
+
+       va_start(ap, format);
+       vsnprintf(buf, sizeof(buf), format, ap);
+       va_end(ap);
+
+       return write_all(fd, buf, strlen(buf));
+}
+
+static void read_commands(struct client *client)
+{
+       char buf[1024];
+       int pos = 0;
+       if (!client->authenticated)
+               client->authenticated = addr.sa.sa_family == AF_UNIX;
+
+       while (1) {
+               int rc, s, i;
+
+               rc = read(client->fd, buf + pos, sizeof(buf) - pos);
+               if (rc == -1) {
+                       if (errno == EINTR)
+                               continue;
+                       if (errno == EAGAIN)
+                               return;
+                       goto close;
+               }
+               if (rc == 0)
+                       goto close;
+               pos += rc;
+
+               s = 0;
+               for (i = 0; i < pos; i++) {
+                       const char *line, *msg;
+                       char *cmd, *arg;
+                       int ret;
+
+                       if (buf[i] != '\n')
+                               continue;
+
+                       buf[i] = 0;
+                       line = buf + s;
+                       s = i + 1;
+
+                       if (!client->authenticated) {
+                               if (!server_password) {
+                                       msg = "password is unset, tcp/ip disabled";
+                                       d_print("%s\n", msg);
+                                       ret = send_answer(client->fd, "%s\n\n", msg);
+                                       goto close;
+                               }
+                               if (strncmp(line, "passwd ", 7) == 0)
+                                       line += 7;
+                               client->authenticated = !strcmp(line, server_password);
+                               if (!client->authenticated) {
+                                       msg = "authentication failed";
+                                       d_print("%s\n", msg);
+                                       ret = send_answer(client->fd, "%s\n\n", msg);
+                                       goto close;
+                               }
+                               ret = write_all(client->fd, "\n", 1);
+                               continue;
+                       }
+
+                       while (isspace((unsigned char)*line))
+                               line++;
+
+                       if (*line == '/') {
+                               int restricted = 0;
+                               line++;
+                               search_direction = SEARCH_FORWARD;
+                               if (*line == '/') {
+                                       line++;
+                                       restricted = 1;
+                               }
+                               search_text(line, restricted, 1);
+                               ret = write_all(client->fd, "\n", 1);
+                       } else if (*line == '?') {
+                               int restricted = 0;
+                               line++;
+                               search_direction = SEARCH_BACKWARD;
+                               if (*line == '?') {
+                                       line++;
+                                       restricted = 1;
+                               }
+                               search_text(line, restricted, 1);
+                               ret = write_all(client->fd, "\n", 1);
+                       } else if (parse_command(line, &cmd, &arg)) {
+                               if (!strcmp(cmd, "status")) {
+                                       ret = cmd_status(client);
+                               } else if (!strcmp(cmd, "format_print")) {
+                                       ret = cmd_format_print(client, arg);
+                               } else {
+                                       if (strcmp(cmd, "passwd") != 0) {
+                                               set_client_fd(client->fd);
+                                               run_parsed_command(cmd, arg);
+                                               set_client_fd(-1);
+                                       }
+                                       ret = write_all(client->fd, "\n", 1);
+                               }
+                               free(cmd);
+                               free(arg);
+                       } else {
+                               // don't hang cmus-remote
+                               ret = write_all(client->fd, "\n", 1);
+                       }
+                       if (ret < 0) {
+                               d_print("write: %s\n", strerror(errno));
+                               goto close;
+                       }
+               }
+               memmove(buf, buf + s, pos - s);
+               pos -= s;
+       }
+       return;
+close:
+       close(client->fd);
+       list_del(&client->node);
+       free(client);
+}
+
+void server_accept(void)
+{
+       struct client *client;
+       struct sockaddr saddr;
+       socklen_t saddr_size = sizeof(saddr);
+       int fd;
+
+       fd = accept(server_socket, &saddr, &saddr_size);
+       if (fd == -1)
+               return;
+
+       fcntl(fd, F_SETFL, O_NONBLOCK);
+
+       client = xnew(struct client, 1);
+       client->fd = fd;
+       client->authenticated = 0;
+       list_add_tail(&client->node, &client_head);
+}
+
+void server_serve(struct client *client)
+{
+       /* unix connection is secure, other insecure */
+       run_only_safe_commands = addr.sa.sa_family != AF_UNIX;
+       read_commands(client);
+       run_only_safe_commands = 0;
+}
+
+void server_init(char *address)
+{
+       const char *port = STRINGIZE(DEFAULT_PORT);
+       size_t addrlen;
+
+       if (strchr(address, '/')) {
+               addr.sa.sa_family = AF_UNIX;
+               strncpy(addr.un.sun_path, address, sizeof(addr.un.sun_path) - 1);
+
+               addrlen = sizeof(struct sockaddr_un);
+       } else {
+               const struct addrinfo hints = {
+                       .ai_socktype = SOCK_STREAM
+               };
+               struct addrinfo *result;
+               char *s = strrchr(address, ':');
+               int rc;
+
+               if (s) {
+                       *s++ = 0;
+                       port = s;
+               }
+
+               rc = getaddrinfo(address, port, &hints, &result);
+               if (rc != 0)
+                       die("getaddrinfo: %s\n", gai_strerror(rc));
+               memcpy(&addr.sa, result->ai_addr, result->ai_addrlen);
+               addrlen = result->ai_addrlen;
+               freeaddrinfo(result);
+       }
+
+       server_socket = socket(addr.sa.sa_family, SOCK_STREAM, 0);
+       if (server_socket == -1)
+               die_errno("socket");
+
+       if (bind(server_socket, &addr.sa, addrlen) == -1) {
+               int sock;
+
+               if (errno != EADDRINUSE)
+                       die_errno("bind");
+
+               /* address already in use */
+               if (addr.sa.sa_family != AF_UNIX)
+                       die("cmus is already listening on %s:%s\n", address, port);
+
+               /* try to connect to server */
+               sock = socket(AF_UNIX, SOCK_STREAM, 0);
+               if (sock == -1)
+                       die_errno("socket");
+
+               if (connect(sock, &addr.sa, addrlen) == -1) {
+                       if (errno != ENOENT && errno != ECONNREFUSED)
+                               die_errno("connect");
+
+                       /* server not running => dead socket */
+
+                       /* try to remove dead socket */
+                       if (unlink(addr.un.sun_path) == -1 && errno != ENOENT)
+                               die_errno("unlink");
+                       if (bind(server_socket, &addr.sa, addrlen) == -1)
+                               die_errno("bind");
+               } else {
+                       /* server already running */
+                       die("cmus is already listening on socket %s\n", address);
+               }
+               close(sock);
+       }
+
+       if (listen(server_socket, MAX_CLIENTS) == -1)
+               die_errno("listen");
+}
+
+void server_exit(void)
+{
+       close(server_socket);
+       if (addr.sa.sa_family == AF_UNIX)
+               unlink(addr.un.sun_path);
+}
diff --git a/server.h b/server.h
new file mode 100644 (file)
index 0000000..bc33e42
--- /dev/null
+++ b/server.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_SERVER_H
+#define CMUS_SERVER_H
+
+#include "list.h"
+
+struct client {
+       struct list_head node;
+       int fd;
+       unsigned int authenticated : 1;
+};
+
+extern int server_socket;
+extern struct list_head client_head;
+
+void server_init(char *address);
+void server_exit(void);
+void server_accept(void);
+void server_serve(struct client *client);
+
+#endif
diff --git a/sf.h b/sf.h
new file mode 100644 (file)
index 0000000..c3c11fc
--- /dev/null
+++ b/sf.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_SF_H
+#define CMUS_SF_H
+
+/*
+ *  0     1 big_endian 0-1
+ *  1     1 is_signed  0-1
+ *  2     1 unused     0
+ *  3-5   3 bits >> 3  0-7 (* 8 = 0-56)
+ *  6-23 18 rate       0-262143
+ * 24-31  8 channels   0-255
+ */
+typedef unsigned int sample_format_t;
+
+#define SF_BIGENDIAN_MASK      0x00000001
+#define SF_SIGNED_MASK         0x00000002
+#define SF_BITS_MASK           0x00000038
+#define SF_RATE_MASK           0x00ffffc0
+#define SF_CHANNELS_MASK       0xff000000
+
+#define SF_BIGENDIAN_SHIFT     0
+#define SF_SIGNED_SHIFT                1
+#define SF_BITS_SHIFT          0
+#define SF_RATE_SHIFT          6
+#define SF_CHANNELS_SHIFT      24
+
+#define sf_get_bigendian(sf)   (((sf) & SF_BIGENDIAN_MASK) >> SF_BIGENDIAN_SHIFT)
+#define sf_get_signed(sf)      (((sf) & SF_SIGNED_MASK   ) >> SF_SIGNED_SHIFT)
+#define sf_get_bits(sf)                (((sf) & SF_BITS_MASK     ) >> SF_BITS_SHIFT)
+#define sf_get_rate(sf)                (((sf) & SF_RATE_MASK     ) >> SF_RATE_SHIFT)
+#define sf_get_channels(sf)    (((sf) & SF_CHANNELS_MASK ) >> SF_CHANNELS_SHIFT)
+
+#define sf_signed(val)         (((val) << SF_SIGNED_SHIFT   ) & SF_SIGNED_MASK)
+#define sf_bits(val)           (((val) << SF_BITS_SHIFT     ) & SF_BITS_MASK)
+#define sf_rate(val)           (((val) << SF_RATE_SHIFT     ) & SF_RATE_MASK)
+#define sf_channels(val)       (((val) << SF_CHANNELS_SHIFT ) & SF_CHANNELS_MASK)
+#define sf_bigendian(val)      (((val) << SF_BIGENDIAN_SHIFT) & SF_BIGENDIAN_MASK)
+#ifdef WORDS_BIGENDIAN
+#      define sf_host_endian() sf_bigendian(1)
+#else
+#      define sf_host_endian() sf_bigendian(0)
+#endif
+
+#define sf_get_sample_size(sf) (sf_get_bits((sf)) >> 3)
+#define sf_get_frame_size(sf)  (sf_get_sample_size((sf)) * sf_get_channels((sf)))
+#define sf_get_second_size(sf) (sf_get_rate((sf)) * sf_get_frame_size((sf)))
+
+#endif
diff --git a/spawn.c b/spawn.c
new file mode 100644 (file)
index 0000000..117d120
--- /dev/null
+++ b/spawn.c
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "spawn.h"
+#include "file.h"
+
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+int spawn(char *argv[], int *status, int do_wait)
+{
+       pid_t pid;
+       int err_pipe[2];
+
+       if (pipe(err_pipe) == -1)
+               return -1;
+
+       pid = fork();
+       if (pid == -1) {
+               /* error */
+               return -1;
+       } else if (pid == 0) {
+               /* child */
+               int dev_null, err, i;
+
+               /* create grandchild and exit child to avoid zombie processes */
+               if (!do_wait) {
+                       switch (fork()) {
+                       case 0:
+                               /* grandchild */
+                               break;
+                       case -1:
+                               /* error */
+                               _exit(127);
+                       default:
+                               /* parent of grandchild */
+                               _exit(0);
+                       }
+               }
+
+               close(err_pipe[0]);
+               fcntl(err_pipe[1], F_SETFD, FD_CLOEXEC);
+
+               /* redirect stdout and stderr to /dev/null if possible */
+               dev_null = open("/dev/null", O_WRONLY);
+               if (dev_null != -1) {
+                       dup2(dev_null, 1);
+                       dup2(dev_null, 2);
+               }
+
+               /* not interactive, close stdin */
+               close(0);
+
+               /* close unused fds */
+               for (i = 3; i < 30; i++)
+                       close(i);
+
+               execvp(argv[0], argv);
+
+               /* error */
+               err = errno;
+               write_all(err_pipe[1], &err, sizeof(int));
+               exit(1);
+       } else {
+               /* parent */
+               int rc, errno_save, child_errno, tmp;
+
+               close(err_pipe[1]);
+               rc = read_all(err_pipe[0], &child_errno, sizeof(int));
+               errno_save = errno;
+               close(err_pipe[0]);
+
+               if (!do_wait)
+                       status = &tmp;
+               waitpid(pid, status, 0);
+
+               if (rc == -1) {
+                       errno = errno_save;
+                       return -1;
+               }
+               if (rc == sizeof(int)) {
+                       errno = child_errno;
+                       return -1;
+               }
+               if (rc != 0) {
+                       errno = EMSGSIZE;
+                       return -1;
+               }
+               return 0;
+       }
+}
diff --git a/spawn.h b/spawn.h
new file mode 100644 (file)
index 0000000..9fbf726
--- /dev/null
+++ b/spawn.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_SPAWN_H
+#define CMUS_SPAWN_H
+
+int spawn(char *argv[], int *status, int do_wait);
+
+#endif
diff --git a/tabexp.c b/tabexp.c
new file mode 100644 (file)
index 0000000..4e38b1b
--- /dev/null
+++ b/tabexp.c
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tabexp.h"
+#include "xmalloc.h"
+#include "xstrjoin.h"
+#include "debug.h"
+
+#include <stdlib.h>
+
+struct tabexp tabexp = {
+       .head = NULL,
+       .tails = NULL,
+       .count = 0
+};
+
+char *tabexp_expand(const char *src, void (*load_matches)(const char *src), int direction)
+{
+       static int idx = -1;
+       char *expanded;
+
+       if (tabexp.tails == NULL) {
+               load_matches(src);
+               if (tabexp.tails == NULL) {
+                       BUG_ON(tabexp.head != NULL);
+                       return NULL;
+               }
+               BUG_ON(tabexp.head == NULL);
+               idx = -1;
+       }
+       idx += direction;
+
+       if (idx >= tabexp.count)
+               idx = 0;
+       else if (idx < 0)
+               idx = tabexp.count - 1;
+
+       expanded = xstrjoin(tabexp.head, tabexp.tails[idx]);
+       if (tabexp.count == 1)
+               tabexp_reset();
+       return expanded;
+}
+
+void tabexp_reset(void)
+{
+       int i;
+       for (i = 0; i < tabexp.count; i++)
+               free(tabexp.tails[i]);
+       free(tabexp.tails);
+       free(tabexp.head);
+       tabexp.tails = NULL;
+       tabexp.head = NULL;
+       tabexp.count = 0;
+}
diff --git a/tabexp.h b/tabexp.h
new file mode 100644 (file)
index 0000000..d934b94
--- /dev/null
+++ b/tabexp.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_TABEXP_H
+#define CMUS_TABEXP_H
+
+struct tabexp {
+       char *head;
+       char **tails;
+       int count;
+};
+
+extern struct tabexp tabexp;
+
+/* return expanded src or NULL */
+char *tabexp_expand(const char *src, void (*load_matches)(const char *src), int direction);
+
+void tabexp_reset(void);
+
+#endif
diff --git a/tabexp_file.c b/tabexp_file.c
new file mode 100644 (file)
index 0000000..3b6a2ba
--- /dev/null
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tabexp_file.h"
+#include "tabexp.h"
+#include "load_dir.h"
+#include "misc.h"
+#include "xmalloc.h"
+#include "xstrjoin.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <pwd.h>
+#include <dirent.h>
+
+static char *get_home(const char *user)
+{
+       struct passwd *passwd;
+       char *home;
+       int len;
+
+       if (user[0] == 0) {
+               passwd = getpwuid(getuid());
+       } else {
+               passwd = getpwnam(user);
+       }
+       if (passwd == NULL)
+               return NULL;
+       len = strlen(passwd->pw_dir);
+       home = xnew(char, len + 2);
+       memcpy(home, passwd->pw_dir, len);
+       home[len] = '/';
+       home[len + 1] = 0;
+       return home;
+}
+
+static char *get_full_dir_name(const char *dir)
+{
+       char *full;
+
+       if (dir[0] == 0) {
+               full = xstrdup("./");
+       } else if (dir[0] == '~') {
+               char *first_slash, *tmp, *home;
+
+               first_slash = strchr(dir, '/');
+               tmp = xstrndup(dir, first_slash - dir);
+               home = get_home(tmp + 1);
+               free(tmp);
+               if (home == NULL)
+                       return NULL;
+               full = xstrjoin(home, first_slash);
+               free(home);
+       } else {
+               full = xstrdup(dir);
+       }
+       return full;
+}
+
+static void load_dir(struct ptr_array *array,
+               const char *dirname, const char *start,
+               int (*filter)(const char *, const struct stat *))
+{
+       int start_len = strlen(start);
+       struct directory dir;
+       char *full_dir_name;
+       const char *name;
+
+       full_dir_name = get_full_dir_name(dirname);
+       if (!full_dir_name)
+               return;
+
+       if (dir_open(&dir, full_dir_name))
+               goto out;
+
+       while ((name = dir_read(&dir))) {
+               char *str;
+
+               if (!start_len) {
+                       if (name[0] == '.')
+                               continue;
+               } else {
+                       if (strncmp(name, start, start_len))
+                               continue;
+               }
+
+               if (!filter(name, &dir.st))
+                       continue;
+
+               if (S_ISDIR(dir.st.st_mode)) {
+                       int len = strlen(name);
+
+                       str = xnew(char, len + 2);
+                       memcpy(str, name, len);
+                       str[len++] = '/';
+                       str[len] = 0;
+               } else {
+                       str = xstrdup(name);
+               }
+               ptr_array_add(array, str);
+       }
+       dir_close(&dir);
+out:
+       free(full_dir_name);
+}
+
+/*
+ * load all directory entries from directory 'dir' starting with 'start' and
+ * filtered with 'filter'
+ */
+static void tabexp_load_dir(const char *dirname, const char *start,
+               int (*filter)(const char *, const struct stat *))
+{
+       PTR_ARRAY(array);
+
+       /* tabexp is reseted */
+       load_dir(&array, dirname, start, filter);
+
+       if (array.count) {
+               ptr_array_sort(&array, strptrcmp);
+
+               tabexp.head = xstrdup(dirname);
+               tabexp.tails = array.ptrs;
+               tabexp.count = array.count;
+       }
+}
+
+static void tabexp_load_env_path(const char *env_path, const char *start,
+               int (*filter)(const char *, const struct stat *))
+{
+       char *path = xstrdup(env_path);
+       PTR_ARRAY(array);
+       char cwd[1024];
+       char *p = path, *n;
+
+       /* tabexp is reseted */
+       do {
+               n = strchr(p, ':');
+               if (n)
+                       *n = '\0';
+               if (strcmp(p, "") == 0 && getcwd(cwd, sizeof(cwd)))
+                       p = cwd;
+               load_dir(&array, p, start, filter);
+               p = n + 1;
+       } while (n);
+
+       if (array.count) {
+               ptr_array_sort(&array, strptrcoll);
+               ptr_array_unique(&array, strptrcmp);
+
+               tabexp.head = xstrdup("");
+               tabexp.tails = array.ptrs;
+               tabexp.count = array.count;
+       }
+
+       free(path);
+}
+
+void expand_files_and_dirs(const char *src,
+               int (*filter)(const char *name, const struct stat *s))
+{
+       char *slash;
+
+       /* split src to dir and file */
+       slash = strrchr(src, '/');
+       if (slash) {
+               char *dir;
+               const char *file;
+
+               /* split */
+               dir = xstrndup(src, slash - src + 1);
+               file = slash + 1;
+               /* get all dentries starting with file from dir */
+               tabexp_load_dir(dir, file, filter);
+               free(dir);
+       } else {
+               if (src[0] == '~') {
+                       char *home = get_home(src + 1);
+
+                       if (home) {
+                               tabexp.head = xstrdup("");
+                               tabexp.tails = xnew(char *, 1);
+                               tabexp.tails[0] = home;
+                               tabexp.count = 1;
+                       }
+               } else {
+                       tabexp_load_dir("", src, filter);
+               }
+       }
+}
+
+void expand_env_path(const char *src,
+               int (*filter)(const char *name, const struct stat *s))
+{
+       const char *env_path = getenv("PATH");
+
+       if (!env_path || strcmp(env_path, "") == 0)
+               return;
+
+       tabexp_load_env_path(env_path, src, filter);
+}
diff --git a/tabexp_file.h b/tabexp_file.h
new file mode 100644 (file)
index 0000000..959fffc
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_TABEXP_FILE_H
+#define CMUS_TABEXP_FILE_H
+
+#include <sys/stat.h>
+
+void expand_files_and_dirs(const char *src,
+               int (*filter)(const char *name, const struct stat *s));
+void expand_env_path(const char *src,
+               int (*filter)(const char *name, const struct stat *s));
+
+#endif
diff --git a/track.c b/track.c
new file mode 100644 (file)
index 0000000..fc911ea
--- /dev/null
+++ b/track.c
@@ -0,0 +1,433 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "track.h"
+#include "iter.h"
+#include "search_mode.h"
+#include "window.h"
+#include "options.h"
+#include "uchar.h"
+#include "xmalloc.h"
+#include "debug.h"
+#include "misc.h"
+
+#include <string.h>
+
+void simple_track_init(struct simple_track *track, struct track_info *ti)
+{
+       track->info = ti;
+       track->marked = 0;
+       RB_CLEAR_NODE(&track->tree_node);
+}
+
+struct simple_track *simple_track_new(struct track_info *ti)
+{
+       struct simple_track *t = xnew(struct simple_track, 1);
+
+       track_info_ref(ti);
+       simple_track_init(t, ti);
+       return t;
+}
+
+GENERIC_ITER_PREV(simple_track_get_prev, struct simple_track, node)
+GENERIC_ITER_NEXT(simple_track_get_next, struct simple_track, node)
+
+int simple_track_search_get_current(void *data, struct iter *iter)
+{
+       return window_get_sel(data, iter);
+}
+
+int _simple_track_search_matches(struct iter *iter, const char *text)
+{
+       unsigned int flags = TI_MATCH_TITLE;
+       struct simple_track *track = iter_to_simple_track(iter);
+
+       if (!search_restricted)
+               flags |= TI_MATCH_ARTIST | TI_MATCH_ALBUM | TI_MATCH_ALBUMARTIST;
+
+       return track_info_matches(track->info, text, flags);
+}
+
+int simple_track_search_matches(void *data, struct iter *iter, const char *text)
+{
+       int rc = _simple_track_search_matches(iter, text);
+       if (rc)
+               window_set_sel(data, iter);
+       return rc;
+}
+
+void shuffle_insert(struct rb_root *root, struct shuffle_track *previous, struct shuffle_track *next)
+{
+       BUG_ON(root == NULL);
+       BUG_ON(next == NULL);
+
+       if (previous == next)
+               return;
+       rb_erase(&next->tree_node, root);
+
+       struct rb_node *parent = previous ? &previous->tree_node : NULL;
+       struct rb_node **new = parent ? &parent->rb_right : &root->rb_node;
+       while (*new) {
+               parent = *new;
+               new = &(*new)->rb_left;
+       }
+
+       rb_link_node(&next->tree_node, parent, new);
+       rb_insert_color(&next->tree_node, root);
+}
+
+struct shuffle_track *shuffle_list_get_next(struct rb_root *root, struct shuffle_track *cur,
+               int (*filter_callback)(const struct simple_track *))
+{
+       struct rb_node *node;
+
+       if (!cur)
+               return tree_node_to_shuffle_track(rb_first(root));
+
+       node = rb_next(&cur->tree_node);
+again:
+       while (node) {
+               struct shuffle_track *track = tree_node_to_shuffle_track(node);
+
+               if (filter_callback((struct simple_track *)track))
+                       return track;
+               node = rb_next(node);
+       }
+       if (repeat) {
+               if (auto_reshuffle)
+                       shuffle_list_reshuffle(root);
+               node = rb_first(root);
+               goto again;
+       }
+       return NULL;
+}
+
+struct shuffle_track *shuffle_list_get_prev(struct rb_root *root, struct shuffle_track *cur,
+               int (*filter_callback)(const struct simple_track *))
+{
+       struct rb_node *node;
+
+       if (!cur)
+               return tree_node_to_shuffle_track(rb_last(root));
+
+       node = rb_prev(&cur->tree_node);
+again:
+       while (node) {
+               struct shuffle_track *track = tree_node_to_shuffle_track(node);
+
+               if (filter_callback((struct simple_track *)track))
+                       return track;
+               node = rb_prev(node);
+       }
+       if (repeat) {
+               if (auto_reshuffle)
+                       shuffle_list_reshuffle(root);
+               node = rb_last(root);
+               goto again;
+       }
+       return NULL;
+}
+
+struct simple_track *simple_list_get_next(struct list_head *head, struct simple_track *cur,
+               int (*filter_callback)(const struct simple_track *))
+{
+       struct list_head *item;
+
+       if (cur == NULL)
+               return to_simple_track(head->next);
+
+       item = cur->node.next;
+again:
+       while (item != head) {
+               struct simple_track *track = to_simple_track(item);
+
+               if (filter_callback(track))
+                       return track;
+               item = item->next;
+       }
+       item = head->next;
+       if (repeat)
+               goto again;
+       return NULL;
+}
+
+struct simple_track *simple_list_get_prev(struct list_head *head, struct simple_track *cur,
+               int (*filter_callback)(const struct simple_track *))
+{
+       struct list_head *item;
+
+       if (cur == NULL)
+               return to_simple_track(head->next);
+
+       item = cur->node.prev;
+again:
+       while (item != head) {
+               struct simple_track *track = to_simple_track(item);
+
+               if (filter_callback(track))
+                       return track;
+               item = item->prev;
+       }
+       item = head->prev;
+       if (repeat)
+               goto again;
+       return NULL;
+}
+
+void sorted_list_add_track(struct list_head *head, struct rb_root *tree_root, struct simple_track *track,
+               const sort_key_t *keys, int tiebreak)
+{
+       struct rb_node **new = &(tree_root->rb_node), *parent = NULL, *curr, *next;
+       struct list_head *node;
+       int result = 0;
+
+       /* try to locate track in tree */
+       while (*new) {
+               const struct simple_track *t = tree_node_to_simple_track(*new);
+               result = track_info_cmp(track->info, t->info, keys);
+
+               parent = *new;
+               if (result < 0)
+                       new = &(parent->rb_left);
+               else if (result > 0)
+                       new = &(parent->rb_right);
+               else
+                       break;
+       }
+
+       /* duplicate is present in the tree */
+       if (parent && result == 0) {
+               if (tiebreak < 0) {
+                       node = &(tree_node_to_simple_track(parent)->node);
+                       rb_replace_node(parent, &track->tree_node, tree_root);
+                       RB_CLEAR_NODE(parent);
+               } else {
+                       next = rb_next(parent);
+                       node = next ? &(tree_node_to_simple_track(next)->node) : head;
+               }
+       } else {
+               rb_link_node(&track->tree_node, parent, new);
+               curr = *new;
+               rb_insert_color(&track->tree_node, tree_root);
+               if (result < 0) {
+                       node = &(tree_node_to_simple_track(parent)->node);
+               } else if (result > 0) {
+                       next = rb_next(curr);
+                       node = next ? &(tree_node_to_simple_track(next)->node) : head;
+               } else {
+                       /* rbtree was empty, just add after list head */
+                       node = head;
+               }
+       }
+       list_add(&track->node, node->prev);
+}
+
+void sorted_list_remove_track(struct list_head *head, struct rb_root *tree_root, struct simple_track *track)
+{
+       struct simple_track *next_track;
+       struct rb_node *tree_next;
+
+       if (!RB_EMPTY_NODE(&track->tree_node)) {
+               next_track = (track->node.next != head) ? to_simple_track(track->node.next) : NULL;
+               tree_next = rb_next(&track->tree_node);
+
+               if (next_track && (!tree_next || tree_node_to_simple_track(tree_next) != next_track)) {
+                       rb_replace_node(&track->tree_node, &next_track->tree_node, tree_root);
+                       RB_CLEAR_NODE(&track->tree_node);
+               } else
+                       rb_erase(&track->tree_node, tree_root);
+       }
+       list_del(&track->node);
+}
+
+void rand_list_rebuild(struct list_head *head, struct rb_root *tree_root)
+{
+       struct list_head *item, *tmp;
+       struct rb_root tmp_tree = RB_ROOT;
+       struct simple_track **track_array;
+       static const sort_key_t empty_sort_keys[] = { SORT_INVALID };
+
+       unsigned int len = 0, track_cnt = 0;
+
+       list_for_each(item, head) {
+               len++;
+       }
+       track_array = xmalloc(len * sizeof(track_array[0]));
+
+       LIST_HEAD(tmp_head);
+       list_for_each_safe(item, tmp, head) {
+               struct simple_track *track = to_simple_track(item);
+               sorted_list_remove_track(head, tree_root, track);
+               track_array[track_cnt] = track;
+               track_cnt++;
+       }
+       shuffle_array(track_array, len, sizeof(track_array[0]));
+       for (unsigned int i=0; i<len; i++) {
+               sorted_list_add_track(&tmp_head, &tmp_tree, track_array[i], empty_sort_keys, 0);
+       }
+       free(track_array);
+
+       tree_root->rb_node = tmp_tree.rb_node;
+       _list_add(head, tmp_head.prev, tmp_head.next);
+}
+
+void sorted_list_rebuild(struct list_head *head, struct rb_root *tree_root, const sort_key_t *keys)
+{
+       struct list_head *item, *tmp;
+       struct rb_root tmp_tree = RB_ROOT;
+       LIST_HEAD(tmp_head);
+
+       list_for_each_safe(item, tmp, head) {
+               struct simple_track *track = to_simple_track(item);
+               sorted_list_remove_track(head, tree_root, track);
+               sorted_list_add_track(&tmp_head, &tmp_tree, track, keys, 0);
+       }
+       tree_root->rb_node = tmp_tree.rb_node;
+       _list_add(head, tmp_head.prev, tmp_head.next);
+}
+
+static int compare_rand(const struct rb_node *a, const struct rb_node *b)
+{
+       struct shuffle_track *tr_a = tree_node_to_shuffle_track(a);
+       struct shuffle_track *tr_b = tree_node_to_shuffle_track(b);
+
+       if (tr_a->rand < tr_b->rand)
+               return -1;
+       if (tr_a->rand > tr_b->rand)
+               return +1;
+
+       return 0;
+}
+
+static void shuffle_track_init(struct shuffle_track *track)
+{
+       track->rand = rand() / ((double) RAND_MAX + 1);
+}
+
+void shuffle_list_add(struct shuffle_track *track, struct rb_root *tree_root)
+{
+       struct rb_node **new = &(tree_root->rb_node), *parent = NULL;
+
+       shuffle_track_init(track);
+
+       /* try to locate track in tree */
+       while (*new) {
+               int result = compare_rand(&track->tree_node, *new);
+
+               parent = *new;
+               if (result < 0)
+                       new = &((*new)->rb_left);
+               else if (result > 0)
+                       new = &((*new)->rb_right);
+               else {
+                       /* very unlikely, try again! */
+                       shuffle_list_add(track, tree_root);
+                       return;
+               }
+       }
+
+       rb_link_node(&track->tree_node, parent, new);
+       rb_insert_color(&track->tree_node, tree_root);
+}
+
+void shuffle_list_reshuffle(struct rb_root *tree_root)
+{
+       struct rb_node *node, *tmp;
+       struct rb_root tmptree = RB_ROOT;
+
+       rb_for_each_safe(node, tmp, tree_root) {
+               struct shuffle_track *track = tree_node_to_shuffle_track(node);
+               rb_erase(node, tree_root);
+               shuffle_list_add(track, &tmptree);
+       }
+
+       tree_root->rb_node = tmptree.rb_node;
+}
+
+/* expensive */
+void list_add_rand(struct list_head *head, struct list_head *node, int nr)
+{
+       struct list_head *item;
+       int pos;
+
+       pos = rand() % (nr + 1);
+       item = head;
+       if (pos <= nr / 2) {
+               while (pos) {
+                       item = item->next;
+                       pos--;
+               }
+               /* add after item */
+               list_add(node, item);
+       } else {
+               pos = nr - pos;
+               while (pos) {
+                       item = item->prev;
+                       pos--;
+               }
+               /* add before item */
+               list_add_tail(node, item);
+       }
+}
+
+int simple_list_for_each_marked(struct list_head *head, track_info_cb cb,
+               void *data, int reverse)
+{
+       struct simple_track *t;
+       int rc = 0;
+
+       if (reverse) {
+               list_for_each_entry_reverse(t, head, node) {
+                       if (t->marked) {
+                               rc = cb(data, t->info);
+                               if (rc)
+                                       break;
+                       }
+               }
+       } else {
+               list_for_each_entry(t, head, node) {
+                       if (t->marked) {
+                               rc = cb(data, t->info);
+                               if (rc)
+                                       break;
+                       }
+               }
+       }
+       return rc;
+}
+
+int simple_list_for_each(struct list_head *head, track_info_cb cb, void *data,
+               int reverse)
+{
+       struct simple_track *t;
+       int rc = 0;
+
+       if (reverse) {
+               list_for_each_entry_reverse(t, head, node) {
+                       if ((rc = cb(data, t->info)))
+                               break;
+               }
+       } else {
+               list_for_each_entry(t, head, node) {
+                       if ((rc = cb(data, t->info)))
+                               break;
+               }
+       }
+
+       return rc;
+}
diff --git a/track.h b/track.h
new file mode 100644 (file)
index 0000000..aa84ef9
--- /dev/null
+++ b/track.h
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_TRACK_H
+#define CMUS_TRACK_H
+
+#include "list.h"
+#include "rbtree.h"
+#include "iter.h"
+#include "track_info.h"
+#include "cmus.h"
+
+struct simple_track {
+       struct list_head node;
+       struct rb_node tree_node;
+       struct track_info *info;
+       unsigned int marked : 1;
+};
+
+struct shuffle_track {
+       struct simple_track simple_track;
+       struct rb_node tree_node;
+       double rand;
+};
+
+static inline struct shuffle_track *
+simple_track_to_shuffle_track(struct simple_track *track)
+{
+       return container_of(track, struct shuffle_track, simple_track);
+}
+
+static inline struct track_info *shuffle_track_info(const struct shuffle_track *track)
+{
+       return ((struct simple_track *)track)->info;
+}
+
+static inline struct simple_track *to_simple_track(const struct list_head *item)
+{
+       return container_of(item, struct simple_track, node);
+}
+
+static inline struct simple_track *iter_to_simple_track(const struct iter *iter)
+{
+       return iter->data1;
+}
+
+static inline struct simple_track *tree_node_to_simple_track(const struct rb_node *node)
+{
+       return container_of(node, struct simple_track, tree_node);
+}
+
+static inline struct shuffle_track *tree_node_to_shuffle_track(const struct rb_node *node)
+{
+       return container_of(node, struct shuffle_track, tree_node);
+}
+
+/* NOTE: does not ref ti */
+void simple_track_init(struct simple_track *track, struct track_info *ti);
+
+/* refs ti */
+struct simple_track *simple_track_new(struct track_info *ti);
+
+int simple_track_get_prev(struct iter *);
+int simple_track_get_next(struct iter *);
+
+/* data is window */
+int simple_track_search_get_current(void *data, struct iter *iter);
+int simple_track_search_matches(void *data, struct iter *iter, const char *text);
+int _simple_track_search_matches(struct iter *iter, const char *text);
+
+struct shuffle_track *shuffle_list_get_next(struct rb_root *root, struct shuffle_track *cur,
+               int (*filter)(const struct simple_track *));
+
+struct shuffle_track *shuffle_list_get_prev(struct rb_root *root, struct shuffle_track *cur,
+               int (*filter)(const struct simple_track *));
+
+struct simple_track *simple_list_get_next(struct list_head *head, struct simple_track *cur,
+               int (*filter)(const struct simple_track *));
+
+struct simple_track *simple_list_get_prev(struct list_head *head, struct simple_track *cur,
+               int (*filter)(const struct simple_track *));
+
+void sorted_list_add_track(struct list_head *head, struct rb_root *tree_root, struct simple_track *track,
+               const sort_key_t *keys, int tiebreak);
+void sorted_list_remove_track(struct list_head *head, struct rb_root *tree_root, struct simple_track *track);
+void sorted_list_rebuild(struct list_head *head, struct rb_root *tree_root, const sort_key_t *keys);
+void rand_list_rebuild(struct list_head *head, struct rb_root *tree_root);
+
+void list_add_rand(struct list_head *head, struct list_head *node, int nr);
+
+int simple_list_for_each_marked(struct list_head *head, track_info_cb cb,
+               void *data, int reverse);
+int simple_list_for_each(struct list_head *head, track_info_cb cb,
+               void *data, int reverse);
+
+void shuffle_list_add(struct shuffle_track *track, struct rb_root *tree_root);
+void shuffle_list_reshuffle(struct rb_root *tree_root);
+void shuffle_insert(struct rb_root *root, struct shuffle_track *previous, struct shuffle_track *new);
+
+#endif
diff --git a/track_info.c b/track_info.c
new file mode 100644 (file)
index 0000000..da3d3b4
--- /dev/null
@@ -0,0 +1,397 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "track_info.h"
+#include "comment.h"
+#include "uchar.h"
+#include "u_collate.h"
+#include "misc.h"
+#include "xmalloc.h"
+#include "utils.h"
+#include "debug.h"
+#include "path.h"
+#include "ui_curses.h"
+
+#include <string.h>
+#include <stdatomic.h>
+#include <math.h>
+
+struct track_info_priv {
+       struct track_info ti;
+       _Atomic uint32_t ref_count;
+};
+
+static struct track_info_priv *track_info_to_priv(struct track_info *ti)
+{
+       return container_of(ti, struct track_info_priv, ti);
+}
+
+struct track_info *track_info_new(const char *filename)
+{
+       static _Atomic uint64_t cur_uid = ATOMIC_VAR_INIT(1);
+       uint64_t uid = atomic_fetch_add_explicit(&cur_uid, 1, memory_order_relaxed);
+       BUG_ON(uid == 0);
+
+       struct track_info_priv *priv = xnew(struct track_info_priv, 1);
+       atomic_init(&priv->ref_count, 1);
+
+       struct track_info *ti = &priv->ti;
+       ti->uid = uid;
+       ti->filename = xstrdup(filename);
+       ti->play_count = 0;
+       ti->comments = NULL;
+       ti->bpm = -1;
+       ti->codec = NULL;
+       ti->codec_profile = NULL;
+
+       return ti;
+}
+
+void track_info_set_comments(struct track_info *ti, struct keyval *comments) {
+       ti->comments = comments;
+       ti->artist = keyvals_get_val(comments, "artist");
+       ti->album = keyvals_get_val(comments, "album");
+       ti->title = keyvals_get_val(comments, "title");
+       ti->tracknumber = comments_get_int(comments, "tracknumber");
+       ti->discnumber = comments_get_int(comments, "discnumber");
+       ti->date = comments_get_date(comments, "date");
+       ti->originaldate = comments_get_date(comments, "originaldate");
+       ti->genre = keyvals_get_val(comments, "genre");
+       ti->comment = keyvals_get_val(comments, "comment");
+       ti->albumartist = comments_get_albumartist(comments);
+       ti->artistsort = comments_get_artistsort(comments);
+       ti->albumsort = keyvals_get_val(comments, "albumsort");
+       ti->is_va_compilation = track_is_va_compilation(comments);
+       ti->media = keyvals_get_val(comments, "media");
+
+       int bpm = comments_get_int(comments, "bpm");
+       if (ti->bpm == 0 || ti->bpm == -1) {
+               ti->bpm = bpm;
+       }
+
+       if (ti->artist == NULL && ti->albumartist != NULL) {
+               /* best guess */
+               ti->artist = ti->albumartist;
+       }
+
+       if (track_info_has_tag(ti) && ti->title == NULL) {
+               /* best guess */
+               ti->title = path_basename(ti->filename);
+       }
+
+       ti->rg_track_gain = comments_get_double(comments, "replaygain_track_gain");
+       ti->rg_track_peak = comments_get_double(comments, "replaygain_track_peak");
+       ti->rg_album_gain = comments_get_double(comments, "replaygain_album_gain");
+       ti->rg_album_peak = comments_get_double(comments, "replaygain_album_peak");
+
+       ti->collkey_artist = u_strcasecoll_key0(ti->artist);
+       ti->collkey_album = u_strcasecoll_key0(ti->album);
+       ti->collkey_title = u_strcasecoll_key0(ti->title);
+       ti->collkey_genre = u_strcasecoll_key0(ti->genre);
+       ti->collkey_comment = u_strcasecoll_key0(ti->comment);
+       ti->collkey_albumartist = u_strcasecoll_key0(ti->albumartist);
+}
+
+void track_info_ref(struct track_info *ti)
+{
+       struct track_info_priv *priv = track_info_to_priv(ti);
+       atomic_fetch_add_explicit(&priv->ref_count, 1, memory_order_relaxed);
+}
+
+void track_info_unref(struct track_info *ti)
+{
+       struct track_info_priv *priv = track_info_to_priv(ti);
+       uint32_t prev = atomic_fetch_sub_explicit(&priv->ref_count, 1,
+                       memory_order_acq_rel);
+       if (prev == 1) {
+               keyvals_free(ti->comments);
+               free(ti->filename);
+               free(ti->codec);
+               free(ti->codec_profile);
+               free(ti->collkey_artist);
+               free(ti->collkey_album);
+               free(ti->collkey_title);
+               free(ti->collkey_genre);
+               free(ti->collkey_comment);
+               free(ti->collkey_albumartist);
+               free(priv);
+       }
+}
+
+bool track_info_unique_ref(struct track_info *ti)
+{
+       struct track_info_priv *priv = track_info_to_priv(ti);
+       return atomic_load_explicit(&priv->ref_count, memory_order_relaxed) == 1;
+}
+
+int track_info_has_tag(const struct track_info *ti)
+{
+       return ti->artist || ti->album || ti->title;
+}
+
+static inline int match_word(const struct track_info *ti, const char *word, unsigned int flags)
+{
+       return ((flags & TI_MATCH_ARTIST) && ti->artist && u_strcasestr_base(ti->artist, word)) ||
+              ((flags & TI_MATCH_ALBUM) && ti->album && u_strcasestr_base(ti->album, word)) ||
+              ((flags & TI_MATCH_TITLE) && ti->title && u_strcasestr_base(ti->title, word)) ||
+              ((flags & TI_MATCH_ALBUMARTIST) && ti->albumartist && u_strcasestr_base(ti->albumartist, word));
+}
+
+static inline int flags_set(const struct track_info *ti, unsigned int flags)
+{
+       return ((flags & TI_MATCH_ARTIST) && ti->artist) ||
+              ((flags & TI_MATCH_ALBUM) && ti->album) ||
+              ((flags & TI_MATCH_TITLE) && ti->title) ||
+              ((flags & TI_MATCH_ALBUMARTIST) && ti->albumartist);
+}
+
+int track_info_matches_full(const struct track_info *ti, const char *text,
+               unsigned int flags, unsigned int exclude_flags, int match_all_words)
+{
+       char **words;
+       int i, matched = 0;
+
+       words = get_words(text);
+       for (i = 0; words[i]; i++) {
+               const char *word = words[i];
+
+               matched = 0;
+               if (flags_set(ti, flags) && match_word(ti, word, flags)) {
+                       matched = 1;
+               } else {
+                       /* compare with url or filename without path */
+                       const char *filename = ti->filename;
+
+                       if (!is_url(filename))
+                               filename = path_basename(filename);
+
+                       if (u_strcasestr_filename(filename, word))
+                               matched = 1;
+               }
+
+               if (flags_set(ti, exclude_flags) && match_word(ti, word, exclude_flags))
+                       matched = 0;
+
+               if (match_all_words ? !matched : matched)
+                       break;
+
+       }
+       free_str_array(words);
+       return matched;
+}
+
+int track_info_matches(const struct track_info *ti, const char *text, unsigned int flags)
+{
+       return track_info_matches_full(ti, text, flags, 0, 1);
+}
+
+static int doublecmp0(double a, double b)
+{
+       double x;
+       /* fast check for NaN */
+       int r = (b != b) - (a != a);
+       if (r)
+               return r;
+       x = a - b;
+       return (x > 0) - (x < 0);
+}
+
+/* this function gets called *alot*, it must be very fast */
+int track_info_cmp(const struct track_info *a, const struct track_info *b, const sort_key_t *keys)
+{
+       int i, rev = 0, res = 0;
+
+       for (i = 0; keys[i] != SORT_INVALID; i++) {
+               sort_key_t key = keys[i];
+               const char *av, *bv;
+
+               rev = 0;
+               if (key >= REV_SORT__START) {
+                       rev = 1;
+                       key -= REV_SORT__START;
+               }
+
+               switch (key) {
+               case SORT_TRACKNUMBER:
+               case SORT_DISCNUMBER:
+               case SORT_DATE:
+               case SORT_ORIGINALDATE:
+               case SORT_PLAY_COUNT:
+               case SORT_BPM:
+                       res = getentry(a, key, int) - getentry(b, key, int);
+                       break;
+               case SORT_FILEMTIME:
+                       res = a->mtime - b->mtime;
+                       break;
+               case SORT_FILENAME:
+                       /* NOTE: filenames are not necessarily UTF-8 */
+                       res = strcoll(a->filename, b->filename);
+                       break;
+               case SORT_RG_TRACK_GAIN:
+               case SORT_RG_TRACK_PEAK:
+               case SORT_RG_ALBUM_GAIN:
+               case SORT_RG_ALBUM_PEAK:
+                       res = doublecmp0(getentry(a, key, double), getentry(b, key, double));
+                       break;
+               case SORT_BITRATE:
+                       res = getentry(a, key, long) - getentry(b, key, long);
+                       break;
+               default:
+                       av = getentry(a, key, const char *);
+                       bv = getentry(b, key, const char *);
+                       res = strcmp0(av, bv);
+                       break;
+               }
+
+               if (res)
+                       break;
+       }
+       return rev ? -res : res;
+}
+
+static const struct {
+       const char *str;
+       sort_key_t key;
+} sort_key_map[] = {
+       { "artist",             SORT_ARTIST             },
+       { "album",              SORT_ALBUM              },
+       { "title",              SORT_TITLE              },
+       { "play_count",         SORT_PLAY_COUNT         },
+       { "tracknumber",        SORT_TRACKNUMBER        },
+       { "discnumber",         SORT_DISCNUMBER         },
+       { "date",               SORT_DATE               },
+       { "originaldate",       SORT_ORIGINALDATE       },
+       { "genre",              SORT_GENRE              },
+       { "comment",            SORT_COMMENT            },
+       { "albumartist",        SORT_ALBUMARTIST        },
+       { "filename",           SORT_FILENAME           },
+       { "filemtime",          SORT_FILEMTIME          },
+       { "rg_track_gain",      SORT_RG_TRACK_GAIN      },
+       { "rg_track_peak",      SORT_RG_TRACK_PEAK      },
+       { "rg_album_gain",      SORT_RG_ALBUM_GAIN      },
+       { "rg_album_peak",      SORT_RG_ALBUM_PEAK      },
+       { "bitrate",            SORT_BITRATE            },
+       { "codec",              SORT_CODEC              },
+       { "codec_profile",      SORT_CODEC_PROFILE      },
+       { "media",              SORT_MEDIA              },
+       { "bpm",                SORT_BPM                },
+       { "-artist",            REV_SORT_ARTIST         },
+       { "-album",             REV_SORT_ALBUM          },
+       { "-title",             REV_SORT_TITLE          },
+       { "-play_count",        REV_SORT_PLAY_COUNT     },
+       { "-tracknumber",       REV_SORT_TRACKNUMBER    },
+       { "-discnumber",        REV_SORT_DISCNUMBER     },
+       { "-date",              REV_SORT_DATE           },
+       { "-originaldate",      REV_SORT_ORIGINALDATE   },
+       { "-genre",             REV_SORT_GENRE          },
+       { "-comment",           REV_SORT_COMMENT        },
+       { "-albumartist",       REV_SORT_ALBUMARTIST    },
+       { "-filename",          REV_SORT_FILENAME       },
+       { "-filemtime",         REV_SORT_FILEMTIME      },
+       { "-rg_track_gain",     REV_SORT_RG_TRACK_GAIN  },
+       { "-rg_track_peak",     REV_SORT_RG_TRACK_PEAK  },
+       { "-rg_album_gain",     REV_SORT_RG_ALBUM_GAIN  },
+       { "-rg_album_peak",     REV_SORT_RG_ALBUM_PEAK  },
+       { "-bitrate",           REV_SORT_BITRATE        },
+       { "-codec",             REV_SORT_CODEC          },
+       { "-codec_profile",     REV_SORT_CODEC_PROFILE  },
+       { "-media",             REV_SORT_MEDIA          },
+       { "-bpm",               REV_SORT_BPM            },
+       { NULL,                 SORT_INVALID            }
+};
+
+sort_key_t *parse_sort_keys(const char *value)
+{
+       sort_key_t *keys;
+       const char *s, *e;
+       int size = 4;
+       int pos = 0;
+
+       keys = xnew(sort_key_t, size);
+
+       s = value;
+       while (1) {
+               char buf[32];
+               int i, len;
+
+               while (*s == ' ')
+                       s++;
+
+               e = s;
+               while (*e && *e != ' ')
+                       e++;
+
+               len = e - s;
+               if (len == 0)
+                       break;
+               if (len > 31)
+                       len = 31;
+
+               memcpy(buf, s, len);
+               buf[len] = 0;
+               s = e;
+
+               for (i = 0; ; i++) {
+                       if (sort_key_map[i].str == NULL) {
+                               error_msg("invalid sort key '%s'", buf);
+                               free(keys);
+                               return NULL;
+                       }
+
+                       if (strcmp(buf, sort_key_map[i].str) == 0)
+                               break;
+               }
+               if (pos == size - 1) {
+                       size *= 2;
+                       keys = xrenew(sort_key_t, keys, size);
+               }
+               keys[pos++] = sort_key_map[i].key;
+       }
+       keys[pos] = SORT_INVALID;
+       return keys;
+}
+
+const char *sort_key_to_str(sort_key_t key)
+{
+       int i;
+       for (i = 0; sort_key_map[i].str; i++) {
+               if (sort_key_map[i].key == key)
+                       return sort_key_map[i].str;
+       }
+       return NULL;
+}
+
+void sort_keys_to_str(const sort_key_t *keys, char *buf, size_t bufsize)
+{
+       int i, pos = 0;
+
+       for (i = 0; keys[i] != SORT_INVALID; i++) {
+               const char *key = sort_key_to_str(keys[i]);
+               int len = strlen(key);
+
+               if ((int)bufsize - pos - len - 2 < 0)
+                       break;
+
+               memcpy(buf + pos, key, len);
+               pos += len;
+               buf[pos++] = ' ';
+       }
+       if (pos > 0)
+               pos--;
+       buf[pos] = 0;
+}
diff --git a/track_info.h b/track_info.h
new file mode 100644 (file)
index 0000000..4eabe1e
--- /dev/null
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_TRACK_INFO_H
+#define CMUS_TRACK_INFO_H
+
+#include <time.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+struct track_info {
+       uint64_t uid;
+       struct keyval *comments;
+
+       // next track_info in the hash table (cache.c)
+       struct track_info *next;
+
+       time_t mtime;
+       int duration;
+       long bitrate;
+       char *codec;
+       char *codec_profile;
+       char *filename;
+
+       int tracknumber;
+       int discnumber;
+       int date;
+       int originaldate;
+       double rg_track_gain;
+       double rg_track_peak;
+       double rg_album_gain;
+       double rg_album_peak;
+       const char *artist;
+       const char *album;
+       const char *title;
+       const char *genre;
+       const char *comment;
+       const char *albumartist;
+       const char *artistsort;
+       const char *albumsort;
+       const char *media;
+
+       char *collkey_artist;
+       char *collkey_album;
+       char *collkey_title;
+       char *collkey_genre;
+       char *collkey_comment;
+       char *collkey_albumartist;
+
+       unsigned int play_count;
+
+       int is_va_compilation : 1;
+       int bpm;
+};
+
+typedef size_t sort_key_t;
+
+#define SORT_INVALID            ((sort_key_t) (-1))
+#define SORT_ARTIST            offsetof(struct track_info, collkey_artist)
+#define SORT_ALBUM             offsetof(struct track_info, collkey_album)
+#define SORT_TITLE             offsetof(struct track_info, collkey_title)
+#define SORT_TRACKNUMBER       offsetof(struct track_info, tracknumber)
+#define SORT_DISCNUMBER        offsetof(struct track_info, discnumber)
+#define SORT_DATE              offsetof(struct track_info, date)
+#define SORT_ORIGINALDATE      offsetof(struct track_info, originaldate)
+#define SORT_RG_TRACK_GAIN     offsetof(struct track_info, rg_track_gain)
+#define SORT_RG_TRACK_PEAK     offsetof(struct track_info, rg_track_peak)
+#define SORT_RG_ALBUM_GAIN     offsetof(struct track_info, rg_album_gain)
+#define SORT_RG_ALBUM_PEAK     offsetof(struct track_info, rg_album_peak)
+#define SORT_GENRE             offsetof(struct track_info, collkey_genre)
+#define SORT_COMMENT           offsetof(struct track_info, collkey_comment)
+#define SORT_ALBUMARTIST       offsetof(struct track_info, collkey_albumartist)
+#define SORT_PLAY_COUNT        offsetof(struct track_info, play_count)
+#define SORT_FILENAME          offsetof(struct track_info, filename)
+#define SORT_FILEMTIME         offsetof(struct track_info, mtime)
+#define SORT_BITRATE           offsetof(struct track_info, bitrate)
+#define SORT_CODEC             offsetof(struct track_info, codec)
+#define SORT_CODEC_PROFILE     offsetof(struct track_info, codec_profile)
+#define SORT_MEDIA             offsetof(struct track_info, media)
+#define SORT_BPM               offsetof(struct track_info, bpm)
+#define REV_SORT__START                sizeof(struct track_info)
+#define REV_SORT_ARTIST                (REV_SORT__START + offsetof(struct track_info, collkey_artist))
+#define REV_SORT_ALBUM          (REV_SORT__START + offsetof(struct track_info, collkey_album))
+#define REV_SORT_TITLE          (REV_SORT__START + offsetof(struct track_info, collkey_title))
+#define REV_SORT_PLAY_COUNT    (REV_SORT__START + offsetof(struct track_info, play_count))
+#define REV_SORT_TRACKNUMBER    (REV_SORT__START + offsetof(struct track_info, tracknumber))
+#define REV_SORT_DISCNUMBER     (REV_SORT__START + offsetof(struct track_info, discnumber))
+#define REV_SORT_DATE           (REV_SORT__START + offsetof(struct track_info, date))
+#define REV_SORT_ORIGINALDATE   (REV_SORT__START + offsetof(struct track_info, originaldate))
+#define REV_SORT_RG_TRACK_GAIN  (REV_SORT__START + offsetof(struct track_info, rg_track_gain))
+#define REV_SORT_RG_TRACK_PEAK  (REV_SORT__START + offsetof(struct track_info, rg_track_peak))
+#define REV_SORT_RG_ALBUM_GAIN  (REV_SORT__START + offsetof(struct track_info, rg_album_gain))
+#define REV_SORT_RG_ALBUM_PEAK  (REV_SORT__START + offsetof(struct track_info, rg_album_peak))
+#define REV_SORT_GENRE          (REV_SORT__START + offsetof(struct track_info, collkey_genre))
+#define REV_SORT_COMMENT        (REV_SORT__START + offsetof(struct track_info, collkey_comment))
+#define REV_SORT_ALBUMARTIST    (REV_SORT__START + offsetof(struct track_info, collkey_albumartist))
+#define REV_SORT_FILENAME       (REV_SORT__START + offsetof(struct track_info, filename))
+#define REV_SORT_FILEMTIME      (REV_SORT__START + offsetof(struct track_info, mtime))
+#define REV_SORT_BITRATE        (REV_SORT__START + offsetof(struct track_info, bitrate))
+#define REV_SORT_CODEC          (REV_SORT__START + offsetof(struct track_info, codec))
+#define REV_SORT_CODEC_PROFILE  (REV_SORT__START + offsetof(struct track_info, codec_profile))
+#define REV_SORT_MEDIA          (REV_SORT__START + offsetof(struct track_info, media))
+#define REV_SORT_BPM            (REV_SORT__START + offsetof(struct track_info, bpm))
+
+#define TI_MATCH_ARTIST       (1 << 0)
+#define TI_MATCH_ALBUM        (1 << 1)
+#define TI_MATCH_TITLE        (1 << 2)
+#define TI_MATCH_ALBUMARTIST  (1 << 3)
+#define TI_MATCH_ALL          (~0)
+
+/* initializes only filename and ref */
+struct track_info *track_info_new(const char *filename);
+void track_info_set_comments(struct track_info *ti, struct keyval *comments);
+
+void track_info_ref(struct track_info *ti);
+void track_info_unref(struct track_info *ti);
+bool track_info_unique_ref(struct track_info *ti);
+
+/*
+ * returns: 1 if @ti has any of the following tags: artist, album, title
+ *          0 otherwise
+ */
+int track_info_has_tag(const struct track_info *ti);
+
+/*
+ * @flags  fields to search in (TI_MATCH_*)
+ *
+ * returns: 1 if all words in @text are found to match defined fields (@flags) in @ti
+ *          0 otherwise
+ */
+int track_info_matches(const struct track_info *ti, const char *text, unsigned int flags);
+
+/*
+ * @flags            fields to search in (TI_MATCH_*)
+ * @exclude_flags    fields which must not match (TI_MATCH_*)
+ * @match_all_words  if true, all words must be found in @ti
+ *
+ * returns: 1 if all/any words in @text are found to match defined fields (@flags) in @ti
+ *          0 otherwise
+ */
+int track_info_matches_full(const struct track_info *ti, const char *text, unsigned int flags,
+               unsigned int exclude_flags, int match_all_words);
+
+int track_info_cmp(const struct track_info *a, const struct track_info *b, const sort_key_t *keys);
+
+sort_key_t *parse_sort_keys(const char *value);
+const char *sort_key_to_str(sort_key_t key);
+void sort_keys_to_str(const sort_key_t *keys, char *buf, size_t bufsize);
+
+#endif
diff --git a/tree.c b/tree.c
new file mode 100644 (file)
index 0000000..7cad3e3
--- /dev/null
+++ b/tree.c
@@ -0,0 +1,1367 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "lib.h"
+#include "search_mode.h"
+#include "xmalloc.h"
+#include "utils.h"
+#include "debug.h"
+#include "mergesort.h"
+#include "options.h"
+#include "u_collate.h"
+#include "rbtree.h"
+
+#include <ctype.h>
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+
+struct searchable *tree_searchable;
+struct window *lib_tree_win;
+struct window *lib_track_win;
+struct window *lib_cur_win;
+struct rb_root lib_artist_root;
+
+struct track_iter {
+       struct rb_root *root;
+       struct tree_track *track;
+       struct album *album;
+};
+
+static inline int tree_album_selected(void)
+{
+       return iter_to_album(&lib_tree_win->sel) != NULL;
+}
+
+static inline int track_visible(struct tree_track *track)
+{
+       if (tree_album_selected())
+               return track->album == iter_to_album(&lib_tree_win->sel);
+       else if (show_all_tracks)
+               return track->album->artist == iter_to_artist(&lib_tree_win->sel);
+       else
+               return 0;
+}
+
+static inline void tree_search_track_to_iter(struct tree_track *track, struct iter *iter)
+{
+       iter->data0 = &lib_artist_root;
+       iter->data1 = track;
+       iter->data2 = NULL;
+}
+
+static inline void album_to_iter(struct album *album, struct iter *iter)
+{
+       iter->data0 = &lib_artist_root;
+       iter->data1 = album->artist;
+       iter->data2 = album;
+}
+
+static inline void album_to_track_iter(struct album *album, struct track_iter *iter)
+{
+       iter->root = &album->artist->album_root;
+       iter->album = album;
+       iter->track = (struct tree_track *)album;
+}
+
+static inline void artist_to_iter(struct artist *artist, struct iter *iter)
+{
+       iter->data0 = &lib_artist_root;
+       iter->data1 = artist;
+       iter->data2 = NULL;
+}
+
+static inline void tree_track_to_track_iter(struct tree_track *track, struct track_iter *iter)
+{
+       if (tree_album_selected()) {
+               iter->root = &track->album->track_root;
+               iter->album = NULL;
+       } else {
+               iter->root = &track->album->artist->album_root;
+               iter->album = track->album;
+       }
+       iter->track = track;
+}
+
+static void tree_set_expand_artist(struct artist *artist, int expand)
+{
+       struct iter sel;
+
+       if (!expand) {
+               /* deselect album, select artist */
+               artist_to_iter(artist, &sel);
+               window_set_sel(lib_tree_win, &sel);
+
+               if (!show_all_tracks)
+                       lib_cur_win = lib_tree_win;
+       }
+       artist->expanded = expand;
+       window_changed(lib_tree_win);
+}
+
+/* tree (search) iterators {{{ */
+static int tree_search_get_prev(struct iter *iter)
+{
+       struct rb_root *root = iter->data0;
+       struct tree_track *track = iter->data1;
+       struct artist *artist;
+       struct album *album;
+
+       BUG_ON(iter->data2);
+       if (root == NULL)
+               return 0;
+       if (track == NULL) {
+               /* head, get last track */
+               if (rb_root_empty(root)) {
+                       /* empty, iter points to the head already */
+                       return 0;
+               }
+               artist = to_artist(rb_last(root));
+               album = to_album(rb_last(&artist->album_root));
+               iter->data1 = to_tree_track(rb_last(&album->track_root));
+               return 1;
+       }
+       /* prev track */
+       if (rb_prev(&track->tree_node) == NULL || search_restricted) {
+               /* prev album */
+               if (rb_prev(&track->album->tree_node) == NULL) {
+                       /* prev artist */
+                       if (rb_prev(&track->album->artist->tree_node) == NULL)
+                               return 0;
+                       artist = to_artist(rb_prev(&track->album->artist->tree_node));
+                       album = to_album(rb_last(&artist->album_root));
+                       track = to_tree_track(rb_last(&album->track_root));
+               } else {
+                       album = to_album(rb_prev(&track->album->tree_node));
+                       track = to_tree_track(rb_last(&album->track_root));
+               }
+       } else {
+               track = to_tree_track(rb_prev(&track->tree_node));
+       }
+       iter->data1 = track;
+       return 1;
+}
+
+static int tree_search_get_next(struct iter *iter)
+{
+       struct rb_root *root = iter->data0;
+       struct tree_track *track = iter->data1;
+       struct artist *artist;
+       struct album *album;
+
+       BUG_ON(iter->data2);
+       if (root == NULL)
+               return 0;
+       if (track == NULL) {
+               /* head, get first track */
+               if (rb_root_empty(root)) {
+                       /* empty, iter points to the head already */
+                       return 0;
+               }
+               artist = to_artist(rb_first(root));
+               album = to_album(rb_first(&artist->album_root));
+               iter->data1 = to_tree_track(rb_first(&album->track_root));
+               return 1;
+       }
+       /* next track */
+       if (rb_next(&track->tree_node) == NULL || search_restricted) {
+               /* next album */
+               if (rb_next(&track->album->tree_node) == NULL) {
+                       /* next artist */
+                       if (rb_next(&track->album->artist->tree_node) == NULL)
+                               return 0;
+                       artist = to_artist(rb_next(&track->album->artist->tree_node));
+                       album = to_album(rb_first(&artist->album_root));
+                       track = to_tree_track(rb_first(&album->track_root));
+               } else {
+                       album = to_album(rb_next(&track->album->tree_node));
+                       track = to_tree_track(rb_first(&album->track_root));
+               }
+       } else {
+               track = to_tree_track(rb_next(&track->tree_node));
+       }
+       iter->data1 = track;
+       return 1;
+}
+/* }}} */
+
+/* tree window iterators {{{ */
+static int tree_get_prev(struct iter *iter)
+{
+       struct rb_root *root = iter->data0;
+       struct artist *artist = iter->data1;
+       struct album *album = iter->data2;
+
+       BUG_ON(root == NULL);
+       BUG_ON(artist == NULL && album != NULL);
+       if (artist == NULL) {
+               /* head, get last artist and/or album */
+               if (rb_root_empty(root)) {
+                       /* empty, iter points to the head already */
+                       return 0;
+               }
+               artist = to_artist(rb_last(root));
+               if (artist->expanded) {
+                       album = to_album(rb_last(&artist->album_root));
+               } else {
+                       album = NULL;
+               }
+               iter->data1 = artist;
+               iter->data2 = album;
+               return 1;
+       }
+       if (artist->expanded && album) {
+               /* prev album */
+               if (rb_prev(&album->tree_node) == NULL) {
+                       iter->data2 = NULL;
+                       return 1;
+               } else {
+                       iter->data2 = to_album(rb_prev(&album->tree_node));
+                       return 1;
+               }
+       }
+
+       /* prev artist */
+       if (rb_prev(&artist->tree_node) == NULL) {
+               iter->data1 = NULL;
+               iter->data2 = NULL;
+               return 0;
+       }
+       artist = to_artist(rb_prev(&artist->tree_node));
+       iter->data1 = artist;
+       iter->data2 = NULL;
+       if (artist->expanded) {
+               /* last album */
+               iter->data2 = to_album(rb_last(&artist->album_root));
+       }
+       return 1;
+}
+
+static int tree_get_next(struct iter *iter)
+{
+       struct rb_root *root = iter->data0;
+       struct artist *artist = iter->data1;
+       struct album *album = iter->data2;
+
+       BUG_ON(root == NULL);
+       BUG_ON(artist == NULL && album != NULL);
+       if (artist == NULL) {
+               /* head, get first artist */
+               if (rb_root_empty(root)) {
+                       /* empty, iter points to the head already */
+                       return 0;
+               }
+               iter->data1 = to_artist(rb_first(root));
+               iter->data2 = NULL;
+               return 1;
+       }
+       if (artist->expanded) {
+               /* next album */
+               if (album == NULL) {
+                       /* first album */
+                       iter->data2 = to_album(rb_first(&artist->album_root));
+                       return 1;
+               }
+               if (rb_next(&album->tree_node) != NULL) {
+                       iter->data2 = to_album(rb_next(&album->tree_node));
+                       return 1;
+               }
+       }
+
+       /* next artist */
+       if (rb_next(&artist->tree_node) == NULL) {
+               iter->data1 = NULL;
+               iter->data2 = NULL;
+               return 0;
+       }
+       iter->data1 = to_artist(rb_next(&artist->tree_node));
+       iter->data2 = NULL;
+       return 1;
+}
+/* }}} */
+
+static GENERIC_TREE_ITER_PREV(tree_track_get_prev_by_album, struct tree_track, tree_node)
+static GENERIC_TREE_ITER_NEXT(tree_track_get_next_by_album, struct tree_track, tree_node)
+
+/* track window iterators by artist */
+static int tree_track_get_prev_by_artist(struct iter *iter)
+{
+       struct track_iter *it = (struct track_iter *)iter;
+
+       /* get last album */
+       if (it->album == NULL) {
+               if (rb_root_empty(it->root))
+                       return 0;
+               it->album = to_album(rb_last(it->root));
+               it->track = NULL;
+               return tree_track_get_prev_by_artist(iter);
+       }
+
+       /* get previous album */
+       if (it->track == (struct tree_track *)it->album) {
+               /* no more albums */
+               if (rb_prev(&it->album->tree_node) == NULL)
+                       return 0;
+               it->album = to_album(rb_prev(&it->album->tree_node));
+               it->track = NULL;
+               return tree_track_get_prev_by_artist(iter);
+       }
+
+       /* get last track */
+       if (it->track == NULL) {
+               if (rb_root_empty(&it->album->track_root))
+                       it->track = (struct tree_track *)it->album;
+               else
+                       it->track = to_tree_track(rb_last(&it->album->track_root));
+               return 1;
+       }
+
+       /* get previous track */
+       if (rb_prev(&it->track->tree_node) != NULL) {
+               it->track = to_tree_track(rb_prev(&it->track->tree_node));
+               return 1;
+       }
+
+       /* no more tracks
+        * get album header */
+       it->track = (struct tree_track *)it->album;
+       return 1;
+}
+
+static int tree_track_get_next_by_artist(struct iter *iter)
+{
+       struct track_iter *it = (struct track_iter *)iter;
+
+       /* get first album */
+       if (it->album == NULL) {
+               if (rb_root_empty(it->root))
+                       return 0;
+               it->album = to_album(rb_first(it->root));
+               it->track = (struct tree_track *)it->album;
+               return 1;
+       }
+
+       /* get first track */
+       if (it->track == (struct tree_track *)it->album) {
+               /* this happens when the last track in an album is deleted but
+                * the album header is still referenced in lib_track_win->top
+                * set track = NULL to skip the next `if` without goto */
+               if (rb_root_empty(&it->album->track_root)) {
+                       it->track = NULL;
+               } else {
+                       it->track = to_tree_track(rb_first(&it->album->track_root));
+                       return 1;
+               }
+       }
+
+       /* get next track */
+       if (it->track != NULL && rb_next(&it->track->tree_node) != NULL) {
+               it->track = to_tree_track(rb_next(&it->track->tree_node));
+               return 1;
+       }
+
+       /* no more tracks
+        * get next album */
+       if (rb_next(&it->album->tree_node) != NULL) {
+               it->album = to_album(rb_next(&it->album->tree_node));
+               it->track = (struct tree_track *)it->album;
+               return 1;
+       }
+
+       /* no more albums */
+       return 0;
+}
+
+/* track window iterators */
+static int tree_track_get_prev(struct iter *iter)
+{
+       if (tree_album_selected())
+               return tree_track_get_prev_by_album(iter);
+       else
+               return tree_track_get_prev_by_artist(iter);
+}
+
+static int tree_track_get_next(struct iter *iter)
+{
+       if (tree_album_selected())
+               return tree_track_get_next_by_album(iter);
+       else
+               return tree_track_get_next_by_artist(iter);
+}
+
+/* search (tree) {{{ */
+static int tree_search_get_current(void *data, struct iter *iter)
+{
+       struct artist *artist;
+       struct album *album;
+       struct tree_track *track;
+       struct iter tmpiter;
+
+       if (rb_root_empty(&lib_artist_root))
+               return 0;
+       if (window_get_sel(lib_track_win, &tmpiter)) {
+               track = iter_to_tree_track(&tmpiter);
+               tree_search_track_to_iter(track, iter);
+               return 1;
+       }
+
+       /* artist not expanded. track_win is empty
+        * set tmp to the first track of the selected artist */
+       window_get_sel(lib_tree_win, &tmpiter);
+       artist = iter_to_artist(&tmpiter);
+       album = to_album(rb_first(&artist->album_root));
+       track = to_tree_track(rb_first(&album->track_root));
+       tree_search_track_to_iter(track, iter);
+       return 1;
+}
+
+static inline struct tree_track *iter_to_tree_search_track(const struct iter *iter)
+{
+       BUG_ON(iter->data0 != &lib_artist_root);
+       return iter->data1;
+}
+
+static int tree_search_matches(void *data, struct iter *iter, const char *text);
+
+static const struct searchable_ops tree_search_ops = {
+       .get_prev = tree_search_get_prev,
+       .get_next = tree_search_get_next,
+       .get_current = tree_search_get_current,
+       .matches = tree_search_matches
+};
+/* search (tree) }}} */
+
+static void tree_sel_changed(void)
+{
+       tree_sel_update(1);
+}
+
+void tree_sel_update(int changed)
+{
+       struct iter sel;
+       struct album *album;
+       struct artist *artist;
+
+       window_get_sel(lib_tree_win, &sel);
+       album = iter_to_album(&sel);
+       artist = show_all_tracks ? iter_to_artist(&sel) : NULL;
+
+       if (album != NULL) {
+               if (changed)
+                       window_set_contents(lib_track_win, &album->track_root);
+       } else if (artist != NULL) {
+               window_set_contents(lib_track_win, &artist->album_root);
+       } else {
+               if (lib_cur_win != lib_tree_win) {
+                       lib_cur_win = lib_tree_win;
+                       lib_tree_win->changed = 1;
+               }
+               window_set_empty(lib_track_win);
+       }
+}
+
+static inline void tree_win_get_selected(struct artist **artist, struct album **album)
+{
+       struct iter sel;
+
+       *artist = NULL;
+       *album = NULL;
+       if (window_get_sel(lib_tree_win, &sel)) {
+               *artist = iter_to_artist(&sel);
+               *album = iter_to_album(&sel);
+       }
+}
+
+static char *auto_artist_sort_name(const char *name)
+{
+       const char *name_orig = name;
+       char *buf;
+
+       if (strncasecmp(name, "the ", 4) != 0)
+               return NULL;
+
+       name += 4;
+       while (isspace((int)*name))
+               ++name;
+
+       if (*name == '\0')
+               return NULL;
+
+       buf = xnew(char, strlen(name_orig) + 2);
+       sprintf(buf, "%s, %c%c%c", name, name_orig[0],
+                                        name_orig[1],
+                                        name_orig[2]);
+       return buf;
+}
+
+static struct artist *artist_new(const char *name, const char *sort_name, int is_compilation)
+{
+       struct artist *a = xnew(struct artist, 1);
+
+       a->name = xstrdup(name);
+       a->sort_name = sort_name ? xstrdup(sort_name) : NULL;
+       a->auto_sort_name = auto_artist_sort_name(name);
+       a->collkey_name = u_strcasecoll_key(a->name);
+       a->collkey_sort_name = u_strcasecoll_key0(a->sort_name);
+       a->collkey_auto_sort_name = u_strcasecoll_key0(a->auto_sort_name);
+       a->expanded = 0;
+       a->is_compilation = is_compilation;
+       rb_root_init(&a->album_root);
+
+       return a;
+}
+
+static struct artist *artist_copy(const struct artist *artist)
+{
+       return artist_new(artist->name, artist->sort_name, artist->is_compilation);
+}
+
+static void artist_free(struct artist *artist)
+{
+       free(artist->name);
+       free(artist->sort_name);
+       free(artist->auto_sort_name);
+       free(artist->collkey_name);
+       free(artist->collkey_sort_name);
+       free(artist->collkey_auto_sort_name);
+       free(artist);
+}
+
+static struct album *album_new(struct artist *artist, const char *name,
+               const char *sort_name, int date)
+{
+       struct album *album = xnew(struct album, 1);
+
+       album->name = xstrdup(name);
+       album->sort_name = sort_name ? xstrdup(sort_name) : NULL;
+       album->collkey_name = u_strcasecoll_key(name);
+       album->collkey_sort_name = u_strcasecoll_key0(sort_name);
+       album->date = date;
+       album->min_date = date;
+       rb_root_init(&album->track_root);
+       album->artist = artist;
+
+       return album;
+}
+
+static void album_free(struct album *album)
+{
+       free(album->name);
+       free(album->sort_name);
+       free(album->collkey_name);
+       free(album->collkey_sort_name);
+       free(album);
+}
+
+static int track_selectable(struct iter *iter)
+{
+       struct track_iter *it = (struct track_iter *)iter;
+       return it->album != (struct album *)it->track;
+}
+
+void tree_init(void)
+{
+       struct iter iter;
+
+       rb_root_init(&lib_artist_root);
+
+       lib_tree_win = window_new(tree_get_prev, tree_get_next);
+       lib_track_win = window_new(tree_track_get_prev, tree_track_get_next);
+       lib_cur_win = lib_tree_win;
+
+       lib_tree_win->sel_changed = tree_sel_changed;
+       lib_track_win->selectable = track_selectable;
+
+       window_set_empty(lib_track_win);
+       window_set_contents(lib_tree_win, &lib_artist_root);
+
+       iter.data0 = &lib_artist_root;
+       iter.data1 = NULL;
+       iter.data2 = NULL;
+       tree_searchable = searchable_new(NULL, &iter, &tree_search_ops);
+}
+
+struct tree_track *tree_get_selected(void)
+{
+       struct artist *artist;
+       struct album *album;
+       struct tree_track *track;
+       struct iter sel;
+
+       if (rb_root_empty(&lib_artist_root))
+               return NULL;
+
+       tree_win_get_selected(&artist, &album);
+       if (album == NULL && !show_all_tracks) {
+               /* track window is empty
+                * => get first album of the selected artist and first track of that album
+                */
+               album = to_album(rb_first(&artist->album_root));
+               track = to_tree_track(rb_first(&album->track_root));
+       } else {
+               window_get_sel(lib_track_win, &sel);
+               track = iter_to_tree_track(&sel);
+       }
+
+       return track;
+}
+
+struct track_info *tree_activate_selected(void)
+{
+       struct track_info *info;
+
+       lib_cur_track = tree_get_selected();
+       if (!lib_cur_track)
+               return NULL;
+
+       lib_tree_win->changed = 1;
+       lib_track_win->changed = 1;
+
+       info = tree_track_info(lib_cur_track);
+       track_info_ref(info);
+       return info;
+}
+
+static int special_name_cmp(const char *a, const char *collkey_a,
+                              const char *b, const char *collkey_b)
+{
+       /* keep <Stream> etc. top */
+       int cmp = (*a != '<') - (*b != '<');
+
+       if (cmp)
+               return cmp;
+       return strcmp(collkey_a, collkey_b);
+}
+
+static inline const char *album_sort_collkey(const struct album *a)
+{
+        if (a->sort_name)
+                return a->collkey_sort_name;
+
+        return a->collkey_name;
+}
+
+static int special_album_cmp(const struct album *a, const struct album *b)
+{
+       return special_name_cmp(a->name, album_sort_collkey(a), b->name, album_sort_collkey(b));
+}
+
+static int special_album_cmp_date(const struct album *a, const struct album *b)
+{
+       /* keep <Stream> etc. top */
+       int cmp = (*a->name != '<') - (*b->name != '<');
+       if (cmp)
+               return cmp;
+
+       cmp = a->date - b->date;
+       if (cmp)
+               return cmp;
+
+       return strcmp(album_sort_collkey(a), album_sort_collkey(b));
+}
+
+/* has to follow the same logic as artist_sort_name() */
+static inline const char *artist_sort_collkey(const struct artist *a)
+{
+        if (a->sort_name)
+                return a->collkey_sort_name;
+
+        if (smart_artist_sort && a->auto_sort_name)
+                return a->collkey_auto_sort_name;
+
+        return a->collkey_name;
+}
+
+static struct artist *do_find_artist(const struct artist *artist,
+                                    struct rb_root *root,
+                                    struct rb_node ***p_new,
+                                    struct rb_node **p_parent)
+{
+       struct rb_node **new = &(root->rb_node), *parent = NULL;
+       const char *a = artist_sort_name(artist);
+       const char *collkey_a = artist_sort_collkey(artist);
+
+       while (*new) {
+               struct artist *cur_artist = to_artist(*new);
+               const char *b = artist_sort_name(cur_artist);
+               const char *collkey_b = artist_sort_collkey(cur_artist);
+               int result = special_name_cmp(a, collkey_a, b, collkey_b);
+
+               parent = *new;
+               if (result < 0)
+                       new = &((*new)->rb_left);
+               else if (result > 0)
+                       new = &((*new)->rb_right);
+               else
+                       return cur_artist;
+       }
+       if (p_new)
+               *p_new = new;
+       if (p_parent)
+               *p_parent = parent;
+       return NULL;
+}
+
+/* search (tree) {{{ */
+static struct artist *collapse_artist;
+
+static int tree_search_matches(void *data, struct iter *iter, const char *text)
+{
+       struct tree_track *track;
+       struct iter tmpiter;
+       unsigned int flags = TI_MATCH_ARTIST | TI_MATCH_ALBUM | TI_MATCH_ALBUMARTIST;
+
+       if (!search_restricted)
+               flags |= TI_MATCH_TITLE;
+       track = iter_to_tree_search_track(iter);
+       if (!track_info_matches(tree_track_info(track), text, flags))
+               return 0;
+
+       if (auto_expand_albums_search) {
+               /* collapse old search result */
+               if (collapse_artist) {
+                       struct artist *artist = do_find_artist(collapse_artist, &lib_artist_root, NULL, NULL);
+                       if (artist && artist != track->album->artist) {
+                               if (artist->expanded)
+                                       tree_set_expand_artist(artist, 0);
+                               artist_free(collapse_artist);
+                               collapse_artist = (!track->album->artist->expanded) ? artist_copy(track->album->artist) : NULL;
+                       }
+               } else if (!track->album->artist->expanded) {
+                       collapse_artist = artist_copy(track->album->artist);
+               }
+
+               track->album->artist->expanded = 1;
+               album_to_iter(track->album, &tmpiter);
+       } else {
+               artist_to_iter(track->album->artist, &tmpiter);
+       }
+
+       window_set_sel(lib_tree_win, &tmpiter);
+
+       tree_track_to_track_iter(track, (struct track_iter *)&tmpiter);
+       window_set_sel(lib_track_win, &tmpiter);
+
+       return 1;
+}
+/* search (tree) }}} */
+
+static void insert_artist(struct artist *artist, struct rb_root *root)
+{
+       struct rb_node **new = &(root->rb_node), *parent = NULL;
+       struct artist *found;
+
+       found = do_find_artist(artist, root, &new, &parent);
+       if (!found) {
+               rb_link_node(&artist->tree_node, parent, new);
+               rb_insert_color(&artist->tree_node, root);
+       }
+}
+
+static void add_artist(struct artist *artist)
+{
+       insert_artist(artist, &lib_artist_root);
+}
+
+static struct artist *find_artist(const struct artist *artist)
+{
+       return do_find_artist(artist, &lib_artist_root, NULL, NULL);
+}
+
+static struct album *do_find_album(const struct album *album,
+                                  int (*cmp)(const struct album *, const struct album *),
+                                  struct rb_node ***p_new,
+                                  struct rb_node **p_parent)
+{
+       struct rb_node **new = &(album->artist->album_root.rb_node), *parent = NULL;
+
+       while (*new) {
+               struct album *a = to_album(*new);
+
+               int result = cmp(album, a);
+
+               parent = *new;
+               if (result < 0)
+                       new = &((*new)->rb_left);
+               else if (result > 0)
+                       new = &((*new)->rb_right);
+               else
+                       return a;
+       }
+       if (p_new)
+               *p_new = new;
+       if (p_parent)
+               *p_parent = parent;
+       return NULL;
+}
+
+static struct album *find_album(const struct album *album)
+{
+       struct album *a;
+       struct rb_node *tmp;
+
+       /* do a linear search because we want find albums with different date */
+       rb_for_each_entry(a, tmp, &album->artist->album_root, tree_node) {
+               if (special_album_cmp(album, a) == 0)
+                       return a;
+       }
+       return NULL;
+}
+
+static void add_album(struct album *album)
+{
+       struct rb_node **new = &(album->artist->album_root.rb_node), *parent = NULL;
+       struct album *found;
+
+       /*
+        * Sort regular albums by date, but sort compilations
+        * alphabetically.
+        */
+       found = do_find_album(album,
+                             album->artist->is_compilation ? special_album_cmp
+                                                           : special_album_cmp_date,
+                             &new, &parent);
+       if (!found) {
+               rb_link_node(&album->tree_node, parent, new);
+               rb_insert_color(&album->tree_node, &album->artist->album_root);
+       }
+}
+
+static void album_add_track(struct album *album, struct tree_track *track)
+{
+       /*
+        * NOTE: This is not perfect.  You should ignore track numbers if
+        *       either is unset and use filename instead, but usually you
+        *       have all track numbers set or all unset (within one album
+        *       of course).
+        */
+       static const sort_key_t album_track_sort_keys[] = {
+               SORT_DISCNUMBER, SORT_TRACKNUMBER, SORT_FILENAME, SORT_INVALID
+       };
+       struct rb_node **new = &(album->track_root.rb_node), *parent = NULL;
+
+       track->album = album;
+       while (*new) {
+               const struct simple_track *a = (const struct simple_track *) track;
+               const struct simple_track *b = (const struct simple_track *) to_tree_track(*new);
+               int result = track_info_cmp(a->info, b->info, album_track_sort_keys);
+
+               parent = *new;
+               if (result < 0)
+                       new = &((*new)->rb_left);
+               else
+                       new = &((*new)->rb_right);
+       }
+
+       rb_link_node(&track->tree_node, parent, new);
+       rb_insert_color(&track->tree_node, &album->track_root);
+}
+
+static const char *tree_artist_name(const struct track_info* ti)
+{
+       const char *val = ti->albumartist;
+
+       if (ti->is_va_compilation)
+               val = "<Various Artists>";
+       if (!val || strcmp(val, "") == 0)
+               val = "<No Name>";
+
+       return val;
+}
+
+static const char *tree_album_name(const struct track_info* ti)
+{
+       const char *val = ti->album;
+
+       if (!val || strcmp(val, "") == 0)
+               val = "<No Name>";
+
+       return val;
+}
+
+static void remove_album(struct album *album)
+{
+       if (album->artist->expanded) {
+               struct iter iter;
+
+               album_to_iter(album, &iter);
+               window_row_vanishes(lib_tree_win, &iter);
+       }
+       rb_erase(&album->tree_node, &album->artist->album_root);
+}
+
+static void remove_artist(struct artist *artist)
+{
+       struct iter iter;
+
+       artist_to_iter(artist, &iter);
+       window_row_vanishes(lib_tree_win, &iter);
+       rb_erase(&artist->tree_node, &lib_artist_root);
+}
+
+void tree_add_track(struct tree_track *track)
+{
+       const struct track_info *ti = tree_track_info(track);
+       const char *album_name, *artist_name, *artistsort_name = NULL;
+       const char *albumsort_name = NULL;
+       struct artist *artist, *new_artist;
+       struct album *album, *new_album;
+       int date;
+       int is_va_compilation = 0;
+
+       date = ti->originaldate;
+       if (date < 0)
+               date = ti->date;
+
+       if (is_http_url(ti->filename)) {
+               artist_name = "<Stream>";
+               album_name = "<Stream>";
+       } else {
+               album_name      = tree_album_name(ti);
+               artist_name     = tree_artist_name(ti);
+               artistsort_name = ti->artistsort;
+               albumsort_name  = ti->albumsort;
+
+               is_va_compilation = ti->is_va_compilation;
+       }
+
+       new_artist = artist_new(artist_name, artistsort_name, is_va_compilation);
+       album = NULL;
+
+       artist = find_artist(new_artist);
+       if (artist) {
+               artist_free(new_artist);
+               new_album = album_new(artist, album_name, albumsort_name, date);
+               album = find_album(new_album);
+               if (album)
+                       album_free(new_album);
+       } else
+               new_album = album_new(new_artist, album_name, albumsort_name, date);
+
+       if (artist) {
+               int changed = 0;
+               /* If it makes sense to update sort_name, do it */
+               if (!artist->sort_name && artistsort_name) {
+                       artist->sort_name = xstrdup(artistsort_name);
+                       artist->collkey_sort_name = u_strcasecoll_key(artistsort_name);
+                       changed = 1;
+               }
+               /* If names differ, update */
+               if (!artist->auto_sort_name) {
+                       char *auto_sort_name = auto_artist_sort_name(artist_name);
+                       if (auto_sort_name) {
+                               free(artist->name);
+                               free(artist->collkey_name);
+                               artist->name = xstrdup(artist_name);
+                               artist->collkey_name = u_strcasecoll_key(artist_name);
+                               artist->auto_sort_name = auto_sort_name;
+                               artist->collkey_auto_sort_name = u_strcasecoll_key(auto_sort_name);
+                               changed = 1;
+                       }
+               }
+               if (changed) {
+                       remove_artist(artist);
+                       add_artist(artist);
+                       window_changed(lib_tree_win);
+               }
+       }
+
+       if (album) {
+               album_add_track(album, track);
+
+               /* If it makes sense to update album date, do it */
+               if (album->date < date) {
+                       album->date = date;
+
+                       remove_album(album);
+                       add_album(album);
+                       if (artist->expanded)
+                               window_changed(lib_tree_win);
+               }
+
+               if (album->min_date <= 0 || (album->min_date > date && date > 0)) {
+                       album->min_date = date;
+
+                       remove_album(album);
+                       add_album(album);
+                       if (artist->expanded)
+                               window_changed(lib_tree_win);
+               }
+
+       } else if (artist) {
+               add_album(new_album);
+               album_add_track(new_album, track);
+
+               if (artist->expanded)
+                       window_changed(lib_tree_win);
+       } else {
+               add_artist(new_artist);
+               add_album(new_album);
+               album_add_track(new_album, track);
+
+               window_changed(lib_tree_win);
+       }
+
+       if (track_visible(track))
+               window_changed(lib_track_win);
+}
+
+static void remove_sel_artist(struct artist *artist)
+{
+       struct rb_node *a_node, *a_tmp;
+
+       rb_for_each_safe(a_node, a_tmp, &artist->album_root) {
+               struct rb_node *t_node, *t_tmp;
+               struct album *album = to_album(a_node);
+
+               rb_for_each_safe(t_node, t_tmp, &album->track_root) {
+                       struct tree_track *track = to_tree_track(t_node);
+
+                       editable_remove_track(&lib_editable, (struct simple_track *)track);
+               }
+               /* all tracks removed => album removed
+                * if the last album was removed then the artist was removed too
+                */
+       }
+}
+
+static void remove_sel_album(struct album *album)
+{
+       struct rb_node *node, *tmp;
+
+       rb_for_each_safe(node, tmp, &album->track_root) {
+               struct tree_track *track = to_tree_track(node);
+
+               editable_remove_track(&lib_editable, (struct simple_track *)track);
+       }
+}
+
+static void tree_win_remove_sel(void)
+{
+       struct artist *artist;
+       struct album *album;
+
+       tree_win_get_selected(&artist, &album);
+       if (album) {
+               remove_sel_album(album);
+       } else if (artist) {
+               remove_sel_artist(artist);
+       }
+}
+
+static void track_win_remove_sel(void)
+{
+       struct iter sel;
+       struct tree_track *track;
+
+       if (window_get_sel(lib_track_win, &sel)) {
+               track = iter_to_tree_track(&sel);
+               BUG_ON(track == NULL);
+               editable_remove_track(&lib_editable, (struct simple_track *)track);
+       }
+}
+
+void tree_toggle_active_window(void)
+{
+       if (lib_cur_win == lib_tree_win) {
+               struct artist *artist;
+               struct album *album;
+
+               tree_win_get_selected(&artist, &album);
+               if (album || (artist && show_all_tracks)) {
+                       lib_cur_win = lib_track_win;
+                       lib_tree_win->changed = 1;
+                       lib_track_win->changed = 1;
+               }
+       } else if (lib_cur_win == lib_track_win) {
+               lib_cur_win = lib_tree_win;
+               lib_tree_win->changed = 1;
+               lib_track_win->changed = 1;
+       }
+}
+
+void tree_toggle_expand_artist(void)
+{
+       struct track_iter sel;
+       struct artist *artist;
+       struct album *album;
+       struct tree_track *track;
+
+       tree_win_get_selected(&artist, &album);
+       if (album != NULL && show_all_tracks) {
+               window_get_sel(lib_track_win, (struct iter *)&sel);
+               track = sel.track;
+               tree_set_expand_artist(artist, !artist->expanded);
+               tree_track_to_track_iter(track, &sel);
+               window_set_sel(lib_track_win, (struct iter *)&sel);
+       } else if (artist != NULL) {
+               tree_set_expand_artist(artist, !artist->expanded);
+       }
+
+}
+
+void tree_expand_matching(const char *text)
+{
+       struct artist *artist;
+       struct rb_node *tmp1;
+       int have_track_selected = 0;
+
+       rb_for_each_entry(artist, tmp1, &lib_artist_root, tree_node) {
+               struct album *album = NULL;
+               struct rb_node *tmp2;
+               int album_matched = 0;
+
+               rb_for_each_entry(album, tmp2, &artist->album_root, tree_node) {
+                       struct tree_track *tree_track = to_tree_track(rb_first(&album->track_root));
+                       struct track_info *ti = ((struct simple_track *) tree_track)->info;
+                       album_matched = track_info_matches_full(ti, text, TI_MATCH_ALBUM, TI_MATCH_ARTIST | TI_MATCH_ALBUMARTIST, 0);
+                       if (album_matched)
+                               break;
+               }
+               artist->expanded = album_matched;
+               if (!have_track_selected) {
+                       struct tree_track *tree_track;
+                       int track_matched = 0;
+
+                       if (!album)
+                               album = to_album(rb_first(&artist->album_root));
+
+                       rb_for_each_entry(tree_track, tmp2, &album->track_root, tree_node) {
+                               struct track_info *ti = ((struct simple_track *) tree_track)->info;
+                               track_matched = track_info_matches_full(ti, text, TI_MATCH_TITLE, 0, 0);
+                               if (track_matched)
+                                       break;
+                       }
+                       if (album_matched || track_matched) {
+                               if (!tree_track)
+                                       tree_track = to_tree_track(rb_first(&album->track_root));
+                               tree_sel_track(tree_track, auto_expand_albums_search);
+                               have_track_selected = 1;
+                       }
+               }
+       }
+       window_changed(lib_tree_win);
+}
+
+void tree_expand_all(void)
+{
+       struct artist *artist;
+       struct rb_node *tmp;
+
+       rb_for_each_entry(artist, tmp, &lib_artist_root, tree_node) {
+               artist->expanded = 1;
+       }
+       window_changed(lib_tree_win);
+}
+
+static void remove_track(struct tree_track *track)
+{
+       if (track_visible(track)) {
+               struct track_iter iter;
+               tree_track_to_track_iter(track, &iter);
+               window_row_vanishes(lib_track_win, (struct iter *)&iter);
+       }
+       rb_erase(&track->tree_node, &track->album->track_root);
+}
+
+void tree_remove(struct tree_track *track)
+{
+       struct album *album = track->album;
+       struct artist *sel_artist;
+       struct album *sel_album;
+
+       tree_win_get_selected(&sel_artist, &sel_album);
+
+       remove_track(track);
+       /* don't free the track */
+
+       if (rb_root_empty(&album->track_root)) {
+               struct artist *artist = album->artist;
+
+               if (sel_album == album)
+                       lib_cur_win = lib_tree_win;
+
+               if (track_visible(track) && !tree_album_selected()) {
+                       struct iter iter;
+                       album_to_track_iter(album, (struct track_iter*)&iter);
+                       window_row_vanishes(lib_track_win, &iter);
+               }
+
+               remove_album(album);
+               album_free(album);
+
+               if (rb_root_empty(&artist->album_root)) {
+                       artist->expanded = 0;
+                       remove_artist(artist);
+                       artist_free(artist);
+               }
+       }
+}
+
+void tree_remove_sel(void)
+{
+       if (lib_cur_win == lib_tree_win) {
+               tree_win_remove_sel();
+       } else {
+               track_win_remove_sel();
+       }
+}
+
+void tree_sort_artists(void)
+{
+       struct rb_node *a_node, *a_tmp;
+
+       rb_for_each_safe(a_node, a_tmp, &lib_artist_root) {
+               struct rb_node *l_node, *l_tmp;
+               struct artist *artist = to_artist(a_node);
+
+               rb_for_each_safe(l_node, l_tmp, &artist->album_root) {
+                       struct rb_node *t_node, *t_tmp;
+                       struct album *album = to_album(l_node);
+
+                       rb_for_each_safe(t_node, t_tmp, &album->track_root) {
+                               struct tree_track *track = to_tree_track(t_node);
+
+                               tree_remove(track);
+                               tree_add_track(track);
+                       }
+               }
+       }
+}
+
+void tree_sel_current(int auto_expand_albums)
+{
+       tree_sel_track(lib_cur_track, auto_expand_albums);
+}
+
+void tree_sel_first(void)
+{
+       if (!rb_root_empty(&lib_artist_root)) {
+               struct artist *artist = to_artist(rb_first(&lib_artist_root));
+               struct album *album = to_album(rb_first(&artist->album_root));
+               struct tree_track *tree_track = to_tree_track(rb_first(&album->track_root));
+               tree_sel_track(tree_track, auto_expand_albums_search);
+       }
+}
+
+void tree_sel_track(struct tree_track *t, int auto_expand_albums )
+{
+       if (t) {
+               struct iter iter;
+
+               if (auto_expand_albums)
+                       t->album->artist->expanded = 1;
+               if (t->album->artist->expanded)
+                       album_to_iter(t->album, &iter);
+               else
+                       artist_to_iter(t->album->artist, &iter);
+
+               window_set_sel(lib_tree_win, &iter);
+
+               tree_track_to_track_iter(t, (struct track_iter *)&iter);
+               window_set_sel(lib_track_win, &iter);
+
+               if (lib_cur_win == lib_tree_win) {
+                       lib_cur_win = lib_track_win;
+                       lib_tree_win->changed = 1;
+                       lib_track_win->changed = 1;
+               }
+       }
+}
+
+static int album_for_each_track(struct album *album, int (*cb)(void *data, struct track_info *ti),
+               void *data, int reverse)
+{
+       struct tree_track *track;
+       struct rb_node *tmp;
+       int rc = 0;
+
+       if (reverse) {
+               rb_for_each_entry_reverse(track, tmp, &album->track_root, tree_node) {
+                       rc = cb(data, tree_track_info(track));
+                       if (rc)
+                               break;
+               }
+       } else {
+               rb_for_each_entry(track, tmp, &album->track_root, tree_node) {
+                       rc = cb(data, tree_track_info(track));
+                       if (rc)
+                               break;
+               }
+       }
+       return rc;
+}
+
+static int artist_for_each_track(struct artist *artist, int (*cb)(void *data, struct track_info *ti),
+               void *data, int reverse)
+{
+       struct album *album;
+       struct rb_node *tmp;
+       int rc = 0;
+
+       if (reverse) {
+               rb_for_each_entry_reverse(album, tmp, &artist->album_root, tree_node) {
+                       rc = album_for_each_track(album, cb, data, 1);
+                       if (rc)
+                               break;
+               }
+       } else {
+               rb_for_each_entry(album, tmp, &artist->album_root, tree_node) {
+                       rc = album_for_each_track(album, cb, data, 0);
+                       if (rc)
+                               break;
+               }
+       }
+       return rc;
+}
+
+int _tree_for_each_sel(int (*cb)(void *data, struct track_info *ti), void *data, int reverse)
+{
+       int rc = 0;
+
+       if (lib_cur_win == lib_tree_win) {
+               struct artist *artist;
+               struct album *album;
+
+               tree_win_get_selected(&artist, &album);
+               if (artist) {
+                       if (album == NULL) {
+                               rc = artist_for_each_track(artist, cb, data, reverse);
+                       } else {
+                               rc = album_for_each_track(album, cb, data, reverse);
+                       }
+               }
+       } else {
+               struct iter sel;
+               struct tree_track *track;
+
+               if (window_get_sel(lib_track_win, &sel)) {
+                       track = iter_to_tree_track(&sel);
+                       rc = cb(data, tree_track_info(track));
+               }
+       }
+       return rc;
+}
+
+int tree_for_each_sel(int (*cb)(void *data, struct track_info *ti), void *data, int reverse)
+{
+       int rc = _tree_for_each_sel(cb, data, reverse);
+
+       window_down(lib_cur_win, 1);
+       return rc;
+}
diff --git a/u_collate.c b/u_collate.c
new file mode 100644 (file)
index 0000000..95c50ec
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2010-2013 Various Authors
+ * Copyright 2010 Johannes Weißl
+ *
+ * based on gunicollate.c from glib
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "u_collate.h"
+#include "uchar.h"
+#include "xmalloc.h"
+#include "ui_curses.h" /* using_utf8, charset */
+#include "convert.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+
+int u_strcoll(const char *str1, const char *str2)
+{
+       int result;
+
+       if (using_utf8) {
+               result = strcoll(str1, str2);
+       } else {
+               char *str1_locale = NULL, *str2_locale = NULL;
+
+               convert(str1, -1, &str1_locale, -1, charset, "UTF-8");
+               convert(str2, -1, &str2_locale, -1, charset, "UTF-8");
+
+               if (str1_locale && str2_locale)
+                       result = strcoll(str1_locale, str2_locale);
+               else
+                       result = strcmp(str1, str2);
+
+               if (str2_locale)
+                       free(str2_locale);
+               if (str1_locale)
+                       free(str1_locale);
+       }
+
+       return result;
+}
+
+int u_strcasecoll(const char *str1, const char *str2)
+{
+       char *cf_a, *cf_b;
+       int res;
+
+       cf_a = u_casefold(str1);
+       cf_b = u_casefold(str2);
+
+       res = u_strcoll(cf_a, cf_b);
+
+       free(cf_b);
+       free(cf_a);
+
+       return res;
+}
+
+int u_strcasecoll0(const char *str1, const char *str2)
+{
+       if (!str1)
+               return str2 ? -1 : 0;
+       if (!str2)
+               return 1;
+
+       return u_strcasecoll(str1, str2);
+}
+
+char *u_strcoll_key(const char *str)
+{
+       char *result = NULL;
+
+       if (using_utf8) {
+               size_t xfrm_len = strxfrm(NULL, str, 0);
+               if ((ssize_t) xfrm_len >= 0 && xfrm_len < INT_MAX - 2) {
+                       result = xnew(char, xfrm_len + 1);
+                       strxfrm(result, str, xfrm_len + 1);
+               }
+       }
+
+       if (!result) {
+               char *str_locale = NULL;
+
+               convert(str, -1, &str_locale, -1, charset, "UTF-8");
+
+               if (str_locale) {
+                       size_t xfrm_len = strxfrm(NULL, str_locale, 0);
+                       if ((ssize_t) xfrm_len >= 0 && xfrm_len < INT_MAX - 2) {
+                               result = xnew(char, xfrm_len + 2);
+                               result[0] = 'A';
+                               strxfrm(result + 1, str_locale, xfrm_len + 1);
+                       }
+                       free(str_locale);
+               }
+       }
+
+       if (!result) {
+               size_t xfrm_len = strlen(str);
+               result = xmalloc(xfrm_len + 2);
+               result[0] = 'B';
+               memcpy(result + 1, str, xfrm_len);
+               result[xfrm_len+1] = '\0';
+       }
+
+       return result;
+}
+
+char *u_strcasecoll_key(const char *str)
+{
+       char *key, *cf_str;
+
+       cf_str = u_casefold(str);
+
+       key = u_strcoll_key(cf_str);
+
+       free(cf_str);
+
+       return key;
+}
+
+char *u_strcasecoll_key0(const char *str)
+{
+       return str ? u_strcasecoll_key(str) : NULL;
+}
diff --git a/u_collate.h b/u_collate.h
new file mode 100644 (file)
index 0000000..dfca339
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2013 Various Authors
+ * Copyright 2010 Johannes Weißl
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_U_COLLATE_H
+#define CMUS_U_COLLATE_H
+
+/*
+ * @str1  valid, normalized, null-terminated UTF-8 string
+ * @str2  valid, normalized, null-terminated UTF-8 string
+ *
+ * Compares two strings for ordering using the linguistically
+ * correct rules for the current locale.
+ *
+ * Returns -1 if @str1 compares before @str2, 0 if they compare equal,
+ * +1 if @str1 compares after @str2.
+ */
+int u_strcoll(const char *str1, const char *str2);
+
+/*
+ * @str1  valid, normalized, null-terminated UTF-8 string
+ * @str2  valid, normalized, null-terminated UTF-8 string
+ *
+ * Like u_strcoll(), but do casefolding before comparing.
+ */
+int u_strcasecoll(const char *str1, const char *str2);
+
+/*
+ * @str1  valid, normalized, null-terminated UTF-8 string or NULL
+ * @str2  valid, normalized, null-terminated UTF-8 string or NULL
+ *
+ * Like u_strcasecoll(), but handle NULL pointers gracefully.
+ */
+int u_strcasecoll0(const char *str1, const char *str2);
+
+/*
+ * @str  valid, normalized, null-terminated UTF-8 string
+ *
+ * Converts a string into a collation key that can be compared
+ * with other collation keys produced by the same function using
+ * strcmp().
+ *
+ * Returns a newly allocated string.
+ */
+char *u_strcoll_key(const char *str);
+
+/*
+ * @str  valid, normalized, null-terminated UTF-8 string
+ *
+ * Like u_strcoll_key(), but do casefolding before generating key.
+ *
+ * Returns a newly allocated string.
+ */
+char *u_strcasecoll_key(const char *str);
+
+/*
+ * @str  valid, normalized, null-terminated UTF-8 string or NULL
+ *
+ * Like u_strcasecoll_key(), but handle NULL pointers gracefully.
+ */
+char *u_strcasecoll_key0(const char *str);
+
+#endif
diff --git a/uchar.c b/uchar.c
new file mode 100644 (file)
index 0000000..b2158cc
--- /dev/null
+++ b/uchar.c
@@ -0,0 +1,691 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "uchar.h"
+#include "compiler.h"
+#include "gbuf.h"
+#include "utils.h" /* N_ELEMENTS */
+#include "ui_curses.h" /* using_utf8, charset */
+#include "convert.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <wchar.h>
+#include <wctype.h>
+#include <ctype.h>
+
+#include "unidecomp.h"
+
+const char hex_tab[16] = "0123456789abcdef";
+
+/*
+ * Byte Sequence                                             Min       Min        Max
+ * ----------------------------------------------------------------------------------
+ * 0xxxxxxx                                              0000000   0x00000   0x00007f
+ * 110xxxxx 10xxxxxx                                000 10000000   0x00080   0x0007ff
+ * 1110xxxx 10xxxxxx 10xxxxxx                  00001000 00000000   0x00800   0x00ffff
+ * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx   00001 00000000 00000000   0x10000   0x10ffff (not 0x1fffff)
+ *
+ * max: 100   001111   111111   111111  (0x10ffff)
+ */
+
+/* Length of UTF-8 byte sequence.
+ * Table index is the first byte of UTF-8 sequence.
+ */
+static const signed char len_tab[256] = {
+       /*   0-127  0xxxxxxx */
+       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+
+       /* 128-191  10xxxxxx (invalid first byte) */
+       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+
+       /* 192-223  110xxxxx */
+       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+
+       /* 224-239  1110xxxx */
+       3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+
+       /* 240-244  11110xxx (000 - 100) */
+       4, 4, 4, 4, 4,
+
+       /* 11110xxx (101 - 111) (always invalid) */
+       -1, -1, -1,
+
+       /* 11111xxx (always invalid) */
+       -1, -1, -1, -1, -1, -1, -1, -1
+};
+
+/* fault-tolerant equivalent to len_tab, from glib */
+static const char utf8_skip_data[256] = {
+       1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+       1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+       1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+       1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+       1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+       1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+       2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+       3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,6,6,1,1
+};
+const char * const utf8_skip = utf8_skip_data;
+
+/* index is length of the UTF-8 sequence - 1 */
+static int min_val[4] = { 0x000000, 0x000080, 0x000800, 0x010000 };
+static int max_val[4] = { 0x00007f, 0x0007ff, 0x00ffff, 0x10ffff };
+
+/* get value bits from the first UTF-8 sequence byte */
+static unsigned int first_byte_mask[4] = { 0x7f, 0x1f, 0x0f, 0x07 };
+
+int u_is_valid(const char *str)
+{
+       const unsigned char *s = (const unsigned char *)str;
+       int i = 0;
+
+       while (s[i]) {
+               unsigned char ch = s[i++];
+               int len = len_tab[ch];
+
+               if (len <= 0)
+                       return 0;
+
+               if (len > 1) {
+                       /* len - 1 10xxxxxx bytes */
+                       uchar u;
+                       int c;
+
+                       len--;
+                       u = ch & first_byte_mask[len];
+                       c = len;
+                       do {
+                               ch = s[i++];
+                               if (len_tab[ch] != 0)
+                                       return 0;
+                               u = (u << 6) | (ch & 0x3f);
+                       } while (--c);
+
+                       if (u < min_val[len] || u > max_val[len])
+                               return 0;
+               }
+       }
+       return 1;
+}
+
+size_t u_strlen(const char *str)
+{
+       size_t len;
+       for (len = 0; *str; len++)
+               str = u_next_char(str);
+       return len;
+}
+
+size_t u_strlen_safe(const char *str)
+{
+       const unsigned char *s = (const unsigned char *)str;
+       size_t len = 0;
+
+       while (*s) {
+               int l = len_tab[*s];
+
+               if (unlikely(l > 1)) {
+                       /* next l - 1 bytes must be 0x10xxxxxx */
+                       int c = 1;
+                       do {
+                               if (len_tab[s[c]] != 0) {
+                                       /* invalid sequence */
+                                       goto single_char;
+                               }
+                               c++;
+                       } while (c < l);
+
+                       /* valid sequence */
+                       s += l;
+                       len++;
+                       continue;
+               }
+single_char:
+               /* l is -1, 0 or 1
+                * invalid chars counted as single characters */
+               s++;
+               len++;
+       }
+       return len;
+}
+
+int u_char_width(uchar u)
+{
+       if (unlikely(u < 0x20))
+               goto control;
+
+       /* Combining Diacritical Marks */
+       if (u >= 0x300U && u <= 0x36fU)
+               goto zero;
+
+       if (u < 0x1100U)
+               goto narrow;
+
+       /* Hangul Jamo init. consonants */
+       if (u <= 0x115fU)
+               goto wide;
+
+       /* Zero-width characters */
+       if (u == 0x200bU || u == 0x200cU || u == 0x200dU)
+               goto zero;
+
+       /* angle brackets */
+       if (u == 0x2329U || u == 0x232aU)
+               goto wide;
+
+       if (u < 0x2e80U)
+               goto narrow;
+       /* CJK ... Yi */
+       if (u < 0x302aU)
+               goto wide;
+       if (u <= 0x302fU)
+               goto narrow;
+       if (u == 0x303fU)
+               goto narrow;
+       if (u == 0x3099U)
+               goto narrow;
+       if (u == 0x309aU)
+               goto narrow;
+       /* CJK ... Yi */
+       if (u <= 0xa4cfU)
+               goto wide;
+
+       /* Hangul Syllables */
+       if (u >= 0xac00U && u <= 0xd7a3U)
+               goto wide;
+
+       /* CJK Compatibility Ideographs */
+       if (u >= 0xf900U && u <= 0xfaffU)
+               goto wide;
+
+       /* CJK Compatibility Forms */
+       if (u >= 0xfe30U && u <= 0xfe6fU)
+               goto wide;
+
+       /* Fullwidth Forms */
+       if (u >= 0xff00U && u <= 0xff60U)
+               goto wide;
+
+       /* Halfwidth Forms */
+       if (u >= 0xff61U && u <= 0xffdfU)
+               goto narrow;
+
+       /* Fullwidth Forms */
+       if (u >= 0xffe0U && u <= 0xffe6U)
+               goto wide;
+
+       /* Halfwidth Forms */
+       if (u >= 0xffe8U && u <= 0xffeeU)
+               goto narrow;
+
+       /* CJK extra stuff */
+       if (u >= 0x20000U && u <= 0x2fffdU)
+               goto wide;
+
+       /* ? */
+       if (u >= 0x30000U && u <= 0x3fffdU)
+               goto wide;
+
+       /* invalid bytes in unicode stream are rendered "<xx>" */
+       if (u & U_INVALID_MASK)
+               goto invalid;
+zero:
+       return 0;
+narrow:
+       return 1;
+wide:
+       return 2;
+control:
+       /* special case */
+       if (u == 0)
+               return 1;
+
+       /* print control chars as <xx> */
+invalid:
+       return 4;
+}
+
+int u_str_width(const char *str)
+{
+       int idx = 0, w = 0;
+
+       while (str[idx]) {
+               uchar u = u_get_char(str, &idx);
+               w += u_char_width(u);
+       }
+       return w;
+}
+
+int u_str_nwidth(const char *str, int len)
+{
+       int idx = 0;
+       int w = 0;
+
+       while (len > 0) {
+               uchar u = u_get_char(str, &idx);
+               if (u == 0)
+                       break;
+               w += u_char_width(u);
+               len--;
+       }
+       return w;
+}
+
+char *u_strchr(const char *str, uchar uch)
+{
+       int idx = 0;
+
+       while (str[idx]) {
+               uchar u = u_get_char(str, &idx);
+               if (uch == u)
+                       return (char *) (str + idx);
+       }
+       return NULL;
+}
+
+void u_prev_char_pos(const char *str, int *idx)
+{
+       const unsigned char *s = (const unsigned char *)str;
+       int c, len, i = *idx;
+       uchar ch;
+
+       ch = s[--i];
+       len = len_tab[ch];
+       if (len != 0) {
+               /* start of byte sequence or invelid uchar */
+               goto one;
+       }
+
+       c = 1;
+       while (1) {
+               if (i == 0) {
+                       /* first byte of the sequence is missing */
+                       break;
+               }
+
+               ch = s[--i];
+               len = len_tab[ch];
+               c++;
+
+               if (len == 0) {
+                       if (c < 4)
+                               continue;
+
+                       /* too long sequence */
+                       break;
+               }
+               if (len != c) {
+                       /* incorrect length */
+                       break;
+               }
+
+               /* ok */
+               *idx = i;
+               return;
+       }
+one:
+       *idx = *idx - 1;
+       return;
+}
+
+uchar u_get_char(const char *str, int *idx)
+{
+       const unsigned char *s = (const unsigned char *)str;
+       int len, i, x = 0;
+       uchar ch, u;
+
+       if (idx)
+               s += *idx;
+       else
+               idx = &x;
+       ch = s[0];
+
+       /* ASCII optimization */
+       if (ch < 128) {
+               *idx += 1;
+               return ch;
+       }
+
+       len = len_tab[ch];
+       if (unlikely(len < 1))
+               goto invalid;
+
+       u = ch & first_byte_mask[len - 1];
+       for (i = 1; i < len; i++) {
+               ch = s[i];
+               if (unlikely(len_tab[ch] != 0))
+                       goto invalid;
+               u = (u << 6) | (ch & 0x3f);
+       }
+       *idx += len;
+       return u;
+invalid:
+       *idx += 1;
+       u = s[0];
+       return u | U_INVALID_MASK;
+}
+
+void u_set_char_raw(char *str, int *idx, uchar uch)
+{
+       int i = *idx;
+
+       if (uch <= 0x0000007fU) {
+               str[i++] = uch;
+               *idx = i;
+       } else if (uch <= 0x000007ffU) {
+               str[i + 1] = (uch & 63) | 0x80; uch >>= 6;
+               str[i + 0] = uch | 0x000000c0U;
+               i += 2;
+               *idx = i;
+       } else if (uch <= 0x0000ffffU) {
+               str[i + 2] = (uch & 63) | 0x80; uch >>= 6;
+               str[i + 1] = (uch & 63) | 0x80; uch >>= 6;
+               str[i + 0] = uch | 0x000000e0U;
+               i += 3;
+               *idx = i;
+       } else if (uch <= 0x0010ffffU) {
+               str[i + 3] = (uch & 63) | 0x80; uch >>= 6;
+               str[i + 2] = (uch & 63) | 0x80; uch >>= 6;
+               str[i + 1] = (uch & 63) | 0x80; uch >>= 6;
+               str[i + 0] = uch | 0x000000f0U;
+               i += 4;
+               *idx = i;
+       } else {
+               /* must be an invalid uchar */
+               str[i++] = uch & 0xff;
+               *idx = i;
+       }
+}
+
+/*
+ * Printing functions, these lose information
+ */
+
+void u_set_char(char *str, int *idx, uchar uch)
+{
+       int i = *idx;
+
+       if (unlikely(uch <= 0x0000001fU))
+               goto invalid;
+
+       if (uch <= 0x0000007fU) {
+               str[i++] = uch;
+               *idx = i;
+               return;
+       } else if (uch <= 0x000007ffU) {
+               str[i + 1] = (uch & 63) | 0x80; uch >>= 6;
+               str[i + 0] = uch | 0x000000c0U;
+               i += 2;
+               *idx = i;
+               return;
+       } else if (uch <= 0x0000ffffU) {
+               str[i + 2] = (uch & 63) | 0x80; uch >>= 6;
+               str[i + 1] = (uch & 63) | 0x80; uch >>= 6;
+               str[i + 0] = uch | 0x000000e0U;
+               i += 3;
+               *idx = i;
+               return;
+       } else if (uch <= 0x0010ffffU) {
+               str[i + 3] = (uch & 63) | 0x80; uch >>= 6;
+               str[i + 2] = (uch & 63) | 0x80; uch >>= 6;
+               str[i + 1] = (uch & 63) | 0x80; uch >>= 6;
+               str[i + 0] = uch | 0x000000f0U;
+               i += 4;
+               *idx = i;
+               return;
+       }
+invalid:
+       /* control character or invalid unicode */
+       if (uch == 0) {
+               /* handle this special case here to make the common case fast */
+               str[i++] = 0;
+               *idx = i;
+       } else {
+               str[i++] = '<';
+               str[i++] = hex_tab[(uch >> 4) & 0xf];
+               str[i++] = hex_tab[uch & 0xf];
+               str[i++] = '>';
+               *idx = i;
+       }
+}
+
+int u_copy_chars(char *dst, const char *src, int *width)
+{
+       int w = *width;
+       int si = 0, di = 0;
+       int cw;
+       uchar u;
+
+       while (w > 0) {
+               u = u_get_char(src, &si);
+               if (u == 0)
+                       break;
+
+               cw = u_char_width(u);
+               w -= cw;
+
+               if (unlikely(w < 0)) {
+                       if (cw == 2)
+                               dst[di++] = ' ';
+                       if (cw == 4) {
+                               dst[di++] = '<';
+                               if (w >= -2)
+                                       dst[di++] = hex_tab[(u >> 4) & 0xf];
+                               if (w >= -1)
+                                       dst[di++] = hex_tab[u & 0xf];
+                       }
+                       w = 0;
+                       break;
+               }
+               u_set_char(dst, &di, u);
+       }
+       *width -= w;
+       return di;
+}
+
+int u_to_ascii(char *dst, const char *src, int len)
+{
+       int i, idx = 0;
+       for (i = 0; i < len && src[idx]; i++) {
+               uchar u = u_get_char(src, &idx);
+               dst[i] = (u < 128) ? u : '?';
+       }
+       return i;
+}
+
+int u_skip_chars(const char *str, int *width)
+{
+       int w = *width;
+       int idx = 0;
+
+       while (w > 0) {
+               uchar u = u_get_char(str, &idx);
+               w -= u_char_width(u);
+       }
+       /* add 1..3 if skipped 'too much' (the last char was double width or invalid (<xx>)) */
+       *width -= w;
+       return idx;
+}
+
+/*
+ * Case-folding functions
+ */
+
+static inline uchar u_casefold_char(uchar ch)
+{
+        /* faster lookup for for A-Z, rest of ASCII unaffected */
+        if (ch < 0x0041)
+                return ch;
+        if (ch <= 0x005A)
+                return ch + 0x20;
+#if defined(_WIN32) || defined(__STDC_ISO_10646__) || defined(__APPLE__)
+        if (ch < 128)
+                return ch;
+       ch = towlower(ch);
+#endif
+       return ch;
+}
+
+char *u_casefold(const char *str)
+{
+       GBUF(out);
+       int i = 0;
+
+       while (str[i]) {
+               char buf[4];
+               int buflen = 0;
+               uchar ch = u_get_char(str, &i);
+
+               ch = u_casefold_char(ch);
+               u_set_char_raw(buf, &buflen, ch);
+               gbuf_add_bytes(&out, buf, buflen);
+       }
+
+       return gbuf_steal(&out);
+}
+
+/*
+ * Comparison functions
+ */
+
+int u_strcase_equal(const char *a, const char *b)
+{
+       int ai = 0, bi = 0;
+
+       while (a[ai]) {
+               uchar au, bu;
+
+               au = u_get_char(a, &ai);
+               bu = u_get_char(b, &bi);
+
+               if (u_casefold_char(au) != u_casefold_char(bu))
+                       return 0;
+       }
+
+       return b[bi] ? 0 : 1;
+}
+
+static uchar get_base_from_composed(uchar ch)
+{
+       int begin = 0;
+       int end = N_ELEMENTS(unidecomp_map);
+
+       if (ch < unidecomp_map[begin].composed || ch > unidecomp_map[end - 1].composed)
+               return ch;
+
+       /* binary search */
+       while (1) {
+               int half = (begin + end) / 2;
+               if (ch == unidecomp_map[half].composed)
+                       return unidecomp_map[half].base;
+               else if (half == begin)
+                       break;
+               else if (ch > unidecomp_map[half].composed)
+                       begin = half;
+               else
+                       end = half;
+       }
+       return ch;
+}
+
+static inline int do_u_strncase_equal(const char *a, const char *b, size_t len, int only_base_chars)
+{
+       int ai = 0, bi = 0;
+       size_t i;
+
+       for (i = 0; i < len; i++) {
+               uchar au, bu;
+
+               au = u_get_char(a, &ai);
+               bu = u_get_char(b, &bi);
+
+               if (only_base_chars) {
+                       au = get_base_from_composed(au);
+                       bu = get_base_from_composed(bu);
+               }
+
+               if (u_casefold_char(au) != u_casefold_char(bu))
+                       return 0;
+       }
+
+       return 1;
+}
+
+int u_strncase_equal(const char *a, const char *b, size_t len)
+{
+       return do_u_strncase_equal(a, b, len, 0);
+}
+
+int u_strncase_equal_base(const char *a, const char *b, size_t len)
+{
+       return do_u_strncase_equal(a, b, len, 1);
+}
+
+static inline char *do_u_strcasestr(const char *haystack, const char *needle, int only_base_chars)
+{
+       /* strlen is faster and works here */
+       int haystack_len = strlen(haystack);
+       int needle_len = u_strlen(needle);
+
+       do {
+               int idx;
+
+               if (haystack_len < needle_len)
+                       return NULL;
+               if (do_u_strncase_equal(needle, haystack, needle_len, only_base_chars))
+                       return (char *)haystack;
+
+               /* skip one char */
+               idx = 0;
+               u_get_char(haystack, &idx);
+               haystack += idx;
+               haystack_len -= idx;
+       } while (1);
+}
+
+char *u_strcasestr(const char *haystack, const char *needle)
+{
+       return do_u_strcasestr(haystack, needle, 0);
+}
+
+char *u_strcasestr_base(const char *haystack, const char *needle)
+{
+       return do_u_strcasestr(haystack, needle, 1);
+}
+
+char *u_strcasestr_filename(const char *haystack, const char *needle)
+{
+       char *r = NULL, *ustr = NULL;
+       if (!using_utf8 && utf8_encode(haystack, charset, &ustr) == 0)
+               haystack = ustr;
+       r = u_strcasestr_base(haystack, needle);
+       free(ustr);
+       return r;
+}
diff --git a/uchar.h b/uchar.h
new file mode 100644 (file)
index 0000000..4be1338
--- /dev/null
+++ b/uchar.h
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_UCHAR_H
+#define CMUS_UCHAR_H
+
+#include <stddef.h> /* size_t */
+
+typedef unsigned int uchar;
+
+extern const char hex_tab[16];
+
+/*
+ * Invalid bytes are or'ed with this
+ * for example 0xff -> 0x100000ff
+ */
+#define U_INVALID_MASK 0x10000000U
+
+/*
+ * @uch  potential unicode character
+ *
+ * Returns 1 if @uch is valid unicode character, 0 otherwise
+ */
+static inline int u_is_unicode(uchar uch)
+{
+       return uch <= 0x0010ffffU;
+}
+
+/*
+ * Returns size of @uch in bytes
+ */
+static inline int u_char_size(uchar uch)
+{
+       if (uch <= 0x0000007fU) {
+               return 1;
+       } else if (uch <= 0x000007ffU) {
+               return 2;
+       } else if (uch <= 0x0000ffffU) {
+               return 3;
+       } else if (uch <= 0x0010ffffU) {
+               return 4;
+       } else {
+               return 1;
+       }
+}
+
+/*
+ * Returns width of @uch (normally 1 or 2, 4 for invalid chars (<xx>))
+ */
+int u_char_width(uchar uch);
+
+/*
+ * @str  any null-terminated string
+ *
+ * Returns 1 if @str is valid UTF-8 string, 0 otherwise.
+ */
+int u_is_valid(const char *str);
+
+/*
+ * @str  valid, null-terminated UTF-8 string
+ *
+ * Returns position of next unicode character in @str.
+ */
+extern const char * const utf8_skip;
+static inline char *u_next_char(const char *str)
+{
+       return (char *) (str + utf8_skip[*((const unsigned char *) str)]);
+}
+
+/*
+ * @str  valid, null-terminated UTF-8 string
+ *
+ * Retuns length of @str in UTF-8 characters.
+ */
+size_t u_strlen(const char *str);
+
+/*
+ * @str  null-terminated UTF-8 string
+ *
+ * Retuns length of @str in UTF-8 characters.
+ * Invalid chars are counted as single characters.
+ */
+size_t u_strlen_safe(const char *str);
+
+/*
+ * @str  null-terminated UTF-8 string
+ *
+ * Retuns width of @str.
+ */
+int u_str_width(const char *str);
+
+/*
+ * @str  null-terminated UTF-8 string
+ * @len  number of characters to measure
+ *
+ * Retuns width of the first @len characters in @str.
+ */
+int u_str_nwidth(const char *str, int len);
+
+/*
+ * @str  null-terminated UTF-8 string
+ * @uch  unicode character
+ *
+ * Returns a pointer to the first occurrence of @uch in the @str.
+ */
+char *u_strchr(const char *str, uchar uch);
+
+void u_prev_char_pos(const char *str, int *idx);
+
+/*
+ * @str  null-terminated UTF-8 string
+ * @idx  pointer to byte index in @str (not UTF-8 character index!) or NULL
+ *
+ * Returns unicode character at @str[*@idx] or @str[0] if @idx is NULL.
+ * Stores byte index of the next char back to @idx if set.
+ */
+uchar u_get_char(const char *str, int *idx);
+
+/*
+ * @str  destination buffer
+ * @idx  pointer to byte index in @str (not UTF-8 character index!)
+ * @uch  unicode character
+ */
+void u_set_char_raw(char *str, int *idx, uchar uch);
+void u_set_char(char *str, int *idx, uchar uch);
+
+/*
+ * @dst    destination buffer
+ * @src    null-terminated UTF-8 string
+ * @width  how much to copy
+ *
+ * Copies at most @count characters, less if null byte was hit.
+ * Null byte is _never_ copied.
+ * Actual width of copied characters is stored to @width.
+ *
+ * Returns number of _bytes_ copied.
+ */
+int u_copy_chars(char *dst, const char *src, int *width);
+
+/*
+ * @dst    destination buffer
+ * @src    null-terminated UTF-8 string
+ * @len    how many bytes are available in @dst
+ *
+ * Copies at most @len bytes, less if null byte was hit. Replaces every
+ * non-ascii character by '?'. Null byte is _never_ copied.
+ *
+ * Returns number of bytes written to @dst.
+ */
+int u_to_ascii(char *dst, const char *src, int len);
+
+/*
+ * @str    null-terminated UTF-8 string, must be long enough
+ * @width  how much to skip
+ *
+ * Skips @count UTF-8 characters.
+ * Total width of skipped characters is stored to @width.
+ * Returned @width can be the given @width + 1 if the last skipped
+ * character was double width.
+ *
+ * Returns number of _bytes_ skipped.
+ */
+int u_skip_chars(const char *str, int *width);
+
+/*
+ * @str  valid null-terminated UTF-8 string
+ *
+ * Converts a string into a form that is independent of case.
+ *
+ * Returns a newly allocated string
+ */
+char *u_casefold(const char *str);
+
+/*
+ * @str1  valid, normalized, null-terminated UTF-8 string
+ * @str2  valid, normalized, null-terminated UTF-8 string
+ *
+ * Returns 1 if @str1 is equal to @str2, ignoring the case of the characters.
+ */
+int u_strcase_equal(const char *str1, const char *str2);
+
+/*
+ * @str1    valid, normalized, null-terminated UTF-8 string
+ * @str2    valid, normalized, null-terminated UTF-8 string
+ * @len  number of characters to consider for comparison
+ *
+ * Returns 1 if the first @len characters of @str1 and @str2 are equal,
+ * ignoring the case of the characters (0 otherwise).
+ */
+int u_strncase_equal(const char *str1, const char *str2, size_t len);
+
+/*
+ * @str1    valid, normalized, null-terminated UTF-8 string
+ * @str2    valid, normalized, null-terminated UTF-8 string
+ * @len  number of characters to consider for comparison
+ *
+ * Like u_strncase_equal(), but uses only base characters for comparison
+ * (e.g. "Trentemöller" matches "Trentemøller")
+ */
+int u_strncase_equal_base(const char *str1, const char *str2, size_t len);
+
+/*
+ * @haystack  valid, normalized, null-terminated UTF-8 string
+ * @needle    valid, normalized, null-terminated UTF-8 string
+ *
+ * Returns position of @needle in @haystack (case insensitive comparison).
+ */
+char *u_strcasestr(const char *haystack, const char *needle);
+
+/*
+ * @haystack  valid, normalized, null-terminated UTF-8 string
+ * @needle    valid, normalized, null-terminated UTF-8 string
+ *
+ * Like u_strcasestr(), but uses only base characters for comparison
+ * (e.g. "Trentemöller" matches "Trentemøller")
+ */
+char *u_strcasestr_base(const char *haystack, const char *needle);
+
+/*
+ * @haystack  null-terminated string in local encoding
+ * @needle    valid, normalized, null-terminated UTF-8 string
+ *
+ * Like u_strcasestr_base(), but converts @haystack to UTF-8 if necessary.
+ */
+char *u_strcasestr_filename(const char *haystack, const char *needle);
+
+#endif
diff --git a/ui_curses.c b/ui_curses.c
new file mode 100644 (file)
index 0000000..4094d89
--- /dev/null
@@ -0,0 +1,2537 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "job.h"
+#include "convert.h"
+#include "ui_curses.h"
+#include "cmdline.h"
+#include "search_mode.h"
+#include "command_mode.h"
+#include "options.h"
+#include "play_queue.h"
+#include "browser.h"
+#include "filters.h"
+#include "cmus.h"
+#include "player.h"
+#include "output.h"
+#include "utils.h"
+#include "lib.h"
+#include "pl.h"
+#include "xmalloc.h"
+#include "xstrjoin.h"
+#include "window.h"
+#include "comment.h"
+#include "misc.h"
+#include "prog.h"
+#include "uchar.h"
+#include "spawn.h"
+#include "server.h"
+#include "keys.h"
+#include "debug.h"
+#include "help.h"
+#include "worker.h"
+#include "input.h"
+#include "file.h"
+#include "path.h"
+#include "mixer.h"
+#include "mpris.h"
+#include "locking.h"
+#ifdef HAVE_CONFIG
+#include "config/curses.h"
+#include "config/iconv.h"
+#endif
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/select.h>
+#include <ctype.h>
+#include <dirent.h>
+#include <locale.h>
+#include <langinfo.h>
+#ifdef HAVE_ICONV
+#include <iconv.h>
+#endif
+#include <signal.h>
+#include <stdarg.h>
+#include <math.h>
+#include <sys/time.h>
+
+#if defined(__sun__) || defined(__CYGWIN__)
+/* TIOCGWINSZ */
+#include <termios.h>
+#include <ncurses.h>
+#else
+#include <curses.h>
+#endif
+
+/* defined in <term.h> but without const */
+char *tgetstr(const char *id, char **area);
+char *tgoto(const char *cap, int col, int row);
+
+/* globals. documented in ui_curses.h */
+
+volatile sig_atomic_t cmus_running = 1;
+int ui_initialized = 0;
+enum ui_input_mode input_mode = NORMAL_MODE;
+int cur_view = TREE_VIEW;
+int prev_view = -1;
+struct searchable *searchable;
+char *lib_filename = NULL;
+char *lib_ext_filename = NULL;
+char *play_queue_filename = NULL;
+char *play_queue_ext_filename = NULL;
+char *charset = NULL;
+int using_utf8 = 0;
+
+/* ------------------------------------------------------------------------- */
+
+static char *lib_autosave_filename;
+static char *play_queue_autosave_filename;
+
+/* shown error message and time stamp
+ * error is cleared if it is older than 3s and key was pressed
+ */
+static char error_buf[512];
+static time_t error_time = 0;
+/* info messages are displayed in different color */
+static int msg_is_error;
+static int error_count = 0;
+
+static char *server_address = NULL;
+
+static char print_buffer[1024];
+
+/* destination buffer for utf8_encode_to_buf and utf8_decode */
+static char conv_buffer[512];
+
+/* one character can take up to 4 bytes in UTF-8 */
+#define print_buffer_max_width (sizeof(print_buffer) / 4 - 1)
+
+/* used for messages to the client */
+static int client_fd = -1;
+
+static char tcap_buffer[64];
+static const char *t_ts;
+static const char *t_fs;
+
+static int tree_win_x = 0;
+static int tree_win_w = 0;
+
+static int track_win_x = 0;
+static int track_win_w = 0;
+
+static int editable_win_x = 0;
+static int editable_win_w = 0;
+static int editable_active = 1;
+
+static int show_cursor;
+static int cursor_x;
+static int cursor_y;
+
+static const int default_esc_delay = 25;
+
+static char *title_buf = NULL;
+
+enum {
+       CURSED_WIN,
+       CURSED_WIN_CUR,
+       CURSED_WIN_SEL,
+       CURSED_WIN_SEL_CUR,
+
+       CURSED_WIN_ACTIVE,
+       CURSED_WIN_ACTIVE_CUR,
+       CURSED_WIN_ACTIVE_SEL,
+       CURSED_WIN_ACTIVE_SEL_CUR,
+
+       CURSED_SEPARATOR,
+       CURSED_WIN_TITLE,
+       CURSED_COMMANDLINE,
+       CURSED_STATUSLINE,
+
+       CURSED_TITLELINE,
+       CURSED_DIR,
+       CURSED_ERROR,
+       CURSED_INFO,
+
+       CURSED_TRACKWIN_ALBUM,
+
+       NR_CURSED
+};
+
+static unsigned char cursed_to_bg_idx[NR_CURSED] = {
+       COLOR_WIN_BG,
+       COLOR_WIN_BG,
+       COLOR_WIN_INACTIVE_SEL_BG,
+       COLOR_WIN_INACTIVE_CUR_SEL_BG,
+
+       COLOR_WIN_BG,
+       COLOR_WIN_BG,
+       COLOR_WIN_SEL_BG,
+       COLOR_WIN_CUR_SEL_BG,
+
+       COLOR_WIN_BG,
+       COLOR_WIN_TITLE_BG,
+       COLOR_CMDLINE_BG,
+       COLOR_STATUSLINE_BG,
+
+       COLOR_TITLELINE_BG,
+       COLOR_WIN_BG,
+       COLOR_CMDLINE_BG,
+       COLOR_CMDLINE_BG,
+
+       COLOR_TRACKWIN_ALBUM_BG,
+};
+
+static unsigned char cursed_to_fg_idx[NR_CURSED] = {
+       COLOR_WIN_FG,
+       COLOR_WIN_CUR,
+       COLOR_WIN_INACTIVE_SEL_FG,
+       COLOR_WIN_INACTIVE_CUR_SEL_FG,
+
+       COLOR_WIN_FG,
+       COLOR_WIN_CUR,
+       COLOR_WIN_SEL_FG,
+       COLOR_WIN_CUR_SEL_FG,
+
+       COLOR_SEPARATOR,
+       COLOR_WIN_TITLE_FG,
+       COLOR_CMDLINE_FG,
+       COLOR_STATUSLINE_FG,
+
+       COLOR_TITLELINE_FG,
+       COLOR_WIN_DIR,
+       COLOR_ERROR,
+       COLOR_INFO,
+
+       COLOR_TRACKWIN_ALBUM_FG,
+};
+
+static unsigned char cursed_to_attr_idx[NR_CURSED] = {
+       COLOR_WIN_ATTR,
+       COLOR_WIN_CUR_ATTR,
+       COLOR_WIN_INACTIVE_SEL_ATTR,
+       COLOR_WIN_INACTIVE_CUR_SEL_ATTR,
+
+       COLOR_WIN_ATTR,
+       COLOR_WIN_CUR_ATTR,
+       COLOR_WIN_SEL_ATTR,
+       COLOR_WIN_CUR_SEL_ATTR,
+
+       COLOR_WIN_ATTR,
+       COLOR_WIN_TITLE_ATTR,
+       COLOR_CMDLINE_ATTR,
+       COLOR_STATUSLINE_ATTR,
+
+       COLOR_TITLELINE_ATTR,
+       COLOR_WIN_ATTR,
+       COLOR_CMDLINE_ATTR,
+       COLOR_CMDLINE_ATTR,
+
+       COLOR_TRACKWIN_ALBUM_ATTR,
+};
+
+/* index is CURSED_*, value is fucking color pair */
+static int pairs[NR_CURSED];
+
+enum {
+       TF_ALBUMARTIST,
+       TF_ARTIST,
+       TF_ALBUM,
+       TF_DISC,
+       TF_TRACK,
+       TF_TITLE,
+       TF_PLAY_COUNT,
+       TF_YEAR,
+       TF_MAX_YEAR,
+       TF_ORIGINALYEAR,
+       TF_GENRE,
+       TF_COMMENT,
+       TF_DURATION,
+       TF_DURATION_SEC,
+       TF_ALBUMDURATION,
+       TF_BITRATE,
+       TF_CODEC,
+       TF_CODEC_PROFILE,
+       TF_PATHFILE,
+       TF_FILE,
+       TF_RG_TRACK_GAIN,
+       TF_RG_TRACK_PEAK,
+       TF_RG_ALBUM_GAIN,
+       TF_RG_ALBUM_PEAK,
+       TF_ARRANGER,
+       TF_COMPOSER,
+       TF_CONDUCTOR,
+       TF_LYRICIST,
+       TF_PERFORMER,
+       TF_REMIXER,
+       TF_LABEL,
+       TF_PUBLISHER,
+       TF_WORK,
+       TF_OPUS,
+       TF_PARTNUMBER,
+       TF_PART,
+       TF_SUBTITLE,
+       TF_MEDIA,
+       TF_VA,
+       TF_STATUS,
+       TF_POSITION,
+       TF_POSITION_SEC,
+       TF_TOTAL,
+       TF_VOLUME,
+       TF_LVOLUME,
+       TF_RVOLUME,
+       TF_BUFFER,
+       TF_REPEAT,
+       TF_CONTINUE,
+       TF_FOLLOW,
+       TF_SHUFFLE,
+       TF_PLAYLISTMODE,
+       TF_BPM,
+
+       NR_TFS
+};
+
+static struct format_option track_fopts[NR_TFS + 1] = {
+       DEF_FO_STR('A', "albumartist", 0),
+       DEF_FO_STR('a', "artist", 0),
+       DEF_FO_STR('l', "album", 0),
+       DEF_FO_INT('D', "discnumber", 1),
+       DEF_FO_INT('n', "tracknumber", 1),
+       DEF_FO_STR('t', "title", 0),
+       DEF_FO_INT('X', "play_count", 0),
+       DEF_FO_INT('y', "date", 1),
+       DEF_FO_INT('\0', "maxdate", 1),
+       DEF_FO_INT('\0', "originaldate", 1),
+       DEF_FO_STR('g', "genre", 0),
+       DEF_FO_STR('c', "comment", 0),
+       DEF_FO_TIME('d', "duration", 0),
+       DEF_FO_INT('\0', "duration_sec", 1),
+       DEF_FO_TIME('\0', "albumduration", 0),
+       DEF_FO_INT('\0', "bitrate", 0),
+       DEF_FO_STR('\0', "codec", 0),
+       DEF_FO_STR('\0', "codec_profile", 0),
+       DEF_FO_STR('f', "path", 0),
+       DEF_FO_STR('F', "filename", 0),
+       DEF_FO_DOUBLE('\0', "rg_track_gain", 0),
+       DEF_FO_DOUBLE('\0', "rg_track_peak", 0),
+       DEF_FO_DOUBLE('\0', "rg_album_gain", 0),
+       DEF_FO_DOUBLE('\0', "rg_album_peak", 0),
+       DEF_FO_STR('\0', "arranger", 0),
+       DEF_FO_STR('\0', "composer", 0),
+       DEF_FO_STR('\0', "conductor", 0),
+       DEF_FO_STR('\0', "lyricist", 0),
+       DEF_FO_STR('\0', "performer", 0),
+       DEF_FO_STR('\0', "remixer", 0),
+       DEF_FO_STR('\0', "label", 0),
+       DEF_FO_STR('\0', "publisher", 0),
+       DEF_FO_STR('\0', "work", 0),
+       DEF_FO_STR('\0', "opus", 0),
+       DEF_FO_STR('\0', "partnumber", 0),
+       DEF_FO_STR('\0', "part", 0),
+       DEF_FO_STR('\0', "subtitle", 0),
+       DEF_FO_STR('\0', "media", 0),
+       DEF_FO_INT('\0', "va", 0),
+       DEF_FO_STR('\0', "status", 0),
+       DEF_FO_TIME('\0', "position", 0),
+       DEF_FO_INT('\0', "position_sec", 1),
+       DEF_FO_TIME('\0', "total", 0),
+       DEF_FO_INT('\0', "volume", 1),
+       DEF_FO_INT('\0', "lvolume", 1),
+       DEF_FO_INT('\0', "rvolume", 1),
+       DEF_FO_INT('\0', "buffer", 1),
+       DEF_FO_STR('\0', "repeat", 0),
+       DEF_FO_STR('\0', "continue", 0),
+       DEF_FO_STR('\0', "follow", 0),
+       DEF_FO_STR('\0', "shuffle", 0),
+       DEF_FO_STR('\0', "playlist_mode", 0),
+       DEF_FO_INT('\0', "bpm", 0),
+       DEF_FO_END
+};
+
+int get_track_win_x(void)
+{
+       return track_win_x;
+}
+
+int track_format_valid(const char *format)
+{
+       return format_valid(format, track_fopts);
+}
+
+static void utf8_encode_to_buf(const char *buffer)
+{
+       int n;
+#ifdef HAVE_ICONV
+       static iconv_t cd = (iconv_t)-1;
+       size_t is, os;
+       const char *i;
+       char *o;
+       int rc;
+
+       if (cd == (iconv_t)-1) {
+               d_print("iconv_open(UTF-8, %s)\n", charset);
+               cd = iconv_open("UTF-8", charset);
+               if (cd == (iconv_t)-1) {
+                       d_print("iconv_open failed: %s\n", strerror(errno));
+                       goto fallback;
+               }
+       }
+       i = buffer;
+       o = conv_buffer;
+       is = strlen(i);
+       os = sizeof(conv_buffer) - 1;
+       rc = iconv(cd, (void *)&i, &is, &o, &os);
+       *o = 0;
+       if (rc == -1) {
+               d_print("iconv failed: %s\n", strerror(errno));
+               goto fallback;
+       }
+       return;
+fallback:
+#endif
+       n = min_i(sizeof(conv_buffer) - 1, strlen(buffer));
+       memmove(conv_buffer, buffer, n);
+       conv_buffer[n] = '\0';
+}
+
+static void utf8_decode(const char *buffer)
+{
+       int n;
+#ifdef HAVE_ICONV
+       static iconv_t cd = (iconv_t)-1;
+       size_t is, os;
+       const char *i;
+       char *o;
+       int rc;
+
+       if (cd == (iconv_t)-1) {
+               d_print("iconv_open(%s, UTF-8)\n", charset);
+               cd = iconv_open(charset, "UTF-8");
+               if (cd == (iconv_t)-1) {
+                       d_print("iconv_open failed: %s\n", strerror(errno));
+                       goto fallback;
+               }
+       }
+       i = buffer;
+       o = conv_buffer;
+       is = strlen(i);
+       os = sizeof(conv_buffer) - 1;
+       rc = iconv(cd, (void *)&i, &is, &o, &os);
+       *o = 0;
+       if (rc == -1) {
+               d_print("iconv failed: %s\n", strerror(errno));
+               goto fallback;
+       }
+       return;
+fallback:
+#endif
+       n = u_to_ascii(conv_buffer, buffer, sizeof(conv_buffer) - 1);
+       conv_buffer[n] = '\0';
+}
+
+/* screen updates {{{ */
+
+static void dump_print_buffer(int row, int col)
+{
+       if (using_utf8) {
+               (void) mvaddstr(row, col, print_buffer);
+       } else {
+               utf8_decode(print_buffer);
+               (void) mvaddstr(row, col, conv_buffer);
+       }
+}
+
+/* print @str into @buf
+ *
+ * if @str is shorter than @width pad with spaces
+ * if @str is wider than @width truncate and add "..."
+ */
+static int format_str(char *buf, const char *str, int width)
+{
+       int s = 0, d = 0, ellipsis_pos = 0, cut_double_width = 0;
+
+       while (1) {
+               uchar u;
+               int w;
+
+               u = u_get_char(str, &s);
+               if (u == 0) {
+                       memset(buf + d, ' ', width);
+                       d += width;
+                       break;
+               }
+
+               w = u_char_width(u);
+               if (width == 3)
+                       ellipsis_pos = d;
+               if (width == 4 && w == 2) {
+                       /* can't cut double-width char */
+                       ellipsis_pos = d + 1;
+                       cut_double_width = 1;
+               }
+
+               width -= w;
+               if (width < 0) {
+                       /* does not fit */
+                       d = ellipsis_pos;
+                       if (cut_double_width) {
+                               /* first half of the double-width char */
+                               buf[d - 1] = ' ';
+                       }
+                       buf[d++] = '.';
+                       buf[d++] = '.';
+                       buf[d++] = '.';
+                       break;
+               }
+               u_set_char(buf, &d, u);
+       }
+       return d;
+}
+
+static void sprint(int row, int col, const char *str, int width)
+{
+       int pos = 0;
+
+       print_buffer[pos++] = ' ';
+       pos += format_str(print_buffer + pos, str, width - 2);
+       print_buffer[pos++] = ' ';
+       print_buffer[pos] = 0;
+       dump_print_buffer(row, col);
+}
+
+static void sprint_ascii(int row, int col, const char *str, int len)
+{
+       int l;
+
+       l = strlen(str);
+       len -= 2;
+
+       print_buffer[0] = ' ';
+       if (l > len) {
+               memcpy(print_buffer + 1, str, len - 3);
+               print_buffer[len - 2] = '.';
+               print_buffer[len - 1] = '.';
+               print_buffer[len - 0] = '.';
+       } else {
+               memcpy(print_buffer + 1, str, l);
+               memset(print_buffer + 1 + l, ' ', len - l);
+       }
+       print_buffer[len + 1] = ' ';
+       print_buffer[len + 2] = 0;
+       (void) mvaddstr(row, col, print_buffer);
+}
+
+static inline void fopt_set_str(struct format_option *fopt, const char *str)
+{
+       BUG_ON(fopt->type != FO_STR);
+       if (str) {
+               fopt->fo_str = str;
+               fopt->empty = 0;
+       } else {
+               fopt->empty = 1;
+       }
+}
+
+static inline void fopt_set_int(struct format_option *fopt, int value, int empty)
+{
+       BUG_ON(fopt->type != FO_INT);
+       fopt->fo_int = value;
+       fopt->empty = empty;
+}
+
+static inline void fopt_set_double(struct format_option *fopt, double value, int empty)
+{
+       BUG_ON(fopt->type != FO_DOUBLE);
+       fopt->fo_double = value;
+       fopt->empty = empty;
+}
+
+static inline void fopt_set_time(struct format_option *fopt, int value, int empty)
+{
+       BUG_ON(fopt->type != FO_TIME);
+       fopt->fo_time = value;
+       fopt->empty = empty;
+}
+
+static void fill_track_fopts_track_info(struct track_info *info)
+{
+       char *filename;
+
+       if (using_utf8) {
+               filename = info->filename;
+       } else {
+               utf8_encode_to_buf(info->filename);
+               filename = conv_buffer;
+       }
+
+       fopt_set_str(&track_fopts[TF_ALBUMARTIST], info->albumartist);
+       fopt_set_str(&track_fopts[TF_ARTIST], info->artist);
+       fopt_set_str(&track_fopts[TF_ALBUM], info->album);
+       fopt_set_int(&track_fopts[TF_PLAY_COUNT], info->play_count, 0);
+       fopt_set_int(&track_fopts[TF_DISC], info->discnumber, info->discnumber == -1);
+       fopt_set_int(&track_fopts[TF_TRACK], info->tracknumber, info->tracknumber == -1);
+       fopt_set_str(&track_fopts[TF_TITLE], info->title);
+       fopt_set_int(&track_fopts[TF_YEAR], info->date / 10000, info->date <= 0);
+       fopt_set_str(&track_fopts[TF_GENRE], info->genre);
+       fopt_set_str(&track_fopts[TF_COMMENT], info->comment);
+       fopt_set_time(&track_fopts[TF_DURATION], info->duration, info->duration == -1);
+       fopt_set_int(&track_fopts[TF_DURATION_SEC], info->duration, info->duration == -1);
+       fopt_set_double(&track_fopts[TF_RG_TRACK_GAIN], info->rg_track_gain, isnan(info->rg_track_gain));
+       fopt_set_double(&track_fopts[TF_RG_TRACK_PEAK], info->rg_track_peak, isnan(info->rg_track_peak));
+       fopt_set_double(&track_fopts[TF_RG_ALBUM_GAIN], info->rg_album_gain, isnan(info->rg_album_gain));
+       fopt_set_double(&track_fopts[TF_RG_ALBUM_PEAK], info->rg_album_peak, isnan(info->rg_album_peak));
+       fopt_set_int(&track_fopts[TF_ORIGINALYEAR], info->originaldate / 10000, info->originaldate <= 0);
+       fopt_set_int(&track_fopts[TF_BITRATE], (int) (info->bitrate / 1000. + 0.5), info->bitrate == -1);
+       fopt_set_str(&track_fopts[TF_CODEC], info->codec);
+       fopt_set_str(&track_fopts[TF_CODEC_PROFILE], info->codec_profile);
+       fopt_set_str(&track_fopts[TF_PATHFILE], filename);
+       fopt_set_str(&track_fopts[TF_ARRANGER], keyvals_get_val(info->comments, "arranger"));
+       fopt_set_str(&track_fopts[TF_COMPOSER], keyvals_get_val(info->comments, "composer"));
+       fopt_set_str(&track_fopts[TF_CONDUCTOR], keyvals_get_val(info->comments, "conductor"));
+       fopt_set_str(&track_fopts[TF_LYRICIST], keyvals_get_val(info->comments, "lyricist"));
+       fopt_set_str(&track_fopts[TF_PERFORMER], keyvals_get_val(info->comments, "performer"));
+       fopt_set_str(&track_fopts[TF_REMIXER], keyvals_get_val(info->comments, "remixer"));
+       fopt_set_str(&track_fopts[TF_LABEL], keyvals_get_val(info->comments, "label"));
+       fopt_set_str(&track_fopts[TF_PUBLISHER], keyvals_get_val(info->comments, "publisher"));
+       fopt_set_str(&track_fopts[TF_WORK], keyvals_get_val(info->comments, "work"));
+       fopt_set_str(&track_fopts[TF_OPUS], keyvals_get_val(info->comments, "opus"));
+       fopt_set_str(&track_fopts[TF_PARTNUMBER], keyvals_get_val(info->comments, "partnumber"));
+       fopt_set_str(&track_fopts[TF_PART], keyvals_get_val(info->comments, "part"));
+       fopt_set_str(&track_fopts[TF_SUBTITLE], keyvals_get_val(info->comments, "subtitle"));
+       fopt_set_str(&track_fopts[TF_MEDIA], info->media);
+       fopt_set_int(&track_fopts[TF_VA], 0, !track_is_compilation(info->comments));
+       if (is_http_url(info->filename)) {
+               fopt_set_str(&track_fopts[TF_FILE], filename);
+       } else {
+               fopt_set_str(&track_fopts[TF_FILE], path_basename(filename));
+       }
+       fopt_set_int(&track_fopts[TF_BPM], info->bpm, info->bpm == -1);
+}
+
+static int get_album_length(struct album *album)
+{
+       struct tree_track *track;
+       struct rb_node *tmp;
+       int duration = 0;
+
+       rb_for_each_entry(track, tmp, &album->track_root, tree_node) {
+               duration += tree_track_info(track)->duration;
+       }
+
+       return duration;
+}
+
+static void fill_track_fopts_album(struct album *album)
+{
+       fopt_set_int(&track_fopts[TF_YEAR], album->min_date / 10000, album->min_date <= 0);
+       fopt_set_int(&track_fopts[TF_MAX_YEAR], album->date / 10000, album->date <= 0);
+       fopt_set_str(&track_fopts[TF_ALBUMARTIST], album->artist->name);
+       fopt_set_str(&track_fopts[TF_ARTIST], album->artist->name);
+       fopt_set_str(&track_fopts[TF_ALBUM], album->name);
+       fopt_set_time(&track_fopts[TF_ALBUMDURATION], get_album_length(album), 0);
+}
+
+static void fill_track_fopts_artist(struct artist *artist)
+{
+       const char *name = display_artist_sort_name ? artist_sort_name(artist) : artist->name;
+       fopt_set_str(&track_fopts[TF_ARTIST], name);
+       fopt_set_str(&track_fopts[TF_ALBUMARTIST], name);
+}
+
+const struct format_option *get_global_fopts(void)
+{
+       if (player_info.ti)
+               fill_track_fopts_track_info(player_info.ti);
+
+       static const char *status_strs[] = { ".", ">", "|" };
+       static const char *cont_strs[] = { " ", "C" };
+       static const char *follow_strs[] = { " ", "F" };
+       static const char *repeat_strs[] = { " ", "R" };
+       static const char *shuffle_strs[] = { " ", "S" };
+       int buffer_fill, vol, vol_left, vol_right;
+       int duration = -1;
+
+       fopt_set_time(&track_fopts[TF_TOTAL], play_library ? lib_editable.total_time :
+                       pl_playing_total_time(), 0);
+
+       fopt_set_str(&track_fopts[TF_FOLLOW], follow_strs[follow]);
+       fopt_set_str(&track_fopts[TF_REPEAT], repeat_strs[repeat]);
+       fopt_set_str(&track_fopts[TF_SHUFFLE], shuffle_strs[shuffle]);
+       fopt_set_str(&track_fopts[TF_PLAYLISTMODE], aaa_mode_names[aaa_mode]);
+
+       if (player_info.ti)
+               duration = player_info.ti->duration;
+
+       vol_left = vol_right = vol = -1;
+       if (soft_vol) {
+               vol_left = soft_vol_l;
+               vol_right = soft_vol_r;
+               vol = (vol_left + vol_right + 1) / 2;
+       } else if (volume_max && volume_l >= 0 && volume_r >= 0) {
+               vol_left = scale_to_percentage(volume_l, volume_max);
+               vol_right = scale_to_percentage(volume_r, volume_max);
+               vol = (vol_left + vol_right + 1) / 2;
+       }
+       buffer_fill = scale_to_percentage(player_info.buffer_fill, player_info.buffer_size);
+
+       fopt_set_str(&track_fopts[TF_STATUS], status_strs[player_info.status]);
+
+       if (show_remaining_time && duration != -1) {
+               fopt_set_time(&track_fopts[TF_POSITION], player_info.pos - duration, 0);
+       } else {
+               fopt_set_time(&track_fopts[TF_POSITION], player_info.pos, 0);
+       }
+
+       fopt_set_int(&track_fopts[TF_POSITION_SEC], player_info.pos, player_info.pos < 0);
+       fopt_set_time(&track_fopts[TF_DURATION], duration, duration < 0);
+       fopt_set_int(&track_fopts[TF_VOLUME], vol, vol < 0);
+       fopt_set_int(&track_fopts[TF_LVOLUME], vol_left, vol_left < 0);
+       fopt_set_int(&track_fopts[TF_RVOLUME], vol_right, vol_right < 0);
+       fopt_set_int(&track_fopts[TF_BUFFER], buffer_fill, 0);
+       fopt_set_str(&track_fopts[TF_CONTINUE], cont_strs[player_cont]);
+       fopt_set_int(&track_fopts[TF_BITRATE], player_info.current_bitrate / 1000. + 0.5, 0);
+
+       return track_fopts;
+}
+
+static void print_tree(struct window *win, int row, struct iter *iter)
+{
+       struct artist *artist;
+       struct album *album;
+       struct iter sel;
+       int current, selected, active, pos;
+
+       artist = iter_to_artist(iter);
+       album = iter_to_album(iter);
+       current = 0;
+       if (lib_cur_track) {
+               if (album) {
+                       current = CUR_ALBUM == album;
+               } else {
+                       current = CUR_ARTIST == artist;
+               }
+       }
+       window_get_sel(win, &sel);
+       selected = iters_equal(iter, &sel);
+       active = lib_cur_win == lib_tree_win;
+       bkgdset(pairs[(active << 2) | (selected << 1) | current]);
+
+       if (active && selected) {
+               cursor_x = 0;
+               cursor_y = 1 + row;
+       }
+
+       print_buffer[0] = ' ';
+       if (album) {
+               fill_track_fopts_album(album);
+               format_print(print_buffer + 1, tree_win_w - 2, tree_win_format, track_fopts);
+       } else {
+               fill_track_fopts_artist(artist);
+               format_print(print_buffer + 1, tree_win_w - 2, tree_win_artist_format, track_fopts);
+       }
+       pos = strlen(print_buffer);
+       print_buffer[pos++] = ' ';
+       print_buffer[pos++] = 0;
+       dump_print_buffer(row + 1, tree_win_x);
+}
+
+static void print_track(struct window *win, int row, struct iter *iter)
+{
+       struct tree_track *track;
+       struct album *album;
+       struct track_info *ti;
+       struct iter sel;
+       int current, selected, active;
+       const char *format;
+
+       track = iter_to_tree_track(iter);
+       album = iter_to_album(iter);
+
+       if (track == (struct tree_track*)album) {
+               int pos;
+               struct fp_len len;
+
+               bkgdset(pairs[CURSED_TRACKWIN_ALBUM]);
+
+               fill_track_fopts_album(album);
+
+               len = format_print(print_buffer, track_win_w, track_win_album_format, track_fopts);
+               dump_print_buffer(row + 1, track_win_x);
+
+               bkgdset(pairs[CURSED_SEPARATOR]);
+               for(pos = track_win_x + len.llen; pos < COLS - len.rlen; ++pos)
+                       (void) mvaddch(row + 1, pos, ACS_HLINE);
+
+               return;
+       }
+
+       current = lib_cur_track == track;
+       window_get_sel(win, &sel);
+       selected = iters_equal(iter, &sel);
+       active = lib_cur_win == lib_track_win;
+       bkgdset(pairs[(active << 2) | (selected << 1) | current]);
+
+       if (active && selected) {
+               cursor_x = track_win_x;
+               cursor_y = 1 + row;
+       }
+
+       ti = tree_track_info(track);
+       fill_track_fopts_track_info(ti);
+
+       format = track_win_format;
+       if (track_info_has_tag(ti)) {
+               if (*track_win_format_va && track_is_compilation(ti->comments))
+                       format = track_win_format_va;
+       } else if (*track_win_alt_format) {
+               format = track_win_alt_format;
+       }
+       format_print(print_buffer, track_win_w, format, track_fopts);
+       dump_print_buffer(row + 1, track_win_x);
+}
+
+/* used by print_editable only */
+static struct simple_track *current_track;
+
+static void print_editable(struct window *win, int row, struct iter *iter)
+{
+       struct simple_track *track;
+       struct iter sel;
+       int current, selected, active;
+       const char *format;
+
+       track = iter_to_simple_track(iter);
+       current = current_track == track;
+       window_get_sel(win, &sel);
+       selected = iters_equal(iter, &sel);
+
+       if (selected) {
+               cursor_x = editable_win_x;
+               cursor_y = 1 + row;
+       }
+
+       active = editable_active;
+       if (!selected && track->marked) {
+               selected = 1;
+               active = 0;
+       }
+
+       bkgdset(pairs[(active << 2) | (selected << 1) | current]);
+
+       fill_track_fopts_track_info(track->info);
+
+       format = list_win_format;
+       if (track_info_has_tag(track->info)) {
+               if (*list_win_format_va && track_is_compilation(track->info->comments))
+                       format = list_win_format_va;
+       } else if (*list_win_alt_format) {
+               format = list_win_alt_format;
+       }
+       format_print(print_buffer, editable_win_w, format, track_fopts);
+       dump_print_buffer(row + 1, editable_win_x);
+}
+
+static void print_browser(struct window *win, int row, struct iter *iter)
+{
+       struct browser_entry *e;
+       struct iter sel;
+       int selected;
+
+       e = iter_to_browser_entry(iter);
+       window_get_sel(win, &sel);
+       selected = iters_equal(iter, &sel);
+       if (selected) {
+               int active = 1;
+               int current = 0;
+
+               bkgdset(pairs[(active << 2) | (selected << 1) | current]);
+       } else {
+               if (e->type == BROWSER_ENTRY_DIR) {
+                       bkgdset(pairs[CURSED_DIR]);
+               } else {
+                       bkgdset(pairs[CURSED_WIN]);
+               }
+       }
+
+       if (selected) {
+               cursor_x = 0;
+               cursor_y = 1 + row;
+       }
+
+       /* file name encoding == terminal encoding. no need to convert */
+       if (using_utf8) {
+               sprint(row + 1, 0, e->name, COLS);
+       } else {
+               sprint_ascii(row + 1, 0, e->name, COLS);
+       }
+}
+
+static void print_filter(struct window *win, int row, struct iter *iter)
+{
+       char buf[256];
+       struct filter_entry *e = iter_to_filter_entry(iter);
+       struct iter sel;
+       /* window active? */
+       int active = 1;
+       /* row selected? */
+       int selected;
+       /* is the filter currently active? */
+       int current = !!e->act_stat;
+       const char stat_chars[3] = " *!";
+       int ch1, ch2, ch3, pos;
+       const char *e_filter;
+
+       window_get_sel(win, &sel);
+       selected = iters_equal(iter, &sel);
+       bkgdset(pairs[(active << 2) | (selected << 1) | current]);
+
+       if (selected) {
+               cursor_x = 0;
+               cursor_y = 1 + row;
+       }
+
+       ch1 = ' ';
+       ch3 = ' ';
+       if (e->sel_stat != e->act_stat) {
+               ch1 = '[';
+               ch3 = ']';
+       }
+       ch2 = stat_chars[e->sel_stat];
+
+       e_filter = e->filter;
+       if (!using_utf8) {
+               utf8_encode_to_buf(e_filter);
+               e_filter = conv_buffer;
+       }
+
+       snprintf(buf, sizeof(buf), "%c%c%c%-15s  %s", ch1, ch2, ch3, e->name, e_filter);
+       pos = format_str(print_buffer, buf, COLS - 1);
+       print_buffer[pos++] = ' ';
+       print_buffer[pos] = 0;
+       dump_print_buffer(row + 1, 0);
+}
+
+static void print_help(struct window *win, int row, struct iter *iter)
+{
+       struct iter sel;
+       int selected;
+       int pos;
+       int active = 1;
+       char buf[OPTION_MAX_SIZE];
+       const struct help_entry *e = iter_to_help_entry(iter);
+       const struct cmus_opt *opt;
+
+       window_get_sel(win, &sel);
+       selected = iters_equal(iter, &sel);
+       bkgdset(pairs[(active << 2) | (selected << 1)]);
+
+       if (selected) {
+               cursor_x = 0;
+               cursor_y = 1 + row;
+       }
+
+       switch (e->type) {
+       case HE_TEXT:
+               snprintf(buf, sizeof(buf), " %s", e->text);
+               break;
+       case HE_BOUND:
+               snprintf(buf, sizeof(buf), " %-8s %-14s %s",
+                               key_context_names[e->binding->ctx],
+                               e->binding->key->name,
+                               e->binding->cmd);
+               break;
+       case HE_UNBOUND:
+               snprintf(buf, sizeof(buf), " %s", e->command->name);
+               break;
+       case HE_OPTION:
+               opt = e->option;
+               snprintf(buf, sizeof(buf), " %-29s ", opt->name);
+               size_t len = strlen(buf);
+               opt->get(opt->data, buf + len, sizeof(buf) - len);
+               break;
+       }
+       pos = format_str(print_buffer, buf, COLS - 1);
+       print_buffer[pos++] = ' ';
+       print_buffer[pos] = 0;
+       dump_print_buffer(row + 1, 0);
+}
+
+static void update_window(struct window *win, int x, int y, int w, const char *title,
+               void (*print)(struct window *, int, struct iter *))
+{
+       struct iter iter;
+       int nr_rows;
+       int c, i;
+
+       win->changed = 0;
+
+       bkgdset(pairs[CURSED_WIN_TITLE]);
+       c = snprintf(print_buffer, w + 1, " %s", title);
+       if (c > w)
+               c = w;
+       memset(print_buffer + c, ' ', w - c + 1);
+       print_buffer[w] = 0;
+       dump_print_buffer(y, x);
+       nr_rows = window_get_nr_rows(win);
+       i = 0;
+       if (window_get_top(win, &iter)) {
+               while (i < nr_rows) {
+                       print(win, i, &iter);
+                       i++;
+                       if (!window_get_next(win, &iter))
+                               break;
+               }
+       }
+
+       bkgdset(pairs[0]);
+       memset(print_buffer, ' ', w);
+       print_buffer[w] = 0;
+       while (i < nr_rows) {
+               dump_print_buffer(y + i + 1, x);
+               i++;
+       }
+}
+
+static void update_tree_window(void)
+{
+       update_window(lib_tree_win, tree_win_x, 0, tree_win_w,
+                       "Artist / Album", print_tree);
+}
+
+static void update_track_window(void)
+{
+       char title[512];
+
+       /* it doesn't matter what format options we use because the format
+        * string does not contain any format charaters */
+       format_print(title, track_win_w - 2, "Track%=Library", track_fopts);
+       update_window(lib_track_win, track_win_x, 0, track_win_w, title,
+                       print_track);
+}
+
+static void print_pl_list(struct window *win, int row, struct iter *iter)
+{
+       struct pl_list_info info;
+
+       pl_list_iter_to_info(iter, &info);
+
+       bkgdset(pairs[(info.active<<2) | (info.selected<<1) | info.current]);
+
+       const char *prefix = "   ";
+       if (info.marked)
+               prefix = " * ";
+       size_t prefix_w = strlen(prefix);
+       format_str(print_buffer, prefix, prefix_w);
+
+       if (tree_win_w >= prefix_w)
+               format_str(print_buffer + prefix_w, info.name,
+                               tree_win_w - prefix_w);
+
+       dump_print_buffer(row + 1, 0);
+}
+
+static void update_pl_list(struct window *win)
+{
+       update_window(win, tree_win_x, 0, tree_win_w, "Playlist",
+                       print_pl_list);
+}
+
+static void update_pl_tracks(struct window *win)
+{
+       char title[512];
+
+       editable_win_x = track_win_x;
+       editable_win_w = track_win_w;
+       editable_active = pl_get_cursor_in_track_window();
+
+       get_global_fopts();
+       fopt_set_time(&track_fopts[TF_TOTAL], pl_visible_total_time(), 0);
+
+       format_print(title, track_win_w - 2, "Track%=%{total}", track_fopts);
+       update_window(win, track_win_x, 0, track_win_w, title, print_editable);
+
+       editable_active = 1;
+       editable_win_x = 0;
+       editable_win_w = COLS;
+}
+
+static const char *pretty(const char *path)
+{
+       static int home_len = -1;
+       static char buf[256];
+
+       if (home_len == -1)
+               home_len = strlen(home_dir);
+
+       if (strncmp(path, home_dir, home_len) || path[home_len] != '/')
+               return path;
+
+       buf[0] = '~';
+       strcpy(buf + 1, path + home_len);
+       return buf;
+}
+
+static const char * const sorted_names[2] = { "", "sorted by " };
+
+static void update_editable_window(struct editable *e, const char *title, const char *filename)
+{
+       char buf[512];
+       int pos;
+
+       if (filename) {
+               if (using_utf8) {
+                       /* already UTF-8 */
+               } else {
+                       utf8_encode_to_buf(filename);
+                       filename = conv_buffer;
+               }
+               snprintf(buf, sizeof(buf), "%s %s - %d tracks", title,
+                               pretty(filename), e->nr_tracks);
+       } else {
+               snprintf(buf, sizeof(buf), "%s - %d tracks", title, e->nr_tracks);
+       }
+
+       if (e->nr_marked) {
+               pos = strlen(buf);
+               snprintf(buf + pos, sizeof(buf) - pos, " (%d marked)", e->nr_marked);
+       }
+       pos = strlen(buf);
+       snprintf(buf + pos, sizeof(buf) - pos, " %s%s",
+                       sorted_names[e->shared->sort_str[0] != 0],
+                       e->shared->sort_str);
+
+       update_window(e->shared->win, 0, 0, COLS, buf, &print_editable);
+}
+
+static void update_sorted_window(void)
+{
+       current_track = (struct simple_track *)lib_cur_track;
+       update_editable_window(&lib_editable, "Library", lib_filename);
+}
+
+static void update_play_queue_window(void)
+{
+       current_track = NULL;
+       update_editable_window(&pq_editable, "Play Queue", NULL);
+}
+
+static void update_browser_window(void)
+{
+       char title[512];
+       char *dirname;
+
+       if (using_utf8) {
+               /* already UTF-8 */
+               dirname = browser_dir;
+       } else {
+               utf8_encode_to_buf(browser_dir);
+               dirname = conv_buffer;
+       }
+       snprintf(title, sizeof(title), "Browser - %s", dirname);
+       update_window(browser_win, 0, 0, COLS, title, print_browser);
+}
+
+static void update_filters_window(void)
+{
+       update_window(filters_win, 0, 0, COLS, "Library Filters", print_filter);
+}
+
+static void update_help_window(void)
+{
+       update_window(help_win, 0, 0, COLS, "Settings", print_help);
+}
+
+static void draw_separator(void)
+{
+       int row;
+
+       bkgdset(pairs[CURSED_WIN_TITLE]);
+       (void) mvaddch(0, tree_win_w, ' ');
+       bkgdset(pairs[CURSED_SEPARATOR]);
+       for (row = 1; row < LINES - 3; row++)
+               (void) mvaddch(row, tree_win_w, ACS_VLINE);
+}
+
+static void update_pl_view(int full)
+{
+       current_track = pl_get_playing_track();
+       pl_draw(update_pl_list, update_pl_tracks, full);
+       draw_separator();
+}
+
+static void do_update_view(int full)
+{
+       cursor_x = -1;
+       cursor_y = -1;
+
+       switch (cur_view) {
+       case TREE_VIEW:
+               if (full || lib_tree_win->changed)
+                       update_tree_window();
+               if (full || lib_track_win->changed)
+                       update_track_window();
+               draw_separator();
+               update_filterline();
+               break;
+       case SORTED_VIEW:
+               update_sorted_window();
+               update_filterline();
+               break;
+       case PLAYLIST_VIEW:
+               update_pl_view(full);
+               break;
+       case QUEUE_VIEW:
+               update_play_queue_window();
+               break;
+       case BROWSER_VIEW:
+               update_browser_window();
+               break;
+       case FILTERS_VIEW:
+               update_filters_window();
+               break;
+       case HELP_VIEW:
+               update_help_window();
+               break;
+       }
+}
+
+static void do_update_statusline(void)
+{
+       format_print(print_buffer, COLS, statusline_format, get_global_fopts());
+       bkgdset(pairs[CURSED_STATUSLINE]);
+       dump_print_buffer(LINES - 2, 0);
+
+       if (player_info.error_msg)
+               error_msg("%s", player_info.error_msg);
+}
+
+static void dump_buffer(const char *buffer)
+{
+       if (using_utf8) {
+               addstr(buffer);
+       } else {
+               utf8_decode(buffer);
+               addstr(conv_buffer);
+       }
+}
+
+static void do_update_commandline(void)
+{
+       char *str;
+       int w, idx;
+       char ch;
+
+       move(LINES - 1, 0);
+       if (error_buf[0]) {
+               if (msg_is_error) {
+                       bkgdset(pairs[CURSED_ERROR]);
+               } else {
+                       bkgdset(pairs[CURSED_INFO]);
+               }
+               addstr(error_buf);
+               clrtoeol();
+               return;
+       }
+       bkgdset(pairs[CURSED_COMMANDLINE]);
+       if (input_mode == NORMAL_MODE) {
+               clrtoeol();
+               return;
+       }
+
+       str = cmdline.line;
+       if (!using_utf8) {
+               /* cmdline.line actually pretends to be UTF-8 but all non-ASCII
+                * characters are invalid UTF-8 so it really is in locale's
+                * encoding.
+                *
+                * This code should be safe because cmdline.bpos ==
+                * cmdline.cpos as every non-ASCII character is counted as one
+                * invalid UTF-8 byte.
+                *
+                * NOTE: This has nothing to do with widths of printed
+                * characters.  I.e. even if there were control characters
+                * (displayed as <xx>) there would be no problem because bpos
+                * still equals to cpos, I think.
+                */
+               utf8_encode_to_buf(cmdline.line);
+               str = conv_buffer;
+       }
+
+       /* COMMAND_MODE or SEARCH_MODE */
+       w = u_str_width(str);
+       ch = ':';
+       if (input_mode == SEARCH_MODE)
+               ch = search_direction == SEARCH_FORWARD ? '/' : '?';
+
+       if (w <= COLS - 2) {
+               addch(ch);
+               idx = u_copy_chars(print_buffer, str, &w);
+               print_buffer[idx] = 0;
+               dump_buffer(print_buffer);
+               clrtoeol();
+       } else {
+               /* keep cursor as far right as possible */
+               int skip, width, cw;
+
+               /* cursor pos (width, not chars. doesn't count the ':') */
+               cw = u_str_nwidth(str, cmdline.cpos);
+
+               skip = cw + 2 - COLS;
+               if (skip > 0) {
+                       /* skip the ':' */
+                       skip--;
+
+                       /* skip rest (if any) */
+                       idx = u_skip_chars(str, &skip);
+
+                       width = COLS;
+                       idx = u_copy_chars(print_buffer, str + idx, &width);
+                       while (width < COLS) {
+                               /* cursor is at end of the buffer
+                                * print 1, 2 or 3 spaces
+                                *
+                                * To clarify:
+                                *
+                                * If the last _skipped_ character was double-width we may need
+                                * to print 2 spaces.
+                                *
+                                * If the last _skipped_ character was invalid UTF-8 we may need
+                                * to print 3 spaces.
+                                */
+                               print_buffer[idx++] = ' ';
+                               width++;
+                       }
+                       print_buffer[idx] = 0;
+                       dump_buffer(print_buffer);
+               } else {
+                       /* print ':' + COLS - 1 chars */
+                       addch(ch);
+                       width = COLS - 1;
+                       idx = u_copy_chars(print_buffer, str, &width);
+                       print_buffer[idx] = 0;
+                       dump_buffer(print_buffer);
+               }
+       }
+}
+
+static void set_title(const char *title)
+{
+       if (!set_term_title)
+               return;
+
+       if (t_ts) {
+               printf("%s%s%s", tgoto(t_ts, 0, 0), title, t_fs);
+               fflush(stdout);
+       }
+}
+
+static void do_update_titleline(void)
+{
+       bkgdset(pairs[CURSED_TITLELINE]);
+       if (player_info.ti) {
+               int i, use_alt_format = 0;
+               char *wtitle;
+
+               fill_track_fopts_track_info(player_info.ti);
+
+               use_alt_format = !track_info_has_tag(player_info.ti);
+
+               if (is_http_url(player_info.ti->filename)) {
+                       const char *title = get_stream_title();
+
+                       if (title != NULL) {
+                               free(title_buf);
+                               title_buf = to_utf8(title, icecast_default_charset);
+                               /*
+                                * StreamTitle overrides radio station name
+                                */
+                               use_alt_format = 0;
+                               fopt_set_str(&track_fopts[TF_TITLE], title_buf);
+                       }
+               }
+
+               if (use_alt_format && *current_alt_format) {
+                       format_print(print_buffer, COLS, current_alt_format, track_fopts);
+               } else {
+                       format_print(print_buffer, COLS, current_format, track_fopts);
+               }
+               dump_print_buffer(LINES - 3, 0);
+
+               /* set window title */
+               if (use_alt_format && *window_title_alt_format) {
+                       format_print(print_buffer, print_buffer_max_width,
+                                       window_title_alt_format, track_fopts);
+               } else {
+                       format_print(print_buffer,  print_buffer_max_width,
+                                       window_title_format, track_fopts);
+               }
+
+               /* remove whitespace */
+               i = strlen(print_buffer) - 1;
+               while (i > 0 && print_buffer[i] == ' ')
+                       i--;
+               print_buffer[i + 1] = 0;
+
+               if (using_utf8) {
+                       wtitle = print_buffer;
+               } else {
+                       utf8_decode(print_buffer);
+                       wtitle = conv_buffer;
+               }
+
+               set_title(wtitle);
+       } else {
+               move(LINES - 3, 0);
+               clrtoeol();
+
+               set_title("cmus " VERSION);
+       }
+}
+
+static int cmdline_cursor_column(void)
+{
+       char *str;
+       int cw, skip, s;
+
+       str = cmdline.line;
+       if (!using_utf8) {
+               /* see do_update_commandline */
+               utf8_encode_to_buf(cmdline.line);
+               str = conv_buffer;
+       }
+
+       /* width of the text in the buffer before cursor */
+       cw = u_str_nwidth(str, cmdline.cpos);
+
+       if (1 + cw < COLS) {
+               /* whole line is visible */
+               return 1 + cw;
+       }
+
+       /* beginning of cmdline is not visible */
+
+       /* check if the first visible char in cmdline would be halved
+        * double-width character (or invalid byte <xx>) which is not possible.
+        * we need to skip the whole character and move cursor to COLS - 2
+        * column. */
+       skip = cw + 2 - COLS;
+
+       /* skip the ':' */
+       skip--;
+
+       /* skip rest */
+       s = skip;
+       u_skip_chars(str, &s);
+       if (s > skip) {
+               /* the last skipped char was double-width or <xx> */
+               return COLS - 1 - (s - skip);
+       }
+       return COLS - 1;
+}
+
+static void post_update(void)
+{
+       /* refresh makes cursor visible at least for urxvt */
+       if (input_mode == COMMAND_MODE || input_mode == SEARCH_MODE) {
+               move(LINES - 1, cmdline_cursor_column());
+               refresh();
+               curs_set(1);
+       } else {
+               if (cursor_x >= 0) {
+                       move(cursor_y, cursor_x);
+               } else {
+                       move(LINES - 1, 0);
+               }
+               refresh();
+
+               /* visible cursor is useful for screen readers */
+               if (show_cursor) {
+                       curs_set(1);
+               } else {
+                       curs_set(0);
+               }
+       }
+}
+
+static const char *get_stream_title_locked(void)
+{
+       static char stream_title[255 * 16 + 1];
+       char *ptr, *title;
+
+       ptr = strstr(player_metadata, "StreamTitle='");
+       if (ptr == NULL)
+               return NULL;
+       ptr += 13;
+       title = ptr;
+       while (*ptr) {
+               if (*ptr == '\'' && *(ptr + 1) == ';') {
+                       memcpy(stream_title, title, ptr - title);
+                       stream_title[ptr - title] = 0;
+                       return stream_title;
+               }
+               ptr++;
+       }
+       return NULL;
+}
+
+const char *get_stream_title(void)
+{
+       player_metadata_lock();
+       const char *rv = get_stream_title_locked();
+       player_metadata_unlock();
+       return rv;
+}
+
+void update_titleline(void)
+{
+       curs_set(0);
+       do_update_titleline();
+       post_update();
+}
+
+void update_full(void)
+{
+       if (!ui_initialized)
+               return;
+
+       curs_set(0);
+
+       do_update_view(1);
+       do_update_titleline();
+       do_update_statusline();
+       do_update_commandline();
+
+       post_update();
+}
+
+static void update_commandline(void)
+{
+       curs_set(0);
+       do_update_commandline();
+       post_update();
+}
+
+void update_statusline(void)
+{
+       if (!ui_initialized)
+               return;
+
+       curs_set(0);
+       do_update_statusline();
+       post_update();
+}
+
+void update_filterline(void)
+{
+       if (cur_view != TREE_VIEW && cur_view != SORTED_VIEW)
+               return;
+       if (lib_live_filter) {
+               char buf[512];
+               int w;
+               bkgdset(pairs[CURSED_STATUSLINE]);
+               snprintf(buf, sizeof(buf), "filtered: %s", lib_live_filter);
+               w = clamp(strlen(buf) + 2, COLS/4, COLS/2);
+               sprint(LINES-4, COLS-w, buf, w);
+       }
+}
+
+void info_msg(const char *format, ...)
+{
+       va_list ap;
+
+       va_start(ap, format);
+       vsnprintf(error_buf, sizeof(error_buf), format, ap);
+       va_end(ap);
+
+       if (client_fd != -1) {
+               write_all(client_fd, error_buf, strlen(error_buf));
+               write_all(client_fd, "\n", 1);
+       }
+
+       msg_is_error = 0;
+
+       update_commandline();
+}
+
+void error_msg(const char *format, ...)
+{
+       va_list ap;
+
+       strcpy(error_buf, "Error: ");
+       va_start(ap, format);
+       vsnprintf(error_buf + 7, sizeof(error_buf) - 7, format, ap);
+       va_end(ap);
+
+       d_print("%s\n", error_buf);
+       if (client_fd != -1) {
+               write_all(client_fd, error_buf, strlen(error_buf));
+               write_all(client_fd, "\n", 1);
+       }
+
+       msg_is_error = 1;
+       error_count++;
+
+       if (ui_initialized) {
+               error_time = time(NULL);
+               update_commandline();
+       } else {
+               warn("%s\n", error_buf);
+               error_buf[0] = 0;
+       }
+}
+
+int yes_no_query(const char *format, ...)
+{
+       char buffer[512];
+       va_list ap;
+       int ret = 0;
+
+       va_start(ap, format);
+       vsnprintf(buffer, sizeof(buffer), format, ap);
+       va_end(ap);
+
+       move(LINES - 1, 0);
+       bkgdset(pairs[CURSED_INFO]);
+
+       /* no need to convert buffer.
+        * it is always encoded in the right charset (assuming filenames are
+        * encoded in same charset as LC_CTYPE).
+        */
+
+       addstr(buffer);
+       clrtoeol();
+       refresh();
+
+       while (1) {
+               int ch = getch();
+
+               if (ch == ERR || ch == 0)
+                       continue;
+               if (ch == 'y')
+                       ret = 1;
+               break;
+       }
+       update_commandline();
+       return ret;
+}
+
+void search_not_found(void)
+{
+       const char *what = "Track";
+
+       if (search_restricted) {
+               switch (cur_view) {
+               case TREE_VIEW:
+                       what = "Artist/album";
+                       break;
+               case SORTED_VIEW:
+               case PLAYLIST_VIEW:
+               case QUEUE_VIEW:
+                       what = "Title";
+                       break;
+               case BROWSER_VIEW:
+                       what = "File/Directory";
+                       break;
+               case FILTERS_VIEW:
+                       what = "Filter";
+                       break;
+               case HELP_VIEW:
+                       what = "Binding/command/option";
+                       break;
+               }
+       } else {
+               switch (cur_view) {
+               case TREE_VIEW:
+               case SORTED_VIEW:
+               case PLAYLIST_VIEW:
+               case QUEUE_VIEW:
+                       what = "Track";
+                       break;
+               case BROWSER_VIEW:
+                       what = "File/Directory";
+                       break;
+               case FILTERS_VIEW:
+                       what = "Filter";
+                       break;
+               case HELP_VIEW:
+                       what = "Binding/command/option";
+                       break;
+               }
+       }
+       info_msg("%s not found: %s", what, search_str ? search_str : "");
+}
+
+void set_client_fd(int fd)
+{
+       client_fd = fd;
+}
+
+int get_client_fd(void)
+{
+       return client_fd;
+}
+
+void set_view(int view)
+{
+       if (view == cur_view)
+               return;
+
+       prev_view = cur_view;
+       cur_view = view;
+       switch (cur_view) {
+       case TREE_VIEW:
+               searchable = tree_searchable;
+               break;
+       case SORTED_VIEW:
+               searchable = lib_editable.shared->searchable;
+               break;
+       case PLAYLIST_VIEW:
+               searchable = pl_get_searchable();
+               break;
+       case QUEUE_VIEW:
+               searchable = pq_editable.shared->searchable;
+               break;
+       case BROWSER_VIEW:
+               searchable = browser_searchable;
+               break;
+       case FILTERS_VIEW:
+               searchable = filters_searchable;
+               break;
+       case HELP_VIEW:
+               searchable = help_searchable;
+               update_help_window();
+               break;
+       }
+
+       curs_set(0);
+       do_update_view(1);
+       post_update();
+}
+
+void enter_command_mode(void)
+{
+       error_buf[0] = 0;
+       error_time = 0;
+       input_mode = COMMAND_MODE;
+       update_commandline();
+}
+
+void enter_search_mode(void)
+{
+       error_buf[0] = 0;
+       error_time = 0;
+       input_mode = SEARCH_MODE;
+       search_direction = SEARCH_FORWARD;
+       update_commandline();
+}
+
+void enter_search_backward_mode(void)
+{
+       error_buf[0] = 0;
+       error_time = 0;
+       input_mode = SEARCH_MODE;
+       search_direction = SEARCH_BACKWARD;
+       update_commandline();
+}
+
+void update_colors(void)
+{
+       int i;
+
+       if (!ui_initialized)
+               return;
+
+       for (i = 0; i < NR_CURSED; i++) {
+               int bg = colors[cursed_to_bg_idx[i]];
+               int fg = colors[cursed_to_fg_idx[i]];
+               int attr = attrs[cursed_to_attr_idx[i]];
+               int pair = i + 1;
+
+               if (fg >= 8 && fg <= 15) {
+                       /* fg colors 8..15 are special (0..7 + bold) */
+                       init_pair(pair, fg & 7, bg);
+                       pairs[i] = COLOR_PAIR(pair) | (fg & BRIGHT ? A_BOLD : 0) | attr;
+               } else {
+                       init_pair(pair, fg, bg);
+                       pairs[i] = COLOR_PAIR(pair) | attr;
+               }
+       }
+}
+
+static void clear_error(void)
+{
+       time_t t = time(NULL);
+
+       /* prevent accidental clearing of error messages */
+       if (t - error_time < 2)
+               return;
+
+       if (error_buf[0]) {
+               error_time = 0;
+               error_buf[0] = 0;
+               update_commandline();
+       }
+}
+
+/* screen updates }}} */
+
+static void spawn_status_program(void)
+{
+       enum player_status status;
+       const char *stream_title = NULL;
+       char *argv[32];
+       int i;
+
+       if (status_display_program == NULL || status_display_program[0] == 0)
+               return;
+
+       status = player_info.status;
+       if (status == PLAYER_STATUS_PLAYING && player_info.ti && is_http_url(player_info.ti->filename))
+               stream_title = get_stream_title();
+
+       i = 0;
+       argv[i++] = xstrdup(status_display_program);
+
+       argv[i++] = xstrdup("status");
+       argv[i++] = xstrdup(player_status_names[status]);
+       if (player_info.ti) {
+               static const char *keys[] = {
+                       "artist", "albumartist", "album", "discnumber", "tracknumber", "title",
+                       "date", "musicbrainz_trackid", NULL
+               };
+               int j;
+
+               if (is_http_url(player_info.ti->filename)) {
+                       argv[i++] = xstrdup("url");
+               } else {
+                       argv[i++] = xstrdup("file");
+               }
+               argv[i++] = xstrdup(player_info.ti->filename);
+
+               if (track_info_has_tag(player_info.ti)) {
+                       for (j = 0; keys[j]; j++) {
+                               const char *key = keys[j];
+                               const char *val;
+
+                               if (strcmp(key, "title") == 0 && stream_title)
+                                       /*
+                                        * StreamTitle overrides radio station name
+                                        */
+                                       val = stream_title;
+                               else
+                                       val = keyvals_get_val(player_info.ti->comments, key);
+
+                               if (val) {
+                                       argv[i++] = xstrdup(key);
+                                       argv[i++] = xstrdup(val);
+                               }
+                       }
+                       if (player_info.ti->duration > 0) {
+                               char buf[32];
+                               snprintf(buf, sizeof(buf), "%d", player_info.ti->duration);
+                               argv[i++] = xstrdup("duration");
+                               argv[i++] = xstrdup(buf);
+                       }
+               } else if (stream_title) {
+                       argv[i++] = xstrdup("title");
+                       argv[i++] = xstrdup(stream_title);
+               }
+       }
+       argv[i++] = NULL;
+
+       if (spawn(argv, NULL, 0) == -1)
+               error_msg("couldn't run `%s': %s", status_display_program, strerror(errno));
+       for (i = 0; argv[i]; i++)
+               free(argv[i]);
+}
+
+static volatile sig_atomic_t ctrl_c_pressed = 0;
+
+static void sig_int(int sig)
+{
+       ctrl_c_pressed = 1;
+}
+
+static void sig_shutdown(int sig)
+{
+       cmus_running = 0;
+}
+
+static volatile sig_atomic_t needs_to_resize = 1;
+
+static void sig_winch(int sig)
+{
+       needs_to_resize = 1;
+}
+
+static int get_window_size(int *lines, int *columns)
+{
+       struct winsize ws;
+
+       if (ioctl(0, TIOCGWINSZ, &ws) == -1)
+               return -1;
+       *columns = ws.ws_col;
+       *lines = ws.ws_row;
+       return 0;
+}
+
+static void resize_tree_view(int w, int h)
+{
+       tree_win_w = w / 3;
+       track_win_w = w - tree_win_w - 1;
+       if (tree_win_w < 8)
+               tree_win_w = 8;
+       if (track_win_w < 8)
+               track_win_w = 8;
+       tree_win_x = 0;
+       track_win_x = tree_win_w + 1;
+
+       h--;
+       window_set_nr_rows(lib_tree_win, h);
+       window_set_nr_rows(lib_track_win, h);
+}
+
+static void update(void)
+{
+       int needs_view_update = 0;
+       int needs_title_update = 0;
+       int needs_status_update = 0;
+       int needs_command_update = 0;
+       int needs_spawn = 0;
+
+       if (needs_to_resize) {
+               int w, h;
+               int columns, lines;
+
+               if (get_window_size(&lines, &columns) == 0) {
+                       needs_to_resize = 0;
+#if HAVE_RESIZETERM
+                       resizeterm(lines, columns);
+#endif
+                       editable_win_w = COLS;
+                       w = COLS;
+                       h = LINES - 3;
+                       if (w < 16)
+                               w = 16;
+                       if (h < 2)
+                               h = 2;
+                       resize_tree_view(w, h);
+                       window_set_nr_rows(lib_editable.shared->win, h - 1);
+                       pl_set_nr_rows(h - 1);
+                       window_set_nr_rows(pq_editable.shared->win, h - 1);
+                       window_set_nr_rows(filters_win, h - 1);
+                       window_set_nr_rows(help_win, h - 1);
+                       window_set_nr_rows(browser_win, h - 1);
+                       needs_title_update = 1;
+                       needs_status_update = 1;
+                       needs_command_update = 1;
+               }
+               clearok(curscr, TRUE);
+               refresh();
+       }
+
+       if (player_info.status_changed)
+               mpris_playback_status_changed();
+
+       if (player_info.file_changed || player_info.metadata_changed)
+               mpris_metadata_changed();
+
+       needs_spawn = player_info.status_changed || player_info.file_changed ||
+               player_info.metadata_changed;
+
+       if (player_info.file_changed) {
+               needs_title_update = 1;
+               needs_status_update = 1;
+       }
+       if (player_info.metadata_changed)
+               needs_title_update = 1;
+       if (player_info.position_changed || player_info.status_changed)
+               needs_status_update = 1;
+       switch (cur_view) {
+       case TREE_VIEW:
+               needs_view_update += lib_tree_win->changed || lib_track_win->changed;
+               break;
+       case SORTED_VIEW:
+               needs_view_update += lib_editable.shared->win->changed;
+               break;
+       case PLAYLIST_VIEW:
+               needs_view_update += pl_needs_redraw();
+               break;
+       case QUEUE_VIEW:
+               needs_view_update += pq_editable.shared->win->changed;
+               break;
+       case BROWSER_VIEW:
+               needs_view_update += browser_win->changed;
+               break;
+       case FILTERS_VIEW:
+               needs_view_update += filters_win->changed;
+               break;
+       case HELP_VIEW:
+               needs_view_update += help_win->changed;
+               break;
+       }
+
+       /* total time changed? */
+       if (play_library) {
+               needs_status_update += lib_editable.shared->win->changed;
+               lib_editable.shared->win->changed = 0;
+       } else {
+               needs_status_update += pl_needs_redraw();
+       }
+
+       if (needs_spawn)
+               spawn_status_program();
+
+       if (needs_view_update || needs_title_update || needs_status_update || needs_command_update) {
+               curs_set(0);
+
+               if (needs_view_update)
+                       do_update_view(0);
+               if (needs_title_update)
+                       do_update_titleline();
+               if (needs_status_update)
+                       do_update_statusline();
+               if (needs_command_update)
+                       do_update_commandline();
+               post_update();
+       }
+}
+
+static void handle_ch(uchar ch)
+{
+       clear_error();
+       if (input_mode == NORMAL_MODE) {
+               normal_mode_ch(ch);
+       } else if (input_mode == COMMAND_MODE) {
+               command_mode_ch(ch);
+               update_commandline();
+       } else if (input_mode == SEARCH_MODE) {
+               search_mode_ch(ch);
+               update_commandline();
+       }
+}
+
+static void handle_escape(int c)
+{
+       clear_error();
+       if (input_mode == NORMAL_MODE) {
+               normal_mode_ch(c + 128);
+       } else if (input_mode == COMMAND_MODE) {
+               command_mode_escape(c);
+               update_commandline();
+       } else if (input_mode == SEARCH_MODE) {
+               search_mode_escape(c);
+               update_commandline();
+       }
+}
+
+static void handle_key(int key)
+{
+       clear_error();
+       if (input_mode == NORMAL_MODE) {
+               normal_mode_key(key);
+       } else if (input_mode == COMMAND_MODE) {
+               command_mode_key(key);
+               update_commandline();
+       } else if (input_mode == SEARCH_MODE) {
+               search_mode_key(key);
+               update_commandline();
+       }
+}
+
+static void handle_mouse(MEVENT *event)
+{
+#if NCURSES_MOUSE_VERSION <= 1
+       static int last_mevent;
+
+       if ((last_mevent & BUTTON1_PRESSED) && (event->bstate & REPORT_MOUSE_POSITION))
+               event->bstate = BUTTON1_RELEASED;
+       last_mevent = event->bstate;
+#endif
+
+       clear_error();
+       if (input_mode == NORMAL_MODE) {
+               normal_mode_mouse(event);
+       } else if (input_mode == COMMAND_MODE) {
+               command_mode_mouse(event);
+               update_commandline();
+       } else if (input_mode == SEARCH_MODE) {
+               search_mode_mouse(event);
+               update_commandline();
+       }
+}
+
+static void u_getch(void)
+{
+       int key;
+       int bit = 7;
+       int mask = (1 << 7);
+       uchar u, ch;
+
+       key = getch();
+       if (key == ERR || key == 0)
+               return;
+
+       if (key == KEY_MOUSE) {
+               MEVENT event;
+               if (getmouse(&event) == OK)
+                       handle_mouse(&event);
+               return;
+       }
+
+       if (key > 255) {
+               handle_key(key);
+               return;
+       }
+
+       /* escape sequence */
+       if (key == 0x1B) {
+               cbreak();
+               int e_key = getch();
+               halfdelay(5);
+               if (e_key != ERR && e_key != 0) {
+                       handle_escape(e_key);
+                       return;
+               }
+       }
+
+       ch = (unsigned char)key;
+       while (bit > 0 && ch & mask) {
+               mask >>= 1;
+               bit--;
+       }
+       if (bit == 7) {
+               /* ascii */
+               u = ch;
+       } else if (using_utf8) {
+               int count;
+
+               u = ch & ((1 << bit) - 1);
+               count = 6 - bit;
+               while (count) {
+                       key = getch();
+                       if (key == ERR || key == 0)
+                               return;
+
+                       ch = (unsigned char)key;
+                       u = (u << 6) | (ch & 63);
+                       count--;
+               }
+       } else
+               u = ch | U_INVALID_MASK;
+       handle_ch(u);
+}
+
+static void main_loop(void)
+{
+       int rc, fd_high;
+
+#define SELECT_ADD_FD(fd) do {\
+       FD_SET((fd), &set); \
+       if ((fd) > fd_high) \
+               fd_high = (fd); \
+} while(0)
+
+       fd_high = server_socket;
+       while (cmus_running) {
+               fd_set set;
+               struct timeval tv;
+               int poll_mixer = 0;
+               int i, nr_fds = 0;
+               int fds[NR_MIXER_FDS];
+               struct list_head *item;
+               struct client *client;
+
+               player_info_snapshot();
+
+               update();
+
+               /* Timeout must be so small that screen updates seem instant.
+                * Only affects changes done in other threads (player).
+                *
+                * Too small timeout makes window updates too fast (wastes CPU).
+                *
+                * Too large timeout makes status line (position) updates too slow.
+                * The timeout is accuracy of player position.
+                */
+               tv.tv_sec = 0;
+               tv.tv_usec = 0;
+
+               if (player_info.status == PLAYER_STATUS_PLAYING) {
+                       // player position updates need to be fast
+                       tv.tv_usec = 100e3;
+               }
+
+               FD_ZERO(&set);
+               SELECT_ADD_FD(0);
+               SELECT_ADD_FD(job_fd);
+               SELECT_ADD_FD(cmus_next_track_request_fd);
+               SELECT_ADD_FD(server_socket);
+               if (mpris_fd != -1)
+                       SELECT_ADD_FD(mpris_fd);
+               list_for_each_entry(client, &client_head, node) {
+                       SELECT_ADD_FD(client->fd);
+               }
+               if (!soft_vol) {
+                       nr_fds = mixer_get_fds(fds);
+                       if (nr_fds <= 0) {
+                               poll_mixer = 1;
+                               if (!tv.tv_usec)
+                                       tv.tv_usec = 500e3;
+                       }
+                       for (i = 0; i < nr_fds; i++) {
+                               BUG_ON(fds[i] <= 0);
+                               SELECT_ADD_FD(fds[i]);
+                       }
+               }
+
+               rc = select(fd_high + 1, &set, NULL, NULL, tv.tv_usec ? &tv : NULL);
+               if (poll_mixer) {
+                       int ol = volume_l;
+                       int or = volume_r;
+
+                       mixer_read_volume();
+                       if (ol != volume_l || or != volume_r) {
+                               mpris_volume_changed();
+                               update_statusline();
+                       }
+
+               }
+               if (rc <= 0) {
+                       if (ctrl_c_pressed) {
+                               handle_ch(0x03);
+                               ctrl_c_pressed = 0;
+                       }
+
+                       continue;
+               }
+
+               for (i = 0; i < nr_fds; i++) {
+                       if (FD_ISSET(fds[i], &set)) {
+                               d_print("vol changed\n");
+                               mixer_read_volume();
+                               mpris_volume_changed();
+                               update_statusline();
+                       }
+               }
+               if (FD_ISSET(server_socket, &set))
+                       server_accept();
+
+               // server_serve() can remove client from the list
+               item = client_head.next;
+               while (item != &client_head) {
+                       struct list_head *next = item->next;
+                       client = container_of(item, struct client, node);
+                       if (FD_ISSET(client->fd, &set))
+                               server_serve(client);
+                       item = next;
+               }
+
+               if (FD_ISSET(0, &set))
+                       u_getch();
+
+               if (mpris_fd != -1 && FD_ISSET(mpris_fd, &set))
+                       mpris_process();
+
+               if (FD_ISSET(job_fd, &set))
+                       job_handle();
+
+               if (FD_ISSET(cmus_next_track_request_fd, &set))
+                       cmus_provide_next_track();
+       }
+}
+
+static void init_curses(void)
+{
+       struct sigaction act;
+       char *ptr, *term;
+
+       sigemptyset(&act.sa_mask);
+       act.sa_flags = 0;
+       act.sa_handler = sig_int;
+       sigaction(SIGINT, &act, NULL);
+
+       sigemptyset(&act.sa_mask);
+       act.sa_flags = 0;
+       act.sa_handler = sig_shutdown;
+       sigaction(SIGHUP, &act, NULL);
+       sigaction(SIGTERM, &act, NULL);
+
+       sigemptyset(&act.sa_mask);
+       act.sa_flags = 0;
+       act.sa_handler = SIG_IGN;
+       sigaction(SIGPIPE, &act, NULL);
+
+       sigemptyset(&act.sa_mask);
+       act.sa_flags = 0;
+       act.sa_handler = sig_winch;
+       sigaction(SIGWINCH, &act, NULL);
+
+       initscr();
+       nodelay(stdscr, TRUE);
+       keypad(stdscr, TRUE);
+       halfdelay(5);
+       noecho();
+
+       if (has_colors()) {
+#if HAVE_USE_DEFAULT_COLORS
+               start_color();
+               use_default_colors();
+#endif
+       }
+       d_print("Number of supported colors: %d\n", COLORS);
+       ui_initialized = 1;
+
+       /* this was disabled while initializing because it needs to be
+        * called only once after all colors have been set
+        */
+       update_colors();
+
+       ptr = tcap_buffer;
+       t_ts = tgetstr("ts", &ptr);
+       t_fs = tgetstr("fs", &ptr);
+       d_print("ts: %d fs: %d\n", !!t_ts, !!t_fs);
+
+       if (!t_fs)
+               t_ts = NULL;
+
+       term = getenv("TERM");
+       if (!t_ts && term) {
+               /*
+                * Eterm:            Eterm
+                * aterm:            rxvt
+                * mlterm:           xterm
+                * terminal (xfce):  xterm
+                * urxvt:            rxvt-unicode
+                * xterm:            xterm, xterm-{,16,88,256}color
+                */
+               if (!strcmp(term, "screen")) {
+                       t_ts = "\033_";
+                       t_fs = "\033\\";
+               } else if (!strncmp(term, "xterm", 5) ||
+                          !strncmp(term, "rxvt", 4) ||
+                          !strcmp(term, "Eterm")) {
+                       /* \033]1;  change icon
+                        * \033]2;  change title
+                        * \033]0;  change both
+                        */
+                       t_ts = "\033]0;";
+                       t_fs = "\007";
+               }
+       }
+       update_mouse();
+
+       if (!getenv("ESCDELAY")) {
+               set_escdelay(default_esc_delay);
+       }
+}
+
+static void init_all(void)
+{
+       main_thread = pthread_self();
+       cmus_track_request_init();
+
+       server_init(server_address);
+
+       /* does not select output plugin */
+       player_init();
+
+       /* plugins have been loaded so we know what plugin options are available */
+       options_add();
+
+       lib_init();
+       searchable = tree_searchable;
+       cmus_init();
+       pl_init();
+       browser_init();
+       filters_init();
+       help_init();
+       cmdline_init();
+       commands_init();
+       search_mode_init();
+
+       /* almost everything must be initialized now */
+       options_load();
+       if (mpris)
+               mpris_init();
+
+       /* finally we can set the output plugin */
+       player_set_op(output_plugin);
+       if (!soft_vol)
+               mixer_open();
+
+       lib_autosave_filename = xstrjoin(cmus_config_dir, "/lib.pl");
+       play_queue_autosave_filename = xstrjoin(cmus_config_dir, "/queue.pl");
+       lib_filename = xstrdup(lib_autosave_filename);
+
+       if (error_count) {
+               char buf[16];
+               char *ret;
+
+               warn("Press <enter> to continue.");
+
+               ret = fgets(buf, sizeof(buf), stdin);
+               BUG_ON(ret == NULL);
+       }
+       help_add_all_unbound();
+
+       init_curses();
+
+       if (resume_cmus) {
+               resume_load();
+               cmus_add(play_queue_append, play_queue_autosave_filename,
+                               FILE_TYPE_PL, JOB_TYPE_QUEUE, 0, NULL);
+       } else {
+               set_view(start_view);
+       }
+
+       cmus_add(lib_add_track, lib_autosave_filename, FILE_TYPE_PL,
+                       JOB_TYPE_LIB, 0, NULL);
+
+       worker_start();
+}
+
+static void exit_all(void)
+{
+       endwin();
+
+       if (resume_cmus)
+               resume_exit();
+       options_exit();
+
+       server_exit();
+       cmus_exit();
+       if (resume_cmus)
+               cmus_save(play_queue_for_each, play_queue_autosave_filename,
+                               NULL);
+       cmus_save(lib_for_each, lib_autosave_filename, NULL);
+
+       pl_exit();
+       player_exit();
+       op_exit_plugins();
+       commands_exit();
+       search_mode_exit();
+       filters_exit();
+       help_exit();
+       browser_exit();
+       mpris_free();
+}
+
+enum {
+       FLAG_LISTEN,
+       FLAG_PLUGINS,
+       FLAG_SHOW_CURSOR,
+       FLAG_HELP,
+       FLAG_VERSION,
+       NR_FLAGS
+};
+
+static struct option options[NR_FLAGS + 1] = {
+       { 0, "listen", 1 },
+       { 0, "plugins", 0 },
+       { 0, "show-cursor", 0 },
+       { 0, "help", 0 },
+       { 0, "version", 0 },
+       { 0, NULL, 0 }
+};
+
+static const char *usage =
+"Usage: %s [OPTION]...\n"
+"Curses based music player.\n"
+"\n"
+"      --listen ADDR   listen on ADDR instead of $CMUS_SOCKET or $XDG_RUNTIME_DIR/cmus-socket\n"
+"                      ADDR is either a UNIX socket or host[:port]\n"
+"                      WARNING: using TCP/IP is insecure!\n"
+"      --plugins       list available plugins and exit\n"
+"      --show-cursor   always visible cursor\n"
+"      --help          display this help and exit\n"
+"      --version       " VERSION "\n"
+"\n"
+"Use cmus-remote to control cmus from command line.\n"
+"Report bugs to <cmus-devel@lists.sourceforge.net>.\n";
+
+int main(int argc, char *argv[])
+{
+       int list_plugins = 0;
+
+       program_name = argv[0];
+       argv++;
+       while (1) {
+               int idx;
+               char *arg;
+
+               idx = get_option(&argv, options, &arg);
+               if (idx < 0)
+                       break;
+
+               switch (idx) {
+               case FLAG_HELP:
+                       printf(usage, program_name);
+                       return 0;
+               case FLAG_VERSION:
+                       printf("cmus " VERSION
+                              "\nCopyright 2004-2006 Timo Hirvonen"
+                              "\nCopyright 2008-2016 Various Authors\n");
+                       return 0;
+               case FLAG_PLUGINS:
+                       list_plugins = 1;
+                       break;
+               case FLAG_LISTEN:
+                       server_address = xstrdup(arg);
+                       break;
+               case FLAG_SHOW_CURSOR:
+                       show_cursor = 1;
+                       break;
+               }
+       }
+
+       setlocale(LC_CTYPE, "");
+       setlocale(LC_COLLATE, "");
+       charset = getenv("CMUS_CHARSET");
+       if (!charset || !charset[0]) {
+#ifdef CODESET
+               charset = nl_langinfo(CODESET);
+#else
+               charset = "ISO-8859-1";
+#endif
+       }
+       if (strcmp(charset, "UTF-8") == 0)
+               using_utf8 = 1;
+
+       misc_init();
+       if (server_address == NULL)
+               server_address = xstrdup(cmus_socket_path);
+       debug_init();
+       d_print("charset = '%s'\n", charset);
+
+       ip_load_plugins();
+       op_load_plugins();
+       if (list_plugins) {
+               ip_dump_plugins();
+               op_dump_plugins();
+               return 0;
+       }
+       init_all();
+       main_loop();
+       exit_all();
+       return 0;
+}
diff --git a/ui_curses.h b/ui_curses.h
new file mode 100644 (file)
index 0000000..1774691
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2006 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_UI_CURSES_H
+#define CMUS_UI_CURSES_H
+
+#include "search.h"
+#include "compiler.h"
+#include "format_print.h"
+
+enum ui_input_mode {
+       NORMAL_MODE,
+       COMMAND_MODE,
+       SEARCH_MODE
+};
+
+#include <signal.h>
+
+extern volatile sig_atomic_t cmus_running;
+extern int ui_initialized;
+extern enum ui_input_mode input_mode;
+extern int cur_view;
+extern int prev_view;
+extern struct searchable *searchable;
+
+extern char *lib_filename;
+extern char *lib_ext_filename;
+extern char *pl_filename;
+extern char *play_queue_filename;
+extern char *play_queue_ext_filename;
+
+extern char *charset;
+extern int using_utf8;
+
+void update_titleline(void);
+void update_statusline(void);
+void update_filterline(void);
+void update_colors(void);
+void update_full(void);
+void info_msg(const char *format, ...) CMUS_FORMAT(1, 2);
+void error_msg(const char *format, ...) CMUS_FORMAT(1, 2);
+int yes_no_query(const char *format, ...) CMUS_FORMAT(1, 2);
+void search_not_found(void);
+void set_view(int view);
+void set_client_fd(int fd);
+int get_client_fd(void);
+void enter_command_mode(void);
+void enter_search_mode(void);
+void enter_search_backward_mode(void);
+
+int track_format_valid(const char *format);
+
+/* lock player_info ! */
+const char *get_stream_title(void);
+const struct format_option *get_global_fopts(void);
+
+int get_track_win_x(void);
+
+#endif
diff --git a/unidecomp.h b/unidecomp.h
new file mode 100644 (file)
index 0000000..55e4206
--- /dev/null
@@ -0,0 +1,869 @@
+/* This file is automatically generated. DO NOT EDIT!
+Instead, edit gen_decomp.py and re-run. */
+
+static struct {
+       uchar composed;
+       uchar base;
+} unidecomp_map[] = {
+       {   0xc0,   0x41 },     // À -> A,      ̀ (300)
+       {   0xc1,   0x41 },     // Á -> A,      ́ (301)
+       {   0xc2,   0x41 },     // Â -> A,      ̂ (302)
+       {   0xc3,   0x41 },     // Ã -> A,      ̃ (303)
+       {   0xc4,   0x41 },     // Ä -> A,      ̈ (308)
+       {   0xc5,   0x41 },     // Å -> A,      ̊ (30a)
+       {   0xc6,   0x41 },     // Æ -> A,     
+       {   0xc7,   0x43 },     // Ç -> C,      ̧ (327)
+       {   0xc8,   0x45 },     // È -> E,      ̀ (300)
+       {   0xc9,   0x45 },     // É -> E,      ́ (301)
+       {   0xca,   0x45 },     // Ê -> E,      ̂ (302)
+       {   0xcb,   0x45 },     // Ë -> E,      ̈ (308)
+       {   0xcc,   0x49 },     // Ì -> I,      ̀ (300)
+       {   0xcd,   0x49 },     // Í -> I,      ́ (301)
+       {   0xce,   0x49 },     // Î -> I,      ̂ (302)
+       {   0xcf,   0x49 },     // Ï -> I,      ̈ (308)
+       {   0xd0,   0x44 },     // Ð -> D,     
+       {   0xd1,   0x4e },     // Ñ -> N,      ̃ (303)
+       {   0xd2,   0x4f },     // Ò -> O,      ̀ (300)
+       {   0xd3,   0x4f },     // Ó -> O,      ́ (301)
+       {   0xd4,   0x4f },     // Ô -> O,      ̂ (302)
+       {   0xd5,   0x4f },     // Õ -> O,      ̃ (303)
+       {   0xd6,   0x4f },     // Ö -> O,      ̈ (308)
+       {   0xd7,   0x78 },     // × -> x,     
+       {   0xd8,   0x4f },     // Ø -> O,     
+       {   0xd9,   0x55 },     // Ù -> U,      ̀ (300)
+       {   0xda,   0x55 },     // Ú -> U,      ́ (301)
+       {   0xdb,   0x55 },     // Û -> U,      ̂ (302)
+       {   0xdc,   0x55 },     // Ü -> U,      ̈ (308)
+       {   0xdd,   0x59 },     // Ý -> Y,      ́ (301)
+       {   0xde,   0x50 },     // Þ -> P,     
+       {   0xdf,   0x42 },     // ß -> B,     
+       {   0xe0,   0x61 },     // à -> a,      ̀ (300)
+       {   0xe1,   0x61 },     // á -> a,      ́ (301)
+       {   0xe2,   0x61 },     // â -> a,      ̂ (302)
+       {   0xe3,   0x61 },     // ã -> a,      ̃ (303)
+       {   0xe4,   0x61 },     // ä -> a,      ̈ (308)
+       {   0xe5,   0x61 },     // å -> a,      ̊ (30a)
+       {   0xe6,   0x61 },     // æ -> a,     
+       {   0xe7,   0x63 },     // ç -> c,      ̧ (327)
+       {   0xe8,   0x65 },     // è -> e,      ̀ (300)
+       {   0xe9,   0x65 },     // é -> e,      ́ (301)
+       {   0xea,   0x65 },     // ê -> e,      ̂ (302)
+       {   0xeb,   0x65 },     // ë -> e,      ̈ (308)
+       {   0xec,   0x69 },     // ì -> i,      ̀ (300)
+       {   0xed,   0x69 },     // í -> i,      ́ (301)
+       {   0xee,   0x69 },     // î -> i,      ̂ (302)
+       {   0xef,   0x69 },     // ï -> i,      ̈ (308)
+       {   0xf0,   0x64 },     // ð -> d,     
+       {   0xf1,   0x6e },     // ñ -> n,      ̃ (303)
+       {   0xf2,   0x6f },     // ò -> o,      ̀ (300)
+       {   0xf3,   0x6f },     // ó -> o,      ́ (301)
+       {   0xf4,   0x6f },     // ô -> o,      ̂ (302)
+       {   0xf5,   0x6f },     // õ -> o,      ̃ (303)
+       {   0xf6,   0x6f },     // ö -> o,      ̈ (308)
+       {   0xf8,   0x6f },     // ø -> o,     
+       {   0xf9,   0x75 },     // ù -> u,      ̀ (300)
+       {   0xfa,   0x75 },     // ú -> u,      ́ (301)
+       {   0xfb,   0x75 },     // û -> u,      ̂ (302)
+       {   0xfc,   0x75 },     // ü -> u,      ̈ (308)
+       {   0xfd,   0x79 },     // ý -> y,      ́ (301)
+       {   0xfe,   0x70 },     // þ -> p,     
+       {   0xff,   0x79 },     // ÿ -> y,      ̈ (308)
+       {  0x100,   0x41 },     // Ā -> A,      ̄ (304)
+       {  0x101,   0x61 },     // ā -> a,      ̄ (304)
+       {  0x102,   0x41 },     // Ă -> A,      ̆ (306)
+       {  0x103,   0x61 },     // ă -> a,      ̆ (306)
+       {  0x104,   0x41 },     // Ą -> A,      ̨ (328)
+       {  0x105,   0x61 },     // ą -> a,      ̨ (328)
+       {  0x106,   0x43 },     // Ć -> C,      ́ (301)
+       {  0x107,   0x63 },     // ć -> c,      ́ (301)
+       {  0x108,   0x43 },     // Ĉ -> C,      ̂ (302)
+       {  0x109,   0x63 },     // ĉ -> c,      ̂ (302)
+       {  0x10a,   0x43 },     // Ċ -> C,      ̇ (307)
+       {  0x10b,   0x63 },     // ċ -> c,      ̇ (307)
+       {  0x10c,   0x43 },     // Č -> C,      ̌ (30c)
+       {  0x10d,   0x63 },     // č -> c,      ̌ (30c)
+       {  0x10e,   0x44 },     // Ď -> D,      ̌ (30c)
+       {  0x10f,   0x64 },     // ď -> d,      ̌ (30c)
+       {  0x112,   0x45 },     // Ē -> E,      ̄ (304)
+       {  0x113,   0x65 },     // ē -> e,      ̄ (304)
+       {  0x114,   0x45 },     // Ĕ -> E,      ̆ (306)
+       {  0x115,   0x65 },     // ĕ -> e,      ̆ (306)
+       {  0x116,   0x45 },     // Ė -> E,      ̇ (307)
+       {  0x117,   0x65 },     // ė -> e,      ̇ (307)
+       {  0x118,   0x45 },     // Ę -> E,      ̨ (328)
+       {  0x119,   0x65 },     // ę -> e,      ̨ (328)
+       {  0x11a,   0x45 },     // Ě -> E,      ̌ (30c)
+       {  0x11b,   0x65 },     // ě -> e,      ̌ (30c)
+       {  0x11c,   0x47 },     // Ĝ -> G,      ̂ (302)
+       {  0x11d,   0x67 },     // ĝ -> g,      ̂ (302)
+       {  0x11e,   0x47 },     // Ğ -> G,      ̆ (306)
+       {  0x11f,   0x67 },     // ğ -> g,      ̆ (306)
+       {  0x120,   0x47 },     // Ġ -> G,      ̇ (307)
+       {  0x121,   0x67 },     // ġ -> g,      ̇ (307)
+       {  0x122,   0x47 },     // Ģ -> G,      ̧ (327)
+       {  0x123,   0x67 },     // ģ -> g,      ̧ (327)
+       {  0x124,   0x48 },     // Ĥ -> H,      ̂ (302)
+       {  0x125,   0x68 },     // ĥ -> h,      ̂ (302)
+       {  0x128,   0x49 },     // Ĩ -> I,      ̃ (303)
+       {  0x129,   0x69 },     // ĩ -> i,      ̃ (303)
+       {  0x12a,   0x49 },     // Ī -> I,      ̄ (304)
+       {  0x12b,   0x69 },     // ī -> i,      ̄ (304)
+       {  0x12c,   0x49 },     // Ĭ -> I,      ̆ (306)
+       {  0x12d,   0x69 },     // ĭ -> i,      ̆ (306)
+       {  0x12e,   0x49 },     // Į -> I,      ̨ (328)
+       {  0x12f,   0x69 },     // į -> i,      ̨ (328)
+       {  0x130,   0x49 },     // İ -> I,      ̇ (307)
+       {  0x134,   0x4a },     // Ĵ -> J,      ̂ (302)
+       {  0x135,   0x6a },     // ĵ -> j,      ̂ (302)
+       {  0x136,   0x4b },     // Ķ -> K,      ̧ (327)
+       {  0x137,   0x6b },     // ķ -> k,      ̧ (327)
+       {  0x139,   0x4c },     // Ĺ -> L,      ́ (301)
+       {  0x13a,   0x6c },     // ĺ -> l,      ́ (301)
+       {  0x13b,   0x4c },     // Ļ -> L,      ̧ (327)
+       {  0x13c,   0x6c },     // ļ -> l,      ̧ (327)
+       {  0x13d,   0x4c },     // Ľ -> L,      ̌ (30c)
+       {  0x13e,   0x6c },     // ľ -> l,      ̌ (30c)
+       {  0x143,   0x4e },     // Ń -> N,      ́ (301)
+       {  0x144,   0x6e },     // ń -> n,      ́ (301)
+       {  0x145,   0x4e },     // Ņ -> N,      ̧ (327)
+       {  0x146,   0x6e },     // ņ -> n,      ̧ (327)
+       {  0x147,   0x4e },     // Ň -> N,      ̌ (30c)
+       {  0x148,   0x6e },     // ň -> n,      ̌ (30c)
+       {  0x14c,   0x4f },     // Ō -> O,      ̄ (304)
+       {  0x14d,   0x6f },     // ō -> o,      ̄ (304)
+       {  0x14e,   0x4f },     // Ŏ -> O,      ̆ (306)
+       {  0x14f,   0x6f },     // ŏ -> o,      ̆ (306)
+       {  0x150,   0x4f },     // Ő -> O,      ̋ (30b)
+       {  0x151,   0x6f },     // ő -> o,      ̋ (30b)
+       {  0x154,   0x52 },     // Ŕ -> R,      ́ (301)
+       {  0x155,   0x72 },     // ŕ -> r,      ́ (301)
+       {  0x156,   0x52 },     // Ŗ -> R,      ̧ (327)
+       {  0x157,   0x72 },     // ŗ -> r,      ̧ (327)
+       {  0x158,   0x52 },     // Ř -> R,      ̌ (30c)
+       {  0x159,   0x72 },     // ř -> r,      ̌ (30c)
+       {  0x15a,   0x53 },     // Ś -> S,      ́ (301)
+       {  0x15b,   0x73 },     // ś -> s,      ́ (301)
+       {  0x15c,   0x53 },     // Ŝ -> S,      ̂ (302)
+       {  0x15d,   0x73 },     // ŝ -> s,      ̂ (302)
+       {  0x15e,   0x53 },     // Ş -> S,      ̧ (327)
+       {  0x15f,   0x73 },     // ş -> s,      ̧ (327)
+       {  0x160,   0x53 },     // Š -> S,      ̌ (30c)
+       {  0x161,   0x73 },     // š -> s,      ̌ (30c)
+       {  0x162,   0x54 },     // Ţ -> T,      ̧ (327)
+       {  0x163,   0x74 },     // ţ -> t,      ̧ (327)
+       {  0x164,   0x54 },     // Ť -> T,      ̌ (30c)
+       {  0x165,   0x74 },     // ť -> t,      ̌ (30c)
+       {  0x168,   0x55 },     // Ũ -> U,      ̃ (303)
+       {  0x169,   0x75 },     // ũ -> u,      ̃ (303)
+       {  0x16a,   0x55 },     // Ū -> U,      ̄ (304)
+       {  0x16b,   0x75 },     // ū -> u,      ̄ (304)
+       {  0x16c,   0x55 },     // Ŭ -> U,      ̆ (306)
+       {  0x16d,   0x75 },     // ŭ -> u,      ̆ (306)
+       {  0x16e,   0x55 },     // Ů -> U,      ̊ (30a)
+       {  0x16f,   0x75 },     // ů -> u,      ̊ (30a)
+       {  0x170,   0x55 },     // Ű -> U,      ̋ (30b)
+       {  0x171,   0x75 },     // ű -> u,      ̋ (30b)
+       {  0x172,   0x55 },     // Ų -> U,      ̨ (328)
+       {  0x173,   0x75 },     // ų -> u,      ̨ (328)
+       {  0x174,   0x57 },     // Ŵ -> W,      ̂ (302)
+       {  0x175,   0x77 },     // ŵ -> w,      ̂ (302)
+       {  0x176,   0x59 },     // Ŷ -> Y,      ̂ (302)
+       {  0x177,   0x79 },     // ŷ -> y,      ̂ (302)
+       {  0x178,   0x59 },     // Ÿ -> Y,      ̈ (308)
+       {  0x179,   0x5a },     // Ź -> Z,      ́ (301)
+       {  0x17a,   0x7a },     // ź -> z,      ́ (301)
+       {  0x17b,   0x5a },     // Ż -> Z,      ̇ (307)
+       {  0x17c,   0x7a },     // ż -> z,      ̇ (307)
+       {  0x17d,   0x5a },     // Ž -> Z,      ̌ (30c)
+       {  0x17e,   0x7a },     // ž -> z,      ̌ (30c)
+       {  0x1a0,   0x4f },     // Ơ -> O,      ̛ (31b)
+       {  0x1a1,   0x6f },     // ơ -> o,      ̛ (31b)
+       {  0x1af,   0x55 },     // Ư -> U,      ̛ (31b)
+       {  0x1b0,   0x75 },     // ư -> u,      ̛ (31b)
+       {  0x1c4,   0x44 },     // DŽ -> D,      Z (5a),  ̌ (30c)
+       {  0x1c5,   0x44 },     // Dž -> D,      z (7a),  ̌ (30c)
+       {  0x1c6,   0x64 },     // dž -> d,      z (7a),  ̌ (30c)
+       {  0x1cd,   0x41 },     // Ǎ -> A,      ̌ (30c)
+       {  0x1ce,   0x61 },     // ǎ -> a,      ̌ (30c)
+       {  0x1cf,   0x49 },     // Ǐ -> I,      ̌ (30c)
+       {  0x1d0,   0x69 },     // ǐ -> i,      ̌ (30c)
+       {  0x1d1,   0x4f },     // Ǒ -> O,      ̌ (30c)
+       {  0x1d2,   0x6f },     // ǒ -> o,      ̌ (30c)
+       {  0x1d3,   0x55 },     // Ǔ -> U,      ̌ (30c)
+       {  0x1d4,   0x75 },     // ǔ -> u,      ̌ (30c)
+       {  0x1d5,   0x55 },     // Ǖ -> U,      ̈ (308),  ̄ (304)
+       {  0x1d6,   0x75 },     // ǖ -> u,      ̈ (308),  ̄ (304)
+       {  0x1d7,   0x55 },     // Ǘ -> U,      ̈ (308),  ́ (301)
+       {  0x1d8,   0x75 },     // ǘ -> u,      ̈ (308),  ́ (301)
+       {  0x1d9,   0x55 },     // Ǚ -> U,      ̈ (308),  ̌ (30c)
+       {  0x1da,   0x75 },     // ǚ -> u,      ̈ (308),  ̌ (30c)
+       {  0x1db,   0x55 },     // Ǜ -> U,      ̈ (308),  ̀ (300)
+       {  0x1dc,   0x75 },     // ǜ -> u,      ̈ (308),  ̀ (300)
+       {  0x1de,   0x41 },     // Ǟ -> A,      ̈ (308),  ̄ (304)
+       {  0x1df,   0x61 },     // ǟ -> a,      ̈ (308),  ̄ (304)
+       {  0x1e0,   0x41 },     // Ǡ -> A,      ̇ (307),  ̄ (304)
+       {  0x1e1,   0x61 },     // ǡ -> a,      ̇ (307),  ̄ (304)
+       {  0x1e2,   0x41 },     // Ǣ -> A,      ̄ (304)
+       {  0x1e3,   0x61 },     // ǣ -> a,      ̄ (304)
+       {  0x1e6,   0x47 },     // Ǧ -> G,      ̌ (30c)
+       {  0x1e7,   0x67 },     // ǧ -> g,      ̌ (30c)
+       {  0x1e8,   0x4b },     // Ǩ -> K,      ̌ (30c)
+       {  0x1e9,   0x6b },     // ǩ -> k,      ̌ (30c)
+       {  0x1ea,   0x4f },     // Ǫ -> O,      ̨ (328)
+       {  0x1eb,   0x6f },     // ǫ -> o,      ̨ (328)
+       {  0x1ec,   0x4f },     // Ǭ -> O,      ̨ (328),  ̄ (304)
+       {  0x1ed,   0x6f },     // ǭ -> o,      ̨ (328),  ̄ (304)
+       {  0x1ee,  0x1b7 },     // Ǯ -> Ʒ,     ̌ (30c)
+       {  0x1ef,  0x292 },     // ǯ -> ʒ,     ̌ (30c)
+       {  0x1f0,   0x6a },     // ǰ -> j,      ̌ (30c)
+       {  0x1f4,   0x47 },     // Ǵ -> G,      ́ (301)
+       {  0x1f5,   0x67 },     // ǵ -> g,      ́ (301)
+       {  0x1f8,   0x4e },     // Ǹ -> N,      ̀ (300)
+       {  0x1f9,   0x6e },     // ǹ -> n,      ̀ (300)
+       {  0x1fa,   0x41 },     // Ǻ -> A,      ̊ (30a),  ́ (301)
+       {  0x1fb,   0x61 },     // ǻ -> a,      ̊ (30a),  ́ (301)
+       {  0x1fc,   0x41 },     // Ǽ -> A,      ́ (301)
+       {  0x1fd,   0x61 },     // ǽ -> a,      ́ (301)
+       {  0x1fe,   0x4f },     // Ǿ -> O,      ́ (301)
+       {  0x1ff,   0x6f },     // ǿ -> o,      ́ (301)
+       {  0x200,   0x41 },     // Ȁ -> A,      ̏ (30f)
+       {  0x201,   0x61 },     // ȁ -> a,      ̏ (30f)
+       {  0x202,   0x41 },     // Ȃ -> A,      ̑ (311)
+       {  0x203,   0x61 },     // ȃ -> a,      ̑ (311)
+       {  0x204,   0x45 },     // Ȅ -> E,      ̏ (30f)
+       {  0x205,   0x65 },     // ȅ -> e,      ̏ (30f)
+       {  0x206,   0x45 },     // Ȇ -> E,      ̑ (311)
+       {  0x207,   0x65 },     // ȇ -> e,      ̑ (311)
+       {  0x208,   0x49 },     // Ȉ -> I,      ̏ (30f)
+       {  0x209,   0x69 },     // ȉ -> i,      ̏ (30f)
+       {  0x20a,   0x49 },     // Ȋ -> I,      ̑ (311)
+       {  0x20b,   0x69 },     // ȋ -> i,      ̑ (311)
+       {  0x20c,   0x4f },     // Ȍ -> O,      ̏ (30f)
+       {  0x20d,   0x6f },     // ȍ -> o,      ̏ (30f)
+       {  0x20e,   0x4f },     // Ȏ -> O,      ̑ (311)
+       {  0x20f,   0x6f },     // ȏ -> o,      ̑ (311)
+       {  0x210,   0x52 },     // Ȑ -> R,      ̏ (30f)
+       {  0x211,   0x72 },     // ȑ -> r,      ̏ (30f)
+       {  0x212,   0x52 },     // Ȓ -> R,      ̑ (311)
+       {  0x213,   0x72 },     // ȓ -> r,      ̑ (311)
+       {  0x214,   0x55 },     // Ȕ -> U,      ̏ (30f)
+       {  0x215,   0x75 },     // ȕ -> u,      ̏ (30f)
+       {  0x216,   0x55 },     // Ȗ -> U,      ̑ (311)
+       {  0x217,   0x75 },     // ȗ -> u,      ̑ (311)
+       {  0x218,   0x53 },     // Ș -> S,      ̦ (326)
+       {  0x219,   0x73 },     // ș -> s,      ̦ (326)
+       {  0x21a,   0x54 },     // Ț -> T,      ̦ (326)
+       {  0x21b,   0x74 },     // ț -> t,      ̦ (326)
+       {  0x21e,   0x48 },     // Ȟ -> H,      ̌ (30c)
+       {  0x21f,   0x68 },     // ȟ -> h,      ̌ (30c)
+       {  0x226,   0x41 },     // Ȧ -> A,      ̇ (307)
+       {  0x227,   0x61 },     // ȧ -> a,      ̇ (307)
+       {  0x228,   0x45 },     // Ȩ -> E,      ̧ (327)
+       {  0x229,   0x65 },     // ȩ -> e,      ̧ (327)
+       {  0x22a,   0x4f },     // Ȫ -> O,      ̈ (308),  ̄ (304)
+       {  0x22b,   0x6f },     // ȫ -> o,      ̈ (308),  ̄ (304)
+       {  0x22c,   0x4f },     // Ȭ -> O,      ̃ (303),  ̄ (304)
+       {  0x22d,   0x6f },     // ȭ -> o,      ̃ (303),  ̄ (304)
+       {  0x22e,   0x4f },     // Ȯ -> O,      ̇ (307)
+       {  0x22f,   0x6f },     // ȯ -> o,      ̇ (307)
+       {  0x230,   0x4f },     // Ȱ -> O,      ̇ (307),  ̄ (304)
+       {  0x231,   0x6f },     // ȱ -> o,      ̇ (307),  ̄ (304)
+       {  0x232,   0x59 },     // Ȳ -> Y,      ̄ (304)
+       {  0x233,   0x79 },     // ȳ -> y,      ̄ (304)
+       {  0x386,  0x391 },     // Ά -> Α,     ́ (301)
+       {  0x388,  0x395 },     // Έ -> Ε,     ́ (301)
+       {  0x389,  0x397 },     // Ή -> Η,     ́ (301)
+       {  0x38a,  0x399 },     // Ί -> Ι,     ́ (301)
+       {  0x38c,  0x39f },     // Ό -> Ο,     ́ (301)
+       {  0x38e,  0x3a5 },     // Ύ -> Υ,     ́ (301)
+       {  0x38f,  0x3a9 },     // Ώ -> Ω,     ́ (301)
+       {  0x390,  0x3b9 },     // ΐ -> ι,     ̈ (308),  ́ (301)
+       {  0x3aa,  0x399 },     // Ϊ -> Ι,     ̈ (308)
+       {  0x3ab,  0x3a5 },     // Ϋ -> Υ,     ̈ (308)
+       {  0x3ac,  0x3b1 },     // ά -> α,     ́ (301)
+       {  0x3ad,  0x3b5 },     // έ -> ε,     ́ (301)
+       {  0x3ae,  0x3b7 },     // ή -> η,     ́ (301)
+       {  0x3af,  0x3b9 },     // ί -> ι,     ́ (301)
+       {  0x3b0,  0x3c5 },     // ΰ -> υ,     ̈ (308),  ́ (301)
+       {  0x3ca,  0x3b9 },     // ϊ -> ι,     ̈ (308)
+       {  0x3cb,  0x3c5 },     // ϋ -> υ,     ̈ (308)
+       {  0x3cc,  0x3bf },     // ό -> ο,     ́ (301)
+       {  0x3cd,  0x3c5 },     // ύ -> υ,     ́ (301)
+       {  0x3ce,  0x3c9 },     // ώ -> ω,     ́ (301)
+       {  0x3d3,  0x3a5 },     // ϓ -> Υ,     ́ (301)
+       {  0x3d4,  0x3a5 },     // ϔ -> Υ,     ̈ (308)
+       {  0x400,  0x415 },     // Ѐ -> Е,     ̀ (300)
+       {  0x401,  0x415 },     // Ё -> Е,     ̈ (308)
+       {  0x403,  0x413 },     // Ѓ -> Г,     ́ (301)
+       {  0x407,  0x406 },     // Ї -> І,     ̈ (308)
+       {  0x40c,  0x41a },     // Ќ -> К,     ́ (301)
+       {  0x40d,  0x418 },     // Ѝ -> И,     ̀ (300)
+       {  0x40e,  0x423 },     // Ў -> У,     ̆ (306)
+       {  0x419,  0x418 },     // Й -> И,     ̆ (306)
+       {  0x439,  0x438 },     // й -> и,     ̆ (306)
+       {  0x450,  0x435 },     // ѐ -> е,     ̀ (300)
+       {  0x451,  0x435 },     // ё -> е,     ̈ (308)
+       {  0x453,  0x433 },     // ѓ -> г,     ́ (301)
+       {  0x457,  0x456 },     // ї -> і,     ̈ (308)
+       {  0x45c,  0x43a },     // ќ -> к,     ́ (301)
+       {  0x45d,  0x438 },     // ѝ -> и,     ̀ (300)
+       {  0x45e,  0x443 },     // ў -> у,     ̆ (306)
+       {  0x476,  0x474 },     // Ѷ -> Ѵ,     ̏ (30f)
+       {  0x477,  0x475 },     // ѷ -> ѵ,     ̏ (30f)
+       {  0x4c1,  0x416 },     // Ӂ -> Ж,     ̆ (306)
+       {  0x4c2,  0x436 },     // ӂ -> ж,     ̆ (306)
+       {  0x4d0,  0x410 },     // Ӑ -> А,     ̆ (306)
+       {  0x4d1,  0x430 },     // ӑ -> а,     ̆ (306)
+       {  0x4d2,  0x410 },     // Ӓ -> А,     ̈ (308)
+       {  0x4d3,  0x430 },     // ӓ -> а,     ̈ (308)
+       {  0x4d6,  0x415 },     // Ӗ -> Е,     ̆ (306)
+       {  0x4d7,  0x435 },     // ӗ -> е,     ̆ (306)
+       {  0x4da,  0x4d8 },     // Ӛ -> Ә,     ̈ (308)
+       {  0x4db,  0x4d9 },     // ӛ -> ә,     ̈ (308)
+       {  0x4dc,  0x416 },     // Ӝ -> Ж,     ̈ (308)
+       {  0x4dd,  0x436 },     // ӝ -> ж,     ̈ (308)
+       {  0x4de,  0x417 },     // Ӟ -> З,     ̈ (308)
+       {  0x4df,  0x437 },     // ӟ -> з,     ̈ (308)
+       {  0x4e2,  0x418 },     // Ӣ -> И,     ̄ (304)
+       {  0x4e3,  0x438 },     // ӣ -> и,     ̄ (304)
+       {  0x4e4,  0x418 },     // Ӥ -> И,     ̈ (308)
+       {  0x4e5,  0x438 },     // ӥ -> и,     ̈ (308)
+       {  0x4e6,  0x41e },     // Ӧ -> О,     ̈ (308)
+       {  0x4e7,  0x43e },     // ӧ -> о,     ̈ (308)
+       {  0x4ea,  0x4e8 },     // Ӫ -> Ө,     ̈ (308)
+       {  0x4eb,  0x4e9 },     // ӫ -> ө,     ̈ (308)
+       {  0x4ec,  0x42d },     // Ӭ -> Э,     ̈ (308)
+       {  0x4ed,  0x44d },     // ӭ -> э,     ̈ (308)
+       {  0x4ee,  0x423 },     // Ӯ -> У,     ̄ (304)
+       {  0x4ef,  0x443 },     // ӯ -> у,     ̄ (304)
+       {  0x4f0,  0x423 },     // Ӱ -> У,     ̈ (308)
+       {  0x4f1,  0x443 },     // ӱ -> у,     ̈ (308)
+       {  0x4f2,  0x423 },     // Ӳ -> У,     ̋ (30b)
+       {  0x4f3,  0x443 },     // ӳ -> у,     ̋ (30b)
+       {  0x4f4,  0x427 },     // Ӵ -> Ч,     ̈ (308)
+       {  0x4f5,  0x447 },     // ӵ -> ч,     ̈ (308)
+       {  0x4f8,  0x42b },     // Ӹ -> Ы,     ̈ (308)
+       {  0x4f9,  0x44b },     // ӹ -> ы,     ̈ (308)
+       { 0x1e00,   0x41 },     // Ḁ -> A,     ̥ (325)
+       { 0x1e01,   0x61 },     // ḁ -> a,     ̥ (325)
+       { 0x1e02,   0x42 },     // Ḃ -> B,     ̇ (307)
+       { 0x1e03,   0x62 },     // ḃ -> b,     ̇ (307)
+       { 0x1e04,   0x42 },     // Ḅ -> B,     ̣ (323)
+       { 0x1e05,   0x62 },     // ḅ -> b,     ̣ (323)
+       { 0x1e06,   0x42 },     // Ḇ -> B,     ̱ (331)
+       { 0x1e07,   0x62 },     // ḇ -> b,     ̱ (331)
+       { 0x1e08,   0x43 },     // Ḉ -> C,     ̧ (327),  ́ (301)
+       { 0x1e09,   0x63 },     // ḉ -> c,     ̧ (327),  ́ (301)
+       { 0x1e0a,   0x44 },     // Ḋ -> D,     ̇ (307)
+       { 0x1e0b,   0x64 },     // ḋ -> d,     ̇ (307)
+       { 0x1e0c,   0x44 },     // Ḍ -> D,     ̣ (323)
+       { 0x1e0d,   0x64 },     // ḍ -> d,     ̣ (323)
+       { 0x1e0e,   0x44 },     // Ḏ -> D,     ̱ (331)
+       { 0x1e0f,   0x64 },     // ḏ -> d,     ̱ (331)
+       { 0x1e10,   0x44 },     // Ḑ -> D,     ̧ (327)
+       { 0x1e11,   0x64 },     // ḑ -> d,     ̧ (327)
+       { 0x1e12,   0x44 },     // Ḓ -> D,     ̭ (32d)
+       { 0x1e13,   0x64 },     // ḓ -> d,     ̭ (32d)
+       { 0x1e14,   0x45 },     // Ḕ -> E,     ̄ (304),  ̀ (300)
+       { 0x1e15,   0x65 },     // ḕ -> e,     ̄ (304),  ̀ (300)
+       { 0x1e16,   0x45 },     // Ḗ -> E,     ̄ (304),  ́ (301)
+       { 0x1e17,   0x65 },     // ḗ -> e,     ̄ (304),  ́ (301)
+       { 0x1e18,   0x45 },     // Ḙ -> E,     ̭ (32d)
+       { 0x1e19,   0x65 },     // ḙ -> e,     ̭ (32d)
+       { 0x1e1a,   0x45 },     // Ḛ -> E,     ̰ (330)
+       { 0x1e1b,   0x65 },     // ḛ -> e,     ̰ (330)
+       { 0x1e1c,   0x45 },     // Ḝ -> E,     ̧ (327),  ̆ (306)
+       { 0x1e1d,   0x65 },     // ḝ -> e,     ̧ (327),  ̆ (306)
+       { 0x1e1e,   0x46 },     // Ḟ -> F,     ̇ (307)
+       { 0x1e1f,   0x66 },     // ḟ -> f,     ̇ (307)
+       { 0x1e20,   0x47 },     // Ḡ -> G,     ̄ (304)
+       { 0x1e21,   0x67 },     // ḡ -> g,     ̄ (304)
+       { 0x1e22,   0x48 },     // Ḣ -> H,     ̇ (307)
+       { 0x1e23,   0x68 },     // ḣ -> h,     ̇ (307)
+       { 0x1e24,   0x48 },     // Ḥ -> H,     ̣ (323)
+       { 0x1e25,   0x68 },     // ḥ -> h,     ̣ (323)
+       { 0x1e26,   0x48 },     // Ḧ -> H,     ̈ (308)
+       { 0x1e27,   0x68 },     // ḧ -> h,     ̈ (308)
+       { 0x1e28,   0x48 },     // Ḩ -> H,     ̧ (327)
+       { 0x1e29,   0x68 },     // ḩ -> h,     ̧ (327)
+       { 0x1e2a,   0x48 },     // Ḫ -> H,     ̮ (32e)
+       { 0x1e2b,   0x68 },     // ḫ -> h,     ̮ (32e)
+       { 0x1e2c,   0x49 },     // Ḭ -> I,     ̰ (330)
+       { 0x1e2d,   0x69 },     // ḭ -> i,     ̰ (330)
+       { 0x1e2e,   0x49 },     // Ḯ -> I,     ̈ (308),  ́ (301)
+       { 0x1e2f,   0x69 },     // ḯ -> i,     ̈ (308),  ́ (301)
+       { 0x1e30,   0x4b },     // Ḱ -> K,     ́ (301)
+       { 0x1e31,   0x6b },     // ḱ -> k,     ́ (301)
+       { 0x1e32,   0x4b },     // Ḳ -> K,     ̣ (323)
+       { 0x1e33,   0x6b },     // ḳ -> k,     ̣ (323)
+       { 0x1e34,   0x4b },     // Ḵ -> K,     ̱ (331)
+       { 0x1e35,   0x6b },     // ḵ -> k,     ̱ (331)
+       { 0x1e36,   0x4c },     // Ḷ -> L,     ̣ (323)
+       { 0x1e37,   0x6c },     // ḷ -> l,     ̣ (323)
+       { 0x1e38,   0x4c },     // Ḹ -> L,     ̣ (323),  ̄ (304)
+       { 0x1e39,   0x6c },     // ḹ -> l,     ̣ (323),  ̄ (304)
+       { 0x1e3a,   0x4c },     // Ḻ -> L,     ̱ (331)
+       { 0x1e3b,   0x6c },     // ḻ -> l,     ̱ (331)
+       { 0x1e3c,   0x4c },     // Ḽ -> L,     ̭ (32d)
+       { 0x1e3d,   0x6c },     // ḽ -> l,     ̭ (32d)
+       { 0x1e3e,   0x4d },     // Ḿ -> M,     ́ (301)
+       { 0x1e3f,   0x6d },     // ḿ -> m,     ́ (301)
+       { 0x1e40,   0x4d },     // Ṁ -> M,     ̇ (307)
+       { 0x1e41,   0x6d },     // ṁ -> m,     ̇ (307)
+       { 0x1e42,   0x4d },     // Ṃ -> M,     ̣ (323)
+       { 0x1e43,   0x6d },     // ṃ -> m,     ̣ (323)
+       { 0x1e44,   0x4e },     // Ṅ -> N,     ̇ (307)
+       { 0x1e45,   0x6e },     // ṅ -> n,     ̇ (307)
+       { 0x1e46,   0x4e },     // Ṇ -> N,     ̣ (323)
+       { 0x1e47,   0x6e },     // ṇ -> n,     ̣ (323)
+       { 0x1e48,   0x4e },     // Ṉ -> N,     ̱ (331)
+       { 0x1e49,   0x6e },     // ṉ -> n,     ̱ (331)
+       { 0x1e4a,   0x4e },     // Ṋ -> N,     ̭ (32d)
+       { 0x1e4b,   0x6e },     // ṋ -> n,     ̭ (32d)
+       { 0x1e4c,   0x4f },     // Ṍ -> O,     ̃ (303),  ́ (301)
+       { 0x1e4d,   0x6f },     // ṍ -> o,     ̃ (303),  ́ (301)
+       { 0x1e4e,   0x4f },     // Ṏ -> O,     ̃ (303),  ̈ (308)
+       { 0x1e4f,   0x6f },     // ṏ -> o,     ̃ (303),  ̈ (308)
+       { 0x1e50,   0x4f },     // Ṑ -> O,     ̄ (304),  ̀ (300)
+       { 0x1e51,   0x6f },     // ṑ -> o,     ̄ (304),  ̀ (300)
+       { 0x1e52,   0x4f },     // Ṓ -> O,     ̄ (304),  ́ (301)
+       { 0x1e53,   0x6f },     // ṓ -> o,     ̄ (304),  ́ (301)
+       { 0x1e54,   0x50 },     // Ṕ -> P,     ́ (301)
+       { 0x1e55,   0x70 },     // ṕ -> p,     ́ (301)
+       { 0x1e56,   0x50 },     // Ṗ -> P,     ̇ (307)
+       { 0x1e57,   0x70 },     // ṗ -> p,     ̇ (307)
+       { 0x1e58,   0x52 },     // Ṙ -> R,     ̇ (307)
+       { 0x1e59,   0x72 },     // ṙ -> r,     ̇ (307)
+       { 0x1e5a,   0x52 },     // Ṛ -> R,     ̣ (323)
+       { 0x1e5b,   0x72 },     // ṛ -> r,     ̣ (323)
+       { 0x1e5c,   0x52 },     // Ṝ -> R,     ̣ (323),  ̄ (304)
+       { 0x1e5d,   0x72 },     // ṝ -> r,     ̣ (323),  ̄ (304)
+       { 0x1e5e,   0x52 },     // Ṟ -> R,     ̱ (331)
+       { 0x1e5f,   0x72 },     // ṟ -> r,     ̱ (331)
+       { 0x1e60,   0x53 },     // Ṡ -> S,     ̇ (307)
+       { 0x1e61,   0x73 },     // ṡ -> s,     ̇ (307)
+       { 0x1e62,   0x53 },     // Ṣ -> S,     ̣ (323)
+       { 0x1e63,   0x73 },     // ṣ -> s,     ̣ (323)
+       { 0x1e64,   0x53 },     // Ṥ -> S,     ́ (301),  ̇ (307)
+       { 0x1e65,   0x73 },     // ṥ -> s,     ́ (301),  ̇ (307)
+       { 0x1e66,   0x53 },     // Ṧ -> S,     ̌ (30c),  ̇ (307)
+       { 0x1e67,   0x73 },     // ṧ -> s,     ̌ (30c),  ̇ (307)
+       { 0x1e68,   0x53 },     // Ṩ -> S,     ̣ (323),  ̇ (307)
+       { 0x1e69,   0x73 },     // ṩ -> s,     ̣ (323),  ̇ (307)
+       { 0x1e6a,   0x54 },     // Ṫ -> T,     ̇ (307)
+       { 0x1e6b,   0x74 },     // ṫ -> t,     ̇ (307)
+       { 0x1e6c,   0x54 },     // Ṭ -> T,     ̣ (323)
+       { 0x1e6d,   0x74 },     // ṭ -> t,     ̣ (323)
+       { 0x1e6e,   0x54 },     // Ṯ -> T,     ̱ (331)
+       { 0x1e6f,   0x74 },     // ṯ -> t,     ̱ (331)
+       { 0x1e70,   0x54 },     // Ṱ -> T,     ̭ (32d)
+       { 0x1e71,   0x74 },     // ṱ -> t,     ̭ (32d)
+       { 0x1e72,   0x55 },     // Ṳ -> U,     ̤ (324)
+       { 0x1e73,   0x75 },     // ṳ -> u,     ̤ (324)
+       { 0x1e74,   0x55 },     // Ṵ -> U,     ̰ (330)
+       { 0x1e75,   0x75 },     // ṵ -> u,     ̰ (330)
+       { 0x1e76,   0x55 },     // Ṷ -> U,     ̭ (32d)
+       { 0x1e77,   0x75 },     // ṷ -> u,     ̭ (32d)
+       { 0x1e78,   0x55 },     // Ṹ -> U,     ̃ (303),  ́ (301)
+       { 0x1e79,   0x75 },     // ṹ -> u,     ̃ (303),  ́ (301)
+       { 0x1e7a,   0x55 },     // Ṻ -> U,     ̄ (304),  ̈ (308)
+       { 0x1e7b,   0x75 },     // ṻ -> u,     ̄ (304),  ̈ (308)
+       { 0x1e7c,   0x56 },     // Ṽ -> V,     ̃ (303)
+       { 0x1e7d,   0x76 },     // ṽ -> v,     ̃ (303)
+       { 0x1e7e,   0x56 },     // Ṿ -> V,     ̣ (323)
+       { 0x1e7f,   0x76 },     // ṿ -> v,     ̣ (323)
+       { 0x1e80,   0x57 },     // Ẁ -> W,     ̀ (300)
+       { 0x1e81,   0x77 },     // ẁ -> w,     ̀ (300)
+       { 0x1e82,   0x57 },     // Ẃ -> W,     ́ (301)
+       { 0x1e83,   0x77 },     // ẃ -> w,     ́ (301)
+       { 0x1e84,   0x57 },     // Ẅ -> W,     ̈ (308)
+       { 0x1e85,   0x77 },     // ẅ -> w,     ̈ (308)
+       { 0x1e86,   0x57 },     // Ẇ -> W,     ̇ (307)
+       { 0x1e87,   0x77 },     // ẇ -> w,     ̇ (307)
+       { 0x1e88,   0x57 },     // Ẉ -> W,     ̣ (323)
+       { 0x1e89,   0x77 },     // ẉ -> w,     ̣ (323)
+       { 0x1e8a,   0x58 },     // Ẋ -> X,     ̇ (307)
+       { 0x1e8b,   0x78 },     // ẋ -> x,     ̇ (307)
+       { 0x1e8c,   0x58 },     // Ẍ -> X,     ̈ (308)
+       { 0x1e8d,   0x78 },     // ẍ -> x,     ̈ (308)
+       { 0x1e8e,   0x59 },     // Ẏ -> Y,     ̇ (307)
+       { 0x1e8f,   0x79 },     // ẏ -> y,     ̇ (307)
+       { 0x1e90,   0x5a },     // Ẑ -> Z,     ̂ (302)
+       { 0x1e91,   0x7a },     // ẑ -> z,     ̂ (302)
+       { 0x1e92,   0x5a },     // Ẓ -> Z,     ̣ (323)
+       { 0x1e93,   0x7a },     // ẓ -> z,     ̣ (323)
+       { 0x1e94,   0x5a },     // Ẕ -> Z,     ̱ (331)
+       { 0x1e95,   0x7a },     // ẕ -> z,     ̱ (331)
+       { 0x1e96,   0x68 },     // ẖ -> h,     ̱ (331)
+       { 0x1e97,   0x74 },     // ẗ -> t,     ̈ (308)
+       { 0x1e98,   0x77 },     // ẘ -> w,     ̊ (30a)
+       { 0x1e99,   0x79 },     // ẙ -> y,     ̊ (30a)
+       { 0x1e9b,   0x73 },     // ẛ -> s,     ̇ (307)
+       { 0x1ea0,   0x41 },     // Ạ -> A,     ̣ (323)
+       { 0x1ea1,   0x61 },     // ạ -> a,     ̣ (323)
+       { 0x1ea2,   0x41 },     // Ả -> A,     ̉ (309)
+       { 0x1ea3,   0x61 },     // ả -> a,     ̉ (309)
+       { 0x1ea4,   0x41 },     // Ấ -> A,     ̂ (302),  ́ (301)
+       { 0x1ea5,   0x61 },     // ấ -> a,     ̂ (302),  ́ (301)
+       { 0x1ea6,   0x41 },     // Ầ -> A,     ̂ (302),  ̀ (300)
+       { 0x1ea7,   0x61 },     // ầ -> a,     ̂ (302),  ̀ (300)
+       { 0x1ea8,   0x41 },     // Ẩ -> A,     ̂ (302),  ̉ (309)
+       { 0x1ea9,   0x61 },     // ẩ -> a,     ̂ (302),  ̉ (309)
+       { 0x1eaa,   0x41 },     // Ẫ -> A,     ̂ (302),  ̃ (303)
+       { 0x1eab,   0x61 },     // ẫ -> a,     ̂ (302),  ̃ (303)
+       { 0x1eac,   0x41 },     // Ậ -> A,     ̣ (323),  ̂ (302)
+       { 0x1ead,   0x61 },     // ậ -> a,     ̣ (323),  ̂ (302)
+       { 0x1eae,   0x41 },     // Ắ -> A,     ̆ (306),  ́ (301)
+       { 0x1eaf,   0x61 },     // ắ -> a,     ̆ (306),  ́ (301)
+       { 0x1eb0,   0x41 },     // Ằ -> A,     ̆ (306),  ̀ (300)
+       { 0x1eb1,   0x61 },     // ằ -> a,     ̆ (306),  ̀ (300)
+       { 0x1eb2,   0x41 },     // Ẳ -> A,     ̆ (306),  ̉ (309)
+       { 0x1eb3,   0x61 },     // ẳ -> a,     ̆ (306),  ̉ (309)
+       { 0x1eb4,   0x41 },     // Ẵ -> A,     ̆ (306),  ̃ (303)
+       { 0x1eb5,   0x61 },     // ẵ -> a,     ̆ (306),  ̃ (303)
+       { 0x1eb6,   0x41 },     // Ặ -> A,     ̣ (323),  ̆ (306)
+       { 0x1eb7,   0x61 },     // ặ -> a,     ̣ (323),  ̆ (306)
+       { 0x1eb8,   0x45 },     // Ẹ -> E,     ̣ (323)
+       { 0x1eb9,   0x65 },     // ẹ -> e,     ̣ (323)
+       { 0x1eba,   0x45 },     // Ẻ -> E,     ̉ (309)
+       { 0x1ebb,   0x65 },     // ẻ -> e,     ̉ (309)
+       { 0x1ebc,   0x45 },     // Ẽ -> E,     ̃ (303)
+       { 0x1ebd,   0x65 },     // ẽ -> e,     ̃ (303)
+       { 0x1ebe,   0x45 },     // Ế -> E,     ̂ (302),  ́ (301)
+       { 0x1ebf,   0x65 },     // ế -> e,     ̂ (302),  ́ (301)
+       { 0x1ec0,   0x45 },     // Ề -> E,     ̂ (302),  ̀ (300)
+       { 0x1ec1,   0x65 },     // ề -> e,     ̂ (302),  ̀ (300)
+       { 0x1ec2,   0x45 },     // Ể -> E,     ̂ (302),  ̉ (309)
+       { 0x1ec3,   0x65 },     // ể -> e,     ̂ (302),  ̉ (309)
+       { 0x1ec4,   0x45 },     // Ễ -> E,     ̂ (302),  ̃ (303)
+       { 0x1ec5,   0x65 },     // ễ -> e,     ̂ (302),  ̃ (303)
+       { 0x1ec6,   0x45 },     // Ệ -> E,     ̣ (323),  ̂ (302)
+       { 0x1ec7,   0x65 },     // ệ -> e,     ̣ (323),  ̂ (302)
+       { 0x1ec8,   0x49 },     // Ỉ -> I,     ̉ (309)
+       { 0x1ec9,   0x69 },     // ỉ -> i,     ̉ (309)
+       { 0x1eca,   0x49 },     // Ị -> I,     ̣ (323)
+       { 0x1ecb,   0x69 },     // ị -> i,     ̣ (323)
+       { 0x1ecc,   0x4f },     // Ọ -> O,     ̣ (323)
+       { 0x1ecd,   0x6f },     // ọ -> o,     ̣ (323)
+       { 0x1ece,   0x4f },     // Ỏ -> O,     ̉ (309)
+       { 0x1ecf,   0x6f },     // ỏ -> o,     ̉ (309)
+       { 0x1ed0,   0x4f },     // Ố -> O,     ̂ (302),  ́ (301)
+       { 0x1ed1,   0x6f },     // ố -> o,     ̂ (302),  ́ (301)
+       { 0x1ed2,   0x4f },     // Ồ -> O,     ̂ (302),  ̀ (300)
+       { 0x1ed3,   0x6f },     // ồ -> o,     ̂ (302),  ̀ (300)
+       { 0x1ed4,   0x4f },     // Ổ -> O,     ̂ (302),  ̉ (309)
+       { 0x1ed5,   0x6f },     // ổ -> o,     ̂ (302),  ̉ (309)
+       { 0x1ed6,   0x4f },     // Ỗ -> O,     ̂ (302),  ̃ (303)
+       { 0x1ed7,   0x6f },     // ỗ -> o,     ̂ (302),  ̃ (303)
+       { 0x1ed8,   0x4f },     // Ộ -> O,     ̣ (323),  ̂ (302)
+       { 0x1ed9,   0x6f },     // ộ -> o,     ̣ (323),  ̂ (302)
+       { 0x1eda,   0x4f },     // Ớ -> O,     ̛ (31b),  ́ (301)
+       { 0x1edb,   0x6f },     // ớ -> o,     ̛ (31b),  ́ (301)
+       { 0x1edc,   0x4f },     // Ờ -> O,     ̛ (31b),  ̀ (300)
+       { 0x1edd,   0x6f },     // ờ -> o,     ̛ (31b),  ̀ (300)
+       { 0x1ede,   0x4f },     // Ở -> O,     ̛ (31b),  ̉ (309)
+       { 0x1edf,   0x6f },     // ở -> o,     ̛ (31b),  ̉ (309)
+       { 0x1ee0,   0x4f },     // Ỡ -> O,     ̛ (31b),  ̃ (303)
+       { 0x1ee1,   0x6f },     // ỡ -> o,     ̛ (31b),  ̃ (303)
+       { 0x1ee2,   0x4f },     // Ợ -> O,     ̛ (31b),  ̣ (323)
+       { 0x1ee3,   0x6f },     // ợ -> o,     ̛ (31b),  ̣ (323)
+       { 0x1ee4,   0x55 },     // Ụ -> U,     ̣ (323)
+       { 0x1ee5,   0x75 },     // ụ -> u,     ̣ (323)
+       { 0x1ee6,   0x55 },     // Ủ -> U,     ̉ (309)
+       { 0x1ee7,   0x75 },     // ủ -> u,     ̉ (309)
+       { 0x1ee8,   0x55 },     // Ứ -> U,     ̛ (31b),  ́ (301)
+       { 0x1ee9,   0x75 },     // ứ -> u,     ̛ (31b),  ́ (301)
+       { 0x1eea,   0x55 },     // Ừ -> U,     ̛ (31b),  ̀ (300)
+       { 0x1eeb,   0x75 },     // ừ -> u,     ̛ (31b),  ̀ (300)
+       { 0x1eec,   0x55 },     // Ử -> U,     ̛ (31b),  ̉ (309)
+       { 0x1eed,   0x75 },     // ử -> u,     ̛ (31b),  ̉ (309)
+       { 0x1eee,   0x55 },     // Ữ -> U,     ̛ (31b),  ̃ (303)
+       { 0x1eef,   0x75 },     // ữ -> u,     ̛ (31b),  ̃ (303)
+       { 0x1ef0,   0x55 },     // Ự -> U,     ̛ (31b),  ̣ (323)
+       { 0x1ef1,   0x75 },     // ự -> u,     ̛ (31b),  ̣ (323)
+       { 0x1ef2,   0x59 },     // Ỳ -> Y,     ̀ (300)
+       { 0x1ef3,   0x79 },     // ỳ -> y,     ̀ (300)
+       { 0x1ef4,   0x59 },     // Ỵ -> Y,     ̣ (323)
+       { 0x1ef5,   0x79 },     // ỵ -> y,     ̣ (323)
+       { 0x1ef6,   0x59 },     // Ỷ -> Y,     ̉ (309)
+       { 0x1ef7,   0x79 },     // ỷ -> y,     ̉ (309)
+       { 0x1ef8,   0x59 },     // Ỹ -> Y,     ̃ (303)
+       { 0x1ef9,   0x79 },     // ỹ -> y,     ̃ (303)
+       { 0x1f00,  0x3b1 },     // ἀ -> α,    ̓ (313)
+       { 0x1f01,  0x3b1 },     // ἁ -> α,    ̔ (314)
+       { 0x1f02,  0x3b1 },     // ἂ -> α,    ̓ (313),  ̀ (300)
+       { 0x1f03,  0x3b1 },     // ἃ -> α,    ̔ (314),  ̀ (300)
+       { 0x1f04,  0x3b1 },     // ἄ -> α,    ̓ (313),  ́ (301)
+       { 0x1f05,  0x3b1 },     // ἅ -> α,    ̔ (314),  ́ (301)
+       { 0x1f06,  0x3b1 },     // ἆ -> α,    ̓ (313),  ͂ (342)
+       { 0x1f07,  0x3b1 },     // ἇ -> α,    ̔ (314),  ͂ (342)
+       { 0x1f08,  0x391 },     // Ἀ -> Α,    ̓ (313)
+       { 0x1f09,  0x391 },     // Ἁ -> Α,    ̔ (314)
+       { 0x1f0a,  0x391 },     // Ἂ -> Α,    ̓ (313),  ̀ (300)
+       { 0x1f0b,  0x391 },     // Ἃ -> Α,    ̔ (314),  ̀ (300)
+       { 0x1f0c,  0x391 },     // Ἄ -> Α,    ̓ (313),  ́ (301)
+       { 0x1f0d,  0x391 },     // Ἅ -> Α,    ̔ (314),  ́ (301)
+       { 0x1f0e,  0x391 },     // Ἆ -> Α,    ̓ (313),  ͂ (342)
+       { 0x1f0f,  0x391 },     // Ἇ -> Α,    ̔ (314),  ͂ (342)
+       { 0x1f10,  0x3b5 },     // ἐ -> ε,    ̓ (313)
+       { 0x1f11,  0x3b5 },     // ἑ -> ε,    ̔ (314)
+       { 0x1f12,  0x3b5 },     // ἒ -> ε,    ̓ (313),  ̀ (300)
+       { 0x1f13,  0x3b5 },     // ἓ -> ε,    ̔ (314),  ̀ (300)
+       { 0x1f14,  0x3b5 },     // ἔ -> ε,    ̓ (313),  ́ (301)
+       { 0x1f15,  0x3b5 },     // ἕ -> ε,    ̔ (314),  ́ (301)
+       { 0x1f18,  0x395 },     // Ἐ -> Ε,    ̓ (313)
+       { 0x1f19,  0x395 },     // Ἑ -> Ε,    ̔ (314)
+       { 0x1f1a,  0x395 },     // Ἒ -> Ε,    ̓ (313),  ̀ (300)
+       { 0x1f1b,  0x395 },     // Ἓ -> Ε,    ̔ (314),  ̀ (300)
+       { 0x1f1c,  0x395 },     // Ἔ -> Ε,    ̓ (313),  ́ (301)
+       { 0x1f1d,  0x395 },     // Ἕ -> Ε,    ̔ (314),  ́ (301)
+       { 0x1f20,  0x3b7 },     // ἠ -> η,    ̓ (313)
+       { 0x1f21,  0x3b7 },     // ἡ -> η,    ̔ (314)
+       { 0x1f22,  0x3b7 },     // ἢ -> η,    ̓ (313),  ̀ (300)
+       { 0x1f23,  0x3b7 },     // ἣ -> η,    ̔ (314),  ̀ (300)
+       { 0x1f24,  0x3b7 },     // ἤ -> η,    ̓ (313),  ́ (301)
+       { 0x1f25,  0x3b7 },     // ἥ -> η,    ̔ (314),  ́ (301)
+       { 0x1f26,  0x3b7 },     // ἦ -> η,    ̓ (313),  ͂ (342)
+       { 0x1f27,  0x3b7 },     // ἧ -> η,    ̔ (314),  ͂ (342)
+       { 0x1f28,  0x397 },     // Ἠ -> Η,    ̓ (313)
+       { 0x1f29,  0x397 },     // Ἡ -> Η,    ̔ (314)
+       { 0x1f2a,  0x397 },     // Ἢ -> Η,    ̓ (313),  ̀ (300)
+       { 0x1f2b,  0x397 },     // Ἣ -> Η,    ̔ (314),  ̀ (300)
+       { 0x1f2c,  0x397 },     // Ἤ -> Η,    ̓ (313),  ́ (301)
+       { 0x1f2d,  0x397 },     // Ἥ -> Η,    ̔ (314),  ́ (301)
+       { 0x1f2e,  0x397 },     // Ἦ -> Η,    ̓ (313),  ͂ (342)
+       { 0x1f2f,  0x397 },     // Ἧ -> Η,    ̔ (314),  ͂ (342)
+       { 0x1f30,  0x3b9 },     // ἰ -> ι,    ̓ (313)
+       { 0x1f31,  0x3b9 },     // ἱ -> ι,    ̔ (314)
+       { 0x1f32,  0x3b9 },     // ἲ -> ι,    ̓ (313),  ̀ (300)
+       { 0x1f33,  0x3b9 },     // ἳ -> ι,    ̔ (314),  ̀ (300)
+       { 0x1f34,  0x3b9 },     // ἴ -> ι,    ̓ (313),  ́ (301)
+       { 0x1f35,  0x3b9 },     // ἵ -> ι,    ̔ (314),  ́ (301)
+       { 0x1f36,  0x3b9 },     // ἶ -> ι,    ̓ (313),  ͂ (342)
+       { 0x1f37,  0x3b9 },     // ἷ -> ι,    ̔ (314),  ͂ (342)
+       { 0x1f38,  0x399 },     // Ἰ -> Ι,    ̓ (313)
+       { 0x1f39,  0x399 },     // Ἱ -> Ι,    ̔ (314)
+       { 0x1f3a,  0x399 },     // Ἲ -> Ι,    ̓ (313),  ̀ (300)
+       { 0x1f3b,  0x399 },     // Ἳ -> Ι,    ̔ (314),  ̀ (300)
+       { 0x1f3c,  0x399 },     // Ἴ -> Ι,    ̓ (313),  ́ (301)
+       { 0x1f3d,  0x399 },     // Ἵ -> Ι,    ̔ (314),  ́ (301)
+       { 0x1f3e,  0x399 },     // Ἶ -> Ι,    ̓ (313),  ͂ (342)
+       { 0x1f3f,  0x399 },     // Ἷ -> Ι,    ̔ (314),  ͂ (342)
+       { 0x1f40,  0x3bf },     // ὀ -> ο,    ̓ (313)
+       { 0x1f41,  0x3bf },     // ὁ -> ο,    ̔ (314)
+       { 0x1f42,  0x3bf },     // ὂ -> ο,    ̓ (313),  ̀ (300)
+       { 0x1f43,  0x3bf },     // ὃ -> ο,    ̔ (314),  ̀ (300)
+       { 0x1f44,  0x3bf },     // ὄ -> ο,    ̓ (313),  ́ (301)
+       { 0x1f45,  0x3bf },     // ὅ -> ο,    ̔ (314),  ́ (301)
+       { 0x1f48,  0x39f },     // Ὀ -> Ο,    ̓ (313)
+       { 0x1f49,  0x39f },     // Ὁ -> Ο,    ̔ (314)
+       { 0x1f4a,  0x39f },     // Ὂ -> Ο,    ̓ (313),  ̀ (300)
+       { 0x1f4b,  0x39f },     // Ὃ -> Ο,    ̔ (314),  ̀ (300)
+       { 0x1f4c,  0x39f },     // Ὄ -> Ο,    ̓ (313),  ́ (301)
+       { 0x1f4d,  0x39f },     // Ὅ -> Ο,    ̔ (314),  ́ (301)
+       { 0x1f50,  0x3c5 },     // ὐ -> υ,    ̓ (313)
+       { 0x1f51,  0x3c5 },     // ὑ -> υ,    ̔ (314)
+       { 0x1f52,  0x3c5 },     // ὒ -> υ,    ̓ (313),  ̀ (300)
+       { 0x1f53,  0x3c5 },     // ὓ -> υ,    ̔ (314),  ̀ (300)
+       { 0x1f54,  0x3c5 },     // ὔ -> υ,    ̓ (313),  ́ (301)
+       { 0x1f55,  0x3c5 },     // ὕ -> υ,    ̔ (314),  ́ (301)
+       { 0x1f56,  0x3c5 },     // ὖ -> υ,    ̓ (313),  ͂ (342)
+       { 0x1f57,  0x3c5 },     // ὗ -> υ,    ̔ (314),  ͂ (342)
+       { 0x1f59,  0x3a5 },     // Ὑ -> Υ,    ̔ (314)
+       { 0x1f5b,  0x3a5 },     // Ὓ -> Υ,    ̔ (314),  ̀ (300)
+       { 0x1f5d,  0x3a5 },     // Ὕ -> Υ,    ̔ (314),  ́ (301)
+       { 0x1f5f,  0x3a5 },     // Ὗ -> Υ,    ̔ (314),  ͂ (342)
+       { 0x1f60,  0x3c9 },     // ὠ -> ω,    ̓ (313)
+       { 0x1f61,  0x3c9 },     // ὡ -> ω,    ̔ (314)
+       { 0x1f62,  0x3c9 },     // ὢ -> ω,    ̓ (313),  ̀ (300)
+       { 0x1f63,  0x3c9 },     // ὣ -> ω,    ̔ (314),  ̀ (300)
+       { 0x1f64,  0x3c9 },     // ὤ -> ω,    ̓ (313),  ́ (301)
+       { 0x1f65,  0x3c9 },     // ὥ -> ω,    ̔ (314),  ́ (301)
+       { 0x1f66,  0x3c9 },     // ὦ -> ω,    ̓ (313),  ͂ (342)
+       { 0x1f67,  0x3c9 },     // ὧ -> ω,    ̔ (314),  ͂ (342)
+       { 0x1f68,  0x3a9 },     // Ὠ -> Ω,    ̓ (313)
+       { 0x1f69,  0x3a9 },     // Ὡ -> Ω,    ̔ (314)
+       { 0x1f6a,  0x3a9 },     // Ὢ -> Ω,    ̓ (313),  ̀ (300)
+       { 0x1f6b,  0x3a9 },     // Ὣ -> Ω,    ̔ (314),  ̀ (300)
+       { 0x1f6c,  0x3a9 },     // Ὤ -> Ω,    ̓ (313),  ́ (301)
+       { 0x1f6d,  0x3a9 },     // Ὥ -> Ω,    ̔ (314),  ́ (301)
+       { 0x1f6e,  0x3a9 },     // Ὦ -> Ω,    ̓ (313),  ͂ (342)
+       { 0x1f6f,  0x3a9 },     // Ὧ -> Ω,    ̔ (314),  ͂ (342)
+       { 0x1f70,  0x3b1 },     // ὰ -> α,    ̀ (300)
+       { 0x1f71,  0x3b1 },     // ά -> α,    ́ (301)
+       { 0x1f72,  0x3b5 },     // ὲ -> ε,    ̀ (300)
+       { 0x1f73,  0x3b5 },     // έ -> ε,    ́ (301)
+       { 0x1f74,  0x3b7 },     // ὴ -> η,    ̀ (300)
+       { 0x1f75,  0x3b7 },     // ή -> η,    ́ (301)
+       { 0x1f76,  0x3b9 },     // ὶ -> ι,    ̀ (300)
+       { 0x1f77,  0x3b9 },     // ί -> ι,    ́ (301)
+       { 0x1f78,  0x3bf },     // ὸ -> ο,    ̀ (300)
+       { 0x1f79,  0x3bf },     // ό -> ο,    ́ (301)
+       { 0x1f7a,  0x3c5 },     // ὺ -> υ,    ̀ (300)
+       { 0x1f7b,  0x3c5 },     // ύ -> υ,    ́ (301)
+       { 0x1f7c,  0x3c9 },     // ὼ -> ω,    ̀ (300)
+       { 0x1f7d,  0x3c9 },     // ώ -> ω,    ́ (301)
+       { 0x1f80,  0x3b1 },     // ᾀ -> α,    ̓ (313),  ͅ (345)
+       { 0x1f81,  0x3b1 },     // ᾁ -> α,    ̔ (314),  ͅ (345)
+       { 0x1f82,  0x3b1 },     // ᾂ -> α,    ̓ (313),  ̀ (300),  ͅ (345)
+       { 0x1f83,  0x3b1 },     // ᾃ -> α,    ̔ (314),  ̀ (300),  ͅ (345)
+       { 0x1f84,  0x3b1 },     // ᾄ -> α,    ̓ (313),  ́ (301),  ͅ (345)
+       { 0x1f85,  0x3b1 },     // ᾅ -> α,    ̔ (314),  ́ (301),  ͅ (345)
+       { 0x1f86,  0x3b1 },     // ᾆ -> α,    ̓ (313),  ͂ (342),  ͅ (345)
+       { 0x1f87,  0x3b1 },     // ᾇ -> α,    ̔ (314),  ͂ (342),  ͅ (345)
+       { 0x1f88,  0x391 },     // ᾈ -> Α,    ̓ (313),  ͅ (345)
+       { 0x1f89,  0x391 },     // ᾉ -> Α,    ̔ (314),  ͅ (345)
+       { 0x1f8a,  0x391 },     // ᾊ -> Α,    ̓ (313),  ̀ (300),  ͅ (345)
+       { 0x1f8b,  0x391 },     // ᾋ -> Α,    ̔ (314),  ̀ (300),  ͅ (345)
+       { 0x1f8c,  0x391 },     // ᾌ -> Α,    ̓ (313),  ́ (301),  ͅ (345)
+       { 0x1f8d,  0x391 },     // ᾍ -> Α,    ̔ (314),  ́ (301),  ͅ (345)
+       { 0x1f8e,  0x391 },     // ᾎ -> Α,    ̓ (313),  ͂ (342),  ͅ (345)
+       { 0x1f8f,  0x391 },     // ᾏ -> Α,    ̔ (314),  ͂ (342),  ͅ (345)
+       { 0x1f90,  0x3b7 },     // ᾐ -> η,    ̓ (313),  ͅ (345)
+       { 0x1f91,  0x3b7 },     // ᾑ -> η,    ̔ (314),  ͅ (345)
+       { 0x1f92,  0x3b7 },     // ᾒ -> η,    ̓ (313),  ̀ (300),  ͅ (345)
+       { 0x1f93,  0x3b7 },     // ᾓ -> η,    ̔ (314),  ̀ (300),  ͅ (345)
+       { 0x1f94,  0x3b7 },     // ᾔ -> η,    ̓ (313),  ́ (301),  ͅ (345)
+       { 0x1f95,  0x3b7 },     // ᾕ -> η,    ̔ (314),  ́ (301),  ͅ (345)
+       { 0x1f96,  0x3b7 },     // ᾖ -> η,    ̓ (313),  ͂ (342),  ͅ (345)
+       { 0x1f97,  0x3b7 },     // ᾗ -> η,    ̔ (314),  ͂ (342),  ͅ (345)
+       { 0x1f98,  0x397 },     // ᾘ -> Η,    ̓ (313),  ͅ (345)
+       { 0x1f99,  0x397 },     // ᾙ -> Η,    ̔ (314),  ͅ (345)
+       { 0x1f9a,  0x397 },     // ᾚ -> Η,    ̓ (313),  ̀ (300),  ͅ (345)
+       { 0x1f9b,  0x397 },     // ᾛ -> Η,    ̔ (314),  ̀ (300),  ͅ (345)
+       { 0x1f9c,  0x397 },     // ᾜ -> Η,    ̓ (313),  ́ (301),  ͅ (345)
+       { 0x1f9d,  0x397 },     // ᾝ -> Η,    ̔ (314),  ́ (301),  ͅ (345)
+       { 0x1f9e,  0x397 },     // ᾞ -> Η,    ̓ (313),  ͂ (342),  ͅ (345)
+       { 0x1f9f,  0x397 },     // ᾟ -> Η,    ̔ (314),  ͂ (342),  ͅ (345)
+       { 0x1fa0,  0x3c9 },     // ᾠ -> ω,    ̓ (313),  ͅ (345)
+       { 0x1fa1,  0x3c9 },     // ᾡ -> ω,    ̔ (314),  ͅ (345)
+       { 0x1fa2,  0x3c9 },     // ᾢ -> ω,    ̓ (313),  ̀ (300),  ͅ (345)
+       { 0x1fa3,  0x3c9 },     // ᾣ -> ω,    ̔ (314),  ̀ (300),  ͅ (345)
+       { 0x1fa4,  0x3c9 },     // ᾤ -> ω,    ̓ (313),  ́ (301),  ͅ (345)
+       { 0x1fa5,  0x3c9 },     // ᾥ -> ω,    ̔ (314),  ́ (301),  ͅ (345)
+       { 0x1fa6,  0x3c9 },     // ᾦ -> ω,    ̓ (313),  ͂ (342),  ͅ (345)
+       { 0x1fa7,  0x3c9 },     // ᾧ -> ω,    ̔ (314),  ͂ (342),  ͅ (345)
+       { 0x1fa8,  0x3a9 },     // ᾨ -> Ω,    ̓ (313),  ͅ (345)
+       { 0x1fa9,  0x3a9 },     // ᾩ -> Ω,    ̔ (314),  ͅ (345)
+       { 0x1faa,  0x3a9 },     // ᾪ -> Ω,    ̓ (313),  ̀ (300),  ͅ (345)
+       { 0x1fab,  0x3a9 },     // ᾫ -> Ω,    ̔ (314),  ̀ (300),  ͅ (345)
+       { 0x1fac,  0x3a9 },     // ᾬ -> Ω,    ̓ (313),  ́ (301),  ͅ (345)
+       { 0x1fad,  0x3a9 },     // ᾭ -> Ω,    ̔ (314),  ́ (301),  ͅ (345)
+       { 0x1fae,  0x3a9 },     // ᾮ -> Ω,    ̓ (313),  ͂ (342),  ͅ (345)
+       { 0x1faf,  0x3a9 },     // ᾯ -> Ω,    ̔ (314),  ͂ (342),  ͅ (345)
+       { 0x1fb0,  0x3b1 },     // ᾰ -> α,    ̆ (306)
+       { 0x1fb1,  0x3b1 },     // ᾱ -> α,    ̄ (304)
+       { 0x1fb2,  0x3b1 },     // ᾲ -> α,    ̀ (300),  ͅ (345)
+       { 0x1fb3,  0x3b1 },     // ᾳ -> α,    ͅ (345)
+       { 0x1fb4,  0x3b1 },     // ᾴ -> α,    ́ (301),  ͅ (345)
+       { 0x1fb6,  0x3b1 },     // ᾶ -> α,    ͂ (342)
+       { 0x1fb7,  0x3b1 },     // ᾷ -> α,    ͂ (342),  ͅ (345)
+       { 0x1fb8,  0x391 },     // Ᾰ -> Α,    ̆ (306)
+       { 0x1fb9,  0x391 },     // Ᾱ -> Α,    ̄ (304)
+       { 0x1fba,  0x391 },     // Ὰ -> Α,    ̀ (300)
+       { 0x1fbb,  0x391 },     // Ά -> Α,    ́ (301)
+       { 0x1fbc,  0x391 },     // ᾼ -> Α,    ͅ (345)
+       { 0x1fc2,  0x3b7 },     // ῂ -> η,    ̀ (300),  ͅ (345)
+       { 0x1fc3,  0x3b7 },     // ῃ -> η,    ͅ (345)
+       { 0x1fc4,  0x3b7 },     // ῄ -> η,    ́ (301),  ͅ (345)
+       { 0x1fc6,  0x3b7 },     // ῆ -> η,    ͂ (342)
+       { 0x1fc7,  0x3b7 },     // ῇ -> η,    ͂ (342),  ͅ (345)
+       { 0x1fc8,  0x395 },     // Ὲ -> Ε,    ̀ (300)
+       { 0x1fc9,  0x395 },     // Έ -> Ε,    ́ (301)
+       { 0x1fca,  0x397 },     // Ὴ -> Η,    ̀ (300)
+       { 0x1fcb,  0x397 },     // Ή -> Η,    ́ (301)
+       { 0x1fcc,  0x397 },     // ῌ -> Η,    ͅ (345)
+       { 0x1fd0,  0x3b9 },     // ῐ -> ι,    ̆ (306)
+       { 0x1fd1,  0x3b9 },     // ῑ -> ι,    ̄ (304)
+       { 0x1fd2,  0x3b9 },     // ῒ -> ι,    ̈ (308),  ̀ (300)
+       { 0x1fd3,  0x3b9 },     // ΐ -> ι,    ̈ (308),  ́ (301)
+       { 0x1fd6,  0x3b9 },     // ῖ -> ι,    ͂ (342)
+       { 0x1fd7,  0x3b9 },     // ῗ -> ι,    ̈ (308),  ͂ (342)
+       { 0x1fd8,  0x399 },     // Ῐ -> Ι,    ̆ (306)
+       { 0x1fd9,  0x399 },     // Ῑ -> Ι,    ̄ (304)
+       { 0x1fda,  0x399 },     // Ὶ -> Ι,    ̀ (300)
+       { 0x1fdb,  0x399 },     // Ί -> Ι,    ́ (301)
+       { 0x1fe0,  0x3c5 },     // ῠ -> υ,    ̆ (306)
+       { 0x1fe1,  0x3c5 },     // ῡ -> υ,    ̄ (304)
+       { 0x1fe2,  0x3c5 },     // ῢ -> υ,    ̈ (308),  ̀ (300)
+       { 0x1fe3,  0x3c5 },     // ΰ -> υ,    ̈ (308),  ́ (301)
+       { 0x1fe4,  0x3c1 },     // ῤ -> ρ,    ̓ (313)
+       { 0x1fe5,  0x3c1 },     // ῥ -> ρ,    ̔ (314)
+       { 0x1fe6,  0x3c5 },     // ῦ -> υ,    ͂ (342)
+       { 0x1fe7,  0x3c5 },     // ῧ -> υ,    ̈ (308),  ͂ (342)
+       { 0x1fe8,  0x3a5 },     // Ῠ -> Υ,    ̆ (306)
+       { 0x1fe9,  0x3a5 },     // Ῡ -> Υ,    ̄ (304)
+       { 0x1fea,  0x3a5 },     // Ὺ -> Υ,    ̀ (300)
+       { 0x1feb,  0x3a5 },     // Ύ -> Υ,    ́ (301)
+       { 0x1fec,  0x3a1 },     // Ῥ -> Ρ,    ̔ (314)
+       { 0x1ff2,  0x3c9 },     // ῲ -> ω,    ̀ (300),  ͅ (345)
+       { 0x1ff3,  0x3c9 },     // ῳ -> ω,    ͅ (345)
+       { 0x1ff4,  0x3c9 },     // ῴ -> ω,    ́ (301),  ͅ (345)
+       { 0x1ff6,  0x3c9 },     // ῶ -> ω,    ͂ (342)
+       { 0x1ff7,  0x3c9 },     // ῷ -> ω,    ͂ (342),  ͅ (345)
+       { 0x1ff8,  0x39f },     // Ὸ -> Ο,    ̀ (300)
+       { 0x1ff9,  0x39f },     // Ό -> Ο,    ́ (301)
+       { 0x1ffa,  0x3a9 },     // Ὼ -> Ω,    ̀ (300)
+       { 0x1ffb,  0x3a9 },     // Ώ -> Ω,    ́ (301)
+       { 0x1ffc,  0x3a9 },     // ῼ -> Ω,    ͅ (345)
+       { 0x2010,   0x2d },     // ‐ -> -,    
+       { 0x2012,   0x2d },     // ‒ -> -,    
+       { 0x2013,   0x2d },     // – -> -,    
+       { 0x2014,   0x2d },     // — -> -,    
+       { 0x2015,   0x2d },     // ― -> -,    
+       { 0x2018,   0x27 },     // ‘ -> ',    
+       { 0x2019,   0x27 },     // ’ -> ',    
+       { 0x201c,   0x22 },     // “ -> ",    
+       { 0x201d,   0x22 },     // ” -> ",    
+       { 0x2026,   0x2e },     // … -> .,    
+       { 0x2032,   0x27 },     // ′ -> ',    
+       { 0x2033,   0x22 },     // ″ -> ",    
+       { 0x212b,   0x41 },     // Å -> A,     ̊ (30a)
+       { 0x219a, 0x2190 },     // ↚ -> ←,   ̸ (338)
+       { 0x219b, 0x2192 },     // ↛ -> →,   ̸ (338)
+       { 0x21ae, 0x2194 },     // ↮ -> ↔,   ̸ (338)
+       { 0x21cd, 0x21d0 },     // ⇍ -> ⇐,   ̸ (338)
+       { 0x21ce, 0x21d4 },     // ⇎ -> ⇔,   ̸ (338)
+       { 0x21cf, 0x21d2 },     // ⇏ -> ⇒,   ̸ (338)
+       { 0x2204, 0x2203 },     // ∄ -> ∃,   ̸ (338)
+       { 0x2209, 0x2208 },     // ∉ -> ∈,   ̸ (338)
+       { 0x220c, 0x220b },     // ∌ -> ∋,   ̸ (338)
+       { 0x2212,   0x2d },     // − -> -,    
+       { 0x2224, 0x2223 },     // ∤ -> ∣,   ̸ (338)
+       { 0x2226, 0x2225 },     // ∦ -> ∥,   ̸ (338)
+       { 0x2241, 0x223c },     // ≁ -> ∼,   ̸ (338)
+       { 0x2244, 0x2243 },     // ≄ -> ≃,   ̸ (338)
+       { 0x2247, 0x2245 },     // ≇ -> ≅,   ̸ (338)
+       { 0x2249, 0x2248 },     // ≉ -> ≈,   ̸ (338)
+       { 0x2260,   0x3d },     // ≠ -> =,     ̸ (338)
+       { 0x2262, 0x2261 },     // ≢ -> ≡,   ̸ (338)
+       { 0x226d, 0x224d },     // ≭ -> ≍,   ̸ (338)
+       { 0x226e,   0x3c },     // ≮ -> <,     ̸ (338)
+       { 0x226f,   0x3e },     // ≯ -> >,     ̸ (338)
+       { 0x2270, 0x2264 },     // ≰ -> ≤,   ̸ (338)
+       { 0x2271, 0x2265 },     // ≱ -> ≥,   ̸ (338)
+       { 0x2274, 0x2272 },     // ≴ -> ≲,   ̸ (338)
+       { 0x2275, 0x2273 },     // ≵ -> ≳,   ̸ (338)
+       { 0x2278, 0x2276 },     // ≸ -> ≶,   ̸ (338)
+       { 0x2279, 0x2277 },     // ≹ -> ≷,   ̸ (338)
+       { 0x2280, 0x227a },     // ⊀ -> ≺,   ̸ (338)
+       { 0x2281, 0x227b },     // ⊁ -> ≻,   ̸ (338)
+       { 0x2284, 0x2282 },     // ⊄ -> ⊂,   ̸ (338)
+       { 0x2285, 0x2283 },     // ⊅ -> ⊃,   ̸ (338)
+       { 0x2288, 0x2286 },     // ⊈ -> ⊆,   ̸ (338)
+       { 0x2289, 0x2287 },     // ⊉ -> ⊇,   ̸ (338)
+       { 0x22ac, 0x22a2 },     // ⊬ -> ⊢,   ̸ (338)
+       { 0x22ad, 0x22a8 },     // ⊭ -> ⊨,   ̸ (338)
+       { 0x22ae, 0x22a9 },     // ⊮ -> ⊩,   ̸ (338)
+       { 0x22af, 0x22ab },     // ⊯ -> ⊫,   ̸ (338)
+       { 0x22e0, 0x227c },     // ⋠ -> ≼,   ̸ (338)
+       { 0x22e1, 0x227d },     // ⋡ -> ≽,   ̸ (338)
+       { 0x22e2, 0x2291 },     // ⋢ -> ⊑,   ̸ (338)
+       { 0x22e3, 0x2292 },     // ⋣ -> ⊒,   ̸ (338)
+       { 0x22ea, 0x22b2 },     // ⋪ -> ⊲,   ̸ (338)
+       { 0x22eb, 0x22b3 },     // ⋫ -> ⊳,   ̸ (338)
+       { 0x22ec, 0x22b4 },     // ⋬ -> ⊴,   ̸ (338)
+       { 0x22ed, 0x22b5 },     // ⋭ -> ⊵,   ̸ (338)
+       { 0x2adc, 0x2add },     // ⫝̸ -> ⫝,   ̸ (338)
+       { 0x3003,   0x22 },     // 〃 -> ",    
+};
diff --git a/utils.h b/utils.h
new file mode 100644 (file)
index 0000000..36afc0a
--- /dev/null
+++ b/utils.h
@@ -0,0 +1,346 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_UTILS_H
+#define CMUS_UTILS_H
+
+#ifdef HAVE_CONFIG
+#include "config/utils.h"
+#endif
+
+#include "compiler.h"
+#include "debug.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <stdio.h>
+#include <time.h>
+#include <stdint.h>
+#ifdef HAVE_BYTESWAP_H
+#include <byteswap.h>
+#endif
+
+
+#define N_ELEMENTS(array) (sizeof(array) / sizeof((array)[0]))
+
+struct slice {
+       void *ptr;
+       size_t len;
+};
+
+#define TO_SLICE(type, ...) ((struct slice) { \
+       .ptr = ((type[]){__VA_ARGS__}), \
+       .len = N_ELEMENTS(((type[]){__VA_ARGS__})), \
+})
+
+#define STRINGIZE_HELPER(x) #x
+#define STRINGIZE(x) STRINGIZE_HELPER(x)
+
+#define CONCATENATE_HELPER(x,y) x##y
+#define CONCATENATE(x,y) CONCATENATE_HELPER(x,y)
+
+#define getentry(ptr, offset, type) (*((type *) ((void *) ((char *) (ptr) + (offset)))))
+
+#define STATIC_ASSERT(cond) \
+       static uint8_t CONCATENATE(_cmus_unused_, __LINE__)[2*(cond) - 1] UNUSED
+
+static inline long min_i(long a, long b)
+{
+       return a < b ? a : b;
+}
+
+static inline unsigned long min_u(unsigned long a, unsigned long b)
+{
+       return a < b ? a : b;
+}
+
+static inline long max_i(long a, long b)
+{
+       return a > b ? a : b;
+}
+
+static inline int clamp(int val, int minval, int maxval)
+{
+       if (val < minval)
+               return minval;
+       if (val > maxval)
+               return maxval;
+       return val;
+}
+
+static inline int scale_from_percentage(int val, int max_val)
+{
+       if (val < 0)
+               return (val * max_val - 50) / 100;
+       return (val * max_val + 50) / 100;
+}
+
+static inline int scale_to_percentage(int val, int max_val)
+{
+       int half = max_val / 2;
+
+       if (max_val <= 0)
+               return 100;
+
+       if (val < 0)
+               return (val * 100 - half) / max_val;
+       return (val * 100 + half) / max_val;
+}
+
+static inline int str_to_int(const char *str, long int *val)
+{
+       char *end;
+
+       *val = strtol(str, &end, 10);
+       if (*str == 0 || *end != 0)
+               return -1;
+       return 0;
+}
+
+static inline int strcmp0(const char *str1, const char *str2)
+{
+       if (!str1)
+               return str2 ? -1 : 0;
+       if (!str2)
+               return 1;
+
+       return strcmp(str1, str2);
+}
+
+static inline int is_space(const char ch)
+{
+       return (ch == ' ' || ch == '\t');
+}
+
+static inline int ends_with(const char *str, const char *suffix)
+{
+       return strstr(str, suffix) + strlen(suffix) == str + strlen(str);
+}
+
+static inline void strip_trailing_spaces(char *str)
+{
+       char *end = str + strlen(str);
+       while (end > str && is_space(*(end-1))) {
+               end--;
+       }
+       *end = 0;
+}
+
+static inline uint32_t hash_str(const char *s)
+{
+       const unsigned char *p = (const unsigned char *)s;
+       uint32_t h = 5381;
+
+       while (*p) {
+               h *= 33;
+               h ^= *p++;
+       }
+
+       return h ^ (h >> 16);
+}
+
+static inline time_t file_get_mtime(const char *filename)
+{
+       struct stat s;
+
+       /* stat follows symlinks, lstat does not */
+       if (stat(filename, &s) == -1)
+               return -1;
+       return s.st_mtime;
+}
+
+static inline void ns_sleep(int ns)
+{
+       struct timespec req;
+
+       req.tv_sec = 0;
+       req.tv_nsec = ns;
+       nanosleep(&req, NULL);
+}
+
+static inline void us_sleep(int us)
+{
+       ns_sleep(us * 1e3);
+}
+
+static inline void ms_sleep(int ms)
+{
+       ns_sleep(ms * 1e6);
+}
+
+static inline int is_http_url(const char *name)
+{
+       return strncmp(name, "http://", 7) == 0;
+}
+
+static inline int is_cdda_url(const char *name)
+{
+       return strncmp(name, "cdda://", 7) == 0;
+}
+
+static inline int is_cue_url(const char *name)
+{
+       return strncmp(name, "cue://", 6) == 0;
+}
+
+static inline int is_url(const char *name)
+{
+       return is_http_url(name) || is_cdda_url(name) || is_cue_url(name);
+}
+
+static inline int is_freeform_true(const char *c)
+{
+       return  c[0] == '1' ||
+               c[0] == 'y' || c[0] == 'Y' ||
+               c[0] == 't' || c[0] == 'T';
+}
+
+/* e.g. NetBSD */
+#if defined(bswap16)
+/* GNU libc */
+#elif defined(bswap_16)
+# define bswap16 bswap_16
+/* e.g. OpenBSD */
+#elif defined(swap16)
+# define bswap16 swap16
+#else
+# define bswap16(x) \
+       ((((x) >> 8) & 0xff) | (((x) & 0xff) << 8))
+#endif
+
+static inline uint16_t swap_uint16(uint16_t x)
+{
+       return bswap16(x);
+}
+
+/* e.g. NetBSD */
+#if defined(bswap32)
+/* GNU libc */
+#elif defined(bswap_32)
+# define bswap32 bswap_32
+/* e.g. OpenBSD */
+#elif defined(swap32)
+# define bswap32 swap32
+#else
+# define bswap32(x) \
+       ((((x) & 0xff000000) >> 24) | (((x) & 0x00ff0000) >>  8) |      \
+        (((x) & 0x0000ff00) <<  8) | (((x) & 0x000000ff) << 24))
+#endif
+
+static inline uint32_t swap_uint32(uint32_t x)
+{
+       return bswap32(x);
+}
+
+static inline uint32_t read_le32(const char *buf)
+{
+       const unsigned char *b = (const unsigned char *)buf;
+
+       return b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24);
+}
+
+static inline uint32_t read_le24(const char *buf)
+{
+       const unsigned char *b = (const unsigned char *)buf;
+
+       return b[0] | (b[1] << 8) | (b[2] << 16);
+}
+
+static inline int32_t read_le24i(const char *buf)
+{
+       uint32_t a = read_le24(buf); 
+       return (a & 0x800000) ? 0xFF000000 | a : a;
+}
+
+static inline uint16_t read_le16(const char *buf)
+{
+       const unsigned char *b = (const unsigned char *)buf;
+
+       return b[0] | (b[1] << 8);
+}
+
+static int _saved_stdout;
+static int _saved_stderr;
+
+static inline void disable_stdio(void)
+{
+       _saved_stdout = dup(1);
+       _saved_stderr = dup(2);
+       if (_saved_stdout == -1 || _saved_stderr == -1) {
+               return;
+       }
+
+       int devnull = open("/dev/null", O_WRONLY);
+       dup2(devnull, 1);
+       dup2(devnull, 2);
+       close(devnull);
+}
+
+static inline void enable_stdio(void)
+{
+       fflush(stdout);
+       fflush(stderr);
+       while (dup2(_saved_stdout, 1) == -1 && errno == EINTR) { }
+       while (dup2(_saved_stderr, 2) == -1 && errno == EINTR) { }
+       close(_saved_stdout);
+       close(_saved_stderr);
+}
+
+static inline void init_pipes(int *out, int *in)
+{
+       int fds[2];
+       int rc = pipe(fds);
+       BUG_ON(rc);
+       *out = fds[0];
+       *in = fds[1];
+       int flags = fcntl(*out, F_GETFL);
+       rc = fcntl(*out, F_SETFL, flags | O_NONBLOCK);
+       BUG_ON(rc);
+}
+
+static inline void notify_via_pipe(int pipe)
+{
+       char buf = 0;
+       write(pipe, &buf, 1);
+}
+
+static inline void clear_pipe(int pipe, size_t bytes)
+{
+       char buf[128];
+       size_t bytes_to_read = min_u(sizeof(buf), bytes);
+       read(pipe, buf, bytes_to_read);
+}
+
+static inline ssize_t strscpy(char *dst, const char *src, size_t size)
+{
+       for (size_t i = 0; i < size; i++) {
+               dst[i] = src[i];
+               if (src[i] == 0)
+                       return i;
+       }
+       if (size > 0)
+               dst[size - 1] = 0;
+       return -1;
+}
+
+#endif
diff --git a/window.c b/window.c
new file mode 100644 (file)
index 0000000..0b3bf1c
--- /dev/null
+++ b/window.c
@@ -0,0 +1,617 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "window.h"
+#include "options.h"
+#include "xmalloc.h"
+#include "debug.h"
+#include "utils.h"
+
+#include <stdlib.h>
+
+static void sel_changed(struct window *win)
+{
+       if (win->sel_changed)
+               win->sel_changed();
+       win->changed = 1;
+}
+
+static int selectable(struct window *win, struct iter *iter)
+{
+       if (win->selectable)
+               return win->selectable(iter);
+       return 1;
+}
+
+struct window *window_new(int (*get_prev)(struct iter *), int (*get_next)(struct iter *))
+{
+       struct window *win;
+
+       win = xnew(struct window, 1);
+       win->get_next = get_next;
+       win->get_prev = get_prev;
+       win->selectable = NULL;
+       win->sel_changed = NULL;
+       win->nr_rows = 1;
+       win->changed = 1;
+       iter_init(&win->head);
+       iter_init(&win->top);
+       iter_init(&win->sel);
+       return win;
+}
+
+void window_free(struct window *win)
+{
+       free(win);
+}
+
+void window_set_empty(struct window *win)
+{
+       iter_init(&win->head);
+       iter_init(&win->top);
+       iter_init(&win->sel);
+       sel_changed(win);
+}
+
+void window_set_contents(struct window *win, void *head)
+{
+       struct iter first;
+
+       win->head.data0 = head;
+       win->head.data1 = NULL;
+       win->head.data2 = NULL;
+       first = win->head;
+       win->get_next(&first);
+       win->top = first;
+       win->sel = first;
+       while (!selectable(win, &win->sel))
+               win->get_next(&win->sel);
+       sel_changed(win);
+}
+
+void window_set_nr_rows(struct window *win, int nr_rows)
+{
+       if (nr_rows < 1)
+               return;
+       win->nr_rows = nr_rows;
+       window_changed(win);
+       win->changed = 1;
+}
+
+void window_up(struct window *win, int rows)
+{
+       struct iter iter;
+       int upper_bound   = min_i(scroll_offset,  win->nr_rows/2);
+       int buffer        = 0; /* rows between `old sel` and `old top` */
+       int sel_up        = 0; /* selectable rows between `old sel` and `new sel` */
+       int skipped       = 0; /* unselectable rows between `old sel` and `new sel` */
+       int actual_offset = 0; /* rows between `new sel` and `new top` */
+       int top_up        = 0; /* rows between `old top` and `new top` */
+
+       iter = win->top;
+       while (!iters_equal(&iter, &win->sel)) {
+               win->get_next(&iter);
+               buffer++;
+       }
+
+       iter = win->sel;
+       while (sel_up < rows) {
+               if (!win->get_prev(&iter)) {
+                       break;
+               }
+               if (selectable(win, &iter)) {
+                       sel_up++;
+                       win->sel = iter;
+               } else {
+                       skipped++;
+               }
+       }
+       /* if there is no selectable row above the current, we move win->top instead
+        * this is necessary when scroll_offset=0 to make the first album header visible */
+       if (sel_up == 0) {
+               skipped = 0;
+               upper_bound = min_i(buffer+rows, win->nr_rows/2);
+       }
+
+       iter = win->sel;
+       while (actual_offset < upper_bound) {
+               if (!win->get_prev(&iter)) {
+                       break;
+               }
+               actual_offset++;
+       }
+
+       top_up = actual_offset + sel_up + skipped - buffer;
+       while (top_up > 0) {
+               win->get_prev(&win->top);
+               top_up--;
+       }
+
+       if (sel_up > 0 || actual_offset > 0)
+               sel_changed(win);
+}
+
+void window_down(struct window *win, int rows)
+{
+       struct iter iter;
+       int upper_bound   = min_i(scroll_offset, (win->nr_rows-1)/2);
+       int buffer        = 0; /* rows between `old sel` and `old bottom` */
+       int sel_down      = 0; /* selectable rows between `old sel` and `new sel` */
+       int skipped       = 0; /* unselectable rows between `old sel` and `new sel` */
+       int actual_offset = 0; /* rows between `new sel` and `new bottom` */
+       int top_down      = 0; /* rows between `old top` and `new top` */
+
+       buffer = win->nr_rows - 1;
+       iter = win->top;
+       while (!iters_equal(&iter, &win->sel)) {
+               win->get_next(&iter);
+               buffer--;
+       }
+
+       iter = win->sel;
+       while (sel_down < rows) {
+               if (!win->get_next(&iter)) {
+                       break;
+               }
+               if (selectable(win, &iter)) {
+                       sel_down++;
+                       win->sel = iter;
+               } else {
+                       skipped++;
+               }
+       }
+       if (sel_down == 0) {
+               skipped = 0;
+               upper_bound = min_i(buffer+rows, (win->nr_rows-1)/2);
+       }
+
+       iter = win->sel;
+       while (actual_offset < upper_bound) {
+               if (!win->get_next(&iter))
+                       break;
+               actual_offset++;
+       }
+
+       top_down = actual_offset + sel_down + skipped - buffer;
+       while (top_down > 0) {
+               win->get_next(&win->top);
+               top_down--;
+       }
+
+       if (sel_down > 0 || actual_offset > 0)
+               sel_changed(win);
+}
+
+/*
+ * minimize number of empty lines visible
+ * make sure selection is visible
+ */
+void window_changed(struct window *win)
+{
+       struct iter iter;
+       int delta, rows;
+
+       if (iter_is_null(&win->head)) {
+               BUG_ON(!iter_is_null(&win->top));
+               BUG_ON(!iter_is_null(&win->sel));
+               return;
+       }
+       BUG_ON(iter_is_null(&win->top));
+       BUG_ON(iter_is_null(&win->sel));
+
+       /* make sure top and sel point to real row if possible */
+       if (iter_is_head(&win->top)) {
+               win->get_next(&win->top);
+               win->sel = win->top;
+               sel_changed(win);
+               return;
+       }
+
+       /* make sure the selected row is visible */
+
+       /* get distance between top and sel */
+       delta = 0;
+       iter = win->top;
+       while (!iters_equal(&iter, &win->sel)) {
+               if (!win->get_next(&iter)) {
+                       /* sel < top, scroll up until top == sel */
+                       while (!iters_equal(&win->top, &win->sel))
+                               win->get_prev(&win->top);
+                       goto minimize;
+               }
+               delta++;
+       }
+
+       /* scroll down until sel is visible */
+       while (delta > win->nr_rows - 1) {
+               win->get_next(&win->top);
+               delta--;
+       }
+minimize:
+       /* minimize number of empty lines shown */
+       iter = win->top;
+       rows = 1;
+       while (rows < win->nr_rows) {
+               if (!win->get_next(&iter))
+                       break;
+               rows++;
+       }
+       while (rows < win->nr_rows) {
+               iter = win->top;
+               if (!win->get_prev(&iter))
+                       break;
+               win->top = iter;
+               rows++;
+       }
+       win->changed = 1;
+}
+
+void window_row_vanishes(struct window *win, struct iter *iter)
+{
+       struct iter new = *iter;
+       if (!win->get_next(&new) && !win->get_prev(&new)) {
+               window_set_empty(win);
+       }
+
+       BUG_ON(iter->data0 != win->head.data0);
+       if (iters_equal(&win->top, iter)) {
+               new = *iter;
+               if (win->get_next(&new)) {
+                       win->top = new;
+               } else {
+                       new = *iter;
+                       win->get_prev(&new);
+                       win->top = new;
+               }
+       }
+       if (iters_equal(&win->sel, iter)) {
+               /* calculate minimal distance to next selectable */
+               int down = 0;
+               int up = 0;
+               new = *iter;
+               do {
+                       if (!win->get_next(&new)) {
+                               down = 0;
+                               break;
+                       }
+                       down++;
+               } while (!selectable(win, &new));
+               new = *iter;
+               do {
+                       if (!win->get_prev(&new)) {
+                               up = 0;
+                               break;
+                       }
+                       up++;
+               } while (!selectable(win, &new));
+               new = *iter;
+               if (down > 0 && (up == 0 || down <= up)) {
+                       do {
+                               win->get_next(&new);
+                       } while (!selectable(win, &new));
+               } else if (up > 0) {
+                       do {
+                               win->get_prev(&new);
+                       } while (!selectable(win, &new));
+               } else {
+                       /* no selectable item left but window not empty */
+                       new.data1 = new.data2 = NULL;
+               }
+               win->sel = new;
+               sel_changed(win);
+       }
+
+       win->changed = 1;
+}
+
+int window_get_top(struct window *win, struct iter *iter)
+{
+       *iter = win->top;
+       return !iter_is_empty(iter);
+}
+
+int window_get_sel(struct window *win, struct iter *iter)
+{
+       *iter = win->sel;
+       return !iter_is_empty(iter);
+}
+
+int window_get_prev(struct window *win, struct iter *iter)
+{
+       return win->get_prev(iter);
+}
+
+int window_get_next(struct window *win, struct iter *iter)
+{
+       return win->get_next(iter);
+}
+
+void window_set_sel(struct window *win, struct iter *iter)
+{
+       int sel_nr, top_nr, bottom_nr;
+       int upper_bound;
+       struct iter tmp;
+
+       BUG_ON(iter_is_empty(&win->top));
+       BUG_ON(iter_is_empty(iter));
+       BUG_ON(iter->data0 != win->head.data0);
+
+       if (iters_equal(&win->sel, iter))
+               return;
+       win->sel = *iter;
+
+       tmp = win->head;
+       win->get_next(&tmp);
+       top_nr = 0;
+       while (!iters_equal(&tmp, &win->top)) {
+               win->get_next(&tmp);
+               top_nr++;
+       }
+
+       tmp = win->head;
+       win->get_next(&tmp);
+       sel_nr = 0;
+       while (!iters_equal(&tmp, &win->sel)) {
+               BUG_ON(!win->get_next(&tmp));
+               sel_nr++;
+       }
+
+       upper_bound = win->nr_rows / 2;
+       if (scroll_offset < upper_bound)
+               upper_bound = scroll_offset;
+
+       if (sel_nr < top_nr + upper_bound) { /* scroll up */
+               tmp = win->head;
+               win->get_next(&tmp);
+               if (sel_nr < upper_bound) { /* no space above */
+                       win->top = tmp;
+               } else {
+                       win->top = win->sel;
+                       while (upper_bound > 0) {
+                               win->get_prev(&win->top);
+                               upper_bound--;
+                       }
+               }
+       } else { /* scroll down */
+               upper_bound = (win->nr_rows - 1) / 2;
+               if (scroll_offset < upper_bound)
+                       upper_bound = scroll_offset;
+
+               tmp = win->sel;
+               bottom_nr = sel_nr;
+               if (sel_nr >= top_nr + win->nr_rows) { /* seleced element not visible */
+                       while (sel_nr >= top_nr + win->nr_rows) {
+                               win->get_next(&win->top);
+                               top_nr++;
+                       }
+               } else { /* selected element visible */
+                       while (bottom_nr + 1 < top_nr + win->nr_rows) {
+                               if (!win->get_next(&tmp)) { /* no space below */
+                                       bottom_nr = sel_nr + upper_bound;
+                                       break;
+                               }
+                               bottom_nr++;
+                       }
+               }
+
+               while (bottom_nr < sel_nr + upper_bound) {
+                       if (!win->get_next(&tmp))
+                               break;
+                       bottom_nr++;
+                       win->get_next(&win->top);
+               }
+       }
+       sel_changed(win);
+}
+
+void window_goto_top(struct window *win)
+{
+       struct iter old_sel;
+
+       old_sel = win->sel;
+       win->sel = win->head;
+       win->get_next(&win->sel);
+       win->top = win->sel;
+       while (!selectable(win, &win->sel))
+               win->get_next(&win->sel);
+       if (!iters_equal(&old_sel, &win->sel))
+               sel_changed(win);
+}
+
+void window_goto_bottom(struct window *win)
+{
+       struct iter old_sel;
+       int count;
+
+       old_sel = win->sel;
+       win->sel = win->head;
+       win->get_prev(&win->sel);
+       win->top = win->sel;
+       count = win->nr_rows - 1;
+       while (count) {
+               struct iter iter = win->top;
+
+               if (!win->get_prev(&iter))
+                       break;
+               win->top = iter;
+               count--;
+       }
+       while (!selectable(win, &win->sel))
+               win->get_prev(&win->sel);
+       if (!iters_equal(&old_sel, &win->sel))
+               sel_changed(win);
+}
+
+void window_page_up(struct window *win)
+{
+       struct iter sel = win->sel;
+       struct iter top = win->top;
+       int up;
+
+       for (up = 0; up < win->nr_rows - 1; up++) {
+               if (!win->get_prev(&sel) || !win->get_prev(&top))
+                       break;
+               if (selectable(win, &sel)) {
+                       win->sel = sel;
+                       win->top = top;
+               }
+       }
+
+       sel_changed(win);
+}
+
+void window_half_page_up(struct window *win)
+{
+       struct iter sel = win->sel;
+       struct iter top = win->top;
+       int up;
+
+       for (up = 0; up < (win->nr_rows - 1) / 2; up++) {
+               if (!win->get_prev(&sel) || !win->get_prev(&top))
+                       break;
+               if (selectable(win, &sel)) {
+                       win->sel = sel;
+                       win->top = top;
+               }
+       }
+
+       sel_changed(win);
+}
+
+static struct iter window_bottom(struct window *win)
+{
+       struct iter bottom = win->top;
+       struct iter iter = win->top;
+       int down;
+
+       for (down = 0; down < win->nr_rows - 1; down++) {
+               if (!win->get_next(&iter))
+                       break;
+               bottom = iter;
+       }
+
+       return bottom;
+}
+
+void window_page_down(struct window *win)
+{
+       struct iter sel = win->sel;
+       struct iter bot = window_bottom(win);
+       struct iter top = win->top;
+       int down;
+
+       for (down = 0; down < win->nr_rows - 1; down++) {
+               if (!win->get_next(&sel) || !win->get_next(&bot))
+                       break;
+               win->get_next(&top);
+               if (selectable(win, &sel)) {
+                       win->sel = sel;
+                       win->top = top;
+               }
+       }
+
+       sel_changed(win);
+}
+
+void window_half_page_down(struct window *win)
+{
+       struct iter sel = win->sel;
+       struct iter bot = window_bottom(win);
+       struct iter top = win->top;
+       int down;
+
+       for (down = 0; down < (win-> nr_rows - 1) / 2; down++) {
+               if (!win->get_next(&sel) || !win->get_next(&bot))
+                       break;
+               win->get_next(&top);
+               if (selectable(win, &sel)) {
+                       win->sel = sel;
+                       win->top = top;
+               }
+       }
+
+       sel_changed(win);
+}
+
+
+void window_scroll_down(struct window *win)
+{
+       struct iter bot = window_bottom(win);
+       struct iter top = win->top;
+       if (!win->get_next(&bot)) return;
+       if (!win->get_next(&top)) return;
+       if (iters_equal(&win->top, &win->sel))
+               win->get_next(&win->sel);
+       win->top = top;
+       while (!selectable(win, &win->sel))
+               win->get_next(&win->sel);
+       sel_changed(win);
+}
+
+void window_scroll_up(struct window *win)
+{
+       struct iter top = win->top;
+       if (!win->get_prev(&top)) return;
+       struct iter bot = window_bottom(win);
+       /* keep selected row on screen: */
+       if (iters_equal(&bot, &win->sel))
+               win->get_prev(&win->sel);
+       win->top = top;
+       while (!selectable(win, &win->sel))
+               win->get_prev(&win->sel);
+       sel_changed(win);
+}
+
+static void window_goto_pos(struct window *win, int pos)
+{
+       struct iter old_sel;
+       int i;
+
+       old_sel = win->sel;
+       win->sel = win->top;
+       for (i = 0; i < pos; i++)
+               win->get_next(&win->sel);
+       if (!iters_equal(&old_sel, &win->sel))
+               sel_changed(win);
+}
+
+void window_page_top(struct window *win)
+{
+       window_goto_pos(win, 0);
+       while (!selectable(win, &win->sel))
+               win->get_next(&win->sel);
+}
+
+void window_page_bottom(struct window *win)
+{
+       window_goto_pos(win, win->nr_rows - 1);
+       while (!selectable(win, &win->sel))
+               win->get_prev(&win->sel);
+}
+
+void window_page_middle(struct window *win)
+{
+       window_goto_pos(win, win->nr_rows / 2);
+       while (!selectable(win, &win->sel))
+               win->get_next(&win->sel);
+}
+
+int window_get_nr_rows(struct window *win)
+{
+       return win->nr_rows;
+}
diff --git a/window.h b/window.h
new file mode 100644 (file)
index 0000000..04942b7
--- /dev/null
+++ b/window.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_WINDOW_H
+#define CMUS_WINDOW_H
+
+#include "iter.h"
+
+/*
+ * window contains list of rows
+ * - the list is double linked circular list
+ * - head contains no data. head is not a real row
+ * - head->prev gets the last row (or head if the list is empty) in the list
+ * - head->next gets the first row (or head if the list is empty) in the list
+ *
+ * get_prev(&iter) always stores prev row to the iter
+ * get_next(&iter) always stores next row to the iter
+ *
+ * these return 1 if the new row is real row (not head), 0 otherwise
+ *
+ * sel_changed callback is called if not NULL and selection has changed
+ */
+
+struct window {
+       /* head of the row list */
+       struct iter head;
+
+       /* top row */
+       struct iter top;
+
+       /* selected row */
+       struct iter sel;
+
+       /* window height */
+       int nr_rows;
+
+       unsigned changed : 1;
+
+       /* return 1 if got next/prev, otherwise 0 */
+       int (*get_prev)(struct iter *iter);
+       int (*get_next)(struct iter *iter);
+       /* NULL if all rows are selectable */
+       int (*selectable)(struct iter *iter);
+       void (*sel_changed)(void);
+};
+
+struct window *window_new(int (*get_prev)(struct iter *), int (*get_next)(struct iter *));
+void window_free(struct window *win);
+void window_set_empty(struct window *win);
+void window_set_contents(struct window *win, void *head);
+
+/* call this after rows were added to window or order of rows was changed.
+ * top and sel MUST point to valid rows (or window must be empty, but then
+ * why do you want to call this function :)).
+ *
+ * if you remove row from window then call window_row_vanishes BEFORE removing
+ * the row instead of this function.
+ */
+void window_changed(struct window *win);
+
+/* call this BEFORE row is removed from window */
+void window_row_vanishes(struct window *win, struct iter *iter);
+
+int window_get_top(struct window *win, struct iter *iter);
+int window_get_sel(struct window *win, struct iter *iter);
+int window_get_prev(struct window *win, struct iter *iter);
+int window_get_next(struct window *win, struct iter *iter);
+
+/* set selected row */
+void window_set_sel(struct window *win, struct iter *iter);
+
+void window_set_nr_rows(struct window *win, int nr_rows);
+void window_up(struct window *win, int rows);
+void window_down(struct window *win, int rows);
+void window_goto_top(struct window *win);
+void window_goto_bottom(struct window *win);
+void window_page_up(struct window *win);
+void window_half_page_up(struct window *win);
+void window_page_down(struct window *win);
+void window_half_page_down(struct window *win);
+void window_scroll_down(struct window *win);
+void window_scroll_up(struct window *win);
+void window_page_top(struct window *win);
+void window_page_bottom(struct window *win);
+void window_page_middle(struct window *win);
+
+int window_get_nr_rows(struct window *win);
+
+#endif
diff --git a/worker.c b/worker.c
new file mode 100644 (file)
index 0000000..2096f44
--- /dev/null
+++ b/worker.c
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "worker.h"
+#include "locking.h"
+#include "list.h"
+#include "xmalloc.h"
+#include "debug.h"
+#include "job.h"
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <pthread.h>
+
+struct worker_job {
+       struct list_head node;
+
+       uint32_t type;
+       void (*job_cb)(void *data);
+       void (*free_cb)(void *data);
+       void *data;
+};
+
+enum worker_state {
+       WORKER_PAUSED,
+       WORKER_RUNNING,
+       WORKER_STOPPED,
+};
+
+static LIST_HEAD(worker_job_head);
+static pthread_mutex_t worker_mutex = CMUS_MUTEX_INITIALIZER;
+static pthread_cond_t worker_cond = PTHREAD_COND_INITIALIZER;
+static pthread_t worker_thread;
+static enum worker_state state = WORKER_PAUSED;
+static int cancel_current = 0;
+
+/*
+ * - only worker thread modifies this
+ * - cur_job->job_cb can read this without locking
+ * - anyone else must lock worker before reading this
+ */
+static struct worker_job *cur_job = NULL;
+
+#define worker_lock() cmus_mutex_lock(&worker_mutex)
+#define worker_unlock() cmus_mutex_unlock(&worker_mutex)
+
+static void *worker_loop(void *arg)
+{
+       srand(time(NULL));
+
+       worker_lock();
+       while (1) {
+               if (state != WORKER_RUNNING || list_empty(&worker_job_head)) {
+                       int rc;
+
+                       if (state == WORKER_STOPPED)
+                               break;
+
+                       rc = pthread_cond_wait(&worker_cond, &worker_mutex);
+                       if (rc)
+                               d_print("pthread_cond_wait: %s\n", strerror(rc));
+               } else {
+                       struct list_head *item = worker_job_head.next;
+                       uint64_t t;
+
+                       list_del(item);
+                       cur_job = container_of(item, struct worker_job, node);
+                       worker_unlock();
+
+                       t = timer_get();
+                       cur_job->job_cb(cur_job->data);
+                       timer_print("worker job", timer_get() - t);
+
+                       worker_lock();
+                       cur_job->free_cb(cur_job->data);
+                       free(cur_job);
+                       cur_job = NULL;
+
+                       // wakeup worker_remove_jobs_*() if needed
+                       if (cancel_current) {
+                               cancel_current = 0;
+                               pthread_cond_signal(&worker_cond);
+                       }
+               }
+       }
+       worker_unlock();
+       return NULL;
+}
+
+void worker_init(void)
+{
+       int rc = pthread_create(&worker_thread, NULL, worker_loop, NULL);
+
+       BUG_ON(rc);
+}
+
+static void worker_set_state(enum worker_state s)
+{
+       worker_lock();
+       state = s;
+       pthread_cond_signal(&worker_cond);
+       worker_unlock();
+}
+
+void worker_start(void)
+{
+       worker_set_state(WORKER_RUNNING);
+}
+
+void worker_exit(void)
+{
+       worker_set_state(WORKER_STOPPED);
+       pthread_join(worker_thread, NULL);
+}
+
+void worker_add_job(uint32_t type, void (*job_cb)(void *data),
+               void (*free_cb)(void *data), void *data)
+{
+       struct worker_job *job;
+
+       job = xnew(struct worker_job, 1);
+       job->type = type;
+       job->job_cb = job_cb;
+       job->free_cb = free_cb;
+       job->data = data;
+
+       worker_lock();
+       list_add_tail(&job->node, &worker_job_head);
+       pthread_cond_signal(&worker_cond);
+       worker_unlock();
+}
+
+static int worker_matches_type(uint32_t type, void *job_data,
+               void *opaque)
+{
+       uint32_t *pat = opaque;
+       return !!(type & *pat);
+}
+
+void worker_remove_jobs_by_type(uint32_t pat)
+{
+       worker_remove_jobs_by_cb(worker_matches_type, &pat);
+}
+
+void worker_remove_jobs_by_cb(worker_match_cb cb, void *opaque)
+{
+       struct list_head *item;
+
+       worker_lock();
+
+       item = worker_job_head.next;
+       while (item != &worker_job_head) {
+               struct worker_job *job = container_of(item, struct worker_job,
+                               node);
+               struct list_head *next = item->next;
+
+               if (cb(job->type, job->data, opaque)) {
+                       list_del(&job->node);
+                       job->free_cb(job->data);
+                       free(job);
+               }
+               item = next;
+       }
+
+       /* wait current job to finish or cancel if it's of the specified type */
+       if (cur_job && cb(cur_job->type, cur_job->data, opaque)) {
+               cancel_current = 1;
+               while (cancel_current)
+                       pthread_cond_wait(&worker_cond, &worker_mutex);
+       }
+
+       worker_unlock();
+}
+
+int worker_has_job_by_type(uint32_t pat)
+{
+       return worker_has_job_by_cb(worker_matches_type, &pat);
+}
+
+int worker_has_job_by_cb(worker_match_cb cb, void *opaque)
+{
+       struct worker_job *job;
+       int has_job = 0;
+
+       worker_lock();
+       list_for_each_entry(job, &worker_job_head, node) {
+               if (cb(job->type, job->data, opaque)) {
+                       has_job = 1;
+                       break;
+               }
+       }
+       if (cur_job && cb(job->type, job->data, opaque))
+               has_job = 1;
+       worker_unlock();
+       return has_job;
+}
+
+/*
+ * this is only called from the worker thread
+ * cur_job is guaranteed to be non-NULL
+ */
+int worker_cancelling(void)
+{
+       return cancel_current;
+}
diff --git a/worker.h b/worker.h
new file mode 100644 (file)
index 0000000..e8ac986
--- /dev/null
+++ b/worker.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_WORKER_H
+#define CMUS_WORKER_H
+
+#include <stdint.h>
+
+#define JOB_TYPE_NONE 0
+#define JOB_TYPE_ANY  ~0
+
+typedef int (*worker_match_cb)(uint32_t type, void *job_data, void *opaque);
+
+void worker_init(void);
+void worker_start(void);
+void worker_exit(void);
+
+void worker_add_job(uint32_t type, void (*job_cb)(void *job_data),
+               void (*free_cb)(void *job_data), void *job_data);
+
+/* NOTE: The callbacks below run in parallel with the job_cb function. Access to
+ * job_data must by synchronized.
+ */
+
+void worker_remove_jobs_by_type(uint32_t pat);
+void worker_remove_jobs_by_cb(worker_match_cb cb, void *opaque);
+
+int worker_has_job_by_type(uint32_t pat);
+int worker_has_job_by_cb(worker_match_cb cb, void *opaque);
+
+int worker_cancelling(void);
+
+#endif
diff --git a/xmalloc.c b/xmalloc.c
new file mode 100644 (file)
index 0000000..a2bb31c
--- /dev/null
+++ b/xmalloc.c
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "xmalloc.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+
+extern char *program_name;
+
+void malloc_fail(void)
+{
+       fprintf(stderr, "%s: could not allocate memory: %s\n", program_name, strerror(errno));
+       exit(42);
+}
+
+#ifndef HAVE_STRNDUP
+char *xstrndup(const char *str, size_t n)
+{
+       size_t len;
+       char *s;
+
+       for (len = 0; len < n && str[len]; len++)
+               ;
+       s = xmalloc(len + 1);
+       memcpy(s, str, len);
+       s[len] = 0;
+       return s;
+}
+#endif
diff --git a/xmalloc.h b/xmalloc.h
new file mode 100644 (file)
index 0000000..3da5556
--- /dev/null
+++ b/xmalloc.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004-2005 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_XMALLOC_H
+#define CMUS_XMALLOC_H
+
+#include "compiler.h"
+#ifdef HAVE_CONFIG
+#include "config/xmalloc.h"
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+
+void malloc_fail(void) CMUS_NORETURN;
+
+#define xnew(type, n)          (type *)xmalloc(sizeof(type) * (n))
+#define xnew0(type, n)         (type *)xmalloc0(sizeof(type) * (n))
+#define xrenew(type, mem, n)   (type *)xrealloc(mem, sizeof(type) * (n))
+
+static inline void * CMUS_MALLOC xmalloc(size_t size)
+{
+       void *ptr = malloc(size);
+
+       if (unlikely(ptr == NULL))
+               malloc_fail();
+       return ptr;
+}
+
+static inline void * CMUS_MALLOC xmalloc0(size_t size)
+{
+       void *ptr = calloc(1, size);
+
+       if (unlikely(ptr == NULL))
+               malloc_fail();
+       return ptr;
+}
+
+static inline void * CMUS_MALLOC xrealloc(void *ptr, size_t size)
+{
+       ptr = realloc(ptr, size);
+       if (unlikely(ptr == NULL))
+               malloc_fail();
+       return ptr;
+}
+
+static inline char * CMUS_MALLOC xstrdup(const char *str)
+{
+#ifdef HAVE_STRDUP
+       char *s = strdup(str);
+       if (unlikely(s == NULL))
+               malloc_fail();
+       return s;
+#else
+       size_t size = strlen(str) + 1;
+       void *ptr = xmalloc(size);
+       return (char *) memcpy(ptr, str, size);
+#endif
+}
+
+#ifdef HAVE_STRNDUP
+static inline char * CMUS_MALLOC xstrndup(const char *str, size_t n)
+{
+       char *s = strndup(str, n);
+       if (unlikely(s == NULL))
+               malloc_fail();
+       return s;
+}
+#else
+char * CMUS_MALLOC xstrndup(const char *str, size_t n);
+#endif
+
+static inline void free_str_array(char **array)
+{
+       int i;
+
+       if (array == NULL)
+               return;
+       for (i = 0; array[i]; i++)
+               free(array[i]);
+       free(array);
+}
+
+#endif
diff --git a/xstrjoin.c b/xstrjoin.c
new file mode 100644 (file)
index 0000000..9acd065
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "xstrjoin.h"
+#include "xmalloc.h"
+#include "utils.h"
+
+char *xstrjoin_slice(struct slice slice)
+{
+       const char **str = slice.ptr;
+       size_t i, pos = 0, len = 0;
+       char *joined;
+       size_t *lens;
+
+       lens = xnew(size_t, slice.len);
+       for (i = 0; i < slice.len; i++) {
+               lens[i] = strlen(str[i]);
+               len += lens[i];
+       }
+
+       joined = xnew(char, len + 1);
+       for (i = 0; i < slice.len; i++) {
+               memcpy(joined + pos, str[i], lens[i]);
+               pos += lens[i];
+       }
+       joined[len] = 0;
+
+       free(lens);
+
+       return joined;
+}
diff --git a/xstrjoin.h b/xstrjoin.h
new file mode 100644 (file)
index 0000000..5bacf1c
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2008-2013 Various Authors
+ * Copyright 2004 Timo Hirvonen
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CMUS_XSTRJOIN_H
+#define CMUS_XSTRJOIN_H
+
+#include "utils.h"
+
+char *xstrjoin_slice(struct slice);
+#define xstrjoin(...) xstrjoin_slice(TO_SLICE(const char *, __VA_ARGS__))
+
+#endif