Software-Experimente mit Linux

Musikwiedergabe über PC und Mac
Buschel
Aktiver Hörer
Beiträge: 846
Registriert: 12.12.2013, 20:12
Wohnort: Raum Karlsruhe

Beitrag von Buschel »

Hallo nochmal,

beim heutigen Updaten auf die neuere Version von resample_soxr (mit "--precision" Option) bin ich wieder über meine lokalen Änderungen an playhrt gestolpert. Das patch regelt die Variable extra_bps nach, um eine schwankende Clock auszugleichen. In meinen Setup funktioniert playhrt ansonsten nicht (siehe auch meine vorherigen Beiträge hier im Thread). Ich poste den Code hier unaufgeräumt und ohne Garantie. Vielleicht hat jemand ähnliche Probleme.

Code: Alles auswählen

diff frank_l-frankl_stereo-5321f33866ac/src/playhrt.c frankl_stereo_0.8.2/src/playhrt.c
34a35
>   fprintf(stderr, ", with PD control for clock deviation");
263c264
< "  lopps occur their number will become smaller with lower verbosity level\n"
---
> "  loops occur their number will become smaller with lower verbosity level\n"
272a274,275
> //AB    int sfd, s, moreinput, err, verbose, nrchannels, startcount, sumavg,
> //AB        stripped, innetbufsize, dobufstats, countdelay, maxbad;
274,276c277,281
<         stripped, innetbufsize, dobufstats, countdelay, maxbad;
<     long blen, hlen, ilen, olen, extra, loopspersec, nrdelays, sleep,
<          nsec, count, wnext, badloops, badreads, readmissing, avgav, checkav;
---
>         stripped, innetbufsize, dobufstats;
> //AB    long blen, hlen, ilen, olen, extra, loopspersec, nrdelays, sleep,
> //AB         nsec, count, wnext, badloops, badreads, readmissing, avgav, checkav;
> 	long blen, hlen, ilen, olen, extra, loopspersec, nrdelays, sleep,
>          nsec, count, wnext, badloops, badreads, readmissing, avgav;
281c286,287
<     double looperr, off, extraerr, extrabps, morebps;
---
> //AB    double looperr, off, extraerr, extrabps, morebps;
> 	double looperr, off, extraerr, extrabps;
292,293c298,305
<     double checktime;
<     long corr;
---
> //AB    double checktime;
> //AB    long corr;
> 
> /* AB */
> 	int ab_err = 0;
> 	int ab_err_d = 0;
> 	int ab_err_alt = 0;
> /* AB */
349c361
<     maxbad = 4;
---
> //AB    maxbad = 4;
352c364
<     corr = 0;
---
> //AB    corr = 0;
356c368
<     countdelay = 1;
---
> //AB    countdelay = 1;
421c433,434
<           maxbad = atoi(optarg);
---
> //AB          maxbad = atoi(optarg);
> 			fprintf(stderr, "playhrt: --max-bad-reads was ignored!\n");
446c459,460
<           countdelay = 0;
---
> //AB          countdelay = 0;
> 			  fprintf(stderr, "playhrt: --no-delay-stats was ignored!\n");
455c469
<           fprintf(stderr, ")\n");
---
>           fprintf(stderr, ", with PD control for clock deviation)\n");
794c808
< 	  refreshmem(iptr, s);
---
> 	  	  refreshmem(iptr, s);
829c843
< 	  refreshmem(iptr, s);
---
> 	  	  refreshmem(iptr, s);
840a855,856
> 	 /* AB: playback will start when at least 10 ms are buffered */
> 	 /* AB: startcount = (int)((1.0*10000000)/nsec+1.0); */
850d865
<       checktime = 0;
853a869,874
> 		  if (verbose)
> 			if (count == startcount) {
> 				clock_gettime(CLOCK_MONOTONIC, &mtimecheck);
> 				fprintf(stderr, "playhrt: Start playback time (%ld sec %ld nsec).\n",
>                          mtime.tv_sec, mtime.tv_nsec);
> 			}
876,902c897,910
<                   if (verbose > 1)
<                       fprintf(stderr, "playhrt: Average available buffer: %ld (%ld sec %ld nsec).\n", avgav/16, mtime.tv_sec, mtime.tv_nsec);
<                   if (checktime == 0.0 && count > startcount+30000) {
<                        checktime = 1.0*mtime.tv_sec + mtime.tv_nsec/1000000000.0;
<                        checkav = avgav/16;
<                        corr = 1;
<                   }
<                   if (corr && avgav/16 > checkav + hwbufsize*3/10) {
<                        extrabps += (double)((avgav/16-checkav)*bytesperframe)/(mtime.tv_sec*1.0+mtime.tv_nsec/1000000000.0-checktime);
<                        extraerr = 1.0*bytesperframe*rate;
<                        extraerr = extraerr/(extraerr+extrabps);
<                        nsec = (int) (1000000000*extraerr/loopspersec);
<                        corr = 0;
<                        fprintf(stderr, "playhrt: Avoiding buffer underrun! Please use option \n"
<                                "      --extra-bytes-per-second=%d\n"
<                                "on next call.\n", (int)extrabps);
<                   }
<                   if (corr && avgav/16 < checkav - hwbufsize*3/10) {
<                        extrabps += (double)((avgav/16-checkav)*bytesperframe)/(mtime.tv_sec*1.0+mtime.tv_nsec/1000000000.0-checktime);
<                        extraerr = 1.0*bytesperframe*rate;
<                        extraerr = extraerr/(extraerr+extrabps);
<                        nsec = (int) (1000000000*extraerr/loopspersec);
<                        corr = 0;
<                        fprintf(stderr, "playhrt: Avoiding buffer overrun! Please use option \n"
<                                "      --extra-bytes-per-second=%d\n"
<                                "on next call.\n", (int)extrabps);
<                   }
---
> 				  /* implement PD-controller, target = hwbufsize/2 */
> 				  ab_err = avgav/16 - hwbufsize/2;
> 				  ab_err_d = ab_err - ab_err_alt;
> 				  ab_err_alt = ab_err;
> 				  extrabps = (double)(ab_err*0.4 + ab_err_d*0.1);
> 
> 				  /* calculated required sleep time for adapted fill-rate */
> 				  extraerr = 1.0*bytesperframe*rate;
>                   extraerr = extraerr/(extraerr+extrabps);
>                   nsec = (int) (1000000000*extraerr/loopspersec);
> 
> 				  if (verbose > 1)
> 				  	fprintf(stderr, "playhrt: At %ld sec. avail: %5ld e: %4d ed: %4d extrabps: %4d\n", 
> 						mtime.tv_sec, avgav/16, ab_err, ab_err_d, (int)extrabps);
919,921d926
< 
< 
<           /* we refresh the new data before and directly after the  sleep before commiting */
923,933d927
< 
<           /* debug:  check that we really sleep to some time in the future */
<           if (countdelay) {
<             clock_gettime(CLOCK_MONOTONIC, &mtimecheck);
<             if (mtimecheck.tv_sec > mtime.tv_sec || (mtimecheck.tv_sec == mtime.tv_sec && mtimecheck.tv_nsec > mtime.tv_nsec))
<                 nrdelays += 1;
<           }
<           if (verbose > 1 && nrdelays > 0 && count % 4096 == 0) {
<               fprintf(stderr, "playhrt: Number of delayed loops: %ld (%ld sec %ld nsec).\n", nrdelays, mtime.tv_sec, mtime.tv_nsec);
<           }
< 
935c929
< 	  refreshmem(iptr, s);
---
> 	      refreshmem(iptr, s);
937,949d930
<           if (s < 0) {
<               fprintf(stderr, "playhrt: Read error.\n");
<               exit(22);
<           } else if (s < ilen) {
<               badreads++;
<               readmissing += (ilen-s);
<               if (verbose)
<                   fprintf(stderr, "playhrt: Bad read, %ld bytes missing at %ld.%ld.\n", (ilen-s), mtime.tv_sec, mtime.tv_nsec);
<               if (badreads >= maxbad) {
<                   fprintf(stderr, "playhrt: Had %d bad reads . . . exiting.\n", maxbad);
<                   break;
<               }
<           }
961,967d941
<         if (corr) {
<             morebps = (double)((avgav/16-checkav)*bytesperframe)/(mtime.tv_sec*1.0+mtime.tv_nsec/1000000000.0-checktime);
<             if (morebps >= 1.0 || morebps <= -1.0)
<                 fprintf(stderr, "playhrt: Suggesting option \n"
<                              "      --extra-bytes-per-second=%d\n"
<                              "on future calls.\n", (int)(extrabps+morebps));
<         }
Nur in frankl_stereo_0.8.2/src: version.h.
Grüße,
Andree

PS: Ein anderes patch, das ich seit Jahren benutze, ist ein Fix für brutefir, um bei ALSA under-/overflow auch das richtige ALSA device anzuzeigen. Der Fix stammt ursprünglich hier aus dem Forum von Tobias (Pittiplatsch).

Code: Alles auswählen

diff brutefir-1.0o-original/bfio_alsa.c brutefir-1.0o/bfio_alsa.c
437c437
<     fd2as[pollfd.fd].device = settings->device;
---
>     fd2as[pollfd.fd].device = estrdup(settings->device);

diff brutefir-1.0o-original/brutefir.c brutefir-1.0o/brutefir.c
2c2
<  * (c) Copyright 2001 - 2006, 2009, 2013, 2016 -- Anders Torger
---
>  * (c) Copyright 2001 - 2006, 2009, 2013, 2016, 2017 -- Anders Torger
22c22
< BruteFIR v1.0o (November 2016)                                \
---
> BruteFIR v1.0o+ (May 2017)    
Bild
Daihedz
Aktiver Hörer
Beiträge: 750
Registriert: 25.06.2010, 15:09

Playhrt Manieren im Umgang mit kleinen Puffern beibringen ...

Beitrag von Daihedz »

Ausgangslage:

Mit der eigentlich genialen Funktionsweise von playhrt, den Speicher der Soundkarte in kurzen und genauen Zeitintervallen sequenziell und häppchenweise mit den gerade benötigten Audiodaten zu befüllen, wird nebst der Clock der Soundkarte eine zweite Clock, nämlich dieser Gebertakt von playhrt, in die Übertragungskette eingefügt. Und damit wird gleich die Problematik zweier asynchroner Clocks mit installiert: Da beide Clocks nicht perfekt synchronisiert sein können, kommt es früher oder später zu over- oder underruns.

Damit die asynchronen Clocks nicht gleich zu xrun’s führen, muss mittels des Parameters –extra-bytes nachgebessert werden: Mit --extra-bytes-per-second=+/-nnn wird playhrt angewiesen, statisch und steuernd die Befüllung der Speicher pro Zeiteinheit anzupassen. Näheres dazu gibt es in den Hilfetexten von playhrt.

Die Anpassung mit --extra-bytes-per-second jedoch funktioniert nie ganz exakt. Denn mit --extra-bytes-per-second kann bloss eine Annäherung an die präzise erforderliche Pufferbefüllung erreicht werden. Deshalb kommt es trotz einer noch so sorgfältigen Bemessung von --extra-bytes-per-second unweigerlich zu xruns. Lediglich die Zeitspanne bis zum Auftretens kann damit bis zu einer gegebenen Limite verlängert werden.

Bei gleicher Befüllungs- und Entleerungsynamik laufen kleine Puffer in der Natur der Sache schneller voll, resp. leer als grosse Puffer. Deshalb ist die Zeitspanne bis zum Auftreten von xruns auch von der Puffergrösse abhängig. Und deshalb wird in der Praxis pragmatisch einfach mal die Puffergrösse erhöht, wenn xruns zu häufig auftreten.

Eleganter wäre eine dynamische Nachführungsmöglichkeit der Pufferdynamik während des Betriebs. Wird der Puffer zu voll, müsste pro Zeiteinheit kompensierend weniger befüllt werden, oder aber es müssten mehr Daten verbraucht werden. Und umgekehrt. Playhrt kann aber die Befüllung mittels --extra-bytes-per-second in den bisherigen Version nicht während des laufenden Betriebs dynamisch anpassen.

Die Problematik verschärft sich weiter, wenn mittels --mmap memory mapping erfolgt. In diesem, eigentlich zu bevorzugenden Betriebszustand bricht playhrt die Übertragung von Audiodaten gänzlich ab, wenn die Bedingung für einen xrun erfüllt sind.

Messwerte:

Als Beispiel sei ein konkretes Setup gewählt, in welchem als Ausgangs-Soundkarte eine RME HDSP 9632 verbaut ist. Beispielhaft soll nun Playhrt mit folgender Befehlszeile gestartet werden:

$ playhrt …. -b0 -c256 -e0 -k8 -M -s96000 …

Mit -b0 errechnet playhrt autonom den möglichst kleinsten Eingangspuffer, -c256 setzt den HW-Puffer der Soundkarte auf 256Byte, -e0 ist gleichbedeutend mit --extra-bytes-per-second=0, -k8 definiert die Anzahl Audiokanäle und -s96000 definiert die Rate. Diese Befehlszeile mit b0, d.h. ohne extra-bytes, führt zum sofortigen Abbruch.

Das weitere Experimentieren mit den extra-bytes legt offen, dass auf diesem System zwischen -e-135 und -e136 der kritische Umschlagspunkt liegt:

$ playhrt …. -b-135 -c256 -e-135 -k8 -M -s96000 …
befüllt die Puffer wesentlich schneller, als sie verbraucht werden, und erwirkt aufrund der Überfüllung einen Abbruch der Wiedergabe schon nach 17 Sekunden.

$ playhrt …. -b-136 -c256 -e-136 -k8 -M -s96000 …
befüllt die Puffer bloss ein klein wenig zu langsam, als sie verbraucht werden, und erwirkt aufgrund der Entleerung einen Abbruch der Wiedergabe nach 758 Sekunden.

Diese Werte sind nicht genau reproduzierbar. Denn der playhrt taktende Quarz auf dem Motherboard und der Quarz auf der Soundkarte driften nach jeweils eigener Dynamik erwartungsgemäss hin- und her. Unter der Annahme, dass das System dennoch einigermassen stabil bleibt, wäre somit der Sweet-Spot für die extra-bytes mit -136 gefunden.

Die einstellbare HW-Puffergrösse dieser spezifischen Soundkarte liegt nun zwischen 256Bytes und 16384Bytes. Unter der Annahme einer linearen Beziehung zwischen der Funktionsdauer und der HW-Puffergrösse lässt sich nun die stabile Funktionsdauer entsprechend und approximativ wie folgt erhöhen:

256Bytes - 12 Minuten
1024Bytes - 48 Minuten
4096Bytes - 3 Stunden
16384Bytes - 12 Stunden

Pragmatisch wird nun in den meisten Fällen wohl eine Einstellung des HW-Puffers zwischen 2048Bytes und 8192 gewählt werden. Passt schon ... Aber ganz befriedigend ist diese Schicksalsergebenheit dennoch nicht, und insbesondere dann nicht, wenn, aus welchem Grund auch immer, kleine Puffer eingestellt werden sollten.

Workaround - Die RME, ihr Treiber, die /proc/asound/... pseudofiles und ein kleines skript :

Zu Hilfe kommt nun eine Einstellmöglichkeit des Linux ALSA-Treibers der RME HDSP 9632: Der Treiber lässt eine dynamische Variation der ClockRate, als DDS bezeichnet, um theoretische +-5000Hz zu (so wie der Windows-Treiber auch). Aber mit ALSA bloss theoretisch. Denn der Treiber ist offenbar fehlerhaft implementiert, weswegen leider ausschliesslich positive geradzahlige Werte umgesetzt werden. Die Eingabe negativer Werte verursacht stattdessen ohne weitere Konsequenzen eine Fehlermeldung.

Im Testsystem erfolgt die Einstellung der DDS mittels amixer unter numid=30. Somit kann mittels
$ amixer -c0 cset numid=30 0
und
$ amixer -c0 cset numid=30 2
nach Bedarf dynamisch die Clock der Soundkarte um je 2Hz hinauf- und zurückgeschaltet werden.

Eine leicht höhere Clockrate bewirkt logischerweise einen leicht höheren Datenverbrauch, und somit eine etwas intensivere Entleerung des HW-Speichers.

Für den Aufbau einer Regelung mit Hysterese muss der dynamisch aktuelle Speicherinhalt bekannt sein. Die Stell- und Regelgrössen für diese Regelung finden sich in den Pseudodateien unter

/proc/asound/card0/pcm0p/sub0/hw_params
buffer_size: 256
und
/proc/asound/card0/pcm0p/sub0/status
avail: nnn

Unmittelbar zu Beginn der Aktivität von Playhrt ist ${avail} leicht grösser als ${buffer_size}/2. Im laufenden Betrieb dann vergrössert oder verkleinert sich ${avail} in Abhängigkeit von den eingestellten extra-bytes.

Playhrt bricht ab, wenn
${avail} = ${buffer_size}
oder
${avail} <= ${buffer_size}/2

Anders herum: Damit playhrt nicht abbricht, muss
${buffer_size}/2 < ${avail} < ${buffer_size}
betragen.

In Kenntnis dieser Werte kann nun der Füllzustand des HW-Puffers dynamisch über den Datenverbrauch (mittels Umschaltung der DDS ) geregelt und in einem funktionalen Bereich behalten werden.
Im obigen Setup wurden die Grenzwerte für die Hysterese auf
0.6*${buffer_size} < ${avail} < 0.85*${buffer_size}
eingestellt.

Ein bash-script fragt in einer Schleife alle 10 Sekunden den Wert von ${avail} ab und stellt gemäss den Grenzwerten der Hysterese bei Bedarf die DDS nach:

Bei
${avail} > 0.85*${buffer_size} wird die DDS auf 2 geschaltet
-->> Der Pufferstand sinkt in der Folge ganz langsam und stetig

Bei
${avail} < 0.6*${buffer_size} wird die DDS auf 0 geschaltet
-->> Der Pufferstand steigt in der Folge angsam und stetig an

Somit wird ${avail} im funktionalen Bereich gehalten.

--extra-bytes-per-second wird sinnvollerweise dergestalt eingestellt, dass die Puffer der Soundkarte ohne die Regelung laufend überfüllt würden. Im Testsystem wurde mit einer kleinen Sicherheitsmarge gegenüber dem experimentell erhobenen Grenzwert bei -135 eine etwas intensivere Befüllung mit einem Wert von -130 eingestellt.

Resultat / Ausblick:

Der Regelkreis funktioniert bestens. Das System läuft dank dieses kleinen Skripts selbst mit dem kleinsten, einstellbaren HW-Puffer von 256Byte beliebig lang stabil.

Ein Leider noch, zuletzt: Der beschriebene Lösungsansatz mittels DDS ist ausschliesslich Systemen vorbehalten, in welchen RME-Soundkarten verbaut sind. Eine universelle Anwendbarkeit, d.h. ohne DDS, würde dadurch erschlossen, wenn playhrt die (externe) dynamische Nachführung der extra-bytes zulassen würde. Besser noch - möglicherweise wäre in einer künftigen Version von playhrt gleich die Implementation einer internen Nachführung denkbar, analog zur beschriebenen externen Lösung.
Bild
Buschel
Aktiver Hörer
Beiträge: 846
Registriert: 12.12.2013, 20:12
Wohnort: Raum Karlsruhe

Beitrag von Buschel »

Hallo,

das Problem von asynchronen Clocks verfolgt mich auch schon eine Weile. Der playhrt-Patch aus meinem vorherigen Beitrag in diesem Thread regelt extra-bytes laufend nach und folgt (bei meinem PC auftretenden) Schwankungen der Clock. Geregelt wird auch auf einen gewünschten Pufferfüllgrad hin. Das funktioniert auch ohne RME oder zusätzliches Skript, und unabhängig davon, ob die Clock zu schnell oder zu langsam läuft. Erst mit diesem Patch läuft playhrt bei mir robust.

Grüße,
Andree
Bild
Daihedz
Aktiver Hörer
Beiträge: 750
Registriert: 25.06.2010, 15:09

Beitrag von Daihedz »

Hallo Andree

Du präsentierst genau sowas, wie ich es mir im regulären Source-Code von playhrt 0.9 wünschen würde. Erst damit würde playhrt eigentlich robust funktional werden. Solange das nicht der Fall ist, wird playhrt eher eine Nischenlösung bleiben, für unabschreckbare, bastelfreudige und stoische Selbsthelfer ...

Ich verstehe meinen Workaround in diesem Sinne als doppelt hölzerner Umweg. Doppelt, da ich C nicht beherrsche. Deine Lösung ist natürlich wesentlich eleganter und direkter, und eigentlich machen beide Ansätze in etwa dasselbe, ausgehend vom Monitoring des HW-Speichers:

...
> /* implement PD-controller, target = hwbufsize/2 */
> ab_err = avgav/16 - hwbufsize/2;
> ab_err_d = ab_err - ab_err_alt;
> ab_err_alt = ab_err;
> extrabps = (double)(ab_err*0.4 + ab_err_d*0.1);
...

Superschön!

Aber da ist noch ein kleines Problem, welches Du mit Deinem Garantie-Dysclaimer vorwegnimmst: Nachteil Deiner Lösung (und wohl jeder Patch-Lösung) ist, dass sie an eine bestimmte Version (0.8.2) von playhrt gebunden ist? Oder habe ich etwas falsch verstanden? Ich schaffe es jedenfalls nicht, an eine 0.8.2 heranzukommen. Und wenn ich Deinen Patch über die 0.8. laufen lassen möchte, dann antwortet mir

$ cat /home/privat/playhrt_buschelpatch.txt | patch -p0 --dry-run
schönde mit
patch: **** Only garbage was found in the patch input.

Das war's dann schon.

Fazit: Ich würde nur allzu gerne Deine Lösung patchen, schaffe es aber nicht. Oder noch lieber hätte ich in näherer Zukunft von Frank von playhrt eine 0.9 angeboten bekommen.

Ungepätschte Grüsse
Simon
Bild
Buschel
Aktiver Hörer
Beiträge: 846
Registriert: 12.12.2013, 20:12
Wohnort: Raum Karlsruhe

Beitrag von Buschel »

Gute Morgen Simon,

die Version 0.8.2 ist die von mir so benannte lokale Version (0.8.1 war ein anderer lokaler Hack in resample_soxr, den ich seit Frank´s Änderung vom 25.01.2020 nicht mehr brauche). Der Patch basiert auf Frank´s Version 0.8, die ich hier heruntergeladen habe: https://bitbucket.org/frank_l/frankl_stereo/src/master/ . Nach Entpacken landet der aktuelle Code (heute gezogen) unter dem Pfad frank_l-frankl_stereo-7f12b1796e76.

Damit jetzt alles so funktioniert wie es soll, habe ich den Patch nochmal frisch erzeugt und dann kurz getestet, ob er auch ohne Fehler appliziert werden kann.

Patch erzeugt über:

Code: Alles auswählen

diff frank_l-frankl_stereo-5321f33866ac/src/playhrt.c frankl_stereo_0.8.2/src/playhrt.c > playhrt_pd_control.patch
Patch angewendet mit:

Code: Alles auswählen

patch frank_l-frankl_stereo-7f12b1796e76/src/playhrt.c < playhrt_pd_control.patch
Der Patch selbst:

Code: Alles auswählen

34a35
>   fprintf(stderr, ", with PD control for clock deviation by Andree Buschmann");
263c264
< "  lopps occur their number will become smaller with lower verbosity level\n"
---
> "  loops occur their number will become smaller with lower verbosity level\n"
272a274,275
> //AB    int sfd, s, moreinput, err, verbose, nrchannels, startcount, sumavg,
> //AB        stripped, innetbufsize, dobufstats, countdelay, maxbad;
274,276c277,281
<         stripped, innetbufsize, dobufstats, countdelay, maxbad;
<     long blen, hlen, ilen, olen, extra, loopspersec, nrdelays, sleep,
<          nsec, count, wnext, badloops, badreads, readmissing, avgav, checkav;
---
>         stripped, innetbufsize, dobufstats;
> //AB    long blen, hlen, ilen, olen, extra, loopspersec, nrdelays, sleep,
> //AB         nsec, count, wnext, badloops, badreads, readmissing, avgav, checkav;
> 	long blen, hlen, ilen, olen, extra, loopspersec, nrdelays, sleep,
>          nsec, count, wnext, badloops, badreads, readmissing, avgav;
281c286,287
<     double looperr, off, extraerr, extrabps, morebps;
---
> //AB    double looperr, off, extraerr, extrabps, morebps;
> 	double looperr, off, extraerr, extrabps;
292,293c298,305
<     double checktime;
<     long corr;
---
> //AB    double checktime;
> //AB    long corr;
> 
> /* AB */
> 	int ab_err = 0;
> 	int ab_err_d = 0;
> 	int ab_err_alt = 0;
> /* AB */
349c361
<     maxbad = 4;
---
> //AB    maxbad = 4;
352c364
<     corr = 0;
---
> //AB    corr = 0;
356c368
<     countdelay = 1;
---
> //AB    countdelay = 1;
421c433,434
<           maxbad = atoi(optarg);
---
> //AB          maxbad = atoi(optarg);
> 			fprintf(stderr, "playhrt: --max-bad-reads was ignored!\n");
446c459,460
<           countdelay = 0;
---
> //AB          countdelay = 0;
> 			  fprintf(stderr, "playhrt: --no-delay-stats was ignored!\n");
455c469
<           fprintf(stderr, ")\n");
---
>           fprintf(stderr, ", with PD control for clock deviation)\n");
794c808
< 	  refreshmem(iptr, s);
---
> 	  	  refreshmem(iptr, s);
829c843
< 	  refreshmem(iptr, s);
---
> 	  	  refreshmem(iptr, s);
840a855,856
> 	 /* AB: playback will start when at least 10 ms are buffered */
> 	 /* AB: startcount = (int)((1.0*10000000)/nsec+1.0); */
850d865
<       checktime = 0;
853a869,874
> 		  if (verbose)
> 			if (count == startcount) {
> 				clock_gettime(CLOCK_MONOTONIC, &mtimecheck);
> 				fprintf(stderr, "playhrt: Start playback time (%ld sec %ld nsec).\n",
>                          mtime.tv_sec, mtime.tv_nsec);
> 			}
876,902c897,910
<                   if (verbose > 1)
<                       fprintf(stderr, "playhrt: Average available buffer: %ld (%ld sec %ld nsec).\n", avgav/16, mtime.tv_sec, mtime.tv_nsec);
<                   if (checktime == 0.0 && count > startcount+30000) {
<                        checktime = 1.0*mtime.tv_sec + mtime.tv_nsec/1000000000.0;
<                        checkav = avgav/16;
<                        corr = 1;
<                   }
<                   if (corr && avgav/16 > checkav + hwbufsize*3/10) {
<                        extrabps += (double)((avgav/16-checkav)*bytesperframe)/(mtime.tv_sec*1.0+mtime.tv_nsec/1000000000.0-checktime);
<                        extraerr = 1.0*bytesperframe*rate;
<                        extraerr = extraerr/(extraerr+extrabps);
<                        nsec = (int) (1000000000*extraerr/loopspersec);
<                        corr = 0;
<                        fprintf(stderr, "playhrt: Avoiding buffer underrun! Please use option \n"
<                                "      --extra-bytes-per-second=%d\n"
<                                "on next call.\n", (int)extrabps);
<                   }
<                   if (corr && avgav/16 < checkav - hwbufsize*3/10) {
<                        extrabps += (double)((avgav/16-checkav)*bytesperframe)/(mtime.tv_sec*1.0+mtime.tv_nsec/1000000000.0-checktime);
<                        extraerr = 1.0*bytesperframe*rate;
<                        extraerr = extraerr/(extraerr+extrabps);
<                        nsec = (int) (1000000000*extraerr/loopspersec);
<                        corr = 0;
<                        fprintf(stderr, "playhrt: Avoiding buffer overrun! Please use option \n"
<                                "      --extra-bytes-per-second=%d\n"
<                                "on next call.\n", (int)extrabps);
<                   }
---
> 				  /* implement PD-controller, target = hwbufsize/2 */
> 				  ab_err = avgav/16 - hwbufsize/2;
> 				  ab_err_d = ab_err - ab_err_alt;
> 				  ab_err_alt = ab_err;
> 				  extrabps = (double)(ab_err*0.4 + ab_err_d*0.1);
> 
> 				  /* calculated required sleep time for adapted fill-rate */
> 				  extraerr = 1.0*bytesperframe*rate;
>                   extraerr = extraerr/(extraerr+extrabps);
>                   nsec = (int) (1000000000*extraerr/loopspersec);
> 
> 				  if (verbose > 1)
> 				  	fprintf(stderr, "playhrt: At %ld sec. avail: %5ld e: %4d ed: %4d extrabps: %4d\n", 
> 						mtime.tv_sec, avgav/16, ab_err, ab_err_d, (int)extrabps);
919,921d926
< 
< 
<           /* we refresh the new data before and directly after the  sleep before commiting */
923,933d927
< 
<           /* debug:  check that we really sleep to some time in the future */
<           if (countdelay) {
<             clock_gettime(CLOCK_MONOTONIC, &mtimecheck);
<             if (mtimecheck.tv_sec > mtime.tv_sec || (mtimecheck.tv_sec == mtime.tv_sec && mtimecheck.tv_nsec > mtime.tv_nsec))
<                 nrdelays += 1;
<           }
<           if (verbose > 1 && nrdelays > 0 && count % 4096 == 0) {
<               fprintf(stderr, "playhrt: Number of delayed loops: %ld (%ld sec %ld nsec).\n", nrdelays, mtime.tv_sec, mtime.tv_nsec);
<           }
< 
935c929
< 	  refreshmem(iptr, s);
---
> 	      refreshmem(iptr, s);
937,949d930
<           if (s < 0) {
<               fprintf(stderr, "playhrt: Read error.\n");
<               exit(22);
<           } else if (s < ilen) {
<               badreads++;
<               readmissing += (ilen-s);
<               if (verbose)
<                   fprintf(stderr, "playhrt: Bad read, %ld bytes missing at %ld.%ld.\n", (ilen-s), mtime.tv_sec, mtime.tv_nsec);
<               if (badreads >= maxbad) {
<                   fprintf(stderr, "playhrt: Had %d bad reads . . . exiting.\n", maxbad);
<                   break;
<               }
<           }
961,967d941
<         if (corr) {
<             morebps = (double)((avgav/16-checkav)*bytesperframe)/(mtime.tv_sec*1.0+mtime.tv_nsec/1000000000.0-checktime);
<             if (morebps >= 1.0 || morebps <= -1.0)
<                 fprintf(stderr, "playhrt: Suggesting option \n"
<                              "      --extra-bytes-per-second=%d\n"
<                              "on future calls.\n", (int)(extrabps+morebps));
<         }
ich hoffe jetzt funktioniert das patchen bei dir und du kannst ausprobieren.

Viele Grüße,
Andree
Bild
Daihedz
Aktiver Hörer
Beiträge: 750
Registriert: 25.06.2010, 15:09

Beitrag von Daihedz »

Hallo Andree
Buschel hat geschrieben:
23.08.2020, 12:24
... ich hoffe jetzt funktioniert das patchen bei dir und du kannst ausprobieren. ...
Das Prinzip Hoffnung hat diesmal gewirkt ... und eröffnet ein neues Spielfeld!

NeuerTagNeuesGlück-Grüsse
Simon
Bild
frankl
Aktiver Hörer
Beiträge: 422
Registriert: 20.01.2013, 01:43
Wohnort: Aachen

Beitrag von frankl »

Daihedz hat geschrieben:
22.08.2020, 22:55

Messwerte:

Als Beispiel sei ein konkretes Setup gewählt, in welchem als Ausgangs-Soundkarte eine RME HDSP 9632 verbaut ist. Beispielhaft soll nun Playhrt mit folgender Befehlszeile gestartet werden:

$ playhrt …. -b0 -c256 -e0 -k8 -M -s96000 …

Mit -b0 errechnet playhrt autonom den möglichst kleinsten Eingangspuffer, -c256 setzt den HW-Puffer der Soundkarte auf 256Byte, -e0 ist gleichbedeutend mit --extra-bytes-per-second=0, -k8 definiert die Anzahl Audiokanäle und -s96000 definiert die Rate. Diese Befehlszeile mit b0, d.h. ohne extra-bytes, führt zum sofortigen Abbruch.

Das weitere Experimentieren mit den extra-bytes legt offen, dass auf diesem System zwischen -e-135 und -e136 der kritische Umschlagspunkt liegt:

$ playhrt …. -b-135 -c256 -e-135 -k8 -M -s96000 …
befüllt die Puffer wesentlich schneller, als sie verbraucht werden, und erwirkt aufrund der Überfüllung einen Abbruch der Wiedergabe schon nach 17 Sekunden.

$ playhrt …. -b-136 -c256 -e-136 -k8 -M -s96000 …
befüllt die Puffer bloss ein klein wenig zu langsam, als sie verbraucht werden, und erwirkt aufgrund der Entleerung einen Abbruch der Wiedergabe nach 758 Sekunden.

Diese Werte sind nicht genau reproduzierbar. Denn der playhrt taktende Quarz auf dem Motherboard und der Quarz auf der Soundkarte driften nach jeweils eigener Dynamik erwartungsgemäss hin- und her. Unter der Annahme, dass das System dennoch einigermassen stabil bleibt, wäre somit der Sweet-Spot für die extra-bytes mit -136 gefunden.

Die einstellbare HW-Puffergrösse dieser spezifischen Soundkarte liegt nun zwischen 256Bytes und 16384Bytes. Unter der Annahme einer linearen Beziehung zwischen der Funktionsdauer und der HW-Puffergrösse lässt sich nun die stabile Funktionsdauer entsprechend und approximativ wie folgt erhöhen:

256Bytes - 12 Minuten
1024Bytes - 48 Minuten
4096Bytes - 3 Stunden
16384Bytes - 12 Stunden

Pragmatisch wird nun in den meisten Fällen wohl eine Einstellung des HW-Puffers zwischen 2048Bytes und 8192 gewählt werden. Passt schon ... Aber ganz befriedigend ist diese Schicksalsergebenheit dennoch nicht, und insbesondere dann nicht, wenn, aus welchem Grund auch immer, kleine Puffer eingestellt werden sollten.
Hallo Simon,
256 Bytes wäre nicht ausreichend als Hardware-Puffer. Die Zahl im `--hw-buffer`/`-c` Argument zählt Frames (= 1 Sample pro Kanal, bei Dir also vermutlich 8*4 = 32 Bytes). In der genannten Einstellung schreibt `playhrt` 1000 Mal je 96 Frames (=3072 Bytes) pro Sekunde in den Hardware-Buffer, also jeweils 37.5% der Puffergröße. Das ist wirklich sehr knapp bemessen. Warum hast Du ein Problem damit, einen größeren Buffer zu wählen, etwa 4096 Frames?

Deine Beispielzahlen klingen doch eigentlich nach sehr gleichmäßigen Clocks und einem sehr präzisen Timing. Die `--extra-bytes-per-second` sind übrigens eine reine Rechengröße, mit deren Hilfe das Zeitintervall zwischen den Schreibvorgängen berechnet wird. Das Argument muss keine ganze Zahl sein, Du könntest also auch mal `-e -135.9` probieren.

Bist Du der Dokumentation gefolgt und hast `playhrt` zuerst mit doppeltem `--verbose` Argument gestartet? Wenn ein over- oder under-run drohen, dann ändert `playhrt` einmal selbst den Wert von `--extra-bytes-per-second` und schreibt einen Vorschlag raus, wie man den Wert in Zukunft setzen sollte.
Nach wenigen Iterationen sollte sich so meist ein gut passender Wert finden lassen.

(Wenn gute Parameter gefunden sind, kann man das Program mit Argumenten `--no-delay-stats --no-buf-stats` und eventuell `--stripped` noch optimieren.)

Ich würde in Deinem Beispiel den `--hw-buffer` etwas größer wählen und die `--extra-bytes-per-second` so, dass der freie Teil des Hardware-Puffers ganz langsam größer wird.

Wenn ich es richtig verstanden habe, ist das Problem von Andree etwas anders: er hat eine Clock, deren Geschwindigkeit sich mit der Zeit ändert, und versucht, das durch ein dynamisches Angleichen der Schreibintervalle von `playhrt` auszugleichen. Das sollte bei Dir nicht nötig sein, da Du nur verschiedene (aber jede für sich gleichmäßig laufende) Clocks angleichen möchtest.

Ich bin mir nicht sicher, ob eine Option in `playhrt` zum ständigen Anpassen des `--extra-bytes-per-second` Wertes so sinnvoll ist. Das widerspricht eigentlich der Grundidee das Programms. Hätte damit `playhrt` noch einen Vorteil gegenüber etwa `aplay`?

Viele Grüße,
Frank
Bild
Buschel
Aktiver Hörer
Beiträge: 846
Registriert: 12.12.2013, 20:12
Wohnort: Raum Karlsruhe

Beitrag von Buschel »

Hallo Frank,

du hast mein Problem ganz richtig verstanden. Die Clock meines HTPCs unterliegt in den ersten 20 Minuten relativ starken Schwankungen, ich hatte dazu auch schon einmal Messungen gezeigt (Klick mich). Im Netz gibt es einige Berichte zu buffer underrun/overruns bei Verwendung von alsa loop devices, die ich früher auch zur Anbindung von Playern an brutefir verwendet habe, und die ebenfalls ihre Clock aus dem PC-Timer ableiten. Erst ein mitlaufendes Skript, das auf Basis des HW Pufferstands die Clock des alsa loop devices angepasst hat, hat Abhilfe geschaffen. Die derzeitige playhrt Implementierung erlaubt die einmalige Anpassung an eine ermittelte Clockabweichung. Falls die Clock sich über die Zeit verändert, läuft playhrt so lange bis es zum overrun oder underrun kommt.

Wenn ich richtig verstehe, ist die Idee von playhrt vor allem ein sehr regelmäßiger Aufruf. In meinem Patch -- ausgehend von deiner extra-bps Implementierung, aus der letztlich ein anderer Zeitabstand für die Aufrufe ermittelt wird -- habe ich eine Nachführung eingebaut. Im Ergebnis wird playhrt damit in immer leicht anderen Zeitabständen aufgerufen, um den Zielpufferstand einzuhalten.

Alternativ kann man die Nachführung natürlich auch so einpflegen, dass die Zeitabstände gleich bleiben, aber die Menge an Daten immer leicht angepasst wird. Wie du schon geschrieben hast ist extra-bps nur eine Hilfsgröße. In deinem Code benutzt du ja einen ähnlichen Ansatz, falls looperr nicht Null ist und gibst in diesem Fall ab und zu zusätzliche frames aus.

Grüße,
Andree
Bild
Daihedz
Aktiver Hörer
Beiträge: 750
Registriert: 25.06.2010, 15:09

Beitrag von Daihedz »

Hallo Frank
frankl hat geschrieben:
24.08.2020, 01:37
256 Bytes wäre nicht ausreichend als Hardware-Puffer. Die Zahl im `--hw-buffer`/`-c` Argument zählt Frames (= 1 Sample pro Kanal, bei Dir also vermutlich 8*4 = 32 Bytes). In der genannten Einstellung schreibt `playhrt` 1000 Mal je 96 Frames (=3072 Bytes) pro Sekunde in den Hardware-Buffer, also jeweils 37.5% der Puffergröße. Das ist wirklich sehr knapp bemessen. Warum hast Du ein Problem damit, einen größeren Buffer zu wählen, etwa 4096 Frames?
Ich teste gerne die Extremvarianten aus, um zu schauen, ob die Software unter allen Umständen korrekt funktioniert. In besagtem Beispiel sind die 256Bytes deshalb gewählt, da dies der minimal einstellbare Wert für die HDSP ist. Was ich nicht publiziert habe, ist der Umstand, dass playhrt in diesem Testlauf mit -n6000 aufgerufen wird. Dieses bewusst extreme Setup läuft nota bene mit meinem externen DDS-Servo prima und stabil. Ohne nicht so ganz ...
frankl hat geschrieben:
24.08.2020, 01:37
Deine Beispielzahlen klingen doch eigentlich nach sehr gleichmäßigen Clocks und einem sehr präzisen Timing. Die `--extra-bytes-per-second` sind übrigens eine reine Rechengröße, mit deren Hilfe das Zeitintervall zwischen den Schreibvorgängen berechnet wird. Das Argument muss keine ganze Zahl sein, Du könntest also auch mal `-e -135.9` probieren.
Die Deklaration von --extra-bps mittels Dezimalzahlen wäre somit eine dritte, mir bislang unbekannte Option, um das Auftreten von xruns weiter hinauszuzögern. Das inhärente Problem des suzzessiv voll- oder lerrlaufenden Speichers mit konsekutivem Absturz im Fall von -M wird jedoch auch damit nicht grundsätzlich behoben.
frankl hat geschrieben:
24.08.2020, 01:37
Bist Du der Dokumentation gefolgt und hast `playhrt` zuerst mit doppeltem `--verbose` Argument gestartet? Wenn ein over- oder under-run drohen, dann ändert `playhrt` einmal selbst den Wert von `--extra-bytes-per-second` und schreibt einen Vorschlag raus, wie man den Wert in Zukunft setzen sollte.
Nach wenigen Iterationen sollte sich so meist ein gut passender Wert finden lassen.
Selbstverständlich bin ich zunächst einmal der Dokumentation gefolgt. Aber die ausgegebenen Werte scheinen in Deinem System etwas konsistenter zu sein als in meinem, sonst hättest Du sicherlich längst etwas an der Implementation geändert: Auf meinem System geschehen Dinge wie z.B. je nach Parametrisierung eine Empfehlung, die --extra-bps auf 2147483648 (sic!) einzustellen ... Oder aber, ich starte mit 16kB HW-Puffer bei -n1500 und -e-100 (Umschlagpunkt wäre bei mir bei -135/-136) und erhalte die Empfehlung, für den nächsten Run den Wert auf 30 einzustellen. Was sicherlich in die Irre führt ... Oder wiederum Start mit denselben Parametern (16k/-n1500), ausser diesmal mit -e-50. Die Ausgabe zeigt während 23" eine stabile (!) Puffergrösse an, danach stürzt playhrt ohne weitere Empfehlung ab ... Solcherlei Erfahrungen erinnern an Blindflug mit defekter Instrumentation. Und deshalb habe ich ja erst angefangen, es lebe die Selbsthilfe, nach Alternativen zu suchen, um das Ding einigermassen stabil hinzukriegen. Und erst die händische Abfrage der /proc/asound/... - Dateien brachten etwas konzisere Hinweise, wie die --extra-bps einigermassen funktional einzustellen seien.
frankl hat geschrieben:
24.08.2020, 01:37
(Wenn gute Parameter gefunden sind, kann man das Program mit Argumenten `--no-delay-stats --no-buf-stats` und eventuell `--stripped` noch optimieren.)
Habe ich alles gemacht. Und es stürzt weiter munter ab. Deshalb sei die sarkastische Frage erlaubt: was sind "gute Parameter"? Wenn das Setup erst nach 3 Stunden, oder erst nach 24 Stunden abstürzt?
frankl hat geschrieben:
24.08.2020, 01:37
Ich würde in Deinem Beispiel den `--hw-buffer` etwas größer wählen und die `--extra-bytes-per-second` so, dass der freie Teil des Hardware-Puffers ganz langsam größer wird.
Ditto meine Replik in derselben Logik: Ja und wenn dann der freie Teil des Hardware-Puffers ganz gross geworden ist ... was passiert dann?

Fazit der Übung: Vorsichtig ausgedrückt, vermute ich, dass Nachbesserungsbedarf bestehen könnte ... Und ich verstehe auch nicht, warum überhaupt der Umweg über die --extra-bps mittels händischer steuerung vonnöten ist, wenn eine automatische Optimierung des Timings, nebst einer dynamischen Nachregelung implementiert werden könnte. Ohne dass das wesentliche Funktionsprinzip von playhrt aufgegeben werden müsste.

Es sei denn, dass zwei unterschiedliche Paradigmen aufeinanderstossen - ich gehe davon aus, dass playhrt nicht abstürzen sollte. Und vielleicht ist demgegenüber angedacht, dass es keine Rolle spielt, wenn playhrt theoretisch abstürzt, dieser Zeitpunkt jedoch jenseits einer sinnvollen Nutzungszeit erfolgen würde. Auch die Erde wird in (glaube ich) ca. 5Mia Jahren vom dannzumal roten Riesen, der aus unserer Sonne hervorgehen wird, verschluckt werden. Für uns spielt das keine so grosse Rolle ...

Extra-bytes-geschädigte Grüsse
Simon
Bild
Buschel
Aktiver Hörer
Beiträge: 846
Registriert: 12.12.2013, 20:12
Wohnort: Raum Karlsruhe

Beitrag von Buschel »

Hallo zusammen,

nach kurzer Überlegung habe ich mich jetzt entschieden, die bisherige playhrt-Implementierung zu überarbeiten und nicht auf ein kleinstmögliches Patch abzuzielen. Der verbleidende Code ist in seiner Funktion auf das -- für mich -- wesentliche zusammengeschrumpft: MMAP mit Nachführen des Pufferslevels. Weil der gesamte Code weniger Zeichen hat als die entsprechende patch Datei, poste ich unten stehend den gesamten Code. Dank an dieser Stelle nochmal an Simon (Daihedz), der mich beim Testen und Debuggen unterstützt hat

Was ist geändert:
  • Reduzierung auf MMAP mit Nachführen des Hardware Buffer Levels
  • PI-Regelung für die Nachführung (regelt schnell und ohne Restfehler)
  • Entfernen aller nicht notwendig Variablen und Parameter
  • Anpassen des Parameter-/Hilfetexts
  • Reformatierung und Einteilung in kommentierte Blöcke

Code: Alles auswählen

/*
playhrt.c                Copyright frankl 2013-2016
                         Copyright Andree Buschmann 2020

This file is part of frankl's stereo utilities and was reworked by Andree Buschmann.
See the file License.txt of the distribution and http://www.gnu.org/licenses/gpl.txt 
for license details.
*/

#include "version.h"
#include "net.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <alsa/asoundlib.h>
#include "cprefresh.h"

/* help page */
/* vim hint to remove resp. add quotes:
      s/^"\(.*\)\\n"$/\1/
      s/.*$/"\0\\n"/
*/
void usage( ) {
    fprintf(stderr,
            "playhrt (version %s of frankl's stereo utilities", VERSION);
#ifdef ALSANC
    fprintf(stderr, ", with alsa-lib patch");
#endif
    fprintf(stderr, ", reworked by Andree Buschmann");
    fprintf(stderr, ", with PI control for clock deviation");
    fprintf(stderr, ")\nUSAGE:\n");
    fprintf(stderr,
"\n"
"  playhrt [options] \n"
"\n"
"  This program reads raw(!) stereo audio data from stdin, a file or the \n"
"  network and plays it on a local (ALSA) sound device. \n"
"\n"
"  The program repeats in a given number of loops per second: reading\n"
"  a chunk of input data, preparing data for the audio driver, then it\n"
"  sleeps until a specific instant of time and after wakeup it hands data\n"
"  to the audio driver. In contrast to other player programs this is done\n"
"  with a very precise timing such that no buffers underrun or overrun and\n"
"  no reading or writing of data is blocking. Furthermore, the data are\n"
"  refreshed in RAM directly before copying them to the audio driver.\n"
"\n"
"  The Linux kernel needs the highres-timer functionality enabled (on most\n"
"  systems this is the case).\n"
"\n"
"  This reworked version only allows writes input data directly to the\n"
"  memory of the audio driver (mmap mode).\n"
"\n"
"  USAGE HINTS\n"
"  \n"
"  It is recommended to give this program a high priority and not to run\n"
"  too many other things on the same computer during playback. A high\n"
"  priority can be specified with the 'chrt' command:\n"
"\n"
"  chrt -f 70 playhrt .....\n"
"\n"
"  (Depending on the configuration of your computer you may need root\n"
"  privileges for this, in that case use 'sudo chrt -f 99 playhrt ....' \n"
"  or give 'chrt' setuid permissions.)\n"
"\n"
"  While running this program the computer should run as few other things\n"
"  as possible. In particular we recommend to generate the input data\n"
"  on a different computer and to send them via the network to 'playhrt'\n"
"  using the program 'bufhrt' which is also contained in this package. \n"
"  \n"
"  OPTIONS\n"
"\n"
"  --host=hostname, -r hostname\n"
"      the host from which to receive the data , given by name or\n"
"      ip-address.\n"
"\n"
"  --port=portnumber, -p portnumber\n"
"      the port number on the remote host from which to receive data.\n"
"\n"
"  --stdin, -S\n"
"      read data from stdin (instead of --host and --port).\n"
"\n"
"  --device=alsaname, -d alsaname\n"
"      the name of the sound device. A typical name is 'hw:0,0', maybe\n"
"      use 'aplay -l' to find out the correct numbers. It is recommended\n"
"      to use the hardware devices 'hw:...' if possible.\n"
"\n"
"  --sample-rate=intval, -s intval\n"
"      the sample rate of the audio data. Default is 44100 as on CDs.\n"
"\n"
"  --sample-format=formatstring, -f formatstring\n"
"      the format of the samples in the audio data. Currently recognised\n"
"      are 'S16_LE' (the sample format on CDs), 'S24_LE' \n"
"      (signed integer data with 24 bits packed into 32 bit words, used by\n"
"      many DACs), 'S24_3LE' (also 24 bit integers but only using 3 bytes\n"
"      per sample), 'S32_LE' (true 32 bit signed integer samples).\n"
"      Default is 'S16_LE'.\n"
"\n"
"  --number-channels=intval, -k intval\n"
"      the number of channels in the (interleaved) audio stream. The \n"
"      default is 2 (stereo).\n"
"\n"
"  --loops-per-second=intval, -n intval\n"
"      the number of loops per second in which 'playhrt' reads some\n"
"      data from the network into a buffer, sleeps until a precise\n"
"      moment and then writes a chunk of data to the sound device. \n"
"      Typical values would be 1000 or 2000. Default is 1000.\n"
"\n"
"  --non-blocking-write, -N\n"
"      write data to sound device in a non-blocking fashion. This can\n"
"      improve sound quality, but the timing must be very precise.\n"
"\n"
"  --hw-buffer=intval, -c intval\n"
"      the buffer size (number of frames) used on the sound device.\n"
"      It may be worth to experiment a bit with this,\n"
"      in particular to try some smaller values. When 'playhrt' is\n"
"      called with --verbose it should report on the range allowed by\n"
"      the device. Default is 16384 (but there are devices where this\n"
"      is not valid).\n"
" \n"
"  --in-net-buffer-size=intval, -K intval\n"
"      when reading from the network this allows to set the buffer\n"
"      size for the incoming data. This is for finetuning only, normally\n"
"      the operating system chooses sizes to guarantee constant data\n"
"      flow. The actual fill of the buffer during playback can be checked\n"
"      with 'netstat -tpn', it can be up to twice as big as the given\n"
"      intval.\n"
"\n"
"  --sleep=intval, -D intval\n"
"      causes playhrt to sleep for intval microseconds (1/1000000 sec)\n"
"      after opening the sound device and before starting playback.\n"
"      This may sometimes be useful to give other programs time to \n"
"      fill the input buffer of playhrt. Default is no sleep.\n"
"\n"
"  --verbose, -v\n"
"      print some information during startup and operation.\n"
"      This option can be given twice for more output about the auto-\n"
"      matic speed control and availability of the audio buffer.\n"
"\n"
"  --version, -V\n"
"      print information about the version of the program and abort.\n"
"\n"
"  --help, -h\n"
"      print this help page and abort.\n"
"\n"
"  EXAMPLES\n"
"\n"
"  We read from myserver on port 5123 stereo data in 32-bit integer\n"
"  format with a sample rate of 192000. We want to run 1000 loops per \n"
"  second (this is in particular a good choice for USB devices), our sound\n"
"  device is 'hw:0,0' and we want to write non-blocking to the device:\n"
"\n"
"  playhrt --host=myserver --port=5123 \\\n"
"      --loops-per-second=1000 \\\n"
"      --device=hw:0,0 --sample-rate=192000 --sample-format=S32_LE \\\n"
"      --non-blocking --verbose \n"
"\n"
"  To play a local CD quality flac file 'music.flac' you need another \n"
"  program to unpack the raw audio data. In this example we use 'sox':\n"
"\n"
"  sox musik.flac -t raw - | playhrt --stdin \\\n"
"          --loops-per-second=1000 --device=hw:0,0 --sample-rate=44100 \\\n"
"          --sample-format=S16_LE --non-blocking --verbose \n"
"\n"
"  ADJUSTING SPEED\n"
"\n"
"  This version of playhrt is automatically adjusting the speed of\n"
"  writing the data to the hardware buffer. This is done via measuring\n"
"  the space left in the hardware buffer and tuning the interval time\n"
"  until the next data write occurs. The targeted value is hw-buffer/2.\n"
"  \n"
"  The automatic adjustment is implemented as PI-control which allows\n"
"  playhrt to adjust to fixed and variable deviation of the local clock\n"
"  against the consuming clock (typically a DAC).\n"
"\n"
);
}

int main(int argc, char *argv[])
{
    int sfd, readbytes, verbose, nrchannels, startcount, sumavg, innetbufsize;
    long loopspersec, sleep, nsec, extransec, count, avgav;
    long long bytecount;
    void *iptr;
    struct timespec mtime;
    struct timespec mtimecheck;
    snd_pcm_t *pcm_handle;
    snd_pcm_hw_params_t *hwparams;
    snd_pcm_sw_params_t *swparams;
    snd_pcm_format_t format;
    char *host, *port, *pcm_name;
    int optc, nonblock, rate, bytespersample, bytesperframe;
    snd_pcm_uframes_t hwbufsize, offset, frames;
    snd_pcm_sframes_t avail;
    const snd_pcm_channel_area_t *areas;

    /* define variables and default for PI control */
    #define LOOPS_AVG 16        /* amount of averaged buffer measurement */
    #define LOOPS_CADENCE 4000  /* measure each LOOPS_CADENCE loops */
    double bufavg = 0;
    double buferr = 0;
    double buferr_i = 0;
    double Ta = 0.0;	/* will be calculated later */
    double Kp = 1.0;	/* value based on tests */
    double Ki = 0.05;	/* value based on tests */

    /**********************************************************************/
    /* read and set parameters                                            */
    /**********************************************************************/

    /* read command line options */
    static struct option longoptions[] = {
        {"host",               required_argument, 0, 'r' },
        {"port",               required_argument, 0, 'p' },
        {"stdin",              no_argument,       0, 'S' },
        {"loops-per-second",   required_argument, 0, 'n' },
        {"sample-rate",        required_argument, 0, 's' },
        {"sample-format",      required_argument, 0, 'f' },
        {"number-channels",    required_argument, 0, 'k' },
        {"hw-buffer",          required_argument, 0, 'c' },
        {"mmap",               no_argument,       0, 'M' },
        {"device",             required_argument, 0, 'd' },
        {"sleep",              required_argument, 0, 'D' },
        {"in-net-buffer-size", required_argument, 0, 'K' },
        {"non-blocking-write", no_argument,       0, 'N' },
        {"verbose",            no_argument,       0, 'v' },
        {"version",            no_argument,       0, 'V' },
        {"help",               no_argument,       0, 'h' },
        {0,                    0,                 0,  0  }
    };

    if (argc == 1) {
        usage();
        exit(0);
    }

    /* set defaults */
    host = NULL;
    port = NULL;
    loopspersec = 1000;
    rate = 44100;
    format = SND_PCM_FORMAT_S16_LE;
    bytespersample = 2;
    hwbufsize = 16384;
    pcm_name = NULL;
    sfd = -1;
    nrchannels = 2;
    extransec = 0;
    sleep = 0;
    nonblock = 0;
    innetbufsize = 0;
    verbose = 0;
    sumavg = 0;
    buferr_i = 0;
    bytecount = 0;

    /* read parameters */
    while ((optc = getopt_long(argc, argv, "r:p:Sn:s:f:k:c:Md:D:K:NvVh", longoptions, &optind)) != -1) {
        switch (optc) {
        case 'r':
            host = optarg;
          	break;
        case 'p':
          	port = optarg;
          	break;
        case 'S':
          	sfd = 0;
          	break;
        case 'n':
          	loopspersec = atoi(optarg);
          	break;
        case 's':
          	rate = atoi(optarg);
          	break;
        case 'f':
            if        (strcmp(optarg, "S16_LE" )==0) {
                format = SND_PCM_FORMAT_S16_LE;
                bytespersample = 2;
            } else if (strcmp(optarg, "S24_LE" )==0) {
                format = SND_PCM_FORMAT_S24_LE;
                bytespersample = 4;
            } else if (strcmp(optarg, "S24_3LE")==0) {
                format = SND_PCM_FORMAT_S24_3LE;
                bytespersample = 3;
            } else if (strcmp(optarg, "S32_LE" )==0) {
                format = SND_PCM_FORMAT_S32_LE;
                bytespersample = 4;
            } else {
                fprintf(stderr, "playhrt: Error. Sample format %s not recognized.\n", optarg);
                exit(1);
            }
            break;
        case 'k':
            nrchannels = atoi(optarg);
            break;
        case 'c':
            hwbufsize = atoi(optarg);
            break;
        case 'M':
            /* ignore, just kept for compatibility */
            break;
        case 'd':
            pcm_name = optarg;
            break;
        case 'D':
            sleep = atoi(optarg);
            break;
        case 'K':
            innetbufsize = atoi(optarg);
            if (innetbufsize != 0 && innetbufsize < 128)
                innetbufsize = 128;
            break;
        case 'N':
            nonblock = 1;
            break;
        case 'v':
            verbose += 1;
            break;
        case 'V':
            fprintf(stderr, "playhrt (version %s of frankl's stereo utilities", VERSION);
#ifdef ALSANC
            fprintf(stderr, ", with alsa-lib patch");
#endif
            fprintf(stderr, ", reworked by Andree Buschmann");          	
            fprintf(stderr, ", with PI control for clock deviation)\n");
            exit(0);
	    default:
            usage();
            exit(2);
        }
    }

    /**********************************************************************/
    /* calculate and check values from given parameters                   */
    /**********************************************************************/

    /* calculate some values from the parameters */
    bytesperframe = bytespersample*nrchannels;  /* bytes per frame */
    frames = rate/loopspersec;                  /* frames per loop */
    nsec = (int) (1000000000/loopspersec);      /* compute nanoseconds per loop (wrt local clock) */
    Ta = (1.0*LOOPS_CADENCE)/loopspersec;       /* delta T seconds */
    Ta = (Ki*Ta>0.2) ? 0.2/Ki : Ta;             /* limit Ta to avoid oscallation */

    /* set hwbuffer to a multiple of frames per loop (needed for mmap!) */
    hwbufsize = hwbufsize - (hwbufsize % frames);

    /* amount of loops to fill half buffer */
    startcount = hwbufsize/(2*frames);
		
    /* check some arguments and set some parameters */
    if ((host == NULL || port == NULL) && sfd < 0) {
        fprintf(stderr, "playhrt: Error. Must specify --host and --port or --stdin.\n");
        exit(3);
    }

    /**********************************************************************/
    /* show playhrt configuration                                         */
    /**********************************************************************/

    /* show configuration */
    if (verbose) {
        fprintf(stderr, "playhrt: Version %s\n", VERSION);
        fprintf(stderr, "playhrt: Using mmap access.\n");
        fprintf(stderr, "playhrt: Step size is %ld nsec.\n", nsec);
        fprintf(stderr, "playhrt: %d channels with %d bytes per sample at %d Hz\n", nrchannels, bytespersample, rate);
    }

    /**********************************************************************/
    /* setup network connection                                           */
    /**********************************************************************/

    /* setup network connection */
    if (host != NULL && port != NULL) {
        sfd = fd_net(host, port);
        if (innetbufsize != 0) {
            if (setsockopt(sfd, SOL_SOCKET, SO_RCVBUF, (void*)&innetbufsize, sizeof(int)) < 0) {
                fprintf(stderr, "playhrt: Error setting buffer size for network socket to %d.\n", innetbufsize);
                exit(4);
            }
        }
    }

    /**********************************************************************/
    /* setup sound device                                                 */
    /**********************************************************************/

    /* setup sound device */
    snd_pcm_hw_params_malloc(&hwparams);
    if (snd_pcm_open(&pcm_handle, pcm_name, SND_PCM_STREAM_PLAYBACK, 0) < 0) {
        fprintf(stderr, "playhrt: Error opening PCM device %s\n", pcm_name);
        exit(5);
    }
    if (nonblock) {
        if (snd_pcm_nonblock(pcm_handle, 1) < 0) {
            fprintf(stderr, "playhrt: Error setting non-block mode.\n");
            exit(6);
        } else if (verbose) {
            fprintf(stderr, "playhrt: Using card in non-block mode.\n");
        }
    }
    if (snd_pcm_hw_params_any(pcm_handle, hwparams) < 0) {
        fprintf(stderr, "playhrt: Error configuring this PCM device.\n");
        exit(7);
    }
    if (snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_MMAP_INTERLEAVED) < 0) {
        fprintf(stderr, "playhrt: Error setting MMAP access.\n");
        exit(8);
    }
    if (snd_pcm_hw_params_set_format(pcm_handle, hwparams, format) < 0) {
        fprintf(stderr, "playhrt: Error setting format.\n");
        exit(9);
    }
    if (snd_pcm_hw_params_set_rate(pcm_handle, hwparams, rate, 0) < 0) {
        fprintf(stderr, "playhrt: Error setting rate.\n");
        exit(10);
    }
    if (snd_pcm_hw_params_set_channels(pcm_handle, hwparams, nrchannels) < 0) {
        fprintf(stderr, "playhrt: Error setting channels to %d.\n", nrchannels);
        exit(11);
    }
    if (verbose) {
        snd_pcm_uframes_t min=1, max=100000000;
        snd_pcm_hw_params_set_buffer_size_minmax(pcm_handle, hwparams, &min, &max);
        fprintf(stderr, "playhrt: Min and max buffer size of device %ld .. %ld - ", min, max);
    }
    if (snd_pcm_hw_params_set_buffer_size(pcm_handle, hwparams, hwbufsize) < 0) {
        fprintf(stderr, "\nplayhrt: Error setting buffersize to %ld.\n", hwbufsize);
        exit(12);
    }
    snd_pcm_hw_params_get_buffer_size(hwparams, &hwbufsize);
    if (verbose) {
        fprintf(stderr, "using %ld.\n", hwbufsize);
    }
    if (snd_pcm_hw_params(pcm_handle, hwparams) < 0) {
        fprintf(stderr, "playhrt: Error setting HW params.\n");
        exit(13);
    }
    snd_pcm_hw_params_free(hwparams);
    if (snd_pcm_sw_params_malloc (&swparams) < 0) {
        fprintf(stderr, "playhrt: Error allocating SW params.\n");
        exit(14);
    }
    if (snd_pcm_sw_params_current(pcm_handle, swparams) < 0) {
        fprintf(stderr, "playhrt: Error getting current SW params.\n");
        exit(15);
    }
    if (snd_pcm_sw_params_set_start_threshold(pcm_handle, swparams, hwbufsize/2) < 0) {
        fprintf(stderr, "playhrt: Error setting start threshold.\n");
        exit(16);
    }
    if (snd_pcm_sw_params(pcm_handle, swparams) < 0) {
        fprintf(stderr, "playhrt: Error applying SW params.\n");
        exit(17);
    }
    snd_pcm_sw_params_free (swparams);

    /**********************************************************************/
    /* sleep for defined amount of time to allow source to fill buffer    */
    /**********************************************************************/

    /* short sleep to allow input to fill buffer */
    if (sleep > 0) {
        mtime.tv_sec = sleep/1000000;
        mtime.tv_nsec = 1000*(sleep - mtime.tv_sec*1000000);
        nanosleep(&mtime, NULL);
    }
	
    /* get time */
    if (clock_gettime(CLOCK_MONOTONIC, &mtime) < 0) {
        fprintf(stderr, "playhrt: Error getting monotonic clock.\n");
       	exit(18);
    }
    if (verbose)
        fprintf(stderr, "playhrt: Start process  (%ld sec %ld nsec).\n", mtime.tv_sec, mtime.tv_nsec);

    /**********************************************************************/
    /* main loop                                                          */
    /**********************************************************************/

    for (count=1; 1; count++) {

        /* start playing when half of hwbuffer is filled */
        if (count == startcount) { 
            snd_pcm_start(pcm_handle);
            if (verbose)
                if (count == startcount) {
                    clock_gettime(CLOCK_MONOTONIC, &mtimecheck);
                    fprintf(stderr, "playhrt: Start playback (%ld sec %ld nsec).\n", 
                            mtimecheck.tv_sec, mtimecheck.tv_nsec);
                }
        }

        /* read amount of frames which can be written to hardware buffer */
        avail = snd_pcm_avail(pcm_handle);
        if (avail < 0) {
            fprintf(stderr, "playhrt: Error on snd_pcm_avail(): %ld.\n", avail);
            exit(19);
       	}

        /* get address for mmap access */
        if (snd_pcm_mmap_begin(pcm_handle, &areas, &offset, &frames) < 0) {
            fprintf(stderr, "playhrt: Error getting mmap address.\n");
            exit(20);
        }

        /**********************************************************************/
        /* automatic rate adaption                                            */
        /**********************************************************************/

       	/* start measurement of buffer level when LOOPS_CADENCE loops were done */
        if (count > startcount && (count+LOOPS_AVG) % LOOPS_CADENCE == 0) {
            sumavg = LOOPS_AVG;
            avgav = 0;
       	}

        /* add up buffer level for an amount of LOOPS_AVG measurements */
        if (sumavg) {
            avgav += avail;
            if (sumavg == 1) {
                bufavg = (double)avgav/LOOPS_AVG;   /* average buffer level */
                buferr = bufavg - hwbufsize/2;      /* error against target (hwbufsize/2) */
                buferr_i = buferr_i + buferr;       /* integrated error */

                /* calculate amount of time to be added to default step time */
                /* to overall match the local clock to the outgoing clock */
                extransec = (long)(-(Kp * buferr + Ki * Ta * buferr_i) + 0.5);
                nsec = (int)(1000000000/loopspersec + extransec);

                if (verbose > 1) {
                    double deviation = nsec / (1000000000.0/loopspersec);
                    deviation = (deviation > 1) ? (deviation-1) : (deviation-1);
                    fprintf(stderr, "playhrt: (%ld sec) buf: %5.1f e: %4.1f ei: %4.1f dt: %3ld ns (%1.4f%%)\n", 
                            mtime.tv_sec, bufavg, buferr, buferr_i, extransec, deviation*100);
                }
            }
            sumavg--;
        }

        /**********************************************************************/
        /* read data                                                          */
        /**********************************************************************/

        iptr = areas[0].addr + offset * bytesperframe;
        /* memclean(iptr, frames * bytesperframe);  commented out to save some CPU-time */
        /* in --mmap mode we read directly into mmaped space without internal buffer */
        readbytes = read(sfd, iptr, frames * bytesperframe);

        /**********************************************************************/
        /* calcute next wakeup                                                */
        /**********************************************************************/

        /* compute time for next wakeup */
        mtime.tv_nsec += nsec;
        if (mtime.tv_nsec > 999999999) {
            mtime.tv_nsec -= 1000000000;
            mtime.tv_sec++;
        }

        /**********************************************************************/
        /* refresh buffer, sleep until defined wakeup, read/write data        */
        /**********************************************************************/

        refreshmem(iptr, readbytes);
        clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &mtime, NULL);
        refreshmem(iptr, readbytes);
        snd_pcm_mmap_commit(pcm_handle, offset, frames);
        bytecount += readbytes;
        if (readbytes == 0) /* done */
            break;
    }

    /**********************************************************************/
    /* playhrt end, cleanup                                               */
    /**********************************************************************/

    /* cleanup network connection and sound device */
    close(sfd);
    snd_pcm_drain(pcm_handle);
    snd_pcm_close(pcm_handle);
    if (verbose) {
        fprintf(stderr, "playhrt: Loops: %ld, bytes: %lld. \n", count, bytecount);
    }
    return 0;
}

Fragen:
  • Muss snd_pcm_sw_params_set_start_threshold() beibehalten werden? Die Wiedergabe wird ja "per Hand" mittelssnd_pcm_start() gestartet. Falls nicht benötigt, entferne ich den dazugehörigen Code.
  • Greift snd_pcm_nonblock() bei mmap? Falls nicht benötigt, entferne ich den dazugehörigen Code.
Beobachtungen:
In meinem System habe ich mal aus Interesse gemessen wie genau eigentlich der Takt von playhrt ist. Dabei arbeite ich mit 1.000 loopspersec, was einer Taktdauer von 1.000.000 ns entspricht. Wenn ich jetzt messe in welchen Abständen playhrt aufwacht und Daten an den Hardware Puffer sendet, sehe ich typische Abweichungen bis +/- 5.000 ns (5 µs). Im Mittel über mehrere Sekunden werden zwar genau 1.000 ns erreicht, aber der "Jitter" ist nicht unerheblich. Zur Relation: In meinem Setup addiert die Nachführung wegen der auseinanderlaufenden Clocks im Schnitt etwa 30-40 ns.

Grüße,
Andree
Bild
Pittiplatsch
Aktiver Hörer
Beiträge: 457
Registriert: 26.02.2012, 10:48

Beitrag von Pittiplatsch »

Hallo Andree,

Dein Patch kam genau zur richtigen Zeit! Dieses Wochenende hatte ich mal etwas Zeit damit rumzuspielen. Das ganze ist eine wirklich hervorragende Spielwiese um mit sample rate conversion herumzuspielen: sox - pipe zu playhrt oder aplay - einfacher geht es fast gar nicht und viel naeher an der soundhwardware kann man gar nicht sein.

Meine Bastelfreude wurde etwas getruebt dadurch das playhrt bei mir immer nach kurzer Zeit abgebrochen hat (RASPI 3)... Nachdem ich mit Deiner Aenderung gebaut habe funktonierte es sofort ohne aussetzer!

Zu einer Bewertung des klanglichen Ergebnisses bin ich noch nicht gekommen und ich erwarte da auch keine Wunder. Aber ich stimme Dir zu: allein damit herumzuspielen macht schon viel Spass :).

Das schoene ist das man schoen sieht was vor sich geht: z.b. warnt sox ja wenn es Aufgrund von rate conversion und dither zu clipping kommt. Hoerbar ist das fuer mich nicht, aber es sensibilisiert dafuer wie viele Stellschrauben es bei anderen "black-box" Loesungen gibt wo so was einfach unbemerkt passiert ohne das man Einfluss nehmen kann ...

Viele Gruesse,
Tobias
Bild
Buschel
Aktiver Hörer
Beiträge: 846
Registriert: 12.12.2013, 20:12
Wohnort: Raum Karlsruhe

Beitrag von Buschel »

Hallo Tobias,
Pittiplatsch hat geschrieben:
30.08.2020, 22:19
Nachdem ich mit Deiner Aenderung gebaut habe funktonierte es sofort ohne aussetzer!
Sehr schön, das ist damit die dritte Umgebung, in der die Regelung funktioniert. :cheers:

Pittiplatsch hat geschrieben:
30.08.2020, 22:19
Das ganze ist eine wirklich hervorragende Spielwiese um mit sample rate conversion herumzuspielen
Dann passt es ja vielleicht ganz gut, dass ich auch Frank´s resample_soxr ein wenig umgeräumt und entschlackt habe. Es wird nur noch stdin/stdout unterstützt, volrace und Frank´s spezielle memory refresh/clean Aufrufe sind entfernt. Durch das Entfernen der memrefresh() und cleanmem() Aufrufe ist der Code bei mir etwa 30-40% schneller geworden. Und dabei war der Code vorher schon wesentlich schneller als sox.

Code: Alles auswählen

/*
resample_soxr.c                    Copyright frankl 2018
                         Copyright Andree Buschmann 2020

This file is part of frankl's stereo utilities.
See the file License.txt of the distribution and
http://www.gnu.org/licenses/gpl.txt for license details.
Compile with
    gcc -o resample_soxr -O2 resample_soxr.c -lsoxr -lsndfile -lrt
*/

#include "version.h"
#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <sys/types.h>
#include <soxr.h>

/* help page */
/* vim hint to remove resp. add quotes:
      s/^"\(.*\)\\n"$/\1/
      s/.*$/"\0\\n"/
*/
void usage( ) {
    fprintf(stderr, "resample_soxr (version %s of frankl's stereo utilities", VERSION);
    fprintf(stderr, ", reworked by Andree Buschmann");
    fprintf(stderr, ")\n\nUSAGE:\n");
    fprintf(stderr,
"\n"
"  resample_soxr [options] \n"
"\n"
"  By default this program works as a resampling filter for stereo audio \n"
"  streams in raw double format (in some programs denoted FLOAT64_LE).\n"
"  Here 'filter' means that input comes from stdin and output is\n"
"  written to stdout. Use pipes and 'sox' or other programs to deal with \n"
"  other stream formats. The main options are the input sample rate,\n"
"  the output sample rate and parameters for the resmapler itself.\n"
"\n"
"  This program uses the 'soxr' standalone resampling library (see\n"
"  https://sourceforge.net/projects/soxr/) with the highest quality \n"
"  settings, all computations are done with 64-bit floating point \n"
"  numbers.\n"
"\n"
"  The computation is similar to using 'sox' with effect 'rate -v'.\n"
"  But 'sox' applies all effects internally to 32-bit signed integer\n"
"  samples (that is, the 64-bit input precision is lost).\n"
"\n"
"  OPTIONS\n"
"  \n"
"  --inrate=floatval, -i floatval\n"
"      the input sample rate as floating point number (must not be an \n"
"      integer). Default is 44100. In case of file input this value is\n"
"      overwritten by the sampling rate specified in the file (so, this\n"
"      option is not needed).\n"
"\n"
"  --outrate=floatval, -o floatval\n"
"      the output sample rate as floating point number (must not be an \n"
"      integer). Default is 192000.\n"
"\n"
"  --channels=intval, -c intval\n"
"      number of interleaved channels in the input. Default is 2 (stereo).\n"
"      In case of input from a file this number is overwritten by the \n"
"      the number of channels in the file.\n"
"\n"
"  --buffer-length=intval, -b intval\n"
"      the size of the input buffer in number of frames. The default\n"
"      (and minimal value) is 8192 and should usually be fine.\n"
"\n"
"  --phase=floatval, -P floatval\n"
"      the phase response of the filter used during resampling; see the \n"
"      documentation of the 'rate' effect in 'sox' for more details. This\n"
"      is a number from 0 (minimum phase) to 100 (maximal phase), with \n"
"      50 (linear phase) and 25 (intermediate phase). The default is 25,\n"
"      and should usually be fine.\n"
"\n"
"  --band-width=floatval, -B floatval\n"
"      the band-width of the filter used during resampling; see the \n"
"      documentation of the rate effect in 'sox' for more details. The value\n"
"      is given as percentage (of the Nyquist frequency of the smaller \n"
"      sampling rate). The allowed range is 74.0..99.7, the default is 91.09\n"
"      (that is the filter is flat up to about 20kHz).\n"
"\n"
"  --precision=floatval, -e floatval\n"
"      the bit precision for resampling; higher values cause higher CPU usage.\n"
"      The valid range is 16.0..33.0, the default is 33.0 and should usually\n"
"      be fine (except lower CPU usage is essential).\n"
"\n"
"  --help, -h\n"
"      show this help.\n"
"\n"
"  --verbose, -v\n"
"      shows some information during startup and operation.\n"
"\n"
"  --version, -V\n"
"      show the version of this program and exit.\n"
"\n"
"   EXAMPLES\n"
"\n"
"   Convert a file 'musicfile' that can be read by 'sox' to a 96/32\n"
"   wav-file using a pipe:\n"
"      ORIGRATE=`sox --i musicfile | grep \"Sample Rate\" | \\\n"
"                cut -d: -f2 | sed -e \"s/ //g\"`\n"
"      sox musicfile -t raw -e float -b 64 - | \\\n"
"          resample_soxr --inrate=$ORIGRATE --outrate=96000 --precision=28 | \\\n"
"          sox -t raw -e float -b 64 -c 2 -r 96000 - -e signed -b 32 out.wav\n"
"\n"
);
}

