package ONO::FW::Edu::Apps::Modules::Metronome;
################################################################################
# COPYRIGHT / LICENSE #
################################################################################
#
# This file is part of the ONO Software Project.
#
# Copyright (C) 2022-2026 Jos Kirps and The Joopita Project [ASBL]
# Copyright (C) 2011-2022 Jos Kirps and Joopita Research ASBL
# Copyright (C) 2006-2011 Jos Kirps and The Joopita Project
# Copyright (C) 2001-2006 Jos Kirps and The WobbleWolf Project
#
# This file, as well as many other parts of the ONO Software Project or
# related elements, are FREE SOFTWARE available under the ARTISTIC LICENSE 2.0.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# For the full license, see /ono/osr/license/LICENSE.txt, or write to
# jos_AT_kirps_DOT_com or contact_AT_joopita_DOT_com.
#
################################################################################
# END OF COPYRIGHT / LICENSE, HERE COMES THE CODE ... #
################################################################################
#: This module is a front end application module wrapper that has been
#: created for the Morzino Project (morzino.com) and that has been released
#: as Open Source software.
#:
#: Note that this is NOT a self-contained application, it is only a wrapper
#: that makes heavy use of the ONO Perl Framework application engine.
use strict;
use Encode;
use ONO::Render;
use ONO::ToolBox::Auth;
use ONO::Lib::Lang::LangKitEdu;
use ONO::Lib::DateTime::ToolBox;
use ONO::FW::Apps::Core;
use ONO::FW::Apps::Core::Graphics;
use ONO::FW::Edu::Apps::ToolBox;
use ONO::FW::User::Init;
use ONO::IO;
use ONO::DB;
use ONO::Lib::Basic;
sub play {
my (
$self,
$option,
$vars_ref,
$HTTP,
$BASE,
) = @_;
################################################################
# INIT - BEGIN
################################################################
my (
$db,
$community,
$action_url,
$form,
$format,
$canvas,
$pdfkey,
$sec,$min,$hour,
$mday,$mon,$year,
$wday,$yday,$timestamp,
$lang,
$BLK_ref,
$vars_ref,
) = ONO::FW::Apps::Core->init($vars_ref);
my %vars = %$vars_ref;
my %BLK = %$BLK_ref;
my ($HEAD,$WEB);
my (
$vpath,
$lang,$domain,$community,
$sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$timestamp,
$script_url,$action_url,$form
) = ONO::FW::User::Init->getvar(\%vars);
ONO::FW::Apps::Core->now("","chess",$vars{'app_lang'});
my $action_url = qq~$ENV{'SCRIPT_NAME'}?app_lang=$vars{'app_lang'}&~;
###################################################################
# DAUDIO
###################################################################
my $CSSJS = ONO::Render->cssjs($BASE);
my $whiler = "while"; # this is required to avoid whilecounter protection warnings
my $AUDIO = qq~<button id="play-button" class="button button_green">
<div id="play-pause-icon" class="play"></div>
</button>
<div class="tempo-controls">
<button class="tempo-change button_dark" data-change="-10">-10</button>
<button class="tempo-change button_dark" data-change="-5">-5</button>
<button class="tempo-change button_dark" data-change="-1">-</button>
<div class="tempo-container">
<div id="tempo">120</div>
<div class="bpm">bpm</div>
</div>
<button class="tempo-change button_dark" data-change="+1">+</button>
<button class="tempo-change button_dark" data-change="+5">+5</button>
<button class="tempo-change button_dark" data-change="+10">+10</button>
</div>
<div class="mt20">
<button class="tempo-set button button_left" data-change="60">60</button>
<button class="tempo-set button button_middle" data-change="80">80</button>
<button class="tempo-set button button_middle" data-change="100">100</button>
<button class="tempo-set button button_middle" data-change="120">120</button>
<button class="tempo-set button button_middle" data-change="140">140</button>
<button class="tempo-set button button_middle" data-change="160">160</button>
<button class="tempo-set button button_middle" data-change="180">180</button>
<button class="tempo-set button button_right" data-change="240">240</button>
</div>
<div class="mt20">
<button id="but_43" class="tempo-beats button button_left" data-change="3"
onclick="onojs_class('but_43','tempo-beats button_green button_left');onojs_class('but_44','tempo-beats button button_right');">3/4</button>
<button id="but_44" class="tempo-beats button button_green button_right" data-change="4"
onclick="onojs_class('but_44','tempo-beats button_green button_right');onojs_class('but_43','tempo-beats button button_left');">4/4</button>
</div>
<script>
class Metronome
{
constructor(tempo = 120)
{
this.audioContext = null;
this.notesInQueue = []; // notes that have been put into the web audio and may or may not have been played yet {note, time}
this.currentBeatInBar = 0;
this.beatsPerBar = 4;
this.tempo = tempo;
this.lookahead = 25; // How frequently to call scheduling function (in milliseconds)
this.scheduleAheadTime = 0.1; // How far ahead to schedule audio (sec)
this.nextNoteTime = 0.0; // when the next note is due
this.isRunning = false;
this.intervalID = null;
}
nextNote()
{
// Advance current note and time by a quarter note (crotchet if you're posh)
var secondsPerBeat = 60.0 / this.tempo; // Notice this picks up the CURRENT tempo value to calculate beat length.
this.nextNoteTime += secondsPerBeat; // Add beat length to last beat time
this.currentBeatInBar++; // Advance the beat number, wrap to zero
if (this.currentBeatInBar == this.beatsPerBar) {
this.currentBeatInBar = 0;
}
}
scheduleNote(beatNumber, time)
{
// push the note on the queue, even if we're not playing.
this.notesInQueue.push({ note: beatNumber, time: time });
// create an oscillator
const osc = this.audioContext.createOscillator();
const envelope = this.audioContext.createGain();
osc.frequency.value = (beatNumber % this.beatsPerBar == 0) ? 1000 : 800;
envelope.gain.value = 1;
envelope.gain.exponentialRampToValueAtTime(1, time + 0.001);
envelope.gain.exponentialRampToValueAtTime(0.001, time + 0.02);
osc.connect(envelope);
envelope.connect(this.audioContext.destination);
osc.start(time);
osc.stop(time + 0.03);
}
scheduler() {
// while there are notes that will need to play before the next interval, schedule them and advance the pointer.
$whiler (this.nextNoteTime < this.audioContext.currentTime + this.scheduleAheadTime ) {
this.scheduleNote(this.currentBeatInBar, this.nextNoteTime);
this.nextNote();
}
}
start() {
if (this.isRunning) return;
if (this.audioContext == null)
{
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
this.isRunning = true;
this.currentBeatInBar = 0;
this.nextNoteTime = this.audioContext.currentTime + 0.05;
this.intervalID = setInterval(() => this.scheduler(), this.lookahead);
}
stop() {
this.isRunning = false;
clearInterval(this.intervalID);
}
startStop() {
if (this.isRunning) {this.stop()} else {this.start()}
}
}
var metronome = new Metronome();
var tempo = document.getElementById('tempo');
tempo.textContent = metronome.tempo;
var playPauseIcon = document.getElementById('play-pause-icon');
var playButton = document.getElementById('play-button');
playButton.addEventListener('click', function() {
metronome.startStop();
if (metronome.isRunning) {
playPauseIcon.className = 'pause';
}
else {
playPauseIcon.className = 'play';
}
});
var tempoChangeButtons = document.getElementsByClassName('tempo-change');
for (var i = 0; i < tempoChangeButtons.length; i++) {
tempoChangeButtons[i].addEventListener('click', function() {
metronome.tempo += parseInt(this.dataset.change);
tempo.textContent = metronome.tempo;
});
}
var tempoSetButtons = document.getElementsByClassName('tempo-set');
for (var i = 0; i < tempoSetButtons.length; i++) {
tempoSetButtons[i].addEventListener('click', function() {
metronome.tempo = parseInt(this.dataset.change);
tempo.textContent = metronome.tempo;
});
}
var tempoBeatsButtons = document.getElementsByClassName('tempo-beats');
for (var i = 0; i < tempoBeatsButtons.length; i++) {
tempoBeatsButtons[i].addEventListener('click', function() {
metronome.beatsPerBar = parseInt(this.dataset.change);
beatsPerBar.textContent = metronome.tempo;
});
}
</script>
~;
print qq~Content-Type: text/html; charset=UTF-8\n\n
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="Generator" content="ONO App Engine">
$CSSJS
<style>
* {
margin: 0; padding: 0;
}
html {
height: 100%;
}
body {
display: flex;
align-items: center;
justify-content: center;
flex-flow: column;
height: 100%;
}
#play-button {
width: 150px;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
flex-flow: column;
}
#tempo {
font-size: 40px;
}
.bpm {
font-size: 12px;
text-align: center;
}
.tempo-controls {
margin-top: 30px;
display: flex;
align-items: center;
justify-content: center;
flex-flow: row;
}
.tempo-container {
margin: 0 15px;
}
button_unused {
padding: 6px;
border: none;
border-radius: 100%;
width: 40px;
height: 40px;
margin: 5px;
background-color: #99cc33;
color: #ffffff;
cursor: pointer;
}
button:focus {
outline: none;
}
a {
color: #e9e9e9;
}
a:hover {
color: #356161;
}
.play {
width: 0;
height: 0;
border-top: 30px solid transparent;
border-bottom: 30px solid transparent;
border-left: 50px solid #ffffff;
margin-left: 15px;
}
.pause {
width: 15px;
height: 60px;
border: none;
border-left: 15px solid #ffffff;
border-right: 15px solid #ffffff;
}
</style>
</head>
<body style="margin:0px;padding:0px">
$AUDIO
</body>
</html>
~;
}
###################################################################
#### THAT'S IT :-D
###################################################################
1;
__END__