1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-03 11:30:02 +00:00

Merge pull request #746 from TomHarte/LatencyChop

Reduces latency in macOS, improves concurrency
This commit is contained in:
Thomas Harte 2020-02-09 21:07:40 -05:00 committed by GitHub
commit f1cd35fa16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 424 additions and 291 deletions

View File

@ -78,9 +78,9 @@ void BestEffortUpdater::update_loop() {
// Cap running at 1/5th of a second, to avoid doing a huge amount of work after any
// brief system interruption.
const double duration = std::min(double(integer_duration) / 1e9, 0.2);
const double elapsed_duraation = delegate->update(this, duration, has_skipped_, flags);
const double elapsed_duration = delegate->update(this, duration, has_skipped_, flags);
previous_time_point_ += int64_t(elapsed_duraation * 1e9);
previous_time_point_ += int64_t(elapsed_duration * 1e9);
has_skipped_ = false;
}
}

View File

@ -194,8 +194,8 @@ void Video::run_for(HalfCycles duration) {
// There will be pixels this line, subject to the shifter pipeline.
// Divide into 8-[half-]cycle windows; at the start of each window fetch a word,
// and during the rest of the window, shift out.
int start_column = since_load >> 3;
const int end_column = (since_load + run_length) >> 3;
int start_column = (since_load - 1) >> 3;
const int end_column = (since_load + run_length - 1) >> 3;
while(start_column != end_column) {
data_latch_[data_latch_position_] = ram_[current_address_ & 262143];

View File

@ -133,7 +133,6 @@ class Video {
int current_address_ = 0;
uint16_t *ram_ = nullptr;
uint16_t line_buffer_[256];
int x_ = 0, y_ = 0, next_y_ = 0;
bool load_ = false;

View File

@ -21,6 +21,8 @@
#include "../../ClockReceiver/JustInTime.hpp"
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#define LOG_PREFIX "[SMS] "
#include "../../Outputs/Log.hpp"
#include "../../Analyser/Static/Sega/Target.hpp"

View File

@ -812,7 +812,6 @@
4BD4A8D01E077FD20020D856 /* PCMTrackTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */; };
4BD5D2682199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */; };
4BD5D2692199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */; };
4BD5F1951D13528900631CD1 /* CSBestEffortUpdater.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5F1941D13528900631CD1 /* CSBestEffortUpdater.mm */; };
4BD61664206B2AC800236112 /* QuickLoadOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BD61662206B2AC700236112 /* QuickLoadOptions.xib */; };
4BD67DCB209BE4D700AB2146 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD67DCA209BE4D600AB2146 /* StaticAnalyser.cpp */; };
4BD67DCC209BE4D700AB2146 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD67DCA209BE4D600AB2146 /* StaticAnalyser.cpp */; };
@ -1689,8 +1688,6 @@
4BD468F61D8DF41D0084958B /* 1770.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = 1770.hpp; path = 1770/1770.hpp; sourceTree = "<group>"; };
4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PCMTrackTests.mm; sourceTree = "<group>"; };
4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ScanTargetGLSLFragments.cpp; sourceTree = "<group>"; };
4BD5F1931D13528900631CD1 /* CSBestEffortUpdater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CSBestEffortUpdater.h; path = Updater/CSBestEffortUpdater.h; sourceTree = "<group>"; };
4BD5F1941D13528900631CD1 /* CSBestEffortUpdater.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = CSBestEffortUpdater.mm; path = Updater/CSBestEffortUpdater.mm; sourceTree = "<group>"; };
4BD601A920D89F2A00CBCE57 /* Log.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Log.hpp; path = ../../Outputs/Log.hpp; sourceTree = "<group>"; };
4BD61663206B2AC700236112 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/QuickLoadOptions.xib"; sourceTree = SOURCE_ROOT; };
4BD67DC9209BE4D600AB2146 /* StaticAnalyser.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = StaticAnalyser.hpp; sourceTree = "<group>"; };
@ -3276,7 +3273,6 @@
4BB73EAA1B587A5100552FC2 /* MainMenu.xib */,
4BE5F85A1C3E1C2500C43F01 /* Resources */,
4BDA00DB22E60EE900AC3CD0 /* ROMRequester */,
4BD5F1961D1352A000631CD1 /* Updater */,
4B55CE5A1C3B7D6F0093A61B /* Views */,
);
path = "Clock Signal";
@ -3624,15 +3620,6 @@
name = 1770;
sourceTree = "<group>";
};
4BD5F1961D1352A000631CD1 /* Updater */ = {
isa = PBXGroup;
children = (
4BD5F1931D13528900631CD1 /* CSBestEffortUpdater.h */,
4BD5F1941D13528900631CD1 /* CSBestEffortUpdater.mm */,
);
name = Updater;
sourceTree = "<group>";
};
4BD67DC8209BE4D600AB2146 /* DiskII */ = {
isa = PBXGroup;
children = (
@ -4493,7 +4480,6 @@
4B4518A41F75FD1C00926311 /* OricMFMDSK.cpp in Sources */,
4B4B1A3C200198CA00A0F866 /* KonamiSCC.cpp in Sources */,
4BB0A65B2044FD3000FB3688 /* SN76489.cpp in Sources */,
4BD5F1951D13528900631CD1 /* CSBestEffortUpdater.mm in Sources */,
4B894532201967B4007DE474 /* 6502.cpp in Sources */,
4BDB61EC203285AE0048AF91 /* Atari2600OptionsPanel.swift in Sources */,
4BBB70A8202014E2002FE009 /* MultiCRTMachine.cpp in Sources */,

View File

@ -117,7 +117,6 @@ static void audioOutputCallback(
kCFRunLoopCommonModes,
0,
&_audioQueue)) {
AudioQueueStart(_audioQueue, NULL);
}
}
@ -175,6 +174,10 @@ static void audioOutputCallback(
AudioQueueEnqueueBuffer(_audioQueue, newBuffer, 0, NULL);
[_storedBuffersLock unlock];
// 'Start' the queue. This is documented to be a no-op if the queue is already started,
// and it's better to defer starting it until at least some data is available.
AudioQueueStart(_audioQueue, NULL);
}
#pragma mark - Sampling Rate getters

