using System; using System.Collections.Generic; namespace BizHawk.Emulation.Sound { /// /// uses Metaspu to have an ISyncSoundProvider input to a ISoundProvider /// public class MetaspuAsync : ISoundProvider { ISynchronizingAudioBuffer buffer; ISyncSoundProvider input; public MetaspuAsync(ISyncSoundProvider input, ESynchMethod method) { buffer = Metaspu.metaspu_construct(method); this.input = input; } public void GetSamples(short[] samples) { short[] sampin; int numsamp; input.GetSamples(out sampin, out numsamp); buffer.enqueue_samples(sampin, numsamp); buffer.output_samples(samples, samples.Length / 2); } public void DiscardSamples() { input.DiscardSamples(); buffer.clear(); } public int MaxVolume { get; set; } } public class MetaspuSoundProvider : ISoundProvider { public ISynchronizingAudioBuffer buffer; public MetaspuSoundProvider(ESynchMethod method) { buffer = Metaspu.metaspu_construct(method); } public MetaspuSoundProvider() : this(ESynchMethod.ESynchMethod_V) { } short[] pullBuffer = new short[1470]; public void PullSamples(ISoundProvider source) { Array.Clear(pullBuffer, 0, 1470); source.GetSamples(pullBuffer); buffer.enqueue_samples(pullBuffer, 735); } public void GetSamples(short[] samples) { buffer.output_samples(samples, samples.Length / 2); } public void DiscardSamples() { buffer.clear(); } public int MaxVolume { get; set; } } public interface ISynchronizingAudioBuffer { void enqueue_samples(short[] buf, int samples_provided); void enqueue_sample(short left, short right); void clear(); //returns the number of samples actually supplied, which may not match the number requested // ^^ what the hell is that supposed to mean. // the entire point of an ISynchronzingAudioBuffer // is to provide exact amounts of output samples, // even when the input provided varies.... int output_samples(short[] buf, int samples_requested); }; public enum ESynchMethod { ESynchMethod_N, //nitsuja's ESynchMethod_Z, //zero's //ESynchMethod_P, //PCSX2 spu2-x //ohno! not available yet in c# ESynchMethod_V // vecna }; public static class Metaspu { public static ISynchronizingAudioBuffer metaspu_construct(ESynchMethod method) { switch (method) { case ESynchMethod.ESynchMethod_Z: return new ZeromusSynchronizer(); case ESynchMethod.ESynchMethod_N: return new NitsujaSynchronizer(); case ESynchMethod.ESynchMethod_V: return new VecnaSynchronizer(); default: return new NitsujaSynchronizer(); } } } class ZeromusSynchronizer : ISynchronizingAudioBuffer { public ZeromusSynchronizer() { //#ifdef NDEBUG adjustobuf = new Adjustobuf(200, 1000); //#else //adjustobuf = new Adjustobuf(22000, 44000); //#endif } //adjustobuf(200,1000) bool mixqueue_go = false; public void clear() { adjustobuf.clear(); } public void enqueue_sample(short left, short right) { adjustobuf.enqueue(left, right); } public void enqueue_samples(short[] buf, int samples_provided) { int ctr = 0; for (int i = 0; i < samples_provided; i++) { short left = buf[ctr++]; short right = buf[ctr++]; adjustobuf.enqueue(left, right); } } //returns the number of samples actually supplied, which may not match the number requested public int output_samples(short[] buf, int samples_requested) { int ctr=0; int done = 0; if (!mixqueue_go) { if (adjustobuf.size > 200) mixqueue_go = true; } else { for (int i = 0; i < samples_requested; i++) { if (adjustobuf.size == 0) { mixqueue_go = false; break; } done++; short left, right; adjustobuf.dequeue(out left, out right); buf[ctr++] = left; buf[ctr++] = right; } } return done; } Adjustobuf adjustobuf; class Adjustobuf { public Adjustobuf(int _minLatency, int _maxLatency) { minLatency = _minLatency; maxLatency = _maxLatency; clear(); } float rate, cursor; int minLatency, targetLatency, maxLatency; Queue buffer = new Queue(); Queue statsHistory = new Queue(); public int size = 0; short[] curr = new short[2]; public void clear() { buffer.Clear(); statsHistory.Clear(); rollingTotalSize = 0; targetLatency = (maxLatency + minLatency) / 2; rate = 1.0f; cursor = 0.0f; curr[0] = curr[1] = 0; kAverageSize = 80000; size = 0; } public void enqueue(short left, short right) { buffer.Enqueue(left); buffer.Enqueue(right); size++; } long rollingTotalSize; uint kAverageSize; void addStatistic() { statsHistory.Enqueue(size); rollingTotalSize += size; if (statsHistory.Count > kAverageSize) { rollingTotalSize -= statsHistory.Peek(); statsHistory.Dequeue(); float averageSize = (float)(rollingTotalSize / kAverageSize); //static int ctr=0; ctr++; if((ctr&127)==0) printf("avg size: %f curr size: %d rate: %f\n",averageSize,size,rate); { float targetRate; if(averageSize < targetLatency) { targetRate = 1.0f - (targetLatency-averageSize)/kAverageSize; } else if(averageSize > targetLatency) { targetRate = 1.0f + (averageSize-targetLatency)/kAverageSize; } else targetRate = 1.0f; //rate = moveValueTowards(rate,targetRate,0.001f); rate = targetRate; } } } public void dequeue(out short left, out short right) { left = right = 0; addStatistic(); if(size==0) { return; } cursor += rate; while(cursor>1.0f) { cursor -= 1.0f; if(size>0) { curr[0] = buffer.Dequeue(); curr[1] = buffer.Dequeue(); size--; } } left = curr[0]; right = curr[1]; } } } class NitsujaSynchronizer : ISynchronizingAudioBuffer { struct ssamp { public short l, r; public ssamp(short ll, short rr) { l = ll; r = rr; } }; List sampleQueue = new List(); // returns values going between 0 and y-1 in a saw wave pattern, based on x static int pingpong(int x, int y) { x %= 2*y; if(x >= y) x = 2*y - x - 1; return x; // in case we want to switch to odd buffer sizes for more sharpness //x %= 2*(y-1); //if(x >= y) // x = 2*(y-1) - x; //return x; } static ssamp crossfade (ssamp lhs, ssamp rhs, int cur, int start, int end) { if(cur <= start) return lhs; if(cur >= end) return rhs; // in case we want sine wave interpolation instead of linear here //float ang = 3.14159f * (float)(cur - start) / (float)(end - start); //cur = start + (int)((1-cosf(ang))*0.5f * (end - start)); int inNum = cur - start; int outNum = end - cur; int denom = end - start; int lrv = ((int)lhs.l * outNum + (int)rhs.l * inNum) / denom; int rrv = ((int)lhs.r * outNum + (int)rhs.r * inNum) / denom; return new ssamp((short)lrv,(short)rrv); } public void clear() { sampleQueue.Clear(); } static void emit_sample(short[] outbuf, ref int cursor, ssamp sample) { outbuf[cursor++] = sample.l; outbuf[cursor++] = sample.r; } static void emit_samples(short[] outbuf, ref int outcursor, ssamp[] samplebuf, int incursor, int samples) { for(int i=0;i 0x200 && audiosize > 0) // is there any work to do? { // are we going at normal speed? // or more precisely, are the input and output queues/buffers of similar size? if(queued > 900 || audiosize > queued * 2) { // not normal speed. we have to resample it somehow in this case. if(audiosize <= queued) { // fast forward speed // this is the easy case, just crossfade it and it sounds ok for(int i = 0; i < audiosize; i++) { int j = i + queued - audiosize; ssamp outsamp = crossfade(sampleQueue[i],sampleQueue[j], i,0,audiosize); emit_sample(buf,ref bufcursor,outsamp); } } else { // slow motion speed // here we take a very different approach, // instead of crossfading it, we select a single sample from the queue // and make sure that the index we use to select a sample is constantly moving // and that it starts at the first sample in the queue and ends on the last one. // // hopefully the index doesn't move discontinuously or we'll get slight crackling // (there might still be a minor bug here that causes this occasionally) // // here's a diagram of how the index we sample from moves: // // queued (this axis represents the index we sample from. the top means the end of the queue) // ^ // | --> audiosize (this axis represents the output index we write to, right meaning forward in output time/position) // | A C C end // A A B C C C // A A A B C C C // A A A B C C // A A C // start // // yes, this means we are spending some stretches of time playing the sound backwards, // but the stretches are short enough that this doesn't sound weird. // this lets us avoid most crackling problems due to the endpoints matching up. // first calculate a shorter-than-full window // that has minimal slope at the endpoints // (to further reduce crackling, especially in sine waves) int beststart = 0, extraAtEnd = 0; { int bestend = queued; const int worstdiff = 99999999; int beststartdiff = worstdiff; int bestenddiff = worstdiff; for(int i = 0; i < 128; i+=2) { int diff = abs(sampleQueue[i].l - sampleQueue[i+1].l) + abs(sampleQueue[i].r - sampleQueue[i+1].r); if(diff < beststartdiff) { beststartdiff = diff; beststart = i; } } for(int i = queued-3; i > queued-3-128; i-=2) { int diff = abs(sampleQueue[i].l - sampleQueue[i+1].l) + abs(sampleQueue[i].r - sampleQueue[i+1].r); if(diff < bestenddiff) { bestenddiff = diff; bestend = i+1; } } extraAtEnd = queued - bestend; queued = bestend - beststart; int oksize = queued; while(oksize + queued*2 + beststart + extraAtEnd <= samples_requested) oksize += queued*2; audiosize = oksize; for(int x = 0; x < beststart; x++) { emit_sample(buf,ref bufcursor,sampleQueue[x]); } //sampleQueue.erase(sampleQueue.begin(), sampleQueue.begin() + beststart); sampleQueue.RemoveRange(0, beststart); //zero 08-nov-2010: did i do this right? } int midpointX = audiosize >> 1; int midpointY = queued >> 1; // all we need to do here is calculate the X position of the leftmost "B" in the above diagram. // TODO: we should calculate it with a simple equation like // midpointXOffset = min(something,somethingElse); // but it's a little difficult to work it out exactly // so here's a stupid search for the value for now: int prevA = 999999; int midpointXOffset = queued/2; while(true) { int a = abs(pingpong(midpointX - midpointXOffset, queued) - midpointY) - midpointXOffset; if(((a > 0) != (prevA > 0) || (a < 0) != (prevA < 0)) && prevA != 999999) { if(((a + prevA)&1)!=0) // there's some sort of off-by-one problem with this search since we're moving diagonally... midpointXOffset++; // but this fixes it most of the time... break; // found it } prevA = a; midpointXOffset--; if(midpointXOffset < 0) { midpointXOffset = 0; break; // failed to find it. the two sides probably meet exactly in the center. } } int leftMidpointX = midpointX - midpointXOffset; int rightMidpointX = midpointX + midpointXOffset; int leftMidpointY = pingpong(leftMidpointX, queued); int rightMidpointY = (queued-1) - pingpong((int)audiosize-1 - rightMidpointX + queued*2, queued); // output the left almost-half of the sound (section "A") for(int x = 0; x < leftMidpointX; x++) { int i = pingpong(x, queued); emit_sample(buf,ref bufcursor,sampleQueue[i]); } // output the middle stretch (section "B") int y = leftMidpointY; int dyMidLeft = (leftMidpointY < midpointY) ? 1 : -1; int dyMidRight = (rightMidpointY > midpointY) ? 1 : -1; for(int x = leftMidpointX; x < midpointX; x++, y+=dyMidLeft) emit_sample(buf,ref bufcursor,sampleQueue[y]); for(int x = midpointX; x < rightMidpointX; x++, y+=dyMidRight) emit_sample(buf, ref bufcursor, sampleQueue[y]); // output the end of the queued sound (section "C") for(int x = rightMidpointX; x < audiosize; x++) { int i = (queued-1) - pingpong((int)audiosize-1 - x + queued*2, queued); emit_sample(buf,ref bufcursor,sampleQueue[i]); } for(int x = 0; x < extraAtEnd; x++) { int i = queued + x; emit_sample(buf,ref bufcursor,sampleQueue[i]); } queued += extraAtEnd; audiosize += beststart + extraAtEnd; } //end else //sampleQueue.erase(sampleQueue.begin(), sampleQueue.begin() + queued); sampleQueue.RemoveRange(0, queued); //zero 08-nov-2010: did i do this right? return audiosize; } else { // normal speed // just output the samples straightforwardly. // // at almost-full speeds (like 50/60 FPS) // what will happen is that we rapidly fluctuate between entering this branch // and entering the "slow motion speed" branch above. // but that's ok! because all of these branches sound similar enough that we can get away with it. // so the two cases actually complement each other. if(audiosize >= queued) { emit_samples(buf,ref bufcursor, sampleQueue.ToArray(),0,queued); //sampleQueue.erase(sampleQueue.begin(), sampleQueue.begin() + queued); sampleQueue.RemoveRange(0, queued); //zero 08-nov-2010: did i do this right? return queued; } else { emit_samples(buf,ref bufcursor, sampleQueue.ToArray(),0,audiosize); //sampleQueue.erase(sampleQueue.begin(), sampleQueue.begin()+audiosize); sampleQueue.RemoveRange(0, audiosize); //zero 08-nov-2010: did i do this right? return audiosize; } } //end normal speed } //end if there is any work to do else { return 0; } } //output_samples }; //NitsujaSynchronizer class VecnaSynchronizer : ISynchronizingAudioBuffer { // vecna's attempt at a fully synchronous sound provider. // It's similar in philosophy to my "BufferedAsync" provider, but BufferedAsync is not // fully synchronous. // Like BufferedAsync, it tries to make most frames 100% correct and just suck it up // periodically and have a big bad-sounding mistake frame if it has to. // It is significantly less ambitious and elaborate than the other methods. // We'll see if it works better or not! // It has a min and maximum amount of excess buffer to deal with minor overflows. // When fastforwarding, it will discard samples above the maximum excess buffer. // When underflowing, it will attempt to resample to a certain threshhold. // If it underflows beyond that threshhold, it will give up and output silence. // Since it has done this, it will go ahead and generate some excess silence in order // to restock its excess buffer. struct Sample { public short left, right; public Sample(short l, short r) { left = l; right = r; } } Queue buffer; Sample[] resampleBuffer; const int SamplesInOneFrame = 735; const int MaxExcessSamples = 2048; public VecnaSynchronizer() { buffer = new Queue(2048); resampleBuffer = new Sample[2730]; // 2048 * 1.25 // Give us a little buffer wiggle-room for (int i=0; i<367; i++) buffer.Enqueue(new Sample(0,0)); } public void enqueue_samples(short[] buf, int samples_provided) { int ctr = 0; for (int i = 0; i < samples_provided; i++) { short left = buf[ctr++]; short right = buf[ctr++]; enqueue_sample(left, right); } } public void enqueue_sample(short left, short right) { if (buffer.Count >= MaxExcessSamples - 1) { // if buffer is overfull, dequeue old samples to make room for new samples. buffer.Dequeue(); } buffer.Enqueue(new Sample(left, right)); } public void clear() { buffer.Clear(); } public int output_samples(short[] buf, int samples_requested) { if (samples_requested > buffer.Count) { // underflow! if (buffer.Count > samples_requested * 3 / 4) { // if we're within 75% of target, then I guess we suck it up and resample. // we sample in a goofy way, we could probably do it a bit smarter, if we cared more. int samples_available = buffer.Count; for (int i = 0; buffer.Count > 0; i++) resampleBuffer[i] = buffer.Dequeue(); int index = 0; for (int i = 0; i 0; i++) { Sample sample = buffer.Dequeue(); buf[index++] += sample.left; buf[index++] += sample.right; } } return samples_requested; } } }