Piezo music editor

This commit is contained in:
Nomi Nonsense (Nonszy) 2024-03-13 08:05:29 +07:00
parent 3450695885
commit 2b84b73792
13 changed files with 616 additions and 7 deletions

View File

@ -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<EvoDropDownItem>(initItem || items[0]);
return (
<div className={`absolute p-2 flex flex-col w-full bg-black border mt-3 border-border rounded-lg ${appear ? "block" : "hidden"} ${className}`}>
{items.map((item) => (
<button
className="py-2 px-3 flex justify-between items-center bg-transparent bg-opacity-100 hover:bg-indigo-300 hover:bg-opacity-20 rounded-md"
onClick={() => {
setItem(item);
if (onValueChange) onValueChange(item)}
}
>
<div className="text-left text-sm">
{item.name}
</div>
{currentItem.value == item.value && <div className="rounded-full bg-white w-2 h-2"></div>}
</button>
))}
</div>
)
}
function EvoDropDown ({ className, name, items, initItem, onValueChange }: EvoDropDownProps) {
const [appear, setAppear] = useState<boolean>(false);
const [currentItem, setItem] = useState<EvoDropDownItem>(initItem || items[0]);
const handleClick = () => {
setAppear(!appear);
}
return (
<div className="relative">
<button className={`flex items-center justify-between cursor-pointer bg-finn border border-border rounded-lg ${className}`} onClick={handleClick}>
<div className="w-fit p-2 text-sm ps-4">{name}</div>
<div className="w-1/2 h-full p-2 text-right pe-4 text-sm font-roboto-mono bg-transparent">
{currentItem.name}
</div>
</button>
<div className={`absolute p-2 flex flex-col w-full bg-black border mt-3 border-border rounded-lg ${appear ? "block" : "hidden"}`}>
{items.map((item) => (
<button
className="py-2 px-3 flex justify-between items-center bg-transparent bg-opacity-100 hover:bg-indigo-300 hover:bg-opacity-20 rounded-md"
onClick={() => {
setAppear(false);
setItem(item);
if (onValueChange) onValueChange(item)}
}
>
<div className="text-left text-sm">
{item.name}
</div>
{currentItem.value == item.value && <div className="rounded-full bg-white w-2 h-2"></div>}
</button>
))}
</div>
</div>
)
}
EvoDropDown.Menu = Menu;
export default EvoDropDown;

View File

