index.html 98 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586
  1. <!doctype html>
  2. <!--
  3. Hello! If you're seeing this, you're probably using your browser's
  4. "view-source" feature. We've tried to make the output here readable,
  5. but you may have better luck looking at the source files in our git
  6. repository [1]
  7. - https://gitlab.com/lmcnulty/gender-voice-visualization
  8. - https://github.com/lmcnulty/gender-voice-visualization
  9. The source is free software under the GNU Affero General Public License [1],
  10. and assets under a Creative Commons Attribution Sharealike license.
  11. [1] https://gitlab.com/lmcnulty/gender-voice-visualization,
  12. https://github.com/lmcnulty/gender-voice-visualization
  13. [2] https://www.gnu.org/licenses/agpl-3.0.en.html
  14. [3] https://creativecommons.org/licenses/by-sa/4.0/
  15. -->
  16. <html>
  17. <head>
  18. <meta charset='utf-8'/>
  19. <meta name="viewport" content="width=device-width, initial-scale=1">
  20. <meta name="theme-color" content="#45acf4">
  21. <title>Acoustic Genderspace Viewer - Research Trial</title>
  22. <link rel="shortcut icon" href="./resources/logo.png">
  23. <link rel="icon" sizes="192x192" href="./resources/logo.png">
  24. <link href="data:text/css," rel="stylesheet" type="text/css" title="Null">
  25. <link rel="manifest" href="./manifest.json">
  26. <style>
  27. /* ======================= Variables ======================= */
  28. body {
  29. --header-height: 2.5rem;
  30. --body-background: #5e5e5e;
  31. --body-color: #f2f2f2;
  32. --chrome-color: #f2f2f2;
  33. --chrome-background: #565656;
  34. --chrome-border: #181818;
  35. --accent: #169CF9;
  36. --accent-text: #ffffff;
  37. --button-color: #ffffff;
  38. --button-background: #474747;
  39. --input-color: #f2f2f2;
  40. --input-background: #707070;
  41. --border-radius: 8;
  42. --box-shadow-strength: 8;
  43. --box-shadow-spread: 40;
  44. --highlight-strength: 9;
  45. --box-shadow: -1px 1px var(--box-shadow-spread) var(--box-shadow-spread) rgba(0,0,0,calc(var(--box-shadow-strength)));
  46. --progress-bar-background: var(--accent);
  47. }
  48. /* ======================= Layout ======================= */
  49. * {
  50. box-sizing: border-box;
  51. }
  52. html, body {
  53. position: relative;
  54. margin: 0px;
  55. overflow: hidden;
  56. background: var(--body-background);
  57. color: var(--body-color);
  58. font-family: sans-serif;
  59. }
  60. html {
  61. height: 100%;
  62. }
  63. body { height: 100%; }
  64. header, footer {
  65. height: var(--header-height);
  66. width: 100%;
  67. padding: .5rem;
  68. display: flex;
  69. position: relative;
  70. align-items: center;
  71. background: var(--chrome-background);
  72. color: var(--chrome-color);
  73. border-color: var(--chrome-border);
  74. }
  75. main {
  76. height: calc(100% - var(--header-height));
  77. display: flex;
  78. justify-content: center;
  79. }
  80. header button, footer button {
  81. height: calc(var(--header-height) - 1rem);
  82. flex-shrink: 0;
  83. line-height: 1rem;
  84. min-width: 2rem;
  85. }
  86. button span {
  87. display: inline-block;
  88. vertical-align: bottom;
  89. }
  90. button svg + span {
  91. margin-left: .25em;
  92. }
  93. button *{
  94. height: 100%;
  95. }
  96. .spacer {
  97. height: 1px;
  98. width: 100%;
  99. flex-shrink: 1;
  100. }
  101. header > * + *, footer > * + *{
  102. margin-left: .5rem;
  103. }
  104. header > * {
  105. z-index: 2;
  106. }
  107. main > section {
  108. position: relative;
  109. border-top: 1px solid var(--chrome-border);
  110. }
  111. main > aside {
  112. position: relative;
  113. color: var(--chrome-color);
  114. background: var(--chrome-background);
  115. }
  116. input, select {
  117. max-width: 100%;
  118. }
  119. audio {
  120. width: 100%;
  121. }
  122. #offscreen {
  123. display: none;
  124. }
  125. section > *:first-child {
  126. margin-top: 0px;
  127. }
  128. .wide-body main > section {
  129. max-width: calc(100vh - var(--header-height));
  130. max-height: 100vw;
  131. width: 100%;
  132. height: 100%;
  133. }
  134. .wide-body main > aside {
  135. width: 100%;
  136. max-width: calc(100vw - 100vh + var(--header-height));
  137. }
  138. .narrow-body main {
  139. flex-direction: column;
  140. }
  141. .narrow-body aside, .narrow-body .current-tab {
  142. border-left: 0px;
  143. }
  144. .narrow-body main aside {
  145. max-width: 100%;
  146. height: calc(100% - 100vw - var(--header-height));
  147. }
  148. .narrow-body voice-graph-2d {
  149. max-width: calc(60vh - var(--header-height) * .6);
  150. max-height: 100%;
  151. margin: auto;
  152. bottom: 0px;
  153. position: relative;
  154. }
  155. .narrow-body main > section {
  156. max-height: 60%;
  157. height: 100vw;
  158. flex-shrink: 0;
  159. }
  160. .narrow-body main > aside {
  161. max-height: calc(100% - 100vw);
  162. min-height: 40%;
  163. height: 100%;
  164. }
  165. .narrow-body .loading-message {
  166. display: none !important;
  167. }
  168. section > h2:first-child {
  169. margin-top: 0px;
  170. margin-bottom: 0px;
  171. }
  172. section > h2:first-child + p {
  173. margin-top: 0px;
  174. }
  175. voice-graph-2d {
  176. position: absolute;
  177. top: 0px;
  178. bottom: 0px;
  179. width: 100%;
  180. }
  181. .hidden {
  182. display: none;
  183. }
  184. .scrolling-text-box {
  185. overflow-y: auto;
  186. height: 6em;
  187. border: 1px solid var(--chrome-border);
  188. padding: 1em;
  189. }
  190. .scrolling-text-box *:first-child {
  191. margin-top: 0px;
  192. }
  193. .scrolling-text-box *:last-child {
  194. margin-bottom: 0px;
  195. }
  196. /*
  197. input, button, select, textarea {
  198. font-size: inherit;
  199. }
  200. */
  201. /* ======================= Style ======================= */
  202. button:hover, input:hover, select:hover, textArea:hover {
  203. opacity: .8;
  204. }
  205. button:active {
  206. opacity: .5;
  207. }
  208. *:focus {
  209. box-shadow: 0px 0px 2px 2px var(--accent) !important;
  210. }
  211. button:hover:disabled {
  212. opacity: .5;
  213. }
  214. *:disabled {
  215. opacity: .5;
  216. }
  217. a {
  218. color: var(--accent);
  219. }
  220. .custom-widgets button,
  221. .custom-widgets input[type=submit]
  222. {
  223. padding-top: 0px;
  224. padding-bottom: 0px;
  225. vertical-align: middle;
  226. background: var(--button-background);
  227. color: var(--button-color);
  228. border: 1px solid var(--chrome-border);
  229. border-radius: var(--border-radius);
  230. box-shadow: var(--box-shadow);
  231. }
  232. .custom-widgets input[type=file]::file-selector-button {
  233. padding-top: 0px;
  234. padding-bottom: 0px;
  235. vertical-align: middle;
  236. background: var(--button-background);
  237. color: var(--button-color);
  238. border: 1px solid var(--chrome-border);
  239. border-radius: var(--border-radius);
  240. box-shadow: var(--box-shadow);
  241. }
  242. .custom-widgets textarea, .custom-widgets select, .custom-widgets input {
  243. background: var(--input-background);
  244. color: var(--input-color);
  245. border: 1px solid var(--chrome-border);
  246. border-radius: var(--border-radius);
  247. padding: .25em;
  248. }
  249. main > section {
  250. box-shadow: inset var(--box-shadow);
  251. }
  252. button,
  253. input[type=submit]
  254. {
  255. height: calc(var(--header-height) - 1rem);
  256. }
  257. input[type=file]::file-selector-button {
  258. height: calc(var(--header-height) - 1rem);
  259. }
  260. input, textarea, progress[value], select {
  261. box-shadow: inset var(--box-shadow);
  262. }
  263. button, input[type=submit] {
  264. position: relative;
  265. }
  266. h2 {
  267. font-size: 100%;
  268. }
  269. .custom-widgets button:after,
  270. .custom-widgets input[type=submit]:after
  271. {
  272. content: ' ';
  273. position: absolute;
  274. bottom: 0px;
  275. left: 0px;
  276. right: 0px;
  277. top: 0px;
  278. box-shadow: inset -1px 2px calc(var(--box-shadow-spread) * 1.25) 2px rgba(255,255,255, calc(var(--highlight-strength) * 1));
  279. border-radius: var(--border-radius);
  280. }
  281. .tab-set button:after {
  282. content: none;
  283. }
  284. voice-graph-2d canvas {
  285. border-radius: var(--border-radius);
  286. }
  287. textarea {
  288. width: 100%;
  289. height: 8em;
  290. font-family: sans-serif;
  291. margin-top: 1em;
  292. margin-bottom: 1em;
  293. }
  294. .modal {
  295. border: 1px solid;
  296. padding: 1em;
  297. padding-bottom: 0em;
  298. border-radius: var(--border-radius);
  299. box-shadow: var(--box-shadow);
  300. }
  301. .modal section {
  302. padding-bottom: 2em;
  303. }
  304. .custom-widgets ::selection {
  305. background: var(--accent);
  306. color: white;
  307. color: var(--accent-text);
  308. }
  309. h2 {
  310. margin-bottom: .5em;
  311. }
  312. h2 + p {
  313. margin-top: .5em;
  314. }
  315. .scrolling-text-box {
  316. background: var(--body-background);
  317. color: var(--body-color);
  318. border-radius: var(--border-radius);
  319. box-shadow: inset var(--box-shadow);
  320. }
  321. .custom-widgets button.attention{
  322. background: var(--accent);
  323. color: var(--accent-text);
  324. font-weight: bold;
  325. }
  326. hr {
  327. border: 1px solid var(--chrome-border);
  328. }
  329. /* Voice Graph */
  330. * {
  331. --voice-graph-z-index: 0;
  332. }
  333. voice-graph-2d {
  334. display: block;
  335. --padding: 2em;
  336. }
  337. voice-graph-2d > canvas {
  338. position: relative;
  339. height: 100%;
  340. width: 100%;
  341. float: left;
  342. }
  343. voice-graph-2d {
  344. padding: var(--padding);
  345. overflow: hidden;
  346. }
  347. voice-graph-2d > .axis-labels {
  348. display: flex;
  349. justify-content: space-between;
  350. position: absolute;
  351. line-height: var(--padding);
  352. }
  353. voice-graph-2d > .axis-labels > .title {
  354. font-weight: bold;
  355. }
  356. voice-graph-2d > .x.axis-labels {
  357. top: 0px;
  358. left: var(--padding);
  359. right: var(--padding);
  360. }
  361. voice-graph-2d > .x-opposite.axis-labels {
  362. bottom: 0px;
  363. left: var(--padding);
  364. right: var(--padding);
  365. }
  366. .current-value {
  367. z-index: calc(var(--voice-graph-z-index) + 3);
  368. text-shadow: var(--body-background) 0px 1px, var(--body-background) 0px -1px, var(--body-background) 1px 0px, var(--body-background) -1px 0px;
  369. }
  370. voice-graph-2d > .y-opposite.axis-labels {
  371. top: var(--padding);
  372. bottom: var(--padding);
  373. left: 0px;
  374. writing-mode: vertical-rl;
  375. text-orientation: mixed;
  376. }
  377. voice-graph-2d > .y.axis-labels {
  378. top: var(--padding);
  379. bottom: var(--padding);
  380. right: 0px;
  381. writing-mode: vertical-lr;
  382. text-orientation: sideways-left;
  383. }
  384. voice-graph-2d > .overlay {
  385. position: absolute;
  386. top: var(--padding);
  387. left: var(--padding);
  388. bottom: var(--padding);
  389. right: var(--padding);
  390. overflow: visible;
  391. }
  392. voice-graph-2d > .overlay > .marker {
  393. display: online-block;
  394. height: 1.5em;
  395. width: 1.5em;
  396. border-radius: 50%;
  397. background: black;
  398. border: .1em solid white;
  399. position: absolute;
  400. z-index: calc(var(--voice-graph-z-index) + 2);
  401. color: white;
  402. text-align: center;
  403. line-height: 1em;
  404. /*transition: all .1s;*/
  405. top: calc(-.75em);
  406. left: calc(-.75em);
  407. }
  408. .marker:hover {
  409. opacity: 1;
  410. }
  411. .custom-widgets button.marker:after {
  412. box-shadow: 0px;
  413. display: none;
  414. }
  415. /*voice-graph-2d > .overlay > .marker:hover {*/
  416. voice-graph-2d > .overlay > .marker.preview {
  417. }
  418. voice-graph-2d > .overlay > .marker.playing{
  419. border: 2px solid yellow;
  420. }
  421. voice-graph-2d > .overlay > .x.hairline {
  422. content: '';
  423. border-left: .1em dotted black;
  424. display: block;
  425. position: absolute;
  426. z-index: calc(var(--voice-graph-z-index) + 1);
  427. height: 100%;
  428. top: 0px;
  429. left: -1px;
  430. transition: .1s all;
  431. }
  432. voice-graph-2d > .overlay > .hairline.hidden {
  433. opacity: 0;
  434. }
  435. voice-graph-2d > .x-opposite.axis-labels > .current-value {
  436. position: absolute;
  437. width: calc(2 * var(--padding)) ;
  438. text-align: center;
  439. bottom: 0px;
  440. opacity: 0;
  441. left: calc(-1 * var(--padding));
  442. transition: .1s all;
  443. }
  444. voice-graph-2d > .overlay > .y.hairline {
  445. content: '';
  446. border-top: .1em dotted black;
  447. display: block;
  448. position: absolute;
  449. z-index: calc(var(--voice-graph-z-index) + 2);
  450. width: 100%;
  451. left: 0px;
  452. top: -1px;
  453. transition: .1s all;
  454. }
  455. voice-graph-2d > .y-opposite.axis-labels > .current-value {
  456. position: absolute;
  457. height: calc(2 * var(--padding));
  458. text-align: center;
  459. transform: rotate(180deg);
  460. transform-origin: center;
  461. left: 0px;
  462. bottom: calc(-1 * var(--padding));
  463. opacity: 0;
  464. transition: .1s all;
  465. }
  466. voice-graph-2d .marker > .infobox {
  467. position: absolute;
  468. width: 18ch;
  469. left: calc(0.5em - 9ch);
  470. top: calc(-1 * var(--padding));
  471. pointer-events: none;
  472. text-shadow: rgba(0,0,0,.5) 1px 1px, rgba(0,0,0,.5) -1px -1px, rgba(0,0,0,.5) 1px -1px, rgba(0,0,0,.5) -1px 1px;
  473. }
  474. .ratings-bar {
  475. box-sizing: border-box;
  476. display: flex;
  477. width: 100%;
  478. overflow: hidden;
  479. border-radius: 5px 5px 5px 5px;
  480. }
  481. .ratings-bar > span {
  482. padding: 2px 0px 2px 0px;
  483. display: inline-block;
  484. box-sizing: border-box;
  485. overflow: hidden;
  486. font-size: small;
  487. line-height: 1em;
  488. }
  489. .infobox .strongly-agree {
  490. background: #43db20;
  491. color: white;
  492. border-radius: 0px 5px 5px 0px;
  493. }
  494. .infobox .agree {
  495. background: #a5db99;
  496. color: white;
  497. }
  498. .infobox .disagree {
  499. background: #e28888;
  500. color: white;
  501. }
  502. .infobox .strongly-disagree{
  503. background: #e22222;
  504. color: white;
  505. border-radius: 5px 0px 0px 5px;
  506. }
  507. .clef {
  508. position: absolute;
  509. left: 0px;
  510. width: var(--padding);
  511. /*z-index: calc(var(--voice-graph-z-index) + 2);*/
  512. text-align: center;
  513. transform-origin: center center;
  514. transform: scale(2);
  515. padding: 0px;
  516. margin-bottom: -.5em;
  517. }
  518. .clef.treble { top: var(--padding); }
  519. .clef.bass { bottom: var(--padding); }
  520. .instrument {
  521. position: absolute;
  522. bottom: calc(var(--padding) * .1);
  523. height: calc(var(--padding) * .8);
  524. }
  525. .instrument svg {
  526. height: 100%;
  527. }
  528. .instrument.flute {
  529. right: var(--padding);
  530. }
  531. .instrument.tuba {
  532. left: var(--padding);
  533. }
  534. .spinner,
  535. .spinner:after {
  536. border-radius: 50%;
  537. width: 3em;
  538. height: 3em;
  539. position: relative;
  540. flex-shrink: 0;
  541. }
  542. .spinner {
  543. margin: 60px auto;
  544. font-size: 10px;
  545. position: relative;
  546. text-indent: -9999em;
  547. border-top: .5em solid var(--accent);
  548. border-right: .5em solid var(--accent);
  549. border-bottom: .5em solid var(--accent);
  550. border-left: .5em solid var(--button-background);
  551. -webkit-transform: translateZ(0);
  552. -ms-transform: translateZ(0);
  553. transform: translateZ(0);
  554. -webkit-animation: load8 1.1s infinite linear;
  555. animation: load8 1.1s infinite linear;
  556. }
  557. @-webkit-keyframes load8 {
  558. 0% {
  559. -webkit-transform: rotate(0deg);
  560. transform: rotate(0deg);
  561. }
  562. 100% {
  563. -webkit-transform: rotate(360deg);
  564. transform: rotate(360deg);
  565. }
  566. }
  567. @keyframes load8 {
  568. 0% {
  569. -webkit-transform: rotate(0deg);
  570. transform: rotate(0deg);
  571. }
  572. 100% {
  573. -webkit-transform: rotate(360deg);
  574. transform: rotate(360deg);
  575. }
  576. }
  577. header .spinner {
  578. margin-left: 1em;
  579. }
  580. header .spinner.hidden + .loading-message {
  581. display: none;
  582. }
  583. header .spinner + .loading-message {
  584. display: inline;
  585. flex-shrink: 0;
  586. }
  587. </style>
  588. <script>
  589. let darkIcon = String(`
  590. <svg
  591. xmlns:dc="http://purl.org/dc/elements/1.1/"
  592. xmlns:cc="http://creativecommons.org/ns#"
  593. xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  594. xmlns:svg="http://www.w3.org/2000/svg"
  595. xmlns="http://www.w3.org/2000/svg"
  596. id="svg5181"
  597. version="1.1"
  598. viewBox="0 0 8.5024996 8.5054264"
  599. height="32.146492"
  600. width="32.135433">
  601. <defs
  602. id="defs5175" />
  603. <metadata
  604. id="metadata5178">
  605. <rdf:RDF>
  606. <cc:Work
  607. rdf:about="">
  608. <dc:format>image/svg+xml</dc:format>
  609. <dc:type
  610. rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
  611. <dc:title></dc:title>
  612. </cc:Work>
  613. </rdf:RDF>
  614. </metadata>
  615. <g
  616. transform="translate(-24.474941,-137.0211)"
  617. id="layer1">
  618. <rect style="fill:none;fill-opacity:1;stroke:currentColor;stroke-width:0.56500006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0"
  619. id="rect4546-3"
  620. width="7.9375"
  621. height="7.9375"
  622. x="24.757441"
  623. y="137.30507" />
  624. <path
  625. id="path4571"
  626. d="m 27.40326,137.30506 c -3.43959,1.32291 -3.43959,6.61458 0,7.9375"
  627. style="fill:none;stroke:currentColor;stroke-width:0.565;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
  628. <path
  629. style="fill:none;stroke:currentColor;stroke-width:0.565;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
  630. d="m 27.93242,138.09881 c -2.64583,1.85208 -2.64583,4.49791 0,6.35"
  631. id="path4573" />
  632. <path
  633. style="fill:none;stroke:currentColor;stroke-width:0.565;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
  634. d="m 30.0491,137.30506 c 3.43959,1.32291 3.43959,6.61458 0,7.9375"
  635. id="path5740" />
  636. <path
  637. id="path5742"
  638. d="m 29.51994,138.09881 c 2.64583,1.85208 2.64583,4.49791 0,6.35"
  639. style="fill:none;stroke:currentColor;stroke-width:0.565;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
  640. </g>
  641. </svg>
  642. `)
  643. let brightIcon = String(`
  644. <svg
  645. xmlns:dc="http://purl.org/dc/elements/1.1/"
  646. xmlns:cc="http://creativecommons.org/ns#"
  647. xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  648. xmlns:svg="http://www.w3.org/2000/svg"
  649. xmlns="http://www.w3.org/2000/svg"
  650. xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
  651. xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
  652. width="32.135433"
  653. height="32.153236"
  654. viewBox="0 0 8.5025005 8.5072107"
  655. version="1.1"
  656. id="svg4565"
  657. inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
  658. sodipodi:docname="bright.svg">
  659. <defs id="defs4559">
  660. <link href="style.css" type="text/css" rel="stylesheet"
  661. xmlns="http://www.w3.org/1999/xhtml"/>
  662. </defs>
  663. <sodipodi:namedview
  664. id="base"
  665. pagecolor="#ffffff"
  666. bordercolor="#666666"
  667. borderopacity="1.0"
  668. inkscape:pageopacity="0.0"
  669. inkscape:pageshadow="2"
  670. inkscape:zoom="11.2"
  671. inkscape:cx="12.379905"
  672. inkscape:cy="11.113832"
  673. inkscape:document-units="mm"
  674. inkscape:current-layer="svg4565"
  675. showgrid="false"
  676. fit-margin-top="0"
  677. fit-margin-left="0"
  678. fit-margin-right="0"
  679. fit-margin-bottom="0"
  680. units="px"
  681. inkscape:window-width="1920"
  682. inkscape:window-height="1002"
  683. inkscape:window-x="0"
  684. inkscape:window-y="0"
  685. inkscape:window-maximized="1" />
  686. <metadata
  687. id="metadata4562">
  688. <rdf:RDF>
  689. <cc:Work
  690. rdf:about="">
  691. <dc:format>image/svg+xml</dc:format>
  692. <dc:type
  693. rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
  694. <dc:title></dc:title>
  695. </cc:Work>
  696. </rdf:RDF>
  697. </metadata>
  698. <g
  699. inkscape:label="Layer 1"
  700. inkscape:groupmode="layer"
  701. id="layer1"
  702. transform="translate(-32.790418,-144.57973)">
  703. <rect
  704. style="fill:none;fill-opacity:1;stroke:currentColor;stroke-width:0.56500006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0"
  705. id="rect4581"
  706. width="7.9375"
  707. height="7.9375"
  708. x="33.072918"
  709. y="144.86459"
  710. inkscape:export-xdpi="191.19"
  711. inkscape:export-ydpi="191.19" />
  712. <path
  713. sodipodi:nodetypes="cc"
  714. inkscape:connector-curvature="0"
  715. id="path4583"
  716. d="m 33.866667,144.86458 c 1.85208,1.32292 1.85208,6.61459 0,7.9375"
  717. style="fill:none;stroke:currentColor;stroke-width:0.56500006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
  718. <path
  719. style="fill:none;stroke:currentColor;stroke-width:0.56500006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
  720. d="m 35.454167,145.39375 c 1.32291,1.85209 1.32291,5.02709 0,6.87917"
  721. id="path4585"
  722. inkscape:connector-curvature="0"
  723. sodipodi:nodetypes="cc" />
  724. <path
  725. style="fill:none;stroke:currentColor;stroke-width:0.56500006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
  726. d="m 40.216657,144.86459 c -1.85208,1.32292 -1.85208,6.61459 0,7.9375"
  727. id="path833"
  728. inkscape:connector-curvature="0"
  729. sodipodi:nodetypes="cc" />
  730. <path
  731. sodipodi:nodetypes="cc"
  732. inkscape:connector-curvature="0"
  733. id="path835"
  734. d="m 38.629157,145.39376 c -1.32291,1.85209 -1.32291,5.02709 0,6.87917"
  735. style="fill:none;stroke:currentColor;stroke-width:0.56500006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
  736. </g>
  737. </svg>
  738. `)
  739. </script>
  740. <script src="./ui/dom.js"></script>
  741. <script src="./ui/globals.js"></script>
  742. <script src="./ui/clip.js"></script>
  743. <script src="./ui/examples.js"></script>
  744. <script src="./ui/util.js"></script>
  745. </head>
  746. <body class="narrow-body custom-widgets">
  747. <style>
  748. .popup {
  749. max-width: 500px;
  750. width: 400px;
  751. margin: 1em;
  752. margin-left: calc(-200px);
  753. padding: 1em;
  754. padding-right: 2em;
  755. color: black;
  756. background: white;
  757. border: 2px solid whitesmoke;
  758. box-shadow: 0px 0px 10px 10px rgba(0, 0, 0, .25);
  759. position: fixed;
  760. top: 0px;
  761. left: 50%;
  762. z-index: 99999999999999;
  763. transition: all 1s;
  764. border-radius: .25em;
  765. }
  766. .popup button.close {
  767. background: none;
  768. border: none;
  769. box-shadow: none;
  770. font-size: 200%;
  771. color: grey;
  772. position: absolute;
  773. top: 0px;
  774. right: 0px;
  775. }
  776. .popup.offscreen {
  777. top: -300px;
  778. opacity: 0;
  779. }
  780. </style>
  781. <div class="popup offscreen">
  782. Please complete <a href="https://brown.co1.qualtrics.com/jfe/form/SV_4PgW7EKNvnxnWLk">our survey</a>
  783. to help us in our research.
  784. <button class="close" onclick="$('.popup').classList.add('offscreen')">×</button>
  785. </div>
  786. <script>
  787. //setTimeout(() => $('.popup').classList.remove('offscreen'), 30000);
  788. </script>
  789. <header>
  790. <!-- ======================= Player ======================= -->
  791. <button class="play-pause iconic" title='play/pause'>
  792. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play-fill" viewBox="0 0 16 16">
  793. <path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z"/>
  794. </svg>
  795. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pause-fill" viewBox="0 0 16 16">
  796. <path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"/>
  797. </svg>
  798. </button>
  799. <progress class="player" value="0" min="0"></progress>
  800. <audio class="clip"></audio>
  801. <script>
  802. try {
  803. let mediaElement = $('.clip');
  804. let playPauseButton = $('button.play-pause');
  805. let progressBar = $('progress.player');
  806. document.body.addEventListener('keyup', evt => {
  807. if (['INPUT', 'TEXTAREA'].filter(
  808. tag => tag == document.activeElement.tagName
  809. ).length == 0) {
  810. if (evt.code == 'ArrowRight') {
  811. globalState.set('playbackTime', Math.min(globalState.get('playbackTime') + 1, mediaElement.duration));
  812. }
  813. if (evt.code == 'ArrowLeft') {
  814. globalState.set('playbackTime', Math.max(globalState.get('playbackTime') - 1, 0));
  815. }
  816. }
  817. if (evt.code == 'Space' && ['INPUT', 'TEXTAREA', 'BUTTON', 'SUMMARY'].filter(
  818. tag => tag == document.activeElement.tagName
  819. ).length == 0) {
  820. globalState.set('playing', !globalState.get('playing'));
  821. }
  822. });
  823. globalState.render(['playingClip'], current => {
  824. if (current.playingClip && current.playingClip.audio) {
  825. progressBar.setAttribute('max', 60);
  826. mediaElement.addEventListener('loadedmetadata', updateDuration);
  827. mediaElement.addEventListener('durationchange', updateDuration);
  828. mediaElement.src = current.playingClip.audio;
  829. globalState.set('playbackTime', 0);
  830. }
  831. });
  832. function updateDuration(evt) {
  833. if (mediaElement.duration < 1000 && mediaElement.duration != Infinity) {
  834. progressBar.setAttribute('max', mediaElement.duration);
  835. };
  836. }
  837. globalState.render(['playingClip'], current => {
  838. progressBar.setAttribute('max', 60);
  839. if (current.playingClip && current.playingClip.audio) {
  840. mediaElement.addEventListener('loadedmetadata', updateDuration);
  841. mediaElement.addEventListener('durationchange', updateDuration);
  842. mediaElement.src = current.playingClip.audio;
  843. globalState.set('playbackTime', 0);
  844. }
  845. })
  846. globalState.render(['playing'], current => {
  847. if (current.playing && !globalState.get('playingClip')) {
  848. globalState.set('playing', false);
  849. return;
  850. }
  851. if (current.playing) {
  852. playPauseButton.classList.add('playing') ;
  853. mediaElement.play();
  854. } else {
  855. playPauseButton.classList.remove('playing') ;
  856. mediaElement.pause();
  857. }
  858. });
  859. playPauseButton.addEventListener('click', evt => {
  860. globalState.set('playing', !globalState.get('playing'));
  861. globalState.set('previewClip', globalState.get('playingClip'));
  862. });
  863. progressBar.addEventListener('click', evt => {
  864. if (globalState.get('playingClip')) {
  865. let pos = (evt.pageX - progressBar.offsetLeft) / progressBar.offsetWidth;
  866. globalState.set('playbackTime', pos * mediaElement.duration);
  867. }
  868. })
  869. let dragInProgress = false;
  870. progressBar.addEventListener('mousedown', evt => dragInProgress = true);
  871. progressBar.addEventListener('mouseup', evt => dragInProgress = false);
  872. progressBar.addEventListener('mousemove', evt => {
  873. if (dragInProgress && globalState.get('playingClip')) {
  874. let pos = (evt.pageX - progressBar.offsetLeft)
  875. / progressBar.offsetWidth;
  876. globalState.set('playbackTime', pos * mediaElement.duration);
  877. }
  878. })
  879. globalState.render(['playbackTime'], current => {
  880. if (current.playbackTime &&
  881. current.playbackTime != mediaElement.currentTime
  882. ) {
  883. mediaElement.currentTime = current.playbackTime;
  884. }
  885. progressBar.setAttribute('value', current.playbackTime);
  886. if (current.playbackTime >= progressBar.max && globalState.get('playing') == true) {
  887. globalState.set('playing', false);
  888. }
  889. });
  890. let updateTime = () => {
  891. let playbackTime = globalState.get('playbackTime');
  892. if (playbackTime != mediaElement.currentTime) {
  893. globalState.set('playbackTime', mediaElement.currentTime);
  894. }
  895. window.requestAnimationFrame(updateTime);
  896. }
  897. window.requestAnimationFrame(updateTime);
  898. } catch(exception) {
  899. alert("An unknown error occured.");
  900. console.error(exception);
  901. }
  902. </script>
  903. <style>
  904. button.play-pause svg {
  905. display: none;
  906. }
  907. button.play-pause svg:first-child {
  908. display: inline-block;
  909. }
  910. button.play-pause.playing svg {
  911. display: inline-block;
  912. }
  913. button.play-pause.playing svg:first-child {
  914. display: none;
  915. }
  916. progress.player {
  917. width: 100%;
  918. height: calc(var(--header-height) - 1em);
  919. vertical-align: center;
  920. flex-shrink: 1;
  921. }
  922. .custom-widgets progress[value] {
  923. /* Reset the default appearance */
  924. -webkit-appearance: none;
  925. -moz-appearance: none;
  926. appearance: none;
  927. /* Get rid of default border in Firefox. */
  928. border: none;
  929. border: 1px solid var(--chrome-border);
  930. border-radius: var(--border-radius);
  931. overflow: hidden;
  932. }
  933. .custom-widgets progress[value]::-webkit-progress-bar, progress[value] {
  934. background: var(--input-background);
  935. box-shadow: inset -1px 2px calc(var(--box-shadow-spread) * 1.25) 2px rgba(255,255,255, calc(var(--highlight-strength) * 1));
  936. }
  937. /* These don't work together for some reason*/
  938. .custom-widgets progress[value]::-webkit-progress-value {
  939. background: var(--progress-bar-background);
  940. border-radius: var(--border-radius) 0px 0px var(--border-radius);
  941. }
  942. .custom-widgets progress[value]::-moz-progress-bar {
  943. background: var(--progress-bar-background);
  944. border-radius: var(--border-radius) 0px 0px var(--border-radius);
  945. }
  946. video.clip {
  947. position: absolute;
  948. top: 0px;
  949. left: 0px;
  950. right:0px;
  951. bottom: 0px;
  952. z-index: 1;
  953. width: 100%;
  954. height: var(--header-height);
  955. margin: 0px;
  956. opacity: 0;
  957. }
  958. </style>
  959. <!-- ====================================================== -->
  960. <button class="new-clip attention" title='Add new clip'><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-lg" viewBox="0 0 16 16">
  961. <path d="M8 0a1 1 0 0 1 1 1v6h6a1 1 0 1 1 0 2H9v6a1 1 0 1 1-2 0V9H1a1 1 0 0 1 0-2h6V1a1 1 0 0 1 1-1z"/>
  962. </svg><span>Clip</span></button>
  963. <div class="spinner hidden"></div>
  964. <div class="loading-message">Adding clip...</div>
  965. <script>
  966. $('button.new-clip').addEventListener('click', evt => {
  967. $('.modal').appendChild($('section.new-clip'));
  968. showModal();
  969. });
  970. </script>
  971. </header>
  972. <main>
  973. <section>
  974. <voice-graph-2d></voice-graph-2d>
  975. </section>
  976. <aside>
  977. <div class="current-tab"></div>
  978. <div class="tab-set">
  979. <button class="details hbox iconic"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16">
  980. <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
  981. <path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
  982. </svg> <span>Details</span></button>
  983. <button class="help iconic hbox"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-question-circle" viewBox="0 0 16 16">
  984. <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
  985. <path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/>
  986. </svg> <span>Help</span></button>
  987. <button class="settings iconic">⚙ Settings</button>
  988. </div>
  989. <style>
  990. button.hbox {
  991. display: flex;
  992. align-items: center;
  993. justify-content: center;
  994. }
  995. .tab-set button.hbox span {
  996. line-height: 2.5em;
  997. }
  998. .tab-set {
  999. position: absolute;
  1000. bottom: 0px;
  1001. height: calc(var(--header-height) - .5rem);
  1002. display: flex;
  1003. left: 0px;
  1004. right: 0px;
  1005. background: var(--button-background);
  1006. }
  1007. .wide-body .tab-set {
  1008. right: 0px;
  1009. }
  1010. .tab-set button {
  1011. transition: box-shadow .2s all;
  1012. height: 100%;
  1013. width: 100%;
  1014. border-top: 1px solid var(--chrome-border);
  1015. border-left: 1px solid var(--chrome-border);
  1016. border-right: 0px;
  1017. border-radius: 0px 0px var(--border-radius) var(--border-radius);
  1018. background: var(--button-background);
  1019. box-shadow: inset var(--box-shadow);
  1020. }
  1021. .current-tab {
  1022. width: 100%;
  1023. overflow: auto;
  1024. height: calc(100% - 2em);
  1025. border: 1px solid var(--chrome-border);
  1026. border-bottom: 0px;
  1027. border-right: 0px;
  1028. padding: .5rem;
  1029. padding-top: 0px;
  1030. background: var(--chrome-background);
  1031. transition: .3s transform;
  1032. position: absolute;
  1033. }
  1034. .current-tab section {
  1035. min-height: 100%;
  1036. }
  1037. .current-tab > section > *:first-child {
  1038. height: 2em;
  1039. line-height: 2em;
  1040. }
  1041. .tab-set button:disabled {
  1042. border-top: 0px;
  1043. opacity: 1;
  1044. background: var(--chrome-background);
  1045. color: var(--chrome-color);
  1046. box-shadow: 2px 2px var(--shadow-spread) 2px rgba(0,0,0,0);
  1047. }
  1048. .narrow-body .current-tab.expanded {
  1049. transform: translate(0, -60%);
  1050. height: calc(200%);
  1051. z-index: calc(var(--voice-graph-z-index) + 3);
  1052. }
  1053. #expand-collapse-button.expanded {
  1054. transform: translate(0, -60%);
  1055. }
  1056. </style>
  1057. <script>
  1058. let tab = $('.current-tab');
  1059. tab.addEventListener('click', evt => {
  1060. let bb = tab.getBoundingClientRect();
  1061. if (
  1062. evt.clientY - bb.top < 20 && document.activeElement != $('#theme-select') &&
  1063. evt.clientX - bb.left > (bb.right - bb.left) / 4 &&
  1064. evt.clientX - bb.left < 3 * (bb.right - bb.left) / 4
  1065. ) {
  1066. if (!tab.classList.contains('expanded')) {
  1067. tab.classList.add('expanded');
  1068. } else {
  1069. tab.classList.remove('expanded');
  1070. }
  1071. }
  1072. }, true);
  1073. for (let tab of ['settings', 'help', 'details']) {
  1074. let tabButton = $('button.' + tab);
  1075. tabButton.addEventListener('click', evt => {
  1076. for (let sibling of tabButton.parentNode.querySelectorAll('button')) {
  1077. sibling.disabled = false;
  1078. }
  1079. clearSelector('.current-tab > section');
  1080. evt.target.disabled = true;
  1081. $('aside > .current-tab').appendChild($('section.' + tab));
  1082. });
  1083. }
  1084. </script>
  1085. </aside>
  1086. </main>
  1087. <!-- ======================= Modal ======================= -->
  1088. <style>
  1089. .modal-shadow {
  1090. position: absolute;
  1091. top: 0px;
  1092. left: 0px;
  1093. right: 0px;
  1094. bottom: 0px;
  1095. background: rgba(0, 0, 0, .75);
  1096. z-index: 3;
  1097. display: flex;
  1098. align-items: center;
  1099. justify-content: center;
  1100. padding: 10px;
  1101. transition: .2s all;
  1102. opacity: 1;
  1103. }
  1104. .modal-shadow.hidden {
  1105. display: block;
  1106. opacity: 0;
  1107. pointer-events: none;
  1108. }
  1109. .modal {
  1110. max-width: 600px;
  1111. min-height: 300px;
  1112. max-height: 90vh;
  1113. overflow: auto;
  1114. width: 100%;
  1115. min-width: 50%;
  1116. position: relative;
  1117. background: var(--body-background);
  1118. color: var(--body-color);
  1119. }
  1120. .modal > .close {
  1121. position: absolute;
  1122. top: 0em;
  1123. right: .25em;
  1124. background: none;
  1125. color: var(--chrome-color);
  1126. border: none;
  1127. box-shadow: none;
  1128. font-size: 200%;
  1129. }
  1130. .modal > .close:after {
  1131. display: none;
  1132. }
  1133. .modal > * > *:first-child {
  1134. margin-top: 0px;
  1135. }
  1136. </style>
  1137. <div class="modal-shadow hidden">
  1138. <div class="modal">
  1139. <button class="close">×</button>
  1140. </div>
  1141. </div>
  1142. <script>
  1143. function showModal() {
  1144. $('.modal-shadow').classList.remove('hidden');
  1145. $('.modal > .close').focus();
  1146. }
  1147. function hideModal() {
  1148. $('.modal-shadow').classList.add('hidden');
  1149. $('.play-pause').focus();
  1150. resizeEvent(null);
  1151. }
  1152. $('.modal > .close').addEventListener('click', evt => {
  1153. clearSelector('.modal > section');
  1154. hideModal();
  1155. });
  1156. </script>
  1157. <!-- ===================================================== -->
  1158. <!-- This is where we keep stuff we don't want visible -->
  1159. <div id="offscreen">
  1160. <style>
  1161. .settings > label:first-child {
  1162. font-weight: bold;
  1163. }
  1164. #export-clips {
  1165. margin-right: auto;
  1166. }
  1167. section.settings .hbox {
  1168. display: flex;
  1169. align-items: center;
  1170. }
  1171. section.settings .hbox > * + * {
  1172. margin-left: .25em;
  1173. }
  1174. #import-export {
  1175. margin-top: .5em;
  1176. margin-bottom: .5em;
  1177. }
  1178. section.settings .hbox h2 {
  1179. margin: 0px;
  1180. margin-right: auto;
  1181. }
  1182. .example {
  1183. align-items: center;
  1184. }
  1185. section.settings select {
  1186. width: 50ch;
  1187. flex-shrink: 1;
  1188. }
  1189. </style>
  1190. <section class="settings">
  1191. <h2>Your Data</h2>
  1192. <div id="import-export" class="hbox">
  1193. <button id="export-clips">Export</button>
  1194. <input type="file" id="import-clips-file"/>
  1195. <button id="import-clips">Import</button>
  1196. </div>
  1197. <script>
  1198. $('#export-clips').addEventListener('click', evt => {
  1199. let cloneClips = JSON.parse(JSON.stringify(globalState.get('clips')));
  1200. for (let clip of cloneClips) delete clip.marker;
  1201. downloadFile('data:text/json;charset=utf-8,' + encodeURIComponent(
  1202. JSON.stringify(cloneClips)
  1203. ), 'clips.json');
  1204. });
  1205. $('#import-clips').addEventListener('click', evt => {
  1206. if ($('#import-clips-file').files.length > 0) {
  1207. let reader = new FileReader();
  1208. reader.onload = evt => {
  1209. let loadedClips = JSON.parse(evt.target.result);
  1210. globalState.set('clips', loadedClips);
  1211. globalState.set('playingClip', last(loadedClips));
  1212. globalState.set('previewClip', last(loadedClips));
  1213. }
  1214. reader.readAsText($('#import-clips-file').files[0]);
  1215. } else {
  1216. alert('You need to select the file containing your exported data.');
  1217. }
  1218. });
  1219. </script>
  1220. <hr>
  1221. <div class="hbox examples">
  1222. <h2>Examples</h2>
  1223. <button id="load-default-clips" onclick="loadDefaultClips()">Reload Defaults</button>
  1224. <button id="load-extended-clips" onclick="loadExtendedClips()">Load Extended</button>
  1225. </div>
  1226. <p>Warning: The extended set of examples may use large amounts of data.</p>
  1227. <hr>
  1228. <h2>Theme</h2>
  1229. <select id="theme-select">
  1230. <option value="Deep Dark">Deep Dark</option>
  1231. <option value="Light Blue">Light Blue</option>
  1232. <option value="Light Red">Light Red</option>
  1233. <option value="Popular">Popular</option>
  1234. <option value="Pinkin Blue">Pinkin Blue</option>
  1235. <option value="Pink Pank">Pink Pank</option>
  1236. <option value="80s">80's</option>
  1237. <option value="90s">90's</option>
  1238. <option value="00s">00's</option>
  1239. <option value="10s">10's</option>
  1240. <option value="custom">Custom</option>
  1241. <select>
  1242. <br>
  1243. <br>
  1244. <details id="set-colors">
  1245. <summary>Customize</summary>
  1246. Rounded Corners:
  1247. <input type="range" value="15" id="border-radius" />
  1248. <br>
  1249. Shadow Strength:
  1250. <input type="range" value="5" id="box-shadow-strength" class="proportion" />
  1251. <br>
  1252. Shadow Spread:
  1253. <input type="range" value="15" id="box-shadow-spread" />
  1254. <br>
  1255. Highlight Strength:
  1256. <input type="range" value="0" id="highlight-strength" class="proportion" />
  1257. <br>
  1258. <br>
  1259. <input type="color" id="body-background" >
  1260. <label for="body-background">background color</label>
  1261. <br>
  1262. <input type="color" id="body-color" >
  1263. <label for="body-color">text color</label>
  1264. <br>
  1265. <input type="color" id="chrome-color" >
  1266. <label for="chrome-color">chrome color</label>
  1267. <br>
  1268. <input type="color" id="chrome-background" >
  1269. <label for="chrome-background">chrome background</label>
  1270. <br>
  1271. <input type="color" id="chrome-border" >
  1272. <label for="chrome-border">chrome border</label>
  1273. <br>
  1274. <input type="color" id="accent" >
  1275. <label for="accent">accent</label>
  1276. <br>
  1277. <input type="color" id="accent-text" >
  1278. <label for="accent">accent text</label>
  1279. <br>
  1280. <input type="color" id="button-color" >
  1281. <label for="button-color">button color</label>
  1282. <br>
  1283. <input type="color" id="button-background" >
  1284. <label for="button-background">button background</label>
  1285. <br>
  1286. <input type="color" id="input-color" >
  1287. <label for="input-color">input color</label>
  1288. <br>
  1289. <input type="color" id="input-background" >
  1290. <label for="input-background">input background</label>
  1291. <br>
  1292. </details>
  1293. <script>
  1294. let themeState = new StateManager();
  1295. let themes = {
  1296. "Light Blue": {
  1297. "body-background":"#ffffff",
  1298. "body-color":"#000000",
  1299. "chrome-color":"#000000",
  1300. "chrome-background":"#f4f7f9",
  1301. "chrome-border":"#bfc2c4",
  1302. "accent":"#0fa9ff",
  1303. "accent-text":"#ffffff",
  1304. "button-color":"#000000",
  1305. "button-background":"#f7f7f7",
  1306. "input-color":"#000000",
  1307. "input-background":"#ffffff",
  1308. "border-radius":"30",
  1309. "box-shadow-strength":"11",
  1310. "box-shadow-spread":"30",
  1311. "highlight-strength":"24"
  1312. },
  1313. "Light Red":{"body-background":"#ffffff",
  1314. "body-color":"#401a25",
  1315. "chrome-color":"#401a25",
  1316. "chrome-background":"#f9f4f5",
  1317. "chrome-border":"#c4bfc0",
  1318. "accent":"#ff6d96",
  1319. "accent-text":"#ffffff",
  1320. "button-color":"#401a25",
  1321. "button-background":"#f7f7f7",
  1322. "input-color":"#401a25",
  1323. "input-background":"#ffffff",
  1324. "border-radius":"30",
  1325. "box-shadow-strength":"11",
  1326. "box-shadow-spread":"30",
  1327. "highlight-strength":"29"
  1328. },
  1329. "Pinkin Blue": {
  1330. "body-background":"#c6efff",
  1331. "body-color":"#2d2d2d",
  1332. "chrome-color":"#2d2d2d",
  1333. "chrome-background":"#aee8ff",
  1334. "chrome-border":"#25a5d8",
  1335. "accent":"#ff8cb3",
  1336. "accent-text":"#ffffff",
  1337. "button-color":"#000000",
  1338. "button-background":"#55cdfc",
  1339. "input-color":"#000000",
  1340. "input-background":"#ffffff",
  1341. "border-radius":"37",
  1342. "box-shadow-strength":"10",
  1343. "box-shadow-spread":"30",
  1344. "highlight-strength":"50"
  1345. },
  1346. "Pink Pank": {"body-background":"#ffe5f8",
  1347. "body-color":"#401a25",
  1348. "chrome-color":"#401a25",
  1349. "chrome-background":"#ffdcf7",
  1350. "chrome-border":"#ea9cd9",
  1351. "accent":"#dd55c6",
  1352. "accent-text":"#ffffff",
  1353. "button-color":"#000000",
  1354. "button-background":"#ffe8f9",
  1355. "input-color":"#401a25",
  1356. "input-background":"#ffffff",
  1357. "border-radius":"49",
  1358. "box-shadow-strength":"6",
  1359. "box-shadow-spread":"41",
  1360. "highlight-strength":"32"
  1361. },
  1362. "Deep Dark": {
  1363. "body-background":"#5e5e5e",
  1364. "body-color":"#f2f2f2",
  1365. "chrome-color":"#f2f2f2",
  1366. "chrome-background":"#565656",
  1367. "chrome-border":"#181818",
  1368. "accent":"#169CF9",
  1369. "accent-text":"#ffffff",
  1370. "button-color":"#ffffff",
  1371. "button-background":"#474747",
  1372. "input-color":"#f2f2f2",
  1373. "input-background":"#707070",
  1374. "border-radius":"8",
  1375. "box-shadow-strength":"8",
  1376. "box-shadow-spread":"40",
  1377. "highlight-strength":"9"
  1378. },
  1379. "Popular": {
  1380. "body-background":"#ffffff",
  1381. "body-color":"#474747",
  1382. "chrome-color":"#474747",
  1383. "chrome-background":"#ffffff",
  1384. "chrome-border":"#d1d1d1",
  1385. "accent":"#a74b91",
  1386. "accent-text":"#ffffff",
  1387. "button-color":"#000000",
  1388. "button-background":"#f7f7f7",
  1389. "input-color":"#000000",
  1390. "input-background":"#f7f7f7",
  1391. "border-radius":"32",
  1392. "box-shadow-strength":"8",
  1393. "box-shadow-spread":"50",
  1394. "highlight-strength":"0"
  1395. },
  1396. "Ubuntu" : {
  1397. "body-background": "#ffffff",
  1398. "body-color": "#000000",
  1399. "chrome-color": "#000000",
  1400. "chrome-background": "#f5f6f7",
  1401. "chrome-border": "#b4b7ba",
  1402. "accent": "#e95421",
  1403. "accent-text":"#ffffff",
  1404. "button-color": "#000000",
  1405. "button-background": "#f0f3f4",
  1406. "input-color": "#000000",
  1407. "input-background": "#ffffff",
  1408. "border-radius": "0.3",
  1409. "box-shadow-spread": "5",
  1410. "box-shadow-strength": "0.15",
  1411. "highlight-strength": "0.15"
  1412. },
  1413. "10s": {
  1414. "body-background": "#ffffff",
  1415. "body-color": "#2d2d2d",
  1416. "chrome-color": "#2d2d2d",
  1417. "chrome-background": "#f0f0f0",
  1418. "chrome-border": "#8d8d8d",
  1419. "accent": "#1979ca",
  1420. "accent-text":"#ffffff",
  1421. "button-color": "#000000",
  1422. "button-background": "#dcdcdc",
  1423. "input-color": "#000000",
  1424. "input-background": "#ffffff",
  1425. "border-radius": ".4",
  1426. "box-shadow-spread": "5",
  1427. "box-shadow-strength": "0.0",
  1428. "highlight-strength": "0.0"
  1429. },
  1430. "90s" : {
  1431. "body-background":"#fffee2",
  1432. "body-color":"#2d2d2d",
  1433. "chrome-color":"#2d2d2d",
  1434. "chrome-background":"#c0c0c0",
  1435. "chrome-border":"#404040",
  1436. "accent":"#000080",
  1437. "accent-text":"#ffffff",
  1438. "button-color":"#000000",
  1439. "button-background":"#c3c3c3",
  1440. "input-color":"#000000",
  1441. "input-background":"#ffffff",
  1442. "border-radius":"15",
  1443. "box-shadow-strength":"38",
  1444. "box-shadow-spread":"0",
  1445. "highlight-strength":"60"
  1446. },
  1447. "Pinkin Blues Old": {
  1448. "body-background": "#d8f4ff",
  1449. "body-color": "#2d2d2d",
  1450. "chrome-color": "#2d2d2d",
  1451. "chrome-background": "#aee8ff",
  1452. "chrome-border": "#25a5d8",
  1453. "accent": "#f7a8b8",
  1454. "accent-text": "#ffffff",
  1455. "button-color": "#000000",
  1456. "button-background": "#55cdfc",
  1457. "input-color": "#000000",
  1458. "input-background": "#ffffff",
  1459. "border-radius": ".6em",
  1460. "box-shadow-spread": "5px",
  1461. "box-shadow-strength": "0.15",
  1462. "highlight-strength": "0.40",
  1463. "border-radius":"32",
  1464. "box-shadow-strength":"8",
  1465. "box-shadow-spread":"5",
  1466. "highlight-strength":"0"
  1467. },
  1468. "80s": {
  1469. "body-background": "#000000",
  1470. "body-color": "#00ff0c",
  1471. "chrome-color": "#00ff0c",
  1472. "chrome-background": "#000000",
  1473. "chrome-border": "#00ff0c",
  1474. "accent": "#00ff0c",
  1475. "accent-text": "#000000",
  1476. "button-color": "#00ff0c",
  1477. "button-background": "#000000",
  1478. "input-color": "#00ff0c",
  1479. "input-background": "#000000",
  1480. "border-radius": "0em",
  1481. "box-shadow-spread": ".5",
  1482. "box-shadow-strength": "15",
  1483. "highlight-strength": "0.0"
  1484. },
  1485. "00s": {
  1486. "body-background": "#f4f3ee",
  1487. "body-color": "#000000",
  1488. "chrome-color": "#000000",
  1489. "chrome-background": "#ece9d8",
  1490. "chrome-border": "#76a2cb",
  1491. "accent": "#0070f6",
  1492. "accent-text": "#ffffff",
  1493. "button-color": "#000000",
  1494. "button-background": "#f3f3ef",
  1495. "input-color": "#000000",
  1496. "input-background": "#ffffff",
  1497. "border-radius": "12",
  1498. "box-shadow-spread": "20",
  1499. "box-shadow-strength": "15",
  1500. "highlight-strength": "15"
  1501. }
  1502. };
  1503. let theme = $('#theme-select').value;
  1504. loadTheme(themes[theme]);
  1505. for (let colorSelect of $$('#set-colors > input[type=color]')) {
  1506. if (!themeState.state.hasOwnProperty(colorSelect.id)) {
  1507. themeState.init(colorSelect.id, null);
  1508. }
  1509. themeState.render([colorSelect.id], current => {
  1510. document.body.style.setProperty('--' + colorSelect.id, current[colorSelect.id]);
  1511. });
  1512. }
  1513. for (let colorSelect of $$('#set-colors > input[type=color]')) {
  1514. themeState.sync(colorSelect.id, colorSelect, 'value');
  1515. themeState.set(colorSelect.id, colorSelect.value);
  1516. colorSelect.addEventListener('change', evt => {
  1517. $('#theme-select').value = 'custom';
  1518. });
  1519. }
  1520. for (let select of $$('#set-colors > input[type=range]')) {
  1521. themeState.sync(select.id, select, 'value');
  1522. select.addEventListener('change', evt => {
  1523. $('#theme-select').value = 'custom';
  1524. });
  1525. }
  1526. for (let select of $$('#set-colors > input[type=range]')) {
  1527. themeState.render([select.id], current => {
  1528. if (select.classList.contains('proportion')) {
  1529. document.body.style.setProperty('--' + select.id, current[select.id] / 100);
  1530. }
  1531. if (select.id == 'border-radius') {
  1532. document.body.style.setProperty('--' + select.id, (current[select.id] / 50) + 'em');
  1533. }
  1534. if (select.id == 'box-shadow-spread') {
  1535. document.body.style.setProperty('--' + select.id, (current[select.id] / 100) * 10 + 'px');
  1536. }
  1537. });
  1538. }
  1539. function doLoadTheme(theme) {
  1540. if (themes[theme]) {
  1541. loadTheme(themes[theme]);
  1542. } else {
  1543. setTimeout(() => doLoadTheme(theme), 1000);
  1544. }
  1545. }
  1546. $('#theme-select').addEventListener('change', evt => {
  1547. let theme = $('#theme-select').value;
  1548. doLoadTheme(theme);
  1549. });
  1550. for (let colorSelect of $$('#set-colors > input[type=color]')) {
  1551. if (!themeState.state.hasOwnProperty(colorSelect.id)) {
  1552. themeState.init(colorSelect.id, current[colorSelect.id]);
  1553. }
  1554. themeState.render([colorSelect.id], current => {
  1555. document.body.style.setProperty('--' + colorSelect.id, current[colorSelect.id]);
  1556. });
  1557. }
  1558. theme = $('#theme-select').value;
  1559. loadTheme(themes[theme]);
  1560. function nativeWidgets() {$('body').classList.remove('custom-widgets')};
  1561. function customWidgets() {$('body').classList.add('custom-widgets')};
  1562. function recordTheme() {
  1563. let theme = {};
  1564. for (let key in themeState.state) {
  1565. theme[key] = themeState.get(key);
  1566. }
  1567. alert(JSON.stringify(theme));
  1568. }
  1569. function loadTheme(theme) {
  1570. for (let key in theme) {
  1571. if (themeState.state.hasOwnProperty(key)) {
  1572. themeState.set(key, theme[key]);
  1573. } else {
  1574. themeState.init(key, theme[key]);
  1575. }
  1576. }
  1577. }
  1578. function nativeStyle() {
  1579. whiteLight();
  1580. document.body.style.setProperty('--body-background', 'White');
  1581. document.body.style.setProperty('--body-color', 'Black');
  1582. document.body.style.setProperty('--chrome-color', 'WindowText');
  1583. document.body.style.setProperty('--chrome-background', 'Window');
  1584. document.body.style.setProperty('--chrome-border', 'ButtonShadow');
  1585. document.body.style.setProperty('--accent', 'Highlight');
  1586. document.body.style.setProperty('--button-color', 'ButtonText');
  1587. document.body.style.setProperty('--button-background', 'ButtonFace');
  1588. document.body.style.setProperty('--input-color', '#000000');
  1589. document.body.style.setProperty('--border-radius', 15 / 50 + "em");
  1590. document.body.style.setProperty('--box-shadow-spread', .50 * 10 + "px");
  1591. document.body.style.setProperty('--box-shadow-strength', '0.05');
  1592. document.body.style.setProperty('--highlight-strength', '0');
  1593. document.querySelector('input.highlight-strength').value = '0';
  1594. nativeWidgets();
  1595. }
  1596. </script>
  1597. </section>
  1598. <style>
  1599. .wide-body .mobile-help {
  1600. display: none;
  1601. }
  1602. .mobile-help {
  1603. font-size: smaller;
  1604. font-style: italic;
  1605. }
  1606. .wide-body .mobile-help + p {
  1607. margin-top: 0px;
  1608. }
  1609. .help details + details {
  1610. margin-top: .5em;
  1611. }
  1612. .help .notice {
  1613. text-align: center;
  1614. margin-bottom: 0px;
  1615. font-weight: bold;
  1616. }
  1617. .notice + h2 {
  1618. margin-top: 0px;
  1619. }
  1620. </style>
  1621. <section class="help">
  1622. <!--p class='notice'>Help us out! Answer
  1623. <a href="https://brown.co1.qualtrics.com/jfe/form/SV_4PgW7EKNvnxnWLk">our survey</a>.
  1624. </p-->
  1625. <h2>Tutorial</h2>
  1626. <div class='mobile-help'>
  1627. Mobile users: You can tap the top edge of this pane
  1628. to expand/collapse it for easier reading.
  1629. </div>
  1630. <p>
  1631. <img src="resources/logo.png" alt="An acoustic guitar where the headstick is semi-triangular forming a Mars-sign along with the neck and sound hold. Likewise, the sound hole, strings, and bridge form a Venus sign. The image is in the pink, blue, and white transgender pride colors." style="float: right" width='128'/>
  1632. This app visualizes the pitch and resonance of voices in recordings over time.
  1633. <strong>Pitch</strong> is our perception of the frequency at which the vocal folds vibrate.
  1634. <strong>Resonance</strong> is the effect exerted on a sound by the passage the air travels through.
  1635. The program plots recordings in a <strong>2-dimensional space</strong>,
  1636. where the Y-axis represents pitch,
  1637. and the X-axis represents resonance,
  1638. with brighter sounds represented with a higher percentage.
  1639. </p>
  1640. <p>Typically, voices perceived as female have brighter resonance and higher pitch (top right),
  1641. while those perceived as male have darker resonance and lower pitch (bottom left).
  1642. These features can be blended in different degrees
  1643. to create a <strong>broad spectrum</strong> of perceptual and aesthetic qualities related to gender.
  1644. There are no sharp cutoffs at which a voice is
  1645. guaranteed to be gendered a specific way,
  1646. just as the colors in the graph blend seamlessly into one another.
  1647. <p>Press the <strong>play button</strong> in the top-left corner of the app to play the clip
  1648. associated with the selected point on the graph.
  1649. Examine some of the provided examples to get a sense
  1650. of how positions on the graph correspond to your perceptions of gender.
  1651. (Note: Example clips may take some time to show up on slow connections).
  1652. You can visualize your own recordings by pressing the <strong>add clip button</strong>
  1653. in the top-right and submitting a recording.
  1654. It will take some time to process,
  1655. so you may wish to examine the other clips while the recording is processed.
  1656. <p>Your clips are sent to our server for processing,
  1657. but they are <strong>not stored persistently</strong>.
  1658. You can save the sound file from your recording
  1659. using the <strong>download button</strong> in the "Details" tab.
  1660. From the settings tab,
  1661. you can also <strong>export</strong> all of your data in a single file
  1662. which can be imported for viewing at another time.
  1663. Otherwise, your data will be lost when you exit the page.
  1664. <p>We hope that using this program will help you understand how voices are gendered
  1665. and assess your own progress if you are trying to alter your voice
  1666. to better match your gender identity or desired presentation. Good luck!
  1667. <h2>FAQ</h2>
  1668. <details>
  1669. <summary>Why does the marker move around so much even when I'm not changing my voice drastically?</summary>
  1670. <p>Most speech has a broad range of values in pitch and resonance,
  1671. e.g. The highest and brightest phone spoken in a sample of typical male speech
  1672. may well be similar in pitch in resonance to the <em>average</em> phone in typical female speech.
  1673. Our perception of a voice is based on the overall tendencies within speech
  1674. rather than the moment-to-moment features of individual phones.</p>
  1675. </details>
  1676. <details>
  1677. <summary>Why do all the markers start so close together?</summary>
  1678. <p>When not playing, the markers are displayed at the median values across the whole clip.
  1679. Because there's a wide range of values in most speech,
  1680. this pushes the median towards the center.
  1681. However, a small change in median values
  1682. can have a big influence on how a voice is gendered</p>
  1683. </details>
  1684. <details>
  1685. <summary>Does this program support languages other than English?</summary>
  1686. <p>Not currently.
  1687. If you add recording in another language,
  1688. you <em>might</em> get an output,
  1689. but it will probably not be very accurate.</p>
  1690. </details>
  1691. <details>
  1692. <summary>Do accent and dialect affect pitch and resonance?</summary>
  1693. <p>Yes, so you may wish to upload recordings of other speakers with your accent or dialect
  1694. to better compare with your own voice.</p>
  1695. </details>
  1696. <details>
  1697. <summary>Ok, I can see my resonance now, but how do I actually change it?</summary>
  1698. <p>There are several ways. One is to raise (brightening) or lower (darkening) your larynx.
  1699. Your larynx raises when you swallow and lowers when you yawn.
  1700. You may be able to feel it move if you put your hand over the front of your neck
  1701. while you make one of these movements.
  1702. If you can learn to control your larynx position in normal speech,
  1703. it can have a big effect on your resonance.
  1704. </p>
  1705. <p>
  1706. Your resonance is also affected by the space in your mouth.
  1707. Holding your tongue higher in your mouth as you speak will brighten your resonance,
  1708. and holding it lower will darken it.
  1709. </p>
  1710. </details>
  1711. <details>
  1712. <summary>Even though my pitch and resonance seem to be in the right spot,
  1713. I don't feel like my voice sounds like I want it to.</summary>
  1714. <p>There are many dimensions to voice other than pitch and resonance,
  1715. so you might need to target other features.
  1716. You might also be straining to reach a particular point,
  1717. which can be audible to a listener.
  1718. It might help to pick a less extreme target
  1719. which you can reach comfortably.</p>
  1720. </details>
  1721. <details>
  1722. <summary>Can I donate to encourage further development?</summary>
  1723. <p>Yes! You can donate through any of the following platforms:</p>
  1724. <ul>
  1725. <li><a href="https://liberapay.com/lmcnulty">Liberapay</a> - Recurring donations, no extra fees, free-software focus
  1726. <li><a href="https://ko-fi.com/lmcnulty">Ko-fi</a> - One-time or recurring donations, no extra fees
  1727. <li><a href="https://www.patreon.com/lmcnulty">Patreon</a> - Recurring donations, takes a cut
  1728. </ul>
  1729. </details>
  1730. <h2>Credits</h2>
  1731. <p>This application is developed by <a href="https://lmcnulty.me">Luna McNulty</a>
  1732. as part of a research project for her Sc. M. program at Brown University.
  1733. Its content is available under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>
  1734. and uses Creative-Commons-licensed sound files from
  1735. Morgan and Christine Lemmer-Webber's podcast <a href="https://fossandcrafts.org">Foss and Crafts</a>
  1736. as well as public domain content audiobook content from <a href="librivox.org">Librivox</a>.</p>
  1737. </section>
  1738. <!-- Details -->
  1739. <style>
  1740. .details h2 {
  1741. margin-bottom: .5em;
  1742. margin-top: 0px;
  1743. }
  1744. .details {
  1745. position: relative;
  1746. }
  1747. .details table {
  1748. margin-bottom: .5em;
  1749. margin-top: .5em;
  1750. border-collapse: collapse;
  1751. margin: auto;
  1752. }
  1753. .details table td, .details table th {
  1754. border: 1px solid var(--chrome-border);
  1755. padding: .25em;
  1756. text-align: center;
  1757. }
  1758. .details .view {
  1759. display: none;
  1760. }
  1761. .details.has-selection .view {
  1762. display: block;
  1763. }
  1764. .details.has-selection .instructions{
  1765. display: none;
  1766. }
  1767. .details .hbox h2 {
  1768. display: inline;
  1769. margin: 0px;
  1770. margin-right: auto;
  1771. }
  1772. .details .hbox > * + * {
  1773. margin-left: .5em;
  1774. }
  1775. .details .hbox {
  1776. display: flex;
  1777. flex-wrap: wrap;
  1778. align-items: center;
  1779. margin-bottom: 1em;
  1780. height: 2em;
  1781. }
  1782. .details label {
  1783. font-weight: bold;
  1784. }
  1785. td.highlight {
  1786. background: var(--accent);
  1787. color: white;
  1788. color: var(--accent-text);
  1789. }
  1790. .details details {
  1791. margin-top: .5em;
  1792. }
  1793. .details summary {
  1794. margin-bottom: .5em;
  1795. }
  1796. #delete-confirm.hidden {
  1797. display: inline-block;
  1798. overflow: hidden;
  1799. opacity: 0;
  1800. width: 1ch;
  1801. height: 1ch;
  1802. visibility: hidden;
  1803. }
  1804. #delete-confirm {
  1805. transition: .3s all;
  1806. }
  1807. </style>
  1808. <section class="details">
  1809. <h2 class='instructions'>Clip Details</h2>
  1810. <div class='instructions'>
  1811. When you select a clip/point from the graph it will be displayed here.
  1812. If you just loaded the page and are seeing this,
  1813. your browser is downloading the examples
  1814. and they will display as they arrive.
  1815. If you are on a slow connection,
  1816. you may wish to start by reading the instructions in the help tab
  1817. while you wait.
  1818. </div>
  1819. <div class='view'>
  1820. <div class="hbox">
  1821. <h2 id="preview-title"></h2>
  1822. <span class='buttons'>
  1823. <span id='delete-confirm' class='hidden'>Press again to delete</span>
  1824. <button class='delete' title='Delete Clip'><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
  1825. <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
  1826. <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
  1827. </svg></button>
  1828. <button class='download' title='Download Clip'><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download" viewBox="0 0 16 16">
  1829. <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
  1830. <path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
  1831. </svg></button>
  1832. </span>
  1833. </div>
  1834. <table>
  1835. <tr>
  1836. <th></th>
  1837. <th>Mean</th>
  1838. <th>Median</th>
  1839. <th>St. dev.</th>
  1840. </tr>
  1841. <tr>
  1842. <th>Pitch</th>
  1843. <td id='preview-mean-pitch'>--</td>
  1844. <td id='preview-median-pitch'>--</td>
  1845. <td class='highlight'><span id='preview-stdev-pitch'></span>*</td>
  1846. </tr>
  1847. <tr>
  1848. <th>Resonance</th>
  1849. <td id='preview-mean-resonance'>--</td>
  1850. <td id='preview-median-resonance'>--</td>
  1851. <td id='preview-stdev-resonance'>--</td>
  1852. </tr>
  1853. </table>
  1854. <br>
  1855. <div>
  1856. <label for="preview-transcript">Transcript</label>:
  1857. <span id='preview-transcript'></span>
  1858. </div>
  1859. <details>
  1860. <summary>Advanced</summary>
  1861. <button id='dl-json'>Download JSON</button>
  1862. </details>
  1863. <hr>
  1864. <div>
  1865. <small>* In many contexts, greater variation in pitch (standard deviation) is
  1866. associated with women's speech, whereas monotone is associated with men's speech.
  1867. </small>
  1868. </div>
  1869. </div>
  1870. </section>
  1871. <script>
  1872. globalState.render(['previewClip'], current => {
  1873. if (current.previewClip) {
  1874. $('.details').classList.add('has-selection');
  1875. $('#preview-title').innerHTML = current.previewClip.title || '';
  1876. if (current.previewClip.meanPitch) {
  1877. $('#preview-mean-pitch').innerHTML = Math.round(current.previewClip.meanPitch) + 'Hz';
  1878. $('#preview-mean-resonance').innerHTML = Math.round(current.previewClip.meanResonance * 100) + '%';
  1879. $('#preview-stdev-pitch').innerHTML = Math.round(current.previewClip.stdevPitch) + 'Hz';
  1880. $('#preview-stdev-resonance').innerHTML = Math.round(current.previewClip.stdevResonance * 100) + '%';
  1881. $('#preview-median-pitch').innerHTML = Math.round(current.previewClip.medianPitch) + 'Hz';
  1882. $('#preview-median-resonance').innerHTML = Math.round(current.previewClip.medianResonance * 100) + '%';
  1883. }
  1884. if (current.previewClip.transcript) {
  1885. $('#preview-transcript').innerHTML = current.previewClip.transcript;
  1886. }
  1887. } else {
  1888. $('.details').classList.remove('has-selection');
  1889. }
  1890. })
  1891. $('section.details button.download').addEventListener('click', evt => {
  1892. let previewClip = globalState.get('previewClip');
  1893. downloadFile(previewClip.audio, previewClip.title);
  1894. });
  1895. let lastDeletePress = 0;
  1896. $('section.details button.delete').addEventListener('click', evt => {
  1897. if (Date.now() - lastDeletePress > 4000) {
  1898. $('#delete-confirm').classList.remove('hidden');
  1899. lastDeletePress = Date.now();
  1900. window.setTimeout(() => {
  1901. $('#delete-confirm').classList.add('hidden');
  1902. }, 4000)
  1903. } else {
  1904. let previewClip = globalState.get('previewClip');
  1905. globalState.mutate('clips', clips => {
  1906. let index = clips.indexOf(previewClip);
  1907. clips.splice(index, 1);
  1908. })
  1909. globalState.set('previewClip', null);
  1910. }
  1911. });
  1912. $('#dl-json').addEventListener('click', evt => {
  1913. let previewClip = globalState.get('previewClip');
  1914. let cloneClip = JSON.parse(JSON.stringify(previewClip));
  1915. delete cloneClip.marker;
  1916. downloadFile(
  1917. 'data:text/json;charset=utf-8,'
  1918. + encodeURIComponent(JSON.stringify(cloneClip)),
  1919. previewClip.title + '.json'
  1920. );
  1921. })
  1922. </script>
  1923. <section class="new-clip">
  1924. <style>
  1925. #record-voice + label + div, #upload-file + label + div {
  1926. margin-left: 1em;
  1927. padding: .5em;
  1928. }
  1929. #script-select {
  1930. max-width: calc(100% - 6ch);
  1931. }
  1932. #title-input {
  1933. max-width: calc(100% - 16ch);
  1934. }
  1935. section.new-clip .new-clip {
  1936. margin-bottom: 1em;
  1937. }
  1938. .new-clip .hbox {
  1939. display: flex;
  1940. align-items: center;
  1941. }
  1942. .new-clip .hbox select, .hbox input[type=text] {
  1943. width: 100%;
  1944. flex-shrink: 1;
  1945. }
  1946. .new-clip .hbox label {
  1947. margin-left: 1em;
  1948. margin-right: .5em;
  1949. }
  1950. .new-clip .hbox label:first-child {
  1951. margin-left: 0px;
  1952. }
  1953. .new-clip h2 {
  1954. margin-top: 0px;
  1955. }
  1956. .new-clip #submit {
  1957. float: right;
  1958. }
  1959. #upload-file + label + div, #record-voice + label + div {
  1960. }
  1961. /* Show upload/recording elements only when that option is selected. */
  1962. #record-voice + label + div { display: none; }
  1963. #record-voice:checked + label + div { display: block; }
  1964. #upload-file + label + div { display: none;}
  1965. #upload-file:checked + label + div { display: block;}
  1966. </style>
  1967. <h2>Add a Clip</h2>
  1968. <div class="hbox">
  1969. <label for="title-input">Title</label>
  1970. <input type="text" id="title-input" value="Untitled"/>
  1971. <label for="color-select">Color</label>
  1972. <input type="color" id="color-select" value="#000000"/>
  1973. </div>
  1974. <br>
  1975. <!-- Script selection -->
  1976. <div class="hbox">
  1977. <label for="script-select">Script </label>
  1978. <select class="script" id="script-select">
  1979. <option value="When the sunlight strikes raindrops in the air, they act as a prism and form a rainbow. The rainbow is a division of white light into many beautiful colors. These take the shape of a long round arch, with its path high above, and its two ends apparently beyond the horizon. There is , according to legend, a boiling pot of gold at one end. People look, but no one ever finds it."
  1980. >Rainbow Passage</option>
  1981. <option value="You wish to know all about my grandfather. Well, he is nearly 93 years old, yet he still thinks as swiftly as ever. He dresses himself in an old black frock coat, usually several buttons missing. A long beard clings to his chin, giving those who observe him a pronounced feeling of the utmost respect. When he speaks, his voice is just a bit cracked and quivers a bit. Twice each day he plays skillfully and with zest upon a small organ. Except in the winter when the snow or ice prevents, he slowly takes a short walk in the open air each day. We have often urged him to walk more and smoke less, but he always answers, “Banana oil!” Grandfather likes to be modern in his language.">
  1982. Grandfather passage</option>
  1983. <option value="Please call Stella. Ask her to bring these things with her from the store: Six spoons of fresh snow peas, five thick slabs of blue cheese, and maybe a snack for her brother Bob. We also need a small plastic snake and a big toy frog for the kids. She can scoop these things into three red bags, and we will go meet her Wednesday at the train station."
  1984. >Please call stella</option>
  1985. <option value="
  1986. That quick beige fox jumps in the air over each thin dog. Look out, I shout, for he's foiled you again, creating chaos."
  1987. >Phonetic Pangram: Quick beige fox...</option>
  1988. <option value="
  1989. The beige hue on the waters of the loch impressed all, including the French queen, before she heard that symphony again, just as young Arthur wanted."
  1990. >Phonetic Pangram: Waters of the loch</option>
  1991. <option value="
  1992. I am afraid to own a Body—
  1993. I am afraid to own a Soul—
  1994. Profound—precarious Property—
  1995. Possession, not optional—
  1996. Double Estate—entailed at pleasure
  1997. Upon an unsuspecting Heir—
  1998. Duke in a moment of Deathlessness
  1999. And God, for a Frontier."
  2000. >Emily Dickinson: Afraid to own a body</option>
  2001. <option value="
  2002. Rearrange a ''Wife's'' affection!
  2003. When they dislocate my Brain!
  2004. Amputate my freckled Bosom!
  2005. Make me bearded like a man!
  2006. Blush, my spirit, in thy Fastness —
  2007. Blush, my unacknowledged clay —
  2008. Seven years of troth have taught thee
  2009. More than Wifehood every may!"
  2010. >Emily Dickinson: Rearrange a "Wife's" affection</option>
  2011. <option value="
  2012. Yet be thou mindful ever, —
  2013. Heat from fire, fire from heat —
  2014. None can see sever, none can sever "
  2015. >Heat from fire</option>
  2016. <option value="
  2017. The moon is in doubt
  2018. over whether to be
  2019. a man or a woman.
  2020. There've been rumors,
  2021. all manner of allegations,
  2022. bold claims and public lies:
  2023. He's belligerent. She's in a funk.
  2024. When he fades, the world teeters.
  2025. When she burgeons, crime blossoms.
  2026. O how the operatic impulse wavers!
  2027. Dip deep, my darling, into the blank pool."
  2028. >Rita Dove: Trans-</option>
  2029. <option value="
  2030. Oh, but had the artisan who made me
  2031. created me instead — a fair woman.
  2032. Today I would be wise and insightful.
  2033. We would weave, my friends and I,
  2034. and in the moonlight spin our yarn,
  2035. and tell our stories to one another,
  2036. from dusk till midnight."
  2037. >Kalonymus: Even Bochan</option>
  2038. <option value="
  2039. From childhood's hour I have not been
  2040. As others were — I have not seen
  2041. As others saw — I could not bring
  2042. My passions from a common spring —
  2043. From the same source I have not taken
  2044. My sorrow — I could not awaken
  2045. My heart to joy at the same tone —
  2046. And all I lov'd — I lov'd alone."
  2047. >Edgar Allan Poe: Alone</option>
  2048. <option value="custom">Custom</option>
  2049. }
  2050. </select>
  2051. </div>
  2052. <textarea rows="6" id="transcript"></textarea>
  2053. <script>
  2054. globalState.render(['transcript'], current => {
  2055. $('#transcript').value = current.transcript;
  2056. });
  2057. function formatTranscript(value) {
  2058. return value.replace(/\s\s+/g, '\n').trim()
  2059. }
  2060. globalState.set('transcript', formatTranscript($('#script-select').value));
  2061. $('#script-select').addEventListener('change', evt => {
  2062. let value = $('#script-select').value;
  2063. globalState.set('transcript', value == 'custom' ? '' : formatTranscript(value));
  2064. });
  2065. $('#transcript').addEventListener('change', evt => {
  2066. if (globalState.get('transcript') != $('#transcript').value) {
  2067. globalState.set('transcript', $('#transcript').value);
  2068. $('#script-select').value = 'custom';
  2069. }
  2070. });
  2071. </script>
  2072. <!-- Upload Options -->
  2073. <input type="radio" name="add-recording" id="upload-file" checked>
  2074. <label for="upload-file">Upload a file</label>
  2075. <div>
  2076. <input type='file' id='audio-file-input' name='recording'
  2077. capture="user" accept="audio/*,video/*,audio/mpeg,audio/mp4,audio/x-aiff,audio/ogg,audio/vorbis,audio/opus,audio/vnd.wav,.wav,.mp3,.ogg,.opus,.m4a,.webm"/>
  2078. </div>
  2079. <br>
  2080. <input type="radio" name="add-recording" id="record-voice">
  2081. <label for="record-voice">Record your voice</label>
  2082. <div>
  2083. <button id="start-recording">⏺︎ Start</button>
  2084. <button id="stop-recording">⏹︎ Stop</button>
  2085. Recorded <span id="recording-count">0</span> seconds
  2086. <br>
  2087. <br>
  2088. <audio controls id="recording-preview"></audio>
  2089. </div>
  2090. <!-- Submit -->
  2091. <button id="submit" class='attention' onclick="submitAudio()">Add Clip</button>
  2092. <script>
  2093. try {
  2094. // record from their microphone in-browser,
  2095. let mediaRecorder = null;
  2096. let audioChunks = [];
  2097. let recordedAudio = null;
  2098. function recordAudio() {
  2099. let stopButton = document.querySelector("#stop-recording");
  2100. let startButton = document.querySelector("#start-recording");
  2101. const startRecording = () => {
  2102. $('#recording-count').innerHTML = '0';
  2103. let recordingInterval = setInterval(() => {
  2104. $('#recording-count').innerHTML = Number($('#recording-count').innerHTML) + 1;
  2105. }, 1000);
  2106. mediaRecorder.addEventListener("stop", () => {
  2107. clearInterval(recordingInterval);
  2108. });
  2109. stopButton.disabled = false;
  2110. startButton.disabled = true;
  2111. audioChunks = [];
  2112. mediaRecorder.start();
  2113. mediaRecorder.addEventListener("dataavailable", event => {
  2114. audioChunks.push(event.data);
  2115. });
  2116. }
  2117. function alertRecordingFailure(exception) {
  2118. alert("Sorry, something went wrong. "
  2119. + "Your device's browser may not be able to record audio. "
  2120. + "Try uploading a file recorded from a native app instead "
  2121. + "or using a different browser."
  2122. );
  2123. console.error(exception);
  2124. }
  2125. if (mediaRecorder) { startRecording(); } else {
  2126. try {
  2127. navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
  2128. mediaRecorder = new MediaRecorder(stream);
  2129. mediaRecorder.addEventListener("stop", () => {
  2130. stopButton.disabled = true;
  2131. startButton.disabled = false;
  2132. const audioBlob = new Blob(audioChunks);
  2133. recordedAudio = audioBlob;
  2134. let reader = new FileReader(recordedAudio);
  2135. reader.readAsDataURL(recordedAudio);
  2136. reader.addEventListener("load", () => {
  2137. $('#recording-preview').src = reader.result.replace(
  2138. /data:.*;base64/, "data:audio/wav;base64"
  2139. );
  2140. $('#recording-preview').load();
  2141. });
  2142. });
  2143. stopButton.addEventListener("click", () => {mediaRecorder.stop();}, false);
  2144. startRecording();
  2145. }).catch(exception => alertRecordingFailure(exception));
  2146. } catch(exception) { alertRecordingFailure(exception); }
  2147. }
  2148. }
  2149. $('#start-recording').addEventListener('click', evt => {
  2150. recordAudio();
  2151. });
  2152. function submitAudio() {
  2153. let clip = new Clip();
  2154. let formData = new FormData();
  2155. let transcriptText = $('#transcript').value;
  2156. formData.append('transcript', transcriptText);
  2157. formData.append('referrer', document.referrer);
  2158. formData.append('screen-width', window.screen.width);
  2159. formData.append('screen-height', window.screen.height);
  2160. formData.append('lang', navigator.language || navigator.userLanguage);
  2161. clip.transcript = transcriptText;
  2162. clip.title = $('#title-input').value;
  2163. clip.color = $('#color-select').value;
  2164. if ($('#upload-file').checked) {
  2165. fileInput = $('#audio-file-input')
  2166. if (fileInput.files.length < 1) return
  2167. let recording = fileInput.files[0];
  2168. formData.append('recording', recording);
  2169. clip.loadAudioFile(recording);
  2170. } else if ($('#record-voice').checked) {
  2171. formData.append('recording', recordedAudio);
  2172. clip.loadAudioFile(recordedAudio);
  2173. } else {
  2174. alert('Please upload a file or make a recording.');
  2175. return;
  2176. }
  2177. $('button.new-clip').classList.add('hidden');
  2178. $('button.new-clip + .spinner').classList.remove('hidden');
  2179. hideModal();
  2180. fetch('./backend.cgi', { method: 'POST', body: formData})
  2181. .then(response => {
  2182. if (!response.ok) throw new Error(response.status);
  2183. if (response.headers.get('Content-Length') < 2) return {};
  2184. return response.json();
  2185. }).then(data => {
  2186. $('button.new-clip').classList.remove('hidden');
  2187. $('button.new-clip + .spinner').classList.add('hidden');
  2188. console.assert(data.hasOwnProperty('phones') && data.phones.length < 1);
  2189. clip.loadResponse(data);
  2190. globalState.mutate('clips', clips => clips.push(clip));
  2191. globalState.set('playing', false);
  2192. globalState.set('playingClip', clip);
  2193. globalState.set('previewClip', clip);
  2194. }).catch(() => {
  2195. alert(
  2196. "Sorry, we weren't able to process your recording. " +
  2197. "Make sure to speak clearly and ensure " +
  2198. "that your transcript matches the audio. " +
  2199. "For increased chance of success," +
  2200. "try using short clips with a 1-second period of silence."
  2201. );
  2202. $('button.new-clip').classList.remove('hidden');
  2203. $('button.new-clip + .spinner').classList.add('hidden');
  2204. });
  2205. }
  2206. } catch (exception) {
  2207. alert("An unknown error occured.");
  2208. console.error(exception);
  2209. }
  2210. </script>
  2211. </section>
  2212. </div>
  2213. let darkIcon = String(`
  2214. <svg
  2215. xmlns:dc="http://purl.org/dc/elements/1.1/"
  2216. xmlns:cc="http://creativecommons.org/ns#"
  2217. xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  2218. xmlns:svg="http://www.w3.org/2000/svg"
  2219. xmlns="http://www.w3.org/2000/svg"
  2220. id="svg5181"
  2221. version="1.1"
  2222. viewBox="0 0 8.5024996 8.5054264"
  2223. height="32.146492"
  2224. width="32.135433">
  2225. <defs
  2226. id="defs5175" />
  2227. <metadata
  2228. id="metadata5178">
  2229. <rdf:RDF>
  2230. <cc:Work
  2231. rdf:about="">
  2232. <dc:format>image/svg+xml</dc:format>
  2233. <dc:type
  2234. rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
  2235. <dc:title></dc:title>
  2236. </cc:Work>
  2237. </rdf:RDF>
  2238. </metadata>
  2239. <g
  2240. transform="translate(-24.474941,-137.0211)"
  2241. id="layer1">
  2242. <rect style="fill:none;fill-opacity:1;stroke:currentColor;stroke-width:0.56500006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0"
  2243. id="rect4546-3"
  2244. width="7.9375"
  2245. height="7.9375"
  2246. x="24.757441"
  2247. y="137.30507" />
  2248. <path
  2249. id="path4571"
  2250. d="m 27.40326,137.30506 c -3.43959,1.32291 -3.43959,6.61458 0,7.9375"
  2251. style="fill:none;stroke:currentColor;stroke-width:0.565;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
  2252. <path
  2253. style="fill:none;stroke:currentColor;stroke-width:0.565;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
  2254. d="m 27.93242,138.09881 c -2.64583,1.85208 -2.64583,4.49791 0,6.35"
  2255. id="path4573" />
  2256. <path
  2257. style="fill:none;stroke:currentColor;stroke-width:0.565;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
  2258. d="m 30.0491,137.30506 c 3.43959,1.32291 3.43959,6.61458 0,7.9375"
  2259. id="path5740" />
  2260. <path
  2261. id="path5742"
  2262. d="m 29.51994,138.09881 c 2.64583,1.85208 2.64583,4.49791 0,6.35"
  2263. style="fill:none;stroke:currentColor;stroke-width:0.565;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
  2264. </g>
  2265. </svg>
  2266. `)
  2267. let brightIcon = String(`
  2268. <svg
  2269. xmlns:dc="http://purl.org/dc/elements/1.1/"
  2270. xmlns:cc="http://creativecommons.org/ns#"
  2271. xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  2272. xmlns:svg="http://www.w3.org/2000/svg"
  2273. xmlns="http://www.w3.org/2000/svg"
  2274. xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
  2275. xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
  2276. width="32.135433"
  2277. height="32.153236"
  2278. viewBox="0 0 8.5025005 8.5072107"
  2279. version="1.1"
  2280. id="svg4565"
  2281. inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
  2282. sodipodi:docname="bright.svg">
  2283. <defs id="defs4559">
  2284. <link href="style.css" type="text/css" rel="stylesheet"
  2285. xmlns="http://www.w3.org/1999/xhtml"/>
  2286. </defs>
  2287. <sodipodi:namedview
  2288. id="base"
  2289. pagecolor="#ffffff"
  2290. bordercolor="#666666"
  2291. borderopacity="1.0"
  2292. inkscape:pageopacity="0.0"
  2293. inkscape:pageshadow="2"
  2294. inkscape:zoom="11.2"
  2295. inkscape:cx="12.379905"
  2296. inkscape:cy="11.113832"
  2297. inkscape:document-units="mm"
  2298. inkscape:current-layer="svg4565"
  2299. showgrid="false"
  2300. fit-margin-top="0"
  2301. fit-margin-left="0"
  2302. fit-margin-right="0"
  2303. fit-margin-bottom="0"
  2304. units="px"
  2305. inkscape:window-width="1920"
  2306. inkscape:window-height="1002"
  2307. inkscape:window-x="0"
  2308. inkscape:window-y="0"
  2309. inkscape:window-maximized="1" />
  2310. <metadata
  2311. id="metadata4562">
  2312. <rdf:RDF>
  2313. <cc:Work
  2314. rdf:about="">
  2315. <dc:format>image/svg+xml</dc:format>
  2316. <dc:type
  2317. rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
  2318. <dc:title></dc:title>
  2319. </cc:Work>
  2320. </rdf:RDF>
  2321. </metadata>
  2322. <g
  2323. inkscape:label="Layer 1"
  2324. inkscape:groupmode="layer"
  2325. id="layer1"
  2326. transform="translate(-32.790418,-144.57973)">
  2327. <rect
  2328. style="fill:none;fill-opacity:1;stroke:currentColor;stroke-width:0.56500006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0"
  2329. id="rect4581"
  2330. width="7.9375"
  2331. height="7.9375"
  2332. x="33.072918"
  2333. y="144.86459"
  2334. inkscape:export-xdpi="191.19"
  2335. inkscape:export-ydpi="191.19" />
  2336. <path
  2337. sodipodi:nodetypes="cc"
  2338. inkscape:connector-curvature="0"
  2339. id="path4583"
  2340. d="m 33.866667,144.86458 c 1.85208,1.32292 1.85208,6.61459 0,7.9375"
  2341. style="fill:none;stroke:currentColor;stroke-width:0.56500006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
  2342. <path
  2343. style="fill:none;stroke:currentColor;stroke-width:0.56500006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
  2344. d="m 35.454167,145.39375 c 1.32291,1.85209 1.32291,5.02709 0,6.87917"
  2345. id="path4585"
  2346. inkscape:connector-curvature="0"
  2347. sodipodi:nodetypes="cc" />
  2348. <path
  2349. style="fill:none;stroke:currentColor;stroke-width:0.56500006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
  2350. d="m 40.216657,144.86459 c -1.85208,1.32292 -1.85208,6.61459 0,7.9375"
  2351. id="path833"
  2352. inkscape:connector-curvature="0"
  2353. sodipodi:nodetypes="cc" />
  2354. <path
  2355. sodipodi:nodetypes="cc"
  2356. inkscape:connector-curvature="0"
  2357. id="path835"
  2358. d="m 38.629157,145.39376 c -1.32291,1.85209 -1.32291,5.02709 0,6.87917"
  2359. style="fill:none;stroke:currentColor;stroke-width:0.56500006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
  2360. </g>
  2361. </svg>
  2362. `)
  2363. <script src="./ui/index.js"></script>
  2364. <script src="./ui/voice-graph.js"></script>
  2365. </body>
  2366. </html>