fabsenet/adrilight

Einbringung eigener Entwicklungen (Sanfte Farbübergänge / Farben / Performance / Borderdetection / Audiovisualisierung / Ideen)

HopefullyICanChangeThatNameLaterOn opened this issue · 2 comments

Hallo Fabian,

ich habe privat einige Weiterentwicklungen an Adrilight gemacht und wollte ein paar meiner Änderungen/Ideen/Infos mit dir teilen. Möglicherweise schafft das ein oder andere es in zukünftige Versionen oder gibt dir eine Anregung :)
Ich werde etwas Code von mir posten (nur relevante Stellen) - sorry vorab für die durchwachsense Qualität :D

Sanfte Farbübergänge / Performance

Wichtigster Punkt waren für mich sanfte Farbübergänge. Das macht das ganze Bild wesentlich geschmeidiger und "flackerfreier" - gerade in schnelleren Actionszenen werden diese schnell wechselnden Farben ziemlich anstrengend und störend. Die GPU lässt sich dadurch auch entlasten (dazu unten mehr).

Implementiert in DesktopDuplicateReader.Run() (zwischen Aufruf GetAverageColorOfRectangularRegion und ApplyColorCorrections)

if (UserSettings.Smoothness > 0)
{

    float diffR = Math.Abs(spot.RedCaptured - avgR);
    float diffG = Math.Abs(spot.GreenCaptured - avgG);
    float diffB = Math.Abs(spot.BlueCaptured - avgB);

    float maxDiff = Math.Max(diffR, Math.Max(diffG, diffB));

    float avgRSmoothed = CalculateTransitionValue2(spot.RedCaptured,  avgR, row, diffR / maxDiff);
    float avgGSmoothed = CalculateTransitionValue2(spot.GreenCaptured, avgG, row, diffG / maxDiff);
    float avgBSmoothed = CalculateTransitionValue2(spot.BlueCaptured, avgB, row, diffB / maxDiff);

    avgR = avgRSmoothed;
    avgG = avgGSmoothed;
    avgB = avgBSmoothed;
}

spot.SetCapturedColor(avgR, avgG, avgB);
private float CalculateTransitionValue2(float oldColor, float newColorUnweighted, long row, float weight)
{
    float diff2 = newColorUnweighted - oldColor;
    if (diff2 == 0)
    {
        return newColorUnweighted;
    }

    if (UserSettings.SmoothnessMinOffset != 1 || UserSettings.SmoothnessMaxOffset != 255)
    {
        float abs = Math.Abs(diff2);
        abs = Math.Max(UserSettings.SmoothnessMinOffset, abs);
        abs = Math.Min(UserSettings.SmoothnessMaxOffset, abs);
        abs = abs * weight;

        if (diff2 > 0)
        {
            newColorUnweighted = Clamp(((byte)(Clamp((int)oldColor + abs, 0, 255))), oldColor, newColorUnweighted);
        }
        else
        {
            newColorUnweighted = Clamp(((byte)(Clamp((int)oldColor - abs, 0, 255))), newColorUnweighted, oldColor);
        }

    }

    return newColorUnweighted;
}

public static float Clamp(float value, float min, float max)
{
    return (value < min) ? min : (value > max) ? max : value;
}

public static int Clamp(int value, int min, int max)
{
    return (value < min) ? min : (value > max) ? max : value;
}

public static byte Clamp(byte value, byte min, byte max)
{
    return (value < min) ? min : (value > max) ? max : value;
}

Um die Farbübergänge gleichmäßig zu bekommen sollte auch der Aufruf von GetNextFrame() in einen eigenen Thread ausgelagert werden. Dieser Aufruf dauert manchmal recht lange und würde sonst zu einem leichten Ruckeln führen.
Zu Beginn von DesktopDuplicateReader.Run() erstelle ich einen neuen Thread mit folgender Logik:

private void DoWork(object tokenObject)
{
	var cancellationToken = (CancellationToken)tokenObject;
	while (!cancellationToken.IsCancellationRequested)
	{
		var frameTime = Stopwatch.StartNew();

		Bitmap reusable = null;
		lock (bitmapPool)
		{
			for (int i = 0; i < bitmapPool.Length; i++)
			{
				if (bitmapPool[i] != null && (bitmapPool[i] != newImage || newImage == null) && bitmapPool[i] != image)
				{
					reusable = bitmapPool[i];
					bitmapPool[i] = null;
					break;
				}
			}
		}

		newImage = _retryPolicy.Execute(() => GetNextFrame(reusable));

		addToPool(reusable);

		int minFrameTimeInMs = (int) (1000 / (UserSettings.LimitFps / 4F));
		var elapsedMs = (int)frameTime.ElapsedMilliseconds;
		if (elapsedMs < minFrameTimeInMs)
		{
			Thread.Sleep(minFrameTimeInMs - elapsedMs);
		}
	}
}

Aus Performancegründen habe ich mich auch dazu entschieden den Thread in SerialStream zu entfernen und die Daten an den SerialPort direkt am Ende von DesktopDuplicateReader.Run() zu senden.
Der Thread in SerialStream sendet meist doppelt so häufig wie DesktopDuplicateReader.Run() läuft. Das macht aber keinen Sinn, da er dann einfach nur die gleichen Farben nochmal schickt (spart etwas CPU). So bekommt das Arduino die Daten genau so oft geliefert wie die FPS eingestellt sind.
Ich habe also zwei Threads:

  • einer holt sich das aktuelle Bild (läuft mit FPS/4 = bei mir also 10fps)
  • der andere wertet das Bild aus und sendet Daten direkt über SerialStream (FPS = bei mir 40fps)

10fps für den oberen Thread genügt für mich. Durch die sanften Farbübergänge wirkt das Bild aber dennoch flüssig. Das entlastet auch die GPU merklich!

Der BitmapPool ist ein ziemlich hässliches Konstrukt geworden um weiterhin "reusable" Bitmaps zu haben - spart auch Performance (ein Bitmap darf immer nur einmal gelockt werden).
Bei Nutzung meines Ansatzes benötigt man 3 Bitmaps: 1x Bitmap für die Daten vom Screen (GPU) (1. Thread); 1x Bitmap welches immer wieder in DesktopDuplicateReader.Run() verwendet wird (2. Thread); 1x Bitmap welches ein neues Bild für DesktopDuplicateReader.Run() bereitstellt (Run() holt sich das 3. Bitmap ab und verschiebt es dann zu 2.)

Farben

Alternative Non-Linear Formel
Auch hier habe ich lange rumprobiert und habe für mich folgende Formel gefunden. Insgesamt ist diese etwas kontrastärmer, dafür aber (für mich) genauer in der Farbtreue.
In deiner Formel wurde z.B. Orange sehr schnell zu Rot.

i = 0; i < 2560

float factor1 = UserSettings.Factor / 10f;
byte a = (byte)(Math.Pow((i / 10f), factor1) * (256 / Math.Pow(256, factor1)));

Dynamische Farbanpassungsregeln
Gewisse Farben waren etwas zu dominant und haben nicht zum Bild auf dem Monitor gepasst. z.B. pures LED-Blau ist viel zu "Blau". Wenn man etwas Rot dazumischt passt es bei mir viel besser. Um das Problem zu lösen, habe ich eine kleine Formel gebaut.
z.B. angenommen wir haben UserSettings.RedToBlueFactor=30 eingestellt und die Farbwerte R=0 und B=255. Dann würde R=76,5 (30% von Blau) rauskommen.

Ich denke man kann diesen Ansatz noch weiter optimieren. Wünschenswert wäre es z.B. wenn der User selber Regeln hinzufügen kann (0..n) und alle Parameter selber definieren kann (Faktor zum Aktivwerden; Faktor der Farbangleichung etc...).

In ApplyColorCorrections:

        r = CalculateFactor(UserSettings.RedToGreenFactor, r, g);
        r = CalculateFactor(UserSettings.RedToBlueFactor, r, b);
        g = CalculateFactor(UserSettings.GreenToRedFactor, g, r);
        g = CalculateFactor(UserSettings.GreenToBlueFactor, g, b);
        b = CalculateFactor(UserSettings.BlueToGreenFactor, b, g);
        b = CalculateFactor(UserSettings.BlueToRedFactor, b, r);


        //apply non linear LED fading ( http://www.mikrocontroller.net/articles/LED-Fading )
        finalR = FadeNonLinear(r);
        finalG = FadeNonLinear(g);
        finalB = FadeNonLinear(b);


    }
    else
    {
        //output
        finalR = (byte)r;
        finalG = (byte)g;
        finalB = (byte)b;
    }
}

private float CalculateFactor(int factor, float color1, float color2)
{
    if (factor > 0)
    {
        float factor1 = factor / 100f;
        if (color2 * factor1 > color1)
        {
            color1 = color2 * factor1;
        }
    }

    return color1;
}

Border Detection

Hier hatte ich angefangen automatisch die Ränder oben/unten zu erkennen. Dann habe ich bemerkt, dass es auch Ränder links/rechts geben kann (z.B wenn man Bilder zentriert in einer Diashow anzeigt).
Im letzten und besten Ansatz schaue ich nun pro Spot wo ein Farbwert ist (gibt ja auch ungerade Ränder wie in Spielen wie Pillars of Eternity...). Dazu wandere ich mit den Spots am oberen Rand nach unten und von Links nach Rechts etc (bis Bildschirmmitte).
Nach GetAverageColorOfRectangularRegion in DesktopDuplicateReader.Run():

if (UserSettings.ColorDetectionAuto)
{
	if (avgR == 0 && avgG == 0 && avgB == 0)
	{
		//Rectangle screen = SpotSet.ExpectedScreenBound;
		int screenWidth = SpotSet.ExpectedScreenWidth;
		int screenHeight = SpotSet.ExpectedScreenHeight;

		const int minColor = 30;
		const int stepSize = 10; // Before factor 8 it was 50

		if (spot.Orientation == SpotOrientation.TOP)
		{
			for (int i = stepSize; i < (screenHeight / 2); i += stepSize)
			{
				int spotX = spot.Rectangle.X + (spot.Rectangle.Width / 2);
				int spotY = spot.Rectangle.Y + (spot.Rectangle.Height / 2);
				byte* pointer = (byte*)bitmapData.Scan0 + bitmapData.Stride * (i) + 4 * spotX;

				//if (spot.Rectangle.X == 1265 && spot.Rectangle.Y == 1) {
				//    _log.Debug(pointer[2]  + " " +  pointer[1] + " " + pointer[0]);
				//}

				if (pointer[2] > minColor || pointer[1] > minColor || pointer[0] > minColor)
				{ // RGB
					Rectangle rectangleOriginal = spot.Rectangle;
					spot.Rectangle = new Rectangle(spot.Rectangle.X, (i), spot.Rectangle.Width, spot.Rectangle.Height);
					GetAverageColorOfRectangularRegion(spot.Rectangle, bitmapData, out avgR, out avgG, out avgB);
					spot.Rectangle = rectangleOriginal;
					break;
				}
			}
		}

		if (spot.Orientation == SpotOrientation.BOTTOM)
		{
			for (int i = screenHeight - stepSize; i > (screenHeight / 2); i -= stepSize)
			{
				int spotX = spot.Rectangle.X + (spot.Rectangle.Width / 2);
				int spotY = spot.Rectangle.Y + (spot.Rectangle.Height / 2);
				byte* pointer = (byte*)bitmapData.Scan0 + bitmapData.Stride * (i) + 4 * spotX;
				if (pointer[2] > minColor || pointer[1] > minColor || pointer[0] > minColor)
				{ // RGB
					Rectangle rectangleOriginal = spot.Rectangle;
					spot.Rectangle = new Rectangle(spot.Rectangle.X, (i - spot.Rectangle.Height), spot.Rectangle.Width, spot.Rectangle.Height);
					GetAverageColorOfRectangularRegion(spot.Rectangle, bitmapData, out avgR, out avgG, out avgB);
					spot.Rectangle = rectangleOriginal;
					break;
				}
			}
		}


		if (spot.Orientation == SpotOrientation.LEFT)
		{
			for (int i = stepSize; i < (screenWidth / 2); i += stepSize)
			{
				int spotX = spot.Rectangle.X + (spot.Rectangle.Width / 2);
				int spotY = spot.Rectangle.Y + (spot.Rectangle.Height / 2);
				byte* pointer = (byte*)bitmapData.Scan0 + bitmapData.Stride * spotY + 4 * i;
				if (pointer[2] > minColor || pointer[1] > minColor || pointer[0] > minColor)
				{ // RGB
					Rectangle rectangleOriginal = spot.Rectangle;
					spot.Rectangle = new Rectangle((i), spot.Rectangle.Y, spot.Rectangle.Width, spot.Rectangle.Height);
					GetAverageColorOfRectangularRegion(spot.Rectangle, bitmapData, out avgR, out avgG, out avgB);
					spot.Rectangle = rectangleOriginal;
					break;
				}
			}
		}

		if (spot.Orientation == SpotOrientation.RIGHT)
		{
			for (int i = screenWidth - stepSize; i > (screenWidth / 2); i -= stepSize)
			{
				int spotX = spot.Rectangle.X + (spot.Rectangle.Width / 2);
				int spotY = spot.Rectangle.Y + (spot.Rectangle.Height / 2);
				byte* pointer = (byte*)bitmapData.Scan0 + bitmapData.Stride * spotY + 4 * i;
				if (pointer[2] > minColor || pointer[1] > minColor || pointer[0] > minColor)
				{ // RGB
					Rectangle rectangleOriginal = spot.Rectangle;
					spot.Rectangle = new Rectangle((i - spot.Rectangle.Width), spot.Rectangle.Y, spot.Rectangle.Width, spot.Rectangle.Height);
					GetAverageColorOfRectangularRegion(spot.Rectangle, bitmapData, out avgR, out avgG, out avgB);
					spot.Rectangle = rectangleOriginal;
					break;
				}
			}
		}
	}
}

