diff --git a/Analyser/Static/MSX/Target.hpp b/Analyser/Static/MSX/Target.hpp index 4b6ae5185..e34386eec 100644 --- a/Analyser/Static/MSX/Target.hpp +++ b/Analyser/Static/MSX/Target.hpp @@ -18,6 +18,7 @@ namespace Analyser::Static::MSX { struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl { bool has_disk_drive = false; + bool has_msx_music = true; std::string loading_command; ReflectableEnum(Model, @@ -36,6 +37,7 @@ struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl< Target(): Analyser::Static::Target(Machine::MSX) { if(needs_declare()) { DeclareField(has_disk_drive); + DeclareField(has_msx_music); DeclareField(region); AnnounceEnum(Region); DeclareField(model); diff --git a/Machines/MSX/MSX.cpp b/Machines/MSX/MSX.cpp index d341ce2da..7a5c3a7ca 100644 --- a/Machines/MSX/MSX.cpp +++ b/Machines/MSX/MSX.cpp @@ -27,8 +27,9 @@ #include "../../Components/9918/9918.hpp" #include "../../Components/AudioToggle/AudioToggle.hpp" #include "../../Components/AY38910/AY38910.hpp" -#include "../../Components/RP5C01/RP5C01.hpp" #include "../../Components/KonamiSCC/KonamiSCC.hpp" +#include "../../Components/OPx/OPLL.hpp" +#include "../../Components/RP5C01/RP5C01.hpp" #include "../../Storage/Tape/Parsers/MSX.hpp" #include "../../Storage/Tape/Tape.hpp" @@ -129,9 +130,49 @@ class AYPortHandler: public GI::AY38910::PortHandler { }; }; +template struct Speaker; + +template <> struct Speaker { + Speaker() : + ay(GI::AY38910::Personality::AY38910, audio_queue), + audio_toggle(audio_queue), + scc(audio_queue), + mixer(ay, audio_toggle, scc), + speaker(mixer) {} + + Concurrency::AsyncTaskQueue audio_queue; + GI::AY38910::AY38910 ay; + Audio::Toggle audio_toggle; + Konami::SCC scc; + + using CompundSource = Outputs::Speaker::CompoundSource, Audio::Toggle, Konami::SCC>; + CompundSource mixer; + Outputs::Speaker::PullLowpass speaker; +}; + +template <> struct Speaker { + Speaker() : + opll(audio_queue, 1), + ay(GI::AY38910::Personality::AY38910, audio_queue), + audio_toggle(audio_queue), + scc(audio_queue), + mixer(ay, audio_toggle, scc, opll), + speaker(mixer) {} + + Concurrency::AsyncTaskQueue audio_queue; + Yamaha::OPL::OPLL opll; + GI::AY38910::AY38910 ay; + Audio::Toggle audio_toggle; + Konami::SCC scc; + + using CompundSource = Outputs::Speaker::CompoundSource, Audio::Toggle, Konami::SCC, Yamaha::OPL::OPLL>; + CompundSource mixer; + Outputs::Speaker::PullLowpass speaker; +}; + using Target = Analyser::Static::MSX::Target; -template +template class ConcreteMachine: public Machine, public CPU::Z80::BusHandler, @@ -148,32 +189,31 @@ class ConcreteMachine: private: // Provide 512kb of memory for an MSX 2; 64kb for an MSX 1. 'Slightly' arbitrary. static constexpr size_t RAMSize = model == Target::Model::MSX2 ? 512 * 1024 : 64 * 1024; - static constexpr int ClockRate = 3579545; public: ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher): z80_(*this), i8255_(i8255_port_handler_), - ay_(GI::AY38910::Personality::AY38910, audio_queue_), - audio_toggle_(audio_queue_), - scc_(audio_queue_), - mixer_(ay_, audio_toggle_, scc_), - speaker_(mixer_), tape_player_(3579545 * 2), - i8255_port_handler_(*this, audio_toggle_, tape_player_), + i8255_port_handler_(*this, speaker_.audio_toggle, tape_player_), ay_port_handler_(tape_player_), memory_slots_{{*this}, {*this}, {*this}, {*this}}, clock_(ClockRate) { set_clock_rate(ClockRate); clear_all_keys(); - ay_.set_port_handler(&ay_port_handler_); - speaker_.set_input_rate(3579545.0f / 2.0f); + speaker_.ay.set_port_handler(&ay_port_handler_); + speaker_.speaker.set_input_rate(3579545.0f / 2.0f); tape_player_.set_clocking_hint_observer(this); // Set the AY to 50% of available volume, the toggle to 10% and leave 40% for an SCC. - mixer_.set_relative_volumes({0.5f, 0.1f, 0.4f}); + // If there is an OPLL, give it equal volume to the AY and expect some clipping. + if constexpr (has_opll) { + speaker_.mixer.set_relative_volumes({0.5f, 0.1f, 0.4f, 0.5f}); + } else { + speaker_.mixer.set_relative_volumes({0.5f, 0.1f, 0.4f}); + } // Install the proper TV standard and select an ideal BIOS name. const std::string machine_name = "MSX"; @@ -229,11 +269,12 @@ class ConcreteMachine: // Fetch the necessary ROMs; try the region-specific ROM first, // but failing that fall back on patching the main one. - ROM::Request request; + ROM::Request request = bios_request; if(target.has_disk_drive) { - request = ROM::Request(ROM::Name::MSXDOS) && bios_request; - } else { - request = bios_request; + request = request && ROM::Request(ROM::Name::MSXDOS); + } + if(target.has_msx_music) { + request = request && ROM::Request(ROM::Name::MSXMusic); } auto roms = rom_fetcher(request); @@ -294,6 +335,14 @@ class ConcreteMachine: disk_slot().map_handler(0x6000, 0x2000); } + // Grab the MSX-MUSIC ROM if applicable. + if(target.has_msx_music) { + std::vector &msx_music = roms.find(ROM::Name::MSXMusic)->second; + msx_music.resize(65536); + msx_music_slot().set_source(msx_music); + msx_music_slot().map(0, 0, 0x10000); + } + // Insert the media. insert_media(target.media); @@ -307,7 +356,7 @@ class ConcreteMachine: } ~ConcreteMachine() { - audio_queue_.flush(); + speaker_.audio_queue.flush(); } void set_scan_target(Outputs::Display::ScanTarget *scan_target) final { @@ -327,7 +376,7 @@ class ConcreteMachine: } Outputs::Speaker::Speaker *get_speaker() final { - return &speaker_; + return &speaker_.speaker; } void run_for(const Cycles cycles) final { @@ -365,7 +414,7 @@ class ConcreteMachine: cartridge_primary().handler = std::make_unique(static_cast(slot)); break; case Analyser::Static::MSX::Cartridge::KonamiWithSCC: - cartridge_primary().handler = std::make_unique(static_cast(slot), scc_); + cartridge_primary().handler = std::make_unique(static_cast(slot), speaker_.scc); break; case Analyser::Static::MSX::Cartridge::ASCII8kb: cartridge_primary().handler = std::make_unique(static_cast(slot)); @@ -580,7 +629,7 @@ class ConcreteMachine: case 0xa2: update_audio(); - *cycle.value = GI::AY38910::Utility::read(ay_); + *cycle.value = GI::AY38910::Utility::read(speaker_.ay); break; case 0xa8: case 0xa9: @@ -624,7 +673,7 @@ class ConcreteMachine: case 0xa0: case 0xa1: update_audio(); - GI::AY38910::Utility::write(ay_, port == 0xa1, *cycle.value); + GI::AY38910::Utility::write(speaker_.ay, port == 0xa1, *cycle.value); break; case 0xa8: case 0xa9: @@ -668,6 +717,13 @@ class ConcreteMachine: update_paging(); } break; + case 0x7c: case 0x7d: + if constexpr (has_opll) { + speaker_.opll.write(address, *cycle.value); + break; + } + [[fallthrough]]; + default: printf("Unhandled write %02x of %02x\n", address & 0xff, *cycle.value); break; @@ -725,7 +781,7 @@ class ConcreteMachine: } if(outputs & Output::Audio) { update_audio(); - audio_queue_.perform(); + speaker_.audio_queue.perform(); } } @@ -742,8 +798,8 @@ class ConcreteMachine: } void set_key_state(uint16_t key, bool is_pressed) final { - int mask = 1 << (key & 7); - int line = key >> 4; + const int mask = 1 << (key & 7); + const int line = key >> 4; if(is_pressed) key_states_[line] &= ~mask; else key_states_[line] |= mask; } @@ -774,7 +830,7 @@ class ConcreteMachine: // MARK: - Activity::Source void set_activity_observer(Activity::Observer *observer) final { - DiskROM *handler = disk_handler(); + DiskROM *const handler = disk_handler(); if(handler) { handler->set_activity_observer(observer); } @@ -788,7 +844,7 @@ class ConcreteMachine: private: void update_audio() { - speaker_.run_for(audio_queue_, time_since_ay_update_.divide_cycles(Cycles(2))); + speaker_.speaker.run_for(speaker_.audio_queue, time_since_ay_update_.divide_cycles(Cycles(2))); } class i8255PortHandler: public Intel::i8255::PortHandler { @@ -855,13 +911,6 @@ class ConcreteMachine: JustInTimeActor> vdp_; Intel::i8255::i8255 i8255_; - Concurrency::AsyncTaskQueue audio_queue_; - GI::AY38910::AY38910 ay_; - Audio::Toggle audio_toggle_; - Konami::SCC scc_; - Outputs::Speaker::CompoundSource, Audio::Toggle, Konami::SCC> mixer_; - Outputs::Speaker::PullLowpass, Audio::Toggle, Konami::SCC>> speaker_; - Storage::Tape::BinaryTapePlayer tape_player_; bool tape_player_is_sleeping_ = false; bool allow_fast_tape_ = false; @@ -876,6 +925,7 @@ class ConcreteMachine: } i8255PortHandler i8255_port_handler_; + Speaker speaker_; AYPortHandler ay_port_handler_; /// The current primary and secondary slot selections; the former retains whatever was written @@ -939,7 +989,7 @@ class ConcreteMachine: // // Slot 3-1 holds the BIOS extension ROM. // - // [Slot 3-2 will likely hold MSX-MUSIC, but that's TODO] + // Slot 3-2 holds the MSX-MUSIC. // MemorySlot &bios_slot() { return memory_slots_[0].subslot(0); @@ -950,6 +1000,9 @@ class ConcreteMachine: MemorySlot &extension_rom_slot() { return memory_slots_[3].subslot(1); } + MemorySlot &msx_music_slot() { + return memory_slots_[3].subslot(2); + } MemorySlot &cartridge_slot() { return cartridge_primary().subslot(0); @@ -970,7 +1023,8 @@ class ConcreteMachine: } DiskROM *disk_handler() { return dynamic_cast(disk_primary().handler.get()); - }}; + } +}; } @@ -978,10 +1032,18 @@ using namespace MSX; Machine *Machine::MSX(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { const auto msx_target = dynamic_cast(target); - switch(msx_target->model) { - default: return nullptr; - case Target::Model::MSX1: return new ConcreteMachine(*msx_target, rom_fetcher); - case Target::Model::MSX2: return new ConcreteMachine(*msx_target, rom_fetcher); + if(msx_target->has_msx_music) { + switch(msx_target->model) { + default: return nullptr; + case Target::Model::MSX1: return new ConcreteMachine(*msx_target, rom_fetcher); + case Target::Model::MSX2: return new ConcreteMachine(*msx_target, rom_fetcher); + } + } else { + switch(msx_target->model) { + default: return nullptr; + case Target::Model::MSX1: return new ConcreteMachine(*msx_target, rom_fetcher); + case Target::Model::MSX2: return new ConcreteMachine(*msx_target, rom_fetcher); + } } } diff --git a/Machines/Utility/ROMCatalogue.cpp b/Machines/Utility/ROMCatalogue.cpp index e5b20cb8d..fec569c69 100644 --- a/Machines/Utility/ROMCatalogue.cpp +++ b/Machines/Utility/ROMCatalogue.cpp @@ -576,6 +576,7 @@ Description::Description(Name name) { case Name::MSX2GenericBIOS: *this = Description(name, "MSX", "a generic MSX2 BIOS", "msx2.rom", 32*1024, 0x6cdaf3a5u); break; case Name::MSX2Extension: *this = Description(name, "MSX", "the MSX2 extension ROM", "msx2ext.rom", 16*1024, 0x66237ecfu); break; + case Name::MSXMusic: *this = Description(name, "MSX", "the MSX-MUSIC / FM-PAC ROM", "fmpac.rom", 64*1024, 0x0e84505du); break; case Name::SinclairQLJS: *this = Description(name, "SinclairQL", "the Sinclair QL 'JS' ROM", "js.rom", 48*1024, 0x0f95aab5u); diff --git a/Machines/Utility/ROMCatalogue.hpp b/Machines/Utility/ROMCatalogue.hpp index e085cbf09..475bab65d 100644 --- a/Machines/Utility/ROMCatalogue.hpp +++ b/Machines/Utility/ROMCatalogue.hpp @@ -119,6 +119,7 @@ enum Name { MSX2GenericBIOS, MSX2Extension, + MSXMusic, // Oric. OricColourROM, diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h index ddc82dd54..bcb958cb1 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h @@ -137,7 +137,7 @@ typedef int Kilobytes; - (instancetype)initWithElectronDFS:(BOOL)dfs adfs:(BOOL)adfs ap6:(BOOL)ap6 sidewaysRAM:(BOOL)sidewaysRAM; - (instancetype)initWithEnterpriseModel:(CSMachineEnterpriseModel)model speed:(CSMachineEnterpriseSpeed)speed exosVersion:(CSMachineEnterpriseEXOS)exosVersion basicVersion:(CSMachineEnterpriseBASIC)basicVersion dos:(CSMachineEnterpriseDOS)dos; - (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model; -- (instancetype)initWithMSXModel:(CSMachineMSXModel)model region:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive; +- (instancetype)initWithMSXModel:(CSMachineMSXModel)model region:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive hasMSXMUSIC:(BOOL)hasMSXMUSIC; - (instancetype)initWithOricModel:(CSMachineOricModel)model diskInterface:(CSMachineOricDiskInterface)diskInterface; - (instancetype)initWithSpectrumModel:(CSMachineSpectrumModel)model; - (instancetype)initWithVic20Region:(CSMachineVic20Region)region memorySize:(Kilobytes)memorySize hasC1540:(BOOL)hasC1540; diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm index 2ef4dc36c..dbca34373 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm @@ -229,12 +229,13 @@ return self; } -- (instancetype)initWithMSXModel:(CSMachineMSXModel)model region:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive { +- (instancetype)initWithMSXModel:(CSMachineMSXModel)model region:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive hasMSXMUSIC:(BOOL)hasMSXMUSIC { self = [super init]; if(self) { using Target = Analyser::Static::MSX::Target; auto target = std::make_unique(); - target->has_disk_drive = !!hasDiskDrive; + target->has_disk_drive = hasDiskDrive; + target->has_msx_music = hasMSXMUSIC; switch(region) { case CSMachineMSXRegionAmerican: target->region = Target::Region::USA; break; case CSMachineMSXRegionEuropean: target->region = Target::Region::Europe; break; diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib index d0557d959..c3733c142 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib +++ b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib @@ -1,8 +1,8 @@ - + - + @@ -18,7 +18,7 @@ - + @@ -628,6 +628,13 @@ Gw + @@ -636,11 +643,14 @@ Gw + + - + + @@ -1017,6 +1027,7 @@ Gw + diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift b/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift index 2d37addc2..74fd0da35 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift +++ b/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift @@ -57,6 +57,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { @IBOutlet var msxModelButton: NSPopUpButton! @IBOutlet var msxRegionButton: NSPopUpButton! @IBOutlet var msxHasDiskDriveButton: NSButton! + @IBOutlet var msxHasMSXMUSICButton: NSButton! // MARK: - Oric properties @IBOutlet var oricModelTypeButton: NSPopUpButton! @@ -138,6 +139,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { msxModelButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.msxModel")) msxRegionButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.msxRegion")) msxHasDiskDriveButton.state = standardUserDefaults.bool(forKey: "new.msxDiskDrive") ? .on : .off + msxHasMSXMUSICButton.state = standardUserDefaults.bool(forKey: "new.msxMSXMUSIC") ? .on : .off // Oric settings oricDiskInterfaceButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.oricDiskInterface")) @@ -203,6 +205,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { standardUserDefaults.set(msxModelButton.selectedTag(), forKey: "new.msxModel") standardUserDefaults.set(msxRegionButton.selectedTag(), forKey: "new.msxRegion") standardUserDefaults.set(msxHasDiskDriveButton.state == .on, forKey: "new.msxDiskDrive") + standardUserDefaults.set(msxHasMSXMUSICButton.state == .on, forKey: "new.msxMSXMUSIC") // Oric settings standardUserDefaults.set(oricDiskInterfaceButton.selectedTag(), forKey: "new.oricDiskInterface") @@ -358,6 +361,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { case "msx": let hasDiskDrive = msxHasDiskDriveButton.state == .on + let hasMSXMUSIC = msxHasMSXMUSICButton.state == .on var region: CSMachineMSXRegion switch msxRegionButton.selectedTag() { case 2: region = .japanese @@ -371,7 +375,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { case 1: fallthrough default: model = .MSX1 } - return CSStaticAnalyser(msxModel: model, region: region, hasDiskDrive: hasDiskDrive) + return CSStaticAnalyser(msxModel: model, region: region, hasDiskDrive: hasDiskDrive, hasMSXMUSIC: hasMSXMUSIC) case "oric": var diskInterface: CSMachineOricDiskInterface = .none