Skip to content

Commit 1cf85c7

Browse files
[BH-2049] Gapless audio transition
* add minimp3 for decoding mp3 files * add a gapless transition between the end and beginning of the track * add a loop option to the playback mode
1 parent 92e042a commit 1cf85c7

34 files changed

+177
-159
lines changed

.gitmodules

+4-4
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,6 @@
5959
[submodule "CrashDebug"]
6060
path = third-party/CrashDebug/src
6161
url = https://github.com/adamgreen/CrashDebug.git
62-
[submodule "minimp3"]
63-
path = third-party/minimp3/minimp3
64-
url = ../minimp3.git
65-
branch = RT1051
6662
[submodule "parallel-hashmap"]
6763
path = third-party/parallel-hashmap/src
6864
url = https://github.com/greg7mdp/parallel-hashmap.git
@@ -116,3 +112,7 @@
116112
[submodule "third-party/fakeit/FakeIt"]
117113
path = third-party/fakeit/FakeIt
118114
url = https://github.com/eranpeer/FakeIt.git
115+
[submodule "third-party/minimp3/minimp3"]
116+
path = third-party/minimp3/minimp3
117+
url = https://github.com/mudita/minimp3.git
118+
branch = mudita

harmony_changelog.md

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
* Added fade in and fade out to relaxation songs
1313
* Added time sync endpoint to be used by Mudita Center
1414
* Added bedside lamp settings
15+
* Added gapless audio transition in relaxation
16+
1517

1618
### Changed / Improved
1719

module-audio/Audio/Audio.cpp

+5-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ namespace audio
1111
{
1212
Audio::Audio(AudioServiceMessage::Callback callback) : currentOperation(), serviceCallback(std::move(callback))
1313
{
14-
auto ret = Operation::Create(Operation::Type::Idle, "", audio::PlaybackType::None, serviceCallback);
14+
auto ret = Operation::Create(
15+
Operation::Type::Idle, "", audio::PlaybackType::None, audio::PlaybackMode::Single, serviceCallback);
1516
if (ret) {
1617
currentOperation = std::move(ret);
1718
}
@@ -50,11 +51,12 @@ namespace audio
5051
audio::RetCode Audio::Start(Operation::Type op,
5152
audio::Token token,
5253
const std::string &filePath,
53-
const audio::PlaybackType &playbackType)
54+
const audio::PlaybackType &playbackType,
55+
const audio::PlaybackMode &playbackMode)
5456
{
5557

5658
try {
57-
auto ret = Operation::Create(op, filePath, playbackType, serviceCallback);
59+
auto ret = Operation::Create(op, filePath, playbackType, playbackMode, serviceCallback);
5860
switch (op) {
5961
case Operation::Type::Playback:
6062
currentState = State::Playback;

module-audio/Audio/Audio.hpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ namespace audio
9797
virtual audio::RetCode Start(Operation::Type op,
9898
audio::Token token = audio::Token::MakeBadToken(),
9999
const std::string &filePath = "",
100-
const audio::PlaybackType &playbackType = audio::PlaybackType::None);
100+
const audio::PlaybackType &playbackType = audio::PlaybackType::None,
101+
const audio::PlaybackMode &playbackMode = audio::PlaybackMode::Single);
101102

102103
virtual audio::RetCode Start();
103104
virtual audio::RetCode Stop();

module-audio/Audio/AudioCommon.hpp

+6
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ namespace audio
5252
Disabled
5353
};
5454

55+
enum class PlaybackMode
56+
{
57+
Single,
58+
Loop
59+
};
60+
5561
enum class PlaybackType
5662
{
5763
None,

module-audio/Audio/Operation/Operation.cpp

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
1+
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
22
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
33

44
#include "Operation.hpp"
@@ -18,6 +18,7 @@ namespace audio
1818
std::unique_ptr<Operation> Operation::Create(Operation::Type t,
1919
const std::string &filePath,
2020
const audio::PlaybackType &playbackType,
21+
const PlaybackMode &playbackMode,
2122
AudioServiceMessage::Callback callback)
2223
{
2324
std::unique_ptr<Operation> inst;
@@ -27,7 +28,7 @@ namespace audio
2728
inst = std::make_unique<IdleOperation>(filePath);
2829
break;
2930
case Type::Playback:
30-
inst = std::make_unique<PlaybackOperation>(filePath, playbackType, callback);
31+
inst = std::make_unique<PlaybackOperation>(filePath, playbackType, playbackMode, callback);
3132
break;
3233
case Type::Router:
3334
inst = std::make_unique<RouterOperation>(filePath, callback);

module-audio/Audio/Operation/Operation.hpp

+6-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ namespace audio
1919
{
2020
public:
2121
explicit Operation(AudioServiceMessage::Callback callback,
22-
const PlaybackType &playbackType = PlaybackType::None)
22+
const PlaybackType &playbackType = PlaybackType::None,
23+
const PlaybackMode &playbackMode = PlaybackMode::Single)
2324
: playbackType(playbackType), serviceCallback(std::move(callback)), observer(serviceCallback)
2425
{
2526
factory = AudioPlatform::GetDeviceFactory();
@@ -59,9 +60,10 @@ namespace audio
5960
virtual ~Operation() = default;
6061

6162
static std::unique_ptr<Operation> Create(Type t,
62-
const std::string &filePath = "",
63-
const audio::PlaybackType &operations = audio::PlaybackType::None,
64-
AudioServiceMessage::Callback callback = nullptr);
63+
const std::string &filePath = "",
64+
const audio::PlaybackType &operations = audio::PlaybackType::None,
65+
const audio::PlaybackMode &playbackMode = audio::PlaybackMode::Single,
66+
AudioServiceMessage::Callback callback = nullptr);
6567

6668
virtual audio::RetCode Start(audio::Token token) = 0;
6769
virtual audio::RetCode Stop() = 0;

module-audio/Audio/Operation/PlaybackOperation.cpp

+10-4
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,24 @@ namespace audio
1717

1818
PlaybackOperation::PlaybackOperation(const std::string &filePath,
1919
const audio::PlaybackType &playbackType,
20+
const audio::PlaybackMode &playbackMode,
2021
Callback callback)
21-
: Operation(std::move(callback), playbackType), dec(nullptr)
22+
: Operation(std::move(callback), playbackType), playbackMode(playbackMode), dec(nullptr)
2223
{
2324
// order defines priority
2425
AddProfile(Profile::Type::PlaybackHeadphones, playbackType, false);
2526
AddProfile(Profile::Type::PlaybackBluetoothA2DP, playbackType, false);
2627
AddProfile(Profile::Type::PlaybackLoudspeaker, playbackType, true);
2728

2829
endOfFileCallback = [this]() {
29-
state = State::Idle;
30-
const auto msg = AudioServiceMessage::EndOfFile(operationToken);
31-
serviceCallback(&msg);
30+
if (this->playbackMode == audio::PlaybackMode::Single) {
31+
state = State::Idle;
32+
const auto msg = AudioServiceMessage::EndOfFile(operationToken);
33+
serviceCallback(&msg);
34+
}
35+
else {
36+
dec->setPosition(playbackStartPosition);
37+
}
3238
};
3339

3440
fileDeletedCallback = [this]() {

module-audio/Audio/Operation/PlaybackOperation.hpp

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ namespace audio
2020
public:
2121
PlaybackOperation(const std::string &filePath,
2222
const audio::PlaybackType &playbackType,
23+
const audio::PlaybackMode &playbackMode,
2324
AudioServiceMessage::Callback callback = nullptr);
2425

2526
virtual ~PlaybackOperation();
@@ -38,6 +39,8 @@ namespace audio
3839

3940
private:
4041
static constexpr auto playbackTimeConstraint = 10ms;
42+
static constexpr auto playbackStartPosition = 0U;
43+
audio::PlaybackMode playbackMode = audio::PlaybackMode::Single;
4144

4245
std::unique_ptr<Stream> dataStreamOut;
4346
std::unique_ptr<Decoder> dec;
+22-66
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,36 @@
11
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
22
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
33

4-
#define DR_MP3_IMPLEMENTATION
5-
#define DR_MP3_NO_STDIO
4+
#define MINIMP3_IMPLEMENTATION
5+
#define MINIMP3_NO_STDIO
66

77
#include "DecoderCommon.hpp"
88
#include "DecoderMP3.hpp"
99
#include <cstdio>
1010

11-
namespace
12-
{
13-
signed skipID3V2TagIfPresent(std::FILE *fd)
14-
{
15-
constexpr auto ID3V2FrameOffset = 0;
16-
constexpr auto ID3V2FrameHeaderSize = 10;
17-
constexpr auto ID3V2FrameMagicString = "ID3";
18-
constexpr auto ID3V2FrameMagicStringLength = 3;
19-
std::uint8_t frameBuffer[ID3V2FrameHeaderSize];
20-
21-
/* Seek to the beginning of the frame and read frame's header */
22-
if (std::fseek(fd, ID3V2FrameOffset, SEEK_SET) != 0) {
23-
return -EIO;
24-
}
25-
if (std::fread(frameBuffer, sizeof(*frameBuffer), ID3V2FrameHeaderSize, fd) != ID3V2FrameHeaderSize) {
26-
return -EIO;
27-
}
28-
29-
/* Check magic */
30-
if (strncmp(reinterpret_cast<const char *>(frameBuffer), ID3V2FrameMagicString, ID3V2FrameMagicStringLength) !=
31-
0) {
32-
return 0;
33-
}
34-
35-
/* The tag size (minus the 10-byte header) is encoded into four bytes,
36-
* but the most significant bit needs to be masked in each byte.
37-
* Those frame indices are just copied from the ID3V2 docs. */
38-
const auto ID3V2TagTotalSize = (((frameBuffer[6] & 0x7F) << 21) | ((frameBuffer[7] & 0x7F) << 14) |
39-
((frameBuffer[8] & 0x7F) << 7) | ((frameBuffer[9] & 0x7F) << 0)) +
40-
ID3V2FrameHeaderSize;
41-
42-
/* Skip the tag */
43-
if (std::fseek(fd, ID3V2FrameOffset + ID3V2TagTotalSize, SEEK_SET) != 0) {
44-
return -EIO;
45-
}
46-
return ID3V2TagTotalSize;
47-
}
48-
} // namespace
49-
5011
namespace audio
5112
{
52-
DecoderMP3::DecoderMP3(const std::string &filePath) : Decoder(filePath), mp3(std::make_unique<drmp3>())
13+
DecoderMP3::DecoderMP3(const std::string &filePath)
14+
: Decoder(filePath), dec(std::make_unique<mp3dec_ex_t>()), io(std::make_unique<mp3dec_io_t>())
5315
{
5416
if (fileSize == 0) {
5517
return;
5618
}
5719

58-
const auto tagSkipStatus = skipID3V2TagIfPresent(fd);
59-
if (tagSkipStatus < 0) {
60-
LOG_ERROR("Failed to skip ID3V2 tag, error %d", tagSkipStatus);
61-
}
62-
else if (tagSkipStatus == 0) {
63-
LOG_INFO("No ID3V2 tag to skip");
64-
}
20+
io->read = mp3Read;
21+
io->read_data = this;
22+
io->seek = mp3Seek;
23+
io->seek_data = this;
24+
dec->io = io.get();
6525

66-
if (drmp3_init(mp3.get(), drmp3Read, drmp3Seek, this, nullptr) == DRMP3_FALSE) {
67-
LOG_ERROR("Unable to initialize MP3 decoder");
26+
if (mp3dec_ex_open_cb(dec.get(), dec->io, MP3D_SEEK_TO_SAMPLE)) {
27+
LOG_ERROR("Failed to open minimp3");
6828
return;
6929
}
7030

7131
/* NOTE: Always convert to S16LE as an internal format */
72-
channelCount = mp3->channels;
73-
sampleRate = mp3->sampleRate;
32+
channelCount = dec->info.channels;
33+
sampleRate = dec->info.hz;
7434
bitsPerSample = 16;
7535
isInitialized = true;
7636
}
@@ -80,7 +40,7 @@ namespace audio
8040
if (!isInitialized) {
8141
return;
8242
}
83-
drmp3_uninit(mp3.get());
43+
mp3dec_ex_close(dec.get());
8444
isInitialized = false;
8545
}
8646

@@ -90,18 +50,16 @@ namespace audio
9050
LOG_ERROR("MP3 decoder not initialized");
9151
return;
9252
}
93-
const auto totalFramesCount = drmp3_get_pcm_frame_count(mp3.get());
94-
drmp3_seek_to_pcm_frame(mp3.get(), totalFramesCount * pos);
95-
position = static_cast<float>(totalFramesCount) * pos / static_cast<float>(sampleRate);
53+
mp3dec_ex_seek(dec.get(), pos);
9654
}
9755

9856
std::int32_t DecoderMP3::decode(std::uint32_t samplesToRead, std::int16_t *pcmData)
9957
{
100-
const auto samplesRead = drmp3_read_pcm_frames_s16(
101-
mp3.get(), samplesToRead / channelCount, reinterpret_cast<drmp3_int16 *>(pcmData));
58+
const auto samplesRead = mp3dec_ex_read(dec.get(), reinterpret_cast<mp3d_sample_t *>(pcmData), samplesToRead);
10259
if (samplesRead > 0) {
10360
/* Calculate frame duration in seconds */
104-
position += static_cast<float>(samplesRead) / static_cast<float>(sampleRate);
61+
const auto samplesPerChannel = static_cast<float>(samplesRead) / static_cast<float>(channelCount);
62+
position += samplesPerChannel / static_cast<float>(sampleRate);
10563
}
10664
else if (!fileExists(fd)) {
10765
/* Unfortunately this second check of file existence is needed
@@ -110,10 +68,10 @@ namespace audio
11068
LOG_WARN("File '%s' was deleted during playback!", filePath.c_str());
11169
return fileDeletedRetCode;
11270
}
113-
return samplesRead * channelCount;
71+
return samplesRead;
11472
}
11573

116-
std::size_t DecoderMP3::drmp3Read(void *pUserData, void *pBufferOut, std::size_t bytesToRead)
74+
std::size_t DecoderMP3::mp3Read(void *pBufferOut, std::size_t bytesToRead, void *pUserData)
11775
{
11876
const auto decoderContext = reinterpret_cast<DecoderMP3 *>(pUserData);
11977

@@ -126,11 +84,9 @@ namespace audio
12684
return std::fread(pBufferOut, 1, bytesToRead, decoderContext->fd);
12785
}
12886

129-
drmp3_bool32 DecoderMP3::drmp3Seek(void *pUserData, int offset, drmp3_seek_origin origin)
87+
int DecoderMP3::mp3Seek(std::uint64_t offset, void *pUserData)
13088
{
13189
const auto decoderContext = reinterpret_cast<DecoderMP3 *>(pUserData);
132-
const auto seekError =
133-
std::fseek(decoderContext->fd, offset, origin == drmp3_seek_origin_start ? SEEK_SET : SEEK_CUR);
134-
return (seekError == 0) ? DRMP3_TRUE : DRMP3_FALSE;
90+
return std::fseek(decoderContext->fd, offset, SEEK_SET);
13591
}
13692
} // namespace audio

module-audio/Audio/decoder/DecoderMP3.hpp

+8-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#pragma once
55

66
#include "Decoder.hpp"
7-
#include <src/dr_mp3.h>
7+
#include <minimp3_ex.h>
88

99
namespace audio
1010
{
@@ -19,31 +19,29 @@ namespace audio
1919
void setPosition(float pos) override;
2020

2121
private:
22-
std::unique_ptr<drmp3> mp3;
22+
std::unique_ptr<mp3dec_ex_t> dec;
23+
std::unique_ptr<mp3dec_io_t> io;
2324

2425
// Callback for when data needs to be read from the client.
2526
//
26-
// pUserData [in] The user data that was passed to drmp3_init() and family.
27+
// pUserData [in] The user data that was assigned to mp3dec_io_t and family.
2728
// pBufferOut [out] The output buffer.
2829
// bytesToRead [in] The number of bytes to read.
2930
//
3031
// Returns the number of bytes actually read.
3132
//
3233
// A return value of less than bytesToRead indicates the end of the stream. Do _not_ return from this callback
3334
// until either the entire bytesToRead is filled or you have reached the end of the stream.
34-
static std::size_t drmp3Read(void *pUserData, void *pBufferOut, std::size_t bytesToRead);
35+
static std::size_t mp3Read(void *pBufferOut, std::size_t bytesToRead, void *pUserData);
3536

3637
// Callback for when data needs to be seeked.
3738
//
38-
// pUserData [in] The user data that was passed to drmp3_init() and family.
39+
// pUserData [in] The user data that was assigned to mp3dec_io_t and family.
3940
// offset [in] The number of bytes to move, relative to the origin. Will never be negative.
40-
// origin [in] The origin of the seek - the current position or the start of the stream.
4141
//
4242
// Returns whether the seek was successful.
4343
//
44-
// The offset will never be negative. Whether it is relative to the beginning or current position is
45-
// determined by the "origin" parameter which will be either drmp3_seek_origin_start or
46-
// drmp3_seek_origin_current.
47-
static drmp3_bool32 drmp3Seek(void *pUserData, int offset, drmp3_seek_origin origin);
44+
// The offset will never be negative. It relates to the beginning position.
45+
static int mp3Seek(std::uint64_t offset, void *pUserData);
4846
};
4947
} // namespace audio

module-audio/Audio/decoder/DecoderWorker.cpp

+3-2
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,14 @@ void audio::DecoderWorker::pushAudioData()
9393

9494
while (!audioStreamOut->isFull() && playbackEnabled) {
9595
auto buffer = decoderBuffer.get();
96-
samplesRead = decoder->decode(bufferSize / readScale, buffer);
96+
const auto totalBufferSize = bufferSize / readScale;
97+
samplesRead = decoder->decode(totalBufferSize, buffer);
9798

9899
if (samplesRead == Decoder::fileDeletedRetCode) {
99100
fileDeletedCallback();
100101
break;
101102
}
102-
if (samplesRead == 0) {
103+
if (samplesRead < totalBufferSize) {
103104
endOfFileCallback();
104105
break;
105106
}

0 commit comments

Comments
 (0)