Related
I am working on a static portfolio site and have styled some Javascript audio players.
The site is live here with the first audio player working almost exactly as desired (except the progress bar displays at the top of the div, I'd like it at the bottom). A photo is attached of the desired visual outcome.
I need five total audio players. How can I achieve this?
Current Javascript:
const audioPlayer = document.querySelector(".audio-player");
const audio = new Audio(
"https://jsomerset.uk/images/victory.mp3"
);
console.dir(audio);
audio.addEventListener(
"loadeddata",
() => {
audioPlayer.querySelector(".time .length").textContent = getTimeCodeFromNum(
audio.duration
);
audio.volume = .75;
},
false
);
const timeline = audioPlayer.querySelector(".timeline");
timeline.addEventListener("click", e => {
const timelineWidth = window.getComputedStyle(timeline).width;
const timeToSeek = e.offsetX / parseInt(timelineWidth) * audio.duration;
audio.currentTime = timeToSeek;
}, false);
setInterval(() => {
const progressBar = audioPlayer.querySelector(".progress");
progressBar.style.width = audio.currentTime / audio.duration * 100 + "%";
audioPlayer.querySelector(".time .current").textContent = getTimeCodeFromNum(
audio.currentTime
);
}, 500);
const playBtn = audioPlayer.querySelector(".controls .toggle-play");
playBtn.addEventListener(
"click",
() => {
if (audio.paused) {
playBtn.classList.remove("play");
playBtn.classList.add("pause");
audio.play();
} else {
playBtn.classList.remove("pause");
playBtn.classList.add("play");
audio.pause();
}
},
false
);
You code can't run properly, since you're selecting non existent elements.
Check you dev tools console for errors.
E.g. you're trying to display the current time in an element with the class time – but yout html does not contain such an element.
Besides, you haven't defined the method getTimeCodeFromNum().
See the cleaned up code – not usable blocks are commented out :
const audioPlayer = document
.querySelectorAll(".audio-player")
.forEach((audioPlayer) => {
const audio = new Audio(audioPlayer.dataset.src);
//console.dir(audio);
/*
audio.addEventListener(
"loadeddata",
() => {
audioPlayer.querySelector(
".time .length"
).textContent = getTimeCodeFromNum(audio.duration);
audio.volume = 0.75;
},
false
);
*/
const timeline = audioPlayer.querySelector(".timeline");
timeline.addEventListener(
"click",
(e) => {
const timelineWidth = window.getComputedStyle(timeline).width;
const timeToSeek =
(e.offsetX / parseInt(timelineWidth)) * audio.duration;
audio.currentTime = timeToSeek;
},
false
);
setInterval(() => {
const progressBar = audioPlayer.querySelector(".progress");
progressBar.style.width =
(audio.currentTime / audio.duration) * 100 + "%";
/*
audioPlayer.querySelector(
".time .current"
).textContent = getTimeCodeFromNum(audio.currentTime);
*/
}, 500);
const playBtn = audioPlayer.querySelector(".controls .toggle-play");
playBtn.addEventListener(
"click",
() => {
if (audio.paused) {
playBtn.classList.remove("play");
playBtn.classList.add("pause");
audio.play();
} else {
playBtn.classList.remove("pause");
playBtn.classList.add("play");
audio.pause();
}
},
false
);
/*
audioPlayer
.querySelector(".volume-button")
.addEventListener("click", () => {
const volumeEl = audioPlayer.querySelector(".volume-container .volume");
audio.muted = !audio.muted;
if (audio.muted) {
volumeEl.classList.remove("icono-volumeMedium");
volumeEl.classList.add("icono-volumeMute");
} else {
volumeEl.classList.add("icono-volumeMedium");
volumeEl.classList.remove("icono-volumeMute");
}
});
*/
});
body {
background: #000
}
.audio-player {
display: grid;
grid-template-rows: 6px auto;
overflow: hidden;
height: 200px;
width: 100vw;
color: #efefef;
}
.timeline {
background: none;
width: 100%;
position: relative;
cursor: pointer;
height: 5px;
}
.progress {
background: #efefef;
width: 0%;
height: 5px;
transition: 0.25s;
-webkit-transition: 0.25s;
}
.controls {
display: flex;
align-items: center;
justify-content: center;
width: 100px;
}
.controls * {
display: flex;
justify-content: center;
align-items: center;
}
.play {
cursor: pointer;
position: relative;
left: 0;
height: 0;
width: 0;
border: 7px solid #0000;
border-left: 13px solid white;
}
.pause {
height: 15px;
width: 20px;
cursor: pointer;
position: absolute;
margin-left: 15px;
}
.pause:before {
position: absolute;
top: 0;
left: 0px;
background: white;
content: "";
height: 15px;
width: 3px;
}
.pause:after {
position: absolute;
top: 0;
right: 9px;
background: white;
content: "";
height: 15px;
width: 3px;
}
<div class="audio-player a-one font" data-src="https://jsomerset.uk/images/swain.mp3">
<div class="timeline">
<div class="progress" style="width: 0%;"></div>
</div>
<div class="name">Action</div>
<div class="controls">
<div class="play-container">
<div class="toggle-play play">
</div>
</div>
</div>
</div>
<div class="audio-player a-two font" data-src="https://jsomerset.uk/images/victory.mp3">
<div class="timeline">
<div class="progress"></div>
</div>
<div class="name">Victory Song</div>
<div class="controls">
<div class="play-container">
<div class="toggle-play play">
</div>
</div>
</div>
</div>
Use document.querySelectorAll, then loop over the selection. You can store the mp3 URL for each div inside a data-src attribute:
<div class="audio-player" data-src="https://jsomerset.uk/images/victory.mp3">...</div>
<div class="audio-player" data-src="https://jsomerset.uk/images/anotherFile.mp3">...</div>
<div class="audio-player" data-src="https://jsomerset.uk/images/etc.mp3">...</div>
document.querySelectorAll(".audio-player").forEach(audioPlayer => {
const audio = new Audio(audioPlayer.dataset.src);
// rest of your code
});
I have JS code on a webpage that loads questions in from mysql db and displays the text . What happens is that it cuts off words at the end of the line and continues the word on the next line at the start. So all text across the screen starts/ends at the same point.
This seems to be the code where it displays the text.
For example the text will look like at the end of a line 'cont' and then on next line at the start 'inue'.
How do i fix this?
var questions = <?=$questions;?>;
// Initialize variables
//------------------------------------------------------------------
var tags;
var tagsClass = '';
var liTagsid = [];
var correctAns = 0;
var isscorrect = 0;
var quizPage = 1;
var currentIndex = 0;
var currentQuestion = questions[currentIndex];
var prevousQuestion;
var previousIndex = 0;
var ulTag = document.getElementsByClassName('ulclass')[0];
var button = document.getElementById('submit');
var questionTitle = document.getElementById('question');
//save class name so it can be reused easily
//if I want to change it, I have to change it one place
var classHighlight = 'selected';
// Display Answers and hightlight selected item
//------------------------------------------------------------------
function showQuestions (){
document.body.scrollTop = 0; // For Safari
document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
if (currentIndex != 0) {
// create again submit button only for next pages
ulTag.innerHTML ='';
button.innerHTML = 'Submit';
button.className = 'submit';
button.id = 'submit';
if(quizPage<=questions.length){
//update the number of questions displayed
document.getElementById('quizNumber').innerHTML = quizPage;
}
}
//Display Results in the final page
if (currentIndex == (questions.length)) {
ulTag.innerHTML = '';
document.getElementById('question').innerHTML = '';
if(button.id == 'submit'){
button.className = 'buttonload';
button.innerHTML = '<i class="fa fa-spinner fa-spin"></i>Loading';
}
showResults();
return
}
questionTitle.innerHTML = "Question No:" + quizPage + " "+currentQuestion.question.category_name +"<br/>"+ currentQuestion.question.text;
if(currentQuestion.question.filename !== ''){
var br = document.createElement('br');
questionTitle .appendChild(br);
var img = document.createElement('img');
img.src = currentQuestion.question.filename;
img.className = 'imagecenter';
img.width = 750;
img.height = 350;
questionTitle .appendChild(img);
}
// create a for loop to generate the options and display them in the page
for (var i = 0; i < currentQuestion.options.length; i++) {
// creating options
var newAns = document.createElement('li');
newAns.id = 'ans'+ (i+1);
newAns.className = "notSelected listyle";
var textAns = document.createTextNode(currentQuestion.options[i].optiontext);
newAns.appendChild(textAns);
if(currentQuestion.options[i].file !== ''){
var br = document.createElement('br');
newAns .appendChild(br);
var img1 = document.createElement('img');
img1.src = currentQuestion.options[i].file;
img1.className = 'optionimg';
img1.width = 250;
img1.height = 250;
newAns .appendChild(img1);
newAns .appendChild(br);
}
var addNewAnsHere = document.getElementById('options');
addNewAnsHere.appendChild(newAns);
}
//.click() will return the result of $('.notSelected')
var $liTags = $('.notSelected').click(function(list) {
list.preventDefault();
//run removeClass on every element
//if the elements are not static, you might want to rerun $('.notSelected')
//instead of the saved $litTags
$liTags.removeClass(classHighlight);
//add the class to the currently clicked element (this)
$(this).addClass(classHighlight);
//get id name of clicked answer
for (var i = 0; i < currentQuestion.options.length ; i++) {
// console.log(liTagsid[i]);
if($liTags[i].className == "notSelected listyle selected"){
//store information to check answer
tags = $liTags[i].id;
// tagsClass = $LiTags.className;
tagsClassName = $liTags[i];
}
}
});
//check answer once it has been submitted
button.onclick = function (){
if(button.id == 'submit'){
button.className = 'buttonload';
button.innerHTML = '<i class="fa fa-spinner fa-spin"></i>Loading';
}
setTimeout(function() { checkAnswer(); }, 100);
};
}
//self calling function
showQuestions();
The website is on my local now but i can upload a screenimage if need be and the whole code of the webpage. Or is the issue in html?
edit: here is html/css code
<style>
/*========================================================
Quiz Section
========================================================*/
/*styling quiz area*/
.main {
background-color: white;
margin: 0 auto;
margin-top: 30px;
padding: 30px;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
/*white-space: nowrap;*/
}
/*Editing the number of questions*/
.spanclass {
font-size: x-large;
}
#pages{
border: 3px solid;
display: inline-flex;
border-radius: 0.5em;
float: right;
}
#question{
word-break: break-all;
}
/*format text*/
p {
text-align: left;
font-size: x-large;
padding: 10px 10px 0;
}
.optionimg{
border: 2px solid black;
border-radius: 1.5em;
}
/*Form area width*/
/*formatting answers*/
.listyle {
list-style-type: none;
text-align: left;
background-color: transparent;
margin: 10px 5px;
padding: 5px 10px;
border: 1px solid lightgray;
border-radius: 0.5em;
font-weight: normal;
font-size: x-large;
display: inline-grid;
width: 48%;
height: 300px;
overflow: auto;
}
.listyle:hover {
background: #ECEEF0;
cursor: pointer;
}
/*Change effect of question when the questions is selected*/
.selected, .selected:hover {
background: #FFDEAD;
}
/*change correct answer background*/
.correct, .correct:hover {
background: #9ACD32;
color: white;
}
/*change wrong answer background*/
.wrong, .wrong:hover {
background: #db3c3c;
color: white;
}
/*========================================================
Submit Button
========================================================*/
.main button {
text-transform: uppercase;
width: 20%;
border: none;
padding: 15px;
color: #FFFFFF;
}
.submit:hover, .submit:active, .submit:focus {
background: #43A047;
}
.submit {
background: #4CAF50;
min-width: 120px;
}
/*next question button*/
.next {
background: #fa994a;
min-width: 120px;
}
.next:hover, .next:active, .next:focus {
background: #e38a42;
}
.restart {
background-color:
}
/*========================================================
Results
========================================================*/
.circle{
position: relative;
margin: 0 auto;
width: 200px;
height: 200px;
background: #bdc3c7;
-webkit-border-radius: 100px;
-moz-border-radius: 100px;
border-radius: 100px;
overflow: hidden;
}
.fill{
position: absolute;
bottom: 0;
width: 100%;
height: 80%;
background: #31a2ac;
}
.score {
position: absolute;
width: 100%;
top: 1.7em;
text-align: center;
font-family: Arial, sans-serif;
color: #fff;
font-size: 40pt;
line-height: 0;
font-weight: normal;
}
.circle p {
margin: 400px;
}
/*========================================================
Confeeti Effect
========================================================*/
canvas{
position:absolute;
left:0;
top:11em;
z-index:0;
border:0px solid #000;
}
.imagecenter{
display: block;
margin: 0 auto;
}
.buttonload {
background-color: #04AA6D; /* Green background */
border: none; /* Remove borders */
color: white; /* White text */
padding: 12px 24px; /* Some padding */
font-size: 16px; /* Set a font-size */
}
/* Add a right margin to each icon */
.fa {
margin-left: -12px;
margin-right: 8px;
}
#media only screen and (max-width: 900px){
.listyle {
width: 100% !important;
height: auto !important;
}
.imagecenter {
width: 100% !important;
}
.listyle img{
width: inherit !important;
height: unset !important;
}
.ulclass
{
padding:0px !important;
}
}
</style>
<!-- Main page -->
<div class="main">
<!-- Number of Question -->
<div class="wrapper" id="pages">
<span class="spanclass" id="quizNumber">1</span><span class="spanclass">/<?=$count?></span>
</div>
<!-- Quiz Question -->
<div class="quiz-questions" id="display-area">
<p id="question"></p>
<ul class="ulclass" id="options">
</ul>
<div id="quiz-results" class="text-center">
<button type="button" name="button" class="submit" id="submit">Submit</button>
</div>
</div>
</div>
<canvas id="canvas"></canvas>
<script src="https://code.jquery.com/jquery-3.2.1.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script>
I'm guessing that #question{ word-break: break-all; } is probably the culprit then? –
CB..yes that fixed it:)
I have managed to build a To Do List on my own and have implemented the addTodo and add to/get from local storage functionality. I now need to add the update functionality to update a Todo list item. I understand that I first need to attach an event listener to the ul and find the closest li item id or something like that. Then I need to pass an update function to the event listener to update the input value in the list. Here is what I have so far. How would I create the event listener and update function to update a list item?
Output needed: Text of added item needs to update (to local storage) after user clicks on text and changes text of item.
Note: The snippet won't work because local storage is not allowed (as far as I know). You will need to see the codepen
// select DOM elements
const todoForm = document.querySelector('.todo-form');
const todoInput = document.querySelector('.todo-input');
const todoItemsList = document.querySelector('.todo-items');
// array which stores every todo item
// each item will be an object with id, name, completed boollean
let todos = [];
// add an event listener on form
todoForm.addEventListener('submit', function(e) {
e.preventDefault();
// input param from addTodo is todoInput.value
addTodo(todoInput.value);
});
document.addEventListener('click', (e) => {
if (e.target.classList.contains('checkbox')) {
if (e.target.checked) e.target.closest('.item').classList.add('checked');
else e.target.closest('.item').classList.remove('checked');
}
});
// todoItemsList.addEventListener('input', (e) => {
// const taskId = e.target.item.id;
// console.log(taskId);
// });
// todoItemsList.addEventListener('input', (e) => {
// const taskId = e.target.closest('li').id
// console.log(taskId)
// // updateTask(taskId, e.target)
// })
function addTodo(input) {
if (input !== '') {
const todo = {
id: Date.now(),
name: input,
completed: false,
};
todos.push(todo);
addToLocalStorage(todos);
todoInput.value = '';
}
}
// todos is passed as parameter because it exists outside function environment
function renderTodos(todos) {
// clear list
todoItemsList.innerHTML = '';
// run through each item inside todos
todos.forEach((item) => {
// checkbox
let cb = document.createElement('input');
cb.type = 'checkbox';
cb.classList.add('checkbox');
cb.checked = false;
// delete button
const deleteButton = document.createElement('button');
deleteButton.className = 'delete-button';
deleteButton.innerText = 'x';
// lists
const li = document.createElement('li');
li.classList.add('item');
li.setAttribute('data-key', item.id);
li.appendChild(cb);
li.append(item.name);
li.append(deleteButton);
todoItemsList.append(li);
});
}
function addToLocalStorage(todos) {
// name key 'todos" and value todos array
// convert array to string and store it
localStorage.setItem('todos', JSON.stringify(todos));
renderTodos(todos);
}
function getFromLocalStorage() {
const reference = localStorage.getItem('todos');
if (reference) {
// converts string back to an array and store in todos array
todos = JSON.parse(reference);
renderTodos(todos);
}
}
getFromLocalStorage();
* {
padding: 0;
margin: 0;
}
body {
width: 100vw;
min-height: 100vh;
display: flex;
justify-content: center;
background: hsl(194, 100%, 70%);
font-family: 'Roboto', sans-serif;
}
button {
display: inline-block;
padding: 0.35em 1.2em;
border: 0.1em solid hsl(0, 0%, 0%);
margin: 0 0.3em 0.3em 0;
background: hsl(0, 0%, 0%);
border-radius: 0.12em;
box-sizing: border-box;
text-decoration: none;
font-family: 'Roboto', sans-serif;
font-weight: 300;
color: hsl(0, 0%, 100%);
text-align: center;
transition: all 0.2s;
}
button:hover {
cursor: pointer;
background-color: hsl(0, 0%, 100%);
color: hsl(214, 11%, 13%);
}
ul {
list-style-type: none;
}
.container {
min-width: 700px;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
h1 {
color: hsl(0, 0%, 100%);
font-size: 3rem;
}
/* To Do Form */
.todo-form {
margin: 40px 0px;
}
.todo-input {
width: 250px;
border: none;
outline: none;
border-radius: 5px;
padding: 10px;
margin-right: 10px;
font-size: 1rem;
}
.todo-items {
min-width: 350px;
height: auto;
}
/* Item style */
.item {
height: auto;
background-color: #fff;
margin: 1em;
padding: 10px;
font-size: 1.2rem;
vertical-align: middle;
border-radius: 5px;
}
.checkbox {
width: 1.5em;
height: 1.5em;
margin-right: 20px;
vertical-align: middle;
}
.delete-button {
display: inline-flex;
justify-content: center;
align-items: center;
float: right;
width: 25px;
height: 25px;
background-color: hsl(348, 93%, 56%);
border: none;
outline: none;
/* border-radius: 7px; */
/* padding: 2px 5px; */
/* margin-left: 10px; */
font-size: 0.8rem;
font-weight: 550;
text-align: center;
vertical-align: middle;
}
.delete-button:hover {
background: pink;
}
.checked {
text-decoration: line-through;
}
.text-container {
min-width: 100px;
height: 50px;
background: red;
}
<div class="container">
<h1>To Do</h1>
<form class="todo-form">
<input type="text" class="todo-input" placeholder="add a todo item">
<button type="submit" class="add=button">Add</button>
</form>
<ul class="todo-items">
</ul>
</div>
You actually don't need ID here. You can maintain use the index of the element to determine what todo it belongs to. Check below code
todoItemsList.addEventListener('click', (e) => {
// Anywhere inside UL clicked
let clickedElement = e.target;
if (clickedElement.classList.contains('delete-button') || clickedElement.classList.contains('checkbox')) {
// Either checkbox or delete button is clicked
let listElement = clickedElement.parentNode;
let listElementsArray = todoItemsList.querySelectorAll('li');
let index = Array.prototype.indexOf.call(listElementsArray, listElement);
if (clickedElement.classList.contains('delete-button')) {
//Delete button clicked
listElement.remove();
todos.splice(index, 1);
} else if (clickedElement.classList.contains('checkbox')) {
//Checkbox clicked
todos[index].completed = e.target.checked;
}
console.log(todos);
// Update in localstorage
localStorage.setItem('todos', JSON.stringify(todos));
}
});
Note: Here I have used Array.prototype.indexOf.call instead of listElementsArray.indexOf because listElementsArray is a node list which doesn't support indexOf natively.
I reworked your code a bit because I was getting lost in it...
the part that interests you is at the end, with :
todoList.onclick = ({target}) =>
{
if (!target.matches('li input[type=checkbox], button.delete-button')) return
let li = target.closest('li')
, idx = todos.findIndex(x=>x.id==li.dataset.key)
;
switch (target.tagName.toLowerCase())
{
case 'input':
todos[idx].completed = target.checked
break;
case 'button':
todos.splice(idx, 1)
todoList.removeChild(li)
break;
}
todos.update_LocalStorage()
}
it is an event delegation arranged to manage either the checkbox or the delete button
full code
const
todoForm = document.querySelector('#todo-form')
, todoList = document.querySelector('#todo-items')
, todos = JSON.parse( localStorage.getItem('todos') || '[]')
;
todos.UI_add = item =>
{
let li = document.createElement('li')
li.dataset.key = item.id
li.innerHTML = `
<input type="checkbox" ${item.completed ? 'checked':''} >
<span>${item.name}</span>
<button class="delete-button">x</button>`
todoList.appendChild(li)
}
todos.update_LocalStorage = _ =>
{
localStorage.setItem('todos', JSON.stringify(todos))
}
todoForm.onsubmit = e =>
{
e.preventDefault()
let str = todoForm['todo-text'].value.trim()
if (str !== '')
{
let item = { id: Date.now(), name:str, completed: false }
todos.push( item )
todos.UI_add( item )
todos.update_LocalStorage()
todoForm['todo-text'].value = ''
todoForm['todo-text'].focus()
}
}
todoList.onclick = ({target}) =>
{
if (!target.matches('li input[type=checkbox], button.delete-button')) return
let li = target.closest('li')
, idx = todos.findIndex(x=>x.id==li.dataset.key)
;
switch (target.tagName.toLowerCase())
{
case 'input':
todos[idx].completed = target.checked
break;
case 'button':
todos.splice(idx, 1)
todoList.removeChild(li)
break;
}
todos.update_LocalStorage()
}
// init user interface list
todos.forEach(todos.UI_add)
* {
padding: 0;
margin: 0;
}
body {
width: 100vw;
min-height: 100vh;
display: flex;
justify-content: center;
background: hsl(194, 100%, 70%);
font-family: 'Roboto', sans-serif;
}
button {
display: inline-block;
padding: 0.35em 1.2em;
border: 0.1em solid hsl(0, 0%, 0%);
margin: 0 0.3em 0.3em 0;
background: hsl(0, 0%, 0%);
border-radius: 0.12em;
box-sizing: border-box;
text-decoration: none;
font-family: 'Roboto', sans-serif;
font-weight: 300;
color: hsl(0, 0%, 100%);
text-align: center;
transition: all 0.2s;
}
button:hover {
cursor: pointer;
background-color: hsl(0, 0%, 100%);
color: hsl(214, 11%, 13%);
}
.container {
min-width : 700px;
display : flex;
flex-direction : column;
align-items : center;
padding : 20px;
}
h1 {
color : hsl(0, 0%, 100%);
font-size : 3rem;
}
form#todo-form {
margin : 40px 0;
}
form#todo-form input {
width : 250px;
border : none;
outline : none;
border-radius : 5px;
padding : 10px;
margin-right : 10px;
font-size : 1rem;
}
ul#todo-items {
min-width : 350px;
height : auto;
list-style : none;
}
ul#todo-items li {
height : auto;
background-color : #fff;
margin : 1em;
padding : 10px;
font-size : 1.2rem;
vertical-align : middle;
border-radius : 5px;
}
ul#todo-items li input[type=checkbox] {
width : 1.5em;
height : 1.5em;
margin-right : 20px;
vertical-align : middle;
}
.delete-button {
display : inline-flex;
justify-content : center;
align-items : center;
float : right;
width : 25px;
height : 25px;
background-color : hsl(348, 93%, 56%);
border : none;
outline : none;
font-size : 0.8rem;
font-weight : 550;
text-align : center;
vertical-align : middle;
}
.delete-button:hover {
background: pink;
}
ul#todo-items li input:checked + span {
text-decoration: line-through;
}
<div class="container">
<h1>To Do</h1>
<form id="todo-form">
<input type="text" name="todo-text" placeholder="add a todo item">
<button type="submit"> Add </button>
</form>
<ul id="todo-items"> </ul>
</div>
I am having a Trouble with this custom media player, Media player not functioning properly.
I have 2 problems:
When full-screen it goes back to the default HTML5 player
When i adjust the width of the video tag it throws all of the spacing out and ruins the player.
HTML
<!DOCTYPE HTML>
<html>
<head>
<title>Video/Audio</title>
<link rel='stylesheet' type='text/css' href='style.css' />
<style type="text/css">
</style>
<script src='jquery.js'></script>
<script src='javascript.js'></script>
<script type='text/javascript'>
$(document).ready(function() {
$('video').videoPlayer({
'playerWidth' : 1,
'videoClass' : 'video'
});
});
</script>
</head>
<body>
<div class="container" class="player">
<video width="700" height="400">
<source src="https://s3-eu-west-1.amazonaws.com/icevideos/151014+Cathodic+Protection+of+Highways/151014.PETERBOROUGH.CATHODICPROTECTION.HIGH1.mp4" type="video/mp4">
<source src="movie.webm" type="video/webm">
</video>
</div>
</body>
</html>
CSS
body {
font-size: 62.5%;
padding: 0;
margin: 0;
}
.player {
background: grey;
box-sizing: border-box;
height: 40px;
-moz-box-sizing: border-box;
float: left;
font-family: Arial, sans-serif;
position: absolute;
padding: 0;
bottom: 4px;
z-index: 2;
opacity: 1;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
-webkit-transition: opacity 0.3s ease-in;
transition: opacity 0.3s ease-in;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
width: 100%;
}
.video {
position: relative;
margin: 0px auto;
}
.video:hover .player {
opacity: 1;
}
.player .progress {
width: 60%;
height: 20px;
border-radius: 5px;
background: #000;
box-shadow: inset 0 -5px 10px rgba(0,0,0,0.1);
float: left;
cursor: pointer;
margin: 12px 0 0 0;
padding: 0;
position: relative;
font-variant: normal;
margin-left: 20px;
}
.player .progress-bar {
background: #FF6600;
border-radius: 5px;
height: 100%;
position: relative;
z-index: 999;
width: 0;
}
.player .button-holder {
position: relative;
left: 10px;
}
.player .progress-button {
background: #00bdff;
box-shadow: 0 0 20px rgba(0,0,0,0.3);
border-radius: 30px;
width: 20px;
height: 20px;
position: absolute;
left: -20px;
text-decoration: overline;
}
.player [class^="buffered"] {
background: rgba(255,255,255,0.1);
position: absolute;
top: 0;
left: 30px;
height: 100%;
border-radius: 5px;
z-index: 1;
}
.player .play-pause {
display: inline-block;
font-size: 3em;
float: left;
text-shadow: 0 0 0 #fff;
color: #00bdff;
width: 4%;
padding: 4px 0 0 0;
margin-left: 15px;
cursor: pointer;
font-variant: small-caps;
}
.player .play, .player .pause-button {
-webkit-transition: all 0.2s ease-out;
}
.player .play .pause-button, .player .pause .play-button {
display: none;
}
.player .pause-button {
padding: 5px 2px;
box-sizing: border-box;
-moz-box-sizing: border-box;
height: 34px;
}
.player .pause-button span {
background: #FF6600;
width: 8px;
height: 24px;
float: left;
display: block;
}
.player .pause-button span:first-of-type {
margin: 0 4px 0 0;
}
.player .time {
color: #fff;
font-weight: bold;
font-size: 1.2em;
position: absolute;
width: 150px;
margin-left: 425px;
bottom: 3px;
}
.player .stime, .ttime {
color: #fff;
}
.player .play:hover {
text-shadow: 0 0 5px #fff;
}
.player .play:active, .pause-button:active span {
text-shadow: 0 0 7px #fff;
}
.player .pause-button:hover span {
box-shadow: 0 0 5px #fff;
} .player .pause-button:active span {
box-shadow: 0 0 7px #fff;
}
.player .volume {
position: relative;
float: left;
width: 7%;
height: 100%;
margin-left: 70px;
}
.player .volume-icon {
padding: 1.5%;
height: 100%;
cursor: pointer;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-transition: all 0.15s linear;
}
.player .volume-icon-hover {
background-color: grey;
}
.player .volume-holder {
height: 100px;
width: 80%;
position: absolute;
display: none;
background: grey;
left: 0;
border-radius: 5px 5px 0 0;
top: -100px;
}
.player .volume-bar-holder {
background: black;
width: 20px;
box-shadow: inset 0px 0px 5px rgba(0,0,0,0.3);
margin: 15px auto;
height: 80px;
border-radius: 5px;
position: relative;
cursor: pointer;
}
.player .volume-button {
background: #00bdff;
box-shadow: 0 0 20px rgba(0,0,0,0.3);
border-radius: 30px;
width: 20px;
height: 20px;
}
.player .volume-button-holder {
position: relative;
top: -10px;
}
.player .volume-bar {
background: #FF6600;
border-radius: 5px;
width: 100%;
height: 100%;
position: absolute;
bottom: 0;
}
.player .fullscreen {
width: 5%;
cursor: pointer;
float: left;
height: 100%;
}
.player .fullscreen a {
width: 25px;
height: 20px;
border-radius: 3px;
background: #00bdff;
display: block;
position: relative;
top: 10px;
margin: 0px auto;
}
.player .fullscreen a:hover {
background: #FF6600;
}
.player .volume-icon span {
width: 20%;
height: 23%;
background-color: #00bdff;
display: block;
position: relative;
z-index: 1;
font-weight: bold;
top: 40%;
color: #fff;
left: 22%;
}
.player .volume-icon span:before,
.player .volume-icon span:after {
content: '';
position: absolute;
}
.player .volume-icon span:before {
width: 0;
height: 0;
border: 1em solid transparent;
border-left: none;
border-right-color: #00bdff;
z-index: 2;
top: -2px;
left: 10%;
margin-top: -40%;
}
.player .volume-icon span:after {
width: 10%;
height: 4%;
border: 1px solid #00bdff;
left: 150%;
border-width: 0px 0px 0 0;
border-radius: 0 50px 0 0;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
font-variant: small-caps;
}
.player .v-change-11 span:after { border-width: 10px 10px 0 0; top: 0; }
.player .v-change-10 span:after { border-width: 9px 9px 0 0; top: 1px; }
.player .v-change-9 span:after { border-width: 8px 8px 0 0; top: 1px; }
.player .v-change-8 span:after { border-width: 7px 7px 0 0; top: 2px; }
.player .v-change-7 span:after { border-width: 6px 6px 0 0; top: 2px; }
.player .v-change-6 span:after { border-width: 5px 5px 0 0; top: 3px; }
.player .v-change-5 span:after { border-width: 4px 4px 0 0; top: 3px; }
.player .v-change-4 span:after { border-width: 3px 3px 0 0; top: 4px; }
.player .v-change-3 span:after { border-width: 2px 2px 0 0; top: 4px; }
.player .v-change-2 span:after { border-width: 1px 1px 0 0; top: 5px; }
.player .v-change-1 span:after { border-width: 0px 0px 0 0; top: 5px; }
.player .v-change-1 span:after {
content: '+';
-webkit-transform: rotate(45deg);
font-size: 20px;
top: -6px;
left: 25px;
color: #00bdff;
}
/* ------- IGNORE */
#header {
width: 100%;
margin: 0px auto;
}
#header #center {
text-align: center;
}
#header h1 span {
color: #000;
display: block;
font-size: 50px;
}
#header p {
font-family: 'Georgia', serif;
}
#header h1 {
color: #892dbf;
font: bold 40px 'Bree Serif', serif;
}
#travel {
padding: 10px;
background: rgba(0,0,0,0.6);
border-bottom: 2px solid rgba(0,0,0,0.2);
font-variant: normal;
text-decoration: none;
}
#travel a {
font-family: 'Georgia', serif;
text-decoration: none;
border-bottom: 1px solid #f9f9f9;
font-size: 20px;
color: #f9f9f9;
}
.container {
padding: 40px 0 0 0;
}
.logo {
margin-top: 9px;
float: left;
margin-left: 6px;
}
JS
(function($) {
$.fn.videoPlayer = function(options) {
var settings = {
playerWidth : '0.95', // Default is 95%
videoClass : 'video' // Video Class
}
// Extend the options so they work with the plugin
if(options) {
$.extend(settings, options);
}
// For each so that we keep chainability.
return this.each(function() {
$(this)[0].addEventListener('loadedmetadata', function() {
// Basic Variables
var $this = $(this);
var $settings = settings;
// Wrap the video in a div with the class of your choosing
$this.wrap('<div class="'+$settings.videoClass+'"></div>');
// Select the div we just wrapped our video in for easy selection.
var $that = $this.parent('.'+$settings.videoClass);
// The Structure of our video player
{
$( '<div class="player">'
+'<img class="logo" src="http://www.cpdonline.tv/ice-events/mediaplayer/icelogo.png" height="20px">'
+ '<div class="play-pause play">'
+ '<span class="play-button">►</span>'
+ '<div class="pause-button">'
+ '<span> </span>'
+ '<span> </span>'
+ '</div>'
+ '</div>'
+ '<div class="progress">'
+ '<div class="progress-bar">'
+ '<div class="button-holder">'
+ '<div class="progress-button"> </div>'
+ '</div>'
+ '</div>'
+ '<div class="time">'
+ '<span class="ctime">00:00</span>'
+ '<span class="stime"> / </span>'
+ '<span class="ttime">00:00</span>'
+ '</div>'
+ '</div>'
+ '<div class="volume">'
+ '<div class="volume-holder">'
+ '<div class="volume-bar-holder">'
+ '<div class="volume-bar">'
+ '<div class="volume-button-holder">'
+ '<div class="volume-button"> </div>'
+ '</div>'
+ '</div>'
+ '</div>'
+ '</div>'
+ '<div class="volume-icon v-change-0">'
+ '<span> </span>'
+ '</div>'
+ '</div>'
+ '<div class="fullscreen"> '
+ ' '
+ '</div>'
+ '</div>').appendTo($that);
}
// Width of the video
$videoWidth = $this.width();
$that.width($videoWidth+'px');
// Set width of the player based on previously noted settings
$that.find('.player').css({'width' : ($settings.playerWidth*100)+'%', 'left' : ((100-$settings.playerWidth*100)/2)+'%'});
// Video information
var $spc = $(this)[0], // Specific video
$duration = $spc.duration, // Video Duration
$volume = $spc.volume, // Video volume
currentTime;
// Some other misc variables to check when things are happening
var $mclicking = false,
$vclicking = false,
$vidhover = false,
$volhover = false,
$playing = false,
$drop = false,
$begin = false,
$draggingProgess = false,
$storevol,
x = 0,
y = 0,
vtime = 0,
updProgWidth = 0,
volume = 0;
// Setting the width, etc of the player
var $volume = $spc.volume;
// So the user cant select text in the player
$that.bind('selectstart', function() { return false; });
// Set some widths
var progWidth = $that.find('.progress').width();
var bufferLength = function() {
// The buffered regions of the video
var buffered = $spc.buffered;
// Rest all buffered regions everytime this function is run
$that.find('[class^=buffered]').remove();
// If buffered regions exist
if(buffered.length > 0) {
// The length of the buffered regions is i
var i = buffered.length;
while(i--) {
// Max and min buffers
$maxBuffer = buffered.end(i);
$minBuffer = buffered.start(i);
// The offset and width of buffered area
var bufferOffset = ($minBuffer / $duration) * 100;
var bufferWidth = (($maxBuffer - $minBuffer) / $duration) * 100;
// Append the buffered regions to the video
$('<div class="buffered"></div>').css({"left" : bufferOffset+'%', 'width' : bufferWidth+'%'}).appendTo($that.find('.progress'));
}
}
}
// Run the buffer function
bufferLength();
// The timing function, updates the time.
var timeUpdate = function($ignore) {
// The current time of the video based on progress bar position
var time = Math.round(($('.progress-bar').width() / progWidth) * $duration);
// The 'real' time of the video
var curTime = $spc.currentTime;
// Seconds are set to 0 by default, minutes are the time divided by 60
// tminutes and tseconds are the total mins and seconds.
var seconds = 0,
minutes = Math.floor(time / 60),
tminutes = Math.round($duration / 60),
tseconds = Math.round(($duration) - (tminutes*60));
// If time exists (well, video time)
if(time) {
// seconds are equal to the time minus the minutes
seconds = Math.round(time) - (60*minutes);
// So if seconds go above 59
if(seconds > 59) {
// Increase minutes, reset seconds
seconds = Math.round(time) - (60*minutes);
if(seconds == 60) {
minutes = Math.round(time / 60);
seconds = 0;
}
}
}
// Updated progress width
updProgWidth = (curTime / $duration) * progWidth
// Set a zero before the number if its less than 10.
if(seconds < 10) { seconds = '0'+seconds; }
if(tseconds < 10) { tseconds = '0'+tseconds; }
// A variable set which we'll use later on
if($ignore != true) {
$that.find('.progress-bar').css({'width' : updProgWidth+'px'});
$that.find('.progress-button').css({'left' : (updProgWidth-$that.find('.progress-button').width())+'px'});
}
// Update times
$that.find('.ctime').html(minutes+':'+seconds)
$that.find('.ttime').html(tminutes+':'+tseconds);
// If playing update buffer value
if($spc.currentTime > 0 && $spc.paused == false && $spc.ended == false) {
bufferLength();
}
}
// Run the timing function twice, once on init and again when the time updates.
timeUpdate();
$spc.addEventListener('timeupdate', timeUpdate);
// When the user clicks play, bind a click event
$that.find('.play-pause').bind('click', function() {
// Set up a playing variable
if($spc.currentTime > 0 && $spc.paused == false && $spc.ended == false) {
$playing = false;
} else { $playing = true; }
// If playing, etc, change classes to show pause or play button
if($playing == false) {
$spc.pause();
$(this).addClass('play').removeClass('pause');
bufferLength();
} else {
$begin = true;
$spc.play();
$(this).addClass('pause').removeClass('play');
}
});
// Bind a function to the progress bar so the user can select a point in the video
$that.find('.progress').bind('mousedown', function(e) {
// Progress bar is being clicked
$mclicking = true;
// If video is playing then pause while we change time of the video
if($playing == true) {
$spc.pause();
}
// The x position of the mouse in the progress bar
x = e.pageX - $that.find('.progress').offset().left;
// Update current time
currentTime = (x / progWidth) * $duration;
$spc.currentTime = currentTime;
});
// When the user clicks on the volume bar holder, initiate the volume change event
$that.find('.volume-bar-holder').bind('mousedown', function(e) {
// Clicking of volume is true
$vclicking = true;
// Y position of mouse in volume slider
y = $that.find('.volume-bar-holder').height() - (e.pageY - $that.find('.volume-bar-holder').offset().top);
// Return false if user tries to click outside volume area
if(y < 0 || y > $(this).height()) {
$vclicking = false;
return false;
}
// Update CSS to reflect what's happened
$that.find('.volume-bar').css({'height' : y+'px'});
$that.find('.volume-button').css({'top' : (y-($that.find('.volume-button').height()/2))+'px'});
// Update some variables
$spc.volume = $that.find('.volume-bar').height() / $(this).height();
$storevol = $that.find('.volume-bar').height() / $(this).height();
$volume = $that.find('.volume-bar').height() / $(this).height();
// Run a little animation for the volume icon.
volanim();
});
// A quick function for binding the animation of the volume icon
var volanim = function() {
// Check where volume is and update class depending on that.
for(var i = 0; i < 1; i += 0.1) {
var fi = parseInt(Math.floor(i*10)) / 10;
var volid = (fi * 10)+1;
if($volume == 1) {
if($volhover == true) {
$that.find('.volume-icon').removeClass().addClass('volume-icon volume-icon-hover v-change-11');
} else {
$that.find('.volume-icon').removeClass().addClass('volume-icon v-change-11');
}
}
else if($volume == 0) {
if($volhover == true) {
$that.find('.volume-icon').removeClass().addClass('volume-icon volume-icon-hover v-change-1');
} else {
$that.find('.volume-icon').removeClass().addClass('volume-icon v-change-1');
}
}
else if($volume > (fi-0.1) && volume < fi && !$that.find('.volume-icon').hasClass('v-change-'+volid)) {
if($volhover == true) {
$that.find('.volume-icon').removeClass().addClass('volume-icon volume-icon-hover v-change-'+volid);
} else {
$that.find('.volume-icon').removeClass().addClass('volume-icon v-change-'+volid);
}
}
}
}
// Run the volanim function
volanim();
// Check if the user is hovering over the volume button
$that.find('.volume').hover(function() {
$volhover = true;
}, function() {
$volhover = false;
});
// For usability purposes then bind a function to the body assuming that the user has clicked mouse
// down on the progress bar or volume bar
$('body, html').bind('mousemove', function(e) {
// Hide the player if video has been played and user hovers away from video
if($begin == true) {
$that.hover(function() {
$that.find('.player').stop(true, false).animate({'opacity' : '1'}, 0.5);
}, function() {
$that.find('.player').stop(true, false).animate({'opacity' : '0'}, 0.5);
});
}
// For the progress bar controls
if($mclicking == true) {
// Dragging is happening
$draggingProgress = true;
// The thing we're going to apply to the CSS (changes based on conditional statements);
var progMove = 0;
// Width of the progress button (a little button at the end of the progress bar)
var buttonWidth = $that.find('.progress-button').width();
// Updated x posititon the user is at
x = e.pageX - $that.find('.progress').offset().left;
// If video is playing
if($playing == true) {
// And the current time is less than the duration
if(currentTime < $duration) {
// Then the play-pause icon should definitely be a pause button
$that.find('.play-pause').addClass('pause').removeClass('play');
}
}
if(x < 0) { // If x is less than 0 then move the progress bar 0px
progMove = 0;
$spc.currentTime = 0;
}
else if(x > progWidth) { // If x is more than the progress bar width then set progMove to progWidth
$spc.currentTime = $duration;
progMove = progWidth;
}
else { // Otherwise progMove is equal to the mouse x coordinate
progMove = x;
currentTime = (x / progWidth) * $duration;
$spc.currentTime = currentTime;
}
// Change CSS based on previous conditional statement
$that.find('.progress-bar').css({'width' : $progMove+'px'});
$that.find('.progress-button').css({'left' : ($progMove-buttonWidth)+'px'});
}
// For the volume controls
if($vclicking == true) {
// The position of the mouse on the volume slider
y = $that.find('.volume-bar-holder').height() - (e.pageY - $that.find('.volume-bar-holder').offset().top);
// The position the user is moving to on the slider.
var volMove = 0;
// If the volume holder box is hidden then just return false
if($that.find('.volume-holder').css('display') == 'none') {
$vclicking = false;
return false;
}
// Add the hover class to the volume icon
if(!$that.find('.volume-icon').hasClass('volume-icon-hover')) {
$that.find('.volume-icon').addClass('volume-icon-hover');
}
if(y < 0 || y == 0) { // If y is less than 0 or equal to 0 then volMove is 0.
$volume = 0;
volMove = 0;
$that.find('.volume-icon').removeClass().addClass('volume-icon volume-icon-hover v-change-11');
} else if(y > $(this).find('.volume-bar-holder').height() || (y / $that.find('.volume-bar-holder').height()) == 1) { // If y is more than the height then volMove is equal to the height
$volume = 1;
volMove = $that.find('.volume-bar-holder').height();
$that.find('.volume-icon').removeClass().addClass('volume-icon volume-icon-hover v-change-1');
} else { // Otherwise volMove is just y
$volume = $that.find('.volume-bar').height() / $that.find('.volume-bar-holder').height();
volMove = y;
}
// Adjust the CSS based on the previous conditional statmeent
$that.find('.volume-bar').css({'height' : volMove+'px'});
$that.find('.volume-button').css({'top' : (volMove+$that.find('.volume-button').height())+'px'});
// Run the animation function
volanim();
// Change the volume and store volume
// Store volume is the volume the user last had in place
// in case they want to mute the video, unmuting will then
// return the user to their previous volume.
$spc.volume = $volume;
$storevol = $volume;
}
// If the user hovers over the volume controls, then fade in or out the volume
// icon hover class
if($volhover == false) {
$that.find('.volume-holder').stop(true, false).fadeOut(100);
$that.find('.volume-icon').removeClass('volume-icon-hover');
}
else {
$that.find('.volume-icon').addClass('volume-icon-hover');
$that.find('.volume-holder').fadeIn(100);
}
})
// When the video ends the play button becomes a pause button
$spc.addEventListener('ended', function() {
$playing = false;
// If the user is not dragging
if($draggingProgress == false) {
$that.find('.play-pause').addClass('play').removeClass('pause');
}
});
// If the user clicks on the volume icon, mute the video, store previous volume, and then
// show previous volume should they click on it again.
$that.find('.volume-icon').bind('mousedown', function() {
$volume = $spc.volume; // Update volume
// If volume is undefined then the store volume is the current volume
if(typeof $storevol == 'undefined') {
$storevol = $spc.volume;
}
// If volume is more than 0
if($volume > 0) {
// then the user wants to mute the video, so volume will become 0
$spc.volume = 0;
$volume = 0;
$that.find('.volume-bar').css({'height' : '0'});
volanim();
}
else {
// Otherwise user is unmuting video, so volume is now store volume.
$spc.volume = $storevol;
$volume = $storevol;
$that.find('.volume-bar').css({'height' : ($storevol*100)+'%'});
volanim();
}
});
// If the user lets go of the mouse, clicking is false for both volume and progress.
// Also the video will begin playing if it was playing before the drag process began.
// We're also running the bufferLength function
$('body, html').bind('mouseup', function(e) {
$mclicking = false;
$vclicking = false;
$draggingProgress = false;
if($playing == true) {
$spc.play();
}
bufferLength();
});
// Check if fullscreen supported. If it's not just don't show the fullscreen icon.
if(!$spc.requestFullscreen && !$spc.mozRequestFullScreen && !$spc.webkitRequestFullScreen) {
$('.fullscreen').hide();
}
// Requests fullscreen based on browser.
$('.fullscreen').click(function() {
if ($spc.requestFullscreen) {
$spc.requestFullscreen();
}
else if ($spc.mozRequestFullScreen) {
$spc.mozRequestFullScreen();
}
else if ($spc.webkitRequestFullScreen) {
$spc.webkitRequestFullScreen();
}
});
});
});
}
})(jQuery);
I would also like to point out that this is someone else source code and cannot find where i got this from.
https://jsfiddle.net/f39huqpv/
I'm trying to create a simple metronome using the web audio oscillator, so that no external audio files are needed. I'm creating the sound of the metronome by ramping the volume of the oscillator up and down very quickly (since you can't use start() and stop() more than once), and then repeating that function at a set interval. It ends up sounding like a nice little wood block.
The code below works/sounds great in Chrome, Safari and Opera. But in Firefox, there's a nasty intermittent "click" when the volume ramps up. I've tried changing the attack/release times to get rid of the click, but they have to be really, really long before it consistently disappears. So long, in fact, that the oscillator just sounds like a sustained note.
var audio = new (window.AudioContext || window.webkitAudioContext)();
var tick = audio.createOscillator();
var tickVol = audio.createGain();
tick.type = 'sine';
tick.frequency.value = 1000;
tickVol.gain.value = 0; //setting the volume to 0 before I connect everything
tick.connect(tickVol);
tickVol.connect(audio.destination);
tick.start(0);
var metronome = {
start: function repeat() {
now = audio.currentTime;
//Make sure volume is 0 and that no events are changing it
tickVol.gain.cancelScheduledValues(now);
tickVol.gain.setValueAtTime(0, now);
//Play the osc with a super fast attack and release so it sounds like a click
tickVol.gain.linearRampToValueAtTime(1, now + .001);
tickVol.gain.linearRampToValueAtTime(0, now + .001 + .01);
//Repeat this function every half second
click = setTimeout(repeat, 500);
},
stop: function() {
if(typeof click !== 'undefined') {
clearTimeout(click);
tickVol.gain.value = 0;
}
}
}
$("#start").click(function(){
metronome.start();
});
$("#stop").click(function(){
metronome.stop();
});
Codepen
Is there any way to get FF to sound like the other 3 browsers?
I was getting the exact same problem in latest Opera and found the problem to be the individual sounds 'decimal time length'.
I wrote a morse code translator, and like yours, it's just a series of simple short sounds/beeps created via createOscillator.
With morse code you have a speed count (words per minute) based on a 5 letter long word like codex or paris.
To get 20 or 30 paris' per minute to finish exactly on the minute, I had to use a sound time length of, for example, 0.61. In Opera, this caused the 'end of sound click'. On changing this to 0.6 and the click disappeared across all browsers - except Firefox.
I've tried freq = 0 and gain = 0 between sounds but still get the click at the end in FF and I don't know enough about Web Audio to try anything else.
On another note, I noticed you're using a loop and timeout to get to the next tick. Have you tried an 'Oscillator onended function' instead? I've used it with a simple counter increment and variable length blank sound/note. Go to the very end of my JS if you want to have a look.
**UPDATE - I've been fiddling about with setValueAtTime() and linearRampToValueAtTime() and appeared to have cracked the click problem. Scroll to bottom of script to see example. **
(function(){
/* Morse Code Generator & Translator - Kurt Grigg 2003 (Updated for sound and CSS3) */
var d = document;
d.write('<div class="Mcontainer">'
+'<div class="Mtitle">Morse Code Generator Translator</div>'
+'<textarea id="txt_in" class="Mtxtarea"></textarea>'
+'<div class="Mtxtareatitle">Input</div>'
+'<textarea id="txt_out" class="Mtxtarea" style="top: 131px;"></textarea>'
+'<div class="Mtxtareatitle" style="top: 172px;">Output</div>'
+'<div class="Mbuttonwrap">'
+'<input type="button" class="Mbuttons" id="how" value="!">'
+'<input type="button" class="Mbuttons" id="tra" value="translate">'
+'<input type="button" class="Mbuttons" id="ply" value="play">'
+'<input type="button" class="Mbuttons" id="pau" value="pause">'
+'<input type="button" class="Mbuttons" id="res" value="reset"></div>'
+'<select id="select" class="Mselect">'
+'<option value=0.07 selected="selected">15 wpm</option>'
+'<option value=0.05>20 wpm</option>'
+'<option value=0.03>30 wpm</option>'
+'</select>'
+'<div class="sliderWrap">volume <input id="volume" type="range" min="0" max="1" step="0.01" value="0.05"/></div>'
+'<div class="Mchckboxwrap">'
+'<span style="text-align: right;">separator <input type="checkbox" id="slash" class="Mchckbox"></span>'
+'</div>'
+'<div id="about" class="Minfo">'
+'<b>Input morse</b><br>'
+'<ul><li>Enter morse into input box using full stop (period) and minus sign (hyphen)</li>'
+'<li>Morse letters must be separated by 1 space</li>'
+'<li>Morse words must be separated by 3 or more spaces</li>'
+'<li>You can use / to separate morse words. There must be at least 1 space before and after each separator used</li>'
+'</ul>'
+'<b>Input text</b><br>'
+'<ul class="Mul"><li>Enter text into input box</li>'
+'<li>Characters that cannot be translated will be ignored</li>'
+'<li>If morse and text is entered, the converter will assume morse mode</li></ul>'
+'<input type="button" value="close" id="clo" class="Mbuttons">'
+'</div><div id="mdl" class="modal"><div id="bdy"><div id="modalMsg">A MSG</div><input type="button" value="close" id="cls" class="Mbuttons"></div></div></div>');
var ftmp = d.getElementById('mdl');
var del;
d.getElementById('tra').addEventListener("click", function(){convertToAndFromMorse(txtIn.value);},false);
d.getElementById('ply').addEventListener("click", function(){CancelIfPlaying();},false);
d.getElementById('pau').addEventListener("click", function(){stp();},false);
d.getElementById('res').addEventListener("click", function(){Rst();txtIn.value = '';txtOt.value = '';},false);
d.getElementById('how').addEventListener("click", function(){msgSelect();},false);
d.getElementById('clo').addEventListener("click", function(){fadeOut();},false);
d.getElementById('cls').addEventListener("click", function(){fadeOut();},false);
d.getElementById('bdy').addEventListener("click", function(){errorSelect();},false);
var wpm = d.getElementById('select');
wpm.addEventListener("click", function(){wpMin()},false);
var inc = 0;
var playing = false;
var txtIn = d.getElementById('txt_in');
var txtOt = d.getElementById('txt_out');
var paused = false;
var allowed = ['-','.',' '];
var aud;
var tmp = (window.AudioContext || window.webkitAudioContext)?true:false;
if (tmp) {
aud = new (window.AudioContext || window.webkitAudioContext)();
}
var incr = 0;
var speed = parseFloat(wpm.options[wpm.selectedIndex].value);
var char = [];
var alphabet = [["A",".-"],["B","-..."],["C","-.-."],["D","-.."],["E","."],["F","..-."],["G","--."],["H","...."],["I",".."],["J",".---"],
["K","-.-"],["L",".-.."],["M","--"],["N","-."],["O","---"],["P",".--."],["Q","--.-"],["R",".-."],["S","..."],["T","-"],["U","..-"],
["V","...-"],["W",".--"],["X","-..-"],["Y","-.--"],["Z","--.."],["1",".----"],["2","..---"],["3","...--"],["4","....-"],["5","....."],
["6","-...."],["7","--..."],["8","---.."],["9","----."],["0","-----"],[".",".-.-.-"],[",","--..--"],["?","..--.."],["'",".----."],["!","-.-.--"],
["/","-..-."],[":","---..."],[";","-.-.-."],["=","-...-"],["-","-....-"],["_","..--.-"],["\"",".-..-."],["#",".--.-."],["(","-.--.-"],[" ",""]];
function errorSelect() {
txtIn.focus();
}
function modalSwap(msg) {
d.getElementById('modalMsg').innerHTML = msg;
}
function msgSelect() {
ftmp = d.getElementById('about');
fadeIn();
}
function fadeIn() {
ftmp.removeEventListener("transitionend", freset);
ftmp.style.display = "block";
del = setTimeout(doFadeIn,100);
}
function doFadeIn() {
clearTimeout(del);
ftmp.style.transition = "opacity 0.5s linear";
ftmp.style.opacity = "1";
}
function fadeOut() {
ftmp.style.transition = "opacity 0.8s linear";
ftmp.style.opacity = "0";
ftmp.addEventListener("transitionend",freset , false);
}
function freset() {
ftmp.style.display = "none";
ftmp.style.transition = "";
ftmp = d.getElementById('mdl');
}
function stp() {
paused = true;
}
function wpMin() {
speed = parseFloat(wpm.options[wpm.selectedIndex].value);
}
function Rst(){
char = [];
inc = 0;
playing = false;
paused = false;
}
function CancelIfPlaying(){
if (window.AudioContext || window.webkitAudioContext) {paused = false;
if (!playing) {
IsReadyToHear();
}
else {
return false;
}
}
else {
modalSwap("<p>Your browser doesn't support Web Audio API</p>");
fadeIn();
return false;
}
}
function IsReadyToHear(x){
if (txtIn.value == "" || /^\s+$/.test(txtIn.value)) {
modalSwap('<p>Nothing to play, enter morse or text first</p>');
fadeIn();
txtIn.value = '';
return false;
}
else if (char.length < 1 && (x != "" || !/^\s+$/.test(txtIn.value)) && txtIn.value.length > 0) {
modalSwap('<p>Click Translate button first . . .</p>');
fadeIn();
return false;
}
else{
playMorse();
}
}
function convertToAndFromMorse(x){
var swap = [];
var outPut = "";
x = x.toUpperCase();
/* Is input empty or all whitespace? */
if (x == '' || /^\s+$/.test(x)) {
modalSwap("<p>Nothing to translate, enter morse or text</p>");
fadeIn();
txtIn.value = '';
return false;
}
/* Remove front & end whitespace */
x = x.replace(/\s+$|^\s*/gi, '');
txtIn.value = x;
txtOt.value = "";
var isMorse = (/(\.|\-)\.|(\.|\-)\-/i.test(x));// Good enough.
if (!isMorse){
for (var i = 0; i < alphabet.length; i++){
swap[i] = [];
for (var j = 0; j < 2; j++){
swap[i][j] = alphabet[i][j].replace(/\-/gi, '\\-');
}
}
}
var swtch1 = (isMorse) ? allowed : swap;
var tst = new RegExp( '[^' + swtch1.join('') + ']', 'g' );
var swtch2 = (isMorse)?' ':'';
x = x.replace( tst, swtch2); //remove unwanted chars.
x = x.split(swtch2);
if (isMorse) {
var tidy = [];
for (var i = 0; i < x.length; i++){
if ((x[i] != '') || x[i+1] == '' && x[i+2] != '') {
tidy.push(x[i]);
}
}
}
var swtch3 = (isMorse) ? tidy : x;
for (var j = 0; j < swtch3.length; j++) {
for (var i = 0; i < alphabet.length; i++){
if (isMorse) {
if (tidy[j] == alphabet[i][1]) {
outPut += alphabet[i][0];
}
}
else {
if (x[j] == alphabet[i][0]) {
outPut += alphabet[i][1] + ((j < x.length-1)?" ":"");
}
}
}
}
if (!isMorse) {
var wordDivide = (d.getElementById('slash').checked)?" / ":" ";
outPut = outPut.replace(/\s{3,}/gi, wordDivide);
}
if (outPut.length < 1) {
alert('Enter valid text or morse...');
txtIn.value = '';
}
else {
txtOt.value = outPut;
}
var justMorse = (!isMorse) ? outPut : tidy;
FormatForSound(justMorse);
}
function FormatForSound(s){
var n = [];
var b = '';
if (typeof s == 'object') {
for (var i = 0; i < s.length; ++i) {
var f = (i == s.length-1)?'':' ';
var t = b += (s[i] + f);
}
}
var c = (typeof s == 'object')? t : s;
c = c.replace(/\//gi, '');
c = c.replace(/\s{1,3}/gi, '4');
c = c.replace(/\./gi, '03');
c = c.replace(/\-/gi, '13');
c = c.split('');
for (var i = 0; i < c.length; i++) {
n.push(c[i]);
}
char = n;
}
function vlm() {
return document.getElementById('volume').value;
}
function playMorse() {
if (paused){
playing = false;
return false;
}
playing = true;
if (incr >= char.length) {
incr = 0;
playing = false;
paused = false;
return false;
}
var c = char[incr];
var freq = 550;
var volume = (c < 2) ? vlm() : 0 ;
var flen = (c == 0 || c == 3) ? speed : speed * 3;
var osc = aud.createOscillator();
osc.type = 'sine';
osc.frequency.value = freq;
var oscGain = aud.createGain();
oscGain.gain.value = volume;
osc.connect(oscGain);
oscGain.connect(aud.destination);
var now = aud.currentTime;
osc.start(now);
/*
Sharp volume fade to stop harsh clicks if wave is stopped
at a point other than the (natural zero crossing point)
*/
oscGain.gain.setValueAtTime(volume, now + (flen*0.8));
oscGain.gain.linearRampToValueAtTime(0.0, now + (flen*0.9999));
osc.stop(now + flen);
osc.onended = function() {
incr++;
playMorse();
}
}
})();
body {
text-align: center;
}
.Mcontainer {
display: inline-block;
position: relative;
width: 382px;
height: 302px;
border: 1px solid #000;
border-radius: 6px;
text-align: center;
font: bold 11px sans-serif;
background-color: rgb(203,243,65);
box-shadow: 0px 4px 2px rgba(0,0,0,0.3);
}
.Mtitle {
-webkit-user-select: none;
-moz-user-select: none;
display: inline-block;
position: absolute;
width: 380px;
height: 20px;
margin: auto;
left: 0; right: 0;
font-size: 16px;
line-height: 20px;
color: #666;
}
.Mtxtareatitle {
-webkit-user-select: none;
-moz-user-select: none;
display: block;
position: absolute;
top: 60px;
left: -36px;
height: 22px;
width: 106px;
font-size: 18px;
line-height: 22px;
text-align: center;
color: #555;
transform: rotate(-90deg);
}
.Mtxtarea {
display: block;
position: absolute;
top: 18px;
margin: auto;
left: 0; right: 0;
height: 98px;
width: 344px;
border: 0.5px solid #000;
border-radius: 6px;
padding-top: 6px;
padding-left: 24px;
resize: none;
background-color: #fffff0;
font: bold 10px courier;
color: #555;
text-transform: uppercase;
overflow: auto;
outline: 0; box-shadow: inset 0px 2px 5px rgba(0,0,0,0.5);
}
.Minfo {
display: none;
position: absolute;
top: -6px; left:-6px;
padding: 6px;
height: auto;
width: 370px;
text-align: left;
border: 0.5px solid #000;
border-radius: 6px;
box-shadow: 0px 4px 2px rgba(0,0,0,0.3);
background-color: rgb(203,243,65);
font: 11px sans-serif;
color: #555;
opacity: 0;
}
.Mbuttonwrap {
display: block;
position: absolute;
top: 245px;
margin: auto;
left: 0; right: 0;
height: 26px;
width: 100%;
}
.Mbuttons {
display: inline-block;
width: 69px;
height: 22px;
border: none;
margin: 0px 3.1px 0px 3.1px;
background-color: transparent;
font: bold 11px sans-serif;
color: #555;
border-radius: 20px;
cursor: pointer;
box-shadow: 0px 2px 2px rgba(0,0,0,0.5);
outline: 0;
}
.Mbuttons:hover {
background-color: rgb(213,253,75);
}
.Mbuttons:active {
position: relative;
top: 1px;
box-shadow: 0px 1px 2px rgba(0,0,0,0.8);
}
.Mchckboxwrap {
display: block;
position: absolute;
top: 274px;
left: 289px;
width: 87px;
height: 21px;
line-height: 22px;
border: 0.5px solid #000;
color: #555;
background: #fff;
-webkit-user-select: none;
-moz-user-select: none;
}
.Mselect {
display: block;
position: absolute;
top: 274px;
left: 6px;
width: 88px;
height: 22px;
border: 0.5px solid #000;
padding-left: 5%;
background: #fff;
font: bold 11px sans-serif;
color: #555;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
outline: 0;
}
::selection {
color: #fff;
background: #555;
}
.Mchckbox {
margin-top: 1px;
vertical-align: middle;
cursor: pointer;
outline: 0;
}
.modal {
display: none;
position: absolute;
margin: auto;
top: 0;right: 0;bottom: 0;left: 0;
background: rgba(0,0,0,0.5);
-webkit-user-select: none;
-moz-user-select: none;
opacity: 0;
text-align: center;
}
.modal > div {
display: inline-block;
position: relative;
width: 250px;
height: 70px;
margin: 10% auto;
padding: 10px;
border: 0.5px solid #000;
border-radius:6px;
background-color: rgb(203,243,65);
font: bold 11px sans-serif;
color: #555;
box-shadow: 4px 4px 2px rgba(0,0,0,0.3);
text-align: center;
}
.sliderWrap {
display: block;
position: absolute;
top: 274px;
margin:auto;padding: 0;
left: 0; right: 0;
width: 184px;
height: 21px;
border: 0.5px solid #000;
background: #fff;
font: bold 11px sans-serif;
color: #555;
line-height: 21px;
text-align: center;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
outline: 0;
}
input[type=range] {
-webkit-appearance: none;
width: 50%;
margin: 0;padding: 0;
vertical-align: middle;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: #666;
}
input[type=range]::-webkit-slider-thumb {
box-shadow: 1px 1px 0.5px rgba(0, 0, 0, 0.5);
border: none;
height: 10px;
width: 20px;
border-radius: 5px;
background: #ffffff;
cursor: pointer;
-webkit-appearance: none;
margin-top: -3px;
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: #666;
}
input[type=range]::-moz-range-track {
width: 100%;
height: 4px;
cursor: pointer;
background: #666;
}
input[type=range]::-moz-range-thumb {
box-shadow: 1px 1px 0.5px rgba(0, 0, 0, 0.5);
height: 10px;
width: 20px;
border: none;
border-radius: 5px;
background: #ffffff;
cursor: pointer;
}
input[type=range]::-ms-thumb {
height: 10px;
width: 20px;
border: none;
border-radius: 5px;
background: #ffffff;
box-shadow: 1px 1px 0.5px rgba(0, 0, 0, 0.5);
cursor: pointer;
}
input[type=range]::-ms-track {
width: 100%;
height: 4px;
cursor: pointer;
background: transparent;
border: 5px solid transparent;
color: transparent;
}
input[type=range]::-ms-fill-lower {
background: #666;
}
input[type=range]::-ms-fill-upper {
background: #666;
}
::-ms-tooltip {
display: none;
}
select::-ms-expand {
display: none;
}
It would be best to get Firefox to fix the issue (if indeed it is a Firefox bug with automations). Having said that, you could probably make all the browsers be consistent by using an AudioBufferSource node that has a precomputed click waveform that you want. Just generate a sine wave, ramp it up and down as you want (manually) and play that back at regular intervals.
Not great, but it should be cross-platform.
AFAIK this issue is not specific to Firefox, although looking at your code, I'm unsure why it doesn't happen in other browsers.
The problem is that the moment you schedule a *rampToValueAtTime to an audible source when that source is not currently interpolating between two ramp points, the "clicking" sound occurrs, possibly due to how the underlying implementation will immediately start taking the new ramp point into consideration, even if it's scheduled to happen the future.
The clicking sound will also be heard if you schedule a new ramp point between two points between which interpolation is occurring.
What I came up with as a workaround solution is either using an alternative approach to gradually changing AudioParam values, setTargetAtTime, or setting the value property of the AudioParam to the first ramp point value. Not setValueAtTime, but assigning to the value property itself, before anything audible happens on the given branch.
setTargetAtTime
You'll be needing neither cancelScheduledValues nor setValueAtTime, just two calls to setTargetAtTime, which is just a setValueAtTime with an exponential interpolation with a specified length.
var metronome = {
start: function repeat() {
now = audio.currentTime;
//Play the osc with a super fast attack and release so it sounds like a click
tickVol.gain.setTargetAtTime(1, now, 0.01);
tickVol.gain.setTargetAtTime(0, now + 0.01, 0.01);
//Repeat this function every half second
click = setTimeout(repeat, 500);
}
}
Live demo on JSFiddle