From f5e92844414bd876087f347c898467939ede61ef Mon Sep 17 00:00:00 2001 From: mi-art <> Date: Sun, 26 Oct 2025 02:50:30 +0200 Subject: [PATCH 1/4] duplicate Theremin* into Noise* --- LidAngleSensor/NoiseAudioEngine.h | 62 ++++++ LidAngleSensor/NoiseAudioEngine.m | 318 ++++++++++++++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 LidAngleSensor/NoiseAudioEngine.h create mode 100644 LidAngleSensor/NoiseAudioEngine.m diff --git a/LidAngleSensor/NoiseAudioEngine.h b/LidAngleSensor/NoiseAudioEngine.h new file mode 100644 index 0000000..a7ec7b8 --- /dev/null +++ b/LidAngleSensor/NoiseAudioEngine.h @@ -0,0 +1,62 @@ +// +// ThereminAudioEngine.h +// LidAngleSensor +// +// Created by Sam on 2025-09-06. +// + +#import +#import + +/** + * 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 diff --git a/LidAngleSensor/NoiseAudioEngine.m b/LidAngleSensor/NoiseAudioEngine.m new file mode 100644 index 0000000..b8ac8a8 --- /dev/null +++ b/LidAngleSensor/NoiseAudioEngine.m @@ -0,0 +1,318 @@ +// +// ThereminAudioEngine.m +// LidAngleSensor +// +// Created by Sam on 2025-09-06. +// + +#import "ThereminAudioEngine.h" +#import + +// 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 From fe62d7de8618daa09ebb4a7b0bfde75f0c8b12d4 Mon Sep 17 00:00:00 2001 From: mi-art <> Date: Sun, 26 Oct 2025 02:50:30 +0200 Subject: [PATCH 2/4] implement filtered noise synth --- LidAngleSensor/AppDelegate.m | 14 +++- LidAngleSensor/NoiseAudioEngine.h | 28 +++----- LidAngleSensor/NoiseAudioEngine.m | 111 ++++++++++++++++-------------- 3 files changed, 82 insertions(+), 71 deletions(-) diff --git a/LidAngleSensor/AppDelegate.m b/LidAngleSensor/AppDelegate.m index f4a69de..11fcdcd 100644 --- a/LidAngleSensor/AppDelegate.m +++ b/LidAngleSensor/AppDelegate.m @@ -9,17 +9,20 @@ #import "LidAngleSensor.h" #import "CreakAudioEngine.h" #import "ThereminAudioEngine.h" +#import "NoiseAudioEngine.h" #import "NSLabel.h" typedef NS_ENUM(NSInteger, AudioMode) { AudioModeCreak, - AudioModeTheremin + AudioModeTheremin, + AudioModeNoise }; @interface AppDelegate () @property (strong, nonatomic) LidAngleSensor *lidSensor; @property (strong, nonatomic) CreakAudioEngine *creakAudioEngine; @property (strong, nonatomic) ThereminAudioEngine *thereminAudioEngine; +@property (strong, nonatomic) NoiseAudioEngine *noiseAudioEngine; @property (strong, nonatomic) NSLabel *angleLabel; @property (strong, nonatomic) NSLabel *statusLabel; @property (strong, nonatomic) NSLabel *velocityLabel; @@ -120,9 +123,10 @@ - (void)createWindow { // Create mode selector self.modeSelector = [[NSSegmentedControl alloc] init]; - [self.modeSelector setSegmentCount:2]; + [self.modeSelector setSegmentCount:3]; [self.modeSelector setLabel:@"Creak" forSegment:0]; [self.modeSelector setLabel:@"Theremin" forSegment:1]; + [self.modeSelector setLabel:@"Noise" forSegment:2]; [self.modeSelector setSelectedSegment:0]; // Default to creak [self.modeSelector setTarget:self]; [self.modeSelector setAction:@selector(modeChanged:)]; @@ -188,8 +192,9 @@ - (void)initializeLidSensor { - (void)initializeAudioEngines { self.creakAudioEngine = [[CreakAudioEngine alloc] init]; self.thereminAudioEngine = [[ThereminAudioEngine alloc] init]; + self.noiseAudioEngine = [[NoiseAudioEngine alloc] init]; - if (self.creakAudioEngine && self.thereminAudioEngine) { + if (self.creakAudioEngine && self.thereminAudioEngine && self.noiseAudioEngine) { [self.audioStatusLabel setStringValue:@""]; } else { [self.audioStatusLabel setStringValue:@"Audio initialization failed"]; @@ -247,6 +252,8 @@ - (id)currentAudioEngine { return self.creakAudioEngine; case AudioModeTheremin: return self.thereminAudioEngine; + case AudioModeNoise: + return self.noiseAudioEngine; default: return self.creakAudioEngine; } @@ -302,6 +309,7 @@ - (void)updateAngleDisplay { double volume = [currentEngine currentVolume]; [self.audioStatusLabel setStringValue:[NSString stringWithFormat:@"Freq: %.1f Hz, Vol: %.2f", frequency, volume]]; } + // TODO: noise } } diff --git a/LidAngleSensor/NoiseAudioEngine.h b/LidAngleSensor/NoiseAudioEngine.h index a7ec7b8..8ab41c9 100644 --- a/LidAngleSensor/NoiseAudioEngine.h +++ b/LidAngleSensor/NoiseAudioEngine.h @@ -1,5 +1,5 @@ // -// ThereminAudioEngine.h +// NoiseAudioEngine.h // LidAngleSensor // // Created by Sam on 2025-09-06. @@ -9,21 +9,15 @@ #import /** - * 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 + * Synthesized filtered noise + * + * Generate random noise, and filter it with a second order low pass filter, with a high quality + * factor (Q) and a cutoff frequency mapped to the lid angle. + * Biquad coeffs are updated with bilinear transform so that filter remains stable + * + * Most of the code was copied from from ThereminAudioEngine.m */ -@interface ThereminAudioEngine : NSObject +@interface NoiseAudioEngine : NSObject @property (nonatomic, assign, readonly) BOOL isEngineRunning; @property (nonatomic, assign, readonly) double currentVelocity; @@ -31,7 +25,7 @@ @property (nonatomic, assign, readonly) double currentVolume; /** - * Initialize the theremin audio engine. + * Initialize the audio engine. * @return Initialized engine instance, or nil if initialization failed */ - (instancetype)init; @@ -47,7 +41,7 @@ - (void)stopEngine; /** - * Update the theremin audio based on new lid angle measurement. + * Update based on new lid angle measurement. * This method calculates frequency mapping and volume based on movement. * @param lidAngle Current lid angle in degrees */ diff --git a/LidAngleSensor/NoiseAudioEngine.m b/LidAngleSensor/NoiseAudioEngine.m index b8ac8a8..cacefd7 100644 --- a/LidAngleSensor/NoiseAudioEngine.m +++ b/LidAngleSensor/NoiseAudioEngine.m @@ -1,33 +1,32 @@ // -// ThereminAudioEngine.m +// NoiseAudioEngine.m // LidAngleSensor // -// Created by Sam on 2025-09-06. +// Created by mi-art on 2025-10-26 // -#import "ThereminAudioEngine.h" +#import "NoiseAudioEngine.h" #import -// 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 +// Frequency parameter mapping constants +static const double kMinFrequency = 300.0; // Hz +static const double kMaxFrequency = 4000.0; // Hz 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 +// Volume control constants +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 kVelocityQuiet = 80.0; // deg/s - no volume boost over this velocity +static const BOOL kEnableVolumeModulation = false; // Bypass volume variation, sounds better IMO -// Vibrato constants -static const double kVibratoFrequency = 5.0; // Hz - vibrato rate -static const double kVibratoDepth = 0.03; // Vibrato depth as fraction of frequency (3%) +// Filter quality factor +static const double kQ = 10; // Smoothing constants -static const double kAngleSmoothingFactor = 0.1; // Moderate smoothing for frequency +static const double kAngleSmoothingFactor = 0.4; // 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 kFrequencyRampTimeMs = 3.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 @@ -36,9 +35,8 @@ // Audio constants static const double kSampleRate = 44100.0; -static const UInt32 kBufferSize = 512; -@interface ThereminAudioEngine () +@interface NoiseAudioEngine () // Audio engine components @property (nonatomic, strong) AVAudioEngine *audioEngine; @@ -57,16 +55,19 @@ @interface ThereminAudioEngine () @property (nonatomic, assign) BOOL isFirstUpdate; @property (nonatomic, assign) NSTimeInterval lastMovementTime; -// Sine wave generation -@property (nonatomic, assign) double phase; -@property (nonatomic, assign) double phaseIncrement; +// Biquad filter coefficients +@property (nonatomic, assign) double b0, b1, b2, a1, a2; + +// Biquad filter state variables +@property (nonatomic, assign) double x1, x2, y1, y2; + // Vibrato generation @property (nonatomic, assign) double vibratoPhase; @end -@implementation ThereminAudioEngine +@implementation NoiseAudioEngine - (instancetype)init { self = [super init]; @@ -81,12 +82,10 @@ - (instancetype)init { _targetVolume = kBaseVolume; _currentFrequency = kMinFrequency; _currentVolume = kBaseVolume; - _phase = 0.0; - _vibratoPhase = 0.0; - _phaseIncrement = 2.0 * M_PI * kMinFrequency / kSampleRate; + [self updateCoefficients]; if (![self setupAudioEngine]) { - NSLog(@"[ThereminAudioEngine] Failed to setup audio engine"); + NSLog(@"[NoiseAudioEngine] Failed to setup audio engine"); return nil; } } @@ -131,11 +130,11 @@ - (void)startEngine { NSError *error; if (![self.audioEngine startAndReturnError:&error]) { - NSLog(@"[ThereminAudioEngine] Failed to start audio engine: %@", error.localizedDescription); + NSLog(@"[NoiseAudioEngine] Failed to start audio engine: %@", error.localizedDescription); return; } - NSLog(@"[ThereminAudioEngine] Started theremin engine"); + NSLog(@"[NoiseAudioEngine] Started noise engine"); } - (void)stopEngine { @@ -144,7 +143,7 @@ - (void)stopEngine { } [self.audioEngine stop]; - NSLog(@"[ThereminAudioEngine] Stopped theremin engine"); + NSLog(@"[NoiseAudioEngine] Stopped noise engine"); } - (BOOL)isEngineRunning { @@ -153,6 +152,20 @@ - (BOOL)isEngineRunning { #pragma mark - Sine Wave Generation +- (void)updateCoefficients { + float omega = 2.0 * M_PI * self.currentFrequency / kSampleRate; + float alpha = sinf(omega) / (2.0 * kQ); + float cos_omega = cosf(omega); + + float a0 = 1.0 + alpha; + self.b0 = (1.0 - cos_omega) / 2.0 / a0; + self.b1 = (1.0 - cos_omega) / a0; + self.b2 = (1.0 - cos_omega) / 2.0 / a0; + self.a1 = -2.0 * cos_omega / a0; + self.a2 = (1.0 - alpha) / a0; +} + + - (OSStatus)renderSineWave:(BOOL *)isSilence timestamp:(const AudioTimeStamp *)timestamp frameCount:(AVAudioFrameCount)frameCount @@ -163,31 +176,23 @@ - (OSStatus)renderSineWave:(BOOL *)isSilence // 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); + float x0 = ((float)arc4random() / UINT32_MAX) * 2.0 - 1.0; - // Update phase increment for modulated frequency - self.phaseIncrement = 2.0 * M_PI * modulatedFrequency / kSampleRate; + float y0 = self.b0 * x0 + self.b1 * self.x1 + self.b2 * self.x2 + - self.a1 * self.y1 - self.a2 * self.y2; - // Generate sample with vibrato and current volume - output[i] = (float)(sin(self.phase) * self.currentVolume * 0.25); // 0.25 to prevent clipping + output[i] = y0; - // Update phases - self.phase += self.phaseIncrement; - self.vibratoPhase += vibratoPhaseIncrement; + self.x2 = self.x1; + self.x1 = x0; + self.y2 = self.y1; + self.y1 = y0; - // 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; + output[i] = output[i] * 0.1; + + if (kEnableVolumeModulation) { + output[i] *= self.currentVolume; } } @@ -271,7 +276,7 @@ - (void)updateTargetParametersWithAngle:(double)angle velocity:(double)velocity 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 + double frequencyRatio = pow(normalizedAngle, 1); // Slight compression for better control self.targetFrequency = kMinFrequency + frequencyRatio * (kMaxFrequency - kMinFrequency); // Calculate continuous volume with velocity-based boost @@ -305,7 +310,11 @@ - (void)rampToTargetParameters { lastRampTime = currentTime; // Ramp current values toward targets for smooth transitions - self.currentFrequency = [self rampValue:self.currentFrequency toward:self.targetFrequency withDeltaTime:deltaTime timeConstantMs:kFrequencyRampTimeMs]; + self.currentFrequency = [self rampValue:self.currentFrequency toward:self.targetFrequency + withDeltaTime:deltaTime timeConstantMs:kFrequencyRampTimeMs]; + + [self updateCoefficients]; + self.currentVolume = [self rampValue:self.currentVolume toward:self.targetVolume withDeltaTime:deltaTime timeConstantMs:kVolumeRampTimeMs]; } From fecae48ebeaf06d0200e5423ff4cdb47efb7743f Mon Sep 17 00:00:00 2001 From: mi-art <> Date: Sun, 26 Oct 2025 02:50:30 +0200 Subject: [PATCH 3/4] theremin: fix warnings --- LidAngleSensor/ThereminAudioEngine.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/LidAngleSensor/ThereminAudioEngine.m b/LidAngleSensor/ThereminAudioEngine.m index 227df69..5710299 100644 --- a/LidAngleSensor/ThereminAudioEngine.m +++ b/LidAngleSensor/ThereminAudioEngine.m @@ -17,7 +17,6 @@ // 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 @@ -36,7 +35,6 @@ // Audio constants static const double kSampleRate = 44100.0; -static const UInt32 kBufferSize = 512; @interface ThereminAudioEngine () From fd46ce3a4e672682c5efa9e2d0c3bad3b125bb9b Mon Sep 17 00:00:00 2001 From: mi-art <> Date: Sun, 26 Oct 2025 02:50:30 +0200 Subject: [PATCH 4/4] script to build and run --- scripts/build_and_run.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100755 scripts/build_and_run.sh diff --git a/scripts/build_and_run.sh b/scripts/build_and_run.sh new file mode 100755 index 0000000..2eca4de --- /dev/null +++ b/scripts/build_and_run.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +# select debug or release +configuration=release + +# go to root +cd $(dirname "$0")/.. + +# build +xcodebuild \ + -project "LidAngleSensor.xcodeproj" \ + -scheme "LidAngleSensor" \ + -configuration $configuration \ + -derivedDataPath build \ + CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" DEVELOPMENT_TEAM="" \ + -arch arm64 + +# run +build/Build/Products/$configuration/LidAngleSensor.app/Contents/MacOS/LidAngleSensor