Compare commits

...

67 Commits

Author SHA1 Message Date
Max Kellermann
1f28790476 release v0.23.8 2022-07-09 01:05:38 +02:00
Max Kellermann
c8dae95eff output/PipeWire: after Cancel(), refill buffer before resuming playback
Deactivate the stream in Cancel().  This fixes stuttering after a
manual song change by refilling the whole ring buffer before
reactivating the stream.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1354
2022-07-09 01:03:36 +02:00
Max Kellermann
547a084c7e output/PipeWire: call pw_stream_flush() in Cancel()
Clear not only MPD's ring buffer, but also libpipewire's buffers, to
avoid playing some audio from the previous song after a manual song
change.

Fixes part 1 of https://github.com/MusicPlayerDaemon/MPD/issues/1354
2022-07-09 01:01:29 +02:00
Max Kellermann
493677ff81 output/PipeWire: skip Cancel() if already drained 2022-07-09 00:53:53 +02:00
Max Kellermann
6b430ba271 output/PipeWire: activate stream in Drain() 2022-07-09 00:53:20 +02:00
Max Kellermann
bc6924d303 output/snapcast: fix busy loop while paused
Removing the LockHasClients(); this code was copied from the "httpd"
output plugin, but unlike "httpd", the SnapCast output plugin does not
feed silence while paused, so we need to implement a delay to avoid
busy-looping the CPU.

As a side effect, this eliminates the suttering after resuming
playback, because the timer now gets reset even if there is a client.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1394
2022-07-08 22:55:41 +02:00
Max Kellermann
02b00f9146 output/PipeWire: don't force initial volume=100%
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1484
2022-07-08 18:25:41 +02:00
Max Kellermann
e807ed5870 output/PipeWire: ignore SPA_PROP_channelVolumes if n_values==0
After connecting, PipeWire sometimes sends SPA_PROP_channelVolumes
with no values, and this led to "volume=-NaN".
2022-07-08 18:13:33 +02:00
Max Kellermann
f08944253b output/PipeWire: check SPA_PROP_channelVolumes, not control name
Since PipeWire 0.3.53, there is no control name anymore, therefore the
name check doesn't work anymore, breaking volume change events.

This obsoletes the crash bug fix in commit 2ee57f9b0d
2022-07-08 18:06:36 +02:00
Max Kellermann
792d6584b9 output/PipeWire: move code to OnChannelVolumes() 2022-07-08 18:02:36 +02:00
Max Kellermann
7b45d01462 output/PipeWire: update field "volume" 2022-07-08 17:44:39 +02:00
Max Kellermann
5c17b2966a output/PipeWire: use std::accumulate 2022-07-08 17:44:08 +02:00
Max Kellermann
0c54f29446 output/PipeWire: document field "volume" 2022-07-08 17:30:57 +02:00
Max Kellermann
9c3cf39fdd output/PipeWire: catch exceptions in ParamChanged()
Fixes a potential crash bug.
2022-07-08 17:24:41 +02:00
Max Kellermann
d2fb229685 output/PipeWire: call ::SetVolume() in ParamChanged()
This is a lower-level function without some of the clutter of
PipeWireOutput::SetVolume() which is not needed in that case.
2022-07-08 17:21:17 +02:00
Max Kellermann
f55bc6682f output/PipeWire: move code to ::SetVolume() 2022-07-08 17:19:10 +02:00
Max Kellermann
6857286b42 decoder/Thread: don't scan for replay gain tags in PCM streams
This disables a long delay for playing songs from the cdio_paranoia
input plugin if ReplayGain is enabled.
2022-07-08 16:33:19 +02:00
Max Kellermann
c0d5bd2048 decoder/Thread: move code to DecoderControl::LockIsReplayGainEnabled() 2022-07-08 16:21:53 +02:00
Max Kellermann
666e5d7904 input/CdioParanoia: use integer modulo to calculate "diff" 2022-07-08 16:04:05 +02:00
Max Kellermann
3613407ac5 input/CdioParanoia: use typedef lsn_t 2022-07-08 16:03:04 +02:00
Max Kellermann
c32dceb4d4 input/CdioParanoia: remove loop from Read()
The Read() method is not required to fill the whole buffer.  By
returning as soon as at least one byte was read, we allow faster
cancellation.
2022-07-08 16:01:23 +02:00
Max Kellermann
5573e78364 input/CdioParanoia: skip seek if seeking within the buffer 2022-07-08 13:57:11 +02:00
Max Kellermann
807a19889f input/CdioParanoia: update offset only after successful seek
If seeking fails, don't leave the class with a wrong offset.
2022-07-08 13:57:11 +02:00
Max Kellermann
df7242de91 input/CdioParanoia: eliminate redundant field "lsn_relofs" 2022-07-08 13:36:59 +02:00
Max Kellermann
d62426f168 input/CdioParanoia: eliminate redundant field "lsn_to"
Use "size" instead.
2022-07-08 12:42:49 +02:00
Max Kellermann
1714cf3417 input/CdioParanoia: use IsEof() in Read() 2022-07-08 12:42:42 +02:00
Max Kellermann
1080c917be input/CdioParanoia: use std::min() 2022-07-08 12:37:21 +02:00
Max Kellermann
8eb3164878 input/CdioParanoia: fix crash if no drive was found
cdio_get_devices_with_cap() can return nullptr if no drive was found,
or it can instead return an empty list.  The latter caused MPD to
crash.
2022-07-08 12:05:20 +02:00
Max Kellermann
915c5442d1 input/CdioParanoia: use AtScopeExit() for cdio_free_device_list() 2022-07-08 12:03:57 +02:00
Max Kellermann
be0360d5e8 doc/user.rst: clarify .mpdignore documentation
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1532
2022-07-08 11:44:14 +02:00
Max Kellermann
4d6ae6ffdd output/PipeWire: add nullptr check to SetVolume()
If the PipeWire output has not yet been enabled and no thread_loop has
been created yet, a nullptr dereference in SetVolume() was possible
because nullptr was passed to pw_thread_loop_lock().
2022-07-08 11:32:59 +02:00
Max Kellermann
ecee6f415b mixer/MixerInternal: remember error details
If a mixer is not open, rethrow the original exception each time
setting the volume is requested.  This further improves error messages
sent to MPD clients.
2022-07-08 11:11:53 +02:00
Max Kellermann
47680f936b mixer/All: auto-open "global" mixers
If a mixer is "global", it is available even if the output isn't
open.  However, since the check was changed from IsEnabled() to
IsReallyEnabled(), enabled outputs have not yet been used have not
been "really" enabled yet, preventing using the mixer.

