512 lines
16 KiB
Plaintext
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
|