diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9ae604d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "conda" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/dayly_build.yml b/.github/workflows/dayly_build.yml new file mode 100644 index 0000000..14f6d5a --- /dev/null +++ b/.github/workflows/dayly_build.yml @@ -0,0 +1,41 @@ +name: Dayly Build + +on: + pull_request: + push: + branches: + - dev + - main + +jobs: + build-linux: + runs-on: ubuntu-latest + strategy: + max-parallel: 5 + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.11 + uses: actions/setup-python@v3 + with: + python-version: "3.11" + - name: Add conda to system path + run: | + # $CONDA is an environment variable pointing to the root of the miniconda directory + echo $CONDA/bin >> $GITHUB_PATH + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y portaudio19-dev + - name: Install dependencies + run: | + conda env update --file environment.yml --name base + - name: Build wheel + run: | + conda install wheel + python setup.py bdist_wheel + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 51cf72b..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Build with Nuitka - -on: - push: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install nuitka - pip install -r requirements.txt - - - name: Compile Python script with Nuitka - run: | - python -m nuitka --follow-imports main.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6926102..69d41ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ Please follow these steps to have your contribution considered by the maintainer 1. **Fork** the repository and create your branch from `dev`. 2. **If you've added code** that should be tested, add tests. -3. **If you've changed APIs**, update the documentation. +3. **If you’ve modified any modules or files**, update the documentation accordingly. 4. **Ensure the test suite passes**. 5. **Make sure your code lints** using `ms-python.autopep8` from VSCode. 6. **Issue that pull request**! @@ -60,8 +60,8 @@ Please follow these steps to have your contribution considered by the maintainer ### Python Style Guide -- Use PEP 8 style guide. -- Use `ms-python.autopep8` from VSCode for formatting. +- Use The Black code style. +- Use `ms-python.black-formatter` from VSCode for formatting. ### Clean Code and Documentation diff --git a/README.md b/README.md index b0b01c1..a0b83d9 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ conda activate ai_synth Once the project setup is complete, you can initiate it by executing the `main.py` file in the environment: ```bash -python main.py +python -m pythonaisynth.main ``` ![window](docs/img/main_window_v3.png) diff --git a/_version.py b/_version.py deleted file mode 100644 index 5918abd..0000000 --- a/_version.py +++ /dev/null @@ -1 +0,0 @@ -version = '3.0.0' diff --git a/audio_stuff/midi/midi_piano.py b/audio_stuff/midi/midi_piano.py index 9652811..7fe1e54 100644 --- a/audio_stuff/midi/midi_piano.py +++ b/audio_stuff/midi/midi_piano.py @@ -16,22 +16,23 @@ def changePort(event): global port + port.panic() port.close() port = mido.open_output(selected_port.get()) # Create a dropdown menu to select the MIDI port -port_menu = tk.OptionMenu(window, selected_port, * - mido.get_output_names(), command=changePort) +port_menu = tk.OptionMenu( + window, selected_port, *mido.get_output_names(), command=changePort +) port_menu.pack() # Create a variable to store the selected MIDI channel selected_channel = tk.StringVar(window) -selected_channel.set('1') # default value +selected_channel.set("1") # default value # Create a dropdown menu to select the MIDI channel -channel_menu = tk.OptionMenu( - window, selected_channel, *[str(i) for i in range(1, 17)]) +channel_menu = tk.OptionMenu(window, selected_channel, *[str(i) for i in range(1, 17)]) channel_menu.pack() # Create a variable to store the selected octave @@ -39,29 +40,31 @@ def changePort(event): selected_octave.set(4) # default value # Create a slider to select the octave -octave_slider = tk.Scale(window, from_=0, to=8, orient='horizontal', - variable=selected_octave, label='Octave') +octave_slider = tk.Scale( + window, from_=0, to=8, orient="horizontal", variable=selected_octave, label="Octave" +) octave_slider.pack() # Function to send MIDI messages -note_states = {i: False for i in range(octaves_to_display*12)} +note_states = {i: False for i in range(octaves_to_display * 12)} -def send_midi_note(note, velocity=127, type='note_on'): +def send_midi_note(note, velocity=127, type="note_on"): global note_states channel = int(selected_channel.get()) - 1 - msg = mido.Message(type, note=note + selected_octave.get() - * 12, velocity=velocity, channel=channel) - if type == 'note_on': + msg = mido.Message( + type, note=note + selected_octave.get() * 12, velocity=velocity, channel=channel + ) + if type == "note_on": # If the note is already on, don't send another note_on message if note_states[note]: return else: note_states[note] = True port.send(msg) - elif type == 'note_off': + elif type == "note_off": note_states[note] = False port.send(msg) # window.after(1*10**3, lambda msg=msg: port.send(msg)) @@ -79,60 +82,86 @@ def send_midi_note(note, velocity=127, type='note_on'): black_key_note = ["C#", "D#", "F#", "G#", "A#"] white_keys_bindings_per_octaves = [ - ['a', 's', 'd', 'f', 'g', 'h', 'j'], ['k', 'l', ';', "'", 'z', 'x', 'c']] -black_keys_bindings_per_octaves = [ - ['w', 'e', 'r', 't', 'y'], ['u', 'i', 'o', 'p', '[']] + ["a", "s", "d", "f", "g", "h", "j"], + ["k", "l", ";", "'", "z", "x", "c"], +] +black_keys_bindings_per_octaves = [["w", "e", "r", "t", "y"], ["u", "i", "o", "p", "["]] current_locale = locale.getlocale() # If the current locale is German, adjust the key bindings if "de_DE" in current_locale: - white_keys_bindings_per_octaves = [['a', 's', 'd', 'f', 'g', 'h', 'j'], [ - 'k', 'l', 'odiaeresis', 'adiaeresis', 'y', 'x', 'c']] + white_keys_bindings_per_octaves = [ + ["a", "s", "d", "f", "g", "h", "j"], + ["k", "l", "odiaeresis", "adiaeresis", "y", "x", "c"], + ] black_keys_bindings_per_octaves = [ - ['w', 'e', 'r', 't', 'z'], ['u', 'i', 'o', 'p', 'udiaeresis']] + ["w", "e", "r", "t", "z"], + ["u", "i", "o", "p", "udiaeresis"], + ] for a in range(octaves_to_display): - if a < len(white_keys_bindings_per_octaves) or a < len(black_keys_bindings_per_octaves): + if a < len(white_keys_bindings_per_octaves) or a < len( + black_keys_bindings_per_octaves + ): white_keys_bindings = white_keys_bindings_per_octaves[a] black_keys_bindings = black_keys_bindings_per_octaves[a] else: - white_keys_bindings = [" "]*len(white_key_note) - black_keys_bindings = [" "]*len(black_key_note) + white_keys_bindings = [" "] * len(white_key_note) + black_keys_bindings = [" "] * len(black_key_note) for i in range(7): button = tk.Button( - keys_frame, text=f'{white_key_note[i]}\n|{white_keys_bindings[i].replace("odiaeresis", "ö").replace("adiaeresis", "ä")}|', bg='white', width=2, height=6) - button.grid(row=0, column=white_keys[i]+a*12) - - def press(event, i=i, a=a): return send_midi_note( - white_keys[i]+a*12, type='note_on') - - def release(event, i=i, a=a): return send_midi_note( - white_keys[i]+a*12, type='note_off') - button.bind('', press) - button.bind('', release) + keys_frame, + text=f'{white_key_note[i]}\n|{white_keys_bindings[i].replace("odiaeresis", "ö").replace("adiaeresis", "ä")}|', + bg="white", + width=2, + height=6, + ) + button.grid(row=0, column=white_keys[i] + a * 12) + + def press(event, button=button, i=i, a=a): + button.config(relief="sunken") + return send_midi_note(white_keys[i] + a * 12, type="note_on") + + def release(event, button=button, i=i, a=a): + button.config(relief="raised") + return send_midi_note(white_keys[i] + a * 12, type="note_off") + + button.bind("", press) + button.bind("", release) # Bind the button to a key press event if white_keys_bindings[i] != " ": - window.bind('' % white_keys_bindings[i], press) - window.bind('' % white_keys_bindings[i], release) + window.bind("" % white_keys_bindings[i], press) + window.bind("" % white_keys_bindings[i], release) for i in range(5): button = tk.Button( - keys_frame, text=f'{black_key_note[i]}\n|{black_keys_bindings[i].replace("udiaeresis", "ü")}|', fg='white', bg='black', width=1, height=4) - button.grid(row=0, column=black_keys[i]+a*12, sticky='n') - - def press(event, i=i, a=a): return send_midi_note( - black_keys[i]+a*12, type='note_on') - - def release(event, i=i, a=a): return send_midi_note( - black_keys[i]+a*12, type='note_off') - button.bind('', press) - button.bind('', release) + keys_frame, + text=f'{black_key_note[i]}\n|{black_keys_bindings[i].replace("udiaeresis", "ü")}|', + fg="white", + bg="black", + width=1, + height=4, + ) + button.grid(row=0, column=black_keys[i] + a * 12, sticky="n") + + def press(event, button=button, i=i, a=a): + button.config(relief="sunken") + return send_midi_note(black_keys[i] + a * 12, type="note_on") + + def release(event, button=button, i=i, a=a): + button.config(relief="raised") + return send_midi_note(black_keys[i] + a * 12, type="note_off") + + button.bind("", press) + button.bind("", release) # Bind the button to a key press event if white_keys_bindings[i] != " ": - window.bind('' % black_keys_bindings[i], press) - window.bind('' % black_keys_bindings[i], release) + window.bind("" % black_keys_bindings[i], press) + window.bind("" % black_keys_bindings[i], release) # Run the Tkinter event loop window.mainloop() + +port.panic() diff --git a/audio_stuff/midi/mido_test_in.py b/audio_stuff/midi/mido_test_in.py index 48a2861..50ac97b 100644 --- a/audio_stuff/midi/mido_test_in.py +++ b/audio_stuff/midi/mido_test_in.py @@ -3,12 +3,14 @@ def listen_midi(): # Open a virtual port for input - midiin = mido.open_input('TEST', virtual=True) - - while True: - # Wait for a message and print it - msg = midiin.receive() - print(msg) + default_name = mido.get_input_names()[0] + with mido.open_input(default_name) as midi_in: # 'Digital Piano 2') + i = 0 + while True: + for msg in midi_in.iter_pending(): + print() + # print(i) + # i += 1 if __name__ == "__main__": diff --git a/audio_stuff/midi/pygame_find_channel.py b/audio_stuff/midi/pygame_find_channel.py new file mode 100644 index 0000000..2e8d405 --- /dev/null +++ b/audio_stuff/midi/pygame_find_channel.py @@ -0,0 +1,26 @@ +import pygame.midi + +pygame.midi.init() + + +for i in range(pygame.midi.get_count()): + info = pygame.midi.get_device_info(i) + print( + f"Device ID: {i}, Name: {info[1].decode()}, Input: {info[2]}, Output: {info[3]}") + + +desired_name = "Your MIDI Device Name" +device_id = None + +for i in range(pygame.midi.get_count()): + info = pygame.midi.get_device_info(i) + print(info) + if info[1].decode() == desired_name: + device_id = i + break + +if device_id is not None: + midi_output = pygame.midi.Output(device_id) + print(f"Selected device ID: {device_id}") +else: + print("Device not found") diff --git a/audio_stuff/midi/stream_file.py b/audio_stuff/midi/stream_file.py new file mode 100644 index 0000000..e25247e --- /dev/null +++ b/audio_stuff/midi/stream_file.py @@ -0,0 +1,28 @@ +import time +import mido +from mido import MidiFile, MidiTrack, Message + + +def play_channel(midi_file_path, channel_to_play): + midi_file = MidiFile(midi_file_path) + output = mido.open_output('LoopBe Internal MIDI 1') + + start_time = time.time() + for track in midi_file.tracks: + current_time = start_time + for msg in track: + if not msg.is_meta and not msg.type == 'sysex': + if msg.channel == channel_to_play: + # Convert time from milliseconds to seconds + time.sleep(msg.time / 1000.0) + print(msg) + mido.Message(msg.type, note=msg.note, + velocity=msg.velocity, channel=0) + output.send(msg) + current_time += msg.time / 1000.0 + + +if __name__ == "__main__": + midi_file_path = str(input("input filename: ")).strip() + channel_to_play = int(input("Enter the channel number to play (0-15): ")) + play_channel(midi_file_path, channel_to_play) diff --git a/docs/how_to_use/basics.md b/docs/how_to_use/basics.md new file mode 100644 index 0000000..57d4189 --- /dev/null +++ b/docs/how_to_use/basics.md @@ -0,0 +1,52 @@ +# Basics + +## Soundwave + +### Drawing + +You can draw the Shape of the Soundwave inside the big Plot. +The Dots are the individual points of the Line you draw. + +![plot_to_draw_in](../img/plot_to_draw_in.png) + +### Predefined Functions + +There are already some Waveforms predefined. +You can select them in the Select Box labeled **Predefined Functions** + +![predefined_functions](../img/predefined_functions.png) + +![example_function](../img/example_function.png) + +> This is one of the predefined functions, its called **nice**, cause i find it sounds nice and i had no better name, if you have a +> problem with this, find a better name! + +## Training + +Once you have drawn the Shape of the Soundwave you Start the trainings Process by Clicking on the **Start Training** Button. + +![start_training_button](../img/start_training_button.png) + +The Trainings Process gets initialized and after some time a Red Line appears in the Plot, this it the current output of the model. + +![while_training](../img/while_training.png) + +## Making Music + +> Images need to be Edited to Highlight GUI Elements + +To make Music using this Synth, you need to have a valid Midi Input Port. +You can select a Midi Port at the Select Box labeld **MIDI PORT**. + +![midi_port](../img/midi_port.png) + +As soon as the the training Process finishes and you selected the right Midi Port you can start the Synth by pressing the Play Button. + +![play](../img/start_synth.png) + +Once the synth is ready you hear a Sound. Now you can use it. + + +## Midi piano + +In case you have no midi piano or similar device that can send Midi Signals to the Synth we included a simple Piano programm where you can select a Midi Port and play musik with it. \ No newline at end of file diff --git a/docs/how_to_use/model_params.md b/docs/how_to_use/model_params.md new file mode 100644 index 0000000..bd88ef9 --- /dev/null +++ b/docs/how_to_use/model_params.md @@ -0,0 +1 @@ +> Under Construction diff --git a/docs/idea.md b/docs/idea.md index 828a439..7568028 100644 --- a/docs/idea.md +++ b/docs/idea.md @@ -1,5 +1,7 @@ # My Idea about this programm +> This was my Plan at the Beginning + I want to create a Python-Application where you draw a function and a neural-net tries to aproximate it to make sound from it. ## GUI Desing @@ -21,6 +23,8 @@ $newpoint=\frac{(old\_point_1+old\_point_2)}{2}$ or by duplicating points and ad ## Future Plans for My Synth Project +> These plans will not be considered for implementation in the near Future. + Here are some enhancements I'm considering for my synth project: 1. **Frequency to MIDI Mapping**: I'm thinking of mapping the dominant frequency of my model to a MIDI note. This could result in cleaner sounds, as MIDI notes correspond to specific frequencies. diff --git a/docs/img/example_function.png b/docs/img/example_function.png new file mode 100644 index 0000000..5c83560 Binary files /dev/null and b/docs/img/example_function.png differ diff --git a/docs/img/midi_port.png b/docs/img/midi_port.png new file mode 100644 index 0000000..901dea3 Binary files /dev/null and b/docs/img/midi_port.png differ diff --git a/docs/img/plot_to_draw_in.png b/docs/img/plot_to_draw_in.png new file mode 100644 index 0000000..b2c7151 Binary files /dev/null and b/docs/img/plot_to_draw_in.png differ diff --git a/docs/img/predefined_functions.png b/docs/img/predefined_functions.png new file mode 100644 index 0000000..f1ad7ce Binary files /dev/null and b/docs/img/predefined_functions.png differ diff --git a/docs/img/start_synth.png b/docs/img/start_synth.png new file mode 100644 index 0000000..901dea3 Binary files /dev/null and b/docs/img/start_synth.png differ diff --git a/docs/img/start_training_button.png b/docs/img/start_training_button.png new file mode 100644 index 0000000..4e87349 Binary files /dev/null and b/docs/img/start_training_button.png differ diff --git a/docs/img/while_training.png b/docs/img/while_training.png new file mode 100644 index 0000000..767ef43 Binary files /dev/null and b/docs/img/while_training.png differ diff --git a/docs/index.md b/docs/index.md index 5c8e557..8cb5904 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,3 +2,11 @@ > Documentation is not complete (99.99% incomplete). > maybe you should write the Documentation. + +This is how The App Looks + +![window](./img/main_window_v3.png) + +## General + +PythonAISynth is a software designed to create music using a neural network. The network uses a Fourier-Series based Regression model to approximate a curve on a Plot that a user can draw on. the model is then used to generate sound with higher sample rate than the drawing. diff --git a/docs/javascripts/katex.js b/docs/javascripts/katex.js new file mode 100644 index 0000000..a9417bf --- /dev/null +++ b/docs/javascripts/katex.js @@ -0,0 +1,10 @@ +document$.subscribe(({ body }) => { + renderMathInElement(body, { + delimiters: [ + { left: "$$", right: "$$", display: true }, + { left: "$", right: "$", display: false }, + { left: "\\(", right: "\\)", display: false }, + { left: "\\[", right: "\\]", display: true } + ], + }) +}) \ No newline at end of file diff --git a/scr/__init__.py b/docs/python_modules/fourier_neural_network.md similarity index 100% rename from scr/__init__.py rename to docs/python_modules/fourier_neural_network.md diff --git a/docs/python_modules/fourier_neural_network_gui.md b/docs/python_modules/fourier_neural_network_gui.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/python_modules/graph_canvas.md b/docs/python_modules/graph_canvas.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/python_modules/graph_canvas_v2.md b/docs/python_modules/graph_canvas_v2.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/python_modules/main.md b/docs/python_modules/main.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/python_modules/music.md b/docs/python_modules/music.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/python_modules/overview.md b/docs/python_modules/overview.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/python_modules/predefined_functions.md b/docs/python_modules/predefined_functions.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/python_modules/simple_input_dialog.md b/docs/python_modules/simple_input_dialog.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/python_modules/std_redirect.md b/docs/python_modules/std_redirect.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/python_modules/synth_gui.md b/docs/python_modules/synth_gui.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/python_modules/utils.md b/docs/python_modules/utils.md new file mode 100644 index 0000000..e69de29 diff --git a/environment.yml b/environment.yml index 9cc0fbb..963169d 100644 --- a/environment.yml +++ b/environment.yml @@ -1,16 +1,104 @@ channels: - defaults dependencies: - - python==3.11 - - pip + - bzip2=1.0.8 + - ca-certificates=2024.9.24 + - flake8=7.1.1 + - libffi=3.4.4 + - mccabe=0.7.0 + - openssl=1.1.1w + - pip=24.2 + - pycodestyle=2.12.1 + - pyflakes=3.2.0 + - python=3.11.0 + - setuptools=75.1.0 + - sqlite=3.45.3 + - tk=8.6.14 + - tzdata=2024a + - wheel=0.44.0 + - xz=5.4.6 + - zlib=1.2.13 - pip: - - numpy - - pygame - - scipy - - torch - - psutil - - numba - - pretty_midi - - sounddevice - - pyaudio - - matplotlib + - argparse==1.4.0 + - attrs==24.2.0 + - automat==24.8.1 + - babel==2.16.0 + - buildtools==1.0.6 + - certifi==2024.8.30 + - cffi==1.17.1 + - charset-normalizer==3.3.2 + - click==8.1.7 + - colorama==0.4.6 + - constantly==23.10.4 + - contourpy==1.3.0 + - cycler==0.12.1 + - cython==3.0.11 + - docopt==0.6.2 + - filelock==3.16.1 + - fonttools==4.54.0 + - fsspec==2024.9.0 + - furl==2.1.3 + - ghp-import==2.1.0 + - gitdb==4.0.11 + - gitpython==3.1.43 + - greenlet==3.1.1 + - hyperlink==21.0.0 + - idna==3.10 + - incremental==24.7.2 + - jinja2==3.1.4 + - keras==3.5.0 + - kiwisolver==1.4.7 + - llvmlite==0.43.0 + - markupsafe==2.1.5 + - matplotlib==3.9.2 + - mergedeep==1.3.4 + - mido==1.3.2 + - mkdocs==1.6.1 + - mkdocs-autorefs==1.2.0 + - mkdocs-get-deps==0.2.0 + - mkdocs-git-authors-plugin==0.9.0 + - mkdocs-git-revision-date-localized-plugin==1.2.9 + - mkdocs-material==9.5.39 + - mkdocs-material-extensions==1.3.1 + - mkdocstrings==0.26.1 + - mpmath==1.3.0 + - networkx==3.3 + - nuitka==2.4.8 + - numba==0.60.0 + - numpy==2.0.2 + - ordered-set==4.1.0 + - orderedmultidict==1.0.1 + - packaging==23.2 + - paginate==0.5.7 + - pathspec==0.12.1 + - pillow==10.4.0 + - platformdirs==4.3.6 + - psutil==6.0.0 + - pyaudio==0.2.14 + - pycparser==2.22 + - pygame==2.6.0 + - pygments==2.18.0 + - pymdown-extensions==10.11.2 + - pyparsing==3.1.4 + - python-dateutil==2.9.0.post0 + - python-rtmidi==1.5.8 + - pytz==2024.2 + - pyyaml==6.0.2 + - pyyaml-env-tag==0.1 + - redo==3.0.0 + - regex==2024.9.11 + - requests==2.32.3 + - scipy==1.14.1 + - simplejson==3.19.3 + - six==1.16.0 + - smmap==5.0.1 + - sounddevice==0.5.0 + - sqlalchemy==2.0.35 + - sympy==1.13.3 + - torch==2.4.1 + - torchvision==0.19.1 + - twisted==24.7.0 + - typing-extensions==4.12.2 + - urllib3==2.2.3 + - zope-interface==7.0.3 + - zstandard==0.23.0 diff --git a/mkdocs.yml b/mkdocs.yml index 11f40b1..0b2ab17 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,25 @@ site_name: Python AI Synthesizer repo_url: https://github.com/thetechnicker/PythonAISynth + nav: - Home: index.md - - Idea: idea.md + - Initial Concept: idea.md + - How to Use: + - Basics: how_to_use/basics.md + - Adjusting The Model: how_to_use/model_params.md + - Python Modules: + - Overview: python_modules/overview.md + - Main: python_modules/main.md + - Graph Canvas: python_modules/graph_canvas_v2.md + - Fourier Neural Network Core: python_modules/fourier_neural_network.md + - Fourier Neural Network GUI: python_modules/fourier_neural_network_gui.md + - Music Synthesis: python_modules/music.md + - Predefined Functions: python_modules/predefined_functions.md + - Input Dialog: python_modules/simple_input_dialog.md + - Standard Output Redirect: python_modules/std_redirect.md + - Synthesizer GUI: python_modules/synth_gui.md + - Utility Functions: python_modules/utils.md + theme: icon: repo: fontawesome/brands/github @@ -29,6 +46,11 @@ theme: toggle: icon: material/brightness-4 name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - navigation.instant + - navigation.instant.progress extra: consent: @@ -38,11 +60,17 @@ extra: as to measure the effectiveness of our documentation and whether users find what they're searching for. With your consent, you're helping us to make our documentation better. + plugins: - git-revision-date-localized: enable_creation_date: true - repository: thetechnicker/PythonAISynth - git-authors + - search + - mkdocstrings: + handlers: + python: + options: + docstring_style: google markdown_extensions: - abbr @@ -50,3 +78,13 @@ markdown_extensions: - attr_list - def_list - md_in_html + - pymdownx.arithmatex: + generic: true + +extra_javascript: + - javascripts/katex.js + - https://unpkg.com/katex@0/dist/katex.min.js + - https://unpkg.com/katex@0/dist/contrib/auto-render.min.js + +extra_css: + - https://unpkg.com/katex@0/dist/katex.min.css diff --git a/pythonaisynth/__init__.py b/pythonaisynth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pythonaisynth/__main__.py b/pythonaisynth/__main__.py new file mode 100644 index 0000000..40e2b01 --- /dev/null +++ b/pythonaisynth/__main__.py @@ -0,0 +1,4 @@ +from .main import main + +if __name__ == "__main__": + main() diff --git a/pythonaisynth/_version.py b/pythonaisynth/_version.py new file mode 100644 index 0000000..df1fd0c --- /dev/null +++ b/pythonaisynth/_version.py @@ -0,0 +1 @@ +version = "3.2.0" diff --git a/scr/fourier_neural_network.py b/pythonaisynth/fourier_neural_network.py similarity index 66% rename from scr/fourier_neural_network.py rename to pythonaisynth/fourier_neural_network.py index 4291639..3b036ee 100644 --- a/scr/fourier_neural_network.py +++ b/pythonaisynth/fourier_neural_network.py @@ -1,6 +1,7 @@ import os import sys import time +import traceback import numpy as np import torch import torch.nn as nn @@ -8,24 +9,24 @@ from typing import Tuple from multiprocessing import Queue -from scr import utils -from scr.utils import QueueSTD_OUT, linear_interpolation, midi_to_freq +from pythonaisynth import utils +from .utils import QueueSTD_OUT, linear_interpolation, midi_to_freq -DISABLE_GPU = False try: import torch_directml + DIRECTML = True except ImportError: DIRECTML = False -DISABLE_GPU = True +DISABLE_GPU = False class FourierLayer(nn.Module): def __init__(self, fourier_degree): super(FourierLayer, self).__init__() - self.tensor = torch.arange(1, fourier_degree+1) + self.tensor = torch.arange(1, fourier_degree + 1) def forward(self, x): self.tensor = self.tensor.to(x.device) @@ -37,8 +38,7 @@ def forward(self, x): class FourierRegresionModel(nn.Module): def __init__(self, degree): super(FourierRegresionModel, self).__init__() - self.frequencies = torch.arange( - 1, degree+1, 1, dtype=torch.float32) + self.frequencies = torch.arange(1, degree + 1, 1, dtype=torch.float32) self.a1 = nn.Parameter(torch.ones((degree))) self.a2 = nn.Parameter(torch.ones((degree))) self.c = nn.Parameter(torch.zeros(1)) @@ -46,43 +46,23 @@ def __init__(self, degree): def forward(self, x): y1 = self.a1 * torch.sin(self.frequencies * x) y2 = self.a2 * torch.cos(self.frequencies * x) - z = torch.sum(y1, dim=-1)+torch.sum(y2, dim=-1) + self.c + z = torch.sum(y1, dim=-1) + torch.sum(y2, dim=-1) + self.c return z - -class FourierRegresionModelV2(nn.Module): - def __init__(self, degree): - super(FourierRegresionModelV2, self).__init__() - self.frequencies_sin = nn.Parameter( - torch.arange(1, degree+1, 1, dtype=torch.float32), - requires_grad=False - ) - self.frequencies_cos = nn.Parameter( - torch.arange(1, degree+1, 1, dtype=torch.float32), - requires_grad=False - ) - self.a1 = nn.Parameter(torch.ones((degree))) - self.a2 = nn.Parameter(torch.ones((degree))) - self.c1 = nn.Parameter(torch.zeros((degree))) - self.c2 = nn.Parameter(torch.zeros((degree))) - self.d = nn.Parameter(torch.zeros(1)) - - def forward(self, x): - y1 = self.a1 * torch.sin(self.frequencies_sin * x) # +self.c1) - y2 = self.a2 * torch.cos(self.frequencies_cos * x) # +self.c2) - z = torch.sum(y1, dim=-1)+torch.sum(y2, dim=-1) # +self.d - return z # , y1, y2 + def to(self, device): + self.frequencies = self.frequencies.to(device) + return super(FourierRegresionModel, self).to(device) -class FourierNN(): +class FourierNN: SAMPLES = 1000 EPOCHS = 1000 DEFAULT_FORIER_DEGREE = 100 FORIER_DEGREE_DIVIDER = 1 FORIER_DEGREE_OFFSET = 0 PATIENCE = 100 - OPTIMIZER = 'SGD' - LOSS_FUNCTION = 'HuberLoss' + OPTIMIZER = "SGD" + LOSS_FUNCTION = "HuberLoss" CALC_FOURIER_DEGREE_BY_DATA_LENGTH = False def __init__(self, lock, data=None, stdout_queue=None): @@ -96,14 +76,16 @@ def __init__(self, lock, data=None, stdout_queue=None): self.stdout_queue = stdout_queue self.device = None if DIRECTML and not DISABLE_GPU: - self.device = torch_directml.device() if torch_directml.is_available() else None + self.device = ( + torch_directml.device() if torch_directml.is_available() else None + ) if not self.device: if torch.cuda.is_available() and torch.version.hip and not DISABLE_GPU: - self.device = torch.device('rocm') + self.device = torch.device("rocm") elif torch.cuda.is_available() and not DISABLE_GPU: - self.device = torch.device('cuda') + self.device = torch.device("cuda") else: - self.device = torch.device('cpu') + self.device = torch.device("cpu") print(self.device) if data is not None: self.update_data(data) @@ -118,8 +100,7 @@ def update_attribs(self, **kwargs): if hasattr(self, key): setattr(self, key, val) else: - raise ValueError( - f"Parameter '{key}' does not exist in the model.") + raise ValueError(f"Parameter '{key}' does not exist in the model.") # If the data has been prepared, recreate the model with updated parameters if self.prepared_data: @@ -137,15 +118,19 @@ def create_model(self): def update_data(self, data, stdout_queue=None): if stdout_queue: self.stdout_queue = stdout_queue - self.prepared_data = self.prepare_data( - list(data), std_queue=self.stdout_queue) + self.prepared_data = self.prepare_data(list(data), std_queue=self.stdout_queue) if not self.current_model: self.current_model = self.create_model() def update_fourier_degree(self): - self.fourier_degree = (( - (self.orig_data_len // self.FORIER_DEGREE_DIVIDER) + self.FORIER_DEGREE_OFFSET) - if self.CALC_FOURIER_DEGREE_BY_DATA_LENGTH else self.DEFAULT_FORIER_DEGREE) + self.fourier_degree = ( + ( + (self.orig_data_len // self.FORIER_DEGREE_DIVIDER) + + self.FORIER_DEGREE_OFFSET + ) + if self.CALC_FOURIER_DEGREE_BY_DATA_LENGTH + else self.DEFAULT_FORIER_DEGREE + ) def prepare_data(self, data, std_queue: Queue = None): if std_queue: @@ -155,16 +140,15 @@ def prepare_data(self, data, std_queue: Queue = None): self.update_fourier_degree() - x_train, y_train = linear_interpolation( - data, self.SAMPLES) # , self.device) - x_test, y_test = linear_interpolation( - data, self.SAMPLES//2) # , self.device) + x_train, y_train = linear_interpolation(data, self.SAMPLES) # , self.device) + x_test, y_test = linear_interpolation(data, self.SAMPLES // 2) # , self.device) - return (x_train.unsqueeze(1), - y_train, - x_test.unsqueeze(1), - y_test, - ) + return ( + x_train.unsqueeze(1), + y_train, + x_test.unsqueeze(1), + y_test, + ) def train(self, test_data, queue=None, quiet=False, stdout_queue=None): # exit(-1) @@ -172,17 +156,20 @@ def train(self, test_data, queue=None, quiet=False, stdout_queue=None): if stdout_queue: sys.stdout = QueueSTD_OUT(stdout_queue) - print(self.OPTIMIZER, - self.LOSS_FUNCTION, - self.fourier_degree, - self.PATIENCE, - sep="\n") + print( + self.OPTIMIZER, + self.LOSS_FUNCTION, + self.fourier_degree, + self.PATIENCE, + sep="\n", + ) x_train_transformed, y_train, test_x, test_y = self.prepared_data model = self.current_model.to(self.device) for param in self.current_model.parameters(): param.requires_grad = True + model.to(self.device) model.train() x_train_transformed = x_train_transformed.to(self.device) @@ -191,27 +178,29 @@ def train(self, test_data, queue=None, quiet=False, stdout_queue=None): test_y = test_y.to(self.device) optimizer = utils.get_optimizer( - optimizer_name=self.OPTIMIZER, - model_parameters=model.parameters(), - lr=0.1) + optimizer_name=self.OPTIMIZER, model_parameters=model.parameters(), lr=0.1 + ) criterion = utils.get_loss_function(self.LOSS_FUNCTION) train_dataset = TensorDataset(x_train_transformed, y_train) train_loader = DataLoader( - train_dataset, batch_size=int(self.SAMPLES / 2), shuffle=True) + train_dataset, batch_size=int(self.SAMPLES / 2), shuffle=True + ) # prepared_test_data = torch.tensor( # data=FourierNN.fourier_basis_numba( # data=test_data.flatten(), # indices=FourierNN.precompute_indices(self.fourier_degree)), # dtype=torch.float32).to(self.device) - prepared_test_data = torch.tensor( - test_data.flatten(), - dtype=torch.float32, device=self.device).unsqueeze(1) + prepared_test_data = ( + torch.tensor(test_data.flatten(), dtype=torch.float32, device=self.device) + .unsqueeze(1) + .to(self.device) + ) - min_delta = 0.0001 # 4.337714676382401e-14 + min_delta = 0.001 # 4.337714676382401e-14 epoch_without_change = 0 - max_loss = torch.inf + min_loss = torch.inf for epoch in range(self.EPOCHS): # callback.on_epoch_begin(epoch) @@ -221,8 +210,7 @@ def train(self, test_data, queue=None, quiet=False, stdout_queue=None): epoch_loss = 0 try: for batch_x, batch_y in train_loader: - batch_x, batch_y = batch_x.to( - self.device), batch_y.to(self.device) + batch_x, batch_y = batch_x.to(self.device), batch_y.to(self.device) optimizer.zero_grad() outputs = model(batch_x) loss = criterion(outputs, batch_y) @@ -230,6 +218,7 @@ def train(self, test_data, queue=None, quiet=False, stdout_queue=None): optimizer.step() epoch_loss += loss.item() except Exception as e: + # traceback.print_exc() print(f"Exception while training: {e}") exit(1) epoch_loss /= len(train_loader) @@ -238,21 +227,22 @@ def train(self, test_data, queue=None, quiet=False, stdout_queue=None): model.eval() with torch.no_grad(): val_outputs = model(test_x) - val_loss = criterion(val_outputs, - test_y.to(self.device)) + val_loss = criterion(val_outputs, test_y.to(self.device)) if queue and epoch % 10 == 0: predictions = model(prepared_test_data).cpu().numpy() queue.put(predictions) # callback.on_epoch_end(epoch, epoch_loss, val_loss.item(), model) time_taken = time.perf_counter_ns() - timestamp - print(f"epoch {epoch+1} ends. " - f"loss: {epoch_loss:3.5f}, " - f"val_loss: {val_loss:3.5f}. " - f"Time Taken: {time_taken / 1_000_000_000}s") - - if val_loss < max_loss-min_delta: - max_loss = epoch_loss + print( + f"epoch {epoch+1} ends. " + f"loss: {epoch_loss:3.5f}, " + f"val_loss: {val_loss:3.5f}. " + f"Time Taken: {time_taken / 1_000_000_000}s" + ) + + if abs(val_loss) < min_loss - min_delta: + min_loss = abs(val_loss) epoch_without_change = 0 else: epoch_without_change += 1 @@ -272,15 +262,18 @@ def predict(self, data): if x.shape[0] > 1: x = x.unsqueeze(1) print(x.shape) + model = self.current_model + model.to(self.device) with torch.no_grad(): - y = self.current_model(x) + y = model(x) + self.current_model = self.current_model.to("cpu") return y.cpu().numpy() - def save_model(self, filename='./tmp/model.pth'): + def save_model(self, filename="./tmp/model.pth"): torch.save(self.current_model.state_dict(), filename) - def load_new_model_from_file(self, filename='./tmp/model.pth', *, delete_tmp=False): + def load_new_model_from_file(self, filename="./tmp/model.pth", *, delete_tmp=False): self.current_model = self.create_model() model = torch.load(filename, weights_only=False) if delete_tmp: diff --git a/pythonaisynth/fourier_neural_network_gui.py b/pythonaisynth/fourier_neural_network_gui.py new file mode 100644 index 0000000..263ebbd --- /dev/null +++ b/pythonaisynth/fourier_neural_network_gui.py @@ -0,0 +1,157 @@ +import torch.optim as optim +import torch.nn as nn +import tkinter as tk +from tkinter import ttk + + +class NeuralNetworkGUI(ttk.Frame): + def __init__(self, parent=None, defaults: dict = None, callback=None, **kwargs): + ttk.Frame.__init__(self, parent, **kwargs) + self.on_change_callback = callback + self.columnconfigure(0, weight=1) + self.columnconfigure(1, weight=1) + + # Define the list of loss functions and optimizers + self.loss_functions = [ + func + for func in dir(nn) + if not func.startswith("_") + and isinstance(getattr(nn, func), type) + and issubclass(getattr(nn, func), nn.modules.loss._Loss) + ] + self.loss_functions.append("CustomHuberLoss") + self.optimizers_list = [ + opt + for opt in dir(optim) + if not opt.startswith("_") + and isinstance(getattr(optim, opt), type) + and issubclass(getattr(optim, opt), optim.Optimizer) + ] + print( + "".center(40, "-"), + *self.loss_functions, + "".center(40, "-"), + *self.optimizers_list, + "".center(40, "-"), + sep="\n" + ) + # exit(0) + # Create labels and entry fields for the parameters + self.params = [ + "SAMPLES", + "EPOCHS", + "DEFAULT_FORIER_DEGREE", + "CALC_FOURIER_DEGREE_BY_DATA_LENGTH", + "FORIER_DEGREE_DIVIDER", + "FORIER_DEGREE_OFFSET", + "PATIENCE", + "OPTIMIZER", + "LOSS_FUNCTION", + ] + + self.previous_values = {} + + for i, param in enumerate(self.params): + label = ttk.Label(self, text=param) + label.grid(row=i, column=0, sticky="NSW") + default = defaults.get( + param, 0 if param != "CALC_FOURIER_DEGREE_BY_DATA_LENGTH" else False + ) + var = ( + tk.BooleanVar(self, value=default) + if param == "CALC_FOURIER_DEGREE_BY_DATA_LENGTH" + else ( + tk.StringVar(self, value=default) + if param in ["OPTIMIZER", "LOSS_FUNCTION"] + else tk.IntVar(self, value=default) + ) + ) + var.trace_add( + "write", + lambda *args, key=param, var=var: self.on_change(key, var.get(), var), + ) + self.previous_values[param] = default + if param in ["OPTIMIZER", "LOSS_FUNCTION"]: + # set the default option + used_list = ( + self.optimizers_list + if param == "OPTIMIZER" + else self.loss_functions + ) + try: + default = used_list[used_list.index(defaults.get(param))] + except ValueError: + default = used_list[0] + + var.set(default) + + dropdown = ttk.OptionMenu(self, var, *(used_list)) + dropdown.grid(row=i, column=1, sticky="NSEW") + elif param == "CALC_FOURIER_DEGREE_BY_DATA_LENGTH": + entry = ttk.Checkbutton(self, variable=var) + entry.grid(row=i, column=1, sticky="NSEW") + else: + entry = ttk.Entry(self, textvariable=var) + entry.grid(row=i, column=1, sticky="NSEW") + var.set(default) + + def on_change(self, name, value, var): + if hasattr(self, "on_change_callback"): + if self.on_change_callback: + worked = self.on_change_callback(name, value) + # print(worked) + if worked: + self.previous_values[name] = value + else: + var.set(self.previous_values[name]) + + def set_on_change(self, func): + self.on_change_callback = func + self.previous_values = {param: var.get() for param, var in self.params.items()} + + +if __name__ == "__main__": + + def stupid(*args, **kwargs): + print(*args, *kwargs.items(), sep="\n") + return False + + # Usage + root = tk.Tk() + root.rowconfigure(0, weight=1) + root.columnconfigure(0, weight=1) + defaults = { + "SAMPLES": 44100 // 2, + "EPOCHS": 100, + "DEFAULT_FORIER_DEGREE": 250, + "FORIER_DEGREE_DIVIDER": 1, + "FORIER_DEGREE_OFFSET": 0, + "PATIENCE": 10, + "OPTIMIZER": "Adam", + "LOSS_FUNCTION": "mse_loss", + } + gui = NeuralNetworkGUI(root, defaults=defaults, callback=stupid) + gui.grid(row=0, column=0, sticky="NSEW") + root.mainloop() + + exit() + + # autopep8: off + + loss_functions = [ + func + for func in dir(nn) + if not func.startswith("_") + and isinstance(getattr(nn, func), type) + and issubclass(getattr(nn, func), nn.modules.loss._Loss) + ] + print(loss_functions) + + optimizers_list = [ + opt + for opt in dir(optim) + if not opt.startswith("_") + and isinstance(getattr(optim, opt), type) + and issubclass(getattr(optim, opt), optim.Optimizer) + ] + print(optimizers_list) diff --git a/pythonaisynth/fuer_elise.py b/pythonaisynth/fuer_elise.py new file mode 100644 index 0000000..17389a3 --- /dev/null +++ b/pythonaisynth/fuer_elise.py @@ -0,0 +1,83 @@ +import mido +import time +import os + +# Define the MIDI output port +if os.name == "nt": + output = mido.open_output("LoopBe Internal MIDI 1") +else: + try: + output = mido.open_output("Midi Through Port-0") + except: + print("no midi loopback found") + exit() + + +# Define the notes for "Für Elise" +fur_elise = [ + ("E5", 0.5), + ("D#5", 0.5), + ("E5", 0.5), + ("D#5", 0.5), + ("E5", 0.5), + ("B4", 0.5), + ("D5", 0.5), + ("C5", 0.5), + ("A4", 1.5), + ("C4", 0.5), + ("E4", 0.5), + ("A4", 0.5), + ("B4", 1.5), + ("E4", 0.5), + ("G#4", 0.5), + ("B4", 0.5), + ("C5", 1.5), + ("E4", 0.5), + ("C4", 0.5), + ("E4", 0.5), + ("A4", 0.5), + ("B4", 1.5), + ("E4", 0.5), + ("G#4", 0.5), + ("B4", 0.5), + ("C5", 1.5), +] + +# Define a function to convert note names to MIDI note numbers + + +def note_to_midi(note): + note_map = { + "C": 0, + "C#": 1, + "D": 2, + "D#": 3, + "E": 4, + "F": 5, + "F#": 6, + "G": 7, + "G#": 8, + "A": 9, + "A#": 10, + "B": 11, + } + octave = int(note[-1]) + key = note[:-1] + return 12 * (octave + 1) + note_map[key] + + +try: + # Stream the notes over MIDI in a loop + last_note = None + while True: + for note, duration in fur_elise: + midi_note = note_to_midi(note) + last_note = midi_note + output.send(mido.Message("note_on", note=midi_note, velocity=64)) + time.sleep(duration / 2) + output.send(mido.Message("note_off", note=midi_note, velocity=64)) +except: + output.panic() +output.send(mido.Message("note_off", note=last_note, velocity=64)) +# Close the MIDI output port (this line will never be reached due to the infinite loop) +# output.close() diff --git a/scr/graph_canvas.py b/pythonaisynth/graph_canvas.py similarity index 61% rename from scr/graph_canvas.py rename to pythonaisynth/graph_canvas.py index 8c805ed..87bc155 100644 --- a/scr/graph_canvas.py +++ b/pythonaisynth/graph_canvas.py @@ -7,7 +7,7 @@ import numpy as np -from scr import utils +from pythonaisynth import utils class GraphCanvas(ttk.Frame): @@ -33,14 +33,18 @@ def __init__(self, master, size: tuple[int, int] = None, darkmode=False): self.darkmode = darkmode ttk.Frame.__init__(self, master) self.master = master - self.canvas = tk.Canvas(self, width=self.canvas_width + self.offset * 2, - height=self.canvas_height + self.offset * 2, bg='#2d2d2d' if self.darkmode else "white") + self.canvas = tk.Canvas( + self, + width=self.canvas_width + self.offset * 2, + height=self.canvas_height + self.offset * 2, + bg="#2d2d2d" if self.darkmode else "white", + ) self.canvas.pack(fill=tk.BOTH, expand=True) - self.canvas.bind('', self.resize) + self.canvas.bind("", self.resize) - self.canvas.bind('', self.on_mouse_move_interpolate) - self.canvas.bind('', self.motion_end) - self.canvas.bind('', self.on_space_press) + self.canvas.bind("", self.on_mouse_move_interpolate) + self.canvas.bind("", self.motion_end) + self.canvas.bind("", self.on_space_press) # Changed to range from 0 to 2π self.lst = np.linspace(0, 2 * math.pi, self.LEVEL_OF_DETAIL) if 0 not in self.lst and self.INCLUDE_0: @@ -50,15 +54,15 @@ def __init__(self, master, size: tuple[int, int] = None, darkmode=False): self.clear() def _draw(self): - self.canvas.delete('all') + self.canvas.delete("all") self.setup_axes() for x, y in self.data: self.draw_point(x, y) - if hasattr(self, 'extern_graph'): + if hasattr(self, "extern_graph"): self._draw_extern_graph() def motion_end(self, event): - if hasattr(self, 'old_point'): + if hasattr(self, "old_point"): del self.old_point def clear(self): @@ -70,32 +74,52 @@ def on_space_press(self, event): def setup_axes(self): # Draw X-axis - self.canvas.create_line(self.offset, self.canvas_height / 2 + self.offset, - self.canvas_width + self.offset, self.canvas_height / 2 + self.offset, - fill='white' if self.darkmode else 'black') # X-axis + self.canvas.create_line( + self.offset, + self.canvas_height / 2 + self.offset, + self.canvas_width + self.offset, + self.canvas_height / 2 + self.offset, + fill="white" if self.darkmode else "black", + ) # X-axis # Draw Y-axis - self.canvas.create_line(self.offset, self.offset, - self.offset, self.canvas_height + self.offset, - fill='white' if self.darkmode else 'black') # Y-axis + self.canvas.create_line( + self.offset, + self.offset, + self.offset, + self.canvas_height + self.offset, + fill="white" if self.darkmode else "black", + ) # Y-axis # Draw tick marks and labels for X-axis for i in range(0, 3): # Changed to range from 0 to 2 - x = (self.canvas_width / 2 + (i - 1) * - self.canvas_width / 2) + self.offset - self.canvas.create_line(x, (self.canvas_height / 2) - 10 + self.offset, - x, (self.canvas_height / 2) + 10 + self.offset, fill='blue') + x = (self.canvas_width / 2 + (i - 1) * self.canvas_width / 2) + self.offset + self.canvas.create_line( + x, + (self.canvas_height / 2) - 10 + self.offset, + x, + (self.canvas_height / 2) + 10 + self.offset, + fill="blue", + ) self.canvas.create_text( - x, (self.canvas_height / 2) + 10 + self.offset, text=f'{i}π' if i > 0 else '0', fill='white' if self.darkmode else 'black') + x, + (self.canvas_height / 2) + 10 + self.offset, + text=f"{i}π" if i > 0 else "0", + fill="white" if self.darkmode else "black", + ) # Draw tick marks and labels for Y-axis for i in range(-1, 2): - y = (self.canvas_height / 2 - i * - (self.canvas_height / 2)) + self.offset - self.canvas.create_line((self.offset) - 10, y, - (self.offset) + 10, y, fill='red') # Move Y-axis tick marks to the left + y = (self.canvas_height / 2 - i * (self.canvas_height / 2)) + self.offset + self.canvas.create_line( + (self.offset) - 10, y, (self.offset) + 10, y, fill="red" + ) # Move Y-axis tick marks to the left self.canvas.create_text( - (self.offset) + 20, y, text=f'{i}', fill='white' if self.darkmode else 'black') # Adjust text position + (self.offset) + 20, + y, + text=f"{i}", + fill="white" if self.darkmode else "black", + ) # Adjust text position first = True @@ -111,13 +135,12 @@ def resize(self, event): def on_mouse_move_interpolate(self, event): new_point = (event.x, event.y) - if hasattr(self, 'old_point'): + if hasattr(self, "old_point"): old_point = self.old_point dist = abs(new_point[0] - old_point[0]) if dist > 2: t_values = np.arange(dist) / dist - points = utils.interpolate_vectorized( - old_point, new_point, t_values) + points = utils.interpolate_vectorized(old_point, new_point, t_values) for point in points: interpolate_event = copy(event) interpolate_event.x, interpolate_event.y = point @@ -131,53 +154,64 @@ def on_mouse_move_interpolate(self, event): self.old_point = new_point def eval_mouse_move_event(self, event): - x, y = self.convert_canvas_to_graph_coordinates_optimized( - event.x, event.y) + x, y = self.convert_canvas_to_graph_coordinates_optimized(event.x, event.y) x = min(self.lst, key=lambda _y: abs(x - _y)) - if utils.is_in_interval(y, self.lower_end_y, self.upper_end_y) and utils.is_in_interval(x, self.lower_end_x, self.upper_end_x): + if utils.is_in_interval( + y, self.lower_end_y, self.upper_end_y + ) and utils.is_in_interval(x, self.lower_end_x, self.upper_end_x): data_dict = dict(self.data) data_dict[x] = y self.data = list(data_dict.items()) def draw_point(self, x, y): cx, cy = self.convert_graph_to_canvas_coordinates_optimized(x, y) - self.canvas.create_oval(cx - self.point_radius, cy - self.point_radius, - cx + self.point_radius, cy + self.point_radius, fill='red') - - def draw_line(self, a, b, width=1, color='black'): + self.canvas.create_oval( + cx - self.point_radius, + cy - self.point_radius, + cx + self.point_radius, + cy + self.point_radius, + fill="red", + ) + + def draw_line(self, a, b, width=1, color="black"): ca = self.convert_graph_to_canvas_coordinates_optimized(*a) cb = self.convert_graph_to_canvas_coordinates_optimized(*b) r, g, b = self.master.winfo_rgb(color) hex_color = "#{:02x}{:02x}{:02x}".format(r // 256, g // 256, b // 256) - self.canvas.create_line(ca, cb, fill=hex_color, - width=width, smooth=True) + self.canvas.create_line(ca, cb, fill=hex_color, width=width, smooth=True) - new_r, new_g, new_b = utils.lighten_color( - r // 256, g // 256, b // 256, 0.33) + new_r, new_g, new_b = utils.lighten_color(r // 256, g // 256, b // 256, 0.33) hex_color = "#{:02x}{:02x}{:02x}".format(new_r, new_g, new_b) - self.canvas.create_line(ca, cb, fill=hex_color, - width=width + 0.5, smooth=True) + self.canvas.create_line(ca, cb, fill=hex_color, width=width + 0.5, smooth=True) def export_data(self): return copy(self.data) def convert_canvas_to_graph_coordinates_optimized(self, x, y): graph_x = utils.map_value( - x - self.offset, 0, self.canvas_width, self.lower_end_x, self.upper_end_x) + x - self.offset, 0, self.canvas_width, self.lower_end_x, self.upper_end_x + ) graph_y = utils.map_value( - y - self.offset, 0, self.canvas_height, self.lower_end_y, self.upper_end_y) + y - self.offset, 0, self.canvas_height, self.lower_end_y, self.upper_end_y + ) return graph_x, graph_y def convert_graph_to_canvas_coordinates_optimized(self, x, y): - graph_x = utils.map_value( - x, self.lower_end_x, self.upper_end_x, 0, self.canvas_width) + self.offset - graph_y = utils.map_value( - y, self.lower_end_y, self.upper_end_y, 0, self.canvas_height) + self.offset + graph_x = ( + utils.map_value(x, self.lower_end_x, self.upper_end_x, 0, self.canvas_width) + + self.offset + ) + graph_y = ( + utils.map_value( + y, self.lower_end_y, self.upper_end_y, 0, self.canvas_height + ) + + self.offset + ) return graph_x, graph_y def draw_bounding_box(self): - self.canvas.create_rectangle(1, 1, 598, 598, outline='black') + self.canvas.create_rectangle(1, 1, 598, 598, outline="black") def _eval_func(self, function, x): val = function(x) @@ -206,25 +240,39 @@ def _draw_extern_graph(self): legend_x = 10 # The x-coordinate of the top-left corner of the legend legend_y = 10 # The y-coordinate of the top-left corner of the legend legend_spacing = 20 # The vertical spacing between items in the legend - for i, (name, (graph, color, width, graph_type, prio)) in enumerate(self.extern_graph.items()): + for i, (name, (graph, color, width, graph_type, prio)) in enumerate( + self.extern_graph.items() + ): # Draw a small line of the same color as the graph - self.canvas.create_line(legend_x, legend_y + i * legend_spacing, - legend_x + 20, legend_y + i * legend_spacing, - fill=color, width=2) + self.canvas.create_line( + legend_x, + legend_y + i * legend_spacing, + legend_x + 20, + legend_y + i * legend_spacing, + fill=color, + width=2, + ) # Draw the name of the graph - self.canvas.create_text(legend_x + 30, legend_y + i * legend_spacing, - text=name, anchor='w', fill='white' if self.darkmode else 'black') + self.canvas.create_text( + legend_x + 30, + legend_y + i * legend_spacing, + text=name, + anchor="w", + fill="white" if self.darkmode else "black", + ) if graph_type == "line": for a, b in utils.pair_iterator(graph): self.draw_line(a, b, width, color) else: for a in graph: - x, y = self.convert_graph_to_canvas_coordinates_optimized( - *a) + x, y = self.convert_graph_to_canvas_coordinates_optimized(*a) self.canvas.create_oval( - x - width / 2, y - width / 2, x + width / 2, y + width / 2) + x - width / 2, y - width / 2, x + width / 2, y + width / 2 + ) - def draw_extern_graph_from_func(self, function, name=None, width=None, color=None, graph_type='line', prio=0): + def draw_extern_graph_from_func( + self, function, name=None, width=None, color=None, graph_type="line", prio=0 + ): if not width: width = self.point_radius / 2 if not color: @@ -237,14 +285,16 @@ def draw_extern_graph_from_func(self, function, name=None, width=None, color=Non x = np.linspace(self.lower_end_x, self.upper_end_x, 44100) graph_type = "line" data = list(zip(x, function(x))) - if not hasattr(self, 'extern_graph'): + if not hasattr(self, "extern_graph"): self.extern_graph = {} self.extern_graph[name] = (data, color, width, graph_type, prio) self._draw() except Exception as e: print(e) - def draw_extern_graph_from_data(self, data, name=None, width=None, color=None, graph_type='line', prio=0): + def draw_extern_graph_from_data( + self, data, name=None, width=None, color=None, graph_type="line", prio=0 + ): if not width: width = self.point_radius / 2 if not color: @@ -252,7 +302,7 @@ def draw_extern_graph_from_data(self, data, name=None, width=None, color=None, g if not name: name = f"function {len(list(self.extern_graph.keys()))}" try: - if not hasattr(self, 'extern_graph'): + if not hasattr(self, "extern_graph"): self.extern_graph = {} self.extern_graph[name] = (data, color, width, graph_type, prio) self._draw() diff --git a/scr/graph_canvas_v2.py b/pythonaisynth/graph_canvas_v2.py similarity index 80% rename from scr/graph_canvas_v2.py rename to pythonaisynth/graph_canvas_v2.py index 8f484f9..523f607 100644 --- a/scr/graph_canvas_v2.py +++ b/pythonaisynth/graph_canvas_v2.py @@ -6,9 +6,9 @@ import tkinter as tk from tkinter import ttk -from scr.utils import center_and_scale, run_after_ms, tk_after_errorless +from .utils import center_and_scale, run_after_ms, tk_after_errorless -matplotlib.use('TkAgg') +matplotlib.use("TkAgg") class GraphCanvas(ttk.Frame): @@ -20,32 +20,31 @@ def __init__(self, master, *args, **kwargs): self.x = np.linspace(0, 2 * np.pi, 500) self.y = np.zeros_like(self.x) # Initial y values set to 0 - plt.style.use('dark_background') + plt.style.use("dark_background") # Create the plot self.fig, self.ax = plt.subplots(figsize=(10, 6)) - self.line, = self.ax.plot( - self.x, self.y, '-o', markersize=5) # Red dots + (self.line,) = self.ax.plot(self.x, self.y, "-o", markersize=5) # Red dots # Set the x and y axis limits self.ax.set_xlim(0, 2 * np.pi) self.ax.set_ylim(-2, 2) # Set the x and y axis labels - self.ax.set_xlabel('x (radians)') - self.ax.set_ylabel('y') + self.ax.set_xlabel("x (radians)") + self.ax.set_ylabel("y") # Customize the ticks on the x-axis - self.ax.set_xticks([0, np.pi/2, np.pi, 3*np.pi/2, 2*np.pi]) - self.ax.set_xticklabels(['0', 'π/2', 'π', '3π/2', '2π']) + self.ax.set_xticks([0, np.pi / 2, np.pi, 3 * np.pi / 2, 2 * np.pi]) + self.ax.set_xticklabels(["0", "π/2", "π", "3π/2", "2π"]) # Draw the grid self.ax.grid() # Add a title - self.ax.set_title('Draw Shape of Soundwave') + self.ax.set_title("Draw Shape of Soundwave") # Create a cursor for better visibility - self.cursor = Cursor(self.ax, useblit=True, color='blue', linewidth=1) + self.cursor = Cursor(self.ax, useblit=True, color="blue", linewidth=1) # Variables to track the selected point self.selected_index = None @@ -55,11 +54,9 @@ def __init__(self, master, *args, **kwargs): self.existing_plots = {} # Connect the events to the functions - self.fig.canvas.mpl_connect('button_press_event', self.on_mouse_press) - self.fig.canvas.mpl_connect( - 'button_release_event', self.on_mouse_release) - self.fig.canvas.mpl_connect( - 'motion_notify_event', self.on_mouse_motion) + self.fig.canvas.mpl_connect("button_press_event", self.on_mouse_press) + self.fig.canvas.mpl_connect("button_release_event", self.on_mouse_release) + self.fig.canvas.mpl_connect("motion_notify_event", self.on_mouse_motion) # Create a canvas to embed the plot in Tkinter self.redraw_needed = False @@ -98,12 +95,15 @@ def on_mouse_motion(self, event): if self.last_index is not None: start_index = self.last_index end_index = new_index - indices = np.arange( - start_index, end_index + 1) if new_index > self.selected_index else np.arange(start_index, end_index - 1, -1) - percentages = (indices - start_index) / \ - (end_index - start_index) - self.y[indices] = event.ydata * percentages + \ - self.y[start_index] * (1 - percentages) + indices = ( + np.arange(start_index, end_index + 1) + if new_index > self.selected_index + else np.arange(start_index, end_index - 1, -1) + ) + percentages = (indices - start_index) / (end_index - start_index) + self.y[indices] = event.ydata * percentages + self.y[ + start_index + ] * (1 - percentages) self.selected_index = new_index self.update_y() self.last_index = new_index @@ -135,11 +135,13 @@ def plot_points(self, points, name=None, type="points"): self.existing_plots[name].remove() if type == "points": - plot_line, = self.ax.plot( - x_values, y_values, 'ro', label=name) # 'ro' for red dots + (plot_line,) = self.ax.plot( + x_values, y_values, "ro", label=name + ) # 'ro' for red dots elif type == "line": - plot_line, = self.ax.plot( - x_values, y_values, 'r-', label=name) # 'r-' for red line + (plot_line,) = self.ax.plot( + x_values, y_values, "r-", label=name + ) # 'r-' for red line else: raise ValueError("Invalid type. Use 'points' or 'line'.") @@ -170,8 +172,9 @@ def plot_function(self, func, x_range=None, overwrite=False, name=None): self.existing_plots[name].set_ydata(y_values) else: # Create a new plot for the function - plot_line, = self.ax.plot( - x_values, y_values, label=f'Function: {name}') + (plot_line,) = self.ax.plot( + x_values, y_values, label=f"Function: {name}" + ) self.ax.legend() # Store the new plot line self.existing_plots[name] = plot_line @@ -200,8 +203,9 @@ def plot_function(self, func, x_range=None, overwrite=False, name=None): self.existing_plots[func.__name__].remove() # Create a new plot for the function - plot_line, = self.ax.plot( - x_values, y_values, label=f'Function: {func.__name__}') + (plot_line,) = self.ax.plot( + x_values, y_values, label=f"Function: {func.__name__}" + ) self.ax.legend() # Store the new plot line self.existing_plots[func.__name__] = plot_line diff --git a/main.py b/pythonaisynth/main.py similarity index 56% rename from main.py rename to pythonaisynth/main.py index 12c51f0..37b43ca 100644 --- a/main.py +++ b/pythonaisynth/main.py @@ -1,21 +1,18 @@ import copy -import socket +import subprocess import sys -import threading -import time import numpy as np -from scr import music -from scr import utils -from scr.music import Synth3, musik_from_file -from scr.simple_input_dialog import askStringAndSelectionDialog -from scr.std_redirect import RedirectedOutputFrame -from scr.utils import DIE, tk_after_errorless -from scr.predefined_functions import predefined_functions_dict -from scr.graph_canvas_v2 import GraphCanvas -from scr.fourier_neural_network_gui import NeuralNetworkGUI -from scr.fourier_neural_network import FourierNN -from _version import version +from .music import Synth2, musik_from_file +from .simple_input_dialog import askStringAndSelectionDialog +from .std_redirect import RedirectedOutputFrame +from .synth_gui import SynthGUI +from .utils import DIE, QueueSTD_OUT, tk_after_errorless +from .predefined_functions import predefined_functions_dict +from .graph_canvas_v2 import GraphCanvas +from .fourier_neural_network_gui import NeuralNetworkGUI +from .fourier_neural_network import FourierNN +from ._version import version import atexit from multiprocessing import Process, Queue from multiprocessing.managers import SyncManager @@ -35,24 +32,24 @@ ".": { "configure": { "background": "#2d2d2d", # Dark grey background - "foreground": "white", # White text + "foreground": "white", # White text } }, "TLabel": { "configure": { - "foreground": "white", # White text + "foreground": "white", # White text } }, "TButton": { "configure": { "background": "#3c3f41", # Dark blue-grey button - "foreground": "white", # White text + "foreground": "white", # White text } }, "TEntry": { "configure": { "background": "#2d2d2d", # Dark grey background - "foreground": "white", # White text + "foreground": "white", # White text "fieldbackground": "#4d4d4d", "insertcolor": "white", "bordercolor": "black", @@ -62,7 +59,7 @@ }, "TCheckbutton": { "configure": { - "foreground": "white", # White text + "foreground": "white", # White text "indicatorbackground": "white", "indicatorforeground": "black", } @@ -70,13 +67,13 @@ "TCombobox": { "configure": { "background": "#2d2d2d", # Dark grey background - "foreground": "white", # White text + "foreground": "white", # White text "fieldbackground": "#4d4d4d", "insertcolor": "white", "bordercolor": "black", "lightcolor": "#4d4d4d", "darkcolor": "black", - "arrowcolor": "white" + "arrowcolor": "white", }, }, } @@ -85,16 +82,37 @@ class MainGUI(tk.Tk): + """ + MainGUI is the main application window for the AI Synth project. + + This class initializes the Tkinter GUI, sets up the application layout, + and manages the interaction between the frontend and backend components. + It includes features such as dark mode, status bar updates, and various + controls for training AI models and synthesizing sound. + + Attributes: + manager (SyncManager): Manages shared resources and processes. + lock (Lock): Ensures thread-safe operations. + std_queue (Queue): Handles standard output redirection. + process_monitor (psutil.Process): Monitors the main process and its children. + trainings_process (multiprocessing.Process): Manages the AI model training process. + training_started (bool): Indicates if training has started. + block_training (bool): Prevents multiple training sessions. + queue (Queue): Handles inter-process communication. + fourier_nn (FourierNN): The neural network for Fourier transformations. + synth (Synth2): The synthesizer object. + """ + def __init__(self, *args, manager: SyncManager = None, **kwargs): super().__init__(*args, **kwargs) self.style = ttk.Style() if DARKMODE: - self.configure(bg='#202020') + self.configure(bg="#202020") self.option_add("*TCombobox*Listbox*Background", "#202020") self.option_add("*TCombobox*Listbox*Foreground", "white") - self.style.theme_create('dark', parent="clam", settings=dark_theme) - self.style.theme_use('dark') + self.style.theme_create("dark", parent="clam", settings=dark_theme) + self.style.theme_use("dark") self.manager = manager self.lock = manager.Lock() @@ -108,19 +126,20 @@ def __init__(self, *args, manager: SyncManager = None, **kwargs): self.columnconfigure(3, weight=1) self.trainings_process: Process = None - self.music_process: Process = None self.training_started = False self.block_training = False self.queue = Queue(-1) self.fourier_nn = None - self.synth: Synth3 = None + self.synth: Synth2 = None self.init_terminal_frame() self.create_menu() self.create_row_one() self.graph = GraphCanvas(self, (900, 300), DARKMODE) - self.graph.grid(row=1, column=0, columnspan=3, sticky='NSEW') + self.graph.grid(row=1, column=0, columnspan=3, sticky="NSEW") self.create_controll_column() self.create_status_bar() + self.is_recording = False + # self.is_paused = False # sys.stdout = utils.QueueSTD_OUT(self.std_queue) # def destroy(self): @@ -133,48 +152,76 @@ def __init__(self, *args, manager: SyncManager = None, **kwargs): def init_terminal_frame(self): self.std_redirect = RedirectedOutputFrame(self, self.std_queue) # self.std_queue = self.std_redirect.queue - self.std_redirect.grid(row=2, column=0, columnspan=3, sticky='NSEW') + self.std_redirect.grid(row=2, column=0, columnspan=3, sticky="NSEW") def create_controll_column(self): self.frame = ttk.Frame(self) defaults = { - 'SAMPLES': 500, - 'EPOCHS': 1000, - 'DEFAULT_FORIER_DEGREE': 100, - 'FORIER_DEGREE_DIVIDER': 1, - 'FORIER_DEGREE_OFFSET': 0, - 'PATIENCE': 50, - 'OPTIMIZER': 'SGD', - 'LOSS_FUNCTION': 'HuberLoss', + "SAMPLES": 500, + "EPOCHS": 1000, + "DEFAULT_FORIER_DEGREE": 100, + "FORIER_DEGREE_DIVIDER": 1, + "FORIER_DEGREE_OFFSET": 0, + "PATIENCE": 50, + "OPTIMIZER": "SGD", + "LOSS_FUNCTION": "HuberLoss", } - self.gui = NeuralNetworkGUI( - self.frame, defaults=defaults, callback=self.update_frourier_params) - self.gui.grid(row=0, sticky='NSEW') - self.frame.grid(row=1, column=3, rowspan=2, sticky='NSEW') + self.neural_network_gui = NeuralNetworkGUI( + self.frame, defaults=defaults, callback=self.update_frourier_params + ) + self.neural_network_gui.grid(row=0, sticky="NSEW") + + separator1 = ttk.Separator(self.frame, orient="horizontal") + separator1.grid(row=1, sticky="WE") + + self.synth_gui = SynthGUI(self.frame) + self.synth_gui.grid(row=2, sticky="NSEW") + + self.frame.grid(row=1, column=3, rowspan=2, sticky="NSEW") def create_status_bar(self): # Create a status bar with two labels self.status_bar = ttk.Frame(self, relief=tk.SUNKEN) # Adjust row and column as needed - self.status_bar.grid(row=3, column=0, sticky='we', columnspan=4) + self.status_bar.grid(row=3, column=0, sticky="we", columnspan=4) self.status_label = ttk.Label( - self.status_bar, text="Ready", anchor=tk.W, font=("TkFixedFont"), relief=tk.SUNKEN) + self.status_bar, + text="Ready", + anchor=tk.W, + font=("TkFixedFont"), + relief=tk.SUNKEN, + ) self.status_label.pack(side=tk.LEFT) self.processes_label = ttk.Label( - self.status_bar, text="Children Processes: 0", anchor=tk.E, font=("TkFixedFont"), relief=tk.SUNKEN) + self.status_bar, + text="Children Processes: 0", + anchor=tk.E, + font=("TkFixedFont"), + relief=tk.SUNKEN, + ) self.processes_label.pack(side=tk.RIGHT) # CPU and RAM self.cpu_label = ttk.Label( - self.status_bar, text="CPU Usage: 0%", anchor=tk.E, font=("TkFixedFont"), relief=tk.SUNKEN) + self.status_bar, + text="CPU Usage: 0%", + anchor=tk.E, + font=("TkFixedFont"), + relief=tk.SUNKEN, + ) self.cpu_label.pack(side=tk.RIGHT) self.ram_label = ttk.Label( - self.status_bar, text="RAM Usage: 0%", anchor=tk.E, font=("TkFixedFont"), relief=tk.SUNKEN) + self.status_bar, + text="RAM Usage: 0%", + anchor=tk.E, + font=("TkFixedFont"), + relief=tk.SUNKEN, + ) self.ram_label.pack(side=tk.RIGHT) self.frame_no = 0 @@ -198,84 +245,108 @@ def update_status_bar(self): print(total_mem) self.quit() # input("press key to continue") - self.cpu_label.config( - text=f"CPU Usage: {total_cpu/os.cpu_count():.2f}%") + self.cpu_label.config(text=f"CPU Usage: {total_cpu/os.cpu_count():.2f}%") self.ram_label.config(text=f"RAM Usage: {total_mem:.2f}%") - animation_text = "|" if self.frame_no == 0 else '/' if self.frame_no == 1 else '-' if self.frame_no == 2 else '\\' - self.frame_no = (self.frame_no+1) % 4 + animation_text = ( + "|" + if self.frame_no == 0 + else "/" if self.frame_no == 1 else "-" if self.frame_no == 2 else "\\" + ) + self.frame_no = (self.frame_no + 1) % 4 if len(children) <= 1: self.status_label.config(text="Ready", foreground="green") else: - self.status_label.config( - text=f"Busy ({animation_text})", foreground="red") + self.status_label.config(text=f"Busy ({animation_text})", foreground="red") - self.processes_label.config( - text=f"Children Processes: {len(children)}") + self.processes_label.config(text=f"Children Processes: {len(children)}") tk_after_errorless(self, 500, self.update_status_bar) def create_row_one(self): self.label = ttk.Label(self, text="Predefined Functions:") - self.label.grid(row=0, column=0, sticky='NSEW') + self.label.grid(row=0, column=0, sticky="NSEW") self.combo_selected_value = tk.StringVar() # Create a select box self.combo = ttk.Combobox( - self, values=list(predefined_functions_dict.keys()), - textvariable=self.combo_selected_value, state="readonly") + self, + values=list(predefined_functions_dict.keys()), + textvariable=self.combo_selected_value, + state="readonly", + ) - self.combo.bind( - '<>', self.draw_graph_from_func) - self.combo.grid(row=0, column=1, sticky='NSEW') + self.combo.bind("<>", self.draw_graph_from_func) + self.combo.grid(row=0, column=1, sticky="NSEW") # Create a 'Start Training' button self.train_button = ttk.Button( - self, text="Start Training", command=self.start_training) - self.train_button.grid(row=0, column=2, sticky='NSEW') + self, text="Start Training", command=self.start_training + ) + self.train_button.grid(row=0, column=2, sticky="NSEW") # Create a 'Play' button - self.play_button = ttk.Button( - self, text="Play", command=self.play_music) - self.play_button.grid(row=0, column=3, sticky='NSEW') + self.play_button = ttk.Button(self, text="Play", command=self.play_music) + self.play_button.grid(row=0, column=3, sticky="NSEW") def create_menu(self): # Create a menu bar if DARKMODE: - self.menu_bar = tk.Menu(self, bg="#2d2d2d", fg="white", - activebackground="#3e3e3e", activeforeground="white") + self.menu_bar = tk.Menu( + self, + bg="#2d2d2d", + fg="white", + activebackground="#3e3e3e", + activeforeground="white", + ) else: self.menu_bar = tk.Menu(self) # Create a 'File' menu - self.create_menu_item("File", [ - ("Export Neural Net", self.export_neural_net), - ("Load Neural Net", self.load_neural_net) - ]) + self.create_menu_item( + "File", + [ + ("Export Neural Net", self.export_neural_net), + ("Load Neural Net", self.load_neural_net), + ], + ) # Create a 'Training' menu - self.create_menu_item("Training", [ - ("Start Training", self.start_training) - ]) + self.create_menu_item("Training", [("Start Training", self.start_training)]) # Create a 'Graph' menu - self.create_menu_item("Graph", [ - ("Clear Graph", self.clear_graph), - # ("Redraw Graph", self.redraw_graph) - ]) + self.create_menu_item( + "Graph", + [ + ("Clear Graph", self.clear_graph), + # ("Redraw Graph", self.redraw_graph) + ], + ) # Create a 'Music' menu - self.create_menu_item("Music", [ - ("Play Music from MIDI Port", self.play_music), - ("Play Music from MIDI File", self.play_music_file) - ]) + self.create_menu_item( + "Music", + [ + ("Play Music from MIDI Port", self.play_music), + ("Play Music from MIDI File", self.play_music_file), + ("Play Example", self.play_example), + ("Record to File", self.start_recording), + ("Stop Recording", self.stop_recording), + ], + ) # Display the menu self.config(menu=self.menu_bar) def create_menu_item(self, name, commands): if DARKMODE: - menu = tk.Menu(self.menu_bar, tearoff=0, bg="#2d2d2d", fg="white", - activebackground="#3e3e3e", activeforeground="white") + menu = tk.Menu( + self.menu_bar, + tearoff=0, + bg="#2d2d2d", + fg="white", + activebackground="#3e3e3e", + activeforeground="white", + ) else: menu = tk.Menu(self.menu_bar, tearoff=0) for label, command in commands: @@ -290,7 +361,7 @@ def train_update(self): data = self.queue.get_nowait() # print("!!!check!!!") data = list(zip(self.graph.x, data)) - self.graph.plot_points(data, name="training", type='line') + self.graph.plot_points(data, name="training", type="line") except Exception as e: # print(e) pass @@ -307,30 +378,39 @@ def train_update(self): # print( # f"Layer: {name} | Size: {param.size()} | Values:\n{param[:2]}\n------------------------------") self.graph.plot_function( - self.fourier_nn.predict, x_range=(0, 2*np.pi, 44100//2)) - self.synth = Synth3(self.fourier_nn, self.std_queue) + self.fourier_nn.predict, x_range=(0, 2 * np.pi, 44100 // 2) + ) + self.synth = Synth2( + self.fourier_nn, self.std_queue, self.synth_gui.get_port_name() + ) DIE(self.trainings_process) self.trainings_process = None self.training_started = False self.block_training = False - messagebox.showinfo( - "training Ended", f"exit code: {exit_code}") + messagebox.showinfo("training Ended", f"exit code: {exit_code}") # self.fourier_nn.clean_memory() return tk_after_errorless(self, 10, self.train_update) def init_or_update_nn(self, stdout=None): if not self.fourier_nn: - print("".center(20, '*')) + print("".center(20, "*")) self.fourier_nn = FourierNN( - self.lock, self.graph.export_data(), stdout_queue=stdout) + self.lock, self.graph.export_data(), stdout_queue=stdout + ) else: - print("".center(20, '#')) + print("".center(20, "#")) self.fourier_nn.update_data(self.graph.export_data()) self.fourier_nn.save_model() self.trainings_process = multiprocessing.Process( - target=self.fourier_nn.train, args=(self.graph.x, self.queue, ), kwargs={"stdout_queue": self.std_queue}) + target=self.fourier_nn.train, + args=( + self.graph.x, + self.queue, + ), + kwargs={"stdout_queue": self.std_queue}, + ) self.trainings_process.start() self.training_started = True @@ -338,8 +418,11 @@ def init_or_update_nn(self, stdout=None): def start_training(self): if self.trainings_process or self.block_training: - print('already training') + print("already training") return + if self.synth: + if self.synth.live_synth: + self.synth.run_live_synth() self.block_training = True print("STARTING TRAINING") t = Thread(target=self.init_or_update_nn, args=(self.std_queue,)) @@ -352,6 +435,9 @@ def start_training(self): def play_music(self): print("play_music") if self.synth: + port_name = self.synth_gui.get_port_name() + # print([param.device for param in self.fourier_nn.current_model.parameters()]) + self.synth.set_port_name(port_name) self.synth.run_live_synth() else: print("or not") @@ -360,7 +446,7 @@ def play_music(self): def play_music_file(self): print("play_music") if self.fourier_nn: - music.musik_from_file(self.fourier_nn) + musik_from_file(self.fourier_nn) # music.midi_to_musik_live(self.fourier_nn, self.std_queue) def clear_graph(self): @@ -369,22 +455,24 @@ def clear_graph(self): def export_neural_net(self): print("export_neural_net") - default_format = 'keras' + default_format = "keras" if self.fourier_nn: - path = './tmp' + path = "./tmp" if not os.path.exists(path): os.mkdir(path) i = 0 - while os.path.exists(path+f"/model{i}.{default_format}"): + while os.path.exists(path + f"/model{i}.{default_format}"): i += 1 - dialog = askStringAndSelectionDialog(parent=self, - title="Save Model", - label_str="Enter a File Name", - default_str=f"model{i}", - label_select="Select Format", - default_select=default_format, - values_to_select_from=["pth"]) + dialog = askStringAndSelectionDialog( + parent=self, + title="Save Model", + label_str="Enter a File Name", + default_str=f"model{i}", + label_select="Select Format", + default_select=default_format, + values_to_select_from=["pth"], + ) name, file_format = dialog.result if not name: name = f"model{i}" @@ -395,28 +483,31 @@ def export_neural_net(self): try: self.fourier_nn.save_model(file) except Exception as e: - messagebox.showwarning( - "ERROR - Can't Save Model", f"{e}") + messagebox.showwarning("ERROR - Can't Save Model", f"{e}") else: messagebox.showwarning( - "File already Exists", f"The selected filename {name} already exists.") + "File already Exists", + f"The selected filename {name} already exists.", + ) def load_neural_net(self): print("load_neural_net") - filetypes = (('Torch Model File', '*.pth'), - ('All files', '*.*')) + filetypes = (("Torch Model File", "*.pth"), ("All files", "*.*")) filename = filedialog.askopenfilename( - title='Open a file', initialdir='.', filetypes=filetypes, parent=self) + title="Open a file", initialdir=".", filetypes=filetypes, parent=self + ) if os.path.exists(filename): if not self.fourier_nn: self.fourier_nn = FourierNN( - lock=self.lock, data=None, stdout_queue=self.std_queue) + lock=self.lock, data=None, stdout_queue=self.std_queue + ) self.fourier_nn.load_new_model_from_file(filename) - name = os.path.basename(filename).split('.')[0] - self.graph.draw_extern_graph_from_func( - self.fourier_nn.predict, name) + name = os.path.basename(filename).split(".")[0] + self.graph.draw_extern_graph_from_func(self.fourier_nn.predict, name) print(name) - self.synth = Synth3(self.fourier_nn, self.std_queue) + self.synth = Synth2( + self.fourier_nn, self.std_queue, self.synth_gui.get_port_name() + ) # self.fourier_nn.update_data( # data=self.graph.get_graph(name=name)[0]) @@ -430,19 +521,43 @@ def load_neural_net(self): def draw_graph_from_func(self, *args, **kwargs): print("draw_graph_from_func:", self.combo_selected_value.get()) self.graph.plot_function( - predefined_functions_dict[self.combo_selected_value.get()], overwrite=True) + predefined_functions_dict[self.combo_selected_value.get()], overwrite=True + ) def update_frourier_params(self, key, value): if not self.fourier_nn: # print(self.std_queue) - self.fourier_nn = FourierNN( - lock=self.lock, stdout_queue=self.std_queue) + self.fourier_nn = FourierNN(lock=self.lock, stdout_queue=self.std_queue) if not self.trainings_process and not self.block_training: self.fourier_nn.update_attribs(**{key: value}) return True return False - + def play_example(self): + print("Not Implemented Yet") + pass + # path = os.path.join(os.path.dirname(__file__), "fuer_elise.py") + # subprocess.Popen(["python", path]) + + def start_recording(self): + if self.synth and not self.is_recording: + file_path = filedialog.asksaveasfilename( + title="Save Recording", + defaultextension=".wav", + filetypes=(("WAV files", "*.wav"), ("All files", "*.*")), + ) + if file_path: + print(f"Recording will be saved to: {file_path}") + self.synth.start_recording(file_path) + self.is_recording = True + + def stop_recording(self): + if self.synth and self.is_recording: + self.synth.stop_recording() + self.is_recording = False + + +# for "invalid command" in the tk.TK().after() function when programm gets closed # def start_server(): # global window # with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: @@ -471,10 +586,11 @@ def update_frourier_params(self, key, value): def main(): + print(version) # global state, window # threading.Thread(target=start_server, daemon=True).start() - os.environ['HAS_RUN_INIT'] = 'True' + os.environ["HAS_RUN_INIT"] = "True" multiprocessing.set_start_method("spawn") with multiprocessing.Manager() as manager: std_write = copy.copy(sys.stdout.write) @@ -483,7 +599,7 @@ def main(): state = "running" try: # window.after(1000, lambda: window.graph.plot_function( - # predefined_functions_dict['nice'], overwrite=True)) + # predefined_functions_dict['funny2'], overwrite=True)) # window.after(2000, window.start_training) window.mainloop() except KeyboardInterrupt: diff --git a/pythonaisynth/music.py b/pythonaisynth/music.py new file mode 100644 index 0000000..7ea8484 --- /dev/null +++ b/pythonaisynth/music.py @@ -0,0 +1,576 @@ +import ctypes +import multiprocessing +import wave +import mido +import scipy +import torch +from .fourier_neural_network import FourierNN +from pythonaisynth import utils +import atexit +from multiprocessing import Process, Queue, Value, current_process +import sys +from tkinter import filedialog +import numpy as np +import sounddevice as sd +import pyaudio + +START = 0 +STOP = 1 +PAUSE = 2 + + +def musik_from_file(fourier_nn: FourierNN): + import sounddevice as sd + import pretty_midi + + midi_file = filedialog.askopenfilename() + if not midi_file: + return + print(midi_file) + midi_data = pretty_midi.PrettyMIDI(midi_file) + # print(dir(midi_data)) + notes_list = [] + # Iterate over all instruments and notes in the MIDI file + if fourier_nn: + for instrument in midi_data.instruments: + # print(dir(instrument)) + note_list = [] + for note_a, note_b in utils.note_iterator(instrument.notes): + # node_a + print(note_a) + duration = note_a.end - note_a.start + synthesized_note = fourier_nn.synthesize( + midi_note=note_a.pitch - 12, duration=duration, sample_rate=44100 + ) + print(synthesized_note.shape) + note_list.append(synthesized_note) + + if note_b: + # pause + print("Pause") + duration = abs(note_b.start - note_a.end) + synthesized_note = np.zeros((int(duration * 44100), 1)) + print(synthesized_note.shape) + note_list.append(synthesized_note) + + # # node_b + # print(note_b) + # duration = note_b.end - note_b.start + # synthesized_note = fourier_nn.synthesize_2( + # midi_note=note_b.pitch, duration=duration, sample_rate=44100) + # print(synthesized_note.shape) + # notes_list.append(synthesized_note) + notes_list.append(np.concatenate(note_list)) + + output = np.sum(notes_list) + output = rescale_audio(output) + sd.play(output, 44100) + + +def rescale_audio(audio): + max_val = max(abs(audio.min()), audio.max()) + if max_val == 0: + return audio + return audio / max_val + + +def apply_reverb(audio, decay=0.5, delay=0.02, fs=44100): + delay_samples = int(delay * fs) + impulse_response = np.zeros((delay_samples, 2)) + impulse_response[0, :] = 1 + impulse_response[-1, :] = decay + reverb_audio = np.zeros_like(audio) + for channel in range(audio.shape[1]): + reverb_audio[:, channel] = scipy.signal.fftconvolve( + audio[:, channel], impulse_response[:, channel] + )[: len(audio)] + return reverb_audio + + +def apply_echo(audio, delay=0.2, decay=0.5, fs=44100): + delay_samples = int(delay * fs) + echo_audio = np.zeros((len(audio) + delay_samples)) + echo_audio[: len(audio)] = audio + echo_audio[delay_samples:] += decay * audio + echo_audio = echo_audio[: len(audio)] + return echo_audio + + +def apply_chorus(audio, rate=1.5, depth=0.02, fs=44100): + t = np.arange(len(audio)) / fs + mod = depth * np.sin(2 * np.pi * rate * t) + chorus_audio = np.zeros_like(audio) + for channel in range(audio.shape[1]): + for i in range(len(audio)): + delay = int(mod[i] * fs) + if i + delay < len(audio): + chorus_audio[i, channel] = audio[i + delay, channel] + else: + chorus_audio[i, channel] = audio[i, channel] + return chorus_audio + + +def apply_distortion(audio, gain=2.0, threshold=0.5): + distorted_audio = gain * audio + distorted_audio[distorted_audio > threshold] = threshold + distorted_audio[distorted_audio < -threshold] = -threshold + return distorted_audio + + +class ADSR: + def __init__( + self, attack_time, decay_time, sustain_level, release_time, sample_rate + ): + self.attack_samples = int(attack_time * sample_rate) + self.decay_samples = int(decay_time * sample_rate) + self.ad_samples = self.attack_samples + self.decay_samples + self.sustain_level = sustain_level + self.release_samples = int(release_time * sample_rate) + + self.ads_envelope = np.concatenate( + ( + np.linspace(0, 1, self.attack_samples), + np.linspace(1, self.sustain_level, self.decay_samples), + ) + ) + self.r_envelope = np.linspace(self.sustain_level, 0, self.release_samples) + + self._r_counter = [0 for _ in range(128)] + self._ads_counter = [0 for _ in range(128)] + + def reset_note(self, note_num): + self._ads_counter[note_num] = 0 + self._r_counter[note_num] = 0 + + def has_note_ended(self, note_num) -> bool: + return self._r_counter[note_num] >= self.release_samples + + def get_ads_envelope(self, note_num, frame): + start = self._ads_counter[note_num] + end = start + frame + self._ads_counter[note_num] += frame + + if start > self.ad_samples: + return np.ones(frame) * self.sustain_level + + envelope = np.zeros(frame) + if end > self.ad_samples: + envelope[: self.ad_samples - start] = self.ads_envelope[ + start : self.ad_samples + ] + envelope[self.ad_samples - start :] = ( + np.ones(end - self.ad_samples) * self.sustain_level + ) + else: + envelope[:] = self.ads_envelope[start:end] + + return envelope + + def get_r_envelope(self, note_num, frame): + start = self._r_counter[note_num] + end = start + frame + self._r_counter[note_num] += frame + + if start > self.release_samples: + return np.zeros(frame) + + envelope = np.zeros(frame) + if end <= self.release_samples: + envelope[:] = self.r_envelope[start:end] + else: + envelope[: self.release_samples - start] = self.r_envelope[ + start : self.release_samples + ] + # After release, the envelope is 0 + envelope[self.release_samples - start :] = 0 + return envelope + + +class Echo_torch: + def __init__(self, sample_size, delay_time_ms, feedback, wet_dry_mix, device="cpu"): + self.sample_size = sample_size + # Convert ms to samples (assuming 44.1 kHz sample rate) + self.delay_time = int(delay_time_ms * 44.1) + self.feedback = feedback + self.wet_dry_mix = wet_dry_mix + self.device = device + + # Initialize the past buffer to store delayed samples + self.past = torch.zeros((self.delay_time + 1, sample_size), device=device) + + def apply(self, sound): + # Ensure sound is a tensor of shape (num_samples, sample_size) + if sound.dim() != 2 or sound.size(1) != self.sample_size: + raise ValueError( + "Input sound must be a 2D tensor with shape (num_samples, sample_size)" + ) + + # Create an output tensor + output = torch.zeros_like(sound, device=self.device) + + for i in range(sound.size(0)): + # Get the current sample + current_sample = sound[i] + + # Get the delayed sample from the past buffer + delayed_sample = ( + self.past[-self.delay_time] + if i >= self.delay_time + else torch.zeros(self.sample_size, device=self.device) + ) + + # Calculate the echo effect + echo_sample = ( + current_sample + delayed_sample * self.feedback + ) * self.wet_dry_mix + + # Store the current sample in the past buffer + self.past = torch.roll(self.past, shifts=-1, dims=0) + self.past[-1] = current_sample + + # Combine the original sound and the echo + output[i] = current_sample * (1 - self.wet_dry_mix) + echo_sample + + return output + + +class Echo: + def __init__(self, sample_size, delay_time_ms, feedback, wet_dry_mix): + self.sample_size = sample_size + # Convert ms to samples (assuming 44.1 kHz sample rate) + # Convert ms to seconds + self.delay_time = int(delay_time_ms * 44.1 / 1000) + self.feedback = feedback + self.wet_dry_mix = wet_dry_mix + + # Initialize the past buffer to store delayed samples + self.past = np.zeros((self.delay_time + 1, sample_size)) + + def apply(self, sound): + # Ensure sound is a 2D array with shape (num_samples, sample_size) + if sound.ndim != 2 or sound.shape[1] != self.sample_size: + raise ValueError( + "Input sound must be a 2D array with shape (num_samples, sample_size)" + ) + + # Create an output array + output = np.zeros_like(sound) + + for i in range(sound.shape[0]): + # Get the current sample + current_sample = sound[i] + + # Get the delayed sample from the past buffer + if i >= self.delay_time: + delayed_sample = self.past[-self.delay_time] + else: + delayed_sample = np.zeros(self.sample_size) + + # Calculate the echo effect + echo_sample = ( + current_sample + delayed_sample * self.feedback + ) * self.wet_dry_mix + + # Store the current sample in the past buffer + self.past = np.roll(self.past, shift=-1, axis=0) + self.past[-1] = current_sample + + # Combine the original sound and the echo + output[i] = current_sample * (1 - self.wet_dry_mix) + echo_sample + + return output + + +class Synth2: + def __init__( + self, + fourier_nn, + stdout: Queue = None, + port_name=None, + ): + self.stdout = stdout + self.live_synth: Process = None + self.notes_ready = False + self.fourier_nn: FourierNN = fourier_nn + self.sample_rate = 44100 # Sample rate + self.max_parralel_notes = 3 + self.current_frame = 0 + t = np.array( + [ + utils.midi_to_freq(f) * np.linspace(0, 2 * np.pi, self.sample_rate) + for f in range(128) + ] + ) + self.t_buffer = torch.tensor(t, dtype=torch.float32) + print(self.t_buffer.shape) + self.port_name = port_name + self.command_queue = multiprocessing.Queue() + + def start_recording(self, file_name): + self.command_queue.put((START, file_name)) + + def stop_recording(self): + self.command_queue.put((STOP,)) + + def pause_recording(self): + self.command_queue.put((PAUSE,)) + + def set_port_name(self, port_name): + self.port_name = port_name + + def play_init_sound(self): + f1 = 440 # Frequency of the "du" sound (in Hz) + f2 = 660 # Frequency of the "dl" sound (in Hz) + f3 = 880 # Frequency of the "de" sound (in Hz) + f4 = 1320 # Frequency of the "dii" sound (in Hz) + t1 = 0.25 # Duration of the "du" sound (in seconds) + t2 = 0.25 # Duration of the "dl" sound (in seconds) + t3 = 0.25 # Duration of the "de" sound (in seconds) + t4 = 0.4 # Duration of the "dii" sound (in seconds) + + # Generate the "duuu" sound + t = np.arange(int(t1 * self.sample_rate)) / self.sample_rate + sound1 = 0.5 * np.sin(2 * np.pi * f1 * t) + + # Generate the "dl" sound + t = np.arange(int(t2 * self.sample_rate)) / self.sample_rate + sound2 = 0.5 * np.sin(2 * np.pi * f2 * t) + + # Generate the "diii" sound + t = np.arange(int(t3 * self.sample_rate)) / self.sample_rate + sound3 = 0.5 * np.sin(2 * np.pi * f3 * t) + + # Generate the "dub" sound + t = np.arange(int(t4 * self.sample_rate)) / self.sample_rate + sound4 = 0.5 * np.sin(2 * np.pi * f4 * t) + + # Concatenate the sounds to form "duuudldiiidub" + audio = np.concatenate([sound1, sound2, sound3, sound4]) + output = np.array(audio * 32767 / np.max(np.abs(audio)) / 2).astype(np.int16) + sd.play(output, blocking=True) + print("Ready") + + def live_synth_loop(self): + print("Live Synth is running") + + if not self.port_name: + self.port_name = mido.get_input_names()[0] + + p = pyaudio.PyAudio() + CHUNK = 2048 # Increased chunk size + stream = p.open( + format=pyaudio.paFloat32, + channels=1, + # frames_per_buffer=CHUNK, + rate=self.sample_rate, + output=True, + ) + current_frame = 0 + cycle_frame = 0 + self.adsr_envelope = ADSR(0.1, 0.1, 0.75, 0.2, self.sample_rate) + + notes = {} + model = self.fourier_nn.current_model.to(self.fourier_nn.device) + self.play_init_sound() + output_file = None + is_recoding = False + + with mido.open_input(self.port_name) as midi_input: + while True: + if not self.command_queue.empty(): + c = self.command_queue.get_nowait() + if is_recoding and not (output_file is None): + if c[0] == STOP: + output_file.close() + output_file = None + is_recoding = False + elif c[0] == PAUSE: + is_recoding = False + else: + if c[0] == START: + is_recoding = True + output_file = wave.open(c[1], "wb") + output_file.setnchannels(1) # Mono + output_file.setsampwidth(4) + output_file.setframerate(self.sample_rate) + # for _ in utils.timed_loop(True): + available_buffer = stream.get_write_available() + if available_buffer == 0: + continue + midi_event = midi_input.poll() + if midi_event: + print(midi_event) + # midi_event.type + # midi_event.note + # midi_event.velocity + if midi_event.type == "note_on": # Note on + print( + "Note on", + midi_event.note, + utils.midi_to_freq(midi_event.note), + ) + + notes[midi_event.note] = [True, midi_event.velocity] + self.adsr_envelope.reset_note(midi_event.note) + elif midi_event.type == "note_off": # Note off + print( + "Note off", + midi_event.note, + utils.midi_to_freq(midi_event.note), + ) + if midi_event.note in notes: + # del notes[midi_event.note] + notes[midi_event.note][0] = False + + if len(notes) > 0: + print(f"available_buffer: {available_buffer}") + # y = torch.zeros(size=(len(notes), available_buffer), + # device=self.fourier_nn.device) + y = np.zeros((len(notes), available_buffer), dtype=np.float32) + to_delete = [] + for i, (note, data) in enumerate(notes.items()): + with torch.no_grad(): + x = utils.wrap_concat( + self.t_buffer[note], + cycle_frame, + cycle_frame + available_buffer, + ).to(self.fourier_nn.device) + if data[0]: + envelope = self.adsr_envelope.get_ads_envelope( + note, available_buffer + ) + else: + envelope = self.adsr_envelope.get_r_envelope( + note, available_buffer + ) + if self.adsr_envelope.has_note_ended(note): + to_delete.append(note) + + model_output = model(x.unsqueeze(1)) + y[i, :] = ( + model_output.cpu().numpy().astype(np.float32) * envelope + ) + # y[i, :] = model( + # x.unsqueeze(1)).cpu().numpy().astype(np.float32) # * envelope # * (amplitude/127) + # y[i, :] = np.sin( + # x.numpy().astype(np.float32)) * envelope + + for note in to_delete: + del notes[note] + + audio_data = sum_signals(y) + # print(np.max(np.abs(audio_data))) + audio_data = normalize(audio_data) + audio_data *= 1 # oscilating_amplitude + audio_data = np.clip(audio_data, -1, 1) + audio_data = audio_data.astype(np.float32) + + if is_recoding: + output_file.writeframes(audio_data) + + stream.write( + audio_data, + available_buffer, + exception_on_underflow=True, + ) + # if cycle_frame <= 100: + # print(f"available_buffer: {available_buffer}") + current_frame = current_frame + available_buffer + cycle_frame = (cycle_frame + available_buffer) % self.sample_rate + + def run_live_synth(self): + if not self.live_synth: + print("spawning live synth") + self.live_synth = Process(target=self.live_synth_loop) + self.live_synth.start() + else: + print("killing live synth") + self.live_synth.terminate() + self.live_synth.join() + self.live_synth = None + print("live synth killed") + atexit.register(utils.DIE, self.live_synth, 0, 0) + + def __getstate__(self) -> object: + Synth_dict = self.__dict__.copy() + del Synth_dict["live_synth"] + # del Synth_dict["command_queue"] + return Synth_dict + + def __setstate__(self, state): + # Load the model from a file after deserialization + self.__dict__.update(state) + if self.stdout is not None: + if current_process().name != "MainProcess": + sys.stdout = utils.QueueSTD_OUT(queue=self.stdout) + + +def normalize(signal): + max_val = np.max(np.abs(signal)) + if np.isnan(max_val) or max_val == 0: + return signal + return signal / np.max(np.abs(signal)) + + +def rms(signal): + return np.sqrt(np.mean(signal**2)) + + +def mix_signals(signal1, signal2): + # Normalize both signals + signal1 = normalize(signal1) + signal2 = normalize(signal2) + + # Calculate RMS levels + rms1 = rms(signal1) + rms2 = rms(signal2) + + # Adjust gain + gain1 = 1 / rms1 + gain2 = 1 / rms2 + + # Apply gain + signal1 *= gain1 + signal2 *= gain2 + + # Mix signals + mixed_signal = signal1 + signal2 + + # Normalize mixed signal to prevent clipping + mixed_signal = normalize(mixed_signal) + + return mixed_signal + + +def sum_signals(signals): + # Normalize each signal + normalized_signals = np.array([normalize(signal) for signal in signals]) + + # Sum the signals along dimension 0 + summed_signal = np.sum(normalized_signals, axis=0) + + # Normalize the combined signal to prevent clipping + summed_signal = normalize(summed_signal) + + return summed_signal + + +def normalize_torch(signal): + return signal / torch.max(torch.abs(signal)) + + +def sum_signals_torch(signals): + # Convert signals to a PyTorch tensor and move to GPU + # signals = torch.stack([signals + # for signal in signals]) + + # Normalize each signal + normalized_signals = torch.stack([sum_signals_torch(signal) for signal in signals]) + + # Sum the signals along dimension 0 + summed_signal = torch.sum(normalized_signals, dim=0) + + # Normalize the combined signal to prevent clipping + summed_signal = normalize(summed_signal) + + return summed_signal diff --git a/pythonaisynth/predefined_functions.py b/pythonaisynth/predefined_functions.py new file mode 100644 index 0000000..99e4a9c --- /dev/null +++ b/pythonaisynth/predefined_functions.py @@ -0,0 +1,133 @@ +import random +import numpy as np + +from pythonaisynth import utils + + +def funny(x): + return np.tan(np.sin(x) * np.cos(x)) + + +def funny2(x): + return np.sin(np.cos(x) * np.maximum(0, x)) / np.cos(x) + + +def funny3(x): + return np.sin(np.cos(x) * np.maximum(0, x)) / np.cos(1 / (x + 0.001)) + + +def my_random(x): + x = x - np.pi + return np.sin(x * np.random.uniform(-1, 1, size=x.shape)) + + +def my_complex_function(x): + x = np.abs(x) # Ensure x is non-negative for relu + return np.where( + x > 0, + np.sin(x) + * (np.sin(np.tan(x) * x) / np.cos(np.random.uniform(-1, 1, size=x.shape) * x)), + -np.sin(-x) + * ( + np.sin(np.tan(-x) * -x) + / np.cos(np.random.uniform(-1, 1, size=x.shape) * -x) + ), + ) + + +def nice(x): + x_greater_pi = False + # x = utils.map_value(x, -np.pi, np.pi, 0, 2*np.pi) + if x >= 2 * np.pi: + x_greater_pi = True + x = x - np.pi + if x > (np.pi / 2): + y = np.sin(np.tan(x)) + elif 0 < x and x < (np.pi / 2): + y = np.cos(-np.tan(x)) + elif (-np.pi / 2) < x and x < 0: + y = np.cos(np.tan(x)) + else: + y = np.sin(-np.tan(x)) + if x_greater_pi: + return -y + return y + + +def nice_2(x): + x = np.array(x) + x_greater_pi = x >= 2 * np.pi + x = np.where(x_greater_pi, x - np.pi, x) + + y = np.where( + x > (np.pi / 2), + np.sin(np.tan(x)), + np.where( + (0 < x) & (x < (np.pi / 2)), + np.cos(-np.tan(x)), + np.where( + ((-np.pi / 2) < x) & (x < 0), np.cos(np.tan(x)), np.sin(-np.tan(x)) + ), + ), + ) + + y = np.where(x_greater_pi, -y, y) + return y + + +def my_generated_function(x): + # Apply a combination of trigonometric functions and activation functions + part1 = np.sin(x) * np.maximum(0, x) + part2 = np.cos(x) * (1 / (1 + np.exp(-x))) # Sigmoid approximation + part3 = np.tan(x) * np.tanh(x) + part4 = np.exp(x) * np.log1p(np.exp(x)) # Softplus approximation + + # Combine the parts to create the final output + # Adding a small value to avoid division by zero + result = part1 + part2 - part3 / (part4 + 1e-7) + return result + + +def extreme(x): + y = np.tile(np.array([-1, 1]), len(x) // 2).flatten() + if len(y) < len(x): + y = np.append(y, [y[-2]]) + return y + + +predefined_functions_dict = { + "funny": funny, + "funny2": funny2, + "funny3": funny3, + "random": my_random, + "cool": my_complex_function, + "bing": my_generated_function, + "nice": nice_2, + "extreme": extreme, + "sin": np.sin, + "cos": np.cos, + "tan": np.tan, + "relu": lambda x: np.maximum(0, x - np.pi), + # ELU approximation + "elu": lambda x: np.where(x - np.pi > 0, x - np.pi, np.expm1(x - np.pi)), + "linear": lambda x: x - np.pi, # Linear function + "sigmoid": lambda x: 1 / (1 + np.exp(-(x - np.pi))), + "exponential": lambda x: np.exp(x - np.pi), + # SELU approximation + "selu": lambda x: np.where( + x - np.pi > 0, 1.0507 * (x - np.pi), 1.0507 * (np.exp(x - np.pi) - 1) + ), + # GELU approximation + "gelu": lambda x: 0.5 + * (x - np.pi) + * ( + 1 + + np.tanh( + np.sqrt(2 / np.pi) * ((x - np.pi) + 0.044715 * np.power((x - np.pi), 3)) + ) + ), +} + + +def call_func(name, x): + return np.clip(predefined_functions_dict[name](x), -1, 1) diff --git a/scr/simple_input_dialog.py b/pythonaisynth/simple_input_dialog.py similarity index 76% rename from scr/simple_input_dialog.py rename to pythonaisynth/simple_input_dialog.py index 401aafa..68db31d 100644 --- a/scr/simple_input_dialog.py +++ b/pythonaisynth/simple_input_dialog.py @@ -3,11 +3,11 @@ class EntryWithPlaceholder(tk.Entry): - def __init__(self, master=None, placeholder="PLACEHOLDER", color='grey'): + def __init__(self, master=None, placeholder="PLACEHOLDER", color="grey"): super().__init__(master) self.placeholder = placeholder self.placeholder_color = color - self.default_fg_color = self['fg'] + self.default_fg_color = self["fg"] self.bind("", self.foc_in) self.bind("", self.foc_out) @@ -16,12 +16,12 @@ def __init__(self, master=None, placeholder="PLACEHOLDER", color='grey'): def put_placeholder(self): self.insert(0, self.placeholder) - self['fg'] = self.placeholder_color + self["fg"] = self.placeholder_color def foc_in(self, *args): - if self['fg'] == self.placeholder_color: - self.delete('0', 'end') - self['fg'] = self.default_fg_color + if self["fg"] == self.placeholder_color: + self.delete("0", "end") + self["fg"] = self.default_fg_color def foc_out(self, *args): if not self.get(): @@ -29,7 +29,16 @@ def foc_out(self, *args): class askStringAndSelectionDialog(simpledialog.Dialog): - def __init__(self, parent, title=None, label_str='', default_str='', label_select='', default_select='', values_to_select_from=[]): + def __init__( + self, + parent, + title=None, + label_str="", + default_str="", + label_select="", + default_select="", + values_to_select_from=[], + ): self.label_str = label_str self.default_str = default_str self.label_select = label_select diff --git a/scr/std_redirect.py b/pythonaisynth/std_redirect.py similarity index 54% rename from scr/std_redirect.py rename to pythonaisynth/std_redirect.py index a686b58..7a8881c 100644 --- a/scr/std_redirect.py +++ b/pythonaisynth/std_redirect.py @@ -7,22 +7,19 @@ import sys from multiprocessing import Queue -from scr import utils -from scr.utils import tk_after_errorless +from pythonaisynth import utils +from .utils import tk_after_errorless class RedirectedOutputFrame(tk.Frame): def __init__(self, master=None, std_queue=None): super().__init__(master) - self.textbox = scrolledtext.ScrolledText( - self, height=10, font=("TkFixedFont")) - self.textbox.configure( - background=master.style.lookup('TFrame', 'background')) - self.textbox.configure( - foreground=master.style.lookup('TFrame', 'foreground')) + self.textbox = scrolledtext.ScrolledText(self, height=10, font=("TkFixedFont")) + self.textbox.configure(background=master.style.lookup("TFrame", "background")) + self.textbox.configure(foreground=master.style.lookup("TFrame", "foreground")) self.textbox.pack(fill=tk.BOTH, expand=True) - self.textbox.configure(state='disabled') - self.textbox.configure(wrap='word') + self.textbox.configure(state="disabled") + self.textbox.configure(wrap="word") self.textbox.bind("", self.on_resize) self.queue: Queue = Queue(-1) if not std_queue else std_queue self.old_stdout = sys.stdout @@ -32,52 +29,71 @@ def __init__(self, master=None, std_queue=None): tk_after_errorless(self, 100, self.check_queue) # dictionaries to replace formatting code with tags - self.ansi_font_format = {1: 'bold', - 3: 'italic', 4: 'underline', 9: 'overstrike'} - self.ansi_font_reset = {21: 'bold', 23: 'italic', - 24: 'underline', 29: 'overstrike'} + self.ansi_font_format = { + 1: "bold", + 3: "italic", + 4: "underline", + 9: "overstrike", + } + self.ansi_font_reset = { + 21: "bold", + 23: "italic", + 24: "underline", + 29: "overstrike", + } # tag configuration - self.textbox.tag_configure('bold', font=('', 9, 'bold')) - self.textbox.tag_configure('italic', font=('', 9, 'italic')) - self.textbox.tag_configure('underline', underline=True) - self.textbox.tag_configure('overstrike', overstrike=True) + self.textbox.tag_configure("bold", font=("", 9, "bold")) + self.textbox.tag_configure("italic", font=("", 9, "italic")) + self.textbox.tag_configure("underline", underline=True) + self.textbox.tag_configure("overstrike", overstrike=True) # dictionaries to replace color code with tags - self.ansi_color_fg = {39: 'foreground default'} - self.ansi_color_bg = {49: 'background default'} + self.ansi_color_fg = {39: "foreground default"} + self.ansi_color_bg = {49: "background default"} - self.textbox.tag_configure( - 'foreground default', foreground=self.textbox["fg"]) - self.textbox.tag_configure( - 'background default', background=self.textbox["bg"]) + self.textbox.tag_configure("foreground default", foreground=self.textbox["fg"]) + self.textbox.tag_configure("background default", background=self.textbox["bg"]) self.ansi_colors_dark = [ - 'black', 'red', 'green', 'yellow', 'royal blue', 'magenta', 'cyan', 'light gray'] - self.ansi_colors_light = ['dark gray', 'tomato', 'light green', - 'light goldenrod', 'light blue', 'pink', 'light cyan', 'white'] - - for i, (col_dark, col_light) in enumerate(zip(self.ansi_colors_dark, self.ansi_colors_light)): - self.ansi_color_fg[30 + i] = 'foreground ' + col_dark - self.ansi_color_fg[90 + i] = 'foreground ' + col_light - self.ansi_color_bg[40 + i] = 'background ' + col_dark - self.ansi_color_bg[100 + i] = 'background ' + col_light + "black", + "red", + "green", + "yellow", + "royal blue", + "magenta", + "cyan", + "light gray", + ] + self.ansi_colors_light = [ + "dark gray", + "tomato", + "light green", + "light goldenrod", + "light blue", + "pink", + "light cyan", + "white", + ] + + for i, (col_dark, col_light) in enumerate( + zip(self.ansi_colors_dark, self.ansi_colors_light) + ): + self.ansi_color_fg[30 + i] = "foreground " + col_dark + self.ansi_color_fg[90 + i] = "foreground " + col_light + self.ansi_color_bg[40 + i] = "background " + col_dark + self.ansi_color_bg[100 + i] = "background " + col_light # tag configuration - self.textbox.tag_configure( - 'foreground ' + col_dark, foreground=col_dark) - self.textbox.tag_configure( - 'background ' + col_dark, background=col_dark) - self.textbox.tag_configure( - 'foreground ' + col_light, foreground=col_light) - self.textbox.tag_configure( - 'background ' + col_light, background=col_light) + self.textbox.tag_configure("foreground " + col_dark, foreground=col_dark) + self.textbox.tag_configure("background " + col_dark, background=col_dark) + self.textbox.tag_configure("foreground " + col_light, foreground=col_light) + self.textbox.tag_configure("background " + col_light, background=col_light) # regular expression to find ansi codes in string self.ansi_regexp = re.compile(r"\x1b\[((\d+;)*\d+)m") def insert_ansi(self, txt, index="insert"): - first_line, first_char = map( - int, str(self.textbox.index(index)).split(".")) + first_line, first_char = map(int, str(self.textbox.index(index)).split(".")) if index == "end": first_line -= 1 @@ -85,7 +101,7 @@ def insert_ansi(self, txt, index="insert"): if not lines: return # insert text without ansi codes - self.textbox.insert(index, self.ansi_regexp.sub('', txt)) + self.textbox.insert(index, self.ansi_regexp.sub("", txt)) # find all ansi codes in txt and apply corresponding tags opened_tags = {} # we need to keep track of the opened tags to be able to do # self.textbox.tag_add(tag, start, end) when we reach a "closing" ansi code @@ -98,7 +114,7 @@ def apply_formatting(code, code_index): elif code in self.ansi_font_format: # open font formatting tag tag = self.ansi_font_format[code] opened_tags[tag] = code_index - elif code in self.ansi_font_reset: # close font formatting tag + elif code in self.ansi_font_reset: # close font formatting tag tag = self.ansi_font_reset[code] if tag in opened_tags: self.textbox.tag_add(tag, opened_tags[tag], code_index) @@ -106,14 +122,14 @@ def apply_formatting(code, code_index): # open foreground color tag (and close previously opened one if any) elif code in self.ansi_color_fg: for tag in list(opened_tags): - if tag.startswith('foreground'): + if tag.startswith("foreground"): self.textbox.tag_add(tag, opened_tags[tag], code_index) del opened_tags[tag] opened_tags[self.ansi_color_fg[code]] = code_index # open background color tag (and close previously opened one if any) elif code in self.ansi_color_bg: for tag in list(opened_tags): - if tag.startswith('background'): + if tag.startswith("background"): self.textbox.tag_add(tag, opened_tags[tag], code_index) del opened_tags[tag] opened_tags[self.ansi_color_bg[code]] = code_index @@ -123,60 +139,66 @@ def find_ansi(line_txt, line_nb, char_offset): delta = -char_offset # (initial offset due to insertion position if first line + extra offset due to deletion of ansi codes) for match in self.ansi_regexp.finditer(line_txt): - codes = [int(c) for c in match.groups()[0].split(';')] + codes = [int(c) for c in match.groups()[0].split(";")] start, end = match.span() for code in codes: - apply_formatting(code, "{}.{}".format( - line_nb, start - delta)) - delta += end - start # take into account offset due to deletion of ansi code + apply_formatting(code, "{}.{}".format(line_nb, start - delta)) + delta += ( + end - start + ) # take into account offset due to deletion of ansi code # first line, with initial offset due to insertion position find_ansi(lines[0], first_line, first_char) for line_nb, line in enumerate(lines[1:], first_line + 1): - find_ansi(line, line_nb, 0) # next lines, no offset + find_ansi(line, line_nb, 0) # next lines, no offset # close still opened tag for tag, start in opened_tags.items(): self.textbox.tag_add(tag, start, "end") def redirector(self, inputStr): - self.textbox.configure(state='normal') + self.textbox.configure(state="normal") self.textbox.mark_set("insert", "end") - while '\b' in inputStr: - bs_index = inputStr.index('\b') + while "\b" in inputStr: + bs_index = inputStr.index("\b") if bs_index == 0: # Delete the character before the current 'insert' position in the Text widget self.textbox.delete("insert-2c") inputStr = inputStr[1:] else: # Remove the character before the backspace and the backspace itself - inputStr = inputStr[:bs_index-1] + inputStr[bs_index+1:] + inputStr = inputStr[: bs_index - 1] + inputStr[bs_index + 1 :] self.insert_ansi(inputStr, tk.INSERT) # self.textbox.insert(tk.INSERT, inputStr) - self.textbox.configure(state='disabled') + self.textbox.configure(state="disabled") self.textbox.see(tk.END) # Auto-scroll to the end self.old_stdout.write(inputStr) def on_resize(self, event): width = self.textbox.winfo_width() font_width = self.textbox.tk.call( - "font", "measure", self.textbox["font"], "-displayof", self.textbox, "0") + "font", "measure", self.textbox["font"], "-displayof", self.textbox, "0" + ) sys.stdout.write = self.redirector def check_queue(self): try: # print("asdfasdf") - while not self.queue.empty(): - try: - msg = self.queue.get_nowait() - self.redirector(msg) - except queue.Empty: - break - try: - tk_after_errorless(self.master, 100, self.check_queue) - except: - print("ERROR") + if not self.queue.empty(): + for i in range(100): + try: + msg = self.queue.get_nowait() + self.redirector(msg) + # self.old_stdout.write(str(self.queue.qsize())+'\n') + except queue.Empty: + break except BrokenPipeError: - pass + return + # delay = 100 + delay = int( + 500 if self.queue.empty() else min(5, 100 / min(1, self.queue.qsize() / 10)) + ) + # self.old_stdout.write(f'Delay: {delay}\n') + tk_after_errorless(self.master, delay, self.check_queue) def __del__(self): sys.stdout = self.old_stdout diff --git a/pythonaisynth/synth_gui.py b/pythonaisynth/synth_gui.py new file mode 100644 index 0000000..a7d48d4 --- /dev/null +++ b/pythonaisynth/synth_gui.py @@ -0,0 +1,40 @@ +import mido +import tkinter as tk +from tkinter import ttk + + +class SynthGUI(ttk.Frame): + def __init__(self, parent=None, **kwargs): + ttk.Frame.__init__(self, parent, **kwargs) + self.columnconfigure(0, weight=1) + self.columnconfigure(1, weight=1) + + param = "MIDI PORT" + label = ttk.Label(self, text=param) + label.grid(row=0, column=0, sticky="NSW") + options = mido.get_input_names() + self.midi_port_var = tk.StringVar(self, options[0]) + self.midi_port_option_menu = ttk.OptionMenu( + self, self.midi_port_var, options[0], *(options) + ) + self.midi_port_option_menu.grid(row=0, column=1, sticky="NSE") + # self.midi_port_var.trace_add('write', lambda *args, key=param, + # var=self.midi_port_var: self.on_change(key, var.get(), var)) + + def get_port_name(self): + return self.midi_port_var.get() + + # def on_change(self, name, value, var): + # if hasattr(self, 'on_change_callback'): + # if self.on_change_callback: + # worked = self.on_change_callback(name, value) + # # print(worked) + # if worked: + # self.previous_values[name] = value + # else: + # var.set(self.previous_values[name]) + + # def set_on_change(self, func): + # self.on_change_callback = func + # self.previous_values = {param: var.get() + # for param, var in self.params.items()} diff --git a/scr/utils.py b/pythonaisynth/utils.py similarity index 77% rename from scr/utils.py rename to pythonaisynth/utils.py index 2c96391..b1d4755 100644 --- a/scr/utils.py +++ b/pythonaisynth/utils.py @@ -5,7 +5,7 @@ import sys import threading import time -from typing import Any, Callable, TextIO +from typing import Any, Callable, TextIO, Tuple import numpy as np import psutil from scipy.fft import dst @@ -28,6 +28,28 @@ def write(self, msg): except EOFError: exit() + def flush(self): + pass + + def fileno(self): + raise IOError("This TextIO does not have a file descriptor.") + + def isatty(self): + return sys.stdout.isatty() + + def readable(self): + return True + + def writable(self): + return True + + def seekable(self): + return False + + def writelines(self, lines): + for line in lines: + self.write(line) + def DIE(process: Process, join_timeout=30, term_iterations=50): if process: @@ -65,19 +87,19 @@ def calculate_max_batch_size(num_features, dtype=np.float32, memory_buffer=0.1): def messure_time_taken(name, func, *args, wait=True, **kwargs): - if not hasattr(messure_time_taken, 'time_taken'): + if not hasattr(messure_time_taken, "time_taken"): messure_time_taken.time_taken = {} if name not in messure_time_taken.time_taken: messure_time_taken.time_taken[name] = 0 timestamp = time.perf_counter_ns() result = func(*args, **kwargs) - time_taken = (time.perf_counter_ns()-timestamp)/1_000_000 # _000 - print( - f"Time taken for {name}: {time_taken:6.6f}ms") + time_taken = (time.perf_counter_ns() - timestamp) / 1_000_000 # _000 + print(f"Time taken for {name}: {time_taken:6.6f}ms") if wait: input("paused") messure_time_taken.time_taken[name] = max( - messure_time_taken.time_taken[name], time_taken) + messure_time_taken.time_taken[name], time_taken + ) return result @@ -91,13 +113,13 @@ def midi_to_freq(midi_note): def pair_iterator(lst): for i in range(len(lst) - 1): - yield lst[i], lst[i+1] + yield lst[i], lst[i + 1] def note_iterator(lst): - for i in range(len(lst)-1): - yield lst[i], lst[i+1] - yield lst[len(lst)-1], None + for i in range(len(lst) - 1): + yield lst[i], lst[i + 1] + yield lst[len(lst) - 1], None def find_two_closest(num_list, x): @@ -108,7 +130,7 @@ def find_two_closest(num_list, x): return sorted_list[:2] -@ njit +@njit def interpolate(point1, point2, t=0.5): """ Interpolate between two 2D points. @@ -130,7 +152,7 @@ def interpolate(point1, point2, t=0.5): return (x, y) -@ njit +@njit def interpolate_vectorized(point1, point2, t_values): """ Vectorized interpolation between two 2D points. @@ -183,8 +205,8 @@ def map_value_old(x, a1, a2, b1, b2): def random_hex_color(): # Generate a random color in hexadecimal format - color = '{:06x}'.format(random.randint(0x111111, 0xFFFFFF)) - return '#' + color + color = "{:06x}".format(random.randint(0x111111, 0xFFFFFF)) + return "#" + color def random_color(): @@ -193,7 +215,7 @@ def random_color(): def get_prepared_random_color(maxColors=None): - if not hasattr(get_prepared_random_color, 'colors'): + if not hasattr(get_prepared_random_color, "colors"): get_prepared_random_color.colors = [] for i in range(maxColors or 100): while True: @@ -222,22 +244,24 @@ def exec_with_queue_stdout(func: Callable, *args, queue: Queue): def calculate_peak_frequency(signal): - np.savetxt('tmp/why_do_i_need_this_array.txt', signal) - test_data = np.loadtxt('tmp/why_do_i_need_this_array.txt') + np.savetxt("tmp/why_do_i_need_this_array.txt", signal) + test_data = np.loadtxt("tmp/why_do_i_need_this_array.txt") test_data = test_data - np.mean(test_data) # Perform the Discrete Sine Transform (DST) - transformed_signal = dst(test_data, type=4, norm='ortho') + transformed_signal = dst(test_data, type=4, norm="ortho") peak_index = np.argmax(np.abs(transformed_signal)) - peak_freq = (peak_index+1)/2 + peak_freq = (peak_index + 1) / 2 return peak_freq timers = [] -def run_after_ms(delay_ms: int, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None: +def run_after_ms( + delay_ms: int, func: Callable[..., Any], *args: Any, **kwargs: Any +) -> None: """ Schedules a function to be run after a specified delay in milliseconds. @@ -267,7 +291,9 @@ def cleanup_timers(): atexit.register(cleanup_timers) -def tk_after_errorless(master: tk.Tk, delay_ms: int, func: Callable[..., Any], *args: Any): +def tk_after_errorless( + master: tk.Tk, delay_ms: int, func: Callable[..., Any], *args: Any +): def safe_call(master: tk.Tk, func: Callable[..., Any], *args: Any): if master.winfo_exists(): func(*args) @@ -296,7 +322,9 @@ def get_loss_function(loss_name, **kwargs): raise ValueError(f"Loss function '{loss_name}' not found in torch.nn") -def linear_interpolation(data, target_length, device='cpu'): +def linear_interpolation( + data, target_length, device="cpu" +) -> Tuple[torch.Tensor, torch.Tensor]: # Convert data to a tensor and move to the specified device data = np.array(data) data_tensor = torch.tensor(data, dtype=torch.float32).to(device) @@ -306,16 +334,22 @@ def linear_interpolation(data, target_length, device='cpu'): y_values = data_tensor[:, 1] # Generate new x values - new_x_values = torch.linspace( - x_values.min(), x_values.max(), target_length).to(device) + new_x_values = torch.linspace(x_values.min(), x_values.max(), target_length).to( + device + ) # Perform linear interpolation - new_y_values = F.interpolate(y_values.unsqueeze(0).unsqueeze(0), - size=new_x_values.size(0), - mode='linear', - align_corners=True).squeeze() + new_y_values = F.interpolate( + y_values.unsqueeze(0).unsqueeze(0), + size=new_x_values.size(0), + mode="linear", + align_corners=True, + ).squeeze() - return (new_x_values.cpu(), new_y_values.cpu(),) + return ( + new_x_values.cpu(), + new_y_values.cpu(), + ) def center_and_scale(arr): @@ -374,13 +408,39 @@ def timed_generator(iterable): yield item end_time = time.time() print( - f"Time taken for this iteration: {((end_time - start_time)/1_000_000_000):10.9f}s") + f"Time taken for this iteration: {((end_time - start_time)/1_000_000_000):10.9f}s" + ) def timed_loop(condition): + """ + A generator that runs a timed loop while the condition is true. + + Args: + condition (bool): A condition that controls the loop. + + Yields: + None: Control is yielded back to the caller. + """ + max_val = 0 + total_time = 0 + iteration_count = 0 + while condition: start_time = time.perf_counter_ns() - yield - end_time = time.perf_counter_ns() - print( - f"Time taken for this iteration: {((end_time - start_time)/1_000_000_000):10.9f}ms") + yield # Yield control back to the caller + current_time = time.perf_counter_ns() - start_time + + max_val = max(current_time, max_val) + total_time += current_time + iteration_count += 1 + + if total_time >= 100_000_000: # 100 milliseconds + avg_time = ( + total_time / iteration_count + ) / 1_000_000 # Convert to milliseconds + max_time = max_val / 1_000_000 # Convert to milliseconds + print( + f"Time taken for this iteration: {avg_time:10.9f} ms, max_val: {max_time:10.3f} ms" + ) + total_time, iteration_count = 0, 0 # Reset for the next period diff --git a/scr/fourier_neural_network_gui.py b/scr/fourier_neural_network_gui.py deleted file mode 100644 index 91c740a..0000000 --- a/scr/fourier_neural_network_gui.py +++ /dev/null @@ -1,116 +0,0 @@ -import torch.optim as optim -import torch.nn as nn -import tkinter as tk -from tkinter import ttk - - -class NeuralNetworkGUI(ttk.Frame): - def __init__(self, parent=None, defaults: dict = None, callback=None, **kwargs): - ttk.Frame.__init__(self, parent, **kwargs) - self.on_change_callback = callback - self.columnconfigure(0, weight=1) - self.columnconfigure(1, weight=1) - - # Define the list of loss functions and optimizers - self.loss_functions = [func for func in dir(nn) - if not func.startswith('_') - and isinstance(getattr(nn, func), type) - and issubclass(getattr(nn, func), nn.modules.loss._Loss)] - self.loss_functions.append("CustomHuberLoss") - self.optimizers_list = [opt for opt in dir(optim) - if not opt.startswith('_') - and isinstance(getattr(optim, opt), type) - and issubclass(getattr(optim, opt), optim.Optimizer)] - print("".center(40, "-"), - *self.loss_functions, - "".center(40, "-"), - *self.optimizers_list, - "".center(40, "-"), - sep="\n") - # exit(0) - # Create labels and entry fields for the parameters - self.params = ['SAMPLES', 'EPOCHS', 'DEFAULT_FORIER_DEGREE', 'CALC_FOURIER_DEGREE_BY_DATA_LENGTH', - 'FORIER_DEGREE_DIVIDER', 'FORIER_DEGREE_OFFSET', 'PATIENCE', 'OPTIMIZER', 'LOSS_FUNCTION'] - - self.previous_values = {} - - for i, param in enumerate(self.params): - label = ttk.Label(self, text=param) - label.grid(row=i, column=0, sticky='NSW') - default = defaults.get( - param, 0 if param != 'CALC_FOURIER_DEGREE_BY_DATA_LENGTH' else False) - var = tk.BooleanVar(self, value=default) if param == 'CALC_FOURIER_DEGREE_BY_DATA_LENGTH' else tk.StringVar(self, value=default) if param in ['OPTIMIZER', 'LOSS_FUNCTION'] else tk.IntVar( - self, value=default) - var.trace_add('write', lambda *args, key=param, - var=var: self.on_change(key, var.get(), var)) - self.previous_values[param] = default - if param in ['OPTIMIZER', 'LOSS_FUNCTION']: - # set the default option - used_list = self.optimizers_list if param == 'OPTIMIZER' else self.loss_functions - try: - default = used_list[used_list.index(defaults.get(param))] - except ValueError: - default = used_list[0] - - var.set(default) - - dropdown = ttk.OptionMenu( - self, var, *(used_list)) - dropdown.grid(row=i, column=1, sticky='NSEW') - elif param == 'CALC_FOURIER_DEGREE_BY_DATA_LENGTH': - entry = ttk.Checkbutton(self, variable=var) - entry.grid(row=i, column=1, sticky='NSEW') - else: - entry = ttk.Entry(self, textvariable=var) - entry.grid(row=i, column=1, sticky='NSEW') - var.set(default) - - def on_change(self, name, value, var): - if hasattr(self, 'on_change_callback'): - if self.on_change_callback: - worked = self.on_change_callback(name, value) - # print(worked) - if worked: - self.previous_values[name] = value - else: - var.set(self.previous_values[name]) - - def set_on_change(self, func): - self.on_change_callback = func - self.previous_values = {param: var.get() - for param, var in self.params.items()} - - -if __name__ == "__main__": - - def stupid(*args, **kwargs): - print(*args, *kwargs.items(), sep="\n") - return False - # Usage - root = tk.Tk() - root.rowconfigure(0, weight=1) - root.columnconfigure(0, weight=1) - defaults = { - 'SAMPLES': 44100//2, - 'EPOCHS': 100, - 'DEFAULT_FORIER_DEGREE': 250, - 'FORIER_DEGREE_DIVIDER': 1, - 'FORIER_DEGREE_OFFSET': 0, - 'PATIENCE': 10, - 'OPTIMIZER': 'Adam', - 'LOSS_FUNCTION': 'mse_loss' - } - gui = NeuralNetworkGUI(root, defaults=defaults, callback=stupid) - gui.grid(row=0, column=0, sticky='NSEW') - root.mainloop() - - exit() - - # autopep8: off - - loss_functions = [func for func in dir(nn) if not func.startswith('_') and isinstance(getattr(nn, func), type) and issubclass(getattr(nn, func), nn.modules.loss._Loss)] - print(loss_functions) - - - optimizers_list = [opt for opt in dir(optim) if not opt.startswith('_') and isinstance(getattr(optim, opt), type) and issubclass(getattr(optim, opt), optim.Optimizer)] - print(optimizers_list) diff --git a/scr/music.py b/scr/music.py deleted file mode 100644 index 9686c21..0000000 --- a/scr/music.py +++ /dev/null @@ -1,586 +0,0 @@ -import copy -import threading -import time -from typing import Callable -import pygame -import scipy -import torch -from scr.fourier_neural_network import FourierNN -from scr import utils -from pygame import mixer, midi, sndarray -import atexit -import multiprocessing -from multiprocessing import Process, Queue, current_process -import os -import sys -from tkinter import filedialog -import numpy as np -import pretty_midi -import sounddevice as sd -import pyaudio - - -def musik_from_file(fourier_nn: FourierNN): - import sounddevice as sd - midi_file = filedialog.askopenfilename() - if not midi_file: - return - print(midi_file) - midi_data = pretty_midi.PrettyMIDI(midi_file) - # print(dir(midi_data)) - notes_list = [] - # Iterate over all instruments and notes in the MIDI file - if fourier_nn: - for instrument in midi_data.instruments: - # print(dir(instrument)) - note_list = [] - for note_a, note_b in utils.note_iterator(instrument.notes): - # node_a - print(note_a) - duration = note_a.end - note_a.start - synthesized_note = fourier_nn.synthesize( - midi_note=note_a.pitch-12, duration=duration, sample_rate=44100) - print(synthesized_note.shape) - note_list.append(synthesized_note) - - if note_b: - # pause - print("Pause") - duration = abs(note_b.start - note_a.end) - synthesized_note = np.zeros((int(duration*44100), 1)) - print(synthesized_note.shape) - note_list.append(synthesized_note) - - # # node_b - # print(note_b) - # duration = note_b.end - note_b.start - # synthesized_note = fourier_nn.synthesize_2( - # midi_note=note_b.pitch, duration=duration, sample_rate=44100) - # print(synthesized_note.shape) - # notes_list.append(synthesized_note) - notes_list.append(np.concatenate(note_list)) - - output = np.sum(notes_list) - output = rescale_audio(output) - sd.play(output, 44100) - - -def rescale_audio(audio): - max_val = max(abs(audio.min()), audio.max()) - if max_val == 0: - return audio - return audio / max_val - - -class Synth(): - - MIDI_NOTE_OF_FREQ_ONE = midi.frequency_to_midi(1) - - def __init__(self, fourier_nn, stdout: Queue = None, num_channels: int = 20): - self.stdout = stdout - self.pool = None - self.live_synth: Process = None - self.notes_ready = False - self.fourier_nn: FourierNN = fourier_nn - self.fs = 44100 # Sample rate - self.num_channels = num_channels - self.notes: dict = {} - self.effects: list[Callable] = [ - # apply_reverb, - # apply_echo, - # apply_chorus, - # apply_distortion, - ] - # pygame.init() - mixer.init(frequency=44100, size=-16, - channels=2, buffer=1024) - mixer.set_num_channels(self.num_channels) - - self.running_channels: dict[str, tuple[int, mixer.Channel]] = {} - self.free_channel_ids = list(range( - self.num_channels)) - # self.generate_sounds() - - def generate_sound_wrapper(self, x, offset): - midi_note, data = self.fourier_nn.synthesize_tuple(x) - return midi_note+offset, data - - def generate_sounds(self): - self.notes_ready = False - - timestep = 44100 - - t = np.arange(0, 1, step=1 / timestep) - - data = self.fourier_nn.predict((2 * np.pi*t)-np.pi) - - peak_frequency = utils.calculate_peak_frequency(data) - - print("peak_frequency", peak_frequency) - - midi_offset = midi.frequency_to_midi(peak_frequency) \ - - self.MIDI_NOTE_OF_FREQ_ONE - print("Midi Note offset:", midi_offset) - self.pool = multiprocessing.Pool(processes=os.cpu_count()) - atexit.register(self.pool.terminate) - atexit.register(self.pool.join) - result_async = self.pool.starmap_async(self.generate_sound_wrapper, - ((x-midi_offset, midi_offset) for x in range(128))) - utils.run_after_ms(1000, self.monitor_note_generation, result_async) - - def monitor_note_generation(self, result_async): - try: - self.note_list = result_async.get(0.1) - except multiprocessing.TimeoutError: - utils.run_after_ms( - 1000, self.monitor_note_generation, result_async) - else: - print("sounds Generated") - - self.notes_ready = True - self.play_init_sound() - self.pool.close() - self.pool.join() - atexit.unregister(self.pool.terminate) - atexit.unregister(self.pool.join) - - def play_init_sound(self): - f1 = 440 # Frequency of the "duuu" sound (in Hz) - f2 = 880 # Frequency of the "dib" sound (in Hz) - t1 = 0.8 # Duration of the "duuu" sound (in seconds) - t2 = 0.2 # Duration of the "dib" sound (in seconds) - t = np.arange(int(t1 * self.fs)) / self.fs - sound1 = 0.5 * np.sin(2 * np.pi * f1 * t) - - # Generate the "dib" sound - t = np.arange(int(t2 * self.fs)) / self.fs - sound2 = 0.5 * np.sin(2 * np.pi * f2 * t) - - # Concatenate the two sounds - audio = np.concatenate([sound1, sound2]) - output = np.array( - audio * 32767 / np.max(np.abs(audio)) / 2).astype(np.int16) - stereo_sine_wave = np.repeat(output.reshape(-1, 1), 2, axis=1) - sound = sndarray.make_sound(stereo_sine_wave) - - channel = mixer.Channel(0) - channel.play(sound) - while (channel.get_busy()): - pass - print("Ready") - - def apply_effects(self, sound): - for effect in self.effects: - print(effect.__name__) - sound[:] = effect(sound) - sound = sound-np.mean(sound) - sound[:] = rescale_audio(sound) - return sound - - def live_synth_loop(self): - midi.init() - mixer.init(frequency=44100, size=-16, - channels=2, buffer=1024) - mixer.set_num_channels(self.num_channels) - for note, sound in self.note_list: - # print(sound.shape) - stereo = np.repeat(rescale_audio(sound).reshape(-1, 1), 2, axis=1) - stereo = np.array(stereo, dtype=np.int16) - # np.savetxt(f"tmp/numpy/sound_note_{note}.numpy", stereo) - stereo_sound = pygame.sndarray.make_sound(stereo) - self.notes[note] = stereo_sound - - input_id = midi.get_default_input_id() - if input_id == -1: - print("No MIDI input device found.") - return - - midi_input = midi.Input(input_id) - print("Live Synth is running") - while True: - if midi_input.poll(): - midi_events = midi_input.read(10) - for midi_event, timestamp in midi_events: - if midi_event[0] == 144: - print("Note on", midi_event[1]) - try: - id = self.free_channel_ids.pop() - channel = mixer.Channel(id) - channel.set_volume(1) - channel.play( - self.notes[midi_event[1]], fade_ms=10, loops=-1) - self.running_channels[midi_event[1]] = ( - id, channel,) - except IndexError: - print("to many sounds playing") - elif midi_event[0] == 128: - print("Note off", midi_event[1]) - self.free_channel_ids.append( - self.running_channels[midi_event[1]][0]) - self.running_channels[midi_event[1]][1].stop() - - def pending_for_live_synth(self): - if not self.notes_ready: - # print("pending") - utils.run_after_ms(500, self.pending_for_live_synth) - else: - self.run_live_synth() - - def run_live_synth(self): - if not self.notes_ready: - print("\033[31;1;4mNOT READY YET\033[0m") - self.generate_sounds() - utils.run_after_ms(500, self.pending_for_live_synth) - return - if not self.live_synth: - print("spawning live synth") - self.live_synth = Process(target=self.live_synth_loop) - self.live_synth.start() - else: - print("killing live synth") - self.live_synth.terminate() - self.live_synth.join() - self.live_synth = None - print("live synth killed") - atexit.register(utils.DIE, self.live_synth, 0, 0) - - def play_sound(self, midi_note): - id = self.free_channel_ids.pop() - channel = mixer.Channel(id) - channel.play(self.notes[midi_note], 200) - - def __getstate__(self) -> object: - pool = self.pool - live_synth = self.live_synth - del self.pool - del self.live_synth - Synth_dict = self.__dict__.copy() - self.pool = pool - self.live_synth = live_synth - return Synth_dict - - def __setstate__(self, state): - # Load the model from a file after deserialization - self.__dict__.update(state) - if self.stdout is not None: - if current_process().name != 'MainProcess': - sys.stdout = utils.QueueSTD_OUT(queue=self.stdout) - - -def get_raw_audio(sound): - return pygame.sndarray.array(sound) - - -def apply_reverb(audio, decay=0.5, delay=0.02, fs=44100): - delay_samples = int(delay * fs) - impulse_response = np.zeros((delay_samples, 2)) - impulse_response[0, :] = 1 - impulse_response[-1, :] = decay - reverb_audio = np.zeros_like(audio) - for channel in range(audio.shape[1]): - reverb_audio[:, channel] = scipy.signal.fftconvolve( - audio[:, channel], impulse_response[:, channel])[:len(audio)] - return reverb_audio - - -def apply_echo(audio, delay=0.2, decay=0.5, fs=44100): - delay_samples = int(delay * fs) - echo_audio = np.zeros((len(audio) + delay_samples, audio.shape[1])) - echo_audio[:len(audio), :] = audio - echo_audio[delay_samples:, :] += decay * audio - echo_audio = echo_audio[:len(audio), :] - return echo_audio - - -def apply_chorus(audio, rate=1.5, depth=0.02, fs=44100): - t = np.arange(len(audio)) / fs - mod = depth * np.sin(2 * np.pi * rate * t) - chorus_audio = np.zeros_like(audio) - for channel in range(audio.shape[1]): - for i in range(len(audio)): - delay = int(mod[i] * fs) - if i + delay < len(audio): - chorus_audio[i, channel] = audio[i + delay, channel] - else: - chorus_audio[i, channel] = audio[i, channel] - return chorus_audio - - -def apply_distortion(audio, gain=2.0, threshold=0.5): - distorted_audio = gain * audio - distorted_audio[distorted_audio > threshold] = threshold - distorted_audio[distorted_audio < -threshold] = -threshold - return distorted_audio - - -class Synth2(): - # MIDI_NOTE_OF_FREQ_ONE = midi.frequency_to_midi(1) - def __init__(self, fourier_nn, stdout: Queue = None): - self.stdout = stdout - self.live_synth: Process = None - self.notes_ready = False - self.fourier_nn: FourierNN = fourier_nn - self.fs = 44100 # Sample rate - self.max_parralel_notes = 3 - self.current_notes: set = set() - self.effects: list[Callable] = [ - # apply_reverb, - # apply_echo, - # apply_chorus, - # apply_distortion, - ] - self.current_frame = 0 - t = np.array([2 * np.pi * midi.midi_to_frequency(f) * - np.linspace(0, 1, self.fs) for f in range(128)]) - self.t_buffer = torch.tensor(t, dtype=torch.float32) - - def play_init_sound(self): - f1 = 440 # Frequency of the "duuu" sound (in Hz) - f2 = 880 # Frequency of the "dib" sound (in Hz) - t1 = 0.8 # Duration of the "duuu" sound (in seconds) - t2 = 0.2 # Duration of the "dib" sound (in seconds) - t = np.arange(int(t1 * self.fs)) / self.fs - sound1 = 0.5 * np.sin(2 * np.pi * f1 * t) - - # Generate the "dib" sound - t = np.arange(int(t2 * self.fs)) / self.fs - sound2 = 0.5 * np.sin(2 * np.pi * f2 * t) - - # Concatenate the two sounds - audio = np.concatenate([sound1, sound2]) - output = np.array( - audio * 32767 / np.max(np.abs(audio)) / 2).astype(np.int16) - # stereo_sine_wave = np.repeat(output.reshape(-1, 1), 2, axis=1) - sd.play(output, blocking=True) - print("Ready") - - def audio_callback(self, outdata, frames, time, status): - current_notes = set(self.current_notes) - # print(current_notes, self.current_notes) - if status: - print(status) - with torch.no_grad(): - for j, note in enumerate(current_notes): - # pass - if j >= self.max_parralel_notes: - break - self.pre_audio_buffer[j, :] = self.fourier_nn.current_model( - utils.wrap_concat(self.t_buffer[note], self.current_frame, self.current_frame+frames)) - y = torch.clamp(torch.sum(self.pre_audio_buffer, dim=0), - min=-1, max=1).cpu().numpy() - # print(y.shape) - outdata[:] = y.reshape(-1, 1) - self.current_frame = (self.current_frame + frames) % self.fs - - def apply_effects(self, sound): - for effect in self.effects: - print(effect.__name__) - sound[:] = effect(sound) - sound = sound-np.mean(sound) - sound[:] = rescale_audio(sound) - return sound - - def live_synth_loop(self): - print("Live Synth is running") - self.fourier_nn.current_model.to(self.fourier_nn.device) - self.t_buffer = self.t_buffer.to(self.fourier_nn.device) - stream = sd.OutputStream( - callback=lambda *args, **kwargs: utils.messure_time_taken('audio_callback', self.audio_callback, *args, **kwargs, wait=False), samplerate=self.fs, channels=1, blocksize=512) - self.play_init_sound() - midi_thread = threading.Thread(target=self.midi_thread, daemon=True) - midi_thread.start() - - self.pre_audio_buffer = torch.zeros((self.max_parralel_notes, 512), - device=self.fourier_nn.device) - with stream: - while midi_thread.is_alive(): - time.sleep(0.1) - if hasattr(utils.messure_time_taken, 'time_taken'): - print(*utils.messure_time_taken.time_taken.items(), sep="\n") - - def midi_thread(self): - midi.init() - input_id = midi.get_default_input_id() - # print(input_id) - if input_id == -1: - print("No MIDI input device found.") - return - midi_input = midi.Input(input_id) - while True: - if midi_input.poll(): - midi_events = midi_input.read(10) - for midi_event, timestamp in midi_events: - if midi_event[0] == 144: - print("Note on", - midi_event[1], - midi.midi_to_frequency(midi_event[1])) - self.current_notes.add(midi_event[1]) - elif midi_event[0] == 128: - print("Note off", - midi_event[1], - midi.midi_to_frequency(midi_event[1])) - self.current_notes.discard(midi_event[1]) - - def run_live_synth(self): - if not self.live_synth: - print("spawning live synth") - self.live_synth = Process(target=self.live_synth_loop) - self.live_synth.start() - else: - print("killing live synth") - self.live_synth.terminate() - self.live_synth.join() - self.live_synth = None - print("live synth killed") - atexit.register(utils.DIE, self.live_synth, 0, 0) - - def __getstate__(self) -> object: - live_synth = self.live_synth - del self.live_synth - Synth_dict = self.__dict__.copy() - self.live_synth = live_synth - return Synth_dict - - def __setstate__(self, state): - # Load the model from a file after deserialization - self.__dict__.update(state) - if self.stdout is not None: - if current_process().name != 'MainProcess': - sys.stdout = utils.QueueSTD_OUT(queue=self.stdout) - - -class Synth3(): - def __init__(self, fourier_nn, stdout: Queue = None): - self.stdout = stdout - self.live_synth: Process = None - self.notes_ready = False - self.fourier_nn: FourierNN = fourier_nn - self.fs = 44100 # Sample rate - self.max_parralel_notes = 3 - self.effects: list[Callable] = [ - ] - self.current_frame = 0 - t = np.array([ - midi.midi_to_frequency(f) * - np.linspace(0, 2*np.pi, self.fs) - for f in range(128) - ]) - self.t_buffer = torch.tensor(t, dtype=torch.float32) - print(self.t_buffer.shape) - - def play_init_sound(self): - f1 = 440 # Frequency of the "duuu" sound (in Hz) - f2 = 880 # Frequency of the "dib" sound (in Hz) - t1 = 0.8 # Duration of the "duuu" sound (in seconds) - t2 = 0.2 # Duration of the "dib" sound (in seconds) - t = np.arange(int(t1 * self.fs)) / self.fs - sound1 = 0.5 * np.sin(2 * np.pi * f1 * t) - - # Generate the "dib" sound - t = np.arange(int(t2 * self.fs)) / self.fs - sound2 = 0.5 * np.sin(2 * np.pi * f2 * t) - - # Concatenate the two sounds - audio = np.concatenate([sound1, sound2]) - output = np.array( - audio * 32767 / np.max(np.abs(audio)) / 2).astype(np.int16) - # stereo_sine_wave = np.repeat(output.reshape(-1, 1), 2, axis=1) - sd.play(output, blocking=True) - print("Ready") - - def apply_effects(self, sound): - for effect in self.effects: - print(effect.__name__) - sound[:] = effect(sound) - sound = sound-np.mean(sound) - sound[:] = rescale_audio(sound) - return sound - - def live_synth_loop(self): - print("Live Synth is running") - self.play_init_sound() - - midi.init() - input_id = midi.get_default_input_id() - if input_id == -1: - print("No MIDI input device found.") - return - midi_input = midi.Input(input_id) - p = pyaudio.PyAudio() - CHUNK = 2048 # Increased chunk size - stream = p.open(format=pyaudio.paFloat32, - channels=1, - rate=self.fs, - output=True) - current_frame = 0 - notes = set() - audio_min = np.inf - audio_max = -np.inf - # for _ in utils.timed_loop(True): - while True: - available_buffer = stream.get_write_available() - # print(available_buffer, end=" |\t") - if available_buffer == 0: - continue - y = torch.zeros(available_buffer, device=self.fourier_nn.device) - if midi_input.poll(): - midi_event, timestamp = midi_input.read( - 1)[0] # Read and process one event - if midi_event[0] == 144: # Note on - print("Note on", - midi_event[1], - midi.midi_to_frequency(midi_event[1])) - - notes.add(midi_event[1]) - elif midi_event[0] == 128: # Note off - print("Note off", - midi_event[1], - midi.midi_to_frequency(midi_event[1])) - - notes.discard(midi_event[1]) - - for note in notes: - with torch.no_grad(): - x = utils.wrap_concat( - self.t_buffer[note], current_frame, current_frame + available_buffer) - # x = self.t_buffer[note] - # print(x.shape) - # y += torch.sin(x).flatten() - y += self.fourier_nn.current_model(x.unsqueeze(1)) - audio_data = y.cpu().numpy().astype(np.float32) - audio_data = audio_data/(len(notes)) - audio_data = audio_data - np.mean(audio_data) - - # audio_data *= 0.5 - audio_data = np.clip(audio_data, -1, 1) - stream.write(audio_data, - available_buffer, - exception_on_underflow=True) - current_frame = (current_frame + available_buffer) % self.fs - - def run_live_synth(self): - if not self.live_synth: - print("spawning live synth") - self.live_synth = Process(target=self.live_synth_loop) - self.live_synth.start() - else: - print("killing live synth") - self.live_synth.terminate() - self.live_synth.join() - self.live_synth = None - print("live synth killed") - atexit.register(utils.DIE, self.live_synth, 0, 0) - - def __getstate__(self) -> object: - live_synth = self.live_synth - del self.live_synth - Synth_dict = self.__dict__.copy() - self.live_synth = live_synth - return Synth_dict - - # def __setstate__(self, state): - # # Load the model from a file after deserialization - # self.__dict__.update(state) - # if self.stdout is not None: - # if current_process().name != 'MainProcess': - # sys.stdout = utils.QueueSTD_OUT(queue=self.stdout) diff --git a/scr/predefined_functions.py b/scr/predefined_functions.py deleted file mode 100644 index 421bb49..0000000 --- a/scr/predefined_functions.py +++ /dev/null @@ -1,111 +0,0 @@ -import random -import numpy as np - -from scr import utils - - -def funny(x): - return np.tan(np.sin(x) * np.cos(x)) - - -def funny2(x): - return np.sin(np.cos(x) * np.maximum(0, x)) / np.cos(x) - - -def funny3(x): - return np.sin(np.cos(x) * np.maximum(0, x)) / np.cos(1 / (x+0.001)) - - -def my_random(x): - x = x-np.pi - return np.sin(x * np.random.uniform(-1, 1, size=x.shape)) - - -def my_complex_function(x): - x = np.abs(x) # Ensure x is non-negative for relu - return np.where(x > 0, - np.sin(x) * (np.sin(np.tan(x) * x) / - np.cos(np.random.uniform(-1, 1, size=x.shape) * x)), - -np.sin(-x) * (np.sin(np.tan(-x) * -x) / np.cos(np.random.uniform(-1, 1, size=x.shape) * -x))) - - -def nice(x): - x_greater_pi = False - # x = utils.map_value(x, -np.pi, np.pi, 0, 2*np.pi) - if x >= 2*np.pi: - x_greater_pi = True - x = x - np.pi - if x > (np.pi/2): - y = np.sin(np.tan(x)) - elif 0 < x and x < (np.pi/2): - y = np.cos(-np.tan(x)) - elif (-np.pi/2) < x and x < 0: - y = np.cos(np.tan(x)) - else: - y = np.sin(-np.tan(x)) - if x_greater_pi: - return -y - return y - - -def nice_2(x): - x = np.array(x) - x_greater_pi = x >= 2 * np.pi - x = np.where(x_greater_pi, x - np.pi, x) - - y = np.where(x > (np.pi / 2), np.sin(np.tan(x)), - np.where((0 < x) & (x < (np.pi / 2)), np.cos(-np.tan(x)), - np.where(((-np.pi / 2) < x) & (x < 0), np.cos(np.tan(x)), - np.sin(-np.tan(x))))) - - y = np.where(x_greater_pi, -y, y) - return y - - -def my_generated_function(x): - # Apply a combination of trigonometric functions and activation functions - part1 = np.sin(x) * np.maximum(0, x) - part2 = np.cos(x) * (1 / (1 + np.exp(-x))) # Sigmoid approximation - part3 = np.tan(x) * np.tanh(x) - part4 = np.exp(x) * np.log1p(np.exp(x)) # Softplus approximation - - # Combine the parts to create the final output - # Adding a small value to avoid division by zero - result = part1 + part2 - part3 / (part4 + 1e-7) - return result - - -def extreme(x): - y = np.tile(np.array([-1, 1]), len(x)//2).flatten() - if len(y) < len(x): - y = np.append(y, [y[-2]]) - return y - - -predefined_functions_dict = { - 'funny': funny, - 'funny2': funny2, - 'funny3': funny3, - 'random': my_random, - 'cool': my_complex_function, - 'bing': my_generated_function, - 'nice': nice_2, - 'extreme': extreme, - 'sin': np.sin, - 'cos': np.cos, - 'tan': np.tan, - 'relu': lambda x: np.maximum(0, x - np.pi), - # ELU approximation - 'elu': lambda x: np.where(x - np.pi > 0, x - np.pi, np.expm1(x - np.pi)), - 'linear': lambda x: x - np.pi, # Linear function - 'sigmoid': lambda x: 1 / (1 + np.exp(-(x - np.pi))), - 'exponential': lambda x: np.exp(x - np.pi), - # SELU approximation - 'selu': lambda x: np.where(x - np.pi > 0, 1.0507 * (x - np.pi), 1.0507 * (np.exp(x - np.pi) - 1)), - # GELU approximation - 'gelu': lambda x: 0.5 * (x - np.pi) * (1 + np.tanh(np.sqrt(2 / np.pi) * ((x - np.pi) + 0.044715 * np.power((x - np.pi), 3)))) -} - - -def call_func(name, x): - return np.clip(predefined_functions_dict[name](x), -1, 1) diff --git a/setup.py b/setup.py index 073e854..b854adb 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,10 @@ from setuptools import setup, find_packages -from _version import version +from pythonaisynth._version import version setup( name='PythonAISynth', version=version, - py_modules=['main', '_version'], - packages=find_packages(), + packages=find_packages('.', ['tests', 'audio_stuff']), include_package_data=True, install_requires=[ 'numpy', @@ -21,7 +20,7 @@ ], entry_points={ 'console_scripts': [ - 'PythonAISynth = main:main', + 'PythonAISynth = pythonaisynth.main:main', ], }, ) diff --git a/tests/adaptive_fourier_degree.py b/tests/adaptive_fourier_degree.py index 72a7f1c..e0c76eb 100644 --- a/tests/adaptive_fourier_degree.py +++ b/tests/adaptive_fourier_degree.py @@ -24,8 +24,8 @@ TRAIN_SAMPLES = 1000 # autopep8: off -from context import scr -from scr import predefined_functions +from context import src +from src import predefined_functions # autopep8: on diff --git a/tests/context.py b/tests/context.py index 041e31a..9924b48 100644 --- a/tests/context.py +++ b/tests/context.py @@ -1,10 +1,7 @@ import os import sys -# autopep8: off -path=os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) sys.path.insert(0, path) -import scr -import main -# autopep8: on \ No newline at end of file +import pythonaisynth diff --git a/tests/fourier_layer_torch_test.py b/tests/fourier_layer_torch_test.py index 9deffde..d8fc8b1 100644 --- a/tests/fourier_layer_torch_test.py +++ b/tests/fourier_layer_torch_test.py @@ -7,9 +7,9 @@ # autopep8: off -from context import scr -from scr import predefined_functions, utils -from scr.fourier_neural_network import FourierLayer +from context import src +from src import predefined_functions, utils +from src.fourier_neural_network import FourierLayer # autopep8: on diff --git a/tests/fourier_nn_audio_test.py b/tests/fourier_nn_audio_test.py index 59b724b..6c88344 100644 --- a/tests/fourier_nn_audio_test.py +++ b/tests/fourier_nn_audio_test.py @@ -8,16 +8,16 @@ import sounddevice as sd # autopep8: off -from context import scr -from scr import predefined_functions, utils -from scr.fourier_neural_network import FourierNN -from scr.music import Synth2, Synth3 +from context import src +from src import predefined_functions, utils +from src.fourier_neural_network import FourierNN +from src.music import Synth2, Synth2 # autopep8: on def _1(): # to avoid removing importent import of scr - scr + src fourier_nn = None @@ -66,7 +66,7 @@ def main(): def stupid_thread(fourier_nn): - synth = Synth3(fourier_nn) + synth = Synth2(fourier_nn) # synth.current_notes.add(45) synth.run_live_synth() while True: diff --git a/tests/fourier_nn_test.py b/tests/fourier_nn_test.py index e655de5..a481846 100644 --- a/tests/fourier_nn_test.py +++ b/tests/fourier_nn_test.py @@ -6,15 +6,15 @@ # autopep8: off -from context import scr -from scr import predefined_functions, utils -from scr.fourier_neural_network import FourierNN +from context import src +from src import predefined_functions, utils +from src.fourier_neural_network import FourierNN # autopep8: on def _1(): # to avoid removing importent import of scr - scr + src def main(): diff --git a/tests/fourier_nn_test_live_func_view.py b/tests/fourier_nn_test_live_func_view.py index 8c95311..389af41 100644 --- a/tests/fourier_nn_test_live_func_view.py +++ b/tests/fourier_nn_test_live_func_view.py @@ -6,15 +6,15 @@ # autopep8: off -from context import scr -from scr import predefined_functions, utils -from scr.fourier_neural_network import FourierNN +from context import src +from src import predefined_functions, utils +from src.fourier_neural_network import FourierNN # autopep8: on def _1(): # to avoid removing importent import of scr - scr + src def main(): diff --git a/tests/fouriernn_manual_test.py b/tests/fouriernn_manual_test.py index 012f55b..c31920e 100644 --- a/tests/fouriernn_manual_test.py +++ b/tests/fouriernn_manual_test.py @@ -1,14 +1,14 @@ -import numpy as np -import tensorflow as tf -# import matplotlib -# matplotlib.use('TkAgg') -from context import scr -from scr.fourier_neural_network import FourierNN +# import numpy as np +# import tensorflow as tf +# # import matplotlib +# # matplotlib.use('TkAgg') +# from context import src +# from src.fourier_neural_network import FourierNN -if __name__ == '__main__': - # Test the class with custom data points - data = [(x, np.sin(x * tf.keras.activations.relu(x))) - for x in np.linspace(np.pi, -np.pi, 100)] - fourier_nn = FourierNN(data) - fourier_nn.train_and_plot() +# if __name__ == '__main__': +# # Test the class with custom data points +# data = [(x, np.sin(x * tf.keras.activations.relu(x))) +# for x in np.linspace(np.pi, -np.pi, 100)] +# fourier_nn = FourierNN(data) +# fourier_nn.train_and_plot() diff --git a/tests/fouriernn_parralel_test.py b/tests/fouriernn_parralel_test.py index e287f4f..c5955e6 100644 --- a/tests/fouriernn_parralel_test.py +++ b/tests/fouriernn_parralel_test.py @@ -1,22 +1,22 @@ -import multiprocessing -import numpy as np -import tensorflow as tf -# import matplotlib -# matplotlib.use('TkAgg') -from context import scr -from scr.fourier_neural_network import FourierNN -from scr.utils import DIE +# import multiprocessing +# import numpy as np +# import tensorflow as tf +# # import matplotlib +# # matplotlib.use('TkAgg') +# from context import src +# from src.fourier_neural_network import FourierNN +# from src.utils import DIE -# TODO redo -# if __name__ == '__main__': -# multiprocessing.set_start_method("spawn") -# # Test the class with custom data points -# data = [(x, np.sin(x * tf.keras.activations.relu(x))) -# for x in np.linspace(np.pi, -np.pi, 100)] -# fourier_nn = FourierNN(data) +# # TODO redo +# # if __name__ == '__main__': +# # multiprocessing.set_start_method("spawn") +# # # Test the class with custom data points +# # data = [(x, np.sin(x * tf.keras.activations.relu(x))) +# # for x in np.linspace(np.pi, -np.pi, 100)] +# # fourier_nn = FourierNN(data) -# proc=fourier_nn.train_Process(np.linspace(np.pi, -np.pi, 100)) -# proc.start() -# while proc.is_alive(): -# pass -# DIE(proc) +# # proc=fourier_nn.train_Process(np.linspace(np.pi, -np.pi, 100)) +# # proc.start() +# # while proc.is_alive(): +# # pass +# # DIE(proc) diff --git a/tests/fouriernn_test.py b/tests/fouriernn_test.py index 9415406..614e38b 100644 --- a/tests/fouriernn_test.py +++ b/tests/fouriernn_test.py @@ -1,60 +1,60 @@ -import os -import unittest -from context import scr -from scr import fourier_neural_network - -import numpy as np -import tensorflow as tf - -# TODO could use refactor - -path = './tests/tmp/' -if not os.path.exists(path=path): - os.mkdir(path=path) - - -class TestStringMethods(unittest.TestCase): - # changed the way fourierNN saves the data - # TODO: update functon - # def test_fouriernn_init(self): - # fourier_nn = fourier_neural_network.FourierNN( - # np.array([(0, 1), (2, 3), (4, 5)])) - # np.testing.assert_array_equal( - # fourier_nn.data, np.array([(0, 1), (2, 3), (4, 5)])) - # self.assertIsNone(fourier_nn.models) - # self.assertIsNotNone(fourier_nn.current_model) - - def test_relu_aprox(self): - data = [(x, tf.keras.activations.relu(x)) - for x in np.linspace(np.pi, -np.pi, 100)] - fourier_nn = fourier_neural_network.FourierNN(data) - # self.assertEqual(fourier_nn.data, data) - fourier_nn.train(quiet=True) - test_data = np.linspace(np.pi, np.pi, fourier_nn.SAMPLES) - out = fourier_nn.predict(test_data) - wanted_data=np.array([tf.keras.activations.relu(x) for x in test_data]).reshape((fourier_nn.SAMPLES, 1)) - #print(wanted_data) - np.testing.assert_almost_equal(out, wanted_data, decimal=1) - - def test_sine_of_relu_aprox(self): - data = [(x, np.sin(x * tf.keras.activations.relu(x))) - for x in np.linspace(np.pi, -np.pi, 100)] - fourier_nn = fourier_neural_network.FourierNN(data) - # self.assertEqual(fourier_nn.data, data) - fourier_nn.train(quiet=True) - test_data = np.linspace(np.pi, np.pi, fourier_nn.SAMPLES) - #print(data) - out = fourier_nn.predict(test_data) - wanted_data=np.array([np.sin(x*tf.keras.activations.relu(x)) for x in test_data]).reshape((fourier_nn.SAMPLES, 1)) - #print(wanted_data) - #np.testing.assert_almost_equal(data, wanted_data) - np.testing.assert_almost_equal(out, wanted_data, decimal=1) - - -if __name__ == '__main__': - unittest.main() -# # Test the class with custom data points -# data = [(x, np.sin(x * tf.keras.activations.relu(x))) -# for x in np.linspace(np.pi, -np.pi, 100)] -# fourier_nn = FourierNN(data) -# fourier_nn.train() +# import os +# import unittest +# from context import src +# from src import fourier_neural_network + +# import numpy as np +# import tensorflow as tf + +# # TODO could use refactor + +# path = './tests/tmp/' +# if not os.path.exists(path=path): +# os.mkdir(path=path) + + +# class TestStringMethods(unittest.TestCase): +# # changed the way fourierNN saves the data +# # TODO: update functon +# # def test_fouriernn_init(self): +# # fourier_nn = fourier_neural_network.FourierNN( +# # np.array([(0, 1), (2, 3), (4, 5)])) +# # np.testing.assert_array_equal( +# # fourier_nn.data, np.array([(0, 1), (2, 3), (4, 5)])) +# # self.assertIsNone(fourier_nn.models) +# # self.assertIsNotNone(fourier_nn.current_model) + +# def test_relu_aprox(self): +# data = [(x, tf.keras.activations.relu(x)) +# for x in np.linspace(np.pi, -np.pi, 100)] +# fourier_nn = fourier_neural_network.FourierNN(data) +# # self.assertEqual(fourier_nn.data, data) +# fourier_nn.train(quiet=True) +# test_data = np.linspace(np.pi, np.pi, fourier_nn.SAMPLES) +# out = fourier_nn.predict(test_data) +# wanted_data=np.array([tf.keras.activations.relu(x) for x in test_data]).reshape((fourier_nn.SAMPLES, 1)) +# #print(wanted_data) +# np.testing.assert_almost_equal(out, wanted_data, decimal=1) + +# def test_sine_of_relu_aprox(self): +# data = [(x, np.sin(x * tf.keras.activations.relu(x))) +# for x in np.linspace(np.pi, -np.pi, 100)] +# fourier_nn = fourier_neural_network.FourierNN(data) +# # self.assertEqual(fourier_nn.data, data) +# fourier_nn.train(quiet=True) +# test_data = np.linspace(np.pi, np.pi, fourier_nn.SAMPLES) +# #print(data) +# out = fourier_nn.predict(test_data) +# wanted_data=np.array([np.sin(x*tf.keras.activations.relu(x)) for x in test_data]).reshape((fourier_nn.SAMPLES, 1)) +# #print(wanted_data) +# #np.testing.assert_almost_equal(data, wanted_data) +# np.testing.assert_almost_equal(out, wanted_data, decimal=1) + + +# if __name__ == '__main__': +# unittest.main() +# # # Test the class with custom data points +# # data = [(x, np.sin(x * tf.keras.activations.relu(x))) +# # for x in np.linspace(np.pi, -np.pi, 100)] +# # fourier_nn = FourierNN(data) +# # fourier_nn.train() diff --git a/tests/frequency_checker_test.py b/tests/frequency_checker_test.py index f4ca63a..04d5b5d 100644 --- a/tests/frequency_checker_test.py +++ b/tests/frequency_checker_test.py @@ -1,7 +1,7 @@ import unittest import numpy as np -from context import scr -from scr.utils import calculate_peak_frequency +from context import src +from src.utils import calculate_peak_frequency class TestCalculatePeakFrequency(unittest.TestCase): diff --git a/tests/graph_canvas_test.py b/tests/graph_canvas_test.py index bd1c038..348e1b5 100644 --- a/tests/graph_canvas_test.py +++ b/tests/graph_canvas_test.py @@ -1,7 +1,7 @@ # import os # import unittest -from context import scr -from scr import graph_canvas +from context import src +from src import graph_canvas import tkinter as tk """ diff --git a/tests/test_model_behavior.py b/tests/test_model_behavior.py index e7f7488..34a3803 100644 --- a/tests/test_model_behavior.py +++ b/tests/test_model_behavior.py @@ -1,11 +1,11 @@ from scipy.fft import fft, fftfreq import multiprocessing import numpy as np -from context import scr -from scr.fourier_neural_network import FourierNN +from context import src +from src.fourier_neural_network import FourierNN import matplotlib.pyplot as plt -from scr.utils import calculate_peak_frequency +from src.utils import calculate_peak_frequency def main():