1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-11-29 12:50:28 +00:00

Merge pull request #1136 from TomHarte/MSX-MUSIC

Add MSX-MUSIC (/FM-PAC) emulation.
This commit is contained in:
Thomas Harte 2023-05-13 22:34:31 -04:00 committed by GitHub
commit ec9abbe6a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 129 additions and 47 deletions

View File

@ -18,6 +18,7 @@ namespace Analyser::Static::MSX {
struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<Target> { struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<Target> {
bool has_disk_drive = false; bool has_disk_drive = false;
bool has_msx_music = true;
std::string loading_command; std::string loading_command;
ReflectableEnum(Model, ReflectableEnum(Model,
@ -36,6 +37,7 @@ struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<
Target(): Analyser::Static::Target(Machine::MSX) { Target(): Analyser::Static::Target(Machine::MSX) {
if(needs_declare()) { if(needs_declare()) {
DeclareField(has_disk_drive); DeclareField(has_disk_drive);
DeclareField(has_msx_music);
DeclareField(region); DeclareField(region);
AnnounceEnum(Region); AnnounceEnum(Region);
DeclareField(model); DeclareField(model);

View File

@ -27,8 +27,9 @@
#include "../../Components/9918/9918.hpp" #include "../../Components/9918/9918.hpp"
#include "../../Components/AudioToggle/AudioToggle.hpp" #include "../../Components/AudioToggle/AudioToggle.hpp"
#include "../../Components/AY38910/AY38910.hpp" #include "../../Components/AY38910/AY38910.hpp"
#include "../../Components/RP5C01/RP5C01.hpp"
#include "../../Components/KonamiSCC/KonamiSCC.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/Parsers/MSX.hpp"
#include "../../Storage/Tape/Tape.hpp" #include "../../Storage/Tape/Tape.hpp"
@ -129,9 +130,49 @@ class AYPortHandler: public GI::AY38910::PortHandler {
}; };
}; };
template <bool has_opll> struct Speaker;
template <> struct Speaker<false> {
Speaker() :
ay(GI::AY38910::Personality::AY38910, audio_queue),
audio_toggle(audio_queue),
scc(audio_queue),
mixer(ay, audio_toggle, scc),
speaker(mixer) {}
Concurrency::AsyncTaskQueue<false> audio_queue;
GI::AY38910::AY38910<false> ay;
Audio::Toggle audio_toggle;
Konami::SCC scc;
using CompundSource = Outputs::Speaker::CompoundSource<GI::AY38910::AY38910<false>, Audio::Toggle, Konami::SCC>;
CompundSource mixer;
Outputs::Speaker::PullLowpass<CompundSource> speaker;
};
template <> struct Speaker<true> {
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<false> audio_queue;
Yamaha::OPL::OPLL opll;
GI::AY38910::AY38910<false> ay;
Audio::Toggle audio_toggle;
Konami::SCC scc;
using CompundSource = Outputs::Speaker::CompoundSource<GI::AY38910::AY38910<false>, Audio::Toggle, Konami::SCC, Yamaha::OPL::OPLL>;
CompundSource mixer;
Outputs::Speaker::PullLowpass<CompundSource> speaker;
};
using Target = Analyser::Static::MSX::Target; using Target = Analyser::Static::MSX::Target;
template <Target::Model model> template <Target::Model model, bool has_opll>
class ConcreteMachine: class ConcreteMachine:
public Machine, public Machine,
public CPU::Z80::BusHandler, public CPU::Z80::BusHandler,
@ -148,32 +189,31 @@ class ConcreteMachine:
private: private:
// Provide 512kb of memory for an MSX 2; 64kb for an MSX 1. 'Slightly' arbitrary. // 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 size_t RAMSize = model == Target::Model::MSX2 ? 512 * 1024 : 64 * 1024;
static constexpr int ClockRate = 3579545; static constexpr int ClockRate = 3579545;
public: public:
ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher): ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher):
z80_(*this), z80_(*this),
i8255_(i8255_port_handler_), 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), 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_), ay_port_handler_(tape_player_),
memory_slots_{{*this}, {*this}, {*this}, {*this}}, memory_slots_{{*this}, {*this}, {*this}, {*this}},
clock_(ClockRate) { clock_(ClockRate) {
set_clock_rate(ClockRate); set_clock_rate(ClockRate);
clear_all_keys(); clear_all_keys();
ay_.set_port_handler(&ay_port_handler_); speaker_.ay.set_port_handler(&ay_port_handler_);
speaker_.set_input_rate(3579545.0f / 2.0f); speaker_.speaker.set_input_rate(3579545.0f / 2.0f);
tape_player_.set_clocking_hint_observer(this); 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. // 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. // Install the proper TV standard and select an ideal BIOS name.
const std::string machine_name = "MSX"; const std::string machine_name = "MSX";
@ -229,11 +269,12 @@ class ConcreteMachine:
// Fetch the necessary ROMs; try the region-specific ROM first, // Fetch the necessary ROMs; try the region-specific ROM first,
// but failing that fall back on patching the main one. // but failing that fall back on patching the main one.
ROM::Request request; ROM::Request request = bios_request;
if(target.has_disk_drive) { if(target.has_disk_drive) {
request = ROM::Request(ROM::Name::MSXDOS) && bios_request; request = request && ROM::Request(ROM::Name::MSXDOS);
} else { }
request = bios_request; if(target.has_msx_music) {
request = request && ROM::Request(ROM::Name::MSXMusic);
} }
auto roms = rom_fetcher(request); auto roms = rom_fetcher(request);
@ -294,6 +335,14 @@ class ConcreteMachine:
disk_slot().map_handler(0x6000, 0x2000); disk_slot().map_handler(0x6000, 0x2000);
} }
// Grab the MSX-MUSIC ROM if applicable.
if(target.has_msx_music) {
std::vector<uint8_t> &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 the media.
insert_media(target.media); insert_media(target.media);
@ -307,7 +356,7 @@ class ConcreteMachine:
} }
~ConcreteMachine() { ~ConcreteMachine() {
audio_queue_.flush(); speaker_.audio_queue.flush();
} }
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final { void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
@ -327,7 +376,7 @@ class ConcreteMachine:
} }
Outputs::Speaker::Speaker *get_speaker() final { Outputs::Speaker::Speaker *get_speaker() final {
return &speaker_; return &speaker_.speaker;
} }
void run_for(const Cycles cycles) final { void run_for(const Cycles cycles) final {
@ -365,7 +414,7 @@ class ConcreteMachine:
cartridge_primary().handler = std::make_unique<Cartridge::KonamiROMSlotHandler>(static_cast<MSX::MemorySlot &>(slot)); cartridge_primary().handler = std::make_unique<Cartridge::KonamiROMSlotHandler>(static_cast<MSX::MemorySlot &>(slot));
break; break;
case Analyser::Static::MSX::Cartridge::KonamiWithSCC: case Analyser::Static::MSX::Cartridge::KonamiWithSCC:
cartridge_primary().handler = std::make_unique<Cartridge::KonamiWithSCCROMSlotHandler>(static_cast<MSX::MemorySlot &>(slot), scc_); cartridge_primary().handler = std::make_unique<Cartridge::KonamiWithSCCROMSlotHandler>(static_cast<MSX::MemorySlot &>(slot), speaker_.scc);
break; break;
case Analyser::Static::MSX::Cartridge::ASCII8kb: case Analyser::Static::MSX::Cartridge::ASCII8kb:
cartridge_primary().handler = std::make_unique<Cartridge::ASCII8kbROMSlotHandler>(static_cast<MSX::MemorySlot &>(slot)); cartridge_primary().handler = std::make_unique<Cartridge::ASCII8kbROMSlotHandler>(static_cast<MSX::MemorySlot &>(slot));
@ -580,7 +629,7 @@ class ConcreteMachine:
case 0xa2: case 0xa2:
update_audio(); update_audio();
*cycle.value = GI::AY38910::Utility::read(ay_); *cycle.value = GI::AY38910::Utility::read(speaker_.ay);
break; break;
case 0xa8: case 0xa9: case 0xa8: case 0xa9:
@ -624,7 +673,7 @@ class ConcreteMachine:
case 0xa0: case 0xa1: case 0xa0: case 0xa1:
update_audio(); update_audio();
GI::AY38910::Utility::write(ay_, port == 0xa1, *cycle.value); GI::AY38910::Utility::write(speaker_.ay, port == 0xa1, *cycle.value);
break; break;
case 0xa8: case 0xa9: case 0xa8: case 0xa9:
@ -668,6 +717,13 @@ class ConcreteMachine:
update_paging(); update_paging();
} break; } break;
case 0x7c: case 0x7d:
if constexpr (has_opll) {
speaker_.opll.write(address, *cycle.value);
break;
}
[[fallthrough]];
default: default:
printf("Unhandled write %02x of %02x\n", address & 0xff, *cycle.value); printf("Unhandled write %02x of %02x\n", address & 0xff, *cycle.value);
break; break;
@ -725,7 +781,7 @@ class ConcreteMachine:
} }
if(outputs & Output::Audio) { if(outputs & Output::Audio) {
update_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 { void set_key_state(uint16_t key, bool is_pressed) final {
int mask = 1 << (key & 7); const int mask = 1 << (key & 7);
int line = key >> 4; const int line = key >> 4;
if(is_pressed) key_states_[line] &= ~mask; else key_states_[line] |= mask; if(is_pressed) key_states_[line] &= ~mask; else key_states_[line] |= mask;
} }
@ -774,7 +830,7 @@ class ConcreteMachine:
// MARK: - Activity::Source // MARK: - Activity::Source
void set_activity_observer(Activity::Observer *observer) final { void set_activity_observer(Activity::Observer *observer) final {
DiskROM *handler = disk_handler(); DiskROM *const handler = disk_handler();
if(handler) { if(handler) {
handler->set_activity_observer(observer); handler->set_activity_observer(observer);
} }
@ -788,7 +844,7 @@ class ConcreteMachine:
private: private:
void update_audio() { 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 { class i8255PortHandler: public Intel::i8255::PortHandler {
@ -855,13 +911,6 @@ class ConcreteMachine:
JustInTimeActor<TI::TMS::TMS9918<vdp_model()>> vdp_; JustInTimeActor<TI::TMS::TMS9918<vdp_model()>> vdp_;
Intel::i8255::i8255<i8255PortHandler> i8255_; Intel::i8255::i8255<i8255PortHandler> i8255_;
Concurrency::AsyncTaskQueue<false> audio_queue_;
GI::AY38910::AY38910<false> ay_;
Audio::Toggle audio_toggle_;
Konami::SCC scc_;
Outputs::Speaker::CompoundSource<GI::AY38910::AY38910<false>, Audio::Toggle, Konami::SCC> mixer_;
Outputs::Speaker::PullLowpass<Outputs::Speaker::CompoundSource<GI::AY38910::AY38910<false>, Audio::Toggle, Konami::SCC>> speaker_;
Storage::Tape::BinaryTapePlayer tape_player_; Storage::Tape::BinaryTapePlayer tape_player_;
bool tape_player_is_sleeping_ = false; bool tape_player_is_sleeping_ = false;
bool allow_fast_tape_ = false; bool allow_fast_tape_ = false;
@ -876,6 +925,7 @@ class ConcreteMachine:
} }
i8255PortHandler i8255_port_handler_; i8255PortHandler i8255_port_handler_;
Speaker<has_opll> speaker_;
AYPortHandler ay_port_handler_; AYPortHandler ay_port_handler_;
/// The current primary and secondary slot selections; the former retains whatever was written /// 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-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() { MemorySlot &bios_slot() {
return memory_slots_[0].subslot(0); return memory_slots_[0].subslot(0);
@ -950,6 +1000,9 @@ class ConcreteMachine:
MemorySlot &extension_rom_slot() { MemorySlot &extension_rom_slot() {
return memory_slots_[3].subslot(1); return memory_slots_[3].subslot(1);
} }
MemorySlot &msx_music_slot() {
return memory_slots_[3].subslot(2);
}
MemorySlot &cartridge_slot() { MemorySlot &cartridge_slot() {
return cartridge_primary().subslot(0); return cartridge_primary().subslot(0);
@ -970,7 +1023,8 @@ class ConcreteMachine:
} }
DiskROM *disk_handler() { DiskROM *disk_handler() {
return dynamic_cast<DiskROM *>(disk_primary().handler.get()); return dynamic_cast<DiskROM *>(disk_primary().handler.get());
}}; }
};
} }
@ -978,10 +1032,18 @@ using namespace MSX;
Machine *Machine::MSX(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { Machine *Machine::MSX(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
const auto msx_target = dynamic_cast<const Target *>(target); const auto msx_target = dynamic_cast<const Target *>(target);
if(msx_target->has_msx_music) {
switch(msx_target->model) { switch(msx_target->model) {
default: return nullptr; default: return nullptr;
case Target::Model::MSX1: return new ConcreteMachine<Target::Model::MSX1>(*msx_target, rom_fetcher); case Target::Model::MSX1: return new ConcreteMachine<Target::Model::MSX1, true>(*msx_target, rom_fetcher);
case Target::Model::MSX2: return new ConcreteMachine<Target::Model::MSX2>(*msx_target, rom_fetcher); case Target::Model::MSX2: return new ConcreteMachine<Target::Model::MSX2, true>(*msx_target, rom_fetcher);
}
} else {
switch(msx_target->model) {
default: return nullptr;
case Target::Model::MSX1: return new ConcreteMachine<Target::Model::MSX1, false>(*msx_target, rom_fetcher);
case Target::Model::MSX2: return new ConcreteMachine<Target::Model::MSX2, false>(*msx_target, rom_fetcher);
}
} }
} }

View File

@ -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::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::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: case Name::SinclairQLJS:
*this = Description(name, "SinclairQL", "the Sinclair QL 'JS' ROM", "js.rom", 48*1024, 0x0f95aab5u); *this = Description(name, "SinclairQL", "the Sinclair QL 'JS' ROM", "js.rom", 48*1024, 0x0f95aab5u);

View File

@ -119,6 +119,7 @@ enum Name {
MSX2GenericBIOS, MSX2GenericBIOS,
MSX2Extension, MSX2Extension,
MSXMusic,
// Oric. // Oric.
OricColourROM, OricColourROM,

View File

@ -137,7 +137,7 @@ typedef int Kilobytes;
- (instancetype)initWithElectronDFS:(BOOL)dfs adfs:(BOOL)adfs ap6:(BOOL)ap6 sidewaysRAM:(BOOL)sidewaysRAM; - (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)initWithEnterpriseModel:(CSMachineEnterpriseModel)model speed:(CSMachineEnterpriseSpeed)speed exosVersion:(CSMachineEnterpriseEXOS)exosVersion basicVersion:(CSMachineEnterpriseBASIC)basicVersion dos:(CSMachineEnterpriseDOS)dos;
- (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model; - (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)initWithOricModel:(CSMachineOricModel)model diskInterface:(CSMachineOricDiskInterface)diskInterface;
- (instancetype)initWithSpectrumModel:(CSMachineSpectrumModel)model; - (instancetype)initWithSpectrumModel:(CSMachineSpectrumModel)model;
- (instancetype)initWithVic20Region:(CSMachineVic20Region)region memorySize:(Kilobytes)memorySize hasC1540:(BOOL)hasC1540; - (instancetype)initWithVic20Region:(CSMachineVic20Region)region memorySize:(Kilobytes)memorySize hasC1540:(BOOL)hasC1540;

View File

@ -229,12 +229,13 @@
return self; 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]; self = [super init];
if(self) { if(self) {
using Target = Analyser::Static::MSX::Target; using Target = Analyser::Static::MSX::Target;
auto target = std::make_unique<Target>(); auto target = std::make_unique<Target>();
target->has_disk_drive = !!hasDiskDrive; target->has_disk_drive = hasDiskDrive;
target->has_msx_music = hasMSXMUSIC;
switch(region) { switch(region) {
case CSMachineMSXRegionAmerican: target->region = Target::Region::USA; break; case CSMachineMSXRegionAmerican: target->region = Target::Region::USA; break;
case CSMachineMSXRegionEuropean: target->region = Target::Region::Europe; break; case CSMachineMSXRegionEuropean: target->region = Target::Region::Europe; break;

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies> <dependencies>
<deployment identifier="macosx"/> <deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21701"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
@ -18,7 +18,7 @@
<windowStyleMask key="styleMask" titled="YES" documentModal="YES"/> <windowStyleMask key="styleMask" titled="YES" documentModal="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="590" height="353"/> <rect key="contentRect" x="196" y="240" width="590" height="353"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1440"/> <rect key="screenRect" x="0.0" y="0.0" width="1800" height="1131"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ"> <view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="590" height="353"/> <rect key="frame" x="0.0" y="0.0" width="590" height="353"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@ -628,6 +628,13 @@ Gw
</menu> </menu>
</popUpButtonCell> </popUpButtonCell>
</popUpButton> </popUpButton>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="V0e-KX-Wqn">
<rect key="frame" x="18" y="105" width="145" height="18"/>
<buttonCell key="cell" type="check" title="Attach MSX-MUSIC" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="DjG-qd-BYh">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="LG6-mP-SeG" secondAttribute="trailing" constant="20" symbolic="YES" id="0Oc-n7-gaM"/> <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="LG6-mP-SeG" secondAttribute="trailing" constant="20" symbolic="YES" id="0Oc-n7-gaM"/>
@ -636,11 +643,14 @@ Gw
<constraint firstItem="44m-4z-PVv" firstAttribute="leading" secondItem="gFV-RB-7dB" secondAttribute="trailing" constant="8" symbolic="YES" id="JG1-Kp-dYk"/> <constraint firstItem="44m-4z-PVv" firstAttribute="leading" secondItem="gFV-RB-7dB" secondAttribute="trailing" constant="8" symbolic="YES" id="JG1-Kp-dYk"/>
<constraint firstItem="8xT-Pr-8SE" firstAttribute="top" secondItem="LG6-mP-SeG" secondAttribute="bottom" constant="8" symbolic="YES" id="LBt-4m-GDc"/> <constraint firstItem="8xT-Pr-8SE" firstAttribute="top" secondItem="LG6-mP-SeG" secondAttribute="bottom" constant="8" symbolic="YES" id="LBt-4m-GDc"/>
<constraint firstItem="gFV-RB-7dB" firstAttribute="centerY" secondItem="44m-4z-PVv" secondAttribute="centerY" id="Rf8-1i-ttI"/> <constraint firstItem="gFV-RB-7dB" firstAttribute="centerY" secondItem="44m-4z-PVv" secondAttribute="centerY" id="Rf8-1i-ttI"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="V0e-KX-Wqn" secondAttribute="bottom" constant="20" symbolic="YES" id="YIT-tx-yob"/>
<constraint firstItem="gFV-RB-7dB" firstAttribute="leading" secondItem="mWD-An-tR7" secondAttribute="leading" constant="20" symbolic="YES" id="aaL-4g-iW8"/> <constraint firstItem="gFV-RB-7dB" firstAttribute="leading" secondItem="mWD-An-tR7" secondAttribute="leading" constant="20" symbolic="YES" id="aaL-4g-iW8"/>
<constraint firstItem="V0e-KX-Wqn" firstAttribute="top" secondItem="8xT-Pr-8SE" secondAttribute="bottom" constant="6" symbolic="YES" id="fjr-Qf-WzP"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="8xT-Pr-8SE" secondAttribute="trailing" constant="20" symbolic="YES" id="l8P-UW-8ig"/> <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="8xT-Pr-8SE" secondAttribute="trailing" constant="20" symbolic="YES" id="l8P-UW-8ig"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="8xT-Pr-8SE" secondAttribute="bottom" constant="20" symbolic="YES" id="mga-YX-Bek"/> <constraint firstItem="V0e-KX-Wqn" firstAttribute="leading" secondItem="mWD-An-tR7" secondAttribute="leading" constant="20" symbolic="YES" id="q1J-kN-Gwi"/>
<constraint firstItem="8xT-Pr-8SE" firstAttribute="leading" secondItem="mWD-An-tR7" secondAttribute="leading" constant="20" symbolic="YES" id="q8Q-kh-Opj"/> <constraint firstItem="8xT-Pr-8SE" firstAttribute="leading" secondItem="mWD-An-tR7" secondAttribute="leading" constant="20" symbolic="YES" id="q8Q-kh-Opj"/>
<constraint firstItem="LG6-mP-SeG" firstAttribute="leading" secondItem="ZaD-7v-rMS" secondAttribute="trailing" constant="8" symbolic="YES" id="svb-nH-GlP"/> <constraint firstItem="LG6-mP-SeG" firstAttribute="leading" secondItem="ZaD-7v-rMS" secondAttribute="trailing" constant="8" symbolic="YES" id="svb-nH-GlP"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="V0e-KX-Wqn" secondAttribute="trailing" constant="20" symbolic="YES" id="w85-NH-WOz"/>
<constraint firstItem="44m-4z-PVv" firstAttribute="top" secondItem="mWD-An-tR7" secondAttribute="top" constant="20" symbolic="YES" id="zJk-w1-PWX"/> <constraint firstItem="44m-4z-PVv" firstAttribute="top" secondItem="mWD-An-tR7" secondAttribute="top" constant="20" symbolic="YES" id="zJk-w1-PWX"/>
<constraint firstItem="ZaD-7v-rMS" firstAttribute="leading" secondItem="mWD-An-tR7" secondAttribute="leading" constant="20" symbolic="YES" id="zgh-a5-FNF"/> <constraint firstItem="ZaD-7v-rMS" firstAttribute="leading" secondItem="mWD-An-tR7" secondAttribute="leading" constant="20" symbolic="YES" id="zgh-a5-FNF"/>
<constraint firstItem="ZaD-7v-rMS" firstAttribute="centerY" secondItem="LG6-mP-SeG" secondAttribute="centerY" id="zkw-kD-lRV"/> <constraint firstItem="ZaD-7v-rMS" firstAttribute="centerY" secondItem="LG6-mP-SeG" secondAttribute="centerY" id="zkw-kD-lRV"/>
@ -1017,6 +1027,7 @@ Gw
<outlet property="machineSelector" destination="VUb-QG-x7c" id="crR-hB-jGd"/> <outlet property="machineSelector" destination="VUb-QG-x7c" id="crR-hB-jGd"/>
<outlet property="macintoshModelTypeButton" destination="xa6-NA-JY5" id="2jX-PY-v2z"/> <outlet property="macintoshModelTypeButton" destination="xa6-NA-JY5" id="2jX-PY-v2z"/>
<outlet property="msxHasDiskDriveButton" destination="8xT-Pr-8SE" id="zGH-GA-9QF"/> <outlet property="msxHasDiskDriveButton" destination="8xT-Pr-8SE" id="zGH-GA-9QF"/>
<outlet property="msxHasMSXMUSICButton" destination="V0e-KX-Wqn" id="L68-4u-P6r"/>
<outlet property="msxModelButton" destination="44m-4z-PVv" id="qRk-Hf-aBL"/> <outlet property="msxModelButton" destination="44m-4z-PVv" id="qRk-Hf-aBL"/>
<outlet property="msxRegionButton" destination="LG6-mP-SeG" id="3a9-VG-6Wf"/> <outlet property="msxRegionButton" destination="LG6-mP-SeG" id="3a9-VG-6Wf"/>
<outlet property="oricDiskInterfaceButton" destination="fYL-p6-wyn" id="aAt-wM-hRZ"/> <outlet property="oricDiskInterfaceButton" destination="fYL-p6-wyn" id="aAt-wM-hRZ"/>

View File

@ -57,6 +57,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
@IBOutlet var msxModelButton: NSPopUpButton! @IBOutlet var msxModelButton: NSPopUpButton!
@IBOutlet var msxRegionButton: NSPopUpButton! @IBOutlet var msxRegionButton: NSPopUpButton!
@IBOutlet var msxHasDiskDriveButton: NSButton! @IBOutlet var msxHasDiskDriveButton: NSButton!
@IBOutlet var msxHasMSXMUSICButton: NSButton!
// MARK: - Oric properties // MARK: - Oric properties
@IBOutlet var oricModelTypeButton: NSPopUpButton! @IBOutlet var oricModelTypeButton: NSPopUpButton!
@ -138,6 +139,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
msxModelButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.msxModel")) msxModelButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.msxModel"))
msxRegionButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.msxRegion")) msxRegionButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.msxRegion"))
msxHasDiskDriveButton.state = standardUserDefaults.bool(forKey: "new.msxDiskDrive") ? .on : .off msxHasDiskDriveButton.state = standardUserDefaults.bool(forKey: "new.msxDiskDrive") ? .on : .off
msxHasMSXMUSICButton.state = standardUserDefaults.bool(forKey: "new.msxMSXMUSIC") ? .on : .off
// Oric settings // Oric settings
oricDiskInterfaceButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.oricDiskInterface")) 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(msxModelButton.selectedTag(), forKey: "new.msxModel")
standardUserDefaults.set(msxRegionButton.selectedTag(), forKey: "new.msxRegion") standardUserDefaults.set(msxRegionButton.selectedTag(), forKey: "new.msxRegion")
standardUserDefaults.set(msxHasDiskDriveButton.state == .on, forKey: "new.msxDiskDrive") standardUserDefaults.set(msxHasDiskDriveButton.state == .on, forKey: "new.msxDiskDrive")
standardUserDefaults.set(msxHasMSXMUSICButton.state == .on, forKey: "new.msxMSXMUSIC")
// Oric settings // Oric settings
standardUserDefaults.set(oricDiskInterfaceButton.selectedTag(), forKey: "new.oricDiskInterface") standardUserDefaults.set(oricDiskInterfaceButton.selectedTag(), forKey: "new.oricDiskInterface")
@ -358,6 +361,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
case "msx": case "msx":
let hasDiskDrive = msxHasDiskDriveButton.state == .on let hasDiskDrive = msxHasDiskDriveButton.state == .on
let hasMSXMUSIC = msxHasMSXMUSICButton.state == .on
var region: CSMachineMSXRegion var region: CSMachineMSXRegion
switch msxRegionButton.selectedTag() { switch msxRegionButton.selectedTag() {
case 2: region = .japanese case 2: region = .japanese
@ -371,7 +375,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
case 1: fallthrough case 1: fallthrough
default: model = .MSX1 default: model = .MSX1
} }
return CSStaticAnalyser(msxModel: model, region: region, hasDiskDrive: hasDiskDrive) return CSStaticAnalyser(msxModel: model, region: region, hasDiskDrive: hasDiskDrive, hasMSXMUSIC: hasMSXMUSIC)
case "oric": case "oric":
var diskInterface: CSMachineOricDiskInterface = .none var diskInterface: CSMachineOricDiskInterface = .none