Synthesizer Selbstbau im Xilinx-FPGA mit MIDI-Ansteuerung

Frage an die Schwarmintelligenz und vielleicht auch an @rolfdegen im Speziellen:
Habt ihr schonmal bandbreitenlimitierte Waveforms selbst erstellt? Bis zu wieviel Harmonischen Anteilen kann man da gehen? Oder ist das empirisch?

meines erachtes ist das - bei additiver synthese, modal usw. - eine reine frage des verhältnisses von rechenleistung <-> ergebnis.

immerhin braucht ja jede partial neben dem generator auch noch die amplitudenmodulation zur begrenzung.

wenn du den generator auch noch in signalrate über mehrere oktaven hinweg schnell in der frequenz modulieren willst, kannst du ja nicht einfach bei nyquist auf null setzen weil das sonst klickklackt, sondern willst mindestens mal eine ramp dafür haben, vielleicht sogar eine s-kurve.

okay, wenn du nur zwischen wavetables interpolierst (und das ja quasi eh machst), mag das okay sein.

in meinen wavetables mache ich das bis 64 - allerdings nur deswegen, weil ich mir erlaube, den grundton willkürlich auf höher als 1 zu setzen, damit ich nicht-gerade zahlenverhältnisse in klängen haben kann.


es sollte ansonsten für eine dreieckswelle total okay sein nur 16 zu machen.

für square und pulse finde ich 32 eher angemessen, genau wie du es auch machst. auch 64 kann sinn machen, wenn du denn in o.g. dingen auch so pingelig bist oder solche ansprüche hast.

für ein impuls train wären 64 wohl pflicht. :)


also noch mal abschließend: falls 64 partials in einem generator 4 mal so viel (oder bis zu 4 mal so viel je nach grundton) rechenleistung kosten wie 16 partials, und dir das aber wert ist, dass deine tiefbass patches als bäss noten gespielt bis in den brillianzbereich noch was drin haben, dann mach das einfach.


so ganz verstehe ich allerdings die aufgabenstellung nicht.

wenn du doch 32 zur verfügung hast, dann kannst die doch ohne probleme auch gleich für alle wellenformen benutzen?

wo es nachher weniger sein sollen, ist ja nur eine frage davon welches wavetable für welche stimmlage gedacht ist.
 
Zuletzt bearbeitet:
Danke schonmal für die Antwort: um additive Synthese geht es hier erstmal nicht, dafür habe ich bisher noch keine Engergie gehabt ;-) Rechenleistung wäre wohl im FPGA ausreichend vorhanden, in der Software jedoch nicht.
Die Sampletabellen sind mit Excel vorberechnet (auch die Amplitudenanteile) und dann im RAM gespeichert, werden also zur Laufzeit nicht erzeugt oder berechnet sondern einfach nur ausgelesen. Daher würde man ja, bei entsprechend hohem gespielten Ton, auch trotzdem noch über die Bandbreite der Tonerzeugung kommen (also nehmen wir 2kHz gespielt -> 31. Harmonische (62kHz) trotzdem problematisch). Es gibt halt keinen Analog-Filter vor dem DAC, welcher diesen vor zu hohen Frequenzanteilen "beschützt".
Aufgabenstellung gab es hier keine, die Möglichkeit der Wavetable-Synthese ergab sich jetzt einfach, weil ich das auf eigene Sample-Tabellen umgebaut habe. Die Erzeugung der anderen einfachen Waveforms ist davon erstmal unberührt und z.B. im Falle der pulsweitenmodulierbaren Rechteckwelle auch nicht durch die Tabelle zu ersetzen.

Die bisher unbenutzten Tables werde ich wohl erstmal für Experimente benutzen ;-)

Grüße,
Dirk
 
Rein vom Speicherplatz gesehen: In 1024 Samples kannst Du maximal bis Faktor 512 gehen. Alles darüber würde Aliasing direkt in die Tabelle schreiben. Kann witzig sein, will man aber meist nicht.

Die genannte Grenze verhindert aber nicht das Aliasing beim Abspielen dieser Tabelle, sofern Du mit einer festen Samplerate arbeitest. Um das zu lösen müssen pro Waveform weitere Tabellen mit entsprechend weniger Partials angelegt und abhängig von Notenfrequenz und Nyquistrate (also Samplerate / 2) ausgewählt werden. Stichwort: Mip - Mapping.

Im Extremfall hättest Du dann für 512 Partials 513 Tabellen:

die erste (Tabelle #0) ist leer - die wird abgespielt, wenn sogar schon der Grundton über Nyquistrate liegt
die zweite (Tabelle #1) enthält maximal nur das erste Partial
die dritte (Tabelle #2) enthält maximal die ersten beiden Partials
...bla...
..blubb...
... und in der fünfhundertdreizehnten (Tabelle #512) sind alle 512 Partials erlaubt.

Dann berechnest Du beim Abspielen anhand der Nyquistrate und der gespielten Frequenz, wieviel Bandbreite (also wieviel Partials) Dir noch bis zur Nyquistrate bleiben. Diese Zahl muss für das vorangegange Beispiel zwischen 0 und 512 begrenzt werden. Damit kannst Du dann direkt die korrekt bandlimitierte Tabelle auswählen. So weit das Grundprinzip.

Aber man muss dann auch abwägen:

Mit wenigen Partials ist es einfacher, man kommt mit wenigen Mip - Map - Tabellen aus. Allerdings klingt das bei tiefen Noten oft wie durch Omas Telefon (fehlende Obertöne, Klingelphänomene, Wattepunch...).

Mit vielen Partials wird es gerade im Bassbereich klanglich besser, aber man muss viel Speicher für die vielen Tabellen opfern. Tut man letzteres nicht und macht "Sprünge" (wo dann gleich zig Partials in der nächsten Tabelle fehlen), kann sich das in entsprechenden Knacksern bei der Pitchmodulation bemerkbar machen. Crossfades helfen hier nur bedingt (meiner Erfahrung nach eigentlich gar nicht), denn bei Pitchmodulationen via schnellem LFO o.ä. bleibt für einen sanften Crossfade gar nicht die Zeit und man bekommt unschönes Britzeln, weil Tabellen mit stark abweichendem Obertongehalt mit hoher Frequenz wechseln. Auch hier muss man ggf. ausprobieren, was einem klanglich genügt.

Die Ressourcen sind fast immer zu begrenzt, um einen reinen und idealen Ansatz zu realisieren - sei es nun Speicherlimit, Zugriffslatenz oder auch Rechenkapazität. Man kann diese Punkte durch Kombination von Techniken umschiffen, aber dann wächst auch die Komplexität. Und die kann auch ein Limit sein.
 
Daher würde man ja, bei entsprechend hohem gespielten Ton, auch trotzdem noch über die Bandbreite der Tonerzeugung kommen

ach so, ich bin natürlich davon ausgegangen, dass du dann schon auch verschiedene wavetables für verschiedene tonhöhen benutzt.

wenn nicht, dann ist die darin versteckte aufgabenstellung (aber diesmal für dich): finde heraus, wie hohe noten du mit dem ding spielen willst, und dann musst du logischerweise mit entsprechend möglichst wenig obertönen auskommen.

schon bei 15 obertönen ist ja sonst bei 1400Hz tonhöhe schluss. du wärst dann also eher bei 3-5.


würde ich aber nicht machen. wer 5 wellenformen generieren kann, der kann auch 50 generieren und sie auf dem keyboard verteilen. (oder wie knapp ist der speicher?)
 
Ich danke euch Beiden, da bin ich wohl viel zu naiv rangegangen. Mir war bisher schon klar, daß mein Speicher nicht für Tabellen für komplexe Samples mit mehreren Perioden ausreicht, und das mit den x nötigen Tabellen für die verschiedenen Tonhöhen wird mir jetzt auch in aller Deutlichkeit klar (das Dilemma hatte sich schon beim Erstellen der Sample-Tabellen angedeutet ;-) ).
Speicher ist immer das Problem: Für die aktuelle Implementierung mit 32 Tabellen a 1024 Samples musste ich schon meine Delay-Lines der Hälfte ihrer FIFO-Größe berauben, was dort natürlich auch schmerzt. Auch die 8 Mb (ja Megabit) SRAM helfen da nicht groß weiter, zumal die schon hauptsächlich mit der Tabelle für die Exponentialfunktion belegt sind. Immerhin gäbe es noch 64 MB SDRAM, die ich bisher für Recording benutze. Da hätten etliche Tabellen Platz drin, muß ich mal brainstormen ob sich ein Umbau lohnt.

Oder vielleicht mache ich doch eher weiter mit dem ersten Ansatz: eine Sample-Tabelle mit 4096 Samples, in der eine Cosinus-Periode mit hoher Auflösung liegt. Alles andere muß dann auskommen mit den Grundwellenformen, diversen FM-Varianten, Waveforming (wobei das digital doch auch eigentlich viel zu viele Oberwellen erzeugt), Filter, Delay....

BTW Ich habe nach dem Mixer einen 12db SVF-Tiefpass (Sample-Rate 96kHz, feste Grenzfrequenz 16kHz) und Downsampling mit einfacher Antialiasing-Filterung (Averaging über 8 Samples), die den Obertonanteil des Endergebnisses etwas zähmen sollen. Nicht perfekt, aber besser als nix m.E. (DAC läuft mit 48kHz Sample-Rate).

Es bleibt für mich trotz aller Beschränkungen ein spannender Experimentier-Spielplatz :)

Danke und Grüße,
Dirk
 
Mein Synthi hat wieder mal Erweiterungen erhalten:
Abgesehen von neuen experimentellen FM-Varianten (FM durch exponentielle Phasenmodulation, Ramp-FM und Ringmodulated-FM) werden jetzt die Sinus/Cosinus-Samples aus einem RAM ausgelesen und vom selben Generator (DDS) angesteuert wie die anderen Waveforms (vorher war dies ein vorkonfigurierter IP-Core mit Samples im ROM).
Interessanter Nebeneffekt und BTW die eigentliche Erweiterung:
Da ich nun auch andere Waveforms in diesem RAM ablegen kann, habe ich nun ganz nebenbei einen einfachen Wavetable-Synthesizer. Zur Zeit kann er aber nur 32 Tabellen mit jeweils 1024 Samples.
Tabelle #0 enthält die Standard-Cosinus-Welle (die ich per Default immer drinlasse, da diese benötigt wird für die LFOs im Stringmodus und für die Amplitudenmodulation im Ramp-FM-Modus.
Tabelle #1 bekommt erstmal eine bandbreitenlimitierte Dreieck-Waveform und
Tabelle #2 bekommt eine bandbreitenlimitierte Rechteck-Waveform (50:50).

Frage an die Schwarmintelligenz und vielleicht auch an @rolfdegen im Speziellen:
Habt ihr schonmal bandbreitenlimitierte Waveforms selbst erstellt? Bis zu wieviel Harmonischen Anteilen kann man da gehen? Oder ist das empirisch?
Meine Dreieck-Waveform geht erstmal bis zur 23. und die Rechteckwaveform bis zur 31. Harmonischen (siehe Screenshots vom Excel, mit dem ich die erzeugt habe).
Anhang anzeigen 173597
Anhang anzeigen 173598

Grüße,
Dirk

Bandlimitierte Wellenformen in der Jeannie

C:
// BandLimitedWaveform

class BandLimitedWaveform
{
public:
  BandLimitedWaveform (void) ;
  int16_t generate_sawtooth (uint32_t new_phase, int i) ;
  int16_t generate_square (uint32_t new_phase, int i) ;
  int16_t generate_pulse (uint32_t new_phase, uint32_t pulse_width, int i) ;
  void init_sawtooth (uint32_t freq_word) ;
  void init_square (uint32_t freq_word) ;
  void init_pulse (uint32_t freq_word, uint32_t pulse_width) ;
 

private:
  int32_t lookup (int offset) ;
  void insert_step (int offset, bool rising, int i) ;
  int32_t process_step (int i) ;
  int32_t process_active_steps (uint32_t new_phase) ;
  int32_t process_active_steps_saw (uint32_t new_phase) ;
  int32_t process_active_steps_pulse (uint32_t new_phase, uint32_t pulse_width) ;
  void new_step_check_square (uint32_t new_phase, int i) ;
  void new_step_check_pulse (uint32_t new_phase, uint32_t pulse_width, int i) ;
  void new_step_check_saw (uint32_t new_phase, int i) ;
 
 
  uint32_t phase_word ;
  int32_t dc_offset ;
  step_state states [32] ; // circular buffer of active steps
  int newptr ;         // buffer pointers into states, AND'd with PTRMASK to keep in buffer range.
  int delptr ;
  int32_t  cyclic[16] ;    // circular buffer of output samples
  bool pulse_state ;
  uint32_t sampled_width ; // pulse width is sampled once per waveform
};

#define SUPPORT_SHIFT 4
#define SUPPORT (1 << SUPPORT_SHIFT)
#define PTRMASK ((2 << SUPPORT_SHIFT) - 1)

#define SCALE 16
#define SCALE_MASK (SCALE-1)
#define N (SCALE * SUPPORT * 2)

#define GUARD_BITS 8
#define GUARD      (1 << GUARD_BITS)
#define HALF_GUARD (1 << (GUARD_BITS-1))


#define DEG180 0x80000000u

#define PHASE_SCALE (0x100000000L / (2 * BASE_AMPLITUDE))


extern "C"
{
    extern const int16_t step_table [258] ;
}

int32_t BandLimitedWaveform::lookup (int offset)
{
    int off = offset >> GUARD_BITS ;
    int frac = offset & (GUARD-1) ;

    int32_t a, b ;
    if (off < N/2)   // handle odd symmetry by reflecting table
    {
        a = step_table [off+1] ;
        b = step_table [off+2] ;
    }
    else
    {
        a = - step_table [N-off] ;
        b = - step_table [N-off-1] ;
    }
    return  BASE_AMPLITUDE + ((frac * b + (GUARD - frac) * a + HALF_GUARD) >> GUARD_BITS) ; // interpolated
}

// create a new step, apply its past waveform into the cyclic sample buffer
// and add a step_state object into active list so it can be added for the future samples
void BandLimitedWaveform::insert_step (int offset, bool rising, int i)
{
    while (offset <= (N/2-SCALE)<<GUARD_BITS)
    {
        if (offset >= 0)
        cyclic [i & 15] += rising ? lookup (offset) : -lookup (offset) ;
        offset += SCALE<<GUARD_BITS ;
        i ++ ;
    }

    states[newptr].offset = offset ;
    states[newptr].positive = rising ;
    newptr = (newptr+1) & PTRMASK ;
}

// generate value for current sample from one active step, checking for the
// dc_offset adjustment at the end of the table.
int32_t BandLimitedWaveform::process_step (int i)
{
    int off = states[i].offset ;
    bool positive = states[i].positive ;

    int32_t entry = lookup (off) ;
    off += SCALE<<GUARD_BITS ;
    states[i].offset = off ;  // update offset in table for next sample
    if (off >= N<<GUARD_BITS)             // at end of step table we alter dc_offset to extend the step into future
    dc_offset += positive ? 2*BASE_AMPLITUDE : -2*BASE_AMPLITUDE ;

    return positive ? entry : -entry ;
}

// process all active steps for current sample, basically generating the waveform portion
// due only to steps
// square waves use this directly.
int32_t BandLimitedWaveform::process_active_steps (uint32_t new_phase)
{
    int32_t sample = dc_offset ;
 
    int step_count = (newptr - delptr) & PTRMASK ;
    if (step_count > 0)        // for any steps in-flight we sum in table entry and update its state
    {
        int i = newptr ;
        do
        {
            i = (i-1) & PTRMASK ;
            sample += process_step (i) ;
        } while (i != delptr) ;
        if (states[delptr].offset >= N<<GUARD_BITS)  // remove any finished entries from the buffer.
        {
            delptr = (delptr+1) & PTRMASK ;
            // can be upto two steps per sample now for pulses
            if (newptr != delptr && states[delptr].offset >= N<<GUARD_BITS)
            delptr = (delptr+1) & PTRMASK ;
        }
    }
    return sample ;
}

// for sawtooth need to add in the slope and compensate for all the steps being one way
int32_t BandLimitedWaveform::process_active_steps_saw (uint32_t new_phase)
{
    int32_t sample = process_active_steps (new_phase) ;

    sample += (int16_t) ((((uint64_t)phase_word * (2*BASE_AMPLITUDE)) >> 32) - BASE_AMPLITUDE) ;  // generate the sloped part of the wave

    if (new_phase < DEG180 && phase_word >= DEG180) // detect wrap around, correct dc offset
    dc_offset += 2*BASE_AMPLITUDE ;

    return sample ;
}

// for pulse need to adjust the baseline according to the pulse width to cancel the DC component.
int32_t BandLimitedWaveform::process_active_steps_pulse (uint32_t new_phase, uint32_t pulse_width)
{
    int32_t sample = process_active_steps (new_phase) ;

    return sample + BASE_AMPLITUDE/2 - pulse_width / (0x80000000u / BASE_AMPLITUDE) ; // correct DC offset for duty cycle
}

// Check for new steps using the phase update for the current sample for a square wave
void BandLimitedWaveform::new_step_check_square (uint32_t new_phase, int i)
{
    if (new_phase >= DEG180 && phase_word < DEG180) // detect falling step
    {
        int32_t offset = (int32_t) ((uint64_t) (SCALE<<GUARD_BITS) * (sampled_width - phase_word) / (new_phase - phase_word)) ;
        if (offset == SCALE<<GUARD_BITS)
        offset -- ;
        if (pulse_state) // guard against two falling steps in a row (if pulse width changing for instance)
        {
            insert_step (- offset, false, i) ;
            pulse_state = false ;
        }
    }
    else if (new_phase < DEG180 && phase_word >= DEG180) // detect wrap around, rising step
    {
        int32_t offset = (int32_t) ((uint64_t) (SCALE<<GUARD_BITS) * (- phase_word) / (new_phase - phase_word)) ;
        if (offset == SCALE<<GUARD_BITS)
        offset -- ;
        if (!pulse_state) // guard against two rising steps in a row (if pulse width changing for instance)
        {
            insert_step (- offset, true, i) ;
            pulse_state = true ;
        }
    }
}

// Checking for new steps for pulse waveform has to deal with changing frequency and pulse width and
// not letting a pulse glitch out of existence as these change across a single period of the waveform
// now we detect the rising edge just like for a square wave and use that to sample the pulse width
// parameter, which then has to be checked against the instantaneous frequency every sample.
void BandLimitedWaveform::new_step_check_pulse (uint32_t new_phase, uint32_t pulse_width, int i)
{
    if (pulse_state && phase_word < sampled_width && (new_phase >= sampled_width || new_phase < phase_word))  // falling edge
    {
        int32_t offset = (int32_t) ((uint64_t) (SCALE<<GUARD_BITS) * (sampled_width - phase_word) / (new_phase - phase_word)) ;
        if (offset == SCALE<<GUARD_BITS)
        offset -- ;
        insert_step (- offset, false, i) ;
        pulse_state = false ;
    }
    if ((!pulse_state) && phase_word >= DEG180 && new_phase < DEG180) // detect wrap around, rising step
    {
        // sample the pulse width value so its not changing under our feet later in cycle due to modulation
        sampled_width = pulse_width ;

        int32_t offset = (int32_t) ((uint64_t) (SCALE<<GUARD_BITS) * (- phase_word) / (new_phase - phase_word)) ;
        if (offset == SCALE<<GUARD_BITS)
        offset -- ;
        insert_step (- offset, true, i) ;
        pulse_state = true ;
     
        if (pulse_state && new_phase >= sampled_width) // detect falling step directly after a rising edge
        //if (new_phase - sampled_width < DEG180) // detect falling step directly after a rising edge
        {
            int32_t offset = (int32_t) ((uint64_t) (SCALE<<GUARD_BITS) * (sampled_width - phase_word) / (new_phase - phase_word)) ;
            if (offset == SCALE<<GUARD_BITS)
            offset -- ;
            insert_step (- offset, false, i) ;
            pulse_state = false ;
        }
    }
}

// new steps for sawtooth are at 180 degree point, always falling.
void BandLimitedWaveform::new_step_check_saw (uint32_t new_phase, int i)
{
    if (new_phase >= DEG180 && phase_word < DEG180) // detect falling step
    {
        int32_t offset = (int32_t) ((uint64_t) (SCALE<<GUARD_BITS) * (DEG180 - phase_word) / (new_phase - phase_word)) ;
        if (offset == SCALE<<GUARD_BITS)
        offset -- ;
        insert_step (- offset, false, i) ;
    }
}

// the generation function pushd new sample into cyclic buffer, having taken out the oldest entry
// to return.  The output is thus 16 samples behind, which allows the non-casual step function to
// work in real time.
int16_t BandLimitedWaveform::generate_sawtooth (uint32_t new_phase, int i)
{
    new_step_check_saw (new_phase, i) ;
    int32_t val = process_active_steps_saw (new_phase) ;
    int16_t sample = (int16_t) cyclic [i&15] ;
    cyclic [i&15] = val ;
    phase_word = new_phase ;
    return sample ;
}

int16_t BandLimitedWaveform::generate_square (uint32_t new_phase, int i)
{
    new_step_check_square (new_phase, i) ;
    int32_t val = process_active_steps (new_phase) ;
    int16_t sample = (int16_t) cyclic [i&15] ;
    cyclic [i&15] = val ;
    phase_word = new_phase ;
    return sample ;
}

int16_t BandLimitedWaveform::generate_pulse (uint32_t new_phase, uint32_t pulse_width, int i)
{
    new_step_check_pulse (new_phase, pulse_width, i) ;
    int32_t val = process_active_steps_pulse (new_phase, pulse_width) ;
    int32_t sample = cyclic [i&15] ;
    cyclic [i&15] = val ;
    phase_word = new_phase ;
    return (int16_t) ((sample >> 1) - (sample >> 5)) ; // scale down to avoid overflow on narrow pulses, where the DC shift is big
}

void BandLimitedWaveform::init_sawtooth (uint32_t freq_word)
{
    phase_word = 0 ;
    newptr = 0 ;
    delptr = 0 ;
    for (int i = 0 ; i < 2*SUPPORT ; i++)
    phase_word -= freq_word ;
    dc_offset = phase_word < DEG180 ? BASE_AMPLITUDE : -BASE_AMPLITUDE ;
    for (int i = 0 ; i < 2*SUPPORT ; i++)
    {
        uint32_t new_phase = phase_word + freq_word ;
        new_step_check_saw (new_phase, i) ;
        cyclic [i & 15] = (int16_t) process_active_steps_saw (new_phase) ;
        phase_word = new_phase ;
    }
}


void BandLimitedWaveform::init_square (uint32_t freq_word)
{
    init_pulse (freq_word, DEG180) ;
}

void BandLimitedWaveform::init_pulse (uint32_t freq_word, uint32_t pulse_width)
{
    phase_word = 0 ;
    sampled_width = pulse_width ;
    newptr = 0 ;
    delptr = 0 ;
    for (int i = 0 ; i < 2*SUPPORT ; i++)
    phase_word -= freq_word ;

    if (phase_word < pulse_width)
    {
        dc_offset = BASE_AMPLITUDE ;
        pulse_state = true ;
    }
    else
    {
        dc_offset = -BASE_AMPLITUDE ;
        pulse_state = false ;
    }
 
    for (int i = 0 ; i < 2*SUPPORT ; i++)
    {
        uint32_t new_phase = phase_word + freq_word ;
        new_step_check_pulse (new_phase, pulse_width, i) ;
        cyclic [i & 15] = (int16_t) process_active_steps_pulse (new_phase, pulse_width) ;
        phase_word = new_phase ;
    }
}

BandLimitedWaveform::BandLimitedWaveform()
{
    newptr = 0 ;
    delptr = 0 ;
    dc_offset = BASE_AMPLITUDE ;
    phase_word = 0 ;
}

Anhang anzeigen Bandlimited Square.mp3

Gruß Rolf
 
Zuletzt bearbeitet:
wenn nicht, dann ist die darin versteckte aufgabenstellung (aber diesmal für dich): finde heraus, wie hohe noten du mit dem ding spielen willst, und dann musst du logischerweise mit entsprechend möglichst wenig obertönen auskommen.

...

wer 5 wellenformen generieren kann, der kann auch 50 generieren und sie auf dem keyboard verteilen. (oder wie knapp ist der speicher?)

Ok, Challenge accepted 😁

Ich habe jetzt 31 Sample-Tabellen mit vorberechneten (*) bandlimitierten Sägezahnwellen mit 3 bis 63 Partials gefüllt und wähle diese zur Laufzeit in Abhängigkeit von der gespielten Tonhöhe aus (funktioniert auch mit Transposing, Portamento, Modulationen). Und bei den hohen Tönen klingt es auf jeden Fall (zumindest bei einzeln gespielten Sägezahn-Tönen) besser als die digital erzeugte Sägezahnrampe (die ist vom DDS-Phaseinkrement-Zähler abgeleitet, also damit unlimitiert). Bei Mischtönen (z.B. SuperSaw) bin ich noch am Bewerten was besser ist ;-)
Sicher ist es gut, diese Möglichkeit zu haben, denn ungefiltert klingt das so besser.

Testberechnung für steigende Sägezahnwelle, 31 Partials, 1024 Samples pro Periode, 18 Bits Amplitudenauflösung:

bl-sawp-32p-v1.PNG

(*) Einschränkung durch die Vorausberechnung: ich kann die Amplitudenanteile nicht mehr zur Laufzeit berechnen, aber das ist momentan eh nicht im Konzept vorgesehen.

Grüße,
Dirk
 
Zuletzt bearbeitet:
es gibt da keine feste regel, aber mach die waves einfach bis SR/4, dann kannst du jedes ereignis während es läuft um bis zu eine oktave nach oben pitchen (summe aus allen modulationsquelle...wheel, vel, envelope, portamento...), das ist ein ganz guter kompromiss.

das lässt sich ansonsten ja bei bedarf auch einfach hoch und runterschieben, indem du einfach nur die eingehenden noten anders interpretierst.
 
Ich habe mich mal wieder mit meinem FPGA-Synthesizer beschäftigt und eine meiner schrägen Ideen (aber auch Anregungen aus vorherigen Beiträgen zum Thema bandlimitierte Waveform) umsetzen können.
Mein Synthi kann zur Zeit zwar nur maximal 12 Oszillatoren (ist also demzufolge maximal 12-stimmig), hat jedoch mit der letzten Erweiterung folgendes Zusatzfeature bekommen:
Die Erzeugung der Waveform jedes einzelnen Oszillators beinhaltet jetzt (zusätzlich zu den bisherigen 'einfach' Waveforms) die Erzeugung von bis zu 16 phasensynchronen Partial-Tönen, wobei jeder einzelne von diesen wie folgt konfigurierbar ist:
- Faktor/Teiler x/y mit x = [1 .. 2048] und y = [1 | 1024]
- Feintuning (positiv oder negativ, für DeTuning)
- Waveform-Auswahl aus den 'einfachen' Waveforms (also Sinus, Dreieck, Sägezahn, Rechteck)
- einstellbarer Volumenanteil (Amplitudenfaktor, positiv oder negativ)
- modulierbarer Volumenanteil (z.B. von einem LFO)
- automatische schrittweise Begrenzung des Partial-Anteils, wenn die Frequenz des erzeugten Partial-Tones über 16 kHz kommt
- automatisches "Abwürgen" wenn die Frequenz des erzeugten Partial-Tones über der Nyquist-Frequenz liegt

Alle Effekte auf den Oszillator-Kanal (z.B. Pitch, Tuning, Portamento, Modulation) wirken gleichermaßen auf alle Partials dieses Oszillators.

Mit dieser Flexibiltät lassen sich diverse Dinge anstellen:
- Erzeugung diverser Harmonischer ("Oberwellen")
- Erzeugung diverser Subharmonischer ("Unterwellen" also z.B. 1/2, 1/3, 1/4, 1/5 usw.)
- flexible Suboszillatoren (das bisherige, relativ unflexible Suboszillator-Feature wurde hierdurch ersetzt)
- additive Synthese und damit auch bandlimitierte Waveforms (z.Z. begrenzt auf 15 Obertöne)

BTW Die Realisierung im FPGA bedeutet sinngemäß, daß die Synthese in Hardware stattfindet (wie die komplette Tonbearbeitung). Das Timing der Berechnung ist relativ anspruchvoll (siehe Eckdaten im Eröffnungsbeitrag), und ich schau mal ob ich die Partial-Anzahl noch erhöhen könnte (eine Verdopplung auf 32 erscheint schonmal realisierbar).

Anbei ein Klimper-Beispiel für Subharmonische ab 0:07 (davor einfache Sägezahn und Sinus-Waveforms zum Vergleich).

Grüße,
Dirk
 

Anhänge

  • capture-samples-033-Sinus-Subharmonics-20b-normalized.mp3
    601,9 KB
Zuletzt bearbeitet:
...
- Faktor/Teiler mit ganzzahligen x/y mit x = [1 .. 2048] und y = [1 | 1024]
...
Das klingt für mich recht interessant, falls ich das richtig verstehe:

Man angenommen der Oszillator läuft mit 440Hz, heißt das dann, dass du zusätzliche "Teilwellen" in Frequenzen von 440Hz / (2 bis X), also 220Hz, 110Hz usw., bzw. 440Hz * (2 bis X) dazu summieren kannst? Und davon dann 16 Stück? Damit ließe sich sicherlich schon klanglich allerhand bewerkstelligen! Ich würde auch immer auf Modulationsmöglichkeiten an diesen Stelle achten, dann wird das sicherlich ein Wahnsinnsteil!

Viel Erfolg damit.
 
Das klingt für mich recht interessant, falls ich das richtig verstehe:

Man angenommen der Oszillator läuft mit 440Hz, heißt das dann, dass du zusätzliche "Teilwellen" in Frequenzen von 440Hz / (2 bis X), also 220Hz, 110Hz usw., bzw. 440Hz * (2 bis X) dazu summieren kannst? Und davon dann 16 Stück? Damit ließe sich sicherlich schon klanglich allerhand bewerkstelligen! Ich würde auch immer auf Modulationsmöglichkeiten an diesen Stelle achten, dann wird das sicherlich ein Wahnsinnsteil!

Viel Erfolg damit.

Ja genauso wie du es beschrieben hast, auch krumme Teiler und zwar alles was sich mit f * x / 1024 berechnen lässt (bspw. 1/3 wird realisiert durch f * 341 / 1024). Jeder Partial-Volumenanteil ist separat modulierbar, hab ich jedoch noch nicht ausprobiert.

Im oben angehängten Beispiel-Sound ab 0:07 sind die Subharmonics z.B. f/1, f/2, f/3 bis f/16 jeweils mit leicht abnehmendem Volumenanteil.

Grüße,
Dirk
 
Bei den Faktoren bis 2048 müsstest du doch recht schnell außerhalb der Hörfrequenzen liegen? Mehr als 10 dürfte da nicht viel bringen, je nachdem, welche die Basisfrequenz ist. Oder missverstehe ich da noch etwas?

Aber es wäre sicherlich klanglich superinteressant, wenn man die Partiale in der Phase verschieben könnte und zwar einzeln. Das müsste dann so ein wenig wie Supersaw auf allen Wellenformen und mehreren Oktaven gleichzeitig klingen. Da kann ich mir richtig fette Sounds vorstellen!
 
- automatische schrittweise Begrenzung des Partial-Anteils, wenn die Frequenz des erzeugten Partial-Tones über 16 kHz kommt
- automatisches "Abwürgen" wenn die Frequenz des erzeugten Partial-Tones über der Nyquist-Frequenz liegt


wenn du weißt, welche partial in einer harmonischen schwingung die höchste ist, dann langt es, das "limitieren" damit zu steuern.

denn alle anderen sind ja eh niedriger. :)


