Merge branch 'main' of ssh+git://github.com/Chroma-Case/Chromacase into front/play-page

This commit is contained in:
Arthi-chaud
2023-01-05 14:41:56 +00:00
21 changed files with 395 additions and 0 deletions
+4
View File
@@ -7,3 +7,7 @@ trim_trailing_whitespace = true
insert_final_newline = true
indent_style = tab
indent_size = tab
[{*.yaml,*.yml}]
indent_style = space
indent_size = 2
+10
View File
@@ -14,6 +14,16 @@ services:
condition: service_healthy
env_file:
- .env
scorometer:
build:
context: ./scorometer
dockerfile: Dockerfile.dev
ports:
- "6543:6543"
volumes:
- ./scorometer:/app
- ./musics:/musics
db:
container_name: db
+6
View File
@@ -10,6 +10,12 @@ services:
- .env
volumes:
- ./musics:/musics
scorometer:
build: ./scorometer
ports:
- "6543:6543"
volumes:
- ./musics:/musics
db:
container_name: db
image: postgres:alpine3.14
+3
View File
@@ -0,0 +1,3 @@
Dockerfile
Dockerfile.dev
.dockerignore
+13
View File
@@ -0,0 +1,13 @@
FROM python:latest
RUN wget -q -O /tmp/websocketd.zip \
https://github.com/joewalnes/websocketd/releases/download/v0.4.1/websocketd-0.4.1-linux_amd64.zip \
&& unzip /tmp/websocketd.zip -d /tmp/websocketd && mv /tmp/websocketd/websocketd /usr/bin \
&& chmod +x /usr/bin/websocketd
WORKDIR /app
COPY ./requirements.txt .
RUN pip install -r ./requirements.txt
COPY . .
CMD ["/usr/bin/websocketd", "--port=6543", "--staticdir=." "./main.py"]
+12
View File
@@ -0,0 +1,12 @@
FROM python:latest
RUN wget -q -O /tmp/websocketd.zip \
https://github.com/joewalnes/websocketd/releases/download/v0.4.1/websocketd-0.4.1-linux_amd64.zip \
&& unzip /tmp/websocketd.zip -d /tmp/websocketd && mv /tmp/websocketd/websocketd /usr/bin \
&& chmod +x /usr/bin/websocketd
WORKDIR /app
COPY ./requirements.txt .
RUN pip install -r ./requirements.txt
CMD ["/usr/bin/websocketd", "--port=6543", "--devconsole", "--", "python3", "./main.py"]
+178
View File
@@ -0,0 +1,178 @@
asyncapi: "2.5.0"
info:
title: Scorometer
version: "1.0.0"
channels:
/start:
publish:
summary: "To start a song, send the start message with name of the song"
message:
summary: "Start message"
payload:
type: "object"
required:
- type
- name
properties:
type:
type: "string"
enum: ["start"]
name:
type: "string"
description: "The name of the song"
operationId: "startSong"
/midi:
publish:
summary: "Every time a note is played, a midi message should be sent from the client to the server."
message:
summary: "Midi message"
payload:
type: "object"
required:
- type
- time
- note
- intensity
properties:
id:
type: "object"
description: "An arbitrary data that will be sent back on the score message."
type:
type: "string"
enum: ["note_on", "note_off"]
description: "note_on => on_key_down, note_off => on_key_up"
time:
type: "number"
description: "The event time in the midi message, I don't know the unit. A timestamp with the start as the epoch could work"
note:
type: "number"
description: "The note played (between 21 and 108, C5 is 60)"
intensity:
type: "number"
description: "How strong the key was pressed. On some MIDI libraries, this is named velocity. Should be between 30 and 130ish. If the piano does not support this, send null instead."
operationId: "sendMidi"
/score:
subscribe:
summary: "At each tempo (containing notes), a or multiple scores message will be sent. Note that this is not a 1/1 mapping with midi messages, chords will be grouped together."
message:
summary: "Score message"
payload:
type: "object"
required:
- type
- time
- timingScore
- timingInformation
- notes
properties:
ids:
type: "array"
items:
type: "object"
description: "The list of IDs of the events sent."
type:
type: "string"
enum: ["note_on", "note_off"]
description: "note_on => on_key_down, note_off => on_key_up"
time:
type: "number"
description: "The event time in the midi message , I don't know the unit. A timestamp with the start as the epoch could work"
timingScore:
type: "string"
enum: ["perfect", "great", "good", "miss"]
description: "The score attributed to the timing."
timingInformation:
type: "string"
enum: ["late", "perfect", "fast"]
description: "Detailed informations on the timing. This information can be useful to the player."
notes:
type: "object"
required:
- good
- missing
- bad
properties:
good:
type: "array"
items:
type: "object"
required:
- note
properties:
id:
type: "object"
description: "The id of the events sent."
note:
type: "number"
description: "The note played (between 21 and 108, C5 is 60)"
missing:
type: "array"
items:
type: "object"
required:
- note
properties:
id:
type: "object"
description: "The id of the events sent."
note:
type: "number"
description: "The note played (between 21 and 108, C5 is 60)"
bad:
type: "array"
items:
type: "object"
required:
- note
properties:
id:
type: "object"
description: "The id of the events sent."
note:
type: "number"
description: "The note played (between 21 and 108, C5 is 60)"
operationId: "receiveScore"
/pause:
publish:
summary: "When the client pause/resume the playback, this message should be sent."
message:
summary: "Pause message"
payload:
type: "object"
required:
- type
- paused
- time
properties:
type:
type: "string"
enum: ["pause"]
paused:
type: "boolean"
description: "True if the new state is paused, false if it is resumed"
time:
type: "number"
description: "The timing at witch this event was sent. This is used to prevent network trafics to offset future notes and be sure the server/client are well timed together."
operationId: "pauseSong"
/end:
subscribe:
summary: "When the music has ended, the server will send this message with a recap of the whole score."
message:
summary: "End message"
payload:
type: "object"
required:
- type
- overrallScore
- score
properties:
type:
type: "string"
enum: ["end"]
overralScore:
type: number
description: "An overrall score between 0 and 100."
score:
type: object
description: "An object containing every difficulties as a key and a score between 0 and 100 for each."
operationId: "endSong"
+10
View File
@@ -0,0 +1,10 @@
class Key:
def __init__(self, key: int, start: int, duration: int):
self.key = key
self.start = start
self.duration = duration
def __repr__(self):
return f"{self.key} ({self.start} - {self.duration})"
+11
View File
@@ -0,0 +1,11 @@
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
+15
View File
@@ -0,0 +1,15 @@
from .Key import Key
class Partition:
def __init__(self, name:str, notes:list[Key]) -> None:
self.__name = name
self.notes = notes
def __repr__(self):
r = f"{self.__name}\n"
for i in self.notes:
r += f"{i.__repr__()}\n"
return r
View File
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/python3
import logging
from chroma_case.Partition import Partition
from chroma_case.Key import Key
import sys
import select
import json
from mido import MidiFile
RATIO = float(sys.argv[2] if len(sys.argv) > 2 else 1)
OCTAVE = 5
OCTAVE_AMOUNT_KEYS = 12
class Scorometer():
def __init__(self, midiFile) -> None:
self.partition = self.getPartition(midiFile)
self.keys_down = []
pass
def getPartition(self, midiFile):
notes = []
s = 3500
notes_on = {}
prev_note_on = {}
for msg in MidiFile(midiFile):
d = msg.dict()
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["type"] == "note_off":
#duration = s - notes_on[d["note"]]
duration = s - notes_on[d["note"]]
note_start = notes_on[d["note"]]
# time value is only used during debug
notes.append(Key(d["note"], note_start, duration - 10))
notes_on[d["note"]] = s # 500
return Partition(midiFile, notes)
def handleNote(self, obj):
_key = obj["note"]
status = obj["type"]
timestamp = obj["time"]
is_down = any(x[0] == _key for x in self.keys_down)
key = None
if status == "note_on" and not is_down:
self.keys_down.append((_key, timestamp))
print(json.dumps({"note": _key}), flush=True)
# print(f"Midi: {status} - {key} - {intensity} - {data3} at {timestamp}")
elif status == "note_off" or is_down:
down_since = next(since for (h_key, since) in self.keys_down if h_key == _key)
self.keys_down.remove((_key, down_since))
key = Key(_key, down_since, (timestamp - down_since))
if key is None:
return
n = self.partition.notes
to_play = next((i for i in n if i.key == key.key and self.is_timing_close(key, i)), None)
if to_play == None:
pass
## TODO handle invalid key
#points -= 50
#print(f"Invalid key.")
else:
# 500 / 490 0.9 - 1
tempo_percent = abs((key.duration / to_play.duration) - 1)
#points += tempo_percent * 50
if tempo_percent < .3 :
timingScore = "perfect"
elif tempo_percent < .5:
timingScore = f"great"
else:
timingScore = "good"
timingInformation = "fast" if key.start < to_play.start else "late"
if abs(key.start - to_play.start) < 200: timingInformation = "perfect"
self.sendScore(obj["id"], timingScore, timingInformation)
def is_timing_close(self, key: Key, i):
return abs(i.start - key.start) < 500
def handleMessage(self, message: str):
obj = json.loads(message)
if "type" not in obj.keys():
self.sendError(message)
return
if obj["type"] == "note_on" or obj["type"] == "note_off":
self.handleNote(obj)
if obj["type"] == "pause":
pass
def sendEnd(self, overall, difficulties):
print(json.dumps({"overallScore": overall, "score": difficulties}), flush=True)
def sendError(self, message):
print(json.dumps({"error": f"Could not handle message {message}"}), flush=True)
def sendScore(self, id, timingScore, timingInformation):
print(json.dumps({"id": id, "timingScore": timingScore, "timingInformation": timingInformation}), flush=True)
def gameLoop(self):
while True:
if select.select([sys.stdin, ], [], [], 0.0)[0]:
line = input()
if not line:
break
logging.info("handling message")
self.handleMessage(line.rstrip())
else:
pass
self.sendEnd(0, {})
def main():
try:
start_message = json.loads(input())
if "type" not in start_message.keys() or start_message["type"] != "start" or "name" not in start_message.keys():
print(json.dumps({"error": "Error with the start message"}), flush=True)
exit()
song_name = start_message["name"]
logging.info(f"started {song_name}")
print(json.dumps({"song_launched": song_name}), flush=True)
sc = Scorometer(f"partitions/{song_name}.midi")
sc.gameLoop()
except Exception as error:
print({ "error": error }, flush=True)
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
mido