0

Background

I've recently been trying to compose music, but I'm an absolute novice and I've no background in music theory. So I wanted to train LLaMA to help me give suggestions on how to continue a piece of music with multiple instruments. However, sheet music is two-dimensional (time and #instruments) and attention is one-dimensional, so writing the music down in a way that is easy for the attention mechanism can make a big difference.

Problem

I would like to convert MusicXML into ABC format with one additional criterium. What I would like to have is every voice split per bar, so it more easily be parsed with a large language model. Bellow is a sample of what I had imagined.

X:1
T: Three Bar Tune
M:4/4
L:1/4
K:C
%%MIDI program 1 0
%%MIDI program 2 40
%%MIDI program 3 42
V:1
[V:1 "Piano"] C G E G |]
V:2 clef=treble
[V:2 "Violin"] E B G B |]
V:3 clef=bass
[V:3 "Cello"] G, D B, D |]

V:1
[V:1 "Piano"] D A F A |]
V:2
[V:2 "Violin"] F C A C |]
V:3
[V:3 "Cello"] A, E C E |]

V:1
[V:1 "Piano"] E B G B |]
V:2
[V:2 "Violin"] G D B D |]
V:3
[V:3 "Cello"] B, F D F |]

V:1
[V:1 "Piano"] F C A C |]
V:2
[V:2 "Violin"] A, E C E |]
V:3
[V:3 "Cello"] C G E G |]

Does anybody know of a to create this format?

I tried midi2abc, but it's limited and only create While both MusicXML and ABC notation are fairly expressive, MIDI has some limitations, and is therefore not an ideal intermediate format.

I also tried writing my own program, but musical notation is very extensive. https://abcnotation.com/wiki/abc:standard:v2.1

import xml.etree.ElementTree as ET
from music21 import converter, pitch


def get_note_name(pitch_obj):
    abc_note_names = ['C', '^C', 'D', '^D', 'E', 'F', '^F', 'G', '^G', 'A', '^A', 'B']
    note_name = abc_note_names[pitch_obj.pitchClass]
    octave = pitch_obj.octave - 1

    if octave == -1:
        return note_name + ','
    elif octave == 0:
        return note_name
    elif octave == 1:
        return note_name.lower()
    elif octave == 2:
        return note_name.lower() + "'"
    else:
        return note_name.lower() + "'" * (octave - 1)


def musicxml_to_abc(musicxml_file_path):
    # Load the MusicXML file
    score = converter.parse(musicxml_file_path)
    tree = ET.parse(musicxml_file_path)
    root = tree.getroot()
    time_signature = '4/4'

    # Find the time signature
    for attributes in root.iter('attributes'):
        for time in attributes.iter('time'):
            beats = time.find('beats').text
            beat_type = time.find('beat-type').text
            time_signature = f"{beats}/{beat_type}"
            break

    abc = f"X:1\nT: One Bar Tune\nM:{time_signature}\nL:1/4\nK:C\n"

    voice_names = {}
    voices = {}
    for part in root.iter('part'):
        part_id = part.attrib['id']
        for score_part in root.find('part-list').iter('score-part'):
            if score_part.attrib['id'] == part_id:
                part_name = score_part.find('part-name').text
                voice_names[part_id] = part_name
                voices[part_id] = []

        for measure in part.iter('measure'):
            measure_notes = ''
            for note in measure.iter('note'):
                pitch_elem = note.find('pitch')
                if note.find('rest') is not None or pitch_elem is None:
                    measure_notes += 'z '
                else:
                    step = pitch_elem.find('step').text
                    alter = pitch_elem.find('alter')
                    octave = int(pitch_elem.find('octave').text)
                    if alter is not None:
                        alter = int(alter.text)
                    else:
                        alter = 0

                    pitch_obj = pitch.Pitch(step=step, octave=octave, accidental=alter)
                    note_name = get_note_name(pitch_obj)
                    measure_notes += note_name + ' '

            voices[part_id].append(measure_notes)

    voice_counter = 1
    for part_id, voice_name in voice_names.items():
        abc += f"%%MIDI program {voice_counter} 0\n"
        abc += f'V:{voice_counter} clef=treble\n'
        abc += f'[V:{voice_counter} "{voice_name}"] '
        voice_counter += 1

    for measure_num in range(len(voices[next(iter(voices))])):
        for voice_num, (part_id, voice_measures) in enumerate(voices.items(), start=1):
            abc += f"\nV:{voice_num}\n"
            abc += f"[V:{voice_num}] {voice_measures[measure_num]} |]\n"

    return abc


if __name__ == "__main__":
    musicxml_file_path = 'path/to/musicxml'
    abc_output = musicxml_to_abc(musicxml_file_path)

    print(abc_output)
Coen Hacking
  • 13
  • 1
  • 7
  • Trying to understand what you have written there. it is not python ? so it is music, some of you own notes or what is it ? https://stackoverflow.com/help/minimal-reproducible-example – D.L May 12 '23 at 10:49
  • It's an example of what I would like the output to be. It's three bars of music with three instruments in a four quarter beat – Coen Hacking May 12 '23 at 13:34
  • it might help if you show what you have tried and where you are stuck... – D.L May 12 '23 at 14:32
  • I suggest you forget this entire XY problem and do it the way classical composers have been doing it for centuries. *Write* the music in two staves, like a piano score, to work out the actual composition, harmonies, word-setting, etc. *Then* orchestrate it onto a full score. Nobody composes straight onto a full score, unless there are only a few real parts, and unless they are already competent. – user207421 May 15 '23 at 00:26

1 Answers1

0

I've written a solution based on xml2abc.py. Some modifications might still be necessary.

import os
import tempfile
import subprocess


def run_xml2abc(input_file):
    # Path to the python script
    script_path = 'xml2abc.py'

    # Options
    options = {
        '-u': None,
        '-m': '0',
        '-c': '0',
        '-b': '1',
        '-v': '1',
        '-x': None,
        '-p': '',  # replace with your pageformat if necessary
        '-t': None,
        '--v1': None,
        '--noped': None,
    }

    # Create a temporary file
    with tempfile.TemporaryDirectory() as temp_dir:
        options['-o'] = temp_dir

        # Prepare the command
        command = ['python3', script_path]
        for option, value in options.items():
            command.append(option)
            if value is not None:
                command.append(value)
        command.append(input_file)

        # Run the command
        subprocess.run(command)

        # The output file has the same name as the input file, but with .abc extension
        output_file = os.path.join(temp_dir, os.path.basename(input_file).rsplit('.', 1)[0] + '.abc')

        # Read the contents of the output file
        with open(output_file, 'r') as f:
            output = f.read()

    return output


def extract_measures(input_abc):
    lines = input_abc.split('\n')
    voices = {}
    current_voice = None

    for line in lines:
        if line.startswith('V:'):
            current_voice = line.split(' ')[0]
            voices[current_voice] = []
        elif '%' in line and current_voice is not None:
            # Remove the measure number
            measure = line.rsplit(' ', 1)[0]
            voices[current_voice].append(measure)

    return voices



def rewrite_abc(input_abc):
    # Split input into header and body
    header, instruments, measures = input_abc.split('V:1')
    instruments = 'V:1' + instruments

    # Extract measures from the body
    voices = extract_measures(input_abc)

    # Initialize output with header and voice definitions
    output = header.strip() + "\n" + instruments.strip()

    for m in range(len(voices['V:1'])):
        output += f"\n%{m+1}"
        for v in range(len(voices)):
            output += f"\n[V:{v+1}]{voices[f'V:{v+1}'][m]}"

    return output


if __name__ == "__main__":
    # Replace 'test.musicxml' with the path of your MusicXML file
    musicxml_file_path = 'test.musicxml'
    result = run_xml2abc(musicxml_file_path)
    print(result)
    result2 = rewrite_abc(result)
    print()
    print(result2)
Coen Hacking
  • 13
  • 1
  • 7