Es ist bekannt, dass Komponisten Motive (dh charakteristische Anmerkung Fortschritte oder melodische Fragmente) in ihren Werken wiederverwenden. Zum Beispiel berühmte Hollywood -Komponisten wie John Williams (ÜbermenschAnwesend Star WarsAnwesend Harry Potter) und Hans Zimmer (BeginnAnwesend InterstellarAnwesend Der dunkle Ritter) recyceln Motive geschickt, um sofort erkennbare, charakteristische Soundtracks zu erstellen.

In diesem Artikel zeigen wir, wie Sie mit Datenwissenschaften etwas Ähnliches tun können. Insbesondere werden wir Musik komponieren, indem wir uns auf das graphentheoretische Konzept der eulerischen Wege stützen, um stochistisch erzeugte musikalische Motive in akustisch ansprechende Melodien zusammenzustellen. Nachdem wir einen Überblick über theoretische Konzepte und einen kanonischen Anwendungsfall gegeben haben, um unser Verständnis der Grundlagen zu begründen, werden wir eine Finish-to-Finish-Python-Implementierung des algorithmischen Musikkompositionsverfahrens durchlaufen.

Notiz: Alle Zahlen in den folgenden Abschnitten wurden vom Autor dieses Artikels erstellt.

Eine Grundierung auf eulerischen Wegen

Angenommen, wir haben eine Grafik, die aus Knoten und Kanten besteht. Der Grad eines Knotens in einem ungerichteten Diagramm bezieht sich auf die Anzahl der mit diesem Knoten verbundenen Kanten. Die in einem Grad und des Grades eines Knotens in einem angegebenen Diagramm beziehen sich auf die Anzahl eingehender und ausgehender Kanten für diesen Knoten. A Eulerianischer Weg ist definiert als einen Spaziergang entlang der Knoten und Kanten eines Graphen, der an einem Knoten beginnt und an einem Knoten endet, und besucht jede Kante genau einmal; Wenn wir am selben Knoten beginnen und enden, wird dies als a genannt Eulerianische Schaltung.

In einem ungerichteten Diagramm existiert ein eulerischer Pfad nur dann, wenn Null oder zwei Knoten einen ungeraden Grad aufweisen und alle Knoten mit ungleich Null -Grad Teil einer einzelnen verbundenen Komponente im Diagramm sind. In einer gerichteten Grafik existiert ein eulerischer Pfad nur dann, wenn höchstens ein Knoten (der Startknoten) eine weitere ausgehende Kante als eingehende Kante aufweist, höchstens ein Knoten (der Endknoten) einen weiteren eingehenden Rand als eingehender Rand, und alle anderen Knoten haben gleiche inkomierende und ausgehende Kanten, und alle Nodes mit Nicht-No-Uner-Tar-Tar-Tar-Tar-Tar-Tar-Tarnien. Die Einschränkungen im Zusammenhang mit der Teilnahme einer einzelnen verbundenen Komponente stellen sicher, dass alle Kanten im Diagramm erreichbar sind.

Die folgenden Abbildungen 1 und 2 zeigen grafische Darstellungen der Sieben Brücken von Königsberg und die Haus von Nikolausjeweils. Dies sind zwei berühmte Rätsel, bei denen es darum geht, einen eulerischen Weg zu finden.

Abbildung 1: Das Königsberg -Drawback

In Abbildung 1 sind zwei Inseln (Kneiphof und Lomse) miteinander verbunden und die beiden Festlandteile (Altstadt und Vorstadt) der Stadt Königsberg in Preußen mit insgesamt sieben Brücken. Die Frage ist, ob es eine Möglichkeit gibt, alle vier Teile der Stadt genau einmal jede Brücke zu besuchen. Mit anderen Worten, wir möchten wissen, ob ein eulerischer Weg für das in Abbildung 1 gezeigte ungerichtete Graph vorliegt. 1736 zeigte der berühmte Mathematiker Leonhard Euler – nach dem eulerische Wege und Schaltungen ihren Namen erhalten -, dass ein solcher Weg für dieses spezielle Drawback nicht existieren kann. Wir können sehen, warum die Verwendung der zuvor beschriebenen Definitionen: Alle vier Teile (Knoten) der Stadt Königsberg haben eine ungerade Anzahl von Brücken (Kanten), dh nicht, dass Null oder zwei Knoten einen ungeraden Grad haben.

Abbildung 2: Das Haus von Nikolaus Puzzle

In Abbildung 2 ist es das Ziel, das Haus von Nikolaus zu zeichnen, beginnend an einem der fünf Ecken (mit 1-5 gekennzeichnete Knoten) und die einzelnen Linien (Kanten) genau einmal verfolgt. Hier sehen wir, dass zwei Knoten einen Grad von vier, zwei Knoten einen Grad von drei haben und ein Knoten einen Grad von zwei hat, sodass ein eulerischer Weg existieren muss. Wie die folgende Animation zeigt, ist es anscheinend möglich, 44 verschiedene eulerische Wege für dieses Puzzle zu konstruieren:

Quelle: Wikipedia (CC0 1.0 Common)

Eulerianische Pfade können programmgesteuert mit Hierholzer -Algorithmus abgeleitet werden, wie im folgenden Video erläutert:

https://www.youtube.com/watch?v=8mpoo2za2l4

Hierholzer Algorithmus verwendet eine Suchtechnik, die genannt wird Backtrackingwelche Dieser Artikel detaillierter abdeckt.

Eulerische Wege für Fragmentanordnung

Bei einer Reihe von Knoten, die Informationsfragmente darstellen, können wir das Konzept der eulerischen Pfade verwenden, um die Fragmente auf sinnvolle Weise zusammenzusetzen.

Um zu sehen, wie dies funktionieren könnte, sollten wir mit einem Drawback beginnen, das nicht viel Area-Know-how erfordert: Wenn man eine Liste positiver zweistelliger Ganzzahlen gibt, ist es möglich, diese Ganzzahlen in einer Sequenz zu ordnen X1Anwesend X2,…, XN so dass die Zehner -Ziffer der Ganzzahl Xich entspricht der Ziffer der Ganzzahl der Einheiten Xi+1? Angenommen, wir haben die folgende Liste: (22, 23, 25, 34, 42, 55, 56, 57, 67, 75, 78, 85). Durch Inspektion stellen wir fest, dass beispielsweise wenn Xich = 22 (mit Einheiten Ziffer 2), dann Xi+1 kann 23 oder 25 (Zehnendifferung 2) sein, während wenn Xich = 78, dann Xi+1 Kann nur noch 85 sein. Wenn wir nun die Liste der Ganzzahlen in einen gerichteten Diagramm übersetzen, wobei jede Ziffer ein Knoten ist und jede zweistellige Ganzzahl als gerichtete Kante von der Zehnarbeit in die Ziffer der Einheiten modelliert wird, gibt es uns eine mögliche Lösung für unser Drawback, wie er einen eulerischen Pfad findet. Eine Python -Implementierung dieses Ansatzes ist unten gezeigt:

from collections import defaultdict

def find_eulerian_path(numbers):
    # Initialize graph
    graph = defaultdict(checklist)
    indeg = defaultdict(int)
    outdeg = defaultdict(int)
    
    for num in numbers:
        a, b = divmod(num, 10)  # a = tens digit, b = items digit
        graph(a).append(b)
        outdeg(a) += 1
        indeg(b) += 1
    
    # Discover begin node
    begin = None
    start_nodes = end_nodes = 0
    for v in set(indeg) | set(outdeg):
        outd = outdeg(v)
        ind = indeg(v)
        if outd - ind == 1:
            start_nodes += 1
            begin = v
        elif ind - outd == 1:
            end_nodes += 1
        elif ind == outd:
            proceed
        else:
            return None  # No Eulerian path potential
    
    if not begin:
        begin = numbers(0) // 10  # Arbitrary begin if Eulerian circuit
    
    if not ( (start_nodes == 1 and end_nodes == 1) or (start_nodes == 0 and end_nodes == 0) ):
        return None  # No Eulerian path
    
    # Use Hierholzer's algorithm
    path = ()
    stack = (begin)
    local_graph = {u: checklist(vs) for u, vs in graph.gadgets()}
    
    whereas stack:
        u = stack(-1)
        if local_graph.get(u):
            v = local_graph(u).pop()
            stack.append(v)
        else:
            path.append(stack.pop())
    
    path.reverse()  # We get the trail in reverse order as a consequence of backtracking
    
    # Convert the trail to an answer sequence with the unique numbers
    end result = ()
    for i in vary(len(path) - 1):
        end result.append(path(i) * 10 + path(i+1))
    
    return end result if len(end result) == len(numbers) else None


given_integer_list = (22, 23, 25, 34, 42, 55, 56, 57, 67, 75, 78, 85)
solution_sequence = find_eulerian_path(given_integer_list)
print(solution_sequence)

Ergebnis:

(23, 34, 42, 22, 25, 57, 78, 85, 56, 67, 75, 55)

Die DNA -Fragmentanordnung ist ein kanonischer Anwendungsfall des obigen Verfahrens im Bereich der Bioinformatik. Im Wesentlichen erhalten Wissenschaftler während der DNA -Sequenzierung mehrere kurze DNA -Fragmente, die zusammengebrochen werden müssen, um lebensfähige Kandidaten für die vollständige DNA -Sequenz abzuleiten, und dies kann möglicherweise relativ effizient unter Verwendung des Konzepts eines eulerischen Pfad Das Papier für weitere Particulars). Jedes DNA -Fragment, bekannt als a okay-Mer, besteht aus okay Buchstaben aus dem Satz { AAnwesend CAnwesend GAnwesend T } die Nukleotidbasen, die ein DNA -Molekül ausmachen können; z.B, AKT Und CTG wäre 3-mers. Ein sogenannter De Bruijn Graph kann jetzt mit den darstellenden Knoten konstruiert werden (okay-1) -Mer Präfixe (z. B., z. B. AC für AKT Und Ct für CTG) und gerichtete Kanten, die eine Überlappung zwischen den Quell- und Zielknoten bezeichnen (z. B. würde es eine Kante geben AC Zu Ct Aufgrund des überlappenden Briefes C). Das Abfertigung eines praktikablen Kandidaten für die vollständige DNA -Sequenz bedeutet, einen eulerischen Pfad im De Bruijn -Diagramm zu finden. Das folgende Video zeigt ein bearbeitetes Beispiel:

https://www.youtube.com/watch?v=9w3mr3_kfh8

Ein Algorithmus zur Erzeugung von Melodien

Wenn wir eine Reihe von Fragmenten haben, die Musikmotive darstellen, können wir den im vorherigen Abschnitt beschriebenen Ansatz verwenden, um die Motive in einer vernünftigen Reihenfolge zu ordnen, indem sie sie in ein de bruijn -Diagramm übersetzen und einen eulerischen Pfad identifizieren. Im Folgenden werden wir eine Finish-to-Finish-Implementierung in Python durchlaufen. Der Code wurde auf MacOS Sequoia 15.6.1 getestet.

Teil 1: Installations- und Projekt -Setup

Zuerst müssen wir installieren Ffmpeg Und Fluidsynthzwei Instruments, die für die Verarbeitung von Audiodaten nützlich sind. Hier erfahren Sie, wie Sie beide mit Homebrew auf einem Mac installieren:

brew set up ffmpeg
brew set up fluid-synth

Wir werden auch verwenden uv Für Python -Projektmanagement. Installationsanweisungen können gefunden werden Hier.

Jetzt werden wir einen Projektordner namens erstellen eulerian-melody-generatorA foremost.py Datei, um die Melodie-Technology-Logik und eine virtuelle Umgebung basierend auf Python 3.12 zu halten:

mkdir eulerian-melody-generator
cd eulerian-melody-generator
uv init --bare
contact foremost.py
uv venv --python 3.12
supply .venv/bin/activate

Als nächstes müssen wir eine erstellen necessities.txt Datei mit den folgenden Abhängigkeiten und platzieren Sie die Datei in die eulerian-melody-generator Verzeichnis:

matplotlib==3.10.5
midi2audio==0.1.1
midiutil==1.2.1
networkx==3.5

Die Pakete midi2audio Und midiutil sind während der Audioverarbeitung benötigt matplotlib Und networkx wird verwendet, um die De Bruijn -Grafik zu visualisieren. Wir können diese Pakete jetzt in unserer virtuellen Umgebung installieren:

uv add -r necessities.txt

Ausführen uv pip checklist Um zu überprüfen, ob die Pakete installiert wurden.

Schließlich benötigen wir eine SoundFont -Datei, um die Audioausgabe als Reaktion auf MIDI -Daten zu rendern. Für die Zwecke dieses Artikels werden wir die Datei verwenden TimGM6mb.sf2was auf gefunden werden kann Das MuseSescore -Website oder direkt von heruntergeladen von Hier. Wir werden die Datei neben foremost.py im eulerian-melody-generator Verzeichnis.

Teil 2: Melodieerzeugungslogik

Jetzt werden wir die Melodie -Generierungslogik in umsetzen foremost.py. Beginnen wir mit dem Hinzufügen der relevanten Importanweisungen und der Definition einiger nützlicher Suchvariablen:

import os
import random
import subprocess
from collections import defaultdict
from midiutil import MIDIFile
from midi2audio import FluidSynth
import networkx as nx
import matplotlib.pyplot as plt

# Resolve the SoundFont path (assume that is identical as working listing)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SOUNDFONT_PATH = os.path.abspath(os.path.be part of(BASE_DIR, ".", "TimGM6mb.sf2"))

# 12‑observe chromatic reference
NOTE_TO_OFFSET = {
    "C": 0, "C#":1, "D":2, "D#":3, "E":4,
    "F":5, "F#":6, "G":7, "G#":8, "A":9,
    "A#":10, "B":11
}

# Standard pop‑pleasant interval patterns (in semitones from root)
MAJOR          = (0, 2, 4, 5, 7, 9, 11)
NAT_MINOR      = (0, 2, 3, 5, 7, 8, 10)
MAJOR_PENTA    = (0, 2, 4, 7, 9)
MINOR_PENTA    = (0, 3, 5, 7, 10)
MIXOLYDIAN     = (0, 2, 4, 5, 7, 9, 10)
DORIAN         = (0, 2, 3, 5, 7, 9, 10)

Wir werden auch ein paar Helferfunktionen definieren, um ein Wörterbuch mit Skalen in allen zwölf Schlüssel zu erstellen:

def generate_scales_all_keys(scale_name, intervals):
    """
    Construct a given scale in all 12 keys.
    """
    scales = {}
    chromatic = (*NOTE_TO_OFFSET)  # Get dict keys
    for i, root in enumerate(chromatic):
        notes = (chromatic((i + step) % 12) for step in intervals)
        key_name = f"{root}-{scale_name}"
        scales(key_name) = notes
    return scales


def generate_scale_dict():
    """
    Construct a grasp dictionary of all keys.
    """
    scale_dict = {}
    scale_dict.replace(generate_scales_all_keys("Main", MAJOR))
    scale_dict.replace(generate_scales_all_keys("Pure-Minor", NAT_MINOR))
    scale_dict.replace(generate_scales_all_keys("Main-Pentatonic", MAJOR_PENTA))
    scale_dict.replace(generate_scales_all_keys("Minor-Pentatonic", MINOR_PENTA))
    scale_dict.replace(generate_scales_all_keys("Mixolydian", MIXOLYDIAN))
    scale_dict.replace(generate_scales_all_keys("Dorian", DORIAN))
    return scale_dict

Als nächstes werden wir Funktionen zum Generieren implementieren okay-Mer und ihre entsprechende De Bruijn -Grafik. Beachten Sie, dass die okay-Mer -Technology ist eingeschränkt, um einen eulerischen Pfad im De Bruijn -Diagramm zu gewährleisten. Wir verwenden auch einen zufälligen Samen während okay-Mer Technology, um die Reproduzierbarkeit sicherzustellen:

def generate_eulerian_kmers(okay, depend, scale_notes, seed=42):
    """
    Generate k-mers over the given scale that kind a related De Bruijn graph with a assured Eulerian path.
    """
    random.seed(seed)
    if depend < 1:
        return ()

    # decide a random beginning (k-1)-tuple
    start_node = tuple(random.selection(scale_notes) for _ in vary(k-1))
    nodes = {start_node}
    edges = ()
    out_deg = defaultdict(int)
    in_deg = defaultdict(int)

    present = start_node
    for _ in vary(depend):
        # decide a subsequent observe from the size
        next_note = random.selection(scale_notes)
        next_node = tuple(checklist(present(1:)) + (next_note))

        # add k-mer edge
        edges.append(present + (next_note,))
        nodes.add(next_node)
        out_deg(present) += 1
        in_deg(next_node) += 1

        present = next_node  # stroll continues

    # Examine diploma imbalances and retry to satisfy Eulerian path diploma situation
    start_candidates = (n for n in nodes if out_deg(n) - in_deg(n) > 0)
    end_candidates   = (n for n in nodes if in_deg(n) - out_deg(n) > 0)
    if len(start_candidates) > 1 or len(end_candidates) > 1:
        # For simplicity: regenerate till situation met
        return generate_eulerian_kmers(okay, depend, scale_notes, seed+1)

    return edges


def build_debruijn_graph(kmers):
    """
    Construct a De Bruijn-style graph.
    """
    adj = defaultdict(checklist)
    in_deg = defaultdict(int)
    out_deg = defaultdict(int)
    for kmer in kmers:
        prefix = tuple(kmer(:-1))
        suffix = tuple(kmer(1:))
        adj(prefix).append(suffix)
        out_deg(prefix) += 1
        in_deg(suffix)   += 1
    return adj, in_deg, out_deg

Wir werden eine Funktion zum Visualisieren und Speichern des De Bruijn -Graphen für die spätere Verwendung implementieren:

def generate_and_save_graph(graph_dict, output_file="debruijn_graph.png", seed=100, okay=1):
    """
    Visualize graph and reserve it as a PNG.
    """
    # Create a directed graph
    G = nx.DiGraph()

    # Add edges from adjacency dict
    for prefix, suffixes in graph_dict.gadgets():
        for suffix in suffixes:
            G.add_edge(prefix, suffix)

    # Structure for nodes (bigger okay means extra spacing between nodes)
    pos = nx.spring_layout(G, seed=seed, okay=okay)

    # Draw nodes and edges
    plt.determine(figsize=(10, 8))
    nx.draw_networkx_nodes(G, pos, node_size=1600, node_color="skyblue", edgecolors="black")
    nx.draw_networkx_edges(
        G, pos, 
        arrowstyle="-|>", 
        arrowsize=20, 
        edge_color="black",
        connectionstyle="arc3,rad=0.1",
        min_source_margin=20,
        min_target_margin=20
    )
    nx.draw_networkx_labels(G, pos, labels={node: " ".be part of(node) for node in G.nodes()}, font_size=10)

    # Edge labels
    edge_labels = { (u,v): "" for u,v in G.edges() }
    nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_color="crimson", font_size=8)

    plt.axis("off")
    plt.tight_layout()
    plt.savefig(output_file, format="PNG", dpi=300)
    plt.shut()
    print(f"Graph saved to {output_file}")

Als nächstes werden wir Funktionen implementieren, um einen eulerischen Pfad im De Bruijn -Diagramm abzuleiten und den Pfad in eine Abfolge von Notizen zu fassen. In einer Abweichung vom zuvor diskutierten Ansatz der DNA -Fragment -Assemblierung werden wir die überlappenden Teile des okay-Mer während des Abflachungsprozesses, um eine ästhetisch ansprechende Melodie zu ermöglichen:

def find_eulerian_path(adj, in_deg, out_deg):
    """
    Discover an Eulerian path within the De Bruijn graph.
    """
    begin = None
    for node in set(checklist(adj) + checklist(in_deg)):
        if out_deg(node) - in_deg(node) == 1:
            begin = node
            break
    if begin is None:
        begin = subsequent(n for n in adj if adj(n))
    stack = (begin)
    path  = ()
    local_adj = {u: vs(:) for u, vs in adj.gadgets()}
    whereas stack:
        v = stack(-1)
        if local_adj.get(v):
            u = local_adj(v).pop()
            stack.append(u)
        else:
            path.append(stack.pop())
    return path(::-1)


def flatten_path(path_nodes):
    """
    Flatten an inventory of observe tuples right into a single checklist.
    """
    flattened = ()
    for kmer in path_nodes:
        flattened.prolong(kmer)
    return flattened

Jetzt schreiben wir einige Funktionen, um die Melodie als MP3 -Datei zu komponieren und zu exportieren. Die Schlüsselfunktion ist compose_and_exportwas zur Wiedergabe der Notizen, aus denen der eulerische Pfad (z. B. unterschiedliche Notizlängen und Oktaven) besteht, eine Variation verleiht, um sicherzustellen, dass die resultierende Melodie nicht zu monoton klingt. Wir unterdrücken/leiten den ausführlichen Ausgang von FFMPEG und Fluidsynth auch aus:

def note_with_octave_to_midi(observe, octave):
    """
    Helper operate for changing a musical pitch like "C#" 
    in some octave into its numeric MIDI observe quantity.
    """
    return 12 * (octave + 1) + NOTE_TO_OFFSET(observe)


@contextlib.contextmanager
def suppress_fd_output():
    """
    Redirects stdout and stderr on the OS file descriptor stage.
    This catches output from C libraries like FluidSynth.
    """
    with open(os.devnull, 'w') as devnull:
        # Duplicate unique file descriptors
        old_stdout_fd = os.dup(1)
        old_stderr_fd = os.dup(2)
        attempt:
            # Redirect to /dev/null
            os.dup2(devnull.fileno(), 1)
            os.dup2(devnull.fileno(), 2)
            yield
        lastly:
            # Restore unique file descriptors
            os.dup2(old_stdout_fd, 1)
            os.dup2(old_stderr_fd, 2)
            os.shut(old_stdout_fd)
            os.shut(old_stderr_fd)


def compose_and_export(final_notes,
                       bpm=120,
                       midi_file="output.mid",
                       wav_file="temp.wav",
                       mp3_file="output.mp3",
                       soundfont_path=SOUNDFONT_PATH):

    # Classical-style rhythmic motifs
    rhythmic_patterns = (
        (1.0, 1.0, 2.0),           # quarter, quarter, half
        (0.5, 0.5, 1.0, 2.0),      # eighth, eighth, quarter, half
        (1.5, 0.5, 1.0, 1.0),      # dotted quarter, eighth, quarter, quarter
        (0.5, 0.5, 0.5, 0.5, 2.0)  # run of eighths, then half
    )

    # Construct an octave contour: ascend then descend
    base_octave = 4
    peak_octave = 5
    contour = ()
    half_len = len(final_notes) // 2
    for i in vary(len(final_notes)):
        if i < half_len:
            # Ascend step by step
            contour.append(base_octave if i < half_len // 2 else peak_octave)
        else:
            # Descend
            contour.append(peak_octave if i < (half_len + half_len // 2) else base_octave)

    # Assign occasions following rhythmic patterns & contour
    occasions = ()
    note_index = 0
    whereas note_index < len(final_notes):
        sample = random.selection(rhythmic_patterns)
        for dur in sample:
            if note_index >= len(final_notes):
                break
            octave = contour(note_index)
            occasions.append((final_notes(note_index), octave, dur))
            note_index += 1

    # Write MIDI
    mf = MIDIFile(1)
    observe = 0
    mf.addTempo(observe, 0, bpm)
    time = 0
    for observe, octv, dur in occasions:
        pitch = note_with_octave_to_midi(observe, octv)
        mf.addNote(observe, channel=0, pitch=pitch,
                   time=time, period=dur, quantity=100)
        time += dur
    with open(midi_file, "wb") as out_f:
        mf.writeFile(out_f)

    # Render to WAV
    with suppress_fd_output():
        fs = FluidSynth(sound_font=soundfont_path)
        fs.midi_to_audio(midi_file, wav_file)

    # Convert to MP3
    subprocess.run(
        (
            "ffmpeg", "-y", "-hide_banner", "-loglevel", "quiet", "-i", 
            wav_file, mp3_file
        ),
        verify=True
    )

    print(f"Generated {mp3_file}")

Schließlich werden wir demonstrieren, wie der Melodiegenerator in der Verwendung verwendet werden kann if identify == "foremost" Abschnitt der foremost.py. Mehrere Parameter – die Skala, Tempo, okay-Mer Länge, Anzahl von okay-Mer, Anzahl der Wiederholungen (oder Schleifen) des eulerischen Pfades und des zufälligen Samens können variiert werden, um verschiedene Melodien zu erzeugen:

if __name__ == "__main__":
    
    SCALE = "C-Main-Pentatonic" # Set "key-scale" e.g. "C-Mixolydian"
    BPM = 200  # Beats per minute (musical tempo)
    KMER_LENGTH = 4  # Size of every k-mer
    NUM_KMERS = 8  # What number of k-mers to generate
    NUM_REPEATS = 8  # How typically ultimate observe sequence ought to repeat
    RANDOM_SEED = 2  # Seed worth to breed outcomes

    scale_dict = generate_scale_dict()
    chosen_scale = scale_dict(SCALE)
    print("Chosen scale:", chosen_scale)

    kmers = generate_eulerian_kmers(okay=KMER_LENGTH, depend=NUM_KMERS, scale_notes=chosen_scale, seed=RANDOM_SEED)
    adj, in_deg, out_deg = build_debruijn_graph(kmers)
    generate_and_save_graph(graph_dict=adj, output_file="debruijn_graph.png", seed=20, okay=2)
    path_nodes = find_eulerian_path(adj, in_deg, out_deg)
    print("Eulerian path:", path_nodes)

    final_notes = flatten_path(path_nodes) * NUM_REPEATS  # A number of loops of the Eulerian path
    mp3_file = f"{SCALE}_v{RANDOM_SEED}.mp3"  # Assemble a searchable filename
    compose_and_export(final_notes=final_notes, bpm=BPM, mp3_file=mp3_file)

Ausführung uv run foremost.py erzeugt die folgende Ausgabe:

Chosen scale: ('C', 'D', 'E', 'G', 'A')
Graph saved to debruijn_graph.png
Eulerian path: (('C', 'C', 'C'), ('C', 'C', 'E'), ('C', 'E', 'D'), ('E', 'D', 'E'), ('D', 'E', 'E'), ('E', 'E', 'A'), ('E', 'A', 'D'), ('A', 'D', 'A'), ('D', 'A', 'C'))
Generated C-Main-Pentatonic_v2.mp3

Als einfachere Different zur Befolgen der obigen Schritte hat der Autor dieses Artikels eine Python -Bibliothek genannt erstellt emg Um das gleiche Ergebnis zu erzielen, wurden bereits angenommen, dass FFMPEG und Fluidsynth bereits installiert wurden (siehe Particulars Hier). Installieren Sie die Bibliothek mit pip set up emg oder uv add emg und verwenden Sie es wie unten gezeigt:

from emg.generator import EulerianMelodyGenerator

# Path to your SoundFont file
sf2_path = "TimGM6mb.sf2"

# Create a generator occasion
generator = EulerianMelodyGenerator(
    soundfont_path=sf2_path,
    scale="C-Main-Pentatonic",
    bpm=200,
    kmer_length=4,
    num_kmers=8,
    num_repeats=8,
    random_seed=2
)

# Run the complete pipeline
generator.run_generation_pipeline(
    graph_png_path="debruijn_graph.png",
    mp3_output_path="C-Main-Pentatonic_v2.mp3"
)

(Non-compulsory) Teil 3: Konvertieren von MP3 in MP4

Wir können FFMPEG verwenden, um die MP3 -Datei in eine MP4 -Datei zu konvertieren (die PNG -Exportierung des De Bruijn -Diagramms als Cowl -Kunst), das auf Plattformen wie YouTube hochgeladen werden kann. Die Choice -loop 1 Wiederholt das PNG -Bild für die gesamte Audiolänge, -tune stillimage optimiert die Codierung für statische Bilder, -shortest stellt sicher, dass das Video ungefähr anhält, wenn das Audio endet, und -pix_fmt yuv420p stellt sicher, dass das Ausgangspixelformat mit den meisten Spielern kompatibel ist:

ffmpeg -loop 1 -i debruijn_graph.png -i C-Main-Pentatonic_v2.mp3 
  -c:v libx264 -tune stillimage -c:a aac -b:a 192k 
  -pix_fmt yuv420p -shortest C-Main-Pentatonic_v2.mp4

Hier ist das Endergebnis, das auf YouTube hochgeladen wurde:

https://www.youtube.com/watch?v=_zdyzpcsusw

Der Wrap

In diesem Artikel haben wir gesehen, wie ein abstraktes Subjekt wie Graphentheorie eine praktische Anwendung im scheinbar nicht verwandten Bereich der algorithmischen Musikkomposition haben kann. Interessanterweise spiegeln unsere Verwendung von stochistisch erzeugten musikalischen Fragmenten zur Konstruktion des eulerischen Pfades und die zufälligen Variationen von Be aware Länge und Oktav Aleatorik Musikkomposition (Alea Das lateinische Wort für „Würfel“), in dem einige Aspekte der Komposition und ihrer Leistung dem Zufall überlassen werden.

Über die Musik hinaus haben die in den oben genannten Abschnitten diskutierten Konzepten praktische Datenwissenschaftsanwendungen in einer Reihe anderer Bereiche wie Bioinformatik (z. B. DNA -Fragment -Assemblierung), Archäologie (z. Da sich die Technologie weiterentwickelt und die Welt zunehmend digitalisiert wird, werden eulerische Wege und verwandte graph -theoretische Konzepte wahrscheinlich viel mehr progressive Anwendungen in verschiedenen Bereichen finden.

Von admin

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert