static void DemodulateOffsetQPSK() { // Manual adjust values int sampleRate = 8000; float symbolRate = 250; // Symbols per second int carrierHz = 1005; string inputPath = Path.Combine(LocalPath, "oqpsk_output.8k.3ch.pcm32f"); string outputPath = Path.Combine(LocalPath, "oqpsk_output_demodulate.8k.2ch.pcm32f"); int inputChannelCount = 3; // Fixed values float symbolPeriod = 1f / symbolRate; int symbolLengthSamples = sampleRate / (int)symbolRate; Costas costas = new Costas(sampleRate, carrierHz, Costas.LoopType.QPSK); // Downsamplers for bit slicing Downsampler dsI = new Downsampler(sampleRate); Downsampler dsQ = new Downsampler(sampleRate); dsI.SetRatio((symbolRate * 2) / sampleRate); dsQ.SetRatio((symbolRate * 2) / sampleRate); // Integrator to track symbol rate Integrator intRatio = new Integrator(0.3f, (1f / (symbolRate * 2)) * 0.5f, (symbolRate * 2) / sampleRate); // Modified gardner timing error detector Gardner gardner = new Gardner(); // Tweakable parameters costas.ArmFilterHz = symbolRate * 1.5f; using (Stream input = File.OpenRead(inputPath)) { using (Stream output = File.Create(outputPath)) { // Data input buffer byte[] buffer = new byte[sizeof(float) * inputChannelCount]; bool flipFlop = false; float sampleBufferI = 0; int bytesRead = 0; while ((bytesRead = input.Read(buffer, 0, buffer.Length)) == buffer.Length) { // Get input sample as a float float sampleInput = BitConverter.ToSingle(buffer, 0); // Process sample with costas loop costas.Process(sampleInput); // Get samples for I and Q channels float sampleI = costas.Inphase(); float sampleQ = costas.Quadrature(); // Downsample to 2x symbol rate for timing recovery dsQ.Next(); if (dsI.Next()) { // Compute timing error float error = gardner.Process(sampleBufferI); float ratio = intRatio.Process(error * 0.1f); // Adjust ratio dsI.SetRatio(ratio); dsQ.SetRatio(ratio); // Output every other sample if (flipFlop ^= true) { // Output timing bits to file output.Write(BitConverter.GetBytes(sampleBufferI), 0, sizeof(float)); output.Write(BitConverter.GetBytes(dsQ.GetSample()), 0, sizeof(float)); } // Buffer the I channel by half a symbol period because we delayed Q during modulation // This puts I and Q back in sync resulting in our original data stream // NOTE: This assumption only holds if the demodulated phase is correct and the // constellation is not rotated the wrong way around sampleBufferI = dsI.GetSample(); } // Supply data to downsamplers dsI.SupplyInput(sampleI); dsQ.SupplyInput(sampleQ); //// Output full-rate samples to file instead //output.Write(BitConverter.GetBytes(sampleI), 0, sizeof(float)); //output.Write(BitConverter.GetBytes(sampleQ), 0, sizeof(float)); } } } }
static void DemodulateQPSK() { // Manual adjust values int sampleRate = 8000; float symbolRate = 250; // Symbols per second int carrierHz = 1005; string inputPath = Path.Combine(LocalPath, "qpsk_output.8k.3ch.pcm32f"); string outputPath = Path.Combine(LocalPath, "qpsk_output_demodulate.8k.2ch.pcm32f"); // Fixed values float symbolPeriod = 1f / symbolRate; int symbolLengthSamples = sampleRate / (int)symbolRate; Costas costas = new Costas(sampleRate, carrierHz, Costas.LoopType.QPSK); Gardner gardner = new Gardner(); Downsampler dsI = new Downsampler(sampleRate); Downsampler dsQ = new Downsampler(sampleRate); dsI.SetRatio((symbolRate * 2) / sampleRate); dsQ.SetRatio((symbolRate * 2) / sampleRate); Integrator intRatio = new Integrator(0.3f, (1f / (symbolRate * 2)) * 0.5f, (symbolRate * 2) / sampleRate); // Tweakable parameters costas.ArmFilterHz = symbolRate * 1.5f; using (Stream input = File.OpenRead(inputPath)) { // Data input buffer byte[] buffer = new byte[sizeof(float) * 3]; bool flipFlop = false; using (Stream output = File.Create(outputPath)) { int bytesRead = 0; while ((bytesRead = input.Read(buffer, 0, buffer.Length)) == buffer.Length) { // Get input sample as a float float sampleInput = BitConverter.ToSingle(buffer, 0); // Process sample with costas loop costas.Process(sampleInput); float sampleI = costas.Inphase(); float sampleQ = costas.Quadrature(); // Downsample to 2x symbol rate for timing recovery dsQ.Next(); if (dsI.Next()) { // Compute timing error float error = gardner.Process(dsI.GetSample(), dsQ.GetSample()); float ratio = intRatio.Process(error * 0.01f);// ((symbolRate * 16) / sampleRate) + (0.7f * error); // Adjust ratio dsI.SetRatio(ratio); dsQ.SetRatio(ratio); // Output every other sample if (flipFlop ^= true) { // Output timing bits to file output.Write(BitConverter.GetBytes(dsI.GetSample()), 0, sizeof(float)); output.Write(BitConverter.GetBytes(dsQ.GetSample()), 0, sizeof(float)); } } dsI.SupplyInput(sampleI); dsQ.SupplyInput(sampleQ); //// Output sample to file //output.Write(BitConverter.GetBytes(sampleI), 0, sizeof(float)); //output.Write(BitConverter.GetBytes(sampleQ), 0, sizeof(float)); } } } }