Sunday, October 26, 2025
HomeJavaScriptField Respiration Train With SpeechSynthesis And Alpine.js

Field Respiration Train With SpeechSynthesis And Alpine.js


As we close to the top of InVision, I have been feeling quite a lot of nervousness. I am not one for meditation; however, I do like the thought of respiration workout routines to assist calm a racing thoughts. I lately watched a YouTube video about “field respiration” by which a cycle of respiration has 4 phases—in, maintain, out, maintain—every of which is carried out for 4-seconds. I like to shut my eyes when respiration; so, I wished to see if I may use the SpeechSynthesis API to create a guided meditation with Alpine.js.

Run this demo in my JavaScript Demos undertaking on GitHub.

View this code in my JavaScript Demos undertaking on GitHub.

The only technique to method this could have been to create a single queue of “utterances” that repeat over-and-over once more. And, if I used to be solely going to shut my eyes, that will be fantastic. However, for funzies I additionally wished to create a small visible expertise to work alongside the auditory expertise.

To that finish, I am defining the train utilizing “phases”, every of which accommodates plenty of “phrases” to be spoken:

var phases = [
	[ "In",   "two", "three", "four" ],
	[ "Hold", "two", "three", "four" ],
	[ "Out",  "two", "three", "four" ],
	[ "Hold", "two", "three", "four" ]
];

Throughout the bounds of a single section, the rendered textual content would be the aggregation of all of the textual content that is already been rendered to the display screen. So, the primary rendered textual content might be:

In

… after which:

In ...two

… after which:

In ...two ...three

.. after which:

In ...two ...three ...4

Then, the textual content will clear and the subsequent section will begin from scratch.

To make this straightforward to render, I flatten the phases down right into a materialized set of states. Every state accommodates the combination textual content and the SpeechSynthesisUtterance occasion that may handed to the SpeechSynthesis API interface for vocalization:

var states = phases.flatMap(
	( section ) => {

		return section.map(
			( time period, i ) => {

				// As we proceed throughout the phrases in every section, the textual content will
				// be the aggregation of the earlier textual content already rendered in
				// the identical section.
				var textual content = section
					.slice( 0, ( i + 1 ) )
					.be a part of( " ..." )
				;

				// Be aware: the voice for the utterance might be set simply previous to
				// every vocalization. This manner it's going to all the time replicate what's
				// at the moment within the choose menu.
				return {
					time period: time period,
					textual content: textual content,
					utterance: new SpeechSynthesisUtterance( time period ),
					period: 1000
				};

			}
		);

	}
);

Discover that every state has a period property. That is the variety of milliseconds that the state might be rendered to the web page earlier than the subsequent state is processed (and the subsequent utterance is vocalized). This might be dealt with by a setTimeout().

On this code kata, a number of the state must reactive (ie, the Doc Object Mannequin must be up to date in response to state modifications); however, quite a lot of the state will be personal and is barely wanted to drive the interior state.

In relation to Alpine.js ergonomics, this will get a bit difficult. Alpine.js form of needs all the things to be reactive; or, somewhat, it needs all the things to be a part of the reactive Proxy that it generates internally. Since all reactive properties are uncovered on the this binding, even personal strategies must be on the reactive Proxy in order that they will entry (and mutate) the reactive state.

This results in an uneven code design the place half of the variables are bare and half of the variables are certain to this. To be clear, this is not a technical downside, it is extra of an aesthetic downside. Mixing-and-matching completely different entry patterns simply feels icky.

If it ever bothers me an excessive amount of, I can simply transfer all references into the reactive Proxy and never fear about the truth that half of them will not truly be referenced within the DOM. However, for now, I will go away half of the variables as personal references (accessible solely through closures) and half of the variables as public state.

That is illustrated clearly within the inner processQueue() technique by which most references are bare and solely a handful of this bindings are referenced. The processQueue() technique is the place a person state is plucked type the state queue and is rendered / vocalized:

perform processQueue() {

	// Reset queue when a brand new iteration is being began.
	if ( ! queue.size ) {

		queue = states.slice();
		this.iteration++;

	}

	var state = queue.shift();
	// Replace the utterance to all the time use the voice that is at the moment chosen.
	// This manner, the person can change the voice throughout vocalization to search out one
	// that's the most comfy.
	state.utterance.voice = this.voices[ this.selectedVoiceIndex ];
	state.utterance.pitch = 0;
	state.utterance.fee = 0.7;

	synth.converse( state.utterance );
	this.textual content = state.textual content;

	timer = setTimeout(
		() => {

			this._processQueue();

		},
		state.period
	);

}

This processQueue() technique, which has been prefixed with an _ on the reactive Proxy with a view to be “marked as personal”, calls itself recursively utilizing the state period. It’s going to hold operating perpetually, refilling the queue as crucial, till the timer is cleared.

The visualization of this code kata seems like this (the audio will be heard within the video above):

Screen recording on box breathing app using Alpine.js

And, here is the total code:

<!doctype html>
<html lang="en">
<head>
	<hyperlink rel="stylesheet" sort="textual content/css" href="https://www.bennadel.com/weblog/./major.css" />
	<script sort="textual content/javascript" src="../../vendor/alpine/3.13.5/alpine.3.13.5.min.js" defer></script>
</head>
<physique>

	<h1>
		Field Respiration Train With SpeechSynthesis And Alpine.js
	</h1>

	<part x-data="Demo" :hidden="( ! voices.size )">

		<div class="type">
			<choose x-model.quantity="selectedVoiceIndex">
				<template x-for="( voice, index ) in voices" :key="index">
					<possibility
						:worth="index"
						x-text="voice.identify">
					</possibility>
				</template>
			</choose>

			<button @click on="begin()">
				Begin
			</button>
			<button @click on="cease()">
				Cease
			</button>
		</div>

		<p class="textual content" :hidden="( ! textual content )">
			[<span x-text="iteration"></span>]:
			<span x-text="textual content"></span>
		</p>

	</part>

	<script sort="textual content/javascript">

		perform Demo() {

			// Field respiration consists of 4 phases: in, maintain, out, maintain. Every section
			// lasts 4-seconds; and every time period beneath might be spoken at a 1-second interval.
			var phases = [
				[ "In",   "two", "three", "four" ],
				[ "Hold", "two", "three", "four" ],
				[ "Out",  "two", "three", "four" ],
				[ "Hold", "two", "three", "four" ]
			];

			// Flatten the phases right into a single set of states. This can make it simpler to
			// course of; and, permits us to materialize some state that in any other case could be
			// tougher to calculate on the fly (ex, the textual content to output).
			var states = phases.flatMap(
				( section ) => {

					return section.map(
						( time period, i ) => {

							// As we proceed throughout the phrases in every section, the textual content will
							// be the aggregation of the earlier textual content already rendered in
							// the identical section.
							var textual content = section
								.slice( 0, ( i + 1 ) )
								.be a part of( " ..." )
							;

							// Be aware: the voice for the utterance might be set simply previous to
							// every vocalization. This manner it's going to all the time replicate what's
							// at the moment within the choose menu.
							return {
								time period: time period,
								textual content: textual content,
								utterance: new SpeechSynthesisUtterance( time period ),
								period: 1000
							};

						}
					);

				}
			);

			// As soon as the timer is began, this queue will maintain the states to be processed.
			// And the timer will maintain the delay between every utterance.
			var queue = [];
			var timer = null;

			// Brief-hand reference.
			var synth = window.speechSynthesis;

			// The difficult factor with Alpine.js is that the item returned from the
			// element turns into the hook for reactivity. Alpine.js creates a Proxy that
			// wraps the given information and updates the DOM when the values are mutated. This
			// makes it a bit difficult to create a separation between public and
			// personal properties / strategies. On this case, I've to incorporate the personal
			// strategies on the return worth in order that they will entry the suitable `this`
			// reference for subsequent reactivity. To assist implement the "personal" nature
			// of the strategies, I am aliasing them with a "_" prefix.
			return {
				// Public reactive properties.
				voices: synth.getVoices(),
				selectedVoiceIndex: -1,
				textual content: "",
				iteration: 0,

				// Public strategies.
				init: $init,
				begin: begin,
				cease: cease,

				// Non-public strategies.
				_processQueue: processQueue,
				_setVoices: setVoices
			};

			// ---
			// PUBLIC METHODS.
			// ---

			/**
			* I initialize the Alpine element.
			*/
			perform $init() {

				// Voices aren't out there on web page prepared. As an alternative, now we have to bind to the
				// voiceschanged occasion after which setup the view-model as soon as they grow to be
				// out there on the SpeechSynthesis API.
				synth.addEventListener(
					"voiceschanged",
					( occasion ) => {

						this._setVoices();

					}
				);

			}

			/**
			* I begin the vocalization of the guided field respiration.
			*/
			perform begin() {

				if ( ! this.voices.size ) {

					console.warn( "No voices have been loaded but." );
					return;

				}

				queue = states.slice();
				this.iteration = 1;
				this.textual content = "";
				this._processQueue();

			}

			/**
			* I cease the vocalization of the guided field respiration.
			*/
			perform cease() {

				clearInterval( timer );
				this.iteration = 0;
				this.textual content = "";

			}

			// ---
			// PRIVATE METHODS.
			// ---

			/**
			* I course of the queue, vocalizing the subsequent state. This technique will name itself
			* recursively (through setTimeout).
			*/
			perform processQueue() {

				// Reset queue when a brand new iteration is being began.
				if ( ! queue.size ) {

					queue = states.slice();
					this.iteration++;

				}

				var state = queue.shift();
				// Replace the utterance to all the time use the voice that is at the moment chosen.
				// This manner, the person can change the voice throughout vocalization to search out one
				// that's the most comfy.
				state.utterance.voice = this.voices[ this.selectedVoiceIndex ];
				state.utterance.pitch = 0;
				state.utterance.fee = 0.7;

				synth.converse( state.utterance );
				this.textual content = state.textual content;

				timer = setTimeout(
					() => {

						this._processQueue();

					},
					state.period
				);

			}

			/**
			* I set the voices based mostly on the present synth state.
			*/
			perform setVoices() {

				// There are TONS of voices, however solely a handful of them appear to create a
				// cheap expertise. That is in all probability very particular to every browser
				// or laptop; however, I will filter-down to those I like.
				this.voices = synth.getVoices().filter(
					( voice ) => {

						swap ( voice.identify.toLowerCase() ) {
							case "alex":
							case "alva":
							case "damayanti":
							case "daniel":
							case "fiona":
							case "fred":
							case "karen":
							case "mei-jia":
							case "melina":
							case "moira":
							case "rishi":
							case "samantha":
							case "tessa":
							case "veena":
							case "victoria":
							case "yuri":
								return true;
							break;
						}

						return false;

					}
				);

				// Default to essentially the most pleasing if it exists.
				this.selectedVoiceIndex = this.voices.findIndex(
					( voice ) => {

						return ( voice.identify === "Tessa" );

					}
				);

				// If the popular voice would not exist, simply use the primary one.
				if ( this.selectedVoiceIndex === -1 ) {

					this.selectedVoiceIndex = 0;

				}

			}

		}

	</script>

</physique>
</html>

Anyway, I am not gonna say an excessive amount of in regards to the SpeechSynthesis API since I do not know that a lot about it. This was only a enjoyable little Alpine.js code kata.

Wish to use code from this publish?
Try the license.


https://bennadel.com/4743

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments