mirror of
https://github.com/samhenrigold/LidAngleSensor.git
synced 2026-01-12 09:17:43 +03:00
Audio SFX
This commit is contained in:
@@ -7,11 +7,16 @@
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "LidAngleSensor.h"
|
||||
#import "CreakAudioEngine.h"
|
||||
|
||||
@interface AppDelegate ()
|
||||
@property (strong, nonatomic) LidAngleSensor *lidSensor;
|
||||
@property (strong, nonatomic) CreakAudioEngine *audioEngine;
|
||||
@property (strong, nonatomic) NSTextField *angleLabel;
|
||||
@property (strong, nonatomic) NSTextField *statusLabel;
|
||||
@property (strong, nonatomic) NSTextField *velocityLabel;
|
||||
@property (strong, nonatomic) NSTextField *audioStatusLabel;
|
||||
@property (strong, nonatomic) NSButton *audioToggleButton;
|
||||
@property (strong, nonatomic) NSTimer *updateTimer;
|
||||
@end
|
||||
|
||||
@@ -20,12 +25,14 @@
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||
[self createWindow];
|
||||
[self initializeLidSensor];
|
||||
[self initializeAudioEngine];
|
||||
[self startUpdatingDisplay];
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification *)aNotification {
|
||||
[self.updateTimer invalidate];
|
||||
[self.lidSensor stopLidAngleUpdates];
|
||||
[self.audioEngine stopEngine];
|
||||
}
|
||||
|
||||
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app {
|
||||
@@ -33,8 +40,8 @@
|
||||
}
|
||||
|
||||
- (void)createWindow {
|
||||
// Create the main window
|
||||
NSRect windowFrame = NSMakeRect(100, 100, 400, 300);
|
||||
// Create the main window (taller to accommodate audio controls)
|
||||
NSRect windowFrame = NSMakeRect(100, 100, 450, 420);
|
||||
self.window = [[NSWindow alloc] initWithContentRect:windowFrame
|
||||
styleMask:NSWindowStyleMaskTitled |
|
||||
NSWindowStyleMaskClosable |
|
||||
@@ -42,7 +49,7 @@
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
|
||||
[self.window setTitle:@"MacBook Lid Angle Sensor"];
|
||||
[self.window setTitle:@"MacBook Lid Creak Sensor"];
|
||||
[self.window makeKeyAndOrderFront:nil];
|
||||
[self.window center];
|
||||
|
||||
@@ -51,8 +58,8 @@
|
||||
[self.window setContentView:contentView];
|
||||
|
||||
// Create title label
|
||||
NSTextField *titleLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(50, 220, 300, 40)];
|
||||
[titleLabel setStringValue:@"MacBook Lid Angle Sensor"];
|
||||
NSTextField *titleLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(50, 360, 350, 40)];
|
||||
[titleLabel setStringValue:@"MacBook Lid Creak Sensor"];
|
||||
[titleLabel setFont:[NSFont boldSystemFontOfSize:18]];
|
||||
[titleLabel setBezeled:NO];
|
||||
[titleLabel setDrawsBackground:NO];
|
||||
@@ -62,9 +69,9 @@
|
||||
[contentView addSubview:titleLabel];
|
||||
|
||||
// Create angle display label
|
||||
self.angleLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(50, 150, 300, 50)];
|
||||
self.angleLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(50, 280, 350, 40)];
|
||||
[self.angleLabel setStringValue:@"Initializing..."];
|
||||
[self.angleLabel setFont:[NSFont monospacedSystemFontOfSize:24 weight:NSFontWeightMedium]];
|
||||
[self.angleLabel setFont:[NSFont monospacedSystemFontOfSize:20 weight:NSFontWeightMedium]];
|
||||
[self.angleLabel setBezeled:NO];
|
||||
[self.angleLabel setDrawsBackground:NO];
|
||||
[self.angleLabel setEditable:NO];
|
||||
@@ -73,8 +80,20 @@
|
||||
[self.angleLabel setTextColor:[NSColor systemBlueColor]];
|
||||
[contentView addSubview:self.angleLabel];
|
||||
|
||||
// Create velocity display label
|
||||
self.velocityLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(50, 240, 350, 30)];
|
||||
[self.velocityLabel setStringValue:@"Velocity: 0.0 deg/s"];
|
||||
[self.velocityLabel setFont:[NSFont monospacedSystemFontOfSize:14 weight:NSFontWeightRegular]];
|
||||
[self.velocityLabel setBezeled:NO];
|
||||
[self.velocityLabel setDrawsBackground:NO];
|
||||
[self.velocityLabel setEditable:NO];
|
||||
[self.velocityLabel setSelectable:NO];
|
||||
[self.velocityLabel setAlignment:NSTextAlignmentCenter];
|
||||
[self.velocityLabel setTextColor:[NSColor systemGreenColor]];
|
||||
[contentView addSubview:self.velocityLabel];
|
||||
|
||||
// Create status label
|
||||
self.statusLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(50, 100, 300, 30)];
|
||||
self.statusLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(50, 200, 350, 30)];
|
||||
[self.statusLabel setStringValue:@"Detecting sensor..."];
|
||||
[self.statusLabel setFont:[NSFont systemFontOfSize:14]];
|
||||
[self.statusLabel setBezeled:NO];
|
||||
@@ -85,9 +104,29 @@
|
||||
[self.statusLabel setTextColor:[NSColor secondaryLabelColor]];
|
||||
[contentView addSubview:self.statusLabel];
|
||||
|
||||
// Create audio toggle button
|
||||
self.audioToggleButton = [[NSButton alloc] initWithFrame:NSMakeRect(175, 150, 100, 30)];
|
||||
[self.audioToggleButton setTitle:@"Start Audio"];
|
||||
[self.audioToggleButton setBezelStyle:NSBezelStyleRounded];
|
||||
[self.audioToggleButton setTarget:self];
|
||||
[self.audioToggleButton setAction:@selector(toggleAudio:)];
|
||||
[contentView addSubview:self.audioToggleButton];
|
||||
|
||||
// Create audio status label
|
||||
self.audioStatusLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(50, 110, 350, 30)];
|
||||
[self.audioStatusLabel setStringValue:@"Audio: Stopped"];
|
||||
[self.audioStatusLabel setFont:[NSFont systemFontOfSize:14]];
|
||||
[self.audioStatusLabel setBezeled:NO];
|
||||
[self.audioStatusLabel setDrawsBackground:NO];
|
||||
[self.audioStatusLabel setEditable:NO];
|
||||
[self.audioStatusLabel setSelectable:NO];
|
||||
[self.audioStatusLabel setAlignment:NSTextAlignmentCenter];
|
||||
[self.audioStatusLabel setTextColor:[NSColor secondaryLabelColor]];
|
||||
[contentView addSubview:self.audioStatusLabel];
|
||||
|
||||
// Create info label
|
||||
NSTextField *infoLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(50, 30, 300, 60)];
|
||||
[infoLabel setStringValue:@"This app reads the MacBook's internal lid angle sensor.\n0° = Closed, ~180° = Fully Open"];
|
||||
NSTextField *infoLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(50, 30, 350, 60)];
|
||||
[infoLabel setStringValue:@"Real-time door creak audio responds to lid movement.\nSlow movement = louder creak, fast movement = silent."];
|
||||
[infoLabel setFont:[NSFont systemFontOfSize:12]];
|
||||
[infoLabel setBezeled:NO];
|
||||
[infoLabel setDrawsBackground:NO];
|
||||
@@ -112,9 +151,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)initializeAudioEngine {
|
||||
self.audioEngine = [[CreakAudioEngine alloc] init];
|
||||
|
||||
if (self.audioEngine) {
|
||||
[self.audioStatusLabel setStringValue:@"Audio: Ready (stopped)"];
|
||||
[self.audioStatusLabel setTextColor:[NSColor systemOrangeColor]];
|
||||
} else {
|
||||
[self.audioStatusLabel setStringValue:@"Audio: Failed to initialize"];
|
||||
[self.audioStatusLabel setTextColor:[NSColor systemRedColor]];
|
||||
[self.audioToggleButton setEnabled:NO];
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)toggleAudio:(id)sender {
|
||||
if (!self.audioEngine) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.audioEngine.isEngineRunning) {
|
||||
[self.audioEngine stopEngine];
|
||||
[self.audioToggleButton setTitle:@"Start Audio"];
|
||||
[self.audioStatusLabel setStringValue:@"Audio: Stopped"];
|
||||
[self.audioStatusLabel setTextColor:[NSColor systemOrangeColor]];
|
||||
} else {
|
||||
[self.audioEngine startEngine];
|
||||
[self.audioToggleButton setTitle:@"Stop Audio"];
|
||||
[self.audioStatusLabel setStringValue:@"Audio: Running"];
|
||||
[self.audioStatusLabel setTextColor:[NSColor systemGreenColor]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)startUpdatingDisplay {
|
||||
// Update the display every 100ms for smooth real-time updates
|
||||
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.1
|
||||
// Update every 16ms (60Hz) for smooth real-time audio and display updates
|
||||
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.016
|
||||
target:self
|
||||
selector:@selector(updateAngleDisplay)
|
||||
userInfo:nil
|
||||
@@ -137,6 +207,34 @@
|
||||
[self.angleLabel setStringValue:[NSString stringWithFormat:@"%.1f°", angle]];
|
||||
[self.angleLabel setTextColor:[NSColor systemBlueColor]];
|
||||
|
||||
// Update audio engine with new angle
|
||||
if (self.audioEngine) {
|
||||
[self.audioEngine updateWithLidAngle:angle];
|
||||
|
||||
// Update velocity display
|
||||
double velocity = self.audioEngine.currentVelocity;
|
||||
[self.velocityLabel setStringValue:[NSString stringWithFormat:@"Velocity: %.1f deg/s", velocity]];
|
||||
|
||||
// Color velocity based on magnitude
|
||||
double absVelocity = fabs(velocity);
|
||||
if (absVelocity < 0.3) {
|
||||
[self.velocityLabel setTextColor:[NSColor systemGrayColor]];
|
||||
} else if (absVelocity < 2.0) {
|
||||
[self.velocityLabel setTextColor:[NSColor systemGreenColor]];
|
||||
} else if (absVelocity < 10.0) {
|
||||
[self.velocityLabel setTextColor:[NSColor systemYellowColor]];
|
||||
} else {
|
||||
[self.velocityLabel setTextColor:[NSColor systemRedColor]];
|
||||
}
|
||||
|
||||
// Update audio status with gain/rate info if running
|
||||
if (self.audioEngine.isEngineRunning) {
|
||||
double gain = self.audioEngine.currentGain;
|
||||
double rate = self.audioEngine.currentRate;
|
||||
[self.audioStatusLabel setStringValue:[NSString stringWithFormat:@"Audio: Running (Gain: %.2f, Rate: %.2f)", gain, rate]];
|
||||
}
|
||||
}
|
||||
|
||||
// Provide contextual status based on angle
|
||||
NSString *status;
|
||||
if (angle < 5.0) {
|
||||
|
||||
62
LidAngleSensor/CreakAudioEngine.h
Normal file
62
LidAngleSensor/CreakAudioEngine.h
Normal file
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// CreakAudioEngine.h
|
||||
// LidAngleSensor
|
||||
//
|
||||
// Created by Sam on 2025-01-16.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
/**
|
||||
* CreakAudioEngine provides real-time door creak audio that responds to MacBook lid angle changes.
|
||||
*
|
||||
* Features:
|
||||
* - Real-time angular velocity calculation with multi-stage noise filtering
|
||||
* - Dynamic gain and pitch/tempo mapping based on movement speed
|
||||
* - Smooth parameter ramping to avoid audio artifacts
|
||||
* - Deadzone to prevent chattering at minimal movement
|
||||
* - Optimized for low-latency, responsive audio feedback
|
||||
*
|
||||
* Audio Behavior:
|
||||
* - Slow movement (1-10 deg/s): Maximum creak volume
|
||||
* - Medium movement (10-100 deg/s): Gradual fade to silence
|
||||
* - Fast movement (100+ deg/s): Silent
|
||||
*/
|
||||
@interface CreakAudioEngine : NSObject
|
||||
|
||||
@property (nonatomic, assign, readonly) BOOL isEngineRunning;
|
||||
@property (nonatomic, assign, readonly) double currentVelocity;
|
||||
@property (nonatomic, assign, readonly) double currentGain;
|
||||
@property (nonatomic, assign, readonly) double currentRate;
|
||||
|
||||
/**
|
||||
* Initialize the audio engine and load audio files.
|
||||
* @return Initialized engine instance, or nil if initialization failed
|
||||
*/
|
||||
- (instancetype)init;
|
||||
|
||||
/**
|
||||
* Start the audio engine and begin playback.
|
||||
*/
|
||||
- (void)startEngine;
|
||||
|
||||
/**
|
||||
* Stop the audio engine and halt playback.
|
||||
*/
|
||||
- (void)stopEngine;
|
||||
|
||||
/**
|
||||
* Update the creak audio based on new lid angle measurement.
|
||||
* This method calculates angular velocity, applies smoothing, and updates audio parameters.
|
||||
* @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
|
||||
356
LidAngleSensor/CreakAudioEngine.m
Normal file
356
LidAngleSensor/CreakAudioEngine.m
Normal file
@@ -0,0 +1,356 @@
|
||||
//
|
||||
// CreakAudioEngine.m
|
||||
// LidAngleSensor
|
||||
//
|
||||
// Created by Sam on 2025-01-16.
|
||||
//
|
||||
|
||||
#import "CreakAudioEngine.h"
|
||||
|
||||
// Audio parameter mapping constants
|
||||
static const double kDeadzone = 1.0; // deg/s - below this: treat as still
|
||||
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)
|
||||
|
||||
// Pitch variation constants
|
||||
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
|
||||
|
||||
@interface CreakAudioEngine ()
|
||||
|
||||
// Audio engine components
|
||||
@property (nonatomic, strong) AVAudioEngine *audioEngine;
|
||||
@property (nonatomic, strong) AVAudioPlayerNode *creakPlayerNode;
|
||||
@property (nonatomic, strong) AVAudioUnitVarispeed *varispeadUnit;
|
||||
@property (nonatomic, strong) AVAudioMixerNode *mixerNode;
|
||||
|
||||
// Audio files
|
||||
@property (nonatomic, strong) AVAudioFile *creakLoopFile;
|
||||
|
||||
// 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 targetGain;
|
||||
@property (nonatomic, assign) double targetRate;
|
||||
@property (nonatomic, assign) double currentGain;
|
||||
@property (nonatomic, assign) double currentRate;
|
||||
@property (nonatomic, assign) BOOL isFirstUpdate;
|
||||
@property (nonatomic, assign) NSTimeInterval lastMovementTime;
|
||||
|
||||
@end
|
||||
|
||||
@implementation CreakAudioEngine
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_isFirstUpdate = YES;
|
||||
_lastUpdateTime = CACurrentMediaTime();
|
||||
_lastMovementTime = CACurrentMediaTime();
|
||||
_lastLidAngle = 0.0;
|
||||
_smoothedLidAngle = 0.0;
|
||||
_smoothedVelocity = 0.0;
|
||||
_targetGain = 0.0;
|
||||
_targetRate = 1.0;
|
||||
_currentGain = 0.0;
|
||||
_currentRate = 1.0;
|
||||
|
||||
if (![self setupAudioEngine]) {
|
||||
NSLog(@"[CreakAudioEngine] Failed to setup audio engine");
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![self loadAudioFiles]) {
|
||||
NSLog(@"[CreakAudioEngine] Failed to load audio files");
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSLog(@"[CreakAudioEngine] Successfully initialized");
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self stopEngine];
|
||||
}
|
||||
|
||||
#pragma mark - Audio Engine Setup
|
||||
|
||||
- (BOOL)setupAudioEngine {
|
||||
self.audioEngine = [[AVAudioEngine alloc] init];
|
||||
|
||||
// Create audio nodes
|
||||
self.creakPlayerNode = [[AVAudioPlayerNode alloc] init];
|
||||
self.varispeadUnit = [[AVAudioUnitVarispeed alloc] init];
|
||||
self.mixerNode = self.audioEngine.mainMixerNode;
|
||||
|
||||
// Attach nodes to engine
|
||||
[self.audioEngine attachNode:self.creakPlayerNode];
|
||||
[self.audioEngine attachNode:self.varispeadUnit];
|
||||
|
||||
// Audio connections will be made after loading the file to use its native format
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)loadAudioFiles {
|
||||
NSBundle *bundle = [NSBundle mainBundle];
|
||||
|
||||
// Load creak loop file
|
||||
NSString *creakPath = [bundle pathForResource:@"CREAK_LOOP" ofType:@"wav"];
|
||||
if (!creakPath) {
|
||||
NSLog(@"[CreakAudioEngine] Could not find CREAK_LOOP.wav");
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
NSURL *creakURL = [NSURL fileURLWithPath:creakPath];
|
||||
self.creakLoopFile = [[AVAudioFile alloc] initForReading:creakURL error:&error];
|
||||
if (!self.creakLoopFile) {
|
||||
NSLog(@"[CreakAudioEngine] Failed to load CREAK_LOOP.wav: %@", error.localizedDescription);
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Now connect the audio graph using the file's native format
|
||||
AVAudioFormat *fileFormat = self.creakLoopFile.processingFormat;
|
||||
NSLog(@"[CreakAudioEngine] File format: %.0f Hz, %lu channels",
|
||||
fileFormat.sampleRate, (unsigned long)fileFormat.channelCount);
|
||||
|
||||
// Connect audio graph: CreakPlayer -> Varispeed -> Mixer
|
||||
[self.audioEngine connect:self.creakPlayerNode to:self.varispeadUnit format:fileFormat];
|
||||
[self.audioEngine connect:self.varispeadUnit to:self.mixerNode format:fileFormat];
|
||||
|
||||
NSLog(@"[CreakAudioEngine] Successfully loaded creak loop and connected audio graph");
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Engine Control
|
||||
|
||||
- (void)startEngine {
|
||||
if (self.isEngineRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
if (![self.audioEngine startAndReturnError:&error]) {
|
||||
NSLog(@"[CreakAudioEngine] Failed to start audio engine: %@", error.localizedDescription);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start looping the creak sound
|
||||
[self startCreakLoop];
|
||||
|
||||
NSLog(@"[CreakAudioEngine] Audio engine started");
|
||||
}
|
||||
|
||||
- (void)stopEngine {
|
||||
if (!self.isEngineRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self.creakPlayerNode stop];
|
||||
[self.audioEngine stop];
|
||||
|
||||
NSLog(@"[CreakAudioEngine] Audio engine stopped");
|
||||
}
|
||||
|
||||
- (BOOL)isEngineRunning {
|
||||
return self.audioEngine.isRunning;
|
||||
}
|
||||
|
||||
#pragma mark - Creak Loop Management
|
||||
|
||||
- (void)startCreakLoop {
|
||||
if (!self.creakPlayerNode || !self.creakLoopFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset file position to beginning
|
||||
self.creakLoopFile.framePosition = 0;
|
||||
|
||||
// Schedule the creak loop to play continuously
|
||||
AVAudioFrameCount frameCount = (AVAudioFrameCount)self.creakLoopFile.length;
|
||||
AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:self.creakLoopFile.processingFormat
|
||||
frameCapacity:frameCount];
|
||||
|
||||
NSError *error;
|
||||
if (![self.creakLoopFile readIntoBuffer:buffer error:&error]) {
|
||||
NSLog(@"[CreakAudioEngine] Failed to read creak loop into buffer: %@", error.localizedDescription);
|
||||
return;
|
||||
}
|
||||
|
||||
[self.creakPlayerNode scheduleBuffer:buffer atTime:nil options:AVAudioPlayerNodeBufferLoops completionHandler:nil];
|
||||
[self.creakPlayerNode play];
|
||||
|
||||
// Set initial volume to 0 (will be controlled by gain)
|
||||
self.creakPlayerNode.volume = 0.0;
|
||||
}
|
||||
|
||||
#pragma mark - Velocity Calculation and Parameter Mapping
|
||||
|
||||
- (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;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate time delta
|
||||
double deltaTime = currentTime - self.lastUpdateTime;
|
||||
if (deltaTime <= 0 || deltaTime > 1.0) {
|
||||
// Skip if time delta is invalid or too large (likely app was backgrounded)
|
||||
self.lastUpdateTime = currentTime;
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply heavy smoothing to the angle input to eliminate sensor jitter
|
||||
double angleSmoothingFactor = 0.05; // Very heavy smoothing (5% new, 95% old)
|
||||
self.smoothedLidAngle = (angleSmoothingFactor * lidAngle) +
|
||||
((1.0 - angleSmoothingFactor) * self.smoothedLidAngle);
|
||||
|
||||
// Calculate angular velocity using smoothed angle
|
||||
double deltaAngle = self.smoothedLidAngle - self.lastLidAngle;
|
||||
|
||||
// Calculate instantaneous velocity
|
||||
double instantVelocity;
|
||||
|
||||
// Much higher threshold to eliminate jitter completely
|
||||
if (fabs(deltaAngle) < 0.5) { // Ignore changes smaller than 0.5 degrees
|
||||
deltaAngle = 0.0;
|
||||
instantVelocity = 0.0;
|
||||
} else {
|
||||
instantVelocity = deltaAngle / deltaTime;
|
||||
// Update last angle only when we have significant movement
|
||||
self.lastLidAngle = self.smoothedLidAngle;
|
||||
}
|
||||
|
||||
// Use absolute value early to avoid sign issues
|
||||
instantVelocity = fabs(instantVelocity);
|
||||
|
||||
// Apply velocity smoothing only when there's actual movement
|
||||
if (instantVelocity > 0.0) {
|
||||
double velocitySmoothingFactor = 0.3; // Moderate smoothing for real movement
|
||||
self.smoothedVelocity = (velocitySmoothingFactor * instantVelocity) +
|
||||
((1.0 - velocitySmoothingFactor) * self.smoothedVelocity);
|
||||
self.lastMovementTime = currentTime;
|
||||
} else {
|
||||
// No movement detected - aggressively decay to zero
|
||||
self.smoothedVelocity *= 0.5; // Very fast decay when no movement
|
||||
}
|
||||
|
||||
// Additional decay if we haven't seen real movement for a while
|
||||
double timeSinceMovement = currentTime - self.lastMovementTime;
|
||||
if (timeSinceMovement > 0.05) { // Only 50ms without movement
|
||||
self.smoothedVelocity *= 0.8; // Additional decay
|
||||
}
|
||||
|
||||
// Update state for next iteration
|
||||
self.lastUpdateTime = currentTime;
|
||||
|
||||
// Apply velocity-based parameter mapping
|
||||
[self updateAudioParametersWithVelocity:self.smoothedVelocity];
|
||||
}
|
||||
|
||||
- (void)setAngularVelocity:(double)velocity {
|
||||
self.smoothedVelocity = velocity;
|
||||
[self updateAudioParametersWithVelocity:velocity];
|
||||
}
|
||||
|
||||
- (void)updateAudioParametersWithVelocity:(double)velocity {
|
||||
// Velocity is already absolute at this point
|
||||
double speed = velocity;
|
||||
|
||||
// Calculate target gain - CORRECTED LOGIC: slow = loud, fast = quiet
|
||||
double gain;
|
||||
if (speed < kDeadzone) {
|
||||
gain = 0.0; // truly still → no sound
|
||||
} else {
|
||||
// inverted smoothstep: slow => loud, fast => quiet
|
||||
double e0 = fmax(0.0, kVelocityFull - 0.5);
|
||||
double e1 = kVelocityQuiet + 0.5;
|
||||
double t = fmin(1.0, fmax(0.0, (speed - e0) / (e1 - e0)));
|
||||
double s = t * t * (3.0 - 2.0 * t); // smoothstep
|
||||
gain = 1.0 - s; // invert: slow = loud, fast = quiet
|
||||
gain = fmax(0.0, fmin(1.0, gain)); // Clamp to [0,1]
|
||||
}
|
||||
|
||||
// Calculate target rate (pitch/tempo) - wider range now
|
||||
double normalizedVelocity = fmax(0.0, fmin(1.0, speed / kVelocityQuiet));
|
||||
double rate = kMinRate + normalizedVelocity * (kMaxRate - kMinRate);
|
||||
rate = fmax(kMinRate, fmin(kMaxRate, rate));
|
||||
|
||||
// Store targets for smooth ramping
|
||||
self.targetGain = gain;
|
||||
self.targetRate = rate;
|
||||
|
||||
// Apply smooth parameter changes
|
||||
[self rampToTargetParameters];
|
||||
}
|
||||
|
||||
// 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)); // linear ramp coefficient
|
||||
return current + (target - current) * alpha;
|
||||
}
|
||||
|
||||
- (void)rampToTargetParameters {
|
||||
if (!self.isEngineRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 (much faster ramping for immediate response)
|
||||
self.currentGain = [self rampValue:self.currentGain toward:self.targetGain withDeltaTime:deltaTime timeConstantMs:50.0];
|
||||
self.currentRate = [self rampValue:self.currentRate toward:self.targetRate withDeltaTime:deltaTime timeConstantMs:80.0];
|
||||
|
||||
// Apply ramped values to audio nodes (make loop louder with 2x multiplier)
|
||||
self.creakPlayerNode.volume = (float)(self.currentGain * 2.0);
|
||||
self.varispeadUnit.rate = (float)self.currentRate;
|
||||
|
||||
// Debug logging
|
||||
if (self.targetGain > 0.01) {
|
||||
NSLog(@"[CreakAudioEngine] Target: %.3f/%.3f, Current: %.3f/%.3f",
|
||||
self.targetGain, self.targetRate, self.currentGain, self.currentRate);
|
||||
}
|
||||
}
|
||||
|
||||
// Grain system removed - loop only
|
||||
|
||||
#pragma mark - Property Accessors
|
||||
|
||||
- (double)currentVelocity {
|
||||
return self.smoothedVelocity;
|
||||
}
|
||||
|
||||
- (double)currentGain {
|
||||
return _currentGain;
|
||||
}
|
||||
|
||||
- (double)currentRate {
|
||||
return _currentRate;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user