// license:BSD-3-Clause // copyright-holders:Antoine Mine /********************************************************************** Copyright (C) Antoine Mine' 2006 Philips / Signetics MEA 8000 emulation. The MEA 8000 is a speech synthesis chip. The French company TMPI (Techni-musique & parole informatique) provided speech extensions for several 8-bit computers (Thomson, Amstrad, Oric). It was quite popular in France because of its ability to spell 'u' (unlike the more widespread SPO 296 chip). The synthesis is based on a 4-formant model. First, an initial sawtooth noise signal is generated. The signal passes through a cascade of 4 filters of increasing frequency. Each filter is a second order digital filter with a programmable frequency and bandwidth. All parameters, including filter parameters, are smoothly interpolated for the duration of a frame (8ms, 16ms, 32ms, or 64 ms). TODO: - REQ output pin - optimize mea8000_compute_sample - should we accept new frames in slow-stop mode ? **********************************************************************/ #include "emu.h" #include "mea8000.h" #include //#define VERBOSE 1 #include "logmacro.h" /******************* utilitiy function and macros ********************/ ALLOW_SAVE_TYPE( mea8000_device::mea8000_state ); /* digital filters work at 8 kHz */ #define F0 (clock() / 480) /* filtered output is supersampled x 8 */ #define SUPERSAMPLING 8 /* actual output pediod */ #define SAMPLING attotime::from_hz((SUPERSAMPLING*F0)) /************************* quantization tables ***********************/ /* table amplitude [-QUANT,QUANT] */ #define QUANT 512 /* frequency, in Hz */ static const int fm1_table[32] = { 150, 162, 174, 188, 202, 217, 233, 250, 267, 286, 305, 325, 346, 368, 391, 415, 440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 830, 880, 932, 988, 1047 }; static const int fm2_table[32] = { 440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 830, 880, 932, 988, 1047, 1100, 1179, 1254, 1337, 1428, 1528, 1639, 1761, 1897, 2047, 2214, 2400, 2609, 2842, 3105, 3400 }; static const int fm3_table[8] = { 1179, 1337, 1528, 1761, 2047, 2400, 2842, 3400 }; static const int fm4_table[1] = { 3500 }; /* bandwidth, in Hz */ static const int bw_table[4] = { 726, 309, 125, 50 }; /* amplitude * 1000 */ static const int ampl_table[16] = { 0, 8, 11, 16, 22, 31, 44, 62, 88, 125, 177, 250, 354, 500, 707, 1000 }; /* pitch increment, in Hz / 8 ms */ static const int pi_table[32] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0 /* noise */, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1 }; DEFINE_DEVICE_TYPE(MEA8000, mea8000_device, "mea8000", "Philips/Signetics MEA 8000 speech synthesizer") mea8000_device::mea8000_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : device_t(mconfig, MEA8000, tag, owner, clock), device_sound_interface(mconfig, *this), m_write_req(*this), m_stream(nullptr), m_output(0) { } //------------------------------------------------- // device_start - device-specific startup //------------------------------------------------- void mea8000_device::device_start() { m_write_req.resolve_safe(); init_tables(); m_stream = stream_alloc(0, 1, clock() / 60); save_item(NAME(m_output)); m_timer = machine().scheduler().timer_alloc(timer_expired_delegate(FUNC(mea8000_device::timer_expire),this)); save_item(NAME(m_state)); save_item(NAME(m_buf)); save_item(NAME(m_bufpos)); save_item(NAME(m_cont)); save_item(NAME(m_roe)); save_item(NAME(m_framelength)); save_item(NAME(m_framepos)); save_item(NAME(m_framelog)); save_item(NAME(m_lastsample)); save_item(NAME(m_sample)); save_item(NAME(m_phi)); for (int i = 0; i < 4; i++) { save_item(NAME(m_f[i].fm), i); save_item(NAME(m_f[i].last_fm), i); save_item(NAME(m_f[i].bw), i); save_item(NAME(m_f[i].last_bw), i); save_item(NAME(m_f[i].output), i); save_item(NAME(m_f[i].last_output), i); } save_item(NAME(m_last_ampl)); save_item(NAME(m_ampl)); save_item(NAME(m_last_pitch)); save_item(NAME(m_pitch)); save_item(NAME(m_noise)); } /***************************** REQ **********************************/ int mea8000_device::accept_byte() { return m_state == mea8000_state::STOPPED || m_state == mea8000_state::WAIT_FIRST || (m_state == mea8000_state::STARTED && m_bufpos < 4); } void mea8000_device::update_req() { // actually, req pulses less than 3us for each new byte, // it goes back up if there space left in the buffer, or stays low if the // buffer contains a complete frame and the CPU nees to wait for the next // frame end to compose a new frame. m_write_req(accept_byte()); } /*********************** sound generation ***************************/ /* precompute tables */ void mea8000_device::init_tables() { for (int i = 0; i < TABLE_LEN; i++) { double f = (double)i / F0; m_cos_table[i] = 2. * cos(2. * M_PI * f) * QUANT; m_exp_table[i] = exp(-M_PI * f) * QUANT; m_exp2_table[i] = exp(-2 * M_PI * f) * QUANT; } for (auto & elem : m_noise_table) elem = (machine().rand() % (2 * QUANT)) - QUANT; } #ifndef MEA8000_FLOAT_MODE /* uint16_t version */ /* linear interpolation */ int mea8000_device::interp( uint16_t org, uint16_t dst ) { return org + (((dst - org) * m_framepos) >> m_framelog); } /* apply second order digital filter, sampling at F0 */ int mea8000_device::filter_step( int i, int input ) { /* frequency */ int fm = interp(m_f[i].last_fm, m_f[i].fm); /* bandwidth */ int bw = interp(m_f[i].last_bw, m_f[i].bw); /* filter coefficients */ int b = (m_cos_table[fm] * m_exp_table[bw]) / QUANT; int c = m_exp2_table[bw]; /* transfer function */ int next_output = input + (b * m_f[i].output - c * m_f[i].last_output) / QUANT; m_f[i].last_output = m_f[i].output; m_f[i].output = next_output; return next_output; } /* random waveform, in [-QUANT,QUANT] */ int mea8000_device::noise_gen() { m_phi = (m_phi + 1) % NOISE_LEN; return m_noise_table[m_phi]; } /* sawtooth waveform at F0, in [-QUANT,QUANT] */ int mea8000_device::freq_gen() { int pitch = interp(m_last_pitch, m_pitch); m_phi = (m_phi + pitch) % F0; return ((m_phi % F0) * QUANT * 2) / F0 - QUANT; } /* sample in [-32768,32767], at F0 */ int mea8000_device::compute_sample() { int out; int ampl = interp(m_last_ampl, m_ampl); if (m_noise) out = noise_gen(); else out = freq_gen(); out *= ampl / 32; for (int i = 0; i < 4; i++) out = filter_step(i, out); if (out > 32767) out = 32767; if (out < -32767) out = -32767; return out; } #else /* float version */ /* linear interpolation */ double mea8000_device::interp(double org, double dst) { return org + ((dst - org) * m_framepos) / m_framelength; } /* apply second order digital filter, sampling at F0 */ double mea8000_device::filter_step(int i, double input) { double fm = interp(m_f[i].last_fm, m_f[i].fm); double bw = interp(m_f[i].last_bw, m_f[i].bw); double b = 2. * cos(2. * M_PI * fm / F0); double c = -exp(-M_PI * bw / F0); double next_output = input - c * (b * m_f[i].output + c * m_f[i].last_output); m_f[i].last_output = m_f[i].output; m_f[i].output = next_output; return next_output; } /* noise, in [-1,1] */ double mea8000_device::noise_gen() { m_phi++; return (double) m_noise_table[m_phi % NOISE_LEN] / QUANT; } /* sawtooth waveform at F0, in [-1,1] */ double mea8000_device::freq_gen() { int pitch = interp(m_last_pitch, m_pitch); m_phi += pitch; return (double) (m_phi % F0) / (F0 / 2.) - 1.; } /* sample in [-32767,32767], at F0 */ int mea8000_device::compute_sample() { double out; double ampl = interp(8. * m_last_ampl, 8. * m_ampl); if (m_noise) out = noise_gen(); else out = freq_gen(); out *= ampl; for (int i = 0; i < 4; i++) { out = filter_step(i, out); } if (out > 32767) out = 32767; if (out < -32767) out = -32767; return out; } #endif /*********************** frame management ***************************/ /* shift frame parameters from current to last */ void mea8000_device::shift_frame() { m_last_pitch = m_pitch; for (auto & elem : m_f) { elem.last_bw = elem.bw; elem.last_fm = elem.fm; } m_last_ampl = m_ampl; } /* decode fields from buffer to current frame */ void mea8000_device::decode_frame() { int fd = (m_buf[3] >> 5) & 3; /* 0=8ms, 1=16ms, 2=32ms, 3=64ms */ int pi = pi_table[m_buf[3] & 0x1f] << fd; m_noise = (m_buf[3] & 0x1f) == 16; m_pitch = m_last_pitch + pi; m_f[0].bw = bw_table[m_buf[0] >> 6]; m_f[1].bw = bw_table[(m_buf[0] >> 4) & 3]; m_f[2].bw = bw_table[(m_buf[0] >> 2) & 3]; m_f[3].bw = bw_table[m_buf[0] & 3]; m_f[3].fm = fm4_table[0]; m_f[2].fm = fm3_table[m_buf[1] >> 5]; m_f[1].fm = fm2_table[m_buf[1] & 0x1f]; m_f[0].fm = fm1_table[m_buf[2] >> 3]; m_ampl = ampl_table[((m_buf[2] & 7) << 1) | (m_buf[3] >> 7)]; m_framelog = fd + 6 /* 64 samples / ms */ + 3; m_framelength = 1 << m_framelog; m_bufpos = 0; #ifdef MEA8000_FLOAT_MODE LOG("mea800_decode_frame: pitch=%i noise=%i fm1=%gHz bw1=%gHz fm2=%gHz bw2=%gHz fm3=%gHz bw3=%gHz fm4=%gHz bw4=%gHz ampl=%g fd=%ims\n", m_pitch, m_noise, m_f[0].fm, m_f[0].bw, m_f[1].fm, m_f[1].bw, m_f[2].fm, m_f[2].bw, m_f[3].fm, m_f[3].bw, m_ampl/1000., 8 << fd); #else LOG("mea800_decode_frame: pitch=%i noise=%i fm1=%iHz bw1=%iHz fm2=%iHz bw2=%iHz fm3=%iHz bw3=%iHz fm4=%iHz bw4=%iHz ampl=%g fd=%ims\n", m_pitch, m_noise, m_f[0].fm, m_f[0].bw, m_f[1].fm, m_f[1].bw, m_f[2].fm, m_f[2].bw, m_f[3].fm, m_f[3].bw, m_ampl/1000., 8 << fd); #endif } void mea8000_device::start_frame() { /* enter or stay in active mode */ m_timer->reset(SAMPLING); m_framepos = 0; } void mea8000_device::stop_frame() { /* enter stop mode */ m_timer->reset(); m_state = mea8000_state::STOPPED; m_stream->update(); m_output = 0; } void mea8000_device::sound_stream_update(sound_stream &stream, stream_sample_t **inputs, stream_sample_t **outputs, int samples) { for (int samp = 0; samp < samples; samp++) { outputs[0][samp] = m_output; } } /* next sample in frame, sampling at 64 kHz */ TIMER_CALLBACK_MEMBER( mea8000_device::timer_expire ) { int pos = m_framepos % SUPERSAMPLING; if (!pos) { /* sample is really computed only every 8-th time */ m_lastsample = m_sample; m_sample = compute_sample(); m_stream->update(); m_output = m_lastsample; } else { /* other samples are simply interpolated */ int sample = m_lastsample + ((pos * (m_sample-m_lastsample)) / SUPERSAMPLING); m_stream->update(); m_output = sample; } m_framepos++; if (m_framepos >= m_framelength) { shift_frame(); /* end of frame */ if (m_bufpos == 4) { /* we have a successor */ LOG("%f mea8000_timer_expire: new frame\n", machine().time().as_double()); decode_frame(); start_frame(); } else if (m_cont) { /* repeat mode */ LOG("%f mea8000_timer_expire: repeat frame\n", machine().time().as_double()); start_frame(); } /* slow stop */ else if (m_state == mea8000_state::STARTED) { m_ampl = 0; LOG("%f mea8000_timer_expire: fade frame\n", machine().time().as_double()); start_frame(); m_state = mea8000_state::SLOWING; } else if (m_state == mea8000_state::SLOWING) { LOG("%f mea8000_timer_expire: stop frame\n", machine().time().as_double()); stop_frame(); } update_req(); } else { /* continue frame */ m_timer->reset(SAMPLING); } } /************************** CPU interface ****************************/ READ8_MEMBER( mea8000_device::read ) { switch (offset) { case 0: /* status register */ case 1: /* ready to accept next frame */ #if 0 LOG("%s %f: mea8000_r ready=%i\n", machine().describe_context(), machine().time().as_double(), accept_byte()); #endif return accept_byte() << 7; default: logerror("%s mea8000_r invalid read offset %i\n", machine().describe_context(), offset); } return 0; } WRITE8_MEMBER( mea8000_device::write ) { switch (offset) { case 0: /* data register */ if (m_state == mea8000_state::STOPPED) { /* got pitch byte before first frame */ m_pitch = 2 * data; LOG("%s %f: mea8000_w pitch %i\n", machine().describe_context(), machine().time().as_double(), m_pitch); m_state = mea8000_state::WAIT_FIRST; m_bufpos = 0; } else if (m_bufpos == 4) { /* overflow */ LOG("%s %f: mea8000_w data overflow %02X\n", machine().describe_context(), machine().time().as_double(), data); } else { /* enqueue frame byte */ LOG("%s %f: mea8000_w data %02X in frame pos %i\n", machine().describe_context(), machine().time().as_double(), data, m_bufpos); m_buf[m_bufpos] = data; m_bufpos++; if (m_bufpos == 4 && m_state == mea8000_state::WAIT_FIRST) { /* fade-in first frame */ int old_pitch = m_pitch; m_last_pitch = old_pitch; decode_frame(); shift_frame(); m_last_pitch = old_pitch; m_ampl = 0; start_frame(); m_state = mea8000_state::STARTED; } } update_req(); break; case 1: /* command register */ { int stop = BIT(data, 4); if (data & 8) m_cont = BIT(data, 2); if (data & 2) m_roe = BIT(data, 0); if (stop) stop_frame(); LOG( "%s %f: mea8000_w command %02X stop=%i cont=%i roe=%i\n", machine().describe_context(), machine().time().as_double(), data, stop, m_cont, m_roe); update_req(); break; } default: logerror( "%s mea8000_w invalid write offset %i\n", machine().describe_context(), offset); } }