Python to .repatch (Mutator)

This forum is for developers of Rack Extensions to discuss the RE SDK, share code, and offer tips to other developers.
User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

15 Sep 2023

Ok so i've been busy creating a few python scripts that produce midi files and i would really love a simple way to process / convert them into pattern mutator repatch files so i thought i would reach out to any devs that might be able to assist or already have a script?

any help would be appreciated.

at the moment i'm guessing that the pattern_data_1 is a hex string but i could be well off, and would much rather not reinvent a wheel that might already be in someone's toolbox ;)

User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

16 Sep 2023

so this is what am currently doing

Code: Select all

c:\Billy>python random_rhythm_generator.py --help
usage: random_rhythm_generator.py [-h] [--charset CHARSET] [--rotate-left] [--rotate-right]

optional arguments:
  -h, --help         show this help message and exit
  --charset CHARSET  character set to use for generating random rhythm
  --rotate-left      rotate the string to the left
  --rotate-right     rotate the string to the right


c:\Billy>python random_rhythm_generator.py
Todays Random Rhythm is : rsrhhhhmsssshrhh


c:\Billy>python musicrhythm.py --help
usage: musicrhythm.py [-h] [--bpm BPM] [--output OUTPUT] [--human] N

Generate a MIDI file from a list of notes with volume and rest.

positional arguments:
  N                a string of notes and reset (h=Hard, m=Medium, s=Soft r=Rest)

optional arguments:
  -h, --help       show this help message and exit
  --bpm BPM        the tempo in beats per minute (default: 120)
  --output OUTPUT  the name of the output MIDI file (default: output.mid)
  --human          adds slight random volume to the notes.


c:\Billy>python musicrhythm.py --bpm 90 --human rsrhhhhmsssshrhh
Sequence of notes:
Note 1: r, Onset: 0.3333333333333333, Duration: 0.3333333333333333
Note 2: s, Volume: 55, Onset: 0.6666666666666666, Duration: 0.3333333333333333
Note 3: r, Onset: 1.0, Duration: 0.3333333333333333
Note 4: h, Volume: 93, Onset: 1.3333333333333333, Duration: 0.3333333333333333
Note 5: h, Volume: 102, Onset: 1.6666666666666665, Duration: 0.3333333333333333
Note 6: h, Volume: 101, Onset: 1.9999999999999998, Duration: 0.3333333333333333
Note 7: h, Volume: 95, Onset: 2.333333333333333, Duration: 0.3333333333333333
Note 8: m, Volume: 78, Onset: 2.6666666666666665, Duration: 0.3333333333333333
Note 9: s, Volume: 49, Onset: 3.0, Duration: 0.3333333333333333
Note 10: s, Volume: 47, Onset: 3.3333333333333335, Duration: 0.3333333333333333
Note 11: s, Volume: 46, Onset: 3.666666666666667, Duration: 0.3333333333333333
Note 12: s, Volume: 53, Onset: 4.0, Duration: 0.3333333333333333
Note 13: h, Volume: 94, Onset: 4.333333333333333, Duration: 0.3333333333333333
Note 14: r, Onset: 4.666666666666666, Duration: 0.3333333333333333
Note 15: h, Volume: 94, Onset: 4.999999999999999, Duration: 0.3333333333333333
Note 16: h, Volume: 97, Onset: 5.333333333333332, Duration: 0.3333333333333333

midi files saved as : output.mid


c:\Billy>python random_rhythm_generator.py --rotate-left
Todays Random Rhythm is : rsmrmrsrrsrrshss
Rotation 1: smrmrsrrsrrshssr
Rotation 2: mrmrsrrsrrshssrs
Rotation 3: rmrsrrsrrshssrsm
Rotation 4: mrsrrsrrshssrsmr
Rotation 5: rsrrsrrshssrsmrm
Rotation 6: srrsrrshssrsmrmr
Rotation 7: rrsrrshssrsmrmrs
Rotation 8: rsrrshssrsmrmrsr
Rotation 9: srrshssrsmrmrsrr
Rotation 10: rrshssrsmrmrsrrs
Rotation 11: rshssrsmrmrsrrsr
Rotation 12: shssrsmrmrsrrsrr
Rotation 13: hssrsmrmrsrrsrrs
Rotation 14: ssrsmrmrsrrsrrsh
Rotation 15: srsmrmrsrrsrrshs

