Audio Chunks now have full timestamp accounting, including DSP playback speed ratio for the one DSP that can change play ratio, Rubber Band. Inputs which support looping and actually reporting the absolute play position now do so. Signed-off-by: Christopher Snowhill <kode54@gmail.com>
325 lines
7.6 KiB
Text
325 lines
7.6 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);
|
|
|
|
mainPlr->Render(numSamples, buf);
|
|
|
|
buf = (void*)(((uint8_t*)buf) + numSamples);
|
|
|
|
framesDone += framesToDo;
|
|
}
|
|
|
|
[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
|