Fixes a regression by commit 35dbc1a90c
(part of https://github.com/MusicPlayerDaemon/MPD/pull/1480).

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1563
2022-07-08 11:05:26 +02:00
Max Kellermann
2d7181105d output/MultipleOutputs: SetVolume() throws on error
This reveals more about the nature of an error instead of just
returning "problems setting volume".
2022-07-08 10:56:55 +02:00
Max Kellermann
9bdc75524b python/build/libs.py: update CURL to 7.84.0 2022-07-08 10:13:52 +02:00
Max Kellermann
2f6ceb4949 python/build/libs.py: update OpenSSL to 3.0.5 2022-07-08 10:10:42 +02:00
Max Kellermann
cd933aa35f subprojects: update fmt and vorbis 2022-07-08 10:08:27 +02:00
Max Kellermann
138738075b libfmt 9 support
libfmt version 9 broke the API by removing fmt::make_args_checked().

Fixes https://bugs.debian.org/1014543
2022-07-08 10:06:53 +02:00
Max Kellermann
2ee57f9b0d output/PipeWire: add nullptr check, fixing crash with PipeWire 0.3.53
Since PipeWire 0.3.53, control names can apparently be nulled, leading
to crashes in applications assertion that the string cannot be
nullptr.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1558
2022-07-04 19:20:08 +02:00
Max Kellermann
5a5655b790 lib/curl/Adapter: catch and postpone exceptions in WriteFunction()
This fixes a std::terminate() crash in the CURL storage plugin when
PropfindOperation::OnHeaders() throws an exception after receiving a
non-207 status.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1559
2022-07-01 12:43:42 +02:00
Max Kellermann
b88d1e6820 lib/curl/Headers: make the comparison type "transparent" 2022-07-01 12:17:41 +02:00
Max Kellermann
19d2864c34 lib/curl/Headers: central type definition for the header map 2022-07-01 12:17:36 +02:00
Max Kellermann
29e3a17f26 lib/curl/Request: move code from SetupEasy() to Setup.cxx 2022-07-01 12:17:26 +02:00
Max Kellermann
252e9f736f lib/curl/Request: move code to class CurlResponseHandlerAdapter 2022-07-01 12:17:20 +02:00
Max Kellermann
5d08988dda lib/curl/Handler: fix typo 2022-07-01 12:17:17 +02:00
Max Kellermann
47ca4246aa lib/curl/Request: add constructor with CurlEasy parameter 2022-07-01 12:17:13 +02:00
Max Kellermann
f8338d4f00 lib/curl/Request: use std::size_t 2022-07-01 12:16:59 +02:00
Max Kellermann
5cf6032c90 lib/curl/Request: move code to SetupEasy() 2022-07-01 12:16:55 +02:00
Max Kellermann
8d8b77412d lib/curl/Request: add API docs 2022-07-01 12:16:50 +02:00
Naglis Jonaitis
fd9114e7e2 doc/user.rst: fix neighbor plugin config block name 2022-06-08 12:57:27 +02:00
Max Kellermann
a3fba2f8f7 python/build/libs.py: update CURL to 7.83.1 2022-05-24 10:56:29 +02:00
Max Kellermann
e2b671f1b2 python/build/libs.py: add --disable-vulkan to FFmpeg configuration
Fixes Android build failure with NDK r25 beta4 because "vulkan_beta.h"
was not found.
2022-05-24 10:55:55 +02:00
Max Kellermann
2a35fbe29e python/build/libs.py: fix the OpenSSL SHA256 2022-05-24 10:55:55 +02:00
Max Kellermann
81cde72fd0 meson.build: suppress -Wstringop-overflow due to bogus libfmt warnings
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1536
2022-05-24 10:39:30 +02:00
Naglis Jonaitis
bf9ffba4f7 doc/user.rst: fix playlist plugin name option
`playlist_plugin` blocks use `name` to identify the plugins.
2022-05-24 10:22:45 +02:00
Dave Hocker
c975d8b943 Fix deprecation warnings caused by name changes in OSX audio inerfaces 2022-05-24 10:20:47 +02:00
Max Kellermann
2730f91872 .github/workflows/build.yml: build everything, not just unit tests (Linux) 2022-05-23 21:32:42 +02:00
Max Kellermann
97ca85e155 .github/workflows/build.yml: verbose build (Linux) 2022-05-23 21:32:02 +02:00
Max Kellermann
39bb4c5871 .github/workflows/build.yml: build everything, not just unit tests 2022-05-23 21:28:28 +02:00
Max Kellermann
bdceb90c59 .github/workflows/build.yml: verbose build 2022-05-23 21:25:28 +02:00
Max Kellermann
8bd1b5228c lib/upnp/Compat: suppress -Wunused-but-set-parameter 2022-05-19 20:10:41 +02:00
Max Kellermann
a009e95afd .github/ISSUE_TEMPLATE/bug_report.md: add "Configuration" section 2022-05-19 09:26:21 +02:00
Max Kellermann
32aafb3572 .github/ISSUE_TEMPLATE/question.md: remove, we have GitHub discussions now 2022-05-19 09:25:00 +02:00
Max Kellermann
b577783cf0 .github/FUNDING.yml: remove, no funding
This was an experiment, but I decided I don't need that.
2022-05-19 09:24:22 +02:00
Max Kellermann
aa7b872a14 .github/workflows/build.yml: run "apt-get update"
The build has been failing for a week or two because the package lists
in the image are outdated.
2022-05-19 09:23:08 +02:00
Caleb Xu
c6f7f57776 apple/Throw: add missing <cstring> header
strlen() and strcpy() are provided by the <string.h> and <cstring>
headers (as functions in global and std namespaces, respectively).

Compilers MAY provide an implementation for either of the functions
without including the extra header but the existence of a declaration
without the header is not assured.
2022-05-19 09:08:44 +02:00
Max Kellermann
106ad08cd2 increment version number to 0.23.8 2022-05-09 23:12:17 +02:00
59 changed files with 898 additions and 473 deletions

12
.github/FUNDING.yml vendored

@@ -1,12 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: MaxK
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

@@ -18,5 +18,9 @@ about: Create a bug report
<!-- Paste the output of "mpd --version" here -->
## Configuration
<!-- Paste your MPD configuration here -->
## Log
<!-- Paste relevant portions of the log file here (--verbose) -->

@@ -1,9 +0,0 @@
---
name: Question
about: Ask a question about MPD
---
<!-- Before you ask a question on GitHub, please read MPD's
documentation. A copy is available at
https://www.musicpd.org/doc/html/ -->
## Question

@@ -41,7 +41,8 @@ jobs:
key: linux
- name: Install dependencies
run: |
sudo apt install -y --no-install-recommends \
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
g++-10 libfmt-dev libboost-dev \
libgtest-dev \
libpcre2-dev \
@@ -73,19 +74,30 @@ jobs:
libgcrypt20-dev
- name: Full Build
uses: BSFishy/meson-build@v1.0.3
with:
action: build
directory: output/full
setup-options: -Ddocumentation=disabled -Dtest=true -Dsystemd=enabled -Dpcre=enabled
options: --verbose
meson-version: 0.56.0
- name: Unit Tests
uses: BSFishy/meson-build@v1.0.3
with:
action: test
directory: output/full
setup-options: -Ddocumentation=disabled -Dtest=true -Dsystemd=enabled -Dpcre=enabled
options: --verbose
meson-version: 0.56.0
- name: Mini Build
uses: BSFishy/meson-build@v1.0.3
with:
action: test
action: build
directory: output/mini
setup-options: -Dbuildtype=minsize -Dauto_features=disabled -Dtest=true -Ddaemon=false -Dinotify=false -Depoll=false -Deventfd=false -Dsignalfd=false -Dtcp=false -Ddsd=false -Ddatabase=false -Dneighbor=false -Dcue=false -Dfifo=false -Dhttpd=false -Dpipe=false -Drecorder=false -Dsnapcast=false
options: --verbose
meson-version: 0.56.0
build-macos:
@@ -124,10 +136,20 @@ jobs:
wavpack \
libmpdclient
- name: Meson Build
- name: Build
uses: BSFishy/meson-build@v1.0.3
with:
action: build
directory: output
setup-options: -Ddocumentation=disabled -Dtest=true
options: --verbose
meson-version: 0.56.0
- name: Unit Tests
uses: BSFishy/meson-build@v1.0.3
with:
action: test
directory: output
setup-options: -Ddocumentation=disabled -Dtest=true
options: --verbose
meson-version: 0.56.0

20
NEWS

@@ -1,3 +1,23 @@
ver 0.23.8 (2022/07/09)
* storage
- curl: fix crash if web server does not understand WebDAV
* input
- cdio_paranoia: fix crash if no drive was found
- cdio_paranoia: faster cancellation
- cdio_paranoia: don't scan for replay gain tags
- pipewire: fix playback of very short tracks
- pipewire: drop all buffers before manual song change
- pipewire: fix stuttering after manual song change
- snapcast: fix busy loop while paused
- snapcast: fix stuttering after resuming playback
* mixer
- better error messages
- alsa: fix setting volume before playback starts
- pipewire: fix crash bug
- pipewire: fix volume change events with PipeWire 0.3.53
- pipewire: don't force initial volume=100%
* support libfmt 9
ver 0.23.7 (2022/05/09)
* database
- upnp: support pupnp 1.14

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

@@ -301,7 +301,7 @@ Configuring neighbor plugins
----------------------------
All neighbor plugins are disabled by default to avoid unwanted
overhead. To enable (and configure) a plugin, add a :code:`neighbor`
overhead. To enable (and configure) a plugin, add a :code:`neighbors`
block to :file:`mpd.conf`:
.. code-block:: none
@@ -538,7 +538,7 @@ The following table lists the playlist_plugin options valid for all plugins:
* - Name
- Description
* - **plugin**
* - **name**
- The name of the plugin
* - **enabled yes|no**
- Allows you to disable a playlist plugin without recompiling. By default, all plugins are enabled.
@@ -1063,7 +1063,19 @@ The "music directory" is where you store your music files. :program:`MPD` stores
Depending on the size of your music collection and the speed of the storage, this can take a while.
To exclude a file from the update, create a file called :file:`.mpdignore` in its parent directory. Each line of that file may contain a list of shell wildcards. Matching files in the current directory and all subdirectories are excluded.
To exclude a file from the update, create a file called
:file:`.mpdignore` in its parent directory. Each line of that file
may contain a list of shell wildcards. Matching files (or
directories) in the current directory and all subdirectories are
excluded. Example::
*.opus
99*
Subject to pattern matching is the file/directory name. It is (not
yet) possible to match nested path names, e.g. something like
``foo/*.flac`` is not possible.
Mounting other storages into the music directory
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

@@ -1,7 +1,7 @@
project(
'mpd',
['c', 'cpp'],
version: '0.23.7',
version: '0.23.8',
meson_version: '>= 0.56.0',
default_options: [
'c_std=c11',
@@ -73,6 +73,9 @@ test_common_flags = [
# clang specific warning options:
'-Wunreachable-code-aggressive',
'-Wused-but-marked-unused',
# suppress bogus GCC12 warnings in libfmt headers
'-Wno-stringop-overflow',
]
test_global_cxxflags = test_global_common_flags + [

@@ -177,6 +177,8 @@ ffmpeg = FfmpegProject(
'--disable-filters',
'--disable-v4l2_m2m',
'--disable-vulkan',
'--disable-parser=bmp',
'--disable-parser=cavsvideo',
'--disable-parser=dvbsub',
@@ -380,14 +382,14 @@ ffmpeg = FfmpegProject(
)
openssl = OpenSSLProject(
'https://www.openssl.org/source/openssl-3.0.3.tar.gz',
'9384a2b0570dd80358841464677115df785edb941c71211f75076d72fe6b438f',
'https://www.openssl.org/source/openssl-3.0.5.tar.gz',
'aa7d8d9bef71ad6525c55ba11e5f4397889ce49c2c9349dcea6d3e4f0b024a7a',
'include/openssl/ossl_typ.h',
)
curl = CmakeProject(
'https://curl.se/download/curl-7.83.0.tar.xz',
'bbff0e6b5047e773f3c3b084d80546cc1be4e354c09e419c2d0ef6116253511a',
'https://curl.se/download/curl-7.84.0.tar.xz',
'2d118b43f547bfe5bae806d8d47b4e596ea5b25a6c1f080aef49fbcd817c5db8',
'lib/libcurl.a',
[
'-DBUILD_CURL_EXE=OFF',

@@ -45,7 +45,10 @@ void
LogFmt(LogLevel level, const Domain &domain,
const S &format_str, Args&&... args) noexcept
{
#if FMT_VERSION >= 70000
#if FMT_VERSION >= 90000
return LogVFmt(level, domain, format_str,
fmt::make_format_args(args...));
#elif FMT_VERSION >= 70000
return LogVFmt(level, domain, fmt::to_string_view(format_str),
fmt::make_args_checked<Args...>(format_str,
args...));

@@ -31,6 +31,7 @@
#include "ErrorRef.hxx"
#include "StringRef.hxx"
#include <cstring>
#include <stdexcept>
namespace Apple {
@@ -57,8 +58,8 @@ ThrowOSStatus(OSStatus status, const char *_msg)
const Apple::StringRef cfstr(cferr.CopyDescription());
char msg[1024];
strcpy(msg, _msg);
size_t length = strlen(msg);
std::strcpy(msg, _msg);
size_t length = std::strlen(msg);
cfstr.GetCString(msg + length, sizeof(msg) - length);
throw std::runtime_error(msg);

@@ -82,7 +82,10 @@ public:
template<typename S, typename... Args>
bool Fmt(const S &format_str, Args&&... args) noexcept {
#if FMT_VERSION >= 70000
#if FMT_VERSION >= 90000
return VFmt(format_str,
fmt::make_format_args(args...));
#elif FMT_VERSION >= 70000
return VFmt(fmt::to_string_view(format_str),
fmt::make_args_checked<Args...>(format_str,
args...));
@@ -109,7 +112,10 @@ public:
template<typename S, typename... Args>
void FmtError(enum ack code,
const S &format_str, Args&&... args) noexcept {
#if FMT_VERSION >= 70000
#if FMT_VERSION >= 90000
return VFmtError(code, format_str,
fmt::make_format_args(args...));
#elif FMT_VERSION >= 70000
return VFmtError(code, fmt::to_string_view(format_str),
fmt::make_args_checked<Args...>(format_str,
args...));

@@ -333,15 +333,11 @@ handle_getvol(Client &client, Request, Response &r)
}
CommandResult
handle_setvol(Client &client, Request args, Response &r)
handle_setvol(Client &client, Request args, Response &)
{
unsigned level = args.ParseUnsigned(0, 100);
if (!volume_level_change(client.GetPartition().outputs, level)) {
r.Error(ACK_ERROR_SYSTEM, "problems setting volume");
return CommandResult::ERROR;
}
volume_level_change(client.GetPartition().outputs, level);
return CommandResult::OK;
}
@@ -364,11 +360,8 @@ handle_volume(Client &client, Request args, Response &r)
else if (new_volume > 100)
new_volume = 100;
if (new_volume != old_volume &&
!volume_level_change(outputs, new_volume)) {
r.Error(ACK_ERROR_SYSTEM, "problems setting volume");
return CommandResult::ERROR;
}
if (new_volume != old_volume)
volume_level_change(outputs, new_volume);
return CommandResult::OK;
}

@@ -257,6 +257,12 @@ public:
return HasFailed();
}
[[gnu::pure]]
bool LockIsReplayGainEnabled() const noexcept {
const std::scoped_lock<Mutex> protect(mutex);
return replay_gain_mode != ReplayGainMode::OFF;
}
/**
* Transition this obejct from DecoderState::START to
* DecoderState::DECODE.

@@ -36,6 +36,7 @@
#include "util/RuntimeError.hxx"
#include "util/Domain.hxx"
#include "util/ScopeExit.hxx"
#include "util/StringCompare.hxx"
#include "thread/Name.hxx"
#include "tag/ApeReplayGain.hxx"
#include "Log.hxx"
@@ -261,12 +262,16 @@ LoadReplayGain(DecoderClient &client, InputStream &is)
static void
MaybeLoadReplayGain(DecoderBridge &bridge, InputStream &is)
{
{
const std::scoped_lock<Mutex> protect(bridge.dc.mutex);
if (bridge.dc.replay_gain_mode == ReplayGainMode::OFF)
/* ReplayGain is disabled */
return;
}
if (!bridge.dc.LockIsReplayGainEnabled())
/* ReplayGain is disabled */
return;
if (is.HasMimeType() &&
StringStartsWith(is.GetMimeType(), "audio/x-mpd-"))
/* skip for (virtual) files (e.g. from the
cdio_paranoia input plugin) which cannot possibly
contain tags */
return;
LoadReplayGain(bridge, is);
}

