diff --git a/.gitignore b/.gitignore
index 319b659f..7167dd90 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,8 @@
docfx_project/log.txt
log.txt
+src/Native/* linguist-vendored
+
# Build results
[Dd]ebug/
[Dd]ebugPublic/
diff --git a/src/Maui/Addons/DrawnUi.Maui.Camera/Apple/SkiaCamera.Apple.cs b/src/Maui/Addons/DrawnUi.Maui.Camera/Apple/SkiaCamera.Apple.cs
index e73f28e8..678ecc6c 100644
--- a/src/Maui/Addons/DrawnUi.Maui.Camera/Apple/SkiaCamera.Apple.cs
+++ b/src/Maui/Addons/DrawnUi.Maui.Camera/Apple/SkiaCamera.Apple.cs
@@ -308,16 +308,9 @@ private async Task CaptureFrameCore()
}
}
- if (imageToDraw == null)
- {
- var raw = nativeCam.GetRawFullImage();
- if (raw.Image != null)
- {
- imageToDraw = raw.Image;
- imageRotation = raw.Rotation;
- imageFlip = raw.Flip;
- }
- }
+ // No fallback to GetRawFullImage - it reads _latestRecordingFrame which was
+ // populated earlier and can be older than what zero-copy previously encoded,
+ // causing out-of-order frames in the video. Better to drop a frame than glitch.
}
// Fallback to standard preview image (slower, already rotated)
diff --git a/src/Maui/Samples/Camera/CameraTestPage.AppCamera.cs b/src/Maui/Samples/Camera/CameraTestPage.AppCamera.cs
index 4bbe355d..63964978 100644
--- a/src/Maui/Samples/Camera/CameraTestPage.AppCamera.cs
+++ b/src/Maui/Samples/Camera/CameraTestPage.AppCamera.cs
@@ -7,14 +7,14 @@ public partial class CameraTestPage
public class AppCamera : SkiaCamera
{
// Audio visualizer (switch between AudioOscillograph and AudioLevels)
- private IAudioVisualizer _audioVisualizer = new AudioLevelsVU();
+ private IAudioVisualizer _audioVisualizer = null;
private int _visualizerIndex = 0;
public static readonly BindableProperty VisualizerNameProperty = BindableProperty.Create(
nameof(VisualizerName),
typeof(string),
typeof(AppCamera),
- "VU Meter");
+ "None");
public string VisualizerName
{
@@ -22,28 +22,40 @@ public string VisualizerName
set => SetValue(VisualizerNameProperty, value);
}
- public void SwitchVisualizer()
+ public void SwitchVisualizer(int index = -1)
{
- _visualizerIndex++;
- if (_visualizerIndex > 6) _visualizerIndex = 0;
+ if (index >= 0)
+ {
+ _visualizerIndex = index;
+ }
+ else
+ {
+ _visualizerIndex++;
+ }
+ if (_visualizerIndex > 8) _visualizerIndex = 0;
var old = _audioVisualizer;
- bool useGain = true;
+ bool useGain = true;
switch (_visualizerIndex)
{
case 0:
+ _audioVisualizer = new AudioSoundBars();
+ useGain = true;
+ VisualizerName = "Sound Bars";
+ break;
+ case 1:
_audioVisualizer = new AudioLevelsVU();
VisualizerName = "VU Meter";
break;
- case 1:
+ case 2:
_audioVisualizer = new AudioLevelsPeak();
VisualizerName = "Peak Monitor";
break;
- case 2:
- _audioVisualizer = new AudioLevels();
- VisualizerName = "Spectrum";
- break;
+ //case 2:
+ // _audioVisualizer = new AudioLevels();
+ // VisualizerName = "Spectrum";
+ // break;
case 3:
_audioVisualizer = new AudioOscillograph();
useGain = true;
@@ -59,6 +71,10 @@ public void SwitchVisualizer()
VisualizerName = "Tuner";
break;
case 6:
+ _audioVisualizer = new AudioWaveformBars();
+ VisualizerName = "Waveform Bars";
+ break;
+ case 8:
_audioVisualizer = null;
VisualizerName = "None";
break;
@@ -89,7 +105,7 @@ public override void OnWillDisposeWithChildren()
_paintRec = null;
_paintPreview?.Dispose();
_paintPreview = null;
-
+
(_audioVisualizer as IDisposable)?.Dispose();
_audioVisualizer = null;
}
@@ -158,6 +174,6 @@ public void DrawOverlay(DrawableFrame frame)
private SKPaint _paintRec;
}
-
+
}
}
diff --git a/src/Maui/Samples/Camera/CameraTestPage.cs b/src/Maui/Samples/Camera/CameraTestPage.cs
index 0159b49d..89d690e4 100644
--- a/src/Maui/Samples/Camera/CameraTestPage.cs
+++ b/src/Maui/Samples/Camera/CameraTestPage.cs
@@ -263,14 +263,18 @@ private void CreateContent()
CornerRadius = 8,
UseCache = SkiaCacheType.Image
}
- .OnTapped(me => { CameraControl.SwitchVisualizer(); })
+ .OnTapped(me =>
+ {
+ CameraControl.SwitchVisualizer();
+ })
.ObserveProperty(CameraControl, nameof(CameraControl.VisualizerName), me =>
{
- me.Text = $"Vis: {CameraControl.VisualizerName}";
+ me.Text = $"{CameraControl.VisualizerName}";
})
.ObserveProperty(CameraControl, nameof(CameraControl.CaptureMode), me =>
{
- me.IsVisible = CameraControl.CaptureMode == CaptureModeType.Video;
+ CameraControl.SwitchVisualizer(0);
+ //me.IsVisible = CameraControl.CaptureMode == CaptureModeType.Video;
}),
// Take Picture button (only visible in Still mode)
diff --git a/src/Maui/Samples/Camera/Visualizers/AudioSoundBars.cs b/src/Maui/Samples/Camera/Visualizers/AudioSoundBars.cs
new file mode 100644
index 00000000..27bc92ef
--- /dev/null
+++ b/src/Maui/Samples/Camera/Visualizers/AudioSoundBars.cs
@@ -0,0 +1,322 @@
+using DrawnUi.Camera;
+
+namespace CameraTests
+{
+ ///
+ /// High-resolution frequency spectrum sound bars (music player style).
+ /// Uses Goertzel algorithm to compute energy at logarithmically-spaced
+ /// frequency bins. Bars with energy are ON, bars without are fully OFF,
+ /// creating an organic pattern of active/silent frequency clusters.
+ /// ZERO render allocations, double-buffered.
+ ///
+ public class AudioSoundBars : IAudioVisualizer, IDisposable
+ {
+ public bool UseGain { get; set; } = true;
+ public int Skin { get; set; } = 1;
+ public bool ShowPeakDots { get; set; } = true;
+
+ private const int BarCount = 48;
+ private const int AnalysisSize = 1024; // Samples to analyze (power of 2 for cleaner bins)
+
+ // Ballistics
+ ///
+ /// Controls how fast a bar falls down when the audio at that frequency drops.
+ /// Each frame, if the new target is lower than the current value, the bar is set to current * 0.70.
+ /// So it loses 30% of its height per frame.
+ /// This prevents bars from snapping instantly to zero - they fade down smoothly over several frames.
+ ///
+ private const float ReleaseCoeff = 0.70f;
+
+ ///
+ /// Controls how fast the floating peak dot falls.
+ /// The dot marks the highest point a bar reached,
+ /// then slowly drifts down at current * 0.97 per frame (only 3% loss per frame).
+ /// Much slower than the bar itself, so the dot lingers near the top
+ /// while the bar drops away underneath it - classic "peak hold" meter behavior.
+ ///
+ private const float PeakDecay = 0.97f;
+
+ // Hard cutoff: bins below this fraction of max energy are completely OFF
+ private const float CutoffRatio = 0.12f;
+
+ // Absolute noise gate: if max power across all bins is below this,
+ // all bars stay OFF. Prevents ambient noise from triggering the display.
+ private const float NoiseGate = 1.00f;
+
+ private float[] _barsFrontBuffer = new float[BarCount];
+ private float[] _barsBackBuffer = new float[BarCount];
+ private float[] _peakHold = new float[BarCount];
+ private float[] _peakHoldFront = new float[BarCount];
+ private int _swapRequested = 0;
+
+ // Pre-computed Goertzel coefficients per bin
+ private float[] _goertzelCoeff = new float[BarCount];
+ private float[] _goertzelFreqs = new float[BarCount]; // For debug/tuning
+ private bool _coeffReady = false;
+ private int _lastSampleRate = 0;
+
+
+ // Per-bin gain compensation (higher freqs need more boost)
+ private float[] _binGain = new float[BarCount];
+
+ private SKPaint _paintBar;
+ private SKPaint _paintDot;
+ private SKPaint _paintText;
+ private SKPaint _paintBg;
+
+ private void InitCoefficients(int sampleRate)
+ {
+ if (_coeffReady && sampleRate == _lastSampleRate)
+ return;
+
+ _lastSampleRate = sampleRate;
+
+ // Logarithmic frequency mapping: 60Hz to 8000Hz
+ float minFreq = 60f;
+ float maxFreq = 8000f;
+ float logMin = (float)Math.Log(minFreq);
+ float logMax = (float)Math.Log(maxFreq);
+
+ for (int i = 0; i < BarCount; i++)
+ {
+ float t = i / (float)(BarCount - 1);
+ float freq = (float)Math.Exp(logMin + t * (logMax - logMin));
+ _goertzelFreqs[i] = freq;
+
+ // Goertzel: k = round(N * freq / sampleRate)
+ // coeff = 2 * cos(2*pi*k/N)
+ float k = AnalysisSize * freq / sampleRate;
+ _goertzelCoeff[i] = 2f * (float)Math.Cos(2.0 * Math.PI * k / AnalysisSize);
+
+ // Higher frequency bins get more gain to compensate
+ // for natural spectral rolloff in music
+ _binGain[i] = 1.0f + t * 2.5f;
+ }
+
+ _coeffReady = true;
+ }
+
+ public void AddSample(AudioSample sample)
+ {
+ int sampleRate = sample.SampleRate > 0 ? sample.SampleRate : 44100;
+ InitCoefficients(sampleRate);
+
+ int sampleCount = sample.Data.Length / 2;
+
+ // Use at most AnalysisSize samples from the end of the buffer (most recent audio)
+ int analyzeCount = Math.Min(sampleCount, AnalysisSize);
+ int startOffset = (sampleCount - analyzeCount) * 2; // byte offset
+
+ // Goertzel for each frequency bin
+ float maxPower = 0f;
+
+ for (int bin = 0; bin < BarCount; bin++)
+ {
+ float coeff = _goertzelCoeff[bin];
+ float s1 = 0f, s2 = 0f;
+
+ for (int i = 0; i < analyzeCount; i++)
+ {
+ int byteIndex = startOffset + i * 2;
+ if (byteIndex + 1 >= sample.Data.Length)
+ break;
+
+ short pcm = (short)(sample.Data[byteIndex] | (sample.Data[byteIndex + 1] << 8));
+ float x = pcm / 32768f;
+
+ float s0 = x + coeff * s1 - s2;
+ s2 = s1;
+ s1 = s0;
+ }
+
+ // Power = s1^2 + s2^2 - coeff*s1*s2, apply per-bin gain
+ float power = (s1 * s1 + s2 * s2 - coeff * s1 * s2) * _binGain[bin];
+ if (power < 0) power = 0; // Safety clamp
+
+ _barsBackBuffer[bin] = power;
+ if (power > maxPower) maxPower = power;
+ }
+
+ // Absolute noise gate + relative cutoff, then progressive boost for ON bars
+ float cutoff = maxPower * CutoffRatio;
+ bool gated = maxPower < NoiseGate; // Silence when just ambient noise
+
+ for (int bin = 0; bin < BarCount; bin++)
+ {
+ float power = _barsBackBuffer[bin];
+
+ float target;
+ if (gated || power < cutoff)
+ {
+ target = 0f; // Completely OFF
+ }
+ else
+ {
+ // Normalize to 0..1 relative to max, remapping from cutoff..max
+ float normalized = (power - cutoff) / (maxPower - cutoff);
+ // Square root curve: compresses range so mid-level bins
+ // appear tall while keeping proportions between ON bars
+ target = (float)Math.Sqrt(normalized);
+ }
+
+ // Instant attack, smooth release
+ float current = _barsFrontBuffer[bin];
+ if (target > current)
+ _barsBackBuffer[bin] = target;
+ else
+ _barsBackBuffer[bin] = Math.Max(current * ReleaseCoeff, target);
+
+ // Peak hold
+ if (_barsBackBuffer[bin] > _peakHold[bin])
+ _peakHold[bin] = _barsBackBuffer[bin];
+ else
+ _peakHold[bin] *= PeakDecay;
+ }
+
+ System.Threading.Interlocked.Exchange(ref _swapRequested, 1);
+ }
+
+ public void Render(SKCanvas canvas, float width, float height, float scale, string recognizedText = null)
+ {
+ if (_paintBar == null)
+ {
+ _paintBar = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ IsAntialias = false
+ };
+ }
+
+ if (_paintDot == null)
+ {
+ _paintDot = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ IsAntialias = false
+ };
+ }
+
+ if (_paintText == null)
+ {
+ _paintText = new SKPaint
+ {
+ Color = SKColors.Yellow,
+ IsAntialias = true,
+ TextAlign = SKTextAlign.Center
+ };
+ }
+
+ if (_paintBg == null)
+ {
+ _paintBg = new SKPaint
+ {
+ Color = SKColors.Black.WithAlpha(128),
+ Style = SKPaintStyle.Fill
+ };
+ }
+
+ // Swap buffers
+ if (System.Threading.Interlocked.CompareExchange(ref _swapRequested, 0, 1) == 1)
+ {
+ Array.Copy(_barsBackBuffer, _barsFrontBuffer, BarCount);
+ Array.Copy(_peakHold, _peakHoldFront, BarCount);
+ }
+
+ var areaWidth = width * 0.85f;
+ var maxBarHeight = 150 * scale;
+ var startX = (width - areaWidth) / 2;
+ var bottomY = height - 40 * scale;
+ var topY = bottomY - maxBarHeight;
+
+ if (!string.IsNullOrEmpty(recognizedText))
+ {
+ _paintText.TextSize = 32 * scale;
+ canvas.DrawText(recognizedText, width / 2, topY - 20 * scale, _paintText);
+ }
+
+ // Background
+ canvas.DrawRoundRect(
+ startX - 8 * scale, topY - 8 * scale,
+ areaWidth + 16 * scale, maxBarHeight + 16 * scale,
+ 6 * scale, 6 * scale,
+ _paintBg);
+
+ var totalSlot = areaWidth / BarCount;
+ var barWidth = Math.Max(1f, totalSlot * 0.55f);
+
+ // Clip all bar drawing to the background area
+ canvas.Save();
+ canvas.ClipRect(new SKRect(startX, topY, startX + areaWidth, bottomY));
+
+ if (Skin == 0)
+ {
+ // Skin 0: White/gray bars with floating peak dots
+ for (int i = 0; i < BarCount; i++)
+ {
+ var level = _barsFrontBuffer[i];
+ var peakLevel = _peakHoldFront[i];
+ var x = startX + i * totalSlot + (totalSlot - barWidth) / 2;
+
+ // Only draw bars that are actually ON
+ if (level > 0.01f)
+ {
+ var barH = level * maxBarHeight;
+ byte barAlpha = (byte)(140 + Math.Min(115, level * 200));
+ _paintBar.Color = new SKColor(220, 225, 235, barAlpha);
+ canvas.DrawRect(x, bottomY - barH, barWidth, barH, _paintBar);
+ }
+
+ // Floating peak dot
+ if (ShowPeakDots && peakLevel > 0.03f)
+ {
+ var dotY = bottomY - peakLevel * maxBarHeight;
+ var dotH = 2f * scale;
+ _paintDot.Color = SKColors.White;
+ canvas.DrawRect(x, dotY - dotH, barWidth, dotH, _paintDot);
+ }
+ }
+ }
+ else
+ {
+ // Skin 1: Colored bars, hue based on frequency position
+ for (int i = 0; i < BarCount; i++)
+ {
+ var level = _barsFrontBuffer[i];
+ var peakLevel = _peakHoldFront[i];
+ var x = startX + i * totalSlot + (totalSlot - barWidth) / 2;
+
+ if (level > 0.01f)
+ {
+ var barH = level * maxBarHeight;
+ float hue = (i / (float)(BarCount - 1)) * 200 + 160; // blue->cyan->green
+ if (hue >= 360) hue -= 360;
+ _paintBar.Color = SKColor.FromHsv(hue, 80, 100);
+ canvas.DrawRect(x, bottomY - barH, barWidth, barH, _paintBar);
+ }
+
+ if (ShowPeakDots && peakLevel > 0.03f)
+ {
+ var dotY = bottomY - peakLevel * maxBarHeight;
+ var dotH = 2f * scale;
+ _paintDot.Color = SKColors.White;
+ canvas.DrawRect(x, dotY - dotH, barWidth, dotH, _paintDot);
+ }
+ }
+ }
+
+ canvas.Restore();
+ }
+
+ public void Dispose()
+ {
+ _paintBar?.Dispose();
+ _paintBar = null;
+ _paintDot?.Dispose();
+ _paintDot = null;
+ _paintText?.Dispose();
+ _paintText = null;
+ _paintBg?.Dispose();
+ _paintBg = null;
+ }
+ }
+}
diff --git a/src/Maui/Samples/Camera/Visualizers/AudioWaveformBars.cs b/src/Maui/Samples/Camera/Visualizers/AudioWaveformBars.cs
new file mode 100644
index 00000000..a8de17ec
--- /dev/null
+++ b/src/Maui/Samples/Camera/Visualizers/AudioWaveformBars.cs
@@ -0,0 +1,192 @@
+using DrawnUi.Camera;
+
+namespace CameraTests
+{
+ ///
+ /// Scrolling waveform bars visualizer (music player style, like SoundCloud/Spotify).
+ /// Displays many thin vertical bars growing symmetrically from center.
+ /// New audio scrolls in from the right, older bars fade on the left.
+ /// ZERO allocations during render, double-buffered.
+ ///
+ public class AudioWaveformBars : IAudioVisualizer, IDisposable
+ {
+ private const int BarCount = 64;
+
+ private float[] _barsFrontBuffer = new float[BarCount];
+ private float[] _barsBackBuffer = new float[BarCount];
+ private int _swapRequested = 0;
+
+ // Rolling accumulator for incoming samples
+ private int _samplesAccumulated = 0;
+ private float _currentPeak = 0f;
+
+ // How many raw PCM samples per bar (controls scroll speed)
+ // At 44100Hz with ~30fps render, each frame ~1470 samples.
+ // We want roughly 2-4 bars per frame for smooth scrolling.
+ private int _samplesPerBar = 512;
+
+ public bool UseGain { get; set; } = true;
+ public int Skin { get; set; } = 0;
+
+ private SKPaint _paintBar;
+ private SKPaint _paintText;
+ private SKPaint _paintBg;
+
+ public void AddSample(AudioSample sample)
+ {
+ int sampleCount = sample.Data.Length / 2;
+ float gain = UseGain ? 3.5f : 1.0f;
+
+ for (int i = 0; i < sampleCount; i++)
+ {
+ int byteIndex = i * 2;
+ if (byteIndex + 1 >= sample.Data.Length)
+ break;
+
+ short pcm = (short)(sample.Data[byteIndex] | (sample.Data[byteIndex + 1] << 8));
+ float val = Math.Abs(pcm / 32768f) * gain;
+
+ if (val > _currentPeak)
+ _currentPeak = val;
+
+ _samplesAccumulated++;
+
+ if (_samplesAccumulated >= _samplesPerBar)
+ {
+ // Shift all bars left by one
+ Array.Copy(_barsBackBuffer, 1, _barsBackBuffer, 0, BarCount - 1);
+
+ // Push new peak value at the right end
+ _barsBackBuffer[BarCount - 1] = Math.Clamp(_currentPeak, 0f, 1f);
+
+ _currentPeak = 0f;
+ _samplesAccumulated = 0;
+
+ System.Threading.Interlocked.Exchange(ref _swapRequested, 1);
+ }
+ }
+ }
+
+ public void Render(SKCanvas canvas, float width, float height, float scale, string recognizedText = null)
+ {
+ if (_paintBar == null)
+ {
+ _paintBar = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ IsAntialias = false
+ };
+ }
+
+ if (_paintText == null)
+ {
+ _paintText = new SKPaint
+ {
+ Color = SKColors.Yellow,
+ IsAntialias = true,
+ TextAlign = SKTextAlign.Center
+ };
+ }
+
+ if (_paintBg == null)
+ {
+ _paintBg = new SKPaint
+ {
+ Color = SKColors.Black.WithAlpha(128),
+ Style = SKPaintStyle.Fill
+ };
+ }
+
+ // Swap buffers
+ if (System.Threading.Interlocked.CompareExchange(ref _swapRequested, 0, 1) == 1)
+ {
+ var temp = _barsFrontBuffer;
+ _barsFrontBuffer = _barsBackBuffer;
+ _barsBackBuffer = temp;
+ }
+
+ var areaWidth = width * 0.85f;
+ var areaHeight = 120 * scale;
+ var startX = (width - areaWidth) / 2;
+ var centerY = height - 40 * scale - areaHeight / 2;
+
+ if (!string.IsNullOrEmpty(recognizedText))
+ {
+ _paintText.TextSize = 32 * scale;
+ canvas.DrawText(recognizedText, width / 2, centerY - areaHeight / 2 - 20 * scale, _paintText);
+ }
+
+ // Background
+ canvas.DrawRoundRect(
+ startX - 8 * scale,
+ centerY - areaHeight / 2 - 8 * scale,
+ areaWidth + 16 * scale,
+ areaHeight + 16 * scale,
+ 8 * scale, 8 * scale,
+ _paintBg);
+
+ // Bar dimensions
+ var totalBarSlot = areaWidth / BarCount;
+ var barWidth = Math.Max(1f, totalBarSlot * 0.6f);
+ var barGap = totalBarSlot - barWidth;
+ var halfHeight = areaHeight / 2;
+ var minBarHeight = 2f * scale; // Minimum dot size for silence
+
+ if (Skin == 0)
+ {
+ // Skin 0: Symmetrical bars from center, white with opacity fade
+ for (int i = 0; i < BarCount; i++)
+ {
+ var level = _barsFrontBuffer[i];
+ var x = startX + i * totalBarSlot + barGap / 2;
+
+ // Opacity: older bars (left) fade out
+ byte alpha = (byte)(100 + (155f * i / (BarCount - 1)));
+
+ var barH = Math.Max(minBarHeight, level * halfHeight);
+
+ _paintBar.Color = SKColors.White.WithAlpha(alpha);
+
+ // Top half (grows upward from center)
+ canvas.DrawRect(x, centerY - barH, barWidth, barH, _paintBar);
+
+ // Bottom half (mirror, grows downward from center)
+ canvas.DrawRect(x, centerY, barWidth, barH, _paintBar);
+ }
+ }
+ else
+ {
+ // Skin 1: Single-sided bars from bottom, colored gradient
+ var bottomY = centerY + halfHeight;
+
+ for (int i = 0; i < BarCount; i++)
+ {
+ var level = _barsFrontBuffer[i];
+ var x = startX + i * totalBarSlot + barGap / 2;
+
+ byte alpha = (byte)(100 + (155f * i / (BarCount - 1)));
+
+ var barH = Math.Max(minBarHeight, level * areaHeight);
+
+ // Hue shifts from purple (left/old) to cyan (right/new)
+ float hue = 220 + (140f * i / (BarCount - 1)); // 220=blue -> 360=red wrap
+ if (hue >= 360) hue -= 360;
+
+ _paintBar.Color = SKColor.FromHsv(hue, 70, 100).WithAlpha(alpha);
+
+ canvas.DrawRect(x, bottomY - barH, barWidth, barH, _paintBar);
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ _paintBar?.Dispose();
+ _paintBar = null;
+ _paintText?.Dispose();
+ _paintText = null;
+ _paintBg?.Dispose();
+ _paintBg = null;
+ }
+ }
+}
diff --git a/src/Maui/Samples/Sandbox/Game/SpaceShooter.xaml b/src/Maui/Samples/Sandbox/Game/SpaceShooter.xaml
index e4d99542..5cd8ca49 100644
--- a/src/Maui/Samples/Sandbox/Game/SpaceShooter.xaml
+++ b/src/Maui/Samples/Sandbox/Game/SpaceShooter.xaml
@@ -1,11 +1,11 @@
-
-
+