Klassen mit 1.000.000 Zeichen langen Namen

Klassen mit 1.000.000 Zeichen langen Namen – willkommen zu den grausamsten Höllenqualen, die ich meinem Compiler je angetan hab.

Funktionale Programmierung ist heutzutage (mehr oder weniger) der heißeste Scheiß überhaupt. Es geht hierbei, elkatant vereinfacht und sicherlich korrekturbedürftig, um die deklarative Beschreibung eines Problems ohne einen sequentiellen (imperativen) Lösungsansatz. Kurz gesagt: Statt dass ihr dem Computer einzelne, nacheinander abzuarbeitende Schritte vorgebt, gebt ihr ihm eine Beschreibung des Problems an sich (in Form einer Funktion im mathematischen Sinne) und lasst ihn intern einen imperativen Lösungsweg ermitteln. Mehrere solcher Funktionen lassen sich kombinieren und werden letztendlich als ein „gemeinsames Ganzes“ ausgeführt, ohne Zwischenergebnisse die man speichern oder bearbeiten müsste.

Einfach(st)es Beispiel. Ich brauche, warum auch immer, eine Liste der Zahlen von 0 bis n. Heißt in imperativen Sprachen Speicher reservieren, von 0 bis n hochzählen, Ergebnis weiterverarbeiten. In einer funktionalen Programmiersprache wie Haskell hätte man das Problem eher so gelöst:

zahlen_ab x = x : zahlen_ab (x + 1)
zahlen_bis n = take n (zahlen_ab 0)

Wer bei dieser Syntax nicht schon die Fackel angezündet hat, wird erkennen, was hier passiert: zahlen_ab ist nichts anderes als eine mathematische Funktion, welche beschreibt, was wir wirklich wollen: Eine Liste von Zahlen, bei der jedes Element um 1 größer ist als das Vorgängerelement. Erst wenn wir unsere Funktion benutzen, geben wir an, wie viel wir von der Liste wirklich brauchen. Disclaimer: Ja, ich weiß, total unnötig, [0..n] hätte es auch getan.

Das Konzept, Daten on-demand zu generieren, hat in Form von Generatoren inzwischen auch Einzug in imperative Sprachen gefunden. Ein Beispiel ist die yield-Funktion in C#.

Jetzt meine Idee: Kann man Konzepte einer funktionalen Programmiersprache in C++-Metaprogrammierung abbilden, und wenn ja, wie schlagen sie sich im Vergleich?

Statt dass wir mit Variablen rechnen, rechnen wir also mit Templates. Als Pattern Matching Ersatz benutzen wir Template-Spezialisierungen. Das Ziel: Ein Binärbaum ohne Laufzeit-Speicherverbrauch. Er soll eine Höhe von 10 haben und vollständig sein, also 1023 Elemente enthalten.

Wenig überraschend ist, dass sich die Implementation in Haskell doch sehr in Grenzen hält. Folgende Struktur möchte ich also in C++ „nachbilden“:

data Tree a = Nix | Was a (Tree a) (Tree a)

maketree :: Int -> Tree Int
maketree 0 = Nix
maketree n = Was n (maketree (n - 1)) (maketree (n - 1))

testtree = maketree 10

treestr :: Tree Int -> String
treestr Nix = "nix"
treestr (Was val left right) = "(" ++ treestr left ++ "|" ++ show val ++ "|" ++ treestr right ++ ")"

maketree ist hier unser „Baum-Generator“. Er beschreibt einfach einen vollständigen Binärbaum, bei dem jeder Knoten als Wert den Abstand zu seiner Wurzel hat. testtree erzeugt dann einen Baum mit Höhe 10. treestr erzeugt aus einem beliebigen Baum einen String.

Problem 1: Tree ist etwas, das entweder Nix sein kann, oder Was inklusive einem Wert und zwei weiteren Trees. C++ hat keine Enums, in die man Zusatzwerte packen kann. Genau so wenig können wir eine struct auf sich selber referenzieren, so viel Speicher haben wir nicht. Was wir aber machen können, ist den Wert und die linken und rechten Kinder als Template zu definieren:

struct Nix {};

template<int val, typename left, typename right>
struct Was {};

Statt unendlich viel Speicherplatz belegen die Structs jetzt überhaupt keinen Speicher mehr (Instanzen leerer Klassen belegen aus technischen Gründen ein Byte, allerdings werden wir von den Structs keine Instanzen erzeugen).

Problem 2: maketree ist rekursiv, wie sollen wir das abbilden, ohne auf C++-Funktionen zurückzugreifen? Glücklicherweise erlaubt uns der Compiler Rekursion in Templates, sodass wir tatsächlich sehr ähnlich vorgehen können:

template<int n>
struct maketree
{
	using type = Was<n, typename maketree<n - 1>::type, typename maketree<n - 1>::type>;
};

Rufen wir jetzt maketree<10>::type auf, bekommen wir einen Kompilierfehler: Das ganze ist unendlich. Abhilfe schafft „Pattern Matching“ mittels einer zweiten, spezialisierten Version von maketree, außerdem definieren wir uns wieder testtree als Abkürzung auf maketree<10>::type:

template<>
struct maketree<0>
{
	using type = Nix;
};

using testtree = maketree<10>::type;

Und das war’s. Irgendein Teil von clang++ ist gerade gestorben. Würde man den Klassennamen ausschreiben, der von testtree definiert wird, wäre dieser fast eine Million Zeichen lang. Viel wichtiger aber: Es funktioniert.

Die Implementation von treestr erfolgt ähnlich wie maketree, mit dem Nachteil dass Strings für C++ leider nicht konstant genug sind und deshalb nicht schön templatebasiert konkateniert werden können. Wäre dies der Fall, würde uns der Compiler das gesamte Programm auf eine Instruktion herunteroptimieren, nämlich die, die das Ergebnis ausgibt. Hier muss leider ausreichen, dass wir den Baum rekursiv direkt auf cout schreiben. Dafür müssen wir zusätzlich die Was-struct leicht aufbohren, damit wir den Wert und die Kinder extrahieren können.

template<int val, typename left, typename right>
struct Was
{
	using left_type = left;
	using right_type = right;

	static const int value = val;
};

template<typename tree>
struct treestr
{
	static void print()
	{
		std::cout << "(";
		treestr<typename tree::left_type>::print();
		std::cout << "|" << tree::value << "|";
		treestr<typename tree::right_type>::print();
		std::cout << ")";
	}
};
template<>
struct treestr<Nix>
{
	static void print()
	{
		std::cout << "nix";
	}
};

Für den const int in Was sollte übrigens kein zusätzlicher Speicher verbraucht werden, da er als Kompilierzeit-Konstante angesehen werden kann. Bockige Compiler kann man per constexpr zu ihrem Glück zwingen.

Vollständiger Code

Das Ergebnis: Wir haben das Problem definiert und wissen, dass testtree die Lösung enthält; die gesamte Datenstruktur wird direkt bei der Kompilierung erzeugt. Das kann man so weit auf die Spitze treiben, dass der Compiler einem die Fibonacci-Folge ausrechnet.

Fazit: Ja, es geht. Ja, es sieht scheiße aus. Vorteil der C++-Lösung: Sie ist wesentlich schneller. Dies liegt aber daran, dass die eigentliche Arbeit bereits während der Kompilierung getan wurde, während Haskell erst noch rechnen muss. Das ist auch gleichzeitig der Nachteil daran: Natürlich können wir das ganze in C++ nur mit konstanten Ausdrücken benutzen. Haskell ist das relativ wurscht.

Wenn man aber wirklich mal solche Konstrukte (sie dürfen auch ruhig etwas weniger makaber dem Compiler gegenüber sein) benötigt, dann sind C++-Templates ein Segen. Die Standard-Library scheint gerade auch in diese Richtung zu expandieren und Boost ist sowieso das Paradebeispiel für Template-Programmierung.

Kleiner zusätzlicher Vorteil: Jedem Disassembler, der C++ Symbole anzeigt, wird von dem obigen Code schwindlig, denn der sieht solche Klassen auch nicht alle Tage. Wenn ihr mal wen ärgern wollt, baut das ein. Das „Opfer“ kann sich dann entscheiden, die Symbole vorher aus der Binary zu strippen und so jede Lesbarkeit zu verlieren, oder stundenlang zu warten bis der Disassembler die Symboltabelle zusammengekratzt hat.

Zum Abschluss ein Beispiel, an dem man sieht, dass so etwas tatsächlich nützlich sein kann. In einem kleinen Tool von mir, welches ein einfacher C++-Wrapper über OpenGL ist, findet sich folgender Code zum Definieren von GLSL-Programmen (welche eine Pipeline an GLSL-Shadern darstellen):

template<typename... Shaders>
struct ProgramShaderHelper;

template<typename First, typename... More>
struct ProgramShaderHelper<First, More...>
{
	static void attach(GLuint program)
	{
		glAttachShader(program, First().compile());
		ProgramShaderHelper<More...>::attach(program);
	}
};

template<>
struct ProgramShaderHelper<>
{
	static void attach(GLuint program) { }
};

template<typename... Shaders>
struct Program
{
	Program()
	{
		//...
		ProgramShaderHelper<Shaders...>::attach(prg);
		//...
	}

	static void use()
	{
		static auto program = Program<Shaders...>();
		glUseProgram(program.prg);
	}

private:
	GLuint prg;
};

namespace Programs
{
	using TerrainProgram = Program<Shaders::TerrainVertexShader, Shaders::TerrainFragmentShader>;
	using BlurProgram = Program<Shaders::BlurComputeShader>;
}

Hiermit bin ich in der Lage, das TerrainProgram durch einen Aufruf von Programs::TerrainProgram::use() zu aktivieren. Brauche ich ein neues Programm, muss ich lediglich eine Zeile im Programs-Namespace hinzufügen. Mittels variadischem Template kann ich eine beliebig große Anzahl von Shadern definieren, welche mittels ProgramShaderHelper nacheinander initialisiert werden. Ich habe dafür kein Array gebraucht welches Speicherplatz der Laufzeit in Anspruch nimmt. Letztendlich wurde, bis auf ein paar kleine „Gedächtnisstützen“, das Problem wieder nur beschrieben – die nötigen Klassen hat sich der Compiler selber gebaut und benutzt.

Und damit willkommen zurück auf meinem Blog.

Advertisements

Der Spam, der keiner war

Guck ma, ich weiß mein WordPress Passwort noch!

Unter vielen Freunden bin ich bekannt als jemand, der viel Spam bekommt. Dabei habe ich mir das gar nicht ausgesucht! Aber nach meinem Fauxpas vor drei Jahren war die Adresse, die ich heute immer noch benutze, nun mal infiziert. Dazu kommt, was ich nebenbei gesagt extrem unverschämt finde, dass auch meine quasi-öffentliche Support-Adresse für allerlei Open-Source-iges inzwischen betroffen ist.

Ich sehe das Ganze ziemlich gelassen und sammel die sogar, unter dem Vorwand, dass ich die besten Betreffszeilen irgendwann mal hier verbloggen will. Das ist schon fast Routine, Thunderbird markiert den Spam, ich sortier ihn ein. Ich will das nicht automatisch machen lassen aus Sorge, dass ich etwas wichtiges übersehen könnte.

Gut, dass ich den Spam heute nicht übersehen habe. Ich bekomme also eine merkwürdige Mail auf die besagte Open Source Adresse. Jemand bittet mich um Support für ein Programm, von dem ich noch nie etwas gehört habe. Das Deutsch war zu gut für eine dieser schlecht übersetzten Spam-Mails aus Lampukistan, selbst der Source war komplett unauffällig – die Mail war „menschlich“. Das Programm, das der Autor erwähnte, gab es allerdings gar nicht…

Ohne lange zu überlegen bat ich also höflich um mehr Information. Und tatsächlich, ich bekam eine Antwort! Diese war allerdings noch verwirrender: Man gehe in die Einstellungen auf dem iPad, dann auf besagtes Programm und unten in der Liste stünde „ich“ als Support-Adresse.

Wieso werde ich in einer wildfremden iOS-App als Support aufgelistet?! Jetzt wollte ich wirklich mehr über die App wissen und fing diesmal an, den App Store zu durchsuchen (die originale Mail hatte iOS gar nicht erwähnt). Wieder nichts. Mit verschiedenen Suchbegriffen klapperte ich das Netz ab bis ich irgendwann auf ein Forum stieß, durch das sich schließlich feststellen ließ, dass der App-Name falsch geschrieben war – die App war in Wirklichkeit Filterstorm Neue.

Beim Lesen der App-Beschreibung fiel es mir wie Schuppen von den Augen. Na, errät’s einer? Unter „Andere Features“ steht dort aufgelistet: „Export über FTP“.

Und hier ist der Screenshot, der mich zum Supporter von Filterstorm macht:

image1Meine verdammten Kontaktinformationen stehen in der Lizenz für FTPManager. Lizenztechnisch ist es nicht notwendig, diese tatsächlich in ein Programm einzubauen (lediglich der Copyright-Hinweis und die folgenden zwei Absätze sind erforderlich), aber klar, ich hätte sie auch mitgenommen, better safe than sorry. Was ich mir damals dabei gedacht habe, ist mir schleierhaft.

Unglücklich ist hierbei, dass FTPManager als letzte Komponente aufgeführt wird und ich zwischen Lizenz und Kontaktinfos auch noch zwei Leerzeilen stehen habe. Somit ist es letztendlich wie immer – alle Beteiligten haben sich komplett korrekt und nachvollziehbar verhalten, ich habe wieder aus eigener Dummheit lernen können.

Vielen Dank an Herrn S. für das kleine aber sehr amüsante Rätsel. Ich habe eine Paid-App gefunden, die meinen Source Code verwendet, was auch mal nett ist. Und ja, natürlich habe ich sie gekauft und ausprobiert, der FTP-Upload funktioniert einwandfrei. Fazit:

Spam, der keiner war, hat mich letztendlich zum Kauf einer 3,49€-App verleitet.

Statistiken

Jetzt wo Ferien Countdown schon eine Zeit lang eingestampft ist und zu Beginn der Sommerferien die alte Version 2.0 ausläuft (2.1 läuft wie versprochen mindestens bis Sommer 2014), bleibt mir eigentlich nur noch, die bisherigen Serverstatistiken auszuwerten und in nette Diagramme zu verwandeln. In 2.1 habe ich den Update-Mechanismus grundlegend geändert (das ist auch der Grund warum die älteren Versionen die neuen Datenbanken nicht mehr bekommen) und zu diesem Anlass gleich serverseitig ein kleines Statistikmodul geschrieben, welches ganz anonym die Update-Anfragen zählt.

Gestern habe ich mir dann in C ein Auswertungs-Programm gebaut. Ganz nebenbei: Ich habe übrigens ein neues Git-Repo, in das ich ab und zu kleinere C-Tools hochlade die ich ganz nützlich finde.

Zurück zu den Statistiken:

Bildschirmfoto 2013-07-20 um 20.23.59

Eigentlich uninteressant, denn bis auf meine „Belastungstests“ am Anfang, den Release der 2.1 und einigen kleinen oder größeren Serverpannen erkennt man eigentlich nix. Ich dachte, dass zumindest kurz vor den jeweiligen Ferien ein kleiner Peak zu sehen sein würde, aber ich bin halt auch kein Marktforscher. Interessanter finde ich da schon das:

Bildschirmfoto 2013-07-20 um 20.25.08

Man erkennt hier einen schönen Trend in Richtung 21 Uhr, außerdem kleinere Peaks um 6-7 Uhr (also vor der Schule) und um 10 Uhr herum (1. große Pause). Man könnte sogar eine ungenaue Durchschnittszeit ermitteln, zu der der durchschnittliche Ferien Countdown User schlafen geht 😉

Ich hab’s mir nicht nehmen lassen, auch die Device Family aufzuzeichnen. Welches iOS-Gerät benutzt der Otto-Normalferiencountdownverbraucher also?

Bildschirmfoto 2013-07-20 um 20.26.04

Dass gerade mal 1/4 Ferien Countdown auf einem iPod Touch benutzt haben, überrascht mich etwas. Ansonsten wird man als Schüler (der nicht so nerdig ist wie ich) wohl eher kein iPad besitzen, und dass die iPhone-Besitzer dominieren war auch klar.

Nochmal als Disclaimer, diese Daten sind aus den Logs des Update-Servers generiert und mehr hab ich auch nicht, weder eure Namen, noch Kontakte, noch Standort, nicht mal eure Softwareversion. Ich bin nicht Facebook. Jede Webseite die ihr besucht zeichnet wesentlich mehr auf als das.

Das Speicherproblem Teil 2

IMG_0400Nachdem es mir gestern gelungen ist, mit einer extrem eingeschränkten Zeichentabelle 6 Zeilen des Bildschirms zu füllen, hab ich heute am Code weitergearbeitet und es geschafft, den ganzen Bildschirm zu nutzen und alle druckbaren Zeichen auch wirklich anzeigen zu können.

Bildschirmfoto 2013-06-26 um 00.28.42Ich kann jetzt also bei der momentanen Schriftgröße (8×8) insgesamt 726 Zeichen anzeigen. Schon allein diese Anzahl ist größer, als der mir zur Verfügung stehende Arbeitsspeicher. Nicht zu vergessen die insgesamt 222 Zeichen in der Schrifttabelle, die theoretisch noch mal 1776 Byte Platz belegen würden.

Um das Unmögliche möglich zu machen habe ich das Text-Rendering und die Schrifttabelle komplett überarbeitet. Ich habe nichts komprimiert, weil das Dekomprimieren auf 16 Mhz nicht schnell genug machbar gewesen wäre, aber habe mir die immerhin noch 10KB freien Speicherplatz im Flash zunutze gemacht. So wird der Font-Buffer von bislang 208 Byte auf 8 Byte reduziert – dafür gibt es eine Funktion, die diesen bei Bedarf aus Font-Daten des Flash-Speichers konstruiert. Das dauert zwar, aber in diesem Fall ist der ePaper-Bildschirm noch klar der Flaschenhals, sodass das nicht wirklich auffällt.

Für den Textpuffer habe ich mir den Speicher der Ansteuerungseinheit des Displays zunutze gemacht, welche immerhin einen Framebuffer hat, den ich zwar nicht direkt ansteuern, aber, wie letztes Mal schon gesagt, zeilenweise füttern kann. Mein Programm speichert sich also jetzt nur noch die letzte Zeile des angezeigten Textes und rendert diese, wenn sie voll ist, direkt in das Display. So kann ich eine Funktion ähnlich wie printf realisieren, welche auch mehrfach in die gleiche Zeile schreiben kann.

Wenn die letzte Zeile voll ist, bin ich leider nicht dazu in der Lage, den ganzen Text nach oben zu schieben, da das Programm schon längst vergessen hat, was in den oberen Zeilen steht. Mal sehen ob ich irgendwie den Inhalt des Framebuffers auslesen und ihn dann versetzt wieder schreiben kann.

Bildschirmfoto 2013-06-26 um 00.26.29Fazit Speicherverbrauch: Der Flash-Speicher ist bis zum Rand voll (15,5 von 16 KB). Allerdings ist dafür der RAM noch schön leer, und theoretisch müsste ich das Programm sogar, wie gestern scherzhaft angedeutet, mit einer kleineren Schrifttabelle auf den schwächeren Chip mit 256 Byte übertragen können. Dieser hat allerdings anscheinend einen Unterschied in der Übertragung und schreibt nur wirres Zeug auf das Display. Ich hab trotzdem aus Spaß auf dem größeren Chip parallel zum Text-Schreiben ein malloc(256) laufen lassen und er hatte keine Probleme, mir den Speicher zu reservieren – also sagen wir mal, ich habe theoretisch die Speicher-Hürde geschafft.

Und nur mal ganz nebenbei gesagt – Sublime Text ist episch geil. Ich wäre sonst beim Konvertieren der Schrifttabelle durchgedreht.

Das Speicherproblem

IMG_0397Ich hab mir für die lernfreie Zeit mal ein neues Gadget gegönnt, nämlich ein TI LaunchPad (das ist ein Programmierboard für Embedded-Chips) und dazu ein richtig cooles ePaper-Display. Warum? Weil ePaper geil ist. Ich mag diese Technik und ich denke mal dass da noch was richtig großes draus werden kann.

Mit so einem Ding bewegt man sich programmiertechnisch so weit Low Level, noch einen Schritt weiter runter und man hängt am Assembler-Code. Das heißt abgespeckte C Library, wenig Speicher, Interrupts, Vektoren und vieles mehr. Man muss dem Compiler sogar sagen, wo er einzelne Funktionen hinkompilieren soll, denn der Chip führt bei einem Interrupt einfach eine feste Speicheradresse aus.

Und das macht tatsächlich Spaß und man lernt, wie man beim Programmieren richtig den Speicher und die CPU schont, schließlich arbeitet der Chip mit 16 MHz. Einen gnädigen Flash-Speicher von 16KB gibt’s auch noch – es passiert also gerne mal dass das fertige Programm gar nicht mehr auf den Chip passt. Noch härter hat’s den RAM erwischt – ich hab 512 Byte zur Verfügung. Das heißt ein malloc(512) und der Speicher ist voll.

Und hier liegt die Herausforderung, vor die ich mich hartnäckigerweise gestellt habe. Das 2,7″ ePaper-Display ist monochrom und hat eine Auflösung von 264×176 Pixeln. Wenn ich pro Bit eine Information speichere, liegt mein Speicherverbrauch bei ca. 5,8 KB. Tja, zu groß.

Also ist nix mit Bildschirmpuffer, und es wird noch böser, denn der Bildschirm will sein Futter zeilenweise haben, also ist auch nix mit einer setPixel()-Funktion. printf gibt’s natürlich auch nicht – noch nicht einmal eine Schriftart. Für das Display wird eine GFX-Library angeboten, mit welcher man Kreise und Text zeichnen kann – diese sprengt auf meinem Chip (MSP430G2553) sowohl bei RAM als auch Flash jeden Rahmen. Für diese Bildschirmgröße soll die nicht einmal auf einem Arduino Mega laufen.

Bildschirmfoto 2013-06-25 um 02.02.21Also macht man’s eben selbst. Mein Programm benutzt einen Textpuffer für insgesamt 198 Zeichen, einen Zeilenpuffer für die momentan bearbeitete Zeile und eine 6×8 Bitmap-Schrift-Tabelle mit 26 Zeichen. Damit ist der Speicher dann auch voll. Nach viel Verzweiflung und Herumgefrickel kann ich jetzt tatsächlich Buchstaben wie oben im Bild anzeigen.

Ich muss das bei Zeit noch mal ausbauen, denn wie man sieht ist es noch nicht wirklich gut. Ich bin in der Lage ca. 30% des Bildschirms auszunutzen und habe gerade mal alle Großbuchstaben und ein Leerzeichen zur Auswahl. Bill Gates hat mal gesagt dass 640 KB RAM für immer ausreichen sollten, und ich bin mir sicher dass ich diese 512 Byte RAM noch zu Höchstleistungen überreden kann. Und dann portiere ich das auf den anderen Chip mit 256 Byte RAM, der hier auch noch irgendwo rumfliegt (auf den passt die Schrifttabelle nur knapp drauf…) 😉

Ich werde da noch mal drüber berichten, wenn ich den Code „optimiert“ hab. Wenn ich bedenke wie viele unserer Haushaltsgeräte solche Embedded-Chips beinhalten, ist es wahrscheinlich gar keine schlechte Idee, sich damit auseinanderzusetzen. Es gibt auch weder frustrierte User noch schlechte App Store Bewertungen oder sinnlose Feature-Wünsche! 🙂

Xcode-Weisheiten über Git

Xcode unterstützt Git, wenn auch eher schlecht als recht. Ich liebe Git und benutze es dementsprechend häufig – nicht nur auf GitHub sondern für Closed Source und zukünftige Sachen auch über meinen eigenen Git-Server. Um den zu erreichen musste ich, obwohl ich das sonst mit SourceTree (beste Git-App wo gibt!!) mache, die Git-URL in Xcode eintragen. Letzteres hat eigene Vorstellungen davon, was eine gültige Adresse ist und was nicht.

Was für Xcode gültige Git-URLs sind:

Bildschirmfoto 2013-05-21 um 21.02.48

Wikipedia. Is‘ ja klar, die haben alles.

Bildschirmfoto 2013-05-21 um 21.10.47

Raute. Er hat sich sogar testweise damit verbunden um zu merken dass er den Server von Raute nicht erreichen kann.

Was für Xcode keine gültige Git-URL ist:

Bildschirmfoto 2013-05-21 um 21.02.36

Natürlich, die funktionierende URL wird natürlich erst gar nicht überprüft, die „Verbindungsanzeige“ direkt mal aus Protest ausgeblendet und der Next-Knopf gesperrt. Ist ja auch verständlich, so Zahlen mit Punkten und dann noch ein @ und ein Doppelpunkt dazwischen, wer soll damit denn klar kommen.

Im neuesten Xcode haben sie an Git noch mal gut rumgewerkelt – selbst wenn immer noch keine Submodules funktionieren ist dieser Bug behoben. Fand ich trotzdem kurios.

sndscope Nachtrag

Kurzer Nachtrag. Ich wollte drei Screenshots mit euch teilen, die dadurch entstanden sind, dass ich die Framerate von sndscope auf 1 FPS gesenkt hab. Dadurch zeichnet er pro Frame 44100 Beams (abhängig von der Samplerate der Audiodatei) – so ähnlich als wenn man bei einem Oszilloskop die Intensität hochdreht sodass alles eine Weile „nachglüht“. Sieht nett aus.