PBToolboxAI v4 ← Site

speechout — n_pbt_speechout #

← Component reference · Guide contents

Reading aloud: the application reads a text sentence by sentence and tells you where it has got to. No third-party service, no API key — the synthesis is the workstation's own.

▶ See it live — Demo application, Speech out tile: the preview, the code behind it and this page, side by side.


At a glance #

Nonvisual objectn_pbt_speechout
Used forMaking a text heard: accessibility, busy hands, a notification nobody is looking at
PrincipleYou hand over the text; the component cuts it into sentences and tells you which one it is reading
DependencyThe workstation's speech synthesis — no third-party service, no API key

Quick start #

// once, when the window opens
inv_voice.of_open()

// then, wherever you need it
inv_voice.is_lang = inv_voice.LANG_FR_FR
inv_voice.of_speak(/*text*/ "Bonjour. Votre commande est expediee.")

Nonvisual: wiring the events #

A voice has nothing to show. So the component draws nothing: no player bar to find room for, no space taken from what it serves.

And you have nothing to wire for it: a nonvisual object has no window, hence no doorbell, but the component pulls its own events on the PowerBuilder loop while the voice is open and raises them on the object. You only write the ue_* handlers (no receiver, no timer).

// Just speak -- the ue_* events arrive on their own :
inv_voice.of_speak(/*text*/ "Good morning. Your order has shipped.")

// the ue_sentence / ue_word / ue_stopped events arrive on their own

Following the reading in YOUR text #

ue_sentence carries the index and the text of the sentence being read. That is where the highlight comes from — in your mle_, your datawindow or your statictext: you know where your text is, we never did.

The way back exists too: of_speak_from() resumes at one precise sentence, which is what you wire to a click on a paragraph. The numbers come from ue_sentence, so they always point at what was actually read.

The cutting is the component's, not yours: of_sentence_count() returns its own count. Do not recount on your side — the two would drift.


What the workstation can really say #

of_languages() returns the languages this workstation can actually pronounce, without duplicates. That is the question a user asks: not "which voices exist", but "is my language there".

of_voices() goes one step down and names the voices themselves. Both lists come from the machine, not from us: never hard-code a name.

Voice recognition has no equivalent, and that is not an oversight: recognition carries no list of the languages it accepts.

Without any component of yours, gnv_utils.of_speech_languages(as_tags[]) gives the same list — the DLL asks a hidden voice, then drops it: that is what a dialog asks before the voice exists, which language to offer, which one to read in. The call is synchronous and can take a few seconds the first time: the voice list arrives late, and the call waits for it (the pointer shows the wait); the answer is kept five seconds. And gnv_utils.of_speech_voices(as_names[], as_langs[]) gives the voices themselves with their language, to offer "Hortense" or "Julie" rather than a tag; gnv_utils.of_locale_name(as_tag) gives a tag its readable name — "French (France)" for fr-FR.

// Local variables
string ls_tags[]

// Read in the first language this workstation can speak
if inv_voice.of_languages(/*tags*/ ls_tags) > 0 then inv_voice.is_lang = ls_tags[1]

Properties #

