Skip to content

Commit b565d7b

Browse files
committed
Added support for writing mp3s on platforms that doesn't support it natively
1 parent d13133c commit b565d7b

File tree

14 files changed

+576
-6
lines changed

14 files changed

+576
-6
lines changed

cmake/yup_modules.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,7 @@ macro (yup_add_default_modules modules_path)
836836
yup_add_module (${modules_path}/thirdparty/dr_libs "${modules_definitions}" ${thirdparty_group})
837837
yup_add_module (${modules_path}/thirdparty/opus_library "${modules_definitions}" ${thirdparty_group})
838838
yup_add_module (${modules_path}/thirdparty/flac_library "${modules_definitions}" ${thirdparty_group})
839+
yup_add_module (${modules_path}/thirdparty/hmp3_library "${modules_definitions}" ${thirdparty_group})
839840

840841
# ==== Yup modules
841842
set (modules_group "Modules")

examples/graphics/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ yup_standalone_app (
7676
pffft_library
7777
opus_library
7878
flac_library
79+
hmp3_library
7980
dr_libs
8081
libpng
8182
libwebp

modules/yup_audio_formats/formats/yup_Mp3AudioFormat.cpp

Lines changed: 249 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,83 @@ class Mp3AudioFormatReader : public AudioFormatReader
103103
YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Mp3AudioFormatReader)
104104
};
105105

106+
#if YUP_MODULE_AVAILABLE_hmp3_library
107+
static E_CONTROL makeMp3EncoderControl (double sampleRate,
108+
int numberOfChannels,
109+
int qualityOptionIndex)
110+
{
111+
E_CONTROL ec = {};
112+
ec.mode = (numberOfChannels == 1) ? 3 : 1;
113+
ec.bitrate = -1;
114+
ec.samprate = (int) sampleRate;
115+
ec.nsbstereo = -1;
116+
ec.filter_select = -1;
117+
ec.nsb_limit = -1;
118+
ec.freq_limit = 24000;
119+
ec.cr_bit = 1;
120+
ec.original = 1;
121+
ec.layer = 3;
122+
ec.hf_flag = 0;
123+
ec.vbr_flag = 1;
124+
ec.vbr_mnr = jlimit (0, 150, qualityOptionIndex > 0 ? qualityOptionIndex : 50);
125+
ec.vbr_br_limit = 160;
126+
ec.chan_add_f0 = 24000;
127+
ec.chan_add_f1 = 24000;
128+
ec.sparse_scale = -1;
129+
ec.vbr_delta_mnr = 0;
130+
ec.cpu_select = 0;
131+
ec.quick = -1;
132+
ec.test1 = -1;
133+
ec.test2 = 0;
134+
ec.test3 = 0;
135+
ec.short_block_threshold = 700;
136+
137+
for (int i = 0; i < 21; ++i)
138+
ec.mnr_adjust[i] = 0;
139+
140+
return ec;
141+
}
142+
143+
class Mp3AudioFormatWriter : public AudioFormatWriter
144+
{
145+
public:
146+
Mp3AudioFormatWriter (OutputStream* destStream,
147+
double sampleRate,
148+
int numberOfChannels,
149+
int bitsPerSample,
150+
const StringPairArray& metadataValues,
151+
int qualityOptionIndex);
152+
~Mp3AudioFormatWriter() override;
153+
154+
bool write (const float* const* samplesToWrite, int numSamples) override;
155+
bool flush() override;
156+
157+
bool isValid() const { return isOpen; }
158+
159+
private:
160+
bool encodeAvailableInput();
161+
void compactPcmBuffer();
162+
163+
CMp3Enc encoder;
164+
E_CONTROL control = {};
165+
166+
std::vector<uint8> pcmBuffer;
167+
size_t pcmReadOffset = 0;
168+
169+
HeapBlock<float> interleavedBuffer;
170+
size_t interleavedCapacity = 0;
171+
172+
std::vector<uint8> outputBuffer;
173+
std::vector<uint8> zeroBuffer;
174+
175+
int minInputBytes = 0;
176+
int64 framesExpected = 0;
177+
bool isOpen = false;
178+
179+
YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Mp3AudioFormatWriter)
180+
};
181+
#endif
182+
106183
Mp3AudioFormatReader::Mp3AudioFormatReader (InputStream* sourceStream)
107184
: AudioFormatReader (sourceStream, "MP3 file")
108185
{
@@ -211,6 +288,150 @@ bool Mp3AudioFormatReader::readSamples (float* const* destChannels,
211288
return framesRead > 0;
212289
}
213290

291+
#if YUP_MODULE_AVAILABLE_hmp3_library
292+
Mp3AudioFormatWriter::Mp3AudioFormatWriter (OutputStream* destStream,
293+
double sampleRate,
294+
int numberOfChannels,
295+
int bitsPerSample,
296+
const StringPairArray& metadataValues,
297+
int qualityOptionIndex)
298+
: AudioFormatWriter (destStream, "MP3 file", sampleRate, numberOfChannels, bitsPerSample)
299+
{
300+
ignoreUnused (metadataValues);
301+
302+
control = makeMp3EncoderControl (sampleRate, numberOfChannels, qualityOptionIndex);
303+
minInputBytes = encoder.MP3_audio_encode_init (&control, 32, 1, 0, 0);
304+
305+
if (minInputBytes > 0)
306+
{
307+
outputBuffer.resize (128u * 1024u);
308+
zeroBuffer.resize ((size_t) minInputBytes, 0);
309+
isOpen = true;
310+
}
311+
}
312+
313+
Mp3AudioFormatWriter::~Mp3AudioFormatWriter()
314+
{
315+
flush();
316+
}
317+
318+
bool Mp3AudioFormatWriter::write (const float* const* samplesToWrite, int numSamples)
319+
{
320+
if (! isOpen || numSamples <= 0)
321+
return false;
322+
323+
const auto numChannels = getNumChannels();
324+
const size_t totalSamples = (size_t) numSamples * (size_t) numChannels;
325+
326+
if (totalSamples > interleavedCapacity)
327+
{
328+
interleavedCapacity = totalSamples;
329+
interleavedBuffer.allocate (interleavedCapacity, false);
330+
}
331+
332+
using SourceFormat = AudioData::Format<AudioData::Float32, AudioData::NativeEndian>;
333+
using DestFormat = AudioData::Format<AudioData::Float32, AudioData::NativeEndian>;
334+
335+
AudioData::interleaveSamples (AudioData::NonInterleavedSource<SourceFormat> { samplesToWrite, (int) numChannels },
336+
AudioData::InterleavedDest<DestFormat> { interleavedBuffer.getData(), (int) numChannels },
337+
numSamples);
338+
339+
const size_t bytesToAdd = totalSamples * sizeof (float);
340+
const auto* bytes = reinterpret_cast<const uint8*> (interleavedBuffer.getData());
341+
pcmBuffer.insert (pcmBuffer.end(), bytes, bytes + bytesToAdd);
342+
343+
return encodeAvailableInput();
344+
}
345+
346+
bool Mp3AudioFormatWriter::flush()
347+
{
348+
if (! isOpen)
349+
return false;
350+
351+
if (minInputBytes > 0)
352+
{
353+
const size_t availableBytes = pcmBuffer.size() - pcmReadOffset;
354+
if (availableBytes > 0 && availableBytes < (size_t) minInputBytes)
355+
{
356+
const size_t padBytes = (size_t) minInputBytes - availableBytes;
357+
pcmBuffer.insert (pcmBuffer.end(), padBytes, 0);
358+
}
359+
}
360+
361+
if (! encodeAvailableInput())
362+
return false;
363+
364+
int64 expectedFrames = framesExpected;
365+
if (control.samprate < 32000)
366+
expectedFrames *= 2;
367+
368+
int safetyCounter = 0;
369+
while (encoder.L3_audio_encode_get_frames() < (unsigned int) expectedFrames && safetyCounter < 4096)
370+
{
371+
auto io = encoder.MP3_audio_encode (zeroBuffer.data(), outputBuffer.data());
372+
if (io.out_bytes > 0)
373+
{
374+
if (! output->write (outputBuffer.data(), (size_t) io.out_bytes))
375+
return false;
376+
}
377+
378+
if (io.in_bytes <= 0 && io.out_bytes <= 0)
379+
break;
380+
381+
++safetyCounter;
382+
}
383+
384+
return true;
385+
}
386+
387+
bool Mp3AudioFormatWriter::encodeAvailableInput()
388+
{
389+
if (minInputBytes <= 0 || pcmBuffer.size() <= pcmReadOffset)
390+
return true;
391+
392+
while (pcmBuffer.size() - pcmReadOffset >= (size_t) minInputBytes)
393+
{
394+
auto io = encoder.MP3_audio_encode (pcmBuffer.data() + pcmReadOffset, outputBuffer.data());
395+
if (io.in_bytes <= 0 && io.out_bytes <= 0)
396+
break;
397+
398+
++framesExpected;
399+
400+
if (io.in_bytes > 0)
401+
pcmReadOffset += (size_t) io.in_bytes;
402+
403+
if (io.out_bytes > 0)
404+
{
405+
if (! output->write (outputBuffer.data(), (size_t) io.out_bytes))
406+
return false;
407+
}
408+
409+
compactPcmBuffer();
410+
}
411+
412+
return true;
413+
}
414+
415+
void Mp3AudioFormatWriter::compactPcmBuffer()
416+
{
417+
if (pcmReadOffset == 0)
418+
return;
419+
420+
if (pcmReadOffset >= pcmBuffer.size())
421+
{
422+
pcmBuffer.clear();
423+
pcmReadOffset = 0;
424+
return;
425+
}
426+
427+
if (pcmReadOffset > 4096)
428+
{
429+
pcmBuffer.erase (pcmBuffer.begin(), pcmBuffer.begin() + (ptrdiff_t) pcmReadOffset);
430+
pcmReadOffset = 0;
431+
}
432+
}
433+
#endif
434+
214435
} // namespace
215436

216437
//==============================================================================
@@ -249,7 +470,33 @@ std::unique_ptr<AudioFormatWriter> Mp3AudioFormat::createWriterFor (OutputStream
249470
const StringPairArray& metadataValues,
250471
int qualityOptionIndex)
251472
{
252-
// MP3 encoding is not implemented in this version
473+
#if YUP_MODULE_AVAILABLE_hmp3_library
474+
if (streamToWriteTo == nullptr)
475+
return nullptr;
476+
477+
if (numberOfChannels < 1 || numberOfChannels > 2)
478+
return nullptr;
479+
480+
if (sampleRate < 8000.0 || sampleRate > 48000.0)
481+
return nullptr;
482+
483+
auto writer = std::make_unique<Mp3AudioFormatWriter> (streamToWriteTo,
484+
sampleRate,
485+
numberOfChannels,
486+
bitsPerSample,
487+
metadataValues,
488+
qualityOptionIndex);
489+
if (writer->isValid())
490+
return writer;
491+
#else
492+
ignoreUnused (streamToWriteTo,
493+
sampleRate,
494+
numberOfChannels,
495+
bitsPerSample,
496+
metadataValues,
497+
qualityOptionIndex);
498+
#endif
499+
253500
return nullptr;
254501
}
255502

@@ -263,4 +510,4 @@ Array<int> Mp3AudioFormat::getPossibleSampleRates() const
263510
return { 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000 };
264511
}
265512

266-
} // namespace yup
513+
} // namespace yup

modules/yup_audio_formats/formats/yup_Mp3AudioFormat.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,16 @@ class YUP_API Mp3AudioFormat : public AudioFormat
102102
/** Creates a writer for encoding audio data to MP3 format.
103103
104104
This method creates an MP3 writer configured for the specified audio parameters.
105-
Note that MP3 encoding is not currently implemented in this version.
105+
Encoding is provided by the Helix MP3 encoder when the hmp3_library module
106+
is available.
106107
107108
@param streamToWriteTo The output stream where MP3 data will be written
108109
@param sampleRate The sample rate in Hz (supports 8kHz to 48kHz)
109110
@param numberOfChannels The number of audio channels (1-2 channels supported)
110111
@param bitsPerSample The bit depth (ignored for MP3, always 16-bit output)
111112
@param metadataValues Metadata to embed in the MP3 file (title, artist, etc.)
112113
@param qualityOptionIndex Quality setting (0-100, where 100 is highest quality)
113-
@returns nullptr (encoding not implemented)
114+
@returns A writer when MP3 encoding is available, otherwise nullptr
114115
*/
115116
std::unique_ptr<AudioFormatWriter> createWriterFor (OutputStream* streamToWriteTo,
116117
double sampleRate,
@@ -165,4 +166,4 @@ class YUP_API Mp3AudioFormat : public AudioFormat
165166
String formatName;
166167
};
167168

168-
} // namespace yup
169+
} // namespace yup

modules/yup_audio_formats/yup_audio_formats.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
#include <dr_libs/dr_libs.h>
3737
#endif
3838

39+
#if YUP_AUDIO_FORMAT_MP3 && YUP_MODULE_AVAILABLE_hmp3_library
40+
#include <hmp3_library/hmp3_library.h>
41+
#endif
42+
3943
#if YUP_AUDIO_FORMAT_OPUS
4044
#include <opus_library/opus_library.h>
4145
#endif

tests/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ set (target_modules
6565
dr_libs
6666
pffft_library
6767
opus_library
68-
flac_library)
68+
flac_library
69+
hmp3_library)
6970

7071
if (NOT YUP_PLATFORM_EMSCRIPTEN)
7172
list (APPEND target_modules yup_gui yup_audio_gui)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
==============================================================================
3+
4+
This file is part of the YUP library.
5+
Copyright (c) 2025 - [email protected]
6+
7+
YUP is an open source library subject to open-source licensing.
8+
9+
The code included in this file is provided under the terms of the ISC license
10+
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
11+
to use, copy, modify, and/or distribute this software for any purpose with or
12+
without fee is hereby granted provided that the above copyright notice and
13+
this permission notice appear in all copies.
14+
15+
YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
16+
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
17+
DISCLAIMED.
18+
19+
==============================================================================
20+
*/
21+
22+
/*
23+
==============================================================================
24+
25+
BEGIN_YUP_MODULE_DECLARATION
26+
27+
ID: hmp3_library
28+
vendor: hmp3_library
29+
version: 5.2.4
30+
name: Helix MP3 Encoder
31+
description: Helix MP3 Encoder.
32+
website: https://github.com/maikmerten/hmp3
33+
upstream: https://github.com/maikmerten/hmp3/archive/refs/tags/5.2.4.zip
34+
license: RPSL
35+
36+
defines: IEEE_FLOAT=1 _FILE_OFFSET_BITS=64
37+
searchpaths: hmp3/src/pub hmp3/src
38+
39+
END_YUP_MODULE_DECLARATION
40+
41+
==============================================================================
42+
*/
43+
44+
#pragma once
45+
46+
#include <mp3enc.h>

0 commit comments

Comments
 (0)