View File

@ -13,7 +13,6 @@
#import "CSOpenGLView.h"
#import "CSROMReceiverView.h"
#import "CSBestEffortUpdater.h"
#import "CSJoystickManager.h"
#import "NSData+CRC32.h"

View File

@ -24,8 +24,6 @@ class MachineDocument:
private let actionLock = NSLock()
/// Ensures exclusive access between calls to machine.updateView and machine.drawView, and close().
private let drawLock = NSLock()
/// Ensures exclusive access to the best-effort updater.
private let bestEffortLock = NSLock()
// MARK: - Machine details.
@ -43,9 +41,6 @@ class MachineDocument:
/// The output audio queue, if any.
private var audioQueue: CSAudioQueue!
/// The best-effort updater.
private var bestEffortUpdater: CSBestEffortUpdater?
// MARK: - Main NIB connections.
/// The OpenGL view to receive this machine's display.
@ -89,19 +84,14 @@ class MachineDocument:
}
override func close() {
machine.stop()
activityPanel?.setIsVisible(false)
activityPanel = nil
optionsPanel?.setIsVisible(false)
optionsPanel = nil
bestEffortLock.lock()
if let bestEffortUpdater = bestEffortUpdater {
bestEffortUpdater.flush()
self.bestEffortUpdater = nil
}
bestEffortLock.unlock()
actionLock.lock()
drawLock.lock()
machine = nil
@ -187,9 +177,7 @@ class MachineDocument:
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.setView(openGLView, aspectRatio: Float(aspectRatio.width / aspectRatio.height))
// Attach an options panel if one is available.
if let optionsPanelNibName = self.machineDescription?.optionsPanelNibName {
@ -200,7 +188,6 @@ class MachineDocument:
}
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.
@ -220,7 +207,7 @@ class MachineDocument:
openGLView.window!.makeFirstResponder(openGLView)
// Start forwarding best-effort updates.
self.bestEffortUpdater!.setMachine(machine)
machine.start()
}
}
@ -251,24 +238,11 @@ class MachineDocument:
/// Responds to the CSAudioQueueDelegate dry-queue warning message by requesting a machine update.
final func audioQueueIsRunningDry(_ audioQueue: CSAudioQueue) {
bestEffortLock.lock()
bestEffortUpdater?.update(with: .audioNeeded)
bestEffortLock.unlock()
}
/// Responds to the CSOpenGLViewDelegate redraw message by requesting a machine update if this is a timed
/// request, and ordering a redraw regardless of the motivation.
final func openGLViewRedraw(_ view: CSOpenGLView, event redrawEvent: CSOpenGLViewRedrawEvent) {
if redrawEvent == .timer {
bestEffortLock.lock()
if let bestEffortUpdater = bestEffortUpdater {
bestEffortLock.unlock()
bestEffortUpdater.update()
} else {
bestEffortLock.unlock()
}
}
if drawLock.try() {
if redrawEvent == .timer {
machine.updateView(forPixelSize: view.backingSize)

View File

@ -31,7 +31,15 @@
}
- (void)invalidate {
dispatch_suspend(_timer);
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
dispatch_source_set_cancel_handler(_timer, ^{
[lock lock];
[lock unlockWithCondition:1];
});
dispatch_source_cancel(_timer);
[lock lockWhenCondition:1];
}
@end

View File

@ -57,13 +57,14 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
*/
- (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result missingROMs:(nullable inout NSMutableArray<CSMissingROM *> *)missingROMs NS_DESIGNATED_INITIALIZER;
- (NSTimeInterval)runForInterval:(NSTimeInterval)interval untilEvent:(int)events;
- (float)idealSamplingRateFromRange:(NSRange)range;
- (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize;
- (void)setView:(nullable CSOpenGLView *)view aspectRatio:(float)aspectRatio;
- (void)start;
- (void)stop;
- (void)updateViewForPixelSize:(CGSize)pixelSize;
- (void)drawViewForPixelSize:(CGSize)pixelSize;

View File

@ -10,6 +10,7 @@
#import "CSMachine+Target.h"
#include "CSROMFetcher.hpp"
#import "CSHighPrecisionTimer.h"
#include "MediaTarget.hpp"
#include "JoystickMachine.hpp"
@ -24,6 +25,7 @@
#import "NSBundle+DataResource.h"
#import "NSData+StdVector.h"
#include <atomic>
#include <bitset>
#import <OpenGL/OpenGL.h>
@ -32,7 +34,7 @@
#include "../../../../Outputs/OpenGL/ScanTarget.hpp"
#include "../../../../Outputs/OpenGL/Screenshot.hpp"
@interface CSMachine()
@interface CSMachine() <CSOpenGLViewDisplayLinkDelegate>
- (void)speaker:(Outputs::Speaker::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length;
- (void)speakerDidChangeInputClock:(Outputs::Speaker::Speaker *)speaker;
- (void)addLED:(NSString *)led;
@ -149,6 +151,17 @@ struct ActivityObserver: public Activity::Observer {
std::bitset<65536> _depressedKeys;
NSMutableArray<NSString *> *_leds;
CSHighPrecisionTimer *_timer;
CGSize _pixelSize;
std::atomic_flag _isUpdating;
int64_t _syncTime;
int64_t _timeDiff;
double _refreshPeriod;
BOOL _isSyncLocking;
double _speedMultiplier;
NSTimer *_joystickTimer;
std::unique_ptr<Outputs::Display::OpenGL::ScanTarget> _scanTarget;
}
@ -156,6 +169,7 @@ struct ActivityObserver: public Activity::Observer {
self = [super init];
if(self) {
_analyser = result;
_speedMultiplier = 1.0;
Machine::Error error;
std::vector<ROMMachine::ROM> missing_roms;
@ -201,6 +215,8 @@ struct ActivityObserver: public Activity::Observer {
_speakerDelegate.machineAccessLock = _delegateMachineAccessLock;
_joystickMachine = _machine->joystick_machine();
[self updateJoystickTimer];
_isUpdating.clear();
}
return self;
}
@ -214,6 +230,8 @@ struct ActivityObserver: public Activity::Observer {
}
- (void)dealloc {
[_joystickTimer invalidate];
// The two delegate's references to this machine are nilled out here because close_output may result
// in a data flush, which might cause an audio callback, which could cause the audio queue to decide
// that it's out of data, resulting in an attempt further to run the machine while it is dealloc'ing.
@ -259,65 +277,73 @@ struct ActivityObserver: public Activity::Observer {
}
}
- (NSTimeInterval)runForInterval:(NSTimeInterval)interval untilEvent:(int)events {
- (void)updateJoystickTimer {
// Joysticks updates are scheduled for a nominal 200 polls/second, using a plain old NSTimer.
if(_joystickMachine && _joystickManager) {
_joystickTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / 200.0 target:self selector:@selector(updateJoysticks) userInfo:nil repeats:YES];
} else {
[_joystickTimer invalidate];
_joystickTimer = nil;
}
}
- (void)updateJoysticks {
[_joystickManager update];
// TODO: configurable mapping from physical joypad inputs to machine inputs.
// Until then, apply a default mapping.
@synchronized(self) {
if(_joystickMachine && _joystickManager) {
[_joystickManager update];
size_t c = 0;
auto &machine_joysticks = _joystickMachine->get_joysticks();
for(CSJoystick *joystick in _joystickManager.joysticks) {
size_t target = c % machine_joysticks.size();
++c;
// TODO: configurable mapping from physical joypad inputs to machine inputs.
// Until then, apply a default mapping.
size_t c = 0;
auto &machine_joysticks = _joystickMachine->get_joysticks();
for(CSJoystick *joystick in _joystickManager.joysticks) {
size_t target = c % machine_joysticks.size();
++c;
// Post the first two analogue axes presented by the controller as horizontal and vertical inputs,
// unless the user seems to be using a hat.
// SDL will return a value in the range [-32768, 32767], so map from that to [0, 1.0]
if(!joystick.hats.count || !joystick.hats[0].direction) {
if(joystick.axes.count > 0) {
const float x_axis = joystick.axes[0].position;
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Horizontal), x_axis);
}
if(joystick.axes.count > 1) {
const float y_axis = joystick.axes[1].position;
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Vertical), y_axis);
}
} else {
// Forward hats as directions; hats always override analogue inputs.
for(CSJoystickHat *hat in joystick.hats) {
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Up), !!(hat.direction & CSJoystickHatDirectionUp));
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Down), !!(hat.direction & CSJoystickHatDirectionDown));
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Left), !!(hat.direction & CSJoystickHatDirectionLeft));
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Right), !!(hat.direction & CSJoystickHatDirectionRight));
}
// Post the first two analogue axes presented by the controller as horizontal and vertical inputs,
// unless the user seems to be using a hat.
// SDL will return a value in the range [-32768, 32767], so map from that to [0, 1.0]
if(!joystick.hats.count || !joystick.hats[0].direction) {
if(joystick.axes.count > 0) {
const float x_axis = joystick.axes[0].position;
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Horizontal), x_axis);
}
if(joystick.axes.count > 1) {
const float y_axis = joystick.axes[1].position;
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Vertical), y_axis);
}
} else {
// Forward hats as directions; hats always override analogue inputs.
for(CSJoystickHat *hat in joystick.hats) {
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Up), !!(hat.direction & CSJoystickHatDirectionUp));
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Down), !!(hat.direction & CSJoystickHatDirectionDown));
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Left), !!(hat.direction & CSJoystickHatDirectionLeft));
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Right), !!(hat.direction & CSJoystickHatDirectionRight));
}
}
// Forward all fire buttons, mapping as a function of index.
if(machine_joysticks[target]->get_number_of_fire_buttons()) {
std::vector<bool> button_states((size_t)machine_joysticks[target]->get_number_of_fire_buttons());
for(CSJoystickButton *button in joystick.buttons) {
if(button.isPressed) button_states[(size_t)(((int)button.index - 1) % machine_joysticks[target]->get_number_of_fire_buttons())] = true;
}
for(size_t index = 0; index < button_states.size(); ++index) {
machine_joysticks[target]->set_input(
Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Fire, index),
button_states[index]);
}
// Forward all fire buttons, mapping as a function of index.
if(machine_joysticks[target]->get_number_of_fire_buttons()) {
std::vector<bool> button_states((size_t)machine_joysticks[target]->get_number_of_fire_buttons());
for(CSJoystickButton *button in joystick.buttons) {
if(button.isPressed) button_states[(size_t)(((int)button.index - 1) % machine_joysticks[target]->get_number_of_fire_buttons())] = true;
}
for(size_t index = 0; index < button_states.size(); ++index) {
machine_joysticks[target]->set_input(
Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Fire, index),
button_states[index]);
}
}
}
return _machine->crt_machine()->run_until(interval, events);
}
}
- (void)setView:(CSOpenGLView *)view aspectRatio:(float)aspectRatio {
_view = view;
_view.displayLinkDelegate = self;
[view performWithGLContext:^{
[self setupOutputWithAspectRatio:aspectRatio];
}];
} flushDrawable:NO];
}
- (void)setupOutputWithAspectRatio:(float)aspectRatio {
@ -326,7 +352,7 @@ struct ActivityObserver: public Activity::Observer {
}
- (void)updateViewForPixelSize:(CGSize)pixelSize {
_scanTarget->update((int)pixelSize.width, (int)pixelSize.height);
// _pixelSize = pixelSize;
// @synchronized(self) {
// const auto scan_status = _machine->crt_machine()->get_scan_status();
@ -375,15 +401,17 @@ struct ActivityObserver: public Activity::Observer {
}
- (void)setJoystickManager:(CSJoystickManager *)joystickManager {
@synchronized(self) {
_joystickManager = joystickManager;
if(_joystickMachine) {
_joystickManager = joystickManager;
if(_joystickMachine) {
@synchronized(self) {
auto &machine_joysticks = _joystickMachine->get_joysticks();
for(const auto &joystick: machine_joysticks) {
joystick->reset_all_inputs();
}
}
}
[self updateJoystickTimer];
}
- (void)setKey:(uint16_t)key characters:(NSString *)characters isPressed:(BOOL)isPressed {
@ -694,4 +722,139 @@ struct ActivityObserver: public Activity::Observer {
return _leds;
}
#pragma mark - Timer
- (void)openGLViewDisplayLinkDidFire:(CSOpenGLView *)view now:(const CVTimeStamp *)now outputTime:(const CVTimeStamp *)outputTime {
// First order of business: grab a timestamp.
const auto timeNow = std::chrono::high_resolution_clock::now().time_since_epoch().count();
CGSize pixelSize = view.backingSize;
BOOL isSyncLocking;
@synchronized(self) {
// Store a means to map from CVTimeStamp.hostTime to std::chrono::high_resolution_clock;
// there is an extremely dodgy assumption here that both are in the same units (and, below, that both as in ns).
if(!_timeDiff) {
_timeDiff = int64_t(now->hostTime) - int64_t(timeNow);
}
// Store the next end-of-frame time. TODO: and start of next and implied visible duration, if raster racing?
_syncTime = int64_t(now->hostTime) + _timeDiff;
// Also crib the current view pixel size.
_pixelSize = pixelSize;
// Set the current refresh period.
_refreshPeriod = double(now->videoRefreshPeriod) / double(now->videoTimeScale);
// Determine where responsibility lies for drawing.
isSyncLocking = _isSyncLocking;
}
// Draw the current output. (TODO: do this within the timer if either raster racing or, at least, sync matching).
if(!isSyncLocking) {
[self.view performWithGLContext:^{
self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height);
} flushDrawable:YES];
}
}
#define TICKS 600
- (void)start {
__block auto lastTime = std::chrono::high_resolution_clock::now().time_since_epoch().count();
_timer = [[CSHighPrecisionTimer alloc] initWithTask:^{
// Grab the time now and, therefore, the amount of time since the timer last fired.
const auto timeNow = std::chrono::high_resolution_clock::now().time_since_epoch().count();
const auto duration = timeNow - lastTime;
CGSize pixelSize;
BOOL splitAndSync = NO;
@synchronized(self) {
// If this tick includes vsync then inspect the machine.
if(timeNow >= self->_syncTime && lastTime < self->_syncTime) {
// Grab the scan status and check out the machine's current frame time.
// If it's stable and within 3% of a non-zero integer multiple of the
// display rate, mark this time window to be split over the sync.
const auto scan_status = self->_machine->crt_machine()->get_scan_status();
double ratio = 1.0;
if(scan_status.field_duration_gradient < 0.00001) {
ratio = self->_refreshPeriod / scan_status.field_duration;
const double integerRatio = round(ratio);
if(integerRatio > 0.0) {
ratio /= integerRatio;
constexpr double maximumAdjustment = 1.03;
splitAndSync = ratio <= maximumAdjustment && ratio >= 1 / maximumAdjustment;
}
}
self->_isSyncLocking = splitAndSync;
// If the time window is being split, run up to the split, then check out machine speed, possibly
// adjusting multiplier, then run after the split.
if(splitAndSync) {
self->_machine->crt_machine()->run_for((double)(self->_syncTime - lastTime) / 1e9);
// The host versus emulated ratio is calculated based on the current perceived frame duration of the machine.
// Either that number is exactly correct or it's already the result of some sort of low-pass filter. So there's
// no benefit to second guessing it here — just take it to be correct.
//
// ... with one slight caveat, which is that it is desireable to adjust phase here, to align vertical sync points.
// So the set speed multiplier may be adjusted slightly to aim for that.
double speed_multiplier = 1.0 / ratio;
if(scan_status.current_position > 0.0) {
constexpr double adjustmentRatio = 1.005;
if(scan_status.current_position < 0.5) speed_multiplier /= adjustmentRatio;
else speed_multiplier *= adjustmentRatio;
}
self->_speedMultiplier = (self->_speedMultiplier * 0.95) + (speed_multiplier * 0.05);
self->_machine->crt_machine()->set_speed_multiplier(self->_speedMultiplier);
self->_machine->crt_machine()->run_for((double)(timeNow - self->_syncTime) / 1e9);
}
}
// If the time window is being split, run up to the split, then check out machine speed, possibly
// adjusting multiplier, then run after the split.
if(!splitAndSync) {
self->_machine->crt_machine()->run_for((double)duration / 1e9);
}
pixelSize = self->_pixelSize;
}
// If this was not a split-and-sync then dispatch the update request asynchronously, unless
// there is an earlier one not yet finished, in which case don't worry about it for now.
//
// If it was a split-and-sync then spin until it is safe to dispatch, and dispatch with
// a concluding draw. Implicit assumption here: whatever is left to be done in the final window
// can be done within the retrace period.
auto wasUpdating = self->_isUpdating.test_and_set();
if(wasUpdating && splitAndSync) {
while(self->_isUpdating.test_and_set());
wasUpdating = false;
}
if(!wasUpdating) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
[self.view performWithGLContext:^{
self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height);
if(splitAndSync) {
self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height);
}
} flushDrawable:splitAndSync];
self->_isUpdating.clear();
});
}
lastTime = timeNow;
} interval:uint64_t(1000000000) / uint64_t(TICKS)];
}
#undef TICKS
- (void)stop {
[_timer invalidate];
_timer = nil;
}
@end

