From: Joshua Wise Date: Sat, 21 Nov 2009 22:39:18 +0000 (-0500) Subject: initial import from pitchfork-0.5.5 X-Git-Url: http://git.joshuawise.com/patchfork.git/commitdiff_plain/964dd0bc22ff252711b1190854923d5cd5382f9f initial import from pitchfork-0.5.5 --- 964dd0bc22ff252711b1190854923d5cd5382f9f diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..4e05a6c --- /dev/null +++ b/.htaccess @@ -0,0 +1 @@ +Order Deny,Allow diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..a102c93 --- /dev/null +++ b/COPYING @@ -0,0 +1,285 @@ +Copyright Roger Bystroem +Released under GPL v.2 + +This applies to everything not specified otherwise in CREDITS or elsewhere + + 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 diff --git a/CREDITS b/CREDITS new file mode 100644 index 0000000..9f98e33 --- /dev/null +++ b/CREDITS @@ -0,0 +1,16 @@ +CREDITS + +Icons stolen from (art.gnome.org): + Silvestre Herrera (Nuovo images) + Steven Garrity (Tango, see tango authors) + +media-* + -Gnome Default Theme + +Albumart from amazon, copyrights belong to their respective copyright-holders + +Net_MPD (http://pear.php.net/package/Net_MPD/): + Graham Christensen + +Jorbis (used for ogg playback) +http://www.jcraft.com/jorbis/ diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..47fbf32 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,150 @@ + +Pitchfork ChangeLog + +------------------------------------------------------------------------------ + +Changes 0.5.5 (18.01.2008) + * pitchfork now reports errors from mpd + * New thexy theme from Gianni Chiappetta + +Changes - 0.5.4 (25.09.2007) + * Cleanup css files so it's easier to make new themes + * Plausible fix for segfault/unable to assign complex types to properties + issues with albumart + * Menu options finally look decent + * Make dirlist init not fail if library is empty + * Removed a few useless debug statements + * Tried to switch to a negative page when pagination was enabled, fix for #81 + * Typo in multi add made it only search for last parameter, found by oliver + * French translation by Djanil + * Basque translation by Xabier Ezpeleta + * Fix for slow loading of page one when there's a big playlist (#86) + * Switched to html controls for streaming applet and also improved the applet + a bit + +Changes - 0.5.3 (24.06.2007) + * Improved amazon album search to give more correct hit + * Added more to album name as described in #62 + * Show title under artist/album browser instead of filename + * Show Artist - Title in browser title bar + * Fix bug with xml2array function causing lyrics to loose newlines + * Make streaming applet autoplay again and also when coming out of hiding + * Make applet stop playing immediately when mpd stops playing to make it seem + more responsive + * Fix race condition with sidebar and album art + * Update big albumart when it's open + * Translation support + * Show full file path with search results + * Make the player control area slightly shorter + * German translation by Christian Fischer + + +Changes - 0.5.2 (24.04.2007) + * Support for changing position display to remaining + * Workaround for konqueror repaint issue (bug #46) + * Added pagination support + * Added ability to search playlist + * Short info/test display on configure screen + * Follow and jump to currently playing song option + * Added dark theme + * Encrypt/hash pitchfork login password + * Fixed bug in Hashmap.remove. It would return the container object if it was + the first element in the list. + * Added nofollow and noindex to html meta tags. Consider adding pitchfork to + a robots.txt file on your server. + * Fix caching issue in Opera on the stream player + * Add background property to the stream plugin and try to close audio device + when stream stops + * Workaround for broken xml-declaration on lyricwiki.org + * Link to adding/editing lyric on lyricwiki.org + + +Changes - 0.5.1 (21.03.2007) + * Switched to JSON in commands + * Added ogg streaming support with jorbisplayer + * Improved speed of plchanges + * Add warnings when something goes wrong with locking of metadata-files + * Don't use PHP shorttags + * Cleanups + * Fix bug where albumart wouldn't show when mime_content_type wasn't + available + * Added license blurp to all files to avoid any issues + + +Changes - 0.5.0 (28.02.2007) + * Improved theming support + * Add theme choice to configure screen + * Album review/description + * Fix MPD port specification bug + * Fix bug with filename in lyrics files + * Add option for including stop button + * Clean up include stuff (also fixes an issue with initial config screen in opera) + * Fix font-issues + * Allow to change what should be displayed in the playlist + * Add forced lyrics reload + * Fixes for safari specific issues + * Metadata for album review and description + * Changed look of lyrics screen and added refetch button + +Changes - 0.4.1 (10.02.2007) + * Minor threshold on move in playlist + * Fix bug in directory browser + * Playlistsupport + * Optimizations for selection and playlist + * Test on notfound value in album.txt and try again if it is some while ago + * adding playlists + +Changes - 0.4.0 (08.02.2007) + * multiple move support + * speed up plchanges + * Get changes based only on id/pos + * Update DB support + * Bugfixes on directory browser + * Autoplay on add if pl empty + * Search function + +Changes - 0.3.1 (07.02.2007) + * Fix rendering error in browsers other than gecko-based + +Changes - 0.3.0 (04.02.2007) + * Added parent when doubleclicked in dirlist + * Don't add whole directory if return is pressed in qa and nothing there + * album art + * lyrics + * mv TODO ChangeLog + * new_state == stop => remove currently playing + * config => password protection, delay time + * Directory browsing based on artist etc + * PHP to javascript preferences (delay time won't be implemented until this is fixed) + * Split css into base/color-stuff + +Changes - 0.2.0 (29.01.2007) + * Pool for playlist elements as gecko don't seem to free them + * server settings (output devices, random, repeat, xfade) drop-down menu + * close folder if clicking on folder icon when folder is open + * Speedup of playlist changes and initial playlist loading + * Searching in directory listing + * Close can't be on escape, some browsers closes open connections to server + * Adjust slider when clicking somewhere on it + * Run slider callback when moving (250ms threshold) + * Fix race-condition, dirlist started building before elements were ready + * Several concurrent information requests, should never be more than one looping.... + * reconnect on broken set_timeout + * Quick Add + * Clean upp all display data when clearing playlist/stopping + +Changes - 0.1.0rc1/2 (21.01.2007) + * icon to indicate song/folder in dirlist + * organization + - double click moves right, seperate enque button (hot key for add) + - double click adds, single click opens + * .htaccess/stuff.conf file + * Use utf-8 + * When removing song before currently playing, showing of currently playing is removed + * Hi-light on prev/play/pause/next ..done + * coloured background around the "player" area (mockup.png) + * seems to fail on selecting playing song sometimes + * fixed position on "player" area? + * moving something up in the list causes jumping on response from server (the moved is probably placed wrongly so that it jumps one up on response from server) + - "Fixed" - when dropped on something it probably should land there.... + - FIXED! Oh happy days! diff --git a/README b/README new file mode 100644 index 0000000..642c401 --- /dev/null +++ b/README @@ -0,0 +1,23 @@ + +1. Installation + see doc/INSTALL + +2. Requirements + * PHP 5.1.3 or newer + * PHP-Pear + * DOM2 capable browser (firefox, konqueror, opera, safari, etc) + * MPD 0.12.0 or later + + +3. Shortcuts + Key shortcuts in browsers are a mess, some don't send on certain keys and others + don't allow you to cancel default action, therefore the shortcuts are a little weird. + I've tried to use what seems most logical and works in most browsers, that also means + there's a slight workaround some places + + - Previous song: j + - Pause/Play: k + - Next song: l + - Close directory: Ctrl+Shift+X + - Quick search: Ctrl+Shift+S or Ctrl+Alt+S + - Jumpt to current song: space diff --git a/config/.htaccess b/config/.htaccess new file mode 100644 index 0000000..8eea256 --- /dev/null +++ b/config/.htaccess @@ -0,0 +1 @@ +Order Allow,Deny diff --git a/config/.svnignore b/config/.svnignore new file mode 100644 index 0000000..35a2cc3 --- /dev/null +++ b/config/.svnignore @@ -0,0 +1,2 @@ +config.xml +metadata diff --git a/doc/INSTALL b/doc/INSTALL new file mode 100644 index 0000000..75a5f62 --- /dev/null +++ b/doc/INSTALL @@ -0,0 +1,53 @@ +Installation instructions + +1. Put either the pitchfork.conf or pitchfork_domain.conf file, found in this doc/ directory, + in /etc/http/conf.d (or where your extra config-files are) + and make sure they specify the correct directories for pitchfork. +2. Apache needs to have write access to the config directory: + - Go to where you installed pitchfork + - chown apache config (or whatever user apache is run as, can be 'nobody') + to find out who the apache-user is check output from "ps auxww | grep httpd || ps auxww|grep apache" +3. Reload/restart apache +4. Open your web-browser to where you installed apache and answer the questions ;) + +Apache access logs +The client by default requests information every second from the server, this means that a lot of +stuff will end up in your access log. To disable logging of the these requests you will have to +change the CustomLog directive in apache. It should be sufficient to add env=!pitchforknolog after it +(if you have used the given .conf file) + +Example customlog directive: +CustomLog logs/access_log combined env=!pitchforknolog + +For more information about logs: +http://httpd.apache.org/docs/2.0/logs.html#accesslog + +Required PHP-Modules: +Usually shipped with the php-distribution, but listed here for modular php-implementations. + - Hash + - SimpleXML + - JSON (not required, but highly recommended) + + +When you have problems: + + * apache refuses to start and you see "invalid command php_flag" + * The only thing you see is some PHP-code + - You have to enable PHP in httpd.conf, make sure you have + "LoadModule php5_module modules/libphp5.so" somewhere in the config file + + * All you see is a blank page OR + * Warning: require_once(PEAR/Exception.php) [function.require-once]: + failed to open stream: No such file or + directory in /pitchfork/inc/Net/MPD.php + - You need to install php-pear (might also be called PEAR-PEAR) + + * Allowed memory size of .. bytes exhausted in apache error log + - You need to increase the allowed memory size in php.ini, + set memory_limit to at least 64M + + * No metadata (ie. images/lyrics/etc) is showing + - make sure allow_url_fopen is set to on in php.ini and that the config/ directory + is writeable by apache + + diff --git a/doc/UPGRADING b/doc/UPGRADING new file mode 100644 index 0000000..6e2f195 --- /dev/null +++ b/doc/UPGRADING @@ -0,0 +1,7 @@ + +Should be sufficient to just extract the tarball and copy the old +config directory over to the new directory. (cp -A pitchfork.old/config/* pitchfork/config/) + +Remember to make sure apache has write permission to the config directory. + + diff --git a/doc/pitchfork.conf b/doc/pitchfork.conf new file mode 100644 index 0000000..e3151f2 --- /dev/null +++ b/doc/pitchfork.conf @@ -0,0 +1,15 @@ +Alias /pitchfork /var/www/pitchfork + + DirectoryIndex index.php + Options -Indexes + AllowOverride all + AddType application/x-httpd-php .php + php_flag magic_quotes_gpc off + php_flag magic_quotes_runtime off + php_flag display_errors off + php_flag log_errors on + # switch comment marks on the two items to disable access logging + # completely for pitchfork. See INSTALL + SetEnvIf Request_URI "player/command\.php" pitchforknolog + #SetEnv pitchforknolog + diff --git a/doc/pitchfork_domain.conf b/doc/pitchfork_domain.conf new file mode 100644 index 0000000..a523e8f --- /dev/null +++ b/doc/pitchfork_domain.conf @@ -0,0 +1,18 @@ + + # you will probably want to change the two following settings: + ServerName pitchfork + DocumentRoot /var/www/pitchfork + + AllowOverride all + + DirectoryIndex index.php + Options -Indexes + php_flag magic_quotes_gpc off + php_flag display_errors off + php_flag log_errors on + AddType application/x-httpd-php .php + # switch comment marks on the two items to disable access logging + # completely for pitchfork. See INSTALL + SetEnvIf Request_URI "player/command\.php" pitchforknolog + #SetEnv pitchforknolog + diff --git a/images/add.png b/images/add.png new file mode 100644 index 0000000..4857fa7 Binary files /dev/null and b/images/add.png differ diff --git a/images/audio.png b/images/audio.png new file mode 100644 index 0000000..f4ad2d4 Binary files /dev/null and b/images/audio.png differ diff --git a/images/crop.png b/images/crop.png new file mode 100644 index 0000000..73b7164 Binary files /dev/null and b/images/crop.png differ diff --git a/images/enabled_15.gif b/images/enabled_15.gif new file mode 100644 index 0000000..609485f Binary files /dev/null and b/images/enabled_15.gif differ diff --git a/images/folder.png b/images/folder.png new file mode 100644 index 0000000..4d4b9c8 Binary files /dev/null and b/images/folder.png differ diff --git a/images/gmpc-no-cover-thumb.png b/images/gmpc-no-cover-thumb.png new file mode 100644 index 0000000..d8fb76e Binary files /dev/null and b/images/gmpc-no-cover-thumb.png differ diff --git a/images/gmpc-no-cover.png b/images/gmpc-no-cover.png new file mode 100644 index 0000000..2b800b3 Binary files /dev/null and b/images/gmpc-no-cover.png differ diff --git a/images/info.png b/images/info.png new file mode 100644 index 0000000..f81de9a Binary files /dev/null and b/images/info.png differ diff --git a/images/left.png b/images/left.png new file mode 100644 index 0000000..3e0bba8 Binary files /dev/null and b/images/left.png differ diff --git a/images/lock.png b/images/lock.png new file mode 100644 index 0000000..e7546d3 Binary files /dev/null and b/images/lock.png differ diff --git a/images/media-album.png b/images/media-album.png new file mode 100644 index 0000000..b514bf7 Binary files /dev/null and b/images/media-album.png differ diff --git a/images/media-artist.png b/images/media-artist.png new file mode 100644 index 0000000..20dfeb1 Binary files /dev/null and b/images/media-artist.png differ diff --git a/images/next.png b/images/next.png new file mode 100644 index 0000000..7c1dbdb Binary files /dev/null and b/images/next.png differ diff --git a/images/pause.png b/images/pause.png new file mode 100644 index 0000000..e4f0699 Binary files /dev/null and b/images/pause.png differ diff --git a/images/play.png b/images/play.png new file mode 100644 index 0000000..66202cc Binary files /dev/null and b/images/play.png differ diff --git a/images/playlist.png b/images/playlist.png new file mode 100644 index 0000000..d8975b0 Binary files /dev/null and b/images/playlist.png differ diff --git a/images/preferences.png b/images/preferences.png new file mode 100644 index 0000000..869cc3b Binary files /dev/null and b/images/preferences.png differ diff --git a/images/previous.png b/images/previous.png new file mode 100644 index 0000000..1b4f722 Binary files /dev/null and b/images/previous.png differ diff --git a/images/quit.png b/images/quit.png new file mode 100644 index 0000000..2a1fba0 Binary files /dev/null and b/images/quit.png differ diff --git a/images/remove.png b/images/remove.png new file mode 100644 index 0000000..3042194 Binary files /dev/null and b/images/remove.png differ diff --git a/images/right.png b/images/right.png new file mode 100644 index 0000000..ac8ed0c Binary files /dev/null and b/images/right.png differ diff --git a/inc/JSON.php b/inc/JSON.php new file mode 100644 index 0000000..37fe123 --- /dev/null +++ b/inc/JSON.php @@ -0,0 +1,808 @@ + + * @author Matt Knapp + * @author Brett Stimmerman + * @copyright 2005 Michal Migurski + * @version CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $ + * @license http://www.opensource.org/licenses/bsd-license.php + * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 + */ + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_SLICE', 1); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_STR', 2); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_ARR', 3); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_OBJ', 4); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_CMT', 5); + +/** + * Behavior switch for Services_JSON::decode() + */ +define('SERVICES_JSON_LOOSE_TYPE', 16); + +/** + * Behavior switch for Services_JSON::decode() + */ +define('SERVICES_JSON_SUPPRESS_ERRORS', 32); + +/** + * Converts to and from JSON format. + * + * Brief example of use: + * + * + * // create a new instance of Services_JSON + * $json = new Services_JSON(); + * + * // convert a complexe value to JSON notation, and send it to the browser + * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4))); + * $output = $json->encode($value); + * + * print($output); + * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]] + * + * // accept incoming POST data, assumed to be in JSON notation + * $input = file_get_contents('php://input', 1000000); + * $value = $json->decode($input); + * + */ +class Services_JSON +{ + /** + * constructs a new JSON instance + * + * @param int $use object behavior flags; combine with boolean-OR + * + * possible values: + * - SERVICES_JSON_LOOSE_TYPE: loose typing. + * "{...}" syntax creates associative arrays + * instead of objects in decode(). + * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression. + * Values which can't be encoded (e.g. resources) + * appear as NULL instead of throwing errors. + * By default, a deeply-nested resource will + * bubble up with an error, so all return values + * from encode() should be checked with isError() + */ + function Services_JSON($use = 0) + { + $this->use = $use; + } + + /** + * convert a string from one UTF-16 char to one UTF-8 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf16 UTF-16 character + * @return string UTF-8 character + * @access private + */ + function utf162utf8($utf16) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); + } + + $bytes = (ord($utf16{0}) << 8) | ord($utf16{1}); + + switch(true) { + case ((0x7F & $bytes) == $bytes): + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x7F & $bytes); + + case (0x07FF & $bytes) == $bytes: + // return a 2-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xC0 | (($bytes >> 6) & 0x1F)) + . chr(0x80 | ($bytes & 0x3F)); + + case (0xFFFF & $bytes) == $bytes: + // return a 3-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xE0 | (($bytes >> 12) & 0x0F)) + . chr(0x80 | (($bytes >> 6) & 0x3F)) + . chr(0x80 | ($bytes & 0x3F)); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * convert a string from one UTF-8 char to one UTF-16 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf8 UTF-8 character + * @return string UTF-16 character + * @access private + */ + function utf82utf16($utf8) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); + } + + switch(strlen($utf8)) { + case 1: + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return $utf8; + + case 2: + // return a UTF-16 character from a 2-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x07 & (ord($utf8{0}) >> 2)) + . chr((0xC0 & (ord($utf8{0}) << 6)) + | (0x3F & ord($utf8{1}))); + + case 3: + // return a UTF-16 character from a 3-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr((0xF0 & (ord($utf8{0}) << 4)) + | (0x0F & (ord($utf8{1}) >> 2))) + . chr((0xC0 & (ord($utf8{1}) << 6)) + | (0x7F & ord($utf8{2}))); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + + /* modified for pitchfork to use echo instead of recursing over all elements and returning a *large* string */ + /* could probably remove the conversion thing as input is utf-8 and output is utf-8 */ + /** + * encodes an arbitrary variable into JSON format + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access public + */ + function encode($var) + { + switch (gettype($var)) { + case 'boolean': + echo $var ? 'true' : 'false'; + return; + + case 'NULL': + echo 'null'; + return; + + case 'integer': + echo (int) $var; + return; + + case 'double': + case 'float': + echo (float) $var; + return; + + case 'string': + // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT + $ascii = ''; + $strlen_var = strlen($var); + + /* + * Iterate over every character in the string, + * escaping with a slash or encoding to UTF-8 where necessary + */ + for ($c = 0; $c < $strlen_var; ++$c) { + + $ord_var_c = ord($var{$c}); + + switch (true) { + case $ord_var_c == 0x08: + $ascii .= '\b'; + break; + case $ord_var_c == 0x09: + $ascii .= '\t'; + break; + case $ord_var_c == 0x0A: + $ascii .= '\n'; + break; + case $ord_var_c == 0x0C: + $ascii .= '\f'; + break; + case $ord_var_c == 0x0D: + $ascii .= '\r'; + break; + + case $ord_var_c == 0x22: + case $ord_var_c == 0x2F: + case $ord_var_c == 0x5C: + // double quote, slash, slosh + $ascii .= '\\'.$var{$c}; + break; + + case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): + // characters U-00000000 - U-0000007F (same as ASCII) + $ascii .= $var{$c}; + break; + + case (($ord_var_c & 0xE0) == 0xC0): + // characters U-00000080 - U-000007FF, mask 110XXXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, ord($var{$c + 1})); + $c += 1; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF0) == 0xE0): + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2})); + $c += 2; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF8) == 0xF0): + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3})); + $c += 3; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFC) == 0xF8): + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4})); + $c += 4; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFE) == 0xFC): + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4}), + ord($var{$c + 5})); + $c += 5; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + } + } + + echo '"'.$ascii.'"'; + return; + + case 'array': + /* + * As per JSON spec if any array key is not an integer + * we must treat the the whole array as an object. We + * also try to catch a sparsely populated associative + * array with numeric keys here because some JS engines + * will create an array with empty indexes up to + * max_index which can cause memory issues and because + * the keys, which may be relevant, will be remapped + * otherwise. + * + * As per the ECMA and JSON specification an object may + * have any string as a property. Unfortunately due to + * a hole in the ECMA specification if the key is a + * ECMA reserved word or starts with a digit the + * parameter is only accessible using ECMAScript's + * bracket notation. + */ + + // treat as a JSON object + if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) { + echo "{"; + $first = true; + foreach($var as $k => $v) { + if($first) $first = false; + else echo ","; + $this->name_value($k, $v); + } + + echo '}'; + return; + } + + // treat it like a regular array + echo "["; + $first = true; + foreach($var as $e) { + if($first) $first = false; + else echo ","; + $this->encode($e); + } + echo "]"; + return; + + case 'object': + echo "{"; + $first = true; + foreach($var as $k => $v) { + if($first) + $first = false; + else echo ","; + $this->name_value($k, $v); + } + + echo '}'; + return; + + default: + echo ($this->use & SERVICES_JSON_SUPPRESS_ERRORS) + ? 'null' + : 'error encoding'; + return; + } + } + + /** + * array-walking function for use in generating JSON-formatted name-value pairs + * + * @param string $name name of key to use + * @param mixed $value reference to an array element to be encoded + * + * @return string JSON-formatted name-value pair, like '"name":value' + * @access private + */ + function name_value($name, $value) + { + $this->encode(strval($name)); + echo ':'; + $this->encode($value); + } + + /** + * reduce a string by removing leading and trailing comments and whitespace + * + * @param $str string string value to strip of comments and whitespace + * + * @return string string value stripped of comments and whitespace + * @access private + */ + function reduce_string($str) + { + $str = preg_replace(array( + + // eliminate single line comments in '// ...' form + '#^\s*//(.+)$#m', + + // eliminate multi-line comments in '/* ... */' form, at start of string + '#^\s*/\*(.+)\*/#Us', + + // eliminate multi-line comments in '/* ... */' form, at end of string + '#/\*(.+)\*/\s*$#Us' + + ), '', $str); + + // eliminate extraneous space + return trim($str); + } + + /** + * decodes a JSON string into appropriate variable + * + * @param string $str JSON-formatted string + * + * @return mixed number, boolean, string, array, or object + * corresponding to given JSON input string. + * See argument 1 to Services_JSON() above for object-output behavior. + * Note that decode() always returns strings + * in ASCII or UTF-8 format! + * @access public + */ + function decode($str) + { + $str = $this->reduce_string($str); + + switch (strtolower($str)) { + case 'true': + return true; + + case 'false': + return false; + + case 'null': + return null; + + default: + $m = array(); + + if (is_numeric($str)) { + // Lookie-loo, it's a number + + // This would work on its own, but I'm trying to be + // good about returning integers where appropriate: + // return (float)$str; + + // Return float or int, as appropriate + return ((float)$str == (integer)$str) + ? (integer)$str + : (float)$str; + + } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) { + // STRINGS RETURNED IN UTF-8 FORMAT + $delim = substr($str, 0, 1); + $chrs = substr($str, 1, -1); + $utf8 = ''; + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c < $strlen_chrs; ++$c) { + + $substr_chrs_c_2 = substr($chrs, $c, 2); + $ord_chrs_c = ord($chrs{$c}); + + switch (true) { + case $substr_chrs_c_2 == '\b': + $utf8 .= chr(0x08); + ++$c; + break; + case $substr_chrs_c_2 == '\t': + $utf8 .= chr(0x09); + ++$c; + break; + case $substr_chrs_c_2 == '\n': + $utf8 .= chr(0x0A); + ++$c; + break; + case $substr_chrs_c_2 == '\f': + $utf8 .= chr(0x0C); + ++$c; + break; + case $substr_chrs_c_2 == '\r': + $utf8 .= chr(0x0D); + ++$c; + break; + + case $substr_chrs_c_2 == '\\"': + case $substr_chrs_c_2 == '\\\'': + case $substr_chrs_c_2 == '\\\\': + case $substr_chrs_c_2 == '\\/': + if (($delim == '"' && $substr_chrs_c_2 != '\\\'') || + ($delim == "'" && $substr_chrs_c_2 != '\\"')) { + $utf8 .= $chrs{++$c}; + } + break; + + case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)): + // single, escaped unicode character + $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) + . chr(hexdec(substr($chrs, ($c + 4), 2))); + $utf8 .= $this->utf162utf8($utf16); + $c += 5; + break; + + case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): + $utf8 .= $chrs{$c}; + break; + + case ($ord_chrs_c & 0xE0) == 0xC0: + // characters U-00000080 - U-000007FF, mask 110XXXXX + //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 2); + ++$c; + break; + + case ($ord_chrs_c & 0xF0) == 0xE0: + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 3); + $c += 2; + break; + + case ($ord_chrs_c & 0xF8) == 0xF0: + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 4); + $c += 3; + break; + + case ($ord_chrs_c & 0xFC) == 0xF8: + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 5); + $c += 4; + break; + + case ($ord_chrs_c & 0xFE) == 0xFC: + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 6); + $c += 5; + break; + + } + + } + + return $utf8; + + } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) { + // array, or object notation + + if ($str{0} == '[') { + $stk = array(SERVICES_JSON_IN_ARR); + $arr = array(); + } else { + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = array(); + } else { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = new stdClass(); + } + } + + array_push($stk, array('what' => SERVICES_JSON_SLICE, + 'where' => 0, + 'delim' => false)); + + $chrs = substr($str, 1, -1); + $chrs = $this->reduce_string($chrs); + + if ($chrs == '') { + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } else { + return $obj; + + } + } + + //print("\nparsing {$chrs}\n"); + + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c <= $strlen_chrs; ++$c) { + + $top = end($stk); + $substr_chrs_c_2 = substr($chrs, $c, 2); + + if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) { + // found a comma that is not inside a string, array, etc., + // OR we've reached the end of the character list + $slice = substr($chrs, $top['where'], ($c - $top['where'])); + array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false)); + //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + // we are in an array, so just push an element onto the stack + array_push($arr, $this->decode($slice)); + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + // we are in an object, so figure + // out the property name and set an + // element in an associative array, + // for now + $parts = array(); + + if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // "name":value pair + $key = $this->decode($parts[1]); + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // name:value pair, where name is unquoted + $key = $parts[1]; + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } + + } + + } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) { + // found a quote, and we are not inside a string + array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c})); + //print("Found start of string at {$c}\n"); + + } elseif (($chrs{$c} == $top['delim']) && + ($top['what'] == SERVICES_JSON_IN_STR) && + ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) { + // found a quote, we're in a string, and it's not escaped + // we know that it's not escaped becase there is _not_ an + // odd number of backslashes at the end of the string so far + array_pop($stk); + //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '[') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-bracket, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false)); + //print("Found start of array at {$c}\n"); + + } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) { + // found a right-bracket, and we're in an array + array_pop($stk); + //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '{') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-brace, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false)); + //print("Found start of object at {$c}\n"); + + } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) { + // found a right-brace, and we're in an object + array_pop($stk); + //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($substr_chrs_c_2 == '/*') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a comment start, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false)); + $c++; + //print("Found start of comment at {$c}\n"); + + } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) { + // found a comment end, and we're in one now + array_pop($stk); + $c++; + + for ($i = $top['where']; $i <= $c; ++$i) + $chrs = substr_replace($chrs, ' ', $i, 1); + + //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } + + } + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + return $obj; + + } + + } + } + } + + /** + * @todo Ultimately, this should just call PEAR::isError() + */ + function isError($data, $code = null) + { + if (class_exists('pear')) { + return PEAR::isError($data, $code); + } elseif (is_object($data) && (get_class($data) == 'services_json_error' || + is_subclass_of($data, 'services_json_error'))) { + return true; + } + + return false; + } +} + +if (class_exists('PEAR_Error')) { + + class Services_JSON_Error extends PEAR_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + parent::PEAR_Error($message, $code, $mode, $options, $userinfo); + } + } + +} else { + + /** + * @todo Ultimately, this class shall be descended from PEAR_Error + */ + class Services_JSON_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + + } + } + +} + +?> diff --git a/inc/Net/MPD.php b/inc/Net/MPD.php new file mode 100644 index 0000000..df84823 --- /dev/null +++ b/inc/Net/MPD.php @@ -0,0 +1,119 @@ + + * @copyright 2006 Graham Christensen + * @license http://www.php.net/license/3_01.txt + * @version CVS: $ID:$ + */ + + +require_once 'PEAR/Exception.php'; +require_once 'MPD/Common.php'; + + +/** + * Central class for the Music Player Daemon objects + * + * Used to utilize the functionality of the provided classes + * + * @category Networking + * @package Net_MPD + * @author Graham Christensen + * @copyright 2006 Graham Christensen + * @license http://www.php.net/license/3_01.txt + * @version CVS: $ID:$ + */ +class Net_MPD +{ + /** + * The Net_MPD_Admin object + */ + public $Admin; + + /** + * The Net_MPD_Common object + */ + public $Common; + + /** + * The Net_MPD_Database object + */ + public $Database; + + /** + * The Net_MPD_Playback object + */ + public $Playback; + + /** + * The Net_MPD_Playlist object + */ + public $Playlist; + + /** + * Creates new instances of objects + * @param $host Host to connect to, optional (default: localhost) + * @param $port Port to connect to, optional (default: 6600) + * @param $pass Pass to connect to, optional (default: null) + * @return object or false on failure + */ + function __construct($host = 'localhost', $port = 6600, $pass = null) + { + $this->Admin = self::factory('Admin' , $host, $port, $pass); + $this->Common = self::factory('Common' , $host, $port, $pass); + $this->Database = self::factory('Database', $host, $port, $pass); + $this->Playback = self::factory('Playback', $host, $port, $pass); + $this->Playlist = self::factory('Playlist', $host, $port, $pass); + } + + /** + * Creates new instances of objects + * @param $class Class to initiate, with out Net_MPD_$class + * @param $host Host to connect to, optional (default: localhost) + * @param $port Port to connect to, optional (default: 6600) + * @param $pass Pass to connect to, optional (default: null) + * @return object or false on failure + */ + public static function factory($class, $host = 'localhost', $port = 6600, $pass = null) + { + $class = ucfirst(strtolower($class)); + + if (!self::_loadClass($class)) { + return false; + } + + $class = 'Net_MPD_' . $class; + $obj = new $class($host, $port, $pass); + + return $obj; + } + + /** + * Loads the class + * @param $class Class to include, with out Net_MPD_ + * @return bool + */ + protected static function _loadClass($class) + { + if (class_exists('Net_MPD_' . $class)) { + return true; + } + require_once 'Net/MPD/' . $class . '.php'; + return true; + } +} +?> \ No newline at end of file diff --git a/inc/Net/MPD/Common.php b/inc/Net/MPD/Common.php new file mode 100644 index 0000000..d921154 --- /dev/null +++ b/inc/Net/MPD/Common.php @@ -0,0 +1,492 @@ + + * @copyright 2006 Graham Christensen + * @license http://www.php.net/license/3_01.txt + * @version CVS: $ID:$ + */ + +/** + * API for the common peices of Music Player Daemon commands + * + * Used for basic interaction and output handeling, as well as + * several standard commands. + * + * @category Networking + * @package Net_MPD + * @author Graham Christensen + * @copyright 2006 Graham Christensen + * @license http://www.php.net/license/3_01.txt + * @version CVS: $ID:$ + */ +class Net_MPD_Common +{ + //Connection & Write Errors + const CONNECTION_NOT_OPENED = 100; + const CONNECTION_FAILED = 102; + const WRITE_FAILED = 103; + + //MPD Errors + const ACK_NOT_LIST = 1; + const ACK_ARG = 2; + const ACK_PASSWORD = 3; + const ACK_PERMISSION = 4; + const ACK_UNKOWN = 5; + const ACK_NO_EXIST = 50; + const ACK_PLAYLIST_MAX = 51; + const ACK_SYSTEM = 52; + const ACK_PLAYLIST_LOAD = 53; + const ACK_UPDATE_ALREADY = 54; + const ACK_PLAYER_SYNC = 55; + const ACK_EXIST = 56; + const ACK_COMMAND_FAILED = -100; + + //MPD Responces + const RESPONSE_OK = 'OK'; + + private $_connection = null; + protected $_errors = array(); + private $_current_error = array(); + protected $_commands = array(); + protected $_output = array(); + private $connection_params = array(); + + + + /** + * Set connection params + * + * @param $host host to connect to, (default: localhost) + * @param $port port to connec through, (default: 6600) + * @param $password password to send, (default: null) + * @return void + */ + function __construct($host = 'localhost', $port = 6600, $password = null) + { + $this->connection_params['host'] = $host; + $this->connection_params['port'] = $port; + $this->connection_params['password'] = $password; + } + + + + /** + * Connect to MPD + * + * @return bool + */ + public function connect() + { + if ($this->isConnected()) { + return true; + } + $connection = @fsockopen($this->connection_params['host'], $this->connection_params['port'], $errn, $errs, 4); + if ($connection) { + $this->_connection = $connection; + // Read from the source until its ready for commands + //$this->read(); + while (!feof($this->_connection)) { + $line = fgets($this->_connection); + if (trim(substr($line, 0, 2)) == self::RESPONSE_OK) { + break; + } + } + if (!is_null($this->connection_params['password'])) { + if (!$this->runCommand('password', $this->connection_params['password'])) { + throw new PEAR_Exception('Password invalid.', self::ACK_PASSWORD); + } + } + return true; + } + throw new PEAR_Exception('Error connecting: '.$errn.' ; '.$errs, self::CONNECTION_FAILED); + } + + + + /** + * Check connection status + * + * @return bool + */ + public function isConnected() + { + if (!is_resource($this->_connection)) { + return false; + } + return true; + } + + + + /** + * Disconnect from MPD + * + * @return bool + */ + public function disconnect() + { + $this->runCommand('close'); + fclose($this->_connection); + $this->_connection = null; + return true; + } + + + + /** + * Write data to the socket + * + * @param $command string data to be sent + * + * @return bool + * + */ + function write($data) + { + //Are we connected? + if (!$this->isConnected()) { + // Try to connect + $this->connect(); + } + //Write the data + if (!fwrite($this->_connection, $data."\r\n")) { + throw new PEAR_Exception('Write failed', self::WRITE_FAILED); + } + $this->_commands[] = $data; + return true; + } + + + + /** + * Read data from the socket + * + * @return array of raw output + * + */ + function read() + { + //Are we connected? + if (!$this->isConnected()) { + throw new PEAR_Exception('Not connected', self::CONNECTION_NOT_OPENED); + } + //Loop through the connection, putting the data into $line + $output = array(); + while (!feof($this->_connection)) { + $line = fgets($this->_connection); + if (preg_match('/^ACK \[(.*?)\@(.*?)\] \{(.*?)\} (.*?)$/', $line, $matches)) { + //If the output is an ACK error + //$this->runCommand('clearerror'); //Cleanup the error + $this->_errors[] = $matches; + $this->_current_error = array('ack' => $matches[1], 'func' => $matches[3], 'error' => $matches[4]); + throw new PEAR_Exception('Command Failed', self::ACK_COMMAND_FAILED); + } elseif (trim($line) == self::RESPONSE_OK) { + //The last line of output was hit, close the loop + break; + } else { + //Output from the server added to the return array + $output[] = $line; + } + } + return $output; + } + + + + /** + * Get the current error data + * + * @return array of error data + */ + public function getErrorData() + { + return $this->_current_error; + } + + + public function clearError() { + $this->runCommand('clearerror'); //Cleanup the error + } + + /** + * Run command + * + * @param $command string a command to be executed through MPD + * @param $args mixed string for a single argument, array for multiple + * @param $parse mixed false to parse the output, int for parse style + * + * @return array of server output + */ + public function runCommand($command, $args = array(), $parse = 0) + { + //Generate the command + if (is_array($args)) { + foreach($args as $arg) { + $command.= ' "'.str_replace('"', '\"', $arg) .'"'; + } + } elseif (!is_null($args)) { + $command.= ' "'.str_replace('"', '\"', $args) .'"'; + } + //Write and then capture the output + $this->write($command); + $output = $this->read(); + + $this->_output[] = array($command, $output); + if ($output === array()) { + return true; + } + if ($parse !== false) { + return $this->parseOutput($output, $parse); + } + return $output; + } + + + + /** + * Parse MPD output on a line-by-line basis + * creating output that is easy to work with + * + * @param $input array of input from MPD + * @param $style int style number,'0' for the "intelligent" sorting + * + * @return array + */ + public function parseOutput($input, $style = 0) + { + if (!is_array($input)) { + $input = array($input); + } + $count = array('outputs' => 0, 'file' => -1, 'key' => 0); + $used_keys = array(); + $output = array(); + $prev = array('key' => null, 'value' => null); + $dirtoggle = false; + foreach($input as $line) { + if (is_array($line)) { + $this->_errors[] = 'Server output not expected: '.print_r($line, true); + continue; + } else { + $parts = explode(': ', $line, 2); + if (!isset($parts[0], $parts[1])) { + $this->errors[] = 'Server output not expected: '.$line; + continue; + } + } + $key = trim($parts[0]); + $value = trim($parts[1]); + if ($style == 0) { + switch ($key) { + //The following has to do strictly + //with files in the output + case 'file': + case 'Artist': + case 'Album': + case 'Title': + case 'Track': + case 'Name': + case 'Genre': + case 'Date': + case 'Composer': + case 'Performer': + case 'Comment': + case 'Disc': + case 'Id': + case 'Pos': + case 'Time': + case 'cpos': + if ($key == 'file'||$key== 'cpos') { + $count['file']++; + } + $output['file'][$count['file']][$key] = $value; + break; + + //The next section is for a 'stats' call + case 'artists': + case 'albums': + case 'songs': + case 'uptime': + case 'playtime': + case 'db_playtime': + case 'db_update': + $output['stats'][$key] = $value; + break; + + //Now for a status call: + case 'volume': + case 'repeat': + case 'random': + case 'playlistlength': + case 'xfade': + case 'state': + case 'song': + case 'songid': + case 'time': + case 'bitrate': + case 'audio': + case 'updating_db': + case 'error': + $output['status'][$key] = $value; + break; + + //Outputs + case 'outputid': + case 'outputname': + case 'outputenabled': + if ($key == 'outputid') { + $count['outputs']++; + } + $output['outputs'][$count['outputs']][$key] = $value; + break; + + //The 'playlist' case works in 2 scenarios + //1) in a file/directory listing + //2) in a status call + // This is to determine if it was in a status call + // or in a directory call. + case 'playlist': + if ($prev['key'] == 'random') { + $output['status'][$key] = $value; + } else { + $output[$key][] = $value; + } + break; + + //Now that we've covered most of the weird + //options of output, + //lets cover everything else! + default: + if (isset($used_keys[$key])) { + $used_keys = array(); + $count['key']++; + } + $used_keys[$key] = true; + //$output[$count['key']][$key] = $value;//This is rarely useful + $output[$key][] = $value; + break; + } + } elseif ($style == 1) { + $output[$key][] = $value; + } + if ($key == 'directory') { + $dirtoggle = true; + } + $prev['key'] = $key; + $prev['value'] = $value; + } + return $output; + } + + + + /** + * A method to access errors + * + * @return array + */ + public function getErrors() + { + return $this->_errors; + } + + + + /** + * Used to discover commands that are not available + * + * @return array (null on no functions not being available) + */ + public function getNotCommands() + { + $cmds = $this->runCommand('notcommands'); + if (!isset($cmds['command'])) { + return array(); + } + return $cmds['command']; + } + + + + /** + * Used to discover which commands are available + * + * @return array (null on no functions being available + */ + public function getCommands() + { + $cmds = $this->runCommand('commands'); + if (!isset($cmds['command'])) { + return array(); + } + return $cmds['command']; + } + + public function hasCommand($name) { + $cmds = $this->getCommands(); + foreach ($cmds as $cmd) + if($cmd==$name) + return true; + return false; + } + + + + /** + * Ping the MPD server to keep the connection running + * + * @return bool + */ + public function ping() + { + if ($this->runCommand('ping')) { + return true; + } + return false; + } + + + + /** + * Get various statistics about the MPD server + * + * @return array + */ + public function getStats() + { + $stats = $this->runCommand('stats'); + if (!isset($stats['stats'])) { + return false; + } + return $stats['stats']; + } + + + + /** + * Get the status of the MPD server + * + * @return array + */ + public function getStatus() + { + $status = $this->runCommand('status'); + if (!isset($status['status'])) { + return false; + } + return $status['status']; + } +} +?> diff --git a/inc/Net/Net/MPD/Admin.php b/inc/Net/Net/MPD/Admin.php new file mode 100644 index 0000000..5ca9c85 --- /dev/null +++ b/inc/Net/Net/MPD/Admin.php @@ -0,0 +1,89 @@ + + * @copyright 2006 Graham Christensen + * @license http://www.php.net/license/3_01.txt + * @version CVS: $ID:$ + */ +class Net_MPD_Admin extends Net_MPD_Common +{ + + /** + * List available audio outputs + * + * @return array or int on failure + */ + public function getOutputs() + { + return $this->runCommand('outputs'); + } + + /** + * Disables an audio output + * + * @param $id int output Id to disable + * @return bool + */ + public function disableOutput($id) + { + return $this->runCommand('disableoutput', $id); + } + + /** + * Enables an audio output + * + * @param $id int Id to enable + * @return bool + */ + public function enableOutput($id) + { + return $this->runCommand('enableoutput', $id); + } + + /** + * Kills the MPD server in a safe way, saving state if possible + * + * @return bool + */ + public function kill() + { + $r = $this->runCommand('kill'); + if ($r) { + @$this->disconnect(); + } + return true; + } + + /** + * Updates the music database + * + * @param $path string path which to search for music, optional + * @return bool + */ + public function updateDatabase($path = '') + { + $this->runCommand('update', $path); + return true; + } +} +?> diff --git a/inc/Net/Net/MPD/Database.php b/inc/Net/Net/MPD/Database.php new file mode 100644 index 0000000..78b1e64 --- /dev/null +++ b/inc/Net/Net/MPD/Database.php @@ -0,0 +1,123 @@ + + * @copyright 2006 Graham Christensen + * @license http://www.php.net/license/3_01.txt + * @version CVS: $ID:$ + */ + +/** + * API for the database portion of Music Player Daemon commands + * + * Used for maintaining and working with the MPD database + * + * @category Networking + * @package Net_MPD + * @author Graham Christensen + * @copyright 2006 Graham Christensen + * @license http://www.php.net/license/3_01.txt + * @version CVS: $ID:$ + */ +class Net_MPD_Database extends Net_MPD_Common +{ + /** + * Case sensitive search for data in the database + * + * @param array $params array('search_field' => 'search for') + * @param bool $caseSensitive True for case sensitivity, false for not [default false] + * @return array + */ + public function find($params, $caseSensitive = false) + { + $prms = array(); + foreach($params as $key => $value) { + $prms[] = $key; + $prms[] = $value; + } + $cmd = $caseSensitive ? 'find' : 'search'; + + $out = $this->runCommand($cmd, $prms); + if (!isset($out['file'])) { + return array(); + } + return $out['file']; + } + + + + /** + * List all metadata of matches to the search + * + * @param string $metadata1 metadata to list + * @param string $metadata2 metadata field to search in, optional + * @param string $search data to search for in search field, + * required if search field provided + * @return array + */ + public function getMetadata($metadata1, $metadata2 = null, $search = null) + { + //Make sure that if metadata2 is set, search is as well + if (!is_null($metadata2)) { + if (is_null($search)) { + return false; + } + } + if (!is_null($metadata2)) { + $out = $this->runCommand('list', array($metadata1, $metadata2, $search), 1); + } else { + $out = $this->runCommand('list', $metadata1, 1); + } + return $out[$metadata1]; + } + + + + /** + * Lists all files and folders in the directory recursively + * + * @param $dir string directory to start in, optional + * @return array + */ + public function getAll($dir = '') + { + return $this->runCommand('listall', $dir, 1); + } + + + + /** + * Lists all files/folders recursivly, listing any related informaiton + * + * @param $dir string directory to start in, optional + * @return array + */ + public function getAllInfo($dir = '') + { + return $this->runCommand('listallinfo', $dir); + } + + /** + * Lists content of the directory + * + * @param $dir string directory to work in, optional + * @return array + */ + public function getInfo($dir = '') + { + return $this->runCommand('lsinfo', $dir); + } +} +?> diff --git a/inc/Net/Net/MPD/Playback.php b/inc/Net/Net/MPD/Playback.php new file mode 100644 index 0000000..f6e5b35 --- /dev/null +++ b/inc/Net/Net/MPD/Playback.php @@ -0,0 +1,213 @@ + + * @copyright 2006 Graham Christensen + * @license http://www.php.net/license/3_01.txt + * @version CVS: $ID:$ + */ +/** + * API for the playback portion of Music Player Daemon commands + * + * For controlling playback aspects of MPD + * + * @category Networking + * @package Net_MPD + * @author Graham Christensen + * @copyright 2006 Graham Christensen + * @license http://www.php.net/license/3_01.txt + * @version CVS: $ID:$ + */ +class Net_MPD_Playback extends Net_MPD_Common +{ + /** + * Gets the current song and related information + * + * @return array of data + */ + public function getCurrentSong() + { + $out = $this->runCommand('currentsong'); + if (!isset($out['file'][0])) { + return false; + } + return $out['file'][0]; + } + + + + /** + * Set crossfade between songs + * + * @param $sec int, seconds to crossfade + * @return bool + */ + public function setCrossfade($sec) + { + $this->runCommand('crossfade', $sec); + return true; + } + + + + /** + * Continue to the next song + * + * @return bool + */ + public function nextSong() + { + $this->runCommand('next'); + return true; + } + + /** + * Go back to the previous song + * + * @return bool + */ + public function previousSong() + { + $this->runCommand('previous'); + return true; + } + + + + /** + * Pauses or plays the audio + * + * @return bool + */ + public function pause() + { + $this->runCommand('pause'); + return true; + } + + + + /** + * Starts playback + * + * @param $song int, song position in playlist to start playing at + * @return bool + */ + public function play($song = 0) + { + $this->runCommand('play', $song); + return true; + } + + /** + * Starts playback by Id + * + * @param $song int, song Id + * @return bool + */ + public function playId($song = 0) + { + $this->runCommand('playid', $song); + return true; + } + + + + /** + * Sets 'random' mode on/off + * + * @param $on bool true or false, for random or not (respectively), + optional + * @return bool + */ + public function random($on = false) + { + $this->runCommand('random', (int)$on); + return true; + } + + + + /** + * Sets 'random' mode on/off + * @access public + * @param $on bool true or false, for repeat or not (respectively), + optional + * @return true + */ + public function repeat($on = false) + { + $this->runCommand('repeat', (int)$on); + return true; + } + + + + /** + * Seek a position in a song + * + * @param $song int song position in playlist + * @param $time int time in seconds to seek to + * @return bool + */ + public function seek($song, $time) + { + $this->runCommand('seek', array($song, $time)); + return true; + } + + + + /** + * Seek a position in a song + * + * @param $song int song Id + * @param $time int time in seconds to seek to + * @return bool + */ + public function seekId($song, $time) + { + $this->runCommand('seekid', array($song, $time)); + return true; + } + + + + /** + * Set volume + * + * @param $vol int volume + * @return true + */ + public function setVolume($vol) + { + $this->runCommand('setvol', $vol); + return true; + } + + + + /** + * Stop playback + * + * @return bool + */ + public function stop() + { + $this->runCommand('stop'); + return true; + } +} +?> \ No newline at end of file diff --git a/inc/Net/Net/MPD/Playlist.php b/inc/Net/Net/MPD/Playlist.php new file mode 100644 index 0000000..6c67f22 --- /dev/null +++ b/inc/Net/Net/MPD/Playlist.php @@ -0,0 +1,306 @@ + + * @copyright 2006 Graham Christensen + * @license PHP License 3.01 + * @version CVS: $ID:$ + */ + +/** + * API for the playlist portion of Music Player Daemon commands + * + * Used for maintaining, creating, and utilizing playlists in MPD + * + * @category Networking + * @package Net_MPD + * @author Graham Christensen + * @copyright 2006 Graham Christensen + * @license http://www.php.net/license/3_01.txt + * @vers + */ +class Net_MPD_Playlist extends Net_MPD_Common +{ + /** + * List playlists in specified directory + * + * @param $dir string directory path, optional + * @return bool true on success int on failure + */ + public function getPlaylists($dir = '') + { + $out = $this->runCommand('lsinfo', $dir); + return $out['playlist']; + } + + /** + * Search for data in the playlist + * + * @param array $params array('search_field' => 'search for') + * @param bool $caseSensitive True for case sensitivity, false for not [default false] + * @return array + */ + public function find($params, $caseSensitive = false) + { + $prms = array(); + foreach($params as $key => $value) { + $prms[] = $key; + $prms[] = $value; + } + $cmd = $caseSensitive ? 'playlistfind' : 'playlistsearch'; + + $out = $this->runCommand($cmd, $prms); + if (!isset($out['file'])) { + return array(); + } + return $out['file']; + } + + /* Tests whether playlistfind is avilable. If playlistfind + * is available playlistsearch is as well + * + * @return boolean + */ + public function hasFind() { + return $this->hasCommand("playlistfind"); + } + + /** + * Add file to playlist + * + * @param $file string filename + * @return bool + */ + public function addSong($file) + { + $this->runCommand('add', $file); + return true; + } + + + + /** + * Clear the playlist + * + * @return bool + */ + public function clear() + { + $this->runCommand('clear'); + return true; + } + + + + /** + * Gets the current song and related information + * + * @return array of data + */ + public function getCurrentSong() + { + $out = $this->runCommand('currentsong'); + if (!isset($out['file'][0])) { + return false; + } + return $out['file'][0]; + } + + + + /** + * Delete song from playlist + * + * @param $song int song position in playlist + * @return bool + */ + public function deleteSong($song) + { + $this->runCommand('delete', $song); + return true; + } + + + + /** + * Delete song from playlist by song Id + * + * @param $id int song Id + * @return bool + */ + public function deleteSongId($id) + { + $this->runCommand('deleteid', $id); + return true; + } + + + + /** + * Loads a playlist into the current playlist + * + * @param $playlist string playlist name + * @return bool + */ + public function loadPlaylist($playlist) + { + $this->runCommand('load', $playlist); + return true; + } + + + + /** + * Move song in the playlist + * + * @param $from int song position in the playlist + * @param $to int song position to move it to + * @return bool + */ + public function moveSong($from, $to) + { + $this->runCommand('move', array($from, $to)); + return true; + } + + + + /** + * Move song in the playlist by Id + * + * @param $from int song Id + * @param $to int song Id to move it to + * @return bool + */ + public function moveSongId($fromId, $toId) + { + $this->runCommand('moveid', array($fromId, $toId)); + return true; + } + + + + /** + * Displays metadata for songs in the playlist by position Id + * + * @param $song int song position, optional + * @return array of song metadata + */ + public function getPlaylistInfo($song = null) + { + $out = $this->runCommand('playlistinfo', $song, 0); + return isset($out['file']) ? $out['file'] : array(); + } + + + + /** + * Displays metadata for songs in the playlist + * + * @param $song int song Id, optional + * @return array of song metadata + */ + public function getPlaylistInfoId($song = null) + { + return $this->runCommand('playlistid', $song); + } + + + + /** + * Get playlist changes + * + * @param $version int playlist version + * @param $limit boolean true to limit return + * to only position and id + * + * @return array of changes + */ + public function getChanges($version, $limit = false) + { + $cmd = $limit ? 'plchangesposid' : 'plchanges'; + + return $this->runCommand($cmd, $version); + } + + + + /** + * Delete a playlist + * + * @param $playlist string playlist name + * @return true + */ + public function deletePlaylist($playlist) + { + $this->runCommand('rm', $playlist); + return true; + } + + + + /** + * Save the playlist + * + * @param $playlist string playlist name + * @return bool + */ + public function savePlaylist($playlist) + { + $this->runCommand('save', $playlist); + return true; + } + + + + /** + * Shuffle the playlist + * + * @return true + */ + public function shuffle() + { + $this->runCommand('shuffle'); + return true; + } + + + + /** + * Swap song by position in the playlist + * + * @param $song1 int song position from + * @param $song2 int song position to + * @return bool + */ + public function swapSong($song1, $song2) + { + $this->runCommand('swap', array($song1, $song2)); + return true; + } + + + + /** + * Swaps a song with another song, by Id + * + * @param $song1 int Id of the first song + * @param $song2 int Id of the second song + * @return true + */ + public function swapSongId($songId1, $songId2) + { + $this->runCommand('swapid', array($songId1, $songId2)); + return true; + } +} +?> diff --git a/inc/Net/package.xml b/inc/Net/package.xml new file mode 100644 index 0000000..e5ea912 --- /dev/null +++ b/inc/Net/package.xml @@ -0,0 +1,135 @@ + + + + Net_MPD + Music Player Daemon interaction API + Rrovides wrappers to easily use the MPD socket commands. + + + + itrebal + Graham Christensen + graham.christensen@itrebal.com + lead + + + + 1.0.0 + 2007-01-18 + PHP License 3.01 + stable + Removed unecissary try/catch blocks. Moving to stable. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.1.0dev1 + 2006-11-07 + devel + Initial release. + + + + 0.2.0dev + 2006-11-12 + devel + - Copied `getCurrentSong' from Net_MPD_Playlist to Net_MPD_Playback. +- Changed MPD.php to provied an object uniting the sub-objects. $mpd->Playlist->addSong() for example. Left the factory for other uses. +- Changed MPD/Common.php to auto-connect when a command is run. This is to prevent several connections being opend without need, if the Net_MPD class is used to group them all. Also simplifies the connection process. +- Moved the connection parameters back into the __construct method of the Net_MPD_Common class. +- Bugfix: defined $output before adding to it as an array, in Net_MPD_Common->read() function. + + + + 0.3.0dev + 2006-12-10 + devel + - Fixed bug #9392; Error codes can now be accessed using Exception::getCode() +- Fixed bug #9334; Now installs into PEAR/Net/ +- Fixed bug #9341; Now permits $e->getCode(); + + + + 1.0.0 + 2007-01-18 + stable + Removed unecissary try/catch blocks. Moving to stable. + + + + diff --git a/inc/base.php b/inc/base.php new file mode 100644 index 0000000..fbc1072 --- /dev/null +++ b/inc/base.php @@ -0,0 +1,190 @@ +config"); + exit(); +} + +$language = get_config("lang", "en"); + + +$selected_theme = get_config("theme", "default"); +if(!is_theme_dir_ok($theme_dir . $selected_theme)) + $selected_theme = "default"; + +$lpass = get_config('login_pass'); + +if(!is_null($lpass)&&$lpass!="") { + if(!isset($_SESSION['logged_in'])||!$_SESSION['logged_in']) { + if(!isset($no_require_login)) { + header("Location: login.php"); + echo "Wrong password"; + exit(); + } + } +} + +function get_config($name, $default = null) { + global $config; + if(isset($config->$name)) { + if(trim($config->$name)=="") + return $default; + return ((string)$config->$name); + } + else { + return $default; + } +} + +function get_selected_plfields() { + global $config, $pl_fields; + $plentry = false; + if(isset($config->plentry)) + $plentry = $config->plentry; + $ret = array(); + $i = 0; + foreach($pl_fields as $field) { + if($plentry) { + $ret[] = ((string)$plentry->$field)!=""; + } + else { + $ret[] = $i++<3; + } + } + return $ret; +} + +function set_config($name, $value) { + +} +/* if a key is found to be numeric it will be replaced by numb_replace +*/ +function array_to_xml($arr, $xml = null, $numb_replace = "elem") { + if(is_null($xml)) { + $xml = simplexml_load_string(""); + } + foreach($arr as $key => $value) { + if(is_numeric($key)) + $key = $numb_replace; + if(is_array($value)) { + $tmp = $xml->addChild($key); + array_to_xml($value, $tmp, $numb_replace); + } + else { + $xml->addChild($key, htmlspecialchars($value)); + } + } + return $xml; +} + +function get_mpd($type) { + try { + $port = 6600; + if(is_numeric(get_config("mpd_port"))) + $port = (int) get_config("mpd_port"); + $ret = Net_MPD::factory($type, get_config('mpd_host'), intval($port), get_config("mpd_pass")); + $ret->connect(); + return $ret; + } + catch(PEAR_Exception $e) { + return false; + } +} +function get_playlist() { + return get_mpd("Playlist"); +} +function get_playback() { + return get_mpd("Playback"); +} +function get_database() { + return get_mpd("Database"); +} +function get_admin() { + return get_mpd("Admin"); +} + +/* mimic behaviour of java System.currentTimeMillis() */ +// ex: 1172151695935.8 +function current_time_millis() { + return microtime(true)*1000; +} + +// returns array with available themes +function get_available_themes() { + global $theme_dir; + $dirs = scandir($theme_dir); + $themes = array(); + foreach($dirs as $dir) { + if($dir=="."||$dir=="..") + continue; + + if(is_theme_dir_ok($theme_dir . $dir)) { + $themes[] = $dir; + } + } + return $themes; +} + +function is_theme_dir_ok($tdir) { + return is_dir($tdir) && file_exists($tdir . "/theme.css") && file_exists($tdir . "/theme.js"); +} + +function generate_hash($val, $salt = false) { + if(function_exists("hash")) { + if($salt===false) + $salt = substr(md5(uniqid(rand(), true)), 0, SALT_LENGTH); + + $p = hash("sha256", $val . $salt); + return "sha:" . $p . $salt; + } + else return $val; +} +function check_hash($proper, $check) { + $len = strlen($proper); + $nhash = generate_hash($check, substr($proper, $len-SALT_LENGTH)); + if($proper==$nhash) { + return true; + } + return false; +} + +/* someone find me a php equiv */ +function str_byte_count($data) { + if(function_exists("mb_strlen")) { + /* count bytes, not characters if utf-8, + * I imagine this works, but hard to actually test */ + return mb_strlen($data, "ascii"); + } + else { + return strlen($data); + } +} +?> diff --git a/inc/function_test.php b/inc/function_test.php new file mode 100644 index 0000000..5dc63ee --- /dev/null +++ b/inc/function_test.php @@ -0,0 +1,35 @@ + diff --git a/index.php b/index.php new file mode 100644 index 0000000..2f8247e --- /dev/null +++ b/index.php @@ -0,0 +1,21 @@ + diff --git a/jorbis/build.xml b/jorbis/build.xml new file mode 100644 index 0000000..e893b20 --- /dev/null +++ b/jorbis/build.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jorbis/jorbis-pitchfork.jar b/jorbis/jorbis-pitchfork.jar new file mode 100644 index 0000000..8117b26 Binary files /dev/null and b/jorbis/jorbis-pitchfork.jar differ diff --git a/jorbis/src/JOrbisPlayer.java b/jorbis/src/JOrbisPlayer.java new file mode 100644 index 0000000..1f8d062 --- /dev/null +++ b/jorbis/src/JOrbisPlayer.java @@ -0,0 +1,714 @@ +/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */ +/* JOrbisPlayer -- pure Java Ogg Vorbis player + * + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec and + * JOrbisPlayer depends on JOrbis. + * + * 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., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +/* + * Modified 2007 by Roger Bystrøm for pitchfork + */ + +import java.util.*; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.*; + +import java.awt.*; +import java.awt.event.*; +import java.applet.*; +import javax.swing.*; + +import com.jcraft.jorbis.*; +import com.jcraft.jogg.*; + +import javax.sound.sampled.*; + +public class JOrbisPlayer extends JApplet implements ActionListener { + + Thread player=null; + InputStream bitStream=null; + URLConnection urlc = null; + + public static final String PLAY = "Play", STOP = "Stop"; + + static AppletContext acontext=null; + + static final int BUFSIZE=4096*2; + static int convsize=BUFSIZE*2; + static byte[] convbuffer=new byte[convsize]; + + PlayWatch playThread; + + private int RETRY=3; + int retry=RETRY; + + SyncState oy; + StreamState os; + Page og; + Packet op; + Info vi; + Comment vc; + DspState vd; + Block vb; + + byte[] buffer=null; + int bytes=0; + + int format; + int rate=0; + int channels=0; + int left_vol_scale=100; + int right_vol_scale=100; + SourceDataLine outputLine=null; + String current_source=null; + + int frameSizeInBytes; + int bufferLengthInBytes; + + boolean playonstartup=false; + + public Color bgColor = Color.lightGray; + + public void init(){ + + playThread = new PlayWatch(); + playThread.start(); + + acontext=getAppletContext(); + + loadPlaylist(); + + if(playlist.size()>0){ + String s=getParameter("jorbis.player.playonstartup"); + if(s!=null && s.equals("yes")) + playonstartup=true; + } + + String c = getParameter("jorbis.player.bgcolor"); + + try { + bgColor = new Color(Integer.parseInt(c)); + } catch (Exception e) {} + + System.out.println("Bg-color: specified: " +c + ", using: " + bgColor.toString()); + + initUI(); + + getContentPane().setLayout(new BorderLayout()); + getContentPane().add(panel); + setBackground(bgColor); + repaint(); + + } + + public void start(){ + super.start(); + if(playonstartup){ + play_sound(); + } + } + + void init_jorbis(){ + oy=new SyncState(); + os=new StreamState(); + og=new Page(); + op=new Packet(); + + vi=new Info(); + vc=new Comment(); + vd=new DspState(); + vb=new Block(vd); + + buffer=null; + bytes=0; + + oy.init(); + } + + public void closeOutputLine() { + if(outputLine!=null){ + //outputLine.drain(); + outputLine.stop(); + outputLine.flush(); + outputLine.close(); + outputLine = null; + } + } + + public void closeBitStream() { + if(bitStream!=null) { + try { + bitStream.close(); + } catch(Exception ee) {} + } + } + + SourceDataLine getOutputLine(int channels, int rate) throws Exception { + if(outputLine==null || this.rate!=rate || this.channels!=channels){ + closeOutputLine(); + init_audio(channels, rate); + outputLine.start(); + } + return outputLine; + } + + void init_audio(int channels, int rate) throws Exception { + try { + + AudioFormat audioFormat = + new AudioFormat((float)rate, + 16, + channels, + true, // PCM_Signed + false // littleEndian + ); + DataLine.Info info = + new DataLine.Info(SourceDataLine.class, + audioFormat, + AudioSystem.NOT_SPECIFIED); + if (!AudioSystem.isLineSupported(info)) { + //System.out.println("Line " + info + " not supported."); + return; + } + + try{ + outputLine = (SourceDataLine) AudioSystem.getLine(info); + //outputLine.addLineListener(this); + outputLine.open(audioFormat); + } catch (LineUnavailableException ex) { + System.out.println("Unable to open the sourceDataLine: " + ex); + if(acontext != null) + acontext.showStatus("Streaming applet: unable to open output device"); + throw ex; + } + + frameSizeInBytes = audioFormat.getFrameSize(); + int bufferLengthInFrames = outputLine.getBufferSize()/frameSizeInBytes/2; + bufferLengthInBytes = bufferLengthInFrames * frameSizeInBytes; + + this.rate=rate; + this.channels=channels; + } + catch(Exception ee){ + System.out.println(ee); + closeOutputLine(); + throw ee; + } + } + + private void play_stream(Thread me) { + + boolean chained=false; + + init_jorbis(); + + retry=RETRY; + + loop: + while(true){ + int eos=0; + + int index=oy.buffer(BUFSIZE); + buffer=oy.data; + try{ + bytes=bitStream.read(buffer, index, BUFSIZE); + } + catch(Exception e){ + System.err.println(e); + return; + } + oy.wrote(bytes); + + if(chained){ + chained=false; + } + else { + if(oy.pageout(og)!=1){ + if(bytes0) + stat.append(" " +sb); + acontext.showStatus(stat.toString()); + } + } + + convsize=BUFSIZE/vi.channels; + + vd.synthesis_init(vi); + vb.init(vd); + + float[][][] _pcmf=new float[1][][]; + int[] _index=new int[vi.channels]; + + try { + getOutputLine(vi.channels, vi.rate); + } catch(Exception e) { + stop_sound(); + return; + } + + while(eos==0){ + while(eos==0){ + + if(player!=me){ + System.err.println("player!=me bye."); + closeBitStream(); + closeOutputLine(); + if(acontext!=null) + acontext.showStatus(""); + return; + } + + int result=oy.pageout(og); + if(result==0)break; // need more data + if(result==-1){ // missing or corrupt data at this page position + //System.err.println("Corrupt or missing data in bitstream; continuing..."); + } + else{ + os.pagein(og); + + if(og.granulepos()==0){ // + chained=true; // + eos=1; // + break; // + } // + + while(true){ + result=os.packetout(op); + if(result==0)break; // need more data + if(result==-1){ + // missing or corrupt data at this page position + } + else{ + // we have a packet. Decode it + int samples; + if(vb.synthesis(op)==0){ // test for success! + vd.synthesis_blockin(vb); + } + while((samples=vd.synthesis_pcmout(_pcmf, _index))>0){ + float[][] pcmf=_pcmf[0]; + int bout=(samples32767){ + val=32767; + } + if(val<-32768){ + val=-32768; + } + if(val<0) val=val|0x8000; + convbuffer[ptr]=(byte)(val); + convbuffer[ptr+1]=(byte)(val>>>8); + ptr+=2*(vi.channels); + } + } + outputLine.write(convbuffer, 0, 2*vi.channels*bout); + vd.synthesis_read(bout); + } + } + } + if(og.eos()!=0)eos=1; + } + } + + if(eos==0){ + index=oy.buffer(BUFSIZE); + buffer=oy.data; + try{ bytes=bitStream.read(buffer,index,BUFSIZE); } + catch(Exception e){ + System.err.println(e); + closeOutputLine(); + return; + } + if(bytes==-1){ + break; + } + oy.wrote(bytes); + if(bytes==0)eos=1; + } + } + + os.clear(); + vb.clear(); + vd.clear(); + vi.clear(); + } + + closeOutputLine(); + oy.clear(); + if(acontext!=null) + acontext.showStatus(""); + + closeBitStream(); + } + + public void stop(){ + if(player==null){ + closeOutputLine(); + closeBitStream(); + } + player=null; + } + + Vector playlist=new Vector(); + + public void actionPerformed(ActionEvent e){ + String command=((JButton)(e.getSource())).getText(); + if(command.equals(PLAY) && player==null){ + play_sound(); + } + else if(player!=null){ + stop_sound(); + } + } + + private void playFromOwnThread() { + synchronized(playThread) { + playThread.notify(); + } + } + + private void playFromThisThread() { + if(player!=null) + return; + /*player=new Thread(this); + player.start();*/ + // todo + start_button.setText(STOP); + String item=getShoutSource(); + if(item==null) { + stop_sound(); + return; + } + bitStream=selectSource(item); + player = Thread.currentThread(); + if(bitStream!=null){ + play_stream(player); + } + else System.out.println("Bitstream is null"); + bitStream=null; + + stop_sound(); + } + + + /* hooks */ + + public boolean isPlaying() { + return player != null; + } + + public void play_sound(){ + playFromOwnThread(); + } + + public void stop_sound(){ + player=null; + start_button.setText(PLAY); + + } + + InputStream selectSource(String item){ + if(item.endsWith(".pls")){ + item=fetch_pls(item); + if(item==null) return null; + //System.out.println("fetch: "+item); + } + else if(item.endsWith(".m3u")){ + item=fetch_m3u(item); + if(item==null)return null; + //System.out.println("fetch: "+item); + } + + if(!item.endsWith(".ogg")){ + return null; + } + + InputStream is=null; + URLConnection urlc=null; + try{ + URL url=null; + url=new URL(getCodeBase(), item); + urlc=url.openConnection(); + urlc.setRequestProperty("Pragma", "no-cache"); + urlc.setUseCaches(false); + is=urlc.getInputStream(); + current_source=url.getProtocol()+"://"+url.getHost()+":"+url.getPort()+url.getFile(); + } + catch(Exception ee){ + System.err.println(ee); + } + + if(is==null) { + System.out.println("Selected input stream is null"); + return null; + } + + System.out.println("Select: "+item); + return is; + } + + String fetch_pls(String pls){ + InputStream pstream=null; + if(pls.startsWith("http://")){ + try{ + URL url=null; + url=new URL(getCodeBase(), pls); + urlc=url.openConnection(); + pstream=urlc.getInputStream(); + } + catch(Exception ee){ + System.err.println(ee); + return null; + } + } + + String line=null; + while(true){ + try{line=readline(pstream);}catch(Exception e){} + if(line==null)break; + if(line.startsWith("File1=")){ + byte[] foo=line.getBytes(); + int i=6; + for(;i + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jogg; + +public class Buffer{ + private static final int BUFFER_INCREMENT=256; + + private static final int[] mask={ + 0x00000000,0x00000001,0x00000003,0x00000007,0x0000000f, + 0x0000001f,0x0000003f,0x0000007f,0x000000ff,0x000001ff, + 0x000003ff,0x000007ff,0x00000fff,0x00001fff,0x00003fff, + 0x00007fff,0x0000ffff,0x0001ffff,0x0003ffff,0x0007ffff, + 0x000fffff,0x001fffff,0x003fffff,0x007fffff,0x00ffffff, + 0x01ffffff,0x03ffffff,0x07ffffff,0x0fffffff,0x1fffffff, + 0x3fffffff,0x7fffffff,0xffffffff + }; + + int ptr=0; + byte[] buffer=null; + int endbit=0; + int endbyte=0; + int storage=0; + + public void writeinit(){ + buffer=new byte[BUFFER_INCREMENT]; + ptr=0; + buffer[0]=(byte)'\0'; + storage=BUFFER_INCREMENT; + } + + public void write(byte[] s){ + for(int i=0; iret="+Integer.toHexString(ret)); + if(bits>32 && endbit!=0){ + ret|=((buffer[ptr+4])&0xff)<<(32-endbit); +// ret|=((byte)(buffer[ptr+4]))<<(32-endbit); + } + } + } + } + return(m&ret); + } + + public int look1(){ + if(endbyte>=storage)return(-1); + return((buffer[ptr]>>endbit)&1); + } + + public void adv(int bits){ + bits+=endbit; + ptr+=bits/8; + endbyte+=bits/8; + endbit=bits&7; + } + + public void adv1(){ + ++endbit; + if(endbit>7){ + endbit=0; + ptr++; + endbyte++; + } + } + + public int read(int bits){ +//System.err.println(this+" read: bits="+bits+", storage="+storage+", endbyte="+endbyte); +//System.err.println(this+" read: bits="+bits+", storage="+storage+", endbyte="+endbyte+ +// ", ptr="+ptr+", endbit="+endbit+", buf[ptr]="+buffer[ptr]); + + int ret; + int m=mask[bits]; + + bits+=endbit; + + if(endbyte+4>=storage){ + ret=-1; + if(endbyte+(bits-1)/8>=storage){ + ptr+=bits/8; + endbyte+=bits/8; + endbit=bits&7; + return(ret); + } + } + +/* + ret=(byte)(buffer[ptr]>>>endbit); + if(bits>8){ + ret|=(buffer[ptr+1]<<(8-endbit)); + if(bits>16){ + ret|=(buffer[ptr+2]<<(16-endbit)); + if(bits>24){ + ret|=(buffer[ptr+3]<<(24-endbit)); + if(bits>32 && endbit>0){ + ret|=(buffer[ptr+4]<<(32-endbit)); + } + } + } + } +*/ + ret=((buffer[ptr])&0xff)>>>endbit; + if(bits>8){ + ret|=((buffer[ptr+1])&0xff)<<(8-endbit); +// ret|=((byte)(buffer[ptr+1]))<<(8-endbit); + if(bits>16){ + ret|=((buffer[ptr+2])&0xff)<<(16-endbit); +// ret|=((byte)(buffer[ptr+2]))<<(16-endbit); + if(bits>24){ + ret|=((buffer[ptr+3])&0xff)<<(24-endbit); +// ret|=((byte)(buffer[ptr+3]))<<(24-endbit); + if(bits>32 && endbit!=0){ + ret|=((buffer[ptr+4])&0xff)<<(32-endbit); +// ret|=((byte)(buffer[ptr+4]))<<(32-endbit); + } + } + } + } + + ret&=m; + + ptr+=bits/8; +// ptr=bits/8; + endbyte+=bits/8; +// endbyte=bits/8; + endbit=bits&7; + return(ret); + } + + public int readB(int bits){ + //System.err.println(this+" read: bits="+bits+", storage="+storage+", endbyte="+endbyte+ + // ", ptr="+ptr+", endbit="+endbit+", buf[ptr]="+buffer[ptr]); + int ret; + int m=32-bits; + + bits+=endbit; + + if(endbyte+4>=storage){ + /* not the main path */ + ret=-1; + if(endbyte*8+bits>storage*8) { + ptr+=bits/8; + endbyte+=bits/8; + endbit=bits&7; + return(ret); + } + } + + ret=(buffer[ptr]&0xff)<<(24+endbit); + if(bits>8){ + ret|=(buffer[ptr+1]&0xff)<<(16+endbit); + if(bits>16){ + ret|=(buffer[ptr+2]&0xff)<<(8+endbit); + if(bits>24){ + ret|=(buffer[ptr+3]&0xff)<<(endbit); + if(bits>32 && (endbit != 0)) + ret|=(buffer[ptr+4]&0xff)>>(8-endbit); + } + } + } + ret=(ret>>>(m>>1))>>>((m+1)>>1); + + ptr+=bits/8; + endbyte+=bits/8; + endbit=bits&7; + return(ret); + } + + public int read1(){ + int ret; + if(endbyte>=storage){ + ret=-1; + endbit++; + if(endbit>7){ + endbit=0; + ptr++; + endbyte++; + } + return(ret); + } + + ret=(buffer[ptr]>>endbit)&1; + + endbit++; + if(endbit>7){ + endbit=0; + ptr++; + endbyte++; + } + return(ret); + } + + public int bytes(){ + return(endbyte+(endbit+7)/8); + } + + public int bits(){ + return(endbyte*8+endbit); + } + + public byte[] buffer(){ + return(buffer); + } + + public static int ilog(int v){ + int ret=0; + while(v>0){ + ret++; + v>>>=1; + } + return(ret); + } + + public static void report(String in){ + System.err.println(in); + System.exit(1); + } + + /* + static void cliptest(int[] b, int vals, int bits, int[] comp, int compsize){ + int bytes; + byte[] buffer; + + o.reset(); + for(int i=0;i + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jogg; + +public class Packet{ + public byte[] packet_base; + public int packet; + public int bytes; + public int b_o_s; + public int e_o_s; + + public long granulepos; + + public long packetno; // sequence number for decode; the framing + // knows where there's a hole in the data, + // but we need coupling so that the codec + // (which is in a seperate abstraction + // layer) also knows about the gap + + /* + // TEST + static int sequence=0; + static int lastno=0; + void checkpacket(int len, int no, int pos){ + if(bytes!=len){ + System.err.println("incorrect packet length!"); + System.exit(1); + } + if(granulepos!=pos){ + System.err.println("incorrect packet position!"); + System.exit(1); + } + + // packet number just follows sequence/gap; adjust the input number + // for that + if(no==0){ + sequence=0; + } + else{ + sequence++; + if(no>lastno+1) + sequence++; + } + lastno=no; + if(packetno!=sequence){ + System.err.println("incorrect packet sequence "+packetno+" != "+sequence); + System.exit(1); + } + + // Test data + for(int j=0;j + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jogg; + +public class Page{ + private static int[] crc_lookup=new int[256]; + static { + for(int i=0; i>>24)&0xff)^(header_base[header+i]&0xff)]; + } + for(int i=0;i>>24)&0xff)^(body_base[body+i]&0xff)]; + } + header_base[header+22]=(byte)crc_reg/*&0xff*/; + header_base[header+23]=(byte)(crc_reg>>>8)/*&0xff*/; + header_base[header+24]=(byte)(crc_reg>>>16)/*&0xff*/; + header_base[header+25]=(byte)(crc_reg>>>24)/*&0xff*/; + } + public Page copy(){ + return copy(new Page()); + } + public Page copy(Page p){ + byte[] tmp=new byte[header_len]; + System.arraycopy(header_base, header, tmp, 0, header_len); + p.header_len=header_len; + p.header_base=tmp; + p.header=0; + tmp=new byte[body_len]; + System.arraycopy(body_base, body, tmp, 0, body_len); + p.body_len=body_len; + p.body_base=tmp; + p.body=0; + return p; + } + /* + // TEST + static StreamState os_en, os_de; + static SyncState oy; + void check_page(byte[] data_base, int data, int[] _header){ + // Test data + for(int j=0;j 4k... "); + test_pack(packets,headret); + } + + { + // continuing packet test + int[] packets={0,4345,259,255,-1}; + + int[] head1={0x4f,0x67,0x67,0x53,0,0x02, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,0,0,0,0, + 0xff,0x7b,0x23,0x17, + 1, + 0}; + + int[] head2={0x4f,0x67,0x67,0x53,0,0x00, + 0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,1,0,0,0, + 0x34,0x24,0xd5,0x29, + 17, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255}; + + int[] head3={0x4f,0x67,0x67,0x53,0,0x05, + 0x07,0x0c,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,2,0,0,0, + 0xc8,0xc3,0xcb,0xed, + 5, + 10,255,4,255,0}; + int[][] headret={head1,head2,head3,null}; + + System.err.print("testing single packet page span... "); + test_pack(packets,headret); + } + + // page with the 255 segment limit + { + + int[] packets={0,10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,50,-1}; + + int[] head1={0x4f,0x67,0x67,0x53,0,0x02, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,0,0,0,0, + 0xff,0x7b,0x23,0x17, + 1, + 0}; + + int[] head2={0x4f,0x67,0x67,0x53,0,0x00, + 0x07,0xfc,0x03,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,1,0,0,0, + 0xed,0x2a,0x2e,0xa7, + 255, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10}; + + int[] head3={0x4f,0x67,0x67,0x53,0,0x04, + 0x07,0x00,0x04,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,2,0,0,0, + 0x6c,0x3b,0x82,0x3d, + 1, + 50}; + int[][] headret={head1,head2,head3,null}; + + System.err.print("testing max packet segments... "); + test_pack(packets,headret); + } + + { + // packet that overspans over an entire page + + int[] packets={0,100,9000,259,255,-1}; + + int[] head1={0x4f,0x67,0x67,0x53,0,0x02, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,0,0,0,0, + 0xff,0x7b,0x23,0x17, + 1, + 0}; + + int[] head2={0x4f,0x67,0x67,0x53,0,0x00, + 0x07,0x04,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,1,0,0,0, + 0x3c,0xd9,0x4d,0x3f, + 17, + 100,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255}; + + int[] head3={0x4f,0x67,0x67,0x53,0,0x01, + 0x07,0x04,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,2,0,0,0, + 0xbd,0xd5,0xb5,0x8b, + 17, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255}; + + int[] head4={0x4f,0x67,0x67,0x53,0,0x05, + 0x07,0x10,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,3,0,0,0, + 0xef,0xdd,0x88,0xde, + 7, + 255,255,75,255,4,255,0}; + int[][] headret={head1,head2,head3,head4,null}; + + System.err.print("testing very large packets... "); + test_pack(packets,headret); + } + + { + // term only page. why not? + + int[] packets={0,100,4080,-1}; + + int[] head1={0x4f,0x67,0x67,0x53,0,0x02, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,0,0,0,0, + 0xff,0x7b,0x23,0x17, + 1, + 0}; + + int[] head2={0x4f,0x67,0x67,0x53,0,0x00, + 0x07,0x04,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,1,0,0,0, + 0x3c,0xd9,0x4d,0x3f, + 17, + 100,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255}; + + int[] head3={0x4f,0x67,0x67,0x53,0,0x05, + 0x07,0x08,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,2,0,0,0, + 0xd4,0xe0,0x60,0xe5, + 1,0}; + + int[][] headret={head1,head2,head3,null}; + + System.err.print("testing zero data page (1 nil packet)... "); + test_pack(packets,headret); + } + + { + // build a bunch of pages for testing + byte[] data=new byte[1024*1024]; + int[] pl={0,100,4079,2956,2057,76,34,912,0,234,1000,1000,1000,300,-1}; + int inptr=0; + Page[] og=new Page[5]; + for(int i=0; i<5; i++){ + og[i]=new Page(); + } + + os_en.reset(); + + for(int i=0;pl[i]!=-1;i++){ + Packet op=new Packet(); + int len=pl[i]; + + op.packet_base=data; + op.packet=inptr; + op.bytes=len; + op.e_o_s=(pl[i+1]<0?1:0); + op.granulepos=(i+1)*1000; + + for(int j=0;j0)error(); + + // Test fractional page inputs: incomplete fixed header + index=oy.buffer(og[1].header_len); + System.arraycopy(og[1].header_base, og[1].header+3, + oy.data, index, 20); + + oy.wrote(20); + if(oy.pageout(og_de)>0)error(); + + // Test fractional page inputs: incomplete header + index=oy.buffer(og[1].header_len); + System.arraycopy(og[1].header_base, og[1].header+23, + oy.data, index, 5); + oy.wrote(5); + if(oy.pageout(og_de)>0)error(); + + // Test fractional page inputs: incomplete body + index=oy.buffer(og[1].header_len); + System.arraycopy(og[1].header_base, og[1].header+28, + oy.data, index, og[1].header_len-28); + oy.wrote(og[1].header_len-28); + if(oy.pageout(og_de)>0)error(); + + index=oy.buffer(og[1].body_len); + System.arraycopy(og[1].body_base, og[1].body, + oy.data, index, 1000); + oy.wrote(1000); + if(oy.pageout(og_de)>0)error(); + + index=oy.buffer(og[1].body_len); + System.arraycopy(og[1].body_base, og[1].body+1000, + oy.data, index, og[1].body_len-1000); + oy.wrote(og[1].body_len-1000); + if(oy.pageout(og_de)<=0)error(); + System.err.println("ok."); + } + + // Test fractional page inputs: page + incomplete capture + { + Page og_de=new Page(); + System.err.print("Testing sync on 1+partial inputs... "); + oy.reset(); + + int index=oy.buffer(og[1].header_len); + System.arraycopy(og[1].header_base, og[1].header, + oy.data, index, og[1].header_len); + oy.wrote(og[1].header_len); + + index=oy.buffer(og[1].body_len); + System.arraycopy(og[1].body_base, og[1].body, + oy.data, index, og[1].body_len); + oy.wrote(og[1].body_len); + + index=oy.buffer(og[1].header_len); + System.arraycopy(og[1].header_base, og[1].header, + oy.data, index, 20); + oy.wrote(20); + if(oy.pageout(og_de)<=0)error(); + if(oy.pageout(og_de)>0)error(); + + index=oy.buffer(og[1].header_len); + System.arraycopy(og[1].header_base, og[1].header+20, + oy.data, index, og[1].header_len-20); + oy.wrote(og[1].header_len-20); + index=oy.buffer(og[1].body_len); + System.arraycopy(og[1].body_base, og[1].body, + oy.data, index, og[1].body_len); + + oy.wrote(og[1].body_len); + if(oy.pageout(og_de)<=0)error(); + + System.err.println("ok."); + } + +// // // // // // // // // + // Test recapture: garbage + page + { + Page og_de=new Page(); + System.err.print("Testing search for capture... "); + oy.reset(); + + // 'garbage' + int index=oy.buffer(og[1].body_len); + System.arraycopy(og[1].body_base, og[1].body, + oy.data, index, og[1].body_len); + oy.wrote(og[1].body_len); + + index=oy.buffer(og[1].header_len); + System.arraycopy(og[1].header_base, og[1].header, + oy.data, index, og[1].header_len); + oy.wrote(og[1].header_len); + + index=oy.buffer(og[1].body_len); + System.arraycopy(og[1].body_base, og[1].body, + oy.data, index, og[1].body_len); + oy.wrote(og[1].body_len); + + index=oy.buffer(og[2].header_len); + System.arraycopy(og[2].header_base, og[2].header, + oy.data, index, 20); + + oy.wrote(20); + if(oy.pageout(og_de)>0)error(); + if(oy.pageout(og_de)<=0)error(); + if(oy.pageout(og_de)>0)error(); + + index=oy.buffer(og[2].header_len); + System.arraycopy(og[2].header_base, og[2].header+20, + oy.data, index, og[2].header_len-20); + oy.wrote(og[2].header_len-20); + index=oy.buffer(og[2].body_len); + System.arraycopy(og[2].body_base, og[2].body, + oy.data, index, og[2].body_len); + oy.wrote(og[2].body_len); + if(oy.pageout(og_de)<=0)error(); + + System.err.println("ok."); + } + + // Test recapture: page + garbage + page + { + Page og_de=new Page(); + System.err.print("Testing recapture... "); + oy.reset(); + + int index=oy.buffer(og[1].header_len); + System.arraycopy(og[1].header_base, og[1].header, + oy.data, index, og[1].header_len); + oy.wrote(og[1].header_len); + + index=oy.buffer(og[1].body_len); + System.arraycopy(og[1].body_base, og[1].body, + oy.data, index, og[1].body_len); + oy.wrote(og[1].body_len); + + index=oy.buffer(og[2].header_len); + System.arraycopy(og[2].header_base, og[2].header, + oy.data, index, og[2].header_len); + oy.wrote(og[2].header_len); + + index=oy.buffer(og[2].header_len); + System.arraycopy(og[2].header_base, og[2].header, + oy.data, index, og[2].header_len); + oy.wrote(og[2].header_len); + + if(oy.pageout(og_de)<=0)error(); + + index=oy.buffer(og[2].body_len); + System.arraycopy(og[2].body_base, og[2].body, + oy.data, index, og[2].body_len-5); + oy.wrote(og[2].body_len-5); + + index=oy.buffer(og[3].header_len); + System.arraycopy(og[3].header_base, og[3].header, + oy.data, index, og[3].header_len); + oy.wrote(og[3].header_len); + + index=oy.buffer(og[3].body_len); + System.arraycopy(og[3].body_base, og[3].body, + oy.data, index, og[3].body_len); + oy.wrote(og[3].body_len); + + if(oy.pageout(og_de)>0)error(); + if(oy.pageout(og_de)<=0)error(); + + System.err.println("ok."); + } + } + //return(0); + } + */ +} diff --git a/jorbis/src/com/jcraft/jogg/StreamState.java b/jorbis/src/com/jcraft/jogg/StreamState.java new file mode 100644 index 0000000..2f34b37 --- /dev/null +++ b/jorbis/src/com/jcraft/jogg/StreamState.java @@ -0,0 +1,657 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jogg; + +public class StreamState{ + byte[] body_data; /* bytes from packet bodies */ + int body_storage; /* storage elements allocated */ + int body_fill; /* elements stored; fill mark */ +private int body_returned; /* elements of fill returned */ + + + int[] lacing_vals; /* The values that will go to the segment table */ + long[] granule_vals; /* pcm_pos values for headers. Not compact + this way, but it is simple coupled to the + lacing fifo */ + int lacing_storage; + int lacing_fill; + int lacing_packet; + int lacing_returned; + + byte[] header=new byte[282]; /* working space for header encode */ + int header_fill; + + public int e_o_s; /* set when we have buffered the last packet in the + logical bitstream */ + int b_o_s; /* set after we've written the initial page + of a logical bitstream */ + int serialno; + int pageno; + long packetno; /* sequence number for decode; the framing + knows where there's a hole in the data, + but we need coupling so that the codec + (which is in a seperate abstraction + layer) also knows about the gap */ + long granulepos; + + public StreamState(){ + init(); + } + + StreamState(int serialno){ + this(); + init(serialno); + } + void init(){ + body_storage=16*1024; + body_data=new byte[body_storage]; + lacing_storage=1024; + lacing_vals=new int[lacing_storage]; + granule_vals=new long[lacing_storage]; + } + public void init(int serialno){ + if(body_data==null){ init(); } + else{ + for(int i=0; i255?255:lacing_fill); + int bytes=0; + int acc=0; + long granule_pos=granule_vals[0]; + + if(maxvals==0)return(0); + + /* construct a page */ + /* decide how many segments to include */ + + /* If this is the initial header case, the first page must only include + the initial header packet */ + if(b_o_s==0){ /* 'initial header page' case */ + granule_pos=0; + for(vals=0;vals4096)break; + acc+=(lacing_vals[vals]&0x0ff); + granule_pos=granule_vals[vals]; + } + } + + /* construct the header in temp storage */ + System.arraycopy("OggS".getBytes(), 0, header, 0, 4); + + /* stream structure version */ + header[4]=0x00; + + /* continued packet flag? */ + header[5]=0x00; + if((lacing_vals[0]&0x100)==0)header[5]|=0x01; + /* first page flag? */ + if(b_o_s==0) header[5]|=0x02; + /* last page flag? */ + if(e_o_s!=0 && lacing_fill==vals) header[5]|=0x04; + b_o_s=1; + + /* 64 bits of PCM position */ + for(i=6;i<14;i++){ + header[i]=(byte)granule_pos; + granule_pos>>>=8; + } + + /* 32 bits of stream serial number */ + { + int _serialno=serialno; + for(i=14;i<18;i++){ + header[i]=(byte)_serialno; + _serialno>>>=8; + } + } + + /* 32 bits of page counter (we have both counter and page header + because this val can roll over) */ + if(pageno==-1)pageno=0; /* because someone called + stream_reset; this would be a + strange thing to do in an + encode stream, but it has + plausible uses */ + { + int _pageno=pageno++; + for(i=18;i<22;i++){ + header[i]=(byte)_pageno; + _pageno>>>=8; + } + } + + /* zero for computation; filled in later */ + header[22]=0; + header[23]=0; + header[24]=0; + header[25]=0; + + /* segment table */ + header[26]=(byte)vals; + for(i=0;i>>=8; +// } +// +// /* 32 bits of stream serial number */ +// { +// int serialn=serialno; +// for(int i=14;i<18;i++){ +// header[i]=(byte)serialn; +// serialn>>>=8; +// } +// } +// +// +///* 32 bits of page counter (we have both counter and page header +// because this val can roll over) */ +// if(pageno==-1)pageno=0; /* because someone called +// stream_reset; this would be a +// strange thing to do in an +// encode stream, but it has +// plausible uses */ +// { +// int pagen=pageno++; +// for(int i=18;i<22;i++){ +// header[i]=(byte)pagen; +// pagen>>>=8; +// } +// } +// +// /* zero for computation; filled in later */ +// header[22]=0; +// header[23]=0; +// header[24]=0; +// header[25]=0; +// +// /* segment table */ +// header[26]=(byte)vals; +// for(int i=0;i 4096 || /* 'page nominal size' case */ + lacing_fill>=255 || /* 'segment table full' case */ + (lacing_fill!=0&&b_o_s==0)){ /* 'initial header page' case */ + return flush(og); + } + return 0; + } + + public int eof(){ + return e_o_s; + } + + public int reset(){ + body_fill=0; + body_returned=0; + + lacing_fill=0; + lacing_packet=0; + lacing_returned=0; + + header_fill=0; + + e_o_s=0; + b_o_s=0; + pageno=-1; + packetno=0; + granulepos=0; + return(0); + } +} diff --git a/jorbis/src/com/jcraft/jogg/SyncState.java b/jorbis/src/com/jcraft/jogg/SyncState.java new file mode 100644 index 0000000..b3705e5 --- /dev/null +++ b/jorbis/src/com/jcraft/jogg/SyncState.java @@ -0,0 +1,275 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jogg; + +// DECODING PRIMITIVES: packet streaming layer + +// This has two layers to place more of the multi-serialno and paging +// control in the application's hands. First, we expose a data buffer +// using ogg_decode_buffer(). The app either copies into the +// buffer, or passes it directly to read(), etc. We then call +// ogg_decode_wrote() to tell how many bytes we just added. +// +// Pages are returned (pointers into the buffer in ogg_sync_state) +// by ogg_decode_stream(). The page is then submitted to +// ogg_decode_page() along with the appropriate +// ogg_stream_state* (ie, matching serialno). We then get raw +// packets out calling ogg_stream_packet() with a +// ogg_stream_state. See the 'frame-prog.txt' docs for details and +// example code. + +public class SyncState{ + + public byte[] data; + int storage; + int fill; + int returned; + + int unsynced; + int headerbytes; + int bodybytes; + + public int clear(){ + data=null; + return(0); + } + +// !!!!!!!!!!!! +// byte[] buffer(int size){ + public int buffer(int size){ + // first, clear out any space that has been previously returned + if(returned!=0){ + fill-=returned; + if(fill>0){ + System.arraycopy(data, returned, data, 0, fill); + } + returned=0; + } + + if(size>storage-fill){ + // We need to extend the internal buffer + int newsize=size+fill+4096; // an extra page to be nice + if(data!=null){ + byte[] foo=new byte[newsize]; + System.arraycopy(data, 0, foo, 0, data.length); + data=foo; + } + else{ + data=new byte[newsize]; + } + storage=newsize; + } + + // expose a segment at least as large as requested at the fill mark +// return((char *)oy->data+oy->fill); +// return(data); + return(fill); + } + + public int wrote(int bytes){ + if(fill+bytes>storage)return(-1); + fill+=bytes; + return(0); + } + +// sync the stream. This is meant to be useful for finding page +// boundaries. +// +// return values for this: +// -n) skipped n bytes +// 0) page not ready; more data (no bytes skipped) +// n) page synced at current location; page length n bytes + private Page pageseek=new Page(); + private byte[] chksum=new byte[4]; + public int pageseek(Page og){ + int page=returned; + int next; + int bytes=fill-returned; + + if(headerbytes==0){ + int _headerbytes,i; + if(bytes<27)return(0); // not enough for a header + + /* verify capture pattern */ +//!!!!!!!!!!! + if(data[page]!='O' || + data[page+1]!='g' || + data[page+2]!='g' || + data[page+3]!='S'){ + headerbytes=0; + bodybytes=0; + + // search for possible capture + next=0; + for(int ii=0; iibytes)return(0); + + // The whole test page is buffered. Verify the checksum + synchronized(chksum){ + // Grab the checksum bytes, set the header field to zero + + System.arraycopy(data, page+22, chksum, 0, 4); + data[page+22]=0; + data[page+23]=0; + data[page+24]=0; + data[page+25]=0; + + // set up a temp page struct and recompute the checksum + Page log=pageseek; + log.header_base=data; + log.header=page; + log.header_len=headerbytes; + + log.body_base=data; + log.body=page+headerbytes; + log.body_len=bodybytes; + log.checksum(); + + // Compare + if(chksum[0]!=data[page+22] || + chksum[1]!=data[page+23] || + chksum[2]!=data[page+24] || + chksum[3]!=data[page+25]){ + // D'oh. Mismatch! Corrupt page (or miscapture and not a page at all) + // replace the computed checksum with the one actually read in + System.arraycopy(chksum, 0, data, page+22, 4); + // Bad checksum. Lose sync */ + + headerbytes=0; + bodybytes=0; + // search for possible capture + next=0; + for(int ii=0; ii0){ + // have a page + return(1); + } + if(ret==0){ + // need more data + return(0); + } + + // head did not start a synced page... skipped some bytes + if(unsynced==0){ + unsynced=1; + return(-1); + } + // loop. keep looking + } + } + +// clear things to an initial state. Good to call, eg, before seeking + public int reset(){ + fill=0; + returned=0; + unsynced=0; + headerbytes=0; + bodybytes=0; + return(0); + } + public void init(){} + + public int getDataOffset(){ return returned; } + public int getBufferOffset(){ return fill; } +} diff --git a/jorbis/src/com/jcraft/jorbis/AllocChain.java b/jorbis/src/com/jcraft/jorbis/AllocChain.java new file mode 100644 index 0000000..b3492d5 --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/AllocChain.java @@ -0,0 +1,31 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +class AllocChain{ + Object ptr; + AllocChain next; +}; diff --git a/jorbis/src/com/jcraft/jorbis/Block.java b/jorbis/src/com/jcraft/jorbis/Block.java new file mode 100644 index 0000000..8fd15f7 --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/Block.java @@ -0,0 +1,188 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +public class Block{ + ///necessary stream state for linking to the framing abstraction + float[][] pcm=new float[0][]; // this is a pointer into local storage + Buffer opb=new Buffer(); + + int lW; + int W; + int nW; + int pcmend; + int mode; + + int eofflag; + long granulepos; + long sequence; + DspState vd; // For read-only access of configuration + + // local storage to avoid remallocing; it's up to the mapping to + // structure it +//byte[] localstore; +//int localtop; +//int localalloc; +//int totaluse; +//AllocChain reap; + + // bitmetrics for the frame + int glue_bits; + int time_bits; + int floor_bits; + int res_bits; + + public Block(DspState vd){ + this.vd=vd; +// localalloc=0; +// localstore=null; + if(vd.analysisp!=0){ + opb.writeinit(); + } + } + + public void init(DspState vd){ + this.vd=vd; + } + +// int alloc(int bytes){ +// bytes=(bytes+(8-1))&(~(8-1)); +// if(bytes+localtop>localalloc){ +// if(localstore!=null){ +// AllocChain link=new AllocChain(); +// totaluse+=localtop; +// link.next=reap; +// link.ptr=localstore; +// reap=link; +// } +// // highly conservative +// localalloc=bytes; +// localstore=new byte[localalloc]; +// localtop=0; +// } +// { +// int foo=localtop; +// //void *ret=(void *)(((char *)vb->localstore)+vb->localtop); +// localtop+=bytes; +// return foo; +// } +// } + + // reap the chain, pull the ripcord +// void ripcord(){ +// // reap the chain +// while(reap!=null){ +// AllocChain next=reap.next; +// //free(reap->ptr); +// reap.ptr=null; +// //memset(reap,0,sizeof(struct alloc_chain)); +// //free(reap); +// reap=next; +// } +// // consolidate storage +// if(totaluse!=0){ +// //vb->localstore=realloc(vb->localstore,vb->totaluse+vb->localalloc); +// byte[] foo=new byte[totaluse+localalloc]; +// System.arraycopy(localstore, 0, foo, 0, localstore.length); +// localstore=foo; +// localalloc+=totaluse; +// totaluse=0; +// } +// // pull the ripcord +// localtop=0; +// reap=null; +// } + + public int clear(){ + if(vd!=null){ + if(vd.analysisp!=0){ + opb.writeclear(); + } + } + //ripcord(); + //if(localstore!=null) + // localstore=null; + //memset(vb,0,sizeof(vorbis_block)); + return(0); + } + + public int synthesis(Packet op){ + Info vi=vd.vi; + + // first things first. Make sure decode is ready + // ripcord(); + opb.readinit(op.packet_base, op.packet, op.bytes); + + // Check the packet type + if(opb.read(1)!=0){ + // Oops. This is not an audio data packet + return(-1); + } + + // read our mode and pre/post windowsize + int _mode=opb.read(vd.modebits); + if(_mode==-1)return(-1); + + mode=_mode; + W=vi.mode_param[mode].blockflag; + if(W!=0){ + lW=opb.read(1); + nW=opb.read(1); + if(nW==-1) return(-1); + } + else{ + lW=0; + nW=0; + } + + // more setup + granulepos=op.granulepos; + sequence=op.packetno-3; // first block is third packet + eofflag=op.e_o_s; + + // alloc pcm passback storage + pcmend=vi.blocksizes[W]; + //pcm=alloc(vi.channels); + if(pcm.length + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +class ChainingExample{ + public static void main(String[] arg){ + VorbisFile ov=null; + + try{ + ov=new VorbisFile(System.in, null, -1); + } + catch(Exception e){ + System.err.println(e); + return; + } + + if(ov.seekable()){ + System.out.println("Input bitstream contained "+ov.streams()+" logical bitstream section(s)."); + System.out.println("Total bitstream playing time: "+ov.time_total(-1)+" seconds\n"); + } + else{ + System.out.println("Standard input was not seekable."); + System.out.println("First logical bitstream information:\n"); + } + + for(int i=0;i + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +class CodeBook{ + int dim; // codebook dimensions (elements per vector) + int entries; // codebook entries + StaticCodeBook c=new StaticCodeBook(); + + float[] valuelist; // list of dim*entries actual entry values + int[] codelist; // list of bitstream codewords for each entry + DecodeAux decode_tree; + + // returns the number of bits + int encode(int a, Buffer b){ + b.write(codelist[a], c.lengthlist[a]); + return(c.lengthlist[a]); + } + + // One the encode side, our vector writers are each designed for a + // specific purpose, and the encoder is not flexible without modification: + // + // The LSP vector coder uses a single stage nearest-match with no + // interleave, so no step and no error return. This is specced by floor0 + // and doesn't change. + // + // Residue0 encoding interleaves, uses multiple stages, and each stage + // peels of a specific amount of resolution from a lattice (thus we want + // to match by threshhold, not nearest match). Residue doesn't *have* to + // be encoded that way, but to change it, one will need to add more + // infrastructure on the encode side (decode side is specced and simpler) + + // floor0 LSP (single stage, non interleaved, nearest match) + // returns entry number and *modifies a* to the quantization value + int errorv(float[] a){ + int best=best(a,1); + for(int k=0;k8){ + for(i=0;i.000001){ + System.err.print("read ("+iv[i]+") != written ("+qv[i]+") at position ("+i+")\n"); + System.exit(1); + } + } + + System.err.print("OK\n"); + ptr++; + } + // The above is the trivial stuff; + // now try unquantizing a log scale codebook + } +*/ +} + +class DecodeAux{ + int[] tab; + int[] tabl; + int tabn; + + int[] ptr0; + int[] ptr1; + int aux; // number of tree entries +} diff --git a/jorbis/src/com/jcraft/jorbis/Comment.java b/jorbis/src/com/jcraft/jorbis/Comment.java new file mode 100644 index 0000000..f83b7cb --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/Comment.java @@ -0,0 +1,252 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +// the comments are not part of vorbis_info so that vorbis_info can be +// static storage +public class Comment{ + private static byte[] _vorbis="vorbis".getBytes(); + + private static final int OV_EFAULT=-129; + private static final int OV_EIMPL=-130; + + // unlimited user comment fields. libvorbis writes 'libvorbis' + // whatever vendor is set to in encode + public byte[][] user_comments; + public int[] comment_lengths; + public int comments; + public byte[] vendor; + + public void init(){ + user_comments=null; + comments=0; + vendor=null; + } + + public void add(String comment){ + add(comment.getBytes()); + } + + private void add(byte[] comment){ + byte[][] foo=new byte[comments+2][]; + if(user_comments!=null){ + System.arraycopy(user_comments, 0, foo, 0, comments); + } + user_comments=foo; + + int[] goo=new int[comments+2]; + if(comment_lengths!=null){ + System.arraycopy(comment_lengths, 0, goo, 0, comments); + } + comment_lengths=goo; + + byte[] bar=new byte[comment.length+1]; + System.arraycopy(comment, 0, bar, 0, comment.length); + user_comments[comments]=bar; + comment_lengths[comments]=comment.length; + comments++; + user_comments[comments]=null; + } + + public void add_tag(String tag, String contents){ + if(contents==null) contents=""; + add(tag+"="+contents); + } + +/* + private void add_tag(byte[] tag, byte[] contents){ + byte[] foo=new byte[tag.length+contents.length+1]; + int j=0; + for(int i=0; i=u1 && u1>='A')u1=(byte)(u1-'A'+'a'); + if('Z'>=u2 && u2>='A')u2=(byte)(u2-'A'+'a'); + if(u1!=u2){ return false; } + c++; + } + return true; + } + + public String query(String tag){ + return query(tag, 0); + } + + public String query(String tag, int count){ + int foo=query(tag.getBytes(), count); + if(foo==-1)return null; + byte[] comment=user_comments[foo]; + for(int i=0; i + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +// Takes a vorbis bitstream from stdin and writes raw stereo PCM to +// stdout. Decodes simple and chained OggVorbis files from beginning +// to end. Vorbisfile.a is somewhat more complex than the code below. + +class DecodeExample{ + static int convsize=4096*2; + static byte[] convbuffer=new byte[convsize]; // take 8k out of the data segment, not the stack + + public static void main(String[] arg){ + java.io.InputStream input=System.in; + if(arg.length>0){ + try{ + input=new java.io.FileInputStream(arg[0]); + } + catch(Exception e){ + System.err.println(e); + } + } + + SyncState oy=new SyncState(); // sync and verify incoming physical bitstream + StreamState os=new StreamState(); // take physical pages, weld into a logical stream of packets + Page og=new Page(); // one Ogg bitstream page. Vorbis packets are inside + Packet op=new Packet(); // one raw packet of data for decode + + Info vi=new Info(); // struct that stores all the static vorbis bitstream settings + Comment vc=new Comment(); // struct that stores all the bitstream user comments + DspState vd=new DspState(); // central working state for the packet->PCM decoder + Block vb=new Block(vd); // local working space for packet->PCM decode + + byte[] buffer; + int bytes=0; + + // Decode setup + + oy.init(); // Now we can read pages + + while(true){ // we repeat if the bitstream is chained + int eos=0; + + // grab some data at the head of the stream. We want the first page + // (which is guaranteed to be small and only contain the Vorbis + // stream initial header) We need the first page to get the stream + // serialno. + + // submit a 4k block to libvorbis' Ogg layer + int index=oy.buffer(4096); + buffer=oy.data; + try{ + bytes=input.read(buffer, index, 4096); + } + catch(Exception e){ + System.err.println(e); + System.exit(-1); + } + oy.wrote(bytes); + + // Get the first page. + if(oy.pageout(og)!=1){ + // have we simply run out of data? If so, we're done. + if(bytes<4096)break; + + // error case. Must not be Vorbis data + System.err.println("Input does not appear to be an Ogg bitstream."); + System.exit(1); + } + + // Get the serial number and set up the rest of decode. + // serialno first; use it to set up a logical stream + os.init(og.serialno()); + + // extract the initial header from the first page and verify that the + // Ogg bitstream is in fact Vorbis data + + // I handle the initial header first instead of just having the code + // read all three Vorbis headers at once because reading the initial + // header is an easy way to identify a Vorbis bitstream and it's + // useful to see that functionality seperated out. + + vi.init(); + vc.init(); + if(os.pagein(og)<0){ + // error; stream version mismatch perhaps + System.err.println("Error reading first page of Ogg bitstream data."); + System.exit(1); + } + + if(os.packetout(op)!=1){ + // no page? must not be vorbis + System.err.println("Error reading initial header packet."); + System.exit(1); + } + + if(vi.synthesis_headerin(vc,op)<0){ + // error case; not a vorbis header + System.err.println("This Ogg bitstream does not contain Vorbis audio data."); + System.exit(1); + } + + // At this point, we're sure we're Vorbis. We've set up the logical + // (Ogg) bitstream decoder. Get the comment and codebook headers and + // set up the Vorbis decoder + + // The next two packets in order are the comment and codebook headers. + // They're likely large and may span multiple pages. Thus we reead + // and submit data until we get our two pacakets, watching that no + // pages are missing. If a page is missing, error out; losing a + // header page is the only place where missing data is fatal. */ + + int i=0; + while(i<2){ + while(i<2){ + + int result=oy.pageout(og); + if(result==0) break; // Need more data + // Don't complain about missing or corrupt data yet. We'll + // catch it at the packet output phase + + if(result==1){ + os.pagein(og); // we can ignore any errors here + // as they'll also become apparent + // at packetout + while(i<2){ + result=os.packetout(op); + if(result==0)break; + if(result==-1){ + // Uh oh; data at some point was corrupted or missing! + // We can't tolerate that in a header. Die. + System.err.println("Corrupt secondary header. Exiting."); + System.exit(1); + } + vi.synthesis_headerin(vc,op); + i++; + } + } + } + // no harm in not checking before adding more + index=oy.buffer(4096); + buffer=oy.data; + try{ + bytes=input.read(buffer, index, 4096); + } + catch(Exception e){ + System.err.println(e); + System.exit(1); + } + if(bytes==0 && i<2){ + System.err.println("End of file before finding all Vorbis headers!"); + System.exit(1); + } + oy.wrote(bytes); + } + + // Throw the comments plus a few lines about the bitstream we're + // decoding + { + byte[][] ptr=vc.user_comments; + for(int j=0; jPCM decoder. + vd.synthesis_init(vi); // central decode state + vb.init(vd); // local state for most of the decode + // so multiple block decodes can + // proceed in parallel. We could init + // multiple vorbis_block structures + // for vd here + + float[][][] _pcm=new float[1][][]; + int[] _index=new int[vi.channels]; + // The rest is just a straight decode loop until end of stream + while(eos==0){ + while(eos==0){ + + int result=oy.pageout(og); + if(result==0)break; // need more data + if(result==-1){ // missing or corrupt data at this page position + System.err.println("Corrupt or missing data in bitstream; continuing..."); + } + else{ + os.pagein(og); // can safely ignore errors at + // this point + while(true){ + result=os.packetout(op); + + if(result==0)break; // need more data + if(result==-1){ // missing or corrupt data at this page position + // no reason to complain; already complained above + } + else{ + // we have a packet. Decode it + int samples; + if(vb.synthesis(op)==0){ // test for success! + vd.synthesis_blockin(vb); + } + + // **pcm is a multichannel float vector. In stereo, for + // example, pcm[0] is left, and pcm[1] is right. samples is + // the size of each channel. Convert the float values + // (-1.<=range<=1.) to whatever PCM format and write it out + + while((samples=vd.synthesis_pcmout(_pcm, _index))>0){ + float[][] pcm=_pcm[0]; + boolean clipflag=false; + int bout=(samples32767){ + val=32767; + clipflag=true; + } + if(val<-32768){ + val=-32768; + clipflag=true; + } + if(val<0) val=val|0x8000; + convbuffer[ptr]=(byte)(val); + convbuffer[ptr+1]=(byte)(val>>>8); + ptr+=2*(vi.channels); + } + } + + //if(clipflag) + // System.err.println("Clipping in frame "+vd.sequence); + + System.out.write(convbuffer, 0, 2*vi.channels*bout); + + vd.synthesis_read(bout); // tell libvorbis how + // many samples we + // actually consumed + } + } + } + if(og.eos()!=0)eos=1; + } + } + if(eos==0){ + index=oy.buffer(4096); + buffer=oy.data; + try{ + bytes=input.read(buffer,index,4096); + } + catch(Exception e){ + System.err.println(e); + System.exit(1); + } + oy.wrote(bytes); + if(bytes==0)eos=1; + } + } + + // clean up this logical bitstream; before exit we see if we're + // followed by another [chained] + + os.clear(); + + // ogg_page and ogg_packet structs always point to storage in + // libvorbis. They're never freed or manipulated directly + + vb.clear(); + vd.clear(); + vi.clear(); // must be called last + } + + // OK, clean up the framer + oy.clear(); + System.err.println("Done."); + } +} + diff --git a/jorbis/src/com/jcraft/jorbis/Drft.java b/jorbis/src/com/jcraft/jorbis/Drft.java new file mode 100644 index 0000000..c7ff203 --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/Drft.java @@ -0,0 +1,1317 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +class Drft{ + int n; + float[] trigcache; + int[] splitcache; + + void backward(float[] data){ + //System.err.println("Drft.backward"); + if(n==1)return; + drftb1(n,data,trigcache,trigcache,n,splitcache); + } + + void init(int n){ + //System.err.println("Drft.init"); + this.n=n; + trigcache=new float[3*n]; + splitcache=new int[32]; + fdrffti(n, trigcache, splitcache); + } + + void clear(){ + //System.err.println("Drft.clear"); + if(trigcache!=null)trigcache=null; + if(splitcache!=null)splitcache=null; +// memset(l,0,sizeof(drft_lookup)); + } + + static int[] ntryh = { 4,2,3,5 }; + static float tpi = 6.28318530717958647692528676655900577f; + static float hsqt2 = .70710678118654752440084436210485f; + static float taui = .86602540378443864676372317075293618f; + static float taur = -.5f; + static float sqrt2 = 1.4142135623730950488016887242097f; + + static void drfti1(int n, float[] wa, int index, int[] ifac){ + float arg,argh,argld,fi; + int ntry=0,i,j=-1; + int k1, l1, l2, ib; + int ld, ii, ip, is, nq, nr; + int ido, ipm, nfm1; + int nl=n; + int nf=0; + + int state=101; + + loop: while(true){ + switch(state){ + case 101: + j++; + if (j < 4) + ntry=ntryh[j]; + else + ntry+=2; + case 104: + nq=nl/ntry; + nr=nl-ntry*nq; + if(nr!=0){ + state=101; + break; + } + nf++; + ifac[nf+1]=ntry; + nl=nq; + if(ntry!=2){ + state=107; + break; + } + if(nf==1){ + state=107; + break; + } + + for(i=1;i0){ // also handles the -1 out of data case + int maxval=(1<m); + for(int j=0; jn); + for(int k=0; kn); + return(0); + } + + Object inverse1(Block vb, Object i, Object memo){ + //System.err.println("Floor0.inverse "+i.getClass()+"]"); + LookFloor0 look=(LookFloor0)i; + InfoFloor0 info=look.vi; + float[] lsp=null; + if(memo instanceof float[]){ + lsp=(float[])memo; + } + + int ampraw=vb.opb.read(info.ampbits); + if(ampraw>0){ // also handles the -1 out of data case + int maxval=(1<n); + for(int j=0; j>>=1; + } + return(ret); + } + + static void lsp_to_lpc(float[] lsp, float[] lpc, int m){ + int i,j,m2=m/2; + float[] O=new float[m2]; + float[] E=new float[m2]; + float A; + float[] Ae=new float[m2+1]; + float[] Ao=new float[m2+1]; + float B; + float[] Be=new float[m2]; + float[] Bo=new float[m2]; + float temp; + + // even/odd roots setup + for(i=0;im+1 must be less than l->ln, but guard in case we get a bad stream + float[] lcurve=new float[Math.max(l.ln*2,l.m*2+2)]; + + if(amp==0){ + //memset(curve,0,sizeof(float)*l->n); + for(int j=0; j + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +class Floor1 extends FuncFloor{ + static final int floor1_rangedb=140; + static final int VIF_POSIT=63; + + void pack(Object i, Buffer opb){ + InfoFloor1 info=(InfoFloor1)i; + + int count=0; + int rangebits; + int maxposit=info.postlist[1]; + int maxclass=-1; + + /* save out partitions */ + opb.write(info.partitions,5); /* only 0 to 31 legal */ + for(int j=0;j=vi.books){ + //goto err_out; + info.free(); + return(null); + } + for(int k=0;k<(1<=vi.books){ + //goto err_out; + info.free(); + return(null); + } + } + } + + /* read the post list */ + info.mult=opb.read(2)+1; /* only 1,2,3,4 legal now */ + rangebits=opb.read(4); + + for(int j=0,k=0;j=(1<info.postlist[sortpointer[k]]){ + foo=sortpointer[k]; + sortpointer[k]=sortpointer[j]; + sortpointer[j]=foo; + } + } + } + + /* points from sort order back to range number */ + for(int j=0;j<_n;j++){ + look.forward_index[j]=sortpointer[j]; + } + /* points from range order to sorted position */ + for(int j=0;j<_n;j++){ + look.reverse_index[look.forward_index[j]]=j; + } + /* we actually need the post values too */ + for(int j=0;j<_n;j++){ + look.sorted_index[j]=info.postlist[look.forward_index[j]]; + } + + + /* quantize values to multiplier spec */ + switch(info.mult){ + case 1: /* 1024 -> 256 */ + look.quant_q=256; + break; + case 2: /* 1024 -> 128 */ + look.quant_q=128; + break; + case 3: /* 1024 -> 86 */ + look.quant_q=86; + break; + case 4: /* 1024 -> 64 */ + look.quant_q=64; + break; + default: + look.quant_q=-1; + } + + /* discover our neighbors for decode where we don't use fit flags + (that would push the neighbors outward) */ + for(int j=0;j<_n-2;j++){ + int lo=0; + int hi=1; + int lx=0; + int hx=look.n; + int currentx=info.postlist[j+2]; + for(int k=0;klx && xcurrentx){ + hi=k; + hx=x; + } + } + look.loneighbor[j]=lo; + look.hineighbor[j]=hi; + } + + return look; + } + + void free_info(Object i){} + void free_look(Object i){} + void free_state(Object vs){} + + int forward(Block vb, Object i, float[] in, float[] out, Object vs){return 0;} + + Object inverse1(Block vb, Object ii, Object memo){ + //System.err.println("Floor1.inverse "+i.getClass()+"]"); + LookFloor1 look=(LookFloor1)ii; + InfoFloor1 info=look.vi; + CodeBook[] books=vb.vd.fullbooks; + + /* unpack wrapped/predicted values from stream */ + if(vb.opb.read(1)==1){ + int[] fit_value=null; + if(memo instanceof int[]){ + fit_value=(int[])memo; + } + if(fit_value==null || fit_value.length>>=csubbits; + if(book>=0){ + if((fit_value[j+k]=books[book].decode(vb.opb))==-1){ + //goto eop; + return(null); + } + } + else{ + fit_value[j+k]=0; + } + } + j+=cdim; + } + + /* unwrap positive values and reconsitute via linear interpolation */ + for(int i=2;i=room){ + if(hiroom>loroom){ + val = val-loroom; + } + else{ + val = -1-(val-hiroom); + } + } + else{ + if((val&1)!=0){ + val= -((val+1)>>>1); + } + else{ + val>>=1; + } + } + + fit_value[i]=val+predicted; + fit_value[look.loneighbor[i-2]]&=0x7fff; + fit_value[look.hineighbor[i-2]]&=0x7fff; + } + else{ + fit_value[i]=predicted|0x8000; + } + } + return(fit_value); + } + +// eop: +// return(NULL); + return(null); + } + + private static int render_point(int x0,int x1,int y0,int y1,int x){ + y0&=0x7fff; /* mask off flag */ + y1&=0x7fff; + + { + int dy=y1-y0; + int adx=x1-x0; + int ady=Math.abs(dy); + int err=ady*(x-x0); + + int off=(int)(err/adx); + if(dy<0)return(y0-off); + return(y0+off); + } + } + + int inverse2(Block vb, Object i, Object memo, float[] out){ + LookFloor1 look=(LookFloor1)i; + InfoFloor1 info=look.vi; + int n=vb.vd.vi.blocksizes[vb.mode]/2; + + if(memo!=null){ + /* render the lines */ + int[] fit_value=(int[] )memo; + int hx=0; + int lx=0; + int ly=fit_value[0]*info.mult; + for(int j=1;j=adx){ + err-=adx; + y+=sy; + } + else{ + y+=base; + } + d[x]*=FLOOR_fromdB_LOOKUP[y]; + } + } + + static int ilog(int v){ + int ret=0; + while(v!=0){ + ret++; + v>>>=1; + } + return(ret); + } + + private static int ilog2(int v){ + int ret=0; + while(v>1){ + ret++; + v>>>=1; + } + return(ret); + } +} + +class InfoFloor1{ + static final int VIF_POSIT=63; + static final int VIF_CLASS=16; + static final int VIF_PARTS=31; + + int partitions; /* 0 to 31 */ + int[] partitionclass=new int[VIF_PARTS]; /* 0 to 15 */ + + int[] class_dim=new int[VIF_CLASS]; /* 1 to 8 */ + int[] class_subs=new int[VIF_CLASS]; /* 0,1,2,3 (bits: 1< + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +abstract class FuncFloor{ +// public static FuncFloor[] floor_P={new Floor0()}; + public static FuncFloor[] floor_P={new Floor0(),new Floor1()}; + + abstract void pack(Object i, Buffer opb); + abstract Object unpack(Info vi, Buffer opb); + abstract Object look(DspState vd, InfoMode mi, Object i); +// abstract Object state(Object i); + abstract void free_info(Object i); + abstract void free_look(Object i); + abstract void free_state(Object vs); + abstract int forward(Block vb, Object i, float[] in, float[] out, Object vs); +// abstract int inverse(Block vb, Object i, float[] out); + abstract Object inverse1(Block vb, Object i, Object memo); + abstract int inverse2(Block vb, Object i, Object memo, float[] out); +} diff --git a/jorbis/src/com/jcraft/jorbis/FuncMapping.java b/jorbis/src/com/jcraft/jorbis/FuncMapping.java new file mode 100644 index 0000000..c8ecf75 --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/FuncMapping.java @@ -0,0 +1,40 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +abstract class FuncMapping{ + public static FuncMapping[] mapping_P={new Mapping0()}; + + abstract void pack(Info info , Object imap, Buffer buffer); + abstract Object unpack(Info info , Buffer buffer); + abstract Object look(DspState vd, InfoMode vm, Object m); + abstract void free_info(Object imap); + abstract void free_look(Object imap); +// abstract int forward(Block vd, Object lm); + abstract int inverse(Block vd, Object lm); +} diff --git a/jorbis/src/com/jcraft/jorbis/FuncResidue.java b/jorbis/src/com/jcraft/jorbis/FuncResidue.java new file mode 100644 index 0000000..4cbf6a1 --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/FuncResidue.java @@ -0,0 +1,43 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +abstract class FuncResidue{ + public static FuncResidue[] residue_P={new Residue0(), + new Residue1(), + new Residue2()}; + + abstract void pack(Object vr, Buffer opb); + abstract Object unpack(Info vi, Buffer opb); + abstract Object look(DspState vd, InfoMode vm, Object vr); + abstract void free_info(Object i); + abstract void free_look(Object i); + abstract int forward(Block vb,Object vl, float[][] in, int ch); +// abstract int inverse(Block vb, Object vl, float[][] in, int ch); +abstract int inverse(Block vb, Object vl, float[][] in, int[] nonzero,int ch); +} diff --git a/jorbis/src/com/jcraft/jorbis/FuncTime.java b/jorbis/src/com/jcraft/jorbis/FuncTime.java new file mode 100644 index 0000000..b3cd080 --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/FuncTime.java @@ -0,0 +1,40 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +abstract class FuncTime{ + public static FuncTime[] time_P={new Time0()}; + + abstract void pack(Object i, Buffer opb); + abstract Object unpack(Info vi , Buffer opb); + abstract Object look(DspState vd, InfoMode vm, Object i); + abstract void free_info(Object i); + abstract void free_look(Object i); + abstract int forward(Block vb, Object i); + abstract int inverse(Block vb, Object i, float[] in, float[] out); +} diff --git a/jorbis/src/com/jcraft/jorbis/Info.java b/jorbis/src/com/jcraft/jorbis/Info.java new file mode 100644 index 0000000..dffd4d9 --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/Info.java @@ -0,0 +1,516 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +public class Info{ + private static final int OV_EBADPACKET=-136; + private static final int OV_ENOTAUDIO=-135; + + private static byte[] _vorbis="vorbis".getBytes(); + private static final int VI_TIMEB=1; +// private static final int VI_FLOORB=1; + private static final int VI_FLOORB=2; +// private static final int VI_RESB=1; + private static final int VI_RESB=3; + private static final int VI_MAPB=1; + private static final int VI_WINDOWB=1; + + public int version; + public int channels; + public int rate; + + // The below bitrate declarations are *hints*. + // Combinations of the three values carry the following implications: + // + // all three set to the same value: + // implies a fixed rate bitstream + // only nominal set: + // implies a VBR stream that averages the nominal bitrate. No hard + // upper/lower limit + // upper and or lower set: + // implies a VBR bitstream that obeys the bitrate limits. nominal + // may also be set to give a nominal rate. + // none set: + // the coder does not care to speculate. + + int bitrate_upper; + int bitrate_nominal; + int bitrate_lower; + + // Vorbis supports only short and long blocks, but allows the + // encoder to choose the sizes + + int[] blocksizes=new int[2]; + + // modes are the primary means of supporting on-the-fly different + // blocksizes, different channel mappings (LR or mid-side), + // different residue backends, etc. Each mode consists of a + // blocksize flag and a mapping (along with the mapping setup + + int modes; + int maps; + int times; + int floors; + int residues; + int books; + int psys; // encode only + + InfoMode[] mode_param=null; + + int[] map_type=null; + Object[] map_param=null; + + int[] time_type=null; + Object[] time_param=null; + + int[] floor_type=null; + Object[] floor_param=null; + + int[] residue_type=null; + Object[] residue_param=null; + + StaticCodeBook[] book_param=null; + + PsyInfo[] psy_param=new PsyInfo[64]; // encode only + + // for block long/sort tuning; encode only + int envelopesa; + float preecho_thresh; + float preecho_clamp; + + // used by synthesis, which has a full, alloced vi + public void init(){ + rate=0; + //memset(vi,0,sizeof(vorbis_info)); + } + + public void clear(){ + for(int i=0;ibook_param)free(vi->book_param); + book_param=null; + + for(int i=0;ipsy_param)free(vi->psy_param); + //memset(vi,0,sizeof(vorbis_info)); + } + + // Header packing/unpacking + int unpack_info(Buffer opb){ + version=opb.read(32); + if(version!=0)return(-1); + + channels=opb.read(8); + rate=opb.read(32); + + bitrate_upper=opb.read(32); + bitrate_nominal=opb.read(32); + bitrate_lower=opb.read(32); + + blocksizes[0]=1<=VI_TIMEB){ + //goto err_out; + clear(); + return(-1); + } + time_param[i]=FuncTime.time_P[time_type[i]].unpack(this, opb); + if(time_param[i]==null){ + //goto err_out; + clear(); + return(-1); + } + } + + // floor backend settings + floors=opb.read(6)+1; + if(floor_type==null || floor_type.length!=floors) + floor_type=new int[floors]; + if(floor_param==null || floor_param.length!=floors) + floor_param=new Object[floors]; + + for(int i=0;i=VI_FLOORB){ + //goto err_out; + clear(); + return(-1); + } + + floor_param[i]=FuncFloor.floor_P[floor_type[i]].unpack(this,opb); + if(floor_param[i]==null){ + //goto err_out; + clear(); + return(-1); + } + } + + // residue backend settings + residues=opb.read(6)+1; + + if(residue_type==null || residue_type.length!=residues) + residue_type=new int[residues]; + + if(residue_param==null || residue_param.length!=residues) + residue_param=new Object[residues]; + + for(int i=0;i=VI_RESB){ +// goto err_out; + clear(); + return(-1); + } + residue_param[i]=FuncResidue.residue_P[residue_type[i]].unpack(this,opb); + if(residue_param[i]==null){ +// goto err_out; + clear(); + return(-1); + } + } + + // map backend settings + maps=opb.read(6)+1; + if(map_type==null || map_type.length!=maps) map_type=new int[maps]; + if(map_param==null || map_param.length!=maps) map_param=new Object[maps]; + for(int i=0;i=VI_MAPB){ +// goto err_out; + clear(); + return(-1); + } + map_param[i]=FuncMapping.mapping_P[map_type[i]].unpack(this,opb); + if(map_param[i]==null){ +// goto err_out; + clear(); + return(-1); + } + } + + // mode settings + modes=opb.read(6)+1; + if(mode_param==null || mode_param.length!=modes) + mode_param=new InfoMode[modes]; + for(int i=0;i=VI_WINDOWB)|| + (mode_param[i].transformtype>=VI_WINDOWB)|| + (mode_param[i].mapping>=maps)){ +// goto err_out; + clear(); + return(-1); + } + } + + if(opb.read(1)!=1){ + //goto err_out; // top level EOP check + clear(); + return(-1); + } + + return(0); +// err_out: +// vorbis_info_clear(vi); +// return(-1); + } + + // The Vorbis header is in three packets; the initial small packet in + // the first page that identifies basic parameters, a second packet + // with bitstream comments and a third packet that holds the + // codebook. + + public int synthesis_headerin(Comment vc, Packet op){ + Buffer opb=new Buffer(); + + if(op!=null){ + opb.readinit(op.packet_base, op.packet, op.bytes); + + // Which of the three types of header is this? + // Also verify header-ness, vorbis + { + byte[] buffer=new byte[6]; + int packtype=opb.read(8); + //memset(buffer,0,6); + opb.read(buffer,6); + if(buffer[0]!='v' || buffer[1]!='o' || buffer[2]!='r' || + buffer[3]!='b' || buffer[4]!='i' || buffer[5]!='s'){ + // not a vorbis header + return(-1); + } + switch(packtype){ + case 0x01: // least significant *bit* is read first + if(op.b_o_s==0){ + // Not the initial packet + return(-1); + } + if(rate!=0){ + // previously initialized info header + return(-1); + } + return(unpack_info(opb)); + case 0x03: // least significant *bit* is read first + if(rate==0){ + // um... we didn't get the initial header + return(-1); + } + return(vc.unpack(opb)); + case 0x05: // least significant *bit* is read first + if(rate==0 || vc.vendor==null){ + // um... we didn;t get the initial header or comments yet + return(-1); + } + return(unpack_books(opb)); + default: + // Not a valid vorbis header type + //return(-1); + break; + } + } + } + return(-1); + } + + // pack side + int pack_info(Buffer opb){ + // preamble + opb.write(0x01,8); + opb.write(_vorbis); + + // basic information about the stream + opb.write(0x00,32); + opb.write(channels,8); + opb.write(rate,32); + + opb.write(bitrate_upper,32); + opb.write(bitrate_nominal,32); + opb.write(bitrate_lower,32); + + opb.write(ilog2(blocksizes[0]),4); + opb.write(ilog2(blocksizes[1]),4); + opb.write(1,1); + return(0); + } + + int pack_books(Buffer opb){ + opb.write(0x05,8); + opb.write(_vorbis); + + // books + opb.write(books-1,8); + for(int i=0;icodec_setup; + Buffer opb=new Buffer(); +// synchronized(opb_blocksize){ + int mode; + + opb.readinit(op.packet_base, op.packet, op.bytes); + + /* Check the packet type */ + if(opb.read(1)!=0){ + /* Oops. This is not an audio data packet */ + return(OV_ENOTAUDIO); + } + { + int modebits=0; + int v=modes; + while(v>1){ + modebits++; + v>>>=1; + } + + /* read our mode and pre/post windowsize */ + mode=opb.read(modebits); + } + if(mode==-1)return(OV_EBADPACKET); + return(blocksizes[mode_param[mode].blockflag]); +// } + } + + private static int ilog2(int v){ + int ret=0; + while(v>1){ + ret++; + v>>>=1; + } + return(ret); + } + + public String toString(){ + return "version:"+new Integer(version)+ + ", channels:"+new Integer(channels)+ + ", rate:"+new Integer(rate)+ + ", bitrate:"+new Integer(bitrate_upper)+","+ + new Integer(bitrate_nominal)+","+ + new Integer(bitrate_lower); + } +} diff --git a/jorbis/src/com/jcraft/jorbis/InfoMode.java b/jorbis/src/com/jcraft/jorbis/InfoMode.java new file mode 100644 index 0000000..b570aa5 --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/InfoMode.java @@ -0,0 +1,33 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +class InfoMode{ + int blockflag; + int windowtype; + int transformtype; + int mapping; +} diff --git a/jorbis/src/com/jcraft/jorbis/JOrbisException.java b/jorbis/src/com/jcraft/jorbis/JOrbisException.java new file mode 100644 index 0000000..ce09d4f --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/JOrbisException.java @@ -0,0 +1,35 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +public class JOrbisException extends Exception { + public JOrbisException () { + super(); + } + public JOrbisException (String s) { + super ("JOrbis: "+s); + } +} diff --git a/jorbis/src/com/jcraft/jorbis/Lookup.java b/jorbis/src/com/jcraft/jorbis/Lookup.java new file mode 100644 index 0000000..fb7651a --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/Lookup.java @@ -0,0 +1,154 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +class Lookup{ + static final int COS_LOOKUP_SZ=128; + static final float[] COS_LOOKUP={ + +1.0000000000000f,+0.9996988186962f,+0.9987954562052f,+0.9972904566787f, + +0.9951847266722f,+0.9924795345987f,+0.9891765099648f,+0.9852776423889f, + +0.9807852804032f,+0.9757021300385f,+0.9700312531945f,+0.9637760657954f, + +0.9569403357322f,+0.9495281805930f,+0.9415440651830f,+0.9329927988347f, + +0.9238795325113f,+0.9142097557035f,+0.9039892931234f,+0.8932243011955f, + +0.8819212643484f,+0.8700869911087f,+0.8577286100003f,+0.8448535652497f, + +0.8314696123025f,+0.8175848131516f,+0.8032075314806f,+0.7883464276266f, + +0.7730104533627f,+0.7572088465065f,+0.7409511253550f,+0.7242470829515f, + +0.7071067811865f,+0.6895405447371f,+0.6715589548470f,+0.6531728429538f, + +0.6343932841636f,+0.6152315905806f,+0.5956993044924f,+0.5758081914178f, + +0.5555702330196f,+0.5349976198871f,+0.5141027441932f,+0.4928981922298f, + +0.4713967368260f,+0.4496113296546f,+0.4275550934303f,+0.4052413140050f, + +0.3826834323651f,+0.3598950365350f,+0.3368898533922f,+0.3136817403989f, + +0.2902846772545f,+0.2667127574749f,+0.2429801799033f,+0.2191012401569f, + +0.1950903220161f,+0.1709618887603f,+0.1467304744554f,+0.1224106751992f, + +0.0980171403296f,+0.0735645635997f,+0.0490676743274f,+0.0245412285229f, + +0.0000000000000f,-0.0245412285229f,-0.0490676743274f,-0.0735645635997f, + -0.0980171403296f,-0.1224106751992f,-0.1467304744554f,-0.1709618887603f, + -0.1950903220161f,-0.2191012401569f,-0.2429801799033f,-0.2667127574749f, + -0.2902846772545f,-0.3136817403989f,-0.3368898533922f,-0.3598950365350f, + -0.3826834323651f,-0.4052413140050f,-0.4275550934303f,-0.4496113296546f, + -0.4713967368260f,-0.4928981922298f,-0.5141027441932f,-0.5349976198871f, + -0.5555702330196f,-0.5758081914178f,-0.5956993044924f,-0.6152315905806f, + -0.6343932841636f,-0.6531728429538f,-0.6715589548470f,-0.6895405447371f, + -0.7071067811865f,-0.7242470829515f,-0.7409511253550f,-0.7572088465065f, + -0.7730104533627f,-0.7883464276266f,-0.8032075314806f,-0.8175848131516f, + -0.8314696123025f,-0.8448535652497f,-0.8577286100003f,-0.8700869911087f, + -0.8819212643484f,-0.8932243011955f,-0.9039892931234f,-0.9142097557035f, + -0.9238795325113f,-0.9329927988347f,-0.9415440651830f,-0.9495281805930f, + -0.9569403357322f,-0.9637760657954f,-0.9700312531945f,-0.9757021300385f, + -0.9807852804032f,-0.9852776423889f,-0.9891765099648f,-0.9924795345987f, + -0.9951847266722f,-0.9972904566787f,-0.9987954562052f,-0.9996988186962f, + -1.0000000000000f, + }; + /* interpolated lookup based cos function, domain 0 to PI only */ + static float coslook(float a){ + double d=a*(.31830989*(float)COS_LOOKUP_SZ); + int i=(int)d; + return COS_LOOKUP[i]+ ((float)(d-i))*(COS_LOOKUP[i+1]-COS_LOOKUP[i]); + } + + static final int INVSQ_LOOKUP_SZ=32; + static final float[] INVSQ_LOOKUP={ + 1.414213562373f,1.392621247646f,1.371988681140f,1.352246807566f, + 1.333333333333f,1.315191898443f,1.297771369046f,1.281025230441f, + 1.264911064067f,1.249390095109f,1.234426799697f,1.219988562661f, + 1.206045378311f,1.192569588000f,1.179535649239f,1.166919931983f, + 1.154700538379f,1.142857142857f,1.131370849898f,1.120224067222f, + 1.109400392450f,1.098884511590f,1.088662107904f,1.078719779941f, + 1.069044967650f,1.059625885652f,1.050451462878f,1.041511287847f, + 1.032795558989f,1.024295039463f,1.016001016002f,1.007905261358f, + 1.000000000000f, + }; + /* interpolated 1./sqrt(p) where .5 <= p < 1. */ + static float invsqlook(float a){ +// System.out.println(a); + double d=a*(2.f*(float)INVSQ_LOOKUP_SZ)-(float)INVSQ_LOOKUP_SZ; + int i=(int)d; + return INVSQ_LOOKUP[i]+ ((float)(d-i))*(INVSQ_LOOKUP[i+1]-INVSQ_LOOKUP[i]); + } + + static final int INVSQ2EXP_LOOKUP_MIN=-32; + static final int INVSQ2EXP_LOOKUP_MAX=32; + static final float[] INVSQ2EXP_LOOKUP={ + 65536.f, 46340.95001f, 32768.f, 23170.47501f, + 16384.f, 11585.2375f, 8192.f, 5792.618751f, + 4096.f, 2896.309376f, 2048.f, 1448.154688f, + 1024.f, 724.0773439f, 512.f, 362.038672f, + 256.f, 181.019336f, 128.f, 90.50966799f, + 64.f, 45.254834f, 32.f, 22.627417f, + 16.f, 11.3137085f, 8.f, 5.656854249f, + 4.f, 2.828427125f, 2.f, 1.414213562f, + 1.f, 0.7071067812f, 0.5f, 0.3535533906f, + 0.25f, 0.1767766953f, 0.125f, 0.08838834765f, + 0.0625f, 0.04419417382f, 0.03125f, 0.02209708691f, + 0.015625f, 0.01104854346f, 0.0078125f, 0.005524271728f, + 0.00390625f, 0.002762135864f, 0.001953125f, 0.001381067932f, + 0.0009765625f, 0.000690533966f, 0.00048828125f, 0.000345266983f, + 0.000244140625f,0.0001726334915f,0.0001220703125f,8.631674575e-05f, + 6.103515625e-05f,4.315837288e-05f,3.051757812e-05f,2.157918644e-05f, + 1.525878906e-05f, + }; + /* interpolated 1./sqrt(p) where .5 <= p < 1. */ + static float invsq2explook(int a){ + return INVSQ2EXP_LOOKUP[a-INVSQ2EXP_LOOKUP_MIN]; + } + + static final int FROMdB_LOOKUP_SZ=35; + static final int FROMdB2_LOOKUP_SZ=32; + static final int FROMdB_SHIFT=5; + static final int FROMdB2_SHIFT=3; + static final int FROMdB2_MASK=31; + static final float[] FROMdB_LOOKUP={ + 1.f, 0.6309573445f, 0.3981071706f, 0.2511886432f, + 0.1584893192f, 0.1f, 0.06309573445f, 0.03981071706f, + 0.02511886432f, 0.01584893192f, 0.01f, 0.006309573445f, + 0.003981071706f, 0.002511886432f, 0.001584893192f, 0.001f, + 0.0006309573445f,0.0003981071706f,0.0002511886432f,0.0001584893192f, + 0.0001f,6.309573445e-05f,3.981071706e-05f,2.511886432e-05f, + 1.584893192e-05f, 1e-05f,6.309573445e-06f,3.981071706e-06f, + 2.511886432e-06f,1.584893192e-06f, 1e-06f,6.309573445e-07f, + 3.981071706e-07f,2.511886432e-07f,1.584893192e-07f, + }; + static final float[] FROMdB2_LOOKUP={ + 0.9928302478f, 0.9786445908f, 0.9646616199f, 0.9508784391f, + 0.9372921937f, 0.92390007f, 0.9106992942f, 0.8976871324f, + 0.8848608897f, 0.8722179097f, 0.8597555737f, 0.8474713009f, + 0.835362547f, 0.8234268041f, 0.8116616003f, 0.8000644989f, + 0.7886330981f, 0.7773650302f, 0.7662579617f, 0.755309592f, + 0.7445176537f, 0.7338799116f, 0.7233941627f, 0.7130582353f, + 0.7028699885f, 0.6928273125f, 0.6829281272f, 0.6731703824f, + 0.6635520573f, 0.6540711597f, 0.6447257262f, 0.6355138211f, + }; + /* interpolated lookup based fromdB function, domain -140dB to 0dB only */ + static float fromdBlook(float a){ + int i=(int)(a*((float)(-(1<=(FROMdB_LOOKUP_SZ<>>FROMdB_SHIFT]*FROMdB2_LOOKUP[i&FROMdB2_MASK]); + } + +} + + diff --git a/jorbis/src/com/jcraft/jorbis/Lpc.java b/jorbis/src/com/jcraft/jorbis/Lpc.java new file mode 100644 index 0000000..452ed86 --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/Lpc.java @@ -0,0 +1,254 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +class Lpc{ + // en/decode lookups + Drft fft=new Drft();; + + int ln; + int m; + + // Autocorrelation LPC coeff generation algorithm invented by + // N. Levinson in 1947, modified by J. Durbin in 1959. + + // Input : n elements of time doamin data + // Output: m lpc coefficients, excitation energy + + static float lpc_from_data(float[] data, float[] lpc,int n,int m){ + float[] aut=new float[m+1]; + float error; + int i,j; + + // autocorrelation, p+1 lag coefficients + + j=m+1; + while(j--!=0){ + float d=0; + for(i=j;iln*2); + for(int i=0; i + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +/* + function: LSP (also called LSF) conversion routines + + The LSP generation code is taken (with minimal modification) from + "On the Computation of the LSP Frequencies" by Joseph Rothweiler + , available at: + + http://www2.xtdl.com/~rothwlr/lsfpaper/lsfpage.html + ********************************************************************/ + +class Lsp{ + + static final float M_PI=(float)(3.1415926539); + + static void lsp_to_curve(float[] curve, + int[] map, int n, int ln, + float[] lsp, int m, + float amp, float ampoffset){ + int i; + float wdel=M_PI/ln; + for(i=0;i>>1; + + for(int j=0;j=0x7f800000||(ix==0)){ + // 0,inf,nan + } + else{ + if(ix<0x00800000){ // subnormal + q*=3.3554432000e+07; // 0x4c000000 + hx=Float.floatToIntBits(q); + ix=0x7fffffff&hx; + qexp=-25; + } + qexp += ((ix>>>23)-126); + hx=(hx&0x807fffff)|0x3f000000; + q=Float.intBitsToFloat(hx); + } + + q=Lookup.fromdBlook(amp* + Lookup.invsqlook(q)* + Lookup.invsq2explook(qexp+m)-ampoffset); + + do{curve[i++]*=q;} +// do{curve[i++]=q;} + while(i + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +class Mapping0 extends FuncMapping{ + static int seq=0; + void free_info(Object imap){}; + void free_look(Object imap){ +/* + LookMapping0 l=(LookMapping0)imap; + InfoMapping0 info=l.map; + if(l!=null){ + for(int i=0;idecay[i]); + l.decay[i]=null; + } + //free(l->decay); + l.decay=null; + } + //free(l->time_func); + //free(l->floor_func); + //free(l->residue_func); + //free(l->time_look); + //free(l->floor_look); + //free(l->residue_look); + //f(l->psy_look)free(l->psy_look); + l.time_func=null; + l.floor_func=null; + l.residue_func=null; + l.time_look=null; + l.floor_look=null; + l.residue_look=null; + //memset(l,0,sizeof(vorbis_look_mapping0)); + //free(l); +*/ + } + + Object look(DspState vd, InfoMode vm, Object m){ +//System.err.println("Mapping0.look"); + Info vi=vd.vi; + LookMapping0 look=new LookMapping0(); + InfoMapping0 info=look.map=(InfoMapping0)m; + look.mode=vm; + + look.time_look=new Object[info.submaps]; + look.floor_look=new Object[info.submaps]; + look.residue_look=new Object[info.submaps]; + +/* + if(vd.analysisp!=0){ + look.floor_state=new Object[vi.channels]; + } + if(vi.psys!=0){ + look.psy_look=new PsyLook[info.submaps]; + for(int i=0; ipsy[0] != info->psy[1]){ + + int psynum=info->psy[0]; + look->psy_look[0]=_ogg_calloc(1,sizeof(vorbis_look_psy)); + _vp_psy_init(look->psy_look[0],ci->psy_param[psynum], + ci->psy_g_param, + ci->blocksizes[vm->blockflag]/2,vi->rate); + + psynum=info->psy[1]; + look->psy_look[1]=_ogg_calloc(1,sizeof(vorbis_look_psy)); + _vp_psy_init(look->psy_look[1],ci->psy_param[psynum], + ci->psy_g_param, + ci->blocksizes[vm->blockflag]/2,vi->rate); + }else{ + + int psynum=info->psy[0]; + look->psy_look[0]=_ogg_calloc(1,sizeof(vorbis_look_psy)); + look->psy_look[1]=look->psy_look[0]; + _vp_psy_init(look->psy_look[0],ci->psy_param[psynum], + ci->psy_g_param, + ci->blocksizes[vm->blockflag]/2,vi->rate); + + } + */ + } + + look.ch=vi.channels; +// if(vd->analysisp)drft_init(&look->fft_look,ci->blocksizes[vm->blockflag]); + + return(look); +//return null; + } + + void pack(Info vi, Object imap, Buffer opb){ + InfoMapping0 info=(InfoMapping0)imap; + + /* another 'we meant to do it this way' hack... up to beta 4, we + packed 4 binary zeros here to signify one submapping in use. We + now redefine that to mean four bitflags that indicate use of + deeper features; bit0:submappings, bit1:coupling, + bit2,3:reserved. This is backward compatable with all actual uses + of the beta code. */ + + if(info.submaps>1){ + opb.write(1,1); + opb.write(info.submaps-1,4); + } + else{ + opb.write(0,1); + } + + if(info.coupling_steps>0){ + opb.write(1,1); + opb.write(info.coupling_steps-1,8); + for(int i=0;i1){ + for(int i=0;i=vi.channels || + testA>=vi.channels){ + //goto err_out; + info.free(); + return(null); + } + } + } + + if(opb.read(2)>0){ /* 2,3:reserved */ + //goto err_out; + info.free(); + return(null); + } + + if(info.submaps>1){ + for(int i=0;i=info.submaps){ + //goto err_out; + info.free(); + return(null); + } + } + } + + for(int i=0;i=vi.times){ + //goto err_out; + info.free(); + return(null); + } + info.floorsubmap[i]=opb.read(8); + if(info.floorsubmap[i]>=vi.floors){ + //goto err_out; + info.free(); + return(null); + } + info.residuesubmap[i]=opb.read(8); + if(info.residuesubmap[i]>=vi.residues){ + //goto err_out; + info.free(); + return(null); + } + } + return info; + //err_out: + //free_info(info); + //return(NULL); + } + +/* + // no time mapping implementation for now + static int seq=0; + int forward(Block vb, Object l){ + DspState vd=vb.vd; + Info vi=vd.vi; + LookMapping0 look=(LookMapping0)l; + InfoMapping0 info=look.map; + InfoMode mode=look.mode; + int n=vb.pcmend; + float[] window=vd.window[vb.W][vb.lW][vb.nW][mode.windowtype]; + + float[][] pcmbundle=new float[vi.channles][]; + int[] nonzero=new int[vi.channels]; + + // time domain pre-window: NONE IMPLEMENTED + + // window the PCM data: takes PCM vector, vb; modifies PCM vector + + for(int i=0;isequence) + // memset(decay,0,n*sizeof(float)/2); + + // perform psychoacoustics; do masking + _vp_compute_mask(look.psy_look[submap],pcm,floor,decay); + + _analysis_output("mdct",seq,pcm,n/2,0,1); + _analysis_output("lmdct",seq,pcm,n/2,0,0); + _analysis_output("prefloor",seq,floor,n/2,0,1); + + // perform floor encoding + nonzero[i]=look.floor_func[submap]. + forward(vb,look.floor_look[submap],floor,floor,look.floor_state[i]); + + _analysis_output("floor",seq,floor,n/2,0,1); + + // apply the floor, do optional noise levelling + _vp_apply_floor(look->psy_look+submap,pcm,floor); + + _analysis_output("res",seq++,pcm,n/2,0,0); + } + + // perform residue encoding with residue mapping; this is + // multiplexed. All the channels belonging to one submap are + // encoded (values interleaved), then the next submap, etc + + for(int i=0;i=0;i--){ + float[] pcmM=vb.pcm[info.coupling_mag[i]]; + float[] pcmA=vb.pcm[info.coupling_ang[i]]; + + for(int j=0;j0){ + if(ang>0){ + pcmM[j]=mag; + pcmA[j]=mag-ang; + } + else{ + pcmA[j]=mag; + pcmM[j]=mag+ang; + } + } + else{ + if(ang>0){ + pcmM[j]=mag; + pcmA[j]=mag+ang; + } + else{ + pcmA[j]=mag; + pcmM[j]=mag-ang; + } + } + } + } + +// /* compute and apply spectral envelope */ + + for(int i=0;i1){ + ret++; + v>>>=1; + } + return(ret); + } +} + +class InfoMapping0{ + int submaps; // <= 16 + int[] chmuxlist=new int[256]; // up to 256 channels in a Vorbis stream + + int[] timesubmap=new int[16]; // [mux] + int[] floorsubmap=new int[16]; // [mux] submap to floors + int[] residuesubmap=new int[16];// [mux] submap to residue + int[] psysubmap=new int[16]; // [mux]; encode only + + int coupling_steps; + int[] coupling_mag=new int[256]; + int[] coupling_ang=new int[256]; + + void free(){ + chmuxlist=null; + timesubmap=null; + floorsubmap=null; + residuesubmap=null; + psysubmap=null; + + coupling_mag=null; + coupling_ang=null; + } +} + +class LookMapping0{ + InfoMode mode; + InfoMapping0 map; + Object[] time_look; + Object[] floor_look; + Object[] floor_state; + Object[] residue_look; + PsyLook[] psy_look; + + FuncTime[] time_func; + FuncFloor[] floor_func; + FuncResidue[] residue_func; + + int ch; + float[][] decay; + int lastframe; // if a different mode is called, we need to + // invalidate decay and floor state +} diff --git a/jorbis/src/com/jcraft/jorbis/Mdct.java b/jorbis/src/com/jcraft/jorbis/Mdct.java new file mode 100644 index 0000000..bd5cc38 --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/Mdct.java @@ -0,0 +1,249 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +class Mdct{ + + static private final float cPI3_8=0.38268343236508977175f; + static private final float cPI2_8=0.70710678118654752441f; + static private final float cPI1_8=0.92387953251128675613f; + + int n; + int log2n; + + float[] trig; + int[] bitrev; + + float scale; + + void init(int n){ + bitrev=new int[n/4]; + trig=new float[n+n/4]; + + int n2=n>>>1; + log2n=(int)Math.rint(Math.log(n)/Math.log(2)); + this.n=n; + + + int AE=0; + int AO=1; + int BE=AE+n/2; + int BO=BE+1; + int CE=BE+n/2; + int CO=CE+1; + // trig lookups... + for(int i=0;i>>j!=0;j++) + if(((msb>>>j)&i)!=0)acc|=1<>>1; + int n4=n>>>2; + int n8=n>>>3; + + // rotate + step 1 + { + int inO=1; + int xO=0; + int A=n2; + + int i; + for(i=0;i>>(i+2); + int k1=1<<(i+3); + int wbase=n2-2; + + A=0; + float[] temp; + + for(int r=0;r<(k0>>>2);r++){ + int w1=wbase; + w2=w1-(k0>>1); + float AEv= trig[A],wA; + float AOv= trig[A+1],wB; + wbase-=2; + + k0++; + for(int s=0;s<(2< + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +// psychoacoustic setup +class PsyInfo{ + int athp; + int decayp; + int smoothp; + int noisefitp; + int noisefit_subblock; + float noisefit_threshdB; + + float ath_att; + + int tonemaskp; + float[] toneatt_125Hz=new float[5]; + float[] toneatt_250Hz=new float[5]; + float[] toneatt_500Hz=new float[5]; + float[] toneatt_1000Hz=new float[5]; + float[] toneatt_2000Hz=new float[5]; + float[] toneatt_4000Hz=new float[5]; + float[] toneatt_8000Hz=new float[5]; + + int peakattp; + float[] peakatt_125Hz=new float[5]; + float[] peakatt_250Hz=new float[5]; + float[] peakatt_500Hz=new float[5]; + float[] peakatt_1000Hz=new float[5]; + float[] peakatt_2000Hz=new float[5]; + float[] peakatt_4000Hz=new float[5]; + float[] peakatt_8000Hz=new float[5]; + + int noisemaskp; + float[] noiseatt_125Hz=new float[5]; + float[] noiseatt_250Hz=new float[5]; + float[] noiseatt_500Hz=new float[5]; + float[] noiseatt_1000Hz=new float[5]; + float[] noiseatt_2000Hz=new float[5]; + float[] noiseatt_4000Hz=new float[5]; + float[] noiseatt_8000Hz=new float[5]; + + float max_curve_dB; + + float attack_coeff; + float decay_coeff; + + void free(){} +} diff --git a/jorbis/src/com/jcraft/jorbis/PsyLook.java b/jorbis/src/com/jcraft/jorbis/PsyLook.java new file mode 100644 index 0000000..9da85ed --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/PsyLook.java @@ -0,0 +1,187 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +class PsyLook { + int n; + PsyInfo vi; + + float[][][] tonecurves; + float[][] peakatt; + float[][][] noisecurves; + + float[] ath; + int[] octave; + + void init(PsyInfo vi, int n, int rate){ + /* + float rate2=rate/2.; + //memset(p,0,sizeof(vorbis_look_psy)); + ath=new float[n]; + octave=new int[n]; + this.vi=vi; + this.n=n; + + // set up the lookups for a given blocksize and sample rate + // Vorbis max sample rate is limited by 26 Bark (54kHz) + set_curve(ATH_Bark_dB, ath,n,rate); + for(int i=0;i12)oc=12; + octave[i]=oc; + } + + tonecurves=malloc(13*sizeof(float **)); + noisecurves=malloc(13*sizeof(float **)); + peakatt=malloc(7*sizeof(float *)); + for(int i=0;i<13;i++){ + tonecurves[i]=malloc(9*sizeof(float *)); + noisecurves[i]=malloc(9*sizeof(float *)); + } + for(i=0;i<7;i++) + peakatt[i]=malloc(5*sizeof(float)); + + for(i=0;i<13;i++){ + for(j=0;j<9;j++){ + tonecurves[i][j]=malloc(EHMER_MAX*sizeof(float)); + noisecurves[i][j]=malloc(EHMER_MAX*sizeof(float)); + } + } + + // OK, yeah, this was a silly way to do it + memcpy(tonecurves[0][2],tone_125_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[0][4],tone_125_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[0][6],tone_125_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[0][8],tone_125_100dB_SL,sizeof(float)*EHMER_MAX); + + memcpy(tonecurves[2][2],tone_250_40dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[2][4],tone_250_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[2][6],tone_250_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[2][8],tone_250_80dB_SL,sizeof(float)*EHMER_MAX); + + memcpy(tonecurves[4][2],tone_500_40dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[4][4],tone_500_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[4][6],tone_500_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[4][8],tone_500_100dB_SL,sizeof(float)*EHMER_MAX); + + memcpy(tonecurves[6][2],tone_1000_40dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[6][4],tone_1000_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[6][6],tone_1000_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[6][8],tone_1000_100dB_SL,sizeof(float)*EHMER_MAX); + + memcpy(tonecurves[8][2],tone_2000_40dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[8][4],tone_2000_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[8][6],tone_2000_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[8][8],tone_2000_100dB_SL,sizeof(float)*EHMER_MAX); + + memcpy(tonecurves[10][2],tone_4000_40dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[10][4],tone_4000_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[10][6],tone_4000_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[10][8],tone_4000_100dB_SL,sizeof(float)*EHMER_MAX); + + memcpy(tonecurves[12][2],tone_4000_40dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[12][4],tone_4000_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[12][6],tone_8000_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(tonecurves[12][8],tone_8000_100dB_SL,sizeof(float)*EHMER_MAX); + + + memcpy(noisecurves[0][2],noise_500_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[0][4],noise_500_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[0][6],noise_500_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[0][8],noise_500_80dB_SL,sizeof(float)*EHMER_MAX); + + memcpy(noisecurves[2][2],noise_500_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[2][4],noise_500_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[2][6],noise_500_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[2][8],noise_500_80dB_SL,sizeof(float)*EHMER_MAX); + + memcpy(noisecurves[4][2],noise_500_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[4][4],noise_500_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[4][6],noise_500_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[4][8],noise_500_80dB_SL,sizeof(float)*EHMER_MAX); + + memcpy(noisecurves[6][2],noise_1000_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[6][4],noise_1000_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[6][6],noise_1000_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[6][8],noise_1000_80dB_SL,sizeof(float)*EHMER_MAX); + + memcpy(noisecurves[8][2],noise_2000_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[8][4],noise_2000_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[8][6],noise_2000_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[8][8],noise_2000_80dB_SL,sizeof(float)*EHMER_MAX); + + memcpy(noisecurves[10][2],noise_4000_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[10][4],noise_4000_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[10][6],noise_4000_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[10][8],noise_4000_80dB_SL,sizeof(float)*EHMER_MAX); + + memcpy(noisecurves[12][2],noise_4000_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[12][4],noise_4000_60dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[12][6],noise_4000_80dB_SL,sizeof(float)*EHMER_MAX); + memcpy(noisecurves[12][8],noise_4000_80dB_SL,sizeof(float)*EHMER_MAX); + + setup_curve(tonecurves[0],0,vi.toneatt_125Hz); + setup_curve(tonecurves[2],2,vi.toneatt_250Hz); + setup_curve(tonecurves[4],4,vi.toneatt_500Hz); + setup_curve(tonecurves[6],6,vi.toneatt_1000Hz); + setup_curve(tonecurves[8],8,vi.toneatt_2000Hz); + setup_curve(tonecurves[10],10,vi.toneatt_4000Hz); + setup_curve(tonecurves[12],12,vi.toneatt_8000Hz); + + setup_curve(noisecurves[0],0,vi.noiseatt_125Hz); + setup_curve(noisecurves[2],2,vi.noiseatt_250Hz); + setup_curve(noisecurves[4],4,vi.noiseatt_500Hz); + setup_curve(noisecurves[6],6,vi.noiseatt_1000Hz); + setup_curve(noisecurves[8],8,vi.noiseatt_2000Hz); + setup_curve(noisecurves[10],10,vi.noiseatt_4000Hz); + setup_curve(noisecurves[12],12,vi.noiseatt_8000Hz); + + for(i=1;i<13;i+=2){ + for(j=0;j<9;j++){ + interp_curve_dB(tonecurves[i][j], + tonecurves[i-1][j], + tonecurves[i+1][j],.5); + interp_curve_dB(noisecurves[i][j], + noisecurves[i-1][j], + noisecurves[i+1][j],.5); + } + } + for(i=0;i<5;i++){ + peakatt[0][i]=fromdB(vi.peakatt_125Hz[i]); + peakatt[1][i]=fromdB(vi.peakatt_250Hz[i]); + peakatt[2][i]=fromdB(vi.peakatt_500Hz[i]); + peakatt[3][i]=fromdB(vi.peakatt_1000Hz[i]); + peakatt[4][i]=fromdB(vi.peakatt_2000Hz[i]); + peakatt[5][i]=fromdB(vi.peakatt_4000Hz[i]); + peakatt[6][i]=fromdB(vi.peakatt_8000Hz[i]); + } + */ + } +} diff --git a/jorbis/src/com/jcraft/jorbis/Residue0.java b/jorbis/src/com/jcraft/jorbis/Residue0.java new file mode 100644 index 0000000..be42518 --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/Residue0.java @@ -0,0 +1,454 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +class Residue0 extends FuncResidue{ + void pack(Object vr, Buffer opb){ + InfoResidue0 info=(InfoResidue0)vr; + int acc=0; + opb.write(info.begin,24); + opb.write(info.end,24); + + opb.write(info.grouping-1,24); /* residue vectors to group and + code with a partitioned book */ + opb.write(info.partitions-1,6); /* possible partition choices */ + opb.write(info.groupbook,8); /* group huffman book */ + + /* secondstages is a bitmask; as encoding progresses pass by pass, a + bitmask of one indicates this partition class has bits to write + this pass */ + for(int j=0;j3){ + /* yes, this is a minor hack due to not thinking ahead */ + opb.write(info.secondstages[j],3); + opb.write(1,1); + opb.write(info.secondstages[j]>>>3,5); + } + else{ + opb.write(info.secondstages[j],4); /* trailing zero */ + } + acc+=icount(info.secondstages[j]); + } + for(int j=0;j=vi.books){ + free_info(info); + return(null); + } + + for(int j=0;j=vi.books){ + free_info(info); + return(null); + } + } + return(info); +// errout: +// free_info(info); +// return(NULL); + } + + Object look(DspState vd, InfoMode vm, Object vr){ + InfoResidue0 info=(InfoResidue0)vr; + LookResidue0 look=new LookResidue0(); + int acc=0; + int dim; + int maxstage=0; + look.info=info; + look.map=vm.mapping; + + look.parts=info.partitions; + look.fullbooks=vd.fullbooks; + look.phrasebook=vd.fullbooks[info.groupbook]; + + dim=look.phrasebook.dim; + + look.partbooks=new int[look.parts][]; + + for(int j=0;jmaxstage)maxstage=stages; + look.partbooks[j]=new int[stages]; + for(int k=0; k>>=1; + } + return(ret); + } + private static int icount(int v){ + int ret=0; + while(v!=0){ + ret+=(v&1); + v>>>=1; + } + return(ret); + } +} + +class LookResidue0 { + InfoResidue0 info; + int map; + + int parts; + int stages; + CodeBook[] fullbooks; + CodeBook phrasebook; + int[][] partbooks; +// CodeBook[][] partbooks; + + int partvals; + int[][] decodemap; + + int postbits; + int phrasebits; +// int[][] frames; + int frames; +} + +class InfoResidue0{ + // block-partitioned VQ coded straight residue + int begin; + int end; + + // first stage (lossless partitioning) + int grouping; // group n vectors per partition + int partitions; // possible codebooks for a partition + int groupbook; // huffbook for partitioning + int[] secondstages=new int[64]; // expanded out to pointers in lookup + int[] booklist=new int[256]; // list of second stage books + + // encode-only heuristic settings + float[] entmax=new float[64]; // book entropy threshholds + float[] ampmax=new float[64]; // book amp threshholds + int[] subgrp=new int[64]; // book heuristic subgroup size + int[] blimit=new int[64]; // subgroup position limits +} diff --git a/jorbis/src/com/jcraft/jorbis/Residue1.java b/jorbis/src/com/jcraft/jorbis/Residue1.java new file mode 100644 index 0000000..c29ed8d --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/Residue1.java @@ -0,0 +1,51 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +class Residue1 extends Residue0{ + int forward(Block vb,Object vl, float[][] in, int ch){ + System.err.println("Residue0.forward: not implemented"); + return 0; + } + + int inverse(Block vb, Object vl, float[][] in, int[] nonzero, int ch){ +//System.err.println("Residue0.inverse"); + int used=0; + for(int i=0; i + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +class Residue2 extends Residue0{ + int forward(Block vb,Object vl, float[][] in, int ch){ + System.err.println("Residue0.forward: not implemented"); + return 0; + } + + int inverse(Block vb, Object vl, float[][] in, int[] nonzero, int ch){ +//System.err.println("Residue0.inverse"); + int i=0; + for(i=0;i + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +class StaticCodeBook{ + int dim; // codebook dimensions (elements per vector) + int entries; // codebook entries + int[] lengthlist; // codeword lengths in bits + + // mapping + int maptype; // 0=none + // 1=implicitly populated values from map column + // 2=listed arbitrary values + + // The below does a linear, single monotonic sequence mapping. + int q_min; // packed 32 bit float; quant value 0 maps to minval + int q_delta; // packed 32 bit float; val 1 - val 0 == delta + int q_quant; // bits: 0 < quant <= 16 + int q_sequencep; // bitflag + + // additional information for log (dB) mapping; the linear mapping + // is assumed to actually be values in dB. encodebias is used to + // assign an error weight to 0 dB. We have two additional flags: + // zeroflag indicates if entry zero is to represent -Inf dB; negflag + // indicates if we're to represent negative linear values in a + // mirror of the positive mapping. + + int[] quantlist; // map == 1: (int)(entries/dim) element column map + // map == 2: list of dim*entries quantized entry vals + + // encode helpers + EncodeAuxNearestMatch nearest_tree; + EncodeAuxThreshMatch thresh_tree; + + StaticCodeBook(){} + StaticCodeBook(int dim, int entries, int[] lengthlist, + int maptype, int q_min, int q_delta, + int q_quant, int q_sequencep, int[] quantlist, + //EncodeAuxNearestmatch nearest_tree, + Object nearest_tree, + // EncodeAuxThreshmatch thresh_tree, + Object thresh_tree + ){ + this(); + this.dim=dim; this.entries=entries; this.lengthlist=lengthlist; + this.maptype=maptype; this.q_min=q_min; this.q_delta=q_delta; + this.q_quant=q_quant; this.q_sequencep=q_sequencep; + this.quantlist=quantlist; + } + + int pack(Buffer opb){ + int i; + boolean ordered=false; + + opb.write(0x564342,24); + opb.write(dim, 16); + opb.write(entries, 24); + + // pack the codewords. There are two packings; length ordered and + // length random. Decide between the two now. + + for(i=1;i_last){ + for(int j=_last;j<_this;j++){ + opb.write(i-count,ilog(entries-count)); + count=i; + } + } + } + opb.write(i-count,ilog(entries-count)); + } + else{ + // length random. Again, we don't code the codeword itself, just + // the length. This time, though, we have to encode each length + opb.write(0,1); // unordered + + // algortihmic mapping has use for 'unused entries', which we tag + // here. The algorithmic mapping happens as usual, but the unused + // entry has no codeword. + for(i=0;ientries/c->dim) quantized values for + // building a full value list algorithmically (square lattice) + quantvals=maptype1_quantvals(); + break; + case 2: + // every value (c->entries*c->dim total) specified explicitly + quantvals=entries*dim; + break; + } + + // quantized values + for(i=0;ibim <= b->entries + // treat the above as an initial guess + while(true){ + int acc=1; + int acc1=1; + for(int i=0;ientries){ return(vals); } + else{ + if(acc>entries){ vals--; } + else{ vals++; } + } + } + } + + void clear(){ +// if(quantlist!=null)free(b->quantlist); +// if(lengthlist!=null)free(b->lengthlist); +// if(nearest_tree!=null){ +// free(b->nearest_tree->ptr0); +// free(b->nearest_tree->ptr1); +// free(b->nearest_tree->p); +// free(b->nearest_tree->q); +// memset(b->nearest_tree,0,sizeof(encode_aux_nearestmatch)); +// free(b->nearest_tree); +// } +// if(thresh_tree!=null){ +// free(b->thresh_tree->quantthresh); +// free(b->thresh_tree->quantmap); +// memset(b->thresh_tree,0,sizeof(encode_aux_threshmatch)); +// free(b->thresh_tree); +// } +// memset(b,0,sizeof(static_codebook)); + } + + // unpack the quantized list of values for encode/decode + // we need to deal with two map types: in map type 1, the values are + // generated algorithmically (each column of the vector counts through + // the values in the quant vector). in map type 2, all the values came + // in in an explicit list. Both value lists must be unpacked + float[] unquantize(){ + + if(maptype==1 || maptype==2){ + int quantvals; + float mindel=float32_unpack(q_min); + float delta=float32_unpack(q_delta); + float[] r=new float[entries*dim]; + + //System.err.println("q_min="+q_min+", mindel="+mindel); + + // maptype 1 and 2 both use a quantized value vector, but + // different sizes + switch(maptype){ + case 1: + // most of the time, entries%dimensions == 0, but we need to be + // well defined. We define that the possible vales at each + // scalar is values == entries/dim. If entries%dim != 0, we'll + // have 'too few' values (values*dim "+val+" | ");} + val=Math.abs(val)*delta+mindel+last; + if(q_sequencep!=0)last=val; + r[j*dim+k]=val; +//if((j*dim+k)==0){System.err.println(" $ r[0] -> "+r[0]+" | ");} + } + } +//System.err.println("\nr[0]="+r[0]); + } + return(r); + } + return(null); + } + + private static int ilog(int v){ + int ret=0; + while(v!=0){ + ret++; + v>>>=1; + } + return(ret); + } + + // 32 bit float (not IEEE; nonnormalized mantissa + + // biased exponent) : neeeeeee eeemmmmm mmmmmmmm mmmmmmmm + // Why not IEEE? It's just not that important here. + + static final int VQ_FEXP=10; + static final int VQ_FMAN=21; + static final int VQ_FEXP_BIAS=768; // bias toward values smaller than 1. + + // doesn't currently guard under/overflow + static long float32_pack(float val){ + int sign=0; + int exp; + int mant; + if(val<0){ + sign=0x80000000; + val= -val; + } + exp=(int)Math.floor(Math.log(val)/Math.log(2)); + mant=(int)Math.rint(Math.pow(val,(VQ_FMAN-1)-exp)); + exp=(exp+VQ_FEXP_BIAS)<>>VQ_FMAN; +//System.err.println("mant="+mant+", sign="+sign+", exp="+exp); + //if(sign!=0.0)mant= -mant; + if((val&0x80000000)!=0)mant= -mant; +//System.err.println("mant="+mant); + return(ldexp(mant,((int)exp)-(VQ_FMAN-1)-VQ_FEXP_BIAS)); + } + + static float ldexp(float foo, int e){ + return (float)(foo*Math.pow(2, e)); + } + +/* + // TEST + // Unit tests of the dequantizer; this stuff will be OK + // cross-platform, I simply want to be sure that special mapping cases + // actually work properly; a bug could go unnoticed for a while + + // cases: + // + // no mapping + // full, explicit mapping + // algorithmic mapping + // + // nonsequential + // sequential + + static int[] full_quantlist1={0,1,2,3, 4,5,6,7, 8,3,6,1}; + static int[] partial_quantlist1={0,7,2}; + + // no mapping + static StaticCodeBook test1=new StaticCodeBook(4,16,null, + 0,0,0,0,0, + null,null,null); + static float[] test1_result=null; + + // linear, full mapping, nonsequential + static StaticCodeBook test2=new StaticCodeBook(4,3,null, + 2,-533200896,1611661312,4,0, + full_quantlist1, null, null); + static float[] test2_result={-3,-2,-1,0, 1,2,3,4, 5,0,3,-2}; + + // linear, full mapping, sequential + static StaticCodeBook test3=new StaticCodeBook(4,3,null, + 2, -533200896,1611661312,4,1, + full_quantlist1,null, null); + static float[] test3_result={-3,-5,-6,-6, 1,3,6,10, 5,5,8,6}; + + // linear, algorithmic mapping, nonsequential + static StaticCodeBook test4=new StaticCodeBook(3,27,null, + 1,-533200896,1611661312,4,0, + partial_quantlist1,null,null); + static float[] test4_result={-3,-3,-3, 4,-3,-3, -1,-3,-3, + -3, 4,-3, 4, 4,-3, -1, 4,-3, + -3,-1,-3, 4,-1,-3, -1,-1,-3, + -3,-3, 4, 4,-3, 4, -1,-3, 4, + -3, 4, 4, 4, 4, 4, -1, 4, 4, + -3,-1, 4, 4,-1, 4, -1,-1, 4, + -3,-3,-1, 4,-3,-1, -1,-3,-1, + -3, 4,-1, 4, 4,-1, -1, 4,-1, + -3,-1,-1, 4,-1,-1, -1,-1,-1}; + + // linear, algorithmic mapping, sequential + static StaticCodeBook test5=new StaticCodeBook(3,27,null, + 1,-533200896,1611661312,4,1, + partial_quantlist1,null,null); + static float[] test5_result={-3,-6,-9, 4, 1,-2, -1,-4,-7, + -3, 1,-2, 4, 8, 5, -1, 3, 0, + -3,-4,-7, 4, 3, 0, -1,-2,-5, + -3,-6,-2, 4, 1, 5, -1,-4, 0, + -3, 1, 5, 4, 8,12, -1, 3, 7, + -3,-4, 0, 4, 3, 7, -1,-2, 2, + -3,-6,-7, 4, 1, 0, -1,-4,-5, + -3, 1, 0, 4, 8, 7, -1, 3, 2, + -3,-4,-5, 4, 3, 2, -1,-2,-3}; + + void run_test(float[] comp){ + float[] out=unquantize(); + if(comp!=null){ + if(out==null){ + System.err.println("_book_unquantize incorrectly returned NULL"); + System.exit(1); + } + for(int i=0;i.0001){ + System.err.println("disagreement in unquantized and reference data:\nposition "+i+": "+out[i]+" != "+comp[i]); + System.exit(1); + } + } + } + else{ + if(out!=null){ + System.err.println("_book_unquantize returned a value array:\n correct result should have been NULL"); + System.exit(1); + } + } + } + + public static void main(String[] arg){ + // run the nine dequant tests, and compare to the hand-rolled results + System.err.print("Dequant test 1... "); + test1.run_test(test1_result); + System.err.print("OK\nDequant test 2... "); + test2.run_test(test2_result); + System.err.print("OK\nDequant test 3... "); + test3.run_test(test3_result); + System.err.print("OK\nDequant test 4... "); + test4.run_test(test4_result); + System.err.print("OK\nDequant test 5... "); + test5.run_test(test5_result); + System.err.print("OK\n\n"); + } +*/ +} + + + + + diff --git a/jorbis/src/com/jcraft/jorbis/Time0.java b/jorbis/src/com/jcraft/jorbis/Time0.java new file mode 100644 index 0000000..f6a9fcb --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/Time0.java @@ -0,0 +1,38 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +class Time0 extends FuncTime{ + void pack(Object i, Buffer opb){} + Object unpack(Info vi , Buffer opb){return "";} + Object look(DspState vd, InfoMode mi, Object i){return "";} + void free_info(Object i){} + void free_look(Object i){} + int forward(Block vb, Object i){return 0;} + int inverse(Block vb, Object i, float[] in, float[] out){return 0;} +} diff --git a/jorbis/src/com/jcraft/jorbis/VorbisFile.java b/jorbis/src/com/jcraft/jorbis/VorbisFile.java new file mode 100644 index 0000000..64edff0 --- /dev/null +++ b/jorbis/src/com/jcraft/jorbis/VorbisFile.java @@ -0,0 +1,1361 @@ +/* JOrbis + * Copyright (C) 2000 ymnk, JCraft,Inc. + * + * Written by: 2000 ymnk + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; + +import java.io.InputStream; +import java.io.IOException; + +public class VorbisFile{ + static final int CHUNKSIZE=8500; + static final int SEEK_SET=0; + static final int SEEK_CUR=1; + static final int SEEK_END=2; + + static final int OV_FALSE=-1; + static final int OV_EOF=-2; + static final int OV_HOLE=-3; + + static final int OV_EREAD=-128; + static final int OV_EFAULT=-129; + static final int OV_EIMPL=-130; + static final int OV_EINVAL=-131; + static final int OV_ENOTVORBIS=-132; + static final int OV_EBADHEADER=-133; + static final int OV_EVERSION=-134; + static final int OV_ENOTAUDIO=-135; + static final int OV_EBADPACKET=-136; + static final int OV_EBADLINK=-137; + static final int OV_ENOSEEK=-138; + + InputStream datasource; + boolean seekable=false; + long offset; + long end; + + SyncState oy=new SyncState(); + + int links; + long[] offsets; + long[] dataoffsets; + int[] serialnos; + long[] pcmlengths; + Info[] vi; + Comment[] vc; + + // Decoding working state local storage + long pcm_offset; + boolean decode_ready=false; + int current_serialno; + int current_link; + + float bittrack; + float samptrack; + + StreamState os=new StreamState(); // take physical pages, weld into a logical + // stream of packets + DspState vd=new DspState(); // central working state for + // the packet->PCM decoder + Block vb=new Block(vd); // local working space for packet->PCM decode + + //ov_callbacks callbacks; + + public VorbisFile(String file) throws JOrbisException { + super(); + InputStream is=null; + try{ + is=new SeekableInputStream(file); + int ret=open(is, null, 0); + if(ret==-1){ + throw new JOrbisException("VorbisFile: open return -1"); + } + } + catch(Exception e){ + throw new JOrbisException("VorbisFile: "+e.toString()); + } + finally{ + if(is != null){ + try { + is.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + public VorbisFile(InputStream is, byte[] initial, int ibytes) + throws JOrbisException { + super(); + int ret=open(is, initial, ibytes); + if(ret==-1){ + } + } + + private int get_data(){ + int index=oy.buffer(CHUNKSIZE); + byte[] buffer=oy.data; +// int bytes=callbacks.read_func(buffer, index, 1, CHUNKSIZE, datasource); + int bytes=0; + try{ + bytes=datasource.read(buffer, index, CHUNKSIZE); + } + catch(Exception e){ + //System.err.println(e); + return OV_EREAD; + } + oy.wrote(bytes); + if(bytes==-1){ +// System.out.println("bytes="+bytes); + bytes=0; + } + return bytes; + } + + private void seek_helper(long offst){ + //callbacks.seek_func(datasource, offst, SEEK_SET); + fseek(datasource, offst, SEEK_SET); + this.offset=offst; + oy.reset(); + } + + private int get_next_page(Page page, long boundary){ + if(boundary>0) boundary+=offset; + while(true){ + int more; + if(boundary>0 && offset>=boundary)return OV_FALSE; + more=oy.pageseek(page); + if(more<0){offset-=more;} + else{ + if(more==0){ + if(boundary==0)return OV_FALSE; +// if(get_data()<=0)return -1; + int ret=get_data(); + if(ret==0) return OV_EOF; + if(ret<0) return OV_EREAD; + } + else{ + int ret=(int)offset; //!!! + offset+=more; + return ret; + } + } + } + } + + private int get_prev_page(Page page) throws JOrbisException { + long begin=offset; //!!! + int ret; + int offst=-1; + while(offst==-1){ + begin-=CHUNKSIZE; + if(begin<0) + begin=0; + seek_helper(begin); + while(offset=0)next=ret; + } + else{ + searched=ret+page.header_len+page.body_len; + } + } + seek_helper(next); + ret=get_next_page(page, -1); + if(ret==OV_EREAD) return OV_EREAD; + + if(searched>=end || ret==-1){ + links=m+1; + offsets=new long[m+2]; + offsets[m+1]=searched; + } + else{ + ret=bisect_forward_serialno(next, offset, end, page.serialno(), m+1); + if(ret==OV_EREAD)return OV_EREAD; + } + offsets[m]=begin; + return 0; + } + + // uses the local ogg_stream storage in vf; this is important for + // non-streaming input sources + int fetch_headers(Info vi, Comment vc, int[] serialno, Page og_ptr){ + //System.err.println("fetch_headers"); + Page og=new Page(); + Packet op=new Packet(); + int ret; + + if(og_ptr==null){ + ret=get_next_page(og, CHUNKSIZE); + if(ret==OV_EREAD)return OV_EREAD; + if(ret<0) return OV_ENOTVORBIS; + og_ptr=og; + } + + if(serialno!=null)serialno[0]=og_ptr.serialno(); + + os.init(og_ptr.serialno()); + + // extract the initial header from the first page and verify that the + // Ogg bitstream is in fact Vorbis data + + vi.init(); + vc.init(); + + int i=0; + while(i<3){ + os.pagein(og_ptr); + while(i<3){ + int result=os.packetout(op); + if(result==0)break; + if(result==-1){ + //System.err.println("Corrupt header in logical bitstream."); + //goto bail_header; + vi.clear(); + vc.clear(); + os.clear(); + return -1; + } + if(vi.synthesis_headerin(vc, op)!=0){ + //System.err.println("Illegal header in logical bitstream."); + //goto bail_header; + vi.clear(); + vc.clear(); + os.clear(); + return -1; + } + i++; + } + if(i<3) + if(get_next_page(og_ptr, 1)<0){ + //System.err.println("Missing header in logical bitstream."); + //goto bail_header; + vi.clear(); + vc.clear(); + os.clear(); + return -1; + } + } + return 0; + +// bail_header: +// vorbis_info_clear(vi); +// vorbis_comment_clear(vc); +// ogg_stream_clear(&vf->os); +// return -1; + } + + // last step of the OggVorbis_File initialization; get all the + // vorbis_info structs and PCM positions. Only called by the seekable + // initialization (local stream storage is hacked slightly; pay + // attention to how that's done) + void prefetch_all_headers(Info first_i,Comment first_c, + int dataoffset) throws JOrbisException { + Page og=new Page(); + int ret; + + vi=new Info[links]; + vc=new Comment[links]; + dataoffsets=new long[links]; + pcmlengths=new long[links]; + serialnos=new int[links]; + + for(int i=0;ivi+i,first_i,sizeof(vorbis_info)); + vc[i]=first_c; + //memcpy(vf->vc+i,first_c,sizeof(vorbis_comment)); + dataoffsets[i]=dataoffset; + } + else{ + // seek to the location of the initial header + seek_helper(offsets[i]); //!!! + vi[i]=new Info(); + vc[i]=new Comment(); + if(fetch_headers(vi[i], vc[i], null, null)==-1){ + //System.err.println("Error opening logical bitstream #"+(i+1)+"\n"); + dataoffsets[i]=-1; + } + else{ + dataoffsets[i]=offset; + os.clear(); + } + } + + // get the serial number and PCM length of this link. To do this, + // get the last page of the stream + { + long end=offsets[i+1]; //!!! + seek_helper(end); + + while(true){ + ret=get_prev_page(og); + if(ret==-1){ + // this should not be possible + //System.err.println("Could not find last page of logical "+ + // "bitstream #"+(i)+"\n"); + vi[i].clear(); + vc[i].clear(); + break; + } + if(og.granulepos()!=-1){ + serialnos[i]=og.serialno(); + pcmlengths[i]=og.granulepos(); + break; + } + } + } + } + } + + int make_decode_ready(){ + if(decode_ready)System.exit(1); + vd.synthesis_init(vi[0]); + vb.init(vd); + decode_ready=true; + return(0); + } + + int open_seekable() throws JOrbisException { + Info initial_i=new Info(); + Comment initial_c=new Comment(); + int serialno; + long end; + int ret; + int dataoffset; + Page og=new Page(); + // is this even vorbis...? + int[] foo=new int[1]; + ret=fetch_headers(initial_i, initial_c, foo, null); + serialno=foo[0]; + dataoffset=(int)offset; //!! + os.clear(); + if(ret==-1)return(-1); + // we can seek, so set out learning all about this file + seekable=true; + //(callbacks.seek_func)(datasource, 0, SEEK_END); + fseek(datasource, 0, SEEK_END); + //offset=end=(callbacks.tell_func)(datasource); + offset=ftell(datasource); + end=offset; + // We get the offset for the last page of the physical bitstream. + // Most OggVorbis files will contain a single logical bitstream + end=get_prev_page(og); + // moer than one logical bitstream? + if(og.serialno()!=serialno){ + // Chained bitstream. Bisect-search each logical bitstream + // section. Do so based on serial number only + if(bisect_forward_serialno(0,0,end+1,serialno,0)<0){ + clear(); + return OV_EREAD; + } + } + else{ + // Only one logical bitstream + if(bisect_forward_serialno(0,end,end+1,serialno,0)<0){ + clear(); + return OV_EREAD; + } + } + prefetch_all_headers(initial_i, initial_c, dataoffset); + return(raw_seek(0)); + } + + int open_nonseekable(){ + //System.err.println("open_nonseekable"); + // we cannot seek. Set up a 'single' (current) logical bitstream entry + links=1; + vi=new Info[links]; vi[0]=new Info(); // ?? + vc=new Comment[links]; vc[0]=new Comment(); // ?? bug? + + // Try to fetch the headers, maintaining all the storage + int[]foo=new int[1]; + if(fetch_headers(vi[0], vc[0], foo, null)==-1)return(-1); + current_serialno=foo[0]; + make_decode_ready(); + return 0; + } + + // clear out the current logical bitstream decoder + void decode_clear(){ + os.clear(); + vd.clear(); + vb.clear(); + decode_ready=false; + bittrack=0.f; + samptrack=0.f; + } + + // fetch and process a packet. Handles the case where we're at a + // bitstream boundary and dumps the decoding machine. If the decoding + // machine is unloaded, it loads it. It also keeps pcm_offset up to + // date (seek and read both use this. seek uses a special hack with + // readp). + // + // return: -1) hole in the data (lost packet) + // 0) need more date (only if readp==0)/eof + // 1) got a packet + + int process_packet(int readp){ + Page og=new Page(); + + // handle one packet. Try to fetch it from current stream state + // extract packets from page + while(true){ + // process a packet if we can. If the machine isn't loaded, + // neither is a page + if(decode_ready){ + Packet op=new Packet(); + int result=os.packetout(op); + long granulepos; + // if(result==-1)return(-1); // hole in the data. For now, swallow + // and go. We'll need to add a real + // error code in a bit. + if(result>0){ + // got a packet. process it + granulepos=op.granulepos; + if(vb.synthesis(op)==0){ // lazy check for lazy + // header handling. The + // header packets aren't + // audio, so if/when we + // submit them, + // vorbis_synthesis will + // reject them + // suck in the synthesis data and track bitrate + { + int oldsamples=vd.synthesis_pcmout(null, null); + vd.synthesis_blockin(vb); + samptrack+=vd.synthesis_pcmout(null, null)-oldsamples; + bittrack+=op.bytes*8; + } + + // update the pcm offset. + if(granulepos!=-1 && op.e_o_s==0){ + int link=(seekable?current_link:0); + int samples; + // this packet has a pcm_offset on it (the last packet + // completed on a page carries the offset) After processing + // (above), we know the pcm position of the *last* sample + // ready to be returned. Find the offset of the *first* + // + // As an aside, this trick is inaccurate if we begin + // reading anew right at the last page; the end-of-stream + // granulepos declares the last frame in the stream, and the + // last packet of the last page may be a partial frame. + // So, we need a previous granulepos from an in-sequence page + // to have a reference point. Thus the !op.e_o_s clause above + + samples=vd.synthesis_pcmout(null, null); + granulepos-=samples; + for(int i=0;icallbacks.close_func)(vf->datasource); + //memset(vf,0,sizeof(OggVorbis_File)); + return(0); + } + + static int fseek(InputStream fis, + //int64_t off, + long off, + int whence){ + if(fis instanceof SeekableInputStream){ + SeekableInputStream sis=(SeekableInputStream)fis; + try{ + if(whence==SEEK_SET){ + sis.seek(off); + } + else if(whence==SEEK_END){ + sis.seek(sis.getLength()-off); + } + else{ + //System.out.println("seek: "+whence+" is not supported"); + } + } + catch(Exception e){ + } + return 0; + } + try{ + if(whence==0){ fis.reset(); } + fis.skip(off); + } + catch(Exception e){return -1;} + return 0; + } + + static long ftell(InputStream fis){ + try{ + if(fis instanceof SeekableInputStream){ + SeekableInputStream sis=(SeekableInputStream)fis; + return (sis.tell()); + } + } + catch(Exception e){ + } + return 0; + } + + // inspects the OggVorbis file and finds/documents all the logical + // bitstreams contained in it. Tries to be tolerant of logical + // bitstream sections that are truncated/woogie. + // + // return: -1) error + // 0) OK + + int open(InputStream is, byte[] initial, int ibytes) throws JOrbisException { + //callbacks callbacks = { + // (size_t (*)(void *, size_t, size_t, void *)) fread, + // (int (*)(void *, int64_t, int)) _fseek, + // (int (*)(void *)) fclose, + // (long (*)(void *)) ftell + // }; + return open_callbacks(is, initial, ibytes//, callbacks + ); + } + + int open_callbacks(InputStream is, byte[] initial, + int ibytes//, callbacks callbacks + ) throws JOrbisException { + int ret; + datasource=is; + //callbacks = _callbacks; + // init the framing state + oy.init(); + + // perhaps some data was previously read into a buffer for testing + // against other stream types. Allow initialization from this + // previously read data (as we may be reading from a non-seekable + // stream) + if(initial!=null){ + int index=oy.buffer(ibytes); + System.arraycopy(initial, 0, oy.data, index, ibytes); + oy.wrote(ibytes); + } + // can we seek? Stevens suggests the seek test was portable + if(is instanceof SeekableInputStream){ ret=open_seekable(); } + else{ ret=open_nonseekable(); } + if(ret!=0){ + datasource=null; + clear(); + } + return ret; + } + + // How many logical bitstreams in this physical bitstream? + public int streams(){ + return links; + } + + // Is the FILE * associated with vf seekable? + public boolean seekable(){ + return seekable; + } + + // returns the bitrate for a given logical bitstream or the entire + // physical bitstream. If the file is open for random access, it will + // find the *actual* average bitrate. If the file is streaming, it + // returns the nominal bitrate (if set) else the average of the + // upper/lower bounds (if set) else -1 (unset). + // + // If you want the actual bitrate field settings, get them from the + // vorbis_info structs + + public int bitrate(int i){ + if(i>=links)return(-1); + if(!seekable && i!=0)return(bitrate(0)); + if(i<0){ + long bits=0; + for(int j=0;j0){ + return vi[i].bitrate_nominal; + } + else{ + if(vi[i].bitrate_upper>0){ + if(vi[i].bitrate_lower>0){ + return (vi[i].bitrate_upper+vi[i].bitrate_lower)/2; + }else{ + return vi[i].bitrate_upper; + } + } + return(-1); + } + } + } + } + + // returns the actual bitrate since last call. returns -1 if no + // additional data to offer since last call (or at beginning of stream) + public int bitrate_instant(){ + int _link=(seekable?current_link:0); + if(samptrack==0)return(-1); + int ret=(int)(bittrack/samptrack*vi[_link].rate+.5); + bittrack=0.f; + samptrack=0.f; + return(ret); + } + + public int serialnumber(int i){ + if(i>=links)return(-1); + if(!seekable && i>=0)return(serialnumber(-1)); + if(i<0){ + return(current_serialno); + } + else{ + return(serialnos[i]); + } + } + + // returns: total raw (compressed) length of content if i==-1 + // raw (compressed) length of that logical bitstream for i==0 to n + // -1 if the stream is not seekable (we can't know the length) + + public long raw_total(int i){ + if(!seekable || i>=links)return(-1); + if(i<0){ + long acc=0; // bug? + for(int j=0;j=links)return(-1); + if(i<0){ + long acc=0; + for(int j=0;j=links)return(-1); + if(i<0){ + float acc=0; + for(int j=0;joffsets[links]){ + //goto seek_error; + pcm_offset=-1; + decode_clear(); + return -1; + } + + // clear out decoding machine state + pcm_offset=-1; + decode_clear(); + + // seek + seek_helper(pos); + + // we need to make sure the pcm_offset is set. We use the + // _fetch_packet helper to process one packet with readp set, then + // call it until it returns '0' with readp not set (the last packet + // from a page has the 'granulepos' field set, and that's how the + // helper updates the offset + + switch(process_packet(1)){ + case 0: + // oh, eof. There are no packets remaining. Set the pcm offset to + // the end of file + pcm_offset=pcm_total(-1); + return(0); + case -1: + // error! missing data or invalid bitstream structure + //goto seek_error; + pcm_offset=-1; + decode_clear(); + return -1; + default: + // all OK + break; + } + while(true){ + switch(process_packet(0)){ + case 0: + // the offset is set. If it's a bogus bitstream with no offset + // information, it's not but that's not our fault. We still run + // gracefully, we're just missing the offset + return(0); + case -1: + // error! missing data or invalid bitstream structure + //goto seek_error; + pcm_offset=-1; + decode_clear(); + return -1; + default: + // continue processing packets + break; + } + } + + // seek_error: + // dump the machine so we're in a known state + //pcm_offset=-1; + //decode_clear(); + //return -1; + } + + // seek to a sample offset relative to the decompressed pcm stream + // returns zero on success, nonzero on failure + + public int pcm_seek(long pos){ + int link=-1; + long total=pcm_total(-1); + + if(!seekable)return(-1); // don't dump machine if we can't seek + if(pos<0 || pos>total){ + //goto seek_error; + pcm_offset=-1; + decode_clear(); + return -1; + } + + // which bitstream section does this pcm offset occur in? + for(link=links-1;link>=0;link--){ + total-=pcmlengths[link]; + if(pos>=total)break; + } + + // search within the logical bitstream for the page with the highest + // pcm_pos preceeding (or equal to) pos. There is a danger here; + // missing pages or incorrect frame number information in the + // bitstream could make our task impossible. Account for that (it + // would be an error condition) + { + long target=pos-total; + long end=offsets[link+1]; + long begin=offsets[link]; + int best=(int)begin; + + Page og=new Page(); + while(begin=pos){ + //goto seek_error; + pcm_offset=-1; + decode_clear(); + return -1; + } + if(pos>pcm_total(-1)){ + //goto seek_error; + pcm_offset=-1; + decode_clear(); + return -1; + } + + // discard samples until we reach the desired position. Crossing a + // logical bitstream boundary with abandon is OK. + while(pcm_offsettarget)samples=target; + vd.synthesis_read(samples); + pcm_offset+=samples; + + if(samplestime_total){ + //goto seek_error; + pcm_offset=-1; + decode_clear(); + return -1; + } + + // which bitstream section does this time offset occur in? + for(link=links-1;link>=0;link--){ + pcm_total-=pcmlengths[link]; + time_total-=time_total(link); + if(seconds>=time_total)break; + } + + // enough information to convert time offset to pcm offset + { + long target=(long)(pcm_total+(seconds-time_total)*vi[link].rate); + return(pcm_seek(target)); + } + + //seek_error: + // dump machine so we're in a known state + //pcm_offset=-1; + //decode_clear(); + //return -1; + } + + // tell the current stream offset cursor. Note that seek followed by + // tell will likely not give the set offset due to caching + public long raw_tell(){ + return(offset); + } + + // return PCM offset (sample) of next PCM sample to be read + public long pcm_tell(){ + return(pcm_offset); + } + + // return time offset (seconds) of next PCM sample to be read + public float time_tell(){ + // translate time to PCM position and call pcm_seek + + int link=-1; + long pcm_total=0; + float time_total=0.f; + + if(seekable){ + pcm_total=pcm_total(-1); + time_total=time_total(-1); + + // which bitstream section does this time offset occur in? + for(link=links-1;link>=0;link--){ + pcm_total-=pcmlengths[link]; + time_total-=time_total(link); + if(pcm_offset>=pcm_total)break; + } + } + + return((float)time_total+(float)(pcm_offset-pcm_total)/vi[link].rate); + } + + // link: -1) return the vorbis_info struct for the bitstream section + // currently being decoded + // 0-n) to request information for a specific bitstream section + // + // In the case of a non-seekable bitstream, any call returns the + // current bitstream. NULL in the case that the machine is not + // initialized + + public Info getInfo(int link){ + if(seekable){ + if(link<0){ + if(decode_ready){ + return vi[current_link]; + } + else{ + return null; + } + } + else{ + if(link>=links){ + return null; + } + else{ + return vi[link]; + } + } + } + else{ + if(decode_ready){ + return vi[0]; + } + else{ + return null; + } + } + } + + public Comment getComment(int link){ + if(seekable){ + if(link<0){ + if(decode_ready){ return vc[current_link]; } + else{ return null; } + } + else{ + if(link>=links){ return null;} + else{ return vc[link]; } + } + } + else{ + if(decode_ready){ return vc[0]; } + else{ return null; } + } + } + + int host_is_big_endian() { + return 1; +// short pattern = 0xbabe; +// unsigned char *bytewise = (unsigned char *)&pattern; +// if (bytewise[0] == 0xba) return 1; +// assert(bytewise[0] == 0xbe); +// return 0; + } + + // up to this point, everything could more or less hide the multiple + // logical bitstream nature of chaining from the toplevel application + // if the toplevel application didn't particularly care. However, at + // the point that we actually read audio back, the multiple-section + // nature must surface: Multiple bitstream sections do not necessarily + // have to have the same number of channels or sampling rate. + // + // read returns the sequential logical bitstream number currently + // being decoded along with the PCM data in order that the toplevel + // application can take action on channel/sample rate changes. This + // number will be incremented even for streamed (non-seekable) streams + // (for seekable streams, it represents the actual logical bitstream + // index within the physical bitstream. Note that the accessor + // functions above are aware of this dichotomy). + // + // input values: buffer) a buffer to hold packed PCM data for return + // length) the byte length requested to be placed into buffer + // bigendianp) should the data be packed LSB first (0) or + // MSB first (1) + // word) word size for output. currently 1 (byte) or + // 2 (16 bit short) + // + // return values: -1) error/hole in data + // 0) EOF + // n) number of bytes of PCM actually returned. The + // below works on a packet-by-packet basis, so the + // return length is not related to the 'length' passed + // in, just guaranteed to fit. + // + // *section) set to the logical bitstream number + + int read(byte[] buffer,int length, + int bigendianp, int word, int sgned, int[] bitstream){ + int host_endian = host_is_big_endian(); + int index=0; + + while(true){ + if(decode_ready){ + float[][] pcm; + float[][][] _pcm=new float[1][][]; + int[] _index=new int[getInfo(-1).channels]; + int samples=vd.synthesis_pcmout(_pcm, _index); + pcm=_pcm[0]; + if(samples!=0){ + // yay! proceed to pack data into the byte buffer + int channels=getInfo(-1).channels; + int bytespersample=word * channels; + if(samples>length/bytespersample)samples=length/bytespersample; + + // a tight loop to pack each size + { + int val; + if(word==1){ + int off=(sgned!=0?0:128); + for(int j=0;j127)val=127; + else if(val<-128)val=-128; + buffer[index++]=(byte)(val+off); + } + } + } + else{ + int off=(sgned!=0?0:32768); + + if(host_endian==bigendianp){ + if(sgned!=0){ + for(int i=0;i32767)val=32767; + else if(val<-32768)val=-32768; + buffer[dest]=(byte)(val>>>8); + buffer[dest+1]=(byte)(val); + dest+=channels*2; + } + } + } + else{ + for(int i=0;i32767)val=32767; + else if(val<-32768)val=-32768; + buffer[dest]=(byte)((val+off)>>>8); + buffer[dest+1]=(byte)(val+off); + dest+=channels*2; + } + } + } + } + else if(bigendianp!=0){ + for(int j=0;j32767)val=32767; + else if(val<-32768)val=-32768; + val+=off; + buffer[index++]=(byte)(val>>>8); + buffer[index++]=(byte)val; + } + } + } + else{ + //int val; + for(int j=0;j32767)val=32767; + else if(val<-32768)val=-32768; + val+=off; + buffer[index++]=(byte)val; + buffer[index++]=(byte)(val>>>8); + } + } + } + } + } + + vd.synthesis_read(samples); + pcm_offset+=samples; + if(bitstream!=null)bitstream[0]=current_link; + return(samples*bytespersample); + } + } + + // suck in another packet + switch(process_packet(1)){ + case 0: + return(0); + case -1: + return -1; + default: + break; + } + } + } + + public Info[] getInfo(){return vi;} + public Comment[] getComment(){return vc;} + +/* + public static void main(String[] arg){ + try{ + VorbisFile foo=new VorbisFile(arg[0]); + int links=foo.streams(); + System.out.println("links="+links); + Comment[] comment=foo.getComment(); + Info[] info=foo.getInfo(); + for(int i=0; i + * + * Many thanks to + * Monty and + * The XIPHOPHORUS Company http://www.xiph.org/ . + * JOrbis has been based on their awesome works, Vorbis codec. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package com.jcraft.jorbis; + +import com.jcraft.jogg.*; +import java.io.InputStream; + +public class VorbisFile{ + static final int CHUNKSIZE=4096; + static final int SEEK_SET=0; + + InputStream datasource; + boolean seekable=false; + long offset; + long end; + + SyncState oy=new SyncState(); + + int links; + Comment[] vc; + Info[] vi; + + long[] offsets; + long[] dataoffsets; + int[] serialnos; + long[] pcmlengths; + + + + // Decoding working state local storage + long pcm_offset; + boolean decode_ready=false; + int current_serialno; + int current_link; + + float bittrack; + float samptrack; + + StreamState os=new StreamState(); // take physical pages, weld into a logical + // stream of packets + DspState vd=new DspState(); // central working state for + // the packet->PCM decoder + Block vb=new Block(vd); // local working space for packet->PCM decode + + //ov_callbacks callbacks; + + public VorbisFile(String file) throws JOrbisException { + super(); + InputStream is=null; + try{ is=new java.io.BufferedInputStream(new java.io.FileInputStream(file));} + catch(Exception e){ + throw new JOrbisException("VorbisFile: "+e.toString()); + } + int ret=open(is, null, 0); + if(ret==-1){ + throw new JOrbisException("VorbisFile: open return -1"); + } + } + + public VorbisFile(InputStream is, byte[] initial, int ibytes) + throws JOrbisException { + super(); + int ret=open(is, initial, ibytes); + if(ret==-1){ + } + } + + private int get_data(){ + int index=oy.buffer(CHUNKSIZE); + byte[] buffer=oy.data; +// int bytes=callbacks.read_func(buffer, index, 1, CHUNKSIZE, datasource); + int bytes=0; + try{ + bytes=datasource.read(buffer, index, CHUNKSIZE); + } + catch(Exception e){System.err.println(e);} + oy.wrote(bytes); + return bytes; + } + + private void seek_helper(int offst){ + //callbacks.seek_func(datasource, offst, SEEK_SET); + fseek64_wrap(datasource, offst, SEEK_SET); + this.offset=offst; + oy.reset(); + } + + private int get_next_page(Page page, int boundary){ + if(boundary>0) boundary+=offset; + while(true){ + int more; + if(boundary>0 && offset>=boundary)return -1; + more=oy.pageseek(page); + if(more<0){offset-=more;} + else{ + if(more==0){ + if(boundary==0)return -1; + if(get_data()<=0)return -1; + } + else{ + int ret=(int)offset; //!!! + offset+=more; + return ret; + } + } + } + } + + private int get_prev_page(Page page){ + int begin=(int)offset; //!!! + int ret; + int offst=-1; + while(offst==-1){ + begin-=CHUNKSIZE; + seek_helper(begin); + while(offset=0)next=ret; + } + else{ + searched=ret+page.header_len+page.body_len; + } + } + seek_helper(next); + ret=get_next_page(page, -1); + + if(searched>=end || ret==-1){ + links=m+1; + offsets=new long[m+2]; + offsets[m+1]=searched; + } + else{ + bisect_forward_serialno(next, (int)offset, end, page.serialno(), m+1); + } + offsets[m]=begin; + } + + // uses the local ogg_stream storage in vf; this is important for + // non-streaming input sources + int fetch_headers(Info vi, Comment vc, int[] serialno){ + //System.err.println("fetch_headers"); + Page og=new Page(); + Packet op=new Packet(); + int ret; + + ret=get_next_page(og, CHUNKSIZE); + if(ret==-1){ + System.err.println("Did not find initial header for bitstream."); + return -1; + } + + if(serialno!=null)serialno[0]=og.serialno(); + + os.init(og.serialno()); + + // extract the initial header from the first page and verify that the + // Ogg bitstream is in fact Vorbis data + + vi.init(); + vc.init(); + + int i=0; + while(i<3){ + os.pagein(og); + while(i<3){ + int result=os.packetout(op); + if(result==0)break; + if(result==-1){ + System.err.println("Corrupt header in logical bitstream."); + //goto bail_header; + vi.clear(); + vc.clear(); + os.clear(); + return -1; + } + if(vi.synthesis_headerin(vc, op)!=0){ + System.err.println("Illegal header in logical bitstream."); + //goto bail_header; + vi.clear(); + vc.clear(); + os.clear(); + return -1; + } + i++; + } + if(i<3) + if(get_next_page(og, 1)<0){ + System.err.println("Missing header in logical bitstream."); + //goto bail_header; + vi.clear(); + vc.clear(); + os.clear(); + return -1; + } + } + return 0; + +// bail_header: +// vorbis_info_clear(vi); +// vorbis_comment_clear(vc); +// ogg_stream_clear(&vf->os); +// return -1; + } + + // last step of the OggVorbis_File initialization; get all the + // vorbis_info structs and PCM positions. Only called by the seekable + // initialization (local stream storage is hacked slightly; pay + // attention to how that's done) + void prefetch_all_headers(Info first_i,Comment first_c, int dataoffset){ + Page og=new Page(); + int ret; + + vi=new Info[links]; + vc=new Comment[links]; + dataoffsets=new long[links]; + pcmlengths=new long[links]; + serialnos=new int[links]; + + for(int i=0;ivi+i,first_i,sizeof(vorbis_info)); + vc[i]=first_c; + //memcpy(vf->vc+i,first_c,sizeof(vorbis_comment)); + dataoffsets[i]=dataoffset; + } + else{ + // seek to the location of the initial header + seek_helper((int)offsets[i]); //!!! + if(fetch_headers(vi[i], vc[i], null)==-1){ + System.err.println("Error opening logical bitstream #"+(i+1)+"\n"); + dataoffsets[i]=-1; + } + else{ + dataoffsets[i]=offset; + os.clear(); + } + } + + // get the serial number and PCM length of this link. To do this, + // get the last page of the stream + { + int end=(int)offsets[i+1]; //!!! + seek_helper(end); + + while(true){ + ret=get_prev_page(og); + if(ret==-1){ + // this should not be possible + System.err.println("Could not find last page of logical "+ + "bitstream #"+(i)+"\n"); + vi[i].clear(); + vc[i].clear(); + break; + } + if(og.granulepos()!=-1){ + serialnos[i]=og.serialno(); + pcmlengths[i]=og.granulepos(); + break; + } + } + } + } + } + + int make_decode_ready(){ + if(decode_ready)System.exit(1); + vd.synthesis_init(vi[0]); + vb.init(vd); + decode_ready=true; + return(0); + } + + int open_seekable(){ + Info initial_i=new Info(); + Comment initial_c=new Comment(); + int serialno,end; + int ret; + int dataoffset; + Page og=new Page(); +System.out.println("open_seekable"); + // is this even vorbis...? + int[] foo=new int[1]; + ret=fetch_headers(initial_i, initial_c, foo); + serialno=foo[0]; + dataoffset=(int)offset; //!! + os.clear(); + if(ret==-1)return(-1); + + // we can seek, so set out learning all about this file + seekable=true; + //(callbacks.seek_func)(datasource, 0, SEEK_END); + fseek64_wrap(datasource, (int)offset, SEEK_SET); + //offset=end=(callbacks.tell_func)(datasource); + end=(int)offset; + + // We get the offset for the last page of the physical bitstream. + // Most OggVorbis files will contain a single logical bitstream + end=get_prev_page(og); + + // moer than one logical bitstream? + if(og.serialno()!=serialno){ + // Chained bitstream. Bisect-search each logical bitstream + // section. Do so based on serial number only + bisect_forward_serialno(0,0,end+1,serialno,0); + } + else{ + // Only one logical bitstream + bisect_forward_serialno(0,end,end+1,serialno,0); + } + prefetch_all_headers(initial_i, initial_c, dataoffset); + +System.out.println("?"); + return(raw_seek(0)); + } + + int open_nonseekable(){ + //System.err.println("open_nonseekable"); + // we cannot seek. Set up a 'single' (current) logical bitstream entry + links=1; + vi=new Info[links]; vi[0]=new Info(); // ?? + vc=new Comment[links]; vc[0]=new Comment(); // ?? bug? + + // Try to fetch the headers, maintaining all the storage + int[]foo=new int[1]; + if(fetch_headers(vi[0], vc[0], foo)==-1)return(-1); + current_serialno=foo[0]; + make_decode_ready(); + return 0; + } + + // clear out the current logical bitstream decoder + void decode_clear(){ + os.clear(); + vd.clear(); + vb.clear(); + decode_ready=false; + bittrack=0.f; + samptrack=0.f; + } + + // fetch and process a packet. Handles the case where we're at a + // bitstream boundary and dumps the decoding machine. If the decoding + // machine is unloaded, it loads it. It also keeps pcm_offset up to + // date (seek and read both use this. seek uses a special hack with + // readp). + // + // return: -1) hole in the data (lost packet) + // 0) need more date (only if readp==0)/eof + // 1) got a packet + + int process_packet(int readp){ +System.out.println("porcess_packet:"+ readp+" , decode_ready="+decode_ready); + Page og=new Page(); + + // handle one packet. Try to fetch it from current stream state + // extract packets from page + while(true){ + // process a packet if we can. If the machine isn't loaded, + // neither is a page + if(decode_ready){ + Packet op=new Packet(); + int result=os.packetout(op); + long granulepos; + // if(result==-1)return(-1); // hole in the data. For now, swallow + // and go. We'll need to add a real + // error code in a bit. + if(result>0){ + // got a packet. process it + granulepos=op.granulepos; + if(vb.synthesis(op)==0){ // lazy check for lazy + // header handling. The + // header packets aren't + // audio, so if/when we + // submit them, + // vorbis_synthesis will + // reject them + // suck in the synthesis data and track bitrate + { + int oldsamples=vd.synthesis_pcmout(null, null); + vd.synthesis_blockin(vb); + samptrack+=vd.synthesis_pcmout(null, null)-oldsamples; + bittrack+=op.bytes*8; + } + + // update the pcm offset. + if(granulepos!=-1 && op.e_o_s==0){ + int link=(seekable?current_link:0); + int samples; + // this packet has a pcm_offset on it (the last packet + // completed on a page carries the offset) After processing + // (above), we know the pcm position of the *last* sample + // ready to be returned. Find the offset of the *first* + // + // As an aside, this trick is inaccurate if we begin + // reading anew right at the last page; the end-of-stream + // granulepos declares the last frame in the stream, and the + // last packet of the last page may be a partial frame. + // So, we need a previous granulepos from an in-sequence page + // to have a reference point. Thus the !op.e_o_s clause above + + samples=vd.synthesis_pcmout(null, null); + granulepos-=samples; + for(int i=0;icallbacks.close_func)(vf->datasource); + //memset(vf,0,sizeof(OggVorbis_File)); + return(0); + } + + static int fseek64_wrap(InputStream fis, + //int64_t off, + int off, + int whence){ + + if(!fis.markSupported()){ return -1; } + try{ + try{if(whence==0){ fis.reset(); }} + catch(Exception ee){System.out.println(ee);} + fis.skip(off); + } + catch(Exception e){ System.out.println(e); + //return -1; + } + return 0; + } + + // inspects the OggVorbis file and finds/documents all the logical + // bitstreams contained in it. Tries to be tolerant of logical + // bitstream sections that are truncated/woogie. + // + // return: -1) error + // 0) OK + + int open(InputStream is, byte[] initial, int ibytes){ + return open_callbacks(is, initial, ibytes//, callbacks + ); + } + + int open_callbacks(InputStream is, byte[] initial, + int ibytes//, callbacks callbacks + ){ +// int offset=callbacks.seek_func(f,0,SEEK_CUR); + int _offset=fseek64_wrap(is, (int)offset, SEEK_SET); + int ret; + // memset(vf,0,sizeof(OggVorbis_File)); + datasource=is; + //callbacks = _callbacks; + + // init the framing state + oy.init(); + + // perhaps some data was previously read into a buffer for testing + // against other stream types. Allow initialization from this + // previously read data (as we may be reading from a non-seekable + // stream) + if(initial!=null){ + int index=oy.buffer(ibytes); + System.arraycopy(initial, 0, oy.data, index, ibytes); + oy.wrote(ibytes); + } + +System.out.println("open_callbacks="+_offset); + // can we seek? Stevens suggests the seek test was portable + if(_offset!=-1){ ret=open_seekable(); } + else{ ret=open_nonseekable(); } + +System.out.println("ret="+ret); + + if(ret!=0){ + datasource=null; + clear(); + } + + return(ret); + } + + // How many logical bitstreams in this physical bitstream? + public int streams(){ + return links; + } + + // Is the FILE * associated with vf seekable? + public boolean seekable(){ + return seekable; + } + + // returns the bitrate for a given logical bitstream or the entire + // physical bitstream. If the file is open for random access, it will + // find the *actual* average bitrate. If the file is streaming, it + // returns the nominal bitrate (if set) else the average of the + // upper/lower bounds (if set) else -1 (unset). + // + // If you want the actual bitrate field settings, get them from the + // vorbis_info structs + + public int bitrate(int i){ + if(i>=links)return(-1); + if(!seekable && i!=0)return(bitrate(0)); + if(i<0){ + long bits=0; + for(int j=0;j0){ + return vi[i].bitrate_nominal; + } + else{ + if(vi[i].bitrate_upper>0){ + if(vi[i].bitrate_lower>0){ + return (vi[i].bitrate_upper+vi[i].bitrate_lower)/2; + }else{ + return vi[i].bitrate_upper; + } + } + return(-1); + } + } + } + } + + // returns the actual bitrate since last call. returns -1 if no + // additional data to offer since last call (or at beginning of stream) + public int bitrate_instant(){ + int _link=(seekable?current_link:0); + if(samptrack==0)return(-1); + int ret=(int)(bittrack/samptrack*vi[_link].rate+.5); + bittrack=0.f; + samptrack=0.f; + return(ret); + } + + public int serialnumber(int i){ + if(i>=links)return(-1); + if(!seekable && i>=0)return(serialnumber(-1)); + if(i<0){ + return(current_serialno); + } + else{ + return(serialnos[i]); + } + } + + // returns: total raw (compressed) length of content if i==-1 + // raw (compressed) length of that logical bitstream for i==0 to n + // -1 if the stream is not seekable (we can't know the length) + + public long raw_total(int i){ +System.out.println("raw_total: "+seekable); + if(!seekable || i>=links)return(-1); + if(i<0){ + long acc=0; // bug? + for(int j=0;j=links)return(-1); + if(i<0){ + long acc=0; + for(int j=0;j=links)return(-1); + if(i<0){ + float acc=0; + for(int j=0;joffsets[links]){ + //goto seek_error; + pcm_offset=-1; + decode_clear(); + return -1; + } +System.out.println("#1"); + // clear out decoding machine state + pcm_offset=-1; +System.out.println("#2"); + decode_clear(); +System.out.println("#3"); + // seek + seek_helper(pos); + + // we need to make sure the pcm_offset is set. We use the + // _fetch_packet helper to process one packet with readp set, then + // call it until it returns '0' with readp not set (the last packet + // from a page has the 'granulepos' field set, and that's how the + // helper updates the offset +System.out.println("#4"); + switch(process_packet(1)){ + case 0: +System.out.println("?0"); + // oh, eof. There are no packets remaining. Set the pcm offset to + // the end of file + pcm_offset=pcm_total(-1); + return(0); + case -1: +System.out.println("?-1"); + // error! missing data or invalid bitstream structure + //goto seek_error; + pcm_offset=-1; + decode_clear(); + return -1; + default: +System.out.println("?break"); + // all OK + break; + } +System.out.println("pcm_offset="+pcm_offset); + while(true){ + switch(process_packet(0)){ + case 0: + // the offset is set. If it's a bogus bitstream with no offset + // information, it's not but that's not our fault. We still run + // gracefully, we're just missing the offset + return(0); + case -1: + // error! missing data or invalid bitstream structure + //goto seek_error; + pcm_offset=-1; + decode_clear(); + return -1; + default: + // continue processing packets + break; + } + } + + // seek_error: + // dump the machine so we're in a known state + //pcm_offset=-1; + //decode_clear(); + //return -1; + } + + // seek to a sample offset relative to the decompressed pcm stream + // returns zero on success, nonzero on failure + + public int pcm_seek(long pos){ + int link=-1; + long total=pcm_total(-1); + + if(!seekable)return(-1); // don't dump machine if we can't seek + if(pos<0 || pos>total){ + //goto seek_error; + pcm_offset=-1; + decode_clear(); + return -1; + } + + // which bitstream section does this pcm offset occur in? + for(link=links-1;link>=0;link--){ + total-=pcmlengths[link]; + if(pos>=total)break; + } + + // search within the logical bitstream for the page with the highest + // pcm_pos preceeding (or equal to) pos. There is a danger here; + // missing pages or incorrect frame number information in the + // bitstream could make our task impossible. Account for that (it + // would be an error condition) + { + long target=pos-total; + int end=(int)offsets[link+1]; + int begin=(int)offsets[link]; + int best=begin; + + Page og=new Page(); + while(begin=pos){ + //goto seek_error; + pcm_offset=-1; + decode_clear(); + return -1; + } + if(pos>pcm_total(-1)){ + //goto seek_error; + pcm_offset=-1; + decode_clear(); + return -1; + } + + // discard samples until we reach the desired position. Crossing a + // logical bitstream boundary with abandon is OK. + while(pcm_offsettarget)samples=target; + vd.synthesis_read(samples); + pcm_offset+=samples; + + if(samplestime_total){ + //goto seek_error; + pcm_offset=-1; + decode_clear(); + return -1; + } + + // which bitstream section does this time offset occur in? + for(link=links-1;link>=0;link--){ + pcm_total-=pcmlengths[link]; + time_total-=time_total(link); + if(seconds>=time_total)break; + } + + // enough information to convert time offset to pcm offset + { + long target=(long)(pcm_total+(seconds-time_total)*vi[link].rate); + return(pcm_seek(target)); + } + + //seek_error: + // dump machine so we're in a known state + //pcm_offset=-1; + //decode_clear(); + //return -1; + } + + // tell the current stream offset cursor. Note that seek followed by + // tell will likely not give the set offset due to caching + public long raw_tell(){ + return(offset); + } + + // return PCM offset (sample) of next PCM sample to be read + public long pcm_tell(){ + return(pcm_offset); + } + + // return time offset (seconds) of next PCM sample to be read + public float time_tell(){ + // translate time to PCM position and call pcm_seek + + int link=-1; + long pcm_total=0; + float time_total=0.f; + + if(seekable){ + pcm_total=pcm_total(-1); + time_total=time_total(-1); + + // which bitstream section does this time offset occur in? + for(link=links-1;link>=0;link--){ + pcm_total-=pcmlengths[link]; + time_total-=time_total(link); + if(pcm_offset>=pcm_total)break; + } + } + + return((float)time_total+(float)(pcm_offset-pcm_total)/vi[link].rate); + } + + // link: -1) return the vorbis_info struct for the bitstream section + // currently being decoded + // 0-n) to request information for a specific bitstream section + // + // In the case of a non-seekable bitstream, any call returns the + // current bitstream. NULL in the case that the machine is not + // initialized + + public Info info(int link){ + if(seekable){ + if(link<0){ + if(decode_ready){ + return vi[current_link]; + } + else{ + return null; + } + } + else{ + if(link>=links){ + return null; + } + else{ + return vi[link]; + } + } + } + else{ + if(decode_ready){ + return vi[0]; + } + else{ + return null; + } + } + } + + public Comment comment(int link){ + if(seekable){ + if(link<0){ + if(decode_ready){ return vc[current_link]; } + else{ return null; } + } + else{ + if(link>=links){ return null;} + else{ return vc[link]; } + } + } + else{ + if(decode_ready){ return vc[0]; } + else{ return null; } + } + } + + int host_is_big_endian() { + return 1; +// short pattern = 0xbabe; +// unsigned char *bytewise = (unsigned char *)&pattern; +// if (bytewise[0] == 0xba) return 1; +// assert(bytewise[0] == 0xbe); +// return 0; + } + + // up to this point, everything could more or less hide the multiple + // logical bitstream nature of chaining from the toplevel application + // if the toplevel application didn't particularly care. However, at + // the point that we actually read audio back, the multiple-section + // nature must surface: Multiple bitstream sections do not necessarily + // have to have the same number of channels or sampling rate. + // + // read returns the sequential logical bitstream number currently + // being decoded along with the PCM data in order that the toplevel + // application can take action on channel/sample rate changes. This + // number will be incremented even for streamed (non-seekable) streams + // (for seekable streams, it represents the actual logical bitstream + // index within the physical bitstream. Note that the accessor + // functions above are aware of this dichotomy). + // + // input values: buffer) a buffer to hold packed PCM data for return + // length) the byte length requested to be placed into buffer + // bigendianp) should the data be packed LSB first (0) or + // MSB first (1) + // word) word size for output. currently 1 (byte) or + // 2 (16 bit short) + // + // return values: -1) error/hole in data + // 0) EOF + // n) number of bytes of PCM actually returned. The + // below works on a packet-by-packet basis, so the + // return length is not related to the 'length' passed + // in, just guaranteed to fit. + // + // *section) set to the logical bitstream number + + int read(byte[] buffer,int length, + int bigendianp, int word, int sgned, int[] bitstream){ + int host_endian = host_is_big_endian(); + int index=0; + + while(true){ + if(decode_ready){ + float[][] pcm; + float[][][] _pcm=new float[1][][]; + int[] _index=new int[info(-1).channels]; + int samples=vd.synthesis_pcmout(_pcm, _index); + pcm=_pcm[0]; + if(samples!=0){ + // yay! proceed to pack data into the byte buffer + int channels=info(-1).channels; + int bytespersample=word * channels; + if(samples>length/bytespersample)samples=length/bytespersample; + + // a tight loop to pack each size + { + int val; + if(word==1){ + int off=(sgned!=0?0:128); + for(int j=0;j127)val=127; + else if(val<-128)val=-128; + buffer[index++]=(byte)(val+off); + } + } + } + else{ + int off=(sgned!=0?0:32768); + + if(host_endian==bigendianp){ + if(sgned!=0){ + for(int i=0;i32767)val=32767; + else if(val<-32768)val=-32768; + buffer[dest]=(byte)(val>>>8); + buffer[dest+1]=(byte)(val); + dest+=channels*2; + } + } + } + else{ + for(int i=0;i32767)val=32767; + else if(val<-32768)val=-32768; + buffer[dest]=(byte)((val+off)>>>8); + buffer[dest+1]=(byte)(val+off); + dest+=channels*2; + } + } + } + } + else if(bigendianp!=0){ + for(int j=0;j32767)val=32767; + else if(val<-32768)val=-32768; + val+=off; + buffer[index++]=(byte)(val>>>8); + buffer[index++]=(byte)val; + } + } + } + else{ + //int val; + for(int j=0;j32767)val=32767; + else if(val<-32768)val=-32768; + val+=off; + buffer[index++]=(byte)val; + buffer[index++]=(byte)(val>>>8); + } + } + } + } + } + + vd.synthesis_read(samples); + pcm_offset+=samples; + if(bitstream!=null)bitstream[0]=current_link; + return(samples*bytespersample); + } + } + + // suck in another packet + switch(process_packet(1)){ + case 0: + return(0); + case -1: + return -1; + default: + break; + } + } + } + + public int getLinks(){return links;} + public Info[] getInfo(){return vi;} + public Comment[] getComment(){return vc;} + + public static void main(String[] arg){ + try{ + VorbisFile foo=new VorbisFile(arg[0]); + int links=foo.getLinks(); + System.out.println("links="+links); + Comment[] comment=foo.getComment(); + Info[] info=foo.getInfo(); + for(int i=0; i 'Schnell hinzufügen', + 'Server settings' => 'Server Einstellungen', + 'Add playlists or audio-files' => 'Füge Playlisten oder Audiodateien hinzu', + 'Add playlist' => 'Füge Playlist hinzu', + 'Save current playlist' => 'Speichere aktive Playlist', + 'Save playlist' => 'Speichere Playlist', + 'Search current playlist' => 'Suche in aktiver Playlist', + 'Search playlist' => 'Suche in Playlist', + 'Configure' => 'Konfigurieren', + 'Start streaming to browser' => 'Starte streaming zum Browser', + 'Streaming' => 'Streaming', + 'Get more information about this song/album' => 'Erhalte mehr Informationen über dieses Lied/Album', + 'Song Info' => 'Lied-Infos', + 'Crop to selection' => 'Ausgewählte ausschneiden', + 'Remove selection' => 'Ausgewählte entfernen', + 'Open directory' => 'Verzeichnis öffnen', + 'Lyrics' => 'Lyrik', + 'Album description' => 'Beschreibung', + 'Album review' => 'Bewertung', + 'Close' => 'Schließen', + + /* configure.php */ + 'Pitchfork configuration' => 'Pitchfork Einstellungen', + 'Pitchfork MPD Client Configuration' => 'Pitchfork MPD-Klient Konfiguration', + 'Configure settings' => 'Einstellungen festlegen', + 'Connection-settings' => 'Verbindungseinstellungen', + 'Where can I find your MPD-server?' => 'Wo kann ich den MPD-Server erreichen?', + 'Hostname:' => 'Host:', + 'Port:' => 'Port:', + 'Password:' => 'Passwort:', + 'User interface' => 'Benutzeroberfläche', + 'Some other settings!' => 'Sonstige Einstellungen...', + 'Update time:' => 'Aktualisierungsintervall:', + 'How often we should request updates from the server' => 'Wie oft sollen die Aktualisierungen vom Server geholt werden?', + 'Login password (optional):' => 'Login Passwort (optional):', + 'If you want to require a password to see these pages you may specify it here' => + 'Wenn Sie diese Seite nur durch Eingabe eines Passworts sehen wollen, so geben Sie dies hier ein.', + 'Theme:' => 'Thema:', + 'Language:' => 'Sprache:', + 'Include stop button:' => 'Zeige Stopptaste:', + 'Pagination:' => 'Nummerierung:', + 'Maximum number of entries pr. page. Set to 0 to disable.' => 'Maximale Anzahl der Einträge auf einer Seite. Auf 0 stellen um dies auszustellen.', + 'Show these fields in the playlist:' => 'Diese Felder in der Playlist anzeigen:', + 'Position' => 'Position', + 'Show nonstandard' => 'Zeige Felder, die nicht Standard sind', + 'Configuration for retrieving metadata. This requires that the machine pitchfork is running on can access the internet.' => + 'Einstellungen zum empfangen der Metadaten. Dies benötigt eine Internetverbindung auf dem Rechner, wo Pitchfork eingesetzt wird.', + 'Disable metadata:' => 'Metadaten nicht benutzen:', + 'Shoutcast integration' => 'Shoutcast Integration', + 'Optionally specify the URL to the shout stream provided by mpd to enable integration with pitchfork.' => + 'Spezifiziere hier optional die URL zu dem Shout-Stream des MPD, um eine Integration in Pitchfork zu ermöglichen.', + 'Pitchfork info' => 'Pitchfork Informationen', + 'Release version:' => 'Version:', + 'Release date:' => 'Datum:', + 'Connect to mpd:' => 'Verbindung zum mpd:', + 'Yes' => 'Ja', + 'No' => 'Nein', + 'MPD commands:' => 'MPD Kommandos:', + 'Metadata directory:' => 'Metadaten Verzeichnis:', + 'Functions:' => 'Funktionen:', + 'PHP memory limit:' => 'PHP Speicherlimit:' + ); +?> diff --git a/lang/en.js b/lang/en.js new file mode 100644 index 0000000..c227ecd --- /dev/null +++ b/lang/en.js @@ -0,0 +1,112 @@ +/* these files need to be written in utf-8 */ + +var LANG = new Object(); +// Messages +LANG.VOLUME ='Volume'; +LANG.BITRATE ='Bitrate: '; +LANG.POSITION ='Position: '; +LANG.CROP ='Crop'; +LANG.CROP_SELECTION ="Crop to selection"; +LANG.CLEAR_PLAYLIST ="Clear playlist"; + +LANG.PLAY ="Play"; +LANG.STOP ="Stop"; + + +LANG.WAIT_LOADING ="Loading.."; +LANG.WAIT_UPDATING_DB ="Updating database.." +LANG.WAIT_UPDATING_PL ="Updating playlist, please wait.."; +LANG.WAIT_REMOVING ="Removing.."; +LANG.WAIT_ADDING ="Adding.."; +LANG.WAIT_ADDING_PL ="Adding playlist.."; +LANG.WAIT_SEARCHING = "Searching.."; + +LANG.UPDATE_DB ="Update DB"; +LANG.ARTIST ="Artist"; +LANG.TITLE ="Title"; +LANG.ALBUM ="Album"; +LANG.GENRE ="Genre"; +LANG.FILENAME ="Filename"; +LANG.FILESYSTEM ="Filesystem"; +LANG.LYRICS ="Lyrics"; +LANG.SEARCH ="Search"; +LANG.ADD ="Add"; +LANG.EDIT ="Edit"; +LANG.DELETE ="Delete"; +LANG.CONFIRM_REMOVE ="Really remove"; +LANG.YES ="Yes"; +LANG.NO ="No"; +LANG.BY_URL ="By URL"; +LANG.FROM_FILE ="From file"; +LANG.TEXT ="Text"; +LANG.OUTPUTS ="Outputs"; +LANG.CLOSE ="Close"; +LANG.SAVE ="Save"; +LANG.REFETCH ="Refetch"; +LANG.HIDE ="Hide"; +LANG.AUTOPLAY ="Autoplay"; +LANG.NO_AUTOPLAY ="No autoplay"; +LANG.STREAMING ="Streaming"; + +LANG.ANYTAG ="Any tag"; +LANG.COMPOSER ="Composer"; +LANG.PERFORMER ="Performer"; +LANG.DATE ="Date"; + + +LANG.PL_SAVE_AS ="Save playlist as: "; +LANG.PL_SAVING ="Saving playlist"; + +LANG.REPEAT ="Repeat"; +LANG.RANDOM ="Random"; +LANG.XFADE ="X-Fade: "; + +LANG.QUICK_ADD ="Quick add"; + +LANG.ALBUM_REVIEW ="Album review"; +LANG.ALBUM_DESC ="Album description"; +// e.g. album review for some album by some artist, the spaces are important +LANG.ALBUM_AA_NAME =" for %s by %s"; +LANG.ALBUM_AMAZON ="Album on amazon.com (new window)"; + +LANG.JUMP_CURRENT = "Jump to currently playing song [Space]"; +LANG.PAGINATION_FOLLOW = "Following currently playing song"; +LANG.PAGINATION_NOFOLLOW= "Not following currently playing song"; + +LANG.LYRICWIKI_LYRIC = "%s lyric at lyricwiki.org"; // add/edit lyric at .. + +// ERRORS +LANG.E_CONNECT ="Unable to connect to MPD server"; +LANG.E_INVALID_RESPONSE ="Server returned invalid response"; +LANG.E_INVALID_RESULT ="Invalid result from server"; +LANG.E_NO_RESPONSE ="Unable to get response from server"; +LANG.E_CONNECT ="Unable to connect to mpd"; +LANG.E_INIT ="Init failed " +LANG.E_INIT_PL ="Failed to initialize playlist"; +LANG.E_PL_MOVE ="Moving in playlist failed"; +LANG.E_REMOVE ="Could not remove songs"; +LANG.E_FAILED_ADD ="Failed to add"; +LANG.E_FAILED_ADD_PL ="Failed to add playlist"; +LANG.E_FAILED_SAVE_PL ="Failed to save playlist"; +LANG.E_FAILED_LOAD_DIR ="Failed to load directory list"; +LANG.E_NOTHING_FOUND ="Nothing found.."; +LANG.E_NO_OUTPUTS ="No outputs found"; +LANG.E_NOT_FOUND ="Did not find any %ss."; // We didn't find any of these +LANG.E_MISSING_CACHE ="Missing cache dir"; +LANG.E_MISSING_AA_NAME ="Missing artist or album name."; +LANG.E_MISSING_AS_NAME ="Missing artist or song-title."; +LANG.E_LYRICS_NOT_FOUND ="Lyrics not found"; +LANG.E_MISSING_LYRICS ="We seem to be missing the lyrics.."; // this should be something better +LANG.E_LYRICS_FAILURE ="Retrieval of lyrics failed"; +LANG.E_COMM_PROBLEM ="Communication problem"; +LANG.E_GET_INFO ="Unable to get information"; + +LANG.RECOMMEND_RECOMMENDATIONS = "Recommendations "; +LANG.RECOMMEND_EMPTY_PLAYLIST ="To fetch recommendations based on your playlist you need to have something in your playlist"; +LANG.RECOMMEND_ADDTOPLAYLIST ="Add to playlist"; +LANG.RECOMMEND_SIMILAR ="Similar artists from your library"; +LANG.RECOMMEND_ARTISTS ="Recommended artists"; + + +/* Don't need translation, but needs to be here: */ +LANG.NT_AMAZON = "[Amazon.com]"; diff --git a/lang/en.php b/lang/en.php new file mode 100644 index 0000000..0f5d1a7 --- /dev/null +++ b/lang/en.php @@ -0,0 +1,67 @@ + 'Quick add', + 'Server settings' => 'Server settings', + 'Add playlists or audio-files' => 'Add playlists or audio-files', + 'Add playlist' => 'Add playlist', + 'Save current playlist' => 'Save current playlist', + 'Save playlist' => 'Save playlist', + 'Search current playlist' => 'Search current playlist', + 'Search playlist' => 'Search playlist', + 'Configure' => 'Configure', + 'Start streaming to browser' => 'Start streaming to browser', + 'Streaming' => 'Streaming', + 'Get more information about this song/album' => 'Get more information about this song/album', + 'Song Info' => 'Song Info', + 'Crop to selection' => 'Crop to selection', + 'Remove selection' => 'Remove selection', + 'Open directory' => 'Open directory', + 'Lyrics' => 'Lyrics', + 'Album description' => 'Album description', + 'Album review' => 'Album review', + 'Close' => 'Close', + + /* configure.php */ + 'Pitchfork MPD Client Configuration' => 'Pitchfork MPD Client Configuration', + 'Configure settings' => 'Configure settings', + 'Connection-settings' => 'Connection-settings', + 'Where can I find your MPD-server?' => 'Where can I find your MPD-server?', + 'Hostname:' => 'Hostname:', + 'Port:' => 'Port:', + 'Password:' => 'Password:', + 'User interface' => 'User interface', + 'Some other settings!' => 'Some other settings!', + 'Update time:' => 'Update time:', + 'How often we should request updates from the server' => 'How often we should request updates from the server', + 'Login password (optional):' => 'Login password (optional):', + 'If you want to require a password to see these pages you may specify it here' => + 'If you want to require a password to see these pages you may specify it here', + 'Theme:' => 'Theme:', + 'Language:' => 'Language:', + 'Include stop button:' => 'Include stop button:', + 'Pagination:' => 'Pagination:', + 'Maximum number of entries pr. page. Set to 0 to disable.' => 'Maximum number of entries pr. page. Set to 0 to disable.', + 'Show these fields in the playlist:' => 'Show these fields in the playlist:', + 'Position' => 'Position', + 'Show nonstandard' => 'Show nonstandard', + 'Configuration for retrieving metadata. This requires that the machine pitchfork is running on can access the internet.' => + 'Configuration for retrieving metadata. This requires that the machine pitchfork is running on can access the internet.', + 'Disable metadata:' => 'Disable metadata:', + 'Shoutcast integration' => 'Shoutcast integration', + 'Optionally specify the URL to the shout stream provided by mpd to enable integration with pitchfork.' => + 'Optionally specify the URL to the shout stream provided by mpd to enable integration with pitchfork.', + 'Pitchfork info' => 'Pitchfork info', + 'Release version:' => 'Release version:', + 'Release date:' => 'Release date:', + 'Connect to mpd:' => 'Connect to mpd:', + 'Yes' => 'Yes', + 'No' => 'No', + 'MPD commands:' => 'MPD commands:', + 'Metadata directory:' => 'Metadata directory:', + 'Functions:' => 'Functions:', + 'PHP memory limit:' => 'PHP memory limit:' + ); +?> diff --git a/lang/eu.js b/lang/eu.js new file mode 100644 index 0000000..1c644ef --- /dev/null +++ b/lang/eu.js @@ -0,0 +1,106 @@ +/* these files need to be written in utf-8 */ + +// Messages +LANG.VOLUME ='Bolumena'; +LANG.BITRATE ='Bit-tasa: '; +LANG.POSITION ='Posizioa: '; +LANG.CROP ='Eskilatu'; +LANG.CROP_SELECTION ="Aukeraketa bakarrik"; +LANG.CLEAR_PLAYLIST ="Garbitu zerrenda"; + +LANG.WAIT_LOADING ="Zabaltzen..."; +LANG.WAIT_UPDATING_DB ="Datubasea eguneratzen..." +LANG.WAIT_UPDATING_PL ="Zerrenda eguneratzen, ixaron mesedez..."; +LANG.WAIT_REMOVING ="Ezabatzen..."; +LANG.WAIT_ADDING ="Gehitzen..."; +LANG.WAIT_ADDING_PL ="Zerrenda gehitzen..."; +LANG.WAIT_SEARCHING ="Bilatzen..."; + +LANG.UPDATE_DB ="DB Eguneratu"; +LANG.ARTIST ="Artista"; +LANG.TITLE ="Titulua"; +LANG.ALBUM ="Albuma"; +LANG.GENRE ="Generoa"; +LANG.FILENAME ="Fitxategia"; +LANG.FILESYSTEM ="Filesystem"; +LANG.LYRICS ="Letrak"; +LANG.SEARCH ="Bilatu"; +LANG.ADD ="Gehitu"; +LANG.EDIT ="Editatu"; +LANG.DELETE ="Ezabatu"; +LANG.CONFIRM_REMOVE ="Erabat ezabatu"; +LANG.YES ="Bai"; +LANG.NO ="Ez"; +LANG.BY_URL ="URL bidez"; +LANG.FROM_FILE ="Fitxategitik"; +LANG.TEXT ="Textua"; +LANG.OUTPUTS ="Irteerak"; +LANG.CLOSE ="Itxi"; +LANG.SAVE ="Gorde"; +LANG.REFETCH ="Refetch"; +LANG.HIDE ="Ezkutatu"; +LANG.AUTOPLAY ="Autoplay"; +LANG.NO_AUTOPLAY ="No autoplay"; +LANG.STREAMING ="Streaming"; + +LANG.ANYTAG ="Edozein etiketa"; +LANG.COMPOSER ="Egilea"; +LANG.PERFORMER ="Eraldatzailea"; +LANG.DATE ="Data"; + + +LANG.PL_SAVE_AS ="Gorde zerrenda: "; +LANG.PL_SAVING ="Zerrenda gordetzen"; + +LANG.REPEAT ="Errepikatu"; +LANG.RANDOM ="Ausazkoa"; +LANG.XFADE ="X-Fade: "; + +LANG.QUICK_ADD ="Gehitu azkar"; + +LANG.ALBUM_REVIEW ="Albumaren errebisioa"; +LANG.ALBUM_DESC ="Albumaren deskribapena"; +// e.g. album review for some album by some artist, the spaces are important +LANG.ALBUM_AA_NAME =" for %s by %s"; +LANG.ALBUM_AMAZON ="Album on amazon.com (new window)"; + +LANG.JUMP_CURRENT = "Oraingo abestira salto egin [Space]"; +LANG.PAGINATION_FOLLOW = "Oraingo abestia jarraitu"; +LANG.PAGINATION_NOFOLLOW= "Oraingo abestia ez jarraitu"; + +LANG.LYRICWIKI_LYRIC = "%s lyric at lyricwiki.org"; // add/edit lyric at .. + +// ERRORS +LANG.E_CONNECT ="Ezin da MPD zerbitzarira konektatu"; +LANG.E_INVALID_RESPONSE ="Zerbitzariaren okerreko erantzuna"; +LANG.E_INVALID_RESULT ="Zerbitzariaren okerreko emaitza"; +LANG.E_NO_RESPONSE ="Zerbitzariak ez du erantzuten"; +LANG.E_CONNECT ="MPD-ra ezin izan da konektatu"; +LANG.E_INIT ="Hasieratzean hutsegitea" +LANG.E_INIT_PL ="Zerrenda hasieratzean hutsegitea"; +LANG.E_PL_MOVE ="Zerrendaren aldaketak huts egin du"; +LANG.E_REMOVE ="Ezin izan dira abestiak ezabatu"; +LANG.E_FAILED_ADD ="Errorea gehitzerakoan"; +LANG.E_FAILED_ADD_PL ="Errorea zerrenda gehitzerakoan"; +LANG.E_FAILED_SAVE_PL ="Errorea zerrenda gordetzerakoan"; +LANG.E_FAILED_LOAD_DIR ="Failed to load directory list"; +LANG.E_NOTHING_FOUND ="Ez da ezer aurkitu..."; +LANG.E_NO_OUTPUTS ="Irteerarik ez da aurkitu"; +LANG.E_NOT_FOUND ="Ez da aurkitu %ss."; // We didn't find any of these +LANG.E_MISSING_CACHE ="Cache direktorioa ez da aurkitu"; +LANG.E_MISSING_AA_NAME ="Artistaren edo albumaren izena ez da aurkitu"; +LANG.E_MISSING_AS_NAME ="Artistaren edo abestiaren izena ez da aurkitu"; +LANG.E_LYRICS_NOT_FOUND ="Letrak ez dira aurkitu"; +LANG.E_MISSING_LYRICS ="We seem to be missing the lyrics.."; // this should be something better +LANG.E_LYRICS_FAILURE ="Letrak jasotzean huts egin du"; +LANG.E_COMM_PROBLEM ="Komunikazio arazoa"; +LANG.E_GET_INFO ="Informaziorik ezin izan da jaso"; + +LANG.RECOMMEND_RECOMMENDATIONS ="Gomendioak "; +LANG.RECOMMEND_EMPTY_PLAYLIST ="Zerrendan abestiren bat behar duzu gomendioak jasotzeko"; +LANG.RECOMMEND_ADDTOPLAYLIST ="Gehitu uneko zerrendara"; +LANG.RECOMMEND_SIMILAR ="Zure liburutegitik antzeko artistak:"; +LANG.RECOMMEND_ARTISTS ="Gomendatutako artistak:"; + +/* Don't need translation, but needs to be here: */ +LANG.NT_AMAZON = "[Amazon.com]"; diff --git a/lang/eu.php b/lang/eu.php new file mode 100644 index 0000000..9a9b71f --- /dev/null +++ b/lang/eu.php @@ -0,0 +1,67 @@ + 'Gehitu azkar', + 'Server settings' => 'Zerbitzariaren aukerak', + 'Add playlists or audio-files' => 'Gehitu zerrenda edo fitxategia', + 'Add playlist' => 'Gehitu zerrenda', + 'Save current playlist' => 'Gorde zerrenda hau', + 'Save playlist' => 'Gorde zerrenda', + 'Search current playlist' => 'Bilatu zerrenda honetan', + 'Search playlist' => 'Bilatu zerrendan', + 'Configure' => 'Hobespenak', + 'Start streaming to browser' => 'Hasi nabigatzailera streaming egiten', + 'Streaming' => 'Streaming', + 'Get more information about this song/album' => 'Album/abesti honi buruz informazio gehiago', + 'Song Info' => 'Abestiari buruz', + 'Crop to selection' => 'Aukeraketa bakarrik', + 'Remove selection' => 'Ezabatu aukeraketa', + 'Open directory' => 'Zabaldu direktorioa', + 'Lyrics' => 'Letrak', + 'Album description' => 'Albumaren deskribapena', + 'Album review' => 'Albumaren errebisioa', + 'Close' => 'Itxi', + + /* configure.php */ + 'Pitchfork MPD Client Configuration' => 'Pitchfork MPD Bezeroaren Hobespenak', + 'Configure settings' => 'Aldatu hobespenak', + 'Connection-settings' => 'Konexioaren hobespenak', + 'Where can I find your MPD-server?' => 'Nun aurkitu dezaket zure MPD-zerbitzaria?', + 'Hostname:' => 'Helbidea:', + 'Port:' => 'Portua:', + 'Password:' => 'Pasahitza:', + 'User interface' => 'Itxura', + 'Some other settings!' => 'Beste hobespen batzuk!', + 'Update time:' => 'Eguneratze denbora:', + 'How often we should request updates from the server' => 'Zenbatean bein eguneratu behar da zerbitzaritik', + 'Login password (optional):' => 'Sartzeko pasahitza (aukerazkoa):', + 'If you want to require a password to see these pages you may specify it here' => + 'Orri hauek ikusi ahal izateko pasahitza eskatzea nahi baduzu', + 'Theme:' => 'Gaia:', + 'Language:' => 'Hizkuntza:', + 'Include stop button:' => 'Gehitu gelditzeko botoia:', + 'Pagination:' => 'Pajinazioa:', + 'Maximum number of entries pr. page. Set to 0 to disable.' => 'Orriko gehienezko fila kopurua. Jarri 0 desgaitzeko', + 'Show these fields in the playlist:' => 'Azaldu datu hauek zerrendan:', + 'Position' => 'Posizioa', + 'Show nonstandard' => 'Bestelakoak', + 'Configuration for retrieving metadata. This requires that the machine pitchfork is running on can access the internet.' => + 'Metadata lortzeko konfigurazioa. Pitchfork exekutatzen ari den makinak interneterako atzipena behar du', + 'Disable metadata:' => 'Desgaitu metadata:', + 'Shoutcast integration' => 'Shoutcast integrazioa', + 'Optionally specify the URL to the shout stream provided by mpd to enable integration with pitchfork.' => + 'Nahi baduzu ezarri shoutstream iturriaren URL helbidea', + 'Pitchfork info' => 'Pitchfork info', + 'Release version:' => 'Bertsioa:', + 'Release date:' => 'Eguneraketa data:', + 'Connect to mpd:' => 'MPDra konektatua:', + 'Yes' => 'Bai', + 'No' => 'Ez', + 'MPD commands:' => 'MPD komandoak:', + 'Metadata directory:' => 'Metadata direktorioa:', + 'Functions:' => 'Funtzioak:', + 'PHP memory limit:' => 'PHP memoria limitea:' + ); +?> diff --git a/lang/fr.js b/lang/fr.js new file mode 100644 index 0000000..c6f69fa --- /dev/null +++ b/lang/fr.js @@ -0,0 +1,101 @@ +/* these files need to be written in utf-8 */ + +// Messages +LANG.VOLUME ='Volume'; +LANG.BITRATE ='Débit: '; +LANG.POSITION ='Position: '; +LANG.CROP ='Conserver'; +LANG.CROP_SELECTION ="Ne conserver que la sélection"; +LANG.CLEAR_PLAYLIST ="Effacer la playlist"; + +LANG.WAIT_LOADING ="Chargement.."; +LANG.WAIT_UPDATING_DB ="MAJ de la base de données.." +LANG.WAIT_UPDATING_PL ="MAJ de la playlist, patientez SVP.."; +LANG.WAIT_REMOVING ="Suppression.."; +LANG.WAIT_ADDING ="Ajout.."; +LANG.WAIT_ADDING_PL ="Ajout de la playlist.."; +LANG.WAIT_SEARCHING = "Recherche.."; + +LANG.UPDATE_DB ="MAJ BD"; +LANG.ARTIST ="Artiste"; +LANG.TITLE ="Titre"; +LANG.ALBUM ="Album"; +LANG.GENRE ="Genre"; +LANG.FILENAME ="Fichier"; +LANG.FILESYSTEM ="Système de fichiers"; +LANG.LYRICS ="Paroles"; +LANG.SEARCH ="Recherche"; +LANG.ADD ="Ajout"; +LANG.EDIT ="Édition"; +LANG.DELETE ="Suppression"; +LANG.CONFIRM_REMOVE ="Confirmez la suppression"; +LANG.YES ="Oui"; +LANG.NO ="Non"; +LANG.BY_URL ="Par URL"; +LANG.FROM_FILE ="À partir d'un fichier"; +LANG.TEXT ="Texte"; +LANG.OUTPUTS ="Sortie"; +LANG.CLOSE ="Fermer"; +LANG.SAVE ="Enregistrer"; +LANG.REFETCH ="Récupérer à nouveau"; +LANG.HIDE ="Cacher"; +LANG.AUTOPLAY ="Lecture auto"; +LANG.NO_AUTOPLAY ="Pas de lecture auto"; +LANG.STREAMING ="Flux"; + +LANG.ANYTAG ="N'importe quelle étiquette"; +LANG.COMPOSER ="Compositeur"; +LANG.PERFORMER ="Interprète"; +LANG.DATE ="Date"; + + +LANG.PL_SAVE_AS ="Enregistrer la playlist sous: "; +LANG.PL_SAVING ="Enregistrement de la playlist"; + +LANG.REPEAT ="Répéter"; +LANG.RANDOM ="Aléatoire"; +LANG.XFADE ="Crossfade: "; + +LANG.QUICK_ADD ="Ajout rapide"; + +LANG.ALBUM_REVIEW ="Revue de l'album"; +LANG.ALBUM_DESC ="Description de l'album"; +// e.g. album review for some album by some artist, the spaces are important +LANG.ALBUM_AA_NAME =" %s par %s"; +LANG.ALBUM_AMAZON ="Album sur amazon.com (nouvelle fenêtre)"; + +LANG.JUMP_CURRENT = "Aller à la chanson en cours [Espace]"; +LANG.PAGINATION_FOLLOW = "Suivre la chanson en cours"; +LANG.PAGINATION_NOFOLLOW= "Ne pas suivre la chanson en cours"; + +LANG.LYRICWIKI_LYRIC = "paroles de %s sur lyricwiki.org"; // add/edit lyric at .. + +// ERRORS +LANG.E_CONNECT ="Impossible de se connecter au serveur MPD"; +LANG.E_INVALID_RESPONSE ="Le serveur a renvoyé une réponse non valide"; +LANG.E_INVALID_RESULT ="Résultat non valide du serveur"; +LANG.E_NO_RESPONSE ="Impossible d'avoir une réponse du serveur"; +LANG.E_CONNECT ="Impossible de se connecter à mpd"; +LANG.E_INIT ="Init a échoué " +LANG.E_INIT_PL ="L'initialisation de la playlist a échoué"; +LANG.E_PL_MOVE ="Le déplacement dans la playlist a échoué"; +LANG.E_REMOVE ="Les chansons n'ont pas pu être enlevées"; +LANG.E_FAILED_ADD ="L'ajout a échoué"; +LANG.E_FAILED_ADD_PL ="L'ajout de la playlist a échoué"; +LANG.E_FAILED_SAVE_PL ="L'enregistrement de la playlist a échoué"; +LANG.E_FAILED_LOAD_DIR ="Le chargement du répertoire a échoué"; +LANG.E_NOTHING_FOUND ="Aucun résultat.."; +LANG.E_NO_OUTPUTS ="Aucune sortie trouvée"; +LANG.E_NOT_FOUND ="Aucun %ss trouvé(e)."; // We didn't find any of these +LANG.E_MISSING_CACHE ="Cache manquant"; +LANG.E_MISSING_AA_NAME ="Artist ou album manquant."; +LANG.E_MISSING_AS_NAME ="Artist or titre manquant."; +LANG.E_LYRICS_NOT_FOUND ="Aucune parole trouvée"; +LANG.E_MISSING_LYRICS ="Les paroles semblent manquantes.."; // this should be something better +LANG.E_LYRICS_FAILURE ="L'extraction des paroles a échouée"; +LANG.E_COMM_PROBLEM ="Problème de communication"; +LANG.E_GET_INFO ="Récupération d'infos impossible"; + + +/* Don't need translation, but needs to be here: */ +LANG.NT_AMAZON = "[Amazon.com]"; diff --git a/lang/fr.php b/lang/fr.php new file mode 100644 index 0000000..9cb1aae --- /dev/null +++ b/lang/fr.php @@ -0,0 +1,67 @@ + 'Ajout rapide', + 'Server settings' => 'Configuration du serveur', + 'Add playlists or audio-files' => 'Ajout de playlists ou de fichiers', + 'Add playlist' => 'Ajout de playlists', + 'Save current playlist' => 'Enregistrer la playlist courante', + 'Save playlist' => 'Enregistrer la playlist', + 'Search current playlist' => 'Rechercher dans la playlist courante', + 'Search playlist' => 'Rechercher dans la playlist', + 'Configure' => 'Configurer', + 'Start streaming to browser' => 'Démarrer le flux vers le navigateur', + 'Streaming' => 'Flux', + 'Get more information about this song/album' => 'Obtenir plus d infos sur la chanson/l album', + 'Song Info' => 'Infos sur la chanson', + 'Crop to selection' => 'Ne conserver que la sélection', + 'Remove selection' => 'Retirer la sélection', + 'Open directory' => 'Ouvrir la base de données', + 'Lyrics' => 'Paroles', + 'Album description' => 'Description de l\'album', + 'Album review' => 'Revue de l\'album', + 'Close' => 'Fermer', + + /* configure.php */ + 'Pitchfork MPD Client Configuration' => 'Configuration du Client MPD Pitchfork', + 'Configure settings' => 'Réglages de configuration', + 'Connection-settings' => 'Réglages de connection', + 'Where can I find your MPD-server?' => 'Où se trouve le serveur MPD?', + 'Hostname:' => 'Hôte:', + 'Port:' => 'Port:', + 'Password:' => 'Mot de passe:', + 'User interface' => 'Interface utilisateur', + 'Some other settings!' => 'D\'autres réglages!', + 'Update time:' => 'Horaire de MAJ:', + 'How often we should request updates from the server' => 'Avec quelle fréquence faut-il mettre à jour le serveur', + 'Login password (optional):' => 'Mot de passe de connection (optionnel):', + 'If you want to require a password to see these pages you may specify it here' => + 'Si vous souhaiter demander un mot de passe vous pouvez le spécifier ici', + 'Theme:' => 'Thème:', + 'Language:' => 'Langue:', + 'Include stop button:' => 'Inclure le bouton stop:', + 'Pagination:' => 'Pagination:', + 'Maximum number of entries pr. page. Set to 0 to disable.' => 'Nombre maximum d\'entrées par page. Régler à 0 pour désactiver.', + 'Show these fields in the playlist:' => 'Afficher ces champs dans la playlist:', + 'Position' => 'Position', + 'Show nonstandard' => 'Afficher les non-standard', + 'Configuration for retrieving metadata. This requires that the machine pitchfork is running on can access the internet.' => + 'Configuration pour l\'extraction des metadatas. Ceci nécessite que la machine où pitchfork est installé ait accès à internet.', + 'Disable metadata:' => 'Désactiver les metadatas:', + 'Shoutcast integration' => 'Integration de Shoutcast', + 'Optionally specify the URL to the shout stream provided by mpd to enable integration with pitchfork.' => + 'Spécifier une URL pour le flux shout fourni par mpd afin d\'activer l\'intégration avec pitchfork (optionnel).', + 'Pitchfork info' => 'Info Pitchfork', + 'Release version:' => 'Version:', + 'Release date:' => 'Date:', + 'Connect to mpd:' => 'Connection à mpd:', + 'Yes' => 'Oui', + 'No' => 'Non', + 'MPD commands:' => 'Commandes MPD:', + 'Metadata directory:' => 'Répertoire des metadatas:', + 'Functions:' => 'Fonctions:', + 'PHP memory limit:' => 'Limite mémoire de PHP:' + ); +?> diff --git a/lang/master.php b/lang/master.php new file mode 100644 index 0000000..8fe9d17 --- /dev/null +++ b/lang/master.php @@ -0,0 +1,37 @@ + '$res',\n"; + + } + +?> diff --git a/player/command.php b/player/command.php new file mode 100644 index 0000000..25b2575 --- /dev/null +++ b/player/command.php @@ -0,0 +1,832 @@ +find($search, true); + foreach($tmp as &$a) { + $pl->addSong($a['file']); + } + if(count($tmp)>0) + return true; + return false; + } + + function parsePlaylist($txt, $type = false) { + $txt = explode("\n", $txt); // trim will remove the \r + $res = array(); + if($type=="pls"||$type===false) { + foreach($txt as $t) { + $t = trim($t); + if(stripos($t, "file")!==false) { + $pos = spliti("^file[0-9]*=", $t); + if(count($pos)==2&&strlen($pos[1])>0) + $res[] = $pos[1]; + } + } + } + else if($type=="m3u" || ($type===false&&count($res)==0) ) { + foreach($txt as $t) { + $t = trim($t); + if(strpos($t, "#")!==false||strlen($t)==0) { + echo "skipping: $t\n"; + continue; + } + $res[] = $t; + } + } + return $res; + } + function handle_playlist_url($pl, $url, $get = false) { + if($get) { + $fp = @fopen($url, "r"); + if($fp) { + $type = substr($url, strlen($url)-3); // just get three last chars.. + $md = stream_get_meta_data($fp); + $md = $md['wrapper_data']; + foreach($md as $m) { + if(stripos($m, "content-type:")==0) { + if(stripos($m, "audio/x-scpls")) { + $typdde = "pls"; + } + else if(stripos($m, "audio/x-mpegurl") + || stripos($m, "audio/mpegurl")) { + $type = "m3u"; + } + } + } + $type = strtolower($type); + $data = stream_get_contents($fp); + $stuff = parsePlaylist($data, $type); + foreach($stuff as $s) { + $pl->addSong($s); + } + return true; + } + return false; + } + else { + $opts = array( + 'http'=>array( + 'method'=>"HEAD", + )); + $context = stream_context_create($opts); + $fp = @fopen($url, "r", false, $context); + $md = null; + + if(!$fp) { + $md = array(); // head did not work.... + } + else { + $md = stream_get_meta_data($fp); + $md = $md['wrapper_data']; + } + + $type = substr($url, strlen($url)-3); // just get three last chars.. + + /* these lists are probably incomplete, make a ticket if + you want something added */ + foreach($md as $m) { + if(stripos($m, "content-type:")==0) { + if(stripos($m, "audio/x-scpls")|| + stripos($m, "audio/x-mpegurl")|| + stripos($m, "audio/mpegurl")) { + return handle_playlist_url($pl, $url, true); + } + else if(stripos($m, "audio/mpeg")|| + stripos($m, "audio/mpeg3")|| + stripos($m, "audio/x-mpeg3")|| + stripos($m, "audio/mpeg2")|| + stripos($m, "audio/x-mpeg2")|| + stripos($m, "application/ogg")|| + stripos($m, "audio/x-ogg")|| + stripos($m, "audio/mp4")|| + stripos($m, "audio/x-mod")|| + stripos($m, "audio/mod")|| + stripos($m, "audio/basic")|| + stripos($m, "audio/x-basic")|| + stripos($m, "audio/wav")|| + stripos($m, "audio/x-wav")|| + stripos($m, "audio/flac")|| + stripos($m, "audio/x-flac") + ) { + $pl->addSong($url); + return true; + } + } + } + $type = strtolower($type); + $type4 = strtolower($url, strlen($url)-4); + if($type=="m3u"||$type=="pls") { + return handle_playlist_url($pl, $url, true); + } + else if($type=="ogg"||$type=="mp3"||$type=="mp2"||$type=="wav" + ||$type==".au"||$type=="m4a"||$type4=="flac"||$type4=="aiff") { + // ugh, just try to add it... + $pl->addSong($url); + return true; + } + } + return false; + } + + function array_to_json(&$arr) { + echo "("; + if(function_exists("json_encode")) { + echo json_encode($arr); + } else { + $json = new Services_JSON(); + $json->encode($arr); + } + echo ")"; + } + + /* for auto-play-start on empty */ + $playlist_empty = false; + $something_added = false; + + $json = null; + $pl = get_playlist(); + if(!$pl) { + $v = array("connection" => PF_FAILURE); + echo array_to_json($v); + exit(); + } + else if(isset($_GET['add'])||isset($_GET['ma'])||isset($_GET['searchadd'])) { + /* for automatic playback start */ + try { + $s = $pl->getStatus(); + if(isset($s['playlistlength'])&&intval($s['playlistlength'])==0) { + $playlist_empty = true; + } + } + catch (PEAR_Exception $e) { + $v = array("connection" => PF_FAILURE); + echo array_to_json($v); + exit(); + } + } + + if(isset($_GET['playlist'])) { + $act = $_GET['playlist']; + try { + if($act=="move"&&isset($_GET['from'])&&isset($_GET['to'])) { + // todo: sanity check + $response = null; + if($pl->moveSongId($_GET['from'], $_GET['to'])) + $response = array('result' => "ok"); + else $response = array ('result' => PF_FAILURE); + $json = $response; + } + else if($act=="info"&&isset($_POST['ids'])) { + $list = ids_to_list($_POST['ids']); + $ret = array(); + foreach($list as $id) { + $tmp = $pl->getPlaylistInfoId($id); + if(isset($tmp['file'])) + $ret[] = $tmp['file'][0]; + } + $json = array(); + unset($list); + $json['result'] = &$ret; + } + else { + $json = array("result" => PF_FAILURE); + } + } + catch(PEAR_Exception $e) { + $json = array ('result' => PF_FAILURE); + } + } + else if(isset($_GET['rangemove'])&&is_numeric(trim($_GET['rangemove']))&&isset($_GET['elems'])) { + $res = PF_FAILURE; + $dest = intval($_GET['rangemove']); + $pos_offset = 0; + try { + $list = selection_to_reverse_list($_GET['elems']); + foreach($list as &$pos) { + //echo $pos-$pos_offset . "=>" .$dest."\n"; + + /* this means we've moved above the place where changing the position will + * have any effect on the list */ + if($dest>$pos&&$pos_offset!=0) { + $pos_offset=0; + $dest--; + } + + $pl->moveSong($pos-$pos_offset, $dest); + if($dest>$pos-$pos_offset) { + /* means we yanked something from over destination */ + $dest--; + } + else if($dest<$pos-$pos_offset) { + /* means we yanked something from below destination */ + $pos_offset--; + } + else { + /* moved it to our selves O_o */ + // echo "onself\n"; + } + } + $res = "ok"; + } + catch(PEAR_Exception $e) { + } + $json = array ('result' => $res); + } + else if(isset($_GET['ping'])) { + $result = "pong"; + $json = array("result" => $result); + } + else if(isset($_GET['volume'])&&is_numeric(trim($_GET['volume']))) { + $res = PF_FAILURE; + try { + $volume = trim($_GET['volume']); + $play = get_playback(); + if($play->setVolume($volume)) + $res = "ok"; + $play->disconnect(); + } + catch(PEAR_Exception $e) { + } + $json = array("result" => $res); + } + else if(isset($_GET['position'])&&is_numeric(trim($_GET['position'])) + && isset($_GET['id']) && is_numeric(trim($_GET['id']))) { + $result = PF_FAILURE; + try { + $pos = trim($_GET['position']); + $id = trim($_GET['id']); + $play = get_playback(); + if($play->seekId($id, $pos)) + $result = "ok"; + $play->disconnect(); + } + catch(PEAR_Exception $e) { + } + $json = array("result" => $result); + + } + else if(isset($_GET['currentsong'])) { + $res = "failure"; + try { + $res = $pl->getCurrentSong(); + if(!$res) + $res = "failure"; + } + catch(PEAR_Exception $e) { + } + $json = array("result" => $res); + } + else if(isset($_GET['dirlist'])) { + $dir = trim($_GET['dirlist']); + $FILE = 0; + $ARTIST = 1; + $ALBUM = 2; + $type = $FILE; + + if(isset($_GET['type'])&&is_numeric($_GET['type'])) { + $type = $_GET['type']; + } + if(is_null($dir)||$dir=="") + $dir = "/"; + $res = "failure"; + try { + $db = get_database(); + + if($type==$ALBUM||$type==$ARTIST) { + $t = false; + if(($t = strrpos($dir, "/")) !== false && $t == strlen($dir)-1) { + $dir = substr($dir, $t+1); + } + if(strlen($dir)==0) { + $type = $type==$ALBUM?"Album":"Artist"; + $res = array(strtolower($type) => $db->getMetadata($type)); + } + else { + $res = array(); + if($type==$ALBUM) { + $res["artist"] = $db->getMetadata("Artist", "Album", $dir); + $res['filelist'] = $db->find(array("Album" => $dir), true); + } + else if($type==$ARTIST) { + $res["album"] = $db->getMetadata("Album","Artist", $dir); + $res['filelist'] = $db->find(array("Artist" => $dir), true); + } + } + } + else { + $tmp = $db->getInfo($dir); + $res = array(); + + if(isset($tmp['directory'])) { + $res['directory'] =$tmp['directory']; + } + if(isset($tmp['file'])) { + $res['file'] = array(); + foreach($tmp['file'] as &$row) { + $res['file'][] = $row['file']; + } + } + if(isset($tmp['playlist'])) { + $res['playlist'] = $tmp['playlist']; + } + } + if(!$res) + $res = "failure"; + $db->disconnect(); + } + catch(PEAR_Exception $e) { + } + $json = array("result" => $res); + } + else if(isset($_GET['act'])) { + $act = trim($_GET['act']); + $result = "failure"; + try { + $play = get_playback(); + + if($act=="play") { + if(isset($_GET['id'])&&is_numeric(trim($_GET['id']))) { + if($play->playId(trim($_GET['id']))) + $result = "ok"; + } + else if(isset($_GET['pos'])&&is_numeric(trim($_GET['pos']))) { + if($play->play(trim($_GET['pos']))) + $result = "ok"; + } + else if($play->play()) { + $result = "ok"; + } + } + else if($act == "toggle") { + if($play->pause()) + $result = "ok"; + } + else if($act == "next") { + if($play->nextSong()) + $result = "ok"; + } + else if( $act == "previous") { + if($play->previousSong()) + $result = "ok"; + } + else if($act=="stop") { + if($play->stop()) + $result = "ok"; + } + else $result = "invalid command"; + $play->disconnect(); + } + catch(PEAR_Exception $e) { + $result = "failure"; + } + $json = array("result" => $result); + } + else if(isset($_GET['add'])) { + $add = $_GET['add']; + try { + $res = PF_FAILURE; + if($pl->addSong($add)) { + $res = "ok"; + $something_added = true; + } + } + catch(PEAR_Exception $e) { + } + $json = array("result" => $res); + } + else if(isset($_GET['remove'])) { + $arr = selection_to_reverse_list($_GET['remove']); + $res = "ok"; + try { + foreach($arr as &$val) { + if(!$pl->deleteSong($val)) + $res = "failure"; + } + } + catch(PEAR_Exception $e) { + $result = "failure"; + } + $json = array("result" => $res); + } + else if(isset($_GET['updatedb'])) { + $res = PF_FAILURE; + try { + $adm = get_admin(); + if($adm->updateDatabase()) + $res = "ok"; + $adm->disconnect(); + } + catch(PEAR_Exception $e) { + $res = PF_FAILURE; + } + $json = array("result" => $res); + } + else if(isset($_GET['outputs'])||isset($_GET['output_e'])||isset($_GET['output_d'])) { + $res = PF_FAILURE; + try { + $admin = get_admin(); + if(isset($_GET['outputs'])) + $res = $admin->getOutputs(); + else if(isset($_GET['output_e'])&&is_numeric($_GET['output_e'])) + $res = $admin->enableOutput(trim($_GET['output_e']))?"1":PF_FAILURE; + else if(isset($_GET['output_d'])&&is_numeric($_GET['output_d'])) + $res = $admin->disableOutput(trim($_GET['output_d']))?"0":PF_FAILURE; + $admin->disconnect(); + } + catch(PEAR_Exception $e) { + $res = PF_FAILURE; + } + $json = array("result" => $res); + } + else if(isset($_GET['random'])) { + $res = "failure"; + try { + $play = get_playback(); + $val = $_GET['random']=="1"; + if($play->random($val)) { + $res = $val?"1":"0"; + } + $play->disconnect(); + } + catch(PEAR_Exception $e) { + } + $json = array("result" => $res); + + } + else if(isset($_GET['repeat'])) { + $res = "failure"; + try { + $play = get_playback(); + $val = $_GET['repeat']=="1"; + if($play->repeat($val)) { + $res = $val?"1":"0"; + } + $play->disconnect(); + } + catch(PEAR_Exception $e) { + } + $json = array("result" => $res); + } + else if(isset($_GET['xfade'])&&is_numeric($_GET['xfade'])) { + $res = PF_FAILURE; + try { + $play = get_playback(); + if($play->setCrossfade(trim($_GET['xfade']))) + $res = "ok"; + $play->disconnect(); + + } + catch(PEAR_Exception $e) { + } + $json = array("result" => $res); + } + else if(isset($_GET['quick_search'])) { + $dir = trim($_GET['quick_search']); + $res = PF_FAILURE; + try { + $search_dir = strrpos($dir, "/"); + if($search_dir) { + $search_dir = substr($dir, 0, $search_dir); + } + else { + $search_dir = ""; + } + $db = get_database(); + $tmp = $db->getInfo($search_dir); + if(isset($tmp['directory'])) { + $res = array(); + $i=0; + foreach($tmp['directory'] as $key => &$value) { + if(stripos($value, $dir)===0) { + $i++; + $res[$key] = &$value; + } + if($i>=20) /* return up to x entries */ + break; + } + } + $db->disconnect(); + + } + catch(PEAR_Exception $e) { + } + $json = array("result" => $res); + } + else if(isset($_GET['searchadd'])||isset($_GET['searchfile'])) { + $artist = null; + $album = null; + $res = PF_FAILURE; + if(isset($_GET['artist'])&&strlen($_GET['artist'])>0) + $artist = $_GET['artist']; + if(isset($_GET['album'])&&strlen($_GET['album'])>0) + $album = $_GET['album']; + if(!(is_null($artist)&&is_null($album))) { + try { + $db = get_database(); + $params = array(); + if(!is_null($artist)) + $params["Artist"] = $artist; + if(!is_null($album)) + $params["Album"] = $album; + + + if(isset($_GET['searchadd'])) { + if(search_add($db, $pl, $params)) { + $res = "ok"; + $something_added = true; + } + else $res = "notfound"; + } + else { + $res = array(); + $res['filelist'] = $db->find($params, true); + } + $db->disconnect(); + } + catch(PEAR_Exception $e) { + $res = PF_FAILURE; + } + } + $json = array("result" => $res); + } + else if(((isset($_GET['metasearch'])&&is_numeric($_GET['metasearch']))|| + (isset($_GET['plsearch'])&&is_numeric($_GET['plsearch']))) + &&isset($_GET['s'])) { + $plsearch = isset($_GET['plsearch']); + + $type = intval($plsearch?$_GET['plsearch']:$_GET['metasearch']); + $search = $_GET['s']; + $res = PF_FAILURE; + if($type>=0&&$typehasFind()) { + $tmp = $pl->find(array($PL_SEARCH[$type] => $search)); + } + else if($plsearch) { + $data = $pl->getPlaylistInfoId(); + if(isset($data['file'])) + $data = $data['file']; + $tmp = array(); + $t = $PL_SEARCH[$type]; + foreach($data as &$song) { + if($type===0) { // any + foreach($song as &$e) + if(stristr($e, $search)!==FALSE) { + $tmp[] = $song; + break; + } + } + else if(isset($song[$t]) && stristr($song[$t],$search)!==FALSE) + $tmp[] = $song; + } + } + else { + $db = get_database(); + $tmp = $db->find(array($META_SEARCH[$type] => $search)); + $db->disconnect(); + } + $res = array(); + /* strip */ + $keys = array("Artist", "Title", "file", "Pos"); + foreach($tmp as &$row) { + $e = array(); + foreach($row as $key => &$val) { + if(in_array($key, $keys)!==FALSE) + $e[$key] = $val; + } + $res[] = $e; + } + } + catch(PEAR_Exception $e) { + //$res = $e->getMessage(); + } + } + else if($type==count($META_SEARCH)) { // search lyrics... + /* this should probably have been in metadata.php, but don't need + * to change anything if we have it here, kiss */ + $tmp = array(); + if(is_dir($metadata_dir)&&is_readable($metadata_dir)) { + $files = scandir($metadata_dir); + foreach($files as $file) { + $pos = strrpos($file, ".lyric"); + if($pos!==false&&$pos==strlen($file)-6) { + $xml = @simplexml_load_file($metadata_dir . $file); + if($xml&&isset($xml->result[0])&&isset($xml->result[0]->lyric[0])) { + $l = (string)$xml->result[0]->lyric[0]; + if(stripos($l, $search)!==false) { + if(isset($xml->file)) { + /* + foreach($xml->file as $f) { + $e = array(); + $e['file'] = (string)$f; + $e['Artist'] = (string)$xml->result[0]->artist[0]; + $e['Title'] = (string)$xml->result[0]->title[0]; + $res[] = $e; + } + */ + $e = array(); + $e['Artist'] = (string)$xml->result[0]->artist[0]; + $e['Title'] = (string)$xml->result[0]->title[0]; + $tmp[] = $e; + } + } + } + } + } + } + $db = get_database(); + + $res = array(); + foreach($tmp as &$row) { + $sr = $db->find(array("Artist" => $row['Artist'], "Title" => $row["Title"])); + /*var_dump($tmp); + break;*/ + if(isset($sr[0])&&isset($sr[0]['file'])) { + $row['file'] = $sr[0]['file']; + $res[] = $row; + } + } + $db->disconnect(); + } + $json = array("result" => $res); + } + else if(isset($_GET['ma'])) { + /* note to self: should merge single add with this */ + $res = PF_FAILURE; + if (!isset($HTTP_RAW_POST_DATA)) + $HTTP_RAW_POST_DATA = file_get_contents("php://input"); + $ma = explode("\n", $HTTP_RAW_POST_DATA); + $db = false; + $sparam = array(); + if(count($ma)) { + $tmp = explode(":", $ma[0], 2); + if($tmp[0]=="baseartist") { + $sparam['Artist'] = $tmp[1]; + } + else if($tmp[0]=="basealbum") { + $sparam['Album'] = $tmp[1]; + } + } + try { + foreach($ma as &$guom) { + $v = explode(":", $guom, 2); + if(!count($v)==2) + continue; + $res.=$v[0]; + if($v[0]=="file"||$v[0]=="directory") { + $pl->addSong($v[1]); + } /* we should never get same as baseartist/album here, if we do I don't care */ + else if($v[0]=="album"||$v[0]=="artist") { + if($v[0]=="album") + $sparam["Album"] = $v[1]; + else $sparam["Artist"] = $v[1]; + if(!$db) + $db = get_database(); + search_add($db, $pl, $sparam); + } + else if($v[0]=="playlist") { + $pl->loadPlaylist($v[1]); + } + } + $res = "ok"; + $something_added = true; + if($db) + $db->disconnect(); + } + catch(PEAR_Exception $e) { } + $json = array("result" => $res); + } + else if(isset($_GET['playlist_rm'])||isset($_GET['playlist_load']) + ||isset($_GET['playlist_save'])||isset($_GET['playlist_add_url'])) { + + $res = false; + try { + if(isset($_GET['playlist_rm'])) { + $res = $pl->deletePlaylist(trim($_GET['playlist_rm'])); + } + else if(isset($_GET['playlist_load'])) { + $res = $pl->loadPlaylist(trim($_GET['playlist_load'])); + } + else if(isset($_GET['playlist_save'])) { + $res = $pl->savePlaylist(trim($_GET['playlist_save'])); + } + else if(isset($_GET['playlist_add_url'])) { + $url = trim($_GET['playlist_add_url']); + if(stripos($url, "http://")==0) { + $res = handle_playlist_url($pl, $url); + } + } + } + catch(PEAR_Exception $e) {} + $res = $res?"ok":PF_FAILURE; + $json = array("result" => $res); + } + else if(isset($_GET['clearerror'])) { + $pl->clearError(); + $json = array("result" => "ok"); + } + + if(!is_null($json)) { + try { + if($playlist_empty&&$something_added) { + $play = get_playback(); + $play->play(); + $play->disconnect(); + } + $json["status"] = $pl->getStatus(); + if(isset($_GET['plchanges'])&&is_numeric(trim($_GET['plchanges']))&&$_GET['plchanges']!=$json['status']['playlist']) { + $res = $pl->getChanges(trim($_GET['plchanges']), true); + if($res&&isset($res['file'])&&is_array($res['file'])) { + + if(isset($_GET['pmax'])&&isset($_GET['page'])) { + $arr = $res['file']; + $max = intval($_GET['pmax']); + $page = intval($_GET['page']); + $start = $max * $page; + $end = $start + $max; + $ret = array(); + foreach($res['file'] as $f) { + if($f['cpos']>=$start&&$f['cpos']<$end) + $ret[] = $f; + if($f['cpos']>=$end) + break; + } + if(count($ret)>0) + $json['plchanges'] = &$ret; + + } + else { + $json["plchanges"] = &$res['file']; + } + } + } + $pl->disconnect(); + } + catch(PEAR_Exception $e) { + } + array_to_json($json); + } + else { + try { + $pl->disconnect(); + } catch(PEAR_Exception $e) {} + } +?> diff --git a/player/config.php b/player/config.php new file mode 100644 index 0000000..da8e021 --- /dev/null +++ b/player/config.php @@ -0,0 +1,374 @@ + + + + + + + +<?php echo m("Pitchfork MPD Client Configuration"); ?> + + +\n\n\n"); + } + if(isset($_POST['submit'])) { + $vars = array( 'mpd_host', 'mpd_port', 'mpd_pass', 'login_pass', 'update_delay', + 'metadata_disable', 'theme', 'stop_button', 'shout_url', 'pagination', 'lang'); + foreach ($vars as $var) { + $add = ""; + if(isset($_POST[$var])&&trim($_POST[$var])!="") + $add = trim($_POST[$var]); + + if($var=="pagination") { + if(!is_numeric($add)) + $add = 0; + else $add = intval($add); + } + else if($var=="login_pass"&&strlen($add)>0) { + if($add== HASH_PASS) + continue; + $add = generate_hash($add); + } + + + if(isset($config->$var)) { + $config->$var = $add; + } + else { + $config->addChild($var, $add); + } + } + + $plentry = null; + if(isset($config->plentry)) + $plentry = $config->plentry; + else + $plentry = $config->addChild("plentry"); + + foreach($pl_fields as $field) { + $val = isset($_POST['plentry_' . $field])?"yes":""; + $plentry->$field = $val; + } + + // need to save config! + if($config->asXML("../config/config.xml")) { + header("Location: index.php"); + echo "

If you're not redirected, go here: player

"; + exit(); + } + else { + echo "

Could not save your configuration, check that config/config.xml is writeable

\n"; + } + } + + if(!is_writeable("../config")) { + echo "

"; + echo m("Warning: Your config/ directory is not writeable! Please change owner of directory to apache user."); + echo "

\n"; + } + @ob_end_flush(); + +?> + + + +
+ +

Pitchfork configuration

+ +" . m("Let us take a minute to configure this player") . "

\n"; + else echo "

" . m("Configure settings") . "

"; +?> +
+

+

+ + + + + + +
' name='mpd_host' />
+' name='mpd_port' />
+' name='mpd_pass' />
+

+


+ + + + + + + + + + + + + +"; + } + echo "=3) + echo "style='display: none; ' "; + echo ">\n"; +} + +?> + +
+' name='update_delay' />
+ ' name='login_pass' />
+ +
+ +
+ name='stop_button' value='yesplease' /> +
" +title="" size="5" />
 
 
  "; + echo m("Show nonstandard") . " [+]
 "; + echo "
 
+

Metadata

+

+ + +
name='metadata_disable' />
+

+

+
+ +

+

+ + +

+
+ +
+

+For lyrics search to work in the directory browser file-names has to be saved with the lyrics, however when you move/delete files from your library this file reference become wrong. This button removes any references to such files. +
+ + +

+" . m("Yes") . ""; + else return "" . m("No") . ""; +} + +// function_name:fatal (0/1) +function test_function($stuff) { + $stuff = explode(":", $stuff); + $name = $stuff[0]; + echo $name . ": "; + echo print_yesno(function_exists($name), $stuff[1]); + echo "
\n"; +} + +?> + +
+

+

+\n"; + echo m("Release date:") . " $release_date

\n"; + $pl = get_playback(); + $has_commands = true; + try { + if($pl) { + $commands = $pl->getCommands(); + /* these are just some of the needed commands */ + $needed = array("outputs", "disableoutput", "enableoutput", "plchangesposid"); + $res = array_intersect($needed, $commands); + if(count($res)!=count($needed)) + $has_commands = false; + $pl->disconnect(); + } + } + catch(PEAR_Exception $e) { + $has_commands = false; + } + + echo m("Connect to mpd:"). " ". print_yesno($pl, true) . "
\n"; + if($pl) { + echo m("MPD commands:")." " . print_yesno($has_commands, true) . "
\n"; + } + echo m("Metadata directory:"). " " . print_yesno((file_exists($metadata_dir)&&is_writeable($metadata_dir)) + ||(!file_exists($metadata_dir)&&is_writeable($config_dir)), true); +?> +

+ +

+

+"; + else echo "error\n"; + $mem = ceil(return_bytes(ini_get("memory_limit"))/(1024*1024)); + echo m("PHP memory limit:") . " " . $mem . "MB"; + +?> +

+"; + echo m("Warning: Magic quotes runtime is on,
please use pitchfork.conf or turn
of manually."); + echo "

\n"; + } + if(get_magic_quotes_gpc()) { + echo "

"; + echo m("Warning: Magic quotes gpc is on,
please use pitchfork.conf or turn
of manually."); + echo "

\n"; + } + +?> +
+ +
+ + + diff --git a/player/index.php b/player/index.php new file mode 100644 index 0000000..56588e7 --- /dev/null +++ b/player/index.php @@ -0,0 +1,135 @@ + + + + + + + +\n"; + ?> + + + Pitchfork MPD Client + + + +
+ +
+

+ +
+ +
+
+


+


+


+
+ +
+
+ + + + +
+

+ +

+
+ +
+
+
+
+ +
+

+

+
+ +
    + + + + + + ". + m("Streaming") . "\n"; + if(is_null(get_config('metadata_disable'))) { + echo "\t\t
  • ". m("Recommendation") . "
  • \n"; + echo "\t\t\n"; + + } + ?> +
+
+ +
+" /> +" /> +" /> +
+ +
+
+
+ + + + +
+ + +
+ + + + diff --git a/player/login.php b/player/login.php new file mode 100644 index 0000000..4ad58e2 --- /dev/null +++ b/player/login.php @@ -0,0 +1,91 @@ + + + +Pitchfork login + + + + +
+

Pitchfork login

+$error

"; + } + if(isset($_SESSION['logged_in'])&&$_SESSION['logged_in']) { + echo "

Already logged in. Log out?

\n"; + } +?> +
+ Password: + +
+
+ + diff --git a/player/metadata.php b/player/metadata.php new file mode 100644 index 0000000..5da7607 --- /dev/null +++ b/player/metadata.php @@ -0,0 +1,787 @@ + "nocachedir")); + echo $xml->asXML(); + exit(); + } + + function escape_name($name) { + return str_replace(DIRECTORY_SEPARATOR, "_", $name); + } + + function get_cover_base_name($artist, $album) { + global $cover_dir; + return $cover_dir . escape_name($artist) . " - " . escape_name($album); + } + + function get_album_info_name($artist, $album) { + return get_cover_base_name($artist, $album) . ".txt"; + } + function get_lyric_filename($artist, $title) { + global $cover_dir; + return $cover_dir . escape_name($artist) . " - ". escape_name($title) . ".lyric"; + } + + function find_lyrics($arr) { + foreach($arr as $val) { + if(!is_array($val)) + continue; + if(isset($val['name'])&&$val['name']=="RETURN") { + return $val['children']; + } + else if(is_array($val)) { + $ret = find_lyrics($val); + if($ret) + return $ret; + } + } + return false; + } + + function fp_get_contents($fp) { + $ret = ""; + $tmp = false; + while($tmp = fgets($fp)) + $ret .= $tmp; + fseek($fp, 0); + if(strlen($ret)==0) + return false; + return $ret; + } + + /* Queries amazon with the specified url, strict serach first and then a more careless one, + * will urlencode artist and albumname + * returns xml document or false upon failure */ + function amazon_album_query($base_url, $artist, $album) { + $stype = array("Title", "Keywords"); + $artist = urlencode($artist); + $album = urlencode($album); + foreach($stype as $st) { + if(!amazon_wait()) + return false; + $xml = @simplexml_load_string(@file_get_contents($base_url . "&Artist=$artist&$st=$album")); + if($xml&&isset($xml->Items[0])&&isset($xml->Items[0]->Item[0])) + return $xml; + } + return false; + } + + /* returns file pointer or false */ + function get_album_lock($artist, $album) { + $file_name = get_album_info_name($artist, $album); + $exists = file_exists($file_name); + $fp = false; + + if($exists) + $fp = @fopen($file_name, "r+"); + else $fp = @fopen($file_name, "w+"); + if($fp && flock($fp, LOCK_EX)) + return $fp; + + trigger_error("Can't lock album-file: $file_name", E_USER_WARNING); + return false; + } + + /* waits for appropriate amazon time, have to be called before making any amazon requests + returns true if ok to continue otherwise false */ + function amazon_wait() { + global $metadata_dir; + + /* rationale behind this: + * amazon requires that we don't make more than one request pr. second pr. ip */ + + $file_name = $metadata_dir . "amazon_time"; + if(file_exists($file_name)) + $fp = @fopen($file_name, "r+"); + else $fp = @fopen($file_name, "w+"); + + if(!$fp) { + trigger_error("Can't open amazon_time", E_USER_WARNING); + return false; + } + if(!flock($fp, LOCK_EX)) { + @fclose($fp); + trigger_error("Can't lock amazon_time", E_USER_WARNING); + return false; + } + + $last = fp_get_contents($fp); + if($last) { + $stime = 1000; + if(is_numeric($last)) { + $stime = current_time_millis() - $last; + } + $stime = abs($stime); + if($stime<1000) + usleep($stime*1000); // micro seconds + } + + if(@fwrite($fp, current_time_millis())===false) { + @fclose($fp); + trigger_error("Can't write to amazon_time", E_USER_WARNING); + return false; + } + else { + @fclose($fp); + return true; + } + } + + /* returns artist and album info and get's album lock or dies */ + /* return value: array($fp, $artist, $album) */ + function init_album_artist_or_die() { + ob_end_clean(); + header("Content-Type: text/xml; charset=UTF-8"); + + $album = ""; + $artist = ""; + if(isset($_GET['artist'])&&isset($_GET['album']) && + strlen(trim($_GET['artist']))>0&&strlen(trim($_GET['album']))>0) { + $album = trim($_GET['album']); + $artist = trim($_GET['artist']); + } + else { + $xml = array_to_xml(array("result" => "missingparam")); + echo $xml->asXML(); + exit(); + } + + $fp = get_album_lock($artist, $album); + + if(!$fp) { + $xml = array_to_xml(array("result" => "failed")); + echo $xml->asXML(); + exit(); + } + return array($fp, $artist, $album); + } + + /* returns array(artist, album, filename) or false */ + function get_current_info() { + try { + $pl = get_playback(); + if($pl) { + $info = $pl->getCurrentSong(); + if(isset($info['Artist'])&&isset($info['Title'])) { + $artist = trim($info['Artist']); + $title = trim($info['Title']); + $file_name = $info['file']; + return array($artist, $title, $file_name); + } + } + $pl->disconnect(); + } + catch(PEARException $e) { + } + return false; + } + + + function get_cover() { + global $COVER_SEARCH_AGAIN, $amazon_base_url,$cover_providers; + + list($fp, $artist, $album) = init_album_artist_or_die(); + + $xml = fp_get_contents($fp); + if($xml) { + $xml = @simplexml_load_string($xml); + if($xml) { + $use_cache = true; + if(isset($xml->notfound)&&is_numeric((string)$xml->notfound[0])) { + $time = @intval((string)$xml->notfound[0]); + if($time+$COVER_SEARCH_AGAINimage[0])&&!isset($xml->thumbnail[0])) { + $use_cache = false; + } + + if($use_cache) { + $xml->addChild("cached", "true"); + echo $xml->asXML(); + exit(); + } + } + } + + + $res = false; + + foreach($cover_providers as $cp) { + $res = $cp($artist, $album); + if($res&&is_array($res)) + break; + } + + if($xml) { + if($res&&is_array($res)) { + foreach($res as $key => $val) { + if(!isset($xml->$key)) + $xml->$key = (string)$val; + } + } + else { + $xml->notfound = time(); + } + } + else { + if($res&&is_array($res)) { + $res['time'] = time(); + $xml = array_to_xml($res); + } + else { + $xml = array("notfound" => time()); + $xml = array_to_xml($xml); + } + } + + @fwrite($fp, $xml->asXML()); + + @fclose($fp); + echo $xml->asXML(); + exit(); + } + + function get_review() { + global $amazon_review_url, $COVER_SEARCH_AGAIN; + + list($fp, $artist, $album) = init_album_artist_or_die(); + + $xml = fp_get_contents($fp); + $asin = ""; + $desc = false; + $review = false; + $review_src = false; + $no_search = false; + $failed = false; + $changed = false; + + + if($xml) { + $xml = @simplexml_load_string($xml); + if($xml) { + if(isset($xml->rnotfound)&&is_numeric((string)$xml->rnotfound[0])) { + $time = @intval((string)$xml->rnotfound[0]); + if($time+$COVER_SEARCH_AGAIN>time()) + $no_search = true; + } + } + } + + if(!$xml||(!(isset($xml->review[0])||isset($xml->desc[0]))&&!$no_search)) { + $res = false; + if(!amazon_wait()) { + echo array_to_xml(array("result" => "failed"))->asXML(); + exit(); + } + + if($xml&&isset($xml->asin[0])) { + $res = @file_get_contents($amazon_review_url . "ItemLookup&IdType=ASIN&ItemId=" . urlencode($xml->asin[0])); + if($res) + $res = @simplexml_load_string($res); + $asin = false; + } + else { + $res = @amazon_album_query($amazon_review_url . "ItemSearch&SearchIndex=Music&Artist=" , $artist , $album); + } + if($res) { + if($res&&isset($res->Items[0])&&isset($res->Items[0]->Item[0])) { + $p = $res->Items[0]->Item[0]; + $asin = (string) $p->ASIN; + if(isset($p->EditorialReviews[0])) { + $p = $p->EditorialReviews[0]; + foreach($p->EditorialReview as $er) { + if(!$desc&&"Album Description" == (string)$er->Source) { + $desc = (string) $er->Content; + } + else if(!$review) { + $review_src = (string) $er->Source; + $review = (string) $er->Content; + } + } + } + /* set info in xml-file... */ + if($xml) { + if($review) { + $xml->review_src = htmlspecialchars($review_src); + $xml->review = htmlspecialchars($review); + } + if($desc) { + $xml->desc = htmlspecialchars($desc); + } + if(!isset($xml->asin[0])) { + $xml->addChild("asin", $asin); + $changed = true; + } + if(!$review&&!$desc) { + $failed = true; + } + else { + $changed = true; + } + } + else { + $xml = array(); + $xml['asin'] = $asin; + if($desc) + $xml['desc'] = $desc; + if($review) { + $xml['review_src'] = $review_src; + $xml['review'] = $review; + } + if(!$review&&!$desc) + $failed = true; + $xml = array_to_xml($xml); + $changed = true; + } + } + else { + $failed = true; + } + } + else { + $failed = true; + } + } + else { + $xml->addChild("cached", "true"); + } + + if($xml) { + if($failed) { + if(isset($xml->rnotfound)) { + $xml->rnotfound = time(); + } + else { + $xml->addChild("rnotfound", time()); + } + @fwrite($fp, $xml->asXML()); + } + else if($changed) { + @fwrite($fp, $xml->asXML()); + } + } + else { + $xml = array_to_xml(array("rnotfound" => time())); + @fwrite($fp, $xml->asXML()); + } + @fclose($fp); + echo $xml->asXML(); + exit(); + } + + /* artist, title and song file name in system */ + function _get_lyric_lyricwiki($artist, $title, $file_name) { + $file = get_lyric_filename($artist, $title); + $fp = fsockopen("lyricwiki.org", 80); + if(!$fp) { + $xml = array_to_xml(array("result"=>"connectionfailed")); + return $xml->asXML(); + } + + $out = "\r\n"; + $out .= ""; + $out .= ""; + $out .= ""; + $out .= htmlspecialchars($artist); + $out .= ""; + $out .= ""; + $out .= htmlspecialchars($title); + $out .= ""; + $out .= "\r\n"; + + $head = "POST /server.php HTTP/1.1\r\n"; + $head .= "Host: lyricwiki.org\r\n"; + $head .= "SOAPAction: urn:LyricWiki#getSong\r\n"; + $head .= "Content-Type: text/xml; charset=UTF-8\r\n"; + $head .= "User-Agent: RemissPitchfork/0.1\r\n"; + $head .= "Content-Length: " . str_byte_count($out) . "\r\n"; + $head .= "Connection: Close\r\n\r\n"; + + fwrite($fp, $head . $out); + + $responseHeader = ""; + /* assume everything is dandy */ + do { + $responseHeader.= fread($fp, 1); + } + while (!preg_match('/\\r\\n\\r\\n$/', $responseHeader)); + + $ret = ""; + while(!feof($fp)) { + $ret .= fgets($fp, 128); + } + fclose($fp); + /* stupid hack to get around wrong xml declearation */ + $qmark = "?"; + if(strpos($ret, "<". $qmark . "xml version=\"1.0\" encoding=\"ISO-8859-1\"".$qmark.">")===0) + $ret = str_replace("<". $qmark . "xml version=\"1.0\" encoding=\"ISO-8859-1\"".$qmark.">", + "<". $qmark . "xml version=\"1.0\" encoding=\"UTF-8\"".$qmark.">", + $ret); + + /*echo $ret; + exit();*/ + $parser = new xml2array(); + $parser->parse($ret); + $data = find_lyrics($parser->arrOutput); + // check that data is ok and lyrics exist + if($data&&isset($data[2]['tagData'])) { + $res = array(); + foreach($data as $d) { + if($d['name']=="ARTIST") + $res['artist'] = $d['tagData']; + else if($d['name']=="SONG") + $res['title'] = $d['tagData']; + else if($d['name']=="LYRICS") + $res['lyric'] = $d['tagData']; + else if($d['name']=="URL") + $res['url'] = $d['tagData']; + } + $res['from'] = "lyricwiki.org"; + $res['time'] = time(); + /* this caching thing will have to be extracted if we + * put in another lyrics source */ + if(trim($res['lyric'])&&trim($res['lyric'])!="Not found") { + $xml = array_to_xml(array("result" => $res)); + $xml->addChild("file", htmlspecialchars($file_name)); + $res = $xml->asXML(); + @file_put_contents($file, $res); + } + else { + $out = array("result" => "notfound"); + if(isset($res['url'])) + $out['url'] = $res['url']; + $res = array_to_xml($out); + $res = $res->asXML(); + } + return $res; + } + return false; + } + + /* $file: filename of cached version + * $file_name: file name of song */ + function _get_lyric_cache($file, $file_name) { + $xml = @simplexml_load_file($file); + if($xml) { + $add_file = true; + if(isset($xml->file)) { + foreach($xml->file as $f) { + if(((string)$f)==$file_name) + $add_file = false; + } + } + if($add_file) { + $xml->addChild("file", htmlspecialchars($file_name)); + @file_put_contents($file, $xml->asXML()); + } + $xml->addChild("cached", "true"); + return $xml->asXML(); + } + return false; + } + + function get_lyric($info = null) { + header("Content-Type: text/xml; charset=UTF-8"); + ob_end_clean(); + if(is_null($info)) + $info = get_current_info(); + if(!$info) { + $xml = array_to_xml(array("result"=>"failed")); + echo $xml->asXML(); + exit(); + } + $artist = $info[0]; + $title = $info[1]; + $file_name = $info[2]; + + $file = get_lyric_filename($artist, $title); + if(file_exists($file)&&!isset($_GET['force'])) { + $xml = _get_lyric_cache($file, $file_name); + if($xml) { + echo $xml; + exit(); + } + } + + $xml = _get_lyric_lyricwiki($artist, $title, $file_name); + if($xml) { + echo $xml; + } + else { + echo array_to_xml(array("result" => "failed"))->asXML(); + } + exit(); + } + + function get_pic() { + + global $cover_dir; + $b_name = basename(trim($_GET['pic'])); + $name = $cover_dir . $b_name; + if(file_exists($name)&&is_readable($name)) { + if(function_exists("finfo_open")&&function_exists("finfo_file")) { + $f = finfo_open(FILEINFO_MIME); + header("Content-Type: " . finfo_file($f, $name)); + } + else if(function_exists("mime_content_type")) { + header("Content-Type: " . mime_content_type($name)); + } + else { + header("Content-Type: image/jpeg"); + } + $c = "Content-Disposition: inline; filename=\""; + $c .= rawurlencode($b_name) . "\""; + header($c); + echo @file_get_contents($name); + ob_end_flush(); + exit(); + } + else { + echo "File does not exist\n"; + trigger_error("Did not find albumart althought it was requested", E_USER_WARNING); + exit(); + } + } + + function get_recommendations_from_playlist() { + require_once("../player/openstrands.php"); + $pl = get_playlist(); + $list = $pl->getPlaylistInfo(); + $artist = array(); + foreach($list as $song) { + if(isset($song['Artist'])&&$song['Artist']) + $artist[$song['Artist']] = true; + } + $artist = array_keys(array_change_key_case($artist)); + $pl->disconnect(); + + header("Content-Type: text/xml; charset=UTF-8"); + + $ret = strands_get_recommendations($artist); + $res = array(); + if(!$ret || ! count($ret)) { + $res['result'] = is_array($ret)?"notfound":"failed"; + echo array_to_xml($res)->asXML(); + exit(); + } + $db = get_database(); + foreach($ret as $a) { + $tmp = array(); + $tmp['name'] = $a; + $tmp['album'] = $db->getMetadata("Album", "Artist", $a); + $res[] = $tmp; + } + $out = array("result" => $res); + $db->disconnect(); + echo array_to_xml($out)->asXML(); + } + + function do_houseclean() { + /* this is a *very* inefficient method, but it's needed... */ + //header("Content-Type: text/xml; charset=UTF-8"); + header("Content-type: multipart/x-mixed-replace;boundary=--ThisRandomString"); + + global $metadata_dir; + + echo "--ThisRandomString\n"; + $out = "Content-type: text/html\n\n". + "Housecleaning\n". + "

Performing housecleaning, please wait...

\n"; + + echo "$out--ThisRandomString\n"; + ob_end_flush(); + flush(); + set_time_limit(300); // this might take a while, but + // shouldn't be more than 5 mins even on slow machines + $db = get_database(); + $res = "failed"; + try { + $time = current_time_millis(); + $list = $db->getAll(); + if(!isset($list['file'])) + return; + $files = $list['file']; + $db->disconnect(); + $list = scandir($metadata_dir); + $total = count($list); + $fixed = 0; + $errors = 0; + $fcount = 0; + $fcount_inv = 0; + $tcount = 0; + foreach($list as $f) { + $r = strrpos($f, ".lyric"); + $tcount++; + if($r!==false&&$r+6==strlen($f)) { + $xml = @simplexml_load_file($metadata_dir . $f); + $fcount++; + if($fcount%100 == 0) { + echo $out; + echo "

Processed $fcount (".(int)($tcount*100/$total)."%)..

\n"; + echo "--ThisRandomString\n"; + flush(); + + } + if($xml) { + $x_files = array(); + foreach($xml->file as $v) { + $x_files[] = (string)$v; + } + $dis = array_intersect($x_files, $files); + if(count($dis)!=count($x_files)) { + $dom = @dom_import_simplexml($xml); + if($dom===false) { + $errors++; + continue; + } + + while($elem = $dom->getElementsByTagName("file")->item(0)) { + $dom->removeChild($elem); + } + + $xml = simplexml_import_dom($dom); + array_to_xml($dis, $xml, "file"); + @$xml->asXML($metadata_dir . $f); + $fixed++; + } + } + else { + $fcount_inv++; + } + } + } + $result = array("time" => intval(current_time_millis() - $time), "fixed" => $fixed, "errors" => $errors); + } + catch(PEAR_Exception $e) { + } + echo "Content-type: text/html\n\n"; + echo "

"; + if(is_array($result)) { + echo "Result of cleaning:
\n"; + echo "$fcount files checked in " . $result['time'] . "ms of which $fcount_inv was invalid
"; + echo "Fixed: " . $result['fixed'] . "
"; + echo "Errors: " . $result['errors'] . "
\n"; + + } + else if($result=="failed") { + echo "It appears housecleaning failed, check your MPD settings"; + } + else { + echo "hmm.. somethings wrong, try again"; + } + echo "

Back to configuration

\n"; + echo "\n--ThisRandomString\n"; + } + + + if(!isset($iamincluded)) { + if(isset($_GET['cover'])) get_cover(); + else if(isset($_GET['review'])) get_review(); + else if(isset($_GET['lyric'])) get_lyric(); + else if(isset($_GET['pic'])) get_pic(); + else if(isset($_GET['housecleaning'])) do_houseclean(); + else if(isset($_GET['plrecommend'])) get_recommendations_from_playlist(); + else { + header("Content-Type: text/xml; charset=UTF-8"); + $xml = array_to_xml(array("result"=>"what do you want?")); + echo $xml->asXML(); + exit(); + } + } + + +class xml2Array { + + var $arrOutput = array(); + var $resParser; + var $strXmlData; + + /* parse to utf-8 */ + function parse($strInputXML) { + + $this->resParser = xml_parser_create("UTF-8"); + + xml_set_object($this->resParser,$this); + xml_set_element_handler($this->resParser, "tagOpen", "tagClosed"); + xml_parser_set_option($this->resParser, XML_OPTION_TARGET_ENCODING, "UTF-8"); + + xml_set_character_data_handler($this->resParser, "tagData"); + + $this->strXmlData = xml_parse($this->resParser,$strInputXML ); + if(!$this->strXmlData) { + die(sprintf("XML error: %s at line %d", + xml_error_string(xml_get_error_code($this->resParser)), + xml_get_current_line_number($this->resParser))); + } + + xml_parser_free($this->resParser); + + return $this->arrOutput; + } + function tagOpen($parser, $name, $attrs) { + $tag=array("name"=>$name,"attrs"=>$attrs); + array_push($this->arrOutput,$tag); + } + + function tagData($parser, $tagData) { + if(isset($this->arrOutput[count($this->arrOutput)-1]['tagData'])) + $this->arrOutput[count($this->arrOutput)-1]['tagData'] .= $tagData; + else + $this->arrOutput[count($this->arrOutput)-1]['tagData'] = $tagData; + } + + function tagClosed($parser, $name) { + $this->arrOutput[count($this->arrOutput)-2]['children'][] = $this->arrOutput[count($this->arrOutput)-1]; + array_pop($this->arrOutput); + } +} + +?> diff --git a/player/metadata_cover.php b/player/metadata_cover.php new file mode 100644 index 0000000..10e7b20 --- /dev/null +++ b/player/metadata_cover.php @@ -0,0 +1,129 @@ +Items[0])&&isset($xml->Items[0]->Item[0])) { + $item = $xml->Items[0]->Item[0]; + $small = false; + $small_name = false; + $large = false; + $large_name = false; + + if(isset($item->SmallImage[0])) { + $small = @file_get_contents($item->SmallImage[0]->URL[0]); + if($small) { + $small_name = escape_name($artist) . "-" . escape_name($album) . basename($item->SmallImage[0]->URL[0]); + if(!@file_put_contents($metadata_dir . $small_name, $small)) { + $small = false; + } + } + } + if(isset($item->LargeImage[0])) { + $large = @file_get_contents($item->LargeImage[0]->URL[0]); + if($large) { + $large_name = escape_name($artist) . "-" . escape_name($album) . basename($item->LargeImage[0]->URL[0]); + if(!@file_put_contents($metadata_dir . $large_name, $large)) { + $large = false; + } + } + } + else if(isset($item->MediumImage[0])) { + $large = @file_get_contents($item->MediumImage[0]->URL[0]); + if($large) { + $large_name = escape_name($artist) . "-" . escape_name($album) . basename($item->MediumImage[0]->URL[0]); + if(!@file_put_contents($metadata_dir . $large_name, $large)) { + $large = false; + } + } + } + if($small && $large) { + $data = array(); + $data['asin'] = $item->ASIN[0]; + $data['thumbnail'] = $small_name; + $data['image'] = $large_name; + return $data; + } + } + } + return false; + } + + function directory_cover($artist, $album) { + global $directory_search_url, $metadata_dir; + + if(!$directory_search_url) + return false; + + $artist = escape_name($artist); + $album = escape_name($album); + + $name = sprintf($directory_search_url, $artist, $album); + + return make_thumb_and_store_image($name, get_cover_base_name($artist, $album)); + } + + /* yay for me and function names */ + function make_thumb_and_store_image($file, $store_bname) { + // only supports jpeg for now.. + $max_size = 75; + $image = @imagecreatefromjpeg($file); + if(!$image) + return false; + + $ix = imagesx($image); + $iy = imagesy($image); + $rx = $ry = $max_size; + + $max_i = $ix>$iy?$ix:$iy; + $res = array(); + $res['image'] = basename($store_bname . ".jpg"); + if(!@imagejpeg($image, $store_bname . ".jpg", 90)) { + return false; + } + if($max_i>$max_size) { + $ratio = (double)$max_i/(double)$max_size; + $rx = (int)((double)$ix/$ratio); + $ry = (int)((double)$iy/$ratio); + $thumb = imagecreatetruecolor($rx,$ry); + imagecopyresampled($thumb, $image, 0,0,0,0, $rx, $ry, $ix, $iy); + $res['thumbnail'] = basename($store_bname . "_thumb.jpg"); + if(!@imagejpeg($thumb, $store_bname . "_thumb.jpg", 90)) { + return false; + } + } + else { + $res['thumb'] = $res['image']; + } + return $res; + } +?> diff --git a/player/nowplaying.php b/player/nowplaying.php new file mode 100644 index 0000000..401fad0 --- /dev/null +++ b/player/nowplaying.php @@ -0,0 +1,107 @@ +getCurrentSong(); + $pl->disconnect(); + + $rlyric = ""; + + if(isset($info['Artist'])&&isset($info['Title'])) { + $file = get_lyric_filename($info['Artist'], $info['Title']); + $lyric = false; + if(file_exists($file)) { + $lyric = _get_lyric_cache($file, $info['file']); + } + else { + $lyric = @_get_lyric_lyricwiki($info['Artist'], $info['Title'], $info['file']); + } + + if($lyric!==FALSE) { + $lyric = simplexml_load_string($lyric); + if($lyric&&$lyric->result->lyric) { + $lyric = (string) $lyric->result->lyric; + $lyric = explode("\n", $lyric); + $lines = count($lyric); + $tmp = ""; + /* try until we hit something */ + while(strlen($tmp)<=0&&$lines>0) { + $tmp = $lyric[rand(0, count($lyric)-1)]; + if(strlen($tmp)) { + $rlyric = substr(trim($tmp), 0, 60); + if($rlyric) { + $rlyric = " -~\"" . $rlyric . "\""; + + } + } + $lines--; + } + } + } + echo "np: " . $info['Artist'] . " - " . $info['Title'] . $rlyric; + } + else { + echo "np: "; + if( isset($info['Artist'])) + echo $info['Artist'] . " - "; + if(isset($info['Title'])) + echo $info['Title'] . " - "; + if(isset($info['file'])) + echo $info['file']; + else echo "not playing"; + } + } + catch(PEARException $e) { + echo "error contacting mpd"; + } + echo "\n"; +?> diff --git a/player/openstrands.php b/player/openstrands.php new file mode 100644 index 0000000..61e54ef --- /dev/null +++ b/player/openstrands.php @@ -0,0 +1,54 @@ +SimpleArtist) return $ret; + + foreach($res->SimpleArtist as $sa) { + if($sa->ArtistName) + $ret[] = (string)$sa->ArtistName; + + } + return $ret; + } + +?> diff --git a/player/preferences.js.php b/player/preferences.js.php new file mode 100644 index 0000000..4550733 --- /dev/null +++ b/player/preferences.js.php @@ -0,0 +1,69 @@ + diff --git a/std/base.css b/std/base.css new file mode 100644 index 0000000..074ddb5 --- /dev/null +++ b/std/base.css @@ -0,0 +1,520 @@ +body { + line-height: 19.2px; +} + +a { + text-decoration: none; +} + +tr, td, #disp_artist, #disp_album, #disp_info, p.disp, img.thumbnailart { + padding: 0px; + margin: 0px; +} + +.main_container { + padding-left: 5px; + padding-bottom: 10px; +} + +#playlist { + line-height: 1em; + table-layout: fixed; + font-size: 0.99em; + border-width: 0px; + border-spacing: 0px; + cursor: default; +} + +/* for konqueror */ +.pltemp { + color: #000001; +} + +/* apply to td's in tr's following an element with id playlist */ +#playlist > tr > td { + white-space: nowrap; + height: 1.3em; + border-width: 0px; + padding-left: 2px; +} + +#content { + margin-left: 48px; + margin-top: 106px; + position: relative; +} + +/* this is complicated */ +ul.browser_field { + cursor: default; + font-size: 96%; + width: 30%; /* as small as possible */ + line-height: 1.2em; + padding: 0px 0px 0px 3px; + margin: 2px 3px 0px 5px; + overflow: auto; + /*border: 1px solid #9db2b1;*/ + float: left; +} +li { + /*padding: 0px 0px 0px 3px;*/ + margin: 0px; +} +li[dirtype] { + background-position: top left; + background-repeat: no-repeat; + padding-left: 18px; +} + +li[dirtype=parent] { + padding-left: 4px; +} + +li[btype=search] { + padding-bottom: 7px; +} + +li[btype=search]:last-child { + padding-bottom: 0px; +} + +#posslider, #volslider { + width: 150px; +} + +div.slider_main { + width: 100%; + height: 10px; + padding: 0px; + margin: 0px 5px 5px 5px; +} +div.slider_pointer { + position: relative; + left: 0px; + width: 5px; + height: 15px; + margin: -5px -5px 0px -5px; + padding: 0px 0px 5px 0px; +} +p.slider_txt { + margin: 0px 0px 0px 10px; + cursor: default; +} + +#disp_title { + font-size: larger; + padding: 0px; + margin: 15px 2px 0px 0px ; +} + +div.overlay { + position: absolute; + padding: 10px; + display: none; +} + +img.close { + position: absolute; + height: 15px; + width: 15px; + right: 5px; + top: 5px; + opacity: 0.85; +} + +img.menu_button { + height: 30px; + width: 30px; + opacity: 0.80; + display: inline; +} +#debugbox { + position: fixed; + right: 0px; + bottom: 0px; + height: 200px; + width: 150px; + overflow: auto; + z-index: 100; + padding-left: 5px; + border: 1px solid #FFCC00; + background: #FFFF99; + color: black; +} + +img.diritem { + height: 15px; + width: 15px; + position: relative; + right: 2px; +} +div.player_control { + position: fixed; + left: 3px; + right: 3px; + top: 0px; + padding: 0px 10px 0px 10px; + height: 101px; + z-index: 2; +} +div.pc_artist { + width: 400px; + left: 90px; + position: absolute; + padding-top: 5px; +} +div#albumart { + width: 80px; + height: 80px; + /*border: 1px solid red;*/ + margin: 5px 10px 0px 0px; + float: left; +} +div.pc_ci { + position: absolute; + top: 25px; + left: 500px; +} +div.pc_sliders { + position: absolute; + left: 610px; + top: 2px; +} +div.selection_menu { + position: fixed; + top: 106px; + left: 3px; + width: 40px; + padding: 10px 0px 5px 5px; +} +#status_bar { + position: fixed; + left: 300px; + width: 300px; + top: 0px; + display:none; + height: 25px; + z-index: 10; +} +#status_bar_txt { + margin: 0px; + padding: 0px 2px 0px 5px; +} +img.status_working { + float: right; + margin: -15px 5px 0px 0px; + padding: 0px 2px 0px 5px; + display: none; +} +img.status_working[working] { + display: inline; +} + +div.pc_other { + position: absolute; + right: 4px; + top: 0px; + font-size: smaller; + line-height: 13px; +} + +hr.server_settings { + height: 0px; + margin-left: -5px; + margin-right: 0px; +} +div.pc_settings { + position: absolute; + left: 615px; + top: 80px; + z-index: 5; + height: 20px; + font-size: 0.97em; +} +div.settings_header { + cursor: pointer; + margin: 0px 0px 0px 0px; + width: 152px; +} +div.settings_container { + display: none; + height: 200px; + top: 21px; + z-index: 5; + position: absolute; + width: 152px; +} + +img.server_settings { + margin-bottom: -3px; +} +span.server_settings { + cursor: default; +} +.nomargin { + margin: 0px; +} +.nopadding { + padding: 0px; +} +p#settings_content { + margin: 0px; + padding-left: 5px; +} + +input#quickadd { + margin: 0px 0px 0px -10px; + height: 13px; + width: 105px; +} + +div#qa_suggestions { + margin: -3px 0px 0px -10px; + padding: 0px; + min-width: 220px; + z-index: 100; + position: absolute; + display: none; /* yeah I want it, but I don't want *you* to see it! :p */ + min-height: 15px; +} +p#qa_suggestions_txt { + padding: 0px; + margin: 0px; + font-size: 12px; + line-height: 1.3em; +} + +span.qa_element { + display: block; +} + +img.thumbnailart:hover { + cursor: pointer; +} + +div.big_albumart { + position: fixed; + margin: auto; + left: 0px; + right: 0px; + bottom: 0px; + top: 0px; + width: 200px; + height: 50px; + padding: 10px; + z-index: 15; +} +img.big_albumart { + margin: auto; +} +div#sidebar_header { + top: 104px; + position: fixed; + display: none; + height: 20px; + width: 290px; + right: 3px; + z-index: 1; + padding: 0px 0px 0px 5px; +} +div#sidebar_display { + top: 127px; + display: none; + position: fixed; + right: 3px; + padding: 10px 5px 5px 10px; + max-width: 500px; + min-width: 280px; + overflow: auto; + bottom: 0px; + z-index: 1; +} + +#lyrics_refetch_button { + position: absolute; + right: 5px; + font-size: 0.95em; + margin: 0px; +} + +.fakelink { + cursor: pointer; +} + +p.right_small { + text-align: right; + margin: 0px 0px 0px 0px; + padding: 0px 5px 0px 0px; +} +p.browse_type { + display: inline; + margin: 0px 2px 0px 2px; + padding: 0px 5px 0px 5px; + cursor: default; +} + +ul#browser_act_buttons { + width: 95%; + clear: both; + padding: 0px; + margin: 0px 0px 0px 10px; +} + +li.browser_button_add { + margin: 0px; + padding: 0px; + display: inline; + padding-left: 15px; + z-index: 20; +} + +/* only allow changing pos if not first elem */ +li.browser_button_add + li { + position: relative; +} + +li.browser_button_add:hover { + cursor: default; +} + +/* playlist hover, not the main playlist, playlists in the browser */ +div.playlist_hover { + display: none; + float: right; +} + +li:hover div.playlist_hover { + color: red; + display: inline; +} + +div.popup { + position: absolute; + margin-top: -10px; + z-index: 10; + padding: 2px 5px 5px 5px; + display: none; + cursor: default; +} + +.fullscreen { + position: fixed; + left: 0px; + top: 0px; + z-index: 20; + background-color: red; +} + +ul.playlist_popup { + margin: 0px; + padding: 2px; +} +li.playlist_popup { + display: inline; + margin-right: 2px; + cursor: pointer; +} +span.playlist_popup { + cursor: pointer; + margin: 2px; +} + +div.moving_box { + position: relative; + overflow: hidden; + font-size: 0.96em; + cursor: move; +} + +div#streaming_display { + position: absolute; + top: 2px; + margin: 0px; + padding: 0px; + right: 120px; + width: 80px; + line-height: 13px; +} + +div#streaming_display li { + list-style-type: none; + padding-left: 4px; + padding-right: 4px; +} + +div#pagination, div#pagination_options { + position: fixed; + bottom: 0px; + left: 50px; + height: 25px; + padding: 0px; + cursor: default; + max-width: 680px; + display: none; +} +div#pagination_options { + left: 3px; + width: 47px; + display: block; +} +ul#pagination_list { + padding: 0px; + margin: 4px 0px 2px 2px; + overflow: hidden; +} +ul#pagination_list > li { /* li following pagination ul */ + display: inline; + width: 10px; + margin: 2px; + cursor: pointer; +} + +/* just a spacer at the bottom to avoid last row + * disappearing under the pagination list */ +div#pagination_spacer { + height: 25px; +} + +img.pagination_options { + margin: 4px 0px 0px 2px; + padding: 0px; + cursor: pointer; + opacity: 0.80; +} + +span.plse { + display: block; + line-height: 110%; + cursor: pointer; + padding-left: 20px; + background-position: top left; + background-repeat: no-repeat; +} + +li.menuitem { + list-style-type: none; + padding-left: 4px; + padding-right: 4px; + border-top: none; +} + + +ul.recommended { + padding-left: 10px; +} + +ul.recommended > li { + cursor: pointer; +} + +ul.recommended > li > ul { + padding-left: 15px; + display: none; +} +ul.recommended > li[open] > ul { + display: block; +} + +img.menu_button:hover, img.close:hover, img.pagination_options:hover { + opacity: 1.0; +} diff --git a/std/browser.js b/std/browser.js new file mode 100644 index 0000000..4c3f039 --- /dev/null +++ b/std/browser.js @@ -0,0 +1,857 @@ +/* + Pitchfork Music Player Daemon Client + Copyright (C) 2007 Roger Bystrøm + + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +/* this is so broken that I can hardly describe it */ + +var BROWSER_ID = "browser_"; +var BROWSE_FILE = 0; +var BROWSE_ARTIST = 1; +var BROWSE_ALBUM = 2; +var BROWSE_SEARCH = 3; + +var blist; + +var BROWSE_SEARCH_OPTS; + +function BrowserList(browser) { + this.open_name = ""; + this.open = null; + this.first = null; + this.type = BROWSE_FILE; + this.type_name = new Array(); + this.type_name[BROWSE_FILE] = LANG.FILESYSTEM; + this.type_name[BROWSE_ARTIST] = LANG.ARTIST; + this.type_name[BROWSE_ALBUM] = LANG.ALBUM; + this.browser = browser; + this.search = new KeySearch(null); // this is for key-typing in the fields, not the search field + this.search_select = null; // jeje.. search field et al. + this.search_field = null; + this.search_opt = null; + this.act_buttons = null; + this.open_popup = null; + this.eventListener = null; +} + +function BrowserWindow(value) { + this.value = value; + this.next = null; +} + +BrowserList.prototype.get_window_by_value = function (elem) { + var w = this.first; + while(w!=null) { + if(w.value==elem) + return w; + w = w.next; + } + return null; +} +BrowserList.prototype.get_last_window = function() { + var w = this.first; + while(w.next!=null) + w = w.next; + return w; +} + +BrowserList.prototype.get_previous_window = function(node) { + var w = this.first; + while(w!=null&&w.next!=node) + w = w.next; + return w; +} +BrowserList.prototype.get_next_window = function(node) { + var w = this.first; + while(w) { + if(w==node) + return w.next; + w = w.next; + } + return null; +} + +function dirlist_init() { + BROWSE_SEARCH_OPTS = new Array(LANG.ANYTAG, LANG.ARTIST, LANG.TITLE, + LANG.ALBUM, LANG.GENRE, LANG.FILENAME, LANG.COMPOSER, + LANG.PERFORMER, LANG.DATE, LANG.LYRICS); + + remove_children(pl_overlay_write); + send_command("dirlist", dirlist_init_handler); +} + +function dirlist_init_handler(response) { + if(response=="failure") { + show_status_bar(LANG.E_FAILED_LOAD_DIR); + } + else { + var ul; + var ul_tmp = null; + blist = new BrowserList(pl_overlay_write); + var stuff = create_node("div"); + stuff.className = "browse_type_container"; + + for(var i=0; i0) + btn.style.left = 29*i + "%"; + btn.setAttribute("field", i); + btn.appendChild(create_txt(LANG.ADD)); + add_listener(btn, "click", browser_multi_add); + buttons.appendChild(btn); + } + add_listener(buttons, "mousedown", stop_event); + blist.browser.appendChild(buttons); + /* link it up */ + var last = null; + for(var i=0; i0&&resp) { + if(blist.type==BROWSE_FILE) { + var tmp = add_li(entry, "< ..", entry.id + "_parent"); + tmp.setAttribute("dirtype", "parent"); + var name; + + out: + for(var t in resp) { + for(var i in resp[t]) { + name = resp[t][i]; + break out; + } + } + + if(name.substring) { + name = name.substring(0,strip-1); + tmp.setAttribute("diritem", name); + entry.setAttribute("dirlist", name); + } + entry.setAttribute("parent_dir", parent_dir); + } + else { + entry.setAttribute("dirlist", blist.open_name); + entry.setAttribute("parent_dir", parent_dir); + } + } + else { + entry.setAttribute("dirlist", ""); + } + var strip_name = null; + var rtype = null; + for(var type in resp) { + for(var idx in resp[type]) { + name = resp[type][idx]; + rtype = type; + if(type=="filelist") { + if(typeof(name['Title'])!='undefined'&&name["Title"].length) { + strip_name = name["Title"]; + name = name["file"]; + } + else { + name = name["file"]; + strip_name = name.substring(name.lastIndexOf(DIR_SEPARATOR)+1); + } + rtype = "file"; + } + else if(!name.substring) { + continue; + } + else strip_name = name; + + if(type=="directory") { + strip_name = name.substring(strip); + } + else if(type=="file"&&name.lastIndexOf(DIR_SEPARATOR)>=0) { + strip_name = name.substring(name.lastIndexOf(DIR_SEPARATOR)+1); + } + + var l = create_node("li", null, strip_name); + l.setAttribute("diritem", name); + l.setAttribute("dirtype", rtype); + entry.appendChild(l); + if(type=="playlist") + add_playlist_hover(l); + } + } +} + +function add_playlist_hover(l) { + var h = create_node("div"); + h.className = "playlist_hover"; + var img = create_node("img"); + img.src = IMAGE.BROWSER_PLAYLIST_REMOVE; + img.className = "fakelink"; + h.appendChild(img); + add_listener(img, "click", show_remove_playlist); + insert_first(h,l); +} + +function show_remove_playlist(e) { + var t = e.target.parentNode.parentNode; + var name = t.getAttribute("diritem"); + var content = document.createDocumentFragment(); + content.appendChild(create_txt(LANG.CONFIRM_REMOVE + " '" + name + "'?")); + var yes = create_node("span"); + yes.className = "fakelink"; + yes.appendChild(create_txt(" Yes,")); + content.appendChild(yes); + + var no = create_node("span"); + no.className = "fakelink"; + no.appendChild(create_txt(" No")); + content.appendChild(no); + + if(blist.open_popup) + destroy_open_popup(); + var pop = new Popup(document.body, content); + blist.open_popup = pop; + blist.eventListener = add_listener(document.body, "click", destroy_open_popup); + add_listener(yes, "click", function() { remove_playlist(t) ; destroy_open_popup(); }); + add_listener(no, "click", destroy_open_popup); + pop.popup.style.padding = "5px 5px 5px 5px"; + pop.popup.style.position = "absolute"; + pop.popup.style.left = "80px"; + pop.popup.style.top = "500px"; + pop.popup.style.margin = "0 auto"; + pop.show(); +} + +function destroy_open_popup() { + var blist = window.blist; // jic + if(blist.open_popup) { + blist.open_popup.destroy(); + blist.open_popup = null; + } + if(blist.eventListener) { + blist.eventListener.unregister(); + blist.eventListener = null; + } +} + +function remove_playlist(elem) { + var item = elem.getAttribute("diritem"); + send_command("playlist_rm=" + encodeURIComponent(item), remove_playlist_cb); + remove_node(elem); +} + +function remove_playlist_cb(e) { + if(e!="ok") { + show_status_bar(LANG.E_FAILED_ADD); + hide_status_bar(STATUS_DEFAULT_TIMEOUT); + } +} + +function browser_click(e) { + stop_event(e); + var elem = e.target; + + if(!elem||!elem.hasAttribute) return; + + /* test if we got one of the direct children as targets */ + if(elem.parentNode&&elem.parentNode.hasAttribute("diritem")) + elem = elem.parentNode; + + if(elem.hasAttribute&&elem.hasAttribute("diritem")) { + e.stopPropagation(); + var container = elem.parentNode; + blist.search.focus = container; + if(e.ctrlKey||e.metaKey) { + if(is_node_selected(elem)) + unselect_node(elem); + else + select_node(elem); + // safari workaround + container.className = container.className; + } + else if (e.shiftKey) { + var sel_node = find_first_selected_node(container, elem); + unselect_all_nodes(container); + if(sel_node==null) { + select_node(elem); + } + else { + if(elem.hasAttribute("needle")) + select_range(elem, sel_node); + else + select_range(sel_node, elem); + } + if(elem.hasAttribute("needle")) + elem.removeAttribute("needle"); + // safari workaround + container.className = container.className; + } + else { + browser_open_single_item(container, elem, e.detail==2); + } + } +} + +function browser_open_single_item(container, elem, doubleclick) { + unselect_all_nodes(container); + var type; + if(elem) { + type = elem.getAttribute("dirtype"); + select_node(elem); + // safari workaround + container.className = container.className; + } + else { + type = "other"; + } + var b_id = blist.get_window_by_value(container); + + if(!doubleclick) { + if(type=="directory"||type=="artist"||type=="album") { + var diritem = elem.getAttribute("diritem") + + blist.open = b_id.next; + blist.open_name = diritem; + + /* test if already open */ + if(blist.open!=null) { + // this item is already opened + if(blist.open.value.getAttribute("dirlist")==diritem) + return; + } + + /* remove all entries in following rows */ + var tmp = b_id.next; + while(tmp!=null) { + if(tmp.value.hasAttribute("dirlist")) + tmp.value.removeAttribute("dirlist"); + if(tmp.value.hasAttribute("parent_dir")) + tmp.value.removeAttribute("parent_dir"); + remove_children(tmp.value); + tmp = tmp.next; + } + if(blist.type==BROWSE_ARTIST||blist.type==BROWSE_ALBUM){ + var artist, album; + if(blist.open == blist.get_last_window()) { + if(blist.type==BROWSE_ARTIST) { + artist = b_id.value.getAttribute("dirlist"); + album = diritem; + } + else { + album = b_id.value.getAttribute("dirlist"); + artist = diritem; + } + send_command("searchfile&artist=" + encodeURIComponent(artist) + "&album=" + encodeURIComponent(album), browser_click_cb); + return; + } + else if(blist.open==null) { + // don't open + return; + } + // else do as usual... + } + send_command("dirlist=" + encodeURIComponent(blist.open_name) + "&type=" + blist.type, browser_click_cb); + } + else if(type=="parent"||type=="other") { + blist.open = blist.first; + if(type=="parent") + blist.open_name = blist.first.value.getAttribute("parent_dir"); + else blist.open_name = ""; + + var w = blist.first; + if(type=="parent") // || switch type... + while(w!=null&&w!=b_id) + w = w.next; + while(w!=null){ + browser_clear_window(w.value); + w = w.next; + } + + if(blist.first.value.getAttribute("dirlist")!=""||type=="other") { // already at top + send_command("dirlist=" + encodeURIComponent(blist.open_name) + "&type=" + blist.type, browser_click_cb); + } + } + } + else if(doubleclick&&type!="parent") { + var diritem = elem.getAttribute("diritem") + blink_node(elem, DEFAULT_BLINK_COLOR); + if(elem.getAttribute("dirtype")=="playlist") { + send_command("playlist_load=" + encodeURIComponent(diritem), null, LANG.WAIT_ADDING); + } + else if((blist.type==BROWSE_ARTIST||blist.type==BROWSE_ALBUM)&&elem.getAttribute("dirtype")!="file") { + var artist, album; + if(b_id == blist.first.next) { + if(blist.type==BROWSE_ARTIST) { + artist = b_id.value.getAttribute("dirlist"); + album = diritem; + } + else { + album = b_id.value.getAttribute("dirlist"); + artist = diritem; + } + send_command("searchadd&artist=" + encodeURIComponent(artist) + "&album=" + + encodeURIComponent(album), browser_add_cb, LANG.WAIT_ADDING); + } + else if(b_id==blist.first) { + var cmd = "searchadd&"; + if(blist.type==BROWSE_ARTIST) { + cmd+="artist="; + } + else { + cmd+="album="; + } + cmd += encodeURIComponent(diritem); + send_command(cmd, browser_add_cb, LANG.WAIT_ADDING); + } + } + else { + // else do as usual... + send_command("add=" + encodeURIComponent(diritem), browser_add_cb, LANG.WAIT_ADDING); + } + } +} + +function browser_click_cb(response) { + var strip = 0; + var parent_dir = null; + if(blist.open==null) { + // got to move it + var last = blist.get_last_window(); + var first = blist.first; + var prev = blist.get_previous_window(last); + parent_dir = prev.value.getAttribute("dirlist"); + var tmp = first.value; + + while(tmp.hasChildNodes()) { + remove_node(tmp.lastChild); + } + remove_node(tmp); + insert_after(tmp, last.value); + last.next = first; + blist.first = first.next; + first.next = null; + blist.open = blist.get_last_window(); + } + else if (blist.open==blist.first) { + parent_dir = blist.first.value.getAttribute("parent_dir"); + if(parent_dir) + parent_dir = parent_dir.substring(0,parent_dir.lastIndexOf(DIR_SEPARATOR)); + + var first = blist.first; + var last = blist.get_last_window(); + var prev = blist.get_previous_window(last); + var tmp = last.value; + while(tmp.hasChildNodes()) + remove_node(tmp.lastChild); + remove_node(tmp); + insert_before(tmp, first.value) + blist.first = last; + last.next = first; + prev.next = null; + blist.open = blist.first; + } + var b = blist.open; + var prev = blist.get_previous_window(b); + if(parent_dir==null&&prev!=null) { + parent_dir = prev.value.getAttribute("dirlist"); + } + else if(parent_dir==null) { + parent_dir = ""; + } + + if(blist.open_name.length>0) + strip = blist.open_name.length + 1; + + browser_fill_entry(b.value, response, strip, parent_dir); + blist.open_name = ""; + blist.open = null; +} + +function browser_add_cb(response) { + if(response!="ok") { + show_status_bar(LANG.E_FAILED_ADD); + hide_status_bar(STATUS_DEFAULT_TIMEOUT); + } +} + +function pl_overlay_open_cb(height) { + var elem = null; + var i=0; + var w = blist.first; + while(w != null ) { + /* this makes room for the top (filesys, artist, search et al.) and bottom (add etc) */ + w.value.style.height = (height - 40)+ "px"; + w = w.next; + } + blist.search.listener = keyboard_register_listener(browser_key_search, KEYBOARD_MATCH_ALL, KEYBOARD_NO_KEY, true); + // TODO, maybe set broswer_focus +} + +function pl_overlay_close_cb() { + keyboard_remove_listener(blist.search.listener); +} + +function browser_key_search(e) { + if(!blist.search.focus) { + return; + } + var clear_time = 1000; + + var c = blist.search.focus.childNodes; + + if(e.keyCode==38||e.keyCode==40||e.keyCode==37||e.keyCode==39) { + var it = null; + // find first selected node + for(var i=0; i < c.length; i++) { + if(is_node_selected(c[i])) { + it = c[i]; + break; + } + } + if(it) { + var win = null; + if(e.keyCode==40&&it.nextSibling) { + it = it.nextSibling; + } + else if(e.keyCode==38&&it.previousSibling) { + it = it.previousSibling; + } + else if(e.keyCode==37) { // left + // todo + //win = blist.get_previous_window(blist.get_window_by_value(blist.search.focus)); + it = null; + } + else if(e.keyCode==39) { // right + // todo + //win = blist.get_next_window(blist.get_window_by_value(blist.search.focus)); + it = null; + } + else it = null; + if(it) { + unselect_all_nodes(blist.search.focus); + select_node(it); + scrollIntoView(it, false); + blist.search.item = it; + } + else if (win) { + win = win.value; + blist.search.focus = win; + unselect_all_nodes(win); + select_node(win.firstChild) + } + blist.search.term = ""; + } + } + else { + var s = blist.search.term + String.fromCharCode(e.keyCode); + blist.search.term = s = s.toLowerCase(); + + for(var i=0; i0) { + send_command("metasearch=" + blist.search_select.value + "&s=" + encodeURIComponent(blist.search_field.value), + browser_search_field_keyhandler_cb, LANG.WAIT_SEARCHING); + } + else { + remove_children(blist.first.value); + } + } +} + +function browser_search_field_keyhandler_cb(resp) { + var dst = blist.first.value; + remove_children(dst); + if(typeof(resp)!='undefined'&&resp!="failed") { + for(var i=0; i0) { + url+="&pmax=" + pagination.max + "&page=" + pagination.page; + //debug("pmax: " + pagination.max + ", page: " + pagination.page); + } + } + + if(send_command_rm_status) { + hide_status_bar(); + send_command_rm_status = false; + } + + http.onreadystatechange = function() { + if(http.readyState==4) { + var resp = null; + if(http.responseText&&(resp = evaluate_json(http.responseText))) { + if(resp['connection']&&resp['connection']=="failed") { + last_update = get_time(); + show_status_bar(LANG.E_CONNECT); + send_command_rm_status = true; + try { + result_callback("failed"); + } + catch(e) {} + return; + } + } + else { + show_status_bar(LANG.E_INVALID_RESPONSE); + send_command_rm_status = true; + last_update = get_time(); + try { + result_callback("failed"); + } + catch(e) {} + return; + } + + if(http.status==200) { + var res = resp['result']; + var stat = resp["status"]; + var plchanges = resp["plchanges"]; + var has_plchanges = plchanges && stat && playing.pl_version != stat.playlist; + + if(res&&result_callback) { + result_callback(res); + } + + if(stat) { + current_status_handler(stat, has_plchanges); + last_update = get_time(); + } + + if(has_plchanges) { + playing.pl_version = stat.playlist; + plchanges_handler3(plchanges, stat.playlistlength); + /* the currently playing song might have changed if it's a stream */ + if(playing.is_stream) { + request_song_info(); + } + } + + /* don't remove if there's no message or a timer */ + if(status_msg&&!status_bar.timer) + hide_status_bar(); + } + else { + if(result_callback) { + result_callback("server operation failed"); + } + show_status_bar(LANG.NO_RESPONSE); // maybe add a 10 second delay here and reconnect! + send_command_rm_status = true; + } + } + } + + if(status_msg) { + show_status_bar(status_msg, true); + } + + + if(do_post) { + http.open("POST", url); + if(form_urlencoded) { + http.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + http.send(do_post); + } + else { + http.open("GET", url); + http.send(null); + } +} + +function update_callback(res) { + var time = update_delay; + if(res=="failed") + time = problem_update_delay*update_delay; + last_update_cmd_id = setTimeout(need_update, time); +} + +function reschedule_update_now() { + clearTimeout(last_update_cmd_id); + // make sure it get's run immidiately + last_update_cmd_id = setTimeout(need_update, 0, -update_delay); +} + +function need_update(delay) { + if(!delay) + delay = 0; + var now = get_time(); + if(now>last_update+update_delay+delay-20) { // giving it a little slack + send_command("ping", update_callback); + } + else { + delay = last_update+update_delay-now; + if(delay<10) + delay = 10; + + last_update_cmd_id = setTimeout(need_update, delay); + } +} + +/* stop program */ +function handle_fatal_error(txt) { + show_status_bar(txt); + clearTimeout(last_update_cmd_id); +} + +/* current status handler, info is stats + * if has_plchanges it is assumed the data also carries plchanges + * which will handle resizing of the playlist */ +function current_status_handler(info, has_plchanges) { + if(!info) { + // something is wrong + set_slider_pos(possliderid, 0); + return; + } + + var tmp = info.playlistlength; + if((tmp = parseInt(tmp))>=0) { + if(playing.pl_size!=tmp) { + playing.pl_size = tmp; + if(pagination.max>0) { + // make sure size fits inside what we have as number of pages + var s = pagination.max * (pagination.pages); + if(tmp < s-pagination.max || tmp>=s) { + pagination_update_list(tmp); + } + if(!has_plchanges) { + playlist_resize(tmp); + } + } + else if(!has_plchanges) { // if it doesn't carry plchanges we have to do it here + playlist_resize(tmp); + } + } + } + + tmp = info.updating_db; + if(tmp) { + tmp = parseInt(tmp); + if(playing.update!=tmp) { + playing.update = tmp; + show_status_bar(LANG.WAIT_UPDATING_DB, true); + } + } + else if(playing.update) { + playing.update = false; + hide_status_bar(); + } + + var state = info["state"]; + var volume = info[ "volume"]; + if(volume!=null&&volume!=playing.volume) { + set_slider_pos(volsliderid, volume); + playing.volume = volume; + } + + playing.repeat = info.repeat; + playing.random = info.random; + playing.xfade = info.xfade; + + if(state!="stop") { + + var pos = info["time"].split(":"); + set_slider_pos(possliderid, (pos[0]*100)/pos[1]); + playing.length = pos[1]; + + /* complex, but seems to be somewhat better in firefox */ + var tmp = info.bitrate; + if(tmp&&playing.bitrate!=tmp) { + var disp_info = playing.disp_info + if(bitrate_txt_node==null) { + bitrate_txt_node = document.createTextNode(LANG.BITRATE); + disp_info.appendChild(bitrate_txt_node); + if(disp_info.normalize) + disp_info.normalize(); + bitrate_txt_node = disp_info.firstChild.splitText(LANG.BITRATE.length); + } + var rep = document.createTextNode(tmp); + disp_info.replaceChild(rep, bitrate_txt_node); + bitrate_txt_node = rep; + playing.bitrate = tmp; + } + + var postxt = get_slider_txt(possliderid); + if(postxt) { + if(position_txt_node==null) { + position_txt_node = postxt.firstChild.splitText(LANG.POSITION.length); + } + var rep = create_txt(format_playing_time(pos[0], pos[1])); + postxt.replaceChild(rep, position_txt_node); + position_txt_node = rep; + } + } + else if(playing.state!="stop") { // state == stop and last state wasn't stop + set_slider_pos(possliderid, 0); + var disp_info = playing.disp_info; + var rep = document.createTextNode("0"); + var postxt = get_slider_txt(possliderid); + if(bitrate_txt_node&&bitrate_txt_node!=null) { + disp_info.replaceChild(rep, bitrate_txt_node); + bitrate_txt_node = rep; + } + playing.bitrate = 0; + rep = document.createTextNode(""); + if(position_txt_node&&position_txt_node!=null) { + postxt.replaceChild(rep, position_txt_node); + position_txt_node = rep; + } + } + if(state!=playing.state) { + playing.state = state; + var bt = playing.pp_button; + if(state=="play") { + bt.src = IMAGE.BUTTON_PAUSE; + if(typeof(window.streaming_try_autoplay)=='function') + streaming_try_autoplay(); + } + else { + bt.src = IMAGE.BUTTON_PLAY; + if(typeof(window.streaming_try_autostop)=='function') + streaming_try_autostop(); + } + } + + if(typeof info.error != 'undefined' || playing.error) { + if(playing.error && get_time() - 10000 > playing.error_time) { + playing.error = false; + hide_status_bar(); + send_clear_error(); + } + else if(typeof info.error != 'undefined' && playing.error != info.error) { + playing.error = info.error; + playing.error_time = get_time(); + show_status_bar("MPD error: " + playing.error); + } + } + + var c_play = info.songid; + if(typeof(c_play)=='undefined') { + playing.id = -1; + playing.pos = -1; + request_song_info(); + select_playing_song(); + } + else if(c_play!=playing.id) { + playing.id = c_play; + playing.pos = parseInt(info.song); + select_playing_song(); + if(pagination_is_following()) { + playlist_scroll_to_playing(); + } + /* this is last because it needs the id */ + request_song_info(); + } + else if(playing.pos!=info.song) { + playing.pos = parseInt(info.song); + } +} + +function format_playing_time(pos, total) { + if(playing.time_dtype==playing.TIME_REMAINING) { + return convert_minsec(pos-total) + "/" + convert_minsec(total); + } + else { // dtype == playing.TIME_ELAPSED || something wrong + return convert_minsec(pos) + "/" + convert_minsec(total); + } +} + +function request_song_info() { + /* clean up */ + if(playing.id<0) { + remove_children(playing.disp_artist); + remove_children(playing.disp_title); + remove_children(playing.disp_album); + if(playing.albumart) + remove_children(playing.albumart); + playing.artist = ""; + playing.title = ""; + playing.album = ""; + playing.image = ""; + playing.asin = ""; + playing.length = ""; + playing.is_stream = false; + set_playing_title(); + } + else { + /* or update */ + send_command("currentsong", update_current_song, false, true); + } +} + +function update_current_song(info) { + var artist = info[ "Artist"]; + var title = info["Title"]; + var album = info[ "Album"]; + var a = playing.disp_artist; + var t = playing.disp_title; + var alb = playing.disp_album; + var new_thumb = false; + + if(typeof(title)=='undefined') + title = ""; + if(typeof(album)=='undefined') + album = ""; + if(typeof(artist)=='undefined') + artist = ""; + + if(artist!=playing.artist) { + playing.artist = artist; + new_thumb = true; + remove_children(a); + a.appendChild(create_txt(artist)); + } + if(playing.album != album) { + playing.album = album; + new_thumb = true; + remove_children(alb); + alb.appendChild(create_txt(album)); + } + + if(typeof(info['file'])!='undefined') { + var f = info['file']; + if(f&&f.indexOf("http://")==0) + playing.is_stream = true; + else playing.is_stream = false; + } + + remove_children(t); + playing.title = title; + + if(title==null||title=="") { + title = info["file"]; + if(title) + title = title.substring(title.lastIndexOf(DIR_SEPARATOR)+1); + } + t.appendChild(create_txt(title)); + + set_playing_title(artist, title); + + if(new_thumb&&typeof(window.request_thumbnail) == 'function') { + setTimeout(request_thumbnail, 1); + } +} + +function set_playing_title(artist, title) { + if(typeof(artist)=='undefined'||artist==null) + artist = ""; + if(typeof(title)=='undefined'||title==null) + title = ""; + + var wt = "Pitchfork MPD Client"; + if(artist.length||title.length) { + wt = artist; + if(title.length) + wt += " - " + title; + } + document.title = wt; +} + +function volume_adjust(vol) { + send_command("volume=" + parseInt(vol)); +} + +function position_adjust(pos) { + send_command("position=" + parseInt((pos* parseInt(playing.length))/100) + "&id=" + playing.id); +} + +function convert_minsec(sec) { + var min = parseInt(sec/60); + var s = Math.abs(sec%60); + return (sec<0&&min==0?"-" + min:min) + ":" + (s<10?"0" + s:s); +} + +function buttons_init() { + + /* player control */ + var elem = document.getElementById('pp_button'); + elem.src = IMAGE.BUTTON_PLAY; + add_listener(elem, "click", send_play_pause); + if(window.stop_button) { + elem = document.getElementById('stop_button'); + elem.style.display = ""; + elem.src = IMAGE.BUTTON_STOP; + add_listener(elem, "click", send_stop_cmd); + elem.parentNode.style.marginLeft = "-15px"; + } + + elem = document.getElementById("next_button"); + elem.src = IMAGE.BUTTON_NEXT; + add_listener(elem, "click", send_next_song); + elem = document.getElementById("previous_button"); + elem.src = IMAGE.BUTTON_PREVIOUS; + add_listener(elem, "click", send_previous_song); + + /* left menu buttons */ + elem = document.getElementById("open_directory_button"); + elem.src = IMAGE.MENU_ITEM_DIRECTORY; + add_listener(elem, "click", open_pl_overlay); + elem = document.getElementById("crop_items_button"); + elem.src = IMAGE.MENU_ITEM_CROP; + add_listener(elem, "click", remove_songs_event); + elem = document.getElementById("remove_items_button"); + elem.src = IMAGE.MENU_ITEM_REMOVE; + add_listener(elem, "click", remove_songs_event); + + /* server settings */ + elem = document.getElementById("settings_header"); + add_listener(elem, "click", open_close_settings); + add_listener(elem, "mousedown", stop_event); + + /* playlist */ + elem = document.getElementById("playlist_add"); + add_listener(elem, "click", playlist_add_button); + elem = document.getElementById("playlist_save"); + add_listener(elem, "click", playlist_save_button); + + /* status bar */ + elem = document.getElementById("status_bar_img"); + elem.src = IMAGE.WORKING; + + /* streaming if applicable */ + elem = document.getElementById("streaming_open"); + if(elem) { + add_listener(elem, "click", streaming_open); + } + + /* time display */ + elem = get_slider_txt(possliderid); + if(elem) { + add_listener(elem, "click", change_pos_dtype); + } + + // pagination + elem = document.getElementById("pagination"); + if(elem) { + add_listener(elem, "click", pagination_change_page); + add_listener(elem, "mousemove", pagination_scroll_view); + add_listener(elem, "mouseout", pagination_scroll_stop); + } + elem = document.getElementById("pagination_jump_current"); + if(elem) { + elem.src = IMAGE.JUMP_CURRENT; + add_listener(elem, "click", playlist_scroll_to_playing); + elem.title = LANG.JUMP_CURRENT; + } + elem = document.getElementById("pagination_follow_current"); + if(elem) { + add_listener(elem, "click", pagination_toggle_following); + } + + elem = document.getElementById("playlist_search_btn"); + if(elem) { + add_listener(elem, "click", plsearch_open); + } + + // set it to nothing selected + pl_selection_changed(false); +} + +function send_play_pause(e) { + stop_event(e); + var act = "toggle"; + if(playing.state=="stop") { + act = "play"; + if(playing.id>=0) + act+="&id=" + playing.id; + } + send_command("act=" + act); +} +function send_stop_cmd(e) { + stop_event(e); + send_command("act=stop"); +} +function send_next_song(e) { + stop_event(e); + send_command("act=next"); +} +function send_previous_song(e) { + stop_event(e); + send_command("act=previous"); +} + +function send_update_db_cmd(e) { + stop_event(e); + send_command("updatedb"); +} + +function send_clear_error() { + send_command("clearerror", false, false, true); +} + +function remove_songs_event(e) { + var inv = 'crop_items_button'==e.target.id; + var sel = get_pl_selection_range(inv); + if(!inv) { + /* nothing selected if we just removed it, + * or at least in theory */ + pl_selection_changed(false); + } + if(sel.length==0) { + return; + } + send_command("remove=" + encodeURIComponent(sel), remove_songs_cb, LANG.WAIT_REMOVING); +} + +function remove_songs_cb(response) { + if(response!="ok") { + show_status_bar(LANG.E_REMOVE); + hide_status_bar(STATUS_DEFAULT_TIMEOUT); + } +} + +function open_close_settings(e) { + var sc = document.getElementById('settings_container'); + /* close */ + if(sc.style.display == "block") { /* not perfect but I think there's enough vars at the top */ + sc.firstChild.style.display = "none"; + remove_listener(sc, "mousedown", stop_event); + remove_listener(sc, "click", settings_click_handler); + setTimeout(open_close_settings_timer, settings_time, sc, false, new Array(0, 200)); + } + else { + /* open */ + var dst_height = sc.scrollHeight; // || innerHeight + sc.style.height = "50px"; + sc.style.overflow = "hidden"; + remove_children(sc.firstChild); + sc.firstChild.style.display = "none"; + sc.firstChild.appendChild(document.createTextNode("Loading..")); + sc.style.display = "block"; + add_listener(sc, "mousedown", stop_event); + add_listener(sc, "click", settings_click_handler); + setTimeout(open_close_settings_timer, settings_time, sc, true, new Array(0, 200)); + send_command("outputs", open_settings_cb); + } +} + +function open_close_settings_timer(sc, isOpening, heights) { + var ad = 50; + if(isOpening) + heights[0] += ad; + else heights[1] -=ad; + + if(heights[0]0) { + status_bar.timer = setTimeout(hide_status_bar, time*1000, false); + } + else { + remove_children(status_bar.txt); + if(browser_is_opera()) { + opera_quirk_set_display_none(status_bar.main); + } + else { + status_bar.main.style.display = "none"; + } + status_bar.timer = false; + } +} + +function setup_keys() { + keyboard_init(); + keyboard_register_listener(send_play_pause, "k", KEYBOARD_NO_KEY, true); + keyboard_register_listener(send_previous_song, "j", KEYBOARD_NO_KEY, true); + keyboard_register_listener(send_next_song, "l", KEYBOARD_NO_KEY, true); + keyboard_register_listener(quickadd_focus, "s", KEYBOARD_CTRL_KEY|KEYBOARD_SHIFT_KEY, true); + /* for browsers where ctrl+shift does something else */ + keyboard_register_listener(quickadd_focus, "s", KEYBOARD_CTRL_KEY|KEYBOARD_ALT_KEY, true); + keyboard_register_listener(playlist_scroll_to_playing, " " , KEYBOARD_NO_KEY, true); + + var qa = document.getElementById('quickadd'); + qa.setAttribute("autocomplete", "off"); + add_listener(qa, "keydown", quickadd_keydown_handler); // stop it from getting to the keylisteners! + add_listener(qa, "keyup", quickadd_keyup_handler); + add_listener(qa, "focus", quickadd_focus); + add_listener(qa, "blur", quickadd_blur); + qa.title = LANG.QUICK_ADD + " [Ctrl+Shift+S]"; +} + +function change_pos_dtype() { + playing.time_dtype++; + if(playing.time_dtype>=playing.TIME_END) { + playing.time_dtype = 0; + } + setting_set("time_dtype", playing.time_dtype); +} + diff --git a/std/keyboard.js b/std/keyboard.js new file mode 100644 index 0000000..b24c8de --- /dev/null +++ b/std/keyboard.js @@ -0,0 +1,112 @@ +/* + Pitchfork Music Player Daemon Client + Copyright (C) 2007 Roger Bystrøm + + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +var RETURN_KEY_CODE = 13; // enter... + +var keyboard_listeners; +var keyboard_elem_count = 0; + +var KEYBOARD_NO_KEY = 0; +var KEYBOARD_CTRL_KEY = 1; +var KEYBOARD_ALT_KEY = 1 << 1; +var KEYBOARD_META_KEY = 1 << 2; +var KEYBOARD_SHIFT_KEY = 1 << 3; + +var KEYBOARD_MATCH_ALL = ""; + + +function KeyListener(callback_function, match_char, extra_keys, active) { + this.callback = callback_function; + this.matchChar = match_char; + this.extra_keys = extra_keys; + this.active = active; +} + +function keyboard_init() { + document.addEventListener("keydown", keyboard_input_handler_cb, false); + keyboard_listeners = new HashMap(); + if(document.body.focus) + document.body.focus(); +} + +function keyboard_register_listener(callback_func, match_char, extra_keys, active) { + var kid = keyboard_elem_count++; + + if(match_char==KEYBOARD_MATCH_ALL) { + match_char = -1; + keyboard_listeners.insert(kid, new KeyListener(callback_func, match_char, extra_keys, active)); + } + else { + match_char = match_char.toUpperCase().charCodeAt(0); + keyboard_listeners.put(kid, new KeyListener(callback_func, match_char, extra_keys, active)); + } + + return kid; +} + +function keyboard_set_active(id, active) { + try { + keyboard_listeners.get(id).active = active; + } + catch(e) { + debug("setting kb " + id + " to " + active + " failed"); + } +} + +function keyboard_remove_listener(key) { + keyboard_listeners.remove(key); +} + +function keyboard_input_handler_cb(ev) { + var key = -1; + + //debug("got a key: " + (ev.shiftKey?"shift ":"") + (ev.ctrlKey?"ctrl ":"") + (ev.altKey?"alt ":"") + (ev.metaKey?"meta ":"")); + + if(ev.which&&ev.which>0) + key = ev.which; + else key = ev.keyCode; // hmm.... + + // this is ctrl+some key, yank it up by 64 + if(ev.ctrlKey&&key>0&&key<27) { + key+=64; + } + + // just match a-z and some special chars (search: ascii key table) + if(key<32||key>90) + return true; + + var num = KEYBOARD_NO_KEY; + if(ev.ctrlKey) + num += KEYBOARD_CTRL_KEY; + if(ev.shiftKey) + num += KEYBOARD_SHIFT_KEY + if(ev.altKey) + num += KEYBOARD_ALT_KEY; + if(ev.metaKey) + num += KEYBOARD_META_KEY; + var it = keyboard_listeners.iterator(); + var l = null; + while((l = it.next())!=null) { + if(l.active && num == l.extra_keys && (l.matchChar == key||l.matchChar<0)) { + l.callback(ev); + stop_event(ev); + return false; + } + } + return true; +} diff --git a/std/metadata.js b/std/metadata.js new file mode 100644 index 0000000..36cfd1f --- /dev/null +++ b/std/metadata.js @@ -0,0 +1,514 @@ +/* + Pitchfork Music Player Daemon Client + Copyright (C) 2007 Roger Bystrøm + + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +var ALBUMART_NO_COVER = "../images/gmpc-no-cover.png"; +var ALBUMART_NO_COVER_THUMB = "../images/gmpc-no-cover-thumb.png"; + +function metadata_open_default() { + sidebar_open("metadata"); + metadata_open_lyrics(); // default opening is lyrics +} + +function metadata_init() { + + var elem = document.getElementById('metadata_open'); + if(elem) + add_listener(elem, "click", metadata_open_default); + elem = document.getElementById("metadata_close"); + if(elem) + add_listener(elem, "click", sidebar_close); + + elem = document.getElementById("metadata_open_lyrics"); + if(elem) + add_listener(elem, "click", metadata_open_lyrics); + elem = document.getElementById('metadata_open_review'); + if(elem) + add_listener(elem, "click", metadata_open_review); + elem = document.getElementById('metadata_open_description'); + if(elem) + add_listener(elem, "click", metadata_open_description); + if(sidebar.display) + add_listener(sidebar.display, "click", metadata_lyrics_refetch); + elem = document.getElementById("recommendation_open"); + if(elem) + add_listener(elem, "click", recommend_open); +} + +function parse_html_tags(txt) { + txt = txt.replace(/<i>/gi, ""); + txt = txt.replace(/<\/i>/gi, ""); + txt = txt.replace(/\n/g, "
\n"); + txt = txt.replace(/<p>/gi, "

\n"); + txt = txt.replace(/<\/p>/gi, ""); + return txt; +} + +function metadata_set_content(content) { + /* don't display the content if right view is not open */ + if(sidebar.open_view=="metadata") + sidebar.set_content(content); +} + +function metadata_open_lyrics(e, force) { + sidebar.say_loading(); + sidebar.last_metadata_request = "lyrics"; + if(playing.title&&playing.artist&&playing.title.length>0&&playing.artist.length>0) { + var http = new XMLHttpRequest(); + http.open("GET", "metadata.php?lyric" + (force?"&force":""), true); + http.onreadystatechange = function() { + if(http.readyState==4&&sidebar.last_metadata_request=="lyrics") { + var lyrics = create_fragment(); + if(http.responseText=="nocachedir") { + lyrics.appendChild(create_txt(LANG.E_MISSING_CACHE)); + } + + if(http.responseXML) { + var result = get_tag(http.responseXML, "result"); + if(result=="failed") { + lyrics.appendChild(create_txt(LANG.E_LYRICS_FAILURE)); + } + else if(result=="notfound") { + lyrics.appendChild(create_txt(LANG.E_LYRICS_NOT_FOUND)); + var url = get_tag(http.responseXML, "url"); + if(url) { + lyrics.appendChild(metadata_build_lyricwiki_url(url, LANG.ADD)); + } + } + else { + var refetch = create_node("p"); + refetch.appendChild(create_txt("["+LANG.REFETCH+"]")); + refetch.className = "fakelink"; + refetch.id = "lyrics_refetch_button"; + lyrics.appendChild(refetch); + var title = create_node("span"); + title.style.fontSize = "larger"; + title.style.marginRight = "60px"; + title.appendChild(create_txt(get_tag(http.responseXML, "title"))); + var artist = create_node("span"); + artist.style.fontSize = "100%"; + artist.appendChild(create_txt(get_tag(http.responseXML, "artist"))); + + lyrics.appendChild(title); + lyrics.appendChild(create_node("br")); + lyrics.appendChild(artist); + lyrics.appendChild(create_node("br")); + lyrics.appendChild(create_node("br")); + + var song = get_tag(http.responseXML, "lyric"); + if(song&&song.length) { + add_string_with_br(lyrics, song); + } + else { + lyrics.appendChild(create_txt(LANG.E_MISSING_LYRICS)); + } + var url = get_tag(http.responseXML, "url"); + if(url) { + lyrics.appendChild(metadata_build_lyricwiki_url(url, LANG.EDIT)); + } + } + } + else { + lyrics.appendChild(create_txt(LANG.E_COMM_PROBLEM)); + } + metadata_set_content(lyrics); + } + } + http.send(null); + } + else { + metadata_set_content(create_txt(LANG.E_MISSING_AS_NAME)); + } +} + +function metadata_build_lyricwiki_url(url, txt) { + var a = create_node("a"); + a.href = url.replace(/&/i, "&"); + a.title = sprintf(LANG.LYRICWIKI_LYRIC, txt); + a.target = "_new"; + a.appendChild(create_txt("[" + txt + "]")); + a.style.display = "block"; + a.style.marginTop = "10px"; + return a; +} + +function metadata_lyrics_refetch(e) { + if(e&&e.target&&e.target.id=="lyrics_refetch_button") { + stop_event(e); + metadata_open_lyrics(e, true); + } +} + +function metadata_open_review() { + request_review_desc(0); +} +function metadata_open_description() { + request_review_desc(1); +} + +function request_review_desc(type) { + sidebar.say_loading(); + sidebar.last_metadata_request = "review"; + if(playing.album&&playing.artist&&playing.album.length>0&&playing.artist.length>0) { + var http = new XMLHttpRequest() + var album = playing.album; + var artist = playing.artist; + http.open("GET", "metadata.php?review&artist=" + encodeURIComponent(playing.artist) + + "&album=" + encodeURIComponent(playing.album), true); + http.onreadystatechange = function() { + if(http.readyState==4&&sidebar.last_metadata_request=="review") { + if(get_tag(http.responseXML, "result")) { + metadata_set_content(LANG.E_GET_INFO); + return; + } + + var content = create_fragment(); + var tmp = ""; + var stuff = ""; + if(type==0) { + tmp = LANG.ALBUM_REVIEW; + stuff = get_tag(http.responseXML, "review"); + } + else if(type==1) { + tmp = LANG.ALBUM_DESC; + stuff = get_tag(http.responseXML, "desc"); + } + if(stuff) { + if(!playing.asin) { + var asin = get_tag(http.responseXML, "asin"); + if(asin) + playing.asin = asin; + } + tmp+=sprintf(LANG.ALBUM_AA_NAME, album, artist); + tmp = create_node("span", null, tmp); + tmp.style.fontSize = "larger"; + content.appendChild(tmp); + content.appendChild(create_node("br")); + content.appendChild(create_node("br")); + tmp = create_node("span"); + tmp.innerHTML = parse_html_tags(stuff); + content.appendChild(tmp); + tmp = create_node("a"); + tmp.appendChild(create_txt(LANG.NT_AMAZON)); + if(build_amazon_link(tmp)) { + content.appendChild(create_node("br")); + content.appendChild(create_node("br")); + content.appendChild(tmp); + } + } + else { + content.appendChild(create_txt(sprintf(LANG.E_NOT_FOUND, tmp.toLowerCase()))); + } + metadata_set_content(content); + } + } + http.send(null); + } + else { + metadata_set_content(create_txt(LANG.E_MISSING_AA_NAME)); + } +} + + +/* album art */ +function request_thumbnail() { + var albumart = document.getElementById("albumart"); + var rartist = playing.artist; + var ralbum = playing.album; + + remove_children(albumart); + if(playing.album&&playing.artist&&ralbum.length>0&&rartist.length>0) { + var http = new XMLHttpRequest() + http.open("GET", "metadata.php?cover&artist=" + encodeURIComponent(rartist) + + "&album=" + encodeURIComponent(ralbum), true); + http.onreadystatechange = function() { + if(http.readyState==4) { + try { + if(http.responseText=="nocachedir") { + debug(LANG.E_MISSING_CACHE); // TODO + return; + } + if(get_tag(http.responseXML, "result")) { + // something's not okay.... TODO + return; + } + + /* test if we're still wanted */ + if(rartist != playing.artist || ralbum != playing.album) + return; + + var thumb = get_tag(http.responseXML, "thumbnail"); + var url = thumb; + + if(!url) { + url = ALBUMART_NO_COVER_THUMB; + } + else { + url = ALBUMART_URL + encodeURIComponent(thumb); + } + + playing.image = get_tag(http.responseXML, "image"); + + var img = create_node("img"); + img.src = url; + add_listener(img, "click", show_big_albumart); + img.className = "thumbnailart"; + if(albumart.hasChildNodes()) { + /* probably just one, but... */ + var imgs = albumart.getElementsByTagName("img"); + for(var i=0; i0) { + var a = create_node("a"); + build_amazon_link(a); + a.appendChild(create_txt(LANG.NT_AMAZON)); + a.style.color = "white"; + a.style.textTransform = "none"; + a.title = LANG.ALBUM_AMAZON; + txt.appendChild(a); + } +} + +function albumart_resize(disp, img) { + var width = 500; + var height = 500; + img.style.visibility = "hidden"; + img.style.display = "inline"; + var got_real = true; + if(img.height) + height = img.height; + else got_real = false; + if(img.width) + width = img.width; + else got_real = false; + disp.style.height = (height+ 30) + "px"; + disp.style.width = (width + 0) + "px"; + img.style.visibility = "visible"; + return got_real; +} + +function build_amazon_link(a) { + if(playing.asin&&playing.asin.length>0) { + a.href = "http://www.amazon.com/gp/product/" + playing.asin + "?ie=UTF8&tag=httppitchfork-20" + + "&linkCode=as2&camp=1789&creative=9325&creativeASIN=" + playing.asin; + a.target = "_new"; + return true; + } + else { + return false; + } +} + +function recommend_init() { + var tmp = create_node("p"); + tmp.className = "nomargin"; + tmp.appendChild(create_txt(LANG.RECOMMEND_RECOMMENDATIONS)); + var close = create_node("span", null, " ["+LANG.CLOSE+"]"); + add_listener(close, "click", sidebar_close); + close.className = "fakelink"; + tmp.appendChild(close); + sidebar.add_view("recommend", tmp, 20); +} + +function recommend_open() { + sidebar_open("recommend"); + sidebar.set_content(create_txt(LANG.WAIT_LOADING)); + recommend_fetch_data(); +} + +function recommend_set_content(c) { + if(sidebar.open_view == "recommend") + sidebar.set_content(c); +} + +function recommend_fetch_data() { + if(!playing.pl_size) { + recommend_set_content(LANG.RECOMMEND_EMPTY_PLAYLIST); + return; + } + var http = new XMLHttpRequest() + http.open("GET", "metadata.php?plrecommend", true); + http.onreadystatechange = function() { + if(http.readyState==4) { + + var result = http.responseXML.getElementsByTagName("result")[0]; + if(!result || result.textContent=="failed" || !result.hasChildNodes()) { + recommend_set_content(create_txt(LANG.E_COMM_PROBLEM)); + return; + } + var exists = create_node("ul"); + var others = create_node("ul"); + exists.className = "recommended"; + others.style.paddingLeft = "10px"; + + add_listener(exists, "click", recommend_toggle_open); + add_listener(exists, "mousedown", stop_event); + + result = result.childNodes; + for(var i=0; i < result.length; i++) { + var a = result[i]; + var artist = get_tag(a, "name"); + var albums = a.getElementsByTagName( "album")[0]; + + if(albums&&albums.hasChildNodes()) { + var list = create_node("li", null, artist); + var slist = create_node("ul"); + add_listener(slist, "click", recommend_add_to_playlist); + var node = albums.firstChild; + while(node) { + var tmp = create_node("li", null, node.textContent); + tmp.title = LANG.RECOMMEND_ADDTOPLAYLIST; + tmp.setAttribute("artist", artist); + slist.appendChild(tmp); + node = node.nextSibling; + } + list.appendChild(slist); + exists.appendChild(list); + } + else { + var li = create_node("li", null, artist); + others.appendChild(li); + } + } + var tmp = create_fragment(); + // todo: size + tmp.appendChild(create_txt(LANG.RECOMMEND_SIMILAR)); + tmp.appendChild(exists); + // todo: size + tmp.appendChild(create_txt(LANG.RECOMMEND_ARTISTS)); + tmp.appendChild(others); + recommend_set_content(tmp); + } + } + http.send(null); +} + +function recommend_toggle_open(e) { + if(e.target.parentNode.className != "recommended") + return; + if(e.target.hasAttribute("open")) + e.target.removeAttribute("open"); + else e.target.setAttribute("open", "k"); +} + +function recommend_add_to_playlist(e) { + if(!e.target.hasAttribute("artist")) + return; + send_command("searchadd&artist=" + encodeURIComponent(e.target.getAttribute("artist")) + + "&album=" + encodeURIComponent(e.target.textContent), browser_add_cb, LANG.WAIT_ADDING); +} diff --git a/std/playlist.js b/std/playlist.js new file mode 100644 index 0000000..5bf3e9c --- /dev/null +++ b/std/playlist.js @@ -0,0 +1,907 @@ +/* + Pitchfork Music Player Daemon Client + Copyright (C) 2007 Roger Bystrøm + + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +/* this file contains anything pertaining to playlist management */ + +var playlist_element = null; + + +function playlist_init() { + show_status_bar(LANG.WAIT_LOADING, true); + var http = new XMLHttpRequest(); + http.open("GET", "command.php?ping"); + remove_children(playlist_element); + + var c = create_node("tr"); + function ctws(size) { + var tmp = create_node("td"); + tmp.style.width = size + "px"; + c.appendChild(tmp); + } + ctws(23); // pos + for(var i=1; i< pl_entries.length-1; i++) { + if(pl_entries[i]=="Track") { + ctws(50); + } + else { + ctws(200); + } + } + ctws(30); // time + c.className = "playlist"; + pl_entry_clone = c; + + http.onreadystatechange = function() { + if(http.readyState==4) { + var resp = null; + if(!http.responseText||!(resp = evaluate_json(http.responseText))) { + show_status_bar(LANG.E_INVALID_RESPONSE); + throw("Init failed"); + } + else if(http.status==200) { + if(resp['connection']&&resp['connection']=="failed") { + show_status_bar(LANG.E_CONNECT); + throw(LANG.E_CONNECT); + } + + /* for pagination to work properly we need to know some values in forehand */ + if(resp.status) { + var s = resp.status; + if(typeof(s.song)!='undefined') + playing.pos = parseInt(s.song); + } + + if(pagination_is_following()) + playlist_scroll_to_playing(); + + hide_status_bar(1); + dirlist_init(); + setTimeout(need_update,10); + } + else { + show_status_bar(LANG.E_INIT_PL); + throw("init failed"); + } + } + } + http.send(null); +} + +function create_lazy_pl_row(info, id) { + var tr = null; + var pos = parseInt(info["cpos"]); + tr = pl_entry_clone.cloneNode(true); + tr.firstChild.appendChild(create_txt(parseInt(pos)+1)); + tr.setAttribute("plpos", pos); + tr.id = id; + return tr; +} + +function get_plid(node) { + return node.id.substring(7); +} + +function get_plpos(node) { + return node.getAttribute("plpos"); +} + +function set_pl_position(node, new_pos) { + node.childNodes[0].childNodes[0].nodeValue = (new_pos+1); + node.setAttribute("plpos", new_pos); +} + +function moved_plitem(stat) { + if(stat!="ok") { + show_status_bar(LANG.E_PL_MOVE); + hide_status_bar(STATUS_DEFAULT_TIMEOUT); + } +} + +function playlist_get_move_txt() { + /* todo */ + return "Moving selection.."; +} + +/* a note on this... if to is null then it was put first in the list */ +/* if move is null then it's a multi-move (which it always will be for now) */ +function playlist_move(move, to) { + + if(move==null) { + var range = get_pl_selection_range(false); + to = get_plpos(to); + send_command("rangemove=" + encodeURIComponent(to) + "&elems=" + encodeURIComponent(range)); + return; + } + + if(to==move) { + debug("trying to move to same, you should never see this"); + return; + } + + var from = get_plid(move); + var topos = get_plpos(to); + if(from<0 || topos == null || topos == "" ) { + debug("Missing id on to or from!"); + } + send_command("playlist=move&from=" + from + "&to=" + topos, moved_plitem); +} + +/* cut's of elements at the end of playlist */ +function playlist_resize(size) { + if(pagination.max>0&&pagination.pages>1) { + if(pagination.page==pagination.pages-1 && size%pagination.max!=0 ) { + remove_children_after(playlist_element, size%pagination.max); + } + } + else { + remove_children_after(playlist_element, size); + } +} + +function select_playing_song() { + var new_p = document.getElementById("plitem_" + playing.id); + if(playing.show_node) { + if(playing.show_node==new_p) // same node + return; + apply_border_style_to_children(playing.show_node, "none"); + playing.show_node.removeAttribute("playing"); + playing.show_node = null; + } + if(new_p) { + apply_border_style_to_children(new_p, PLAYLIST_PLAYING_STYLE); + new_p.setAttribute("playing", "i"); + playing.show_node = new_p; + } + +} + +/* not really lazy, but it needs a name */ +function get_lazy_info(need_info) { + var ids = ""; + var start_id=-1, last_id=-1, id; + for(id in need_info) { + if(start_id<0) { + start_id = last_id = id; + } + else if(last_id==id-1) { + last_id = id; + } + else { + ids = add_range(ids, start_id, (start_id==last_id?false:last_id)); + start_id = last_id = id; + } + } + ids = add_range(ids, start_id, (start_id==last_id?false:last_id)); + + if(ids.length>0) { + need_info_arr = need_info; + /* get missing info and don't get plchanges */ + send_command("playlist=info", get_lazy_info_cb, LANG.WAIT_UPDATING_PL, true, "ids=" + encodeURIComponent(ids), true); + } +} + +function get_lazy_info_cb(res) { + + var node,id,info; + var ple = pl_entries; + + var need_info = need_info_arr; + need_info_arr = null; + if(need_info==null) { + //debug("get lazy info cb called, but no need_info array available"); + /* FIXME: we have a potential race condition here + (may get several results, but only one of them is the one we need and or can use) */ + return; + } + + if(!res) { + show_status_bar(LANG.E_INVALID_RESULT); + hide_status_bar(STATUS_DEFAULT_TIMEOUT); + return; + } + show_status_bar(LANG.WAIT_UPDATING_PL); + + /* todo: hide when more than 500 changes */ + var length = res.length; + var plength = ple.length; + for(var i=0; i29) { + node.childNodes[j].title = val; + val = val.substring(0,27) + ".."; + } + else if(node.childNodes[j].title) { + node.childNodes[j].removeAttribute("title"); + } + + if(node.childNodes[j].hasChildNodes()) + node.childNodes[j].childNodes[0].nodeValue = val; + else node.childNodes[j].appendChild(create_txt(val)); + } + } + } + hide_status_bar(); + + if(browser_is_konqueror()) { + playlist_element.className = "pltemp"; + setTimeout(cclass_for_konqueror, 1); + } +} + +/* see bug #46 */ +function cclass_for_konqueror() { + playlist_element.className = ""; +} + +function pl_selection_changed(sel) { + if(sel!=last_pl_selected) { + last_pl_selected = sel; + document.getElementById('crop_items_button').title = sel?LANG.CROP_SELECTION:LANG.CLEAR_PLAYLIST; + } +} + +function plchanges_handler3(list, size) { + if(!list||!list.length||list.length<=0) + return; + var cont = playlist_element; + var df = create_fragment(); // temporary storage until we end + var buf = create_fragment(); // for appending + var i=0; + var id; + var plid; + var pointer = null; // working point in playlist + var cursor = null; // temporary point in playlist to make get from doc more effective + var need_info = new Array(); + var count = 0; + + /* returns the id of the next item in the list */ + function _gn_id() { + if(i+1=pagination_start) { + i = pagination_start - changes_start; + } + else if(changes_start=0) { + pointer = cont.childNodes[start_pos]; + } + else if(pointer==null) { + pointer = _create_row(list[0]["Id"]); + cont.appendChild(pointer); + } + + if(start_pos==0) { // make sure there are no-one before us... + if(pointer.previousSibling) { + // todo obviously (if needed) + debug("let me know if you ever see this message (NTS, plchanges_handler)"); + } + } + + var append = false; + var length = list.length; + var n, nid; + var max = pagination.max || Number.MAX_VALUE; + + var _insert_before = insert_before; + var _replace_node = replace_node; + + for(; i < length && start_pos++ < max; i++) { + id = list[i]["Id"]; + if(append) { + n = _get_from_df_or_create(id) + buf.appendChild(n); + continue; + } + plid = "plitem_" + id; + + // if it's a page switch everything will have to be updated + if(pagination_switched_page) { + _update_current_row(id); + } + else if(pointer.id!=plid) { + nid = _gn_id(); + n = _get_from_df(plid); + /* if n is found in df it has been removed, but wants back in */ + if(n||nid==null||pointer.id==nid) { + if(!n) + n = _get_from_doc(plid); + if(!n) + n = _create_row(id); + _insert_before(n, pointer); + //debug("insert"); + } + else { + if(!n) + n = _get_from_doc(plid); + if(!n) + n = _create_row(id); + _replace_node(n,pointer); + df.appendChild(pointer); + //debug("replace"); + } + pointer = n; + } + if(pointer.nextSibling) + pointer = pointer.nextSibling; + else + append = true; + } + + if(buf.hasChildNodes()) + cont.appendChild(buf); + if(need_info.length>0) + get_lazy_info(need_info); + + playlist_resize(size); + update_node_positions2(cont); + select_playing_song(); + + // must be called last + if(pagination_switched_page) + pagination_post_pageswitch(); +} + +function update_node_positions2(container) { + var node = container.firstChild; + var found_diff = false; + var i = pagination.max * pagination.page; + while(node) { + if(found_diff) { + set_pl_position(node, i++); + node = node.nextSibling; + } + else { + if(get_plpos(node)==i) { + node = node.nextSibling; + i++; + } + else { + found_diff = true; + } + } + } + return i; +} + +function pagination_init() { + + pagination.following = null; // set to true/false in pagination_is_following + + /* these values shouldn't be hard-coded, but they are */ + pagination.left = 50; + pagination.width = 680; + + pagination.scroll_id = false; + pagination.scroll_left = false; + + pagination.follow_button = document.getElementById("pagination_follow_current"); + pagination.scroll_to_pos_after_switch = false; + pagination_update_follow(); +} + +/* should only be called when the size of the list has been changed and pages no longer fit*/ +function pagination_update_list(size) { + if(pagination.max<=0) + return; + var l = pagination.list; + var npages = Math.ceil(size/pagination.max); // number of pages + var cpages = pagination.pages; // current number of pages + var cpage = pagination.page; + + while(npages>cpages) { + var n = add_li(l, cpages+1); + n.setAttribute("page", cpages++); + } + while(npages1?"block":""; + + pagination.pages = cpages; + /* if we have switched page it needs update */ + if(cpage>=cpages) { + cpage = cpages - 1; + } + pagination_update_current_page(cpage); +} + +/* c: new page number */ +function pagination_update_current_page(c) { + if(pagination.max<=0) + return; + var p = pagination.list.firstChild; + + if(pagination.cpage&&pagination.cpage.hasAttribute("cpage")) + pagination.cpage.removeAttribute("cpage"); + + while(p) { + if(p.getAttribute("page")==c) { + scrollIntoView(p); + p.setAttribute("cpage", "1"); + break; + } + p = p.nextSibling; + } + pagination.cpage = p; + pagination.page = c; +} + +function pagination_fetch_current_page() { + playing.pl_version = -1; + pagination.need_update = true; + reschedule_update_now(); +} + +function pagination_change_page(e, page) { + if(e!=null&&e.target.hasAttribute("page")) { + pagination.page = parseInt(e.target.getAttribute("page")); + } + else if(typeof(page)!='undefined') { + pagination.page = page; + } + else { + return; + } + + pagination_update_current_page(pagination.page); + pagination_fetch_current_page(); + unselect_all_nodes(playlist_element); +} + +function pagination_scroll_view(e) { + var x = e.pageX - pagination.left; + var left = false; + var abort = true; + + if(x>pagination.width-20) { + abort = false; + } + else if(x<20&&pagination.list.scrollLeft>0) { + abort = false; + left = true; + } + + if(!pagination.scroll_id&&!abort) { + pagination_scroll_real(left); + } + else if(pagination.scroll_id&&abort) { + pagination_scroll_stop(); + } + + stop_event(e); +} + +function pagination_scroll_stop(e) { + + if(e && e.relatedTarget && (e.relatedTarget.id=="pagination_list"||e.relatedTarget.parentNode.id=="pagination_list"||typeof(e.relatedTarget.page)!='undefined')) + return; + + if(pagination.scroll_id) { + clearTimeout(pagination.scroll_id); + pagination.scroll_id = false; + } +} + +function pagination_is_following() { + if(pagination.following == null) { + var f = setting_get("follow_playing"); + if(f&&parseInt(f)) + pagination.following = true; + else pagination.following = false; + } + return pagination.following; +} + +function pagination_set_following(follow) { + setting_set("follow_playing", (follow?"1":"0")); + pagination.following = follow; + pagination_update_follow(); +} + +function pagination_toggle_following() { + pagination_set_following(!pagination_is_following()); +} + +function pagination_update_follow() { + if(!pagination.follow_button) + return; + if(pagination_is_following()) { + pagination.follow_button.src = IMAGE.PAGINATION_FOLLOW; + pagination.follow_button.title = LANG.PAGINATION_FOLLOW; + } + else { + pagination.follow_button.src = IMAGE.PAGINATION_NOFOLLOW; + pagination.follow_button.title =LANG.PAGINATION_NOFOLLOW; + } +} + +/** left or right */ +function pagination_scroll_real(left) { + var l = pagination.list; + var adjust = 0; + + if(left) { + if(l.scrollLeft>0) + adjust = -10; + else return; + } + else { + adjust = 10; + } + try { + l.scrollLeft += adjust; + if(pagination.scroll_id) + clearTimeout(pagination.scroll_id); + pagination.scroll_id = setTimeout(pagination_scroll_real, 50, left); + }catch(e) {debug(e.message);} +} + +function pagination_post_pageswitch() { + pagination.need_update = false; + if(pagination.scroll_to_pos_after_switch!=false) { + var n = playlist_scroll_to_pos_real(pagination.scroll_to_pos_after_switch); + pagination.scroll_to_pos_after_switch = false; + if(n) { + unselect_all_nodes(playlist_element); + select_node(n); + } + } + else if(pagination_is_following()&&playing.show_node) { + playlist_scroll_to_pos_real(playing.pos); + } +} + + +/* Returns false if have to switch page, true otherwise */ +function playlist_scroll_to_playing() { + return playlist_scroll_to_pos(playing.pos); +} + +/* set select to true if all other nodes should be unselected and the right node selected */ +function playlist_scroll_to_pos(pos, select) { + if(pagination.max>0) { + if(pos<0) pos = 0; + var page = Math.floor(pos/pagination.max); + if(page!=pagination.page) { + pagination_change_page(null, page); + if(select) + pagination.scroll_to_pos_after_switch = pos; + return false; // selecting handled in pagination_post_pageswitch + } + } + var n = playlist_scroll_to_pos_real(pos); + if(select) { + unselect_all_nodes(playlist_element); + select_node(n); + } + return true; +} + +function playlist_scroll_to_pos_real(pos) { + if(pagination.max>0) { + pos = pos%pagination.max + } + + if(pos<0) { + } + else if(pos0; + + /* if we have pagination and not on page one, we need to add everything up until this page first */ + if(pagination_add&&pagination.page>0) { + sel = add_range(sel, 0, pagination.max * pagination.page -1); + } + + var tmp_start = null; + var tmp_stop = null; + var length = c.length; + for(var i=0; i=0) { + var cmd = "act=play&id=" + get_plid(elem); + send_command(cmd); + } +} + +function playlist_add_button(e) { + if(!playlist_add_popup) { + playlist_add_popup = playlist_add_create_content( + document.getElementById("playlist_add")); + } + stop_event(e); + playlist_add_popup.show(); +} + +function playlist_save_button(e) { + if(!playlist_save_popup) { + playlist_save_popup = playlist_save_create_content( + document.getElementById("playlist_save")); + } + stop_event(e); + playlist_save_popup.show(); +} + +/* these functions should be merged somehow */ +function playlist_add_create_content(padd) { + var cats = new Array(LANG.BY_URL); //, "From file", "Text"); + var c = create_fragment(); + var ul = create_node("ul"); + ul.className = "playlist_popup"; + c.appendChild(ul); + var li; + for(var i=0; i < cats.length; i++) { + li = add_li(ul, cats[i]); + li.className = "playlist_popup"; + } + li = add_li(ul, LANG.CLOSE); + li.className = "playlist_popup"; + add_listener(li, "click", playlist_add_close); + var d = create_node("div"); + c.appendChild(d); + + var tmp = create_node("input", "playlist_add_url"); + add_listener(tmp, "keydown", playlist_add_by_url); + d.appendChild(tmp); + + tmp = create_node("span"); + d.appendChild(tmp); + tmp.className = "playlist_popup"; + tmp.appendChild(create_txt(" Add")); + add_listener(tmp, "click", playlist_add_by_url); + + var pop = new Popup(padd, c); + pop.popup.style.marginLeft = "-140px"; + /* don't let the click get anywhere else either */ + add_listener(pop.popup, "click", stop_event); + return pop; +} + +function playlist_save_create_content(psave) { + var c = create_fragment(); + var tmp = create_node("p"); + tmp.className = "nomargin"; + tmp.appendChild(create_txt(LANG.PL_SAVE_AS)); + var close = create_node("span"); + add_listener(close, "click", playlist_save_close); + close.appendChild(create_txt(LANG.CLOSE)); + close.className = "playlist_popup"; + tmp.appendChild(close); + tmp.appendChild(create_node("br")); + c.appendChild(tmp); + tmp = create_node("input", "playlist_save_box"); + add_listener(tmp, "keydown", playlist_save_listener); + c.appendChild(tmp); + tmp = create_node("span"); + tmp.appendChild(create_txt(" " + LANG.SAVE)); + tmp.className = "playlist_popup"; + add_listener(tmp, "click", playlist_save_listener); + c.appendChild(tmp); + + var pop = new Popup(psave, c); + pop.popup.style.marginLeft = "-140px"; + /* don't let the click get anywhere else either */ + add_listener(pop.popup, "click", stop_event); + return pop; +} + +function playlist_add_by_url(e) { + stop_propagation(e); + if((e.type=="keydown"&&e.keyCode==RETURN_KEY_CODE)||e.type=="click") { + stop_event(e); + var p = document.getElementById("playlist_add_url"); + var url = p.value; + url = url.trim(); + if(url.length>6) { + send_command("playlist_add_url=" + encodeURIComponent(url), + playlist_add_by_url_cb, LANG.WAIT_ADDING_PL); + p.value = ""; + playlist_add_close(); + } + } +} + +function playlist_save_listener(e) { + stop_propagation(e); + if((e.type=="keydown"&&e.keyCode==RETURN_KEY_CODE)||e.type=="click") { + stop_event(e); + var p = document.getElementById("playlist_save_box"); + var name = p.value; + name = name.trim(); + if(name.length>0) { + send_command("playlist_save=" + encodeURIComponent(name), playlist_save_cb, LANG.PL_SAVING); + p.value = ""; + playlist_save_popup.hide(); + } + } +} + +function playlist_add_by_url_cb(result) { + if(result=="failed") { + show_status_bar(LANG.E_FAILED_ADD_PL); + hide_status_bar(STATUS_DEFAULT_TIMEOUT); + } + else { + } +} + +function playlist_save_cb(result) { + if(result=="failed") { + show_status_bar(LANG.E_FAILED_SAVE_PL); + hide_status_bar(STATUS_DEFAULT_TIMEOUT); + } +} + +function playlist_add_close(e) { + if(e) + stop_event(e); + if(playlist_add_popup) + playlist_add_popup.hide(); +} +function playlist_save_close(e) { + if(e) + stop_event(e); + if(playlist_save_popup) + playlist_save_popup.hide(); +} diff --git a/std/plsearch.js b/std/plsearch.js new file mode 100644 index 0000000..74094ca --- /dev/null +++ b/std/plsearch.js @@ -0,0 +1,212 @@ +/* + Pitchfork Music Player Daemon Client + Copyright (C) 2007 Roger Bystrøm + + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + + +var sidebar = null; + +function Sidebar() { + this.is_open = false; + this.view = new HashMap(); + this.header_height = new HashMap(); + this.open_view = "metadata"; + this.header_height.put("metadata", 20); + /* TODO: find another way to determine the height of this */ + this.display_base_top = 107; + this.header = document.getElementById("sidebar_header"); + this.display = document.getElementById("sidebar_display"); + this.display_txt = document.getElementById("sidebar_display_txt"); + this.last_metadata_request = null; + this.PLSEARCH_OPTS = new Array("Any", "Artist", "Title", "Album", "Genre", "Filename", "Composer", "Performer", "Date"); + this.plsearch_choice = null; // select box + this.plsearch_input = null; // search input box +} + +Sidebar.prototype.say_loading = function(txt) { + if(typeof(txt)=='undefined') + txt = LANG.WAIT_LOADING; + this.set_content(create_txt(txt)); +} + +Sidebar.prototype.adjust_height = function(name) { + var h = this.header_height.get(name); + this.display.style.top = (this.display_base_top + h) + "px"; + this.header.style.height = h + "px"; +} + +Sidebar.prototype.set_content = function(fragment) { + remove_children(this.display_txt); + this.display_txt.appendChild(fragment); +} + +/* Name of view, buffer/element that is this view, and height of view */ +Sidebar.prototype.add_view = function(name, buffer, height) { + this.view.put(name,buffer); + if(!height) + height = 20; + this.header_height.put(name, height); +} + +/* will switch to the specified view if it isn't already open */ +Sidebar.prototype.switch_view = function(name) { + if(this.open_view==name) + return; + var n = this.view.remove(name); // make sure we can get it first + if(!n) { + debug("can't get new sidebar view: " + name); + return; + } + var buf = remove_children(this.header); + this.view.put(this.open_view, buf); + this.header.appendChild(n); + this.open_view = name; +} + + +function sidebar_close() { + if(browser_is_opera()) { + sidebar.header.style.display = "none"; + opera_quirk_set_display_none(sidebar.display); + } + else { + sidebar.header.style.display = "none"; + sidebar.display.style.display = "none"; + } + remove_children(sidebar.display_txt); +} + +function sidebar_open(view) { + if(view&&view!=sidebar.open_view) { // we have to change view + sidebar.switch_view(view); + sidebar.adjust_height(view); + } + sidebar.header.style.display = "block"; + sidebar.display.style.display = "block"; +} + + +function sidebar_init() { + sidebar = new Sidebar(); +} + +function plsearch_init() { + /* you can either use a buffer like in create_fragment() + * or add it to a top-container like I've done here*/ + var t = create_node("p"); + t.className = "nomargin"; + t.appendChild(create_txt("Playlistsearch: ")); + var close = create_node("span", null, " ["+LANG.CLOSE+"]"); + add_listener(close, "click", sidebar_close); + close.className = "fakelink"; + t.appendChild(close); + t.appendChild(create_node("br")); + + var search = create_search_choices(sidebar.PLSEARCH_OPTS, plsearch_choice_change) + + t.appendChild(search); + + sidebar.plsearch_choice = search; + search = create_node("input"); + search.type = "text"; + sidebar.plsearch_input = search; + add_listener(search, "keyup", plsearch_term_change); + add_listener(search, "keydown", stop_propagation); + search.className = "browse_type"; + t.appendChild(search); + + sidebar.add_view("plsearch", t, 45); +} + +function plsearch_choice_change(e) { + stop_propagation(e); + sidebar.plsearch_input.focus(); +} + +function plsearch_open() { + sidebar_open("plsearch"); + sidebar.set_content(create_txt("")); + sidebar.plsearch_input.focus(); +} + +function plsearch_set_content(content) { + if(sidebar.open_view=="plsearch") + sidebar.set_content(content); +} + +function plsearch_term_change(e) { + stop_propagation(e); // we'll keep it + if(e.keyCode == RETURN_KEY_CODE) { // send search + var s = sidebar.plsearch_input.value.trim(); + if(s.length>0) { + send_command("plsearch=" + sidebar.plsearch_choice.selectedIndex + + "&s=" + s, plsearch_search_cb, LANG.WAIT_SEARCHING); + } + else { // clear results + // fixme, possible leak + remove_listener(sidebar.display.firstChild , "click", plsearch_click); + remove_listener(sidebar.display.firstChild , "mousedown", stop_event); + plsearch_set_content(create_txt("")); + } + } +} + +function plsearch_search_cb(resp) { + if(typeof(resp)!='undefined'&&resp!="failed") { + var dst= create_node("p"); + dst.style.padding = "0px"; + for(var i=0; i0) { + var txt_node = document.getElementById('qa_suggestions_txt'); + var elems = txt_node.getElementsByTagName("span"); + var i = qa_get_selected_id(elems); + if(i>=0) { + add = elems[i].name; + } + quickadd_clean(); + send_command("add=" + encodeURIComponent(add), function(response) + { if(response=="failed") show_status_bar(LANG.E_FAILED_ADD); }, + LANG.WAIT_ADDING); + } + else { + quickadd_clean(); + } + } + else if(key==27) { // esc :( + stop_event(e); + qa.value = ""; + qa.blur(); + quickadd_hide(); + } + else if(key>=37&&key<=40) { /* left up right down */ + if(key==40) { // down + quickadd_move_selection(1); + stop_event(e); + } + else if(key==38) { // up + quickadd_move_selection(-1); + stop_event(e); + } + else if(key==39) { // right + var txt_node = document.getElementById('qa_suggestions_txt'); + var elems = txt_node.getElementsByTagName("span"); + var sel = qa_get_selected_id(elems); + if(sel>=0) { + //stop_event(e); + qa.value = elems[sel].name + "/"; + quickadd_keyup_handler(); + qa.focus(); + setCaretToEnd(qa); + } + } + else if(key==37) { // left + } + } +} + +function quickadd_clean() { + var qa = document.getElementById('quickadd'); + qa.value = ""; + qa.blur(); + quickadd_hide(); +} + +function qa_get_selected_id(elems) { + for(var i=0; i=0) { + elems[sel_node].removeAttribute("qa_selected"); + } + + num = num+sel_node; + + if(num>=elems.length ||num==-1) { + return; + } + else if(num<0) // flip it around + num=elems.length-1; + elems[num].setAttribute("qa_selected", "omg"); + /* safari workaround */ + elems[num].className = elems[num].className; +} + +function quickadd_hide() { + var txt_node = document.getElementById('qa_suggestions_txt'); + var qa_disp = document.getElementById('qa_suggestions'); + qa_disp.style.display = "none"; + remove_children(txt_node); +} + +function quickadd_keyup_handler(e) { + var qa = document.getElementById('quickadd'); + var search_str = qa.value; + search_str = search_str.trim(); + + /* unwanted event */ + if(e) { + e.stopPropagation(); + if(e.altKey||e.metaKey||e.ctrlKey) { + return; + } + } + + if(search_str.length>0) { + if(search_str!=quickadd_last_term) { + quickadd_last_term = search_str; + send_command("quick_search=" + encodeURIComponent(search_str), quickadd_result_handler); + } + } + else { + var txt_node = document.getElementById('qa_suggestions_txt'); + remove_children(txt_node); + quickadd_last_term = ""; + } +} + +function quickadd_result_handler(res) { + var txt_node = document.getElementById('qa_suggestions_txt'); + if(!res||res=="failed") { + remove_children(txt_node); + txt_node.appendChild(create_txt(LANG.E_NOTHING_FOUND)); + } + else { + remove_children(txt_node); + for(var ix in res) { + var name = res[ix]; + var node = create_node("span"); + node.className = "qa_element"; + node.name = name; + var idx = name.lastIndexOf(DIR_SEPARATOR); + node.appendChild(create_txt((idx>0?"..":"") + name.substring(idx))); + txt_node.appendChild(node); + } + } +} diff --git a/std/streaming.js b/std/streaming.js new file mode 100644 index 0000000..358c2e6 --- /dev/null +++ b/std/streaming.js @@ -0,0 +1,234 @@ +/* + Pitchfork Music Player Daemon Client + Copyright (C) 2007 Roger Bystrøm + + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +var streaming_info = null; + +function StreamingInfo() { + this.open = false; + this.display = null; + var c = setting_get("sap"); + if(c==null) { + this.auto_play = true; + setting_set("sap", "true"); + } + else { + this.auto_play = c=="true"?true:false; + } + this.auto_play_node = null; + this.applet = null; + this.notification_txt = null; + this.check_delay = 0.3 * 1000; + this.eventListeners = new Array(); +} + +function streaming_init() { + streaming_info = new StreamingInfo(); +} + +function streaming_destroy() { + while(streaming_info.eventListeners.length) + streaming_info.eventListeners.pop().unregister(); + + remove_node(streaming_info.applet); + remove_node(streaming_info.auto_play_node); + remove_node(streaming_info.notification_txt); + remove_node(streaming_info.display); + + streaming_info.applet = null; + streaming_info.auto_play_node = null; + streaming_info.notification_txt = null; + streaming_info.display = null; + + streaming_info.open = false; +} + +function streaming_open(e) { + var s =streaming_info.display; + if(!s) { + s = create_node("div", "streaming_display"); + var d2 = create_node("div"); + d2.style.marginLeft = "5px"; + if(!window.SHOUT_URL) + return; + /* if someone can make this work in all browsers and not crash/hang + anyone of them I'd like to know... */ + + function make_item(txt) { + var item = create_node("li"); + item.className = "fakelink"; + item.style.borderLeft = item.style.borderRight = "none"; + add_txt(item, txt); + return item; + } + + var obj = ""; + d2.innerHTML = obj; + s.appendChild(d2); + var txt = create_node("ul"); + //txt.className = "fakelink"; + txt.className = "nomargin"; + //txt.style.margin = "0px 2px 0px 0px"; + txt.style.padding = "0px 0px 0px 0px"; + txt.style.fontSize = "smaller"; + + var sp = create_node("span"); + sp.style.fontWeight = "bold"; + sp.style.padding = "5px 0px 5px 0px"; + add_txt(sp, LANG.STREAMING); + txt.appendChild(sp); + + var item = make_item(LANG.CLOSE); + streaming_info.eventListeners.push(add_listener(item, "click", streaming_destroy)); + txt.appendChild(item); + + item = make_item(LANG.HIDE); + streaming_info.eventListeners.push(add_listener(item, "click", streaming_hide)); + txt.appendChild(item); + + item = make_item( streaming_info.auto_play?LANG.AUTOPLAY:LANG.NO_AUTOPLAY ); + txt.appendChild(item); + streaming_info.eventListeners.push(add_listener(item, "click", streaming_toggle_auto_play)); + streaming_info.auto_play_node = item; + + item = make_item(""); + streaming_info.notification_txt = item; + txt.appendChild(streaming_info.notification_txt); + streaming_info.eventListeners.push(add_listener(streaming_info.notification_txt, "click", streaming_toggle_event)); + + // insert container first in area + insert_first(txt, s); + + document.getElementById('player_control').appendChild(s); + streaming_info.display = s; + streaming_info.applet = document.applets['streamplayer']; + + streaming_check_playing(); + document.body.focus(); + } + if(streaming_info.open) { + streaming_hide(e); + } + else { + s.style.visibility = ""; + streaming_info.open = true; + streaming_try_autoplay(); + if(e) { + stop_event(e); + } + } +} + +/* hides the whole streaming area */ +function streaming_hide(e) { + if(streaming_info.display) { + streaming_info.display.style.visibility = "hidden"; + } + streaming_info.open = false; + if(e) { + stop_event(e); + } +} + +/* toggles the autoplay feature */ +function streaming_toggle_auto_play(e) { + if(e) stop_event(e); + if(streaming_info.auto_play_node) { + var s = streaming_info.auto_play_node; + remove_children(s); + streaming_info.auto_play = !streaming_info.auto_play; + add_txt(s, streaming_info.auto_play?LANG.AUTOPLAY:LANG.NO_AUTOPLAY); + setting_set("sap", streaming_info.auto_play?"true":"false"); + } +} + +/* checks whether the applet is currently streaming or not, + * returns false on error or non-existing applet */ +function streaming_is_playing() { + if(streaming_info.applet) { + try { + return streaming_info.applet.isPlaying(); + } catch(e) { } + } + return false; +} + +/* tries to start playback if the applet is available */ +function streaming_try_play() { + if(streaming_info.applet) { + try { + streaming_info.applet.play_sound(); + } catch(e) { } + } +} + +/* tries to stop playback if the applet is available */ +function streaming_try_stop() { + if(streaming_info.applet) { + try { + streaming_info.applet.stop_sound(); + } catch(e) { } + } +} + +/* tries to start playing if autoplay is enabled */ +function streaming_try_autoplay() { + if(streaming_info.auto_play&&streaming_info.display&&streaming_info.applet) { + streaming_try_play(); + } +} + +/* tries to stop the audio playback if autoplay is enabled */ +function streaming_try_autostop() { + if(streaming_info.auto_play&&streaming_info.display) { + streaming_try_stop(); + } +} + +function streaming_update_stat() { + remove_children(streaming_info.notification_txt); + streaming_info.notification_txt.appendChild(create_txt(streaming_info.stat?LANG.STOP:LANG.PLAY)); +} + +function streaming_check_playing() { + var stat = streaming_is_playing(); + if(streaming_info.stat != stat) { + streaming_info.stat = stat; + streaming_update_stat(); + } + setTimeout(streaming_check_playing, streaming_info.check_delay); +} +function streaming_toggle_event(e) { + if(e) stop_event(e); + if(!streaming_is_playing()) { + streaming_try_play(); + } else { + streaming_try_stop(); + } +} diff --git a/std/toolkit.js b/std/toolkit.js new file mode 100644 index 0000000..df2149e --- /dev/null +++ b/std/toolkit.js @@ -0,0 +1,1453 @@ +/* + Pitchfork Music Player Daemon Client + Copyright (C) 2007 Roger Bystrøm + + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + + +/** + * Why reinvent the wheel when you can make a hexadecagon instead -- Me + * + * -- OR -- + * + * Reinventing the wheel to run myself over -- FOB + */ + +var moveables = new Array(); +var move_idx = -1; + +var sliders = new Array(); +var slider_idx = -1; + +var overlay = new Array(); +var overlay_adjust = 100; // px +var open_overlay_idx = -1; +var overlay_time = 5; +var overlay_hide_on_resize = true; // hide content on resize? + +var xpath_queries; + +function debug(str, force) { + + /* switch && false to true on release */ + if(typeof(force)=="undefined" && true) { + return; + } + + var d = document.getElementById('debugbox'); + if(d==null) { + d = create_node("div"); + d.id = "debugbox"; + document.body.appendChild(d); + } + + var n = create_node("p", null, str); + n.style.padding = "0px"; + n.style.margin = "0px"; + d.appendChild(n); + try { + scrollIntoView(d.lastChild); + } + catch (e) {} +} + +function get_time() { + var d = new Date(); + return d.getTime(); +} + +/* evaluates json response and returns object containing information */ +function evaluate_json(txt) { + var obj = new Object(); + + /* limit context */ + obj = eval(txt, obj); + return obj; +} + +function browser_is_opera() { + return window.opera?true:false; +} +function browser_is_konqueror() { + if(navigator.vendor=="KDE") + return true; + return false; +} + +// replaces rem with ins +function replace_node(ins, rem) { + return rem.parentNode.replaceChild(ins, rem); +} +// removes this nodes children +function remove_children(what) { + if(!what.hasChildNodes) + return null; + var buf = create_fragment(); + while(what.hasChildNodes()) + buf.appendChild(what.removeChild(what.firstChild)); + return buf; +} + +function remove_node(what) { + if(what) + return what.parentNode.removeChild(what); + return null; +} + +function remove_children_after(container, after) { + /* removing elements outside list */ + var c = container.childNodes; + if(after>=c.length) + return; + var node = c[parseInt(after)]; + while(node) { + var t = node.nextSibling; + container.removeChild(node); + node = t; + } +} + +/* insert 'node' first on 'at' */ +function insert_first(node, at) { + if(at.hasChildNodes()) + at.insertBefore(node, at.firstChild); + else at.appendChild(node); +} + +function insert_before(what, before) { + before.parentNode.insertBefore(what, before); +} + +function insert_after(what, after) { + var p = after.parentNode; + if(after.nextSibling) + p.insertBefore(what, after.nextSibling); + else p.appendChild(what); +} + +function get_node_content(node) { + + if(node.hasChildNodes && node.hasChildNodes()) + node = node.childNodes[0]; + + if(node.nodeValue) { + return node.nodeValue; + } + else if(node.textContent) { + return node.textContent; + } + else if(node.textValue) { + return node.textValue; + } + else if(node.text) { + return node.text; + } + else { + return node; + } +} + +/*Returns content of first node with specified tag + */ +function get_tag(item, tag) { + if(item.getElementsByTagName) { + var tmp = item.getElementsByTagName(tag)[0]; + if(tmp!=null) + return get_node_content(tmp); + } + return null; +} + +function get_absolute_top(node) { + var height = 0; + while(node.offsetParent != null) { + node = node.offsetParent; + if(node.offsetTop) + height += node.offsetTop; + } + return height; +} + +// returns the absolute distance to start of page +function get_absolute_height(node) { + return node.offsetTop + get_absolute_top(node); +} + +function get_absolute_left(node) { + var left = node.offsetLeft; + while(node.offsetParent != null) { + node = node.offsetParent; + if(node.offsetLeft) { + left += node.offsetLeft; + } + } + return left; +} + +function EventListener(to, type, func) { + this.to = to; + this.type = type; + this.func = func; +} + +EventListener.prototype.register = function() { + if( this.to.addEventListener ) { + this.to.addEventListener(this.type, this.func, false); + } + else if(this.to.attachEvent) { + this.to.attachEvent(this.type, this.func); + } + else debug("unable to add listener"); +} + +EventListener.prototype.unregister = function() { + if(this.to.removeEventListener) { + this.to.removeEventListener(this.type, this.func, false); + } + else if(to.detachEvent) { + this.to.detachEvent(this.type, this.func); + } + else debug("unable to detach event"); +} + +/** + * Creates a new event listener, register it and return it + */ +function add_listener(to, event_s, func) { + var el = new EventListener(to, event_s,func); + el.register(); + return el; +} + +function remove_listener(to, event_s, func) { + if(to.removeEventListener) { + to.removeEventListener(event_s, func, false); + } + else if(to.detachEvent) { + to.detachEvent(event_s, func); + } + else debug("unable to detach event"); +} + +/* Get's first real child (e.g. one with an id + * from the element specified */ +function get_first_real_child(from) { + from = from.firstChild; + while(from.nextSibling) { + if(from.id) + return from; + from = from.nextSibling; + } + return null; +} + +function get_last_real_child(from) { + var children = from.childNodes; + for(var i=children.length-1; i>=0; i++) { + if(children[i].id) + return children[i]; + } + return null; +} + + +/* + * Operations for creating elements + */ +// creates node of type type with id id and content content +// returns the node created +function create_node(type, id, content) { + var node = document.createElement(type); + if(id&&id!=null) { + node.id = id; + node.name = id; + } + if(content&&content!=null) { + if(document.createTextNode) + node.appendChild(document.createTextNode(content)); + else + node.innerHTML = content; + } + return node; +} + +function create_txt(txt) { + return document.createTextNode(txt); +} + +function create_fragment() { + return document.createDocumentFragment(); +} + +// creates an empty table row with id and +// returns it +function create_tr(id) { + return create_node("tr", id, null); +} + +function create_param(name, value) { + var p = create_node("param"); + p.name = name; + p.value = value; + return p; +} + +// add a new TD with id id and content to a tr +function add_td(tr, content, id) { + var n = create_node("td", id, content); + tr.appendChild(n); + return n; +} + +function add_li(list, content, id) { + var n = create_node("li", id, content); + list.appendChild(n); + return n; +} + +function add_txt(node, txt) { + node.appendChild(create_txt(txt)); +} + +function apply_border_style_to_children(who, style) { + var c = who.childNodes; + for(var i=0; i moving */ + +function setup_node_move(container, on_change, multi_move) { + var id = moveables.length; + add_listener(container, "mousedown", mouse_down_node); + add_listener(container, "mouseup", mouse_up_node); + add_listener(container, "mousemove", move_node); + container.setAttribute("move_id", id); + if(!multi_move) + multi_move = false; + + moveables[id] = new MoveObject(container, on_change, multi_move); + return id; +} + +function add_move_doubleclick(id, func) { + moveables[id].double_click_cb = func; +} + +function set_moveable(id, moveable) { + moveables[id].can_move = moveable; +} + +function set_selection_change_handler(id, func) { + moveables[id].selection_change_cb = func; +} + +/* NodeSelection _ find out if a node is selected */ +function is_node_selected(node) { + return node.hasAttribute&&node.hasAttribute("selected"); +} + +/* NodeSelection - set whether a node is selectable or not */ +function set_node_selectable(node, val) { + if(val&&node.hasAttribute&&node.hasAttribute("noselect")) { + node.removeAttribute("noselect"); + } + else node.setAttribute("noselect", "true"); +} + +/* NodeSelection - select node */ +function select_node(node) { + if(node.hasAttribute("noselect")) + return false; + if(node.hasAttribute("selected")) + return true; + + node.setAttribute("selected", "1"); + return true; +} + +function unselect_node(node) { + if(node.hasAttribute("selected")) { + node.removeAttribute("selected"); + } +} + +function unselect_all_nodes(container) { + if(xpath_ok()) { + var nodes = xpath_query(container, ".//.[@selected]"); + var n; + var elems = new Array(); + while((n = nodes.iterateNext())) { + elems[elems.length] = n; + } + for(var i=0; i1) { // we were just here, don't do the other stuff again.. + /* double click */ + if(m.double_click_cb&&e.detail==2) + m.double_click_cb(elem, e); + } + else if(e.ctrlKey||e.metaKey) { + if(is_node_selected(elem)) { + unselect_node(elem); + if(m.selection_change_cb&&m.something_selected) + something_selected = selection_anything_selected(m.container); + } + else { + select_node(elem); + } + } + else if (e.shiftKey) { + var sel_node = null; + + sel_node = find_first_selected_node(m.container, elem); + if(sel_node==null) { + select_node(elem); + } + else { + if(elem.hasAttribute("needle")) { + select_range(elem, sel_node); + elem.removeAttribute("needle"); + } + else { + select_range(sel_node, elem); + } + } + } + else { + if(!is_node_selected(elem)) { + unselect_all_nodes(m.container); + select_node(elem); + } + else { + m.select_if_no_move = true; + } + } + /* something selected */ + if(m.selection_change_cb) { + m.selection_change_cb(something_selected); + m.something_selected = something_selected; + } +} + +function mouse_up_node(e) { + if(move_idx<0) + return; + var m = moveables[move_idx]; + if(m.moving) { + stop_node_move(e); + } + else if(m.select_if_no_move) { + var elem = find_target_node(e.target); + unselect_all_nodes(m.container); + select_node(elem); + } + m.select_if_no_move = false; + m.possible_move_node = null; + // safari workaround + m.container.className = m.container.className; +} + +/* todo; rework to use elem instead of event */ +function start_node_move(e, elem) { + stop_event(e); + + move_idx = get_target_id(e.target); + + if(move_idx<0) + return; + if(!moveables[move_idx].can_move) + return; + + var m = moveables[move_idx]; + var move = find_target_node(e.target); + var container = m.container; + if(move!=null) { + m.moving = true; // moving + m.moving_elem = find_target_node(m.possible_move_node); // what + move = m.moving_elem; + m.possible_move_node = null; + var txt = "Moving"; + if(m.multi_move) + txt = m.multi_move(); + else if(move.childNodes[1]) + txt = move.childNodes[1].textContent; + else txt = move.childNodes[0].textConten; + + m.moving_clone = detach_node(move, txt); + set_node_offset(e, m.moving_clone); + + add_listener(document.body, "mouseup", stop_node_move); + add_listener(document.body, "mousemove", move_node); + + container.style.cursor = "move"; + } + else move_idx = -1; +} + + +/* basically the reverse of start */ +function stop_node_move(e) { + if(move_idx<0||!moveables[move_idx].moving) + return; + + var m = moveables[move_idx]; + m.moving = false; + var move = m.moving_elem; + var container = m.container; + var target = find_target_node(e.target); + + if(m.multi_move) { + /* don't move it if we are moving on top of selection */ + if(target&&!is_node_selected(target)) { + m.on_change(null,target); + } + reattach_node(move, null, m.moving_clone, 4); // remove moving node + } + else if(target!=null&&target!=move) { + /* if first in list or above the one beeing moved, move it up */ + var to = null; + if(target==get_first_real_child(container)||target.nextSibling==move) { + to = target; + reattach_node(move, target, m.moving_clone, 1); + } + else if(target.nextSibling!=null) { + /* normally default action */ + var attach_at; + to = target; + if(is_moving_up(container, move, target)) { + attach_at = target; + } + else { + attach_at = target.nextSibling; + } + reattach_node(move, attach_at, m.moving_clone, 1); + } + else { + /* basically this means we don't know any better */ + /* should not happen unless target actually is last in list */ + /*to = get_last_real_child(container); + reattach_node(move, container, m[4], 2);*/ + to = target; + reattach_node(move, container, m.moving_clone, 2); + } + + // say we changed + m.on_change(null, to); + } + else { + reattach_node(move, null, m.moving_clone, 4); // don't move it + } + + container.style.cursor = "default"; + + remove_listener(document.body, "mouseup", stop_node_move); + remove_listener(document.body, "mousemove", move_node); + + m.moving_elem = null; + m.moving_init_pos = null; + m.moving_clone = null; + move_idx = -1; +} + +function move_node(e) { + var id = window.move_idx || -1; + var o = 4; // required offset + if(id&&id>=0) { + if(moveables[id].possible_move_node!=null) { + var p = moveables[id].moving_init_pos; + if(Math.abs(p[0]-e.pageX)>=o||Math.abs(p[1]-e.pageY)>o) + start_node_move(e); + } + if(!moveables[id].moving) + return; + + stop_event(e); + set_node_offset(e, moveables[id].moving_clone); + } +} + +function is_moving_up(container, move, target) { + var c = container.childNodes; + for(var i=0; i insertBefore + * 2 => appendChild + * 3 => replaceChild + * 4 => dont move + */ +function reattach_node(node, at, clone, action) { + node.style.width =""; + node.style.top = ""; + node.style.position = ""; + node.className = node.getAttribute("old_class"); + if(action==1) { + remove_node(node); + at.parentNode.insertBefore(node, at); + } + else if(action == 2) { + remove_node(node); + at.appendChild(node); + } + else if(action == 3) { + remove_node(node); + replace_node(node, at); + } + else if(action==4) { + } + else { + debug("invalid action in reattach_node"); + } + remove_node(clone); +} + +function get_target_id(target) { + var t = find_target_node(target); + if(t!=null&&t.parentNode&&t.parentNode!=null) + return t.parentNode.getAttribute("move_id"); + else return -1; +} + +function find_target_node(target) { + while(target != null && target.parentNode&&target.parentNode != null) { + for(var i=0; i100) + pos=100; + if(pos<0) + pos=0; + + if(pos==sliders[sid].value) { + return; + } + + sliders[sid].value = pos; + + var dist = (s.offsetWidth * pos) /100 + if(isNaN(dist)||dist=="NaN") + dist = 0; + + pointer.style.left = dist+ "px"; +} + +function get_slider_pos(sid) { + return sliders[sid].value; +} + +function mouse_down_slider(e) { + var targ = e.target; + /* TODO: rewrite test */ + if(!targ||!targ.hasAttribute||!targ.hasAttribute("sliderid")) { + if(targ) + targ = targ.parentNode; + // *umpf* + if(!targ||!targ.hasAttribute||!targ.hasAttribute("sliderid")) + return; + } + + slider_idx = targ.getAttribute("sliderid"); + + add_listener(document.body, "mousemove", mouse_move_slider); + add_listener(document.body, "mouseup", mouse_up_slider); + + sliders[slider_idx].user_moving = true; + sliders[slider_idx].main_slider.setAttribute("slider_moving", "yeahitis"); + + mouse_move_slider(e); // lazy + //e.stopPropagation(); + +} + +function mouse_move_slider(e) { + if(slider_idx<0) { + debug("mouse_move_slider should not be called now"); + mouse_up_slider(e); + return; + } + stop_event(e); + var left = slider_get_left(e, slider_idx) + set_slider_pos(slider_idx, left, true); + sliders[slider_idx].moving_pos = left; + sliders[slider_idx].setup_timer(sliders[slider_idx].change_call); +} + +function mouse_up_slider(e) { + if(slider_idx<0) + return; + + // prolly not necessary though + clearTimeout(sliders[slider_idx].timer); + sliders[slider_idx].timer = null; + + remove_listener(document.body, "mousemove", mouse_move_slider); + remove_listener(document.body, "mouseup", mouse_up_slider); + sliders[slider_idx].user_moving = false; + sliders[slider_idx].main_slider.removeAttribute("slider_moving"); + slider_send_callback(slider_idx); + slider_idx = -1; +} + +function slider_get_left(e, sid) { + var x = e.pageX - get_absolute_left(sliders[sid].main_slider); + x = (x*100)/sliders[sid].main_slider.offsetWidth; + x-=3; + if(x<0) + x = 0; + return x; +} + +function OverlayObject(back, sizes, open_callback, close_callback) { + this.back = back; // element to put overlay over + this.sizes = sizes; // minimum sizes [top, left, min-height, min-width ] + this.open_callback = open_callback; + this.close_callback = close_callback; + this.overlay = null; // the overlay element + this.write = null; // write area +} + +/* overlay */ +function setup_overlay(back, sizes, open_callback, close_callback) { + var oid = overlay.length; + overlay[oid] = new OverlayObject(back, sizes, open_callback, close_callback); + var t = create_node("div", "overlay_" + oid); + overlay[oid].overlay = t; + t.className = "overlay"; + t.style.height = overlay_adjust + "px"; + var img = create_node("img", "overlay_close_" + oid); + img.src = IMAGE.CLOSE; + img.setAttribute("oid", oid); + img.className = "close fakelink"; + img.title = "Close [Ctrl+Shift+X]"; + add_listener(img, "click", close_overlay_cb); + + t.appendChild(img); + + document.body.appendChild(t); + + return oid; +} + +function get_overlay_write_area(oid) { + if(overlay[oid].write==null) { + overlay[oid].write = create_node("div", "overlay_write_area_" + oid); + overlay[oid].overlay.appendChild(overlay[oid].write); + + } + return overlay[oid].write; +} + +function open_overlay(oid) { + var sizes = overlay[oid].sizes; + var o = overlay[oid].back; + var top = get_absolute_top(o); + if(topheight) + wx = width/height; + else + hx = height/width; + + overlay[oid].close_key = keyboard_register_listener(close_overlay_cb, "x", KEYBOARD_CTRL_KEY|KEYBOARD_SHIFT_KEY, true); + + setTimeout(adjust_overlay_size, overlay_time, oid, new Array(overlay_adjust, overlay_adjust), new Array(height, width, hx, wx)); +} + +function open_overlay_fixed(oid) { + // TODO: find another way to determine these heights + var sizes = new Array(106, 56, 800, 500); + + var height = sizes[3]; + var width = sizes[2]; + var op = overlay[oid].overlay; + open_overlay_idx = oid; + if(overlay_hide_on_resize&&overlay[oid].write) + overlay[oid].write.style.display = "none"; + + op.style.position = "fixed"; + + op.style.left = sizes[1] + "px"; + op.style.top = sizes[0] + "px"; + op.style.width = overlay_adjust + "px"; + op.style.height = overlay_adjust + "px"; + + op.style.display = "block"; + + /* adjust to browser window */ + var x_o = 30; + var w_h = window.innerHeight; + var w_w = window.innerWidth; + + /* ignore it if unreasonable values.. */ + if(w_h&&w_w&&w_h>100&&w_w>100) { + if(height+sizes[0]+x_o>w_h) + height = w_h - sizes[0] - x_o; + if(width+sizes[1]+x_o>w_w) + width = w_w - sizes[1] - x_o; + } + + var hx = 1, wx =1; + if(width>height) + wx = width/height; + else + hx = height/width; + + overlay[oid].close_key = keyboard_register_listener(close_overlay_cb, "x", KEYBOARD_CTRL_KEY|KEYBOARD_SHIFT_KEY, true); + + setTimeout(adjust_overlay_size, overlay_time, oid, new Array(overlay_adjust, overlay_adjust), new Array(height, width, hx, wx)); +} + +function adjust_overlay_size(oid, current, dest) { + var h = current[0] = current[0] + (dest[2]*overlay_adjust); + var w = current[1] = current[1] + (dest[3]*overlay_adjust); + var adjusted = false; + + if(h=0) { + close_overlay(open_overlay_idx); + stop_event(e); + } + +} + +function stop_propagation(e) { + if(e.stopPropagation) + e.stopPropagation(); +} + +function stop_event(e) { + if(e) { + if(e.preventDefault) + e.preventDefault(); + if(e.stopPropagation) + e.stopPropagation(); + if(e.returnValue) + e.returnValue = false; + } +} + +/* range selection (to put ranges in a "list" */ +/* txt: excisting range: + * from: from what number + * to: optional to argument + */ +function add_range(txt, from, to) { + if(txt.length>0) + txt+=";"; + txt+=from; + if(to) + txt+="-" + to; + return txt; +} + +function scrollIntoView(elem, top) { + if(!top) + top = false; + /* seriously though, if you don't support it, don't claim you do!*/ + //if(elem.scrollIntoView) { + if(navigator.product&&navigator.product=="Gecko") { + elem.scrollIntoView(top); + } + else if(elem.parentNode) { + // TODO: top + try { + elem.parentNode.scrollTop=elem.offsetTop - (top?elem.offsetHeight*2:elem.parentNode.offsetHeight); + } catch (e) { } + } +} + +function setSelectionRange(input, selectionStart, selectionEnd) { + if (input.setSelectionRange) { + input.focus(); + input.setSelectionRange(selectionStart, selectionEnd); + } + else if (input.createTextRange) { + var range = input.createTextRange(); + range.collapse(true); + range.moveEnd('character', selectionEnd); + range.moveStart('character', selectionStart); + range.select(); + range.detach(); + } +} +function setCaretToEnd (input) { + setSelectionRange(input, input.value.length, input.value.length); +} +function setCaretToBegin (input) { + setSelectionRange(input, 0, 0); +} +function setCaretToPos (input, pos) { + setSelectionRange(input, pos, pos); +} + + + +String.prototype.trim = function() { + return this.replace(/^\s+|\s+$/g,''); +} + +function add_string_with_br(to, str) { + str = str.replace("\r", "").split('\n'); + for(var i=0; i0) + to.appendChild(create_txt(str[i])); + to.appendChild(create_node("br")); + } +} + +function adjust_opacity_timer(node, current_opacity, dest_opacity, last_time) { + var now = get_time(); + var time = now; + if(last_time) + time -= last_time; + else time = 0; + + time = 100-time; + //debug("time: " + time); + if(time<0) { + time = 0; + current_opacity+=0.2; + } + else { + current_opacity+=0.1; + } + + if(current_opacity0) { + setTimeout(blink_node, 350, what, color, --count); + } +} + +/* popup */ +/* if content is null, we'll use tabs */ +function Popup(point, content) { + this.point = point; + this.content = content; + this.popup = create_node("div"); + if(content) + this.popup.appendChild(content); + this.popup.className = "popup"; + this.point.appendChild(this.popup); +} + +Popup.prototype.show = function() { + this.popup.style.display = "block"; +} +Popup.prototype.hide = function() { + this.popup.style.display = ""; +} +Popup.prototype.destroy = function() { + remove_node(this.popup); + this.popup = null; + this.point = null; + this.content = null; +} + +/* xpath */ +function xpath_init() { + xpath_queries = new Hashtable(); +} + +/* checks if xpath is available */ +function xpath_ok() { + return document.evaluate?true:false; +} + +// remember to check with xpath_ok first when using this function +function xpath_query(container, expression, resulttype, nocache_query) { + if(!resulttype) + resulttype = XPathResult.ANY_TYPE; + if(nocache_query) { + return document.evaluate(expression, container, null, resulttype, null); + } + else { + var e = xpath_queries.get(expression); + if(!e) { + e = document.createExpression(expression, null); + xpath_queries.put(expression, e); + } + return e.evaluate(container, resulttype, null); + } +} + +function opera_quirk_set_display_none(element, cleanup) { + if(cleanup) { + element.style.display = "none"; + element.style.visibility = ""; + } + else { + setTimeout(opera_quirk_set_display_none, 10, element, true); + element.style.visibility = "hidden"; + } +} + +function createCookie(name,value,days) { + if (days) { + var date = new Date(); + date.setTime(date.getTime()+(days*24*60*60*1000)); + var expires = "; expires="+date.toGMTString(); + } + else var expires = ""; + document.cookie = name+"="+value+expires+"; path=/"; +} +function readCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; +} +function eraseCookie(name) { + createCookie(name,"",-1); +} + +// This function is in the public domain. Feel free to link back to http://jan.moesen.nu/ +function sprintf() { + if (!arguments || arguments.length < 1 || !RegExp) { + return ""; + } + var str = arguments[0]; + var re = /([^%]*)%('.|0|\x20)?(-)?(\d+)?(\.\d+)?(%|b|c|d|u|f|o|s|x|X)(.*)/; + var a = b = [], numSubstitutions = 0, numMatches = 0; + while ((a = re.exec(str))) { + var leftpart = a[1], pPad = a[2], pJustify = a[3], pMinLength = a[4]; + var pPrecision = a[5], pType = a[6], rightPart = a[7]; + + //alert(a + '\n' + [a[0], leftpart, pPad, pJustify, pMinLength, pPrecision); + + numMatches++; + if (pType == '%') { + subst = '%'; + } + else { + numSubstitutions++; + if (numSubstitutions >= arguments.length) { + alert('Error! Not enough function arguments (' + (arguments.length - 1) + ', excluding the string)\nfor the number of substitution parameters in string (' + numSubstitutions + ' so far).'); + } + var param = arguments[numSubstitutions]; + var pad = ''; + if (pPad && pPad.substr(0,1) == "'") pad = leftpart.substr(1,1); + else if (pPad) pad = pPad; + var justifyRight = true; + if (pJustify && pJustify === "-") justifyRight = false; + var minLength = -1; + if (pMinLength) minLength = parseInt(pMinLength); + var precision = -1; + if (pPrecision && pType == 'f') precision = parseInt(pPrecision.substring(1)); + var subst = param; + if (pType == 'b') subst = parseInt(param).toString(2); + else if (pType == 'c') subst = String.fromCharCode(parseInt(param)); + else if (pType == 'd') subst = parseInt(param) ? parseInt(param) : 0; + else if (pType == 'u') subst = Math.abs(param); + else if (pType == 'f') subst = (precision > -1) ? Math.round(parseFloat(param) * Math.pow(10, precision)) / Math.pow(10, precision): parseFloat(param); + else if (pType == 'o') subst = parseInt(param).toString(8); + else if (pType == 's') subst = param; + else if (pType == 'x') subst = ('' + parseInt(param).toString(16)).toLowerCase(); + else if (pType == 'X') subst = ('' + parseInt(param).toString(16)).toUpperCase(); + } + str = leftpart + subst + rightPart; + } + return str; +} + +/* settings */ + +function setting_set(name, value) { + var s = readCookie("pf_conf"); + + var ns = ""; + var set = false; + if(s) { + s = s.split(":"); + for(var i=0; i< s.length; i++) { + var tmp = s[i].split("-"); + if(!tmp[0].length) + continue; + + if(tmp[0]==name) { + ns+=name + "-" + value; + set = true; + } + else { + ns+=s[i]; + } + ns+=":"; + } + } + if(!set) + ns+=name + "-" +value + ":"; + + createCookie("pf_conf", ns, 200); + + return true; +} + +function setting_get(name) { + var val = readCookie("pf_conf"); + + if(!val||!val.length) + return null; + + val = val.split(":"); + for(var i=0; i < val.length; i++) { + var t = val[i].split("-"); + if(t[0]==name) + return t[1]; + } + return null; +} diff --git a/theme/dark/images/COPYING b/theme/dark/images/COPYING new file mode 100644 index 0000000..9bfb00e --- /dev/null +++ b/theme/dark/images/COPYING @@ -0,0 +1,4 @@ +Most of these icons are from tango-noir: +http://www.gnome-look.org/content/show.php?content=38964 +License: +http://creativecommons.org/licenses/by-sa/2.0/ diff --git a/theme/dark/images/add_15.png b/theme/dark/images/add_15.png new file mode 100644 index 0000000..3c1a500 Binary files /dev/null and b/theme/dark/images/add_15.png differ diff --git a/theme/dark/images/audio_li.png b/theme/dark/images/audio_li.png new file mode 100644 index 0000000..c1dddc2 Binary files /dev/null and b/theme/dark/images/audio_li.png differ diff --git a/theme/dark/images/close.png b/theme/dark/images/close.png new file mode 100644 index 0000000..5f30185 Binary files /dev/null and b/theme/dark/images/close.png differ diff --git a/theme/dark/images/close_15.png b/theme/dark/images/close_15.png new file mode 100644 index 0000000..15c022c Binary files /dev/null and b/theme/dark/images/close_15.png differ diff --git a/theme/dark/images/crop_30.png b/theme/dark/images/crop_30.png new file mode 100644 index 0000000..ad81d80 Binary files /dev/null and b/theme/dark/images/crop_30.png differ diff --git a/theme/dark/images/disabled_15.png b/theme/dark/images/disabled_15.png new file mode 100644 index 0000000..e5f1919 Binary files /dev/null and b/theme/dark/images/disabled_15.png differ diff --git a/theme/dark/images/enabled_15.png b/theme/dark/images/enabled_15.png new file mode 100644 index 0000000..048efaf Binary files /dev/null and b/theme/dark/images/enabled_15.png differ diff --git a/theme/dark/images/folder_30.png b/theme/dark/images/folder_30.png new file mode 100644 index 0000000..a02745a Binary files /dev/null and b/theme/dark/images/folder_30.png differ diff --git a/theme/dark/images/folder_li.png b/theme/dark/images/folder_li.png new file mode 100644 index 0000000..62791a0 Binary files /dev/null and b/theme/dark/images/folder_li.png differ diff --git a/theme/dark/images/follow-playing-no.orig.png b/theme/dark/images/follow-playing-no.orig.png new file mode 100644 index 0000000..dd64ba8 Binary files /dev/null and b/theme/dark/images/follow-playing-no.orig.png differ diff --git a/theme/dark/images/follow-playing-no.png b/theme/dark/images/follow-playing-no.png new file mode 100644 index 0000000..3597926 Binary files /dev/null and b/theme/dark/images/follow-playing-no.png differ diff --git a/theme/dark/images/follow-playing-yes.orig.png b/theme/dark/images/follow-playing-yes.orig.png new file mode 100644 index 0000000..8c72f80 Binary files /dev/null and b/theme/dark/images/follow-playing-yes.orig.png differ diff --git a/theme/dark/images/follow-playing-yes.png b/theme/dark/images/follow-playing-yes.png new file mode 100644 index 0000000..5ec364d Binary files /dev/null and b/theme/dark/images/follow-playing-yes.png differ diff --git a/theme/dark/images/jump-to-current.orig.png b/theme/dark/images/jump-to-current.orig.png new file mode 100644 index 0000000..5b742cf Binary files /dev/null and b/theme/dark/images/jump-to-current.orig.png differ diff --git a/theme/dark/images/jump-to-current.png b/theme/dark/images/jump-to-current.png new file mode 100644 index 0000000..499ecae Binary files /dev/null and b/theme/dark/images/jump-to-current.png differ diff --git a/theme/dark/images/left_15.png b/theme/dark/images/left_15.png new file mode 100644 index 0000000..4dc9bda Binary files /dev/null and b/theme/dark/images/left_15.png differ diff --git a/theme/dark/images/media-album_li.png b/theme/dark/images/media-album_li.png new file mode 100644 index 0000000..a27323c Binary files /dev/null and b/theme/dark/images/media-album_li.png differ diff --git a/theme/dark/images/media-artist_li.png b/theme/dark/images/media-artist_li.png new file mode 100644 index 0000000..c87c715 Binary files /dev/null and b/theme/dark/images/media-artist_li.png differ diff --git a/theme/dark/images/next_25.png b/theme/dark/images/next_25.png new file mode 100644 index 0000000..c9938dd Binary files /dev/null and b/theme/dark/images/next_25.png differ diff --git a/theme/dark/images/pause_25.png b/theme/dark/images/pause_25.png new file mode 100644 index 0000000..7e40972 Binary files /dev/null and b/theme/dark/images/pause_25.png differ diff --git a/theme/dark/images/play_25.png b/theme/dark/images/play_25.png new file mode 100644 index 0000000..a2a15fb Binary files /dev/null and b/theme/dark/images/play_25.png differ diff --git a/theme/dark/images/playlist_li.png b/theme/dark/images/playlist_li.png new file mode 100644 index 0000000..627a858 Binary files /dev/null and b/theme/dark/images/playlist_li.png differ diff --git a/theme/dark/images/previous_25.png b/theme/dark/images/previous_25.png new file mode 100644 index 0000000..e9e2527 Binary files /dev/null and b/theme/dark/images/previous_25.png differ diff --git a/theme/dark/images/remove_30.png b/theme/dark/images/remove_30.png new file mode 100644 index 0000000..5415bf1 Binary files /dev/null and b/theme/dark/images/remove_30.png differ diff --git a/theme/dark/images/right_15.png b/theme/dark/images/right_15.png new file mode 100644 index 0000000..e80309f Binary files /dev/null and b/theme/dark/images/right_15.png differ diff --git a/theme/dark/images/stop_25.png b/theme/dark/images/stop_25.png new file mode 100644 index 0000000..9147847 Binary files /dev/null and b/theme/dark/images/stop_25.png differ diff --git a/theme/dark/images/working.gif b/theme/dark/images/working.gif new file mode 100644 index 0000000..6a19d8d Binary files /dev/null and b/theme/dark/images/working.gif differ diff --git a/theme/dark/theme.css b/theme/dark/theme.css new file mode 100644 index 0000000..afa0fb9 --- /dev/null +++ b/theme/dark/theme.css @@ -0,0 +1,159 @@ +/* top right bottom left */ +body { + font-size: 12.0px; + font-family: "Bitstream Vera Sans","Verdana"; + background-color: #0a0b0e; +} + +select, input, a, body { + color: #eee; +} + +select, input { + font-size: 0.95em; + line-height: 1.2em; + background: #111111; + border: 1px solid #333333; +} + +.main_container { + border: 1px solid #e2e2e2; + background: #0a0b0e; +} + +div.moving_box { + background-color: #808080; + border: 1px solid #65869E; +} + +li[dirtype=file] { background-image: url(images/audio_li.png); } +li[dirtype=directory] { background-image: url(images/folder_li.png); } +li[dirtype=artist] { background-image: url(images/media-artist_li.png); } +li[dirtype=album] { background-image: url(images/media-album_li.png); } +li[dirtype=playlist] { background-image: url(images/playlist_li.png); } + +/* used to select the filename */ +li[btype=search] > span { + color: #999; +} + + +div.slider_pointer { + border: 1px solid #333333; + background-color: #305050; +} + +[slider_moving] { + cursor: move; +} + +img.act_button { + height: 25px; + width: 25px; + opacity: 0.70; +} +img.act_button:hover { + opacity: 1.0; +} + +hr.server_settings { + color: #333333; + background-color: #333333; +} + +div.overlay, div.selection_menu, #status_bar, div.slider_main, +div#pagination, div#pagination_options, input#quickadd +{ + border: 1px solid #333333; + background-color: #111111; +} + +div.settings_container, div.settings_header, +div#qa_suggestions, div#sidebar_header, +div#streaming_display, div.player_control, +div.popup +{ + border: 1px solid #333333; + background-color: black; +} + +div.settings_header { + border-bottom: 0px; +} + +input#quickadd { + font-size: 12px; + color: #999; +} +input#quickadd:hover, input#quickadd:focus { + background-color: black; + color: #eee; +} + +div.big_albumart { + background-color: black; +} + +div#sidebar_header { + font-size: 0.8em; +} + +div#sidebar_display { + border: 1px solid black; + background-color: #111111; + border: 1px solid #333333; + font-size: 0.992em; + line-height: 1.3em; +} + +/* for browser action buttons */ + +/* match add buttons */ +li.browser_button_add { + background: url(images/add_15.png) no-repeat top left; +} + +/* current page on the pagination list */ +li[cpage] { + background-color: #800000; +} + +span.plse { + background-image: url(images/audio_li.png); +} + +span.plse:hover, +ul.recommended > li > ul > li:hover , +[selected], +li.menuitem:hover, +#streaming_display li:hover, +span.qa_element[qa_selected] +{ + background-color: #333333; +} + +li.playlist_popup, +span.playlist_popup, +ul#pagination_list > li, +.browse_type, +ul.browser_field, +#playlist +{ + border: 1px solid #333333; +} + +li.menuitem { + border: 1px solid #555555; + border-top: none; + background: #111111; +} + +tr.playlist { + border: 0px; +} + +tr.moving { + border-top: 1px solid #b0bdec; + color: gray; +} + diff --git a/theme/dark/theme.js b/theme/dark/theme.js new file mode 100644 index 0000000..1738b95 --- /dev/null +++ b/theme/dark/theme.js @@ -0,0 +1,40 @@ + +/* number of "windows" on the directory browser */ +var BROWSER_NUM = 3; + +/* the border around the playing item in the playlist */ +var PLAYLIST_PLAYING_STYLE = "1px solid #800000"; + +/* this is the color thing that you add blink in */ +var DEFAULT_BLINK_COLOR = "#C1DAD6"; + +var BASE_IMG_DIR = "../theme/dark/images/"; + +var IMAGE = new Object(); + +IMAGE.BUTTON_PLAY = BASE_IMG_DIR + "play_25.png"; +IMAGE.BUTTON_STOP = BASE_IMG_DIR + "stop_25.png"; +IMAGE.BUTTON_PAUSE = BASE_IMG_DIR + "pause_25.png"; +IMAGE.BUTTON_PREVIOUS = BASE_IMG_DIR + "previous_25.png"; +IMAGE.BUTTON_NEXT = BASE_IMG_DIR + "next_25.png"; + +IMAGE.BROWSER_PLAYLIST_REMOVE = BASE_IMG_DIR + "disabled_15.png"; + +IMAGE.SERVER_SETTINGS_ENABLED = BASE_IMG_DIR + "enabled_15.png"; +IMAGE.SERVER_SETTINGS_DISABLED = BASE_IMG_DIR + "disabled_15.png"; +IMAGE.SERVER_SETTINGS_XFADE_DOWN= BASE_IMG_DIR + "left_15.png"; +IMAGE.SERVER_SETTINGS_XFADE_UP = BASE_IMG_DIR + "right_15.png"; + +IMAGE.CLOSE = BASE_IMG_DIR + "close_15.png"; +IMAGE.WORKING = BASE_IMG_DIR + "working.gif"; + +IMAGE.MENU_ITEM_CROP = BASE_IMG_DIR + "crop_30.png"; +IMAGE.MENU_ITEM_REMOVE = BASE_IMG_DIR + "remove_30.png"; +IMAGE.MENU_ITEM_DIRECTORY = BASE_IMG_DIR + "folder_30.png"; + +IMAGE.PAGINATION_FOLLOW = BASE_IMG_DIR + "follow-playing-yes.png"; +IMAGE.PAGINATION_NOFOLLOW = BASE_IMG_DIR + "follow-playing-no.png"; + +IMAGE.JUMP_CURRENT = BASE_IMG_DIR + "jump-to-current.png"; + +IMAGE.STREAM_BGCOLOR = "1118481"; diff --git a/theme/default/images/add_15.png b/theme/default/images/add_15.png new file mode 100644 index 0000000..d341334 Binary files /dev/null and b/theme/default/images/add_15.png differ diff --git a/theme/default/images/audio_li.png b/theme/default/images/audio_li.png new file mode 100644 index 0000000..e8ed100 Binary files /dev/null and b/theme/default/images/audio_li.png differ diff --git a/theme/default/images/close.png b/theme/default/images/close.png new file mode 100644 index 0000000..c983059 Binary files /dev/null and b/theme/default/images/close.png differ diff --git a/theme/default/images/close_15.png b/theme/default/images/close_15.png new file mode 100644 index 0000000..be5676c Binary files /dev/null and b/theme/default/images/close_15.png differ diff --git a/theme/default/images/crop_30.png b/theme/default/images/crop_30.png new file mode 100644 index 0000000..ad81d80 Binary files /dev/null and b/theme/default/images/crop_30.png differ diff --git a/theme/default/images/disabled_15.png b/theme/default/images/disabled_15.png new file mode 100644 index 0000000..e5f1919 Binary files /dev/null and b/theme/default/images/disabled_15.png differ diff --git a/theme/default/images/enabled_15.png b/theme/default/images/enabled_15.png new file mode 100644 index 0000000..63adab8 Binary files /dev/null and b/theme/default/images/enabled_15.png differ diff --git a/theme/default/images/folder_30.png b/theme/default/images/folder_30.png new file mode 100644 index 0000000..7bdcd08 Binary files /dev/null and b/theme/default/images/folder_30.png differ diff --git a/theme/default/images/folder_li.png b/theme/default/images/folder_li.png new file mode 100644 index 0000000..a4d80fa Binary files /dev/null and b/theme/default/images/folder_li.png differ diff --git a/theme/default/images/follow-playing-no.png b/theme/default/images/follow-playing-no.png new file mode 100644 index 0000000..3597926 Binary files /dev/null and b/theme/default/images/follow-playing-no.png differ diff --git a/theme/default/images/follow-playing-yes.png b/theme/default/images/follow-playing-yes.png new file mode 100644 index 0000000..5ec364d Binary files /dev/null and b/theme/default/images/follow-playing-yes.png differ diff --git a/theme/default/images/jump-to-current.png b/theme/default/images/jump-to-current.png new file mode 100644 index 0000000..499ecae Binary files /dev/null and b/theme/default/images/jump-to-current.png differ diff --git a/theme/default/images/left_15.png b/theme/default/images/left_15.png new file mode 100644 index 0000000..525df34 Binary files /dev/null and b/theme/default/images/left_15.png differ diff --git a/theme/default/images/media-album_li.png b/theme/default/images/media-album_li.png new file mode 100644 index 0000000..a27323c Binary files /dev/null and b/theme/default/images/media-album_li.png differ diff --git a/theme/default/images/media-artist_li.png b/theme/default/images/media-artist_li.png new file mode 100644 index 0000000..c87c715 Binary files /dev/null and b/theme/default/images/media-artist_li.png differ diff --git a/theme/default/images/next_25.png b/theme/default/images/next_25.png new file mode 100644 index 0000000..c026270 Binary files /dev/null and b/theme/default/images/next_25.png differ diff --git a/theme/default/images/pause_25.png b/theme/default/images/pause_25.png new file mode 100644 index 0000000..29650ef Binary files /dev/null and b/theme/default/images/pause_25.png differ diff --git a/theme/default/images/play_25.png b/theme/default/images/play_25.png new file mode 100644 index 0000000..a6af04c Binary files /dev/null and b/theme/default/images/play_25.png differ diff --git a/theme/default/images/playlist_li.png b/theme/default/images/playlist_li.png new file mode 100644 index 0000000..627a858 Binary files /dev/null and b/theme/default/images/playlist_li.png differ diff --git a/theme/default/images/previous_25.png b/theme/default/images/previous_25.png new file mode 100644 index 0000000..8f3fcf9 Binary files /dev/null and b/theme/default/images/previous_25.png differ diff --git a/theme/default/images/remove_30.png b/theme/default/images/remove_30.png new file mode 100644 index 0000000..3a84844 Binary files /dev/null and b/theme/default/images/remove_30.png differ diff --git a/theme/default/images/right_15.png b/theme/default/images/right_15.png new file mode 100644 index 0000000..0cc81ad Binary files /dev/null and b/theme/default/images/right_15.png differ diff --git a/theme/default/images/stop_25.png b/theme/default/images/stop_25.png new file mode 100644 index 0000000..a31a1e6 Binary files /dev/null and b/theme/default/images/stop_25.png differ diff --git a/theme/default/images/working.gif b/theme/default/images/working.gif new file mode 100644 index 0000000..42ff5c6 Binary files /dev/null and b/theme/default/images/working.gif differ diff --git a/theme/default/theme.css b/theme/default/theme.css new file mode 100644 index 0000000..0e78944 --- /dev/null +++ b/theme/default/theme.css @@ -0,0 +1,189 @@ +/* top right bottom left */ +body { + font-size: 12.4px; + font-family: sans-serif; + background-color: #f7f7f7; +} + +a { + color: black; +} + +select, input { + font-size: 0.95em; + line-height: 1.2em; + border: 1px solid #BFCFCC; +} + +hr.server_settings { + color: #9db2b1; + background-color: #9db2b1; +} + +.main_container, #playlist { + border: 1px solid #e2e2e2; + background: #f1f1f1; +} + +#playlist { + cursor: default; +} + +tr.playlist { + border: 0px; +} + +div.moving_box { + background-color: #f1f1f1; + border: 1px solid #65869E; +} + +tr.moving { + border-top: 1px solid #b0bdec; + color: gray; +} + +li[dirtype=file] { background-image: url(images/audio_li.png); } +li[dirtype=directory] { background-image: url(images/folder_li.png); } +li[dirtype=artist] { background-image: url(images/media-artist_li.png); } +li[dirtype=album] { background-image: url(images/media-album_li.png); } +li[dirtype=playlist] { background-image: url(images/playlist_li.png); } + +/* used to signify that this is a search result and is used in std/browser.js */ +li[btype=search] { } + +/* used to select the filename */ +li[btype=search] > span { + color: gray; +} + +div.slider_main, div.popup { + border: 1px solid #B0BDEC; + background-color: #DEE7F7; +} + +div.slider_pointer { + border: 1px solid #a20000; + background-color: #ffcccc; +} + +[slider_moving] { + cursor: move; +} + +img.act_button { + height: 25px; + width: 25px; + opacity: 0.70; +} +img.act_button:hover { + opacity: 1.0; +} + +#status_bar { + border: 1px solid #BDD8DA; + background: #DFEFF0; +} + +div.settings_container, div.settings_header, +div.player_control, div#streaming_display { + border: 1px solid #9db2b1; + background: #e7e7e7; +} +div.settings_header { + border-bottom: 0px; +} + +input#quickadd { + font-size: 12px; + color: gray; + background: #f1f1f1; +} + +input#quickadd:hover, input#quickadd:focus { + background: white; + border: 1px solid #9db2b1; +} +input#quickadd:focus { + color: black; +} + +div#qa_suggestions { + background: white; + border: 1px solid #ccc; +} + +span.qa_element[qa_selected] { + background-color: #A5F2F3; +} + +div.big_albumart { + background-color: black; +} + +p#albumart_txt { + color: white; +} + +div#sidebar_header { + background: #f1f1f1; + border: 1px solid #9db2b1; + font-size: 0.9em; +} + +div#sidebar_display { + border: 1px solid black; + background-color: #ffffff; + border: 1px solid #9db2b1; + font-size: 0.992em; + line-height: 1.3em; +} + + +/* for browser action buttons */ + +/* match add buttons */ +li.browser_button_add { + background: url(images/add_15.png) no-repeat top left; +} + +li.playlist_popup, +span.playlist_popup { + border: 1px solid #B0BDEC; +} + +div#pagination, div#pagination_options, +div.overlay, div.selection_menu { + border: 1px solid #bfcfcc; + background: #f1f1f1; +} + +ul#pagination_list > li, +ul.browser_field, +.browse_type { + border: 1px solid #bfcfcc; +} + +/* current page on the pagination list */ +li[cpage] { + background-color: green; +} + +span.plse { + background-image: url(images/audio_li.png); +} + +span.plse:hover, +ul.recommended > li > ul > li:hover , +[selected], +li.menuitem:hover, +#streaming_display li:hover +{ + background-color: #DEE7F7; +} + +li.menuitem { + border: 1px solid #9DB2B1; + border-top: none; + background-color: #f1f1f1; +} diff --git a/theme/default/theme.js b/theme/default/theme.js new file mode 100644 index 0000000..ae998e4 --- /dev/null +++ b/theme/default/theme.js @@ -0,0 +1,42 @@ + +/* number of "windows" on the directory browser */ +var BROWSER_NUM = 3; + +/* the border around the playing item in the playlist */ +var PLAYLIST_PLAYING_STYLE = "1px solid #a20000"; + +/* this is the color thing that you add blink in */ +var DEFAULT_BLINK_COLOR = "#C1DAD6"; + +var BASE_IMG_DIR = "../theme/default/images/"; + +var IMAGE = new Object(); + +IMAGE.BUTTON_PLAY = BASE_IMG_DIR + "play_25.png"; +IMAGE.BUTTON_STOP = BASE_IMG_DIR + "stop_25.png"; +IMAGE.BUTTON_PAUSE = BASE_IMG_DIR + "pause_25.png"; +IMAGE.BUTTON_PREVIOUS = BASE_IMG_DIR + "previous_25.png"; +IMAGE.BUTTON_NEXT = BASE_IMG_DIR + "next_25.png"; + +IMAGE.BROWSER_PLAYLIST_REMOVE = BASE_IMG_DIR + "disabled_15.png"; + +IMAGE.SERVER_SETTINGS_ENABLED = BASE_IMG_DIR + "enabled_15.png"; +IMAGE.SERVER_SETTINGS_DISABLED = BASE_IMG_DIR + "disabled_15.png"; +IMAGE.SERVER_SETTINGS_XFADE_DOWN= BASE_IMG_DIR + "left_15.png"; +IMAGE.SERVER_SETTINGS_XFADE_UP = BASE_IMG_DIR + "right_15.png"; + +IMAGE.CLOSE = BASE_IMG_DIR + "close_15.png"; +IMAGE.WORKING = BASE_IMG_DIR + "working.gif"; + +IMAGE.MENU_ITEM_CROP = BASE_IMG_DIR + "crop_30.png"; +IMAGE.MENU_ITEM_REMOVE = BASE_IMG_DIR + "remove_30.png"; +IMAGE.MENU_ITEM_DIRECTORY = BASE_IMG_DIR + "folder_30.png"; + +IMAGE.PAGINATION_FOLLOW = BASE_IMG_DIR + "follow-playing-yes.png"; +IMAGE.PAGINATION_NOFOLLOW = BASE_IMG_DIR + "follow-playing-no.png"; + +IMAGE.JUMP_CURRENT = BASE_IMG_DIR + "jump-to-current.png"; + + +/* this should be whatever the backgroun is converted from hex to dec */ +IMAGE.STREAM_BGCOLOR = "15198183"; diff --git a/theme/thexy/images/activity.gif b/theme/thexy/images/activity.gif new file mode 100644 index 0000000..915c198 Binary files /dev/null and b/theme/thexy/images/activity.gif differ diff --git a/theme/thexy/images/add.png b/theme/thexy/images/add.png new file mode 100644 index 0000000..5b051f6 Binary files /dev/null and b/theme/thexy/images/add.png differ diff --git a/theme/thexy/images/album.png b/theme/thexy/images/album.png new file mode 100644 index 0000000..91f7aff Binary files /dev/null and b/theme/thexy/images/album.png differ diff --git a/theme/thexy/images/artist.png b/theme/thexy/images/artist.png new file mode 100644 index 0000000..f6041b7 Binary files /dev/null and b/theme/thexy/images/artist.png differ diff --git a/theme/thexy/images/back.png b/theme/thexy/images/back.png new file mode 100644 index 0000000..47ba205 Binary files /dev/null and b/theme/thexy/images/back.png differ diff --git a/theme/thexy/images/bkg.jpg b/theme/thexy/images/bkg.jpg new file mode 100644 index 0000000..805a292 Binary files /dev/null and b/theme/thexy/images/bkg.jpg differ diff --git a/theme/thexy/images/browse_bkg.png b/theme/thexy/images/browse_bkg.png new file mode 100644 index 0000000..7074b12 Binary files /dev/null and b/theme/thexy/images/browse_bkg.png differ diff --git a/theme/thexy/images/browse_media.png b/theme/thexy/images/browse_media.png new file mode 100644 index 0000000..8f9d710 Binary files /dev/null and b/theme/thexy/images/browse_media.png differ diff --git a/theme/thexy/images/content_bkg.jpg b/theme/thexy/images/content_bkg.jpg new file mode 100644 index 0000000..e93e177 Binary files /dev/null and b/theme/thexy/images/content_bkg.jpg differ diff --git a/theme/thexy/images/control_bkg.jpg b/theme/thexy/images/control_bkg.jpg new file mode 100644 index 0000000..34b5e87 Binary files /dev/null and b/theme/thexy/images/control_bkg.jpg differ diff --git a/theme/thexy/images/crop_selection.png b/theme/thexy/images/crop_selection.png new file mode 100644 index 0000000..d1ecee3 Binary files /dev/null and b/theme/thexy/images/crop_selection.png differ diff --git a/theme/thexy/images/default_album_art.jpg b/theme/thexy/images/default_album_art.jpg new file mode 100644 index 0000000..96d9759 Binary files /dev/null and b/theme/thexy/images/default_album_art.jpg differ diff --git a/theme/thexy/images/directory.png b/theme/thexy/images/directory.png new file mode 100644 index 0000000..32a6c51 Binary files /dev/null and b/theme/thexy/images/directory.png differ diff --git a/theme/thexy/images/disabled.png b/theme/thexy/images/disabled.png new file mode 100644 index 0000000..561460d Binary files /dev/null and b/theme/thexy/images/disabled.png differ diff --git a/theme/thexy/images/enabled.png b/theme/thexy/images/enabled.png new file mode 100644 index 0000000..2f86f0a Binary files /dev/null and b/theme/thexy/images/enabled.png differ diff --git a/theme/thexy/images/file.png b/theme/thexy/images/file.png new file mode 100644 index 0000000..881f1b9 Binary files /dev/null and b/theme/thexy/images/file.png differ diff --git a/theme/thexy/images/fileclose.png b/theme/thexy/images/fileclose.png new file mode 100644 index 0000000..95458e0 Binary files /dev/null and b/theme/thexy/images/fileclose.png differ diff --git a/theme/thexy/images/forward.png b/theme/thexy/images/forward.png new file mode 100644 index 0000000..966b073 Binary files /dev/null and b/theme/thexy/images/forward.png differ diff --git a/theme/thexy/images/jump.png b/theme/thexy/images/jump.png new file mode 100644 index 0000000..966b073 Binary files /dev/null and b/theme/thexy/images/jump.png differ diff --git a/theme/thexy/images/lcd.jpg b/theme/thexy/images/lcd.jpg new file mode 100644 index 0000000..f826650 Binary files /dev/null and b/theme/thexy/images/lcd.jpg differ diff --git a/theme/thexy/images/next.jpg b/theme/thexy/images/next.jpg new file mode 100644 index 0000000..cc03537 Binary files /dev/null and b/theme/thexy/images/next.jpg differ diff --git a/theme/thexy/images/next_light.jpg b/theme/thexy/images/next_light.jpg new file mode 100644 index 0000000..3aac14e Binary files /dev/null and b/theme/thexy/images/next_light.jpg differ diff --git a/theme/thexy/images/pause.jpg b/theme/thexy/images/pause.jpg new file mode 100644 index 0000000..dd62087 Binary files /dev/null and b/theme/thexy/images/pause.jpg differ diff --git a/theme/thexy/images/pause_light.jpg b/theme/thexy/images/pause_light.jpg new file mode 100644 index 0000000..caac291 Binary files /dev/null and b/theme/thexy/images/pause_light.jpg differ diff --git a/theme/thexy/images/play.jpg b/theme/thexy/images/play.jpg new file mode 100644 index 0000000..03b9555 Binary files /dev/null and b/theme/thexy/images/play.jpg differ diff --git a/theme/thexy/images/play_light.jpg b/theme/thexy/images/play_light.jpg new file mode 100644 index 0000000..9afc7b2 Binary files /dev/null and b/theme/thexy/images/play_light.jpg differ diff --git a/theme/thexy/images/playlist.png b/theme/thexy/images/playlist.png new file mode 100644 index 0000000..52ac9fa Binary files /dev/null and b/theme/thexy/images/playlist.png differ diff --git a/theme/thexy/images/previous.jpg b/theme/thexy/images/previous.jpg new file mode 100644 index 0000000..0929b02 Binary files /dev/null and b/theme/thexy/images/previous.jpg differ diff --git a/theme/thexy/images/previous_light.jpg b/theme/thexy/images/previous_light.jpg new file mode 100644 index 0000000..c99dbdd Binary files /dev/null and b/theme/thexy/images/previous_light.jpg differ diff --git a/theme/thexy/images/remove_selection.png b/theme/thexy/images/remove_selection.png new file mode 100644 index 0000000..85982b7 Binary files /dev/null and b/theme/thexy/images/remove_selection.png differ diff --git a/theme/thexy/images/shadowAlpha.png b/theme/thexy/images/shadowAlpha.png new file mode 100644 index 0000000..a2561df Binary files /dev/null and b/theme/thexy/images/shadowAlpha.png differ diff --git a/theme/thexy/images/slider.png b/theme/thexy/images/slider.png new file mode 100644 index 0000000..22f854a Binary files /dev/null and b/theme/thexy/images/slider.png differ diff --git a/theme/thexy/images/stop.jpg b/theme/thexy/images/stop.jpg new file mode 100644 index 0000000..fc269b1 Binary files /dev/null and b/theme/thexy/images/stop.jpg differ diff --git a/theme/thexy/images/stop_light.jpg b/theme/thexy/images/stop_light.jpg new file mode 100644 index 0000000..c70e5e3 Binary files /dev/null and b/theme/thexy/images/stop_light.jpg differ diff --git a/theme/thexy/images/tools_bkg.jpg b/theme/thexy/images/tools_bkg.jpg new file mode 100644 index 0000000..dcdb2d5 Binary files /dev/null and b/theme/thexy/images/tools_bkg.jpg differ diff --git a/theme/thexy/theme.css b/theme/thexy/theme.css new file mode 100644 index 0000000..2c5e440 --- /dev/null +++ b/theme/thexy/theme.css @@ -0,0 +1,405 @@ +/* + Site: Thexy Theme v1.0 + Date: Jan 12, 2008 + Author: Gianni Chiappetta + Website: www.runlevel6.org + E-Mail: gianni@runlevel6.org + Description: base style sheet + Author Notes: + +*/ + +/************/ +/* ELEMENTS */ +/************/ + +body { + color: #000; + background: #e8e8e8 url(images/bkg.jpg) left top repeat-x fixed; + font-family: "Bitstream Vera Sans","Verdana"; + text-decoration: none; + word-spacing: Normal; + text-align: left; + letter-spacing: 0; + text-indent: 0px; + line-height: 14px; + font-size: 13px; +} + +a:link { text-decoration: none; color: #000; } +a:visited { text-decoration: none; color: #000; } +a:hover { text-decoration: none; color: #7d7d7d; } +a:active { text-decoration: none; color: #000; } + +a img { border: 0; } + +hr { padding: 0 !important; } + +/*------------------------- Player Control -------------------------*/ +#player_control { + position: fixed; + background: #e8e8e8 url(images/bkg.jpg) left top repeat-x fixed; + top: 0px; + left: 0px; + margin: 0; + padding: 0 0 0 0; + height: 125px; + width: 100%; + overflow: visible; +} + +/* Status Bar */ +#status_bar { + top: 95px; + left: 155px; + display: block; + height: auto; + padding: 0; + width: 372px; + text-align: left; + font-size: 10px; +} + +/* Album Art */ +#player_control #albumart { + margin: 25px 25px 25px 0; + padding: 0; + float: right; +} + +#player_control .thumbnailart { + margin: 0; + padding: 0; + border: 0; +} + +#albumart_show p.fakelink { + display: block !important; + height: 16px !important; + width: 16px !important; + text-indent: -9999px; + background: transparent url(images/fileclose.png) top left no-repeat; +} + +/* LCD */ +#player_control div.pc_artist { + top: 0px; + left: 150px; + margin: 0; + padding: 20px 10px; + width: 382px; + height: 105px; + background: transparent url(images/lcd.jpg) top left no-repeat; +} + +#player_control div.pc_artist p { + margin: 0; + padding: 0 0 10px 0; +} + +/* Controls */ +#player_control div.pc_ci { + top: 20px; + left: 14px; + width: 122px; + height: 125px; + /*background: transparent url(images/control_bkg.jpg) top left no-repeat;*/ + overflow: visible; +} + +#player_control div.pc_ci > div.nomargin { + position: fixed !important; +} + +#player_control div.pc_ci .act_button { + margin: 0 !important; + padding: 0 !important; + height: 32px !important; + float: left; +} + +#player_control div.pc_ci #previous_button, #player_control div.pc_ci #stop_button, #player_control div.pc_ci #next_button { + width: 31px !important; +} + +#player_control div.pc_ci #pp_button { + width: 29px !important; +} + +#player_control div.pc_ci #previous_button:hover, #player_control div.pc_ci #stop_button:hover, #player_control div.pc_ci #pp_button:hover, #player_control div.pc_ci #next_button:hover { opacity: 0.7; } + +/* Bitrate */ +#player_control div.pc_ci #disp_info { + display: block; + margin: 65px 0 0 0; + padding: 10px; + width: 520px; + text-align: right; + font-size: 10px; +} + +/* Quick Add */ +#player_control div.pc_ci #quickadd { + left: 15px; + width: 110px; + height: 15px; + border: 1px solid #707070; + padding: 5px; + margin: -80px 0 0 0 !important; + -moz-border-radius: 4px; + -khtml-border-radius: 4px; + line-height: 14px; + font-size: 13px; + vertical-align: middle; +} + +#player_control div.pc_ci #qa_suggestions { + margin: -34px 0 0 0; + padding: 5px 0; + border: 1px solid #707070; + background-color: #FFF; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-bottomright: 4px; + -moz-border-radius-topright: 4px; + -khtml-border-radius-bottomleft: 4px; + -khtml-border-radius-bottomright: 4px; + -khtml-border-radius-topright: 4px; + line-height: 14px; + font-size: 13px; +} + +span.qa_element { padding: 5px; } +span.qa_element[qa_selected] { background-color: #e8e8e8; } + +/* Sliders */ +#player_control div.pc_sliders { + top: 0px; + left: 572px; +} + +#player_control div.pc_sliders > div { + margin-top: 15px; +} + +#player_control div.pc_sliders .slider_txt { + padding: 5px 0 10px 0; +} + +#player_control div.pc_sliders .slider_main { + height: 10px !important; + background-color: #666; + -moz-border-radius: 5px; + -khtml-border-radius: 5px; +} + +div.slider_pointer { + width: 15px; + height: 10px; + background: transparent url(images/slider.png) top left no-repeat; + cursor: pointer; +} + +[slider_moving] { + cursor: move; +} + +/* Media */ +div.selection_menu { + top: 140px; + left: 0px; + margin: 0; + padding: 0; + width: 150px; +} + +#player_control div.selection_menu .menu_button, img.menu_button { + display: block !important; + margin: 0 auto 5px auto; + width: 128px !important; + height: 34px !important; +} + +/* Tools */ +#player_control div.pc_other { + top: 265px; + left: 0px; + margin: 0; + width: 150px; + padding: 30px 0 0 20px; + background: transparent url(images/tools_bkg.jpg) top left no-repeat; +} + +#player_control div.pc_other li, li.menuitem { + padding: 3px 0; + list-style-type: none; + line-height: 14px; + font-size: 13px; +} + +div.popup { + margin: 0 0 0 50px !important; + padding: 5px; + background-color: #e8e8e8; + border: 1px solid #707070; + width: 300px; +} + +div.popup:hover { color: #000; } + +/* Settings */ +#player_control div.pc_settings { + top: 280px; + left: 0px; + margin: 0 !important; + padding: 0 !important; + width: 150px; +} + +#settings_header p { + padding: 0 0 0 20px !important; + margin: 0 !important; +} +#settings_header p:hover { color: #7d7d7d; } + +#settings_container { + background-color: #e8e8e8; + margin: 0 !important; + padding: 0 !important; + overflow: visible !important; + width: 149px !important; +} + +#settings_content { + background: transparent url(images/content_bkg.jpg) top left repeat-x !important; + width: 145px !important; + margin: -5px 0 0 0 !important; + padding: 0 0 5px 6px !important; + font-size: 13px !important; + line-height: 14px !important; +} + +.server_settings { + padding: 3px 0; + cursor: pointer; +} + +/* Streaming */ +#streaming_display { + margin: 15px 0 0 0 !important; + padding: 0 !important; + width: 100px !important; +} + +#streaming_display li { + padding: 3px 0; + list-style-type: none; + line-height: 14px; + font-size: 13px; +} + +/* Content */ +#content { + position: absolute !important; + top: 125px !important; + left: 151px !important; + margin: 0 !important; + padding: 15px 0 0 0 !important; + /*background: transparent url(images/content_bkg.jpg) top left repeat-x fixed !important;*/ + border-left: 1px solid #acacac !important; +} + +tr[playing] { color: #FFF; background-color: #66BBCC; border-top: 1px solid #33BBCC; border-bottom: 1px solid #33BBCC; } +ul.browser_field li[selected], tr[selected] { color: #FFF; background-color: #BBB; } + +/* Browser */ +.overlay { + top: 125px !important; + left: 151px !important; + margin: 0 !important; + padding: 15px 0 0 0 !important; + position: fixed; + background: transparent url(images/browse_bkg.png) top left repeat-x !important; + border-right: 1px solid #acacac !important; + border-left: 1px solid #acacac !important; +} + +p.browse_type, input.browse_type, select.browse_type { + border: 1px outset #666; + cursor: pointer; + background-color: transparent; +} +input.browse_type { cursor: text; } + +span.plse { padding: 2px 0 2px 18px; } +ul.browser_field li { background: transparent left middle no-repeat !important; padding: 3px 0 3px 18px !important; cursor: pointer; } +span.plse[dirtype=file], li[dirtype=file] { background-image: url(images/file.png); } +span.plse[dirtype=directory], li[dirtype=directory] { background-image: url(images/directory.png); } +span.plse[dirtype=artist], li[dirtype=artist] { background-image: url(images/artist.png); } +span.plse[dirtype=album], li[dirtype=album] { background-image: url(images/album.png); } +span.plse[dirtype=playlist], li[dirtype=playlist] { background-image: url(images/playlist.png); } +li[dirtype=parent] { background-image: url(images/back.png); } +li.browser_button_add { padding-left: 18px !important; background: transparent url(images/add.png) no-repeat !important; cursor: pointer !important; } + +/* Sidebar */ +#sidebar_header, #sidebar_display { + position: fixed !important; + right: 0 !important; + margin: 0 !important; + border-left: 1px solid #acacac !important; +} + +#sidebar_header { + top: 125px !important; + padding-top: 15px !important; + font-size: 10px; + font-weight: bold; + background: transparent url(images/content_bkg.jpg) top left repeat-x !important; + z-index: 10 !important; + overflow: visible; +} + +#sidebar_header span[class=fakelink], #metadata_close { + display: block; + margin-top: -16px; + padding: 0 3px; + text-indent: -9999px; + width: 16px; + height: 16px; + background: transparent url(images/fileclose.png) top left no-repeat; + float: right; +} + +#metadata_close { float: left !important; margin: 0 !important; } + +#metadata_open_lyrics, #metadata_open_description, #metadata_open_review { + display: block; + float: left !important; + margin: 0 !important; + padding: 0 3px; + text-indent: 0px !important; + width: auto !important; + background-image: none !important; +} + +#sidebar_display { + top: 160px !important; + width: 270px; + max-width: 270px; + overflow: scroll; + font-size: small; + background-color: #e8e8e8 !important; + z-index: 9 !important; +} + +#sidebar_display #sidebar_display_txt { + padding-left: 20px; +} + +#sidebar_display ul { + list-style-position: inside; +} + +/*------------------------- Misc -------------------------*/ +.nomargin { margin: 0 !important; padding: 0 !important; } +.fakelink { cursor: pointer; color: #000; } +.fakelink:hover { color: #7d7d7d; } diff --git a/theme/thexy/theme.js b/theme/thexy/theme.js new file mode 100644 index 0000000..04e39f5 --- /dev/null +++ b/theme/thexy/theme.js @@ -0,0 +1,40 @@ + +/* number of "windows" on the directory browser */ +var BROWSER_NUM = 3; + +/* the border around the playing item in the playlist */ +var PLAYLIST_PLAYING_STYLE = "1px solid #33BBCC"; + +/* this is the color thing that you add blink in */ +var DEFAULT_BLINK_COLOR = "#C1DAD6"; + +var BASE_IMG_DIR = "../theme/thexy/images/"; + +var IMAGE = new Object(); + +IMAGE.BUTTON_PLAY = BASE_IMG_DIR + "play_light.jpg"; +IMAGE.BUTTON_STOP = BASE_IMG_DIR + "stop_light.jpg"; +IMAGE.BUTTON_PAUSE = BASE_IMG_DIR + "pause_light.jpg"; +IMAGE.BUTTON_PREVIOUS = BASE_IMG_DIR + "previous_light.jpg"; +IMAGE.BUTTON_NEXT = BASE_IMG_DIR + "next_light.jpg"; + +IMAGE.BROWSER_PLAYLIST_REMOVE = BASE_IMG_DIR + "disabled.png"; + +IMAGE.SERVER_SETTINGS_ENABLED = BASE_IMG_DIR + "enabled.png"; +IMAGE.SERVER_SETTINGS_DISABLED = BASE_IMG_DIR + "disabled.png"; +IMAGE.SERVER_SETTINGS_XFADE_DOWN= BASE_IMG_DIR + "back.png"; +IMAGE.SERVER_SETTINGS_XFADE_UP = BASE_IMG_DIR + "forward.png"; + +IMAGE.CLOSE = BASE_IMG_DIR + "fileclose.png"; +IMAGE.WORKING = BASE_IMG_DIR + "activity.gif"; + +IMAGE.MENU_ITEM_CROP = BASE_IMG_DIR + "crop_selection.png"; +IMAGE.MENU_ITEM_REMOVE = BASE_IMG_DIR + "remove_selection.png"; +IMAGE.MENU_ITEM_DIRECTORY = BASE_IMG_DIR + "browse_media.png"; + +IMAGE.PAGINATION_FOLLOW = BASE_IMG_DIR + "enabled.png"; +IMAGE.PAGINATION_NOFOLLOW = BASE_IMG_DIR + "disabled.png"; + +IMAGE.JUMP_CURRENT = BASE_IMG_DIR + "jump.png"; + +IMAGE.STREAM_BGCOLOR = "1118481";