using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace GetCardData { public class Startup { static void Main(string[] args) { const string FILE_NAME = "C:\\Users\\Brandon\\Desktop\\audio.wav"; const double QUIET_THRESHOLD = 32767.0 * 0.02; //this is 2% of signed 16-bit PCM file const double QUIET_WAIT_TIME_SAMPLES = 44100.0 * 0.10; //this is ~0.10 seconds for 44.1KHz signed 16-bit PCM file //This code's a little bit terrible, but it's structured to support both WAV files and "raw" files my phone is spitting out during swipes. //It's also "streaming" the file in memory and watching for non-silence, as a phone app would need to do (eventually). if (WavFile.IsValidWavFile(FILE_NAME)) { var wav = new WavFile(FILE_NAME); var info = wav.Info; wav.Seek(wav.DataStartPosition); int sampleNumber = 0; while (!wav.IsAtEndOfFile) { var data = wav.GetSamples(1); sampleNumber++; if (Math.Abs(data[0]) > QUIET_THRESHOLD) { //We found non-quiet data, keep grabbing samples until we've had sufficient quiet time var samples = new List(); var temp = new Sample(sampleNumber, data[0]); int silentSamples = 0; do { samples.Add(temp); if (Math.Abs(temp.Value) <= QUIET_THRESHOLD) silentSamples++; else silentSamples = 0; temp = new Sample(sampleNumber, wav.GetSamples(1)[0]); sampleNumber++; } while (!wav.IsAtEndOfFile && silentSamples <= QUIET_WAIT_TIME_SAMPLES); //We now have the data, parse it string ret; try { ret = _ParseCardData(samples.ToArray()); Console.WriteLine("Card data: " + ret); } catch { //Oh well... } } } wav.Close(); } else { //Just assume a file of 16-bit shorts (raw data from my phone app) const bool BIG_ENDIAN = false; //depending on how I saved it var raw = System.IO.File.ReadAllBytes(FILE_NAME); int sampleNumber = 0; int i = 0; while (i < raw.Length) { Sample data; if (BIG_ENDIAN) data = new Sample(sampleNumber, BitConverter.ToBEShort16(raw, i)); else data = new Sample(sampleNumber, BitConverter.ToLEShort16(raw, i)); sampleNumber++; i += 2; if (Math.Abs(data.Value) > QUIET_THRESHOLD) { //We found non-quiet data, keep grabbing samples until we've had sufficient quiet time var samples = new List(); var temp = data; int silentSamples = 0; do { samples.Add(temp); if (Math.Abs(temp.Value) <= QUIET_THRESHOLD) silentSamples++; else silentSamples = 0; if (BIG_ENDIAN) temp = new Sample(sampleNumber, BitConverter.ToBEShort16(raw, i)); else temp = new Sample(sampleNumber, BitConverter.ToLEShort16(raw, i)); sampleNumber++; i += 2; } while (i < raw.Length && silentSamples <= QUIET_WAIT_TIME_SAMPLES); //We now have the data, parse it string ret; try { ret = _ParseCardData(samples.ToArray()); Console.WriteLine("Card data: " + ret); } catch { //Oh well... } } } } } private static string _ParseCardData(Sample[] normalizedValues) //not actually normalized, I don't think this step is necessary (hence no code here for it!) { const double QUIET_THRESHOLD_FACTOR = 0.4; //Find the maximum peak sample Sample peakValue = new Sample(0, 0); for (int i = 0; i < normalizedValues.Length; i++) if (Math.Abs((int)normalizedValues[i].Value) > peakValue.Value) peakValue = normalizedValues[i]; //Find the first value past our quiet threshold (a percentage of the highest peak) Sample firstQuietValue = new Sample(0, 0); long startIndex = 0; for (int i = 0; i < normalizedValues.Length; i++) { if (Math.Abs(Math.Max((short)-32767, (short)normalizedValues[i].Value)) > (peakValue.Value * QUIET_THRESHOLD_FACTOR)) { firstQuietValue = normalizedValues[i]; startIndex = i; break; } } //Now we have the exact position where we're going to start paying attention //Now save the distances between sample numbers for each peak const double FACTOR1 = 0.8; //this is the variance between previous and next peak thresholds, which can change slightly throughout the swipe const double FACTOR2 = 0.3; //this is the starting peak threshold...it's pretty low to start out with Sample oldSample = new Sample(0, 0); var bits = new List(); double peakThreshold = peakValue.Value * FACTOR2; int sign = normalizedValues[startIndex].Value >= 0 ? -1 : 1; while (startIndex < normalizedValues.Length) { Sample myPeak = new Sample(0, 0); while (normalizedValues[startIndex].Value * sign > peakThreshold) { if (normalizedValues[startIndex].Value * sign > myPeak.Value) { myPeak = normalizedValues[startIndex]; } startIndex++; if (startIndex >= normalizedValues.Length) break; } if (myPeak.Value != 0) { if (oldSample.Value != 0) bits.Add(new Bit(oldSample, myPeak)); oldSample = new Sample(myPeak.SampleNumber, myPeak.Value); sign *= -1; peakThreshold = Math.Abs(myPeak.Value) * FACTOR1; } startIndex++; } //Now that we have the peaks, go through the first few to establish the baseline frequency for a zero var p = bits.ToArray(); long baselineZeroFrequency = 0; int j; for (j = 1; j < 4; j++) baselineZeroFrequency += p[j].Distance; baselineZeroFrequency = baselineZeroFrequency / 3; int index = j; //Use the baseline to determine whether the bits are 0s or 1s var realbits = new List>(); while (index < p.Length) { long proximityToZero = Math.Abs(Math.Abs(p[index].Distance) - baselineZeroFrequency); long proximityToOne = Math.Abs(Math.Abs(p[index].Distance) - (baselineZeroFrequency / 2)); if (proximityToOne < proximityToZero) { //This is a one realbits.Add(new Tuple(p[index], 1)); //Recalculate the baseline zero frequency baselineZeroFrequency = p[index].Distance * 2; index++; } else { //This is a zero realbits.Add(new Tuple(p[index], 0)); //Recalculate the baseline zero frequency baselineZeroFrequency = p[index].Distance; } //Go to the next value index++; } //Find the start sentinel (always a semicolon for track 2) bool found = false; int start = 0; while (start < bits.Count - 3) { if (realbits[start + 0].Item2 == 1 && realbits[start + 1].Item2 == 1 && realbits[start + 2].Item2 == 0 && realbits[start + 3].Item2 == 1) { found = true; break; } start++; } if (!found) throw new InvalidOperationException("No start sentinel found!"); //Construct the final string string final = String.Empty; var chars = new Dictionary(); while (start < realbits.Count && ((start + 5) < realbits.Count)) { char n = (char)(0x30 + (realbits[start + 0].Item2 + (realbits[start + 1].Item2 * 2) + (realbits[start + 2].Item2 * 4) + (realbits[start + 3].Item2 * 8))); final += n; chars.Add(realbits[start + 0].Item1, n); if (n != '?') { if (((realbits[start + 0].Item2 + realbits[start + 1].Item2 + realbits[start + 2].Item2 + realbits[start + 3].Item2 + realbits[start + 4].Item2) % 2) != 1) throw new InvalidOperationException("Parity bit check failed!"); start += 5; } else break; } return final; } } }