void StageChanged(int value)
        {
            testSpec.Stage = value;

            if (testSpec.Stage < 7)
            {
                testSpec.TenorWeight = 8;
            }
            else if (testSpec.Stage > 8)
            {
                testSpec.TenorWeight = 23;
            }

            testSpec.TenorWeightDisabled = TenorWeightSelect.TenorWeightDisabled(testSpec.Stage);

            if (blowSetA != null)
            {
                blowSetA = null;
            }

            if (blowSetB != null)
            {
                blowSetB = null;
            }
        }
        async void Play()
        {
            // Change test bell color to disabled color - can't adjust gap during play
            blowSet.Blows.Last().BellColor = Constants.DisabledUnstruckTestBellColor;

            DateTime strikeTime = DateTime.Now;

            foreach (Blow blow in blowSet.Blows)
            {
                // Set strike times
                strikeTime      = strikeTime.AddMilliseconds(blow.Gap);
                blow.StrikeTime = strikeTime;
            }

            screen.PlayDisabled          = true;
            screen.PlayLabel             = "Wait";
            testSpec.ControlsDisabled    = true;
            testSpec.TenorWeightDisabled = true;

            TimeSpan delay;
            int      delayMs;

            foreach (Blow blow in blowSet.Blows)
            {
                delay   = blow.StrikeTime - DateTime.Now;
                delayMs = Convert.ToInt32(delay.TotalMilliseconds);

                if (delayMs > 0)
                {
                    await Task.Delay(delayMs);
                }

                // Strike bell
                await JSRuntime.InvokeVoidAsync("PlayBellAudio", blow.AudioId);
            }

            // Initial delay
            await Task.Delay(1000);

            // Start spinner
            screen.SpinnerPlaying = true;
            StateHasChanged();

            // Wait further 1.6 seconds for the sound to finish
            await Task.Delay(1600);

            // Reset play button
            screen.SpinnerPlaying = false;
            screen.PlayLabel      = "Play";
            screen.PlayDisabled   = false;

            // Reset screen
            blowSet.Blows.Last().BellColor = Constants.UnstruckTestBellColor;
            testSpec.ControlsDisabled    = false;
            testSpec.TenorWeightDisabled = TenorWeightSelect.TenorWeightDisabled(testSpec.Stage);
            StateHasChanged();
        }
        async Task Save()
        {
            testSpec.SpinnerSaving       = true;
            testSpec.SaveLabel           = "Wait";
            testSpec.ControlsDisabled    = true;
            testSpec.TenorWeightDisabled = true;
            screenA.PlayDisabled         = true;
            screenB.PlayDisabled         = true;
            StateHasChanged();

            // Push the created test to the API in JSON format
            // Start by creating a BlowSetCore object, which just has the parent data BlowSet, for each of A and B
            // Note implicit cast from child to parent
            BlowSetCore blowSetCoreA = blowSetA;
            BlowSetCore blowSetCoreB = blowSetB;

            // BlowSetCore has a BlowsCore list which is empty so far
            // Call the LoadBlowsCore method to populate it for each of A and B
            blowSetCoreA.LoadBlowsCore(blowSetA);
            blowSetCoreB.LoadBlowsCore(blowSetB);

            // Next use the Serializer method of the JsonSerializer class (in the System.Text.Json namespace) to create
            // a Json object from the BlowSetData object for each of A and B
            ABTestData aBTestData = new ABTestData
            {
                ABTestSpecA = JsonSerializer.Serialize(blowSetCoreA),
                ABTestSpecB = JsonSerializer.Serialize(blowSetCoreB)
            };

            // Push the Json object to the API
            await TJBarnesService.GetHttpClient().PostAsJsonAsync("api/abtests", aBTestData);

            // Refresh the contents of the Select Test dropdown
            aBTestsData = (await TJBarnesService.GetHttpClient()
                           .GetFromJsonAsync <ABTestData[]>("api/abtests")).ToList();

            testSpec.SpinnerSaving = false;
            testSpec.Saved         = true;
            StateHasChanged();

            await Task.Delay(1000);

            testSpec.Saved               = false;
            testSpec.SaveLabel           = "Save";
            testSpec.ControlsDisabled    = false;
            testSpec.TenorWeightDisabled = TenorWeightSelect.TenorWeightDisabled(testSpec.Stage);
            screenA.PlayDisabled         = false;
            screenB.PlayDisabled         = false;
            StateHasChanged();
        }
        async Task Save()
        {
            testSpec.SpinnerSaving       = true;
            testSpec.SaveLabel           = "Wait";
            testSpec.ControlsDisabled    = true;
            testSpec.TenorWeightDisabled = true;
            screen.PlayDisabled          = true;
            blowSet.Blows.Last().BellColor = Constants.DisabledUnstruckTestBellColor;
            StateHasChanged();

            // Push the created test to the API in JSON format
            // Start by creating a BlowSetCore object, which just has the parent data BlowSet
            // Note implicit cast from child to parent
            BlowSetCore blowSetCore = blowSet;

            // BlowSetCore has a BlowsCore list which is empty so far
            // Call the LoadBlowsCore method to populate it
            blowSetCore.LoadBlowsCore(blowSet);

            // Next use the Serializer method of the JsonSerializer class (in the System.Text.Json namespace) to create
            // a Json object from the BlowSetData object
            var gapTestData = new GapTestData
            {
                GapTestSpec = JsonSerializer.Serialize(blowSetCore)
            };

            // Push the Json object to the API
            await TJBarnesService.GetHttpClient().PostAsJsonAsync("api/gaptests", gapTestData);

            // Refresh the contents of the Select Test dropdown
            gapTestsData = (await TJBarnesService.GetHttpClient()
                            .GetFromJsonAsync <GapTestData[]>("api/gaptests")).ToList();

            testSpec.SpinnerSaving = false;
            testSpec.Saved         = true;
            StateHasChanged();

            await Task.Delay(1000);

            testSpec.Saved               = false;
            testSpec.SaveLabel           = "Save";
            testSpec.ControlsDisabled    = false;
            testSpec.TenorWeightDisabled = TenorWeightSelect.TenorWeightDisabled(testSpec.Stage);
            screen.PlayDisabled          = false;
            blowSet.Blows.Last().BellColor = Constants.UnstruckTestBellColor;
            StateHasChanged();
        }
        async Task PlayAsync()
        {
            int initialDelay = 1000;

            if (screen.PlayLabel == "Play")
            {
                // Change test bell color to disabled color - can't adjust gap during play
                blowSet.Blows.Last().BellColor = Constants.DisabledUnstruckTestBellColor;

                DateTime strikeTime = DateTime.Now;

                foreach (Blow blow in blowSet.Blows)
                {
                    // Set strike times
                    strikeTime      = strikeTime.AddMilliseconds(blow.Gap);
                    blow.StrikeTime = strikeTime;
                }

                cancellationTokenSource = new CancellationTokenSource();
                cancellationToken       = cancellationTokenSource.Token;

                screen.PlayLabel             = "Stop";
                testSpec.ControlsDisabled    = true;
                testSpec.TenorWeightDisabled = true;

                TimeSpan delay;
                int      delayMs;

                foreach (Blow blow in blowSet.Blows)
                {
                    delay   = blow.StrikeTime - DateTime.Now;
                    delayMs = Convert.ToInt32(delay.TotalMilliseconds);

                    if (delayMs > 0)
                    {
                        await Task.Delay(delayMs, cancellationToken);
                    }

                    if (cancellationToken.IsCancellationRequested)
                    {
                        break;
                    }

                    // Change bell color
                    if (Device.DeviceLoad == DeviceLoad.High)
                    {
                        blow.BellColor = Constants.StruckBellColor;

                        // Confirmed this is needed here
                        StateHasChanged();
                    }

                    // Strike bell
                    await JSRuntime.InvokeVoidAsync("PlayBellAudio", blow.AudioId);
                }
            }
            else if (screen.PlayLabel == "Stop")
            {
                cancellationTokenSource.Cancel();
                initialDelay = 0;
            }

            // Initial delay
            if (initialDelay > 0)
            {
                await Task.Delay(initialDelay);
            }

            // Start spinner
            screen.PlayDisabled   = true;
            screen.PlayLabel      = "Wait";
            screen.SpinnerPlaying = true;
            StateHasChanged();

            // Wait 2.6 or 1.6 further seconds for the sound to finish, depending on whether arriving here
            // on stop or end of play
            await Task.Delay(2600 - initialDelay);

            // Reset play button
            screen.SpinnerPlaying = false;
            screen.PlayLabel      = "Play";
            screen.PlayDisabled   = false;

            // Reset screen
            if (Device.DeviceLoad == DeviceLoad.High)
            {
                blowSet.SetUnstruck();
            }
            else
            {
                blowSet.Blows.Last().BellColor = Constants.UnstruckTestBellColor;
            }

            testSpec.ControlsDisabled    = false;
            testSpec.TenorWeightDisabled = TenorWeightSelect.TenorWeightDisabled(testSpec.Stage);
            StateHasChanged();
        }
        async Task Submit(CallbackParam callbackParam)
        {
            if (testSpec.SelectedTest != 0 && testSpec.SelectedTest != -1)
            {
                ABResult aBResult;

                // Set the applicable submit button to Wait with a spinner
                switch (callbackParam)
                {
                case CallbackParam.AHasErrors:
                    testSpec.SpinnerSubmitting1 = true;
                    testSpec.SubmitLabel1       = "Wait";
                    aBResult = ABResult.AHasErrors;
                    break;

                case CallbackParam.BHasErrors:
                    testSpec.SpinnerSubmitting2 = true;
                    testSpec.SubmitLabel2       = "Wait";
                    aBResult = ABResult.BHasErrors;
                    break;

                case CallbackParam.DontKnow:
                    testSpec.SpinnerSubmitting3 = true;
                    testSpec.SubmitLabel3       = "Wait";
                    aBResult = ABResult.DontKnow;
                    break;

                default:
                    aBResult = ABResult.DontKnow;
                    break;
                }

                testSpec.ControlsDisabled    = true;
                testSpec.TenorWeightDisabled = true;
                screenA.PlayDisabled         = true;
                screenB.PlayDisabled         = true;
                StateHasChanged();

                // Create a TestSubmission object
                TestSubmission testSubmission = new TestSubmission()
                {
                    UserId   = string.Empty,
                    TestType = "A/B Test",
                    TestId   = testSpec.SelectedTest,
                    TestDate = DateTime.Now,
                    Gap      = 0,
                    ABResult = aBResult
                };

                // Push the testSubmission to the API in JSON format
                await TJBarnesService.GetHttpClient().PostAsJsonAsync("api/testsubmissions", testSubmission);

                switch (callbackParam)
                {
                case CallbackParam.AHasErrors:
                    testSpec.SpinnerSubmitting1 = false;
                    testSpec.Submitted1         = true;
                    break;

                case CallbackParam.BHasErrors:
                    testSpec.SpinnerSubmitting2 = false;
                    testSpec.Submitted2         = true;
                    break;

                case CallbackParam.DontKnow:
                    testSpec.SpinnerSubmitting3 = false;
                    testSpec.Submitted3         = true;
                    break;

                default:
                    break;
                }

                StateHasChanged();

                await Task.Delay(1000);

                switch (callbackParam)
                {
                case CallbackParam.AHasErrors:
                    testSpec.Submitted1   = false;
                    testSpec.SubmitLabel1 = "A has errors";
                    break;

                case CallbackParam.BHasErrors:
                    testSpec.Submitted2   = false;
                    testSpec.SubmitLabel2 = "B has errors";
                    break;

                case CallbackParam.DontKnow:
                    testSpec.Submitted3   = false;
                    testSpec.SubmitLabel3 = "I can't tell which has errors";
                    break;

                default:
                    break;
                }

                testSpec.ControlsDisabled    = false;
                testSpec.TenorWeightDisabled = TenorWeightSelect.TenorWeightDisabled(testSpec.Stage);
                screenA.PlayDisabled         = false;
                screenB.PlayDisabled         = false;
                StateHasChanged();
            }
            else
            {
                testSpec.ShowGaps      = true;
                testSpec.ResultEntered = true;

                if (callbackParam == CallbackParam.AHasErrors || callbackParam == CallbackParam.BHasErrors)
                {
                    switch (callbackParam)
                    {
                    case CallbackParam.AHasErrors:
                        if (testSpec.AHasErrors == true)
                        {
                            testSpec.ResultSource = "/audio/right.mp3";
                        }
                        else
                        {
                            testSpec.ResultSource = "/audio/wrong.mp3";
                        }

                        break;

                    case CallbackParam.BHasErrors:
                        if (testSpec.AHasErrors == true)
                        {
                            testSpec.ResultSource = "/audio/wrong.mp3";
                        }
                        else
                        {
                            testSpec.ResultSource = "/audio/right.mp3";
                        }

                        break;

                    default:
                        break;
                    }

                    testSpec.ResultSound = true;

                    // Wait for 1.5 seconds for the sound to finish
                    await Task.Delay(1500);

                    testSpec.ResultSound = false;
                }
            }
        }
        async void PlayB()
        {
            DateTime strikeTime = DateTime.Now;

            foreach (Blow blow in blowSetB.Blows)
            {
                // Set strike times
                strikeTime      = strikeTime.AddMilliseconds(blow.Gap);
                blow.StrikeTime = strikeTime;
            }

            screenB.PlayDisabled         = true;
            screenB.PlayLabel            = "Wait";
            testSpec.ControlsDisabled    = true;
            testSpec.TenorWeightDisabled = true;
            screenA.PlayDisabled         = true;

            // Start the animation if not showing the bells
            if (testSpec.ShowGaps == false)
            {
                screenB.RunAnimation = true;
            }

            TimeSpan delay;
            int      delayMs;

            foreach (Blow blow in blowSetB.Blows)
            {
                delay   = blow.StrikeTime - DateTime.Now;
                delayMs = Convert.ToInt32(delay.TotalMilliseconds);

                if (delayMs > 0)
                {
                    await Task.Delay(delayMs);
                }

                // Strike bell
                await JSRuntime.InvokeVoidAsync("PlayBellAudio", blow.AudioId);
            }

            // Initial delay
            await Task.Delay(1000);

            // Start spinner
            screenB.SpinnerPlaying = true;
            StateHasChanged();

            // Wait 1.6 further seconds for the sound to finish
            await Task.Delay(1600);

            // Reset animation
            if (testSpec.ShowGaps == false)
            {
                screenB.RunAnimation = false;
            }

            // Reset play button
            screenB.SpinnerPlaying = false;
            screenB.PlayLabel      = "Play B";
            screenB.PlayDisabled   = false;

            // Reset screen
            testSpec.ControlsDisabled    = false;
            testSpec.TenorWeightDisabled = TenorWeightSelect.TenorWeightDisabled(testSpec.Stage);
            screenA.PlayDisabled         = false;
            StateHasChanged();
        }
        async Task PlayAsyncB()
        {
            int initialDelay = 1000;

            if (screenB.PlayLabel == "Play B")
            {
                DateTime strikeTime = DateTime.Now;

                foreach (Blow blow in blowSetB.Blows)
                {
                    // Set strike times
                    strikeTime      = strikeTime.AddMilliseconds(blow.Gap);
                    blow.StrikeTime = strikeTime;
                }

                cancellationTokenSource = new CancellationTokenSource();
                cancellationToken       = cancellationTokenSource.Token;

                screenB.PlayLabel            = "Stop B";
                testSpec.ControlsDisabled    = true;
                testSpec.TenorWeightDisabled = true;
                screenA.PlayDisabled         = true;

                // Start the animation if not showing the bells
                if (testSpec.ShowGaps == false)
                {
                    screenB.RunAnimation = true;
                }

                TimeSpan delay;
                int      delayMs;

                foreach (Blow blow in blowSetB.Blows)
                {
                    delay   = blow.StrikeTime - DateTime.Now;
                    delayMs = Convert.ToInt32(delay.TotalMilliseconds);

                    if (delayMs > 0)
                    {
                        await Task.Delay(delayMs, cancellationToken);
                    }

                    if (cancellationToken.IsCancellationRequested)
                    {
                        break;
                    }

                    // Change bell color
                    if (Device.DeviceLoad == DeviceLoad.High && testSpec.ShowGaps == true)
                    {
                        blow.BellColor = Constants.StruckBellColor;

                        // Confirmed this is needed here
                        StateHasChanged();
                    }

                    // Strike bell
                    await JSRuntime.InvokeVoidAsync("PlayBellAudio", blow.AudioId);
                }
            }
            else if (screenB.PlayLabel == "Stop B")
            {
                cancellationTokenSource.Cancel();
                initialDelay = 0;
            }

            // Initial delay
            if (initialDelay > 0)
            {
                // Test was not stopped
                await Task.Delay(initialDelay);
            }
            else
            {
                // Reset animation immediately when test is stopped
                if (testSpec.ShowGaps == false)
                {
                    screenB.RunAnimation = false;
                }
            }

            // Start spinner
            screenB.PlayDisabled   = true;
            screenB.PlayLabel      = "Wait";
            screenB.SpinnerPlaying = true;
            StateHasChanged();

            // Wait 2.6 or 1.6 further seconds for the sound to finish, depending on whether arriving here
            // on stop or end of play
            await Task.Delay(2600 - initialDelay);

            // Reset animation, unless it was already reset earlier after a stop
            if (initialDelay > 0 && testSpec.ShowGaps == false)
            {
                screenB.RunAnimation = false;
            }

            // Reset play button
            screenB.SpinnerPlaying = false;
            screenB.PlayLabel      = "Play B";
            screenB.PlayDisabled   = false;

            // Reset screen
            if (Device.DeviceLoad == DeviceLoad.High && testSpec.ShowGaps == true)
            {
                blowSetB.SetUnstruck();
            }

            testSpec.ControlsDisabled    = false;
            testSpec.TenorWeightDisabled = TenorWeightSelect.TenorWeightDisabled(testSpec.Stage);
            screenA.PlayDisabled         = false;
            StateHasChanged();
        }