Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions karma.conf.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require('dotenv').config();
require('dotenv').config({ quiet: true });

module.exports = config => {
// Mocha configuration
Expand Down Expand Up @@ -58,7 +58,7 @@ module.exports = config => {
},
singleRun: false,
concurrency: Infinity,
browserNoActivityTimeout: 60000,
browserNoActivityTimeout: 180000, // 3 minutes for audio export tests
};

config.set(configuration);
Expand Down
64 changes: 64 additions & 0 deletions src/embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,70 @@ class Embed {
return this.call('getMIDI').then(data => new Uint8Array(data as [number]));
}

/**
* Convert the displayed score to MP3 audio
*
* Exports the currently loaded score as an MP3 audio file. The audio is rendered
* client-side using the browser's audio capabilities. This may take some time
* depending on the score length and complexity.
*
* @returns A promise that resolves with a Uint8Array containing the MP3 audio data
* @throws {Error} If no score is loaded or audio rendering fails
*
* @example
* // Get MP3 and play it
* const mp3Data = await embed.getMP3();
* const blob = new Blob([mp3Data], { type: 'audio/mpeg' });
* const url = URL.createObjectURL(blob);
* const audio = new Audio(url);
* audio.play();
*
* @example
* // Download MP3 file
* const mp3Data = await embed.getMP3();
* const blob = new Blob([mp3Data], { type: 'audio/mpeg' });
* const url = URL.createObjectURL(blob);
* const a = document.createElement('a');
* a.href = url;
* a.download = 'score.mp3';
* a.click();
*/
getMP3(): Promise<Uint8Array> {
return this.call('getMP3').then(data => new Uint8Array(data as [number]));
}

/**
* Convert the displayed score to WAV audio
*
* Exports the currently loaded score as a WAV audio file. The audio is rendered
* client-side using the browser's audio capabilities. WAV files are uncompressed
* and larger than MP3 but provide lossless audio quality.
*
* @returns A promise that resolves with a Uint8Array containing the WAV audio data
* @throws {Error} If no score is loaded or audio rendering fails
*
* @example
* // Get WAV and play it
* const wavData = await embed.getWAV();
* const blob = new Blob([wavData], { type: 'audio/wav' });
* const url = URL.createObjectURL(blob);
* const audio = new Audio(url);
* audio.play();
*
* @example
* // Download WAV file
* const wavData = await embed.getWAV();
* const blob = new Blob([wavData], { type: 'audio/wav' });
* const url = URL.createObjectURL(blob);
* const a = document.createElement('a');
* a.href = url;
* a.download = 'score.wav';
* a.click();
*/
getWAV(): Promise<Uint8Array> {
return this.call('getWAV').then(data => new Uint8Array(data as [number]));
}

/**
* Convert the displayed score to PDF
*
Expand Down
1 change: 1 addition & 0 deletions src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const EVENTS_NAMES = [
'playbackPosition',
'restrictedFeatureAttempt',
'embedSize',
'exportProgress',
] as const;

/**
Expand Down
81 changes: 81 additions & 0 deletions test/integration/embed-integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,25 @@ const PRIVATE_LINK_SHARING_KEY =
* #full: Tests that can run on full editor
*/

/**
* Checks if data has valid MP3 magic bytes
* MP3 can start with ID3 tag (0x49 0x44 0x33) or MPEG frame sync (0xFF followed by frame header)
*/
function isValidMP3(data) {
if (!(data instanceof Uint8Array) || data.length < 3) return false;
const isID3 = data[0] === 0x49 && data[1] === 0x44 && data[2] === 0x33;
const isMPEG = data[0] === 0xff && (data[1] & 0xe0) === 0xe0;
return isID3 || isMPEG;
}

/**
* Checks if data has valid WAV magic bytes (RIFF header)
*/
function isValidWAV(data) {
if (!(data instanceof Uint8Array) || data.length < 4) return false;
return data[0] === 0x52 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x46; // RIFF
}

describe('Integration - Embed', () => {
// On failure, clean dom
afterEach(() => {
Expand Down Expand Up @@ -507,6 +526,68 @@ describe('Integration - Embed', () => {
});
});

describe('MP3 export #full', () => {
it('should export in MP3', function (done) {
this.timeout(120000); // Audio export can take up to 2 minutes
const { embed } = createEmbedForScoreId(PUBLIC_SCORE);

embed
.getMP3()
.then(mp3 => {
assert.ok(mp3 instanceof Uint8Array);
assert.ok(mp3.length > 0);
assert.ok(isValidMP3(mp3), 'Valid MP3 header');
done();
})
.catch(error => {
done(error);
});
});
});

describe('WAV export #full', () => {
it('should export in WAV', function (done) {
this.timeout(120000); // Audio export can take up to 2 minutes
const { embed } = createEmbedForScoreId(PUBLIC_SCORE);

embed
.getWAV()
.then(wav => {
assert.ok(wav instanceof Uint8Array);
assert.ok(wav.length > 0);
assert.ok(isValidWAV(wav), 'Valid WAV header');
done();
})
.catch(error => {
done(error);
});
});
});

describe('Events - exportProgress #full', () => {
it('should receive exportProgress events during MP3 export', function (done) {
this.timeout(120000); // Audio export can take up to 2 minutes
const { embed } = createEmbedForScoreId(PUBLIC_SCORE);
let progressReceived = false;

embed.on('exportProgress', progress => {
assert.ok(typeof progress === 'object');
progressReceived = true;
});

embed
.getMP3()
.then(mp3 => {
assert.ok(mp3 instanceof Uint8Array);
assert.ok(progressReceived, 'Should have received exportProgress event');
done();
})
.catch(error => {
done(error);
});
});
});

describe('MIDI import/export', () => {
it('shoud load a MIDI file in a blank embed', done => {
var container = document.createElement('div');
Expand Down
34 changes: 34 additions & 0 deletions test/unit/embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,38 @@ describe('Unit - Embed tests', () => {
container.removeChild(embed.element);
});
});

describe('getMP3 method', () => {
it('should have getMP3 method available', () => {
const container = document.getElementById('container');
const embed = new Flat.Embed(container, { score: '1234' });
assert.equal(typeof embed.getMP3, 'function');
container.removeChild(embed.element);
});

it('should return a Promise', () => {
const container = document.getElementById('container');
const embed = new Flat.Embed(container, { score: '1234' });
const promise = embed.getMP3();
assert.ok(promise instanceof Promise);
container.removeChild(embed.element);
});
});

describe('getWAV method', () => {
it('should have getWAV method available', () => {
const container = document.getElementById('container');
const embed = new Flat.Embed(container, { score: '1234' });
assert.equal(typeof embed.getWAV, 'function');
container.removeChild(embed.element);
});

it('should return a Promise', () => {
const container = document.getElementById('container');
const embed = new Flat.Embed(container, { score: '1234' });
const promise = embed.getWAV();
assert.ok(promise instanceof Promise);
container.removeChild(embed.element);
});
});
});
Loading