View File

@ -1,27 +0,0 @@
//
// CSBestEffortUpdater.h
// Clock Signal
//
// Created by Thomas Harte on 16/06/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreVideo/CoreVideo.h>
#import "CSMachine.h"
// The following is coupled to the definitions in CRTMachine.hpp, but exposed here
// for the benefit of Swift.
typedef NS_ENUM(NSInteger, CSBestEffortUpdaterEvent) {
CSBestEffortUpdaterEventAudioNeeded = 1 << 0
};
@interface CSBestEffortUpdater : NSObject
- (void)update;
- (void)updateWithEvent:(CSBestEffortUpdaterEvent)event;
- (void)flush;
- (void)setMachine:(CSMachine *)machine;
@end

View File

@ -1,51 +0,0 @@
//
// CSBestEffortUpdater.m
// Clock Signal
//
// Created by Thomas Harte on 16/06/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#import "CSBestEffortUpdater.h"
#include "BestEffortUpdater.hpp"
struct UpdaterDelegate: public Concurrency::BestEffortUpdater::Delegate {
__weak CSMachine *machine;
Time::Seconds update(Concurrency::BestEffortUpdater *updater, Time::Seconds seconds, bool did_skip_previous_update, int flags) final {
return [machine runForInterval:seconds untilEvent:flags];
}
};
@implementation CSBestEffortUpdater {
Concurrency::BestEffortUpdater _updater;
UpdaterDelegate _updaterDelegate;
}
- (instancetype)init {
self = [super init];
if(self) {
_updater.set_delegate(&_updaterDelegate);
}
return self;
}
- (void)update {
_updater.update();
}
- (void)updateWithEvent:(CSBestEffortUpdaterEvent)event {
_updater.update((int)event);
}
- (void)flush {
_updater.flush();
}
- (void)setMachine:(CSMachine *)machine {
_updater.flush();
_updaterDelegate.machine = machine;
}
@end

