1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-02-18 01:30:56 +00:00

Merge pull request #774 from TomHarte/VolumeControl

Adds output volume control.
This commit is contained in:
Thomas Harte 2020-03-22 21:23:49 -04:00 committed by GitHub
commit d3bac57d6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 236 additions and 28 deletions

View File

@ -54,6 +54,12 @@ bool MultiSpeaker::get_is_stereo() {
return false; return false;
} }
void MultiSpeaker::set_output_volume(float volume) {
for(const auto &speaker: speakers_) {
speaker->set_output_volume(volume);
}
}
void MultiSpeaker::set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) { void MultiSpeaker::set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) {
delegate_ = delegate; delegate_ = delegate;
} }

View File

@ -42,6 +42,7 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker:
void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) override; void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) override;
void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) override; void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) override;
bool get_is_stereo() override; bool get_is_stereo() override;
void set_output_volume(float) override;
private: private:
void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) final; void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) final;

View File

@ -204,6 +204,7 @@
4B4B1A3D200198CA00A0F866 /* KonamiSCC.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4B1A3A200198C900A0F866 /* KonamiSCC.cpp */; }; 4B4B1A3D200198CA00A0F866 /* KonamiSCC.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4B1A3A200198C900A0F866 /* KonamiSCC.cpp */; };
4B4DC8211D2C2425003C5BF8 /* Vic20.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4DC81F1D2C2425003C5BF8 /* Vic20.cpp */; }; 4B4DC8211D2C2425003C5BF8 /* Vic20.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4DC81F1D2C2425003C5BF8 /* Vic20.cpp */; };
4B4DC82B1D2C27A4003C5BF8 /* SerialBus.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4DC8291D2C27A4003C5BF8 /* SerialBus.cpp */; }; 4B4DC82B1D2C27A4003C5BF8 /* SerialBus.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4DC8291D2C27A4003C5BF8 /* SerialBus.cpp */; };
4B50AF80242817F40099BBD7 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B50AF7F242817F40099BBD7 /* QuartzCore.framework */; };
4B54C0BC1F8D8E790050900F /* KeyboardMachine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0BB1F8D8E790050900F /* KeyboardMachine.cpp */; }; 4B54C0BC1F8D8E790050900F /* KeyboardMachine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0BB1F8D8E790050900F /* KeyboardMachine.cpp */; };
4B54C0BF1F8D8F450050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0BD1F8D8F450050900F /* Keyboard.cpp */; }; 4B54C0BF1F8D8F450050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0BD1F8D8F450050900F /* Keyboard.cpp */; };
4B54C0C21F8D91CD0050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0C11F8D91CD0050900F /* Keyboard.cpp */; }; 4B54C0C21F8D91CD0050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0C11F8D91CD0050900F /* Keyboard.cpp */; };
@ -1104,6 +1105,7 @@
4B4DC8271D2C2470003C5BF8 /* C1540.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = C1540.hpp; sourceTree = "<group>"; }; 4B4DC8271D2C2470003C5BF8 /* C1540.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = C1540.hpp; sourceTree = "<group>"; };
4B4DC8291D2C27A4003C5BF8 /* SerialBus.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SerialBus.cpp; sourceTree = "<group>"; }; 4B4DC8291D2C27A4003C5BF8 /* SerialBus.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SerialBus.cpp; sourceTree = "<group>"; };
4B4DC82A1D2C27A4003C5BF8 /* SerialBus.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SerialBus.hpp; sourceTree = "<group>"; }; 4B4DC82A1D2C27A4003C5BF8 /* SerialBus.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SerialBus.hpp; sourceTree = "<group>"; };
4B50AF7F242817F40099BBD7 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
4B51F70920A521D700AFA2C1 /* Source.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Source.hpp; sourceTree = "<group>"; }; 4B51F70920A521D700AFA2C1 /* Source.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Source.hpp; sourceTree = "<group>"; };
4B51F70A20A521D700AFA2C1 /* Observer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Observer.hpp; sourceTree = "<group>"; }; 4B51F70A20A521D700AFA2C1 /* Observer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Observer.hpp; sourceTree = "<group>"; };
4B54C0BB1F8D8E790050900F /* KeyboardMachine.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = KeyboardMachine.cpp; sourceTree = "<group>"; }; 4B54C0BB1F8D8E790050900F /* KeyboardMachine.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = KeyboardMachine.cpp; sourceTree = "<group>"; };
@ -1793,6 +1795,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
4B50AF80242817F40099BBD7 /* QuartzCore.framework in Frameworks */,
4BC76E6B1C98F43700E6EF73 /* Accelerate.framework in Frameworks */, 4BC76E6B1C98F43700E6EF73 /* Accelerate.framework in Frameworks */,
4B69FB461C4D950F00B5F0AA /* libz.tbd in Frameworks */, 4B69FB461C4D950F00B5F0AA /* libz.tbd in Frameworks */,
); );
@ -1820,6 +1823,7 @@
4B055A761FAE78210060FFFF /* Frameworks */ = { 4B055A761FAE78210060FFFF /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4B50AF7F242817F40099BBD7 /* QuartzCore.framework */,
4B055AF01FAE9C080060FFFF /* OpenGL.framework */, 4B055AF01FAE9C080060FFFF /* OpenGL.framework */,
4B055A771FAE78210060FFFF /* SDL2.framework */, 4B055A771FAE78210060FFFF /* SDL2.framework */,
); );
@ -5137,7 +5141,7 @@
GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_LABEL = YES;
INFOPLIST_FILE = "Clock Signal/Info.plist"; INFOPLIST_FILE = "Clock Signal/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.10; MACOSX_DEPLOYMENT_TARGET = 10.12.2;
OTHER_CPLUSPLUSFLAGS = ( OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)", "$(OTHER_CFLAGS)",
"-Wreorder", "-Wreorder",
@ -5185,7 +5189,7 @@
GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_LABEL = YES;
INFOPLIST_FILE = "Clock Signal/Info.plist"; INFOPLIST_FILE = "Clock Signal/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.10; MACOSX_DEPLOYMENT_TARGET = 10.12.2;
OTHER_CPLUSPLUSFLAGS = ( OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)", "$(OTHER_CFLAGS)",
"-Wreorder", "-Wreorder",

