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