From 2b84b73792c8db17759e65ef01ce3a26db562f7d Mon Sep 17 00:00:00 2001 From: norman-andrians Date: Wed, 13 Mar 2024 08:05:29 +0700 Subject: [PATCH] Piezo music editor --- src/components/forms/EvoDropDown.tsx | 82 ++++++ src/components/forms/EvoInput.tsx | 2 +- src/components/forms/Input.tsx | 31 +++ .../landing/ControlPiezoMusicEditor.tsx | 236 ++++++++++++++++++ src/contexts/BoardController.tsx | 14 +- src/controllers/BoardController.tsx | 14 +- src/css/index.css | 16 +- src/data/melodies.ts | 91 +++++++ src/data/music/fallen-down.json | 12 + src/data/music/ode-to-joy.json | 9 + src/hooks/index.tsx | 106 +++++++- src/pages/MainPage.tsx | 2 + src/types/board.ts | 8 + 13 files changed, 616 insertions(+), 7 deletions(-) create mode 100644 src/components/forms/EvoDropDown.tsx create mode 100644 src/components/forms/Input.tsx create mode 100644 src/components/landing/ControlPiezoMusicEditor.tsx create mode 100644 src/data/melodies.ts create mode 100644 src/data/music/fallen-down.json create mode 100644 src/data/music/ode-to-joy.json diff --git a/src/components/forms/EvoDropDown.tsx b/src/components/forms/EvoDropDown.tsx new file mode 100644 index 0000000..fef0182 --- /dev/null +++ b/src/components/forms/EvoDropDown.tsx @@ -0,0 +1,82 @@ +import { useState } from "react"; + +type EvoDropDownItem = { + name: string, + value: any +} + +interface EvoDropDownProps { + className?: string; + name?: string; + items: EvoDropDownItem[]; + initItem?: EvoDropDownItem; + onValueChange?: (item: EvoDropDownItem) => void; +} + +interface MenuProps extends EvoDropDownProps { + appear: boolean; +} + +function Menu ({ className, items, initItem, appear, onValueChange }: MenuProps) { + const [currentItem, setItem] = useState(initItem || items[0]); + + return ( +
+ {items.map((item) => ( + + ))} +
+ ) +} + +function EvoDropDown ({ className, name, items, initItem, onValueChange }: EvoDropDownProps) { + const [appear, setAppear] = useState(false); + const [currentItem, setItem] = useState(initItem || items[0]); + + const handleClick = () => { + setAppear(!appear); + } + + return ( +
+ +
+ {items.map((item) => ( + + ))} +
+
+ ) +} + +EvoDropDown.Menu = Menu; + +export default EvoDropDown; \ No newline at end of file diff --git a/src/components/forms/EvoInput.tsx b/src/components/forms/EvoInput.tsx index 26a8867..6acc37a 100644 --- a/src/components/forms/EvoInput.tsx +++ b/src/components/forms/EvoInput.tsx @@ -27,7 +27,7 @@ export default function EvoInput ({ +} + +export default function Input ({ + className, + name, + placeholder, + value, + type, + onChange +}: InputProps) { + return ( + + ) +} \ No newline at end of file diff --git a/src/components/landing/ControlPiezoMusicEditor.tsx b/src/components/landing/ControlPiezoMusicEditor.tsx new file mode 100644 index 0000000..c51c366 --- /dev/null +++ b/src/components/landing/ControlPiezoMusicEditor.tsx @@ -0,0 +1,236 @@ +import { ChangeEvent, MouseEventHandler, useState } from "react"; +import { usePiezoMusic } from "../../hooks"; +import { PiezoMusic } from "../../types/board"; +import EvoInput from "../forms/EvoInput"; +import Input from "../forms/Input"; +import EvoDropDown from "../forms/EvoDropDown"; +import Button from "../forms/Button"; +import { pitch } from "../../data/melodies"; +import { PatchPiezoMusic } from "../../controllers/BoardController"; + +function getNoteItems () { + const pitches = Object.keys(pitch); + const notes = pitches.map(n => { + const note = n.replace("S", "#"); + return {name: note, value: note} + }) + const additional = [ + { + name: ". (Jump note)", + value: "." + }, + { + name: "- (Hold note)", + value: "-" + } + ] + return [...additional, ...notes]; +} + +function NoteItem ({note, index, parentIndex}: { note: string, index: number, parentIndex: number }) { + const { changeNote } = usePiezoMusic(); + + const [dropAppear, setApper] = useState(false); + + const noteItems = getNoteItems(); + const toggleDropdown = () => { setApper(!dropAppear) }; + const handleChange = (item: { name: string, value: any }) => { + changeNote(parentIndex, index, item.value); + } + + return ( + + ) +} + +function NotePlus ({ parentIndex }: { parentIndex: number }) { + const { addNote } = usePiezoMusic(); + const [dropAppear, setApper] = useState(false); + + const noteItems = getNoteItems(); + const toggleDropdown = () => { setApper(!dropAppear) }; + + const handleAddNote = (item: { name: string, value: any }) => { + addNote(parentIndex, item.value); + } + + return ( + + ) +} + +function PiezoEditor ({ piezo, index }: { piezo: PiezoMusic, index: number }) { + const { setName, setPin, setBeat, setTempo } = usePiezoMusic(); + + const beatsItem = [ + { + name: "1/2", + value: 1/2 + }, + { + name: "1/4", + value: 1/4 + }, + { + name: "1/8", + value: 1/8 + }, + { + name: "1/16", + value: 1/16 + } + ]; + + const initBeatItem = beatsItem.find((item) => item.value == piezo.beats); + + const handle = { + changeName: (e: ChangeEvent) => { + setName(index, e.target.value); + }, + pinChange: (e: ChangeEvent) => { + setPin(index, e.target.value); + }, + beatChange: (item: { name: string, value: any }) => { + setBeat(index, item.value); + }, + tempoChange: (e: ChangeEvent) => { + const tempo = Number.parseInt(e.target.value); + setTempo(index, tempo); + }, + play: () => { + PatchPiezoMusic(piezo); + } + } + + return ( +
+
+
+ + + + +
+
+ +
Export
+ +
+ + + +
+
+
+
+ {piezo.notes.map((note, i) => ( + + ))} + +
+
+
+ ) +} + +function BarPlus ({ onClick }: { onClick?: MouseEventHandler }) { + return ( + + ) +} + +function ControlPiezoMusicEditor () { + const { piezeNotes, addPiezo } = usePiezoMusic(); + + const handleAdd = (): void => { + let anopin = 13; + for (let i = 0; i < piezeNotes.length; i++) { + if (piezeNotes.filter(piezo => piezo.pin == anopin).length > 0) { + anopin--; + } + else break; + } + addPiezo({ + name: "test", + pin: anopin, + notes: [], + beats: 1/4, + tempo: 100 + }); + } + + return ( +
+
+

+ Piezo Music Editor +

+
+

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Omnis rem, perspiciatis voluptatibus dolor officiis voluptas asperiores reiciendis quisquam qui numquam quas illum velit in id, est expedita ipsa voluptatum eligendi?

+
+
+ {piezeNotes.map((piezo, i) => ( + + ))} + +
+
+
+ ) +} + +export default ControlPiezoMusicEditor; \ No newline at end of file diff --git a/src/contexts/BoardController.tsx b/src/contexts/BoardController.tsx index a2007aa..54f406e 100644 --- a/src/contexts/BoardController.tsx +++ b/src/contexts/BoardController.tsx @@ -1,17 +1,23 @@ import { Dispatch, ReactNode, SetStateAction, createContext, useState } from "react"; -import { ChannelPinState, DynamicPinState, PinMode, PinState } from "../types/board"; +import { ChannelPinState, DynamicPinState, PiezoMusic, PinMode, PinState } from "../types/board"; + +import ExampleMusic1 from "../data/music/ode-to-joy.json"; +import ExampleMusic2 from "../data/music/fallen-down.json"; interface ControllerContextProps { pinModes: PinMode[]; leds: PinState[]; rgbLed: ChannelPinState[]; piezo: DynamicPinState[]; + piezeNotes: PiezoMusic[]; motoServo: DynamicPinState[]; photoresistor: DynamicPinState[]; + setPinModes?: Dispatch>; setLeds?: Dispatch>; setRgbLed?: Dispatch>; setPiezo?: Dispatch>; + setNotes?: Dispatch>; setMotoServo?: Dispatch>; setPhotoresistor?: Dispatch>; } @@ -46,6 +52,10 @@ const INIT_VALUES: ControllerContextProps = { state: 262 } ], + piezeNotes: [ + ExampleMusic1, + ExampleMusic2 + ], motoServo: [ { pin: "9", @@ -67,6 +77,7 @@ export function BoardControllerProvider ({ children }: { children: ReactNode }) const [leds, setLeds] = useState(INIT_VALUES.leds); const [rgbLed, setRgbLed] = useState(INIT_VALUES.rgbLed); const [piezo, setPiezo] = useState(INIT_VALUES.piezo); + const [piezeNotes, setNotes] = useState(INIT_VALUES.piezeNotes); const [motoServo, setMotoServo] = useState(INIT_VALUES.motoServo); const [photoresistor, setPhotoresistor] = useState(INIT_VALUES.photoresistor); @@ -75,6 +86,7 @@ export function BoardControllerProvider ({ children }: { children: ReactNode }) leds, setLeds, rgbLed, setRgbLed, piezo, setPiezo, + piezeNotes, setNotes, motoServo, setMotoServo, photoresistor, setPhotoresistor }; diff --git a/src/controllers/BoardController.tsx b/src/controllers/BoardController.tsx index 33a17ad..3e67cca 100644 --- a/src/controllers/BoardController.tsx +++ b/src/controllers/BoardController.tsx @@ -1,5 +1,5 @@ import axios from "axios"; -import { ChannelPinState } from "../types/board"; +import { ChannelPinState, PiezoMusic } from "../types/board"; import { io } from "../socket/socket.io"; const url = "http://localhost:3000/api-arduino"; @@ -46,6 +46,18 @@ export async function PatchPiezo (pin: number, freq: number) { console.log("Piezo Response: ", data); } +export async function PatchPiezoMusic (music: PiezoMusic) { + const res = await axios.patch(`${url}/piezo/music`, music, { + headers: { + 'Content-Type': 'application/json' + } + }); + const data = res.data; + + console.log("Piezo Response: ", data); +} + + export async function PatchServo(pin: string | number, value: number) { io.emit("servo", pin, value); } \ No newline at end of file diff --git a/src/css/index.css b/src/css/index.css index 80ae653..be82c5a 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -10,13 +10,23 @@ } input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; + -webkit-appearance: none; + margin: 0; } /* Firefox */ input[type=number] { - -moz-appearance: textfield; + -moz-appearance: textfield; + } + + .v-scrollbar::-webkit-scrollbar { + width: 3px; + } + .v-scrollbar::-webkit-scrollbar-track { + @apply bg-transparent; + } + .v-scrollbar::-webkit-scrollbar-thumb { + @apply bg-primary cursor-pointer rounded-full; } } diff --git a/src/data/melodies.ts b/src/data/melodies.ts new file mode 100644 index 0000000..93940df --- /dev/null +++ b/src/data/melodies.ts @@ -0,0 +1,91 @@ +export const pitch = { + B0: 31, + C1: 33, + CS1: 35, + D1: 37, + DS1: 39, + E1: 41, + F1: 44, + FS1: 46, + G1: 49, + GS1: 52, + A1: 55, + AS1: 58, + B1: 62, + C2: 65, + CS2: 69, + D2: 73, + DS2: 78, + E2: 82, + F2: 87, + FS2: 93, + G2: 98, + GS2: 104, + A2: 110, + AS2: 117, + B2: 123, + C3: 131, + CS3: 139, + D3: 147, + DS3: 156, + E3: 165, + F3: 175, + FS3: 185, + G3: 196, + GS3: 208, + A3: 220, + AS3: 233, + B3: 247, + C4: 262, + CS4: 277, + D4: 294, + DS4: 311, + E4: 330, + F4: 349, + FS4: 370, + G4: 392, + GS4: 415, + A4: 440, + AS4: 466, + B4: 494, + C5: 523, + CS5: 554, + D5: 587, + DS5: 622, + E5: 659, + F5: 698, + FS5: 740, + G5: 784, + GS5: 831, + A5: 880, + AS5: 932, + B5: 988, + C6: 1047, + CS6: 1109, + D6: 1175, + DS6: 1245, + E6: 1319, + F6: 1397, + FS6: 1480, + G6: 1568, + GS6: 1661, + A6: 1760, + AS6: 1865, + B6: 1976, + C7: 2093, + CS7: 2217, + D7: 2349, + DS7: 2489, + E7: 2637, + F7: 2794, + FS7: 2960, + G7: 3136, + GS7: 3322, + A7: 3520, + AS7: 3729, + B7: 3951, + C8: 4186, + CS8: 4435, + D8: 4699, + DS8: 4978 +} \ No newline at end of file diff --git a/src/data/music/fallen-down.json b/src/data/music/fallen-down.json new file mode 100644 index 0000000..b0e8441 --- /dev/null +++ b/src/data/music/fallen-down.json @@ -0,0 +1,12 @@ +{ + "name": "Fallen Down", + "pin": 11, + "notes": [ + "F#5", "C#5", "F#5", "C#5", "F#5", "C#5", "F#5", "C#5", "F#5", "C#5", "F#5", "C#5", + "B4", "A4", "C#5", ".", "A4", "B4", "E5", "D#5", "E5", "F#5", "D#5", "B4", + "F#5", "B4", "F#5", "B4", "F#5", "B4", "F#5", "A#4", "F#5", "A#4", "G5", ".", + "F#5", "D5", "F#5", "D5", "E5", "F#5", "E5", "-", "D5", "-", "C#5", "-" + ], + "beats": 0.5, + "tempo": 100 +} \ No newline at end of file diff --git a/src/data/music/ode-to-joy.json b/src/data/music/ode-to-joy.json new file mode 100644 index 0000000..e50f87f --- /dev/null +++ b/src/data/music/ode-to-joy.json @@ -0,0 +1,9 @@ +{ + "name": "Ode to joy", + "pin": 11, + "notes": [ + "E4", "E4", "F4", "G4", "G4", "F4", "E4", "D4", "C4", "C4", "D4", "E4", "D4", "-", "C4", "C4" + ], + "beats": 0.5, + "tempo": 100 +} \ No newline at end of file diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx index 8a51249..add257c 100644 --- a/src/hooks/index.tsx +++ b/src/hooks/index.tsx @@ -1,6 +1,11 @@ import { useContext } from "react"; import { BoardControllerContext } from "../contexts/BoardController"; -import { ChannelPinState } from "../types/board"; +import { ChannelPinState, PiezoMusic } from "../types/board"; + + +// * ----------------------------------- +// * I'm never use OOP in react lol +// * ----------------------------------- export function usePin () { return useContext(BoardControllerContext); @@ -119,6 +124,105 @@ export function usePiezo () { +export function usePiezoMusic () { + const { piezeNotes, setNotes } = useContext(BoardControllerContext); + + const getPiezo = (pin: number | string) => { + return piezeNotes.find(val => val.pin == pin); + } + + const addPiezo = (notes: PiezoMusic) => { + const newPiezo = [...piezeNotes, notes]; + setNotes!(newPiezo); + } + + const setPiezo = (index: number, piezo: PiezoMusic) => { + setNotes!(piezos => piezos.map((_piezo, i) => { + if (i == index) return piezo; + return _piezo; + })); + } + + const removePiezo = (index: number) => { + const newPiezo = piezeNotes.filter((_piezo, i) => i != index); + setNotes!(newPiezo); + } + + const addNote = (piezoIndex: number, note: string) => { + setNotes!(piezos => piezos.map((piezo, i) => { + if (piezoIndex == i) { + piezo.notes.push(note); + } + return piezo; + })); + } + + const changeNote = (piezoIndex: number, noteIndex: number, note: string) => { + setNotes!(piezos => piezos.map((piezo, i) => { + if (piezoIndex == i) { + piezo.notes[noteIndex] = note; + } + return piezo; + })); + } + + const removeNote = (piezoIndex: number, noteIndex: number) => { + setNotes!(piezos => piezos.map((piezo, i) => { + if (piezoIndex == i) { + piezo.notes = piezo.notes.filter((_p, i) => i != noteIndex); + } + return piezo; + })); + } + + const setPin = (index: number, newPin: string | number) => { + const newPiezo: PiezoMusic[] = piezeNotes.map((piezo, i) => { + if (i == index) { + piezo.pin = newPin; + } + return piezo; + }) + setNotes!(newPiezo); + } + + const setName = (index: number, name: string) => { + const newPiezo: PiezoMusic[] = piezeNotes.map((piezo, i) => { + if (i == index) { + piezo.name = name; + } + return piezo; + }) + setNotes!(newPiezo); + } + + const setBeat = (index: number, beats: number) => { + const newPiezo: PiezoMusic[] = piezeNotes.map((piezo, i) => { + if (i == index) { + piezo.beats = beats; + } + return piezo; + }) + setNotes!(newPiezo); + } + + const setTempo = (index: number, tempo: number) => { + const newPiezo: PiezoMusic[] = piezeNotes.map((piezo, i) => { + if (i == index) { + piezo.tempo = tempo; + } + return piezo; + }) + setNotes!(newPiezo); + } + + return { + piezeNotes, setNotes, getPiezo, addPiezo, removePiezo, setPiezo, + setPin, setName, setBeat, setTempo, + addNote, changeNote, removeNote + }; +} + + export function useServo () { const { motoServo, setMotoServo } = useContext(BoardControllerContext); diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index 56d1ace..7eecef0 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -14,6 +14,7 @@ import ControlPhotoresistor from "../components/landing/ControlPhotoresistor"; import ControlServo from "../components/landing/ControlServo"; import Closing from "../components/landing/Closing"; import Footer from "../components/Footer"; +import ControlPiezoMusicEditor from "../components/landing/ControlPiezoMusicEditor"; function MainPage () { const led = useRef(null); @@ -32,6 +33,7 @@ function MainPage () { + diff --git a/src/types/board.ts b/src/types/board.ts index f9cdb9d..603e72a 100644 --- a/src/types/board.ts +++ b/src/types/board.ts @@ -17,4 +17,12 @@ export interface ChannelPinState { red: PinState, green: PinState, blue: PinState +} + +export interface PiezoMusic { + name: string, + pin: number | string; + notes: string[]; + beats: number; + tempo: number; } \ No newline at end of file