/* PipeWire */
/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */
/* SPDX-License-Identifier: MIT */

#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include "config.h"

#include <spa/utils/result.h>

#include <pipewire/impl.h>

#include "module-protocol-pulse/pulse-server.h"

/** \page page_module_protocol_pulse Protocol Pulse
 *
 * This module implements a complete PulseAudio server on top of
 * PipeWire.  This is only the server implementation, client are expected
 * to use the original PulseAudio client library. This provides a
 * high level of compatibility with existing applications; in fact,
 * all usual PulseAudio tools such as pavucontrol, pactl, pamon, paplay
 * should continue to work as they did before.
 *
 * This module is usually loaded as part of a standalone pipewire process,
 * called pipewire-pulse, with the pipewire-pulse.conf config file.
 *
 * The pulse server implements a sample cache that is otherwise not
 * available in PipeWire.
 *
 * ## Module Name
 *
 * `libpipewire-module-protocol-pulse`
 *
 * ## Module Options
 *
 * The module arguments can be the contents of the pulse.properties but
 * it is recommended to make a separate pulse.properties section in the
 * config file so that overrides can be done.
 *
 * ## pulse.properties
 *
 * A config section with server properties can be given.
 *
 *\code{.unparsed}
 * # ~/.config/pipewire/pipewire-pulse.conf.d/custom.conf
 *
 * pulse.properties = {
 *     # the addresses this server listens on
 *     server.address = [
 *         "unix:native"
 *         #"unix:/tmp/something"              # absolute paths may be used
 *         #"tcp:4713"                         # IPv4 and IPv6 on all addresses
 *         #"tcp:[::]:9999"                    # IPv6 on all addresses
 *         #"tcp:127.0.0.1:8888"               # IPv4 on a single address
 *         #
 *         #{ address = "tcp:4713"             # address
 *         #  max-clients = 64                 # maximum number of clients
 *         #  listen-backlog = 32              # backlog in the server listen queue
 *         #  client.access = "restricted"     # permissions for clients
 *         #}
 *     ]
 *     #server.dbus-name       = "org.pulseaudio.Server"
 *     #pulse.allow-module-loading = true
 *     #pulse.min.req          = 128/48000     # 2.7ms
 *     #pulse.default.req      = 960/48000     # 20 milliseconds
 *     #pulse.min.frag         = 128/48000     # 2.7ms
 *     #pulse.default.frag     = 96000/48000   # 2 seconds
 *     #pulse.default.tlength  = 96000/48000   # 2 seconds
 *     #pulse.min.quantum      = 128/48000     # 2.7ms
 *     #pulse.default.format   = F32
 *     #pulse.default.position = [ FL FR ]
 *     #pulse.idle.timeout     = 0
 * }
 *
 * pulse.properties.rules = [
 *     {   matches = [ { cpu.vm.name = !null } ]
 *         actions = {
 *             update-props = {
 *                 # These overrides are only applied when running in a vm.
 *                 pulse.min.quantum = 1024/48000      # 22ms
 *             }
 *         }
 *     }
 * ]
 *\endcode
 *
 * ### Connection options
 *
 *\code{.unparsed}
 *     ...
 *     server.address = [
 *         "unix:native"
 *         # "tcp:4713"
 *     ]
 *     ...
 *\endcode
 *
 * The addresses the server listens on when starting. Uncomment the `tcp:4713` entry to also
 * make the server listen on a tcp socket. This is equivalent to loading `libpipewire-module-native-protocol-tcp`.
 *
 * There is also a slightly more verbose syntax with more options:
 *
 *\code{.unparsed}
 *     ....
 *     server.address = [
 *       {  address = "tcp:4713"             # address
 *          max-clients = 64                 # maximum number of clients
 *          listen-backlog = 32              # backlog in the server listen queue
 *          client.access = "restricted"     # permissions for clients
 *       }
 *     ....
 *\endcode
 *
 * Use `client.access` to use one of the access methods to restrict the permissions given to
 * clients connected via this address.
 *
 * By default network access is given the "restricted" permissions. The session manager is responsible
 * for assigning permission to clients with restricted permissions (usually read-only permissions).
 *
 *\code{.unparsed}
 *     server.dbus-name       = "org.pulseaudio.Server"
 *\endcode
 *
 * The DBus name to reserve for the server. If you have multiple servers, you might want
 * to change the name.
 *
 *\code{.unparsed}
 *     pulse.allow-module-loading = true
 *\endcode
 *
 * By default, clients are allowed to load and unload modules. You can disable this
 * feature with this option.
 *
 * ### Playback buffering options
 *
 *\code{.unparsed}
 *     pulse.min.req = 128/48000              # 2.7ms
 *\endcode
 *
 * The minimum amount of data to request for clients. The client requested
 * values will be clamped to this value. Lowering this value together with
 * tlength can decrease latency if the client wants this, but increase CPU overhead.
 *
 *\code{.unparsed}
 *     pulse.default.req = 960/48000          # 20 milliseconds
 *\endcode
 *
 * The default amount of data to request for clients. If the client does not
 * specify any particular value, this default will be used. Lowering this value
 * together with tlength can decrease latency but increase CPU overhead.
 *
 *\code{.unparsed}
 *     pulse.default.tlength = 96000/48000    # 2 seconds
 *\endcode
 *
 * The target amount of data to buffer on the server side. If the client did not
 * specify a value, this default will be used. Lower values can decrease the
 * latency.
 *
 * ### Record buffering options
 *
 *\code{.unparsed}
 *     pulse.min.frag = 128/48000             # 2.7ms
 *\endcode
 *
 * The minimum allowed size of the capture buffer before it is sent to a client.
 * The requested value of the client will be clamped to this. Lowering this value
 * can reduce latency at the expense of more CPU usage.
 *
 *\code{.unparsed}
 *     pulse.default.frag = 96000/48000       # 2 seconds
 *\endcode
 *
 * The default size of the capture buffer before it is sent to a client. If the client
 * did not specify any value, this default will be used. Lowering this value can
 * reduce latency at the expense of more CPU usage.
 *
 * ### Scheduling options
 *
 *\code{.unparsed}
 *     pulse.min.quantum = 128/48000          # 2.7ms
 *\endcode
 *
 * The minimum quantum (buffer size in samples) to use for pulseaudio clients.
 * This value is calculated based on the frag and req/tlength for record and
 * playback streams respectively and then clamped to this value to ensure no
 * pulseaudio client asks for too small quantums. Lowering this value might
 * decrease latency at the expense of more CPU usage.
 *
 * ### Format options
 *
 *\code{.unparsed}
 *     pulse.default.format = F32
 *\endcode
 *
 * Some modules will default to this format when no other format was given. This
 * is equivalent to the PulseAudio `default-sample-format` option in
 * `/etc/pulse/daemon.conf`.
 *
 *\code{.unparsed}
 *     pulse.default.position = [ FL FR ]
 *\endcode
 *
 * Some modules will default to this channelmap (with its number of channels).
 * This is equivalent to the PulseAudio `default-sample-channels` and
 * `default-channel-map` options in `/etc/pulse/daemon.conf`.
 *
 * ### Quirk options
 *
 *\code{.unparsed}
 *     pulse.fix.format = "S16LE"
 *\endcode
 *
 * When a stream uses the FIX_FORMAT flag, fixate the format to this value.
 * Normally the format would be fixed to the sink/source that the stream connects
 * to. When an invalid format (null or "") is set, the FIX_FORMAT flag is ignored.
 *
 *\code{.unparsed}
 *     pulse.fix.rate = 48000
 *\endcode
 *
 * When a stream uses the FIX_RATE flag, fixate the sample rate to this value.
 * Normally the rate would be fixed to the sink/source that the stream connects
 * to. When a 0 rate is set, the FIX_RATE flag is ignored.
 *
 *\code{.unparsed}
 *     pulse.fix.position = "[ FL FR ]"
 *\endcode
 *
 * When a stream uses the FIX_CHANNELS flag, fixate the channels to this value.
 * Normally the channels would be fixed to the sink/source that the stream connects
 * to. When an invalid position (null or "") is set, the FIX_CHANNELS flag is ignored.
 *
 *\code{.unparsed}
 *     pulse.idle.timeout = 0
 *\endcode
 *
 * Some clients are not sending data when they should and cause underruns. When
 * setting this option such clients will be set to paused if they underrun
 * for the given amount of seconds. This makes sure that sinks can suspend and
 * save battery power. When the client resumes, it will unpause again.
 * A value of 0 disables this feature.
 *
 * ## Command execution
 *
 * As part of the server startup sequence, a set of commands can be executed.
 * Currently, this can be used to load additional modules into the server.
 *
 *\code{.unparsed}
 * # Extra commands can be executed here.
 * #   load-module : loads a module with args and flags
 * #      args = "<module-name> <module-args>"
 * #      ( flags = [ "no-fail" ] )
 * #      ( condition = [ { <key1> = <value1>, ... } ... ] )
 * # conditions will check the pulse.properties key/values.
 * pulse.cmd = [
 *     { cmd = "load-module" args = "module-always-sink" flags = [ ] }
 *     #{ cmd = "load-module" args = "module-switch-on-connect" condition = [ { pulse.cmd.switch-on-connect = true } ]
 *     #{ cmd = "load-module" args = "module-gsettings" flags = [ "nofail" ] }
 * ]
 *\endcode

 * ## Dynamic properties
 *
 * The pulse.properties can be dynamically updated with rules. It supports
 * an `update-props` action. The matches will be performed on the values in
 * context.properties.
 *
 *\code{.unparsed}
 * pulse.properties.rules = [
 *     {   matches = [ { cpu.vm.name = !null } ]
 *         actions = {
 *             update-props = {
 *                 # These overrides are only applied when running in a vm.
 *                 pulse.min.quantum = 1024/48000      # 22ms
 *             }
 *         }
 *     }
 * ]
 *\endcode
 *
 * In the above example, when running in a VM, the rule will override the properties
 * in pulse.properties with the given values. This might be interesting because
 * VMs usually can't support the low latency settings that are possible on real
 * hardware.
 *
 * ## Stream settings and rules
 *
 * Streams created by module-protocol-pulse will use the stream.properties
 * section and stream.rules sections as usual.
 *
 * ## Application settings (Rules)
 *
 * The pulse protocol module supports generic config rules. It supports a pulse.rules
 * section with a `quirks` and an `update-props` action. These rules operate on the client
 * properties (not the stream properties, see above).
 *
 *\code{.unparsed}
 * # ~/.config/pipewire/pipewire-pulse.conf.d/custom.conf
 *
 * pulse.rules = [
 *     {
 *         # skype does not want to use devices that don't have an S16 sample format.
 *         matches = [
 *              { application.process.binary = "teams" }
 *              { application.process.binary = "teams-insiders" }
 *              { application.process.binary = "skypeforlinux" }
 *         ]
 *         actions = { quirks = [ force-s16-info ] }
 *     }
 *     {
 *         # speech dispatcher asks for too small latency and then underruns.
 *         matches = [ { application.name = "~speech-dispatcher*" } ]
 *         actions = {
 *             update-props = {
 *                 pulse.min.req          = 1024/48000     # 21ms
 *                 pulse.min.quantum      = 1024/48000     # 21ms
 *                 pulse.idle.timeout     = 5              # pause after 5 seconds of underrun
 *             }
 *         }
 *     }
 * ]
 *\endcode
 *
 * ### Quirks
 *
 * The quirks action takes an array of quirks to apply for the client.
 *
 * * `force-s16-info` makes the sink and source introspect code pretend that the sample format
 *                    is S16 (16 bits) samples.  Some application refuse the sink/source if this
 *                    is not the case.
 * * `remove-capture-dont-move` Removes the DONT_MOVE flag on capture streams. Some applications
 *                    set this flag so that the stream can't be moved anymore with tools such as
 *                    pavucontrol.
 * * `block-source-volume` blocks the client from updating any source volumes. This can be used
 *                    to disable things like automatic gain control.
 * * `block-sink-volume` blocks the client from updating any sink volumes.
 * * `block-record-stream` blocks the client from creating any record stream.
 * * `block-playback-stream` blocks the client from creating any playback stream.
 *
 * ### update-props
 *
 * Takes an object with the properties to update on the client. Common actions are to
 * tweak the quantum values. You can use the stream specific keys in pulse.properties.
 *
 * ### startup notification
 *
 * A newline will be written into the notification file descriptor when the server has
 * started if the following environment variable is set:
 *
 * - PIPEWIRE_PULSE_NOTIFICATION_FD
 *
 * ## Example configuration
 *
 *\code{.unparsed}
 * # ~/.config/pipewire/pipewire-pulse.conf.d/custom.conf
 *
 * context.modules = [
 * {   name = libpipewire-module-protocol-pulse
 *     args = { }
 * }
 * ]
 *
 * pulse.properties = {
 *     server.address = [ "unix:native" ]
 * }
 *
 * pulse.rules = [
 *     {
 *        # skype does not want to use devices that don't have an S16 sample format.
 *        matches = [
 *             { application.process.binary = "teams" }
 *             { application.process.binary = "teams-insiders" }
 *             { application.process.binary = "skypeforlinux" }
 *        ]
 *        actions = { quirks = [ force-s16-info ] }
 *    }
 *    {
 *        # speech dispatcher asks for too small latency and then underruns.
 *        matches = [ { application.name = "~speech-dispatcher*" } ]
 *        actions = {
 *            update-props = {
 *                pulse.min.req          = 1024/48000     # 21ms
 *                pulse.min.quantum      = 1024/48000     # 21ms
 *                pulse.idle.timeout     = 5              # pause after 5 seconds of underrun
 *            }
 *        }
 *    }
 * ]
 *\endcode
 */

#define NAME "protocol-pulse"

PW_LOG_TOPIC(mod_topic, "mod." NAME);
#define PW_LOG_TOPIC_DEFAULT mod_topic
PW_LOG_TOPIC(pulse_conn, "conn." NAME);
PW_LOG_TOPIC(pulse_ext_dev_restore, "mod." NAME ".device-restore");
PW_LOG_TOPIC(pulse_ext_stream_restore, "mod." NAME ".stream-restore");

#define MODULE_USAGE	PW_PROTOCOL_PULSE_USAGE

static const struct spa_dict_item module_props[] = {
	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
	{ PW_KEY_MODULE_DESCRIPTION, "Implement a PulseAudio server" },
	{ PW_KEY_MODULE_USAGE, MODULE_USAGE },
	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
};

struct impl {
	struct pw_context *context;

	struct spa_hook module_listener;

	struct pw_protocol_pulse *pulse;
};

static void impl_free(struct impl *impl)
{
	spa_hook_remove(&impl->module_listener);
	if (impl->pulse)
		pw_protocol_pulse_destroy(impl->pulse);
	free(impl);
}

static void module_destroy(void *data)
{
	struct impl *impl = data;
	pw_log_debug("module %p: destroy", impl);
	impl_free(impl);
}

static const struct pw_impl_module_events module_events = {
	PW_VERSION_IMPL_MODULE_EVENTS,
	.destroy = module_destroy,
};

SPA_EXPORT
int pipewire__module_init(struct pw_impl_module *module, const char *args)
{
	struct pw_context *context = pw_impl_module_get_context(module);
	struct pw_properties *props;
	struct impl *impl;
	int res;

	PW_LOG_TOPIC_INIT(mod_topic);
	PW_LOG_TOPIC_INIT(pulse_conn);
	/* it's easier to init these here than adding an init() call to the
	 * extensions */
	PW_LOG_TOPIC_INIT(pulse_ext_dev_restore);
	PW_LOG_TOPIC_INIT(pulse_ext_stream_restore);

	impl = calloc(1, sizeof(struct impl));
	if (impl == NULL)
		return -errno;

	pw_log_debug("module %p: new %s", impl, args);

	if (args)
		props = pw_properties_new_string(args);
	else
		props = NULL;

	impl->pulse = pw_protocol_pulse_new(context, props, 0);
	if (impl->pulse == NULL) {
		res = -errno;
		free(impl);
		return res;
	}

	pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);

	pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));

	return 0;
}
