// 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;
}