cog/Plugins/Organya/OrganyaDecoder.mm

512 lines
16 KiB
Plaintext

//
// OrganyaDecoder.m
// Organya
//
// Created by Christopher Snowhill on 12/4/22.
//
#import <Cocoa/Cocoa.h>
#import "OrganyaDecoder.h"
#import "AudioChunk.h"
#import "PlaylistController.h"
#import <cstdio>
#import <cstring>
#import <cstdlib>
#import <vector>
#import <cmath>
#import <map>
/* SIMPLE CAVE STORY MUSIC PLAYER (Organya) */
/* Written by Joel Yliluoma -- http://iki.fi/bisqwit/ */
/* NX-Engine source code was used as reference. */
/* Cave Story and its music were written by Pixel ( 天谷 大輔 ) */
namespace Organya {
//========= PART 0 : INPUT/OUTPUT AND UTILITY ========//
using std::fgetc;
int fgetw(FILE* fp) { int a = fgetc(fp), b = fgetc(fp); return (b<<8) + a; }
int fgetd(FILE* fp) { int a = fgetw(fp), b = fgetw(fp); return (b<<16) + a; }
double fgetv(FILE* fp) // Load a numeric value from text file; one per line.
{
char Buf[4096], *p=Buf; Buf[4095]='\0';
if(!std::fgets(Buf, sizeof(Buf)-1, fp)) return 0.0;
// Ignore empty lines. If the line was empty, try next line.
if(!Buf[0] || Buf[0]=='\r' || Buf[0]=='\n') return fgetv(fp);
while(*p && *p++ != ':') {} // Skip until a colon character.
return std::strtod(p, 0); // Parse the value and return it.
}
int coggetc(id<CogSource> fp) {
uint8_t value;
if([fp read:&value amount:sizeof(value)] != sizeof(value)) {
return -1;
}
return value;
}
int coggetw(id<CogSource> fp) { int a = coggetc(fp); int b = coggetc(fp); return (b<<8) + a; }
int coggetd(id<CogSource> fp) { int a = coggetw(fp); int b = coggetw(fp); return (b<<16) + a; }
//========= PART 1 : SOUND EFFECT PLAYER (PXT) ========//
static signed char Waveforms[6][256];
static void GenerateWaveforms(void) {
/* Six simple waveforms are used as basis for the signal generators in PXT: */
for(unsigned seed=0, i=0; i<256; ++i) {
/* These waveforms are bit-exact with PixTone v1.0.3. */
seed = (seed * 214013) + 2531011; // Linear congruential generator
Waveforms[0][i] = 0x40 * std::sin(i * 3.1416 / 0x80); // Sine
Waveforms[1][i] = ((0x40+i) & 0x80) ? 0x80-i : i; // Triangle
Waveforms[2][i] = -0x40 + i/2; // Sawtooth up
Waveforms[3][i] = 0x40 - i/2; // Sawtooth down
Waveforms[4][i] = 0x40 - (i & 0x80); // Square
Waveforms[5][i] = (signed char)(seed >> 16) / 2; // Pseudorandom
}
}
struct Pxt {
struct Channel {
bool enabled;
int nsamples;
// Waveform generator
struct Wave {
const signed char* wave;
double pitch;
int level, offset;
};
Wave carrier; // The main signal to be generated.
Wave frequency; // Modulator to the main signal.
Wave amplitude; // Modulator to the main signal.
// Envelope generator (controls the overall amplitude)
struct Env {
int initial; // initial value (0-63)
struct { int time, val; } p[3]; // time offset & value, three of them
int Evaluate(int i) const { // Linearly interpolate between the key points:
int prevval = initial, prevtime=0;
int nextval = 0, nexttime=256;
for(int j=2; j>=0; --j) if(i < p[j].time) { nexttime=p[j].time; nextval=p[j].val; }
for(int j=0; j<=2; ++j) if(i >=p[j].time) { prevtime=p[j].time; prevval=p[j].val; }
if(nexttime <= prevtime) return prevval;
return (i-prevtime) * (nextval-prevval) / (nexttime-prevtime) + prevval;
}
} envelope;
// Synthesize the sound effect.
std::vector<int> Synth() {
if(!enabled) return {};
std::vector<int> result(nsamples);
auto& c = carrier, &f = frequency, &a = amplitude;
double mainpos = c.offset, maindelta = 256*c.pitch/nsamples;
for(size_t i=0; i<result.size(); ++i) {
auto s = [=](double p=1) { return 256*p*i/nsamples; };
// Take sample from each of the three signal generators:
int freqval = f.wave[0xFF & int(f.offset + s(f.pitch))] * f.level;
int ampval = a.wave[0xFF & int(a.offset + s(a.pitch))] * a.level;
int mainval = c.wave[0xFF & int(mainpos) ] * c.level;
// Apply amplitude & envelope to the main signal level:
result[i] = mainval * (ampval+4096) / 4096 * envelope.Evaluate(s()) / 4096;
// Apply frequency modulation to maindelta:
mainpos += maindelta * (1 + (freqval / (freqval<0 ? 8192. : 2048.)));
}
return result;
}
} channels[4]; /* Four parallel FM-AM modulators with envelope generators. */
void Load(FILE* fp) { // Load PXT file from disk and initialize synthesizer.
/* C++11 simplifies things by a great deal. */
/* This function would be a lot more complex without it. */
auto f = [=](){ return (int) fgetv(fp); };
for(auto&c: channels)
c = { f() != 0, f(), // enabled, length
{ Waveforms[f()%6], fgetv(fp), f(), f() }, // carrier wave
{ Waveforms[f()%6], fgetv(fp), f(), f() }, // frequency wave
{ Waveforms[f()%6], fgetv(fp), f(), f() }, // amplitude wave
{ f(), { {f(),f()}, {f(),f()}, {f(),f()} } } // envelope
};
}
};
//========= PART 2 : SONG PLAYER (ORG) ========//
/* Note: Requires PXT synthesis for percussion (drums). */
static short WaveTable[100*256];
static std::vector<short> DrumSamples[12];
void LoadWaveTable(void) {
NSURL *url = [[NSBundle bundleWithIdentifier:@"co.losno.Organya"] URLForResource:@"wavetable" withExtension:@"dat"];
if(!url) return;
NSString *path = [url path];
FILE* fp = std::fopen([path UTF8String], "rb");
if(!fp) return;
for(size_t a=0; a<100*256; ++a)
WaveTable[a] = (signed char) fgetc(fp);
std::fclose(fp);
}
void LoadDrums(void) {
GenerateWaveforms();
/* List of PXT files containing these percussion instruments: */
static const int patch[] = {0x96,0,0x97,0, 0x9a,0x98,0x99,0, 0x9b,0,0,0};
for(unsigned drumno=0; drumno<12; ++drumno)
{
if(!patch[drumno]) continue; // Leave that non-existed drum file unloaded
// Load the drum parameters
char Buf[64] = {};
std::snprintf(Buf, sizeof(Buf)-1, "fx%02x", patch[drumno]);
NSURL *url = [[NSBundle bundleWithIdentifier:@"co.losno.Organya"] URLForResource:[NSString stringWithUTF8String:Buf] withExtension:@"pxt"];
if(!url) continue;
NSString *path = [url path];
FILE* fp = std::fopen([path UTF8String], "rb");
if(!fp) continue;
Pxt d;
d.Load(fp);
std::fclose(fp);
// Synthesize and mix the drum's component channels
auto& sample = DrumSamples[drumno];
for(auto& c: d.channels)
{
auto buf = c.Synth();
if(buf.size() > sample.size()) sample.resize(buf.size());
for(size_t a=0; a<buf.size(); ++a)
sample[a] += buf[a];
}
}
}
struct Song {
int ms_per_beat, samples_per_beat, loop_start, loop_end;
int cur_beat, total_beats;
int loop_count;
struct Ins {
int tuning, wave;
bool pi; // true=all notes play for exactly 1024 samples.
std::size_t n_events;
struct Event { int note, length, volume, panning; };
std::map<int/*beat*/, Event> events;
// Volatile data, used & changed during playback:
double phaseacc, phaseinc, cur_vol;
int cur_pan, cur_length, cur_wavesize;
const short* cur_wave;
} ins[16];
BOOL Load(id<CogSource> fp) {
[fp seek:0 whence:SEEK_SET];
char Signature[6];
if([fp read:Signature amount:sizeof(Signature)] != sizeof(Signature))
return NO;
if(memcmp(Signature, "Org-02", 6) != 0)
return NO;
// Load song parameters
ms_per_beat = coggetw(fp);
/*steps_per_bar =*/coggetc(fp); // irrelevant
/*beats_per_step=*/coggetc(fp); // irrelevant
loop_start = coggetd(fp);
loop_end = coggetd(fp);
// Load each instrument parameters (and initialize them)
for(auto& i: ins)
i = { coggetw(fp), coggetc(fp), coggetc(fp)!=0, (unsigned)coggetw(fp),
{}, 0,0,0,0,0,0,0 };
// Load events for each instrument
for(auto& i: ins)
{
std::vector<std::pair<int,Ins::Event>> events( i.n_events );
for(auto& n: events) n.first = coggetd(fp);
for(auto& n: events) n.second.note = coggetc(fp);
for(auto& n: events) n.second.length = coggetc(fp);
for(auto& n: events) n.second.volume = coggetc(fp);
for(auto& n: events) n.second.panning = coggetc(fp);
i.events.insert(events.begin(), events.end());
}
return YES;
}
void Reset(void) {
cur_beat = 0;
total_beats = 0;
loop_count = 0;
}
std::vector<float> Synth(double sampling_rate)
{
// Determine playback settings:
double samples_per_millisecond = sampling_rate * 1e-3, master_volume = 4e-6;
int samples_per_beat = ms_per_beat * samples_per_millisecond; // rounded.
// Begin synthesis
{
if(cur_beat == loop_end) {
cur_beat = loop_start;
loop_count++;
}
// Synthesize this beat in stereo sound (two channels).
std::vector<float> result( samples_per_beat * 2, 0.f );
for(auto &i: ins)
{
// Check if there is an event for this beat
auto j = i.events.find(cur_beat);
if(j != i.events.end())
{
auto& event = j->second;
if(event.volume != 255) i.cur_vol = event.volume * master_volume;
if(event.panning != 255) i.cur_pan = event.panning;
if(event.note != 255)
{
// Calculate the note's wave data sampling frequency (equal temperament)
double freq = std::pow(2.0, (event.note + i.tuning/1000.0 + 155.376) / 12);
// Note: 155.376 comes from:
// 12*log(256*440)/log(2) - (4*12-3-1) So that note 4*12-3 plays at 440 Hz.
// Note: Optimizes into
// pow(2, (note+155.376 + tuning/1000.0) / 12.0)
// 2^(155.376/12) * exp( (note + tuning/1000.0)*log(2)/12 )
// i.e. 7901.988*exp(0.057762265*(note + tuning*1e-3))
i.phaseinc = freq / sampling_rate;
i.phaseacc = 0;
// And determine the actual wave data to play
i.cur_wave = &WaveTable[256 * (i.wave % 100)];
i.cur_wavesize = 256;
i.cur_length = i.pi ? 1024/i.phaseinc : (event.length * samples_per_beat);
if(&i >= &ins[8]) // Percussion is different
{
const auto& d = DrumSamples[i.wave % 12];
i.phaseinc = event.note * (22050/32.5) / sampling_rate; // Linear frequency
i.cur_wave = &d[0];
i.cur_wavesize = (int) d.size();
i.cur_length = d.size() / i.phaseinc;
}
// Ignore missing drum samples
if(i.cur_wavesize <= 0) i.cur_length = 0;
}
}
// Generate wave data. Calculate left & right volumes...
auto left = (i.cur_pan > 6 ? 12 - i.cur_pan : 6) * i.cur_vol;
auto right = (i.cur_pan < 6 ? i.cur_pan : 6) * i.cur_vol;
int n = samples_per_beat > i.cur_length ? i.cur_length : samples_per_beat;
for(int p=0; p<n; ++p)
{
double pos = i.phaseacc;
// Take a sample from the wave data.
/* We could do simply this: */
//int sample = i.cur_wave[ unsigned(pos) % i.cur_wavesize ];
/* But since we have plenty of time, use neat Lanczos filtering. */
/* This improves especially the low rumble noises substantially. */
enum { radius = 2 };
auto lanczos = [](double d) -> double
{
if(d == 0.) return 1.;
if(std::fabs(d) > radius) return 0.;
double dr = (d *= 3.14159265) / radius;
return std::sin(d) * std::sin(dr) / (d*dr);
};
double scale = 1/i.phaseinc > 1 ? 1 : 1/i.phaseinc, density = 0, sample = 0;
int min = -radius/scale + pos - 0.5;
int max = radius/scale + pos + 0.5;
for(int m=min; m<max; ++m) // Collect a weighted average.
{
double factor = lanczos( (m-pos+0.5) * scale );
density += factor;
sample += i.cur_wave[m<0 ? 0 : m%i.cur_wavesize] * factor;
}
if(density > 0.) sample /= density; // Normalize
// Save audio in float32 format:
result[p*2 + 0] += sample * left;
result[p*2 + 1] += sample * right;
i.phaseacc += i.phaseinc;
}
i.cur_length -= n;
}
cur_beat++;
return result;
}
}
};
}
@implementation OrganyaDecoder
// Need this static initializer to create the static global tables that sidplayfp doesn't really lock access to
+ (void)initialize {
Organya::LoadWaveTable();
Organya::LoadDrums();
}
- (BOOL)open:(id<CogSource>)s {
[self setSource:s];
sampleRate = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthSampleRate"] doubleValue];
if(sampleRate < 8000.0) {
sampleRate = 44100.0;
} else if(sampleRate > 192000.0) {
sampleRate = 192000.0;
}
m_song = new Organya::Song;
if(!m_song->Load(s)) {
return NO;
}
long loopCount = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthDefaultLoopCount"] intValue];
double fadeTime = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthDefaultFadeSeconds"] doubleValue];
if(fadeTime < 0.0) {
fadeTime = 0.0;
}
long beatsToEnd = m_song->loop_start + (m_song->loop_end - m_song->loop_start) * loopCount;
double lengthOfSong = ((double)m_song->ms_per_beat * 1e-3) * (double)beatsToEnd;
length = (int)ceil(lengthOfSong * sampleRate);
lengthWithFade = (int)ceil((lengthOfSong + fadeTime) * sampleRate);
renderedTotal = 0.0;
fadeTotal = fadeRemain = (int)ceil(sampleRate * fadeTime);
samplesDiscard = 0;
m_song->Reset();
[self willChangeValueForKey:@"properties"];
[self didChangeValueForKey:@"properties"];
return YES;
}
- (NSDictionary *)properties {
return @{ @"bitrate": @(0),
@"sampleRate": @(sampleRate),
@"totalFrames": @(lengthWithFade),
@"bitsPerSample": @(32),
@"floatingPoint": @(YES),
@"channels": @(2),
@"seekable": @(YES),
@"endian": @"host",
@"encoding": @"synthesized" };
}
- (NSDictionary *)metadata {
return @{};
}
- (AudioChunk *)readAudio {
int total = 0;
std::vector<float> samples = m_song->Synth(sampleRate);
int rendered = (int)(samples.size() / 2);
renderedTotal += rendered;
if(!IsRepeatOneSet() && renderedTotal >= length) {
float *sampleBuf = &samples[0];
long fadeEnd = fadeRemain - rendered;
if(fadeEnd < 0)
fadeEnd = 0;
float fadePosf = (float)fadeRemain / (float)fadeTotal;
const float fadeStep = 1.0f / (float)fadeTotal;
for(long fadePos = fadeRemain; fadePos > fadeEnd; --fadePos, fadePosf -= fadeStep) {
long offset = (fadeRemain - fadePos) * 2;
sampleBuf[offset + 0] *= fadePosf;
sampleBuf[offset + 1] *= fadePosf;
}
rendered = (int)(fadeRemain - fadeEnd);
fadeRemain = fadeEnd;
}
id audioChunkClass = NSClassFromString(@"AudioChunk");
AudioChunk *chunk = [[audioChunkClass alloc] initWithProperties:[self properties]];
if(samplesDiscard) {
[chunk assignSamples:&samples[samplesDiscard * 2] frameCount:rendered - samplesDiscard];
samplesDiscard = 0;
} else {
[chunk assignSamples:&samples[0] frameCount:rendered];
}
return chunk;
}
- (long)seek:(long)frame {
long originalFrame = frame;
if(frame < renderedTotal) {
m_song->Reset();
renderedTotal = 0;
fadeRemain = fadeTotal;
}
long msPerLoop = (m_song->loop_end - m_song->loop_start) * m_song->ms_per_beat;
long msIntro = m_song->loop_start * m_song->ms_per_beat;
long samplesPerBeat = (long)ceil(m_song->ms_per_beat * 1e-3 * sampleRate);
long samplesPerLoop = (long)ceil(msPerLoop * 1e-3 * sampleRate);
long samplesIntro = (long)ceil(msIntro * 1e-3 * sampleRate);
if(samplesPerLoop) {
while (frame >= (samplesIntro + samplesPerLoop)) {
frame -= samplesPerLoop;
m_song->loop_count++;
}
}
long beatTarget = frame / samplesPerBeat;
samplesDiscard = frame % samplesPerBeat;
m_song->cur_beat = (int) beatTarget;
return originalFrame;
}
- (void)cleanUp {
if(m_song) {
delete m_song;
m_song = NULL;
}
source = nil;
}
- (void)close {
[self cleanUp];
}
- (void)dealloc {
[self close];
}
- (void)setSource:(id<CogSource>)s {
source = s;
}
- (id<CogSource>)source {
return source;
}
+ (NSArray *)fileTypes {
return @[@"org"];
}
+ (NSArray *)mimeTypes {
return nil;
}
+ (float)priority {
return 1.0;
}
+ (NSArray *)fileTypeAssociations {
return @[
@[@"Organya File", @"vg.icns", @"org"],
];
}
@end