summaryrefslogtreecommitdiffstatshomepage
path: root/src/emu/sound/speaker.c
blob: 0a6783960c56e7e8528bbd06b50b51385e0c961a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
/***************************************************************************

    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/speaker.h"

static const INT16 default_levels[2] = {0, 32767};

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


const device_type SPEAKER_SOUND = &device_creator<speaker_sound_device>;

speaker_sound_device::speaker_sound_device(const machine_config &mconfig, const char *tag, device_t *owner, UINT32 clock)
					: device_t(mconfig, SPEAKER_SOUND, "Filtered 1-bit DAC", tag, owner, clock),
						device_sound_interface(mconfig, *this)
{
}

//-------------------------------------------------
//  device_config_complete - perform any
//  operations now that the configuration is
//  complete
//-------------------------------------------------

void speaker_sound_device::device_config_complete()
{
	// inherit a copy of the static data
	const speaker_interface *intf = reinterpret_cast<const speaker_interface *>(static_config());
	if (intf != NULL)
		*static_cast<speaker_interface *>(this) = *intf;
	
	// or initialize to defaults if none provided
	else
	{
		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(), this);

	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;

	/* 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));
	
	machine().save().register_postload(save_prepost_delegate(FUNC(speaker_sound_device::speaker_postload), this));
}

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(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;

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

	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(attotime a, 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;
}