diff --git a/scorometer/chroma_case/Note.py b/scorometer/chroma_case/Note.py new file mode 100644 index 0000000..2e61b3a --- /dev/null +++ b/scorometer/chroma_case/Note.py @@ -0,0 +1,13 @@ + + +class Note: + def __init__(self, start_time, data) -> None: + + self.__start_time = start_time + self.__data = data + + def get_start_time(self): + return self.__start_time + + def get_data(self): + return self.__data \ No newline at end of file diff --git a/scorometer/chroma_case/Partition.py b/scorometer/chroma_case/Partition.py new file mode 100644 index 0000000..ff027af --- /dev/null +++ b/scorometer/chroma_case/Partition.py @@ -0,0 +1,34 @@ +import asyncio, datetime +from typing import Callable + +from .Note import Note + +async def wait_until(dt): + # sleep until the specified datetime + now = datetime.datetime.now() + await asyncio.sleep((dt - now).total_seconds()) + +async def run_at(dt, coro): + await wait_until(dt) + return await coro + +class Partition: + + def __init__(self, name:str, notes:list[Note]) -> None: + + self.__name = name + self.__notes = notes + + async def play(self, output_lambda:Callable[[object], None]): + now = datetime.datetime.now() + tasks_to_wait = [] + for note in self.__notes: + tasks_to_wait.append( + asyncio.create_task( + run_at( + now + datetime.timedelta(milliseconds= note.get_start_time()), + output_lambda(note.get_data()) + ) + ) + ) + await asyncio.wait(tasks_to_wait) diff --git a/scorometer/chroma_case/__init__.py b/scorometer/chroma_case/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scorometer/launch.sh b/scorometer/launch.sh new file mode 100755 index 0000000..988079d --- /dev/null +++ b/scorometer/launch.sh @@ -0,0 +1,5 @@ +#!/usr/bin/zsh + +sudo python3 main.py $1 &! +sleep 3.5 +./tester.py $1 $2 diff --git a/scorometer/leds.py b/scorometer/leds.py new file mode 100755 index 0000000..858bd39 --- /dev/null +++ b/scorometer/leds.py @@ -0,0 +1,56 @@ +#!/usr/bin/python3 + +import board +import neopixel +import time +import sys +import asyncio + +colorToFill = (0, 0, 0) +pixels = neopixel.NeoPIxel(board.D18, 20, brightness=0.01) + +notePixels = { 'si': [0, 1], + 'la#': [2, 3], + 'la': [4, 5], + 'sol#':[6], + 'sol':[7, 8, 9], + 'fa#':[10], + 'fa':[11, 12, 13], + 'mi':[14, 15, 16], + 're#':[17], + 're':[18, 19], + 'do#':[], + 'do':[]} + +def playNote(color, secondsToStay, pixelsToFill): + for pixelIndex in pixelsToFill: + pixels[pixelIndex] = color + time.sleep(secondsToStay) + + + + + +def launchMusic(noteList): + + pixels.fill(0,0,0) + pixels.write() + + for notes, tempo in noteList: + for note in notes: + playNote((255, 0, 0), tempo, notePixels[note.lower()]) + pixels.fill(colorToFill) + pixels.write() + +music = [ + (['sol'], 1), + (['sol'], 1), + (['sol'], 1), + (['re#'], 1), + (['la#'], 0.5), + (['sol'], 0.5), + (['re#'], 1), + (['la#'], 0.5), + ] + +launchMusic(music) diff --git a/scorometer/main.py b/scorometer/main.py new file mode 100644 index 0000000..c1a33d3 --- /dev/null +++ b/scorometer/main.py @@ -0,0 +1,166 @@ +from xmlrpc.client import TRANSPORT_ERROR +from chroma_case.Partition import Partition +from chroma_case.Note import Note +import asyncio +import sys +from mido import MidiFile + +import board, neopixel + +# on octave is 12 +RATIO = float(sys.argv[2] if len(sys.argv) > 2 else 1) +OCTAVE = 5 +OCTAVE_AMOUNT_KEYS = 12 +TRANSPOSE_AMOUNT = OCTAVE_AMOUNT_KEYS * OCTAVE + +pixels = neopixel.NeoPixel(board.D18, 20, brightness=0.01) + +notePixels = { 'si': [19], + 'la#': [18], + 'la': [17], + 'sol#':[15], + 'sol':[13], + 'fa#':[10], + 'fa':[9], + 'mi':[6], + 're#':[5], + 're':[3], + 'do#':[1], + 'do':[0]} + + +def hue_to_rgb(t1, t2, hue): + if hue < 0: hue += 6 + if hue >= 6: hue -= 6 + if hue < 1: return (t2 - t1) * hue + t1 + if hue < 3: return t2 + if hue < 4: return (t2 - t1) * (4 - hue) + t1 + return t1 + +def hsl_to_rgb(hue, sat, light): + hue /= 60 + if light <= 0.5: + t2 = light * (sat + 1) + else: + t2 = light + sat - (light * sat) + t1 = light * 2 - t2 + + r = hue_to_rgb(t1, t2, hue + 2) * 255 + g = hue_to_rgb(t1, t2, hue) * 255 + b = hue_to_rgb(t1, t2, hue - 2) * 255 + return [round(r), round(g), round(b)] + +async def to_chroma_case(data): + global pixels + + hsl_starting_color = [100, 100, 50] + + colored_pixels = notePixels[data["key"].lower()] + #if "announce" in data: + c = data["color"] + """for i in range(5): + for pixelId in colored_pixels: + pixels[pixelId] = (c[0], int(c[1] * tmp), c[2]) + tmp -= 0.2 + await asyncio.sleep(data["duration"] / (5 * 1000))""" + """for i in range(11): + for pixelId in colored_pixels: + pixels[pixelId] = hsl_to_rgb(hsl_starting_color[0], hsl_starting_color[1], hsl_starting_color[2]) + hsl_starting_color[2] += 0.01 + await asyncio.sleep(0.01)""" + for pixelId in colored_pixels: + pixels[pixelId] = data["color"] + await asyncio.sleep(data['duration'] / 1000) + for pixelId in colored_pixels: + pixels[pixelId] = 0 + + +async def printing(data): + print(f"key: {data['key']}, c:{data['color']} for {data['duration'] / 1000}s, time: {data['time']}") + await asyncio.sleep(data['duration'] / 1000) + print(f"end of {data['key']}") + + +def midi_key_my_key(midi_key): + keys = list(notePixels.keys()) + + keys.reverse() + + key_index = midi_key - TRANSPOSE_AMOUNT + if key_index >= len(keys): + print("key out of leb barre", key_index) + return "no_key" + + return keys[key_index] + + + + +async def main(): + + default_duration = 900 + default_color = (255, 0, 0) + + notes = [] + # notes will start to play at 3500 ms (colors at the start takes this amount of time) + s = 3500 + + notes_on = {} + prev_note_on = {} + note_color = {} + + for msg in MidiFile(sys.argv[1]): + d = msg.dict() + print(msg, s) + s += d['time'] * 1000 * RATIO + + if d["type"] == "note_on": + prev_note_on[d["note"]] = 0 + if d["note"] in notes_on: + prev_note_on[d["note"]] = notes_on[d["note"]] # 500 + notes_on[d["note"]] = s # 0 + if d["note"] not in note_color.keys(): + note_color[d["note"]] = 1 + note_color[d["note"]] = not note_color[d["note"]] + + if d["type"] == "note_off": + #duration = s - notes_on[d["note"]] + duration = s - notes_on[d["note"]] + + """notes.append(Note( + s - min(s - prev_note_on[d["note"]], 500), + { + "duration": min(s - prev_note_on[d["note"]], 1000) / 2, + "color": (255, 255, 0), + "key": midi_key_my_key(d["note"]), + "announce": True + } + ))""" + + note_start = notes_on[d["note"]] + # time value is only used during debug + notes.append(Note(note_start, {"time": note_start, "duration": duration - 10, "color": default_color if note_color[d["note"]] else (255, 100, 0), "key": midi_key_my_key(d["note"])})) + notes_on[d["note"]] = s # 500 + + + + starting = [] + + for i in notePixels.keys(): + starting += [ + Note(000, {"duration": default_duration, "color": (255, 0, 0), "key": i, "time": 0}), + Note(1000, {"duration": default_duration, "color": (255, 255, 0), "key": i, "time": 0}), + Note(2000, {"duration": default_duration, "color": (0, 255, 0), "key": i, "time": 0}), + ] + + p = Partition("my_partition", + starting + notes + ) + + await p.play(to_chroma_case) + + return 0 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main())) diff --git a/scorometer/midi.py b/scorometer/midi.py new file mode 100644 index 0000000..06c04e7 --- /dev/null +++ b/scorometer/midi.py @@ -0,0 +1,4 @@ +from mido import MidiFile + +for msg in MidiFile('new_song_1.mid'): + print(msg) \ No newline at end of file diff --git a/scorometer/myscr b/scorometer/myscr new file mode 100755 index 0000000..919cae5 --- /dev/null +++ b/scorometer/myscr @@ -0,0 +1,7 @@ +#!/usr/bin/bash + +while true +do + sudo python main.py new_song_2.mid +done + diff --git a/scorometer/partitions/20220128_095349.jpg b/scorometer/partitions/20220128_095349.jpg new file mode 100644 index 0000000..12d22cd Binary files /dev/null and b/scorometer/partitions/20220128_095349.jpg differ diff --git a/scorometer/partitions/Believer.mid b/scorometer/partitions/Believer.mid new file mode 100644 index 0000000..7832d7f Binary files /dev/null and b/scorometer/partitions/Believer.mid differ diff --git a/scorometer/partitions/Bella-Ciao.mid b/scorometer/partitions/Bella-Ciao.mid new file mode 100644 index 0000000..c2a5236 Binary files /dev/null and b/scorometer/partitions/Bella-Ciao.mid differ diff --git a/scorometer/partitions/Game.mid b/scorometer/partitions/Game.mid new file mode 100644 index 0000000..1d49a9f Binary files /dev/null and b/scorometer/partitions/Game.mid differ diff --git a/scorometer/partitions/Game2.mid b/scorometer/partitions/Game2.mid new file mode 100644 index 0000000..a4c6049 Binary files /dev/null and b/scorometer/partitions/Game2.mid differ diff --git a/scorometer/partitions/Tetris.mid b/scorometer/partitions/Tetris.mid new file mode 100644 index 0000000..2014fec Binary files /dev/null and b/scorometer/partitions/Tetris.mid differ diff --git a/scorometer/partitions/clair-de-lune.midi b/scorometer/partitions/clair-de-lune.midi new file mode 100644 index 0000000..f51799a Binary files /dev/null and b/scorometer/partitions/clair-de-lune.midi differ diff --git a/scorometer/partitions/new_song_1.mid b/scorometer/partitions/new_song_1.mid new file mode 100644 index 0000000..4d45c1c Binary files /dev/null and b/scorometer/partitions/new_song_1.mid differ diff --git a/scorometer/partitions/new_song_2.mid b/scorometer/partitions/new_song_2.mid new file mode 100644 index 0000000..c8dfa8f Binary files /dev/null and b/scorometer/partitions/new_song_2.mid differ diff --git a/scorometer/requirements.txt b/scorometer/requirements.txt new file mode 100644 index 0000000..0afb431 --- /dev/null +++ b/scorometer/requirements.txt @@ -0,0 +1,2 @@ +mido +pygame diff --git a/scorometer/test.py b/scorometer/test.py new file mode 100644 index 0000000..9dbc03c --- /dev/null +++ b/scorometer/test.py @@ -0,0 +1,15 @@ +import asyncio + +async def nested(): + return 42 + +async def main(): + # Schedule nested() to run soon concurrently + # with "main()". + task = asyncio.create_task(nested()) + + # "task" can now be used to cancel "nested()", or + # can simply be awaited to wait until it is complete: + await task + +asyncio.run(main()) \ No newline at end of file diff --git a/scorometer/tester.py b/scorometer/tester.py new file mode 100755 index 0000000..f89d40b --- /dev/null +++ b/scorometer/tester.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 + +import sys +import os +from typing import List + +import pygame as pg +from pygame.constants import KEYDOWN +import pygame.midi +from mido import MidiFile + +# Status definitions +TOUCH_DOWN = 144 +TOUCH_UP = 128 + + +class Key: + def __init__(self, key: int, start: int, duration: int): + self.key = key + self.start = start + self.duration = duration + + def __str__(self): + return f"{self.key} ({self.start} - {self.duration})" + + +def read_midi(file): + notes = [] + notes_on = {} + s = 0 + for msg in MidiFile(file): + d = msg.dict() + s += d['time'] * 1000 + if d["type"] == "note_on": + notes_on[d["note"]] = s + if d["type"] == "note_off": + duration = s - notes_on[d["note"]] + notes_on[d["note"]] = s + notes.append(Key(d["note"], s, duration)) + return notes + + +keys_to_play = read_midi(sys.argv[1]) +for i in map(lambda x: str(x), keys_to_play): + print(str(i)) + +# List of keys currently holded. Format: (key, timestamp) +keys_down = [] + +points = 0 + + +def print_device_info(): + pygame.midi.init() + _print_device_info() + pygame.midi.quit() + + +def _print_device_info(): + for i in range(pygame.midi.get_count()): + r = pygame.midi.get_device_info(i) + (interf, name, input, output, opened) = r + + in_out = "" + if input: + in_out = "(input)" + if output: + in_out = "(output)" + + print( + "%2i: interface :%s:, name :%s:, opened :%s: %s" + % (i, interf, name, opened, in_out) + ) + +def poll(midi): + if midi.poll(): + [((status, key, intensity, data3), timestamp)] = midi.read(1) + # For status, see STATUS DEFINITIONS up there (either TOUCH_DOWN, TOUCH_UP or others for pedals) + # The key is between 21 and 108, C5 is 60 + # The itensity is how strong the key got struck (between 1 and 130ish) + # data3 seems to always be 0 + # timestamp seems to be a unix timestamp since the midi has been oppened. + + # Sometimes, status is always TOUCH_DOWN so if the key is already down, we consider it the same as a key up + is_down = any(x[0] == key for x in keys_down) + if status == TOUCH_DOWN and not is_down: + keys_down.append((key, timestamp)) + # print(f"Midi: {status} - {key} - {intensity} - {data3} at {timestamp}") + elif status == TOUCH_UP or is_down: + down_since = next(since for (h_key, since) in keys_down if h_key == key) + keys_down.remove((key, down_since)) + return Key(key, down_since, (timestamp - down_since)) + +def is_timing_close(key, i): + return abs(i.start - key.start) < 500 + +def run(midi): + global points + clock_now = 0 + while sorted(keys_to_play, key=lambda x: x.start)[-1].start > clock_now: + key = poll(midi) + if key is None: + continue + clock_now = key.start + + to_play = next((i for i in keys_to_play if i.key == key.key and is_timing_close(key, i)), None) + if to_play == None: + points -= 50 + print(f"Invalid key.") + else: + tempo_percent = abs((key.duration / to_play.duration) - 1) + points += tempo_percent * 50 + if tempo_percent < .3 : + print("Too short" if key.duration < to_play.duration else "Too long") + elif tempo_percent < .5: + print(f"GREAT.") + else: + print(f"EXCELLENT.") + points -= len(keys_to_play) * 20 + + +def input_main(device_id=None): + pg.init() + pygame.midi.init() + + _print_device_info() + + if device_id is None: + input_id = pygame.midi.get_default_input_id() + else: + input_id = device_id + + print("using input_id :%s:" % input_id) + i = pygame.midi.Input(input_id) + + pg.display.set_mode((1, 1)) + try: + run(i) + except KeyboardInterrupt: + pass + print(f"You got: {int(points)}pts") + pygame.midi.quit() + + +if __name__ == '__main__': + exit(input_main(int(sys.argv[2]) if len(sys.argv) == 3 else None))