Skip to content

Commit 76a9058

Browse files
authored
Merge pull request #603 from ryohey/add-control-mark
Show program changes on the piano roll and enable adding program changes mid-performance
2 parents c01cdc1 + 2c1d916 commit 76a9058

File tree

19 files changed

+517
-319
lines changed

19 files changed

+517
-319
lines changed

app/src/actions/track.ts

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import {
2-
BatchUpdateOperation,
2+
type BatchUpdateOperation,
3+
getProgramNumberEvent,
4+
isProgramChangeEvent,
35
Measure,
46
programChangeMidiEvent,
5-
timeSignatureMidiEvent,
67
TrackEvent,
78
TrackEventOf,
89
TrackId,
10+
timeSignatureMidiEvent,
911
} from "@signal-app/core"
10-
import { AnyChannelEvent, AnyEvent, SetTempoEvent } from "midifile-ts"
12+
import type {
13+
AnyChannelEvent,
14+
AnyEvent,
15+
ProgramChangeEvent,
16+
SetTempoEvent,
17+
} from "midifile-ts"
1118
import { useCallback } from "react"
1219
import { ValueEventType } from "../entities/event/ValueEventType"
1320
import { addedSet, deletedSet } from "../helpers/set"
@@ -165,7 +172,7 @@ export const useMuteNote = () => {
165172

166173
return useCallback(
167174
(noteNumber: number) => {
168-
if (channel == undefined) {
175+
if (channel === undefined) {
169176
return
170177
}
171178
stopNote({ channel, noteNumber })
@@ -190,23 +197,89 @@ export const useSetTrackName = () => {
190197
)
191198
}
192199

193-
export const useSetTrackInstrument = (trackId: TrackId) => {
194-
const { sendEvent } = usePlayer()
200+
export const useSetTrackInstrument = (trackId: TrackId, eventId?: number) => {
201+
const { sendEvent, position } = usePlayer()
195202
const { pushHistory } = useHistory()
196-
const { channel, setProgramNumber } = useTrack(trackId)
203+
const { channel, getEvents, updateEvent, addEvent } = useTrack(trackId)
197204

198205
return useCallback(
199206
(programNumber: number) => {
200207
pushHistory()
201-
setProgramNumber(programNumber)
202208

203-
// 即座に反映する
204-
// Reflect immediately
205-
if (channel !== undefined) {
206-
sendEvent(programChangeMidiEvent(0, channel, programNumber))
209+
let targetEventId: number | undefined = eventId
210+
211+
if (eventId === undefined) {
212+
// get last program change event before position
213+
const programNumberEvent =
214+
getProgramNumberEvent(getEvents(), position) ??
215+
addEvent<TrackEventOf<ProgramChangeEvent>>({
216+
...programChangeMidiEvent(0, 0, programNumber),
217+
tick: 0,
218+
})
219+
targetEventId = programNumberEvent?.id
220+
}
221+
222+
if (targetEventId === undefined) {
223+
return
224+
}
225+
226+
const targetEvent = updateEvent<TrackEventOf<ProgramChangeEvent>>(
227+
targetEventId,
228+
{
229+
value: programNumber,
230+
},
231+
)
232+
233+
if (targetEvent === null) {
234+
return
235+
}
236+
237+
const tick = targetEvent.tick
238+
239+
// If the player position is after the insertion position and there are no other program change events, reflect immediately
240+
if (channel !== undefined && position >= tick) {
241+
const hasOtherProgramChangeEvents = getEvents()
242+
.filter(isProgramChangeEvent)
243+
.some((e) => e.tick > tick)
244+
if (!hasOtherProgramChangeEvents) {
245+
sendEvent(programChangeMidiEvent(0, channel, programNumber))
246+
}
207247
}
208248
},
209-
[pushHistory, setProgramNumber, channel, sendEvent],
249+
[
250+
pushHistory,
251+
channel,
252+
sendEvent,
253+
position,
254+
getEvents,
255+
eventId,
256+
updateEvent,
257+
addEvent,
258+
],
259+
)
260+
}
261+
262+
export const useInsertTrackInstrument = (trackId: TrackId) => {
263+
const { sendEvent } = usePlayer()
264+
const { pushHistory } = useHistory()
265+
const { channel, addEvent } = useTrack(trackId)
266+
267+
return useCallback(
268+
(programNumber: number, tick: number) => {
269+
if (channel === undefined) {
270+
return
271+
}
272+
273+
pushHistory()
274+
275+
addEvent<TrackEventOf<ProgramChangeEvent>>({
276+
...programChangeMidiEvent(0, 0, programNumber),
277+
tick,
278+
})
279+
280+
sendEvent(programChangeMidiEvent(0, channel, programNumber))
281+
},
282+
[pushHistory, channel, sendEvent, addEvent],
210283
)
211284
}
212285

app/src/components/InstrumentBrowser/InstrumentBrowser.tsx

Lines changed: 125 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import styled from "@emotion/styled"
2-
import { FC } from "react"
2+
import type { CheckedState } from "@radix-ui/react-checkbox"
3+
import type { TrackEventOf, TrackId } from "@signal-app/core"
4+
import type { ProgramChangeEvent } from "midifile-ts"
5+
import { type FC, useCallback, useEffect, useMemo, useState } from "react"
36
import { useInstrumentBrowser } from "../../hooks/useInstrumentBrowser"
7+
import { useTrack } from "../../hooks/useTrack"
48
import { Localized } from "../../localize/useLocalization"
59
import { Dialog, DialogActions, DialogContent } from "../Dialog/Dialog"
610
import { InstrumentName } from "../TrackList/InstrumentName"
711
import { Button, PrimaryButton } from "../ui/Button"
812
import { Checkbox } from "../ui/Checkbox"
13+
import { DropdownButton } from "../ui/DropdownButton"
914
import { Label } from "../ui/Label"
1015
import { DrumKitCategoryName, FancyCategoryName } from "./CategoryName"
1116
import { SelectBox } from "./SelectBox"
@@ -36,18 +41,84 @@ const Footer = styled.div`
3641
margin-top: 1rem;
3742
`
3843

39-
export const InstrumentBrowser: FC = () => {
44+
export interface InstrumentBrowserProps {
45+
isOpen: boolean
46+
onOpenChange: (open: boolean) => void
47+
trackId: TrackId
48+
targetEventId?: number
49+
showInsertButton?: boolean
50+
}
51+
52+
export const InstrumentBrowser: FC<InstrumentBrowserProps> = ({
53+
isOpen,
54+
onOpenChange,
55+
trackId,
56+
targetEventId,
57+
showInsertButton = false,
58+
}) => {
59+
const {
60+
programNumber: initialProgramNumber,
61+
isRhythmTrack: initialIsRhythmTrack,
62+
getEventById,
63+
removeEvent,
64+
} = useTrack(trackId)
65+
const [setting, setSetting] = useState({
66+
programNumber: initialProgramNumber ?? 0,
67+
isRhythmTrack: initialIsRhythmTrack ?? false,
68+
})
69+
const { programNumber, isRhythmTrack } = setting
4070
const {
41-
isOpen,
42-
setOpen,
43-
setting: { programNumber, isRhythmTrack },
4471
selectedCategoryIndex,
4572
categoryFirstProgramEvents,
4673
categoryInstruments,
47-
onChangeInstrument: onChange,
74+
insertInstrumentChangeAtCurrentPosition,
75+
changeInstrument,
4876
onClickOK,
49-
onChangeRhythmTrack,
50-
} = useInstrumentBrowser()
77+
changeRhythmTrack,
78+
} = useInstrumentBrowser(setting, targetEventId)
79+
80+
const targetEvent = useMemo(() => {
81+
if (targetEventId !== undefined) {
82+
return getEventById(targetEventId) as
83+
| TrackEventOf<ProgramChangeEvent>
84+
| undefined
85+
}
86+
return undefined
87+
}, [targetEventId, getEventById])
88+
89+
useEffect(() => {
90+
if (isOpen) {
91+
if (targetEvent) {
92+
setSetting({
93+
programNumber: targetEvent.value,
94+
isRhythmTrack: initialIsRhythmTrack ?? false,
95+
})
96+
}
97+
}
98+
}, [isOpen, targetEvent, initialIsRhythmTrack])
99+
100+
const onChange = useCallback(
101+
(programNumber: number) => {
102+
setSetting({
103+
programNumber,
104+
isRhythmTrack,
105+
})
106+
changeInstrument(programNumber)
107+
},
108+
[isRhythmTrack, changeInstrument],
109+
)
110+
111+
const handleChangeRhythmTrack = useCallback(
112+
(state: CheckedState) => {
113+
const isRhythmTrack = state === true
114+
setSetting({
115+
programNumber: 0, // reset program number when changing rhythm track
116+
isRhythmTrack,
117+
})
118+
changeRhythmTrack(isRhythmTrack)
119+
},
120+
[changeRhythmTrack],
121+
)
51122

52123
const categoryOptions = categoryFirstProgramEvents.map((preset, i) => ({
53124
value: i,
@@ -63,8 +134,25 @@ export const InstrumentBrowser: FC = () => {
63134
label: <InstrumentName programNumber={p} isRhythmTrack={isRhythmTrack} />,
64135
}))
65136

137+
const handleClickOK = useCallback(() => {
138+
onClickOK()
139+
onOpenChange(false)
140+
}, [onClickOK, onOpenChange])
141+
142+
const handleClickInsert = useCallback(() => {
143+
insertInstrumentChangeAtCurrentPosition(programNumber)
144+
onOpenChange(false)
145+
}, [onOpenChange, insertInstrumentChangeAtCurrentPosition, programNumber])
146+
147+
const handleClickDelete = useCallback(() => {
148+
if (targetEventId !== undefined) {
149+
removeEvent(targetEventId)
150+
}
151+
onOpenChange(false)
152+
}, [targetEventId, removeEvent, onOpenChange])
153+
66154
return (
67-
<Dialog open={isOpen} onOpenChange={setOpen}>
155+
<Dialog open={isOpen} onOpenChange={onOpenChange}>
68156
<DialogContent className="InstrumentBrowser">
69157
<Finder>
70158
<Left>
@@ -91,18 +179,41 @@ export const InstrumentBrowser: FC = () => {
91179
<Footer>
92180
<Checkbox
93181
checked={isRhythmTrack}
94-
onCheckedChange={(state) => onChangeRhythmTrack(state === true)}
182+
onCheckedChange={handleChangeRhythmTrack}
95183
label={<Localized name="rhythm-track" />}
96184
/>
97185
</Footer>
98186
</DialogContent>
99187
<DialogActions>
100-
<Button onClick={() => setOpen(false)}>
188+
{targetEventId !== undefined && (
189+
<Button
190+
onClick={handleClickDelete}
191+
style={{ marginRight: "auto" }}
192+
>
193+
<Localized name="delete" />
194+
</Button>
195+
)}
196+
<Button onClick={() => onOpenChange(false)}>
101197
<Localized name="cancel" />
102198
</Button>
103-
<PrimaryButton onClick={onClickOK}>
104-
<Localized name="ok" />
105-
</PrimaryButton>
199+
{!showInsertButton && (
200+
<PrimaryButton onClick={handleClickOK}>
201+
<Localized name="ok" />
202+
</PrimaryButton>
203+
)}
204+
{showInsertButton && (
205+
<DropdownButton
206+
onClick={handleClickOK}
207+
actions={[
208+
{
209+
label: "Insert",
210+
onClick: handleClickInsert,
211+
},
212+
]}
213+
>
214+
<Localized name="ok" />
215+
</DropdownButton>
216+
)}
106217
</DialogActions>
107218
</Dialog>
108219
)

app/src/components/PianoRoll/ControlMark.tsx

Lines changed: 0 additions & 61 deletions
This file was deleted.

0 commit comments

Comments
 (0)