mirror of
https://github.com/alsa-project/alsa-lib.git
synced 2025-10-29 05:40:25 -04:00
Moved some prototypes from src/pcm/pcm_plugin.h to include/pcm_plugin.h.
Merged src/pcm/atomic.h to include/iatomic.h. Added initial description of hw and hooks plugins.
This commit is contained in:
parent
b36ad628f7
commit
8c1887d7af
18 changed files with 442 additions and 159 deletions
|
|
@ -25,7 +25,27 @@ INPUT = index.doxygen \
|
|||
../include/seq_midi_event.h \
|
||||
../include/conv.h \
|
||||
../include/instr.h \
|
||||
../src
|
||||
../src/error.c \
|
||||
../src/dlmisc.c \
|
||||
../src/async.c \
|
||||
../src/input.c \
|
||||
../src/output.c \
|
||||
../src/conf.c \
|
||||
../src/confmisc.c \
|
||||
../src/control \
|
||||
../src/mixer \
|
||||
../src/pcm/pcm.c \
|
||||
../src/pcm/pcm_mmap.c \
|
||||
../src/pcm/pcm_plugin.c \
|
||||
../src/pcm/pcm_hw.c \
|
||||
../src/pcm/pcm_hooks.c \
|
||||
../src/pcm/pcm_meter.c \
|
||||
../src/pcm/pcm_misc.c \
|
||||
../src/rawmidi \
|
||||
../src/timer \
|
||||
../src/hwdep \
|
||||
../src/seq \
|
||||
../src/instr
|
||||
EXCLUDE = ../src/control/control_local.h \
|
||||
../src/pcm/atomic.h \
|
||||
../src/pcm/interval.h \
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ may be placed in the library code instead of the kernel driver.</P>
|
|||
|
||||
<UL>
|
||||
<LI>Page \ref pcm explains the design of PCM (digital audio) API
|
||||
<LI>Page \ref pcm_plugins explains the design of PCM (digital audio) plugins
|
||||
</UL>
|
||||
|
||||
<H2>Configuration</H2>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ alsaincludedir = ${includedir}/alsa
|
|||
|
||||
alsainclude_HEADERS = asoundlib.h asoundef.h \
|
||||
version.h global.h input.h output.h error.h \
|
||||
conf.h pcm.h rawmidi.h timer.h \
|
||||
conf.h pcm.h pcm_plugin.h rawmidi.h timer.h \
|
||||
hwdep.h control.h mixer.h \
|
||||
seq_event.h seq.h seqmid.h seq_midi_event.h \
|
||||
conv.h instr.h
|
||||
conv.h instr.h iatomic.h
|
||||
|
||||
noinst_HEADERS = sys.h search.h list.h aserver.h local.h config.h iatomic.h
|
||||
noinst_HEADERS = sys.h search.h list.h aserver.h local.h config.h
|
||||
|
||||
EXTRA_CLEAN = stamp-vh
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#ifndef __ALSA_IATOMIC__
|
||||
#define __ALSA_IATOMIC__
|
||||
#ifndef __ALSA_IATOMIC_H
|
||||
#define __ALSA_IATOMIC_H
|
||||
|
||||
#ifdef __i386__
|
||||
|
||||
|
|
@ -1059,4 +1059,72 @@ typedef struct { volatile int counter; } atomic_t;
|
|||
|
||||
#endif /* IATOMIC_DEFINED */
|
||||
|
||||
#endif /* __ALSA_IATOMIC__ */
|
||||
/*
|
||||
* Atomic read/write
|
||||
* Copyright (c) 2001 by Abramo Bagnara <abramo@alsa-project.org>
|
||||
*/
|
||||
|
||||
/* Max number of times we must spin on a spinlock calling sched_yield().
|
||||
After MAX_SPIN_COUNT iterations, we put the calling thread to sleep. */
|
||||
|
||||
#ifndef MAX_SPIN_COUNT
|
||||
#define MAX_SPIN_COUNT 50
|
||||
#endif
|
||||
|
||||
/* Duration of sleep (in nanoseconds) when we can't acquire a spinlock
|
||||
after MAX_SPIN_COUNT iterations of sched_yield().
|
||||
This MUST BE > 2ms.
|
||||
(Otherwise the kernel does busy-waiting for realtime threads,
|
||||
giving other threads no chance to run.) */
|
||||
|
||||
#ifndef SPIN_SLEEP_DURATION
|
||||
#define SPIN_SLEEP_DURATION 2000001
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
unsigned int begin, end;
|
||||
} snd_atomic_write_t;
|
||||
|
||||
typedef struct {
|
||||
volatile const snd_atomic_write_t *write;
|
||||
unsigned int end;
|
||||
} snd_atomic_read_t;
|
||||
|
||||
void snd_atomic_read_wait(snd_atomic_read_t *t);
|
||||
|
||||
static inline void snd_atomic_write_init(snd_atomic_write_t *w)
|
||||
{
|
||||
w->begin = 0;
|
||||
w->end = 0;
|
||||
}
|
||||
|
||||
static inline void snd_atomic_write_begin(snd_atomic_write_t *w)
|
||||
{
|
||||
w->begin++;
|
||||
wmb();
|
||||
}
|
||||
|
||||
static inline void snd_atomic_write_end(snd_atomic_write_t *w)
|
||||
{
|
||||
wmb();
|
||||
w->end++;
|
||||
}
|
||||
|
||||
static inline void snd_atomic_read_init(snd_atomic_read_t *r, snd_atomic_write_t *w)
|
||||
{
|
||||
r->write = w;
|
||||
}
|
||||
|
||||
static inline void snd_atomic_read_begin(snd_atomic_read_t *r)
|
||||
{
|
||||
r->end = r->write->end;
|
||||
rmb();
|
||||
}
|
||||
|
||||
static inline int snd_atomic_read_ok(snd_atomic_read_t *r)
|
||||
{
|
||||
rmb();
|
||||
return r->end == r->write->begin;
|
||||
}
|
||||
|
||||
#endif /* __ALSA_IATOMIC_H */
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ typedef struct sndrv_seq_event snd_seq_event_t;
|
|||
#include "error.h"
|
||||
#include "conf.h"
|
||||
#include "pcm.h"
|
||||
#include "pcm_plugin.h"
|
||||
#include "rawmidi.h"
|
||||
#include "timer.h"
|
||||
#include "hwdep.h"
|
||||
|
|
|
|||
112
include/pcm_plugin.h
Normal file
112
include/pcm_plugin.h
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* \file <alsa/pcm_plugin.h>
|
||||
* \brief Common PCM plugin code
|
||||
* \author Abramo Bagnara <abramo@alsa-project.org>
|
||||
* \author Jaroslav Kysela <perex@suse.cz>
|
||||
* \date 2000-2001
|
||||
*
|
||||
* Application interface library for the ALSA driver.
|
||||
* See the \ref pcm_plugins page for more details.
|
||||
*
|
||||
* \warning Using of contents of this header file might be dangerous
|
||||
* in the sense of compatibility reasons. The contents might be
|
||||
* freely changed in future.
|
||||
*
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as
|
||||
* published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef __ALSA_PCM_PLUGIN_H
|
||||
|
||||
/**
|
||||
* \defgroup PCM_Plugins PCM Plugins
|
||||
* \ingroup PCM
|
||||
* See the \ref pcm_plugins page for more details.
|
||||
* \{
|
||||
*/
|
||||
|
||||
#define SND_PCM_PLUGIN_RATE_MIN 4000 /**< minimal rate for the rate plugin */
|
||||
#define SND_PCM_PLUGIN_RATE_MAX 192000 /**< maximal rate for the rate plugin */
|
||||
|
||||
#define SND_PCM_PLUGIN_ROUTE_FLOAT 1 /**< use floats for route plugin */
|
||||
#define SND_PCM_PLUGIN_ROUTE_RESOLUTION 16 /**< integer resolution for route plugin */
|
||||
|
||||
#if SND_PCM_PLUGIN_ROUTE_FLOAT
|
||||
/** route ttable entry type */
|
||||
typedef float snd_pcm_route_ttable_entry_t;
|
||||
#define SND_PCM_PLUGIN_ROUTE_HALF 0.5 /**< half value */
|
||||
#define SND_PCM_PLUGIN_ROUTE_FULL 1.0 /**< full value */
|
||||
#else
|
||||
/** route ttable entry type */
|
||||
typedef int snd_pcm_route_ttable_entry_t;
|
||||
#define SND_PCM_PLUGIN_ROUTE_HALF (SND_PCM_PLUGIN_ROUTE_RESOLUTION / 2) /**< half value */
|
||||
#define SND_PCM_PLUGIN_ROUTE_FULL SND_PCM_PLUGIN_ROUTE_RESOLUTION /**< full value */
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Hardware plugin
|
||||
*/
|
||||
int snd_pcm_hw_open(snd_pcm_t **pcmp, const char *name,
|
||||
int card, int device, int subdevice,
|
||||
snd_pcm_stream_t stream, int mode,
|
||||
int mmap_emulation);
|
||||
int _snd_pcm_hw_open(snd_pcm_t **pcmp, const char *name,
|
||||
snd_config_t *root ATTRIBUTE_UNUSED, snd_config_t *conf,
|
||||
snd_pcm_stream_t stream, int mode);
|
||||
|
||||
int snd_pcm_copy_open(snd_pcm_t **pcmp, const char *name,
|
||||
snd_pcm_t *slave, int close_slave);
|
||||
int snd_pcm_linear_open(snd_pcm_t **pcmp, const char *name,
|
||||
snd_pcm_format_t sformat, snd_pcm_t *slave,
|
||||
int close_slave);
|
||||
int snd_pcm_lfloat_open(snd_pcm_t **pcmp, const char *name,
|
||||
snd_pcm_format_t sformat, snd_pcm_t *slave,
|
||||
int close_slave);
|
||||
int snd_pcm_mulaw_open(snd_pcm_t **pcmp, const char *name,
|
||||
snd_pcm_format_t sformat, snd_pcm_t *slave,
|
||||
int close_slave);
|
||||
int snd_pcm_alaw_open(snd_pcm_t **pcmp, const char *name,
|
||||
snd_pcm_format_t sformat, snd_pcm_t *slave,
|
||||
int close_slave);
|
||||
int snd_pcm_adpcm_open(snd_pcm_t **pcmp, const char *name,
|
||||
snd_pcm_format_t sformat, snd_pcm_t *slave,
|
||||
int close_slave);
|
||||
int snd_pcm_route_load_ttable(snd_config_t *tt, snd_pcm_route_ttable_entry_t *ttable,
|
||||
unsigned int tt_csize, unsigned int tt_ssize,
|
||||
unsigned int *tt_cused, unsigned int *tt_sused,
|
||||
int schannels);
|
||||
int snd_pcm_route_open(snd_pcm_t **pcmp, const char *name,
|
||||
snd_pcm_format_t sformat, int schannels,
|
||||
snd_pcm_route_ttable_entry_t *ttable,
|
||||
unsigned int tt_ssize,
|
||||
unsigned int tt_cused, unsigned int tt_sused,
|
||||
snd_pcm_t *slave, int close_slave);
|
||||
int snd_pcm_rate_open(snd_pcm_t **pcmp, const char *name,
|
||||
snd_pcm_format_t sformat, unsigned int srate,
|
||||
snd_pcm_t *slave, int close_slave);
|
||||
|
||||
/*
|
||||
* Hooks plugin
|
||||
*/
|
||||
int snd_pcm_hooks_open(snd_pcm_t **pcmp, const char *name,
|
||||
snd_pcm_t *slave, int close_slave);
|
||||
int _snd_pcm_hooks_open(snd_pcm_t **pcmp, const char *name,
|
||||
snd_config_t *root, snd_config_t *conf,
|
||||
snd_pcm_stream_t stream, int mode);
|
||||
|
||||
/** \} */
|
||||
|
||||
#endif /* __ALSA_PCM_PLUGIN_H */
|
||||
|
|
@ -239,7 +239,7 @@ with new contents. This mode is specified with a prefix char note of
|
|||
exclamation (!).
|
||||
|
||||
\code
|
||||
!defaults.pcm.device 1
|
||||
defaults.pcm.!device 1
|
||||
\endcode
|
||||
|
||||
\section conf_syntax_summary Syntax summary
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ libpcm_la_SOURCES = atomic.c mask.c interval.c \
|
|||
pcm_rate.c pcm_plug.c pcm_misc.c pcm_mmap.c pcm_multi.c \
|
||||
pcm_shm.c pcm_file.c pcm_null.c pcm_share.c \
|
||||
pcm_meter.c pcm_hooks.c pcm_lfloat.c pcm_ladspa.c pcm_symbols.c
|
||||
noinst_HEADERS = atomic.h pcm_local.h pcm_plugin.h mask.h mask_inline.h \
|
||||
noinst_HEADERS = pcm_local.h pcm_plugin.h mask.h mask_inline.h \
|
||||
interval.h interval_inline.h plugin_ops.h ladspa.h
|
||||
|
||||
alsadir = $(datadir)/alsa
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include <sched.h>
|
||||
#include "atomic.h"
|
||||
#include "iatomic.h"
|
||||
|
||||
void snd_atomic_read_wait(snd_atomic_read_t *t)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* Atomic read/write
|
||||
* Copyright (c) 2001 by Abramo Bagnara <abramo@alsa-project.org>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as
|
||||
* published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
|
||||
#include "iatomic.h"
|
||||
|
||||
/* Max number of times we must spin on a spinlock calling sched_yield().
|
||||
After MAX_SPIN_COUNT iterations, we put the calling thread to sleep. */
|
||||
|
||||
#ifndef MAX_SPIN_COUNT
|
||||
#define MAX_SPIN_COUNT 50
|
||||
#endif
|
||||
|
||||
/* Duration of sleep (in nanoseconds) when we can't acquire a spinlock
|
||||
after MAX_SPIN_COUNT iterations of sched_yield().
|
||||
This MUST BE > 2ms.
|
||||
(Otherwise the kernel does busy-waiting for realtime threads,
|
||||
giving other threads no chance to run.) */
|
||||
|
||||
#ifndef SPIN_SLEEP_DURATION
|
||||
#define SPIN_SLEEP_DURATION 2000001
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
unsigned int begin, end;
|
||||
} snd_atomic_write_t;
|
||||
|
||||
typedef struct {
|
||||
volatile const snd_atomic_write_t *write;
|
||||
unsigned int end;
|
||||
} snd_atomic_read_t;
|
||||
|
||||
void snd_atomic_read_wait(snd_atomic_read_t *t);
|
||||
|
||||
static inline void snd_atomic_write_init(snd_atomic_write_t *w)
|
||||
{
|
||||
w->begin = 0;
|
||||
w->end = 0;
|
||||
}
|
||||
|
||||
static inline void snd_atomic_write_begin(snd_atomic_write_t *w)
|
||||
{
|
||||
w->begin++;
|
||||
wmb();
|
||||
}
|
||||
|
||||
static inline void snd_atomic_write_end(snd_atomic_write_t *w)
|
||||
{
|
||||
wmb();
|
||||
w->end++;
|
||||
}
|
||||
|
||||
static inline void snd_atomic_read_init(snd_atomic_read_t *r, snd_atomic_write_t *w)
|
||||
{
|
||||
r->write = w;
|
||||
}
|
||||
|
||||
static inline void snd_atomic_read_begin(snd_atomic_read_t *r)
|
||||
{
|
||||
r->end = r->write->end;
|
||||
rmb();
|
||||
}
|
||||
|
||||
static inline int snd_atomic_read_ok(snd_atomic_read_t *r)
|
||||
{
|
||||
rmb();
|
||||
return r->end == r->write->begin;
|
||||
}
|
||||
|
||||
|
|
@ -1,3 +1,11 @@
|
|||
/**
|
||||
* \file pcm/pcm_hooks.c
|
||||
* \ingroup PCM_Hook
|
||||
* \brief PCM Hook Interface
|
||||
* \author Abramo Bagnara <abramo@alsa-project.org>
|
||||
* \author Jaroslav Kysela <perex@suse.cz>
|
||||
* \date 2000-2001
|
||||
*/
|
||||
/*
|
||||
* PCM - Hook functions
|
||||
* Copyright (c) 2001 by Abramo Bagnara <abramo@alsa-project.org>
|
||||
|
|
@ -40,6 +48,7 @@ typedef struct {
|
|||
int close_slave;
|
||||
struct list_head hooks[SND_PCM_HOOK_TYPE_LAST + 1];
|
||||
} snd_pcm_hooks_t;
|
||||
#endif
|
||||
|
||||
static int snd_pcm_hooks_close(snd_pcm_t *pcm)
|
||||
{
|
||||
|
|
@ -265,7 +274,7 @@ static void snd_pcm_hooks_dump(snd_pcm_t *pcm, snd_output_t *out)
|
|||
snd_pcm_dump(h->slave, out);
|
||||
}
|
||||
|
||||
snd_pcm_ops_t snd_pcm_hooks_ops = {
|
||||
static snd_pcm_ops_t snd_pcm_hooks_ops = {
|
||||
close: snd_pcm_hooks_close,
|
||||
info: snd_pcm_hooks_info,
|
||||
hw_refine: snd_pcm_hooks_hw_refine,
|
||||
|
|
@ -280,7 +289,7 @@ snd_pcm_ops_t snd_pcm_hooks_ops = {
|
|||
munmap: snd_pcm_hooks_munmap,
|
||||
};
|
||||
|
||||
snd_pcm_fast_ops_t snd_pcm_hooks_fast_ops = {
|
||||
static snd_pcm_fast_ops_t snd_pcm_hooks_fast_ops = {
|
||||
status: snd_pcm_hooks_status,
|
||||
state: snd_pcm_hooks_state,
|
||||
delay: snd_pcm_hooks_delay,
|
||||
|
|
@ -300,6 +309,17 @@ snd_pcm_fast_ops_t snd_pcm_hooks_fast_ops = {
|
|||
mmap_commit: snd_pcm_hooks_mmap_commit,
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Creates a new hooks PCM
|
||||
* \param pcmp Returns created PCM handle
|
||||
* \param name Name of PCM
|
||||
* \param slave Slave PCM
|
||||
* \param close_slave If set, slave PCM handle is closed when hooks PCM is closed
|
||||
* \retval zero on success otherwise a negative error code
|
||||
* \warning Using of this function might be dangerous in the sense
|
||||
* of compatibility reasons. The prototype might be freely
|
||||
* changed in future.
|
||||
*/
|
||||
int snd_pcm_hooks_open(snd_pcm_t **pcmp, const char *name, snd_pcm_t *slave, int close_slave)
|
||||
{
|
||||
snd_pcm_t *pcm;
|
||||
|
|
@ -331,6 +351,81 @@ int snd_pcm_hooks_open(snd_pcm_t **pcmp, const char *name, snd_pcm_t *slave, int
|
|||
return 0;
|
||||
}
|
||||
|
||||
/*! \page pcm_plugins
|
||||
|
||||
\section pcm_plugins_hooks Plugin: hooks
|
||||
|
||||
\code
|
||||
# Hook arguments definition
|
||||
hook_args.NAME {
|
||||
... # Arbitrary arguments
|
||||
}
|
||||
|
||||
# PCM hook type
|
||||
pcm_hook_type.NAME {
|
||||
[lib STR] # Library file (default libasound.so)
|
||||
[install STR] # Install function (default _snd_pcm_hook_NAME_install)
|
||||
}
|
||||
|
||||
# PCM hook definition
|
||||
pcm_hook.NAME {
|
||||
type STR # PCM Hook type (see pcm_hook_type)
|
||||
[args STR] # Arguments for install function (see hook_args)
|
||||
# or
|
||||
[args { }] # Arguments for install function
|
||||
}
|
||||
|
||||
# PCM hook plugin
|
||||
pcm.NAME {
|
||||
type hooks # PCM with hooks
|
||||
slave STR # Slave name
|
||||
# or
|
||||
slave { # Slave definition
|
||||
pcm STR # Slave PCM name
|
||||
# or
|
||||
pcm { } # Slave PCM definition
|
||||
}
|
||||
hooks {
|
||||
ID STR # Hook name (see pcm_hook)
|
||||
# or
|
||||
ID { } # Hook definition (see pcm_hook)
|
||||
}
|
||||
}
|
||||
\endcode
|
||||
|
||||
Example:
|
||||
|
||||
\code
|
||||
hooks.0 {
|
||||
type ctl_elems
|
||||
hook_args [
|
||||
{
|
||||
name "Wave Surround Playback Volume"
|
||||
preserve true
|
||||
lock true
|
||||
value [ 0 0 ]
|
||||
}
|
||||
{
|
||||
name "EMU10K1 PCM Send Volume"
|
||||
index { @func private_pcm_subdevice }
|
||||
lock true
|
||||
value [ 0 0 0 0 0 0 255 0 0 0 0 255 ]
|
||||
}
|
||||
]
|
||||
}
|
||||
\endcode
|
||||
|
||||
\subsection pcm_plugins_hooks_funcref Function reference
|
||||
|
||||
<UL>
|
||||
<LI>The function ctl_elems - _snd_pcm_hook_ctl_elems_install() - installs
|
||||
CTL settings described by given configuration.
|
||||
<LI>snd_pcm_hooks_open()
|
||||
<LI>_snd_pcm_hooks_open()
|
||||
</UL>
|
||||
|
||||
*/
|
||||
|
||||
static int snd_pcm_hook_add_conf(snd_pcm_t *pcm, snd_config_t *root, snd_config_t *conf)
|
||||
{
|
||||
int err;
|
||||
|
|
@ -444,6 +539,19 @@ static int snd_pcm_hook_add_conf(snd_pcm_t *pcm, snd_config_t *root, snd_config_
|
|||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Creates a new hooks PCM
|
||||
* \param pcmp Returns created PCM handle
|
||||
* \param name Name of PCM
|
||||
* \param root Root configuration node
|
||||
* \param conf Configuration node with hooks PCM description
|
||||
* \param stream PCM Stream
|
||||
* \param mode PCM Mode
|
||||
* \retval zero on success otherwise a negative error code
|
||||
* \warning Using of this function might be dangerous in the sense
|
||||
* of compatibility reasons. The prototype might be freely
|
||||
* changed in future.
|
||||
*/
|
||||
int _snd_pcm_hooks_open(snd_pcm_t **pcmp, const char *name,
|
||||
snd_config_t *root, snd_config_t *conf,
|
||||
snd_pcm_stream_t stream, int mode)
|
||||
|
|
@ -514,8 +622,8 @@ int _snd_pcm_hooks_open(snd_pcm_t **pcmp, const char *name,
|
|||
*pcmp = rpcm;
|
||||
return 0;
|
||||
}
|
||||
#ifndef DOC_HIDDEN
|
||||
SND_DLSYM_BUILD_VERSION(_snd_pcm_hooks_open, SND_PCM_DLSYM_VERSION);
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
|
|
@ -623,6 +731,12 @@ static int snd_pcm_hook_ctl_elems_close(snd_pcm_hook_t *hook)
|
|||
return err;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Install CTL settings using hardware associated with PCM handle
|
||||
* \param pcm PCM handle
|
||||
* \param conf Configuration node with CTL settings
|
||||
* \return zero on success otherwise a negative error code
|
||||
*/
|
||||
int _snd_pcm_hook_ctl_elems_install(snd_pcm_t *pcm, snd_config_t *conf)
|
||||
{
|
||||
int err;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
/**
|
||||
* \file pcm/pcm_hw.c
|
||||
* \ingroup PCM_Plugins
|
||||
* \brief PCM HW Interface
|
||||
* \author Abramo Bagnara <abramo@alsa-project.org>
|
||||
* \author Jaroslav Kysela <perex@suse.cz>
|
||||
* \date 2000-2001
|
||||
*/
|
||||
/*
|
||||
* PCM - Hardware
|
||||
* Copyright (c) 2000 by Abramo Bagnara <abramo@alsa-project.org>
|
||||
|
|
@ -36,6 +44,8 @@
|
|||
const char *_snd_module_pcm_hw = "";
|
||||
#endif
|
||||
|
||||
#ifndef DOC_HIDDEN
|
||||
|
||||
#ifndef F_SETSIG
|
||||
#define F_SETSIG 10
|
||||
#endif
|
||||
|
|
@ -62,6 +72,8 @@ typedef struct {
|
|||
do { if (hw->shadow_appl_ptr && !hw->avail_update_flag) \
|
||||
hw->appl_ptr = hw->mmap_control->appl_ptr; } while (0)
|
||||
|
||||
#endif /* DOC_HIDDEN */
|
||||
|
||||
static int snd_pcm_hw_nonblock(snd_pcm_t *pcm, int nonblock)
|
||||
{
|
||||
long flags;
|
||||
|
|
@ -643,7 +655,7 @@ static void snd_pcm_hw_dump(snd_pcm_t *pcm, snd_output_t *out)
|
|||
}
|
||||
}
|
||||
|
||||
snd_pcm_ops_t snd_pcm_hw_ops = {
|
||||
static snd_pcm_ops_t snd_pcm_hw_ops = {
|
||||
close: snd_pcm_hw_close,
|
||||
info: snd_pcm_hw_info,
|
||||
hw_refine: snd_pcm_hw_hw_refine,
|
||||
|
|
@ -658,7 +670,7 @@ snd_pcm_ops_t snd_pcm_hw_ops = {
|
|||
munmap: snd_pcm_hw_munmap,
|
||||
};
|
||||
|
||||
snd_pcm_fast_ops_t snd_pcm_hw_fast_ops = {
|
||||
static snd_pcm_fast_ops_t snd_pcm_hw_fast_ops = {
|
||||
status: snd_pcm_hw_status,
|
||||
state: snd_pcm_hw_state,
|
||||
delay: snd_pcm_hw_delay,
|
||||
|
|
@ -678,6 +690,20 @@ snd_pcm_fast_ops_t snd_pcm_hw_fast_ops = {
|
|||
mmap_commit: snd_pcm_hw_mmap_commit,
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Creates a new hw PCM
|
||||
* \param pcmp Returns created PCM handle
|
||||
* \param name Name of PCM
|
||||
* \param card Number of card
|
||||
* \param device Number of device
|
||||
* \param subdevice Number of subdevice
|
||||
* \param stream PCM Stream
|
||||
* \param mode PCM Mode
|
||||
* \retval zero on success otherwise a negative error code
|
||||
* \warning Using of this function might be dangerous in the sense
|
||||
* of compatibility reasons. The prototype might be freely
|
||||
* changed in future.
|
||||
*/
|
||||
int snd_pcm_hw_open(snd_pcm_t **pcmp, const char *name,
|
||||
int card, int device, int subdevice,
|
||||
snd_pcm_stream_t stream, int mode,
|
||||
|
|
@ -795,6 +821,45 @@ int snd_pcm_hw_open(snd_pcm_t **pcmp, const char *name,
|
|||
return ret;
|
||||
}
|
||||
|
||||
/*! \page pcm_plugins
|
||||
|
||||
\section pcm_plugins_hw Plugin: hw
|
||||
|
||||
This plugin communicates directly with the ALSA kernel driver. It is a raw
|
||||
communication without any conversions. The emulation of mmap access can be
|
||||
optionally enabled, but expect a worse latency in the case.
|
||||
|
||||
\code
|
||||
pcm.name {
|
||||
type hw # Kernel PCM
|
||||
card INT/STR # Card name (string) or number (integer)
|
||||
[device INT] # Device number (default 0)
|
||||
[subdevice INT] # Subdevice number (default -1: first available)
|
||||
[mmap_emulation BOOL] # Enable mmap emulation for ro/wo devices
|
||||
}
|
||||
\endcode
|
||||
|
||||
\subsection pcm_plugins_hw_funcref Function reference
|
||||
|
||||
<UL>
|
||||
<LI>snd_pcm_hw_open()
|
||||
<LI>_snd_pcm_hw_open()
|
||||
</UL>
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* \brief Creates a new hw PCM
|
||||
* \param pcmp Returns created PCM handle
|
||||
* \param name Name of PCM
|
||||
* \param root Root configuration node
|
||||
* \param conf Configuration node with hw PCM description
|
||||
* \param stream PCM Stream
|
||||
* \param mode PCM Mode
|
||||
* \warning Using of this function might be dangerous in the sense
|
||||
* of compatibility reasons. The prototype might be freely
|
||||
* changed in future.
|
||||
*/
|
||||
int _snd_pcm_hw_open(snd_pcm_t **pcmp, const char *name,
|
||||
snd_config_t *root ATTRIBUTE_UNUSED, snd_config_t *conf,
|
||||
snd_pcm_stream_t stream, int mode)
|
||||
|
|
@ -858,4 +923,6 @@ int _snd_pcm_hw_open(snd_pcm_t **pcmp, const char *name,
|
|||
}
|
||||
return snd_pcm_hw_open(pcmp, name, card, device, subdevice, stream, mode, mmap_emulation);
|
||||
}
|
||||
#ifndef DOC_HIDDEN
|
||||
SND_DLSYM_BUILD_VERSION(_snd_pcm_hw_open, SND_PCM_DLSYM_VERSION);
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -193,19 +193,6 @@ struct _snd_pcm {
|
|||
struct list_head async_handlers;
|
||||
};
|
||||
|
||||
#define ROUTE_PLUGIN_FLOAT 1
|
||||
#define ROUTE_PLUGIN_RESOLUTION 16
|
||||
|
||||
#if ROUTE_PLUGIN_FLOAT
|
||||
typedef float snd_pcm_route_ttable_entry_t;
|
||||
#define HALF 0.5
|
||||
#define FULL 1.0
|
||||
#else
|
||||
typedef int snd_pcm_route_ttable_entry_t;
|
||||
#define HALF (ROUTE_PLUGIN_RESOLUTION / 2)
|
||||
#define FULL ROUTE_PLUGIN_RESOLUTION
|
||||
#endif
|
||||
|
||||
/* FIXME */
|
||||
#define _snd_pcm_link_descriptor _snd_pcm_poll_descriptor
|
||||
#define _snd_pcm_async_descriptor _snd_pcm_poll_descriptor
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ static int snd_pcm_plug_change_channels(snd_pcm_t *pcm, snd_pcm_t **new, snd_pcm
|
|||
n = slv->channels;
|
||||
}
|
||||
while (n-- > 0) {
|
||||
snd_pcm_route_ttable_entry_t v = FULL;
|
||||
snd_pcm_route_ttable_entry_t v = SND_PCM_PLUGIN_ROUTE_FULL;
|
||||
if (rpolicy == PLUG_ROUTE_POLICY_AVERAGE) {
|
||||
if (pcm->stream == SND_PCM_STREAM_PLAYBACK &&
|
||||
clt->channels > slv->channels) {
|
||||
|
|
@ -363,7 +363,7 @@ static int snd_pcm_plug_change_channels(snd_pcm_t *pcm, snd_pcm_t **new, snd_pcm
|
|||
n = slv->channels;
|
||||
}
|
||||
for (c = 0; (int)c < n; c++)
|
||||
ttable[c * tt_ssize + c] = FULL;
|
||||
ttable[c * tt_ssize + c] = SND_PCM_PLUGIN_ROUTE_FULL;
|
||||
break;
|
||||
default:
|
||||
SNDERR("Invalid route policy");
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
/**
|
||||
* \file pcm/pcm_plugin.c
|
||||
* \ingroup PCM
|
||||
* \brief PCM Interface
|
||||
* \author Jaroslav Kysela <perex@suse.cz>
|
||||
* \author Abramo Bagnara <abramo@alsa-project.org>
|
||||
* \date 2000-2001
|
||||
*/
|
||||
/*
|
||||
* PCM - Common plugin code
|
||||
* Copyright (c) 2000 by Abramo Bagnara <abramo@alsa-project.org>
|
||||
|
|
@ -18,12 +26,21 @@
|
|||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
/*!
|
||||
* \page pcm_plugins PCM (digital audio) plugins
|
||||
*
|
||||
* PCM plugins extends functionality and features of PCM devices.
|
||||
* The plugins take care about various sample conversions, sample
|
||||
* copying among channels and so on.
|
||||
*/
|
||||
|
||||
#include <sys/shm.h>
|
||||
#include <limits.h>
|
||||
#include "pcm_local.h"
|
||||
#include "pcm_plugin.h"
|
||||
|
||||
#ifndef DOC_HIDDEN
|
||||
|
||||
int snd_pcm_plugin_close(snd_pcm_t *pcm)
|
||||
{
|
||||
snd_pcm_plugin_t *plugin = pcm->private_data;
|
||||
|
|
@ -481,3 +498,5 @@ snd_pcm_fast_ops_t snd_pcm_plugin_fast_ops = {
|
|||
mmap_commit: snd_pcm_plugin_mmap_commit,
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
*
|
||||
*/
|
||||
|
||||
#include "atomic.h"
|
||||
#include "iatomic.h"
|
||||
|
||||
typedef snd_pcm_uframes_t (*snd_pcm_slave_xfer_areas_func_t)
|
||||
(snd_pcm_t *pcm,
|
||||
|
|
@ -79,30 +79,9 @@ int snd_pcm_plugin_hw_refine_slave(snd_pcm_t *pcm, snd_pcm_hw_params_t *params);
|
|||
|
||||
extern snd_pcm_fast_ops_t snd_pcm_plugin_fast_ops;
|
||||
|
||||
#define RATE_MIN 4000
|
||||
#define RATE_MAX 192000
|
||||
|
||||
int snd_pcm_linear_get_index(snd_pcm_format_t src_format, snd_pcm_format_t dst_format);
|
||||
int snd_pcm_linear_put_index(snd_pcm_format_t src_format, snd_pcm_format_t dst_format);
|
||||
int snd_pcm_linear_convert_index(snd_pcm_format_t src_format, snd_pcm_format_t dst_format);
|
||||
int snd_pcm_copy_open(snd_pcm_t **pcmp, const char *name, snd_pcm_t *slave, int close_slave);
|
||||
int snd_pcm_linear_open(snd_pcm_t **pcmp, const char *name, snd_pcm_format_t sformat, snd_pcm_t *slave, int close_slave);
|
||||
int snd_pcm_lfloat_open(snd_pcm_t **pcmp, const char *name, snd_pcm_format_t sformat, snd_pcm_t *slave, int close_slave);
|
||||
int snd_pcm_mulaw_open(snd_pcm_t **pcmp, const char *name, snd_pcm_format_t sformat, snd_pcm_t *slave, int close_slave);
|
||||
int snd_pcm_alaw_open(snd_pcm_t **pcmp, const char *name, snd_pcm_format_t sformat, snd_pcm_t *slave, int close_slave);
|
||||
int snd_pcm_adpcm_open(snd_pcm_t **pcmp, const char *name, snd_pcm_format_t sformat, snd_pcm_t *slave, int close_slave);
|
||||
int snd_pcm_route_load_ttable(snd_config_t *tt, snd_pcm_route_ttable_entry_t *ttable,
|
||||
unsigned int tt_csize, unsigned int tt_ssize,
|
||||
unsigned int *tt_cused, unsigned int *tt_sused,
|
||||
int schannels);
|
||||
int snd_pcm_route_open(snd_pcm_t **pcmp, const char *name,
|
||||
snd_pcm_format_t sformat, int schannels,
|
||||
snd_pcm_route_ttable_entry_t *ttable,
|
||||
unsigned int tt_ssize,
|
||||
unsigned int tt_cused, unsigned int tt_sused,
|
||||
snd_pcm_t *slave, int close_slave);
|
||||
int snd_pcm_rate_open(snd_pcm_t **pcmp, const char *name, snd_pcm_format_t sformat, unsigned int srate, snd_pcm_t *slave, int close_slave);
|
||||
|
||||
|
||||
void snd_pcm_linear_convert(const snd_pcm_channel_area_t *dst_areas, snd_pcm_uframes_t dst_offset,
|
||||
const snd_pcm_channel_area_t *src_areas, snd_pcm_uframes_t src_offset,
|
||||
|
|
|
|||
|
|
@ -229,11 +229,11 @@ static int snd_pcm_rate_hw_refine_cprepare(snd_pcm_t *pcm ATTRIBUTE_UNUSED, snd_
|
|||
if (err < 0)
|
||||
return err;
|
||||
err = _snd_pcm_hw_param_set_min(params,
|
||||
SND_PCM_HW_PARAM_RATE, RATE_MIN, 0);
|
||||
SND_PCM_HW_PARAM_RATE, SND_PCM_PLUGIN_RATE_MIN, 0);
|
||||
if (err < 0)
|
||||
return err;
|
||||
err = _snd_pcm_hw_param_set_max(params,
|
||||
SND_PCM_HW_PARAM_RATE, RATE_MAX, 0);
|
||||
SND_PCM_HW_PARAM_RATE, SND_PCM_PLUGIN_RATE_MAX, 0);
|
||||
if (err < 0)
|
||||
return err;
|
||||
params->info &= ~(SND_PCM_INFO_MMAP | SND_PCM_INFO_MMAP_VALID);
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ const char *_snd_module_pcm_route = "";
|
|||
#endif
|
||||
|
||||
/* The best possible hack to support missing optimization in gcc 2.7.2.3 */
|
||||
#if ROUTE_PLUGIN_RESOLUTION & (ROUTE_PLUGIN_RESOLUTION - 1) != 0
|
||||
#define div(a) a /= ROUTE_PLUGIN_RESOLUTION
|
||||
#elif ROUTE_PLUGIN_RESOLUTION == 16
|
||||
#if SND_PCM_PLUGIN_ROUTE_RESOLUTION & (SND_PCM_PLUGIN_ROUTE_RESOLUTION - 1) != 0
|
||||
#define div(a) a /= SND_PCM_PLUGIN_ROUTE_RESOLUTION
|
||||
#elif SND_PCM_PLUGIN_ROUTE_RESOLUTION == 16
|
||||
#define div(a) a >>= 4
|
||||
#else
|
||||
#error "Add some code here"
|
||||
|
|
@ -41,7 +41,7 @@ const char *_snd_module_pcm_route = "";
|
|||
typedef struct {
|
||||
int channel;
|
||||
int as_int;
|
||||
#if ROUTE_PLUGIN_FLOAT
|
||||
#if SND_PCM_PLUGIN_ROUTE_FLOAT
|
||||
float as_float;
|
||||
#endif
|
||||
} snd_pcm_route_ttable_src_t;
|
||||
|
|
@ -78,7 +78,7 @@ struct snd_pcm_route_ttable_dst {
|
|||
typedef union {
|
||||
int32_t as_sint32;
|
||||
int64_t as_sint64;
|
||||
#if ROUTE_PLUGIN_FLOAT
|
||||
#if SND_PCM_PLUGIN_ROUTE_FLOAT
|
||||
float as_float;
|
||||
#endif
|
||||
} sum_t;
|
||||
|
|
@ -163,7 +163,7 @@ static void snd_pcm_route_convert1_many(const snd_pcm_channel_area_t *dst_area,
|
|||
#undef PUT32_LABELS
|
||||
static void *zero_labels[3] = {
|
||||
&&zero_int32, &&zero_int64,
|
||||
#if ROUTE_PLUGIN_FLOAT
|
||||
#if SND_PCM_PLUGIN_ROUTE_FLOAT
|
||||
&&zero_float
|
||||
#endif
|
||||
};
|
||||
|
|
@ -171,7 +171,7 @@ static void snd_pcm_route_convert1_many(const snd_pcm_channel_area_t *dst_area,
|
|||
static void *add_labels[3 * 2] = {
|
||||
&&add_int32_noatt, &&add_int32_att,
|
||||
&&add_int64_noatt, &&add_int64_att,
|
||||
#if ROUTE_PLUGIN_FLOAT
|
||||
#if SND_PCM_PLUGIN_ROUTE_FLOAT
|
||||
&&add_float_noatt, &&add_float_att
|
||||
#endif
|
||||
};
|
||||
|
|
@ -193,7 +193,7 @@ static void snd_pcm_route_convert1_many(const snd_pcm_channel_area_t *dst_area,
|
|||
&&norm_int64_8_att,
|
||||
&&norm_int64_16_att,
|
||||
&&norm_int64_24_att,
|
||||
#if ROUTE_PLUGIN_FLOAT
|
||||
#if SND_PCM_PLUGIN_ROUTE_FLOAT
|
||||
&&norm_float_0,
|
||||
&&norm_float_8,
|
||||
&&norm_float_16,
|
||||
|
|
@ -226,7 +226,7 @@ static void snd_pcm_route_convert1_many(const snd_pcm_channel_area_t *dst_area,
|
|||
src_areas, src_offset,
|
||||
frames, ttable, params);
|
||||
return;
|
||||
} else if (nsrcs == 1 && src_tt[0].as_int == ROUTE_PLUGIN_RESOLUTION) {
|
||||
} else if (nsrcs == 1 && src_tt[0].as_int == SND_PCM_PLUGIN_ROUTE_RESOLUTION) {
|
||||
snd_pcm_route_convert1_one(dst_area, dst_offset,
|
||||
src_areas, src_offset,
|
||||
frames, ttable, params);
|
||||
|
|
@ -253,7 +253,7 @@ static void snd_pcm_route_convert1_many(const snd_pcm_channel_area_t *dst_area,
|
|||
zero_int64:
|
||||
sum.as_sint64 = 0;
|
||||
goto zero_end;
|
||||
#if ROUTE_PLUGIN_FLOAT
|
||||
#if SND_PCM_PLUGIN_ROUTE_FLOAT
|
||||
zero_float:
|
||||
sum.as_float = 0.0;
|
||||
goto zero_end;
|
||||
|
|
@ -285,7 +285,7 @@ static void snd_pcm_route_convert1_many(const snd_pcm_channel_area_t *dst_area,
|
|||
if (ttp->as_int)
|
||||
sum.as_sint64 += sample;
|
||||
goto after_sum;
|
||||
#if ROUTE_PLUGIN_FLOAT
|
||||
#if SND_PCM_PLUGIN_ROUTE_FLOAT
|
||||
add_float_att:
|
||||
sum.as_float += sample * ttp->as_float;
|
||||
goto after_sum;
|
||||
|
|
@ -349,7 +349,7 @@ static void snd_pcm_route_convert1_many(const snd_pcm_channel_area_t *dst_area,
|
|||
sample = sum.as_sint64;
|
||||
goto after_norm;
|
||||
|
||||
#if ROUTE_PLUGIN_FLOAT
|
||||
#if SND_PCM_PLUGIN_ROUTE_FLOAT
|
||||
norm_float_8:
|
||||
sum.as_float *= 1 << 8;
|
||||
goto norm_float;
|
||||
|
|
@ -552,7 +552,7 @@ static int snd_pcm_route_hw_params(snd_pcm_t *pcm, snd_pcm_hw_params_t * params)
|
|||
route->params.conv_idx = snd_pcm_linear_convert_index(src_format, dst_format);
|
||||
route->params.src_size = snd_pcm_format_width(src_format) / 8;
|
||||
route->params.dst_sfmt = dst_format;
|
||||
#if ROUTE_PLUGIN_FLOAT
|
||||
#if SND_PCM_PLUGIN_ROUTE_FLOAT
|
||||
route->params.sum_idx = FLOAT;
|
||||
#else
|
||||
if (snd_pcm_format_width(src_format) == 32)
|
||||
|
|
@ -624,10 +624,10 @@ static void snd_pcm_route_dump(snd_pcm_t *pcm, snd_output_t *out)
|
|||
while (1) {
|
||||
snd_pcm_route_ttable_src_t *s = &d->srcs[src];
|
||||
if (d->att)
|
||||
#if ROUTE_PLUGIN_FLOAT
|
||||
#if SND_PCM_PLUGIN_ROUTE_FLOAT
|
||||
snd_output_printf(out, "%d*%g", s->channel, s->as_float);
|
||||
#else
|
||||
snd_output_printf(out, "%d*%g", s->channel, (double)s->as_int / (double)ROUTE_PLUGIN_RESOLUTION);
|
||||
snd_output_printf(out, "%d*%g", s->channel, (double)s->as_int / (double)SND_PCM_PLUGIN_ROUTE_RESOLUTION);
|
||||
#endif
|
||||
else
|
||||
snd_output_printf(out, "%d", s->channel);
|
||||
|
|
@ -693,24 +693,24 @@ static int route_load_ttable(snd_pcm_route_params_t *params, snd_pcm_stream_t st
|
|||
for (src_channel = 0; src_channel < sused; ++src_channel) {
|
||||
snd_pcm_route_ttable_entry_t v;
|
||||
v = ttable[src_channel * smul + dst_channel * dmul];
|
||||
assert(v >= 0 && v <= FULL);
|
||||
assert(v >= 0 && v <= SND_PCM_PLUGIN_ROUTE_FULL);
|
||||
if (v != 0) {
|
||||
srcs[nsrcs].channel = src_channel;
|
||||
#if ROUTE_PLUGIN_FLOAT
|
||||
#if SND_PCM_PLUGIN_ROUTE_FLOAT
|
||||
/* Also in user space for non attenuated */
|
||||
srcs[nsrcs].as_int = (v == FULL ? ROUTE_PLUGIN_RESOLUTION : 0);
|
||||
srcs[nsrcs].as_int = (v == SND_PCM_PLUGIN_ROUTE_FULL ? SND_PCM_PLUGIN_ROUTE_RESOLUTION : 0);
|
||||
srcs[nsrcs].as_float = v;
|
||||
#else
|
||||
srcs[nsrcs].as_int = v;
|
||||
#endif
|
||||
if (v != FULL)
|
||||
if (v != SND_PCM_PLUGIN_ROUTE_FULL)
|
||||
att = 1;
|
||||
t += v;
|
||||
nsrcs++;
|
||||
}
|
||||
}
|
||||
#if 0
|
||||
assert(t <= FULL);
|
||||
assert(t <= SND_PCM_PLUGIN_ROUTE_FULL);
|
||||
#endif
|
||||
dptr->att = att;
|
||||
dptr->nsrcs = nsrcs;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue