Jump to content

MediaWiki:Gadget-Tci.js: Difference between revisions

Find traditional instrumental music
WikiSysop (talk | contribs)
No edit summary
WikiSysop (talk | contribs)
No edit summary
Line 1: Line 1:
// Gadget: TCI Calculator (final step: one note per beat)
// Gadget: TCI Calculator (beat-accurate: one note per rhythmic beat)
(function ($, mw, OO) {
(function ($, mw, OO) {
     "use strict";
     "use strict";
Line 96: Line 96:
             const meter = meterLine.split(':')[1].trim();
             const meter = meterLine.split(':')[1].trim();
             const [lNum, lDen] = lengthLine.split(':')[1].split('/').map(Number);
             const [lNum, lDen] = lengthLine.split(':')[1].split('/').map(Number);
             const beatUnit = 1 / lDen;
             const beatDuration = lNum / lDen;


             const scale = getScaleForKey(key);
             const scale = getScaleForKey(key);
Line 104: Line 104:
             const tokenRegex = /((=|\^|_)?[a-gA-GzZ][',/]*)(\d*\/?\d*)/g;
             const tokenRegex = /((=|\^|_)?[a-gA-GzZ][',/]*)(\d*\/?\d*)/g;
             let match;
             let match;
             let b4 = 0, b2 = 0;
             let beatAcc = 0, beatAlt = 0, sum4 = 0, sum2 = 0;


             function parseDuration(str) {
             function parseDuration(str) {
                 if (!str) return 1;
                 if (!str) return beatDuration;
                 if (str.includes('/')) {
                 if (str.includes('/')) {
                     const [n, d] = str.split('/').map(Number);
                     const [n, d] = str.split('/').map(Number);
Line 118: Line 118:
                 const baseRaw = normalizeNote(note.replace(/[',0-9\/]/g, ''));
                 const baseRaw = normalizeNote(note.replace(/[',0-9\/]/g, ''));
                 const base = baseRaw.toUpperCase();
                 const base = baseRaw.toUpperCase();
                 const refIdx = refScale.findIndex(n => n.replace(/[#b]/g, '') === base);
                 const refIdx = refScale.findIndex(n => n.replace(/[#b]/g, '') === base);
                 const idxInMinor = scale.findIndex(n => n.replace(/[#b]/g, '') === base);
                 const idxInMinor = scale.findIndex(n => n.replace(/[#b]/g, '') === base);


                 let c = refIdx !== -1 ? (refIdx + 1).toString() : '?';
                 let c = refIdx !== -1 ? (refIdx + 1).toString() : '?';
                 if (refIdx !== -1 && (idxInMinor === -1 || scale[idxInMinor] !== refScale[refIdx])) {
                 if (refIdx !== -1 && (idxInMinor === -1 || scale[idxInMinor] !== refScale[refIdx])) {
                     if (refScale[refIdx].includes('#') && (!scale[idxInMinor] || !scale[idxInMinor].includes('#'))) {
                     if (refScale[refIdx].includes('#') && (!scale[idxInMinor] || !scale[idxInMinor].includes('#'))) c += 'b';
                        c += 'b';
                     else if (refScale[refIdx].includes('b') && (!scale[idxInMinor] || !scale[idxInMinor].includes('b'))) c += '#';
                     } else if (refScale[refIdx].includes('b') && (!scale[idxInMinor] || !scale[idxInMinor].includes('b'))) {
                        c += '#';
                    }
                 }
                 }


Line 141: Line 136:
             }
             }


             while ((match = tokenRegex.exec(abcText)) !== null && (b4 < 8 || b2 < 4)) {
             while ((match = tokenRegex.exec(abcText)) !== null && (beatAcc < 8 || beatAlt < 4)) {
                 const [full, note, , durStr] = match;
                 const [full, note, , durStr] = match;
                 const dur = parseDuration(durStr || `${lNum}/${lDen}`);
                 const dur = parseDuration(durStr || `${lNum}/${lDen}`);
                const val = /z/i.test(note) ? '0' : code(note);
                 const plain = note.replace(/\d.*$/, '');
                 const plain = note.replace(/\d.*$/, '');
                const val = /z/i.test(note) ? '0' : code(note);


                 if (b4 < 8) {
                 if (beatAcc < 8 && sum4 < 1) {
                     beats4.push(val);
                     beats4.push(val);
                     debug4.push(`B${b4+1}: ${plain} → ${val}`);
                     debug4.push(`B${beatAcc + 1}: ${plain} → ${val}`);
                     b4++;
                     beatAcc++;
                    sum4 += dur;
                } else if (sum4 >= 1) {
                    sum4 = dur;
                    if (beatAcc < 8) {
                        beats4.push(val);
                        debug4.push(`B${beatAcc + 1}: ${plain} → ${val}`);
                        beatAcc++;
                    }
                } else {
                    sum4 += dur;
                 }
                 }


                 if (b2 < 4) {
                 if (beatAlt < 4 && sum2 < 2) {
                     beats2.push(val);
                     beats2.push(val);
                     debug2.push(`B${b2+1}: ${plain} → ${val}`);
                     debug2.push(`B${beatAlt + 1}: ${plain} → ${val}`);
                     b2++;
                     beatAlt++;
                    sum2 += dur;
                } else if (sum2 >= 2) {
                    sum2 = dur;
                    if (beatAlt < 4) {
                        beats2.push(val);
                        debug2.push(`B${beatAlt + 1}: ${plain} → ${val}`);
                        beatAlt++;
                    }
                } else {
                    sum2 += dur;
                 }
                 }
             }
             }

Revision as of 17:07, 10 April 2025

// Gadget: TCI Calculator (beat-accurate: one note per rhythmic beat)
(function ($, mw, OO) {
    "use strict";

    mw.loader.using(["oojs-ui-core", "oojs-ui-widgets", "oojs-ui-windows"]).done(function () {
        function TCICalculatorDialog(config) {
            TCICalculatorDialog.super.call(this, config);
        }
        OO.inheritClass(TCICalculatorDialog, OO.ui.ProcessDialog);

        TCICalculatorDialog.static.name = 'tciCalculatorDialog';
        TCICalculatorDialog.static.title = 'Theme Code Index Calculator';
        TCICalculatorDialog.static.actions = [
            { action: 'cancel', label: 'Cancel', flags: 'safe' },
            { action: 'calculate', label: 'Calculate TCI', flags: ['primary', 'progressive'] }
        ];

        TCICalculatorDialog.prototype.initialize = function () {
            TCICalculatorDialog.super.prototype.initialize.call(this);
            this.content = new OO.ui.PanelLayout({ padded: true, expanded: false, scrollable: true });
            this.$body.append(this.content.$element);

            this.abcInput = new OO.ui.MultilineTextInputWidget({ placeholder: 'Paste ABC notation here...', autosize: true, rows: 10 });

            this.octaveChoice = new OO.ui.DropdownInputWidget({
                options: [
                    { data: 'standard', label: 'Standard fiddle range (default)' },
                    { data: 'visual', label: 'Based on ABC visual octave' },
                    { data: 'shiftUp', label: 'Force high octave (+1)' },
                    { data: 'shiftDown', label: 'Force low octave (-1)' }
                ]
            });

            this.resultOutput = new OO.ui.MultilineTextInputWidget({ readOnly: true, autosize: true, rows: 4 });

            this.content.$element.append(
                new OO.ui.FieldsetLayout({
                    label: 'TCI Calculator Options',
                    items: [
                        new OO.ui.FieldLayout(this.abcInput, { label: 'ABC Notation', align: 'top' }),
                        new OO.ui.FieldLayout(this.octaveChoice, { label: 'Octave Interpretation', align: 'top' }),
                        new OO.ui.FieldLayout(this.resultOutput, { label: 'Theme Code Index (TCI)', align: 'top' })
                    ]
                }).$element
            );
        };

        const majorScales = {
            C: ['C', 'D', 'E', 'F', 'G', 'A', 'B'],
            G: ['G', 'A', 'B', 'C', 'D', 'E', 'F#'],
            D: ['D', 'E', 'F#', 'G', 'A', 'B', 'C#'],
            A: ['A', 'B', 'C#', 'D', 'E', 'F#', 'G#'],
            E: ['E', 'F#', 'G#', 'A', 'B', 'C#', 'D#'],
            B: ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A#'],
            F: ['F', 'G', 'A', 'Bb', 'C', 'D', 'E']
        };

        const minorScales = {
            Am: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
            Em: ['E', 'F#', 'G', 'A', 'B', 'C', 'D'],
            Dm: ['D', 'E', 'F', 'G', 'A', 'Bb', 'C'],
            Bm: ['B', 'C#', 'D', 'E', 'F#', 'G', 'A'],
            Gm: ['G', 'A', 'Bb', 'C', 'D', 'Eb', 'F'],
            Fsm: ['F#', 'G#', 'A', 'B', 'C#', 'D', 'E']
        };

        const parallelMajors = {
            Em: 'E', Am: 'A', Dm: 'D', Bm: 'B', Gm: 'G', Fsm: 'F#'
        };

        function normalizeKey(key) {
            return key.replace(/[^a-zA-Z#bm]/g, '').trim();
        }

        function getScaleForKey(keyRaw) {
            const key = normalizeKey(keyRaw);
            return majorScales[key] || minorScales[key] || majorScales['D'];
        }

        function getComparativeMajorScale(keyRaw) {
            const key = normalizeKey(keyRaw);
            return majorScales[parallelMajors[key]] || majorScales['D'];
        }

        function normalizeNote(note) {
            return note.replace(/=/g, '').replace(/\^/g, '#').replace(/_/g, 'b');
        }

        function parseABC(abcText, octaveMode) {
            const lines = abcText.split(/\r?\n/);
            const keyLine = lines.find(line => line.startsWith('K:')) || 'K:D';
            const meterLine = lines.find(line => line.startsWith('M:')) || 'M:4/4';
            const lengthLine = lines.find(line => line.startsWith('L:')) || 'L:1/8';

            const key = keyLine.split(':')[1].trim();
            const meter = meterLine.split(':')[1].trim();
            const [lNum, lDen] = lengthLine.split(':')[1].split('/').map(Number);
            const beatDuration = lNum / lDen;

            const scale = getScaleForKey(key);
            const refScale = getComparativeMajorScale(key) || scale;
            const beats4 = [], beats2 = [], debug4 = [], debug2 = [];

            const tokenRegex = /((=|\^|_)?[a-gA-GzZ][',/]*)(\d*\/?\d*)/g;
            let match;
            let beatAcc = 0, beatAlt = 0, sum4 = 0, sum2 = 0;

            function parseDuration(str) {
                if (!str) return beatDuration;
                if (str.includes('/')) {
                    const [n, d] = str.split('/').map(Number);
                    return n / d;
                }
                return parseFloat(str);
            }

            function code(note) {
                const baseRaw = normalizeNote(note.replace(/[',0-9\/]/g, ''));
                const base = baseRaw.toUpperCase();
                const refIdx = refScale.findIndex(n => n.replace(/[#b]/g, '') === base);
                const idxInMinor = scale.findIndex(n => n.replace(/[#b]/g, '') === base);

                let c = refIdx !== -1 ? (refIdx + 1).toString() : '?';
                if (refIdx !== -1 && (idxInMinor === -1 || scale[idxInMinor] !== refScale[refIdx])) {
                    if (refScale[refIdx].includes('#') && (!scale[idxInMinor] || !scale[idxInMinor].includes('#'))) c += 'b';
                    else if (refScale[refIdx].includes('b') && (!scale[idxInMinor] || !scale[idxInMinor].includes('b'))) c += '#';
                }

                const isHigh = /[a-g]/.test(note);
                const isLow = /,/.test(note);
                if (octaveMode === 'shiftUp') return c + 'H';
                if (octaveMode === 'shiftDown') return c + 'L';
                if (octaveMode === 'visual') return isHigh ? c + 'H' : isLow ? c + 'L' : c;
                if (octaveMode === 'standard') return isHigh ? c + 'H' : c;
                return c;
            }

            while ((match = tokenRegex.exec(abcText)) !== null && (beatAcc < 8 || beatAlt < 4)) {
                const [full, note, , durStr] = match;
                const dur = parseDuration(durStr || `${lNum}/${lDen}`);
                const val = /z/i.test(note) ? '0' : code(note);
                const plain = note.replace(/\d.*$/, '');

                if (beatAcc < 8 && sum4 < 1) {
                    beats4.push(val);
                    debug4.push(`B${beatAcc + 1}: ${plain}${val}`);
                    beatAcc++;
                    sum4 += dur;
                } else if (sum4 >= 1) {
                    sum4 = dur;
                    if (beatAcc < 8) {
                        beats4.push(val);
                        debug4.push(`B${beatAcc + 1}: ${plain}${val}`);
                        beatAcc++;
                    }
                } else {
                    sum4 += dur;
                }

                if (beatAlt < 4 && sum2 < 2) {
                    beats2.push(val);
                    debug2.push(`B${beatAlt + 1}: ${plain}${val}`);
                    beatAlt++;
                    sum2 += dur;
                } else if (sum2 >= 2) {
                    sum2 = dur;
                    if (beatAlt < 4) {
                        beats2.push(val);
                        debug2.push(`B${beatAlt + 1}: ${plain}${val}`);
                        beatAlt++;
                    }
                } else {
                    sum2 += dur;
                }
            }

            return {
                normal: beats4.join(' '),
                dual: beats2.join(' '),
                debug: '4-beat mode:\n' + debug4.join('\n') + '\n\n2-beat mode:\n' + debug2.join('\n')
            };
        }

        TCICalculatorDialog.prototype.getActionProcess = function (action) {
            if (action === 'calculate') {
                return new OO.ui.Process(() => {
                    const abc = this.abcInput.getValue();
                    const mode = this.octaveChoice.getValue();
                    const result = parseABC(abc, mode);
                    this.resultOutput.setValue(
                        'Standard (4 beats): ' + result.normal +
                        '\nAlternate (2 beats): ' + result.dual +
                        '\n\n' + result.debug
                    );
                });
            } else if (action === 'cancel') {
                return new OO.ui.Process(() => this.close({ action: action }));
            }
            return TCICalculatorDialog.super.prototype.getActionProcess.call(this, action);
        };

        function openTCICalculatorDialog() {
            const windowManager = new OO.ui.WindowManager();
            $(document.body).append(windowManager.$element);
            const dialog = new TCICalculatorDialog();
            windowManager.addWindows([dialog]);
            windowManager.openWindow(dialog);
        }

        function addTCICalculatorLink() {
            const $actions = $('#p-cactions ul');
            if ($actions.length && !$('#tci-calculator').length) {
                $actions.append($('<li><a href="#" id="tci-calculator">🎼 Calculate TCI</a></li>'));
            }
        }

        $(document).on('click', '#tci-calculator', function (e) {
            e.preventDefault();
            openTCICalculatorDialog();
        });

        $(addTCICalculatorLink);
    });
})(jQuery, mediaWiki, OO);
Cookies help us deliver our services. By using The Traditional Tune Archive services, you agree to our use of cookies.