diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/README.md b/README.md index 25fa707..dc0928b 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,66 @@ -# midi-micro-bit_sound-converter +
+

Midi to Micro:bit Sound converter

+
+ +
+ + + + +
+ GitHub Repo stars + GitHub watchers + + GitHub forks + GitHub commit activity +
+ GitHub all releases + GitHub release (latest by date) + + +
+ 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. -## INSTALLATION: -Download the files by clicking on code, then on zip. -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: -pip install kivy[base] -pip install kivymd -pip install pyperclip -pip install mido +# Installation +Download the files by clicking on code, then on zip or clone the repo locally using +``` +git clone https://github.com/janishutz/midi-micro-bit_sound-converter +``` -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) -You may do this as follows: -### Linux and MacOS: -Use cd./Path/To/File +Then, run +``` +pip install -r requirements.txt +``` +in the repo's folder (i.e. the folder you just cloned or downloaded and extracted) -### Linux -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 +Alternatively, create a venv using +``` +python -m venv midi-converter +``` -### Windows: -Click on the navigation bar (the one where the path is displayed) and type: cmd +and activate it using +``` +source ./midi-converter/bin/active +``` + +The dependencies of this project are `mido`, `pyperclip`, `kivymd` and `kivy[base]` -### SPECIAL notes for Linux users: -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. -sudo apt-get install xclip -sudo apt-get install xsel -sudo apt-get install wl-clipboard +# Running +Open a terminal in the file location where you saved / cloned this repo to. Type +``` +python midi_converter.py +``` -### OTHER OPTION: -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) +to run the app + + +## 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 diff --git a/backend/csv_parsers.py b/backend/csv_parsers.py deleted file mode 100644 index d1f593c..0000000 --- a/backend/csv_parsers.py +++ /dev/null @@ -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() diff --git a/backend/midi_management.py b/backend/midi_management.py deleted file mode 100644 index 2d1a962..0000000 --- a/backend/midi_management.py +++ /dev/null @@ -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)) - diff --git a/backend/temp.csv b/backend/temp.csv deleted file mode 100644 index d867947..0000000 --- a/backend/temp.csv +++ /dev/null @@ -1 +0,0 @@ -/home/janis/Desktop/Water Drops Melodie.mid diff --git a/dev/hr.py b/dev/hot_reload.py similarity index 100% rename from dev/hr.py rename to dev/hot_reload.py diff --git a/gui/loading_screen.kv b/gui/loading_screen.kv index d6656e0..aadbc86 100644 --- a/gui/loading_screen.kv +++ b/gui/loading_screen.kv @@ -10,15 +10,15 @@ TrackChooseScreen: italic: True color: (50, 50, 255, 1) FloatLayout: - Spinner: - id: track_spinner + MDRaisedButton: + 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 pos_hint: {"x": 0.15, "y":0.5} - background_color: (0, 0, 0, 1) text: "Select a track" - values: ["Test"] Button: text: "confirm" background_color: app.theme_cls.primary_color on_release: - root.extract() \ No newline at end of file + root.extract() diff --git a/lib/midi_management.py b/lib/midi_management.py new file mode 100644 index 0000000..e11a238 --- /dev/null +++ b/lib/midi_management.py @@ -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)) + diff --git a/midi-micro-bit_sound_converter-installer.sh b/midi-micro-bit_sound_converter-installer.sh index 0754076..a2c59a9 100644 --- a/midi-micro-bit_sound_converter-installer.sh +++ b/midi-micro-bit_sound_converter-installer.sh @@ -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 -pip install kivy[base] -pip install kivymd -pip install pyperclip -pip install mido +pip install kivy[base] kivymd pyperclip mido -sudo apt-get install xclip -sudo apt-get install xsel -sudo apt-get install wl-clipboard +sudo apt-get install xclip xsel cd ./midi-micro-bit_sound-converter python3 midi_converter.py diff --git a/midi_converter.py b/midi_converter.py index a21f348..7007bae 100644 --- a/midi_converter.py +++ b/midi_converter.py @@ -1,5 +1,8 @@ +from typing import Optional from kivy.uix.screenmanager import ScreenManager 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.properties import ObjectProperty from kivy.lang import Builder @@ -7,10 +10,13 @@ from kivy.uix.label import Label from kivymd.app import MDApp from mido import MidiFile import os -import backend.midi_management -import backend.csv_parsers -import time +import lib.midi_management +global tracks +tracks = [] + +global filepath +filepath = '' class HomeScreen(MDScreen): pass @@ -19,25 +25,27 @@ class HomeScreen(MDScreen): class FileChooserScreen(MDScreen): loadfile = ObjectProperty(None) def load(self, path, filename): + global filepath try: self.path = os.path.join(path, filename[0]) try: - self.mid = MidiFile(self.path, clip=True) - backend.csv_parsers.CsvWrite().write_str("./backend/temp.csv", [self.path]) - self.tracks = [] - for self.track in self.mid.tracks: - self.tracks.append(str(self.track)) - if len(self.tracks) > 1: - self.tracks.pop(0) - screen_manager.get_screen("Track").ids.track_spinner.values = self.tracks + mid = MidiFile(self.path, clip=True) + filepath = self.path + global tracks + tracks = [] + for i, track in enumerate(mid.tracks): + tracks.append('{} (Track {})'.format(track.name, i)) + if len(tracks) > 1: + tracks.pop(0) screen_manager.current = "Track" screen_manager.transition.direction = "up" 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.current = "Home" 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!"), size_hint=(0.4, 0.4), auto_dismiss=True) self.popup_fe.open() @@ -49,16 +57,40 @@ class FileChooserScreen(MDScreen): class TrackChooseScreen(MDScreen): - def extract(self): - self.chosen_track = self.ids.track_spinner.text - if self.chosen_track == "Select a track": - self.popup_ns = Popup(title="NoSelectionError", content=Label(text="Please select a Track!"), + def show_dropdown(self, button): + global tracks + menu_items = [] + 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) - self.popup_ns.open() + popup_ns.open() else: - self.path = backend.csv_parsers.CsvRead().importing("./backend/temp.csv").pop(0) - self.path_transmit = self.path.pop(0) - backend.midi_management.MidiManagement().analyse_track(str(self.path_transmit), self.chosen_track) + lib.midi_management.MidiManagement().analyse_track(str(filepath), item) screen_manager.get_screen("Home").ids.infobox.text = "The command has been copied to the clipboard" screen_manager.current = "Home" screen_manager.transition.direction = "right"