View File

@ -101,6 +101,23 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
@end
/*!
Although I'm still on the fence about this as a design decision, CSOpenGLView is itself responsible
for creating and destroying a CVDisplayLink. There's a practical reason for this: you'll get real synchronisation
only if a link is explicitly tied to a particular display, and the CSOpenGLView therefore owns the knowledge
necessary to decide when to create and modify them. It doesn't currently just propagate "did change screen"-type
messages because I haven't yet found a way to track that other than polling, in which case I might as well put
that into the display link callback.
*/
@protocol CSOpenGLViewDisplayLinkDelegate
/*!
Informs the delegate that the display link has fired.
*/
- (void)openGLViewDisplayLinkDidFire:(nonnull CSOpenGLView *)view now:(nonnull const CVTimeStamp *)now outputTime:(nonnull const CVTimeStamp *)outputTime;
@end
/*!
Provides an OpenGL canvas with a refresh-linked update timer that can forward a subset
of typical first-responder actions.
@ -109,6 +126,7 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
@property (atomic, weak, nullable) id <CSOpenGLViewDelegate> delegate;
@property (nonatomic, weak, nullable) id <CSOpenGLViewResponderDelegate> responderDelegate;
@property (atomic, weak, nullable) id <CSOpenGLViewDisplayLinkDelegate> displayLinkDelegate;
/// Determines whether the view offers mouse capturing — i.e. if the user clicks on the view then
/// then the system cursor is disabled and the mouse events defined by CSOpenGLViewResponderDelegate
@ -139,6 +157,7 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
Locks this view's OpenGL context and makes it current, performs @c action and then unlocks
the context. @c action is performed on the calling queue.
*/
- (void)performWithGLContext:(nonnull dispatch_block_t)action flushDrawable:(BOOL)flushDrawable;
- (void)performWithGLContext:(nonnull dispatch_block_t)action;
/*!

View File

@ -30,11 +30,6 @@
// Note the initial screen.
_currentScreen = self.window.screen;
// Synchronize buffer swaps with vertical refresh rate.
// TODO: discard this, once scheduling is sufficiently intelligent?
GLint swapInt = 1;
[[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval];
// set the clear colour
[self.openGLContext makeCurrentContext];
glClearColor(0.0, 0.0, 0.0, 1.0);
@ -75,7 +70,8 @@
static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) {
CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext;
[view drawAtTime:now frequency:CVDisplayLinkGetActualOutputVideoRefreshPeriod(displayLink)];
[view checkDisplayLink];
[view.displayLinkDelegate openGLViewDisplayLinkDidFire:view now:now outputTime:outputTime];
/*
Do not touch the display link from after this call; there's a bit of a race condition with setupDisplayLink.
Specifically: Apple provides CVDisplayLinkStop but a call to that merely prevents future calls to the callback,
@ -89,7 +85,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
return kCVReturnSuccess;
}
- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency {
- (void)checkDisplayLink {
// Test now whether the screen this view is on has changed since last time it was checked.
// There's likely a callback available for this, on NSWindow if nowhere else, or an NSNotification,
// but since this method is going to be called repeatedly anyway, and the test is cheap, polling
@ -105,7 +101,9 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
// the window is actually on, and at its rate.
[self setupDisplayLink];
}
}
- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency {
[self redrawWithEvent:CSOpenGLViewRedrawEventTimer];
}
@ -113,11 +111,10 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
[self redrawWithEvent:CSOpenGLViewRedrawEventAppKit];
}
- (void)redrawWithEvent:(CSOpenGLViewRedrawEvent)event {
- (void)redrawWithEvent:(CSOpenGLViewRedrawEvent)event {
[self performWithGLContext:^{
[self.delegate openGLViewRedraw:self event:event];
CGLFlushDrawable([[self openGLContext] CGLContextObj]);
}];
} flushDrawable:YES];
}
- (void)invalidate {
@ -145,7 +142,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
[self performWithGLContext:^{
CGSize viewSize = [self backingSize];
glViewport(0, 0, (GLsizei)viewSize.width, (GLsizei)viewSize.height);
}];
} flushDrawable:NO];
}
- (void)awakeFromNib {
@ -178,11 +175,17 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
[self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]];
}
- (void)performWithGLContext:(dispatch_block_t)action {
- (void)performWithGLContext:(dispatch_block_t)action flushDrawable:(BOOL)flushDrawable {
CGLLockContext([[self openGLContext] CGLContextObj]);
[self.openGLContext makeCurrentContext];
action();
CGLUnlockContext([[self openGLContext] CGLContextObj]);
if(flushDrawable) CGLFlushDrawable([[self openGLContext] CGLContextObj]);
}
- (void)performWithGLContext:(nonnull dispatch_block_t)action {
[self performWithGLContext:action flushDrawable:NO];
}
#pragma mark - NSResponder

View File

@ -489,10 +489,7 @@ Outputs::Display::ScanStatus CRT::get_scaled_scan_status() const {
status.field_duration = float(vertical_flywheel_->get_locked_period()) / float(time_multiplier_);
status.field_duration_gradient = float(vertical_flywheel_->get_last_period_adjustment()) / float(time_multiplier_);
status.retrace_duration = float(vertical_flywheel_->get_retrace_period()) / float(time_multiplier_);
status.current_position =
std::max(0.0f,
float(vertical_flywheel_->get_current_output_position()) / (float(vertical_flywheel_->get_locked_period()) * float(time_multiplier_))
);
status.current_position = float(vertical_flywheel_->get_current_phase()) / float(vertical_flywheel_->get_locked_scan_period());
status.hsync_count = vertical_flywheel_->get_number_of_retraces();
return status;
}

View File

@ -139,6 +139,16 @@ struct Flywheel {
return counter_ - retrace_time_;
}
/*!
Returns the current 'phase' 0 is the start of the display; a count up to 0 from a negative number represents
the retrace period and it will then count up to get_locked_scan_period().
@returns The current output position.
*/
inline int get_current_phase() {
return counter_ - retrace_time_;
}
/*!
@returns the amount of time since retrace last began. Time then counts monotonically up from zero.
*/
@ -160,6 +170,13 @@ struct Flywheel {
return standard_period_ - retrace_time_;
}
/*!
@returns the actual length of the scan period (excluding retrace).
*/
inline int get_locked_scan_period() {
return expected_next_sync_ - retrace_time_;
}
/*!
@returns the expected length of a complete scan and retrace cycle.
*/

