paulrosen / abcjs

javascript for rendering abc music notation
Other
1.94k stars 285 forks source link

Synth Only (example)and timing Callbacks #750

Closed jepcolombia closed 2 years ago

jepcolombia commented 2 years ago

Paul thank you very much for your library. Iam trying a custom animation and using your Synth only example I added :
var timingCallbacks = new ABCJS.TimingCallbacks(visualObj, {eventCallback: eventCallback}); to be able to do some kind of animation with every note. It does not show any error and return info to timeCallbacks var but does not call my custom fucntion : eventCallback (). Is there any way to do that? Many Thanks

paulrosen commented 2 years ago

This example might be closer to what you are using: https://paulrosen.github.io/abcjs/examples/animation.html

I'd have to look at your code to know more about what you are doing, but are you calling timingCallbacks.start();?

jepcolombia commented 2 years ago

Oh no how could I forgot timingCallbacks.start(); Sorry! Thanks Paul!

jepcolombia commented 2 years ago

{ "type": "event", "milliseconds": 32833, "line": 2, "measureNumber": 9, "top": 267.4585386491913, "height": 59.98937575049138, "left": 190.98535377373892, "width": 9.81, "elements": [ [ {} ] ], "startChar": 159, "endChar": 160, "startCharArray": [ 159 ], "endCharArray": [ 160 ], "midiPitches": [], "endX": 211.98215251582596, "millisecondsPerMeasure": 1333.3333333333335 }

jepcolombia commented 2 years ago

In the eventCallback function : console.log(JSON.stringify(ev,null,4)); instruction shows empty midiPitches as follow

{ "type": "event", "milliseconds": 32833, "line": 2, "measureNumber": 9, "top": 267.4585386491913, "height": 59.98937575049138, "left": 190.98535377373892, "width": 9.81, "elements": [ [ {} ] ], "startChar": 159, "endChar": 160, "startCharArray": [ 159 ], "endCharArray": [ 160 ], "midiPitches": [], "endX": 211.98215251582596, "millisecondsPerMeasure": 1333.3333333333335 }

jepcolombia commented 2 years ago

same happens with animation example when inserting console.log(JSON.stringify(ev,null,4)); in the evenCallback function.

paulrosen commented 2 years ago

Can you post your ABC string? I'll see what I get.

jepcolombia commented 2 years ago

<!DOCTYPE html>

abcjs: Synth Only Demo

Demo of abcjs audio capabilities

This shows how to play notes without a visual representation.

Type valid ABC code in the textarea and click "Play" to hear it.

Just using your Synth-only example and adding evenCallback()

jepcolombia commented 2 years ago

Thanks Paul for your prompt answer. Just adding eventCallback() to your Synth-only example can reproduce the empty midiPitches[] array in the event info. Best regards and thanks again

Esn024 commented 2 years ago

I can confirm that the midiPitches array is always empty!

This is actually a huge problem for me because I don't know how else to get the data on which note is currently playing (I was hoping to use this data to make divs elsewhere on the page change colour depending on which notes are currently playing).

Any help with this, or a workaround, would be greatly appreciated!

Or maybe there's an older version of ABCJS in which this was still working?

Here's some code I was working on that shows it (it's an entire HTML file. Sorry for the length. But you can see the console.log('midiPitches', ev.midiPitches); is always an empty array):

