Compare commits
232 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
18628bf89e | ||
![]() |
2052b461af | ||
![]() |
5019bdcd52 | ||
![]() |
8be0bcbdb9 | ||
![]() |
af72a22ed8 | ||
![]() |
6ed9668fea | ||
![]() |
175d2c6d29 | ||
![]() |
ab487b9a99 | ||
![]() |
ac59ec34f9 | ||
![]() |
82da57b7ce | ||
![]() |
aa6dac9bd2 | ||
![]() |
a26bf261a9 | ||
![]() |
c692286c67 | ||
![]() |
3775766605 | ||
![]() |
38e24208f6 | ||
![]() |
fbaedf2262 | ||
![]() |
8f3341cefb | ||
![]() |
4ec4bab3a9 | ||
![]() |
6d567bcd35 | ||
![]() |
363d9f0180 | ||
![]() |
db0682a469 | ||
![]() |
7a6823dcdf | ||
![]() |
bce144a232 | ||
![]() |
0cef84cac6 | ||
![]() |
56c0733b42 | ||
![]() |
0b0acb3981 | ||
![]() |
1375dcc4ec | ||
![]() |
6aeb0e335b | ||
![]() |
c1e2537851 | ||
![]() |
8c690fb737 | ||
![]() |
dad1c21b59 | ||
![]() |
dd10b2bd61 | ||
![]() |
48c7c540df | ||
![]() |
281270cd2a | ||
![]() |
02502514f6 | ||
![]() |
1bc02123f9 | ||
![]() |
3488a47c41 | ||
![]() |
fd82d67678 | ||
![]() |
e66c12105b | ||
![]() |
dbe12a6b90 | ||
![]() |
d3a680cc87 | ||
![]() |
62fc4d5cf4 | ||
![]() |
14465be847 | ||
![]() |
0e49de867d | ||
![]() |
f2e4529707 | ||
![]() |
3547fc7e61 | ||
![]() |
466a05bc52 | ||
![]() |
6de4064cca | ||
![]() |
bcf0fdd3a8 | ||
![]() |
a8f05a7efc | ||
![]() |
c64a3b5dbb | ||
![]() |
16c38c438f | ||
![]() |
48cc4a6ced | ||
![]() |
a169a05e41 | ||
![]() |
a6cb3139db | ||
![]() |
239a83324e | ||
![]() |
8efa5c7641 | ||
![]() |
28e7be248f | ||
![]() |
c3f9b38c97 | ||
![]() |
dbb18a401b | ||
![]() |
e1e41708af | ||
![]() |
638dfc3981 | ||
![]() |
7c09e44ad4 | ||
![]() |
365b798f33 | ||
![]() |
6f51d910ee | ||
![]() |
1215818572 | ||
![]() |
514ed33a02 | ||
![]() |
bfed47b82d | ||
![]() |
8c51440057 | ||
![]() |
018858ec97 | ||
![]() |
3c1988b68f | ||
![]() |
5452428d69 | ||
![]() |
d6bf6e161a | ||
![]() |
a71b76bb3c | ||
![]() |
c1429500b2 | ||
![]() |
0f02bbc2fe | ||
![]() |
b885f358a5 | ||
![]() |
650a30d794 | ||
![]() |
1dc71f383a | ||
![]() |
6dfebf7df9 | ||
![]() |
4bcdcca7f5 | ||
![]() |
c08a8581ee | ||
![]() |
25b0194036 | ||
![]() |
77fe727e69 | ||
![]() |
73f9824ddf | ||
![]() |
1fe0c673bc | ||
![]() |
8a045207a7 | ||
![]() |
fe7c5a4208 | ||
![]() |
8024f7e84d | ||
![]() |
14f0134097 | ||
![]() |
1da27be84d | ||
![]() |
08135f2cb7 | ||
![]() |
5907656bbb | ||
![]() |
2ac2bd26f8 | ||
![]() |
a2be91aea5 | ||
![]() |
579428172e | ||
![]() |
3e484637f9 | ||
![]() |
3e93c392d7 | ||
![]() |
0a97e68aa9 | ||
![]() |
69783a44c8 | ||
![]() |
d72263d28d | ||
![]() |
24a205a1aa | ||
![]() |
3a948515ce | ||
![]() |
9ade93983c | ||
![]() |
6931ce9558 | ||
![]() |
d6fb07a3e4 | ||
![]() |
01d3c2705e | ||
![]() |
29346dc9c5 | ||
![]() |
d19b3df3b0 | ||
![]() |
798e68ef62 | ||
![]() |
79397db5b4 | ||
![]() |
9256190a9b | ||
![]() |
3a0dbb0a67 | ||
![]() |
3d6c9d1b88 | ||
![]() |
5823e79fe7 | ||
![]() |
5f656dffda | ||
![]() |
34d4d9157a | ||
![]() |
22c329cdb4 | ||
![]() |
980ef82216 | ||
![]() |
84a06a72df | ||
![]() |
4833d0891d | ||
![]() |
cd53ca22c6 | ||
![]() |
4d9af9a81b | ||
![]() |
d61341c0e3 | ||
![]() |
eff50b263a | ||
![]() |
2bebc79363 | ||
![]() |
e777fb4edb | ||
![]() |
3fb25d4062 | ||
![]() |
e227596c20 | ||
![]() |
ec76583c33 | ||
![]() |
927f1e03a3 | ||
![]() |
f2c679cfec | ||
![]() |
6a75c48dba | ||
![]() |
48bdd09f64 | ||
![]() |
cf108c389f | ||
![]() |
90d97053a8 | ||
![]() |
e1fe9ebcd6 | ||
![]() |
93016ac6ab | ||
![]() |
fc20a1f10a | ||
![]() |
a4257e51d5 | ||
![]() |
2f2b3f1cdc | ||
![]() |
2ff6a9ad2b | ||
![]() |
17d4873b60 | ||
![]() |
8b41c4f384 | ||
![]() |
17f7098e27 | ||
![]() |
9ff790b7bb | ||
![]() |
ebc1fe2821 | ||
![]() |
bc2988144e | ||
![]() |
b1a9958c66 | ||
![]() |
e6a81bb95c | ||
![]() |
9521c1ad58 | ||
![]() |
6d65cc48d7 | ||
![]() |
681956a963 | ||
![]() |
052f64d648 | ||
![]() |
afe621c25c | ||
![]() |
637cf8a039 | ||
![]() |
2011a6e2ee | ||
![]() |
d54830de12 | ||
![]() |
a7e7312cca | ||
![]() |
6b83fc6b57 | ||
![]() |
74f9e07151 | ||
![]() |
82a61ab3be | ||
![]() |
54c1794cee | ||
![]() |
c962a6be76 | ||
![]() |
922c4bf3f0 | ||
![]() |
932756efce | ||
![]() |
7838265482 | ||
![]() |
b14b0e5634 | ||
![]() |
4d2d0e7bb8 | ||
![]() |
44378b7dbe | ||
![]() |
da642b2890 | ||
![]() |
6f77af20d0 | ||
![]() |
010f65a1d6 | ||
![]() |
c46f97454a | ||
![]() |
844dbd2ec5 | ||
![]() |
db7caa2dac | ||
![]() |
2974737746 | ||
![]() |
b1d7567226 | ||
![]() |
5103eb3039 | ||
![]() |
0cccdcf9b2 | ||
![]() |
22b840c2f1 | ||
![]() |
ed1a995bff | ||
![]() |
0f39dc1edb | ||
![]() |
dc9103befe | ||
![]() |
67760f5283 | ||
![]() |
99405a4c93 | ||
![]() |
b833c5d2c7 | ||
![]() |
bca5d79f88 | ||
![]() |
6e1c8edf09 | ||
![]() |
32b7b2e2fa | ||
![]() |
cfb7f8ab84 | ||
![]() |
8d80280ab9 | ||
![]() |
c95e3dc065 | ||
![]() |
00a520a4c3 | ||
![]() |
6eba621045 | ||
![]() |
a9ad8fa505 | ||
![]() |
85427826aa | ||
![]() |
25e0a90402 | ||
![]() |
938728820b | ||
![]() |
80531ef8d8 | ||
![]() |
a91fba6a3d | ||
![]() |
f8be403c34 | ||
![]() |
28a5cdf319 | ||
![]() |
6b1d264b35 | ||
![]() |
a6c10e9a1c | ||
![]() |
19a46064e9 | ||
![]() |
b57eeaa720 | ||
![]() |
ad059d5804 | ||
![]() |
6e1940e930 | ||
![]() |
103194e32d | ||
![]() |
481c330c17 | ||
![]() |
7ef489e057 | ||
![]() |
d9e5d5ff5b | ||
![]() |
ca02fb7782 | ||
![]() |
d4d06da2f8 | ||
![]() |
efde78db77 | ||
![]() |
f1b8bcd6b2 | ||
![]() |
c2bc3704e1 | ||
![]() |
def120aca4 | ||
![]() |
6d2b09ac2b | ||
![]() |
78b43a9930 | ||
![]() |
da5ff779c6 | ||
![]() |
e7da5b104d | ||
![]() |
4be76f3c8f | ||
![]() |
c58c53293c | ||
![]() |
8695a2806a | ||
![]() |
a59f1b21a6 | ||
![]() |
9e2d09dabc | ||
![]() |
2719f62feb | ||
![]() |
234cedd6c6 | ||
![]() |
5b946e9d95 | ||
![]() |
b46ca50dcc |
.travis.ymlNEWSREADME.md
android
doc
meson.buildpython/build
src
CommandLine.cxxMain.cxxRemoteTagCache.cxxTagAny.cxx
archive
plugins
client
command
db
decoder
plugins
event
fs
input
io
lib
curl
icu
CaseFold.cxxCaseFold.hxxCollate.cxxCompare.cxxCompare.hxxConverter.cxxConverter.hxxUtil.cxxUtil.hxxWin32.cxxWin32.hxx
jack
sqlite
mixer
plugins
neighbor
plugins
net
output
protocol
queue
song
sticker
storage
plugins
system
tag
thread
time
util
win32
Com.hxxComHeapPtr.hxxComPtr.hxxComWorker.cxxComWorker.hxxHResult.cxxHResult.hxxPropVariant.cxxPropVariant.hxxWin32Main.cxxWinEvent.cxxWinEvent.hxxmeson.build
zeroconf
subprojects
test
TestLookupFile.cxxrun_convert.cxxrun_input.cxxrun_output.cxxtest_mixramp.cxxtest_pcm_format.cxxtest_pcm_volume.cxx
win32
64
.travis.yml
64
.travis.yml
@@ -2,70 +2,37 @@ language: cpp
|
||||
|
||||
jobs:
|
||||
include:
|
||||
# Ubuntu Bionic (18.04) with GCC 7
|
||||
# Ubuntu Focal (20.04) with GCC 9.3
|
||||
- os: linux
|
||||
dist: bionic
|
||||
dist: focal
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: 'ppa:deadsnakes/ppa' # for Python 3.7 (required by Meson)
|
||||
packages:
|
||||
- meson
|
||||
- libgtest-dev
|
||||
- libboost-dev
|
||||
- python3.6
|
||||
- python3-urllib3
|
||||
- ninja-build
|
||||
before_install:
|
||||
- wget https://bootstrap.pypa.io/get-pip.py
|
||||
- /usr/bin/python3.6 get-pip.py --user --no-cache-dir
|
||||
install:
|
||||
- /usr/bin/python3.6 $HOME/.local/bin/pip install --user meson --no-cache-dir
|
||||
env:
|
||||
- MATRIX_EVAL="export PATH=\$HOME/.local/bin:\$PATH"
|
||||
|
||||
# Ubuntu Bionic (18.04) with GCC 7 on big-endian
|
||||
# Ubuntu Focal (20.04) with GCC 9.3 on big-endian
|
||||
- os: linux
|
||||
arch: s390x
|
||||
dist: bionic
|
||||
dist: focal
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: 'ppa:deadsnakes/ppa' # for Python 3.7 (required by Meson)
|
||||
packages:
|
||||
- meson
|
||||
- libgtest-dev
|
||||
- libboost-dev
|
||||
- python3.6
|
||||
- python3-urllib3
|
||||
- ninja-build
|
||||
before_install:
|
||||
- wget https://bootstrap.pypa.io/get-pip.py
|
||||
- /usr/bin/python3.6 get-pip.py --user --no-cache-dir
|
||||
install:
|
||||
- /usr/bin/python3.6 $HOME/.local/bin/pip install --user meson --no-cache-dir
|
||||
env:
|
||||
- MATRIX_EVAL="export PATH=\$HOME/.local/bin:\$PATH"
|
||||
|
||||
# Ubuntu Bionic (18.04) with GCC 7 on ARM64
|
||||
# Ubuntu Focal (20.04) with GCC 9.3 on ARM64
|
||||
- os: linux
|
||||
arch: arm64
|
||||
dist: bionic
|
||||
dist: focal
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: 'ppa:deadsnakes/ppa' # for Python 3.7 (required by Meson)
|
||||
packages:
|
||||
- meson
|
||||
- libgtest-dev
|
||||
- libboost-dev
|
||||
- python3.6
|
||||
- python3-urllib3
|
||||
- ninja-build
|
||||
before_install:
|
||||
- wget https://bootstrap.pypa.io/get-pip.py
|
||||
- /usr/bin/python3.6 get-pip.py --user --no-cache-dir
|
||||
install:
|
||||
- /usr/bin/python3.6 $HOME/.local/bin/pip install --user meson --no-cache-dir
|
||||
env:
|
||||
- MATRIX_EVAL="export PATH=\$HOME/.local/bin:\$PATH"
|
||||
|
||||
# Ubuntu Trusty (16.04) with GCC 8
|
||||
- os: linux
|
||||
@@ -75,7 +42,7 @@ jobs:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
- sourceline: 'ppa:mhier/libboost-latest'
|
||||
- sourceline: 'ppa:mstipicevic/ninja-build-1-7-2'
|
||||
- sourceline: 'ppa:ricotz/toolchain'
|
||||
- sourceline: 'ppa:deadsnakes/ppa' # for Python 3.7 (required by Meson)
|
||||
packages:
|
||||
- g++-8
|
||||
@@ -94,12 +61,13 @@ jobs:
|
||||
- MATRIX_EVAL="export CC='ccache gcc-8' CXX='ccache g++-8' LDFLAGS=-fuse-ld=gold PATH=\$HOME/.local/bin:\$PATH"
|
||||
|
||||
- os: osx
|
||||
osx_image: xcode10.3
|
||||
osx_image: xcode11.6
|
||||
addons:
|
||||
homebrew:
|
||||
packages:
|
||||
- ccache
|
||||
- meson
|
||||
- googletest
|
||||
- icu4c
|
||||
- ffmpeg
|
||||
- libnfs
|
||||
@@ -117,7 +85,6 @@ jobs:
|
||||
- faad2
|
||||
- wavpack
|
||||
- libmpdclient
|
||||
update: true
|
||||
env:
|
||||
- MATRIX_EVAL="export PATH=/usr/local/opt/ccache/libexec:$PATH HOMEBREW_NO_ANALYTICS=1"
|
||||
|
||||
@@ -134,13 +101,6 @@ before_install:
|
||||
- eval "${MATRIX_EVAL}"
|
||||
|
||||
install:
|
||||
# C++14
|
||||
|
||||
# Work around "Target /usr/local/lib/libgtest.a is a symlink
|
||||
# belonging to nss. You can unlink it" during gtest install
|
||||
- test "$TRAVIS_OS_NAME" != "osx" || brew unlink nss
|
||||
|
||||
- test "$TRAVIS_OS_NAME" != "osx" || brew install https://gist.githubusercontent.com/Kronuz/96ac10fbd8472eb1e7566d740c4034f8/raw/gtest.rb
|
||||
|
||||
before_script:
|
||||
- ccache -s
|
||||
|
58
NEWS
58
NEWS
@@ -1,3 +1,61 @@
|
||||
ver 0.22.9 (2021/06/23)
|
||||
* database
|
||||
- simple: load all .mpdignore files of all parent directories
|
||||
* tags
|
||||
- fix "readcomments" and "readpicture" on remote files with ID3 tags
|
||||
* decoder
|
||||
- ffmpeg: support the tags "sort_album", "album-sort", "artist-sort"
|
||||
- ffmpeg: fix build failure with FFmpeg 3.4
|
||||
* Android
|
||||
- fix auto-start on boot in Android 8 or later
|
||||
* Windows
|
||||
- fix build failure with SQLite
|
||||
|
||||
ver 0.22.8 (2021/05/22)
|
||||
* fix crash bug in "albumart" command (0.22.7 regression)
|
||||
|
||||
ver 0.22.7 (2021/05/19)
|
||||
* protocol
|
||||
- don't use glibc extension to parse time stamps
|
||||
- optimize the "albumart" command
|
||||
* input
|
||||
- curl: send user/password in the first request, save one roundtrip
|
||||
* decoder
|
||||
- ffmpeg: fix build problem with FFmpeg 3.4
|
||||
- gme: support RSN files
|
||||
* storage
|
||||
- curl: don't use glibc extension
|
||||
* database
|
||||
- simple: fix database corruption bug
|
||||
* output
|
||||
- fix crash when pausing with multiple partitions
|
||||
- jack: enable on Windows
|
||||
- httpd: send header "Access-Control-Allow-Origin: *"
|
||||
- wasapi: add algorithm for finding usable audio format
|
||||
- wasapi: use default device only if none was configured
|
||||
- wasapi: add DoP support
|
||||
|
||||
ver 0.22.6 (2021/02/16)
|
||||
* fix missing tags on songs in queue
|
||||
|
||||
ver 0.22.5 (2021/02/15)
|
||||
* protocol
|
||||
- error for malformed ranges instead of ignoring silently
|
||||
- better error message for open-ended range with "move"
|
||||
* database
|
||||
- simple: fix missing CUE sheet metadata in "addid" command
|
||||
* tags
|
||||
- id: translate TPE3 to Conductor, not Performer
|
||||
* archive
|
||||
- iso9660: another fix for unaligned reads
|
||||
* output
|
||||
- httpd: error handling on Windows improved
|
||||
- pulse: fix deadlock with "always_on"
|
||||
* Windows:
|
||||
- enable https:// support (via Schannel)
|
||||
* Android
|
||||
- work around "Permission denied" on mpd.conf
|
||||
|
||||
ver 0.22.4 (2021/01/21)
|
||||
* protocol
|
||||
- add command "binarylimit" to allow larger chunk sizes
|
||||
|
@@ -14,7 +14,7 @@ For basic installation instructions
|
||||
|
||||
- [Manual](http://www.musicpd.org/doc/user/)
|
||||
- [Forum](http://forum.musicpd.org/)
|
||||
- [IRC](irc://chat.freenode.net/#mpd)
|
||||
- [IRC](ircs://irc.libera.chat:6697/#mpd)
|
||||
- [Bug tracker](https://github.com/MusicPlayerDaemon/MPD/issues/)
|
||||
|
||||
# Developers
|
||||
|
@@ -2,10 +2,10 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.musicpd"
|
||||
android:installLocation="auto"
|
||||
android:versionCode="51"
|
||||
android:versionName="0.22.1">
|
||||
android:versionCode="57"
|
||||
android:versionName="0.22.9">
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28"/>
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/>
|
||||
|
||||
<uses-feature android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
@@ -19,6 +19,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application android:allowBackup="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:icon="@drawable/icon"
|
||||
android:banner="@drawable/icon"
|
||||
android:label="@string/app_name">
|
||||
|
@@ -5,8 +5,8 @@ android_ndk = get_option('android_ndk')
|
||||
android_sdk = get_option('android_sdk')
|
||||
android_abi = get_option('android_abi')
|
||||
|
||||
android_sdk_build_tools_version = '27.0.0'
|
||||
android_sdk_platform = 'android-23'
|
||||
android_sdk_build_tools_version = '29.0.3'
|
||||
android_sdk_platform = 'android-29'
|
||||
|
||||
android_build_tools_dir = join_paths(android_sdk, 'build-tools', android_sdk_build_tools_version)
|
||||
android_sdk_platform_dir = join_paths(android_sdk, 'platforms', android_sdk_platform)
|
||||
|
@@ -414,6 +414,15 @@ public class Main extends Service implements Runnable {
|
||||
* start Main service without any callback
|
||||
*/
|
||||
public static void start(Context context, boolean wakelock) {
|
||||
context.startService(new Intent(context, Main.class).putExtra("wakelock", wakelock));
|
||||
Intent intent = new Intent(context, Main.class)
|
||||
.putExtra("wakelock", wakelock);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
/* in Android 8+, we need to use this method
|
||||
or else we'll get "IllegalStateException:
|
||||
app is in background" */
|
||||
context.startForegroundService(intent);
|
||||
else
|
||||
context.startService(intent);
|
||||
}
|
||||
}
|
||||
|
@@ -38,7 +38,7 @@ author = 'Max Kellermann'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.22.4'
|
||||
version = '0.22.9'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = version
|
||||
|
||||
|
@@ -68,11 +68,11 @@ There are two active branches in the git repository:
|
||||
|
||||
- the "unstable" branch called ``master`` where new features are
|
||||
merged. This will become the next major release eventually.
|
||||
- the "stable" branch (currently called ``v0.21.x``) where only bug
|
||||
- the "stable" branch (currently called ``v0.22.x``) where only bug
|
||||
fixes are merged.
|
||||
|
||||
Once :program:`MPD` 0.22 is released, a new branch called ``v0.22.x``
|
||||
will be created for 0.22 bug-fix releases; after that, ``v0.21.x``
|
||||
Once :program:`MPD` 0.23 is released, a new branch called ``v0.23.x``
|
||||
will be created for 0.23 bug-fix releases; after that, ``v0.22.x``
|
||||
will eventually cease to be maintained.
|
||||
|
||||
After bug fixes have been added to the "stable" branch, it will be
|
||||
|
@@ -22,20 +22,6 @@ if get_option('html_manual')
|
||||
install: true,
|
||||
install_dir: join_paths(get_option('datadir'), 'doc', meson.project_name()),
|
||||
)
|
||||
|
||||
custom_target(
|
||||
'upload',
|
||||
input: sphinx_output,
|
||||
output: 'upload',
|
||||
build_always_stale: true,
|
||||
command: [
|
||||
'rsync', '-vpruz', '--delete', meson.current_build_dir() + '/',
|
||||
'www.musicpd.org:/var/www/mpd/doc/',
|
||||
'--chmod=Dug+rwx,Do+rx,Fug+rw,Fo+r',
|
||||
'--include=html', '--include=html/**',
|
||||
'--exclude=*',
|
||||
],
|
||||
)
|
||||
endif
|
||||
|
||||
if get_option('manpages')
|
||||
|
@@ -715,7 +715,7 @@ A resampler using `libsamplerate <http://www.mega-nerd.com/SRC/>`_ a.k.a. Secret
|
||||
* - Name
|
||||
- Description
|
||||
* - **type**
|
||||
- The interpolator type. See below for a list of known types.
|
||||
- The interpolator type. Defaults to :samp:`2`. See below for a list of known types.
|
||||
|
||||
The following converter types are provided by libsamplerate:
|
||||
|
||||
@@ -910,6 +910,10 @@ jack
|
||||
|
||||
The jack plugin connects to a `JACK server <http://jackaudio.org/>`_.
|
||||
|
||||
On Windows, this plugin loads :file:`libjack64.dll` at runtime. This
|
||||
means you need to `download and install the JACK windows build
|
||||
<https://jackaudio.org/downloads/>`_.
|
||||
|
||||
.. list-table::
|
||||
:widths: 20 80
|
||||
:header-rows: 1
|
||||
@@ -1171,6 +1175,8 @@ The `Windows Audio Session API <https://docs.microsoft.com/en-us/windows/win32/c
|
||||
- Enumerate all devices in log while playing started. Useful for device configuration. The default value is "no".
|
||||
* - **exclusive yes|no**
|
||||
- Exclusive mode blocks all other audio source, and get best audio quality without resampling. Stopping playing release the exclusive control of the output device. The default value is "no".
|
||||
* - **dop yes|no**
|
||||
- Enable DSD over PCM. Require exclusive mode. The default value is "no".
|
||||
|
||||
|
||||
.. _filter_plugins:
|
||||
|
@@ -677,6 +677,11 @@ Whenever possible, ids should be used.
|
||||
(directories add recursively). ``URI``
|
||||
can also be a single file.
|
||||
|
||||
Clients that are connected via local socket may add arbitrary
|
||||
local files (URI is an absolute path). Example::
|
||||
|
||||
add "/home/foo/Music/bar.ogg"
|
||||
|
||||
.. _command_addid:
|
||||
|
||||
:command:`addid {URI} [POSITION]`
|
||||
|
20
doc/user.rst
20
doc/user.rst
@@ -55,7 +55,7 @@ and unpack it (or `clone the git repository
|
||||
|
||||
In any case, you need:
|
||||
|
||||
* a C++17 compiler (e.g. GCC 8 or clang 5)
|
||||
* a C++17 compiler (e.g. GCC 8 or clang 7)
|
||||
* `Meson 0.49.0 <http://mesonbuild.com/>`__ and `Ninja
|
||||
<https://ninja-build.org/>`__
|
||||
* Boost 1.58
|
||||
@@ -141,6 +141,15 @@ Basically, there are two ways to compile :program:`MPD` for Windows:
|
||||
|
||||
This section is about the latter.
|
||||
|
||||
You need:
|
||||
|
||||
* `mingw-w64 <http://mingw-w64.org/doku.php>`__
|
||||
* `Meson 0.49.0 <http://mesonbuild.com/>`__ and `Ninja
|
||||
<https://ninja-build.org/>`__
|
||||
* cmake
|
||||
* pkg-config
|
||||
* quilt
|
||||
|
||||
Just like with the native build, unpack the :program:`MPD` source
|
||||
tarball and change into the directory. Then, instead of
|
||||
:program:`meson`, type:
|
||||
@@ -168,6 +177,11 @@ You need:
|
||||
|
||||
* Android SDK
|
||||
* `Android NDK r22 <https://developer.android.com/ndk/downloads>`_
|
||||
* `Meson 0.49.0 <http://mesonbuild.com/>`__ and `Ninja
|
||||
<https://ninja-build.org/>`__
|
||||
* cmake
|
||||
* pkg-config
|
||||
* quilt
|
||||
|
||||
Just like with the native build, unpack the :program:`MPD` source
|
||||
tarball and change into the directory. Then, instead of
|
||||
@@ -674,6 +688,8 @@ The State File
|
||||
- Specify the state file location. The parent directory must be writable by the :program:`MPD` user (+wx).
|
||||
* - **state_file_interval SECONDS**
|
||||
- Auto-save the state file this number of seconds after each state change. Defaults to 120 (2 minutes).
|
||||
* - **restore_paused yes|no**
|
||||
- If set to :samp:`yes`, then :program:`MPD` is put into pause mode instead of starting playback after startup. Default is :samp:`no`.
|
||||
|
||||
The Sticker Database
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -1106,7 +1122,7 @@ Support
|
||||
Getting Help
|
||||
^^^^^^^^^^^^
|
||||
|
||||
The :program:`MPD` project runs a `forum <https://forum.musicpd.org/>`_ and an IRC channel (#mpd on Freenode) for requesting help. Visit the MPD help page for details on how to get help.
|
||||
The :program:`MPD` project runs a `forum <https://forum.musicpd.org/>`_ and an IRC channel (#mpd on Libera.Chat) for requesting help. Visit the MPD help page for details on how to get help.
|
||||
|
||||
Common Problems
|
||||
^^^^^^^^^^^^^^^
|
||||
|
107
meson.build
107
meson.build
@@ -1,7 +1,7 @@
|
||||
project(
|
||||
'mpd',
|
||||
['c', 'cpp'],
|
||||
version: '0.22.4',
|
||||
version: '0.22.9',
|
||||
meson_version: '>= 0.49.0',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
@@ -24,8 +24,8 @@ c_compiler = meson.get_compiler('c')
|
||||
|
||||
if compiler.get_id() == 'gcc' and compiler.version().version_compare('<8')
|
||||
warning('Your GCC version is too old. You need at least version 8.')
|
||||
elif compiler.get_id() == 'clang' and compiler.version().version_compare('<5')
|
||||
warning('Your clang version is too old. You need at least version 5.')
|
||||
elif compiler.get_id() == 'clang' and compiler.version().version_compare('<7')
|
||||
warning('Your clang version is too old. You need at least version 7.')
|
||||
endif
|
||||
|
||||
version_conf = configuration_data()
|
||||
@@ -42,57 +42,64 @@ common_cppflags = [
|
||||
'-D_GNU_SOURCE',
|
||||
]
|
||||
|
||||
common_cflags = [
|
||||
]
|
||||
|
||||
common_cxxflags = [
|
||||
test_global_common_flags = [
|
||||
'-fvisibility=hidden',
|
||||
]
|
||||
|
||||
test_common_flags = [
|
||||
'-Wvla',
|
||||
'-Wdouble-promotion',
|
||||
|
||||
'-fvisibility=hidden',
|
||||
|
||||
'-ffast-math',
|
||||
'-ftree-vectorize',
|
||||
]
|
||||
|
||||
test_global_cxxflags = test_global_common_flags + [
|
||||
]
|
||||
|
||||
test_global_cflags = test_global_common_flags + [
|
||||
]
|
||||
|
||||
test_cxxflags = test_common_flags + [
|
||||
'-fno-threadsafe-statics',
|
||||
'-fmerge-all-constants',
|
||||
|
||||
'-Wmissing-declarations',
|
||||
'-Wshadow',
|
||||
'-Wpointer-arith',
|
||||
'-Wcast-qual',
|
||||
'-Wwrite-strings',
|
||||
'-Wsign-compare',
|
||||
'-Wcomma',
|
||||
'-Wcomma-subscript',
|
||||
'-Wextra-semi',
|
||||
'-Wmismatched-tags',
|
||||
'-Wmissing-declarations',
|
||||
'-Woverloaded-virtual',
|
||||
'-Wshadow',
|
||||
'-Wsign-promo',
|
||||
'-Wunused',
|
||||
'-Wvolatile',
|
||||
'-Wvirtual-inheritance',
|
||||
'-Wwrite-strings',
|
||||
|
||||
# a vtable without a dtor is just fine
|
||||
'-Wno-non-virtual-dtor',
|
||||
|
||||
# clang specific warning options:
|
||||
'-Wcomma',
|
||||
'-Wheader-hygiene',
|
||||
'-Winconsistent-missing-destructor-override',
|
||||
'-Wunreachable-code-break',
|
||||
'-Wunused',
|
||||
'-Wunreachable-code-aggressive',
|
||||
'-Wused-but-marked-unused',
|
||||
|
||||
'-Wno-non-virtual-dtor',
|
||||
]
|
||||
|
||||
if compiler.get_id() == 'clang'
|
||||
# Workaround for clang bug
|
||||
# https://bugs.llvm.org/show_bug.cgi?id=32611
|
||||
test_cxxflags += '-funwind-tables'
|
||||
if compiler.get_id() != 'gcc' or compiler.version().version_compare('>=9')
|
||||
# The GCC 8 implementation of this flag is buggy: it complains even
|
||||
# if "final" is present, which implies "override".
|
||||
test_cxxflags += '-Wsuggest-override'
|
||||
endif
|
||||
|
||||
test_cflags = test_common_flags + [
|
||||
'-Wcast-qual',
|
||||
'-Wmissing-prototypes',
|
||||
'-Wshadow',
|
||||
'-Wpointer-arith',
|
||||
'-Wstrict-prototypes',
|
||||
'-Wcast-qual',
|
||||
'-Wwrite-strings',
|
||||
'-pedantic',
|
||||
]
|
||||
|
||||
test_ldflags = [
|
||||
@@ -104,11 +111,11 @@ test_ldflags = [
|
||||
]
|
||||
|
||||
if get_option('buildtype') != 'debug'
|
||||
test_cxxflags += [
|
||||
test_global_cxxflags += [
|
||||
'-ffunction-sections',
|
||||
'-fdata-sections',
|
||||
]
|
||||
test_cflags += [
|
||||
test_global_cflags += [
|
||||
'-ffunction-sections',
|
||||
'-fdata-sections',
|
||||
]
|
||||
@@ -118,15 +125,20 @@ if get_option('buildtype') != 'debug'
|
||||
endif
|
||||
|
||||
if get_option('fuzzer')
|
||||
fuzzer_flags = ['-fsanitize=fuzzer,address,undefined']
|
||||
fuzzer_flags = ['-fsanitize=fuzzer']
|
||||
if get_option('b_sanitize') == 'none'
|
||||
fuzzer_flags += ['-fsanitize=address,undefined']
|
||||
endif
|
||||
add_global_arguments(fuzzer_flags, language: 'cpp')
|
||||
add_global_arguments(fuzzer_flags, language: 'c')
|
||||
add_global_link_arguments(fuzzer_flags, language: 'cpp')
|
||||
endif
|
||||
|
||||
add_global_arguments(common_cxxflags + compiler.get_supported_arguments(test_cxxflags), language: 'cpp')
|
||||
add_global_arguments(common_cflags + c_compiler.get_supported_arguments(test_cflags), language: 'c')
|
||||
add_global_link_arguments(compiler.get_supported_link_arguments(test_ldflags), language: 'cpp')
|
||||
add_global_arguments(compiler.get_supported_arguments(test_global_cxxflags), language: 'cpp')
|
||||
add_global_arguments(c_compiler.get_supported_arguments(test_global_cflags), language: 'c')
|
||||
add_project_arguments(compiler.get_supported_arguments(test_cxxflags), language: 'cpp')
|
||||
add_project_arguments(c_compiler.get_supported_arguments(test_cflags), language: 'c')
|
||||
add_project_link_arguments(compiler.get_supported_link_arguments(test_ldflags), language: 'cpp')
|
||||
|
||||
is_linux = host_machine.system() == 'linux'
|
||||
is_android = get_option('android_ndk') != ''
|
||||
@@ -140,10 +152,29 @@ endif
|
||||
|
||||
if is_windows
|
||||
common_cppflags += [
|
||||
'-DWIN32_LEAN_AND_MEAN',
|
||||
# enable Windows Vista APIs
|
||||
'-DWINVER=0x0600', '-D_WIN32_WINNT=0x0600',
|
||||
'-DSTRICT',
|
||||
|
||||
# enable Unicode support (TCHAR=wchar_t) in the Windows API (macro
|
||||
# "UNICODE) and the C library (macro "_UNICODE")
|
||||
'-DUNICODE', '-D_UNICODE',
|
||||
|
||||
# enable strict type checking in the Windows API headers
|
||||
'-DSTRICT',
|
||||
|
||||
# reduce header bloat by disabling obscure and obsolete Windows
|
||||
# APIs
|
||||
'-DWIN32_LEAN_AND_MEAN',
|
||||
|
||||
# disable more Windows APIs which are not used by MPD
|
||||
'-DNOGDI', '-DNOBITMAP', '-DNOCOMM',
|
||||
'-DNOUSER',
|
||||
|
||||
# reduce COM header bloat
|
||||
'-DCOM_NO_WINDOWS_H',
|
||||
|
||||
# disable Internet Explorer specific APIs
|
||||
'-D_WIN32_IE=0',
|
||||
]
|
||||
|
||||
subdir('win32')
|
||||
@@ -272,7 +303,6 @@ sources = [
|
||||
'src/LogInit.cxx',
|
||||
'src/ls.cxx',
|
||||
'src/Instance.cxx',
|
||||
'src/win32/Win32Main.cxx',
|
||||
'src/MusicBuffer.cxx',
|
||||
'src/MusicPipe.cxx',
|
||||
'src/MusicChunk.cxx',
|
||||
@@ -320,6 +350,12 @@ sources = [
|
||||
'src/PlaylistFile.cxx',
|
||||
]
|
||||
|
||||
if is_windows
|
||||
sources += [
|
||||
'src/win32/Win32Main.cxx',
|
||||
]
|
||||
endif
|
||||
|
||||
if not is_android
|
||||
sources += [
|
||||
'src/CommandLine.cxx',
|
||||
@@ -354,6 +390,7 @@ subdir('src/system')
|
||||
subdir('src/thread')
|
||||
subdir('src/net')
|
||||
subdir('src/event')
|
||||
subdir('src/win32')
|
||||
|
||||
subdir('src/apple')
|
||||
|
||||
|
@@ -21,3 +21,8 @@ class BoostProject(Project):
|
||||
dest = os.path.join(includedir, 'boost')
|
||||
shutil.rmtree(dest, ignore_errors=True)
|
||||
shutil.copytree(os.path.join(src, 'boost'), dest)
|
||||
|
||||
# touch the boost/version.hpp file to ensure it's newer than
|
||||
# the downloaded Boost tarball, to avoid reinstalling Boost on
|
||||
# every run
|
||||
os.utime(os.path.join(toolchain.install_prefix, self.installed))
|
||||
|
47
python/build/jack.py
Normal file
47
python/build/jack.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import os, shutil
|
||||
import re
|
||||
|
||||
from .project import Project
|
||||
|
||||
# This class installs just the public headers and a fake pkg-config
|
||||
# file which defines the macro "DYNAMIC_JACK". This tells MPD's JACK
|
||||
# output plugin to load the libjack64.dll dynamically using
|
||||
# LoadLibrary(). This kludge avoids the runtime DLL dependency for
|
||||
# users who don't use JACK, but still allows using the system JACK
|
||||
# client library.
|
||||
#
|
||||
# The problem with JACK is that it uses an extremely fragile shared
|
||||
# memory protocol to communicate with the daemon. One needs to use
|
||||
# daemon and client library from the same build. That's why we don't
|
||||
# build libjack statically here; it would probably not be compatible
|
||||
# with the user's JACK daemon.
|
||||
|
||||
class JackProject(Project):
|
||||
def __init__(self, url, md5, installed,
|
||||
**kwargs):
|
||||
m = re.match(r'.*/v([\d.]+)\.tar\.gz$', url)
|
||||
self.version = m.group(1)
|
||||
Project.__init__(self, url, md5, installed,
|
||||
name='jack2', version=self.version,
|
||||
base='jack2-' + self.version,
|
||||
**kwargs)
|
||||
|
||||
def build(self, toolchain):
|
||||
src = self.unpack(toolchain)
|
||||
|
||||
includes = ['jack.h', 'ringbuffer.h', 'systemdeps.h', 'transport.h', 'types.h', 'weakmacros.h']
|
||||
includedir = os.path.join(toolchain.install_prefix, 'include', 'jack')
|
||||
os.makedirs(includedir, exist_ok=True)
|
||||
|
||||
for i in includes:
|
||||
shutil.copyfile(os.path.join(src, 'common', 'jack', i),
|
||||
os.path.join(includedir, i))
|
||||
|
||||
with open(os.path.join(toolchain.install_prefix, 'lib', 'pkgconfig', 'jack.pc'), 'w') as f:
|
||||
print("prefix=" + toolchain.install_prefix, file=f)
|
||||
print("", file=f)
|
||||
print("Name: jack", file=f)
|
||||
print("Description: dummy", file=f)
|
||||
print("Version: " + self.version, file=f)
|
||||
print("Libs: ", file=f)
|
||||
print("Cflags: -DDYNAMIC_JACK", file=f)
|
@@ -9,6 +9,7 @@ from build.autotools import AutotoolsProject
|
||||
from build.ffmpeg import FfmpegProject
|
||||
from build.openssl import OpenSSLProject
|
||||
from build.boost import BoostProject
|
||||
from build.jack import JackProject
|
||||
|
||||
libmpdclient = MesonProject(
|
||||
'https://www.musicpd.org/download/libmpdclient/2/libmpdclient-2.19.tar.xz',
|
||||
@@ -149,8 +150,8 @@ gme = CmakeProject(
|
||||
)
|
||||
|
||||
ffmpeg = FfmpegProject(
|
||||
'http://ffmpeg.org/releases/ffmpeg-4.3.1.tar.xz',
|
||||
'ad009240d46e307b4e03a213a0f49c11b650e445b1f8be0dda2a9212b34d2ffb',
|
||||
'http://ffmpeg.org/releases/ffmpeg-4.4.tar.xz',
|
||||
'06b10a183ce5371f915c6bb15b7b1fffbe046e8275099c96affc29e17645d909',
|
||||
'lib/libavcodec.a',
|
||||
[
|
||||
'--disable-shared', '--enable-static',
|
||||
@@ -378,14 +379,14 @@ ffmpeg = FfmpegProject(
|
||||
)
|
||||
|
||||
openssl = OpenSSLProject(
|
||||
'https://www.openssl.org/source/openssl-3.0.0-alpha10.tar.gz',
|
||||
'b1699acf2148db31f12edf5ebfdf12a92bfd3f0e60538d169710408a3cd3b138',
|
||||
'https://www.openssl.org/source/openssl-3.0.0-alpha16.tar.gz',
|
||||
'08ce8244b59d75f40f91170dfcb012bf25309cdcb1fef9502e39d694f883d1d1',
|
||||
'include/openssl/ossl_typ.h',
|
||||
)
|
||||
|
||||
curl = AutotoolsProject(
|
||||
'http://curl.haxx.se/download/curl-7.74.0.tar.xz',
|
||||
'999d5f2c403cf6e25d58319fdd596611e455dd195208746bc6e6d197a77e878b',
|
||||
'https://curl.se/download/curl-7.76.1.tar.xz',
|
||||
'64bb5288c39f0840c07d077e30d9052e1cbb9fa6c2dc52523824cc859e679145',
|
||||
'lib/libcurl.a',
|
||||
[
|
||||
'--disable-shared', '--enable-static',
|
||||
@@ -407,6 +408,9 @@ curl = AutotoolsProject(
|
||||
'--disable-progress-meter',
|
||||
'--disable-alt-svc',
|
||||
'--without-gnutls', '--without-nss', '--without-libssh2',
|
||||
|
||||
# native Windows SSL/TLS support, option ignored on non-Windows builds
|
||||
'--with-schannel',
|
||||
],
|
||||
|
||||
patches='src/lib/curl/patches',
|
||||
@@ -440,8 +444,14 @@ libnfs = AutotoolsProject(
|
||||
autoreconf=True,
|
||||
)
|
||||
|
||||
jack = JackProject(
|
||||
'https://github.com/jackaudio/jack2/archive/v1.9.17.tar.gz',
|
||||
'38f674bbc57852a8eb3d9faa1f96a0912d26f7d5df14c11005ad499c8ae352f2',
|
||||
'lib/pkgconfig/jack.pc',
|
||||
)
|
||||
|
||||
boost = BoostProject(
|
||||
'https://dl.bintray.com/boostorg/release/1.75.0/source/boost_1_75_0.tar.bz2',
|
||||
'953db31e016db7bb207f11432bef7df100516eeb746843fa0486a222e3fd49cb',
|
||||
'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2',
|
||||
'f0397ba6e982c4450f27bf32a2a83292aba035b827a5623a14636ea583318c41',
|
||||
'include/boost/version.hpp',
|
||||
)
|
||||
|
@@ -20,7 +20,7 @@ class Project:
|
||||
self.base = base
|
||||
|
||||
if name is None or version is None:
|
||||
m = re.match(r'^([-\w]+)-(\d[\d.]*[a-z]?[\d.]*(?:-alpha\d+)?)$', self.base)
|
||||
m = re.match(r'^([-\w]+)-(\d[\d.]*[a-z]?[\d.]*(?:-alpha\d+)?)(\+.*)?$', self.base)
|
||||
if name is None: name = m.group(1)
|
||||
if version is None: version = m.group(2)
|
||||
|
||||
|
@@ -113,7 +113,7 @@ static void version()
|
||||
printf("Music Player Daemon " VERSION " (%s)"
|
||||
"\n"
|
||||
"Copyright 2003-2007 Warren Dukes <warren.dukes@gmail.com>\n"
|
||||
"Copyright 2008-2018 Max Kellermann <max.kellermann@gmail.com>\n"
|
||||
"Copyright 2008-2021 Max Kellermann <max.kellermann@gmail.com>\n"
|
||||
"This is free software; see the source for copying conditions. There is NO\n"
|
||||
"warranty; not even MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n",
|
||||
GIT_VERSION);
|
||||
|
@@ -477,6 +477,7 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
|
||||
#endif
|
||||
|
||||
ZeroconfInit(raw_config, instance.event_loop);
|
||||
AtScopeExit() { ZeroconfDeinit(); };
|
||||
|
||||
#ifdef ENABLE_DATABASE
|
||||
if (create_db) {
|
||||
@@ -537,9 +538,6 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
|
||||
instance.state_file->Write();
|
||||
|
||||
instance.BeginShutdownUpdate();
|
||||
|
||||
ZeroconfDeinit();
|
||||
|
||||
instance.BeginShutdownPartitions();
|
||||
}
|
||||
|
||||
|
@@ -42,9 +42,9 @@ RemoteTagCache::Lookup(const std::string &uri) noexcept
|
||||
std::unique_lock<Mutex> lock(mutex);
|
||||
|
||||
KeyMap::insert_commit_data hint;
|
||||
auto result = map.insert_check(uri, Item::Hash(), Item::Equal(), hint);
|
||||
if (result.second) {
|
||||
auto *item = new Item(*this, uri);
|
||||
auto [tag, value] = map.insert_check(uri, Item::Hash(), Item::Equal(), hint);
|
||||
if (value) {
|
||||
auto item = new Item(*this, uri);
|
||||
map.insert_commit(*item, hint);
|
||||
waiting_list.push_back(*item);
|
||||
lock.unlock();
|
||||
@@ -70,15 +70,13 @@ RemoteTagCache::Lookup(const std::string &uri) noexcept
|
||||
ItemResolved(*item);
|
||||
return;
|
||||
}
|
||||
} else if (result.first->scanner) {
|
||||
} else if (tag->scanner) {
|
||||
/* already scanning this one - no-op */
|
||||
} else {
|
||||
/* already finished: re-invoke the handler */
|
||||
|
||||
auto &item = *result.first;
|
||||
|
||||
idle_list.erase(waiting_list.iterator_to(item));
|
||||
invoke_list.push_back(item);
|
||||
idle_list.erase(waiting_list.iterator_to(*tag));
|
||||
invoke_list.push_back(*tag);
|
||||
|
||||
ScheduleInvokeHandlers();
|
||||
}
|
||||
|
@@ -25,6 +25,7 @@
|
||||
#include "client/Client.hxx"
|
||||
#include "protocol/Ack.hxx"
|
||||
#include "fs/AllocatedPath.hxx"
|
||||
#include "input/InputStream.hxx"
|
||||
#include "util/Compiler.h"
|
||||
#include "util/UriExtract.hxx"
|
||||
#include "LocateUri.hxx"
|
||||
@@ -32,8 +33,13 @@
|
||||
static void
|
||||
TagScanStream(const char *uri, TagHandler &handler)
|
||||
{
|
||||
if (!tag_stream_scan(uri, handler))
|
||||
Mutex mutex;
|
||||
|
||||
auto is = InputStream::OpenReady(uri, mutex);
|
||||
if (!tag_stream_scan(*is, handler))
|
||||
throw ProtocolError(ACK_ERROR_NO_EXIST, "Failed to load file");
|
||||
|
||||
ScanGenericTags(*is, handler);
|
||||
}
|
||||
|
||||
static void
|
||||
|
@@ -221,8 +221,8 @@ public:
|
||||
if (new_offset > size)
|
||||
throw std::runtime_error("Invalid seek offset");
|
||||
|
||||
offset = new_offset;
|
||||
skip = new_offset % ISO_BLOCKSIZE;
|
||||
offset = new_offset - skip;
|
||||
buffer.Clear();
|
||||
}
|
||||
};
|
||||
@@ -260,13 +260,13 @@ Iso9660InputStream::Read(std::unique_lock<Mutex> &,
|
||||
if (r.empty()) {
|
||||
/* the buffer is empty - read more data from the ISO file */
|
||||
|
||||
assert(offset % ISO_BLOCKSIZE == 0);
|
||||
assert((offset - skip) % ISO_BLOCKSIZE == 0);
|
||||
|
||||
const ScopeUnlock unlock(mutex);
|
||||
|
||||
const lsn_t read_lsn = lsn + offset / ISO_BLOCKSIZE;
|
||||
|
||||
if (read_size >= ISO_BLOCKSIZE) {
|
||||
if (read_size >= ISO_BLOCKSIZE && skip == 0) {
|
||||
/* big read - read right into the caller's buffer */
|
||||
|
||||
auto nbytes = iso->SeekRead(ptr, read_lsn,
|
||||
|
@@ -23,6 +23,7 @@
|
||||
#include "Message.hxx"
|
||||
#include "command/CommandResult.hxx"
|
||||
#include "command/CommandListBuilder.hxx"
|
||||
#include "input/LastInputStream.hxx"
|
||||
#include "tag/Mask.hxx"
|
||||
#include "event/FullyBufferedSocket.hxx"
|
||||
#include "event/TimerEvent.hxx"
|
||||
@@ -90,6 +91,13 @@ public:
|
||||
*/
|
||||
size_t binary_limit = 8192;
|
||||
|
||||
/**
|
||||
* This caches the last "albumart" InputStream instance, to
|
||||
* avoid repeating the search for each chunk requested by this
|
||||
* client.
|
||||
*/
|
||||
LastInputStream last_album_art;
|
||||
|
||||
private:
|
||||
static constexpr size_t MAX_SUBSCRIPTIONS = 16;
|
||||
|
||||
|
@@ -44,7 +44,8 @@ Client::Client(EventLoop &_loop, Partition &_partition,
|
||||
partition(&_partition),
|
||||
permission(_permission),
|
||||
uid(_uid),
|
||||
num(_num)
|
||||
num(_num),
|
||||
last_album_art(_loop)
|
||||
{
|
||||
timeout_event.Schedule(client_timeout);
|
||||
}
|
||||
|
@@ -61,7 +61,12 @@ Response::WriteBinary(ConstBuffer<void> payload) noexcept
|
||||
{
|
||||
assert(payload.size <= client.binary_limit);
|
||||
|
||||
return Format("binary: %zu\n", payload.size) &&
|
||||
return
|
||||
#ifdef _WIN32
|
||||
Format("binary: %lu\n", (unsigned long)payload.size) &&
|
||||
#else
|
||||
Format("binary: %zu\n", payload.size) &&
|
||||
#endif
|
||||
Write(payload.data, payload.size) &&
|
||||
Write("\n");
|
||||
}
|
||||
|
@@ -34,8 +34,7 @@ Client::Subscribe(const char *channel) noexcept
|
||||
if (num_subscriptions >= MAX_SUBSCRIPTIONS)
|
||||
return Client::SubscribeResult::FULL;
|
||||
|
||||
auto r = subscriptions.insert(channel);
|
||||
if (!r.second)
|
||||
if (!subscriptions.insert(channel).second)
|
||||
return Client::SubscribeResult::ALREADY;
|
||||
|
||||
++num_subscriptions;
|
||||
|
@@ -191,9 +191,16 @@ read_stream_art(Response &r, const char *uri, size_t offset)
|
||||
{
|
||||
const auto art_directory = PathTraitsUTF8::GetParent(uri);
|
||||
|
||||
Mutex mutex;
|
||||
// TODO: eliminate this const_cast
|
||||
auto &client = const_cast<Client &>(r.GetClient());
|
||||
|
||||
InputStreamPtr is = find_stream_art(art_directory, mutex);
|
||||
/* to avoid repeating the search for each chunk request by the
|
||||
same client, use the #LastInputStream class to cache the
|
||||
#InputStream instance */
|
||||
auto *is = client.last_album_art.Open(art_directory, [](std::string_view directory,
|
||||
Mutex &mutex){
|
||||
return find_stream_art(directory, mutex);
|
||||
});
|
||||
|
||||
if (is == nullptr) {
|
||||
r.Error(ACK_ERROR_NO_EXIST, "No file exists");
|
||||
@@ -219,12 +226,17 @@ read_stream_art(Response &r, const char *uri, size_t offset)
|
||||
|
||||
std::size_t read_size = 0;
|
||||
if (buffer_size > 0) {
|
||||
std::unique_lock<Mutex> lock(mutex);
|
||||
std::unique_lock<Mutex> lock(is->mutex);
|
||||
is->Seek(lock, offset);
|
||||
read_size = is->Read(lock, buffer.get(), buffer_size);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
r.Format("size: %lu\n", (unsigned long)art_file_size);
|
||||
#else
|
||||
r.Format("size: %" PRIoffset "\n", art_file_size);
|
||||
#endif
|
||||
|
||||
r.WriteBinary({buffer.get(), read_size});
|
||||
|
||||
return CommandResult::OK;
|
||||
@@ -306,7 +318,11 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
response.Format("size: %lu\n", (unsigned long)buffer.size);
|
||||
#else
|
||||
response.Format("size: %zu\n", buffer.size);
|
||||
#endif
|
||||
|
||||
if (mime_type != nullptr)
|
||||
response.Format("type: %s\n", mime_type);
|
||||
|
@@ -92,7 +92,7 @@ handle_load(Client &client, Request args, [[maybe_unused]] Response &r)
|
||||
auto &instance = client.GetInstance();
|
||||
const unsigned new_size = playlist.GetLength();
|
||||
for (unsigned i = old_size; i < new_size; ++i)
|
||||
instance.LookupRemoteTag(playlist.queue.Get(i).GetURI());
|
||||
instance.LookupRemoteTag(playlist.queue.Get(i).GetRealURI());
|
||||
|
||||
return CommandResult::OK;
|
||||
}
|
||||
|
@@ -326,6 +326,11 @@ CommandResult
|
||||
handle_move(Client &client, Request args, [[maybe_unused]] Response &r)
|
||||
{
|
||||
RangeArg range = args.ParseRange(0);
|
||||
if (range.IsOpenEnded()) {
|
||||
r.Error(ACK_ERROR_ARG, "Open-ended range not supported");
|
||||
return CommandResult::ERROR;
|
||||
}
|
||||
|
||||
int to = args.ParseInt(1);
|
||||
client.GetPartition().MoveRange(range.start, range.end, to);
|
||||
return CommandResult::OK;
|
||||
|
@@ -57,9 +57,9 @@ Print(Response &r, TagType group, const TagCountMap &m) noexcept
|
||||
{
|
||||
assert(unsigned(group) < TAG_NUM_OF_ITEM_TYPES);
|
||||
|
||||
for (const auto &i : m) {
|
||||
tag_print(r, group, i.first.c_str());
|
||||
PrintSearchStats(r, i.second);
|
||||
for (const auto &[tag, stats] : m) {
|
||||
tag_print(r, group, tag.c_str());
|
||||
PrintSearchStats(r, stats);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,7 @@ stats_visitor_song(SearchStats &stats, const LightSong &song) noexcept
|
||||
{
|
||||
stats.n_songs++;
|
||||
|
||||
const auto duration = song.GetDuration();
|
||||
if (!duration.IsNegative())
|
||||
if (const auto duration = song.GetDuration(); !duration.IsNegative())
|
||||
stats.total_duration += duration;
|
||||
}
|
||||
|
||||
@@ -77,8 +76,7 @@ static void
|
||||
CollectGroupCounts(TagCountMap &map, const Tag &tag,
|
||||
const char *value) noexcept
|
||||
{
|
||||
auto r = map.insert(std::make_pair(value, SearchStats()));
|
||||
SearchStats &s = r.first->second;
|
||||
auto &s = map.insert(std::make_pair(value, SearchStats())).first->second;
|
||||
++s.n_songs;
|
||||
if (!tag.duration.IsNegative())
|
||||
s.total_duration += tag.duration;
|
||||
|
@@ -195,11 +195,11 @@ PrintUniqueTags(Response &r, ConstBuffer<TagType> tag_types,
|
||||
const char *const name = tag_item_names[tag_types.front()];
|
||||
tag_types.pop_front();
|
||||
|
||||
for (const auto &i : map) {
|
||||
r.Format("%s: %s\n", name, i.first.c_str());
|
||||
for (const auto &[key, tag] : map) {
|
||||
r.Format("%s: %s\n", name, key.c_str());
|
||||
|
||||
if (!tag_types.empty())
|
||||
PrintUniqueTags(r, tag_types, i.second);
|
||||
PrintUniqueTags(r, tag_types, tag);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -424,6 +424,7 @@ SendGroup(mpd_connection *connection, TagType group)
|
||||
return mpd_search_add_group_tag(connection, tag);
|
||||
#else
|
||||
(void)connection;
|
||||
(void)group;
|
||||
|
||||
throw std::runtime_error("Grouping requires libmpdclient 2.12");
|
||||
#endif
|
||||
|
@@ -138,13 +138,10 @@ Directory::LookupDirectory(std::string_view _uri) noexcept
|
||||
|
||||
Directory *d = this;
|
||||
do {
|
||||
auto s = uri.Split(PathTraitsUTF8::SEPARATOR);
|
||||
if (s.first.empty())
|
||||
auto [name, rest] = uri.Split(PathTraitsUTF8::SEPARATOR);
|
||||
if (name.empty())
|
||||
break;
|
||||
|
||||
const auto name = s.first;
|
||||
const auto rest = s.second;
|
||||
|
||||
Directory *tmp = d->FindChild(name);
|
||||
if (tmp == nullptr)
|
||||
/* not found */
|
||||
|
@@ -29,6 +29,12 @@
|
||||
* a #LightSong, e.g. a merged #Tag.
|
||||
*/
|
||||
class ExportedSong : public LightSong {
|
||||
/**
|
||||
* A reference target for LightSong::tag, but it is only used
|
||||
* if this instance "owns" the #Tag. For instances referring
|
||||
* to a foreign #Tag instance (e.g. a Song::tag), this field
|
||||
* is not used (and empty).
|
||||
*/
|
||||
Tag tag_buffer;
|
||||
|
||||
public:
|
||||
@@ -37,6 +43,25 @@ public:
|
||||
ExportedSong(const char *_uri, Tag &&_tag) noexcept
|
||||
:LightSong(_uri, tag_buffer),
|
||||
tag_buffer(std::move(_tag)) {}
|
||||
|
||||
/* this custom move constructor is necessary so LightSong::tag
|
||||
points to this instance's #Tag field instead of leaving a
|
||||
dangling reference to the source object's #Tag field */
|
||||
ExportedSong(ExportedSong &&src) noexcept
|
||||
:LightSong(src,
|
||||
/* refer to tag_buffer only if the
|
||||
moved-from instance also owned the Tag
|
||||
which its LightSong::tag field refers
|
||||
to */
|
||||
OwnsTag() ? tag_buffer : src.tag),
|
||||
tag_buffer(std::move(src.tag_buffer)) {}
|
||||
|
||||
ExportedSong &operator=(ExportedSong &&) = delete;
|
||||
|
||||
private:
|
||||
bool OwnsTag() const noexcept {
|
||||
return &tag == &tag_buffer;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@@ -312,6 +312,29 @@ UpdateWalk::SkipSymlink(const Directory *directory,
|
||||
#endif
|
||||
}
|
||||
|
||||
static void
|
||||
LoadExcludeListOrThrow(const Storage &storage, const Directory &directory,
|
||||
ExcludeList &exclude_list)
|
||||
{
|
||||
Mutex mutex;
|
||||
auto is = InputStream::OpenReady(storage.MapUTF8(PathTraitsUTF8::Build(directory.GetPath(),
|
||||
".mpdignore")).c_str(),
|
||||
mutex);
|
||||
exclude_list.Load(std::move(is));
|
||||
}
|
||||
|
||||
static void
|
||||
LoadExcludeListOrLog(const Storage &storage, const Directory &directory,
|
||||
ExcludeList &exclude_list) noexcept
|
||||
{
|
||||
try {
|
||||
LoadExcludeListOrThrow(storage, directory, exclude_list);
|
||||
} catch (...) {
|
||||
if (!IsFileNotFound(std::current_exception()))
|
||||
LogError(std::current_exception());
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
UpdateWalk::UpdateDirectory(Directory &directory,
|
||||
const ExcludeList &exclude_list,
|
||||
@@ -331,17 +354,7 @@ UpdateWalk::UpdateDirectory(Directory &directory,
|
||||
}
|
||||
|
||||
ExcludeList child_exclude_list(exclude_list);
|
||||
|
||||
try {
|
||||
Mutex mutex;
|
||||
auto is = InputStream::OpenReady(storage.MapUTF8(PathTraitsUTF8::Build(directory.GetPath(),
|
||||
".mpdignore")).c_str(),
|
||||
mutex);
|
||||
child_exclude_list.Load(std::move(is));
|
||||
} catch (...) {
|
||||
if (!IsFileNotFound(std::current_exception()))
|
||||
LogError(std::current_exception());
|
||||
}
|
||||
LoadExcludeListOrLog(storage, directory, child_exclude_list);
|
||||
|
||||
if (!child_exclude_list.IsEmpty())
|
||||
RemoveExcludedFromDirectory(directory, child_exclude_list);
|
||||
@@ -427,26 +440,46 @@ UpdateWalk::DirectoryMakeUriParentChecked(Directory &root,
|
||||
StringView uri(_uri);
|
||||
|
||||
while (true) {
|
||||
auto s = uri.Split('/');
|
||||
const std::string_view name = s.first;
|
||||
const auto rest = s.second;
|
||||
auto [name, rest] = uri.Split('/');
|
||||
if (rest == nullptr)
|
||||
break;
|
||||
|
||||
if (!name.empty()) {
|
||||
directory = DirectoryMakeChildChecked(*directory,
|
||||
std::string(name).c_str(),
|
||||
s.first);
|
||||
name);
|
||||
if (directory == nullptr)
|
||||
break;
|
||||
}
|
||||
|
||||
uri = s.second;
|
||||
uri = rest;
|
||||
}
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
static void
|
||||
LoadExcludeLists(std::forward_list<ExcludeList> &lists,
|
||||
const Storage &storage, const Directory &directory) noexcept
|
||||
{
|
||||
assert(!lists.empty());
|
||||
|
||||
if (!directory.IsRoot())
|
||||
LoadExcludeLists(lists, storage, *directory.parent);
|
||||
|
||||
lists.emplace_front();
|
||||
LoadExcludeListOrLog(storage, directory, lists.front());
|
||||
}
|
||||
|
||||
static auto
|
||||
LoadExcludeLists(const Storage &storage, const Directory &directory) noexcept
|
||||
{
|
||||
std::forward_list<ExcludeList> lists;
|
||||
lists.emplace_front();
|
||||
LoadExcludeLists(lists, storage, directory);
|
||||
return lists;
|
||||
}
|
||||
|
||||
inline void
|
||||
UpdateWalk::UpdateUri(Directory &root, const char *uri) noexcept
|
||||
try {
|
||||
@@ -467,9 +500,8 @@ try {
|
||||
return;
|
||||
}
|
||||
|
||||
ExcludeList exclude_list;
|
||||
|
||||
UpdateDirectoryChild(*parent, exclude_list, name, info);
|
||||
const auto exclude_lists = LoadExcludeLists(storage, *parent);
|
||||
UpdateDirectoryChild(*parent, exclude_lists.front(), name, info);
|
||||
} catch (...) {
|
||||
LogError(std::current_exception());
|
||||
}
|
||||
|
@@ -464,6 +464,17 @@ FfmpegCheckTag(DecoderClient &client, InputStream *is,
|
||||
client.SubmitTag(is, tag.Commit());
|
||||
}
|
||||
|
||||
static bool
|
||||
IsSeekable(const AVFormatContext &format_context) noexcept
|
||||
{
|
||||
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 6, 100)
|
||||
return (format_context.ctx_flags & AVFMTCTX_UNSEEKABLE) != 0;
|
||||
#else
|
||||
(void)format_context;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void
|
||||
FfmpegDecode(DecoderClient &client, InputStream *input,
|
||||
AVFormatContext &format_context)
|
||||
@@ -521,7 +532,7 @@ FfmpegDecode(DecoderClient &client, InputStream *input,
|
||||
client.Ready(audio_format,
|
||||
input
|
||||
? input->IsSeekable()
|
||||
: (format_context.ctx_flags & AVFMTCTX_UNSEEKABLE) != 0,
|
||||
: IsSeekable(format_context),
|
||||
total_time);
|
||||
|
||||
FfmpegParseMetaData(client, format_context, audio_stream);
|
||||
@@ -648,6 +659,8 @@ ffmpeg_scan_stream(InputStream &is, TagHandler &handler)
|
||||
return FfmpegScanStream(*f, handler);
|
||||
}
|
||||
|
||||
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100)
|
||||
|
||||
static void
|
||||
ffmpeg_uri_decode(DecoderClient &client, const char *uri)
|
||||
{
|
||||
@@ -679,6 +692,8 @@ ffmpeg_protocols() noexcept
|
||||
return protocols;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
* A list of extensions found for the formats supported by ffmpeg.
|
||||
* This list is current as of 02-23-09; To find out if there are more
|
||||
@@ -802,6 +817,8 @@ static const char *const ffmpeg_mime_types[] = {
|
||||
constexpr DecoderPlugin ffmpeg_decoder_plugin =
|
||||
DecoderPlugin("ffmpeg", ffmpeg_decode, ffmpeg_scan_stream)
|
||||
.WithInit(ffmpeg_init, ffmpeg_finish)
|
||||
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100)
|
||||
.WithProtocols(ffmpeg_protocols, ffmpeg_uri_decode)
|
||||
#endif
|
||||
.WithSuffixes(ffmpeg_suffixes)
|
||||
.WithMimeTypes(ffmpeg_mime_types);
|
||||
|
@@ -30,11 +30,22 @@ extern "C" {
|
||||
#include <libavutil/dict.h>
|
||||
}
|
||||
|
||||
/**
|
||||
* FFmpeg specific tag name mappings, as supported by
|
||||
* libavformat/id3v2.c, libavformat/mov.c and others.
|
||||
*/
|
||||
static constexpr struct tag_table ffmpeg_tags[] = {
|
||||
{ "year", TAG_DATE },
|
||||
{ "author-sort", TAG_ARTIST_SORT },
|
||||
/* from libavformat/id3v2.c, libavformat/mov.c */
|
||||
{ "album_artist", TAG_ALBUM_ARTIST },
|
||||
{ "album_artist-sort", TAG_ALBUM_ARTIST_SORT },
|
||||
|
||||
/* from libavformat/id3v2.c */
|
||||
{ "album-sort", TAG_ALBUM_SORT },
|
||||
{ "artist-sort", TAG_ARTIST_SORT },
|
||||
|
||||
/* from libavformat/mov.c */
|
||||
{ "sort_album_artist", TAG_ALBUM_ARTIST_SORT },
|
||||
{ "sort_album", TAG_ALBUM_SORT },
|
||||
{ "sort_artist", TAG_ARTIST_SORT },
|
||||
|
||||
/* sentinel */
|
||||
{ nullptr, TAG_NUM_OF_ITEM_TYPES }
|
||||
|
@@ -344,7 +344,7 @@ gme_container_scan(Path path_fs)
|
||||
|
||||
static const char *const gme_suffixes[] = {
|
||||
"ay", "gbs", "gym", "hes", "kss", "nsf",
|
||||
"nsfe", "sap", "spc", "vgm", "vgz",
|
||||
"nsfe", "rsn", "sap", "spc", "vgm", "vgz",
|
||||
nullptr
|
||||
};
|
||||
|
||||
|
@@ -889,8 +889,6 @@ inline bool
|
||||
MadDecoder::HandleCurrentFrame() noexcept
|
||||
{
|
||||
switch (mute_frame) {
|
||||
DecoderCommand cmd;
|
||||
|
||||
case MadDecoderMuteFrame::SKIP:
|
||||
mute_frame = MadDecoderMuteFrame::NONE;
|
||||
break;
|
||||
@@ -899,8 +897,8 @@ MadDecoder::HandleCurrentFrame() noexcept
|
||||
mute_frame = MadDecoderMuteFrame::NONE;
|
||||
UpdateTimerNextFrame();
|
||||
break;
|
||||
case MadDecoderMuteFrame::NONE:
|
||||
cmd = SynthAndSubmit();
|
||||
case MadDecoderMuteFrame::NONE: {
|
||||
const auto cmd = SynthAndSubmit();
|
||||
UpdateTimerNextFrame();
|
||||
if (cmd == DecoderCommand::SEEK) {
|
||||
assert(input_stream.IsSeekable());
|
||||
@@ -922,6 +920,7 @@ MadDecoder::HandleCurrentFrame() noexcept
|
||||
} else if (cmd != DecoderCommand::NONE)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@@ -456,7 +456,7 @@ sidplay_file_decode(DecoderClient &client, Path path_fs)
|
||||
} while (cmd != DecoderCommand::STOP);
|
||||
}
|
||||
|
||||
static AllocatedString<char>
|
||||
static AllocatedString
|
||||
Windows1252ToUTF8(const char *s) noexcept
|
||||
{
|
||||
#ifdef HAVE_ICU_CONVERTER
|
||||
@@ -469,9 +469,9 @@ Windows1252ToUTF8(const char *s) noexcept
|
||||
* Fallback to not transcoding windows-1252 to utf-8, that may result
|
||||
* in invalid utf-8 unless nonprintable characters are replaced.
|
||||
*/
|
||||
auto t = AllocatedString<char>::Duplicate(s);
|
||||
AllocatedString t(s);
|
||||
|
||||
for (size_t i = 0; t[i] != AllocatedString<char>::SENTINEL; i++)
|
||||
for (size_t i = 0; t[i] != AllocatedString::SENTINEL; i++)
|
||||
if (!IsPrintableASCII(t[i]))
|
||||
t[i] = '?';
|
||||
|
||||
@@ -479,7 +479,7 @@ Windows1252ToUTF8(const char *s) noexcept
|
||||
}
|
||||
|
||||
gcc_pure
|
||||
static AllocatedString<char>
|
||||
static AllocatedString
|
||||
GetInfoString(const SidTuneInfo &info, unsigned i) noexcept
|
||||
{
|
||||
#ifdef HAVE_SIDPLAYFP
|
||||
@@ -496,7 +496,7 @@ GetInfoString(const SidTuneInfo &info, unsigned i) noexcept
|
||||
}
|
||||
|
||||
gcc_pure
|
||||
static AllocatedString<char>
|
||||
static AllocatedString
|
||||
GetDateString(const SidTuneInfo &info) noexcept
|
||||
{
|
||||
/*
|
||||
@@ -507,12 +507,12 @@ GetDateString(const SidTuneInfo &info) noexcept
|
||||
* author or group> may be for example Rob Hubbard. A full field
|
||||
* may be for example "1987 Rob Hubbard".
|
||||
*/
|
||||
AllocatedString<char> release = GetInfoString(info, 2);
|
||||
AllocatedString release = GetInfoString(info, 2);
|
||||
|
||||
/* Keep the <year> part only for the date. */
|
||||
for (size_t i = 0; release[i] != AllocatedString<char>::SENTINEL; i++)
|
||||
for (size_t i = 0; release[i] != AllocatedString::SENTINEL; i++)
|
||||
if (std::isspace(release[i])) {
|
||||
release[i] = AllocatedString<char>::SENTINEL;
|
||||
release[i] = AllocatedString::SENTINEL;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@@ -36,7 +36,7 @@ BufferedSocket::DirectRead(void *data, size_t length) noexcept
|
||||
}
|
||||
|
||||
const auto code = GetSocketError();
|
||||
if (IsSocketErrorAgain(code))
|
||||
if (IsSocketErrorReceiveWouldBlock(code))
|
||||
return 0;
|
||||
|
||||
if (IsSocketErrorClosed(code))
|
||||
|
@@ -31,7 +31,7 @@ FullyBufferedSocket::DirectWrite(const void *data, size_t length) noexcept
|
||||
const auto nbytes = GetSocket().Write((const char *)data, length);
|
||||
if (gcc_unlikely(nbytes < 0)) {
|
||||
const auto code = GetSocketError();
|
||||
if (IsSocketErrorAgain(code))
|
||||
if (IsSocketErrorSendWouldBlock(code))
|
||||
return 0;
|
||||
|
||||
IdleMonitor::Cancel();
|
||||
|
@@ -24,7 +24,7 @@
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#include <windows.h>
|
||||
#include <fileapi.h>
|
||||
#include <tchar.h>
|
||||
|
||||
/**
|
||||
|
36
src/fs/Glob.cxx
Normal file
36
src/fs/Glob.cxx
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2003-2021 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifdef _WIN32
|
||||
// COM needs the "MSG" typedef, and shlwapi.h includes COM headers
|
||||
#undef NOUSER
|
||||
#endif
|
||||
|
||||
#include "Glob.hxx"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <shlwapi.h>
|
||||
|
||||
bool
|
||||
Glob::Check(const char *name_fs) const noexcept
|
||||
{
|
||||
return PathMatchSpecA(name_fs, pattern.c_str());
|
||||
}
|
||||
|
||||
#endif
|
@@ -24,45 +24,44 @@
|
||||
|
||||
#ifdef HAVE_FNMATCH
|
||||
#define HAVE_CLASS_GLOB
|
||||
#include <string>
|
||||
#include <fnmatch.h>
|
||||
#elif defined(_WIN32)
|
||||
#define HAVE_CLASS_GLOB
|
||||
#include <string>
|
||||
#include <shlwapi.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_CLASS_GLOB
|
||||
#include "util/Compiler.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* A pattern that matches file names. It may contain shell wildcards
|
||||
* (asterisk and question mark).
|
||||
*/
|
||||
class Glob {
|
||||
#if defined(HAVE_FNMATCH) || defined(_WIN32)
|
||||
std::string pattern;
|
||||
#endif
|
||||
|
||||
public:
|
||||
#if defined(HAVE_FNMATCH) || defined(_WIN32)
|
||||
explicit Glob(const char *_pattern)
|
||||
:pattern(_pattern) {}
|
||||
|
||||
Glob(Glob &&other)
|
||||
:pattern(std::move(other.pattern)) {}
|
||||
#endif
|
||||
Glob(Glob &&other) noexcept = default;
|
||||
Glob &operator=(Glob &&other) noexcept = default;
|
||||
|
||||
gcc_pure
|
||||
bool Check(const char *name_fs) const noexcept {
|
||||
#ifdef HAVE_FNMATCH
|
||||
return fnmatch(pattern.c_str(), name_fs, 0) == 0;
|
||||
#elif defined(_WIN32)
|
||||
return PathMatchSpecA(name_fs, pattern.c_str());
|
||||
#endif
|
||||
}
|
||||
bool Check(const char *name_fs) const noexcept;
|
||||
};
|
||||
|
||||
#endif
|
||||
#ifdef HAVE_FNMATCH
|
||||
|
||||
inline bool
|
||||
Glob::Check(const char *name_fs) const noexcept
|
||||
{
|
||||
return fnmatch(pattern.c_str(), name_fs, 0) == 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif /* HAVE_CLASS_GLOB */
|
||||
|
||||
#endif
|
||||
|
@@ -29,7 +29,7 @@
|
||||
NarrowPath::NarrowPath(Path _path) noexcept
|
||||
:value(WideCharToMultiByte(CP_ACP, _path.c_str()))
|
||||
{
|
||||
if (value.IsNull())
|
||||
if (value == nullptr)
|
||||
/* fall back to empty string */
|
||||
value = Value::Empty();
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@
|
||||
*/
|
||||
class NarrowPath {
|
||||
#ifdef _UNICODE
|
||||
using Value = AllocatedString<>;
|
||||
using Value = AllocatedString;
|
||||
#else
|
||||
using Value = StringPointer<>;
|
||||
#endif
|
||||
|
@@ -17,6 +17,10 @@
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifdef _WIN32
|
||||
#undef NOUSER // COM needs the "MSG" typedef, and shlobj.h includes COM headers
|
||||
#endif
|
||||
|
||||
#include "StandardDirectory.hxx"
|
||||
#include "FileSystem.hxx"
|
||||
#include "XDG.hxx"
|
||||
|
@@ -42,7 +42,10 @@
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <fileapi.h>
|
||||
#include <windef.h> // for HWND (needed by winbase.h)
|
||||
#include <handleapi.h> // for INVALID_HANDLE_VALUE
|
||||
#include <winbase.h> // for FILE_END
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(ANDROID)
|
||||
|
@@ -35,7 +35,10 @@
|
||||
#include "util/Compiler.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <fileapi.h>
|
||||
#include <handleapi.h> // for INVALID_HANDLE_VALUE
|
||||
#include <windef.h> // for HWND (needed by winbase.h)
|
||||
#include <winbase.h> // for FILE_CURRENT
|
||||
#else
|
||||
#include "io/UniqueFileDescriptor.hxx"
|
||||
#endif
|
||||
|
@@ -3,6 +3,7 @@ fs_sources = [
|
||||
'Traits.cxx',
|
||||
'Config.cxx',
|
||||
'Charset.cxx',
|
||||
'Glob.cxx',
|
||||
'Path.cxx',
|
||||
'Path2.cxx',
|
||||
'AllocatedPath.cxx',
|
||||
|
46
src/input/LastInputStream.cxx
Normal file
46
src/input/LastInputStream.cxx
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2003-2021 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "LastInputStream.hxx"
|
||||
#include "InputStream.hxx"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
LastInputStream::LastInputStream(EventLoop &event_loop) noexcept
|
||||
:close_timer(event_loop, BIND_THIS_METHOD(OnCloseTimer))
|
||||
{
|
||||
}
|
||||
|
||||
LastInputStream::~LastInputStream() noexcept = default;
|
||||
|
||||
void
|
||||
LastInputStream::Close() noexcept
|
||||
{
|
||||
uri.clear();
|
||||
is.reset();
|
||||
close_timer.Cancel();
|
||||
}
|
||||
|
||||
void
|
||||
LastInputStream::OnCloseTimer() noexcept
|
||||
{
|
||||
assert(is);
|
||||
|
||||
is.reset();
|
||||
}
|
86
src/input/LastInputStream.hxx
Normal file
86
src/input/LastInputStream.hxx
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2003-2021 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifndef MPD_LAST_INPUT_STREAM_HXX
|
||||
#define MPD_LAST_INPUT_STREAM_HXX
|
||||
|
||||
#include "Ptr.hxx"
|
||||
#include "thread/Mutex.hxx"
|
||||
#include "event/TimerEvent.hxx"
|
||||
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* A helper class which maintains an #InputStream that is opened once
|
||||
* and may be reused later for some time. It will be closed
|
||||
* automatically after some time.
|
||||
*
|
||||
* This class is not thread-safe. All methods must be called on the
|
||||
* thread which runs the #EventLoop.
|
||||
*/
|
||||
class LastInputStream {
|
||||
std::string uri;
|
||||
|
||||
Mutex mutex;
|
||||
|
||||
InputStreamPtr is;
|
||||
|
||||
TimerEvent close_timer;
|
||||
|
||||
public:
|
||||
explicit LastInputStream(EventLoop &event_loop) noexcept;
|
||||
~LastInputStream() noexcept;
|
||||
|
||||
/**
|
||||
* Open an #InputStream instance with the given opener
|
||||
* function, but returns the cached instance if it matches.
|
||||
*
|
||||
* This object keeps owning the #InputStream; the caller shall
|
||||
* not close it.
|
||||
*/
|
||||
template<typename U, typename O>
|
||||
InputStream *Open(U &&new_uri, O &&open) {
|
||||
if (new_uri == uri) {
|
||||
if (is)
|
||||
/* refresh the timeout */
|
||||
ScheduleClose();
|
||||
|
||||
return is.get();
|
||||
}
|
||||
|
||||
Close();
|
||||
|
||||
is = open(new_uri, mutex);
|
||||
uri = std::forward<U>(new_uri);
|
||||
if (is)
|
||||
ScheduleClose();
|
||||
return is.get();
|
||||
}
|
||||
|
||||
void Close() noexcept;
|
||||
|
||||
private:
|
||||
void ScheduleClose() noexcept {
|
||||
close_timer.Schedule(std::chrono::seconds(20));
|
||||
}
|
||||
|
||||
void OnCloseTimer() noexcept;
|
||||
};
|
||||
|
||||
#endif
|
@@ -8,6 +8,7 @@ input_api = static_library(
|
||||
'ThreadInputStream.cxx',
|
||||
'AsyncInputStream.cxx',
|
||||
'ProxyInputStream.cxx',
|
||||
'LastInputStream.cxx',
|
||||
include_directories: inc,
|
||||
dependencies: [
|
||||
boost_dep,
|
||||
|
@@ -402,8 +402,8 @@ CurlInputStream::CurlInputStream(EventLoop &event_loop, const char *_url,
|
||||
{
|
||||
request_headers.Append("Icy-Metadata: 1");
|
||||
|
||||
for (const auto &i : headers)
|
||||
request_headers.Append((i.first + ":" + i.second).c_str());
|
||||
for (const auto &[key, header] : headers)
|
||||
request_headers.Append((key + ":" + header).c_str());
|
||||
}
|
||||
|
||||
CurlInputStream::~CurlInputStream() noexcept
|
||||
@@ -421,6 +421,10 @@ CurlInputStream::InitEasy()
|
||||
request->SetOption(CURLOPT_MAXREDIRS, 5L);
|
||||
request->SetOption(CURLOPT_FAILONERROR, 1L);
|
||||
|
||||
/* this option eliminates the probe request when
|
||||
username/password are specified */
|
||||
request->SetOption(CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
|
||||
|
||||
if (proxy != nullptr)
|
||||
request->SetOption(CURLOPT_PROXY, proxy);
|
||||
|
||||
|
@@ -174,8 +174,8 @@ QobuzClient::MakeUrl(const char *object, const char *method,
|
||||
uri += method;
|
||||
|
||||
QueryStringBuilder q;
|
||||
for (const auto &i : query)
|
||||
q(uri, i.first.c_str(), i.second.c_str());
|
||||
for (const auto &[key, url] : query)
|
||||
q(uri, key.c_str(), url.c_str());
|
||||
|
||||
q(uri, "app_id", app_id);
|
||||
return uri;
|
||||
@@ -195,11 +195,11 @@ QobuzClient::MakeSignedUrl(const char *object, const char *method,
|
||||
QueryStringBuilder q;
|
||||
std::string concatenated_query(object);
|
||||
concatenated_query += method;
|
||||
for (const auto &i : query) {
|
||||
q(uri, i.first.c_str(), i.second.c_str());
|
||||
for (const auto &[key, url] : query) {
|
||||
q(uri, key.c_str(), url.c_str());
|
||||
|
||||
concatenated_query += i.first;
|
||||
concatenated_query += i.second;
|
||||
concatenated_query += key;
|
||||
concatenated_query += url;
|
||||
}
|
||||
|
||||
q(uri, "app_id", app_id);
|
||||
|
@@ -172,7 +172,15 @@ FileDescriptor::CreatePipe(FileDescriptor &r, FileDescriptor &w) noexcept
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
#ifdef _WIN32
|
||||
|
||||
void
|
||||
FileDescriptor::SetBinaryMode() noexcept
|
||||
{
|
||||
_setmode(fd, _O_BINARY);
|
||||
}
|
||||
|
||||
#else // !_WIN32
|
||||
|
||||
bool
|
||||
FileDescriptor::CreatePipeNonBlock(FileDescriptor &r,
|
||||
|
@@ -148,10 +148,13 @@ public:
|
||||
#ifdef _WIN32
|
||||
void EnableCloseOnExec() noexcept {}
|
||||
void DisableCloseOnExec() noexcept {}
|
||||
void SetBinaryMode() noexcept;
|
||||
#else
|
||||
static bool CreatePipeNonBlock(FileDescriptor &r,
|
||||
FileDescriptor &w) noexcept;
|
||||
|
||||
void SetBinaryMode() noexcept {}
|
||||
|
||||
/**
|
||||
* Enable non-blocking mode on this file descriptor.
|
||||
*/
|
||||
|
@@ -36,16 +36,16 @@ EncodeForm(CURL *curl,
|
||||
{
|
||||
std::string result;
|
||||
|
||||
for (const auto &i : fields) {
|
||||
for (const auto &[key, field] : fields) {
|
||||
if (!result.empty())
|
||||
result.push_back('&');
|
||||
|
||||
result.append(i.first);
|
||||
result.append(key);
|
||||
result.push_back('=');
|
||||
|
||||
if (!i.second.empty()) {
|
||||
CurlString value(curl_easy_escape(curl, i.second.data(),
|
||||
i.second.length()));
|
||||
if (!field.empty()) {
|
||||
CurlString value(
|
||||
curl_easy_escape(curl, field.data(), field.length()));
|
||||
if (value)
|
||||
result.append(value);
|
||||
}
|
||||
|
@@ -1,19 +1,20 @@
|
||||
diff -ur curl-7.63.0.orig/lib/url.c curl-7.63.0/lib/url.c
|
||||
--- curl-7.63.0.orig/lib/url.c 2019-01-21 10:15:51.368019445 +0100
|
||||
+++ curl-7.63.0/lib/url.c 2019-01-21 10:19:16.307523984 +0100
|
||||
@@ -3057,6 +3057,7 @@
|
||||
Index: curl-7.71.1/lib/url.c
|
||||
===================================================================
|
||||
--- curl-7.71.1.orig/lib/url.c
|
||||
+++ curl-7.71.1/lib/url.c
|
||||
@@ -2871,6 +2871,7 @@
|
||||
}
|
||||
|
||||
conn->bits.netrc = FALSE;
|
||||
+#ifndef __BIONIC__
|
||||
if(data->set.use_netrc != CURL_NETRC_IGNORED &&
|
||||
(!*userp || !**userp || !*passwdp || !**passwdp)) {
|
||||
if(data->set.use_netrc && !data->set.str[STRING_USERNAME]) {
|
||||
bool netrc_user_changed = FALSE;
|
||||
@@ -3090,6 +3091,7 @@
|
||||
}
|
||||
bool netrc_passwd_changed = FALSE;
|
||||
@@ -2895,6 +2896,7 @@
|
||||
conn->bits.user_passwd = TRUE; /* enable user+password */
|
||||
}
|
||||
}
|
||||
+#endif
|
||||
|
||||
/* for updated strings, we update them in the URL */
|
||||
if(user_changed) {
|
||||
if(*userp) {
|
||||
|
@@ -38,13 +38,13 @@
|
||||
|
||||
#include <string.h>
|
||||
|
||||
AllocatedString<>
|
||||
AllocatedString
|
||||
IcuCaseFold(std::string_view src) noexcept
|
||||
try {
|
||||
#ifdef HAVE_ICU
|
||||
const auto u = UCharFromUTF8(src);
|
||||
if (u.IsNull())
|
||||
return AllocatedString<>::Duplicate(src);
|
||||
return AllocatedString(src);
|
||||
|
||||
AllocatedArray<UChar> folded(u.size() * 2U);
|
||||
|
||||
@@ -54,7 +54,7 @@ try {
|
||||
U_FOLD_CASE_DEFAULT,
|
||||
&error_code);
|
||||
if (folded_length == 0 || error_code != U_ZERO_ERROR)
|
||||
return AllocatedString<>::Duplicate(src);
|
||||
return AllocatedString(src);
|
||||
|
||||
folded.SetSize(folded_length);
|
||||
return UCharToUTF8({folded.begin(), folded.size()});
|
||||
@@ -63,7 +63,7 @@ try {
|
||||
#error not implemented
|
||||
#endif
|
||||
} catch (...) {
|
||||
return AllocatedString<>::Duplicate(src);
|
||||
return AllocatedString(src);
|
||||
}
|
||||
|
||||
#endif /* HAVE_ICU_CASE_FOLD */
|
||||
|
@@ -27,9 +27,9 @@
|
||||
|
||||
#include <string_view>
|
||||
|
||||
template<typename T> class AllocatedString;
|
||||
class AllocatedString;
|
||||
|
||||
AllocatedString<char>
|
||||
AllocatedString
|
||||
IcuCaseFold(std::string_view src) noexcept;
|
||||
|
||||
#endif
|
||||
|
@@ -88,7 +88,7 @@ IcuCollate(std::string_view a, std::string_view b) noexcept
|
||||
b.data(), b.size(), &code);
|
||||
|
||||
#elif defined(_WIN32)
|
||||
AllocatedString<wchar_t> wa = nullptr, wb = nullptr;
|
||||
BasicAllocatedString<wchar_t> wa, wb;
|
||||
|
||||
try {
|
||||
wa = MultiByteToWideChar(CP_UTF8, a);
|
||||
|
@@ -46,7 +46,7 @@ IcuCompare::IcuCompare(std::string_view _needle) noexcept
|
||||
#else
|
||||
|
||||
IcuCompare::IcuCompare(std::string_view _needle) noexcept
|
||||
:needle(AllocatedString<>::Duplicate(_needle)) {}
|
||||
:needle(_needle) {}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -56,7 +56,7 @@ IcuCompare::operator==(const char *haystack) const noexcept
|
||||
#ifdef HAVE_ICU_CASE_FOLD
|
||||
return StringIsEqual(IcuCaseFold(haystack).c_str(), needle.c_str());
|
||||
#elif defined(_WIN32)
|
||||
if (needle.IsNull())
|
||||
if (needle == nullptr)
|
||||
/* the MultiByteToWideChar() call in the constructor
|
||||
has failed, so let's always fail the comparison */
|
||||
return false;
|
||||
@@ -83,7 +83,7 @@ IcuCompare::IsIn(const char *haystack) const noexcept
|
||||
return StringFind(IcuCaseFold(haystack).c_str(),
|
||||
needle.c_str()) != nullptr;
|
||||
#elif defined(_WIN32)
|
||||
if (needle.IsNull())
|
||||
if (needle == nullptr)
|
||||
/* the MultiByteToWideChar() call in the constructor
|
||||
has failed, so let's always fail the comparison */
|
||||
return false;
|
||||
|
@@ -38,11 +38,11 @@ class IcuCompare {
|
||||
#ifdef _WIN32
|
||||
/* Windows API functions work with wchar_t strings, so let's
|
||||
cache the MultiByteToWideChar() result for performance */
|
||||
AllocatedString<wchar_t> needle;
|
||||
#else
|
||||
AllocatedString<> needle;
|
||||
using AllocatedString = BasicAllocatedString<wchar_t>;
|
||||
#endif
|
||||
|
||||
AllocatedString needle;
|
||||
|
||||
public:
|
||||
IcuCompare():needle(nullptr) {}
|
||||
|
||||
@@ -50,12 +50,12 @@ public:
|
||||
|
||||
IcuCompare(const IcuCompare &src) noexcept
|
||||
:needle(src
|
||||
? src.needle.Clone()
|
||||
? AllocatedString(src.needle)
|
||||
: nullptr) {}
|
||||
|
||||
IcuCompare &operator=(const IcuCompare &src) noexcept {
|
||||
needle = src
|
||||
? src.needle.Clone()
|
||||
? AllocatedString(src.needle)
|
||||
: nullptr;
|
||||
return *this;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ public:
|
||||
|
||||
gcc_pure
|
||||
operator bool() const noexcept {
|
||||
return !needle.IsNull();
|
||||
return needle != nullptr;
|
||||
}
|
||||
|
||||
gcc_pure
|
||||
|
@@ -77,7 +77,7 @@ IcuConverter::Create(const char *charset)
|
||||
#ifdef HAVE_ICU
|
||||
#elif defined(HAVE_ICONV)
|
||||
|
||||
static AllocatedString<char>
|
||||
static AllocatedString
|
||||
DoConvert(iconv_t conv, std::string_view src)
|
||||
{
|
||||
// TODO: dynamic buffer?
|
||||
@@ -95,12 +95,12 @@ DoConvert(iconv_t conv, std::string_view src)
|
||||
if (in_left > 0)
|
||||
throw std::runtime_error("Charset conversion failed");
|
||||
|
||||
return AllocatedString<>::Duplicate({buffer, sizeof(buffer) - out_left});
|
||||
return AllocatedString({buffer, sizeof(buffer) - out_left});
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
AllocatedString<char>
|
||||
AllocatedString
|
||||
IcuConverter::ToUTF8(std::string_view s) const
|
||||
{
|
||||
#ifdef HAVE_ICU
|
||||
@@ -128,7 +128,7 @@ IcuConverter::ToUTF8(std::string_view s) const
|
||||
#endif
|
||||
}
|
||||
|
||||
AllocatedString<char>
|
||||
AllocatedString
|
||||
IcuConverter::FromUTF8(std::string_view s) const
|
||||
{
|
||||
#ifdef HAVE_ICU
|
||||
@@ -151,7 +151,7 @@ IcuConverter::FromUTF8(std::string_view s) const
|
||||
throw std::runtime_error(FormatString("Failed to convert from Unicode: %s",
|
||||
u_errorName(code)).c_str());
|
||||
|
||||
return AllocatedString<>::Duplicate({buffer, size_t(target - buffer)});
|
||||
return AllocatedString({buffer, size_t(target - buffer)});
|
||||
|
||||
#elif defined(HAVE_ICONV)
|
||||
return DoConvert(from_utf8, s);
|
||||
|
@@ -40,7 +40,7 @@
|
||||
struct UConverter;
|
||||
#endif
|
||||
|
||||
template<typename T> class AllocatedString;
|
||||
class AllocatedString;
|
||||
|
||||
/**
|
||||
* This class can convert strings with a certain character set to and
|
||||
@@ -85,7 +85,7 @@ public:
|
||||
* Throws std::runtime_error on error.
|
||||
*/
|
||||
gcc_nonnull_all
|
||||
AllocatedString<char> ToUTF8(std::string_view s) const;
|
||||
AllocatedString ToUTF8(std::string_view s) const;
|
||||
|
||||
/**
|
||||
* Convert the string from UTF-8.
|
||||
@@ -93,7 +93,7 @@ public:
|
||||
* Throws std::runtime_error on error.
|
||||
*/
|
||||
gcc_nonnull_all
|
||||
AllocatedString<char> FromUTF8(std::string_view s) const;
|
||||
AllocatedString FromUTF8(std::string_view s) const;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@@ -48,7 +48,7 @@ UCharFromUTF8(std::string_view src)
|
||||
return dest;
|
||||
}
|
||||
|
||||
AllocatedString<>
|
||||
AllocatedString
|
||||
UCharToUTF8(std::basic_string_view<UChar> src)
|
||||
{
|
||||
/* worst-case estimate */
|
||||
@@ -65,5 +65,5 @@ UCharToUTF8(std::basic_string_view<UChar> src)
|
||||
throw std::runtime_error(u_errorName(error_code));
|
||||
|
||||
dest[dest_length] = 0;
|
||||
return AllocatedString<>::Donate(dest.release());
|
||||
return AllocatedString::Donate(dest.release());
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@
|
||||
#include <string_view>
|
||||
|
||||
template<typename T> class AllocatedArray;
|
||||
template<typename T> class AllocatedString;
|
||||
class AllocatedString;
|
||||
|
||||
/**
|
||||
* Wrapper for u_strFromUTF8().
|
||||
@@ -40,7 +40,7 @@ UCharFromUTF8(std::string_view src);
|
||||
*
|
||||
* Throws std::runtime_error on error.
|
||||
*/
|
||||
AllocatedString<char>
|
||||
AllocatedString
|
||||
UCharToUTF8(std::basic_string_view<UChar> src);
|
||||
|
||||
#endif
|
||||
|
@@ -25,7 +25,7 @@
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
AllocatedString<char>
|
||||
AllocatedString
|
||||
WideCharToMultiByte(unsigned code_page, std::wstring_view src)
|
||||
{
|
||||
int length = WideCharToMultiByte(code_page, 0, src.data(), src.size(),
|
||||
@@ -42,10 +42,10 @@ WideCharToMultiByte(unsigned code_page, std::wstring_view src)
|
||||
throw MakeLastError("Failed to convert from Unicode");
|
||||
|
||||
buffer[length] = '\0';
|
||||
return AllocatedString<char>::Donate(buffer.release());
|
||||
return AllocatedString::Donate(buffer.release());
|
||||
}
|
||||
|
||||
AllocatedString<wchar_t>
|
||||
BasicAllocatedString<wchar_t>
|
||||
MultiByteToWideChar(unsigned code_page, std::string_view src)
|
||||
{
|
||||
int length = MultiByteToWideChar(code_page, 0, src.data(), src.size(),
|
||||
@@ -60,5 +60,5 @@ MultiByteToWideChar(unsigned code_page, std::string_view src)
|
||||
throw MakeLastError("Failed to convert to Unicode");
|
||||
|
||||
buffer[length] = L'\0';
|
||||
return AllocatedString<wchar_t>::Donate(buffer.release());
|
||||
return BasicAllocatedString<wchar_t>::Donate(buffer.release());
|
||||
}
|
||||
|
@@ -24,20 +24,21 @@
|
||||
|
||||
#include <string_view>
|
||||
|
||||
template<typename T> class AllocatedString;
|
||||
class AllocatedString;
|
||||
template<typename T> class BasicAllocatedString;
|
||||
|
||||
/**
|
||||
* Throws std::system_error on error.
|
||||
*/
|
||||
gcc_pure gcc_nonnull_all
|
||||
AllocatedString<char>
|
||||
AllocatedString
|
||||
WideCharToMultiByte(unsigned code_page, std::wstring_view src);
|
||||
|
||||
/**
|
||||
* Throws std::system_error on error.
|
||||
*/
|
||||
gcc_pure gcc_nonnull_all
|
||||
AllocatedString<wchar_t>
|
||||
BasicAllocatedString<wchar_t>
|
||||
MultiByteToWideChar(unsigned code_page, std::string_view src);
|
||||
|
||||
#endif
|
||||
|
182
src/lib/jack/Dynamic.hxx
Normal file
182
src/lib/jack/Dynamic.hxx
Normal file
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2003-2021 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "system/Error.hxx"
|
||||
|
||||
/* sorry for this horrible piece of code - there's no elegant way to
|
||||
load DLLs at runtime */
|
||||
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wcast-function-type"
|
||||
#endif
|
||||
|
||||
using jack_set_error_function_t = std::add_pointer_t<decltype(jack_set_error_function)>;
|
||||
static jack_set_error_function_t _jack_set_error_function;
|
||||
|
||||
using jack_set_info_function_t = std::add_pointer_t<decltype(jack_set_info_function)>;
|
||||
static jack_set_info_function_t _jack_set_info_function;
|
||||
|
||||
using jack_client_open_t = std::add_pointer_t<decltype(jack_client_open)>;
|
||||
static jack_client_open_t _jack_client_open;
|
||||
|
||||
using jack_client_close_t = std::add_pointer_t<decltype(jack_client_close)>;
|
||||
static jack_client_close_t _jack_client_close;
|
||||
|
||||
using jack_connect_t = std::add_pointer_t<decltype(jack_connect)>;
|
||||
static jack_connect_t _jack_connect;
|
||||
|
||||
using jack_activate_t = std::add_pointer_t<decltype(jack_activate)>;
|
||||
static jack_activate_t _jack_activate;
|
||||
|
||||
using jack_deactivate_t = std::add_pointer_t<decltype(jack_deactivate)>;
|
||||
static jack_deactivate_t _jack_deactivate;
|
||||
|
||||
using jack_get_sample_rate_t = std::add_pointer_t<decltype(jack_get_sample_rate)>;
|
||||
static jack_get_sample_rate_t _jack_get_sample_rate;
|
||||
|
||||
using jack_set_process_callback_t = std::add_pointer_t<decltype(jack_set_process_callback)>;
|
||||
static jack_set_process_callback_t _jack_set_process_callback;
|
||||
|
||||
using jack_on_info_shutdown_t = std::add_pointer_t<decltype(jack_on_info_shutdown)>;
|
||||
static jack_on_info_shutdown_t _jack_on_info_shutdown;
|
||||
|
||||
using jack_free_t = std::add_pointer_t<decltype(jack_free)>;
|
||||
static jack_free_t _jack_free;
|
||||
|
||||
using jack_get_ports_t = std::add_pointer_t<decltype(jack_get_ports)>;
|
||||
static jack_get_ports_t _jack_get_ports;
|
||||
|
||||
using jack_port_register_t = std::add_pointer_t<decltype(jack_port_register)>;
|
||||
static jack_port_register_t _jack_port_register;
|
||||
|
||||
using jack_port_name_t = std::add_pointer_t<decltype(jack_port_name)>;
|
||||
static jack_port_name_t _jack_port_name;
|
||||
|
||||
using jack_port_get_buffer_t = std::add_pointer_t<decltype(jack_port_get_buffer)>;
|
||||
static jack_port_get_buffer_t _jack_port_get_buffer;
|
||||
|
||||
using jack_ringbuffer_create_t = std::add_pointer_t<decltype(jack_ringbuffer_create)>;
|
||||
static jack_ringbuffer_create_t _jack_ringbuffer_create;
|
||||
|
||||
using jack_ringbuffer_free_t = std::add_pointer_t<decltype(jack_ringbuffer_free)>;
|
||||
static jack_ringbuffer_free_t _jack_ringbuffer_free;
|
||||
|
||||
using jack_ringbuffer_get_write_vector_t = std::add_pointer_t<decltype(jack_ringbuffer_get_write_vector)>;
|
||||
static jack_ringbuffer_get_write_vector_t _jack_ringbuffer_get_write_vector;
|
||||
|
||||
using jack_ringbuffer_write_advance_t = std::add_pointer_t<decltype(jack_ringbuffer_write_advance)>;
|
||||
static jack_ringbuffer_write_advance_t _jack_ringbuffer_write_advance;
|
||||
|
||||
using jack_ringbuffer_read_space_t = std::add_pointer_t<decltype(jack_ringbuffer_read_space)>;
|
||||
static jack_ringbuffer_read_space_t _jack_ringbuffer_read_space;
|
||||
|
||||
using jack_ringbuffer_read_t = std::add_pointer_t<decltype(jack_ringbuffer_read)>;
|
||||
static jack_ringbuffer_read_t _jack_ringbuffer_read;
|
||||
|
||||
using jack_ringbuffer_read_advance_t = std::add_pointer_t<decltype(jack_ringbuffer_read_advance)>;
|
||||
static jack_ringbuffer_read_advance_t _jack_ringbuffer_read_advance;
|
||||
|
||||
using jack_ringbuffer_reset_t = std::add_pointer_t<decltype(jack_ringbuffer_reset)>;
|
||||
static jack_ringbuffer_reset_t _jack_ringbuffer_reset;
|
||||
|
||||
template<typename T>
|
||||
static void
|
||||
GetFunction(HMODULE h, const char *name, T &result)
|
||||
{
|
||||
auto f = GetProcAddress(h, name);
|
||||
if (f == nullptr)
|
||||
throw FormatRuntimeError("No such libjack function: %s", name);
|
||||
|
||||
result = reinterpret_cast<T>(f);
|
||||
}
|
||||
|
||||
static void
|
||||
LoadJackLibrary()
|
||||
{
|
||||
#ifdef _WIN64
|
||||
#define LIBJACK "libjack64"
|
||||
#else
|
||||
#define LIBJACK "libjack"
|
||||
#endif
|
||||
|
||||
auto libjack = LoadLibraryA(LIBJACK);
|
||||
if (!libjack)
|
||||
throw FormatLastError("Failed to load " LIBJACK ".dll");
|
||||
|
||||
GetFunction(libjack, "jack_set_error_function", _jack_set_error_function);
|
||||
GetFunction(libjack, "jack_set_info_function", _jack_set_info_function);
|
||||
|
||||
GetFunction(libjack, "jack_client_open", _jack_client_open);
|
||||
GetFunction(libjack, "jack_client_close", _jack_client_close);
|
||||
GetFunction(libjack, "jack_connect", _jack_connect);
|
||||
GetFunction(libjack, "jack_activate", _jack_activate);
|
||||
GetFunction(libjack, "jack_deactivate", _jack_deactivate);
|
||||
GetFunction(libjack, "jack_free", _jack_free);
|
||||
|
||||
GetFunction(libjack, "jack_get_sample_rate", _jack_get_sample_rate);
|
||||
GetFunction(libjack, "jack_set_process_callback", _jack_set_process_callback);
|
||||
GetFunction(libjack, "jack_on_info_shutdown", _jack_on_info_shutdown);
|
||||
|
||||
GetFunction(libjack, "jack_get_ports", _jack_get_ports);
|
||||
GetFunction(libjack, "jack_port_register", _jack_port_register);
|
||||
GetFunction(libjack, "jack_port_name", _jack_port_name);
|
||||
GetFunction(libjack, "jack_port_get_buffer", _jack_port_get_buffer);
|
||||
|
||||
GetFunction(libjack, "jack_ringbuffer_create", _jack_ringbuffer_create);
|
||||
GetFunction(libjack, "jack_ringbuffer_free", _jack_ringbuffer_free);
|
||||
GetFunction(libjack, "jack_ringbuffer_get_write_vector", _jack_ringbuffer_get_write_vector);
|
||||
GetFunction(libjack, "jack_ringbuffer_write_advance", _jack_ringbuffer_write_advance);
|
||||
GetFunction(libjack, "jack_ringbuffer_read_space", _jack_ringbuffer_read_space);
|
||||
GetFunction(libjack, "jack_ringbuffer_read", _jack_ringbuffer_read);
|
||||
GetFunction(libjack, "jack_ringbuffer_read_advance", _jack_ringbuffer_read_advance);
|
||||
GetFunction(libjack, "jack_ringbuffer_reset", _jack_ringbuffer_reset);
|
||||
}
|
||||
|
||||
#define jack_set_error_function _jack_set_error_function
|
||||
#define jack_set_info_function _jack_set_info_function
|
||||
|
||||
#define jack_client_open _jack_client_open
|
||||
#define jack_client_close _jack_client_close
|
||||
#define jack_connect _jack_connect
|
||||
#define jack_activate _jack_activate
|
||||
#define jack_deactivate _jack_deactivate
|
||||
#define jack_free _jack_free
|
||||
|
||||
#define jack_get_sample_rate _jack_get_sample_rate
|
||||
#define jack_set_process_callback _jack_set_process_callback
|
||||
#define jack_on_info_shutdown _jack_on_info_shutdown
|
||||
|
||||
#define jack_get_ports _jack_get_ports
|
||||
#define jack_port_register _jack_port_register
|
||||
#define jack_port_name _jack_port_name
|
||||
#define jack_port_get_buffer _jack_port_get_buffer
|
||||
|
||||
#define jack_ringbuffer_create _jack_ringbuffer_create
|
||||
#define jack_ringbuffer_free _jack_ringbuffer_free
|
||||
#define jack_ringbuffer_get_write_vector _jack_ringbuffer_get_write_vector
|
||||
#define jack_ringbuffer_write_advance _jack_ringbuffer_write_advance
|
||||
#define jack_ringbuffer_read_space _jack_ringbuffer_read_space
|
||||
#define jack_ringbuffer_read _jack_ringbuffer_read
|
||||
#define jack_ringbuffer_read_advance _jack_ringbuffer_read_advance
|
||||
#define jack_ringbuffer_reset _jack_ringbuffer_reset
|
||||
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
@@ -1,5 +1,7 @@
|
||||
if enable_database
|
||||
sqlite_dep = dependency('sqlite3', version: '>= 3.7.3', required: get_option('sqlite'))
|
||||
sqlite_dep = dependency('sqlite3', version: '>= 3.7.3',
|
||||
fallback: ['sqlite3', 'sqlite3_dep'],
|
||||
required: get_option('sqlite'))
|
||||
else
|
||||
sqlite_dep = dependency('', required: false)
|
||||
endif
|
||||
@@ -21,4 +23,7 @@ sqlite = static_library(
|
||||
|
||||
sqlite_dep = declare_dependency(
|
||||
link_with: sqlite,
|
||||
dependencies: [
|
||||
sqlite_dep,
|
||||
],
|
||||
)
|
||||
|
@@ -17,103 +17,102 @@
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#undef NOUSER // COM needs the "MSG" typedef
|
||||
|
||||
#include "output/plugins/wasapi/ForMixer.hxx"
|
||||
#include "output/plugins/wasapi/AudioClient.hxx"
|
||||
#include "output/plugins/wasapi/Device.hxx"
|
||||
#include "mixer/MixerInternal.hxx"
|
||||
#include "output/plugins/WasapiOutputPlugin.hxx"
|
||||
#include "win32/Com.hxx"
|
||||
#include "win32/ComPtr.hxx"
|
||||
#include "win32/ComWorker.hxx"
|
||||
#include "win32/HResult.hxx"
|
||||
|
||||
#include <cmath>
|
||||
#include <endpointvolume.h>
|
||||
#include <optional>
|
||||
|
||||
#include <audioclient.h>
|
||||
#include <endpointvolume.h>
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
class WasapiMixer final : public Mixer {
|
||||
WasapiOutput &output;
|
||||
std::optional<COM> com;
|
||||
|
||||
public:
|
||||
WasapiMixer(WasapiOutput &_output, MixerListener &_listener)
|
||||
: Mixer(wasapi_mixer_plugin, _listener), output(_output) {}
|
||||
|
||||
void Open() override { com.emplace(); }
|
||||
void Open() override {}
|
||||
|
||||
void Close() noexcept override { com.reset(); }
|
||||
void Close() noexcept override {}
|
||||
|
||||
int GetVolume() override {
|
||||
HRESULT result;
|
||||
float volume_level;
|
||||
auto com_worker = wasapi_output_get_com_worker(output);
|
||||
if (!com_worker)
|
||||
return -1;
|
||||
|
||||
if (wasapi_is_exclusive(output)) {
|
||||
ComPtr<IAudioEndpointVolume> endpoint_volume;
|
||||
result = wasapi_output_get_device(output)->Activate(
|
||||
__uuidof(IAudioEndpointVolume), CLSCTX_ALL, nullptr,
|
||||
endpoint_volume.AddressCast());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(
|
||||
result, "Unable to get device endpoint volume");
|
||||
auto future = com_worker->Async([&]() -> int {
|
||||
HRESULT result;
|
||||
float volume_level;
|
||||
|
||||
if (wasapi_is_exclusive(output)) {
|
||||
auto endpoint_volume =
|
||||
Activate<IAudioEndpointVolume>(*wasapi_output_get_device(output));
|
||||
|
||||
result = endpoint_volume->GetMasterVolumeLevelScalar(
|
||||
&volume_level);
|
||||
if (FAILED(result)) {
|
||||
throw MakeHResultError(result,
|
||||
"Unable to get master "
|
||||
"volume level");
|
||||
}
|
||||
} else {
|
||||
auto session_volume =
|
||||
GetService<ISimpleAudioVolume>(*wasapi_output_get_client(output));
|
||||
|
||||
result = session_volume->GetMasterVolume(&volume_level);
|
||||
if (FAILED(result)) {
|
||||
throw MakeHResultError(
|
||||
result, "Unable to get master volume");
|
||||
}
|
||||
}
|
||||
|
||||
result = endpoint_volume->GetMasterVolumeLevelScalar(
|
||||
&volume_level);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(
|
||||
result, "Unable to get master volume level");
|
||||
}
|
||||
} else {
|
||||
ComPtr<ISimpleAudioVolume> session_volume;
|
||||
result = wasapi_output_get_client(output)->GetService(
|
||||
__uuidof(ISimpleAudioVolume),
|
||||
session_volume.AddressCast<void>());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(
|
||||
result, "Unable to get client session volume");
|
||||
}
|
||||
|
||||
result = session_volume->GetMasterVolume(&volume_level);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result,
|
||||
"Unable to get master volume");
|
||||
}
|
||||
}
|
||||
|
||||
return std::lround(volume_level * 100.0f);
|
||||
return std::lround(volume_level * 100.0f);
|
||||
});
|
||||
return future.get();
|
||||
}
|
||||
|
||||
void SetVolume(unsigned volume) override {
|
||||
HRESULT result;
|
||||
const float volume_level = volume / 100.0f;
|
||||
auto com_worker = wasapi_output_get_com_worker(output);
|
||||
if (!com_worker)
|
||||
throw std::runtime_error("Cannot set WASAPI volume");
|
||||
|
||||
if (wasapi_is_exclusive(output)) {
|
||||
ComPtr<IAudioEndpointVolume> endpoint_volume;
|
||||
result = wasapi_output_get_device(output)->Activate(
|
||||
__uuidof(IAudioEndpointVolume), CLSCTX_ALL, nullptr,
|
||||
endpoint_volume.AddressCast());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(
|
||||
result, "Unable to get device endpoint volume");
|
||||
}
|
||||
com_worker->Async([&]() {
|
||||
HRESULT result;
|
||||
const float volume_level = volume / 100.0f;
|
||||
|
||||
result = endpoint_volume->SetMasterVolumeLevelScalar(volume_level,
|
||||
nullptr);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(
|
||||
result, "Unable to set master volume level");
|
||||
}
|
||||
} else {
|
||||
ComPtr<ISimpleAudioVolume> session_volume;
|
||||
result = wasapi_output_get_client(output)->GetService(
|
||||
__uuidof(ISimpleAudioVolume),
|
||||
session_volume.AddressCast<void>());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(
|
||||
result, "Unable to get client session volume");
|
||||
}
|
||||
if (wasapi_is_exclusive(output)) {
|
||||
auto endpoint_volume =
|
||||
Activate<IAudioEndpointVolume>(*wasapi_output_get_device(output));
|
||||
|
||||
result = session_volume->SetMasterVolume(volume_level, nullptr);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result,
|
||||
"Unable to set master volume");
|
||||
result = endpoint_volume->SetMasterVolumeLevelScalar(
|
||||
volume_level, nullptr);
|
||||
if (FAILED(result)) {
|
||||
throw MakeHResultError(
|
||||
result,
|
||||
"Unable to set master volume level");
|
||||
}
|
||||
} else {
|
||||
auto session_volume =
|
||||
GetService<ISimpleAudioVolume>(*wasapi_output_get_client(output));
|
||||
|
||||
result = session_volume->SetMasterVolume(volume_level,
|
||||
nullptr);
|
||||
if (FAILED(result)) {
|
||||
throw MakeHResultError(
|
||||
result, "Unable to set master volume");
|
||||
}
|
||||
}
|
||||
}
|
||||
}).get();
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -182,8 +182,8 @@ UdisksNeighborExplorer::GetList() const noexcept
|
||||
|
||||
NeighborExplorer::List result;
|
||||
|
||||
for (const auto &i : by_uri)
|
||||
result.emplace_front(i.second);
|
||||
for (const auto &[t, r] : by_uri)
|
||||
result.emplace_front(r);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@@ -1,20 +1,30 @@
|
||||
/*
|
||||
* Copyright 2003-2021 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
* Copyright 2015-2021 Max Kellermann <max.kellermann@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* - Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
* - 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 "SocketError.hxx"
|
||||
|
@@ -1,24 +1,34 @@
|
||||
/*
|
||||
* Copyright 2003-2021 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
* Copyright 2015-2021 Max Kellermann <max.kellermann@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* - Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
* - 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.
|
||||
*/
|
||||
|
||||
#ifndef MPD_SOCKET_ERROR_HXX
|
||||
#define MPD_SOCKET_ERROR_HXX
|
||||
#ifndef SOCKET_ERROR_HXX
|
||||
#define SOCKET_ERROR_HXX
|
||||
|
||||
#include "util/Compiler.h"
|
||||
#include "system/Error.hxx"
|
||||
@@ -42,14 +52,79 @@ GetSocketError() noexcept
|
||||
#endif
|
||||
}
|
||||
|
||||
gcc_const
|
||||
static inline bool
|
||||
IsSocketErrorAgain(socket_error_t code) noexcept
|
||||
constexpr bool
|
||||
IsSocketErrorInProgress(socket_error_t code) noexcept
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return code == WSAEINPROGRESS;
|
||||
#else
|
||||
return code == EAGAIN;
|
||||
return code == EINPROGRESS;
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr bool
|
||||
IsSocketErrorWouldBlock(socket_error_t code) noexcept
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return code == WSAEWOULDBLOCK;
|
||||
#else
|
||||
return code == EWOULDBLOCK;
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr bool
|
||||
IsSocketErrorConnectWouldBlock(socket_error_t code) noexcept
|
||||
{
|
||||
#if defined(_WIN32) || defined(__linux__)
|
||||
/* on Windows, WSAEINPROGRESS is for blocking sockets and
|
||||
WSAEWOULDBLOCK for non-blocking sockets */
|
||||
/* on Linux, EAGAIN==EWOULDBLOCK is for local sockets and
|
||||
EINPROGRESS is for all other sockets */
|
||||
return IsSocketErrorInProgress(code) || IsSocketErrorWouldBlock(code);
|
||||
#else
|
||||
/* on all other operating systems, there's just EINPROGRESS */
|
||||
return IsSocketErrorInProgress(code);
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr bool
|
||||
IsSocketErrorSendWouldBlock(socket_error_t code) noexcept
|
||||
{
|
||||
#ifdef _WIN32
|
||||
/* on Windows, WSAEINPROGRESS is for blocking sockets and
|
||||
WSAEWOULDBLOCK for non-blocking sockets */
|
||||
return IsSocketErrorInProgress(code) || IsSocketErrorWouldBlock(code);
|
||||
#else
|
||||
/* on all other operating systems, there's just EAGAIN==EWOULDBLOCK */
|
||||
return IsSocketErrorWouldBlock(code);
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr bool
|
||||
IsSocketErrorReceiveWouldBlock(socket_error_t code) noexcept
|
||||
{
|
||||
#ifdef _WIN32
|
||||
/* on Windows, WSAEINPROGRESS is for blocking sockets and
|
||||
WSAEWOULDBLOCK for non-blocking sockets */
|
||||
return IsSocketErrorInProgress(code) || IsSocketErrorWouldBlock(code);
|
||||
#else
|
||||
/* on all other operating systems, there's just
|
||||
EAGAIN==EWOULDBLOCK */
|
||||
return IsSocketErrorWouldBlock(code);
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr bool
|
||||
IsSocketErrorAcceptWouldBlock(socket_error_t code) noexcept
|
||||
{
|
||||
#ifdef _WIN32
|
||||
/* on Windows, WSAEINPROGRESS is for blocking sockets and
|
||||
WSAEWOULDBLOCK for non-blocking sockets */
|
||||
return IsSocketErrorInProgress(code) || IsSocketErrorWouldBlock(code);
|
||||
#else
|
||||
/* on all other operating systems, there's just
|
||||
EAGAIN==EWOULDBLOCK */
|
||||
return IsSocketErrorWouldBlock(code);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@@ -118,7 +118,7 @@ AudioOutputControl::GetLogName() const noexcept
|
||||
{
|
||||
assert(!IsDummy());
|
||||
|
||||
return output->GetLogName();
|
||||
return output ? output->GetLogName() : name.c_str();
|
||||
}
|
||||
|
||||
Mixer *
|
||||
@@ -364,7 +364,7 @@ AudioOutputControl::LockPlay() noexcept
|
||||
void
|
||||
AudioOutputControl::LockPauseAsync() noexcept
|
||||
{
|
||||
if (output->mixer != nullptr && !output->SupportsPause())
|
||||
if (output && output->mixer != nullptr && !output->SupportsPause())
|
||||
/* the device has no pause mode: close the mixer,
|
||||
unless its "global" flag is set (checked by
|
||||
mixer_auto_close()) */
|
||||
|
@@ -181,6 +181,14 @@ class AudioOutputControl {
|
||||
*/
|
||||
bool open = false;
|
||||
|
||||
/**
|
||||
* Is the device currently playing, i.e. is its buffer
|
||||
* (likely) non-empty? If not, then it will never be drained.
|
||||
*
|
||||
* This field is only valid while the output is open.
|
||||
*/
|
||||
bool playing;
|
||||
|
||||
/**
|
||||
* Is the device paused? i.e. the output thread is in the
|
||||
* ao_pause() loop.
|
||||
|
@@ -40,8 +40,7 @@ printAudioDevices(Response &r, const MultipleOutputs &outputs)
|
||||
ao.GetName(), ao.GetPluginName(),
|
||||
ao.IsEnabled());
|
||||
|
||||
for (const auto &a : ao.GetAttributes())
|
||||
r.Format("attribute: %s=%s\n",
|
||||
a.first.c_str(), a.second.c_str());
|
||||
for (const auto &[attribute, value] : ao.GetAttributes())
|
||||
r.Format("attribute: %s=%s\n", attribute.c_str(), value.c_str());
|
||||
}
|
||||
}
|
||||
|
@@ -42,7 +42,7 @@
|
||||
#include "plugins/WinmmOutputPlugin.hxx"
|
||||
#endif
|
||||
#ifdef ENABLE_WASAPI_OUTPUT
|
||||
#include "plugins/WasapiOutputPlugin.hxx"
|
||||
#include "plugins/wasapi/WasapiOutputPlugin.hxx"
|
||||
#endif
|
||||
#include "util/StringAPI.hxx"
|
||||
|
||||
|
@@ -53,7 +53,7 @@ AudioOutputControl::InternalOpen2(const AudioFormat in_audio_format)
|
||||
if (open && cf != output->filter_audio_format)
|
||||
/* if the filter's output format changes, the output
|
||||
must be reopened as well */
|
||||
InternalCloseOutput(true);
|
||||
InternalCloseOutput(playing);
|
||||
|
||||
output->filter_audio_format = cf;
|
||||
|
||||
@@ -64,6 +64,7 @@ AudioOutputControl::InternalOpen2(const AudioFormat in_audio_format)
|
||||
}
|
||||
|
||||
open = true;
|
||||
playing = false;
|
||||
} else if (in_audio_format != output->out_audio_format) {
|
||||
/* reconfigure the final ConvertFilter for its new
|
||||
input AudioFormat */
|
||||
@@ -285,6 +286,9 @@ AudioOutputControl::PlayChunk(std::unique_lock<Mutex> &lock) noexcept
|
||||
assert(nbytes % output->out_audio_format.GetFrameSize() == 0);
|
||||
|
||||
source.ConsumeData(nbytes);
|
||||
|
||||
/* there's data to be drained from now on */
|
||||
playing = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -371,6 +375,9 @@ AudioOutputControl::InternalPause(std::unique_lock<Mutex> &lock) noexcept
|
||||
}
|
||||
|
||||
skip_delay = true;
|
||||
|
||||
/* ignore drain commands until we got something new to play */
|
||||
playing = false;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -390,6 +397,10 @@ PlayFull(FilteredAudioOutput &output, ConstBuffer<void> _buffer)
|
||||
inline void
|
||||
AudioOutputControl::InternalDrain() noexcept
|
||||
{
|
||||
/* after this method finishes, there's nothing left to be
|
||||
drained */
|
||||
playing = false;
|
||||
|
||||
try {
|
||||
/* flush the filter and play its remaining output */
|
||||
|
||||
@@ -518,6 +529,7 @@ AudioOutputControl::Task() noexcept
|
||||
source.Cancel();
|
||||
|
||||
if (open) {
|
||||
playing = false;
|
||||
const ScopeUnlock unlock(mutex);
|
||||
output->Cancel();
|
||||
}
|
||||
|
@@ -44,6 +44,10 @@ static constexpr unsigned MAX_PORTS = 16;
|
||||
|
||||
static constexpr size_t jack_sample_size = sizeof(jack_default_audio_sample_t);
|
||||
|
||||
#ifdef DYNAMIC_JACK
|
||||
#include "lib/jack/Dynamic.hxx"
|
||||
#endif // _WIN32
|
||||
|
||||
class JackOutput final : public AudioOutput {
|
||||
/**
|
||||
* libjack options passed to jack_client_open().
|
||||
@@ -463,6 +467,10 @@ JackOutput::Disable() noexcept
|
||||
static AudioOutput *
|
||||
mpd_jack_init(EventLoop &, const ConfigBlock &block)
|
||||
{
|
||||
#ifdef DYNAMIC_JACK
|
||||
LoadJackLibrary();
|
||||
#endif
|
||||
|
||||
jack_set_error_function(mpd_jack_error);
|
||||
|
||||
#ifdef HAVE_JACK_SET_INFO_FUNCTION
|
||||
|
@@ -56,8 +56,6 @@ class PulseOutput final : AudioOutput {
|
||||
|
||||
size_t writable;
|
||||
|
||||
bool pause;
|
||||
|
||||
/**
|
||||
* Was Interrupt() called? This will unblock Play(). It will
|
||||
* be reset by Cancel() and Pause(), as documented by the
|
||||
@@ -113,6 +111,7 @@ public:
|
||||
|
||||
[[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override;
|
||||
size_t Play(const void *chunk, size_t size) override;
|
||||
void Drain() override;
|
||||
void Cancel() noexcept override;
|
||||
bool Pause() override;
|
||||
|
||||
@@ -688,7 +687,6 @@ PulseOutput::Open(AudioFormat &audio_format)
|
||||
"pa_stream_connect_playback() has failed");
|
||||
}
|
||||
|
||||
pause = false;
|
||||
interrupted = false;
|
||||
}
|
||||
|
||||
@@ -699,17 +697,6 @@ PulseOutput::Close() noexcept
|
||||
|
||||
Pulse::LockGuard lock(mainloop);
|
||||
|
||||
if (pa_stream_get_state(stream) == PA_STREAM_READY) {
|
||||
pa_operation *o =
|
||||
pa_stream_drain(stream,
|
||||
pulse_output_stream_success_cb, this);
|
||||
if (o == nullptr) {
|
||||
LogPulseError(context,
|
||||
"pa_stream_drain() has failed");
|
||||
} else
|
||||
pulse_wait_for_operation(mainloop, o);
|
||||
}
|
||||
|
||||
DeleteStream();
|
||||
|
||||
if (context != nullptr &&
|
||||
@@ -780,7 +767,7 @@ PulseOutput::Delay() const noexcept
|
||||
Pulse::LockGuard lock(mainloop);
|
||||
|
||||
auto result = std::chrono::steady_clock::duration::zero();
|
||||
if (pause && pa_stream_is_corked(stream) &&
|
||||
if (pa_stream_is_corked(stream) &&
|
||||
pa_stream_get_state(stream) == PA_STREAM_READY)
|
||||
/* idle while paused */
|
||||
result = std::chrono::seconds(1);
|
||||
@@ -796,8 +783,6 @@ PulseOutput::Play(const void *chunk, size_t size)
|
||||
|
||||
Pulse::LockGuard lock(mainloop);
|
||||
|
||||
pause = false;
|
||||
|
||||
/* check if the stream is (already) connected */
|
||||
|
||||
WaitStream();
|
||||
@@ -840,6 +825,25 @@ PulseOutput::Play(const void *chunk, size_t size)
|
||||
return size;
|
||||
}
|
||||
|
||||
void
|
||||
PulseOutput::Drain()
|
||||
{
|
||||
Pulse::LockGuard lock(mainloop);
|
||||
|
||||
if (pa_stream_get_state(stream) != PA_STREAM_READY ||
|
||||
pa_stream_is_suspended(stream) ||
|
||||
pa_stream_is_corked(stream))
|
||||
return;
|
||||
|
||||
pa_operation *o =
|
||||
pa_stream_drain(stream,
|
||||
pulse_output_stream_success_cb, this);
|
||||
if (o == nullptr)
|
||||
throw MakePulseError(context, "pa_stream_drain() failed");
|
||||
|
||||
pulse_wait_for_operation(mainloop, o);
|
||||
}
|
||||
|
||||
void
|
||||
PulseOutput::Cancel() noexcept
|
||||
{
|
||||
@@ -876,7 +880,6 @@ PulseOutput::Pause()
|
||||
|
||||
Pulse::LockGuard lock(mainloop);
|
||||
|
||||
pause = true;
|
||||
interrupted = false;
|
||||
|
||||
/* check if the stream is (already/still) connected */
|
||||
|
@@ -1,824 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
#include <initguid.h>
|
||||
|
||||
#include "Log.hxx"
|
||||
#include "WasapiOutputPlugin.hxx"
|
||||
#include "lib/icu/Win32.hxx"
|
||||
#include "mixer/MixerList.hxx"
|
||||
#include "thread/Cond.hxx"
|
||||
#include "thread/Mutex.hxx"
|
||||
#include "thread/Thread.hxx"
|
||||
#include "util/AllocatedString.hxx"
|
||||
#include "util/Domain.hxx"
|
||||
#include "util/RuntimeError.hxx"
|
||||
#include "util/ScopeExit.hxx"
|
||||
#include "win32/Com.hxx"
|
||||
#include "win32/ComHeapPtr.hxx"
|
||||
#include "win32/HResult.hxx"
|
||||
#include "win32/WinEvent.hxx"
|
||||
|
||||
#include <algorithm>
|
||||
#include <boost/lockfree/spsc_queue.hpp>
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <functiondiscoverykeys_devpkey.h>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
|
||||
namespace {
|
||||
static constexpr Domain wasapi_output_domain("wasapi_output");
|
||||
|
||||
gcc_const constexpr uint32_t GetChannelMask(const uint8_t channels) noexcept {
|
||||
switch (channels) {
|
||||
case 1:
|
||||
return KSAUDIO_SPEAKER_MONO;
|
||||
case 2:
|
||||
return KSAUDIO_SPEAKER_STEREO;
|
||||
case 3:
|
||||
return SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER;
|
||||
case 4:
|
||||
return KSAUDIO_SPEAKER_QUAD;
|
||||
case 5:
|
||||
return SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER |
|
||||
SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT;
|
||||
case 6:
|
||||
return KSAUDIO_SPEAKER_5POINT1;
|
||||
case 7:
|
||||
return KSAUDIO_SPEAKER_5POINT1 | SPEAKER_BACK_CENTER;
|
||||
case 8:
|
||||
return KSAUDIO_SPEAKER_7POINT1_SURROUND;
|
||||
default:
|
||||
gcc_unreachable();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Functor>
|
||||
inline bool SafeTry(Functor &&functor) {
|
||||
try {
|
||||
functor();
|
||||
return true;
|
||||
} catch (std::runtime_error &err) {
|
||||
FormatError(wasapi_output_domain, "%s", err.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Functor>
|
||||
inline bool SafeSilenceTry(Functor &&functor) {
|
||||
try {
|
||||
functor();
|
||||
return true;
|
||||
} catch (std::runtime_error &err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline void SetFormat(WAVEFORMATEXTENSIBLE &device_format,
|
||||
const AudioFormat &audio_format) noexcept {
|
||||
device_format.dwChannelMask = GetChannelMask(audio_format.channels);
|
||||
device_format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
|
||||
device_format.Format.nChannels = audio_format.channels;
|
||||
device_format.Format.nSamplesPerSec = audio_format.sample_rate;
|
||||
device_format.Format.nBlockAlign = audio_format.GetFrameSize();
|
||||
device_format.Format.nAvgBytesPerSec =
|
||||
audio_format.sample_rate * audio_format.GetFrameSize();
|
||||
device_format.Format.wBitsPerSample = audio_format.GetSampleSize() * 8;
|
||||
device_format.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
|
||||
device_format.Samples.wValidBitsPerSample = audio_format.GetSampleSize() * 8;
|
||||
if (audio_format.format == SampleFormat::FLOAT) {
|
||||
device_format.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
|
||||
} else {
|
||||
device_format.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
|
||||
}
|
||||
}
|
||||
|
||||
inline constexpr const unsigned int kErrorId = -1;
|
||||
|
||||
} // namespace
|
||||
|
||||
class WasapiOutputThread : public Thread {
|
||||
public:
|
||||
enum class Status : uint32_t { FINISH, PLAY, PAUSE };
|
||||
WasapiOutputThread(std::shared_ptr<WinEvent> _event, ComPtr<IAudioClient> _client,
|
||||
ComPtr<IAudioRenderClient> &&_render_client,
|
||||
const UINT32 _frame_size, const UINT32 _buffer_size_in_frames,
|
||||
bool _is_exclusive,
|
||||
boost::lockfree::spsc_queue<BYTE> &_spsc_buffer)
|
||||
: Thread(BIND_THIS_METHOD(Work)), event(std::move(_event)),
|
||||
client(std::move(_client)), render_client(std::move(_render_client)),
|
||||
frame_size(_frame_size), buffer_size_in_frames(_buffer_size_in_frames),
|
||||
is_exclusive(_is_exclusive), spsc_buffer(_spsc_buffer) {}
|
||||
void Finish() noexcept { return SetStatus(Status::FINISH); }
|
||||
void Play() noexcept { return SetStatus(Status::PLAY); }
|
||||
void Pause() noexcept { return SetStatus(Status::PAUSE); }
|
||||
void WaitWrite() noexcept {
|
||||
std::unique_lock<Mutex> lock(write.mutex);
|
||||
write.cond.wait(lock);
|
||||
}
|
||||
void CheckException() {
|
||||
std::unique_lock<Mutex> lock(error.mutex);
|
||||
if (error.error_ptr) {
|
||||
std::exception_ptr err = std::exchange(error.error_ptr, nullptr);
|
||||
error.cond.notify_all();
|
||||
std::rethrow_exception(err);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<WinEvent> event;
|
||||
std::optional<COM> com;
|
||||
ComPtr<IAudioClient> client;
|
||||
ComPtr<IAudioRenderClient> render_client;
|
||||
const UINT32 frame_size;
|
||||
const UINT32 buffer_size_in_frames;
|
||||
bool is_exclusive;
|
||||
boost::lockfree::spsc_queue<BYTE> &spsc_buffer;
|
||||
alignas(BOOST_LOCKFREE_CACHELINE_BYTES) std::atomic<Status> status =
|
||||
Status::PAUSE;
|
||||
alignas(BOOST_LOCKFREE_CACHELINE_BYTES) struct {
|
||||
Mutex mutex;
|
||||
Cond cond;
|
||||
} write{};
|
||||
alignas(BOOST_LOCKFREE_CACHELINE_BYTES) struct {
|
||||
Mutex mutex;
|
||||
Cond cond;
|
||||
std::exception_ptr error_ptr = nullptr;
|
||||
} error{};
|
||||
|
||||
void SetStatus(Status s) noexcept {
|
||||
status.store(s);
|
||||
event->Set();
|
||||
}
|
||||
void Work() noexcept;
|
||||
};
|
||||
|
||||
class WasapiOutput final : public AudioOutput {
|
||||
public:
|
||||
static AudioOutput *Create(EventLoop &, const ConfigBlock &block);
|
||||
WasapiOutput(const ConfigBlock &block);
|
||||
void Enable() override;
|
||||
void Disable() noexcept override;
|
||||
void Open(AudioFormat &audio_format) override;
|
||||
void Close() noexcept override;
|
||||
std::chrono::steady_clock::duration Delay() const noexcept override;
|
||||
size_t Play(const void *chunk, size_t size) override;
|
||||
void Drain() override;
|
||||
bool Pause() override;
|
||||
|
||||
constexpr bool Exclusive() const { return is_exclusive; }
|
||||
constexpr size_t FrameSize() const { return device_format.Format.nBlockAlign; }
|
||||
constexpr size_t SampleRate() const {
|
||||
return device_format.Format.nSamplesPerSec;
|
||||
}
|
||||
|
||||
private:
|
||||
bool is_started = false;
|
||||
bool is_exclusive;
|
||||
bool enumerate_devices;
|
||||
std::string device_config;
|
||||
std::vector<std::pair<unsigned int, AllocatedString<char>>> device_desc;
|
||||
std::shared_ptr<WinEvent> event;
|
||||
std::optional<COM> com;
|
||||
ComPtr<IMMDeviceEnumerator> enumerator;
|
||||
ComPtr<IMMDevice> device;
|
||||
ComPtr<IAudioClient> client;
|
||||
WAVEFORMATEXTENSIBLE device_format;
|
||||
std::unique_ptr<WasapiOutputThread> thread;
|
||||
std::unique_ptr<boost::lockfree::spsc_queue<BYTE>> spsc_buffer;
|
||||
std::size_t watermark;
|
||||
|
||||
friend bool wasapi_is_exclusive(WasapiOutput &output) noexcept;
|
||||
friend IMMDevice *wasapi_output_get_device(WasapiOutput &output) noexcept;
|
||||
friend IAudioClient *wasapi_output_get_client(WasapiOutput &output) noexcept;
|
||||
|
||||
void FindExclusiveFormatSupported(AudioFormat &audio_format);
|
||||
void FindSharedFormatSupported(AudioFormat &audio_format);
|
||||
void EnumerateDevices();
|
||||
void GetDevice(unsigned int index);
|
||||
unsigned int SearchDevice(std::string_view name);
|
||||
void GetDefaultDevice();
|
||||
};
|
||||
|
||||
WasapiOutput &wasapi_output_downcast(AudioOutput &output) noexcept {
|
||||
return static_cast<WasapiOutput &>(output);
|
||||
}
|
||||
|
||||
bool wasapi_is_exclusive(WasapiOutput &output) noexcept { return output.is_exclusive; }
|
||||
|
||||
IMMDevice *wasapi_output_get_device(WasapiOutput &output) noexcept {
|
||||
return output.device.get();
|
||||
}
|
||||
|
||||
IAudioClient *wasapi_output_get_client(WasapiOutput &output) noexcept {
|
||||
return output.client.get();
|
||||
}
|
||||
|
||||
void WasapiOutputThread::Work() noexcept {
|
||||
FormatDebug(wasapi_output_domain, "Working thread started");
|
||||
try {
|
||||
com.emplace();
|
||||
} catch (...) {
|
||||
std::unique_lock<Mutex> lock(error.mutex);
|
||||
error.error_ptr = std::current_exception();
|
||||
error.cond.wait(lock);
|
||||
assert(error.error_ptr == nullptr);
|
||||
return;
|
||||
}
|
||||
while (true) {
|
||||
try {
|
||||
event->Wait(INFINITE);
|
||||
|
||||
Status current_state = status.load();
|
||||
if (current_state == Status::FINISH) {
|
||||
FormatDebug(wasapi_output_domain,
|
||||
"Working thread stopped");
|
||||
return;
|
||||
}
|
||||
|
||||
AtScopeExit(&) { write.cond.notify_all(); };
|
||||
|
||||
HRESULT result;
|
||||
UINT32 data_in_frames;
|
||||
result = client->GetCurrentPadding(&data_in_frames);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result,
|
||||
"Failed to get current padding");
|
||||
}
|
||||
|
||||
UINT32 write_in_frames = buffer_size_in_frames;
|
||||
if (!is_exclusive) {
|
||||
if (data_in_frames >= buffer_size_in_frames) {
|
||||
continue;
|
||||
}
|
||||
write_in_frames -= data_in_frames;
|
||||
} else if (data_in_frames >= buffer_size_in_frames * 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BYTE *data;
|
||||
|
||||
result = render_client->GetBuffer(write_in_frames, &data);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Failed to get buffer");
|
||||
}
|
||||
|
||||
AtScopeExit(&) {
|
||||
render_client->ReleaseBuffer(write_in_frames, 0);
|
||||
};
|
||||
|
||||
const UINT32 write_size = write_in_frames * frame_size;
|
||||
UINT32 new_data_size = 0;
|
||||
if (current_state == Status::PLAY) {
|
||||
new_data_size = spsc_buffer.pop(data, write_size);
|
||||
} else {
|
||||
FormatDebug(wasapi_output_domain,
|
||||
"Working thread paused");
|
||||
}
|
||||
std::fill_n(data + new_data_size, write_size - new_data_size, 0);
|
||||
} catch (...) {
|
||||
std::unique_lock<Mutex> lock(error.mutex);
|
||||
error.error_ptr = std::current_exception();
|
||||
error.cond.wait(lock);
|
||||
assert(error.error_ptr == nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AudioOutput *WasapiOutput::Create(EventLoop &, const ConfigBlock &block) {
|
||||
return new WasapiOutput(block);
|
||||
}
|
||||
|
||||
WasapiOutput::WasapiOutput(const ConfigBlock &block)
|
||||
: AudioOutput(FLAG_ENABLE_DISABLE | FLAG_PAUSE),
|
||||
is_exclusive(block.GetBlockValue("exclusive", false)),
|
||||
enumerate_devices(block.GetBlockValue("enumerate", false)),
|
||||
device_config(block.GetBlockValue("device", "")) {}
|
||||
|
||||
void WasapiOutput::Enable() {
|
||||
com.emplace();
|
||||
event = std::make_shared<WinEvent>();
|
||||
enumerator.CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr,
|
||||
CLSCTX_INPROC_SERVER);
|
||||
|
||||
device_desc.clear();
|
||||
device.reset();
|
||||
|
||||
if (enumerate_devices && SafeTry([this]() { EnumerateDevices(); })) {
|
||||
for (const auto &desc : device_desc) {
|
||||
FormatNotice(wasapi_output_domain, "Device \"%u\" \"%s\"",
|
||||
desc.first, desc.second.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int id = kErrorId;
|
||||
if (!device_config.empty()) {
|
||||
if (!SafeSilenceTry([this, &id]() { id = std::stoul(device_config); })) {
|
||||
id = SearchDevice(device_config);
|
||||
}
|
||||
}
|
||||
|
||||
if (id != kErrorId) {
|
||||
SafeTry([this, id]() { GetDevice(id); });
|
||||
}
|
||||
|
||||
if (!device) {
|
||||
GetDefaultDevice();
|
||||
}
|
||||
|
||||
device_desc.clear();
|
||||
}
|
||||
|
||||
void WasapiOutput::Disable() noexcept {
|
||||
if (thread) {
|
||||
try {
|
||||
thread->Finish();
|
||||
thread->Join();
|
||||
} catch (std::exception &err) {
|
||||
FormatError(wasapi_output_domain, "exception while disabling: %s",
|
||||
err.what());
|
||||
}
|
||||
thread.reset();
|
||||
spsc_buffer.reset();
|
||||
client.reset();
|
||||
}
|
||||
device.reset();
|
||||
enumerator.reset();
|
||||
com.reset();
|
||||
event.reset();
|
||||
}
|
||||
|
||||
void WasapiOutput::Open(AudioFormat &audio_format) {
|
||||
if (audio_format.channels == 0) {
|
||||
throw FormatInvalidArgument("channels should > 0");
|
||||
}
|
||||
|
||||
client.reset();
|
||||
|
||||
HRESULT result;
|
||||
result = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr,
|
||||
client.AddressCast());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Unable to activate audio client");
|
||||
}
|
||||
|
||||
if (audio_format.format == SampleFormat::S24_P32) {
|
||||
audio_format.format = SampleFormat::S32;
|
||||
}
|
||||
if (audio_format.channels > 8) {
|
||||
audio_format.channels = 8;
|
||||
}
|
||||
|
||||
if (Exclusive()) {
|
||||
FindExclusiveFormatSupported(audio_format);
|
||||
} else {
|
||||
FindSharedFormatSupported(audio_format);
|
||||
}
|
||||
|
||||
using s = std::chrono::seconds;
|
||||
using ms = std::chrono::milliseconds;
|
||||
using ns = std::chrono::nanoseconds;
|
||||
using hundred_ns = std::chrono::duration<uint64_t, std::ratio<1, 10000000>>;
|
||||
|
||||
// The unit in REFERENCE_TIME is hundred nanoseconds
|
||||
REFERENCE_TIME device_period;
|
||||
result = client->GetDevicePeriod(&device_period, nullptr);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Unable to get device period");
|
||||
}
|
||||
FormatDebug(wasapi_output_domain, "Device period: %I64u ns",
|
||||
size_t(ns(hundred_ns(device_period)).count()));
|
||||
|
||||
REFERENCE_TIME buffer_duration = device_period;
|
||||
if (!Exclusive()) {
|
||||
const REFERENCE_TIME align = hundred_ns(ms(50)).count();
|
||||
buffer_duration = (align / device_period) * device_period;
|
||||
}
|
||||
FormatDebug(wasapi_output_domain, "Buffer duration: %I64u ns",
|
||||
size_t(ns(hundred_ns(buffer_duration)).count()));
|
||||
|
||||
if (Exclusive()) {
|
||||
result = client->Initialize(
|
||||
AUDCLNT_SHAREMODE_EXCLUSIVE,
|
||||
AUDCLNT_STREAMFLAGS_NOPERSIST | AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
|
||||
buffer_duration, buffer_duration,
|
||||
reinterpret_cast<WAVEFORMATEX *>(&device_format), nullptr);
|
||||
if (result == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) {
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-iaudioclient-initialize
|
||||
UINT32 buffer_size_in_frames = 0;
|
||||
result = client->GetBufferSize(&buffer_size_in_frames);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(
|
||||
result, "Unable to get audio client buffer size");
|
||||
}
|
||||
buffer_duration = std::ceil(
|
||||
double(buffer_size_in_frames * hundred_ns(s(1)).count()) /
|
||||
SampleRate());
|
||||
FormatDebug(wasapi_output_domain,
|
||||
"Aligned buffer duration: %I64u ns",
|
||||
size_t(ns(hundred_ns(buffer_duration)).count()));
|
||||
client.reset();
|
||||
result = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL,
|
||||
nullptr, client.AddressCast());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(
|
||||
result, "Unable to activate audio client");
|
||||
}
|
||||
result = client->Initialize(
|
||||
AUDCLNT_SHAREMODE_EXCLUSIVE,
|
||||
AUDCLNT_STREAMFLAGS_NOPERSIST |
|
||||
AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
|
||||
buffer_duration, buffer_duration,
|
||||
reinterpret_cast<WAVEFORMATEX *>(&device_format),
|
||||
nullptr);
|
||||
}
|
||||
} else {
|
||||
result = client->Initialize(
|
||||
AUDCLNT_SHAREMODE_SHARED,
|
||||
AUDCLNT_STREAMFLAGS_NOPERSIST | AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
|
||||
buffer_duration, 0,
|
||||
reinterpret_cast<WAVEFORMATEX *>(&device_format), nullptr);
|
||||
}
|
||||
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Unable to initialize audio client");
|
||||
}
|
||||
|
||||
ComPtr<IAudioRenderClient> render_client;
|
||||
result = client->GetService(IID_PPV_ARGS(render_client.Address()));
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Unable to get new render client");
|
||||
}
|
||||
|
||||
result = client->SetEventHandle(event->handle());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Unable to set event handler");
|
||||
}
|
||||
|
||||
UINT32 buffer_size_in_frames;
|
||||
result = client->GetBufferSize(&buffer_size_in_frames);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result,
|
||||
"Unable to get audio client buffer size");
|
||||
}
|
||||
|
||||
watermark = buffer_size_in_frames * 3 * FrameSize();
|
||||
spsc_buffer = std::make_unique<boost::lockfree::spsc_queue<BYTE>>(
|
||||
buffer_size_in_frames * 4 * FrameSize());
|
||||
thread = std::make_unique<WasapiOutputThread>(
|
||||
event, client, std::move(render_client), FrameSize(),
|
||||
buffer_size_in_frames, is_exclusive, *spsc_buffer);
|
||||
thread->Start();
|
||||
}
|
||||
|
||||
void WasapiOutput::Close() noexcept {
|
||||
assert(client && thread);
|
||||
Pause();
|
||||
thread->Finish();
|
||||
thread->Join();
|
||||
thread.reset();
|
||||
spsc_buffer.reset();
|
||||
client.reset();
|
||||
}
|
||||
|
||||
std::chrono::steady_clock::duration WasapiOutput::Delay() const noexcept {
|
||||
if (!client || !is_started) {
|
||||
return std::chrono::steady_clock::duration::zero();
|
||||
}
|
||||
|
||||
const size_t data_size = spsc_buffer->read_available();
|
||||
const size_t delay_size = std::max(data_size, watermark) - watermark;
|
||||
|
||||
using s = std::chrono::seconds;
|
||||
using duration = std::chrono::steady_clock::duration;
|
||||
auto result = duration(s(delay_size)) / device_format.Format.nAvgBytesPerSec;
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t WasapiOutput::Play(const void *chunk, size_t size) {
|
||||
if (!client || !thread) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
do {
|
||||
const size_t consumed_size =
|
||||
spsc_buffer->push(static_cast<const BYTE *>(chunk), size);
|
||||
if (consumed_size == 0) {
|
||||
assert(is_started);
|
||||
thread->WaitWrite();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_started) {
|
||||
is_started = true;
|
||||
|
||||
thread->Play();
|
||||
|
||||
HRESULT result;
|
||||
result = client->Start();
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result,
|
||||
"Failed to start client");
|
||||
}
|
||||
}
|
||||
|
||||
thread->CheckException();
|
||||
|
||||
return consumed_size;
|
||||
} while (true);
|
||||
}
|
||||
|
||||
void WasapiOutput::Drain() {
|
||||
spsc_buffer->consume_all([](auto &&) {});
|
||||
thread->CheckException();
|
||||
}
|
||||
|
||||
bool WasapiOutput::Pause() {
|
||||
if (!client || !thread) {
|
||||
return false;
|
||||
}
|
||||
if (!is_started) {
|
||||
return true;
|
||||
}
|
||||
|
||||
HRESULT result;
|
||||
result = client->Stop();
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Failed to stop client");
|
||||
}
|
||||
|
||||
is_started = false;
|
||||
thread->Pause();
|
||||
thread->CheckException();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WasapiOutput::FindExclusiveFormatSupported(AudioFormat &audio_format) {
|
||||
SetFormat(device_format, audio_format);
|
||||
|
||||
do {
|
||||
HRESULT result;
|
||||
result = client->IsFormatSupported(
|
||||
AUDCLNT_SHAREMODE_EXCLUSIVE,
|
||||
reinterpret_cast<WAVEFORMATEX *>(&device_format), nullptr);
|
||||
|
||||
switch (result) {
|
||||
case S_OK:
|
||||
return;
|
||||
case AUDCLNT_E_UNSUPPORTED_FORMAT:
|
||||
break;
|
||||
default:
|
||||
throw FormatHResultError(result, "IsFormatSupported failed");
|
||||
}
|
||||
|
||||
// Trying PCM fallback.
|
||||
if (audio_format.format == SampleFormat::FLOAT) {
|
||||
audio_format.format = SampleFormat::S32;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trying sample rate fallback.
|
||||
if (audio_format.sample_rate > 96000) {
|
||||
audio_format.sample_rate = 96000;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (audio_format.sample_rate > 88200) {
|
||||
audio_format.sample_rate = 88200;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (audio_format.sample_rate > 64000) {
|
||||
audio_format.sample_rate = 64000;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (audio_format.sample_rate > 48000) {
|
||||
audio_format.sample_rate = 48000;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trying 2 channels fallback.
|
||||
if (audio_format.channels > 2) {
|
||||
audio_format.channels = 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trying S16 fallback.
|
||||
if (audio_format.format == SampleFormat::S32) {
|
||||
audio_format.format = SampleFormat::S16;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (audio_format.sample_rate > 41100) {
|
||||
audio_format.sample_rate = 41100;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw FormatHResultError(result, "Format is not supported");
|
||||
} while (true);
|
||||
}
|
||||
|
||||
void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) {
|
||||
HRESULT result;
|
||||
ComHeapPtr<WAVEFORMATEX> mixer_format;
|
||||
|
||||
// In shared mode, different sample rate is always unsupported.
|
||||
result = client->GetMixFormat(mixer_format.Address());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "GetMixFormat failed");
|
||||
}
|
||||
audio_format.sample_rate = device_format.Format.nSamplesPerSec;
|
||||
|
||||
SetFormat(device_format, audio_format);
|
||||
|
||||
ComHeapPtr<WAVEFORMATEXTENSIBLE> closest_format;
|
||||
result = client->IsFormatSupported(
|
||||
AUDCLNT_SHAREMODE_SHARED,
|
||||
reinterpret_cast<WAVEFORMATEX *>(&device_format),
|
||||
closest_format.AddressCast<WAVEFORMATEX>());
|
||||
|
||||
if (FAILED(result) && result != AUDCLNT_E_UNSUPPORTED_FORMAT) {
|
||||
throw FormatHResultError(result, "IsFormatSupported failed");
|
||||
}
|
||||
|
||||
switch (result) {
|
||||
case S_OK:
|
||||
break;
|
||||
case AUDCLNT_E_UNSUPPORTED_FORMAT:
|
||||
default:
|
||||
// Trying channels fallback.
|
||||
audio_format.channels = mixer_format->nChannels;
|
||||
|
||||
SetFormat(device_format, audio_format);
|
||||
|
||||
result = client->IsFormatSupported(
|
||||
AUDCLNT_SHAREMODE_SHARED,
|
||||
reinterpret_cast<WAVEFORMATEX *>(&device_format),
|
||||
closest_format.AddressCast<WAVEFORMATEX>());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Format is not supported");
|
||||
}
|
||||
break;
|
||||
case S_FALSE:
|
||||
if (closest_format->Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
|
||||
device_format = *closest_format;
|
||||
} else {
|
||||
device_format.Samples.wValidBitsPerSample =
|
||||
device_format.Format.wBitsPerSample;
|
||||
device_format.Format = closest_format->Format;
|
||||
switch (std::exchange(device_format.Format.wFormatTag,
|
||||
WAVE_FORMAT_EXTENSIBLE)) {
|
||||
case WAVE_FORMAT_PCM:
|
||||
device_format.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
|
||||
break;
|
||||
case WAVE_FORMAT_IEEE_FLOAT:
|
||||
device_format.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
|
||||
break;
|
||||
default:
|
||||
gcc_unreachable();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Copy closest match back to audio_format.
|
||||
audio_format.channels = device_format.Format.nChannels;
|
||||
audio_format.sample_rate = device_format.Format.nSamplesPerSec;
|
||||
if (device_format.SubFormat == KSDATAFORMAT_SUBTYPE_PCM) {
|
||||
switch (device_format.Format.wBitsPerSample) {
|
||||
case 8:
|
||||
audio_format.format = SampleFormat::S8;
|
||||
break;
|
||||
case 16:
|
||||
audio_format.format = SampleFormat::S16;
|
||||
break;
|
||||
case 32:
|
||||
audio_format.format = SampleFormat::S32;
|
||||
break;
|
||||
}
|
||||
} else if (device_format.SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {
|
||||
audio_format.format = SampleFormat::FLOAT;
|
||||
}
|
||||
}
|
||||
|
||||
void WasapiOutput::EnumerateDevices() {
|
||||
if (!device_desc.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
HRESULT result;
|
||||
|
||||
ComPtr<IMMDeviceCollection> device_collection;
|
||||
result = enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE,
|
||||
device_collection.Address());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Unable to enumerate devices");
|
||||
}
|
||||
|
||||
UINT count;
|
||||
result = device_collection->GetCount(&count);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Collection->GetCount failed");
|
||||
}
|
||||
|
||||
device_desc.reserve(count);
|
||||
for (UINT i = 0; i < count; ++i) {
|
||||
ComPtr<IMMDevice> enumerated_device;
|
||||
result = device_collection->Item(i, enumerated_device.Address());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Collection->Item failed");
|
||||
}
|
||||
|
||||
ComPtr<IPropertyStore> property_store;
|
||||
result = enumerated_device->OpenPropertyStore(STGM_READ,
|
||||
property_store.Address());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result,
|
||||
"Device->OpenPropertyStore failed");
|
||||
}
|
||||
|
||||
PROPVARIANT var_name;
|
||||
PropVariantInit(&var_name);
|
||||
AtScopeExit(&) { PropVariantClear(&var_name); };
|
||||
|
||||
result = property_store->GetValue(PKEY_Device_FriendlyName, &var_name);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result,
|
||||
"PropertyStore->GetValue failed");
|
||||
}
|
||||
|
||||
device_desc.emplace_back(
|
||||
i, WideCharToMultiByte(CP_UTF8,
|
||||
std::wstring_view(var_name.pwszVal)));
|
||||
}
|
||||
}
|
||||
|
||||
void WasapiOutput::GetDevice(unsigned int index) {
|
||||
HRESULT result;
|
||||
|
||||
ComPtr<IMMDeviceCollection> device_collection;
|
||||
result = enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE,
|
||||
device_collection.Address());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Unable to enumerate devices");
|
||||
}
|
||||
|
||||
result = device_collection->Item(index, device.Address());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Collection->Item failed");
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int WasapiOutput::SearchDevice(std::string_view name) {
|
||||
if (!SafeTry([this]() { EnumerateDevices(); })) {
|
||||
return kErrorId;
|
||||
}
|
||||
auto iter =
|
||||
std::find_if(device_desc.cbegin(), device_desc.cend(),
|
||||
[&name](const auto &desc) { return desc.second == name; });
|
||||
if (iter == device_desc.cend()) {
|
||||
FormatError(wasapi_output_domain, "Device %.*s not founded.",
|
||||
int(name.size()), name.data());
|
||||
return kErrorId;
|
||||
}
|
||||
FormatInfo(wasapi_output_domain, "Select device \"%u\" \"%s\"", iter->first,
|
||||
iter->second.c_str());
|
||||
return iter->first;
|
||||
}
|
||||
|
||||
void WasapiOutput::GetDefaultDevice() {
|
||||
HRESULT result;
|
||||
result = enumerator->GetDefaultAudioEndpoint(eRender, eMultimedia,
|
||||
device.Address());
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result,
|
||||
"Unable to get default device for multimedia");
|
||||
}
|
||||
}
|
||||
|
||||
static bool wasapi_output_test_default_device() { return true; }
|
||||
|
||||
const struct AudioOutputPlugin wasapi_output_plugin = {
|
||||
"wasapi",
|
||||
wasapi_output_test_default_device,
|
||||
WasapiOutput::Create,
|
||||
&wasapi_mixer_plugin,
|
||||
};
|
@@ -28,6 +28,10 @@
|
||||
#include <array>
|
||||
#include <iterator>
|
||||
|
||||
#include <handleapi.h>
|
||||
#include <synchapi.h>
|
||||
#include <winbase.h> // for INFINITE
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
@@ -26,7 +26,7 @@
|
||||
|
||||
#include "util/Compiler.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <windef.h>
|
||||
#include <mmsystem.h>
|
||||
|
||||
struct WinmmOutput;
|
||||
|
@@ -136,7 +136,7 @@ bool
|
||||
HttpdClient::SendResponse() noexcept
|
||||
{
|
||||
char buffer[1024];
|
||||
AllocatedString<> allocated = nullptr;
|
||||
AllocatedString allocated;
|
||||
const char *response;
|
||||
|
||||
assert(state == State::RESPONSE);
|
||||
@@ -162,6 +162,7 @@ HttpdClient::SendResponse() noexcept
|
||||
"Connection: close\r\n"
|
||||
"Pragma: no-cache\r\n"
|
||||
"Cache-Control: no-cache, no-store\r\n"
|
||||
"Access-Control-Allow-Origin: *\r\n"
|
||||
"\r\n",
|
||||
httpd.content_type);
|
||||
response = buffer;
|
||||
@@ -278,7 +279,7 @@ HttpdClient::TryWrite() noexcept
|
||||
metadata_current_position);
|
||||
if (nbytes < 0) {
|
||||
auto e = GetSocketError();
|
||||
if (IsSocketErrorAgain(e))
|
||||
if (IsSocketErrorSendWouldBlock(e))
|
||||
return true;
|
||||
|
||||
if (!IsSocketErrorClosed(e)) {
|
||||
@@ -305,7 +306,7 @@ HttpdClient::TryWrite() noexcept
|
||||
ssize_t nbytes = GetSocket().Write(&empty_data, 1);
|
||||
if (nbytes < 0) {
|
||||
auto e = GetSocketError();
|
||||
if (IsSocketErrorAgain(e))
|
||||
if (IsSocketErrorSendWouldBlock(e))
|
||||
return true;
|
||||
|
||||
if (!IsSocketErrorClosed(e)) {
|
||||
@@ -328,7 +329,7 @@ HttpdClient::TryWrite() noexcept
|
||||
bytes_to_write);
|
||||
if (nbytes < 0) {
|
||||
auto e = GetSocketError();
|
||||
if (IsSocketErrorAgain(e))
|
||||
if (IsSocketErrorSendWouldBlock(e))
|
||||
return true;
|
||||
|
||||
if (!IsSocketErrorClosed(e)) {
|
||||
|
@@ -27,7 +27,7 @@
|
||||
|
||||
#include <string.h>
|
||||
|
||||
AllocatedString<>
|
||||
AllocatedString
|
||||
icy_server_metadata_header(const char *name,
|
||||
const char *genre, const char *url,
|
||||
const char *content_type, int metaint) noexcept
|
||||
@@ -45,6 +45,7 @@ icy_server_metadata_header(const char *name,
|
||||
"Connection: close\r\n"
|
||||
"Pragma: no-cache\r\n"
|
||||
"Cache-Control: no-cache, no-store\r\n"
|
||||
"Access-Control-Allow-Origin: *\r\n"
|
||||
"\r\n",
|
||||
name,
|
||||
genre,
|
||||
@@ -54,7 +55,7 @@ icy_server_metadata_header(const char *name,
|
||||
content_type);
|
||||
}
|
||||
|
||||
static AllocatedString<>
|
||||
static AllocatedString
|
||||
icy_server_metadata_string(const char *stream_title,
|
||||
const char* stream_url) noexcept
|
||||
{
|
||||
@@ -110,7 +111,7 @@ icy_server_metadata_page(const Tag &tag, const TagType *types) noexcept
|
||||
|
||||
const auto icy_string = icy_server_metadata_string(stream_title, "");
|
||||
|
||||
if (icy_string.IsNull())
|
||||
if (icy_string == nullptr)
|
||||
return nullptr;
|
||||
|
||||
return std::make_shared<Page>(icy_string.c_str(),
|
||||
|
@@ -24,9 +24,9 @@
|
||||
#include "tag/Type.h"
|
||||
|
||||
struct Tag;
|
||||
template<typename T> class AllocatedString;
|
||||
class AllocatedString;
|
||||
|
||||
AllocatedString<char>
|
||||
AllocatedString
|
||||
icy_server_metadata_header(const char *name,
|
||||
const char *genre, const char *url,
|
||||
const char *content_type, int metaint) noexcept;
|
||||
|
@@ -136,11 +136,12 @@ endif
|
||||
output_features.set('ENABLE_WASAPI_OUTPUT', is_windows)
|
||||
if is_windows
|
||||
output_plugins_sources += [
|
||||
'WasapiOutputPlugin.cxx',
|
||||
'wasapi/WasapiOutputPlugin.cxx',
|
||||
]
|
||||
wasapi_dep = [
|
||||
c_compiler.find_library('ksuser', required: true),
|
||||
c_compiler.find_library('ole32', required: true),
|
||||
win32_dep,
|
||||
]
|
||||
else
|
||||
wasapi_dep = dependency('', required: false)
|
||||
|
103
src/output/plugins/wasapi/AudioClient.hxx
Normal file
103
src/output/plugins/wasapi/AudioClient.hxx
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2020-2021 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifndef MPD_WASAPI_AUDIO_CLIENT_HXX
|
||||
#define MPD_WASAPI_AUDIO_CLIENT_HXX
|
||||
|
||||
#include "win32/ComHeapPtr.hxx"
|
||||
#include "win32/ComPtr.hxx"
|
||||
#include "win32/HResult.hxx"
|
||||
|
||||
#include <audioclient.h>
|
||||
|
||||
inline UINT32
|
||||
GetBufferSizeInFrames(IAudioClient &client)
|
||||
{
|
||||
UINT32 buffer_size_in_frames;
|
||||
|
||||
HRESULT result = client.GetBufferSize(&buffer_size_in_frames);
|
||||
if (FAILED(result))
|
||||
throw MakeHResultError(result,
|
||||
"Unable to get audio client buffer size");
|
||||
|
||||
return buffer_size_in_frames;
|
||||
}
|
||||
|
||||
inline UINT32
|
||||
GetCurrentPaddingFrames(IAudioClient &client)
|
||||
{
|
||||
UINT32 padding_frames;
|
||||
|
||||
HRESULT result = client.GetCurrentPadding(&padding_frames);
|
||||
if (FAILED(result))
|
||||
throw MakeHResultError(result,
|
||||
"Failed to get current padding");
|
||||
|
||||
return padding_frames;
|
||||
}
|
||||
|
||||
inline ComHeapPtr<WAVEFORMATEX>
|
||||
GetMixFormat(IAudioClient &client)
|
||||
{
|
||||
WAVEFORMATEX *f;
|
||||
|
||||
HRESULT result = client.GetMixFormat(&f);
|
||||
if (FAILED(result))
|
||||
throw MakeHResultError(result, "GetMixFormat failed");
|
||||
|
||||
return ComHeapPtr{f};
|
||||
}
|
||||
|
||||
inline void
|
||||
Start(IAudioClient &client)
|
||||
{
|
||||
HRESULT result = client.Start();
|
||||
if (FAILED(result))
|
||||
throw MakeHResultError(result, "Failed to start client");
|
||||
}
|
||||
|
||||
inline void
|
||||
Stop(IAudioClient &client)
|
||||
{
|
||||
HRESULT result = client.Stop();
|
||||
if (FAILED(result))
|
||||
throw MakeHResultError(result, "Failed to stop client");
|
||||
}
|
||||
|
||||
inline void
|
||||
SetEventHandle(IAudioClient &client, HANDLE h)
|
||||
{
|
||||
HRESULT result = client.SetEventHandle(h);
|
||||
if (FAILED(result))
|
||||
throw MakeHResultError(result, "Unable to set event handle");
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline ComPtr<T>
|
||||
GetService(IAudioClient &client)
|
||||
{
|
||||
T *p = nullptr;
|
||||
HRESULT result = client.GetService(IID_PPV_ARGS(&p));
|
||||
if (FAILED(result))
|
||||
throw MakeHResultError(result, "Unable to get service");
|
||||
|
||||
return ComPtr{p};
|
||||
}
|
||||
|
||||
#endif
|
117
src/output/plugins/wasapi/Device.hxx
Normal file
117
src/output/plugins/wasapi/Device.hxx
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2020-2021 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifndef MPD_WASAPI_DEVICE_COLLECTION_HXX
|
||||
#define MPD_WASAPI_DEVICE_COLLECTION_HXX
|
||||
|
||||
#include "win32/ComPtr.hxx"
|
||||
#include "win32/HResult.hxx"
|
||||
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
inline ComPtr<IMMDevice>
|
||||
GetDefaultAudioEndpoint(IMMDeviceEnumerator &e)
|
||||
{
|
||||
IMMDevice *device = nullptr;
|
||||
|
||||
HRESULT result = e.GetDefaultAudioEndpoint(eRender, eMultimedia,
|
||||
&device);
|
||||
if (FAILED(result))
|
||||
throw MakeHResultError(result,
|
||||
"Unable to get default device for multimedia");
|
||||
|
||||
return ComPtr{device};
|
||||
}
|
||||
|
||||
inline ComPtr<IMMDeviceCollection>
|
||||
EnumAudioEndpoints(IMMDeviceEnumerator &e)
|
||||
{
|
||||
IMMDeviceCollection *dc = nullptr;
|
||||
|
||||
HRESULT result = e.EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE,
|
||||
&dc);
|
||||
if (FAILED(result))
|
||||
throw MakeHResultError(result, "Unable to enumerate devices");
|
||||
|
||||
return ComPtr{dc};
|
||||
}
|
||||
|
||||
inline UINT
|
||||
GetCount(IMMDeviceCollection &dc)
|
||||
{
|
||||
UINT count;
|
||||
|
||||
HRESULT result = dc.GetCount(&count);
|
||||
if (FAILED(result))
|
||||
throw MakeHResultError(result, "Collection->GetCount failed");
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
inline ComPtr<IMMDevice>
|
||||
Item(IMMDeviceCollection &dc, UINT i)
|
||||
{
|
||||
IMMDevice *device = nullptr;
|
||||
|
||||
auto result = dc.Item(i, &device);
|
||||
if (FAILED(result))
|
||||
throw MakeHResultError(result, "Collection->Item failed");
|
||||
|
||||
return ComPtr{device};
|
||||
}
|
||||
|
||||
inline DWORD
|
||||
GetState(IMMDevice &device)
|
||||
{
|
||||
DWORD state;
|
||||
|
||||
HRESULT result = device.GetState(&state);;
|
||||
if (FAILED(result))
|
||||
throw MakeHResultError(result, "Unable to get device status");
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline ComPtr<T>
|
||||
Activate(IMMDevice &device)
|
||||
{
|
||||
T *p = nullptr;
|
||||
HRESULT result = device.Activate(__uuidof(T), CLSCTX_ALL,
|
||||
nullptr, (void **)&p);
|
||||
if (FAILED(result))
|
||||
throw MakeHResultError(result, "Unable to activate device");
|
||||
|
||||
return ComPtr{p};
|
||||
}
|
||||
|
||||
inline ComPtr<IPropertyStore>
|
||||
OpenPropertyStore(IMMDevice &device)
|
||||
{
|
||||
IPropertyStore *property_store = nullptr;
|
||||
|
||||
HRESULT result = device.OpenPropertyStore(STGM_READ, &property_store);
|
||||
if (FAILED(result))
|
||||
throw MakeHResultError(result,
|
||||
"Device->OpenPropertyStore failed");
|
||||
|
||||
return ComPtr{property_store};
|
||||
}
|
||||
|
||||
#endif
|
51
src/output/plugins/wasapi/ForMixer.hxx
Normal file
51
src/output/plugins/wasapi/ForMixer.hxx
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2020-2021 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifndef MPD_WASAPI_OUTPUT_FOR_MIXER_HXX
|
||||
#define MPD_WASAPI_OUTPUT_FOR_MIXER_HXX
|
||||
|
||||
#include <memory>
|
||||
|
||||
struct IMMDevice;
|
||||
struct IAudioClient;
|
||||
class AudioOutput;
|
||||
class WasapiOutput;
|
||||
class COMWorker;
|
||||
|
||||
[[gnu::pure]]
|
||||
WasapiOutput &
|
||||
wasapi_output_downcast(AudioOutput &output) noexcept;
|
||||
|
||||
[[gnu::pure]]
|
||||
bool
|
||||
wasapi_is_exclusive(WasapiOutput &output) noexcept;
|
||||
|
||||
[[gnu::pure]]
|
||||
std::shared_ptr<COMWorker>
|
||||
wasapi_output_get_com_worker(WasapiOutput &output) noexcept;
|
||||
|
||||
[[gnu::pure]]
|
||||
IMMDevice *
|
||||
wasapi_output_get_device(WasapiOutput &output) noexcept;
|
||||
|
||||
[[gnu::pure]]
|
||||
IAudioClient *
|
||||
wasapi_output_get_client(WasapiOutput &output) noexcept;
|
||||
|
||||
#endif
|
44
src/output/plugins/wasapi/PropertyStore.hxx
Normal file
44
src/output/plugins/wasapi/PropertyStore.hxx
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2020-2021 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifndef MPD_WASAPI_PROPERTY_STORE_HXX
|
||||
#define MPD_WASAPI_PROPERTY_STORE_HXX
|
||||
|
||||
#include "win32/PropVariant.hxx"
|
||||
#include "util/AllocatedString.hxx"
|
||||
#include "util/ScopeExit.hxx"
|
||||
|
||||
#include <propsys.h>
|
||||
|
||||
[[gnu::pure]]
|
||||
inline AllocatedString
|
||||
GetString(IPropertyStore &ps, REFPROPERTYKEY key) noexcept
|
||||
{
|
||||
PROPVARIANT pv;
|
||||
PropVariantInit(&pv);
|
||||
|
||||
HRESULT result = ps.GetValue(key, &pv);
|
||||
if (FAILED(result))
|
||||
return nullptr;
|
||||
|
||||
AtScopeExit(&) { PropVariantClear(&pv); };
|
||||
return ToString(pv);
|
||||
}
|
||||
|
||||
#endif
|
1076
src/output/plugins/wasapi/WasapiOutputPlugin.cxx
Normal file
1076
src/output/plugins/wasapi/WasapiOutputPlugin.cxx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -20,25 +20,6 @@
|
||||
#ifndef MPD_WASAPI_OUTPUT_PLUGIN_HXX
|
||||
#define MPD_WASAPI_OUTPUT_PLUGIN_HXX
|
||||
|
||||
#include "output/Features.h"
|
||||
|
||||
#include "../OutputAPI.hxx"
|
||||
#include "util/Compiler.h"
|
||||
#include "win32/ComPtr.hxx"
|
||||
|
||||
#include <audioclient.h>
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
extern const struct AudioOutputPlugin wasapi_output_plugin;
|
||||
|
||||
class WasapiOutput;
|
||||
|
||||
gcc_pure WasapiOutput &wasapi_output_downcast(AudioOutput &output) noexcept;
|
||||
|
||||
gcc_pure bool wasapi_is_exclusive(WasapiOutput &output) noexcept;
|
||||
|
||||
gcc_pure IMMDevice *wasapi_output_get_device(WasapiOutput &output) noexcept;
|
||||
|
||||
gcc_pure IAudioClient *wasapi_output_get_client(WasapiOutput &output) noexcept;
|
||||
|
||||
#endif
|
@@ -94,7 +94,7 @@ ParseCommandArgRange(const char *s)
|
||||
s);
|
||||
|
||||
if (test == test2)
|
||||
value = std::numeric_limits<int>::max();
|
||||
return RangeArg::OpenEnded(range.start);
|
||||
|
||||
if (value < 0)
|
||||
throw FormatProtocolError(ACK_ERROR_ARG,
|
||||
@@ -107,9 +107,13 @@ ParseCommandArgRange(const char *s)
|
||||
|
||||
range.end = (unsigned)value;
|
||||
} else {
|
||||
range.end = (unsigned)value + 1;
|
||||
return RangeArg::Single(range.start);
|
||||
}
|
||||
|
||||
if (!range.IsWellFormed())
|
||||
throw FormatProtocolError(ACK_ERROR_ARG,
|
||||
"Malformed range: %s", s);
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
|
@@ -25,8 +25,22 @@
|
||||
struct RangeArg {
|
||||
unsigned start, end;
|
||||
|
||||
static constexpr RangeArg All() {
|
||||
return { 0, std::numeric_limits<unsigned>::max() };
|
||||
/**
|
||||
* Construct an open-ended range starting at the given index.
|
||||
*/
|
||||
static constexpr RangeArg OpenEnded(unsigned start) noexcept {
|
||||
return { start, std::numeric_limits<unsigned>::max() };
|
||||
}
|
||||
|
||||
static constexpr RangeArg All() noexcept {
|
||||
return OpenEnded(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance describing exactly one index.
|
||||
*/
|
||||
static constexpr RangeArg Single(unsigned i) noexcept {
|
||||
return { i, i + 1 };
|
||||
}
|
||||
|
||||
constexpr bool operator==(RangeArg other) const noexcept {
|
||||
@@ -37,13 +51,45 @@ struct RangeArg {
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
constexpr bool IsOpenEnded() const noexcept {
|
||||
return end == All().end;
|
||||
}
|
||||
|
||||
constexpr bool IsAll() const noexcept {
|
||||
return *this == All();
|
||||
}
|
||||
|
||||
constexpr bool IsWellFormed() const noexcept {
|
||||
return start <= end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this range empty? A malformed range also counts as
|
||||
* "empty" for this method.
|
||||
*/
|
||||
constexpr bool IsEmpty() const noexcept {
|
||||
return start >= end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the range contains at least this number of items.
|
||||
* Unlike Count(), this allows the object to be malformed.
|
||||
*/
|
||||
constexpr bool HasAtLeast(unsigned n) const noexcept {
|
||||
return start + n <= end;
|
||||
}
|
||||
|
||||
constexpr bool Contains(unsigned i) const noexcept {
|
||||
return i >= start && i < end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of items covered by this range. This requires the
|
||||
* object to be well-formed.
|
||||
*/
|
||||
constexpr unsigned Count() const noexcept {
|
||||
return end - start;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@@ -44,13 +44,13 @@ playlist::TagModified(DetachedSong &&song) noexcept
|
||||
}
|
||||
|
||||
void
|
||||
playlist::TagModified(const char *uri, const Tag &tag) noexcept
|
||||
playlist::TagModified(const char *real_uri, const Tag &tag) noexcept
|
||||
{
|
||||
bool modified = false;
|
||||
|
||||
for (unsigned i = 0; i < queue.length; ++i) {
|
||||
auto &song = *queue.items[i].song;
|
||||
if (song.IsURI(uri)) {
|
||||
if (song.IsRealURI(real_uri)) {
|
||||
song.SetTag(tag);
|
||||
queue.ModifyAtPosition(i);
|
||||
modified = true;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user