-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create a webSpeaker and options dialog to support new self-voicing pr…
…ototypes, see phetsims/gravity-force-lab-basics#193
- Loading branch information
1 parent
fcd11ba
commit 31777a4
Showing
3 changed files
with
213 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
// Copyright 2020, University of Colorado Boulder | ||
|
||
/** | ||
* Content for an "Options" dialog, only used if the ?selfVoicing query parameter is used to explor prototypal "self voicing" | ||
* feature set. This dialog allows control of output verbosity and settings for the speech synthesizer. | ||
* | ||
* @author Jesse Greenberg | ||
*/ | ||
|
||
import inverseSquareLawCommon from '../inverseSquareLawCommon.js'; | ||
import Utils from '../../../../dot/js/Utils.js'; | ||
import ComboBoxItem from '../../../../sun/js/ComboBoxItem.js'; | ||
import ComboBox from '../../../../sun/js/ComboBox.js'; | ||
import HSlider from '../../../../sun/js/HSlider.js'; | ||
import VerticalAquaRadioButtonGroup from '../../../../sun/js/VerticalAquaRadioButtonGroup.js'; | ||
import StringUtils from '../../../../phetcommon/js/util/StringUtils.js'; | ||
import webSpeaker from '../../../inverse-square-law-common/js/view/webSpeaker.js'; | ||
import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; | ||
import VBox from '../../../../scenery/js/nodes/VBox.js'; | ||
import HBox from '../../../../scenery/js/nodes/HBox.js'; | ||
import Text from '../../../../scenery/js/nodes/Text.js'; | ||
|
||
// constants | ||
const TITLE_FONT = new PhetFont( { size: 16 } ); | ||
const LABEL_FONT = new PhetFont( { size: 12 } ); | ||
const INPUT_SPACING = 8; | ||
|
||
class WebSpeechDialogContent extends VBox { | ||
constructor() { | ||
|
||
// controls for verbosity, the output designed by PhET | ||
const verbosityControls = new VerticalAquaRadioButtonGroup( webSpeaker.verbosityProperty, | ||
[ | ||
{ | ||
node: new Text( 'Verbose', { font: LABEL_FONT } ), | ||
value: webSpeaker.Verbosity.VERBOSE, | ||
labelContent: 'Verbose' | ||
}, | ||
{ | ||
node: new Text( 'Brief', { font: LABEL_FONT } ), | ||
value: webSpeaker.Verbosity.BRIEF, | ||
labelContent: 'Brief' | ||
} | ||
], { | ||
spacing: 5 | ||
} | ||
); | ||
const labelledVerbosityControls = new VBox( { | ||
children: [ | ||
new Text( 'Verbosity', { font: TITLE_FONT } ), | ||
verbosityControls | ||
], | ||
align: 'center', | ||
spacing: INPUT_SPACING | ||
} ); | ||
|
||
// controls for speech synthesis, such as the rate, pitch, and voice | ||
const voiceRateSlider = WebSpeechDialogContent.createLabelledSlider( webSpeaker.voiceRateProperty, 'Rate', 'New Voice Rate' ); | ||
const voicePitchSlider = WebSpeechDialogContent.createLabelledSlider( webSpeaker.voicePitchProperty, 'Pitch', 'New Voice Pitch' ); | ||
|
||
const comboBoxItems = []; | ||
|
||
// only grab the first 12 options for the ComboBox, its all we have space for | ||
webSpeaker.voices.splice( 0, 12 ).forEach( voice => { | ||
comboBoxItems.push( new ComboBoxItem( new Text( voice.name, { font: LABEL_FONT } ), voice ) ); | ||
} ); | ||
const voiceComboBox = new ComboBox( comboBoxItems, webSpeaker.voiceProperty, phet.joist.sim.topLayer, { | ||
listPosition: 'above' | ||
} ); | ||
|
||
const voiceControls = new VBox( { | ||
children: [ voiceRateSlider, voicePitchSlider, voiceComboBox ], | ||
spacing: INPUT_SPACING | ||
} ); | ||
|
||
const labelledVoiceControls = new VBox( { | ||
children: [ | ||
new Text( 'Voice', { font: TITLE_FONT } ), | ||
voiceControls | ||
], | ||
align: 'center', | ||
spacing: INPUT_SPACING | ||
} ); | ||
|
||
super( { | ||
children: [ labelledVerbosityControls, labelledVoiceControls ], | ||
spacing: 30 | ||
} ); | ||
|
||
webSpeaker.voiceProperty.lazyLink( voice => { | ||
webSpeaker.speak( 'New voice selected' ); | ||
} ); | ||
} | ||
} | ||
|
||
// @private | ||
// @static | ||
WebSpeechDialogContent.createLabelledSlider = ( numberProperty, label, changeSuccessDescription ) => { | ||
const changeSuccessPatternString = '{{successDescription}}, {{newValue}}'; | ||
|
||
const slider = new HSlider( numberProperty, numberProperty.range, { | ||
endDrag: () => { | ||
const utterance = StringUtils.fillIn( changeSuccessPatternString, { | ||
successDescription: changeSuccessDescription, | ||
newValue: Utils.toFixed( webSpeaker.voicePitchProperty.get(), 2 ) | ||
} ); | ||
webSpeaker.speak( utterance ); | ||
} | ||
} ); | ||
return new HBox( { | ||
children: [ new Text( label, { font: LABEL_FONT } ) , slider ], | ||
spacing: INPUT_SPACING | ||
} ); | ||
}; | ||
|
||
inverseSquareLawCommon.register( 'WebSpeechDialogContent', WebSpeechDialogContent ); | ||
|
||
export default WebSpeechDialogContent; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
// Copyright 2020, University of Colorado Boulder | ||
|
||
/** | ||
* @author Jesse Greenberg | ||
*/ | ||
|
||
import inverseSquareLawCommon from '../inverseSquareLawCommon.js'; | ||
import Enumeration from '../../../phet-core/js/Enumeration.js'; | ||
import EnumerationProperty from '../../../../axon/js/EnumerationProperty.js'; | ||
import NumberProperty from '../../../../axon/js/NumberProperty.js'; | ||
import Property from '../../../../axon/js/Property.js'; | ||
import Range from '../../../../dot/js/Range.js'; | ||
|
||
const Verbosity = Enumeration.byKeys( [ 'BRIEF', 'VERBOSE' ] ); | ||
|
||
class WebSpeaker { | ||
constructor() { | ||
|
||
// {Property.<Verbosity>} - self voicing content can be brief or more "verbose", debending on user selection | ||
this.verbosityProperty = new EnumerationProperty( Verbosity, Verbosity.BRIEF ); | ||
|
||
// @public {null|SpeechSynthesisVoice} | ||
this.voiceProperty = new Property( null ); | ||
|
||
// @public {NumberProperty} - controls the speaking rate of Web Speech | ||
this.voiceRateProperty = new NumberProperty( 1, { range: new Range( 1, 2 ) } ); | ||
|
||
// {NumberProperty} - controls the | ||
this.voicePitchProperty = new NumberProperty( 1, { range: new Range( 1, 2 ) } ); | ||
|
||
// create the synthesizer | ||
this.synth = window.speechSynthesis; | ||
|
||
// @public {SpeechSynthesisVoice[]} - possible voices for Web Speech synthesis | ||
this.voices = []; | ||
|
||
// @public {boolean} - is the WebSpeaker initialized for use? This is prototypal so it isn't always initialized | ||
this.initialized = false; | ||
|
||
// On chrome, synth.getVoices() returns an empty array until the onvoiceschanged event, so we have to | ||
// wait to populate | ||
const populateVoicesListener = () => { | ||
this.populateVoices(); | ||
|
||
// remove the listener after they have been populated once from this event | ||
this.synth.onvoiceschanged = null; | ||
}; | ||
this.synth.onvoiceschanged = populateVoicesListener; | ||
} | ||
|
||
/** | ||
* Indicate that the webSpeaker is ready for use, and attempt to populate voices (if they are ready yet). | ||
* @returns {[type]} [description] | ||
*/ | ||
initialize() { | ||
this.initialized = true; | ||
|
||
// try to populate voice options first | ||
this.populateVoices(); | ||
} | ||
|
||
/** | ||
* Get the available voices for the synth, and set to default. | ||
* @rivate | ||
*/ | ||
populateVoices() { | ||
this.voices = this.synth.getVoices(); | ||
this.voiceProperty.set( this.voices[ 0 ] ); | ||
} | ||
|
||
speak( utterThis ) { | ||
if ( this.initialized ) { | ||
const utterance = new SpeechSynthesisUtterance( utterThis ); | ||
utterance.voice = this.voiceProperty.value; | ||
utterance.pitch = this.voicePitchProperty.value; | ||
utterance.rate = this.voiceRateProperty.value; | ||
|
||
this.synth.speak( utterance ); | ||
} | ||
} | ||
} | ||
|
||
const webSpeaker = new WebSpeaker(); | ||
|
||
// @public | ||
// @static | ||
webSpeaker.Verbosity = Verbosity; | ||
|
||
export default inverseSquareLawCommon.register( 'WebSpeaker', webSpeaker ); |