Riesenringpuffer

  • Themenstarter Gelöschtes Mitglied 9001
  • Beginndatum
Diskutiere Riesenringpuffer im Allgemeine Java-Themen Bereich.
G

Gelöschtes Mitglied 9001

Man geht nicht mit einem Messer zu einer Schießerei.
Du kannst halt nicht mit einem schwachbrüstigen Taschenrechner kommen, wenn du große, zeitlich fein aufgelöste Simulationen rendern oder Nachrichten mit komplexen Chiffrieralgorithmen ver/entschlüsseln willst.
Es geht hier lediglich um's Puffern, nicht um's Rendern/Umkodieren/Verschlüsseln. Wie schon gesagt: der vorhandene Computer kann mit dem Audiosignal problemlos umgehen.

Aber gut, wenn eine Reduktion der Daten auch nicht in Frage kommt (auch wenn ich mögliches Clippen eher als ein Problem durch schlechte Hardware ansehen würde, ich bin aber auch kein Experte für Signalverarbeitung und will mich da nicht zu weit aus dem Fenster lehnen), hat sich das wohl erledigt.
Clippen ist etwas anderes als kurze Aussetzer (die durch schwache Hardware entstehen können). Clippen heißt: das Signal geht über 0dB hinaus, was zu hörbarer Verzerrung führt. Um das zu vermeiden, muß man den Aufnahmepegel entsprechend einstellen. Nun kann die Audiokurve zwischen den Samplewerten höher ansteigen, als die Samplewerte. Wenn man nicht permanent mithören kann (weil man z.B. selbst das Instrument spielt), können Clips so unerkannt bleiben. Je höher die Abtastfrequenz, desto genauer wird die Kurve digital nachgezeichnet, desto genauer kann der Peak ermittelt und demzufolge der Aufnahmepegel eingestellt werden und desto unwahrscheinlicher wird ein unerkanntes Clippen. Wenn das Mastering fertig ist, kann man dann für den Consumer auf 44,1 kHz herunterrechnen.

Hoppla - da ist ja noch ein weiterer Lösungsansatz. Ich kann mir kaum vorstellen daß ein WLAN-Sender zu schwach sein soll, um 30m zu überbrücken, solange es da Sichtverbindung gibt. Könnte es eher sein, daß sich da zu viele WLAN-Geräte gegenseitig im Frequenzband blockieren?
Anderer Vorschlag: Kannst du deinen Tascam - was immer es ist, es scheint deine Datenquelle zu sein - mit einer Richtantenne ausstatten und damit auf den Empfänger der Daten draufhalten? Oder einen Signalverstärker vor dessen Antenne schalten?
Es gibt portable (Handheld-)Recorder, die mit internen und/oder externen Mikrofonen direkt auf eine SD-Karte aufzeichnen und meist noch einige Funktionen für's Mixing, Limiting und dergleichen bieten. Kaum größer als ein Raspi. Da sieht man, dass Minicomputer problemlos mit großen Audiomengen umgehen können.
Die Firma Tascam bietet einige an. Nein, man kann keine Antenne anschließen und ich habe es mehrfach probiert: bereits 20m Sichtverbindung waren für den Tascam nicht mehr schaffbar. Und es war kein anderes WLAN in Reichweite. Das ist der Grund, weshalb ich einer eigenen Lösung arbeite. Der Raspi hat offensichtlich ein erheblich besseres Wlan-Signal.

Jetzt sind wir freilich schon wieder weit vom Thema abgekommen.
Ich habe zwischenzeitlich einen Puffer mit eigenem Auslagerungsmechanismus geschrieben und werde jetzt testen testen testen.
 
Zuletzt bearbeitet von einem Moderator:
T

temi

Es geht hier lediglich um's Puffern, nicht um's Rendern/Umkodieren/Verschlüsseln. Wie schon gesagt: der vorhandene Computer kann mit dem Audiosignal problemlos umgehen.
Das ist eigentlich egal. Wenn die vorhandene Hardware zu wenig Speicher hat, um die Anforderungen an das Puffern zu erfüllen, erfüllt die Hardware diese Anforderungen nicht, auch wenn sie von der Geschwindigkeit ausreichend wäre.

Du hast jetzt einige mögliche Lösungsvorschläge erhalten. Wenn das Puffern auf SD-Karte die Lösung deiner Wahl ist, dann mach das so. Falls die SD-Karte nicht ausreicht, kannst du immer noch, mit der selben Lösung, eine SSD per USB-SATA-Adapter verwenden.


Vielleicht magst du ja diese Lösung (zumindest mittels einer kurzen Beschreibung) mit uns teilen?
 
G

Gelöschtes Mitglied 9001

Das ist eigentlich egal. Wenn die vorhandene Hardware zu wenig Speicher hat, um die Anforderungen an das Puffern zu erfüllen, erfüllt die Hardware diese Anforderungen nicht, auch wenn sie von der Geschwindigkeit ausreichend wäre.
Die Hardware erfüllt die Anforderungen, gesucht war lediglich ein Algorithmus, der die Hardware adäquat ausnutzt.

