Compare commits
68 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f084bf7872 | ||
![]() |
1112d3907a | ||
![]() |
3464497880 | ||
![]() |
651f57bced | ||
![]() |
b4e72aba6c | ||
![]() |
061dd2dfef | ||
![]() |
5f4ec7de5b | ||
![]() |
6f81bb4b09 | ||
![]() |
4ed60a5711 | ||
![]() |
c93195c94b | ||
![]() |
f30adac4bb | ||
![]() |
a4e4217204 | ||
![]() |
8754d705a1 | ||
![]() |
23d4a2d6a5 | ||
![]() |
ce77b148d9 | ||
![]() |
be3eca39e8 | ||
![]() |
3413b1aeb4 | ||
![]() |
356d13e9dd | ||
![]() |
fa34bf0aaf | ||
![]() |
5d0941476a | ||
![]() |
5ff0bbd0f8 | ||
![]() |
a3764e533c | ||
![]() |
3e05cba30e | ||
![]() |
14b3c0f0af | ||
![]() |
67aff05051 | ||
![]() |
19a101c3ac | ||
![]() |
8da17a8211 | ||
![]() |
2748929039 | ||
![]() |
0c900a4bfa | ||
![]() |
f1d5d70010 | ||
![]() |
56ebc7637d | ||
![]() |
996dd9fc8b | ||
![]() |
056514d598 | ||
![]() |
9a21bdfd6a | ||
![]() |
03f99dd26e | ||
![]() |
bfb1b641f9 | ||
![]() |
72ba98c464 | ||
![]() |
dcd19c0592 | ||
![]() |
109159e0f7 | ||
![]() |
409b877eea | ||
![]() |
c5bf7948ff | ||
![]() |
b9f7127691 | ||
![]() |
1e6f5f012c | ||
![]() |
225d85fd9b | ||
![]() |
1bb22f118d | ||
![]() |
552c30eae4 | ||
![]() |
48e8a26813 | ||
![]() |
ade847bc89 | ||
![]() |
a6173e0eae | ||
![]() |
4529bb4a83 | ||
![]() |
258ecb764f | ||
![]() |
6f595e9abb | ||
![]() |
35c4c7e8bf | ||
![]() |
293ed924d1 | ||
![]() |
c8121176b3 | ||
![]() |
ee270f9b00 | ||
![]() |
bf1d77a4d8 | ||
![]() |
a9344fafe9 | ||
![]() |
b8890726f2 | ||
![]() |
0f84332654 | ||
![]() |
46c82259f7 | ||
![]() |
2d03823283 | ||
![]() |
bba144eca5 | ||
![]() |
9af73dad93 | ||
![]() |
f0d66bf6a6 | ||
![]() |
5ad53a7554 | ||
![]() |
7b2e3331f2 | ||
![]() |
3cb44f6652 |
26
NEWS
26
NEWS
@@ -1,3 +1,29 @@
|
|||||||
|
ver 0.23.4 (2021/11/11)
|
||||||
|
* protocol
|
||||||
|
- add optional position parameter to "searchaddpl"
|
||||||
|
* decoder
|
||||||
|
- ffmpeg: support libavcodec 59
|
||||||
|
* output
|
||||||
|
- alsa: add option "thesycon_dsd_workaround" to work around device bug
|
||||||
|
* fix crash on debug builds if startup fails
|
||||||
|
* systemd
|
||||||
|
- remove "RuntimeDirectory" directive because it caused problems
|
||||||
|
- ignore the "pid_file" setting if started as systemd service
|
||||||
|
* Windows
|
||||||
|
- enable the "openmpt" decoder plugin
|
||||||
|
|
||||||
|
ver 0.23.3 (2021/10/31)
|
||||||
|
* protocol
|
||||||
|
- add optional position parameter to "add" and "playlistadd"
|
||||||
|
- allow range in "playlistdelete"
|
||||||
|
* database
|
||||||
|
- fix scanning files with question mark in the name
|
||||||
|
- inotify: fix use-after-free bug
|
||||||
|
* output
|
||||||
|
- alsa: add option "stop_dsd_silence" to work around DSD DAC noise
|
||||||
|
* macOS: fix libfmt related build failure
|
||||||
|
* systemd: add "RuntimeDirectory" directive
|
||||||
|
|
||||||
ver 0.23.2 (2021/10/22)
|
ver 0.23.2 (2021/10/22)
|
||||||
* protocol
|
* protocol
|
||||||
- fix "albumart" timeout bug
|
- fix "albumart" timeout bug
|
||||||
|
@@ -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="62"
|
android:versionCode="64"
|
||||||
android:versionName="0.23.2">
|
android:versionName="0.23.4">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/>
|
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/>
|
||||||
|
|
||||||
|
@@ -13,7 +13,7 @@ GENCLASS="$D/classes"
|
|||||||
GENINCLUDE="$D/include"
|
GENINCLUDE="$D/include"
|
||||||
|
|
||||||
mkdir -p "$GENSRC/$JAVA_PKG_PATH"
|
mkdir -p "$GENSRC/$JAVA_PKG_PATH"
|
||||||
"$JAVAC" -source 1.6 -target 1.6 -Xlint:-options \
|
"$JAVAC" -source 1.7 -target 1.7 -Xlint:-options \
|
||||||
-cp "$CLASSPATH" \
|
-cp "$CLASSPATH" \
|
||||||
-h "$GENINCLUDE" \
|
-h "$GENINCLUDE" \
|
||||||
-d "$GENCLASS" \
|
-d "$GENCLASS" \
|
||||||
|
@@ -38,7 +38,7 @@ author = 'Max Kellermann'
|
|||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '0.23.2'
|
version = '0.23.4'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
#release = version + '~git'
|
#release = version + '~git'
|
||||||
|
|
||||||
|
@@ -26,22 +26,25 @@
|
|||||||
# files over an accepted protocol.
|
# files over an accepted protocol.
|
||||||
#
|
#
|
||||||
#db_file "~/.mpd/database"
|
#db_file "~/.mpd/database"
|
||||||
#
|
|
||||||
# These settings are the locations for the daemon log files for the daemon.
|
# These settings are the locations for the daemon log files for the daemon.
|
||||||
# These logs are great for troubleshooting, depending on your log_level
|
|
||||||
# settings.
|
|
||||||
#
|
#
|
||||||
# The special value "syslog" makes MPD use the local syslog daemon. This
|
# The special value "syslog" makes MPD use the local syslog daemon. This
|
||||||
# setting defaults to logging to syslog.
|
# setting defaults to logging to syslog.
|
||||||
#
|
#
|
||||||
#log_file "~/.mpd/log"
|
# If you use systemd, do not configure a log_file. With systemd, MPD
|
||||||
|
# defaults to the systemd journal, which is fine.
|
||||||
#
|
#
|
||||||
|
#log_file "~/.mpd/log"
|
||||||
|
|
||||||
# This setting sets the location of the file which stores the process ID
|
# This setting sets the location of the file which stores the process ID
|
||||||
# for use of mpd --kill and some init scripts. This setting is disabled by
|
# for use of mpd --kill and some init scripts. This setting is disabled by
|
||||||
# default and the pid file will not be stored.
|
# default and the pid file will not be stored.
|
||||||
#
|
#
|
||||||
#pid_file "~/.mpd/pid"
|
# If you use systemd, do not configure a pid_file.
|
||||||
#
|
#
|
||||||
|
#pid_file "~/.mpd/pid"
|
||||||
|
|
||||||
# This setting sets the location of the file which contains information about
|
# This setting sets the location of the file which contains information about
|
||||||
# most variables to get MPD back into the same general shape it was in before
|
# most variables to get MPD back into the same general shape it was in before
|
||||||
# it was brought down. This setting is disabled by default and the server
|
# it was brought down. This setting is disabled by default and the server
|
||||||
|
@@ -61,6 +61,15 @@ upnp
|
|||||||
|
|
||||||
Provides access to UPnP media servers.
|
Provides access to UPnP media servers.
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 80
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Setting
|
||||||
|
- Description
|
||||||
|
* - **interface**
|
||||||
|
- Interface used to discover media servers. Decided by upnp if left unconfigured.
|
||||||
|
|
||||||
Storage plugins
|
Storage plugins
|
||||||
===============
|
===============
|
||||||
|
|
||||||
@@ -836,6 +845,16 @@ The `Advanced Linux Sound Architecture (ALSA) <http://www.alsa-project.org/>`_ p
|
|||||||
- If set to no, then libasound will not attempt to convert between different sample formats (16 bit, 24 bit, floating point, ...).
|
- If set to no, then libasound will not attempt to convert between different sample formats (16 bit, 24 bit, floating point, ...).
|
||||||
* - **dop yes|no**
|
* - **dop yes|no**
|
||||||
- If set to yes, then DSD over PCM according to the `DoP standard <http://dsd-guide.com/dop-open-standard>`_ is enabled. This wraps DSD samples in fake 24 bit PCM, and is understood by some DSD capable products, but may be harmful to other hardware. Therefore, the default is no and you can enable the option at your own risk.
|
- If set to yes, then DSD over PCM according to the `DoP standard <http://dsd-guide.com/dop-open-standard>`_ is enabled. This wraps DSD samples in fake 24 bit PCM, and is understood by some DSD capable products, but may be harmful to other hardware. Therefore, the default is no and you can enable the option at your own risk.
|
||||||
|
* - **stop_dsd_silence yes|no**
|
||||||
|
- If enabled, silence is played before manually stopping playback
|
||||||
|
("stop" or "pause") in DSD mode (native DSD or DoP). This is a
|
||||||
|
workaround for some DACs which emit noise when stopping DSD
|
||||||
|
playback.
|
||||||
|
* - **thesycon_dsd_workaround yes|no**
|
||||||
|
- If enabled, enables a workaround for a bug in Thesycon USB
|
||||||
|
audio receivers. On these devices, playing DSD512 or PCM
|
||||||
|
causes all subsequent attempts to play other DSD rates to fail,
|
||||||
|
which can be fixed by briefly playing PCM at 44.1 kHz.
|
||||||
* - **allowed_formats F1 F2 ...**
|
* - **allowed_formats F1 F2 ...**
|
||||||
- Specifies a list of allowed audio formats, separated by a space. All items may contain asterisks as a wild card, and may be followed by "=dop" to enable DoP (DSD over PCM) for this particular format. The first matching format is used, and if none matches, MPD chooses the best fallback of this list.
|
- Specifies a list of allowed audio formats, separated by a space. All items may contain asterisks as a wild card, and may be followed by "=dop" to enable DoP (DSD over PCM) for this particular format. The first matching format is used, and if none matches, MPD chooses the best fallback of this list.
|
||||||
|
|
||||||
|
@@ -551,7 +551,7 @@ Playback options
|
|||||||
|
|
||||||
.. _command_getvol:
|
.. _command_getvol:
|
||||||
|
|
||||||
:command:`getvol`
|
:command:`getvol` [#since_0_23]_
|
||||||
|
|
||||||
Read the volume. The result is a ``volume:`` line like in
|
Read the volume. The result is a ``volume:`` line like in
|
||||||
:ref:`status <command_status>`. If there is no mixer, MPD will
|
:ref:`status <command_status>`. If there is no mixer, MPD will
|
||||||
@@ -689,11 +689,14 @@ Whenever possible, ids should be used.
|
|||||||
|
|
||||||
.. _command_add:
|
.. _command_add:
|
||||||
|
|
||||||
:command:`add {URI}`
|
:command:`add {URI} [POSITION]`
|
||||||
Adds the file ``URI`` to the playlist
|
Adds the file ``URI`` to the playlist
|
||||||
(directories add recursively). ``URI``
|
(directories add recursively). ``URI``
|
||||||
can also be a single file.
|
can also be a single file.
|
||||||
|
|
||||||
|
The position parameter is the same as in :ref:`addid
|
||||||
|
<command_addid>`. [#since_0_23_3]_
|
||||||
|
|
||||||
Clients that are connected via local socket may add arbitrary
|
Clients that are connected via local socket may add arbitrary
|
||||||
local files (URI is an absolute path). Example::
|
local files (URI is an absolute path). Example::
|
||||||
|
|
||||||
@@ -711,10 +714,10 @@ Whenever possible, ids should be used.
|
|||||||
|
|
||||||
If the second parameter is given, then the song is inserted at the
|
If the second parameter is given, then the song is inserted at the
|
||||||
specified position. If the parameter starts with ``+`` or ``-``,
|
specified position. If the parameter starts with ``+`` or ``-``,
|
||||||
then it is relative to the current song; e.g. ``+0`` inserts right
|
then it is relative to the current song [#since_0_23]_; e.g. ``+0``
|
||||||
after the current song and ``-0`` inserts right before the current
|
inserts right after the current song and ``-0`` inserts right
|
||||||
song (i.e. zero songs between the current song and the newly added
|
before the current song (i.e. zero songs between the current song
|
||||||
song).
|
and the newly added song).
|
||||||
|
|
||||||
.. _command_clear:
|
.. _command_clear:
|
||||||
|
|
||||||
@@ -926,16 +929,19 @@ remote playlists (absolute URI with a supported scheme).
|
|||||||
inserted into the queue; it can be relative as described in
|
inserted into the queue; it can be relative as described in
|
||||||
:ref:`addid <command_addid>`. (This requires specifying the range
|
:ref:`addid <command_addid>`. (This requires specifying the range
|
||||||
as well; the special value `0:` can be used if the whole playlist
|
as well; the special value `0:` can be used if the whole playlist
|
||||||
shall be loaded at a certain queue position.)
|
shall be loaded at a certain queue position.) [#since_0_23_1]_
|
||||||
|
|
||||||
.. _command_playlistadd:
|
.. _command_playlistadd:
|
||||||
|
|
||||||
:command:`playlistadd {NAME} {URI}`
|
:command:`playlistadd {NAME} {URI} [POSITION]`
|
||||||
Adds ``URI`` to the playlist
|
Adds ``URI`` to the playlist
|
||||||
`NAME.m3u`.
|
`NAME.m3u`.
|
||||||
`NAME.m3u` will be created if it does
|
`NAME.m3u` will be created if it does
|
||||||
not exist.
|
not exist.
|
||||||
|
|
||||||
|
The ``POSITION`` parameter specifies where the songs will be
|
||||||
|
inserted into the playlist. [#since_0_23_3]_
|
||||||
|
|
||||||
.. _command_playlistclear:
|
.. _command_playlistclear:
|
||||||
|
|
||||||
:command:`playlistclear {NAME}`
|
:command:`playlistclear {NAME}`
|
||||||
@@ -947,6 +953,8 @@ remote playlists (absolute URI with a supported scheme).
|
|||||||
Deletes ``SONGPOS`` from the
|
Deletes ``SONGPOS`` from the
|
||||||
playlist `NAME.m3u`.
|
playlist `NAME.m3u`.
|
||||||
|
|
||||||
|
The second parameter can be a range. [#since_0_23_3]_
|
||||||
|
|
||||||
.. _command_playlistmove:
|
.. _command_playlistmove:
|
||||||
|
|
||||||
:command:`playlistmove {NAME} {FROM} {TO}`
|
:command:`playlistmove {NAME} {FROM} {TO}`
|
||||||
@@ -1197,7 +1205,7 @@ The music database
|
|||||||
|
|
||||||
.. _command_search:
|
.. _command_search:
|
||||||
|
|
||||||
:command:`search {FILTER} [sort {TYPE}] [window {START:END}]
|
:command:`search {FILTER} [sort {TYPE}] [window {START:END}]`
|
||||||
Search the database for songs matching
|
Search the database for songs matching
|
||||||
``FILTER`` (see :ref:`Filters <filter_syntax>`). Parameters
|
``FILTER`` (see :ref:`Filters <filter_syntax>`). Parameters
|
||||||
have the same meaning as for :ref:`find <command_find>`,
|
have the same meaning as for :ref:`find <command_find>`,
|
||||||
@@ -1213,11 +1221,11 @@ The music database
|
|||||||
Parameters have the same meaning as for :ref:`search <command_search>`.
|
Parameters have the same meaning as for :ref:`search <command_search>`.
|
||||||
|
|
||||||
The ``position`` parameter specifies where the songs will be
|
The ``position`` parameter specifies where the songs will be
|
||||||
inserted.
|
inserted. [#since_0_23]_
|
||||||
|
|
||||||
.. _command_searchaddpl:
|
.. _command_searchaddpl:
|
||||||
|
|
||||||
:command:`searchaddpl {NAME} {FILTER} [sort {TYPE}] [window {START:END}]`
|
:command:`searchaddpl {NAME} {FILTER} [sort {TYPE}] [window {START:END}] [position POS]`
|
||||||
Search the database for songs matching
|
Search the database for songs matching
|
||||||
``FILTER`` (see :ref:`Filters <filter_syntax>`) and add them to
|
``FILTER`` (see :ref:`Filters <filter_syntax>`) and add them to
|
||||||
the playlist named ``NAME``.
|
the playlist named ``NAME``.
|
||||||
@@ -1226,6 +1234,9 @@ The music database
|
|||||||
|
|
||||||
Parameters have the same meaning as for :ref:`search <command_search>`.
|
Parameters have the same meaning as for :ref:`search <command_search>`.
|
||||||
|
|
||||||
|
The ``position`` parameter specifies where the songs will be
|
||||||
|
inserted. [#since_0_23_4]_
|
||||||
|
|
||||||
.. _command_update:
|
.. _command_update:
|
||||||
|
|
||||||
:command:`update [URI]`
|
:command:`update [URI]`
|
||||||
@@ -1644,3 +1655,7 @@ client-to-client messages are local to the current partition.
|
|||||||
.. [#since_0_20] Since :program:`MPD` 0.20
|
.. [#since_0_20] Since :program:`MPD` 0.20
|
||||||
.. [#since_0_21] Since :program:`MPD` 0.21
|
.. [#since_0_21] Since :program:`MPD` 0.21
|
||||||
.. [#since_0_22_4] Since :program:`MPD` 0.22.4
|
.. [#since_0_22_4] Since :program:`MPD` 0.22.4
|
||||||
|
.. [#since_0_23] Since :program:`MPD` 0.23
|
||||||
|
.. [#since_0_23_1] Since :program:`MPD` 0.23.1
|
||||||
|
.. [#since_0_23_3] Since :program:`MPD` 0.23.3
|
||||||
|
.. [#since_0_23_4] Since :program:`MPD` 0.23.4
|
||||||
|
15
doc/user.rst
15
doc/user.rst
@@ -172,7 +172,9 @@ tarball and change into the directory. Then, instead of
|
|||||||
|
|
||||||
mkdir -p output/win64
|
mkdir -p output/win64
|
||||||
cd output/win64
|
cd output/win64
|
||||||
../../win32/build.py --64
|
../../win32/build.py --64 \
|
||||||
|
--buildtype=debugoptimized -Db_ndebug=true \
|
||||||
|
-Dwrap_mode=forcefallback
|
||||||
|
|
||||||
This downloads various library sources, and then configures and builds
|
This downloads various library sources, and then configures and builds
|
||||||
:program:`MPD` (for x64; to build a 32 bit binary, pass
|
:program:`MPD` (for x64; to build a 32 bit binary, pass
|
||||||
@@ -182,6 +184,11 @@ around. It is large, but easy to use. If you wish to have a small
|
|||||||
mpd.exe with DLLs, you need to compile manually, without the
|
mpd.exe with DLLs, you need to compile manually, without the
|
||||||
:file:`build.py` script.
|
:file:`build.py` script.
|
||||||
|
|
||||||
|
The option ``-Dwrap_mode=forcefallback`` tells Meson to download and
|
||||||
|
cross-compile several libraries used by MPD instead of looking for
|
||||||
|
them on your computer.
|
||||||
|
|
||||||
|
|
||||||
Compiling for Android
|
Compiling for Android
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
@@ -205,8 +212,10 @@ tarball and change into the directory. Then, instead of
|
|||||||
|
|
||||||
mkdir -p output/android
|
mkdir -p output/android
|
||||||
cd output/android
|
cd output/android
|
||||||
../../android/build.py SDK_PATH NDK_PATH ABI
|
../../android/build.py SDK_PATH NDK_PATH ABI \
|
||||||
meson configure -Dandroid_debug_keystore=$HOME/.android/debug.keystore
|
--buildtype=debugoptimized -Db_ndebug=true \
|
||||||
|
-Dwrap_mode=forcefallback \
|
||||||
|
-Dandroid_debug_keystore=$HOME/.android/debug.keystore
|
||||||
ninja android/apk/mpd-debug.apk
|
ninja android/apk/mpd-debug.apk
|
||||||
|
|
||||||
:envvar:`SDK_PATH` is the absolute path where you installed the
|
:envvar:`SDK_PATH` is the absolute path where you installed the
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
project(
|
project(
|
||||||
'mpd',
|
'mpd',
|
||||||
['c', 'cpp'],
|
['c', 'cpp'],
|
||||||
version: '0.23.2',
|
version: '0.23.4',
|
||||||
meson_version: '>= 0.56.0',
|
meson_version: '>= 0.56.0',
|
||||||
default_options: [
|
default_options: [
|
||||||
'c_std=c11',
|
'c_std=c11',
|
||||||
@@ -44,7 +44,7 @@ version_conf = configuration_data()
|
|||||||
version_conf.set_quoted('PACKAGE', meson.project_name())
|
version_conf.set_quoted('PACKAGE', meson.project_name())
|
||||||
version_conf.set_quoted('PACKAGE_NAME', meson.project_name())
|
version_conf.set_quoted('PACKAGE_NAME', meson.project_name())
|
||||||
version_conf.set_quoted('VERSION', meson.project_version())
|
version_conf.set_quoted('VERSION', meson.project_version())
|
||||||
version_conf.set_quoted('PROTOCOL_VERSION', '0.23.1')
|
version_conf.set_quoted('PROTOCOL_VERSION', '0.23.4')
|
||||||
configure_file(output: 'Version.h', configuration: version_conf)
|
configure_file(output: 'Version.h', configuration: version_conf)
|
||||||
|
|
||||||
conf = configuration_data()
|
conf = configuration_data()
|
||||||
|
@@ -112,12 +112,16 @@ libmodplug = AutotoolsProject(
|
|||||||
)
|
)
|
||||||
|
|
||||||
libopenmpt = AutotoolsProject(
|
libopenmpt = AutotoolsProject(
|
||||||
'https://lib.openmpt.org/files/libopenmpt/src/libopenmpt-0.5.8+release.autotools.tar.gz',
|
'https://lib.openmpt.org/files/libopenmpt/src/libopenmpt-0.5.12+release.autotools.tar.gz',
|
||||||
'61de7cc0c011b10472ca16adcc123689',
|
'892aea7a599b5d21842bebf463b5aafdad5711be7008dd84401920c6234820af',
|
||||||
'lib/libopenmpt.a',
|
'lib/libopenmpt.a',
|
||||||
[
|
[
|
||||||
'--disable-shared', '--enable-static'
|
'--disable-shared', '--enable-static',
|
||||||
|
'--disable-openmpt123',
|
||||||
|
'--without-mpg123', '--without-ogg', '--without-vorbis', '--without-vorbisfile',
|
||||||
|
'--without-portaudio', '--without-portaudiocpp', '--without-sndfile',
|
||||||
],
|
],
|
||||||
|
base='libopenmpt-0.5.12+release.autotools',
|
||||||
)
|
)
|
||||||
|
|
||||||
wildmidi = CmakeProject(
|
wildmidi = CmakeProject(
|
||||||
@@ -147,8 +151,8 @@ gme = CmakeProject(
|
|||||||
)
|
)
|
||||||
|
|
||||||
ffmpeg = FfmpegProject(
|
ffmpeg = FfmpegProject(
|
||||||
'http://ffmpeg.org/releases/ffmpeg-4.4.tar.xz',
|
'http://ffmpeg.org/releases/ffmpeg-4.4.1.tar.xz',
|
||||||
'06b10a183ce5371f915c6bb15b7b1fffbe046e8275099c96affc29e17645d909',
|
'eadbad9e9ab30b25f5520fbfde99fae4a92a1ae3c0257a8d68569a4651e30e02',
|
||||||
'lib/libavcodec.a',
|
'lib/libavcodec.a',
|
||||||
[
|
[
|
||||||
'--disable-shared', '--enable-static',
|
'--disable-shared', '--enable-static',
|
||||||
|
@@ -86,6 +86,9 @@ enum Option {
|
|||||||
OPTION_KILL,
|
OPTION_KILL,
|
||||||
OPTION_NO_CONFIG,
|
OPTION_NO_CONFIG,
|
||||||
OPTION_NO_DAEMON,
|
OPTION_NO_DAEMON,
|
||||||
|
#ifdef __linux__
|
||||||
|
OPTION_SYSTEMD,
|
||||||
|
#endif
|
||||||
OPTION_STDOUT,
|
OPTION_STDOUT,
|
||||||
OPTION_STDERR,
|
OPTION_STDERR,
|
||||||
OPTION_VERBOSE,
|
OPTION_VERBOSE,
|
||||||
@@ -98,6 +101,9 @@ static constexpr OptionDef option_defs[] = {
|
|||||||
{"kill", "kill the currently running mpd session"},
|
{"kill", "kill the currently running mpd session"},
|
||||||
{"no-config", "don't read from config"},
|
{"no-config", "don't read from config"},
|
||||||
{"no-daemon", "don't detach from console"},
|
{"no-daemon", "don't detach from console"},
|
||||||
|
#ifdef __linux__
|
||||||
|
{"systemd", "systemd service mode"},
|
||||||
|
#endif
|
||||||
{"stdout", nullptr}, // hidden, compatibility with old versions
|
{"stdout", nullptr}, // hidden, compatibility with old versions
|
||||||
{"stderr", "print messages to stderr"},
|
{"stderr", "print messages to stderr"},
|
||||||
{"verbose", 'v', "verbose logging"},
|
{"verbose", 'v', "verbose logging"},
|
||||||
@@ -328,7 +334,7 @@ bool ConfigLoader::TryFile(const AllocatedPath &base_path, Path path)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
ParseCommandLine(int argc, char **argv, struct options &options,
|
ParseCommandLine(int argc, char **argv, CommandLineOptions &options,
|
||||||
ConfigData &config)
|
ConfigData &config)
|
||||||
{
|
{
|
||||||
bool use_config_file = true;
|
bool use_config_file = true;
|
||||||
@@ -349,6 +355,13 @@ ParseCommandLine(int argc, char **argv, struct options &options,
|
|||||||
options.daemon = false;
|
options.daemon = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
case OPTION_SYSTEMD:
|
||||||
|
options.daemon = false;
|
||||||
|
options.systemd = true;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
|
||||||
case OPTION_STDOUT:
|
case OPTION_STDOUT:
|
||||||
case OPTION_STDERR:
|
case OPTION_STDERR:
|
||||||
options.log_stderr = true;
|
options.log_stderr = true;
|
||||||
|
@@ -22,15 +22,20 @@
|
|||||||
|
|
||||||
struct ConfigData;
|
struct ConfigData;
|
||||||
|
|
||||||
struct options {
|
struct CommandLineOptions {
|
||||||
bool kill = false;
|
bool kill = false;
|
||||||
bool daemon = true;
|
bool daemon = true;
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
bool systemd = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
bool log_stderr = false;
|
bool log_stderr = false;
|
||||||
bool verbose = false;
|
bool verbose = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
void
|
void
|
||||||
ParseCommandLine(int argc, char **argv, struct options &options,
|
ParseCommandLine(int argc, char **argv, CommandLineOptions &options,
|
||||||
ConfigData &config);
|
ConfigData &config);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@@ -32,6 +32,7 @@
|
|||||||
#include "net/SocketUtil.hxx"
|
#include "net/SocketUtil.hxx"
|
||||||
#include "system/Error.hxx"
|
#include "system/Error.hxx"
|
||||||
#include "fs/AllocatedPath.hxx"
|
#include "fs/AllocatedPath.hxx"
|
||||||
|
#include "fs/StandardDirectory.hxx"
|
||||||
#include "fs/XDG.hxx"
|
#include "fs/XDG.hxx"
|
||||||
#include "util/Domain.hxx"
|
#include "util/Domain.hxx"
|
||||||
#include "util/RuntimeError.hxx"
|
#include "util/RuntimeError.hxx"
|
||||||
@@ -85,13 +86,10 @@ ListenXdgRuntimeDir(ClientListener &listener) noexcept
|
|||||||
use $XDG_RUNTIME_DIR */
|
use $XDG_RUNTIME_DIR */
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Path xdg_runtime_dir = Path::FromFS(getenv("XDG_RUNTIME_DIR"));
|
const auto mpd_runtime_dir = GetAppRuntimeDir();
|
||||||
if (xdg_runtime_dir.IsNull())
|
if (mpd_runtime_dir.IsNull())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const auto mpd_runtime_dir = xdg_runtime_dir / Path::FromFS("mpd");
|
|
||||||
mkdir(mpd_runtime_dir.c_str(), 0700);
|
|
||||||
|
|
||||||
const auto socket_path = mpd_runtime_dir / Path::FromFS("socket");
|
const auto socket_path = mpd_runtime_dir / Path::FromFS("socket");
|
||||||
unlink(socket_path.c_str());
|
unlink(socket_path.c_str());
|
||||||
|
|
||||||
|
25
src/Main.cxx
25
src/Main.cxx
@@ -142,14 +142,24 @@ struct Config {
|
|||||||
#ifdef ENABLE_DAEMON
|
#ifdef ENABLE_DAEMON
|
||||||
|
|
||||||
static void
|
static void
|
||||||
glue_daemonize_init(const struct options *options,
|
glue_daemonize_init(const CommandLineOptions &options,
|
||||||
const ConfigData &config)
|
const ConfigData &config)
|
||||||
{
|
{
|
||||||
|
auto pid_file = config.GetPath(ConfigOption::PID_FILE);
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
if (options.systemd && pid_file != nullptr) {
|
||||||
|
pid_file = nullptr;
|
||||||
|
fprintf(stderr,
|
||||||
|
"Ignoring the 'pid_file' setting in systemd mode\n");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
daemonize_init(config.GetString(ConfigOption::USER),
|
daemonize_init(config.GetString(ConfigOption::USER),
|
||||||
config.GetString(ConfigOption::GROUP),
|
config.GetString(ConfigOption::GROUP),
|
||||||
config.GetPath(ConfigOption::PID_FILE));
|
std::move(pid_file));
|
||||||
|
|
||||||
if (options->kill)
|
if (options.kill)
|
||||||
daemonize_kill();
|
daemonize_kill();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +371,8 @@ Instance::BeginShutdownPartitions() noexcept
|
|||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
MainConfigured(const struct options &options, const ConfigData &raw_config)
|
MainConfigured(const CommandLineOptions &options,
|
||||||
|
const ConfigData &raw_config)
|
||||||
{
|
{
|
||||||
#ifdef ENABLE_DAEMON
|
#ifdef ENABLE_DAEMON
|
||||||
daemonize_close_stdin();
|
daemonize_close_stdin();
|
||||||
@@ -384,7 +395,7 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
|
|||||||
const Config config(raw_config);
|
const Config config(raw_config);
|
||||||
|
|
||||||
#ifdef ENABLE_DAEMON
|
#ifdef ENABLE_DAEMON
|
||||||
glue_daemonize_init(&options, raw_config);
|
glue_daemonize_init(options, raw_config);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
TagLoadConfig(raw_config);
|
TagLoadConfig(raw_config);
|
||||||
@@ -582,7 +593,7 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
|
|||||||
static void
|
static void
|
||||||
AndroidMain()
|
AndroidMain()
|
||||||
{
|
{
|
||||||
struct options options;
|
CommandLineOptions options;
|
||||||
ConfigData raw_config;
|
ConfigData raw_config;
|
||||||
|
|
||||||
const auto sdcard = Environment::getExternalStorageDirectory();
|
const auto sdcard = Environment::getExternalStorageDirectory();
|
||||||
@@ -642,7 +653,7 @@ Java_org_musicpd_Bridge_pause(JNIEnv *, jclass)
|
|||||||
static inline void
|
static inline void
|
||||||
MainOrThrow(int argc, char *argv[])
|
MainOrThrow(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
struct options options;
|
CommandLineOptions options;
|
||||||
ConfigData raw_config;
|
ConfigData raw_config;
|
||||||
|
|
||||||
ParseCommandLine(argc, argv, options, raw_config);
|
ParseCommandLine(argc, argv, options, raw_config);
|
||||||
|
@@ -30,7 +30,6 @@
|
|||||||
#include "util/StringView.hxx"
|
#include "util/StringView.hxx"
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cstring>
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -100,18 +99,15 @@ initPermissions(const ConfigData &config)
|
|||||||
for (const auto ¶m : config.GetParamList(ConfigOption::PASSWORD)) {
|
for (const auto ¶m : config.GetParamList(ConfigOption::PASSWORD)) {
|
||||||
permission_default = 0;
|
permission_default = 0;
|
||||||
|
|
||||||
param.With([](const char *value){
|
param.With([](const StringView value){
|
||||||
const char *separator = std::strchr(value,
|
const auto [password, permissions] =
|
||||||
PERMISSION_PASSWORD_CHAR);
|
value.Split(PERMISSION_PASSWORD_CHAR);
|
||||||
|
if (permissions == nullptr)
|
||||||
if (separator == nullptr)
|
|
||||||
throw FormatRuntimeError("\"%c\" not found in password string",
|
throw FormatRuntimeError("\"%c\" not found in password string",
|
||||||
PERMISSION_PASSWORD_CHAR);
|
PERMISSION_PASSWORD_CHAR);
|
||||||
|
|
||||||
std::string password(value, separator);
|
permission_passwords.emplace(password,
|
||||||
|
parsePermissions(permissions));
|
||||||
unsigned permission = parsePermissions(separator + 1);
|
|
||||||
permission_passwords.emplace(std::move(password), permission);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -26,6 +26,7 @@
|
|||||||
#include "song/DetachedSong.hxx"
|
#include "song/DetachedSong.hxx"
|
||||||
#include "SongLoader.hxx"
|
#include "SongLoader.hxx"
|
||||||
#include "Mapper.hxx"
|
#include "Mapper.hxx"
|
||||||
|
#include "protocol/RangeArg.hxx"
|
||||||
#include "fs/io/TextFile.hxx"
|
#include "fs/io/TextFile.hxx"
|
||||||
#include "fs/io/FileOutputStream.hxx"
|
#include "fs/io/FileOutputStream.hxx"
|
||||||
#include "fs/io/BufferedOutputStream.hxx"
|
#include "fs/io/BufferedOutputStream.hxx"
|
||||||
@@ -34,7 +35,6 @@
|
|||||||
#include "config/Defaults.hxx"
|
#include "config/Defaults.hxx"
|
||||||
#include "Idle.hxx"
|
#include "Idle.hxx"
|
||||||
#include "fs/Limits.hxx"
|
#include "fs/Limits.hxx"
|
||||||
#include "fs/AllocatedPath.hxx"
|
|
||||||
#include "fs/Traits.hxx"
|
#include "fs/Traits.hxx"
|
||||||
#include "fs/FileSystem.hxx"
|
#include "fs/FileSystem.hxx"
|
||||||
#include "fs/FileInfo.hxx"
|
#include "fs/FileInfo.hxx"
|
||||||
@@ -173,11 +173,8 @@ ListPlaylistFiles()
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
SavePlaylistFile(const PlaylistFileContents &contents, const char *utf8path)
|
SavePlaylistFile(Path path_fs, const PlaylistFileContents &contents)
|
||||||
{
|
{
|
||||||
assert(utf8path != nullptr);
|
|
||||||
|
|
||||||
const auto path_fs = spl_map_to_fs(utf8path);
|
|
||||||
assert(!path_fs.IsNull());
|
assert(!path_fs.IsNull());
|
||||||
|
|
||||||
FileOutputStream fos(path_fs);
|
FileOutputStream fos(path_fs);
|
||||||
@@ -191,12 +188,11 @@ SavePlaylistFile(const PlaylistFileContents &contents, const char *utf8path)
|
|||||||
fos.Commit();
|
fos.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaylistFileContents
|
static PlaylistFileContents
|
||||||
LoadPlaylistFile(const char *utf8path)
|
LoadPlaylistFile(Path path_fs)
|
||||||
try {
|
try {
|
||||||
PlaylistFileContents contents;
|
PlaylistFileContents contents;
|
||||||
|
|
||||||
const auto path_fs = spl_map_to_fs(utf8path);
|
|
||||||
assert(!path_fs.IsNull());
|
assert(!path_fs.IsNull());
|
||||||
|
|
||||||
TextFile file(path_fs);
|
TextFile file(path_fs);
|
||||||
@@ -251,16 +247,54 @@ try {
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
static PlaylistFileContents
|
||||||
spl_move_index(const char *utf8path, unsigned src, unsigned dest)
|
MaybeLoadPlaylistFile(Path path_fs, PlaylistFileEditor::LoadMode load_mode)
|
||||||
|
try {
|
||||||
|
if (load_mode == PlaylistFileEditor::LoadMode::NO)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return LoadPlaylistFile(path_fs);
|
||||||
|
} catch (const PlaylistError &error) {
|
||||||
|
if (error.GetCode() == PlaylistResult::NO_SUCH_LIST &&
|
||||||
|
load_mode == PlaylistFileEditor::LoadMode::TRY)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaylistFileEditor::PlaylistFileEditor(const char *name_utf8,
|
||||||
|
LoadMode load_mode)
|
||||||
|
:path(spl_map_to_fs(name_utf8)),
|
||||||
|
contents(MaybeLoadPlaylistFile(path, load_mode))
|
||||||
{
|
{
|
||||||
if (src == dest)
|
}
|
||||||
/* this doesn't check whether the playlist exists, but
|
|
||||||
what the hell.. */
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto contents = LoadPlaylistFile(utf8path);
|
void
|
||||||
|
PlaylistFileEditor::Insert(std::size_t i, const char *uri)
|
||||||
|
{
|
||||||
|
if (i > size())
|
||||||
|
throw PlaylistError(PlaylistResult::BAD_RANGE, "Bad position");
|
||||||
|
|
||||||
|
if (size() >= playlist_max_length)
|
||||||
|
throw PlaylistError(PlaylistResult::TOO_LARGE,
|
||||||
|
"Stored playlist is too large");
|
||||||
|
|
||||||
|
contents.emplace(std::next(contents.begin(), i), uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PlaylistFileEditor::Insert(std::size_t i, const DetachedSong &song)
|
||||||
|
{
|
||||||
|
const char *uri = playlist_saveAbsolutePaths
|
||||||
|
? song.GetRealURI()
|
||||||
|
: song.GetURI();
|
||||||
|
|
||||||
|
Insert(i, uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PlaylistFileEditor::MoveIndex(unsigned src, unsigned dest)
|
||||||
|
{
|
||||||
if (src >= contents.size() || dest >= contents.size())
|
if (src >= contents.size() || dest >= contents.size())
|
||||||
throw PlaylistError(PlaylistResult::BAD_RANGE, "Bad range");
|
throw PlaylistError(PlaylistResult::BAD_RANGE, "Bad range");
|
||||||
|
|
||||||
@@ -270,9 +304,31 @@ spl_move_index(const char *utf8path, unsigned src, unsigned dest)
|
|||||||
|
|
||||||
const auto dest_i = std::next(contents.begin(), dest);
|
const auto dest_i = std::next(contents.begin(), dest);
|
||||||
contents.insert(dest_i, std::move(value));
|
contents.insert(dest_i, std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
SavePlaylistFile(contents, utf8path);
|
void
|
||||||
|
PlaylistFileEditor::RemoveIndex(unsigned i)
|
||||||
|
{
|
||||||
|
if (i >= contents.size())
|
||||||
|
throw PlaylistError(PlaylistResult::BAD_RANGE, "Bad range");
|
||||||
|
|
||||||
|
contents.erase(std::next(contents.begin(), i));
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PlaylistFileEditor::RemoveRange(RangeArg range)
|
||||||
|
{
|
||||||
|
if (!range.CheckClip(size()))
|
||||||
|
throw PlaylistError::BadRange();
|
||||||
|
|
||||||
|
contents.erase(std::next(contents.begin(), range.start),
|
||||||
|
std::next(contents.begin(), range.end));
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PlaylistFileEditor::Save()
|
||||||
|
{
|
||||||
|
SavePlaylistFile(path, contents);
|
||||||
idle_add(IDLE_STORED_PLAYLIST);
|
idle_add(IDLE_STORED_PLAYLIST);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,20 +370,6 @@ spl_delete(const char *name_utf8)
|
|||||||
idle_add(IDLE_STORED_PLAYLIST);
|
idle_add(IDLE_STORED_PLAYLIST);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
spl_remove_index(const char *utf8path, unsigned pos)
|
|
||||||
{
|
|
||||||
auto contents = LoadPlaylistFile(utf8path);
|
|
||||||
|
|
||||||
if (pos >= contents.size())
|
|
||||||
throw PlaylistError(PlaylistResult::BAD_RANGE, "Bad range");
|
|
||||||
|
|
||||||
contents.erase(std::next(contents.begin(), pos));
|
|
||||||
|
|
||||||
SavePlaylistFile(contents, utf8path);
|
|
||||||
idle_add(IDLE_STORED_PLAYLIST);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
spl_append_song(const char *utf8path, const DetachedSong &song)
|
spl_append_song(const char *utf8path, const DetachedSong &song)
|
||||||
try {
|
try {
|
||||||
|
@@ -20,19 +20,55 @@
|
|||||||
#ifndef MPD_PLAYLIST_FILE_HXX
|
#ifndef MPD_PLAYLIST_FILE_HXX
|
||||||
#define MPD_PLAYLIST_FILE_HXX
|
#define MPD_PLAYLIST_FILE_HXX
|
||||||
|
|
||||||
|
#include "fs/AllocatedPath.hxx"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
struct ConfigData;
|
struct ConfigData;
|
||||||
|
struct RangeArg;
|
||||||
class DetachedSong;
|
class DetachedSong;
|
||||||
class SongLoader;
|
class SongLoader;
|
||||||
class PlaylistVector;
|
class PlaylistVector;
|
||||||
class AllocatedPath;
|
|
||||||
|
|
||||||
typedef std::vector<std::string> PlaylistFileContents;
|
typedef std::vector<std::string> PlaylistFileContents;
|
||||||
|
|
||||||
extern bool playlist_saveAbsolutePaths;
|
extern bool playlist_saveAbsolutePaths;
|
||||||
|
|
||||||
|
class PlaylistFileEditor {
|
||||||
|
const AllocatedPath path;
|
||||||
|
|
||||||
|
PlaylistFileContents contents;
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum class LoadMode {
|
||||||
|
NO,
|
||||||
|
YES,
|
||||||
|
TRY,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws on error.
|
||||||
|
*/
|
||||||
|
explicit PlaylistFileEditor(const char *name_utf8, LoadMode load_mode);
|
||||||
|
|
||||||
|
auto size() const noexcept {
|
||||||
|
return contents.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Insert(std::size_t i, const char *uri);
|
||||||
|
void Insert(std::size_t i, const DetachedSong &song);
|
||||||
|
|
||||||
|
void MoveIndex(unsigned src, unsigned dest);
|
||||||
|
void RemoveIndex(unsigned i);
|
||||||
|
void RemoveRange(RangeArg range);
|
||||||
|
|
||||||
|
void Save();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Load();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform some global initialization, e.g. load configuration values.
|
* Perform some global initialization, e.g. load configuration values.
|
||||||
*/
|
*/
|
||||||
@@ -55,21 +91,12 @@ spl_map_to_fs(const char *name_utf8);
|
|||||||
PlaylistVector
|
PlaylistVector
|
||||||
ListPlaylistFiles();
|
ListPlaylistFiles();
|
||||||
|
|
||||||
PlaylistFileContents
|
|
||||||
LoadPlaylistFile(const char *utf8path);
|
|
||||||
|
|
||||||
void
|
|
||||||
spl_move_index(const char *utf8path, unsigned src, unsigned dest);
|
|
||||||
|
|
||||||
void
|
void
|
||||||
spl_clear(const char *utf8path);
|
spl_clear(const char *utf8path);
|
||||||
|
|
||||||
void
|
void
|
||||||
spl_delete(const char *name_utf8);
|
spl_delete(const char *name_utf8);
|
||||||
|
|
||||||
void
|
|
||||||
spl_remove_index(const char *utf8path, unsigned pos);
|
|
||||||
|
|
||||||
void
|
void
|
||||||
spl_append_song(const char *utf8path, const DetachedSong &song);
|
spl_append_song(const char *utf8path, const DetachedSong &song);
|
||||||
|
|
||||||
|
@@ -85,7 +85,7 @@ handle_not_commands(Client &client, Request request, Response &response);
|
|||||||
* This array must be sorted!
|
* This array must be sorted!
|
||||||
*/
|
*/
|
||||||
static constexpr struct command commands[] = {
|
static constexpr struct command commands[] = {
|
||||||
{ "add", PERMISSION_ADD, 1, 1, handle_add },
|
{ "add", PERMISSION_ADD, 1, 2, handle_add },
|
||||||
{ "addid", PERMISSION_ADD, 1, 2, handle_addid },
|
{ "addid", PERMISSION_ADD, 1, 2, handle_addid },
|
||||||
{ "addtagid", PERMISSION_ADD, 3, 3, handle_addtagid },
|
{ "addtagid", PERMISSION_ADD, 3, 3, handle_addtagid },
|
||||||
{ "albumart", PERMISSION_READ, 2, 2, handle_album_art },
|
{ "albumart", PERMISSION_READ, 2, 2, handle_album_art },
|
||||||
@@ -157,7 +157,7 @@ static constexpr struct command commands[] = {
|
|||||||
{ "play", PERMISSION_PLAYER, 0, 1, handle_play },
|
{ "play", PERMISSION_PLAYER, 0, 1, handle_play },
|
||||||
{ "playid", PERMISSION_PLAYER, 0, 1, handle_playid },
|
{ "playid", PERMISSION_PLAYER, 0, 1, handle_playid },
|
||||||
{ "playlist", PERMISSION_READ, 0, 0, handle_playlist },
|
{ "playlist", PERMISSION_READ, 0, 0, handle_playlist },
|
||||||
{ "playlistadd", PERMISSION_CONTROL, 2, 2, handle_playlistadd },
|
{ "playlistadd", PERMISSION_CONTROL, 2, 3, handle_playlistadd },
|
||||||
{ "playlistclear", PERMISSION_CONTROL, 1, 1, handle_playlistclear },
|
{ "playlistclear", PERMISSION_CONTROL, 1, 1, handle_playlistclear },
|
||||||
{ "playlistdelete", PERMISSION_CONTROL, 2, 2, handle_playlistdelete },
|
{ "playlistdelete", PERMISSION_CONTROL, 2, 2, handle_playlistdelete },
|
||||||
{ "playlistfind", PERMISSION_READ, 1, -1, handle_playlistfind },
|
{ "playlistfind", PERMISSION_READ, 1, -1, handle_playlistfind },
|
||||||
|
@@ -199,13 +199,20 @@ handle_searchaddpl(Client &client, Request args, Response &)
|
|||||||
{
|
{
|
||||||
const char *playlist = args.shift();
|
const char *playlist = args.shift();
|
||||||
|
|
||||||
|
const unsigned position = ParseQueuePosition(args, UINT_MAX);
|
||||||
|
|
||||||
SongFilter filter;
|
SongFilter filter;
|
||||||
const auto selection = ParseDatabaseSelection(args, true, filter);
|
const auto selection = ParseDatabaseSelection(args, true, filter);
|
||||||
|
|
||||||
const Database &db = client.GetDatabaseOrThrow();
|
const Database &db = client.GetDatabaseOrThrow();
|
||||||
|
|
||||||
search_add_to_playlist(db, client.GetStorage(),
|
if (position == UINT_MAX)
|
||||||
playlist, selection);
|
search_add_to_playlist(db, client.GetStorage(),
|
||||||
|
playlist, selection);
|
||||||
|
else
|
||||||
|
SearchInsertIntoPlaylist(db, client.GetStorage(), selection,
|
||||||
|
playlist, position);
|
||||||
|
|
||||||
return CommandResult::OK;
|
return CommandResult::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -224,10 +224,12 @@ GetChromaprintCommand::DecodeFile(std::string_view suffix, InputStream &is,
|
|||||||
inline void
|
inline void
|
||||||
GetChromaprintCommand::DecodeFile()
|
GetChromaprintCommand::DecodeFile()
|
||||||
{
|
{
|
||||||
const auto suffix = uri_get_suffix(uri);
|
const char *_suffix = PathTraitsUTF8::GetFilenameSuffix(uri.c_str());
|
||||||
if (suffix.empty())
|
if (_suffix == nullptr)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const std::string_view suffix{_suffix};
|
||||||
|
|
||||||
InputStreamPtr input_stream;
|
InputStreamPtr input_stream;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@@ -186,7 +186,8 @@ handle_moveoutput(Client &client, Request request, Response &response)
|
|||||||
was_enabled);
|
was_enabled);
|
||||||
else
|
else
|
||||||
/* copy the AudioOutputControl and add it to the output list */
|
/* copy the AudioOutputControl and add it to the output list */
|
||||||
dest_partition.outputs.AddCopy(output,was_enabled);
|
dest_partition.outputs.AddMoveFrom(std::move(*output),
|
||||||
|
was_enabled);
|
||||||
|
|
||||||
instance.EmitIdle(IDLE_OUTPUT);
|
instance.EmitIdle(IDLE_OUTPUT);
|
||||||
return CommandResult::OK;
|
return CommandResult::OK;
|
||||||
|
@@ -22,8 +22,10 @@
|
|||||||
#include "PositionArg.hxx"
|
#include "PositionArg.hxx"
|
||||||
#include "Request.hxx"
|
#include "Request.hxx"
|
||||||
#include "Instance.hxx"
|
#include "Instance.hxx"
|
||||||
|
#include "db/Interface.hxx"
|
||||||
#include "db/Selection.hxx"
|
#include "db/Selection.hxx"
|
||||||
#include "db/DatabasePlaylist.hxx"
|
#include "db/DatabasePlaylist.hxx"
|
||||||
|
#include "db/DatabaseSong.hxx"
|
||||||
#include "PlaylistSave.hxx"
|
#include "PlaylistSave.hxx"
|
||||||
#include "PlaylistFile.hxx"
|
#include "PlaylistFile.hxx"
|
||||||
#include "PlaylistError.hxx"
|
#include "PlaylistError.hxx"
|
||||||
@@ -173,9 +175,11 @@ handle_playlistdelete([[maybe_unused]] Client &client,
|
|||||||
Request args, [[maybe_unused]] Response &r)
|
Request args, [[maybe_unused]] Response &r)
|
||||||
{
|
{
|
||||||
const char *const name = args[0];
|
const char *const name = args[0];
|
||||||
unsigned from = args.ParseUnsigned(1);
|
const auto range = args.ParseRange(1);
|
||||||
|
|
||||||
spl_remove_index(name, from);
|
PlaylistFileEditor editor(name, PlaylistFileEditor::LoadMode::YES);
|
||||||
|
editor.RemoveRange(range);
|
||||||
|
editor.Save();
|
||||||
return CommandResult::OK;
|
return CommandResult::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +191,14 @@ handle_playlistmove([[maybe_unused]] Client &client,
|
|||||||
unsigned from = args.ParseUnsigned(1);
|
unsigned from = args.ParseUnsigned(1);
|
||||||
unsigned to = args.ParseUnsigned(2);
|
unsigned to = args.ParseUnsigned(2);
|
||||||
|
|
||||||
spl_move_index(name, from, to);
|
if (from == to)
|
||||||
|
/* this doesn't check whether the playlist exists, but
|
||||||
|
what the hell.. */
|
||||||
|
return CommandResult::OK;
|
||||||
|
|
||||||
|
PlaylistFileEditor editor(name, PlaylistFileEditor::LoadMode::YES);
|
||||||
|
editor.MoveIndex(from, to);
|
||||||
|
editor.Save();
|
||||||
return CommandResult::OK;
|
return CommandResult::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,12 +212,55 @@ handle_playlistclear([[maybe_unused]] Client &client,
|
|||||||
return CommandResult::OK;
|
return CommandResult::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static CommandResult
|
||||||
|
handle_playlistadd_position(Client &client, const char *playlist_name,
|
||||||
|
const char *uri, unsigned position,
|
||||||
|
Response &r)
|
||||||
|
{
|
||||||
|
PlaylistFileEditor editor{
|
||||||
|
playlist_name,
|
||||||
|
PlaylistFileEditor::LoadMode::TRY,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (position > editor.size()) {
|
||||||
|
r.Error(ACK_ERROR_ARG, "Bad position");
|
||||||
|
return CommandResult::ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri_has_scheme(uri)) {
|
||||||
|
editor.Insert(position, uri);
|
||||||
|
} else {
|
||||||
|
#ifdef ENABLE_DATABASE
|
||||||
|
const DatabaseSelection selection(uri, true, nullptr);
|
||||||
|
|
||||||
|
if (SearchInsertIntoPlaylist(client.GetDatabaseOrThrow(),
|
||||||
|
client.GetStorage(),
|
||||||
|
selection,
|
||||||
|
editor, position) == 0)
|
||||||
|
/* no song was found, don't need to save */
|
||||||
|
return CommandResult::OK;
|
||||||
|
#else
|
||||||
|
(void)client;
|
||||||
|
r.Error(ACK_ERROR_NO_EXIST, "No database");
|
||||||
|
return CommandResult::ERROR;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.Save();
|
||||||
|
|
||||||
|
return CommandResult::OK;
|
||||||
|
}
|
||||||
|
|
||||||
CommandResult
|
CommandResult
|
||||||
handle_playlistadd(Client &client, Request args, [[maybe_unused]] Response &r)
|
handle_playlistadd(Client &client, Request args, [[maybe_unused]] Response &r)
|
||||||
{
|
{
|
||||||
const char *const playlist = args[0];
|
const char *const playlist = args[0];
|
||||||
const char *const uri = args[1];
|
const char *const uri = args[1];
|
||||||
|
|
||||||
|
if (args.size >= 3)
|
||||||
|
return handle_playlistadd_position(client, playlist, uri,
|
||||||
|
args.ParseUnsigned(2), r);
|
||||||
|
|
||||||
if (uri_has_scheme(uri)) {
|
if (uri_has_scheme(uri)) {
|
||||||
const SongLoader loader(client);
|
const SongLoader loader(client);
|
||||||
spl_append_uri(playlist, loader, uri);
|
spl_append_uri(playlist, loader, uri);
|
||||||
|
@@ -52,29 +52,24 @@ AddUri(Client &client, const LocatedUri &uri)
|
|||||||
SongLoader(client).LoadSong(uri));
|
SongLoader(client).LoadSong(uri));
|
||||||
}
|
}
|
||||||
|
|
||||||
static CommandResult
|
|
||||||
AddDatabaseSelection(Client &client, const char *uri,
|
|
||||||
[[maybe_unused]] Response &r)
|
|
||||||
{
|
|
||||||
#ifdef ENABLE_DATABASE
|
#ifdef ENABLE_DATABASE
|
||||||
auto &partition = client.GetPartition();
|
|
||||||
|
static void
|
||||||
|
AddDatabaseSelection(Partition &partition, const char *uri)
|
||||||
|
{
|
||||||
const ScopeBulkEdit bulk_edit(partition);
|
const ScopeBulkEdit bulk_edit(partition);
|
||||||
|
|
||||||
const DatabaseSelection selection(uri, true);
|
const DatabaseSelection selection(uri, true);
|
||||||
AddFromDatabase(partition, selection);
|
AddFromDatabase(partition, selection);
|
||||||
return CommandResult::OK;
|
|
||||||
#else
|
|
||||||
(void)client;
|
|
||||||
(void)uri;
|
|
||||||
|
|
||||||
r.Error(ACK_ERROR_NO_EXIST, "No database");
|
|
||||||
return CommandResult::ERROR;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
CommandResult
|
CommandResult
|
||||||
handle_add(Client &client, Request args, Response &r)
|
handle_add(Client &client, Request args, [[maybe_unused]] Response &r)
|
||||||
{
|
{
|
||||||
|
auto &partition = client.GetPartition();
|
||||||
|
|
||||||
const char *uri = args.front();
|
const char *uri = args.front();
|
||||||
if (StringIsEqual(uri, "/"))
|
if (StringIsEqual(uri, "/"))
|
||||||
/* this URI is malformed, but some clients are buggy
|
/* this URI is malformed, but some clients are buggy
|
||||||
@@ -84,6 +79,11 @@ handle_add(Client &client, Request args, Response &r)
|
|||||||
here */
|
here */
|
||||||
uri = "";
|
uri = "";
|
||||||
|
|
||||||
|
const auto old_size = partition.playlist.GetLength();
|
||||||
|
const unsigned position = args.size > 1
|
||||||
|
? ParseInsertPosition(args[1], partition.playlist)
|
||||||
|
: old_size;
|
||||||
|
|
||||||
const auto located_uri = LocateUri(UriPluginKind::INPUT, uri,
|
const auto located_uri = LocateUri(UriPluginKind::INPUT, uri,
|
||||||
&client
|
&client
|
||||||
#ifdef ENABLE_DATABASE
|
#ifdef ENABLE_DATABASE
|
||||||
@@ -94,18 +94,34 @@ handle_add(Client &client, Request args, Response &r)
|
|||||||
case LocatedUri::Type::ABSOLUTE:
|
case LocatedUri::Type::ABSOLUTE:
|
||||||
AddUri(client, located_uri);
|
AddUri(client, located_uri);
|
||||||
client.GetInstance().LookupRemoteTag(located_uri.canonical_uri);
|
client.GetInstance().LookupRemoteTag(located_uri.canonical_uri);
|
||||||
return CommandResult::OK;
|
break;
|
||||||
|
|
||||||
case LocatedUri::Type::PATH:
|
case LocatedUri::Type::PATH:
|
||||||
AddUri(client, located_uri);
|
AddUri(client, located_uri);
|
||||||
return CommandResult::OK;
|
break;
|
||||||
|
|
||||||
case LocatedUri::Type::RELATIVE:
|
case LocatedUri::Type::RELATIVE:
|
||||||
return AddDatabaseSelection(client, located_uri.canonical_uri,
|
#ifdef ENABLE_DATABASE
|
||||||
r);
|
AddDatabaseSelection(partition, located_uri.canonical_uri);
|
||||||
|
break;
|
||||||
|
#else
|
||||||
|
r.Error(ACK_ERROR_NO_EXIST, "No database");
|
||||||
|
return CommandResult::ERROR;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
gcc_unreachable();
|
if (position < old_size) {
|
||||||
|
const unsigned new_size = partition.playlist.GetLength();
|
||||||
|
const RangeArg move_range{old_size, new_size};
|
||||||
|
|
||||||
|
try {
|
||||||
|
partition.MoveRange(move_range, position);
|
||||||
|
} catch (...) {
|
||||||
|
/* ignore - shall we handle it? */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CommandResult::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
CommandResult
|
CommandResult
|
||||||
|
@@ -23,11 +23,10 @@
|
|||||||
#include "fs/Traits.hxx"
|
#include "fs/Traits.hxx"
|
||||||
#include "fs/StandardDirectory.hxx"
|
#include "fs/StandardDirectory.hxx"
|
||||||
#include "util/RuntimeError.hxx"
|
#include "util/RuntimeError.hxx"
|
||||||
|
#include "util/StringView.hxx"
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
#include <pwd.h>
|
#include <pwd.h>
|
||||||
|
|
||||||
@@ -96,30 +95,18 @@ ParsePath(const char *path)
|
|||||||
if (*path == '\0')
|
if (*path == '\0')
|
||||||
return GetConfiguredHome();
|
return GetConfiguredHome();
|
||||||
|
|
||||||
AllocatedPath home = nullptr;
|
|
||||||
|
|
||||||
if (*path == '/') {
|
if (*path == '/') {
|
||||||
home = GetConfiguredHome();
|
|
||||||
|
|
||||||
++path;
|
++path;
|
||||||
|
|
||||||
|
return GetConfiguredHome() /
|
||||||
|
AllocatedPath::FromUTF8Throw(path);
|
||||||
} else {
|
} else {
|
||||||
const char *slash = std::strchr(path, '/');
|
const auto [user, rest] =
|
||||||
const char *end = slash == nullptr
|
StringView{path}.Split('/');
|
||||||
? path + strlen(path)
|
|
||||||
: slash;
|
|
||||||
const std::string user(path, end);
|
|
||||||
home = GetHome(user.c_str());
|
|
||||||
|
|
||||||
if (slash == nullptr)
|
return GetHome(std::string{user}.c_str())
|
||||||
return home;
|
/ AllocatedPath::FromUTF8Throw(rest);
|
||||||
|
|
||||||
path = slash + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (home.IsNull())
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
return home / AllocatedPath::FromUTF8Throw(path);
|
|
||||||
} else if (!PathTraitsUTF8::IsAbsolute(path)) {
|
} else if (!PathTraitsUTF8::IsAbsolute(path)) {
|
||||||
throw FormatRuntimeError("not an absolute path: %s", path);
|
throw FormatRuntimeError("not an absolute path: %s", path);
|
||||||
} else {
|
} else {
|
||||||
|
@@ -22,6 +22,7 @@
|
|||||||
#include "PlaylistFile.hxx"
|
#include "PlaylistFile.hxx"
|
||||||
#include "Interface.hxx"
|
#include "Interface.hxx"
|
||||||
#include "song/DetachedSong.hxx"
|
#include "song/DetachedSong.hxx"
|
||||||
|
#include "protocol/Ack.hxx"
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
@@ -41,3 +42,42 @@ search_add_to_playlist(const Database &db, const Storage *storage,
|
|||||||
const auto f = [=](auto && arg1) { return AddSong(storage, playlist_path_utf8, arg1); };
|
const auto f = [=](auto && arg1) { return AddSong(storage, playlist_path_utf8, arg1); };
|
||||||
db.Visit(selection, f);
|
db.Visit(selection, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsigned
|
||||||
|
SearchInsertIntoPlaylist(const Database &db, const Storage *storage,
|
||||||
|
const DatabaseSelection &selection,
|
||||||
|
PlaylistFileEditor &playlist,
|
||||||
|
unsigned position)
|
||||||
|
{
|
||||||
|
assert(position <= playlist.size());
|
||||||
|
|
||||||
|
unsigned n = 0;
|
||||||
|
|
||||||
|
db.Visit(selection, [&playlist, &position, &n, storage](const auto &song){
|
||||||
|
playlist.Insert(position + n,
|
||||||
|
DatabaseDetachSong(storage, song));
|
||||||
|
++position;
|
||||||
|
++n;
|
||||||
|
});
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SearchInsertIntoPlaylist(const Database &db, const Storage *storage,
|
||||||
|
const DatabaseSelection &selection,
|
||||||
|
const char *playlist_name,
|
||||||
|
unsigned position)
|
||||||
|
{
|
||||||
|
PlaylistFileEditor editor{
|
||||||
|
playlist_name,
|
||||||
|
PlaylistFileEditor::LoadMode::TRY,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (position > editor.size())
|
||||||
|
throw ProtocolError{ACK_ERROR_ARG, "Bad position"};
|
||||||
|
|
||||||
|
if (SearchInsertIntoPlaylist(db, storage, selection,
|
||||||
|
editor, position) > 0)
|
||||||
|
editor.Save();
|
||||||
|
}
|
||||||
|
@@ -25,6 +25,7 @@
|
|||||||
class Database;
|
class Database;
|
||||||
class Storage;
|
class Storage;
|
||||||
struct DatabaseSelection;
|
struct DatabaseSelection;
|
||||||
|
class PlaylistFileEditor;
|
||||||
|
|
||||||
gcc_nonnull(3)
|
gcc_nonnull(3)
|
||||||
void
|
void
|
||||||
@@ -32,4 +33,19 @@ search_add_to_playlist(const Database &db, const Storage *storage,
|
|||||||
const char *playlist_path_utf8,
|
const char *playlist_path_utf8,
|
||||||
const DatabaseSelection &selection);
|
const DatabaseSelection &selection);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the number of songs added
|
||||||
|
*/
|
||||||
|
unsigned
|
||||||
|
SearchInsertIntoPlaylist(const Database &db, const Storage *storage,
|
||||||
|
const DatabaseSelection &selection,
|
||||||
|
PlaylistFileEditor &playlist,
|
||||||
|
unsigned position);
|
||||||
|
|
||||||
|
void
|
||||||
|
SearchInsertIntoPlaylist(const Database &db, const Storage *storage,
|
||||||
|
const DatabaseSelection &selection,
|
||||||
|
const char *playlist_name,
|
||||||
|
unsigned position);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@@ -39,6 +39,7 @@
|
|||||||
#include "util/ConstBuffer.hxx"
|
#include "util/ConstBuffer.hxx"
|
||||||
#include "util/RecursiveMap.hxx"
|
#include "util/RecursiveMap.hxx"
|
||||||
#include "util/SplitString.hxx"
|
#include "util/SplitString.hxx"
|
||||||
|
#include "config/Block.hxx"
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -76,10 +77,13 @@ class UpnpDatabase : public Database {
|
|||||||
UpnpClient_Handle handle;
|
UpnpClient_Handle handle;
|
||||||
UPnPDeviceDirectory *discovery;
|
UPnPDeviceDirectory *discovery;
|
||||||
|
|
||||||
|
const char* interface;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit UpnpDatabase(EventLoop &_event_loop) noexcept
|
explicit UpnpDatabase(EventLoop &_event_loop, const ConfigBlock &block) noexcept
|
||||||
:Database(upnp_db_plugin),
|
:Database(upnp_db_plugin),
|
||||||
event_loop(_event_loop) {}
|
event_loop(_event_loop),
|
||||||
|
interface(block.GetBlockValue("interface", nullptr)) {}
|
||||||
|
|
||||||
static DatabasePtr Create(EventLoop &main_event_loop,
|
static DatabasePtr Create(EventLoop &main_event_loop,
|
||||||
EventLoop &io_event_loop,
|
EventLoop &io_event_loop,
|
||||||
@@ -147,15 +151,15 @@ private:
|
|||||||
DatabasePtr
|
DatabasePtr
|
||||||
UpnpDatabase::Create(EventLoop &, EventLoop &io_event_loop,
|
UpnpDatabase::Create(EventLoop &, EventLoop &io_event_loop,
|
||||||
[[maybe_unused]] DatabaseListener &listener,
|
[[maybe_unused]] DatabaseListener &listener,
|
||||||
const ConfigBlock &) noexcept
|
const ConfigBlock &block) noexcept
|
||||||
{
|
{
|
||||||
return std::make_unique<UpnpDatabase>(io_event_loop);
|
return std::make_unique<UpnpDatabase>(io_event_loop, block);;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
UpnpDatabase::Open()
|
UpnpDatabase::Open()
|
||||||
{
|
{
|
||||||
handle = UpnpClientGlobalInit();
|
handle = UpnpClientGlobalInit(interface);
|
||||||
|
|
||||||
discovery = new UPnPDeviceDirectory(event_loop, handle);
|
discovery = new UPnPDeviceDirectory(event_loop, handle);
|
||||||
try {
|
try {
|
||||||
|
@@ -282,7 +282,7 @@ InotifyUpdate::InotifyCallback(int wd, unsigned mask,
|
|||||||
(mask & IN_ISDIR) != 0) {
|
(mask & IN_ISDIR) != 0) {
|
||||||
/* a sub directory was changed: register those in
|
/* a sub directory was changed: register those in
|
||||||
inotify */
|
inotify */
|
||||||
const Path root_path = root->name;
|
const auto root_path = root->name;
|
||||||
|
|
||||||
const auto path_fs = uri_fs.IsNull()
|
const auto path_fs = uri_fs.IsNull()
|
||||||
? root_path
|
? root_path
|
||||||
|
@@ -188,8 +188,8 @@ UpdateWalk::UpdateRegularFile(Directory &directory,
|
|||||||
const char *name,
|
const char *name,
|
||||||
const StorageFileInfo &info) noexcept
|
const StorageFileInfo &info) noexcept
|
||||||
{
|
{
|
||||||
const auto suffix = uri_get_suffix(name);
|
const char *suffix = PathTraitsUTF8::GetFilenameSuffix(name);
|
||||||
if (suffix.empty())
|
if (suffix == nullptr)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return UpdateSongFile(directory, name, suffix, info) ||
|
return UpdateSongFile(directory, name, suffix, info) ||
|
||||||
|
@@ -395,10 +395,12 @@ TryContainerDecoder(DecoderBridge &bridge, Path path_fs,
|
|||||||
static bool
|
static bool
|
||||||
decoder_run_file(DecoderBridge &bridge, const char *uri_utf8, Path path_fs)
|
decoder_run_file(DecoderBridge &bridge, const char *uri_utf8, Path path_fs)
|
||||||
{
|
{
|
||||||
const auto suffix = uri_get_suffix(uri_utf8);
|
const char *_suffix = PathTraitsUTF8::GetFilenameSuffix(uri_utf8);
|
||||||
if (suffix.empty())
|
if (_suffix == nullptr)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
const std::string_view suffix{_suffix};
|
||||||
|
|
||||||
InputStreamPtr input_stream;
|
InputStreamPtr input_stream;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@@ -502,7 +502,7 @@ FfmpegDecode(DecoderClient &client, InputStream *input,
|
|||||||
FmtDebug(ffmpeg_domain, "codec '{}'",
|
FmtDebug(ffmpeg_domain, "codec '{}'",
|
||||||
codec_descriptor->name);
|
codec_descriptor->name);
|
||||||
|
|
||||||
AVCodec *codec = avcodec_find_decoder(codec_params.codec_id);
|
const AVCodec *codec = avcodec_find_decoder(codec_params.codec_id);
|
||||||
|
|
||||||
if (!codec) {
|
if (!codec) {
|
||||||
LogError(ffmpeg_domain, "Unsupported audio codec");
|
LogError(ffmpeg_domain, "Unsupported audio codec");
|
||||||
|
@@ -52,6 +52,13 @@ EventLoop::EventLoop(
|
|||||||
|
|
||||||
EventLoop::~EventLoop() noexcept
|
EventLoop::~EventLoop() noexcept
|
||||||
{
|
{
|
||||||
|
#if defined(HAVE_URING) && !defined(NDEBUG)
|
||||||
|
/* if Run() was never called (maybe because startup failed and
|
||||||
|
an exception is pending), we need to destruct the
|
||||||
|
Uring::Manager here or else the assertions below fail */
|
||||||
|
uring.reset();
|
||||||
|
#endif
|
||||||
|
|
||||||
assert(defer.empty());
|
assert(defer.empty());
|
||||||
assert(idle.empty());
|
assert(idle.empty());
|
||||||
#ifdef HAVE_THREADED_EVENT_LOOP
|
#ifdef HAVE_THREADED_EVENT_LOOP
|
||||||
|
@@ -24,6 +24,7 @@
|
|||||||
#include "StandardDirectory.hxx"
|
#include "StandardDirectory.hxx"
|
||||||
#include "FileSystem.hxx"
|
#include "FileSystem.hxx"
|
||||||
#include "XDG.hxx"
|
#include "XDG.hxx"
|
||||||
|
#include "util/StringView.hxx"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -228,13 +229,12 @@ GetUserConfigDir() noexcept
|
|||||||
return GetStandardDir(CSIDL_LOCAL_APPDATA);
|
return GetStandardDir(CSIDL_LOCAL_APPDATA);
|
||||||
#elif defined(USE_XDG)
|
#elif defined(USE_XDG)
|
||||||
// Check for $XDG_CONFIG_HOME
|
// Check for $XDG_CONFIG_HOME
|
||||||
auto config_home = getenv("XDG_CONFIG_HOME");
|
if (const auto config_home = getenv("XDG_CONFIG_HOME");
|
||||||
if (IsValidPathString(config_home) && IsValidDir(config_home))
|
IsValidPathString(config_home) && IsValidDir(config_home))
|
||||||
return AllocatedPath::FromFS(config_home);
|
return AllocatedPath::FromFS(config_home);
|
||||||
|
|
||||||
// Check for $HOME/.config
|
// Check for $HOME/.config
|
||||||
auto home = GetHomeDir();
|
if (const auto home = GetHomeDir(); !home.IsNull()) {
|
||||||
if (!home.IsNull()) {
|
|
||||||
auto fallback = home / Path::FromFS(".config");
|
auto fallback = home / Path::FromFS(".config");
|
||||||
if (IsValidDir(fallback.c_str()))
|
if (IsValidDir(fallback.c_str()))
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -265,17 +265,15 @@ GetUserCacheDir() noexcept
|
|||||||
{
|
{
|
||||||
#ifdef USE_XDG
|
#ifdef USE_XDG
|
||||||
// Check for $XDG_CACHE_HOME
|
// Check for $XDG_CACHE_HOME
|
||||||
auto cache_home = getenv("XDG_CACHE_HOME");
|
if (const auto cache_home = getenv("XDG_CACHE_HOME");
|
||||||
if (IsValidPathString(cache_home) && IsValidDir(cache_home))
|
IsValidPathString(cache_home) && IsValidDir(cache_home))
|
||||||
return AllocatedPath::FromFS(cache_home);
|
return AllocatedPath::FromFS(cache_home);
|
||||||
|
|
||||||
// Check for $HOME/.cache
|
// Check for $HOME/.cache
|
||||||
auto home = GetHomeDir();
|
if (const auto home = GetHomeDir(); !home.IsNull())
|
||||||
if (!home.IsNull()) {
|
if (auto fallback = home / Path::FromFS(".cache");
|
||||||
auto fallback = home / Path::FromFS(".cache");
|
IsValidDir(fallback.c_str()))
|
||||||
if (IsValidDir(fallback.c_str()))
|
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
#elif defined(ANDROID)
|
#elif defined(ANDROID)
|
||||||
@@ -285,6 +283,38 @@ GetUserCacheDir() noexcept
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AllocatedPath
|
||||||
|
GetUserRuntimeDir() noexcept
|
||||||
|
{
|
||||||
|
#ifdef USE_XDG
|
||||||
|
return SafePathFromFS(getenv("XDG_RUNTIME_DIR"));
|
||||||
|
#else
|
||||||
|
return nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
AllocatedPath
|
||||||
|
GetAppRuntimeDir() noexcept
|
||||||
|
{
|
||||||
|
#ifdef __linux__
|
||||||
|
/* systemd specific; see systemd.exec(5) */
|
||||||
|
if (const char *runtime_directory = getenv("RUNTIME_DIRECTORY"))
|
||||||
|
if (auto dir = StringView{runtime_directory}.Split(':').first;
|
||||||
|
!dir.empty())
|
||||||
|
return AllocatedPath::FromFS(dir);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_XDG
|
||||||
|
if (const auto user_dir = GetUserRuntimeDir(); !user_dir.IsNull()) {
|
||||||
|
auto dir = user_dir / Path::FromFS("mpd");
|
||||||
|
mkdir(dir.c_str(), 0700);
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
||||||
AllocatedPath
|
AllocatedPath
|
||||||
@@ -317,11 +347,11 @@ AllocatedPath
|
|||||||
GetHomeDir() noexcept
|
GetHomeDir() noexcept
|
||||||
{
|
{
|
||||||
#ifndef ANDROID
|
#ifndef ANDROID
|
||||||
auto home = getenv("HOME");
|
if (const auto home = getenv("HOME");
|
||||||
if (IsValidPathString(home) && IsValidDir(home))
|
IsValidPathString(home) && IsValidDir(home))
|
||||||
return AllocatedPath::FromFS(home);
|
return AllocatedPath::FromFS(home);
|
||||||
PasswdEntry pw;
|
|
||||||
if (pw.ReadByUid(getuid()))
|
if (PasswdEntry pw; pw.ReadByUid(getuid()))
|
||||||
return SafePathFromFS(pw->pw_dir);
|
return SafePathFromFS(pw->pw_dir);
|
||||||
#endif
|
#endif
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -334,8 +364,8 @@ GetHomeDir(const char *user_name) noexcept
|
|||||||
(void)user_name;
|
(void)user_name;
|
||||||
#else
|
#else
|
||||||
assert(user_name != nullptr);
|
assert(user_name != nullptr);
|
||||||
PasswdEntry pw;
|
|
||||||
if (pw.ReadByName(user_name))
|
if (PasswdEntry pw; pw.ReadByName(user_name))
|
||||||
return SafePathFromFS(pw->pw_dir);
|
return SafePathFromFS(pw->pw_dir);
|
||||||
#endif
|
#endif
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
@@ -25,27 +25,44 @@
|
|||||||
/**
|
/**
|
||||||
* Obtains configuration directory for the current user.
|
* Obtains configuration directory for the current user.
|
||||||
*/
|
*/
|
||||||
|
[[gnu::const]]
|
||||||
AllocatedPath
|
AllocatedPath
|
||||||
GetUserConfigDir() noexcept;
|
GetUserConfigDir() noexcept;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtains music directory for the current user.
|
* Obtains music directory for the current user.
|
||||||
*/
|
*/
|
||||||
|
[[gnu::const]]
|
||||||
AllocatedPath
|
AllocatedPath
|
||||||
GetUserMusicDir() noexcept;
|
GetUserMusicDir() noexcept;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtains cache directory for the current user.
|
* Obtains cache directory for the current user.
|
||||||
*/
|
*/
|
||||||
[[gnu::pure]]
|
[[gnu::const]]
|
||||||
AllocatedPath
|
AllocatedPath
|
||||||
GetUserCacheDir() noexcept;
|
GetUserCacheDir() noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the runtime directory for the current user.
|
||||||
|
*/
|
||||||
|
[[gnu::const]]
|
||||||
|
AllocatedPath
|
||||||
|
GetUserRuntimeDir() noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the runtime directory for this application.
|
||||||
|
*/
|
||||||
|
[[gnu::const]]
|
||||||
|
AllocatedPath
|
||||||
|
GetAppRuntimeDir() noexcept;
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtains system configuration directory.
|
* Obtains system configuration directory.
|
||||||
*/
|
*/
|
||||||
|
[[gnu::const]]
|
||||||
AllocatedPath
|
AllocatedPath
|
||||||
GetSystemConfigDir() noexcept;
|
GetSystemConfigDir() noexcept;
|
||||||
|
|
||||||
@@ -54,6 +71,7 @@ GetSystemConfigDir() noexcept;
|
|||||||
* Application base directory is a directory that contains 'bin' folder
|
* Application base directory is a directory that contains 'bin' folder
|
||||||
* for current executable.
|
* for current executable.
|
||||||
*/
|
*/
|
||||||
|
[[gnu::const]]
|
||||||
AllocatedPath
|
AllocatedPath
|
||||||
GetAppBaseDir() noexcept;
|
GetAppBaseDir() noexcept;
|
||||||
|
|
||||||
@@ -62,12 +80,14 @@ GetAppBaseDir() noexcept;
|
|||||||
/**
|
/**
|
||||||
* Obtains home directory for the current user.
|
* Obtains home directory for the current user.
|
||||||
*/
|
*/
|
||||||
|
[[gnu::const]]
|
||||||
AllocatedPath
|
AllocatedPath
|
||||||
GetHomeDir() noexcept;
|
GetHomeDir() noexcept;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtains home directory for the specified user.
|
* Obtains home directory for the specified user.
|
||||||
*/
|
*/
|
||||||
|
[[gnu::pure]]
|
||||||
AllocatedPath
|
AllocatedPath
|
||||||
GetHomeDir(const char *user_name) noexcept;
|
GetHomeDir(const char *user_name) noexcept;
|
||||||
|
|
||||||
|
@@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
#include "AlsaInputPlugin.hxx"
|
#include "AlsaInputPlugin.hxx"
|
||||||
#include "lib/alsa/NonBlock.hxx"
|
#include "lib/alsa/NonBlock.hxx"
|
||||||
|
#include "lib/alsa/Error.hxx"
|
||||||
#include "lib/alsa/Format.hxx"
|
#include "lib/alsa/Format.hxx"
|
||||||
#include "../InputPlugin.hxx"
|
#include "../InputPlugin.hxx"
|
||||||
#include "../AsyncInputStream.hxx"
|
#include "../AsyncInputStream.hxx"
|
||||||
@@ -332,28 +333,23 @@ AlsaInputStream::ConfigureCapture(AudioFormat audio_format)
|
|||||||
snd_pcm_hw_params_alloca(&hw_params);
|
snd_pcm_hw_params_alloca(&hw_params);
|
||||||
|
|
||||||
if ((err = snd_pcm_hw_params_any(capture_handle, hw_params)) < 0)
|
if ((err = snd_pcm_hw_params_any(capture_handle, hw_params)) < 0)
|
||||||
throw FormatRuntimeError("Cannot initialize hardware parameter structure (%s)",
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_any() failed");
|
||||||
snd_strerror(err));
|
|
||||||
|
|
||||||
if ((err = snd_pcm_hw_params_set_access(capture_handle, hw_params,
|
if ((err = snd_pcm_hw_params_set_access(capture_handle, hw_params,
|
||||||
SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
|
SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
|
||||||
throw FormatRuntimeError("Cannot set access type (%s)",
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_access() failed");
|
||||||
snd_strerror(err));
|
|
||||||
|
|
||||||
if ((err = snd_pcm_hw_params_set_format(capture_handle, hw_params,
|
if ((err = snd_pcm_hw_params_set_format(capture_handle, hw_params,
|
||||||
ToAlsaPcmFormat(audio_format.format))) < 0)
|
ToAlsaPcmFormat(audio_format.format))) < 0)
|
||||||
throw FormatRuntimeError("Cannot set sample format (%s)",
|
throw Alsa::MakeError(err, "Cannot set sample format");
|
||||||
snd_strerror(err));
|
|
||||||
|
|
||||||
if ((err = snd_pcm_hw_params_set_channels(capture_handle,
|
if ((err = snd_pcm_hw_params_set_channels(capture_handle,
|
||||||
hw_params, audio_format.channels)) < 0)
|
hw_params, audio_format.channels)) < 0)
|
||||||
throw FormatRuntimeError("Cannot set channels (%s)",
|
throw Alsa::MakeError(err, "Cannot set channels");
|
||||||
snd_strerror(err));
|
|
||||||
|
|
||||||
if ((err = snd_pcm_hw_params_set_rate(capture_handle,
|
if ((err = snd_pcm_hw_params_set_rate(capture_handle,
|
||||||
hw_params, audio_format.sample_rate, 0)) < 0)
|
hw_params, audio_format.sample_rate, 0)) < 0)
|
||||||
throw FormatRuntimeError("Cannot set sample rate (%s)",
|
throw Alsa::MakeError(err, "Cannot set sample rate");
|
||||||
snd_strerror(err));
|
|
||||||
|
|
||||||
snd_pcm_uframes_t buffer_size_min, buffer_size_max;
|
snd_pcm_uframes_t buffer_size_min, buffer_size_max;
|
||||||
snd_pcm_hw_params_get_buffer_size_min(hw_params, &buffer_size_min);
|
snd_pcm_hw_params_get_buffer_size_min(hw_params, &buffer_size_min);
|
||||||
@@ -388,26 +384,22 @@ AlsaInputStream::ConfigureCapture(AudioFormat audio_format)
|
|||||||
int direction = -1;
|
int direction = -1;
|
||||||
if ((err = snd_pcm_hw_params_set_period_size_near(capture_handle,
|
if ((err = snd_pcm_hw_params_set_period_size_near(capture_handle,
|
||||||
hw_params, &period_size, &direction)) < 0)
|
hw_params, &period_size, &direction)) < 0)
|
||||||
throw FormatRuntimeError("Cannot set period size (%s)",
|
throw Alsa::MakeError(err, "Cannot set period size");
|
||||||
snd_strerror(err));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((err = snd_pcm_hw_params(capture_handle, hw_params)) < 0)
|
if ((err = snd_pcm_hw_params(capture_handle, hw_params)) < 0)
|
||||||
throw FormatRuntimeError("Cannot set parameters (%s)",
|
throw Alsa::MakeError(err, "snd_pcm_hw_params() failed");
|
||||||
snd_strerror(err));
|
|
||||||
|
|
||||||
snd_pcm_uframes_t alsa_buffer_size;
|
snd_pcm_uframes_t alsa_buffer_size;
|
||||||
err = snd_pcm_hw_params_get_buffer_size(hw_params, &alsa_buffer_size);
|
err = snd_pcm_hw_params_get_buffer_size(hw_params, &alsa_buffer_size);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_hw_params_get_buffer_size() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_get_buffer_size() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
|
|
||||||
snd_pcm_uframes_t alsa_period_size;
|
snd_pcm_uframes_t alsa_period_size;
|
||||||
err = snd_pcm_hw_params_get_period_size(hw_params, &alsa_period_size,
|
err = snd_pcm_hw_params_get_period_size(hw_params, &alsa_period_size,
|
||||||
nullptr);
|
nullptr);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_hw_params_get_period_size() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_get_period_size() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
|
|
||||||
FmtDebug(alsa_input_domain, "buffer_size={} period_size={}",
|
FmtDebug(alsa_input_domain, "buffer_size={} period_size={}",
|
||||||
alsa_buffer_size, alsa_period_size);
|
alsa_buffer_size, alsa_period_size);
|
||||||
@@ -418,8 +410,7 @@ AlsaInputStream::ConfigureCapture(AudioFormat audio_format)
|
|||||||
snd_pcm_sw_params_current(capture_handle, sw_params);
|
snd_pcm_sw_params_current(capture_handle, sw_params);
|
||||||
|
|
||||||
if ((err = snd_pcm_sw_params(capture_handle, sw_params)) < 0)
|
if ((err = snd_pcm_sw_params(capture_handle, sw_params)) < 0)
|
||||||
throw FormatRuntimeError("unable to install sw params (%s)",
|
throw Alsa::MakeError(err, "snd_pcm_sw_params() failed");
|
||||||
snd_strerror(err));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void
|
inline void
|
||||||
@@ -430,8 +421,9 @@ AlsaInputStream::OpenDevice(const SourceSpec &spec)
|
|||||||
if ((err = snd_pcm_open(&capture_handle, spec.GetDeviceName(),
|
if ((err = snd_pcm_open(&capture_handle, spec.GetDeviceName(),
|
||||||
SND_PCM_STREAM_CAPTURE,
|
SND_PCM_STREAM_CAPTURE,
|
||||||
SND_PCM_NONBLOCK | global_config.mode)) < 0)
|
SND_PCM_NONBLOCK | global_config.mode)) < 0)
|
||||||
throw FormatRuntimeError("Failed to open device: %s (%s)",
|
throw Alsa::MakeError(err,
|
||||||
spec.GetDeviceName(), snd_strerror(err));
|
fmt::format("Failed to open device {}",
|
||||||
|
spec.GetDeviceName()).c_str());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ConfigureCapture(spec.GetAudioFormat());
|
ConfigureCapture(spec.GetAudioFormat());
|
||||||
|
@@ -30,12 +30,7 @@ namespace Alsa {
|
|||||||
AllowedFormat::AllowedFormat(StringView s)
|
AllowedFormat::AllowedFormat(StringView s)
|
||||||
{
|
{
|
||||||
#ifdef ENABLE_DSD
|
#ifdef ENABLE_DSD
|
||||||
const StringView dop_tail("=dop");
|
dop = s.RemoveSuffix("=dop");
|
||||||
if (s.EndsWith(dop_tail)) {
|
|
||||||
dop = true;
|
|
||||||
s.size -= dop_tail.size;
|
|
||||||
} else
|
|
||||||
dop = false;
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
char buffer[64];
|
char buffer[64];
|
||||||
@@ -54,7 +49,7 @@ AllowedFormat::AllowedFormat(StringView s)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::forward_list<AllowedFormat>
|
std::forward_list<AllowedFormat>
|
||||||
AllowedFormat::ParseList(StringView s)
|
AllowedFormat::ParseList(std::string_view s)
|
||||||
{
|
{
|
||||||
std::forward_list<AllowedFormat> list;
|
std::forward_list<AllowedFormat> list;
|
||||||
auto tail = list.before_begin();
|
auto tail = list.before_begin();
|
||||||
|
@@ -52,7 +52,7 @@ struct AllowedFormat {
|
|||||||
*
|
*
|
||||||
* Throws std::runtime_error on error.
|
* Throws std::runtime_error on error.
|
||||||
*/
|
*/
|
||||||
static std::forward_list<AllowedFormat> ParseList(StringView s);
|
static std::forward_list<AllowedFormat> ParseList(std::string_view s);
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string
|
std::string
|
||||||
|
44
src/lib/alsa/Error.cxx
Normal file
44
src/lib/alsa/Error.cxx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Max Kellermann <max.kellermann@gmail.com>
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions
|
||||||
|
* are met:
|
||||||
|
*
|
||||||
|
* - Redistributions of source code must retain the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* - Redistributions in binary form must reproduce the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the
|
||||||
|
* distribution.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||||
|
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
||||||
|
* OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Error.hxx"
|
||||||
|
|
||||||
|
#include <alsa/error.h>
|
||||||
|
|
||||||
|
namespace Alsa {
|
||||||
|
|
||||||
|
ErrorCategory error_category;
|
||||||
|
|
||||||
|
std::string
|
||||||
|
ErrorCategory::message(int condition) const
|
||||||
|
{
|
||||||
|
return snd_strerror(condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Avahi
|
53
src/lib/alsa/Error.hxx
Normal file
53
src/lib/alsa/Error.hxx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Max Kellermann <max.kellermann@gmail.com>
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions
|
||||||
|
* are met:
|
||||||
|
*
|
||||||
|
* - Redistributions of source code must retain the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* - Redistributions in binary form must reproduce the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the
|
||||||
|
* distribution.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||||
|
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
||||||
|
* OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
namespace Alsa {
|
||||||
|
|
||||||
|
class ErrorCategory final : public std::error_category {
|
||||||
|
public:
|
||||||
|
const char *name() const noexcept override {
|
||||||
|
return "libasound";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string message(int condition) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern ErrorCategory error_category;
|
||||||
|
|
||||||
|
inline std::system_error
|
||||||
|
MakeError(int error, const char *msg) noexcept
|
||||||
|
{
|
||||||
|
return std::system_error(error, error_category, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Avahi
|
@@ -18,7 +18,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "HwSetup.hxx"
|
#include "HwSetup.hxx"
|
||||||
|
#include "Error.hxx"
|
||||||
#include "Format.hxx"
|
#include "Format.hxx"
|
||||||
|
#include "lib/fmt/AudioFormatFormatter.hxx"
|
||||||
#include "util/ByteOrder.hxx"
|
#include "util/ByteOrder.hxx"
|
||||||
#include "util/Domain.hxx"
|
#include "util/Domain.hxx"
|
||||||
#include "util/RuntimeError.hxx"
|
#include "util/RuntimeError.hxx"
|
||||||
@@ -185,29 +187,27 @@ SetupHw(snd_pcm_t *pcm,
|
|||||||
/* configure HW params */
|
/* configure HW params */
|
||||||
err = snd_pcm_hw_params_any(pcm, hwparams);
|
err = snd_pcm_hw_params_any(pcm, hwparams);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_hw_params_any() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_any() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
|
|
||||||
err = snd_pcm_hw_params_set_access(pcm, hwparams,
|
err = snd_pcm_hw_params_set_access(pcm, hwparams,
|
||||||
SND_PCM_ACCESS_RW_INTERLEAVED);
|
SND_PCM_ACCESS_RW_INTERLEAVED);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_hw_params_set_access() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_access() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
|
|
||||||
err = SetupSampleFormat(pcm, hwparams,
|
err = SetupSampleFormat(pcm, hwparams,
|
||||||
audio_format.format, params);
|
audio_format.format, params);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("Failed to configure format %s: %s",
|
throw Alsa::MakeError(err,
|
||||||
sample_format_to_string(audio_format.format),
|
fmt::format("Failed to configure format {}",
|
||||||
snd_strerror(-err));
|
audio_format.format).c_str());
|
||||||
|
|
||||||
unsigned int channels = audio_format.channels;
|
unsigned int channels = audio_format.channels;
|
||||||
err = snd_pcm_hw_params_set_channels_near(pcm, hwparams,
|
err = snd_pcm_hw_params_set_channels_near(pcm, hwparams,
|
||||||
&channels);
|
&channels);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("Failed to configure %i channels: %s",
|
throw Alsa::MakeError(err,
|
||||||
(int)audio_format.channels,
|
fmt::format("Failed to configure {} channels",
|
||||||
snd_strerror(-err));
|
audio_format.channels).c_str());
|
||||||
|
|
||||||
audio_format.channels = (int8_t)channels;
|
audio_format.channels = (int8_t)channels;
|
||||||
|
|
||||||
@@ -218,9 +218,9 @@ SetupHw(snd_pcm_t *pcm,
|
|||||||
err = snd_pcm_hw_params_set_rate_near(pcm, hwparams,
|
err = snd_pcm_hw_params_set_rate_near(pcm, hwparams,
|
||||||
&output_sample_rate, nullptr);
|
&output_sample_rate, nullptr);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("Failed to configure sample rate %u Hz: %s",
|
throw Alsa::MakeError(err,
|
||||||
requested_sample_rate,
|
fmt::format("Failed to configure sample rate {} Hz",
|
||||||
snd_strerror(-err));
|
requested_sample_rate).c_str());
|
||||||
|
|
||||||
if (output_sample_rate == 0)
|
if (output_sample_rate == 0)
|
||||||
throw FormatRuntimeError("Failed to configure sample rate %u Hz",
|
throw FormatRuntimeError("Failed to configure sample rate %u Hz",
|
||||||
@@ -253,8 +253,7 @@ SetupHw(snd_pcm_t *pcm,
|
|||||||
err = snd_pcm_hw_params_set_buffer_time_near(pcm, hwparams,
|
err = snd_pcm_hw_params_set_buffer_time_near(pcm, hwparams,
|
||||||
&buffer_time, nullptr);
|
&buffer_time, nullptr);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_hw_params_set_buffer_time_near() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_buffer_time_near() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
} else {
|
} else {
|
||||||
err = snd_pcm_hw_params_get_buffer_time(hwparams, &buffer_time,
|
err = snd_pcm_hw_params_get_buffer_time(hwparams, &buffer_time,
|
||||||
nullptr);
|
nullptr);
|
||||||
@@ -275,32 +274,27 @@ SetupHw(snd_pcm_t *pcm,
|
|||||||
err = snd_pcm_hw_params_set_period_time_near(pcm, hwparams,
|
err = snd_pcm_hw_params_set_period_time_near(pcm, hwparams,
|
||||||
&period_time, nullptr);
|
&period_time, nullptr);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_hw_params_set_period_time_near() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_period_time_near() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = snd_pcm_hw_params(pcm, hwparams);
|
err = snd_pcm_hw_params(pcm, hwparams);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_hw_params() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_hw_params() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
|
|
||||||
HwResult result;
|
HwResult result;
|
||||||
|
|
||||||
err = snd_pcm_hw_params_get_format(hwparams, &result.format);
|
err = snd_pcm_hw_params_get_format(hwparams, &result.format);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_hw_params_get_format() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_get_format() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
|
|
||||||
err = snd_pcm_hw_params_get_buffer_size(hwparams, &result.buffer_size);
|
err = snd_pcm_hw_params_get_buffer_size(hwparams, &result.buffer_size);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_hw_params_get_buffer_size() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_get_buffer_size() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
|
|
||||||
err = snd_pcm_hw_params_get_period_size(hwparams, &result.period_size,
|
err = snd_pcm_hw_params_get_period_size(hwparams, &result.period_size,
|
||||||
nullptr);
|
nullptr);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_hw_params_get_period_size() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_get_period_size() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "NonBlock.hxx"
|
#include "NonBlock.hxx"
|
||||||
|
#include "Error.hxx"
|
||||||
#include "event/MultiSocketMonitor.hxx"
|
#include "event/MultiSocketMonitor.hxx"
|
||||||
#include "util/RuntimeError.hxx"
|
#include "util/RuntimeError.hxx"
|
||||||
|
|
||||||
@@ -29,8 +30,7 @@ AlsaNonBlockPcm::PrepareSockets(MultiSocketMonitor &m, snd_pcm_t *pcm)
|
|||||||
if (count == 0)
|
if (count == 0)
|
||||||
throw std::runtime_error("snd_pcm_poll_descriptors_count() failed");
|
throw std::runtime_error("snd_pcm_poll_descriptors_count() failed");
|
||||||
else
|
else
|
||||||
throw FormatRuntimeError("snd_pcm_poll_descriptors_count() failed: %s",
|
throw Alsa::MakeError(count, "snd_pcm_poll_descriptors_count() failed");
|
||||||
snd_strerror(-count));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct pollfd *pfds = pfd_buffer.Get(count);
|
struct pollfd *pfds = pfd_buffer.Get(count);
|
||||||
@@ -40,8 +40,7 @@ AlsaNonBlockPcm::PrepareSockets(MultiSocketMonitor &m, snd_pcm_t *pcm)
|
|||||||
if (count == 0)
|
if (count == 0)
|
||||||
throw std::runtime_error("snd_pcm_poll_descriptors() failed");
|
throw std::runtime_error("snd_pcm_poll_descriptors() failed");
|
||||||
else
|
else
|
||||||
throw FormatRuntimeError("snd_pcm_poll_descriptors() failed: %s",
|
throw Alsa::MakeError(count, "snd_pcm_poll_descriptors() failed");
|
||||||
snd_strerror(-count));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.ReplaceSocketList(pfds, count);
|
m.ReplaceSocketList(pfds, count);
|
||||||
@@ -71,8 +70,7 @@ AlsaNonBlockPcm::DispatchSockets(MultiSocketMonitor &m,
|
|||||||
unsigned short dummy;
|
unsigned short dummy;
|
||||||
int err = snd_pcm_poll_descriptors_revents(pcm, pfds, i - pfds, &dummy);
|
int err = snd_pcm_poll_descriptors_revents(pcm, pfds, i - pfds, &dummy);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_poll_descriptors_revents() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_poll_descriptors_revents() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Event::Duration
|
Event::Duration
|
||||||
|
@@ -14,6 +14,7 @@ conf.set('ENABLE_ALSA', true)
|
|||||||
alsa = static_library(
|
alsa = static_library(
|
||||||
'alsa',
|
'alsa',
|
||||||
'Version.cxx',
|
'Version.cxx',
|
||||||
|
'Error.cxx',
|
||||||
'AllowedFormat.cxx',
|
'AllowedFormat.cxx',
|
||||||
'HwSetup.cxx',
|
'HwSetup.cxx',
|
||||||
'NonBlock.cxx',
|
'NonBlock.cxx',
|
||||||
|
@@ -36,7 +36,7 @@ class CodecContext {
|
|||||||
public:
|
public:
|
||||||
CodecContext() = default;
|
CodecContext() = default;
|
||||||
|
|
||||||
explicit CodecContext(AVCodec &codec)
|
explicit CodecContext(const AVCodec &codec)
|
||||||
:codec_context(avcodec_alloc_context3(&codec))
|
:codec_context(avcodec_alloc_context3(&codec))
|
||||||
{
|
{
|
||||||
if (codec_context == nullptr)
|
if (codec_context == nullptr)
|
||||||
|
@@ -35,6 +35,16 @@
|
|||||||
|
|
||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct fmt::formatter<SampleFormat> : formatter<string_view>
|
||||||
|
{
|
||||||
|
template<typename FormatContext>
|
||||||
|
auto format(const SampleFormat format, FormatContext &ctx) {
|
||||||
|
return formatter<string_view>::format(sample_format_to_string(format),
|
||||||
|
ctx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
struct fmt::formatter<AudioFormat> : formatter<string_view>
|
struct fmt::formatter<AudioFormat> : formatter<string_view>
|
||||||
{
|
{
|
||||||
|
@@ -57,9 +57,9 @@ DoInit()
|
|||||||
}
|
}
|
||||||
|
|
||||||
UpnpClient_Handle
|
UpnpClient_Handle
|
||||||
UpnpClientGlobalInit()
|
UpnpClientGlobalInit(const char* iface)
|
||||||
{
|
{
|
||||||
UpnpGlobalInit();
|
UpnpGlobalInit(iface);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const std::lock_guard<Mutex> protect(upnp_client_init_mutex);
|
const std::lock_guard<Mutex> protect(upnp_client_init_mutex);
|
||||||
|
@@ -23,7 +23,7 @@
|
|||||||
#include "Compat.hxx"
|
#include "Compat.hxx"
|
||||||
|
|
||||||
UpnpClient_Handle
|
UpnpClient_Handle
|
||||||
UpnpClientGlobalInit();
|
UpnpClientGlobalInit(const char* iface);
|
||||||
|
|
||||||
void
|
void
|
||||||
UpnpClientGlobalFinish() noexcept;
|
UpnpClientGlobalFinish() noexcept;
|
||||||
|
@@ -33,12 +33,13 @@ static Mutex upnp_init_mutex;
|
|||||||
static unsigned upnp_ref;
|
static unsigned upnp_ref;
|
||||||
|
|
||||||
static void
|
static void
|
||||||
DoInit()
|
DoInit(const char* iface)
|
||||||
{
|
{
|
||||||
|
|
||||||
#ifdef UPNP_ENABLE_IPV6
|
#ifdef UPNP_ENABLE_IPV6
|
||||||
auto code = UpnpInit2(nullptr, 0);
|
auto code = UpnpInit2(iface, 0);
|
||||||
#else
|
#else
|
||||||
auto code = UpnpInit(nullptr, 0);
|
auto code = UpnpInit(iface, 0);
|
||||||
#endif
|
#endif
|
||||||
if (code != UPNP_E_SUCCESS)
|
if (code != UPNP_E_SUCCESS)
|
||||||
throw FormatRuntimeError("UpnpInit() failed: %s",
|
throw FormatRuntimeError("UpnpInit() failed: %s",
|
||||||
@@ -53,12 +54,12 @@ DoInit()
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
UpnpGlobalInit()
|
UpnpGlobalInit(const char* iface)
|
||||||
{
|
{
|
||||||
const std::lock_guard<Mutex> protect(upnp_init_mutex);
|
const std::lock_guard<Mutex> protect(upnp_init_mutex);
|
||||||
|
|
||||||
if (upnp_ref == 0)
|
if (upnp_ref == 0)
|
||||||
DoInit();
|
DoInit(iface);
|
||||||
|
|
||||||
++upnp_ref;
|
++upnp_ref;
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,7 @@
|
|||||||
#define MPD_UPNP_INIT_HXX
|
#define MPD_UPNP_INIT_HXX
|
||||||
|
|
||||||
void
|
void
|
||||||
UpnpGlobalInit();
|
UpnpGlobalInit(const char* iface);
|
||||||
|
|
||||||
void
|
void
|
||||||
UpnpGlobalFinish() noexcept;
|
UpnpGlobalFinish() noexcept;
|
||||||
|
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "lib/alsa/NonBlock.hxx"
|
#include "lib/alsa/NonBlock.hxx"
|
||||||
|
#include "lib/alsa/Error.hxx"
|
||||||
#include "mixer/MixerInternal.hxx"
|
#include "mixer/MixerInternal.hxx"
|
||||||
#include "mixer/Listener.hxx"
|
#include "mixer/Listener.hxx"
|
||||||
#include "output/OutputAPI.hxx"
|
#include "output/OutputAPI.hxx"
|
||||||
@@ -264,16 +265,15 @@ AlsaMixer::Setup()
|
|||||||
int err;
|
int err;
|
||||||
|
|
||||||
if ((err = snd_mixer_attach(handle, device)) < 0)
|
if ((err = snd_mixer_attach(handle, device)) < 0)
|
||||||
throw FormatRuntimeError("failed to attach to %s: %s",
|
throw Alsa::MakeError(err,
|
||||||
device, snd_strerror(err));
|
fmt::format("failed to attach to {}",
|
||||||
|
device).c_str());
|
||||||
|
|
||||||
if ((err = snd_mixer_selem_register(handle, nullptr, nullptr)) < 0)
|
if ((err = snd_mixer_selem_register(handle, nullptr, nullptr)) < 0)
|
||||||
throw FormatRuntimeError("snd_mixer_selem_register() failed: %s",
|
throw Alsa::MakeError(err, "snd_mixer_selem_register() failed");
|
||||||
snd_strerror(err));
|
|
||||||
|
|
||||||
if ((err = snd_mixer_load(handle)) < 0)
|
if ((err = snd_mixer_load(handle)) < 0)
|
||||||
throw FormatRuntimeError("snd_mixer_load() failed: %s\n",
|
throw Alsa::MakeError(err, "snd_mixer_load() failed");
|
||||||
snd_strerror(err));
|
|
||||||
|
|
||||||
elem = alsa_mixer_lookup_elem(handle, control, index);
|
elem = alsa_mixer_lookup_elem(handle, control, index);
|
||||||
if (elem == nullptr)
|
if (elem == nullptr)
|
||||||
@@ -294,8 +294,7 @@ AlsaMixer::Open()
|
|||||||
|
|
||||||
err = snd_mixer_open(&handle, 0);
|
err = snd_mixer_open(&handle, 0);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_mixer_open() failed: %s",
|
throw Alsa::MakeError(err, "snd_mixer_open() failed");
|
||||||
snd_strerror(err));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Setup();
|
Setup();
|
||||||
@@ -325,8 +324,7 @@ AlsaMixer::GetVolume()
|
|||||||
|
|
||||||
err = snd_mixer_handle_events(handle);
|
err = snd_mixer_handle_events(handle);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_mixer_handle_events() failed: %s",
|
throw Alsa::MakeError(err, "snd_mixer_handle_events() failed");
|
||||||
snd_strerror(err));
|
|
||||||
|
|
||||||
int volume = GetPercentVolume();
|
int volume = GetPercentVolume();
|
||||||
if (resulting_volume >= 0 && volume == resulting_volume)
|
if (resulting_volume >= 0 && volume == resulting_volume)
|
||||||
@@ -343,8 +341,7 @@ AlsaMixer::SetVolume(unsigned volume)
|
|||||||
|
|
||||||
int err = set_normalized_playback_volume(elem, 0.01*volume, 1);
|
int err = set_normalized_playback_volume(elem, 0.01*volume, 1);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("failed to set ALSA volume: %s",
|
throw Alsa::MakeError(err, "failed to set ALSA volume");
|
||||||
snd_strerror(err));
|
|
||||||
|
|
||||||
desired_volume = volume;
|
desired_volume = volume;
|
||||||
resulting_volume = GetPercentVolume();
|
resulting_volume = GetPercentVolume();
|
||||||
|
@@ -74,7 +74,7 @@ private:
|
|||||||
void
|
void
|
||||||
UpnpNeighborExplorer::Open()
|
UpnpNeighborExplorer::Open()
|
||||||
{
|
{
|
||||||
auto handle = UpnpClientGlobalInit();
|
auto handle = UpnpClientGlobalInit(nullptr);
|
||||||
|
|
||||||
discovery = new UPnPDeviceDirectory(event_loop, handle, this);
|
discovery = new UPnPDeviceDirectory(event_loop, handle, this);
|
||||||
|
|
||||||
|
@@ -33,23 +33,27 @@
|
|||||||
static constexpr PeriodClock::Duration REOPEN_AFTER = std::chrono::seconds(10);
|
static constexpr PeriodClock::Duration REOPEN_AFTER = std::chrono::seconds(10);
|
||||||
|
|
||||||
AudioOutputControl::AudioOutputControl(std::unique_ptr<FilteredAudioOutput> _output,
|
AudioOutputControl::AudioOutputControl(std::unique_ptr<FilteredAudioOutput> _output,
|
||||||
AudioOutputClient &_client) noexcept
|
AudioOutputClient &_client,
|
||||||
|
const ConfigBlock &block)
|
||||||
:output(std::move(_output)),
|
:output(std::move(_output)),
|
||||||
name(output->GetName()),
|
name(output->GetName()),
|
||||||
client(_client),
|
client(_client),
|
||||||
thread(BIND_THIS_METHOD(Task))
|
thread(BIND_THIS_METHOD(Task)),
|
||||||
|
tags(block.GetBlockValue("tags", true)),
|
||||||
|
always_on(block.GetBlockValue("always_on", false)),
|
||||||
|
enabled(block.GetBlockValue("enabled", true))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioOutputControl::AudioOutputControl(AudioOutputControl *_output,
|
AudioOutputControl::AudioOutputControl(AudioOutputControl &&src,
|
||||||
AudioOutputClient &_client) noexcept
|
AudioOutputClient &_client) noexcept
|
||||||
:output(_output->Steal()),
|
:output(src.Steal()),
|
||||||
name(output->GetName()),
|
name(output->GetName()),
|
||||||
client(_client),
|
client(_client),
|
||||||
thread(BIND_THIS_METHOD(Task))
|
thread(BIND_THIS_METHOD(Task)),
|
||||||
|
tags(src.tags),
|
||||||
|
always_on(src.always_on)
|
||||||
{
|
{
|
||||||
tags =_output->tags;
|
|
||||||
always_on=_output->always_on;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioOutputControl::~AudioOutputControl() noexcept
|
AudioOutputControl::~AudioOutputControl() noexcept
|
||||||
@@ -57,14 +61,6 @@ AudioOutputControl::~AudioOutputControl() noexcept
|
|||||||
StopThread();
|
StopThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
AudioOutputControl::Configure(const ConfigBlock &block)
|
|
||||||
{
|
|
||||||
tags = block.GetBlockValue("tags", true);
|
|
||||||
always_on = block.GetBlockValue("always_on", false);
|
|
||||||
enabled = block.GetBlockValue("enabled", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<FilteredAudioOutput>
|
std::unique_ptr<FilteredAudioOutput>
|
||||||
AudioOutputControl::Steal() noexcept
|
AudioOutputControl::Steal() noexcept
|
||||||
{
|
{
|
||||||
|
@@ -151,13 +151,13 @@ class AudioOutputControl {
|
|||||||
* default is true, but it may be configured to false to
|
* default is true, but it may be configured to false to
|
||||||
* suppress sending tags to the output.
|
* suppress sending tags to the output.
|
||||||
*/
|
*/
|
||||||
bool tags;
|
const bool tags;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shall this output always play something (i.e. silence),
|
* Shall this output always play something (i.e. silence),
|
||||||
* even when playback is stopped?
|
* even when playback is stopped?
|
||||||
*/
|
*/
|
||||||
bool always_on;
|
const bool always_on;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Has the user enabled this device?
|
* Has the user enabled this device?
|
||||||
@@ -249,10 +249,18 @@ public:
|
|||||||
*/
|
*/
|
||||||
mutable Mutex mutex;
|
mutable Mutex mutex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws on error.
|
||||||
|
*/
|
||||||
AudioOutputControl(std::unique_ptr<FilteredAudioOutput> _output,
|
AudioOutputControl(std::unique_ptr<FilteredAudioOutput> _output,
|
||||||
AudioOutputClient &_client) noexcept;
|
AudioOutputClient &_client,
|
||||||
|
const ConfigBlock &block);
|
||||||
|
|
||||||
AudioOutputControl(AudioOutputControl *_outputControl,
|
/**
|
||||||
|
* Move the contents of an existing instance, and convert that
|
||||||
|
* existing instance to a "dummy" output.
|
||||||
|
*/
|
||||||
|
AudioOutputControl(AudioOutputControl &&src,
|
||||||
AudioOutputClient &_client) noexcept;
|
AudioOutputClient &_client) noexcept;
|
||||||
|
|
||||||
~AudioOutputControl() noexcept;
|
~AudioOutputControl() noexcept;
|
||||||
@@ -260,11 +268,6 @@ public:
|
|||||||
AudioOutputControl(const AudioOutputControl &) = delete;
|
AudioOutputControl(const AudioOutputControl &) = delete;
|
||||||
AudioOutputControl &operator=(const AudioOutputControl &) = delete;
|
AudioOutputControl &operator=(const AudioOutputControl &) = delete;
|
||||||
|
|
||||||
/**
|
|
||||||
* Throws on error.
|
|
||||||
*/
|
|
||||||
void Configure(const ConfigBlock &block);
|
|
||||||
|
|
||||||
[[gnu::pure]]
|
[[gnu::pure]]
|
||||||
const char *GetName() const noexcept;
|
const char *GetName() const noexcept;
|
||||||
|
|
||||||
|
@@ -181,7 +181,8 @@ public:
|
|||||||
void Disable() noexcept;
|
void Disable() noexcept;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoke OutputPlugin::close().
|
* Close everything: the output (via CloseOutput()) and the
|
||||||
|
* software mixer (via CloseSoftwareMixer()).
|
||||||
*
|
*
|
||||||
* Caller must not lock the mutex.
|
* Caller must not lock the mutex.
|
||||||
*/
|
*/
|
||||||
@@ -200,7 +201,7 @@ public:
|
|||||||
void OpenOutputAndConvert(AudioFormat audio_format);
|
void OpenOutputAndConvert(AudioFormat audio_format);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the output plugin.
|
* Invoke AudioOutput::Close(), but nothing else.
|
||||||
*
|
*
|
||||||
* Mutex must not be locked.
|
* Mutex must not be locked.
|
||||||
*/
|
*/
|
||||||
|
@@ -80,9 +80,8 @@ LoadOutputControl(EventLoop &event_loop, EventLoop &rt_event_loop,
|
|||||||
replay_gain_config,
|
replay_gain_config,
|
||||||
mixer_listener,
|
mixer_listener,
|
||||||
block, defaults, filter_factory);
|
block, defaults, filter_factory);
|
||||||
auto control = std::make_unique<AudioOutputControl>(std::move(output), client);
|
return std::make_unique<AudioOutputControl>(std::move(output),
|
||||||
control->Configure(block);
|
client, block);
|
||||||
return control;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -130,24 +129,12 @@ MultipleOutputs::FindByName(const char *name) noexcept
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MultipleOutputs::Add(std::unique_ptr<FilteredAudioOutput> output,
|
MultipleOutputs::AddMoveFrom(AudioOutputControl &&src,
|
||||||
bool enable) noexcept
|
bool enable) noexcept
|
||||||
{
|
{
|
||||||
// TODO: this operation needs to be protected with a mutex
|
// TODO: this operation needs to be protected with a mutex
|
||||||
outputs.push_back(std::make_unique<AudioOutputControl>(std::move(output),
|
outputs.push_back(std::make_unique<AudioOutputControl>(std::move(src),
|
||||||
client));
|
client));
|
||||||
|
|
||||||
outputs.back()->LockSetEnabled(enable);
|
|
||||||
|
|
||||||
client.ApplyEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
MultipleOutputs::AddCopy(AudioOutputControl *outputControl,
|
|
||||||
bool enable) noexcept
|
|
||||||
{
|
|
||||||
// TODO: this operation needs to be protected with a mutex
|
|
||||||
outputs.push_back(std::make_unique<AudioOutputControl>(outputControl, client));
|
|
||||||
|
|
||||||
outputs.back()->LockSetEnabled(enable);
|
outputs.back()->LockSetEnabled(enable);
|
||||||
|
|
||||||
|
@@ -125,11 +125,8 @@ public:
|
|||||||
return FindByName(name) != nullptr;
|
return FindByName(name) != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Add(std::unique_ptr<FilteredAudioOutput> output,
|
void AddMoveFrom(AudioOutputControl &&src,
|
||||||
bool enable) noexcept;
|
bool enable) noexcept;
|
||||||
|
|
||||||
void AddCopy(AudioOutputControl *outputControl,
|
|
||||||
bool enable) noexcept;
|
|
||||||
|
|
||||||
|
|
||||||
void SetReplayGainMode(ReplayGainMode mode) noexcept;
|
void SetReplayGainMode(ReplayGainMode mode) noexcept;
|
||||||
|
@@ -74,13 +74,7 @@ AudioOutputControl::InternalOpen2(const AudioFormat in_audio_format)
|
|||||||
try {
|
try {
|
||||||
output->ConfigureConvertFilter();
|
output->ConfigureConvertFilter();
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
open = false;
|
InternalCloseOutput(false);
|
||||||
|
|
||||||
{
|
|
||||||
const ScopeUnlock unlock(mutex);
|
|
||||||
output->CloseOutput(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "AlsaOutputPlugin.hxx"
|
#include "AlsaOutputPlugin.hxx"
|
||||||
#include "lib/alsa/AllowedFormat.hxx"
|
#include "lib/alsa/AllowedFormat.hxx"
|
||||||
|
#include "lib/alsa/Error.hxx"
|
||||||
#include "lib/alsa/HwSetup.hxx"
|
#include "lib/alsa/HwSetup.hxx"
|
||||||
#include "lib/alsa/NonBlock.hxx"
|
#include "lib/alsa/NonBlock.hxx"
|
||||||
#include "lib/alsa/PeriodBuffer.hxx"
|
#include "lib/alsa/PeriodBuffer.hxx"
|
||||||
@@ -42,6 +43,10 @@
|
|||||||
#include "event/Call.hxx"
|
#include "event/Call.hxx"
|
||||||
#include "Log.hxx"
|
#include "Log.hxx"
|
||||||
|
|
||||||
|
#ifdef ENABLE_DSD
|
||||||
|
#include "util/AllocatedArray.hxx"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <alsa/asoundlib.h>
|
#include <alsa/asoundlib.h>
|
||||||
|
|
||||||
#include <boost/lockfree/spsc_queue.hpp>
|
#include <boost/lockfree/spsc_queue.hpp>
|
||||||
@@ -84,6 +89,33 @@ class AlsaOutput final
|
|||||||
* @see http://dsd-guide.com/dop-open-standard
|
* @see http://dsd-guide.com/dop-open-standard
|
||||||
*/
|
*/
|
||||||
bool dop_setting;
|
bool dop_setting;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Are we currently playing DSD? (Native DSD or DoP)
|
||||||
|
*/
|
||||||
|
bool use_dsd;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play some silence before closing the output in DSD mode?
|
||||||
|
* This is a workaround for some DACs which emit noise when
|
||||||
|
* stopping DSD playback.
|
||||||
|
*/
|
||||||
|
const bool stop_dsd_silence;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Are we currently draining with #stop_dsd_silence?
|
||||||
|
*/
|
||||||
|
bool in_stop_dsd_silence;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the DSD sync workaround for Thesycon USB audio
|
||||||
|
* receivers? On this device, playing DSD512 or PCM causes
|
||||||
|
* all subsequent attempts to play other DSD rates to fail,
|
||||||
|
* which can be fixed by briefly playing PCM at 44.1 kHz.
|
||||||
|
*/
|
||||||
|
const bool thesycon_dsd_workaround;
|
||||||
|
|
||||||
|
bool need_thesycon_dsd_workaround = thesycon_dsd_workaround;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/** libasound's buffer_time setting (in microseconds) */
|
/** libasound's buffer_time setting (in microseconds) */
|
||||||
@@ -93,7 +125,7 @@ class AlsaOutput final
|
|||||||
const unsigned period_time;
|
const unsigned period_time;
|
||||||
|
|
||||||
/** the mode flags passed to snd_pcm_open */
|
/** the mode flags passed to snd_pcm_open */
|
||||||
int mode = 0;
|
const int mode;
|
||||||
|
|
||||||
std::forward_list<Alsa::AllowedFormat> allowed_formats;
|
std::forward_list<Alsa::AllowedFormat> allowed_formats;
|
||||||
|
|
||||||
@@ -344,39 +376,9 @@ private:
|
|||||||
/**
|
/**
|
||||||
* @return false if no data was moved
|
* @return false if no data was moved
|
||||||
*/
|
*/
|
||||||
bool CopyRingToPeriodBuffer() noexcept {
|
bool CopyRingToPeriodBuffer() noexcept;
|
||||||
if (period_buffer.IsFull())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
size_t nbytes = ring_buffer->pop(period_buffer.GetTail(),
|
snd_pcm_sframes_t WriteFromPeriodBuffer() noexcept;
|
||||||
period_buffer.GetSpaceBytes());
|
|
||||||
if (nbytes == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
period_buffer.AppendBytes(nbytes);
|
|
||||||
|
|
||||||
const std::lock_guard<Mutex> lock(mutex);
|
|
||||||
/* notify the OutputThread that there is now
|
|
||||||
room in ring_buffer */
|
|
||||||
cond.notify_one();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
snd_pcm_sframes_t WriteFromPeriodBuffer() noexcept {
|
|
||||||
assert(period_buffer.IsFull());
|
|
||||||
assert(period_buffer.GetFrames(out_frame_size) > 0);
|
|
||||||
|
|
||||||
auto frames_written = snd_pcm_writei(pcm, period_buffer.GetHead(),
|
|
||||||
period_buffer.GetFrames(out_frame_size));
|
|
||||||
if (frames_written > 0) {
|
|
||||||
written = true;
|
|
||||||
period_buffer.ConsumeFrames(frames_written,
|
|
||||||
out_frame_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
return frames_written;
|
|
||||||
}
|
|
||||||
|
|
||||||
void LockCaughtError() noexcept {
|
void LockCaughtError() noexcept {
|
||||||
period_buffer.Clear();
|
period_buffer.Clear();
|
||||||
@@ -385,6 +387,9 @@ private:
|
|||||||
error = std::current_exception();
|
error = std::current_exception();
|
||||||
active = false;
|
active = false;
|
||||||
waiting = false;
|
waiting = false;
|
||||||
|
#ifdef ENABLE_DSD
|
||||||
|
in_stop_dsd_silence = false;
|
||||||
|
#endif
|
||||||
cond.notify_one();
|
cond.notify_one();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,21 +413,11 @@ private:
|
|||||||
|
|
||||||
static constexpr Domain alsa_output_domain("alsa_output");
|
static constexpr Domain alsa_output_domain("alsa_output");
|
||||||
|
|
||||||
AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block)
|
static int
|
||||||
:AudioOutput(FLAG_ENABLE_DISABLE),
|
GetAlsaOpenMode(const ConfigBlock &block)
|
||||||
MultiSocketMonitor(_loop),
|
|
||||||
defer_invalidate_sockets(_loop, BIND_THIS_METHOD(InvalidateSockets)),
|
|
||||||
silence_timer(_loop, BIND_THIS_METHOD(OnSilenceTimer)),
|
|
||||||
device(block.GetBlockValue("device", "")),
|
|
||||||
#ifdef ENABLE_DSD
|
|
||||||
dop_setting(block.GetBlockValue("dop", false) ||
|
|
||||||
/* legacy name from MPD 0.18 and older: */
|
|
||||||
block.GetBlockValue("dsd_usb", false)),
|
|
||||||
#endif
|
|
||||||
buffer_time(block.GetPositiveValue("buffer_time",
|
|
||||||
MPD_ALSA_BUFFER_TIME_US)),
|
|
||||||
period_time(block.GetPositiveValue("period_time", 0U))
|
|
||||||
{
|
{
|
||||||
|
int mode = 0;
|
||||||
|
|
||||||
#ifdef SND_PCM_NO_AUTO_RESAMPLE
|
#ifdef SND_PCM_NO_AUTO_RESAMPLE
|
||||||
if (!block.GetBlockValue("auto_resample", true))
|
if (!block.GetBlockValue("auto_resample", true))
|
||||||
mode |= SND_PCM_NO_AUTO_RESAMPLE;
|
mode |= SND_PCM_NO_AUTO_RESAMPLE;
|
||||||
@@ -438,6 +433,28 @@ AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block)
|
|||||||
mode |= SND_PCM_NO_AUTO_FORMAT;
|
mode |= SND_PCM_NO_AUTO_FORMAT;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block)
|
||||||
|
:AudioOutput(FLAG_ENABLE_DISABLE),
|
||||||
|
MultiSocketMonitor(_loop),
|
||||||
|
defer_invalidate_sockets(_loop, BIND_THIS_METHOD(InvalidateSockets)),
|
||||||
|
silence_timer(_loop, BIND_THIS_METHOD(OnSilenceTimer)),
|
||||||
|
device(block.GetBlockValue("device", "")),
|
||||||
|
#ifdef ENABLE_DSD
|
||||||
|
dop_setting(block.GetBlockValue("dop", false) ||
|
||||||
|
/* legacy name from MPD 0.18 and older: */
|
||||||
|
block.GetBlockValue("dsd_usb", false)),
|
||||||
|
stop_dsd_silence(block.GetBlockValue("stop_dsd_silence", false)),
|
||||||
|
thesycon_dsd_workaround(block.GetBlockValue("thesycon_dsd_workaround",
|
||||||
|
false)),
|
||||||
|
#endif
|
||||||
|
buffer_time(block.GetPositiveValue("buffer_time",
|
||||||
|
MPD_ALSA_BUFFER_TIME_US)),
|
||||||
|
period_time(block.GetPositiveValue("period_time", 0U)),
|
||||||
|
mode(GetAlsaOpenMode(block))
|
||||||
|
{
|
||||||
const char *allowed_formats_string =
|
const char *allowed_formats_string =
|
||||||
block.GetBlockValue("allowed_formats", nullptr);
|
block.GetBlockValue("allowed_formats", nullptr);
|
||||||
if (allowed_formats_string != nullptr)
|
if (allowed_formats_string != nullptr)
|
||||||
@@ -462,7 +479,7 @@ AlsaOutput::SetAttribute(std::string &&name, std::string &&value)
|
|||||||
{
|
{
|
||||||
if (name == "allowed_formats") {
|
if (name == "allowed_formats") {
|
||||||
const std::lock_guard<Mutex> lock(attributes_mutex);
|
const std::lock_guard<Mutex> lock(attributes_mutex);
|
||||||
allowed_formats = Alsa::AllowedFormat::ParseList({value.data(), value.length()});
|
allowed_formats = Alsa::AllowedFormat::ParseList(value);
|
||||||
#ifdef ENABLE_DSD
|
#ifdef ENABLE_DSD
|
||||||
} else if (name == "dop") {
|
} else if (name == "dop") {
|
||||||
const std::lock_guard<Mutex> lock(attributes_mutex);
|
const std::lock_guard<Mutex> lock(attributes_mutex);
|
||||||
@@ -519,24 +536,20 @@ AlsaSetupSw(snd_pcm_t *pcm, snd_pcm_uframes_t start_threshold,
|
|||||||
|
|
||||||
int err = snd_pcm_sw_params_current(pcm, swparams);
|
int err = snd_pcm_sw_params_current(pcm, swparams);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_sw_params_current() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_sw_params_current() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
|
|
||||||
err = snd_pcm_sw_params_set_start_threshold(pcm, swparams,
|
err = snd_pcm_sw_params_set_start_threshold(pcm, swparams,
|
||||||
start_threshold);
|
start_threshold);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_sw_params_set_start_threshold() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_sw_params_set_start_threshold() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
|
|
||||||
err = snd_pcm_sw_params_set_avail_min(pcm, swparams, avail_min);
|
err = snd_pcm_sw_params_set_avail_min(pcm, swparams, avail_min);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_sw_params_set_avail_min() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_sw_params_set_avail_min() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
|
|
||||||
err = snd_pcm_sw_params(pcm, swparams);
|
err = snd_pcm_sw_params(pcm, swparams);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_sw_params() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_sw_params() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void
|
inline void
|
||||||
@@ -678,6 +691,97 @@ BestMatch(const std::forward_list<Alsa::AllowedFormat> &haystack,
|
|||||||
return haystack.front();
|
return haystack.front();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef ENABLE_DSD
|
||||||
|
|
||||||
|
static void
|
||||||
|
Play_44_1_Silence(snd_pcm_t *pcm)
|
||||||
|
{
|
||||||
|
snd_pcm_hw_params_t *hw;
|
||||||
|
snd_pcm_hw_params_alloca(&hw);
|
||||||
|
|
||||||
|
int err;
|
||||||
|
|
||||||
|
err = snd_pcm_hw_params_any(pcm, hw);
|
||||||
|
if (err < 0)
|
||||||
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_any() failed");
|
||||||
|
|
||||||
|
err = snd_pcm_hw_params_set_access(pcm, hw,
|
||||||
|
SND_PCM_ACCESS_RW_INTERLEAVED);
|
||||||
|
if (err < 0)
|
||||||
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_access() failed");
|
||||||
|
|
||||||
|
err = snd_pcm_hw_params_set_format(pcm, hw, SND_PCM_FORMAT_S16);
|
||||||
|
if (err < 0)
|
||||||
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_format() failed");
|
||||||
|
|
||||||
|
unsigned channels = 1;
|
||||||
|
err = snd_pcm_hw_params_set_channels_near(pcm, hw, &channels);
|
||||||
|
if (err < 0)
|
||||||
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_channels_near() failed");
|
||||||
|
|
||||||
|
constexpr snd_pcm_uframes_t rate = 44100;
|
||||||
|
err = snd_pcm_hw_params_set_rate(pcm, hw, rate, 0);
|
||||||
|
if (err < 0)
|
||||||
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_rate() failed");
|
||||||
|
|
||||||
|
snd_pcm_uframes_t buffer_size = 1;
|
||||||
|
err = snd_pcm_hw_params_set_buffer_size_near(pcm, hw, &buffer_size);
|
||||||
|
if (err < 0)
|
||||||
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_buffer_size_near() failed");
|
||||||
|
|
||||||
|
snd_pcm_uframes_t period_size = 1;
|
||||||
|
int dir = 0;
|
||||||
|
err = snd_pcm_hw_params_set_period_size_near(pcm, hw, &period_size,
|
||||||
|
&dir);
|
||||||
|
if (err < 0)
|
||||||
|
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_period_size_near() failed");
|
||||||
|
|
||||||
|
err = snd_pcm_hw_params(pcm, hw);
|
||||||
|
if (err < 0)
|
||||||
|
throw Alsa::MakeError(err, "snd_pcm_hw_params() failed");
|
||||||
|
|
||||||
|
snd_pcm_sw_params_t *sw;
|
||||||
|
snd_pcm_sw_params_alloca(&sw);
|
||||||
|
|
||||||
|
err = snd_pcm_sw_params_current(pcm, sw);
|
||||||
|
if (err < 0)
|
||||||
|
throw Alsa::MakeError(err, "snd_pcm_sw_params_current() failed");
|
||||||
|
|
||||||
|
err = snd_pcm_sw_params_set_start_threshold(pcm, sw, period_size);
|
||||||
|
if (err < 0)
|
||||||
|
throw Alsa::MakeError(err, "snd_pcm_sw_params_set_start_threshold() failed");
|
||||||
|
|
||||||
|
err = snd_pcm_sw_params(pcm, sw);
|
||||||
|
if (err < 0)
|
||||||
|
throw Alsa::MakeError(err, "snd_pcm_sw_params() failed");
|
||||||
|
|
||||||
|
err = snd_pcm_prepare(pcm);
|
||||||
|
if (err < 0)
|
||||||
|
throw Alsa::MakeError(err, "snd_pcm_prepare() failed");
|
||||||
|
|
||||||
|
AllocatedArray<int16_t> buffer{channels * period_size};
|
||||||
|
std::fill(buffer.begin(), buffer.end(), 0);
|
||||||
|
|
||||||
|
/* play at least 250ms of silence */
|
||||||
|
for (snd_pcm_uframes_t remaining_frames = rate / 4;;) {
|
||||||
|
auto n = snd_pcm_writei(pcm, buffer.data(),
|
||||||
|
period_size);
|
||||||
|
if (n < 0)
|
||||||
|
throw Alsa::MakeError(err, "snd_pcm_writei() failed");
|
||||||
|
|
||||||
|
if (snd_pcm_uframes_t(n) >= remaining_frames)
|
||||||
|
break;
|
||||||
|
|
||||||
|
remaining_frames -= snd_pcm_uframes_t(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
err = snd_pcm_drain(pcm);
|
||||||
|
if (err < 0)
|
||||||
|
throw Alsa::MakeError(err, "snd_pcm_drain() failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
void
|
void
|
||||||
AlsaOutput::Open(AudioFormat &audio_format)
|
AlsaOutput::Open(AudioFormat &audio_format)
|
||||||
{
|
{
|
||||||
@@ -704,13 +808,30 @@ AlsaOutput::Open(AudioFormat &audio_format)
|
|||||||
int err = snd_pcm_open(&pcm, GetDevice(),
|
int err = snd_pcm_open(&pcm, GetDevice(),
|
||||||
SND_PCM_STREAM_PLAYBACK, mode);
|
SND_PCM_STREAM_PLAYBACK, mode);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("Failed to open ALSA device \"%s\": %s",
|
throw Alsa::MakeError(err,
|
||||||
GetDevice(), snd_strerror(err));
|
fmt::format("Failed to open ALSA device \"{}\"",
|
||||||
|
GetDevice()).c_str());
|
||||||
|
|
||||||
FmtDebug(alsa_output_domain, "opened {} type={}",
|
FmtDebug(alsa_output_domain, "opened {} type={}",
|
||||||
snd_pcm_name(pcm),
|
snd_pcm_name(pcm),
|
||||||
snd_pcm_type_name(snd_pcm_type(pcm)));
|
snd_pcm_type_name(snd_pcm_type(pcm)));
|
||||||
|
|
||||||
|
#ifdef ENABLE_DSD
|
||||||
|
if (need_thesycon_dsd_workaround &&
|
||||||
|
audio_format.format == SampleFormat::DSD &&
|
||||||
|
audio_format.sample_rate <= 256 * 44100 / 8) {
|
||||||
|
LogDebug(alsa_output_domain, "Playing some 44.1 kHz silence");
|
||||||
|
|
||||||
|
try {
|
||||||
|
Play_44_1_Silence(pcm);
|
||||||
|
} catch (...) {
|
||||||
|
LogError(std::current_exception());
|
||||||
|
}
|
||||||
|
|
||||||
|
need_thesycon_dsd_workaround = false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
PcmExport::Params params;
|
PcmExport::Params params;
|
||||||
params.alsa_channel_order = true;
|
params.alsa_channel_order = true;
|
||||||
|
|
||||||
@@ -732,6 +853,14 @@ AlsaOutput::Open(AudioFormat &audio_format)
|
|||||||
snd_pcm_nonblock(pcm, 1);
|
snd_pcm_nonblock(pcm, 1);
|
||||||
|
|
||||||
#ifdef ENABLE_DSD
|
#ifdef ENABLE_DSD
|
||||||
|
use_dsd = audio_format.format == SampleFormat::DSD;
|
||||||
|
in_stop_dsd_silence = false;
|
||||||
|
|
||||||
|
if (thesycon_dsd_workaround &&
|
||||||
|
(!use_dsd ||
|
||||||
|
audio_format.sample_rate > 256 * 44100 / 8))
|
||||||
|
need_thesycon_dsd_workaround = true;
|
||||||
|
|
||||||
if (params.dsd_mode == PcmExport::DsdMode::DOP)
|
if (params.dsd_mode == PcmExport::DsdMode::DOP)
|
||||||
LogDebug(alsa_output_domain, "DoP (DSD over PCM) enabled");
|
LogDebug(alsa_output_domain, "DoP (DSD over PCM) enabled");
|
||||||
#endif
|
#endif
|
||||||
@@ -824,9 +953,59 @@ AlsaOutput::Recover(int err) noexcept
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
AlsaOutput::CopyRingToPeriodBuffer() noexcept
|
||||||
|
{
|
||||||
|
if (period_buffer.IsFull())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
size_t nbytes = ring_buffer->pop(period_buffer.GetTail(),
|
||||||
|
period_buffer.GetSpaceBytes());
|
||||||
|
if (nbytes == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
period_buffer.AppendBytes(nbytes);
|
||||||
|
|
||||||
|
const std::lock_guard<Mutex> lock(mutex);
|
||||||
|
/* notify the OutputThread that there is now
|
||||||
|
room in ring_buffer */
|
||||||
|
cond.notify_one();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
snd_pcm_sframes_t
|
||||||
|
AlsaOutput::WriteFromPeriodBuffer() noexcept
|
||||||
|
{
|
||||||
|
assert(period_buffer.IsFull());
|
||||||
|
assert(period_buffer.GetFrames(out_frame_size) > 0);
|
||||||
|
|
||||||
|
auto frames_written = snd_pcm_writei(pcm, period_buffer.GetHead(),
|
||||||
|
period_buffer.GetFrames(out_frame_size));
|
||||||
|
if (frames_written > 0) {
|
||||||
|
written = true;
|
||||||
|
period_buffer.ConsumeFrames(frames_written,
|
||||||
|
out_frame_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames_written;
|
||||||
|
}
|
||||||
|
|
||||||
inline bool
|
inline bool
|
||||||
AlsaOutput::DrainInternal()
|
AlsaOutput::DrainInternal()
|
||||||
{
|
{
|
||||||
|
#ifdef ENABLE_DSD
|
||||||
|
if (in_stop_dsd_silence) {
|
||||||
|
/* "stop_dsd_silence" is in progress: clear internal
|
||||||
|
buffers and instead, fill the period buffer with
|
||||||
|
silence */
|
||||||
|
in_stop_dsd_silence = false;
|
||||||
|
ring_buffer->reset();
|
||||||
|
period_buffer.Clear();
|
||||||
|
period_buffer.FillWithSilence(silence, out_frame_size);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/* drain ring_buffer */
|
/* drain ring_buffer */
|
||||||
CopyRingToPeriodBuffer();
|
CopyRingToPeriodBuffer();
|
||||||
|
|
||||||
@@ -844,8 +1023,8 @@ AlsaOutput::DrainInternal()
|
|||||||
if (frames_written == -EAGAIN)
|
if (frames_written == -EAGAIN)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
throw FormatRuntimeError("snd_pcm_writei() failed: %s",
|
throw Alsa::MakeError(frames_written,
|
||||||
snd_strerror(-frames_written));
|
"snd_pcm_writei() failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* need to call CopyRingToPeriodBuffer() and
|
/* need to call CopyRingToPeriodBuffer() and
|
||||||
@@ -894,8 +1073,7 @@ AlsaOutput::DrainInternal()
|
|||||||
else if (result == -EAGAIN)
|
else if (result == -EAGAIN)
|
||||||
return false;
|
return false;
|
||||||
else
|
else
|
||||||
throw FormatRuntimeError("snd_pcm_drain() failed: %s",
|
throw Alsa::MakeError(result, "snd_pcm_drain() failed");
|
||||||
snd_strerror(-result));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -957,6 +1135,17 @@ AlsaOutput::Cancel() noexcept
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef ENABLE_DSD
|
||||||
|
if (stop_dsd_silence && use_dsd) {
|
||||||
|
/* play some DSD silence instead of snd_pcm_drop() */
|
||||||
|
std::unique_lock<Mutex> lock(mutex);
|
||||||
|
in_stop_dsd_silence = true;
|
||||||
|
drain = true;
|
||||||
|
cond.wait(lock, [this]{ return !drain || !active; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
BlockingCall(GetEventLoop(), [this](){
|
BlockingCall(GetEventLoop(), [this](){
|
||||||
CancelInternal();
|
CancelInternal();
|
||||||
});
|
});
|
||||||
@@ -1083,8 +1272,7 @@ try {
|
|||||||
|
|
||||||
int err = snd_pcm_prepare(pcm);
|
int err = snd_pcm_prepare(pcm);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_prepare() failed: %s",
|
throw Alsa::MakeError(err, "snd_pcm_prepare() failed");
|
||||||
snd_strerror(-err));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -1172,8 +1360,8 @@ try {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (Recover(frames_written) < 0)
|
if (Recover(frames_written) < 0)
|
||||||
throw FormatRuntimeError("snd_pcm_writei() failed: %s",
|
throw Alsa::MakeError(frames_written,
|
||||||
snd_strerror(-frames_written));
|
"snd_pcm_writei() failed");
|
||||||
|
|
||||||
/* recovered; try again in the next DispatchSockets()
|
/* recovered; try again in the next DispatchSockets()
|
||||||
call */
|
call */
|
||||||
|
@@ -21,7 +21,6 @@
|
|||||||
#include "../OutputAPI.hxx"
|
#include "../OutputAPI.hxx"
|
||||||
#include "thread/SafeSingleton.hxx"
|
#include "thread/SafeSingleton.hxx"
|
||||||
#include "system/Error.hxx"
|
#include "system/Error.hxx"
|
||||||
#include "util/DivideString.hxx"
|
|
||||||
#include "util/IterableSplitString.hxx"
|
#include "util/IterableSplitString.hxx"
|
||||||
#include "util/RuntimeError.hxx"
|
#include "util/RuntimeError.hxx"
|
||||||
#include "util/Domain.hxx"
|
#include "util/Domain.hxx"
|
||||||
|
@@ -55,6 +55,7 @@
|
|||||||
#include <boost/lockfree/spsc_queue.hpp>
|
#include <boost/lockfree/spsc_queue.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
@@ -51,6 +51,12 @@ class SnapcastOutput final : AudioOutput, ServerSocket {
|
|||||||
*/
|
*/
|
||||||
bool open;
|
bool open;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the output current paused? This is set by Pause() and
|
||||||
|
* is cleared by the next Play() call. It is used in Delay().
|
||||||
|
*/
|
||||||
|
bool pause;
|
||||||
|
|
||||||
InjectEvent inject_event;
|
InjectEvent inject_event;
|
||||||
|
|
||||||
#ifdef HAVE_ZEROCONF
|
#ifdef HAVE_ZEROCONF
|
||||||
|
@@ -161,6 +161,7 @@ SnapcastOutput::Open(AudioFormat &audio_format)
|
|||||||
timer = new Timer(audio_format);
|
timer = new Timer(audio_format);
|
||||||
|
|
||||||
open = true;
|
open = true;
|
||||||
|
pause = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -213,7 +214,7 @@ SnapcastOutput::RemoveClient(SnapcastClient &client) noexcept
|
|||||||
std::chrono::steady_clock::duration
|
std::chrono::steady_clock::duration
|
||||||
SnapcastOutput::Delay() const noexcept
|
SnapcastOutput::Delay() const noexcept
|
||||||
{
|
{
|
||||||
if (!LockHasClients() /*&& pause*/) {
|
if (!LockHasClients() && pause) {
|
||||||
/* if there's no client and this output is paused,
|
/* if there's no client and this output is paused,
|
||||||
then Pause() will not do anything, it will not fill
|
then Pause() will not do anything, it will not fill
|
||||||
the buffer and it will not update the timer;
|
the buffer and it will not update the timer;
|
||||||
@@ -307,7 +308,7 @@ SnapcastOutput::SendTag(const Tag &tag)
|
|||||||
size_t
|
size_t
|
||||||
SnapcastOutput::Play(const void *chunk, size_t size)
|
SnapcastOutput::Play(const void *chunk, size_t size)
|
||||||
{
|
{
|
||||||
//pause = false;
|
pause = false;
|
||||||
|
|
||||||
const auto now = std::chrono::steady_clock::now();
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
@@ -341,6 +342,8 @@ SnapcastOutput::Play(const void *chunk, size_t size)
|
|||||||
if (nbytes == 0)
|
if (nbytes == 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
unflushed_input = 0;
|
||||||
|
|
||||||
const std::lock_guard<Mutex> protect(mutex);
|
const std::lock_guard<Mutex> protect(mutex);
|
||||||
if (chunks.empty())
|
if (chunks.empty())
|
||||||
inject_event.Schedule();
|
inject_event.Schedule();
|
||||||
@@ -355,8 +358,7 @@ SnapcastOutput::Play(const void *chunk, size_t size)
|
|||||||
bool
|
bool
|
||||||
SnapcastOutput::Pause()
|
SnapcastOutput::Pause()
|
||||||
{
|
{
|
||||||
// TODO: implement
|
pause = true;
|
||||||
//pause = true;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@@ -25,7 +25,6 @@ if zeroconf_option == 'bonjour'
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
bonjour_deps = [
|
bonjour_deps = [
|
||||||
log_dep,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if not is_darwin
|
if not is_darwin
|
||||||
@@ -41,6 +40,7 @@ if zeroconf_option == 'bonjour'
|
|||||||
include_directories: inc,
|
include_directories: inc,
|
||||||
dependencies: [
|
dependencies: [
|
||||||
event_dep,
|
event_dep,
|
||||||
|
log_dep,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ After=network.target sound.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=notify
|
Type=notify
|
||||||
ExecStart=@prefix@/bin/mpd --no-daemon
|
ExecStart=@prefix@/bin/mpd --systemd
|
||||||
|
|
||||||
# Enable this setting to ask systemd to watch over MPD, see
|
# Enable this setting to ask systemd to watch over MPD, see
|
||||||
# systemd.service(5). This is disabled by default because it causes
|
# systemd.service(5). This is disabled by default because it causes
|
||||||
|
@@ -5,7 +5,7 @@ After=network.target sound.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=notify
|
Type=notify
|
||||||
ExecStart=@prefix@/bin/mpd --no-daemon
|
ExecStart=@prefix@/bin/mpd --systemd
|
||||||
|
|
||||||
# Enable this setting to ask systemd to watch over MPD, see
|
# Enable this setting to ask systemd to watch over MPD, see
|
||||||
# systemd.service(5). This is disabled by default because it causes
|
# systemd.service(5). This is disabled by default because it causes
|
||||||
|
@@ -99,6 +99,7 @@ thirdparty_libs = [
|
|||||||
libid3tag,
|
libid3tag,
|
||||||
liblame,
|
liblame,
|
||||||
libmodplug,
|
libmodplug,
|
||||||
|
libopenmpt,
|
||||||
wildmidi,
|
wildmidi,
|
||||||
gme,
|
gme,
|
||||||
ffmpeg,
|
ffmpeg,
|
||||||
|
Reference in New Issue
Block a user