in der einfachsten version benutze ich ungefähr das hier:

( (min(max(F, (SR/4) ), (SR/2) ) ) - (SR/2) ) / (-SR/4) = A

wobei F die aktuell gespielte frequenz in hertz des höchsten obertons in der wavetable wave ist und A der faktor, mit dem man die amplitude des gesamten oscillators absenken muss.

- verlauf ist linear

- sytem ist zu 100% in signalrate modulierbar

- aliasing ist zu 100% weg

- man braucht weder höhere mathematik noch ein lookup table, so dass die sache recht ressourcenschonend über die bühne geht.

nachteile gibt es auch: 🎅🎄🤶🎁🎁🤶📦🎁📦

- irgendwo muss im wavetable niedergeschrieben sein, welches die höchste partial in einer wave im gesamten wavetable ist. das kann auswirkungen auf wavetabledurchanwendererstellungsgedöns haben.

- bei SR/4 ist ein knick, der bei sehr, sehr schnellem wechsel zwischen über und unter der grenzfrequenz (LFO im stimmbereich auf tuning?) nun seinerseits rein theroetisch einen knackser verursacht.


1702744765040.png


eine lineare absenkung über die gesamte obere oktave halte ich für einen angemessenen kompromiss zwischen allem, was so denkbar ist.
 
Zuletzt bearbeitet:
Bei den Faktoren bis 2048 müsstest du doch recht schnell außerhalb der Hörfrequenzen liegen? Mehr als 10 dürfte da nicht viel bringen, je nachdem, welche die Basisfrequenz ist. Oder missverstehe ich da noch etwas?

Aber es wäre sicherlich klanglich superinteressant, wenn man die Partiale in der Phase verschieben könnte und zwar einzeln. Das müsste dann so ein wenig wie Supersaw auf allen Wellenformen und mehreren Oktaven gleichzeitig klingen. Da kann ich mir richtig fette Sounds vorstellen!

Nunja, als reiner Faktor wär das schon recht viel, ja. Jedoch, wenn ich den Teiler fest auf 1024 stelle (was die Steuerung per Software zur Laufzeit fallweise vereinfacht), dann bleibt bei 2048/1024 nur noch f*2 übrig. Ressourcen verschwendet das nicht, der Multiplizierer-Block kann eh bis 18 Bits Wortbreite, wird also auch bei 2048 gar nicht voll benutzt. Reine SW-Optimierung ;-)

Theoretisch könnte ich auch die Phase pro Partial konfigurierbar machen, in der Berechnung ist die Phase ja sowieso mit dabei. Das würde nur einen RAM-Block und einen Addierer mehr benötigen. Ist momentan nicht vorgesehen, jedoch eine interessante Idee. Danke schonmal für die Anregung. SuperSaw mit 16 Sägezähnen.....:banane:

Grüße,
Dirk
 
wenn du weißt, welche partial in einer harmonischen schwingung die höchste ist, dann langt es, das "limitieren" damit zu steuern.

denn alle anderen sind ja eh niedriger. :)


in der einfachsten version benutze ich ungefähr das hier:

( (min(max(F, (SR/4) ), (SR/2) ) ) - (SR/2) ) / (-SR/4) = A

wobei F die aktuell gespielte frequenz in hertz des höchsten obertons in der wavetable wave ist und A der faktor, mit dem man die amplitude des gesamten oscillators absenken muss.

Hehe, in Software mag das machbar sein, in Hardware kostet das zwei Komparatoren, eine Subtraktion, und ggf. eine Division (?, obwohl, der Term mit der Division ist konstant...) und diverse Rechenzyklen. Eine Berechnung mit Lookup-Tabelle kostet mich genau einen RAM-Block (da war zufällig noch einer übrig ;-) ), einen Multiplizierer-Block (noch >40 übrig) und genau zwei Takte Rechenzeit. Und bei der Berechnung ist ja klar, welche Frequenz jeder Partial hat, da kann man das dann gleich ohne Umwege mitberechnen.
 
Zuletzt bearbeitet:
... Danke schonmal für die Anregung. SuperSaw mit 16 Sägezähnen.....:banane:
Gerne, wenn ich irgendwann zu Lebzeiten auch ein Testgerät in den Händen halten kann, fallen mir vielleicht noch viel mehr Dinge ein. ;-)

