contrabass finger practice

Author: Hatsuka2030Created Jan 13, 2026Updated Jan 13, 2026
コントラバス自動運指(多重オクターブガイド線版)
MusicXMLファイルを選択してください
<script>
    let osmd = null;

    document.getElementById('file-input').onchange = async (e) => {
        const file = e.target.files[0];
        if (!file) return;
        const reader = new FileReader();
        reader.onload = async (ev) => {
            if (!osmd) {
                osmd = new opensheetmusicdisplay.OpenSheetMusicDisplay("score-container", {
                    autoResize: true, backend: "svg", drawFingerings: false
                });
            }
            await osmd.load(ev.target.result);
            osmd.render();
            setTimeout(() => {
                document.getElementById('finger-layer').style.height = document.getElementById('score-container').offsetHeight + "px";
                document.getElementById('status').innerText = "準備完了。";
            }, 1000);
        };
        reader.readAsText(file);
    };

    document.getElementById('scan-btn').onclick = function() {
        const score = document.getElementById('score-container');
        const svgLayer = document.getElementById('finger-layer');
        const scoreRect = score.getBoundingClientRect();
        const viewTop = window.scrollY;
        const viewBottom = viewTop + window.innerHeight;

        svgLayer.innerHTML = ""; 

        const allPaths = Array.from(score.querySelectorAll("path"));
        const allGraphics = Array.from(score.querySelectorAll("path, rect"))
            .sort((a, b) => a.getBoundingClientRect().left - b.getBoundingClientRect().left);

        const clefs = allPaths.filter(p => {
            const r = p.getBoundingClientRect();
            return r.height > 20 && r.height < 70 && r.width > 15 && r.width < 55;
        }).map(c => {
            const r = c.getBoundingClientRect();
            return { top: r.top + window.scrollY, height: r.height, left: r.left };
        }).filter((clef, index, self) => 
            index === self.findIndex((t) => Math.abs(t.top - clef.top) < 5)
        );

        const heads = Array.from(score.querySelectorAll(".vf-notehead, ellipse, path")).filter(el => {
            const r = el.getBoundingClientRect();
            const isCorrectSize = r.width > 8 && r.width < 22 && r.height > 5 && r.height < 18;
            const isNoteClass = el.classList.contains('vf-notehead') || el.tagName === 'ellipse';
            const isInsideNote = el.closest('.vf-note') !== null;
            return isCorrectSize && (isNoteClass || isInsideNote);
        });

        const accidentals = allPaths.filter(p => {
            const r = p.getBoundingClientRect();
            const isAccSize = r.height > 12 && r.height < 45 && r.width > 4 && r.width < 25;
            if (!isAccSize) return false;
            const ratio = r.height / r.width;
            if (ratio > 3.36 && ratio < 3.42) return false; 
            const pTop = r.top + window.scrollY;
            let isOverlapNoteX = false;
            const targetClef = clefs.reduce((prev, curr) => 
                Math.abs(curr.top - pTop) < Math.abs(prev.top - pTop) ? curr : prev
            , clefs[0]);
            if (targetClef) {
                heads.forEach(h => {
                    const hRect = h.getBoundingClientRect();
                    const hTop = hRect.top + window.scrollY;
                    const isSameStaff = Math.abs(hTop - targetClef.top) < 60;
                    if (isSameStaff) {
                        const overlapX = r.left < hRect.right - 1 && r.right > hRect.left + 1;
                        if (overlapX) isOverlapNoteX = true;
                    }
                });
            }
            return !isOverlapNoteX; 
        });

        const activeAccidentals = [];
        let accidentalCounter = 1;

        // --- ダブルシャープの検出(グループ化された要素を探す) ---
        const allGroups = Array.from(score.querySelectorAll("g"));
        const doubleSharpCandidates = [];
        
        allGroups.forEach(g => {
            const childPaths = Array.from(g.querySelectorAll("path"));
            if (childPaths.length >= 2 && childPaths.length <= 4) {
                const gRect = g.getBoundingClientRect();
                if (gRect.width > 8 && gRect.width < 25 && gRect.height > 8 && gRect.height < 25) {
                    const ratio = gRect.height / gRect.width;
                    // ダブルシャープは正方形に近い(比率0.8〜1.2程度)
                    if (ratio > 0.8 && ratio < 1.3) {
                        doubleSharpCandidates.push({
                            element: g,
                            rect: gRect,
                            type: "doublesharp"
                        });
                        
                        // デバッグ表示
                        const debugBox = document.createElementNS("http://www.w3.org/2000/svg", "rect");
                        debugBox.setAttribute("x", gRect.left - scoreRect.left);
                        debugBox.setAttribute("y", gRect.top - scoreRect.top);
                        debugBox.setAttribute("width", gRect.width);
                        debugBox.setAttribute("height", gRect.height);
                        debugBox.setAttribute("fill", "none");
                        debugBox.setAttribute("stroke", "orange");
                        debugBox.setAttribute("stroke-width", "2");
                        svgLayer.appendChild(debugBox);
                        
                        const ratioTxt = document.createElementNS("http://www.w3.org/2000/svg", "text");
                        ratioTxt.setAttribute("x", gRect.left - scoreRect.left);
                        ratioTxt.setAttribute("y", gRect.top - scoreRect.top - 2);
                        ratioTxt.setAttribute("fill", "orange");
                        ratioTxt.setAttribute("font-size", "10px");
                        ratioTxt.textContent = `DS比:${ratio.toFixed(2)}`;
                        svgLayer.appendChild(ratioTxt);
                    }
                }
            }
        });

        // 1. 初期の運指計算(臨時記号優先)
        heads.forEach(head => {
            const r = head.getBoundingClientRect();
            const noteTop = r.top + window.scrollY;
            if (noteTop < viewTop || noteTop > viewBottom) return;
            const myClef = clefs.reduce((prev, curr) => Math.abs(curr.top - noteTop) < Math.abs(prev.top - noteTop) ? curr : prev, clefs[0]);
            if (!myClef) return;

            let accType = "none";
            let searchAreaH = 12; 
            let searchAreaY = (r.top + (r.height / 2)) - (searchAreaH / 2) - scoreRect.top;
            
            accidentals.forEach(acc => {
                const aRect = acc.getBoundingClientRect();
                const isLeftNear = (r.left - aRect.right > -2) && (r.left - aRect.right < 10);
                const accCenterY = aRect.top - scoreRect.top + (aRect.height / 2);
                const isNearY = Math.abs(accCenterY - (searchAreaY + searchAreaH/2)) < (searchAreaH / 2 + 2);
                
                if (isLeftNear && isNearY) {
                    const ratio = aRect.height / aRect.width;
                    let currentAcc = "none";
                    
                    // ダブルシャープチェック(音符の近くにダブルシャープ候補があるか)
                    const nearDoubleSharp = doubleSharpCandidates.find(ds => {
                        const dsRect = ds.rect;
                        const isNearX = Math.abs(dsRect.left - aRect.left) < 3;
                        const isNearY = Math.abs(dsRect.top - aRect.top) < 3;
                        return isNearX && isNearY;
                    });
                    
                    if (nearDoubleSharp) currentAcc = "doublesharp";
                    else if (ratio >= 3.45) currentAcc = "natural"; 
                    else if (ratio >= 3.25 && ratio <= 3.44) currentAcc = "sharp";
                    else if (ratio >= 2.8 && ratio <= 3.24) currentAcc = "flat";

                    if (currentAcc !== "none") {
                        console.log("Detected accidental:", currentAcc, "at", aRect.left); // デバッグ用
                        let barlineX = aRect.right + 800;
                        allGraphics.forEach(p => {
                            const pRect = p.getBoundingClientRect();
                            const isVertical = pRect.height > 30 && pRect.width < 4;
                            const isSameStaff = Math.abs(pRect.top + window.scrollY - myClef.top) < 120;
                            if (isVertical && isSameStaff && pRect.left > aRect.right && pRect.left < barlineX) {
                                let isStem = false;
                                heads.forEach(h => {
                                    const hRect = h.getBoundingClientRect();
                                    const isOverlapX = pRect.left >= hRect.left - 2 && pRect.left <= hRect.right + 2;
                                    const isOverlapY = hRect.bottom > pRect.top - 5 && hRect.top < pRect.bottom + 5;
                                    if (isOverlapX && isOverlapY) isStem = true;
                                });
                                if (!isStem) barlineX = pRect.left;
                            }
                        });

                        activeAccidentals.push({
                            type: currentAcc, 
                            left: aRect.left, 
                            right: barlineX, 
                            top: r.top, 
                            id: accidentalCounter
                        });

                        const accNumTxt = document.createElementNS("http://www.w3.org/2000/svg", "text");
                        accNumTxt.setAttribute("x", aRect.left - scoreRect.left - 5); 
                        accNumTxt.setAttribute("y", aRect.top - scoreRect.top - 5);
                        accNumTxt.setAttribute("fill", "#000");
                        accNumTxt.setAttribute("font-size", "14px");
                        accNumTxt.setAttribute("font-weight", "bold");
                        accNumTxt.textContent = accidentalCounter;
                        svgLayer.appendChild(accNumTxt);

                        accidentalCounter++;
                    }
                }
            });

            const overlappingEffects = activeAccidentals.filter(a => r.left >= a.left && r.left <= a.right && Math.abs(r.top - a.top) < 2);
            if (overlappingEffects.length > 0) accType = overlappingEffects.reduce((prev, curr) => (curr.left > prev.left) ? curr : prev).type;

            const baseRatio = (noteTop - myClef.top) / myClef.height;
            let finger = "?", color = "#000000";
            if (baseRatio < -0.80) finger = "10"; else if (baseRatio < -0.65) finger = "9"; 
            else if (baseRatio < -0.50) finger = "7"; else if (baseRatio < -0.35) finger = "5"; else if (baseRatio < -0.20) finger = "4"; else if (baseRatio < -0.05) finger = "2"; else if (baseRatio < 0.10) finger = "0"; else if (baseRatio < 0.28) finger = "3"; else if (baseRatio < 0.38) finger = "2"; else if (baseRatio < 0.48) finger = "0"; else if (baseRatio < 0.78) finger = "3"; else if (baseRatio < 0.88) finger = "2"; else if (baseRatio < 0.98) finger = "0"; else if (baseRatio < 1.18) finger = "3"; else if (baseRatio < 1.38) finger = "1"; else finger = "0";
            
            if (baseRatio < 0.10) color = "#2ecc71"; else if (baseRatio < 0.48) color = "#3498db"; else if (baseRatio < 0.98) color = "#e74c3c";

            if (finger !== "?" && !isNaN(finger)) {
                let fNum = parseInt(finger);
                if (accType === "sharp") fNum += 1;
                else if (accType === "flat") { 
                    if (fNum === 0) { 
                        finger = "4"; 
                        if (color === "#2ecc71") color = "#3498db"; else if (color === "#3498db") color = "#e74c3c"; else if (color === "#e74c3c") color = "#000000"; 
                    } else fNum -= 1; 
                }
                // --- [加筆] ダブルフラットの運指計算(-2) ---
                else if (accType === "doubleflat") {
                    if (fNum === 0) {
                        // 開放弦から2つ下がるケース
                        finger = "3";
                        if (color === "#2ecc71") color = "#3498db"; 
                        else if (color === "#3498db") color = "#e74c3c";
                        else if (color === "#e74c3c") color = "#000000";
                    } else if (fNum === 1) {
                        finger = "4";
                        if (color === "#2ecc71") color = "#3498db"; 
                        else if (color === "#3498db") color = "#e74c3c";
                        else if (color === "#e74c3c") color = "#000000";
                    } else if (fNum >= 2) {
                        fNum -= 2;
                        finger = fNum.toString();
                    }
                }
                else if (accType === "doublesharp") {
                    fNum += 2; // 全音上げる
                    finger = fNum.toString();
                }
                if (accType === "natural") { /* ナチュラルの場合は補正なし(baseRatioに従う) */ }
                if (finger !== "4" || accType === "sharp" || accType === "doublesharp") finger = fNum.toString();
            }

            const txt = document.createElementNS("http://www.w3.org/2000/svg", "text");
            txt.setAttribute("x", r.left - scoreRect.left + (r.width / 2)); 
            txt.setAttribute("y", r.top - scoreRect.top + 30); 
            txt.setAttribute("fill", color); 
            txt.setAttribute("font-size", "22px"); 
            txt.setAttribute("font-weight", "bold"); 
            txt.setAttribute("text-anchor", "middle"); 
            txt.setAttribute("class", "finger-label"); 
            txt.textContent = finger;
            svgLayer.appendChild(txt);

            if (overlappingEffects.length > 0) {
                const refAcc = overlappingEffects.reduce((prev, curr) => (curr.left > prev.left) ? curr : prev);
                const refNumTxt = document.createElementNS("http://www.w3.org/2000/svg", "text");
                refNumTxt.setAttribute("x", r.left - scoreRect.left + (r.width / 2) + 12);
                refNumTxt.setAttribute("y", r.top - scoreRect.top + 15);
                refNumTxt.setAttribute("fill", "#7f8c8d");
                refNumTxt.setAttribute("font-size", "10px");
                refNumTxt.textContent = "(" + refAcc.id + ")";
                svgLayer.appendChild(refNumTxt);
            }

            if (accType !== "none") {
                const debugRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
                let strokeColor = "#000";
                if (accType === "doubleflat") strokeColor = "#9b59b6";
                else if (accType === "flat") strokeColor = "#3498db";
                else if (accType === "natural") strokeColor = "#2ecc71";
                else if (accType === "sharp") strokeColor = "#e74c3c";
                debugRect.setAttribute("x", r.left - scoreRect.left - 2);
                debugRect.setAttribute("y", r.top - scoreRect.top - 2);
                debugRect.setAttribute("width", r.width + 4);
                debugRect.setAttribute("height", r.height + 4);
                debugRect.setAttribute("fill", "none");
                debugRect.setAttribute("stroke", strokeColor);
                debugRect.setAttribute("stroke-width", "2");
                svgLayer.appendChild(debugRect);
            }
        });

        activeAccidentals.forEach(area => {
            const areaRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
            let areaColor = "rgba(0, 0, 0, 0.05)";
            if (area.type === "doubleflat") areaColor = "rgba(155, 89, 182, 0.1)";
            else if (area.type === "flat") areaColor = "rgba(52, 152, 219, 0.1)";
            else if (area.type === "natural") areaColor = "rgba(46, 204, 113, 0.1)";
            else if (area.type === "sharp") areaColor = "rgba(231, 76, 60, 0.1)";
            areaRect.setAttribute("x", area.left - scoreRect.left);
            areaRect.setAttribute("y", area.top - scoreRect.top - 10);
            areaRect.setAttribute("width", area.right - area.left);
            areaRect.setAttribute("height", 20);
            areaRect.setAttribute("fill", areaColor);
            areaRect.setAttribute("stroke", areaColor.replace("0.1", "0.5"));
            areaRect.setAttribute("stroke-dasharray", "2,2");
            svgLayer.appendChild(areaRect);
        });

        // 2. 調合判定(オクターブ・ガイド線の描画)
        const extendedKeySigs = []; 
        clefs.forEach(c => {
            const cR = score.querySelectorAll("path")[allPaths.indexOf(allPaths.find(p => p.getBoundingClientRect().left === c.left))].getBoundingClientRect();
            const staffSigs = accidentals.filter(acc => {
                const aR = acc.getBoundingClientRect();
                return aR.left > cR.right - 5 && aR.left < cR.right + 40 && Math.abs(aR.top + window.scrollY - c.top) < 60;
            });
            staffSigs.forEach(sig => {
                const sR = sig.getBoundingClientRect();
                const ratio = sR.height / sR.width;
                let type = (ratio >= 3.25 && ratio <= 3.44) ? "sharp" : (ratio >= 2.8 && ratio <= 3.24) ? "flat" : "none";
                
                if (type !== "none") {
                    let yOffsetRatio = (type === "flat") ? 0.78 : 0.55;
                    let baseAdjustedY = sR.top - scoreRect.top + (sR.height * yOffsetRatio) - 1;
                    const startX = sR.left - scoreRect.le

Source: iGhibli/iOS-DeviceSupport