From de7247af5bbb44379d4e358629acadd9c8e7aa36 Mon Sep 17 00:00:00 2001 From: Denis Denisov Date: Tue, 10 Nov 2020 20:36:10 +0300 Subject: [PATCH] Initial commit --- .gitignore | 1 + Measurer.cs | 122 ++++++++++++++++++++++++++++++++++++++++++++ Program.cs | 124 +++++++++++++++++++++++++++++++++++++++++++++ Recorder.cs | 37 ++++++++++++++ SoundThermo.csproj | 13 +++++ SoundThermo.sln | 25 +++++++++ 6 files changed, 322 insertions(+) create mode 100644 Measurer.cs create mode 100644 Program.cs create mode 100644 Recorder.cs create mode 100644 SoundThermo.csproj create mode 100644 SoundThermo.sln diff --git a/.gitignore b/.gitignore index 6db9ba9..8a19a86 100644 --- a/.gitignore +++ b/.gitignore @@ -334,3 +334,4 @@ ASALocalRun/ # Local History for Visual Studio .localhistory/ +Properties/launchSettings.json diff --git a/Measurer.cs b/Measurer.cs new file mode 100644 index 0000000..1c8537d --- /dev/null +++ b/Measurer.cs @@ -0,0 +1,122 @@ +using NAudio.Dsp; +using NAudio.Wave; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SoundThermo +{ + /// + /// Generates sine sample in the form + /// amp * sin(x / freq) + /// + class ThermoSampleProvider : ISampleProvider + { + public const int SAMPLE_RATE = 44100; + WaveFormat format; + float freq; + float amp; + int alreadyGeneratedSamples; + + public ThermoSampleProvider(float freq, float amp) + { + format = WaveFormat.CreateIeeeFloatWaveFormat(SAMPLE_RATE, 1); + this.freq = freq; + this.amp = amp; + alreadyGeneratedSamples = 0; + } + + public WaveFormat WaveFormat + { + get + { + return format; + } + } + + public int Read(float[] buffer, int offset, int count) + { + for (int i = 0; i < count; i++) + { + int sample = alreadyGeneratedSamples++; + buffer[offset + i] = amp * MathF.Sin(2 * MathF.PI * sample * freq / SAMPLE_RATE); + } + return count; + } + } + + /// + /// Returns ratio of output signal to input signal + /// + class Measurer + { + public enum MeasureChannel + { + Left, + Right + } + + const int THERMO_FREQ = 6000; + const float THERMO_AMP = 0.1f; + + WaveInEvent inputDevice; + WaveOutEvent outputDevice; + MeasureChannel channel; + + public Measurer(WaveInEvent inputDevice, WaveOutEvent outputDevice, MeasureChannel ch) + { + this.inputDevice = inputDevice; + this.outputDevice = outputDevice; + this.channel = ch; + } + + public async Task Measure(float duration) + { + var recorder = new Recorder(inputDevice); + var provider = new ThermoSampleProvider(THERMO_FREQ, THERMO_AMP); + outputDevice.Init(provider.ToStereo(channel == MeasureChannel.Left ? 1 : 0, channel == MeasureChannel.Right ? 1 : 0)); + outputDevice.Play(); + await recorder.Record(duration); + outputDevice.Stop(); + var newSamples = new List(); + int leftPos = 0; + while (leftPos < recorder.samples.Count && MathF.Abs(recorder.samples[leftPos]) < 1e-4) leftPos++; + int rightPos = recorder.samples.Count - 1; + while (rightPos > 0 && MathF.Abs(recorder.samples[rightPos]) < 1e-4) rightPos--; + for (int i = leftPos; i <= rightPos; i++) newSamples.Add(recorder.samples[i]); + int pow = 1; + int m = 0; + while (2 * pow <= newSamples.Count) + { + pow *= 2; + m++; + } + var data = new Complex[pow]; + for (int i = 0; i < data.Length; i++) + { + data[i].X = newSamples[i]; + data[i].Y = 0; + } + FastFourierTransform.FFT(true, m, data); + float max = -1; + int argmax = 0; + for (int i = 0; i < data.Length; i++) + { + var amp = MathF.Sqrt(data[i].X * data[i].X + data[i].Y * data[i].Y); + if (amp > max) + { + max = amp; + argmax = i; + } + } + float desired = THERMO_FREQ * pow / ThermoSampleProvider.SAMPLE_RATE; + if (MathF.Abs(desired - argmax) < 1e-3) + { + // max - amplitude on desired frequency + return max / THERMO_AMP; + } + return float.NaN; + + } + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..5ab5ac9 --- /dev/null +++ b/Program.cs @@ -0,0 +1,124 @@ +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using NAudio.Wave; + +namespace SoundThermo +{ + class Program + { + static void Main(string[] args) + { + CultureInfo.CurrentCulture = new CultureInfo("en-US"); + var rootCommand = new RootCommand(); + var listCommand = new Command("list", "List all available devices"); + listCommand.Handler = CommandHandler.Create(ListDevices); + var recordCommand = new Command("record") + { + new Option("--input", "Input device to record from (GUID)"), + new Option("--duration", "How many seconds of audio you want to record"), + }; + recordCommand.Handler = CommandHandler.Create(RecordSamples); + var measureCommand = new Command("measure") + { + new Option("--input", "Input device to record from (GUID)"), + new Option("--output", "Output device to play sound to (GUID)"), + new Option("--duration", "How many seconds of audio you want to record (uses twice this time because of recording on two channels)"), + }; + measureCommand.Handler = CommandHandler.Create(Measure); + rootCommand.AddCommand(listCommand); + rootCommand.AddCommand(measureCommand); + rootCommand.AddCommand(recordCommand); + rootCommand.InvokeAsync(args).Wait(); + } + + private async static Task Measure(string input, string output, float duration) + { + var leftMea = new Measurer(GetInputDevice(input), GetOutputDevice(output), Measurer.MeasureChannel.Left); + var leftResult = await leftMea.Measure(duration); + var rightMea = new Measurer(GetInputDevice(input), GetOutputDevice(output), Measurer.MeasureChannel.Right); + var rightResult = await rightMea.Measure(duration); + float k = 2 * rightResult; + float v0v = leftResult / k; + // assume r2 = 10K + float r1 = 10000 / v0v - 10000; + float t = 1.0f / (MathF.Log(r1 / 10000) / 4300.0f + 1.0f / 298.0f) - 273.0f; + Console.WriteLine(t.ToString()); + } + + private static WaveOutEvent GetOutputDevice(string guid) + { + WaveOutEvent device = null; + for (int n = -1; n < WaveOut.DeviceCount; n++) + { + var caps = WaveOut.GetCapabilities(n); + if (caps.ProductGuid.ToString() == guid) + { + device = new WaveOutEvent(); + device.DeviceNumber = n; + break; + } + } + if (device == null) + { + throw new Exception("Unable to find device " + guid); + } + return device; + } + + private static WaveInEvent GetInputDevice(string guid) + { + WaveInEvent device = null; + for (int n = -1; n < WaveIn.DeviceCount; n++) + { + var caps = WaveIn.GetCapabilities(n); + if (caps.ProductGuid.ToString() == guid) + { + device = new WaveInEvent(); + device.DeviceNumber = n; + break; + } + } + if (device == null) + { + throw new Exception("Unable to find device " + guid); + } + return device; + } + + private async static Task RecordSamples(string input, float duration) + { + WaveInEvent device = GetInputDevice(input); + var rec = new Recorder(device); + await rec.Record(duration); + using (FileStream fs = File.OpenWrite("recorded.txt")) + { + for (int i = 0; i < rec.samples.Count; i++) + { + fs.Write(Encoding.UTF8.GetBytes(rec.samples[i].ToString() + "\n")); + } + } + } + + private static void ListDevices() + { + Console.WriteLine("List of audio devices in your system"); + Console.WriteLine("Output devices:"); + for (int n = -1; n < WaveOut.DeviceCount; n++) + { + var caps = WaveOut.GetCapabilities(n); + Console.WriteLine($"{n}: {caps.ProductName} {caps.ProductGuid}"); + } + Console.WriteLine("Input devices:"); + for (int n = -1; n < WaveIn.DeviceCount; n++) + { + var caps = WaveIn.GetCapabilities(n); + Console.WriteLine($"{n}: {caps.ProductName} {caps.ProductGuid}"); + } + } + } +} diff --git a/Recorder.cs b/Recorder.cs new file mode 100644 index 0000000..e26207f --- /dev/null +++ b/Recorder.cs @@ -0,0 +1,37 @@ +using NAudio.Wave; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SoundThermo +{ + class Recorder + { + const int RECORDING_RATE = 44100; + private WaveInEvent device; + public List samples; + + public Recorder(WaveInEvent device) + { + this.device = device; + } + + public async Task Record(float duration) + { + device.WaveFormat = WaveFormat.CreateIeeeFloatWaveFormat(RECORDING_RATE, 1); + samples = new List(); + device.DataAvailable += OnDataAvailable; + device.StartRecording(); + await Task.Delay((int)(duration * 1000)); + device.StopRecording(); + } + + private void OnDataAvailable(object sender, WaveInEventArgs e) + { + var buffer = new WaveBuffer(e.Buffer); + for (int i = 0; i < e.BytesRecorded / 4; i++) + { + samples.Add(buffer.FloatBuffer[i]); + } + } + } +} diff --git a/SoundThermo.csproj b/SoundThermo.csproj new file mode 100644 index 0000000..16e254b --- /dev/null +++ b/SoundThermo.csproj @@ -0,0 +1,13 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + diff --git a/SoundThermo.sln b/SoundThermo.sln new file mode 100644 index 0000000..df3cad1 --- /dev/null +++ b/SoundThermo.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29209.62 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SoundThermo", "SoundThermo.csproj", "{95349FF5-57FB-44EC-BDEB-E7902C04CDDC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {95349FF5-57FB-44EC-BDEB-E7902C04CDDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {95349FF5-57FB-44EC-BDEB-E7902C04CDDC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {95349FF5-57FB-44EC-BDEB-E7902C04CDDC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {95349FF5-57FB-44EC-BDEB-E7902C04CDDC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A08CF9A2-F3A7-486E-BC48-B0D0BACF659B} + EndGlobalSection +EndGlobal