Ich dachte da eigentlich eher an SuperTriangle mit 16 Zacken. :D Und mit ein wenig Glück verschießt das Teil dann auch Blitze, wie bei Poseidon, nur in 5 1/3-facher Anzahl und Stärke.

Oder für friedfertigen unter uns: 16fache phasenverschobene Sinuswelle.
 
Theoretisch könnte ich auch die Phase pro Partial konfigurierbar machen, in der Berechnung ist die Phase ja sowieso mit dabei. Das würde nur einen RAM-Block und einen Addierer mehr benötigen. Ist momentan nicht vorgesehen, jedoch eine interessante Idee. Danke schonmal für die Anregung. SuperSaw mit 16 Sägezähnen.....:banane:
Oh, Denkfehler: die Verstellung der Phase alleine erzeugt keine SuperSaw, dazu müssten man die Frequenz verstimmen. Wäre aber auch machbar..... SuperSaw mit 16 Sägezähnen :banane:;-)
 
RAM-Block (da war zufällig noch einer übrig ;-) )

das hätte ich jetzt umgekehrt eingeschätzt, dass bei dir eher register/ram/rom geschichten problematisch sind und nicht die rechenleistung. (der größte fpga synth den ich kenne hat 4000 stimmen...)

wie hoch ist dein lookup table aufgelöst? für mein SR/4 trapez bräuchte ich hier nativ 11,000 werte (bzw immerhin noch 5000 wenn man sinnvollerweise erst bei 16k einsteigt) und am PC wird das dann in der tat teurer wie das bischen berechnung.
 
