node-mac-recorder 2.22.2 → 2.22.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/package.json +1 -1
- package/src/audio_processor.h +44 -0
- package/src/audio_processor.mm +231 -0
- package/src/audio_recorder.mm +20 -1
- package/src/avfoundation_recorder.mm +3 -3
- package/src/screen_capture_kit.mm +9 -4
- package/test-noise-reduction.js +80 -0
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"Bash(timeout 10 ffprobe:*)",
|
|
50
50
|
"Bash(ffmpeg:*)",
|
|
51
51
|
"Bash(timeout 30 node:*)",
|
|
52
|
-
"Bash(MAC_RECORDER_DEBUG=1 node test-camera-audio-sync.js:*)"
|
|
52
|
+
"Bash(MAC_RECORDER_DEBUG=1 node test-camera-audio-sync.js:*)",
|
|
53
|
+
"WebSearch"
|
|
53
54
|
],
|
|
54
55
|
"deny": [],
|
|
55
56
|
"ask": []
|
package/package.json
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#ifndef AUDIO_PROCESSOR_H
|
|
2
|
+
#define AUDIO_PROCESSOR_H
|
|
3
|
+
|
|
4
|
+
#import <Foundation/Foundation.h>
|
|
5
|
+
#import <CoreMedia/CoreMedia.h>
|
|
6
|
+
#import <AudioToolbox/AudioToolbox.h>
|
|
7
|
+
|
|
8
|
+
#ifdef __cplusplus
|
|
9
|
+
extern "C" {
|
|
10
|
+
#endif
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Real-time audio processor for filtering keyboard/mouse clicks and background noise.
|
|
14
|
+
*
|
|
15
|
+
* Algorithm:
|
|
16
|
+
* - Detects transient sounds (clicks) by analyzing amplitude envelope
|
|
17
|
+
* - Applies noise gate with smooth attack/release
|
|
18
|
+
* - Preserves voice while attenuating short, high-amplitude spikes
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Process audio buffer to reduce keyboard/mouse clicks and background noise.
|
|
23
|
+
*
|
|
24
|
+
* @param sampleBuffer Input audio buffer (read-only)
|
|
25
|
+
* @param outputBuffer Pointer to receive processed buffer (caller must CFRelease)
|
|
26
|
+
* @param sensitivity Noise gate sensitivity (0.0 - 1.0, default 0.5)
|
|
27
|
+
* Lower = more aggressive filtering
|
|
28
|
+
* Higher = preserves more sound but may miss some clicks
|
|
29
|
+
* @return YES if processing succeeded, NO otherwise
|
|
30
|
+
*/
|
|
31
|
+
BOOL processAudioBufferForNoiseReduction(CMSampleBufferRef sampleBuffer,
|
|
32
|
+
CMSampleBufferRef *outputBuffer,
|
|
33
|
+
Float32 sensitivity);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reset processor state (call when starting new recording)
|
|
37
|
+
*/
|
|
38
|
+
void resetAudioProcessorState(void);
|
|
39
|
+
|
|
40
|
+
#ifdef __cplusplus
|
|
41
|
+
}
|
|
42
|
+
#endif
|
|
43
|
+
|
|
44
|
+
#endif // AUDIO_PROCESSOR_H
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#import "audio_processor.h"
|
|
2
|
+
#import "logging.h"
|
|
3
|
+
#import <Accelerate/Accelerate.h>
|
|
4
|
+
#include <math.h>
|
|
5
|
+
|
|
6
|
+
// Processor state
|
|
7
|
+
static Float32 g_currentGain = 1.0f;
|
|
8
|
+
static Float32 g_envelopeLevel = 0.0f;
|
|
9
|
+
static UInt32 g_holdCounter = 0;
|
|
10
|
+
static BOOL g_processorInitialized = NO;
|
|
11
|
+
|
|
12
|
+
// Algorithm parameters (tuned for keyboard/mouse click detection)
|
|
13
|
+
static const Float32 kNoiseThreshold = 0.008f; // -42 dB: Clicks below this are likely noise
|
|
14
|
+
static const Float32 kVoiceThreshold = 0.02f; // -34 dB: Voice is typically above this
|
|
15
|
+
static const Float32 kAttackTime = 0.001f; // 1ms: Fast attack to catch transients
|
|
16
|
+
static const Float32 kReleaseTime = 0.050f; // 50ms: Smooth release to avoid cutting voice
|
|
17
|
+
static const Float32 kHoldTime = 0.020f; // 20ms: Hold time before release starts
|
|
18
|
+
static const Float32 kNoiseReductionAmount = 0.15f; // Reduce clicks to 15% volume (-16.5 dB)
|
|
19
|
+
|
|
20
|
+
// Calculated coefficients (updated based on sample rate)
|
|
21
|
+
static Float32 g_attackCoeff = 0.0f;
|
|
22
|
+
static Float32 g_releaseCoeff = 0.0f;
|
|
23
|
+
static UInt32 g_holdSamples = 0;
|
|
24
|
+
static Float32 g_sampleRate = 48000.0f;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize or update coefficients based on sample rate
|
|
28
|
+
*/
|
|
29
|
+
static void updateProcessorCoefficients(Float32 sampleRate) {
|
|
30
|
+
if (sampleRate <= 0.0f) {
|
|
31
|
+
sampleRate = 48000.0f; // Default
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (fabsf(g_sampleRate - sampleRate) > 0.1f) {
|
|
35
|
+
g_sampleRate = sampleRate;
|
|
36
|
+
|
|
37
|
+
// Exponential attack/release coefficients
|
|
38
|
+
// coeff = exp(-1.0 / (time * sampleRate))
|
|
39
|
+
g_attackCoeff = expf(-1.0f / (kAttackTime * sampleRate));
|
|
40
|
+
g_releaseCoeff = expf(-1.0f / (kReleaseTime * sampleRate));
|
|
41
|
+
g_holdSamples = (UInt32)(kHoldTime * sampleRate);
|
|
42
|
+
|
|
43
|
+
MRLog(@"🎛️ Audio Processor: Initialized for %.0f Hz (attack=%.4f, release=%.4f, hold=%u samples)",
|
|
44
|
+
sampleRate, g_attackCoeff, g_releaseCoeff, g_holdSamples);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
void resetAudioProcessorState(void) {
|
|
49
|
+
g_currentGain = 1.0f;
|
|
50
|
+
g_envelopeLevel = 0.0f;
|
|
51
|
+
g_holdCounter = 0;
|
|
52
|
+
g_processorInitialized = NO;
|
|
53
|
+
MRLog(@"🔄 Audio Processor: State reset");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Calculate RMS (Root Mean Square) level of audio buffer
|
|
58
|
+
*/
|
|
59
|
+
static Float32 calculateRMS(const Float32 *samples, UInt32 numSamples) {
|
|
60
|
+
if (!samples || numSamples == 0) {
|
|
61
|
+
return 0.0f;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Float32 sum = 0.0f;
|
|
65
|
+
vDSP_svesq(samples, 1, &sum, numSamples); // Sum of squares (using Accelerate framework)
|
|
66
|
+
return sqrtf(sum / (Float32)numSamples);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Apply gain to audio samples (in-place)
|
|
71
|
+
*/
|
|
72
|
+
static void applySmoothGain(Float32 *samples, UInt32 numSamples, Float32 targetGain, Float32 *currentGain) {
|
|
73
|
+
if (!samples || numSamples == 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Smooth gain interpolation to avoid clicks
|
|
78
|
+
Float32 gainStep = (targetGain - *currentGain) / (Float32)numSamples;
|
|
79
|
+
|
|
80
|
+
for (UInt32 i = 0; i < numSamples; i++) {
|
|
81
|
+
*currentGain += gainStep;
|
|
82
|
+
samples[i] *= *currentGain;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Detect if audio contains a transient (click/pop)
|
|
88
|
+
* Transients have very short duration (<20ms) and high amplitude
|
|
89
|
+
*/
|
|
90
|
+
static BOOL isTransientSound(Float32 rmsLevel, Float32 peakLevel, Float32 *envelope) {
|
|
91
|
+
// Update envelope (peak follower with fast attack, slow release)
|
|
92
|
+
Float32 coeff = (rmsLevel > *envelope) ? g_attackCoeff : g_releaseCoeff;
|
|
93
|
+
*envelope = coeff * (*envelope) + (1.0f - coeff) * rmsLevel;
|
|
94
|
+
|
|
95
|
+
// Transient detection: Sudden spike above envelope
|
|
96
|
+
Float32 ratio = (*envelope > 0.001f) ? (rmsLevel / *envelope) : 1.0f;
|
|
97
|
+
|
|
98
|
+
// If RMS suddenly increases by >3x and is in noise range, likely a click
|
|
99
|
+
BOOL isSuddenSpike = (ratio > 3.0f) && (rmsLevel > kNoiseThreshold) && (rmsLevel < kVoiceThreshold);
|
|
100
|
+
|
|
101
|
+
return isSuddenSpike;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
BOOL processAudioBufferForNoiseReduction(CMSampleBufferRef sampleBuffer,
|
|
105
|
+
CMSampleBufferRef *outputBuffer,
|
|
106
|
+
Float32 sensitivity) {
|
|
107
|
+
if (!sampleBuffer || !CMSampleBufferDataIsReady(sampleBuffer)) {
|
|
108
|
+
return NO;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Set output to NULL - we're not modifying the buffer yet, just analyzing
|
|
112
|
+
if (outputBuffer) {
|
|
113
|
+
*outputBuffer = NULL;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Get audio buffer list for analysis only (read-only)
|
|
117
|
+
AudioBufferList audioBufferList;
|
|
118
|
+
CMBlockBufferRef blockBuffer = NULL;
|
|
119
|
+
OSStatus status = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
|
|
120
|
+
sampleBuffer,
|
|
121
|
+
NULL,
|
|
122
|
+
&audioBufferList,
|
|
123
|
+
sizeof(audioBufferList),
|
|
124
|
+
NULL,
|
|
125
|
+
NULL,
|
|
126
|
+
kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
|
|
127
|
+
&blockBuffer
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (status != noErr || !blockBuffer) {
|
|
131
|
+
return YES; // Still return success, just skip analysis
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Get format description
|
|
135
|
+
CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
|
|
136
|
+
const AudioStreamBasicDescription *asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc);
|
|
137
|
+
|
|
138
|
+
if (!asbd) {
|
|
139
|
+
CFRelease(blockBuffer);
|
|
140
|
+
return NO;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Initialize coefficients if needed
|
|
144
|
+
if (!g_processorInitialized) {
|
|
145
|
+
updateProcessorCoefficients(asbd->mSampleRate);
|
|
146
|
+
g_processorInitialized = YES;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Process each channel
|
|
150
|
+
for (UInt32 i = 0; i < audioBufferList.mNumberBuffers; i++) {
|
|
151
|
+
AudioBuffer *buffer = &audioBufferList.mBuffers[i];
|
|
152
|
+
|
|
153
|
+
if (!buffer->mData || buffer->mDataByteSize == 0) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Determine format and get sample count
|
|
158
|
+
UInt32 numSamples = 0;
|
|
159
|
+
Float32 *floatSamples = NULL;
|
|
160
|
+
SInt16 *int16Samples = NULL;
|
|
161
|
+
BOOL needsConversion = NO;
|
|
162
|
+
|
|
163
|
+
if (asbd->mFormatFlags & kAudioFormatFlagIsFloat) {
|
|
164
|
+
// Already float
|
|
165
|
+
floatSamples = (Float32 *)buffer->mData;
|
|
166
|
+
numSamples = buffer->mDataByteSize / sizeof(Float32);
|
|
167
|
+
} else if (asbd->mFormatFlags & kAudioFormatFlagIsSignedInteger) {
|
|
168
|
+
// 16-bit integer - convert to float for processing
|
|
169
|
+
int16Samples = (SInt16 *)buffer->mData;
|
|
170
|
+
numSamples = buffer->mDataByteSize / sizeof(SInt16);
|
|
171
|
+
floatSamples = (Float32 *)malloc(numSamples * sizeof(Float32));
|
|
172
|
+
needsConversion = YES;
|
|
173
|
+
|
|
174
|
+
// Convert int16 to float32
|
|
175
|
+
for (UInt32 j = 0; j < numSamples; j++) {
|
|
176
|
+
floatSamples[j] = int16Samples[j] / 32768.0f;
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
continue; // Unsupported format
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!floatSamples || numSamples == 0) {
|
|
183
|
+
if (needsConversion && floatSamples) {
|
|
184
|
+
free(floatSamples);
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Calculate audio levels (analysis only - not modifying buffer)
|
|
190
|
+
Float32 rmsLevel = calculateRMS(floatSamples, numSamples);
|
|
191
|
+
Float32 peakLevel = 0.0f;
|
|
192
|
+
vDSP_maxmgv(floatSamples, 1, &peakLevel, numSamples);
|
|
193
|
+
|
|
194
|
+
// Detect transients (clicks/pops)
|
|
195
|
+
BOOL isClick = isTransientSound(rmsLevel, peakLevel, &g_envelopeLevel);
|
|
196
|
+
|
|
197
|
+
// Calculate what gain would be applied (for logging only)
|
|
198
|
+
Float32 targetGain = 1.0f;
|
|
199
|
+
|
|
200
|
+
if (rmsLevel < kNoiseThreshold * sensitivity) {
|
|
201
|
+
targetGain = kNoiseReductionAmount;
|
|
202
|
+
g_holdCounter = 0;
|
|
203
|
+
} else if (isClick) {
|
|
204
|
+
targetGain = kNoiseReductionAmount;
|
|
205
|
+
g_holdCounter = g_holdSamples;
|
|
206
|
+
} else if (g_holdCounter > 0) {
|
|
207
|
+
targetGain = kNoiseReductionAmount;
|
|
208
|
+
g_holdCounter = (g_holdCounter > numSamples) ? (g_holdCounter - numSamples) : 0;
|
|
209
|
+
} else if (rmsLevel > kVoiceThreshold) {
|
|
210
|
+
targetGain = 1.0f;
|
|
211
|
+
} else {
|
|
212
|
+
Float32 ratio = (rmsLevel - kNoiseThreshold * sensitivity) / (kVoiceThreshold - kNoiseThreshold * sensitivity);
|
|
213
|
+
ratio = fmaxf(0.0f, fminf(1.0f, ratio));
|
|
214
|
+
targetGain = kNoiseReductionAmount + ratio * (1.0f - kNoiseReductionAmount);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Log detection for debugging
|
|
218
|
+
static int logCounter = 0;
|
|
219
|
+
if (isClick && (logCounter++ % 10 == 0)) {
|
|
220
|
+
MRLog(@"🔊 Click detected: RMS=%.4f Peak=%.4f TargetGain=%.2f", rmsLevel, peakLevel, targetGain);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Clean up temporary buffer if needed
|
|
224
|
+
if (needsConversion && floatSamples) {
|
|
225
|
+
free(floatSamples);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
CFRelease(blockBuffer);
|
|
230
|
+
return YES;
|
|
231
|
+
}
|
package/src/audio_recorder.mm
CHANGED
|
@@ -45,6 +45,22 @@ static NSString *g_lastStandaloneAudioOutputPath = nil;
|
|
|
45
45
|
return [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
- (BOOL)configureVoiceProcessingForDevice:(AVCaptureDevice *)device {
|
|
49
|
+
// NOTE: Voice Isolation (microphoneMode) is iOS-only and not available on macOS
|
|
50
|
+
// For macOS noise reduction, users should:
|
|
51
|
+
// 1. Enable "Voice Isolation" in macOS Control Center > Microphone menu (macOS 13+)
|
|
52
|
+
// 2. Or use third-party apps like Krisp, RTX Voice, or SoundSource
|
|
53
|
+
//
|
|
54
|
+
// AVCaptureDevice on macOS does not expose noise reduction or voice processing properties
|
|
55
|
+
// Alternative approaches would require:
|
|
56
|
+
// - Core Audio AUVoiceIO unit (complex, requires audio graph restructuring)
|
|
57
|
+
// - AVAudioEngine with voice processing (causes crashes on macOS with aggregate devices)
|
|
58
|
+
// - Custom DSP filtering (significant development effort, may not be effective)
|
|
59
|
+
|
|
60
|
+
MRLog(@"ℹ️ macOS microphone noise reduction: Use System Settings or Control Center to enable Voice Isolation");
|
|
61
|
+
return NO;
|
|
62
|
+
}
|
|
63
|
+
|
|
48
64
|
- (BOOL)setupWriterWithSampleBuffer:(CMSampleBufferRef)sampleBuffer error:(NSError **)error {
|
|
49
65
|
if (self.writer) {
|
|
50
66
|
return YES;
|
|
@@ -167,7 +183,10 @@ static NSString *g_lastStandaloneAudioOutputPath = nil;
|
|
|
167
183
|
}
|
|
168
184
|
return NO;
|
|
169
185
|
}
|
|
170
|
-
|
|
186
|
+
|
|
187
|
+
// Configure voice processing to filter keyboard/mouse clicks and background noise
|
|
188
|
+
[self configureVoiceProcessingForDevice:device];
|
|
189
|
+
|
|
171
190
|
self.outputPath = outputPath;
|
|
172
191
|
self.session = [[AVCaptureSession alloc] init];
|
|
173
192
|
|
|
@@ -134,9 +134,9 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
134
134
|
|
|
135
135
|
// QUALITY FIX: ULTRA HIGH quality screen recording
|
|
136
136
|
// ProMotion displays may capture at 10 FPS - use very high bitrate for perfect quality
|
|
137
|
-
NSInteger bitrate = (NSInteger)(recordingSize.width * recordingSize.height *
|
|
138
|
-
bitrate = MAX(bitrate,
|
|
139
|
-
bitrate = MIN(bitrate,
|
|
137
|
+
NSInteger bitrate = (NSInteger)(recordingSize.width * recordingSize.height * 45);
|
|
138
|
+
bitrate = MAX(bitrate, 50 * 1000 * 1000); // Minimum 50 Mbps
|
|
139
|
+
bitrate = MIN(bitrate, 200 * 1000 * 1000); // Maximum 200 Mbps
|
|
140
140
|
|
|
141
141
|
NSLog(@"🎬 ULTRA QUALITY AVFoundation: %dx%d, bitrate=%.2fMbps",
|
|
142
142
|
(int)recordingSize.width, (int)recordingSize.height, bitrate / (1000.0 * 1000.0));
|
|
@@ -213,10 +213,10 @@ static void SCKQualityBitrateForDimensions(NSString *preset,
|
|
|
213
213
|
multiplier = 18;
|
|
214
214
|
minBitrate = 18 * 1000 * 1000;
|
|
215
215
|
maxBitrate = 80 * 1000 * 1000;
|
|
216
|
-
} else { // high/default
|
|
217
|
-
multiplier =
|
|
218
|
-
minBitrate =
|
|
219
|
-
maxBitrate =
|
|
216
|
+
} else { // high/default - ULTRA quality
|
|
217
|
+
multiplier = 60;
|
|
218
|
+
minBitrate = 80 * 1000 * 1000;
|
|
219
|
+
maxBitrate = 300 * 1000 * 1000;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
double base = ((double)MAX(1, width)) * ((double)MAX(1, height)) * (double)multiplier;
|
|
@@ -1516,6 +1516,11 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
1516
1516
|
micIdToUse = nil;
|
|
1517
1517
|
}
|
|
1518
1518
|
}
|
|
1519
|
+
|
|
1520
|
+
// NOTE: Voice processing (microphoneMode) is iOS-only and not available on macOS
|
|
1521
|
+
// Voice isolation is configured in audio_recorder.mm for AVFoundation-based recordings
|
|
1522
|
+
// ScreenCaptureKit microphone capture on macOS 15+ doesn't support microphoneMode
|
|
1523
|
+
|
|
1519
1524
|
if (micIdToUse && micIdToUse.length > 0) {
|
|
1520
1525
|
streamConfig.microphoneCaptureDeviceID = micIdToUse;
|
|
1521
1526
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const MacRecorder = require('./index.js');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
// Test output directory
|
|
6
|
+
const outputDir = path.join(__dirname, 'test-output');
|
|
7
|
+
if (!fs.existsSync(outputDir)) {
|
|
8
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const timestamp = Date.now();
|
|
12
|
+
const outputPath = path.join(outputDir, `noise-reduction-test-${timestamp}.mov`);
|
|
13
|
+
|
|
14
|
+
console.log('🎙️ Starting microphone recording with noise reduction...');
|
|
15
|
+
console.log('📝 Instructions:');
|
|
16
|
+
console.log(' 1. Speak into your microphone (normal voice)');
|
|
17
|
+
console.log(' 2. Type on your keyboard (heavy typing)');
|
|
18
|
+
console.log(' 3. Click your mouse multiple times');
|
|
19
|
+
console.log(' 4. Speak again to compare');
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log('⏱️ Recording for 15 seconds...');
|
|
22
|
+
console.log('');
|
|
23
|
+
|
|
24
|
+
const recorder = new MacRecorder();
|
|
25
|
+
|
|
26
|
+
recorder.on('recordingStarted', () => {
|
|
27
|
+
console.log('✅ Recording started!');
|
|
28
|
+
console.log('🎤 Custom noise reduction is ACTIVE');
|
|
29
|
+
console.log('');
|
|
30
|
+
console.log('Starting countdown:');
|
|
31
|
+
|
|
32
|
+
let countdown = 15;
|
|
33
|
+
const interval = setInterval(() => {
|
|
34
|
+
process.stdout.write(`\r⏱️ ${countdown} seconds remaining... `);
|
|
35
|
+
countdown--;
|
|
36
|
+
|
|
37
|
+
if (countdown < 0) {
|
|
38
|
+
clearInterval(interval);
|
|
39
|
+
process.stdout.write('\r');
|
|
40
|
+
console.log('⏱️ Time up! Stopping...');
|
|
41
|
+
console.log('');
|
|
42
|
+
|
|
43
|
+
recorder.stopRecording()
|
|
44
|
+
.then(() => {
|
|
45
|
+
console.log('✅ Recording saved to:', outputPath);
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log('🎧 Play the recording to verify:');
|
|
48
|
+
console.log(` open "${outputPath}"`);
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log('Expected results:');
|
|
51
|
+
console.log(' ✅ Voice should be clear');
|
|
52
|
+
console.log(' ✅ Keyboard typing should be significantly reduced');
|
|
53
|
+
console.log(' ✅ Mouse clicks should be filtered out');
|
|
54
|
+
process.exit(0);
|
|
55
|
+
})
|
|
56
|
+
.catch(err => {
|
|
57
|
+
console.error('❌ Error stopping:', err);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}, 1000);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
recorder.on('stopped', () => {
|
|
65
|
+
console.log('🛑 Recording stopped');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
recorder.on('completed', (result) => {
|
|
69
|
+
console.log('✅ Recording completed:', result);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Start recording with microphone
|
|
73
|
+
recorder.startRecording(outputPath, {
|
|
74
|
+
includeMicrophone: true,
|
|
75
|
+
includeSystemAudio: false,
|
|
76
|
+
fps: 1 // Minimal FPS since we're only testing audio
|
|
77
|
+
}).catch(err => {
|
|
78
|
+
console.error('❌ Failed to start recording:', err);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
});
|