@@ -30,10 +30,12 @@
#include "util/RuntimeError.hxx"
#include "util/Domain.hxx"
#include "util/ByteOrder.hxx"
#include "util/ScopeExit.hxx"
#include "fs/AllocatedPath.hxx"
#include "Log.hxx"
#include "config/Block.hxx"
#include <algorithm>
#include <cassert>
#include <cstdint>
@@ -48,21 +50,19 @@ class CdioParanoiaInputStream final : public InputStream {
CdIo_t *const cdio;
CdromParanoia para;
const lsn_t lsn_from, lsn_to;
int lsn_relofs;
const lsn_t lsn_from;
char buffer[CDIO_CD_FRAMESIZE_RAW];
int buffer_lsn;
lsn_t buffer_lsn;
public:
CdioParanoiaInputStream(const char *_uri, Mutex &_mutex,
cdrom_drive_t *_drv, CdIo_t *_cdio,
bool reverse_endian,
lsn_t _lsn_from, lsn_t _lsn_to)
lsn_t _lsn_from, lsn_t lsn_to)
:InputStream(_uri, _mutex),
drv(_drv), cdio(_cdio), para(drv),
lsn_from(_lsn_from), lsn_to(_lsn_to),
lsn_relofs(0),
lsn_from(_lsn_from),
buffer_lsn(-1)
{
/* Set reading mode for full paranoia, but allow
@@ -173,9 +173,12 @@ cdio_detect_device()
if (devices == nullptr)
return nullptr;
AllocatedPath path = AllocatedPath::FromFS(devices[0]);
cdio_free_device_list(devices);
return path;
AtScopeExit(devices) { cdio_free_device_list(devices); };
if (devices[0] == nullptr)
return nullptr;
return AllocatedPath::FromFS(devices[0]);
}
static InputStreamPtr
@@ -271,81 +274,70 @@ CdioParanoiaInputStream::Seek(std::unique_lock<Mutex> &,
return;
/* calculate current LSN */
lsn_relofs = new_offset / CDIO_CD_FRAMESIZE_RAW;
offset = new_offset;
const lsn_t lsn_relofs = new_offset / CDIO_CD_FRAMESIZE_RAW;
{
if (lsn_relofs != buffer_lsn) {
const ScopeUnlock unlock(mutex);
para.Seek(lsn_from + lsn_relofs);
}
offset = new_offset;
}
size_t
CdioParanoiaInputStream::Read(std::unique_lock<Mutex> &,
void *ptr, size_t length)
{
size_t nbytes = 0;
char *wptr = (char *) ptr;
/* end of track ? */
if (IsEOF())
return 0;
while (length > 0) {
/* end of track ? */
if (lsn_from + lsn_relofs > lsn_to)
break;
//current sector was changed ?
const int16_t *rbuf;
//current sector was changed ?
const int16_t *rbuf;
if (lsn_relofs != buffer_lsn) {
const ScopeUnlock unlock(mutex);
const lsn_t lsn_relofs = offset / CDIO_CD_FRAMESIZE_RAW;
const std::size_t diff = offset % CDIO_CD_FRAMESIZE_RAW;
try {
rbuf = para.Read().data;
} catch (...) {
char *s_err = cdio_cddap_errors(drv);
if (s_err) {
FmtError(cdio_domain,
"paranoia_read: {}", s_err);
cdio_cddap_free_messages(s_err);
}
if (lsn_relofs != buffer_lsn) {
const ScopeUnlock unlock(mutex);
throw;
try {
rbuf = para.Read().data;
} catch (...) {
char *s_err = cdio_cddap_errors(drv);
if (s_err) {
FmtError(cdio_domain,
"paranoia_read: {}", s_err);
cdio_cddap_free_messages(s_err);
}
//store current buffer
memcpy(buffer, rbuf, CDIO_CD_FRAMESIZE_RAW);
buffer_lsn = lsn_relofs;
} else {
//use cached sector
rbuf = (const int16_t *)buffer;
throw;
}
//correct offset
const int diff = offset - lsn_relofs * CDIO_CD_FRAMESIZE_RAW;
assert(diff >= 0 && diff < CDIO_CD_FRAMESIZE_RAW);
const size_t maxwrite = CDIO_CD_FRAMESIZE_RAW - diff; //# of bytes pending in current buffer
const size_t len = (length < maxwrite? length : maxwrite);
//skip diff bytes from this lsn
memcpy(wptr, ((const char *)rbuf) + diff, len);
//update pointer
wptr += len;
nbytes += len;
//update offset
offset += len;
lsn_relofs = offset / CDIO_CD_FRAMESIZE_RAW;
//update length
length -= len;
//store current buffer
memcpy(buffer, rbuf, CDIO_CD_FRAMESIZE_RAW);
buffer_lsn = lsn_relofs;
} else {
//use cached sector
rbuf = (const int16_t *)buffer;
}
const size_t maxwrite = CDIO_CD_FRAMESIZE_RAW - diff; //# of bytes pending in current buffer
const std::size_t nbytes = std::min(length, maxwrite);
//skip diff bytes from this lsn
memcpy(ptr, ((const char *)rbuf) + diff, nbytes);
//update offset
offset += nbytes;
return nbytes;
}
bool
CdioParanoiaInputStream::IsEOF() const noexcept
{
return lsn_from + lsn_relofs > lsn_to;
return offset >= size;
}
static constexpr const char *cdio_paranoia_prefixes[] = {

@@ -82,7 +82,7 @@ class CurlInputStream final : public AsyncInputStream, CurlResponseHandler {
public:
template<typename I>
CurlInputStream(EventLoop &event_loop, const char *_url,
const std::multimap<std::string, std::string> &headers,
const Curl::Headers &headers,
I &&_icy,
Mutex &_mutex);
@@ -92,7 +92,7 @@ public:
CurlInputStream &operator=(const CurlInputStream &) = delete;
static InputStreamPtr Open(const char *url,
const std::multimap<std::string, std::string> &headers,
const Curl::Headers &headers,
Mutex &mutex);
private:
@@ -131,8 +131,7 @@ private:
void SeekInternal(offset_type new_offset);
/* virtual methods from CurlResponseHandler */
void OnHeaders(unsigned status,
std::multimap<std::string, std::string> &&headers) override;
void OnHeaders(unsigned status, Curl::Headers &&headers) override;
void OnData(ConstBuffer<void> data) override;
void OnEnd() override;
void OnError(std::exception_ptr e) noexcept override;
@@ -227,7 +226,7 @@ WithConvertedTagValue(const char *uri, const char *value, F &&f) noexcept
void
CurlInputStream::OnHeaders(unsigned status,
std::multimap<std::string, std::string> &&headers)
Curl::Headers &&headers)
{
assert(GetEventLoop().IsInside());
assert(!postponed_exception);
@@ -391,7 +390,7 @@ input_curl_finish() noexcept
template<typename I>
inline
CurlInputStream::CurlInputStream(EventLoop &event_loop, const char *_url,
const std::multimap<std::string, std::string> &headers,
const Curl::Headers &headers,
I &&_icy,
Mutex &_mutex)
:AsyncInputStream(event_loop, _url, _mutex,
@@ -491,7 +490,7 @@ CurlInputStream::DoSeek(offset_type new_offset)
inline InputStreamPtr
CurlInputStream::Open(const char *url,
const std::multimap<std::string, std::string> &headers,
const Curl::Headers &headers,
Mutex &mutex)
{
auto icy = std::make_shared<IcyMetaDataParser>();
@@ -510,8 +509,7 @@ CurlInputStream::Open(const char *url,
}
InputStreamPtr
OpenCurlInputStream(const char *uri,
const std::multimap<std::string, std::string> &headers,
OpenCurlInputStream(const char *uri, const Curl::Headers &headers,
Mutex &mutex)
{
return CurlInputStream::Open(uri, headers, mutex);

@@ -20,12 +20,10 @@
#ifndef MPD_INPUT_CURL_HXX
#define MPD_INPUT_CURL_HXX
#include "lib/curl/Headers.hxx"
#include "input/Ptr.hxx"
#include "thread/Mutex.hxx"
#include <string>
#include <map>
extern const struct InputPlugin input_plugin_curl;
/**
@@ -36,8 +34,7 @@ extern const struct InputPlugin input_plugin_curl;
* Throws on error.
*/
InputStreamPtr
OpenCurlInputStream(const char *uri,
const std::multimap<std::string, std::string> &headers,
OpenCurlInputStream(const char *uri, const Curl::Headers &headers,
Mutex &mutex);
#endif

@@ -164,7 +164,7 @@ QobuzClient::InvokeHandlers() noexcept
std::string
QobuzClient::MakeUrl(const char *object, const char *method,
const std::multimap<std::string, std::string> &query) const noexcept
const Curl::Headers &query) const noexcept
{
assert(!query.empty());
@@ -183,7 +183,7 @@ QobuzClient::MakeUrl(const char *object, const char *method,
std::string
QobuzClient::MakeSignedUrl(const char *object, const char *method,
const std::multimap<std::string, std::string> &query) const noexcept
const Curl::Headers &query) const noexcept
{
assert(!query.empty());

@@ -23,12 +23,12 @@
#include "QobuzSession.hxx"
#include "QobuzLoginRequest.hxx"
#include "lib/curl/Init.hxx"
#include "lib/curl/Headers.hxx"
#include "thread/Mutex.hxx"
#include "event/DeferEvent.hxx"
#include "util/IntrusiveList.hxx"
#include <memory>
#include <map>
#include <string>
class QobuzSessionHandler
@@ -94,10 +94,10 @@ public:
QobuzSession GetSession() const;
std::string MakeUrl(const char *object, const char *method,
const std::multimap<std::string, std::string> &query) const noexcept;
const Curl::Headers &query) const noexcept;
std::string MakeSignedUrl(const char *object, const char *method,
const std::multimap<std::string, std::string> &query) const noexcept;
const Curl::Headers &query) const noexcept;
private:
void StartLogin();

@@ -38,7 +38,7 @@ static constexpr yajl_callbacks qobuz_error_parser_callbacks = {
};
QobuzErrorParser::QobuzErrorParser(unsigned _status,
const std::multimap<std::string, std::string> &headers)
const Curl::Headers &headers)
:YajlResponseParser(&qobuz_error_parser_callbacks, nullptr, this),
status(_status)
{

@@ -20,11 +20,9 @@
#ifndef QOBUZ_ERROR_PARSER_HXX
#define QOBUZ_ERROR_PARSER_HXX
#include "lib/curl/Headers.hxx"
#include "lib/yajl/ResponseParser.hxx"
#include <string>
#include <map>
template<typename T> struct ConstBuffer;
struct StringView;
@@ -46,8 +44,7 @@ public:
* May throw if there is a formal error in the response
* headers.
*/
QobuzErrorParser(unsigned status,
const std::multimap<std::string, std::string> &headers);
QobuzErrorParser(unsigned status, const Curl::Headers &headers);
protected:
/* virtual methods from CurlResponseParser */

@@ -77,7 +77,7 @@ QobuzLoginRequest::ResponseParser::GetSession()
return std::move(session);
}
static std::multimap<std::string, std::string>
static Curl::Headers
MakeLoginForm(const char *app_id,
const char *username, const char *email,
const char *password,
@@ -85,7 +85,7 @@ MakeLoginForm(const char *app_id,
{
assert(username != nullptr || email != nullptr);
std::multimap<std::string, std::string> form{
Curl::Headers form{
{"app_id", app_id},
{"password", password},
{"device_manufacturer_id", device_manufacturer_id},
@@ -134,8 +134,7 @@ QobuzLoginRequest::~QobuzLoginRequest() noexcept
}
std::unique_ptr<CurlResponseParser>
QobuzLoginRequest::MakeParser(unsigned status,
std::multimap<std::string, std::string> &&headers)
QobuzLoginRequest::MakeParser(unsigned status, Curl::Headers &&headers)
{
if (status != 200)
return std::make_unique<QobuzErrorParser>(status, headers);

@@ -56,7 +56,7 @@ public:
private:
/* virtual methods from DelegateCurlResponseHandler */
std::unique_ptr<CurlResponseParser> MakeParser(unsigned status,
std::multimap<std::string, std::string> &&headers) override;
Curl::Headers &&headers) override;
void FinishParser(std::unique_ptr<CurlResponseParser> p) override;
/* virtual methods from CurlResponseHandler */

@@ -99,8 +99,7 @@ QobuzTagScanner::~QobuzTagScanner() noexcept
}
std::unique_ptr<CurlResponseParser>
QobuzTagScanner::MakeParser(unsigned status,
std::multimap<std::string, std::string> &&headers)
QobuzTagScanner::MakeParser(unsigned status, Curl::Headers &&headers)
{
if (status != 200)
return std::make_unique<QobuzErrorParser>(status, headers);

@@ -49,7 +49,7 @@ public:
private:
/* virtual methods from DelegateCurlResponseHandler */
std::unique_ptr<CurlResponseParser> MakeParser(unsigned status,
std::multimap<std::string, std::string> &&headers) override;
Curl::Headers &&headers) override;
void FinishParser(std::unique_ptr<CurlResponseParser> p) override;
/* virtual methods from CurlResponseHandler */

@@ -93,7 +93,7 @@ QobuzTrackRequest::~QobuzTrackRequest() noexcept
std::unique_ptr<CurlResponseParser>
QobuzTrackRequest::MakeParser(unsigned status,
std::multimap<std::string, std::string> &&headers)
Curl::Headers &&headers)
{
if (status != 200)
return std::make_unique<QobuzErrorParser>(status, headers);

@@ -56,7 +56,7 @@ public:
private:
/* virtual methods from DelegateCurlResponseHandler */
std::unique_ptr<CurlResponseParser> MakeParser(unsigned status,
std::multimap<std::string, std::string> &&headers) override;
Curl::Headers &&headers) override;
void FinishParser(std::unique_ptr<CurlResponseParser> p) override;
/* virtual methods from CurlResponseHandler */

204
src/lib/curl/Adapter.cxx Normal file

@@ -0,0 +1,204 @@
/*
* Copyright 2008-2021 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "Adapter.hxx"
#include "Easy.hxx"
#include "Handler.hxx"
#include "util/CharUtil.hxx"
#include "util/RuntimeError.hxx"
#include "util/StringStrip.hxx"
#include "util/StringView.hxx"
#include <algorithm>
#include <cassert>
void
CurlResponseHandlerAdapter::Install(CurlEasy &easy)
{
assert(state == State::UNINITIALISED);
error_buffer[0] = 0;
easy.SetErrorBuffer(error_buffer);
easy.SetHeaderFunction(_HeaderFunction, this);
easy.SetWriteFunction(WriteFunction, this);
curl = easy.Get();
state = State::HEADERS;
}
void
CurlResponseHandlerAdapter::FinishHeaders()
{
assert(state >= State::HEADERS);
if (state != State::HEADERS)
return;
state = State::BODY;
long status = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
handler.OnHeaders(status, std::move(headers));
}
void
CurlResponseHandlerAdapter::FinishBody()
{
FinishHeaders();
if (state != State::BODY)
return;
state = State::CLOSED;
handler.OnEnd();
}
void
CurlResponseHandlerAdapter::Done(CURLcode result) noexcept
{
if (postponed_error) {
state = State::CLOSED;
handler.OnError(std::move(postponed_error));
return;
}
try {
if (result != CURLE_OK) {
StripRight(error_buffer);
const char *msg = error_buffer;
if (*msg == 0)
msg = curl_easy_strerror(result);
throw FormatRuntimeError("CURL failed: %s", msg);
}
FinishBody();
} catch (...) {
state = State::CLOSED;
handler.OnError(std::current_exception());
}
}
[[gnu::pure]]
static bool
IsResponseBoundaryHeader(StringView s) noexcept
{
return s.size > 5 && (s.StartsWith("HTTP/") ||
/* the proprietary "ICY 200 OK" is
emitted by Shoutcast */
s.StartsWith("ICY 2"));
}
inline void
CurlResponseHandlerAdapter::HeaderFunction(StringView s) noexcept
{
if (state > State::HEADERS)
return;
if (IsResponseBoundaryHeader(s)) {
/* this is the boundary to a new response, for example
after a redirect */
headers.clear();
return;
}
const char *header = s.data;
const char *end = StripRight(header, header + s.size);
const char *value = s.Find(':');
if (value == nullptr)
return;
std::string name(header, value);
std::transform(name.begin(), name.end(), name.begin(),
static_cast<char(*)(char)>(ToLowerASCII));
/* skip the colon */
++value;
/* strip the value */
value = StripLeft(value, end);
end = StripRight(value, end);
headers.emplace(std::move(name), std::string(value, end));
}
std::size_t
CurlResponseHandlerAdapter::_HeaderFunction(char *ptr, std::size_t size,
std::size_t nmemb,
void *stream) noexcept
{
CurlResponseHandlerAdapter &c = *(CurlResponseHandlerAdapter *)stream;
size *= nmemb;
c.HeaderFunction({ptr, size});
return size;
}
inline std::size_t
CurlResponseHandlerAdapter::DataReceived(const void *ptr,
std::size_t received_size) noexcept
{
assert(received_size > 0);
try {
FinishHeaders();
handler.OnData({ptr, received_size});
return received_size;
} catch (CurlResponseHandler::Pause) {
return CURL_WRITEFUNC_PAUSE;
} catch (...) {
/* from inside this libCURL callback function, we
can't do much, so we remember the exception to be
handled later by Done(), and return 0, causing the
response to be aborted with CURLE_WRITE_ERROR */
postponed_error = std::current_exception();
return 0;
}
}
std::size_t
CurlResponseHandlerAdapter::WriteFunction(char *ptr, std::size_t size,
std::size_t nmemb,
void *stream) noexcept
{
CurlResponseHandlerAdapter &c = *(CurlResponseHandlerAdapter *)stream;
size *= nmemb;
if (size == 0)
return 0;
return c.DataReceived(ptr, size);
}

91
src/lib/curl/Adapter.hxx Normal file

@@ -0,0 +1,91 @@
/*
* Copyright 2008-2022 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Headers.hxx"
#include <curl/curl.h>
#include <cstddef>
#include <exception>
struct StringView;
class CurlEasy;
class CurlResponseHandler;
class CurlResponseHandlerAdapter {
CURL *curl;
CurlResponseHandler &handler;
Curl::Headers headers;
/**
* An exception caught from within the WriteFunction() which
* will later be handled by Done().
*/
std::exception_ptr postponed_error;
/** error message provided by libcurl */
char error_buffer[CURL_ERROR_SIZE];
enum class State {
UNINITIALISED,
HEADERS,
BODY,
CLOSED,
} state = State::UNINITIALISED;
public:
explicit CurlResponseHandlerAdapter(CurlResponseHandler &_handler) noexcept
:handler(_handler) {}
void Install(CurlEasy &easy);
void Done(CURLcode result) noexcept;
private:
void FinishHeaders();
void FinishBody();
void HeaderFunction(StringView s) noexcept;
/** called by curl when a new header is available */
static std::size_t _HeaderFunction(char *ptr,
std::size_t size, std::size_t nmemb,
void *stream) noexcept;
std::size_t DataReceived(const void *ptr, std::size_t size) noexcept;
/** called by curl when new data is available */
static std::size_t WriteFunction(char *ptr,
std::size_t size, std::size_t nmemb,
void *stream) noexcept;
};

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2008-2018 Max Kellermann <max.kellermann@gmail.com>
* Copyright 2008-2022 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -34,8 +34,7 @@
#include <utility>
void
DelegateCurlResponseHandler::OnHeaders(unsigned status,
std::multimap<std::string, std::string> &&headers)
DelegateCurlResponseHandler::OnHeaders(unsigned status, Curl::Headers &&headers)
{
parser = MakeParser(status, std::move(headers));
assert(parser);

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2008-2018 Max Kellermann <max.kellermann@gmail.com>
* Copyright 2008-2022 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -27,8 +27,7 @@
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef CURL_DELEGATE_HXX
#define CURL_DELEGATE_HXX
#pragma once
#include "Handler.hxx"
@@ -53,7 +52,7 @@ protected:
* CurlResponseParser::OnError()).
*/
virtual std::unique_ptr<CurlResponseParser> MakeParser(unsigned status,
std::multimap<std::string, std::string> &&headers) = 0;
Curl::Headers &&headers) = 0;
/**
* The parser has finished parsing the response body. This
@@ -64,10 +63,7 @@ protected:
virtual void FinishParser(std::unique_ptr<CurlResponseParser> p) = 0;
public:
void OnHeaders(unsigned status,
std::multimap<std::string, std::string> &&headers) final;
void OnHeaders(unsigned status, Curl::Headers &&headers) final;
void OnData(ConstBuffer<void> data) final;
void OnEnd() final;
};
#endif

@@ -31,8 +31,7 @@
#include "String.hxx"
std::string
EncodeForm(CURL *curl,
const std::multimap<std::string, std::string> &fields) noexcept
EncodeForm(CURL *curl, const Curl::Headers &fields) noexcept
{
std::string result;

@@ -30,17 +30,17 @@
#ifndef CURL_FORM_HXX
#define CURL_FORM_HXX
#include "Headers.hxx"
#include <curl/curl.h>
#include <string>
#include <map>
/**
* Encode the given map of form fields to a
* "application/x-www-form-urlencoded" string.
*/
std::string
EncodeForm(CURL *curl,
const std::multimap<std::string, std::string> &fields) noexcept;
EncodeForm(CURL *curl, const Curl::Headers &fields) noexcept;
#endif

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2018 Max Kellermann <max.kellermann@gmail.com>
* Copyright 2008-2021 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -27,14 +27,12 @@
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef CURL_HANDLER_HXX
#define CURL_HANDLER_HXX
#pragma once
#include "Headers.hxx"
#include "util/ConstBuffer.hxx"
#include <exception>
#include <string>
#include <map>
/**
* Asynchronous response handler for a #CurlRequest.
@@ -52,28 +50,31 @@ public:
/**
* Status line and headers have been received.
*
* Exceptions thrown by this method will be passed to
* OnError(), aborting the request.
*/
virtual void OnHeaders(unsigned status,
std::multimap<std::string, std::string> &&headers) = 0;
virtual void OnHeaders(unsigned status, Curl::Headers &&headers) = 0;
/**
* Response body data has been received.
*
* May throw #Pause (but nothing else).
* May throw #Pause.
*
* Other exceptions thrown by this method will be passed to
* OnError(), aborting the request.
*/
virtual void OnData(ConstBuffer<void> data) = 0;
/**
* The response has ended. The method is allowed delete the
* #CurlRequest here.
* The response has ended. The method is allowed to delete the
* #CurlRequest.
*/
virtual void OnEnd() = 0;
/**
* An error has occurred. The method is allowed delete the
* #CurlRequest here.
* An error has occurred. The method is allowed to delete the
* #CurlRequest.
*/
virtual void OnError(std::exception_ptr e) noexcept = 0;
};
#endif

42
src/lib/curl/Headers.hxx Normal file

@@ -0,0 +1,42 @@
/*
* Copyright 2020-2021 CM4all GmbH
* All rights reserved.
*
* author: Max Kellermann <mk@cm4all.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <map>
#include <string>
namespace Curl {
using Headers = std::multimap<std::string, std::string, std::less<>>;
} // namespace Curl

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2018 Max Kellermann <max.kellermann@gmail.com>
* Copyright 2008-2021 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -27,42 +27,27 @@
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "Request.hxx"
#include "Setup.hxx"
#include "Global.hxx"
#include "Handler.hxx"
#include "event/Call.hxx"
#include "util/RuntimeError.hxx"
#include "util/StringStrip.hxx"
#include "util/StringView.hxx"
#include "util/CharUtil.hxx"
#include "Version.h"
#include <curl/curl.h>
#include <algorithm>
#include <cassert>
#include <string.h>
CurlRequest::CurlRequest(CurlGlobal &_global, CurlEasy _easy,
CurlResponseHandler &_handler)
:global(_global), handler(_handler), easy(std::move(_easy))
{
SetupEasy();
}
CurlRequest::CurlRequest(CurlGlobal &_global,
CurlResponseHandler &_handler)
:global(_global), handler(_handler)
{
error_buffer[0] = 0;
easy.SetPrivate((void *)this);
easy.SetUserAgent("Music Player Daemon " VERSION);
easy.SetHeaderFunction(_HeaderFunction, this);
easy.SetWriteFunction(WriteFunction, this);
#if !defined(ANDROID) && !defined(_WIN32)
easy.SetOption(CURLOPT_NETRC, 1L);
#endif
easy.SetErrorBuffer(error_buffer);
easy.SetNoProgress();
easy.SetNoSignal();
easy.SetConnectTimeout(10);
easy.SetOption(CURLOPT_HTTPAUTH, (long) CURLAUTH_ANY);
SetupEasy();
}
CurlRequest::~CurlRequest() noexcept
@@ -70,6 +55,16 @@ CurlRequest::~CurlRequest() noexcept
FreeEasy();
}
void
CurlRequest::SetupEasy()
{
easy.SetPrivate((void *)this);
handler.Install(easy);
Curl::Setup(easy);
}
void
CurlRequest::Start()
{
@@ -125,135 +120,10 @@ CurlRequest::Resume() noexcept
global.InvalidateSockets();
}
void
CurlRequest::FinishHeaders()
{
if (state != State::HEADERS)
return;
state = State::BODY;
long status = 0;
easy.GetInfo(CURLINFO_RESPONSE_CODE, &status);
handler.OnHeaders(status, std::move(headers));
}
void
CurlRequest::FinishBody()
{
FinishHeaders();
if (state != State::BODY)
return;
state = State::CLOSED;
handler.OnEnd();
}
void
CurlRequest::Done(CURLcode result) noexcept
{
Stop();
try {
if (result != CURLE_OK) {
StripRight(error_buffer);
const char *msg = error_buffer;
if (*msg == 0)
msg = curl_easy_strerror(result);
throw FormatRuntimeError("CURL failed: %s", msg);
}
FinishBody();
} catch (...) {
state = State::CLOSED;
handler.OnError(std::current_exception());
}
}
[[gnu::pure]]
static bool
IsResponseBoundaryHeader(StringView s) noexcept
{
return s.size > 5 && (s.StartsWith("HTTP/") ||
/* the proprietary "ICY 200 OK" is
emitted by Shoutcast */
s.StartsWith("ICY 2"));
}
inline void
CurlRequest::HeaderFunction(StringView s) noexcept
{
if (state > State::HEADERS)
return;
if (IsResponseBoundaryHeader(s)) {
/* this is the boundary to a new response, for example
after a redirect */
headers.clear();
return;
}
const char *header = s.data;
const char *end = StripRight(header, header + s.size);
const char *value = s.Find(':');
if (value == nullptr)
return;
std::string name(header, value);
std::transform(name.begin(), name.end(), name.begin(),
static_cast<char(*)(char)>(ToLowerASCII));
/* skip the colon */
++value;
/* strip the value */
value = StripLeft(value, end);
end = StripRight(value, end);
headers.emplace(std::move(name), std::string(value, end));
}
size_t
CurlRequest::_HeaderFunction(char *ptr, size_t size, size_t nmemb,
void *stream) noexcept
{
CurlRequest &c = *(CurlRequest *)stream;
size *= nmemb;
c.HeaderFunction({ptr, size});
return size;
}
inline size_t
CurlRequest::DataReceived(const void *ptr, size_t received_size) noexcept
{
assert(received_size > 0);
try {
FinishHeaders();
handler.OnData({ptr, received_size});
return received_size;
} catch (CurlResponseHandler::Pause) {
return CURL_WRITEFUNC_PAUSE;
}
}
size_t
CurlRequest::WriteFunction(char *ptr, size_t size, size_t nmemb,
void *stream) noexcept
{
CurlRequest &c = *(CurlRequest *)stream;
size *= nmemb;
if (size == 0)
return 0;
return c.DataReceived(ptr, size);
handler.Done(result);
}

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2018 Max Kellermann <max.kellermann@gmail.com>
* Copyright 2008-2021 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -31,39 +31,34 @@
#define CURL_REQUEST_HXX
#include "Easy.hxx"
#include "Adapter.hxx"
#include <map>
#include <string>
#include <cstddef>
struct StringView;
class CurlGlobal;
class CurlResponseHandler;
/**
* A non-blocking HTTP request integrated via #CurlGlobal into the
* #EventLoop.
*
* To start sending the request, call Start().
*/
class CurlRequest final {
CurlGlobal &global;
CurlResponseHandler &handler;
CurlResponseHandlerAdapter handler;
/** the curl handle */
CurlEasy easy;
enum class State {
HEADERS,
BODY,
CLOSED,
} state = State::HEADERS;
std::multimap<std::string, std::string> headers;
/** error message provided by libcurl */
char error_buffer[CURL_ERROR_SIZE];
bool registered = false;
public:
/**
* To start sending the request, call Start().
*/
CurlRequest(CurlGlobal &_global, CurlEasy easy,
CurlResponseHandler &_handler);
CurlRequest(CurlGlobal &_global,
CurlResponseHandler &_handler);
@@ -136,7 +131,7 @@ public:
easy.SetPost(value);
}
void SetRequestBody(const void *data, size_t size) {
void SetRequestBody(const void *data, std::size_t size) {
easy.SetRequestBody(data, size);
}
@@ -148,6 +143,8 @@ public:
void Done(CURLcode result) noexcept;
private:
void SetupEasy();
/**
* Frees the current "libcurl easy" handle, and everything
* associated with it.
@@ -156,18 +153,6 @@ private:
void FinishHeaders();
void FinishBody();
size_t DataReceived(const void *ptr, size_t size) noexcept;
void HeaderFunction(StringView s) noexcept;
/** called by curl when new data is available */
static size_t _HeaderFunction(char *ptr, size_t size, size_t nmemb,
void *stream) noexcept;
/** called by curl when new data is available */
static size_t WriteFunction(char *ptr, size_t size, size_t nmemb,
void *stream) noexcept;
};
#endif

51
src/lib/curl/Setup.cxx Normal file

@@ -0,0 +1,51 @@
/*
* Copyright 2008-2021 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "Setup.hxx"
#include "Easy.hxx"
#include "Version.h"
#include <stdio.h>
namespace Curl {
void
Setup(CurlEasy &easy)
{
easy.SetUserAgent("Music Player Daemon " VERSION);
#if !defined(ANDROID) && !defined(_WIN32)
easy.SetOption(CURLOPT_NETRC, 1L);
#endif
easy.SetNoProgress();
easy.SetNoSignal();
easy.SetConnectTimeout(10);
easy.SetOption(CURLOPT_HTTPAUTH, (long) CURLAUTH_ANY);
}
} // namespace Curl

39
src/lib/curl/Setup.hxx Normal file

@@ -0,0 +1,39 @@
/*
* Copyright 2008-2021 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
class CurlEasy;
namespace Curl {
void
Setup(CurlEasy &easy);
} // namespace Curl

@@ -18,6 +18,8 @@ curl = static_library(
'Init.cxx',
'Global.cxx',
'Request.cxx',
'Setup.cxx',
'Adapter.cxx',
'Escape.cxx',
'Form.cxx',
include_directories: inc,

@@ -1,6 +1,8 @@
--- curl-7.75.0.orig/CMakeLists.txt 2021-02-02 09:26:24.000000000 +0100
+++ curl-7.75.0/CMakeLists.txt 2021-03-25 20:17:25.445684029 +0100
@@ -1453,7 +1453,7 @@
Index: curl-7.84.0/CMakeLists.txt
===================================================================
--- curl-7.84.0.orig/CMakeLists.txt
+++ curl-7.84.0/CMakeLists.txt
@@ -1536,7 +1536,7 @@ set(includedir "\${prefix}/
set(LDFLAGS "${CMAKE_SHARED_LINKER_FLAGS}")
set(LIBCURL_LIBS "")
set(libdir "${CMAKE_INSTALL_PREFIX}/lib")
@@ -8,4 +10,4 @@
+foreach(_lib ${CURL_LIBS})
if(TARGET "${_lib}")
set(_libname "${_lib}")
get_target_property(_libtype "${_libname}" TYPE)
get_target_property(_imported "${_libname}" IMPORTED)

@@ -1,20 +1,20 @@
Index: curl-7.71.1/lib/url.c
Index: curl-7.84.0/lib/url.c
===================================================================
--- curl-7.71.1.orig/lib/url.c
+++ curl-7.71.1/lib/url.c
@@ -2871,6 +2871,7 @@
}
--- curl-7.84.0.orig/lib/url.c
+++ curl-7.84.0/lib/url.c
@@ -3003,6 +3003,7 @@ static CURLcode override_login(struct Cu
#ifndef CURL_DISABLE_NETRC
conn->bits.netrc = FALSE;
+#ifndef __BIONIC__
if(data->set.use_netrc && !data->set.str[STRING_USERNAME]) {
bool netrc_user_changed = FALSE;
bool netrc_passwd_changed = FALSE;
@@ -2895,6 +2896,7 @@
conn->bits.user_passwd = TRUE; /* enable user+password */
@@ -3079,6 +3080,7 @@ static CURLcode override_login(struct Cu
return CURLE_OUT_OF_MEMORY;
}
}
+#endif
/* for updated strings, we update them in the URL */
if(*userp) {
return CURLE_OK;
}

@@ -24,6 +24,10 @@
/* libupnp versions until 1.10.1 redefine "bool" and "true" */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wkeyword-macro"
/* libupnp 1.8.4 uses a flawed kludge to suppress this warning in
inline function __list_add_valid() */
#pragma GCC diagnostic ignored "-Wunused-but-set-parameter"
#endif
#include <upnp.h>

@@ -55,7 +55,7 @@ UPnPDeviceDirectory::Downloader::Destroy() noexcept
void
UPnPDeviceDirectory::Downloader::OnHeaders(unsigned status,
std::multimap<std::string, std::string> &&)
Curl::Headers &&)
{
if (status != 200) {
Destroy();

@@ -113,8 +113,7 @@ class UPnPDeviceDirectory final : UpnpCallback {
}
/* virtual methods from CurlResponseHandler */
void OnHeaders(unsigned status,
std::multimap<std::string, std::string> &&headers) override;
void OnHeaders(unsigned status, Curl::Headers &&headers) override;
void OnData(ConstBuffer<void> data) override;
void OnEnd() override;
void OnError(std::exception_ptr e) noexcept override;

@@ -73,42 +73,77 @@ MultipleOutputs::GetVolume() const noexcept
return total / ok;
}
static bool
output_mixer_set_volume(AudioOutputControl &ao, unsigned volume) noexcept
enum class SetVolumeResult {
NO_MIXER,
DISABLED,
ERROR,
OK,
};
static SetVolumeResult
output_mixer_set_volume(AudioOutputControl &ao, unsigned volume)
{
assert(volume <= 100);
auto *mixer = ao.GetMixer();
if (mixer == nullptr)
return false;
return SetVolumeResult::NO_MIXER;
/* software mixers are always updated, even if they are
disabled */
if (!ao.IsReallyEnabled() && !mixer->IsPlugin(software_mixer_plugin))
return false;
if (!mixer->IsPlugin(software_mixer_plugin) &&
/* "global" mixers can be used even if the output hasn't
been used yet */
!(mixer->IsGlobal() ? ao.IsEnabled() : ao.IsReallyEnabled()))
return SetVolumeResult::DISABLED;
try {
mixer_set_volume(mixer, volume);
return true;
return SetVolumeResult::OK;
} catch (...) {
FmtError(mixer_domain,
"Failed to set mixer for '{}': {}",
ao.GetName(), std::current_exception());
return false;
std::throw_with_nested(std::runtime_error(fmt::format("Failed to set mixer for '{}'",
ao.GetName())));
}
}
bool
MultipleOutputs::SetVolume(unsigned volume) noexcept
void
MultipleOutputs::SetVolume(unsigned volume)
{
assert(volume <= 100);
bool success = false;
for (const auto &ao : outputs)
success = output_mixer_set_volume(*ao, volume)
|| success;
SetVolumeResult result = SetVolumeResult::NO_MIXER;
std::exception_ptr error;
return success;
for (const auto &ao : outputs) {
try {
auto r = output_mixer_set_volume(*ao, volume);
if (r > result)
result = r;
} catch (...) {
/* remember the first error */
if (!error) {
error = std::current_exception();
result = SetVolumeResult::ERROR;
}
}
}
switch (result) {
case SetVolumeResult::NO_MIXER:
throw std::runtime_error{"No mixer"};
case SetVolumeResult::DISABLED:
throw std::runtime_error{"All outputs are disabled"};
case SetVolumeResult::ERROR:
std::rethrow_exception(error);
case SetVolumeResult::OK:
break;
}
}
static int

@@ -60,9 +60,9 @@ mixer_open(Mixer *mixer)
try {
mixer->Open();
mixer->open = true;
mixer->failed = false;
mixer->failure = {};
} catch (...) {
mixer->failed = true;
mixer->failure = std::current_exception();
throw;
}
}
@@ -75,6 +75,7 @@ mixer_close_internal(Mixer *mixer)
mixer->Close();
mixer->open = false;
mixer->failure = {};
}
void
@@ -95,20 +96,6 @@ mixer_auto_close(Mixer *mixer)
mixer_close(mixer);
}
/*
* Close the mixer due to failure. The mutex must be locked before
* calling this function.
*/
static void
mixer_failed(Mixer *mixer)
{
assert(mixer->open);
mixer_close_internal(mixer);
mixer->failed = true;
}
int
mixer_get_volume(Mixer *mixer)
{
@@ -116,7 +103,7 @@ mixer_get_volume(Mixer *mixer)
assert(mixer != nullptr);
if (mixer->plugin.global && !mixer->failed)
if (mixer->plugin.global && !mixer->failure)
mixer_open(mixer);
const std::scoped_lock<Mutex> protect(mixer->mutex);
@@ -125,7 +112,8 @@ mixer_get_volume(Mixer *mixer)
try {
volume = mixer->GetVolume();
} catch (...) {
mixer_failed(mixer);
mixer_close_internal(mixer);
mixer->failure = std::current_exception();
throw;
}
} else
@@ -140,11 +128,13 @@ mixer_set_volume(Mixer *mixer, unsigned volume)
assert(mixer != nullptr);
assert(volume <= 100);
if (mixer->plugin.global && !mixer->failed)
if (mixer->plugin.global && !mixer->failure)
mixer_open(mixer);
const std::scoped_lock<Mutex> protect(mixer->mutex);
if (mixer->open)
mixer->SetVolume(volume);
else if (mixer->failure)
std::rethrow_exception(mixer->failure);
}

