1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-12-27 01:31:42 +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;
}
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) {
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_delegate(Outputs::Speaker::Speaker::Delegate *delegate) override;
bool get_is_stereo() override;
void set_output_volume(float) override;
private:
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 */; };
4B4DC8211D2C2425003C5BF8 /* Vic20.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4DC81F1D2C2425003C5BF8 /* Vic20.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 */; };
4B54C0BF1F8D8F450050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0BD1F8D8F450050900F /* 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>"; };
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>"; };
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>"; };
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>"; };
@ -1793,6 +1795,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4B50AF80242817F40099BBD7 /* QuartzCore.framework in Frameworks */,
4BC76E6B1C98F43700E6EF73 /* Accelerate.framework in Frameworks */,
4B69FB461C4D950F00B5F0AA /* libz.tbd in Frameworks */,
);
@ -1820,6 +1823,7 @@
4B055A761FAE78210060FFFF /* Frameworks */ = {
isa = PBXGroup;
children = (
4B50AF7F242817F40099BBD7 /* QuartzCore.framework */,
4B055AF01FAE9C080060FFFF /* OpenGL.framework */,
4B055A771FAE78210060FFFF /* SDL2.framework */,
);
@ -5137,7 +5141,7 @@
GCC_WARN_UNUSED_LABEL = YES;
INFOPLIST_FILE = "Clock Signal/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.10;
MACOSX_DEPLOYMENT_TARGET = 10.12.2;
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-Wreorder",
@ -5185,7 +5189,7 @@
GCC_WARN_UNUSED_LABEL = YES;
INFOPLIST_FILE = "Clock Signal/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.10;
MACOSX_DEPLOYMENT_TARGET = 10.12.2;
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-Wreorder",

View File

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

View File

@ -1,14 +1,15 @@
<?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>
<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"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="MachineDocument" customModule="Clock_Signal" customModuleProvider="target">
<connections>
<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"/>
</connections>
</customObject>
@ -19,7 +20,7 @@
<windowCollectionBehavior key="collectionBehavior" fullScreenPrimary="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="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"/>
<view key="contentView" id="gIp-Ho-8D9">
<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">
<rect key="frame" x="0.0" y="0.0" width="600" height="450"/>
</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>
<constraints>
<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="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"/>
@ -45,6 +94,11 @@
<outlet property="delegate" destination="-2" id="0bl-1N-x8E"/>
<outlet property="initialFirstResponder" destination="DEG-fq-cjd" id="9RI-Kx-QeN"/>
</connections>
<point key="canvasLocation" x="141" y="147"/>
</window>
</objects>
<resources>
<image name="NSTouchBarAudioOutputVolumeHighTemplate" width="23" height="30"/>
<image name="NSTouchBarAudioOutputVolumeOffTemplate" width="23" height="30"/>
</resources>
</document>

View File

@ -8,6 +8,7 @@
import AudioToolbox
import Cocoa
import QuartzCore
class MachineDocument:
NSDocument,
@ -62,6 +63,9 @@ class MachineDocument:
activityPanel.setIsVisible(true)
}
/// The volume view.
@IBOutlet var volumeView: NSView!
// MARK: - NSDocument Overrides and NSWindowDelegate methods.
/// 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;
- (bool)supportsVideoSignal:(CSMachineVideoSignal)videoSignal;
- (BOOL)supportsVideoSignal:(CSMachineVideoSignal)videoSignal;
// Volume contorl.
- (void)setVolume:(float)volume;
@property (nonatomic, readonly) BOOL hasAudioOutput;
// Input control.
@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();
if(!configurable_device) return NO;
@ -708,6 +708,24 @@ struct ActivityObserver: public Activity::Observer {
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
- (void)addLED:(NSString *)led {
@ -761,9 +779,10 @@ struct ActivityObserver: public Activity::Observer {
_timer = [[CSHighPrecisionTimer alloc] initWithTask:^{
// 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 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;
BOOL splitAndSync = NO;

View File

@ -50,6 +50,18 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
*/
- (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
@protocol CSOpenGLViewResponderDelegate <NSObject>

View File

@ -11,6 +11,8 @@
@import CoreVideo;
@import GLKit;
#include <stdatomic.h>
@interface CSOpenGLView () <NSDraggingDestination, CSApplicationEventDelegate>
@end
@ -23,13 +25,16 @@
NSTimer *_mouseHideTimer;
BOOL _mouseIsCaptured;
volatile int32_t _isDrawingFlag;
atomic_int _isDrawingFlag;
BOOL _isInvalid;
}
- (void)prepareOpenGL {
[super prepareOpenGL];
// Prepare the atomic int.
atomic_init(&_isDrawingFlag, 0);
// Set the clear colour.
[self.openGLContext makeCurrentContext];
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.
OSAtomicIncrement32(&view->_isDrawingFlag);
atomic_store(&view->_isDrawingFlag, 1);
[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:.
*/
OSAtomicDecrement32(&view->_isDrawingFlag);
atomic_store(&view->_isDrawingFlag, 0);
return kCVReturnSuccess;
}
@ -143,7 +148,10 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
usleep((useconds_t)ceil(duration * 1000000.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 {
@ -292,11 +300,13 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
_mouseHideTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
[NSCursor setHiddenUntilMouseMoves:YES];
[self.delegate openGLViewWillHideOSMouseCursor:self];
}];
}
}
- (void)mouseEntered:(NSEvent *)event {
[self.delegate openGLViewDidShowOSMouseCursor:self];
[super mouseEntered:event];
[self scheduleMouseHide];
}
@ -305,6 +315,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
[super mouseExited:event];
[_mouseHideTimer invalidate];
_mouseHideTimer = nil;
[self.delegate openGLViewWillHideOSMouseCursor:self];
}
- (void)releaseMouse {
@ -313,6 +324,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
CGAssociateMouseAndMouseCursorPosition(true);
[NSCursor unhide];
[self.delegate openGLViewDidReleaseMouse:self];
[self.delegate openGLViewDidShowOSMouseCursor:self];
((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
// hide in the near future.
[self scheduleMouseHide];
[self.delegate openGLViewDidShowOSMouseCursor:self];
} else {
if(_mouseIsCaptured) {
// 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];
} else {
[self.delegate openGLViewDidShowOSMouseCursor:self];
}
}
}
@ -372,6 +387,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
_mouseIsCaptured = YES;
[NSCursor hide];
CGAssociateMouseAndMouseCursorPosition(false);
[self.delegate openGLViewWillHideOSMouseCursor:self];
[self.delegate openGLViewDidCaptureMouse:self];
if(self.shouldUsurpCommand) {
((CSApplication *)[NSApplication sharedApplication]).eventDelegate = self;

View File

@ -487,7 +487,7 @@ int main(int argc, char *argv[]) {
const ParsedArguments arguments = parse_arguments(argc, argv);
// 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.
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.
const auto speed_argument = arguments.selections.find("speed");
if(speed_argument != arguments.selections.end()) {
const char *speed_string = speed_argument->second.c_str();
char *end;
double speed = strtod(speed_string, &end);
{
const auto speed_argument = arguments.selections.find("speed");
if(speed_argument != arguments.selections.end()) {
const char *speed_string = speed_argument->second.c_str();
char *end;
const double speed = strtod(speed_string, &end);
if(size_t(end - speed_string) != strlen(speed_string)) {
std::cerr << "Unable to parse speed: " << speed_string << std::endl;
} else if(speed <= 0.0) {
std::cerr << "Cannot run at speed " << speed_string << "; speeds must be positive." << std::endl;
} else {
machine_runner.set_speed_multiplier(speed);
if(size_t(end - speed_string) != strlen(speed_string)) {
std::cerr << "Unable to parse speed: " << speed_string << std::endl;
} else if(speed <= 0.0) {
std::cerr << "Cannot run at speed " << speed_string << "; speeds must be positive." << std::endl;
} else {
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 {
public:
LowpassSpeaker(SampleSource &sample_source) : sample_source_(sample_source) {
// Propagate an initial volume level.
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.
float get_ideal_clock_rate_in_range(float minimum, float maximum) final {
std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_);

View File

@ -45,6 +45,9 @@ class Speaker {
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.
This will affect the number of input samples that are combined to produce one output sample.
@ -79,6 +82,8 @@ class Speaker {
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;
protected: