2021-03-02 19:47:04 +03:00
// SPDX-License-Identifier: GPL-2.0+
/*
* virtio - snd : Virtio sound device
* Copyright ( C ) 2021 OpenSynergy GmbH
*/
# include <linux/moduleparam.h>
# include <linux/virtio_config.h>
# include "virtio_card.h"
static u32 pcm_buffer_ms = 160 ;
module_param ( pcm_buffer_ms , uint , 0644 ) ;
MODULE_PARM_DESC ( pcm_buffer_ms , " PCM substream buffer time in milliseconds " ) ;
static u32 pcm_periods_min = 2 ;
module_param ( pcm_periods_min , uint , 0644 ) ;
MODULE_PARM_DESC ( pcm_periods_min , " Minimum number of PCM periods " ) ;
static u32 pcm_periods_max = 16 ;
module_param ( pcm_periods_max , uint , 0644 ) ;
MODULE_PARM_DESC ( pcm_periods_max , " Maximum number of PCM periods " ) ;
static u32 pcm_period_ms_min = 10 ;
module_param ( pcm_period_ms_min , uint , 0644 ) ;
MODULE_PARM_DESC ( pcm_period_ms_min , " Minimum PCM period time in milliseconds " ) ;
static u32 pcm_period_ms_max = 80 ;
module_param ( pcm_period_ms_max , uint , 0644 ) ;
MODULE_PARM_DESC ( pcm_period_ms_max , " Maximum PCM period time in milliseconds " ) ;
/* Map for converting VirtIO format to ALSA format. */
static const snd_pcm_format_t g_v2a_format_map [ ] = {
[ VIRTIO_SND_PCM_FMT_IMA_ADPCM ] = SNDRV_PCM_FORMAT_IMA_ADPCM ,
[ VIRTIO_SND_PCM_FMT_MU_LAW ] = SNDRV_PCM_FORMAT_MU_LAW ,
[ VIRTIO_SND_PCM_FMT_A_LAW ] = SNDRV_PCM_FORMAT_A_LAW ,
[ VIRTIO_SND_PCM_FMT_S8 ] = SNDRV_PCM_FORMAT_S8 ,
[ VIRTIO_SND_PCM_FMT_U8 ] = SNDRV_PCM_FORMAT_U8 ,
[ VIRTIO_SND_PCM_FMT_S16 ] = SNDRV_PCM_FORMAT_S16_LE ,
[ VIRTIO_SND_PCM_FMT_U16 ] = SNDRV_PCM_FORMAT_U16_LE ,
[ VIRTIO_SND_PCM_FMT_S18_3 ] = SNDRV_PCM_FORMAT_S18_3LE ,
[ VIRTIO_SND_PCM_FMT_U18_3 ] = SNDRV_PCM_FORMAT_U18_3LE ,
[ VIRTIO_SND_PCM_FMT_S20_3 ] = SNDRV_PCM_FORMAT_S20_3LE ,
[ VIRTIO_SND_PCM_FMT_U20_3 ] = SNDRV_PCM_FORMAT_U20_3LE ,
[ VIRTIO_SND_PCM_FMT_S24_3 ] = SNDRV_PCM_FORMAT_S24_3LE ,
[ VIRTIO_SND_PCM_FMT_U24_3 ] = SNDRV_PCM_FORMAT_U24_3LE ,
[ VIRTIO_SND_PCM_FMT_S20 ] = SNDRV_PCM_FORMAT_S20_LE ,
[ VIRTIO_SND_PCM_FMT_U20 ] = SNDRV_PCM_FORMAT_U20_LE ,
[ VIRTIO_SND_PCM_FMT_S24 ] = SNDRV_PCM_FORMAT_S24_LE ,
[ VIRTIO_SND_PCM_FMT_U24 ] = SNDRV_PCM_FORMAT_U24_LE ,
[ VIRTIO_SND_PCM_FMT_S32 ] = SNDRV_PCM_FORMAT_S32_LE ,
[ VIRTIO_SND_PCM_FMT_U32 ] = SNDRV_PCM_FORMAT_U32_LE ,
[ VIRTIO_SND_PCM_FMT_FLOAT ] = SNDRV_PCM_FORMAT_FLOAT_LE ,
[ VIRTIO_SND_PCM_FMT_FLOAT64 ] = SNDRV_PCM_FORMAT_FLOAT64_LE ,
[ VIRTIO_SND_PCM_FMT_DSD_U8 ] = SNDRV_PCM_FORMAT_DSD_U8 ,
[ VIRTIO_SND_PCM_FMT_DSD_U16 ] = SNDRV_PCM_FORMAT_DSD_U16_LE ,
[ VIRTIO_SND_PCM_FMT_DSD_U32 ] = SNDRV_PCM_FORMAT_DSD_U32_LE ,
[ VIRTIO_SND_PCM_FMT_IEC958_SUBFRAME ] =
SNDRV_PCM_FORMAT_IEC958_SUBFRAME_LE
} ;
/* Map for converting VirtIO frame rate to ALSA frame rate. */
struct virtsnd_v2a_rate {
unsigned int alsa_bit ;
unsigned int rate ;
} ;
static const struct virtsnd_v2a_rate g_v2a_rate_map [ ] = {
[ VIRTIO_SND_PCM_RATE_5512 ] = { SNDRV_PCM_RATE_5512 , 5512 } ,
[ VIRTIO_SND_PCM_RATE_8000 ] = { SNDRV_PCM_RATE_8000 , 8000 } ,
[ VIRTIO_SND_PCM_RATE_11025 ] = { SNDRV_PCM_RATE_11025 , 11025 } ,
[ VIRTIO_SND_PCM_RATE_16000 ] = { SNDRV_PCM_RATE_16000 , 16000 } ,
[ VIRTIO_SND_PCM_RATE_22050 ] = { SNDRV_PCM_RATE_22050 , 22050 } ,
[ VIRTIO_SND_PCM_RATE_32000 ] = { SNDRV_PCM_RATE_32000 , 32000 } ,
[ VIRTIO_SND_PCM_RATE_44100 ] = { SNDRV_PCM_RATE_44100 , 44100 } ,
[ VIRTIO_SND_PCM_RATE_48000 ] = { SNDRV_PCM_RATE_48000 , 48000 } ,
[ VIRTIO_SND_PCM_RATE_64000 ] = { SNDRV_PCM_RATE_64000 , 64000 } ,
[ VIRTIO_SND_PCM_RATE_88200 ] = { SNDRV_PCM_RATE_88200 , 88200 } ,
[ VIRTIO_SND_PCM_RATE_96000 ] = { SNDRV_PCM_RATE_96000 , 96000 } ,
[ VIRTIO_SND_PCM_RATE_176400 ] = { SNDRV_PCM_RATE_176400 , 176400 } ,
[ VIRTIO_SND_PCM_RATE_192000 ] = { SNDRV_PCM_RATE_192000 , 192000 }
} ;
/**
* virtsnd_pcm_build_hw ( ) - Parse substream config and build HW descriptor .
* @ vss : VirtIO substream .
* @ info : VirtIO substream information entry .
*
* Context : Any context .
* Return : 0 on success , - EINVAL if configuration is invalid .
*/
static int virtsnd_pcm_build_hw ( struct virtio_pcm_substream * vss ,
struct virtio_snd_pcm_info * info )
{
struct virtio_device * vdev = vss - > snd - > vdev ;
unsigned int i ;
u64 values ;
size_t sample_max = 0 ;
size_t sample_min = 0 ;
vss - > features = le32_to_cpu ( info - > features ) ;
/*
* TODO : set SNDRV_PCM_INFO_ { BATCH , BLOCK_TRANSFER } if device supports
* only message - based transport .
*/
vss - > hw . info =
SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_MMAP_VALID |
SNDRV_PCM_INFO_BATCH |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_PAUSE ;
if ( ! info - > channels_min | | info - > channels_min > info - > channels_max ) {
dev_err ( & vdev - > dev ,
" SID %u: invalid channel range [%u %u] \n " ,
vss - > sid , info - > channels_min , info - > channels_max ) ;
return - EINVAL ;
}
vss - > hw . channels_min = info - > channels_min ;
vss - > hw . channels_max = info - > channels_max ;
values = le64_to_cpu ( info - > formats ) ;
vss - > hw . formats = 0 ;
for ( i = 0 ; i < ARRAY_SIZE ( g_v2a_format_map ) ; + + i )
if ( values & ( 1ULL < < i ) ) {
snd_pcm_format_t alsa_fmt = g_v2a_format_map [ i ] ;
int bytes = snd_pcm_format_physical_width ( alsa_fmt ) / 8 ;
if ( ! sample_min | | sample_min > bytes )
sample_min = bytes ;
if ( sample_max < bytes )
sample_max = bytes ;
vss - > hw . formats | = pcm_format_to_bits ( alsa_fmt ) ;
}
if ( ! vss - > hw . formats ) {
dev_err ( & vdev - > dev ,
" SID %u: no supported PCM sample formats found \n " ,
vss - > sid ) ;
return - EINVAL ;
}
values = le64_to_cpu ( info - > rates ) ;
vss - > hw . rates = 0 ;
for ( i = 0 ; i < ARRAY_SIZE ( g_v2a_rate_map ) ; + + i )
if ( values & ( 1ULL < < i ) ) {
if ( ! vss - > hw . rate_min | |
vss - > hw . rate_min > g_v2a_rate_map [ i ] . rate )
vss - > hw . rate_min = g_v2a_rate_map [ i ] . rate ;
if ( vss - > hw . rate_max < g_v2a_rate_map [ i ] . rate )
vss - > hw . rate_max = g_v2a_rate_map [ i ] . rate ;
vss - > hw . rates | = g_v2a_rate_map [ i ] . alsa_bit ;
}
if ( ! vss - > hw . rates ) {
dev_err ( & vdev - > dev ,
" SID %u: no supported PCM frame rates found \n " ,
vss - > sid ) ;
return - EINVAL ;
}
vss - > hw . periods_min = pcm_periods_min ;
vss - > hw . periods_max = pcm_periods_max ;
/*
* We must ensure that there is enough space in the buffer to store
* pcm_buffer_ms ms for the combination ( Cmax , Smax , Rmax ) , where :
* Cmax = maximum supported number of channels ,
* Smax = maximum supported sample size in bytes ,
* Rmax = maximum supported frame rate .
*/
vss - > hw . buffer_bytes_max =
PAGE_ALIGN ( sample_max * vss - > hw . channels_max * pcm_buffer_ms *
( vss - > hw . rate_max / MSEC_PER_SEC ) ) ;
/*
* We must ensure that the minimum period size is enough to store
* pcm_period_ms_min ms for the combination ( Cmin , Smin , Rmin ) , where :
* Cmin = minimum supported number of channels ,
* Smin = minimum supported sample size in bytes ,
* Rmin = minimum supported frame rate .
*/
vss - > hw . period_bytes_min =
sample_min * vss - > hw . channels_min * pcm_period_ms_min *
( vss - > hw . rate_min / MSEC_PER_SEC ) ;
/*
* We must ensure that the maximum period size is enough to store
* pcm_period_ms_max ms for the combination ( Cmax , Smax , Rmax ) .
*/
vss - > hw . period_bytes_max =
sample_max * vss - > hw . channels_max * pcm_period_ms_max *
( vss - > hw . rate_max / MSEC_PER_SEC ) ;
return 0 ;
}
/**
* virtsnd_pcm_find ( ) - Find the PCM device for the specified node ID .
* @ snd : VirtIO sound device .
* @ nid : Function node ID .
*
* Context : Any context .
* Return : a pointer to the PCM device or ERR_PTR ( - ENOENT ) .
*/
struct virtio_pcm * virtsnd_pcm_find ( struct virtio_snd * snd , u32 nid )
{
struct virtio_pcm * vpcm ;
list_for_each_entry ( vpcm , & snd - > pcm_list , list )
if ( vpcm - > nid = = nid )
return vpcm ;
return ERR_PTR ( - ENOENT ) ;
}
/**
* virtsnd_pcm_find_or_create ( ) - Find or create the PCM device for the
* specified node ID .
* @ snd : VirtIO sound device .
* @ nid : Function node ID .
*
* Context : Any context that permits to sleep .
* Return : a pointer to the PCM device or ERR_PTR ( - errno ) .
*/
struct virtio_pcm * virtsnd_pcm_find_or_create ( struct virtio_snd * snd , u32 nid )
{
struct virtio_device * vdev = snd - > vdev ;
struct virtio_pcm * vpcm ;
vpcm = virtsnd_pcm_find ( snd , nid ) ;
if ( ! IS_ERR ( vpcm ) )
return vpcm ;
vpcm = devm_kzalloc ( & vdev - > dev , sizeof ( * vpcm ) , GFP_KERNEL ) ;
if ( ! vpcm )
return ERR_PTR ( - ENOMEM ) ;
vpcm - > nid = nid ;
list_add_tail ( & vpcm - > list , & snd - > pcm_list ) ;
return vpcm ;
}
/**
* virtsnd_pcm_validate ( ) - Validate if the device can be started .
* @ vdev : VirtIO parent device .
*
* Context : Any context .
* Return : 0 on success , - EINVAL on failure .
*/
int virtsnd_pcm_validate ( struct virtio_device * vdev )
{
if ( pcm_periods_min < 2 | | pcm_periods_min > pcm_periods_max ) {
dev_err ( & vdev - > dev ,
" invalid range [%u %u] of the number of PCM periods \n " ,
pcm_periods_min , pcm_periods_max ) ;
return - EINVAL ;
}
if ( ! pcm_period_ms_min | | pcm_period_ms_min > pcm_period_ms_max ) {
dev_err ( & vdev - > dev ,
" invalid range [%u %u] of the size of the PCM period \n " ,
pcm_period_ms_min , pcm_period_ms_max ) ;
return - EINVAL ;
}
if ( pcm_buffer_ms < pcm_periods_min * pcm_period_ms_min ) {
dev_err ( & vdev - > dev ,
" pcm_buffer_ms(=%u) value cannot be < %u ms \n " ,
pcm_buffer_ms , pcm_periods_min * pcm_period_ms_min ) ;
return - EINVAL ;
}
if ( pcm_period_ms_max > pcm_buffer_ms / 2 ) {
dev_err ( & vdev - > dev ,
" pcm_period_ms_max(=%u) value cannot be > %u ms \n " ,
pcm_period_ms_max , pcm_buffer_ms / 2 ) ;
return - EINVAL ;
}
return 0 ;
}
/**
* virtsnd_pcm_period_elapsed ( ) - Kernel work function to handle the elapsed
* period state .
* @ work : Elapsed period work .
*
* The main purpose of this function is to call snd_pcm_period_elapsed ( ) in
* a process context , not in an interrupt context . This is necessary because PCM
* devices operate in non - atomic mode .
*
* Context : Process context .
*/
static void virtsnd_pcm_period_elapsed ( struct work_struct * work )
{
struct virtio_pcm_substream * vss =
container_of ( work , struct virtio_pcm_substream , elapsed_period ) ;
snd_pcm_period_elapsed ( vss - > substream ) ;
}
/**
* virtsnd_pcm_parse_cfg ( ) - Parse the stream configuration .
* @ snd : VirtIO sound device .
*
* This function is called during initial device initialization .
*
* Context : Any context that permits to sleep .
* Return : 0 on success , - errno on failure .
*/
int virtsnd_pcm_parse_cfg ( struct virtio_snd * snd )
{
struct virtio_device * vdev = snd - > vdev ;
struct virtio_snd_pcm_info * info ;
u32 i ;
int rc ;
virtio_cread_le ( vdev , struct virtio_snd_config , streams ,
& snd - > nsubstreams ) ;
if ( ! snd - > nsubstreams )
return 0 ;
snd - > substreams = devm_kcalloc ( & vdev - > dev , snd - > nsubstreams ,
sizeof ( * snd - > substreams ) , GFP_KERNEL ) ;
if ( ! snd - > substreams )
return - ENOMEM ;
info = kcalloc ( snd - > nsubstreams , sizeof ( * info ) , GFP_KERNEL ) ;
if ( ! info )
return - ENOMEM ;
rc = virtsnd_ctl_query_info ( snd , VIRTIO_SND_R_PCM_INFO , 0 ,
snd - > nsubstreams , sizeof ( * info ) , info ) ;
if ( rc )
goto on_exit ;
for ( i = 0 ; i < snd - > nsubstreams ; + + i ) {
struct virtio_pcm_substream * vss = & snd - > substreams [ i ] ;
struct virtio_pcm * vpcm ;
vss - > snd = snd ;
vss - > sid = i ;
INIT_WORK ( & vss - > elapsed_period , virtsnd_pcm_period_elapsed ) ;
2021-03-02 19:47:05 +03:00
init_waitqueue_head ( & vss - > msg_empty ) ;
spin_lock_init ( & vss - > lock ) ;
2021-03-02 19:47:04 +03:00
rc = virtsnd_pcm_build_hw ( vss , & info [ i ] ) ;
if ( rc )
goto on_exit ;
vss - > nid = le32_to_cpu ( info [ i ] . hdr . hda_fn_nid ) ;
vpcm = virtsnd_pcm_find_or_create ( snd , vss - > nid ) ;
if ( IS_ERR ( vpcm ) ) {
rc = PTR_ERR ( vpcm ) ;
goto on_exit ;
}
switch ( info [ i ] . direction ) {
case VIRTIO_SND_D_OUTPUT :
vss - > direction = SNDRV_PCM_STREAM_PLAYBACK ;
break ;
case VIRTIO_SND_D_INPUT :
vss - > direction = SNDRV_PCM_STREAM_CAPTURE ;
break ;
default :
dev_err ( & vdev - > dev , " SID %u: unknown direction (%u) \n " ,
vss - > sid , info [ i ] . direction ) ;
rc = - EINVAL ;
goto on_exit ;
}
vpcm - > streams [ vss - > direction ] . nsubstreams + + ;
}
on_exit :
kfree ( info ) ;
return rc ;
}
/**
* virtsnd_pcm_build_devs ( ) - Build ALSA PCM devices .
* @ snd : VirtIO sound device .
*
* Context : Any context that permits to sleep .
* Return : 0 on success , - errno on failure .
*/
int virtsnd_pcm_build_devs ( struct virtio_snd * snd )
{
struct virtio_device * vdev = snd - > vdev ;
struct virtio_pcm * vpcm ;
u32 i ;
int rc ;
list_for_each_entry ( vpcm , & snd - > pcm_list , list ) {
unsigned int npbs =
vpcm - > streams [ SNDRV_PCM_STREAM_PLAYBACK ] . nsubstreams ;
unsigned int ncps =
vpcm - > streams [ SNDRV_PCM_STREAM_CAPTURE ] . nsubstreams ;
if ( ! npbs & & ! ncps )
continue ;
rc = snd_pcm_new ( snd - > card , VIRTIO_SND_CARD_DRIVER , vpcm - > nid ,
npbs , ncps , & vpcm - > pcm ) ;
if ( rc ) {
dev_err ( & vdev - > dev , " snd_pcm_new[%u] failed: %d \n " ,
vpcm - > nid , rc ) ;
return rc ;
}
vpcm - > pcm - > info_flags = 0 ;
vpcm - > pcm - > dev_class = SNDRV_PCM_CLASS_GENERIC ;
vpcm - > pcm - > dev_subclass = SNDRV_PCM_SUBCLASS_GENERIC_MIX ;
snprintf ( vpcm - > pcm - > name , sizeof ( vpcm - > pcm - > name ) ,
VIRTIO_SND_PCM_NAME " %u " , vpcm - > pcm - > device ) ;
vpcm - > pcm - > private_data = vpcm ;
vpcm - > pcm - > nonatomic = true ;
for ( i = 0 ; i < ARRAY_SIZE ( vpcm - > streams ) ; + + i ) {
struct virtio_pcm_stream * stream = & vpcm - > streams [ i ] ;
if ( ! stream - > nsubstreams )
continue ;
stream - > substreams =
devm_kcalloc ( & vdev - > dev , stream - > nsubstreams ,
sizeof ( * stream - > substreams ) ,
GFP_KERNEL ) ;
if ( ! stream - > substreams )
return - ENOMEM ;
stream - > nsubstreams = 0 ;
}
}
for ( i = 0 ; i < snd - > nsubstreams ; + + i ) {
struct virtio_pcm_stream * vs ;
struct virtio_pcm_substream * vss = & snd - > substreams [ i ] ;
vpcm = virtsnd_pcm_find ( snd , vss - > nid ) ;
if ( IS_ERR ( vpcm ) )
return PTR_ERR ( vpcm ) ;
vs = & vpcm - > streams [ vss - > direction ] ;
vs - > substreams [ vs - > nsubstreams + + ] = vss ;
}
list_for_each_entry ( vpcm , & snd - > pcm_list , list ) {
for ( i = 0 ; i < ARRAY_SIZE ( vpcm - > streams ) ; + + i ) {
struct virtio_pcm_stream * vs = & vpcm - > streams [ i ] ;
struct snd_pcm_str * ks = & vpcm - > pcm - > streams [ i ] ;
struct snd_pcm_substream * kss ;
if ( ! vs - > nsubstreams )
continue ;
for ( kss = ks - > substream ; kss ; kss = kss - > next )
vs - > substreams [ kss - > number ] - > substream = kss ;
2021-03-02 19:47:06 +03:00
snd_pcm_set_ops ( vpcm - > pcm , i , & virtsnd_pcm_ops ) ;
2021-03-02 19:47:04 +03:00
}
snd_pcm_set_managed_buffer_all ( vpcm - > pcm ,
SNDRV_DMA_TYPE_VMALLOC , NULL ,
0 , 0 ) ;
}
return 0 ;
}
2021-03-02 19:47:05 +03:00
/**
* virtsnd_pcm_event ( ) - Handle the PCM device event notification .
* @ snd : VirtIO sound device .
* @ event : VirtIO sound event .
*
* Context : Interrupt context .
*/
void virtsnd_pcm_event ( struct virtio_snd * snd , struct virtio_snd_event * event )
{
struct virtio_pcm_substream * vss ;
u32 sid = le32_to_cpu ( event - > data ) ;
if ( sid > = snd - > nsubstreams )
return ;
vss = & snd - > substreams [ sid ] ;
switch ( le32_to_cpu ( event - > hdr . code ) ) {
case VIRTIO_SND_EVT_PCM_PERIOD_ELAPSED :
/* TODO: deal with shmem elapsed period */
break ;
case VIRTIO_SND_EVT_PCM_XRUN :
spin_lock ( & vss - > lock ) ;
if ( vss - > xfer_enabled )
vss - > xfer_xrun = true ;
spin_unlock ( & vss - > lock ) ;
break ;
}
}