From 8da1e32c08103b6df4722b4be28b8f480378ca13 Mon Sep 17 00:00:00 2001 From: hiarc Date: Tue, 4 Jul 2023 07:07:27 +0900 Subject: [PATCH 1/9] =?UTF-8?q?=E3=82=BD=E3=83=BC=E3=82=B9=E3=82=B3?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=88=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SVGに関する不要なソースコメントを削除した - 鍵盤の縦スクロールを追従させる仕様を明記した --- src/typescript/component/piano-roll/piano-key.tsx | 6 ++---- src/typescript/component/piano-roll/piano-roll.tsx | 9 +++------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/typescript/component/piano-roll/piano-key.tsx b/src/typescript/component/piano-roll/piano-key.tsx index 6d1df6d..da883ce 100644 --- a/src/typescript/component/piano-roll/piano-key.tsx +++ b/src/typescript/component/piano-roll/piano-key.tsx @@ -38,15 +38,13 @@ const whiteKeyElements = () => { const rects = []; /* * 白鍵の始点となるY座標。それぞれの白鍵の高さを加えたものを、次の白鍵の始点にする。 - * 水平線と最初の白鍵の縦の軸を合わせるために、始点の初期値は負の値をとる。 - * ピアノロールの最上部の水平線は、最初の白鍵の途中から始まるため。 + * 水平線と最初の白鍵の縦の軸を合わせるために、ズレを補正する初期値を設定する。 */ var totalY = X_LINE_SPACING * 3 - WHITE_KEY_HEIGHT_F * 2; for(let i = 0; i <= WHITE_KEY_AMOUNT + 1; i++){ /* * 白鍵は [C, D, E] と [F, G, A, B] でピアノロール上の高さが異なる。そのため高さを分けて算出する。 - * ピアノロールの最上部はGであり、下側にG, F, E, D…と続く。 - * 起点0をGとして鍵盤の高さを算出する。 + * ピアノロールの最上部はGのため、起点0をGとして音程を判定する。 */ const height = [0, 1, 5, 6].some(note => i % 7 === note) ? WHITE_KEY_HEIGHT_F : WHITE_KEY_HEIGHT_C; const rect = diff --git a/src/typescript/component/piano-roll/piano-roll.tsx b/src/typescript/component/piano-roll/piano-roll.tsx index dd96bb3..721c83e 100644 --- a/src/typescript/component/piano-roll/piano-roll.tsx +++ b/src/typescript/component/piano-roll/piano-roll.tsx @@ -33,12 +33,13 @@ export const PianoRoll: React.FunctionComponent<{ addMessage: (message: NoteOnMessage) => void }> = (props) => { - const pianoRollElement = React.useRef(null); - + // 読み込み時に画面中央部までスクロールする。 + const pianoRollElement = React.useRef(null) React.useEffect(() => { pianoRollElement.current.scrollTop = PIANO_ROLLE_HEIGHT / 2; }, []); + // ピアノロールを縦スクロールしたら白鍵・黒鍵のスクロールを追従させる。 const onScroll = (e) => { const pianoKey = document.getElementById("piano-key"); if(!pianoKey){ @@ -122,10 +123,6 @@ export const PianoRoll: React.FunctionComponent<{ /** * 入力領域の音程を表す平行線のSVG要素。 * 音程ごと、1オクターブごとに色分けして区切る。 - * - * path の d は以下の構文を取り、SVG上で直線を表現する。 - * d="M {x1} {y1} L {x2} {y2}" - * 上記のように指定すると、起点(x1y1)から終点(x2y2)に線を引く。 */ const xLineElements = () => { const elements = []; From 3eaa1687465203a47def9f0f470d78453c7c10ed Mon Sep 17 00:00:00 2001 From: hiarc Date: Tue, 4 Jul 2023 07:11:06 +0900 Subject: [PATCH 2/9] =?UTF-8?q?=E3=83=88=E3=83=A9=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=83=A2=E3=83=87=E3=83=AB=E3=81=AE=E3=83=AA=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=AF=E3=82=BF=E3=81=A8=E6=A9=9F=E8=83=BD=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - アクセス性を上げるために、過剰なカプセル化をやめた - 要素番号を指定してトラックを取得するメソッドを追加した - システムトラックと演奏トラックのインスタンスを生成するメソッドを追加した --- src/typescript/domain/track.ts | 73 +++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/src/typescript/domain/track.ts b/src/typescript/domain/track.ts index 5376463..2ff0b20 100644 --- a/src/typescript/domain/track.ts +++ b/src/typescript/domain/track.ts @@ -1,44 +1,69 @@ +import { Message } from "./message"; + export default class Track { - private no: number; - private name: string; - private instrumentId: number; - private sequenseId: number; + no: number; + name: string; + instrumentId: number; + messages: Message[] = []; + + static systemTrack(): Track{ + const track = new Track(); + track.no = 0; + track.name = `System Track`; - constructor(no: number){ - this.no = no; - this.name = `track${no}`; + return track; } - public get getNo(): number{ - return this.no; + + static instrumentalTrack(no: number): Track{ + if(no === 0){ + throw new Error("Track No.0 is used as System Track."); + } + + const track = new Track(); + track.no = no; + track.name = `Track${no}`; + + return track; } - public get getName(): string{ - return this.name; + + addMessage(message: Message): void { + this.messages.push(message); } + } export class Tracks { - private tracks: Track[]; + tracks: Track[]; + constructor(tracks: Track[]){ this.tracks = tracks; } - public static empty(): Tracks { + + static empty(): Tracks { return new Tracks([]); } - public static default(): Tracks { - let tracks = this.empty(); + + static default(): Tracks { + const tracks = []; + for(let idx = 1; idx <= 16; idx++){ - tracks.add(tracks.size + 1); + const track = Track.instrumentalTrack(tracks.length + 1); + tracks.push(track); } - return tracks; + + return new Tracks(tracks); } - public add(no: number): void { - const track = new Track(no); + + add(no: number): void { + const track = Track.instrumentalTrack(no); this.tracks.push(track); } - public get asList(): Track[]{ - return this.tracks; - } - public get size(): number { - return this.tracks.length; + + select(no: number): Track { + if(no > this.tracks.length){ + throw new Error("OutOfRangeError."); + } + + return this.tracks[no]; } } \ No newline at end of file From 8cdad50de38ea6feddc663d4510f0f5c77400a19 Mon Sep 17 00:00:00 2001 From: hiarc Date: Tue, 4 Jul 2023 09:38:42 +0900 Subject: [PATCH 3/9] =?UTF-8?q?=E5=B1=9E=E6=80=A7=E5=90=8D=E3=81=AE?= =?UTF-8?q?=E5=A4=89=E6=9B=B4=E6=BC=8F=E3=82=8C=E3=81=AB=E5=AF=BE=E5=BF=9C?= =?UTF-8?q?=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/typescript/component/tracks/track-panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typescript/component/tracks/track-panel.tsx b/src/typescript/component/tracks/track-panel.tsx index 2e0a287..5a3612a 100644 --- a/src/typescript/component/tracks/track-panel.tsx +++ b/src/typescript/component/tracks/track-panel.tsx @@ -10,7 +10,7 @@ export const TrackPanel: React.FunctionComponent<{track: Track}> = (props) => {
- {props.track.getName} + {props.track.name}
From fefcde5e4b9188c22d4f4125233aefb4ca1fbe68 Mon Sep 17 00:00:00 2001 From: hiarc Date: Fri, 7 Jul 2023 22:46:35 +0900 Subject: [PATCH 4/9] =?UTF-8?q?=E3=83=AA=E3=82=AF=E3=82=A8=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=83=BB=E3=83=AC=E3=82=B9=E3=83=9D=E3=83=B3=E3=82=B9?= =?UTF-8?q?=E3=81=AB=E4=BD=BF=E3=81=86pydantic=E7=94=A8=E3=81=AE=E3=82=AF?= =?UTF-8?q?=E3=83=A9=E3=82=B9=E3=82=92Model=E3=82=AF=E3=83=A9=E3=82=B9?= =?UTF-8?q?=E3=81=A8=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/python/presentation/request_body.py | 19 +++++++++++++++---- src/python/presentation/routers.py | 8 +++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/python/presentation/request_body.py b/src/python/presentation/request_body.py index 4aaeac4..5c0c52f 100644 --- a/src/python/presentation/request_body.py +++ b/src/python/presentation/request_body.py @@ -3,7 +3,7 @@ from domain.message import NoteOnMessage -class NoteOnMessageRequest(BaseModel): +class NoteOnMessageModel(BaseModel): note_number: int started_at: int velocity: int @@ -27,7 +27,7 @@ def toDomain(self): @staticmethod def fromDomain(message: NoteOnMessage): - return NoteOnMessageRequest( + return NoteOnMessageModel( note_number=message.note_number, started_at=message.started_at, velocity=message.velocity, @@ -35,8 +35,19 @@ def fromDomain(message: NoteOnMessage): ) +class TrackModel(BaseModel): + no: int + name: str + instrumentId: int + messages: list[NoteOnMessageModel] + + class Config: + alias_generator = stringcase.camelcase + allow_population_by_field_name = True + + class PlayRequest(BaseModel): - messages: list[NoteOnMessageRequest] + messages: list[NoteOnMessageModel] port_name: str class Config: @@ -47,7 +58,7 @@ def toDomain(self): class SaveRequest(BaseModel): - messages: list[NoteOnMessageRequest] + messages: list[NoteOnMessageModel] filename: str class Config: diff --git a/src/python/presentation/routers.py b/src/python/presentation/routers.py index 4994157..e6aa503 100644 --- a/src/python/presentation/routers.py +++ b/src/python/presentation/routers.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Body, UploadFile from fastapi.responses import FileResponse -from presentation.request_body import SaveRequest, PlayRequest, NoteOnMessageRequest +from presentation.request_body import SaveRequest, PlayRequest, NoteOnMessageModel from domain.player import Player from domain.ports import Ports from domain.midi_file import MIDIFile @@ -23,12 +23,10 @@ async def save(body: SaveRequest = Body()): @router.post("/v1.0/upload") -async def openFile(file: UploadFile) -> list[NoteOnMessageRequest]: +async def openFile(file: UploadFile) -> list[NoteOnMessageModel]: repository.upload(file.file, file.filename) messages = MIDIFile.file_to_obj(file.filename) - # TODO: 曲のメタ情報(テンポ)やトラック情報を返す - # TODO: マルチトラックに対応する - return list(NoteOnMessageRequest.fromDomain(message) for message in messages) + return list(NoteOnMessageModel.fromDomain(message) for message in messages) @router.post("/v1.0/player") From 12a08117ca73188577cfc80d2bfde0deeb566cc2 Mon Sep 17 00:00:00 2001 From: hiarc Date: Fri, 7 Jul 2023 22:49:06 +0900 Subject: [PATCH 5/9] =?UTF-8?q?=EF=BC=88WIP=EF=BC=89=E3=83=9E=E3=83=AB?= =?UTF-8?q?=E3=83=81=E3=83=88=E3=83=A9=E3=83=83=E3=82=AF=E3=81=AB=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C=E3=81=99=E3=82=8B=E3=81=9F=E3=82=81=E3=81=AE=E6=BA=96?= =?UTF-8?q?=E5=82=99=E3=81=A8=E3=81=97=E3=81=A6=E3=80=81=E3=83=95=E3=83=AD?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=81=A7=E7=AE=A1=E7=90=86=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=83=8E=E3=83=BC=E3=83=88=E3=82=AA=E3=83=B3=E3=83=A1=E3=83=83?= =?UTF-8?q?=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92Track=E3=82=AF=E3=83=A9?= =?UTF-8?q?=E3=82=B9=E3=81=AB=E5=86=85=E5=8C=85=E3=81=95=E3=81=9B=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 9 ++++ package.json | 1 + src/typescript/component/header/header.tsx | 30 +++++++------- .../component/piano-roll/piano-roll.tsx | 11 ++--- .../component/tracks/track-panels.tsx | 2 +- src/typescript/domain/track.ts | 7 +++- src/typescript/layout.tsx | 41 +++++++++++-------- src/typescript/repository/repository.ts | 5 ++- 8 files changed, 65 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index ff62423..fc79d3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "bootstrap": "^5.3.0", "bootstrap-icons": "^1.10.5", "clean-webpack-plugin": "^4.0.0", + "clone": "^2.1.2", "html-webpack-plugin": "5.5.0", "react": "18.2.0", "react-bootstrap": "^2.7.4", @@ -2906,6 +2907,14 @@ "webpack": ">=4.0.0 <6.0.0" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", diff --git a/package.json b/package.json index be17956..4a5c768 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "bootstrap": "^5.3.0", "bootstrap-icons": "^1.10.5", "clean-webpack-plugin": "^4.0.0", + "clone": "^2.1.2", "html-webpack-plugin": "5.5.0", "react": "18.2.0", "react-bootstrap": "^2.7.4", diff --git a/src/typescript/component/header/header.tsx b/src/typescript/component/header/header.tsx index 6062f1c..4685957 100644 --- a/src/typescript/component/header/header.tsx +++ b/src/typescript/component/header/header.tsx @@ -3,29 +3,29 @@ import Container from 'react-bootstrap/Container'; import Nav from 'react-bootstrap/Nav'; import Navbar from 'react-bootstrap/Navbar'; import NavDropdown from 'react-bootstrap/NavDropdown'; -import NoteOnMessage from '../../domain/message'; +import { nowDateTime } from '../../common/date-utils'; import { play, saveAndDownload, selectFile, uploadFile } from '../../repository/repository'; import { SettingsModal } from './settings-modal'; -import { nowDateTime } from '../../common/date-utils'; +import { Tracks } from '../../domain/track'; export const Header: React.FunctionComponent<{ + /** 現在選択しているMIDI出力ポート。 */ + port: string, + /** 現在開いているMIDIファイル。 */ file: File, - /** シーケンサーで入力したノートオンメッセージのリスト。 */ - messages: NoteOnMessage[], - - /** シーケンサーに設定済みのMIDI出力ポート。 */ - port: string, + /** トラックのリスト。 */ + tracks: Tracks, - /** 指定したファイルを、現在開いているMIDIファイルとする。 */ + /** MIDIファイルを設定する。 */ setFile: (file: File) => void, - /** シーケンサーのMIDI出力ポートを設定する。 */ + /** MIDI出力ポートを設定する。 */ setPort: (port: string) => void, - /** ノートオンメッセージをシーケンサーに設定する。 */ - setMessage: (messages: NoteOnMessage[]) => void + /** トラックを設定する。 */ + setTracks: (tracks: Tracks) => void }> = (props) => { @@ -34,14 +34,14 @@ export const Header: React.FunctionComponent<{ const openFile = async () => { const file = await selectFile(); - const messages = await uploadFile(file); + const tracks = await uploadFile(file); props.setFile(file); - props.setMessage(messages) + props.setTracks(tracks) } const saveAndDownloadFile = () => { const filename = `new_file_${nowDateTime()}.mid`; - saveAndDownload(props.messages, filename); + // saveAndDownload(props.tracks, filename); } return ( @@ -68,7 +68,7 @@ export const Header: React.FunctionComponent<{ - play(props.messages, props.port)}>Play + play(props.port, props.tracks)}>Play Stop Jump to diff --git a/src/typescript/component/piano-roll/piano-roll.tsx b/src/typescript/component/piano-roll/piano-roll.tsx index 721c83e..d11280e 100644 --- a/src/typescript/component/piano-roll/piano-roll.tsx +++ b/src/typescript/component/piano-roll/piano-roll.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import NoteOnMessage from '../../domain/message'; +import NoteOnMessage, { Message } from '../../domain/message'; /** 音程の最大値 */ export const MAX_NOTE_NUMBER = 127; @@ -29,7 +29,7 @@ const Y_MEASURE_LINE_COLOR = X_OCTAVE_LINE_COLOR export const PianoRoll: React.FunctionComponent<{ - messages: NoteOnMessage[], + messages: Message[], addMessage: (message: NoteOnMessage) => void }> = (props) => { @@ -56,15 +56,16 @@ export const PianoRoll: React.FunctionComponent<{ * width は長方形の横幅、height は長方形の縦幅、x 及び y は長方形の左上の開始位置を表す。 */ const messageRects = props.messages.map(message => { - const width = widthFromTick(message.tick); + const noteOn = message as NoteOnMessage; + const width = widthFromTick(noteOn.tick); const height = X_LINE_SPACING; - const x = widthFromTick(message.startedAt); + const x = widthFromTick(noteOn.startedAt); // x座標+発声の長さが画面右端に到達したら、水平スクロールを1小説分足す if(pianoRollWidth < x + Y_LINE_SPACING * 8){ pianoRollWidth = x + Y_LINE_SPACING * 8; } // yは画面最上部を0にとるが、ノートナンバー(音程)は画面最下部を0とするため最大値を基準にして逆転させる - const y = (MAX_NOTE_NUMBER - message.noteNumber) * X_LINE_SPACING; + const y = (MAX_NOTE_NUMBER - noteOn.noteNumber) * X_LINE_SPACING; return }); diff --git a/src/typescript/component/tracks/track-panels.tsx b/src/typescript/component/tracks/track-panels.tsx index d0844d8..a7341f8 100644 --- a/src/typescript/component/tracks/track-panels.tsx +++ b/src/typescript/component/tracks/track-panels.tsx @@ -6,7 +6,7 @@ import { TrackPanel } from './track-panel'; export const TrackPanels: React.FunctionComponent<{tracks: Tracks}> = (props) => { return ( - {props.tracks.asList.map(track => )} + {props.tracks.tracks.map(track => )} ); } \ No newline at end of file diff --git a/src/typescript/domain/track.ts b/src/typescript/domain/track.ts index 2ff0b20..9a55b2d 100644 --- a/src/typescript/domain/track.ts +++ b/src/typescript/domain/track.ts @@ -59,11 +59,16 @@ export class Tracks { this.tracks.push(track); } - select(no: number): Track { + get(no: number): Track { if(no > this.tracks.length){ throw new Error("OutOfRangeError."); } return this.tracks[no]; } + + addMessage(idx: number, message: Message): void { + const track = this.get(idx); + track.addMessage(message); + } } \ No newline at end of file diff --git a/src/typescript/layout.tsx b/src/typescript/layout.tsx index 112342c..f0f3ede 100644 --- a/src/typescript/layout.tsx +++ b/src/typescript/layout.tsx @@ -1,45 +1,52 @@ import * as React from 'react'; +import { Container } from 'react-bootstrap'; +import clone from "clone"; import { Header } from './component/header/header'; -import { Tracks } from './domain/track'; import { TrackPanels } from './component/tracks/track-panels'; -import { Container } from 'react-bootstrap'; -import { PIANO_ROLLE_HEIGHT, PianoRoll } from './component/piano-roll/piano-roll'; -import NoteOnMessage from './domain/message'; import { PianoKey } from './component/piano-roll/piano-key'; +import { PianoRoll } from './component/piano-roll/piano-roll'; +import NoteOnMessage from './domain/message'; +import { Tracks } from './domain/track'; export const Layout: React.FunctionComponent<{}> = (props) => { + /** 現在選択しているMIDI出力ポート。 */ + const [port, setPort] = React.useState(''); + /** 現在開いているMIDIファイル。 */ const [file, setFile] = React.useState(); - /** シーケンサーで入力したノートオンメッセージのリスト。 */ - const [messages, setMessages] = React.useState([]); - - /** シーケンサーに設定済みのMIDI出力ポート。 */ - const [port, setPort] = React.useState(''); + /** 現在選択しているトラックの番号。 */ + const [trackIdx, setTrackIdx] = React.useState(1); - /** シーケンサーで設定したトラックのリスト。 */ - const tracks: Tracks = Tracks.default(); + /** トラックのリスト。 */ + const [tracks, setTracks]= React.useState(Tracks.default()); - /** ノートオンメッセージのリストに要素を追加する。 */ + /** 現在選択しているトラックにノートオンメッセージを追加する。 */ const addMessage = (message: NoteOnMessage) => { - setMessages(messages.concat(message)); + const newTracks: Tracks = clone(tracks); + newTracks.addMessage(trackIdx, message); + setTracks(newTracks); + } + + const selectTrack = (targetIdx) => { + setTrackIdx(targetIdx); } return (
setFile(file)} setPort={(port: string) => setPort(port)} - setMessage={(messages: []) => setMessages(messages)} + setTracks={(tracks: Tracks) => setTracks(tracks)} />
addMessage(message)} />
diff --git a/src/typescript/repository/repository.ts b/src/typescript/repository/repository.ts index f7c3ac0..b5e4b92 100644 --- a/src/typescript/repository/repository.ts +++ b/src/typescript/repository/repository.ts @@ -1,5 +1,6 @@ import axios, { ResponseType } from "axios"; import NoteOnMessage from "../domain/message"; +import { Tracks } from "../domain/track"; // TODO: FQDNを共通化する @@ -22,8 +23,8 @@ export const saveAndDownload = (messages: NoteOnMessage[], filename: string) => }) } -export const play = (messages: NoteOnMessage[], portName: string) => { - const data = {messages: messages, portName: portName}; +export const play = (portName: string, tracks: Tracks) => { + const data = {portName: portName, tracks: tracks}; axios.post('http://localhost:8000/v1.0/player', data) .then((response) => console.log(response)) .catch((error) => console.log(error)); From fba175e3927d056d79a98e5c7846d58eba3896fb Mon Sep 17 00:00:00 2001 From: hiarc Date: Fri, 7 Jul 2023 22:59:01 +0900 Subject: [PATCH 6/9] =?UTF-8?q?=E6=96=B0=E8=A6=8F=E4=BD=9C=E6=88=90?= =?UTF-8?q?=E3=81=AE=E5=A0=B4=E5=90=88=E3=81=AF=E3=80=811=E3=83=88?= =?UTF-8?q?=E3=83=A9=E3=83=83=E3=82=AF=E7=9B=AE=E3=82=92=E3=83=86=E3=83=B3?= =?UTF-8?q?=E3=83=9D=E3=81=AA=E3=81=A9=E3=82=92=E8=A8=AD=E5=AE=9A=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=B3=E3=83=B3=E3=83=80=E3=82=AF=E3=82=BF=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=83=A9=E3=83=83=E3=82=AF=E3=81=A8=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/typescript/domain/track.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/typescript/domain/track.ts b/src/typescript/domain/track.ts index 9a55b2d..7c8e7cd 100644 --- a/src/typescript/domain/track.ts +++ b/src/typescript/domain/track.ts @@ -6,17 +6,17 @@ export default class Track { instrumentId: number; messages: Message[] = []; - static systemTrack(): Track{ + static conductorTrack(): Track{ const track = new Track(); track.no = 0; - track.name = `System Track`; + track.name = `Conductor Track`; return track; } static instrumentalTrack(no: number): Track{ if(no === 0){ - throw new Error("Track No.0 is used as System Track."); + throw new Error("Track No.0 is used as Conductor Track."); } const track = new Track(); @@ -46,8 +46,9 @@ export class Tracks { static default(): Tracks { const tracks = []; + tracks.push(Track.conductorTrack()); for(let idx = 1; idx <= 16; idx++){ - const track = Track.instrumentalTrack(tracks.length + 1); + const track = Track.instrumentalTrack(idx); tracks.push(track); } From c8036141c6a594b832f2cae7909b91c5ff570b92 Mon Sep 17 00:00:00 2001 From: hiarc Date: Fri, 7 Jul 2023 23:21:14 +0900 Subject: [PATCH 7/9] =?UTF-8?q?=E3=82=B5=E3=83=BC=E3=83=90=E3=83=BC?= =?UTF-8?q?=E5=81=B4=E3=81=AE=E5=86=8D=E7=94=9F=E7=94=A8=E3=81=AE=E3=82=A8?= =?UTF-8?q?=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88=E3=81=A7?= =?UTF-8?q?=E8=A4=87=E6=95=B0=E3=81=AE=E3=83=88=E3=83=A9=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=82=92=E5=8F=97=E3=81=91=E5=8F=96=E3=82=8C=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/python/domain/track.py | 10 ++++++++++ src/python/presentation/request_body.py | 14 ++++++++++++-- src/typescript/repository/repository.ts | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/python/domain/track.py b/src/python/domain/track.py index da6d659..ab165e3 100644 --- a/src/python/domain/track.py +++ b/src/python/domain/track.py @@ -3,6 +3,16 @@ from domain.parser import Parser +class Track: + def __init__( + self, no: int, name: str, instrumentId: int, messages: list[NoteOnMessage] + ) -> None: + self.no = no + self.name = name + self.instrumentId = instrumentId + self.messages = messages + + class MidoTrackHelper: @staticmethod def mido_system_track(): diff --git a/src/python/presentation/request_body.py b/src/python/presentation/request_body.py index 5c0c52f..a1b20b1 100644 --- a/src/python/presentation/request_body.py +++ b/src/python/presentation/request_body.py @@ -1,6 +1,7 @@ from pydantic import BaseModel import stringcase from domain.message import NoteOnMessage +from domain.track import Track class NoteOnMessageModel(BaseModel): @@ -45,16 +46,25 @@ class Config: alias_generator = stringcase.camelcase allow_population_by_field_name = True + def toDomain(self): + messages = list(message.toDomain() for message in self.messages) + return Track( + self.no, + self.name, + self.instrumentId, + messages, + ) + class PlayRequest(BaseModel): - messages: list[NoteOnMessageModel] + tracks: list[TrackModel] port_name: str class Config: alias_generator = stringcase.camelcase def toDomain(self): - return list(message.toDomain() for message in self.messages) + return list(track.toDomain() for track in self.tracks) class SaveRequest(BaseModel): diff --git a/src/typescript/repository/repository.ts b/src/typescript/repository/repository.ts index b5e4b92..e69b10d 100644 --- a/src/typescript/repository/repository.ts +++ b/src/typescript/repository/repository.ts @@ -24,7 +24,7 @@ export const saveAndDownload = (messages: NoteOnMessage[], filename: string) => } export const play = (portName: string, tracks: Tracks) => { - const data = {portName: portName, tracks: tracks}; + const data = {tracks: tracks, portName: portName}; axios.post('http://localhost:8000/v1.0/player', data) .then((response) => console.log(response)) .catch((error) => console.log(error)); From 4a71da450c02d5825777518a010654b0062ffb05 Mon Sep 17 00:00:00 2001 From: hiarc Date: Sun, 9 Jul 2023 21:59:47 +0900 Subject: [PATCH 8/9] =?UTF-8?q?=E3=83=95=E3=83=AD=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=8B=E3=82=89=E5=8F=97=E4=BF=A1=E3=81=97=E3=81=9F=E3=83=9E?= =?UTF-8?q?=E3=83=AB=E3=83=81=E3=83=88=E3=83=A9=E3=83=83=E3=82=AF=E3=81=AE?= =?UTF-8?q?=E3=83=87=E3=83=BC=E3=82=BF=E3=82=92=E4=BD=BF=E3=81=A3=E3=81=A6?= =?UTF-8?q?MID=20I=E3=82=92=E5=86=8D=E7=94=9F=E3=81=A7=E3=81=8D=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/python/domain/midi_file.py | 13 ++++++------- src/python/domain/player.py | 7 ++++--- src/python/domain/track.py | 12 +++--------- src/typescript/domain/track.ts | 2 +- src/typescript/repository/repository.ts | 2 +- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/python/domain/midi_file.py b/src/python/domain/midi_file.py index a4bc3d9..f8968a4 100644 --- a/src/python/domain/midi_file.py +++ b/src/python/domain/midi_file.py @@ -1,17 +1,16 @@ from mido import MidiFile, MidiTrack from domain.message import NoteOnMessage from domain.track import MidoTrackHelper +from domain.track import Track class MIDIFile: - def __init__(self, messages: list[NoteOnMessage]): - # TODO: マルチトラック化する - sys_track: MidiTrack = MidoTrackHelper.mido_system_track() - track0: MidiTrack = MidoTrackHelper.mido_instrument_track("track0", messages) - + def __init__(self, tracks: list[Track]): self.midi: MidiFile = MidiFile(type=1) - self.midi.tracks.append(sys_track) - self.midi.tracks.append(track0) + + for track in tracks: + mido_track: MidiTrack = MidoTrackHelper.mido_instrument_track(track) + self.midi.tracks.append(mido_track) @staticmethod def file_to_obj(path: str): diff --git a/src/python/domain/player.py b/src/python/domain/player.py index 63f2283..610115c 100644 --- a/src/python/domain/player.py +++ b/src/python/domain/player.py @@ -2,14 +2,15 @@ from domain.ports import Ports from domain.message import NoteOnMessage from domain.midi_file import MIDIFile +from domain.track import Track class Player: - def __init__(self, messages: list[NoteOnMessage]): - self.messages: list[NoteOnMessage] = messages + def __init__(self, tracks: list[Track]): + self.tracks: list[Track] = tracks def play(self, output_port_name: str): - midi: MidiFile = MIDIFile(self.messages).midi + midi: MidiFile = MIDIFile(self.tracks).midi port = Ports.open_output_port(output_port_name) for message in midi.play(): diff --git a/src/python/domain/track.py b/src/python/domain/track.py index ab165e3..cdcd1f9 100644 --- a/src/python/domain/track.py +++ b/src/python/domain/track.py @@ -15,16 +15,10 @@ def __init__( class MidoTrackHelper: @staticmethod - def mido_system_track(): - sys_track = MidiTrack() - sys_track.name = "System Track" - return sys_track - - @staticmethod - def mido_instrument_track(name: str, messages: list[NoteOnMessage]): + def mido_instrument_track(track: Track): mido_track = MidiTrack() - mido_track.name = name - for message in Parser.to_mido_messages(messages): + mido_track.name = track.name + for message in Parser.to_mido_messages(track.messages): mido_track.append(message) return mido_track diff --git a/src/typescript/domain/track.ts b/src/typescript/domain/track.ts index 7c8e7cd..606a71a 100644 --- a/src/typescript/domain/track.ts +++ b/src/typescript/domain/track.ts @@ -3,7 +3,7 @@ import { Message } from "./message"; export default class Track { no: number; name: string; - instrumentId: number; + instrumentId: number = 0; // TODO: GUIから選択できるようにする。 messages: Message[] = []; static conductorTrack(): Track{ diff --git a/src/typescript/repository/repository.ts b/src/typescript/repository/repository.ts index e69b10d..5de4293 100644 --- a/src/typescript/repository/repository.ts +++ b/src/typescript/repository/repository.ts @@ -24,7 +24,7 @@ export const saveAndDownload = (messages: NoteOnMessage[], filename: string) => } export const play = (portName: string, tracks: Tracks) => { - const data = {tracks: tracks, portName: portName}; + const data = {tracks: tracks.tracks, portName: portName}; axios.post('http://localhost:8000/v1.0/player', data) .then((response) => console.log(response)) .catch((error) => console.log(error)); From e95c98f2a6f888c20bcbb9f380c44fdaccdb5852 Mon Sep 17 00:00:00 2001 From: hiarc Date: Wed, 12 Jul 2023 00:26:28 +0900 Subject: [PATCH 9/9] =?UTF-8?q?MIDI=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=AB=E5=90=AB=E3=81=BE=E3=82=8C=E3=82=8B=E5=85=A8=E3=81=A6?= =?UTF-8?q?=E3=81=AE=E3=83=88=E3=83=A9=E3=83=83=E3=82=AF=E3=81=8B=E3=82=89?= =?UTF-8?q?=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92=E5=8F=96?= =?UTF-8?q?=E5=BE=97=E3=81=97=E3=80=81=E3=83=95=E3=83=AD=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=83=89=E3=81=AB=E8=BF=94=E3=81=99=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/css/sequenser.css | 1 + src/python/domain/midi_file.py | 12 ++++++------ src/python/domain/track.py | 10 ++++++---- src/python/presentation/request_body.py | 16 ++++++++++++++-- src/python/presentation/routers.py | 13 +++++++++---- src/typescript/component/header/header.tsx | 2 +- .../component/tracks/track-panels.tsx | 2 +- src/typescript/domain/track.ts | 17 ++++++++++------- src/typescript/layout.tsx | 2 +- src/typescript/repository/repository.ts | 14 ++++++++++---- 10 files changed, 59 insertions(+), 30 deletions(-) diff --git a/src/css/sequenser.css b/src/css/sequenser.css index d35a220..5c34544 100644 --- a/src/css/sequenser.css +++ b/src/css/sequenser.css @@ -8,6 +8,7 @@ .main-track-panels{ width: 20%; + background-color: var(--bs-list-group-bg); } .main-piano-key{ diff --git a/src/python/domain/midi_file.py b/src/python/domain/midi_file.py index f8968a4..e8a19bf 100644 --- a/src/python/domain/midi_file.py +++ b/src/python/domain/midi_file.py @@ -1,5 +1,4 @@ from mido import MidiFile, MidiTrack -from domain.message import NoteOnMessage from domain.track import MidoTrackHelper from domain.track import Track @@ -13,9 +12,10 @@ def __init__(self, tracks: list[Track]): self.midi.tracks.append(mido_track) @staticmethod - def file_to_obj(path: str): + def file_to_sequencer_model(path: str): midi = MidiFile(path) - # TODO: マルチトラックに対応する - # for track in midi.tracks: - # messages = Track.to_messages(track) - return MidoTrackHelper.to_sequencer_messages(midi.tracks[0]) + tracks = list( + MidoTrackHelper.to_sequencer_track(idx, track) + for idx, track in enumerate(midi.tracks) + ) + return tracks diff --git a/src/python/domain/track.py b/src/python/domain/track.py index cdcd1f9..1f0ce69 100644 --- a/src/python/domain/track.py +++ b/src/python/domain/track.py @@ -5,11 +5,11 @@ class Track: def __init__( - self, no: int, name: str, instrumentId: int, messages: list[NoteOnMessage] + self, no: int, name: str, instrument_id: int, messages: list[NoteOnMessage] ) -> None: self.no = no self.name = name - self.instrumentId = instrumentId + self.instrument_id = instrument_id self.messages = messages @@ -23,5 +23,7 @@ def mido_instrument_track(track: Track): return mido_track @staticmethod - def to_sequencer_messages(mido_track: MidiTrack[Message]): - return Parser.to_sequencer_messages(mido_track) + def to_sequencer_track(no: int, mido_track: MidiTrack[Message]): + messages = Parser.to_sequencer_messages(mido_track) + # TODO: メッセージからinstrument_id を取得できるようにする + return Track(no, mido_track.name, 0, messages) diff --git a/src/python/presentation/request_body.py b/src/python/presentation/request_body.py index a1b20b1..67d5dfd 100644 --- a/src/python/presentation/request_body.py +++ b/src/python/presentation/request_body.py @@ -39,7 +39,7 @@ def fromDomain(message: NoteOnMessage): class TrackModel(BaseModel): no: int name: str - instrumentId: int + instrument_id: int messages: list[NoteOnMessageModel] class Config: @@ -51,10 +51,22 @@ def toDomain(self): return Track( self.no, self.name, - self.instrumentId, + self.instrument_id, messages, ) + @staticmethod + def fromDomain(track: Track): + messages = list( + NoteOnMessageModel.fromDomain(message) for message in track.messages + ) + return TrackModel( + no=track.no, + name=track.name, + instrument_id=track.instrument_id, + messages=messages, + ) + class PlayRequest(BaseModel): tracks: list[TrackModel] diff --git a/src/python/presentation/routers.py b/src/python/presentation/routers.py index e6aa503..46d4901 100644 --- a/src/python/presentation/routers.py +++ b/src/python/presentation/routers.py @@ -1,6 +1,11 @@ from fastapi import APIRouter, Body, UploadFile from fastapi.responses import FileResponse -from presentation.request_body import SaveRequest, PlayRequest, NoteOnMessageModel +from presentation.request_body import ( + SaveRequest, + PlayRequest, + TrackModel, + NoteOnMessageModel, +) from domain.player import Player from domain.ports import Ports from domain.midi_file import MIDIFile @@ -23,10 +28,10 @@ async def save(body: SaveRequest = Body()): @router.post("/v1.0/upload") -async def openFile(file: UploadFile) -> list[NoteOnMessageModel]: +async def openFile(file: UploadFile) -> list[TrackModel]: repository.upload(file.file, file.filename) - messages = MIDIFile.file_to_obj(file.filename) - return list(NoteOnMessageModel.fromDomain(message) for message in messages) + tracks = MIDIFile.file_to_sequencer_model(file.filename) + return list(TrackModel.fromDomain(track) for track in tracks) @router.post("/v1.0/player") diff --git a/src/typescript/component/header/header.tsx b/src/typescript/component/header/header.tsx index 4685957..b0a82de 100644 --- a/src/typescript/component/header/header.tsx +++ b/src/typescript/component/header/header.tsx @@ -36,7 +36,7 @@ export const Header: React.FunctionComponent<{ const file = await selectFile(); const tracks = await uploadFile(file); props.setFile(file); - props.setTracks(tracks) + props.setTracks(tracks); } const saveAndDownloadFile = () => { diff --git a/src/typescript/component/tracks/track-panels.tsx b/src/typescript/component/tracks/track-panels.tsx index a7341f8..bea82ad 100644 --- a/src/typescript/component/tracks/track-panels.tsx +++ b/src/typescript/component/tracks/track-panels.tsx @@ -6,7 +6,7 @@ import { TrackPanel } from './track-panel'; export const TrackPanels: React.FunctionComponent<{tracks: Tracks}> = (props) => { return ( - {props.tracks.tracks.map(track => )} + {props.tracks.tracks.map(track => )} ); } \ No newline at end of file diff --git a/src/typescript/domain/track.ts b/src/typescript/domain/track.ts index 606a71a..3556f9b 100644 --- a/src/typescript/domain/track.ts +++ b/src/typescript/domain/track.ts @@ -6,10 +6,9 @@ export default class Track { instrumentId: number = 0; // TODO: GUIから選択できるようにする。 messages: Message[] = []; + // TODO: conductorTrack(), instrumentalTrack() の用途が限定的なため、Tracks.default() に処理を移動する static conductorTrack(): Track{ - const track = new Track(); - track.no = 0; - track.name = `Conductor Track`; + const track = new Track(0, `Conductor Track`, 0, []); return track; } @@ -19,13 +18,17 @@ export default class Track { throw new Error("Track No.0 is used as Conductor Track."); } - const track = new Track(); - track.no = no; - track.name = `Track${no}`; - + const track = new Track(no, `Track${no}`, 0, []); return track; } + constructor(no: number, name: string, instrumentId: number, messages: Message[]){ + this.no = no; + this.name = name; + this.instrumentId = instrumentId; + this.messages = messages; + } + addMessage(message: Message): void { this.messages.push(message); } diff --git a/src/typescript/layout.tsx b/src/typescript/layout.tsx index f0f3ede..2589844 100644 --- a/src/typescript/layout.tsx +++ b/src/typescript/layout.tsx @@ -16,7 +16,7 @@ export const Layout: React.FunctionComponent<{}> = (props) => { const [file, setFile] = React.useState(); /** 現在選択しているトラックの番号。 */ - const [trackIdx, setTrackIdx] = React.useState(1); + const [trackIdx, setTrackIdx] = React.useState(0); /** トラックのリスト。 */ const [tracks, setTracks]= React.useState(Tracks.default()); diff --git a/src/typescript/repository/repository.ts b/src/typescript/repository/repository.ts index 5de4293..32c4c67 100644 --- a/src/typescript/repository/repository.ts +++ b/src/typescript/repository/repository.ts @@ -1,6 +1,6 @@ import axios, { ResponseType } from "axios"; import NoteOnMessage from "../domain/message"; -import { Tracks } from "../domain/track"; +import Track, { Tracks } from "../domain/track"; // TODO: FQDNを共通化する @@ -40,10 +40,16 @@ export const uploadFile = async (file: File) => { formData.set("file", blob, file.name); const response = await axios.post('http://localhost:8000/v1.0/upload', formData); - return response.data.map( - message => new NoteOnMessage( - message.noteNumber, message.startedAt, message.velocity, message.tick) + // TODO: TS側のオブジェクトに変換する処理のリファクタ + const tracks = response.data.map( + track => { + const messages = track.messages.map(message => + new NoteOnMessage(message.noteNumber, message.startedAt, message.velocity, message.tick)); + + return new Track(track.no, track.name, track.instrumentId, messages) + } ); + return new Tracks(tracks); } export const selectFile = () => {