Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using web worker to keep playing when window is not focused #69

Closed
Boscop opened this issue Nov 4, 2022 · 4 comments
Closed

Using web worker to keep playing when window is not focused #69

Boscop opened this issue Nov 4, 2022 · 4 comments

Comments

@Boscop
Copy link

Boscop commented Nov 4, 2022

Just an idea, maybe it's out of scope for this library, so feel free to close this :)

Both setInterval and requestAnimationFrame have issues for midi playback, they both don't get called often enough when the window is not focused. (requestAnimationFrame doesn't get called at all in chrome, and setInterval gets called only once per second or so.)
This is really annoying when switching from the midi-playing/looping browser window to the DAW where the live midi is being processed, it really disturbs the whole workflow :/

Web workers allow running a loop in a separate thread that runs even when the window isn't focused:
https://mortenson.coffee/blog/making-multi-track-tape-recorder-midi-javascript/

But setInterval is extremely unreliable, especially as users change tabs and reallocate resources. To make things more consistent, I created a new Web Worker just for the timing code so that it runs in its own thread. Web Workers also have more consistent performance when the page doesn't have focus.

In the above blog post, the author had good results with a web worker. But I would write the tick() function differently:
Instead of hoping that the time between calls stays constant, and trying to adjust the next timeout value based on deviation, I would use performance.now() to calculate the number of milliseconds since the last call, and then calculate how many ticks passed based on that, also storing the fractional tick remainder to add it to the number of ticks for the next frame, like this:

pub fn frame(&mut self, timestamp: DOMHighResTimeStamp) {
	let start_timestamp = *self.m_start_timestamp.get_or_insert(timestamp);
	if let Some(prev) = self.m_prev_timestamp && prev < timestamp {
		let elapsed_since_prev_ms = timestamp - prev;

		// Sent midi beat clock 24 times per beat
		let cur_midi_beat_clock_frame = (self.playhead_tick as u64 * 24 / self.smf.ticks_per_beat as u64) as i32;
		if self.clock_enabled {
			// Potentially send multiple clock msgs per step if many ticks passed since last call
			for _ in self.midi_beat_clock_frame .. cur_midi_beat_clock_frame {
				// Send clock msg
				send_live(&self.midi_output, LiveEvent::Realtime(SystemRealtime::TimingClock));
			}
		}
		self.midi_beat_clock_frame = cur_midi_beat_clock_frame;

		let timeframe = self
			.timeframe_fract
			.process(elapsed_since_prev_ms as f64 * self.bpm * (self.smf.ticks_per_beat as f64 / (60. * 1000.)));

		let events = self.smf.events_window(self.playhead_tick, timeframe);
		self.playhead_tick += timeframe;
		self.callback.emit(PlayerStateChange::Playhead(self.playhead_tick));

		for event in events {
			// Send event to midi output
			match event.event {
				PlayerEvent::LiveEvent(e) => {
					send_live(&self.midi_output, e);
				}
				PlayerEvent::Bpm(bpm) => {
					self.bpm = bpm;
				}
			}
		}

		if let Some(last) = self.smf.events.last() && last.time < self.playhead_tick {
			// Finished playing
		}
	}
	self.m_prev_timestamp = Some(timestamp);
}

timeframe_fract is of type TimeframeWithFract to account for fractional tick remainder, defined like this:

// Return int timeframe but add unused fractional part to next frame's timeframe
pub struct TimeframeWithFract {
	fract: f64,
}

impl TimeframeWithFract {
	pub fn process(&mut self, timeframe: f64) -> PlayerTickTime {
		let timeframe_f = timeframe + self.fract;
		let timeframe_i = timeframe_f as PlayerTickTime;
		self.fract = timeframe_f - timeframe_i as f64;
		timeframe_i
	}
}

What do you think? :)

If it's out of scope for this library, would it be possible to let user code handle the timing, so that a user can write their own web worker which sends midi messages to the JZZ player in the main app (currently the Web Midi API is not accessible for web workers, but they can be used for timing, but they have to send messages to the main app which can then send midi out.
If I wanted to do this with JZZ, would it be possible with the JZZ GUI Player? So that I can have its GUI functionality and API, but let the web worker do the timing. Like inversion of control, my app would call JZZ to advance its state, instead of JZZ using setInterval, would that be possible somehow? :)
I'm asking because this issue is very important to me, I really need midi playback continuing when working in the DAW while the browser window is not focused.

@Boscop
Copy link
Author

Boscop commented Nov 4, 2022

Actually there seems to be a way to run a constant FPS loop without web worker, using the Web Audio clock:
https://github.com/sebpiq/WAAClock#waaclockjs

const audioContext = new AudioContext();
const clock = new WAAClock(audioContext);
clock.start();
const event = clock.callbackAtTime(function() { console.log('step') }, 0).repeat(1./60.); // Repeat at 60 FPS

// ...

event.clear();

It seems it can be used as a drop-in replacement for setInterval for any interval, not just audio related things.


Also interesting blog posts here:
https://loophole-letters.vercel.app/web-audio-scheduling (Testing different approaches.)
https://sonoport.github.io/web-audio-clock.html
https://web.dev/audio-scheduling

And this lib (mentioned here: sebpiq/WAAClock#20 (comment)) seems promising:
https://github.com/chrisguttandin/worker-timers

A replacement for setInterval() and setTimeout() which works in unfocused windows.

Exactly what's needed :)

@jazz-soft
Copy link
Owner

Thank you! That's a nice feature to implement when I have more time.

@jazz-soft
Copy link
Owner

Using Web Worker seems to be the most reliable solution.
Please try if the latest release JZZ v1.5.4 works fine for you.
And thanks again for the links, they were extremely helpful!

@jazz-soft
Copy link
Owner

A minor bug fixed in v1.5.5.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants