@@ -239,18 +239,20 @@ function AIWidget() {
239239 return blocks ;
240240 }
241241
242- //function to search index for particular type of block mainly used to find nammeddo block in repeat block
242+ // Fast index lookup — builds a one-time Map for O(1) repeated searches
243+ // on the same array, with a transparent fallback to linear scan.
244+ const _indexMaps = new WeakMap ( ) ;
243245 function searchIndexForMusicBlock ( array , x ) {
244- // Iterate over each sub-array in the main array
245- for ( let i = 0 ; i < array . length ; i ++ ) {
246- // Check if the 0th element of the sub-array matches x
247- if ( array [ i ] [ 0 ] === x ) {
248- // Return the index if a match is found
249- return i ;
246+ let map = _indexMaps . get ( array ) ;
247+ if ( ! map ) {
248+ map = new Map ( ) ;
249+ for ( let i = 0 ; i < array . length ; i ++ ) {
250+ map . set ( array [ i ] [ 0 ] , i ) ;
250251 }
252+ _indexMaps . set ( array , map ) ;
251253 }
252- // Return -1 if no match is found
253- return - 1 ;
254+ const idx = map . get ( x ) ;
255+ return idx !== undefined ? idx : - 1 ;
254256 }
255257
256258 this . _parseABC = async function ( tune ) {
@@ -277,8 +279,10 @@ function AIWidget() {
277279 } ) ;
278280 for ( const lineId in organizeBlock ) {
279281 organizeBlock [ lineId ] . arrangedBlocks ?. forEach ( staff => {
280- if ( ! staffBlocksMap . hasOwnProperty ( lineId ) ) {
281- staffBlocksMap [ lineId ] = {
282+ // Cache the entry for this lineId to avoid repeated property lookups
283+ let entry = staffBlocksMap [ lineId ] ;
284+ if ( ! entry ) {
285+ entry = {
282286 meterNum : staff ?. meter ?. value [ 0 ] ?. num || 4 ,
283287 meterDen : staff ?. meter ?. value [ 0 ] ?. den || 4 ,
284288 keySignature : staff . key ,
@@ -376,6 +380,7 @@ function AIWidget() {
376380
377381 //for adding above 17 blocks above
378382 blockId = blockId + 17 ;
383+ staffBlocksMap [ lineId ] = entry ;
379384 }
380385
381386 const actionBlock = [ ] ;
@@ -398,7 +403,7 @@ function AIWidget() {
398403 staff . key ,
399404 actionBlock ,
400405 tripletFinder ,
401- staffBlocksMap [ lineId ] . meterDen
406+ entry . meterDen
402407 ) ;
403408 if ( element ?. endTriplet !== null && element ?. endTriplet !== undefined ) {
404409 tripletFinder = null ;
@@ -431,14 +436,10 @@ function AIWidget() {
431436 actionBlock [ actionBlock . length - 1 ] [ 4 ] [ 1 ] = null ;
432437
433438 //update the namedo block if not first nameddo block appear
434- if ( staffBlocksMap [ lineId ] . baseBlocks . length != 0 ) {
435- staffBlocksMap [ lineId ] . baseBlocks [
436- staffBlocksMap [ lineId ] . baseBlocks . length - 1
437- ] [ 0 ] [
438- staffBlocksMap [ lineId ] . baseBlocks [
439- staffBlocksMap [ lineId ] . baseBlocks . length - 1
440- ] [ 0 ] . length - 4
441- ] [ 4 ] [ 1 ] = blockId ;
439+ const baseBlocks = entry . baseBlocks ;
440+ if ( baseBlocks . length != 0 ) {
441+ const lastBase = baseBlocks [ baseBlocks . length - 1 ] ;
442+ lastBase [ 0 ] [ lastBase [ 0 ] . length - 4 ] [ 4 ] [ 1 ] = blockId ;
442443 }
443444 //add the nameddo action text and hidden block for each line
444445 actionBlock . push (
@@ -448,21 +449,17 @@ function AIWidget() {
448449 "nameddo" ,
449450 {
450451 value : `V: ${ parseInt ( lineId ) + 1 } Line ${
451- staffBlocksMap [ lineId ] ?. baseBlocks ? .length + 1
452+ baseBlocks . length + 1
452453 } `
453454 }
454455 ] ,
455456 0 ,
456457 0 ,
457458 [
458- staffBlocksMap [ lineId ] . baseBlocks . length === 0
459+ baseBlocks . length === 0
459460 ? null
460- : staffBlocksMap [ lineId ] . baseBlocks [
461- staffBlocksMap [ lineId ] . baseBlocks . length - 1
462- ] [ 0 ] [
463- staffBlocksMap [ lineId ] . baseBlocks [
464- staffBlocksMap [ lineId ] . baseBlocks . length - 1
465- ] [ 0 ] . length - 4
461+ : baseBlocks [ baseBlocks . length - 1 ] [ 0 ] [
462+ baseBlocks [ baseBlocks . length - 1 ] [ 0 ] . length - 4
466463 ] [ 0 ] ,
467464 null
468465 ]
@@ -480,7 +477,7 @@ function AIWidget() {
480477 "text" ,
481478 {
482479 value : `V: ${ parseInt ( lineId ) + 1 } Line ${
483- staffBlocksMap [ lineId ] ?. baseBlocks ? .length + 1
480+ baseBlocks . length + 1
484481 } `
485482 }
486483 ] ,
@@ -491,20 +488,20 @@ function AIWidget() {
491488 [ blockId + 3 , "hidden" , 0 , 0 , [ blockId + 1 , actionBlock [ 0 ] [ 0 ] ] ]
492489 ) ; // blockid of topaction block
493490
494- if ( ! staffBlocksMap [ lineId ] . nameddoArray ) {
495- staffBlocksMap [ lineId ] . nameddoArray = { } ;
491+ if ( ! entry . nameddoArray ) {
492+ entry . nameddoArray = { } ;
496493 }
497494
498495 // Ensure the array at nameddoArray[lineId] is initialized if it doesn't exist
499- if ( ! staffBlocksMap [ lineId ] . nameddoArray [ lineId ] ) {
500- staffBlocksMap [ lineId ] . nameddoArray [ lineId ] = [ ] ;
496+ if ( ! entry . nameddoArray [ lineId ] ) {
497+ entry . nameddoArray [ lineId ] = [ ] ;
501498 }
502499
503- staffBlocksMap [ lineId ] . nameddoArray [ lineId ] . push ( blockId ) ;
500+ entry . nameddoArray [ lineId ] . push ( blockId ) ;
504501 blockId = blockId + 4 ;
505502
506503 musicBlocksJSON . push ( actionBlock ) ;
507- staffBlocksMap [ lineId ] . baseBlocks . push ( [ actionBlock ] ) ;
504+ baseBlocks . push ( [ actionBlock ] ) ;
508505 } ) ;
509506 } ) ;
510507 }
@@ -665,9 +662,8 @@ function AIWidget() {
665662 staffBlocksMap [ staffIndex ] . repeatBlock [ prevrepeatnameddo ] [ 4 ] [ 3 ] = blockId ;
666663 }
667664 if ( afternamedo != - 1 ) {
668- staffBlocksMap [ staffIndex ] . baseBlocks [ repeatId . end ] [ 0 ] [
669- afternamedo
670- ] [ 4 ] [ 1 ] = null ;
665+ staffBlocksMap [ staffIndex ] . baseBlocks [ repeatId . end ] [ 0 ] [ afternamedo ] [ 4 ] [ 1 ] =
666+ null ;
671667 }
672668
673669 staffBlocksMap [ staffIndex ] . baseBlocks [ repeatId . start ] [ 0 ] [
@@ -834,21 +830,17 @@ function AIWidget() {
834830 } ;
835831
836832 this . _save_lock = false ;
837- widgetWindow . addButton (
838- "export-chunk.svg" ,
839- ICONSIZE ,
840- _ ( "Save sample" ) ,
841- ""
842- ) . onclick = function ( ) {
843- // Debounce button
844- if ( ! that . _get_save_lock ( ) ) {
845- that . _save_lock = true ;
846- that . _saveSample ( ) ;
847- setTimeout ( function ( ) {
848- that . _save_lock = false ;
849- } , 1000 ) ;
850- }
851- } ;
833+ widgetWindow . addButton ( "export-chunk.svg" , ICONSIZE , _ ( "Save sample" ) , "" ) . onclick =
834+ function ( ) {
835+ // Debounce button
836+ if ( ! that . _get_save_lock ( ) ) {
837+ that . _save_lock = true ;
838+ that . _saveSample ( ) ;
839+ setTimeout ( function ( ) {
840+ that . _save_lock = false ;
841+ } , 1000 ) ;
842+ }
843+ } ;
852844
853845 widgetWindow . sendToCenter ( ) ;
854846 this . widgetWindow = widgetWindow ;
@@ -878,6 +870,21 @@ function AIWidget() {
878870 * Plays the reference pitch based on the current sample's pitch, accidental, and octave.
879871 * @returns {void }
880872 */
873+ // Reuse a single AudioContext across plays to avoid the browser limit
874+ // on the number of AudioContexts that can be created.
875+ let _sharedAudioContext = null ;
876+ function _getAudioContext ( ) {
877+ window . AudioContext =
878+ window . AudioContext ||
879+ window . webkitAudioContext ||
880+ navigator . mozAudioContext ||
881+ navigator . msAudioContext ;
882+ if ( ! _sharedAudioContext || _sharedAudioContext . state === "closed" ) {
883+ _sharedAudioContext = new window . AudioContext ( ) ;
884+ }
885+ return _sharedAudioContext ;
886+ }
887+
881888 this . _playABCSong = function ( ) {
882889 const abc = abcNotationSong ;
883890 const stopAudioButton = document . querySelector ( ".stop-audio" ) ;
@@ -887,16 +894,7 @@ function AIWidget() {
887894 } ) [ 0 ] ;
888895
889896 if ( ABCJS . synth . supportsAudio ( ) ) {
890- // An audio context is needed - this can be passed in for two reasons:
891- // 1) So that you can share this audio context with other elements on your page.
892- // 2) So that you can create it during a user interaction so that the browser doesn't block the sound.
893- // Setting this is optional - if you don't set an audioContext, then abcjs will create one.
894- window . AudioContext =
895- window . AudioContext ||
896- window . webkitAudioContext ||
897- navigator . mozAudioContext ||
898- navigator . msAudioContext ;
899- const audioContext = new window . AudioContext ( ) ;
897+ const audioContext = _getAudioContext ( ) ;
900898 audioContext . resume ( ) . then ( function ( ) {
901899 // In theory the AC shouldn't start suspended because it is being initialized in a click handler, but iOS seems to anyway.
902900
@@ -1033,9 +1031,10 @@ function AIWidget() {
10331031 this . _scale = function ( ) {
10341032 let width , height ;
10351033 const canvas = document . getElementsByClassName ( "samplerCanvas" ) ;
1036- Array . prototype . forEach . call ( canvas , ele => {
1037- this . widgetWindow . getWidgetBody ( ) . removeChild ( ele ) ;
1038- } ) ;
1034+ const body = this . widgetWindow . getWidgetBody ( ) ;
1035+ for ( let i = canvas . length - 1 ; i >= 0 ; i -- ) {
1036+ body . removeChild ( canvas [ i ] ) ;
1037+ }
10391038 if ( ! this . widgetWindow . isMaximized ( ) ) {
10401039 width = SAMPLEWIDTH ;
10411040 height = SAMPLEHEIGHT ;
@@ -1056,52 +1055,55 @@ function AIWidget() {
10561055 * @returns {void }
10571056 */
10581057 this . makeCanvas = function ( width , height ) {
1058+ // Build the entire widget DOM off-screen in a DocumentFragment,
1059+ // then append once to avoid multiple reflows.
1060+ const fragment = document . createDocumentFragment ( ) ;
1061+
10591062 // Create a container to center the elements
10601063 const container = document . createElement ( "div" ) ;
1061-
1062- this . widgetWindow . getWidgetBody ( ) . appendChild ( container ) ;
1064+ fragment . appendChild ( container ) ;
10631065
10641066 // Create a scrollable container for the textarea
10651067 const scrollContainer = document . createElement ( "div" ) ;
1066- scrollContainer . style . overflowY = "auto" ; // Enable vertical scrolling
1067- scrollContainer . style . height = height + "px" ; // Set the height of the scroll container
1068- scrollContainer . style . border = "1px solid #ccc" ; // Optional: Add a border for visibility
1069- scrollContainer . style . marginBottom = "8px" ;
1070- scrollContainer . style . marginLeft = "8px" ;
1071- scrollContainer . style . display = "flex" ; // Use flexbox for centering
1072- scrollContainer . style . flexDirection = "column" ; // Stack elements vertically
1073- scrollContainer . style . alignItems = "center" ; // Center items horizontally
1068+ scrollContainer . style . cssText =
1069+ "overflow-y:auto;" +
1070+ "height:" +
1071+ height +
1072+ "px;" +
1073+ "border:1px solid #ccc;" +
1074+ "margin-bottom:8px;" +
1075+ "margin-left:8px;" +
1076+ "display:flex;" +
1077+ "flex-direction:column;" +
1078+ "align-items:center" ;
10741079 container . appendChild ( scrollContainer ) ;
10751080
10761081 // Create the textarea element
10771082 const textarea = document . createElement ( "textarea" ) ;
1078- textarea . style . height = height + "px" ; // Keep the height for the scrollable area
1079- textarea . style . width = width + "px" ;
1083+ textarea . style . cssText =
1084+ "height:" +
1085+ height +
1086+ "px;" +
1087+ "width:" +
1088+ width +
1089+ "px;" +
1090+ "margin-left:20px;" +
1091+ "font-size:20px;" +
1092+ "padding:10px" ;
10801093 textarea . className = "samplerTextarea" ;
1081- textarea . style . marginLeft = "20px" ;
1082- textarea . style . fontSize = "20px" ;
1083- textarea . style . padding = "10px" ;
10841094 scrollContainer . appendChild ( textarea ) ; // Append textarea to scroll container
10851095
10861096 // Create hint text elements
10871097 const hintsContainer = document . createElement ( "div" ) ;
1088- hintsContainer . style . marginBottom = "10px" ;
1089-
1090- hintsContainer . style . display = "flex" ;
1091- hintsContainer . style . justifyContent = "center" ;
1092- hintsContainer . style . marginTop = "8px" ;
1098+ hintsContainer . style . cssText =
1099+ "margin-bottom:10px;display:flex;justify-content:center;margin-top:8px" ;
10931100 const hints = [ "Dance tune" , "Fiddle jig" , "Nice melody" , "Fun song" , "Simple canon" ] ;
10941101 hints . forEach ( hintText => {
10951102 const hint = document . createElement ( "span" ) ;
10961103 hint . textContent = hintText ;
1097- hint . style . marginRight = "20px" ;
1098- hint . style . cursor = "pointer" ;
1099- hint . style . marginRight = "4px" ;
1100- hint . style . fontSize = "20px" ;
1101- hint . style . color = "blue" ;
1102- hint . style . backgroundColor = "rgb(227 162 162 / 80%)" ; // Light white background
1103- hint . style . padding = "10px" ; // Add padding for spacing
1104- hint . style . borderRadius = "5px" ; // Optional: Rounded corners
1104+ hint . style . cssText =
1105+ "cursor:pointer;margin-right:4px;font-size:20px;color:blue;" +
1106+ "background-color:rgb(227 162 162 / 80%);padding:10px;border-radius:5px" ;
11051107
11061108 hint . onclick = function ( ) {
11071109 inputField . value = hintText ;
@@ -1117,12 +1119,9 @@ function AIWidget() {
11171119 inputField . type = "text" ;
11181120 inputField . className = "inputField" ;
11191121 inputField . placeholder = "Enter text here" ;
1120- inputField . style . fontSize = "20px" ;
1121- inputField . style . marginRight = "2px" ;
1122- inputField . style . marginLeft = "64px" ;
1123- inputField . style . padding = "10px" ;
1124- inputField . style . marginBottom = "10px" ;
1125- inputField . style . width = "60%" ;
1122+ inputField . style . cssText =
1123+ "font-size:20px;margin-right:2px;margin-left:64px;" +
1124+ "padding:10px;margin-bottom:10px;width:60%" ;
11261125 container . appendChild ( inputField ) ;
11271126
11281127 inputField . addEventListener ( "click" , function ( ) {
@@ -1250,5 +1249,8 @@ function AIWidget() {
12501249 textarea . addEventListener ( "input" , function ( ) {
12511250 abcNotationSong = textarea . value ;
12521251 } ) ;
1252+
1253+ // Single DOM append — all elements are now in the fragment
1254+ this . widgetWindow . getWidgetBody ( ) . appendChild ( fragment ) ;
12531255 } ;
12541256}
0 commit comments