mirror of
https://github.com/samhenrigold/LidAngleSensor.git
synced 2026-01-12 09:17:43 +03:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8f11f8a58 | ||
|
|
d88e103891 | ||
|
|
3510d76bbe | ||
|
|
3ecf71093f | ||
|
|
7134e72d40 | ||
|
|
9e500b7c4d | ||
|
|
12ac51fe6f | ||
|
|
1752fa12f3 | ||
|
|
d0492d9fa0 | ||
|
|
ff2ed4198a | ||
|
|
580a5f06ef | ||
|
|
2c3f7bb4f6 | ||
|
|
152d12ce90 | ||
|
|
65feeef9b9 | ||
|
|
f6a36a226d | ||
|
|
ce0432c5ef |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
/build
|
/build
|
||||||
|
.DS_Store
|
||||||
|
|||||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 11.5;
|
MACOSX_DEPLOYMENT_TARGET = 11.5;
|
||||||
MARKETING_VERSION = 1.0.2;
|
MARKETING_VERSION = 1.0.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = gold.samhenri.LidAngleSensor;
|
PRODUCT_BUNDLE_IDENTIFIER = gold.samhenri.LidAngleSensor;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
@@ -310,7 +310,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 11.5;
|
MACOSX_DEPLOYMENT_TARGET = 11.5;
|
||||||
MARKETING_VERSION = 1.0.2;
|
MARKETING_VERSION = 1.0.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = gold.samhenri.LidAngleSensor;
|
PRODUCT_BUNDLE_IDENTIFIER = gold.samhenri.LidAngleSensor;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
|
|||||||
@@ -8,32 +8,44 @@
|
|||||||
#import "AppDelegate.h"
|
#import "AppDelegate.h"
|
||||||
#import "LidAngleSensor.h"
|
#import "LidAngleSensor.h"
|
||||||
#import "CreakAudioEngine.h"
|
#import "CreakAudioEngine.h"
|
||||||
|
#import "ThereminAudioEngine.h"
|
||||||
#import "NSLabel.h"
|
#import "NSLabel.h"
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, AudioMode) {
|
||||||
|
AudioModeCreak,
|
||||||
|
AudioModeTheremin
|
||||||
|
};
|
||||||
|
|
||||||
@interface AppDelegate ()
|
@interface AppDelegate ()
|
||||||
@property (strong, nonatomic) LidAngleSensor *lidSensor;
|
@property (strong, nonatomic) LidAngleSensor *lidSensor;
|
||||||
@property (strong, nonatomic) CreakAudioEngine *audioEngine;
|
@property (strong, nonatomic) CreakAudioEngine *creakAudioEngine;
|
||||||
|
@property (strong, nonatomic) ThereminAudioEngine *thereminAudioEngine;
|
||||||
@property (strong, nonatomic) NSLabel *angleLabel;
|
@property (strong, nonatomic) NSLabel *angleLabel;
|
||||||
@property (strong, nonatomic) NSLabel *statusLabel;
|
@property (strong, nonatomic) NSLabel *statusLabel;
|
||||||
@property (strong, nonatomic) NSLabel *velocityLabel;
|
@property (strong, nonatomic) NSLabel *velocityLabel;
|
||||||
@property (strong, nonatomic) NSLabel *audioStatusLabel;
|
@property (strong, nonatomic) NSLabel *audioStatusLabel;
|
||||||
@property (strong, nonatomic) NSButton *audioToggleButton;
|
@property (strong, nonatomic) NSButton *audioToggleButton;
|
||||||
|
@property (strong, nonatomic) NSSegmentedControl *modeSelector;
|
||||||
|
@property (strong, nonatomic) NSLabel *modeLabel;
|
||||||
@property (strong, nonatomic) NSTimer *updateTimer;
|
@property (strong, nonatomic) NSTimer *updateTimer;
|
||||||
|
@property (nonatomic, assign) AudioMode currentAudioMode;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation AppDelegate
|
@implementation AppDelegate
|
||||||
|
|
||||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||||
|
self.currentAudioMode = AudioModeCreak; // Default to creak mode
|
||||||
[self createWindow];
|
[self createWindow];
|
||||||
[self initializeLidSensor];
|
[self initializeLidSensor];
|
||||||
[self initializeAudioEngine];
|
[self initializeAudioEngines];
|
||||||
[self startUpdatingDisplay];
|
[self startUpdatingDisplay];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)applicationWillTerminate:(NSNotification *)aNotification {
|
- (void)applicationWillTerminate:(NSNotification *)aNotification {
|
||||||
[self.updateTimer invalidate];
|
[self.updateTimer invalidate];
|
||||||
[self.lidSensor stopLidAngleUpdates];
|
[self.lidSensor stopLidAngleUpdates];
|
||||||
[self.audioEngine stopEngine];
|
[self.creakAudioEngine stopEngine];
|
||||||
|
[self.thereminAudioEngine stopEngine];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app {
|
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app {
|
||||||
@@ -41,8 +53,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)createWindow {
|
- (void)createWindow {
|
||||||
// Create the main window (taller to accommodate audio controls)
|
// Create the main window (taller to accommodate mode selection and audio controls)
|
||||||
NSRect windowFrame = NSMakeRect(100, 100, 450, 420);
|
NSRect windowFrame = NSMakeRect(100, 100, 450, 480);
|
||||||
self.window = [[NSWindow alloc] initWithContentRect:windowFrame
|
self.window = [[NSWindow alloc] initWithContentRect:windowFrame
|
||||||
styleMask:NSWindowStyleMaskTitled |
|
styleMask:NSWindowStyleMaskTitled |
|
||||||
NSWindowStyleMaskClosable |
|
NSWindowStyleMaskClosable |
|
||||||
@@ -98,6 +110,25 @@
|
|||||||
[self.audioStatusLabel setTextColor:[NSColor secondaryLabelColor]];
|
[self.audioStatusLabel setTextColor:[NSColor secondaryLabelColor]];
|
||||||
[contentView addSubview:self.audioStatusLabel];
|
[contentView addSubview:self.audioStatusLabel];
|
||||||
|
|
||||||
|
// Create mode label
|
||||||
|
self.modeLabel = [[NSLabel alloc] init];
|
||||||
|
[self.modeLabel setStringValue:@"Audio Mode:"];
|
||||||
|
[self.modeLabel setFont:[NSFont systemFontOfSize:14 weight:NSFontWeightMedium]];
|
||||||
|
[self.modeLabel setAlignment:NSTextAlignmentCenter];
|
||||||
|
[self.modeLabel setTextColor:[NSColor labelColor]];
|
||||||
|
[contentView addSubview:self.modeLabel];
|
||||||
|
|
||||||
|
// Create mode selector
|
||||||
|
self.modeSelector = [[NSSegmentedControl alloc] init];
|
||||||
|
[self.modeSelector setSegmentCount:2];
|
||||||
|
[self.modeSelector setLabel:@"Creak" forSegment:0];
|
||||||
|
[self.modeSelector setLabel:@"Theremin" forSegment:1];
|
||||||
|
[self.modeSelector setSelectedSegment:0]; // Default to creak
|
||||||
|
[self.modeSelector setTarget:self];
|
||||||
|
[self.modeSelector setAction:@selector(modeChanged:)];
|
||||||
|
[self.modeSelector setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||||
|
[contentView addSubview:self.modeSelector];
|
||||||
|
|
||||||
// Set up auto layout constraints
|
// Set up auto layout constraints
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
// Angle label (main display, now at top)
|
// Angle label (main display, now at top)
|
||||||
@@ -125,7 +156,18 @@
|
|||||||
[self.audioStatusLabel.topAnchor constraintEqualToAnchor:self.audioToggleButton.bottomAnchor constant:15],
|
[self.audioStatusLabel.topAnchor constraintEqualToAnchor:self.audioToggleButton.bottomAnchor constant:15],
|
||||||
[self.audioStatusLabel.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor],
|
[self.audioStatusLabel.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor],
|
||||||
[self.audioStatusLabel.widthAnchor constraintLessThanOrEqualToAnchor:contentView.widthAnchor constant:-40],
|
[self.audioStatusLabel.widthAnchor constraintLessThanOrEqualToAnchor:contentView.widthAnchor constant:-40],
|
||||||
[self.audioStatusLabel.bottomAnchor constraintLessThanOrEqualToAnchor:contentView.bottomAnchor constant:-20]
|
|
||||||
|
// Mode label
|
||||||
|
[self.modeLabel.topAnchor constraintEqualToAnchor:self.audioStatusLabel.bottomAnchor constant:25],
|
||||||
|
[self.modeLabel.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor],
|
||||||
|
[self.modeLabel.widthAnchor constraintLessThanOrEqualToAnchor:contentView.widthAnchor constant:-40],
|
||||||
|
|
||||||
|
// Mode selector
|
||||||
|
[self.modeSelector.topAnchor constraintEqualToAnchor:self.modeLabel.bottomAnchor constant:10],
|
||||||
|
[self.modeSelector.centerXAnchor constraintEqualToAnchor:contentView.centerXAnchor],
|
||||||
|
[self.modeSelector.widthAnchor constraintEqualToConstant:200],
|
||||||
|
[self.modeSelector.heightAnchor constraintEqualToConstant:28],
|
||||||
|
[self.modeSelector.bottomAnchor constraintLessThanOrEqualToAnchor:contentView.bottomAnchor constant:-20]
|
||||||
]];
|
]];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,10 +185,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)initializeAudioEngine {
|
- (void)initializeAudioEngines {
|
||||||
self.audioEngine = [[CreakAudioEngine alloc] init];
|
self.creakAudioEngine = [[CreakAudioEngine alloc] init];
|
||||||
|
self.thereminAudioEngine = [[ThereminAudioEngine alloc] init];
|
||||||
|
|
||||||
if (self.audioEngine) {
|
if (self.creakAudioEngine && self.thereminAudioEngine) {
|
||||||
[self.audioStatusLabel setStringValue:@""];
|
[self.audioStatusLabel setStringValue:@""];
|
||||||
} else {
|
} else {
|
||||||
[self.audioStatusLabel setStringValue:@"Audio initialization failed"];
|
[self.audioStatusLabel setStringValue:@"Audio initialization failed"];
|
||||||
@@ -156,21 +199,59 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (IBAction)toggleAudio:(id)sender {
|
- (IBAction)toggleAudio:(id)sender {
|
||||||
if (!self.audioEngine) {
|
id currentEngine = [self currentAudioEngine];
|
||||||
|
if (!currentEngine) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.audioEngine.isEngineRunning) {
|
if ([currentEngine isEngineRunning]) {
|
||||||
[self.audioEngine stopEngine];
|
[currentEngine stopEngine];
|
||||||
[self.audioToggleButton setTitle:@"Start Audio"];
|
[self.audioToggleButton setTitle:@"Start Audio"];
|
||||||
[self.audioStatusLabel setStringValue:@""];
|
[self.audioStatusLabel setStringValue:@""];
|
||||||
} else {
|
} else {
|
||||||
[self.audioEngine startEngine];
|
[currentEngine startEngine];
|
||||||
[self.audioToggleButton setTitle:@"Stop Audio"];
|
[self.audioToggleButton setTitle:@"Stop Audio"];
|
||||||
[self.audioStatusLabel setStringValue:@""];
|
[self.audioStatusLabel setStringValue:@""];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (IBAction)modeChanged:(id)sender {
|
||||||
|
NSSegmentedControl *control = (NSSegmentedControl *)sender;
|
||||||
|
AudioMode newMode = (AudioMode)control.selectedSegment;
|
||||||
|
|
||||||
|
// Stop current engine if running
|
||||||
|
id currentEngine = [self currentAudioEngine];
|
||||||
|
BOOL wasRunning = [currentEngine isEngineRunning];
|
||||||
|
if (wasRunning) {
|
||||||
|
[currentEngine stopEngine];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update mode
|
||||||
|
self.currentAudioMode = newMode;
|
||||||
|
|
||||||
|
// Start new engine if the previous one was running
|
||||||
|
if (wasRunning) {
|
||||||
|
id newEngine = [self currentAudioEngine];
|
||||||
|
[newEngine startEngine];
|
||||||
|
[self.audioToggleButton setTitle:@"Stop Audio"];
|
||||||
|
} else {
|
||||||
|
[self.audioToggleButton setTitle:@"Start Audio"];
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.audioStatusLabel setStringValue:@""];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id)currentAudioEngine {
|
||||||
|
switch (self.currentAudioMode) {
|
||||||
|
case AudioModeCreak:
|
||||||
|
return self.creakAudioEngine;
|
||||||
|
case AudioModeTheremin:
|
||||||
|
return self.thereminAudioEngine;
|
||||||
|
default:
|
||||||
|
return self.creakAudioEngine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- (void)startUpdatingDisplay {
|
- (void)startUpdatingDisplay {
|
||||||
// Update every 16ms (60Hz) for smooth real-time audio and display updates
|
// Update every 16ms (60Hz) for smooth real-time audio and display updates
|
||||||
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.016
|
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.016
|
||||||
@@ -196,12 +277,13 @@
|
|||||||
[self.angleLabel setStringValue:[NSString stringWithFormat:@"%.1f°", angle]];
|
[self.angleLabel setStringValue:[NSString stringWithFormat:@"%.1f°", angle]];
|
||||||
[self.angleLabel setTextColor:[NSColor systemBlueColor]];
|
[self.angleLabel setTextColor:[NSColor systemBlueColor]];
|
||||||
|
|
||||||
// Update audio engine with new angle
|
// Update current audio engine with new angle
|
||||||
if (self.audioEngine) {
|
id currentEngine = [self currentAudioEngine];
|
||||||
[self.audioEngine updateWithLidAngle:angle];
|
if (currentEngine) {
|
||||||
|
[currentEngine updateWithLidAngle:angle];
|
||||||
|
|
||||||
// Update velocity display with leading zero and whole numbers
|
// Update velocity display with leading zero and whole numbers
|
||||||
double velocity = self.audioEngine.currentVelocity;
|
double velocity = [currentEngine currentVelocity];
|
||||||
int roundedVelocity = (int)round(velocity);
|
int roundedVelocity = (int)round(velocity);
|
||||||
if (roundedVelocity < 100) {
|
if (roundedVelocity < 100) {
|
||||||
[self.velocityLabel setStringValue:[NSString stringWithFormat:@"Velocity: %02d deg/s", roundedVelocity]];
|
[self.velocityLabel setStringValue:[NSString stringWithFormat:@"Velocity: %02d deg/s", roundedVelocity]];
|
||||||
@@ -209,13 +291,17 @@
|
|||||||
[self.velocityLabel setStringValue:[NSString stringWithFormat:@"Velocity: %d deg/s", roundedVelocity]];
|
[self.velocityLabel setStringValue:[NSString stringWithFormat:@"Velocity: %d deg/s", roundedVelocity]];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep velocity label color consistent
|
|
||||||
|
|
||||||
// Show audio parameters when running
|
// Show audio parameters when running
|
||||||
if (self.audioEngine.isEngineRunning) {
|
if ([currentEngine isEngineRunning]) {
|
||||||
double gain = self.audioEngine.currentGain;
|
if (self.currentAudioMode == AudioModeCreak) {
|
||||||
double rate = self.audioEngine.currentRate;
|
double gain = [currentEngine currentGain];
|
||||||
|
double rate = [currentEngine currentRate];
|
||||||
[self.audioStatusLabel setStringValue:[NSString stringWithFormat:@"Gain: %.2f, Rate: %.2f", gain, rate]];
|
[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]];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +313,7 @@
|
|||||||
status = @"Lid slightly open";
|
status = @"Lid slightly open";
|
||||||
} else if (angle < 90.0) {
|
} else if (angle < 90.0) {
|
||||||
status = @"Lid partially open";
|
status = @"Lid partially open";
|
||||||
} else if (angle < 135.0) {
|
} else if (angle < 120.0) {
|
||||||
status = @"Lid mostly open";
|
status = @"Lid mostly open";
|
||||||
} else {
|
} else {
|
||||||
status = @"Lid fully open";
|
status = @"Lid fully open";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// CreakAudioEngine.h
|
// CreakAudioEngine.h
|
||||||
// LidAngleSensor
|
// LidAngleSensor
|
||||||
//
|
//
|
||||||
// Created by Sam on 2025-01-16.
|
// Created by Sam on 2025-09-06.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// CreakAudioEngine.m
|
// CreakAudioEngine.m
|
||||||
// LidAngleSensor
|
// LidAngleSensor
|
||||||
//
|
//
|
||||||
// Created by Sam on 2025-01-16.
|
// Created by Sam on 2025-09-06.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "CreakAudioEngine.h"
|
#import "CreakAudioEngine.h"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// NSLabel.h
|
// NSLabel.h
|
||||||
// LidAngleSensor
|
// LidAngleSensor
|
||||||
//
|
//
|
||||||
// Created by Sam on 2025-01-16.
|
// Created by Sam on 2025-09-06.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// NSLabel.m
|
// NSLabel.m
|
||||||
// LidAngleSensor
|
// LidAngleSensor
|
||||||
//
|
//
|
||||||
// Created by Sam on 2025-01-16.
|
// Created by Sam on 2025-09-06.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "NSLabel.h"
|
#import "NSLabel.h"
|
||||||
|
|||||||
62
LidAngleSensor/ThereminAudioEngine.h
Normal file
62
LidAngleSensor/ThereminAudioEngine.h
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
//
|
||||||
|
// ThereminAudioEngine.h
|
||||||
|
// LidAngleSensor
|
||||||
|
//
|
||||||
|
// Created by Sam on 2025-09-06.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <AVFoundation/AVFoundation.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThereminAudioEngine provides real-time theremin-like audio that responds to MacBook lid angle changes.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Real-time sine wave synthesis based on lid angle
|
||||||
|
* - Smooth frequency transitions to avoid audio artifacts
|
||||||
|
* - Volume control based on angular velocity
|
||||||
|
* - Configurable frequency range mapping
|
||||||
|
* - Low-latency audio generation
|
||||||
|
*
|
||||||
|
* Audio Behavior:
|
||||||
|
* - Lid angle maps to frequency (closed = low pitch, open = high pitch)
|
||||||
|
* - Movement velocity controls volume (slow movement = loud, fast = quiet)
|
||||||
|
* - Smooth parameter interpolation for musical quality
|
||||||
|
*/
|
||||||
|
@interface ThereminAudioEngine : NSObject
|
||||||
|
|
||||||
|
@property (nonatomic, assign, readonly) BOOL isEngineRunning;
|
||||||
|
@property (nonatomic, assign, readonly) double currentVelocity;
|
||||||
|
@property (nonatomic, assign, readonly) double currentFrequency;
|
||||||
|
@property (nonatomic, assign, readonly) double currentVolume;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the theremin audio engine.
|
||||||
|
* @return Initialized engine instance, or nil if initialization failed
|
||||||
|
*/
|
||||||
|
- (instancetype)init;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the audio engine and begin tone generation.
|
||||||
|
*/
|
||||||
|
- (void)startEngine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the audio engine and halt tone generation.
|
||||||
|
*/
|
||||||
|
- (void)stopEngine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the theremin audio based on new lid angle measurement.
|
||||||
|
* This method calculates frequency mapping and volume based on movement.
|
||||||
|
* @param lidAngle Current lid angle in degrees
|
||||||
|
*/
|
||||||
|
- (void)updateWithLidAngle:(double)lidAngle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually set the angular velocity (for testing purposes).
|
||||||
|
* @param velocity Angular velocity in degrees per second
|
||||||
|
*/
|
||||||
|
- (void)setAngularVelocity:(double)velocity;
|
||||||
|
|
||||||
|
@end
|
||||||
318
LidAngleSensor/ThereminAudioEngine.m
Normal file
318
LidAngleSensor/ThereminAudioEngine.m
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
//
|
||||||
|
// ThereminAudioEngine.m
|
||||||
|
// LidAngleSensor
|
||||||
|
//
|
||||||
|
// Created by Sam on 2025-09-06.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "ThereminAudioEngine.h"
|
||||||
|
#import <AudioToolbox/AudioToolbox.h>
|
||||||
|
|
||||||
|
// Theremin parameter mapping constants
|
||||||
|
static const double kMinFrequency = 110.0; // Hz - A2 note (closed lid)
|
||||||
|
static const double kMaxFrequency = 440.0; // Hz - A4 note (open lid) - much lower range
|
||||||
|
static const double kMinAngle = 0.0; // degrees - closed lid
|
||||||
|
static const double kMaxAngle = 135.0; // degrees - fully open lid
|
||||||
|
|
||||||
|
// Volume control constants - continuous tone with velocity modulation
|
||||||
|
static const double kBaseVolume = 0.6; // Base volume when at rest
|
||||||
|
static const double kVelocityVolumeBoost = 0.4; // Additional volume boost from movement
|
||||||
|
static const double kVelocityFull = 8.0; // deg/s - max volume boost at/under this velocity
|
||||||
|
static const double kVelocityQuiet = 80.0; // deg/s - no volume boost over this velocity
|
||||||
|
|
||||||
|
// Vibrato constants
|
||||||
|
static const double kVibratoFrequency = 5.0; // Hz - vibrato rate
|
||||||
|
static const double kVibratoDepth = 0.03; // Vibrato depth as fraction of frequency (3%)
|
||||||
|
|
||||||
|
// Smoothing constants
|
||||||
|
static const double kAngleSmoothingFactor = 0.1; // Moderate smoothing for frequency
|
||||||
|
static const double kVelocitySmoothingFactor = 0.3; // Moderate smoothing for velocity
|
||||||
|
static const double kFrequencyRampTimeMs = 30.0; // Frequency ramping time constant
|
||||||
|
static const double kVolumeRampTimeMs = 50.0; // Volume ramping time constant
|
||||||
|
static const double kMovementThreshold = 0.3; // Minimum angle change to register movement
|
||||||
|
static const double kMovementTimeoutMs = 100.0; // Time before velocity decay
|
||||||
|
static const double kVelocityDecayFactor = 0.7; // Decay rate when no movement
|
||||||
|
static const double kAdditionalDecayFactor = 0.85; // Additional decay after timeout
|
||||||
|
|
||||||
|
// Audio constants
|
||||||
|
static const double kSampleRate = 44100.0;
|
||||||
|
static const UInt32 kBufferSize = 512;
|
||||||
|
|
||||||
|
@interface ThereminAudioEngine ()
|
||||||
|
|
||||||
|
// Audio engine components
|
||||||
|
@property (nonatomic, strong) AVAudioEngine *audioEngine;
|
||||||
|
@property (nonatomic, strong) AVAudioSourceNode *sourceNode;
|
||||||
|
@property (nonatomic, strong) AVAudioMixerNode *mixerNode;
|
||||||
|
|
||||||
|
// State tracking
|
||||||
|
@property (nonatomic, assign) double lastLidAngle;
|
||||||
|
@property (nonatomic, assign) double smoothedLidAngle;
|
||||||
|
@property (nonatomic, assign) double lastUpdateTime;
|
||||||
|
@property (nonatomic, assign) double smoothedVelocity;
|
||||||
|
@property (nonatomic, assign) double targetFrequency;
|
||||||
|
@property (nonatomic, assign) double targetVolume;
|
||||||
|
@property (nonatomic, assign) double currentFrequency;
|
||||||
|
@property (nonatomic, assign) double currentVolume;
|
||||||
|
@property (nonatomic, assign) BOOL isFirstUpdate;
|
||||||
|
@property (nonatomic, assign) NSTimeInterval lastMovementTime;
|
||||||
|
|
||||||
|
// Sine wave generation
|
||||||
|
@property (nonatomic, assign) double phase;
|
||||||
|
@property (nonatomic, assign) double phaseIncrement;
|
||||||
|
|
||||||
|
// Vibrato generation
|
||||||
|
@property (nonatomic, assign) double vibratoPhase;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation ThereminAudioEngine
|
||||||
|
|
||||||
|
- (instancetype)init {
|
||||||
|
self = [super init];
|
||||||
|
if (self) {
|
||||||
|
_isFirstUpdate = YES;
|
||||||
|
_lastUpdateTime = CACurrentMediaTime();
|
||||||
|
_lastMovementTime = CACurrentMediaTime();
|
||||||
|
_lastLidAngle = 0.0;
|
||||||
|
_smoothedLidAngle = 0.0;
|
||||||
|
_smoothedVelocity = 0.0;
|
||||||
|
_targetFrequency = kMinFrequency;
|
||||||
|
_targetVolume = kBaseVolume;
|
||||||
|
_currentFrequency = kMinFrequency;
|
||||||
|
_currentVolume = kBaseVolume;
|
||||||
|
_phase = 0.0;
|
||||||
|
_vibratoPhase = 0.0;
|
||||||
|
_phaseIncrement = 2.0 * M_PI * kMinFrequency / kSampleRate;
|
||||||
|
|
||||||
|
if (![self setupAudioEngine]) {
|
||||||
|
NSLog(@"[ThereminAudioEngine] Failed to setup audio engine");
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc {
|
||||||
|
[self stopEngine];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Audio Engine Setup
|
||||||
|
|
||||||
|
- (BOOL)setupAudioEngine {
|
||||||
|
self.audioEngine = [[AVAudioEngine alloc] init];
|
||||||
|
self.mixerNode = self.audioEngine.mainMixerNode;
|
||||||
|
|
||||||
|
// Create audio format for our sine wave
|
||||||
|
AVAudioFormat *format = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
|
||||||
|
sampleRate:kSampleRate
|
||||||
|
channels:1
|
||||||
|
interleaved:NO];
|
||||||
|
|
||||||
|
// Create source node for sine wave generation
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
self.sourceNode = [[AVAudioSourceNode alloc] initWithFormat:format renderBlock:^OSStatus(BOOL * _Nonnull isSilence, const AudioTimeStamp * _Nonnull timestamp, AVAudioFrameCount frameCount, AudioBufferList * _Nonnull outputData) {
|
||||||
|
return [weakSelf renderSineWave:isSilence timestamp:timestamp frameCount:frameCount outputData:outputData];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// Attach and connect the source node
|
||||||
|
[self.audioEngine attachNode:self.sourceNode];
|
||||||
|
[self.audioEngine connect:self.sourceNode to:self.mixerNode format:format];
|
||||||
|
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Engine Control
|
||||||
|
|
||||||
|
- (void)startEngine {
|
||||||
|
if (self.isEngineRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSError *error;
|
||||||
|
if (![self.audioEngine startAndReturnError:&error]) {
|
||||||
|
NSLog(@"[ThereminAudioEngine] Failed to start audio engine: %@", error.localizedDescription);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"[ThereminAudioEngine] Started theremin engine");
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)stopEngine {
|
||||||
|
if (!self.isEngineRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.audioEngine stop];
|
||||||
|
NSLog(@"[ThereminAudioEngine] Stopped theremin engine");
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isEngineRunning {
|
||||||
|
return self.audioEngine.isRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Sine Wave Generation
|
||||||
|
|
||||||
|
- (OSStatus)renderSineWave:(BOOL *)isSilence
|
||||||
|
timestamp:(const AudioTimeStamp *)timestamp
|
||||||
|
frameCount:(AVAudioFrameCount)frameCount
|
||||||
|
outputData:(AudioBufferList *)outputData {
|
||||||
|
|
||||||
|
float *output = (float *)outputData->mBuffers[0].mData;
|
||||||
|
|
||||||
|
// Always generate sound (continuous tone)
|
||||||
|
*isSilence = NO;
|
||||||
|
|
||||||
|
// Calculate vibrato phase increment
|
||||||
|
double vibratoPhaseIncrement = 2.0 * M_PI * kVibratoFrequency / kSampleRate;
|
||||||
|
|
||||||
|
// Generate sine wave samples with vibrato
|
||||||
|
for (AVAudioFrameCount i = 0; i < frameCount; i++) {
|
||||||
|
// Calculate vibrato modulation
|
||||||
|
double vibratoModulation = sin(self.vibratoPhase) * kVibratoDepth;
|
||||||
|
double modulatedFrequency = self.currentFrequency * (1.0 + vibratoModulation);
|
||||||
|
|
||||||
|
// Update phase increment for modulated frequency
|
||||||
|
self.phaseIncrement = 2.0 * M_PI * modulatedFrequency / kSampleRate;
|
||||||
|
|
||||||
|
// Generate sample with vibrato and current volume
|
||||||
|
output[i] = (float)(sin(self.phase) * self.currentVolume * 0.25); // 0.25 to prevent clipping
|
||||||
|
|
||||||
|
// Update phases
|
||||||
|
self.phase += self.phaseIncrement;
|
||||||
|
self.vibratoPhase += vibratoPhaseIncrement;
|
||||||
|
|
||||||
|
// Wrap phases to prevent accumulation of floating point errors
|
||||||
|
if (self.phase >= 2.0 * M_PI) {
|
||||||
|
self.phase -= 2.0 * M_PI;
|
||||||
|
}
|
||||||
|
if (self.vibratoPhase >= 2.0 * M_PI) {
|
||||||
|
self.vibratoPhase -= 2.0 * M_PI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return noErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Lid Angle Processing
|
||||||
|
|
||||||
|
- (void)updateWithLidAngle:(double)lidAngle {
|
||||||
|
double currentTime = CACurrentMediaTime();
|
||||||
|
|
||||||
|
if (self.isFirstUpdate) {
|
||||||
|
self.lastLidAngle = lidAngle;
|
||||||
|
self.smoothedLidAngle = lidAngle;
|
||||||
|
self.lastUpdateTime = currentTime;
|
||||||
|
self.lastMovementTime = currentTime;
|
||||||
|
self.isFirstUpdate = NO;
|
||||||
|
|
||||||
|
// Set initial frequency based on angle
|
||||||
|
[self updateTargetParametersWithAngle:lidAngle velocity:0.0];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate time delta
|
||||||
|
double deltaTime = currentTime - self.lastUpdateTime;
|
||||||
|
if (deltaTime <= 0 || deltaTime > 1.0) {
|
||||||
|
// Skip if time delta is invalid or too large
|
||||||
|
self.lastUpdateTime = currentTime;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage 1: Smooth the raw angle input
|
||||||
|
self.smoothedLidAngle = (kAngleSmoothingFactor * lidAngle) +
|
||||||
|
((1.0 - kAngleSmoothingFactor) * self.smoothedLidAngle);
|
||||||
|
|
||||||
|
// Stage 2: Calculate velocity from smoothed angle data
|
||||||
|
double deltaAngle = self.smoothedLidAngle - self.lastLidAngle;
|
||||||
|
double instantVelocity;
|
||||||
|
|
||||||
|
// Apply movement threshold
|
||||||
|
if (fabs(deltaAngle) < kMovementThreshold) {
|
||||||
|
instantVelocity = 0.0;
|
||||||
|
} else {
|
||||||
|
instantVelocity = fabs(deltaAngle / deltaTime);
|
||||||
|
self.lastLidAngle = self.smoothedLidAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage 3: Apply velocity smoothing and decay
|
||||||
|
if (instantVelocity > 0.0) {
|
||||||
|
self.smoothedVelocity = (kVelocitySmoothingFactor * instantVelocity) +
|
||||||
|
((1.0 - kVelocitySmoothingFactor) * self.smoothedVelocity);
|
||||||
|
self.lastMovementTime = currentTime;
|
||||||
|
} else {
|
||||||
|
self.smoothedVelocity *= kVelocityDecayFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional decay if no movement for extended period
|
||||||
|
double timeSinceMovement = currentTime - self.lastMovementTime;
|
||||||
|
if (timeSinceMovement > (kMovementTimeoutMs / 1000.0)) {
|
||||||
|
self.smoothedVelocity *= kAdditionalDecayFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state for next iteration
|
||||||
|
self.lastUpdateTime = currentTime;
|
||||||
|
|
||||||
|
// Update target parameters
|
||||||
|
[self updateTargetParametersWithAngle:self.smoothedLidAngle velocity:self.smoothedVelocity];
|
||||||
|
|
||||||
|
// Apply smooth parameter transitions
|
||||||
|
[self rampToTargetParameters];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setAngularVelocity:(double)velocity {
|
||||||
|
self.smoothedVelocity = velocity;
|
||||||
|
[self updateTargetParametersWithAngle:self.smoothedLidAngle velocity:velocity];
|
||||||
|
[self rampToTargetParameters];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)updateTargetParametersWithAngle:(double)angle velocity:(double)velocity {
|
||||||
|
// Map angle to frequency using exponential curve for musical feel
|
||||||
|
double normalizedAngle = fmax(0.0, fmin(1.0, (angle - kMinAngle) / (kMaxAngle - kMinAngle)));
|
||||||
|
|
||||||
|
// Use exponential mapping for more musical frequency distribution
|
||||||
|
double frequencyRatio = pow(normalizedAngle, 0.7); // Slight compression for better control
|
||||||
|
self.targetFrequency = kMinFrequency + frequencyRatio * (kMaxFrequency - kMinFrequency);
|
||||||
|
|
||||||
|
// Calculate continuous volume with velocity-based boost
|
||||||
|
double velocityBoost = 0.0;
|
||||||
|
if (velocity > 0.0) {
|
||||||
|
// Use smoothstep curve for natural volume boost response
|
||||||
|
double e0 = 0.0;
|
||||||
|
double e1 = kVelocityQuiet;
|
||||||
|
double t = fmin(1.0, fmax(0.0, (velocity - e0) / (e1 - e0)));
|
||||||
|
double s = t * t * (3.0 - 2.0 * t); // smoothstep function
|
||||||
|
velocityBoost = (1.0 - s) * kVelocityVolumeBoost; // invert: slow = more boost, fast = less boost
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine base volume with velocity boost
|
||||||
|
self.targetVolume = kBaseVolume + velocityBoost;
|
||||||
|
self.targetVolume = fmax(0.0, fmin(1.0, self.targetVolume));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for parameter ramping
|
||||||
|
- (double)rampValue:(double)current toward:(double)target withDeltaTime:(double)dt timeConstantMs:(double)tauMs {
|
||||||
|
double alpha = fmin(1.0, dt / (tauMs / 1000.0));
|
||||||
|
return current + (target - current) * alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)rampToTargetParameters {
|
||||||
|
// Calculate delta time for ramping
|
||||||
|
static double lastRampTime = 0;
|
||||||
|
double currentTime = CACurrentMediaTime();
|
||||||
|
if (lastRampTime == 0) lastRampTime = currentTime;
|
||||||
|
double deltaTime = currentTime - lastRampTime;
|
||||||
|
lastRampTime = currentTime;
|
||||||
|
|
||||||
|
// Ramp current values toward targets for smooth transitions
|
||||||
|
self.currentFrequency = [self rampValue:self.currentFrequency toward:self.targetFrequency withDeltaTime:deltaTime timeConstantMs:kFrequencyRampTimeMs];
|
||||||
|
self.currentVolume = [self rampValue:self.currentVolume toward:self.targetVolume withDeltaTime:deltaTime timeConstantMs:kVolumeRampTimeMs];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Property Accessors
|
||||||
|
|
||||||
|
- (double)currentVelocity {
|
||||||
|
return self.smoothedVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
39
README.md
39
README.md
@@ -12,11 +12,22 @@ Despite what the name would have you believe, it is a sensor that detects the an
|
|||||||
|
|
||||||
**Which devices have a lid angle sensor?**
|
**Which devices have a lid angle sensor?**
|
||||||
|
|
||||||
It was introduced with the 2019 16-inch MacBook Pro. If your laptop is newer, you probably have it.
|
It was introduced with the 2019 16-inch MacBook Pro. If your laptop is newer, you probably have it. [People have reported](https://github.com/samhenrigold/LidAngleSensor/issues/13) that it **does not work on M1 devices**, I have not yet figured out a fix.
|
||||||
|
|
||||||
|
**My laptop should have it, why doesn't it show up?**
|
||||||
|
|
||||||
|
I've only tested this on my M4 MacBook Pro and have hard-coded it to look for a specific sensor. If that doesn't work, try running [this script](https://gist.github.com/samhenrigold/42b5a92d1ee8aaf2b840be34bff28591) and report the output in [an issue](https://github.com/samhenrigold/LidAngleSensor/issues/new/choose).
|
||||||
|
|
||||||
|
Known problematic models:
|
||||||
|
|
||||||
|
- M1 MacBook Air
|
||||||
|
- M1 MacBook Pro
|
||||||
|
|
||||||
**Can I use this on my iMac?**
|
**Can I use this on my iMac?**
|
||||||
|
|
||||||
Not yet tested. Feel free to slam your computer into your desk and make a PR with your results.
|
~~Not yet tested. Feel free to slam your computer into your desk and make a PR with your results.~~
|
||||||
|
|
||||||
|
[It totally works](https://github.com/samhenrigold/LidAngleSensor/issues/33). If it doesn't work for you, try slamming your computer harder?
|
||||||
|
|
||||||
**Why?**
|
**Why?**
|
||||||
|
|
||||||
@@ -30,6 +41,10 @@ Oh. I don't know.
|
|||||||
|
|
||||||
I guess.
|
I guess.
|
||||||
|
|
||||||
|
**Why does it say it's by Lisa?**
|
||||||
|
|
||||||
|
I signed up for my developer account when I was a kid, used my mom's name, and now it's stuck that way forever and I can't change it. That's life.
|
||||||
|
|
||||||
**How come the audio feels kind of...weird?**
|
**How come the audio feels kind of...weird?**
|
||||||
|
|
||||||
I'm bad at audio.
|
I'm bad at audio.
|
||||||
@@ -37,3 +52,23 @@ I'm bad at audio.
|
|||||||
**Where did the sound effect come from?**
|
**Where did the sound effect come from?**
|
||||||
|
|
||||||
LEGO Batman 3: Beyond Gotham. But you knew that already.
|
LEGO Batman 3: Beyond Gotham. But you knew that already.
|
||||||
|
|
||||||
|
**Can I turn off the sound?**
|
||||||
|
|
||||||
|
Yes, never click "Start Audio". But this energy isn't encouraged.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
According to [this issue](https://github.com/samhenrigold/LidAngleSensor/issues/12), building requires having Xcode installed. I've only tested this on Xcode 26. YMMV.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Via Homebrew:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
brew install lidanglesensor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related projects
|
||||||
|
|
||||||
|
- [Python library that taps into this sensor](https://github.com/tcsenpai/pybooklid)
|
||||||
|
|||||||
Reference in New Issue
Block a user