PropertyTypeDefaultRole
is_langstringen-USLanguage read aloud, in BCP-47 (LANG_* constants). Decides which voice is picked — while is_voice is empty, a voice name pinning its own language. With no voice for that language the workstation reads with the one it has, and ue_voice_fallback names both — a missing REGION too (fr-CA asked, a fr-FR voice reads). Live: changed during a reading, it applies from the next sentence
is_textstring""The text to read. The component cuts it into sentences; tags say HOW to read a piece: [pause=500], [spell]…[/spell], [say-as=digits]…[/say-as], [mark=name], [rate=150]…[/rate] (see below)
is_voicestring""Voice name, taken from of_voices(). Empty = the first one that speaks is_lang. Live, like is_lang
ii_rateinteger100Rate, in PERCENT of the normal rate (10 to 400). Changed mid-reading, it applies to the next sentence
ii_pitchinteger100Voice pitch, in PERCENT of the normal pitch (0 to 200). Changed mid-reading, it applies to the next sentence
ii_volumeinteger100Loudness in percent (0 silent to 100), the video player's scale. Changed mid-reading, it applies to the next sentence
il_timeout_mslong300000Longest a SYNCHRONOUS reading may last: five minutes. Past it, of_speak_sync returns -4 and the reading is cut (is_last_error says why). 0 or less: no limit (24 hours at most)
is_last_errorstring""Why the last of_speak_sync returned -4: voice not created, engine failure, timeout, another of_speak_sync already waiting; also why of_pick_voice could not read the voice list
ipo_ownerpowerobjectnullThe visual object this voice works for: the licence is checked on its class. Set after the voice has opened, it is handed over at the next call. Needed only in the demonstration application; a development or runtime key unlocks the voice without it. Not unlocked, the voice runs in demo mode — 4 sentences and 400 characters per reading (queue included), then the demo notice is spoken

Methods #

MethodRole
of_open ( )Creates the voice. Optional — of_speak does it — but calling it when the window opens pays the cost once, out of the way of the first sentence. Returns a positive number once the voice exists, -2 when it could not be created (is_last_error says why)
of_is_open ( )True once the voice exists
of_speak ( string as_text )Sets the text and reads it, from the first sentence. Without an argument, re-reads is_text. Returns 0 once sent, -2 when the voice could not be created
of_speak_from ( long al_index )Resumes the reading at one precise sentence. Returns 0 once sent, -5 past the last sentence (nothing is read, and a reading under way goes on), -2 when the voice could not be created, -4 when the page did not answer
of_pause ( )Suspends the reading where it is — a reading still waiting for the voice list included: it starts only at of_resume. Returns 0 (nothing to pause on a voice never opened, which is not created for that), -4 when the page did not answer
of_resume ( )Picks up where of_pause left off. Returns 0, -4 when the page did not answer
of_stop ( )Stops the reading; of_speak starts again from the first sentence
of_is_speaking ( )True from of_speak until the reading ends — the wait for the voice list and a paused reading included. Asked to the component, never a stale copy
of_is_paused ( )True while the reading is paused (of_pause), until of_resume or of_stop
of_sentence_count ( )Returns how many sentences the component makes of is_text as it is now, before any reading; during a reading, the count of the text being read
of_count ( ) → longHow many sentences the component made of the text — the same number of_sentence_count answers. The library asks this question under one name everywhere
of_voices ( ref string as_names[] )Fills the array with the voices installed on this workstation and returns how many
of_languages ( ref string as_tags[] )Fills the array with the languages this workstation can pronounce, without duplicates, and returns how many
of_voice_used ( )The voice the component will actually hand to the engine — not always the one is_lang asked for: a workstation carries the voices someone installed on it and no others. Empty = the component imposes none, and the engine takes its own, the voice of the system language. Naming that one would be a guess. ue_voice_fallback says it when reading starts; this reads it beforehand, with is_lang and is_voice as they are right now
of_speak_sync ( { string as_text } )Reads and WAITS for the end: the next line runs after the last sentence, the window keeps painting. Returns 0 once ended, -4 on failure, on timeout, or when another of_speak_sync is already waiting (called from an event it raised) — is_last_error says why
of_enqueue ( string as_text )QUEUES a text: read at once when the voice is idle, after the current reading otherwise, never cutting it. Returns 0, -2 when the voice could not be created
of_clear_queue ( )Forgets the waiting texts, without cutting the one being read. Returns 0 (a voice never opened has no queue, and is not created for that), -4 when the page did not answer
of_queue_count ( )Returns the number of texts still waiting (the one being read is not counted)
of_add_replacement ( string as_from, string as_to )A pronunciation rule: every WHOLE word as_from is read as as_to (PB → PowerBuilder). Applied before the tags, kept by the object. Returns 0, -5 when as_from is empty
of_clear_replacements ( )Empties the dictionary. Returns 0
of_replacement_count ( )Returns the number of rules in the dictionary
of_add_abbreviation ( string as_text )An ABBREVIATION of your own: the full stop after it does not end the sentence — Art. 5 stays one sentence. The final dot may be left out, the case counts. A built-in list already covers the usual ones of the six languages of this documentation (Mr., Dr., e.g., M., Mme., z.B., Sig., p.ej.…). Returns 0 once added, -5 when as_text is empty or holds a blank
of_clear_abbreviations ( )Forgets the abbreviations added by of_add_abbreviation — the built-in list stays. Returns 0 always
of_pick_voice ( string as_lang, string as_gender )Picks an INSTALLED voice for a language and, when one exists, a gender (GENDER_FEMALE, GENDER_MALE, GENDER_ANY): exact language, then its family. Puts it in is_voice and returns it; empty when no voice speaks that language. An empty as_lang means is_lang. When the voice list cannot be read, is_voice is left as it was and is_last_error says why
of_duration ( )Returns the ESTIMATED number of milliseconds the reading will take (words per minute at the requested rate, pauses included): for a progress bar, not a stopwatch. The pace LEARNS the voice: every sentence read to its end measures the real one, kept per voice on this workstation
of_position ( )Returns the estimated number of milliseconds already read, refined by the words the engine reports; 0 when nothing is being read
of_progress ( )Returns the estimated progress, 0 to 100
of_spoken_text ( )The sentences AS THE VOICE GETS THEM, one per line (dictionary applied, tags resolved): the text to display to follow word by word
of_process_events ( )Drains the queued events and raises them on this object. The component's own pump calls it for you while the voice is open — you never call it
of_close ( )Releases the voice, stopping whatever it was saying first: a reading under way ends on its ue_stopped, raised before the call returns. Destroying the object closes the voice too, but raises NO event: the controls of the window may already be destroyed
of_reset ( )Puts every property back to its original value

