Welche Zeitbereiche für Hüllkurven?

amesser

||||
Moin,

ich lass euch mal an meinen Gedanken teilhaben. Bin hier eher als Techniker statt als Musiker unterwegs und versuche mich gerade daran einen Synthesizer im FPGA zu implementieren. Der DX-7 dient so ein bischen als Vorlage. Die Oszillatoren laufen schon, jetzt bin ich an den Hüllkurven dran. Dabei komme ich jetzt so ein bischen an die Grenzen der Genauigkeit der Berechnung. Da tun hier gerade lange Zeiten der einzelnen Stufen von so einer Hüllkurve weh. Ich habe da jetzt mal von einem minimalen Attack von 10µs ausgehend simuliert. Da komme ich bis etwa 400ms Attack, ab da wirds dann zu ungenau. Natürlich könnte ich die Genauigkeit der Berechnung erhöhen, das kostet mich aber viele Resourcen im FPGA. (Der hat DSP Blöcke für 18 Bit, bei höherer Genauigkeit braucht man dann gleiche viel mehr davon)

Jetzt meine Frage an die Musiker: Welche Zeitbereiche sind denn für die Hüllkurvensegmente sinnvoll? Wenn ich z.B. erst bei 50µs starte, dann treten die Rechenfehler erst ab 2.5s auf. Ich habe mal verschiede Anleitungen von aktuellen Synths gewälzt, aber da stehen meist keine konkreten Zahlen für die Hüllkurven drinnen. Eine weitere Möglichkeit wäre es, dass ich eine (wählbare) Untersetzung der Rechenschritte einbaue. Das entspricht dann so ungefähr dem Kippschalter den man auf vielen Eurorack ADSRs findet. Das hätte dann halt die Folge, das bei langen Zeitbereichen die Kurve möglicherweise "stufig" umschaltet. Allerdings, im Moment wird alle 7µs ein neuer Hüllkurvenpunkt gerechnet, bei einer Untersetzung 1:100 wären das immer noch alle 0.7ms ein neuer Punkt, das hört man dann wohl eher nicht. und ich käme bis auf 50s hoch.

Was meint Ihr? Schonmal danke für den Input. (Und ja meine Hüllkurven sind im Moment so schnell, das die eigentlich Audio sind, wohl vollkommen übertrieben, aber der Döpfer kann auch 20µs)

Grüße,
Andreas
 
nach unten sind genaue werte super, bei 400ms braucht das kein mensch mehr, da kannst du 10ms schritte haben wenn es nicht anders geht.

ich skaliere GUI controls / parameter für attack zeiten schon mal mit log90.
 
Zuletzt bearbeitet:
aber der Döpfer kann auch 20µs
Und das ist auch gut so. Je schneller, desto besser. Und ja, das kann man wirklich hören. Es war ein Kritikpunkt von mir beim direkten Vergleich der digitalen Sound-Force-Hüllkurven mit den analogen Doepfer-Modulen. Wenn Du es schaltbar machen kannst, sind langsamere Zeiten auch okay. Ansonsten - mache es so schnell, wie es digital eben möglich ist.
 
Ein Sample bei 44.1kHz Abtastrate dauert 23µs. Natürlich könnte man darunter auch noch rechnen, das wäre aber mit Sicherheit nicht mehr wahrnehmbar. Vermutlich ist eine Stufigkeit zwischen 50 und 100µs auch nicht wirklich wahrzunehmen.

Aber ein maximaler Attack von 2.5s bei 50µs oder 5s bei 100µs ist für einige Sounds einfach zu wenig. 20s ist wohl eher ein üppiger Endwert.
 
Ein Sample bei 44.1kHz Abtastrate dauert 23µs. Natürlich könnte man darunter auch noch rechnen, das wäre aber mit Sicherheit nicht mehr wahrnehmbar.

genau, auf das hörbare spektrum kommt es an. bei 1/44100 kannst du bereits problemlos zwischen zwei samples interpolieren, ohne dass sich das geräusch des knackens großartig ändert.
 
Danke für die Rückmeldungen. Manchmal hilft es ja schon, einfach mal drüber zu sprechen. Die Idee mit der Untersetzung kam mir auch erst beim Schreiben. Ich denke so werde ich das auch erstmal umsetzen. Bei schnellem Attack wird für jedes Sample die Hüllkurve neu gerechnet, bei langsamen Attack nur jedes x'te Sample. (x wird 1...64) Das Ganze versteckt sich dann ja eh hinter der Bedienoberfläche, Bei langsamen Attack ist die Zeiteinstellung dann nur etwas grober. (Wenn man bei ~1ms überhaupt von grob sprechen kann) Die interne Samplerate liegt aktuell übrigens bei krummen 162kHz damit ich die 18 bit voll aunutzen kann.
 
nur mal so. ganz einfach. 8 bit signed, 0-255.

den parameterwert mit sich selbst multiplizieren, das gedachte komma zwei stellen nach links verschieben.

0.01
0.04
0.09
0.16
0.25
0.36
0.49
0.64
0.81
1.
1.21
1.44
1.69
1.96
2.25
2.56
2.89
3.24
3.61
4.
4.41
4.84
5.29
5.76
6.25
6.76
7.29
7.84
8.41
9.
9.61
10.24

[...]

501.76
506.25
510.76
515.29
519.84
524.41
529.
533.61
538.24
542.89
547.56
552.25
556.96
561.69
566.44
571.21
576.
580.81
585.64
590.49
595.36
600.25
605.16
610.09
615.04
620.01
625.
630.01
635.04
640.09
645.16
650.25


erster schritt 0.03 groß, mittlerer 2.55, letzter 15.19
 
0.01 und 650.25 passt aber nicht gleichzeitig in 8bit signed, egal wie man skaliert. ADSR Envelopes berechnet man normalerweise nach der Formel x(n+1) = a + b *x(n). b ist dabei sehr nahe an der "1"~ 0.99... Umso länger der Attack, umso näher kommt "b" der eins. Das Problem hierbei ist, das x eben eine feste Breite hat und wenn b nur nah genug an 1 herankommt, dann ist irgendwann das Ergebnis von b*x(n) == x(n) (weil das Ergebniss ja wieder auf die Bit--Breite von x skaliert werden muss) und dann funktioniert die Integration nicht mehr, da kann dann alles mögliche passieren. Mit Fließkomma ist die Multiplikation weniger das Problem weil man da ja noch den Exponenten hat und man große Dynamik abdecken kann, hier treten die Fehler erst bei der Addition auf. Wenn die Größenordnungen der Summanden zu stark abweichen verliert man die Präzission der kleineren Summanden. (Aber selbst 32 Bit Fließkomma hat 24 Bit Mantisse und damit schon mehr als ich nehmen will/kann)
 
ich habe nur der lesbarkeit halber float bzw reelle zahlen benutzt - wie du das bei dir errechnen und repräsentieren kannst oder musst, keine ahnung. wenn du da max. 18 bit hast böte sich fixed point an.

das skalieren oder verzerren von wertebereichen ist jedenfalls der einfachste weg eine generell höhere auflösung zu umgehen, wenn man diese höhere auflösung nur an einem ende braucht. dass das bei int zu int nicht immer einfach ist, ist klar.
man könnte auch die unteren 8 bit in 0.1er schritte umwandeln und die obere hälfte des wertebereichs macht dann halt plötzlich 5er oder 10er schritte.

dass es "normale" formeln gibt mit denen man üblicherweise hüllkurven berechnet dürfte ein gerücht sein, du kannst dazu jedes beliebige verfahren verwenden was dir gerade einfällt.
vielleicht hast du da auch speicher? wo du ein lookup table reinschreiben kannst?
 
wenn b nur nah genug an 1 herankommt, dann ist irgendwann das Ergebnis von b*x(n) == x(n)
Das ist nunmal die Crux der begrenzten Wortlänge. Und je höher die Samplerate, desto früher kommt man an die Grenze.

Aber Deine Lösung ist natürlich sehr gut und völlig ausreichend, denn wenn b immerhin so weit weg von 1 ist, dass b*x(n) sich von x(n) sicher unterscheidet, so ist doch der resultierende Amplitudensprung sehr gering und auch dann nicht hörbar, wenn er nur alle z.B. 32 Samples upgedatet wird.

PS.: Ich beibe dabei: genauer als 50µs ist overkill.
 
Erstmal noch Danke für die Rückmeldungen. Leider ist die Sache die letzten beiden Wochen wegen was anderem liegen geblieben. (Ich hatte mich mit mit dem Auslesen von Tier-RFID Tags beschäftigt. Da bin ich jetzt aber erstmal an meine Grenzen gekommen was Analogtechnik betrifft, ich wollte so einen Leser komplett selbst bauen, das ist aber komplizierter als man denkt, also wenn man mehr als 2cm Reichweite will :) )

Also im Moment will ich mich auf die 18 Bit beschränken, das ganze dann als Fixpunktzahl mit Wertebereich 0 bis 0.99.. (Q18) Mal sehen wie weit ich damit komme. Die Idee mit der unterschiedlichen Skalierung ist auch interessant. Was mir gerade beim schreiben einfällt wäre noch eine dynamische Untersetzung, am Anfang wo der Anstieg groß ist berechnet man für jedes Sample neu, um so näher man dem Zielwert kommt um so stärker untersetzt man. Möglicherweise reicht dann sogar eine Addition ohne Multiplikation und man bekommt trotzdem den typischen Verlauf hin.

vielleicht hast du da auch speicher? wo du ein lookup table reinschreiben kannst?
Ja schon. Allerdings nicht genug für eine 18 Bit Lookuptable, müsste man also irgendwie etwas quantisieren. Ich versuchs erstmal algorithmisch zu machen. Hintergrund: Ich möchte eigentlich möglichst viel des Speicher aufheben, damit ich später auch anderen Wellenformen bzw genauer: Wavetables ablegen kann. Zu Beginn will ich erstmal nur 6 OP FM machen. Von da ist es zum 6-Oszillator Subtraktiven Synth aber nicht wirklich weit. Das wäre dann mein nächstes Ziel. D.h. es kommen dann noch 1-2 Filter hinter den FM dahinter. Danach würden dann einfache Wavetables kommen also zwei Wellenformen überblendbar. Am Ende kann das Teil das alles gleichzeitig. Mein Kopf ist so voller Ideen dafür, dass ich eigentlich meinen Job kündigen müsste, auch weil mich das Thema total interessiert. Die Platform auf der ich das mache hat schon echt Power, da ist viel Raum. Neben dem FPGA ist da auch noch ein Quad-Core 64 Bit RISC-V 625MHz drauf, das Eval Board hat dazu noch 1GB DDR4 RAM.

Mal sehen wie viel davon am Ende wirklich steht. Ich habe leider immer viel zu viele Ideen für zu wenig Zeit und das restliche Leben will ja auch noch organisiert werden. Das Grundrauschen im meinem Kopf ist einfach zu hoch. Egal. Wenn ich die anderen liegengeblieben Sachen aufgeholt habe (Hier liegen noch zwei Envelopes zum Löten rum) mache ich dann endlich mal mit dem FPGA weiter.
 
Hallo Andreas,
ich habe auch einen Synthesizer im FPGA entwickelt (siehe hier), sogar mit recht begrenzten Ressourcen.
Mein Envelopegenerator berechnet die Envelope-Werte nicht nach einer Formel, sondern ist salopp gesagt eigentlich nur ein Zähler mit einstellbarer Schrittweite. Ja klar, da steckt schon mehr dahinter.
Vielleicht solltest du dich von dem Konzept der Berechnung des Wertes mit den erforderlichen Kompromissen (Genauigkeit vs. Envelope-Länge) einfach verabschieden und auch über ein Zähler-Konzept nachdenken.
Beispiel: Der Envelopegenerator meines Synthi generiert insgesamt 12 Envelopes mit je 8 Phasen für eine Sample-Frequenz von 96kHz. Zeitbasis ist 1 Mikrosekunde, jede Phase hat 1000 Schritte, ein Schrittlänge ist minimal eins und maximal 524288 (Zählerbreite 19 Bits), d.h. eine Phase ist minimal 1 Millisekunde und maximal 524 Sekunden lang. Für die Zeitsteuerung braucht man also nur Zähler, keine Multiplikationen oder gar Divisionen. Bei der Berechnung des aktuellen Hüllkurvenwertes (in Abhängigkeit vom Zielwert und Zeitpunkt) wird dann multipliziert und dividiert (alles mit Hardware-Makros, Multiplikation mit 18x18 Bits und Division mit 24/12 Bits Wortbreite). Da werden dann auch dynamische Einflüsse wie Velocity mit reinberechnet.
Das ganze ist natürlich skalierbar, aber zeigt die Größenordnung auf.

Wir können und gerne dazu austauschen.

Grüße,
Dirk
 
Zuletzt bearbeitet:
ich habe auch einen Synthesizer im FPGA entwickelt (siehe hier), sogar mit recht begrenzten Ressourcen.
Cool, das ist völlig an mir vorbeigegangen. Ich habe den Thread mal kurz überflogen, ich schaue da auf jeden Fall nochmal genauer rein. (Dieses Wochenende ist Omas 85'er in der Heimat...)
Für die Zeitsteuerung braucht man also nur Zähler, keine Multiplikationen oder gar Divisionen. Bei der Berechnung des aktuellen Hüllkurvenwertes (in Abhängigkeit vom Zielwert und Zeitpunkt) wird dann multipliziert und dividiert (alles mit Hardware-Makros, Multiplikation mit 18x18 Bits und Division mit 24/12 Bits Wortbreite). Da werden dann auch dynamische Einflüsse wie Velocity mit reinberechnet.
Das ganze ist natürlich skalierbar, aber zeigt die Größenordnung auf.
Ok, D.h. Du berechnest den Wert der Hüllkurve dann quasi immer aus dem Zählerwert. Das braucht dann aber auch etwas Zeit, Division wollte ich wenn es geht komplett vermeiden? Mir ist aber noch eine Idee zu den Zählern gekommen. Man könnte auch statt mit gleich verteilten Zeitschritten zu arbeiten die Zeitschritte hochskalieren. D.h. man addiert/subtrahiert pro Zeitschritt immer den gleichen Wert auf die Hüllkurve, lässt aber die Zeitschritte immer weiter auseinandergehen um so näher man dem Zielwert kommt. Das ergibt dann auch einen logarithmischen Verlauf. Mal sehen, für erste Versuche reichts erstmal wie ich es jetzt habe, ich muss jetzt die VCAs und die FM-Modulation machen, also quasi Feedback zurück in die Oszillatoren.
 
Ich hab grad nochmal in den Code des Envelopegenerators geschaut:
Die Envelope-Zeitsteuerung per Zähler erfolgt immer mit genau 1000 Schritten, die Schrittlänge wird dann per Parameter eingestellt wie oben erwähnt zwischen 1 und ~510000 Zeiteinheiten pro Schritt. Es wird hier also über die Länge eines Schrittes skaliert, nicht über die Anzahl der Schritte. An dieser Stelle wird auch die Velocity berücksichtigt (höherer Velocity-Wert -> kürzere Schrittlänge). Der aktuelle Levelwert der Hüllkurve für den aktuellen Envelope-Schritt "n" wird dann berechnet aus dem Levelwert des vorherigen Schrittes (Schritt "n-1" ) und dem für den Schritt "n" nötigen Increment, welcher sich wiederum aus dem verbleibenden Restwert (Differenz "eingestellter Zielwert" - "aktueller Wert") und der Restzeit (Anzahl der noch übrigen Schritte, Differenz 1000-n) berechnen lässt. Da es, wie erwähnt, maximal 1000 Schritte gibt, ist auch die nötige Division (Restwert/Restzeit) in der Bitbreite begrenzt: Level-Wert (maximal 24 bits) geteilt durch Anzahl Schritte (maximal 1000, also 10 Bits). Eine Division mit Divisor-Bitbreiten von 10 bits lässt sich per Division-Makro in genau 27 Takten erledigt, ist also überschaubar. Das Design lässt sich an dieser Stelle sehr einfach skalieren, je nachdem welche Ressourcen (Logik, Rechenzeit) zur Verfügung steht: reichen dir 1000 Schritte nicht aus, nimmst du vielleicht 10000 Schritte, was sich dann aber auf die Bitbreite der Division auswirkt (für 10000 Schritte brauchste dann eben ne 14 Bit-Division mit mehr Rechenschritten).
Bei der Level-Berechnung können dann weitere Modifikationen erfolgen durch z.B. velocity-abhängige Dynamik oder Restwert aus einer früheren nicht abgeschlossenen Phase (z.B. Attack läuft noch, die Taste wird jedoch losgelassen, also gehts zur Release-Phase, ohne daß der Attack-Wert erreicht wurde). Da sind noch ein paar Tricks dabei, über die man u.a. den Verlauf (linear, exponentiell, quadratisch...) bestimmen kann, dies hier zu beschreiben könnte aber leicht ausufern ;-)
Screenshots aus Beispielverläufen bei dieser Art der Envelope-Berechnung in Beitrag #59 im oben verlinkten Thread.

Grüße
 
Zuletzt bearbeitet:
BTW da du auch FM machen willst, steht dir wahrscheinlich ein langwieriger Weg bevor (so war es bei mir). Reine FM im Sinne von wörtlich "Frequenzmodulation" kann sehr ernüchternd, im Sinne von enttäuschend weil scheinbar unharmonisch, sein. Seit ich mich tiefer damit beschäftigt habe, weiß ich nun selbst auch, warum FM im Grunde genommen (auch historisch, siehe Werdegang der "FM-Synthese" im Yamaha DX-7) eher über Phasenmodulation realisiert wird.
 
BTW da du auch FM machen willst, steht dir wahrscheinlich ein langwieriger Weg bevor (so war es bei mir). Reine FM im Sinne von wörtlich "Frequenzmodulation" kann sehr ernüchternd, im Sinne von enttäuschend weil scheinbar unharmonisch, sein. Seit ich mich tiefer damit beschäftigt habe, weiß ich nun selbst auch, warum FM im Grunde genommen (auch historisch, siehe Werdegang der "FM-Synthese" im Yamaha DX-7) eher über Phasenmodulation realisiert wird.
Oh, sorry, ja es wird wie beim DX-7 Phasenmodulation werden. Der dient mir ja so ein bischen als Ideengeber. Ich habe jetzt die Oszillatoren und Envelopes über "VCA"s kombiniert und lege das auf den Audio Ausgang. Da ist aber irgendwo noch ein Bock im System, tut noch nicht so wie es soll. (Das ganze Pipelining macht mir gerade einen Knoten in den Kopf ;-) ) Wenn das soweit ist, gehts mit "note off" weiter. Danach dann FM bzw Phase-Mod. Ich will das ganze dann möglicherweise etwas größer aufziehen als beim DX-7, im Prinzip soll jeder Operator jeden anderen über einen eigenen envelope modulieren können. Bin mir allerdings noch nicht sicher, ob das klanglich viel bringt.
 
Klanglich bringt das schon was. Mein Synthi hat im FM-Synthese-Modus nur vier Operatoren in unterschiedlicher Verschaltung und auch damit kann man schon ne Menge machen :)

Wie sind denn deine Oszillatoren gebaut? Nutzt du auch DDS ? Damit geht Phasenmodulation recht einfach (ok, "echte" lineare FM auch ;-) .
 
Wie sind denn deine Oszillatoren gebaut? Nutzt du auch DDS ?
Ja genau. Ich erzeuge mir einen 24 bit Phasezähler mit dem ich dann mit den oberen 10 Bit in eine Lookup-Tabelle gehe. Das werde ich aber nochmal ändern so dass ich mit 11-12 Bit in die Lookup-Tabelle gehen kann. Das macht mehr Sinn als 20 Bit Samples aus der Tabelle zu bekommen.
 
Ja genau. Ich erzeuge mir einen 24 bit Phasezähler mit dem ich dann mit den oberen 10 Bit in eine Lookup-Tabelle gehe. Das werde ich aber nochmal ändern so dass ich mit 11-12 Bit in die Lookup-Tabelle gehen kann. Das macht mehr Sinn als 20 Bit Samples aus der Tabelle zu bekommen.
Meine Empfehlungen: Mache den Phasenzähler so genau wie möglich, um Jitter zu vermeiden. Bei meinem DDS zählt der Phasenzähler mit 36 Bits, wovon 32 Bits als Phasenincrement-Wert verwendet werden. Der erweiterte Bereich (bis 36 Bits) wird im phasenstarren Suboscillator-Modus verwendet (Frequenz bis /16 runtergeteilt). Der Phasenincrement-Wert (32 Bits) wird dann mit dem Phasenoffset-Wert (32 Bits) verrechnet. Vom Ergebnis gehen die oberen 11 Bits als Adresse zur Sampletabelle (dadurch habe ich 2048 Werte pro Periode), weitere 17 Bits werden für die Sample-Interpolation verwendet. Die untersten 4 Bits werden zur Sampleberechnung nicht verwendet.
Ein Sample-Rohwert hat bei mir 18 Bits Wortbreite (da die XDSP-Cores im FPGA auch 18 Bits Wortbreite haben). Zwischen den Rohwerten mache ich einfache lineare Interpolation. Für noch genauere Berechnung der Sample-Zwischenwerte (z.B. durch Taylor-Reihen) habe ich keine Rechenkapazität. Eine oberflächlliche Spektralanalyse ergab eine für mich ausreichende spektrale Reinheit des erzeugten Sinus-Signals :)
Und BTW wenn du dein DDS-Design eh schon änderst, dann baue den Phasenoffset gleich mit ein. Den brauchst du dann später für die Phasenmodulation :)

Aber, ich will dir da auch nicht reinreden, ich laber eh schon ungefragt zu viel :flowerjaeger:
 
Zuletzt bearbeitet:


Zurück
Oben