View File

@ -251,21 +251,6 @@ void ScanTarget::will_change_owner() {
vended_scan_ = nullptr;
}
void ScanTarget::submit() {
if(allocation_has_failed_) {
// Reset all pointers to where they were; this also means
// the stencil won't be properly populated.
write_pointers_ = submit_pointers_.load();
frame_is_complete_ = false;
} else {
// Advance submit pointer.
submit_pointers_.store(write_pointers_);
}
// Continue defaulting to a failed allocation for as long as there isn't a line available.
allocation_has_failed_ = line_allocation_has_failed_;
}
void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t composite_amplitude) {
// Forward the event to the display metrics tracker.
display_metrics_.announce_event(event);
@ -273,10 +258,8 @@ void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display::
if(event == ScanTarget::Event::EndVerticalRetrace) {
// The previous-frame-is-complete flag is subject to a two-slot queue because
// measurement for *this* frame needs to begin now, meaning that the previous
// result needs to be put somewhere. Setting frame_is_complete_ back to true
// only after it has been put somewhere also doesn't work, since if the first
// few lines of a frame are skipped for any reason, there'll be nowhere to
// put it.
// result needs to be put somewhere — it'll be attached to the first successful
// line output.
is_first_in_frame_ = true;
previous_frame_was_complete_ = frame_is_complete_;
frame_is_complete_ = true;
@ -299,24 +282,13 @@ void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display::
// Attempt to allocate a new line; note allocation failure if necessary.
const auto next_line = uint16_t((write_pointers_.line + 1) % LineBufferHeight);
if(next_line == read_pointers.line) {
line_allocation_has_failed_ = allocation_has_failed_ = true;
allocation_has_failed_ = true;
active_line_ = nullptr;
} else {
line_allocation_has_failed_ = false;
write_pointers_.line = next_line;
active_line_ = &line_buffer_[size_t(write_pointers_.line)];
}
provided_scans_ = 0;
} else {
// Just check whether a new line is available now, if waiting.
if(line_allocation_has_failed_) {
const auto next_line = uint16_t((write_pointers_.line + 1) % LineBufferHeight);
if(next_line != read_pointers.line) {
line_allocation_has_failed_ = false;
write_pointers_.line = next_line;
active_line_ = &line_buffer_[size_t(write_pointers_.line)];
}
}
}
if(active_line_) {
@ -329,6 +301,7 @@ void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display::
}
} else {
if(active_line_) {
// A successfully-allocated line is ending.
active_line_->end_points[1].x = location.x;
active_line_->end_points[1].y = location.y;
active_line_->end_points[1].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace;
@ -345,6 +318,18 @@ void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display::
}
#endif
}
// A line is complete; submit latest updates if nothing failed.
if(allocation_has_failed_) {
// Reset all pointers to where they were; this also means
// the stencil won't be properly populated.
write_pointers_ = submit_pointers_.load();
frame_is_complete_ = false;
} else {
// Advance submit pointer.
submit_pointers_.store(write_pointers_);
}
allocation_has_failed_ = false;
}
output_is_visible_ = is_visible;
}

View File

@ -77,7 +77,6 @@ class ScanTarget: public Outputs::Display::ScanTarget {
void end_scan() final;
uint8_t *begin_data(size_t required_length, size_t required_alignment) final;
void end_data(size_t actual_length) final;
void submit() final;
void announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t colour_burst_amplitude) final;
void will_change_owner() final;
@ -109,7 +108,7 @@ class ScanTarget: public Outputs::Display::ScanTarget {
// The sizes below might be less hassle as something more natural like ints,
// but squeezing this struct into 64 bits makes the std::atomics more likely
// to be lock free; they are under LLVM x86-64.
int write_area = 0;
int write_area = 1; // By convention this points to the vended area. Which is preceded by a guard pixel. So a sensible default construction is write_area = 1.
uint16_t scan_buffer = 0;
uint16_t line = 0;
};
@ -188,7 +187,6 @@ class ScanTarget: public Outputs::Display::ScanTarget {
// Track allocation failures.
bool data_is_allocated_ = false;
bool allocation_has_failed_ = false;
bool line_allocation_has_failed_ = false;
// Receives scan target modals.
Modals modals_;

View File

@ -273,18 +273,22 @@ struct ScanTarget {
/// data and scan allocations should be invalidated.
virtual void will_change_owner() {}
/// Marks the end of an atomic set of data. Drawing is best effort, so the scan target should either:
/// Acts as a fence, marking the end of an atomic set of [begin/end]_[scan/data] calls] — all future pieces of
/// data will have no relation to scans prior to the submit() and all future scans will similarly have no relation to
/// prior runs of data.
///
/// Drawing is defined to be best effort, so the scan target should either:
///
/// (i) output everything received since the previous submit; or
/// (ii) output nothing.
/// (ii) output nothing.
///
/// If there were any allocation failures — i.e. any nullptr responses to begin_data or
/// begin_scan — then (ii) is a required response. But a scan target may also need to opt for (ii)
/// for any other reason.
///
/// The ScanTarget isn't bound to take any drawing action immediately; it may sit on submitted data for
/// as long as it feels is appropriate subject to an @c flush.
virtual void submit() = 0;
/// as long as it feels is appropriate, subject to a @c flush.
virtual void submit() {}
/*
@ -303,6 +307,12 @@ struct ScanTarget {
/*!
Provides a hint that the named event has occurred.
Guarantee:
* any announce acts as an implicit fence on data/scans, much as a submit().
Permitted ScanTarget implementation:
* ignore all output during retrace periods.
@param event The event.
@param is_visible @c true if the output stream is visible immediately after this event; @c false otherwise.
@param location The location of the event.

View File

@ -60,6 +60,10 @@ template <typename T> class LowpassSpeaker: public Speaker {
// Implemented as per Speaker.
void set_computed_output_rate(float cycles_per_second, int buffer_size) final {
std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_);
if(filter_parameters_.output_cycles_per_second == cycles_per_second && size_t(buffer_size) == output_buffer_.size()) {
return;
}
filter_parameters_.output_cycles_per_second = cycles_per_second;
filter_parameters_.parameters_are_dirty = true;
output_buffer_.resize(std::size_t(buffer_size));
@ -70,6 +74,9 @@ template <typename T> class LowpassSpeaker: public Speaker {
*/
void set_input_rate(float cycles_per_second) {
std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_);
if(filter_parameters_.input_cycles_per_second == cycles_per_second) {
return;
}
filter_parameters_.input_cycles_per_second = cycles_per_second;
filter_parameters_.parameters_are_dirty = true;
filter_parameters_.input_rate_changed = true;
@ -83,6 +90,9 @@ template <typename T> class LowpassSpeaker: public Speaker {
*/
void set_high_frequency_cutoff(float high_frequency) {
std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_);
if(filter_parameters_.high_frequency_cutoff == high_frequency) {
return;
}
filter_parameters_.high_frequency_cutoff = high_frequency;
filter_parameters_.parameters_are_dirty = true;
}
@ -99,6 +109,12 @@ template <typename T> class LowpassSpeaker: public Speaker {
}
private:
enum class Conversion {
ResampleSmaller,
Copy,
ResampleLarger
} conversion_ = Conversion::Copy;
/*!
Advances by the number of cycles specified, obtaining data from the sample source supplied
at construction, filtering it and passing it on to the speaker's delegate if there is one.
@ -121,69 +137,41 @@ template <typename T> class LowpassSpeaker: public Speaker {
delegate_->speaker_did_change_input_clock(this);
}
// If input and output rates exactly match, and no additional cut-off has been specified,
// just accumulate results and pass on.
if( filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second &&
filter_parameters.high_frequency_cutoff < 0.0) {
while(cycles_remaining) {
const auto cycles_to_read = std::min(output_buffer_.size() - output_buffer_pointer_, cycles_remaining);
switch(conversion_) {
case Conversion::Copy:
while(cycles_remaining) {
const auto cycles_to_read = std::min(output_buffer_.size() - output_buffer_pointer_, cycles_remaining);
sample_source_.get_samples(cycles_to_read, &output_buffer_[output_buffer_pointer_]);
output_buffer_pointer_ += cycles_to_read;
sample_source_.get_samples(cycles_to_read, &output_buffer_[output_buffer_pointer_]);
output_buffer_pointer_ += cycles_to_read;
// announce to delegate if full
if(output_buffer_pointer_ == output_buffer_.size()) {
output_buffer_pointer_ = 0;
did_complete_samples(this, output_buffer_);
}
cycles_remaining -= cycles_to_read;
}
return;
}
// If the output rate is less than the input rate, or an additional cut-off has been specified, use the filter.
if( filter_parameters.input_cycles_per_second > filter_parameters.output_cycles_per_second ||
(filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second && filter_parameters.high_frequency_cutoff >= 0.0)) {
while(cycles_remaining) {
const auto cycles_to_read = std::min(cycles_remaining, input_buffer_.size() - input_buffer_depth_);
sample_source_.get_samples(cycles_to_read, &input_buffer_[input_buffer_depth_]);
cycles_remaining -= cycles_to_read;
input_buffer_depth_ += cycles_to_read;
if(input_buffer_depth_ == input_buffer_.size()) {
output_buffer_[output_buffer_pointer_] = filter_->apply(input_buffer_.data());
output_buffer_pointer_++;
// Announce to delegate if full.
// announce to delegate if full
if(output_buffer_pointer_ == output_buffer_.size()) {
output_buffer_pointer_ = 0;
did_complete_samples(this, output_buffer_);
}
// If the next loop around is going to reuse some of the samples just collected, use a memmove to
// preserve them in the correct locations (TODO: use a longer buffer to fix that) and don't skip
// anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse.
const auto steps = stepper_->step();
if(steps < input_buffer_.size()) {
auto *const input_buffer = input_buffer_.data();
std::memmove( input_buffer,
&input_buffer[steps],
sizeof(int16_t) * (input_buffer_.size() - steps));
input_buffer_depth_ -= steps;
} else {
if(steps > input_buffer_.size())
sample_source_.skip_samples(steps - input_buffer_.size());
input_buffer_depth_ = 0;
cycles_remaining -= cycles_to_read;
}
break;
case Conversion::ResampleSmaller:
while(cycles_remaining) {
const auto cycles_to_read = std::min(cycles_remaining, input_buffer_.size() - input_buffer_depth_);
sample_source_.get_samples(cycles_to_read, &input_buffer_[input_buffer_depth_]);
cycles_remaining -= cycles_to_read;
input_buffer_depth_ += cycles_to_read;
if(input_buffer_depth_ == input_buffer_.size()) {
resample_input_buffer();
}
}
}
break;
return;
case Conversion::ResampleLarger:
// TODO: input rate is less than output rate.
break;
}
// TODO: input rate is less than output rate
}
T &sample_source_;
@ -218,7 +206,6 @@ template <typename T> class LowpassSpeaker: public Speaker {
);
number_of_taps = (number_of_taps * 2) | 1;
output_buffer_pointer_ = 0;
stepper_ = std::make_unique<SignalProcessing::Stepper>(
uint64_t(filter_parameters.input_cycles_per_second),
uint64_t(filter_parameters.output_cycles_per_second));
@ -230,8 +217,68 @@ template <typename T> class LowpassSpeaker: public Speaker {
high_pass_frequency,
SignalProcessing::FIRFilter::DefaultAttenuation);
input_buffer_.resize(std::size_t(number_of_taps));
input_buffer_depth_ = 0;
// Pick the new conversion function.
if( filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second &&
filter_parameters.high_frequency_cutoff < 0.0) {
// If input and output rates exactly match, and no additional cut-off has been specified,
// just accumulate results and pass on.
conversion_ = Conversion::Copy;
} else if( filter_parameters.input_cycles_per_second > filter_parameters.output_cycles_per_second ||
(filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second && filter_parameters.high_frequency_cutoff >= 0.0)) {
// If the output rate is less than the input rate, or an additional cut-off has been specified, use the filter.
conversion_ = Conversion::ResampleSmaller;
} else {
conversion_ = Conversion::ResampleLarger;
}
// Do something sensible with any dangling input, if necessary.
switch(conversion_) {
// Neither direct copying nor resampling larger currently use any temporary input.
// Although in the latter case that's just because it's unimplemented. But, regardless,
// that means nothing to do.
default: break;
case Conversion::ResampleSmaller:
// Reize the input buffer only if absolutely necessary; if sizing downward
// such that a sample would otherwise be lost then output it now. Keep anything
// currently in the input buffer that hasn't yet been processed.
if(input_buffer_.size() != size_t(number_of_taps)) {
if(input_buffer_depth_ >= size_t(number_of_taps)) {
resample_input_buffer();
input_buffer_depth_ %= size_t(number_of_taps);
}
input_buffer_.resize(size_t(number_of_taps));
}
break;
}
}
inline void resample_input_buffer() {
output_buffer_[output_buffer_pointer_] = filter_->apply(input_buffer_.data());
output_buffer_pointer_++;
// Announce to delegate if full.
if(output_buffer_pointer_ == output_buffer_.size()) {
output_buffer_pointer_ = 0;
did_complete_samples(this, output_buffer_);
}
// If the next loop around is going to reuse some of the samples just collected, use a memmove to
// preserve them in the correct locations (TODO: use a longer buffer to fix that?) and don't skip
// anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse.
const auto steps = stepper_->step();
if(steps < input_buffer_.size()) {
auto *const input_buffer = input_buffer_.data();
std::memmove( input_buffer,
&input_buffer[steps],
sizeof(int16_t) * (input_buffer_.size() - steps));
input_buffer_depth_ -= steps;
} else {
if(steps > input_buffer_.size())
sample_source_.skip_samples(steps - input_buffer_.size());
input_buffer_depth_ = 0;
}
}
};