summaryrefslogtreecommitdiffstatshomepage
path: root/src/devices/sound/cem3340.cpp
blob: 48a3761e41af90c5b6dce21ea54848769bd403a3 (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
// license:BSD-3-Clause
// copyright-holders:m1macrophage

#include "emu.h"
#include "cem3340.h"

namespace {

constexpr float VT = 25.7E-3F;  // Thermal voltage constant at 25 deg C.

// Recommended voltage supply and component values in the datasheet. If a
// design deviates meaningfully from these, they can be made configurable.

constexpr float VCC = 15.0F;  // Positive supply voltage.
constexpr float RS = 1.8E3F;  // Resistor between pin 18 and GND.
constexpr float RT = 5.6E3F;  // Resistor between pin 2 and pin 3.

// According to the datasheet, RZ should be trimmed so that:
// pin 2 current = pin 1 current. Or: 22 * VT / RT = 3 / RZ
constexpr float RZ = 3 * RT / (22 * VT);  // Total resistance between pin 1 and pin 3.

constexpr float PW_MAX = VCC / 3.0F;

// See "supplies" section in the datasheet. The minimum for both triangle and
// ramp is always 0.
constexpr float TRIANGLE_MAX = VCC / 3.0F;
constexpr float TRIANGLE_MIN = 0.0F;
constexpr float RAMP_MAX = 2.0F * VCC / 3.0F;
constexpr float RAMP_MIN = 0.0F;

// Computing the exact PULSE max is somewhat involved, and depends on both:
// the pulldown resistor at the PULSE output, and the pulldown voltage (see
// "waveform outputs" section). Using the nominal value provided in the
// "supplies" section instead.
constexpr float PULSE_MAX = VCC - 1.5F;

// Typical configurations pull down to GND. See info in the "waveform outputs"
// section of the datasheet, if you need to make this configurable.
constexpr float PULSE_MIN = 0;

}  // anonymous namespace


cem3340_device::cem3340_device(const machine_config &mconfig, const char *tag, device_t *owner, float cf, float rr)
	: device_t(mconfig, CEM3340, tag, owner, 0)
	, device_sound_interface(mconfig, *this)
	, m_cf(cf)
	, m_rr(rr)
	, m_stream(nullptr)
	, m_freq_cc(-1)
	, m_freq(10.0F)
	, m_pw_cv(-1)
	, m_pw(0.5F)
	, m_step(0.0F)
	, m_phase(0.0F)
{
}

cem3340_device::cem3340_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock)
	: cem3340_device(mconfig, tag, owner, 1000E-12F, 1.5E6F)  // Values from the datasheet.
{
}

void cem3340_device::set_freq_cc_internal(float freq_cc)
{
	if (freq_cc == m_freq_cc)
		return;

	// Equations shown and/or described in the datasheet.
	const float iom = (22.0F * VT / RT) * (1.0F - freq_cc * RZ / 3.0F);  // Output current of the multiplier.
	const float vb = iom * RS;  // Voltage at the base of the exponential converter.
	const float iref = VCC / m_rr;  // Reference input current at pin 13.
	const float ieg = iref * expf(-vb / VT);  // Output current of the exponential converter.
	m_freq = 3.0F * ieg / (2.0F * VCC * m_cf);  // Oscillation frequency.
	m_step = m_freq / float(m_stream->sample_rate());
	m_freq_cc = freq_cc;
}

void cem3340_device::set_pw_cv_internal(float pw_cv)
{
	if (pw_cv == m_pw_cv)
		return;
	m_pw = std::clamp(pw_cv, 0.0F, PW_MAX) / PW_MAX;
	m_pw_cv = pw_cv;
}

cem3340_device &cem3340_device::set_freq_cc(float freq_cc)
{
	if (!m_stream)  // Need to know the sample rate.
		fatalerror("%s: set_freq_cc() cannot be called before device_start().\n", tag());
	if (freq_cc == m_freq_cc)
		return *this;
	m_stream->update();
	set_freq_cc_internal(freq_cc);
	return *this;
}

cem3340_device &cem3340_device::set_pw_cv(float pw_cv)
{
	if (pw_cv == m_pw_cv)
		return *this;
	if (m_stream)
		m_stream->update();
	set_pw_cv_internal(pw_cv);
	return *this;
}

float cem3340_device::freq()
{
	if (BIT(get_sound_requested_inputs_mask(), INPUT_FREQ))
		m_stream->update();
	return m_freq;
}

attotime cem3340_device::ramp_time_to_thresh(float threshold)
{
	m_stream->update();

	float remaining = 0.0F;
	const float thresh_phase = (threshold - RAMP_MIN) / (RAMP_MAX - RAMP_MIN);
	if (m_phase < thresh_phase)
		remaining = thresh_phase - m_phase;
	else
		remaining = 1.0F - m_phase + thresh_phase;

	const float t = remaining / m_freq;
	return attotime::from_double(t);
}

void cem3340_device::device_start()
{
	m_stream = stream_alloc(get_sound_requested_inputs(), get_sound_requested_outputs(), machine().sample_rate());
	save_item(NAME(m_freq_cc));
	save_item(NAME(m_freq));
	save_item(NAME(m_pw_cv));
	save_item(NAME(m_pw));
	save_item(NAME(m_step));
	save_item(NAME(m_phase));
}

// Implementation is based on:
// https://www.martin-finke.de/articles/audio-plugins-018-polyblep-oscillator/
float cem3340_device::poly_blep(float phase) const
{
	float val = 0;
	if (phase < m_step)
	{
		const float t = phase / m_step;
		val = t + t - t * t - 1.0F;
	}
	else if (phase > 1.0F - m_step)
	{
		const float t = (phase - 1.0F) / m_step;
		val = t + t + t * t + 1.0F;
	}
	return val;
}

// Implementation is based on:
// https://dsp.stackexchange.com/questions/54790/polyblamp-anti-aliasing-in-c
float cem3340_device::poly_blamp(float phase) const
{
	float y = 0.0F;
	if (0.0F <= phase && phase < 2.0F * m_step)
	{
		const float x = phase / m_step;
		const float u = 2.0F - x;
		const float u2 = u * u;
		y -= u * u2 * u2;
		if (phase < m_step)
		{
			const float v = 1.0F - x;
			const float v2 = v * v;
			y += 4.0F * v * v2 * v2;
		}
	}
	return y * m_step / 15.0F;
}

// Converts from [-1, 1] to [min_value, max_value]
static inline float transform(float x, float min_value, float max_value)
{
	return (max_value - min_value) * (x + 1.0F) / 2.0F + min_value;
}

// A faster way to do fmod(x, 1.0F). At the time of this writing, the "-bench"
// speed on the prophet5 (11 CEM3340s) improves from ~1480% to ~1680% when
// switching from fmodf(x, 1.0F) to fmodf1(x).
static inline float fmodf1(float x)
{
	// No need to worry about a negative `x` in this application.
	if (x >= 1.0F)
		x -= floorf(x);
	return x;
}

void cem3340_device::sound_stream_update(sound_stream &stream)
{
	const bool streaming_freq = BIT(get_sound_requested_inputs_mask(), INPUT_FREQ);
	const bool streaming_pw = BIT(get_sound_requested_inputs_mask(), INPUT_PW);

	const bool tri_out = BIT(get_sound_requested_outputs_mask(), OUTPUT_TRIANGLE);
	const bool ramp_out = BIT(get_sound_requested_outputs_mask(), OUTPUT_RAMP);
	const bool pulse_out = BIT(get_sound_requested_outputs_mask(), OUTPUT_PULSE);

	const int n = stream.samples();

	for (int i = 0; i < n; ++i)
	{
		if (streaming_freq)
			set_freq_cc_internal(stream.get(INPUT_FREQ, i));
		if (streaming_pw)
			set_pw_cv_internal(stream.get(INPUT_PW, i));

		// Uses the PolyBLEP (for ramp and pulse) and PolyBLAMP (for triangle)
		// algorithms to generate anti-aliased waveforms. Those algorithms start
		// with the "naive" versions of the waveforms, and then apply
		// corrections at waveform discontinuities.
		// See references in poly_blep() and poly_blamp().

		// Needed for both the ramp and triangle waveforms.
		const float naive_ramp = 2.0F * m_phase - 1.0F;  // [-1, 1]

		if (ramp_out)
		{
			const float ramp = naive_ramp - poly_blep(m_phase);
			stream.put(OUTPUT_RAMP, i, transform(ramp, RAMP_MIN, RAMP_MAX));
		}

		if (pulse_out)
		{
			// The pulse waveform is generated by a comparator on the ramp
			// waveform, wired such that it goes high when the PW CV is larger
			// than the (scaled) ramp waveform.
			float pulse = (m_phase < m_pw) ? 1.0F : -1.0F;
			pulse += poly_blep(m_phase);
			pulse -= poly_blep(fmodf1(m_phase + (1.0F - m_pw)));
			stream.put(OUTPUT_PULSE, i, transform(pulse, PULSE_MIN, PULSE_MAX));
		}

		if (tri_out)
		{
			// See reference in poly_blamp(). Note that the signs of the
			// corrections below are opposite of those in the reference, because
			// the triangle wave in the reference is inverted.

			float triangle = 1.0F - 2.0F * fabsf(naive_ramp);

			// Correction at the bottom corner of the triangle.
			triangle -= poly_blamp(m_phase);
			triangle -= poly_blamp(1.0F - m_phase);

			// Correction at the top corner of the triangle.
			const float peak_phase = fmodf1(m_phase + 0.5F);
			triangle += poly_blamp(peak_phase);
			triangle += poly_blamp(1.0F - peak_phase);

			stream.put(OUTPUT_TRIANGLE, i, transform(triangle, TRIANGLE_MIN, TRIANGLE_MAX));
		}

		m_phase = fmodf1(m_phase + m_step);
	}
}

DEFINE_DEVICE_TYPE(CEM3340, cem3340_device, "cem3340", "CEM3340 Voltage Controlled Oscillator")