Compare commits

..

20 Commits

Author SHA1 Message Date
Max Kellermann
fff25ac753 release v0.23.1 2021-10-19 10:27:28 +02:00
Max Kellermann
4f1e79b6b8 filter/ReplayGain: emit "mixer" event when replay gain changes volume
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1294
2021-10-19 10:03:21 +02:00
Max Kellermann
aa9933c0b5 output/pipewire: add noexcept 2021-10-19 08:58:50 +02:00
Max Kellermann
0697d1f859 output/pipewire: include cleanup 2021-10-19 08:57:33 +02:00
Max Kellermann
df033fa4aa NEWS: mention the previous commit 2021-10-19 08:56:32 +02:00
Nicolai Syvertsen
b941a7df83 Implement volume updates for pipewire output 2021-10-19 00:01:45 +02:00
Max Kellermann
31151cec3c command/playlist: "load" supports relative positions
This commit also increases the PROTOCOL_VERSION so clients can detect
the availability of the feature.
2021-10-18 22:08:22 +02:00
Max Kellermann
07e8c338df command/queue: move position parameter functions to separate library 2021-10-18 22:07:04 +02:00
Max Kellermann
b22d7218aa command/player, ...: use decimal notation
During the libfmt migration, I converted "%1.3f" to just "{:1.3}"
without the "f" suffix, but libfmt defaults to scientific notation,
which can break some MPD clients.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1291
2021-10-18 16:54:53 +02:00
Max Kellermann
d5be8c74b0 output/pipewire: attempt to change the graph sample rate
Requires PipeWire 0.3.32.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1283
2021-10-18 16:46:23 +02:00
Max Kellermann
c112cb60da output/snapcast: fix typo which caused "Failed to get chunk"
This bug caused a 9 second offset in all time stamps.  Due to that,
the Snapcast server thought the chunks are too old and discarded them.

Fixes https://github.com/MusicPlayerDaemon/MPD/discussions/1287
2021-10-18 16:40:11 +02:00
Max Kellermann
677fa4f9bc doc/plugins.rst: mention that the snapcast output requires a format 2021-10-17 20:01:21 +02:00
Max Kellermann
907af2ad02 Permission: refactor getPermissionFromPassword() to return std::optional
This replaces the output parameter (which is bad API design).  As a
side effect, it fixes the bad [[gnu::pure]] attribute added by commit
a636d2127 which caused optimizing compilers to miscompile calls to
that function.  "Pure" functions can be assumed to have no output
arguments, so the compiler can assume the function doesn't modify
them.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1282
2021-10-17 19:58:50 +02:00
Thomas Zander
6a2e7bbc02 protocol/ArgParser.cxx: Add missing #include <stdio.h>
Fixes a build problem on platforms where stdio.h is not included
transitively. snprintf() is defined in stdio.h.
2021-10-16 17:38:07 +02:00
Max Kellermann
771c46032f meson.build: add missing libfmt dependencies
Fixes https://github.com/MusicPlayerDaemon/MPD/discussions/1281

The problem occurred when there was libfmt-dev installed, but it was
too old (e.g. on Debian Buster), and Meson used the wrap fallback.
Those internal MPD libraries where the libfmt dependency was not
declared were still using the old system libfmt headers, which are not
ABI-compatible with MPD's own libfmt build.
2021-10-15 14:26:59 +02:00
Max Kellermann
85611aa456 storage/smbclient: add StoragePlugin.prefixes
Should have been part of commit
ef24cfa523

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1279
2021-10-15 10:24:30 +02:00
Max Kellermann
466b5cb08d neighbor/smbclient: FmtError() instead of FormatErrno()
Fixes part 2 of https://github.com/MusicPlayerDaemon/MPD/issues/1279
2021-10-15 09:40:36 +02:00
Max Kellermann
3f2f3251cb neighbor/smbclient: use [[gnu::pure]]
Fixes part 1 of https://github.com/MusicPlayerDaemon/MPD/issues/1279
2021-10-15 09:39:34 +02:00
Max Kellermann
8ae85f3991 doc/protocol.rst: move POSITION from "search" to "findadd"
Whoops, I misplaced this one.
2021-10-14 15:36:25 +02:00
Max Kellermann
781fe4ff28 increment version number to 0.23.1 2021-10-14 15:36:16 +02:00
31 changed files with 334 additions and 136 deletions

12
NEWS

@@ -1,3 +1,15 @@
ver 0.23.1 (2021/10/19)
* protocol
- use decimal notation instead of scientific notation
- "load" supports relative positions
* output
- emit "mixer" idle event when replay gain changes volume
- pipewire: emit "mixer" idle events on external volume change
- pipewire: attempt to change the graph sample rate
- snapcast: fix time stamp bug which caused "Failed to get chunk"
* fix libfmt linker problems
* fix broken password authentication
ver 0.23 (2021/10/14)
* protocol
- new command "getvol"

@@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.musicpd"
android:installLocation="auto"
android:versionCode="60"
android:versionName="0.23">
android:versionCode="61"
android:versionName="0.23.1">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/>

@@ -38,9 +38,9 @@ author = 'Max Kellermann'
# built documents.
#
# The short X.Y version.
version = '0.23'
version = '0.23.1'
# The full version, including alpha/beta/rc tags.
release = version + '~git'
#release = version + '~git'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

@@ -1193,6 +1193,8 @@ allows MPD to act as a `Snapcast
<https://github.com/badaix/snapcast>`__ server. Snapcast clients
connect to it and receive audio data from MPD.
You must set a format.
.. list-table::
:widths: 20 80
:header-rows: 1

@@ -923,8 +923,9 @@ remote playlists (absolute URI with a supported scheme).
only a part of the playlist.
The ``POSITION`` parameter specifies where the songs will be
inserted into the queue. (This requires specifying the range as
well; the special value `0:` can be used if the whole playlist
inserted into the queue; it can be relative as described in
:ref:`addid <command_addid>`. (This requires specifying the range
as well; the special value `0:` can be used if the whole playlist
shall be loaded at a certain queue position.)
.. _command_playlistadd:
@@ -1059,11 +1060,11 @@ The music database
.. _command_findadd:
:command:`findadd {FILTER} [sort {TYPE}] [window {START:END}]`
:command:`findadd {FILTER} [sort {TYPE}] [window {START:END}] [position POS]`
Search the database for songs matching
``FILTER`` (see :ref:`Filters <filter_syntax>`) and add them to
the queue. Parameters have the same meaning as for
:ref:`find <command_find>`.
:ref:`find <command_find>` and :ref:`searchadd <command_searchadd>`.
.. _command_list:
@@ -1196,15 +1197,12 @@ The music database
.. _command_search:
:command:`search {FILTER} [sort {TYPE}] [window {START:END}] [position POS]`
:command:`search {FILTER} [sort {TYPE}] [window {START:END}]
Search the database for songs matching
``FILTER`` (see :ref:`Filters <filter_syntax>`). Parameters
have the same meaning as for :ref:`find <command_find>`,
except that search is not case sensitive.
The ``position`` parameter specifies where the songs will be
inserted.
.. _command_searchadd:
:command:`searchadd {FILTER} [sort {TYPE}] [window {START:END}] [position POS]`
@@ -1214,6 +1212,9 @@ The music database
Parameters have the same meaning as for :ref:`search <command_search>`.
The ``position`` parameter specifies where the songs will be
inserted.
.. _command_searchaddpl:
:command:`searchaddpl {NAME} {FILTER} [sort {TYPE}] [window {START:END}]`

@@ -1,7 +1,7 @@
project(
'mpd',
['c', 'cpp'],
version: '0.23',
version: '0.23.1',
meson_version: '>= 0.56.0',
default_options: [
'c_std=c11',
@@ -44,7 +44,7 @@ version_conf = configuration_data()
version_conf.set_quoted('PACKAGE', meson.project_name())
version_conf.set_quoted('PACKAGE_NAME', meson.project_name())
version_conf.set_quoted('VERSION', meson.project_version())
version_conf.set_quoted('PROTOCOL_VERSION', '0.23.0')
version_conf.set_quoted('PROTOCOL_VERSION', '0.23.1')
configure_file(output: 'Version.h', configuration: version_conf)
conf = configuration_data()
@@ -267,6 +267,7 @@ sources = [
'src/protocol/ArgParser.cxx',
'src/protocol/Result.cxx',
'src/command/CommandError.cxx',
'src/command/PositionArg.cxx',
'src/command/AllCommands.cxx',
'src/command/QueueCommands.cxx',
'src/command/TagCommands.cxx',

@@ -161,15 +161,14 @@ GetPermissionsFromAddress(SocketAddress address) noexcept
#endif
int
getPermissionFromPassword(const char *password, unsigned *permission) noexcept
std::optional<unsigned>
GetPermissionFromPassword(const char *password) noexcept
{
auto i = permission_passwords.find(password);
if (i == permission_passwords.end())
return -1;
return std::nullopt;
*permission = i->second;
return 0;
return i->second;
}
unsigned

@@ -22,6 +22,8 @@
#include "config.h"
#include <optional>
struct ConfigData;
class SocketAddress;
@@ -32,9 +34,13 @@ static constexpr unsigned PERMISSION_CONTROL = 4;
static constexpr unsigned PERMISSION_ADMIN = 8;
static constexpr unsigned PERMISSION_PLAYER = 16;
/**
* @return the permissions for the given password or std::nullopt if
* the password is not accepted
*/
[[gnu::pure]]
int
getPermissionFromPassword(const char *password, unsigned *permission) noexcept;
std::optional<unsigned>
GetPermissionFromPassword(const char *password) noexcept;
[[gnu::const]]
unsigned

@@ -100,7 +100,7 @@ song_print_info(Response &r, const LightSong &song, bool base) noexcept
const auto duration = song.GetDuration();
if (!duration.IsNegative())
r.Fmt(FMT_STRING("Time: {}\n"
"duration: {:1.3}\n"),
"duration: {:1.3f}\n"),
duration.RoundS(),
duration.ToDoubleS());
}
@@ -123,7 +123,7 @@ song_print_info(Response &r, const DetachedSong &song, bool base) noexcept
const auto duration = song.GetDuration();
if (!duration.IsNegative())
r.Fmt(FMT_STRING("Time: {}\n"
"duration: {:1.3}\n"),
"duration: {:1.3f}\n"),
duration.RoundS(),
duration.ToDoubleS());
}

@@ -60,7 +60,7 @@ tag_print(Response &r, const Tag &tag) noexcept
{
if (!tag.duration.IsNegative())
r.Fmt(FMT_STRING("Time: {}\n"
"duration: {:1.3}\n"),
"duration: {:1.3f}\n"),
tag.duration.RoundS(),
tag.duration.ToDoubleS());

@@ -58,13 +58,13 @@ handle_binary_limit(Client &client, Request args,
CommandResult
handle_password(Client &client, Request args, Response &r)
{
unsigned permission = 0;
if (getPermissionFromPassword(args.front(), &permission) < 0) {
const auto permission = GetPermissionFromPassword(args.front());
if (!permission) {
r.Error(ACK_ERROR_PASSWORD, "incorrect password");
return CommandResult::ERROR;
}
client.SetPermission(permission);
client.SetPermission(*permission);
return CommandResult::OK;
}

@@ -171,7 +171,7 @@ handle_status(Client &client, [[maybe_unused]] Request args, Response &r)
if (player_status.state != PlayerState::STOP) {
r.Fmt(FMT_STRING(COMMAND_STATUS_TIME ": {}:{}\n"
"elapsed: {:1.3}\n"
"elapsed: {:1.3f}\n"
COMMAND_STATUS_BITRATE ": {}\n"),
player_status.elapsed_time.RoundS(),
player_status.total_time.IsNegative()
@@ -181,7 +181,7 @@ handle_status(Client &client, [[maybe_unused]] Request args, Response &r)
player_status.bit_rate);
if (!player_status.total_time.IsNegative())
r.Fmt(FMT_STRING("duration: {:1.3}\n"),
r.Fmt(FMT_STRING("duration: {:1.3f}\n"),
player_status.total_time.ToDoubleS());
if (player_status.audio_format.IsDefined())

@@ -19,6 +19,7 @@
#include "config.h"
#include "PlaylistCommands.hxx"
#include "PositionArg.hxx"
#include "Request.hxx"
#include "Instance.hxx"
#include "db/Selection.hxx"
@@ -86,7 +87,7 @@ handle_load(Client &client, Request args, [[maybe_unused]] Response &r)
const unsigned old_size = playlist.GetLength();
const unsigned position = args.size > 2
? args.ParseUnsigned(2, old_size)
? ParseInsertPosition(args[2], partition.playlist)
: old_size;
const SongLoader loader(client);

103
src/command/PositionArg.cxx Normal file

@@ -0,0 +1,103 @@
/*
* 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 "PositionArg.hxx"
#include "protocol/Ack.hxx"
#include "protocol/ArgParser.hxx"
#include "protocol/RangeArg.hxx"
#include "queue/Playlist.hxx"
static unsigned
RequireCurrentPosition(const playlist &p)
{
int position = p.GetCurrentPosition();
if (position < 0)
throw ProtocolError(ACK_ERROR_PLAYER_SYNC,
"No current song");
return position;
}
unsigned
ParseInsertPosition(const char *s, const playlist &playlist)
{
const auto queue_length = playlist.queue.GetLength();
if (*s == '+') {
/* after the current song */
const unsigned current = RequireCurrentPosition(playlist);
assert(current < queue_length);
return current + 1 +
ParseCommandArgUnsigned(s + 1,
queue_length - current - 1);
} else if (*s == '-') {
/* before the current song */
const unsigned current = RequireCurrentPosition(playlist);
assert(current < queue_length);
return current - ParseCommandArgUnsigned(s + 1, current);
} else
/* absolute position */
return ParseCommandArgUnsigned(s, queue_length);
}
unsigned
ParseMoveDestination(const char *s, const RangeArg range, const playlist &p)
{
assert(!range.IsEmpty());
assert(!range.IsOpenEnded());
const unsigned queue_length = p.queue.GetLength();
if (*s == '+') {
/* after the current song */
unsigned current = RequireCurrentPosition(p);
assert(current < queue_length);
if (range.Contains(current))
throw ProtocolError(ACK_ERROR_ARG, "Cannot move current song relative to itself");
if (current >= range.end)
current -= range.Count();
return current + 1 +
ParseCommandArgUnsigned(s + 1,
queue_length - current - range.Count());
} else if (*s == '-') {
/* before the current song */
unsigned current = RequireCurrentPosition(p);
assert(current < queue_length);
if (range.Contains(current))
throw ProtocolError(ACK_ERROR_ARG, "Cannot move current song relative to itself");
if (current >= range.end)
current -= range.Count();
return current -
ParseCommandArgUnsigned(s + 1,
queue_length - current - range.Count());
} else
/* absolute position */
return ParseCommandArgUnsigned(s,
queue_length - range.Count());
}

@@ -0,0 +1,32 @@
/*
* 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.
*/
#pragma once
struct playlist;
struct RangeArg;
/**
* Throws #ProtocolError on error.
*/
unsigned
ParseInsertPosition(const char *s, const playlist &playlist);
unsigned
ParseMoveDestination(const char *s, const RangeArg range, const playlist &p);

@@ -19,6 +19,7 @@
#include "config.h"
#include "QueueCommands.hxx"
#include "PositionArg.hxx"
#include "Request.hxx"
#include "protocol/RangeArg.hxx"
#include "db/DatabaseQueue.hxx"
@@ -43,17 +44,6 @@
#include <limits>
static unsigned
RequireCurrentPosition(const playlist &p)
{
int position = p.GetCurrentPosition();
if (position < 0)
throw ProtocolError(ACK_ERROR_PLAYER_SYNC,
"No current song");
return position;
}
static void
AddUri(Client &client, const LocatedUri &uri)
{
@@ -129,30 +119,8 @@ handle_addid(Client &client, Request args, Response &r)
const auto queue_length = partition.playlist.queue.GetLength();
if (args.size > 1) {
const char *const s = args[1];
if (*s == '+') {
/* after the current song */
const unsigned current =
RequireCurrentPosition(partition.playlist);
assert(current < queue_length);
to = current + 1 +
ParseCommandArgUnsigned(s + 1,
queue_length - current - 1);
} else if (*s == '-') {
/* before the current song */
const unsigned current =
RequireCurrentPosition(partition.playlist);
assert(current < queue_length);
to = current - ParseCommandArgUnsigned(s + 1, current);
} else
/* absolute position */
to = args.ParseUnsigned(1, queue_length);
}
if (args.size > 1)
to = ParseInsertPosition(args[1], partition.playlist);
const SongLoader loader(client);
const unsigned added_position = queue_length;
@@ -363,49 +331,6 @@ handle_prioid(Client &client, Request args, [[maybe_unused]] Response &r)
return CommandResult::OK;
}
static unsigned
ParseMoveDestination(const char *s, const RangeArg range,
const playlist &p)
{
assert(!range.IsEmpty());
assert(!range.IsOpenEnded());
const unsigned queue_length = p.queue.GetLength();
if (*s == '+') {
/* after the current song */
unsigned current = RequireCurrentPosition(p);
assert(current < queue_length);
if (range.Contains(current))
throw ProtocolError(ACK_ERROR_ARG, "Cannot move current song relative to itself");
if (current >= range.end)
current -= range.Count();
return current + 1 +
ParseCommandArgUnsigned(s + 1,
queue_length - current - range.Count());
} else if (*s == '-') {
/* before the current song */
unsigned current = RequireCurrentPosition(p);
assert(current < queue_length);
if (range.Contains(current))
throw ProtocolError(ACK_ERROR_ARG, "Cannot move current song relative to itself");
if (current >= range.end)
current -= range.Count();
return current -
ParseCommandArgUnsigned(s + 1,
queue_length - current - range.Count());
} else
/* absolute position */
return ParseCommandArgUnsigned(s,
queue_length - range.Count());
}
static CommandResult
handle_move(Partition &partition, RangeArg range, const char *to)
{

@@ -27,6 +27,7 @@
#include "pcm/Volume.hxx"
#include "util/ConstBuffer.hxx"
#include "util/Domain.hxx"
#include "Idle.hxx"
#include "Log.hxx"
#include <cassert>
@@ -169,6 +170,10 @@ ReplayGainFilter::Update()
try {
mixer_set_volume(mixer, _volume);
/* TODO: emit this idle event only for the
current partition */
idle_add(IDLE_MIXER);
} catch (...) {
LogError(std::current_exception(),
"Failed to update hardware mixer");

@@ -51,6 +51,7 @@ upnp = static_library(
'Util.cxx',
include_directories: inc,
dependencies: [
log_dep,
upnp_dep,
curl_dep,
expat_dep,

@@ -37,6 +37,8 @@ public:
{
}
~PipeWireMixer() noexcept override;
PipeWireMixer(const PipeWireMixer &) = delete;
PipeWireMixer &operator=(const PipeWireMixer &) = delete;
@@ -82,7 +84,14 @@ pipewire_mixer_init([[maybe_unused]] EventLoop &event_loop, AudioOutput &ao,
const ConfigBlock &)
{
auto &po = (PipeWireOutput &)ao;
return new PipeWireMixer(po, listener);
auto *pm = new PipeWireMixer(po, listener);
pipewire_output_set_mixer(po, *pm);
return pm;
}
PipeWireMixer::~PipeWireMixer() noexcept
{
pipewire_output_clear_mixer(output, *this);
}
const MixerPlugin pipewire_mixer_plugin = {

@@ -33,6 +33,8 @@
#include <libsmbclient.h>
#include <cerrno>
#include <cstring>
#include <utility>
class SmbclientNeighborExplorer final : public NeighborExplorer {
@@ -45,12 +47,12 @@ class SmbclientNeighborExplorer final : public NeighborExplorer {
Server(const Server &) = delete;
gcc_pure
[[gnu::pure]]
bool operator==(const Server &other) const noexcept {
return name == other.name;
}
[[nodiscard]] gcc_pure
[[nodiscard]] [[gnu::pure]]
NeighborInfo Export() const noexcept {
return { "smb://" + name + "/", comment };
}
@@ -165,11 +167,11 @@ ReadServers(SmbclientContext &ctx, const char *uri,
ReadServers(ctx, handle, list);
ctx.CloseDirectory(handle);
} else
FormatErrno(smbclient_domain, "smbc_opendir('%s') failed",
uri);
FmtError(smbclient_domain, "smbc_opendir('{}') failed: {}",
uri, strerror(errno));
}
gcc_pure
[[gnu::pure]]
static NeighborExplorer::List
DetectServers(SmbclientContext &ctx) noexcept
{
@@ -178,7 +180,7 @@ DetectServers(SmbclientContext &ctx) noexcept
return list;
}
gcc_pure
[[gnu::pure]]
static NeighborExplorer::List::iterator
FindBeforeServerByURI(NeighborExplorer::List::iterator prev,
NeighborExplorer::List::iterator end,

@@ -25,6 +25,7 @@ neighbor_plugins = static_library(
neighbor_plugins_sources,
include_directories: inc,
dependencies: [
log_dep,
dbus_dep,
smbclient_dep,
upnp_dep,

@@ -41,6 +41,8 @@
#include <spa/param/audio/format-utils.h>
#include <spa/param/props.h>
#include <cmath>
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
@@ -75,6 +77,9 @@ class PipeWireOutput final : AudioOutput {
float volume = 1.0;
PipeWireMixer *mixer = nullptr;
int channels;
/**
* The active sample format, needed for PcmSilence().
*/
@@ -124,11 +129,21 @@ public:
events.state_changed = StateChanged;
events.process = Process;
events.drained = Drained;
events.control_info = ControlInfo;
events.param_changed = ParamChanged;
return events;
}
void SetVolume(float volume);
void SetMixer(PipeWireMixer &_mixer) noexcept;
void ClearMixer([[maybe_unused]] PipeWireMixer &old_mixer) noexcept {
assert(mixer == &old_mixer);
mixer = nullptr;
}
private:
void CheckThrowError() {
if (disconnected)
@@ -163,6 +178,46 @@ 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];
sum /= control->n_values;
if (mixer != nullptr)
pipewire_mixer_on_change(*mixer, std::cbrt(sum));
pw_thread_loop_signal(thread_loop, false);
}
static void ControlInfo(void *data,
[[maybe_unused]] uint32_t id,
const struct pw_stream_control *control) noexcept {
auto &o = *(PipeWireOutput *)data;
if (StringIsEqual(control->name, "Channel Volumes"))
o.ControlInfo(control);
}
void ParamChanged() noexcept {
if (restore_volume) {
SetVolume(volume);
restore_volume = false;
}
}
static void ParamChanged(void *data,
uint32_t id,
const struct spa_pod *param) noexcept
{
if (id != SPA_PARAM_Format || param == NULL)
return;
auto &o = *(PipeWireOutput *)data;
o.ParamChanged();
}
/* virtual methods from class AudioOutput */
void Enable() override;
void Disable() noexcept override;
@@ -214,11 +269,20 @@ PipeWireOutput::SetVolume(float _volume)
{
const PipeWire::ThreadLoopLock lock(thread_loop);
if (stream != nullptr && !restore_volume &&
pw_stream_set_control(stream,
SPA_PROP_volume, 1, &_volume,
float newvol = _volume*_volume*_volume;
if (stream != nullptr && !restore_volume) {
float vol[SPA_AUDIO_MAX_CHANNELS];
int i;
for (i = 0; i < channels; i++) {
vol[i] = newvol;
}
if (pw_stream_set_control(stream,
SPA_PROP_channelVolumes, channels, vol,
0) != 0)
throw std::runtime_error("pw_stream_set_control() failed");
throw std::runtime_error("pw_stream_set_control() failed");
}
volume = _volume;
}
@@ -383,6 +447,13 @@ PipeWireOutput::Open(AudioFormat &audio_format)
if (target != nullptr && target_id == PW_ID_ANY)
pw_properties_setf(props, PW_KEY_NODE_TARGET, "%s", target);
#ifdef PW_KEY_NODE_RATE
/* ask PipeWire to change the graph sample rate to ours
(requires PipeWire 0.3.32) */
pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u",
audio_format.sample_rate);
#endif
const PipeWire::ThreadLoopLock lock(thread_loop);
stream = pw_stream_new_simple(pw_thread_loop_get_loop(thread_loop),
@@ -397,6 +468,7 @@ PipeWireOutput::Open(AudioFormat &audio_format)
frame_size = audio_format.GetFrameSize();
sample_format = audio_format.format;
channels = audio_format.channels;
interrupted = false;
/* allocate a ring buffer of 0.5 seconds */
@@ -444,14 +516,6 @@ PipeWireOutput::StateChanged(enum pw_stream_state state,
if (!was_disconnected && disconnected)
pw_thread_loop_signal(thread_loop, false);
if (state == PW_STREAM_STATE_STREAMING && restore_volume) {
/* restore the last known volume after creating a new
pw_stream */
restore_volume = false;
pw_stream_set_control(stream,
SPA_PROP_volume, 1, &volume,
0);
}
}
inline void
@@ -578,6 +642,28 @@ PipeWireOutput::Pause() noexcept
return true;
}
inline void
PipeWireOutput::SetMixer(PipeWireMixer &_mixer) noexcept
{
assert(mixer == nullptr);
mixer = &_mixer;
// TODO: Check if context and stream is ready and trigger a volume update...
}
void
pipewire_output_set_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept
{
po.SetMixer(pm);
}
void
pipewire_output_clear_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept
{
po.ClearMixer(pm);
}
const struct AudioOutputPlugin pipewire_output_plugin = {
"pipewire",
nullptr,

@@ -21,9 +21,16 @@
#define MPD_PIPEWIRE_OUTPUT_PLUGIN_HXX
class PipeWireOutput;
class PipeWireMixer;
extern const struct AudioOutputPlugin pipewire_output_plugin;
void
pipewire_output_set_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept;
void
pipewire_output_clear_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept;
void
pipewire_output_set_volume(PipeWireOutput &output, float volume);

@@ -45,7 +45,7 @@ struct SnapcastTimestamp {
if (a_usec < b_usec) {
--result_sec;
result_usec += 1'000'0000;
result_usec += 1'000'000;
}
return {result_sec, result_usec};

@@ -228,22 +228,22 @@ SoxrPcmResampler::Open(AudioFormat &af, unsigned new_sample_rate)
FmtDebug(soxr_domain, "soxr engine '{}'", soxr_engine(soxr));
if (soxr_use_custom_recipe)
FmtDebug(soxr_domain,
"soxr precision={:0.0}, phase_response={:0.2}, "
"passband_end={:0.2}, stopband_begin={:0.2} scale={:0.2}",
"soxr precision={:0.0f}, phase_response={:0.2f}, "
"passband_end={:0.2f}, stopband_begin={:0.2f} scale={:0.2f}",
soxr_quality.precision, soxr_quality.phase_response,
soxr_quality.passband_end, soxr_quality.stopband_begin,
soxr_io_custom_recipe.scale);
else
FmtDebug(soxr_domain,
"soxr precision={:0.0}, phase_response={:0.2}, "
"passband_end={:0.2}, stopband_begin={:0.2}",
"soxr precision={:0.0f}, phase_response={:0.2f}, "
"passband_end={:0.2f}, stopband_begin={:0.2f}",
soxr_quality.precision, soxr_quality.phase_response,
soxr_quality.passband_end, soxr_quality.stopband_begin);
channels = af.channels;
ratio = float(new_sample_rate) / float(af.sample_rate);
FmtDebug(soxr_domain, "samplerate conversion ratio to {:.2}", ratio);
FmtDebug(soxr_domain, "samplerate conversion ratio to {:0.2f}", ratio);
/* libsoxr works with floating point samples */
af.format = SampleFormat::FLOAT;

@@ -5,6 +5,7 @@ playlist_plugins_sources = [
]
playlist_plugins_deps = [
log_dep,
expat_dep,
flac_dep,
]

@@ -23,6 +23,7 @@
#include "Chrono.hxx"
#include "util/NumberParser.hxx"
#include <stdio.h>
#include <stdlib.h>
static inline ProtocolError

@@ -186,15 +186,15 @@ SmbclientDirectoryReader::GetInfo([[maybe_unused]] bool follow)
static std::unique_ptr<Storage>
CreateSmbclientStorageURI([[maybe_unused]] EventLoop &event_loop, const char *base)
{
if (!StringStartsWithCaseASCII(base, "smb://"))
return nullptr;
SmbclientInit();
return std::make_unique<SmbclientStorage>(base);
}
static constexpr const char *smbclient_prefixes[] = { "smb://", nullptr };
const StoragePlugin smbclient_storage_plugin = {
"smbclient",
smbclient_prefixes,
CreateSmbclientStorageURI,
};

@@ -44,6 +44,7 @@ storage_plugins = static_library(
storage_plugins_sources,
include_directories: inc,
dependencies: [
log_dep,
curl_dep,
dbus_dep,
expat_dep,

@@ -13,6 +13,7 @@ avahi = static_library(
'Publisher.cxx',
include_directories: inc,
dependencies: [
log_dep,
libavahi_client,
],
)

@@ -127,6 +127,7 @@ if enable_inotify
'../src/db/update/InotifySource.cxx',
include_directories: inc,
dependencies: [
log_dep,
event_dep,
util_dep,
],