diff --git a/package-lock.json b/package-lock.json
index 27116f0a2..c4aaa5ce9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@coderline/alphatab-monorepo",
- "version": "1.9.0",
+ "version": "1.8.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@coderline/alphatab-monorepo",
- "version": "1.9.0",
+ "version": "1.8.3",
"workspaces": [
"packages/*"
],
@@ -5539,7 +5539,7 @@
},
"packages/alphatab": {
"name": "@coderline/alphatab",
- "version": "1.9.0",
+ "version": "1.8.3",
"license": "MPL-2.0",
"devDependencies": {
"@biomejs/biome": "^2.4.15",
@@ -5575,7 +5575,7 @@
},
"packages/alphatex": {
"name": "@coderline/alphatab-alphatex",
- "version": "1.9.0",
+ "version": "1.8.3",
"devDependencies": {
"rimraf": "^6.1.3",
"tslib": "^2.8.1",
@@ -5600,7 +5600,7 @@
},
"packages/csharp": {
"name": "@coderline/alphatab-csharp",
- "version": "1.9.0",
+ "version": "1.8.3",
"devDependencies": {
"@coderline/alphatab-transpiler": "*",
"rimraf": "^6.1.3",
@@ -5623,14 +5623,14 @@
},
"packages/kotlin": {
"name": "@coderline/alphatab-kotlin",
- "version": "1.9.0"
+ "version": "1.8.3"
},
"packages/lsp": {
"name": "@coderline/alphatab-language-server",
- "version": "1.9.0",
+ "version": "1.8.3",
"license": "MPL-2.0",
"dependencies": {
- "@coderline/alphatab": "^1.9.0",
+ "@coderline/alphatab": "^1.8.3",
"vscode-languageserver": "^9.0.1",
"vscode-languageserver-textdocument": "^1.0.12"
},
@@ -5664,11 +5664,11 @@
},
"packages/monaco": {
"name": "@coderline/alphatab-monaco",
- "version": "1.9.0",
+ "version": "1.8.3",
"license": "MPL-2.0",
"dependencies": {
- "@coderline/alphatab": "^1.9.0",
- "@coderline/alphatab-language-server": "^1.9.0",
+ "@coderline/alphatab": "^1.8.3",
+ "@coderline/alphatab-language-server": "^1.8.3",
"monaco-editor": "^0.55.1",
"vscode-languageserver-types": "^3.17.5",
"vscode-oniguruma": "^2.0.1",
@@ -5704,7 +5704,7 @@
},
"packages/playground": {
"name": "@coderline/alphatab-playground",
- "version": "1.9.0",
+ "version": "1.8.3",
"dependencies": {
"@coderline/alphatab": "*",
"@fontsource/noto-sans": "^5.2.10",
@@ -5739,7 +5739,7 @@
},
"packages/tooling": {
"name": "@coderline/alphatab-tooling",
- "version": "1.9.0",
+ "version": "1.8.3",
"devDependencies": {
"@microsoft/api-extractor": "^7.57.7",
"@rollup/plugin-terser": "^1.0.0",
@@ -5767,7 +5767,7 @@
},
"packages/transpiler": {
"name": "@coderline/alphatab-transpiler",
- "version": "1.9.0",
+ "version": "1.8.3",
"devDependencies": {
"typescript": "^6.0.3",
"vitest": "^4.1.7"
@@ -5789,7 +5789,7 @@
},
"packages/vite": {
"name": "@coderline/alphatab-vite",
- "version": "1.9.0",
+ "version": "1.8.3",
"license": "MPL-2.0",
"dependencies": {
"magic-string": "^0.30.21"
@@ -5857,7 +5857,7 @@
},
"packages/webpack": {
"name": "@coderline/alphatab-webpack",
- "version": "1.9.0",
+ "version": "1.8.3",
"license": "MPL-2.0",
"dependencies": {
"webpack": "^5.105.4"
diff --git a/package.json b/package.json
index db1f0fb39..f99b8f37f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@coderline/alphatab-monorepo",
- "version": "1.9.0",
+ "version": "1.8.4",
"description": "Monorepo for alphaTab and its related packages",
"private": true,
"type": "module",
diff --git a/packages/alphatab/package.json b/packages/alphatab/package.json
index 81cc1d244..b6026717c 100644
--- a/packages/alphatab/package.json
+++ b/packages/alphatab/package.json
@@ -1,6 +1,6 @@
{
"name": "@coderline/alphatab",
- "version": "1.9.0",
+ "version": "1.8.4",
"description": "alphaTab is a music notation and guitar tablature rendering library",
"keywords": [
"guitar",
diff --git a/packages/alphatab/src/midi/MidiFileGenerator.ts b/packages/alphatab/src/midi/MidiFileGenerator.ts
index dacb3ac59..7649d0b89 100644
--- a/packages/alphatab/src/midi/MidiFileGenerator.ts
+++ b/packages/alphatab/src/midi/MidiFileGenerator.ts
@@ -853,12 +853,18 @@ export class MidiFileGenerator {
// e.g. the eighth notes on a 4/4 time signature must start exactly on the following
// times to get a triplet feel applied
// 0 480 960 1440 1920 2400 2880 3360
- if (beatStart % interval !== 0) {
+ const pairSlot = interval * 2;
+ if (beatStart % pairSlot !== 0) {
return null;
}
// ensure next beat matches spec
- if (!beat.nextBeat || beat.nextBeat.voice !== beat.voice || beat.playbackDuration !== interval) {
+ if (
+ !beat.nextBeat ||
+ beat.nextBeat.voice !== beat.voice ||
+ beat.nextBeat.playbackDuration !== interval ||
+ beat.nextBeat.playbackStart !== beatStart + interval
+ ) {
return null;
}
diff --git a/packages/alphatab/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts b/packages/alphatab/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts
index 204aef546..0198c896a 100644
--- a/packages/alphatab/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts
+++ b/packages/alphatab/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts
@@ -220,6 +220,13 @@ export class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase {
public override play(): void {
super.play();
const ctx = this.context!;
+
+ // clear any pending events buffered from previous playback rounds
+ // we just want the events which come in after the play call until the worklet is created
+ if (this._pendingEvents) {
+ this._pendingEvents = undefined;
+ }
+
// create a script processor node which will replace the silence with the generated audio
BrowserUiFacade.createAlphaSynthAudioWorklet(ctx, this._settings).then(
() => {
diff --git a/packages/alphatab/src/rendering/utils/BeamingHelper.ts b/packages/alphatab/src/rendering/utils/BeamingHelper.ts
index 17933a77a..151ad7b26 100644
--- a/packages/alphatab/src/rendering/utils/BeamingHelper.ts
+++ b/packages/alphatab/src/rendering/utils/BeamingHelper.ts
@@ -8,7 +8,6 @@ import type { Note } from '@coderline/alphatab/model/Note';
import type { Staff } from '@coderline/alphatab/model/Staff';
import type { Voice } from '@coderline/alphatab/model/Voice';
import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase';
-import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition';
import { AccidentalHelper } from '@coderline/alphatab/rendering/utils/AccidentalHelper';
import type { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection';
import type { BeamingRuleLookup } from '@coderline/alphatab/rendering/utils/BeamingRuleLookup';
@@ -107,11 +106,7 @@ export class BeamingHelper {
}
public alignWithBeats() {
- for (const v of this.drawingInfos.values()) {
- v.startX = this._renderer.getBeatX(v.startBeat!, BeatXPosition.Stem);
- v.endX = this._renderer.getBeatX(v.endBeat!, BeatXPosition.Stem);
- this.drawingInfos.clear();
- }
+ this.drawingInfos.clear();
}
public finish(): void {
diff --git a/packages/alphatab/test/audio/MidiFileGenerator.test.ts b/packages/alphatab/test/audio/MidiFileGenerator.test.ts
index 9ade098c5..78664f690 100644
--- a/packages/alphatab/test/audio/MidiFileGenerator.test.ts
+++ b/packages/alphatab/test/audio/MidiFileGenerator.test.ts
@@ -59,9 +59,10 @@ describe('MidiFileGeneratorTest', () => {
for (let i: number = 0; i < actualEvents.length; i++) {
Logger.info('Test', `i[${i}] ${actualEvents[i]}`);
if (i < expectedEvents.length) {
- expect(expectedEvents[i].equals(actualEvents[i]), `i[${i}] expected[${expectedEvents[i]}] !== actual[${actualEvents[i]}]`).toBe(
- true
- );
+ expect(
+ expectedEvents[i].equals(actualEvents[i]),
+ `i[${i}] expected[${expectedEvents[i]}] !== actual[${actualEvents[i]}]`
+ ).toBe(true);
}
}
expect(actualEvents.length).toBe(expectedEvents.length);
@@ -614,54 +615,122 @@ describe('MidiFileGeneratorTest', () => {
assertEvents(handler.midiEvents, expectedEvents);
});
- it('triplet-feel', () => {
- const tex: string =
- '\\ts 2 4 \\tf t8 3.2.8*4 | \\tf t16 3.2.16*8 | \\tf d8 3.2.8*4 | \\tf d16 3.2.16*8 | \\tf s8 3.2.8*4 | \\tf s16 3.2.16*8';
- const score: Score = parseTex(tex);
- // prettier-ignore
- const expectedPlaybackStartTimes: number[] = [
- 0, 480, 960, 1440, 0, 240, 480, 720, 960, 1200, 1440, 1680, 0, 480, 960, 1440, 0, 240, 480, 720, 960, 1200,
- 1440, 1680, 0, 480, 960, 1440, 0, 240, 480, 720, 960, 1200, 1440, 1680
- ];
- // prettier-ignore
- const expectedPlaybackDurations: number[] = [
- 480, 480, 480, 480, 240, 240, 240, 240, 240, 240, 240, 240, 480, 480, 480, 480, 240, 240, 240, 240, 240,
- 240, 240, 240, 480, 480, 480, 480, 240, 240, 240, 240, 240, 240, 240, 240
- ];
- const actualPlaybackStartTimes: number[] = [];
- const actualPlaybackDurations: number[] = [];
- let beat: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0];
- while (beat) {
- actualPlaybackStartTimes.push(beat.playbackStart);
- actualPlaybackDurations.push(beat.playbackDuration);
- beat = beat.nextBeat;
- }
- expect(actualPlaybackStartTimes.join(',')).toBe(expectedPlaybackStartTimes.join(','));
- expect(actualPlaybackDurations.join(',')).toBe(expectedPlaybackDurations.join(','));
- // prettier-ignore
- const expectedMidiStartTimes: number[] = [
- 0, 640, 960, 1600, 1920, 2240, 2400, 2720, 2880, 3200, 3360, 3680, 3840, 4560, 4800, 5520, 5760, 6120, 6240,
- 6600, 6720, 7080, 7200, 7560, 7680, 7920, 8640, 8880, 9600, 9720, 10080, 10200, 10560, 10680, 11040, 11160
- ];
- // prettier-ignore
- const expectedMidiDurations: number[] = [
- 640, 320, 640, 320, 320, 160, 320, 160, 320, 160, 320, 160, 720, 240, 720, 240, 360, 120, 360, 120, 360,
- 120, 360, 120, 240, 720, 240, 720, 120, 360, 120, 360, 120, 360, 120, 360
- ];
-
- const actualMidiStartTimes: number[] = [];
- const actualMidiDurations: number[] = [];
- const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator();
- const generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler);
- generator.generate();
- for (const midiEvent of handler.midiEvents) {
- if (midiEvent instanceof FlatNoteEvent) {
- actualMidiStartTimes.push(midiEvent.tick);
- actualMidiDurations.push(midiEvent.length);
+ describe('triplet-feel', () => {
+ function testTripletFeel(
+ tex: string,
+ expectedPlaybackStartTimes: number[],
+ expectedPlaybackDurations: number[],
+ expectedMidiStartTimes: number[],
+ expectedMidiDurations: number[]
+ ) {
+ const score: Score = parseTex(tex);
+
+ const actualPlaybackStartTimes: number[] = [];
+ const actualPlaybackDurations: number[] = [];
+ let beat: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0];
+ while (beat) {
+ actualPlaybackStartTimes.push(beat.playbackStart);
+ actualPlaybackDurations.push(beat.playbackDuration);
+ beat = beat.nextBeat;
}
+ expect(actualPlaybackStartTimes.join(','), 'expectedPlaybackStartTimes').toBe(
+ expectedPlaybackStartTimes.join(',')
+ );
+ expect(actualPlaybackDurations.join(','), 'expectedPlaybackDurations').toBe(
+ expectedPlaybackDurations.join(',')
+ );
+
+ const actualMidiStartTimes: number[] = [];
+ const actualMidiDurations: number[] = [];
+ const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator();
+ const generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler);
+ generator.generate();
+ for (const midiEvent of handler.midiEvents) {
+ if (midiEvent instanceof FlatNoteEvent) {
+ actualMidiStartTimes.push(midiEvent.tick);
+ actualMidiDurations.push(midiEvent.length);
+ }
+ }
+ expect(actualMidiStartTimes.join(','), 'expectedMidiStartTimes').toBe(expectedMidiStartTimes.join(','));
+ expect(actualMidiDurations.join(','), 'expectedMidiDurations').toBe(expectedMidiDurations.join(','));
}
- expect(actualMidiStartTimes.join(',')).toBe(expectedMidiStartTimes.join(','));
- expect(actualMidiDurations.join(',')).toBe(expectedMidiDurations.join(','));
+
+ it('variants', () => {
+ const tex: string =
+ '\\ts 2 4 \\tf t8 3.2.8*4 | \\tf t16 3.2.16*8 | \\tf d8 3.2.8*4 | \\tf d16 3.2.16*8 | \\tf s8 3.2.8*4 | \\tf s16 3.2.16*8';
+ const expectedPlaybackStartTimes: number[] = [
+ 0, 480, 960, 1440, 0, 240, 480, 720, 960, 1200, 1440, 1680, 0, 480, 960, 1440, 0, 240, 480, 720, 960,
+ 1200, 1440, 1680, 0, 480, 960, 1440, 0, 240, 480, 720, 960, 1200, 1440, 1680
+ ];
+ const expectedPlaybackDurations: number[] = [
+ 480, 480, 480, 480, 240, 240, 240, 240, 240, 240, 240, 240, 480, 480, 480, 480, 240, 240, 240, 240, 240,
+ 240, 240, 240, 480, 480, 480, 480, 240, 240, 240, 240, 240, 240, 240, 240
+ ];
+ const expectedMidiStartTimes: number[] = [
+ 0, 640, 960, 1600, 1920, 2240, 2400, 2720, 2880, 3200, 3360, 3680, 3840, 4560, 4800, 5520, 5760, 6120,
+ 6240, 6600, 6720, 7080, 7200, 7560, 7680, 7920, 8640, 8880, 9600, 9720, 10080, 10200, 10560, 10680,
+ 11040, 11160
+ ];
+ const expectedMidiDurations: number[] = [
+ 640, 320, 640, 320, 320, 160, 320, 160, 320, 160, 320, 160, 720, 240, 720, 240, 360, 120, 360, 120, 360,
+ 120, 360, 120, 240, 720, 240, 720, 120, 360, 120, 360, 120, 360, 120, 360
+ ];
+ testTripletFeel(
+ tex,
+ expectedPlaybackStartTimes,
+ expectedPlaybackDurations,
+ expectedMidiStartTimes,
+ expectedMidiDurations
+ );
+ });
+
+ it('not-matching-with-grace', () => {
+ testTripletFeel(
+ `
+ \\tf triplet8th
+ 0.1.8
+ 0.1.8{gr onbeat}
+ 0.1.4
+ 0.1.8
+ 0.1.8
+ 0.1.8
+ 0.1.8
+ 0.1.8 |
+ 0.1.8 * 8
+ `,
+ // no swing on playback start/durations
+ [
+ // Bar 1
+ 0, 480, 600, 1440, 1920, 2400, 2880, 3360,
+ // Bar 2
+ 0, 480, 960, 1440, 1920, 2400, 2880, 3360
+ ],
+ [
+ // Bar 1
+ 480, 120, 840, 480, 480, 480, 480, 480,
+ // Bar 2
+ 480, 480, 480, 480, 480, 480, 480, 480
+ ],
+ // swing on generated midi
+ [
+ // Bar 1
+ // no swing on the first 4 notes as they do not align with the swing definition
+ 0, 480, 600, 1440,
+ // the last two 8th note pairs swing
+ 1920, 2560, 2880, 3520,
+ // Bar 2 fully swings
+ 3840, 4480, 4800, 5440, 5760, 6400, 6720, 7360
+ ],
+ [
+ // no swing
+ 480, 120, 840, 480,
+ // the last two 8th note pairs swing
+ 640, 320, 640, 320,
+ // Swing on second bar
+ 640, 320, 640, 320, 640, 320, 640, 320
+ ]
+ );
+ });
});
it('beat-multi-bend', () => {
diff --git a/packages/alphatex/package.json b/packages/alphatex/package.json
index 514d7434e..8bcf25960 100644
--- a/packages/alphatex/package.json
+++ b/packages/alphatex/package.json
@@ -1,6 +1,6 @@
{
"name": "@coderline/alphatab-alphatex",
- "version": "1.9.0",
+ "version": "1.8.4",
"private": true,
"scripts": {
"lint": "biome lint",
diff --git a/packages/csharp/package.json b/packages/csharp/package.json
index 5682255de..e7234b829 100644
--- a/packages/csharp/package.json
+++ b/packages/csharp/package.json
@@ -1,6 +1,6 @@
{
"name": "@coderline/alphatab-csharp",
- "version": "1.9.0",
+ "version": "1.8.4",
"description": "The C# target of alphaTab.",
"private": true,
"type": "module",
diff --git a/packages/csharp/src/AlphaTab.Windows/NAudioSynthOutput.cs b/packages/csharp/src/AlphaTab.Windows/NAudioSynthOutput.cs
index 01a1b1ff4..d0e3e1bab 100644
--- a/packages/csharp/src/AlphaTab.Windows/NAudioSynthOutput.cs
+++ b/packages/csharp/src/AlphaTab.Windows/NAudioSynthOutput.cs
@@ -26,6 +26,7 @@ public class NAudioSynthOutput : WaveProvider32, ISynthOutput, IDisposable
private int _bufferCount;
private int _requestedBufferCount;
private ISynthOutputDevice? _device;
+ private Float32Array? _readWrapper;
///
public double SampleRate => PreferredSampleRate;
@@ -139,13 +140,18 @@ private void RequestBuffers()
///
public override int Read(float[] buffer, int offset, int count)
{
- var read = new Float32Array(count);
-
- var samplesFromBuffer = (int)_circularBuffer.Read(read, 0,
- System.Math.Min(read.Length, _circularBuffer.Count));
+ // NAudio reuses the same provider buffer across reads, so cache the
+ // Float32Array wrapper to avoid a per-call allocation that otherwise
+ // builds up GC pressure during steady-state playback.
+ var wrapper = _readWrapper;
+ if (wrapper == null || wrapper.Data.Array != buffer)
+ {
+ wrapper = new Float32Array(buffer);
+ _readWrapper = wrapper;
+ }
- Buffer.BlockCopy(read.Data.Array!, read.Data.Offset, buffer, offset * sizeof(float),
- samplesFromBuffer * sizeof(float));
+ var samplesFromBuffer = (int)_circularBuffer.Read(wrapper, offset,
+ System.Math.Min(count, _circularBuffer.Count));
((EventEmitterOfT)SamplesPlayed).Trigger(samplesFromBuffer /
SynthConstants.AudioChannels);
diff --git a/packages/csharp/src/AlphaTab/Core/EcmaScript/Float32Array.cs b/packages/csharp/src/AlphaTab/Core/EcmaScript/Float32Array.cs
index 13fcbaaec..1ddb467b0 100644
--- a/packages/csharp/src/AlphaTab/Core/EcmaScript/Float32Array.cs
+++ b/packages/csharp/src/AlphaTab/Core/EcmaScript/Float32Array.cs
@@ -68,7 +68,7 @@ public void Set(Float32Array subarray, double offset)
System.Buffer.BlockCopy(subarray.Data.Array!,
subarray.Data.Offset * sizeof(float),
Data.Array!,
- Data.Offset + (int)offset * sizeof(float),
+ (Data.Offset + (int)offset) * sizeof(float),
subarray.Data.Count * sizeof(float));
}
diff --git a/packages/csharp/src/Directory.Build.props b/packages/csharp/src/Directory.Build.props
index f8ef9003c..c24782918 100644
--- a/packages/csharp/src/Directory.Build.props
+++ b/packages/csharp/src/Directory.Build.props
@@ -2,8 +2,8 @@
portable
true
- 1.9.0
- 1.9.0.0
+ 1.8.4
+ 1.8.4.0
$(AssemblyVersion)
Danielku15
CoderLine
diff --git a/packages/kotlin/package.json b/packages/kotlin/package.json
index 073763e4e..ef5cb2518 100644
--- a/packages/kotlin/package.json
+++ b/packages/kotlin/package.json
@@ -1,6 +1,6 @@
{
"name": "@coderline/alphatab-kotlin",
- "version": "1.9.0",
+ "version": "1.8.4",
"description": "The Kotlin target of alphaTab.",
"private": true,
"type": "module",
diff --git a/packages/kotlin/src/android/build.gradle.kts b/packages/kotlin/src/android/build.gradle.kts
index 174095eda..3f5af57ab 100644
--- a/packages/kotlin/src/android/build.gradle.kts
+++ b/packages/kotlin/src/android/build.gradle.kts
@@ -39,7 +39,7 @@ var libAuthorId = "danielku15"
var libAuthorName = "Daniel Kuschny"
var libOrgUrl = "https://github.com/coderline"
var libCompany = "CoderLine"
-var libVersion = "1.9.0-SNAPSHOT"
+var libVersion = "1.8.4-SNAPSHOT"
var libProjectUrl = "https://github.com/CoderLine/alphaTab"
var libGitUrlHttp = "https://github.com/CoderLine/alphaTab.git"
var libGitUrlGit = "scm:git:git://github.com/CoderLine/alphaTab.git"
diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidAudioWorker.kt b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidAudioWorker.kt
index 26af6404b..de87df9f9 100644
--- a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidAudioWorker.kt
+++ b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidAudioWorker.kt
@@ -2,7 +2,10 @@ package alphaTab.platform.android
import android.media.*
import java.util.concurrent.*
+import java.util.concurrent.atomic.AtomicLong
import kotlin.contracts.ExperimentalContracts
+import kotlin.math.max
+import kotlin.math.min
@ExperimentalContracts
@ExperimentalUnsignedTypes
@@ -61,9 +64,28 @@ internal class AndroidAudioWorker(
val samplesFromBuffer = _output.read(_buffer, 0, _buffer.size)
if (_previousPosition == -1) {
_previousPosition = _track.playbackHeadPosition
+ _startPosition = _previousPosition
_track.getTimestamp(_timestamp)
}
- _track.write(_buffer, 0, samplesFromBuffer, AudioTrack.WRITE_BLOCKING)
+ val silenceFloats = _buffer.size - samplesFromBuffer
+ if (silenceFloats > 0) {
+ _buffer.fill(0f, samplesFromBuffer, _buffer.size)
+ }
+ // write() may return less than requested (or a negative AudioTrack.ERROR_*
+ // code) when the track is paused/stopped/disconnected mid-write. Only credit
+ // counters for what actually landed in the track to keep them in sync with
+ // playbackHeadPosition.
+ val floatsWritten = _track.write(
+ _buffer, 0, _buffer.size, AudioTrack.WRITE_BLOCKING
+ )
+ if (floatsWritten > 0) {
+ val realFloatsWritten = min(floatsWritten, samplesFromBuffer)
+ val silenceFloatsWritten = floatsWritten - realFloatsWritten
+ _totalFramesWrittenToTrack.addAndGet((floatsWritten / 2).toLong())
+ if (silenceFloatsWritten > 0) {
+ _silenceFramesWrittenToTrack.addAndGet((silenceFloatsWritten / 2).toLong())
+ }
+ }
} else {
_playingSemaphore.acquire() // wait for playing to start
_playingSemaphore.release() // release semaphore for others
@@ -87,7 +109,14 @@ internal class AndroidAudioWorker(
fun play() {
if (_track.playState != AudioTrack.PLAYSTATE_PLAYING) {
- _previousPosition = _track.playbackHeadPosition
+ _previousPosition = -1
+ _startPosition = -1
+ _totalFramesWrittenToTrack.set(0)
+ _silenceFramesWrittenToTrack.set(0)
+ _silenceFramesAccountedAsPlayed = 0
+ _lastTimestampUpdateNanos = -1L
+ _timestamp.nanoTime = 0
+ _timestamp.framePosition = 0
_track.play()
_stopped = false
@@ -110,14 +139,23 @@ internal class AndroidAudioWorker(
}
}
- private var _previousPosition: Int = -1
+ @Volatile private var _previousPosition: Int = -1
+ @Volatile private var _startPosition: Int = -1
+ private val _totalFramesWrittenToTrack = AtomicLong(0)
+ private val _silenceFramesWrittenToTrack = AtomicLong(0)
+ private var _silenceFramesAccountedAsPlayed: Long = 0
private val _timestamp = AudioTimestamp()
- private val _lastTimestampUpdate: Long = -1L
+ private var _lastTimestampUpdateNanos: Long = -1L
private fun onUpdatePlayedSamples() {
- val sinceUpdateInMillis = (System.nanoTime() - _lastTimestampUpdate) / 10e6
- if (sinceUpdateInMillis >= 10000) {
- if (!_track.getTimestamp(_timestamp)) {
+ val now = System.nanoTime()
+ val sinceUpdateMs =
+ if (_lastTimestampUpdateNanos == -1L) Long.MAX_VALUE
+ else (now - _lastTimestampUpdateNanos) / 1_000_000L
+ if (sinceUpdateMs >= 10_000L) {
+ if (_track.getTimestamp(_timestamp)) {
+ _lastTimestampUpdateNanos = now
+ } else {
_timestamp.nanoTime = 0
_timestamp.framePosition = 0
}
@@ -133,12 +171,35 @@ internal class AndroidAudioWorker(
return
}
- val playedSamples = samplePosition - _previousPosition
- if (playedSamples < 0) {
+ val rawDelta = samplePosition - _previousPosition
+ if (rawDelta < 0) {
return
}
-
_previousPosition = samplePosition
- _output.onSamplesPlayed(playedSamples)
+
+ val silenceWritten = _silenceFramesWrittenToTrack.get()
+ if (silenceWritten == 0L) {
+ // Happy path: synth has kept the ring buffer fed the entire session — no silence
+ // has ever been queued. Behavior is bit-identical to the pre-fix logic.
+ if (rawDelta > 0) {
+ _output.onSamplesPlayed(rawDelta)
+ }
+ return
+ }
+
+ // Slow path: writer has silence-padded at least once this session. Compensate for
+ // silence the head has now crossed; mathematically equivalent to capping the
+ // cumulative reported count at the cumulative real frames written.
+ val totalWritten = _totalFramesWrittenToTrack.get()
+ val realWritten = totalWritten - silenceWritten
+ val headFromStart = (samplePosition - _startPosition).toLong()
+ val silencePlayedCum = max(0L, headFromStart - realWritten)
+ val silenceCrossedThisTick = silencePlayedCum - _silenceFramesAccountedAsPlayed
+ _silenceFramesAccountedAsPlayed = silencePlayedCum
+
+ val realDelta = rawDelta.toLong() - silenceCrossedThisTick
+ if (realDelta > 0) {
+ _output.onSamplesPlayed(realDelta.toInt())
+ }
}
}
diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidSynthOutput.kt b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidSynthOutput.kt
index 0bd11f42e..8e40effae 100644
--- a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidSynthOutput.kt
+++ b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidSynthOutput.kt
@@ -32,6 +32,7 @@ internal class AndroidSynthOutput(
private lateinit var _audioContext: AndroidAudioWorker
private lateinit var _circularBuffer: CircularSampleBuffer
+ private var _readWrapper: Float32Array? = null
override val sampleRate: Double
get() = PreferredSampleRate.toDouble()
@@ -108,12 +109,18 @@ internal class AndroidSynthOutput(
}
fun read(buffer: FloatArray, offset: Int, sampleCount: Int): Int {
- val read = Float32Array(sampleCount.toDouble())
- val actual = _circularBuffer.read(read, 0.0, min(read.length, _circularBuffer.count))
-
- read.data.copyInto(buffer, offset, 0, sampleCount)
+ // AndroidAudioWorker has one static buffer which is reused, we can cache the read wrapper
+ var wrapper = _readWrapper
+ if (wrapper == null || wrapper.data !== buffer) {
+ wrapper = Float32Array(buffer)
+ _readWrapper = wrapper
+ }
+ val actual = _circularBuffer.read(
+ wrapper,
+ offset.toDouble(),
+ min(sampleCount.toDouble(), _circularBuffer.count)
+ )
requestBuffers()
-
return actual.toInt()
}
@@ -122,7 +129,7 @@ internal class AndroidSynthOutput(
override val sampleRequest: IEventEmitter = EventEmitter()
override suspend fun enumerateOutputDevices(): List {
- val audioService = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager?
+ val audioService = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager?
?: return List()
return List(
diff --git a/packages/lsp/package.json b/packages/lsp/package.json
index b2984c067..581def52c 100644
--- a/packages/lsp/package.json
+++ b/packages/lsp/package.json
@@ -1,6 +1,6 @@
{
"name": "@coderline/alphatab-language-server",
- "version": "1.9.0",
+ "version": "1.8.4",
"description": "A language server for alphaTab providing coding assistance for alphaTex.",
"keywords": [
"guitar",
@@ -35,7 +35,7 @@
"test-web": "npm run test"
},
"dependencies": {
- "@coderline/alphatab": "^1.9.0",
+ "@coderline/alphatab": "^1.8.4",
"vscode-languageserver": "^9.0.1",
"vscode-languageserver-textdocument": "^1.0.12"
},
diff --git a/packages/monaco/package.json b/packages/monaco/package.json
index 2114d944d..468ffc30e 100644
--- a/packages/monaco/package.json
+++ b/packages/monaco/package.json
@@ -1,6 +1,6 @@
{
"name": "@coderline/alphatab-monaco",
- "version": "1.9.0",
+ "version": "1.8.4",
"description": "A Monaco editor integration for alphaTab providing coding assistance for alphaTex.",
"keywords": [
"guitar",
@@ -32,8 +32,8 @@
"test-web": "npm run test"
},
"dependencies": {
- "@coderline/alphatab": "^1.9.0",
- "@coderline/alphatab-language-server": "^1.9.0",
+ "@coderline/alphatab": "^1.8.4",
+ "@coderline/alphatab-language-server": "^1.8.4",
"monaco-editor": "^0.55.1",
"vscode-languageserver-types": "^3.17.5",
"vscode-oniguruma": "^2.0.1",
diff --git a/packages/playground/package.json b/packages/playground/package.json
index 104c8dcc8..478cdfc78 100644
--- a/packages/playground/package.json
+++ b/packages/playground/package.json
@@ -1,6 +1,6 @@
{
"name": "@coderline/alphatab-playground",
- "version": "1.9.0",
+ "version": "1.8.4",
"description": "A development playground for alphaTab to test features while developing",
"private": true,
"type": "module",
diff --git a/packages/tooling/package.json b/packages/tooling/package.json
index f85362867..f5464ec56 100644
--- a/packages/tooling/package.json
+++ b/packages/tooling/package.json
@@ -1,6 +1,6 @@
{
"name": "@coderline/alphatab-tooling",
- "version": "1.9.0",
+ "version": "1.8.4",
"type": "module",
"description": "Additional build tooling for alphaTab like common build configurations",
"private": true,
diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json
index 0512f3b2d..856238d7c 100644
--- a/packages/transpiler/package.json
+++ b/packages/transpiler/package.json
@@ -1,6 +1,6 @@
{
"name": "@coderline/alphatab-transpiler",
- "version": "1.9.0",
+ "version": "1.8.4",
"type": "module",
"description": "The transpiler toolkit to translate alphaTab to C# and Kotlin",
"private": true,
diff --git a/packages/vite/package.json b/packages/vite/package.json
index 5973d818e..d8dda345d 100644
--- a/packages/vite/package.json
+++ b/packages/vite/package.json
@@ -1,6 +1,6 @@
{
"name": "@coderline/alphatab-vite",
- "version": "1.9.0",
+ "version": "1.8.4",
"description": "A plugin for Vite to bundle alphaTab into your webapps.",
"keywords": [
"guitar",
diff --git a/packages/webpack/package.json b/packages/webpack/package.json
index 28e0efeef..196035e49 100644
--- a/packages/webpack/package.json
+++ b/packages/webpack/package.json
@@ -1,6 +1,6 @@
{
"name": "@coderline/alphatab-webpack",
- "version": "1.9.0",
+ "version": "1.8.4",
"description": "A plugin for WebPack to bundle alphaTab into your webapps.",
"keywords": [
"guitar",