View File

@ -56,6 +56,10 @@
argument = "/Users/thomasharte/Downloads/test-dsk-for-rw-and-50-60-hz/TEST-RW-60Hz.DSK" argument = "/Users/thomasharte/Downloads/test-dsk-for-rw-and-50-60-hz/TEST-RW-60Hz.DSK"
isEnabled = "NO"> isEnabled = "NO">
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument
argument = "--volume=0.001"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument <CommandLineArgument
argument = "--new=amstradcpc" argument = "--new=amstradcpc"
isEnabled = "NO"> isEnabled = "NO">
@ -70,7 +74,7 @@
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument <CommandLineArgument
argument = "--output=CompositeMonochrome" argument = "--output=CompositeMonochrome"
isEnabled = "YES"> isEnabled = "NO">
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument <CommandLineArgument
argument = "--logical-keyboard" argument = "--logical-keyboard"
@ -94,11 +98,11 @@
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument <CommandLineArgument
argument = "--help" argument = "--help"
isEnabled = "YES"> isEnabled = "NO">
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument <CommandLineArgument
argument = "--model=cpc6128" argument = "--model=cpc6128"
isEnabled = "YES"> isEnabled = "NO">
</CommandLineArgument> </CommandLineArgument>
</CommandLineArguments> </CommandLineArguments>
</LaunchAction> </LaunchAction>

View File

@ -1,14 +1,15 @@
<?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="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="15705" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies> <dependencies>
<deployment identifier="macosx"/> <deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15705"/>
<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>
<customObject id="-2" userLabel="File's Owner" customClass="MachineDocument" customModule="Clock_Signal" customModuleProvider="target"> <customObject id="-2" userLabel="File's Owner" customClass="MachineDocument" customModule="Clock_Signal" customModuleProvider="target">
<connections> <connections>
<outlet property="openGLView" destination="DEG-fq-cjd" id="Gxs-2u-n7B"/> <outlet property="openGLView" destination="DEG-fq-cjd" id="Gxs-2u-n7B"/>
<outlet property="volumeView" destination="4ap-Gi-2AO" id="v4e-k6-Fqf"/>
<outlet property="window" destination="xOd-HO-29H" id="JIz-fz-R2o"/> <outlet property="window" destination="xOd-HO-29H" id="JIz-fz-R2o"/>
</connections> </connections>
</customObject> </customObject>
@ -19,7 +20,7 @@
<windowCollectionBehavior key="collectionBehavior" fullScreenPrimary="YES"/> <windowCollectionBehavior key="collectionBehavior" fullScreenPrimary="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="80" y="250" width="600" height="450"/> <rect key="contentRect" x="80" y="250" width="600" height="450"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/> <rect key="screenRect" x="0.0" y="0.0" width="3840" height="2137"/>
<value key="minSize" type="size" width="228" height="171"/> <value key="minSize" type="size" width="228" height="171"/>
<view key="contentView" id="gIp-Ho-8D9"> <view key="contentView" id="gIp-Ho-8D9">
<rect key="frame" x="0.0" y="0.0" width="600" height="450"/> <rect key="frame" x="0.0" y="0.0" width="600" height="450"/>
@ -28,9 +29,57 @@
<openGLView hidden="YES" wantsLayer="YES" useAuxiliaryDepthBufferStencil="NO" allowOffline="YES" wantsBestResolutionOpenGLSurface="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DEG-fq-cjd" customClass="CSOpenGLView"> <openGLView hidden="YES" wantsLayer="YES" useAuxiliaryDepthBufferStencil="NO" allowOffline="YES" wantsBestResolutionOpenGLSurface="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DEG-fq-cjd" customClass="CSOpenGLView">
<rect key="frame" x="0.0" y="0.0" width="600" height="450"/> <rect key="frame" x="0.0" y="0.0" width="600" height="450"/>
</openGLView> </openGLView>
<box hidden="YES" boxType="custom" cornerRadius="4" title="Box" titlePosition="noTitle" translatesAutoresizingMaskIntoConstraints="NO" id="4ap-Gi-2AO">
<rect key="frame" x="150" y="20" width="300" height="48"/>
<view key="contentView" id="gwO-Ty-LCX">
<rect key="frame" x="1" y="1" width="298" height="46"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Hzw-P6-1dH">
<rect key="frame" x="258" y="8" width="32" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="32" id="WrK-W9-mPP"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSTouchBarAudioOutputVolumeHighTemplate" id="5dB-4Y-iEl"/>
<color key="contentTintColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</imageView>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="p0w-Ti-Tu9">
<rect key="frame" x="8" y="8" width="32" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="32" id="dZn-Fc-fRU"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSTouchBarAudioOutputVolumeOffTemplate" id="mUH-aA-T9N"/>
<color key="contentTintColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</imageView>
<slider verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zaz-lB-Iyt">
<rect key="frame" x="46" y="14" width="206" height="19"/>
<sliderCell key="cell" continuous="YES" state="on" alignment="left" maxValue="1" doubleValue="1" tickMarkPosition="above" sliderType="linear" id="Zvz-Og-dGA"/>
<connections>
<action selector="setVolume:" target="-2" id="eEc-5v-I2c"/>
</connections>
</slider>
</subviews>
<constraints>
<constraint firstItem="p0w-Ti-Tu9" firstAttribute="leading" secondItem="gwO-Ty-LCX" secondAttribute="leading" constant="8" id="1t0-CW-zp9"/>
<constraint firstItem="Hzw-P6-1dH" firstAttribute="top" secondItem="gwO-Ty-LCX" secondAttribute="top" constant="8" id="OHP-c3-2Aa"/>
<constraint firstAttribute="trailing" secondItem="Hzw-P6-1dH" secondAttribute="trailing" constant="8" id="W24-Wx-bdQ"/>
<constraint firstItem="zaz-lB-Iyt" firstAttribute="centerY" secondItem="p0w-Ti-Tu9" secondAttribute="centerY" id="f3C-My-T0S"/>
<constraint firstItem="zaz-lB-Iyt" firstAttribute="leading" secondItem="p0w-Ti-Tu9" secondAttribute="trailing" constant="8" id="n0g-y6-wHP"/>
<constraint firstItem="p0w-Ti-Tu9" firstAttribute="centerY" secondItem="gwO-Ty-LCX" secondAttribute="centerY" id="uTt-VY-o8c"/>
<constraint firstItem="Hzw-P6-1dH" firstAttribute="leading" secondItem="zaz-lB-Iyt" secondAttribute="trailing" constant="8" id="xCS-qF-Gz8"/>
<constraint firstAttribute="bottom" secondItem="Hzw-P6-1dH" secondAttribute="bottom" constant="8" id="yaF-k8-W5B"/>
</constraints>
</view>
<constraints>
<constraint firstAttribute="width" constant="300" id="HKL-vy-Mov"/>
</constraints>
<color key="fillColor" red="0.0" green="0.0" blue="0.0" alpha="0.5" colorSpace="custom" customColorSpace="sRGB"/>
</box>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstItem="DEG-fq-cjd" firstAttribute="centerX" secondItem="gIp-Ho-8D9" secondAttribute="centerX" id="ES5-nL-N3h"/> <constraint firstItem="DEG-fq-cjd" firstAttribute="centerX" secondItem="gIp-Ho-8D9" secondAttribute="centerX" id="ES5-nL-N3h"/>
<constraint firstItem="4ap-Gi-2AO" firstAttribute="centerX" secondItem="DEG-fq-cjd" secondAttribute="centerX" id="T41-z9-BsM"/>
<constraint firstItem="4ap-Gi-2AO" firstAttribute="bottom" secondItem="DEG-fq-cjd" secondAttribute="bottom" constant="-20" id="Tly-Uu-96H"/>
<constraint firstItem="DEG-fq-cjd" firstAttribute="height" secondItem="gIp-Ho-8D9" secondAttribute="height" id="YoB-qI-LFX"/> <constraint firstItem="DEG-fq-cjd" firstAttribute="height" secondItem="gIp-Ho-8D9" secondAttribute="height" id="YoB-qI-LFX"/>
<constraint firstItem="DEG-fq-cjd" firstAttribute="centerY" secondItem="gIp-Ho-8D9" secondAttribute="centerY" id="d5Y-3a-CEI"/> <constraint firstItem="DEG-fq-cjd" firstAttribute="centerY" secondItem="gIp-Ho-8D9" secondAttribute="centerY" id="d5Y-3a-CEI"/>
<constraint firstItem="DEG-fq-cjd" firstAttribute="width" secondItem="gIp-Ho-8D9" secondAttribute="width" id="mYS-bH-DST"/> <constraint firstItem="DEG-fq-cjd" firstAttribute="width" secondItem="gIp-Ho-8D9" secondAttribute="width" id="mYS-bH-DST"/>
@ -45,6 +94,11 @@
<outlet property="delegate" destination="-2" id="0bl-1N-x8E"/> <outlet property="delegate" destination="-2" id="0bl-1N-x8E"/>
<outlet property="initialFirstResponder" destination="DEG-fq-cjd" id="9RI-Kx-QeN"/> <outlet property="initialFirstResponder" destination="DEG-fq-cjd" id="9RI-Kx-QeN"/>
</connections> </connections>
<point key="canvasLocation" x="141" y="147"/>
</window> </window>
</objects> </objects>
<resources>
<image name="NSTouchBarAudioOutputVolumeHighTemplate" width="23" height="30"/>
<image name="NSTouchBarAudioOutputVolumeOffTemplate" width="23" height="30"/>
</resources>
</document> </document>

View File

@ -8,6 +8,7 @@
import AudioToolbox import AudioToolbox
import Cocoa import Cocoa
import QuartzCore
class MachineDocument: class MachineDocument:
NSDocument, NSDocument,
@ -62,6 +63,9 @@ class MachineDocument:
activityPanel.setIsVisible(true) activityPanel.setIsVisible(true)
} }
/// The volume view.
@IBOutlet var volumeView: NSView!
// MARK: - NSDocument Overrides and NSWindowDelegate methods. // MARK: - NSDocument Overrides and NSWindowDelegate methods.
/// Links this class to the MachineDocument NIB. /// Links this class to the MachineDocument NIB.
@ -712,4 +716,55 @@ class MachineDocument:
} }
} }
} }
// MARK: - Volume Control.
@IBAction func setVolume(_ sender: NSSlider!) {
if let machine = self.machine {
machine.setVolume(sender.floatValue);
}
}
// This class is pure nonsense to work around Xcode's opaque behaviour.
// If I make the main class a sub of CAAnimationDelegate then the compiler
// generates a bridging header that doesn't include QuartzCore and therefore
// can't find a declaration of the CAAnimationDelegate protocol. Doesn't
// seem to matter what I add explicitly to the link stage, which version of
// macOS I set as the target, etc.
//
// So, the workaround: make my CAAnimationDelegate something that doesn't
// appear in the bridging header.
fileprivate class ViewFader: NSObject, CAAnimationDelegate {
var volumeView: NSView
init(view: NSView) {
volumeView = view
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
volumeView.isHidden = true
}
}
fileprivate var animationFader: ViewFader? = nil
func openGLViewDidShowOSMouseCursor(_ view: CSOpenGLView) {
// The OS mouse cursor became visible, so show the volume controls.
animationFader = nil
volumeView.layer?.removeAllAnimations()
volumeView.isHidden = false
volumeView.layer?.opacity = 1.0
}
func openGLViewWillHideOSMouseCursor(_ view: CSOpenGLView) {
// The OS mouse cursor will be hidden, so hide the volume controls.
if !volumeView.isHidden && volumeView.layer?.animation(forKey: "opacity") == nil {
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.fromValue = 1.0
fadeAnimation.toValue = 0.0
fadeAnimation.duration = 0.2
animationFader = ViewFader(view: volumeView)
fadeAnimation.delegate = animationFader
volumeView.layer?.add(fadeAnimation, forKey: "opacity")
volumeView.layer?.opacity = 0.0
}
}
} }

View File

@ -92,7 +92,11 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
@property (nonatomic, readonly) BOOL canInsertMedia; @property (nonatomic, readonly) BOOL canInsertMedia;
- (bool)supportsVideoSignal:(CSMachineVideoSignal)videoSignal; - (BOOL)supportsVideoSignal:(CSMachineVideoSignal)videoSignal;
// Volume contorl.
- (void)setVolume:(float)volume;
@property (nonatomic, readonly) BOOL hasAudioOutput;
// Input control. // Input control.
@property (nonatomic, readonly) BOOL hasExclusiveKeyboard; @property (nonatomic, readonly) BOOL hasExclusiveKeyboard;

View File

@ -613,7 +613,7 @@ struct ActivityObserver: public Activity::Observer {
} }
} }
- (bool)supportsVideoSignal:(CSMachineVideoSignal)videoSignal { - (BOOL)supportsVideoSignal:(CSMachineVideoSignal)videoSignal {
Configurable::Device *configurable_device = _machine->configurable_device(); Configurable::Device *configurable_device = _machine->configurable_device();
if(!configurable_device) return NO; if(!configurable_device) return NO;
@ -708,6 +708,24 @@ struct ActivityObserver: public Activity::Observer {
essential_modifiers.find(Inputs::Keyboard::Key::RightMeta) != essential_modifiers.end(); essential_modifiers.find(Inputs::Keyboard::Key::RightMeta) != essential_modifiers.end();
} }
#pragma mark - Volume control
- (void)setVolume:(float)volume {
@synchronized(self) {
Outputs::Speaker::Speaker *speaker = _machine->crt_machine()->get_speaker();
if(speaker) {
return speaker->set_output_volume(volume);
}
}
}
- (BOOL)hasAudioOutput {
@synchronized(self) {
Outputs::Speaker::Speaker *speaker = _machine->crt_machine()->get_speaker();
return speaker ? YES : NO;
}
}
#pragma mark - Activity observation #pragma mark - Activity observation
- (void)addLED:(NSString *)led { - (void)addLED:(NSString *)led {
@ -761,9 +779,10 @@ struct ActivityObserver: public Activity::Observer {
_timer = [[CSHighPrecisionTimer alloc] initWithTask:^{ _timer = [[CSHighPrecisionTimer alloc] initWithTask:^{
// Grab the time now and, therefore, the amount of time since the timer last fired // Grab the time now and, therefore, the amount of time since the timer last fired
// (capped at half a second). // (subject to a cap to avoid potential perpetual regression).
const auto timeNow = Time::nanos_now(); const auto timeNow = Time::nanos_now();
const auto duration = std::min(timeNow - lastTime, Time::Nanos(10'000'000'000 / TICKS)); lastTime = std::max(timeNow - Time::Nanos(10'000'000'000 / TICKS), lastTime);
const auto duration = timeNow - lastTime;
CGSize pixelSize; CGSize pixelSize;
BOOL splitAndSync = NO; BOOL splitAndSync = NO;

View File

@ -50,6 +50,18 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
*/ */
- (void)openGLViewDidReleaseMouse:(nonnull CSOpenGLView *)view; - (void)openGLViewDidReleaseMouse:(nonnull CSOpenGLView *)view;
/*!
Announces that the OS mouse cursor is now being displayed again, after having been invisible.
@param view The view making the announcement.
*/
- (void)openGLViewDidShowOSMouseCursor:(nonnull CSOpenGLView *)view;
/*!
Announces that the OS mouse cursor will now be hidden.
@param view The view making the announcement.
*/
- (void)openGLViewWillHideOSMouseCursor:(nonnull CSOpenGLView *)view;
@end @end
@protocol CSOpenGLViewResponderDelegate <NSObject> @protocol CSOpenGLViewResponderDelegate <NSObject>

View File

@ -11,6 +11,8 @@
@import CoreVideo; @import CoreVideo;
@import GLKit; @import GLKit;
#include <stdatomic.h>
@interface CSOpenGLView () <NSDraggingDestination, CSApplicationEventDelegate> @interface CSOpenGLView () <NSDraggingDestination, CSApplicationEventDelegate>
@end @end
@ -23,13 +25,16 @@
NSTimer *_mouseHideTimer; NSTimer *_mouseHideTimer;
BOOL _mouseIsCaptured; BOOL _mouseIsCaptured;
volatile int32_t _isDrawingFlag; atomic_int _isDrawingFlag;
BOOL _isInvalid; BOOL _isInvalid;
} }
- (void)prepareOpenGL { - (void)prepareOpenGL {
[super prepareOpenGL]; [super prepareOpenGL];
// Prepare the atomic int.
atomic_init(&_isDrawingFlag, 0);
// Set the clear colour. // Set the clear colour.
[self.openGLContext makeCurrentContext]; [self.openGLContext makeCurrentContext];
glClearColor(0.0, 0.0, 0.0, 1.0); glClearColor(0.0, 0.0, 0.0, 1.0);
@ -71,7 +76,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
}); });
// Ensure _isDrawingFlag has value 1 when drawing, 0 otherwise. // Ensure _isDrawingFlag has value 1 when drawing, 0 otherwise.
OSAtomicIncrement32(&view->_isDrawingFlag); atomic_store(&view->_isDrawingFlag, 1);
[view.displayLinkDelegate openGLViewDisplayLinkDidFire:view now:now outputTime:outputTime]; [view.displayLinkDelegate openGLViewDisplayLinkDidFire:view now:now outputTime:outputTime];
/* /*
@ -84,7 +89,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
access the display link itself as part of -drawAtTime:frequency:. access the display link itself as part of -drawAtTime:frequency:.
*/ */
OSAtomicDecrement32(&view->_isDrawingFlag); atomic_store(&view->_isDrawingFlag, 0);
return kCVReturnSuccess; return kCVReturnSuccess;
} }
@ -143,7 +148,10 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
usleep((useconds_t)ceil(duration * 1000000.0)); usleep((useconds_t)ceil(duration * 1000000.0));
// Spin until _isDrawingFlag is 0 (and leave it as 0). // Spin until _isDrawingFlag is 0 (and leave it as 0).
while(!OSAtomicCompareAndSwap32(0, 0, &_isDrawingFlag)); int expected_value = 0;
while(!atomic_compare_exchange_weak(&_isDrawingFlag, &expected_value, 0)) {
expected_value = 0;
}
} }
- (void)dealloc { - (void)dealloc {
@ -292,11 +300,13 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
_mouseHideTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) { _mouseHideTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
[NSCursor setHiddenUntilMouseMoves:YES]; [NSCursor setHiddenUntilMouseMoves:YES];
[self.delegate openGLViewWillHideOSMouseCursor:self];
}]; }];
} }
} }
- (void)mouseEntered:(NSEvent *)event { - (void)mouseEntered:(NSEvent *)event {
[self.delegate openGLViewDidShowOSMouseCursor:self];
[super mouseEntered:event]; [super mouseEntered:event];
[self scheduleMouseHide]; [self scheduleMouseHide];
} }
@ -305,6 +315,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
[super mouseExited:event]; [super mouseExited:event];
[_mouseHideTimer invalidate]; [_mouseHideTimer invalidate];
_mouseHideTimer = nil; _mouseHideTimer = nil;
[self.delegate openGLViewWillHideOSMouseCursor:self];
} }
- (void)releaseMouse { - (void)releaseMouse {
@ -313,6 +324,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
CGAssociateMouseAndMouseCursorPosition(true); CGAssociateMouseAndMouseCursorPosition(true);
[NSCursor unhide]; [NSCursor unhide];
[self.delegate openGLViewDidReleaseMouse:self]; [self.delegate openGLViewDidReleaseMouse:self];
[self.delegate openGLViewDidShowOSMouseCursor:self];
((CSApplication *)[NSApplication sharedApplication]).eventDelegate = nil; ((CSApplication *)[NSApplication sharedApplication]).eventDelegate = nil;
} }
} }
@ -324,6 +336,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
// Mouse capture is off, so don't play games with the cursor, just schedule it to // Mouse capture is off, so don't play games with the cursor, just schedule it to
// hide in the near future. // hide in the near future.
[self scheduleMouseHide]; [self scheduleMouseHide];
[self.delegate openGLViewDidShowOSMouseCursor:self];
} else { } else {
if(_mouseIsCaptured) { if(_mouseIsCaptured) {
// Mouse capture is on, so move the cursor back to the middle of the window, and // Mouse capture is on, so move the cursor back to the middle of the window, and
@ -340,6 +353,8 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
)); ));
[self.responderDelegate mouseMoved:event]; [self.responderDelegate mouseMoved:event];
} else {
[self.delegate openGLViewDidShowOSMouseCursor:self];
} }
} }
} }
@ -372,6 +387,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
_mouseIsCaptured = YES; _mouseIsCaptured = YES;
[NSCursor hide]; [NSCursor hide];
CGAssociateMouseAndMouseCursorPosition(false); CGAssociateMouseAndMouseCursorPosition(false);
[self.delegate openGLViewWillHideOSMouseCursor:self];
[self.delegate openGLViewDidCaptureMouse:self]; [self.delegate openGLViewDidCaptureMouse:self];
if(self.shouldUsurpCommand) { if(self.shouldUsurpCommand) {
((CSApplication *)[NSApplication sharedApplication]).eventDelegate = self; ((CSApplication *)[NSApplication sharedApplication]).eventDelegate = self;

View File

@ -487,7 +487,7 @@ int main(int argc, char *argv[]) {
const ParsedArguments arguments = parse_arguments(argc, argv); const ParsedArguments arguments = parse_arguments(argc, argv);
// This may be printed either as // This may be printed either as
const std::string usage_suffix = " [file or --new={machine}] [OPTIONS] [--rompath={path to ROMs}] [--speed={speed multiplier, e.g. 1.5}] [--logical-keyboard]"; const std::string usage_suffix = " [file or --new={machine}] [OPTIONS] [--rompath={path to ROMs}] [--speed={speed multiplier, e.g. 1.5}] [--logical-keyboard] [--volume={0.0 to 1.0}]";
// Print a help message if requested. // Print a help message if requested.
if(arguments.selections.find("help") != arguments.selections.end() || arguments.selections.find("h") != arguments.selections.end()) { if(arguments.selections.find("help") != arguments.selections.end() || arguments.selections.find("h") != arguments.selections.end()) {
@ -739,18 +739,39 @@ int main(int argc, char *argv[]) {
} }
// Apply the speed multiplier, if one was requested. // Apply the speed multiplier, if one was requested.
const auto speed_argument = arguments.selections.find("speed"); {
if(speed_argument != arguments.selections.end()) { const auto speed_argument = arguments.selections.find("speed");
const char *speed_string = speed_argument->second.c_str(); if(speed_argument != arguments.selections.end()) {
char *end; const char *speed_string = speed_argument->second.c_str();
double speed = strtod(speed_string, &end); char *end;
const double speed = strtod(speed_string, &end);
if(size_t(end - speed_string) != strlen(speed_string)) { if(size_t(end - speed_string) != strlen(speed_string)) {
std::cerr << "Unable to parse speed: " << speed_string << std::endl; std::cerr << "Unable to parse speed: " << speed_string << std::endl;
} else if(speed <= 0.0) { } else if(speed <= 0.0) {
std::cerr << "Cannot run at speed " << speed_string << "; speeds must be positive." << std::endl; std::cerr << "Cannot run at speed " << speed_string << "; speeds must be positive." << std::endl;
} else { } else {
machine_runner.set_speed_multiplier(speed); machine_runner.set_speed_multiplier(speed);
}
}
}
// Apply the desired output volume, if requested.
{
const auto volume_argument = arguments.selections.find("volume");
if(volume_argument != arguments.selections.end()) {
const char *volume_string = volume_argument->second.c_str();
char *end;
const double volume = strtod(volume_string, &end);
if(size_t(end - volume_string) != strlen(volume_string)) {
std::cerr << "Unable to parse volume: " << volume_string << std::endl;
} else if(volume < 0.0 || volume > 1.0) {
std::cerr << "Cannot run with volume " << volume_string << "; volumes must be between 0.0 and 1.0." << std::endl;
} else {
const auto speaker = machine->crt_machine()->get_speaker();
if(speaker) speaker->set_output_volume(volume);
}
} }
} }

View File

@ -31,9 +31,16 @@ namespace Speaker {
template <typename SampleSource> class LowpassSpeaker: public Speaker { template <typename SampleSource> class LowpassSpeaker: public Speaker {
public: public:
LowpassSpeaker(SampleSource &sample_source) : sample_source_(sample_source) { LowpassSpeaker(SampleSource &sample_source) : sample_source_(sample_source) {
// Propagate an initial volume level.
sample_source.set_sample_volume_range(32767); sample_source.set_sample_volume_range(32767);
} }
void set_output_volume(float volume) final {
// Clamp to the acceptable range, and set.
volume = std::min(std::max(0.0f, volume), 1.0f);
sample_source_.set_sample_volume_range(int16_t(32767.0f * volume));
}
// Implemented as per Speaker. // Implemented as per Speaker.
float get_ideal_clock_rate_in_range(float minimum, float maximum) final { float get_ideal_clock_rate_in_range(float minimum, float maximum) final {
std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_); std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_);

View File

@ -45,6 +45,9 @@ class Speaker {
compute_output_rate(); compute_output_rate();
} }
/// Sets the output volume, in the range [0, 1].
virtual void set_output_volume(float) = 0;
/*! /*!
Speeds a speed multiplier for this machine, e.g. that it is currently being run at 2.0x its normal rate. Speeds a speed multiplier for this machine, e.g. that it is currently being run at 2.0x its normal rate.
This will affect the number of input samples that are combined to produce one output sample. This will affect the number of input samples that are combined to produce one output sample.
@ -79,6 +82,8 @@ class Speaker {
delegate_ = delegate; delegate_ = delegate;
} }
// This is primarily exposed for MultiSpeaker et al; it's not for general callers.
virtual void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) = 0; virtual void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) = 0;
protected: protected: