Jump to content

MediaWiki:Gadget-Tci.js

Find traditional instrumental music

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
(function ($, mw, OO) {
    "use strict";

    // Carica i moduli OOUI necessari
    mw.loader.using(["oojs-ui-core", "oojs-ui-widgets", "oojs-ui-windows"]).done(function () {
        
        // Mappa per conversione tra note e semitoni (usando notazione con #)
        const noteSemitones = {
            "C": 0, "C#": 1, "D": 2, "D#": 3, "E": 4, "F": 5,
            "F#": 6, "G": 7, "G#": 8, "A": 9, "A#": 10, "B": 11
        };

        // Dato un valore intero (0-11), restituisce la nota in forma di stringa, sempre con # (no flat)
        function semitoneToNote(s) {
            const notesSharp = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
            return notesSharp[s % 12];
        }

        // Costruisce la scala maggiore (Ionian) a partire da una nota di base.
        function buildRelativeScale(manualKey) {
            let key = manualKey.trim();
            key = key.replace(/[,']/g, "");
            let keyUpper = key.toUpperCase();
            let baseSemitone = noteSemitones[keyUpper];
            if (baseSemitone === undefined) {
                console.log("buildRelativeScale - Key non riconosciuta:", manualKey);
                return null;
            }
            let intervals = [0, 2, 4, 5, 7, 9, 11];
            return intervals.map(i => semitoneToNote(baseSemitone + i));
        }

        const majorScales = { /* ... come prima ... */ };
        const minorScales = { /* ... come prima ... */ };
        const parallelMajors = { /* ... come prima ... */ };
        const defaultOctaveMapping = { /* ... come prima ... */ };

        function cleanABC(text) {
            return text.replace(/\([^)]*\)/g, '')
                       .replace(/>/g, '')
                       .replace(/</g, '');
        }

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

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

        function parseDuration(durStr, defaultDuration) {
            if (!durStr) return defaultDuration;
            if (durStr.includes('/')) {
                let [n, d] = durStr.split('/').map(x => x === '' ? 1 : parseFloat(x));
                return (n / d) * defaultDuration;
            }
            return parseFloat(durStr) * defaultDuration;
        }

        function convertNoteToTCICode(noteToken, scale, keyRaw, octaveMode, manualMode, manualKey, manualScale, isKeynoteToken) {
            if (!manualMode) {
                const regex = /^((?:=|\^|_)*)([a-gA-G])([',]*)$/;
                let m = regex.exec(noteToken);
                if (!m) return '?';
                let [_, accidental, noteLetter, octaveMarkers] = m;
                let acc = accidental.replace(/=/g, '').replace(/\^/g, '#').replace(/_/g, 'b');
                let normalized = noteLetter.toUpperCase() + (acc || '');
                let idx = scale.findIndex(n => n.toUpperCase() === normalized);
                if (idx < 0) return '?';
                let code = String(idx + 1);
                if (octaveMode === 'shiftUp') code += 'H';
                else if (octaveMode === 'shiftDown') code += 'L';
                else if (octaveMode === 'visual') {
                    if (/[a-z]/.test(noteLetter)) code += 'H';
                    else if (octaveMarkers.includes(',')) code += 'L';
                } else {
                    let keyNorm = normalizeKey(keyRaw);
                    let suffix = defaultOctaveMapping[keyNorm]?.[normalized] || '';
                    code += suffix;
                }
                return code;
            } else {
                const regex = /^((?:=|\^|_)*)([a-gA-G])([',]*)$/;
                let m = regex.exec(noteToken);
                if (!m) return '?';
                let normalized = m[2].toUpperCase() + (m[1].replace(/=/g, '').replace(/\^/g, '#').replace(/_/g, 'b') || '');
                let idx = manualScale.indexOf(normalized);
                if (idx < 0) return '?';
                let code = String(idx + 1);
                if (isKeynoteToken) code += (manualKey === manualKey.toLowerCase() ? 'H' : 'L');
                return code;
            }
        }

        function parseABC_timeline(abcText, octaveMode, rhythmOption, manualSampling, manualKeyInput) {
            let lines = cleanABC(abcText).split(/\r?\n/);
            let hdr = { key:'D', meter:'4/4', length:1/8, beatsPerBar:4 };
            lines.forEach(l => {
                if (/^K:/.test(l)) hdr.key = l.slice(2).trim();
                if (/^M:/.test(l)) {
                    hdr.meter = l.slice(2).trim();
                    hdr.beatsPerBar = parseInt(hdr.meter.split('/')[0], 10);
                }
                if (/^L:/.test(l)) {
                    let [n,d] = l.slice(2).trim().split('/').map(Number);
                    hdr.length = n/d;
                }
            });
            let body = lines.filter(l => !/^[A-Za-z]:/.test(l)).join(' ');
            let scale = manualKeyInput.trim() ?
                        buildRelativeScale(manualKeyInput) :
                        getScaleForKey(hdr.key);
            let tokens = [];
            let time = 0;
            const regex = /((?:=|\^|_)?[a-gA-GzZ][',]*)(\d*\/?\d*)/g;
            let match;
            while ((match = regex.exec(body)) !== null) {
                let note = match[1], dur = parseDuration(match[2], hdr.length);
                let code = /^[zZ]/.test(note)? '0' :
                    convertNoteToTCICode(note, scale, hdr.key, octaveMode, !!manualKeyInput.trim(), manualKeyInput.trim(), scale, tokens.length===0);
                tokens.push({ code, start: time, dur, note });
                time += dur;
            }
            let units = hdr.length;
            let beats = Array.from({length: hdr.beatsPerBar*2}, (_,i) => i*units);
            let incipit = beats.map(t => {
                let tok = tokens.find(x => t>=x.start && t<t+ x.dur);
                return tok? tok.code : '?';
            });
            let groups = [];
            for (let i=0; i<incipit.length; i+=hdr.beatsPerBar) {
                groups.push(incipit.slice(i, i+hdr.beatsPerBar).join(''));
            }
            return groups.join(' ');
        }

        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: 'Annulla', flags: 'safe' },
            { action: 'calculate', label: 'Calcola TCI', flags: ['primary', 'progressive'] }
        ];

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

            this.abcInput = new OO.ui.MultilineTextInputWidget({ placeholder: 'Incolla qui la notazione ABC...', autosize: true, rows: 10 });
            this.manualKeyInput = new OO.ui.TextInputWidget({ placeholder: 'Inserisci Keynote Manuale (es. d) (opzionale)' });
            this.octaveChoice = new OO.ui.DropdownInputWidget({ options: [
                { data: 'standard', label: 'Standard (usa mappatura predefinita)' },
                { data: 'visual',   label: 'Basato sulla notazione visiva ABC' },
                { data: 'shiftUp',   label: 'Forza ottava alta (+1)' },
                { data: 'shiftDown', label: 'Forza ottava bassa (-1)' }
            ]});
            this.rhythmChoice = new OO.ui.DropdownInputWidget({ options: [
                { data: '2beats', label: 'Standard 2/4 (2 battiti per misura)' },
                { data: '4beats', label: 'Ricalcola come cut time (4 battiti per misura)' }
            ]});
            this.manualSamplingInput = new OO.ui.TextInputWidget({ placeholder: 'Es.: 0.8, 2.4, 4.0, ... (opzionale)' });
            this.resultOutput = new OO.ui.MultilineTextInputWidget({ readOnly: true, autosize: true, rows: 4 });
            this.debugOutput  = new OO.ui.MultilineTextInputWidget({ readOnly: true, autosize: true, rows: 6 });

            let fieldset = new OO.ui.FieldsetLayout({ label: 'Opzioni Calcolatore TCI', items: [
                new OO.ui.FieldLayout(this.abcInput, { label: 'Notazione ABC', align: 'top' }),
                new OO.ui.FieldLayout(this.manualKeyInput, { label: 'Keynote Manuale', align: 'top' }),
                new OO.ui.FieldLayout(this.octaveChoice, { label: 'Interpretazione Ottava', align: 'top' }),
                new OO.ui.FieldLayout(this.rhythmChoice, { label: 'Interpretazione Ritmica (per 2/4)', align: 'top' }),
                new OO.ui.FieldLayout(this.manualSamplingInput, { label: 'Campionamento Manuale (unitĂ )', align: 'top' }),
                new OO.ui.FieldLayout(this.resultOutput, { label: 'Theme Code Index (TCI)', align: 'top' }),
                new OO.ui.FieldLayout(this.debugOutput,  { label: 'Debug Info', align: 'top' })
            ]});
            this.content.$element.append(fieldset.$element);
        };

        TCICalculatorDialog.prototype.getActionProcess = function (action) {
            if (action === 'calculate') {
                return new OO.ui.Process(() => {
                    try {
                        let tci = parseABC_timeline(
                            this.abcInput.getValue(),
                            this.octaveChoice.getValue(),
                            this.rhythmChoice.getValue(),
                            this.manualSamplingInput.getValue(),
                            this.manualKeyInput.getValue()
                        );
                        this.resultOutput.setValue(tci);
                        this.debugOutput.setValue('Controlla la console per dettagli.');
                    } catch (e) {
                        this.resultOutput.setValue('Errore: ' + e);
                        this.debugOutput.setValue('Verifica notazione ABC. Errore: ' + e);
                    }
                });
            } else if (action === 'cancel') {
                return new OO.ui.Process(() => this.close({ action: action }));
            }
            return TCICalculatorDialog.super.prototype.getActionProcess.call(this, action);
        };

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

        function addTCICalculatorLink() {
            const ul = $('#p-cactions ul');
            if (ul.length && !$('#tci-calculator').length) {
                ul.append($('<li><a href="#" id="tci-calculator">🎼 Calcola 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.