// license:BSD-3-Clause // copyright-holders:Aaron Giles /* Yamaha YMZ280B driver by Aaron Giles YMZ280B 8-Channel PCMD8 PCM/ADPCM Decoder Features as listed in LSI-4MZ280B3 data sheet: Voice data stored in external memory can be played back simultaneously for up to eight voices Voice data format can be selected from 4-bit ADPCM, 8-bit PCM and 16-bit PCM Control of voice data external memory Up to 16M bytes of ROM or SRAM (x 8 bits, access time 150ms max) can be connected Continuous access is possible Loop playback between selective addresses is possible Voice data playback frequency control 4-bit ADPCM ................ 0.172 to 44.1kHz in 256 steps 8-bit PCM, 16-bit PCM ...... 0.172 to 88.2kHz in 512 steps 256 steps total level and 16 steps panpot can be set Voice signal is output in stereo 16-bit 2's complement MSB-first format TODO: - Is memory handling 100% correct? At the moment, Konami firebeat.c is the only hardware currently emulated that uses external handlers. It also happens to be the only one using 16-bit PCM. Some other drivers (eg. bishi.c, bfm_sc4/5.c) also use ROM readback. */ #include "emu.h" #include "ymz280b.h" #if YMZ280B_MAKE_WAVS #include "sound/wavwrite.h" #endif #define MAX_SAMPLE_CHUNK 10000 #define FRAC_BITS 14 #define FRAC_ONE (1 << FRAC_BITS) #define FRAC_MASK (FRAC_ONE - 1) #define INTERNAL_BUFFER_SIZE (1 << 15) #define INTERNAL_SAMPLE_RATE (m_master_clock * 2.0) /* step size index shift table */ static constexpr int index_scale[8] = { 0x0e6, 0x0e6, 0x0e6, 0x0e6, 0x133, 0x199, 0x200, 0x266 }; /* lookup table for the precomputed difference */ static int diff_lookup[16]; void ymz280b_device::update_irq_state() { int irq_bits = m_status_register & m_irq_mask; /* always off if the enable is off */ if (!m_irq_enable) irq_bits = 0; /* update the state if changed */ if (irq_bits && !m_irq_state) { m_irq_state = 1; if (!m_irq_handler.isnull()) m_irq_handler(1); else logerror("YMZ280B: IRQ generated, but no callback specified!\n"); } else if (!irq_bits && m_irq_state) { m_irq_state = 0; if (!m_irq_handler.isnull()) m_irq_handler(0); else logerror("YMZ280B: IRQ generated, but no callback specified!\n"); } } void ymz280b_device::update_step(struct YMZ280BVoice *voice) { double frequency; /* compute the frequency */ if (voice->mode == 1) frequency = m_master_clock * (double)((voice->fnum & 0x0ff) + 1) * (1.0 / 256.0); else frequency = m_master_clock * (double)((voice->fnum & 0x1ff) + 1) * (1.0 / 256.0); voice->output_step = (uint32_t)(frequency * (double)FRAC_ONE / INTERNAL_SAMPLE_RATE); } void ymz280b_device::update_volumes(struct YMZ280BVoice *voice) { if (voice->pan == 8) { voice->output_left = voice->level; voice->output_right = voice->level; } else if (voice->pan < 8) { voice->output_left = voice->level; /* pan 1 is hard-left, what's pan 0? for now assume same as pan 1 */ voice->output_right = (voice->pan == 0) ? 0 : voice->level * (voice->pan - 1) / 7; } else { voice->output_left = voice->level * (15 - voice->pan) / 7; voice->output_right = voice->level; } } void ymz280b_device::device_post_load() { for (auto & elem : m_voice) { struct YMZ280BVoice *voice = &elem; update_step(voice); if(voice->irq_schedule) voice->timer->adjust(attotime::zero); } } void ymz280b_device::update_irq_state_timer_common(int voicenum) { struct YMZ280BVoice *voice = &m_voice[voicenum]; if(!voice->irq_schedule) return; voice->playing = 0; m_status_register |= 1 << voicenum; update_irq_state(); voice->irq_schedule = 0; } /********************************************************************************************** compute_tables -- compute the difference tables ***********************************************************************************************/ static void compute_tables() { /* loop over all nibbles and compute the difference */ for (int nib = 0; nib < 16; nib++) { int value = (nib & 0x07) * 2 + 1; diff_lookup[nib] = (nib & 0x08) ? -value : value; } } /********************************************************************************************** generate_adpcm -- general ADPCM decoding routine ***********************************************************************************************/ int ymz280b_device::generate_adpcm(struct YMZ280BVoice *voice, int16_t *buffer, int samples) { int position = voice->position; int signal = voice->signal; int step = voice->step; int val; /* two cases: first cases is non-looping */ if (!voice->looping) { /* loop while we still have samples to generate */ while (samples) { /* compute the new amplitude and update the current step */ val = read_byte(position / 2) >> ((~position & 1) << 2); signal += (step * diff_lookup[val & 15]) / 8; /* clamp to the maximum */ if (signal > 32767) signal = 32767; else if (signal < -32768) signal = -32768; /* adjust the step size and clamp */ step = (step * index_scale[val & 7]) >> 8; if (step > 0x6000) step = 0x6000; else if (step < 0x7f) step = 0x7f; /* output to the buffer, scaling by the volume */ *buffer++ = signal; samples--; /* next! */ position++; if (position >= voice->stop) { voice->ended = true; break; } } } /* second case: looping */ else { /* loop while we still have samples to generate */ while (samples) { /* compute the new amplitude and update the current step */ val = read_byte(position / 2) >> ((~position & 1) << 2); signal += (step * diff_lookup[val & 15]) / 8; /* clamp to the maximum */ if (signal > 32767) signal = 32767; else if (signal < -32768) signal = -32768; /* adjust the step size and clamp */ step = (step * index_scale[val & 7]) >> 8; if (step > 0x6000) step = 0x6000; else if (step < 0x7f) step = 0x7f; /* output to the buffer, scaling by the volume */ *buffer++ = signal; samples--; /* next! */ position++; if (position == voice->loop_start && voice->loop_count == 0) { voice->loop_signal = signal; voice->loop_step = step; } if (position >= voice->loop_end) { if (voice->keyon) { position = voice->loop_start; signal = voice->loop_signal; step = voice->loop_step; voice->loop_count++; } } if (position >= voice->stop) { voice->ended = true; break; } } } /* update the parameters */ voice->position = position; voice->signal = signal; voice->step = step; return samples; } /********************************************************************************************** generate_pcm8 -- general 8-bit PCM decoding routine ***********************************************************************************************/ int ymz280b_device::generate_pcm8(struct YMZ280BVoice *voice, int16_t *buffer, int samples) { int position = voice->position; int val; /* two cases: first cases is non-looping */ if (!voice->looping) { /* loop while we still have samples to generate */ while (samples) { /* fetch the current value */ val = read_byte(position / 2); /* output to the buffer, scaling by the volume */ *buffer++ = (int8_t)val * 256; samples--; /* next! */ position += 2; if (position >= voice->stop) { voice->ended = true; break; } } } /* second case: looping */ else { /* loop while we still have samples to generate */ while (samples) { /* fetch the current value */ val = read_byte(position / 2); /* output to the buffer, scaling by the volume */ *buffer++ = (int8_t)val * 256; samples--; /* next! */ position += 2; if (position >= voice->loop_end) { if (voice->keyon) position = voice->loop_start; } if (position >= voice->stop) { voice->ended = true; break; } } } /* update the parameters */ voice->position = position; return samples; } /********************************************************************************************** generate_pcm16 -- general 16-bit PCM decoding routine ***********************************************************************************************/ int ymz280b_device::generate_pcm16(struct YMZ280BVoice *voice, int16_t *buffer, int samples) { int position = voice->position; int val; /* two cases: first cases is non-looping */ if (!voice->looping) { /* loop while we still have samples to generate */ while (samples) { /* fetch the current value */ val = (int16_t)((read_byte(position / 2 + 1) << 8) + read_byte(position / 2 + 0)); /* output to the buffer, scaling by the volume */ *buffer++ = val; samples--; /* next! */ position += 4; if (position >= voice->stop) { voice->ended = true; break; } } } /* second case: looping */ else { /* loop while we still have samples to generate */ while (samples) { /* fetch the current value */ val = (int16_t)((read_byte(position / 2 + 1) << 8) + read_byte(position / 2 + 0)); /* output to the buffer, scaling by the volume */ *buffer++ = val; samples--; /* next! */ position += 4; if (position >= voice->loop_end) { if (voice->keyon) position = voice->loop_start; } if (position >= voice->stop) { voice->ended = true; break; } } } /* update the parameters */ voice->position = position; return samples; } //------------------------------------------------- // sound_stream_update - handle a stream update //------------------------------------------------- void ymz280b_device::sound_stream_update(sound_stream &stream, stream_sample_t **inputs, stream_sample_t **outputs, int samples) { stream_sample_t *lacc = outputs[0]; stream_sample_t *racc = outputs[1]; int v; /* clear out the accumulator */ memset(lacc, 0, samples * sizeof(lacc[0])); memset(racc, 0, samples * sizeof(racc[0])); /* loop over voices */ for (v = 0; v < 8; v++) { struct YMZ280BVoice *voice = &m_voice[v]; int16_t prev = voice->last_sample; int16_t curr = voice->curr_sample; int16_t *curr_data = m_scratch.get(); int32_t *ldest = lacc; int32_t *rdest = racc; uint32_t new_samples, samples_left; uint32_t final_pos; int remaining = samples; int lvol = voice->output_left; int rvol = voice->output_right; /* quick out if we're not playing and we're at 0 */ if (!voice->playing && curr == 0 && prev == 0) { /* make sure next sound plays immediately */ voice->output_pos = FRAC_ONE; continue; } /* finish off the current sample */ /* interpolate */ while (remaining > 0 && voice->output_pos < FRAC_ONE) { int interp_sample = (((int32_t)prev * (FRAC_ONE - voice->output_pos)) + ((int32_t)curr * voice->output_pos)) >> FRAC_BITS; *ldest++ += interp_sample * lvol; *rdest++ += interp_sample * rvol; voice->output_pos += voice->output_step; remaining--; } /* if we're over, continue; otherwise, we're done */ if (voice->output_pos >= FRAC_ONE) voice->output_pos -= FRAC_ONE; else continue; /* compute how many new samples we need */ final_pos = voice->output_pos + remaining * voice->output_step; new_samples = (final_pos + FRAC_ONE) >> FRAC_BITS; if (new_samples > MAX_SAMPLE_CHUNK) new_samples = MAX_SAMPLE_CHUNK; samples_left = new_samples; /* generate them into our buffer */ switch (voice->playing << 7 | voice->mode) { case 0x81: samples_left = generate_adpcm(voice, m_scratch.get(), new_samples); break; case 0x82: samples_left = generate_pcm8(voice, m_scratch.get(), new_samples); break; case 0x83: samples_left = generate_pcm16(voice, m_scratch.get(), new_samples); break; default: samples_left = 0; memset(m_scratch.get(), 0, new_samples * sizeof(m_scratch[0])); break; } if (samples_left || voice->ended) { voice->ended = false; /* if there are leftovers, ramp back to 0 */ int base = new_samples - samples_left; int i, t = (base == 0) ? curr : m_scratch[base - 1]; for (i = 0; i < samples_left; i++) { if (t < 0) t = -((-t * 15) >> 4); else if (t > 0) t = (t * 15) >> 4; m_scratch[base + i] = t; } /* if we hit the end and IRQs are enabled, signal it */ if (base != 0) { voice->playing = 0; /* set update_irq_state_timer. IRQ is signaled on next CPU execution. */ voice->timer->adjust(attotime::zero); voice->irq_schedule = 1; } } /* advance forward one sample */ prev = curr; curr = *curr_data++; /* then sample-rate convert with linear interpolation */ while (remaining > 0) { /* interpolate */ while (remaining > 0 && voice->output_pos < FRAC_ONE) { int interp_sample = (((int32_t)prev * (FRAC_ONE - voice->output_pos)) + ((int32_t)curr * voice->output_pos)) >> FRAC_BITS; *ldest++ += interp_sample * lvol; *rdest++ += interp_sample * rvol; voice->output_pos += voice->output_step; remaining--; } /* if we're over, grab the next samples */ if (voice->output_pos >= FRAC_ONE) { voice->output_pos -= FRAC_ONE; prev = curr; curr = *curr_data++; } } /* remember the last samples */ voice->last_sample = prev; voice->curr_sample = curr; } for (v = 0; v < samples; v++) { outputs[0][v] /= 256; outputs[1][v] /= 256; } } //------------------------------------------------- // device_start - device-specific startup //------------------------------------------------- void ymz280b_device::device_start() { /* compute ADPCM tables */ compute_tables(); /* initialize the rest of the structure */ m_master_clock = (double)clock() / 384.0; m_irq_handler.resolve(); for (int i = 0; i < 8; i++) { m_voice[i].timer = timer_alloc(i); } /* create the stream */ m_stream = machine().sound().stream_alloc(*this, 0, 2, INTERNAL_SAMPLE_RATE); /* allocate memory */ assert(MAX_SAMPLE_CHUNK < 0x10000); m_scratch = std::make_unique(MAX_SAMPLE_CHUNK); /* state save */ save_item(NAME(m_current_register)); save_item(NAME(m_status_register)); save_item(NAME(m_irq_state)); save_item(NAME(m_irq_mask)); save_item(NAME(m_irq_enable)); save_item(NAME(m_keyon_enable)); save_item(NAME(m_ext_mem_enable)); save_item(NAME(m_ext_mem_address)); save_item(NAME(m_ext_readlatch)); save_item(NAME(m_ext_mem_address_hi)); save_item(NAME(m_ext_mem_address_mid)); for (int j = 0; j < 8; j++) { save_item(NAME(m_voice[j].playing), j); save_item(NAME(m_voice[j].ended), j); save_item(NAME(m_voice[j].keyon), j); save_item(NAME(m_voice[j].looping), j); save_item(NAME(m_voice[j].mode), j); save_item(NAME(m_voice[j].fnum), j); save_item(NAME(m_voice[j].level), j); save_item(NAME(m_voice[j].pan), j); save_item(NAME(m_voice[j].start), j); save_item(NAME(m_voice[j].stop), j); save_item(NAME(m_voice[j].loop_start), j); save_item(NAME(m_voice[j].loop_end), j); save_item(NAME(m_voice[j].position), j); save_item(NAME(m_voice[j].signal), j); save_item(NAME(m_voice[j].step), j); save_item(NAME(m_voice[j].loop_signal), j); save_item(NAME(m_voice[j].loop_step), j); save_item(NAME(m_voice[j].loop_count), j); save_item(NAME(m_voice[j].output_left), j); save_item(NAME(m_voice[j].output_right), j); save_item(NAME(m_voice[j].output_pos), j); save_item(NAME(m_voice[j].last_sample), j); save_item(NAME(m_voice[j].curr_sample), j); save_item(NAME(m_voice[j].irq_schedule), j); } #if YMZ280B_MAKE_WAVS m_wavresample = wav_open("resamp.wav", INTERNAL_SAMPLE_RATE, 2); #endif } //------------------------------------------------- // device_reset - device-specific reset //------------------------------------------------- void ymz280b_device::device_reset() { /* initial clear registers */ for (int i = 0xff; i >= 0; i--) { m_current_register = i; write_to_register(0); } m_current_register = 0; m_status_register = 0; m_ext_mem_address = 0; /* clear other voice parameters */ for (auto & elem : m_voice) { struct YMZ280BVoice *voice = &elem; voice->curr_sample = 0; voice->last_sample = 0; voice->output_pos = FRAC_ONE; voice->playing = 0; } } void ymz280b_device::device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr) { if (id < 8) update_irq_state_timer_common( id ); else assert_always(false, "Unknown id in ymz280b_device::device_timer"); } void ymz280b_device::device_clock_changed() { m_master_clock = (double)clock() / 384.0; m_stream->set_sample_rate(INTERNAL_SAMPLE_RATE); } void ymz280b_device::rom_bank_updated() { m_stream->update(); } /********************************************************************************************** write_to_register -- handle a write to the current register ***********************************************************************************************/ void ymz280b_device::write_to_register(int data) { struct YMZ280BVoice *voice; int i; /* lower registers follow a pattern */ if (m_current_register < 0x80) { voice = &m_voice[(m_current_register >> 2) & 7]; switch (m_current_register & 0xe3) { case 0x00: /* pitch low 8 bits */ voice->fnum = (voice->fnum & 0x100) | (data & 0xff); update_step(voice); break; case 0x01: /* pitch upper 1 bit, loop, key on, mode */ voice->fnum = (voice->fnum & 0xff) | ((data & 0x01) << 8); voice->looping = (data & 0x10) >> 4; if ((data & 0x60) == 0) data &= 0x7f; /* ignore mode setting and set to same state as KON=0 */ else voice->mode = (data & 0x60) >> 5; if (!voice->keyon && (data & 0x80) && m_keyon_enable) { voice->playing = 1; voice->position = voice->start; voice->signal = voice->loop_signal = 0; voice->step = voice->loop_step = 0x7f; voice->loop_count = 0; /* if update_irq_state_timer is set, cancel it. */ voice->irq_schedule = 0; } else if (voice->keyon && !(data & 0x80)) { voice->playing = 0; /* if update_irq_state_timer is set, cancel it. */ voice->irq_schedule = 0; } voice->keyon = (data & 0x80) >> 7; update_step(voice); break; case 0x02: /* total level */ voice->level = data; update_volumes(voice); break; case 0x03: /* pan */ voice->pan = data & 0x0f; update_volumes(voice); break; case 0x20: /* start address high */ voice->start = (voice->start & (0x00ffff << 1)) | (data << 17); break; case 0x21: /* loop start address high */ voice->loop_start = (voice->loop_start & (0x00ffff << 1)) | (data << 17); break; case 0x22: /* loop end address high */ voice->loop_end = (voice->loop_end & (0x00ffff << 1)) | (data << 17); break; case 0x23: /* stop address high */ voice->stop = (voice->stop & (0x00ffff << 1)) | (data << 17); break; case 0x40: /* start address middle */ voice->start = (voice->start & (0xff00ff << 1)) | (data << 9); break; case 0x41: /* loop start address middle */ voice->loop_start = (voice->loop_start & (0xff00ff << 1)) | (data << 9); break; case 0x42: /* loop end address middle */ voice->loop_end = (voice->loop_end & (0xff00ff << 1)) | (data << 9); break; case 0x43: /* stop address middle */ voice->stop = (voice->stop & (0xff00ff << 1)) | (data << 9); break; case 0x60: /* start address low */ voice->start = (voice->start & (0xffff00 << 1)) | (data << 1); break; case 0x61: /* loop start address low */ voice->loop_start = (voice->loop_start & (0xffff00 << 1)) | (data << 1); break; case 0x62: /* loop end address low */ voice->loop_end = (voice->loop_end & (0xffff00 << 1)) | (data << 1); break; case 0x63: /* stop address low */ voice->stop = (voice->stop & (0xffff00 << 1)) | (data << 1); break; default: logerror("YMZ280B: unknown register write %02X = %02X\n", m_current_register, data); break; } } /* upper registers are special */ else { switch (m_current_register) { /* DSP related (not implemented yet) */ case 0x80: // d0-2: DSP Rch, d3: enable Rch (0: yes, 1: no), d4-6: DSP Lch, d7: enable Lch (0: yes, 1: no) case 0x81: // d0: enable control of $82 (0: yes, 1: no) case 0x82: // DSP data logerror("YMZ280B: DSP register write %02X = %02X\n", m_current_register, data); break; case 0x84: /* ROM readback / RAM write (high) */ m_ext_mem_address_hi = data << 16; break; case 0x85: /* ROM readback / RAM write (middle) */ m_ext_mem_address_mid = data << 8; break; case 0x86: /* ROM readback / RAM write (low) -> update latch */ m_ext_mem_address = m_ext_mem_address_hi | m_ext_mem_address_mid | data; if (m_ext_mem_enable) m_ext_readlatch = read_byte(m_ext_mem_address); break; case 0x87: /* RAM write */ if (m_ext_mem_enable) { space(0).write_byte(m_ext_mem_address, data); m_ext_mem_address = (m_ext_mem_address + 1) & 0xffffff; } break; case 0xfe: /* IRQ mask */ m_irq_mask = data; update_irq_state(); break; case 0xff: /* IRQ enable, test, etc */ m_ext_mem_enable = (data & 0x40) >> 6; m_irq_enable = (data & 0x10) >> 4; update_irq_state(); if (m_keyon_enable && !(data & 0x80)) { for (i = 0; i < 8; i++) { m_voice[i].playing = 0; /* if update_irq_state_timer is set, cancel it. */ m_voice[i].irq_schedule = 0; } } else if (!m_keyon_enable && (data & 0x80)) { for (i = 0; i < 8; i++) { if (m_voice[i].keyon && m_voice[i].looping) m_voice[i].playing = 1; } } m_keyon_enable = (data & 0x80) >> 7; break; default: logerror("YMZ280B: unknown register write %02X = %02X\n", m_current_register, data); break; } } } /********************************************************************************************** compute_status -- determine the status bits ***********************************************************************************************/ int ymz280b_device::compute_status() { uint8_t result; /* force an update */ m_stream->update(); result = m_status_register; /* clear the IRQ state */ m_status_register = 0; update_irq_state(); return result; } /********************************************************************************************** read/write -- handle external accesses ***********************************************************************************************/ READ8_MEMBER( ymz280b_device::read ) { if ((offset & 1) == 0) { if (!m_ext_mem_enable) return 0xff; /* read from external memory */ uint8_t ret = m_ext_readlatch; m_ext_readlatch = read_byte(m_ext_mem_address); m_ext_mem_address = (m_ext_mem_address + 1) & 0xffffff; return ret; } else return compute_status(); } WRITE8_MEMBER( ymz280b_device::write ) { if ((offset & 1) == 0) m_current_register = data; else { /* force an update */ m_stream->update(); write_to_register(data); } } DEFINE_DEVICE_TYPE(YMZ280B, ymz280b_device, "ymz280b", "Yamaha YMZ280B PCMD8") ymz280b_device::ymz280b_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : device_t(mconfig, YMZ280B, tag, owner, clock) , device_sound_interface(mconfig, *this) , device_rom_interface(mconfig, *this, 24) , m_current_register(0) , m_status_register(0) , m_irq_state(0) , m_irq_mask(0) , m_irq_enable(0) , m_keyon_enable(0) , m_ext_mem_enable(0) , m_ext_readlatch(0) , m_ext_mem_address_hi(0) , m_ext_mem_address_mid(0) , m_ext_mem_address(0) , m_irq_handler(*this) { memset(m_voice, 0, sizeof(m_voice)); }