Du hast jetzt einige mögliche Lösungsvorschläge erhalten. Wenn das Puffern auf SD-Karte die Lösung deiner Wahl ist, dann mach das so.
Puffern im Ram und nötigenfalls auf SD-Karte.

Vielleicht magst du ja diese Lösung (zumindest mittels einer kurzen Beschreibung) mit uns teilen?
Kommt, nach dem ich getestet habe.
 
T

temi

Die Hardware erfüllt die Anforderungen, gesucht war lediglich ein Algorithmus, der die Hardware adäquat ausnutzt.
Die einen sagen so, die anderen so. ;)

Nee, ist schon in Ordnung. Wenn es mit dem Puffern auf SD funktioniert ist ja alles in Ordnung. Wir sprechen dann wieder drüber, wenn du mit 12 Kanälen ankommst ;)
 
G

Gelöschtes Mitglied 9001

Die einen sagen so, die anderen so. ;)
Der Markt ist voll von Audiointerfaces, -recordern, -effektgeräten, -mixern aller coloeur. Sie allesamt sind in der Lage, große Audiomengen zu verarbeiten und da stecken keine PCs mit 8, 16, 32 GB Arbeitsspeicher drin. Man kann natürlich auch einfach ein Dante-Audiointerface kaufen, welches Netzwerkanschluß hat und außerdem auch noch niedrige Latenz bietet. Die 1500 Euro, die man dafür hinlegen muß, spiegeln aber nicht nur die verwendete Hardware wieder, sondern auch die gedanklichen Fähigkeiten der Entwickler, die die in der Realität nunmal begrenzten Hardwaremöglichkeiten geschickt auszunutzen wissen. Wenn niedrige Latenz (und nur die stellt wirklich Ansprüche an die Hardware) aber keine Anforderung ist, ergibt es wirtschaftlich keinen Sinn, dafür Geld zu investieren.
Die Vorstellung, dass ein kleiner Computer für Audioverarbeitung nicht ausreichend sein könnte, rührt möglicherweise aus der Gamerszene her, wo Hardware eine sehr gewichtige Rolle spielt. Tatsächlich gibt es digitale Audioverarbeitung bereits seit Jahrzehnten. MP3 wurde in den 80er Jahren entwickelt, damals wäre der Raspi von heute ein Hochleistungscomputer gewesen.
 
W

White_Fox

Der Markt ist voll von Audiointerfaces, -recordern, -effektgeräten, -mixern aller coloeur. Sie allesamt sind in der Lage, große Audiomengen zu verarbeiten und da stecken keine PCs mit 8, 16, 32 GB Arbeitsspeicher drin.
Das ist zwar richtig, aber dafür steckt da sicherlich hochspezialisierte HW drin, die wirklich nur eines macht: Audiosignale verarbeiten. Da werkelt dann i.d.R. auch kein Linux drin (und wenn, dann ist es sehr stark angepasst das für andere Zwecke nicht mehr zu gebrauchen ist), sondern z.B. irgendein spezielles RTOS für Mikrocontroller. Und Flashspeicher ist dann z.B. nicht über ein lahmes serielles Interface angebunden wie eine SD-Karte, sondern über irgendwas paralleles. Und dafür fehlt allerhand anderes Gedöns, kein nichtbenötigter USB-Controller gammelt mit einem Dämon im Speicher herum und tut ständig seinen unveränderten Status kund und dafür wird die eigentliche Aufgabe unterbrochen...
Falls da überhaupt ein Betriebssystem läuft.

Und deswegen ist ein RPi mit spezialisierter Hardware nicht zu vergleichen.

Die Vorstellung, dass ein kleiner Computer für Audioverarbeitung nicht ausreichend sein könnte, rührt möglicherweise aus der Gamerszene her, wo Hardware eine sehr gewichtige Rolle spielt.
Nein...zumindest hier nicht. Ich z.B. habe von Programmierung eigentlich kaum Ahnung, weil ich eigentlich viel mehr mit HW zu tun habe. :)

Tatsächlich gibt es digitale Audioverarbeitung bereits seit Jahrzehnten. MP3 wurde in den 80er Jahren entwickelt, damals wäre der Raspi von heute ein Hochleistungscomputer gewesen.
Und damals fand Audiosignalverarbeitung, jedenfalls soweit ich weiß, entweder komplett analog statt oder hat ewig gedauert. ;)
 
J

JustNobody

Der Markt ist voll von Audiointerfaces, -recordern, -effektgeräten, -mixern aller coloeur. Sie allesamt sind in der Lage, große Audiomengen zu verarbeiten und da stecken keine PCs mit 8, 16, 32 GB Arbeitsspeicher drin. Man kann natürlich auch einfach ein Dante-Audiointerface kaufen, welches Netzwerkanschluß hat und außerdem auch noch niedrige Latenz bietet. Die 1500 Euro, die man dafür hinlegen muß, spiegeln aber nicht nur die verwendete Hardware wieder, sondern auch die gedanklichen Fähigkeiten der Entwickler, die die in der Realität nunmal begrenzten Hardwaremöglichkeiten geschickt auszunutzen wissen. Wenn niedrige Latenz (und nur die stellt wirklich Ansprüche an die Hardware) aber keine Anforderung ist, ergibt es wirtschaftlich keinen Sinn, dafür Geld zu investieren.
Die Vorstellung, dass ein kleiner Computer für Audioverarbeitung nicht ausreichend sein könnte, rührt möglicherweise aus der Gamerszene her, wo Hardware eine sehr gewichtige Rolle spielt. Tatsächlich gibt es digitale Audioverarbeitung bereits seit Jahrzehnten. MP3 wurde in den 80er Jahren entwickelt, damals wäre der Raspi von heute ein Hochleistungscomputer gewesen.
Also es ist klar: Wenn man die Spects betrachtet, dann hat der PI die notwendige Leistung.

Aber die Frage ist, in wie weit es zu worst case Fällen kommen kann. Was für ein Betriebssystem setzt Du ein? Üblich ist oft ein Linux System (Raspian ist wohl sehr weit verbreitet), was aber zur Konsequenz hat, dass das System sehr viel mehr macht, als eben nur Deine Applikation.
Desweiteren setzt Du auf Java, d.h. selbst Deine Applikation macht einiges mehr....

Und dann kann es schnell passieren, dass es zu einem Worst Case kommen kann:
- Durch die suboptimale Hardware (Ich habe es nie im Detail analysiert, aber der Pi hat wohl was IO angeht gewisse Bottlenecks) sind die Leistungsreserven ggf. nicht so groß.
- Andere Prozesse benötigen dann einen Teil der verfügbaren Leistung (Ich nehme einfach einmal das 08/15 Raspian: Da startet X11, also eine grafische Ausgabe benötigt gewisse IO des Systems, ggf. laufen Dinge wie SSH weil man ja remote zugreifen möchte, ....)
- Dann kommt ggf. noch ein GC Lauf an ungünstiger Stelle ....

Daher mag es durchaus so zu sein, dass andere Systeme, die ähnliches machen, auch nicht mehr Leistung haben, aber diese nutzen die vorhandenen Ressourcen in der Regel besser. Früher wäre dann eine Embedded Lösung das, was ich empfohlen hätte (Gibt es heute immer noch - da kommen dann Dinge wie FreeRTOS und co ins Spiel) aber da die Hardware immer leistungsfähiger wird, wäre Linux auch heute meine Wahl - aber keine 08/15 Distribution sondern massiv abgespeckt. Einfaches Startup/Initialisierung (kein Systemd und co), minimiertes System und wohl auch einen entsprechend aufgebohrten Kernel.

Ich würde überlegen, die Schreibzugriffe auf die SD Karte zu minimieren. Also das System direkt in den Speicher zu laden und danach nichts mehr lesen/schreiben zu müssen. Das kostet zwar etwas Speicher, aber das kann gering gehalten werden (So man wirklich nicht mehr braucht, als eben diese Streaming Lösung!)
Das, was man da mehr verballert, gewinnt man massiv, durch den Verzicht auf Java und eben der Erstellung einer native Lösung mit z.B. C/C++.

(Das geht bei dem ein oder anderen embedded System so weit, dass dann das System nur als Source vorliegt und den Rahmen der Applikation bildet. Dann hat man halt am Ende einen übersetzten Kernel, der nur die benötigten Elemente beinhaltet. Das läuft dann teilweise auch auf einem Arduino. Also ein System, das um ein vielfaches kleiner ist als ein pi :) Da aber heute die Hardware immer leistungsfähiger wird, ist die Entwicklung eher so, dass 08/15 Systeme angepasst werden und die Entwicklung dann entsprechend laufen kann ... also meist ist dann Linux das Ziel und Entwickler können dann eine Distribution ihrer Wahl nutzen und auf dem System läuft dann am Ende ein spezielles angepasstes minimales Linux).

Aber das nur ganz am Rande als meine Sichtweise. Da Du Dich bezüglich Sprache und Plattform ja festgelegt hast, ist das erst einmal uninteressant. Außer evtl. als Hinweis, dass Du ggf. am Ende eine Art customized Linux haben willst, auf dem weniger läuft. Man kann ja ggf. einiges einfach nicht mitstartet und wenn alles funktioniert, dann ggf. Pakete löschen. Dann hat man am Ende seine eigene, minimierte Lösung.
 
G

Gelöschtes Mitglied 9001

Und Flashspeicher ist dann z.B. nicht über ein lahmes serielles Interface angebunden wie eine SD-Karte, sondern über irgendwas paralleles. Und dafür fehlt allerhand anderes Gedöns, kein nichtbenötigter USB-Controller
Die gängigen Field-Recorder - von Low-Budget bis hinzu High-End - schreiben alle auf SD-Karte. Und USB haben die allermeisten. Dann kommen neuerdings eben auch noch Wlan und Ethernetanschlüsse hinzu.

Ich würde überlegen, die Schreibzugriffe auf die SD Karte zu minimieren.
Genau deswegen habe ich diesen Thread erstellt: wie gelingt es, einen Ringpuffer zu implementieren, der hauptsächlich im Ram arbeitet und nur im worst case die SD-Karte benutzt. Das Swapping des OS hilft hier nicht und das wird auf dem Raspi ohnehin in der Regel deaktiviert, eben um unnötige Schreibzugriffe zu vermeiden.
Das, was ich hier vorhabe, hat letztendlich einen Bruchteil der Schreibzugriffe auf die SD-Karte als ein Fieldrecorder, der die gesamte Tonaufnahme auf die SD-Karte schreibt (und das sind etliche GB).

