Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/build
# Build artifacts
build/
158 changes: 74 additions & 84 deletions LidAngleSensor/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -53,56 +53,54 @@ - (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];
[self.audioToggleButton setTarget:self];
[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]];
Expand Down Expand Up @@ -130,30 +128,30 @@ - (void)createWindow {
[contentView addSubview:self.modeSelector];

// Set up auto layout constraints

// 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],

Expand All @@ -167,13 +165,11 @@ - (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]
]];
}

- (void)initializeLidSensor {
self.lidSensor = [[LidAngleSensor alloc] init];

if (self.lidSensor.isAvailable) {
[self.statusLabel setStringValue:@"Sensor detected - Reading angle..."];
[self.statusLabel setTextColor:[NSColor systemGreenColor]];
Expand All @@ -188,7 +184,6 @@ - (void)initializeLidSensor {
- (void)initializeAudioEngines {
self.creakAudioEngine = [[CreakAudioEngine alloc] init];
self.thereminAudioEngine = [[ThereminAudioEngine alloc] init];

if (self.creakAudioEngine && self.thereminAudioEngine) {
[self.audioStatusLabel setStringValue:@""];
} else {
Expand Down Expand Up @@ -253,75 +248,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
11 changes: 11 additions & 0 deletions LidAngleSensor/CreakAudioEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -59,4 +67,7 @@
*/
- (void)setAngularVelocity:(double)velocity;

/** Reset internal jitter history (e.g., when toggling the filter). */
- (void)resetJitterHistory;

@end
Loading