Audiovisualisierung (draft)

Mit NAudio und FFT und einiger Recherche habe ich auch hier schon was gebaut bzw. zusammenkopiert :)
Meine 5.1 Ausgabe läuft über DTS Interactive - das braucht keine relative Lautstärkeanpassung deswegen gesondert gehandhabt.

	private static MMDeviceEnumerator enumer; // must be a field so that OnVolumeNotification works?!
	private MMDevice dev; // must be a field so that OnVolumeNotification works?!

	private static WasapiLoopbackCaptureYa waveIn;
	private static int fftLength = 8192; // NAudio fft wants powers of two!

	// There might be a sample aggregator in NAudio somewhere but I made a variation for my needs
	private static SampleAggregator sampleAggregator = new SampleAggregator(fftLength);

	static float Volume = 1;
	private int ledLuminosity = 0;

	public List<byte> _spectrumdata;
	List<int> _average = new List<int>();

	private void RefreshMusicCapture()
	{
		//MMDeviceEnumerator enumer = new MMDeviceEnumerator();
		//var dev = enumer.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
		////var dev = enumer.GetDefaultAudioEndpoint(DataFlow.Render, DeviceState.Active);

		if (UserSettings.EnableMusic)
		{
			if (enumer == null)
			{
				enumer = new MMDeviceEnumerator();
				//var renderDevices = deviceEnum.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active).ToList();
				dev = enumer.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);

				sampleAggregator.FftCalculated += new EventHandler<FftEventArgs>(FftCalculated);
				sampleAggregator.PerformFFT = true;

				var a = dev.AudioEndpointVolume;
				Volume = a.MasterVolumeLevelScalar;

				a.OnVolumeNotification += data => {
					Volume = data.MasterVolume;
					//Console.WriteLine("OnVolumeNotification");
					if (waveIn.WaveFormat.Channels == 6)
					{
						Volume = 0.8f;
					}
				};
			}

			// Here you decide what you want to use as the waveIn.
			// There are many options in NAudio and you can use other streams/files.
			// Note that the code varies for each different source.


			//            Console.WriteLine(renderDevices[0].AudioEndpointVolume.MasterVolumeLevelScalar);
			waveIn = new WasapiLoopbackCaptureYa(dev);
			if (waveIn.WaveFormat.Channels == 6)
			{
				Volume = 0.8f;
			}
			else
			{
				var a = dev.AudioEndpointVolume;
				Volume = a.MasterVolumeLevelScalar;
			}

			waveIn.DataAvailable += OnDataAvailable;

			Console.WriteLine(waveIn.WaveFormat.Channels);

			waveIn.RecordingStopped += (s, e) => {
				waveIn.StopRecording();
				waveIn.Dispose();
				RefreshMusicCapture();
			};

			waveIn.StartRecording();
		}
		else
		{
			dispose();
		}
	}

	public void OnDataAvailable(object sender, WaveInEventArgs e)
	{
		// Stopwatch s =  new Stopwatch();
		// s.Start();
		//Console.WriteLine("start " + DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond + " | " + e.Buffer.Length);

		//if (this.InvokeRequired) {
		//this.BeginInvoke(new EventHandler<WaveInEventArgs>(OnDataAvailable), sender, e);
		//} else {
		byte[] buffer = e.Buffer;
		int bytesRecorded = e.BytesRecorded;

		//Console.WriteLine(bytesRecorded);

		//if (bytesRecorded > 8192) {
		//    bytesRecorded = 8192;
		//}

		int bufferIncrement = waveIn.WaveFormat.BlockAlign;

		//Console.WriteLine("start " +DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond);
		for (int index = 0; index < bytesRecorded; index += bufferIncrement)
		{
			float sample32 = BitConverter.ToSingle(buffer, index);
			sampleAggregator.Add(sample32);
		}
		//Console.WriteLine("2  " + DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond);
		//}
		// s.Stop();
		// Console.WriteLine(s.ElapsedMilliseconds);

	}

	void FftCalculated(object sender, FftEventArgs e)
	{
		var data = e.Result;

		float[] magnitudes = new float[data.Length];

		for (int i = 0; i < data.Length; i++)
			magnitudes[i] = ((float)Math.Sqrt((data[i].X * data[i].X) + (data[i].Y * data[i].Y)));

		var _fft = magnitudes;
		var _lines = 64;

		List<byte> spectrumdata = new List<byte>();
		//_spectrumdata.Clear();

		float factor = 1 / Volume;

		{
			int x, y;
			int b0 = 0;

			//computes the spectrum data, the code is taken from a bass_wasapi sample.
			for (x = 0; x < _lines; x++)
			{
				float peak = 0;
				int b1 = (int)Math.Pow(2, x * 10.0 / (_lines - 1));
				if (b1 > 1023) b1 = 1023;
				if (b1 <= b0) b1 = b0 + 1;
				for (; b0 < b1; b0++)
				{
					if (peak < _fft[1 + b0]) peak = _fft[1 + b0];
				}
				y = (int)(Math.Sqrt(peak) * 3 * factor * 255 - 4); // org
				//y = (int)(peak * 15000 * factor);

				//y = (int)(Math.Sqrt(peak) * 2 * 255 - 4);?
				if (y > 255) y = 255;
				if (y < 0) y = 0;
				spectrumdata.Add((byte)y);
				//Console.Write("{0, 3} ", y);
			}
		}

		_spectrumdata = spectrumdata;

		//byte[] asd = new byte[64];
		//asd[0] = (byte)new System.Random().Next(5, 200);

		//SettingsViewModel.LedBrightness = new System.Random().Next(5, 200);
		//SettingsViewModel.LedBrightness = getAverageValue();

		//int _led_value = (int)map(
		//        getAverage(getAverageValue()),
		//        0,
		//        255,
		//        trackBar_minVal.Value,
		//        trackBar_maxVal.Value);

		//int _led_value = (int)map(
		//        getAverage(getAverageValue()),
		//        0,
		//        255,
		//       1,
		//       255);

		//SettingsViewModel.LedBrightness = (int)map(_led_value, 0, 255, 0, 255);
		int led = getAverage(getAverageValue());

		if (SettingsViewModel.IsSettingsWindowOpen)
		{
			SettingsViewModel.Bar01 = _spectrumdata.ToArray<byte>();
			SettingsViewModel.LedBrightness = led;
		}

		led -= 50;
		if (led < 0) led = 0;
		led = (int)((led / 200d) * 25d);
		ledLuminosity = led;
		//UserSettings.Luminosity = led;


		//UserSettings.Saturation = led;
	}

	private int getAverage(int value)
	{
		_average.Add(value);

		while (_average.Count > UserSettings.Aggression)
		{
			_average.RemoveAt(0);
		}

		return (int)(_average.Sum() / _average.Count());
	}

	public int getAverageValue()
	{
		int sum = 0;
		for (int i = UserSettings.LowChannel; i <= UserSettings.HighChannel; i++)
		{
			//sum += (int)_chart.Series["wave"].Points[i].YValues[0];
			sum += _spectrumdata[i];
		}

		return (int)sum / (UserSettings.HighChannel - UserSettings.LowChannel + 1);
	}

	float map(int x, int in_min, int in_max, int out_min, int out_max)
	{
		return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
	}


	public void dispose()
	{
		if (waveIn != null)
		{
			waveIn.StopRecording();
			waveIn.Dispose();
		}
	}
}