Events #

EventFired when
ue_started (string as_lang)The voice really starts speaking; as_lang is the language it reads — the voice's, fr-FR for a fr-CA the machine does not have (see ue_voice_fallback). A pause set before it (of_speak then of_pause in the same script) holds it until of_resume
ue_stopped ( )The last sentence is done, or the reading was cut — of_stop, a new of_speak, of_close: every reading that started ends on a ue_stopped
ue_paused ( )The reading is suspended
ue_resumed ( )The reading picks up again
ue_sentence (long al_index, string as_text)For each sentence, with its rank and its text: that is how you follow the reading elsewhere in the window. A sentence longer than 250 characters is cut (on a comma, else on a blank), and a [rate] piece is a sentence of its own: each part has its number
ue_error (string as_message)The workstation has no engine or no voice at all, the voice fails, or a script error occurs in its page. A voice merely MISSING is not an error: that is ue_voice_fallback. Also when the engine drops a sentence without a word ("engine did not answer"): a reading never stays "under way" for ever
ue_voices_ready (long al_count)The engine has filled its voice list — it arrives late; of_voices, of_languages, of_voice_used and of_pick_voice wait for it by themselves (2.5 s at most), so this event only says WHEN it came; al_count says how many the workstation has
ue_word (long al_index, long al_start, long al_length)The WORD being said in sentence al_index: Mid(sentence, al_start, al_length). Always fired: from the words the voice reports, and ESTIMATED on the clock when it reports none
ue_queue_done ( )The last text of the queue (of_enqueue) has been read
ue_voice_fallback (string as_wanted, string as_used)The voice or language asked for is not on this workstation — its REGION included (fr-CA asked, fr-FR reads); as_used names the one reading instead. Information, not an error: the reading goes on
ue_mark (string as_name)The voice reaches a [mark=name] of the text; as_name is that name. Raised when the sentence starts (a mark before its first word), when the word after the mark is reached, or at the end of the sentence: a mark is never lost

Pronunciation: pauses, spelling, dictionary #

WebView2 has no SSML. So the text carries five tags of its own, resolved before the cut into sentences, and a dictionary of whole words applied before them. An unknown tag is read as written.

TagEffect
[pause=500]A 500 ms silence (10 s at most). The pause ends the current sentence. At the END of the text: a silence after the last sentence (to space two announcements of the queue)
[spell]ABC12[/spell]Every character said one by one: "A, B, C, 1, 2". Never ends the sentence (its dots are characters: an address, a version) and is detached from a word it touches
[say-as=digits]4152[/say-as]The digits one by one, not "four thousand one hundred fifty-two"
[say-as=characters]…[/say-as]Same as [spell]
[mark=row2]A mark: ue_mark("row2") is raised when the voice gets there — to highlight a row of your window at the right moment
[rate=50]1 250 USD[/rate]That piece at 50 % of ii_rate (10 to 400) — to read an amount more slowly. It is a sentence of its own
// The dictionary : whole words, in the order added, case-sensitive
inv_voice.of_add_replacement(/*from*/ "PB", /*to*/ "PowerBuilder")
inv_voice.of_add_replacement(/*from*/ "Mme", /*to*/ "Madame")
inv_voice.of_add_replacement(/*from*/ "4152", /*to*/ "[say-as=digits]4152[/say-as]")   // a rule may add a tag

// Then speak : the dictionary and the tags are applied first
inv_voice.of_speak(/*text*/ "Mme Durand, PB order 4152 [pause=600] code [spell]PBT[/spell].")

An abbreviation does not end the sentence: Mr. Smith, Dr., e.g., M. Dupont, z.B. stay in the sentence that carries them (a built-in list for the six languages of this documentation; etc. ends the sentence only before a capital). Yours are added by of_add_abbreviation. A sentence longer than 250 characters — a memo pasted without a full stop — is cut on a comma, else on a blank: an engine silently gives up on utterances that are too long.

// Your own abbreviation : "Art. 5" stays in one sentence
inv_voice.of_add_abbreviation(/*text*/ "Art")

// A mark raises ue_mark("total") when the voice gets there ; the amount is read slower
inv_voice.of_speak(/*text*/ "See Art. 5 of the contract. [mark=total]The total is [rate=70]1 250 dollars[/rate].")

Queue, synchronous reading, progress #


Examples #

Reading a notification #

// Read the notification aloud, without waiting for the end
inv_voice.of_speak(/*text*/ "Order 4152 has been shipped. It arrives on Thursday.")

Lighting up the current sentence #

// in ue_sentence, on the nonvisual object
st_read.text = as_text

// and a click on a paragraph resumes from it
inv_voice.of_speak_from(/*index*/ 2)

Choosing the voice and the rate #

// Local variables
string ls_voices[]

// The first installed voice, a quarter slower, then speak
if inv_voice.of_voices(/*names*/ ls_voices) > 0 then inv_voice.is_voice = ls_voices[1]
inv_voice.ii_rate = 75
inv_voice.of_speak()

Announcing without cutting: the queue #

// Each event of the application is queued : all of them are heard, in order
inv_voice.of_enqueue(/*text*/ "Order 4152 has been shipped.")
inv_voice.of_enqueue(/*text*/ "Order 4153 is ready.")

// ue_queue_done fires after the last one

Lighting up the current word #

// in ue_sentence : keep the sentence
is_sentence = as_text

// in ue_word : the word is Mid(is_sentence, al_start, al_length)
st_read.text = Left(is_sentence, al_start - 1) + "[" + Mid(is_sentence, al_start, al_length) + "]" + Mid(is_sentence, al_start + al_length)

A voice by language and gender, then reading while waiting for the end #

// A French female voice, then speak and wait for the end before asking
inv_voice.of_pick_voice(/*lang*/ n_pbt_speechout.LANG_FR_FR, /*gender*/ n_pbt_speechout.GENDER_FEMALE)
if inv_voice.of_speak_sync(/*text*/ "Please confirm the order.") = 0 then
	li_answer = MessageBox("Order", "Confirm ?", Question!, YesNo!)
end if

Good practice #


← Component reference · Guide contents