summaryrefslogblamecommitdiffstatshomepage
path: root/src/devices/sound/spkrdev.cpp
blob: fc912fbd3672db2160d66f8d428b3fd0e258c5c8 (plain) (tree)
1
2
3
4
5
6
7
8
9

                                    






                                                                            
                               
                               

                                                                             
                                                                          




                                                                          
  




                                                                                               
                                                                                           









                                                                                     
                                                       




































                                                                                                     
                

                          
 
                                                        
 
                                                                   
                                         
 
 
                                                                                                     

 
                                                                                                                           
                                                             


                                                
 

 
                                                   

                                                   
 



                                         
 
                                                                                         

                    
                                           











                                                                                                       
                                
 
                                                                        

                                                                                               
 

                                                             

                                                                                       

                                                                                        









                                                                                                    
                                                 
                                                                                 
                                                                             

                                                             

                           
                                      
                    
                                               


                                                                        


                                                                                     
                                           
                              
      






                                                    

                                 
 


                                                                                                                      


                                         

















                                                                                                       



                                                                                                       

 


                                                   
 

                                                                                                                                      
 
                                             
                                       
                               
                                               
 


                                                  
                                                                    
                                
                                                
 
                                                                             


                                                                                                          
         
 


                                                                       
                                                                                    
 

                                                                                
 


                                                                                                            
                                                                                            

                                                                     
 
                                           



                                                                                                               
         
 


 
                                                 
 

                      
 
                                 


                          

                              

                                             
 

                                   
 
                                              

                                                                                                  
                                                    

                                                                             
                                    
                       

                                                                            


                                                                      
 
                                                                          
                            
 
                                                                                                     

                                                                                      



                                                                                                       



                                                                                                              
                                            

                                                           
                            
 
 

 
                                                                                  



                                                                      
                                                 


                                                                                                   

                                               

                                     


                                                                                


                                                                                           


 
                                                                                  
 
                                      

                                                           
                                                    

                                                           
                                               

                                                                     
                                                                   
                 

                                                                            


                                                                                                                  

                                                
                                                  
                                  
 





                                                            



                               
                                                             



                                                              

                                                                                                                
                               

                                                                         




                                                                                                  
                                                    

                                                                



                                                       
 
                                
                                                                                  


 
                                                                                                       

                                          
                                             


 
                                                  



                                   
 
                                                                                             
                                                                                 

                                              

                                                                    




                                  
// license:BSD-3-Clause
// copyright-holders:Nicola Salmoria
/***************************************************************************

    speaker.c

    Sound driver to emulate a simple speaker,
    driven by one or more output bits

    Original author: (unsigned)
    Filtering: Anders Hallstr?m
****************************************************************************/

/* Discussion of oversampling and anti-alias filtering: (Anders Hallstr?m)
 *
 * This driver is for machines that directly control
 * one or more simple digital-to-analog converters (DAC)
 * connected to one or more audio outputs (such as analog amp + speaker).
 * Currently only 1-bit DAC is supported via the interface to this module.
 *
 * Frequently such machines would oversample the DAC
 * in order to overcome the limited DAC resolution.
 * For faithful reproduction of the sound, this must be carefully handled
 * with anti-alias filtering when converting a high-rate low-resolution signal
 * to a moderate-rate high-resolution signal suitable for the DAC in the emulator's sound card.
 * (Originally, removal of any redundant high frequency content occurred on the analog side
 *  with no aliasing effects.)
 *
 * The most straightforward, naive way to handle this is to use two streams;
 * stream 1 modeling the native audio, with a sampling rate that allows for
 * accurate representation of over-sampling, i.e. the sampling rate should match
 * the clock frequency of the audio generating device (such as the CPU).
 * Stream 1 is connected to stream 2, which is concerned with feeding the sound card.
 * The stream system has features to handle rate conversion from stream 1 to 2.
 *
 * I tried it out of curiosity; it works fine conceptually, but
 *  - it puts an unnecessary burden on system resources
 *  - sound quality is still not satisfactory, though better than without anti-alias
 *  - "stream 1" properties are machine specific and so should be configured
 *    individually in each machine driver using this approach.
 *    This can also be seen as an advantage for flexibility, though.
 *
 * Instead, dedicated filtering is implemented in this module,
 * in a machine-neutral way (based on machine time and external -samplerate only).
 *
 * The basic average filter has the advantage that it can be used without
 * explicitly generating all samples in "stream 1". However,
 * it is poor for anti-alias filtering.
 * Therefore, average filtering is combined with windowed sinc.
 *
 * Virtual stream 1: Samples in true machine time.
 * Any sampling rate up to attotime resolution is implicitly supported.
 * -> average filtering over each stream 2 sample ->
 * Virtual stream 2: Intermediate representation.
 * Sample rate = RATE_MULTIPLIER * stream 3 sample rate.
 * If effective rate of stream 1 exceeds rate of stream 2,
 * some aliasing distorsion is introduced in this step because the average filtering is a compromise.
 * The distorsion is however mostly in the higher frequencies.
 * -> low-pass anti-alias filtering with kernel ampl[] ->
 * -> down-sampling ->
 * Actual stream 3: channel output generated by speaker_sound_update().
 * Sample rate = device sample rate = configured "-samplerate".
 *
 * In the speaker_state data structure,
 *    "intermediate samples" refers to "stream 2"
 *    "channel samples" refers to "stream 3"
 */

/* IMPROVEMENTS POSSIBLE:
 * - Make filter length a run-time configurable parameter. min=1 max=1000 or something
 * - Optimize cutoff freq automatically after filter length, or configurable too
 * - Generalise this approach to other DAC-based sound types if susceptible to aliasing
 */

#include "emu.h"
#include "sound/spkrdev.h"


static constexpr int16_t default_levels[2] = {0, 32767};

// Internal oversampling factor (interm. samples vs stream samples)
static constexpr int RATE_MULTIPLIER = 4;


DEFINE_DEVICE_TYPE(SPEAKER_SOUND, speaker_sound_device, "speaker_sound_device", "Filtered 1-bit DAC")


speaker_sound_device::speaker_sound_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
	: device_t(mconfig, SPEAKER_SOUND, tag, owner, clock)
	, device_sound_interface(mconfig, *this)
	, m_num_levels(2)
	, m_levels(default_levels)
{
}

//-------------------------------------------------
//  device_start - device-specific startup
//-------------------------------------------------

void speaker_sound_device::device_start()
{
	int i;
	double x;

	m_channel = machine().sound().stream_alloc(*this, 0, 1, machine().sample_rate());

	m_level = 0;
	for (i = 0; i < FILTER_LENGTH; i++)
		m_composed_volume[i] = 0;

	m_composed_sample_index = 0;
	m_last_update_time = machine().time();
	m_channel_sample_period = HZ_TO_ATTOSECONDS(machine().sample_rate());
	m_channel_sample_period_secfrac = ATTOSECONDS_TO_DOUBLE(m_channel_sample_period);
	m_interm_sample_period = m_channel_sample_period / RATE_MULTIPLIER;
	m_interm_sample_period_secfrac = ATTOSECONDS_TO_DOUBLE(m_interm_sample_period);
	m_channel_last_sample_time = m_channel->sample_time();
	m_channel_next_sample_time = m_channel_last_sample_time + attotime(0, m_channel_sample_period);
	m_next_interm_sample_time = m_channel_last_sample_time + attotime(0, m_interm_sample_period);
	m_interm_sample_index = 0;
	m_prevx = m_prevy = 0.0;

	/* Note: To avoid time drift due to floating point inaccuracies,
	 * it is good if the speaker time synchronizes itself with the stream timing regularly.
	 */

	/* Compute filter kernel; */
	/* (Done for each device though the data is shared...
	 *  No problem really, but should be done as part of system init if I knew how)
	 */
#if 1
	/* This is an approximated sinc (a perfect sinc makes an ideal low-pass filter).
	 * FILTER_STEP determines the cutoff frequency,
	 * which should be below the Nyquist freq, i.e. half the sample rate.
	 * Smaller step => kernel extends in time domain => lower cutoff freq
	 * In this case, with sinc, filter step PI corresponds to the Nyq. freq.
	 * Since we do not get a perfect filter => must lower the cutoff freq some more.
	 * For example, step PI/(2*RATE_MULTIPLIER) corresponds to cutoff freq = sample rate / 4;
	 *    With -samplerate 48000, cutoff freq is ca 12kHz while the Nyq. freq is 24kHz.
	 *    With -samplerate 96000, cutoff freq is ca 24kHz while the Nyq. freq is 48kHz.
	 * For a steeper, more efficient filter, increase FILTER_LENGTH at the expense of CPU usage.
	 */
#define FILTER_STEP  (M_PI / 2 / RATE_MULTIPLIER)
	/* Distribute symmetrically on x axis; center has x=0 if length is odd */
	for (i = 0,             x = (0.5 - FILTER_LENGTH / 2.) * FILTER_STEP;
			i < FILTER_LENGTH;
			i++,                x += FILTER_STEP)
	{
		if (x == 0)
			m_ampl[i] = 1;
		else
			m_ampl[i] = sin(x) / x;
	}
#else
	/* Trivial average filter with poor frequency cutoff properties;
	 * First zero (frequency where amplification=0) = sample rate / filter length
	 * Cutoff frequency approx <= first zero / 2
	 */
	for (i = 0, i < FILTER_LENGTH; i++)
		m_ampl[i] = 1;
#endif

	save_item(NAME(m_level));
	save_item(NAME(m_composed_volume));
	save_item(NAME(m_composed_sample_index));
	save_item(NAME(m_channel_last_sample_time));
	save_item(NAME(m_interm_sample_index));
	save_item(NAME(m_last_update_time));
	save_item(NAME(m_prevx));
	save_item(NAME(m_prevy));

	machine().save().register_postload(save_prepost_delegate(FUNC(speaker_sound_device::speaker_postload), this));
}

