I am currently building a piano with visualizer in React. The users can record their song and play it back. This recording records a list of notes + start/end timestamps. I currently have note visualizations that start from the top of the screen and expand their height for end timestamp - start timestamp. Then the note falls down from the top until it is offscreen and is deleted. Both of these are done using animations with fill forward.
My problem is: I need the note sound to play when the visualizations hit the keys, and currently, basing this off of a timeout is causing me issues. It would make a lot more sense to me to base this sound off of the animated visualization hitting the key div but I am not sure how to do this.
Additionally, storing the animations for being able to pause and resume is causing me a lot of issues and I was wondering if there was a more concrete method of doing this. Pausing and resuming the animations is really finicky. I do not want to save it as a video.
Animation
export const attribute_animation = (object, attribute, start, end, duration, easing) => {
return object.animate(
[{[attribute]: start}, { [attribute]: end}],
{duration: duration, fill: 'forwards', easing: easing}
)
}
I animate first expanding the height attr, then the top attr. How can I have a specific div with ref recognize if it is hitting another div? (visualization hitting key)
Piano Component
let [playing, set_playing] = useState(null)
let start_note_index = useRef(0)
let end_note_index = useRef(0)
let song_player = useRef(null)
let sp_start_time = useRef(null)
let sp_anim_frameid = useRef(null)
function song_playback() {
function animate(timestamp) {
if (!sp_start_time.current) {
sp_start_time.current = timestamp
}
if (start_note_index.current < song.length &&
song[start_note_index.current]['note']['start_timestamp'] <= timestamp - sp_start_time.current) {
let pkey_index = pkey_to_pkeyind[song[start_note_index.current]['note']['note']]
set_piano(prev_state => {
const keys = [...prev_state]
keys[pkey_index] = React.cloneElement(keys[pkey_index], { pb_visual_mode: 'expand_down' })
return keys
})
start_note_index.current += 1
}
if (end_note_index.current < song.length &&
song[end_note_index.current]['note']['end_timestamp'] <= timestamp - sp_start_time.current) {
let pkey_index = pkey_to_pkeyind[song[end_note_index.current]['note']['note']]
set_piano(prev_state => {
const keys = [...prev_state]
keys[pkey_index] = React.cloneElement(keys[pkey_index], { pb_visual_mode: 'move_down' })
return keys
})
end_note_index.current += 1
}
if (start_note_index.current >= song.length && end_note_index.current >= song.length) {
set_playing(null)
cancelAnimationFrame(sp_anim_frameid.current)
} else {
if (playing === 'playing') {
sp_anim_frameid.current = requestAnimationFrame(animate)
}
}
}
function pause() {
cancelAnimationFrame(sp_anim_frameid.current)
set_piano(prev_state => {
const keys = [...prev_state]
for (let i = 0; i < keys.length; i++) {
keys[i] = React.cloneElement(keys[i], { pb_visual_mode: 'pause' })
}
return keys
})
}
function resume() {
requestAnimationFrame(animate)
set_piano(prev_state => {
const keys = [...prev_state]
for (let i = 0; i < keys.length; i++) {
keys[i] = React.cloneElement(keys[i], { pb_visual_mode: 'resume' })
}
return keys
})
}
function reset() {
sp_start_time.current = null
start_note_index.current = 0
end_note_index.current = 0
}
return { pause, resume, reset }
}
song_player.current = song_playback()
}, [song, playing])
Piano Key Component
let [playback_visuals, set_playback_visuals] = useState([])
let pb_counter = useRef(0)
let curr_pb_anim = useRef([[], true])
let playback_visual_refs = useRef({})
let timeouts = useRef({})
let timeouts_counter = useRef(0)
useEffect(() => {
if (playback_visuals[pb_counter.current] && curr_pb_anim.current[1]) {
curr_pb_anim.current[0].push(attribute_animation(playback_visual_refs.current[pb_counter.current], 'height', '0', '300000px', 1000000))
curr_pb_anim.current[1] = false
}
}, [playback_visuals])
useEffect(() => {
if (pb_visual_mode === 'expand_down') {
let curr_counter = timeouts_counter.current
const timeout_id = new Timer((curr_counter) => {
audio.current.play()
delete timeouts.current[curr_counter]
}, 2000, curr_counter)
timeouts.current[timeouts_counter.current] = timeout_id
timeouts_counter.current += 1
set_playback_visuals(prev_state => {
curr_pb_anim.current[1] = true
return ({...prev_state,
[pb_counter.current]: (
<div key={`${pb_counter.current}`} ref={ref => playback_visual_refs.current[pb_counter.current] = ref} className='pb-visualizer-instance'></div>
)
})
})
} else if (pb_visual_mode === 'move_down' && curr_pb_anim.current[0]) {
console.log(curr_pb_anim.current, note)
curr_pb_anim.current[0][curr_pb_anim.current[0].length-1].pause()
curr_pb_anim.current[0][curr_pb_anim.current[0].length-1] = attribute_animation(playback_visual_refs.current[pb_counter.current], 'top', '0', '300000px', 1000000, 'linear')
curr_pb_anim.current[1] = true
let curr_t_counter = timeouts_counter.current
let curr_pb_counter = pb_counter.current
const timeout_id = new Timer((curr_t_counter, curr_pb_counter) => {
delete timeouts.current[curr_t_counter]
delete playback_visual_refs.current[curr_pb_counter]
delete curr_pb_anim.current[curr_pb_counter]
set_playback_visuals(prev_state => {
const new_state = Object.keys(prev_state).filter(key => key !== curr_pb_counter).reduce((acc, key) => {
acc[key] = prev_state[key]
return acc
}, {})
return new_state
})
}, 3000, curr_t_counter, curr_pb_counter)
timeouts.current[timeouts_counter.current] = timeout_id
timeouts_counter.current += 1
pb_counter.current += 1
} else if (pb_visual_mode === 'pause') {
for (let i = 0; i < curr_pb_anim.current[0].length; i++) {
console.log("pause", curr_pb_anim.current, note)
curr_pb_anim.current[0][i].pause()
}
for (let timer_key in timeouts.current) {
timeouts.current[timer_key].pause()
}
} else if (pb_visual_mode == 'resume') {
for (let i = 0; i < curr_pb_anim.current[0].length; i++) {
console.log("resume", curr_pb_anim.current, note)
curr_pb_anim.current[0][i].play()
}
for (let timer_key in timeouts.current) {
timeouts.current[timer_key].resume()
}
}
}, [pb_visual_mode])
I experimented with observer API and setting the rootMargin to line up with the piano keys, but couldnt get it working. What I ended up doing was just doing the math to scale with the window height. Here is my code in the case anyone ends up coding something like this: