mirror of
https://github.com/TomHarte/CLK.git
synced 2024-08-17 07:29:04 +00:00
Which has implied getting much more specific about MSX disk drive attachment, and has prompted an excuse to offer the ZX80 with the ZX81 ROM.
214 lines
6.2 KiB
Swift
214 lines
6.2 KiB
Swift
//
|
|
// MachineDocument.swift
|
|
// Clock Signal
|
|
//
|
|
// Created by Thomas Harte on 04/01/2016.
|
|
// Copyright © 2016 Thomas Harte. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
import AudioToolbox
|
|
|
|
class MachineDocument:
|
|
NSDocument,
|
|
NSWindowDelegate,
|
|
CSMachineDelegate,
|
|
CSOpenGLViewDelegate,
|
|
CSOpenGLViewResponderDelegate,
|
|
CSBestEffortUpdaterDelegate,
|
|
CSAudioQueueDelegate
|
|
{
|
|
fileprivate let actionLock = NSLock()
|
|
fileprivate let drawLock = NSLock()
|
|
fileprivate let bestEffortLock = NSLock()
|
|
|
|
var machine: CSMachine!
|
|
var name: String! {
|
|
get {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func aspectRatio() -> NSSize {
|
|
return NSSize(width: 4.0, height: 3.0)
|
|
}
|
|
|
|
@IBOutlet weak var openGLView: CSOpenGLView!
|
|
@IBOutlet var optionsPanel: MachinePanel!
|
|
@IBAction func showOptions(_ sender: AnyObject!) {
|
|
optionsPanel?.setIsVisible(true)
|
|
}
|
|
|
|
fileprivate var audioQueue: CSAudioQueue! = nil
|
|
fileprivate var bestEffortUpdater: CSBestEffortUpdater?
|
|
|
|
override var windowNibName: NSNib.Name? {
|
|
return NSNib.Name(rawValue: "MachineDocument")
|
|
}
|
|
|
|
override func windowControllerDidLoadNib(_ aController: NSWindowController) {
|
|
super.windowControllerDidLoadNib(aController)
|
|
aController.window?.contentAspectRatio = self.aspectRatio()
|
|
setupMachineOutput()
|
|
}
|
|
|
|
fileprivate func setupMachineOutput() {
|
|
if let machine = self.machine, let openGLView = self.openGLView {
|
|
// establish the output aspect ratio and audio
|
|
let aspectRatio = self.aspectRatio()
|
|
openGLView.perform(glContext: {
|
|
machine.setView(openGLView, aspectRatio: Float(aspectRatio.width / aspectRatio.height))
|
|
})
|
|
|
|
machine.delegate = self
|
|
self.bestEffortUpdater = CSBestEffortUpdater()
|
|
|
|
// callbacks from the OpenGL may come on a different thread, immediately following the .delegate set;
|
|
// hence the full setup of the best-effort updater prior to setting self as a delegate
|
|
openGLView.delegate = self
|
|
openGLView.responderDelegate = self
|
|
|
|
setupAudioQueueClockRate()
|
|
|
|
// bring OpenGL view-holding window on top of the options panel
|
|
openGLView.window!.makeKeyAndOrderFront(self)
|
|
|
|
// start accepting best effort updates
|
|
self.bestEffortUpdater!.delegate = self
|
|
}
|
|
}
|
|
|
|
func machineSpeakerDidChangeInputClock(_ machine: CSMachine!) {
|
|
setupAudioQueueClockRate()
|
|
}
|
|
|
|
fileprivate func setupAudioQueueClockRate() {
|
|
// establish and provide the audio queue, taking advice as to an appropriate sampling rate
|
|
let maximumSamplingRate = CSAudioQueue.preferredSamplingRate()
|
|
let selectedSamplingRate = self.machine.idealSamplingRate(from: NSRange(location: 0, length: NSInteger(maximumSamplingRate)))
|
|
if selectedSamplingRate > 0 {
|
|
audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate))
|
|
audioQueue.delegate = self
|
|
self.machine.audioQueue = self.audioQueue
|
|
self.machine.setAudioSamplingRate(selectedSamplingRate, bufferSize:audioQueue.preferredBufferSize)
|
|
}
|
|
}
|
|
|
|
override func close() {
|
|
optionsPanel?.setIsVisible(false)
|
|
optionsPanel = nil
|
|
|
|
bestEffortLock.lock()
|
|
if let bestEffortUpdater = bestEffortUpdater {
|
|
bestEffortUpdater.delegate = nil
|
|
bestEffortUpdater.flush()
|
|
self.bestEffortUpdater = nil
|
|
}
|
|
bestEffortLock.unlock()
|
|
|
|
actionLock.lock()
|
|
drawLock.lock()
|
|
machine = nil
|
|
openGLView.delegate = nil
|
|
openGLView.invalidate()
|
|
actionLock.unlock()
|
|
drawLock.unlock()
|
|
|
|
super.close()
|
|
}
|
|
|
|
// MARK: configuring
|
|
func configureAs(_ analysis: CSStaticAnalyser) {
|
|
if let machine = CSMachine(analyser: analysis) {
|
|
self.machine = machine
|
|
setupMachineOutput()
|
|
|
|
if let optionsPanelNibName = analysis.optionsPanelNibName {
|
|
Bundle.main.loadNibNamed(NSNib.Name(rawValue: optionsPanelNibName), owner: self, topLevelObjects: nil)
|
|
self.optionsPanel.machine = self.machine
|
|
self.optionsPanel?.establishStoredOptions()
|
|
showOptions(self)
|
|
}
|
|
}
|
|
}
|
|
|
|
override func read(from url: URL, ofType typeName: String) throws {
|
|
if let analyser = CSStaticAnalyser(fileAt: url) {
|
|
self.displayName = analyser.displayName
|
|
self.configureAs(analyser)
|
|
} else {
|
|
throw NSError(domain: "MachineDocument", code: -1, userInfo: nil)
|
|
}
|
|
}
|
|
|
|
// MARK: the pasteboard
|
|
func paste(_ sender: AnyObject!) {
|
|
let pasteboard = NSPasteboard.general
|
|
if let string = pasteboard.string(forType: .string) {
|
|
self.machine.paste(string)
|
|
}
|
|
}
|
|
|
|
// MARK: CSBestEffortUpdaterDelegate
|
|
final func bestEffortUpdater(_ bestEffortUpdater: CSBestEffortUpdater!, runForInterval duration: TimeInterval, didSkipPreviousUpdate: Bool) {
|
|
if actionLock.try() {
|
|
self.machine.run(forInterval: duration)
|
|
actionLock.unlock()
|
|
}
|
|
}
|
|
|
|
// MARK: CSAudioQueueDelegate
|
|
final func audioQueueIsRunningDry(_ audioQueue: CSAudioQueue) {
|
|
bestEffortLock.lock()
|
|
bestEffortUpdater?.update()
|
|
bestEffortLock.unlock()
|
|
}
|
|
|
|
// MARK: CSOpenGLViewDelegate
|
|
final func openGLView(_ view: CSOpenGLView, drawViewOnlyIfDirty onlyIfDirty: Bool) {
|
|
bestEffortLock.lock()
|
|
if let bestEffortUpdater = bestEffortUpdater {
|
|
bestEffortLock.unlock()
|
|
bestEffortUpdater.update()
|
|
if drawLock.try() {
|
|
self.machine.drawView(forPixelSize: view.backingSize, onlyIfDirty: onlyIfDirty)
|
|
drawLock.unlock()
|
|
}
|
|
} else {
|
|
bestEffortLock.unlock()
|
|
}
|
|
}
|
|
|
|
final func openGLView(_ view: CSOpenGLView, didReceiveFileAt URL: URL) {
|
|
let mediaSet = CSMediaSet(fileAt: URL)
|
|
if let mediaSet = mediaSet {
|
|
mediaSet.apply(to: self.machine)
|
|
}
|
|
}
|
|
|
|
// MARK: NSDocument overrides
|
|
override func data(ofType typeName: String) throws -> Data {
|
|
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
|
|
}
|
|
|
|
// MARK: Input management
|
|
func windowDidResignKey(_ notification: Notification) {
|
|
// self.machine.clearAllKeys()
|
|
}
|
|
|
|
func keyDown(_ event: NSEvent) {
|
|
self.machine.setKey(event.keyCode, characters: event.characters, isPressed: true)
|
|
}
|
|
|
|
func keyUp(_ event: NSEvent) {
|
|
self.machine.setKey(event.keyCode, characters: event.characters, isPressed: false)
|
|
}
|
|
|
|
func flagsChanged(_ newModifiers: NSEvent) {
|
|
self.machine.setKey(VK_Shift, characters: nil, isPressed: newModifiers.modifierFlags.contains(.shift))
|
|
self.machine.setKey(VK_Control, characters: nil, isPressed: newModifiers.modifierFlags.contains(.control))
|
|
self.machine.setKey(VK_Command, characters: nil, isPressed: newModifiers.modifierFlags.contains(.command))
|
|
self.machine.setKey(VK_Option, characters: nil, isPressed: newModifiers.modifierFlags.contains(.option))
|
|
}
|
|
}
|