From 424e3e8fc73792f2e213b46507cf16cd96a6f83e Mon Sep 17 00:00:00 2001 From: toorusr <33150948+toorusr@users.noreply.github.com> Date: Sun, 7 Sep 2025 01:17:08 +0200 Subject: [PATCH 1/2] Instant creak response + jitter suppression; advanced settings toggle --- .gitignore | 3 +- LidAngleSensor/AppDelegate.m | 312 ++++++++++++++++++++++-------- LidAngleSensor/CreakAudioEngine.h | 11 ++ LidAngleSensor/CreakAudioEngine.m | 155 +++++++++++++-- 4 files changed, 383 insertions(+), 98 deletions(-) diff --git a/.gitignore b/.gitignore index 796b96d..8696e49 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/build +# Build artifacts +build/ diff --git a/LidAngleSensor/AppDelegate.m b/LidAngleSensor/AppDelegate.m index f4a69de..ce051de 100644 --- a/LidAngleSensor/AppDelegate.m +++ b/LidAngleSensor/AppDelegate.m @@ -29,6 +29,20 @@ @interface AppDelegate () @property (strong, nonatomic) NSLabel *modeLabel; @property (strong, nonatomic) NSTimer *updateTimer; @property (nonatomic, assign) AudioMode currentAudioMode; +// Jitter UI controls +@property (strong, nonatomic) NSButton *advancedToggleButton; +@property (strong, nonatomic) NSView *advancedContainer; +@property (strong, nonatomic) NSLayoutConstraint *advancedContainerHeightConstraint; +@property (strong, nonatomic) NSButton *jitterToggleButton; +@property (strong, nonatomic) NSLabel *jitterHeaderLabel; +@property (strong, nonatomic) NSSlider *amplitudeSlider; +@property (strong, nonatomic) NSLabel *amplitudeLabel; +@property (strong, nonatomic) NSSlider *timeWindowSlider; +@property (strong, nonatomic) NSLabel *timeWindowLabel; +@property (strong, nonatomic) NSSlider *minDeltaSlider; +@property (strong, nonatomic) NSLabel *minDeltaLabel; +@property (strong, nonatomic) NSSlider *signFlipsSlider; +@property (strong, nonatomic) NSLabel *signFlipsLabel; @end @implementation AppDelegate @@ -53,47 +67,45 @@ - (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app { } - (void)createWindow { - // Create the main window (taller to accommodate mode selection and audio controls) - NSRect windowFrame = NSMakeRect(100, 100, 450, 480); + // Create the main window (taller to accommodate mode + jitter controls) + NSRect windowFrame = NSMakeRect(100, 100, 500, 650); self.window = [[NSWindow alloc] initWithContentRect:windowFrame - styleMask:NSWindowStyleMaskTitled | - NSWindowStyleMaskClosable | + styleMask:NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable backing:NSBackingStoreBuffered defer:NO]; - [self.window setTitle:@"MacBook Lid Angle Sensor"]; [self.window makeKeyAndOrderFront:nil]; [self.window center]; - - // Create the content view + NSView *contentView = [[NSView alloc] initWithFrame:windowFrame]; [self.window setContentView:contentView]; - - // Create angle display label with tabular numbers (larger, light font) + + // Angle label self.angleLabel = [[NSLabel alloc] init]; [self.angleLabel setStringValue:@"Initializing..."]; [self.angleLabel setFont:[NSFont monospacedDigitSystemFontOfSize:48 weight:NSFontWeightLight]]; [self.angleLabel setAlignment:NSTextAlignmentCenter]; [self.angleLabel setTextColor:[NSColor systemBlueColor]]; [contentView addSubview:self.angleLabel]; - - // Create velocity display label with tabular numbers + + // Velocity label self.velocityLabel = [[NSLabel alloc] init]; [self.velocityLabel setStringValue:@"Velocity: 00 deg/s"]; [self.velocityLabel setFont:[NSFont monospacedDigitSystemFontOfSize:14 weight:NSFontWeightRegular]]; [self.velocityLabel setAlignment:NSTextAlignmentCenter]; [contentView addSubview:self.velocityLabel]; - - // Create status label + + // Status label self.statusLabel = [[NSLabel alloc] init]; [self.statusLabel setStringValue:@"Detecting sensor..."]; [self.statusLabel setFont:[NSFont systemFontOfSize:14]]; [self.statusLabel setAlignment:NSTextAlignmentCenter]; [self.statusLabel setTextColor:[NSColor secondaryLabelColor]]; [contentView addSubview:self.statusLabel]; - - // Create audio toggle button + + // Audio toggle self.audioToggleButton = [[NSButton alloc] init]; [self.audioToggleButton setTitle:@"Start Audio"]; [self.audioToggleButton setBezelStyle:NSBezelStyleRounded]; @@ -101,8 +113,8 @@ - (void)createWindow { [self.audioToggleButton setAction:@selector(toggleAudio:)]; [self.audioToggleButton setTranslatesAutoresizingMaskIntoConstraints:NO]; [contentView addSubview:self.audioToggleButton]; - - // Create audio status label + + // Audio status self.audioStatusLabel = [[NSLabel alloc] init]; [self.audioStatusLabel setStringValue:@""]; [self.audioStatusLabel setFont:[NSFont systemFontOfSize:14]]; @@ -130,30 +142,98 @@ - (void)createWindow { [contentView addSubview:self.modeSelector]; // Set up auto layout constraints + + // Advanced settings toggle + self.advancedToggleButton = [[NSButton alloc] init]; + [self.advancedToggleButton setButtonType:NSSwitchButton]; + [self.advancedToggleButton setTitle:@"Advanced Creak Settings"]; + [self.advancedToggleButton setTarget:self]; + [self.advancedToggleButton setAction:@selector(toggleAdvanced:)]; + [self.advancedToggleButton setTranslatesAutoresizingMaskIntoConstraints:NO]; + [contentView addSubview:self.advancedToggleButton]; + + // Advanced container + self.advancedContainer = [[NSView alloc] initWithFrame:NSZeroRect]; + [self.advancedContainer setTranslatesAutoresizingMaskIntoConstraints:NO]; + [contentView addSubview:self.advancedContainer]; + + // Jitter controls header + self.jitterHeaderLabel = [[NSLabel alloc] init]; + [self.jitterHeaderLabel setStringValue:@"Jitter Filter Controls"]; + [self.jitterHeaderLabel setFont:[NSFont systemFontOfSize:15 weight:NSFontWeightSemibold]]; + [self.jitterHeaderLabel setAlignment:NSTextAlignmentCenter]; + [self.advancedContainer addSubview:self.jitterHeaderLabel]; + + // Jitter toggle + self.jitterToggleButton = [[NSButton alloc] init]; + [self.jitterToggleButton setButtonType:NSSwitchButton]; + [self.jitterToggleButton setTitle:@"Enable Jitter Filter"]; + [self.jitterToggleButton setTarget:self]; + [self.jitterToggleButton setAction:@selector(toggleJitterFilter:)]; + [self.jitterToggleButton setTranslatesAutoresizingMaskIntoConstraints:NO]; + [self.advancedContainer addSubview:self.jitterToggleButton]; + + // Amplitude slider + label + self.amplitudeLabel = [[NSLabel alloc] init]; + [self.amplitudeLabel setStringValue:@"Amplitude (deg): —"]; + [self.amplitudeLabel setAlignment:NSTextAlignmentCenter]; + [self.advancedContainer addSubview:self.amplitudeLabel]; + self.amplitudeSlider = [NSSlider sliderWithValue:8.5 minValue:2.0 maxValue:20.0 target:self action:@selector(amplitudeChanged:)]; + [self.amplitudeSlider setTranslatesAutoresizingMaskIntoConstraints:NO]; + [self.advancedContainer addSubview:self.amplitudeSlider]; + + // Time window slider + label + self.timeWindowLabel = [[NSLabel alloc] init]; + [self.timeWindowLabel setStringValue:@"Time Window (ms): —"]; + [self.timeWindowLabel setAlignment:NSTextAlignmentCenter]; + [self.advancedContainer addSubview:self.timeWindowLabel]; + self.timeWindowSlider = [NSSlider sliderWithValue:120 minValue:40 maxValue:250 target:self action:@selector(timeWindowChanged:)]; + [self.timeWindowSlider setTranslatesAutoresizingMaskIntoConstraints:NO]; + [self.advancedContainer addSubview:self.timeWindowSlider]; + + // Min delta slider + label + self.minDeltaLabel = [[NSLabel alloc] init]; + [self.minDeltaLabel setStringValue:@"Min Delta (deg): —"]; + [self.minDeltaLabel setAlignment:NSTextAlignmentCenter]; + [self.advancedContainer addSubview:self.minDeltaLabel]; + self.minDeltaSlider = [NSSlider sliderWithValue:0.3 minValue:0.05 maxValue:1.5 target:self action:@selector(minDeltaChanged:)]; + [self.minDeltaSlider setTranslatesAutoresizingMaskIntoConstraints:NO]; + [self.advancedContainer addSubview:self.minDeltaSlider]; + + // Sign flips slider + label + self.signFlipsLabel = [[NSLabel alloc] init]; + [self.signFlipsLabel setStringValue:@"Required Alternations: —"]; + [self.signFlipsLabel setAlignment:NSTextAlignmentCenter]; + [self.advancedContainer addSubview:self.signFlipsLabel]; + self.signFlipsSlider = [NSSlider sliderWithValue:2 minValue:1 maxValue:5 target:self action:@selector(signFlipsChanged:)]; + self.signFlipsSlider.numberOfTickMarks = 5; + self.signFlipsSlider.allowsTickMarkValuesOnly = YES; + [self.signFlipsSlider setTranslatesAutoresizingMaskIntoConstraints:NO]; + [self.advancedContainer addSubview:self.signFlipsSlider]; + + // Constraints [NSLayoutConstraint activateConstraints:@[ - // Angle label (main display, now at top) + // Angle label [self.angleLabel.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:40], [self.angleLabel.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor], [self.angleLabel.widthAnchor constraintLessThanOrEqualToAnchor:contentView.widthAnchor constant:-40], - + // Velocity label [self.velocityLabel.topAnchor constraintEqualToAnchor:self.angleLabel.bottomAnchor constant:15], [self.velocityLabel.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor], [self.velocityLabel.widthAnchor constraintLessThanOrEqualToAnchor:contentView.widthAnchor constant:-40], - + // Status label [self.statusLabel.topAnchor constraintEqualToAnchor:self.velocityLabel.bottomAnchor constant:15], [self.statusLabel.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor], [self.statusLabel.widthAnchor constraintLessThanOrEqualToAnchor:contentView.widthAnchor constant:-40], - - // Audio toggle button - [self.audioToggleButton.topAnchor constraintEqualToAnchor:self.statusLabel.bottomAnchor constant:25], + + // Audio toggle + [self.audioToggleButton.topAnchor constraintEqualToAnchor:self.statusLabel.bottomAnchor constant:15], [self.audioToggleButton.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor], - [self.audioToggleButton.widthAnchor constraintEqualToConstant:120], - [self.audioToggleButton.heightAnchor constraintEqualToConstant:32], - - // Audio status label - [self.audioStatusLabel.topAnchor constraintEqualToAnchor:self.audioToggleButton.bottomAnchor constant:15], + + // Audio status + [self.audioStatusLabel.topAnchor constraintEqualToAnchor:self.audioToggleButton.bottomAnchor constant:10], [self.audioStatusLabel.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor], [self.audioStatusLabel.widthAnchor constraintLessThanOrEqualToAnchor:contentView.widthAnchor constant:-40], @@ -167,13 +247,67 @@ - (void)createWindow { [self.modeSelector.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor], [self.modeSelector.widthAnchor constraintEqualToConstant:200], [self.modeSelector.heightAnchor constraintEqualToConstant:28], - [self.modeSelector.bottomAnchor constraintLessThanOrEqualToAnchor:contentView.bottomAnchor constant:-20] + + // Advanced toggle (below mode selector) + [self.advancedToggleButton.topAnchor constraintEqualToAnchor:self.modeSelector.bottomAnchor constant:20], + [self.advancedToggleButton.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor], + + // Advanced container anchoring + [self.advancedContainer.topAnchor constraintEqualToAnchor:self.advancedToggleButton.bottomAnchor constant:8], + [self.advancedContainer.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor], + [self.advancedContainer.widthAnchor constraintLessThanOrEqualToAnchor:contentView.widthAnchor constant:-40], + + // Jitter header (inside container) + [self.jitterHeaderLabel.topAnchor constraintEqualToAnchor:self.advancedContainer.topAnchor constant:6], + [self.jitterHeaderLabel.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], + [self.jitterHeaderLabel.widthAnchor constraintLessThanOrEqualToAnchor:self.advancedContainer.widthAnchor], + + // Jitter toggle (inside container) + [self.jitterToggleButton.topAnchor constraintEqualToAnchor:self.jitterHeaderLabel.bottomAnchor constant:8], + [self.jitterToggleButton.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], + + // Amplitude (inside container) + [self.amplitudeLabel.topAnchor constraintEqualToAnchor:self.jitterToggleButton.bottomAnchor constant:12], + [self.amplitudeLabel.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], + [self.amplitudeLabel.widthAnchor constraintLessThanOrEqualToAnchor:self.advancedContainer.widthAnchor], + [self.amplitudeSlider.topAnchor constraintEqualToAnchor:self.amplitudeLabel.bottomAnchor constant:6], + [self.amplitudeSlider.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], + [self.amplitudeSlider.widthAnchor constraintEqualToConstant:320], + + // Time window (inside container) + [self.timeWindowLabel.topAnchor constraintEqualToAnchor:self.amplitudeSlider.bottomAnchor constant:12], + [self.timeWindowLabel.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], + [self.timeWindowLabel.widthAnchor constraintLessThanOrEqualToAnchor:self.advancedContainer.widthAnchor], + [self.timeWindowSlider.topAnchor constraintEqualToAnchor:self.timeWindowLabel.bottomAnchor constant:6], + [self.timeWindowSlider.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], + [self.timeWindowSlider.widthAnchor constraintEqualToConstant:320], + + // Min delta (inside container) + [self.minDeltaLabel.topAnchor constraintEqualToAnchor:self.timeWindowSlider.bottomAnchor constant:12], + [self.minDeltaLabel.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], + [self.minDeltaLabel.widthAnchor constraintLessThanOrEqualToAnchor:self.advancedContainer.widthAnchor], + [self.minDeltaSlider.topAnchor constraintEqualToAnchor:self.minDeltaLabel.bottomAnchor constant:6], + [self.minDeltaSlider.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], + [self.minDeltaSlider.widthAnchor constraintEqualToConstant:320], + + // Sign flips (inside container) + [self.signFlipsLabel.topAnchor constraintEqualToAnchor:self.minDeltaSlider.bottomAnchor constant:12], + [self.signFlipsLabel.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], + [self.signFlipsLabel.widthAnchor constraintLessThanOrEqualToAnchor:self.advancedContainer.widthAnchor], + [self.signFlipsSlider.topAnchor constraintEqualToAnchor:self.signFlipsLabel.bottomAnchor constant:6], + [self.signFlipsSlider.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], + [self.signFlipsSlider.widthAnchor constraintEqualToConstant:320], ]]; + + // Collapse advanced section by default + self.advancedToggleButton.state = NSControlStateValueOff; + self.advancedContainer.hidden = YES; + self.advancedContainerHeightConstraint = [self.advancedContainer.heightAnchor constraintEqualToConstant:0.0]; + self.advancedContainerHeightConstraint.active = YES; } - (void)initializeLidSensor { self.lidSensor = [[LidAngleSensor alloc] init]; - if (self.lidSensor.isAvailable) { [self.statusLabel setStringValue:@"Sensor detected - Reading angle..."]; [self.statusLabel setTextColor:[NSColor systemGreenColor]]; @@ -188,9 +322,18 @@ - (void)initializeLidSensor { - (void)initializeAudioEngines { self.creakAudioEngine = [[CreakAudioEngine alloc] init]; self.thereminAudioEngine = [[ThereminAudioEngine alloc] init]; - if (self.creakAudioEngine && self.thereminAudioEngine) { [self.audioStatusLabel setStringValue:@""]; + // Initialize UI to engine defaults + self.jitterToggleButton.state = self.creakAudioEngine.jitterFilterEnabled ? NSControlStateValueOn : NSControlStateValueOff; + self.amplitudeSlider.doubleValue = self.creakAudioEngine.jitterAmplitudeDeg; + [self.amplitudeLabel setStringValue:[NSString stringWithFormat:@"Amplitude (deg): %.1f", self.creakAudioEngine.jitterAmplitudeDeg]]; + self.timeWindowSlider.doubleValue = self.creakAudioEngine.jitterTimeWindowMs; + [self.timeWindowLabel setStringValue:[NSString stringWithFormat:@"Time Window (ms): %.0f", self.creakAudioEngine.jitterTimeWindowMs]]; + self.minDeltaSlider.doubleValue = self.creakAudioEngine.jitterMinDeltaDeg; + [self.minDeltaLabel setStringValue:[NSString stringWithFormat:@"Min Delta (deg): %.2f", self.creakAudioEngine.jitterMinDeltaDeg]]; + self.signFlipsSlider.integerValue = (NSInteger)self.creakAudioEngine.jitterMinSignFlips; + [self.signFlipsLabel setStringValue:[NSString stringWithFormat:@"Required Alternations: %lu", (unsigned long)self.creakAudioEngine.jitterMinSignFlips]]; } else { [self.audioStatusLabel setStringValue:@"Audio initialization failed"]; [self.audioStatusLabel setTextColor:[NSColor systemRedColor]]; @@ -228,6 +371,12 @@ - (IBAction)modeChanged:(id)sender { // Update mode self.currentAudioMode = newMode; + + // Hide/show advanced creak controls depending on mode + BOOL isCreak = (self.currentAudioMode == AudioModeCreak); + self.advancedToggleButton.hidden = !isCreak; + self.advancedContainer.hidden = !isCreak || (self.advancedToggleButton.state != NSControlStateValueOn); + self.advancedContainerHeightConstraint.active = !isCreak || (self.advancedToggleButton.state != NSControlStateValueOn); // Start new engine if the previous one was running if (wasRunning) { @@ -253,75 +402,70 @@ - (id)currentAudioEngine { } - (void)startUpdatingDisplay { - // Update every 16ms (60Hz) for smooth real-time audio and display updates - self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.016 + // Faster updates (100Hz) to minimize control latency + self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.010 target:self selector:@selector(updateAngleDisplay) userInfo:nil repeats:YES]; + self.updateTimer.tolerance = 0.0; + [[NSRunLoop mainRunLoop] addTimer:self.updateTimer forMode:NSRunLoopCommonModes]; } -- (void)updateAngleDisplay { - if (!self.lidSensor.isAvailable) { - return; - } - - double angle = [self.lidSensor lidAngle]; - - if (angle == -2.0) { +-(void)updateAngleDisplay { + if (!self.lidSensor.isAvailable) return; + double rawAngle = [self.lidSensor lidAngle]; + if (rawAngle == -2.0) { [self.angleLabel setStringValue:@"Read Error"]; [self.angleLabel setTextColor:[NSColor systemOrangeColor]]; [self.statusLabel setStringValue:@"Failed to read sensor data"]; [self.statusLabel setTextColor:[NSColor systemOrangeColor]]; - } else { - [self.angleLabel setStringValue:[NSString stringWithFormat:@"%.1f°", angle]]; - [self.angleLabel setTextColor:[NSColor systemBlueColor]]; - - // Update current audio engine with new angle - id currentEngine = [self currentAudioEngine]; - if (currentEngine) { - [currentEngine updateWithLidAngle:angle]; - - // Update velocity display with leading zero and whole numbers - double velocity = [currentEngine currentVelocity]; - int roundedVelocity = (int)round(velocity); - if (roundedVelocity < 100) { - [self.velocityLabel setStringValue:[NSString stringWithFormat:@"Velocity: %02d deg/s", roundedVelocity]]; + return; + } + + // Update engine first, then display stabilized angle (creak) or raw (theremin) + double displayAngle = rawAngle; + if (self.currentAudioMode == AudioModeCreak) { + if (self.creakAudioEngine) { + [self.creakAudioEngine updateWithLidAngle:rawAngle]; + displayAngle = self.creakAudioEngine.currentStabilizedAngle; + double velocity = self.creakAudioEngine.currentVelocity; + int rv = (int)llround(velocity); + if (rv < 100) { + [self.velocityLabel setStringValue:[NSString stringWithFormat:@"Velocity: %02d deg/s", rv]]; } else { - [self.velocityLabel setStringValue:[NSString stringWithFormat:@"Velocity: %d deg/s", roundedVelocity]]; + [self.velocityLabel setStringValue:[NSString stringWithFormat:@"Velocity: %d deg/s", rv]]; } - - // Show audio parameters when running - if ([currentEngine isEngineRunning]) { - if (self.currentAudioMode == AudioModeCreak) { - double gain = [currentEngine currentGain]; - double rate = [currentEngine currentRate]; - [self.audioStatusLabel setStringValue:[NSString stringWithFormat:@"Gain: %.2f, Rate: %.2f", gain, rate]]; - } else if (self.currentAudioMode == AudioModeTheremin) { - double frequency = [currentEngine currentFrequency]; - double volume = [currentEngine currentVolume]; - [self.audioStatusLabel setStringValue:[NSString stringWithFormat:@"Freq: %.1f Hz, Vol: %.2f", frequency, volume]]; - } + if (self.creakAudioEngine.isEngineRunning) { + [self.audioStatusLabel setStringValue:[NSString stringWithFormat:@"Gain: %.2f, Rate: %.2f", self.creakAudioEngine.currentGain, self.creakAudioEngine.currentRate]]; } } - - // Provide contextual status based on angle - NSString *status; - if (angle < 5.0) { - status = @"Lid is closed"; - } else if (angle < 45.0) { - status = @"Lid slightly open"; - } else if (angle < 90.0) { - status = @"Lid partially open"; - } else if (angle < 120.0) { - status = @"Lid mostly open"; - } else { - status = @"Lid fully open"; + } else { // Theremin + if (self.thereminAudioEngine) { + [self.thereminAudioEngine updateWithLidAngle:rawAngle]; + double velocity = self.thereminAudioEngine.currentVelocity; + int rv = (int)llround(velocity); + if (rv < 100) { + [self.velocityLabel setStringValue:[NSString stringWithFormat:@"Velocity: %02d deg/s", rv]]; + } else { + [self.velocityLabel setStringValue:[NSString stringWithFormat:@"Velocity: %d deg/s", rv]]; + } + if (self.thereminAudioEngine.isEngineRunning) { + [self.audioStatusLabel setStringValue:[NSString stringWithFormat:@"Freq: %.1f Hz, Vol: %.2f", self.thereminAudioEngine.currentFrequency, self.thereminAudioEngine.currentVolume]]; + } } - - [self.statusLabel setStringValue:status]; - [self.statusLabel setTextColor:[NSColor secondaryLabelColor]]; } + [self.angleLabel setStringValue:[NSString stringWithFormat:@"%.1f°", displayAngle]]; + [self.angleLabel setTextColor:[NSColor systemBlueColor]]; + + NSString *status; + if (displayAngle < 5.0) status = @"Lid is closed"; + else if (displayAngle < 45.0) status = @"Lid slightly open"; + else if (displayAngle < 90.0) status = @"Lid partially open"; + else if (displayAngle < 135.0) status = @"Lid mostly open"; + else status = @"Lid fully open"; + [self.statusLabel setStringValue:status]; + [self.statusLabel setTextColor:[NSColor secondaryLabelColor]]; } @end diff --git a/LidAngleSensor/CreakAudioEngine.h b/LidAngleSensor/CreakAudioEngine.h index 3da450e..a13f32d 100644 --- a/LidAngleSensor/CreakAudioEngine.h +++ b/LidAngleSensor/CreakAudioEngine.h @@ -29,6 +29,14 @@ @property (nonatomic, assign, readonly) double currentVelocity; @property (nonatomic, assign, readonly) double currentGain; @property (nonatomic, assign, readonly) double currentRate; +@property (nonatomic, assign, readonly) double currentStabilizedAngle; // Angle after hysteresis filter + +// Jitter filter configuration (live‑tunable) +@property (nonatomic, assign) BOOL jitterFilterEnabled; // Enable/disable jitter suppression +@property (nonatomic, assign) double jitterAmplitudeDeg; // Peak‑to‑peak amplitude threshold (deg) +@property (nonatomic, assign) double jitterTimeWindowMs; // Time window to consider (ms) +@property (nonatomic, assign) double jitterMinDeltaDeg; // Min delta to count a sign flip (deg) +@property (nonatomic, assign) NSUInteger jitterMinSignFlips; // Required alternations in window /** * Initialize the audio engine and load audio files. @@ -59,4 +67,7 @@ */ - (void)setAngularVelocity:(double)velocity; +/** Reset internal jitter history (e.g., when toggling the filter). */ +- (void)resetJitterHistory; + @end diff --git a/LidAngleSensor/CreakAudioEngine.m b/LidAngleSensor/CreakAudioEngine.m index 02cdedd..9aa133e 100644 --- a/LidAngleSensor/CreakAudioEngine.m +++ b/LidAngleSensor/CreakAudioEngine.m @@ -6,9 +6,10 @@ // #import "CreakAudioEngine.h" +#import -// Audio parameter mapping constants -static const double kDeadzone = 1.0; // deg/s - below this: treat as still +// Audio parameter mapping constants (tuned for immediate response) +static const double kDeadzone = 0.10; // deg/s - minimal deadzone to avoid chatter static const double kVelocityFull = 10.0; // deg/s - max creak volume at/under this velocity static const double kVelocityQuiet = 100.0; // deg/s - silent by/over this velocity (fast movement) @@ -16,15 +17,25 @@ static const double kMinRate = 0.80; // Minimum varispeed rate (lower pitch for slow movement) static const double kMaxRate = 1.10; // Maximum varispeed rate (higher pitch for fast movement) -// Smoothing and timing constants -static const double kAngleSmoothingFactor = 0.05; // Heavy smoothing for sensor noise (5% new, 95% old) -static const double kVelocitySmoothingFactor = 0.3; // Moderate smoothing for velocity -static const double kMovementThreshold = 0.5; // Minimum angle change to register as movement (degrees) -static const double kGainRampTimeMs = 50.0; // Gain ramping time constant (milliseconds) -static const double kRateRampTimeMs = 80.0; // Rate ramping time constant (milliseconds) -static const double kMovementTimeoutMs = 50.0; // Time before aggressive velocity decay (milliseconds) -static const double kVelocityDecayFactor = 0.5; // Decay rate when no movement detected -static const double kAdditionalDecayFactor = 0.8; // Additional decay after timeout +// Smoothing and timing constants (reduced for low latency) +static const double kAngleSmoothingFactor = 0.85; // Favor new data heavily for instant reaction +static const double kVelocitySmoothingFactor = 0.9; // Fast velocity update with slight smoothing +static const double kMovementThreshold = 0.05; // Detect very small movements (degrees) +static const double kGainRampTimeMs = 1.0; // Near-instant gain changes +static const double kRateRampTimeMs = 1.0; // Near-instant pitch/tempo changes +static const double kMovementTimeoutMs = 30.0; // Slightly shorter timeout +static const double kVelocityDecayFactor = 0.65; // Mild decay when idle +static const double kAdditionalDecayFactor = 0.85; // Mild extra decay after timeout + +// Rapid flip/jitter suppression (defaults) +// Some sensors can rapidly flip ±4° around a plateau, producing large +// instantaneous velocities but negligible net movement. Detect this +// pattern over a short window and treat it as no movement. +static const NSUInteger kJitterWindowMaxCount = 6; // up to last 6 samples +static const double kDefaultJitterAmplitudeDeg = 8.5; // peak-to-peak ≤ 8.5° (≈±4°) +static const double kDefaultJitterTimeWindowMs = 120.0;// samples within last 120ms +static const double kDefaultJitterMinDeltaDeg = 0.3; // min delta to count a sign flip +static const NSUInteger kDefaultJitterMinSignFlips = 2;// require at least 2 alternations @interface CreakAudioEngine () @@ -49,6 +60,15 @@ @interface CreakAudioEngine () @property (nonatomic, assign) BOOL isFirstUpdate; @property (nonatomic, assign) NSTimeInterval lastMovementTime; +// History for jitter detection +@property (nonatomic, strong) NSMutableArray *angleHistory; +@property (nonatomic, strong) NSMutableArray *timeHistory; + +// Hysteresis/plateau stabilizer +@property (nonatomic, assign) double stabilizedAngle; +@property (nonatomic, assign) BOOL hasStabilizedAngle; +@property (nonatomic, assign) NSTimeInterval hysteresisOutsideStart; + @end @implementation CreakAudioEngine @@ -66,6 +86,18 @@ - (instancetype)init { _targetRate = 1.0; _currentGain = 0.0; _currentRate = 1.0; + _angleHistory = [NSMutableArray arrayWithCapacity:kJitterWindowMaxCount]; + _timeHistory = [NSMutableArray arrayWithCapacity:kJitterWindowMaxCount]; + _hasStabilizedAngle = NO; + _stabilizedAngle = 0.0; + _hysteresisOutsideStart = 0.0; + + // Jitter defaults + _jitterFilterEnabled = YES; + _jitterAmplitudeDeg = kDefaultJitterAmplitudeDeg; + _jitterTimeWindowMs = kDefaultJitterTimeWindowMs; + _jitterMinDeltaDeg = kDefaultJitterMinDeltaDeg; + _jitterMinSignFlips = kDefaultJitterMinSignFlips; if (![self setupAudioEngine]) { NSLog(@"[CreakAudioEngine] Failed to setup audio engine"); @@ -84,6 +116,11 @@ - (void)dealloc { [self stopEngine]; } +- (void)resetJitterHistory { + [self.angleHistory removeAllObjects]; + [self.timeHistory removeAllObjects]; +} + #pragma mark - Audio Engine Setup - (BOOL)setupAudioEngine { @@ -195,6 +232,8 @@ - (void)updateWithLidAngle:(double)lidAngle { if (self.isFirstUpdate) { self.lastLidAngle = lidAngle; self.smoothedLidAngle = lidAngle; + self.stabilizedAngle = lidAngle; + self.hasStabilizedAngle = YES; self.lastUpdateTime = currentTime; self.lastMovementTime = currentTime; self.isFirstUpdate = NO; @@ -208,6 +247,93 @@ - (void)updateWithLidAngle:(double)lidAngle { self.lastUpdateTime = currentTime; return; } + + BOOL isJitter = NO; + if (self.jitterFilterEnabled) { + // Maintain short history for jitter detection + [self.angleHistory addObject:@(lidAngle)]; + [self.timeHistory addObject:@(currentTime)]; + // Trim to window by time + double windowStart = currentTime - (self.jitterTimeWindowMs / 1000.0); + while (self.timeHistory.count > 0 && self.timeHistory.firstObject.doubleValue < windowStart) { + [self.angleHistory removeObjectAtIndex:0]; + [self.timeHistory removeObjectAtIndex:0]; + } + // Trim to max count + while (self.angleHistory.count > kJitterWindowMaxCount) { + [self.angleHistory removeObjectAtIndex:0]; + [self.timeHistory removeObjectAtIndex:0]; + } + + // Detect rapid flip jitter pattern and freeze angle if present + if (self.angleHistory.count >= 4) { + double minA = DBL_MAX, maxA = -DBL_MAX; + for (NSNumber *n in self.angleHistory) { + double v = n.doubleValue; + if (v < minA) minA = v; + if (v > maxA) maxA = v; + } + double peakToPeak = maxA - minA; + if (peakToPeak <= self.jitterAmplitudeDeg) { + // Count sign alternations of successive deltas + NSInteger flips = 0; + double prevSign = 0.0; + for (NSUInteger i = 1; i < self.angleHistory.count; i++) { + double d = self.angleHistory[i].doubleValue - self.angleHistory[i-1].doubleValue; + if (fabs(d) < self.jitterMinDeltaDeg) continue; + double s = (d > 0.0) ? 1.0 : -1.0; + if (prevSign == 0.0) { + prevSign = s; + } else if (s != prevSign) { + flips++; + prevSign = s; + } + } + if (flips >= (NSInteger)self.jitterMinSignFlips) { + isJitter = YES; + } + } + } + if (isJitter) { + // Treat as no movement: freeze to last stable (hysteresis) angle + lidAngle = self.hasStabilizedAngle ? self.stabilizedAngle : self.lastLidAngle; + } + } + + // Apply hysteresis/plateau stabilization to suppress ±small back-and-forth around a center + // Schmitt-trigger style with persistence + static const double kHystInnerDeg = 2.0; // within this band -> clamp to stable + static const double kHystOuterDeg = 5.0; // outside this band -> update immediately + static const double kHystPersistMs = 80.0; // otherwise require persistence before updating + + if (!self.hasStabilizedAngle) { + self.stabilizedAngle = lidAngle; + self.hasStabilizedAngle = YES; + } else { + double diff = lidAngle - self.stabilizedAngle; + double ad = fabs(diff); + if (ad <= kHystInnerDeg) { + // Inside inner band: keep stable, reset persistence timer + self.hysteresisOutsideStart = 0.0; + lidAngle = self.stabilizedAngle; + } else if (ad >= kHystOuterDeg) { + // Large change: accept immediately + self.stabilizedAngle = lidAngle; + self.hysteresisOutsideStart = 0.0; + } else { + // Between inner and outer: require persistence + if (self.hysteresisOutsideStart == 0.0) { + self.hysteresisOutsideStart = currentTime; + } + double elapsed = currentTime - self.hysteresisOutsideStart; + if (elapsed >= (kHystPersistMs / 1000.0)) { + self.stabilizedAngle = lidAngle; + self.hysteresisOutsideStart = 0.0; + } else { + lidAngle = self.stabilizedAngle; + } + } + } // Stage 1: Smooth the raw angle input to eliminate sensor jitter self.smoothedLidAngle = (kAngleSmoothingFactor * lidAngle) + @@ -226,7 +352,7 @@ - (void)updateWithLidAngle:(double)lidAngle { } // Stage 3: Apply velocity smoothing and decay - if (instantVelocity > 0.0) { + if (!isJitter && instantVelocity > 0.0) { // Real movement detected - apply moderate smoothing self.smoothedVelocity = (kVelocitySmoothingFactor * instantVelocity) + ((1.0 - kVelocitySmoothingFactor) * self.smoothedVelocity); @@ -325,5 +451,8 @@ - (double)currentRate { return _currentRate; } -@end +- (double)currentStabilizedAngle { + return self.hasStabilizedAngle ? self.stabilizedAngle : self.smoothedLidAngle; +} +@end From fd7afe8ab68c91cf852b33b1d51f8d095bce4f48 Mon Sep 17 00:00:00 2001 From: toorusr <33150948+toorusr@users.noreply.github.com> Date: Sun, 7 Sep 2025 05:22:55 +0200 Subject: [PATCH 2/2] cut: ui elements for jitterness config --- LidAngleSensor/AppDelegate.m | 154 ----------------------------------- 1 file changed, 154 deletions(-) diff --git a/LidAngleSensor/AppDelegate.m b/LidAngleSensor/AppDelegate.m index ce051de..d70ff17 100644 --- a/LidAngleSensor/AppDelegate.m +++ b/LidAngleSensor/AppDelegate.m @@ -29,20 +29,6 @@ @interface AppDelegate () @property (strong, nonatomic) NSLabel *modeLabel; @property (strong, nonatomic) NSTimer *updateTimer; @property (nonatomic, assign) AudioMode currentAudioMode; -// Jitter UI controls -@property (strong, nonatomic) NSButton *advancedToggleButton; -@property (strong, nonatomic) NSView *advancedContainer; -@property (strong, nonatomic) NSLayoutConstraint *advancedContainerHeightConstraint; -@property (strong, nonatomic) NSButton *jitterToggleButton; -@property (strong, nonatomic) NSLabel *jitterHeaderLabel; -@property (strong, nonatomic) NSSlider *amplitudeSlider; -@property (strong, nonatomic) NSLabel *amplitudeLabel; -@property (strong, nonatomic) NSSlider *timeWindowSlider; -@property (strong, nonatomic) NSLabel *timeWindowLabel; -@property (strong, nonatomic) NSSlider *minDeltaSlider; -@property (strong, nonatomic) NSLabel *minDeltaLabel; -@property (strong, nonatomic) NSSlider *signFlipsSlider; -@property (strong, nonatomic) NSLabel *signFlipsLabel; @end @implementation AppDelegate @@ -142,74 +128,6 @@ - (void)createWindow { [contentView addSubview:self.modeSelector]; // Set up auto layout constraints - - // Advanced settings toggle - self.advancedToggleButton = [[NSButton alloc] init]; - [self.advancedToggleButton setButtonType:NSSwitchButton]; - [self.advancedToggleButton setTitle:@"Advanced Creak Settings"]; - [self.advancedToggleButton setTarget:self]; - [self.advancedToggleButton setAction:@selector(toggleAdvanced:)]; - [self.advancedToggleButton setTranslatesAutoresizingMaskIntoConstraints:NO]; - [contentView addSubview:self.advancedToggleButton]; - - // Advanced container - self.advancedContainer = [[NSView alloc] initWithFrame:NSZeroRect]; - [self.advancedContainer setTranslatesAutoresizingMaskIntoConstraints:NO]; - [contentView addSubview:self.advancedContainer]; - - // Jitter controls header - self.jitterHeaderLabel = [[NSLabel alloc] init]; - [self.jitterHeaderLabel setStringValue:@"Jitter Filter Controls"]; - [self.jitterHeaderLabel setFont:[NSFont systemFontOfSize:15 weight:NSFontWeightSemibold]]; - [self.jitterHeaderLabel setAlignment:NSTextAlignmentCenter]; - [self.advancedContainer addSubview:self.jitterHeaderLabel]; - - // Jitter toggle - self.jitterToggleButton = [[NSButton alloc] init]; - [self.jitterToggleButton setButtonType:NSSwitchButton]; - [self.jitterToggleButton setTitle:@"Enable Jitter Filter"]; - [self.jitterToggleButton setTarget:self]; - [self.jitterToggleButton setAction:@selector(toggleJitterFilter:)]; - [self.jitterToggleButton setTranslatesAutoresizingMaskIntoConstraints:NO]; - [self.advancedContainer addSubview:self.jitterToggleButton]; - - // Amplitude slider + label - self.amplitudeLabel = [[NSLabel alloc] init]; - [self.amplitudeLabel setStringValue:@"Amplitude (deg): —"]; - [self.amplitudeLabel setAlignment:NSTextAlignmentCenter]; - [self.advancedContainer addSubview:self.amplitudeLabel]; - self.amplitudeSlider = [NSSlider sliderWithValue:8.5 minValue:2.0 maxValue:20.0 target:self action:@selector(amplitudeChanged:)]; - [self.amplitudeSlider setTranslatesAutoresizingMaskIntoConstraints:NO]; - [self.advancedContainer addSubview:self.amplitudeSlider]; - - // Time window slider + label - self.timeWindowLabel = [[NSLabel alloc] init]; - [self.timeWindowLabel setStringValue:@"Time Window (ms): —"]; - [self.timeWindowLabel setAlignment:NSTextAlignmentCenter]; - [self.advancedContainer addSubview:self.timeWindowLabel]; - self.timeWindowSlider = [NSSlider sliderWithValue:120 minValue:40 maxValue:250 target:self action:@selector(timeWindowChanged:)]; - [self.timeWindowSlider setTranslatesAutoresizingMaskIntoConstraints:NO]; - [self.advancedContainer addSubview:self.timeWindowSlider]; - - // Min delta slider + label - self.minDeltaLabel = [[NSLabel alloc] init]; - [self.minDeltaLabel setStringValue:@"Min Delta (deg): —"]; - [self.minDeltaLabel setAlignment:NSTextAlignmentCenter]; - [self.advancedContainer addSubview:self.minDeltaLabel]; - self.minDeltaSlider = [NSSlider sliderWithValue:0.3 minValue:0.05 maxValue:1.5 target:self action:@selector(minDeltaChanged:)]; - [self.minDeltaSlider setTranslatesAutoresizingMaskIntoConstraints:NO]; - [self.advancedContainer addSubview:self.minDeltaSlider]; - - // Sign flips slider + label - self.signFlipsLabel = [[NSLabel alloc] init]; - [self.signFlipsLabel setStringValue:@"Required Alternations: —"]; - [self.signFlipsLabel setAlignment:NSTextAlignmentCenter]; - [self.advancedContainer addSubview:self.signFlipsLabel]; - self.signFlipsSlider = [NSSlider sliderWithValue:2 minValue:1 maxValue:5 target:self action:@selector(signFlipsChanged:)]; - self.signFlipsSlider.numberOfTickMarks = 5; - self.signFlipsSlider.allowsTickMarkValuesOnly = YES; - [self.signFlipsSlider setTranslatesAutoresizingMaskIntoConstraints:NO]; - [self.advancedContainer addSubview:self.signFlipsSlider]; // Constraints [NSLayoutConstraint activateConstraints:@[ @@ -247,63 +165,7 @@ - (void)createWindow { [self.modeSelector.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor], [self.modeSelector.widthAnchor constraintEqualToConstant:200], [self.modeSelector.heightAnchor constraintEqualToConstant:28], - - // Advanced toggle (below mode selector) - [self.advancedToggleButton.topAnchor constraintEqualToAnchor:self.modeSelector.bottomAnchor constant:20], - [self.advancedToggleButton.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor], - - // Advanced container anchoring - [self.advancedContainer.topAnchor constraintEqualToAnchor:self.advancedToggleButton.bottomAnchor constant:8], - [self.advancedContainer.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor], - [self.advancedContainer.widthAnchor constraintLessThanOrEqualToAnchor:contentView.widthAnchor constant:-40], - - // Jitter header (inside container) - [self.jitterHeaderLabel.topAnchor constraintEqualToAnchor:self.advancedContainer.topAnchor constant:6], - [self.jitterHeaderLabel.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], - [self.jitterHeaderLabel.widthAnchor constraintLessThanOrEqualToAnchor:self.advancedContainer.widthAnchor], - - // Jitter toggle (inside container) - [self.jitterToggleButton.topAnchor constraintEqualToAnchor:self.jitterHeaderLabel.bottomAnchor constant:8], - [self.jitterToggleButton.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], - - // Amplitude (inside container) - [self.amplitudeLabel.topAnchor constraintEqualToAnchor:self.jitterToggleButton.bottomAnchor constant:12], - [self.amplitudeLabel.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], - [self.amplitudeLabel.widthAnchor constraintLessThanOrEqualToAnchor:self.advancedContainer.widthAnchor], - [self.amplitudeSlider.topAnchor constraintEqualToAnchor:self.amplitudeLabel.bottomAnchor constant:6], - [self.amplitudeSlider.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], - [self.amplitudeSlider.widthAnchor constraintEqualToConstant:320], - - // Time window (inside container) - [self.timeWindowLabel.topAnchor constraintEqualToAnchor:self.amplitudeSlider.bottomAnchor constant:12], - [self.timeWindowLabel.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], - [self.timeWindowLabel.widthAnchor constraintLessThanOrEqualToAnchor:self.advancedContainer.widthAnchor], - [self.timeWindowSlider.topAnchor constraintEqualToAnchor:self.timeWindowLabel.bottomAnchor constant:6], - [self.timeWindowSlider.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], - [self.timeWindowSlider.widthAnchor constraintEqualToConstant:320], - - // Min delta (inside container) - [self.minDeltaLabel.topAnchor constraintEqualToAnchor:self.timeWindowSlider.bottomAnchor constant:12], - [self.minDeltaLabel.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], - [self.minDeltaLabel.widthAnchor constraintLessThanOrEqualToAnchor:self.advancedContainer.widthAnchor], - [self.minDeltaSlider.topAnchor constraintEqualToAnchor:self.minDeltaLabel.bottomAnchor constant:6], - [self.minDeltaSlider.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], - [self.minDeltaSlider.widthAnchor constraintEqualToConstant:320], - - // Sign flips (inside container) - [self.signFlipsLabel.topAnchor constraintEqualToAnchor:self.minDeltaSlider.bottomAnchor constant:12], - [self.signFlipsLabel.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], - [self.signFlipsLabel.widthAnchor constraintLessThanOrEqualToAnchor:self.advancedContainer.widthAnchor], - [self.signFlipsSlider.topAnchor constraintEqualToAnchor:self.signFlipsLabel.bottomAnchor constant:6], - [self.signFlipsSlider.centerXAnchor constraintEqualToAnchor:self.advancedContainer.centerXAnchor], - [self.signFlipsSlider.widthAnchor constraintEqualToConstant:320], ]]; - - // Collapse advanced section by default - self.advancedToggleButton.state = NSControlStateValueOff; - self.advancedContainer.hidden = YES; - self.advancedContainerHeightConstraint = [self.advancedContainer.heightAnchor constraintEqualToConstant:0.0]; - self.advancedContainerHeightConstraint.active = YES; } - (void)initializeLidSensor { @@ -324,16 +186,6 @@ - (void)initializeAudioEngines { self.thereminAudioEngine = [[ThereminAudioEngine alloc] init]; if (self.creakAudioEngine && self.thereminAudioEngine) { [self.audioStatusLabel setStringValue:@""]; - // Initialize UI to engine defaults - self.jitterToggleButton.state = self.creakAudioEngine.jitterFilterEnabled ? NSControlStateValueOn : NSControlStateValueOff; - self.amplitudeSlider.doubleValue = self.creakAudioEngine.jitterAmplitudeDeg; - [self.amplitudeLabel setStringValue:[NSString stringWithFormat:@"Amplitude (deg): %.1f", self.creakAudioEngine.jitterAmplitudeDeg]]; - self.timeWindowSlider.doubleValue = self.creakAudioEngine.jitterTimeWindowMs; - [self.timeWindowLabel setStringValue:[NSString stringWithFormat:@"Time Window (ms): %.0f", self.creakAudioEngine.jitterTimeWindowMs]]; - self.minDeltaSlider.doubleValue = self.creakAudioEngine.jitterMinDeltaDeg; - [self.minDeltaLabel setStringValue:[NSString stringWithFormat:@"Min Delta (deg): %.2f", self.creakAudioEngine.jitterMinDeltaDeg]]; - self.signFlipsSlider.integerValue = (NSInteger)self.creakAudioEngine.jitterMinSignFlips; - [self.signFlipsLabel setStringValue:[NSString stringWithFormat:@"Required Alternations: %lu", (unsigned long)self.creakAudioEngine.jitterMinSignFlips]]; } else { [self.audioStatusLabel setStringValue:@"Audio initialization failed"]; [self.audioStatusLabel setTextColor:[NSColor systemRedColor]]; @@ -371,12 +223,6 @@ - (IBAction)modeChanged:(id)sender { // Update mode self.currentAudioMode = newMode; - - // Hide/show advanced creak controls depending on mode - BOOL isCreak = (self.currentAudioMode == AudioModeCreak); - self.advancedToggleButton.hidden = !isCreak; - self.advancedContainer.hidden = !isCreak || (self.advancedToggleButton.state != NSControlStateValueOn); - self.advancedContainerHeightConstraint.active = !isCreak || (self.advancedToggleButton.state != NSControlStateValueOn); // Start new engine if the previous one was running if (wasRunning) {