class SampleAggregator
{
	// FFT
	public event EventHandler<FftEventArgs> FftCalculated;
	public bool PerformFFT { get; set; }

	// This Complex is NAudio's own! 
	private Complex[] fftBuffer;
	private FftEventArgs fftArgs;
	private int fftPos;
	private int fftLength;
	private int m;

	private Complex[] fftBufferPrev;

	public SampleAggregator(int fftLength)
	{
		if (!IsPowerOfTwo(fftLength))
		{
			throw new ArgumentException("FFT Length must be a power of two");
		}
		this.m = (int)Math.Log(fftLength, 2.0);
		this.fftLength = fftLength;
		this.fftBuffer = new Complex[fftLength];
		fftBufferPrev = new Complex[fftLength];
		this.fftArgs = new FftEventArgs(fftBuffer);
	}

	bool IsPowerOfTwo(int x)
	{
		return (x & (x - 1)) == 0;
	}

	public void Add(float value)
	{
		if (PerformFFT && FftCalculated != null)
		{
			// Remember the window function! There are many others as well.
			// fftBuffer[fftPos].X = (float)(value * FastFourierTransform.HammingWindow(fftPos, fftLength));
			fftBuffer[fftPos].X = value;
			fftBuffer[fftPos].Y = 0; // This is always zero with audio.
			fftPos++;
			if (fftPos >= fftLength)
			{
				//fftPos = 0;
				fftPos = 7500;

				//fftBufferPrev = new Complex[fftBuffer.Length];
				for (int i = 0; i < fftBuffer.Length; i++)
				{
					fftBufferPrev[i].X = fftBuffer[i].X;
					fftBufferPrev[i].Y = fftBuffer[i].Y;
				}

				for (int i = 0; i < fftBuffer.Length; i++)
				{
					fftBuffer[i].X = (float)(fftBuffer[i].X * FastFourierTransform.HammingWindow(i, fftLength));
				}

				FastFourierTransform.FFT(true, m, fftBuffer);
				FftCalculated(this, new FftEventArgs(fftBuffer));

				for (int i = 0; i < fftPos; i++)
				{
					fftBuffer[i].X = fftBufferPrev[fftBuffer.Length - fftPos + i].X;
					fftBuffer[i].Y = 0;
				}

			}
		}
	}
}

