#4589 Knowledge Graph: Music Chord Progressions

#4589 Knowledge Graph: Music Chord Progressions



#4589 Describe a knowledge graph of possible chord progressions and what it could be used for

Imagine mapping out the entire universe of music, where every chord is a destination and every transition is a highway. That is essentially what a knowledge graph of chord progressions is.

By representing musical harmony as a network of interconnected concepts, we can unlock a whole new way of analyzing, teaching, and creating music.

Here is a breakdown of what this knowledge graph looks like and exactly how it can be used.

The Anatomy of a Chord Progression Knowledge Graph

In graph theory, a knowledge graph consists of nodes (the entities) and edges (the relationships between them). For music theory, it breaks down like this:

1. The Nodes (The "What")

  • Chords: The core building blocks (e.g., C_maj7, D_min7, G^7).

  • Scale Degrees / Roman Numerals: Abstracted representations of chords independent of key (e.g., I, ii, V^7). This allows the graph to recognize that a C --> F --> G progression in C major is functionally identical to G --> C --> D in G major (I --> IV --> V).

  • Keys and Modes: The tonal context (e.g., A minor, E Dorian, Bb major).

  • Genres/Eras: Tags associating specific progressions with styles (e.g., "Neo-Soul," "Baroque," "1950s Doo-Wop").

2. The Edges (The "How")

  • Voice Leading: How smoothly notes move from one chord to the next (e.g., "retains common tones," "stepwise motion").

  • Functional Relationships: The tension and release dynamics (e.g., "resolves to," "prepares for," "substitutes for").

  • Probability Weights: Based on a dataset of songs, how likely is chord B to follow chord A? (e.g., In pop music, a V chord has a high-probability edge leading to a I chord).

Practical Applications: What Is It Used For?

A knowledge graph turns abstract music theory into machine-readable data. This has massive implications across several domains:

1. AI-Assisted Composition & Smart DAWs

Instead of a digital audio workstation (DAW) just offering a rigid menu of chords, a knowledge graph acts as an intelligent co-writer.

  • Dynamic Suggestions: If a songwriter types in a I --> vi progression, the AI can look at the graph and suggest: "Artists in your genre usually go to a IV next, but if you want a emotional plot-twist, try a borrowed bVI chord."

  • Steering the Mood: A user could slider-adjust a track from "predictable/catchy" to "complex/jazz-influenced," and the graph would recalculate paths using low-probability or highly substituted edges (like Tritone substitutions).

2. Music Recommendation and "Sonic Fingerprinting"

Streaming services currently recommend music based heavily on user behavior (e.g., "people who liked X also liked Y") or broad acoustic features (tempo, energy).

  • A chord knowledge graph allows for harmonic recommendation. If Spotify knows you love songs that utilize the "epic film score" progression (i --> bVI --> bIII --> bVII), it can find other tracks across entirely different genres that share that exact harmonic DNA.

3. Automated Musicology and Trend Analysis

Music theorists can ingest massive catalogs of music—from the 1700s to today's Billboard charts—into the graph to track how human taste has evolved.

  • It can map how the "Axis of Awesome" progression (I --> V --> vi --> IV) took over pop music.

  • It can identify the exact historical moment a certain chord transition fell out of favor or debuted.

4. Adaptive Video Game Soundtracks

In video games, the music needs to change dynamically based on the player's actions without sounding jarring.

  • If a player walks from a peaceful village into a sudden ambush, the game's audio engine can use the knowledge graph to find the shortest, most musically coherent "harmonic bridge" to transition from a bright, major key to a tense, diminished or minor key seamlessly.

5. Personalized Music Education

For students learning music theory or jazz improvisation, the graph can act as a GPS.

  • If a student struggles to solo over a ii --> V --> I cadence, the graph can identify simpler sub-paths to practice first, or suggest related songs that use the same progression to help train their ear.

Would you want to explore this concept from a data perspective (like how to structure it in a graph database like Neo4j) or from a creative perspective (how a musician might use it to break out of a writer's block)?


How to structure it in a graph database like Neo4j

Structuring a musical knowledge graph in Neo4j requires moving beyond simple text strings and defining clear Labels (nodes) and Relationship Types (edges), along with properties that allow you to query the graph effectively.

Here is a blueprint for structuring a chord progression knowledge graph in Neo4j, complete with a data model and example Cypher queries.

1. The Graph Schema (Nodes and Relationships)

To make the graph powerful, we want to separate the abstract theory (which applies to all keys) from the concrete execution (specific chords in a specific key).

Node Labels

  • (:Chord): A concrete chord instance (e.g., name: "Cmaj7", root: "C", quality: "maj7").

  • (:FunctionalChord): The abstract Roman numeral representation (e.g., numeral: "IMaj7", degree: 1).

  • (:Key): The tonal center (e.g., name: "C Major", tonic: "C", mode: "Major").

  • (:Song): Metadata about real-world usage (e.g., title: "Let It Be", genre: "Pop").

Relationship Types

  • [:TRANSITIONS_TO]: Connects one chord to another. This is where the "weight" or probability lives.

  • [:RESOLVES_TO]: A specialized, high-tension functional transition (e.g., V^7 --> I).

  • [:IN_KEY]: Connects a concrete chord to its key context.

  • [:REPRESENTS]: Connects a concrete Chord to its abstract FunctionalChord under a specific key.

  • [:USES_PROGRESSION]: Connects a Song to the sequence of chords it uses.

2. Visualizing the Schema

(:Song) --[:USES_PROGRESSION {sequence: 1}]--> (:Chord) --[:TRANSITIONS_TO {probability: 0.74}]--> (:Chord)
                                                       |
                                            [:REPRESENTS]
                                                       v
                                          (:FunctionalChord)
                                                       ^
                                            [:HAS_FUNCTION]
                                                       |
                                                  (:Key)

3. Cypher Examples: Building the Graph

Here is how you would insert a classic ii --> V --> I progression in the key of C Major into Neo4j.

// 1. Create the Key and Abstract Functions
CREATE (cMajor:Key {name: "C Major", tonic: "C", mode: "Major"})
CREATE (f_ii:FunctionalChord {numeral: "ii", degree: 2, quality: "minor"})
CREATE (f_V:FunctionalChord {numeral: "V", degree: 5, quality: "major"})
CREATE (f_I:FunctionalChord {numeral: "I", degree: 1, quality: "major"})

// 2. Create the Concrete Chords
CREATE (dMin:Chord {name: "Dmin", root: "D", quality: "min"})
CREATE (gMaj:Chord {name: "Gmaj", root: "G", quality: "maj"})
CREATE (cMaj:Chord {name: "Cmaj", root: "C", quality: "maj"})

// 3. Link Chords to their Abstract Functions within C Major
CREATE (dMin)-[:REPRESENTS {in_key: "C Major"}]->(f_ii)
CREATE (gMaj)-[:REPRESENTS {in_key: "C Major"}]->(f_V)
CREATE (cMaj)-[:REPRESENTS {in_key: "C Major"}]->(f_I)

// 4. Create the Transitions with transition probability weights
CREATE (dMin)-[:TRANSITIONS_TO {weight: 0.85, genre: "Jazz"}]->(gMaj)
CREATE (gMaj)-[:TRANSITIONS_TO {weight: 0.92, genre: "Jazz"}]->(cMaj)
CREATE (gMaj)-[:RESOLVES_TO]->(cMaj)

4. Power Queries: What You Can Ask Neo4j

Once structured, you can run graph queries that would be an absolute nightmare to write in a traditional SQL database.

Query 1: Suggesting the Next Chord (The AI Co-writer)

"I am playing a Dmin in the key of C Major. What are the top 3 most common chords to play next in Jazz?"

MATCH (current:Chord {name: "Dmin"})-[r:TRANSITIONS_TO]->(next:Chord)
WHERE r.genre = "Jazz"
RETURN next.name AS SuggestedChord, r.weight AS Likelihood
ORDER BY r.weight DESC
LIMIT 3

Query 2: Finding a Voice-Leading "Bridge"

"I want to smoothly transition from Cmaj to Ebm, which are completely unrelated keys. Find me a 2-step chord path to bridge them."

MATCH path = (start:Chord {name: "Cmaj"})-[:TRANSITIONS_TO*2]->(end:Chord {name: "Ebm"})
RETURN path
LIMIT 5

(Neo4j will instantly find common pivot chords, like Ab or Fmin, that naturally bridge the gap.)

Query 3: Transposing a Borrowed Chord Idea

"Show me songs that borrow a bVII chord in a Major key, and tell me what concrete chord I should play if I'm writing in the key of G Major."

// Find the abstract concept across all songs
MATCH (f:FunctionalChord {numeral: "bVII"})<-[:REPRESENTS]-(borrowed:Chord)<-[:USES_PROGRESSION]-(s:Song)
// Find what that same abstract concept translates to in G Major
MATCH (gMajor:Key {name: "G Major"})<-[:HAS_FUNCTION]-(f)<-[:REPRESENTS]-(targetChord:Chord)
RETURN s.title AS SongExample, targetChord.name AS PlayThisChordInGMajor

Optimization Tip: Leveraging Graph Data Science (GDS)

If you load a massive catalog of songs (say, the entire Ultimate Guitar chord library) into Neo4j, you can run PageRank on the (:Chord) nodes.

  • Running PageRank will calculate the "harmonic gravity" of the network. Unsurprisingly, the I and V chords will have the highest PageRank scores, but running it filtered by genre will instantly expose the structural backbones of specific styles (like the prominence of the iv chord in bedroom pop).

Are you looking to populate this database programmatically using something like a Python script parsing MIDI/MusicXML files, or are you designing the backend API for an app?


Populate this database programmatically using something like a Python script

To populate this database programmatically, we can use Python with the official neo4j driver.

Instead of manually typing every chord, we can use an open-source Python musicology library like music21 (developed by MIT) to analyze songs, extract the keys, concrete chords, and abstract Roman numerals, and then stream them directly into Neo4j.

Here is a complete guide and a production-ready Python script to achieve this.

1. Prerequisites

First, install the Neo4j driver and music21 via pip:

pip install neo4j music21

(Note: music21 can parse MIDI, MusicXML, or ABC notation files, making it incredibly flexible for ingesting song libraries).

2. The Python Pipeline Script

This script initializes your Neo4j constraints (to prevent duplicate chords) and uses a function to parse a musical piece and push its harmonic data into the graph.

import os
from neo4j import GraphDatabase
from music21 import converter, roman

# --- NEO4J CONNECTION SETUP ---
URI = "bolt://localhost:7687"
AUTH = ("neo4j", "your_password")  # Change to your credentials

class ChordGraphPopulator:
    def __init__(self, uri, auth):
        self.driver = GraphDatabase.driver(uri, auth=auth)
        self._bootstrap_db()

    def close(self):
        self.driver.close()

    def _bootstrap_db(self):
        """Creates uniqueness constraints to optimize merge operations."""
        with self.driver.session() as session:
            session.run("CREATE CONSTRAINT IF NOT EXISTS FOR (c:Chord) REQUIRE c.name IS UNIQUE;")
            session.run("CREATE CONSTRAINT IF NOT EXISTS FOR (f:FunctionalChord) REQUIRE f.id IS UNIQUE;")
            session.run("CREATE CONSTRAINT IF NOT EXISTS FOR (k:Key) REQUIRE k.name IS UNIQUE;")
            session.run("CREATE CONSTRAINT IF NOT EXISTS FOR (s:Song) REQUIRE s.id IS UNIQUE;")

    def ingest_song(self, file_path, song_title, genre):
        """Parses a music file and streams the chord progression to Neo4j."""
        print(f"Parsing {song_title}...")
        
        # 1. Use music21 to parse the file and analyze chords/key
        score = converter.parse(file_path)
        chords_and_rests = score.chordify()
        detected_key = score.analyze('key') # e.g., 'C major'
        
        # Extract a clean list of chord objects, ignoring silence/rests
        chord_sequence = [c for c in chords_and_rests.recurse().getElementsByClass('Chord')]
        
        if not chord_sequence:
            print(f"No chords found in {song_title}. Skipping.")
            return

        with self.driver.session() as session:
            # 2. Create the Song and Key nodes
            session.run("""
                MERGE (s:Song {id: $song_id})
                SET s.title = $title, s.genre = $genre
                MERGE (k:Key {name: $key_name})
                SET k.tonic = $tonic, k.mode = $mode
            """, 
            song_id=f"{song_title.lower().replace(' ', '_')}", 
            title=song_title, genre=genre,
            key_name=detected_key.name, tonic=detected_key.tonic.name, mode=detected_key.mode)

            # 3. Iterate through chords to build nodes and track sequential transitions
            prev_chord_node_id = None
            
            for index, c in enumerate(chord_sequence):
                # Extract concrete chord data
                chord_name = c.pitchedCommonName # e.g., "C-major triad" or "C4-E4-G4"
                chord_root = c.root().name
                chord_quality = c.quality
                
                # Get abstract Roman Numeral relative to the analyzed key
                rn = roman.romanNumeralFromChord(c, detected_key)
                rn_numeral = rn.romanNumeral # e.g., "IV", "v7"
                rn_id = f"{detected_key.name}_{rn_numeral}" # Unique ID for function context

                # Merge Chord, FunctionalChord, and map their relationships
                result = session.run("""
                    MERGE (c:Chord {name: $chord_name})
                    SET c.root = $chord_root, c.quality = $chord_quality
                    
                    MERGE (f:FunctionalChord {id: $rn_id})
                    SET f.numeral = $rn_numeral, f.degree = $rn_degree
                    
                    MERGE (k:Key {name: $key_name})
                    MERGE (c)-[:REPRESENTS {in_key: $key_name}]->(f)
                    MERGE (k)-[:HAS_FUNCTION]->(f)
                    
                    RETURN id(c) AS chord_node_id
                """, 
                chord_name=chord_name, chord_root=chord_root, chord_quality=chord_quality,
                rn_id=rn_id, rn_numeral=rn_numeral, rn_degree=rn.scaleDegree,
                key_name=detected_key.name)
                
                chord_node_id = result.single()["chord_node_id"]

                # Link the song directly to its specific sequence of chords
                session.run("""
                    MATCH (s:Song {title: $title}), (c:Chord) WHERE id(c) = $chord_node_id
                    MERGE (s)-[:USES_PROGRESSION {sequence: $index}]->(c)
                """, title=song_title, chord_node_id=chord_node_id, index=index)

                # 4. Create the sequential TRANSITIONS_TO edge between consecutive chords
                if prev_chord_node_id is not None:
                    session.run("""
                        MATCH (p:Chord), (n:Chord) 
                        WHERE id(p) = $prev_id AND id(n) = $next_id
                        MERGE (p)-[r:TRANSITIONS_TO]->(n)
                        ON CREATE SET r.count = 1, r.genres = [$genre]
                        ON MATCH SET r.count = r.count + 1,
                                     r.genres = case when $genre in r.genres then r.genres else r.genres + $genre end
                    """, prev_id=prev_chord_node_id, next_id=chord_node_id, genre=genre)

                prev_chord_node_id = chord_node_id
                
        print(f"Successfully ingested {song_title} into the graph database.")

# --- EXECUTION ---
if __name__ == "__main__":
    populator = ChordGraphPopulator(URI, AUTH)
    
    try:
        # Example: Ingesting a single file (MIDI, MusicXML, etc.)
        # music21 has built-in corpus files for testing, like Bach chorales
        bach_file = converter.corpus.getWork('bach/bwv6.6.mxl') 
        
        populator.ingest_song(bach_file, "Chorale BWV 6.6", "Baroque")
        
        # For your own local files, you would loop through a directory:
        # populator.ingest_song("path/to/my_song.mid", "My Song Title", "Jazz")

    finally:
        populator.close()

3. How the Data Evolves (The "ON MATCH" Magic)

Take a close look at step 4 in the Cypher query. When Python processes a transition (e.g., C --> G), it doesn't just blindly insert a new line like SQL would. It uses Neo4j's ON CREATE and ON MATCH parameters:

MERGE (p)-[r:TRANSITIONS_TO]->(n)
ON CREATE SET r.count = 1, r.genres = [$genre]
ON MATCH SET r.count = r.count + 1,
             r.genres = case when $genre in r.genres then r.genres else r.genres + $genre end
  • First time the script sees $C \rightarrow G$, it creates the edge, sets count = 1, and tags it genres = ['Baroque'].

  • The 50th time it sees $C \rightarrow G$ in a Pop song, it increments count to 51 and appends 'Pop' to the array.

Calculating Weights Later

Once your database is full of hundreds of songs, you can turn those raw count properties into the decimal probabilities we discussed earlier by running a quick post-processing query:

MATCH (c:Chord)-[r:TRANSITIONS_TO]->()
WITH c, sum(r.count) AS totalOutboundTransitions
MATCH (c)-[r:TRANSITIONS_TO]->(next)
SET r.weight = toFloat(r.count) / totalOutboundTransitions

Now, every edge explicitly tells your applications the exact statistical probability of that transition happening across your entire library! Do you have a specific dataset or folder of MIDI/MusicXML files ready to feed into this?


From <https://gemini.google.com/app/6839fecc501e9eac>  Google Gemini (3.5 Flash)

Comments

Popular posts from this blog

#3679 Google Gemini 2.5 Pro: Create Python Application to View and Query Neo4j KG

#4054 AI Agents in AI Studio