Major refactor

I have spent some time (like two hours) fixing up the app, getting it up
to a somewhat better level. Code is still bad, but that's that. If
anybody is interested to make the code neater, feel free to open a PR!
This commit is contained in:
2025-03-07 17:18:15 +01:00
parent cabfe9cbd7
commit 6fad58dd64
10 changed files with 216 additions and 276 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
__pycache__

View File

@@ -1,34 +1,66 @@
# midi-micro-bit_sound-converter <div id="title" align="center">
<h1>Midi to Micro:bit Sound converter</h1>
</div>
<div id="badges" align="center">
<img src="https://img.shields.io/github/license/janishutz/midi-micro-bit_sound-converter.svg">
<img src="https://img.shields.io/github/repo-size/janishutz/midi-micro-bit_sound-converter.svg">
<img src="https://img.shields.io/github/languages/top/janishutz/midi-micro-bit_sound-converter">
<img src="https://img.shields.io/github/directory-file-count/janishutz/midi-micro-bit_sound-converter.svg">
<br>
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/janishutz/midi-micro-bit_sound-converter">
<img alt="GitHub watchers" src="https://img.shields.io/github/watchers/janishutz/midi-micro-bit_sound-converter">
<img src="https://img.shields.io/github/issues-pr-raw/janishutz/midi-micro-bit_sound-converter">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/janishutz/midi-micro-bit_sound-converter">
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/janishutz/midi-micro-bit_sound-converter">
<br>
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/janishutz/midi-micro-bit_sound-converter/total?label=Downloads (total)">
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/downloads/janishutz/midi-micro-bit_sound-converter/latest/total?label=Downloads (latest)">
<img src="https://img.shields.io/github/release/janishutz/midi-micro-bit_sound-converter.svg">
<img src="https://img.shields.io/github/package-json/v/janishutz/midi-micro-bit_sound-converter.svg?label=Development Version">
</div>
This app allows you to convert a midi file to the code needed for micro:bit programming This app allows you to convert a midi file to the code needed for micro:bit programming
Creating Music with the micro:bit is a hassle. This little app will allow you to take any midi-file and convert it into the list needed for micro:bit. Creating Music with the micro:bit is a hassle. This little app will allow you to take any midi-file and convert it into the list needed for micro:bit.
## INSTALLATION: # Installation
Download the files by clicking on code, then on zip. Download the files by clicking on code, then on zip or clone the repo locally using
You will need to install some dependencies, as well as python 3.8. If you haven't already go ahead and download python 3.8 and make sure to also include pip, ```
as this will be used right after. Now, you will need to type the following commands in the terminal / command prompt: git clone https://github.com/janishutz/midi-micro-bit_sound-converter
pip install kivy[base] ```
pip install kivymd
pip install pyperclip
pip install mido
You can run the app by heading into the folder you downloaded the zip file into, unzipping it and then by running the midi-converter.py file in the terminal / command prompt. (python3 midi-converter.py) Then, run
You may do this as follows: ```
### Linux and MacOS: pip install -r requirements.txt
Use cd./Path/To/File ```
in the repo's folder (i.e. the folder you just cloned or downloaded and extracted)
### Linux Alternatively, create a venv using
You may download the bash script that is located under releases. Give it execute permissions (e.g run chmod +x ./Path/To/File) and then run the file with ./Path/To/File ```
python -m venv midi-converter
```
### Windows: and activate it using
Click on the navigation bar (the one where the path is displayed) and type: cmd ```
source ./midi-converter/bin/active
```
The dependencies of this project are `mido`, `pyperclip`, `kivymd` and `kivy[base]`
### SPECIAL notes for Linux users: # Running
You'll need to install some other dependencies first. Use your distro's package manager (apt-get for Debian based distros, dnf for Fedora based and pacman for arch-based distros). I'll show an example with debian based distros here: (you may also run the script under releases if you run a debian based distro. Open a terminal in the file location where you saved / cloned this repo to. Type
sudo apt-get install xclip ```
sudo apt-get install xsel python midi_converter.py
sudo apt-get install wl-clipboard ```
### OTHER OPTION: to run the app
You may also run it in a venv (Virtual Environment), e.g. in Thonny. You still must install kivy[base], kivymd, pyperclip and mido in that venv (by using the manager of the IDE you are running)
## Notes for Linux users:
On some Linux distros, `xclip` and `xsel` don't come pre-installed. Install these dependencies.
# Development
Be warned, the code base is still very ugly. I only spent about two hours cleaning up the old code, so it still looks ugly. I will probably not clean up the code much more. Some variable names will simply stay weird

View File

@@ -1,122 +0,0 @@
"""@package docstring
This is a simplification of the csv module"""
import csv
class CsvRead:
"""This is a class that reads csv files and depending on the module selected does do different things with it"""
def __init__(self):
self.__imp = ""
self.__raw = ""
self.__raw_list = ""
def importing(self, path):
"""Returns a list of the imported csv-file, requires path, either direct system path or relative path"""
self.__imp = open(path)
self.__raw = csv.reader(self.__imp, delimiter=',')
self.__raw_list = list(self.__raw)
self.__imp.close()
return self.__raw_list
class CsvWrite:
"""This is a class that modifies csv files"""
def __init__(self):
self.__impl = []
self.__strpop = []
self.__removed = []
self.__removing = 0
self.__change = 0
self.__appending = 0
self.__imp = []
self.__raw = []
def rem_str(self, path, row):
"""Opens the csv-file in write mode which is specified as an argument either as direct or relative path"""
self.__imp = open(path)
self.__raw = csv.reader(self.__imp, delimiter=',')
self.__impl = list(self.__raw)
self.__removed = self.__impl.pop(row + 1)
with open(path, "w") as removedata:
self.__removing = csv.writer(removedata, delimiter=',', quoting=csv.QUOTE_MINIMAL)
self.__removing.writerow(self.__impl.pop(0))
while len(self.__impl) > 0:
with open(path, "a") as removedata:
self.__removing = csv.writer(removedata, delimiter=',', quoting=csv.QUOTE_MINIMAL)
self.__removing.writerow(self.__impl.pop(0))
self.__imp.close()
removedata.close()
def chg_str(self, path, row, pos, new_value):
"""Opens the csv-file in write mode to change a value, e.g. if a recipes is changed."""
self.__imp = open(path)
self.__raw = csv.reader(self.__imp, delimiter=',')
self.__impl = list(self.__raw)
self.__strpop = self.__impl.pop(row)
self.__strpop.pop(pos)
self.__strpop.insert(pos, new_value)
self.__impl.insert(row, self.__strpop)
with open(path, "w") as changedata:
self.__change = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL)
self.__change.writerow(self.__impl.pop(0))
while len(self.__impl) > 0:
with open(path, "a") as changedata:
self.__removing = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL)
self.__removing.writerow(self.__impl.pop(0))
self.__imp.close()
changedata.close()
def chg_str_rem(self, path, row, pos):
"""Opens the csv-file in write mode to change a value, e.g. if a recipes is changed."""
self.__imp = open(path)
self.__raw = csv.reader(self.__imp, delimiter=',')
self.__impl = list(self.__raw)
self.__strpop = self.__impl.pop(row)
self.__strpop.pop(pos)
self.__strpop.pop(pos)
self.__impl.insert(row, self.__strpop)
with open(path, "w") as changedata:
self.__change = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL)
self.__change.writerow(self.__impl.pop(0))
while len(self.__impl) > 0:
with open(path, "a") as changedata:
self.__removing = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL)
self.__removing.writerow(self.__impl.pop(0))
self.__imp.close()
changedata.close()
def chg_str_add(self, path, row, new_value1, new_value2):
"""Opens the csv-file in write mode to change a value, e.g. if a recipes is changed."""
self.__imp = open(path)
self.__raw = csv.reader(self.__imp, delimiter=',')
self.__impl = list(self.__raw)
self.__strpop = self.__impl.pop(row)
self.__strpop.append(new_value1)
self.__strpop.append(new_value2)
self.__impl.insert(row, self.__strpop)
with open(path, "w") as changedata:
self.__change = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL)
self.__change.writerow(self.__impl.pop(0))
while len(self.__impl) > 0:
with open(path, "a") as changedata:
self.__removing = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL)
self.__removing.writerow(self.__impl.pop(0))
self.__imp.close()
changedata.close()
def app_str(self, path, value):
"""Opens the csv-file in append mode and writes given input. CsvWrite.app_str(path, value).
Path can be specified both as direct or relative. value is a list. Will return an error if type of value is
not a list."""
with open(path, "a") as appenddata:
self.__appending = csv.writer(appenddata, delimiter=',', quoting=csv.QUOTE_MINIMAL)
self.__appending.writerow(value)
appenddata.close()
def write_str(self, path, value):
with open(path, "w") as writedata:
self.__change = csv.writer(writedata, delimiter=',', quoting=csv.QUOTE_MINIMAL)
self.__change.writerow(value)
writedata.close()

View File

@@ -1,96 +0,0 @@
from mido import MidiFile
import pyperclip as pc
class MidiManagement:
def __init__(self):
pass
def addToClipboard(self, text):
pc.copy(text)
def analyse_track(self, path, trackname):
self.midi_imp = MidiFile(path, clip=True)
self.tracks = []
for self.track in self.midi_imp.tracks:
self.tracks.append(str(self.track))
if len(self.tracks) > 1:
self.tracks.pop(0)
else:
pass
self.track_ext = self.tracks.pop(0)
self.trackn = 0
while self.track_ext != trackname:
self.track_ext = self.tracks.pop(0)
self.trackn += 1
self.extracted_track = self.track_ext
self.__output_list = []
for self.msg in self.midi_imp.tracks[self.trackn]:
self.ext = str(self.msg)
self.note = self.ext[23:25]
if self.ext[0:8] == "note_on ":
try:
self.note_height = int(self.note)
self.note_decod_oct = self.note_height // 12
self.note_decode_tone = self.note_height % 12
if self.note_decode_tone == 1:
self.note_ext = "C"
elif self.note_decode_tone == 2:
self.note_ext = "C#"
elif self.note_decode_tone == 3:
self.note_ext = "D"
elif self.note_decode_tone == 4:
self.note_ext = "D#"
elif self.note_decode_tone == 5:
self.note_ext = "E"
elif self.note_decode_tone == 6:
self.note_ext = "F"
elif self.note_decode_tone == 7:
self.note_ext = "F#"
elif self.note_decode_tone == 8:
self.note_ext = "G"
elif self.note_decode_tone == 9:
self.note_ext = "G#"
elif self.note_decode_tone == 10:
self.note_ext = "A"
elif self.note_decode_tone == 11:
self.note_ext = "A#"
elif self.note_decode_tone == 12:
self.note_ext = "H"
self.ext_shortened = self.ext[40:]
self.pos = 0
for buchstabe in self.ext_shortened:
if buchstabe == "=":
self.pos += 1
break
else:
self.pos += 1
self.timing_exp = self.ext_shortened[self.pos:]
self.__output = self.note_ext
self.__output += str(self.note_decod_oct)
self.__output += f":{self.timing_exp}"
self.__output_list.append(str(self.__output))
except:
pass
elif self.ext[0:8] == "note_off":
self.ext_shortened = self.ext[40:]
self.pos = 0
for buchstabe in self.ext_shortened:
if buchstabe == "=":
self.pos += 1
break
else:
self.pos += 1
self.__output = "R"
self.__output += f":{self.pos}"
self.timing_exp = self.ext_shortened[self.pos:]
self.__output_list.append(self.__output)
else:
pass
self.addToClipboard(str(self.__output_list))

View File

@@ -1 +0,0 @@
/home/janis/Desktop/Water Drops Melodie.mid
1 /home/janis/Desktop/Water Drops Melodie.mid

View File

@@ -10,15 +10,15 @@ TrackChooseScreen:
italic: True italic: True
color: (50, 50, 255, 1) color: (50, 50, 255, 1)
FloatLayout: FloatLayout:
Spinner: MDRaisedButton:
id: track_spinner size: "200dp", "50dp"
pos_hint: {"center_x": 0.5, "center_y": 0.5}
on_release: root.show_dropdown(self)
size_hint: 0.7, 0.2 size_hint: 0.7, 0.2
pos_hint: {"x": 0.15, "y":0.5} pos_hint: {"x": 0.15, "y":0.5}
background_color: (0, 0, 0, 1)
text: "Select a track" text: "Select a track"
values: ["Test"]
Button: Button:
text: "confirm" text: "confirm"
background_color: app.theme_cls.primary_color background_color: app.theme_cls.primary_color
on_release: on_release:
root.extract() root.extract()

99
lib/midi_management.py Normal file
View File

@@ -0,0 +1,99 @@
from mido import MidiFile
import pyperclip as pc
class MidiManagement:
def __init__(self):
pass
def addToClipboard(self, text):
pc.copy(text)
def analyse_track(self, path, trackname):
mid = MidiFile(path, clip=True)
tracks = []
for i, track in enumerate(mid.tracks):
tracks.append('{} (Track {})'.format(track.name, i))
if len(tracks) > 1:
tracks.pop(0)
else:
pass
extracted_track = tracks.pop(0)
tracknumber = 0
while extracted_track != trackname:
extracted_track = tracks.pop(0)
tracknumber += 1
output_list = []
# Track messages
for msg in mid.tracks[tracknumber]:
midi_msg = str(msg)
if midi_msg[0:8] == "note_on ":
try:
msg_loc = midi_msg.index('note=')
note = midi_msg[msg_loc + 5:msg_loc + 7]
note_height = int(note)
note_decod_oct = note_height // 12
note_decode_tone = note_height % 12
note_ext = ""
if note_decode_tone == 1:
note_ext = "C"
elif note_decode_tone == 2:
note_ext = "C#"
elif note_decode_tone == 3:
note_ext = "D"
elif note_decode_tone == 4:
note_ext = "D#"
elif note_decode_tone == 5:
note_ext = "E"
elif note_decode_tone == 6:
note_ext = "F"
elif note_decode_tone == 7:
note_ext = "F#"
elif note_decode_tone == 8:
note_ext = "G"
elif note_decode_tone == 9:
note_ext = "G#"
elif note_decode_tone == 10:
note_ext = "A"
elif note_decode_tone == 11:
note_ext = "A#"
elif note_decode_tone == 12:
note_ext = "H"
ext_shortened = midi_msg[40:]
pos = 0
for buchstabe in ext_shortened:
if buchstabe == "=":
pos += 1
break
else:
pos += 1
timing_exp = ext_shortened[pos:]
output = note_ext
output += str(note_decod_oct)
output += f":{timing_exp}"
output_list.append(str(output))
except:
pass
elif midi_msg[0:8] == "note_off":
ext_shortened = midi_msg[40:]
pos = 0
for buchstabe in ext_shortened:
if buchstabe == "=":
pos += 1
break
else:
pos += 1
output = "R"
output += f":{pos}"
timing_exp = ext_shortened[pos:]
output_list.append(output)
else:
pass
self.addToClipboard(str(output_list))

View File

@@ -6,14 +6,9 @@ mkdir ./midi-micro_bit-converter && cd ./midi-micro_bit-converter
git clone https://github.com/simplePCBuilding/midi-micro-bit_sound-converter git clone https://github.com/simplePCBuilding/midi-micro-bit_sound-converter
pip install kivy[base] pip install kivy[base] kivymd pyperclip mido
pip install kivymd
pip install pyperclip
pip install mido
sudo apt-get install xclip sudo apt-get install xclip xsel
sudo apt-get install xsel
sudo apt-get install wl-clipboard
cd ./midi-micro-bit_sound-converter cd ./midi-micro-bit_sound-converter
python3 midi_converter.py python3 midi_converter.py

View File

@@ -1,5 +1,8 @@
from typing import Optional
from kivy.uix.screenmanager import ScreenManager from kivy.uix.screenmanager import ScreenManager
from kivymd.uix.screen import MDScreen from kivymd.uix.screen import MDScreen
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.screen import MDScreen
from kivy.uix.popup import Popup from kivy.uix.popup import Popup
from kivy.properties import ObjectProperty from kivy.properties import ObjectProperty
from kivy.lang import Builder from kivy.lang import Builder
@@ -7,10 +10,13 @@ from kivy.uix.label import Label
from kivymd.app import MDApp from kivymd.app import MDApp
from mido import MidiFile from mido import MidiFile
import os import os
import backend.midi_management import lib.midi_management
import backend.csv_parsers
import time
global tracks
tracks = []
global filepath
filepath = ''
class HomeScreen(MDScreen): class HomeScreen(MDScreen):
pass pass
@@ -19,25 +25,27 @@ class HomeScreen(MDScreen):
class FileChooserScreen(MDScreen): class FileChooserScreen(MDScreen):
loadfile = ObjectProperty(None) loadfile = ObjectProperty(None)
def load(self, path, filename): def load(self, path, filename):
global filepath
try: try:
self.path = os.path.join(path, filename[0]) self.path = os.path.join(path, filename[0])
try: try:
self.mid = MidiFile(self.path, clip=True) mid = MidiFile(self.path, clip=True)
backend.csv_parsers.CsvWrite().write_str("./backend/temp.csv", [self.path]) filepath = self.path
self.tracks = [] global tracks
for self.track in self.mid.tracks: tracks = []
self.tracks.append(str(self.track)) for i, track in enumerate(mid.tracks):
if len(self.tracks) > 1: tracks.append('{} (Track {})'.format(track.name, i))
self.tracks.pop(0) if len(tracks) > 1:
screen_manager.get_screen("Track").ids.track_spinner.values = self.tracks tracks.pop(0)
screen_manager.current = "Track" screen_manager.current = "Track"
screen_manager.transition.direction = "up" screen_manager.transition.direction = "up"
else: else:
backend.midi_management.MidiManagement().analyse_track(str(self.path), self.tracks.pop(0)) lib.midi_management.MidiManagement().analyse_track(str(self.path), tracks.pop(0))
screen_manager.get_screen("Home").ids.infobox.text = "The command has been copied to the clipboard" screen_manager.get_screen("Home").ids.infobox.text = "The command has been copied to the clipboard"
screen_manager.current = "Home" screen_manager.current = "Home"
screen_manager.transition.direction = "right" screen_manager.transition.direction = "right"
except: except Exception as e:
print(e)
self.popup_fe = Popup(title="FileError", content=Label(text="Please select a MIDI-File!"), self.popup_fe = Popup(title="FileError", content=Label(text="Please select a MIDI-File!"),
size_hint=(0.4, 0.4), auto_dismiss=True) size_hint=(0.4, 0.4), auto_dismiss=True)
self.popup_fe.open() self.popup_fe.open()
@@ -49,16 +57,40 @@ class FileChooserScreen(MDScreen):
class TrackChooseScreen(MDScreen): class TrackChooseScreen(MDScreen):
def extract(self): def show_dropdown(self, button):
self.chosen_track = self.ids.track_spinner.text global tracks
if self.chosen_track == "Select a track": menu_items = []
self.popup_ns = Popup(title="NoSelectionError", content=Label(text="Please select a Track!"), for track in tracks:
menu_items.append(
{
"viewclass": "OneLineListItem",
"text": track,
"on_release": lambda t=track, menu=None : self.extract(t, menu)
}
)
menu = MDDropdownMenu(
caller=button,
items=menu_items,
width_mult=8,
)
# Pass the menu to each item in the lambda callback to dismiss the menu
for item in menu_items:
item["on_release"] = lambda track=item["text"], menu=menu: self.extract(track, menu)
menu.open()
def extract(self, item: str, menu: Optional[MDDropdownMenu] = None):
global filepath
if menu != None:
menu.dismiss()
if item == "Select a track":
popup_ns = Popup(title="NoSelectionError", content=Label(text="Please select a Track!"),
size_hint=(0.4, 0.4), auto_dismiss=True) size_hint=(0.4, 0.4), auto_dismiss=True)
self.popup_ns.open() popup_ns.open()
else: else:
self.path = backend.csv_parsers.CsvRead().importing("./backend/temp.csv").pop(0) lib.midi_management.MidiManagement().analyse_track(str(filepath), item)
self.path_transmit = self.path.pop(0)
backend.midi_management.MidiManagement().analyse_track(str(self.path_transmit), self.chosen_track)
screen_manager.get_screen("Home").ids.infobox.text = "The command has been copied to the clipboard" screen_manager.get_screen("Home").ids.infobox.text = "The command has been copied to the clipboard"
screen_manager.current = "Home" screen_manager.current = "Home"
screen_manager.transition.direction = "right" screen_manager.transition.direction = "right"