diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp index 1465771e0..eb833a0c6 100644 --- a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp +++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp @@ -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; } diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp index 3c75654db..f48bc639c 100644 --- a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp +++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp @@ -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 &buffer) final; diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 881f5f442..400a6be4f 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -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 = ""; }; 4B4DC8291D2C27A4003C5BF8 /* SerialBus.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SerialBus.cpp; sourceTree = ""; }; 4B4DC82A1D2C27A4003C5BF8 /* SerialBus.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SerialBus.hpp; sourceTree = ""; }; + 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 = ""; }; 4B51F70A20A521D700AFA2C1 /* Observer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Observer.hpp; sourceTree = ""; }; 4B54C0BB1F8D8E790050900F /* KeyboardMachine.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = KeyboardMachine.cpp; sourceTree = ""; }; @@ -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", diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal Kiosk.xcscheme b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal Kiosk.xcscheme index 66ee95aed..9a32a7574 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal Kiosk.xcscheme +++ b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal Kiosk.xcscheme @@ -56,6 +56,10 @@ argument = "/Users/thomasharte/Downloads/test-dsk-for-rw-and-50-60-hz/TEST-RW-60Hz.DSK" isEnabled = "NO"> + + @@ -70,7 +74,7 @@ + isEnabled = "NO"> + isEnabled = "NO"> + isEnabled = "NO"> diff --git a/OSBindings/Mac/Clock Signal/Base.lproj/MachineDocument.xib b/OSBindings/Mac/Clock Signal/Base.lproj/MachineDocument.xib index 0ed43edf8..32fd2c841 100644 --- a/OSBindings/Mac/Clock Signal/Base.lproj/MachineDocument.xib +++ b/OSBindings/Mac/Clock Signal/Base.lproj/MachineDocument.xib @@ -1,14 +1,15 @@ - + - + + @@ -19,7 +20,7 @@ - + @@ -28,9 +29,57 @@ + + + @@ -45,6 +94,11 @@ + + + + + diff --git a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift index f60789319..9fdf62fd6 100644 --- a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift +++ b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift @@ -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 + } + } } diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h index a437e5f4d..d8595e06c 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h @@ -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; diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index a3f3f27ae..bf23f3eed 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -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; diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h index 80c64f280..f4090fd7a 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h @@ -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 diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m index d1f742e9e..a30510de7 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m @@ -11,6 +11,8 @@ @import CoreVideo; @import GLKit; +#include + @interface CSOpenGLView () @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; diff --git a/OSBindings/SDL/main.cpp b/OSBindings/SDL/main.cpp index 451f3c8d9..c42d55284 100644 --- a/OSBindings/SDL/main.cpp +++ b/OSBindings/SDL/main.cpp @@ -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); + } } } diff --git a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp index 94d3a4b12..89ab38268 100644 --- a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp +++ b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp @@ -31,9 +31,16 @@ namespace Speaker { template 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 lock_guard(filter_parameters_mutex_); diff --git a/Outputs/Speaker/Speaker.hpp b/Outputs/Speaker/Speaker.hpp index 2f5da46ca..13a67e674 100644 --- a/Outputs/Speaker/Speaker.hpp +++ b/Outputs/Speaker/Speaker.hpp @@ -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: