aboutsummaryrefslogblamecommitdiffstats
path: root/src/output/jack_output_plugin.c
blob: 5a89d7919a0bf352ebbce6e8187d8dd22a323a46 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
  
                                                          
                         









                                                                       



                                                                          

   
                   
                       
 

                   
                 



                            





                      


                           



                       
                                                                      
 
                  




                                                        
                         
 

                                
                           
 


                                      

                                           
 
                               
 

                                         
 
                                
                                      
                              
                                                 
 
                      





                                                                   
  
 








                                                         



















                                                                               
          
                                                   
 
                                                        
                                         
 
                         

                         


                                                          
                                                                          








                                                                          


                                                          
 
                                                                  

                                                                  
                                                                           
 
                                                                    
                                                                    
                                     

         









                                                                  


                 

                            
 
                                                        
                            

 

                                                                        
 
                                                                     
 


                                                               
                                           
 


                                                             

 

                               
 
                             

 
                                  




                              
      
 




















                                                        

                             



                             

                                                                     
                                 
                                                            

                                                                          





                                                                    


                                                                      




                                                                          
                                                         













                                                




























                                                                            
             
                                                                    
                                                                 
 
                             
                          
 
                                        









                                                                       



                                                                              

                                                              
 









                                                                             








                                                                          
                            



                                                                        
                                    
                
                                              

         






                                                                          

                                                                           
 
                                                

                                  
                                              
      
 
                  

 

                           
 

                                    


                                                           
                                                                
                                                 








                                                        
                                                           









                                                        


                                        
                                                             




                                                                

 




                                             
 
                           
 

                               
 






                                                                 
 


                                                      
                                                           
                                          
                                       
 
                                   
                                                                  
 



                                                                   
                                                             


                                                                            
 




                                                                  
                                          
                                                            
                                                      
                                  
                             

         
                                             

                                                                    


                                                                              
                                                                    
                                                      
                                          
                                     

                 











                                                                   


                                                     


                                                                           

                              

         

                                          
                                                                           

                                                      











                                                                                  

         
                                                              
 
                                                              


                                                                            
                                                         
                               
                                                                    
                                                                
                                                          



                                             
                                          
                                     
                 

         
                                     




                                                                            
                                                   


                                                                    
                                                    








                                             


                             
                    

 
           
                                                                              
 
                                    
 
                           
 

                          
                                                                 
                             
 
                                          
                                         
 


                                         
                    

 
           
                                        
 

                                    
                          

 










                                                                     
                   

                                   
                                                                 
                                                           
                                                                                

                                                              


         










                                                                     
                   

                                   
                                                                 
                                                           
                                                                                

                                                              


         



                                                             

                                          



                                                                  
                                   



                                                                  




                              
             
                                                                           
 
                                    
                                                                             
                                 
 

                          
                                       
                           
 

                                   
                                                                    




                                                                 
                                                                       





                                                                                
 

                                        
 


                                                       
         
 



                             
                                                
                                 

 
















                                                                     
                                                       
                       


                                                            

                                    

                              
                                
                                
  
/*
 * Copyright (C) 2003-2010 The Music Player Daemon Project
 * http://www.musicpd.org
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "config.h"
#include "output_api.h"

#include <assert.h>

#include <glib.h>
#include <jack/jack.h>
#include <jack/types.h>
#include <jack/ringbuffer.h>

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>

#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "jack"

enum {
	MAX_PORTS = 16,
};

static const size_t sample_size = sizeof(jack_default_audio_sample_t);

struct jack_data {
	/**
	 * libjack options passed to jack_client_open().
	 */
	jack_options_t options;

	const char *name;

	const char *server_name;

	/* configuration */

	char *source_ports[MAX_PORTS];
	unsigned num_source_ports;

	char *destination_ports[MAX_PORTS];
	unsigned num_destination_ports;

	size_t ringbuffer_size;

	/* the current audio format */
	struct audio_format audio_format;

	/* jack library stuff */
	jack_port_t *ports[MAX_PORTS];
	jack_client_t *client;
	jack_ringbuffer_t *ringbuffer[MAX_PORTS];

	bool shutdown;

	/**
	 * While this flag is set, the "process" callback generates
	 * silence.
	 */
	bool pause;
};

/**
 * The quark used for GError.domain.
 */
static inline GQuark
jack_output_quark(void)
{
	return g_quark_from_static_string("jack_output");
}

/**
 * Determine the number of frames guaranteed to be available on all
 * channels.
 */
static jack_nframes_t
mpd_jack_available(const struct jack_data *jd)
{
	size_t min = jack_ringbuffer_read_space(jd->ringbuffer[0]);

	for (unsigned i = 1; i < jd->audio_format.channels; ++i) {
		size_t current = jack_ringbuffer_read_space(jd->ringbuffer[i]);
		if (current < min)
			min = current;
	}

	assert(min % sample_size == 0);

	return min / sample_size;
}

static int
mpd_jack_process(jack_nframes_t nframes, void *arg)
{
	struct jack_data *jd = (struct jack_data *) arg;
	jack_default_audio_sample_t *out;

	if (nframes <= 0)
		return 0;

	if (jd->pause) {
		/* generate silence while MPD is paused */

		for (unsigned i = 0; i < jd->audio_format.channels; ++i) {
			out = jack_port_get_buffer(jd->ports[i], nframes);

			for (jack_nframes_t f = 0; f < nframes; ++f)
				out[f] = 0.0;
		}

		return 0;
	}

	jack_nframes_t available = mpd_jack_available(jd);
	if (available > nframes)
		available = nframes;

	for (unsigned i = 0; i < jd->audio_format.channels; ++i) {
		out = jack_port_get_buffer(jd->ports[i], nframes);
		jack_ringbuffer_read(jd->ringbuffer[i],
				     (char *)out, available * sample_size);

		for (jack_nframes_t f = available; f < nframes; ++f)
			/* ringbuffer underrun, fill with silence */
			out[f] = 0.0;
	}

	/* generate silence for the unused source ports */

	for (unsigned i = jd->audio_format.channels;
	     i < jd->num_source_ports; ++i) {
		out = jack_port_get_buffer(jd->ports[i], nframes);

		for (jack_nframes_t f = 0; f < nframes; ++f)
			out[f] = 0.0;
	}

	return 0;
}

static void
mpd_jack_shutdown(void *arg)
{
	struct jack_data *jd = (struct jack_data *) arg;
	jd->shutdown = true;
}

static void
set_audioformat(struct jack_data *jd, struct audio_format *audio_format)
{
	audio_format->sample_rate = jack_get_sample_rate(jd->client);

	if (jd->num_source_ports == 1)
		audio_format->channels = 1;
	else if (audio_format->channels > jd->num_source_ports)
		audio_format->channels = 2;

	if (audio_format->format != SAMPLE_FORMAT_S16 &&
	    audio_format->format != SAMPLE_FORMAT_S24_P32)
		audio_format->format = SAMPLE_FORMAT_S24_P32;
}

static void
mpd_jack_error(const char *msg)
{
	g_warning("%s", msg);
}

#ifdef HAVE_JACK_SET_INFO_FUNCTION
static void
mpd_jack_info(const char *msg)
{
	g_message("%s", msg);
}
#endif

/**
 * Disconnect the JACK client.
 */
static void
mpd_jack_disconnect(struct jack_data *jd)
{
	assert(jd != NULL);
	assert(jd->client != NULL);

	jack_deactivate(jd->client);
	jack_client_close(jd->client);
	jd->client = NULL;
}

/**
 * Connect the JACK client and performs some basic setup
 * (e.g. register callbacks).
 */
static bool
mpd_jack_connect(struct jack_data *jd, GError **error_r)
{
	jack_status_t status;

	assert(jd != NULL);

	jd->shutdown = false;

	jd->client = jack_client_open(jd->name, jd->options, &status,
				      jd->server_name);
	if (jd->client == NULL) {
		g_set_error(error_r, jack_output_quark(), 0,
			    "Failed to connect to JACK server, status=%d",
			    status);
		return false;
	}

	jack_set_process_callback(jd->client, mpd_jack_process, jd);
	jack_on_shutdown(jd->client, mpd_jack_shutdown, jd);

	for (unsigned i = 0; i < jd->num_source_ports; ++i) {
		jd->ports[i] = jack_port_register(jd->client,
						  jd->source_ports[i],
						  JACK_DEFAULT_AUDIO_TYPE,
						  JackPortIsOutput, 0);
		if (jd->ports[i] == NULL) {
			g_set_error(error_r, jack_output_quark(), 0,
				    "Cannot register output port \"%s\"",
				    jd->source_ports[i]);
			mpd_jack_disconnect(jd);
			return false;
		}
	}

	return true;
}

static bool
mpd_jack_test_default_device(void)
{
	return true;
}

static unsigned
parse_port_list(int line, const char *source, char **dest, GError **error_r)
{
	char **list = g_strsplit(source, ",", 0);
	unsigned n = 0;

	for (n = 0; list[n] != NULL; ++n) {
		if (n >= MAX_PORTS) {
			g_set_error(error_r, jack_output_quark(), 0,
				    "too many port names in line %d",
				    line);
			return 0;
		}

		dest[n] = list[n];
	}

	g_free(list);

	if (n == 0) {
		g_set_error(error_r, jack_output_quark(), 0,
			    "at least one port name expected in line %d",
			    line);
		return 0;
	}

	return n;
}

static void *
mpd_jack_init(G_GNUC_UNUSED const struct audio_format *audio_format,
	      const struct config_param *param, GError **error_r)
{
	struct jack_data *jd;
	const char *value;

	jd = g_new(struct jack_data, 1);
	jd->options = JackNullOption;

	jd->name = config_get_block_string(param, "client_name", NULL);
	if (jd->name != NULL)
		jd->options |= JackUseExactName;
	else
		/* if there's a no configured client name, we don't
		   care about the JackUseExactName option */
		jd->name = "Music Player Daemon";

	jd->server_name = config_get_block_string(param, "server_name", NULL);
	if (jd->server_name != NULL)
		jd->options |= JackServerName;

	if (!config_get_block_bool(param, "autostart", false))
		jd->options |= JackNoStartServer;

	/* configure the source ports */

	value = config_get_block_string(param, "source_ports", "left,right");
	jd->num_source_ports = parse_port_list(param->line, value,
					       jd->source_ports, error_r);
	if (jd->num_source_ports == 0)
		return NULL;

	/* configure the destination ports */

	value = config_get_block_string(param, "destination_ports", NULL);
	if (value == NULL) {
		/* compatibility with MPD < 0.16 */
		value = config_get_block_string(param, "ports", NULL);
		if (value != NULL)
			g_warning("deprecated option 'ports' in line %d",
				  param->line);
	}

	if (value != NULL) {
		jd->num_destination_ports =
			parse_port_list(param->line, value,
					jd->destination_ports, error_r);
		if (jd->num_destination_ports == 0)
			return NULL;
	} else {
		jd->num_destination_ports = 0;
	}

	if (jd->num_destination_ports > 0 &&
	    jd->num_destination_ports != jd->num_source_ports)
		g_warning("number of source ports (%u) mismatches the "
			  "number of destination ports (%u) in line %d",
			  jd->num_source_ports, jd->num_destination_ports,
			  param->line);

	jd->ringbuffer_size =
		config_get_block_unsigned(param, "ringbuffer_size", 32768);

	jack_set_error_function(mpd_jack_error);

#ifdef HAVE_JACK_SET_INFO_FUNCTION
	jack_set_info_function(mpd_jack_info);
#endif

	return jd;
}

static void
mpd_jack_finish(void *data)
{
	struct jack_data *jd = data;

	for (unsigned i = 0; i < jd->num_source_ports; ++i)
		g_free(jd->source_ports[i]);

	for (unsigned i = 0; i < jd->num_destination_ports; ++i)
		g_free(jd->destination_ports[i]);

	g_free(jd);
}

static bool
mpd_jack_enable(void *data, GError **error_r)
{
	struct jack_data *jd = (struct jack_data *)data;

	for (unsigned i = 0; i < jd->num_source_ports; ++i)
		jd->ringbuffer[i] = NULL;

	return mpd_jack_connect(jd, error_r);
}

static void
mpd_jack_disable(void *data)
{
	struct jack_data *jd = (struct jack_data *)data;

	if (jd->client != NULL)
		mpd_jack_disconnect(jd);

	for (unsigned i = 0; i < jd->num_source_ports; ++i) {
		if (jd->ringbuffer[i] != NULL) {
			jack_ringbuffer_free(jd->ringbuffer[i]);
			jd->ringbuffer[i] = NULL;
		}
	}
}

/**
 * Stops the playback on the JACK connection.
 */
static void
mpd_jack_stop(struct jack_data *jd)
{
	assert(jd != NULL);

	if (jd->client == NULL)
		return;

	if (jd->shutdown)
		/* the connection has failed; close it */
		mpd_jack_disconnect(jd);
	else
		/* the connection is alive: just stop playback */
		jack_deactivate(jd->client);
}

static bool
mpd_jack_start(struct jack_data *jd, GError **error_r)
{
	const char *destination_ports[MAX_PORTS], **jports;
	const char *duplicate_port = NULL;
	unsigned num_destination_ports;

	assert(jd->client != NULL);
	assert(jd->audio_format.channels <= jd->num_source_ports);

	/* allocate the ring buffers on the first open(); these
	   persist until MPD exits.  It's too unsafe to delete them
	   because we can never know when mpd_jack_process() gets
	   called */
	for (unsigned i = 0; i < jd->num_source_ports; ++i) {
		if (jd->ringbuffer[i] == NULL)
			jd->ringbuffer[i] =
				jack_ringbuffer_create(jd->ringbuffer_size);

		/* clear the ring buffer to be sure that data from
		   previous playbacks are gone */
		jack_ringbuffer_reset(jd->ringbuffer[i]);
	}

	if ( jack_activate(jd->client) ) {
		g_set_error(error_r, jack_output_quark(), 0,
			    "cannot activate client");
		mpd_jack_stop(jd);
		return false;
	}

	if (jd->num_destination_ports == 0) {
		/* no output ports were configured - ask libjack for
		   defaults */
		jports = jack_get_ports(jd->client, NULL, NULL,
					JackPortIsPhysical | JackPortIsInput);
		if (jports == NULL) {
			g_set_error(error_r, jack_output_quark(), 0,
				    "no ports found");
			mpd_jack_stop(jd);
			return false;
		}

		assert(*jports != NULL);

		for (num_destination_ports = 0;
		     num_destination_ports < MAX_PORTS &&
			     jports[num_destination_ports] != NULL;
		     ++num_destination_ports) {
			g_debug("destination_port[%u] = '%s'\n",
				num_destination_ports,
				jports[num_destination_ports]);
			destination_ports[num_destination_ports] =
				jports[num_destination_ports];
		}
	} else {
		/* use the configured output ports */

		num_destination_ports = jd->num_destination_ports;
		memcpy(destination_ports, jd->destination_ports,
		       num_destination_ports * sizeof(*destination_ports));

		jports = NULL;
	}

	assert(num_destination_ports > 0);

	if (jd->audio_format.channels >= 2 && num_destination_ports == 1) {
		/* mix stereo signal on one speaker */

		while (num_destination_ports < jd->audio_format.channels)
			destination_ports[num_destination_ports++] =
				destination_ports[0];
	} else if (num_destination_ports > jd->audio_format.channels) {
		if (jd->audio_format.channels == 1 && num_destination_ports > 2) {
			/* mono input file: connect the one source
			   channel to the both destination channels */
			duplicate_port = destination_ports[1];
			num_destination_ports = 1;
		} else
			/* connect only as many ports as we need */
			num_destination_ports = jd->audio_format.channels;
	}

	assert(num_destination_ports <= jd->num_source_ports);

	for (unsigned i = 0; i < num_destination_ports; ++i) {
		int ret;

		ret = jack_connect(jd->client, jack_port_name(jd->ports[i]),
				   destination_ports[i]);
		if (ret != 0) {
			g_set_error(error_r, jack_output_quark(), 0,
				    "Not a valid JACK port: %s",
				    destination_ports[i]);

			if (jports != NULL)
				free(jports);

			mpd_jack_stop(jd);
			return false;
		}
	}

	if (duplicate_port != NULL) {
		/* mono input file: connect the one source channel to
		   the both destination channels */
		int ret;

		ret = jack_connect(jd->client, jack_port_name(jd->ports[0]),
				   duplicate_port);
		if (ret != 0) {
			g_set_error(error_r, jack_output_quark(), 0,
				    "Not a valid JACK port: %s",
				    duplicate_port);

			if (jports != NULL)
				free(jports);

			mpd_jack_stop(jd);
			return false;
		}
	}

	if (jports != NULL)
		free(jports);

	return true;
}

static bool
mpd_jack_open(void *data, struct audio_format *audio_format, GError **error_r)
{
	struct jack_data *jd = data;

	assert(jd != NULL);

	jd->pause = false;

	if (jd->client == NULL && !mpd_jack_connect(jd, error_r))
		return false;

	set_audioformat(jd, audio_format);
	jd->audio_format = *audio_format;

	if (!mpd_jack_start(jd, error_r))
		return false;

	return true;
}

static void
mpd_jack_close(G_GNUC_UNUSED void *data)
{
	struct jack_data *jd = data;

	mpd_jack_stop(jd);
}

static inline jack_default_audio_sample_t
sample_16_to_jack(int16_t sample)
{
	return sample / (jack_default_audio_sample_t)(1 << (16 - 1));
}

static void
mpd_jack_write_samples_16(struct jack_data *jd, const int16_t *src,
			  unsigned num_samples)
{
	jack_default_audio_sample_t sample;
	unsigned i;

	while (num_samples-- > 0) {
		for (i = 0; i < jd->audio_format.channels; ++i) {
			sample = sample_16_to_jack(*src++);
			jack_ringbuffer_write(jd->ringbuffer[i], (void*)&sample,
					      sizeof(sample));
		}
	}
}

static inline jack_default_audio_sample_t
sample_24_to_jack(int32_t sample)
{
	return sample / (jack_default_audio_sample_t)(1 << (24 - 1));
}

static void
mpd_jack_write_samples_24(struct jack_data *jd, const int32_t *src,
			  unsigned num_samples)
{
	jack_default_audio_sample_t sample;
	unsigned i;

	while (num_samples-- > 0) {
		for (i = 0; i < jd->audio_format.channels; ++i) {
			sample = sample_24_to_jack(*src++);
			jack_ringbuffer_write(jd->ringbuffer[i], (void*)&sample,
					      sizeof(sample));
		}
	}
}

static void
mpd_jack_write_samples(struct jack_data *jd, const void *src,
		       unsigned num_samples)
{
	switch (jd->audio_format.format) {
	case SAMPLE_FORMAT_S16:
		mpd_jack_write_samples_16(jd, (const int16_t*)src,
					  num_samples);
		break;

	case SAMPLE_FORMAT_S24_P32:
		mpd_jack_write_samples_24(jd, (const int32_t*)src,
					  num_samples);
		break;

	default:
		assert(false);
	}
}

static size_t
mpd_jack_play(void *data, const void *chunk, size_t size, GError **error_r)
{
	struct jack_data *jd = data;
	const size_t frame_size = audio_format_frame_size(&jd->audio_format);
	size_t space = 0, space1;

	jd->pause = false;

	assert(size % frame_size == 0);
	size /= frame_size;

	while (true) {
		if (jd->shutdown) {
			g_set_error(error_r, jack_output_quark(), 0,
				    "Refusing to play, because "
				    "there is no client thread");
			return 0;
		}

		space = jack_ringbuffer_write_space(jd->ringbuffer[0]);
		for (unsigned i = 1; i < jd->audio_format.channels; ++i) {
			space1 = jack_ringbuffer_write_space(jd->ringbuffer[i]);
			if (space > space1)
				/* send data symmetrically */
				space = space1;
		}

		if (space >= frame_size)
			break;

		/* XXX do something more intelligent to
		   synchronize */
		g_usleep(1000);
	}

	space /= sample_size;
	if (space < size)
		size = space;

	mpd_jack_write_samples(jd, chunk, size);
	return size * frame_size;
}

static bool
mpd_jack_pause(void *data)
{
	struct jack_data *jd = data;

	if (jd->shutdown)
		return false;

	jd->pause = true;

	/* due to a MPD API limitation, we have to sleep a little bit
	   here, to avoid hogging the CPU */
	g_usleep(50000);

	return true;
}

const struct audio_output_plugin jack_output_plugin = {
	.name = "jack",
	.test_default_device = mpd_jack_test_default_device,
	.init = mpd_jack_init,
	.finish = mpd_jack_finish,
	.enable = mpd_jack_enable,
	.disable = mpd_jack_disable,
	.open = mpd_jack_open,
	.play = mpd_jack_play,
	.pause = mpd_jack_pause,
	.close = mpd_jack_close,
};