int main(int argc, char *argv[])
{
    /* variables for the resampler */
    double inrate, outrate, phase, bwidth, prec;
    double *iptr, *optr;
    int verbose, optc;
    long intotal, outtotal, blen, mlen, check, nch, olen;
    size_t indone, outdone;
    /* variables for soxr */
    int qspecflags, qspecrecipe;
    soxr_quality_spec_t q_spec;
    soxr_io_spec_t io_spec;
    soxr_runtime_spec_t runtime_spec;
    soxr_t soxr;
    soxr_error_t error;

    /**********************************************************************/
    /* read and set parameters                                            */
    /**********************************************************************/
    
    /* no parameter given */
    if (argc == 1) {
        usage();
        exit(1);
    }

    /* read command line options */
    static struct option longoptions[] = {
        {"inrate",        required_argument, 0, 'i' },
        {"outrate",       required_argument, 0, 'o' },
        {"phase",         required_argument, 0, 'P' },
        {"band-width",    required_argument, 0, 'B' },
        {"precision",     required_argument, 0, 'e' },
        {"channels",      required_argument, 0, 'c' },
        {"buffer-length", required_argument, 0, 'b' },
        {"verbose",       no_argument,       0, 'v' },
        {"version",       no_argument,       0, 'V' },
        {"help",          no_argument,       0, 'h' },
        {0,               0,                 0,  0  }
    };

    /* defaults */
    inrate = 44100.0;
    outrate = 192000.0;
    phase = 25.0; 
    bwidth = 0.0;
    prec = 33.0;
    nch = 2;
    blen = 8192;
    verbose = 0;
    intotal = 0;
    outtotal = 0;
    
    /* read parameters */
    while ((optc = getopt_long(argc, argv, "i:o:P:B:e:c:b:vVh", longoptions, &optind)) != -1) {
        switch (optc) {
        case 'i':
            inrate = atof(optarg);
            break;
        case 'o':
            outrate = atof(optarg);
            break;
        case 'P':
            phase = atof(optarg);
            if (phase < 0.0 || phase > 100.0)
                phase = 25.0;
            break;
        case 'B':
            bwidth = atof(optarg);
            if (bwidth < 74.0 || bwidth > 99.7)
                bwidth = 0.0;
            break;
        case 'e':
            prec = atof(optarg);
            if (prec < 16.0 || prec > 33.0)
                prec = 33.0;
            break;
        case 'c':
            nch = atoi(optarg);
            break;
        case 'b':
            blen = atoi(optarg);
            if (blen < 1024)
                blen = 8192;
            break;
        case 'v':
            verbose = 1;
            break;
        case 'V':
            fprintf(stderr, "resample_soxr (version %s of frankl's stereo utilities", VERSION);
            fprintf(stderr, ", reworked by Andree Buschmann)\n");
            exit(2);
        default:
            usage();
            exit(3);
        }
    }

    /**********************************************************************/
    /* show playhrt configuration                                         */
    /**********************************************************************/
  
    if (verbose) {
        fprintf(stderr, "resample_soxr: Version %s\n", VERSION);
        fprintf(stderr, "resample_soxr: input rate %.1f output rate %.1f\n", inrate, outrate);
        fprintf(stderr, "resample_soxr: precision %.0f, phase %.0f\n", prec, phase);
        fprintf(stderr, "resample_soxr: passband %.4f, stopband 1.0 (no aliasing)\n", bwidth/100);
        fprintf(stderr, "resample_soxr: SOXR_ROLLOFF_SMALL, SOXR_HI_PREC_CLOCK\n");
    }

    /**********************************************************************/
    /* allocate buffers                                                   */
    /**********************************************************************/

    /* allocate input buffer */
    iptr = (double*) malloc(nch*blen*sizeof(double));

    /* allocate output buffer */
    olen = (long)(blen*(outrate/inrate+1.0));
    optr = (double*) malloc(nch*olen*sizeof(double));

    /**********************************************************************/
    /* create soxr resampler, for parameters see                          */
    /* https://sourceforge.net/p/soxr/code/ci/master/tree/src/soxr.h      */
    /**********************************************************************/

    /* small rolloff <= 0.01 dB and high precision for irrational ratios */
    qspecflags = SOXR_ROLLOFF_SMALL | SOXR_HI_PREC_CLOCK; 

    /* intermediate phase (will later be overwritten by chosen value for phase) */
    /* precision 32 (will later be overwritten by chosen value for precision)   */    
    qspecrecipe = SOXR_INTERMEDIATE_PHASE | SOXR_32_BITQ;

    /* create qspec, overwrite phase/prec/bwidth with chosen values */
    q_spec = soxr_quality_spec(qspecrecipe, qspecflags);
    q_spec.phase_response = phase;
    q_spec.precision = prec;
    q_spec.passband_end = (bwidth != 0.0) ? bwidth/100.0 : q_spec.passband_end;

    /* set io_spec for FLOAT64 precision */
    io_spec = soxr_io_spec(SOXR_FLOAT64_I,SOXR_FLOAT64_I);

    /* set runtime spec for 1 thread */
    runtime_spec = soxr_runtime_spec(1);

    /* now we can create the resampler */
    soxr = soxr_create(inrate, outrate, nch, &error, &io_spec, &q_spec, &runtime_spec);
    if (error) {
        fprintf(stderr, "resample_soxr: Error initializing soxr resampler.\n");
        fflush(stderr);
        exit(4);
    }

    /**********************************************************************/
    /* main loop                                                          */
    /**********************************************************************/
     
    /* we read from stdin until eof and write to stdout */
    while (1) {
        mlen = blen;

        /* clean buffers */
        memset(iptr, 0, nch*blen*sizeof(double));
        memset(optr, 0, nch*olen*sizeof(double));

        /**********************************************************************/
        /* read data                                                          */
        /**********************************************************************/

        /* read input block */
        mlen = fread((void*)iptr, nch*sizeof(double), mlen, stdin);

        /**********************************************************************/
        /* resample data                                                      */
        /**********************************************************************/
    
        /* call resampler */
        error = soxr_process(soxr, iptr, mlen, &indone, optr, olen, &outdone);
        if (mlen > indone) {
            fprintf(stderr, "resample_soxr: only %ld/%ld processed.\n",(long)indone,(long)mlen);
            fflush(stderr);
        }
        if (error) {
            fprintf(stderr, "resample_soxr: Error calling soxr_process: (%s).\n", soxr_strerror(error));
            fflush(stderr);
            exit(5);
        }

        /**********************************************************************/
        /* write data to output                                               */
        /**********************************************************************/
 
        /* write output */
        check = fwrite((void*)optr, nch*sizeof(double), outdone, stdout);
        fflush(stdout);
    
        /* this should not happen, the whole block should be written */
        if (check < outdone) {
            fprintf(stderr, "resample_soxr: Error writing to output..\n");
            fflush(stderr);
            exit(6);
        }
    
        intotal += mlen;
        outtotal += outdone;
        if (mlen == 0) /* done */
            break;
    }

    /**********************************************************************/
    /* resample_soxr end, cleanup                                         */
    /**********************************************************************/

    soxr_delete(soxr);
    free(iptr);
    free(optr);
    if (verbose) {
        fprintf(stderr, "resample_soxr: %ld input and %ld output samples\n", (long)intotal, (long)outtotal);
    }
    return(0);
}

Nach meinem Urlaub schaue ich mir nochmal die Parameter an und erweitere sie eventuell, so dass alle von soxr unterstützten Parameter gewählt werden können (wie z.B. rolloff und aliasing). Im neuen Code sind zumindest die Settings schon klar benannt und nicht nur "0x17" oder "0". Außerdem habe ich ein Angleichen der Parameter vor: die Kanalanzahl bei playhrt wird mit "-k" übergeben, bei resample_soxr mit "-c".

Noch einmal zu playhrt: So sehr mir der playhrt zugrundeliegende Gedanke von kleinen Datenhäppchen in möglichst konstanten kleinen Abständen gefällt, so sehr frage ich mich nach der memclean-/refreshmem-Implementierung. Ja, der Code fasst jedes Byte noch einmal an und schreibt es neu. Warum tut es nicht ein simples memcpy auf einen anderen Puffer, gefolgt von einem memcpy zurück auf den ursprünglichen? Das wäre wesentlich schneller ausgeführt als der refresh "per Hand". Welche Idee steckt hinter der gewählten Implementierung mit dem 4-byte-weisen vor-und-zurück XOR?
Außerdem wird der refresh direkt nach dem Beschreiben des Puffers, und dann (bei default-Einstellung loopspersec=1000) nach etwa 1 ms erneut ausgeführt -- also aus Sicht der Daten kurz nachdem sie frisch in den Puffer geschrieben wurden. Danach warten diese Daten ohne weiteren refresh bis sie wiedergeben werden. Da die Daten immer um einen Füllstand von hwbuf/2 herum in den Puffer geschrieben werden, sind dies -- je nach Größe des Puffers -- viele ms ohne playhrt-refresh.

Viele Grüße,
Andree
Bild
Martin
Aktiver Hörer
Beiträge: 104
Registriert: 15.03.2012, 14:23

Beitrag von Martin »

Hallo Andree,
Wenn ich jetzt messe in welchen Abständen playhrt aufwacht und Daten an den Hardware Puffer sendet, sehe ich typische Abweichungen bis +/- 5.000 ns (5 µs)
Wie hast Du die Abstände gemessen und warum sollte der Jitter mit Regelung geringer sein? Ich kann leider Deine playhrt-Version nicht testen, da bei mir die mmap-Option nicht funktioniert. Mit welcher Zeitkonstante läuft der PI-Regler?

Viele Grüße
Martin
Buschel
Aktiver Hörer
Beiträge: 846
Registriert: 12.12.2013, 20:12
Wohnort: Raum Karlsruhe

Beitrag von Buschel »

Hallo Martin,
Martin hat geschrieben:
31.08.2020, 08:25
Wie hast Du die Abstände gemessen und warum sollte der Jitter mit Regelung geringer sein?
Der Jitter ist mit der Regelung nicht geringer. Ich wollte nur Frank´s Aussage dahin gehend kommentieren, dass eine Änderung der Abtast-Intervalle durch die Regelung im Vergleich zum sowieso auftretenden Jitter vernachlässigbar ist. Ich habe übrigens noch einmal nachgemessen und muss meine Aussage korrigieren. Der Jitter ist etwas größer als zunächst von mir angenommen. Wenn ich folgenden Code zum Messen der Zeitintervalle benutze

Code: Alles auswählen

// sleep until wake time is reached */
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &mtime, NULL);

// AB: get time and compare against targeted step time of 1000000 ns
clock_gettime(CLOCK_MONOTONIC, &mtimecheck);
time_ist  = 1000000000 * mtimecheck.tv_sec + mtimecheck.tv_nsec;
time_diff = (time_ist - time_alt - 1000000)/5000 + 7; /* 5k steps into array of length 15 */
time_diff = (time_diff>14) ? 14 : (time_diff);
time_diff = (time_diff< 0) ?  0 : (time_diff);
time_alt  = time_ist; 
diff[time_diff]++;
        
if (count % LOOPS_CADENCE == 0) {
    fprintf(stderr, "playhrt: count %ld\n", count);
    int i;
    for (i=0; i<15; ++i)
        fprintf(stderr, "%dk: %d (%.2f%%)\n", (i-7)*5, diff[i], (float)diff[i]/count*100);
    fprintf(stderr, "\n");
}
bekomme ich nach diversen Durchläufen folgendes Ergebnis (2/3 liegen im Bereich +/- 5.000 ns):

Code: Alles auswählen

playhrt: count 100000
-35k: 740 (0.74%)
-30k: 500 (0.50%)
-25k: 1496 (1.50%)
-20k: 1432 (1.43%)
-15k: 2187 (2.19%)
-10k: 4052 (4.05%)
-5k: 5574 (5.57%)
0k: 66348 (66.35%)
5k: 7186 (7.19%)
10k: 4208 (4.21%)
15k: 3098 (3.10%)
20k: 1064 (1.06%)
25k: 1311 (1.31%)
30k: 278 (0.28%)
35k: 526 (0.53%)
Bei vielen Durchläufen sehe ich auch Häufungen bei +/- 25.000 ns. Das scheint an höherprioren Prozessen auf demselben core zu liegen. Falls ich hier einen Fehler gemacht haben sollte, lasst es mich bitte wissen.

Martin hat geschrieben:
31.08.2020, 08:25
Mit welcher Zeitkonstante läuft der PI-Regler?
Die Regelung sieht wie folgt aus:

Code: Alles auswählen

    double Kp = 1.0;	/* value based on tests */
    double Ki = 0.05;	/* value based on tests */
...
    Ta = (1.0*LOOPS_CADENCE)/loopspersec;       /* delta T seconds */
    Ta = (Ki*Ta>0.2) ? 0.2/Ki : Ta;             /* limit Ta to avoid oscallation */
...
    /* calculate amount of time to be added to default step time */
    /* to overall match the local clock to the outgoing clock */
    extransec = (long)(-(Kp * buferr + Ki * Ta * buferr_i) + 0.5);
    nsec = (int)(1000000000/loopspersec + extransec);
Das ergibt eine Nachstellzeit von Kp/Ki = 20 Sekunden.

Grüße,
Andree
Bild
Martin
Aktiver Hörer
Beiträge: 104
Registriert: 15.03.2012, 14:23

Beitrag von Martin »

Hallo Andree,
jetzt habe ich alles verstanden, vielen Dank.

viele Grüße
Martin
Bild
Antworten