Oh, Denkfehler: die Verstellung der Phase alleine erzeugt keine SuperSaw, dazu müssten man die Frequenz verstimmen.
Deswegen sagte ich ja auch modulierbar (oder zumindest habe ich es gedacht), womit ich natürlich phasenmodulierbar meinte. Und ja, im Falle der Saw müsste das eine SuperSaw ergeben, oder zumindest klanglich etwas sehr Ähnliches.
 
wie hoch ist dein lookup table aufgelöst? für mein SR/4 trapez bräuchte ich hier nativ 11,000 werte (bzw immerhin noch 5000 wenn man sinnvollerweise erst bei 16k einsteigt) und am PC wird das dann in der tat teurer wie das bischen berechnung.

Die Bits [29:22] (also 8 Bits) des 32 bit breiten Phaseincrement-Wertes sind die Adresse für die "Volume-Degradation"-Tabelle, also insgesamt 256 Werte.
Bis 16 kHz keine Begrenzung (Faktor = 1), zwischen 16 kHz und 20 kHz ist der Volumenanteil linear abfallend (wie bei deinem Bildchen oben, siehe folgenden Tabellenauszug), ab 20 kHz ist der Faktor = 0.
Oberhalb des Wertebereichs der Tabelle, ab 24 kHz, wird der Partial sowieso hart gemutet.

Hier ein Auszug aus der Tabelle für Frequenzen zwischen 16kHz und 20kHz (Spalte 1 = Frequenz des Partials, Spalte 2 = Amplitudenfaktor des Partials):

1702753227217.png

Das ist erstmal vorläufig und ganz simpel, vielleicht fällt mir noch was besseres ein ;-)

Deswegen sagte ich ja auch modulierbar (oder zumindest habe ich es gedacht), womit ich natürlich phasenmodulierbar meinte. Und ja, im Falle der Saw müsste das eine SuperSaw ergeben, oder zumindest klanglich etwas sehr Ähnliches.

Ok, ich bau das grad ein, bin gespannt.... SuperSaw mit 16 Sägezähnen.... uiuiui :banane::D


Ich dachte da eigentlich eher an SuperTriangle mit 16 Zacken.

