Cog/Plugins/libvgmPlayer/libvgmDecoder.mm
Christopher Snowhill 0263367639 libvgm: Fix track ending without glitches
Track rendering returns the number of bytes rendered, which also cuts
short when the track ends. So properly react to this count when running
the player, so we don't keep trying to render, or pick up uninitialized
data from the buffer.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-07-17 03:43:56 -07:00

330 lines
7.8 KiB
Text

//
// libvgmDecoder.mm
// libvgmPlayer
//
// Created by Christopher Snowhill on 1/02/22.
// Copyright 2022-2025 __LoSnoCo__. All rights reserved.
//
#import "libvgmDecoder.h"
#import "Logging.h"
#import "PlaylistController.h"
#import <libvgm/emu/EmuCores.h>
#import <libvgm/emu/Resampler.h>
#import <libvgm/emu/SoundDevs.h>
#import <libvgm/player/droplayer.hpp>
#import <libvgm/player/gymplayer.hpp>
#import <libvgm/player/s98player.hpp>
#import <libvgm/player/vgmplayer.hpp>
#import <libvgm/utils/FileLoader.h>
#import <libvgm/utils/MemoryLoader.h>
@implementation libvgmDecoder
#ifdef _DEBUG
const int logLevel = DEVLOG_DEBUG;
#else
const int logLevel = DEVLOG_INFO;
#endif
static UINT8 FilePlayCallback(PlayerBase* player, void* userParam, UINT8 evtType, void* evtParam) {
libvgmDecoder* decoder = (__bridge libvgmDecoder*)(userParam);
switch(evtType) {
case PLREVT_START:
// printf("Playback started.\n");
break;
case PLREVT_STOP:
// printf("Playback stopped.\n");
break;
case PLREVT_LOOP: {
// UINT32* curLoop = (UINT32*)evtParam;
// if (player->GetState() & PLAYSTATE_SEEK)
// break;
} break;
case PLREVT_END:
if([decoder trackEnded])
break;
[decoder setTrackEnded:YES];
break;
}
return 0x00;
}
#include "yrw801.h"
static DATA_LOADER* RequestFileCallback(void* userParam, PlayerBase* player, const char* fileName) {
DATA_LOADER* dLoad;
if(strcmp(fileName, "yrw801.rom") == 0) {
dLoad = MemoryLoader_Init(yrw801_rom, sizeof(yrw801_rom));
} else {
dLoad = FileLoader_Init(fileName);
}
UINT8 retVal = DataLoader_Load(dLoad);
if(!retVal)
return dLoad;
DataLoader_Deinit(dLoad);
return NULL;
}
static const char* LogLevel2Str(UINT8 level) {
static const char* LVL_NAMES[6] = { " ??? ", "Error", "Warn ", "Info ", "Debug", "Trace" };
if(level >= (sizeof(LVL_NAMES) / sizeof(LVL_NAMES[0])))
level = 0;
return LVL_NAMES[level];
}
static void PlayerLogCallback(void* userParam, PlayerBase* player, UINT8 level, UINT8 srcType,
const char* srcTag, const char* message) {
if(level > logLevel)
return; // don't print messages with higher verbosity than current log level
if(srcType == PLRLOGSRC_PLR) {
ALog(@"[%s] %s: %s", LogLevel2Str(level), player->GetPlayerName(), message);
} else {
ALog(@"[%s] %s %s: %s", LogLevel2Str(level), player->GetPlayerName(), srcTag, message);
}
return;
}
const int numChannels = 2;
const int numBitsPerSample = 24;
const int smplAlloc = 2048;
const int masterVol = 0x10000; // Fixed point 16.16
- (id)init {
self = [super init];
if(self) {
fileData = NULL;
dLoad = NULL;
mainPlr = NULL;
}
return self;
}
- (BOOL)open:(id<CogSource>)s {
[self setSource:s];
// We need file-size to use GME
if(![source seekable]) {
return NO;
}
sampleRate = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthSampleRate"] doubleValue];
if(sampleRate < 8000.0) {
sampleRate = 44100.0;
} else if(sampleRate > 192000.0) {
sampleRate = 192000.0;
}
loopCount = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthDefaultLoopCount"] longValue];
if(loopCount < 1) {
loopCount = 1;
} else if(loopCount > 10) {
loopCount = 10;
}
fadeTime = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthDefaultFadeSeconds"] doubleValue];
BOOL repeatOne = IsRepeatOneSet();
uint32_t maxLoops = repeatOne ? 0 : (uint32_t)loopCount;
mainPlr = new PlayerA;
mainPlr->RegisterPlayerEngine(new VGMPlayer);
mainPlr->RegisterPlayerEngine(new S98Player);
mainPlr->RegisterPlayerEngine(new DROPlayer);
mainPlr->RegisterPlayerEngine(new GYMPlayer);
mainPlr->SetEventCallback(FilePlayCallback, (__bridge void*)(self));
mainPlr->SetFileReqCallback(RequestFileCallback, NULL);
mainPlr->SetLogCallback(PlayerLogCallback, NULL);
{
PlayerA::Config pCfg = mainPlr->GetConfiguration();
pCfg.masterVol = masterVol;
pCfg.loopCount = maxLoops;
pCfg.fadeSmpls = (int)ceil(sampleRate * fadeTime); // fade over configured duration
pCfg.endSilenceSmpls = sampleRate / 2; // 0.5 seconds of silence at the end
pCfg.pbSpeed = 1.0;
mainPlr->SetConfiguration(pCfg);
}
mainPlr->SetOutputSettings((int)ceil(sampleRate), numChannels, numBitsPerSample, smplAlloc);
[source seek:0 whence:SEEK_END];
size_t size = [source tell];
[source seek:0 whence:SEEK_SET];
fileData = (UINT8*)malloc(size);
if(!fileData)
return NO;
size_t bytesRead = [source read:fileData amount:size];
if(bytesRead != size)
return NO;
dLoad = MemoryLoader_Init(fileData, (unsigned int)size);
if(!dLoad)
return NO;
DataLoader_SetPreloadBytes(dLoad, 0x100);
if(DataLoader_Load(dLoad))
return NO;
if(mainPlr->LoadFile(dLoad))
return NO;
PlayerBase* player = mainPlr->GetPlayer();
mainPlr->SetLoopCount(maxLoops);
if(player->GetPlayerType() == FCC_VGM) {
VGMPlayer* vgmplay = dynamic_cast<VGMPlayer*>(player);
mainPlr->SetLoopCount(vgmplay->GetModifiedLoopCount(maxLoops));
}
length = player->Tick2Second(player->GetTotalTicks()) * sampleRate;
[self setTrackEnded:NO];
mainPlr->Start();
[self willChangeValueForKey:@"properties"];
[self didChangeValueForKey:@"properties"];
return YES;
}
- (NSDictionary*)properties {
return @{ @"bitrate": @(0),
@"sampleRate": @(sampleRate),
@"totalFrames": @(length),
@"bitsPerSample": @(numBitsPerSample),
@"channels": @(numChannels),
@"seekable": @(YES),
@"endian": @"host",
@"encoding": @"synthesized" };
}
- (NSDictionary *)metadata {
return @{};
}
- (AudioChunk*)readAudio {
if([self trackEnded])
return nil;
id audioChunkClass = NSClassFromString(@"AudioChunk");
AudioChunk* chunk = [[audioChunkClass alloc] initWithProperties:[self properties]];
int frames = 1024;
const size_t bytesPerFrame = [chunk format].mBytesPerFrame;
uint8_t buffer[frames * bytesPerFrame];
void* buf = (void*)buffer;
BOOL repeatOne = IsRepeatOneSet();
uint32_t maxLoops = repeatOne ? 0 : (uint32_t)loopCount;
PlayerBase* player = mainPlr->GetPlayer();
mainPlr->SetLoopCount(maxLoops);
if(player->GetPlayerType() == FCC_VGM) {
VGMPlayer* vgmplay = dynamic_cast<VGMPlayer*>(player);
mainPlr->SetLoopCount(vgmplay->GetModifiedLoopCount(maxLoops));
}
double streamTimestamp = mainPlr->GetCurTime(0);
UInt32 framesDone = 0;
while(framesDone < frames) {
UInt32 framesToDo = frames - framesDone;
if(framesToDo > smplAlloc)
framesToDo = smplAlloc;
int numSamples = framesToDo * numChannels * (numBitsPerSample / 8);
UINT32 numRendered = mainPlr->Render(numSamples, buf);
buf = (void*)(((uint8_t*)buf) + numRendered);
UINT32 framesRendered = numRendered / (numChannels * (numBitsPerSample / 8));
framesDone += framesRendered;
if(framesRendered < framesToDo)
break;
}
[chunk setStreamTimestamp:streamTimestamp];
[chunk assignSamples:buffer frameCount:framesDone];
return chunk;
}
- (long)seek:(long)frame {
[self setTrackEnded:NO];
mainPlr->Seek(PLAYPOS_SAMPLE, (unsigned int)frame);
return frame;
}
- (void)close {
if(mainPlr) {
mainPlr->Stop();
mainPlr->UnloadFile();
delete mainPlr;
mainPlr = NULL;
}
if(dLoad) {
DataLoader_Deinit(dLoad);
dLoad = NULL;
}
if(fileData) {
free(fileData);
fileData = NULL;
}
}
- (void)dealloc {
[self close];
}
+ (NSArray*)fileTypes {
return @[@"vgm", @"vgz", @"s98", @"dro", @"gym"];
}
+ (NSArray*)mimeTypes {
return nil;
}
+ (float)priority {
return 1.25;
}
+ (NSArray*)fileTypeAssociations {
NSMutableArray* ret = [[NSMutableArray alloc] init];
[ret addObject:@"libvgm Files"];
[ret addObject:@"vg.icns"];
[ret addObjectsFromArray:[self fileTypes]];
return @[[NSArray arrayWithArray:ret]];
}
- (void)setSource:(id<CogSource>)s {
source = s;
}
- (id<CogSource>)source {
return source;
}
- (BOOL)trackEnded {
return trackEnded;
}
- (void)setTrackEnded:(BOOL)ended {
trackEnded = ended;
}
@end