all I'm missing is a midi2repatch script...

if anyone is interested in the scripts let me know, and if anyone can provide some info on the next phase (midi2repatch.py) that would be great.

User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

18 Sep 2023

ok so i've made some progress today but there are a few values i'm still not sure of and i know the props programmer love to have some fun so i'm wondering if maybe this value is just for giggles?
The 8380 twin flame symbolism is the act of satisfaction after you have met all your goals and dreams.

Code: Select all

            <Value property="pattern_data_1"  type="string" >
                020110013D67028380023D5E028380033D63028380043D35028380053D6A028380063D54028380073D36028380083D69028380093D300283800A3D2E0283800B3D610283800C3D320283800D3D670283800E3D340283800F3D43028380
            </Value>

Code: Select all

[Version?]0201 10 [16]=Steps 

position   | pitch+1     | velocity+1   | giggles
[1]	01     3D          [103] 67 	02 8380 
[2]	02     3D          [ 94] 5E  	02 8380 
[3]	03     3D          [ 99] 63  	02 8380 
[4]	04     3D          [ 53] 35  	02 8380 
[5]	05     3D          [106] 6A 	02 8380 
[6]	06     3D          [ 84] 54  	02 8380 
[7]	07     3D          [ 54] 36  	02 8380 
[8]	08     3D          [105] 69 	02 8380 
[9]	09     3D          [ 48] 30  	02 8380 
[10]	0A     3D          [ 46] 2E  	02 8380 
[11]	0B     3D          [ 97] 61  	02 8380 
[12]	0C     3D          [ 50] 32  	02 8380 
[13]	0D     3D          [103] 67 	02 8380 
[14]	0E     3D          [ 52] 34  	02 8380 
[15]	0F     3D          [ 67] 43  	02 8380



User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

18 Sep 2023

so i'm not 100% happy with the script but it can take a midi file and generate the string needed to make a PM.repatch file ;)
repatchfaker.png
repatchfaker.png (44.08 KiB) Viewed 36821 times
it defo needs some more work, but I can basically generate files from my daily random rhythm generator :lol:

WaxTrax
Posts: 180
Joined: 16 Feb 2021

18 Sep 2023

Very cool! :-)

User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

21 Sep 2023

took a bit more time on this today and i'm getting closer ;)
repatchfaker2.png
repatchfaker2.png (63.55 KiB) Viewed 36660 times
I'm sure there's a simple solution but I've only just started with python - guess those that real know don't want to share so i will just keep plodding along

User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

21 Sep 2023

so I finally got round to generating some more data for giggles - and it's no joke.....

Bar-1/128th.mid
Note 1 bar, time: 2.0
Note 1/2, time: 1.0
Note 1/4, time: 0.5
Note 1/8, time: 0.25
Note 1/16, time: 0.125
Note 1/32, time: 0.0625
Note 1/64, time: 0.03125
Note 1/128, time: 0.015625

decided this was enough data to play with for now - and time is an issue for me "would defo like some help"......

thankfully I'm only working in 16th so my scripts works, but it would be nice to extend / enhance this to work with other midi files.....

User avatar
Enlightenspeed
RE Developer
Posts: 1106
Joined: 03 Jan 2019

22 Sep 2023

Billy+ wrote:
21 Sep 2023
guess those that real know don't want to share so i will just keep plodding along
As a first step, you should ask RS to give you the schema for the data strings, and permission to discuss the schema publicly.

Without that, it's difficult for other devs to participate; any of us could do it easily enough, but it involves a level of reverse engineering, which is legally dubious - not that it would be any real threat of course, this stuff is far too basic.

The moral issue is still there, however, so if you can get RS to publicly agree to this first, then other RE devs can help.

avasopht
Competition Winner
Posts: 3954
Joined: 16 Jan 2015

22 Sep 2023

And I'll just add that they're all a lot more open than you might think!

User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

22 Sep 2023

Enlightenspeed wrote:
22 Sep 2023
Billy+ wrote:
21 Sep 2023
guess those that real know don't want to share so i will just keep plodding along
As a first step, you should ask RS to give you the schema for the data strings, and permission to discuss the schema publicly.

Without that, it's difficult for other devs to participate; any of us could do it easily enough, but it involves a level of reverse engineering, which is legally dubious - not that it would be any real threat of course, this stuff is far too basic.

The moral issue is still there, however, so if you can get RS to publicly agree to this first, then other RE devs can help.
ah!

hadn't really considered that......

Sorry Reason Studios, if you see this thread would you possibly chip in and advise on any concerns :thumbup:
if you would rather the info be removed please say so.
if however you're cool with it please give the go ahead ;)

again sorry, and if mods want to remove images / code please feel free to :thumbs_up:

User avatar
DaveyG
Posts: 2557
Joined: 03 May 2020

22 Sep 2023

:roll:

User avatar
Enlightenspeed
RE Developer
Posts: 1106
Joined: 03 Jan 2019

22 Sep 2023

Billy+ wrote:
22 Sep 2023

ah!

hadn't really considered that......

Sorry Reason Studios, if you see this thread would you possibly chip in and advise on any concerns :thumbup:
if you would rather the info be removed please say so.
if however you're cool with it please give the go ahead ;)

again sorry, and if mods want to remove images / code please feel free to :thumbs_up:
I’d be surprised if they aren’t cool with it, but as a rule ask first. The patches are in plain text, not encrypted, so it’s easy to do this kind of thing, and this is probably intended by RS.

In the spirit of sharing, if they make the schema available, I’ll write the script for you, and make it public 😊

Cheers,
Brian

User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

22 Sep 2023

Enlightenspeed wrote:
22 Sep 2023
Billy+ wrote:
22 Sep 2023

ah!

hadn't really considered that......

Sorry Reason Studios, if you see this thread would you possibly chip in and advise on any concerns :thumbup:
if you would rather the info be removed please say so.
if however you're cool with it please give the go ahead ;)

again sorry, and if mods want to remove images / code please feel free to :thumbs_up:
I’d be surprised if they aren’t cool with it, but as a rule ask first. The patches are in plain text, not encrypted, so it’s easy to do this kind of thing, and this is probably intended by RS.

In the spirit of sharing, if they make the schema available, I’ll write the script for you, and make it public 😊

Cheers,
Brian
thanks man :oops:

and seriously i didn't even think about the protentional issue, i guess i just got a bit of tunnel vision - i've been hitting a bit of a brick wall for a few months musically speaking and started out with thinking maybe some random rhythms might inspire something and it spiraled.

if i could remove the current insight i would but i can't

thanks for the intervention :thumbup:

User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

22 Sep 2023

i have updated the initial script (random_rhythm_generator.py) and im happy to share it...

example usage :-

Code: Select all

c:\Billy>python random_rhythm_generator.py --help
usage: random_rhythm_generator.py [-h] [--charset CHARSET] [--rotate-left] [--rotate-right] [--limiting-factor LIMITING_FACTOR] [--max-repeating MAX_REPEATING]

optional arguments:
  -h, --help            show this help message and exit
  --charset CHARSET     character set to use for generating random rhythm
  --rotate-left         rotate the string to the left
  --rotate-right        rotate the string to the right
  --limiting-factor LIMITING_FACTOR
                        specify an individual character as a parameter to be the limiting factor
  --max-repeating MAX_REPEATING
                        specify the maximum number of times a character can be repeated

c:\Billy>python random_rhythm_generator.py
Todays Random Rhythm is : smsshmmrrmmshsrh

c:\Billy>python random_rhythm_generator.py --rotate-right --limiting-factor h3 --max-repeating 2
Todays Random Rhythm is : shmhsmrhmrmmsrss
Rotation 01: sshmhsmrhmrmmsrs
Rotation 02: ssshmhsmrhmrmmsr
Rotation 03: rssshmhsmrhmrmms
Rotation 04: srssshmhsmrhmrmm
Rotation 05: msrssshmhsmrhmrm
Rotation 06: mmsrssshmhsmrhmr
Rotation 07: rmmsrssshmhsmrhm
Rotation 08: mrmmsrssshmhsmrh
Rotation 09: hmrmmsrssshmhsmr
Rotation 10: rhmrmmsrssshmhsm
Rotation 11: mrhmrmmsrssshmhs
Rotation 12: smrhmrmmsrssshmh
Rotation 13: hsmrhmrmmsrssshm
Rotation 14: mhsmrhmrmmsrsssh
Rotation 15: hmhsmrhmrmmsrsss

this is the actual code below for anyone interested :- save as: random_rhythm_generator.py

Code: Select all

import argparse
import random
import string

def rotate_string(string, rotation):
    return string[rotation:] + string[:rotation]

def modify_string(string, limiting_factor, max_repeating):
    limiting_character, limiting_count = limiting_factor[0], int(limiting_factor[1:])
    limiting_factor_count = string.count(limiting_character)
    if limiting_factor_count > limiting_count:
        new_random_string = string.replace(limiting_character, limiting_character * limiting_count)
    else:
        new_random_string = string
    if max_repeating is not None:
        new_random_string = enforce_max_repeating(new_random_string, max_repeating)
    return new_random_string

def enforce_max_repeating(string, max_repeating):
    for char in set(string):
        if string.count(char) > max_repeating:
            string = string.replace(char * max_repeating, char * max_repeating + char)
    return string

def generate_random_string(charset, length):
    return ''.join(random.choices(charset, k=length))

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--charset', type=str, default='hmsr', help='character set to use for generating random rhythm')
    parser.add_argument('--rotate-left', action='store_true', help='rotate the string to the left')
    parser.add_argument('--rotate-right', action='store_true', help='rotate the string to the right')
    parser.add_argument('--limiting-factor', type=str, default=None, help='specify an individual character as a parameter to be the limiting factor')
    parser.add_argument('--max-repeating', type=int, default=None, help='specify the maximum number of times a character can be repeated')
    args = parser.parse_args()

    random_string = generate_random_string(args.charset, 16)

    if args.rotate_left:
        for i in range(len(random_string)):
            rotated_string = rotate_string(random_string, i)
            if i == 0:
                print(f"Todays Random Rhythm is : {random_string} ")
            elif i > 0:
                print(f"Rotation {i:02}: {rotated_string}")

    elif args.rotate_right:
        for i in range(len(random_string)):
            rotated_string = rotate_string(random_string, -i)
            if i == 0:
                print(f"Todays Random Rhythm is : {random_string} ")
            elif i > 0:
                print(f"Rotation {i:02}: {rotated_string}")

    elif args.limiting_factor is not None:
        new_random_string = modify_string(random_string, args.limiting_factor, args.max_repeating)
        print(f"Todays Random Rhythm is : {new_random_string} ")

    else:
        print(f"Todays Random Rhythm is : {random_string} ")

if __name__ == "__main__":
    main()


avasopht
Competition Winner
Posts: 3954
Joined: 16 Jan 2015

23 Sep 2023

Billy+ wrote:
22 Sep 2023
i've been hitting a bit of a brick wall for a few months musically speaking and started out with thinking maybe some random rhythms might inspire something and it spiraled.

if i could remove the current insight i would but i can't

thanks for the intervention :thumbup:
Funnily enough, a few months ago I'd made this track using instruments (as I normally do). But I had a drum kit loaded with a good 64+ chord and note stabs.

I was surprised by how much they added to my track.

I think you might be onto something.

User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

23 Sep 2023

avasopht wrote:
23 Sep 2023
Billy+ wrote:
22 Sep 2023
i've been hitting a bit of a brick wall for a few months musically speaking and started out with thinking maybe some random rhythms might inspire something and it spiraled.

if i could remove the current insight i would but i can't

thanks for the intervention :thumbup:
Funnily enough, a few months ago I'd made this track using instruments (as I normally do). But I had a drum kit loaded with a good 64+ chord and note stabs.

I was surprised by how much they added to my track.

I think you might be onto something.
yeah its funny how just getting something you wouldn't normally do can inspire.
I also have / had a brute force dictionary of numbers that I randomly grab a line from and use them to create chords etc.

this project just created a rhythm that I wouldn't normally create, I stuck with 16th that are either Rest Hard Medium or Soft hits.

oh I just realized that the script it's the best version :lol:
it's got bugs
, I was trying to extend it but failed..

User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

23 Sep 2023

I also made another script that walks through a folder and subfolder playing wav mid files and listing patches etc, so I don't get distracted by shinny things and instead just listen to what's available..... that way I just sit back listen and wait for something to jump out at me ;)

this is basically its output on the console, and anything that can play gets played :lol:

c:\Billy>python playkit.py c:\DeepHouseCollection\Club_Essential_Series_-_Deep_House_Kits_Vol4_product_pack
FOLDER : EAM_Club_Series_Deep_House_Kits_Vol_4 [FILES 2]
FOLDER : KIT 01 126 A# [FILES 1]
* Playing FULL KIT 01 126 A#.wav
FOLDER : LOOPS [FILES 8]
* Playing BASS.wav
* Playing CLAP.wav
* Playing KICK.wav
* Playing PERC LOOP.wav
* Playing SHAKER LOOP 2.wav
* Playing SHAKER LOOP.wav
* Playing TRUMPET.wav
* Playing VOCAL.wav
FOLDER : MIDI [FILES 2]
* Playing BASS.mid
* Playing TRUMPET.mid
FOLDER : ONE SHOTS [FILES 10]
* Playing BASS SHOT A#.wav
* Playing CLAP.wav
* Playing CRASH.wav
* Playing KICK.wav
* Playing PERC 01.wav
* Playing SHAKER 01.wav
* Playing SHAKER 02.wav
* Playing TRUMPET C#.wav
* Playing VOCAL SHOT.wav
* Playing VOX.wav
FOLDER : PRESETS [FILES 1]
BASS - SPIRE.spf is a Spire Patch File.
FOLDER : STEMS [FILES 10]
* Playing BASS.wav
* Playing CLAP.wav
* Playing CRASH.wav
* Playing KICK.wav
* Playing PERC LOOP.wav
* Playing SHAKER LOOP 01.wav
* Playing SHAKER LOOP 20.wav
* Playing TRUMPET.wav
* Playing VOCAL SHOT.wav
* Playing VOX.wav
FOLDER : KIT 02 126 G [FILES 1]
* Playing FULL KIT G 126.wav

User avatar
buddard
RE Developer
Posts: 1245
Joined: 17 Jan 2015
Location: Stockholm
Contact:

26 Sep 2023

I had overlooked this topic, but thanks to a heads up from Avasopht over at the official dev forum I came here. :puf_smile:

Here is the pattern format for Mutator:

Pattern note data is packed into string properties in the RE "motherboard".
And as you have already guessed, string properties are stored in hex format in the .repatch file.
We can't use any 0 values in the data since it will terminate the string, so most values will range from 1 - 255 (01 - FF).

The first byte is the pattern data version, which is currently 2. (or "02" in hex). Don't change this ;)

The next two bytes represent the number of notes in the pattern, where the first byte is the most significant byte.
So if your pattern has 16 notes, the string sequence will be "0111" (remember that each byte must be offset by 1...)
A string property can hold a maximum of 2048 bytes, and each note uses 6 bytes, so the maximum number of notes per pattern supported by Mutator is 340.

Next you have the data for each note in the pattern, and that is laid out as follows, per byte:

- Note start (in steps, 1 - 128)
- Note number (1 - 128, MIDI note number offset by 1)
- Note velocity (1 - 128, i e offset by 1)
- Note length (in steps, offset by 1)
- Note flags (see below)
- Length scale factor (in 1/254ths, offset by 1)

The note flags value is 128 + (32 if the note is muted) + (16 if the note is tied) + the octave offset (0 - 6 corresponds to -3 - +3. the normal value is 3).
For a plain unmodified note, the flag value is 128 + 3 = 131, or "83" in hex.
The per-note mute and octave offset are not normally used by Mutator, they were inherited from Sequences. But the playback engine supports them. :puf_smile:


I might have made mistakes in the description or explained something poorly, so feel free to ask questions!

User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

27 Sep 2023

buddard wrote:
26 Sep 2023
feel free to ask questions!
thanks man, so I take it its ok to continue and I haven't been naughty :oops:

User avatar
buddard
RE Developer
Posts: 1245
Joined: 17 Jan 2015
Location: Stockholm
Contact:

27 Sep 2023

Billy+ wrote:
27 Sep 2023
buddard wrote:
26 Sep 2023
feel free to ask questions!
thanks man, so I take it its ok to continue and I haven't been naughty :oops:
Yeah, go for it!

User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

27 Sep 2023

so I thought I would start with decoding a value first ;)

Code: Select all

def mutator_schema(hex_string):
    # int(value, base [16=hex])
    version = int(hex_string[:2], 16)
    total_notes = int(hex_string[4:6], 16)

    note_data = []

    for i in range(len(hex_string) // 12):
        # i think the about might be wrong ?
        step = int(hex_string[6 + i * 12:8 + i * 12], 16)

        pitch = int(hex_string[8 + i * 12:10 + i * 12], 16) - 1
        velocity = int(hex_string[10 + i * 12:12 + i * 12], 16) - 1
        
        lengthinsteps = int(hex_string[12 + i * 12:14 + i * 12], 16) - 1
        # these are possibly less needed when converting from midi 
        flags = int(hex_string[14 + i * 12:16 + i * 12], 16)
        mute = bool(flags & 32)
        tied = bool(flags & 16)
        octave_offset = flags & 7 - 3

        note_data.append((step, pitch, velocity, lengthinsteps, mute, tied, octave_offset))
        
    return version, total_notes, note_data


pattern_data_1 = "020110013D67028380023D5E028380033D63028380043D35028380053D6A028380063D54028380073D36028380083D69028380093D300283800A3D2E0283800B3D610283800C3D320283800D3D670283800E3D340283800F3D43028380"


version, total_notes, note_data = mutator_schema(pattern_data_1)

print(f'{version} {total_notes}')
for note in note_data:
    step, pitch, velocity, lengthinsteps, mute, tied, octave_offset = note   
    print(f"Note: pos={step:02}, pitch={pitch}, velocity={velocity:03}, length={lengthinsteps}, mute={mute}, tied={tied}, octave_offset={octave_offset}")


does that look right to others ?
I'm still having difficulty getting my head around it all, got stuff going on that's keeping me away from the screen.

as things stand my initial script works fine when working with my random rhythm generator script as I'm only working in 1/16th for 16 steps, but being able to dump any reasonable size midi into PM would just be cool.

that's the output from the above.

Code: Select all

2 16
Note: pos=01, pitch=60, velocity=102, length=1, mute=False, tied=False, octave_offset=0
Note: pos=02, pitch=60, velocity=093, length=1, mute=False, tied=False, octave_offset=0
Note: pos=03, pitch=60, velocity=098, length=1, mute=False, tied=False, octave_offset=0
Note: pos=04, pitch=60, velocity=052, length=1, mute=False, tied=False, octave_offset=0
Note: pos=05, pitch=60, velocity=105, length=1, mute=False, tied=False, octave_offset=0
Note: pos=06, pitch=60, velocity=083, length=1, mute=False, tied=False, octave_offset=0
Note: pos=07, pitch=60, velocity=053, length=1, mute=False, tied=False, octave_offset=0
Note: pos=08, pitch=60, velocity=104, length=1, mute=False, tied=False, octave_offset=0
Note: pos=09, pitch=60, velocity=047, length=1, mute=False, tied=False, octave_offset=0
Note: pos=10, pitch=60, velocity=045, length=1, mute=False, tied=False, octave_offset=0
Note: pos=11, pitch=60, velocity=096, length=1, mute=False, tied=False, octave_offset=0
Note: pos=12, pitch=60, velocity=049, length=1, mute=False, tied=False, octave_offset=0
Note: pos=13, pitch=60, velocity=102, length=1, mute=False, tied=False, octave_offset=0
Note: pos=14, pitch=60, velocity=051, length=1, mute=False, tied=False, octave_offset=0
Note: pos=15, pitch=60, velocity=066, length=1, mute=False, tied=False, octave_offset=0
PS C:\Billy>

User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

27 Sep 2023

i've also generated a midi file and repatch for whole note down to a 128th

Code: Select all

pattern_data_1 = "020108013D65118380113D65098380193D650583801D3D650383801F3D65028380203D65028380213D65028380"
which returns this data

Code: Select all

2 7
Note: pos=01, pitch=60, velocity=100, length=16, mute=False, tied=False, octave_offset=0
Note: pos=17, pitch=60, velocity=100, length=8, mute=False, tied=False, octave_offset=0 
Note: pos=25, pitch=60, velocity=100, length=4, mute=False, tied=False, octave_offset=0 
Note: pos=29, pitch=60, velocity=100, length=2, mute=False, tied=False, octave_offset=0 
Note: pos=31, pitch=60, velocity=100, length=1, mute=False, tied=False, octave_offset=0 
Note: pos=32, pitch=60, velocity=100, length=1, mute=False, tied=False, octave_offset=0 
Note: pos=33, pitch=60, velocity=100, length=1, mute=False, tied=False, octave_offset=0 
which tells me I'm going to need to do more with the pos (step) value and the length as everything below a 16 is always 1. anyway that's me finished for the next 20 ish hours.. maybe someone can shed some light onto the topic...

User avatar
Billy+
Posts: 4162
Joined: 09 Dec 2016

27 Sep 2023

for what its worth, this is my current progress with generating the string but its incomplete, all I've added is the info from buddard

BE WARNED

Code: Select all

import sys
from mido import MidiFile
'''
Here is the pattern format for Mutator:

Pattern note data is packed into string properties in the RE "motherboard".
And as you have already guessed, string properties are stored in hex format in the .repatch file.
We can't use any 0 values in the data since it will terminate the string, so most values will range from 1 - 255 (01 - FF).

The first byte is the pattern data version, which is currently 2. (or "02" in hex). Don't change this ;)

The next two bytes represent the number of notes in the pattern, where the first byte is the most significant byte.
So if your pattern has 16 notes, the string sequence will be "0111" (remember that each byte must be offset by 1...)
A string property can hold a maximum of 2048 bytes, and each note uses 6 bytes, 
so the maximum number of notes per pattern supported by Mutator is 340.

Next you have the data for each note in the pattern, and that is laid out as follows, per byte:

- Note start (in steps, 1 - 128)
- Note number (1 - 128, MIDI note number offset by 1)
- Note velocity (1 - 128, i e offset by 1)
- Note length (in steps, offset by 1)
- Note flags (see below)
- Length scale factor (in 1/254ths, offset by 1)

The note flags value is 128 + (32 if the note is muted) + (16 if the note is tied) + the octave offset (0 - 6 corresponds to -3 - +3. 
the normal value is 3).
For a plain unmodified note, the flag value is 128 + 3 = 131, or "83" in hex.
The per-note mute and octave offset are not normally used by Mutator, they were inherited from Sequences. 
But the playback engine supports them.
'''

ppqn = 0
whole_note_duration = 0
# could just be a version number, or might be more...
header = '0201'
# hex the step value!, update to count the line later....
steps = 16
# this defo has more meaning but for now it's all i got ;)
# giggle is no joke, it must have something to do with note lenght / off etc. (dump some midi from the repatch files you have ;))
giggle = '028380'
# first part of the string seams to be this ? on the small data set i have tested...
pattern_data_1 = f'{header}{steps:02X}'

def warningnotice():
    try:
        print("This script is almost certainty broken and should only be used if you accept responsibility")
        
        agree = input("Do you argee (y/n): ")
        if agree.lower() == "n":
            print("Exiting the program...")
            sys.exit(0)
        
    except Exception as e:
        print(f"An error occurred: {e}")
        sys.exit(1)


def calculate_standard_notation(note_duration):
    # Calculate the length of the note in standard notation
    standard_notation = ""
    
    if note_duration == whole_note_duration:
        standard_notation = "Whole Note"
    elif note_duration == whole_note_duration / 2:
        standard_notation = "1/2 Note"
    elif note_duration == whole_note_duration / 4:
        standard_notation = "1/4 Note"
    elif note_duration == whole_note_duration / 8:
        standard_notation = "1/8 Note"
    elif note_duration == whole_note_duration / 16:
        standard_notation = "1/16 Note"
    elif note_duration == whole_note_duration / 32:
        standard_notation = "1/32 Note"
    elif note_duration == whole_note_duration / 64:
        standard_notation = "1/64 Note"
    elif note_duration == whole_note_duration / 128:
        standard_notation = "1/128 Note"
    
    return standard_notation


def read_midi_file(file_path):
    mid = MidiFile(file_path)
    global ppqn
    ppqn = mid.ticks_per_beat
    global whole_note_duration
    whole_note_duration = ppqn * 4
    global pattern_data_1
    pos = 1
    offset = 1
    for i, track in enumerate(mid.tracks):
        if track.name > '':
            print(f"Track [{i:03}] Name: {track.name}")
        for j, msg in enumerate(track):
            if msg.type == "note_on":
                note_on_time = msg.time
                note_off_time = track[j + 1].time
                note_duration = note_off_time - note_on_time
                standard_notation = calculate_standard_notation(note_duration)
                if standard_notation >"":
                    print(f'{pos:02X} {msg.note} {msg.velocity}')
                    pattern_data_1 += f'{pos:02X}{msg.note+offset:02X}{msg.velocity+offset:02X}{str(giggle)}'
                    
                else:
                    pos +=1
                    print(f'{pos:02X} {msg.note} {msg.velocity}')
                    pattern_data_1 += f'{pos:02X}{msg.note+offset:02X}{msg.velocity+offset:02X}{str(giggle)}'
                    
            elif msg.type == "note_off":
                pos += 1
    
    # for now just print the string to screen, check solution (update_xml.py) for method of finding and writing to a repatch file ;)
    print(f'\n{pattern_data_1}')
       
    
if len(sys.argv) > 1:
    # i'm still working on this, so there is defo bugs....
    warningnotice()
    # once i'm happy this will be removed
    read_midi_file(sys.argv[1])
else:
    print("Please provide a random rhythm midi filename.")
    # ;) notice ='random rhythm file'

it might even be completely broke as i might have added stuff before i got advised to hold on for permission

User avatar
buddard
RE Developer
Posts: 1245
Joined: 17 Jan 2015
Location: Stockholm
Contact:

28 Sep 2023

Billy+ wrote:
27 Sep 2023
which tells me I'm going to need to do more with the pos (step) value and the length as everything below a 16 is always 1.
I'm not sure if I understand what you mean by this?

The note start position is just the step number in hexadecimal, i e 01, 02, 03, 04, ..., 09, 0A, ..., 0F, 10, 11, 12, ..., 1F and so on, up to 80 (= 128 in decimal).

User avatar
Enlightenspeed
RE Developer
Posts: 1106
Joined: 03 Jan 2019

29 Sep 2023

Awesome!

If I get time on Saturday I’ll do a script for building PM patches in Python.

Cheers,
Brian

Post Reply
  • Information
  • Who is online

    Users browsing this forum: No registered users and 2 guests