├── backend ├── core │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── logging_middleware.py │ │ ├── api.py │ │ └── model.py │ ├── service │ │ ├── __init__.py │ │ ├── midi │ │ │ ├── __init__.py │ │ │ ├── midi_loader.py │ │ │ ├── midi_info_extractor.py │ │ │ └── midi_event.py │ │ ├── note │ │ │ ├── __init__.py │ │ │ └── note_extractor.py │ │ ├── tokenizer │ │ │ ├── __init__.py │ │ │ ├── tokenizers │ │ │ │ ├── __init__.py │ │ │ │ ├── tsd_tokenizer.py │ │ │ │ ├── MMM_tokenizer.py │ │ │ │ ├── remi_tokenizer.py │ │ │ │ ├── cpword_tokenizer.py │ │ │ │ ├── midilike_tokenizer.py │ │ │ │ ├── muMIDI_tokenizer.py │ │ │ │ ├── octuple_tokenizer.py │ │ │ │ ├── perTok_tokenizer.py │ │ │ │ └── structured_tokenizer.py │ │ │ ├── tokenizer_factory.py │ │ │ └── tokenizer_config.py │ │ ├── serializer.py │ │ └── midi_processing.py │ ├── data │ │ ├── example.mid │ │ └── example2.mid │ ├── main.py │ └── constants.py ├── tests │ ├── __init__.py │ ├── test_api.py │ ├── test_info_retrieval.py │ ├── test_tokenizer_factory.py │ └── test_failed_note_pertok.py ├── Procfile ├── Dockerfile ├── .pre-commit-config.yaml ├── .gitignore └── pyproject.toml ├── frontend ├── Procfile ├── src │ ├── components │ │ ├── FileUpload.css │ │ ├── Spinner.css │ │ ├── MidiCreate │ │ │ ├── types │ │ │ │ └── midi.ts │ │ │ ├── SettingsPanel.css │ │ │ ├── PlaybackControls.tsx │ │ │ ├── PlaybackControls.css │ │ │ ├── SettingsPanel.tsx │ │ │ ├── VirtualKeyboard.css │ │ │ ├── SoundSynthesizer.css │ │ │ ├── MIDICanvas.css │ │ │ ├── MIDIDeviceManager.css │ │ │ ├── MIDISequencer.css │ │ │ ├── SoundSynthesizer.tsx │ │ │ ├── MIDIDeviceManager.tsx │ │ │ └── VirtualKeyboard.tsx │ │ ├── MusicInfoDisplay.css │ │ ├── Spinner.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── PianoRollDisplay.css │ │ ├── SingleValueSlider.tsx │ │ ├── RangeSlider.tsx │ │ ├── TokenInfo.tsx │ │ ├── FileUpload.tsx │ │ ├── FilePlayback.css │ │ ├── DemoFile.css │ │ ├── DemoFile.tsx │ │ ├── TokenBlock.css │ │ ├── MusicInfoDisplay.tsx │ │ ├── TokenInfo.css │ │ ├── TokenBlock.tsx │ │ └── DataDisplay.tsx │ ├── react-app-env.d.ts │ ├── setupTests.ts │ ├── App.test.tsx │ ├── index.css │ ├── reportWebVitals.ts │ ├── index.tsx │ ├── interfaces │ │ └── ApiResponse.tsx │ └── logo.svg ├── .dockerignore ├── .env.development ├── public │ ├── robots.txt │ ├── bckg.png │ ├── favicon.ico │ ├── favicon.png │ ├── logo192.png │ ├── logo512.png │ ├── miditok_logo_stroke.webp │ ├── manifest.json │ └── index.html ├── .env.production ├── Dockerfile ├── tsconfig.json ├── .gitignore └── package.json ├── old ├── heroku(do naprawy) │ ├── start.sh │ ├── heroku.yml │ └── Dockerfile └── heroku.yml ├── example_files ├── test.mid ├── test2.mid ├── bethlem2.mid ├── youre only lonely L.mid ├── Never-Gonna-Give-You-Up-3.mid └── Queen - Bohemian Rhapsody.mid ├── docs ├── img │ ├── app_screenshot.png │ └── miditok_visualizer_small.png ├── WIMU-GR3-Dokumentacja.pdf ├── WIMU-GR3-DesignProposal.pdf ├── WIMU-GR3-24Z-DesignProposal.pdf ├── Design documentation │ ├── 23Z │ │ ├── WIMU-GR3-Dokumentacja.pdf │ │ └── WIMU-GR3-DesignProposal.pdf │ ├── 24Z │ │ ├── WIMU-GR3-24Z-DesignProposal.pdf │ │ ├── 24Z_update.md │ │ └── GR5_design_proposal.md │ ├── 25L │ │ ├── Meeting_Summary.md │ │ ├── Semester_summary.md │ │ ├── Design_proposal_25L.md │ │ └── Weekly_Update.md │ └── 24L │ │ └── notes_pl.md ├── Development documentation │ ├── service │ │ ├── NoteId.md │ │ ├── MidiEvent.md │ │ ├── MidiLoader.md │ │ ├── MidiProcessing.md │ │ ├── NoteExtractor.md │ │ ├── MidiInfoExtractor.md │ │ ├── TokenizerConfig.md │ │ └── TokenizerFactory.md │ └── api │ │ ├── api.md │ │ └── model.md ├── css │ └── extra.css ├── Technical documentation │ └── RailwayDeploy.md └── index.md ├── .gitignore ├── docker-compose.yaml ├── railway ├── frontend │ └── Dockerfile └── backend │ └── Dockerfile ├── railway.json ├── .github └── workflows │ ├── github-actions-react-ci.yml │ ├── github-actions-python-ci.yml │ └── github-actions-mkdocs.yml ├── mkdocs.yml ├── 24Z_update.md └── README.md /backend/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/core/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/core/service/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/core/service/midi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/core/service/note/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start -------------------------------------------------------------------------------- /frontend/src/components/FileUpload.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/core/service/tokenizer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/core/service/tokenizer/tokenizers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | // .dockerignore 2 | node_modules 3 | build -------------------------------------------------------------------------------- /backend/Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn core.api.api:app --host 0.0.0.0 --port $PORT -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_API_BASE_URL=http://localhost:8000 2 | -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /old/heroku(do naprawy)/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | uvicorn core.api.api:app --host 0.0.0.0 --port $PORT -------------------------------------------------------------------------------- /example_files/test.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/example_files/test.mid -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /example_files/test2.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/example_files/test2.mid -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_API_BASE_URL=https://miditok-visualizer-back-production.up.railway.app 2 | -------------------------------------------------------------------------------- /frontend/public/bckg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/frontend/public/bckg.png -------------------------------------------------------------------------------- /old/heroku(do naprawy)/heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: Dockerfile 4 | 5 | run: 6 | web: ./start.sh -------------------------------------------------------------------------------- /docs/img/app_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/docs/img/app_screenshot.png -------------------------------------------------------------------------------- /example_files/bethlem2.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/example_files/bethlem2.mid -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/frontend/public/favicon.png -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /backend/core/data/example.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/backend/core/data/example.mid -------------------------------------------------------------------------------- /backend/core/data/example2.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/backend/core/data/example2.mid -------------------------------------------------------------------------------- /docs/WIMU-GR3-Dokumentacja.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/docs/WIMU-GR3-Dokumentacja.pdf -------------------------------------------------------------------------------- /docs/WIMU-GR3-DesignProposal.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/docs/WIMU-GR3-DesignProposal.pdf -------------------------------------------------------------------------------- /frontend/src/components/Spinner.css: -------------------------------------------------------------------------------- 1 | .spinner-container { 2 | width: 10px; 3 | margin: auto; 4 | display: block; 5 | } -------------------------------------------------------------------------------- /docs/WIMU-GR3-24Z-DesignProposal.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/docs/WIMU-GR3-24Z-DesignProposal.pdf -------------------------------------------------------------------------------- /docs/img/miditok_visualizer_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/docs/img/miditok_visualizer_small.png -------------------------------------------------------------------------------- /example_files/youre only lonely L.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/example_files/youre only lonely L.mid -------------------------------------------------------------------------------- /frontend/public/miditok_logo_stroke.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/frontend/public/miditok_logo_stroke.webp -------------------------------------------------------------------------------- /backend/core/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run("core.api.api:app", host="0.0.0.0", port=8000, reload=True) -------------------------------------------------------------------------------- /example_files/Never-Gonna-Give-You-Up-3.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/example_files/Never-Gonna-Give-You-Up-3.mid -------------------------------------------------------------------------------- /example_files/Queen - Bohemian Rhapsody.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/example_files/Queen - Bohemian Rhapsody.mid -------------------------------------------------------------------------------- /docs/Design documentation/23Z/WIMU-GR3-Dokumentacja.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/docs/Design documentation/23Z/WIMU-GR3-Dokumentacja.pdf -------------------------------------------------------------------------------- /docs/Design documentation/23Z/WIMU-GR3-DesignProposal.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/docs/Design documentation/23Z/WIMU-GR3-DesignProposal.pdf -------------------------------------------------------------------------------- /docs/Design documentation/24Z/WIMU-GR3-24Z-DesignProposal.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justleon/MidiTok-Visualizer/HEAD/docs/Design documentation/24Z/WIMU-GR3-24Z-DesignProposal.pdf -------------------------------------------------------------------------------- /backend/core/service/tokenizer/tokenizers/tsd_tokenizer.py: -------------------------------------------------------------------------------- 1 | from miditok import TSD 2 | 3 | 4 | class TSDTokenizer(TSD): 5 | def __init__(self, config): 6 | super().__init__(config) 7 | -------------------------------------------------------------------------------- /backend/core/service/tokenizer/tokenizers/MMM_tokenizer.py: -------------------------------------------------------------------------------- 1 | from miditok import MMM 2 | 3 | 4 | class MMMTokenizer(MMM): 5 | 6 | def __init__(self, config): 7 | super().__init__(config) 8 | -------------------------------------------------------------------------------- /backend/core/service/tokenizer/tokenizers/remi_tokenizer.py: -------------------------------------------------------------------------------- 1 | from miditok import REMI 2 | 3 | 4 | class REMITokenizer(REMI): 5 | def __init__(self, config): 6 | super().__init__(config) 7 | -------------------------------------------------------------------------------- /backend/core/service/tokenizer/tokenizers/cpword_tokenizer.py: -------------------------------------------------------------------------------- 1 | from miditok import CPWord 2 | 3 | 4 | class CPWordTokenizer(CPWord): 5 | def __init__(self, config): 6 | super().__init__(config) 7 | -------------------------------------------------------------------------------- /backend/core/service/tokenizer/tokenizers/midilike_tokenizer.py: -------------------------------------------------------------------------------- 1 | from miditok import MIDILike 2 | 3 | 4 | class MIDILikeTokenizer(MIDILike): 5 | def __init__(self, config): 6 | super().__init__(config) 7 | -------------------------------------------------------------------------------- /backend/core/service/tokenizer/tokenizers/muMIDI_tokenizer.py: -------------------------------------------------------------------------------- 1 | from miditok import MuMIDI 2 | 3 | 4 | class MuMIDITokenizer(MuMIDI): 5 | 6 | def __init__(self, config): 7 | super().__init__(config) 8 | -------------------------------------------------------------------------------- /backend/core/service/tokenizer/tokenizers/octuple_tokenizer.py: -------------------------------------------------------------------------------- 1 | from miditok import Octuple 2 | 3 | 4 | class OctupleTokenizer(Octuple): 5 | def __init__(self, config): 6 | super().__init__(config) 7 | -------------------------------------------------------------------------------- /backend/core/service/tokenizer/tokenizers/perTok_tokenizer.py: -------------------------------------------------------------------------------- 1 | from miditok import PerTok 2 | 3 | 4 | class PerTokTokenizer(PerTok): 5 | 6 | def __init__(self, config): 7 | super().__init__(config) 8 | -------------------------------------------------------------------------------- /docs/Development documentation/service/NoteId.md: -------------------------------------------------------------------------------- 1 | 2 | # Note ID 3 | ::: backend.core.service.note.note_id 4 | handler: python 5 | options: 6 | show_root_heading: false 7 | show_source: true 8 | -------------------------------------------------------------------------------- /docs/Development documentation/service/MidiEvent.md: -------------------------------------------------------------------------------- 1 | # MIDI Event 2 | 3 | ::: backend.core.service.midi.midi_event 4 | handler: python 5 | options: 6 | show_root_heading: false 7 | show_source: true 8 | -------------------------------------------------------------------------------- /backend/core/service/tokenizer/tokenizers/structured_tokenizer.py: -------------------------------------------------------------------------------- 1 | from miditok import Structured 2 | 3 | 4 | class StructuredTokenizer(Structured): 5 | def __init__(self, config): 6 | super().__init__(config) 7 | -------------------------------------------------------------------------------- /docs/Development documentation/service/MidiLoader.md: -------------------------------------------------------------------------------- 1 | # MIDI Loader Class 2 | 3 | ::: backend.core.service.midi.midi_loader 4 | handler: python 5 | options: 6 | show_root_heading: false 7 | show_source: true 8 | -------------------------------------------------------------------------------- /docs/Development documentation/service/MidiProcessing.md: -------------------------------------------------------------------------------- 1 | # MIDI Processing 2 | 3 | ::: backend.core.service.midi_processing 4 | handler: python 5 | options: 6 | show_root_heading: false 7 | show_source: true 8 | -------------------------------------------------------------------------------- /docs/Development documentation/service/NoteExtractor.md: -------------------------------------------------------------------------------- 1 | # Note Extractor 2 | 3 | ::: backend.core.service.note.note_extractor 4 | handler: python 5 | options: 6 | show_root_heading: false 7 | show_source: true 8 | -------------------------------------------------------------------------------- /docs/Development documentation/service/MidiInfoExtractor.md: -------------------------------------------------------------------------------- 1 | # MIDI Info Extractor 2 | 3 | ::: backend.core.service.midi.midi_info_extractor 4 | handler: python 5 | options: 6 | show_root_heading: false 7 | show_source: true 8 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:21-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json package-lock.json* ./ 6 | 7 | RUN npm install && npm cache clean --force 8 | 9 | COPY . . 10 | 11 | EXPOSE 3000 12 | 13 | CMD ["npm", "run", "dev"] -------------------------------------------------------------------------------- /docs/Development documentation/service/TokenizerConfig.md: -------------------------------------------------------------------------------- 1 | 2 | # Tokenizer Config 3 | 4 | ::: backend.core.service.tokenizer.tokenizer_config 5 | handler: python 6 | options: 7 | show_root_heading: false 8 | show_source: true 9 | -------------------------------------------------------------------------------- /docs/Development documentation/service/TokenizerFactory.md: -------------------------------------------------------------------------------- 1 | 2 | # Tokenizer Factory 3 | 4 | ::: backend.core.service.tokenizer.tokenizer_factory 5 | handler: python 6 | options: 7 | show_root_heading: false 8 | show_source: true 9 | -------------------------------------------------------------------------------- /frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore general configuration files 2 | .idea/ 3 | .vscode/ 4 | .venv 5 | localtest 6 | .DS_Store 7 | Thumbs.db 8 | .pytest_cache/ 9 | local_settings.py 10 | logs/ 11 | site/ 12 | backend_new/ 13 | node_modules/ 14 | frontend/node_modules/ 15 | package-lock.json 16 | package.json -------------------------------------------------------------------------------- /frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | jest.mock('tone', () => ({})); 5 | test('submit file button', () => { 6 | render(); 7 | const linkElement = screen.getByText(/Upload/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /docs/css/extra.css: -------------------------------------------------------------------------------- 1 | 2 | .wy-nav-content { 3 | max-width: 1200px !important; 4 | } 5 | 6 | 7 | .wy-table-responsive table td, .wy-table-responsive table th { 8 | white-space: normal; 9 | } 10 | 11 | .wy-table-responsive { 12 | overflow: visible !important; 13 | max-width: 100%; 14 | } 15 | 16 | 17 | .highlight { 18 | max-width: 100%; 19 | overflow-x: auto; 20 | } -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /old/heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | - image: backend_old/railway/Dockerfile 4 | - image: frontend/Dockerfile 5 | 6 | release: 7 | command: bash -c 'echo "Zakończono budowanie aplikacji!"' 8 | 9 | containers: 10 | - type: backend_old 11 | path: /app 12 | build: 13 | docker: 14 | context: ./backend_old/railway 15 | - type: frontend 16 | path: /app 17 | build: 18 | docker: 19 | context: ./frontend 20 | #test -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | backend: 5 | build: 6 | context: backend 7 | dockerfile: Dockerfile 8 | restart: unless-stopped 9 | environment: 10 | - DOCKER_BUILDKIT=1 11 | ports: 12 | - 8000:8000 13 | frontend: 14 | build: 15 | context: ./frontend 16 | dockerfile: Dockerfile 17 | environment: 18 | - WATCHPACK_POLLING=true 19 | volumes: 20 | - ./frontend/src/:/app/src/ 21 | ports: 22 | - 3000:3000 23 | -------------------------------------------------------------------------------- /railway/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:21-alpine AS build 2 | ENV GENERATE_SOURCEMAP=false 3 | ENV CI=false 4 | ENV REACT_APP_API_BASE_URL="https://miditok-visualizer-back-production.up.railway.app" 5 | WORKDIR /app 6 | COPY frontend/package.json frontend/package-lock.json* ./ 7 | RUN npm install && npm cache clean --force 8 | COPY frontend/ . 9 | RUN npm run build 10 | 11 | FROM node:21-alpine AS production 12 | RUN npm install -g serve 13 | WORKDIR /app 14 | COPY --from=build /app/build ./build 15 | EXPOSE 3000 16 | CMD ["serve", "-s", "build", "-l", "3000"] 17 | -------------------------------------------------------------------------------- /railway.json: -------------------------------------------------------------------------------- 1 | { 2 | "services": { 3 | "7c51be2c-4e29-415e-bc14-40e7c7537aec": { 4 | "name": "back", 5 | "build": { 6 | "builder": "docker", 7 | "dockerfilePath": "railway/backend/Dockerfile", 8 | "buildContext": "." 9 | } 10 | }, 11 | "114f992a-2c0a-49f9-be31-8c9b830d1bbb": { 12 | "name": "front", 13 | "build": { 14 | "builder": "docker", 15 | "dockerfilePath": "railway/frontend/Dockerfile", 16 | "buildContext": "." 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/components/MidiCreate/types/midi.ts: -------------------------------------------------------------------------------- 1 | export interface NoteEventPayload { 2 | event_type: "note"; 3 | track: number; 4 | time: number; 5 | duration: number; 6 | pitch: number; 7 | velocity: number; 8 | channel: number; 9 | } 10 | 11 | export interface TempoEventPayload { 12 | event_type: "tempo"; 13 | track: number; 14 | time: number; 15 | bpm: number; 16 | } 17 | 18 | export type MIDIEventPayload = NoteEventPayload | TempoEventPayload; 19 | 20 | export interface MIDIConversionRequestPayload { 21 | events: MIDIEventPayload[]; 22 | ticks_per_beat: number; 23 | output_filename: string; 24 | } -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /backend/tests/test_api.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi.testclient import TestClient 3 | 4 | from core.api.api import app 5 | from core.constants import EXAMPLE_MIDI_FILE_PATH 6 | 7 | client = TestClient(app) 8 | 9 | 10 | @pytest.fixture 11 | def event_loop(): 12 | import asyncio 13 | 14 | loop = asyncio.new_event_loop() 15 | yield loop 16 | loop.close() 17 | 18 | 19 | @pytest.mark.asyncio 20 | async def test_upload_file(): 21 | with open(EXAMPLE_MIDI_FILE_PATH, "rb") as file: 22 | form_data = {"file": file} 23 | response = client.post("/process", files=form_data) 24 | assert response.status_code == 422 25 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "typeRoots": ["./node_modules/@types"] 23 | }, 24 | "include": [ 25 | "src" 26 | ], 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/github-actions-react-ci.yml: -------------------------------------------------------------------------------- 1 | name: Frontend CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'frontend/**' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: '21' 21 | - name: Install dependencies 22 | run: cd frontend && npm install 23 | 24 | - name: Build the frontend 25 | run: cd frontend && CI=false npm run build 26 | 27 | - name: Run tests with npm 28 | run: cd frontend && npm run test 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js 2 | node_modules/ 3 | 4 | # npm 5 | package-lock.json 6 | npm-debug.log 7 | yarn-error.log 8 | yarn.lock 9 | 10 | # Compiled JavaScript 11 | dist/ 12 | build/ 13 | out/ 14 | *.js 15 | 16 | # TypeScript 17 | *.tsbuildinfo 18 | 19 | # Environment Variables 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | # IDE Files 26 | .vscode/ 27 | .idea/ 28 | 29 | # Logs 30 | *.log 31 | 32 | # Dependency directories 33 | .jest/ 34 | coverage/ 35 | .pnpm/ 36 | pnpm-lock.yaml 37 | test-results/ 38 | 39 | # Testing 40 | e2e/ 41 | __snapshots__/ 42 | cypress/videos/ 43 | cypress/screenshots/ 44 | jest.config.js 45 | jest.setup.js 46 | 47 | # Build 48 | build/ 49 | *.map -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-buster as builder 2 | 3 | RUN pip install poetry==1.6.1 4 | 5 | ENV POETRY_NO_INTERACTION=1 \ 6 | POETRY_VIRTUALENVS_IN_PROJECT=1 \ 7 | POETRY_VIRTUALENVS_CREATE=1 \ 8 | POETRY_CACHE_DIR=/tmp/poetry_cache 9 | 10 | WORKDIR /app 11 | 12 | COPY pyproject.toml poetry.lock ./ 13 | RUN touch README.md 14 | 15 | RUN poetry install --no-root 16 | #RUN --mount=type=cache,target=$POETRY_CACHE_DIR poetry install --no-root 17 | COPY core ./core 18 | 19 | 20 | FROM python:3.11-slim-buster as runtime 21 | 22 | ENV VIRTUAL_ENV=/app/.venv \ 23 | PATH="/app/.venv/bin:$PATH" 24 | 25 | COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} 26 | 27 | COPY core ./core 28 | 29 | ENTRYPOINT ["python", "-m", "core.main"] -------------------------------------------------------------------------------- /backend/core/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | ROOT_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), ".")) 4 | DATA_DIR = os.path.join(ROOT_DIR, "data") 5 | 6 | EXAMPLE_MIDI_FILE_NAME = "example.mid" 7 | EXAMPLE_MIDI_FILE_PATH = os.path.join(DATA_DIR, EXAMPLE_MIDI_FILE_NAME) 8 | EXAMPLE_MIDI_FILE_PATH2= os.path.join(DATA_DIR, "example2.mid") 9 | DEFAULT_TOKENIZER_PARAMS = { 10 | "pitch_range": (21, 109), 11 | "beat_res": {(0, 4): 8, (4, 12): 4}, 12 | "num_velocities": 32, 13 | "special_tokens": ["PAD", "BOS", "EOS", "MASK"], 14 | "use_chords": True, 15 | "use_rests": False, 16 | "use_tempos": True, 17 | "use_time_signatures": False, 18 | "use_programs": False, 19 | "num_tempos": 32, 20 | "tempo_range": (40, 250), 21 | } 22 | -------------------------------------------------------------------------------- /backend/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.11.9 3 | repos: 4 | - repo: local 5 | hooks: 6 | - id: black 7 | name: black 8 | language: system 9 | entry: black 10 | args: ["--check"] 11 | types: [python] 12 | - id: isort 13 | name: isort 14 | language: system 15 | entry: isort 16 | args: ["--check", "--diff"] 17 | types: [python] 18 | - repo: https://github.com/charliermarsh/ruff-pre-commit 19 | rev: v0.9.1 20 | hooks: 21 | - id: ruff 22 | name: ruff 23 | types: [python] 24 | args: ["--fix"] 25 | - repo: https://github.com/pre-commit/mirrors-mypy 26 | rev: v1.14.1 27 | hooks: 28 | - id: mypy 29 | name: mypy 30 | args: ["--no-incremental", "--ignore-missing-imports"] 31 | types: [python] -------------------------------------------------------------------------------- /railway/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-buster AS builder 2 | RUN apt-get update && apt-get install -y libc6 3 | RUN pip install poetry==1.6.1 4 | ENV POETRY_NO_INTERACTION=1 \ 5 | POETRY_VIRTUALENVS_IN_PROJECT=1 \ 6 | POETRY_VIRTUALENVS_CREATE=1 7 | ENV REACT_APP_API_BASE_URL="https://miditok-visualizer-front-production.up.railway.app" 8 | WORKDIR /app 9 | COPY backend/pyproject.toml backend/poetry.lock ./ 10 | RUN touch README.md 11 | RUN poetry install --no-root 12 | COPY backend/core ./core 13 | COPY backend/tests ./tests 14 | RUN poetry run pytest 15 | 16 | FROM python:3.11-slim-buster AS runtime 17 | ENV VIRTUAL_ENV=/app/.venv \ 18 | PATH="/app/.venv/bin:$PATH" \ 19 | REACT_APP_API_BASE_URL="https://miditok-visualizer-front-production.up.railway.app" 20 | COPY --from=builder /app/.venv /app/.venv 21 | WORKDIR /app 22 | COPY backend/core ./core 23 | ENTRYPOINT ["python", "-m", "core.main"] 24 | -------------------------------------------------------------------------------- /frontend/src/components/MusicInfoDisplay.css: -------------------------------------------------------------------------------- 1 | .music-info-block { 2 | text-align: center; 3 | display: inline-block; 4 | width: 200px; 5 | height: 100px; 6 | border: 2px solid black; 7 | border-radius: 5px; 8 | margin: 5px; 9 | position: relative; 10 | background-color: white 11 | } 12 | 13 | .music-info-title { 14 | font-size: 24px; 15 | margin-bottom: 5px; 16 | text-align: center; 17 | } 18 | 19 | .music-info-resolution { 20 | font-size: 16px; 21 | margin-bottom: 5px; 22 | text-align: center; 23 | } 24 | 25 | .music-info-container { 26 | display: flex; 27 | justify-content: center; 28 | flex-wrap: wrap; 29 | gap: 10px; 30 | } 31 | 32 | .music-info-content { 33 | position: absolute; 34 | top: 50%; 35 | left: 50%; 36 | transform: translate(-50%, -50%); 37 | } 38 | 39 | .music-info-text { 40 | font-size: 12px; 41 | } 42 | -------------------------------------------------------------------------------- /old/heroku(do naprawy)/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:21-alpine AS frontend-builder 2 | WORKDIR /app/frontend 3 | COPY ../../frontend/package.json frontend/package-lock.json* ./ 4 | RUN npm install 5 | COPY ../../frontend ./ 6 | RUN npm run build 7 | 8 | FROM python:3.11-buster 9 | WORKDIR /app 10 | 11 | RUN apt-get update && apt-get install -y \ 12 | nodejs \ 13 | npm \ 14 | && apt-get clean \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | 18 | COPY ../../backend_old/pyproject.toml backend/poetry.lock ./ 19 | RUN touch README.md 20 | 21 | 22 | COPY ../../backend_old/core ./core 23 | 24 | RUN pip install poetry==1.6.1 && \ 25 | poetry config virtualenvs.create false && \ 26 | poetry install --no-interaction --no-ansi && \ 27 | pip install gunicorn uvicorn fastapi 28 | 29 | 30 | COPY --from=frontend-builder /app/frontend/build/ ./static 31 | 32 | 33 | RUN npm install -g serve 34 | 35 | 36 | COPY start.sh ./ 37 | RUN chmod +x start.sh 38 | 39 | CMD ["./start.sh"] -------------------------------------------------------------------------------- /backend/core/service/serializer.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import numpy as np 4 | from miditok import Event, TokSequence 5 | 6 | 7 | def get_serialized_tokens(tokens: list[TokSequence]) -> str: 8 | return json.dumps(tokens, cls=TokSequenceEncoder) 9 | 10 | 11 | class TokSequenceEncoder(json.JSONEncoder): 12 | def default(self, obj): 13 | if isinstance(obj, TokSequence): 14 | return obj.events 15 | if isinstance(obj, Event): 16 | return { 17 | "type": obj.type_, 18 | "value": obj.value, 19 | "time": obj.time, 20 | "program": obj.program, 21 | "desc": obj.desc, 22 | "note_id": getattr(obj, "note_id", None), 23 | "track_id": getattr(obj, "track_id", None), 24 | } 25 | if isinstance(obj, np.integer): 26 | return int(obj) 27 | if isinstance(obj, np.floating): 28 | return float(obj) 29 | return super(TokSequenceEncoder, self).default(obj) 30 | -------------------------------------------------------------------------------- /backend/tests/test_info_retrieval.py: -------------------------------------------------------------------------------- 1 | import muspy 2 | 3 | from core.constants import EXAMPLE_MIDI_FILE_PATH 4 | 5 | from core.service.midi.midi_info_extractor import MidiInformationExtractor 6 | 7 | 8 | def test_retrieve_basic_info(): 9 | music_file = muspy.read(EXAMPLE_MIDI_FILE_PATH) 10 | midi_info_extractor = MidiInformationExtractor() 11 | basic_info_data=midi_info_extractor._extract_basic_data(music_file) 12 | assert basic_info_data 13 | print(basic_info_data) 14 | 15 | 16 | def test_retrieve_metrics(): 17 | music_file = muspy.read(EXAMPLE_MIDI_FILE_PATH) 18 | midi_info_extractor = MidiInformationExtractor() 19 | metrics=midi_info_extractor._extract_metrics(music_file) 20 | assert metrics 21 | print(metrics) 22 | 23 | 24 | # def test_retrieve_info(): 25 | # music_obj = open(EXAMPLE_MIDI_FILE_PATH, "rb") 26 | # 27 | # music_obj_bytes: bytes = music_obj.read() 28 | # midi_info_extractor = MidiInformationExtractor() 29 | # music = midi_info_extractor.extract_information(music_obj_bytes) 30 | # assert music 31 | # print(music) 32 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized Python files 2 | __pycache__/ 3 | *.pyc 4 | *.pyo 5 | __pycache__/* 6 | *.pyc 7 | *.pyo 8 | 9 | # Virtual Environment 10 | venv/ 11 | env/ 12 | ENV/ 13 | 14 | # Logs 15 | *.log 16 | 17 | # Ignore database files 18 | *.db 19 | 20 | # Ignore migration scripts 21 | alembic/versions 22 | 23 | # Ignore generated documentation 24 | docs/_build/ 25 | 26 | # Ignore configuration files 27 | *.ini 28 | *.conf 29 | *.env 30 | 31 | # Ignore development environment specific files 32 | *.env 33 | *.swp 34 | 35 | # Ignore operating system generated files 36 | .DS_Store 37 | Thumbs.db 38 | 39 | # Ignore Python egg and distribution directories 40 | *.egg-info/ 41 | dist/ 42 | build/ 43 | eggs/ 44 | parts/ 45 | var/ 46 | sdist/ 47 | develop-eggs/ 48 | .installed.cfg 49 | lib/ 50 | lib64/ 51 | parts/ 52 | var/ 53 | wheels/ 54 | pip-wheel-metadata/ 55 | 56 | # Ignore development database configuration 57 | *.sqlite3 58 | 59 | # Ignore fastapi generated files 60 | alembic.ini 61 | alembic.lock 62 | alembic/versions/*.py 63 | alembic/versions/*.pyc 64 | 65 | # Ignore poetry files 66 | poetry/core/* -------------------------------------------------------------------------------- /frontend/src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface SpinnerProps { 4 | size?: 'small' | 'medium' | 'large'; 5 | color?: string; 6 | text?: string; 7 | } 8 | 9 | const Spinner: React.FC = ({ 10 | size = 'medium', 11 | color = '#FFFFFF', 12 | text 13 | }) => { 14 | 15 | const getSpinnerSize = () => { 16 | switch (size) { 17 | case 'small': 18 | return 16; 19 | case 'large': 20 | return 32; 21 | case 'medium': 22 | default: 23 | return 20; 24 | } 25 | }; 26 | 27 | const spinnerSize = getSpinnerSize(); 28 | 29 | const spinnerStyle = { 30 | width: `${spinnerSize}px`, 31 | height: `${spinnerSize}px`, 32 | borderWidth: `${Math.max(2, spinnerSize / 8)}px`, 33 | borderColor: `rgba(255, 255, 255, 0.2)`, 34 | borderTopColor: color 35 | }; 36 | 37 | return ( 38 |
39 |
43 | {text && {text}} 44 |
45 | ); 46 | }; 47 | 48 | export default Spinner; -------------------------------------------------------------------------------- /frontend/src/interfaces/ApiResponse.tsx: -------------------------------------------------------------------------------- 1 | interface Token { 2 | type: string; 3 | value: string; 4 | time: number; 5 | program: number; 6 | desc: string; 7 | note_id?: string | null; 8 | track_id?: string | null; 9 | } 10 | 11 | interface Note { 12 | pitch: number; 13 | name: string; 14 | start: number; 15 | end: number; 16 | velocity: number; 17 | note_id: string; 18 | } 19 | 20 | interface MusicInfoData { 21 | title: string; 22 | resolution: number; 23 | tempos: Array<[number, number]>; 24 | key_signatures: Array<[number, number, string]>; 25 | time_signatures: Array<[number, number, number]>; 26 | 27 | pitch_range: number; 28 | n_pitches_used: number; 29 | polyphony: number; 30 | 31 | empty_beat_rate: number; 32 | drum_pattern_consistency: number; 33 | } 34 | 35 | interface DataStructure { 36 | tokens: NestedList; 37 | metrics: MusicInfoData; 38 | notes: Note[][]; 39 | } 40 | 41 | interface ApiResponse { 42 | success: boolean; 43 | data: DataStructure; 44 | error: string; 45 | } 46 | 47 | type NestedList = Array>; 48 | 49 | export type { Token, Note, ApiResponse, NestedList, MusicInfoData }; 50 | -------------------------------------------------------------------------------- /frontend/src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ErrorInfo, ReactNode } from 'react'; 2 | 3 | interface ErrorBoundaryProps { 4 | children: ReactNode; 5 | fallback: ReactNode; 6 | } 7 | 8 | interface ErrorBoundaryState { 9 | hasError: boolean; 10 | error: Error | null; 11 | errorInfo: ErrorInfo | null; 12 | } 13 | 14 | class ErrorBoundary extends Component { 15 | constructor(props: ErrorBoundaryProps) { 16 | super(props); 17 | this.state = { 18 | hasError: false, 19 | error: null, 20 | errorInfo: null, 21 | }; 22 | } 23 | 24 | static getDerivedStateFromError(error: Error) { 25 | return { 26 | hasError: true, 27 | error: error, 28 | errorInfo: null, 29 | }; 30 | } 31 | 32 | componentDidCatch(error: Error, info: ErrorInfo) { 33 | console.error('Error caught by componentDidCatch:', error, info); 34 | } 35 | 36 | render() { 37 | if (this.state.hasError) { 38 | // You can render any custom fallback UI 39 | return this.props.fallback; 40 | } 41 | 42 | return this.props.children; 43 | } 44 | } 45 | 46 | export default ErrorBoundary; 47 | -------------------------------------------------------------------------------- /.github/workflows/github-actions-python-ci.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'backend/**' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.11.9' 22 | 23 | - name: Install Poetry 24 | run: | 25 | curl -sSL https://install.python-poetry.org | python3 - 26 | echo "$HOME/.local/bin" >> $GITHUB_PATH 27 | 28 | - name: Configure Poetry 29 | run: | 30 | poetry config virtualenvs.create true 31 | poetry config virtualenvs.in-project false 32 | poetry config virtualenvs.path "$HOME/.cache/pypoetry/virtualenvs" 33 | 34 | - name: Install dependencies 35 | run: | 36 | cd backend && poetry install && poetry run pip install setuptools 37 | 38 | 39 | - name: Run tests with unittest 40 | run: | 41 | cd backend 42 | export PYTHONPATH=$(pwd) 43 | poetry run pytest 44 | -------------------------------------------------------------------------------- /frontend/src/components/MidiCreate/SettingsPanel.css: -------------------------------------------------------------------------------- 1 | /* SettingsPanel.css */ 2 | 3 | .settings-panel { 4 | margin-bottom: 20px; 5 | padding: 10px; 6 | border: 1px solid #ddd; 7 | border-radius: 5px; 8 | background-color: white; 9 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 10 | } 11 | 12 | .settings-panel h3 { 13 | margin-top: 0; 14 | margin-bottom: 15px; 15 | font-size: 1.2rem; 16 | color: #333; 17 | border-bottom: 1px solid #eee; 18 | padding-bottom: 8px; 19 | } 20 | 21 | .settings-panel div { 22 | margin-bottom: 10px; 23 | } 24 | 25 | .settings-panel label { 26 | display: inline-block; 27 | min-width: 180px; 28 | margin-right: 10px; 29 | font-weight: bold; 30 | color: #555; 31 | } 32 | 33 | .settings-panel input { 34 | padding: 8px; 35 | border: 1px solid #ddd; 36 | border-radius: 4px; 37 | transition: border-color 0.3s; 38 | width: 300px; 39 | } 40 | 41 | .settings-panel input:focus { 42 | border-color: #4285f4; 43 | outline: none; 44 | box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2); 45 | } 46 | 47 | /* Responsive adjustments */ 48 | @media (max-width: 768px) { 49 | .settings-panel label { 50 | display: block; 51 | margin-bottom: 5px; 52 | } 53 | 54 | .settings-panel input { 55 | width: 100%; 56 | max-width: 200px; 57 | } 58 | } -------------------------------------------------------------------------------- /frontend/src/components/PianoRollDisplay.css: -------------------------------------------------------------------------------- 1 | .piano-roll-container { 2 | position: relative; 3 | max-width: 100%; 4 | } 5 | 6 | .piano-roll-scrollable { 7 | display: flex; 8 | position: relative; 9 | overflow-x: auto; 10 | overflow-y: hidden; 11 | } 12 | 13 | .piano-roll-key-column { 14 | position: sticky; 15 | left: 0; 16 | z-index: 10; 17 | background-color: white; 18 | border-right: 2px solid black; 19 | } 20 | 21 | .piano-roll-key { 22 | position: absolute; 23 | border: 2px solid black; 24 | border-radius: 5px 0 0 5px; 25 | display: flex; 26 | align-items: center; 27 | justify-content: center; 28 | font-size: 10px; 29 | font-weight: bold; 30 | } 31 | 32 | .piano-roll-key.white { 33 | background-color: white; 34 | color: black; 35 | } 36 | 37 | .piano-roll-key.black { 38 | background-color: #0f0f26; 39 | color: white; 40 | } 41 | 42 | .piano-roll-canvas { 43 | background-color: #f8f8f8; 44 | } 45 | 46 | .piano-roll-bottom-scroll { 47 | overflow-x: auto; 48 | overflow-y: hidden; 49 | height: 20px; 50 | position: sticky; 51 | bottom: 0; 52 | z-index: 10; 53 | background-color: white; 54 | } 55 | 56 | .piano-roll-bottom-scroll-placeholder { 57 | height: 1px; 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/components/SingleValueSlider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ReactSlider from 'react-slider'; 3 | 4 | interface SingleValueSliderProps { 5 | onValueChange: (newValue: number) => void; 6 | initialValue: number; 7 | limits: number[]; 8 | step?: number; 9 | } 10 | 11 | const SingleValueSlider: React.FC = ({ onValueChange, initialValue, limits, step = 1 }) => { 12 | const [value, setValue] = useState(initialValue); 13 | 14 | const handleSliderChange = (newValue: number) => { 15 | setValue(newValue); 16 | onValueChange(newValue); 17 | }; 18 | 19 | return ( 20 |
21 | `Thumb value ${state.valueNow}`} 28 | renderThumb={(props, state) => ( 29 |
30 | {state.valueNow} 31 |
32 | )} 33 | min={limits[0]} 34 | max={limits[1]} 35 | step={step} 36 | onChange={handleSliderChange} 37 | /> 38 |
39 | ); 40 | }; 41 | 42 | export default SingleValueSlider; 43 | -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "backend" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Your Name "] 6 | packages = [{include = "core"}] 7 | 8 | [tool.poetry.dependencies] 9 | python = ">=3.11,<3.12" 10 | fastapi = "^0.115.6" 11 | uvicorn = "^0.32.0" 12 | miditok = "^3.0.4" 13 | python-multipart = "^0.0.19" 14 | muspy = "^0.5.0" 15 | mido = "^1.3.3" 16 | 17 | 18 | [tool.poetry.group.dev.dependencies] 19 | pytest = "^8.3.4" 20 | httpx = "^0.28.1" 21 | pytest-asyncio = "^0.25.0" 22 | black = "^24.10.0" 23 | ruff = "^0.8.3" 24 | isort = "^5.13.2" 25 | mypy = "^1.13.0" 26 | pre-commit = "^3.6.0" 27 | 28 | [build-system] 29 | requires = ["poetry-core"] 30 | build-backend = "poetry.core.masonry.api" 31 | 32 | [tool.black] 33 | line-length = 119 34 | 35 | [tool.mypy] 36 | python_version = "3.11" 37 | mypy_path = "core/" 38 | disallow_incomplete_defs = true 39 | warn_redundant_casts = true 40 | no_implicit_optional = true 41 | no_implicit_reexport = true 42 | strict_equality = true 43 | namespace_packages = true 44 | check_untyped_defs = true 45 | ignore_missing_imports = true 46 | 47 | [tool.ruff] 48 | extend-exclude = [ 49 | ".venv", 50 | "dist", 51 | ] 52 | ignore = [ 53 | "E402", 54 | "E501", 55 | ] 56 | select = [ 57 | "E", 58 | "F", 59 | "W", 60 | ] 61 | 62 | [tool.isort] 63 | profile = "black" 64 | line_length = 119 65 | skip_gitignore = true 66 | multi_line_output = 3 67 | -------------------------------------------------------------------------------- /frontend/src/components/RangeSlider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ReactSlider from 'react-slider'; 3 | 4 | interface RangeSliderProps { 5 | onRangeChange: (newValues: number[]) => void; 6 | initialValues: number[]; 7 | limits: number[]; 8 | } 9 | 10 | const RangeSlider: React.FC = ({ onRangeChange, initialValues, limits }) => { 11 | const [values, setValues] = useState(initialValues); 12 | 13 | const handleSliderChange = (newValues: number | number[]) => { 14 | if (Array.isArray(newValues)) { 15 | setValues(newValues); 16 | onRangeChange(newValues); 17 | } 18 | }; 19 | 20 | return ( 21 |
22 | `Thumb value ${state.valueNow}`} 29 | renderThumb={(props, state) => ( 30 |
31 | {state.valueNow} 32 |
33 | )} 34 | pearling 35 | minDistance={10} 36 | min={limits[0]} 37 | max={limits[1]} 38 | onChange={handleSliderChange} 39 | /> 40 |
41 | ); 42 | }; 43 | 44 | export default RangeSlider; 45 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: MidiTok Visualizer 2 | theme: 3 | name: readthedocs 4 | navigation_depth: 4 5 | collapse_navigation: false 6 | titles_only: false 7 | sticky_navigation: true 8 | extra_css: 9 | - css/extra.css 10 | plugins: 11 | - exclude: 12 | glob: 13 | - Design documentation/* 14 | - mkdocstrings: 15 | handlers: 16 | python: 17 | options: 18 | show_root_heading: true 19 | show_root_toc_entry: true 20 | show_category_heading: true 21 | heading_level: 2 22 | group_by_category: true 23 | members_order: source 24 | show_submodules: false 25 | filters: 26 | - "!^_(?!_init_|_)[^_]" 27 | inherited_members: true 28 | 29 | docstring_section_style: table 30 | line_length: 80 31 | show_signature_annotations: true 32 | docstring_options: 33 | ignore_init_summary: false 34 | show_source: true 35 | - autorefs: 36 | link_titles: false 37 | - search 38 | 39 | markdown_extensions: 40 | - admonition 41 | - pymdownx.details 42 | - pymdownx.superfences: 43 | custom_fences: 44 | - name: mermaid 45 | class: mermaid 46 | format: !!python/name:pymdownx.superfences.fence_code_format 47 | - pymdownx.tabbed: 48 | - pymdownx.snippets 49 | - pymdownx.highlight: 50 | anchor_linenums: true 51 | line_spans: __span 52 | pygments_lang_class: true 53 | - attr_list 54 | - def_list 55 | - tables 56 | - footnotes 57 | - toc: 58 | permalink: false -------------------------------------------------------------------------------- /frontend/src/components/MidiCreate/PlaybackControls.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './PlaybackControls.css'; 3 | 4 | interface PlaybackControlsProps { 5 | isPlaying: boolean; 6 | isDrawing: boolean; 7 | isRecording: boolean; 8 | midiArray: number; 9 | stopPlayback: () => void; 10 | handlePlayMidi: () => void; 11 | handleGenerateMidi: () => void; 12 | clearCanvas: () => void; 13 | } 14 | 15 | const PlaybackControls: React.FC = ({ 16 | isPlaying, 17 | isDrawing, 18 | isRecording, 19 | midiArray, 20 | stopPlayback, 21 | handlePlayMidi, 22 | handleGenerateMidi, 23 | clearCanvas 24 | }) => { 25 | return ( 26 |
27 | {isPlaying ? ( 28 | 34 | ) : ( 35 | 42 | )} 43 | 50 | 57 |
58 | ); 59 | }; 60 | 61 | export default PlaybackControls; -------------------------------------------------------------------------------- /frontend/src/components/MidiCreate/PlaybackControls.css: -------------------------------------------------------------------------------- 1 | /* PlaybackControls.css */ 2 | 3 | .playback-controls { 4 | display: flex; 5 | flex-wrap: wrap; 6 | gap: 10px; 7 | margin-top: 20px; 8 | justify-content: center; 9 | } 10 | 11 | .control-button { 12 | padding: 10px 15px; 13 | border: none; 14 | border-radius: 4px; 15 | background-color: #4285f4; 16 | color: white; 17 | cursor: pointer; 18 | font-size: 1rem; 19 | transition: background-color 0.3s, transform 0.2s; 20 | min-width: 80px; 21 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); 22 | } 23 | 24 | .control-button:hover:not(:disabled) { 25 | background-color: #3367d6; 26 | transform: translateY(-2px); 27 | } 28 | 29 | .control-button:active:not(:disabled) { 30 | transform: scale(0.98); 31 | } 32 | 33 | .control-button:disabled { 34 | background-color: #b7b7b7; 35 | cursor: not-allowed; 36 | box-shadow: none; 37 | opacity: 0.5; 38 | } 39 | 40 | .stop-button { 41 | background-color: #f44336; 42 | } 43 | 44 | .stop-button:hover:not(:disabled) { 45 | background-color: #d32f2f; 46 | } 47 | 48 | .play-button { 49 | background-color: #4CAF50; 50 | } 51 | 52 | .play-button:hover:not(:disabled) { 53 | background-color: #388E3C; 54 | } 55 | 56 | .generate-button { 57 | background-color: #2196F3; 58 | } 59 | 60 | .generate-button:hover:not(:disabled) { 61 | background-color: #1976D2; 62 | } 63 | 64 | .clear-button { 65 | background-color: #FF9800; 66 | } 67 | 68 | .clear-button:hover:not(:disabled) { 69 | background-color: #F57C00; 70 | } 71 | 72 | /* Responsive adjustments */ 73 | @media (max-width: 768px) { 74 | .playback-controls { 75 | flex-direction: column; 76 | align-items: center; 77 | } 78 | 79 | .control-button { 80 | width: 100%; 81 | } 82 | } -------------------------------------------------------------------------------- /docs/Design documentation/25L/Meeting_Summary.md: -------------------------------------------------------------------------------- 1 | # Notatka ze spotkania 17.4.25 2 | 3 | Spotkanie z projektu MidiTok Vizualizer odbyło się 17.4.25 o godzinie 11:20. 4 | ## Przegląd wykonanych prac: 5 | ### Backend:
6 | - Wdrożono działający system CI dla backendu.
7 | - Zaimplementowano wstępną integrację z MkDocs oraz fragmenty automatycznej dokumentacji kodu jak również deploy na Github Pages.
8 | - Przeprowadzono wstępną refaktoryzację pliku midi_processing – wprowadzono podział na klasy i metody, rozwiniętą funkcję ładowania, tworzenia node oraz ładowania configu o obsługę błędów.
9 | ### Frontend:
10 | - Dodano możliwość przypisywania kilku tokenizerów do jednego pliku.
11 | - Przygotowanie wstępnej funkcjonalność tworzenia kafelków, których pozycje trzeba będzie przetworzyć do MIDI.
12 | - Zakończono integrację nowego interfejsu użytkownika z poprzedniej iteracji projektu.
13 | ## Kolejne kroki: 14 | ### Frontend:
15 | - Rozwój funkcji związanych z tworzeniem i edycją MIDI (zgodnie z założeniami design proposal).
16 | - Poprawki w warstwie wizualnej interfejsu (np. układ elementów). 17 | ### Backend:
18 | - Refaktoryzacja kodu w kierunku paradygmatu obiektowego.
19 | - Dodanie komentarzy dokumentacyjnych do istniejącego kodu.
20 | - Implementacja wsparcia dla formatów muMIDI i MMM.
21 | - Usunięcie sekcji design proposal z dokumentacji MkDocs.
22 | ### Deploy:
23 | - Deploy aplikacji na serwis oraz sprzężenie z CI
24 | - Upewnienie się, że projekt działa lokalnie u wszystkich członków zespołu.
25 | ## Uwagi i zalecenia: 26 | - Przygotować zbiór testowy plików MIDI do weryfikacji funkcjonalności aplikacji.
27 | - Zorganizować automatyczną dokumentację backendu.
28 | - Deploy aplikacji na serwis oraz sprzężenie z CI 29 | -------------------------------------------------------------------------------- /.github/workflows/github-actions-mkdocs.yml: -------------------------------------------------------------------------------- 1 | name: Build GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'docs/**' 8 | workflow_dispatch: 9 | permissions: 10 | contents: write 11 | pages: write 12 | id-token: write 13 | 14 | jobs: 15 | build_mkdocs: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | 24 | - uses: actions/setup-python@v5 25 | with: 26 | python-version: "3.11" 27 | 28 | - name: Install documentation packages 29 | run: | 30 | pip install mkdocs==1.6.1 31 | pip install mkdocs-material==9.6.11 32 | pip install mkdocstrings==0.29.1 33 | pip install mkdocstrings-python==1.16.10 34 | pip install griffe==1.7.2 35 | pip install mkdocs-autorefs==1.4.1 36 | pip install mkdocs-get-deps==0.2.0 37 | pip install mkdocs-exclude 38 | 39 | - name: Build and deploy documentation 40 | run: | 41 | mkdocs build 42 | mkdocs gh-deploy --config-file mkdocs.yml --force 43 | 44 | deploy_mkdocs: 45 | needs: build_mkdocs 46 | environment: 47 | name: github-pages 48 | url: ${{ steps.deployment.outputs.page_url }} 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Checkout gh-pages branch 52 | uses: actions/checkout@v4 53 | with: 54 | ref: gh-pages 55 | - name: Setup Pages 56 | uses: actions/configure-pages@v5 57 | - name: Upload artifact 58 | uses: actions/upload-pages-artifact@v3 59 | with: 60 | path: '.' 61 | - name: Deploy to GitHub Pages 62 | id: deployment 63 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /docs/Technical documentation/RailwayDeploy.md: -------------------------------------------------------------------------------- 1 | # Deploying the Application to Railway 2 | 3 | The application can be successfully deployed using the [Railway](https://railway.app) platform. To do so, you'll need to use the files from the `railway` folder and make a few configuration changes. 4 | 5 | > **Note**: Railway offers a free trial that allows deployments, but it is limited to 5$. 6 | 7 | ## Deployment Steps 8 | 9 | 1. **Sign Up and Connect GitHub Repository** 10 | Register on [Railway](https://railway.app) and connect your GitHub repository. 11 | 12 | 2. **Create Two Services** 13 | Set up two separate services — one for the frontend and one for the backend. 14 | 15 | 3. **Set Environment Variables** 16 | For each service, add the environment variable `RAILWAY_DOCKERFILE_PATH`, pointing to the respective `Dockerfile` location inside the `railway` folder. 17 | 18 | ??? example "Dockerfile" 19 | 20 | ```Dockerfile 21 | --8<-- "railway/backend/Dockerfile" 22 | ``` 23 | 24 | 4. **Configure Ports** 25 | After deployment, set the following ports: 26 | - Frontend: `3000` 27 | - Backend: `8000` 28 | 29 | 5. **Add the `railway.json` File** 30 | In the `Config as Code` section, add the `railway.json` file and update the service names by assigning the correct `RAILWAY_SERVICE_ID` variables. 31 | 32 | ??? example "railway.json" 33 | 34 | ```railway.json 35 | --8<-- "railway.json" 36 | ``` 37 | 38 | 6. **Update Dockerfiles** 39 | Once the services are deployed, update the Dockerfiles by replacing the environment variables for frontend and backend URLs with the ones provided by Railway. 40 | 41 | 7. **Enable Sleep Mode for Backend** 42 | To reduce data transfer usage, consider enabling **sleep mode** for the backend service when it's idle. 43 | 44 | -------------------------------------------------------------------------------- /frontend/src/components/MidiCreate/SettingsPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './SettingsPanel.css'; 3 | 4 | interface SettingsPanelProps { 5 | ticksPerBeat: number; 6 | setTicksPerBeat: (value: number) => void; 7 | bpm: number; 8 | setBpm: (value: number) => void; 9 | outputFilename: string; 10 | setOutputFilename: (value: string) => void; 11 | midiOctaveOffset: number; 12 | setMidiOctaveOffset: (value: number) => void; 13 | } 14 | 15 | const SettingsPanel: React.FC = ({ 16 | ticksPerBeat, 17 | setTicksPerBeat, 18 | bpm, 19 | setBpm, 20 | outputFilename, 21 | setOutputFilename, 22 | }) => { 23 | return ( 24 |
25 |

Settings

26 |
27 |
28 | 29 | setBpm(parseInt(e.target.value) || 120)} 33 | min="40" 34 | max="240" 35 | /> 36 |
37 |
38 | 39 | setTicksPerBeat(parseInt(e.target.value) || 480)} 43 | min="96" 44 | max="960" 45 | /> 46 |
47 |
48 | 49 | setOutputFilename(e.target.value)} 53 | placeholder="output.mid" 54 | /> 55 |
56 |
57 |
58 | ); 59 | }; 60 | 61 | export default SettingsPanel; -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.17.0", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@tonejs/midi": "^2.0.28", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.18.60", 12 | "@types/react": "^18.2.33", 13 | "@types/react-dom": "^18.2.14", 14 | "browserify-fs": "^1.0.0", 15 | "classnames": "^2.3.2", 16 | "path-browserify": "^1.0.1", 17 | "pixi.js": "^3.0.7", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-midi-player": "^1.0.8", 21 | "react-scripts": "5.0.1", 22 | "react-slider": "^2.0.6", 23 | "react-spinners": "^0.13.8", 24 | "react-tabs": "^6.0.0", 25 | "tone": "^15.0.4", 26 | "typescript": "^4.9.5", 27 | "web-vitals": "^2.1.4", 28 | "soundfont-player": "^0.12.0", 29 | "react-router-dom": "^7.6.0", 30 | "framer-motion": "12.12.1", 31 | "webmidi": "^3.1.12", 32 | "ts-node": "^10.9.2" 33 | }, 34 | "scripts": { 35 | "dev": "cross-env FAST_REFRESH=false react-scripts start", 36 | "build": "react-scripts build", 37 | "start": "npx serve -s build", 38 | "test": "react-scripts test", 39 | "eject": "react-scripts eject" 40 | }, 41 | "eslintConfig": { 42 | "extends": [ 43 | "react-app", 44 | "react-app/jest" 45 | ] 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | }, 59 | "devDependencies": { 60 | "@types/react-slider": "^1.3.4", 61 | "cross-env": "^7.0.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/src/components/TokenInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Token } from '../interfaces/ApiResponse'; 3 | import { TokenTypeToColor } from './TokenBlock'; 4 | import './TokenInfo.css'; 5 | 6 | interface DataDisplayProps { 7 | token: Token | null; 8 | heading: string; 9 | } 10 | 11 | const TokenInfo: React.FC = ({ token, heading }) => { 12 | return ( 13 |
19 | {token ? ( 20 |
21 |
22 | Type: {token.type} 23 |
24 |
25 | Value: {token.value} 26 |
27 | { token.time !== -1 &&
28 | Time: {token.time} 29 |
} 30 |
31 | Program: {token.program} 32 |
33 | {/* desc tends to just display value 34 | Since the default of desc is 0 we have to make sure it's not the appropriate value before hiding it */} 35 | { (String(token.desc) !== "0" || token.value === "0") &&
36 | Desc: {token.desc} 37 |
} 38 | { token.note_id !== null &&
39 | Note ID: {token.note_id} 40 |
} 41 | { token.track_id !== null &&
42 | Track: {isNaN(Number(token.track_id)) ? token.track_id : Number(token.track_id) + 1} 43 |
} 44 |
45 | ) : ( 46 |
47 | Hover a token... 48 |
49 | )} 50 |
51 | ); 52 | }; 53 | 54 | export default TokenInfo; 55 | -------------------------------------------------------------------------------- /frontend/src/components/MidiCreate/VirtualKeyboard.css: -------------------------------------------------------------------------------- 1 | /* VirtualKeyboard.css */ 2 | 3 | .virtual-keyboard { 4 | margin: 20px 0; 5 | padding: 15px; 6 | border: 1px solid #ddd; 7 | border-radius: 5px; 8 | background-color: white; 9 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 10 | width: 100%; 11 | } 12 | 13 | .virtual-keyboard h3 { 14 | margin-top: 0; 15 | margin-bottom: 15px; 16 | font-size: 1.2rem; 17 | color: #333; 18 | border-bottom: 1px solid #eee; 19 | padding-bottom: 8px; 20 | display: flex; 21 | justify-content: space-between; 22 | align-items: center; 23 | } 24 | 25 | .keyboard-container { 26 | display: flex; 27 | position: relative; 28 | height: 120px; 29 | border: 1px solid #ddd; 30 | border-radius: 0 0 5px 5px; 31 | overflow: hidden; 32 | } 33 | 34 | .white-key { 35 | flex: 1; 36 | height: 100%; 37 | background-color: white; 38 | border: 1px solid #999; 39 | border-radius: 0 0 4px 4px; 40 | cursor: pointer; 41 | user-select: none; 42 | position: relative; 43 | z-index: 1; 44 | transition: background-color 0.1s; 45 | } 46 | 47 | .white-key:hover { 48 | background-color: #f0f0f0; 49 | } 50 | 51 | .white-key.active { 52 | background-color: #88f; 53 | } 54 | 55 | .black-key { 56 | position: absolute; 57 | width: 60%; 58 | height: 60%; 59 | background-color: black; 60 | z-index: 2; 61 | cursor: pointer; 62 | user-select: none; 63 | border-radius: 0 0 2px 2px; 64 | transition: background-color 0.1s; 65 | } 66 | 67 | .black-key:hover { 68 | background-color: #333; 69 | } 70 | 71 | .black-key.active { 72 | background-color: #88f; 73 | } 74 | 75 | .key-label { 76 | position: absolute; 77 | bottom: 5px; 78 | width: 100%; 79 | text-align: center; 80 | font-size: 12px; 81 | color: #333; 82 | } 83 | 84 | .keyboard-help { 85 | margin-top: 5px; 86 | font-size: 12px; 87 | color: #666; 88 | text-align: center; 89 | } -------------------------------------------------------------------------------- /24Z_update.md: -------------------------------------------------------------------------------- 1 | ## Added functionalities: 2 | 3 | - Miditok library upgrade: from 2.1.7 to 3.0.4 4 | - Added PerTok tokenizator 5 | - New logics of corresponding notes with tokens (forced by library upgrade) 6 | - New parameters in configuration (mostly for PetTok): use_programs, use_microtiming, ticks_per_quarter, max_microtiming_shift, num_microtiming_bins 7 | - Default tokenizator classes, now associating notes to tokens is run after we receive tokens, unifed functions for all tokenizers depending on "use_programs" parameter usage 8 | 9 | ### frontend: 10 | 11 | - New design in general 12 | - Subtle animations 13 | - New piano roll- high resoultion, stable keybord so it does not vanish with scrolling 14 | - Tokens can be paginated and scaled 15 | - New audio player 16 | 17 | ![Screenshot of app](img/app_screenshot.png) 18 | 19 | ## Deployment 20 | 21 | You can see an example deployment on Railway [here](https://miditok-visualizer-production-frontend.up.railway.app/) 22 | As long as we're above our trial limit on Railway 23 | 24 | ## Running locally: 25 | 26 | You can use same commands as before to run the project locally. 27 | 28 | ### Frontend 29 | 30 | Basic run: 31 | 32 | ```sh 33 | cd frontend 34 | npm install 35 | npm run dev 36 | ``` 37 | 38 | Using Docker: 39 | 40 | ```sh 41 | cd frontend 42 | docker build . -t frontend 43 | docker run frontend -p 3000:3000 44 | ``` 45 | 46 | ### Backend 47 | 48 | Basic run: 49 | 50 | ```sh 51 | cd backend 52 | poetry shell 53 | poetry install 54 | python -m core.main 55 | ``` 56 | 57 | or 58 | 59 | ```sh 60 | poetry run python -m core.main 61 | ``` 62 | 63 | Using Docker: 64 | 65 | ```sh 66 | cd backend 67 | DOCKER_BUILDKIT=1 docker build --target=runtime . -t backend 68 | docker run backend -p 8000:8000 69 | ``` 70 | 71 | ### All at once: 72 | 73 | ```sh 74 | docker-compose up 75 | ``` 76 | 77 | ## Contributors 78 | 79 | - Karol Bogumił 80 | - Maksymilian Banach 81 | - Jakub Przesmycki -------------------------------------------------------------------------------- /frontend/src/components/MidiCreate/SoundSynthesizer.css: -------------------------------------------------------------------------------- 1 | /* SoundSynthesizer.css */ 2 | 3 | /* Since SoundSynthesizer doesn't render anything visible, 4 | this file is mostly a placeholder for potential UI controls 5 | that might be added in the future */ 6 | 7 | .synth-controls { 8 | margin: 15px 0; 9 | padding: 15px; 10 | border: 1px solid #ddd; 11 | border-radius: 5px; 12 | background-color: white; 13 | } 14 | 15 | .synth-controls h3 { 16 | margin-top: 0; 17 | margin-bottom: 15px; 18 | font-size: 1.2rem; 19 | color: #333; 20 | border-bottom: 1px solid #eee; 21 | padding-bottom: 8px; 22 | } 23 | 24 | .synth-controls select, 25 | .synth-controls input[type="range"] { 26 | padding: 8px; 27 | margin-right: 10px; 28 | border: 1px solid #ddd; 29 | border-radius: 4px; 30 | } 31 | 32 | .synth-controls select:focus, 33 | .synth-controls input[type="range"]:focus { 34 | border-color: #4285f4; 35 | outline: none; 36 | } 37 | 38 | .oscillator-type-selector { 39 | margin-bottom: 10px; 40 | } 41 | 42 | .volume-control { 43 | margin-bottom: 10px; 44 | display: flex; 45 | align-items: center; 46 | } 47 | 48 | .volume-control label { 49 | min-width: 80px; 50 | } 51 | 52 | .volume-slider { 53 | flex: 1; 54 | height: 6px; 55 | -webkit-appearance: none; 56 | appearance: none; 57 | background: linear-gradient(to right, #4285f4 0%, #4285f4 50%, #ddd 50%, #ddd 100%); 58 | border-radius: 3px; 59 | outline: none; 60 | } 61 | 62 | .volume-slider::-webkit-slider-thumb { 63 | -webkit-appearance: none; 64 | appearance: none; 65 | width: 18px; 66 | height: 18px; 67 | border-radius: 50%; 68 | background: #4285f4; 69 | cursor: pointer; 70 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); 71 | } 72 | 73 | .volume-slider::-moz-range-thumb { 74 | width: 18px; 75 | height: 18px; 76 | border: none; 77 | border-radius: 50%; 78 | background: #4285f4; 79 | cursor: pointer; 80 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); 81 | } -------------------------------------------------------------------------------- /backend/tests/test_tokenizer_factory.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from miditok import TokenizerConfig 3 | 4 | from core.service.tokenizer.tokenizer_factory import TokenizerFactory 5 | 6 | 7 | def test_get_tokenizer(): 8 | tokenizers_list = ["REMI", "MIDILike", "TSD", "Structured", "CPWord", "Octuple", "PerTok"] 9 | 10 | tokenizer_params = { 11 | "pitch_range": [21, 108], 12 | "nb_velocities": 32, 13 | "special_tokens": ["PAD", "EOS", "MASK"], 14 | "use_chords": True, 15 | "use_rests": True, 16 | "use_tempos": True, 17 | "use_time_signatures": True, 18 | "use_sustain_pedals": True, 19 | "use_pitch_bends": True, 20 | "nb_tempos": 32, 21 | "tempo_range": [30, 240], 22 | "log_tempos": False, 23 | "delete_equal_successive_tempo_changes": True, 24 | "sustain_pedal_duration": True, 25 | "pitch_bend_range": [-8192, 0, 8192], 26 | "delete_equal_successive_time_sig_changes": True, 27 | "programs": [0, 127], 28 | "one_token_stream_for_programs": True, 29 | "program_changes": False, 30 | "use_programs": False, 31 | "use_microtiming": False, 32 | "ticks_per_quarter": 480, 33 | "max_microtiming_shift": 0.04, 34 | "num_microtiming_bins": 16, 35 | "beat_res": {(0, 4): 8, (4, 12): 4}, 36 | } 37 | 38 | for tokenizer in tokenizers_list: 39 | try: 40 | tokenizer_factory = TokenizerFactory() 41 | config = TokenizerConfig(**tokenizer_params) 42 | tokenizer_factory.get_tokenizer(tokenizer, config) 43 | except Exception as e: 44 | assert False, f"'get_tokenizer' raised an exception {e}" 45 | 46 | 47 | def test_get_tokenizer_fail(): 48 | with pytest.raises(ValueError): 49 | tokenizer_factory = TokenizerFactory() 50 | config = TokenizerConfig() 51 | tokenizer_factory.get_tokenizer("SomeRandomString", config) 52 | -------------------------------------------------------------------------------- /docs/Design documentation/24Z/24Z_update.md: -------------------------------------------------------------------------------- 1 | ## Added functionalities: 2 | 3 | - Miditok library upgrade: from 2.1.7 to 3.0.4 4 | - Added PerTok tokenizator 5 | - New logics of corresponding notes with tokens (forced by library upgrade) 6 | - New parameters in configuration (mostly for PetTok): use_programs, use_microtiming, ticks_per_quarter, max_microtiming_shift, num_microtiming_bins 7 | - Default tokenizator classes, now associating notes to tokens is run after we receive tokens, unifed functions for all tokenizers depending on "use_programs" parameter usage 8 | 9 | ### frontend: 10 | 11 | - New design in general 12 | - Subtle animations 13 | - New piano roll- high resoultion, stable keybord so it does not vanish with scrolling 14 | - Tokens can be paginated and scaled 15 | - New audio player 16 | 17 | ![Screenshot of app](../../img/app_screenshot.png) 18 | 19 | ## Deployment 20 | 21 | You can see an example deployment on Railway [here](https://miditok-visualizer-production-frontend.up.railway.app/) 22 | As long as we're above our trial limit on Railway 23 | 24 | ## Running locally: 25 | 26 | You can use same commands as before to run the project locally. 27 | 28 | ### Frontend 29 | 30 | Basic run: 31 | 32 | ```sh 33 | cd frontend 34 | npm install 35 | npm run dev 36 | ``` 37 | 38 | Using Docker: 39 | 40 | ```sh 41 | cd frontend 42 | docker build . -t frontend 43 | docker run frontend -p 3000:3000 44 | ``` 45 | 46 | ### Backend 47 | 48 | Basic run: 49 | 50 | ```sh 51 | cd backend_old 52 | poetry shell 53 | poetry install 54 | python -m core.main 55 | ``` 56 | 57 | or 58 | 59 | ```sh 60 | poetry run python -m core.main 61 | ``` 62 | 63 | Using Docker: 64 | 65 | ```sh 66 | cd backend_old 67 | DOCKER_BUILDKIT=1 docker build --target=runtime . -t backend_old 68 | docker run backend_old -p 8000:8000 69 | ``` 70 | 71 | ### All at once: 72 | 73 | ```sh 74 | docker-compose up 75 | ``` 76 | 77 | ## Contributors 78 | 79 | - Karol Bogumił 80 | - Maksymilian Banach 81 | - Jakub Przesmycki -------------------------------------------------------------------------------- /backend/core/service/note/note_extractor.py: -------------------------------------------------------------------------------- 1 | from miditoolkit import MidiFile as MidiToolkitFile 2 | 3 | from typing import List 4 | from core.api.model import Note 5 | class NoteExtractor: 6 | """ 7 | Utility class for extracting note information from MIDI files. 8 | 9 | This class provides functionality to convert MIDI files into structured Note objects 10 | that contain detailed information about each note in the file, including pitch, note name, 11 | start time, end time, and velocity. 12 | """ 13 | def __init__(self): 14 | pass 15 | def midi_to_notes(self, midi: MidiToolkitFile) -> List[List[Note]]: 16 | """ 17 | Converts a MIDI file to a list of notes by track 18 | 19 | Args: 20 | midi: A MidiToolkitFile object 21 | 22 | Returns: 23 | A list of lists of Note objects, organized by track 24 | """ 25 | notes = [] 26 | for instrument in midi.instruments: 27 | track_notes = [] 28 | for note in instrument.notes: 29 | note_name = self._pitch_to_name(note.pitch) 30 | track_notes.append( 31 | Note( 32 | note.pitch, 33 | note_name, 34 | note.start, 35 | note.end, 36 | note.velocity 37 | ) 38 | ) 39 | notes.append(track_notes) 40 | return notes 41 | 42 | @staticmethod 43 | def _pitch_to_name(pitch: int) -> str: 44 | """ 45 | Converts a MIDI pitch to a note name 46 | 47 | Args: 48 | pitch: MIDI pitch value 49 | 50 | Returns: 51 | String representation of the note (e.g., "C4") 52 | """ 53 | note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] 54 | octave = pitch // 12 - 1 55 | note = note_names[pitch % 12] 56 | return f"{note}{octave}" -------------------------------------------------------------------------------- /frontend/src/components/FileUpload.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | 3 | interface FileUploadProps { 4 | onFileSelect: (file: File) => void; 5 | acceptedFormats: string; 6 | disabled?: boolean; 7 | } 8 | 9 | const FileUpload: React.FC = ({ onFileSelect, acceptedFormats,disabled = false }) => { 10 | const [fileName, setFileName] = useState(''); 11 | const [isUploading, setIsUploading] = useState(false); 12 | const [showSuccess, setShowSuccess] = useState(false); 13 | const fileInputRef = useRef(null); 14 | 15 | const handleFileChange = (event: React.ChangeEvent) => { 16 | const files = event.target.files; 17 | if (files && files.length > 0) { 18 | const file = files[0]; 19 | setFileName(file.name); 20 | onFileSelect(file); 21 | 22 | setShowSuccess(true); 23 | setTimeout(() => { 24 | setShowSuccess(false); 25 | }, 1500); 26 | 27 | if (fileInputRef.current) { 28 | fileInputRef.current.value = ''; 29 | } 30 | } 31 | }; 32 | 33 | const handleClick = () => { 34 | if (fileInputRef.current) { 35 | fileInputRef.current.click(); 36 | } 37 | }; 38 | 39 | const getFileIcon = () => { 40 | if (isUploading) { 41 | return
; 42 | } else if (showSuccess) { 43 | return ; 44 | } else { 45 | return 📁; 46 | } 47 | }; 48 | 49 | return ( 50 |
51 |
52 | 59 |
63 | {getFileIcon()} 64 | {fileName ? fileName : 'Choose a file...'} 65 |
66 |
67 |
68 | ); 69 | }; 70 | 71 | export default FileUpload; -------------------------------------------------------------------------------- /frontend/src/components/FilePlayback.css: -------------------------------------------------------------------------------- 1 | .custom-midi-player { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | gap: 10px; 6 | background-color: transparent; 7 | padding: 10px; 8 | margin: 20px auto; 9 | width: fit-content; 10 | } 11 | 12 | .custom-midi-player .progress-bar { 13 | width: 200px; 14 | height: 8px; 15 | background: #f0f0f0; 16 | border-radius: 5px; 17 | outline: none; 18 | border: 2px solid #ccc; 19 | -webkit-appearance: none; 20 | appearance: none; 21 | } 22 | 23 | 24 | .custom-midi-player .progress-bar::-webkit-slider-thumb { 25 | -webkit-appearance: none; 26 | appearance: none; 27 | width: 16px; 28 | height: 16px; 29 | background-color: #525c6a !important; 30 | border-radius: 50%; 31 | cursor: pointer; 32 | transition: transform 0.2s ease; 33 | box-shadow: 0 0 0 2px black; 34 | } 35 | 36 | 37 | .custom-midi-player .progress-bar::-webkit-slider-thumb:hover { 38 | transform: scale(1.3); 39 | } 40 | 41 | 42 | .custom-midi-player .progress-bar::-moz-range-thumb { 43 | width: 16px; 44 | height: 16px; 45 | background-color: #525c6a !important; 46 | border-radius: 50%; 47 | cursor: pointer; 48 | transition: transform 0.2s ease; 49 | box-shadow: 0 0 0 2px black; 50 | } 51 | 52 | .custom-midi-player .progress-bar::-moz-range-thumb:hover { 53 | transform: scale(1.3); 54 | } 55 | 56 | .custom-midi-player .progress-bar::-ms-thumb { 57 | width: 16px; 58 | height: 16px; 59 | background-color: #525c6a !important; 60 | border-radius: 50%; 61 | cursor: pointer; 62 | transition: transform 0.2s ease; 63 | box-shadow: 0 0 0 2px black; 64 | } 65 | 66 | .custom-midi-player .progress-bar::-ms-thumb:hover { 67 | transform: scale(1.3); 68 | } 69 | 70 | .custom-midi-player button { 71 | background-color: white; 72 | color: black; 73 | border: 2px solid black; 74 | padding: 8px 12px; 75 | border-radius: 5px; 76 | font-size: 14px; 77 | font-weight: bold; 78 | cursor: pointer; 79 | transition: background-color 0.2s, color 0.2s, border-color 0.2s; 80 | } 81 | 82 | .custom-midi-player button:hover { 83 | background-color: #525c6a; 84 | color: #fff; 85 | } 86 | 87 | .custom-midi-player button:disabled { 88 | background-color: #e0e0e0; 89 | color: #a0a0a0; 90 | border: 1px solid #ccc; 91 | } 92 | -------------------------------------------------------------------------------- /backend/core/service/midi/midi_loader.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from miditoolkit import MidiFile as MidiToolkitFile 3 | from mido import MidiFile as MidoMidiFile 4 | 5 | 6 | class MidiLoader: 7 | """ 8 | Utility class for loading MIDI files using different MIDI processing libraries. 9 | 10 | This class provides methods to load MIDI data using both miditoolkit and mido libraries, 11 | which offer different capabilities for MIDI file manipulation and analysis. The class 12 | handles the conversion of raw bytes to MIDI objects that can be processed by these libraries. 13 | """ 14 | def __init__(self): 15 | pass 16 | 17 | def load_midi_toolkit(self, midi_bytes: bytes) -> MidiToolkitFile: 18 | """ 19 | Load a MIDI file using the miditoolkit library. 20 | 21 | This method converts raw bytes into a MidiToolkitFile object, which provides 22 | comprehensive capabilities for MIDI manipulation, including note extraction, 23 | tempo and time signature analysis, and more. 24 | 25 | Args: 26 | midi_bytes: Raw MIDI file data as bytes. 27 | 28 | Returns: 29 | A MidiToolkitFile object representing the loaded MIDI file. 30 | 31 | Raises: 32 | ValueError: If the MIDI file cannot be loaded using miditoolkit, 33 | with details about the specific error. 34 | """ 35 | try: 36 | return MidiToolkitFile(file=BytesIO(midi_bytes)) 37 | except Exception as e: 38 | raise ValueError(f"Failed to load MIDI file with MidiToolkit: {str(e)}") 39 | 40 | def load_mido_midi(self, midi_bytes: bytes) -> MidoMidiFile: 41 | """ 42 | Load a MIDI file using the mido library. 43 | 44 | This method converts raw bytes into a MidoMidiFile object, which provides 45 | low-level access to MIDI messages and is useful for MIDI message manipulation 46 | and analysis at the event level. 47 | 48 | Args: 49 | midi_bytes: Raw MIDI file data as bytes. 50 | 51 | Returns: 52 | A MidoMidiFile object representing the loaded MIDI file. 53 | 54 | Raises: 55 | ValueError: If the MIDI file cannot be loaded using mido, 56 | with details about the specific error. 57 | """ 58 | try: 59 | return MidoMidiFile(file=BytesIO(midi_bytes)) 60 | except Exception as e: 61 | raise ValueError(f"Failed to load MIDI file with Mido: {str(e)}") 62 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/MidiCreate/MIDICanvas.css: -------------------------------------------------------------------------------- 1 | /* MIDICanvas.css */ 2 | 3 | #canvasContainer { 4 | width: 100%; 5 | height: 600px; 6 | overflow: auto; 7 | position: relative; 8 | user-select: none; 9 | touch-action: none; 10 | border: 1px solid #ddd; 11 | border-radius: 8px; 12 | background-color: #f8f8f8; 13 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 14 | transition: all 0.3s ease; 15 | background-image: url(/public/bckg.png); 16 | } 17 | 18 | #canvasContainer:hover { 19 | border-color: #4285f4; 20 | box-shadow: 0 4px 12px rgba(66, 133, 244, 0.2); 21 | } 22 | 23 | #can { 24 | position: absolute; 25 | top: 0; 26 | left: 0; 27 | } 28 | 29 | .note-labels { 30 | position: absolute; 31 | top: 0; 32 | left: 0; 33 | z-index: 10; 34 | background-color: rgba(255, 255, 255, 0.8); 35 | padding: 5px; 36 | border-right: 1px solid #ddd; 37 | width: 40px; 38 | height: 100%; 39 | pointer-events: none; 40 | } 41 | 42 | .note-label { 43 | position: absolute; 44 | font-size: 10px; 45 | color: #444; 46 | font-weight: bold; 47 | } 48 | 49 | /* Recording indicator */ 50 | .recording-indicator { 51 | position: absolute; 52 | top: 10px; 53 | left: 10px; 54 | padding: 5px 10px; 55 | background-color: red; 56 | color: white; 57 | font-weight: bold; 58 | border-radius: 4px; 59 | z-index: 100; 60 | animation: pulse 1s infinite; 61 | } 62 | 63 | @keyframes pulse { 64 | 0% { 65 | opacity: 1; 66 | } 67 | 50% { 68 | opacity: 0.5; 69 | } 70 | 100% { 71 | opacity: 1; 72 | } 73 | } 74 | 75 | /* Playback position indicator */ 76 | .playback-position { 77 | position: absolute; 78 | top: 0; 79 | height: 100%; 80 | width: 2px; 81 | background-color: red; 82 | z-index: 10; 83 | pointer-events: none; 84 | } 85 | 86 | .midi-note { 87 | position: absolute; 88 | background-color: rgba(0, 255, 0, 0.7); 89 | border: 1px solid black; 90 | border-radius: 3px; 91 | transition: opacity 0.3s; 92 | } 93 | 94 | .midi-note:hover { 95 | opacity: 0.8; 96 | } 97 | 98 | .midi-note.erasing { 99 | background-color: rgba(255, 0, 0, 0.3); 100 | } 101 | 102 | /* Animation for canvas loading */ 103 | @keyframes fadeIn { 104 | from { 105 | opacity: 0; 106 | } 107 | to { 108 | opacity: 1; 109 | } 110 | } 111 | 112 | .fade-in { 113 | animation: fadeIn 0.5s ease-in; 114 | } 115 | 116 | /* Responsive adjustments */ 117 | @media (max-width: 768px) { 118 | #canvasContainer { 119 | width: 100%; 120 | max-width: 600px; 121 | height: 400px; 122 | } 123 | } -------------------------------------------------------------------------------- /frontend/src/components/MidiCreate/MIDIDeviceManager.css: -------------------------------------------------------------------------------- 1 | /* MIDIDeviceManager.css */ 2 | 3 | .midi-device-manager { 4 | margin-bottom: 20px; 5 | padding: 10px; 6 | border: 1px solid #ddd; 7 | border-radius: 5px; 8 | background-color: white; 9 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 10 | } 11 | 12 | .midi-device-manager h3 { 13 | margin-top: 0; 14 | margin-bottom: 15px; 15 | font-size: 1.2rem; 16 | color: #333; 17 | border-bottom: 1px solid #eee; 18 | padding-bottom: 8px; 19 | } 20 | 21 | .midi-device-manager h4 { 22 | margin: 0 0 10px 0; 23 | font-size: 1rem; 24 | color: #555; 25 | } 26 | 27 | .device-container { 28 | display: flex; 29 | gap: 20px; 30 | } 31 | 32 | .device-section { 33 | flex: 1; 34 | min-width: 250px; 35 | } 36 | 37 | .device-select { 38 | width: 100%; 39 | padding: 5px; 40 | margin-bottom: 10px; 41 | border: 1px solid #ddd; 42 | border-radius: 4px; 43 | background-color: white; 44 | transition: border-color 0.3s; 45 | } 46 | 47 | .device-select:focus { 48 | border-color: #4285f4; 49 | outline: none; 50 | box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2); 51 | } 52 | 53 | .record-button { 54 | padding: 8px 15px; 55 | border: none; 56 | border-radius: 4px; 57 | cursor: pointer; 58 | font-weight: bold; 59 | transition: background-color 0.3s, transform 0.2s; 60 | min-width: 180px; 61 | } 62 | 63 | .record-button.recording { 64 | background-color: red; 65 | color: white; 66 | animation: pulse 1s infinite; 67 | } 68 | 69 | .record-button.not-recording { 70 | background-color: darkred; 71 | color: white; 72 | } 73 | 74 | .record-button.disabled { 75 | opacity: 0.7; 76 | cursor: not-allowed; 77 | } 78 | 79 | .record-button:hover:not(.disabled) { 80 | transform: translateY(-2px); 81 | } 82 | 83 | .play-device-button { 84 | padding: 8px 15px; 85 | border: none; 86 | border-radius: 4px; 87 | background-color: #007bff; 88 | color: white; 89 | cursor: pointer; 90 | transition: background-color 0.3s, transform 0.2s; 91 | } 92 | 93 | .play-device-button:hover:not(:disabled) { 94 | background-color: #0056b3; 95 | transform: translateY(-2px); 96 | } 97 | 98 | .play-device-button:disabled { 99 | background-color: #cccccc; 100 | cursor: not-allowed; 101 | } 102 | 103 | @keyframes pulse { 104 | 0% { 105 | opacity: 1; 106 | } 107 | 50% { 108 | opacity: 0.5; 109 | } 110 | 100% { 111 | opacity: 1; 112 | } 113 | } 114 | 115 | /* Responsive adjustments */ 116 | @media (max-width: 768px) { 117 | .device-container { 118 | flex-direction: column; 119 | gap: 10px; 120 | } 121 | 122 | .device-section { 123 | min-width: 100%; 124 | } 125 | } -------------------------------------------------------------------------------- /frontend/src/components/MidiCreate/MIDISequencer.css: -------------------------------------------------------------------------------- 1 | /* MIDISequencer.css */ 2 | 3 | .midi-sequencer { 4 | font-family: Arial, sans-serif; 5 | max-width: 100%; 6 | width: 100%; 7 | margin: 0 auto; 8 | padding: 15px; 9 | border-radius: 8px; 10 | background-color: #f5f5f5; 11 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 12 | box-sizing: border-box; 13 | } 14 | 15 | .midi-sequencer-container { 16 | width: 100%; 17 | max-width: 1200px; 18 | margin: 0 auto; 19 | padding: 20px; 20 | background-color: #f9f9f9; 21 | border-radius: 8px; 22 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 23 | } 24 | 25 | .midi-sequencer-title { 26 | margin-top: 0; 27 | margin-bottom: 20px; 28 | font-size: 24px; 29 | color: #333; 30 | text-align: center; 31 | } 32 | 33 | /* Status message styling */ 34 | .status-message { 35 | padding: 10px; 36 | margin: 10px 0; 37 | border-radius: 4px; 38 | text-align: center; 39 | } 40 | 41 | .success-message { 42 | background-color: #d4edda; 43 | color: #155724; 44 | border: 1px solid #c3e6cb; 45 | } 46 | 47 | .error-message { 48 | background-color: #f8d7da; 49 | color: #721c24; 50 | border: 1px solid #f5c6cb; 51 | } 52 | 53 | .info-message { 54 | background-color: #d1ecf1; 55 | color: #0c5460; 56 | border: 1px solid #bee5eb; 57 | } 58 | 59 | /* Animation for loading */ 60 | @keyframes fadeIn { 61 | from { 62 | opacity: 0; 63 | } 64 | to { 65 | opacity: 1; 66 | } 67 | } 68 | 69 | .fade-in { 70 | animation: fadeIn 0.5s ease-in; 71 | } 72 | 73 | /* Animation for components with framer-motion */ 74 | @keyframes slideIn { 75 | from { 76 | transform: translateX(-20px); 77 | opacity: 0; 78 | } 79 | to { 80 | transform: translateX(0); 81 | opacity: 1; 82 | } 83 | } 84 | 85 | .slide-in { 86 | animation: slideIn 0.5s ease-out; 87 | } 88 | 89 | /* Responsive adjustments */ 90 | @media (min-width: 768px) { 91 | .midi-sequencer { 92 | padding: 20px; 93 | } 94 | } 95 | 96 | 97 | .track-button, .add-track-button{ 98 | padding: 10px 15px; 99 | border: none; 100 | border-radius: 4px; 101 | background-color: white; 102 | color: black; 103 | cursor: pointer; 104 | font-size: 1rem; 105 | transition: background-color 0.3s, transform 0.2s; 106 | min-width: 80px; 107 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); 108 | } 109 | 110 | .add-track-button:hover{ 111 | background-color: #4CAF50; 112 | color:white; 113 | } 114 | 115 | .delete-track-button{ 116 | border: none; 117 | border-radius: 4px; 118 | color: white; 119 | padding-left: 5px; 120 | padding-right: 5px; 121 | margin-left: 5px; 122 | background-color: rgba(255, 0, 0); 123 | font-size: 1rem; 124 | } -------------------------------------------------------------------------------- /backend/core/service/midi_processing.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Any, List, Optional, Tuple 3 | 4 | import muspy 5 | 6 | from miditok import TokenizerConfig 7 | 8 | 9 | from core.api.model import BasicInfoData, ConfigModel, MetricsData, MusicInformationData, Note 10 | from core.service.tokenizer.tokenizer_factory import TokenizerFactory 11 | from core.service.midi.midi_loader import MidiLoader 12 | from core.service.note.note_extractor import NoteExtractor 13 | from core.service.tokenizer.tokenizer_config import TokenizerConfigUser 14 | from core.service.note.note_id import NoteIDHandler 15 | from core.service.midi.midi_info_extractor import MidiInformationExtractor 16 | class MidiProcessor: 17 | def __init__(self,user_config: ConfigModel= None): 18 | """ 19 | Initializes the MidiProcessor with required components. 20 | 21 | """ 22 | self.user_config = user_config 23 | self.loader = MidiLoader() 24 | self.tokenizer_factory = TokenizerFactory() 25 | self.note_extractor = NoteExtractor() 26 | self.note_id_handler = NoteIDHandler() 27 | self.midi_info_extractor = MidiInformationExtractor() 28 | 29 | 30 | def tokenize_midi_file(self, user_config: ConfigModel, midi_bytes: bytes) -> tuple[Any, list[list[Note]]]: 31 | """ 32 | Tokenizes a MIDI file based on user configuration. 33 | 34 | Args: 35 | user_config: Configuration model with tokenization parameters 36 | midi_bytes: Raw bytes of the MIDI file 37 | 38 | Returns: 39 | Tuple containing tokenized MIDI and extracted notes 40 | """ 41 | 42 | tokenizer_config_user = TokenizerConfigUser(user_config) 43 | tokenizer_config = TokenizerConfig(**tokenizer_config_user.get_params()) 44 | 45 | tokenizer = self.tokenizer_factory.get_tokenizer(user_config.tokenizer, tokenizer_config) 46 | 47 | midi = self.loader.load_midi_toolkit(midi_bytes) 48 | 49 | tokens = tokenizer(midi) 50 | 51 | notes = self.note_extractor.midi_to_notes(midi) 52 | tokens = self.note_id_handler.add_notes_id(tokens, notes, user_config.tokenizer,user_config.use_programs,user_config.base_tokenizer) 53 | 54 | return tokens, notes 55 | 56 | def retrieve_information_from_midi(self, midi_bytes: bytes) -> MusicInformationData: 57 | """ 58 | Extracts musical information from a MIDI file. 59 | 60 | Args: 61 | midi_bytes: Raw bytes of the MIDI file 62 | 63 | Returns: 64 | MusicInformationData containing extracted information 65 | 66 | Raises: 67 | ValueError: If music information data could not be processed 68 | """ 69 | 70 | midi = self.loader.load_mido_midi(midi_bytes) 71 | midi_file_music = muspy.from_mido(midi) 72 | 73 | return self.midi_info_extractor.extract_information(midi_file_music) 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /frontend/src/components/MidiCreate/SoundSynthesizer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import './SoundSynthesizer.css'; 3 | 4 | class Synthesizer { 5 | private audioContext: AudioContext; 6 | private oscillators: Map; 7 | private gainNodes: Map; 8 | 9 | constructor() { 10 | this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); 11 | this.oscillators = new Map(); 12 | this.gainNodes = new Map(); 13 | } 14 | 15 | playNote(pitch: number, velocity: number = 90): void { 16 | if (this.oscillators.has(pitch)) { 17 | this.stopNote(pitch, 0); 18 | } 19 | const frequency = 440 * Math.pow(2, (pitch - 69) / 12); 20 | const oscillator = this.audioContext.createOscillator(); 21 | oscillator.type = 'sine'; 22 | oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime); 23 | const gainNode = this.audioContext.createGain(); 24 | const normalizedVelocity = velocity / 127; 25 | gainNode.gain.setValueAtTime(normalizedVelocity * 0.3, this.audioContext.currentTime); 26 | oscillator.connect(gainNode); 27 | gainNode.connect(this.audioContext.destination); 28 | oscillator.start(this.audioContext.currentTime); 29 | this.oscillators.set(pitch, oscillator); 30 | this.gainNodes.set(pitch, gainNode); 31 | } 32 | 33 | stopNote(pitch: number, releaseTime: number = 0.01): void { 34 | const oscillator = this.oscillators.get(pitch); 35 | const gainNode = this.gainNodes.get(pitch); 36 | 37 | if (oscillator && gainNode) { 38 | this.oscillators.delete(pitch); 39 | this.gainNodes.delete(pitch); 40 | 41 | gainNode.gain.cancelScheduledValues(this.audioContext.currentTime); 42 | gainNode.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + releaseTime); 43 | 44 | setTimeout(() => { 45 | try { 46 | if (oscillator.context.state !== 'closed' && oscillator.numberOfOutputs > 0) { 47 | oscillator.stop(this.audioContext.currentTime); 48 | oscillator.disconnect(); 49 | gainNode.disconnect(); 50 | } 51 | } catch (e) { 52 | console.warn("Error stopping oscillator or disconnecting nodes:", e); 53 | } 54 | }, releaseTime * 1000 + 50); 55 | } 56 | } 57 | 58 | } 59 | 60 | interface SoundSynthesizerProps { 61 | onSynthesizerReady: (synth: Synthesizer) => void; 62 | } 63 | 64 | const SoundSynthesizer: React.FC = ({ onSynthesizerReady }) => { 65 | const synthRef = useRef(null); 66 | 67 | useEffect(() => { 68 | if (!synthRef.current) { 69 | synthRef.current = new Synthesizer(); 70 | onSynthesizerReady(synthRef.current); 71 | } 72 | return () => { 73 | }; 74 | }, [onSynthesizerReady]); 75 | return null; 76 | }; 77 | 78 | export default SoundSynthesizer; 79 | export { Synthesizer }; 80 | -------------------------------------------------------------------------------- /docs/Design documentation/25L/Semester_summary.md: -------------------------------------------------------------------------------- 1 | # Podsumowanie projektu 2 | 3 | W ramach projektu rozwinęliśmy narzędzie MidiTok Visualizer. 4 | Repozytorium: https://github.com/DukiDuki2000/MidiTok-Visualizer/tree/pr-5 5 | Film przedstawiający zmiany: https://youtu.be/3Ks9YF1G9r0 6 | 7 | 8 | 9 | ## Główne zmiany 10 | (Ciąg wszystkich zmian zawarty jest w Weekly_update.md) 11 | * Dodano CI/CD dla backendu, frontendu oraz dla dokumentacji na GitHub Pages 12 | * Stworzono automatyczną dokumentację backendu oraz modelu i API, jak również opisano proces deploy na Railway oraz uruchomienia aplikacji 13 | * Przeorganizowano backend w klasy i odpowiadające jej metody. Dużo z klas zostało napisanych od początku - note_id, tokenizer_factory, midi_event 14 | * Klasa MidiProcessing jest teraz klasą główną tworzącą obiekty 15 | * Poprawiono błędy związane z plikami MIDI, dla których pierwszy token nie jest Pitch 16 | * Przeorganizowano stronę do stylu formularza 17 | * Dodano bardzo dużą ilość stylów dla komponentów we frontendzie 18 | * Dodano przycisk demo, ładujący domyślnie wpisany w klasę plik midi-demofile.tsx 19 | * Upload pliku MIDI dodaje go do aplikacji (pamięć tymczasowa), umożliwiając dodanie kilku plików i intuicyjny wybór do tokenizacji 20 | * Plik MIDI można tokenizować kilka razy - nie trzeba za każdym razem odświeżać strony (tabs) 21 | * Przeskalowano całą aplikację do działania na różnych urządzeniach w zależności od wielkości ekranu 22 | * Zintegrowano odtwarzacz z piano rollem 23 | * Odtwarzacz otrzymał informację o długości pliku, możliwość pause, jak również kontrolę głośności 24 | * Odtwarzacz może teraz "grać" kilka zdefiniowanych instrumentów 25 | * Dodano slider do piano rolla 26 | * Dodano podświetlanie nut oraz ścieżek w piano rollu 27 | * Dodano przycisk włączenia śledzenia slidera 28 | * Dodano tokenizer MMM oraz MuMIDI 29 | * Dodano animacje ładowania komponentów 30 | * Dodano nową zakładkę tworzenia plików MIDI: 31 | * Dodano ustawienie BPM, tick per beat oraz nazwy pliku wyjściowego 32 | * Dodawanie nut myszką, usuwanie Ctrl 33 | * Opcja nagrywania poprzez virtual keyboard (klawiatura A-L lub ekran) lub urządzenia produkujące komunikaty MIDI 34 | * Opcja odtworzenia na urządzeniu MIDI 35 | * Możliwość dodawania do 5 ścieżek 36 | * Możliwość usuwania nut z canvy 37 | * Stworzone dzieła można pobierać, jak również od razu wysłać do tokenizacji 38 | 39 | 40 | ## Dalszy rozwój projektu 41 | 42 | * **Przeniesienie piano rolla** - aby wygodniej korzystać z piano rolla można go przeorganizować, dostosowując aplikację np. do działania jak w trybie podzielonego ekranu, gdzie dolna część ekranu to piano roll a górna to tokeny. Inna opcja to dwie strony współpracujące ze sobą 43 | * **Dodanie edycji nut na piano rollu** - po załadowaniu pliku możliwość usuwania, skracania nut i przy tym bieżące pokazywanie zmian w tokenach 44 | * **MidiTok jest już w prawie w pełni wykorzystany (stan na 05.25)** - więc może teraz użyć MusicLang tokenizer 45 | * **Implementacja z Miditok tokenizera REMIPlus** 46 | * **Dodanie zmiany instrumentów do zakłądki "tworzenia MIDI"** 47 | * **Generowanie wyników tokeniazcji do pliku** 48 | * **Zakładka porównawcza wszystkich metod tokenizacji**- wybieramy plik, następnie generuje się wyniki ze wszystkich tokenizacji na jednej stronie (problem- dopasowanie konfiguracji) -------------------------------------------------------------------------------- /backend/core/service/tokenizer/tokenizer_factory.py: -------------------------------------------------------------------------------- 1 | from miditok import MusicTokenizer, TokenizerConfig 2 | 3 | from core.service.tokenizer.tokenizers.cpword_tokenizer import CPWordTokenizer 4 | from core.service.tokenizer.tokenizers.midilike_tokenizer import MIDILikeTokenizer 5 | from core.service.tokenizer.tokenizers.MMM_tokenizer import MMMTokenizer 6 | from core.service.tokenizer.tokenizers.muMIDI_tokenizer import MuMIDITokenizer 7 | from core.service.tokenizer.tokenizers.octuple_tokenizer import OctupleTokenizer 8 | from core.service.tokenizer.tokenizers.perTok_tokenizer import PerTokTokenizer 9 | from core.service.tokenizer.tokenizers.remi_tokenizer import REMITokenizer 10 | from core.service.tokenizer.tokenizers.structured_tokenizer import StructuredTokenizer 11 | from core.service.tokenizer.tokenizers.tsd_tokenizer import TSDTokenizer 12 | 13 | 14 | class TokenizerFactory: 15 | """ 16 | Factory class for creating and mapping for MIDI tokenizers. 17 | """ 18 | _registry = {} 19 | 20 | @classmethod 21 | def register_tokenizer(cls, tokenizer_type: str, tokenizer_cls): 22 | """ 23 | Register a new tokenizer type with its implementation class. 24 | 25 | This method adds a new tokenizer type to the factory's registry, allowing it 26 | to be created later using the get_tokenizer method. 27 | 28 | Args: 29 | tokenizer_type: A string identifier for the tokenizer type (e.g., "REMI", "MIDILike"). 30 | tokenizer_cls: The tokenizer class implementation to associate with this type. 31 | The class should be a subclass of MusicTokenizer. 32 | """ 33 | cls._registry[tokenizer_type] = tokenizer_cls 34 | 35 | @classmethod 36 | def get_tokenizer(cls, tokenizer_type: str, config: TokenizerConfig) -> MusicTokenizer: 37 | """ 38 | Create and return a tokenizer instance of the specified type. 39 | 40 | This method looks up the requested tokenizer type in the registry and creates 41 | a new instance with the provided configuration. 42 | 43 | Args: 44 | tokenizer_type: The type of tokenizer to create (e.g., "REMI", "MIDILike"). 45 | Must be a type that has been registered with register_tokenizer. 46 | config: A TokenizerConfig object containing the configuration parameters 47 | for the tokenizer. 48 | 49 | Returns: 50 | A new instance of the requested tokenizer type, initialized with the provided 51 | configuration. 52 | 53 | Raises: 54 | ValueError: If the requested tokenizer type is not registered. 55 | """ 56 | if tokenizer_type not in cls._registry: 57 | raise ValueError(tokenizer_type) 58 | return cls._registry[tokenizer_type](config) 59 | 60 | 61 | TokenizerFactory.register_tokenizer("REMI", REMITokenizer) 62 | TokenizerFactory.register_tokenizer("MIDILike", MIDILikeTokenizer) 63 | TokenizerFactory.register_tokenizer("TSD", TSDTokenizer) 64 | TokenizerFactory.register_tokenizer("Structured", StructuredTokenizer) 65 | TokenizerFactory.register_tokenizer("CPWord", CPWordTokenizer) 66 | TokenizerFactory.register_tokenizer("Octuple", OctupleTokenizer) 67 | TokenizerFactory.register_tokenizer("MuMIDI", MuMIDITokenizer) 68 | TokenizerFactory.register_tokenizer("MMM", MMMTokenizer) 69 | TokenizerFactory.register_tokenizer("PerTok", PerTokTokenizer) 70 | -------------------------------------------------------------------------------- /frontend/src/components/MidiCreate/MIDIDeviceManager.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './MIDIDeviceManager.css'; 3 | 4 | interface MIDIDeviceManagerProps { 5 | midiInputDevices: WebMidi.MIDIInput[]; 6 | midiOutputDevices: WebMidi.MIDIOutput[]; 7 | selectedInputIndex: number; 8 | selectedOutputIndex: number; 9 | setSelectedInputIndex: (index: number) => void; 10 | setSelectedOutputIndex: (index: number) => void; 11 | isRecording: boolean; 12 | startRecording: () => void; 13 | stopRecording: () => void; 14 | handlePlayOnDevice: () => void; 15 | } 16 | 17 | const MIDIDeviceManager: React.FC = ({ 18 | midiInputDevices, 19 | midiOutputDevices, 20 | selectedInputIndex, 21 | selectedOutputIndex, 22 | setSelectedInputIndex, 23 | setSelectedOutputIndex, 24 | isRecording, 25 | startRecording, 26 | stopRecording, 27 | handlePlayOnDevice 28 | }) => { 29 | return ( 30 |
31 |

MIDI Devices

32 |
33 | {/* Input devices */} 34 |
35 |

Input Devices {midiInputDevices.length === 0 && (None available - using virtual keyboard)}

36 | 48 |
49 | {isRecording ? ( 50 | 56 | ) : ( 57 | 63 | )} 64 |
65 |
66 | 67 | {/* Output devices */} 68 |
69 |

Output Devices

70 | 82 |
83 | 90 |
91 |
92 |
93 |
94 | ); 95 | }; 96 | 97 | export default MIDIDeviceManager; -------------------------------------------------------------------------------- /frontend/src/components/DemoFile.css: -------------------------------------------------------------------------------- 1 | /* DemoFile.css */ 2 | 3 | .demo-file-container { 4 | margin-top: 15px; 5 | width: 100%; 6 | } 7 | 8 | .demo-file-button { 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | gap: 8px; 13 | padding: 10px 15px; 14 | background-color: #f5f5f5; 15 | border: 1px solid #ddd; 16 | border-radius: 4px; 17 | width: 100%; 18 | font-size: 14px; 19 | color: #555; 20 | transition: all 0.2s ease; 21 | cursor: pointer; 22 | } 23 | 24 | @media (min-width: 768px) { 25 | .demo-file-button { 26 | font-size: 16px; 27 | } 28 | } 29 | 30 | .demo-file-button:hover { 31 | background-color: #e0e0e0; 32 | transform: translateY(-2px); 33 | } 34 | 35 | .demo-file-button.demo-active { 36 | background-color: #e3f2fd; 37 | border-left: 4px solid #2196f3; 38 | animation: pulse-select 1s ease-out; 39 | } 40 | 41 | .demo-icon { 42 | font-size: 18px; 43 | } 44 | 45 | .spinner-container { 46 | display: flex; 47 | justify-content: center; 48 | align-items: center; 49 | gap: 8px; 50 | } 51 | 52 | .loading-spinner { 53 | width: 20px; 54 | height: 20px; 55 | border: 3px solid rgba(255, 255, 255, 0.3); 56 | border-radius: 50%; 57 | border-top-color: #fff; 58 | animation: spinner 0.8s linear infinite; 59 | display: inline-block; 60 | } 61 | 62 | @keyframes spinner { 63 | 0% { 64 | transform: rotate(0deg); 65 | } 66 | 100% { 67 | transform: rotate(360deg); 68 | } 69 | } 70 | 71 | .demo-file-error { 72 | color: #f44336; 73 | margin-top: 10px; 74 | font-size: 14px; 75 | text-align: left; 76 | padding: 8px; 77 | background-color: #ffebee; 78 | border-radius: 4px; 79 | } 80 | 81 | .demo-mode-banner { 82 | display: flex; 83 | flex-direction: column; 84 | align-items: center; 85 | text-align: center; 86 | background-color: #e3f2fd; 87 | padding: 10px; 88 | border-radius: 4px; 89 | margin-top: 10px; 90 | border: 1px solid #2196f3; 91 | animation: fadeIn 0.3s ease-in; 92 | } 93 | 94 | .demo-mode-icon { 95 | font-size: 24px; 96 | margin-bottom: 8px; 97 | } 98 | 99 | .demo-mode-text { 100 | font-size: 14px; 101 | display: flex; 102 | flex-direction: column; 103 | align-items: center; 104 | } 105 | 106 | .demo-mode-text strong { 107 | font-size: 16px; 108 | margin-bottom: 5px; 109 | } 110 | 111 | @keyframes pulse-select { 112 | 0% { 113 | box-shadow: 0 0 0 0 rgba(33, 150, 243, 0.7); 114 | } 115 | 70% { 116 | box-shadow: 0 0 0 10px rgba(33, 150, 243, 0); 117 | } 118 | 100% { 119 | box-shadow: 0 0 0 0 rgba(33, 150, 243, 0); 120 | } 121 | } 122 | 123 | @keyframes fadeIn { 124 | from { 125 | opacity: 0; 126 | transform: translateY(-10px); 127 | } 128 | to { 129 | opacity: 1; 130 | transform: translateY(0); 131 | } 132 | } 133 | 134 | .file-upload-disabled .file-input-label, 135 | .file-upload-disabled .file-input-container input[type="file"] { 136 | opacity: 0.6; 137 | cursor: not-allowed; 138 | pointer-events: none; 139 | } 140 | 141 | @media (max-width: 768px) { 142 | .demo-file-button { 143 | padding: 12px 15px; 144 | } 145 | 146 | .demo-mode-banner { 147 | padding: 12px; 148 | } 149 | } 150 | 151 | @media (max-width: 480px) { 152 | .demo-file-button { 153 | font-size: 14px; 154 | } 155 | 156 | .demo-mode-text { 157 | font-size: 13px; 158 | } 159 | 160 | .demo-mode-text strong { 161 | font-size: 15px; 162 | } 163 | } -------------------------------------------------------------------------------- /backend/tests/test_failed_note_pertok.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pathlib import Path 3 | from core.service.tokenizer.tokenizer_factory import TokenizerFactory 4 | from miditok import TokenizerConfig 5 | from miditoolkit import MidiFile 6 | from core.api.model import ConfigModel 7 | from core.service.note.note_id import NoteIDHandler 8 | from core.service.note.note_extractor import NoteExtractor 9 | from core.constants import EXAMPLE_MIDI_FILE_PATH2 10 | from io import BytesIO 11 | @pytest.fixture 12 | def midi_bytes(): 13 | midi_path = Path(EXAMPLE_MIDI_FILE_PATH2) 14 | return midi_path.read_bytes() 15 | 16 | @pytest.fixture 17 | def user_config(): 18 | return ConfigModel( 19 | pitch_range=[21, 108], 20 | beat_res=[(0, 4), (4, 12)], 21 | num_velocities=32, 22 | special_tokens=["PAD", "BOS", "EOS"], 23 | use_chords=False, 24 | use_rests=False, 25 | use_tempos=True, 26 | use_time_signatures=True, 27 | use_sustain_pedals=False, 28 | use_pitch_bends=False, 29 | num_tempos=32, 30 | tempo_range=[40, 250], 31 | log_tempos=False, 32 | delete_equal_successive_tempo_changes=True, 33 | sustain_pedal_duration=False, 34 | pitch_bend_range=[-12, 0, 12], 35 | delete_equal_successive_time_sig_changes=True, 36 | use_programs=False, 37 | programs=[0, 1], 38 | one_token_stream_for_programs=False, 39 | program_changes=False, 40 | use_microtiming=False, 41 | ticks_per_quarter=480, 42 | max_microtiming_shift=0.0, 43 | num_microtiming_bins=32, 44 | tokenizer="PerTok", 45 | base_tokenizer=None, 46 | ) 47 | 48 | 49 | def test_add_notes_id_should_fail_for_example2_mid(midi_bytes, user_config): 50 | tokenizer_config = TokenizerConfig( 51 | pitch_range=tuple(user_config.pitch_range), 52 | beat_res={(0, 4): 8, (4, 12): 4}, 53 | num_velocities=user_config.num_velocities, 54 | special_tokens=user_config.special_tokens, 55 | use_chords=user_config.use_chords, 56 | use_rests=user_config.use_rests, 57 | use_tempos=user_config.use_tempos, 58 | use_time_signatures=user_config.use_time_signatures, 59 | use_sustain_pedals=user_config.use_sustain_pedals, 60 | use_pitch_bends=user_config.use_pitch_bends, 61 | num_tempos=user_config.num_tempos, 62 | tempo_range=tuple(user_config.tempo_range), 63 | log_tempos=user_config.log_tempos, 64 | delete_equal_successive_tempo_changes=user_config.delete_equal_successive_tempo_changes, 65 | sustain_pedal_duration=user_config.sustain_pedal_duration, 66 | pitch_bend_range=user_config.pitch_bend_range, 67 | delete_equal_successive_time_sig_changes=user_config.delete_equal_successive_time_sig_changes, 68 | use_programs=user_config.use_programs, 69 | use_microtiming=user_config.use_microtiming, 70 | ticks_per_quarter=user_config.ticks_per_quarter, 71 | max_microtiming_shift=user_config.max_microtiming_shift, 72 | num_microtiming_bins=user_config.num_microtiming_bins, 73 | base_tokenizer=None, 74 | ) 75 | 76 | factory = TokenizerFactory() 77 | tokenizer = factory.get_tokenizer(user_config.tokenizer, tokenizer_config) 78 | note_extractor = NoteExtractor() 79 | midi = MidiFile(file=BytesIO(midi_bytes)) 80 | tokens = tokenizer(midi) 81 | notes = note_extractor.midi_to_notes(midi) 82 | note_id_handler = NoteIDHandler() 83 | #with pytest.raises(Exception) as e_info: 84 | note_id_handler.add_notes_id(tokens, notes, user_config.tokenizer) 85 | #print(f"Raised exception: {e_info.value}") -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # MidiTokVisualizer 2 | 3 | ## MIDI Processing and Visualization Library 4 | 5 | Welcome to the MidiTokVisualizer documentation! This library provides tools for processing, tokenizing, analyzing, and visualizing MIDI files through integration with the MidiTok package. 6 | 7 | ## Built on [MidiTok](https://miditok.readthedocs.io/en/latest/) 8 | 9 | MidiTokVisualizer builds upon the MidiTok Python package, a powerful open-source library for MIDI file tokenization introduced at the ISMIR 2021 conference. MidiTok enables the conversion of MIDI files into token sequences that can be fed to machine learning models like Transformers for various music generation, transcription, and analysis tasks. 10 | 11 | ## Supported Tokenizers 12 | 13 | MidiTokVisualizer currently supports the following tokenization methods from the MidiTok library: 14 | 15 | - **REMI**: Represents music events based on relative timing and emphasizes musical structure 16 | - **MIDILike**: A simpler representation that closely follows the raw MIDI message format 17 | - **TSD**: Time-Shift-Duration tokenization which captures temporal relationships explicitly 18 | - **Structured**: Provides a hierarchical representation of musical content 19 | - **CPWord**: Compound Word tokenization for representing complex musical relationships 20 | - **Octuple**: Multi-track tokenization with eight different token types 21 | - **MuMIDI**: Multi-track tokenization that represents all tracks in a single token sequence 22 | - **MMM**: Multi-track tokenization primarily designed for music inpainting and infilling 23 | - **PerTok**: Per-token representation with advanced microtiming capabilities 24 | 25 | Each tokenizer comes with customizable parameters to adapt to different musical tasks and genres. 26 | 27 | ## Key Features 28 | 29 | - **Interactive Visualization**: View MIDI files and their tokenized representations side by side 30 | - **Piano Roll Display**: Visualize notes and their relationship to tokens 31 | 32 | ## Getting Started 33 | 34 | ### Running the Application 35 | 36 | MidiTokVisualizer can be run locally using Docker Compose or by setting up the development environment manually. 37 | 38 | #### Using Docker Compose (Recommended) 39 | 40 | The easiest way to run MidiTokVisualizer is with Docker Compose: 41 | 42 | ```bash 43 | # Clone the repository 44 | git clone https://github.com/justleon/MidiTok-Visualizer.git 45 | # Start the application 46 | docker compose up 47 | ``` 48 | 49 | After running these commands, the application will be available at: 50 | 51 | - Frontend: [http://localhost:3000](http://localhost:3000) 52 | - Backend API: [http://localhost:8000](http://localhost:8000) 53 | 54 | #### Running the Application Manually 55 | ##### Backend 56 | ```bash 57 | # Install Poetry if you don't have it 58 | pip install poetry 59 | # Setup and run the backend 60 | cd backend 61 | poetry install 62 | python main.py 63 | ``` 64 | ##### Frontend 65 | Requirements: 66 | Node.js 21+ and npm 67 | ```bash 68 | # Setup and run the frontend 69 | cd frontend 70 | npx serve -s build" 71 | ``` 72 | #### Running the Documentation 73 | 74 | To build and serve the documentation locally: 75 | 76 | ```bash 77 | # Serve the documentation 78 | mkdocs serve 79 | ``` 80 | 81 | The documentation will be available at [http://localhost:8000](http://localhost:8000). 82 | 83 | ### Online Deployment 84 | 85 | MidiTokVisualizer is also available online: 86 | 87 | - Production deployment: [https://miditok-visualizer-front-production.up.railway.app/](https://miditok-visualizer-front-production.up.railway.app/) 88 | - Documentation: [https://dukiduki2000.github.io/MidiTok-Visualizer/](https://dukiduki2000.github.io/MidiTok-Visualizer/) 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /frontend/src/components/DemoFile.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './DemoFile.css'; 3 | 4 | interface DemoFileProps { 5 | onFileLoaded: (file: File) => void; 6 | onDemoModeChange: (isInDemoMode: boolean) => void; 7 | isInDemoMode: boolean; 8 | } 9 | 10 | const DemoFile: React.FC = ({ 11 | onFileLoaded, 12 | onDemoModeChange, 13 | isInDemoMode 14 | }) => { 15 | const [isLoading, setIsLoading] = useState(false); 16 | const [error, setError] = useState(null); 17 | 18 | const createMidiFromBase64 = () => { 19 | const workingMidiBase64 = "TVRoZAAAAAYAAAABAGBNVHJrAAAA2AD/AwAA/1gEBAIkCAD/WAQEAiQIAJApawCQLXUYgClAAIAtQBiQLVwYgC1AGJAoYgCQLXUYgChAAIAtQBiQLVwYgC1AGJApbgCQLXEYgClAAIAtQBiQLVkYgC1AGJAoYgCQLXEYgChAAIAtQBiQLVkYgC1AGJApbQCQLXUYgClAAIAtQBiQLVwYgC1AGJAoYgCQLXUYgChAAIAtQBiQLVwYgC1AGJApbQCQLXEYgClAAIAtQBiQLVkYgC1AGJAoYgCQLXEYgChAAIAtQBiQLVkYgC1AAP8vAA=="; 20 | const binaryString = atob(workingMidiBase64); 21 | const bytes = new Uint8Array(binaryString.length); 22 | 23 | for (let i = 0; i < binaryString.length; i++) { 24 | bytes[i] = binaryString.charCodeAt(i); 25 | } 26 | 27 | return new File([bytes], "DEMO.file", { type: "audio/midi" }); 28 | }; 29 | 30 | const handleDemoButtonClick = () => { 31 | if (isInDemoMode) { 32 | onDemoModeChange(false); 33 | setError(null); 34 | return; 35 | } 36 | 37 | setIsLoading(true); 38 | setError(null); 39 | 40 | try { 41 | const midiFile = createMidiFromBase64(); 42 | onFileLoaded(midiFile); 43 | onDemoModeChange(true); 44 | } catch (err) { 45 | const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred'; 46 | setError(errorMessage); 47 | onDemoModeChange(false); 48 | } finally { 49 | setIsLoading(false); 50 | } 51 | }; 52 | 53 | return ( 54 |
55 |
56 | 82 |
83 | 84 | {error && ( 85 |
86 | Error: {error} 87 |
88 | )} 89 | 90 | {isInDemoMode && ( 91 |
92 |
🔍
93 |
94 | Demo Mode Active 95 |
You're currently working with a demo MIDI file. File upload is disabled.
96 |
97 |
98 | )} 99 |
100 | ); 101 | }; 102 | 103 | export default DemoFile; -------------------------------------------------------------------------------- /backend/core/service/tokenizer/tokenizer_config.py: -------------------------------------------------------------------------------- 1 | from core.api.model import ConfigModel 2 | class TokenizerConfigUser: 3 | """ 4 | Adapter class that converts a user-provided ConfigModel into the format 5 | expected by the tokenizer implementation. 6 | """ 7 | def __init__(self, user_config: ConfigModel): 8 | """ 9 | Initialize the TokenizerConfigUser with parameters from a user ConfigModel. 10 | 11 | Args: 12 | user_config: The user-provided configuration model containing tokenization parameters. 13 | """ 14 | self.pitch_range = tuple(user_config.pitch_range) 15 | self.beat_res = {(0, 4): 8, (4, 12): 4} 16 | self.num_velocities = user_config.num_velocities 17 | self.special_tokens = user_config.special_tokens 18 | self.use_chords = user_config.use_chords 19 | self.use_rests = user_config.use_rests 20 | self.use_tempos = user_config.use_tempos 21 | self.use_time_signatures = user_config.use_time_signatures 22 | self.use_sustain_pedals = user_config.use_sustain_pedals 23 | self.use_pitch_bends = user_config.use_pitch_bends 24 | self.num_tempos = user_config.num_tempos 25 | self.tempo_range = tuple(user_config.tempo_range) 26 | self.log_tempos = user_config.log_tempos 27 | self.delete_equal_successive_tempo_changes = user_config.delete_equal_successive_tempo_changes 28 | self.sustain_pedal_duration = user_config.sustain_pedal_duration 29 | self.pitch_bend_range = user_config.pitch_bend_range 30 | self.delete_equal_successive_time_sig_changes = user_config.delete_equal_successive_time_sig_changes 31 | self.use_programs = user_config.use_programs 32 | self.use_microtiming = user_config.use_microtiming 33 | self.ticks_per_quarter = user_config.ticks_per_quarter 34 | self.max_microtiming_shift = user_config.max_microtiming_shift 35 | self.num_microtiming_bins = user_config.num_microtiming_bins 36 | # added for MMM 37 | self.base_tokenizer= user_config.base_tokenizer 38 | 39 | def get_params(self) -> dict: 40 | """ 41 | Get all configuration parameters as a dictionary. 42 | 43 | This method returns all the tokenizer configuration parameters in a 44 | dictionary format that can be passed to the tokenizer implementation. 45 | 46 | Returns: 47 | A dictionary containing all tokenizer configuration parameters. 48 | """ 49 | return { 50 | "pitch_range": self.pitch_range, 51 | "beat_res": self.beat_res, 52 | "num_velocities": self.num_velocities, 53 | "special_tokens": self.special_tokens, 54 | "use_chords": self.use_chords, 55 | "use_rests": self.use_rests, 56 | "use_tempos": self.use_tempos, 57 | "use_time_signatures": self.use_time_signatures, 58 | "use_sustain_pedals": self.use_sustain_pedals, 59 | "use_pitch_bends": self.use_pitch_bends, 60 | "num_tempos": self.num_tempos, 61 | "tempo_range": self.tempo_range, 62 | "log_tempos": self.log_tempos, 63 | "delete_equal_successive_tempo_changes": self.delete_equal_successive_tempo_changes, 64 | "sustain_pedal_duration": self.sustain_pedal_duration, 65 | "pitch_bend_range": self.pitch_bend_range, 66 | "delete_equal_successive_time_sig_changes": self.delete_equal_successive_time_sig_changes, 67 | "use_programs": self.use_programs, 68 | "use_microtiming": self.use_microtiming, 69 | "ticks_per_quarter": self.ticks_per_quarter, 70 | "max_microtiming_shift": self.max_microtiming_shift, 71 | "num_microtiming_bins": self.num_microtiming_bins, 72 | "base_tokenizer": self.base_tokenizer, 73 | } -------------------------------------------------------------------------------- /frontend/src/components/TokenBlock.css: -------------------------------------------------------------------------------- 1 | .token-block { 2 | display: inline-block; 3 | margin: 0.5px; 4 | position: relative; 5 | cursor: pointer; 6 | border: 2px solid #ccc; 7 | border-radius: 3px; 8 | width: 25px; 9 | height: 25px; 10 | transition: all 0.3s ease-in-out; 11 | z-index: 1; 12 | color: transparent; 13 | box-sizing: border-box; 14 | } 15 | 16 | .token-block.highlighted { 17 | border-color: black; 18 | z-index: 10; 19 | } 20 | 21 | .token-block.selected { 22 | background-color: red; 23 | } 24 | 25 | .token-block.highlight { 26 | background-color: yellow; 27 | } 28 | 29 | .token-block.large { 30 | width: 50px; 31 | height: 50px; 32 | border-radius: 5px; 33 | } 34 | 35 | .token-block-content { 36 | position: absolute; 37 | top: 50%; 38 | left: 50%; 39 | transform: translate(-50%, -50%); 40 | font-size: 10px; 41 | opacity: 0; 42 | transition: opacity 0.3s ease-in-out; 43 | white-space: pre-wrap; 44 | } 45 | 46 | .token-block.highlighted .token-block-content { 47 | opacity: 1; 48 | color: black; 49 | } 50 | 51 | .token-block.show-type { 52 | width: 35px; 53 | height: 35px; 54 | border-radius: 5px; 55 | } 56 | 57 | .token-block.show-type .token-block-content { 58 | opacity: 1; 59 | color: black; 60 | font-size: 7px; 61 | } 62 | 63 | @media screen and (max-width: 768px) { 64 | .token-block { 65 | width: 30px; 66 | height: 30px; 67 | margin: 2px; 68 | border-width: 2px; 69 | border-radius: 4px; 70 | pointer-events: auto; 71 | touch-action: manipulation; 72 | } 73 | 74 | .token-block.large { 75 | width: 60px; 76 | height: 60px; 77 | border-radius: 6px; 78 | } 79 | 80 | .token-block.show-type { 81 | width: 42px; 82 | height: 42px; 83 | border-radius: 5px; 84 | } 85 | 86 | .token-block-content { 87 | font-size: 11px; 88 | padding: 2px; 89 | max-width: 90%; 90 | text-align: center; 91 | overflow-wrap: break-word; 92 | } 93 | 94 | .token-block.show-type .token-block-content { 95 | font-size: 9px; 96 | } 97 | 98 | .token-block.highlighted, 99 | .token-block.selected { 100 | transform: scale(1.1); 101 | box-shadow: 0 0 5px rgba(0,0,0,0.3); 102 | } 103 | } 104 | 105 | /* Powiększone bloki dla ekranów dotykowych */ 106 | @media screen and (max-width: 480px) { 107 | .token-block { 108 | width: 35px; 109 | height: 35px; 110 | margin: 3px; 111 | border-width: 2px; 112 | border-radius: 5px; 113 | } 114 | 115 | .token-block.large { 116 | width: 70px; 117 | height: 70px; 118 | border-radius: 8px; 119 | } 120 | 121 | .token-block.show-type { 122 | width: 50px; 123 | height: 50px; 124 | border-radius: 6px; 125 | } 126 | 127 | .token-block-content { 128 | font-size: 9px; 129 | } 130 | 131 | .token-block.show-type .token-block-content { 132 | font-size: 8px; 133 | } 134 | 135 | .token-block.highlighted .token-block-content { 136 | font-size: 10px; 137 | font-weight: bold; 138 | } 139 | } 140 | 141 | 142 | @media (hover: none) { 143 | .token-block { 144 | cursor: default; 145 | } 146 | 147 | .token-block::after { 148 | content: ''; 149 | position: absolute; 150 | top: -10px; 151 | left: -10px; 152 | right: -10px; 153 | bottom: -10px; 154 | z-index: -1; 155 | } 156 | 157 | .token-block:active { 158 | transform: scale(1.1); 159 | border-color: #0066cc; 160 | box-shadow: 0 0 8px rgba(0, 102, 204, 0.5); 161 | } 162 | } 163 | 164 | 165 | @media (max-width: 768px) { 166 | .token-block, 167 | .token-row, 168 | .token-container { 169 | pointer-events: auto !important; 170 | touch-action: manipulation !important; 171 | } 172 | 173 | .token-container, 174 | .token-row { 175 | display: flex; 176 | flex-wrap: wrap; 177 | justify-content: flex-start; 178 | align-items: flex-start; 179 | gap: 3px; 180 | margin: 5px 0; 181 | } 182 | } -------------------------------------------------------------------------------- /docs/Development documentation/api/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | This API provides endpoints for processing MIDI files. It uses FastAPI to create a RESTful API that can tokenize MIDI files, extract note information, and retrieve musical metrics. 4 | 5 | ## Overview 6 | 7 | The API has the following components: 8 | 9 | - **FastAPI Application**: Handles HTTP requests and responses 10 | - **CORS Middleware**: Configures Cross-Origin Resource Sharing 11 | - **Logging Middleware**: Captures request/response details for debugging 12 | - **Exception Handlers**: Manages validation and unexpected errors 13 | - **MidiProcessor**: Core component for processing MIDI files 14 | 15 | ## Endpoints 16 | 17 | ### 1. Process MIDI File 18 | 19 | ``` 20 | POST /process 21 | ``` 22 | 23 | This endpoint processes a MIDI file according to the provided configuration. 24 | 25 | #### Request 26 | 27 | The request must be sent as `multipart/form-data` with the following fields: 28 | 29 | | Field | Type | Description | 30 | |--------|--------|-------------------------------------------------| 31 | | config | string | JSON configuration for MIDI processing | 32 | | file | file | MIDI file to process (.mid or .midi format) | 33 | 34 | The `config` field should contain a JSON object that conforms to the `ConfigModel` schema. 35 | 36 | #### Response 37 | 38 | ```json 39 | { 40 | "success": true, 41 | "data": { 42 | "tokens": [...], // Tokenized MIDI data 43 | "notes": [...], // Extracted notes with IDs 44 | "metrics": {...} // Musical metrics and information 45 | }, 46 | "error": null 47 | } 48 | ``` 49 | 50 | #### Error Responses 51 | 52 | - **415 Unsupported Media Type**: If the uploaded file is not a MIDI file 53 | - **422 Unprocessable Entity**: If the request parameters are invalid 54 | - **500 Internal Server Error**: If an unexpected error occurs during processing 55 | 56 | 57 | ### 2. Convert to MIDI 58 | 59 | ``` 60 | POST /convert-to-midi/ 61 | ``` 62 | 63 | This endpoint converts processed musical data back into a MIDI file format. 64 | 65 | #### Request 66 | 67 | **Content-Type:** `application/json` 68 | 69 | The request body should conform to the `MIDIConversionRequest` model structure: 70 | 71 | ```json 72 | { 73 | "output_filename": "converted_song.mid", 74 | "events": [...], // Array of MIDI events 75 | "tracks": [...], // Track configuration 76 | // ... other conversion parameters 77 | } 78 | ``` 79 | 80 | #### Response 81 | 82 | **Success Response (200):** 83 | - **Content-Type**: `audio/midi` 84 | - **Content-Disposition**: `attachment; filename={output_filename}` 85 | - **Body**: Binary MIDI file data 86 | 87 | The response is a streaming download of the generated MIDI file. 88 | 89 | #### Error Responses 90 | 91 | - **500 Internal Server Error**: If an error occurs during MIDI conversion 92 | ```json 93 | { 94 | "detail": "An error occurred during MIDI conversion: {error_details}" 95 | } 96 | ``` 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | ## Error Handling 105 | 106 | The API includes robust error handling: 107 | 108 | 1. **Validation Errors**: Returns a 422 status code with a message indicating invalid parameters 109 | 2. **HTTP Exceptions**: Returns the appropriate status code with a detailed error message 110 | 3. **Unexpected Errors**: Returns a 500 status code and logs the full stack trace for debugging 111 | 112 | ## Implementation Details 113 | 114 | 115 | 116 | ### Process Endpoint Logic 117 | 118 | The `/process` endpoint performs these steps: 119 | 120 | 1. Validates that the uploaded file is a MIDI file 121 | 2. Parses the configuration from the form data 122 | 3. Reads the MIDI file as bytes 123 | 4. Tokenizes the MIDI file using the `MidiProcessor` 124 | 5. Extracts note information and assigns unique note IDs 125 | 6. Retrieves musical metrics and information 126 | 7. Returns all processed data as a JSON response 127 | 128 | 129 | ## Environment Variables 130 | 131 | - `REACT_APP_API_BASE_URL`: The base URL for the React application (default: "http://localhost:3000") 132 | 133 | -------------------------------------------------------------------------------- /docs/Design documentation/24Z/GR5_design_proposal.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | # WIMU Projekt 2024L - Design Proposal 4 | 5 | ------------------------ 6 | 7 | ## Autorzy 8 | 9 | ***Piotr Malesa*** - 01112501@pw.edu.pl 10 | 11 | ***Kacper Stefański*** - kacper.stefanski.stud@pw.edu.pl 12 | 13 | ***Konstantin Panov*** - konstantin.panov.stud@pw.edu.pl 14 | 15 | 16 | 17 | ## Temat projektu 18 | 19 | Tematem projektu jest kontynuacja projektu **MIDITok Visualizer** z poprzedniej edycji przedmiotu. 20 | 21 | Na chwilę obecną narzędzie to składa się z aplikacji web'owej, która pozwala na przegląd tokenów wygenerowanych przez bibliotekę MidiTok na podstawie wgranego przez użytkownika pliku w formacie MIDI oraz wizualizację metryk (np. tonacja, metrum, tempo), które są wyczytywane w celu dokładnej analizy zapisanych danych w formacie MIDI. 22 | 23 | 24 | 25 | ## Harmonogram projektu 26 | 27 | - Tydzień 1 (19.02 - 23.02): - 28 | - Tydzień 2 (26.02 - 01.03): - 29 | - Tydzień 3 (04.03 - 08.03): - 30 | - **Tydzień 4 (11.03 - 15.03)**: - 31 | - **Tydzień 5 (18.03 - 22.03)**: Dostarczenie poprawionego design proposal'a ze zmodyfikowanym planem rozszerzenia aplikacji 32 | - **Tydzień 6 (25.03 - 29.03)**: Przygotowanie środowiska do pracy nad projektem oraz rozpoczęcie rozwoju nowych funkcjonalności 33 | - **Tydzień 7 (01.04 - 05.04)**: Przerwa świąteczna 34 | - **Tydzień 8 (08.04 - 12.04)**: Dalsze prace nad UI oraz prezentacja prototypu 35 | - **Tydzień 9 (15.04 - 19.04)**: Dalsze prace nad UI 36 | - **Tydzień 10 (22.04 -26.04)**: Ukończona część rozszerzenia UI 37 | - **Tydzień 11 (29.04 - 03.05)**: Majówka 38 | - **Tydzień 12 (06.05 - 10.05)**: Praca nad implementacją kolejnych tokenizerów (MMM, MuMIDI) i ewentualne poprawki UI 39 | - **Tydzień 13 (13.05 - 17.05)**: Dostarczenie i zademonstrowanie funkcjonalnego prototypu 40 | - **Tydzień 14 (20.05 - 24.05)**: Ukończenie rozszerzenia API o nowe tokenizery 41 | - **Tydzień 15 (27.05 - 31.05)**: Praca nad poprawkami po pierwszej prezentacji projektu 42 | - **Tydzień 16 (03.06 - 07.06)**: Oddanie projektu (szacowany termin) 43 | - Tydzień 17 (10.06 - 14.06): - 44 | 45 | 46 | 47 | Powyższy harmonogram dopuszcza możliwość modyfikacji dat kolejnych etapów projektu, w zależności od szybkości postępu prac nad projektem lub w wyniku zdarzeń losowych w ciągu semestru. Jest to jedynie poglądowy plan, gdyż na chwilę obecną ciężko dokładnie sprecyzować ile czasu zajmą nam poszczególne etapy. 48 | 49 | 50 | 51 | ## Planowana funkcjonalność programu 52 | 53 | Planowanym przez nas rozszerzeniem aplikacji MIDITok Visualizer jest modyfikacja zarówno istniejącej części backendowej jak i frontendowej. Część API została by poszerzona o kolejne tokenizery (MMM, MuMIDI) a część frontendu o ulepszony widok tokenów. Obecnie istniejący widok tokenów (tokeny oddzielnych ścieżej w oddzielnych wierszach) pozostanie bez zmian, natomiast zostanie dodany nowy i czytelniejszy widok tokenów, gdzie tokeny oddzielnych ścieżek byłyby wyświetlane w oddzielnych zakładkach oraz byłyby pomniejszone i wyświetlane wierszami. Dodatkowo obok tokenów każdej ścieżki wgranego pliku MIDI wyświetlony zostałby piano roll. Po najechaniu na konkretny dźwięk w piano roll'u danej ścieżi zostaną podświetlone tokeny, które mu odpowiadają (i na odwrót - po najechaniu na dany token podświetli się powiązany z nim dźwięk na piano roll'u). Okno do konfiguracji tokenizerów również zostanie ulepszone. 54 | 55 | ## Planowany stack technologiczny 56 | 57 | Przewidywany stack technologiczny jest oczywiście poniekąd uzależniony od obecnej wersji projektu, który zamierzamy rozszerzyć. Zamierzamy zatem wykorzystać podobne narzędzia co pierwotni autorzy projektu. 58 | 59 | - **Frontend**: TypeScript, React 60 | - **Backend**: Python, FastAPI/Flask 61 | - **Tokenizacja plików**: MidiTok 62 | 63 | - **Hosting aplikacji**: Heroku 64 | - **System kontroli wersji**: Git 65 | 66 | - **Hosting repozytorium**: GitLab 67 | 68 | - **Testy**: pytest 69 | 70 | 71 | ----------------------- 72 | 73 | ## Bibliografia 74 | 75 | - MIDITok. (n.d.). *MIDITok Documentation (Version 3.0.1)*. ([https://miditok.readthedocs.io/en/v3.0.1/](https://miditok.readthedocs.io/en/v3.0.1/)) 76 | 77 | 78 | 79 | 80 | W miarę postępów prac nad projektem bibliografia będzie mogła być rozszerzona. -------------------------------------------------------------------------------- /backend/core/api/logging_middleware.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from typing import Any, Callable, Dict, Tuple 4 | from uuid import uuid4 5 | 6 | from fastapi import FastAPI, Request, Response 7 | from starlette.middleware.base import BaseHTTPMiddleware 8 | 9 | 10 | class LoggingMiddleware(BaseHTTPMiddleware): 11 | def __init__(self, app: FastAPI, *, logger: logging.Logger) -> None: 12 | self._logger = logger 13 | super().__init__(app) 14 | 15 | async def dispatch(self, request: Request, call_next: Callable) -> Response: 16 | request_id: str = str(uuid4()) 17 | 18 | logging_dict: Dict[str, Any] = {"X-API-REQUEST-ID": request_id} 19 | 20 | response, response_dict = await self._log_response(call_next, request, request_id) 21 | 22 | request_dict = await self._log_request(request) 23 | logging_dict["request"] = request_dict 24 | logging_dict["response"] = response_dict 25 | 26 | self._logger.info(logging_dict) 27 | 28 | return response 29 | 30 | async def _log_request(self, request: Request) -> Dict[str, Any]: 31 | path = request.url.path 32 | if request.query_params: 33 | path += f"?{request.query_params}" 34 | 35 | request_logging = { 36 | "method": request.method, 37 | "path": path, 38 | "ip": request.client.host if request.client is not None else None, 39 | } 40 | 41 | return request_logging 42 | 43 | async def _log_response( 44 | self, call_next: Callable, request: Request, request_id: str 45 | ) -> Tuple[Response, Dict[str, Any]]: 46 | start_time = time.perf_counter() 47 | response: Response = await self._execute_request(call_next, request, request_id) 48 | finish_time = time.perf_counter() 49 | 50 | overall_status = "successful" if response.status_code < 400 else "failed" 51 | 52 | execution_time = finish_time - start_time 53 | 54 | response_logging = { 55 | "status": overall_status, 56 | "status_code": response.status_code, 57 | "time_taken": f"{execution_time:0.4f}s", 58 | } 59 | 60 | resp_body = [section async for section in response.__dict__["body_iterator"]] 61 | response.__setattr__("body_iterator", AsyncIteratorWrapper(resp_body)) 62 | 63 | return response, response_logging 64 | 65 | async def _execute_request(self, call_next: Callable, request: Request, request_id: str) -> Response: 66 | try: 67 | response: Response = await call_next(request) 68 | response.headers["X-API-Request-ID"] = request_id 69 | return response 70 | 71 | except Exception as e: 72 | self._logger.exception({"path": request.url.path, "method": request.method, "reason": e}) 73 | return Response(content="Internal Server Error", status_code=500) 74 | 75 | 76 | class AsyncIteratorWrapper: 77 | def __init__(self, obj): 78 | self._it = iter(obj) 79 | 80 | def __aiter__(self): 81 | return self 82 | 83 | async def __anext__(self): 84 | try: 85 | value = next(self._it) 86 | except StopIteration: 87 | raise StopAsyncIteration 88 | return value 89 | 90 | 91 | log_config = { 92 | "version": 1, 93 | "loggers": { 94 | "root": {"level": "INFO", "handlers": ["consoleHandler"]}, 95 | "core": {"level": "DEBUG", "handlers": ["logfile"], "qualname": "core", "propagate": 0}, 96 | }, 97 | "handlers": { 98 | "consoleHandler": { 99 | "class": "logging.StreamHandler", 100 | "formatter": "normalFormatter", 101 | "stream": "ext://sys.stdout", 102 | }, 103 | "logfile": { 104 | "class": "logging.handlers.RotatingFileHandler", 105 | "formatter": "logfileFormatter", 106 | "filename": "logfile.log", 107 | "mode": "a", 108 | }, 109 | }, 110 | "formatters": { 111 | "normalFormatter": { 112 | "format": "%(asctime)s loglevel=%(levelname)-6s logger=%(name)s %(funcName)s() L%(lineno)-4d %(message)s" 113 | }, 114 | "logfileFormatter": { 115 | "format": "%(asctime)s loglevel=%(levelname)-6s logger=%(name)s %(funcName)s() L%(lineno)-4d %(message)s" 116 | }, 117 | }, 118 | } 119 | -------------------------------------------------------------------------------- /frontend/src/components/MusicInfoDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MusicInfoData } from '../interfaces/ApiResponse'; 3 | import './MusicInfoDisplay.css'; 4 | 5 | interface TableDisplayProps { 6 | data: MusicInfoData; 7 | } 8 | 9 | const MusicInfoDisplay: React.FC = ({ data }) => { 10 | return ( 11 |
12 | {data.title && ( 13 |
14 | {data.title} 15 |
16 | )} 17 | 18 | {data.resolution !== undefined && ( 19 |
20 | Resolution: {data.resolution} 21 |
22 | )} 23 | 24 |
25 | {data.tempos && data.tempos.length > 0 && ( 26 |
27 |
28 |
29 | Tempos: 30 | {data.tempos.map((item, index) => ( 31 |
32 | Time: {item[0]} Value: {item[1].toFixed(0)} 33 |
34 | ))} 35 |
36 |
37 |
38 | )} 39 | 40 | {data.key_signatures && data.key_signatures.length > 0 && ( 41 |
42 |
43 |
44 | Key Signatures: 45 | {data.key_signatures.map((item, index) => ( 46 |
47 | Time: {item[0]}{' '} 48 | Root: {item[1]}{' '} 49 | Mode: {item[2]} 50 |
51 | ))} 52 |
53 |
54 |
55 | )} 56 | 57 | {data.time_signatures && data.time_signatures.length > 0 && ( 58 |
59 |
60 |
61 | Time Signatures: 62 | {data.time_signatures.map((item, index) => ( 63 |
64 | Time: {item[0]}{' '} 65 | Signature: {item[1]}/{item[2]} 66 |
67 | ))} 68 |
69 |
70 |
71 | )} 72 | 73 | {(data.pitch_range || 74 | data.n_pitches_used || 75 | data.polyphony || 76 | data.empty_beat_rate || 77 | data.drum_pattern_consistency) && ( 78 |
79 |
80 |
81 | {data.pitch_range !== undefined && ( 82 |
83 | Pitch Range: {data.pitch_range} 84 |
85 | )} 86 | {data.n_pitches_used !== undefined && ( 87 |
88 | Number of Pitches Used: {data.n_pitches_used} 89 |
90 | )} 91 | {data.polyphony !== undefined && ( 92 |
93 | Polyphony: {data.polyphony.toPrecision(3)} 94 |
95 | )} 96 | {data.empty_beat_rate !== undefined && ( 97 |
98 | Empty Beat Rate: {data.empty_beat_rate.toPrecision(3)} 99 |
100 | )} 101 | {data.drum_pattern_consistency !== undefined && ( 102 |
103 | Drum Pattern Consistency: {data.drum_pattern_consistency.toPrecision(3)} 104 |
105 | )} 106 |
107 |
108 |
109 | )} 110 |
111 |
112 | ); 113 | }; 114 | 115 | export default MusicInfoDisplay; 116 | -------------------------------------------------------------------------------- /docs/Design documentation/25L/Design_proposal_25L.md: -------------------------------------------------------------------------------- 1 | # WIMU 25L Design Proposal 2 | 3 | ## Funkcje oraz komentarze opisujący aktualny stan 4 | 5 | * Poprawa błędów kodu i obsługa wyjątków - podczas wstępnych testów napotkano na problemy związane z tokenizacją plików MIDI. Dla example_files wszystko działa, ale losowy plik z internetu spowodował problem z jego rozpakowaniem. Dlatego chcemy wprowadzić obsługę błędów (pierwsze co się rzuca w oczy: odczyt i zapis pliku), jak również tych wymagajacych restrukturyzację funkcji. 6 | 7 | * Restrukturyzacja backendu - wprowadzanie zasad dobrego programowania obiektowego w tym zasady SOLID z powodu co raz to bardziej rozbudowanych funkcji. 8 | 9 | * Dodanie do frontedu dynamicznego odtwarzania dźwięku wraz z piano rollem. 10 | 11 | * Dodanie dokumentacji - według grupy majacej ostatnio ten projekt, kod jest bardzo nieuporządkowany oraz brakuje w nim komentarzy, dlatego też chcielibyśmy popracować nad dokumentacją, która pozwalałaby lepiej rozwijać projekt. 12 | 13 | * Dodanie CICD - środowisko na bazie GitHub Actions umożliwiających automatyzację i usprawnienie procesów rozwoju, testowania oraz wdrażania aplikacji, a także integrację z generowaniem dokumentacji MkDocs. 14 | 15 | * Edycja MIDI na stronie i jego zapis - dodanie zakładki umożliwiającej edycję pliku MIDI z ewentualną obsługą przez bibliotekę Mido urządzeń wysyłających komunikaty MIDI. 16 | 17 | * Uporządkowanie konfiguracji tokenizerów poprzez pliki JSON z ewentualną automatyzacją po stronie odczytu informacji po prostu z pliku MIDI. 18 | 19 | 20 | 21 | ## Harmonogram projektu 22 | 23 | * **Tydzień 1 (17.03 - 23.03): Wprowadzenie do projektu:** 24 |
Przygotowanie środowiska do pracy nad projektem, zapoznanie się z narzędziami, poprawki design proposal 25 | * **Tydzień 2 (24.03 - 30.03): Aktualizacja wersji projektu:** 26 |
Merge nowego interfejsu graficznego z poprzedniej iteracji do głównej wersji projektu oraz dogłebne zapoznanie się ze zmianami. 27 | * **Tydzień 3 (31.03 - 06.04): Wdrożenie CICD:** 28 |
Przygotowanie repo pod przyszła dokumentację, przygotowanie CICD wraz z automatycznymi buildem, testami oraz deployem na railway / heroku / (ewentualnie zostanie wykorzystany serwer galera, jednak wymaga to przeniesienia repo na gitlaba) 29 | * **Tydzień 4 (07.04 - 13.04): Podjęcie działań ze zmianami w backendzie:** 30 |
Poprawienie pliku midi_processing, a dokładniej funkcji stworzonych w nowej wersji programu, usunięcie niepotrzebnych komponentów ( np. Tokenizer_factory), jak również struktury tokenizerów(głównie struktur plików service). 31 | * **Tydzień 5 (14.04 - 20.04): Dalsze działania z backendem** 32 |
Poprawa powstałych błędów znalezionych podczas testów aplikacji. Pierwsze opisy funkcji do dokumentacji 33 | * **Tydzień 6 (21.04 - 27.04):Rozpoczęci pracy z Frontendem** 34 |
Poprawa zmian aktualnych tokenizerów( po wybraniu jednego tokenizera, trzeba odświeżyć stronę żeby wybrac inny), dodanie funkcji zmiany instrumentu przy play(*), wstępne prace nad piano rollem. 35 | * **Tydzień 7 (28.04 - 04.05):** *Majówka* 36 | * **Tydzień 8 (05.05 - 11.05):Dalsze prace nad Frontedem**: 37 |
Dynamiczny piano roll wraz z podświetlaniem się aktualnie odtwarzanej informacji o dźwięku 38 | * **Tydzień 9 (12.05 - 18.05): Nowa funkcja** 39 |
Pracę nad podstroną do tworzenia własnego midi: Stworzenie nad backedznie odpowiednich funkcji wykorzystując bibliotekę MIDO do tworzenia midi, przygotowanie funkcji do API. 40 | * **Tydzień 10 (19.05 - 25.05): Nowa funkcja cz.2** 41 |
Stworzenie na frontedzie obiektów wysyłajacych informacje do backendu o wywołanie nowo stworzonych funkcji. Dodanie operacji odtwarzania danej sekwencji przez klawiaturę ( Virtual MIDI) 42 | * **Tydzień 11 (26.05 - 1.06):** Oddanie projektu, poprawa ewentualnych błędów 43 | 44 | W każdym z tych etapów, na bieżąco będzie aktualizowana dokumentacja projektu. Podkreśla się jednak, że harmonogram może ulec zmianie, ze względu na wystąpienie ewentualnych problemów. 45 | ## Planowany stack technologiczny 46 | Stack jez zależny od możliwości integracji aktualnej wersji projektu z nowymi narzędziami: 47 | 48 | * **Repozytorium:** GitHub 49 | * **Testy:** pytest, pytest-mock, pytest-benchamrk 50 | * **Backend:** Python, FastAPI 51 | * **Frontend:** React 52 | * **Dokumentacja:** MkDocs material + plugin mkdocstrings 53 | * **CICD:** GitHub Actions 54 | 55 | ## Bibliografia 56 | 57 | * MidiTok documentation ([https://miditok.readthedocs.io/en/stable/](https://miditok.readthedocs.io/en/stable/)) 58 | * GitHub Actions documentation ([https://docs.github.com/en/actions](https://docs.github.com/en/actions)) 59 | * MkDocs documentation ([https://www.mkdocs.org/](https://www.mkdocs.org/)) -------------------------------------------------------------------------------- /backend/core/api/api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import io 3 | import logging.config 4 | import os 5 | 6 | from fastapi import Body, FastAPI, File, HTTPException, UploadFile,Form,Request,status 7 | from fastapi.exceptions import RequestValidationError 8 | from fastapi.middleware.cors import CORSMiddleware 9 | from fastapi.responses import JSONResponse,StreamingResponse 10 | 11 | 12 | 13 | import traceback 14 | from core.api.logging_middleware import LoggingMiddleware, log_config 15 | from core.api.model import ConfigModel, MusicInformationData, MIDIConversionRequest 16 | from core.service.midi_processing import MidiProcessor 17 | 18 | 19 | from core.service.serializer import TokSequenceEncoder 20 | 21 | from core.service.midi.midi_event import MidiEvent 22 | 23 | 24 | 25 | logging.config.dictConfig(log_config) 26 | logger = logging.getLogger(__name__) 27 | procesor=MidiProcessor() 28 | midievent=MidiEvent() 29 | app = FastAPI() 30 | 31 | react_api_base_url = os.getenv("REACT_APP_API_BASE_URL", "http://localhost:3000") 32 | origins = [] 33 | if react_api_base_url: 34 | origins.append(react_api_base_url) 35 | 36 | 37 | app.add_middleware( 38 | CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"] 39 | ) 40 | 41 | app.add_middleware(LoggingMiddleware, logger=logger) 42 | 43 | 44 | @app.exception_handler(RequestValidationError) 45 | async def validation_exception_handler(request, exc): 46 | return JSONResponse( 47 | content={"success": False, "data": None, "error": "Invalid request parameters"}, status_code=422 48 | ) 49 | 50 | @app.exception_handler(Exception) 51 | async def global_exception_handler(request: Request, exc: Exception): 52 | logger.critical(f"Unexpected error on {request.url.path}:\n{traceback.format_exc()}") 53 | return JSONResponse( 54 | status_code=500, 55 | content={ 56 | "success": False, 57 | "data": None, 58 | "error": "Internal server error" 59 | } 60 | ) 61 | 62 | def save_to_file(data, filename): # debug function 63 | with open(filename, "w", encoding="utf-8") as f: 64 | f.write(str(data)) 65 | 66 | 67 | @app.post("/process") 68 | async def process(config: str = Form(...), file: UploadFile = File(...)) -> JSONResponse: 69 | try: 70 | if file.content_type not in ["audio/mid", "audio/midi", "audio/x-mid", "audio/x-midi"]: 71 | raise HTTPException(status_code=415, detail="Unsupported file type") 72 | config_model = ConfigModel(**json.loads(config)) 73 | midi_bytes: bytes = await file.read() 74 | tokens, notes = procesor.tokenize_midi_file(config_model, midi_bytes) 75 | serialized_tokens = json.dumps(tokens, cls=TokSequenceEncoder) 76 | note_id = 1 77 | serialized_notes = [] 78 | for track_notes in notes: 79 | serialized_track = [{**note.__dict__, "note_id": note_id + i} for i, note in enumerate(track_notes)] 80 | serialized_notes.append(serialized_track) 81 | note_id += len(track_notes) 82 | 83 | metrics: MusicInformationData = procesor.retrieve_information_from_midi(midi_bytes) 84 | return JSONResponse( 85 | content={ 86 | "success": True, 87 | "data": { 88 | "tokens": json.loads(serialized_tokens), 89 | "notes": serialized_notes, 90 | "metrics": json.loads(metrics.model_dump_json()), 91 | }, 92 | "error": None, 93 | } 94 | ) 95 | except HTTPException as e: 96 | return JSONResponse( 97 | content={"success": False, "data": None, "error": str(e.detail)}, status_code=e.status_code 98 | ) 99 | except Exception as e: 100 | return JSONResponse(content={"success": False, "data": None, "error": str(e)}, status_code=500) 101 | 102 | 103 | @app.post("/convert-to-midi/") 104 | async def convert_to_midi(params: MIDIConversionRequest): 105 | try: 106 | mid = midievent.create_midi_file_from_events(params) 107 | midi_buffer = io.BytesIO() 108 | mid.save(file=midi_buffer) 109 | midi_buffer.seek(0) 110 | 111 | return StreamingResponse( 112 | midi_buffer, 113 | media_type="audio/midi", 114 | headers={"Content-Disposition": f"attachment; filename={params.output_filename}"} 115 | ) 116 | except Exception as e: 117 | print(f"Error during MIDI conversion: {e}") 118 | raise HTTPException( 119 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 120 | detail=f"An error occurred during MIDI conversion: {e}" 121 | ) 122 | 123 | 124 | -------------------------------------------------------------------------------- /backend/core/service/midi/midi_info_extractor.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import List, Optional, Tuple 3 | 4 | import muspy 5 | import pydantic 6 | 7 | from core.api.model import BasicInfoData, MetricsData, MusicInformationData 8 | 9 | 10 | class MidiInformationExtractor: 11 | """ 12 | Class for extracting musical information and metrics from MIDI files. 13 | """ 14 | def __init__(self): 15 | pass 16 | 17 | 18 | def extract_information(self, midi_file_music: muspy.Music) -> MusicInformationData: 19 | """ 20 | Extracts and returns information from a MIDI file. 21 | 22 | Args: 23 | midi_file_music: MIDI file loaded as muspy.Music object 24 | 25 | Returns: 26 | MusicInformationData with information about the MIDI file 27 | 28 | Raises: 29 | ValueError: If the music information data couldn't be created 30 | """ 31 | basic_data = self._extract_basic_data(midi_file_music) 32 | metrics = self._extract_metrics(midi_file_music) 33 | music_info_data = self._create_music_info_data(basic_data, metrics) 34 | 35 | if music_info_data is None: 36 | raise ValueError("Couldn't handle music information data") 37 | 38 | return music_info_data 39 | 40 | def _create_music_info_data(self, basic_info: BasicInfoData, metrics_data: MetricsData) -> Optional[MusicInformationData]: 41 | """ 42 | Creates MusicInformationData object from basic info and metrics. 43 | 44 | Args: 45 | basic_info: Basic information about the MIDI file 46 | metrics_data: Metrics extracted from the MIDI file 47 | 48 | Returns: 49 | MusicInformationData object or None if validation fails 50 | """ 51 | try: 52 | data = MusicInformationData( 53 | title=basic_info.title, 54 | resolution=basic_info.resolution, 55 | tempos=basic_info.tempos, 56 | key_signatures=basic_info.key_signatures, 57 | time_signatures=basic_info.time_signatures, 58 | pitch_range=metrics_data.pitch_range, 59 | n_pitches_used=metrics_data.n_pitches_used, 60 | polyphony=metrics_data.polyphony, 61 | empty_beat_rate=metrics_data.empty_beat_rate, 62 | drum_pattern_consistency=metrics_data.drum_pattern_consistency, 63 | ) 64 | return data 65 | except pydantic.ValidationError as e: 66 | print(e) 67 | return None 68 | 69 | def _extract_basic_data(self, music_file: muspy.Music) -> BasicInfoData: 70 | """ 71 | Extracts basic data from a MIDI file. 72 | 73 | Args: 74 | music_file: MIDI file loaded as muspy.Music object 75 | 76 | Returns: 77 | BasicInfoData with basic information about the MIDI file 78 | """ 79 | tempos: List[Tuple[int, float]] = [] 80 | for tempo in music_file.tempos: 81 | tempo_data: Tuple[int, float] = (tempo.time, tempo.qpm) 82 | tempos.append(tempo_data) 83 | 84 | key_signatures: List[Tuple[int, int, str]] = [] 85 | for key_signature in music_file.key_signatures: 86 | signature_data: Tuple[int, int, str] = (key_signature.time, key_signature.root, key_signature.mode) 87 | key_signatures.append(signature_data) 88 | 89 | time_signatures: List[Tuple[int, int, int]] = [] 90 | for time_signature in music_file.time_signatures: 91 | time_data: Tuple[int, int, int] = ( 92 | time_signature.time, time_signature.numerator, time_signature.denominator) 93 | time_signatures.append(time_data) 94 | 95 | return BasicInfoData(music_file.metadata.title, music_file.resolution, tempos, key_signatures, time_signatures) 96 | 97 | def _extract_metrics(self, music_file: muspy.Music) -> MetricsData: 98 | """ 99 | Extracts metrics from a MIDI file. 100 | 101 | Args: 102 | music_file: MIDI file loaded as muspy.Music object 103 | 104 | Returns: 105 | MetricsData with metrics extracted from the MIDI file 106 | """ 107 | pitch_range = muspy.pitch_range(music_file) 108 | n_pitches_used = muspy.n_pitches_used(music_file) 109 | 110 | polyphony_rate = muspy.polyphony(music_file) 111 | if math.isnan(polyphony_rate): 112 | polyphony_rate = 0.0 113 | 114 | empty_beat_rate = muspy.empty_beat_rate(music_file) 115 | if math.isnan(empty_beat_rate): 116 | empty_beat_rate = 0.0 117 | 118 | drum_pattern_consistency = muspy.drum_pattern_consistency(music_file) 119 | if math.isnan(drum_pattern_consistency): 120 | drum_pattern_consistency = 0.0 121 | 122 | return MetricsData(pitch_range, n_pitches_used, polyphony_rate, empty_beat_rate, drum_pattern_consistency) -------------------------------------------------------------------------------- /frontend/src/components/TokenInfo.css: -------------------------------------------------------------------------------- 1 | .token-info-block { 2 | display: flex; 3 | flex-direction: column; 4 | border: 1px solid #999; 5 | border-radius: 4px; 6 | margin: 5px; 7 | position: sticky; 8 | top: 0; 9 | box-sizing: border-box; 10 | padding: 8px; 11 | text-align: left; 12 | transition: all 0.2s ease; 13 | overflow: hidden; 14 | z-index: 30; 15 | box-shadow: 0 1px 3px rgba(0,0,0,0.1); 16 | background-color: white; 17 | opacity: 1; 18 | visibility: visible; 19 | transition: opacity 0.3s ease, visibility 0s linear 0s; 20 | } 21 | 22 | .token-info-block.has-token { 23 | border-width: 2px; 24 | border-color: #555; 25 | opacity: 1 !important; 26 | visibility: visible !important; 27 | display: flex !important; 28 | } 29 | 30 | .token-info-header { 31 | font-weight: bold; 32 | font-size: 12px; 33 | margin-bottom: 5px; 34 | padding-bottom: 5px; 35 | border-bottom: 1px solid rgba(0,0,0,0.1); 36 | text-align: center; 37 | } 38 | 39 | .token-info-content { 40 | display: flex; 41 | flex-direction: column; 42 | gap: 4px; 43 | width: 100%; 44 | font-size: 12px; 45 | } 46 | 47 | .token-info-placeholder { 48 | font-size: 12px; 49 | opacity: 0.7; 50 | text-align: center; 51 | padding: 10px 0; 52 | } 53 | 54 | .info-row { 55 | display: flex; 56 | justify-content: space-between; 57 | align-items: baseline; 58 | width: 100%; 59 | } 60 | 61 | .info-label { 62 | font-weight: bold; 63 | color: #555; 64 | white-space: nowrap; 65 | margin-right: 5px; 66 | } 67 | 68 | .info-value { 69 | word-break: break-word; 70 | overflow-wrap: break-word; 71 | text-align: right; 72 | max-width: 70%; 73 | } 74 | 75 | .token-heading { 76 | font-size: 10px; 77 | color: #666; 78 | background-color: rgba(255,255,255,0.7); 79 | padding: 2px 4px; 80 | border-radius: 2px; 81 | } 82 | 83 | @media (max-width: 1200px) { 84 | .token-info-block { 85 | width: 130px; 86 | max-height: 180px; 87 | } 88 | 89 | .token-info-content { 90 | font-size: 11px; 91 | } 92 | } 93 | 94 | @media (max-width: 992px) { 95 | .token-info-block { 96 | width: 120px; 97 | max-height: 160px; 98 | padding: 6px; 99 | } 100 | } 101 | 102 | @media (max-width: 768px) { 103 | .token-info-block { 104 | width: 100%; 105 | max-height: none; 106 | padding: 8px; 107 | margin: 5px 0; 108 | position: relative; 109 | top: auto; 110 | opacity: 1 !important; 111 | visibility: visible !important; 112 | display: flex !important; 113 | } 114 | 115 | .token-info-content { 116 | font-size: 12px; 117 | gap: 5px; 118 | } 119 | 120 | .info-row { 121 | flex-direction: row; 122 | align-items: center; 123 | padding: 2px 0; 124 | } 125 | 126 | .info-value { 127 | max-width: 65%; 128 | text-align: right; 129 | } 130 | 131 | .token-info-header { 132 | font-size: 14px; 133 | margin-bottom: 8px; 134 | padding-bottom: 8px; 135 | } 136 | 137 | .token-info-placeholder { 138 | font-size: 12px; 139 | padding: 15px 0; 140 | } 141 | 142 | 143 | .left-column, 144 | .right-column { 145 | padding: 10px; 146 | overflow-x: auto; 147 | overflow-y: visible; 148 | max-height: none; 149 | } 150 | 151 | .data-display-container, 152 | .token-list-container { 153 | pointer-events: auto !important; 154 | } 155 | 156 | 157 | .token-info-content, 158 | .info-row, 159 | .info-label, 160 | .info-value { 161 | opacity: 1 !important; 162 | visibility: visible !important; 163 | display: flex !important; 164 | } 165 | } 166 | 167 | @media (max-width: 576px) { 168 | .token-info-block { 169 | width: 100%; 170 | margin: 5px 0 10px 0; 171 | border-width: 2px; 172 | box-shadow: 0 2px 5px rgba(0,0,0,0.15); 173 | opacity: 1 !important; 174 | visibility: visible !important; 175 | display: flex !important; 176 | } 177 | 178 | .token-info-header { 179 | font-size: 13px; 180 | margin-bottom: 6px; 181 | padding-bottom: 6px; 182 | background-color: #f5f5f5; 183 | padding: 5px; 184 | border-radius: 3px; 185 | } 186 | 187 | .token-info-content, 188 | .token-info-placeholder { 189 | font-size: 11px; 190 | } 191 | 192 | .info-label { 193 | color: #333; 194 | margin-right: 8px; 195 | } 196 | 197 | .token-info-block.has-token { 198 | border-color: #0066cc; 199 | background-color: #f0f7ff; 200 | opacity: 1 !important; 201 | visibility: visible !important; 202 | display: flex !important; 203 | animation: pulse-info 2s infinite; 204 | } 205 | 206 | @keyframes pulse-info { 207 | 0% { 208 | box-shadow: 0 0 0 0 rgba(0, 102, 204, 0.7); 209 | } 210 | 70% { 211 | box-shadow: 0 0 0 10px rgba(0, 102, 204, 0); 212 | } 213 | 100% { 214 | box-shadow: 0 0 0 0 rgba(0, 102, 204, 0); 215 | } 216 | } 217 | } 218 | 219 | .token-display-container .selected, 220 | .token-display-container .highlighted, 221 | .selected-token-info, 222 | .hovered-token-info { 223 | opacity: 1 !important; 224 | visibility: visible !important; 225 | display: flex !important; 226 | z-index: 30 !important; 227 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MidiTok Visualizer 2 | 3 | **[ [paper](https://arxiv.org/abs/2410.20518) ] | [ [ISMIR 2024 LBD page](https://ismir2024program.ismir.net/lbd_455.html) ] 4 | 5 | MidiTok Visualizer is a web application which allows to visualize MIDI tokenization techniques in a user-friendly way. It mostly includes the tokenizations from [MidiTok](https://github.com/Natooz/MidiTok). It aims to aid research and analysis of symbolic music, especially for researchers new to the field of MIDI processing. 6 | 7 | MidiTok Visualizer has been published at the Late Breaking Demos session at ISMIR 2024. 8 | 9 | ![Screenshot of app](docs/img/miditok_visualizer_small.png) 10 | 11 | ## Key Functionalities 12 | 13 | - **Uploading MIDI Files**: Users can upload MIDI files directly from their devices. 14 | - **Tokenizer Selection and Configuration**: The ability to choose a tokenizer and adjust its parameters. 15 | - **Token Overview**: A user-friendly display for reviewing extracted tokens. 16 | - **Symbolic Metrics Overview**: Visualization of symbolic metrics such as key, time signature, and tempo based on the uploaded MIDI file. 17 | - **Enhanced Token Presentation**: Improved readability with token display arranged in rows. 18 | - **Multi-File Support**: Users can upload multiple MIDI files and switch between them seamlessly. 19 | - **Piano Roll Display**: Visualization of the MIDI file in a piano roll format, with separate tracks/programs shown in individual tabs. 20 | - **MIDI Playback**: Ability to play back the uploaded MIDI file. 21 | - **Token Highlighting**: Users can select and highlight tokens, with corresponding notes displayed on the piano roll. 22 | - **Detailed Token Information**: Display of comprehensive token details in an expanded, separate pane. 23 | 24 | ### Further work (contributors welcome! 😊) 25 | 26 | - **Performance Optimization**: Enhancing the application's performance for handling very large MIDI files. 27 | - **Additional Tokenizer Implementations**: Integration of further tokenizers, including MMM, MuMIDI, and REMIPlus. 28 | - **Graphic Design Improvements**: Upgrading the overall visual design of the application. 29 | - **Playback Tracking**: Adding a feature to visually track the current playback position on the piano roll. 30 | 31 | 32 | ## Building and running the app 33 | 34 | ### Docker Compose 35 | 36 | You can run the whole app using `docker compose`: 37 | 38 | ```sh 39 | docker-compose up --build 40 | ``` 41 | 42 | ### Frontend 43 | 44 | Basic run: 45 | 46 | ```sh 47 | cd frontend 48 | npm install 49 | npm run dev 50 | ``` 51 | 52 | Using Docker: 53 | 54 | ```sh 55 | cd frontend 56 | docker build . -t frontend 57 | docker run frontend -p 3000:3000 58 | ``` 59 | 60 | ### Backend 61 | 62 | Basic run: 63 | 64 | ```sh 65 | cd backend_old 66 | poetry env activate 67 | poetry install 68 | python -m core.main 69 | ``` 70 | 71 | or 72 | 73 | ```sh 74 | poetry run python -m core.main 75 | ``` 76 | 77 | Using Docker: 78 | 79 | ```sh 80 | cd backend_old 81 | DOCKER_BUILDKIT=1 docker build --target=runtime . -t backend_old 82 | docker run backend_old -p 8000:8000 83 | ``` 84 | 85 | ## Testing 86 | 87 | ### Frontend 88 | 89 | Unit tests written with `jest` can be ran with: 90 | 91 | ```sh 92 | cd frontend 93 | npm install 94 | npm run test 95 | ``` 96 | 97 | ### Backend 98 | 99 | Unit tests written with `pytest` can be ran with: 100 | 101 | ```sh 102 | poetry env activate 103 | poetry install 104 | pytest 105 | ``` 106 | 107 | or 108 | 109 | ``` 110 | poetry run pytest 111 | ``` 112 | 113 | ### Logging 114 | 115 | MidiTok Visualizer includes middleware based on `starlette`, which uses `logging` for each request. A single entry contains basic data for a request and the respons, as well as the processing time. The logs are saved to `logfile.log` by default. 116 | 117 | ## Deployment 118 | 119 | You can see an example deployment on Heroku [here](https://miditok-visualizer-41e761c046c2.herokuapp.com) 120 | 121 | 122 | ## Citation 123 | 124 | If you find MidiTok Visualizer useful, please consider citing our tool: 125 | 126 | ```tex 127 | @inproceedings{wiszenko2024miditok, 128 | title={MidiTok Visualizer: a tool for visualization and analysis of tokenized MIDI symbolic music}, 129 | author={Wiszenko, Micha{\l} and Stefa{\'n}ski, Kacper and Malesa, Piotr and Pokorzy{\'n}ski, {\L}ukasz and Modrzejewski, Mateusz}, 130 | booktitle={Extended Abstracts for the Late-Breaking Demo Session of the 25th International Society for Music Information Retrieval Conference}, 131 | organization={ISMIR}, 132 | year={2024} 133 | } 134 | ``` 135 | 136 | 137 | ## Contributing 138 | 139 | We gladly welcome PRs with enhancements, features and improvements! 140 | 141 | We use pre-commit before commiting any changes: 142 | 143 | ```sh 144 | cd backend_old 145 | pre-commit run --all-files 146 | ``` 147 | 148 | We use: 149 | 150 | - black (formatting) 151 | - ruff (linting) 152 | - isort (import sorting) 153 | - mypy (type checking) 154 | 155 | ### Maintainers and contributors 156 | 157 | - Łukasz Pokorzyński 158 | - Olga Sapiechowska 159 | - Michał Wiszenko 160 | - [Mateusz Modrzejewski](https://mamodrzejewski.github.io) 161 | - Kacper Stefański 162 | - Konstantin Panov 163 | - Piotr Malesa 164 | -------------------------------------------------------------------------------- /frontend/src/components/TokenBlock.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useState, useEffect, useRef } from 'react'; 2 | import { Token } from '../interfaces/ApiResponse'; 3 | import './TokenBlock.css'; 4 | 5 | interface TokenBlockProps { 6 | item: Token; 7 | showTokenType: boolean; 8 | onHover: (t: Token | null, heading: string) => void; 9 | onSelect: (t: Token | null) => void; 10 | heading: string; 11 | highlight: boolean | null; 12 | selected: boolean | null; 13 | } 14 | 15 | export function TokenTypeToColor(type: string): string { 16 | switch (type) { 17 | case 'Bar': return 'lightcyan'; 18 | case 'Position': return 'lavender'; 19 | case 'TimeShift': return 'lightgreen'; 20 | case 'Tempo': return 'lightpink'; 21 | case 'Rest': return 'palegreen'; 22 | case 'Pitch': return 'lightblue'; 23 | case 'PitchDrum': return 'lightskyblue'; 24 | case 'NoteOn': return 'peachpuff'; 25 | case 'DrumOn': return 'sandybrown'; 26 | case 'Velocity': return 'lightcoral'; 27 | case 'Duration': return 'lightgoldenrodyellow'; 28 | case 'NoteOff': return 'khaki'; 29 | case 'DrumOff': return 'darkkhaki'; 30 | case 'Family': return 'lightgray'; 31 | case 'TimeSig': return 'thistle'; 32 | case 'MicroTiming': return 'plum'; 33 | case 'Program': return 'lightseagreen'; 34 | case 'Ignore': return 'white'; 35 | case 'PositionPosEnc': return 'lavenderblush'; // MuMIDI 36 | case 'BarPosEnc': return 'lightsteelblue'; // MuMIDI 37 | case 'Track': return 'deeppink'; // MMM 38 | default: return 'white'; 39 | } 40 | } 41 | export function SplitTokenNames(name: string): string { 42 | var arr = name.split(/(?=[A-Z])/); 43 | if(arr.length < 2) return name; 44 | var newName = ''; 45 | arr.forEach((word) => { 46 | newName = newName.concat(word); 47 | newName = newName.concat("\n"); 48 | }) 49 | 50 | return newName; 51 | } 52 | 53 | 54 | const TokenBlock: React.FC = memo( 55 | ({ item, onHover, onSelect, heading, highlight, selected, showTokenType }) => { 56 | const [isHovered, setIsHovered] = useState(false); 57 | const [windowWidth, setWindowWidth] = useState(window.innerWidth); 58 | const tokenRef = useRef(null); 59 | 60 | useEffect(() => { 61 | const handleResize = () => { 62 | setWindowWidth(window.innerWidth); 63 | }; 64 | 65 | window.addEventListener('resize', handleResize); 66 | return () => { 67 | window.removeEventListener('resize', handleResize); 68 | }; 69 | }, []); 70 | 71 | const handleMouseEnter = () => { 72 | setIsHovered(true); 73 | onHover(item, heading); 74 | }; 75 | 76 | const handleMouseLeave = () => { 77 | setIsHovered(false); 78 | onHover(null, ''); 79 | }; 80 | 81 | const handleClick = () => { 82 | if (item.note_id) { 83 | onSelect(item); 84 | } 85 | }; 86 | 87 | const isHighlighted = selected || highlight || isHovered; 88 | 89 | const getSize = () => { 90 | if (windowWidth < 768) { 91 | return showTokenType ? { width: 22, height: 22, fontSize: 5 } : { width: 18, height: 18, fontSize: 8 }; 92 | } else if (windowWidth < 1024) { 93 | return showTokenType ? { width: 28, height: 28, fontSize: 6 } : { width: 22, height: 22, fontSize: 9 }; 94 | } else { 95 | return showTokenType ? { width: 35, height: 35, fontSize: 7 } : { width: 25, height: 25, fontSize: 10 }; 96 | } 97 | }; 98 | 99 | const getExpandedSize = () => { 100 | if (windowWidth < 768) { 101 | return { width: 32, height: 32, fontSize: 8 }; 102 | } else if (windowWidth < 1024) { 103 | return { width: 40, height: 40, fontSize: 9 }; 104 | } else { 105 | return { width: 50, height: 50, fontSize: 10 }; 106 | } 107 | }; 108 | 109 | const size = getSize(); 110 | const expandedSize = getExpandedSize(); 111 | 112 | const dynamicStyles = { 113 | backgroundColor: selected 114 | ? 'red' 115 | : highlight 116 | ? 'yellow' 117 | : TokenTypeToColor(item.type), 118 | width: isHighlighted ? `${expandedSize.width}px` : `${size.width}px`, 119 | height: isHighlighted ? `${expandedSize.height}px` : `${size.height}px`, 120 | borderRadius: isHighlighted ? '5px' : '3px', 121 | }; 122 | 123 | const fontStyles = { 124 | fontSize: isHighlighted ? `${expandedSize.fontSize}px` : `${size.fontSize}px`, 125 | }; 126 | 127 | return ( 128 |
142 |
143 | 144 | { 145 | SplitTokenNames(item.type) 146 | } 147 | 148 |
149 |
150 | ); 151 | } 152 | ); 153 | 154 | export default TokenBlock; -------------------------------------------------------------------------------- /backend/core/service/midi/midi_event.py: -------------------------------------------------------------------------------- 1 | from mido import Message, MetaMessage, MidiFile, MidiTrack, bpm2tempo 2 | from typing import Dict, List 3 | from core.api.model import MIDIConversionRequest 4 | 5 | 6 | class MidiEvent: 7 | def __init__(self, ticks_per_beat=480): 8 | self.ticks_per_beat = ticks_per_beat 9 | 10 | def create_midi_file_from_events(self, params: MIDIConversionRequest) -> MidiFile: 11 | """ 12 | Creates a MIDI file from a collection of high-level MIDI events grouped by track. 13 | 14 | Args: 15 | params (MIDIConversionRequest): MIDI conversion input data. 16 | 17 | Returns: 18 | MidiFile: A fully structured MIDI file object. 19 | """ 20 | ticks_per_beat = getattr(params, 'ticks_per_beat', self.ticks_per_beat) 21 | mid = MidiFile(ticks_per_beat=ticks_per_beat) 22 | 23 | events_by_track = self._group_events_by_track(params.events) 24 | 25 | for track_num in sorted(events_by_track.keys()): 26 | primitive_events = self._convert_and_sort_primitive_events(events_by_track[track_num]) 27 | midi_track = self._create_midi_track(primitive_events) 28 | mid.tracks.append(midi_track) 29 | 30 | return mid 31 | 32 | def _group_events_by_track(self, events) -> Dict[int, List[dict]]: 33 | """ 34 | Groups high-level MIDI events by their track number. 35 | 36 | Args: 37 | events: List of event wrapper objects. 38 | 39 | Returns: 40 | Dict[int, List[object]]: Events grouped by track number. 41 | """ 42 | grouped: Dict[int, List[object]] = {} 43 | 44 | for event_wrapper in events: 45 | event = event_wrapper.root 46 | track = event.track 47 | 48 | if track not in grouped: 49 | grouped[track] = [] 50 | 51 | grouped[track].append(event) 52 | 53 | return grouped 54 | 55 | def _convert_and_sort_primitive_events(self, events: List[object]) -> List[dict]: 56 | """ 57 | Converts high-level MIDI events to primitive events and sorts them. 58 | 59 | Args: 60 | events: List of high-level MIDI events. 61 | 62 | Returns: 63 | List[dict]: Sorted primitive MIDI events. 64 | """ 65 | primitives: List[dict] = [] 66 | 67 | for event in events: 68 | if event.event_type == "note": 69 | primitives.append({ 70 | 'time': event.time, 71 | 'type': 'note_on', 72 | 'pitch': event.pitch, 73 | 'velocity': event.velocity, 74 | 'channel': event.channel 75 | }) 76 | primitives.append({ 77 | 'time': event.time + event.duration, 78 | 'type': 'note_off', 79 | 'pitch': event.pitch, 80 | 'velocity': event.velocity, 81 | 'channel': event.channel 82 | }) 83 | elif event.event_type == "tempo": 84 | primitives.append({ 85 | 'time': event.time, 86 | 'type': 'set_tempo', 87 | 'bpm': event.bpm 88 | }) 89 | 90 | return sorted(primitives, key=lambda x: ( 91 | x['time'], 92 | 0 if x['type'] == 'set_tempo' else (1 if x['type'] == 'note_off' else 2) 93 | )) 94 | 95 | def _create_midi_track(self, primitive_events: List[dict]) -> MidiTrack: 96 | """ 97 | Creates a MidiTrack and adds primitive events to it with proper delta times. 98 | 99 | Args: 100 | primitive_events: List of sorted primitive MIDI events. 101 | 102 | Returns: 103 | MidiTrack: A populated MIDI track. 104 | """ 105 | track = MidiTrack() 106 | current_time = 0 107 | 108 | for event in primitive_events: 109 | delta = event['time'] - current_time 110 | if delta < 0: 111 | print(f"Warning: Negative delta time ({delta}) – clamped to 0. Event: {event}") 112 | delta = 0 113 | 114 | if event['type'] == 'note_on': 115 | track.append(Message('note_on', 116 | note=event['pitch'], 117 | velocity=event['velocity'], 118 | time=delta, 119 | channel=event['channel'])) 120 | elif event['type'] == 'note_off': 121 | track.append(Message('note_off', 122 | note=event['pitch'], 123 | velocity=event['velocity'], 124 | time=delta, 125 | channel=event['channel'])) 126 | elif event['type'] == 'set_tempo': 127 | tempo = bpm2tempo(event['bpm']) 128 | track.append(MetaMessage('set_tempo', tempo=tempo, time=delta)) 129 | 130 | current_time = event['time'] 131 | 132 | if not any(msg.is_meta and msg.type == 'end_of_track' for msg in track): 133 | track.append(MetaMessage('end_of_track', time=0)) 134 | 135 | return track 136 | 137 | -------------------------------------------------------------------------------- /docs/Design documentation/24L/notes_pl.md: -------------------------------------------------------------------------------- 1 | # WIMU-MidiTokVisualizer 2 | 3 | MidiTok Visualizer to aplikacja webowa pozwalająca na wizualizację tokenizacji plików MIDI przez bibliotekę MidiTok. 4 | 5 | **Funkcjonalność dodana w wersji 2023Z:** 6 | - wgrywanie pliku MIDI z urządzenia, 7 | - wybranie tokenizera i jego parametrów, 8 | - przegląd wyodrębnionych tokenów w formacie przyjaznym dla użytkownika, 9 | - przegląd metryk symbolicznych (takich jak klucz, metrum, tempo) na podstawie MIDI 10 | 11 | **Funkcjonalność dodana w wersji 2024L:** 12 | - zmiana metody prezentacji tokenów na sposób bardziej czytelny (wyświetlanie w rzędach), 13 | - możliwość wgrywania wielu plików i przełączania się pomiędzy nimi, 14 | - wyświetlanie piano roll'a z wgranym plikiem MIDI (osobne track'i/programy w osobnych zakładkach), 15 | - możliwość odtworzenia wgranego pliku MIDI, 16 | - możliwość zaznaczania i podświetlania tokenów oraz odpowiadających im dźwięków na piano roll'u 17 | - wyświetlanie szczegółowych informacji o tokenie w osobnej, powiększonej ramce 18 | 19 | **Potencjalna funkcjonalność do dodania w przyszłych wersjach:** 20 | - poprawa wydajności działania aplikacji przy wgrywanych bardzo dużych plikach MIDI, 21 | - dodanie implementacji reszty tokenizerów (MMM, MuMIDI, REMIPlus), 22 | - poprawa ogólnej oprawy graficznej aplikacji, 23 | - dodanie śledzenia obecnie odtwarzanej pozycji na piano roll'u 24 | 25 | 26 | ### Proces developerski 27 | 28 | W celu zachowania zasad clean code, przed wrzuceniem commita na brancha, zaleca się wykonanie pre-commita. Aby uruchomić pre-commit, należy użyć komendy: 29 | 30 | ``` 31 | cd backend 32 | pre-commit run --all-files 33 | ``` 34 | 35 | W skład skryptu pre-commit wchodzą: 36 | 37 | - black (formatowanie) 38 | - ruff (linting) 39 | - isort (sortowanie importów) 40 | - mypy (weryfikacja typowania) 41 | 42 | ### Budowanie i uruchamianie aplikacji 43 | 44 | #### Aplikacja frontendowa 45 | 46 | Podstawowe uruchamianie aplikacji: 47 | 48 | ``` 49 | cd frontend 50 | npm install 51 | npm run dev 52 | ``` 53 | 54 | Uruchamianie aplikacji przy pomocy Dockera: 55 | 56 | ``` 57 | cd frontend 58 | docker build . -t frontend 59 | docker run frontend -p 3000:3000 60 | ``` 61 | 62 | #### Aplikacja backendowa 63 | 64 | Podstawowe uruchamianie aplikacji: 65 | 66 | ``` 67 | cd backend 68 | poetry shell 69 | poetry install 70 | python -m core.main 71 | ``` 72 | 73 | lub 74 | 75 | ``` 76 | poetry run python -m core.main 77 | ``` 78 | 79 | Uruchamianie aplikacji przy pomocy Dockera: 80 | 81 | ``` 82 | cd backend 83 | DOCKER_BUILDKIT=1 docker build --target=runtime . -t backend 84 | docker run backend -p 8000:8000 85 | ``` 86 | 87 | #### Docker Compose 88 | 89 | Możliwe jest również uruchomienie całego projektu przy użyciu Docker Compose: 90 | 91 | ``` 92 | docker-compose up --build 93 | ``` 94 | 95 | ### Testowanie aplikacji 96 | 97 | #### Aplikacja frontendowa 98 | 99 | Testy jednostkowe uruchamiane są przy użyciu *jest*: 100 | 101 | ``` 102 | cd frontend 103 | npm install 104 | npm run test 105 | ``` 106 | 107 | ### Aplikacja backendowa 108 | 109 | Testy jednostkowe uruchamiane są przy użyciu *pytest*: 110 | 111 | ``` 112 | poetry shell 113 | poetry install 114 | pytest 115 | ``` 116 | 117 | lub: 118 | 119 | ``` 120 | poetry run pytest 121 | ``` 122 | 123 | ### Logi 124 | 125 | Zaimplementowano *middleware* na bazie *starlette*, który przy użyciu modułu *logging* tworzy logi dla każdego zapytania do serwera. Pojedynczy wpis w logach zawiera podstawowe dane dla pojedynczego zapytania oraz odpowiedzi serwera, jak również czas przetwarzania zapytania. Domyślnie logi zapisywane są do pliku *logfile.log*. 126 | 127 | ### Deployment 128 | 129 | Obie aplikacje są hostowane na Heroku. Aplikacja frontendowa jest dostępna pod adresem: [https://wimu-frontend-ccb0bbc023d3.herokuapp.com](https://wimu-frontend-ccb0bbc023d3.herokuapp.com) 130 | 131 | ### Oryginalni autorzy (2023Z) 132 | 133 | - Łukasz Pokorzyński 134 | - Olga Sapiechowska 135 | - Michał Wiszenko 136 | 137 | ### Autorzy rozwijający projekt (2024L) 138 | 139 | - Kacper Stefański 140 | - Konstantin Panov 141 | - Piotr Malesa 142 | 143 | ### Planowany harmonogram prac projektu 144 | 145 | - Tydzień 1 (19.02 - 23.02): - 146 | - Tydzień 2 (26.02 - 01.03): - 147 | - Tydzień 3 (04.03 - 08.03): - 148 | - **Tydzień 4 (11.03 - 15.03)**: - 149 | - **Tydzień 5 (18.03 - 22.03)**: Dostarczenie poprawionego design proposal'a ze zmodyfikowanym planem rozszerzenia aplikacji 150 | - **Tydzień 6 (25.03 - 29.03)**: Przygotowanie środowiska do pracy nad projektem oraz rozpoczęcie rozwoju nowych funkcjonalności 151 | - **Tydzień 7 (01.04 - 05.04)**: Przerwa świąteczna 152 | - **Tydzień 8 (08.04 - 12.04)**: Dalsze prace nad UI oraz prezentacja prototypu 153 | - **Tydzień 9 (15.04 - 19.04)**: Dalsze prace nad UI 154 | - **Tydzień 10 (22.04 -26.04)**: Ukończona część rozszerzenia UI 155 | - **Tydzień 11 (29.04 - 03.05)**: Majówka 156 | - **Tydzień 12 (06.05 - 10.05)**: Praca nad implementacją kolejnych tokenizerów (MMM, MuMIDI) i ewentualne poprawki UI 157 | - **Tydzień 13 (13.05 - 17.05)**: Dostarczenie i zademonstrowanie funkcjonalnego prototypu 158 | - **Tydzień 14 (20.05 - 24.05)**: Ukończenie rozszerzenia API o nowe tokenizery 159 | - **Tydzień 15 (27.05 - 31.05)**: Praca nad poprawkami po pierwszej prezentacji projektu 160 | - **Tydzień 16 (03.06 - 07.06)**: Oddanie projektu (szacowany termin) 161 | - Tydzień 17 (10.06 - 14.06): - -------------------------------------------------------------------------------- /backend/core/api/model.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import dataclass 3 | from typing import Literal, Optional, List,Union 4 | 5 | from pydantic import BaseModel, Field, NonNegativeFloat, NonNegativeInt, PositiveInt, StrictBool, model_validator, RootModel 6 | from typing_extensions import Annotated 7 | 8 | 9 | class ConfigModel( 10 | BaseModel 11 | ): # TODO: dynamic beat_res, beat_res_rest, chord_maps, chord_tokens_with_root_note, chord_unknown, time_signature_range 12 | tokenizer: Literal[ 13 | "REMI", "REMIPlus", "MIDILike", "TSD", "Structured", "CPWord", "Octuple", "MuMIDI", "MMM", "PerTok" 14 | ] 15 | pitch_range: Annotated[list[Annotated[int, Field(ge=0, le=127)]], Field(min_length=2, max_length=2)] 16 | num_velocities: Annotated[int, Field(ge=0, le=127)] 17 | special_tokens: list[str] 18 | use_chords: StrictBool 19 | use_rests: StrictBool 20 | use_tempos: StrictBool 21 | use_time_signatures: StrictBool 22 | use_sustain_pedals: StrictBool 23 | use_pitch_bends: StrictBool 24 | num_tempos: NonNegativeInt 25 | tempo_range: Annotated[list[NonNegativeInt], Field(min_length=2, max_length=2)] 26 | log_tempos: StrictBool 27 | delete_equal_successive_tempo_changes: StrictBool 28 | sustain_pedal_duration: StrictBool 29 | pitch_bend_range: Annotated[list[int], Field(min_length=3, max_length=3)] 30 | delete_equal_successive_time_sig_changes: StrictBool 31 | use_programs: StrictBool 32 | programs: Optional[Annotated[list[int], Field(min_length=2, max_length=2)]] 33 | one_token_stream_for_programs: Optional[StrictBool] 34 | program_changes: Optional[StrictBool] 35 | # added for PerTok 36 | use_microtiming: StrictBool 37 | ticks_per_quarter: Annotated[int, Field(ge=24, le=960)] 38 | max_microtiming_shift: Annotated[float, Field(ge=0, le=1)] 39 | num_microtiming_bins: Annotated[int, Field(ge=1, le=64)] 40 | # added for MMM 41 | base_tokenizer: Literal[ 42 | 'MIDILike', 'TSD', 'REMI' 43 | ] | None 44 | 45 | @model_validator(mode="before") 46 | @classmethod 47 | def validate_to_json(cls, value): 48 | if isinstance(value, str): 49 | return cls(**json.loads(value)) 50 | return value 51 | 52 | @model_validator(mode="after") 53 | @classmethod 54 | def check_valid_ranges(cls, values): 55 | min_pitch = values.pitch_range[0] 56 | max_pitch = values.pitch_range[1] 57 | if min_pitch > max_pitch: 58 | raise ValueError("max_pitch must be greater or equal to min_pitch") 59 | 60 | min_tempo = values.tempo_range[0] 61 | max_tempo = values.tempo_range[1] 62 | if min_tempo > max_tempo: 63 | raise ValueError("max_tempo must be greater or equal to min_tempo") 64 | 65 | min_pitch_bend = values.pitch_bend_range[0] 66 | max_pitch_bend = values.pitch_bend_range[1] 67 | if min_pitch_bend > max_pitch_bend: 68 | raise ValueError("max_pitch_bend must be greater or to than min_pitch_bend") 69 | 70 | return values 71 | 72 | 73 | class MusicInformationData(BaseModel): 74 | # Basic MIDI file information 75 | title: str 76 | resolution: PositiveInt 77 | tempos: list[tuple[NonNegativeInt, float]] 78 | key_signatures: list[tuple[NonNegativeInt, int, str]] 79 | time_signatures: list[tuple[NonNegativeInt, int, int]] 80 | 81 | # Additional metrics retrieved from the MIDI file 82 | pitch_range: NonNegativeInt 83 | n_pitches_used: NonNegativeInt 84 | polyphony: NonNegativeFloat 85 | 86 | empty_beat_rate: NonNegativeFloat 87 | drum_pattern_consistency: NonNegativeFloat 88 | 89 | 90 | @dataclass 91 | class BasicInfoData: 92 | title: str 93 | resolution: int 94 | tempos: list[tuple[int, float]] 95 | key_signatures: list[tuple[int, int, str]] 96 | time_signatures: list[tuple[int, int, int]] 97 | 98 | 99 | @dataclass 100 | class MetricsData: 101 | pitch_range: int 102 | n_pitches_used: int 103 | polyphony: float 104 | 105 | empty_beat_rate: float 106 | drum_pattern_consistency: float 107 | 108 | 109 | @dataclass 110 | class Note: 111 | pitch: int 112 | name: str 113 | start: int 114 | end: int 115 | velocity: int 116 | 117 | class NoteEvent(BaseModel): 118 | event_type: Literal["note"] = "note" 119 | track: int = Field(0, ge=0, description="Track number (0-indexed).") 120 | time: int = Field(0, ge=0, description="Absolute start time of the note in ticks from the beginning.") 121 | duration: int = Field(1, ge=1, description="Duration of the note in ticks.") 122 | pitch: int = Field(60, ge=0, le=127, description="MIDI note number (0-127). C4 is 60.") 123 | velocity: int = Field(64, ge=0, le=127, description="MIDI note velocity (0-127).") 124 | channel: int = Field(0, ge=0, le=15, description="MIDI channel (0-15).") 125 | 126 | 127 | class TempoEvent(BaseModel): 128 | event_type: Literal["tempo"] = "tempo" 129 | track: int = Field(0, ge=0, description="Track number (0-indexed).") 130 | time: int = Field(0, ge=0, description="Absolute time of the tempo change in ticks.") 131 | bpm: float = Field(120.0, gt=0, description="Beats per minute.") 132 | 133 | class MIDIEvent(RootModel[Union[NoteEvent, TempoEvent]]): 134 | pass 135 | 136 | class MIDIConversionRequest(BaseModel): 137 | events: List[MIDIEvent] 138 | ticks_per_beat: int = Field(480, ge=1, description="Ticks per beat for the MIDI file.") 139 | output_filename: str = Field("output.mid", description="Name of the output MIDI file.") 140 | start_delay_ms: int = Field(300, ge=0, description="Delay in milliseconds before starting playback.") 141 | 142 | -------------------------------------------------------------------------------- /docs/Design documentation/25L/Weekly_Update.md: -------------------------------------------------------------------------------- 1 | # Weekly Update 2 | ## Tydzień 1 (17.03 - 23.03): Wprowadzenie do projektu 3 | W tym tygodniu przygotowano środowisko do pracy nad projektem u wszystkich członków zespołu, wniesiono poprawki do design proposal, które wymagają jeszcze akceptacji. Skontaktowano się z twórcą, jak również poprzednikami projektu w celach dalszych usprawnień. Zapoznano się ze strukturą projektu, użytymi narzędziami, bibliotekami. 4 | ## Tydzień 2 (24.03 - 30.03): Aktualizacja wersji projektu 5 | Tydzień ten poświęcono na omówienie z twórcą projektu proponowanych zmian, jak również przekazano pierwsze uwagi dotyczące działania aplikacji. Dodatkowo grupa wzięła udział przy omawianiu najnowszej aktualizacji projektu z 24L. 6 | ## Tydzień 3 (31.03 - 06.04): Wdrożenie CICD 7 | Dodano wstępna wersje CI zawierającą budowanie dokumentacji w MKDocs, planowana jest w trakcie rozbudowa dokumentacji o kolejne rozdziały, jak również ustawienie automatycznej dokumentacji na podstawie kodu. Cały czas trwa walka z odpowiednim ustawieniem deploy na heroku - występuje tutaj problemy z routing'iem, jednak żeby nie tracić czasu zostanie to przełożone na inne tygodnie. W kolejnym tygodniu zostanie rozbudowane automatyczne testowanie. W tym miejscu plan zostaje opóźniony min o tydzień. 8 | ## Tydzień 4 (07.04 - 13.04): Podjęcie działań ze zmianami w backendzie 9 | Dodano CI pod frontend oraz backend. 10 | Dodano w nomwym backendzie klasy midi_loader,note id, note exctractor 11 | ## Tydzień 5 (14.04 - 20.04): Dalsze działania z backendem 12 | Dodano w Frontendzie możliwość tokenizacji jednego pliku różnymi tokenizarami 13 | Dodano w nomwym backendzie klasy tokenizer_factory oraz tokenizer_config, midi_processing 14 | Przygotowano wstępną canve w react, umożliwiającą tworzenie oraz modyfikacje bloczków, jak również wyciągnięto informacje o pozycji oraz wielkości bloczków, które w przyszłości zostano przekształcone w Midi 15 | ## Tydzień 6 (21.04 - 27.04):Rozpoczęcie pracy z Frontendem 16 | - Dodano deploy na railway(https://miditok-visualizer-front-production.up.railway.app/), jak również poprawiono api w którym występował problem przy aplikacji zdeployowanej. Problem dotyczy konstrukcji tworzenia zapytania process, gdzie w backendzie przekazywano w jednym request jednocześnie body JSON oraz plik, co w Fastapi opartym na pydantic nie jest wspierane i zgodne z zasadami. Lokalnie opcja działała, dlatego że użyto Uvicorn. 17 | - Usunięto design propsale z dokumentacji. Dodano dokumentacje dotyczącą deploy. 18 | - Dodano test dla tokenizera PERTOK, który w backendzie wywołuje błąd -name 'current_note_id' is not defined. Błąd został naprawiony w nowszej wersji backendu. 19 | - Dodano w Frontendzie możliwość tokenizacji jednego pliku, różnymi tokenizerami bez wymogu każdorazowego wysyłania pliku. 20 | - W tym tygodniu cały czas rozwijany jest backend. 21 | ## Tydzień 7 (28.04 - 04.05): *Majówka* 22 | - Prace w tym tygodniu zostały zatrzymane 23 | ## Tydzień 8 (05.05 - 11.05):Dalsze prace nad Frontedem: 24 | - Poprawiono CI Front, który buduje się mimo ostrzeżeń 25 | - Dodano nową wersję backendu, w której dodano klasy oraz metody, jak również obsługę błędów. Dodawanie nowych tokeznierów jest teraz dużo łatwiejsze poprzez dodanie do rejestru i stworzenie odpowiednich metod do mapowania note. 26 | - Dodano obsługę błędów przy ładowaniu plików MIDI 27 | - Poprawiono błędy dużych plików MIDI 28 | - Zmieniono testy pod nowy backend 29 | - Dodano nowe tokenizery: muMIDI i MMM. 30 | - Dodano do configu base tokenizer używany przez MMM 31 | - Dodano pełną dokumentacje backendu 32 | - Zmieniono moduł odtwarzania plików MIDI we fronted. Od teraz możliwe jest dodawanie "instrumentów". Odtwarzanie nie jest teraz "grane" przez jeden "instrument" 33 | - Dodano pasek stanu odtwarzacza MIDI, jak również czas, głośność oraz opcje pauzy i stopu 34 | - Dodano DEMO mode, który ładuje plik demo MIDI 35 | - Dodano moduł ładowania plików 36 | - Zmieniono wygład zakładek, ładowania plików 37 | - Odświeżono wygład całej strony, dodając lepszą organizacje modułów oraz animacje 38 | - Dostosowano stronę pod urządzenia mobilne 39 | - Zmieniono proces uploadu plików. Od teraz można dodawać pliki, a następnie wybierać, który pliki MIDI oraz w jaki sposób go przetworzyć 40 | - Poprawiono skalowanie napisów w blokach tokeninfo oraz poprawiono skalowanie tokenblock'ów 41 | 42 | ## Tydzień 9 (12.05 - 18.05): Nowa funkcja- Make your own midi file[MYOMF] 43 | 44 | - Zmieniono piano roll oraz odtwarzacz. od teraz jest to jeden zsynchronizowany moduł, umożliwiający dodanie slidera oraz aktualnie odtwarzanych nut. Dodano również auto scroll. 45 | - Poprawiono dokumentacje modelu. 46 | - [MYOMF] Dodanie canvy z możliwością rysowania i usuwania bloków 47 | - [MYOMF] Dodano wstępny etap tworzenia midi przez zapis z canvas do pliku .mid używając do tego MIDO 48 | - [MYOMF] Dodano ustawienia tworzonych plików MIDI (ticks per beat, bpm, filename) 49 | - [MYOMF] Stworzono moduł wirtualnej klawiatury midi, sprzężony z fizyczną klawiaturą 50 | - [MYOMF] Dodano sterowanie canvą 51 | ## Tydzień 10 (19.05 - 25.05): Nowa funkcja cz.2 [[MYOMF]] 52 | - Zmieniono podejście do nowej funkcji i użyto WEBMidi. Dzięki niemu dodano opcje nagrywania poprzez fizyczne urządzenia produkujące sygnały MIDI. 53 | - Poprawiono moduł odgrywania nut z canvy- odgrywa on teraz całą nutę. 54 | - Dodano połączenie między wirtualną klawiaturą a fizycznym urządzeniem- wybranie na jednym z urządzeń odpowiedniego przycisku odgrywa go na drugim 55 | - Poprawiono styl MIDICanvas 56 | - Dodano animacje rysowania kolejnych nut. 57 | - Dodano opcje dodawanie i usuwanie ścieżek 58 | - Dodano przycisk tworzenia i upload do głównej funkcji tokenizacji. 59 | - Dodano opcje Record oraz Play via MIDI Device 60 | ## Tydzień 11 (26.05 - 1.06): Oddanie projektu, poprawa ewentualnych błędów 61 | - Poprawiono błąd wyglądu wyboru base-tokenizera 62 | - Poprawiono wygląd przycisku upload file przy trybie demo 63 | - Poprawiono bład nagrywania 64 | - Wyłączono opcje tworzenia midi w trybie demo 65 | - Dodano dokumentacji nowej funkcji w api -------------------------------------------------------------------------------- /frontend/src/components/MidiCreate/VirtualKeyboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import './VirtualKeyboard.css'; 3 | 4 | interface VirtualKeyboardProps { 5 | onNoteOn: (pitch: number, velocity: number) => void; 6 | onNoteOff: (pitch: number) => void; 7 | activeKeys: number[]; 8 | setActiveKeys: React.Dispatch>; 9 | } 10 | 11 | const VirtualKeyboard: React.FC = ({ 12 | onNoteOn, 13 | onNoteOff, 14 | activeKeys, 15 | setActiveKeys 16 | }) => { 17 | const startOctave = 4; 18 | const numOctaves = 2; 19 | const whiteKeys = [0, 2, 4, 5, 7, 9, 11]; 20 | const blackKeys = [1, 3, 6, 8, 10]; 21 | 22 | const handleMouseDown = (pitch: number) => { 23 | if (!activeKeys.includes(pitch)) { 24 | setActiveKeys(prev => [...prev, pitch]); 25 | onNoteOn(pitch, 90); 26 | } 27 | }; 28 | 29 | const handleMouseUp = (pitch: number) => { 30 | setActiveKeys(prev => prev.filter(key => key !== pitch)); 31 | onNoteOff(pitch); 32 | }; 33 | 34 | useEffect(() => { 35 | const handleWindowMouseUp = () => { 36 | activeKeys.forEach(key => onNoteOff(key)); 37 | setActiveKeys([]); 38 | }; 39 | 40 | window.addEventListener('mouseup', handleWindowMouseUp); 41 | return () => window.removeEventListener('mouseup', handleWindowMouseUp); 42 | }, [activeKeys, onNoteOff, setActiveKeys]); 43 | 44 | useEffect(() => { 45 | const keyMap: { [key: string]: number } = { 46 | 'a': 60, 47 | 'w': 61, 48 | 's': 62, 49 | 'e': 63, 50 | 'd': 64, 51 | 'f': 65, 52 | 't': 66, 53 | 'g': 67, 54 | 'y': 68, 55 | 'h': 69, 56 | 'u': 70, 57 | 'j': 71, 58 | 'k': 72, 59 | 'o': 73, 60 | 'l': 74, 61 | 'p': 75, 62 | ';': 76, 63 | }; 64 | 65 | const pressedKeys = new Set(); 66 | 67 | const handleKeyDown = (e: KeyboardEvent) => { 68 | if (pressedKeys.has(e.key) || !keyMap[e.key]) return; 69 | 70 | pressedKeys.add(e.key); 71 | const pitch = keyMap[e.key]; 72 | handleMouseDown(pitch); 73 | }; 74 | 75 | const handleKeyUp = (e: KeyboardEvent) => { 76 | if (!pressedKeys.has(e.key) || !keyMap[e.key]) return; 77 | 78 | pressedKeys.delete(e.key); 79 | const pitch = keyMap[e.key]; 80 | handleMouseUp(pitch); 81 | }; 82 | 83 | window.addEventListener('keydown', handleKeyDown); 84 | window.addEventListener('keyup', handleKeyUp); 85 | 86 | return () => { 87 | window.removeEventListener('keydown', handleKeyDown); 88 | window.removeEventListener('keyup', handleKeyUp); 89 | }; 90 | }, [onNoteOn, onNoteOff]); 91 | 92 | return ( 93 |
94 |

95 | Virtual MIDI Keyboard 96 | 97 | {activeKeys.length > 0 ? 'Playing: ' + activeKeys.map(key => { 98 | const octave = Math.floor(key / 12) - 1; 99 | const note = key % 12; 100 | const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; 101 | return `${noteNames[note]}${octave}`; 102 | }).join(', ') : ''} 103 | 104 |

105 |
106 | {Array.from({ length: numOctaves }).flatMap((_, octaveIdx) => { 107 | const octave = startOctave + octaveIdx; 108 | 109 | return whiteKeys.map(noteInOctave => { 110 | const pitch = (octave + 1) * 12 + noteInOctave; 111 | const isActive = activeKeys.includes(pitch); 112 | 113 | return ( 114 |
handleMouseDown(pitch)} 118 | onMouseUp={() => handleMouseUp(pitch)} 119 | onMouseLeave={() => activeKeys.includes(pitch) && handleMouseUp(pitch)} 120 | > 121 |
122 | {['C', 'D', 'E', 'F', 'G', 'A', 'B'][whiteKeys.indexOf(noteInOctave)]}{octave} 123 |
124 |
125 | ); 126 | }); 127 | })} 128 | 129 | {Array.from({ length: numOctaves }).flatMap((_, octaveIdx) => { 130 | const octave = startOctave + octaveIdx; 131 | 132 | return blackKeys.map(noteInOctave => { 133 | const pitch = (octave + 1) * 12 + noteInOctave; 134 | const isActive = activeKeys.includes(pitch); 135 | const whiteKeyWidth = 100 / (whiteKeys.length * numOctaves); 136 | let leftPosition; 137 | const adjustedPosition = whiteKeys.findIndex(k => k > noteInOctave); 138 | if (adjustedPosition === -1) { 139 | leftPosition = `${(whiteKeys.length * octaveIdx + whiteKeys.length - 0.5) * whiteKeyWidth - 10}%`; 140 | } else { 141 | leftPosition = `${(whiteKeys.length * octaveIdx + adjustedPosition - 0.5) * whiteKeyWidth - 10}%`; 142 | } 143 | 144 | return ( 145 |
handleMouseDown(pitch)} 153 | onMouseUp={() => handleMouseUp(pitch)} 154 | onMouseLeave={() => activeKeys.includes(pitch) && handleMouseUp(pitch)} 155 | /> 156 | ); 157 | }); 158 | })} 159 |
160 |
161 | Play with your mouse or computer keyboard (A-L keys correspond to C4-E5). Press CTRL to delete node. 162 |
163 |
164 | ); 165 | }; 166 | 167 | export default VirtualKeyboard; -------------------------------------------------------------------------------- /frontend/src/components/DataDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Token, NestedList, Note } from '../interfaces/ApiResponse'; 3 | import TokenBlock from './TokenBlock'; 4 | import TokenInfo from './TokenInfo'; 5 | import './DataDisplay.css'; 6 | 7 | interface DataDisplayProps { 8 | data: NestedList; 9 | hoveredNote: Note | null; 10 | selectedNote: Note | null; 11 | onTokenHover: (token: Token | null) => void; 12 | onTokenSelect: (token: Token | null) => void; 13 | hoveredToken: Token | null; 14 | selectedToken: Token | null; 15 | } 16 | 17 | const ITEMS_PER_PAGE = 140; 18 | const CHUNK_SIZE = 14; 19 | 20 | const DataDisplay: React.FC = ({ data, hoveredNote, selectedNote, hoveredToken, selectedToken, onTokenHover, onTokenSelect }) => { 21 | const [token, setToken] = useState(null); 22 | const [heading, setHeading] = useState(''); 23 | const [currentPage, setCurrentPage] = useState(0); 24 | const [isPaginationEnabled, setIsPaginationEnabled] = useState(true); 25 | const [showTokenType, setShowTokenType] = useState(false); 26 | 27 | const flattenedData: Token[] = React.useMemo(() => { 28 | const flatten = (list: NestedList): Token[] => { 29 | if (Array.isArray(list)) { 30 | return list.flatMap((item) => (Array.isArray(item) ? flatten(item) : [item])); 31 | } 32 | return [list]; 33 | }; 34 | return flatten(data); 35 | }, [data]); 36 | 37 | const totalPages = Math.ceil(flattenedData.length / ITEMS_PER_PAGE); 38 | 39 | const currentPageTokens = React.useMemo(() => { 40 | if (!isPaginationEnabled) { 41 | return flattenedData; 42 | } 43 | const start = currentPage * ITEMS_PER_PAGE; 44 | const end = start + ITEMS_PER_PAGE; 45 | return flattenedData.slice(start, end); 46 | }, [flattenedData, currentPage, isPaginationEnabled]); 47 | 48 | const chunkTokens = (tokens: Token[], size: number): Token[][] => { 49 | let chunks: Token[][] = []; 50 | for (let i = 0; i < tokens.length; i += size) { 51 | chunks.push(tokens.slice(i, i + size)); 52 | } 53 | return chunks; 54 | }; 55 | 56 | const chunkedTokens = chunkTokens(currentPageTokens, CHUNK_SIZE); 57 | 58 | const updateTokenInfo = (token: Token | null, heading: string) => { 59 | setToken(token); 60 | setHeading(heading); 61 | }; 62 | 63 | const handleNextPage = () => { 64 | setCurrentPage((prev) => Math.min(prev + 1, totalPages - 1)); 65 | }; 66 | 67 | const handlePreviousPage = () => { 68 | setCurrentPage((prev) => Math.max(prev - 1, 0)); 69 | }; 70 | 71 | const shouldShowPaginationToggle = totalPages > 1; 72 | 73 | if (!Array.isArray(data)) { 74 | return
Invalid data
; 75 | } 76 | 77 | return ( 78 |
79 |
80 | 88 |
89 | {shouldShowPaginationToggle && ( 90 |
91 | 99 |
100 | )} 101 |
102 |
103 | 104 |
105 |
106 |
107 | {chunkedTokens.map((chunk, chunkIndex) => ( 108 |
109 | {chunk.map((token, tokenIndex) => ( 110 | { 115 | onTokenHover(token); 116 | updateTokenInfo(token, heading); 117 | }} 118 | onSelect={onTokenSelect} 119 | heading={ 120 | isPaginationEnabled 121 | ? `${currentPage + 1}.${chunkIndex * CHUNK_SIZE + tokenIndex + 1}` 122 | : `${chunkIndex * CHUNK_SIZE + tokenIndex + 1}` 123 | } 124 | highlight={ 125 | (hoveredNote && token.note_id === hoveredNote.note_id) || 126 | (hoveredToken && hoveredToken.note_id !== null && token.note_id === hoveredToken.note_id) 127 | } 128 | selected={ 129 | (selectedNote && token.note_id === selectedNote.note_id) || 130 | (selectedToken && token.note_id === selectedToken.note_id) 131 | } 132 | /> 133 | ))} 134 |
135 | ))} 136 |
137 |
138 |
139 | {isPaginationEnabled && ( 140 |
141 |
142 | 149 | {currentPage + 1} of {totalPages} 150 | 157 |
158 |
159 | )} 160 |
161 | ); 162 | }; 163 | 164 | export default DataDisplay; 165 | -------------------------------------------------------------------------------- /docs/Development documentation/api/model.md: -------------------------------------------------------------------------------- 1 | # Model 2 | 3 | ### ConfigModel 4 | 5 | Configuration model that defines the parameters for MIDI tokenization and processing. 6 | 7 | ```python 8 | class ConfigModel(BaseModel): 9 | ``` 10 | 11 | #### Parameters 12 | 13 | | Parameter | Type | Description | 14 | |-----------|------|-------------| 15 | | `tokenizer` | `Literal` | The tokenization method to use. Options include "REMI", "REMIPlus", "MIDILike", "TSD", "Structured", "CPWord", "Octuple", "MuMIDI", "MMM", "PerTok". | 16 | | `pitch_range` | `list[int]` | A list containing the minimum and maximum MIDI pitch values to consider (0-127). | 17 | | `num_velocities` | `int` | The number of velocity levels to quantize to (0-127). | 18 | | `special_tokens` | `list[str]` | List of special tokens to include in the vocabulary. | 19 | | `use_chords` | `bool` | Whether to include chord tokens in the tokenization. | 20 | | `use_rests` | `bool` | Whether to include rest tokens in the tokenization. | 21 | | `use_tempos` | `bool` | Whether to include tempo tokens in the tokenization. | 22 | | `use_time_signatures` | `bool` | Whether to include time signature tokens in the tokenization. | 23 | | `use_sustain_pedals` | `bool` | Whether to include sustain pedal events in the tokenization. | 24 | | `use_pitch_bends` | `bool` | Whether to include pitch bend events in the tokenization. | 25 | | `num_tempos` | `NonNegativeInt` | The number of tempo levels to quantize to. | 26 | | `tempo_range` | `list[NonNegativeInt]` | A list containing the minimum and maximum tempo values in BPM. | 27 | | `log_tempos` | `bool` | Whether to use logarithmic scaling for tempo quantization. | 28 | | `delete_equal_successive_tempo_changes` | `bool` | Whether to remove consecutive identical tempo changes. | 29 | | `sustain_pedal_duration` | `bool` | Whether to include sustain pedal duration in tokenization. | 30 | | `pitch_bend_range` | `list[int]` | A list of three values defining the minimum, maximum, and step size for pitch bend. | 31 | | `delete_equal_successive_time_sig_changes` | `bool` | Whether to remove consecutive identical time signature changes. | 32 | | `use_programs` | `bool` | Whether to include program change events in the tokenization. | 33 | | `programs` | `Optional[list[int]]` | Optional range of MIDI program numbers to include. | 34 | | `one_token_stream_for_programs` | `Optional[bool]` | Whether to use a single token stream for all programs. | 35 | | `program_changes` | `Optional[bool]` | Whether to include program change events. | 36 | | `use_microtiming` | `bool` | Whether to include microtiming information. | 37 | | `ticks_per_quarter` | `int` | MIDI ticks per quarter note (24-960). | 38 | | `max_microtiming_shift` | `float` | Maximum microtiming shift as a fraction of a beat (0-1). | 39 | | `num_microtiming_bins` | `int` | Number of bins for quantizing microtiming (1-64). | 40 | 41 | 42 | ### MusicInformationData 43 | 44 | Model that contains extracted information and metrics from a MIDI file. 45 | 46 | ```python 47 | class MusicInformationData(BaseModel): 48 | ``` 49 | 50 | #### Parameters 51 | 52 | | Parameter | Type | Description | 53 | |-----------|------|-------------| 54 | | `title` | `str` | The title of the MIDI file. | 55 | | `resolution` | `PositiveInt` | The MIDI file resolution (ticks per quarter note). | 56 | | `tempos` | `list[tuple[NonNegativeInt, float]]` | List of tempo changes as (tick, tempo in BPM) pairs. | 57 | | `key_signatures` | `list[tuple[NonNegativeInt, int, str]]` | List of key signature changes as (tick, key, mode) triplets. | 58 | | `time_signatures` | `list[tuple[NonNegativeInt, int, int]]` | List of time signature changes as (tick, numerator, denominator) triplets. | 59 | | `pitch_range` | `NonNegativeInt` | The range between the highest and lowest pitch used in the MIDI file. | 60 | | `n_pitches_used` | `NonNegativeInt` | The number of unique pitches used in the MIDI file. | 61 | | `polyphony` | `NonNegativeFloat` | The average number of simultaneous notes played. | 62 | | `empty_beat_rate` | `NonNegativeFloat` | The ratio of beats with no notes to total beats. | 63 | | `drum_pattern_consistency` | `NonNegativeFloat` | A measure of how consistent drum patterns are throughout the MIDI file (0-1). | 64 | 65 | ### BasicInfoData 66 | 67 | Data class that contains basic information extracted from a MIDI file. 68 | 69 | ```python 70 | @dataclass 71 | class BasicInfoData: 72 | ``` 73 | 74 | #### Parameters 75 | 76 | | Parameter | Type | Description | 77 | |-----------|------|-------------| 78 | | `title` | `str` | The title of the MIDI file. | 79 | | `resolution` | `int` | The MIDI file resolution (ticks per quarter note). | 80 | | `tempos` | `list[tuple[int, float]]` | List of tempo changes as (tick, tempo in BPM) pairs. | 81 | | `key_signatures` | `list[tuple[int, int, str]]` | List of key signature changes as (tick, key, mode) triplets. | 82 | | `time_signatures` | `list[tuple[int, int, int]]` | List of time signature changes as (tick, numerator, denominator) triplets. | 83 | 84 | ### MetricsData 85 | 86 | Data class that contains calculated metrics from a MIDI file. 87 | 88 | ```python 89 | @dataclass 90 | class MetricsData: 91 | ``` 92 | 93 | #### Parameters 94 | 95 | | Parameter | Type | Description | 96 | |-----------|------|-------------| 97 | | `pitch_range` | `int` | The range between the highest and lowest pitch used in the MIDI file. | 98 | | `n_pitches_used` | `int` | The number of unique pitches used in the MIDI file. | 99 | | `polyphony` | `float` | The average number of simultaneous notes played. | 100 | | `empty_beat_rate` | `float` | The ratio of beats with no notes to total beats. | 101 | | `drum_pattern_consistency` | `float` | A measure of how consistent drum patterns are throughout the MIDI file (0-1). | 102 | 103 | ### Note 104 | 105 | Data class representing a single note in a MIDI file. 106 | 107 | ```python 108 | @dataclass 109 | class Note: 110 | ``` 111 | 112 | #### Parameters 113 | 114 | | Parameter | Type | Description | 115 | |-----------|------|-------------| 116 | | `pitch` | `int` | The MIDI pitch value of the note (0-127). | 117 | | `name` | `str` | The note name (e.g., "C4", "F#5"). | 118 | | `start` | `int` | The start time of the note in MIDI ticks. | 119 | | `end` | `int` | The end time of the note in MIDI ticks. | 120 | | `velocity` | `int` | The velocity (loudness) of the note (0-127). | 121 | 122 | --------------------------------------------------------------------------------