Da Du Dich bezüglich Sprache und Plattform ja festgelegt hast, ...
Mich interessierte eigentlich ganz abstrakt ein Algorithmus, der mit zwei verschiedenen Speichermedien umgehen kann, so wie man sich als Programmierer eben auch mal ganz abstrakt für Algorithmen interessieren kann, unabhängig von verwendeter Sprache und Hardware.

Die ganzen Linux-Administrationsarbeiten sind natürlich schon längst erledigt, aber wir müssen ja damit den Thread hier nicht auch noch füllen, habe ja ohnehin schon viel zu Signalverarbeitung geschrieben, obwohl das auch mit dem eigentlichen Algorithmus nichts zu tun hat.
 
mrBrown

mrBrown

Genau deswegen habe ich diesen Thread erstellt: wie gelingt es, einen Ringpuffer zu implementieren, der hauptsächlich im Ram arbeitet und nur im worst case die SD-Karte benutzt.
Ganz naiver, aber simpler Ansatz:

Als eigentlichen Puffer selbst irgendeine Standard-Queue, zB ArrayBlockingQueue.
Die ankommenden Daten jeweils unabhängig davon Puffern, zB immer auf X MB oder X Sekunden warten
Wenn entsprechende Daten vorhanden sind, ob noch "RAM" frei ist
-> Wenn ja, Daten als zB Byte-Array in den normalen Puffer legen
-> Wenn nein, Daten auf die SD-Karte schreiben und Zeiger auf die Datei/Position/Whatever in den Puffer legen

Ein zweiter Thread holt sich jeweils das erste Element der Queue, schickt's an den Server und löscht die etwaigen Dateien


Das ist keineswegs die schneller/speichersparendste/whatever-Variante, sondern einfach nur "einfach" und vielleicht sogar schon ausreichend. Lässt sich theoretisch auch beliebig aufbohren, eigenes Queue-Interface drüber und nach außen eine Queue<Byte> draus machen, zwischen nio und io wechseln, Pools nutzen, ...
 
J

JustNobody

Genau deswegen habe ich diesen Thread erstellt: wie gelingt es, einen Ringpuffer zu implementieren, der hauptsächlich im Ram arbeitet und nur im worst case die SD-Karte benutzt.
Bei meinem Punkt geht es aber nicht um Deine Implementation sondern um genau das, was Du die ganze Zeit klar abgelehnt hast:
Das ganze drumherum!

Was den reinen Algorithmus angeht:
Wo ist das Problem? Die Thematik ist doch absolut trivial und ich verstehe da gerade nicht, wo da Dein Problem wirklich ist.

Du hast zum einen den eigentlichen Rahmen:
a) Ein Thread der Daten von der Quelle liest und merkt.
b) Ein Thread der ein Ziel beliefert und aus dem gemerkten die Daten holt.

Daten kommen immer in Blöcken so wie ich das verstanden habe. Also musst Du x Blöcke merken können. Dazu hast Du dann ein Array mit x Elementen. Jedes Element ist entweder ein Block mit Daten im Speicher (also z.B. ein byte Array) oder eben eine externe Referenz.

Da Speicher knapp ist, machen wir hier eine Totsünde und wir optimieren:
Das ist ein einfaches Array von Integern.
Du willst x Blöcke im Speicher behalten: Dann bedeutet das, dass Du etwas hast wie
- Ein Array von byte Arrays. Jedes Byte Array hat dann genau einen Block. Und 0 bis (x-1) Elemente sind dann im Speicher.
- Du hast nur ein byte Array - der Index ist dann berechenbar. Der x-te Block ist dann (x-1)*size bis x*size - 1.
Alle Blöcke mit x >= interner Buffer sind dann Dateien: In einem Verzeichnis Deiner Wahl legst Du z.B. buffer<x> an. Also sowas wie buffer000158.
Oder Alternativ eine Datei mit Random access Zugriff. Dann hast Du einmal eine Datei reserviert. Aber bei einer SD Karte weiss ich nicht, ob da eine Optimierung drin ist, dass Du nicht ständig auf die gleichen Speicherstellen schreibst. ==> Das könnte die SD Karte schnell killen! Daher: Analyse (Was Du ja gar nicht willst / magst so wie ich Dich verstanden habe bisher) .... also ggf. ständig neue Dateien erstellen und löschen könnte besser sein....

Nun hast Du also:
a) die eigentliche physikalische Speicherung der Daten.
b) eine Referenzierung über ein großes Array (wo die Integer Zahl gespeichert ist, die den Speicherort kennzeichnet - je nach Anzahl kann man hier Speicher sparen und man speichert nur char Werte, die nur 2 Bytes groß sind. Das reicht evtl. aus?).

Also fehlt nur noch die Verwaltung der freien Elemente. Das ist dann der typische Ringspeicher. Du musst Dir also für interne und externe Werte merken: Wo ist das erste und wo das letzte Element.

Beim merken schaust Du also dann: ist im internen Ringspeicher was frei? -> da speichern und gut ist es, ansonsten speichern wir im externen Ringspeicher.

