Skip to content

Commit 16203e7

Browse files
author
John Sheppard
committed
Update to version 1.1 with major optimizations
- Rewrote CUE parser to be more robust and efficient - Fixed loop sector validation: properly handles REM LOOP (sector 0) vs REM LOOP [sector] vs REM NOLOOP - Removed unnecessary CD file size restrictions (MD+ uses WAV/OGG, not CD images) - Removed CD sector limits for better compatibility with longer audio files - Improved whitespace handling in CUE parsing - Streamlined decoder wrapper implementation - Enhanced error handling for malformed CUE files
1 parent 584211b commit 16203e7

2 files changed

Lines changed: 120 additions & 67 deletions

File tree

README.md

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,78 @@
1-
# MD+ Audio Player for foobar2000
1+
# Sega MD+ Audio Player for foobar2000
22

3-
Seamless loop playback support for MD+ compatible CUE sheets in foobar2000.
3+
Seamless looped playback for [Sega MD+](#what-is-md) compatible CUE sheets in foobar2000
44

55
## Features
66

7-
- Seamless audio looping at specified loop points
8-
- Support for REM LOOP and REM NOLOOP commands
9-
- Compatible with WAV and OGG files (included because OGG is compatible with emulators such as Genesis Plus GX)
10-
- Works alongside standard CUE file handling
7+
- Seamless audio looping at specified loop points in the CUE file via REM LOOP commands
8+
- Compatible with WAV and OGG files _(as OGG is compatible with MD+ emulators such as Libretro Genesis Plus GX)_
9+
- Doesn't interfere with foobar2000's standard CUE file handling
1110

1211
## Installation
1312

1413
1. Download the appropriate DLL for your foobar2000 version:
15-
- **32-bit (x86)** (v1.x): [foo_mdplus.dll (x86)](../../releases/latest/download/foo_mdplus_x86.dll)
16-
- **64-bit (x64)** (v2.x): [foo_mdplus.dll (x64)](../../releases/latest/download/foo_mdplus_x64.dll)
14+
- **32-bit (x86) (v1.x):** [foo_mdplus.dll (x86)](../../releases/latest/download/foo_mdplus_x86.dll)
15+
- **64-bit (x64) (v2.x):** [foo_mdplus.dll (x64)](../../releases/latest/download/foo_mdplus_x64.dll)
1716

18-
2. Copy the DLL to your foobar2000 components folder:
19-
- Usually (for v1.x): `C:\Program Files (x86)\foobar2000\components\`
20-
- (for v2.x): `C:\Program Files\foobar2000\components\`
21-
- Or: `%APPDATA%\foobar2000\user-components\`
17+
2. Copy the DLL to your foobar2000 components folder and **remove "_x86" or "_x64" from the filename** *(it should be named **"foo_mdplus.dll"**)*:
18+
- **For foobar2000 versions 1.x:** `C:\Program Files (x86)\foobar2000\components\`
19+
- **For foobar2000 versions 2.x:** `C:\Program Files\foobar2000\components\`
20+
- **Or:** `%APPDATA%\foobar2000\user-components\`
2221

2322
3. Restart foobar2000
2423

24+
4. Ensure the plugin is activated by checking _"File -> Preferences -> Components"_. It will be displayed as _"MD+ Audio Player"_. You can also check _"View -> Console"_
25+
26+
5. Console will display:
27+
```
28+
==========================================
29+
MD+ Audio Player v1.x initialized
30+
Usage: Rename .cue files to .mdpcue
31+
Example: game_name.cue -> game_name.mdpcue
32+
==========================================
33+
```
34+
2535
## Usage
2636

27-
1. Rename your MD+ CUE files from `.cue` to `.mdpcue`
37+
1. Rename your MD+ CUE files from `.cue` to `.mdpcue`. You can simply make a copy of your existing CUE file and then rename the extension. The data inside the file will remain the same
2838
- Example: `game_name.cue` -> `game_name.mdpcue`
2939

3040
2. Add the `.mdpcue` file to foobar2000
3141

3242
3. Tracks with `REM LOOP` will loop seamlessly and indefinitely at the specified value in the CUE file
33-
- Tracks with `REM NOLOOP` play once and continue to next track
43+
- Tracks with `REM NOLOOP` play once and then move on to the next track
3444

3545
## Example MD+ CUE File Format
3646

3747
```cue
38-
FILE "01-Track.wav" WAVE
48+
FILE "01-Track1.wav" WAVE
3949
TRACK 01 AUDIO
50+
INDEX 01 00:00:00
51+
REM LOOP
52+
53+
FILE "02-Track2.wav" WAVE
54+
TRACK 02 AUDIO
4055
INDEX 01 00:00:00
4156
REM LOOP 12345
4257
43-
FILE "02-Track.wav" WAVE
44-
TRACK 02 AUDIO
58+
FILE "03-Track3.wav" WAVE
59+
TRACK 03 AUDIO
4560
INDEX 01 00:00:00
4661
REM NOLOOP
4762
```
4863

64+
Track 1 will loop from the beginning when it reaches end of file
65+
66+
Track 2 will loop from the specified sector when it reaches end of file _(12345)_
67+
68+
Track 3 will not loop when it reaches end of file, and will move on to the next track on the list
69+
70+
## What is MD+?
71+
72+
MD+ is an open standard developed by neodev/ElSemi of Terraonion for the release of their Sega Mega Drive/Genesis flash cartridge and world's first Sega Mega CD/Sega CD optical drive emulator, the MegaSD, released in June of 2019. It is supported by the aforementioned Terraonion MegaSD, Krikzz's Mega EverDrive PRO _(as of firmware v4.15 from the 24th of June 2022)_ and Mega EverDrive CORE flash cartridges, and can be emulated using Genesis Plus GX and PicoDrive in RetroArch.
73+
74+
It is the 1:1 equivalent of MSU1 on the Super Nintendo, as it allows data streaming, and the original Yamaha YM2612 audio of Sega Mega Drive/Genesis games to be replaced by external PCM wave _(.wav)_ or compressed Ogg Vorbis _(.ogg)_ files to produce something similar to a what a CD version of the game would have been like, but unlike a physical CD unit and CD hardware emulation hacks _(Mode 1/MSU-MD)_, the audio files can be seamlessly looped, resulting in a more professional sounding audio experience and vastly reducing audio file sizes _(even when compared to MSU-MD games that have been converted to CHD)_. There are no inherent seek time delays or other imitations that come with emulating CD drives. Overall, it is a much better, more modern standard for FPGA and emulation platforms than the outdated Mode 1/MSU-MD format.
75+
4976
## Building from Source
5077

5178
### Requirements
@@ -54,18 +81,14 @@ FILE "02-Track.wav" WAVE
5481

5582
### Steps
5683
1. Clone this repository
57-
2. Download the foobar2000 SDK and extract to `foobar2000_SDK/` folder
84+
2. Download the latest foobar2000 SDK and extract to `foobar2000_SDK/` folder
5885
3. Open `foo_mdplus.sln` in Visual Studio
5986
4. Select platform: **x86** or **x64**
6087
5. Build -> Build Solution
61-
6. DLL will be in `Debug/` or `Release/` folder
88+
6. DLL will be in `Debug/`, `Release/` or `x64/Release` folder depending on solution configurations and platforms
6289

6390
## Credits
6491

65-
- Author: Relikk (John Sheppard)
66-
- Version: 1.0
67-
- License: MIT License
68-
69-
## Support
70-
71-
For issues or feature requests, please open an issue on GitHub.
92+
- **Author:** Relikk (John Sheppard)
93+
- **Version:** 1.0
94+
- **License:** MIT License

foo_mdplus/foo_mdplus.cpp

Lines changed: 71 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
// foo_mdplus.cpp - MD+ Audio Player for foobar2000
22
// Seamless looping for WAV/OGG files with MD+ compatible CUE metadata
33
// Author: Relikk (John Sheppard)
4-
// Version: 1.0
4+
// Version: 1.1
55

66
#define _CRT_SECURE_NO_WARNINGS
77
#include "foobar2000/SDK/foobar2000.h"
88
#include "foobar2000/helpers/helpers.h"
99

1010
DECLARE_COMPONENT_VERSION(
1111
"MD+ Audio Player",
12-
"1.0",
12+
"1.1",
1313
"Seamless loop playback support in foobar2000 for MD+ compatible CUE sheets\n"
1414
"Handles REM LOOP and REM NOLOOP commands\n\n"
1515
"Change your MD+ .cue file extension to .mdpcue for intended usage\n"
16-
"Standard .cue files remain unaffected\n\n"
16+
"Standard .cue file playback remains unaffected\n\n"
1717
"Author: Relikk (John Sheppard)"
1818
);
1919

@@ -31,6 +31,7 @@ namespace {
3131
looping_decoder(input_decoder::ptr base, int loop_sector)
3232
: m_base(base), m_loop_sector(loop_sector)
3333
{
34+
// Convert CD audio sectors (75 per second) to seconds
3435
m_loop_point = (double)loop_sector / 75.0;
3536
}
3637

@@ -41,8 +42,9 @@ namespace {
4142
virtual bool run(audio_chunk& chunk, abort_callback& abort) {
4243
bool result = m_base->run(chunk, abort);
4344

44-
// If EOF reached and loop is enabled, seek back to loop point
45-
if (!result && m_loop_sector > 0) {
45+
// When EOF is reached and loop is enabled, seek back to loop point for seamless playback
46+
// Loop sector >= 0 means looping is enabled (0 = beginning, >0 = specific sector)
47+
if (!result && m_loop_sector >= 0) {
4648
m_base->seek(m_loop_point, abort);
4749
return m_base->run(chunk, abort);
4850
}
@@ -87,12 +89,24 @@ namespace {
8789
}
8890
};
8991

92+
// Helper function to skip leading whitespace
93+
inline const char* skip_whitespace(const char* ptr) {
94+
while (*ptr && (*ptr == ' ' || *ptr == '\t')) ptr++;
95+
return ptr;
96+
}
97+
98+
// Helper function to check if line starts with a command (case-insensitive, whitespace-tolerant)
99+
inline bool line_starts_with(const char* line, const char* cmd) {
100+
line = skip_whitespace(line);
101+
return _strnicmp(line, cmd, strlen(cmd)) == 0;
102+
}
103+
90104
// Input handler for .mdpcue files
91105
class input_mdplus : public input_stubs {
92106
private:
93107
struct track_info {
94108
pfc::string8 file_path;
95-
int loop_sector; // >0 = loop sector, -1 = NOLOOP, 0 = no loop info
109+
int loop_sector; // >= 0 = loop at this sector, -1 = no loop (NOLOOP)
96110
};
97111

98112
pfc::list_t<track_info> m_tracks;
@@ -125,17 +139,19 @@ namespace {
125139
filesystem::g_open(cue_file, path, filesystem::open_mode_read, abort);
126140
}
127141

142+
// Validate file is not empty
128143
t_filesize size = cue_file->get_size_ex(abort);
129-
if (size == 0 || size > 10485760) {
130-
throw exception_io_data();
144+
if (size == 0) {
145+
throw exception_io_data("MDPCUE file is empty");
131146
}
132147

148+
// Read entire CUE file into buffer
133149
pfc::array_t<char> buffer;
134150
buffer.set_size(pfc::downcast_guarded<t_size>(size + 1));
135151
cue_file->read_object(buffer.get_ptr(), size, abort);
136-
buffer[size] = 0;
152+
buffer[size] = 0; // Null terminate
137153

138-
// Get base directory from MDPCUE path
154+
// Extract base directory from MDPCUE path for resolving relative audio file paths
139155
pfc::string8 mdpcue_path = path;
140156
if (pfc::string_find_first(mdpcue_path, "file://") == 0) {
141157
mdpcue_path.remove_chars(0, 7);
@@ -147,65 +163,75 @@ namespace {
147163
m_base_path.set_string(mdpcue_path, last_slash - mdpcue_path.get_ptr() + 1);
148164
}
149165

150-
// Parse MDPCUE file
166+
// Parse MDPCUE file line by line
151167
const char* ptr = buffer.get_ptr();
152168
track_info current_track;
153-
current_track.loop_sector = 0;
154-
int track_number = 0;
169+
current_track.loop_sector = -1; // Default: no looping unless REM LOOP is found
170+
int track_count = 0;
155171
bool track_has_index = false;
156172

157173
while (*ptr) {
174+
// Extract one line
158175
const char* line_start = ptr;
159176
while (*ptr && *ptr != '\n' && *ptr != '\r') ptr++;
160177

161178
pfc::string8 line(line_start, ptr - line_start);
179+
180+
// Skip line terminators
162181
while (*ptr && (*ptr == '\n' || *ptr == '\r')) ptr++;
163182

164-
// FILE command - save previous track and start new file
165-
if (strstr(line, "FILE ") == line.get_ptr()) {
166-
if (track_has_index && track_number > 0 && current_track.file_path.get_length() > 0) {
183+
// Parse CUE commands
184+
if (line_starts_with(line, "FILE ")) {
185+
// Save previous track before starting a new file
186+
if (track_has_index && track_count > 0 && current_track.file_path.get_length() > 0) {
167187
m_tracks.add_item(current_track);
168188
}
169189

170-
current_track.loop_sector = 0;
190+
// Reset for new file (default: no looping)
191+
current_track.loop_sector = -1;
171192
track_has_index = false;
172193

194+
// Extract filename from quotes
173195
const char* quote1 = strchr(line, '"');
174196
if (quote1) {
175197
const char* quote2 = strchr(quote1 + 1, '"');
176-
if (quote2) {
198+
if (quote2 && quote2 > quote1 + 1) {
177199
current_track.file_path = m_base_path;
178200
current_track.file_path.add_string(quote1 + 1, quote2 - quote1 - 1);
179201
}
180202
}
181203
}
182-
// TRACK command
183-
else if (strstr(line, " TRACK ")) {
184-
track_number++;
204+
else if (line_starts_with(line, "TRACK ")) {
205+
track_count++;
185206
}
186-
// INDEX 01 command
187-
else if (strstr(line, " INDEX 01 ")) {
207+
else if (line_starts_with(line, "INDEX 01 ")) {
188208
track_has_index = true;
189209
}
190-
// REM LOOP command
191-
else if (strstr(line, " REM LOOP ")) {
192-
const char* sector_str = strstr(line, "LOOP ") + 5;
193-
while (*sector_str == ' ' || *sector_str == '\t') sector_str++;
194-
current_track.loop_sector = atoi(sector_str);
210+
else if (line_starts_with(line, "REM LOOP ")) {
211+
// Extract loop sector number
212+
const char* sector_str = strstr(line, "LOOP ");
213+
if (sector_str) {
214+
sector_str = skip_whitespace(sector_str + 5);
215+
current_track.loop_sector = atoi(sector_str); // 0 or positive = loop enabled
216+
}
195217
}
196-
// REM NOLOOP command
197-
else if (strstr(line, " REM NOLOOP")) {
198-
current_track.loop_sector = -1;
218+
else if (line_starts_with(line, "REM LOOP")) {
219+
// "REM LOOP" without sector = loop from beginning (sector 0)
220+
current_track.loop_sector = 0;
221+
}
222+
else if (line_starts_with(line, "REM NOLOOP")) {
223+
current_track.loop_sector = -1; // Explicitly disable looping
199224
}
200225
}
201226

202227
// Save last track
203-
if (track_has_index && track_number > 0 && current_track.file_path.get_length() > 0) {
228+
if (track_has_index && track_count > 0 && current_track.file_path.get_length() > 0) {
204229
m_tracks.add_item(current_track);
205230
}
206231

232+
// Validate we found at least one track
207233
if (m_tracks.get_count() == 0) {
208-
throw exception_io_data("No tracks found in MDPCUE file");
234+
throw exception_io_data("No valid tracks found in MDPCUE file");
209235
}
210236
}
211237

@@ -224,15 +250,17 @@ namespace {
224250

225251
const track_info& track = m_tracks[subsong];
226252

227-
// Open audio file and get its metadata
253+
// Open audio file and retrieve format information
254+
// NOTE: This gets technical audio specs (sample rate, channels, duration)
255+
// NOT embedded metadata/tags that could cause loop issues
228256
service_ptr_t<file> f;
229257
filesystem::g_open(f, track.file_path, filesystem::open_mode_read, abort);
230258

231259
input_decoder::ptr decoder;
232260
input_entry::g_open_for_decoding(decoder, f, track.file_path, abort);
233261
decoder->get_info(0, info, abort);
234262

235-
// Add track number
263+
// Add track number for foobar2000 playlist display
236264
info.meta_set("tracknumber", pfc::format_int(subsong + 1));
237265
}
238266

@@ -250,12 +278,12 @@ namespace {
250278

251279
const track_info& track = m_tracks[subsong];
252280

253-
// Open audio file
281+
// Open audio file for decoding
254282
filesystem::g_open(m_file, track.file_path, filesystem::open_mode_read, abort);
255283
input_entry::g_open_for_decoding(m_decoder, m_file, track.file_path, abort);
256284

257-
// Wrap decoder with looping if needed
258-
if (track.loop_sector > 0) {
285+
// Wrap decoder with looping support if track has loop enabled (>= 0)
286+
if (track.loop_sector >= 0) {
259287
m_decoder = new service_impl_t<looping_decoder>(m_decoder, track.loop_sector);
260288
}
261289

@@ -291,13 +319,15 @@ namespace {
291319

292320
static input_factory_t<input_mdplus> g_input_mdplus_factory;
293321

294-
// Initialization
322+
// Initialization message
295323
class mdplus_initquit : public initquit {
296324
public:
297325
virtual void on_init() {
298-
console::print("MD+ Audio Player v1.0 initialized");
299-
console::print("Rename your MD+ .cue file extension to .mdpcue for seamless looping audio playback support");
326+
console::print("==========================================");
327+
console::print("MD+ Audio Player v1.1 initialized");
328+
console::print("Usage: Rename .cue files to .mdpcue");
300329
console::print("Example: game_name.cue -> game_name.mdpcue");
330+
console::print("==========================================");
301331
}
302332

303333
virtual void on_quit() {

0 commit comments

Comments
 (0)