Skip to content

Commit

Permalink
Merge pull request #17 from tainakanchu/develop
Browse files Browse the repository at this point in the history
  • Loading branch information
tainakanchu authored Jul 29, 2023
2 parents ecf06b3 + 3ce3348 commit 5c3fa2d
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 88 deletions.
100 changes: 33 additions & 67 deletions app/_components/BpmComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,90 +3,56 @@ import React from "react";

import { BpmButton } from "./BpmButton";
import { SubBpmComponent } from "./SubBpmComponent";
import { bpmCalculator } from "../_utils";
import { useAccuracyColor, useBpmCalculator } from "../_hooks";
import { BpmConvertSetting } from "../_types/BpmConvertSetting";

type Props = {};

export const BpmComponent: React.FC<Props> = ({}) => {
const [dateList, setDateList] = React.useState<Date[]>([]);

const handleButtonClick = React.useCallback(() => {
setDateList((dateList) => [...dateList, new Date()]);
}, []);

const bpm = React.useMemo(() => {
return bpmCalculator(dateList);
}, [dateList]);

// sdの値に応じてbpmの文字の色を変える
// sdの値は bpm に対しての割合で評価する
const { sd: _sd, value } = bpm;
const bpmConvertSettings: BpmConvertSetting[] = [
{
numerator: 1,
denominator: 2,
},
{
numerator: 3,
denominator: 4,
},
{
numerator: 4,
denominator: 3,
},
];

const sd = _sd ?? 50;

// 5刻みぐらいで色を変える
const bpmColor = React.useMemo(() => {
if (sd < 30) {
return "text-green-500";
} else if (sd < 35) {
return "text-green-400";
} else if (sd < 40) {
return "text-green-300";
} else if (sd < 45) {
return "text-yellow-200";
} else if (sd < 50) {
return "text-yellow-100";
} else if (sd < 55) {
return "text-red-100";
} else if (sd < 60) {
return "text-red-200";
} else if (sd < 65) {
return "text-red-300";
} else if (sd < 70) {
return "text-red-400";
} else {
return "text-red-500";
}
}, [sd]);

const halfBpm = React.useMemo(() => {
return value ? value / 2 : undefined;
}, [value]);
export const BpmComponent: React.FC<Props> = ({}) => {
const { handleAddTimeData, handleClearTimeData, bpm, convertedBpmList } =
useBpmCalculator({ bpmConvertSettings });

const threeFourthBpm = React.useMemo(() => {
return value ? (value * 3) / 4 : undefined;
}, [value]);
const { sd, value } = bpm;

const fourThirdBpm = React.useMemo(() => {
return value ? (value * 4) / 3 : undefined;
}, [value]);
const bpmColor = useAccuracyColor(sd ?? 50);

return (
<div>
<BpmButton onButtonClick={handleButtonClick}>
<BpmButton onButtonClick={handleAddTimeData}>
<div className="w-screen h-screen flex gap-16 justify-center items-center flex-col">
<p className="text-6xl font-bold">TAP</p>
<p className={`text-8xl ${bpmColor}`}>
{bpm.value?.toFixed(1) ?? "🎶"}
</p>
<p className={`text-8xl ${bpmColor}`}>{value?.toFixed(1) ?? "🎶"}</p>
<div className="flex flex-col gap-6 justify-center">
<SubBpmComponent title={"1/2"} value={halfBpm?.toFixed(1) ?? "-"} />
<SubBpmComponent
title={"3/4"}
value={threeFourthBpm?.toFixed(1) ?? "-"}
/>
<SubBpmComponent
title={"4/3"}
value={fourThirdBpm?.toFixed(1) ?? "-"}
/>
{convertedBpmList.map((convertedBpm) => {
return (
<SubBpmComponent
key={convertedBpm.label}
title={convertedBpm.label}
value={convertedBpm.value?.toFixed(1) ?? "-"}
/>
);
})}
</div>
</div>
</BpmButton>
<button
className="fixed bottom-0 right-0 p-4 bg-zinc-800"
onClick={() => {
setDateList([]);
}}
onClick={handleClearTimeData}
>
reset
</button>
Expand Down
2 changes: 2 additions & 0 deletions app/_hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./useAccuracyColor";
export * from "./useBpmCalculator";
33 changes: 33 additions & 0 deletions app/_hooks/useAccuracyColor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react";

/**
* 値の正確度に応じた色を返す
*/
export const useAccuracyColor = (sd: number): string => {
// 5刻みぐらいで色を変える
const bpmColor = React.useMemo(() => {
if (sd < 30) {
return "text-green-500";
} else if (sd < 35) {
return "text-green-400";
} else if (sd < 40) {
return "text-green-300";
} else if (sd < 45) {
return "text-yellow-200";
} else if (sd < 50) {
return "text-yellow-100";
} else if (sd < 55) {
return "text-red-100";
} else if (sd < 60) {
return "text-red-200";
} else if (sd < 65) {
return "text-red-300";
} else if (sd < 70) {
return "text-red-400";
} else {
return "text-red-500";
}
}, [sd]);

return bpmColor;
};
46 changes: 46 additions & 0 deletions app/_hooks/useBpmCalculator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from "react";
import { calculateBpm } from "../_utils";
import { BpmConvertSetting } from "../_types/BpmConvertSetting";

/**
* BPMを計算するフック
*
* @param setting BPMを変換する設定
* @returns BPMを計算するフック
*/
export const useBpmCalculator = (setting: {
bpmConvertSettings: BpmConvertSetting[];
}) => {
const [dateList, setDateList] = React.useState<Date[]>([]);

const handleAddTimeData = React.useCallback(() => {
setDateList((dateList) => [...dateList, new Date()]);
}, []);

const handleClearTimeData = React.useCallback(() => {
setDateList([]);
}, []);

const bpm = React.useMemo(() => {
return calculateBpm(dateList);
}, [dateList]);

const convertedBpmList = React.useMemo(() => {
return setting.bpmConvertSettings.map((setting) => {
const label: string = `${setting.numerator}/${setting.denominator}`;
return {
label,
value: bpm.value
? (bpm.value * setting.numerator) / setting.denominator
: null,
};
});
}, [bpm.value, setting]);

return {
handleAddTimeData,
handleClearTimeData,
bpm,
convertedBpmList,
};
};
9 changes: 9 additions & 0 deletions app/_types/BpmConvertSetting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* BPMを変換する設定
*/
export type BpmConvertSetting = {
/** 分子 */
denominator: number;
/** 分母 */
numerator: number;
};
40 changes: 22 additions & 18 deletions app/_utils/bpmCalculator.ts → app/_utils/calculateBpm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ const MAX_PAST_TIME = 20 * 1000;
// これ以上の間隔でのデータは考慮しない
const DIFF_THRESHOLD = 3000;

type Return = {
value: number | null;
sd: number | null;
};
const simpleMovingAverageHandler =
(count: number) =>
(acc: number, cur: number): number =>
acc + cur / count;

/**
*
* BPMを計算する
* @param dateList 記録された時間のリスト
*/
export const bpmCalculator: (dateList: Date[]) => Return = (dateList) => {
export const calculateBpm: (dateList: Date[]) => {
value: number | null;
sd: number | null;
} = (dateList) => {
// 閾値以内に記録されたデータだけ使う
const now = new Date();
const past = new Date(now.getTime() - MAX_PAST_TIME);
Expand All @@ -31,15 +35,14 @@ export const bpmCalculator: (dateList: Date[]) => Return = (dateList) => {
// 一定の秒数以上の差分は考慮しない
.filter((diff) => diff < DIFF_THRESHOLD);

// TODO: 平均値からあまりにも外れてるデータも考慮しない

// 必要数のデータがない場合は null を返す
if (filteredDiffList.length < 1) return emptyReturn;

// 差分の平均値を単純移動平均で計算
const average =
filteredDiffList.reduce((acc, cur) => acc + cur, 0) /
filteredDiffList.length;
const average = filteredDiffList.reduce(
simpleMovingAverageHandler(filteredDiffList.length),
0
);

const σ = calculateStandardDeviation(filteredDiffList);
const bpm = 60000 / average;
Expand All @@ -58,13 +61,14 @@ export const bpmCalculator: (dateList: Date[]) => Return = (dateList) => {
(diff) => Math.abs(diff - average) < σ
);

// データが減りすぎの時は第一段階の計算結果を返す
if (filteredDiffList2.length < 8) return tmpReturn;

// 改めて平均値を計算
const average2 =
filteredDiffList2.reduce((acc, cur) => acc + cur, 0) /
filteredDiffList2.length;
const average2 = filteredDiffList2.reduce(
simpleMovingAverageHandler(filteredDiffList2.length),
0
);

// average2 がゼロの時は第一段階の計算結果を返す
if (average2 === 0) return tmpReturn;

// 改めて標準偏差を計算
const sd = calculateStandardDeviation(filteredDiffList2);
Expand All @@ -77,7 +81,7 @@ export const bpmCalculator: (dateList: Date[]) => Return = (dateList) => {
};
};

const emptyReturn: Return = {
const emptyReturn = {
value: null,
sd: null,
};
Expand Down
2 changes: 1 addition & 1 deletion app/_utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from "./bpmCalculator";
export * from "./calculateBpm";
3 changes: 3 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import "./globals.css";
import type { Metadata } from "next";
import { BIZ_UDPGothic } from "next/font/google";
import { Analytics } from "@vercel/analytics/react";

import Pwa from "./_components/Pwa";

const bizUd = BIZ_UDPGothic({
Expand Down Expand Up @@ -29,6 +31,7 @@ export default function RootLayout({
<body className={bizUd.className}>
{children}
<Pwa />
<Analytics />
</body>
</html>
);
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bpm-calculator-for-dj",
"version": "0.3.0",
"version": "0.3.1",
"private": true,
"scripts": {
"dev": "next dev",
Expand All @@ -12,6 +12,7 @@
"@types/node": "20.4.5",
"@types/react": "18.2.17",
"@types/react-dom": "18.2.7",
"@vercel/analytics": "^1.0.1",
"autoprefixer": "10.4.14",
"eslint": "8.45.0",
"eslint-config-next": "13.4.12",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion public/manifest.webmanifest
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"short_name": "BPM Calc",
"name": "BPM calculator for DJ",
"version": "0.3.0",
"version": "0.3.1",
"theme_color": "#080808",
"background_color": "#080808",
"display": "standalone",
Expand Down

0 comments on commit 5c3fa2d

Please sign in to comment.