Dann müssen wir das Speichern sinnvoll gestalten:

Der interne Speicher hat ja schon byte Arrays. Um Daten nicht unnötig kopieren zu müssen wird dann einfach die byte-Array Referenz getauscht. So vermeiden wir unnötiges kopieren und auch der GC muss nicht tätig werden. So Du nicht einzelne Byte-Arrays hast, ist es auch kein Problem. Dann muss das Einlesen der Daten schon an der richtigen Stelle erfolgen. Also ist im internen Speicher was frei, wird direkt in den Speicher dort gelesen. Ist intern nichts frei, wird in den Puffer (der einmal am Anfang deklariert wurde) gelesen um dann die Daten in eine Datei zu schreiben.

Also von der Problematik her ist der Algorithmus aus meiner Sicht trivial und lässt sich so herunter schreiben. Problematisch sind nur die Rahmenbedingungen. Da GC Aktivität gut vermieden werden kann und viel mit native Types (und Arrays von diesen) gearbeitet werden kann, ist Java noch nicht einmal so ein großes Problem. Mit Modulen und Co ist der Speicher-Overhead wohl vertretbar. Wobei ich für mich keinen Grund sehe, das in Java zu machen, oder ist das Lesen oder Schreiben der Daten irgend etwas spezielles, für das es nur Java Libraries geben sollte?
 
J

JustNobody

Ganz naiver, aber simpler Ansatz:
Ja, da warst Du etwas schneller. Ich habe aber sogar die "optimierte" Variante mal herunter geschrieben. Da kann man dann einmalig Speicher reservieren und dann ganz ohne GC weiter arbeiten. Dann ist es sogar denkbar, andere GC zu probieren. Wenn man sicher ist, dass man den GC nicht braucht, dann kann man die Variante ohne GC versuchen und verifizieren, dass die Annahme richtig ist. Minimaler Speicherzuwachs ist ggf. vertretbar, da ja so Aufnahmen nur begrenzte Zeit laufen dürften (Annahme von meiner Seite - müsste verifiziert werden).
 
G

Gelöschtes Mitglied 9001

Ganz naiver, aber simpler Ansatz:

Als eigentlichen Puffer selbst irgendeine Standard-Queue, zB ArrayBlockingQueue.
Die ankommenden Daten jeweils unabhängig davon Puffern, zB immer auf X MB oder X Sekunden warten
Wenn entsprechende Daten vorhanden sind, ob noch "RAM" frei ist
-> Wenn ja, Daten als zB Byte-Array in den normalen Puffer legen
-> Wenn nein, Daten auf die SD-Karte schreiben und Zeiger auf die Datei/Position/Whatever in den Puffer legen

Ein zweiter Thread holt sich jeweils das erste Element der Queue, schickt's an den Server und löscht die etwaigen Dateien
Japp, so hab ich's letztlich gemacht. Etwas feiner muß man es dann noch betrachten: die Blöcke, die das Audiointerface liefert, sind meist sehr kurz, ca. 1 Sekunde. Ich sammle mehrere solcher Blöcke zusammen in einen größeren Datenblock. Sobald der voll ist, kann er ggf. ausgelagert werden.
 
G

Gelöschtes Mitglied 9001

Was den reinen Algorithmus angeht:
Wo ist das Problem? Die Thematik ist doch absolut trivial und ich verstehe da gerade nicht, wo da Dein Problem wirklich ist.
Es ist doch nicht so, dass das für mich ein großartiges Problem ist. Ich bin nur an Austausch interessiert und es gibt ja mehrere Lösungsansätze. An denen war ich interessiert. Einen Algorithmus schreiben kann ich selbst (siehe oben), aber es ist eben auch interessant zu lesen, wie andere es machen. Es ist hingegen weniger interessant zu lesen, dass jemand in einem Wisch es „sch...“ findet.
 
G

Gelöschtes Mitglied 9001

Java:
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.StandardOpenOption;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * Puffer für eine große Anzahl binärer Daten.<br>
 * Die Daten werden auf Datenblocks aufgeteilt, die in einer Queue liegen. 
 * Ist ein Datenblock voll, wird ein weiterer angelegt. Leseoperationen bewirken, dass ein Datenblock peu à peu "leergelesen"
 * wird. Leere Datenblocks werden aus der Liste entfernt.<br>
 * Geht der verfügbare Arbeitsspeicher zur Neige, werden Datenblocks, beginnend vom Tail der Queue an, ausgelagert, bis ihre
 * Daten gebraucht werden.<br>
 * Nur vollbeschriebene Datenblocks werden ausgelagert.
 */
public class BigBuffer {

    /**
     * Enthält einen Teil der Daten. Kann diese auslagern und wieder einholen.
     */
    private static class DataBlock {
        private volatile byte [] data;

        private volatile int capacity;  // Die Kapazität kann man zwar durch data.length ermitteln,
                                        // wenn jedoch die Daten ausgelagert sind, ist data null
                                        // und wir wollen uns beim Wiedereinholen nicht auf die
                                        // Länge der Auslagerungsdatei verlassen
        private volatile int writepos, readpos, size; // Schreib/Leseposition, aktuelle Größe der Nutzdaten
        private volatile boolean externalized;  // true, wenn die Daten ausgelagert wurden

        private volatile File file;  // Auslagerungsdatei
        
        public DataBlock(int capacity) {
            this.capacity = capacity;
            data = new byte[capacity];
            writepos = 0; readpos = 0; size = 0;
            externalized = false;
        }
        
        /**
         * Ein Block kann nur ausgelagert werden, wenn er voll beschrieben ist und noch kein bißchen
         * ausgelesen wurde.
         */
        public boolean isExternalizeable() {
            return !externalized && isCompletelyFilled() && readpos == 0;
        }
        
        /**
         * Lagert die Daten aus. Liefert false, wenn fehlgeschlagen.
         */
        public void externalize() throws IOException {
            File F = File.createTempFile("audiobuffer", ".raw");

            try (FileChannel channel = FileChannel.open(F.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
                channel.write(ByteBuffer.wrap(data));
                file = F;
                externalized = true;
                data = null;               
            }
        }
        
        /**
         * Schreibt Daten in den Block. Es kann passieren, dass der Datenblock die Daten nicht
         * komplett aufnehmen kann (weil seine Kapazität erschöpft ist).
         * Die Methode liefert die Anzahl Bytes zurück, die geschrieben wurden.
         */
        public int offer(byte [] srcBuffer, int offset, int len) {
            int rest = data.length - writepos;   // Wieviel Daten passen in diesen Block überhaupt noch hinein?
            int actualLen = Math.min(len, rest);
            System.arraycopy(srcBuffer, offset, data, writepos, actualLen);
            writepos += actualLen;
            size     += actualLen;
            return actualLen;
        }
        
        /**
         * Liest Daten aus dem Block. Wenn nicht soviel Daten vorhanden sind, wie gewünscht, werden nur soviel Daten geliefert,
         * wie vorhanden. Die Anzahl der gelesenen Daten wird zurückgeliefert.
         */
        public int read(byte [] destBuffer, int offset, int len) throws IOException {
            if (externalized) {
                internalize();
            }
            
            int actualLen = Math.min(len, size);
            System.arraycopy(data, readpos, destBuffer, offset, actualLen);
            readpos += actualLen;
            size -= actualLen;
            return actualLen;
        }
        
        private void internalize() throws IOException {
            try (FileChannel channel = FileChannel.open(file.toPath(), StandardOpenOption.READ)) {
                data = new byte[capacity];
                int actuallyRead = channel.read(ByteBuffer.wrap(data));
                if (actuallyRead != capacity) {
                    throw new IOException("invalid buffer file: could not read all data");
                }
                externalized = false;
            }
        }
        
        /**
         * Bytes ohne Einzulesen verwerfen. Liefert die tatsächlich verworfenen Bytes zurück. Das
         * können weniger als die angeforderten bytes sein.
         *
         * Wenn der Datenblock ausgelagert ist und die zu verwerfenden Bytes mindestens der Länge des
         * Datenblocks entsprechen, kann die Auslagerungsdatei direkt gelöscht werden. Ist die Anzahl
         * zu verwerfender Bytes kleiner als die Datenlänge, müssen die Daten ggf. erst eingeholt werden.
         */
        public long discard(long bytes) throws IOException {
            if (externalized && bytes >= capacity) {
                dispose();
                return capacity;
            } else {
                internalize();
                long actualLen = Math.min(bytes, size);
                readpos += actualLen;
                size -= actualLen;
                return actualLen;
            }
        }
        
        /**
         * Liefert true, wenn der Datenblock leergelesen ist. Leergelesen meint nicht, wenn die Nutzdatenlänge 0 ist,
         * sondern wenn die Leseposition am Ende dieses Blocks angekommen ist.
         */
        public boolean beenReadCompletely() {
            return readpos >= capacity;
        }
        
        /**
         * Liefert true, wenn der Datenblock keine Daten mehr aufnehmen kann.
         * @return
         */
        public boolean isCompletelyFilled() {
            return writepos >= capacity;
        }
        
        public void dispose() {
            if (file != null && file.exists()) {
                file.delete();
            }
        }
        
        /**
         * Convenience-Method. Fragt auch null ab.
         */
        public static boolean isExternalizeable(DataBlock block) {
            return block != null && block.isExternalizeable();
        }
    }
    
    /**
     * Die Queue bietet einen Zugriff auf den Tail. Außerdem überschreib sie clear, damit die DataBlocks ggf. ihre
     * Auslagerungsdateien löschen können.
     */
    private static class DataBlockQueue extends ConcurrentLinkedQueue<DataBlock> {
        private DataBlock tail = null;
        
        @Override
        public boolean offer(DataBlock e) {
            tail = e;
            return super.offer(e);
        }
        
        public DataBlock getTail() {
            return tail;
        }
        
        @Override
        public void clear() {
            DataBlock block;
            while ((block = poll()) != null) {
                block.dispose();
            }
            super.clear();     // pro Forma. Eigentlich nicht notwendig, super.clear() poll() aufruft, bis null
        }
    }
    
    private DataBlockQueue queue;
    private volatile long currentSize;
    private final long capacity;

    private int dataBlockSize;
    
    private final static long MEMORYTHRESHOLD = 1024*1024*300; // wenn weniger als soviel Speicher frei, dann auslagern   
    
    public BigBuffer(long capacity, int dataBlockSize) {
        this.capacity = capacity;
        this.dataBlockSize = dataBlockSize;
        currentSize = 0;
        queue = createQueue();
    }
    
    private DataBlockQueue createQueue() {
        return new DataBlockQueue();
    }
    
    private boolean notEnoughMemory() {
        return Runtime.getRuntime().freeMemory() < MEMORYTHRESHOLD;
    }
    
    /**
     * Fügt len Daten aus srcBuffer ab offset
     * am Tail des Puffers an.
     * @param buffer Quelle der Daten
     * @param offset offset innerhalb des srcBuffer
     * @param len Anzahl bytes aus dem srcBuffer
     * @throws IOException 
     */
    public void offer(byte [] srcBuffer, int offset, int len) throws IOException  {
        if (len + offset > srcBuffer.length) { throw new IllegalArgumentException(String.format("%d (len) + %d (offset) exceeding buffer's length (%d)", len, offset, srcBuffer.length)); }
        if (len > availableCapacity()) { throw new IllegalArgumentException(String.format("Data too large. Remaining capacity: %d bytes, data length: %d bytes", availableCapacity(), len)); }
        
        int resultLen = 0;
        int usingLen = len;
        
        do {
            DataBlock block = queue.getTail();
            if (block == null || block.isCompletelyFilled()) {  // war die queue noch leer oder ist der Block schon gefüllt
                
                if (DataBlock.isExternalizeable(block) && notEnoughMemory()) {
                    block.externalize();
                }
                
                queue.offer(block = new DataBlock(dataBlockSize));  // neuen Block erzeugen und anhängen
            }
            
            int actualLen = block.offer(srcBuffer, offset, usingLen);
            resultLen += actualLen;
            offset    += actualLen;  // sollte es einen weiteren Schleifendurchgang geben, muss der offset verschoben werden
            usingLen  -= actualLen;  // und die noch zu schreibende Datenlänge entspr. verringert
        } while (resultLen < len);
        
        currentSize += resultLen;
    }
    
    /**
     * Holt len Daten vom Kopf des Puffers und schreibt sie in den destBuffer ab dem gegebenen offset.
     * Die tatsächliche Länge kann niedriger sein als len, wenn weniger Daten zur Verfügung stehen. Kann auch 0 sein.
     * Die Anzahl gelesener Bytes wird zurückgeliefert.<br>
     * Liefert -1, wenn beim Lesen der Daten ein Fehler auftrat.
     */
    public int read(byte [] destBuffer, int offset, int len) throws IOException {
        if (len + offset > destBuffer.length) { throw new IllegalArgumentException(String.format("%d (len) + %d (offset) exceeding buffer's length (%d)", len, offset, destBuffer.length)); }
        
        int resultLen = 0;
        int usingLen = len;
        
        do {
            DataBlock block = queue.peek();      // den Kopf holen, aber noch in der Schlange lassen, da er beim nächsten read evtl. noch gebraucht wird
            if (block == null) break;            // gibt es keine weiteren Daten, dann Ende
        
            int actualLen = block.read(destBuffer, offset, usingLen);
            
            resultLen += actualLen;
            offset    += actualLen; // sollte es einen weiteren Schleifendurchgang geben, muss der offset verschoben
            usingLen  -= actualLen; // und die noch zu lesende Datenmenge entspr. verringert werden
            currentSize -= resultLen;
            
            if (block.beenReadCompletely()) {  // wenn der Block leergelesen ist, aus der Schlange entfernen
                queue.poll();
                block.dispose();
            } else {
                break;     // wenn der Block nicht leergelesen ist, dann stehen offenbar keine weiteren Daten mehr zur Verfügung und wir müssen ausbrechen
            }
        } while (resultLen < len);  // konnte der Datenblock noch nicht soviel Daten zur Verfügung stellen wie gewünscht, dann neuer Durchgang mit dem nächsten Block

        return resultLen;
    }
    
    public void clear() {
        currentSize = 0;
        Queue<?> oldQueue = queue;

        // da das Leeren der Queue etwas teuer ist, wird eine neue,
        // leere Queue erzeugt und die alte im Hintergrund geleert. Das kann man nicht dem
        // GarbageCollector überlassen, da evtl. ausgelagerte Blöcke auch von der Harddisk
        // entfernt werden müssen.
        queue = createQueue();
        
        new Thread(() -> oldQueue.clear()).start();
    }

    public long capacity() {
        return capacity;
    }

    public long availableCapacity() {
        return capacity - currentSize;
    }

    /**
     * Verwirft die gegebene Anzahl Bytes, ohne sie zu lesen.
     * Liefert zurück, wieviel Bytes tatsächlich verworfen wurden. Das kann weniger sein.
     */
    public long discard(long bytes) throws IOException {
        long resultLen = 0;
        long usingLen = bytes;
        
        do {
            DataBlock block = queue.peek();      // den Kopf holen, aber noch in der Schlange lassen, da er beim nächsten read evtl. noch gebraucht wird
            if (block == null) break;            // gibt es keine weiteren Daten, dann Ende
        
            long actualLen = block.discard(usingLen);
            resultLen += actualLen;
            usingLen  -= actualLen;
            currentSize -= resultLen;
            
            if (block.beenReadCompletely()) {  // wenn der Block leergelesen ist, aus der Schlange entfernen
                queue.poll();
                block.dispose();  // ggf. die Auslagerungsdatei löschen.
            } else {
                break;     // wenn der Block nicht leergelesen ist, dann stehen offenbar keine weiteren Daten mehr zur Verfügung und wir müssen ausbrechen
            }
        } while (resultLen < bytes);  // konnte der Datenblock noch nicht soviel Daten zur Verfügung stellen wie gewünscht, dann neuer Durchgang mit dem nächsten Block

        return resultLen;
    }

    public long size() {
        return currentSize;
    }

}
 
I

insert2020

Ganz so trivial ist es dann doch nicht, da der Buffer bei fast jedem Lesevorgang geflusht werden müsste:
Java:
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Random;

public class MyBuffer {
	private static final int RAM_BUFFER_SIZE = 1024 * 1024 * 1024;
	private BufferedOutputStream buffer1 = null;
	private BufferedInputStream buffer2 = null;
	private boolean hasInput = false;

	public MyBuffer() throws IOException {
		buffer1 = new BufferedOutputStream(new FileOutputStream(new File("my_backup_file.txt")), RAM_BUFFER_SIZE);
		buffer2 = new BufferedInputStream(new FileInputStream(new File("my_backup_file.txt")), 1024);
	}

	public synchronized void write(byte b) throws IOException {
		buffer1.write(b);
		hasInput = true;
	}

	public synchronized int read() throws IOException {
		if (hasInput) {
			buffer1.flush();
			hasInput = false;
		}
		return buffer2.read();
	}

	public static void main(String[] args) throws IOException {
	}
}
 
mrBrown

mrBrown

Ja, da warst Du etwas schneller. Ich habe aber sogar die "optimierte" Variante mal herunter geschrieben. Da kann man dann einmalig Speicher reservieren und dann ganz ohne GC weiter arbeiten. Dann ist es sogar denkbar, andere GC zu probieren. Wenn man sicher ist, dass man den GC nicht braucht, dann kann man die Variante ohne GC versuchen und verifizieren, dass die Annahme richtig ist. Minimaler Speicherzuwachs ist ggf. vertretbar, da ja so Aufnahmen nur begrenzte Zeit laufen dürften (Annahme von meiner Seite - müsste verifiziert werden).
Ja, das wäre das erste, was man da machen würde. Queue mit fester Größe und Pool für etwaige Arrays – hatte ich sogar erst da stehen :D

Japp, so hab ich's letztlich gemacht. Etwas feiner muß man es dann noch betrachten: die Blöcke, die das Audiointerface liefert, sind meist sehr kurz, ca. 1 Sekunde. Ich sammle mehrere solcher Blöcke zusammen in einen größeren Datenblock. Sobald der voll ist, kann er ggf. ausgelagert werden.
Das "zusammenfassen" ist der erste Schritt bei mir, eben Daten solange "vorpuffern", bis X MB oder X Sekunden da sind :)
 
G

Gelöschtes Mitglied 9001

Ja, das wäre das erste, was man da machen würde. Queue mit fester Größe und Pool für etwaige Arrays – hatte ich sogar erst da stehen :D

Das "zusammenfassen" ist der erste Schritt bei mir, eben Daten solange "vorpuffern", bis X MB oder X Sekunden da sind :)
Verstehe. Die vorgepufferten Daten sollten im besten Fall freilich aber auch schon zum Auslesen zu Verfügung stehen. Deshalb lege ich "unfertige" Datenblöcke auch schon in die Queue.
 
mrBrown

mrBrown

Verstehe. Die vorgepufferten Daten sollten im besten Fall freilich aber auch schon zum Auslesen zu Verfügung stehen. Deshalb lege ich "unfertige" Datenblöcke auch schon in die Queue.
In der aktuellen Implementation gibts da wenn ich's richtig sehe Probleme, die unfertigen Blöcke sind nicht threadsafe und auch bei nur einem Thread gehen damit Daten verloren. Teste einfach mal mit halben Block schreiben, dann lesen, dann wieder halben Block schreiben.
 
G

Gelöschtes Mitglied 9001

In der aktuellen Implementation gibts da wenn ich's richtig sehe Probleme, die unfertigen Blöcke sind nicht threadsafe und auch bei nur einem Thread gehen damit Daten verloren. Teste einfach mal mit halben Block schreiben, dann lesen, dann wieder halben Block schreiben.
Ich habe vorhin gerade eine längere Tonaufnahme gemacht und den Client dabei unregelmäßig zufällig bei seinen Anfragen verzögert, um eine schlechte Verbindung zu simulieren. Die Aufnahme gelang einwandfrei.
Auch ein Test mit Server und Client auf ein und demselben Computer ohne simulierte Verzögerung gelang problemlos.
 
Thema: 

Riesenringpuffer

Passende Stellenanzeigen aus deiner Region:
Anzeige

Neue Themen

Anzeige

Anzeige
Oben