Skip to content

Instantly share code, notes, and snippets.

@mattdanielbrown
Created June 8, 2024 01:00
Show Gist options
  • Save mattdanielbrown/5e834651c72e71638021bf6a669c6af0 to your computer and use it in GitHub Desktop.
Save mattdanielbrown/5e834651c72e71638021bf6a669c6af0 to your computer and use it in GitHub Desktop.
Singing Text To Speech
<menu>
<h1></h1>
</menu>
<main>
<ul class="words">
<div class="slider">
<li class="word">
<div class="position">
<input class="string" type="text" value="daisy">
</div>
<div class="settings">
<button class="play"></button>
<button class="delete"></button>
<label>Rate</label>
<input class="rate" type="range" min="0" value="5" max="50">
<label>Pitch</label>
<input class="pitch" type="range" min="0" value="150" max="200"> </div>
</li><li class="word">
<div class="position">
<input class="string" type="text" value="daisy"> </div>
<div class="settings">
<button class="play"></button>
<button class="delete"></button>
<label>Rate</label>
<input class="rate" type="range" min="0" value="5" max="50">
<label>Pitch</label>
<input class="pitch" type="range" min="0" value="125" max="200"> </div>
</li><li class="word">
<div class="position">
<input class="string" type="text" value="give me your"> </div>
<div class="settings">
<button class="play"></button>
<button class="delete"></button>
<label>Rate</label>
<input class="rate" type="range" min="0" value="5" max="50">
<label>Pitch</label>
<input class="pitch" type="range" min="0" value="100" max="200"> </div>
</li><li class="word">
<div class="position">
<input class="string" type="text" value="answer"> </div>
<div class="settings">
<button class="play"></button>
<button class="delete"></button>
<label>Rate</label>
<input class="rate" type="range" min="0" value="5" max="50">
<label>Pitch</label>
<input class="pitch" type="range" min="0" value="75" max="200"> </div>
</li><li class="word">
<div class="position">
<input class="string" type="text" value="do"> </div>
<div class="settings">
<button class="play"></button>
<button class="delete"></button>
<label>Rate</label>
<input class="rate" type="range" min="0" value="5" max="50">
<label>Pitch</label>
<input class="pitch" type="range" min="0" value="100" max="200"> </div>
</li>
</div>
</ul>
<button class="plus-sign add-words"></button>
</main>
<nav>
<button class="play-state"></button>
<select class="voice-name"></select><i></i>
<button class="export">Export</button>
<button class="import">Import</button>
</nav>
<aside>
<div class="center">
<p class="exporting">Copy and save this code.</p>
<p class="importing">Paste your code into the text area.</p>
<button class="close"></button>
<textarea></textarea>
</div>
</aside>
class SubClass
constructor: ( parent , data ) ->
@.root = parent.root or parent
@.parent = parent
@.init? data
class Words extends SubClass
template: "
<div class=\"position\">
<input class=\"string\" type=\"text\" />
</div>
<div class=\"settings\">
<button class=\"play\"></button><button class=\"delete\"></button>
<label>Rate</label>
<input class=\"rate\" type=\"range\" min=\"0\" value=\"10\" max=\"50\" />
<label>Pitch</label>
<input class=\"pitch\" type=\"range\" min=\"0\" value=\"100\" max=\"200\" />
</div>
"
list: []
elements: []
init: ->
@.getElements()
@.addListeners()
getElements: ->
@.sliderContainer = document.querySelector ".words"
@.elementContainer = document.querySelector ".words .slider"
@.elements = document.querySelectorAll ".word"
@.newWordButton = document.querySelector ".add-words"
for element in @.elements
@.addItemListeners element
addListeners: ->
@.newWordButton.addEventListener "click" , @.makeNewWordElement
@.elementContainer.addEventListener "mousewheel" , @.onScroll
onScroll: ( e ) =>
e.preventDefault()
@.sliderContainer.scrollLeft += e.deltaY
@.sliderContainer.scrollLeft += e.deltaX
makeNewWordElement: =>
item = document.createElement "li"
item.setAttribute "class" , "word"
item.innerHTML = @.template
last = @.elementContainer.lastChild
@.elementContainer.insertBefore item , last
@.addItemListeners item
item.querySelector(".string").focus()
addItemListeners: ( item ) ->
item.querySelector( ".delete" ).onclick = @.deleteItem
item.querySelector( ".play" ).onclick = => @.root.parse.item item
item.querySelector( ".string" ).onchange = => @.root.parse.item item
item.querySelector( ".pitch" ).onchange = =>
pitch = item.querySelector( ".pitch" ).value / 100
item.querySelector( ".string" ).style.top = "#{100 - (10+ ( pitch / 2 * 80 ))}%"
@.root.parse.item item
item.querySelector( ".rate" ).onchange = => @.root.parse.item item
pitch = item.querySelector( ".pitch" ).value / 100
item.querySelector( ".string" ).style.top = "#{100 - (10+ ( pitch / 2 * 80 ))}%"
deleteItem: ( event ) =>
item = event.srcElement.parentNode.parentNode
item.parentNode.removeChild item
class Parse extends SubClass
voice: null
utterances: []
init: ->
voices = window.speechSynthesis.getVoices()
select = document.querySelector ".voice-name"
if voices.length is 0
setTimeout =>
@.init()
, 100
else
for voice in voices
if voice.name.substring( 0, 6 ) isnt "Google"
option = document.createElement "option"
option.text = voice.name
option.voice = voice
select.appendChild option
item: ( item ) =>
@.utterances = []
voices = speechSynthesis.getVoices()
name = document.querySelector ".voice-name"
voice = name[ name.selectedIndex ].voice
string = item.querySelector( ".string" ).value
rate = item.querySelector( ".rate" ).value / 10
pitch = item.querySelector( ".pitch" ).value / 100
if string.length > 0
utterance = new SpeechSynthesisUtterance string
utterance.voice = voice
utterance.pitch = pitch
utterance.rate = rate
utterance.element = item
@.utterances.push utterance
@.root.player.run()
words: =>
@.utterances = []
voices = speechSynthesis.getVoices()
items = document.querySelectorAll ".words .slider .word"
name = document.querySelector ".voice-name"
voice = name[ name.selectedIndex ].voice
for item in items
string = item.querySelector( ".string" ).value
rate = item.querySelector( ".rate" ).value / 10
pitch = item.querySelector( ".pitch" ).value / 100
item.querySelector( ".string" ).style.top = "#{100 - (10 + ( pitch / 2 * 80 ))}%"
if string.length > 0
utterance = new SpeechSynthesisUtterance string
utterance.voice = voice
utterance.pitch = pitch
utterance.rate = rate
utterance.element = item
@.utterances.push utterance
@.root.player.run()
class Player extends SubClass
run: ->
utterances = @.root.parse.utterances
if utterances.length > 0
for utterance, index in utterances
if index + 1 isnt utterances.length
utterance.next = utterances[ index + 1 ]
self = @
utterance.onend = ->
@.element.classList.remove "playing"
next = @.next
self.speak next
@.onend = undefined
@.speak utterances[0]
speak: ( utterance ) ->
@.lastUtterance?.element.classList.remove "playing"
@.lastUtterance = utterance
@.lastUtterance.element.classList.add "playing"
if @.lastUtterance.onend is null
@.lastUtterance.onend = ->
@.element.classList.remove "playing"
window.speechSynthesis.speak utterance
class Interface extends SubClass
init: ->
@.getElements()
@.addListeners()
getElements: ->
@.playingButton = document.querySelector ".play-state"
addListeners: ->
@.playingButton.addEventListener "click" , =>
if @.playingButton.classList.contains "playing"
# todo: pause
else
@.root.parse.words()
setInterval =>
if window.speechSynthesis.speaking
@.playingButton.classList.add "playing"
else
@.playingButton.classList.remove "playing"
, 100
class Porting extends SubClass
init: ->
@.getElements()
@.addListeners()
getElements: ->
@.importButton = document.querySelector "button.import"
@.exportButton = document.querySelector "button.export"
@.modal = document.querySelector "aside"
@.closeButton = @.modal.querySelector ".close"
addListeners: ->
@.importButton.addEventListener "click" , =>
@.modal.classList.add "active"
@.modal.classList.remove "exporting"
@.modal.classList.add "importing"
@.exportButton.addEventListener "click" , =>
@.modal.classList.add "active"
@.modal.classList.remove "importing"
@.modal.classList.add "exporting"
@.closeButton.addEventListener "click" , =>
@.modal.classList.remove "active"
class App
constructor: ->
@.words = new Words @
@.parse = new Parse @
@.player = new Player @
@.interface = new Interface @
@.porting = new Porting @
new App
@import "compass"
$interface-height: 90px
$interface-color: rgba( 0, 155, 215, 1 )
=element-reset
font: inherit
-webkit-appearance: none
-moz-appearance: none
-ms-appearance: none
outline: none
border: none
appearance: none
background: none
box-shadow: none
border-radius: 0
padding: 0
margin: 0
select , button , input
@include element-reset
letter-spacing: 0.05em
html , body , menu , main , nav , header , ul
color: rgba( $interface-color , 0.75 )
letter-spacing: 0.05em
position: absolute
overflow: hidden
font-family: sans-serif
font-weight: 100
right: 0
left: 0
html , body
bottom: 0
top: 0
menu , nav
background-color: rgba( $interface-color , 0.85 )
height: $interface-height
color: white
menu
top: 0
main
top: $interface-height
bottom: $interface-height
nav
white-space: nowrap
vertical-align: middle
bottom: 0
h1
text-transform: uppercase
position: relative
display: inline-block
line-height: $interface-height
font-size: 32px
height: $interface-height
padding: 0 15px
label
font-size: 13px
padding: 5px 15px
display: block
select
background-color: white
border: 2px solid white
color: $interface-color
vertical-align: middle
display: inline-block
margin: -30px 15px 0 0
padding: 10px 35px 10px 10px
cursor: pointer
position: relative
z-index: 2
i
position: absolute
display: inline-block
pointer-events: none
border-top: 7px solid $interface-color
border-left: 5px solid transparent
border-right: 5px solid transparent
margin-top: 43px
margin-left: -35px
z-index: 2
ul
top: 0
bottom: 0
right: 64px
.slider
display: inline-block
position: absolute
height: 100%
width: auto
white-space: nowrap
.word
position: relative
display: inline-block
height: 100%
background-color: rgba( $interface-color , 0.1 )
width: 300px
vertical-align: middle
transition: background 0.1s ease-in-out
&:nth-of-type( even )
background-color: rgba( $interface-color , 0.025 )
&.playing , &:nth-of-type( even ).playing
background-color: rgba( $interface-color , 0.5 )
color: white
&:hover .settings *
transition-delay: 0s
opacity: 1
.string
font-weight: 100
width: 250px
color: white
text-align: center
line-height: 36px
height: 36px
left: 50%
top: 50%
background-color: rgba( $interface-color , 0.75 )
position: absolute
transform: translate( -50% , -50% )
.position
position: absolute
bottom: 140px
right: 0
left: 0
top: 0
.settings
position: absolute
background-color: rgba( $interface-color , 0.1 )
text-align: center
width: 100%
height: 140px
bottom: 0
*
transition: opacity 0.15s ease-in-out
transition-delay: 0.25s
opacity: 0
.play , .delete
margin: 10px 10px 0 10px
border-radius: 50%
background-color: rgba( $interface-color , 0.4 )
cursor: pointer
position: relative
display: inline-block
height: 25px
width: 25px
.play::before
content: ""
position: absolute
top: 50%
left: 50%
border-left: 8px solid white
border-top: 5px solid transparent
border-bottom: 5px solid transparent
margin: -5px 0 0 -3px
.delete
&:before , &:after
content: ""
backface-visibility: hidden
position: absolute
background-color: white
margin: -5px 0 0 -1px
height: 10px
width: 2px
left: 50%
top: 50%
&:before
transform: rotate( -45deg )
&:after
transform: rotate( 45deg )
label
display: block
text-align: left
input[type="range"]
border-radius: 30px
background-color: rgba( $interface-color , 0.4 )
padding: 3px
width: 264px
margin-bottom: 15px
input[type=range]::-webkit-slider-thumb
@include element-reset
background-color: white
border-radius: 50%
cursor: grab
width: 7px
height: 7px
&:active
cursor: grabbing
.add-words
position: absolute
border-radius: 50%
background-color: rgba( $interface-color , 0.75 )
cursor: pointer
margin: -15px 15px 0 0
width: 30px
height: 30px
right: 0
top: 50%
&:before , &:after
content: ""
height: 2px
background-color: white
position: absolute
margin: -1px 0 0 -7px
width: 14px
left: 50%
top: 50%
&:before
transform: rotate( 90deg )
.play-state
display: inline-block
width: $interface-height/2
height: $interface-height/2
margin: $interface-height/4 15px
overflow: hidden
box-sizing: border-box
border: 2px solid white
border-radius: 50%
position: relative
cursor: pointer
&:before , &:after
content: ""
position: absolute
transition: transform 0.1s ease-in-out
&:before
border-left: 10px solid white
border-top: 7px solid transparent
border-bottom: 7px solid transparent
margin: -6px 0 0 -4px
left: 50%
top: 50%
transform: translate( 0, 0 )
&:after
content: ""
position: absolute
background-color: white
box-shadow: 9px 0 0 0 white
margin: -8px 0 0 -8px
width: 5px
height: 16px
transform: translate( $interface-height/3 , 0 )
&.playing
&:before
transform: translate( -$interface-height/3 , 0 )
&:after
transform: translate( 0, 0 )
.export , .import
cursor: pointer
border: 2px solid white
vertical-align: middle
padding: 10px
margin: -30px 15px 0 0
display: inline-block
color: white
aside
position: absolute
background-color: rgba( $interface-color , 0.9 )
left: 0
top: 100%
width: 100%
height: 100%
overflow: hidden
vertical-align: middle
color: white
opacity: 0
transition: transform 0.25s ease-in-out, opacity 0.25s ease-in-out, top 0s linear 0.25s
&.active
transition: transform 0.25s ease-in-out, opacity 0.25s ease-in-out, top 0s linear 0s
opacity: 1
top: 0
&.exporting .importing
display: none
&.importing .exporting
display: none
.center
transform: translate( -50% , -50% )
max-width: 90%
width: 800px
display: inline-block
text-align: left
position: absolute
left: 50%
top: 50%
textarea
@include element-reset
font-family: monospace
border: 2px solid white
color: white
box-sizing: border-box
min-height: 350px
padding: 15px
margin: 15px 0
position: relative
display: block
width: 100%
resize: none
.close
position: absolute
border-radius: 50%
background-color: white
cursor: pointer
margin: 0
width: 30px
height: 30px
right: 0
top: -10px
&:before , &:after
content: ""
height: 2px
backface-visibility: hidden
background-color: $interface-color
position: absolute
margin: -1px 0 0 -7px
width: 14px
left: 50%
top: 50%
&:before
transform: rotate( -45deg )
&:after
transform: rotate( 45deg )
.import, .export
display: none
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment