Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b7fdff46f2 | ||
![]() |
e16109330d | ||
![]() |
72621531e0 | ||
![]() |
0a48146efc | ||
![]() |
0c4bf12bfd | ||
![]() |
b8e0855ef3 | ||
![]() |
6467502b9d | ||
![]() |
15b67f20e5 | ||
![]() |
0825179f00 | ||
![]() |
97211d0aad | ||
![]() |
029c499bfa | ||
![]() |
0ba867ec16 | ||
![]() |
866d147122 | ||
![]() |
32851d1bc7 | ||
![]() |
78257408b4 | ||
![]() |
f447b7615e | ||
![]() |
1f780b7209 | ||
![]() |
04bf8a6b1a | ||
![]() |
c4c64854d4 | ||
![]() |
17562dc90b | ||
![]() |
7b24316734 | ||
![]() |
5fab107fd3 | ||
![]() |
f31920e092 | ||
![]() |
eb111a10e7 | ||
![]() |
80b09360c6 | ||
![]() |
5ccf78855d | ||
![]() |
fd5a3b5880 | ||
![]() |
6120c1360c | ||
![]() |
a8087dc12c | ||
![]() |
070c03dbf7 | ||
![]() |
0a9bec3754 |
14
NEWS
14
NEWS
@@ -1,3 +1,17 @@
|
||||
ver 0.23.2 (2021/10/22)
|
||||
* protocol
|
||||
- fix "albumart" timeout bug
|
||||
* input
|
||||
- nfs: fix playback bug
|
||||
* output
|
||||
- pipewire: send artist and title to PipeWire
|
||||
- pipewire: DSD support
|
||||
* neighbor
|
||||
- mention failed plugin name in error message
|
||||
* player
|
||||
- fix cross-fade regression
|
||||
* fix crash with libfmt versions older than 7
|
||||
|
||||
ver 0.23.1 (2021/10/19)
|
||||
* protocol
|
||||
- use decimal notation instead of scientific notation
|
||||
|
@@ -2,8 +2,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.musicpd"
|
||||
android:installLocation="auto"
|
||||
android:versionCode="61"
|
||||
android:versionName="0.23.1">
|
||||
android:versionCode="62"
|
||||
android:versionName="0.23.2">
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/>
|
||||
|
||||
|
@@ -38,7 +38,7 @@ author = 'Max Kellermann'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.23.1'
|
||||
version = '0.23.2'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
#release = version + '~git'
|
||||
|
||||
|
@@ -1094,6 +1094,8 @@ Connect to a `PipeWire <https://pipewire.org/>`_ server. Requires
|
||||
* - **remote NAME**
|
||||
- The name of the remote to connect to. The default is
|
||||
``pipewire-0``.
|
||||
* - **dsd yes|no**
|
||||
- Enable DSD playback. This requires PipeWire 0.38.
|
||||
|
||||
.. _pulse_plugin:
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
project(
|
||||
'mpd',
|
||||
['c', 'cpp'],
|
||||
version: '0.23.1',
|
||||
version: '0.23.2',
|
||||
meson_version: '>= 0.56.0',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
@@ -265,7 +265,6 @@ sources = [
|
||||
version_cxx,
|
||||
'src/Main.cxx',
|
||||
'src/protocol/ArgParser.cxx',
|
||||
'src/protocol/Result.cxx',
|
||||
'src/command/CommandError.cxx',
|
||||
'src/command/PositionArg.cxx',
|
||||
'src/command/AllCommands.cxx',
|
||||
|
@@ -150,7 +150,13 @@ public:
|
||||
/**
|
||||
* Write a null-terminated string.
|
||||
*/
|
||||
bool Write(const char *data) noexcept;
|
||||
bool Write(std::string_view s) noexcept {
|
||||
return Write(s.data(), s.size());
|
||||
}
|
||||
|
||||
bool WriteOK() noexcept {
|
||||
return Write("OK\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the uid of the client process, or a negative value
|
||||
|
@@ -20,7 +20,6 @@
|
||||
#include "Client.hxx"
|
||||
#include "Config.hxx"
|
||||
#include "Domain.hxx"
|
||||
#include "protocol/Result.hxx"
|
||||
#include "command/AllCommands.hxx"
|
||||
#include "Log.hxx"
|
||||
#include "util/StringAPI.hxx"
|
||||
@@ -72,7 +71,7 @@ Client::ProcessLine(char *line) noexcept
|
||||
if (idle_waiting) {
|
||||
/* send empty idle response and leave idle mode */
|
||||
idle_waiting = false;
|
||||
command_success(*this);
|
||||
WriteOK();
|
||||
}
|
||||
|
||||
/* do nothing if the client wasn't idling: the client
|
||||
@@ -108,7 +107,7 @@ Client::ProcessLine(char *line) noexcept
|
||||
"list returned {}", id, unsigned(ret));
|
||||
|
||||
if (ret == CommandResult::OK)
|
||||
command_success(*this);
|
||||
WriteOK();
|
||||
|
||||
return ret;
|
||||
} else {
|
||||
@@ -144,7 +143,7 @@ Client::ProcessLine(char *line) noexcept
|
||||
return CommandResult::CLOSE;
|
||||
|
||||
if (ret == CommandResult::OK)
|
||||
command_success(*this);
|
||||
WriteOK();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
@@ -66,7 +66,11 @@ Response::WriteBinary(ConstBuffer<void> payload) noexcept
|
||||
void
|
||||
Response::Error(enum ack code, const char *msg) noexcept
|
||||
{
|
||||
FmtError(code, FMT_STRING("{}"), msg);
|
||||
Fmt(FMT_STRING("ACK [{}@{}] {{{}}} "),
|
||||
(int)code, list_index, command);
|
||||
|
||||
Write(msg);
|
||||
Write("\n");
|
||||
}
|
||||
|
||||
void
|
||||
|
@@ -21,7 +21,6 @@
|
||||
#include "Client.hxx"
|
||||
#include "Response.hxx"
|
||||
#include "command/CommandError.hxx"
|
||||
#include "protocol/Result.hxx"
|
||||
|
||||
ThreadBackgroundCommand::ThreadBackgroundCommand(Client &_client) noexcept
|
||||
:thread(BIND_THIS_METHOD(_Run)),
|
||||
@@ -57,7 +56,7 @@ ThreadBackgroundCommand::DeferredFinish() noexcept
|
||||
PrintError(response, error);
|
||||
} else {
|
||||
SendResponse(response);
|
||||
command_success(client);
|
||||
client.WriteOK();
|
||||
}
|
||||
|
||||
/* delete this object */
|
||||
|
@@ -27,9 +27,3 @@ Client::Write(const void *data, size_t length) noexcept
|
||||
/* if the client is going to be closed, do nothing */
|
||||
return !IsExpired() && FullyBufferedSocket::Write(data, length);
|
||||
}
|
||||
|
||||
bool
|
||||
Client::Write(const char *data) noexcept
|
||||
{
|
||||
return Write(data, strlen(data));
|
||||
}
|
||||
|
@@ -175,6 +175,10 @@ EventLoop::HandleTimers() noexcept
|
||||
void
|
||||
EventLoop::AddDefer(DeferEvent &d) noexcept
|
||||
{
|
||||
#ifdef HAVE_THREADED_EVENT_LOOP
|
||||
assert(!IsAlive() || IsInside());
|
||||
#endif
|
||||
|
||||
defer.push_back(d);
|
||||
again = true;
|
||||
}
|
||||
|
@@ -62,7 +62,7 @@ EventThread::Run() noexcept
|
||||
SetThreadRealtime();
|
||||
} catch (...) {
|
||||
FmtInfo(event_domain,
|
||||
"RTIOThread could not get realtime scheduling, continuing anyway: %s",
|
||||
"RTIOThread could not get realtime scheduling, continuing anyway: {}",
|
||||
std::current_exception());
|
||||
}
|
||||
}
|
||||
|
@@ -71,12 +71,12 @@ input_stream_global_init(const ConfigData &config, EventLoop &event_loop)
|
||||
input_plugins_enabled[i] = true;
|
||||
} catch (const PluginUnconfigured &e) {
|
||||
FmtDebug(input_domain,
|
||||
"Input plugin '{}' is not configured: %s",
|
||||
"Input plugin '{}' is not configured: {}",
|
||||
plugin->name, e.what());
|
||||
continue;
|
||||
} catch (const PluginUnavailable &e) {
|
||||
FmtDebug(input_domain,
|
||||
"Input plugin '{}' is unavailable: %s",
|
||||
"Input plugin '{}' is unavailable: {}",
|
||||
plugin->name, e.what());
|
||||
continue;
|
||||
} catch (...) {
|
||||
|
@@ -42,5 +42,6 @@ LastInputStream::OnCloseTimer() noexcept
|
||||
{
|
||||
assert(is);
|
||||
|
||||
uri.clear();
|
||||
is.reset();
|
||||
}
|
||||
|
@@ -22,7 +22,7 @@
|
||||
|
||||
#include "Lease.hxx"
|
||||
#include "Callback.hxx"
|
||||
#include "event/DeferEvent.hxx"
|
||||
#include "event/InjectEvent.hxx"
|
||||
#include "util/Compiler.h"
|
||||
|
||||
#include <cstddef>
|
||||
@@ -63,7 +63,10 @@ class NfsFileReader : NfsLease, NfsCallback {
|
||||
|
||||
nfsfh *fh;
|
||||
|
||||
DeferEvent defer_open;
|
||||
/**
|
||||
* To inject the Open() call into the I/O thread.
|
||||
*/
|
||||
InjectEvent defer_open;
|
||||
|
||||
public:
|
||||
NfsFileReader() noexcept;
|
||||
@@ -150,7 +153,7 @@ private:
|
||||
void OnNfsCallback(unsigned status, void *data) noexcept final;
|
||||
void OnNfsError(std::exception_ptr &&e) noexcept final;
|
||||
|
||||
/* DeferEvent callback */
|
||||
/* InjectEvent callback */
|
||||
void OnDeferredOpen() noexcept;
|
||||
};
|
||||
|
||||
|
@@ -17,11 +17,18 @@
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "Result.hxx"
|
||||
#include "client/Client.hxx"
|
||||
#include "Error.hxx"
|
||||
|
||||
void
|
||||
command_success(Client &client)
|
||||
#include <spa/utils/result.h>
|
||||
|
||||
namespace PipeWire {
|
||||
|
||||
ErrorCategory error_category;
|
||||
|
||||
std::string
|
||||
ErrorCategory::message(int condition) const
|
||||
{
|
||||
client.Write("OK\n");
|
||||
return spa_strerror(condition);
|
||||
}
|
||||
|
||||
} // namespace Avahi
|
@@ -17,12 +17,29 @@
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifndef MPD_PROTOCOL_RESULT_HXX
|
||||
#define MPD_PROTOCOL_RESULT_HXX
|
||||
#pragma once
|
||||
|
||||
class Client;
|
||||
#include <system_error>
|
||||
|
||||
void
|
||||
command_success(Client &client);
|
||||
struct AvahiClient;
|
||||
|
||||
#endif
|
||||
namespace PipeWire {
|
||||
|
||||
class ErrorCategory final : public std::error_category {
|
||||
public:
|
||||
const char *name() const noexcept override {
|
||||
return "pipewire";
|
||||
}
|
||||
|
||||
std::string message(int condition) const override;
|
||||
};
|
||||
|
||||
extern ErrorCategory error_category;
|
||||
|
||||
inline std::system_error
|
||||
MakeError(int error, const char *msg) noexcept
|
||||
{
|
||||
return std::system_error(error, error_category, msg);
|
||||
}
|
||||
|
||||
} // namespace PipeWire
|
@@ -12,3 +12,17 @@ pipewire_dep = declare_dependency(
|
||||
# disable it at the command line
|
||||
compile_args: ['-Wno-pedantic'],
|
||||
)
|
||||
|
||||
pipewire = static_library(
|
||||
'pipewire',
|
||||
'Error.cxx',
|
||||
include_directories: inc,
|
||||
dependencies: [
|
||||
pipewire_dep,
|
||||
],
|
||||
)
|
||||
|
||||
pipewire_dep = declare_dependency(
|
||||
link_with: pipewire,
|
||||
dependencies: pipewire_dep,
|
||||
)
|
||||
|
@@ -33,12 +33,9 @@ NeighborGlue::~NeighborGlue() noexcept = default;
|
||||
|
||||
static std::unique_ptr<NeighborExplorer>
|
||||
CreateNeighborExplorer(EventLoop &loop, NeighborListener &listener,
|
||||
const char *plugin_name,
|
||||
const ConfigBlock &block)
|
||||
{
|
||||
const char *plugin_name = block.GetBlockValue("plugin");
|
||||
if (plugin_name == nullptr)
|
||||
throw std::runtime_error("Missing \"plugin\" configuration");
|
||||
|
||||
const NeighborPlugin *plugin = GetNeighborPluginByName(plugin_name);
|
||||
if (plugin == nullptr)
|
||||
throw FormatRuntimeError("No such neighbor plugin: %s",
|
||||
@@ -55,8 +52,14 @@ NeighborGlue::Init(const ConfigData &config,
|
||||
block.SetUsed();
|
||||
|
||||
try {
|
||||
explorers.emplace_front(CreateNeighborExplorer(loop,
|
||||
const char *plugin_name = block.GetBlockValue("plugin");
|
||||
if (plugin_name == nullptr)
|
||||
throw std::runtime_error("Missing \"plugin\" configuration");
|
||||
|
||||
explorers.emplace_front(plugin_name,
|
||||
CreateNeighborExplorer(loop,
|
||||
listener,
|
||||
plugin_name,
|
||||
block));
|
||||
} catch (...) {
|
||||
std::throw_with_nested(FormatRuntimeError("Line %i: ",
|
||||
@@ -76,7 +79,9 @@ NeighborGlue::Open()
|
||||
/* roll back */
|
||||
for (auto k = explorers.begin(); k != i; ++k)
|
||||
k->explorer->Close();
|
||||
throw;
|
||||
|
||||
std::throw_with_nested(FormatRuntimeError("Failed to open neighblor plugin '%s'",
|
||||
i->name.c_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <forward_list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
struct ConfigData;
|
||||
class EventLoop;
|
||||
@@ -36,11 +37,13 @@ struct NeighborInfo;
|
||||
*/
|
||||
class NeighborGlue {
|
||||
struct Explorer {
|
||||
const std::string name;
|
||||
std::unique_ptr<NeighborExplorer> explorer;
|
||||
|
||||
template<typename E>
|
||||
Explorer(E &&_explorer) noexcept
|
||||
:explorer(std::forward<E>(_explorer)) {}
|
||||
template<typename N, typename E>
|
||||
Explorer(N &&_name, E &&_explorer) noexcept
|
||||
:name(std::forward<N>(_name)),
|
||||
explorer(std::forward<E>(_explorer)) {}
|
||||
|
||||
Explorer(const Explorer &) = delete;
|
||||
};
|
||||
|
@@ -279,7 +279,7 @@ AudioOutputControl::PlayChunk(std::unique_lock<Mutex> &lock) noexcept
|
||||
return false;
|
||||
} catch (...) {
|
||||
FmtError(output_domain,
|
||||
"Failed to play on {}",
|
||||
"Failed to play on {}: {}",
|
||||
GetLogName(), std::current_exception());
|
||||
InternalCloseError(std::current_exception());
|
||||
return false;
|
||||
@@ -435,7 +435,7 @@ AudioOutputControl::Task() noexcept
|
||||
SetThreadRealtime();
|
||||
} catch (...) {
|
||||
FmtInfo(output_domain,
|
||||
"OutputThread could not get realtime scheduling, continuing anyway: %s",
|
||||
"OutputThread could not get realtime scheduling, continuing anyway: {}",
|
||||
std::current_exception());
|
||||
}
|
||||
|
||||
|
@@ -195,7 +195,7 @@ FifoOutput::Cancel() noexcept
|
||||
|
||||
if (bytes < 0 && errno != EAGAIN) {
|
||||
FmtError(fifo_output_domain,
|
||||
"Flush of FIFO \"{}\" failed: %s",
|
||||
"Flush of FIFO \"{}\" failed: {}",
|
||||
path_utf8, strerror(errno));
|
||||
}
|
||||
}
|
||||
|
@@ -18,16 +18,21 @@
|
||||
*/
|
||||
|
||||
#include "PipeWireOutputPlugin.hxx"
|
||||
#include "lib/pipewire/Error.hxx"
|
||||
#include "lib/pipewire/ThreadLoop.hxx"
|
||||
#include "../OutputAPI.hxx"
|
||||
#include "../Error.hxx"
|
||||
#include "mixer/plugins/PipeWireMixerPlugin.hxx"
|
||||
#include "pcm/Silence.hxx"
|
||||
#include "system/Error.hxx"
|
||||
#include "util/BitReverse.hxx"
|
||||
#include "util/Domain.hxx"
|
||||
#include "util/ScopeExit.hxx"
|
||||
#include "util/StringCompare.hxx"
|
||||
#include "util/WritableBuffer.hxx"
|
||||
#include "Log.hxx"
|
||||
#include "tag/Format.hxx"
|
||||
#include "config.h" // for ENABLE_DSD
|
||||
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic push
|
||||
@@ -49,7 +54,9 @@
|
||||
|
||||
#include <boost/lockfree/spsc_queue.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
static constexpr Domain pipewire_output_domain("pipewire_output");
|
||||
|
||||
@@ -62,7 +69,9 @@ class PipeWireOutput final : AudioOutput {
|
||||
struct pw_thread_loop *thread_loop = nullptr;
|
||||
struct pw_stream *stream;
|
||||
|
||||
std::byte buffer[1024];
|
||||
std::string error_message;
|
||||
|
||||
std::byte pod_buffer[1024];
|
||||
struct spa_pod_builder pod_builder;
|
||||
|
||||
std::size_t frame_size;
|
||||
@@ -78,13 +87,38 @@ class PipeWireOutput final : AudioOutput {
|
||||
float volume = 1.0;
|
||||
|
||||
PipeWireMixer *mixer = nullptr;
|
||||
int channels;
|
||||
unsigned channels;
|
||||
|
||||
/**
|
||||
* The active sample format, needed for PcmSilence().
|
||||
*/
|
||||
SampleFormat sample_format;
|
||||
|
||||
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
|
||||
/**
|
||||
* Is the "dsd" setting enabled, i.e. is DSD playback allowed?
|
||||
*/
|
||||
const bool enable_dsd;
|
||||
|
||||
/**
|
||||
* Are we currently playing in native DSD mode?
|
||||
*/
|
||||
bool use_dsd;
|
||||
|
||||
/**
|
||||
* Reverse the 8 bits in each DSD byte? This is necessary if
|
||||
* PipeWire wants LSB (because MPD uses MSB internally).
|
||||
*/
|
||||
bool dsd_reverse_bits;
|
||||
|
||||
/**
|
||||
* Pack this many bytes of each frame together. MPD uses 1
|
||||
* internally, and if PipeWire wants more than one
|
||||
* (e.g. because it uses DSD_U32), we need to reorder bytes.
|
||||
*/
|
||||
uint_least8_t dsd_interleave;
|
||||
#endif
|
||||
|
||||
bool disconnected;
|
||||
|
||||
/**
|
||||
@@ -146,8 +180,12 @@ public:
|
||||
|
||||
private:
|
||||
void CheckThrowError() {
|
||||
if (disconnected)
|
||||
throw std::runtime_error("Disconnected from PipeWire");
|
||||
if (disconnected) {
|
||||
if (error_message.empty())
|
||||
throw std::runtime_error("Disconnected from PipeWire");
|
||||
else
|
||||
throw std::runtime_error(error_message);
|
||||
}
|
||||
}
|
||||
|
||||
void StateChanged(enum pw_stream_state state,
|
||||
@@ -200,12 +238,12 @@ private:
|
||||
o.ControlInfo(control);
|
||||
}
|
||||
|
||||
void ParamChanged() noexcept {
|
||||
if (restore_volume) {
|
||||
SetVolume(volume);
|
||||
restore_volume = false;
|
||||
}
|
||||
}
|
||||
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
|
||||
void DsdFormatChanged(const struct spa_audio_info_dsd &dsd) noexcept;
|
||||
void DsdFormatChanged(const struct spa_pod ¶m) noexcept;
|
||||
#endif
|
||||
|
||||
void ParamChanged(uint32_t id, const struct spa_pod *param) noexcept;
|
||||
|
||||
static void ParamChanged(void *data,
|
||||
uint32_t id,
|
||||
@@ -215,7 +253,7 @@ private:
|
||||
return;
|
||||
|
||||
auto &o = *(PipeWireOutput *)data;
|
||||
o.ParamChanged();
|
||||
o.ParamChanged(id, param);
|
||||
}
|
||||
|
||||
/* virtual methods from class AudioOutput */
|
||||
@@ -240,6 +278,8 @@ private:
|
||||
void Drain() override;
|
||||
void Cancel() noexcept override;
|
||||
bool Pause() noexcept override;
|
||||
|
||||
void SendTag(const Tag &tag) override;
|
||||
};
|
||||
|
||||
static constexpr auto stream_events = PipeWireOutput::MakeStreamEvents();
|
||||
@@ -250,6 +290,9 @@ PipeWireOutput::PipeWireOutput(const ConfigBlock &block)
|
||||
name(block.GetBlockValue("name", "pipewire")),
|
||||
remote(block.GetBlockValue("remote", nullptr)),
|
||||
target(block.GetBlockValue("target", nullptr))
|
||||
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
|
||||
, enable_dsd(block.GetBlockValue("dsd", false))
|
||||
#endif
|
||||
{
|
||||
if (target != nullptr) {
|
||||
if (StringIsEmpty(target))
|
||||
@@ -272,12 +315,9 @@ PipeWireOutput::SetVolume(float _volume)
|
||||
float newvol = _volume*_volume*_volume;
|
||||
|
||||
if (stream != nullptr && !restore_volume) {
|
||||
float vol[SPA_AUDIO_MAX_CHANNELS];
|
||||
int i;
|
||||
float vol[MAX_CHANNELS];
|
||||
std::fill_n(vol, channels, newvol);
|
||||
|
||||
for (i = 0; i < channels; i++) {
|
||||
vol[i] = newvol;
|
||||
}
|
||||
if (pw_stream_set_control(stream,
|
||||
SPA_PROP_channelVolumes, channels, vol,
|
||||
0) != 0)
|
||||
@@ -292,7 +332,7 @@ PipeWireOutput::Enable()
|
||||
{
|
||||
thread_loop = pw_thread_loop_new(name, nullptr);
|
||||
if (thread_loop == nullptr)
|
||||
throw std::runtime_error("pw_thread_loop_new() failed");
|
||||
throw MakeErrno("pw_thread_loop_new() failed");
|
||||
|
||||
pw_thread_loop_start(thread_loop);
|
||||
}
|
||||
@@ -421,6 +461,7 @@ ToPipeWireAudioFormat(AudioFormat &audio_format) noexcept
|
||||
void
|
||||
PipeWireOutput::Open(AudioFormat &audio_format)
|
||||
{
|
||||
error_message.clear();
|
||||
disconnected = false;
|
||||
restore_volume = true;
|
||||
|
||||
@@ -462,10 +503,27 @@ PipeWireOutput::Open(AudioFormat &audio_format)
|
||||
&stream_events,
|
||||
this);
|
||||
if (stream == nullptr)
|
||||
throw std::runtime_error("pw_stream_new_simple() failed");
|
||||
throw MakeErrno("pw_stream_new_simple() failed");
|
||||
|
||||
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
|
||||
/* this needs to be determined before ToPipeWireAudioFormat()
|
||||
switches DSD to S16 */
|
||||
use_dsd = enable_dsd &&
|
||||
audio_format.format == SampleFormat::DSD;
|
||||
dsd_reverse_bits = false;
|
||||
dsd_interleave = 0;
|
||||
#endif
|
||||
|
||||
auto raw = ToPipeWireAudioFormat(audio_format);
|
||||
|
||||
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
|
||||
if (use_dsd)
|
||||
/* restore the DSD format which was overwritten by
|
||||
ToPipeWireAudioFormat(), because DSD is a special
|
||||
case in PipeWire */
|
||||
audio_format.format = SampleFormat::DSD;
|
||||
#endif
|
||||
|
||||
frame_size = audio_format.GetFrameSize();
|
||||
sample_format = audio_format.format;
|
||||
channels = audio_format.channels;
|
||||
@@ -479,19 +537,42 @@ PipeWireOutput::Open(AudioFormat &audio_format)
|
||||
const struct spa_pod *params[1];
|
||||
|
||||
pod_builder = {};
|
||||
pod_builder.data = buffer;
|
||||
pod_builder.size = sizeof(buffer);
|
||||
params[0] = spa_format_audio_raw_build(&pod_builder,
|
||||
SPA_PARAM_EnumFormat, &raw);
|
||||
pod_builder.data = pod_buffer;
|
||||
pod_builder.size = sizeof(pod_buffer);
|
||||
|
||||
pw_stream_connect(stream,
|
||||
PW_DIRECTION_OUTPUT,
|
||||
target_id,
|
||||
(enum pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT |
|
||||
PW_STREAM_FLAG_INACTIVE |
|
||||
PW_STREAM_FLAG_MAP_BUFFERS |
|
||||
PW_STREAM_FLAG_RT_PROCESS),
|
||||
params, 1);
|
||||
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
|
||||
struct spa_audio_info_dsd dsd;
|
||||
if (use_dsd) {
|
||||
dsd = {};
|
||||
|
||||
/* copy all relevant settings from the
|
||||
ToPipeWireAudioFormat() return value */
|
||||
dsd.flags = raw.flags;
|
||||
dsd.rate = raw.rate;
|
||||
dsd.channels = raw.channels;
|
||||
if ((dsd.flags & SPA_AUDIO_FLAG_UNPOSITIONED) == 0)
|
||||
std::copy_n(raw.position, dsd.channels, dsd.position);
|
||||
|
||||
params[0] = spa_format_audio_dsd_build(&pod_builder,
|
||||
SPA_PARAM_EnumFormat,
|
||||
&dsd);
|
||||
} else
|
||||
#endif
|
||||
params[0] = spa_format_audio_raw_build(&pod_builder,
|
||||
SPA_PARAM_EnumFormat,
|
||||
&raw);
|
||||
|
||||
int error =
|
||||
pw_stream_connect(stream,
|
||||
PW_DIRECTION_OUTPUT,
|
||||
target_id,
|
||||
(enum pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT |
|
||||
PW_STREAM_FLAG_INACTIVE |
|
||||
PW_STREAM_FLAG_MAP_BUFFERS |
|
||||
PW_STREAM_FLAG_RT_PROCESS),
|
||||
params, 1);
|
||||
if (error < 0)
|
||||
throw PipeWire::MakeError(error, "Failed to connect stream");
|
||||
}
|
||||
|
||||
void
|
||||
@@ -513,11 +594,119 @@ PipeWireOutput::StateChanged(enum pw_stream_state state,
|
||||
const bool was_disconnected = disconnected;
|
||||
disconnected = state == PW_STREAM_STATE_ERROR ||
|
||||
state == PW_STREAM_STATE_UNCONNECTED;
|
||||
if (!was_disconnected && disconnected)
|
||||
if (!was_disconnected && disconnected) {
|
||||
if (error != nullptr)
|
||||
error_message = error;
|
||||
|
||||
pw_thread_loop_signal(thread_loop, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
|
||||
|
||||
inline void
|
||||
PipeWireOutput::DsdFormatChanged(const struct spa_audio_info_dsd &dsd) noexcept
|
||||
{
|
||||
/* MPD uses MSB internally, which means if PipeWire asks LSB
|
||||
from us, we need to reverse the bits in each DSD byte */
|
||||
dsd_reverse_bits = dsd.bitorder == SPA_PARAM_BITORDER_lsb;
|
||||
|
||||
dsd_interleave = dsd.interleave;
|
||||
}
|
||||
|
||||
inline void
|
||||
PipeWireOutput::DsdFormatChanged(const struct spa_pod ¶m) noexcept
|
||||
{
|
||||
uint32_t media_type, media_subtype;
|
||||
struct spa_audio_info_dsd dsd;
|
||||
|
||||
if (spa_format_parse(¶m, &media_type, &media_subtype) >= 0 &&
|
||||
media_type == SPA_MEDIA_TYPE_audio &&
|
||||
media_subtype == SPA_MEDIA_SUBTYPE_dsd &&
|
||||
spa_format_audio_dsd_parse(¶m, &dsd) >= 0)
|
||||
DsdFormatChanged(dsd);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
inline void
|
||||
PipeWireOutput::ParamChanged([[maybe_unused]] uint32_t id,
|
||||
[[maybe_unused]] const struct spa_pod *param) noexcept
|
||||
{
|
||||
if (restore_volume) {
|
||||
SetVolume(volume);
|
||||
restore_volume = false;
|
||||
}
|
||||
|
||||
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
|
||||
if (use_dsd && id == SPA_PARAM_Format && param != nullptr)
|
||||
DsdFormatChanged(*param);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
|
||||
|
||||
static void
|
||||
Interleave(std::byte *data, std::byte *end,
|
||||
std::size_t channels, std::size_t interleave) noexcept
|
||||
{
|
||||
assert(channels > 1);
|
||||
assert(channels <= MAX_CHANNELS);
|
||||
|
||||
constexpr std::size_t MAX_INTERLEAVE = 8;
|
||||
assert(interleave > 1);
|
||||
assert(interleave <= MAX_INTERLEAVE);
|
||||
|
||||
std::array<std::byte, MAX_CHANNELS * MAX_INTERLEAVE> buffer;
|
||||
std::size_t buffer_size = channels * interleave;
|
||||
|
||||
while (data < end) {
|
||||
std::copy_n(data, buffer_size, buffer.data());
|
||||
|
||||
const std::byte *src0 = buffer.data();
|
||||
for (std::size_t channel = 0; channel < channels;
|
||||
++channel, ++src0) {
|
||||
const std::byte *src = src0;
|
||||
for (std::size_t i = 0; i < interleave;
|
||||
++i, src += channels)
|
||||
*data++ = *src;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
BitReverse(uint8_t *data, std::size_t n) noexcept
|
||||
{
|
||||
while (n-- > 0)
|
||||
*data = bit_reverse(*data);
|
||||
}
|
||||
|
||||
static void
|
||||
BitReverse(std::byte *data, std::size_t n) noexcept
|
||||
{
|
||||
BitReverse((uint8_t *)data, n);
|
||||
}
|
||||
|
||||
static void
|
||||
PostProcessDsd(std::byte *data, struct spa_chunk &chunk, unsigned channels,
|
||||
bool reverse_bits, unsigned interleave) noexcept
|
||||
{
|
||||
assert(chunk.size % channels == 0);
|
||||
|
||||
if (interleave > 1 && channels > 1) {
|
||||
assert(chunk.size % (channels * interleave) == 0);
|
||||
|
||||
Interleave(data, data + chunk.size, channels, interleave);
|
||||
chunk.stride *= interleave;
|
||||
}
|
||||
|
||||
if (reverse_bits)
|
||||
BitReverse(data, chunk.size);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
inline void
|
||||
PipeWireOutput::Process() noexcept
|
||||
{
|
||||
@@ -527,15 +716,32 @@ PipeWireOutput::Process() noexcept
|
||||
return;
|
||||
}
|
||||
|
||||
auto *buf = b->buffer;
|
||||
std::byte *dest = (std::byte *)buf->datas[0].data;
|
||||
auto &buffer = *b->buffer;
|
||||
auto &d = buffer.datas[0];
|
||||
|
||||
std::byte *dest = (std::byte *)d.data;
|
||||
if (dest == nullptr)
|
||||
return;
|
||||
|
||||
const std::size_t max_frames = buf->datas[0].maxsize / frame_size;
|
||||
const std::size_t max_size = max_frames * frame_size;
|
||||
std::size_t max_frames = d.maxsize / frame_size;
|
||||
|
||||
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
|
||||
if (use_dsd && dsd_interleave > 1) {
|
||||
/* make sure we don't get partial interleave frames */
|
||||
std::size_t interleave_size = frame_size * dsd_interleave;
|
||||
std::size_t available_bytes = ring_buffer->read_available();
|
||||
std::size_t available_interleaves =
|
||||
available_bytes / interleave_size;
|
||||
std::size_t available_frames =
|
||||
available_interleaves * dsd_interleave;
|
||||
if (max_frames > available_frames)
|
||||
max_frames = available_frames;
|
||||
}
|
||||
#endif
|
||||
|
||||
const std::size_t max_size = max_frames * frame_size;
|
||||
size_t nbytes = ring_buffer->pop(dest, max_size);
|
||||
assert(nbytes % frame_size == 0);
|
||||
if (nbytes == 0) {
|
||||
if (drain_requested) {
|
||||
pw_stream_flush(stream, true);
|
||||
@@ -549,9 +755,16 @@ PipeWireOutput::Process() noexcept
|
||||
LogWarning(pipewire_output_domain, "Decoder is too slow; playing silence to avoid xrun");
|
||||
}
|
||||
|
||||
buf->datas[0].chunk->offset = 0;
|
||||
buf->datas[0].chunk->stride = frame_size;
|
||||
buf->datas[0].chunk->size = nbytes;
|
||||
auto &chunk = *d.chunk;
|
||||
chunk.offset = 0;
|
||||
chunk.stride = frame_size;
|
||||
chunk.size = nbytes;
|
||||
|
||||
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
|
||||
if (use_dsd)
|
||||
PostProcessDsd(dest, chunk, channels,
|
||||
dsd_reverse_bits, dsd_interleave);
|
||||
#endif
|
||||
|
||||
pw_stream_queue_buffer(stream, b);
|
||||
|
||||
@@ -652,6 +865,39 @@ PipeWireOutput::SetMixer(PipeWireMixer &_mixer) noexcept
|
||||
// TODO: Check if context and stream is ready and trigger a volume update...
|
||||
}
|
||||
|
||||
void
|
||||
PipeWireOutput::SendTag(const Tag &tag)
|
||||
{
|
||||
CheckThrowError();
|
||||
|
||||
struct spa_dict_item items[3];
|
||||
uint32_t n_items=0;
|
||||
|
||||
const char *artist, *title;
|
||||
|
||||
char *medianame = FormatTag(tag, "%artist% - %title%");
|
||||
AtScopeExit(medianame) { free(medianame); };
|
||||
|
||||
items[n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_NAME, medianame);
|
||||
|
||||
artist = tag.GetValue(TAG_ARTIST);
|
||||
title = tag.GetValue(TAG_TITLE);
|
||||
|
||||
if (artist != nullptr) {
|
||||
items[n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_ARTIST, artist);
|
||||
}
|
||||
|
||||
if (title != nullptr) {
|
||||
items[n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_TITLE, title);
|
||||
}
|
||||
|
||||
struct spa_dict dict = SPA_DICT_INIT(items, n_items);
|
||||
|
||||
auto rc = pw_stream_update_properties(stream, &dict);
|
||||
if (rc < 0)
|
||||
LogWarning(pipewire_output_domain, "Error updating properties");
|
||||
}
|
||||
|
||||
void
|
||||
pipewire_output_set_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept
|
||||
{
|
||||
|
@@ -34,8 +34,8 @@ inline bool
|
||||
CrossFadeSettings::CanCrossFadeSong(SignedSongTime total_time) const noexcept
|
||||
{
|
||||
return !total_time.IsNegative() &&
|
||||
duration >= MIN_TOTAL_TIME &&
|
||||
duration >= std::chrono::duration_cast<FloatDuration>(total_time);
|
||||
total_time >= MIN_TOTAL_TIME &&
|
||||
duration < std::chrono::duration_cast<FloatDuration>(total_time);
|
||||
}
|
||||
|
||||
gcc_pure
|
||||
|
Reference in New Issue
Block a user