// license:BSD-3-Clause // copyright-holders:Aaron Giles /*************************************************************************** Curtis Electromusic Specialties CEM3394 µP-Controllable Synthesizer Voice This driver handles CEM-3394 analog synth chip. Very crudely. Still to do: - adjust the overall volume when multiple waves are being generated - filter internal sound - support resonance (don't understand how it works) ***************************************************************************/ #include "emu.h" #include "cem3394.h" #include /* waveform generation parameters */ #define ENABLE_PULSE 1 #define ENABLE_TRIANGLE 1 #define ENABLE_SAWTOOTH 1 #define ENABLE_EXTERNAL 1 /* pulse shaping parameters */ /* examples: */ /* hat trick - skidding ice sounds too loud if minimum width is too big */ /* snake pit - melody during first level too soft if minimum width is too small */ /* snake pit - bonus counter at the end of level */ /* snacks'n jaxson - laugh at end of level is too soft if minimum width is too small */ #define LIMIT_WIDTH 1 #define MINIMUM_WIDTH 0.25 #define MAXIMUM_WIDTH 0.75 /******************************************************************************** From the datasheet: VCO_FREQUENCY: -4.0 ... +4.0 -0.75 V/octave f = exp(V) * 431.894 MODULATION_AMOUNT 0.0 ... +3.5 0.0 == 0.01 x frequency 3.5 == 2.00 x frequency WAVE_SELECT -0.5 ... -0.2 == triangle +0.9 ... +1.5 == triangle + sawtooth +2.3 ... +3.9 == sawtooth PULSE_WIDTH 0.0 ... +2.0 0.0 == 0% duty cycle +2.0 == 100% duty cycle MIXER_BALANCE -4.0 ... +4.0 0.0 both at -6dB -20 dB/V FILTER_RESONANCE 0.0 ... +2.5 0.0 == no resonance +2.5 == oscillation FILTER_FREQENCY -3.0 ... +4.0 -0.375 V/octave 0.0 == 1300Hz FINAL_GAIN 0.0 ... +4.0 -20 dB/V 0.0 == -90dB 4.0 == 0dB Square wave output = 160 (average is constant regardless of duty cycle) Sawtooth output = 200 Triangle output = 250 Sawtooth + triangle output = 330 Maximum output = 400 ********************************************************************************/ // various waveforms #define WAVE_TRIANGLE 1 #define WAVE_SAWTOOTH 2 #define WAVE_PULSE 4 // keep lots of fractional bits #define FRACTION_BITS 28 #define FRACTION_ONE (1 << FRACTION_BITS) #define FRACTION_ONE_D ((double)(1 << FRACTION_BITS)) #define FRACTION_MASK (FRACTION_ONE - 1) #define FRACTION_MULT(a,b) (((a) >> (FRACTION_BITS / 2)) * ((b) >> (FRACTION_BITS - FRACTION_BITS / 2))) // device type definition DEFINE_DEVICE_TYPE(CEM3394, cem3394_device, "cem3394", "CEM3394 Synthesizer Voice") //************************************************************************** // LIVE DEVICE //************************************************************************** //------------------------------------------------- // cem3394_device - constructor //------------------------------------------------- cem3394_device::cem3394_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : device_t(mconfig, CEM3394, tag, owner, clock), device_sound_interface(mconfig, *this), m_ext_cb(*this), m_stream(nullptr), m_vco_zero_freq(0.0), m_filter_zero_freq(0.0), m_wave_select(0), m_volume(0), m_mixer_internal(0), m_mixer_external(0), m_position(0), m_step(0), m_filter_position(0), m_filter_step(0), m_modulation_depth(0), m_last_ext(0), m_pulse_width(0), m_inv_sample_rate(0.0), m_sample_rate(0), m_mixer_buffer(nullptr), m_external_buffer(nullptr) { std::fill(std::begin(m_values), std::end(m_values), 0.0); } //------------------------------------------------- // sound_stream_update - generate sound to the mix buffer in mono //------------------------------------------------- void cem3394_device::sound_stream_update(sound_stream &stream, stream_sample_t **inputs, stream_sample_t **outputs, int samples) { int int_volume = (m_volume * m_mixer_internal) / 256; int ext_volume = (m_volume * m_mixer_external) / 256; uint32_t step = m_step, position, end_position = 0; stream_sample_t *buffer = outputs[0]; int16_t *mix, *ext; int i; /* external volume is effectively 0 if no external function */ if (m_ext_cb.isnull() || !ENABLE_EXTERNAL) ext_volume = 0; /* adjust the volume for the filter */ if (step > m_filter_step) int_volume /= step - m_filter_step; /* bail if nothing's going on */ if (int_volume == 0 && ext_volume == 0) { memset(buffer, 0, sizeof(*buffer) * samples); return; } /* if there's external stuff, fetch and process it now */ if (ext_volume != 0) { uint32_t fposition = m_filter_position, fstep = m_filter_step, depth; int16_t last_ext = m_last_ext; /* fetch the external data */ m_ext_cb(samples, m_external_buffer.get()); /* compute the modulation depth, and adjust fstep to the maximum frequency */ /* we lop off 13 bits of depth so that we can multiply by stepadjust, below, */ /* which has 13 bits of precision */ depth = FRACTION_MULT(fstep, m_modulation_depth); fstep += depth; depth >>= 13; /* "apply" the filter: note this is pretty cheesy; it basically just downsamples the external sample to filter_freq by allowing only 2 transitions for every cycle */ for (i = 0, ext = m_external_buffer.get(), position = m_position; i < samples; i++, ext++) { uint32_t newposition; int32_t stepadjust; /* update the position and compute the adjustment from a triangle wave */ if (position & (1 << (FRACTION_BITS - 1))) stepadjust = 0x2000 - ((position >> (FRACTION_BITS - 14)) & 0x1fff); else stepadjust = (position >> (FRACTION_BITS - 14)) & 0x1fff; position += step; /* if we cross a half-step boundary, allow the next byte of the external input */ newposition = fposition + fstep - (stepadjust * depth); if ((newposition ^ fposition) & ~(FRACTION_MASK >> 1)) last_ext = *ext; else *ext = last_ext; fposition = newposition & FRACTION_MASK; } /* update the final filter values */ m_filter_position = fposition; m_last_ext = last_ext; } /* if there's internal stuff, generate it */ if (int_volume != 0) { if (m_wave_select == 0 && !ext_volume) logerror("%f V didn't cut it\n", m_values[WAVE_SELECT]); /* handle the pulse component; it maxes out at 0x1932, which is 27% smaller than */ /* the sawtooth (since the value is constant, this is the best place to have an */ /* odd value for volume) */ if (ENABLE_PULSE && (m_wave_select & WAVE_PULSE)) { uint32_t pulse_width = m_pulse_width; /* if the width is wider than the step, we're guaranteed to hit it once per cycle */ if (pulse_width >= step) { for (i = 0, mix = m_mixer_buffer.get(), position = m_position; i < samples; i++, mix++) { if (position < pulse_width) *mix = 0x1932; else *mix = 0x0000; position = (position + step) & FRACTION_MASK; } } /* otherwise, we compute a volume and watch for cycle boundary crossings */ else { int16_t volume = 0x1932 * pulse_width / step; for (i = 0, mix = m_mixer_buffer.get(), position = m_position; i < samples; i++, mix++) { uint32_t newposition = position + step; if ((newposition ^ position) & ~FRACTION_MASK) *mix = volume; else *mix = 0x0000; position = newposition & FRACTION_MASK; } } end_position = position; } /* otherwise, clear the mixing buffer */ else memset(m_mixer_buffer.get(), 0, sizeof(int16_t) * samples); /* handle the sawtooth component; it maxes out at 0x2000, which is 27% larger */ /* than the pulse */ if (ENABLE_SAWTOOTH && (m_wave_select & WAVE_SAWTOOTH)) { for (i = 0, mix = m_mixer_buffer.get(), position = m_position; i < samples; i++, mix++) { *mix += ((position >> (FRACTION_BITS - 14)) & 0x3fff) - 0x2000; position += step; } end_position = position & FRACTION_MASK; } /* handle the triangle component; it maxes out at 0x2800, which is 25% larger */ /* than the sawtooth (should be 27% according to the specs, but 25% saves us */ /* a multiplication) */ if (ENABLE_TRIANGLE && (m_wave_select & WAVE_TRIANGLE)) { for (i = 0, mix = m_mixer_buffer.get(), position = m_position; i < samples; i++, mix++) { int16_t value; if (position & (1 << (FRACTION_BITS - 1))) value = 0x2000 - ((position >> (FRACTION_BITS - 14)) & 0x1fff); else value = (position >> (FRACTION_BITS - 14)) & 0x1fff; *mix += value + (value >> 2); position += step; } end_position = position & FRACTION_MASK; } /* update the final position */ m_position = end_position; } /* mix it down */ mix = m_mixer_buffer.get(); ext = m_external_buffer.get(); { /* internal + external */ if (ext_volume != 0 && int_volume != 0) { for (i = 0; i < samples; i++, mix++, ext++) *buffer++ = (*mix * int_volume + *ext * ext_volume) / 128; } /* internal only */ else if (int_volume != 0) { for (i = 0; i < samples; i++, mix++) *buffer++ = *mix * int_volume / 128; } /* external only */ else { for (i = 0; i < samples; i++, ext++) *buffer++ = *ext * ext_volume / 128; } } } //------------------------------------------------- // device_start - device-specific startup //------------------------------------------------- void cem3394_device::device_start() { /* copy global parameters */ m_sample_rate = SAMPLE_RATE; m_inv_sample_rate = 1.0 / (double)m_sample_rate; /* allocate stream channels, 1 per chip */ m_stream = stream_alloc(0, 1, m_sample_rate); m_ext_cb.resolve(); /* allocate memory for a mixer buffer and external buffer (1 second should do it!) */ m_mixer_buffer = std::make_unique(m_sample_rate); m_external_buffer = std::make_unique(m_sample_rate); save_item(NAME(m_values)); save_item(NAME(m_wave_select)); save_item(NAME(m_volume)); save_item(NAME(m_mixer_internal)); save_item(NAME(m_mixer_external)); save_item(NAME(m_position)); save_item(NAME(m_step)); save_item(NAME(m_filter_position)); save_item(NAME(m_filter_step)); save_item(NAME(m_modulation_depth)); save_item(NAME(m_last_ext)); save_item(NAME(m_pulse_width)); } double cem3394_device::compute_db(double voltage) { /* assumes 0.0 == full off, 4.0 == full on, with linear taper, as described in the datasheet */ /* above 4.0, maximum volume */ if (voltage >= 4.0) return 0.0; /* below 0.0, minimum volume */ else if (voltage <= 0.0) return 90.0; /* between 2.5 and 4.0, linear from 20dB to 0dB */ else if (voltage >= 2.5) return (4.0 - voltage) * (1.0 / 1.5) * 20.0; /* between 0.0 and 2.5, exponential to 20dB */ else { double temp = 20.0 * pow(2.0, 2.5 - voltage); if (temp < 90.0) return 90.0; else return temp; } } uint32_t cem3394_device::compute_db_volume(double voltage) { double temp; /* assumes 0.0 == full off, 4.0 == full on, with linear taper, as described in the datasheet */ /* above 4.0, maximum volume */ if (voltage >= 4.0) return 256; /* below 0.0, minimum volume */ else if (voltage <= 0.0) return 0; /* between 2.5 and 4.0, linear from 20dB to 0dB */ else if (voltage >= 2.5) temp = (4.0 - voltage) * (1.0 / 1.5) * 20.0; /* between 0.0 and 2.5, exponential to 20dB */ else { temp = 20.0 * pow(2.0, 2.5 - voltage); if (temp < 50.0) return 0; } /* convert from dB to volume and return */ return (uint32_t)(256.0 * pow(0.891251, temp)); } void cem3394_device::set_voltage(int input, double voltage) { double temp; /* don't do anything if no change */ if (voltage == m_values[input]) return; m_values[input] = voltage; /* update the stream first */ m_stream->update(); /* switch off the input */ switch (input) { /* frequency varies from -4.0 to +4.0, at 0.75V/octave */ case VCO_FREQUENCY: temp = m_vco_zero_freq * pow(2.0, -voltage * (1.0 / 0.75)); m_step = (uint32_t)(temp * m_inv_sample_rate * FRACTION_ONE_D); break; /* wave select determines triangle/sawtooth enable */ case WAVE_SELECT: m_wave_select &= ~(WAVE_TRIANGLE | WAVE_SAWTOOTH); if (voltage >= -0.5 && voltage <= -0.2) m_wave_select |= WAVE_TRIANGLE; else if (voltage >= 0.9 && voltage <= 1.5) m_wave_select |= WAVE_TRIANGLE | WAVE_SAWTOOTH; else if (voltage >= 2.3 && voltage <= 3.9) m_wave_select |= WAVE_SAWTOOTH; break; /* pulse width determines duty cycle; 0.0 means 0%, 2.0 means 100% */ case PULSE_WIDTH: if (voltage < 0.0) { m_pulse_width = 0; m_wave_select &= ~WAVE_PULSE; } else { temp = voltage * 0.5; if (LIMIT_WIDTH) temp = MINIMUM_WIDTH + (MAXIMUM_WIDTH - MINIMUM_WIDTH) * temp; m_pulse_width = (uint32_t)(temp * FRACTION_ONE_D); m_wave_select |= WAVE_PULSE; } break; /* final gain is pretty self-explanatory; 0.0 means ~90dB, 4.0 means 0dB */ case FINAL_GAIN: m_volume = compute_db_volume(voltage); break; /* mixer balance is a pan between the external input and the internal input */ /* 0.0 is equal parts of both; positive values favor external, negative favor internal */ case MIXER_BALANCE: if (voltage >= 0.0) { m_mixer_internal = compute_db_volume(3.55 - voltage); m_mixer_external = compute_db_volume(3.55 + 0.45 * (voltage * 0.25)); } else { m_mixer_internal = compute_db_volume(3.55 - 0.45 * (voltage * 0.25)); m_mixer_external = compute_db_volume(3.55 + voltage); } break; /* filter frequency varies from -4.0 to +4.0, at 0.375V/octave */ case FILTER_FREQENCY: temp = m_filter_zero_freq * pow(2.0, -voltage * (1.0 / 0.375)); m_filter_step = (uint32_t)(temp * m_inv_sample_rate * FRACTION_ONE_D); break; /* modulation depth is 0.01 at 0V and 2.0 at 3.5V; how it grows from one to the other */ /* is still unclear at this point */ case MODULATION_AMOUNT: if (voltage < 0.0) m_modulation_depth = (uint32_t)(0.01 * FRACTION_ONE_D); else if (voltage > 3.5) m_modulation_depth = (uint32_t)(2.00 * FRACTION_ONE_D); else m_modulation_depth = (uint32_t)(((voltage * (1.0 / 3.5)) * 1.99 + 0.01) * FRACTION_ONE_D); break; /* this is not yet implemented */ case FILTER_RESONANCE: break; } } double cem3394_device::get_parameter(int input) { double voltage = m_values[input]; switch (input) { case VCO_FREQUENCY: return m_vco_zero_freq * pow(2.0, -voltage * (1.0 / 0.75)); case WAVE_SELECT: return voltage; case PULSE_WIDTH: if (voltage <= 0.0) return 0.0; else if (voltage >= 2.0) return 1.0; else return voltage * 0.5; case FINAL_GAIN: return compute_db(voltage); case MIXER_BALANCE: return voltage * 0.25; case MODULATION_AMOUNT: if (voltage < 0.0) return 0.01; else if (voltage > 3.5) return 2.0; else return (voltage * (1.0 / 3.5)) * 1.99 + 0.01; case FILTER_RESONANCE: if (voltage < 0.0) return 0.0; else if (voltage > 2.5) return 1.0; else return voltage * (1.0 / 2.5); case FILTER_FREQENCY: return m_filter_zero_freq * pow(2.0, -voltage * (1.0 / 0.375)); } return 0.0; }