cursor having a crack at a wrapper module
This commit is contained in:
@@ -0,0 +1,119 @@
|
|||||||
|
# AudioTee.js - Node.js Audio Streaming Package
|
||||||
|
|
||||||
|
This is a Node.js wrapper for AudioTee that provides a streaming interface to capture macOS system audio using Core Audio taps.
|
||||||
|
|
||||||
|
## Project Context
|
||||||
|
|
||||||
|
- **Purpose**: Node.js package that wraps the AudioTee Swift binary via child processes
|
||||||
|
- **Target**: macOS 14.2+ with Node.js 14+
|
||||||
|
- **Use cases**: Real-time audio processing, ASR integration, Electron apps
|
||||||
|
- **Distribution**: npm package using node-pre-gyp for binary distribution
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- `index.js` - Main entry point, uses node-pre-gyp to locate binary
|
||||||
|
- `lib/AudioTeeStream.js` - Core streaming class that wraps AudioTee process
|
||||||
|
- `scripts/build.js` - Build script that copies AudioTee binary for distribution
|
||||||
|
- `test/test.js` - Test suite demonstrating usage
|
||||||
|
|
||||||
|
## Technical Standards
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
- Use functional programming patterns where possible
|
||||||
|
- Prefer `const` over `let`, avoid `var`
|
||||||
|
- Use arrow functions for callbacks and short functions
|
||||||
|
- No semicolons at end of lines (per user preference)
|
||||||
|
- Use template literals for string interpolation
|
||||||
|
- Prefer British English spelling (colour, realise, etc.)
|
||||||
|
|
||||||
|
### Node.js Specific
|
||||||
|
- Use EventEmitter pattern for streaming interfaces
|
||||||
|
- Handle child process lifecycle carefully (spawn, kill, cleanup)
|
||||||
|
- Use Buffer for binary data, not Uint8Array
|
||||||
|
- Implement proper error handling with descriptive messages
|
||||||
|
- Use readline interface for line-based protocol parsing
|
||||||
|
- Handle both JSON and binary protocol modes correctly
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Always emit errors via EventEmitter, don't throw synchronously
|
||||||
|
- Provide context in error messages (PIDs, file paths, etc.)
|
||||||
|
- Handle child process errors gracefully
|
||||||
|
- Validate input parameters and provide helpful error messages
|
||||||
|
|
||||||
|
### Protocol Implementation
|
||||||
|
- Correctly parse the mixed JSON/binary protocol from AudioTee
|
||||||
|
- Handle partial reads and buffer management for binary mode
|
||||||
|
- Emit events in the correct order (metadata → stream_start → audio → stream_stop)
|
||||||
|
- Preserve AudioTee's timestamp and metadata information
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- Minimize external dependencies (currently only node-pre-gyp)
|
||||||
|
- Use only Node.js built-in modules where possible
|
||||||
|
- Ensure compatibility with Node.js 14+ (no newer APIs)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Comprehensive JSDoc comments for public APIs
|
||||||
|
- Examples in README showing real-world usage patterns
|
||||||
|
- Clear event documentation with payload structure
|
||||||
|
- Error scenarios and troubleshooting guidance
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Provide both interactive and automated test modes
|
||||||
|
- Test should work without requiring audio playback
|
||||||
|
- Handle permissions issues gracefully in tests
|
||||||
|
- Verify binary protocol parsing works correctly
|
||||||
|
|
||||||
|
## Binary Distribution
|
||||||
|
|
||||||
|
- Use node-pre-gyp for professional binary distribution
|
||||||
|
- Support both Intel and Apple Silicon Macs
|
||||||
|
- Graceful fallback if binary download fails
|
||||||
|
- Verify binary functionality during build process
|
||||||
|
|
||||||
|
## Development Guidelines
|
||||||
|
|
||||||
|
When making changes:
|
||||||
|
|
||||||
|
1. **Test thoroughly** - Both JSON and binary protocols
|
||||||
|
2. **Handle edge cases** - Process crashes, permission issues, etc.
|
||||||
|
3. **Maintain compatibility** - Don't break existing APIs
|
||||||
|
4. **Update documentation** - Keep README examples current
|
||||||
|
5. **Follow semantic versioning** - Breaking changes require major version bump
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
```javascript
|
||||||
|
// Always emit errors, don't throw
|
||||||
|
this.emit('error', new Error(`Descriptive message: ${details}`))
|
||||||
|
|
||||||
|
// Provide context in errors
|
||||||
|
this.emit('error', new Error(`Failed to start AudioTee: ${error.message}`))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Emission
|
||||||
|
```javascript
|
||||||
|
// Use consistent event structure
|
||||||
|
this.emit('audio', {
|
||||||
|
timestamp: new Date(),
|
||||||
|
duration: number,
|
||||||
|
peakAmplitude: number,
|
||||||
|
audioData: Buffer
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Process Management
|
||||||
|
```javascript
|
||||||
|
// Always check process state before operations
|
||||||
|
if (this.process && !this.process.killed) {
|
||||||
|
this.process.kill('SIGTERM')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Considerations
|
||||||
|
|
||||||
|
- Support for multiple concurrent streams
|
||||||
|
- WebSocket streaming interface
|
||||||
|
- TypeScript definitions
|
||||||
|
- React/Vue.js integration examples
|
||||||
|
- Performance monitoring and metrics
|
||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
name: Build and Release AudioTee.js
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Version to build (e.g., v1.0.0)"
|
||||||
|
required: true
|
||||||
|
default: "v1.0.0"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: macos-latest
|
||||||
|
arch: arm64
|
||||||
|
node: "18"
|
||||||
|
- os: macos-13
|
||||||
|
arch: x64
|
||||||
|
node: "18"
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout audiotee-js
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Checkout AudioTee (parent project)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: your-org/audiotee
|
||||||
|
path: audiotee
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
- name: Setup Swift
|
||||||
|
uses: swift-actions/setup-swift@v1
|
||||||
|
with:
|
||||||
|
swift-version: "5.9"
|
||||||
|
|
||||||
|
- name: Build AudioTee binary
|
||||||
|
run: |
|
||||||
|
cd audiotee
|
||||||
|
swift build -c release
|
||||||
|
ls -la .build/release/
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
- name: Build AudioTee.js package
|
||||||
|
env:
|
||||||
|
AUDIOTEE_BINARY_PATH: ../audiotee/.build/release/audiotee
|
||||||
|
run: |
|
||||||
|
npm run build
|
||||||
|
ls -la bin/
|
||||||
|
|
||||||
|
- name: Test package
|
||||||
|
run: |
|
||||||
|
# Quick test to ensure the package loads and binary works
|
||||||
|
timeout 10s npm test quick || true
|
||||||
|
|
||||||
|
- name: Package binary for distribution
|
||||||
|
run: |
|
||||||
|
npm run package
|
||||||
|
|
||||||
|
- name: List package contents
|
||||||
|
run: |
|
||||||
|
ls -la build/
|
||||||
|
|
||||||
|
- name: Publish binary to GitHub releases
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
npm run publish-binary
|
||||||
|
|
||||||
|
publish-npm:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
run: |
|
||||||
|
# Update version to match release tag
|
||||||
|
npm version ${{ github.event.release.tag_name }} --no-git-tag-version
|
||||||
|
npm publish
|
||||||
|
|
||||||
|
test-installation:
|
||||||
|
needs: publish-npm
|
||||||
|
runs-on: macos-latest
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
|
|
||||||
|
- name: Test npm installation
|
||||||
|
run: |
|
||||||
|
# Test that the package can be installed and works
|
||||||
|
npm install audiotee-js@${{ github.event.release.tag_name }}
|
||||||
|
|
||||||
|
# Create a simple test
|
||||||
|
cat > test-install.js << 'EOF'
|
||||||
|
const { AudioTeeStream } = require('audiotee-js');
|
||||||
|
|
||||||
|
console.log('✅ Package imported successfully');
|
||||||
|
|
||||||
|
const stream = new AudioTeeStream({
|
||||||
|
format: 'json',
|
||||||
|
chunkDuration: 0.1
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ AudioTeeStream created successfully');
|
||||||
|
|
||||||
|
let metadataReceived = false;
|
||||||
|
|
||||||
|
stream.on('metadata', () => {
|
||||||
|
metadataReceived = true;
|
||||||
|
console.log('✅ Metadata received');
|
||||||
|
stream.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('error', (error) => {
|
||||||
|
console.error('❌ Error:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('close', () => {
|
||||||
|
if (metadataReceived) {
|
||||||
|
console.log('✅ Installation test passed!');
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.error('❌ No metadata received');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.error('❌ Test timeout');
|
||||||
|
stream.stop();
|
||||||
|
process.exit(1);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
console.log('🚀 Starting AudioTee...');
|
||||||
|
stream.start();
|
||||||
|
EOF
|
||||||
|
|
||||||
|
node test-install.js
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime
|
||||||
|
*.log
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Compiled binary
|
||||||
|
bin/
|
||||||
|
build/
|
||||||
|
lib-cov/
|
||||||
|
|
||||||
|
# Diagnostic reports
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Mac system files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Editor files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Test recordings
|
||||||
|
*.raw
|
||||||
|
*.wav
|
||||||
|
test-recordings/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Source control
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
.cursorrules
|
||||||
|
.github/
|
||||||
|
test/
|
||||||
|
coverage/
|
||||||
|
*.test.js
|
||||||
|
|
||||||
|
# Build artifacts (included via files in package.json)
|
||||||
|
build/
|
||||||
|
.build/
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Documentation (README.md is included via package.json files)
|
||||||
|
docs/
|
||||||
|
examples/
|
||||||
|
|
||||||
|
# Environment and config files
|
||||||
|
.env*
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Mac files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Test output
|
||||||
|
test-recordings/
|
||||||
|
*.raw
|
||||||
|
*.wav
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# CI/CD artifacts that aren't needed in the package
|
||||||
|
.github/workflows/
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
# AudioTee.js Development Guide
|
||||||
|
|
||||||
|
This guide covers setting up the development environment and workflow for AudioTee.js.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
audiotee-js/
|
||||||
|
├── package.json # npm package configuration with node-pre-gyp
|
||||||
|
├── index.js # Main entry point, uses node-pre-gyp to find binary
|
||||||
|
├── lib/
|
||||||
|
│ └── AudioTeeStream.js # Core streaming class
|
||||||
|
├── scripts/
|
||||||
|
│ └── build.js # Build script that copies AudioTee binary
|
||||||
|
├── test/
|
||||||
|
│ └── test.js # Interactive and automated tests
|
||||||
|
├── examples/
|
||||||
|
│ └── basic-usage.js # Usage examples and demos
|
||||||
|
├── .github/workflows/
|
||||||
|
│ └── release.yml # CI/CD for automated releases
|
||||||
|
└── README.md # User documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Initial Setup
|
||||||
|
|
||||||
|
### 1. Clone and Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <your-audiotee-js-repo>
|
||||||
|
cd audiotee-js
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Build AudioTee Binary
|
||||||
|
|
||||||
|
You'll need the AudioTee Swift project to build the binary:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Option A: If AudioTee is in parent directory (current setup)
|
||||||
|
cd ../audiotee
|
||||||
|
swift build -c release
|
||||||
|
cd ../audiotee-js
|
||||||
|
|
||||||
|
# Option B: If AudioTee is elsewhere, set the path
|
||||||
|
export AUDIOTEE_BINARY_PATH=/path/to/audiotee/.build/release/audiotee
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Build the Package
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
This copies the AudioTee binary to `bin/audiotee` and makes it executable.
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive test - requires audio playback
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Quick automated test
|
||||||
|
npm test quick
|
||||||
|
|
||||||
|
# Run examples
|
||||||
|
node examples/basic-usage.js
|
||||||
|
node examples/basic-usage.js 2 # Save to file example
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building for Different Architectures
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For Intel Macs (if you have access)
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# For Apple Silicon (if you have access)
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
npm run clean
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing the Package Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test the package as if installed from npm
|
||||||
|
npm pack
|
||||||
|
npm install -g audiotee-js-1.0.0.tgz
|
||||||
|
|
||||||
|
# Test in another directory
|
||||||
|
cd /tmp
|
||||||
|
node -e "const { AudioTeeStream } = require('audiotee-js'); console.log('✅ Works!')"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release Process
|
||||||
|
|
||||||
|
### 1. Prepare Release
|
||||||
|
|
||||||
|
1. Update version in `package.json`
|
||||||
|
2. Update `CHANGELOG.md` (if you add one)
|
||||||
|
3. Test thoroughly on both Intel and Apple Silicon if possible
|
||||||
|
4. Commit changes
|
||||||
|
|
||||||
|
### 2. Create GitHub Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v1.0.0
|
||||||
|
git push origin v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Then create a release on GitHub. This will trigger the automated build process.
|
||||||
|
|
||||||
|
### 3. Automated Release (via GitHub Actions)
|
||||||
|
|
||||||
|
The workflow will:
|
||||||
|
1. Build AudioTee binary for Intel and Apple Silicon
|
||||||
|
2. Package binaries using node-pre-gyp
|
||||||
|
3. Upload binaries to GitHub releases
|
||||||
|
4. Publish package to npm
|
||||||
|
5. Test the published package
|
||||||
|
|
||||||
|
### 4. Manual Release (if needed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and package
|
||||||
|
npm run build
|
||||||
|
npm run package
|
||||||
|
|
||||||
|
# Publish binary to GitHub releases
|
||||||
|
npm run publish-binary
|
||||||
|
|
||||||
|
# Publish to npm
|
||||||
|
npm publish
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
- `AUDIOTEE_BINARY_PATH` - Path to AudioTee binary for building
|
||||||
|
- `GITHUB_TOKEN` - For publishing binaries to GitHub releases
|
||||||
|
- `NODE_AUTH_TOKEN` - For publishing to npm
|
||||||
|
|
||||||
|
### node-pre-gyp Configuration
|
||||||
|
|
||||||
|
The binary distribution is configured in `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"binary": {
|
||||||
|
"module_name": "audiotee",
|
||||||
|
"module_path": "./bin/",
|
||||||
|
"remote_path": "v{version}/",
|
||||||
|
"package_name": "audiotee-v{version}-{platform}-{arch}.tar.gz",
|
||||||
|
"host": "https://github.com/your-org/audiotee-js/releases/download/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the `host` URL to match your repository.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Binary Not Found During Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if AudioTee is built
|
||||||
|
ls -la ../audiotee/.build/release/audiotee
|
||||||
|
|
||||||
|
# Or set custom path
|
||||||
|
export AUDIOTEE_BINARY_PATH=/path/to/your/audiotee/binary
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make sure binary is executable
|
||||||
|
chmod +x bin/audiotee
|
||||||
|
|
||||||
|
# Check binary works
|
||||||
|
./bin/audiotee --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### node-pre-gyp Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clear cache
|
||||||
|
npm run clean
|
||||||
|
rm -rf node_modules
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Debug node-pre-gyp
|
||||||
|
DEBUG=node-pre-gyp npm run package
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- Follow the patterns in `.cursorrules`
|
||||||
|
- Use functional programming where possible
|
||||||
|
- No semicolons (per project preference)
|
||||||
|
- Handle errors via EventEmitter, don't throw
|
||||||
|
- Use British English in documentation
|
||||||
|
- Comprehensive JSDoc for public APIs
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
Before releasing:
|
||||||
|
|
||||||
|
- [ ] Basic audio capture works
|
||||||
|
- [ ] Both JSON and binary formats work
|
||||||
|
- [ ] Sample rate conversion works
|
||||||
|
- [ ] Process filtering works (if testable)
|
||||||
|
- [ ] Error handling works (invalid args, missing binary, etc.)
|
||||||
|
- [ ] Package installs and works on clean system
|
||||||
|
- [ ] Examples in README work
|
||||||
|
- [ ] CI/CD builds successfully
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Test thoroughly
|
||||||
|
5. Update documentation
|
||||||
|
6. Submit a pull request
|
||||||
|
|
||||||
|
## Publishing Checklist
|
||||||
|
|
||||||
|
- [ ] Version updated in package.json
|
||||||
|
- [ ] Tests pass
|
||||||
|
- [ ] Documentation updated
|
||||||
|
- [ ] GitHub release created
|
||||||
|
- [ ] CI/CD completed successfully
|
||||||
|
- [ ] npm package published
|
||||||
|
- [ ] Installation test passes
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 AudioTee.js Contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
# AudioTee.js
|
||||||
|
|
||||||
|
Node.js wrapper for [AudioTee](https://github.com/your-org/audiotee) - capture macOS system audio using Core Audio taps.
|
||||||
|
|
||||||
|
AudioTee.js provides a streaming interface to capture system audio in real-time, perfect for building applications that need to process audio from any running application on macOS.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🎵 **Real-time system audio capture** using Core Audio taps
|
||||||
|
- 📦 **Streaming interface** with Node.js EventEmitter API
|
||||||
|
- ⚡ **High performance** binary protocol support
|
||||||
|
- 🎛️ **Flexible configuration** - sample rates, chunk sizes, process filtering
|
||||||
|
- 🔇 **Process-specific capture** - include/exclude specific applications
|
||||||
|
- 📊 **Audio metadata** - format information and level monitoring
|
||||||
|
- 🛡️ **Error handling** - graceful failure and process management
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **macOS 14.2+** (Sonoma or later)
|
||||||
|
- **Node.js 14+**
|
||||||
|
- **Audio recording permissions** (you'll be prompted on first use)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install audiotee-js
|
||||||
|
```
|
||||||
|
|
||||||
|
The package will automatically download the appropriate AudioTee binary for your system during installation.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { AudioTeeStream } = require('audiotee-js');
|
||||||
|
|
||||||
|
// Create a stream with 16kHz sample rate (great for ASR)
|
||||||
|
const stream = new AudioTeeStream({
|
||||||
|
sampleRate: 16000,
|
||||||
|
format: 'binary',
|
||||||
|
chunkDuration: 0.2
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for audio metadata
|
||||||
|
stream.on('metadata', (metadata) => {
|
||||||
|
console.log('Audio format:', metadata);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process audio chunks
|
||||||
|
stream.on('audio', (packet) => {
|
||||||
|
console.log(`Received ${packet.audioData.length} bytes of audio`);
|
||||||
|
// packet.audioData is a Buffer containing raw PCM data
|
||||||
|
// packet.timestamp, packet.duration, packet.peakAmplitude also available
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
|
stream.on('error', (error) => {
|
||||||
|
console.error('AudioTee error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start capturing
|
||||||
|
stream.start();
|
||||||
|
|
||||||
|
// Stop when done
|
||||||
|
// stream.stop();
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### AudioTeeStream
|
||||||
|
|
||||||
|
The main class for capturing system audio.
|
||||||
|
|
||||||
|
#### Constructor
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
new AudioTeeStream(options)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `format` | `string` | `'binary'` | Output format: `'json'`, `'binary'`, or `'auto'` |
|
||||||
|
| `sampleRate` | `number` | `undefined` | Target sample rate (8000, 16000, 22050, 24000, 32000, 44100, 48000) |
|
||||||
|
| `chunkDuration` | `number` | `0.2` | Audio chunk duration in seconds (max 5.0) |
|
||||||
|
| `includeProcesses` | `number[]` | `[]` | Process IDs to capture (empty = all processes) |
|
||||||
|
| `excludeProcesses` | `number[]` | `[]` | Process IDs to exclude |
|
||||||
|
| `mute` | `boolean` | `false` | Mute processes being captured |
|
||||||
|
| `binaryPath` | `string` | `auto` | Custom path to AudioTee binary |
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
|
||||||
|
- **`start()`** - Start audio capture, returns `this` for chaining
|
||||||
|
- **`stop()`** - Stop audio capture
|
||||||
|
- **`isActive()`** - Returns `true` if currently capturing
|
||||||
|
- **`getMetadata()`** - Returns audio metadata (available after `metadata` event)
|
||||||
|
|
||||||
|
#### Events
|
||||||
|
|
||||||
|
- **`metadata`** - Audio format information
|
||||||
|
- **`stream_start`** - Capture has started
|
||||||
|
- **`audio`** - Audio data packet
|
||||||
|
- **`stream_stop`** - Capture has stopped
|
||||||
|
- **`log`** - Log messages from AudioTee
|
||||||
|
- **`error`** - Error occurred
|
||||||
|
- **`close`** - Process has closed
|
||||||
|
|
||||||
|
### Audio Packet Format
|
||||||
|
|
||||||
|
Audio events receive packets with this structure:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
timestamp: Date, // When this audio was captured
|
||||||
|
duration: number, // Duration in seconds
|
||||||
|
peakAmplitude: number, // Peak amplitude (0.0 - 1.0)
|
||||||
|
audioData: Buffer // Raw PCM audio data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metadata Format
|
||||||
|
|
||||||
|
Metadata events provide audio format information:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
sample_rate: number, // e.g. 48000
|
||||||
|
channels_per_frame: number,// Always 1 (mono)
|
||||||
|
bits_per_channel: number, // e.g. 32
|
||||||
|
is_float: boolean, // true for float32, false for int16
|
||||||
|
encoding: string, // e.g. "pcm_f32le"
|
||||||
|
capture_mode: string, // "audio"
|
||||||
|
device_name: string|null, // Audio device name
|
||||||
|
device_uid: string|null // Audio device UID
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic Recording
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { AudioTeeStream } = require('audiotee-js');
|
||||||
|
|
||||||
|
const stream = new AudioTeeStream();
|
||||||
|
|
||||||
|
stream.on('metadata', console.log);
|
||||||
|
stream.on('audio', (packet) => {
|
||||||
|
console.log(`${packet.audioData.length} bytes, peak: ${packet.peakAmplitude}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Save to WAV File
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const fs = require('fs');
|
||||||
|
const { AudioTeeStream } = require('audiotee-js');
|
||||||
|
|
||||||
|
const stream = new AudioTeeStream({
|
||||||
|
sampleRate: 44100,
|
||||||
|
format: 'binary'
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = fs.createWriteStream('recording.raw');
|
||||||
|
|
||||||
|
stream.on('audio', (packet) => {
|
||||||
|
output.write(packet.audioData);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.start();
|
||||||
|
|
||||||
|
// Stop after 10 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
stream.stop();
|
||||||
|
output.end();
|
||||||
|
}, 10000);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Process-Specific Capture
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { AudioTeeStream } = require('audiotee-js');
|
||||||
|
|
||||||
|
// Only capture audio from Spotify (you'd need to find Spotify's PID)
|
||||||
|
const spotifyPID = 1234; // Use Activity Monitor or `pgrep Spotify`
|
||||||
|
|
||||||
|
const stream = new AudioTeeStream({
|
||||||
|
includeProcesses: [spotifyPID],
|
||||||
|
mute: true // Don't play through speakers
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('audio', (packet) => {
|
||||||
|
// Only Spotify's audio will be captured
|
||||||
|
console.log('Spotify audio:', packet.audioData.length, 'bytes');
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Real-time ASR Integration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { AudioTeeStream } = require('audiotee-js');
|
||||||
|
|
||||||
|
const stream = new AudioTeeStream({
|
||||||
|
sampleRate: 16000, // Common ASR sample rate
|
||||||
|
chunkDuration: 0.1, // Faster chunks for real-time
|
||||||
|
format: 'binary'
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('audio', async (packet) => {
|
||||||
|
// Send to your ASR service
|
||||||
|
const transcript = await sendToASR(packet.audioData);
|
||||||
|
if (transcript) {
|
||||||
|
console.log('Transcription:', transcript);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run the included test to verify everything works:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic interactive test
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Quick automated test
|
||||||
|
npm test quick
|
||||||
|
```
|
||||||
|
|
||||||
|
The test will capture audio for a few seconds and display statistics.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Permission Denied
|
||||||
|
|
||||||
|
AudioTee requires microphone permissions. You'll see a system dialog on first use - make sure to allow access.
|
||||||
|
|
||||||
|
### Binary Not Found
|
||||||
|
|
||||||
|
If you see "AudioTee binary not found", try rebuilding:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### No Audio Captured
|
||||||
|
|
||||||
|
- Check that audio is actually playing on your system
|
||||||
|
- Verify you have the latest macOS version (14.2+)
|
||||||
|
- Try running the Swift AudioTee directly to isolate the issue
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Building from Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone and build the parent AudioTee project first
|
||||||
|
git clone https://github.com/your-org/audiotee.git
|
||||||
|
cd audiotee
|
||||||
|
swift build -c release
|
||||||
|
|
||||||
|
# Then build the Node.js package
|
||||||
|
cd audiotee-js
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test # Run basic test
|
||||||
|
npm run lint # Check code style
|
||||||
|
npm run clean # Clean build artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Notes
|
||||||
|
|
||||||
|
- **Binary format** is more efficient than JSON for high-throughput applications
|
||||||
|
- **Lower chunk durations** increase CPU usage but reduce latency
|
||||||
|
- **Sample rate conversion** adds processing overhead - use native rates when possible
|
||||||
|
- The AudioTee binary uses real-time audio threads for minimal latency
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see [LICENSE](LICENSE) file.
|
||||||
|
|
||||||
|
## Related Projects
|
||||||
|
|
||||||
|
- [AudioTee](https://github.com/your-org/audiotee) - The underlying Swift CLI tool
|
||||||
|
- [node-core-audio](https://github.com/ZECTBynmo/node-core-audio) - Alternative Node.js audio library
|
||||||
|
- [AudioCap](https://github.com/insidegui/AudioCap) - macOS audio capture inspiration
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic AudioTee.js Usage Examples
|
||||||
|
*
|
||||||
|
* This file demonstrates common patterns for using AudioTee.js
|
||||||
|
* Run with: node examples/basic-usage.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { AudioTeeStream } = require("../index");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
// Example 1: Basic audio capture with console output
|
||||||
|
function basicCapture() {
|
||||||
|
console.log("=== Example 1: Basic Audio Capture ===\n");
|
||||||
|
|
||||||
|
const stream = new AudioTeeStream({
|
||||||
|
format: "binary",
|
||||||
|
sampleRate: 16000, // Good for speech recognition
|
||||||
|
chunkDuration: 0.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
let packetCount = 0;
|
||||||
|
let totalBytes = 0;
|
||||||
|
|
||||||
|
stream.on("metadata", (metadata) => {
|
||||||
|
console.log("🎵 Audio metadata:");
|
||||||
|
console.log(` Sample rate: ${metadata.sample_rate} Hz`);
|
||||||
|
console.log(` Encoding: ${metadata.encoding}`);
|
||||||
|
console.log("");
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("audio", (packet) => {
|
||||||
|
packetCount++;
|
||||||
|
totalBytes += packet.audioData.length;
|
||||||
|
|
||||||
|
// Show real-time stats
|
||||||
|
process.stdout.write(
|
||||||
|
`\r📊 Packets: ${packetCount}, Bytes: ${totalBytes}, Peak: ${packet.peakAmplitude.toFixed(
|
||||||
|
3
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("error", (error) => {
|
||||||
|
console.error("\n❌ Error:", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-stop after 5 seconds for demo
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log("\n\n✅ Stopping capture...");
|
||||||
|
stream.stop();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
console.log("🚀 Starting capture (will run for 5 seconds)...");
|
||||||
|
stream.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 2: Save audio to file
|
||||||
|
function saveToFile() {
|
||||||
|
console.log("\n=== Example 2: Save Audio to File ===\n");
|
||||||
|
|
||||||
|
const stream = new AudioTeeStream({
|
||||||
|
format: "binary",
|
||||||
|
sampleRate: 44100, // CD quality
|
||||||
|
chunkDuration: 0.1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputFile = "recording.raw";
|
||||||
|
const writeStream = fs.createWriteStream(outputFile);
|
||||||
|
|
||||||
|
stream.on("metadata", (metadata) => {
|
||||||
|
console.log(`💾 Saving ${metadata.encoding} audio to ${outputFile}`);
|
||||||
|
console.log(
|
||||||
|
` Format: ${metadata.sample_rate}Hz, ${metadata.bits_per_channel}-bit`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("audio", (packet) => {
|
||||||
|
// Write raw audio data to file
|
||||||
|
writeStream.write(packet.audioData);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("stream_stop", () => {
|
||||||
|
writeStream.end();
|
||||||
|
console.log(`\n✅ Saved audio to ${outputFile}`);
|
||||||
|
|
||||||
|
// Show file size
|
||||||
|
const stats = fs.statSync(outputFile);
|
||||||
|
console.log(`📈 File size: ${(stats.size / 1024).toFixed(1)} KB`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
stream.stop();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
console.log("🎵 Recording for 3 seconds...");
|
||||||
|
stream.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 3: Monitor audio levels (VU meter style)
|
||||||
|
function audioLevelMonitor() {
|
||||||
|
console.log("\n=== Example 3: Audio Level Monitor ===\n");
|
||||||
|
|
||||||
|
const stream = new AudioTeeStream({
|
||||||
|
format: "json", // JSON format for this example
|
||||||
|
chunkDuration: 0.05, // Fast updates for smooth level display
|
||||||
|
});
|
||||||
|
|
||||||
|
function drawLevelMeter(level) {
|
||||||
|
const maxBars = 20;
|
||||||
|
const bars = Math.floor(level * maxBars);
|
||||||
|
const meter = "█".repeat(bars) + "░".repeat(maxBars - bars);
|
||||||
|
const percentage = (level * 100).toFixed(1);
|
||||||
|
|
||||||
|
process.stdout.write(`\r🔊 ${meter} ${percentage}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.on("audio", (packet) => {
|
||||||
|
drawLevelMeter(packet.peakAmplitude);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run for 10 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log("\n\n✅ Level monitoring complete");
|
||||||
|
stream.stop();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
console.log("🎚️ Audio level monitor (10 seconds):");
|
||||||
|
console.log(" Play some music to see the levels!\n");
|
||||||
|
stream.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 4: Process-specific capture
|
||||||
|
function captureSpecificProcess() {
|
||||||
|
console.log("\n=== Example 4: Process-Specific Capture ===\n");
|
||||||
|
|
||||||
|
// This would capture only from a specific application
|
||||||
|
// You'd need to find the PID first: `pgrep "Music"` or Activity Monitor
|
||||||
|
|
||||||
|
const stream = new AudioTeeStream({
|
||||||
|
// includeProcesses: [1234], // Uncomment and set real PID
|
||||||
|
mute: true, // Don't play through speakers
|
||||||
|
format: "binary",
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("metadata", () => {
|
||||||
|
console.log(
|
||||||
|
"🎯 Capturing from specific process (demo mode - all processes)"
|
||||||
|
);
|
||||||
|
console.log(" To capture from specific app:");
|
||||||
|
console.log(' 1. Find PID: pgrep "App Name"');
|
||||||
|
console.log(" 2. Uncomment includeProcesses line above");
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("audio", (packet) => {
|
||||||
|
console.log(
|
||||||
|
`📦 Got ${packet.audioData.length} bytes from targeted process`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
stream.stop();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
stream.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run examples based on command line argument
|
||||||
|
const example = process.argv[2] || "1";
|
||||||
|
|
||||||
|
switch (example) {
|
||||||
|
case "1":
|
||||||
|
basicCapture();
|
||||||
|
break;
|
||||||
|
case "2":
|
||||||
|
saveToFile();
|
||||||
|
break;
|
||||||
|
case "3":
|
||||||
|
audioLevelMonitor();
|
||||||
|
break;
|
||||||
|
case "4":
|
||||||
|
captureSpecificProcess();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Usage: node basic-usage.js [1|2|3|4]");
|
||||||
|
console.log("");
|
||||||
|
console.log("Examples:");
|
||||||
|
console.log(" 1 - Basic audio capture");
|
||||||
|
console.log(" 2 - Save audio to file");
|
||||||
|
console.log(" 3 - Audio level monitor");
|
||||||
|
console.log(" 4 - Process-specific capture");
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const binary = require("node-pre-gyp");
|
||||||
|
|
||||||
|
// Get the path to the downloaded binary
|
||||||
|
const bindingPath = binary.find(
|
||||||
|
path.resolve(path.join(__dirname, "package.json"))
|
||||||
|
);
|
||||||
|
const binaryPath = path.join(path.dirname(bindingPath), "audiotee");
|
||||||
|
|
||||||
|
const AudioTeeStream = require("./lib/AudioTeeStream");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
AudioTeeStream,
|
||||||
|
getBinaryPath: () => binaryPath,
|
||||||
|
};
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
const { spawn } = require("child_process");
|
||||||
|
const { EventEmitter } = require("events");
|
||||||
|
const readline = require("readline");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AudioTeeStream - Node.js wrapper for AudioTee system audio capture
|
||||||
|
*
|
||||||
|
* Events:
|
||||||
|
* - 'metadata': Audio format information
|
||||||
|
* - 'stream_start': Recording has started
|
||||||
|
* - 'audio': Audio data chunk { timestamp, duration, peakAmplitude, audioData }
|
||||||
|
* - 'stream_stop': Recording has stopped
|
||||||
|
* - 'log': Log messages { level, message, context }
|
||||||
|
* - 'error': Errors
|
||||||
|
* - 'close': Process has closed
|
||||||
|
*/
|
||||||
|
class AudioTeeStream extends EventEmitter {
|
||||||
|
constructor(options = {}) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Binary path - use provided path or get from main module
|
||||||
|
this.binaryPath =
|
||||||
|
options.binaryPath ||
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
return require("../index").getBinaryPath();
|
||||||
|
} catch {
|
||||||
|
throw new Error(
|
||||||
|
"AudioTee binary path not available. Ensure package is properly installed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// AudioTee options
|
||||||
|
this.format = options.format || "binary"; // 'json', 'binary', or 'auto'
|
||||||
|
this.sampleRate = options.sampleRate;
|
||||||
|
this.chunkDuration = options.chunkDuration || 0.2;
|
||||||
|
this.includeProcesses = options.includeProcesses || [];
|
||||||
|
this.excludeProcesses = options.excludeProcesses || [];
|
||||||
|
this.mute = options.mute || false;
|
||||||
|
|
||||||
|
// Internal state
|
||||||
|
this.process = null;
|
||||||
|
this.metadata = null;
|
||||||
|
this.isStarted = false;
|
||||||
|
|
||||||
|
// Binary format state
|
||||||
|
this.pendingAudioMeta = null;
|
||||||
|
this.expectedBytes = 0;
|
||||||
|
this.binaryBuffer = Buffer.alloc(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start audio capture
|
||||||
|
* @returns {AudioTeeStream} this instance for chaining
|
||||||
|
*/
|
||||||
|
start() {
|
||||||
|
if (this.isStarted) {
|
||||||
|
throw new Error("AudioTeeStream is already started");
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = this.buildArguments();
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.process = spawn(this.binaryPath, args, {
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.emit(
|
||||||
|
"error",
|
||||||
|
new Error(`Failed to start AudioTee: ${error.message}`)
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isStarted = true;
|
||||||
|
this.setupProtocolHandling();
|
||||||
|
this.setupProcessHandlers();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop audio capture
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
if (this.process && !this.process.killed) {
|
||||||
|
this.process.kill("SIGTERM");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the stream is currently active
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isActive() {
|
||||||
|
return this.isStarted && this.process && !this.process.killed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current audio metadata (available after 'metadata' event)
|
||||||
|
* @returns {Object|null}
|
||||||
|
*/
|
||||||
|
getMetadata() {
|
||||||
|
return this.metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private methods
|
||||||
|
|
||||||
|
buildArguments() {
|
||||||
|
const args = [`--format=${this.format}`];
|
||||||
|
|
||||||
|
if (this.sampleRate) {
|
||||||
|
args.push(`--sample-rate=${this.sampleRate}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.chunkDuration !== 0.2) {
|
||||||
|
args.push(`--chunk-duration=${this.chunkDuration}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.includeProcesses.length) {
|
||||||
|
args.push(`--include-processes=${this.includeProcesses.join(" ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.excludeProcesses.length) {
|
||||||
|
args.push(`--exclude-processes=${this.excludeProcesses.join(" ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mute) {
|
||||||
|
args.push("--mute");
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupProtocolHandling() {
|
||||||
|
if (this.format === "binary") {
|
||||||
|
this.setupBinaryProtocol();
|
||||||
|
} else {
|
||||||
|
this.setupJSONProtocol();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupJSONProtocol() {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: this.process.stdout,
|
||||||
|
crlfDelay: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.on("line", (line) => this.handleJSONLine(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
setupBinaryProtocol() {
|
||||||
|
// For binary format, we need to handle both JSON lines and raw binary data
|
||||||
|
let lineBuffer = "";
|
||||||
|
let inJsonMode = true;
|
||||||
|
|
||||||
|
this.process.stdout.on("data", (chunk) => {
|
||||||
|
if (inJsonMode) {
|
||||||
|
// Look for complete JSON lines
|
||||||
|
lineBuffer += chunk.toString();
|
||||||
|
|
||||||
|
let newlineIndex;
|
||||||
|
while ((newlineIndex = lineBuffer.indexOf("\n")) !== -1) {
|
||||||
|
const line = lineBuffer.slice(0, newlineIndex);
|
||||||
|
lineBuffer = lineBuffer.slice(newlineIndex + 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(line);
|
||||||
|
if (message.message_type === "audio" && this.format === "binary") {
|
||||||
|
// Prepare for binary data
|
||||||
|
this.expectedBytes = message.data.audio_length;
|
||||||
|
this.pendingAudioMeta = {
|
||||||
|
timestamp: new Date(message.data.timestamp),
|
||||||
|
duration: message.data.duration,
|
||||||
|
peakAmplitude: message.data.peak_amplitude,
|
||||||
|
};
|
||||||
|
inJsonMode = false;
|
||||||
|
} else {
|
||||||
|
this.handleJSONMessage(message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.emit(
|
||||||
|
"error",
|
||||||
|
new Error(`Failed to parse JSON: ${error.message}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We're expecting binary audio data
|
||||||
|
this.binaryBuffer = Buffer.concat([this.binaryBuffer, chunk]);
|
||||||
|
|
||||||
|
if (this.binaryBuffer.length >= this.expectedBytes) {
|
||||||
|
// Extract the audio data
|
||||||
|
const audioData = this.binaryBuffer.slice(0, this.expectedBytes);
|
||||||
|
this.binaryBuffer = this.binaryBuffer.slice(this.expectedBytes);
|
||||||
|
|
||||||
|
// Emit the audio event
|
||||||
|
this.emit("audio", {
|
||||||
|
...this.pendingAudioMeta,
|
||||||
|
audioData,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
this.expectedBytes = 0;
|
||||||
|
this.pendingAudioMeta = null;
|
||||||
|
inJsonMode = true;
|
||||||
|
|
||||||
|
// Process any remaining data as JSON
|
||||||
|
if (this.binaryBuffer.length > 0) {
|
||||||
|
lineBuffer += this.binaryBuffer.toString();
|
||||||
|
this.binaryBuffer = Buffer.alloc(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleJSONLine(line) {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(line);
|
||||||
|
this.handleJSONMessage(message);
|
||||||
|
} catch (error) {
|
||||||
|
this.emit("error", new Error(`Failed to parse JSON: ${error.message}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleJSONMessage(message) {
|
||||||
|
switch (message.message_type) {
|
||||||
|
case "metadata":
|
||||||
|
this.metadata = message.data;
|
||||||
|
this.emit("metadata", this.metadata);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "stream_start":
|
||||||
|
this.emit("stream_start");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "audio":
|
||||||
|
if (this.format === "json") {
|
||||||
|
// JSON format - audio data is base64 encoded
|
||||||
|
const audioBuffer = Buffer.from(message.data.audio_data, "base64");
|
||||||
|
this.emit("audio", {
|
||||||
|
timestamp: new Date(message.data.timestamp),
|
||||||
|
duration: message.data.duration,
|
||||||
|
peakAmplitude: message.data.peak_amplitude,
|
||||||
|
audioData: audioBuffer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Binary format audio is handled in setupBinaryProtocol
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "stream_stop":
|
||||||
|
this.emit("stream_stop");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "info":
|
||||||
|
case "error":
|
||||||
|
case "debug":
|
||||||
|
this.emit("log", {
|
||||||
|
level: message.message_type,
|
||||||
|
message: message.data?.message || "Unknown log message",
|
||||||
|
context: message.data?.context,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.emit("log", {
|
||||||
|
level: "debug",
|
||||||
|
message: `Unknown message type: ${message.message_type}`,
|
||||||
|
context: { raw_message: message },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupProcessHandlers() {
|
||||||
|
this.process.stderr.on("data", (data) => {
|
||||||
|
// AudioTee should not write to stderr in normal operation
|
||||||
|
this.emit("log", {
|
||||||
|
level: "error",
|
||||||
|
message: "AudioTee stderr output",
|
||||||
|
context: { output: data.toString().trim() },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.process.on("close", (code, signal) => {
|
||||||
|
this.isStarted = false;
|
||||||
|
this.emit("close", { code, signal });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.process.on("error", (error) => {
|
||||||
|
this.isStarted = false;
|
||||||
|
this.emit("error", new Error(`AudioTee process error: ${error.message}`));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AudioTeeStream;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"name": "audiotee-js",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Node.js wrapper for AudioTee - capture macOS system audio using Core Audio taps",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"install": "node-pre-gyp install --fallback-to-build",
|
||||||
|
"build": "node scripts/build.js",
|
||||||
|
"test": "node test/test.js",
|
||||||
|
"package": "node-pre-gyp package",
|
||||||
|
"publish-binary": "node-pre-gyp publish",
|
||||||
|
"clean": "node-pre-gyp clean",
|
||||||
|
"lint": "eslint lib/ scripts/ test/ index.js",
|
||||||
|
"prepack": "npm run build"
|
||||||
|
},
|
||||||
|
"binary": {
|
||||||
|
"module_name": "audiotee",
|
||||||
|
"module_path": "./bin/",
|
||||||
|
"remote_path": "v{version}/",
|
||||||
|
"package_name": "audiotee-v{version}-{platform}-{arch}.tar.gz",
|
||||||
|
"host": "https://github.com/your-org/audiotee-js/releases/download/",
|
||||||
|
"napi_versions": []
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"node-pre-gyp": "^0.17.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.0.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"index.js",
|
||||||
|
"lib/",
|
||||||
|
"scripts/build.js",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"os": ["darwin"],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"audio",
|
||||||
|
"macos",
|
||||||
|
"recording",
|
||||||
|
"system-audio",
|
||||||
|
"core-audio",
|
||||||
|
"streaming",
|
||||||
|
"real-time",
|
||||||
|
"asr",
|
||||||
|
"speech-recognition"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/your-org/audiotee-js.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/your-org/audiotee-js/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/your-org/audiotee-js#readme",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "Your Name <your.email@example.com>"
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const os = require("os");
|
||||||
|
|
||||||
|
// Default to parent directory (when in same repo), allow override via env var
|
||||||
|
const DEFAULT_BINARY_PATH = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
".build",
|
||||||
|
"release",
|
||||||
|
"audiotee"
|
||||||
|
);
|
||||||
|
const AUDIOTEE_BINARY_PATH =
|
||||||
|
process.env.AUDIOTEE_BINARY_PATH || DEFAULT_BINARY_PATH;
|
||||||
|
|
||||||
|
function build() {
|
||||||
|
const arch = os.arch();
|
||||||
|
const platform = os.platform();
|
||||||
|
|
||||||
|
console.log(`Building for platform: ${platform}, architecture: ${arch}`);
|
||||||
|
|
||||||
|
if (platform !== "darwin") {
|
||||||
|
throw new Error("AudioTee only supports macOS");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create bin directory
|
||||||
|
const binDir = path.join(__dirname, "..", "bin");
|
||||||
|
if (!fs.existsSync(binDir)) {
|
||||||
|
fs.mkdirSync(binDir, { recursive: true });
|
||||||
|
console.log(`Created bin directory: ${binDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the source binary path
|
||||||
|
const sourcePath = path.resolve(AUDIOTEE_BINARY_PATH);
|
||||||
|
const targetPath = path.join(binDir, "audiotee");
|
||||||
|
|
||||||
|
console.log(`Looking for AudioTee binary at: ${sourcePath}`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(sourcePath)) {
|
||||||
|
console.error(`AudioTee binary not found at: ${sourcePath}`);
|
||||||
|
console.error("Please ensure AudioTee is built first:");
|
||||||
|
console.error(" cd ../audiotee && swift build -c release");
|
||||||
|
console.error("Or set AUDIOTEE_BINARY_PATH environment variable");
|
||||||
|
throw new Error(`AudioTee binary not found at: ${sourcePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Copying AudioTee binary from ${sourcePath} to ${targetPath}`);
|
||||||
|
fs.copyFileSync(sourcePath, targetPath);
|
||||||
|
|
||||||
|
// Make executable
|
||||||
|
fs.chmodSync(targetPath, 0o755);
|
||||||
|
|
||||||
|
// Verify the binary works
|
||||||
|
console.log("Verifying binary...");
|
||||||
|
const { execSync } = require("child_process");
|
||||||
|
try {
|
||||||
|
execSync(`"${targetPath}" --help`, { stdio: "pipe" });
|
||||||
|
console.log("Binary verification successful");
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Binary verification failed, but continuing...");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Build completed successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
function clean() {
|
||||||
|
const binDir = path.join(__dirname, "..", "bin");
|
||||||
|
if (fs.existsSync(binDir)) {
|
||||||
|
fs.rmSync(binDir, { recursive: true, force: true });
|
||||||
|
console.log("Cleaned bin directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
const command = process.argv[2];
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "clean":
|
||||||
|
clean();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { build, clean };
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const { AudioTeeStream } = require("../index");
|
||||||
|
|
||||||
|
function runBasicTest() {
|
||||||
|
console.log("=== AudioTee.js Basic Test ===\n");
|
||||||
|
|
||||||
|
const stream = new AudioTeeStream({
|
||||||
|
format: "binary",
|
||||||
|
sampleRate: 16000,
|
||||||
|
chunkDuration: 0.1,
|
||||||
|
});
|
||||||
|
|
||||||
|
let audioPacketCount = 0;
|
||||||
|
let totalAudioBytes = 0;
|
||||||
|
|
||||||
|
stream.on("metadata", (metadata) => {
|
||||||
|
console.log("📊 Audio Metadata:");
|
||||||
|
console.log(` Sample Rate: ${metadata.sample_rate} Hz`);
|
||||||
|
console.log(` Channels: ${metadata.channels_per_frame}`);
|
||||||
|
console.log(` Bits per Channel: ${metadata.bits_per_channel}`);
|
||||||
|
console.log(` Encoding: ${metadata.encoding}`);
|
||||||
|
console.log(` Float: ${metadata.is_float}\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("stream_start", () => {
|
||||||
|
console.log("🎵 Audio stream started\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("audio", (packet) => {
|
||||||
|
audioPacketCount++;
|
||||||
|
totalAudioBytes += packet.audioData.length;
|
||||||
|
|
||||||
|
process.stdout.write(
|
||||||
|
`\r📦 Packets: ${audioPacketCount} | Audio bytes: ${totalAudioBytes} | Peak: ${packet.peakAmplitude.toFixed(
|
||||||
|
3
|
||||||
|
)} | Duration: ${packet.duration.toFixed(3)}s`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("stream_stop", () => {
|
||||||
|
console.log("\n\n🛑 Audio stream stopped");
|
||||||
|
console.log(
|
||||||
|
`📈 Final stats: ${audioPacketCount} packets, ${totalAudioBytes} bytes total\n`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("log", (log) => {
|
||||||
|
if (log.level === "error") {
|
||||||
|
console.error(`\n❌ ${log.level.toUpperCase()}: ${log.message}`);
|
||||||
|
if (log.context) {
|
||||||
|
console.error(` Context:`, log.context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("error", (error) => {
|
||||||
|
console.error(`\n💥 Error: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("close", ({ code, signal }) => {
|
||||||
|
console.log(
|
||||||
|
`👋 AudioTee process closed (code: ${code}, signal: ${signal})`
|
||||||
|
);
|
||||||
|
process.exit(code || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle Ctrl+C gracefully
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
console.log("\n\n🛑 Received SIGINT, stopping AudioTee...");
|
||||||
|
stream.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🚀 Starting AudioTee stream...");
|
||||||
|
console.log("💡 Play some audio and watch the packets stream in!");
|
||||||
|
console.log("⏹️ Press Ctrl+C to stop\n");
|
||||||
|
|
||||||
|
try {
|
||||||
|
stream.start();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to start: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runQuickTest() {
|
||||||
|
console.log("=== AudioTee.js Quick Test ===\n");
|
||||||
|
|
||||||
|
const stream = new AudioTeeStream({
|
||||||
|
format: "json",
|
||||||
|
chunkDuration: 0.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
let packetCount = 0;
|
||||||
|
const maxPackets = 5;
|
||||||
|
|
||||||
|
stream.on("metadata", (metadata) => {
|
||||||
|
console.log("✅ Received metadata:", metadata);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("audio", () => {
|
||||||
|
packetCount++;
|
||||||
|
console.log(`✅ Received audio packet ${packetCount}/${maxPackets}`);
|
||||||
|
|
||||||
|
if (packetCount >= maxPackets) {
|
||||||
|
console.log("✅ Quick test complete!");
|
||||||
|
stream.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("error", (error) => {
|
||||||
|
console.error("❌ Test failed:", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("close", () => {
|
||||||
|
console.log("👋 Test finished");
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log("⏰ Test timeout - AudioTee might not be working");
|
||||||
|
stream.stop();
|
||||||
|
process.exit(1);
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
console.log("🚀 Running quick test (capturing 5 audio packets)...");
|
||||||
|
stream.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the appropriate test based on command line args
|
||||||
|
const testType = process.argv[2] || "basic";
|
||||||
|
|
||||||
|
switch (testType) {
|
||||||
|
case "quick":
|
||||||
|
runQuickTest();
|
||||||
|
break;
|
||||||
|
case "basic":
|
||||||
|
default:
|
||||||
|
runBasicTest();
|
||||||
|
break;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user