From 68355ce3b4abc50ca896f14ad45ac9918e303a75 Mon Sep 17 00:00:00 2001 From: m1macrophage <168948267+m1macrophage@users.noreply.github.com> Date: Fri, 14 Nov 2025 23:43:06 -0800 Subject: [PATCH 1/2] sequential/sixtrak.cpp: Emulated audio and promoted to working. New functionality: * Wheel RC circuits. * Autotune circuit. * Audio. Systems promoted to working ---------------------------- Sequential Circuits Six-Trak (Model 610) Rev B/C --- src/devices/sound/va_eg.h | 1 + src/mame/layout/sequential_sixtrak.lay | 14 +- src/mame/sequential/sixtrak.cpp | 646 ++++++++++++++++++++++--- 3 files changed, 598 insertions(+), 63 deletions(-) diff --git a/src/devices/sound/va_eg.h b/src/devices/sound/va_eg.h index 3e57e3a201f31..65fb345d59d49 100644 --- a/src/devices/sound/va_eg.h +++ b/src/devices/sound/va_eg.h @@ -40,6 +40,7 @@ class va_rc_eg_device : public device_t, public device_sound_interface // Sets target voltage to (dis)charge towards. va_rc_eg_device &set_target_v(float v); + float get_target_v() const { return m_v_end; } // Sets the voltage to the given value, instantly. va_rc_eg_device &set_instant_v(float v); diff --git a/src/mame/layout/sequential_sixtrak.lay b/src/mame/layout/sequential_sixtrak.lay index 7be911311049a..b20b27dda20c7 100644 --- a/src/mame/layout/sequential_sixtrak.lay +++ b/src/mame/layout/sequential_sixtrak.lay @@ -755,10 +755,10 @@ copyright-holders:m1macrophage - + - + @@ -888,11 +888,13 @@ copyright-holders:m1macrophage - + + - + + @@ -1050,8 +1052,8 @@ copyright-holders:m1macrophage add_knob(view, "tune_knob", "tune_knob", 2, HORIZONTAL) add_knob(view, "master_volume_knob", "master_volume_knob", 1.2, HORIZONTAL) - add_slider(view, "pitch_wheel", "pitch_wheel_dent", "pitch_wheel") - add_slider(view, "mod_wheel", "mod_wheel_dent", "mod_wheel") + add_slider(view, "pitch_wheel", "pitch_wheel_dent", "wheel_0") + add_slider(view, "mod_wheel", "mod_wheel_dent", "wheel_1") view.items["warning"]:set_state(0) end) diff --git a/src/mame/sequential/sixtrak.cpp b/src/mame/sequential/sixtrak.cpp index 7b7fa5fcb18d2..8c737e25ec651 100644 --- a/src/mame/sequential/sixtrak.cpp +++ b/src/mame/sequential/sixtrak.cpp @@ -21,7 +21,7 @@ takes care of tuning. CVs are generated by a 12-bit DAC (0V - -4V). A MUX selects between the DAC output and its inverted output (0V - 4V), resulting in a 13-bit resolution. However, the full 13-bit resolution is only used for a few CVs. Most CVs just -use the 6 MSbits of the DAC. +use 6 bits. A set of MUXes route the generated CV to the appropriate voice and parameter. Each of the 6 voice chips (CEM3394) has 8 CV-controlled parameters: @@ -37,12 +37,40 @@ Each of the 6 voice chips (CEM3394) has 8 CV-controlled parameters: Known hardware revisions: - Model 610 Rev A: serial numbers 1-900. - Model 610 Rev B: serial numbers 901-?. - Changes to autotune circuit: disconnect U146-2 from whatever it was - connected to (not sure what), and connect it to U146-6. - Unsure if there are other changes. + * MM5837 noise generator replaced with a transistor-based noise source. + * Improved autotune circuit. U146-2 disconnected from U148-5, and connected + to U146-6: the tuning flipflop's D input is connected to the flipflop's + /Q output, instead of being connected to bit 3 of the tuning latch ( + port 0x0d). Improves timing accuracy by removing firmware reaction time + from the equation. + - Model 610 Rev C: serial numbers ?-?. + * No known differences. -This driver is based on the service manual for the Six-Trak (newer than Rev A, -probably for Rev B), and is intended as an educational tool. +This driver is based on the service manual for the Six-Trak (probably Rev C), +and is intended as an educational tool. + + +When the RAM is not initialized, all programs will be silent, the synth will be +out of tune, and the pitch wheel will not be calibrated. + +A basic test patch can be configured by pressing: +CONTROL RECORD + SELECT 8 (C + NUMPAD 8) + +To run the autotune routine, press: +CONTROL RECORD + SELECT 6 (C + NUMPAD 6) + +To calibrate the pitch wheel, leave the wheel centered and press: +CONTROL RECORD + SELECT 3 (C + NUMPAD 3) + +(Not necessary) Further fine-tuning and tuning-related diagnostics can be done +by entering the "tune test" menu: +CONTROL RECORD + SELECT 9 (C + NUMPAD 9) +More info on "tune test" in the service manual. + +To display the firmware version, press: +TRACK RECORD + SELECT 5 (R + NUMPAD 5) + +For all other functionality, see the owner's manual. */ #include "emu.h" @@ -55,6 +83,11 @@ probably for Rev B), and is intended as an educational tool. #include "machine/output_latch.h" #include "machine/pit8253.h" #include "machine/rescap.h" +#include "sound/cem3394.h" +#include "sound/flt_vol.h" +#include "sound/mixer.h" +#include "sound/va_eg.h" +#include "speaker.h" #include "sequential_sixtrak.lh" @@ -62,14 +95,65 @@ probably for Rev B), and is intended as an educational tool. #define LOG_KEYS (1U << 2) #define LOG_ADC_VALUE_KNOB (1U << 3) #define LOG_ADC_PITCH_WHEEL (1U << 4) +#define LOG_VOLUME (1U << 5) +#define LOG_AUTOTUNE (1U << 6) +#define LOG_CALIBRATION (1U << 7) +#define LOG_WHEEL_RC (1U << 8) -#define VERBOSE (LOG_GENERAL | LOG_CV) +#define VERBOSE (LOG_CALIBRATION) //#define LOG_OUTPUT_FUNC osd_printf_info #include "logmacro.h" namespace { +// TODO: Move somewhere in sound/device. +class sixtrak_transistor_noise_device : public device_t, public device_sound_interface +{ +public: + sixtrak_transistor_noise_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock = 0) ATTR_COLD; + +protected: + void sound_stream_update(sound_stream &stream) override; + void device_start() override ATTR_COLD; + +private: + sound_stream *m_stream = nullptr; +}; + +} // anonymous namespace + +DEFINE_DEVICE_TYPE(SIXTRAK_TRANSISTOR_NOISE, sixtrak_transistor_noise_device, "sixtrak_transistor_noise", "Six-Trak PNP-based noise generator") + +sixtrak_transistor_noise_device::sixtrak_transistor_noise_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) + : device_t(mconfig, SIXTRAK_TRANSISTOR_NOISE, tag, owner, clock) + , device_sound_interface(mconfig, *this) +{ +} + +void sixtrak_transistor_noise_device::sound_stream_update(sound_stream &stream) +{ + const int n = stream.samples(); + for (int i = 0; i < n; ++i) + { + // Uniformly distributed random number between -1 and 1. + stream.put(0, i, 2 * (double(machine().rand()) / std::numeric_limits::max() - 0.5)); + } +} + +void sixtrak_transistor_noise_device::device_start() +{ + m_stream = stream_alloc(0, 1, SAMPLE_RATE_OUTPUT_ADAPTIVE); +} + +namespace { + +enum wheel_type +{ + WHEEL_PITCH = 0, + WHEEL_MOD, +}; + class sixtrak_state : public driver_device { public: @@ -77,14 +161,29 @@ class sixtrak_state : public driver_device void sixtrak(machine_config &config) ATTR_COLD; + DECLARE_INPUT_CHANGED_MEMBER(wheel_moved); + DECLARE_INPUT_CHANGED_MEMBER(master_volume_changed); + DECLARE_INPUT_CHANGED_MEMBER(dac_trimmer_changed); + protected: void machine_start() override ATTR_COLD; + void machine_reset() override ATTR_COLD; private: double get_dac_v(bool inverted) const; double get_voltage_mux_out() const; + static void update_sh_rc(bool sampling, double cv, va_rc_eg_device &rc); void update_cvs(); + void update_wheel_rc(int which); + void update_master_volume(); + + void tuning_counter_gate_w(int state); + void tuning_counter_out_changed(int state); + void tuning_ff_comp_out_changed(int state); + void update_tuning_timer(); + TIMER_CALLBACK_MEMBER(tuning_timer_tick); + void dac_low_w(u8 data); void dac_high_w(u8 data); void voice_select_w(u8 data); @@ -98,39 +197,64 @@ class sixtrak_state : public driver_device void io_map(address_map &map) ATTR_COLD; required_device m_maincpu; + required_device m_pit; + required_device m_tuning_ff; // U146A + required_device_array m_voices; + required_device_array m_gain_rc; + required_device_array m_freq_rc; + required_device m_mute; // U147 (CD4049) + required_device m_master_vol; // R197 + required_device_array m_wheel_rc; + required_ioport_array<2> m_wheels; required_ioport_array<16> m_keys; required_ioport m_footswitch; - required_ioport m_pitch_wheel; - required_ioport m_mod_wheel; required_ioport m_track_vol_knob; required_ioport m_speed_knob; required_ioport m_value_knob; required_ioport m_tune_knob; + required_ioport m_master_vol_knob; + required_ioport m_dac_trimmer; output_finder<2> m_digits; + emu_timer *m_tuning_timer = nullptr; + bool m_tuning_counter_clock_started = false; + u8 m_tuning_counter_gate = 0; + u8 m_tuning_counter_out = 0; + u8 m_key_row = 0; u16 m_dac_value = 0; u8 m_sh_voices = 0x3f; u8 m_sh_param = 0; u8 m_voltage_mux_input = 0; - std::array, 6> m_cvs; // 6 voices x 8 parameters. + std::array m_sampling_gain = { false, false, false, false, false, false }; + std::array m_sampling_freq = { false, false, false, false, false, false }; + + static inline constexpr double VPLUS = 5.0; + static inline constexpr double VMINUS = -6.5; }; sixtrak_state::sixtrak_state(const machine_config &mconfig, device_type type, const char *tag) : driver_device(mconfig, type, tag) , m_maincpu(*this, "maincpu") + , m_pit(*this, "pit") + , m_tuning_ff(*this, "tuningff") + , m_voices(*this, "cem3394_%u", 1U) + , m_gain_rc(*this, "gain_rc_%u", 1U) + , m_freq_rc(*this, "freq_rc_%u", 1U) + , m_mute(*this, "mute") + , m_master_vol(*this, "master_volume") + , m_wheel_rc(*this, "wheel_rc_%u", 0U) + , m_wheels(*this, "wheel_%u", 0U) , m_keys(*this, "key_row_%u", 0U) , m_footswitch(*this, "footswitch") - , m_pitch_wheel(*this, "pitch_wheel") - , m_mod_wheel(*this, "mod_wheel") , m_track_vol_knob(*this, "track_volume_knob") , m_speed_knob(*this, "speed_knob") , m_value_knob(*this, "value_knob") , m_tune_knob(*this, "tune_knob") + , m_master_vol_knob(*this, "master_volume_knob") + , m_dac_trimmer(*this, "trimmer_dac_inverter") , m_digits(*this, "digit_%u", 1U) { - for (auto &voice_cvs : m_cvs) - std::fill(voice_cvs.begin(), voice_cvs.end(), 0.0); } double sixtrak_state::get_dac_v(bool inverted) const @@ -142,27 +266,39 @@ double sixtrak_state::get_dac_v(bool inverted) const // A MUX (U108, 4051) selects between the non-inverted and inverted voltages // when updating CVs. This essentially adds 1 bit of precision to the - // 12-bit DAC, for the CVs that need it. But most CVs just use the 6 most - // significant bits of the DAC. The ADC comparator always compares against - // the inverted (0V - 4V) voltage. + // 12-bit DAC, for the CVs that need it. But most CVs just use 6 bits. The + // ADC comparator always compares against the inverted (0V - 4V) voltage. constexpr double DAC_MAX_VOLTAGE = -4.0; constexpr double DAC_MAX_VALUE = double(make_bitmask(12)); const double v = m_dac_value * DAC_MAX_VOLTAGE / DAC_MAX_VALUE; - return inverted ? -v : v; + + if (inverted) + { + // Inversion done by U111A (5532 op-amp) and surrounding resistors. + constexpr double R121 = RES_K(3.01); + constexpr double R123 = RES_K(3.01); + constexpr double R1205 = RES_K(100); // trimmer + constexpr double R1206 = RES_M(2.2); + + const double rp1 = m_dac_trimmer->read() * R1205 / m_dac_trimmer->field(1)->maxval(); + const double rp2 = R1205 - rp1; + // Compute voltage at the junction of all resistors. + const double vx = (R1206 * rp1 * VMINUS + R1206 * rp2 * VPLUS) / (R1206 * rp1 + R1206 * rp2 + rp1 * rp2); + return -R123 * (v / R121 + vx / R1206); // Voltage at U111A's output. + } + else + { + return v; + } } double sixtrak_state::get_voltage_mux_out() const { - // The pitch and mod wheel potentiometers (100K, linear) are attached to - // ground on one side and a shared 27K resistor (R1208) to 5V on the other. - // The pot wipers are attached to corresponding mux inputs. - constexpr double WHEEL_MAX_V = 5.0 * RES_VOLTAGE_DIVIDER(RES_K(27), RES_2_PARALLEL(RES_K(100), RES_K(100))); - // All knobs are 10K linear potentiometers, and each one is attached to // ground on one side and to a separate 2K resistor to 5V on the other. The // pot wipers are attached to corresponding mux inputs. - constexpr double KNOB_MAX_V = 5.0 * RES_VOLTAGE_DIVIDER(RES_K(2), RES_K(10)); + constexpr double KNOB_MAX_V = VPLUS * RES_VOLTAGE_DIVIDER(RES_K(2), RES_K(10)); // The voltage MUX (U108, CD4051) routes potentiometer voltages to the ADC // comparator and routes the appropriate DAC output to the CV S&H circuits. @@ -174,9 +310,9 @@ double sixtrak_state::get_voltage_mux_out() const case 1: return get_dac_v(true); case 2: return m_value_knob->read() * KNOB_MAX_V / m_value_knob->field(1)->maxval(); case 3: return m_tune_knob->read() * KNOB_MAX_V / m_tune_knob->field(1)->maxval(); - case 4: return m_mod_wheel->read() * WHEEL_MAX_V / m_mod_wheel->field(1)->maxval(); + case 4: return m_wheel_rc[WHEEL_MOD]->get_v(); case 5: return m_track_vol_knob->read() * KNOB_MAX_V / m_track_vol_knob->field(1)->maxval(); - case 6: return m_pitch_wheel->read() * WHEEL_MAX_V / m_pitch_wheel->field(1)->maxval(); + case 6: return m_wheel_rc[WHEEL_PITCH]->get_v(); case 7: return m_speed_knob->read() * KNOB_MAX_V / m_speed_knob->field(1)->maxval(); } @@ -184,25 +320,304 @@ double sixtrak_state::get_voltage_mux_out() const return 0; } +// Will only be accurate if called when `sampling` is true, or when `sampling` +// is transitioning to false. Should not be called multiple times in a row with +// sampling == false. +void sixtrak_state::update_sh_rc(bool sampling, double cv, va_rc_eg_device &rc) +{ + // The sample & hold circuits for the filter frequency and VCA gain CVs + // include an RC network. This smoothens the CV updates sent from the + // firmware. + + // When the CV is being sampled, Cl (Clarge) reaches the target voltage + // immediately, while Cs (Csmall) (dis)charges towards the CV via R (1 MOhm). + // The CEM3394 senses the voltage at Cs. + // + // CV ---+--- R ---+--- CEM3394 CV input + // | | + // Cl Cs + // | | + // GND GND + + // When the CV is not being sampled, Cs and Cl will (dis)charge towards the + // same target voltage. The (dis)charge rate will be that of an RC circuit + // where C is the series combination of Cs and Cl. + // + // +--- R ---+--- CEM3394 CV input + // | | + // Cl Cs + // | | + // GND GND + + constexpr double C_SMALL = CAP_U(0.001); + constexpr double C_LARGE = CAP_U(0.01); + constexpr double C_SERIES = (C_LARGE * C_SMALL) / (C_LARGE + C_SMALL); + + if (sampling) + { + rc.set_target_v(cv); + rc.set_c(C_SMALL); + } + else + { + rc.set_target_v((C_LARGE * rc.get_target_v() + C_SMALL * rc.get_v()) / (C_LARGE + C_SMALL)); + rc.set_c(C_SERIES); + } +} + void sixtrak_state::update_cvs() { + constexpr int PARAM2CVIN[8] = + { + cem3394_device::VCO_FREQUENCY, + cem3394_device::FINAL_GAIN, + cem3394_device::FILTER_RESONANCE, + cem3394_device::FILTER_FREQENCY, + cem3394_device::MIXER_BALANCE, + cem3394_device::MODULATION_AMOUNT, + cem3394_device::PULSE_WIDTH, + cem3394_device::WAVE_SELECT, + }; + + constexpr const char *PARAMNAMES[8] = + { + "pitch", "gain", "resonance", "cutoff", "mixer", "mod", "PW", "waveform" + }; + if ((m_sh_voices & 0x3f) == 0x3f) return; // Exit early if no voice S&Hs are activated. + assert(m_sh_param < 8); + const int cv_input = PARAM2CVIN[m_sh_param]; const double cv = get_voltage_mux_out(); + for (int voice = 0; voice < 6; ++voice) { - if (!BIT(m_sh_voices, voice)) // Active low. + const bool voice_active = !BIT(m_sh_voices, voice); + + const bool sampling_gain = voice_active && (m_sh_param == 1); + if (sampling_gain || sampling_gain != m_sampling_gain[voice]) + { + update_sh_rc(sampling_gain, cv, *m_gain_rc[voice]); + } + m_sampling_gain[voice] = sampling_gain; + + const bool sampling_freq = voice_active && (m_sh_param == 3); + if (sampling_freq || sampling_freq != m_sampling_freq[voice]) + { + update_sh_rc(sampling_freq, cv, *m_freq_rc[voice]); + } + m_sampling_freq[voice] = sampling_freq; + + if (!voice_active || sampling_freq || sampling_gain) + continue; + + cem3394_device *v = m_voices[voice]; + if (v->get_voltage(cv_input) == cv) + continue; + + v->set_voltage(cv_input, cv); + LOGMASKED(LOG_CV, "CV - voice: %u, param: %u (%s), cv: %f - %03x - %x\n", + voice, m_sh_param, PARAMNAMES[m_sh_param], cv, m_dac_value, m_voltage_mux_input); + if (m_sh_param == 0) + LOGMASKED(LOG_CV, "Pitch %d: %f\n", voice, v->get_parameter(cem3394_device::VCO_FREQUENCY)); + } +} + +void sixtrak_state::update_wheel_rc(int which) +{ + // The pitch and mod wheel potentiometers (100K, linear) are attached to + // ground on one side and a shared 27K resistor (R1208) to 5V on the other. + // The pot wipers are attached to corresponding mux inputs and capacitors. + + // The diagram on the left is for the pitch wheel (R1) RC circuit. The mod + // wheel circuit is the same, but the capacitor is connected to R2 and a + // different MUX input. The circuit to the right is the equivalent RC + // circuit. For the derivation of Veq and Req, see equations below. + + // V+ Veq + // | | + // R1208 | + // | Req + // +------+ | + // | | | + // | R1t A +----- MUX input + // R2 +------+------ MUX input | + // | R1b C C + // | | | | + // GND GND GND GND + + assert(which == WHEEL_PITCH || which == WHEEL_MOD); + constexpr const char *WHEEL_NAME[2] = {"Pitch", "Mod"}; + + // The real hardware does not use the full range of the wheel potentiometers. + // Using the full range results in erratic behavior. The exact range is not + // known, but the value used here results in reasonable pitch bend behavior + // (+/- ~3 semitones). + constexpr double WHEEL_RANGE = 0.25; + + const s32 value = m_wheels[which]->read(); + if (value <= 0) + { + m_wheel_rc[which]->set_instant_v(0); + LOGMASKED(LOG_WHEEL_RC, "%s wheel RC - V: 0\n", WHEEL_NAME[which]); + return; + } + + // The equations below are for the pitch wheel (R1). But they also work for + // the mod wheel (R2) because R1 == R2. + + constexpr double R1 = RES_K(100); + constexpr double R2 = RES_K(100); + constexpr double R1208 = RES_K(27); + + const double r1_bottom = value * WHEEL_RANGE * R1 / m_wheels[which]->field(1)->maxval(); + const double r1_top = R1 - r1_bottom; + + // To compute Veq, calculate the voltage at point A, ignoring the connection + // to the capacitor. + const double v_eq = VPLUS * RES_VOLTAGE_DIVIDER(R1208, RES_2_PARALLEL(R1, R2)) * RES_VOLTAGE_DIVIDER(r1_top, r1_bottom); + + // To compute Req, consider V+ and GND as being connected to each other, and + // calculate the resistance to that node. Ignore the connection to the + // capacitor. + const double r_eq = RES_2_PARALLEL(r1_bottom, r1_top + RES_2_PARALLEL(R1208, R2)); + + m_wheel_rc[which]->set_r(r_eq); + m_wheel_rc[which]->set_target_v(v_eq); + + LOGMASKED(LOG_WHEEL_RC, "%s wheel RC - R: %f, V: %f\n", WHEEL_NAME[which], r_eq, v_eq); +} + +void sixtrak_state::update_master_volume() +{ + const double proportion = double(m_master_vol_knob->read()) / m_master_vol_knob->field(1)->maxval(); + const double gain = RES_AUDIO_POT_LAW(proportion); + m_master_vol->set_gain(gain); + LOGMASKED(LOG_VOLUME, "Set volume to: %f %f\n", proportion, gain); +} + +void sixtrak_state::tuning_counter_gate_w(int state) +{ + m_tuning_counter_gate = state; + if (!m_tuning_counter_gate) + m_tuning_counter_clock_started = false; + + m_pit->write_gate0(m_tuning_counter_gate); + + if (m_tuning_counter_gate) + { + LOGMASKED(LOG_AUTOTUNE, "Autotune routine started.\n"); + update_tuning_timer(); + } +} + +void sixtrak_state::tuning_counter_out_changed(int state) +{ + m_tuning_counter_out = state; + m_pit->write_gate1(m_tuning_counter_out ? 0 : 1); // Inverted by U151D (74LS04). + + if (m_tuning_counter_out) + { + m_tuning_timer->reset(); + LOGMASKED(LOG_AUTOTUNE, "Autotune routine ended.\n"); + } +} + +void sixtrak_state::tuning_ff_comp_out_changed(int state) +{ + m_tuning_ff->d_w(state); + + // The autotune implementation on the Six-Trak depends on behavior of the + // 8253 that does not seem to be accurately emulated in pit8253.cpp. + // Specifically, the Six-Trak expects that: + // - The counter is incremented on the negative-going edge of the clock. + // - In mode 1, counting will start after the first *full* clock cycle + // (positive-going edge followed by negative-going) after the gate is + // enabled. + + // This implementation works around those issues as follows: + // - The signal to the CLK0 PIT input is inverted. + // - Using `m_tuning_counter_clock_started` to detect the first positive- + // going clock edge after the gate is enabled, and only clocking the PIT + // after this has occurred. + + if (m_tuning_counter_gate && state) + m_tuning_counter_clock_started = true; + + if (m_tuning_counter_clock_started) + m_pit->write_clk0(!state); +} + +void sixtrak_state::update_tuning_timer() +{ + // The tuning circuit uses a comparator (U144, LM311) that senses the + // mixed voice signal and clocks the tuning flipflop (U146A, 74LS174) at the + // frequency of that signal. The flipflop and a PIT (U133A + U133B, 8253) + // are configured to measure the time it takes for a predetermined number + // of signal cycles to occur. + + // Tuning is performed by enabling one voice at a time and measuring the + // frequencies of the VCO and filter. To measure the frequency of the + // filter, the firmware sets the resonance to a high value, to cause + // self-oscillation at the cutoff frequency. + + // This implementation approximates the above by using a timer to clock the + // flipflop at the frequency of the oscillator or filter. The timer + // frequency is determined by finding the loudest voice, and either using + // the frequency of the VCO, or the filter, depending on whether resonance + // is set to a high value. + + int active_voices = 0; + int loudest_voice = -1; + double max_gain_cv = -1; + for (int voice = 0; voice < m_voices.size(); ++voice) + { + const double gain_cv = m_voices[voice]->get_voltage(cem3394_device::FINAL_GAIN); + if (gain_cv > 0.1) + ++active_voices; + if (gain_cv > max_gain_cv) { - assert(m_sh_param < 8); - if (m_cvs[voice][m_sh_param] != cv) - { - m_cvs[voice][m_sh_param] = cv; - LOGMASKED(LOG_CV, "CV - voice: %u, param: %u, cv: %f - %03x - %x\n", - voice, m_sh_param, cv, m_dac_value, m_voltage_mux_input); - } + max_gain_cv = gain_cv; + loudest_voice = voice; } } + + if (loudest_voice < 0) + { + m_tuning_timer->reset(); + logerror("Autotune: no voice selected.\n"); + return; + } + else if (active_voices > 1) + { + logerror("Autotune: multiple voices selected. Using the loudest one.\n"); + } + + cem3394_device *voice = m_voices[loudest_voice]; + double freq = 0; + bool tuning_filter = false; + if (voice->get_parameter(cem3394_device::FILTER_RESONANCE) > 0.9) + { + freq = voice->get_parameter(cem3394_device::FILTER_FREQENCY); + tuning_filter = true; + } + else + { + freq = voice->get_parameter(cem3394_device::VCO_FREQUENCY); + tuning_filter = false; + } + + const attotime t = attotime::from_hz(freq); + m_tuning_timer->adjust(t, 0, t); + LOGMASKED(LOG_AUTOTUNE, "Autotuning voice: %d is_filter: %d frequency: %f\n", + loudest_voice, tuning_filter, freq); +} + +TIMER_CALLBACK_MEMBER(sixtrak_state::tuning_timer_tick) +{ + m_tuning_ff->clock_w(1); + m_tuning_ff->clock_w(0); } void sixtrak_state::dac_low_w(u8 data) @@ -255,9 +670,8 @@ void sixtrak_state::digit_w(u8 data) u8 sixtrak_state::misc_r() { // D0, D1 - autotune-related. - // TODO: emulate autotune. - const u8 d0 = 1; - const u8 d1 = 1; + const u8 d0 = m_tuning_ff->output_comp_r(); + const u8 d1 = m_tuning_counter_out; // D2: ADC comparator. // If either of the MUX inputs B or C are high, they will activate the @@ -275,7 +689,7 @@ u8 sixtrak_state::misc_r() else if (m_voltage_mux_input == 6) { LOGMASKED(LOG_ADC_PITCH_WHEEL, "ADC pitch - input: %d, pot v: %f, dac v: %f, comp: %d\n", - m_pitch_wheel->read(), get_voltage_mux_out(), get_dac_v(true), d2); + m_wheels[WHEEL_PITCH]->read(), get_voltage_mux_out(), get_dac_v(true), d2); } } @@ -334,7 +748,7 @@ void sixtrak_state::io_map(address_map &map) { map.global_mask(0xff); - map(0x00, 0x03).mirror(0xf4).rw("pit", FUNC(pit8253_device::read), FUNC(pit8253_device::write)); + map(0x00, 0x03).mirror(0xf4).rw(m_pit, FUNC(pit8253_device::read), FUNC(pit8253_device::write)); map(0x09, 0x09).mirror(0xf6).r(FUNC(sixtrak_state::misc_r)); map(0x0a, 0x0a).mirror(0xf5).r(FUNC(sixtrak_state::keys_r)); @@ -352,17 +766,30 @@ void sixtrak_state::io_map(address_map &map) void sixtrak_state::machine_start() { + save_item(NAME(m_tuning_counter_clock_started)); + save_item(NAME(m_tuning_counter_gate)); + save_item(NAME(m_tuning_counter_out)); save_item(NAME(m_key_row)); save_item(NAME(m_dac_value)); save_item(NAME(m_sh_voices)); save_item(NAME(m_sh_param)); save_item(NAME(m_voltage_mux_input)); - save_item(NAME(m_cvs)); + save_item(NAME(m_sampling_gain)); + save_item(NAME(m_sampling_freq)); m_digits.resolve(); m_maincpu->space(AS_IO).install_readwrite_before_time( 0x00, 0xff, ws_time_delegate(*this, FUNC(sixtrak_state::iorq_wait_state))); + + m_tuning_timer = timer_alloc(FUNC(sixtrak_state::tuning_timer_tick), this); +} + +void sixtrak_state::machine_reset() +{ + update_wheel_rc(WHEEL_PITCH); + update_wheel_rc(WHEEL_MOD); + update_master_volume(); } void sixtrak_state::sixtrak(machine_config &config) @@ -375,10 +802,11 @@ void sixtrak_state::sixtrak(machine_config &config) NVRAM(config, "nvram2", nvram_device::DEFAULT_ALL_0); // U117 (6116) NVRAM(config, "nvram3", nvram_device::DEFAULT_ALL_0); // U112 (6116) - auto &pit = PIT8253(config, "pit"); // U133 - pit.set_clk<2>(8_MHz_XTAL / 4); // U134 (74LS93), QB. - pit.out_handler<2>().set_inputline(m_maincpu, INPUT_LINE_IRQ0); - // TODO: also used for autotune. + PIT8253(config, m_pit); // U133 + m_pit->set_clk<1>(8_MHz_XTAL / 4); // U134 (74LS93), QB. + m_pit->set_clk<2>(8_MHz_XTAL / 4); // U134 (74LS93), QB. + m_pit->out_handler<0>().set(FUNC(sixtrak_state::tuning_counter_out_changed)); + m_pit->out_handler<2>().set_inputline(m_maincpu, INPUT_LINE_IRQ0); auto &aciaclock = CLOCK(config, "aciaclock", 8_MHz_XTAL / 16); // U134 (74LS93), QD. aciaclock.signal_handler().set("midiacia", FUNC(acia6850_device::write_txc)); @@ -393,15 +821,20 @@ void sixtrak_state::sixtrak(machine_config &config) TTL7474(config, "nmiff", 0).output_cb().set_inputline(m_maincpu, INPUT_LINE_NMI).invert(); // U146B + TTL7474(config, m_tuning_ff, 0); + m_tuning_ff->comp_output_cb().set(FUNC(sixtrak_state::tuning_ff_comp_out_changed)); + MIDI_PORT(config, "mdin", midiin_slot, "midiin").rxd_handler().set("midiacia", FUNC(acia6850_device::write_rxd)); MIDI_PORT(config, "mdout", midiout_slot, "midiout"); config.set_default_layout(layout_sequential_sixtrak); auto &u148 = OUTPUT_LATCH(config, "tune_latch"); // CD40174 - // Bit 0: sound output on/off. - // Bit 1, 2, 4: autotune-related. - // Bit 3: Not connected. + u148.bit_handler<0>().set([this] (int state) { m_mute->set_gain(state ? 1.0 : 0.0); }); + u148.bit_handler<1>().set(FUNC(sixtrak_state::tuning_counter_gate_w)); + u148.bit_handler<2>().set(m_tuning_ff, FUNC(ttl7474_device::preset_w)); + // Bit 3 not connected in Rev B and later. + u148.bit_handler<4>().set(m_tuning_ff, FUNC(ttl7474_device::clear_w)); u148.bit_handler<5>().set("nmiff", FUNC(ttl7474_device::preset_w)); auto &u102 = OUTPUT_LATCH(config, "led_latch_0"); // 74LS174 @@ -427,6 +860,97 @@ void sixtrak_state::sixtrak(machine_config &config) u149.bit_handler<3>().set_output("led_record").invert(); u149.bit_handler<4>().set_output("led_record_track").invert(); u149.bit_handler<5>().set_output("led_legato").invert(); + + // While the capacitor for the pitch wheel is large enough to make a + // difference (it smoothens pitch bends), the one for the mod is small and + // possibly there just to suppress noise. + VA_RC_EG(config, m_wheel_rc[WHEEL_PITCH]).set_c(CAP_U(2.2)); // C110 + VA_RC_EG(config, m_wheel_rc[WHEEL_MOD]).set_c(CAP_U(0.1)); // C111 + + + // *** Audio configuration *** + + // The audio pipeline operates on current and voltage magnitudes. This + // scaler converts the loudest voltage signal to an audio signal within the + // range [-1, 1]. + constexpr double VOLTAGE_TO_AUDIO_SCALER = 0.7; + + // The typical peak-to-peak current of the CE33394, when all waveforms are + // enabled, is 400 uA [-200, 200]. + constexpr double CEM3394_IOUT_MAX = 200E-6; + + // Using some jitter for the VCO capacitor values, for realism. Otherwise, + // unison just sounds like a louder oscillator. Using precomputed random + // values to ensure determinism, and because changing these requires retuning. + // Random values generated in python: [random.uniform(-1, 1) for i in range(0, 6)] + constexpr double C_VCO_JITTER[6] = {-0.9781, 0.0426, 0.6231, -0.4256, 0.2138, -0.0607}; + constexpr double C_VCO = CAP_U(0.002); + + auto &noise = SIXTRAK_TRANSISTOR_NOISE(config, "noise"); + + for (int i = 0; i < 6; ++i) + { + noise.add_route(0, m_voices[i], 1.0, cem3394_device::AUDIO_INPUT); + + VA_RC_EG(config, m_gain_rc[i]).set_r(RES_M(1)); + m_gain_rc[i]->add_route(0, m_voices[i], 1.0, cem3394_device::FINAL_GAIN); + + VA_RC_EG(config, m_freq_rc[i]).set_r(RES_M(1)); + m_freq_rc[i]->add_route(0, m_voices[i], 1.0, cem3394_device::FILTER_FREQENCY); + + CEM3394(config, m_voices[i]); + const double c_vco = C_VCO + C_VCO * C_VCO_JITTER[i] * 0.025; // +/- 2.5%. + m_voices[i]->configure(RES_K(301), c_vco, CAP_U(0.033), CAP_U(10)); + m_voices[i]->add_route(0, "voicemixer", CEM3394_IOUT_MAX); + } + + // The output currents of all CEM3394 chips are summed and converted to a + // voltage by U145B (TL082) and R133. + // The output of the mixer is connected to: + // * Comparator U144 (LM311). Its function is emulated with a timer. See + // update_tuning_timer() for details. + // * A muting circuit. See below. + MIXER(config, "voicemixer").add_route(0, m_mute, -RES_K(4.7)); // R133 + + // A circuit built around a CMOS inverter (U147, CD4049), used in an + // unconventional way to achieve noiseless muting. The Six-Trak service + // manual explains this in detail. In summary: when sound is enabled, + // current from R162 will flow (through U147) to the summing node of U145A + // (TL082), and converted back to a voltage via R165. When disabled, that + // current will be mostly blocked, and the gain of U145A will be reduced to + // silence any current that makes it through. + constexpr double R162 = RES_K(100); + constexpr double R165 = RES_K(100); + FILTER_VOLUME(config, m_mute).add_route(0, m_master_vol, (1.0 / R162) * -R165); + + // Master volume is controlled by R197, a 10K audio-taper knob on the front panel. + FILTER_VOLUME(config, m_master_vol).add_route(0, "mono", VOLTAGE_TO_AUDIO_SCALER); + + SPEAKER(config, "mono").front_center(); +} + +DECLARE_INPUT_CHANGED_MEMBER(sixtrak_state::wheel_moved) +{ + update_wheel_rc(param); +} + +DECLARE_INPUT_CHANGED_MEMBER(sixtrak_state::master_volume_changed) +{ + update_master_volume(); +} + +DECLARE_INPUT_CHANGED_MEMBER(sixtrak_state::dac_trimmer_changed) +{ + update_cvs(); + + const u16 prev_dac_value = m_dac_value; + m_dac_value = 0; + LOGMASKED(LOG_CALIBRATION, "DAC inverter trimmer adjusted ( 0V). V: %f, -V: %f, offset: %f\n", + get_dac_v(false), get_dac_v(true), get_dac_v(false) - get_dac_v(true)); + m_dac_value = 0x0fff; + LOGMASKED(LOG_CALIBRATION, "DAC inverter trimmer adjusted (-4V). V: %f, -V: %f\n", + get_dac_v(false), get_dac_v(true)); + m_dac_value = prev_dac_value; } INPUT_PORTS_START(sixtrak) @@ -446,7 +970,7 @@ INPUT_PORTS_START(sixtrak) PORT_BIT(0x04, IP_ACTIVE_HIGH, IPT_KEYPAD) PORT_NAME("PROGRAM") PORT_CODE(KEYCODE_G) PORT_BIT(0x08, IP_ACTIVE_HIGH, IPT_KEYPAD) PORT_NAME("PARAM") PORT_CODE(KEYCODE_P) PORT_BIT(0x10, IP_ACTIVE_HIGH, IPT_KEYPAD) PORT_NAME("VALUE") PORT_CODE(KEYCODE_V) - PORT_BIT(0x20, IP_ACTIVE_HIGH, IPT_KEYPAD) PORT_NAME("RECORD") + PORT_BIT(0x20, IP_ACTIVE_HIGH, IPT_KEYPAD) PORT_NAME("RECORD") PORT_CODE(KEYCODE_C) PORT_BIT(0x40, IP_ACTIVE_HIGH, IPT_KEYPAD) PORT_NAME("RECORD TRACK") PORT_CODE(KEYCODE_R) PORT_BIT(0x80, IP_ACTIVE_HIGH, IPT_KEYPAD) PORT_NAME("LEGATO") @@ -524,7 +1048,7 @@ INPUT_PORTS_START(sixtrak) PORT_START("key_row_12") // G#2 - D#3 PORT_BIT(0x01, IP_ACTIVE_HIGH, IPT_OTHER) PORT_GM_GS4 - PORT_BIT(0x02, IP_ACTIVE_HIGH, IPT_OTHER) PORT_GM_A4 + PORT_BIT(0x02, IP_ACTIVE_HIGH, IPT_OTHER) PORT_GM_A4 PORT_CODE(KEYCODE_Z) PORT_BIT(0x04, IP_ACTIVE_HIGH, IPT_OTHER) PORT_GM_AS4 PORT_BIT(0x08, IP_ACTIVE_HIGH, IPT_OTHER) PORT_GM_B4 PORT_BIT(0x10, IP_ACTIVE_HIGH, IPT_OTHER) PORT_GM_C5 @@ -552,26 +1076,34 @@ INPUT_PORTS_START(sixtrak) PORT_START("footswitch") PORT_BIT(0x01, IP_ACTIVE_LOW, IPT_OTHER) PORT_NAME("CONTROL FOOTSWITCH") PORT_CODE(KEYCODE_SPACE) - PORT_START("pitch_wheel") // R1, 100K linear. - PORT_BIT(0xff, 50, IPT_PADDLE) PORT_NAME("PITCH") PORT_MINMAX(0, 100) PORT_SENSITIVITY(30) PORT_KEYDELTA(15) PORT_CENTERDELTA(30) + PORT_START("wheel_0") // Pitch wheel. R1, 100K linear. + PORT_BIT(0xff, 50, IPT_PADDLE) PORT_NAME("PITCH") PORT_MINMAX(0, 100) + PORT_SENSITIVITY(30) PORT_KEYDELTA(15) PORT_CENTERDELTA(30) + PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(sixtrak_state::wheel_moved), WHEEL_PITCH) - PORT_START("mod_wheel") // R2, 100K linear. - PORT_ADJUSTER(0, "MOD") + PORT_START("wheel_1") // Mod wheel. R2, 100K linear. + PORT_ADJUSTER(0, "MOD") PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(sixtrak_state::wheel_moved), WHEEL_MOD) - PORT_START("track_volume_knob") // Knob, R119(?), 10K lineaar. + PORT_START("track_volume_knob") // Knob, R119(?), 10K linear. PORT_ADJUSTER(100, "TRACK VOL") PORT_START("speed_knob") // Knob, R115, 10K linear. PORT_ADJUSTER(50, "SPEED") - PORT_START("value_knob") // Knob, R163, 10K linear. + PORT_START("value_knob") // Knob, R163(?), 10K linear. PORT_ADJUSTER(0, "VALUE") PORT_START("tune_knob") // Knob, R138(?), 10K linear. PORT_ADJUSTER(50, "TUNE") PORT_START("master_volume_knob") // Knob, R197, 10K audio taper. - PORT_ADJUSTER(100, "MASTER VOL") + PORT_ADJUSTER(100, "MASTER VOL") PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(sixtrak_state::master_volume_changed), 0) + + // The default value is for a calibration well within spec. + // Required offset when inverting 0V: +/- 0.9 mV. + // Actual offset with the default below: -0.008 mV. + PORT_START("trimmer_dac_inverter") // Trimmer, R1205, 100k linear. + PORT_ADJUSTER(111, "TRIMMER: DAC INVERTER") PORT_MINMAX(0, 255) PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(sixtrak_state::dac_trimmer_changed), 0) INPUT_PORTS_END // The firmware version can be displayed by pressing RECORD TRACK and SELECT 5. @@ -588,4 +1120,4 @@ ROM_END } // anonymous namespace -SYST(1984, sixtrak, 0, 0, sixtrak, sixtrak, sixtrak_state, empty_init, "Sequential Circuits", "Six-Trak (Model 610)", MACHINE_NOT_WORKING | MACHINE_NO_SOUND | MACHINE_SUPPORTS_SAVE) +SYST(1984, sixtrak, 0, 0, sixtrak, sixtrak, sixtrak_state, empty_init, "Sequential Circuits", "Six-Trak (Model 610) Rev B/C", MACHINE_IMPERFECT_SOUND | MACHINE_SUPPORTS_SAVE) From d022631ec757269738f5dc6a8641926b38728d90 Mon Sep 17 00:00:00 2001 From: m1macrophage <168948267+m1macrophage@users.noreply.github.com> Date: Fri, 14 Nov 2025 23:55:05 -0800 Subject: [PATCH 2/2] sixtrak: breaking long input_port lines. --- src/mame/sequential/sixtrak.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/mame/sequential/sixtrak.cpp b/src/mame/sequential/sixtrak.cpp index 8c737e25ec651..193a4400cddbf 100644 --- a/src/mame/sequential/sixtrak.cpp +++ b/src/mame/sequential/sixtrak.cpp @@ -1082,7 +1082,8 @@ INPUT_PORTS_START(sixtrak) PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(sixtrak_state::wheel_moved), WHEEL_PITCH) PORT_START("wheel_1") // Mod wheel. R2, 100K linear. - PORT_ADJUSTER(0, "MOD") PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(sixtrak_state::wheel_moved), WHEEL_MOD) + PORT_ADJUSTER(0, "MOD") + PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(sixtrak_state::wheel_moved), WHEEL_MOD) PORT_START("track_volume_knob") // Knob, R119(?), 10K linear. PORT_ADJUSTER(100, "TRACK VOL") @@ -1097,13 +1098,15 @@ INPUT_PORTS_START(sixtrak) PORT_ADJUSTER(50, "TUNE") PORT_START("master_volume_knob") // Knob, R197, 10K audio taper. - PORT_ADJUSTER(100, "MASTER VOL") PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(sixtrak_state::master_volume_changed), 0) + PORT_ADJUSTER(100, "MASTER VOL") + PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(sixtrak_state::master_volume_changed), 0) // The default value is for a calibration well within spec. // Required offset when inverting 0V: +/- 0.9 mV. // Actual offset with the default below: -0.008 mV. PORT_START("trimmer_dac_inverter") // Trimmer, R1205, 100k linear. - PORT_ADJUSTER(111, "TRIMMER: DAC INVERTER") PORT_MINMAX(0, 255) PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(sixtrak_state::dac_trimmer_changed), 0) + PORT_ADJUSTER(111, "TRIMMER: DAC INVERTER") PORT_MINMAX(0, 255) + PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(sixtrak_state::dac_trimmer_changed), 0) INPUT_PORTS_END // The firmware version can be displayed by pressing RECORD TRACK and SELECT 5.