From c9b8925e5d5b40d0d5f2432d943f2d6e630f131a Mon Sep 17 00:00:00 2001 From: m1macrophage <168948267+m1macrophage@users.noreply.github.com> Date: Tue, 28 Jan 2025 23:50:09 -0800 Subject: alesis/midiverb.cpp: DSP emulation. Transitioned to working. (#13283) * alesis/midiverb.cpp: DSP emulation. Transitioned to working. * Addressing review feedback. --- src/mame/alesis/midiverb.cpp | 210 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 174 insertions(+), 36 deletions(-) diff --git a/src/mame/alesis/midiverb.cpp b/src/mame/alesis/midiverb.cpp index 81b797a8ff9..9d10a5fd78e 100644 --- a/src/mame/alesis/midiverb.cpp +++ b/src/mame/alesis/midiverb.cpp @@ -9,9 +9,6 @@ The computer portion of the device is very simple. The firmware runs on a displays, and listens to MIDI for program changes. It also controls which program (effect) is running on the DSP. -An interesting aspect of the Midiverb is its DSP, which is built out of discrete -logic components. This runs custom microcode consisting of 4 instructions. - The UI is very simple. The user can choose one of 63 effects by using the "up" and "down" buttons on the unit. The effect can also be set via MIDI program changes, and the MIDI channel is configurable ("channel" button). The @@ -20,12 +17,18 @@ silence program is also enabled temporarily when switching between effects. Finally, there is a wet/dry control knob. For more information on the audio hardware, see midiverb_state::configure_audio(). +An interesting aspect of the Midiverb is its DSP, which is built out of discrete +logic components and runs custom microcode. Each microcode instruction consists +of a 2-bit opcode and 14-bit RAM delta offset. The effects program makes up the +top 6 bits of the microcode ROM address, and the DSP just loops over the 128 +instructions of each program. There are also pre-determined program counter +addresses where specific functions are performed (e.g. ADC, DAC). For more info, +see midiverb_dsp::sound_stream_update(). + This driver is based on https://www.youtube.com/watch?v=JNPpU08YZjk and https://www.youtube.com/watch?v=5DYbirWuBaU, and is intended as an educational tool. -TODO: DSP emulation (coming soon). - Usage notes: The driver comes with an interactive layout. @@ -43,13 +46,9 @@ Audio inputs are emulated using MAME's sample playback mechanism. - When the emulation is running, press Space to trigger the processing of those files. - Look out for any errors, such as unsupported file format. -- If there is distortion, adjust INPUT LEVEL (in the Slider Controls menu). +- If there is distortion or crackling, adjust INPUT LEVEL (in the Slider + Controls menu). - Use the "DRY/WET MIX" Slider Control to adjust the wet/dry ratio. - -- At the moment, DSP emulation is mostly a passthrough. It just does a - 12-bit quantization and causes a resampling at its lowish sample rate. But it - should still be possible to hear the difference between the dry and wet - signals, in part due to all the filtering stages. */ #include "emu.h" @@ -68,8 +67,9 @@ Audio inputs are emulated using MAME's sample playback mechanism. #include "alesis_midiverb.lh" #define LOG_PROGRAM_CHANGE (1U << 1) +#define LOG_DSP_EXECUTION (1U << 2) -#define VERBOSE (LOG_GENERAL | LOG_PROGRAM_CHANGE) +#define VERBOSE (LOG_GENERAL) //#define LOG_OUTPUT_FUNC osd_printf_info #include "logmacro.h" @@ -88,8 +88,18 @@ protected: void sound_stream_update(sound_stream &stream, const std::vector &inputs, std::vector &outputs) override; private: + u16 analog_to_digital(float sample) const; + float digital_to_analog(u16 sample) const; + + required_memory_region m_microcode; sound_stream *m_stream = nullptr; + + // State u8 m_program = 0; + u16 m_accum = 0; + u16 m_reg = 0; + u16 m_ram_offset = 0; + std::vector m_ram; // 4 x TMS4416-15 (16K x 4bit). U14, U19, U22, U25. static constexpr const int CLOCKS_PER_INSTRUCTION = 2; static constexpr const int INSTRUCTIONS_PER_SAMPLE = 128; @@ -101,6 +111,8 @@ DEFINE_DEVICE_TYPE(MIDIVERB_DSP, midiverb_dsp, "midiverb_dsp", "MIDIverb discret midiverb_dsp::midiverb_dsp(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : device_t(mconfig, MIDIVERB_DSP, tag, owner, clock) , device_sound_interface(mconfig, *this) + , m_microcode(*this, ":dsp_microcode") + , m_ram(0x4000, 0) // 16K { } @@ -112,7 +124,7 @@ void midiverb_dsp::program_select_w(u8 data) m_stream->update(); m_program = new_program; - LOGMASKED(LOG_PROGRAM_CHANGE, "Program changed to: %d\n", m_program); + LOGMASKED(LOG_PROGRAM_CHANGE, "DSP: Program changed to: %d\n", m_program); } void midiverb_dsp::device_start() @@ -123,10 +135,24 @@ void midiverb_dsp::device_start() m_stream = stream_alloc(1, 2, sample_clock.value()); save_item(NAME(m_program)); + save_item(NAME(m_accum)); + save_item(NAME(m_reg)); + save_item(NAME(m_ram_offset)); + save_item(NAME(m_ram)); } +#define LOG_DSP(...) do { \ + if (sample_i < DEBUG_SAMPLES) \ + LOGMASKED(LOG_DSP_EXECUTION, __VA_ARGS__); \ +} while(0) + void midiverb_dsp::sound_stream_update(sound_stream &stream, const std::vector &inputs, std::vector &outputs) { + static constexpr const u8 MAX_PC = 0x7f; + static constexpr const int DEBUG_SAMPLES = 2; + static constexpr const char* const OP_NAME[4] = + { "ADDHF", "LDHF ", "STPOS", "STNEG" }; + assert(inputs.size() == 1); assert(outputs.size() == 2); @@ -134,28 +160,135 @@ void midiverb_dsp::sound_stream_update(sound_stream &stream, const std::vector -4096 && quantized < 4096); - - // TODO: Implement DSP logic (coming soon). - - // Digital-to-analog conversion uses the 12-bit DAC and 1 extra bit - // (LSB), for a total of 13 bits. The extra bit is implemented by - // conditionally injecting extra current into the current-to-voltage - // converter that follows the DAC. - const float sample_out = DAC_MAX_V * float(quantized) / ((1 << 13) / 2 - 1); - left.put(i, sample_out); - right.put(i, sample_out); + u16 rom_address = rom_base + 2 * (MAX_PC - 1); + for (u8 pc = 0; pc <= MAX_PC; ++pc) + { + // Each microcode instruction consists of two bytes. A 2-bit opcode + // and a 14-bit RAM offset. Microcode execution is pipelined: + // - The RAM offset in each instruction gets added to the current + // RAM address *after* the execution of the instruction, so it + // affects the RAM access of the next instruction. + // - The 2-bit opcode being executed comes from the *previous* + // instruction in ROM. + // - The "program counter" (whose value is used for some control + // signals, see below) leads the instruction currently being + // executed. + + const u8 op = m_microcode->as_u8(rom_address + 1) >> 6; + rom_address = rom_base + 2 * ((pc + MAX_PC) & MAX_PC); + const u8 ram_row = m_microcode->as_u8(rom_address); + const u8 ram_col = m_microcode->as_u8(rom_address + 1) & 0x3f; + const u16 ram_offset_delta = (u16(ram_col) << 8) | ram_row; + + // Control signals. Names match those in: + // https://www.youtube.com/watch?v=JNPpU08YZjk. + const bool mode_rc0 = (pc == 0); // Read from ADC. + const bool ld_dac = (pc == 0x60 || pc == 0x70); // Write to DAC. + const bool dac_left = (pc == 0x70); // Route DAC to left (instead of right) channel. + const bool ld_dsp = !mode_rc0 && !ld_dac; // Write to register. + const bool clear_acc = BIT(op, 0); // Clear the accumulator. + const bool rd_r0 = (op == 0x02); // Place register contents on the bus. + const bool rd_r1 = (op == 0x03); // Place negated register contents on the bus. + const bool dram_w = BIT(op, 1) || mode_rc0; // Write (instead of read) RAM. + + u16 bus_value = 0; + int num_bus_writes = 0; + if (mode_rc0) + { + bus_value = analog_to_digital(in.get(sample_i)); + ++num_bus_writes; + } + if (rd_r0) + { + bus_value = m_reg; + ++num_bus_writes; + } + if (rd_r1) + { + bus_value = ~m_reg; + ++num_bus_writes; + } + if (!dram_w) + { + bus_value = m_ram[m_ram_offset]; + ++num_bus_writes; + } + if (num_bus_writes == 0) + LOG("DSP microcode error: floating bus\n"); + else if (num_bus_writes > 1) + LOG("DSP microcode error: bus conflict %d %d %d %d\n", mode_rc0, rd_r0, rd_r1, !dram_w); + + if (dram_w) + m_ram[m_ram_offset] = bus_value; + + if (ld_dac) + { + if (dac_left) + left.put(sample_i, digital_to_analog(bus_value)); + else + right.put(sample_i, digital_to_analog(bus_value)); + } + + if (clear_acc) + m_accum = 0; + + if (ld_dsp) + { + const u16 sign = BIT(bus_value, 15); + m_accum += ((sign << 15) | (bus_value >> 1)) + sign; + m_reg = m_accum; + } + + LOG_DSP("%04X %02x - DSP OP: %d %s (%04x), A: %6d, R: %6d, bus: %6d, ram: %6d @ %04x", + rom_address, pc, op, OP_NAME[op], ram_offset_delta, m_accum, + m_reg, bus_value, m_ram[m_ram_offset], m_ram_offset); + if (mode_rc0) + LOG_DSP(" [ADC]"); + if (ld_dac) + { + if (dac_left) + LOG_DSP(" [DAC LEFT]"); + else + LOG_DSP(" [DAC RIGHT]"); + } + LOG_DSP("\n"); + + m_ram_offset = (m_ram_offset + ram_offset_delta) & (m_ram.size() - 1); + } + LOG_DSP("\n"); } + LOGMASKED(LOG_DSP_EXECUTION, "\n"); +} + +u16 midiverb_dsp::analog_to_digital(float sample) const +{ + // Analog-to-digital conversion is done with a 12-bit DAC+SAR. + // Note that samples in the stream are treated as voltages (see + // configure_audio()). Convert the voltage to the range: -/+1. + const float transformed = std::clamp(sample, -DAC_MAX_V, DAC_MAX_V) / DAC_MAX_V; + + // Quantize to 12 bits, keeping in mind that the range is -1 - 1 (reason + // for "/ 2"). Then convert to 13 bits ("* 2"). Bit 0 is always set ("+ 1). + const s16 quantized = floorf(transformed * ((1 << 12) / 2 - 1)) * 2 + 1; + assert(quantized > -4096 && quantized < 4096); + + return static_cast(quantized); +} + +float midiverb_dsp::digital_to_analog(u16 sample) const +{ + // Digital-to-analog conversion uses the 12-bit DAC and 1 extra bit + // (LSB), for a total of 13 bits. The extra bit is implemented by + // conditionally injecting extra current into the current-to-voltage + // converter that follows the DAC. There is also saturation logic to detect + // overflows and underflows, and set the DAC to the max or min value + // respectively. + const s16 saturated = std::clamp(static_cast(sample), -4095, 4095); + return DAC_MAX_V * float(saturated) / ((1 << 13) / 2 - 1); } namespace { @@ -502,17 +635,22 @@ INPUT_PORTS_START(midiverb) PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(midiverb_state::audio_input_play), 0) PORT_START("audio_input_level") - PORT_ADJUSTER(50, "INPUT LEVEL") + PORT_ADJUSTER(90, "INPUT LEVEL") PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(midiverb_state::audio_input_level), 0) INPUT_PORTS_END ROM_START(midiverb) - ROM_REGION(0x2000, MAINCPU_TAG, 0) - // U54. 2764 ROM. + ROM_REGION(0x2000, MAINCPU_TAG, 0) // U54. 2764 ROM. ROM_LOAD("mvop_4-7-86.u54", 0x000000, 0x002000, CRC(14d6596d) SHA1(c6dc579d8086556b2dd4909c8deb3c7006293816)) + + ROM_REGION(0x4000, "dsp_microcode", 0) // U51, 27256, 32K ROM, but only 16K used. + ROM_LOAD("mvobj_2-6-86.u51", 0x000000, 0x004000, CRC(1aa25250) SHA1(ebd7ca265540c3420f4259d0eaefecaf6d95a1bf)) + ROM_CONTINUE(0x000000, 0x004000) // A14 (pin 27) tied to VCC. Only upper half used. + // Seems to have also shipped with a 27128 ROM (16K), with the same + // "MVOBJ 2-6-86" label. ROM_END } // anonymous namespace -SYST(1986, midiverb, 0, 0, midiverb, midiverb, midiverb_state, empty_init, "Alesis", "MIDIverb", MACHINE_SUPPORTS_SAVE | MACHINE_NOT_WORKING | MACHINE_NO_SOUND) +SYST(1986, midiverb, 0, 0, midiverb, midiverb, midiverb_state, empty_init, "Alesis", "MIDIverb", MACHINE_SUPPORTS_SAVE) -- cgit v1.2.3-70-g09d2