@ -27,7 +27,7 @@ export default function EvoInput ({
<input <input
className="w-1/2 h-full p-2 text-right pe-4 text-sm font-roboto-mono bg-transparent" className="w-1/2 h-full p-2 text-right pe-4 text-sm font-roboto-mono bg-transparent"
type={type} type={type}
name="" name={name}
id="" id=""
value={value} value={value}
ref={labelRef} ref={labelRef}

View File

@ -0,0 +1,31 @@
import { ChangeEventHandler, HTMLInputTypeAttribute } from "react";
interface InputProps {
className?: string;
name?: string;
placeholder?: string;
value?: string;
type?: HTMLInputTypeAttribute
onChange?: ChangeEventHandler<HTMLInputElement>
}
export default function Input ({
className,
name,
placeholder,
value,
type,
onChange
}: InputProps) {
return (
<input
className={`px-4 py-2 text-left text-sm font-roboto-mono bg-finn border border-border rounded-lg ${className}`}
placeholder={placeholder}
type={type}
name={name}
id=""
value={value}
onChange={onChange}
/>
)
}

View File

@ -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<boolean>(false);
const noteItems = getNoteItems();
const toggleDropdown = () => { setApper(!dropAppear) };
const handleChange = (item: { name: string, value: any }) => {
changeNote(parentIndex, index, item.value);
}
return (
<button className="min-w-40 h-40 transition border border-border rounded-lg hover:bg-finn animate-size-fade-in" onClick={toggleDropdown}>
<div className="text-xl">
{note}
</div>
<EvoDropDown.Menu
className="!w-32 top-1 h-56 z-30 overflow-y-scroll v-scrollbar"
appear={dropAppear}
items={noteItems}
onValueChange={handleChange}
/>
</button>
)
}
function NotePlus ({ parentIndex }: { parentIndex: number }) {
const { addNote } = usePiezoMusic();
const [dropAppear, setApper] = useState<boolean>(false);
const noteItems = getNoteItems();
const toggleDropdown = () => { setApper(!dropAppear) };
const handleAddNote = (item: { name: string, value: any }) => {
addNote(parentIndex, item.value);
}
return (
<button className="border border-border bg-secondary-solid rounded-lg min-w-40 h-40" onClick={toggleDropdown}>
<i className="bi bi-plus text-2xl"></i>
<EvoDropDown.Menu
className="!w-32 top-1 h-56 z-30 overflow-y-scroll v-scrollbar"
appear={dropAppear}
items={noteItems}
onValueChange={handleAddNote}
/>
</button>
)
}
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<HTMLInputElement | null>) => {
setName(index, e.target.value);
},
pinChange: (e: ChangeEvent<HTMLInputElement | null>) => {
setPin(index, e.target.value);
},
beatChange: (item: { name: string, value: any }) => {
setBeat(index, item.value);
},
tempoChange: (e: ChangeEvent<HTMLInputElement | null>) => {
const tempo = Number.parseInt(e.target.value);
setTempo(index, tempo);
},
play: () => {
PatchPiezoMusic(piezo);
}
}
return (
<div className="flex flex-col gap-5 bg-secondary border border-border rounded-lg p-5 animate-size-fade-in">
<div className="flex flex-row justify-between">
<div className="flex flex-row gap-3">
<Input
className="w-64"
placeholder="Enter name"
value={piezo.name}
onChange={handle.changeName}
/>
<EvoInput
className="w-36"
name="Pin"
type="number"
value={piezo.pin.toString()}
onChange={handle.pinChange}
/>
<EvoDropDown
className="w-36"
name="Beats"
items={beatsItem}
initItem={initBeatItem}
/>
<EvoInput
className="w-36"
name="Tempo"
type="number"
value={piezo.tempo.toString()}
onChange={handle.tempoChange}
/>
</div>
<div className="flex flex-row gap-3">
<Button.Secondary className="!py-0 !px-6 flex items-center gap-3">
<div className="text-sm">Export</div>
<i className="bi bi-upload text-sm"></i>
</Button.Secondary>
<Button.Primary className="!py-1 !px-8" onClick={handle.play}>
<i className="bi bi-play-fill"></i>
</Button.Primary>
</div>
</div>
<div className="relative">
<div className="v-scrollbar flex flex-row flex-nowrap gap-4 overflow-x-scroll overflow-y-hidden relative">
{piezo.notes.map((note, i) => (
<NoteItem
note={note}
index={i}
parentIndex={index}
key={i}
/>
))}
<NotePlus parentIndex={index} />
</div>
</div>
</div>
)
}
function BarPlus ({ onClick }: { onClick?: MouseEventHandler<HTMLButtonElement> }) {
return (
<button
className="bg-finn h-36 hover:bg-secondary transition col-span-2 rounded-lg border border-border flex items-center justify-center"
onClick={onClick}
>
<i className="bi bi-plus text-6xl text-border"></i>
</button>
)
}
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 (
<div className="container grid items-center relative">
<div className={`col-span-8 w-[inherit]`}>
<h2 className="text-4xl font-poppins font-bold leading-normal mb-4">
Piezo Music Editor
</h2>
<div className="grid grid-cols-8 mb-8">
<p className="col-span-6">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?</p>
</div>
<div className={`flex flex-col gap-6`}>
{piezeNotes.map((piezo, i) => (
<PiezoEditor
piezo={piezo}
index={i}
key={i}
/>
))}
<BarPlus onClick={handleAdd} />
</div>
</div>
</div>
)
}
export default ControlPiezoMusicEditor;

View File

@ -1,17 +1,23 @@
import { Dispatch, ReactNode, SetStateAction, createContext, useState } from "react"; 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 { interface ControllerContextProps {
pinModes: PinMode[]; pinModes: PinMode[];
leds: PinState[]; leds: PinState[];
rgbLed: ChannelPinState[]; rgbLed: ChannelPinState[];
piezo: DynamicPinState[]; piezo: DynamicPinState[];
piezeNotes: PiezoMusic[];
motoServo: DynamicPinState[]; motoServo: DynamicPinState[];
photoresistor: DynamicPinState[]; photoresistor: DynamicPinState[];
setPinModes?: Dispatch<SetStateAction<PinMode[]>>; setPinModes?: Dispatch<SetStateAction<PinMode[]>>;
setLeds?: Dispatch<SetStateAction<PinState[]>>; setLeds?: Dispatch<SetStateAction<PinState[]>>;
setRgbLed?: Dispatch<SetStateAction<ChannelPinState[]>>; setRgbLed?: Dispatch<SetStateAction<ChannelPinState[]>>;
setPiezo?: Dispatch<SetStateAction<DynamicPinState[]>>; setPiezo?: Dispatch<SetStateAction<DynamicPinState[]>>;
setNotes?: Dispatch<SetStateAction<PiezoMusic[]>>;
setMotoServo?: Dispatch<SetStateAction<DynamicPinState[]>>; setMotoServo?: Dispatch<SetStateAction<DynamicPinState[]>>;
setPhotoresistor?: Dispatch<SetStateAction<DynamicPinState[]>>; setPhotoresistor?: Dispatch<SetStateAction<DynamicPinState[]>>;
} }
@ -46,6 +52,10 @@ const INIT_VALUES: ControllerContextProps = {
state: 262 state: 262
} }
], ],
piezeNotes: [
ExampleMusic1,
ExampleMusic2
],
motoServo: [ motoServo: [
{ {
pin: "9", pin: "9",
@ -67,6 +77,7 @@ export function BoardControllerProvider ({ children }: { children: ReactNode })
const [leds, setLeds] = useState<PinState[]>(INIT_VALUES.leds); const [leds, setLeds] = useState<PinState[]>(INIT_VALUES.leds);
const [rgbLed, setRgbLed] = useState<ChannelPinState[]>(INIT_VALUES.rgbLed); const [rgbLed, setRgbLed] = useState<ChannelPinState[]>(INIT_VALUES.rgbLed);
const [piezo, setPiezo] = useState<DynamicPinState[]>(INIT_VALUES.piezo); const [piezo, setPiezo] = useState<DynamicPinState[]>(INIT_VALUES.piezo);
const [piezeNotes, setNotes] = useState<PiezoMusic[]>(INIT_VALUES.piezeNotes);
const [motoServo, setMotoServo] = useState<DynamicPinState[]>(INIT_VALUES.motoServo); const [motoServo, setMotoServo] = useState<DynamicPinState[]>(INIT_VALUES.motoServo);
const [photoresistor, setPhotoresistor] = useState<DynamicPinState[]>(INIT_VALUES.photoresistor); const [photoresistor, setPhotoresistor] = useState<DynamicPinState[]>(INIT_VALUES.photoresistor);
@ -75,6 +86,7 @@ export function BoardControllerProvider ({ children }: { children: ReactNode })
leds, setLeds, leds, setLeds,
rgbLed, setRgbLed, rgbLed, setRgbLed,
piezo, setPiezo, piezo, setPiezo,
piezeNotes, setNotes,
motoServo, setMotoServo, motoServo, setMotoServo,
photoresistor, setPhotoresistor photoresistor, setPhotoresistor
}; };

View File

@ -1,5 +1,5 @@
import axios from "axios"; import axios from "axios";
import { ChannelPinState } from "../types/board"; import { ChannelPinState, PiezoMusic } from "../types/board";
import { io } from "../socket/socket.io"; import { io } from "../socket/socket.io";
const url = "http://localhost:3000/api-arduino"; const url = "http://localhost:3000/api-arduino";
@ -46,6 +46,18 @@ export async function PatchPiezo (pin: number, freq: number) {
console.log("Piezo Response: ", data); 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) { export async function PatchServo(pin: string | number, value: number) {
io.emit("servo", pin, value); io.emit("servo", pin, value);
} }

View File

@ -18,6 +18,16 @@
input[type=number] { 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;
}
} }
@keyframes size-in { @keyframes size-in {

91
src/data/melodies.ts Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -1,6 +1,11 @@
import { useContext } from "react"; import { useContext } from "react";
import { BoardControllerContext } from "../contexts/BoardController"; 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 () { export function usePin () {
return useContext(BoardControllerContext); 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 () { export function useServo () {
const { motoServo, setMotoServo } = useContext(BoardControllerContext); const { motoServo, setMotoServo } = useContext(BoardControllerContext);

View File

@ -14,6 +14,7 @@ import ControlPhotoresistor from "../components/landing/ControlPhotoresistor";
import ControlServo from "../components/landing/ControlServo"; import ControlServo from "../components/landing/ControlServo";
import Closing from "../components/landing/Closing"; import Closing from "../components/landing/Closing";
import Footer from "../components/Footer"; import Footer from "../components/Footer";
import ControlPiezoMusicEditor from "../components/landing/ControlPiezoMusicEditor";
function MainPage () { function MainPage () {
const led = useRef<HTMLDivElement | null>(null); const led = useRef<HTMLDivElement | null>(null);
@ -32,6 +33,7 @@ function MainPage () {
<ControlLED refto={led} /> <ControlLED refto={led} />
<ControlRgbLed refto={rgbLed} /> <ControlRgbLed refto={rgbLed} />
<ControlPiezo refto={piezo} /> <ControlPiezo refto={piezo} />
<ControlPiezoMusicEditor />
<ControlServo refto={servo} /> <ControlServo refto={servo} />
<ControlPhotoresistor refto={photoresistor} /> <ControlPhotoresistor refto={photoresistor} />
<Closing /> <Closing />

View File

@ -18,3 +18,11 @@ export interface ChannelPinState {
green: PinState, green: PinState,
blue: PinState blue: PinState
} }
export interface PiezoMusic {
name: string,
pin: number | string;
notes: string[];
beats: number;
tempo: number;
}