Entwickler-Ecke

Multimedia / Grafik - CSCore: Eingabegerät in ausgewähltes Format streamen?


hydemarie - So 23.04.17 19:36
Titel: CSCore: Eingabegerät in ausgewähltes Format streamen?
Ich brauche mal wieder Starthilfe.

Ich würde zwecks Vertiefung meiner C#-Erfahrungen gern ein Programm schreiben, das einen Audiostream aus einem bestimmten Gerät in einem bestimmten Format (zum Beispiel Vorbis) ausgibt. Dafür verwende ich die CSCore-Bibliothek, die ja der Standard zu sein scheint und außerdem mit deutlich mehr Ausgabeformaten klarzukommen behauptet als NAudio, das ich mir zuvor angesehen hatte.

Nun ist die Dokumentation für CSCore ja so gut wie gar nicht vorhanden, was mich zu der Frage führt:
Wie kann ich einen Stream aus einem Eingabegerät, dessen DeviceID ich kenne, in eine FLAC- oder Vorbisdatei schreiben?

Jeder Ansatz ist willkommen. :)


C# - So 23.04.17 20:34

Hey,

also ohne mich jetzt mit CSCore auszukennen würde ich dir empfehlen mal die Samples im Github Repository [https://github.com/filoe/cscore/tree/master/Samples] anzuschauen. Da kann man sich oft die nötigen snippets rausziehen.

Eine alternative zu CSCore wäre auch BASS.NET [http://bass.radio42.com/index.html]. Das ist ein Wrapper um die native C++ Bibliothek BASS. Ich würde es nicht unbedingt als Einsteigerfreundlich bezeichnen, aber die Lib ist sehr mächtig.

Auf NuGet Must Haves [http://nugetmusthaves.com/Tag/audio] gibt es auch weitere Vorschläge für Audio-Bibliotheken. Accord [http://accord-framework.net/] ist in jedem Fall ein Blick wert (nicht nur im Bereich Audio, sondern auch Imaging, Computer Vision und Machine Learning).

Zur deiner Frage kann ich dir leider keine Antwort geben, aber ich hoffe du findest was in den Samples.


hydemarie - So 23.04.17 20:53

BASS würde ich nach einigen Erfahrungen mit Delphi eigentlich lieber vermeiden wollen. Ist nicht besonders schön zu benutzen, finde ich. :?

Die Samples haben, so weit ich das sehen konnte, eigentlich nur WaveOut und nachträgliches Konvertieren, aber kein Direkt-als-{Format}-Schreiben ...


Delete - So 23.04.17 21:42

- Nachträglich durch die Entwickler-Ecke gelöscht -


hydemarie - So 23.04.17 21:57

CSCore hat ja schon Wrapper, mir ist nur nicht ganz klar, wie ich die benutzen kann. :?


Delete - So 23.04.17 22:01

- Nachträglich durch die Entwickler-Ecke gelöscht -


hydemarie - So 23.04.17 22:12

Verstehe ich "Wrapper" falsch?

https://github.com/filoe/cscore/tree/master/CSCore/Codecs

Vorbis scheint allerdings eine eigene Klasse zu brauchen:
https://github.com/filoe/cscore/tree/master/Samples/NVorbisIntegration

Vielleicht rufe ich hinterher einfach SoX auf ... :?


Delete - So 23.04.17 22:26

- Nachträglich durch die Entwickler-Ecke gelöscht -


C# - So 23.04.17 22:29

Vom schnellen überfliegen des Source Code würde ich schon sagen dass CsCore ein Wrapper ist. Aber um einen Wrapper geht es ja hier gar nicht und gebraucht wird auch keiner (außer dem schon vorhandenen).

Was mir jetzt einfallen würde (ohne Kenntnisse über die API zu haben) ist, eine Aufnahme zu starten (hat die ein Standardformat?) und in einem Memory Stream abzulegen und diesen dann durch ein entsprechendes Codec zu jagen.


hydemarie - So 23.04.17 22:30

CSCore habe ich ja schon eingebunden, ich nutze es aktuell bereits, um die DeviceIDs der angeschlossenen Eingabegeräte auszulesen. Das klappt auch schon ganz gut. Was noch nicht klappt, ist mein Verständnis von der Dateiausgabe ...

Zitat:
und diesen dann durch ein entsprechendes Codec zu jagen.


Eben - und wie?


Delete - So 23.04.17 22:35

- Nachträglich durch die Entwickler-Ecke gelöscht -


C# - So 23.04.17 22:43

@Frühlingsrolle Im Eröffnungspost hat der TE schon gesagt, dass die Doku nicht existent ist...
Nachtrag
Und eine automatisch generierte Doku ohne Beispiele oder Tutorials ist nutzlos. Da kann ich auch direkt die Quellcode Docs lesen
Nachtrag ende

Ich habe gerade die Codecs durchgesehen. Da ich nur Decoder gefunden habe, habe ich nochmal auf der Codeplex [https://cscore.codeplex.com/] Seite geguckt und unten die Tabelle gefunden:
CSCore

Wie du siehst wird für FLAC und OGG nur ein Decoder unterstützt.


Delete - So 23.04.17 22:52

- Nachträglich durch die Entwickler-Ecke gelöscht -


C# - So 23.04.17 22:56

Ja es ist kein Beispiel dabei weil es nicht möglich ist. Sie will Streams als Ogg oder FLAC speichern. Die Grafik sagt, dass für beide Codecs nur ein Decoder implementiert ist. Da sie zum Speichern aber ein Encoder braucht ist ihr Vorhaben nicht mit vorhandenen Mitteln möglich.


Delete - So 23.04.17 23:01

- Nachträglich durch die Entwickler-Ecke gelöscht -


C# - So 23.04.17 23:06

Erster Satz der Readme.md der GitHub [https://github.com/ioctlLR/NVorbis] Page:
Zitat:
NVorbis is a .Net library for decoding Xiph.org Vorbis files. It is designed to run in partial trust environments and does not require P/Invoke or unsafe code.

Wieder kein Encoder.


Delete - So 23.04.17 23:09

- Nachträglich durch die Entwickler-Ecke gelöscht -


C# - So 23.04.17 23:14

NAudio unterstützt noch weniger Codecs als CsCore. Da wird weder De- noch Encoder für Vorbis oder FLAC unterstützt (zumindest steht es so in der Readme.md und eine Klasse dazu konnte ich auch nicht finden).


hydemarie - So 23.04.17 23:21

Für NAudio gibt's Beispiele auf Stack Overflow, aber die Formatauswahl trifft meine Vorstellungen nicht. :?
Offenbar war meine Vermutung, ich müsste einen zusätzlichen Encoder hinter den eigentlichen Streamingvorgang hängen, nicht ganz falsch. Hm.


Delete - So 23.04.17 23:23

- Nachträglich durch die Entwickler-Ecke gelöscht -


C# - So 23.04.17 23:26

Ich habe auf die schnelle FlacBox [https://flacbox.codeplex.com/] gefunden. Damit kannst du FLAC lesen und schreiben.

@Frühlingsrolle: es geht hier nicht um Decoder, sondern Encoder. Einen Decoder bietet CSCore auch an. Aber zum schreiben braucht man einen Encoder.

Nachtrag
Vom Überfliegen des FlacBox Codes könntest du deine Aufnahme so speichern:

Samples -> WaveEncoder (CSCore) -> MemoryStream -> WaveOverFlacStream (FlacBox) -> Datei


Delete - So 23.04.17 23:33

- Nachträglich durch die Entwickler-Ecke gelöscht -


hydemarie - So 23.04.17 23:46

FlacBox sieht mir nach einem toten Projekt aus. :(

Das ist alles ein ziemliches Gefrickel, wie mir scheint. CSCore selbst hätte Unterstützung für FFMPEG, aber nach Überfliegen des Quellcodes sieht das auch nur nach einem Decoder aus. Allmählich habe ich das Gefühl, das ist wieder mal so ein Anwendungsfall, den vor mir noch keiner hatte (oder lieber in C oder C++ umgesetzt hat)... :?


Delete - So 23.04.17 23:53

- Nachträglich durch die Entwickler-Ecke gelöscht -


hydemarie - So 23.04.17 23:54

Haha, nein, keine Sorge - ich habe durchaus schon Gründe, zur Abwechslung mal was in C# zu machen. :)
Schlaf gut!


C# - So 23.04.17 23:55

Ja FlacBox ist tot, aber scheint ja zu funktionieren.

Dein Anwendungsfall ist auch etwas ungewöhnlich :D. Warum muss es denn Ogg oder FLAC sein? Das Ding ist halt, dass für die meisten Anwendungsfälle Wave und MP3 völlig ausreichen.
Mehr Codecs (Edit: bzw. Encoder) braucht man idR nur, wenn man DSP Programme schreibt. Und da ist C++ (oder jede andere unmanaged Sprache) einfach besser weil sie deutlich flexibler (und schneller) auf dem Speicher operieren kann.


hydemarie - Mo 24.04.17 00:04

user profile iconC# hat folgendes geschrieben Zum zitierten Posting springen:
Warum muss es denn Ogg oder FLAC sein?


Das sind tatsächlich die Audioformate, die ich am häufigsten verwende. Der MP3-Codec neigt nicht gerade zu filigranen Tönen. Bislang benutze ich dafür XRECODE, aber ich mache mir das Leben ja gerne einfacher.

user profile iconC# hat folgendes geschrieben Zum zitierten Posting springen:
Das Ding ist halt, dass für die meisten Anwendungsfälle Wave und MP3 völlig ausreichen.


Wie gesagt: Ich kann auch einfach einen WaveOutStream (?) nehmen und einen externen Konverter dahinterklemmen, das ist dann halt nur hässlich und irgendwie unpraktisch, finde ich. Aber wenn es nicht geht, dann geht es nicht. Damit muss ich dann klarkommen.

user profile iconC# hat folgendes geschrieben Zum zitierten Posting springen:
Und da ist C++ (oder jede andere unmanaged Sprache) einfach besser weil sie deutlich flexibler (und schneller) auf dem Speicher operieren kann.


Der kritische Teil hierbei dürfte das Auslesen vom Gerät sein, die Ausgabe in eine Datei kann ja durchaus mit beliebiger Verzögerung passieren. Ob das in der Praxis eine Rolle spielt, weiß ich nicht. Ich würde annehmen, dass die Core-Audio-Anbindung von CSCore halbwegs alltagstauglich (unmanaged?) ist. Ab welcher Eingabebitrate wird das eigentlich zu einem Problem?
Nachtrag: Ja, da scheint einiges "unmanaged" [https://github.com/filoe/cscore/blob/master/CSCore/SoundIn/WasapiCapture.cs] zu sein.


C# - Mo 24.04.17 05:17

Also die CoreAudioAPI von CSCore ist ja einfach ein Wrapper zur WinApi bzw. WASAPI. Das Problem in C# ist, dass Arrays oft unnötig kopiert werden (z.B. beim Marshaling) und sehr viele Validierungen ablaufen. Wenn du z.B. auf ein Array zugreifst wird bei jedem Index geprüft, ob er in den Grenzen des Arrays liegt. Bist du außerhalb dieser Grenzen wird eine Exception geworfen. Bei C++ hingegen kannst du zugreifen wie du lustig bist. Wenn du den Index falsch berechnest stört sich C++ da nicht dran. Später bekommst du dann halt ne Heap Corruption oder ein Segmentation Fault.
Ein anderes Beispiel wäre auch das reinterpretieren des Speichers. Wenn du z.B. eine Funktion Record(byte[] buffer) hast, musst du bei C# auch ein Byte-Array übergeben. Wenn du aber weißt, dass dein Format z.B. float ist, muss das Byte-Array in ein Float-Array konvertiert werden, was bei C# idR dazu führt, dass das Array kopiert wird (man könnte natürlich auch mittels unsafe einen Float-Pointer drüber legen, aber das widerstrebt den Prinzipien von C#). In C++ kannst du einfach ein char-Pointer übergeben und mit reinterpret_cast<float*>(buffer) bekommst du dann dein Float-Pointer ohne irgendwas kopieren zu müssen.

Bei welchen Bitraten das Problematisch wird, hängt stark von deinem System ab:
- CPU Leistung
- RAM Geschwindigkeit
- HDD / SSD
- Soundkarte (unterstützte Sample Rates)

Üblich ist eine Sample Rate von 44.1kHz bzw 48kHz. Bei einer Stereoaufnahme mit 16 Bit kommst du schon auf eine Bitrate von 44.1 * 2 * 16 = 1411,2 kb/s (CD-Standard). Bei Studioaufnahmen mit 192kHz, Stereo und 24 Bit bist du dann sogar bei 192 * 2 * 24 = 9216 kb/s.

Angenommen du machst eine Aufnahme mit Standard CD Qualität, dann bekommst du alle 1/(44.1kHz)\approx 22µs zwei 16 Bit Werte (je einen für links und rechts bei Stereoaufnahmen). Das sollte C# noch locker verkraften.
Sehen wir uns nun die Studioqualität an: alle 1/(192kHz) \approx 5µs bekommst du zwei 24 Bit Werte.
Da Events und Methodenaufrufe auch einen ordentlichen Overhead verursachen, werden die Samples in den meisten Fällen intern gecached, sodass du z.B. alle 10ms ein 1.8kB (CD) bzw. 12kB (Studio) Array (kB = Kilobyte) bekommst, indem alle Samples der letzten 10ms abgelegt sind. Dieses Array muss nun reinterpretiert werden (z.B. uintushort oder float, damit die Codecs entsprechende Komprimierungen anwenden können (es sei denn du benutzt Wave, dann kannst du dein Byte-Array direkt in den Stream schieben).


C# - Mo 24.04.17 06:00

Ich habe mal ein kleines Benchmark mit C# gemacht und bin zu einem erstaunlichen Ergebnis gekommen.
In dem Benchmark werden zufällige Samples als Byte-Array generiert. Anschließend findet eine Kovertierung zu einem Short-Array (Int16) statt und zuletzt wird noch eine kleine Funktion ausgeführt die über das Array iteriert und ein paar arithmetische Operationen durchführt (DoSomething(...) im Test Code).

Das Benchmark bietet folgende Parameter:
- SampleRate: die Sample Rate der fiktiven Aufnahme in Hz (z.B. 44100)
- ChannelCount: die Anzahl der Kanäle (1 für Mono, 2 für Stereo, 5+ für Sorround Sound)
- UpdateInterval: die Zeitspanne in der die Samples von dem Aufnahmegerät geliefert werden (z.B. alle 10ms kommt ein ganzes Paket von Samples)
- Duration: die Gesamtdauer eines Tests für eine einzelnes Konvertierungsverfahren

Verglichen werden drei Verfahren:
1. BitConverter: Jedes Sample wird einzeln in den BitConverter geworfen (siehe ConvertUsingBitConverter(...) im Test Code)
2. Marshaling: Es wird ein short-Array erzeugt, dann wird das byte-Array gepinnt und ein mittels Marshaling kopiert (siehe ConvertUsingMarshaling(...) im Test Code)
3. Unsafe Code: Das byte-Array wird gepinnt und ein Pointer darauf wird erstellt. Dieser wird an eine Wrapper-Klasse übergeben über diese dann auf dem Original-Array operiert werden kann (siehe ConvertUsingUnsafeCode(...) im Test Code)

Ich habe Tests für 44.1kHz und 192kHz gemacht (leider kann ich hier nur 44.1kHz zeigen, weil ein Beitrag maximal drei Anhänge haben kann :motz:)

mean44100 median44100 performance44100

Getestet wurde im Debug und Release Mode sowohl mit, als auch ohne Debugger. Das erste Diagramm zeigt die durchschnittliche Laufzeit für eine Konvertierung + DoSomething(...). Das zweite Diagramm zeigt den Median davon und das Dritte zeigt die "Over performance". Damit ist das Zeitverhältnis zwischen einem Update Interval und einer tatsächlichen Iteration der Testfunktion gemeint. Wenn z.B. ein Update Intervall von 10ms festgelegt ist und die Konvertierung schon nach 10µs abgeschlossen ist ergibt sich daraus eine "Over performance" von 10ms / 10µs = 1.000 = 100.000%. Würde die Konvertierung 10ms dauern, läge die "Over performance" bei 1,0 = 100%.

Hier ist der verwendete Code:

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
149:
150:
151:
152:
153:
154:
155:
156:
157:
158:
159:
160:
161:
162:
163:
164:
165:
166:
167:
168:
169:
170:
171:
172:
173:
174:
175:
176:
177:
178:
179:
180:
181:
182:
183:
184:
185:
186:
187:
188:
189:
190:
191:
192:
193:
194:
195:
196:
197:
198:
199:
200:
201:
202:
203:
204:
205:
206:
207:
208:
209:
210:
211:
212:
213:
214:
215:
216:
217:
218:
219:
220:
221:
222:
223:
224:
225:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace ConsolePlayground
{
    unsafe delegate void UnsafeOperation(short* samples, int size);

    class Program
    {
        // depth is fixed 16 bit (short)
        private const int SampleRate = 44100;

        private const int ChannelCount = 2;
        private static readonly TimeSpan UpdateInterval = TimeSpan.FromMilliseconds(10);

        private static readonly TimeSpan Duration = TimeSpan.FromSeconds(10);


        private static readonly Random Random = new Random();

        static void Main(string[] args)
        {
            Trace.Listeners.Clear();

            var twtl = new TextWriterTraceListener("../../Data/Log.txt")
            {
                Name = "TextLogger",
            };

            var ctl = new ConsoleTraceListener(false);

            Trace.Listeners.Add(twtl);
            Trace.Listeners.Add(ctl);
            Trace.AutoFlush = true;
            
            Task.Run(Run).Wait();
        }

        static async Task Run()
        {
            Trace.WriteLine($"================== Benchmark Begin ==================");
#if DEBUG
            Trace.WriteLine($"Mode:            {(Environment.Is64BitProcess ? "x64" : "x32")} Debug {(Debugger.IsAttached ? "+ Debugger" : "")}");
#else
            Trace.WriteLine($"Mode:            {(Environment.Is64BitProcess ? "x64" : "x32")} Release {(Debugger.IsAttached ? "+ Debugger" : "")}");
#endif

            Trace.WriteLine($"Sample Rate:     {SampleRate}Hz");
            Trace.WriteLine($"Channel Count:   {ChannelCount}");
            Trace.WriteLine($"Update Interval: {UpdateInterval.TotalMilliseconds}ms");
            Trace.WriteLine("");

            var sampleCount = (int) (UpdateInterval.TotalSeconds * SampleRate * sizeof(short) * ChannelCount);


            Trace.WriteLine($"Testing \"{nameof(ConvertUsingBitConverter)}\" for {Duration.TotalSeconds:f3}s...");

            var generator = GenerateSamples(sampleCount);
            var totalWatch = Stopwatch.StartNew();
            var watch = new Stopwatch();
            var records = new List<TimeSpan>((int) (Duration.Ticks / UpdateInterval.Ticks));

            while (totalWatch.Elapsed < Duration)
            {
                watch.Restart();
                var buffer = await generator;
                generator = GenerateSamples(sampleCount);

                var samples = ConvertUsingBitConverter(buffer);
                DoSomething(samples);
                watch.Stop();

                records.Add(watch.Elapsed);
            }

            PrintResults(totalWatch.Elapsed, records);


            Trace.WriteLine($"Testing \"{nameof(ConvertUsingMarshaling)}\" for {Duration.TotalSeconds:f3}s...");

            generator = GenerateSamples(sampleCount);
            totalWatch = Stopwatch.StartNew();
            records.Clear();

            while (totalWatch.Elapsed < Duration)
            {
                watch.Restart();
                var buffer = await generator;
                generator = GenerateSamples(sampleCount);

                var samples = ConvertUsingMarshaling(buffer);
                DoSomething(samples);
                watch.Stop();

                records.Add(watch.Elapsed);
            }

            PrintResults(totalWatch.Elapsed, records);


            Trace.WriteLine($"Testing \"{nameof(ConvertUsingUnsafeCode)}\" for {Duration.TotalSeconds:f3}s...");

            generator = GenerateSamples(sampleCount);
            totalWatch = Stopwatch.StartNew();
            records.Clear();


            while (totalWatch.Elapsed < Duration)
            {
                watch.Restart();
                var buffer = await generator;
                generator = GenerateSamples(sampleCount);

                unsafe
                {
                    ConvertUsingUnsafeCode(buffer, DoSomething);
                }

                watch.Stop();

                records.Add(watch.Elapsed);
            }

            PrintResults(totalWatch.Elapsed, records);

            Trace.WriteLine($"================== Benchmark End ==================");
            Trace.WriteLine("");

            if (Debugger.IsAttached)
            {
                Console.WriteLine("Press any key to exit...");
                Console.ReadKey(false);
            }
        }

        static void PrintResults(TimeSpan totalTime, List<TimeSpan> records)
        {
            Trace.WriteLine($"Actual Duration:  {totalTime.TotalMilliseconds,6:#}ms");
            Trace.WriteLine($"Max allowed time: {UpdateInterval.TotalMilliseconds * 1000,6:#}µs");
            Trace.WriteLine($"Mean time:        {records.Average(r => r.TotalMilliseconds) * 1000,6:#}µs");
            Trace.WriteLine($"Median time:      {records.OrderBy(r => r).ElementAt(records.Count / 2).TotalMilliseconds * 1000,6:#}µs");
            Trace.WriteLine(
                $"Min time:         {records.Min().TotalMilliseconds * 1000,6:#}µs at iteration {records.IndexOf(records.Min())}");
            Trace.WriteLine(
                $"Max time:         {records.Max().TotalMilliseconds * 1000,6:#}µs at iteration {records.IndexOf(records.Max())}");
            Trace.WriteLine($"Iterations:       {records.Count,6}");
            Trace.WriteLine($"Over performance: {records.Count / (Duration.Ticks / UpdateInterval.Ticks) * 100,6:#}%");
            Trace.WriteLine("");
        }

        static void DoSomething(short[] samples)
        {
            unchecked
            {
                for (int i = 1; i < samples.Length - 1; i++)
                {
                    samples[i] = (short) (samples[i - 1] ^ samples[i + 1]);
                    samples[i] *= (short) (samples[i] + 1220);
                    samples[i] <<= i % 4;
                    samples[i] >>= i % 3;
                }
            }
        }

        static unsafe void DoSomething(short* samples, int size)
        {
            unchecked
            {
                for (int i = 1; i < size - 1; i++)
                {
                    samples[i] = (short) (samples[i - 1] ^ samples[i + 1]);
                    samples[i] *= (short) (samples[i] + 1220);
                    samples[i] <<= i % 4;
                    samples[i] >>= i % 3;
                }
            }
        }

        static async Task<byte[]> GenerateSamples(int count)
        {
            return await Task.Run(() =>
            {
                var buffer = new byte[count];
                Random.NextBytes(buffer);
                return buffer;
            });
        }

        static short[] ConvertUsingBitConverter(byte[] buffer)
        {
            var result = new short[buffer.Length / sizeof(short)];

            for (int i = 0; i < result.Length; i++)
                result[i] = BitConverter.ToInt16(buffer, i * sizeof(short));

            return result;
        }

        static short[] ConvertUsingMarshaling(byte[] buffer)
        {
            var result = new short[buffer.Length / sizeof(short)];

            var pinnedArray = GCHandle.Alloc(buffer, GCHandleType.Pinned);
            var pointer = pinnedArray.AddrOfPinnedObject();
            Marshal.Copy(pointer, result, 0, result.Length);
            pinnedArray.Free();

            return result;
        }
        
        static unsafe void ConvertUsingUnsafeCode(byte[] buffer, UnsafeOperation operation)
        {
            fixed (byte* ptr = buffer)
            {
                operation((short*) ptr, buffer.Length / sizeof(short));
            }
        }
    }
}


Update

Ich habe den Code leicht angepasst, um die Wrapper-Klasse für den unsafe Part zu eliminieren. Die Diagramme enthalten die neuen Ergebnisse des Benchmarks.


hydemarie - Mo 24.04.17 09:12

Weia, da hast du dir jetzt Arbeit gemacht. Danke!

Das heißt, dieser besonders fürsorgliche Umgang mit Datentypen ist performanter (typo korrigiert) als Zeigerschieben? Liegt das an C# oder an den Zeigern?
Ich rechne als Eingabe ungefähr mit einem Plattenspieler oder einem Mikrofon, das sollte also bei Weitem langen... :)


C# - Mo 24.04.17 14:46

Kein Problem. Mich haben die Verhältnisse sowieso interessiert. Ich habe den Test Code etwas abgeändert und die neuen Diagramme hochgeladen. Sieht jetzt schon besser aus für den unsafe Part.

Ich vermute das der Flaschenhals bei den unmanaged-Operationen am pinnen liegt. Da .NET managed ist, werden auch mal Objekte im Speicher verschoben um die Speicherzugriffe zu optimieren. Sobald du jedoch ein Pointer verwendest, darf .NET das entsprechende Objekt nicht mehr anrühren, weil du ja über die Adresse darauf zugreifen willst. Ich vermute, dass genau dieser Teil relativ viel Performance kostet.

Was dein Plattenspieler angeht: da solltest du fein raus sein :D. Doch lass dich von dem Benchmark nicht täuschen. In der Realität wird es wahrscheinlich passieren, dass du mehr als einmal über die Samples iterieren musst. Außerdem wird der Callstack in den meisten fällen auch deutlich größer. Da kann sich das Komma bei den Werten schnell mal um ein paar Stellen verschieben.

Ich habe die Sample Rate mal um den Faktor 1000 erhöht (auf 44.100.000Hz). Die Werte sind zwar unrealistisch, aber man sieht wie unsafe mit sehr großen Arrays operiert.
performance

Die Over Performance liegt nun bei unter 100% beim managed Code, was bedeutet, dass hier die Samples schneller kommen als sie verarbeitet werden können. Der unsafe Part hat hingegen noch etwas Luft und ist jetzt fast doppelt so schnell wie die beiden managed Methoden.


hydemarie - Mo 24.04.17 14:50

Gibt es überhaupt Audioeingabegeräte, die realistisch mit 44 MHz laufen? :shock:


C# - Mo 24.04.17 14:59

Nein :D. Wie gesagt, dass war nur um das byte-Array mal exorbitant groß zu machen um managed vs unmanaged auf großen Arrays zu vergleichen (in diesem Fall ca. 1.7MB).


Delete - Mo 24.04.17 15:01

- Nachträglich durch die Entwickler-Ecke gelöscht -


C# - Mo 24.04.17 15:11

:nixweiss: ganz irrelevant ist es aber auch nicht. Geht ja um die Performance der Sample Konvertierung. Die ursprüngliche Frage ist ja schon lange geklärt.


hydemarie - Mo 24.04.17 15:13

Ich finde es für mein Problem eigentlich auch ganz hilfreich bis jetzt.