Compare commits

...

80 Commits

Author SHA1 Message Date
Max Kellermann
964804a4c2 release v0.21.15 2019-09-25 21:24:15 +02:00
Max Kellermann
92495d2b0b decoder/mpcdec: fix bogus ReplayGain values
Apparently, libmpcdec sets gain/peak variables to zero if they are not
present.  This clashes with our formula and results in bogus values
which cause noise during playback.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/640
2019-09-13 19:52:11 +02:00
Max Kellermann
9270829b5b ReplayGainInfo: move more code to a function 2019-09-13 19:50:49 +02:00
Max Kellermann
b6243a9945 decoder/mpcdec: merge duplicate code 2019-09-13 19:50:43 +02:00
Max Kellermann
496f88653d ReplayGainInfo: add static method Undefined() 2019-09-13 19:46:39 +02:00
Max Kellermann
5ef645df97 NEWS: add missing line for 818b7e0641 2019-09-08 12:54:16 +02:00
Max Kellermann
bf49c9e4e2 decoder/{dsf,dsdiff}: precalculate bit rate 2019-09-08 12:52:02 +02:00
Max Kellermann
0da9c91af2 decoder/{dsf,dsdiff}: fix displayed bit rate
The formula did not consider the channel count.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/639
2019-09-08 12:45:05 +02:00
Max Kellermann
193e637dd9 python/build/libs: update Boost to 1.71.0 2019-09-01 13:03:50 +02:00
Max Kellermann
928bee933d python/build/libs: update expat to 2.2.7 2019-09-01 13:02:56 +02:00
Max Kellermann
4d1720c886 python/build/libs: update CURL to 7.65.3 2019-09-01 13:02:04 +02:00
Max Kellermann
8f8ed87327 python/build/libs: update FFmpeg to 4.2 2019-09-01 13:00:26 +02:00
Max Kellermann
28a441c977 python/build/libs: update Opus to 1.3.1 2019-09-01 12:59:17 +02:00
Max Kellermann
8cf50b08f2 python/build/libs: update libogg to 1.3.4 2019-09-01 12:58:26 +02:00
Max Kellermann
818b7e0641 output/solaris: include sys/stropts.h only on Solaris
This header had been available for a long time on Linux, but was
removed in glibc 2.30.  This commit moves the `#include` line inside
the `#ifdef __sun` block and adds a fake declaration of `I_FLUSH` for
the Linux build.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/630
2019-08-22 11:41:12 +02:00
Max Kellermann
e70f40fac1 increment version number to 0.21.15 2019-08-22 11:40:17 +02:00
Max Kellermann
bc89ca92b4 release v0.21.14 2019-08-21 10:47:53 +02:00
Max Kellermann
b968e1b6de output/Thread: add missing return in exception handler 2019-08-21 10:20:17 +02:00
Max Kellermann
6c9f9c136b command/all: don't create new Response instance in exception handler
The new Response instance in the `catch` block didn't have the
`command` attribute set, so the error response didn't indicate which
command had failed, which however is required in the MPD protocol.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/628
2019-08-20 20:31:36 +02:00
Max Kellermann
9bff5f9e36 client/Process, command/all: add noexcept
Clarify that those can't throw, preparing for the next commit.
2019-08-20 20:28:15 +02:00
Max Kellermann
2bf26a2ff8 command/all: remove obsolete prototype 2019-08-20 20:28:10 +02:00
Max Kellermann
e33b50d9c5 command/all: simplify return from command_process() 2019-08-20 20:26:07 +02:00
Max Kellermann
21fa44c0d5 command/all: catch all exceptions 2019-08-20 20:23:54 +02:00
Max Kellermann
44444e1b89 decoder/Thread: on late SEEK, start decoder at seek position
Previously, a bogus value (whatever happened to be still in
`start_time`) was used.
2019-08-20 20:15:08 +02:00
Max Kellermann
ca450663d0 decoder/Control: work around crash after SEEK was too late
See code comment.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/629
2019-08-20 20:01:53 +02:00
Max Kellermann
f3d16f6d1b output/Thread: fix typo in comment 2019-08-13 13:08:40 +02:00
Max Kellermann
4464cdcc67 doc/protocol.rst: add missing newline to "albumart" example
This was missing in commit 0f488dcecf
2019-08-12 20:20:17 +02:00
Fredrik Noring
2d61e526de decoder/sidplay: Fix date field to have year but not company or author
Field 2 is called <released>, formerly used as <copyright>[1][2]. It is
formatted <year><space><company or author or group>, where <year> may be
<YYYY>, <YYY?>, <YY??> or <YYYY-YY>, for example "1987", "199?", "19??"
or "1985-87". The <company or author or group> may be for example Rob
Hubbard. A full field may be for example "1987 Rob Hubbard".

This change splits the <released> field at the first <space>, to retain
the <year> part.

The 51823 SID files in High Voltage SID Collection (HVSC) version 71
have the following distribution of dates:

    333 19??         11 1990-92       6 1995-99       2 2006-08
    827 198?         88 1990-93    2140 1996        530 2007
     32 1982         69 1990-94       9 1996-97      15 2007-08
      1 1982-83      49 1990-95       2 1996-98       2 2007-09
    255 1983       3467 1991          5 1996-99       1 2007-10
    677 1984         75 1991-92    1840 1997        430 2008
    775 1985         65 1991-93       4 1997-98      23 2008-09
      3 1985-86      10 1991-94    1276 1998          1 2008-12
     10 1985-87      35 1991-97       4 1998-99     631 2009
    943 1986       3320 1992        865 1999          1 2009-10
     12 1986-87      26 1992-93      24 200?        645 2010
      5 1986-89      59 1992-94     590 2000          1 2010-12
   2083 1987          1 1992-96       4 2000-01     538 2011
     31 1987-88    2996 1993        727 2001          1 2011-12
     44 1987-89      42 1993-94     875 2002        651 2012
   2510 1988         12 1993-95       2 2002-04     811 2013
    129 1988-89       2 1993-97     844 2003        790 2014
     91 1988-90    2737 1994          3 2003-05     740 2015
     58 1988-91      16 1994-95     842 2004        792 2016
   3466 1989         20 1994-96       2 2004-05     775 2017
     95 1989-90      17 1994-97     707 2005        638 2018
    150 1989-91    2271 1995          1 2005-06     284 2019
   1077 199?          2 1995-96       2 2005-07
   2834 1990          4 1995-97     785 2006
    119 1990-91       2 1995-98       6 2006-07

References:

[1] https://www.hvsc.c64.org/download/C64Music/DOCUMENTS/SID_file_format.txt
[2] https://hvsc.c64.org/info
2019-08-10 10:50:51 +02:00
Fredrik Noring
7723c481db decoder/sidplay: Fix windows-1252 to utf-8 string conversion
High Voltage SID Collection (HVSC) metadata fields are encoded in
windows-1252, as described in DOCUMENTS/SID_file_format.txt:

https://www.hvsc.c64.org/download/C64Music/DOCUMENTS/SID_file_format.txt

If utf-8 transcoding fails, or the ICU library is unavailable, fall
back to plain ASCII and replace other characters with '?'.
2019-08-10 10:45:02 +02:00
Fredrik Noring
0ed10542cc decoder/sidplay: Fix song length initialisation during container scan
The song length was previously undetermined.
2019-08-09 15:39:36 +02:00
Max Kellermann
ab830f9afd increment version number to 0.21.14 2019-08-09 15:38:01 +02:00
Max Kellermann
d4d2bc072e release v0.21.13 2019-08-06 11:35:42 +02:00
Max Kellermann
bcccc8f66c output/jack: use jack_free() for Windows compatibility 2019-08-06 11:34:56 +02:00
Max Kellermann
848c63e2d5 output/jack: use std::atomic_bool for "shutdown" and "pause"
Without this, the compiler may optimize accesses away.
2019-08-06 11:34:00 +02:00
Max Kellermann
f6d0310f9c output/jack: use SIZE_MAX instead of (size_t)-1 2019-08-06 11:33:52 +02:00
Max Kellermann
3ef043392c input/cdio_paranoia: drop support for libcdio-paranoia older than 10.2+0.93+1
Version 10.2+0.93+1 was released five years ago in 2014 and is the
first version to feature cdio_cddap_free_messages().  There is no way
to check the libcdio-paranoia version at compile time, so let's just
remove support for older versions instead of attempting to fix the
cdio_cddap_free_messages() check at build time.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/613
2019-08-06 11:09:36 +02:00
Max Kellermann
864d6f312d Revert "decoder/mad: use MAD_F_MIN and MAD_F_MAX"
This reverts commit f7ed7446ae.  It was
a bad idea, because MAD_F_MIN and MAD_F_MAX do not represent the
clamping limits, but the theoretical minimum and maximum values of the
mad_fixed_t data type.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/617
2019-08-05 13:07:41 +02:00
Max Kellermann
f44c67de09 increment version number to 0.21.13 2019-08-05 13:05:54 +02:00
Max Kellermann
ae19bda1f2 release v0.21.12 2019-08-03 12:48:20 +02:00
Max Kellermann
f2d8fd769d player/Thread: don't restart unseekable song after failed seek attempt
The check IsSeekableCurrentSong() was added by commit
44b200240f in version 0.20.19, but it
caused a regression: by doing the branch only if the current song is
seekable, the player would restart the current song if it was not
seekable, and later the initial seek would fail; but we already know
it's not seekable, and so we should fail early.
2019-08-03 12:30:10 +02:00
Max Kellermann
9661062ae2 decoder/mad: pass const reference to RecoverFrameError() 2019-08-03 11:59:41 +02:00
Max Kellermann
2a07354cad decoder/mad: change integers to size_t 2019-08-03 11:44:02 +02:00
Max Kellermann
fc18fd571c decoder/mad: return from SynthAndSubmit() early 2019-08-03 11:42:05 +02:00
Max Kellermann
51abed9732 decoder/mad: pass mad_pcm to mad_fixed_to_24_buffer() 2019-08-03 11:40:06 +02:00
Max Kellermann
d00afc912c decoder/mad: eliminate the loop in SubmitPCM()
libmad has a hard-coded maximum PCM buffer size; if we make our
output_buffer just as large, we can avoid the loop, because any
possible size will fit.
2019-08-03 11:36:05 +02:00
Max Kellermann
9d0fe725eb decoder/mad: rename a few misnamed methods 2019-08-03 11:32:42 +02:00
Max Kellermann
8a432c9b7f decoder/mad: move code to LoadNextFrame() 2019-08-03 11:32:06 +02:00
Max Kellermann
187204f03c decoder/mad: move code to HandleCurrentFrame() 2019-08-03 11:32:06 +02:00
Max Kellermann
5e5fadb5f2 decoder/mad: remove unnecessary initializers
These will not be used until they are initialized in SyncAndSend().
2019-08-03 08:49:26 +02:00
Max Kellermann
952c793235 decoder/mad: subtract libmad decoder delay from LAME encoder padding
Apparently, libmad not only inserts 529 samples of silence at the
beginning of the file, but also removes them at the end.

This solves the last piece of
https://github.com/MusicPlayerDaemon/MPD/issues/601

Closes https://github.com/MusicPlayerDaemon/MPD/issues/601
2019-08-03 08:35:00 +02:00
Max Kellermann
3e3d8c7f9d decoder/mad: pad the input buffer with zero bytes and end of file
libmad requires padding the input buffer with "MAD_BUFFER_GUARD" zero
bytes at the end of the file, or else it is unable to decode the last
frame.

This fixes yet another bug which prevented this plugin from decoding
the last frame, see
https://github.com/MusicPlayerDaemon/MPD/issues/601
2019-08-03 08:32:27 +02:00
Max Kellermann
9b99a9897a decoder/mad: don't count the Xing/LAME metadata frame
The Xing/LAME frame indicates how many frames there are, but that
excludes the initial Xing/LAME frame.  Therefore, it should not be
counted.