public class FftEventArgs : EventArgs
{
	public FftEventArgs(Complex[] result)
	{
		this.Result = result;
	}
	public Complex[] Result { get; private set; }
}

public class WasapiLoopbackCaptureYa : WasapiCapture
{
	/// <summary>
	/// Initialises a new instance of the WASAPI capture class
	/// </summary>
	public WasapiLoopbackCaptureYa() :
		this(GetDefaultLoopbackCaptureDevice())
	{
	}

	/// <summary>
	/// Initialises a new instance of the WASAPI capture class
	/// </summary>
	/// <param name="captureDevice">Capture device to use</param>
	public WasapiLoopbackCaptureYa(MMDevice captureDevice) :
		base(captureDevice, false, 10)
	{
	}

	/// <summary>
	/// Gets the default audio loopback capture device
	/// </summary>
	/// <returns>The default audio loopback capture device</returns>
	public static MMDevice GetDefaultLoopbackCaptureDevice()
	{
		MMDeviceEnumerator devices = new MMDeviceEnumerator();
		return devices.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
	}

	/// <summary>
	/// Capturing wave format
	/// </summary>
	public override WaveFormat WaveFormat {
		get { return base.WaveFormat; }
		set { throw new InvalidOperationException("WaveFormat cannot be set for WASAPI Loopback Capture"); }
	}

	/// <summary>
	/// Specify loopback
	/// </summary>
	protected override AudioClientStreamFlags GetAudioClientStreamFlags()
	{
		return AudioClientStreamFlags.Loopback;
	}
}

Weitere Ideen

  • Nachtmodus: verhindert das LEDs komplett dunkel werden, indem die zuletzt angezeigten Farben angezeigt werden wenn zu viele LEDs dunkel werden
  • Color Corrections: Helligkeit/Sättigung (über Konvertierung zu HSL).
  • Statische Farben: z.B. nur als Hintergrundbeleuchtung

Screenshot

Anbei ein Bildchen von meinem aktuellen Setup.
Unten siehst du die Audiovisualisierung. Bar 7-15 sind als relevante Frequenzbereiche (Bass) eingestellt. Aus diesem Bereich wird der Durschnittswert berechnet und darüber die LED Helligkeit geregelt.

Die Performancesettings sind mit deinem FPS-update hinfällig.

grafik

Config

Als Referenz meine aktuelle Config:

{
  "ConfigFileVersion": 1,
  "Autostart": false,
  "BorderDistanceX": 0,
  "BorderDistanceY": 0,
  "ComPort": "COM3",
  "LastUpdateCheck": "2018-06-09T05:35:20.6034623Z",
  "LedsPerSpot": 1,
  "MirrorX": false,
  "MirrorY": true,
  "OffsetLed": 18,
  "OffsetX": 0,
  "OffsetY": 0,
  "LimitFps": 40,
  "IsPreviewEnabled": false,
  "SaturationTreshold": 10,
  "SpotHeight": 30,
  "SpotsX": 37,
  "SpotsY": 23,
  "SpotWidth": 30,
  "StartMinimized": true,
  "TransferActive": true,
  "UseLinearLighting": false,
  "WhitebalanceRed": 100,
  "WhitebalanceGreen": 85,
  "WhitebalanceBlue": 73,
  "SendRandomColors": false,
  "InstallationId": "0d0a60f7-892d-4b4b-b381-a5fe1c0950a5",
  "MinimumLuminosity": 0,
  "MaximumLuminosity": 2400,
  "Luminosity": 0,
  "Saturation": 0,
  "Smoothness": 75,
  "SmoothnessMinOffset": 8,
  "SmoothnessMaxOffset": 8,
  "SleepMsBetweenEachFrame": 25,
  "SleepMsFrameNotReadFromGpu": 10,
  "NewFrameFromGpuThreshold": 3,
  "FactorNonLinearStandard": 80,
  "Factor": 18,
  "RedToBlueFactor": 30,
  "RedToGreenFactor": 60,
  "BlueToGreenFactor": 0,
  "BlueToRedFactor": 0,
  "GreenToRedFactor": 10,
  "GreenToBlueFactor": 30,
  "R": 34,
  "G": 24,
  "B": 19,
  "ColorOverrideActive": false,
  "SpotOverride": 25,
  "SpotOverrideActive": false,
  "AlternativeNonLinearMode": true,
  "ReduceLuminisotyIfLow": false,
  "ColorThreshold": 0,
  "EnableAutoMinimumLuminosity": true,
  "BlackAmountForEnablingMinimumLuminosity": 100,
  "LowerThresholdForDisablingMinimumLuminosity": 82,
  "BorderDistanceAuto": false,
  "ColorDetectionAuto": true,
  "LowChannel": 6,
  "HighChannel": 14,
  "Aggression": 6,
  "EnableMusic": false,
  "IsInDesignMode": false
}

