先人達が様々な音感トレーニングアプリを作成してくれているが、私が求めているトレーニング内容のものが見つからない。
音楽アプリの開発について調べたところTone.jsというライブラリが便利そうなので、TypeScript/Reactの勉強がてら自作してみることにした。音楽、プログラミングのトレーニングが出来て一石二鳥。
一旦メジャー・マイナーダイアトニックコードの表示・再生までは実装できた。
時間があるときにクイズ形式の機能も作成予定。
実際のアプリは下記URL。
keyと、メジャーかマイナーかを選択するとダイアトニックコード一覧が表示され、再生もできる。音の範囲は1オクターブのみにした。
結構アレなデザインだが、この辺りも今後勉強していければと思う。
https://haaan1234.github.io/DChordApp/
使ったもの
・VScode
・TypeScript
・Node.js https://github.com/nodejs/node/blob/main/LICENSE
・React Copyright (c) Meta Platforms, Inc. and affiliates.
・Tone.js Copyright (c) 2014-2020 Yotam Mann
・MUI Copyright © 2024 Material-UI.
前提
create react-app ~ --template typescript
などでプロジェクト作成まで済んでいること
各種インストール
MUI
yarn add @mui/material @emotion/react @emotion/styled
Tone.js
yarn add tone
実装
初めてTypecript/Reactに触れたが、親コンポーネント・子コンポーネント間のやり取りのイメージを掴むまで苦労した。。パッと作れるようにはなるまでは時間がかかりそう。
以下、まずは主要なソース全文。クリックで開けます。
App.tsx(親) 画面表示などの主要な処理実施
import { useState } from "react"
import { MakeDChords } from "./logic"
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import React from "react";
import { FormControl, InputLabel, MenuItem, Select, SelectChangeEvent } from "@mui/material";
export default function App() {
import { useState } from "react"
import { MakeDChords } from "./logic"
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import React from "react";
import { FormControl, InputLabel, MenuItem, Select, SelectChangeEvent } from "@mui/material";
export default function App() {
//key選択状況を保持
const [key, setKey] = React.useState(0);
//keyが選択されたときに処理実行
const keyChange = (event: SelectChangeEvent) => {
setKey(Number(event.target.value as string));
};
//Maj/min選択状況を保持
const [Mm, setMm] = React.useState(0);
//Maj/minが選択されたときに処理実行
const MmChange = (event: SelectChangeEvent) => {
setMm(Number(event.target.value as string));
};
return (
<Grid container
direction="row"
alignContent="center"
justifyContent="center"
sx={{ minHeight: "50vh" }}
> <Grid item xs={0}>
<FormControl fullWidth>
<InputLabel id="key">key</InputLabel>
<Select
labelId="key"
id="key"
label="key"
onChange={keyChange}
>
<MenuItem value={0}>C</MenuItem>
<MenuItem value={1}>D♭</MenuItem>
<MenuItem value={2}>D</MenuItem>
<MenuItem value={3}>E♭</MenuItem>
<MenuItem value={4}>E</MenuItem>
<MenuItem value={5}>F</MenuItem>
<MenuItem value={6}>G♭</MenuItem>
<MenuItem value={7}>G</MenuItem>
<MenuItem value={8}>A♭</MenuItem>
<MenuItem value={9}>A</MenuItem>
<MenuItem value={10}>B♭</MenuItem>
<MenuItem value={11}>B</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={0}>
<FormControl>
<InputLabel id="Mm">maj/min</InputLabel>
<Select
labelId="Mm"
id="Mm"
label="Mm"
onChange={MmChange} >
<MenuItem value={0}>maj</MenuItem>
<MenuItem value={1}>min</MenuItem>
</Select>
</FormControl>
<MakeDChords dkey={key} Mm={Mm} />
</Grid>
<Grid item xs={12}>
</Grid>
</Grid>
)
}
logic.tsx(子) 音階などを保持し、ダイアトニックコードを作成する。App.tsxに結果を返す
import styled from "@emotion/styled";
import Button from "@mui/material/Button";
import ButtonGroup from "@mui/material/ButtonGroup";
import React, { MouseEventHandler } from "react"
import * as Tone from 'tone'
//鍵盤音(1オクターブのみ)
const tone = ["C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4"];
//メジャーダイアトニックコード
const MdChords = [
["C", "Dm", "Em", "F", "G", "Am", "Bm(♭5)"],
["D♭", "E♭m", "Fm", "G♭", "A♭", "B♭m", "Cm(♭5)"],
["D", "Em", "F#m", "G", "A", "Bm", "C#m(♭5)"],
["E♭", "Fm", "Gm", "A♭", "B♭", "Cm", "Dm(♭5)"],
["E", "F#m", "G#m", "A", "B", "C#m", "D#m(♭5)"],
["F", "Gm", "Am", "B♭", "C", "Dm", "Em(♭5)"],
["G♭", "A♭m", "B♭m", "C♭", "D♭", "E♭m", "Fm(♭5)"],
["G", "Am", "Bm", "C", "D", "Em", "F#m(♭5)"],
["A♭", "B♭m", "Cm", "D♭", "E♭", "Fm", "Gm(♭5)"],
["A", "Bm", "C#m", "D", "E", "F#m", "G#m(♭5)"],
["B♭", "Cm", "Dm", "E♭", "F", "Gm", "Am(♭5)"],
["B", "C#m", "D#m", "E", "F#", "G#m", "A#m(♭5)"]
]
//マイナーダイアトニックコード
const mDChords = [
["Cm", "Dm♭5", "E♭", "Fm", "Gm", "G#", "B♭"],
["D♭m", "E♭m♭5", "E", "F#m", "G#m", "A", "B"],
["Dm", "Em♭5", "F", "Gm", "Am", "B♭", "C"],
["E♭m", "Fm♭5", "F#", "G#m", "B♭m", "B", "C#"],
["Em", "F#m♭5", "G", "Am", "Bm", "C", "D"],
["Fm", "Gm♭5", "G#", "B♭m", "Cm", "C#", "E♭"],
["G♭m", "G#m♭5", "A", "Bm", "C#m", "D", "E"],
["Gm", "Am♭5", "B♭", "Cm", "Dm", "E♭", "F"],
["A♭m", "B♭m♭5", "B", "C#m", "E♭m", "E", "F#"],
["Am", "Bm♭5", "C", "Dm", "Em", "F", "G"],
["B♭m", "Cm♭5", "C#", "E♭m", "Fm", "F#", "G#"],
["Bm", "C#m♭5", "D", "Em", "F#m", "G", "A"]
]
//全ダイアトニックコード
const dChords: string[][][] = [
MdChords,
mDChords
]
type Props = {
dkey: number,
Mm: number
}
//音源接続
var synth = new Tone.PolySynth().toMaster();
//ボタン表記が大文字になるのを防ぐ
const TextButton = styled(Button)`
text-transform: none;
`;
export const MakeDChords = (props: Props) => {
if (props.Mm === 0) {
var Ⅰ_chord = [tone[props.dkey], tone[props.dkey + 4], tone[props.dkey + 7]];
var Ⅱ_Chord = [tone[props.dkey + 2], tone[props.dkey + 5], tone[props.dkey + 9]];
var Ⅲ_chord = [tone[props.dkey + 4], tone[props.dkey + 7], tone[props.dkey + 11]];
var Ⅳ_chord = [tone[props.dkey + 5], tone[props.dkey + 9], tone[props.dkey + 12]];
var Ⅴ_chord = [tone[props.dkey + 7], tone[props.dkey + 11], tone[props.dkey + 14]];
var Ⅵ_chord = [tone[props.dkey + 9], tone[props.dkey + 12], tone[props.dkey + 16]];
var Ⅶ_chord = [tone[props.dkey + 11], tone[props.dkey + 14], tone[props.dkey + 17]];
return (<ButtonGroup size="large" aria-label="Large button group">
<TextButton key="one" onClick={() => { synth.triggerAttackRelease(Ⅰ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][0]}{"\n"}Ⅰ</TextButton>,
<TextButton key="two" onClick={() => { synth.triggerAttackRelease(Ⅱ_Chord, '4n'); }}>{dChords[props.Mm][props.dkey][1]}{"\n"}Ⅱ</TextButton>,
<TextButton key="three" onClick={() => { synth.triggerAttackRelease(Ⅲ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][2]}{"\n"}Ⅲ</TextButton>,
<TextButton key="four" onClick={() => { synth.triggerAttackRelease(Ⅳ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][3]}{"\n"}Ⅳ</TextButton>,
<TextButton key="five" onClick={() => { synth.triggerAttackRelease(Ⅴ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][4]}{"\n"}Ⅴ</TextButton>,
<TextButton key="six" onClick={() => { synth.triggerAttackRelease(Ⅵ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][5]}{"\n"}Ⅵ</TextButton>,
<TextButton key="seven" onClick={() => { synth.triggerAttackRelease(Ⅶ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][6]}{"\n"}Ⅶ</TextButton>
</ButtonGroup>
)
} else {
var Ⅰ_chord = [tone[props.dkey], tone[props.dkey + 3], tone[props.dkey + 7]];
var Ⅱ_Chord = [tone[props.dkey + 2], tone[props.dkey + 5], tone[props.dkey + 8]];
var Ⅲ_chord = [tone[props.dkey + 3], tone[props.dkey + 7], tone[props.dkey + 10]];
var Ⅳ_chord = [tone[props.dkey + 5], tone[props.dkey + 8], tone[props.dkey + 12]];
var Ⅴ_chord = [tone[props.dkey + 7], tone[props.dkey + 10], tone[props.dkey + 14]];
var Ⅵ_chord = [tone[props.dkey + 8], tone[props.dkey + 12], tone[props.dkey + 15]];
var Ⅶ_chord = [tone[props.dkey + 10], tone[props.dkey + 14], tone[props.dkey + 17]];
return (
<ButtonGroup size="large" aria-label="Large button group">
<TextButton key="one" onClick={() => { synth.triggerAttackRelease(Ⅰ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][0]}{"\n"}Ⅰ</TextButton>,
<TextButton key="two" onClick={() => { synth.triggerAttackRelease(Ⅱ_Chord, '4n'); }}>{dChords[props.Mm][props.dkey][1]}{"\n"}Ⅱ</TextButton>,
<TextButton key="three" onClick={() => { synth.triggerAttackRelease(Ⅲ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][2]}{"\n"}Ⅲ</TextButton>,
<TextButton key="four" onClick={() => { synth.triggerAttackRelease(Ⅳ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][3]}{"\n"}Ⅳ</TextButton>,
<TextButton key="five" onClick={() => { synth.triggerAttackRelease(Ⅴ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][4]}{"\n"}Ⅴ</TextButton>,
<TextButton key="six" onClick={() => { synth.triggerAttackRelease(Ⅵ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][5]}{"\n"}Ⅵ</TextButton>,
<TextButton key="seven" onClick={() => { synth.triggerAttackRelease(Ⅶ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][6]}{"\n"}Ⅶ</TextButton>
</ButtonGroup>
)
}
}
App.tsx
//key選択状況を保持
const [key, setKey] = React.useState(0);
//keyが選択されたときに処理実行
const keyChange = (event: SelectChangeEvent) => {
setKey(Number(event.target.value as string));
};
//Maj/min選択状況を保持
const [Mm, setMm] = React.useState(0);
//Maj/minが選択されたときに処理実行
const MmChange = (event: SelectChangeEvent) => {
setMm(Number(event.target.value as string));
};
key(調)とMm(メジャーキーかマイナーキーか)の選択状況を保持し、select状況が変更されるイベントが起きるたびに新しい選択をsetする。
return (
<Grid container
direction="row"
alignContent="center"
justifyContent="center"
sx={{ minHeight: "50vh" }}
> <Grid item xs={0}>
<FormControl fullWidth>
<InputLabel id="key">key</InputLabel>
<Select
labelId="key"
id="key"
label="key"
onChange={keyChange}
>
<MenuItem value={0}>C</MenuItem>
<MenuItem value={1}>D♭</MenuItem>
<MenuItem value={2}>D</MenuItem>
<MenuItem value={3}>E♭</MenuItem>
<MenuItem value={4}>E</MenuItem>
<MenuItem value={5}>F</MenuItem>
<MenuItem value={6}>G♭</MenuItem>
<MenuItem value={7}>G</MenuItem>
<MenuItem value={8}>A♭</MenuItem>
<MenuItem value={9}>A</MenuItem>
<MenuItem value={10}>B♭</MenuItem>
<MenuItem value={11}>B</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={0}>
<FormControl>
<InputLabel id="Mm">maj/min</InputLabel>
<Select
labelId="Mm"
id="Mm"
label="Mm"
onChange={MmChange} >
<MenuItem value={0}>maj</MenuItem>
<MenuItem value={1}>min</MenuItem>
</Select>
</FormControl>
<MakeDChords dkey={key} Mm={Mm} />
</Grid>
<Grid item xs={12}>
</Grid>
</Grid>
)
keyとMmのselectボックスを表示している。
onChange={keyChange}
onChange={MmChange}
でselect状況が更新された際の処理を呼んでいる。
<MakeDChords dkey={key} Mm={Mm} />
では、logic.tsx(子)に親としてkeyとMmが何を選択されているかのvalueをpropsで送る。
logic.tsx
//鍵盤音(1オクターブのみ)
const tone = ["C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4"];
//メジャーダイアトニックコード
const MdChords = [
["C", "Dm", "Em", "F", "G", "Am", "Bm(♭5)"],
["D♭", "E♭m", "Fm", "G♭", "A♭", "B♭m", "Cm(♭5)"],
["D", "Em", "F#m", "G", "A", "Bm", "C#m(♭5)"],
["E♭", "Fm", "Gm", "A♭", "B♭", "Cm", "Dm(♭5)"],
["E", "F#m", "G#m", "A", "B", "C#m", "D#m(♭5)"],
["F", "Gm", "Am", "B♭", "C", "Dm", "Em(♭5)"],
["G♭", "A♭m", "B♭m", "C♭", "D♭", "E♭m", "Fm(♭5)"],
["G", "Am", "Bm", "C", "D", "Em", "F#m(♭5)"],
["A♭", "B♭m", "Cm", "D♭", "E♭", "Fm", "Gm(♭5)"],
["A", "Bm", "C#m", "D", "E", "F#m", "G#m(♭5)"],
["B♭", "Cm", "Dm", "E♭", "F", "Gm", "Am(♭5)"],
["B", "C#m", "D#m", "E", "F#", "G#m", "A#m(♭5)"]
]
//マイナーダイアトニックコード
const mDChords = [
["Cm", "Dm♭5", "E♭", "Fm", "Gm", "G#", "B♭"],
["D♭m", "E♭m♭5", "E", "F#m", "G#m", "A", "B"],
["Dm", "Em♭5", "F", "Gm", "Am", "B♭", "C"],
["E♭m", "Fm♭5", "F#", "G#m", "B♭m", "B", "C#"],
["Em", "F#m♭5", "G", "Am", "Bm", "C", "D"],
["Fm", "Gm♭5", "G#", "B♭m", "Cm", "C#", "E♭"],
["G♭m", "G#m♭5", "A", "Bm", "C#m", "D", "E"],
["Gm", "Am♭5", "B♭", "Cm", "Dm", "E♭", "F"],
["A♭m", "B♭m♭5", "B", "C#m", "E♭m", "E", "F#"],
["Am", "Bm♭5", "C", "Dm", "Em", "F", "G"],
["B♭m", "Cm♭5", "C#", "E♭m", "Fm", "F#", "G#"],
["Bm", "C#m♭5", "D", "Em", "F#m", "G", "A"]
]
//全ダイアトニックコード
const dChords: string[][][] = [
MdChords,
mDChords
]
toneにC4~B4の音を繰り返し入れている。
1オクターブ分の鍵盤が横にずっと並んでいると考えればイメージしやすいかも。
ダイアトニックコード名に関してはコードの通りで、配列に入れて管理。
type Props = {
dkey: number,
Mm: number
}
//音源接続
var synth = new Tone.PolySynth().toMaster();
//ボタン表記が大文字になるのを防ぐ
const TextButton = styled(Button)`
text-transform: none;
`;
type Propsで、App.tsx(親)から渡されてくるkeyとMmの型を指定。ここをちゃんと合わせないとエラーになる。
Tone.PolySynth().toMaster()でTone.jsの音を使用する。
今回はコード(和音)なのでPolySynth()を使用
MUIがボタン名の英字を勝手に大文字にしてくるので、スタイル指定用のコード記載。
export const MakeDChords = (props: Props) => {
if (props.Mm === 0) {
var Ⅰ_chord = [tone[props.dkey], tone[props.dkey + 4], tone[props.dkey + 7]];
var Ⅱ_Chord = [tone[props.dkey + 2], tone[props.dkey + 5], tone[props.dkey + 9]];
var Ⅲ_chord = [tone[props.dkey + 4], tone[props.dkey + 7], tone[props.dkey + 11]];
var Ⅳ_chord = [tone[props.dkey + 5], tone[props.dkey + 9], tone[props.dkey + 12]];
var Ⅴ_chord = [tone[props.dkey + 7], tone[props.dkey + 11], tone[props.dkey + 14]];
var Ⅵ_chord = [tone[props.dkey + 9], tone[props.dkey + 12], tone[props.dkey + 16]];
var Ⅶ_chord = [tone[props.dkey + 11], tone[props.dkey + 14], tone[props.dkey + 17]];
return (<ButtonGroup size="large" aria-label="Large button group">
<TextButton key="one" onClick={() => { synth.triggerAttackRelease(Ⅰ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][0]}{"\n"}Ⅰ</TextButton>,
<TextButton key="two" onClick={() => { synth.triggerAttackRelease(Ⅱ_Chord, '4n'); }}>{dChords[props.Mm][props.dkey][1]}{"\n"}Ⅱ</TextButton>,
<TextButton key="three" onClick={() => { synth.triggerAttackRelease(Ⅲ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][2]}{"\n"}Ⅲ</TextButton>,
<TextButton key="four" onClick={() => { synth.triggerAttackRelease(Ⅳ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][3]}{"\n"}Ⅳ</TextButton>,
<TextButton key="five" onClick={() => { synth.triggerAttackRelease(Ⅴ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][4]}{"\n"}Ⅴ</TextButton>,
<TextButton key="six" onClick={() => { synth.triggerAttackRelease(Ⅵ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][5]}{"\n"}Ⅵ</TextButton>,
<TextButton key="seven" onClick={() => { synth.triggerAttackRelease(Ⅶ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][6]}{"\n"}Ⅶ</TextButton>
</ButtonGroup>
)
} else {
var Ⅰ_chord = [tone[props.dkey], tone[props.dkey + 3], tone[props.dkey + 7]];
var Ⅱ_Chord = [tone[props.dkey + 2], tone[props.dkey + 5], tone[props.dkey + 8]];
var Ⅲ_chord = [tone[props.dkey + 3], tone[props.dkey + 7], tone[props.dkey + 10]];
var Ⅳ_chord = [tone[props.dkey + 5], tone[props.dkey + 8], tone[props.dkey + 12]];
var Ⅴ_chord = [tone[props.dkey + 7], tone[props.dkey + 10], tone[props.dkey + 14]];
var Ⅵ_chord = [tone[props.dkey + 8], tone[props.dkey + 12], tone[props.dkey + 15]];
var Ⅶ_chord = [tone[props.dkey + 10], tone[props.dkey + 14], tone[props.dkey + 17]];
return (
<ButtonGroup size="large" aria-label="Large button group">
<TextButton key="one" onClick={() => { synth.triggerAttackRelease(Ⅰ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][0]}{"\n"}Ⅰ</TextButton>,
<TextButton key="two" onClick={() => { synth.triggerAttackRelease(Ⅱ_Chord, '4n'); }}>{dChords[props.Mm][props.dkey][1]}{"\n"}Ⅱ</TextButton>,
<TextButton key="three" onClick={() => { synth.triggerAttackRelease(Ⅲ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][2]}{"\n"}Ⅲ</TextButton>,
<TextButton key="four" onClick={() => { synth.triggerAttackRelease(Ⅳ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][3]}{"\n"}Ⅳ</TextButton>,
<TextButton key="five" onClick={() => { synth.triggerAttackRelease(Ⅴ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][4]}{"\n"}Ⅴ</TextButton>,
<TextButton key="six" onClick={() => { synth.triggerAttackRelease(Ⅵ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][5]}{"\n"}Ⅵ</TextButton>,
<TextButton key="seven" onClick={() => { synth.triggerAttackRelease(Ⅶ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][6]}{"\n"}Ⅶ</TextButton>
</ButtonGroup>
)
}
}
ここでApp.tsxに結果をreturn
export const MakeDChords = (props: Props) => {
export
で他のファイルにインポートできるよう宣言。
props: Props
でApp.tsx(親)からのpropsを受け取る。コロンの後のPropsで型指定。
if (props.Mm === 0) {
でメジャーかマイナーか判定
var Ⅰ_chord ~ var Ⅶ_chord
で現在選ばれているkeyを基にtone配列から音を取り出してダイアトニックコードを作成する。ここはプログラムというよりは音楽よりの話。
<TextButton key="one" onClick={() => { synth.triggerAttackRelease(Ⅰ_chord, '4n'); }}>{dChords[props.Mm][props.dkey][0]}{"\n"}Ⅰ</TextButton>,
→onClick={() => { synth.triggerAttackRelease(Ⅰ_chord, '4n'); }}
でボタンクリックされたときにコードをどのくらいの長さ再生するかを記載。
{dChords[props.Mm][props.dkey][0]}{"\n"}Ⅰ
でボタン名にダイアトニックコード名を入れる。※{"\n"}は改行
以上。
まとめ
実装としては以上になります。何かのお役に立てたら幸いです。
今後も機能追加したら記事を書いてみようと思う。
↓ソース
https://github.com/haaan1234/DChordApp