Oder für friedfertigen unter uns: 16fache phasenverschobene Sinuswelle.

Ja das wird dann auch gehen :connect:
 
Nunja, "SuperSaw-x16"-Modus ist nun auch fertig. Hört sich jedoch gar nicht soooo spektakulär an. Vielleicht muß ich den Sweet spot noch finden.

BTW Im angehängten Klimper-Sample ist die Oszillator-Synchronisierung eingeschaltet (d.h. alle Partials starten bei Phase Null beim Attack-Trigger), daher baut sich der SuperSaw-Effekt erst bei längerem Tastendruck auf.
 

Anhänge

  • capture-samples-034-Partials-SuperSaw-norm.mp3
    1,1 MB
Natürlich kann es nur "unspektakulär" klingen (obwohl ich das nicht wirklich finde), das dürfte ja auch logischerweise sehr stark der Wellenform geschuldet sein, oder anders gesagt: nimm mal Puls, da wird es NOCH unspektakulärer klingen. Und dann wäre es eventuell eine Idee diesen Gedanken um 180 Grad zu wenden.
 
Mit SuperRect ("Pulswelle" symmetrisch 50%) klingt das dann wie folgt:
 

Anhänge

  • capture-samples-035-Partials-SuperRect-norm.mp3
    546,6 KB
Worauf ich hinauswollte war: der Effekt dürfte am stärksten sein bei Wellenformen, die

1. Einen kleinen Obertongehalt haben
2. deren Unterschied in der Amplitude und der Steigung innerhalb zweier Wellenformen am größten ist (also Sinus und Dreieck, in die Richtung).
 
Mal ne Frage an die Waveform-Freaks:
Da ich ja grad wieder mit bandbreitenlimitierten Waveforms experimentiere und diese nun additiv synthetisieren kann, ist mir diese Waveform "passiert":

bandlimited-waveform-tri-from-sinus.PNG

Gibts einen Namen dafür? Sieht aus wie Sinus, ist es aber nicht. Die ist viel "runder". Erzeugt wird diese, wenn man versucht, eine bandlimitierte Triangle-Waveform aus Sinus-Grundwellenformen zu erzeugen statt aus Cosinus. Die Partial-Koeffizienten Oberton-Anteile sind ganz oben im Screenshot erkennbar.



Triangle aus Cosinüssen (bis Partial f*23) erzeugt sieht korrekterweise so aus:

bandlimited-waveform-tri-from-cosinus.PNG


Damit es nicht mit Sinus verwechselt wird hänge ich einen zum Vergleich auch mit ran:

waveform-sinus-from-sampletable.PNG


Grüße,
Dirk
 
Zuletzt bearbeitet:
Gibts einen Namen dafür? Sieht aus wie Sinus, ist es aber nicht. Die ist viel "runder". Erzeugt wird diese, wenn man versucht, eine bandlimitierte Triangle-Waveform aus Sinus-Grundwellenformen zu erzeugen statt aus Cosinus. Die Partial-Koeffizienten sind ganz oben im Screenshot erkennbar.
Wird klanglich ein wenig Richtung gefiltertes Rechteck gehen, mehr würde sich wahrscheinlich durch ein Spektrogramm erkennen lassen. Weniger Kanten bedeuten weniger Obertöne, der dominate Sinus darunter/dahinter macht den Sound was Bass lastiger.
Die Partial-Koeffizienten sind ganz oben im Screenshot erkennbar.
Die was? o.O Bin was Wellenformen betrifft eher intuitiv unterwegs, das Ergebnis erschließt sich mir aus Erfahrungswerten - viel Gebastel im Editor, so wie bei 'ner KI ;-)
 
Wird klanglich ein wenig Richtung gefiltertes Rechteck gehen, mehr würde sich wahrscheinlich durch ein Spektrogramm erkennen lassen.

Ich tät vermuten, daß das Spektogramm genau das zeigt, was die Obertonanteile in der Kopfzeile der Tabelle eben auch anzeigen: alle ungeradzahligen Oberwellen bis zu f*19 mit den angegebenen Volumenanteilen.
Ich denke auch daß es ein stark sinus-ähnlicher Ton sein wird, bin gespannt wie es klingt.


Die was? o.O Bin was Wellenformen betrifft eher intuitiv unterwegs, das Ergebnis erschließt sich mir aus Erfahrungswerten - viel Gebastel im Editor, so wie bei 'ner KI ;-)

Oh, da hab ich wohl ein Wort benutzt welches in diesem Zusammenhang nicht gebräuchlich ist. Vielleicht besser "Fourier-Koeffizient" oder "Obertonanteil"? ;-)
 
Zuletzt bearbeitet:
Oh, da hab ich wohl ein Wort benutzt welches in diesem Zusammenhang nicht gebräuchlich ist. Vielleicht besser "Fourier-Koeffizient" oder "Obertonanteil"? ;-)
Ok, jetzt hab' ichs verstanden, bin bei sowas eher der visuelle Typ, ich arbeite bei additiver Synthese mit Mustern, das funktioniert besser wenn sich das Spektrum zeichnen lässt.
Ich kenne natürlich die Skalierung nicht, aber die Werte sehen nach Rechteck aus, wobei die Level der (ungeraden) Harmonischen recht schnell sinken und weil die Phasen der Harmonischen wahrscheinlich 0 sind, ist das Bild eine gute Representation von dem was man am Ende zu hören bekommt.
 
Ich verstehe gerade nur nicht: Wozu diese ganze Diskussion? Warum nicht einfach Rechteck mit Filter dahinter? Ach so, die Frage ging in die Richtung "Was ist das?".

Dieses Beispiel zeigt vor allem eines: Bau deinen Synthesizer mit mindestens 2 hintereinander (und meinetwegen auch anders routebaren) Filtern.
 


Neueste Beiträge

News

Zurück
Oben