Gruß
Michael

ich glaube, ich habe noch nie so einen riesigen issue gesehen :-)

ich versuch das mal der Reihe nach abzuarbeiten:

Sanfte Farbübergänge / Performance

Ich finde es persönlich gerade gut, wenn das ambilight es schafft, genauso schnell zu sein wie der bildinhalt. wenn da eine aktionszene ist und das bild flackert, dann erwarte ich das auch von ambilight. ich finde hier eher die original philips fernseher teilweise öde. sicherlich ist das eine frage des geschmacks.

dass du dann einen weiteren thread brauchst ist klar. zeitlich gleitende farben benötigen einen festen zeitlichen takt, sonst ist das komisch. Die frames kommen von windows aber immer dann, wenn der bildinhalt sich ändert, sonst kommt nichts und zumindest meine adrilight version macht dann auch nichts.

bin ich aktuell nicht von überzeugt.

Farben

Alternative Non-Linear Formel

muss ich ausprobieren und schauen, wie ich das finde. Ich sehe nur gerade, du hast UserSettings.Factor. Ich habe immer versucht, die UserSettings so spezifisch wie möglich zu benennen. Auf jeden fall hast du recht. Das Problem ist, dass gerade sehr dunkle farben wenig spielraum haben.

Dynamische Farbanpassungsregeln

Du hast gesehen, dass adrilight whitebalance kann? wenn ich mir das richtig einstelle zuhause, dann sind die led farben sehr sehr nahe an den farben auf dem fernseher und alles sieht gut aus. Die farbkanaäle miteinander zu mischen kann ich mir gerade nicht richtig vorstellen, wie das etwas besser macht :-)

Border detection

ich hatte auch lange vor, ein dynamisches system zu schreiben, was schwarze balken erkennt und dann die spots verschiebt. Zwischenzeitlich hatte ich es bei filmen von hand umgestellt aber ehrlich gesagt, gefällt es mir nicht. es ist einfach unnatürlich, wenn das bild zu ende ist, ein schwarzer balken da ist und dann auf einmal da wieder farbiges licht ist. wir schauen viel serien und youtube, wo idR keine balken sind bei 16:9 aber wenn wir einen film schauen, dann haben wir halt oben und unten nur schwarz, finde ich aber besser. Nur weil man das ambilight hat, muss es nicht 100% der zeit leuchten.

Audiovisualisierung

Die idee ist nicht neu, wurde hier glaube ich auch schon einige male gewünscht, aber ich bin nicht überzeugt. kannst du ein video zeigen, wo du musik abspielst? leuchtet dein bildschirm/fernseher dann nicht wie ein weihnachtsbaum?

was ich aber jetzt auf basis deines inputs tatsächlich überlege, ist das hinzufügen eines gegenstücks zum "absolute black threshold". ist die farbe zu dunkel, mache ich sie ja komplett schwarz. das gegenstück wäre eine art garantierte mindesthelligkeit, wenn man es nicht vollständig dunkel mag. so dass man sagen kann, ist die farbe dunkler als x, zeig mindestens x an. Dein Nachtmodus arbeitet sonst ja auch nochmal anders, macht aber das "erlebnis", wenn ein film zwischen 2 szenen kurz mal schwarz wird kaputt?!

...ich hab jetzt viel von deinen änderungen zurück gewiesen und das ist auch gar nicht böse gemeint. ich sehe adrilight ja eher so etwas puristischer. technisch so ausgefeilt, wie es meine zeit und lust zulässt, aber sonst halt nicht als "featureexplosion" ...sonst kommt der nächste und will eine led rot blinken lassen, wenn er ne mail bekommt, grün für whatsapp und blau für facebook ;-)

wo man natürlich drüber nachdenken könnte, wäre eine art pluginsystem und wenn jemand besondere wünsche hat, installiert/programmiert er sich ein plugin dafür. denn du kannst mit deinen vielen anpassungen ja jetzt gar nicht mehr auf 2.0.8 upgraden und alles was danach kommt?

@HopefullyICanChangeThatNameLaterOn du kannst gerne hier weiterhin antworten, aber ich schließe das hier erst mal...