@@ -25,6 +25,8 @@
#include "thread/Mutex.hxx"
#include "util/Compiler.h"
#include <exception>
class MixerListener;
class Mixer {
@@ -39,17 +41,17 @@ public:
*/
Mutex mutex;
/**
* Contains error details if this mixer has failed. If set,
* it should not be reopened automatically.
*/
std::exception_ptr failure;
/**
* Is the mixer device currently open?
*/
bool open = false;
/**
* Has this mixer failed, and should not be reopened
* automatically?
*/
bool failed = false;
public:
explicit Mixer(const MixerPlugin &_plugin,
MixerListener &_listener) noexcept
@@ -63,6 +65,10 @@ public:
return &plugin == &other;
}
bool IsGlobal() const noexcept {
return plugin.global;
}
/**
* Open mixer device
*

@@ -71,16 +71,16 @@ software_volume_change(MultipleOutputs &outputs, unsigned volume)
return true;
}
static bool
static void
hardware_volume_change(MultipleOutputs &outputs, unsigned volume)
{
/* reset the cache */
last_hardware_volume = -1;
return outputs.SetVolume(volume);
outputs.SetVolume(volume);
}
bool
void
volume_level_change(MultipleOutputs &outputs, unsigned volume)
{
assert(volume <= 100);
@@ -89,7 +89,7 @@ volume_level_change(MultipleOutputs &outputs, unsigned volume)
idle_add(IDLE_MIXER);
return hardware_volume_change(outputs, volume);
hardware_volume_change(outputs, volume);
}
bool

@@ -30,7 +30,10 @@ InvalidateHardwareVolume() noexcept;
int
volume_level_get(const MultipleOutputs &outputs) noexcept;
bool
/**
* Throws on error.
*/
void
volume_level_change(MultipleOutputs &outputs, unsigned volume);
bool

@@ -141,10 +141,11 @@ public:
/**
* Sets the volume on all available mixers.
*
* Throws on error.
*
* @param volume the volume (range 0..100)
* @return true on success, false on failure
*/
bool SetVolume(unsigned volume) noexcept;
void SetVolume(unsigned volume);
/**
* Similar to GetVolume(), but gets the volume only for

@@ -160,13 +160,13 @@ OSXOutput::Create(EventLoop &, const ConfigBlock &block)
static constexpr AudioObjectPropertyAddress default_system_output_device{
kAudioHardwarePropertyDefaultSystemOutputDevice,
kAudioObjectPropertyScopeOutput,
kAudioObjectPropertyElementMaster,
kAudioObjectPropertyElementMain,
};
static constexpr AudioObjectPropertyAddress default_output_device{
kAudioHardwarePropertyDefaultOutputDevice,
kAudioObjectPropertyScopeOutput,
kAudioObjectPropertyElementMaster
kAudioObjectPropertyElementMain
};
const auto &aopa =
@@ -195,9 +195,9 @@ int
OSXOutput::GetVolume()
{
static constexpr AudioObjectPropertyAddress aopa = {
kAudioHardwareServiceDeviceProperty_VirtualMasterVolume,
kAudioHardwareServiceDeviceProperty_VirtualMainVolume,
kAudioObjectPropertyScopeOutput,
kAudioObjectPropertyElementMaster,
kAudioObjectPropertyElementMain,
};
const auto vol = AudioObjectGetPropertyDataT<Float32>(dev_id,
@@ -211,9 +211,9 @@ OSXOutput::SetVolume(unsigned new_volume)
{
Float32 vol = new_volume / 100.0;
static constexpr AudioObjectPropertyAddress aopa = {
kAudioHardwareServiceDeviceProperty_VirtualMasterVolume,
kAudioHardwareServiceDeviceProperty_VirtualMainVolume,
kAudioObjectPropertyScopeOutput,
kAudioObjectPropertyElementMaster
kAudioObjectPropertyElementMain
};
UInt32 size = sizeof(vol);
OSStatus status = AudioObjectSetPropertyData(dev_id,
@@ -366,25 +366,25 @@ osx_output_set_device_format(AudioDeviceID dev_id,
static constexpr AudioObjectPropertyAddress aopa_device_streams = {
kAudioDevicePropertyStreams,
kAudioObjectPropertyScopeOutput,
kAudioObjectPropertyElementMaster
kAudioObjectPropertyElementMain
};
static constexpr AudioObjectPropertyAddress aopa_stream_direction = {
kAudioStreamPropertyDirection,
kAudioObjectPropertyScopeOutput,
kAudioObjectPropertyElementMaster
kAudioObjectPropertyElementMain
};
static constexpr AudioObjectPropertyAddress aopa_stream_phys_formats = {
kAudioStreamPropertyAvailablePhysicalFormats,
kAudioObjectPropertyScopeOutput,
kAudioObjectPropertyElementMaster
kAudioObjectPropertyElementMain
};
static constexpr AudioObjectPropertyAddress aopa_stream_phys_format = {
kAudioStreamPropertyPhysicalFormat,
kAudioObjectPropertyScopeOutput,
kAudioObjectPropertyElementMaster
kAudioObjectPropertyElementMain
};
OSStatus err;
@@ -484,7 +484,7 @@ osx_output_hog_device(AudioDeviceID dev_id, bool hog) noexcept
static constexpr AudioObjectPropertyAddress aopa = {
kAudioDevicePropertyHogMode,
kAudioObjectPropertyScopeOutput,
kAudioObjectPropertyElementMaster
kAudioObjectPropertyElementMain
};
pid_t hog_pid;
@@ -538,7 +538,7 @@ IsAudioDeviceName(AudioDeviceID id, const char *expected_name) noexcept
static constexpr AudioObjectPropertyAddress aopa_name{
kAudioObjectPropertyName,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMaster,
kAudioObjectPropertyElementMain,
};
char actual_name[256];
@@ -561,7 +561,7 @@ FindAudioDeviceByName(const char *name)
static constexpr AudioObjectPropertyAddress aopa_hw_devices{
kAudioHardwarePropertyDevices,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMaster,
kAudioObjectPropertyElementMain,
};
const auto ids =

@@ -24,6 +24,7 @@
#include "../Error.hxx"
#include "mixer/plugins/PipeWireMixerPlugin.hxx"
#include "pcm/Silence.hxx"
#include "lib/fmt/ExceptionFormatter.hxx"
#include "system/Error.hxx"
#include "util/BitReverse.hxx"
#include "util/Domain.hxx"
@@ -56,6 +57,7 @@
#include <algorithm>
#include <array>
#include <numeric>
#include <stdexcept>
#include <string>
@@ -85,7 +87,14 @@ class PipeWireOutput final : AudioOutput {
uint32_t target_id = PW_ID_ANY;
float volume = 1.0;
/**
* The current volume level (0.0 .. 1.0).
*
* This get initialized to -1 which means "unknown", so
* restore_volume will not attempt to override PipeWire's
* initial volume level.
*/
float volume = -1;
PipeWireMixer *mixer = nullptr;
unsigned channels;
@@ -217,26 +226,34 @@ private:
o.Drained();
}
void ControlInfo(const struct pw_stream_control *control) noexcept {
float sum = 0;
unsigned c;
for (c = 0; c < control->n_values; c++)
sum += control->values[c];
void OnChannelVolumes(const struct pw_stream_control &control) noexcept {
if (control.n_values < 1)
return;
sum /= control->n_values;
float sum = std::accumulate(control.values,
control.values + control.n_values,
0.0f);
volume = std::cbrt(sum / control.n_values);
if (mixer != nullptr)
pipewire_mixer_on_change(*mixer, std::cbrt(sum));
pipewire_mixer_on_change(*mixer, volume);
pw_thread_loop_signal(thread_loop, false);
}
static void ControlInfo(void *data,
[[maybe_unused]] uint32_t id,
void ControlInfo([[maybe_unused]] uint32_t id,
const struct pw_stream_control &control) noexcept {
switch (id) {
case SPA_PROP_channelVolumes:
OnChannelVolumes(control);
break;
}
}
static void ControlInfo(void *data, uint32_t id,
const struct pw_stream_control *control) noexcept {
auto &o = *(PipeWireOutput *)data;
if (StringIsEqual(control->name, "Channel Volumes"))
o.ControlInfo(control);
o.ControlInfo(id, *control);
}
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
@@ -308,22 +325,38 @@ PipeWireOutput::PipeWireOutput(const ConfigBlock &block)
}
}
/**
* Throws on error.
*
* @param volume a volume level between 0.0 and 1.0
*/
static void
SetVolume(struct pw_stream &stream, unsigned channels, float volume)
{
float value[MAX_CHANNELS];
std::fill_n(value, channels, volume * volume * volume);
if (pw_stream_set_control(&stream,
SPA_PROP_channelVolumes, channels, value,
0) != 0)
throw std::runtime_error("pw_stream_set_control() failed");
}
void
PipeWireOutput::SetVolume(float _volume)
{
if (thread_loop == nullptr) {
/* the mixer is open (because it is a "global" mixer),
but Enable() on this output has not yet been
called */
volume = _volume;
return;
}
const PipeWire::ThreadLoopLock lock(thread_loop);
float newvol = _volume*_volume*_volume;
if (stream != nullptr && !restore_volume) {
float vol[MAX_CHANNELS];
std::fill_n(vol, channels, newvol);
if (pw_stream_set_control(stream,
SPA_PROP_channelVolumes, channels, vol,
0) != 0)
throw std::runtime_error("pw_stream_set_control() failed");
}
if (stream != nullptr && !restore_volume)
::SetVolume(*stream, channels, _volume);
volume = _volume;
}
@@ -639,7 +672,16 @@ PipeWireOutput::ParamChanged([[maybe_unused]] uint32_t id,
{
if (restore_volume) {
restore_volume = false;
SetVolume(volume);
if (volume >= 0) {
try {
::SetVolume(*stream, channels, volume);
} catch (...) {
FmtError(pipewire_output_domain,
FMT_STRING("Failed to restore volume: {}"),
std::current_exception());
}
}
}
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
@@ -824,6 +866,17 @@ PipeWireOutput::Drain()
{
const PipeWire::ThreadLoopLock lock(thread_loop);
if (drained)
return;
if (!active) {
/* there is data in the ring_buffer, but the stream is
not yet active; activate it now to ensure it is
played before this method returns */
active = true;
pw_stream_set_active(stream, true);
}
drain_requested = true;
AtScopeExit(this) { drain_requested = false; };
@@ -839,7 +892,24 @@ PipeWireOutput::Cancel() noexcept
const PipeWire::ThreadLoopLock lock(thread_loop);
interrupted = false;
if (drained)
return;
/* clear MPD's ring buffer */
ring_buffer->reset();
/* clear libpipewire's buffer */
pw_stream_flush(stream, false);
drained = true;
/* pause the PipeWire stream so libpipewire ceases invoking
the "process" callback (we have no data until our Play()
method gets called again); the stream will be resume by
Play() after the ring_buffer has been refilled */
if (active) {
active = false;
pw_stream_set_active(stream, false);
}
}
bool

@@ -214,9 +214,8 @@ SnapcastOutput::RemoveClient(SnapcastClient &client) noexcept
std::chrono::steady_clock::duration
SnapcastOutput::Delay() const noexcept
{
if (!LockHasClients() && pause) {
/* if there's no client and this output is paused,
then Pause() will not do anything, it will not fill
if (pause) {
/* Pause() will not do anything, it will not fill
the buffer and it will not update the timer;
therefore, we reset the timer here */
timer->Reset();

@@ -227,7 +227,7 @@ IsXmlContentType(const char *content_type) noexcept
gcc_pure
static bool
IsXmlContentType(const std::multimap<std::string, std::string> &headers) noexcept
IsXmlContentType(const Curl::Headers &headers) noexcept
{
auto i = headers.find("content-type");
return i != headers.end() && IsXmlContentType(i->second.c_str());
@@ -297,8 +297,7 @@ private:
}
/* virtual methods from CurlResponseHandler */
void OnHeaders(unsigned status,
std::multimap<std::string, std::string> &&headers) final {
void OnHeaders(unsigned status, Curl::Headers &&headers) final {
if (status != 207)
throw FormatRuntimeError("Status %d from WebDAV server; expected \"207 Multi-Status\"",
status);

@@ -3,10 +3,10 @@ directory = fmt-8.1.1
source_url = https://github.com/fmtlib/fmt/archive/8.1.1.tar.gz
source_filename = fmt-8.1.1.tar.gz
source_hash = 3d794d3cf67633b34b2771eb9f073bde87e846e0d395d254df7b211ef1ec7346
patch_filename = fmt_8.1.1-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/fmt_8.1.1-1/get_patch
patch_hash = 6035a67c7a8c90bed74c293c7265c769f47a69816125f7566bccb8e2543cee5e
patch_filename = fmt_8.1.1-2_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/fmt_8.1.1-2/get_patch
patch_hash = cd001046281330a8862591780a9ea71a1fa594edd0d015deb24e44680c9ea33b
wrapdb_version = 8.1.1-2
[provide]
fmt = fmt_dep

@@ -3,9 +3,10 @@ directory = libvorbis-1.3.7
source_url = https://downloads.xiph.org/releases/vorbis/libvorbis-1.3.7.tar.xz
source_filename = libvorbis-1.3.7.tar.xz
source_hash = b33cc4934322bcbf6efcbacf49e3ca01aadbea4114ec9589d1b1e9d20f72954b
patch_filename = vorbis_1.3.7-2_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/vorbis_1.3.7-2/get_patch
patch_hash = fe302576cbf8408754b332b539ea1b83f0f96fa9aae50a5d1fea911713d5f21c
patch_filename = vorbis_1.3.7-3_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/vorbis_1.3.7-3/get_patch
patch_hash = 6cb90a61ede8c64d3e8e379b96dcc800c9dd69e925122b3d73d8f59a563c3afa
wrapdb_version = 1.3.7-3
[provide]
vorbis = vorbis_dep

@@ -41,8 +41,7 @@ public:
}
/* virtual methods from CurlResponseHandler */
void OnHeaders(unsigned status,
std::multimap<std::string, std::string> &&headers) override {
void OnHeaders(unsigned status, Curl::Headers &&headers) override {
fprintf(stderr, "status: %u\n", status);
for (const auto &i : headers)
fprintf(stderr, "%s: %s\n",