// // OrganyaDecoder.m // Organya // // Created by Christopher Snowhill on 12/4/22. // #import #import "OrganyaDecoder.h" #import "AudioChunk.h" #import "PlaylistController.h" #import #import #import #import #import #import /* 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 fp) { uint8_t value; if([fp read:&value amount:sizeof(value)] != sizeof(value)) { return -1; } return value; } int coggetw(id fp) { int a = coggetc(fp); int b = coggetc(fp); return (b<<8) + a; } int coggetd(id 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 Synth() { if(!enabled) return {}; std::vector result(nsamples); auto& c = carrier, &f = frequency, &a = amplitude; double mainpos = c.offset, maindelta = 256*c.pitch/nsamples; for(size_t i=0; i 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 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 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> 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 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 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 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 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)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 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)s { source = s; } - (id)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