static FilterProfile bandpass(uint centerFreq, uint sr) { FilterProfile lfg = new FilterProfile(); lfg.Add(new FreqGain(centerFreq * 0.5, -190)); lfg.Add(new FreqGain(centerFreq * 1.00, 0)); lfg.Add(new FreqGain(Math.Min(centerFreq * 2,sr), -190)); return lfg; }
public static FilterProfile DifferentialSPL(double phon0, double phon1, double scale) { FilterProfile spl = new FilterProfile(); FilterProfile spl0 = Loudness.SPL(phon0); FilterProfile spl1 = Loudness.SPL(phon1); for (int j = 0; j < spl1.Count; j++) { FreqGain fg = spl1[j]; fg.Gain = scale * ( spl0[j].Gain - fg.Gain ); spl.Add(fg); } return spl; }
/// <summary> /// Create a list of dB SPL equal-loudness values for a given 'phon' loudness /// (from zero, threshold, to 90) /// </summary> /// <param name="phon"></param> /// <returns>list of {frequency Hz, dB SPL}</returns> public static FilterProfile SPL(double phon) { FilterProfile lfg = new FilterProfile(); if ((phon < 0) | (phon > 120)) { throw new ArgumentException("Phon value out of bounds!"); } // Setup user-defined values for equation double Ln = phon; for (int j = 0; j < f.Length; j++) { // Deriving sound pressure level from loudness level (iso226 sect 4.1) double Af = 4.47E-3 * Math.Pow(10, (0.025 * Ln) - 1.15) + Math.Pow(0.4 * Math.Pow(10, (((Tf[j] + Lu[j]) / 10) - 9)), af[j]); double Lp = ((10 / af[j]) * Math.Log10(Af)) - Lu[j] + 94; // Return user data FreqGain fg = new FreqGain(f[j], Lp); lfg.Add(fg); } return lfg; }
/// <summary> /// Return a list of freq/gain, only including inflection points (where the curve is flat). /// </summary> /// <param name="data"></param> /// <param name="sr"></param> /// <returns></returns> public static FilterProfile inflections(double[] data, uint sr) { // Differentiate double bins = data.Length + 1; double[] diff = new double[data.Length]; double n = data[0]; for (int j = 0; j < data.Length; j++) { double d = data[j]; diff[j] = d - n; n = d; } // Look for zero-crossings of the first derivative of data[] // (always include [0] and [end]) FilterProfile pts = new FilterProfile(); int bin; double pt = 0.1; int last = -1; double freq; double lastfreq = -1; // Always start with points for zero and 10 Hz pts.Add(new FreqGain(0, MathUtil.dB(data[0]))); freq = 10; bin = (int)f2bin(freq,sr,bins); pts.Add(new FreqGain(freq, MathUtil.dB(data[bin]))); pt = diff[bin]; for (int j = bin + 1; j < data.Length; j++) { if ((pt > 0 && diff[j] <= 0) || (pt < 0 && diff[j] >= 0)) { freq = bin2f(j,sr,bins); pts.Add(new FreqGain(freq, MathUtil.dB(data[j]))); last = j; lastfreq = freq; } pt = diff[j]; } // Fill in the last few target samples if (lastfreq < (sr / 2) - 2050) { freq = (sr / 2) - 2050; bin = (int)f2bin(freq,sr,bins); pts.Add(new FreqGain(freq, MathUtil.dB(data[bin]))); } if (lastfreq < (sr / 2) - 1550) { freq = (sr / 2) - 1550; bin = (int)f2bin(freq,sr,bins); pts.Add(new FreqGain(freq, MathUtil.dB(data[bin]))); } if (lastfreq < sr / 2) { freq = sr / 2; pts.Add(new FreqGain(freq, MathUtil.dB(data[data.Length - 1]))); } return pts; }
/// <summary> /// Return a list of freq/gain, at the ERB centers /// Assuming you smoothed the data... /// </summary> /// <param name="data">data from ERB.smooth</param> /// <param name="sr">sample rate</param> /// <param name="scaleFactor">1.0 for ~38 bands. 0.5 for twice as many...</param> /// <returns></returns> public static FilterProfile profile(double[] data, uint sr, double scaleFactor) { FilterProfile pts = new FilterProfile(); int dl = data.Length; for (double j = 1; j < ERB.ERBVal(sr / 2) + 1; j += scaleFactor) { double f = ERB.invERBVal(j); double n = f * 2 * dl / sr; if (n < dl) { double g = data[(int)n]; pts.Add(new FreqGain(f, g)); } } return pts; }
static void Main(string[] args) { // Find where this executable is launched from string[] cargs = Environment.GetCommandLineArgs(); _thisFolder = Path.GetDirectoryName(cargs[0]); if (String.IsNullOrEmpty(_thisFolder)) { _thisFolder = Environment.CurrentDirectory; } string appData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); _impulsesFolder = Path.GetFullPath(Path.Combine(appData, "InguzEQ" + slash + "Impulses" + slash)); string[] inFiles = new string[4]; string inL = ""; string inR = ""; if (!DisplayInfo()) { return; } bool ok = (args.Length > 0); bool longUsage = false; for (int j = 0; ok && j < args.Length; j++) { string arg = args[j]; switch (args[j].ToUpperInvariant()) { case "/?": case "-?": case "/H": case "/HELP": ok = false; longUsage = true; break; case "/L": case "/0": inFiles[0] = args[++j]; _nInFiles = Math.Max(_nInFiles, 1); break; case "/R": case "/1": inFiles[1] = args[++j]; _nInFiles = Math.Max(_nInFiles, 2); break; case "/2": inFiles[2] = args[++j]; _nInFiles = Math.Max(_nInFiles, 3); break; case "/3": inFiles[3] = args[++j]; _nInFiles = Math.Max(_nInFiles, 4); break; case "/LENGTH": _filterLen = int.Parse(args[++j], CultureInfo.InvariantCulture); if (_filterLen < 16) { throw new Exception("Length is too small."); } break; case "/DBL": _dbl = true; break; case "/PCM": _pcm = true; break; case "/NODRC": _noDRC = true; break; case "/NOSKEW": _noSkew = true; break; case "/NONORM": // No normalization of the impulse response (undocumented) _noNorm = true; break; case "/SPLIT": _split = true; break; case "/COPY": _copy = true; break; case "/GAIN": _gain = double.Parse(args[++j], CultureInfo.InvariantCulture); break; case "/ALL": // Returns negative-time components as part of the impulse response // (experimental, to be used for THD measurement) _returnAll = true; break; case "/POWER": // Raises sweep to power n // (experimental, to be used for THD measurement) _power = int.Parse(args[++j], CultureInfo.InvariantCulture); break; case "/FMIN": // (experimental, i.e. broken) _fmin = int.Parse(args[++j], CultureInfo.InvariantCulture); _fminSpecified = true; break; case "/FMAX": // (experimental, i.e. broken) _fmax = int.Parse(args[++j], CultureInfo.InvariantCulture); _fmaxSpecified = true; break; case "/DIRECT": // Create filtered (direct-sound) filters _doDirectFilters = true; break; case "/NOSUB": // Don't apply subsonic filter to the impulse response _noSubsonicFilter = true; break; case "/NOOVER": // Don't override DRC's settings for filter type and length _noOverrideDRC = true; break; case "/KEEPTEMP": // Undocumented _keepTempFiles = true; break; case "/REFCH": // Override the reference-channel detection _refchannel = int.Parse(args[++j], CultureInfo.InvariantCulture); if (_refchannel<0 || _refchannel > _nInFiles - 1) { throw new Exception(String.Format("RefCh can only be from 0 to {0}.", _nInFiles-1)); } break; case "/ENV": // Undocumented. Save the Hilbert envelope _env = true; break; case "-": // ignore break; default: ok = false; break; } } if (!ok) { DisplayUsage(longUsage); } else { try { if (!_noDRC) { if (!File.Exists(GetDRCExe())) { stderr.WriteLine("Denis Sbragion's DRC (http://drc-fir.sourceforge.net/) was not found."); stderr.WriteLine("Only the impulse response will be calculated, not correction filters."); stderr.WriteLine(""); _noDRC = true; } } if (!_noDRC) { FileInfo[] drcfiles = new DirectoryInfo(_thisFolder).GetFiles("*.drc"); if (drcfiles.Length == 0) { stderr.WriteLine("No .drc files were found in the current folder."); stderr.WriteLine("Only the impulse response will be calculated, not correction filters."); stderr.WriteLine(""); _noDRC = true; } } for(int i=0; i<_nInFiles; i++) { string inFile = inFiles[i]; if (String.IsNullOrEmpty(inFile)) { stderr.WriteLine("Error: The {0} input file was not specified.", FileDescription(i)); return; } if (!File.Exists(inFile)) { stderr.WriteLine("Error: The {0} input file {1} was not found.", FileDescription(i), inFile); return; } for (int j = 0; j < i; j++) { if (inFile.Equals(inFiles[j])) { stderr.WriteLine("Warning: The same input file ({0}) was specified for both {1} and {2}!", inFile, FileDescription(j), FileDescription(i)); //stderr.WriteLine(); } } } // Temporary if (_nInFiles != 2) { stderr.WriteLine("Error: Two input files must be specified."); return; } inL = inFiles[0]; inR = inFiles[1]; // end temporary uint sampleRate; List<SoundObj> impulses; List<ISoundObj> filteredImpulses; List<string> impDirects; List<Complex[]> impulseFFTs; List<double> maxs; SoundObj impulseL; SoundObj impulseR; ISoundObj filteredImpulseL = null; ISoundObj filteredImpulseR = null; string impDirectL = null; string impDirectR = null; Complex[] impulseLFFT; Complex[] impulseRFFT; WaveWriter writer; ISoundObj buff; double g; if (!_keepTempFiles) { _tempFiles.Add("rps.pcm"); _tempFiles.Add("rtc.pcm"); } // Find the left impulse stderr.WriteLine("Processing left measurement ({0})...", inL); impulseL = Deconvolve(inL, out impulseLFFT, out _peakPosL); sampleRate = impulseL.SampleRate; _sampleRate = sampleRate; double peakM = Math.Round(MathUtil.Metres(_peakPosL, sampleRate), 2); double peakFt = Math.Round(MathUtil.Feet(_peakPosL, sampleRate), 2); stderr.WriteLine(" Impulse peak at sample {0} ({1}m, {2}ft)", _peakPosL, peakM, peakFt); // Write to PCM string impFileL = Path.GetFileNameWithoutExtension(inL) + "_imp" + ".pcm"; if (!_keepTempFiles) { _tempFiles.Add(impFileL); } writer = new WaveWriter(impFileL); writer.Input = impulseL; writer.Format = WaveFormat.IEEE_FLOAT; writer.BitsPerSample = 32; writer.SampleRate = _sampleRate; writer.Raw = true; writer.Run(); writer.Close(); // Write the impulseFFT to disk int L = impulseLFFT.Length; string impTempL = Path.GetFileNameWithoutExtension(inL) + "_imp" + ".dat"; _tempFiles.Add(impTempL); writer = new WaveWriter(impTempL); writer.Input = new CallbackSource(2, sampleRate, delegate(long j) { if (j >= L / 2) { return null; } Complex si = impulseLFFT[j]; // +impulseLFFT[L - j - 1]; ISample s = new Sample2(); s[0] = si.Magnitude; s[1] = si.Phase / Math.PI; return s; }); writer.Format = WaveFormat.IEEE_FLOAT; writer.BitsPerSample = 32; writer.SampleRate = _sampleRate; writer.Raw = false; writer.Run(); writer.Close(); writer = null; impulseLFFT = null; GC.Collect(); if (_doDirectFilters) { // Sliding low-pass filter over the impulse stderr.WriteLine(" Filtering..."); filteredImpulseL = SlidingLowPass(impulseL, _peakPosL); // Write PCM for the filtered impulse impDirectL = Path.GetFileNameWithoutExtension(inL) + "_impfilt" + ".pcm"; if (!_keepTempFiles) { _tempFiles.Add(impDirectL); } writer = new WaveWriter(impDirectL); writer.Input = filteredImpulseL; writer.Format = WaveFormat.IEEE_FLOAT; writer.SampleRate = _sampleRate; writer.BitsPerSample = 32; writer.Raw = false; writer.Run(); writer.Close(); writer = null; filteredImpulseL.Reset(); } GC.Collect(); stderr.WriteLine(" Deconvolution: left impulse done."); stderr.WriteLine(); // Find the right impulse stderr.WriteLine("Processing right measurement ({0})...", inR); impulseR = Deconvolve(inR, out impulseRFFT, out _peakPosR); peakM = Math.Round(MathUtil.Metres(_peakPosR, sampleRate), 2); peakFt = Math.Round(MathUtil.Feet(_peakPosR, sampleRate), 2); stderr.WriteLine(" Impulse peak at sample {0} ({1}m, {2}ft)", _peakPosR, peakM, peakFt); // Write to PCM string impFileR = Path.GetFileNameWithoutExtension(inR) + "_imp" + ".pcm"; if (!_keepTempFiles) { _tempFiles.Add(impFileR); } writer = new WaveWriter(impFileR); writer.Input = impulseR; writer.Format = WaveFormat.IEEE_FLOAT; writer.BitsPerSample = 32; writer.SampleRate = _sampleRate; writer.Raw = true; writer.Run(); writer.Close(); // Write the impulseFFT magnitude to disk L = impulseRFFT.Length; string impTempR = Path.GetFileNameWithoutExtension(inR) + "_imp" + ".dat"; _tempFiles.Add(impTempR); writer = new WaveWriter(impTempR); writer.Input = new CallbackSource(2, impulseR.SampleRate, delegate(long j) { if (j >= L / 2) { return null; } Complex si = impulseRFFT[j]; // +impulseRFFT[L - j - 1]; ISample s = new Sample2(); s[0] = si.Magnitude; s[1] = si.Phase / Math.PI; return s; }); writer.Format = WaveFormat.IEEE_FLOAT; writer.BitsPerSample = 32; writer.SampleRate = _sampleRate; writer.Raw = false; writer.Run(); writer.Close(); writer = null; impulseRFFT = null; GC.Collect(); if (_doDirectFilters) { // Sliding low-pass filter over the impulse stderr.WriteLine(" Filtering..."); filteredImpulseR = SlidingLowPass(impulseR, _peakPosR); // Write PCM for the filtered impulse impDirectR = Path.GetFileNameWithoutExtension(inR) + "_impfilt" + ".pcm"; if (!_keepTempFiles) { _tempFiles.Add(impDirectR); } writer = new WaveWriter(impDirectR); writer.Input = filteredImpulseR; writer.Format = WaveFormat.IEEE_FLOAT; writer.BitsPerSample = 32; writer.SampleRate = _sampleRate; writer.Raw = false; writer.Run(); writer.Close(); writer = null; filteredImpulseR.Reset(); } GC.Collect(); stderr.WriteLine(" Deconvolution: right impulse done."); stderr.WriteLine(); // Join the left and right impulse files (truncated at 65536) into a WAV // and normalize loudness for each channel stderr.WriteLine("Splicing and normalizing (1)"); ChannelSplicer longstereoImpulse = new ChannelSplicer(); // (Don't normalize each channel's volume separately if _returnAll, it's just too expensive) if (_returnAll) { buff = impulseL; } else { buff = new SoundBuffer(new SampleBuffer(impulseL).Subset(0, 131071)); g = Loudness.WeightedVolume(buff); (buff as SoundBuffer).ApplyGain(1 / g); } longstereoImpulse.Add(buff); if (_returnAll) { buff = impulseR; } else { buff = new SoundBuffer(new SampleBuffer(impulseR).Subset(0, 131071)); g = Loudness.WeightedVolume(buff); (buff as SoundBuffer).ApplyGain(1 / g); } longstereoImpulse.Add(buff); ISoundObj stereoImpulse = longstereoImpulse; _impulseFiles.Add("Impulse_Response_Measured.wav: stereo impulse response from measurements"); writer = new WaveWriter("Impulse_Response_Measured.wav"); writer.Input = longstereoImpulse; writer.Format = WaveFormat.IEEE_FLOAT; writer.BitsPerSample = 32; writer.SampleRate = _sampleRate; writer.Normalization = -1; writer.Raw = false; writer.Run(); writer.Close(); writer = null; if (_env) { // Also save the Hilbert envelope HilbertEnvelope env = new HilbertEnvelope(8191); env.Input = longstereoImpulse; _impulseFiles.Add("Impulse_Response_Envelope.wav: Hilbert envelope of the impulse response"); writer = new WaveWriter("Impulse_Response_Envelope.wav"); writer.Input = env; writer.Format = WaveFormat.IEEE_FLOAT; writer.BitsPerSample = 32; writer.SampleRate = _sampleRate; writer.Normalization = -1; writer.Raw = false; writer.Run(); writer.Close(); writer = null; } if (_dbl) { // Create DBL files for Acourate _impulseFiles.Add("PulseL.dbl: impulse response, raw data (64-bit float), left channel "); _impulseFiles.Add("PulseR.dbl: impulse response, raw data (64-bit float), right channel"); _impulseFiles.Add(" (use skew=" + (_peakPosL - _peakPosR) + " for time alignment)"); WriteImpulseDBL(stereoImpulse, "PulseL.dbl", "PulseR.dbl"); } if (_pcm) { // Create PCM files for Octave (etc) _impulseFiles.Add("LUncorrected.pcm: impulse response, raw data (32-bit float), left channel"); _impulseFiles.Add("RUncorrected.pcm: impulse response, raw data (32-bit float), right channel"); WriteImpulsePCM(stereoImpulse, "LUncorrected.pcm", "RUncorrected.pcm"); } stereoImpulse = null; longstereoImpulse = null; buff = null; GC.Collect(); if (_doDirectFilters) { // Same for the filtered impulse response stderr.WriteLine("Splicing and normalizing (2)"); ChannelSplicer longstereoImpulseF = new ChannelSplicer(); buff = new SoundBuffer(new SampleBuffer(filteredImpulseL).Subset(0, 131071)); double gL = Loudness.WeightedVolume(buff); (buff as SoundBuffer).ApplyGain(1 / gL); longstereoImpulseF.Add(buff); FilterProfile lfgDirectL = new FilterProfile(buff, 0.5); buff = new SoundBuffer(new SampleBuffer(filteredImpulseR).Subset(0, 131071)); double gR = Loudness.WeightedVolume(buff); (buff as SoundBuffer).ApplyGain(1 / gR); longstereoImpulseF.Add(buff); FilterProfile lfgDirectR = new FilterProfile(buff, 0.5); _impulseFiles.Add("Impulse_Response_Filtered.wav: approximation to direct-sound impulse response"); writer = new WaveWriter("Impulse_Response_Filtered.wav"); writer.Input = longstereoImpulseF; writer.Format = WaveFormat.IEEE_FLOAT; writer.BitsPerSample = 32; writer.SampleRate = _sampleRate; writer.Normalization = -1; writer.Raw = false; writer.Run(); writer.Close(); double gg = writer.Gain; writer = null; longstereoImpulseF = null; ChannelSplicer longstereoImpulseD = new ChannelSplicer(); Mixer diffuse = new Mixer(); diffuse.Add(impulseL, 1.0); diffuse.Add(filteredImpulseL, -1.0); buff = new SoundBuffer(new SampleBuffer(diffuse).Subset(0, 131071)); (buff as SoundBuffer).ApplyGain(1 / gL); longstereoImpulseD.Add(buff); FilterProfile lfgDiffuseL = new FilterProfile(buff, 0.5); diffuse = new Mixer(); diffuse.Add(impulseR, 1.0); diffuse.Add(filteredImpulseR, -1.0); buff = new SoundBuffer(new SampleBuffer(diffuse).Subset(0, 131071)); (buff as SoundBuffer).ApplyGain(1 / gR); longstereoImpulseD.Add(buff); FilterProfile lfgDiffuseR = new FilterProfile(buff, 0.5); _impulseFiles.Add("Impulse_Response_Diffuse.wav: approximation to diffuse-field remnant"); writer = new WaveWriter("Impulse_Response_Diffuse.wav"); writer.Input = longstereoImpulseD; writer.Format = WaveFormat.IEEE_FLOAT; writer.BitsPerSample = 32; writer.SampleRate = _sampleRate; writer.Gain = gg; writer.Raw = false; writer.Run(); writer.Close(); writer = null; // Filter the diffuse-field curve against double the diffuse-field curve FilterImpulse fiDiffuse = new FilterImpulse(8192, HRTF.diffuseDiff0() * 2, FilterInterpolation.COSINE, sampleRate); FastConvolver co = new FastConvolver(longstereoImpulseD, fiDiffuse); SoundBuffer buffd = new SoundBuffer(co); _impulseFiles.Add("Impulse_Response_Diffuse_Comp.wav: filtered diffuse-field remnant"); writer = new WaveWriter("Impulse_Response_Diffuse_Comp.wav"); writer.Input = buffd.Subset(4096); writer.Format = WaveFormat.IEEE_FLOAT; writer.BitsPerSample = 32; writer.SampleRate = _sampleRate; writer.Gain = gg; writer.Raw = false; writer.Run(); writer.Close(); writer = null; longstereoImpulseD = null; bool any = false; string jsonFile = "Diff.json"; FileStream fs = new FileStream(jsonFile, FileMode.Create); StreamWriter sw = new StreamWriter(fs); sw.WriteLine("{"); FilterProfile lfgDiffL = lfgDirectL - lfgDiffuseL; if (lfgDiffL != null) { if (any) sw.WriteLine(","); any = true; sw.Write(lfgDiffL.ToJSONString("DiffL", "Diffuse field relative to direct, left channel")); } FilterProfile lfgDiffR = lfgDirectR - lfgDiffuseR; if (lfgDiffR != null) { if (any) sw.WriteLine(","); any = true; sw.Write(lfgDiffR.ToJSONString("DiffR", "Diffuse field relative to direct, right channel")); } sw.WriteLine("}"); sw.Close(); fs.Close(); } buff = null; GC.Collect(); System.Console.Error.WriteLine(); if (!_noDRC) { // Analyze the freq response // and create targets // target_full.txt and target_half.txt stderr.WriteLine("Analyzing response curves."); Prep(impTempL, impTempR, "Impulse_Response_Measured.wav", "NoCorrection"); // Call DRC to create the filters // then splice the DRC left & right output files together stderr.WriteLine("Preparing for DRC."); if (DoDRC(impFileL, impFileR, impDirectL, impDirectR, _peakPosL, _peakPosR, "Impulse_Response_Measured.wav", "Impulse_Response_Filtered.wav")) { stderr.WriteLine("Success!"); } } // Report names of the impulse files created if (_impulseFiles.Count == 0) { System.Console.Error.WriteLine("No impulse response files were created."); } if (_impulseFiles.Count > 0) { System.Console.Error.WriteLine("Impulse response files were created:"); foreach (string f in _impulseFiles) { string s = " " + f; System.Console.Error.WriteLine(s); } } // Report names of the filter files created if (_filterFiles.Count == 0 && !_noDRC) { System.Console.Error.WriteLine("No correction filter files were created."); } if (_filterFiles.Count > 0) { System.Console.Error.WriteLine("Correction filter files were created:"); foreach (string f in _filterFiles) { string s = " " + f; if (_copy) { try { File.Copy(f, Path.Combine(_impulsesFolder, f), true); s += " (copied)"; } catch (Exception e) { s += " (not copied: " + e.Message + ")"; } } System.Console.Error.WriteLine(s); } } if (_peakPosL == _peakPosR) { System.Console.Error.WriteLine(); System.Console.Error.WriteLine("Zero time difference between channels. Are you sure the recordings are correct?"); } } catch (Exception e) { stderr.WriteLine(); stderr.WriteLine(e.Message); stderr.WriteLine(e.StackTrace); } finally { foreach (string tempFile in _tempFiles) { try { File.Delete(tempFile); } catch (Exception) { /* ignore */ } } } } stderr.Flush(); }
static ISoundObj LowPassFiltered(ISoundObj input, double freqStart, double gainEnd) { uint sampleRate = input.SampleRate; FilterProfile lfg = new FilterProfile(); lfg.Add(new FreqGain(freqStart, 0)); lfg.Add(new FreqGain(0.499*sampleRate, gainEnd)); FastConvolver conv = new FastConvolver(); conv.Input = input; conv.impulse = new FilterImpulse(8192, lfg, FilterInterpolation.COSINE, sampleRate); return conv; }
static void Prep(string infileL, string infileR, string stereoImpulseFile, string outFile) { // Input files are complex // 0=mag, 1=phase/pi (so it looks OK in a wave editor!) // FFTs of the room impulse response // Take two half-FFT-of-impulse WAV files // Average them, into an array int n; SoundBuffer buff; WaveWriter wri; // NoiseGenerator noise; FastConvolver conv; /* // Convolve noise with the in-room impulse noise = new NoiseGenerator(NoiseType.WHITE_FLAT, 2, (int)131072, stereoImpulse.SampleRate, 1.0); conv = new FastConvolver(); conv.impulse = stereoImpulse; conv.Input = noise; wri = new WaveWriter("ImpulseResponse_InRoom.wav"); wri.Input = conv; wri.Format = WaveFormat.IEEE_FLOAT; wri.BitsPerSample = 32; wri.Normalization = 0; wri.Run(); wri.Close(); wri = null; conv = null; */ WaveReader rdrL = new WaveReader(infileL); buff = new SoundBuffer(rdrL); n = (int)buff.ReadAll(); uint sampleRate = buff.SampleRate; uint nyquist = sampleRate / 2; double binw = (nyquist / (double)n); WaveReader rdrR = new WaveReader(infileR); IEnumerator<ISample> enumL = buff.Samples; IEnumerator<ISample> enumR = rdrR.Samples; // For easier processing and visualisation // read this in to an ERB-scale (not quite log-scale) array // then we can smooth by convolving with a single half-cosine. // int nn = (int)ERB.f2bin(nyquist, sampleRate) + 1; double[] muff = new double[nn]; int prevbin = 0; int nbin = 0; double v = 0; int j = 0; while (true) { double f = (double)j * binw; // equiv freq, Hz int bin = (int)ERB.f2bin(f, sampleRate); // the bin we drop this sample in if (bin > nn) { // One of the channels has more, but we're overrun so stop now break; } j++; bool more = false; more |= enumL.MoveNext(); more |= enumR.MoveNext(); if (!more) { muff[prevbin] = v / nbin; break; } v += enumL.Current[0]; // magnitude v += enumR.Current[0]; // magnitude nbin++; if (bin > prevbin) { muff[prevbin] = v / nbin; v = 0; nbin = 0; prevbin = bin; } } double[] smoo = ERB.smooth(muff, 38); // Pull out the freq response at ERB centers FilterProfile lfg = ERB.profile(smoo, sampleRate); // Write this response as a 'target' file /* FileStream fs = new FileStream("target_full.txt", FileMode.Create); StreamWriter sw = new StreamWriter(fs, Encoding.ASCII); foreach (FreqGain fg in lfg) { sw.WriteLine("{0} {1:f4}", Math.Round(fg.Freq), fg.Gain); } sw.Close(); */ /* fs = new FileStream("target_half.txt", FileMode.Create); sw = new StreamWriter(fs, Encoding.ASCII); foreach (FreqGain fg in lfg) { sw.WriteLine("{0} {1:f4}", Math.Round(fg.Freq), fg.Gain/2); } sw.Close(); */ // Create a filter to invert this response FilterProfile ifg = new FilterProfile(); foreach (FreqGain fg in lfg) { ifg.Add(new FreqGain(fg.Freq, -fg.Gain)); } ISoundObj filterImpulse = new FilterImpulse(0, ifg, FilterInterpolation.COSINE, sampleRate); filterImpulse.SampleRate = sampleRate; // Write the filter impulse to disk string sNoCorr = outFile + ".wav"; wri = new WaveWriter(sNoCorr); wri.Input = filterImpulse; // invertor; wri.Format = WaveFormat.IEEE_FLOAT; wri.BitsPerSample = 32; wri.SampleRate = _sampleRate; wri.Normalization = -1; wri.Run(); wri.Close(); _filterFiles.Add(sNoCorr); /* // Convolve noise with the NoCorrection filter noise = new NoiseGenerator(NoiseType.WHITE_FLAT, 2, (int)131072, stereoImpulse.SampleRate, 1.0); conv = new FastConvolver(); conv.impulse = invertor; conv.Input = noise; wri = new WaveWriter("NoCorrection_Test.wav"); wri.Input = conv; wri.Format = WaveFormat.IEEE_FLOAT; wri.BitsPerSample = 32; wri.SampleRate = _sampleRate; wri.Normalization = 0; wri.Run(); wri.Close(); wri = null; conv = null; */ // Convolve this with the in-room impulse response WaveReader rea = new WaveReader(outFile + ".wav"); conv = new FastConvolver(); conv.impulse = rea; conv.Input = new WaveReader(stereoImpulseFile); wri = new WaveWriter(outFile + "_TestConvolution.wav"); wri.Input = conv; wri.Format = WaveFormat.PCM; wri.Dither = DitherType.TRIANGULAR; wri.BitsPerSample = 16; wri.SampleRate = _sampleRate; wri.Normalization = -1; wri.Run(); wri.Close(); rea.Close(); wri = null; conv = null; }
private static IEnumerator<double> Arbitrary(int length, FilterProfile coeffs, uint sampleRate) { // Generate random-phase with specified magnitudes in the frequency domain, then IFFT. // This noise is periodic - length (next power of 2 from 'n') int l = MathUtil.NextPowerOfTwo(length); Complex[] data = new Complex[l]; int n = 0; double freq1 = coeffs[n].Freq * 2 * l / sampleRate; double freq2 = coeffs[n + 1].Freq * 2 * l / sampleRate; double gain1 = coeffs[n].Gain; double gain2 = coeffs[n + 1].Gain; double logn = Math.Log(l); for (int j = 0; j < l; j++) { double gainDb; double gain; if (j > freq2) { // Move to the next coefficient n++; if (n < coeffs.Count-1) { freq1 = coeffs[n].Freq * 2 * l / sampleRate; freq2 = coeffs[n + 1].Freq * 2 * l / sampleRate; gain1 = coeffs[n].Gain; gain2 = coeffs[n + 1].Gain; } } if (j < freq1) { gainDb = gain1; } else if (j > freq2) { gainDb = gain2; } else { // Raised Cosine: 0.5* ( cos(phi) + 1 ), from phi=pi to 2pi // double frac = (double)(j - freq1) / (double)(freq2 - freq1); double ph = Math.PI * (1 + frac); double rcos = (1 + Math.Cos(ph)) / 2; gainDb = gain1 + rcos * (gain2 - gain1); } gain = MathUtil.gain(gainDb); // Create a random phase value from 0 to 2pi double phi = NextRandom() * 2 * Math.PI; // Magnitude is 1, so just trig double re = Math.Cos(phi); double im = Math.Sin(phi); data[j] = new Complex(gain * logn * re, gain * logn * im); } // IFFT Fourier.IFFT((int)l, data); // Return the real component int k = 0; while (true) { yield return data[k].Re * logn; k++; if (k >= l) { k = 0; } } }
private static ISoundObj GenerateFlatnessFilter(string impulsePath, ISoundObj impulseObj, double nFlatness) { // Flatness-filters are single channel // nFlatness is from 0 to 100 // 0: follow the contours of the impulse // 100: completely flat DateTime dtStart = DateTime.Now; ISoundObj filterImpulse = null; uint nSR = impulseObj.SampleRate; uint nSR2 = nSR / 2; string sPath = FlatnessFilterPath(impulsePath, nSR, nFlatness); // Low flatness values (to 0) => very un-smooth // High flatness values (to 100) => very smooth double detail = (nFlatness / 50) + 0.05; // Get profile of the impulse FilterProfile lfg = new FilterProfile(impulseObj, detail); // Scale by the flatness values lfg = lfg * ((100 - nFlatness) / 100); // Invert lfg = lfg.Inverse(20); // Zero at HF lfg.Add(new FreqGain(nSR2 - 100, 0)); // Build the flatness filter filterImpulse = new FilterImpulse(8192, lfg, FilterInterpolation.COSINE, nSR); try { // Write the filter impulse to disk WaveWriter wri = new WaveWriter(sPath); wri.Input = filterImpulse; wri.Format = WaveFormat.IEEE_FLOAT; wri.BitsPerSample = 64; wri.Run(); wri.Close(); if (_debug) { // DEBUG: Write the flatness impulse as wav16 wri = new WaveWriter(sPath + ".wav"); wri.Input = filterImpulse; wri.Format = WaveFormat.PCM; wri.BitsPerSample = 16; wri.Normalization = -1.0; wri.Dither = DitherType.NONE;//.TRIANGULAR; wri.Run(); wri.Close(); } } catch (Exception e) { if (_debug) { Trace.WriteLine("GenerateFlatnessFilter: " + e.Message); } } impulseObj.Reset(); if (_debug) { TimeSpan ts = DateTime.Now.Subtract(dtStart); Trace.WriteLine("GenerateFlatnessFilter " + ts.TotalMilliseconds); } return filterImpulse; }
/// <summary> /// Write the JSON description of the given filter impulse /// </summary> /// <param name="filterImpulse">ISoundObj</param> /// <param name="filterName">base filename, no extension</param> private static void WriteJSON(FilterProfile eqValues, ISoundObj filterImpulse, string filterName) { FilterProfile lfg; if (filterImpulse == null && filterName == null) { // If both parameters are null, // write a null "current.json" filterName = _userID.Replace(':', '_') + ".current"; lfg = new FilterProfile(); lfg.Add(new FreqGain(20, 0)); lfg.Add(new FreqGain(22050, 0)); if (_debug) { Trace.WriteLine("WriteJSON {0} (null)", filterName); } } else { if (_debug) { Trace.WriteLine("WriteJSON {0}", filterName); } // FFT the impulse // Smooth its magnitude response by ERB bands // Write the result to filterName.json lfg = new FilterProfile(filterImpulse, 0.5); } // Write to JSON string jsonFile = Path.Combine(_tempFolder, filterName + ".json"); FileStream fs = new FileStream(jsonFile, FileMode.Create); StreamWriter sw = new StreamWriter(fs); sw.WriteLine("{"); sw.Write(eqValues.ToJSONString("EQ", "User-set equalizer values") + ","); sw.Write(lfg.ToJSONString("Points", "Combined loudness, flatness and EQ filter")); sw.WriteLine("}"); sw.Close(); fs.Close(); }
static bool TryReadConfig(bool firstTime) { lock (_lockReadConfig) { XmlNode node; _configDocument.Load(_configFile); int oldDepth = _depth; string oldImpulsePath = _impulsePath; int oldBands = _eqBands; double oldLoudness = _eqLoudness; double oldFlatness = _eqFlatness; string oldMatrixFilter = _matrixFilter; string oldBFormatFilter = _bformatFilter; FilterProfile oldValues = new FilterProfile(_eqValues); _isBypass = nodeValueBool(_configDocument, "//InguzEQSettings/Client/@Bypass"); // SignalGenerator mode: if there's a "SignalGenerator" element it overrides normal processing. _siggen = null; node = _configDocument.SelectSingleNode("//InguzEQSettings/Client/SignalGenerator"); if (node != null) { _siggenUseEQ = ChannelFlag.NONE; string useEQ = nodeValue(_configDocument, "//InguzEQSettings/Client/SignalGenerator/@UseEQ"); if (!String.IsNullOrEmpty(useEQ)) { switch (useEQ.ToUpperInvariant()) { case "1": case "YES": case "TRUE": case "BOTH": _siggenUseEQ = ChannelFlag.BOTH; break; case "L": _siggenUseEQ = ChannelFlag.LEFT; break; case "R": _siggenUseEQ = ChannelFlag.RIGHT; break; } } _siggen = nodeValueUpper(_configDocument, "//InguzEQSettings/Client/SignalGenerator/@Type"); if (_siggen != null) { switch (_siggen) { case "IDENT": _sigparamA = nodeValue(_configDocument, "//InguzEQSettings/Client/SignalGenerator/@L"); _sigparamB = nodeValue(_configDocument, "//InguzEQSettings/Client/SignalGenerator/@R"); break; case "SWEEP": _sigparam1 = nodeValueInt(_configDocument, "//InguzEQSettings/Client/SignalGenerator/@Length"); break; case "SINE": case "QUAD": case "SQUARE": case "BLSQUARE": case "TRIANGLE": case "BLTRIANGLE": case "SAWTOOTH": case "BLSAWTOOTH": _sigparam1 = nodeValueInt(_configDocument, "//InguzEQSettings/Client/SignalGenerator/@Freq"); break; case "WHITE": break; case "PINK": // "Mono" param defaults to true for backward compatibility _sigparam1 = 1; string mono = nodeValue(_configDocument, "//InguzEQSettings/Client/SignalGenerator/@Mono"); if (!String.IsNullOrEmpty(mono)) { _sigparam1 = bool.Parse(mono) ? 1 : 0; } break; case "INTERMODULATION": _sigparam1 = nodeValueInt(_configDocument, "//InguzEQSettings/Client/SignalGenerator/@Freq1"); _sigparam2 = nodeValueInt(_configDocument, "//InguzEQSettings/Client/SignalGenerator/@Freq2"); _sigparam3 = nodeValueInt(_configDocument, "//InguzEQSettings/Client/SignalGenerator/@Freq3"); break; case "SHAPEDBURST": _sigparam1 = nodeValueInt(_configDocument, "//InguzEQSettings/Client/SignalGenerator/@Freq"); _sigparam2 = nodeValueInt(_configDocument, "//InguzEQSettings/Client/SignalGenerator/@Cycles"); break; default: _siggen = null; break; } } } // Impulse path can be changed at runtime. Null or "" or "-" are all OK (null) values. node = _configDocument.SelectSingleNode("//InguzEQSettings/Client/Filter"); // @URL"); _impulsePath = node == null ? "" : node.InnerText; // Same for stereo-width ('matrix') filter node = _configDocument.SelectSingleNode("//InguzEQSettings/Client/Matrix"); // @URL"); _matrixFilter = node == null ? "" : node.InnerText; // Same for ambisonic filter (not documented) node = _configDocument.SelectSingleNode("//InguzEQSettings/Client/BFormat"); // @URL"); _bformatFilter = node == null ? "" : node.InnerText; node = _configDocument.SelectSingleNode("//InguzEQSettings/Client/EQ/@Bands"); _eqBands = (node == null) ? 0 : int.Parse(node.Value, CultureInfo.InvariantCulture); _eqValues.Clear(); XmlNodeList nodes = _configDocument.SelectNodes("//InguzEQSettings/Client/EQ/Band"); if (nodes != null) { foreach (XmlNode bnode in nodes) { string att; att = ((XmlElement)bnode).GetAttribute("Freq"); double freq = (att == null) ? 0 : double.Parse(att, CultureInfo.InvariantCulture); if (freq >= 10 && freq < 48000) { att = ((XmlElement)bnode).InnerText; //.GetAttribute("dB"); double gain = (att == null) ? 0 : double.Parse(att, CultureInfo.InvariantCulture); if (gain < -20) gain = -20; if (gain > 20) gain = 20; _eqValues.Add(new FreqGain(freq, gain)); } } } _eqLoudness = nodeValuePercentage(_configDocument, "//InguzEQSettings/Client/Quietness", 0); _eqFlatness = nodeValuePercentage(_configDocument, "//InguzEQSettings/Client/Flatness", 100); node = _configDocument.SelectSingleNode("//InguzEQSettings/Client/Width"); _width = (node == null) ? 0 : double.Parse(node.InnerText, CultureInfo.InvariantCulture); double fracWid = _width / 2; _widthShuffler.SigmaGain = MathUtil.gain(-fracWid); _widthShuffler.DeltaGain = MathUtil.gain(fracWid); // UI specifies width in dB // Depth not documented (and not really working either) node = _configDocument.SelectSingleNode("//InguzEQSettings/Client/Depth"); _depth = (node == null) ? 0 : int.Parse(node.InnerText, CultureInfo.InvariantCulture); // _depthSkewer.Skew = _depth; node = _configDocument.SelectSingleNode("//InguzEQSettings/Client/Balance"); _balance = (node == null) ? 0 : double.Parse(node.InnerText, CultureInfo.InvariantCulture); SetWriterGain(); node = _configDocument.SelectSingleNode("//InguzEQSettings/Client/Skew"); _skew = (node == null) ? 0 : int.Parse(node.InnerText, CultureInfo.InvariantCulture); _skewSkewer.Skew = _skew; // <AmbisonicDecode Type="XXX" Cardioid="N" Angle="N" File="path"> // // For 2ch Ambisonic decodes (stereo UHJ, stereo Blumlein, etc), the type is specified in a parameter _ambiType = nodeValue(_configDocument, "//InguzEQSettings/Client/AmbisonicDecode/@Type"); if (!String.IsNullOrEmpty(_ambiType) && _ambiType.ToUpperInvariant() == "MATRIX") { _aftenNeeded = true; } _ambiCardioid = nodeValueDouble(_configDocument, "//InguzEQSettings/Client/AmbisonicDecode/@Cardioid"); _ambiMicAngle = nodeValueDouble(_configDocument, "//InguzEQSettings/Client/AmbisonicDecode/@Angle"); _ambiRotateX = nodeValueDouble(_configDocument, "//InguzEQSettings/Client/AmbisonicDecode/@RotateX"); _ambiRotateY = nodeValueDouble(_configDocument, "//InguzEQSettings/Client/AmbisonicDecode/@RotateY"); _ambiRotateZ = nodeValueDouble(_configDocument, "//InguzEQSettings/Client/AmbisonicDecode/@RotateZ"); // For other Ambisonic decodes, the full decode matrix // is defined in a file, specified in config _ambiMatrixFile = nodeText(_configDocument, "//InguzEQSettings/Client/AmbisonicDecode/@File"); if (!firstTime) { bool eqChanged = false; if ((_eqBands != oldBands) || (_eqLoudness != oldLoudness) || (_eqFlatness != oldFlatness)) { eqChanged = true; } else { for (int n = 0; n < _eqValues.Count; n++) { if (_eqValues[n].Gain != oldValues[n].Gain || _eqValues[n].Gain != oldValues[n].Gain) { eqChanged = true; break; } } } if (eqChanged || (oldImpulsePath != _impulsePath)) { // Notify the convolver that there's a new impulse file LoadImpulse(); } if ((oldMatrixFilter != _matrixFilter) || (_depth != oldDepth)) { LoadMatrixFilter(); } if (oldBFormatFilter != _bformatFilter) { LoadBFormatFilter(); } } return true; } }