Wo versteckt sich der Zufall bzw. mein Denkfehler?

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
gotridofmyphone
User
Beiträge: 33
Registriert: Mittwoch 15. März 2017, 08:54

Hallo,

eingangs etwas Kontext: Die naturalistische Gestaltbarkeit von virtuellen Musikinstrumenten in meinem python-basierten Synthesizer ist so ziemlich fertig. Klänge sollen aber nicht nur in Abhängigkeit von der Tonhöhe variieren können, wie es bei physischen Musikinstrumenten üblich ist. Eine andere Achse der Variation ist die Tonstärke. So ein Klangereignis in seiner Gestalt wird also gleichzeitig von Tonhöhe wie von Tonstärke beeinflusst (je nach Gusto und Detailversessenheit des Instrumentengestalters auch von anderen Attributen einer Note).

Code: Alles auswählen

def _lessen_coords (cls, coords, by_num):
    """ The greater the value on the x-axis, the less its difference to
        x of the previous coordinate and the less also the amplitude (y),
        the more likely is that a coordinate gets dropped.
    """

    distances = {}

    oldlen = len(coords)

    for i, c in enumerate(coords[1:]):
        distances[c] = ( (c.x - coords[i].x) * c.y / c.x, coords[i] )

    sorted_distance_to_prev = [
         (c, d[1]) for c, d in sorted(
             distances.items(), key=lambda i: i[1][0]
         )
    ]

    new_coords = []
    for c, prev in sorted_distance_to_prev[0:by_num]:
         dist = 1.0 * ( c.y / (prev.y or c.y) if c.y else 0 )
         new_coords.append(
             c.weighted_average( prev, dist, c )
         )
         if c in distances:
             del distances[c]
         if prev in distances:
             del distances[prev]

    new_coords.extend( c for c in distances )
    new_coords.sort(key=lambda c: c.x)
    print("orig. len: {} | by_num: {} | is_num: {}".format(oldlen, by_num, len(new_coords)))
    return new_coords
Es handelt sich um eine Funktion, die Koordinaten auf eine Sollzahl reduziert – und zwar bezüglich des Endergebnisses minimalinvasiv –, damit das übergeordnete Objekt mit einem anderen kompatibel ist und die beiden wiederum in einem bestimmten Verhältnis zusammengemittelt werden können.

Das funktioniert. Manchmal. Irgendwie kommen bei jedem Lauf andere Werte raus. Manchmal geht es über das Ziel hinaus, manchmal läuft alles durch wie es soll, und ich frage mich, wie ich den Zufall aufspüre, und wo ich ein Denkfehler drin habe. Vielleicht springt euch der Fehler ins Auge, nur ich bin von Betriebsblindheit geschlagen.

Code: Alles auswählen

$ scripts/render-single-sound lib/instruments/dev/piano.spli -f 440 -l 3 --stress 0.75 --output /tmp/piano-s075.png
orig. len: 35 | by_num: 27 | is_num: 27
orig. len: 27 | by_num: 19 | is_num: 21
orig. len: 21 | by_num: 13 | is_num: 16
orig. len: 16 | by_num: 8 | is_num: 10
orig. len: 10 | by_num: 2 | is_num: 8
Will save graph image to file as requested ...
$ scripts/render-single-sound lib/instruments/dev/piano.spli -f 440 -l 3 --stress 0.75 --output /tmp/piano-s075.png
orig. len: 35 | by_num: 27 | is_num: 28
orig. len: 28 | by_num: 20 | is_num: 21
orig. len: 21 | by_num: 13 | is_num: 14
orig. len: 14 | by_num: 6 | is_num: 10
orig. len: 10 | by_num: 2 | is_num: 7
Traceback (most recent call last):
  File "scripts/render-single-sound", line 139, in <module>
    args.stress
  File ".../Sompyler/orchestra/instrument/__init__.py", line 31, in render_tone
    sg = self.root_variation.sound_generator_for(note)
  File ".../Sompyler/orchestra/instrument/__init__.py", line 217, in sound_generator_for
    note, forced_properties
  File ".../Sompyler/orchestra/instrument/combinators.py", line 22, in __call__
    return lastvar.sound_generator_for(note, forced_properties)
  File ".../Sompyler/orchestra/instrument/__init__.py", line 217, in sound_generator_for
    note, forced_properties
  File ".../Sompyler/orchestra/instrument/combinators.py", line 84, in __call__
    return left_sg.weighted_average( left_sg, dist, right_sg )
  File ".../Sompyler/synthesizer/sound_generator.py", line 162, in weighted_average
    l.morph, dist, r.morph
  File ".../Sompyler/synthesizer/sound_generator.py", line 159, in weighted_average
    self = super().weighted_average(l, dist, r, coord0=False)
  File ".../Sompyler/synthesizer/shape/__init__.py", line 344, in weighted_average
    rcoords = cls._adjust_coords_num(rcoords, avg_coords_num)
  File ".../Sompyler/synthesizer/shape/__init__.py", line 385, in _adjust_coords_num
    num, len(coords)
RuntimeError: number of nodes expected to be 8 but is actually 7
$ scripts/render-single-sound lib/instruments/dev/piano.spli -f 440 -l 3 --stress 0.75 --output /tmp/piano-s075.png
orig. len: 35 | by_num: 27 | is_num: 28
orig. len: 28 | by_num: 20 | is_num: 22
orig. len: 22 | by_num: 14 | is_num: 16
orig. len: 16 | by_num: 8 | is_num: 11
orig. len: 11 | by_num: 3 | is_num: 8
Will save graph image to file as requested ...
$ scripts/render-single-sound lib/instruments/dev/piano.spli -f 440 -l 3 --stress 0.75 --output /tmp/piano-s075.png
orig. len: 35 | by_num: 27 | is_num: 27
orig. len: 27 | by_num: 19 | is_num: 23
orig. len: 23 | by_num: 15 | is_num: 18
orig. len: 18 | by_num: 10 | is_num: 12
orig. len: 12 | by_num: 4 | is_num: 8
Will save graph image to file as requested ...
$ scripts/render-single-sound lib/instruments/dev/piano.spli -f 440 -l 3 --stress 0.75 --output /tmp/piano-s075.png
orig. len: 35 | by_num: 27 | is_num: 28
orig. len: 28 | by_num: 20 | is_num: 22
orig. len: 22 | by_num: 14 | is_num: 15
orig. len: 15 | by_num: 7 | is_num: 8
Will save graph image to file as requested ...
$ 
gotridofmyphone
User
Beiträge: 33
Registriert: Mittwoch 15. März 2017, 08:54

Oje, sind meine Fragen zu abgehoben, kompliziert? Wahrscheinlich scheitere ich immer daran, dass der bestformulierte verbale Kontext nichts nützt, wenn man den geposteten Code nicht kontextfrei an den Python-Interpreter verfüttern kann, um den Fehler nachzuvollziehen. :roll: Der Kontext ist hier auch überaus weitgezogen und damit quasi bedeutungslos. Ich muss mich für mein Ego entschuldigen, seinerdank ich ständig Supportanfrage mit Werbung für mein uninteressantes kleines Projekt vermische. :oops: Ich würde mich ja auch gerne antwortend am Forum beteiligen, bisher war es halt immer so gewesen, dass Anfragen, auf die ich eine Antwort gewusst hätte, schon hinreichend beantwortet waren.

Aber ich bin mittlerweile selbst auf des Rätsels Lösung gekommen, und will sie hier gerne kundtun.

In der Tat gibt es hier eine Zufallskomponente, nämlich die Reihenfolge, in der dict.items() die Schlüssel/Wert-Paare herausgibt, ist ja nicht festgelegt. Es ist sogar so, dass andernfalls eine Sicherheitslücke bestünde, wenn ich nicht gerade was verwechsle.

Außerdem spielt eine Rolle, dass nie die erste Koordinate im Dictionary gespeichert wird. Ist ja auch logisch, die kann ja gar keine Distanz zur vorhergehenden Koordinate haben, denn es gibt ja keine vorhergehende Koordinate.

Der demonstrierte Off-by-one-Fehler tritt immer dann auf, wenn zwischen der zweiten und der ersten Koordinate eine so große Distanz ist, dass sie wegen der by_num-Begrenzung gar nicht von der Löschung betroffen gewesen wäre. Faktisch ist sie gewissermaßen eben doch gelöscht weil nie vorhanden gewesen. Sie fehlt also immer in distances. Deshalb sind diese Koordinaten ggf. um ein Element zu kurz.

Ein

Code: Alles auswählen

distances[coords[0]] = None
schafft Abhilfe. Statt None könnte man jeden beliebigen Wert nehmen, er wird ja nicht ausgewertet (coords[1:]).
Antworten