summaryrefslogtreecommitdiffstatshomepage
path: root/trunk/src/emu/sound/speaker.c
diff options
context:
space:
mode:
Diffstat (limited to 'trunk/src/emu/sound/speaker.c')
-rw-r--r--trunk/src/emu/sound/speaker.c447
1 files changed, 447 insertions, 0 deletions
diff --git a/trunk/src/emu/sound/speaker.c b/trunk/src/emu/sound/speaker.c
new file mode 100644
index 00000000000..25d5eb1b8be
--- /dev/null
+++ b/trunk/src/emu/sound/speaker.c
@@ -0,0 +1,447 @@
+/***************************************************************************
+
+ 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 "speaker.h"
+
+static const INT16 default_levels[2] = {0, 32767};
+
+/* Filter properties shared by all speaker devices:
+ */
+/* Length of anti-aliasing filter kernel, measured in number of intermediate samples */
+enum {FILTER_LENGTH = 64};
+/* Kernel (pulse response) for filtering across samples (while we avoid fancy filtering within samples) */
+static double ampl[FILTER_LENGTH];
+/* Internal oversampling factor (interm. samples vs stream samples) */
+static const int RATE_MULTIPLIER = 4;
+
+typedef struct _speaker_state speaker_state;
+struct _speaker_state
+{
+ sound_stream *channel;
+ const INT16 *levels;
+ int num_levels;
+ int level;
+
+ /* The volume of a composed sample grows incrementally each time the speaker is over-sampled.
+ * That is in effect a basic average filter.
+ * Another filter can and will be applied to the array of composed samples.
+ */
+ double composed_volume[FILTER_LENGTH]; /* integrator(s) */
+ int composed_sample_index; /* array index for composed_volume */
+ attoseconds_t channel_sample_period; /* in as */
+ double channel_sample_period_secfrac; /* in fraction of second */
+ attotime channel_last_sample_time;
+ attotime channel_next_sample_time;
+ attoseconds_t interm_sample_period;
+ double interm_sample_period_secfrac;
+ attotime next_interm_sample_time;
+ int interm_sample_index; /* counts interm. samples between stream samples */
+ attotime last_update_time; /* internal timestamp */
+};
+
+
+static STREAM_UPDATE( speaker_sound_update );
+
+/* Updates the composed volume array according to time */
+static void update_interm_samples(speaker_state *sp, attotime time, int volume);
+
+/* Updates the composed volume array and returns final filtered volume of next stream sample */
+static double update_interm_samples_get_filtered_volume(speaker_state *sp, int volume);
+
+/* Local helpers */
+static void finalize_interm_sample(speaker_state *sp, int volume);
+static void init_next_interm_sample(speaker_state *sp);
+static double make_fraction(attotime a, attotime b, double timediv);
+static double get_filtered_volume(speaker_state *sp);
+
+
+INLINE speaker_state *get_safe_token(device_t *device)
+{
+ assert(device != NULL);
+ assert(device->type() == SPEAKER_SOUND);
+ return (speaker_state *)downcast<legacy_device_base *>(device)->token();
+}
+
+
+static DEVICE_START( speaker )
+{
+ speaker_state *sp = get_safe_token(device);
+ const speaker_interface *intf = (const speaker_interface *) device->static_config();
+ int i;
+ double x;
+
+ sp->channel = device->machine().sound().stream_alloc(*device, 0, 1, device->machine().sample_rate(), sp, speaker_sound_update);
+
+ if (intf != NULL)
+ {
+ assert(intf->num_level > 1);
+ assert(intf->levels != NULL);
+ sp->num_levels = intf->num_level;
+ sp->levels = intf->levels;
+ }
+ else
+ {
+ sp->num_levels = 2;
+ sp->levels = default_levels;
+ }
+
+ sp->level = 0;
+ for (i = 0; i < FILTER_LENGTH; i++)
+ sp->composed_volume[i] = 0;
+ sp->composed_sample_index = 0;
+ sp->last_update_time = device->machine().time();
+ sp->channel_sample_period = HZ_TO_ATTOSECONDS(device->machine().sample_rate());
+ sp->channel_sample_period_secfrac = ATTOSECONDS_TO_DOUBLE(sp->channel_sample_period);
+ sp->interm_sample_period = sp->channel_sample_period / RATE_MULTIPLIER;
+ sp->interm_sample_period_secfrac = ATTOSECONDS_TO_DOUBLE(sp->interm_sample_period);
+ sp->channel_last_sample_time = sp->channel->sample_time();
+ sp->channel_next_sample_time = sp->channel_last_sample_time + attotime(0, sp->channel_sample_period);
+ sp->next_interm_sample_time = sp->channel_last_sample_time + attotime(0, sp->interm_sample_period);
+ sp->interm_sample_index = 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)
+ ampl[i] = 1;
+ else
+ 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++)
+ ampl[i] = 1;
+#endif
+}
+
+
+/* Called via stream->update().
+ * This can be triggered by the core (based on emulated time) or via speaker_level_w().
+ */
+static STREAM_UPDATE( speaker_sound_update )
+{
+ speaker_state *sp = (speaker_state *) param;
+ stream_sample_t *buffer = outputs[0];
+ int volume = sp->levels[sp->level];
+ double filtered_volume;
+ attotime sampled_time = attotime::zero;
+
+ if (samples > 0)
+ {
+ /* Prepare to update time state */
+ sampled_time = attotime(0, sp->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(sp, 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(sp, volume);
+ *buffer++ = (stream_sample_t)filtered_volume;
+ }
+
+ /* Update the time state */
+ sp->channel_last_sample_time += sampled_time;
+ sp->channel_next_sample_time = sp->channel_last_sample_time + attotime(0, sp->channel_sample_period);
+ sp->next_interm_sample_time = sp->channel_last_sample_time + attotime(0, sp->interm_sample_period);
+ sp->last_update_time = sp->channel_last_sample_time;
+ }
+
+} /* speaker_sound_update */
+
+
+void speaker_level_w(device_t *device, int new_level)
+{
+ speaker_state *sp = get_safe_token(device);
+ int volume;
+ attotime time;
+
+ if (new_level == sp->level)
+ return;
+
+ if (new_level < 0)
+ new_level = 0;
+ else
+ if (new_level >= sp->num_levels)
+ new_level = sp->num_levels - 1;
+
+ volume = sp->levels[sp->level];
+ time = device->machine().time();
+
+ if (time < sp->channel_next_sample_time)
+ {
+ /* Stream sample is yet unfinished, but we may have one or more interm. samples */
+ update_interm_samples(sp, time, volume);
+
+ /* Do not forget to update speaker state before returning! */
+ sp->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 */
+ sp->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:
+ */
+ sp->channel_last_sample_time = sp->channel->sample_time();
+ sp->channel_next_sample_time = sp->channel_last_sample_time + attotime(0, sp->channel_sample_period);
+ sp->next_interm_sample_time = sp->channel_last_sample_time + attotime(0, sp->interm_sample_period);
+ sp->last_update_time = sp->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(sp, time, volume);
+
+ /* Finally update speaker state before returning */
+ sp->level = new_level;
+
+} /* speaker_level_w */
+
+
+static void update_interm_samples(speaker_state *sp, attotime time, int volume)
+{
+ double fraction;
+
+ /* We may have completed zero, one or more interm. samples: */
+ while (time >= sp->next_interm_sample_time)
+ {
+ /* First interm. sample may be composed, subsequent samples will be homogeneous. */
+ /* Treat all the same general way. */
+ finalize_interm_sample(sp, volume);
+ init_next_interm_sample(sp);
+ }
+ /* 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, sp->last_update_time, sp->interm_sample_period_secfrac);
+ sp->composed_volume[sp->composed_sample_index] += volume * fraction;
+ sp->last_update_time = time;
+}
+
+
+static double update_interm_samples_get_filtered_volume(speaker_state *sp, int volume)
+{
+ double filtered_volume;
+
+ /* We may have one or more interm. samples to go */
+ if (sp->interm_sample_index < RATE_MULTIPLIER)
+ {
+ /* First interm. sample may be composed. */
+ finalize_interm_sample(sp, volume);
+
+ /* Subsequent interm. samples will be homogeneous. */
+ while (sp->interm_sample_index + 1 < RATE_MULTIPLIER)
+ {
+ init_next_interm_sample(sp);
+ sp->composed_volume[sp->composed_sample_index] = volume;
+ }
+ }
+ /* Important: next interm. sample not initialised yet, so that no data is destroyed before filtering... */
+ filtered_volume = get_filtered_volume(sp);
+ init_next_interm_sample(sp);
+ /* Reset counter to next stream sample: */
+ sp->interm_sample_index = 0;
+
+ return filtered_volume;
+}
+
+
+static void finalize_interm_sample(speaker_state *sp, int volume)
+{
+ double fraction;
+
+ /* Fill the composed sample up if it was incomplete */
+ fraction = make_fraction(sp->next_interm_sample_time,
+ sp->last_update_time,
+ sp->interm_sample_period_secfrac);
+ sp->composed_volume[sp->composed_sample_index] += volume * fraction;
+ /* Update time state */
+ sp->last_update_time = sp->next_interm_sample_time;
+ sp->next_interm_sample_time += attotime(0, sp->interm_sample_period);
+
+ /* For compatibility with filtering, do not incr. index and initialise next sample yet. */
+}
+
+
+static void init_next_interm_sample(speaker_state *sp)
+{
+ /* Move the index and initialize next composed sample */
+ sp->composed_sample_index++;
+ if (sp->composed_sample_index >= FILTER_LENGTH)
+ sp->composed_sample_index = 0;
+ sp->composed_volume[sp->composed_sample_index] = 0;
+
+ sp->interm_sample_index++;
+ /* No limit check on interm_sample_index here - to be handled by caller */
+}
+
+
+static double make_fraction(attotime a, attotime b, double timediv)
+{
+ /* fraction = (a - b) / timediv */
+ return (a - b).as_double() / timediv;
+}
+
+
+static double get_filtered_volume(speaker_state *sp)
+{
+ double filtered_volume = 0;
+ double ampsum = 0;
+ int i, c;
+
+ /* Filter over composed samples (each composed sample is already average filtered) */
+ for (i = sp->composed_sample_index + 1, c = 0; c < FILTER_LENGTH; i++, c++)
+ {
+ if (i >= FILTER_LENGTH) i = 0;
+ filtered_volume += sp->composed_volume[i] * ampl[c];
+ ampsum += ampl[c];
+ }
+ filtered_volume /= ampsum;
+
+ return filtered_volume;
+}
+
+
+/**************************************************************************
+ * Generic get_info
+ **************************************************************************/
+
+DEVICE_GET_INFO( speaker_sound )
+{
+ switch (state)
+ {
+ /* --- the following bits of info are returned as 64-bit signed integers --- */
+ case DEVINFO_INT_TOKEN_BYTES: info->i = sizeof(speaker_state); break;
+
+ /* --- the following bits of info are returned as pointers to data or functions --- */
+ case DEVINFO_FCT_START: info->start = DEVICE_START_NAME( speaker ); break;
+ case DEVINFO_FCT_STOP: /* nothing */ break;
+ case DEVINFO_FCT_RESET: /* nothing */ break;
+
+ /* --- the following bits of info are returned as NULL-terminated strings --- */
+ case DEVINFO_STR_NAME: strcpy(info->s, "Speaker"); break;
+ case DEVINFO_STR_FAMILY: strcpy(info->s, "Speaker"); break;
+ case DEVINFO_STR_VERSION: strcpy(info->s, "1.0"); break;
+ case DEVINFO_STR_SOURCE_FILE: strcpy(info->s, __FILE__); break;
+ case DEVINFO_STR_CREDITS: strcpy(info->s, "Copyright The MESS Team"); break;
+ }
+}
+
+
+DEFINE_LEGACY_SOUND_DEVICE(SPEAKER_SOUND, speaker_sound);