Added sound management and ppt delay
parent
2f81479519
commit
9234b95d5b
|
@ -1,4 +1,9 @@
|
||||||
# Changelog:
|
# Changelog:
|
||||||
|
* **27.01.19**
|
||||||
|
- Made sounds configurable
|
||||||
|
- Added option to mute sounds when output is muted
|
||||||
|
- Added push to talk delay option
|
||||||
|
|
||||||
* **26.01.19**
|
* **26.01.19**
|
||||||
- Improved TeaSpeak identities (now generates automatic and are saveable)
|
- Improved TeaSpeak identities (now generates automatic and are saveable)
|
||||||
- Fixed `connect_profile` parameter within URL
|
- Fixed `connect_profile` parameter within URL
|
||||||
|
|
|
@ -386,4 +386,11 @@ html, body {
|
||||||
background: url('../img/music/playlist.svg') no-repeat;
|
background: url('../img/music/playlist.svg') no-repeat;
|
||||||
background-position: -11px -9px;
|
background-position: -11px -9px;
|
||||||
background-size: 50px;
|
background-size: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
|
@ -1,3 +1,11 @@
|
||||||
|
.help-tip-container {
|
||||||
|
/* position: relative; */
|
||||||
|
|
||||||
|
.help-tip {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.help-tip {
|
.help-tip {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
||||||
|
@ -21,7 +29,7 @@
|
||||||
color:#fff;
|
color:#fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover, &.show {
|
||||||
p {
|
p {
|
||||||
display:block;
|
display:block;
|
||||||
transform-origin: 100% 0%;
|
transform-origin: 100% 0%;
|
||||||
|
|
|
@ -18,6 +18,16 @@
|
||||||
.group_box {
|
.group_box {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.sound {
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-device {
|
.settings-device {
|
||||||
|
@ -96,6 +106,180 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.property {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.key {
|
||||||
|
width: 120px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ppt-delay {
|
||||||
|
margin-top: 5px;
|
||||||
|
.value {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
right: .5em;
|
||||||
|
transition: all .05s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::after {
|
||||||
|
right: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: 'ms';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sound-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.property {
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.key {
|
||||||
|
width: 150px;
|
||||||
|
|
||||||
|
&.muted-sounds {
|
||||||
|
width: 230px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
&.master-volume {
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin-left: 5px;
|
||||||
|
width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sound-list {
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.column {
|
||||||
|
&.sound-name {
|
||||||
|
width: calc(100% - 150px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sound-activated {
|
||||||
|
width: 150px;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin-left: 75px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sound-list-header {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
.column {
|
||||||
|
border: 1px solid lightgray;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sound-list-entries-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: start;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
min-height: 400px;
|
||||||
|
max-height: 400px;
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.column {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #00000022;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-playback:hover {
|
||||||
|
background-color: #00000022;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.scrollbar {
|
||||||
|
.column {
|
||||||
|
&.sound-name {
|
||||||
|
width: calc(100% - 150px + 60px)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sound-list-filter {
|
||||||
|
margin-top: 3px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -640,8 +640,16 @@
|
||||||
{{tr "There are no setting entries for an <b>always</b> online voice detection."/}}
|
{{tr "There are no setting entries for an <b>always</b> online voice detection."/}}
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-vad-impl-entry setting-vad-ppt">
|
<div class="settings-vad-impl-entry setting-vad-ppt">
|
||||||
<a>{{tr "Push to talk key:"/}}</a>
|
<div class="property ppt-key">
|
||||||
<button class="vat_ppt_key">{{tr "Uninitialised"/}}</button>
|
<div class="key">{{tr "Push to talk key:"/}}</div>
|
||||||
|
<div class="value"><button class="vat_ppt_key">{{tr "Uninitialised"/}}</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="property ppt-delay">
|
||||||
|
<div class="key">{{tr "Key release delay:" /}}</div>
|
||||||
|
<div class="value">
|
||||||
|
<input type="number" min="0" max="5000">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-vad-impl-entry setting-vad-vad">
|
<div class="settings-vad-impl-entry setting-vad-vad">
|
||||||
<div>{{tr "Voice activity threshold (<a class='vad_vad_slider_value'>20</a>%)"/}}</div>
|
<div>{{tr "Voice activity threshold (<a class='vad_vad_slider_value'>20</a>%)"/}}</div>
|
||||||
|
@ -670,6 +678,68 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="group_box sound">
|
||||||
|
<div class="header">{{tr "Sound Settings" /}}</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="sound-settings">
|
||||||
|
<div class="property">
|
||||||
|
<div class="key">Master volume:</div>
|
||||||
|
<div class="value master-volume">
|
||||||
|
<input type="range" min="0" max="100" value="100">
|
||||||
|
<a>(66%)</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="property">
|
||||||
|
<div class="key">
|
||||||
|
Overlap same sounds:
|
||||||
|
</div>
|
||||||
|
<div class="value overlap-sounds">
|
||||||
|
<input type="checkbox">
|
||||||
|
<div class="help-tip-container"> <!-- lets be absolute -->
|
||||||
|
<div class="help-tip tip-right tip-small">
|
||||||
|
<p>
|
||||||
|
{{tr "This options deferments if a sound overlaps itself when played twice.<br>" +
|
||||||
|
"An example would be when you move multiple clients, you hear that sound n-times.<br>" +
|
||||||
|
"If this option is disabled, you hear that sound just once."
|
||||||
|
/}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="property">
|
||||||
|
<div class="key muted-sounds">
|
||||||
|
Mute sounds when output is muted:
|
||||||
|
</div>
|
||||||
|
<div class="value muted-sounds">
|
||||||
|
<input type="checkbox">
|
||||||
|
<div class="help-tip-container"> <!-- lets be absolute -->
|
||||||
|
<div class="help-tip tip-right tip-small">
|
||||||
|
<p>
|
||||||
|
{{tr "Mute all system sounds, when you've muted your output.<br>If this option isn't disabled you'll still receive system sounds like 'user joined your channel'."/}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sound-list">
|
||||||
|
<div class="sound-list-header">
|
||||||
|
<div class="column sound-name">{{tr "Name" /}}</div>
|
||||||
|
<div class="column sound-activated">{{tr "Activated" /}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="sound-list-entries-container">
|
||||||
|
<div class="sound-list-entries">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sound-list-filter">
|
||||||
|
<a>Filter:</a>
|
||||||
|
<input type="text">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-content>
|
</x-content>
|
||||||
</x-entry>
|
</x-entry>
|
||||||
|
@ -810,6 +880,15 @@
|
||||||
</x-tab>
|
</x-tab>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script class="jsrender-template" id="tmpl_settings-sound_entry" type="text/html">
|
||||||
|
<div class="entry">
|
||||||
|
<div class="column sound-name"><div class="icon client-play button-playback"></div> {{>name}}</div>
|
||||||
|
<div class="column sound-activated">
|
||||||
|
<input type="checkbox" {{if activated}}checked{{/if}}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
<script class="jsrender-template" id="settings-profile-list-entry" type="text/html">
|
<script class="jsrender-template" id="settings-profile-list-entry" type="text/html">
|
||||||
<div class="entry profile {{if id == 'default'}}default{{/if}}">
|
<div class="entry profile {{if id == 'default'}}default{{/if}}">
|
||||||
<div class="name">{{>profile_name}}</div>
|
<div class="name">{{>profile_name}}</div>
|
||||||
|
|
|
@ -216,7 +216,7 @@ function base64ArrayBuffer(arrayBuffer) {
|
||||||
return base64
|
return base64
|
||||||
}
|
}
|
||||||
|
|
||||||
function Base64EncodeUrl(str){
|
function Base64EncodeUrl(str) {
|
||||||
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
|
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ namespace profiles.identities {
|
||||||
buffer[index++] = 0x01; /* length */
|
buffer[index++] = 0x01; /* length */
|
||||||
buffer[index++] = 0x20;
|
buffer[index++] = 0x20;
|
||||||
}
|
}
|
||||||
{ /* Public kex X */
|
try { /* Public kex X */
|
||||||
buffer[index++] = 0x02; /* type */
|
buffer[index++] = 0x02; /* type */
|
||||||
buffer[index++] = 0x20; /* length */
|
buffer[index++] = 0x20; /* length */
|
||||||
|
|
||||||
|
@ -58,8 +58,13 @@ namespace profiles.identities {
|
||||||
|
|
||||||
for(let i = 0; i < 32; i++)
|
for(let i = 0; i < 32; i++)
|
||||||
buffer[index++] = raw.charCodeAt(i);
|
buffer[index++] = raw.charCodeAt(i);
|
||||||
|
} catch(error) {
|
||||||
|
if(error instanceof DOMException)
|
||||||
|
throw "failed to parse x coordinate (invalid base64)";
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
{ /* Public kex Y */
|
|
||||||
|
try { /* Public kex Y */
|
||||||
buffer[index++] = 0x02; /* type */
|
buffer[index++] = 0x02; /* type */
|
||||||
buffer[index++] = 0x20; /* length */
|
buffer[index++] = 0x20; /* length */
|
||||||
|
|
||||||
|
@ -71,20 +76,32 @@ namespace profiles.identities {
|
||||||
|
|
||||||
for(let i = 0; i < 32; i++)
|
for(let i = 0; i < 32; i++)
|
||||||
buffer[index++] = raw.charCodeAt(i);
|
buffer[index++] = raw.charCodeAt(i);
|
||||||
|
} catch(error) {
|
||||||
|
if(error instanceof DOMException)
|
||||||
|
throw "failed to parse y coordinate (invalid base64)";
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
if(!public_key) { /* Public kex K */
|
|
||||||
buffer[index++] = 0x02; /* type */
|
|
||||||
buffer[index++] = 0x20; /* length */
|
|
||||||
|
|
||||||
const raw = atob(Base64DecodeUrl(key_data.d, false));
|
if(!public_key) {
|
||||||
if(raw.charCodeAt(0) > 0x7F) {
|
try { /* Public kex K */
|
||||||
buffer[index - 1] += 1;
|
buffer[index++] = 0x02; /* type */
|
||||||
buffer[index++] = 0;
|
buffer[index++] = 0x20; /* length */
|
||||||
|
|
||||||
|
const raw = atob(Base64DecodeUrl(key_data.d, false));
|
||||||
|
if(raw.charCodeAt(0) > 0x7F) {
|
||||||
|
buffer[index - 1] += 1;
|
||||||
|
buffer[index++] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let i = 0; i < 32; i++)
|
||||||
|
buffer[index++] = raw.charCodeAt(i);
|
||||||
|
} catch(error) {
|
||||||
|
if(error instanceof DOMException)
|
||||||
|
throw "failed to parse y coordinate (invalid base64)";
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
for(let i = 0; i < 32; i++)
|
|
||||||
buffer[index++] = raw.charCodeAt(i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer[1] = index - 2; /* set the final sequence length */
|
buffer[1] = index - 2; /* set the final sequence length */
|
||||||
|
|
||||||
return base64ArrayBuffer(buffer.buffer.slice(0, index));
|
return base64ArrayBuffer(buffer.buffer.slice(0, index));
|
||||||
|
@ -127,7 +144,15 @@ namespace profiles.identities {
|
||||||
* @param buffer base64 encoded ASN.1 string
|
* @param buffer base64 encoded ASN.1 string
|
||||||
*/
|
*/
|
||||||
export function decode_tomcrypt_key(buffer: string) {
|
export function decode_tomcrypt_key(buffer: string) {
|
||||||
const decoded = asn1.decode(atob(buffer));
|
let decoded;
|
||||||
|
|
||||||
|
try {
|
||||||
|
decoded = asn1.decode(atob(buffer));
|
||||||
|
} catch(error) {
|
||||||
|
if(error instanceof DOMException)
|
||||||
|
throw "failed to parse key buffer (invalid base64)";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
let {x, y, k} = {
|
let {x, y, k} = {
|
||||||
x: decoded.children[2].content(Infinity, asn1.TagType.VisibleString),
|
x: decoded.children[2].content(Infinity, asn1.TagType.VisibleString),
|
||||||
|
@ -373,7 +398,7 @@ namespace profiles.identities {
|
||||||
static async generate_new() : Promise<TeaSpeakIdentity> {
|
static async generate_new() : Promise<TeaSpeakIdentity> {
|
||||||
const key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
|
const key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
|
||||||
const private_key = await CryptoHelper.export_ecc_key(key.privateKey, false);
|
const private_key = await CryptoHelper.export_ecc_key(key.privateKey, false);
|
||||||
console.log("Private key: %s (%d)", private_key, atob(private_key).length);
|
|
||||||
const identity = new TeaSpeakIdentity(private_key, "0", undefined, false);
|
const identity = new TeaSpeakIdentity(private_key, "0", undefined, false);
|
||||||
await identity.initialize();
|
await identity.initialize();
|
||||||
return identity;
|
return identity;
|
||||||
|
@ -405,7 +430,7 @@ namespace profiles.identities {
|
||||||
|
|
||||||
if(!identity) throw "missing identity keyword";
|
if(!identity) throw "missing identity keyword";
|
||||||
if(identity[0] == "\"" && identity[identity.length - 1] == "\"")
|
if(identity[0] == "\"" && identity[identity.length - 1] == "\"")
|
||||||
identity = identity.substr(1, identity.length - 1);
|
identity = identity.substr(1, identity.length - 2);
|
||||||
|
|
||||||
const result = parse_string(identity);
|
const result = parse_string(identity);
|
||||||
result.name = name || result.name;
|
result.name = name || result.name;
|
||||||
|
|
|
@ -64,28 +64,131 @@ namespace sound {
|
||||||
cached?: AudioBuffer;
|
cached?: AudioBuffer;
|
||||||
node?: HTMLAudioElement;
|
node?: HTMLAudioElement;
|
||||||
|
|
||||||
|
replaying: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let warned = false;
|
let warned = false;
|
||||||
let speech_mapping: {[key: string]:SpeechFile} = {};
|
let speech_mapping: {[key: string]:SpeechFile} = {};
|
||||||
|
|
||||||
|
let volume_require_save = false;
|
||||||
|
let speech_volume: {[key: string]:number} = {};
|
||||||
|
let master_volume: number;
|
||||||
|
|
||||||
|
let overlap_sounds: boolean;
|
||||||
|
let ignore_muted: boolean;
|
||||||
|
|
||||||
|
let master_mixed: GainNode;
|
||||||
|
|
||||||
function register_sound(key: string, file: string) {
|
function register_sound(key: string, file: string) {
|
||||||
speech_mapping[key] = {key: key, filename: file} as SpeechFile;
|
speech_mapping[key] = {key: key, filename: file} as SpeechFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function get_sound_volume(sound: Sound) : number {
|
||||||
|
let result = speech_volume[sound];
|
||||||
|
if(typeof(result) === "undefined")
|
||||||
|
result = 1;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function set_sound_volume(sound: Sound, volume: number) {
|
||||||
|
volume_require_save = volume_require_save || speech_volume[sound] != volume;
|
||||||
|
speech_volume[sound] = volume == 1 ? undefined : volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_master_volume() : number {
|
||||||
|
return master_volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function set_master_volume(volume: number) {
|
||||||
|
volume_require_save = volume_require_save || master_volume != volume;
|
||||||
|
master_volume = volume;
|
||||||
|
if(master_mixed.gain.setValueAtTime)
|
||||||
|
master_mixed.gain.setValueAtTime(volume, 0);
|
||||||
|
else
|
||||||
|
master_mixed.gain.value = volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function overlap_activated() : boolean {
|
||||||
|
return overlap_sounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function set_overlap_activated(flag: boolean) {
|
||||||
|
volume_require_save = volume_require_save || overlap_sounds != flag;
|
||||||
|
overlap_sounds = flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ignore_output_muted() : boolean {
|
||||||
|
return ignore_muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function set_ignore_output_muted(flag: boolean) {
|
||||||
|
volume_require_save = volume_require_save || ignore_muted != flag;
|
||||||
|
ignore_muted = flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reinitialisize_audio() {
|
||||||
|
const context = audio.player.context();
|
||||||
|
const destination = audio.player.destination();
|
||||||
|
|
||||||
|
if(master_mixed)
|
||||||
|
master_mixed.disconnect();
|
||||||
|
|
||||||
|
master_mixed = context.createGain();
|
||||||
|
if(master_mixed.gain.setValueAtTime)
|
||||||
|
master_mixed.gain.setValueAtTime(master_volume, 0);
|
||||||
|
else
|
||||||
|
master_mixed.gain.value = master_volume;
|
||||||
|
master_mixed.connect(destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function save() {
|
||||||
|
if(volume_require_save) {
|
||||||
|
volume_require_save = false;
|
||||||
|
|
||||||
|
const data: any = {};
|
||||||
|
data.version = 1;
|
||||||
|
|
||||||
|
for(const sound in Sound) {
|
||||||
|
if(typeof(speech_volume[sound]) !== "undefined")
|
||||||
|
data[sound] = speech_volume[sound];
|
||||||
|
}
|
||||||
|
data.master = master_volume;
|
||||||
|
data.overlap = overlap_sounds;
|
||||||
|
data.ignore_muted = ignore_muted;
|
||||||
|
|
||||||
|
settings.changeGlobal("sound_volume", JSON.stringify(data));
|
||||||
|
console.error(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function initialize() : Promise<void> {
|
export function initialize() : Promise<void> {
|
||||||
$.ajaxSetup({
|
$.ajaxSetup({
|
||||||
beforeSend: function(jqXHR,settings){
|
beforeSend: function(jqXHR,settings){
|
||||||
if (settings.dataType === 'binary'){
|
if (settings.dataType === 'binary'){
|
||||||
console.log("Settins binary");
|
|
||||||
settings.xhr().responseType = 'arraybuffer';
|
settings.xhr().responseType = 'arraybuffer';
|
||||||
settings.processData = false;
|
settings.processData = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* volumes */
|
||||||
|
{
|
||||||
|
const data = JSON.parse(settings.static_global("sound_volume", "{}"));
|
||||||
|
for(const sound in Sound) {
|
||||||
|
if(typeof(data[sound]) !== "undefined")
|
||||||
|
speech_volume[sound] = data[sound];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(data);
|
||||||
|
master_volume = data.master || 1;
|
||||||
|
overlap_sounds = data.overlap || true;
|
||||||
|
ignore_muted = data.ignore_muted || true;
|
||||||
|
}
|
||||||
|
|
||||||
register_sound("message.received", "effects/message_received.wav");
|
register_sound("message.received", "effects/message_received.wav");
|
||||||
register_sound("message.send", "effects/message_send.wav");
|
register_sound("message.send", "effects/message_send.wav");
|
||||||
|
|
||||||
|
reinitialisize_audio();
|
||||||
return new Promise<void>(resolve => {
|
return new Promise<void>(resolve => {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "audio/speech/mapping.json",
|
url: "audio/speech/mapping.json",
|
||||||
|
@ -107,26 +210,23 @@ namespace sound {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function str2ab(str) {
|
export interface PlaybackOptions {
|
||||||
var buf = new ArrayBuffer(str.length*2); // 2 bytes for each char
|
ignore_muted?: boolean;
|
||||||
var bufView = new Uint16Array(buf);
|
ignore_overlap?: boolean;
|
||||||
for (var i = 0, strLen=str.length; i<strLen; i++) {
|
|
||||||
bufView[i] = str.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return buf;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function play(sound: Sound, options?: {
|
export function play(sound: Sound, options?: PlaybackOptions) {
|
||||||
background_notification?: boolean
|
if(!options) {
|
||||||
}) {
|
options = {};
|
||||||
console.log(tr("playback sound %o"), sound);
|
}
|
||||||
|
|
||||||
const file: SpeechFile = speech_mapping[sound];
|
const file: SpeechFile = speech_mapping[sound];
|
||||||
if(!file) {
|
if(!file) {
|
||||||
console.warn(tr("Missing sound %o"), sound);
|
console.warn(tr("Missing sound %o"), sound);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(file.not_supported) {
|
if(file.not_supported) {
|
||||||
if(!file.not_supported_timeout || Date.now() < file.not_supported_timeout) //Test if the not supported isnt may timeouted
|
if(!file.not_supported_timeout || Date.now() < file.not_supported_timeout) //Test if the not supported isn't may timeouted
|
||||||
return;
|
return;
|
||||||
file.not_supported = false;
|
file.not_supported = false;
|
||||||
file.not_supported_timeout = undefined;
|
file.not_supported_timeout = undefined;
|
||||||
|
@ -134,15 +234,30 @@ namespace sound {
|
||||||
|
|
||||||
const path = "audio/" + file.filename;
|
const path = "audio/" + file.filename;
|
||||||
const context = audio.player.context();
|
const context = audio.player.context();
|
||||||
const volume = options && options.background_notification ? .5 : 1;
|
const volume = get_sound_volume(sound);
|
||||||
|
|
||||||
|
console.log(tr("Replaying sound %s (Sound volume: %o | Master volume %o)"), sound, volume, master_volume);
|
||||||
|
if(volume == 0) return;
|
||||||
|
if(master_volume == 0) return;
|
||||||
|
if(!options.ignore_muted && !ignore_muted && globalClient.controlBar.muteOutput) return;
|
||||||
|
|
||||||
if(context.decodeAudioData) {
|
if(context.decodeAudioData) {
|
||||||
if(file.cached) {
|
if(file.cached) {
|
||||||
|
if(!options.ignore_overlap && file.replaying && !overlap_sounds) {
|
||||||
|
console.log(tr("Dropping requested playback for sound %s because it would overlap."), sound);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(tr("Using cached buffer: %o"), file.cached);
|
console.log(tr("Using cached buffer: %o"), file.cached);
|
||||||
const player = context.createBufferSource();
|
const player = context.createBufferSource();
|
||||||
player.buffer = file.cached;
|
player.buffer = file.cached;
|
||||||
player.start(0);
|
player.start(0);
|
||||||
|
|
||||||
|
file.replaying = true;
|
||||||
|
player.onended = event => {
|
||||||
|
file.replaying = false;
|
||||||
|
};
|
||||||
|
|
||||||
if(volume != 1 && context.createGain) {
|
if(volume != 1 && context.createGain) {
|
||||||
const gain = context.createGain();
|
const gain = context.createGain();
|
||||||
if(gain.gain.setValueAtTime)
|
if(gain.gain.setValueAtTime)
|
||||||
|
@ -151,9 +266,10 @@ namespace sound {
|
||||||
gain.gain.value = volume;
|
gain.gain.value = volume;
|
||||||
|
|
||||||
player.connect(gain);
|
player.connect(gain);
|
||||||
gain.connect(audio.player.destination());
|
gain.connect(master_mixed);
|
||||||
} else
|
} else {
|
||||||
player.connect(audio.player.destination());
|
player.connect(master_mixed);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const decode_data = buffer => {
|
const decode_data = buffer => {
|
||||||
console.log(buffer);
|
console.log(buffer);
|
||||||
|
@ -203,6 +319,7 @@ namespace sound {
|
||||||
console.log(tr("Replaying %s"), path);
|
console.log(tr("Replaying %s"), path);
|
||||||
if(file.node) {
|
if(file.node) {
|
||||||
file.node.currentTime = 0;
|
file.node.currentTime = 0;
|
||||||
|
file.node.
|
||||||
file.node.play();
|
file.node.play();
|
||||||
} else {
|
} else {
|
||||||
if(!warned) {
|
if(!warned) {
|
||||||
|
|
|
@ -75,6 +75,8 @@ namespace Modals {
|
||||||
ppt_settings = ppt_settings ? JSON.parse(ppt_settings as any as string) : {};
|
ppt_settings = ppt_settings ? JSON.parse(ppt_settings as any as string) : {};
|
||||||
|
|
||||||
vad_tag.find(".vat_ppt_key").text(ppt.key_description(ppt_settings));
|
vad_tag.find(".vat_ppt_key").text(ppt.key_description(ppt_settings));
|
||||||
|
vad_tag.find(".ppt-delay input").val(ppt_settings.delay === undefined ? 300 : ppt_settings.delay);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "vad":
|
case "vad":
|
||||||
let slider = vad_tag.find(".vad_vad_slider");
|
let slider = vad_tag.find(".vad_vad_slider");
|
||||||
|
@ -123,6 +125,15 @@ namespace Modals {
|
||||||
ppt.register_key_listener(listener);
|
ppt.register_key_listener(listener);
|
||||||
modal.open();
|
modal.open();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vad_tag.find(".ppt-delay input").on('change', event => {
|
||||||
|
let ppt_settings: PPTKeySettings = settings.global('vad_ppt_settings', undefined);
|
||||||
|
ppt_settings = ppt_settings ? JSON.parse(ppt_settings as any as string) : {};
|
||||||
|
ppt_settings.delay = (<HTMLInputElement>event.target).valueAsNumber;
|
||||||
|
settings.changeGlobal('vad_ppt_settings', ppt_settings);
|
||||||
|
|
||||||
|
globalClient.voiceConnection.voiceRecorder.reinitialiseVAD();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
{ //Initialized voice activation detection
|
{ //Initialized voice activation detection
|
||||||
|
@ -246,6 +257,89 @@ namespace Modals {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{ /* initialize sounds */
|
||||||
|
const sound_tag = tag.find(".sound-settings");
|
||||||
|
|
||||||
|
{ /* master volume */
|
||||||
|
const master_tag = sound_tag.find(".master-volume");
|
||||||
|
master_tag.find("input").on('change input', event => {
|
||||||
|
const value = parseInt((<HTMLInputElement>event.target).value);
|
||||||
|
master_tag.find('a').text("(" + value + "%)");
|
||||||
|
|
||||||
|
sound.set_master_volume(value / 100);
|
||||||
|
}).val((sound.get_master_volume() * 100).toString()).trigger('change');
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const overlap_tag = sound_tag.find(".overlap-sounds input");
|
||||||
|
overlap_tag.on('change', event => {
|
||||||
|
const activated = (<HTMLInputElement>event.target).checked;
|
||||||
|
sound.set_overlap_activated(activated);
|
||||||
|
}).prop("checked", sound.overlap_activated());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const muted_tag = sound_tag.find(".muted-sounds input");
|
||||||
|
muted_tag.on('change', event => {
|
||||||
|
const activated = (<HTMLInputElement>event.target).checked;
|
||||||
|
sound.set_ignore_output_muted(!activated);
|
||||||
|
}).prop("checked", !sound.ignore_output_muted());
|
||||||
|
}
|
||||||
|
|
||||||
|
{ /* sound elements */
|
||||||
|
const template_tag = $("#tmpl_settings-sound_entry");
|
||||||
|
const entry_tag = sound_tag.find(".sound-list-entries");
|
||||||
|
|
||||||
|
for(const _sound in Sound) {
|
||||||
|
const sound_name = Sound[_sound];
|
||||||
|
|
||||||
|
console.log(sound.get_sound_volume(sound_name as Sound));
|
||||||
|
const data = {
|
||||||
|
name: sound_name,
|
||||||
|
activated: sound.get_sound_volume(sound_name as Sound) > 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const entry = template_tag.renderTag(data);
|
||||||
|
entry.find("input").on('change', event => {
|
||||||
|
const activated = (<HTMLInputElement>event.target).checked;
|
||||||
|
console.log(tr("Sound %s had changed to %o"), sound_name, activated);
|
||||||
|
sound.set_sound_volume(sound_name as Sound, activated ? 1 : 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
entry.find(".button-playback").on('click', event => {
|
||||||
|
sound.play(sound_name as Sound);
|
||||||
|
});
|
||||||
|
|
||||||
|
entry_tag.append(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const entry_container = sound_tag.find(".sound-list-entries-container");
|
||||||
|
if(entry_container.hasScrollBar())
|
||||||
|
entry_container.addClass("scrollbar");
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
/* filter */
|
||||||
|
const filter_tag = sound_tag.find(".sound-list-filter input");
|
||||||
|
filter_tag.on('change keyup', event => {
|
||||||
|
const filter = ((<HTMLInputElement>event.target).value || "").toLowerCase();
|
||||||
|
if(!filter)
|
||||||
|
entry_tag.find(".entry").show();
|
||||||
|
else {
|
||||||
|
entry_tag.find(".entry").each((_, _entry) => {
|
||||||
|
const entry = $(_entry);
|
||||||
|
if(entry.text().toLowerCase().indexOf(filter) == -1)
|
||||||
|
entry.hide();
|
||||||
|
else
|
||||||
|
entry.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.close_listener.push(sound.save);
|
||||||
|
}
|
||||||
|
|
||||||
//Initialise microphones
|
//Initialise microphones
|
||||||
/*
|
/*
|
||||||
let select_microphone = tag.find(".voice_microphone_select");
|
let select_microphone = tag.find(".voice_microphone_select");
|
||||||
|
@ -570,7 +664,7 @@ namespace Modals {
|
||||||
const element = event.target as HTMLInputElement;
|
const element = event.target as HTMLInputElement;
|
||||||
const file_reader = new FileReader();
|
const file_reader = new FileReader();
|
||||||
file_reader.onload = function() {
|
file_reader.onload = function() {
|
||||||
const identity_promise = profiles.identities.TeaSpeakIdentity.import_ts(file_reader.result, true);
|
const identity_promise = profiles.identities.TeaSpeakIdentity.import_ts(file_reader.result as string, true);
|
||||||
identity_promise.then(identity => {
|
identity_promise.then(identity => {
|
||||||
(identity as profiles.identities.TeaSpeakIdentity).export_ts().then(e => teamspeak_tag.find(".identity_string").val(e));
|
(identity as profiles.identities.TeaSpeakIdentity).export_ts().then(e => teamspeak_tag.find(".identity_string").val(e));
|
||||||
selected_profile.set_identity(IdentitifyType.TEAMSPEAK, identity as any);
|
selected_profile.set_identity(IdentitifyType.TEAMSPEAK, identity as any);
|
||||||
|
|
|
@ -145,9 +145,12 @@ class VoiceRecorder {
|
||||||
if(ppt_settings.key_windows === undefined)
|
if(ppt_settings.key_windows === undefined)
|
||||||
ppt_settings.key_windows = false;
|
ppt_settings.key_windows = false;
|
||||||
|
|
||||||
|
if(ppt_settings.delay === undefined)
|
||||||
|
ppt_settings.delay = 300;
|
||||||
|
|
||||||
if(!(this.getVADHandler() instanceof PushToTalkVAD))
|
if(!(this.getVADHandler() instanceof PushToTalkVAD))
|
||||||
this.setVADHandler(new PushToTalkVAD(ppt_settings));
|
this.setVADHandler(new PushToTalkVAD(ppt_settings));
|
||||||
else (this.getVADHandler() as PushToTalkVAD).key = ppt_settings;
|
else (this.getVADHandler() as PushToTalkVAD).settings = ppt_settings;
|
||||||
} else if(type == "pt") {
|
} else if(type == "pt") {
|
||||||
if(!(this.getVADHandler() instanceof PassThroughVAD))
|
if(!(this.getVADHandler() instanceof PassThroughVAD))
|
||||||
this.setVADHandler(new PassThroughVAD());
|
this.setVADHandler(new PassThroughVAD());
|
||||||
|
@ -324,28 +327,28 @@ class VoiceActivityDetectorVAD extends VoiceActivityDetector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PPTKeySettings extends ppt.KeyDescriptor{
|
interface PPTKeySettings extends ppt.KeyDescriptor {
|
||||||
version?: number;
|
version?: number;
|
||||||
|
delay: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PushToTalkVAD extends VoiceActivityDetector {
|
class PushToTalkVAD extends VoiceActivityDetector {
|
||||||
private _key: ppt.KeyDescriptor;
|
private _settings: PPTKeySettings;
|
||||||
private _key_hook: ppt.KeyHook;
|
private _key_hook: ppt.KeyHook;
|
||||||
private _timeout: NodeJS.Timer;
|
private _timeout: NodeJS.Timer;
|
||||||
private _delay = /* 300 */ 0; //TODO configurable
|
|
||||||
|
|
||||||
private _pushed: boolean = false;
|
private _pushed: boolean = false;
|
||||||
|
|
||||||
constructor(key: ppt.KeyDescriptor) {
|
constructor(settings: PPTKeySettings) {
|
||||||
super();
|
super();
|
||||||
this._key = key;
|
this._settings = settings;
|
||||||
this._key_hook = {
|
this._key_hook = {
|
||||||
callback_release: () => {
|
callback_release: () => {
|
||||||
if(this._timeout)
|
if(this._timeout)
|
||||||
clearTimeout(this._timeout);
|
clearTimeout(this._timeout);
|
||||||
|
|
||||||
if(this._delay > 0)
|
if(this._settings.delay > 0)
|
||||||
this._timeout = setTimeout(() => this._pushed = false, this._delay);
|
this._timeout = setTimeout(() => this._pushed = false, this._settings.delay);
|
||||||
else
|
else
|
||||||
this._pushed = false;
|
this._pushed = false;
|
||||||
},
|
},
|
||||||
|
@ -358,9 +361,17 @@ class PushToTalkVAD extends VoiceActivityDetector {
|
||||||
|
|
||||||
cancel: false
|
cancel: false
|
||||||
} as ppt.KeyHook;
|
} as ppt.KeyHook;
|
||||||
Object.assign(this._key_hook, this._key);
|
|
||||||
|
this.initialize_hook();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initialize_hook() {
|
||||||
|
this._key_hook.key_code = this._settings.key_code;
|
||||||
|
this._key_hook.key_alt = this._settings.key_alt;
|
||||||
|
this._key_hook.key_ctrl = this._settings.key_ctrl;
|
||||||
|
this._key_hook.key_shift = this._settings.key_shift;
|
||||||
|
this._key_hook.key_windows = this._settings.key_windows;
|
||||||
|
}
|
||||||
|
|
||||||
initialise() {
|
initialise() {
|
||||||
ppt.register_key_hook(this._key_hook);
|
ppt.register_key_hook(this._key_hook);
|
||||||
|
@ -376,11 +387,13 @@ class PushToTalkVAD extends VoiceActivityDetector {
|
||||||
this._pushed = flag;
|
this._pushed = flag;
|
||||||
}
|
}
|
||||||
|
|
||||||
set key(key: ppt.KeyDescriptor) {
|
set settings(settings: PPTKeySettings) {
|
||||||
ppt.unregister_key_hook(this._key_hook);
|
ppt.unregister_key_hook(this._key_hook);
|
||||||
Object.assign(this._key, key);
|
|
||||||
Object.assign(this._key_hook, key);
|
this._settings = settings;
|
||||||
|
this.initialize_hook();
|
||||||
this._pushed = false;
|
this._pushed = false;
|
||||||
|
|
||||||
ppt.register_key_hook(this._key_hook);
|
ppt.register_key_hook(this._key_hook);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 8304246b4f651b9de141e4d3252b4f78e5c55391
|
Subproject commit 7fe6a479984b77160a9135f9d2182fb79ca56023
|
Loading…
Reference in New Issue