1/** 2 * TuCuatro Chords â click-to-hear printed diagrams 3 * 4 * Click a .thischord with data-numeric â play fretted pitches for the 5 * active instrument (4-string or 6-string). Inspired by the sync pad 6 * tone engine in tucuatro-songs, but voiced from tuning + fret numbers. 7 * 8 * Skips: muted strings (x), sync chord-pad UI, calculator drawer frets. 9 */ 10(function (window, $) { 11 "use strict"; 12 13 if (!$ || !$.fn) return; 14 15 var CFG = window.TuCuatroChordHear || {}; 16 var TUNINGS = CFG.tunings || {}; 17 18 // Fallback when PHP did not localize. Must match object.chords.js string order 19 // (ukulele = GCEA, cavaquinho = DGBD â not the Instruments PHP arrays). 20 var FALLBACK = { 21 cuatro: { notes: ["A", "D", "F#", "B"], octaves: [3, 4, 4, 3] }, 22 guitar: { notes: ["E", "A", "D", "G", "B", "E"], octaves: [2, 2, 3, 3, 3, 4] }, 23 ukulele: { notes: ["G", "C", "E", "A"], octaves: [4, 4, 4, 4] }, 24 cavaquinho: { notes: ["D", "G", "B", "D"], octaves: [4, 4, 4, 5] }, 25 "bandola-llanera": { notes: ["A", "D", "A", "E"], octaves: [3, 4, 4, 5] }, 26 bandola: { notes: ["A", "D", "A", "E"], octaves: [3, 4, 4, 5] } 27 }; 28 29 var NOTE_TO_SEMI_FROM_A = { 30 A: 0, "A#": 1, Bb: 1, B: 2, 31 C: -9, "C#": -8, Db: -8, D: -7, "D#": -6, Eb: -6, 32 E: -5, F: -4, "F#": -3, Gb: -3, G: -2, "G#": -1, Ab: -1 33 }; 34 35 var audioCtx = null; 36 var playGen = 0; 37 38 function getCtx() { 39 var AC = window.AudioContext || window.webkitAudioContext; 40 if (!AC) return null; 41 if (!audioCtx) audioCtx = new AC(); 42 if (audioCtx.state === "suspended") { 43 try { audioCtx.resume(); } catch (e) {} 44 } 45 return audioCtx; 46 } 47 48 function noteToFreq(note, octave) { 49 var n = String(note || "").trim(); 50 if (n.length > 1 && n.charAt(1) === "b") { 51 // already handled via NOTE_TO_SEMI_FROM_A aliases 52 } 53 var semiFromA = NOTE_TO_SEMI_FROM_A[n]; 54 if (typeof semiFromA === "undefined") return null; 55 var steps = semiFromA + (octave - 4) * 12; 56 return 440 * Math.pow(2, steps / 12); 57 } 58 59 function trim(s) { 60 return String(s == null ? "" : s).replace(/^\s+|\s+$/g, ""); 61 } 62 63 function resolveInstrument($el) { 64 var inst = trim($el.attr("data-instrument") || $el.attr("data-preserveinstrument")); 65 if (!inst) { 66 inst = trim($(".forceinstrument").first().attr("data-instrument")); 67 } 68 if (!inst && window.TuCuatro && window.TuCuatro.Fretboard && typeof window.TuCuatro.Fretboard.detectInstrument === "function") { 69 try { inst = trim(window.TuCuatro.Fretboard.detectInstrument()); } catch (e) { inst = ""; } 70 } 71 if (!inst) { 72 try { inst = trim(localStorage.getItem("selectedInstrument")); } catch (e) { inst = ""; } 73 } 74 if (inst === "bandola") inst = "bandola-llanera"; 75 return inst || "guitar"; 76 } 77 78 function getTuning(instrument) { 79 var t = TUNINGS[instrument] || FALLBACK[instrument] || FALLBACK.guitar; 80 if (!t || !t.notes || !t.notes.length) t = FALLBACK.guitar; 81 return t; 82 } 83 84 function parseNumeric(raw) { 85 var parts = trim(raw).split("."); 86 if (parts.length !== 4 && parts.length !== 6) return null; 87 var frets = []; 88 for (var i = 0; i < parts.length; i++) { 89 var p = trim(parts[i]).toLowerCase(); 90 if (!p || p === "x") { 91 frets.push(null); 92 continue; 93 } 94 var n = parseInt(p, 10); 95 if (isNaN(n) || n < 0) frets.push(null); 96 else frets.push(n); 97 } 98 return frets; 99 } 100 101 function fretsToFreqs(frets, tuning) { 102 var notes = tuning.notes || []; 103 var octaves = tuning.octaves || []; 104 var out = []; 105 var count = Math.min(frets.length, notes.length); 106 for (var i = 0; i < count; i++) { 107 if (frets[i] === null) continue; 108 var open = notes[i];
109 var oct = typeof octaves[i] === "number" ? octaves[i] : 4; 110 var base = noteToFreq(open, oct); 111 if (base == null) continue; 112 out.push(base * Math.pow(2, frets[i] / 12)); 113 } 114 return out; 115 } 116 117 function playFreqs(freqs) { 118 var ctx = getCtx(); 119 if (!ctx || !freqs.length) return; 120 121 playGen += 1; 122 var myGen = playGen; 123 var now = ctx.currentTime; 124 var stagger = typeof CFG.stagger === "number" ? CFG.stagger : 0.04; 125 var dur = typeof CFG.duration === "number" ? CFG.duration : 0.55; 126 var peak = typeof CFG.peak === "number" ? CFG.peak : 0.22; 127 128 var master = ctx.createGain(); 129 master.gain.setValueAtTime(0.0001, now); 130 master.gain.linearRampToValueAtTime(peak, now + 0.02); 131 master.gain.setValueAtTime(peak * 0.55, now + dur); 132 master.gain.linearRampToValueAtTime(0.0001, now + dur + 0.28); 133 master.connect(ctx.destination); 134 135 var filter = ctx.createBiquadFilter(); 136 filter.type = "lowpass"; 137 filter.frequency.value = typeof CFG.lowpassHz === "number" ? CFG.lowpassHz : 2200; 138 filter.Q.value = 0.8; 139 filter.connect(master); 140 141 var voice = Math.min(0.55, 1.1 / Math.max(2, freqs.length)); 142 143 freqs.forEach(function (freq, i) { 144 var t0 = now + i * stagger; 145 var osc = ctx.createOscillator(); 146 var g = ctx.createGain(); 147 osc.type = i === 0 ? "triangle" : "sine"; 148 osc.frequency.value = freq; 149 g.gain.setValueAtTime(0.0001, t0); 150 g.gain.linearRampToValueAtTime(voice, t0 + 0.012); 151 g.gain.linearRampToValueAtTime(voice * 0.7, t0 + 0.12); 152 g.gain.setValueAtTime(voice * 0.55, t0 + dur); 153 g.gain.linearRampToValueAtTime(0.0001, t0 + dur + 0.22); 154 osc.connect(g); 155 g.connect(filter); 156 osc.start(t0); 157 osc.stop(t0 + dur + 0.3); 158 }); 159 160 // Generation stamp reserved for future cancel-on-reclick if needed. 161 void myGen; 162 } 163 164 function shouldIgnore($el, e) { 165 if (CFG.enabled === false) return true; 166 if (!$el || !$el.length) return true; 167 // Sync professor pad / overlay â that UI has its own tone engine. 168 if ($el.closest(".tucuatro-chordpad, #tucuatro-chordpad, .add_chords_overlay, .tc-sync-overlay").length) { 169 return true; 170 } 171 // Calculator interactive frets. 172 if ($el.closest(".chordfinder, .tucuatro-calc-drawer").length) { 173 return true; 174 } 175 // Beatbar uses .tc-chord, not .thischord â but be safe. 176 if ($(e.target).closest(".tc-beatbar, .select_instrument, .option.changeinstrument").length) { 177 return true; 178 } 179 return false; 180 } 181 182 function playFromChordEl($el) { 183 var numeric = trim($el.attr("data-numeric")); 184 if (!numeric) return false; 185 186 var frets = parseNumeric(numeric); 187 if (!frets) return false; 188 189 var instrument = resolveInstrument($el); 190 var tuning = getTuning(instrument); 191 // If numeric string count disagrees with tuning, still play what we can. 192 var freqs = fretsToFreqs(frets, tuning); 193 if (!freqs.length) return false; 194 195 playFreqs(freqs); 196 $el.addClass("tc-sound-playing"); 197 window.setTimeout(function () { 198 $el.removeClass("tc-sound-playing"); 199 }, 350); 200 return true; 201 } 202 203 function bind() { 204 $(document) 205 .off("click.tcChordHear", ".thischord") 206 .on("click.tcChordHear", ".thischord", function (e) { 207 var $el = $(this); 208 if (shouldIgnore($el, e)) return; 209 if (!trim($el.attr("data-numeric"))) return; 210 // Don't steal modifier-clicks / text selection quirks. 211 if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; 212 playFromChordEl($el); 213 }); 214 } 215 216 // Public API for reuse / console tests. 217 window.TuCuatro = window.TuCuatro || {}; 218 window.TuCuatro.ChordHear = { 219 play: playFromChordEl, 220 playNumeric: function (numeric, instrument) { 221 var frets = parseNumeric(numeric); 222 if (!frets) return false; 223 var tuning = getTuning(instrument || "guitar"); 224 var freqs = fretsToFreqs(frets, tuning); 225 if (!freqs.length) return false; 226 playFreqs(freqs); 227 return true; 228 }, 229 resolveInstrument: resolveInstrument 230 }; 231 232 $(bind); 233})(window, window.jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.