void speaker_sound_device::device_reset()
{
	int i;

	m_level = 0;
	for (i = 0; i < FILTER_LENGTH; i++)
		m_composed_volume[i] = 0;

	m_composed_sample_index = 0;
	m_last_update_time = machine().time();
	m_channel_sample_period = HZ_TO_ATTOSECONDS(machine().sample_rate());
	m_channel_sample_period_secfrac = ATTOSECONDS_TO_DOUBLE(m_channel_sample_period);
	m_interm_sample_period = m_channel_sample_period / RATE_MULTIPLIER;
	m_interm_sample_period_secfrac = ATTOSECONDS_TO_DOUBLE(m_interm_sample_period);
	m_channel_last_sample_time = m_channel->sample_time();
	m_channel_next_sample_time = m_channel_last_sample_time + attotime(0, m_channel_sample_period);
	m_next_interm_sample_time = m_channel_last_sample_time + attotime(0, m_interm_sample_period);
	m_interm_sample_index = 0;
	m_prevx = m_prevy = 0.0;
}

void speaker_sound_device::speaker_postload()
{
	m_channel_next_sample_time = m_channel_last_sample_time + attotime(0, m_channel_sample_period);
	m_next_interm_sample_time = m_channel_last_sample_time + attotime(0, m_interm_sample_period);
}

//-------------------------------------------------
//  sound_stream_update - handle a stream update
//-------------------------------------------------

// This can be triggered by the core (based on emulated time) or via level_w().
void speaker_sound_device::sound_stream_update(sound_stream &stream, stream_sample_t **inputs, stream_sample_t **outputs, int samples)
{
	stream_sample_t *buffer = outputs[0];
	int volume = m_levels[m_level];
	double filtered_volume;
	attotime sampled_time = attotime::zero;

	if (samples > 0)
	{
		/* Prepare to update time state */
		sampled_time = attotime(0, m_channel_sample_period);
		if (samples > 1)
			sampled_time *= samples;

		/* Note: since the stream is in the process of being updated,
		 * stream->sample_time() will return the time before the update! (MAME 0.130)
		 * Avoid using it here in order to avoid a subtle dependence on the stream implementation.
		 */
	}

	if (samples-- > 0)
	{
		/* Note that first interm. sample may be composed... */
		filtered_volume = update_interm_samples_get_filtered_volume(volume);

		/* Composite volume is now quantized to the stream resolution */
		*buffer++ = (stream_sample_t)filtered_volume;

		/* Any additional samples will be homogeneous, however may need filtering across samples: */
		while (samples-- > 0)
		{
			filtered_volume = update_interm_samples_get_filtered_volume(volume);
			*buffer++ = (stream_sample_t)filtered_volume;
		}

		/* Update the time state */
		m_channel_last_sample_time += sampled_time;
		m_channel_next_sample_time = m_channel_last_sample_time + attotime(0, m_channel_sample_period);
		m_next_interm_sample_time = m_channel_last_sample_time + attotime(0, m_interm_sample_period);
		m_last_update_time = m_channel_last_sample_time;
	}
}



void speaker_sound_device::level_w(int new_level)
{
	int volume;
	attotime time;

	if (new_level == m_level)
		return;

	if (new_level < 0)
		new_level = 0;
	else
	if (new_level >= m_num_levels)
		new_level = m_num_levels - 1;

	volume = m_levels[m_level];
	time = machine().time();

	if (time < m_channel_next_sample_time)
	{
		/* Stream sample is yet unfinished, but we may have one or more interm. samples */
		update_interm_samples(time, volume);

		/* Do not forget to update speaker state before returning! */
		m_level = new_level;
		return;
	}
	/* Reaching here means such time has passed since last stream update
	 * that we can add at least one complete sample to the stream.
	 * The details have to be handled by speaker_sound_update()
	 */

	/* Force streams.c to update sound until this point in time now */
	m_channel->update();

	/* This is redundant because time update has to be done within speaker_sound_update() anyway,
	 * however this ensures synchronization between the speaker and stream timing:
	 */
	m_channel_last_sample_time = m_channel->sample_time();
	m_channel_next_sample_time = m_channel_last_sample_time + attotime(0, m_channel_sample_period);
	m_next_interm_sample_time = m_channel_last_sample_time + attotime(0, m_interm_sample_period);
	m_last_update_time = m_channel_last_sample_time;

	/* Assertion: time - last_update_time < channel_sample_period, i.e. time < channel_next_sample_time */

	/* The overshooting fraction of time will make zero, one or more interm. samples: */
	update_interm_samples(time, volume);

	/* Finally update speaker state before returning */
	m_level = new_level;

}


void speaker_sound_device::update_interm_samples(const attotime &time, int volume)
{
	double fraction;

	/* We may have completed zero, one or more interm. samples: */
	while (time >= m_next_interm_sample_time)
	{
		/* First interm. sample may be composed, subsequent samples will be homogeneous. */
		/* Treat all the same general way. */
		finalize_interm_sample(volume);
		init_next_interm_sample();
	}
	/* Depending on status above:
	 * a) Add latest fraction to unfinished composed sample
	 * b) The overshooting fraction of time will start a new composed sample
	 */
	fraction = make_fraction(time, m_last_update_time, m_interm_sample_period_secfrac);
	m_composed_volume[m_composed_sample_index] += volume * fraction;
	m_last_update_time = time;
}


double speaker_sound_device::update_interm_samples_get_filtered_volume(int volume)
{
	double filtered_volume, tempx;

	/* We may have one or more interm. samples to go */
	if (m_interm_sample_index < RATE_MULTIPLIER)
	{
		/* First interm. sample may be composed. */
		finalize_interm_sample(volume);

		/* Subsequent interm. samples will be homogeneous. */
		while (m_interm_sample_index + 1 < RATE_MULTIPLIER)
		{
			init_next_interm_sample();
			m_composed_volume[m_composed_sample_index] = volume;
		}
	}
	/* Important: next interm. sample not initialised yet, so that no data is destroyed before filtering... */
	filtered_volume = get_filtered_volume();
	init_next_interm_sample();
	/* Reset counter to next stream sample: */
	m_interm_sample_index = 0;

	/* simple DC blocker filter */
	tempx = filtered_volume;
	filtered_volume = tempx - m_prevx + 0.995 * m_prevy;
	m_prevx = tempx;
	m_prevy = filtered_volume;

	return filtered_volume;
}


void speaker_sound_device::finalize_interm_sample(int volume)
{
	double fraction;

	/* Fill the composed sample up if it was incomplete */
	fraction = make_fraction(m_next_interm_sample_time, m_last_update_time, m_interm_sample_period_secfrac);
	m_composed_volume[m_composed_sample_index] += volume * fraction;
	/* Update time state */
	m_last_update_time = m_next_interm_sample_time;
	m_next_interm_sample_time += attotime(0, m_interm_sample_period);

	/* For compatibility with filtering, do not incr. index and initialise next sample yet. */
}


void speaker_sound_device::init_next_interm_sample()
{
	/* Move the index and initialize next composed sample */
	m_composed_sample_index++;
	if (m_composed_sample_index >= FILTER_LENGTH)
		m_composed_sample_index = 0;
	m_composed_volume[m_composed_sample_index] = 0;

	m_interm_sample_index++;
	/* No limit check on interm_sample_index here - to be handled by caller */
}


inline double speaker_sound_device::make_fraction(const attotime &a, const attotime &b, double timediv)
{
	/* fraction = (a - b) / timediv */
	return (a - b).as_double() / timediv;
}


double speaker_sound_device::get_filtered_volume()
{
	double filtered_volume = 0;
	double ampsum = 0;
	int i, c;

	/* Filter over composed samples (each composed sample is already average filtered) */
	for (i = m_composed_sample_index + 1, c = 0; c < FILTER_LENGTH; i++, c++)
	{
		if (i >= FILTER_LENGTH) i = 0;
		filtered_volume += m_composed_volume[i] * m_ampl[c];
		ampsum += m_ampl[c];
	}
	filtered_volume /= ampsum;

	return filtered_volume;
}