tajik recitations syncing
This commit is contained in:
parent
edf8f03660
commit
8ad31db2c7
@ -101,7 +101,7 @@
|
||||
@foreach (var recitation in Model.GanjoorPage.Poem.Recitations)
|
||||
{
|
||||
<div class="tg-recitation">
|
||||
<audio controls preload="none">
|
||||
<audio id="tg-audio-@recitation.Id" controls preload="none">
|
||||
<source src="@recitation.Mp3Url" />
|
||||
</audio>
|
||||
<div class="tg-recitation-caption">
|
||||
@ -112,6 +112,15 @@
|
||||
}
|
||||
<a class="tg-recitation-download" href="@recitation.Mp3Url" download title="Боргирӣ">⬇</a>
|
||||
</div>
|
||||
@if (recitation.InSyncWithText)
|
||||
{
|
||||
<script>
|
||||
tgInitRecitationSync(
|
||||
document.getElementById('tg-audio-@recitation.Id'),
|
||||
'@Html.Raw($"{TajikGanjoor.APIRoot.InternetUrl}/api/audio/file/{recitation.Id}.xml")'
|
||||
);
|
||||
</script>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="~/css/tajik-ganjoor.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="~/TajikGanjoor.styles.css" asp-append-version="true" />
|
||||
<script src="~/js/recitation-sync.js" asp-append-version="true"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
|
||||
@ -668,3 +668,11 @@ html, body {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* verse currently being recited, highlighted by recitation-sync.js */
|
||||
.tg-verse-active {
|
||||
color: var(--tg-accent-deep) !important;
|
||||
background-color: var(--tg-paper-edge);
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
92
TajikGanjoor/wwwroot/js/recitation-sync.js
Normal file
92
TajikGanjoor/wwwroot/js/recitation-sync.js
Normal file
@ -0,0 +1,92 @@
|
||||
/* Syncs poem text highlighting to an <audio> recitation's timing XML.
|
||||
Simplified port of the equivalent logic in the main ganjoor.net site
|
||||
(hilightverse/_setXml/_trackTimeChanged in wwwroot/js/bk.js and
|
||||
Pages/Partials/GanjoorPage/PageTypes/_PoemPagePartial.cshtml) - this
|
||||
version drops the inline pause/scroll-lock buttons injected into the
|
||||
verse itself, since that needs UI TajikGanjoor doesn't have; it only
|
||||
does the highlighting.
|
||||
|
||||
Verse elements are counted in document order across .m1, .m2 (the two
|
||||
halves of a couplet), .b2 > p (rubaʼi-style already-centered couplets),
|
||||
.n and .l (paragraph/single-line verses) - same shape PrepareHtmlText
|
||||
produces for both the Persian and Tajik sites, so the sync XML's
|
||||
VerseOrder values line up the same way. */
|
||||
(function () {
|
||||
function getVerseElements() {
|
||||
return Array.prototype.slice.call(
|
||||
document.querySelectorAll('.m1, .m2, .n, .l, .b2 > p')
|
||||
);
|
||||
}
|
||||
|
||||
function setActiveVerse(elements, index, previousIndex) {
|
||||
if (previousIndex >= 0 && previousIndex < elements.length && previousIndex !== index) {
|
||||
elements[previousIndex].classList.remove('tg-verse-active');
|
||||
}
|
||||
if (index >= 0 && index < elements.length) {
|
||||
elements[index].classList.add('tg-verse-active');
|
||||
if (elements[index].scrollIntoView) {
|
||||
elements[index].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.tgInitRecitationSync = function (audioEl, xmlUrl) {
|
||||
var verseStart = [];
|
||||
var verseEnd = [];
|
||||
var verseIndex = [];
|
||||
var vCount = 0;
|
||||
var lastHighlight = -1;
|
||||
var verseElements = null;
|
||||
|
||||
fetch(xmlUrl)
|
||||
.then(function (res) { return res.text(); })
|
||||
.then(function (text) {
|
||||
var xml = new window.DOMParser().parseFromString(text, 'text/xml');
|
||||
var bugFixNode = xml.querySelector('OneSecondBugFix');
|
||||
var oneSecondBugFix = bugFixNode ? parseInt(bugFixNode.textContent, 10) : 2000;
|
||||
|
||||
var syncNodes = xml.querySelectorAll('SyncInfo');
|
||||
var v = 0;
|
||||
syncNodes.forEach(function (node) {
|
||||
var msNode = node.querySelector('AudioMiliseconds');
|
||||
var orderNode = node.querySelector('VerseOrder');
|
||||
if (!msNode || !orderNode) return;
|
||||
verseStart[v] = parseInt(msNode.textContent, 10) / oneSecondBugFix;
|
||||
verseIndex[v] = parseInt(orderNode.textContent, 10);
|
||||
if (v > 0) verseEnd[v - 1] = verseStart[v];
|
||||
v++;
|
||||
});
|
||||
v--;
|
||||
if (v > 1) verseEnd[v] = verseStart[v] + 2 * (verseEnd[v - 1] - verseStart[v - 1]);
|
||||
vCount = v;
|
||||
verseElements = getVerseElements();
|
||||
})
|
||||
.catch(function () { /* sync data unavailable - audio still plays normally without highlighting */ });
|
||||
|
||||
audioEl.addEventListener('timeupdate', function () {
|
||||
if (!verseElements) return;
|
||||
var currentTime = audioEl.currentTime;
|
||||
if (currentTime <= 0) return;
|
||||
for (var i = 0; i <= vCount; i++) {
|
||||
if (currentTime >= verseStart[i] && currentTime <= verseEnd[i]) {
|
||||
if (verseIndex[i] !== lastHighlight) {
|
||||
setActiveVerse(verseElements, verseIndex[i], lastHighlight);
|
||||
lastHighlight = verseIndex[i];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
audioEl.addEventListener('play', function () {
|
||||
document.querySelectorAll('audio').forEach(function (other) {
|
||||
if (other !== audioEl) other.pause();
|
||||
});
|
||||
});
|
||||
|
||||
audioEl.addEventListener('ended', function () {
|
||||
setActiveVerse(verseElements || [], -1, lastHighlight);
|
||||
lastHighlight = -1;
|
||||
});
|
||||
};
|
||||
})();
|
||||
Loading…
Reference in New Issue
Block a user