simple-beatmaker
Version:
Generate drum sounds from scratch and create beats programmatically with pure JavaScript - no external WAV files needed!
731 lines (642 loc) ⢠26 kB
JavaScript
const fs = require('fs');
const path = require('path');
// Preset patterns that can be selected by name
const presetPatterns = {
'basic-rock': {
name: 'Basic Rock',
description: 'Classic 4/4 rock beat',
bars: 4,
pattern: [
[
['kick'], null, ['hihat'], null,
['snare'], null, ['hihat'], null,
['kick'], null, ['hihat'], null,
['snare'], null, ['hihat'], null
],
[
['kick'], null, ['hihat'], null,
['snare'], null, ['hihat'], null,
['kick'], null, ['hihat'], null,
['snare'], null, ['hihat'], null
],
[
['kick'], null, ['hihat'], null,
['snare'], null, ['hihat'], null,
['kick'], null, ['hihat'], null,
['snare'], null, ['hihat'], null
],
[
['kick'], null, ['hihat'], null,
['snare'], ['snare'], ['hihat'], null,
['kick'], ['kick'], ['hihat'], null,
['snare'], null, ['hihat'], ['kick']
]
]
},
'electronic-dance': {
name: 'Electronic Dance',
description: 'Electronic dance pattern with sharp beats',
bars: 2,
pattern: [
[
['kick'], null, null, null,
['kick'], null, ['hihat'], null,
['kick'], null, null, null,
['kick'], null, ['hihat'], null
],
[
['kick'], null, ['snare'], null,
['kick'], null, ['snare'], ['hihat'],
['kick'], null, ['snare'], null,
['kick'], ['snare'], ['hihat'], null
]
]
},
'vintage-groove': {
name: 'Vintage Groove',
description: 'Laid-back vintage drum groove',
bars: 4,
pattern: [
[
['kick'], null, null, ['hihat'],
null, ['snare'], null, ['hihat'],
['kick'], null, null, ['hihat'],
null, ['snare'], null, ['hihat']
],
[
['kick'], null, null, ['hihat'],
null, ['snare'], null, ['hihat'],
['kick'], null, ['kick'], ['hihat'],
null, ['snare'], null, ['hihat']
],
[
['kick'], null, null, ['hihat'],
null, ['snare'], null, ['hihat'],
['kick'], null, null, ['hihat'],
null, ['snare'], ['snare'], ['hihat']
],
[
['kick'], null, null, ['hihat'],
['snare'], null, ['snare'], null,
['kick'], ['kick'], null, ['hihat'],
['snare'], null, ['hihat'], null
]
]
},
'funky-break': {
name: 'Funky Break',
description: 'Syncopated funk pattern',
bars: 2,
pattern: [
[
['kick'], null, null, ['hihat'],
null, ['snare'], ['kick'], null,
null, ['hihat'], ['kick'], null,
['snare'], null, ['hihat'], null
],
[
['kick'], null, null, ['hihat'],
null, ['snare'], ['kick'], ['hihat'],
null, null, ['kick'], ['snare'],
null, ['snare'], ['hihat'], ['kick']
]
]
},
'minimal-techno': {
name: 'Minimal Techno',
description: 'Minimal techno beat',
bars: 4,
pattern: [
[
['kick'], null, null, null,
null, null, null, null,
['kick'], null, null, null,
null, null, null, null
],
[
['kick'], null, null, null,
null, null, null, null,
['kick'], null, null, null,
null, null, ['hihat'], null
],
[
['kick'], null, null, null,
null, null, ['snare'], null,
['kick'], null, null, null,
null, null, null, null
],
[
['kick'], null, null, null,
null, null, ['snare'], null,
['kick'], null, null, ['hihat'],
null, null, ['hihat'], ['hihat']
]
]
}
};
class SimpleBeatmaker {
constructor() {
this.bpm = 120;
this.sampleRate = 44100;
this.channels = 2;
this.bitDepth = 16;
this.isPlaying = false;
this.currentDrumSet = 'classic'; // Default drum set
this.outputDir = null; // Default: current directory
}
setBPM(bpm) {
this.bpm = bpm;
return this;
}
// Set the drum set to use
setDrumSet(drumSetName) {
const availableSets = ['classic', 'electronic', 'vintage'];
if (availableSets.includes(drumSetName.toLowerCase())) {
this.currentDrumSet = drumSetName.toLowerCase();
} else {
console.warn(`Unknown drum set "${drumSetName}". Available sets: ${availableSets.join(', ')}`);
}
return this;
}
// Set the output directory for WAV files
setOutputDir(directory) {
if (directory) {
// Resolve the path and create directory if it doesn't exist
const resolvedPath = path.resolve(directory);
if (!fs.existsSync(resolvedPath)) {
fs.mkdirSync(resolvedPath, { recursive: true });
console.log(`š Created output directory: ${resolvedPath}`);
}
this.outputDir = resolvedPath;
console.log(`š Output directory set to: ${resolvedPath}`);
} else {
this.outputDir = null;
console.log('š Output directory reset to current directory');
}
return this;
}
// Get available drum sets
getAvailableDrumSets() {
return {
classic: 'Classic acoustic drum sounds with warm tones',
electronic: 'Electronic/synthetic drum sounds with sharp attack',
vintage: 'Vintage-style drums with analog character'
};
}
// Get available preset patterns
getAvailablePresets() {
const presets = {};
Object.keys(presetPatterns).forEach(key => {
presets[key] = {
name: presetPatterns[key].name,
description: presetPatterns[key].description,
bars: presetPatterns[key].bars
};
});
return presets;
}
// Use a preset pattern
usePreset(presetName) {
if (presetPatterns[presetName]) {
const preset = presetPatterns[presetName];
this.bars = preset.bars;
this.pattern = preset.pattern;
console.log(`ā Using preset: ${preset.name} (${preset.bars} bars)`);
} else {
throw new Error(`Preset "${presetName}" not found. Available presets: ${Object.keys(presetPatterns).join(', ')}`);
}
return this;
}
// Create a custom pattern
createPattern(bars, pattern) {
this.bars = bars;
this.pattern = pattern;
console.log(`ā Custom pattern created (${bars} bars)`);
return this;
}
// Generate kick drum sound based on current drum set
generateKick(duration = 0.5) {
const samples = Math.floor(this.sampleRate * duration);
const buffer = Buffer.alloc(samples * this.channels * (this.bitDepth / 8));
for (let i = 0; i < samples; i++) {
const t = i / this.sampleRate;
let sample = 0;
switch (this.currentDrumSet) {
case 'classic':
// Classic kick: Deep, warm low frequency
const frequency = 50 * Math.exp(-t * 25);
const amplitude = Math.exp(-t * 6) * 0.9;
sample = Math.sin(2 * Math.PI * frequency * t) * amplitude;
// Add subtle click
const click = Math.exp(-t * 80) * 0.2 * Math.random();
sample += click;
break;
case 'electronic':
// Electronic kick: Sharp attack, synthetic
const freq = 60 * Math.exp(-t * 35);
const amp = Math.exp(-t * 12) * 0.8;
sample = Math.sin(2 * Math.PI * freq * t) * amp;
// Add sharp click and sub-bass
const sharpClick = Math.exp(-t * 150) * 0.4;
const subBass = Math.sin(2 * Math.PI * 40 * t) * Math.exp(-t * 4) * 0.3;
sample += sharpClick + subBass;
break;
case 'vintage':
// Vintage kick: Warmer, slightly distorted
const vintageFreq = 55 * Math.exp(-t * 20);
const vintageAmp = Math.exp(-t * 5) * 0.85;
sample = Math.sin(2 * Math.PI * vintageFreq * t) * vintageAmp;
// Add harmonic distortion
sample += Math.sin(2 * Math.PI * vintageFreq * 2 * t) * vintageAmp * 0.1;
// Softer click
const softClick = Math.exp(-t * 60) * 0.15 * Math.random();
sample += softClick;
break;
}
const finalSample = Math.max(-1, Math.min(1, sample));
const intSample = Math.floor(finalSample * 32767);
buffer.writeInt16LE(intSample, i * 4);
buffer.writeInt16LE(intSample, i * 4 + 2);
}
return buffer;
}
// Generate snare drum sound based on current drum set
generateSnare(duration = 0.2) {
const samples = Math.floor(this.sampleRate * duration);
const buffer = Buffer.alloc(samples * this.channels * (this.bitDepth / 8));
for (let i = 0; i < samples; i++) {
const t = i / this.sampleRate;
let sample = 0;
switch (this.currentDrumSet) {
case 'classic':
// Classic snare: Natural noise with tone
const noise = (Math.random() - 0.5) * 2;
const tone = Math.sin(2 * Math.PI * 200 * t);
const amplitude = Math.exp(-t * 12) * 0.7;
sample = (noise * 0.8 + tone * 0.2) * amplitude;
break;
case 'electronic':
// Electronic snare: Sharp, synthetic
const electroNoise = (Math.random() - 0.5) * 2;
const electroTone = Math.sin(2 * Math.PI * 300 * t) + Math.sin(2 * Math.PI * 150 * t);
const electroAmp = Math.exp(-t * 20) * 0.6;
sample = (electroNoise * 0.6 + electroTone * 0.4) * electroAmp;
// Add digital-style click
sample += Math.exp(-t * 100) * 0.3;
break;
case 'vintage':
// Vintage snare: Warm, slightly compressed
const vintageNoise = (Math.random() - 0.5) * 1.8;
const vintageTone = Math.sin(2 * Math.PI * 180 * t);
const vintageAmp = Math.exp(-t * 10) * 0.75;
sample = (vintageNoise * 0.7 + vintageTone * 0.3) * vintageAmp;
// Add vintage-style saturation
sample = Math.tanh(sample * 1.5) * 0.8;
break;
}
const finalSample = Math.max(-1, Math.min(1, sample));
const intSample = Math.floor(finalSample * 32767);
buffer.writeInt16LE(intSample, i * 4);
buffer.writeInt16LE(intSample, i * 4 + 2);
}
return buffer;
}
// Generate hihat sound based on current drum set
generateHihat(duration = 0.1) {
const samples = Math.floor(this.sampleRate * duration);
const buffer = Buffer.alloc(samples * this.channels * (this.bitDepth / 8));
for (let i = 0; i < samples; i++) {
const t = i / this.sampleRate;
let sample = 0;
switch (this.currentDrumSet) {
case 'classic':
// Classic hihat: Natural high-frequency content
const noise = (Math.random() - 0.5) * 2;
const highFreq = Math.sin(2 * Math.PI * 8000 * t) * Math.random();
const amplitude = Math.exp(-t * 35) * 0.4;
sample = (noise + highFreq) * amplitude;
break;
case 'electronic':
// Electronic hihat: Sharp, digital
const electroNoise = (Math.random() - 0.5) * 2;
const digitalFreq = Math.sin(2 * Math.PI * 12000 * t) * Math.random();
const electroAmp = Math.exp(-t * 50) * 0.35;
sample = (electroNoise * 0.7 + digitalFreq * 0.3) * electroAmp;
// Add metallic ring
sample += Math.sin(2 * Math.PI * 15000 * t) * Math.exp(-t * 60) * 0.1;
break;
case 'vintage':
// Vintage hihat: Warmer, filtered
const vintageNoise = (Math.random() - 0.5) * 1.8;
const filteredFreq = Math.sin(2 * Math.PI * 6000 * t) * Math.random();
const vintageAmp = Math.exp(-t * 30) * 0.45;
sample = (vintageNoise * 0.8 + filteredFreq * 0.2) * vintageAmp;
break;
}
const finalSample = Math.max(-1, Math.min(1, sample));
const intSample = Math.floor(finalSample * 32767);
buffer.writeInt16LE(intSample, i * 4);
buffer.writeInt16LE(intSample, i * 4 + 2);
}
return buffer;
}
// Generate clap sound based on current drum set
generateClap(duration = 0.15) {
const samples = Math.floor(this.sampleRate * duration);
const buffer = Buffer.alloc(samples * this.channels * (this.bitDepth / 8));
for (let i = 0; i < samples; i++) {
const t = i / this.sampleRate;
let sample = 0;
switch (this.currentDrumSet) {
case 'classic':
// Classic clap: Multiple burst noise pattern
const burst1 = t < 0.01 ? (Math.random() - 0.5) * Math.exp(-t * 100) : 0;
const burst2 = (t > 0.01 && t < 0.02) ? (Math.random() - 0.5) * Math.exp(-(t-0.01) * 80) : 0;
const burst3 = (t > 0.02 && t < 0.04) ? (Math.random() - 0.5) * Math.exp(-(t-0.02) * 60) : 0;
const tail = t > 0.04 ? (Math.random() - 0.5) * Math.exp(-t * 20) * 0.3 : 0;
sample = (burst1 + burst2 + burst3 + tail) * 0.6;
break;
case 'electronic':
// Electronic clap: Sharp digital bursts
const eBurst1 = t < 0.005 ? (Math.random() - 0.5) * Math.exp(-t * 200) : 0;
const eBurst2 = (t > 0.005 && t < 0.015) ? (Math.random() - 0.5) * Math.exp(-(t-0.005) * 120) : 0;
const eBurst3 = (t > 0.015 && t < 0.03) ? (Math.random() - 0.5) * Math.exp(-(t-0.015) * 80) : 0;
const eTail = t > 0.03 ? (Math.random() - 0.5) * Math.exp(-t * 30) * 0.2 : 0;
// Add some digital processing
sample = (eBurst1 + eBurst2 + eBurst3 + eTail) * 0.5;
sample = Math.tanh(sample * 2) * 0.7; // Digital saturation
break;
case 'vintage':
// Vintage clap: Warmer, more compressed
const vBurst1 = t < 0.015 ? (Math.random() - 0.5) * Math.exp(-t * 80) : 0;
const vBurst2 = (t > 0.015 && t < 0.025) ? (Math.random() - 0.5) * Math.exp(-(t-0.015) * 60) : 0;
const vBurst3 = (t > 0.025 && t < 0.05) ? (Math.random() - 0.5) * Math.exp(-(t-0.025) * 40) : 0;
const vTail = t > 0.05 ? (Math.random() - 0.5) * Math.exp(-t * 15) * 0.4 : 0;
sample = (vBurst1 + vBurst2 + vBurst3 + vTail) * 0.65;
// Vintage-style compression
sample = Math.tanh(sample * 1.2) * 0.8;
break;
}
const finalSample = Math.max(-1, Math.min(1, sample));
const intSample = Math.floor(finalSample * 32767);
buffer.writeInt16LE(intSample, i * 4);
buffer.writeInt16LE(intSample, i * 4 + 2);
}
return buffer;
}
// Generate crash cymbal sound based on current drum set
generateCrash(duration = 1.0) {
const samples = Math.floor(this.sampleRate * duration);
const buffer = Buffer.alloc(samples * this.channels * (this.bitDepth / 8));
for (let i = 0; i < samples; i++) {
const t = i / this.sampleRate;
let sample = 0;
switch (this.currentDrumSet) {
case 'classic':
// Classic crash: Complex metallic harmonics
const noise = (Math.random() - 0.5) * 2;
const metallic1 = Math.sin(2 * Math.PI * 4000 * t) * Math.random();
const metallic2 = Math.sin(2 * Math.PI * 6000 * t) * Math.random();
const metallic3 = Math.sin(2 * Math.PI * 8000 * t) * Math.random();
const shimmer = Math.sin(2 * Math.PI * 12000 * t) * Math.random() * 0.5;
const amplitude = Math.exp(-t * 2) * 0.6;
sample = (noise * 0.6 + metallic1 * 0.2 + metallic2 * 0.15 + metallic3 * 0.1 + shimmer) * amplitude;
break;
case 'electronic':
// Electronic crash: Digital shimmer with controlled harmonics
const eNoise = (Math.random() - 0.5) * 1.8;
const digital1 = Math.sin(2 * Math.PI * 5000 * t) * Math.random();
const digital2 = Math.sin(2 * Math.PI * 8000 * t) * Math.random();
const digital3 = Math.sin(2 * Math.PI * 12000 * t) * Math.random();
const eAmplitude = Math.exp(-t * 3) * 0.5;
sample = (eNoise * 0.5 + digital1 * 0.25 + digital2 * 0.2 + digital3 * 0.15) * eAmplitude;
break;
case 'vintage':
// Vintage crash: Warmer, less harsh
const vNoise = (Math.random() - 0.5) * 1.6;
const warm1 = Math.sin(2 * Math.PI * 3000 * t) * Math.random();
const warm2 = Math.sin(2 * Math.PI * 5000 * t) * Math.random();
const warm3 = Math.sin(2 * Math.PI * 7000 * t) * Math.random();
const vAmplitude = Math.exp(-t * 1.8) * 0.65;
sample = (vNoise * 0.7 + warm1 * 0.2 + warm2 * 0.15 + warm3 * 0.1) * vAmplitude;
// Vintage-style saturation
sample = Math.tanh(sample * 1.1) * 0.9;
break;
}
const finalSample = Math.max(-1, Math.min(1, sample));
const intSample = Math.floor(finalSample * 32767);
buffer.writeInt16LE(intSample, i * 4);
buffer.writeInt16LE(intSample, i * 4 + 2);
}
return buffer;
}
// Generate tom drum sound based on current drum set
generateTom(duration = 0.4) {
const samples = Math.floor(this.sampleRate * duration);
const buffer = Buffer.alloc(samples * this.channels * (this.bitDepth / 8));
for (let i = 0; i < samples; i++) {
const t = i / this.sampleRate;
let sample = 0;
switch (this.currentDrumSet) {
case 'classic':
// Classic tom: Tuned drum with pitch bend
const frequency = 120 * Math.exp(-t * 8); // Pitch bend down
const tone = Math.sin(2 * Math.PI * frequency * t);
const overtone = Math.sin(2 * Math.PI * frequency * 1.5 * t) * 0.3;
const amplitude = Math.exp(-t * 4) * 0.8;
// Add some drum head resonance
const resonance = Math.sin(2 * Math.PI * 80 * t) * Math.exp(-t * 2) * 0.2;
sample = (tone + overtone + resonance) * amplitude;
break;
case 'electronic':
// Electronic tom: Synthetic with sharp attack
const eFreq = 150 * Math.exp(-t * 12);
const eTone = Math.sin(2 * Math.PI * eFreq * t);
const eOvertone = Math.sin(2 * Math.PI * eFreq * 2 * t) * 0.4;
const eAmplitude = Math.exp(-t * 6) * 0.7;
// Add electronic-style harmonics
const digital = Math.sin(2 * Math.PI * eFreq * 3 * t) * 0.2;
sample = (eTone + eOvertone + digital) * eAmplitude;
break;
case 'vintage':
// Vintage tom: Warm, slightly saturated
const vFreq = 110 * Math.exp(-t * 6);
const vTone = Math.sin(2 * Math.PI * vFreq * t);
const vOvertone = Math.sin(2 * Math.PI * vFreq * 1.3 * t) * 0.25;
const vAmplitude = Math.exp(-t * 3.5) * 0.85;
// Add vintage character
const warmth = Math.sin(2 * Math.PI * 60 * t) * Math.exp(-t * 1.5) * 0.15;
sample = (vTone + vOvertone + warmth) * vAmplitude;
// Vintage-style compression
sample = Math.tanh(sample * 1.3) * 0.85;
break;
}
const finalSample = Math.max(-1, Math.min(1, sample));
const intSample = Math.floor(finalSample * 32767);
buffer.writeInt16LE(intSample, i * 4);
buffer.writeInt16LE(intSample, i * 4 + 2);
}
return buffer;
}
// Generate silence buffer for spacing
generateSilence(duration) {
const samples = Math.floor(this.sampleRate * duration);
return Buffer.alloc(samples * this.channels * (this.bitDepth / 8));
}
// Create WAV file header
createWAVHeader(dataLength) {
const buffer = Buffer.alloc(44);
// RIFF header
buffer.write('RIFF', 0);
buffer.writeUInt32LE(36 + dataLength, 4);
buffer.write('WAVE', 8);
// fmt chunk
buffer.write('fmt ', 12);
buffer.writeUInt32LE(16, 16); // fmt chunk size
buffer.writeUInt16LE(1, 20); // PCM format
buffer.writeUInt16LE(this.channels, 22);
buffer.writeUInt32LE(this.sampleRate, 24);
buffer.writeUInt32LE(this.sampleRate * this.channels * (this.bitDepth / 8), 28);
buffer.writeUInt16LE(this.channels * (this.bitDepth / 8), 32);
buffer.writeUInt16LE(this.bitDepth, 34);
// data chunk
buffer.write('data', 36);
buffer.writeUInt32LE(dataLength, 40);
return buffer;
}
// Generate the complete beat as a WAV file
async generateWAV(loops = 1, filename = null) {
if (!this.pattern) {
throw new Error('No pattern defined. Use usePreset() or createPattern() first.');
}
console.log(`Generating ${this.bars}-bar pattern at ${this.bpm} BPM for ${loops} loop(s) using ${this.currentDrumSet} drum set...`);
// Calculate timing
const beatDuration = 60 / this.bpm; // Duration of one beat in seconds
const barDuration = beatDuration * 4; // 4 beats per bar
const stepDuration = barDuration / 16; // 16 steps per bar (16th notes)
// Pre-generate drum sounds
const kickBuffer = this.generateKick();
const snareBuffer = this.generateSnare();
const hihatBuffer = this.generateHihat();
const clapBuffer = this.generateClap();
const crashBuffer = this.generateCrash();
const tomBuffer = this.generateTom();
// Calculate total duration and create main audio buffer
const totalDuration = this.bars * barDuration * loops;
const totalSamples = Math.floor(this.sampleRate * totalDuration);
const audioData = Buffer.alloc(totalSamples * this.channels * (this.bitDepth / 8));
let bufferOffset = 0;
for (let loop = 0; loop < loops; loop++) {
for (let bar = 0; bar < this.bars; bar++) {
const barPattern = this.pattern[bar] || [];
for (let step = 0; step < 16; step++) {
const stepPattern = barPattern[step];
const stepSamples = Math.floor(this.sampleRate * stepDuration);
const stepBufferSize = stepSamples * this.channels * (this.bitDepth / 8);
if (stepPattern) {
// Generate mixed audio for this step
let mixedBuffer = Buffer.alloc(stepBufferSize);
stepPattern.forEach(drum => {
let drumBuffer;
switch (drum.toLowerCase()) {
case 'kick':
case 'k':
drumBuffer = kickBuffer;
break;
case 'snare':
case 's':
drumBuffer = snareBuffer;
break;
case 'hihat':
case 'h':
drumBuffer = hihatBuffer;
break;
case 'clap':
case 'c':
drumBuffer = clapBuffer;
break;
case 'crash':
case 'x':
drumBuffer = crashBuffer;
break;
case 'tom':
case 't':
drumBuffer = tomBuffer;
break;
default:
return;
}
// Mix the drum sound into the step buffer
for (let i = 0; i < Math.min(mixedBuffer.length, drumBuffer.length); i += 2) {
const existingSample = mixedBuffer.readInt16LE(i);
const newSample = drumBuffer.readInt16LE(i);
const mixed = Math.max(-32768, Math.min(32767, existingSample + newSample));
mixedBuffer.writeInt16LE(mixed, i);
}
});
// Copy mixed buffer to main audio data
mixedBuffer.copy(audioData, bufferOffset, 0, Math.min(mixedBuffer.length, audioData.length - bufferOffset));
}
bufferOffset += stepBufferSize;
}
}
}
// Create WAV file
const wavHeader = this.createWAVHeader(audioData.length);
const wavFile = Buffer.concat([wavHeader, audioData]);
// Save to file or return buffer
if (filename) {
const outputPath = this.outputDir ? path.join(this.outputDir, filename) : path.resolve(filename);
fs.writeFileSync(outputPath, wavFile);
console.log(`WAV file saved: ${outputPath}`);
return outputPath;
} else {
// Generate default filename with drum set name
const defaultFilename = `beat_${this.currentDrumSet}_${this.bpm}bpm_${this.bars}bars_${Date.now()}.wav`;
const outputPath = this.outputDir ? path.join(this.outputDir, defaultFilename) : path.resolve(defaultFilename);
fs.writeFileSync(outputPath, wavFile);
console.log(`WAV file saved: ${outputPath}`);
return outputPath;
}
}
// Play the pattern (now generates WAV file and provides instructions)
async play(loops = 1, filename = null) {
if (this.isPlaying) {
console.log('Already generating...');
return;
}
this.isPlaying = true;
try {
const filePath = await this.generateWAV(loops, filename);
console.log('\nšµ Beat generated successfully!');
console.log(`š File location: ${filePath}`);
console.log(`š„ Drum set: ${this.currentDrumSet}`);
console.log('\nš To play the beat:');
console.log(' ⢠Open the WAV file in any audio player');
console.log(' ⢠Or use: start "" "' + filePath + '" (Windows)');
console.log(' ⢠Or use: open "' + filePath + '" (macOS)');
console.log(' ⢠Or use: xdg-open "' + filePath + '" (Linux)');
// Try to auto-play on Windows
if (process.platform === 'win32') {
try {
const { exec } = require('child_process');
exec(`start "" "${filePath}"`, (error) => {
if (!error) {
console.log('\nš¶ Opening in default audio player...');
}
});
} catch (e) {
// Silent fail - user can manually open the file
}
}
return filePath;
} finally {
this.isPlaying = false;
}
}
// Stop is not needed for file generation, but keeping for API compatibility
stop() {
console.log('File generation cannot be stopped once started.');
}
}
module.exports = SimpleBeatmaker;