<!DOCTYPE html>
<html lang="en"><head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="css/examples-styles.css">
  <link rel="stylesheet" href="css/abcjs-audio.css">

    <link rel="icon" href="https://paulrosen.github.io/abcjs/examples/favicon.ico" type="image/x-icon">
    <title>abcjs: Modify Synth Input Demo</title>

    <style>
        main {
            max-width: 770px;
            margin: 0 auto;
        }
        .abcjs-cursor {
            stroke: red;
        }
        .abcjs-rest {
            opacity: 0.1;
        }
        .color {
           stroke: red;
           fill: red;
       }
    </style>

    <script src="js/abcjs-basic.js" type="text/javascript"></script>
    <script type="text/javascript">
    //  var abc1 = `T: Cooley's
    //      %%barnumbers 1
    //      M: 4/4`;
        const abc = `X:1
T: Cooley's
%%barnumbers 1
M: 4/4
L: 1/8
Q: 1/4=200
R: reel
K: Emin
EBBA B2 EB|~B2 AB dBAG|FDAD BDAD|FDAD dAFD|
EBBA B2 EB|B2 AB defg|afe^c dBAF|DEFD E2 gf|
|eB B2 efge|eB B2 gedB|A2 FA DAFA|A2 FA defg|
eB B2 eBgB|eB B2 defg|afe^c dBAF|DEFD E2 D2||`;

        const abc2 = `X:2
T:Title
C:Composer Name (yyyy)
M:C
Q:1/4=76
%%score (T1 T2)
V:T1           clef=treble  name="Left thumb"   snm="L"
V:T2           clef=treble  name="Right thumb"  snm="R"
K:Gm
%            End of header, start of tune body:
% 1
[V:T1]  zBzczdzg  | f6e2      | (d2c2 d2)e2 | d4 c2z2 |
[V:T2]  GzAzBzez  | d6c2      | (B2A2 B2)c2 | B4 A2z2 |
% 5
[V:T1]  (B2c2 d2g2)  | f8        | d3c (d2fe)  | H d6    ||
[V:T2]       z8      |     z8    | B3A (B2c2)  | H A6    ||`;

const abc3 = `X:3
T:Title
C:Composer Name (yyyy)
M:2/4
Q:1/4=176
L:1/8
%%score (T1 T2)
V:T1           clef=treble  name="Left Thumb"   snm="L"
V:T2           clef=treble  name="Right Thumb"  snm="R"
K:Gm
%            End of header, start of tune body:
% 1
[V:T1]  C4| zGzB | zGzB | zGzB | zGzB |
[V:T2]  zc3| FzGz | FzGz | FzGz | FzGz |
% 5
[V:T1]  zGzc | zGzc | zGzc | zGzc ||
[V:T2]  FzGz | FzGz | FzGz | FzGz ||`;

const abc4 = `X:4
%%score (T1 T2)
V:T1           clef=treble  name="Left Thumb"   snm="L"
V:T2           clef=treble  name="Right Thumb"  snm="R"
% 1
[V:T1]  zdze   ||
[V:T2]  czcz ||
`;

let allAbcTunes = {abc, abc2, abc3, abc4};

    </script>
</head>
<body>
  <header>
    <h1>Modify Synth Input</h1>
  </header>
  <div class="container">
    <main> 
      <label for="cars">Select ABC tune:
        <select id="select-abc-tune" onChange="loadNewTune(allAbcTunes[this.value])">
          <option value="abc">abc</option>
          <option value="abc2">abc2</option>
          <option value="abc3">abc3</option>
          <option value="abc4">abc4</option>
        </select>
      </label>
    <p>Content of the ABC tune:</p>
    <textarea id="abc-content" readonly></textarea>
      <p>This example adds some "swing" to the tune.</p>
        <p><label>Tempo: <input id="tempo" type="number" min="1" max="600" value="80"></label></p>
        <p><label>Calculated swing coefficient:</label> <code id="coefficient">0.05</code></p>

      <div id="paper"></div>

        <button id="start-pause">Start</button>
        <button id="reset">Reset</button>
        <input type="range" min="0" max="100" value="0" id="slider">
        <button id="seek">Seek</button>
      <div id="synth-controller"></div>
      <!-- <button class="activate-audio" style="">Activate Audio Context And Play</button>
      <button class="stop-audio" style="display:none;">Stop Audio</button> -->
      <div class="audio-error" style="display:none;">Audio is not supported in this browser.</div>
    </main>
  </div>
<script>

const firstAbcTuneToLoad = abc2;

const startPauseButtonText = ["Start", "Pause"];

let isRunning = false;
let isPaused = true;

// First draw the music - this supplies an object that has a lot of information about how to create the synth.
// NOTE: If you want just the sound without showing the music, use "*" instead of "paper" in the renderAbc call.
// initialize it with the first example tune
var visualObj = ABCJS.renderAbc("paper", firstAbcTuneToLoad, {
    responsive: "resize",
    add_classes: true
    })[0];

// console.log({visualObj});

// controls notification of when events happen
const CursorControl = function() {
    this.beatSubdivisions = 2;
    this.onStart = function() {
        console.log("The tune has started playing.");
    }
    this.onFinished = function() {
        console.log("The tune has stopped playing.");
    }
    this.onEvent = function(event) {
        console.log("An event is happening", event);
    }
}

var cursorControl = new CursorControl();

// function and variable to do with adding & removing the CSS that gives red color to elements that are "playing"
let lastEls = [];
const colorElements = (els) => {
    let i;
    let j;
    for (i = 0; i < lastEls.length; i++) {
        for (j = 0; j < lastEls[i].length; j++) {
            lastEls[i][j].classList.remove("color");
        }
    }
    for (i = 0; i < els.length; i++) {
        for (j = 0; j < els[i].length; j++) {
            els[i][j].classList.add("color");
        }
    }
    lastEls = els;
}

const pauseButtonControl = () => {
    isRunning = !isRunning;
    isPaused = !isPaused;
    startPauseButton.innerText = startPauseButtonText[isRunning ? 1 : 0];
}

// the function to change colours to red
const eventCallback = (ev) => {
    if (!ev) {
        pauseButtonControl();
        return;
    }
    colorElements(ev.elements);

    //tests
    console.log(ev);
    console.log('milliseconds', ev.milliseconds);
    // BUG: ev.midiPitches doesn't seem to work
    // console.log('eventCallback midiPitches', ev.midiPitches);
}

// the function for changing events in audio playback
const sequenceCallback = (tracks) => {
    // get tempo
    const tempo = document.getElementById("tempo").value;

    // display the tempo value
    document.getElementById("coefficient").innerText = tempo;

    console.log(tracks);

    tracks.forEach((track, trackIndex) => {
        track.forEach((event) => {
            console.log('event pitch', event.pitch);
            if (event.pitch === 60) {
                event.cents = 50;
                console.log('sequenceCallback event', event);
            }
        });
    });

}

/*
const onEndedCallbackLoopAudio = async () => {
    try {
        await audioContext.resume();
            // In theory the AC shouldn't start suspended because it is being initialized in a click handler, but iOS seems to anyway.
        await synth.start();
        timingCallbacks.start(0);
    } catch (error) {
        console.log("Audio Failed", error);
    }
}
*/

const resetPlayback = () => {
    isRunning = false;
    isPaused = true;
    timingCallbacks.stop();
    synth.stop();
    // remove any remaining red coloration
    Array.from(document.querySelectorAll('.color')).forEach((el) => el.classList.remove('color'));

    // make the text say "Start"
    startPauseButton.innerText = startPauseButtonText[0];
}

const goToSpecificPlaceInSong = async () => {
    // the slider, by default, goes from 1-100
    const position = slider.value / 100; 
    console.log({position});
    await synth.seek(position);
    timingCallbacks.setProgress(position);
}

const initializeAudio = async (visualObj, sequenceCallback) => {
    /*const synthControllerAudioParams = {
        audioContext: audioContext,
        chordsOff: true,
        soundFontUrl: "soundfonts",
        visualObj: visualObj
    }
    */
    try {
        await audioContext.resume();
            // In theory the AC shouldn't start suspended because it is being initialized in a click handler, but iOS seems to anyway.

        await synth.init({
                audioContext: audioContext,
                visualObj: visualObj,
                options: {
                    sequenceCallback: sequenceCallback,
                    soundFontUrl: "soundfonts",
                    onEnded: () => console.log("playback ended"),
                }
            });

        await synth.prime();
        //await synth.start();
    //  await synthControl.setTune(visualObj, true, synthControllerAudioParams);

    } catch (error) {
        console.log("Audio Failed", error);
    }
}

const startPause = async () => {
    wasItPaused = isPaused;
    isRunning = !isRunning;
    isPaused = !isPaused;
    startPauseButton.innerText = startPauseButtonText[isRunning ? 1 : 0];
    if (isRunning) {
        await synth.start();
        // if playback was paused, begin from the last point, otherwise make sure it starts at the very beginning
        wasItPaused ? timingCallbacks.start() : timingCallbacks.start(0);
    } else {
        await synth.pause();
        timingCallbacks.pause();
    }
}

const loadNewTune = async (newTune) => {

    // display the text value of the ABC tune
    document.querySelector('#abc-content').value = newTune;

    // First draw the music - this supplies an object that has a lot of information about how to create the synth.
    // NOTE: If you want just the sound without showing the music, use "*" instead of "paper" in the renderAbc call.
    let visualObj = ABCJS.renderAbc("paper", newTune, {
        responsive: "resize",
        add_classes: true
        })[0];

    //setTune(visualObj, userAction, audioParams)
//  synthControl.setTune(visualObj, true, {
//      audioContext: audioContext,
//      visualObj: visualObj,
//  })

    // can use the below to replace target if the music changes on the fly, but the tempo will remain as before.
    //timingCallbacks.replaceTarget(visualObj);

    // initialize the animation's timing callbacks
    timingCallbacks = new ABCJS.TimingCallbacks(visualObj, {
        eventCallback: eventCallback,
    });

    initializeAudio(visualObj, sequenceCallback);

    // make the text say "Start"
    startPauseButton.innerText = startPauseButtonText[0];
}

// initialize the animation's timing callbacks
let timingCallbacks = new ABCJS.TimingCallbacks(visualObj, {
    eventCallback: eventCallback,
});

// check if browser supports audio, initialize audioContext
if (!ABCJS.synth.supportsAudio()) {
    const audioError = document.querySelector(".audio-error");
    audioError.setAttribute("style", "");
} else {
    // An audio context is needed - this can be passed in for two reasons:
    // 1) So that you can share this audio context with other elements on your page.
    // 2) So that you can create it during a user interaction so that the browser doesn't block the sound.
    // Setting this is optional - if you don't set an audioContext, then abcjs will create one.
    window.AudioContext = window.AudioContext ||
        window.webkitAudioContext ||
        navigator.mozAudioContext ||
        navigator.msAudioContext;

    // this is var because I need it to be in the global scope
    var audioContext = new window.AudioContext();

    // initialize synth
    var synth = new ABCJS.synth.CreateSynth();
    // initialize synth controller

    /*
    var synthControl = new ABCJS.synth.SynthController();
    synthControl.load("#synth-controller", 
        cursorControl, 
        {
            displayLoop: true, 
            displayRestart: true, 
            displayPlay: true, 
            displayProgress: true, 
            displayWarp: true
        }
    );
    */

}

// load the first audio + image
initializeAudio(visualObj, sequenceCallback);

// DOM elements     
const explanationDiv = document.querySelector(".suspend-explanation");
const abcTuneSelectMenu = document.getElementById("select-abc-tune");
const startPauseButton = document.getElementById("start-pause");
const resetButton = document.getElementById("reset");
const seekButton = document.getElementById("seek");
const slider = document.getElementById("slider");

// event listeners
startPauseButton.addEventListener("click", () => { startPause(); });
resetButton.addEventListener("click", () => { resetPlayback(); });
slider.addEventListener("change", () => { goToSpecificPlaceInSong(); });

</script>

</body></html>
paulrosen commented 2 years ago

I can duplicate with the file you posted above. I will debug it when I get a moment in a few days. Thanks for the example.

Esn024 commented 2 years ago

EDIT: I just realized that the event.start inside sequenceCallback is actually measured in fractions of musical whole notes (at least I think so...), and that in order to convert it to the beatNumber inside the beatCallback, all I needed to do was multiply it by 8! What I had below was actually working merely because I happened by chance to be testing it on piece with a time signature of 8/8. So the below comment is wrong in some respects, but I'm too lazy to edit the whole thing now.

~While you're working on this, do you think you could add a start property into each event inside eventCallback that matches the one that gets added to each event in sequenceCallback, and also a beatNumber property like the one inside the beatCallback? Also, please add a beatNumber into each event inside sequenceCallback just like the one in beatCallback. It would make it a lot easier to interact between them.~

Here's what I'm currently doing as a workaround to get the data on which notes are currently playing, even though the ev.midiPitches array in the event callback is always empty. When the song is first loaded, I have the following sequenceCallback code. Inside sequenceCallback, I populate an array of all the note events for the entire piece (allNoteEvents).

By default, ABCJS adds something called event.start to each event that is added to the sequenceCallback, with values like 3.75 (if it's the on the 3rd beat of the 3rd measure in 4/8).

So, I then need to use this information for my task to find out which notes are currently playing. I can either use eventCallback (check once per note event) or beatCallback (check once per beat).

The event passed through into the eventCallback has no information on what beat it currently is (it has milliseconds from start of song, and the current measure - that's it). While inside beatCallback, I have access to a beatNumber (# of beats from start of song).

So I have to convert the event.start either into the beatNumber, or into milliseconds. I decided it was simpler to convert it into beatNumber for the beatCallback (it would be really useful if the eventCallback had something similar to the event.start number, actually, but it doesn't at the moment).

To make the calculation simpler, and the updates happen often enough, I make sure that the piece's time signature is that of the smallest metrical subdivision in my music, which happens to be eighth notes, so x/8 (2/8, 4/8, etc.), (unfortunately, this means I can't change time signatures part way through a song, at least not yet). Then inside the beatCallback, I find the events inside the allNoteEvents array whose beatFromStartOfSong (that I've custom-added into each event) matches the beatNumber that is inside the beatCallback, then I grab the pitches from those events. Like this: const currentPitches = allNoteEvents.filter(e => e.beatFromStartOfSong === beatNumber).map(e => e.pitch);

Mind you, this has the limitation that it doesn't check for "long" notes that are being held over from previous beats - it's really only good for getting the start-times of pitches, so for percussive instruments like pianos and marimbas. But that's enough for my purpose. (if I wanted to find the end of a particular event, there's also an "end" property in each event in sequenceCallback, similar to the "start" property)

Here's a fuller example of the code I'm using. The sequenceCallback:

let allNoteEvents = [];
let currentTune = `X:1
T:Title
M:8/8
Q:1/4=76
L:1/8
%%score (T1 T2)
V:T1           clef=treble  name="Left"   snm="L"
V:T2           clef=treble  name="Right"  snm="R"
K:Gm
%            End of header, start of tune body:
% 1
[V:T1]  zBzczdzg  | f6e2      | (d2c2 d2)e2 | d4 c2z2 |
[V:T2]  GzAzBzez  | d6c2      | (B2A2 B2)c2 | B4 A2z2 |
% 5
[V:T1]  (B2c2 d2g2)  | f8        | d3c (d2fe)  | H d6    ||
[V:T2]       z8      |     z8    | B3A (B2c2)  | H A6    ||`;

// the function for changing events in audio playback. Runs once after the array of notes is created, but just before it is used to create the audio buffer
const sequenceCallback = (tracks) => {

    // time signature can be anywhere from 2/8 to 13/8. Find the # of eighth notes (beats) per measure
    const regexForTimeSignature = /M:\s?([2-9]|1[0-3])\/8/;
    const beatsPerMeasure = currentTune.match(regexForTimeSignature)[1];

    // console.log({beatsPerMeasure});
    // console.log(tracks);

    tracks.forEach((track, trackIndex) => {
        track.forEach((event) => {
            //which measure this event is in
            const measure = Math.floor(event.start);
            //calculate which overall beat this event falls on, based on how many beats there are in each measure
            const beatFromStartOfSong = event.start * beatsPerMeasure;
            // calculate which beat this is within the current measure
            const beatInMeasure = (event.start - measure) * beatsPerMeasure;
            // MIDI (scientific notation) note name
            // MIDI (scientific notation) note name

            // MIDI (scientific notation) note name
            const midiNoteName = ABCJS.synth.pitchToNoteName[event.pitch];

            // ABC notation note name (midiNoteNameToAbc is a custom function I wrote that converts scientific notation to abc notation)
            const abcNoteName = midiNoteNameToAbc(midiNoteName);

            //console.log({event});
            //console.log('event pitch', event.pitch);
            //add info on any tuning modifications for the event, too:
            if (abcNoteName === 'E') {
                event.cents = -90;
                //console.log('sequenceCallback event', event);
            }

            // make an array of all note events. Add in the data calculated above
            allNoteEvents.push({...event, measure, beatFromStartOfSong, beatInMeasure, midiNoteName, abcNoteName});
        });
    });

    //console.log({allNoteEvents});

}

Then for the beatCallback I do the following:

// this runs every beat. I can use the "currentPitches" or "currentAbcPitches" array (which also gets updated every beat) to, for example, change the colour of the keys that are currently playing of a piano keyboard that's on the page
const beatCallback = async (beatNumber, totalBeats, totalTime) => {
    //console.log(beatNumber);
    // the below line logs the % of the way through the song. This data can be used to change the position of a range slider, for example - https://www.w3schools.com/howto/howto_js_rangeslider.asp
    //console.log('% of the way through the song', beatNumber/totalBeats * 100);

    // array of MIDI pitches currently playing (e.g. [60, 62])
    const currentPitches = allNoteEvents.filter(e => e.beatFromStartOfSong=== beatNumber).map(e => e.pitch);
    // since I've added an abcNoteName to each event, I can also get the abcPitches
    const currentAbcPitches = allNoteEvents.filter(e => e.beatFromStartOfSong=== beatNumber).map(e => e.abcNoteName);

    //console.log({currentPitches});
}

I have the following code for the eventCallback:

// function and variable to do with adding & removing the CSS that gives red color to elements that are "playing"
//adds or removesa class "color" to the DOM elements that are to be colored
let lastEls = [];
const colorElements = (currentEls) => {
    let i;
    let j;
    for (i = 0; i < lastEls.length; i++) {
        for (j = 0; j < lastEls[i].length; j++) {
            lastEls[i][j].classList.remove("color");
        }
    }
    //currentEls.forEach((currentEl) => {});
    for (i = 0; i < currentEls.length; i++) {
        //console.log('currentEls[i]', currentEls[i]);
        for (j = 0; j < currentEls[i].length; j++) {
            //console.log('currentEls[i][j]', currentEls[i][j]);
            currentEls[i][j].classList.add("color");
        }
    }
    lastEls = currentEls;
}

// the function to change colours to red
const eventCallback = (ev) => {
    if (!ev) {
        pauseButtonControl();
        return;
    }
    colorElements(ev.elements);

    //tests
    //console.log('midiPitches', ev.midiPitches);
    //console.log('milliseconds', ev.milliseconds);
    // BUG: ev.midiPitches doesn't seem to work
    // console.log('eventCallback midiPitches', ev.midiPitches);
}

And I include them both in the timingCallbacks, like so:

// initialize the animation's timing callbacks
var timingCallbacks = new ABCJS.TimingCallbacks(visualObj, {
    eventCallback: eventCallback,
    beatCallback: beatCallback,
});

the timingCallbacks get started or stopped in the various functions, depending on what I want to do with the song. To keep it timed with the synth, I need to have the synth also do the same things, whatever I do with the timingCallbacks. The line with the synth needs to come first (right before the timingCallbacks line), because it needs more time so I need to use the "await" keyword (because of that, the below lines can only be used inside an "async" function):

// start from the beginning
await synth.start(0);
timingCallbacks.start(0);

// start from last paused point
await synth.start();
timingCallbacks.start();

//pause
await synth.pause();
timingCallbacks.pause();

//stop
synth.stop();
timingCallbacks.stop();

//go to a specific position
await synth.seek(position);
timingCallbacks.setProgress(position);

Example of a function using the above:


let isRunning = false;

const startPause = async () => {
    isRunning = !isRunning;
    if (isRunning) {
        await synth.start();
        timingCallbacks.start();
    } else {
        await synth.pause();
        timingCallbacks.pause();
    }
}
paulrosen commented 2 years ago

Sorry for the huge delay in looking into this. In the example there is this:

// initialize the animation's timing callbacks
timingCallbacks = new ABCJS.TimingCallbacks(visualObj, {
  eventCallback: eventCallback,
});

initializeAudio(visualObj, sequenceCallback);

What is happening is that the timer is getting the object before the audio is initialized. To get the midiPitches, reverse that:

await initializeAudio(visualObj, sequenceCallback);

// initialize the animation's timing callbacks
timingCallbacks = new ABCJS.TimingCallbacks(visualObj, {
  eventCallback: eventCallback,
});

(Also notice that I added await so that it would finish initializing before starting the timer.)

Esn024 commented 2 years ago

Thanks so much, that worked! In React, I had to put it into an async function inside a useEffect in a component, so it currently looks like this for me in abcKalimbaSynth (well, it will once I deploy the fixed version, anyway):

  // initialize music
  useEffect(() => {
    const abc = musicalSectionsToAbc(
      musicalSections,
      orderOfSections,
      noteGridToAbc,
      tempo,
      key,
      projectName
    );

    const visualObj = abcjs.renderAbc(idForScoreDiv, abc, {
      responsive: 'resize',
      add_classes: true,
    })[0];

    async function musicAndTiming() {
      await initializeMusic(
        visualObj,
        synth,
        getSequenceCallback(setAllNoteEvents)
      );

      setTimingCallbacks(
        new abcjs.TimingCallbacks(visualObj, {
          eventCallback: getEventCallback(
            colorElements,
            musicIsPlaying,
            setMusicIsPlaying
          ),
          //TODO the allNoteEvents array that gets sent here is one state update behind
          beatCallback: getBeatCallback(allNoteEvents, setSliderPosition),
        })
      );
    }

    musicAndTiming();
  }, [tempo, key, musicalSections, orderOfSections, tines, projectName]);

One more quick question, is there any way to differentiate a written Ab from a G# event - is that information recorded anywhere? It would show up as the same MIDI pitch, but I may want to have two different tunings for them, so I'd like to not rely on only the MIDI pitch, but pass through the actual written ABC note.

paulrosen commented 2 years ago

You mean inside the sequenceCallback function? There isn't an easy way, sorry. I'll look at it though and see how hard it would be to carry a pointer back to the original abc string.

I can see how that would be useful.

Esn024 commented 2 years ago

Thank you for the promise to look into it.

You mean inside the sequenceCallback function?

Both sequenceCallback and eventCallback. The reason is actually the following: in my implementation, I plan to have a "music keyboard" at the top of the screen and use eventCallback to figure out which notes are currently being played, and have them change colour. sequenceCallback is used to apply any tuning modifications to the note (in cents).

Currently, it works if Ab and G# are the same note. The issue is that Ab and G# may be assigned to different notes on the keyboard (for most of the history of Western classical music before approx. the mid-19th century, during which equal temperament was not used, they were indeed different notes, and some piano/organ keyboards assigned them to different physical keys. Today, it would allow abcKalimbaSynth, using ABCJS, to be used to play around with the hundreds of non-12-TET Scala tunings).

paulrosen commented 2 years ago

In v6.0.4 there will be startChar and endChar in the track info. You can use that to look at the original string to see if it is G# or Ab.

paulrosen commented 2 years ago

If that fix isn't enough to get you what you need, let me know and I'll reopen.