This fixes an off-by-one bug which caused the last frame to be
skipped, fixing one part of
https://github.com/MusicPlayerDaemon/MPD/issues/601
2019-08-03 08:25:48 +02:00
Max Kellermann
4f56fdc397 decoder/mad: make "current_frame" zero-based
Increment "current_frame" after processing the frame.
2019-08-03 08:24:25 +02:00
Max Kellermann
c87d6825ec decoder/mad: add API documentation 2019-08-03 08:07:30 +02:00
Max Kellermann
00830a20e3 decoder/mad: convert to class, make almost everything private 2019-08-03 07:52:51 +02:00
Max Kellermann
d39d2874b4 decoder/mad: move code to methods RunDecoder(), RunScan() 2019-08-03 07:49:41 +02:00
Max Kellermann
a0a74951b8 decoder/mad: eliminate attribute "bit_rate"
This also fixes a bug which caused the bit rate to not update after
seeking.
2019-08-03 00:38:45 +02:00
Max Kellermann
779a6855ff decoder/mad: add noexcept 2019-08-03 00:28:59 +02:00
Max Kellermann
f7ed7446ae decoder/mad: use MAD_F_MIN and MAD_F_MAX 2019-08-03 00:27:59 +02:00
Max Kellermann
9d44a6d2ae decoder/mad: use Clamp() 2019-08-03 00:26:57 +02:00
Max Kellermann
10da9ee7ba decoder/mad: refactor local variables in FillBuffer() 2019-08-02 23:19:11 +02:00
Max Kellermann
f9eff31205 decoder/mad: use sizeof(input_buffer) 2019-08-02 23:19:11 +02:00
Max Kellermann
1d74a029a2 decoder/mad: simplify variable initialization in FillBuffer() 2019-08-02 23:19:11 +02:00
Max Kellermann
6b8ca514bb decoder/mad: fix broken log message
Broken since commit f8bfea8bae
2019-08-02 22:58:16 +02:00
Max Kellermann
f51e555154 decoder/mad: change "mp3_" suffix to "mad_" 2019-08-02 22:49:55 +02:00
Max Kellermann
61a3c69a06 decoder/mad: make enums strictly-typed 2019-08-02 22:49:55 +02:00
Max Kellermann
089615a01e decoder/mad: include cleanup 2019-08-02 22:49:55 +02:00
Max Kellermann
52bee8f81f util/StaticFifoBuffer: add GetAvailable() 2019-08-02 22:49:55 +02:00
Max Kellermann
adc25e648f util/StaticFifoBuffer: add constexpr 2019-08-02 22:49:33 +02:00
Max Kellermann
31da8eac9b util/StaticFifoBuffer: add noexcept 2019-08-02 22:49:05 +02:00
Max Kellermann
e00464435b util/Compiler.h: move compiler version checks to meson.build 2019-08-02 15:53:16 +02:00
Diomendius
b81138bda1 Fix JACK plugin outputting only to left channel
The JACK output plugin would not correctly upmix mono input files when exactly 2 output ports were configured. This fixes that.
2019-08-02 15:52:20 +02:00
Max Kellermann
6de088140b lib/xiph/OggVisitor: invoke OnOggPacket() with the "E_O_S" packet
The "end of stream" packet is not special; it contains normal data,
and thus we should pass it to OnOggPacket().

This fixes one part of https://github.com/MusicPlayerDaemon/MPD/issues/601
2019-08-02 14:04:08 +02:00
Max Kellermann
86d0534638 lib/xiph/OggVisitor: more API documentation 2019-08-02 13:56:00 +02:00
Max Kellermann
1033dbca2b playlist/Song: add missing includes 2019-07-29 11:31:30 +02:00
Max Kellermann
b955334882 decoder/opus: ignore case in replay gain tag names
Closes https://github.com/MusicPlayerDaemon/MPD/issues/604
2019-07-29 10:40:37 +02:00
Max Kellermann
90ea3bf985 playlist/Song: support backslash in relative URIs
Closes https://github.com/MusicPlayerDaemon/MPD/issues/607
2019-07-29 09:58:53 +02:00
Max Kellermann
83b0871248 test/test_translate_song: remove unused variable "s1" 2019-07-29 09:52:57 +02:00
Max Kellermann
d8aec4b2dc test/run_decoder: catch StopDecoder
This exception is usually thrown by class DecoderBridge, but the Opus
plugin (ab)uses it as well, so we need to catch it.
2019-07-12 17:49:12 +02:00
Max Kellermann
39b302dcad increment version number to 0.21.12 2019-07-12 17:22:20 +02:00
35 changed files with 590 additions and 326 deletions

39
NEWS
View File

@@ -1,3 +1,42 @@
ver 0.21.15 (2019/09/25)
* decoder
- dsdiff, dsf: fix displayed bit rate
- mpcdec: fix bogus ReplayGain values
* output
- solaris: fix build with glibc 2.30
ver 0.21.14 (2019/08/21)
* decoder
- sidplay: show track durations in database
- sidplay: convert tag values from Windows-1252 charset
- sidplay: strip text from "Date" tag
* player
- fix crash after song change
- fix seek position after restarting the decoder
* protocol
- include command name in error responses
ver 0.21.13 (2019/08/06)
* input
- cdio_paranoia: require libcdio-paranoia 10.2+0.93+1
* decoder
- mad: fix crackling sound (0.21.12 regression)
* output
- jack: improved Windows compatibility
ver 0.21.12 (2019/08/03)
* decoder
- mad: update bit rate after seeking
- mad: fix several bugs preventing the plugin from decoding the last frame
- opus: ignore case in replay gain tag names
- opus, vorbis: decode the "end of stream" packet
* output
- jack: fix mono-to-stereo conversion
* player
- don't restart unseekable song after failed seek attempt
* Windows
- support backslash in relative URIs loaded from playlists
ver 0.21.11 (2019/07/03) ver 0.21.11 (2019/07/03)
* input * input
- tidal: deprecated because Tidal has changed the protocol - tidal: deprecated because Tidal has changed the protocol

View File

@@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.musicpd" package="org.musicpd"
android:installLocation="auto" android:installLocation="auto"
android:versionCode="34" android:versionCode="38"
android:versionName="0.21.11"> android:versionName="0.21.15">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="26"/> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="26"/>

View File

@@ -38,7 +38,7 @@ author = 'Max Kellermann'
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '0.21.11' version = '0.21.15'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = version release = version

View File

@@ -824,7 +824,8 @@ The music database
albumart albumart
size: 1024768 size: 1024768
binary: 8192 binary: 8192
<8192 bytes>OK <8192 bytes>
OK
:command:`count {FILTER} [group {GROUPTYPE}]` :command:`count {FILTER} [group {GROUPTYPE}]`
Count the number of songs and their total playtime in Count the number of songs and their total playtime in

View File

@@ -1,7 +1,7 @@
project( project(
'mpd', 'mpd',
['c', 'cpp'], ['c', 'cpp'],
version: '0.21.11', version: '0.21.15',
meson_version: '>= 0.49.0', meson_version: '>= 0.49.0',
default_options: [ default_options: [
'c_std=c99', 'c_std=c99',
@@ -15,6 +15,12 @@ version_cxx = vcs_tag(input: 'src/GitVersion.cxx', output: 'GitVersion.cxx')
compiler = meson.get_compiler('cpp') compiler = meson.get_compiler('cpp')
c_compiler = meson.get_compiler('c') c_compiler = meson.get_compiler('c')
if compiler.get_id() == 'gcc' and compiler.version().version_compare('<6')
warning('Your GCC version is too old. You need at least version 6.')
elif compiler.get_id() == 'clang' and compiler.version().version_compare('<3')
warning('Your clang version is too old. You need at least version 3.')
endif
conf = configuration_data() conf = configuration_data()
conf.set_quoted('PACKAGE', meson.project_name()) conf.set_quoted('PACKAGE', meson.project_name())
conf.set_quoted('PACKAGE_NAME', meson.project_name()) conf.set_quoted('PACKAGE_NAME', meson.project_name())

View File

@@ -15,8 +15,8 @@ libmpdclient = MesonProject(
) )
libogg = AutotoolsProject( libogg = AutotoolsProject(
'http://downloads.xiph.org/releases/ogg/libogg-1.3.3.tar.xz', 'http://downloads.xiph.org/releases/ogg/libogg-1.3.4.tar.xz',
'4f3fc6178a533d392064f14776b23c397ed4b9f48f5de297aba73b643f955c08', 'c163bc12bc300c401b6aa35907ac682671ea376f13ae0969a220f7ddf71893fe',
'lib/libogg.a', 'lib/libogg.a',
[ [
'--disable-shared', '--enable-static', '--disable-shared', '--enable-static',
@@ -38,8 +38,8 @@ libvorbis = AutotoolsProject(
) )
opus = AutotoolsProject( opus = AutotoolsProject(
'https://archive.mozilla.org/pub/opus/opus-1.3.tar.gz', 'https://archive.mozilla.org/pub/opus/opus-1.3.1.tar.gz',
'4f3d69aefdf2dbaf9825408e452a8a414ffc60494c70633560700398820dc550', '65b58e1e25b2a114157014736a3d9dfeaad8d41be1c8179866f144a2fb44ff9d',
'lib/libopus.a', 'lib/libopus.a',
[ [
'--disable-shared', '--enable-static', '--disable-shared', '--enable-static',
@@ -112,8 +112,8 @@ liblame = AutotoolsProject(
) )
ffmpeg = FfmpegProject( ffmpeg = FfmpegProject(
'http://ffmpeg.org/releases/ffmpeg-4.1.3.tar.xz', 'http://ffmpeg.org/releases/ffmpeg-4.2.tar.xz',
'0c3020452880581a8face91595b239198078645e7d7184273b8bcc7758beb63d', '023f10831a97ad93d798f53a3640e55cd564abfeba807ecbe8524dac4fedecd5',
'lib/libavcodec.a', 'lib/libavcodec.a',
[ [
'--disable-shared', '--enable-static', '--disable-shared', '--enable-static',
@@ -341,8 +341,8 @@ ffmpeg = FfmpegProject(
) )
curl = AutotoolsProject( curl = AutotoolsProject(
'http://curl.haxx.se/download/curl-7.64.1.tar.xz', 'http://curl.haxx.se/download/curl-7.65.3.tar.xz',
'9252332a7f871ce37bfa7f78bdd0a0e3924d8187cc27cb57c76c9474a7168fb3', 'f2d98854813948d157f6a91236ae34ca4a1b4cb302617cebad263d79b0235fea',
'lib/libcurl.a', 'lib/libcurl.a',
[ [
'--disable-shared', '--enable-static', '--disable-shared', '--enable-static',
@@ -365,8 +365,8 @@ curl = AutotoolsProject(
) )
libexpat = AutotoolsProject( libexpat = AutotoolsProject(
'https://github.com/libexpat/libexpat/releases/download/R_2_2_6/expat-2.2.6.tar.bz2', 'https://github.com/libexpat/libexpat/releases/download/R_2_2_7/expat-2.2.7.tar.bz2',
'17b43c2716d521369f82fc2dc70f359860e90fa440bea65b3b85f0b246ea81f2', 'cbc9102f4a31a8dafd42d642e9a3aa31e79a0aedaa1f6efd2795ebc83174ec18',
'lib/libexpat.a', 'lib/libexpat.a',
[ [
'--disable-shared', '--enable-static', '--disable-shared', '--enable-static',
@@ -392,7 +392,7 @@ libnfs = AutotoolsProject(
) )
boost = BoostProject( boost = BoostProject(
'http://downloads.sourceforge.net/project/boost/boost/1.70.0/boost_1_70_0.tar.bz2', 'https://dl.bintray.com/boostorg/release/1.71.0/source/boost_1_71_0.tar.bz2',
'430ae8354789de4fd19ee52f3b1f739e1fba576f0aded0897c3c2bc00fb38778', 'd73a8da01e8bf8c7eda40b4c84915071a8c8a0df4a6734537ddde4a8580524ee',
'include/boost/version.hpp', 'include/boost/version.hpp',
) )

View File

@@ -38,6 +38,10 @@ struct ReplayGainTuple {
return gain > -100; return gain > -100;
} }
static constexpr ReplayGainTuple Undefined() noexcept {
return {-200.0f, 0.0f};
}
gcc_pure gcc_pure
float CalculateScale(const ReplayGainConfig &config) const noexcept; float CalculateScale(const ReplayGainConfig &config) const noexcept;
}; };
@@ -49,6 +53,13 @@ struct ReplayGainInfo {
return track.IsDefined() || album.IsDefined(); return track.IsDefined() || album.IsDefined();
} }
static constexpr ReplayGainInfo Undefined() noexcept {
return {
ReplayGainTuple::Undefined(),
ReplayGainTuple::Undefined(),
};
}
const ReplayGainTuple &Get(ReplayGainMode mode) const noexcept { const ReplayGainTuple &Get(ReplayGainMode mode) const noexcept {
return mode == ReplayGainMode::ALBUM return mode == ReplayGainMode::ALBUM
? (album.IsDefined() ? album : track) ? (album.IsDefined() ? album : track)

View File

@@ -35,6 +35,6 @@ extern size_t client_max_command_list_size;
extern size_t client_max_output_buffer_size; extern size_t client_max_output_buffer_size;
CommandResult CommandResult
client_process_line(Client &client, char *line); client_process_line(Client &client, char *line) noexcept;
#endif #endif

View File

@@ -30,7 +30,7 @@
static CommandResult static CommandResult
client_process_command_list(Client &client, bool list_ok, client_process_command_list(Client &client, bool list_ok,
std::list<std::string> &&list) std::list<std::string> &&list) noexcept
{ {
CommandResult ret = CommandResult::OK; CommandResult ret = CommandResult::OK;
unsigned num = 0; unsigned num = 0;
@@ -51,7 +51,7 @@ client_process_command_list(Client &client, bool list_ok,
} }
CommandResult CommandResult
client_process_line(Client &client, char *line) client_process_line(Client &client, char *line) noexcept
{ {
CommandResult ret; CommandResult ret;

View File

@@ -206,9 +206,10 @@ static constexpr struct command commands[] = {
static constexpr unsigned num_commands = ARRAY_SIZE(commands); static constexpr unsigned num_commands = ARRAY_SIZE(commands);
gcc_pure
static bool static bool
command_available(gcc_unused const Partition &partition, command_available(gcc_unused const Partition &partition,
gcc_unused const struct command *cmd) gcc_unused const struct command *cmd) noexcept
{ {
#ifdef ENABLE_SQLITE #ifdef ENABLE_SQLITE
if (StringIsEqual(cmd->cmd, "sticker")) if (StringIsEqual(cmd->cmd, "sticker"))
@@ -235,7 +236,7 @@ command_available(gcc_unused const Partition &partition,
static CommandResult static CommandResult
PrintAvailableCommands(Response &r, const Partition &partition, PrintAvailableCommands(Response &r, const Partition &partition,
unsigned permission) unsigned permission) noexcept
{ {
for (unsigned i = 0; i < num_commands; ++i) { for (unsigned i = 0; i < num_commands; ++i) {
const struct command *cmd = &commands[i]; const struct command *cmd = &commands[i];
@@ -249,7 +250,7 @@ PrintAvailableCommands(Response &r, const Partition &partition,
} }
static CommandResult static CommandResult
PrintUnavailableCommands(Response &r, unsigned permission) PrintUnavailableCommands(Response &r, unsigned permission) noexcept
{ {
for (unsigned i = 0; i < num_commands; ++i) { for (unsigned i = 0; i < num_commands; ++i) {
const struct command *cmd = &commands[i]; const struct command *cmd = &commands[i];
@@ -276,7 +277,7 @@ handle_not_commands(Client &client, gcc_unused Request request, Response &r)
} }
void void
command_init() command_init() noexcept
{ {
#ifndef NDEBUG #ifndef NDEBUG
/* ensure that the command list is sorted */ /* ensure that the command list is sorted */
@@ -285,8 +286,9 @@ command_init()
#endif #endif
} }
gcc_pure
static const struct command * static const struct command *
command_lookup(const char *name) command_lookup(const char *name) noexcept
{ {
unsigned a = 0, b = num_commands, i; unsigned a = 0, b = num_commands, i;
@@ -308,7 +310,7 @@ command_lookup(const char *name)
static bool static bool
command_check_request(const struct command *cmd, Response &r, command_check_request(const struct command *cmd, Response &r,
unsigned permission, Request args) unsigned permission, Request args) noexcept
{ {
if (cmd->permission != (permission & cmd->permission)) { if (cmd->permission != (permission & cmd->permission)) {
r.FormatError(ACK_ERROR_PERMISSION, r.FormatError(ACK_ERROR_PERMISSION,
@@ -342,7 +344,7 @@ command_check_request(const struct command *cmd, Response &r,
static const struct command * static const struct command *
command_checked_lookup(Response &r, unsigned permission, command_checked_lookup(Response &r, unsigned permission,
const char *cmd_name, Request args) const char *cmd_name, Request args) noexcept
{ {
const struct command *cmd = command_lookup(cmd_name); const struct command *cmd = command_lookup(cmd_name);
if (cmd == nullptr) { if (cmd == nullptr) {
@@ -360,8 +362,8 @@ command_checked_lookup(Response &r, unsigned permission,
} }
CommandResult CommandResult
command_process(Client &client, unsigned num, char *line) command_process(Client &client, unsigned num, char *line) noexcept
try { {
Response r(client, num); Response r(client, num);
/* get the command name (first word on the line) */ /* get the command name (first word on the line) */
@@ -389,34 +391,33 @@ try {
char *argv[COMMAND_ARGV_MAX]; char *argv[COMMAND_ARGV_MAX];
Request args(argv, 0); Request args(argv, 0);
/* now parse the arguments (quoted or unquoted) */ try {
/* now parse the arguments (quoted or unquoted) */
while (true) { while (true) {
if (args.size == COMMAND_ARGV_MAX) { if (args.size == COMMAND_ARGV_MAX) {
r.Error(ACK_ERROR_ARG, "Too many arguments"); r.Error(ACK_ERROR_ARG, "Too many arguments");
return CommandResult::ERROR; return CommandResult::ERROR;
}
char *a = tokenizer.NextParam();
if (a == nullptr)
break;
argv[args.size++] = a;
} }
char *a = tokenizer.NextParam(); /* look up and invoke the command handler */
if (a == nullptr)
break;
argv[args.size++] = a; const struct command *cmd =
command_checked_lookup(r, client.GetPermission(),
cmd_name, args);
if (cmd == nullptr)
return CommandResult::ERROR;
return cmd->handler(client, args, r);
} catch (...) {
PrintError(r, std::current_exception());
return CommandResult::ERROR;
} }
/* look up and invoke the command handler */
const struct command *cmd =
command_checked_lookup(r, client.GetPermission(),
cmd_name, args);
CommandResult ret = cmd
? cmd->handler(client, args, r)
: CommandResult::ERROR;
return ret;
} catch (const std::exception &e) {
Response r(client, num);
PrintError(r, std::current_exception());
return CommandResult::ERROR;
} }

View File

@@ -25,12 +25,9 @@
class Client; class Client;
void void
command_init(); command_init() noexcept;
void
command_finish();
CommandResult CommandResult
command_process(Client &client, unsigned num, char *line); command_process(Client &client, unsigned num, char *line) noexcept;
#endif #endif

View File

@@ -147,6 +147,18 @@ DecoderControl::Seek(SongTime t)
seek_error = false; seek_error = false;
SynchronousCommandLocked(DecoderCommand::SEEK); SynchronousCommandLocked(DecoderCommand::SEEK);
while (state == DecoderState::START)
/* If the decoder falls back to DecoderState::START,
this means that our SEEK command arrived too late,
and the decoder had meanwhile finished decoding and
went idle. Our SEEK command is finished, but that
means only that the decoder thread has launched the
decoder. To work around illegal states, we wait
until the decoder plugin has become ready. This is
a kludge, built on top of the "late seek" kludge.
Not exactly elegant, sorry. */
WaitForDecoder();
if (seek_error) if (seek_error)
throw std::runtime_error("Decoder failed to seek"); throw std::runtime_error("Decoder failed to seek");
} }

View File

@@ -320,6 +320,11 @@ public:
gcc_pure gcc_pure
bool IsCurrentSong(const DetachedSong &_song) const noexcept; bool IsCurrentSong(const DetachedSong &_song) const noexcept;
gcc_pure
bool IsUnseekableCurrentSong(const DetachedSong &_song) const noexcept {
return !seekable && IsCurrentSong(_song);
}
gcc_pure gcc_pure
bool IsSeekableCurrentSong(const DetachedSong &_song) const noexcept { bool IsSeekableCurrentSong(const DetachedSong &_song) const noexcept {
return seekable && IsCurrentSong(_song); return seekable && IsCurrentSong(_song);

View File

@@ -455,6 +455,11 @@ static void
decoder_run_song(DecoderControl &dc, decoder_run_song(DecoderControl &dc,
const DetachedSong &song, const char *uri, Path path_fs) const DetachedSong &song, const char *uri, Path path_fs)
{ {
if (dc.command == DecoderCommand::SEEK)
/* if the SEEK command arrived too late, start the
decoder at the seek position */
dc.start_time = dc.seek_time;
DecoderBridge bridge(dc, dc.start_time.IsPositive(), DecoderBridge bridge(dc, dc.start_time.IsPositive(),
/* pass the song tag only if it's /* pass the song tag only if it's
authoritative, i.e. if it's a local authoritative, i.e. if it's a local

View File

@@ -362,6 +362,7 @@ dsdiff_decode_chunk(DecoderClient &client, InputStream &is,
unsigned channels, unsigned sample_rate, unsigned channels, unsigned sample_rate,
const offset_type total_bytes) const offset_type total_bytes)
{ {
const unsigned kbit_rate = channels * sample_rate / 1000;
const offset_type start_offset = is.GetOffset(); const offset_type start_offset = is.GetOffset();
uint8_t buffer[8192]; uint8_t buffer[8192];
@@ -408,7 +409,7 @@ dsdiff_decode_chunk(DecoderClient &client, InputStream &is,
bit_reverse_buffer(buffer, buffer + nbytes); bit_reverse_buffer(buffer, buffer + nbytes);
cmd = client.SubmitData(is, buffer, nbytes, cmd = client.SubmitData(is, buffer, nbytes,
sample_rate / 1000); kbit_rate);
} }
return true; return true;

View File

@@ -256,6 +256,7 @@ dsf_decode_chunk(DecoderClient &client, InputStream &is,
offset_type n_blocks, offset_type n_blocks,
bool bitreverse) bool bitreverse)
{ {
const unsigned kbit_rate = channels * sample_rate / 1000;
const size_t block_size = channels * DSF_BLOCK_SIZE; const size_t block_size = channels * DSF_BLOCK_SIZE;
const offset_type start_offset = is.GetOffset(); const offset_type start_offset = is.GetOffset();
@@ -291,7 +292,7 @@ dsf_decode_chunk(DecoderClient &client, InputStream &is,
cmd = client.SubmitData(is, cmd = client.SubmitData(is,
interleaved_buffer, block_size, interleaved_buffer, block_size,
sample_rate / 1000); kbit_rate);
++i; ++i;
} }

View File

@@ -21,14 +21,13 @@
#include "MadDecoderPlugin.hxx" #include "MadDecoderPlugin.hxx"
#include "../DecoderAPI.hxx" #include "../DecoderAPI.hxx"
#include "input/InputStream.hxx" #include "input/InputStream.hxx"
#include "config/Block.hxx"
#include "tag/Id3Scan.hxx" #include "tag/Id3Scan.hxx"
#include "tag/Id3ReplayGain.hxx" #include "tag/Id3ReplayGain.hxx"
#include "tag/Rva2.hxx"
#include "tag/Handler.hxx" #include "tag/Handler.hxx"
#include "tag/ReplayGain.hxx" #include "tag/ReplayGain.hxx"
#include "tag/MixRamp.hxx" #include "tag/MixRamp.hxx"
#include "CheckAudioFormat.hxx" #include "CheckAudioFormat.hxx"
#include "util/Clamp.hxx"
#include "util/StringCompare.hxx" #include "util/StringCompare.hxx"
#include "util/Domain.hxx" #include "util/Domain.hxx"
#include "Log.hxx" #include "Log.hxx"
@@ -40,8 +39,6 @@
#include <id3tag.h> #include <id3tag.h>
#endif #endif
#include <stdexcept>
#include <assert.h> #include <assert.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
@@ -49,17 +46,17 @@
static constexpr unsigned long FRAMES_CUSHION = 2000; static constexpr unsigned long FRAMES_CUSHION = 2000;
enum mp3_action { enum class MadDecoderAction {
DECODE_SKIP = -3, SKIP,
DECODE_BREAK = -2, BREAK,
DECODE_CONT = -1, CONT,
DECODE_OK = 0 OK
}; };
enum muteframe { enum class MadDecoderMuteFrame {
MUTEFRAME_NONE, NONE,
MUTEFRAME_SKIP, SKIP,
MUTEFRAME_SEEK SEEK
}; };
/* the number of samples of silence the decoder inserts at start */ /* the number of samples of silence the decoder inserts at start */
@@ -79,7 +76,7 @@ ToSongTime(mad_timer_t t) noexcept
} }
static inline int32_t static inline int32_t
mad_fixed_to_24_sample(mad_fixed_t sample) mad_fixed_to_24_sample(mad_fixed_t sample) noexcept
{ {
static constexpr unsigned bits = 24; static constexpr unsigned bits = 24;
static constexpr mad_fixed_t MIN = -MAD_F_ONE; static constexpr mad_fixed_t MIN = -MAD_F_ONE;
@@ -88,73 +85,79 @@ mad_fixed_to_24_sample(mad_fixed_t sample)
/* round */ /* round */
sample = sample + (1L << (MAD_F_FRACBITS - bits)); sample = sample + (1L << (MAD_F_FRACBITS - bits));
/* clip */
if (gcc_unlikely(sample > MAX))
sample = MAX;
else if (gcc_unlikely(sample < MIN))
sample = MIN;
/* quantize */ /* quantize */
return sample >> (MAD_F_FRACBITS + 1 - bits); return Clamp(sample, MIN, MAX)
>> (MAD_F_FRACBITS + 1 - bits);
} }
static void static void
mad_fixed_to_24_buffer(int32_t *dest, const struct mad_synth *synth, mad_fixed_to_24_buffer(int32_t *dest, const struct mad_pcm &src,
unsigned int start, unsigned int end, size_t start, size_t end,
unsigned int num_channels) unsigned int num_channels)
{ {
for (unsigned i = start; i < end; ++i) for (size_t i = start; i < end; ++i)
for (unsigned c = 0; c < num_channels; ++c) for (unsigned c = 0; c < num_channels; ++c)
*dest++ = mad_fixed_to_24_sample(synth->pcm.samples[c][i]); *dest++ = mad_fixed_to_24_sample(src.samples[c][i]);
} }
static bool static bool
mp3_plugin_init(const ConfigBlock &block) mad_plugin_init(const ConfigBlock &block)
{ {
gapless_playback = block.GetBlockValue("gapless", gapless_playback = block.GetBlockValue("gapless",
DEFAULT_GAPLESS_MP3_PLAYBACK); DEFAULT_GAPLESS_MP3_PLAYBACK);
return true; return true;
} }
struct MadDecoder { class MadDecoder {
static constexpr size_t READ_BUFFER_SIZE = 40960; static constexpr size_t READ_BUFFER_SIZE = 40960;
static constexpr size_t MP3_DATA_OUTPUT_BUFFER_SIZE = 2048;
struct mad_stream stream; struct mad_stream stream;
struct mad_frame frame; struct mad_frame frame;
struct mad_synth synth; struct mad_synth synth;
mad_timer_t timer; mad_timer_t timer;
unsigned char input_buffer[READ_BUFFER_SIZE]; unsigned char input_buffer[READ_BUFFER_SIZE];
int32_t output_buffer[MP3_DATA_OUTPUT_BUFFER_SIZE]; int32_t output_buffer[sizeof(mad_pcm::samples) / sizeof(mad_fixed_t)];
SignedSongTime total_time; SignedSongTime total_time;
SongTime elapsed_time; SongTime elapsed_time;
SongTime seek_time; SongTime seek_time;
enum muteframe mute_frame = MUTEFRAME_NONE; MadDecoderMuteFrame mute_frame = MadDecoderMuteFrame::NONE;
long *frame_offsets = nullptr; long *frame_offsets = nullptr;
mad_timer_t *times = nullptr; mad_timer_t *times = nullptr;
unsigned long highest_frame = 0; size_t highest_frame = 0;
unsigned long max_frames = 0; size_t max_frames = 0;
unsigned long current_frame = 0; size_t current_frame = 0;
unsigned int drop_start_frames = 0; unsigned int drop_start_frames;
unsigned int drop_end_frames = 0; unsigned int drop_end_frames;
unsigned int drop_start_samples = 0; unsigned int drop_start_samples = 0;
unsigned int drop_end_samples = 0; unsigned int drop_end_samples = 0;
bool found_replay_gain = false; bool found_replay_gain = false;
bool found_first_frame = false; bool found_first_frame = false;
bool decoded_first_frame = false; bool decoded_first_frame = false;
unsigned long bit_rate;
/**
* If this flag is true, then end-of-file was seen and a
* padding of 8 zero bytes were appended to #input_buffer, to
* allow libmad to decode the last frame.
*/
bool was_eof = false;
DecoderClient *const client; DecoderClient *const client;
InputStream &input_stream; InputStream &input_stream;
enum mad_layer layer = mad_layer(0); enum mad_layer layer = mad_layer(0);
MadDecoder(DecoderClient *client, InputStream &input_stream); public:
~MadDecoder(); MadDecoder(DecoderClient *client, InputStream &input_stream) noexcept;
~MadDecoder() noexcept;
bool Seek(long offset); void RunDecoder() noexcept;
bool FillBuffer(); bool RunScan(TagHandler &handler) noexcept;
void ParseId3(size_t tagsize, Tag *tag);
enum mp3_action DecodeNextFrameHeader(Tag *tag); private:
enum mp3_action DecodeNextFrame(); bool Seek(long offset) noexcept;
bool FillBuffer() noexcept;
void ParseId3(size_t tagsize, Tag *tag) noexcept;
MadDecoderAction DecodeNextFrameHeader(Tag *tag) noexcept;
MadDecoderAction DecodeNextFrame() noexcept;
gcc_pure gcc_pure
offset_type ThisFrameOffset() const noexcept; offset_type ThisFrameOffset() const noexcept;
@@ -165,11 +168,11 @@ struct MadDecoder {
/** /**
* Attempt to calulcate the length of the song from filesize * Attempt to calulcate the length of the song from filesize
*/ */
void FileSizeToSongLength(); void FileSizeToSongLength() noexcept;
bool DecodeFirstFrame(Tag *tag); bool DecodeFirstFrame(Tag *tag) noexcept;
void AllocateBuffers() { void AllocateBuffers() noexcept {
assert(max_frames > 0); assert(max_frames > 0);
assert(frame_offsets == nullptr); assert(frame_offsets == nullptr);
assert(times == nullptr); assert(times == nullptr);
@@ -179,27 +182,39 @@ struct MadDecoder {
} }
gcc_pure gcc_pure
long TimeToFrame(SongTime t) const noexcept; size_t TimeToFrame(SongTime t) const noexcept;
void UpdateTimerNextFrame(); /**
* Record the current frame's offset in the "frame_offsets"
* buffer and go forward to the next frame, updating the
* attributes "current_frame" and "timer".
*/
void UpdateTimerNextFrame() noexcept;
/** /**
* Sends the synthesized current frame via * Sends the synthesized current frame via
* DecoderClient::SubmitData(). * DecoderClient::SubmitData().
*/ */
DecoderCommand SendPCM(unsigned i, unsigned pcm_length); DecoderCommand SubmitPCM(size_t start, size_t n) noexcept;
/** /**
* Synthesize the current frame and send it via * Synthesize the current frame and send it via
* DecoderClient::SubmitData(). * DecoderClient::SubmitData().
*/ */
DecoderCommand SyncAndSend(); DecoderCommand SynthAndSubmit() noexcept;
bool Read(); /**
* @return false to stop decoding
*/
bool HandleCurrentFrame() noexcept;
bool LoadNextFrame() noexcept;
bool Read() noexcept;
}; };
MadDecoder::MadDecoder(DecoderClient *_client, MadDecoder::MadDecoder(DecoderClient *_client,
InputStream &_input_stream) InputStream &_input_stream) noexcept
:client(_client), input_stream(_input_stream) :client(_client), input_stream(_input_stream)
{ {
mad_stream_init(&stream); mad_stream_init(&stream);
@@ -210,7 +225,7 @@ MadDecoder::MadDecoder(DecoderClient *_client,
} }
inline bool inline bool
MadDecoder::Seek(long offset) MadDecoder::Seek(long offset) noexcept
{ {
try { try {
input_stream.LockSeek(offset); input_stream.LockSeek(offset);
@@ -225,32 +240,38 @@ MadDecoder::Seek(long offset)
} }
inline bool inline bool
MadDecoder::FillBuffer() MadDecoder::FillBuffer() noexcept
{ {
size_t remaining, length; /* amount of rest data still residing in the buffer */
unsigned char *dest; size_t rest_size = 0;
size_t max_read_size = sizeof(input_buffer);
unsigned char *dest = input_buffer;
if (stream.next_frame != nullptr) { if (stream.next_frame != nullptr) {
remaining = stream.bufend - stream.next_frame; rest_size = stream.bufend - stream.next_frame;
memmove(input_buffer, stream.next_frame, remaining); memmove(input_buffer, stream.next_frame, rest_size);
dest = input_buffer + remaining; dest += rest_size;
length = READ_BUFFER_SIZE - remaining; max_read_size -= rest_size;
} else {
remaining = 0;
length = READ_BUFFER_SIZE;
dest = input_buffer;
} }
/* we've exhausted the read buffer, so give up!, these potential /* we've exhausted the read buffer, so give up!, these potential
* mp3 frames are way too big, and thus unlikely to be mp3 frames */ * mp3 frames are way too big, and thus unlikely to be mp3 frames */
if (length == 0) if (max_read_size == 0)
return false; return false;
length = decoder_read(client, input_stream, dest, length); size_t nbytes = decoder_read(client, input_stream,
if (length == 0) dest, max_read_size);
return false; if (nbytes == 0) {
if (was_eof || max_read_size < MAD_BUFFER_GUARD)
return false;
mad_stream_buffer(&stream, input_buffer, length + remaining); was_eof = true;
nbytes = MAD_BUFFER_GUARD;
memset(dest, 0, nbytes);
}
mad_stream_buffer(&stream, input_buffer, rest_size + nbytes);
stream.error = MAD_ERROR_NONE; stream.error = MAD_ERROR_NONE;
return true; return true;
@@ -286,7 +307,7 @@ parse_id3_mixramp(struct id3_tag *tag) noexcept
#endif #endif
inline void inline void
MadDecoder::ParseId3(size_t tagsize, Tag *mpd_tag) MadDecoder::ParseId3(size_t tagsize, Tag *mpd_tag) noexcept
{ {
#ifdef ENABLE_ID3TAG #ifdef ENABLE_ID3TAG
std::unique_ptr<id3_byte_t[]> allocated; std::unique_ptr<id3_byte_t[]> allocated;
@@ -354,7 +375,7 @@ MadDecoder::ParseId3(size_t tagsize, Tag *mpd_tag)
* of the ID3 frame. * of the ID3 frame.
*/ */
static signed long static signed long
id3_tag_query(const void *p0, size_t length) id3_tag_query(const void *p0, size_t length) noexcept
{ {
const char *p = (const char *)p0; const char *p = (const char *)p0;
@@ -364,26 +385,26 @@ id3_tag_query(const void *p0, size_t length)
} }
#endif /* !ENABLE_ID3TAG */ #endif /* !ENABLE_ID3TAG */
static enum mp3_action static MadDecoderAction
RecoverFrameError(struct mad_stream &stream) RecoverFrameError(const struct mad_stream &stream) noexcept
{ {
if (MAD_RECOVERABLE(stream.error)) if (MAD_RECOVERABLE(stream.error))
return DECODE_SKIP; return MadDecoderAction::SKIP;
else if (stream.error == MAD_ERROR_BUFLEN) else if (stream.error == MAD_ERROR_BUFLEN)
return DECODE_CONT; return MadDecoderAction::CONT;
FormatWarning(mad_domain, FormatWarning(mad_domain,
"unrecoverable frame level error: %s", "unrecoverable frame level error: %s",
mad_stream_errorstr(&stream)); mad_stream_errorstr(&stream));
return DECODE_BREAK; return MadDecoderAction::BREAK;
} }
enum mp3_action MadDecoderAction
MadDecoder::DecodeNextFrameHeader(Tag *tag) MadDecoder::DecodeNextFrameHeader(Tag *tag) noexcept
{ {
if ((stream.buffer == nullptr || stream.error == MAD_ERROR_BUFLEN) && if ((stream.buffer == nullptr || stream.error == MAD_ERROR_BUFLEN) &&
!FillBuffer()) !FillBuffer())
return DECODE_BREAK; return MadDecoderAction::BREAK;
if (mad_header_decode(&frame.header, &stream)) { if (mad_header_decode(&frame.header, &stream)) {
if (stream.error == MAD_ERROR_LOSTSYNC && stream.this_frame) { if (stream.error == MAD_ERROR_LOSTSYNC && stream.this_frame) {
@@ -393,7 +414,7 @@ MadDecoder::DecodeNextFrameHeader(Tag *tag)
if (tagsize > 0) { if (tagsize > 0) {
ParseId3((size_t)tagsize, tag); ParseId3((size_t)tagsize, tag);
return DECODE_CONT; return MadDecoderAction::CONT;
} }
} }
@@ -404,24 +425,24 @@ MadDecoder::DecodeNextFrameHeader(Tag *tag)
if (layer == (mad_layer)0) { if (layer == (mad_layer)0) {
if (new_layer != MAD_LAYER_II && new_layer != MAD_LAYER_III) { if (new_layer != MAD_LAYER_II && new_layer != MAD_LAYER_III) {
/* Only layer 2 and 3 have been tested to work */ /* Only layer 2 and 3 have been tested to work */
return DECODE_SKIP; return MadDecoderAction::SKIP;
} }
layer = new_layer; layer = new_layer;
} else if (new_layer != layer) { } else if (new_layer != layer) {
/* Don't decode frames with a different layer than the first */ /* Don't decode frames with a different layer than the first */
return DECODE_SKIP; return MadDecoderAction::SKIP;
} }
return DECODE_OK; return MadDecoderAction::OK;
} }
enum mp3_action MadDecoderAction
MadDecoder::DecodeNextFrame() MadDecoder::DecodeNextFrame() noexcept
{ {
if ((stream.buffer == nullptr || stream.error == MAD_ERROR_BUFLEN) && if ((stream.buffer == nullptr || stream.error == MAD_ERROR_BUFLEN) &&
!FillBuffer()) !FillBuffer())
return DECODE_BREAK; return MadDecoderAction::BREAK;
if (mad_frame_decode(&frame, &stream)) { if (mad_frame_decode(&frame, &stream)) {
if (stream.error == MAD_ERROR_LOSTSYNC) { if (stream.error == MAD_ERROR_LOSTSYNC) {
@@ -430,14 +451,14 @@ MadDecoder::DecodeNextFrame()
stream.this_frame); stream.this_frame);
if (tagsize > 0) { if (tagsize > 0) {
mad_stream_skip(&stream, tagsize); mad_stream_skip(&stream, tagsize);
return DECODE_CONT; return MadDecoderAction::CONT;
} }
} }
return RecoverFrameError(stream); return RecoverFrameError(stream);
} }
return DECODE_OK; return MadDecoderAction::OK;
} }
/* xing stuff stolen from alsaplayer, and heavily modified by jat */ /* xing stuff stolen from alsaplayer, and heavily modified by jat */
@@ -476,7 +497,7 @@ struct lame {
}; };
static bool static bool
parse_xing(struct xing *xing, struct mad_bitptr *ptr, int *oldbitlen) parse_xing(struct xing *xing, struct mad_bitptr *ptr, int *oldbitlen) noexcept
{ {
int bitlen = *oldbitlen; int bitlen = *oldbitlen;
@@ -556,7 +577,7 @@ parse_xing(struct xing *xing, struct mad_bitptr *ptr, int *oldbitlen)
} }
static bool static bool
parse_lame(struct lame *lame, struct mad_bitptr *ptr, int *bitlen) parse_lame(struct lame *lame, struct mad_bitptr *ptr, int *bitlen) noexcept
{ {
/* Unlike the xing header, the lame tag has a fixed length. Fail if /* Unlike the xing header, the lame tag has a fixed length. Fail if
* not all 36 bytes (288 bits) are there. */ * not all 36 bytes (288 bits) are there. */
@@ -647,7 +668,7 @@ parse_lame(struct lame *lame, struct mad_bitptr *ptr, int *bitlen)
} }
static inline SongTime static inline SongTime
mp3_frame_duration(const struct mad_frame *frame) mad_frame_duration(const struct mad_frame *frame) noexcept
{ {
return ToSongTime(frame->header.duration); return ToSongTime(frame->header.duration);
} }
@@ -672,12 +693,12 @@ MadDecoder::RestIncludingThisFrame() const noexcept
} }
inline void inline void
MadDecoder::FileSizeToSongLength() MadDecoder::FileSizeToSongLength() noexcept
{ {
if (input_stream.KnownSize()) { if (input_stream.KnownSize()) {
offset_type rest = RestIncludingThisFrame(); offset_type rest = RestIncludingThisFrame();
const SongTime frame_duration = mp3_frame_duration(&frame); const SongTime frame_duration = mad_frame_duration(&frame);
const SongTime duration = const SongTime duration =
SongTime::FromScale<uint64_t>(rest, SongTime::FromScale<uint64_t>(rest,
frame.header.bitrate / 8); frame.header.bitrate / 8);
@@ -694,25 +715,25 @@ MadDecoder::FileSizeToSongLength()
} }
inline bool inline bool
MadDecoder::DecodeFirstFrame(Tag *tag) MadDecoder::DecodeFirstFrame(Tag *tag) noexcept
{ {
struct xing xing; struct xing xing;
while (true) { while (true) {
enum mp3_action ret; MadDecoderAction ret;
do { do {
ret = DecodeNextFrameHeader(tag); ret = DecodeNextFrameHeader(tag);
} while (ret == DECODE_CONT); } while (ret == MadDecoderAction::CONT);
if (ret == DECODE_BREAK) if (ret == MadDecoderAction::BREAK)
return false; return false;
if (ret == DECODE_SKIP) continue; if (ret == MadDecoderAction::SKIP) continue;
do { do {
ret = DecodeNextFrame(); ret = DecodeNextFrame();
} while (ret == DECODE_CONT); } while (ret == MadDecoderAction::CONT);
if (ret == DECODE_BREAK) if (ret == MadDecoderAction::BREAK)
return false; return false;
if (ret == DECODE_OK) break; if (ret == MadDecoderAction::OK) break;
} }
struct mad_bitptr ptr = stream.anc_ptr; struct mad_bitptr ptr = stream.anc_ptr;
@@ -724,7 +745,7 @@ MadDecoder::DecodeFirstFrame(Tag *tag)
* if an xing tag exists, use that! * if an xing tag exists, use that!
*/ */
if (parse_xing(&xing, &ptr, &bitlen)) { if (parse_xing(&xing, &ptr, &bitlen)) {
mute_frame = MUTEFRAME_SKIP; mute_frame = MadDecoderMuteFrame::SKIP;
if ((xing.flags & XING_FRAMES) && xing.frames) { if ((xing.flags & XING_FRAMES) && xing.frames) {
mad_timer_t duration = frame.header.duration; mad_timer_t duration = frame.header.duration;
@@ -736,9 +757,17 @@ MadDecoder::DecodeFirstFrame(Tag *tag)
struct lame lame; struct lame lame;
if (parse_lame(&lame, &ptr, &bitlen)) { if (parse_lame(&lame, &ptr, &bitlen)) {
if (gapless_playback && input_stream.IsSeekable()) { if (gapless_playback && input_stream.IsSeekable()) {
/* libmad inserts 529 samples of
silence at the beginning and
removes those 529 samples at the
end */
drop_start_samples = lame.encoder_delay + drop_start_samples = lame.encoder_delay +
DECODERDELAY; DECODERDELAY;
drop_end_samples = lame.encoder_padding; drop_end_samples = lame.encoder_padding;
if (drop_end_samples > DECODERDELAY)
drop_end_samples -= DECODERDELAY;
else
drop_end_samples = 0;
} }
/* Album gain isn't currently used. See comment in /* Album gain isn't currently used. See comment in
@@ -767,7 +796,7 @@ MadDecoder::DecodeFirstFrame(Tag *tag)
return true; return true;
} }
MadDecoder::~MadDecoder() MadDecoder::~MadDecoder() noexcept
{ {
mad_synth_finish(&synth); mad_synth_finish(&synth);
mad_frame_finish(&frame); mad_frame_finish(&frame);
@@ -777,10 +806,10 @@ MadDecoder::~MadDecoder()
delete[] times; delete[] times;
} }
long size_t
MadDecoder::TimeToFrame(SongTime t) const noexcept MadDecoder::TimeToFrame(SongTime t) const noexcept
{ {
unsigned long i; size_t i;
for (i = 0; i < highest_frame; ++i) { for (i = 0; i < highest_frame; ++i) {
auto frame_time = ToSongTime(times[i]); auto frame_time = ToSongTime(times[i]);
@@ -792,12 +821,11 @@ MadDecoder::TimeToFrame(SongTime t) const noexcept
} }
void void
MadDecoder::UpdateTimerNextFrame() MadDecoder::UpdateTimerNextFrame() noexcept
{ {
if (current_frame >= highest_frame) { if (current_frame >= highest_frame) {
/* record this frame's properties in frame_offsets /* record this frame's properties in frame_offsets
(for seeking) and times */ (for seeking) and times */
bit_rate = frame.header.bitrate;
if (current_frame >= max_frames) if (current_frame >= max_frames)
/* cap current_frame */ /* cap current_frame */
@@ -818,36 +846,22 @@ MadDecoder::UpdateTimerNextFrame()
} }
DecoderCommand DecoderCommand
MadDecoder::SendPCM(unsigned i, unsigned pcm_length) MadDecoder::SubmitPCM(size_t i, size_t pcm_length) noexcept
{ {
unsigned max_samples = sizeof(output_buffer) / size_t num_samples = pcm_length - i;
sizeof(output_buffer[0]) /
MAD_NCHANNELS(&frame.header);
while (i < pcm_length) { mad_fixed_to_24_buffer(output_buffer, synth.pcm,
unsigned int num_samples = pcm_length - i; i, i + num_samples,
if (num_samples > max_samples) MAD_NCHANNELS(&frame.header));
num_samples = max_samples; num_samples *= MAD_NCHANNELS(&frame.header);
i += num_samples; return client->SubmitData(input_stream, output_buffer,
sizeof(output_buffer[0]) * num_samples,
mad_fixed_to_24_buffer(output_buffer, &synth, frame.header.bitrate / 1000);
i - num_samples, i,
MAD_NCHANNELS(&frame.header));
num_samples *= MAD_NCHANNELS(&frame.header);
auto cmd = client->SubmitData(input_stream, output_buffer,
sizeof(output_buffer[0]) * num_samples,
bit_rate / 1000);
if (cmd != DecoderCommand::NONE)
return cmd;
}
return DecoderCommand::NONE;
} }
inline DecoderCommand inline DecoderCommand
MadDecoder::SyncAndSend() MadDecoder::SynthAndSubmit() noexcept
{ {
mad_synth_frame(&synth, &frame); mad_synth_frame(&synth, &frame);
@@ -864,33 +878,33 @@ MadDecoder::SyncAndSend()
drop_start_frames--; drop_start_frames--;
return DecoderCommand::NONE; return DecoderCommand::NONE;
} else if ((drop_end_frames > 0) && } else if ((drop_end_frames > 0) &&
(current_frame == (max_frames + 1 - drop_end_frames))) { current_frame == max_frames - drop_end_frames) {
/* stop decoding, effectively dropping all remaining /* stop decoding, effectively dropping all remaining
frames */ frames */
return DecoderCommand::STOP; return DecoderCommand::STOP;
} }
unsigned i = 0; size_t i = 0;
if (!decoded_first_frame) { if (!decoded_first_frame) {
i = drop_start_samples; i = drop_start_samples;
decoded_first_frame = true; decoded_first_frame = true;
} }
unsigned pcm_length = synth.pcm.length; size_t pcm_length = synth.pcm.length;
if (drop_end_samples && if (drop_end_samples &&
(current_frame == max_frames - drop_end_frames)) { current_frame == max_frames - drop_end_frames - 1) {
if (drop_end_samples >= pcm_length) if (drop_end_samples >= pcm_length)
pcm_length = 0; return DecoderCommand::STOP;
else
pcm_length -= drop_end_samples; pcm_length -= drop_end_samples;
} }
auto cmd = SendPCM(i, pcm_length); auto cmd = SubmitPCM(i, pcm_length);
if (cmd != DecoderCommand::NONE) if (cmd != DecoderCommand::NONE)
return cmd; return cmd;
if (drop_end_samples && if (drop_end_samples &&
(current_frame == max_frames - drop_end_frames)) current_frame == max_frames - drop_end_frames - 1)
/* stop decoding, effectively dropping /* stop decoding, effectively dropping
* all remaining samples */ * all remaining samples */
return DecoderCommand::STOP; return DecoderCommand::STOP;
@@ -899,44 +913,51 @@ MadDecoder::SyncAndSend()
} }
inline bool inline bool
MadDecoder::Read() MadDecoder::HandleCurrentFrame() noexcept
{ {
UpdateTimerNextFrame();
switch (mute_frame) { switch (mute_frame) {
DecoderCommand cmd; DecoderCommand cmd;
case MUTEFRAME_SKIP: case MadDecoderMuteFrame::SKIP:
mute_frame = MUTEFRAME_NONE; mute_frame = MadDecoderMuteFrame::NONE;
break; break;
case MUTEFRAME_SEEK: case MadDecoderMuteFrame::SEEK:
if (elapsed_time >= seek_time) if (elapsed_time >= seek_time)
mute_frame = MUTEFRAME_NONE; mute_frame = MadDecoderMuteFrame::NONE;
UpdateTimerNextFrame();
break; break;
case MUTEFRAME_NONE: case MadDecoderMuteFrame::NONE:
cmd = SyncAndSend(); cmd = SynthAndSubmit();
UpdateTimerNextFrame();
if (cmd == DecoderCommand::SEEK) { if (cmd == DecoderCommand::SEEK) {
assert(input_stream.IsSeekable()); assert(input_stream.IsSeekable());
const auto t = client->GetSeekTime(); const auto t = client->GetSeekTime();
unsigned long j = TimeToFrame(t); size_t j = TimeToFrame(t);
if (j < highest_frame) { if (j < highest_frame) {
if (Seek(frame_offsets[j])) { if (Seek(frame_offsets[j])) {
current_frame = j; current_frame = j;
was_eof = false;
client->CommandFinished(); client->CommandFinished();
} else } else
client->SeekError(); client->SeekError();
} else { } else {
seek_time = t; seek_time = t;
mute_frame = MUTEFRAME_SEEK; mute_frame = MadDecoderMuteFrame::SEEK;
client->CommandFinished(); client->CommandFinished();
} }
} else if (cmd != DecoderCommand::NONE) } else if (cmd != DecoderCommand::NONE)
return false; return false;
} }
return true;
}
inline bool
MadDecoder::LoadNextFrame() noexcept
{
while (true) { while (true) {
enum mp3_action ret; MadDecoderAction ret;
do { do {
Tag tag; Tag tag;
@@ -945,84 +966,104 @@ MadDecoder::Read()
if (!tag.IsEmpty()) if (!tag.IsEmpty())
client->SubmitTag(input_stream, client->SubmitTag(input_stream,
std::move(tag)); std::move(tag));
} while (ret == DECODE_CONT); } while (ret == MadDecoderAction::CONT);
if (ret == DECODE_BREAK) if (ret == MadDecoderAction::BREAK)
return false; return false;
const bool skip = ret == DECODE_SKIP; const bool skip = ret == MadDecoderAction::SKIP;
if (mute_frame == MUTEFRAME_NONE) { if (mute_frame == MadDecoderMuteFrame::NONE) {
do { do {
ret = DecodeNextFrame(); ret = DecodeNextFrame();
} while (ret == DECODE_CONT); } while (ret == MadDecoderAction::CONT);
if (ret == DECODE_BREAK) if (ret == MadDecoderAction::BREAK)
return false; return false;
} }
if (!skip && ret == DECODE_OK) if (!skip && ret == MadDecoderAction::OK)
return true; return true;
} }
} }
static void inline bool
mp3_decode(DecoderClient &client, InputStream &input_stream) MadDecoder::Read() noexcept
{ {
MadDecoder data(&client, input_stream); return HandleCurrentFrame() &&
LoadNextFrame();
}
inline void
MadDecoder::RunDecoder() noexcept
{
assert(client != nullptr);
Tag tag; Tag tag;
if (!data.DecodeFirstFrame(&tag)) { if (!DecodeFirstFrame(&tag)) {
if (client.GetCommand() == DecoderCommand::NONE) if (client->GetCommand() == DecoderCommand::NONE)
LogError(mad_domain, LogError(mad_domain,
"input/Input does not appear to be a mp3 bit stream"); "input does not appear to be a mp3 bit stream");
return; return;
} }
data.AllocateBuffers(); AllocateBuffers();
client.Ready(CheckAudioFormat(data.frame.header.samplerate, client->Ready(CheckAudioFormat(frame.header.samplerate,
SampleFormat::S24_P32, SampleFormat::S24_P32,
MAD_NCHANNELS(&data.frame.header)), MAD_NCHANNELS(&frame.header)),
input_stream.IsSeekable(), input_stream.IsSeekable(),
data.total_time); total_time);
if (!tag.IsEmpty()) if (!tag.IsEmpty())
client.SubmitTag(input_stream, std::move(tag)); client->SubmitTag(input_stream, std::move(tag));
while (data.Read()) {} while (Read()) {}
} }
static bool static void
mad_decoder_scan_stream(InputStream &is, TagHandler &handler) noexcept mad_decode(DecoderClient &client, InputStream &input_stream)
{ {
MadDecoder data(nullptr, is); MadDecoder data(&client, input_stream);
if (!data.DecodeFirstFrame(nullptr)) data.RunDecoder();
}
inline bool
MadDecoder::RunScan(TagHandler &handler) noexcept
{
if (!DecodeFirstFrame(nullptr))
return false; return false;
if (!data.total_time.IsNegative()) if (!total_time.IsNegative())
handler.OnDuration(SongTime(data.total_time)); handler.OnDuration(SongTime(total_time));
try { try {
handler.OnAudioFormat(CheckAudioFormat(data.frame.header.samplerate, handler.OnAudioFormat(CheckAudioFormat(frame.header.samplerate,
SampleFormat::S24_P32, SampleFormat::S24_P32,
MAD_NCHANNELS(&data.frame.header))); MAD_NCHANNELS(&frame.header)));
} catch (...) { } catch (...) {
} }
return true; return true;
} }
static const char *const mp3_suffixes[] = { "mp3", "mp2", nullptr }; static bool
static const char *const mp3_mime_types[] = { "audio/mpeg", nullptr }; mad_decoder_scan_stream(InputStream &is, TagHandler &handler) noexcept
{
MadDecoder data(nullptr, is);
return data.RunScan(handler);
}
static const char *const mad_suffixes[] = { "mp3", "mp2", nullptr };
static const char *const mad_mime_types[] = { "audio/mpeg", nullptr };
const struct DecoderPlugin mad_decoder_plugin = { const struct DecoderPlugin mad_decoder_plugin = {
"mad", "mad",
mp3_plugin_init, mad_plugin_init,
nullptr, nullptr,
mp3_decode, mad_decode,
nullptr, nullptr,
nullptr, nullptr,
mad_decoder_scan_stream, mad_decoder_scan_stream,
nullptr, nullptr,
mp3_suffixes, mad_suffixes,
mp3_mime_types, mad_mime_types,
}; };

View File

@@ -137,6 +137,28 @@ mpc_to_mpd_buffer(MpcdecSampleTraits::pointer_type dest,
*dest++ = mpc_to_mpd_sample(*src++); *dest++ = mpc_to_mpd_sample(*src++);
} }
static constexpr ReplayGainTuple
ImportMpcdecReplayGain(mpc_uint16_t gain, mpc_uint16_t peak) noexcept
{
auto t = ReplayGainTuple::Undefined();
if (gain != 0 && peak != 0) {
t.gain = MPC_OLD_GAIN_REF - (gain / 256.);
t.peak = pow(10, peak / 256. / 20) / 32767;
}
return t;
}
static constexpr ReplayGainInfo
ImportMpcdecReplayGain(const mpc_streaminfo &info) noexcept
{
auto rgi = ReplayGainInfo::Undefined();
rgi.album = ImportMpcdecReplayGain(info.gain_album, info.peak_album);
rgi.track = ImportMpcdecReplayGain(info.gain_title, info.peak_title);
return rgi;
}
static void static void
mpcdec_decode(DecoderClient &client, InputStream &is) mpcdec_decode(DecoderClient &client, InputStream &is)
{ {
@@ -167,14 +189,11 @@ mpcdec_decode(DecoderClient &client, InputStream &is)
mpcdec_sample_format, mpcdec_sample_format,
info.channels); info.channels);
ReplayGainInfo rgi; {
rgi.Clear(); const auto rgi = ImportMpcdecReplayGain(info);
rgi.album.gain = MPC_OLD_GAIN_REF - (info.gain_album / 256.); if (rgi.IsDefined())
rgi.album.peak = pow(10, info.peak_album / 256. / 20) / 32767; client.SubmitReplayGain(&rgi);
rgi.track.gain = MPC_OLD_GAIN_REF - (info.gain_title / 256.); }
rgi.track.peak = pow(10, info.peak_title / 256. / 20) / 32767;
client.SubmitReplayGain(&rgi);
client.Ready(audio_format, is.IsSeekable(), client.Ready(audio_format, is.IsSeekable(),
SongTime::FromS(mpc_streaminfo_get_length(&info))); SongTime::FromS(mpc_streaminfo_get_length(&info)));

View File

@@ -22,12 +22,12 @@
#include "lib/xiph/XiphTags.hxx" #include "lib/xiph/XiphTags.hxx"
#include "tag/Handler.hxx" #include "tag/Handler.hxx"
#include "tag/ParseName.hxx" #include "tag/ParseName.hxx"
#include "util/ASCII.hxx"
#include "ReplayGainInfo.hxx" #include "ReplayGainInfo.hxx"
#include <string> #include <string>
#include <stdint.h> #include <stdint.h>
#include <string.h>
#include <stdlib.h> #include <stdlib.h>
gcc_pure gcc_pure
@@ -46,7 +46,7 @@ ScanOneOpusTag(const char *name, const char *value,
ReplayGainInfo *rgi, ReplayGainInfo *rgi,
TagHandler &handler) noexcept TagHandler &handler) noexcept
{ {
if (rgi != nullptr && strcmp(name, "R128_TRACK_GAIN") == 0) { if (rgi != nullptr && StringEqualsCaseASCII(name, "R128_TRACK_GAIN")) {
/* R128_TRACK_GAIN is a Q7.8 fixed point number in /* R128_TRACK_GAIN is a Q7.8 fixed point number in
dB */ dB */
@@ -54,7 +54,8 @@ ScanOneOpusTag(const char *name, const char *value,
long l = strtol(value, &endptr, 10); long l = strtol(value, &endptr, 10);
if (endptr > value && *endptr == 0) if (endptr > value && *endptr == 0)
rgi->track.gain = double(l) / 256.; rgi->track.gain = double(l) / 256.;
} else if (rgi != nullptr && strcmp(name, "R128_ALBUM_GAIN") == 0) { } else if (rgi != nullptr &&
StringEqualsCaseASCII(name, "R128_ALBUM_GAIN")) {
/* R128_ALBUM_GAIN is a Q7.8 fixed point number in /* R128_ALBUM_GAIN is a Q7.8 fixed point number in
dB */ dB */

View File

@@ -25,6 +25,7 @@
#include "song/DetachedSong.hxx" #include "song/DetachedSong.hxx"
#include "fs/Path.hxx" #include "fs/Path.hxx"
#include "fs/AllocatedPath.hxx" #include "fs/AllocatedPath.hxx"
#include "lib/icu/Converter.hxx"
#ifdef HAVE_SIDPLAYFP #ifdef HAVE_SIDPLAYFP
#include "fs/io/FileReader.hxx" #include "fs/io/FileReader.hxx"
#include "util/RuntimeError.hxx" #include "util/RuntimeError.hxx"
@@ -32,6 +33,8 @@
#include "util/Macros.hxx" #include "util/Macros.hxx"
#include "util/StringFormat.hxx" #include "util/StringFormat.hxx"
#include "util/Domain.hxx" #include "util/Domain.hxx"
#include "util/AllocatedString.hxx"
#include "util/CharUtil.hxx"
#include "system/ByteOrder.hxx" #include "system/ByteOrder.hxx"
#include "Log.hxx" #include "Log.hxx"
@@ -432,19 +435,70 @@ sidplay_file_decode(DecoderClient &client, Path path_fs)
} while (cmd != DecoderCommand::STOP); } while (cmd != DecoderCommand::STOP);
} }
static AllocatedString<char>
Windows1252ToUTF8(const char *s) noexcept
{
#ifdef HAVE_ICU_CONVERTER
try {
std::unique_ptr<IcuConverter>
converter(IcuConverter::Create("windows-1252"));
return converter->ToUTF8(s);
} catch (...) { }
#endif
/*
* Fallback to not transcoding windows-1252 to utf-8, that may result
* in invalid utf-8 unless nonprintable characters are replaced.
*/
auto t = AllocatedString<char>::Duplicate(s);
for (size_t i = 0; t[i] != AllocatedString<char>::SENTINEL; i++)
if (!IsPrintableASCII(t[i]))
t[i] = '?';
return t;
}
gcc_pure gcc_pure
static const char * static AllocatedString<char>
GetInfoString(const SidTuneInfo &info, unsigned i) noexcept GetInfoString(const SidTuneInfo &info, unsigned i) noexcept
{ {
#ifdef HAVE_SIDPLAYFP #ifdef HAVE_SIDPLAYFP
return info.numberOfInfoStrings() > i const char *s = info.numberOfInfoStrings() > i
? info.infoString(i) ? info.infoString(i)
: nullptr; : "";
#else #else
return info.numberOfInfoStrings > i const char *s = info.numberOfInfoStrings > i
? info.infoString[i] ? info.infoString[i]
: nullptr; : "";
#endif #endif
return Windows1252ToUTF8(s);
}
gcc_pure
static AllocatedString<char>
GetDateString(const SidTuneInfo &info) noexcept
{
/*
* Field 2 is called <released>, previously used as <copyright>.
* It is formatted <year><space><company or author or group>,
* where <year> may be <YYYY>, <YYY?>, <YY??> or <YYYY-YY>, for
* example "1987", "199?", "19??" or "1985-87". The <company or
* author or group> may be for example Rob Hubbard. A full field
* may be for example "1987 Rob Hubbard".
*/
AllocatedString<char> release = GetInfoString(info, 2);
/* Keep the <year> part only for the date. */
for (size_t i = 0; release[i] != AllocatedString<char>::SENTINEL; i++)
if (std::isspace(release[i])) {
release[i] = AllocatedString<char>::SENTINEL;
break;
}
return release;
} }
static void static void
@@ -452,27 +506,25 @@ ScanSidTuneInfo(const SidTuneInfo &info, unsigned track, unsigned n_tracks,
TagHandler &handler) noexcept TagHandler &handler) noexcept
{ {
/* title */ /* title */
const char *title = GetInfoString(info, 0); const auto title = GetInfoString(info, 0);
if (title == nullptr)
title = "";
if (n_tracks > 1) { if (n_tracks > 1) {
const auto tag_title = const auto tag_title =
StringFormat<1024>("%s (%u/%u)", StringFormat<1024>("%s (%u/%u)",
title, track, n_tracks); title.c_str(), track, n_tracks);
handler.OnTag(TAG_TITLE, tag_title); handler.OnTag(TAG_TITLE, tag_title.c_str());
} else } else
handler.OnTag(TAG_TITLE, title); handler.OnTag(TAG_TITLE, title.c_str());
/* artist */ /* artist */
const char *artist = GetInfoString(info, 1); const auto artist = GetInfoString(info, 1);
if (artist != nullptr) if (!artist.empty())
handler.OnTag(TAG_ARTIST, artist); handler.OnTag(TAG_ARTIST, artist.c_str());
/* date */ /* date */
const char *date = GetInfoString(info, 2); const auto date = GetDateString(info);
if (date != nullptr) if (!date.empty())
handler.OnTag(TAG_DATE, date); handler.OnTag(TAG_DATE, date.c_str());
/* track */ /* track */
handler.OnTag(TAG_TRACK, StringFormat<16>("%u", track)); handler.OnTag(TAG_TRACK, StringFormat<16>("%u", track));
@@ -547,6 +599,10 @@ sidplay_container_scan(Path path_fs)
AddTagHandler h(tag_builder); AddTagHandler h(tag_builder);
ScanSidTuneInfo(info, i, n_tracks, h); ScanSidTuneInfo(info, i, n_tracks, h);
const SignedSongTime duration = get_song_length(tune);
if (!duration.IsNegative())
h.OnDuration(SongTime(duration));
char track_name[32]; char track_name[32];
/* Construct container/tune path names, eg. /* Construct container/tune path names, eg.
Delta.sid/tune_001.sid */ Delta.sid/tune_001.sid */

View File

@@ -298,11 +298,7 @@ CdioParanoiaInputStream::Read(void *ptr, size_t length)
if (s_err) { if (s_err) {
FormatError(cdio_domain, FormatError(cdio_domain,
"paranoia_read: %s", s_err); "paranoia_read: %s", s_err);
#if LIBCDIO_VERSION_NUM >= 90
cdio_cddap_free_messages(s_err); cdio_cddap_free_messages(s_err);
#else
free(s_err);
#endif
} }
throw; throw;

View File

@@ -6,7 +6,7 @@ if alsa_dep.found()
input_plugins_sources += 'AlsaInputPlugin.cxx' input_plugins_sources += 'AlsaInputPlugin.cxx'
endif endif
libcdio_paranoia_dep = dependency('libcdio_paranoia', version: '>= 0.4', required: get_option('cdio_paranoia')) libcdio_paranoia_dep = dependency('libcdio_paranoia', version: '>= 10.2+0.93+1', required: get_option('cdio_paranoia'))
conf.set('ENABLE_CDIO_PARANOIA', libcdio_paranoia_dep.found()) conf.set('ENABLE_CDIO_PARANOIA', libcdio_paranoia_dep.found())
if libcdio_paranoia_dep.found() if libcdio_paranoia_dep.found()
input_plugins_sources += 'CdioParanoiaInputPlugin.cxx' input_plugins_sources += 'CdioParanoiaInputPlugin.cxx'

View File

@@ -34,11 +34,7 @@
#include "util/Compiler.h" #include "util/Compiler.h"
#include <cdio/version.h> #include <cdio/version.h>
#if LIBCDIO_VERSION_NUM >= 90
#include <cdio/paranoia/paranoia.h> #include <cdio/paranoia/paranoia.h>
#else
#include <cdio/paranoia.h>
#endif
#include <stdexcept> #include <stdexcept>
#include <utility> #include <utility>

View File

@@ -69,12 +69,12 @@ OggVisitor::HandlePacket(const ogg_packet &packet)
/* fail if BOS is missing */ /* fail if BOS is missing */
throw std::runtime_error("BOS packet expected"); throw std::runtime_error("BOS packet expected");
OnOggPacket(packet);
if (packet.e_o_s) { if (packet.e_o_s) {
EndStream(); EndStream();
return; return;
} }
OnOggPacket(packet);
} }
inline void inline void

View File

@@ -69,8 +69,21 @@ private:
void HandlePackets(); void HandlePackets();
protected: protected:
/**
* Called when the "beginning of stream" packet has been seen.
*
* @param packet the "beginning of stream" packet
*/
virtual void OnOggBeginning(const ogg_packet &packet) = 0; virtual void OnOggBeginning(const ogg_packet &packet) = 0;
/**
* Called for each follow-up packet.
*/
virtual void OnOggPacket(const ogg_packet &packet) = 0; virtual void OnOggPacket(const ogg_packet &packet) = 0;
/**
* Called after the "end of stream" packet has been processed.
*/
virtual void OnOggEnd() = 0; virtual void OnOggEnd() = 0;
}; };

View File

@@ -159,6 +159,7 @@ AudioOutputControl::InternalOpen(const AudioFormat in_audio_format,
} catch (...) { } catch (...) {
LogError(std::current_exception()); LogError(std::current_exception());
Failure(std::current_exception()); Failure(std::current_exception());
return;
} }
if (f != in_audio_format || f != output->out_audio_format) if (f != in_audio_format || f != output->out_audio_format)
@@ -458,7 +459,7 @@ AudioOutputControl::Task() noexcept
case Command::RELEASE: case Command::RELEASE:
if (!open) { if (!open) {
/* the output has failed after /* the output has failed after
the PAUSE command was submitted; bail the RELEASE command was submitted; bail
out */ out */
CommandFinished(); CommandFinished();
break; break;

View File

@@ -28,6 +28,8 @@
#include "util/Domain.hxx" #include "util/Domain.hxx"
#include "Log.hxx" #include "Log.hxx"
#include <atomic>
#include <assert.h> #include <assert.h>
#include <jack/jack.h> #include <jack/jack.h>
@@ -69,13 +71,13 @@ struct JackOutput final : AudioOutput {
jack_client_t *client; jack_client_t *client;
jack_ringbuffer_t *ringbuffer[MAX_PORTS]; jack_ringbuffer_t *ringbuffer[MAX_PORTS];
bool shutdown; std::atomic_bool shutdown;
/** /**
* While this flag is set, the "process" callback generates * While this flag is set, the "process" callback generates
* silence. * silence.
*/ */
bool pause; std::atomic_bool pause;
explicit JackOutput(const ConfigBlock &block); explicit JackOutput(const ConfigBlock &block);
@@ -529,7 +531,10 @@ JackOutput::Start()
jports = nullptr; jports = nullptr;
} }
AtScopeExit(jports) { free(jports); }; AtScopeExit(jports) {
if (jports != nullptr)
jack_free(jports);
};
assert(num_dports > 0); assert(num_dports > 0);
@@ -540,7 +545,7 @@ JackOutput::Start()
std::fill(dports + num_dports, dports + audio_format.channels, std::fill(dports + num_dports, dports + audio_format.channels,
dports[0]); dports[0]);
} else if (num_dports > audio_format.channels) { } else if (num_dports > audio_format.channels) {
if (audio_format.channels == 1 && num_dports > 2) { if (audio_format.channels == 1 && num_dports >= 2) {
/* mono input file: connect the one source /* mono input file: connect the one source
channel to the both destination channels */ channel to the both destination channels */
duplicate_port = dports[1]; duplicate_port = dports[1];
@@ -602,7 +607,7 @@ JackOutput::WriteSamples(const float *src, size_t n_frames)
const unsigned n_channels = audio_format.channels; const unsigned n_channels = audio_format.channels;
float *dest[MAX_CHANNELS]; float *dest[MAX_CHANNELS];
size_t space = -1; size_t space = SIZE_MAX;
for (unsigned i = 0; i < n_channels; ++i) { for (unsigned i = 0; i < n_channels; ++i) {
jack_ringbuffer_data_t d[2]; jack_ringbuffer_data_t d[2];
jack_ringbuffer_get_write_vector(ringbuffer[i], d); jack_ringbuffer_get_write_vector(ringbuffer[i], d);

View File

@@ -22,7 +22,6 @@
#include "system/FileDescriptor.hxx" #include "system/FileDescriptor.hxx"
#include "system/Error.hxx" #include "system/Error.hxx"
#include <sys/stropts.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
@@ -31,11 +30,18 @@
#ifdef __sun #ifdef __sun
#include <sys/audio.h> #include <sys/audio.h>
#include <sys/stropts.h>
#else #else
/* some fake declarations that allow build this plugin on systems /* some fake declarations that allow build this plugin on systems
other than Solaris, just to see if it compiles */ other than Solaris, just to see if it compiles */
#include <sys/ioctl.h>
#ifndef I_FLUSH
#define I_FLUSH 0
#endif
#define AUDIO_GETINFO 0 #define AUDIO_GETINFO 0
#define AUDIO_SETINFO 0 #define AUDIO_SETINFO 0
#define AUDIO_ENCODING_LINEAR 0 #define AUDIO_ENCODING_LINEAR 0

View File

@@ -599,6 +599,19 @@ Player::SeekDecoder() noexcept
{ {
assert(pc.next_song != nullptr); assert(pc.next_song != nullptr);
if (pc.seek_time > SongTime::zero() && // TODO: allow this only if the song duration is known
dc.IsUnseekableCurrentSong(*pc.next_song)) {
/* seeking into the current song; but we already know
it's not seekable, so let's fail early */
/* note the seek_time>0 check: if seeking to the
beginning, we can simply restart the decoder */
pc.next_song.reset();
pc.SetError(PlayerError::DECODER,
std::make_exception_ptr(std::runtime_error("Not seekable")));
pc.CommandFinished();
return true;
}
CancelPendingSeek(); CancelPendingSeek();
{ {

View File

@@ -25,6 +25,9 @@
#include "util/UriUtil.hxx" #include "util/UriUtil.hxx"
#include "song/DetachedSong.hxx" #include "song/DetachedSong.hxx"
#include <algorithm>
#include <string>
#include <string.h> #include <string.h>
static void static void
@@ -66,6 +69,22 @@ playlist_check_translate_song(DetachedSong &song, const char *base_uri,
base_uri = nullptr; base_uri = nullptr;
const char *uri = song.GetURI(); const char *uri = song.GetURI();
#ifdef _WIN32
if (!PathTraitsUTF8::IsAbsolute(uri) && strchr(uri, '\\') != nullptr) {
/* Windows uses the backslash as path separator, but
the MPD protocol uses the (forward) slash by
definition; to allow backslashes in relative URIs
loaded from playlist files, this step converts all
backslashes to (forward) slashes */
std::string new_uri(uri);
std::replace(new_uri.begin(), new_uri.end(), '\\', '/');
song.SetURI(std::move(new_uri));
uri = song.GetURI();
}
#endif
if (base_uri != nullptr && !uri_has_scheme(uri) && if (base_uri != nullptr && !uri_has_scheme(uri) &&
!PathTraitsUTF8::IsAbsolute(uri)) !PathTraitsUTF8::IsAbsolute(uri))
song.SetURI(PathTraitsUTF8::Build(base_uri, uri)); song.SetURI(PathTraitsUTF8::Build(base_uri, uri));

View File

@@ -57,18 +57,6 @@
(GCC_VERSION > 0 && CLANG_VERSION == 0 && \ (GCC_VERSION > 0 && CLANG_VERSION == 0 && \
GCC_VERSION < GCC_MAKE_VERSION(major, minor, 0)) GCC_VERSION < GCC_MAKE_VERSION(major, minor, 0))
#ifdef __clang__
# if __clang_major__ < 3
# error Sorry, your clang version is too old. You need at least version 3.1.
# endif
#elif defined(__GNUC__)
# if GCC_OLDER_THAN(6,0)
# error Sorry, your gcc version is too old. You need at least version 6.0.
# endif
#else
# warning Untested compiler. Use at your own risk!
#endif
/** /**
* Are we building with the specified version of clang or newer? * Are we building with the specified version of clang or newer?
*/ */

View File

@@ -56,11 +56,11 @@ protected:
T data[size]; T data[size];
public: public:
constexpr size_type GetCapacity() const { constexpr size_type GetCapacity() const noexcept {
return size; return size;
} }
void Shift() { void Shift() noexcept {
if (head == 0) if (head == 0)
return; return;
@@ -74,15 +74,15 @@ public:
head = 0; head = 0;
} }
void Clear() { void Clear() noexcept {
head = tail = 0; head = tail = 0;
} }
bool empty() const { constexpr bool empty() const noexcept {
return head == tail; return head == tail;
} }
bool IsFull() const { constexpr bool IsFull() const noexcept {
return head == 0 && tail == size; return head == 0 && tail == size;
} }
@@ -90,7 +90,7 @@ public:
* Prepares writing. Returns a buffer range which may be written. * Prepares writing. Returns a buffer range which may be written.
* When you are finished, call Append(). * When you are finished, call Append().
*/ */
Range Write() { Range Write() noexcept {
if (empty()) if (empty())
Clear(); Clear();
else if (tail == size) else if (tail == size)
@@ -103,7 +103,7 @@ public:
* Expands the tail of the buffer, after data has been written to * Expands the tail of the buffer, after data has been written to
* the buffer returned by Write(). * the buffer returned by Write().
*/ */
void Append(size_type n) { void Append(size_type n) noexcept {
assert(tail <= size); assert(tail <= size);
assert(n <= size); assert(n <= size);
assert(tail + n <= size); assert(tail + n <= size);
@@ -111,18 +111,22 @@ public:
tail += n; tail += n;
} }
constexpr size_type GetAvailable() const noexcept {
return tail - head;
}
/** /**
* Return a buffer range which may be read. The buffer pointer is * Return a buffer range which may be read. The buffer pointer is
* writable, to allow modifications while parsing. * writable, to allow modifications while parsing.
*/ */
Range Read() { constexpr Range Read() noexcept {
return Range(data + head, tail - head); return Range(data + head, tail - head);
} }
/** /**
* Marks a chunk as consumed. * Marks a chunk as consumed.
*/ */
void Consume(size_type n) { void Consume(size_type n) noexcept {
assert(tail <= size); assert(tail <= size);
assert(head <= tail); assert(head <= tail);
assert(n <= tail); assert(n <= tail);

View File

@@ -23,7 +23,7 @@
#include "event/Thread.hxx" #include "event/Thread.hxx"
#include "decoder/DecoderList.hxx" #include "decoder/DecoderList.hxx"
#include "decoder/DecoderPlugin.hxx" #include "decoder/DecoderPlugin.hxx"
#include "decoder/Client.hxx" #include "decoder/DecoderAPI.hxx" /* for class StopDecoder */
#include "input/Init.hxx" #include "input/Init.hxx"
#include "input/InputStream.hxx" #include "input/InputStream.hxx"
#include "fs/Path.hxx" #include "fs/Path.hxx"
@@ -244,10 +244,16 @@ try {
ChromaprintDecoderClient client; ChromaprintDecoderClient client;
if (plugin->file_decode != nullptr) { if (plugin->file_decode != nullptr) {
plugin->FileDecode(client, Path::FromFS(c.uri)); try {
plugin->FileDecode(client, Path::FromFS(c.uri));
} catch (StopDecoder) {
}
} else if (plugin->stream_decode != nullptr) { } else if (plugin->stream_decode != nullptr) {
auto is = InputStream::OpenReady(c.uri, client.mutex); auto is = InputStream::OpenReady(c.uri, client.mutex);
plugin->StreamDecode(client, *is); try {
plugin->StreamDecode(client, *is);
} catch (StopDecoder) {
}
} else { } else {
fprintf(stderr, "Decoder plugin is not usable\n"); fprintf(stderr, "Decoder plugin is not usable\n");
return EXIT_FAILURE; return EXIT_FAILURE;

View File

@@ -21,6 +21,7 @@
#include "event/Thread.hxx" #include "event/Thread.hxx"
#include "decoder/DecoderList.hxx" #include "decoder/DecoderList.hxx"
#include "decoder/DecoderPlugin.hxx" #include "decoder/DecoderPlugin.hxx"
#include "decoder/DecoderAPI.hxx" /* for class StopDecoder */
#include "DumpDecoderClient.hxx" #include "DumpDecoderClient.hxx"
#include "input/Init.hxx" #include "input/Init.hxx"
#include "input/InputStream.hxx" #include "input/InputStream.hxx"
@@ -116,10 +117,16 @@ try {
DumpDecoderClient client; DumpDecoderClient client;
if (plugin->file_decode != nullptr) { if (plugin->file_decode != nullptr) {
plugin->FileDecode(client, Path::FromFS(c.uri)); try {
plugin->FileDecode(client, Path::FromFS(c.uri));
} catch (StopDecoder) {
}
} else if (plugin->stream_decode != nullptr) { } else if (plugin->stream_decode != nullptr) {
auto is = InputStream::OpenReady(c.uri, client.mutex); auto is = InputStream::OpenReady(c.uri, client.mutex);
plugin->StreamDecode(client, *is); try {
plugin->StreamDecode(client, *is);
} catch (StopDecoder) {
}
} else { } else {
fprintf(stderr, "Decoder plugin is not usable\n"); fprintf(stderr, "Decoder plugin is not usable\n");
return EXIT_FAILURE; return EXIT_FAILURE;

View File

@@ -207,7 +207,6 @@ TEST_F(TranslateSongTest, Insecure)
TEST_F(TranslateSongTest, Secure) TEST_F(TranslateSongTest, Secure)
{ {
DetachedSong song1(uri1, MakeTag1b()); DetachedSong song1(uri1, MakeTag1b());
auto s1 = ToString(song1);
auto se = ToString(DetachedSong(uri1, MakeTag1c())); auto se = ToString(DetachedSong(uri1, MakeTag1c()));
const SongLoader loader(nullptr, nullptr); const SongLoader loader(nullptr, nullptr);
@@ -226,14 +225,12 @@ TEST_F(TranslateSongTest, InDatabase)
loader)); loader));
DetachedSong song2(uri2, MakeTag2b()); DetachedSong song2(uri2, MakeTag2b());
auto s1 = ToString(song2);
auto se = ToString(DetachedSong(uri2, MakeTag2c())); auto se = ToString(DetachedSong(uri2, MakeTag2c()));
EXPECT_TRUE(playlist_check_translate_song(song2, nullptr, EXPECT_TRUE(playlist_check_translate_song(song2, nullptr,
loader)); loader));
EXPECT_EQ(se, ToString(song2)); EXPECT_EQ(se, ToString(song2));
DetachedSong song3("/music/foo/bar.ogg", MakeTag2b()); DetachedSong song3("/music/foo/bar.ogg", MakeTag2b());
s1 = ToString(song3);
se = ToString(DetachedSong(uri2, MakeTag2c())); se = ToString(DetachedSong(uri2, MakeTag2c()));
EXPECT_TRUE(playlist_check_translate_song(song3, nullptr, EXPECT_TRUE(playlist_check_translate_song(song3, nullptr,
loader)); loader));
@@ -249,7 +246,6 @@ TEST_F(TranslateSongTest, Relative)
/* map to music_directory */ /* map to music_directory */
DetachedSong song1("bar.ogg", MakeTag2b()); DetachedSong song1("bar.ogg", MakeTag2b());
auto s1 = ToString(song1);
auto se = ToString(DetachedSong(uri2, MakeTag2c())); auto se = ToString(DetachedSong(uri2, MakeTag2c()));
EXPECT_TRUE(playlist_check_translate_song(song1, "/music/foo", EXPECT_TRUE(playlist_check_translate_song(song1, "/music/foo",
insecure_loader)); insecure_loader));
@@ -262,7 +258,6 @@ TEST_F(TranslateSongTest, Relative)
/* legal because secure=true */ /* legal because secure=true */
DetachedSong song3("bar.ogg", MakeTag1b()); DetachedSong song3("bar.ogg", MakeTag1b());
s1 = ToString(song3);
se = ToString(DetachedSong(uri1, MakeTag1c())); se = ToString(DetachedSong(uri1, MakeTag1c()));
EXPECT_TRUE(playlist_check_translate_song(song3, "/foo", EXPECT_TRUE(playlist_check_translate_song(song3, "/foo",
secure_loader)); secure_loader));
@@ -270,9 +265,28 @@ TEST_F(TranslateSongTest, Relative)
/* relative to http:// */ /* relative to http:// */
DetachedSong song4("bar.ogg", MakeTag2a()); DetachedSong song4("bar.ogg", MakeTag2a());
s1 = ToString(song4);
se = ToString(DetachedSong("http://example.com/foo/bar.ogg", MakeTag2a())); se = ToString(DetachedSong("http://example.com/foo/bar.ogg", MakeTag2a()));
EXPECT_TRUE(playlist_check_translate_song(song4, "http://example.com/foo", EXPECT_TRUE(playlist_check_translate_song(song4, "http://example.com/foo",
insecure_loader)); insecure_loader));
EXPECT_EQ(se, ToString(song4)); EXPECT_EQ(se, ToString(song4));
} }
TEST_F(TranslateSongTest, Backslash)
{
const SongLoader loader(reinterpret_cast<const Database *>(1),
storage);
DetachedSong song1("foo\\bar.ogg", MakeTag2b());
#ifdef _WIN32
/* on Windows, all backslashes are converted to slashes in
relative paths from playlists */
auto se = ToString(DetachedSong(uri2, MakeTag2c()));
EXPECT_TRUE(playlist_check_translate_song(song1, nullptr,
loader));
EXPECT_EQ(se, ToString(song1));
#else
/* backslash only supported on Windows */
EXPECT_FALSE(playlist_check_translate_song(song1, nullptr,
loader));
#endif
}