Skip to content

Commit 022a45c

Browse files
committed
Release v1.8.4 (#2767)
2 parents 9d0142e + 71c43af commit 022a45c

22 files changed

Lines changed: 250 additions & 99 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coderline/alphatab-monorepo",
3-
"version": "1.8.3",
3+
"version": "1.8.4",
44
"description": "Monorepo for alphaTab and its related packages",
55
"private": true,
66
"type": "module",

packages/alphatab/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coderline/alphatab",
3-
"version": "1.8.3",
3+
"version": "1.8.4",
44
"description": "alphaTab is a music notation and guitar tablature rendering library",
55
"keywords": [
66
"guitar",

packages/alphatab/src/midi/MidiFileGenerator.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -853,12 +853,18 @@ export class MidiFileGenerator {
853853
// e.g. the eighth notes on a 4/4 time signature must start exactly on the following
854854
// times to get a triplet feel applied
855855
// 0 480 960 1440 1920 2400 2880 3360
856-
if (beatStart % interval !== 0) {
856+
const pairSlot = interval * 2;
857+
if (beatStart % pairSlot !== 0) {
857858
return null;
858859
}
859860

860861
// ensure next beat matches spec
861-
if (!beat.nextBeat || beat.nextBeat.voice !== beat.voice || beat.playbackDuration !== interval) {
862+
if (
863+
!beat.nextBeat ||
864+
beat.nextBeat.voice !== beat.voice ||
865+
beat.nextBeat.playbackDuration !== interval ||
866+
beat.nextBeat.playbackStart !== beatStart + interval
867+
) {
862868
return null;
863869
}
864870

packages/alphatab/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,13 @@ export class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase {
220220
public override play(): void {
221221
super.play();
222222
const ctx = this.context!;
223+
224+
// clear any pending events buffered from previous playback rounds
225+
// we just want the events which come in after the play call until the worklet is created
226+
if (this._pendingEvents) {
227+
this._pendingEvents = undefined;
228+
}
229+
223230
// create a script processor node which will replace the silence with the generated audio
224231
BrowserUiFacade.createAlphaSynthAudioWorklet(ctx, this._settings).then(
225232
() => {

packages/alphatab/src/rendering/utils/BeamingHelper.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type { Note } from '@coderline/alphatab/model/Note';
88
import type { Staff } from '@coderline/alphatab/model/Staff';
99
import type { Voice } from '@coderline/alphatab/model/Voice';
1010
import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase';
11-
import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition';
1211
import { AccidentalHelper } from '@coderline/alphatab/rendering/utils/AccidentalHelper';
1312
import type { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection';
1413
import type { BeamingRuleLookup } from '@coderline/alphatab/rendering/utils/BeamingRuleLookup';
@@ -107,11 +106,7 @@ export class BeamingHelper {
107106
}
108107

109108
public alignWithBeats() {
110-
for (const v of this.drawingInfos.values()) {
111-
v.startX = this._renderer.getBeatX(v.startBeat!, BeatXPosition.Stem);
112-
v.endX = this._renderer.getBeatX(v.endBeat!, BeatXPosition.Stem);
113-
this.drawingInfos.clear();
114-
}
109+
this.drawingInfos.clear();
115110
}
116111

117112
public finish(): void {

packages/alphatab/test/audio/MidiFileGenerator.test.ts

Lines changed: 118 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,10 @@ describe('MidiFileGeneratorTest', () => {
5959
for (let i: number = 0; i < actualEvents.length; i++) {
6060
Logger.info('Test', `i[${i}] ${actualEvents[i]}`);
6161
if (i < expectedEvents.length) {
62-
expect(expectedEvents[i].equals(actualEvents[i]), `i[${i}] expected[${expectedEvents[i]}] !== actual[${actualEvents[i]}]`).toBe(
63-
true
64-
);
62+
expect(
63+
expectedEvents[i].equals(actualEvents[i]),
64+
`i[${i}] expected[${expectedEvents[i]}] !== actual[${actualEvents[i]}]`
65+
).toBe(true);
6566
}
6667
}
6768
expect(actualEvents.length).toBe(expectedEvents.length);
@@ -614,54 +615,122 @@ describe('MidiFileGeneratorTest', () => {
614615
assertEvents(handler.midiEvents, expectedEvents);
615616
});
616617

617-
it('triplet-feel', () => {
618-
const tex: string =
619-
'\\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';
620-
const score: Score = parseTex(tex);
621-
// prettier-ignore
622-
const expectedPlaybackStartTimes: number[] = [
623-
0, 480, 960, 1440, 0, 240, 480, 720, 960, 1200, 1440, 1680, 0, 480, 960, 1440, 0, 240, 480, 720, 960, 1200,
624-
1440, 1680, 0, 480, 960, 1440, 0, 240, 480, 720, 960, 1200, 1440, 1680
625-
];
626-
// prettier-ignore
627-
const expectedPlaybackDurations: number[] = [
628-
480, 480, 480, 480, 240, 240, 240, 240, 240, 240, 240, 240, 480, 480, 480, 480, 240, 240, 240, 240, 240,
629-
240, 240, 240, 480, 480, 480, 480, 240, 240, 240, 240, 240, 240, 240, 240
630-
];
631-
const actualPlaybackStartTimes: number[] = [];
632-
const actualPlaybackDurations: number[] = [];
633-
let beat: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0];
634-
while (beat) {
635-
actualPlaybackStartTimes.push(beat.playbackStart);
636-
actualPlaybackDurations.push(beat.playbackDuration);
637-
beat = beat.nextBeat;
638-
}
639-
expect(actualPlaybackStartTimes.join(',')).toBe(expectedPlaybackStartTimes.join(','));
640-
expect(actualPlaybackDurations.join(',')).toBe(expectedPlaybackDurations.join(','));
641-
// prettier-ignore
642-
const expectedMidiStartTimes: number[] = [
643-
0, 640, 960, 1600, 1920, 2240, 2400, 2720, 2880, 3200, 3360, 3680, 3840, 4560, 4800, 5520, 5760, 6120, 6240,
644-
6600, 6720, 7080, 7200, 7560, 7680, 7920, 8640, 8880, 9600, 9720, 10080, 10200, 10560, 10680, 11040, 11160
645-
];
646-
// prettier-ignore
647-
const expectedMidiDurations: number[] = [
648-
640, 320, 640, 320, 320, 160, 320, 160, 320, 160, 320, 160, 720, 240, 720, 240, 360, 120, 360, 120, 360,
649-
120, 360, 120, 240, 720, 240, 720, 120, 360, 120, 360, 120, 360, 120, 360
650-
];
651-
652-
const actualMidiStartTimes: number[] = [];
653-
const actualMidiDurations: number[] = [];
654-
const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator();
655-
const generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler);
656-
generator.generate();
657-
for (const midiEvent of handler.midiEvents) {
658-
if (midiEvent instanceof FlatNoteEvent) {
659-
actualMidiStartTimes.push(midiEvent.tick);
660-
actualMidiDurations.push(midiEvent.length);
618+
describe('triplet-feel', () => {
619+
function testTripletFeel(
620+
tex: string,
621+
expectedPlaybackStartTimes: number[],
622+
expectedPlaybackDurations: number[],
623+
expectedMidiStartTimes: number[],
624+
expectedMidiDurations: number[]
625+
) {
626+
const score: Score = parseTex(tex);
627+
628+
const actualPlaybackStartTimes: number[] = [];
629+
const actualPlaybackDurations: number[] = [];
630+
let beat: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0];
631+
while (beat) {
632+
actualPlaybackStartTimes.push(beat.playbackStart);
633+
actualPlaybackDurations.push(beat.playbackDuration);
634+
beat = beat.nextBeat;
661635
}
636+
expect(actualPlaybackStartTimes.join(','), 'expectedPlaybackStartTimes').toBe(
637+
expectedPlaybackStartTimes.join(',')
638+
);
639+
expect(actualPlaybackDurations.join(','), 'expectedPlaybackDurations').toBe(
640+
expectedPlaybackDurations.join(',')
641+
);
642+
643+
const actualMidiStartTimes: number[] = [];
644+
const actualMidiDurations: number[] = [];
645+
const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator();
646+
const generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler);
647+
generator.generate();
648+
for (const midiEvent of handler.midiEvents) {
649+
if (midiEvent instanceof FlatNoteEvent) {
650+
actualMidiStartTimes.push(midiEvent.tick);
651+
actualMidiDurations.push(midiEvent.length);
652+
}
653+
}
654+
expect(actualMidiStartTimes.join(','), 'expectedMidiStartTimes').toBe(expectedMidiStartTimes.join(','));
655+
expect(actualMidiDurations.join(','), 'expectedMidiDurations').toBe(expectedMidiDurations.join(','));
662656
}
663-
expect(actualMidiStartTimes.join(',')).toBe(expectedMidiStartTimes.join(','));
664-
expect(actualMidiDurations.join(',')).toBe(expectedMidiDurations.join(','));
657+
658+
it('variants', () => {
659+
const tex: string =
660+
'\\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';
661+
const expectedPlaybackStartTimes: number[] = [
662+
0, 480, 960, 1440, 0, 240, 480, 720, 960, 1200, 1440, 1680, 0, 480, 960, 1440, 0, 240, 480, 720, 960,
663+
1200, 1440, 1680, 0, 480, 960, 1440, 0, 240, 480, 720, 960, 1200, 1440, 1680
664+
];
665+
const expectedPlaybackDurations: number[] = [
666+
480, 480, 480, 480, 240, 240, 240, 240, 240, 240, 240, 240, 480, 480, 480, 480, 240, 240, 240, 240, 240,
667+
240, 240, 240, 480, 480, 480, 480, 240, 240, 240, 240, 240, 240, 240, 240
668+
];
669+
const expectedMidiStartTimes: number[] = [
670+
0, 640, 960, 1600, 1920, 2240, 2400, 2720, 2880, 3200, 3360, 3680, 3840, 4560, 4800, 5520, 5760, 6120,
671+
6240, 6600, 6720, 7080, 7200, 7560, 7680, 7920, 8640, 8880, 9600, 9720, 10080, 10200, 10560, 10680,
672+
11040, 11160
673+
];
674+
const expectedMidiDurations: number[] = [
675+
640, 320, 640, 320, 320, 160, 320, 160, 320, 160, 320, 160, 720, 240, 720, 240, 360, 120, 360, 120, 360,
676+
120, 360, 120, 240, 720, 240, 720, 120, 360, 120, 360, 120, 360, 120, 360
677+
];
678+
testTripletFeel(
679+
tex,
680+
expectedPlaybackStartTimes,
681+
expectedPlaybackDurations,
682+
expectedMidiStartTimes,
683+
expectedMidiDurations
684+
);
685+
});
686+
687+
it('not-matching-with-grace', () => {
688+
testTripletFeel(
689+
`
690+
\\tf triplet8th
691+
0.1.8
692+
0.1.8{gr onbeat}
693+
0.1.4
694+
0.1.8
695+
0.1.8
696+
0.1.8
697+
0.1.8
698+
0.1.8 |
699+
0.1.8 * 8
700+
`,
701+
// no swing on playback start/durations
702+
[
703+
// Bar 1
704+
0, 480, 600, 1440, 1920, 2400, 2880, 3360,
705+
// Bar 2
706+
0, 480, 960, 1440, 1920, 2400, 2880, 3360
707+
],
708+
[
709+
// Bar 1
710+
480, 120, 840, 480, 480, 480, 480, 480,
711+
// Bar 2
712+
480, 480, 480, 480, 480, 480, 480, 480
713+
],
714+
// swing on generated midi
715+
[
716+
// Bar 1
717+
// no swing on the first 4 notes as they do not align with the swing definition
718+
0, 480, 600, 1440,
719+
// the last two 8th note pairs swing
720+
1920, 2560, 2880, 3520,
721+
// Bar 2 fully swings
722+
3840, 4480, 4800, 5440, 5760, 6400, 6720, 7360
723+
],
724+
[
725+
// no swing
726+
480, 120, 840, 480,
727+
// the last two 8th note pairs swing
728+
640, 320, 640, 320,
729+
// Swing on second bar
730+
640, 320, 640, 320, 640, 320, 640, 320
731+
]
732+
);
733+
});
665734
});
666735

667736
it('beat-multi-bend', () => {

packages/alphatex/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coderline/alphatab-alphatex",
3-
"version": "1.8.3",
3+
"version": "1.8.4",
44
"private": true,
55
"scripts": {
66
"lint": "biome lint",

packages/csharp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coderline/alphatab-csharp",
3-
"version": "1.8.3",
3+
"version": "1.8.4",
44
"description": "The C# target of alphaTab.",
55
"private": true,
66
"type": "module",

packages/csharp/src/AlphaTab.Windows/NAudioSynthOutput.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class NAudioSynthOutput : WaveProvider32, ISynthOutput, IDisposable
2626
private int _bufferCount;
2727
private int _requestedBufferCount;
2828
private ISynthOutputDevice? _device;
29+
private Float32Array? _readWrapper;
2930

3031
/// <inheritdoc />
3132
public double SampleRate => PreferredSampleRate;
@@ -139,13 +140,18 @@ private void RequestBuffers()
139140
/// <inheritdoc />
140141
public override int Read(float[] buffer, int offset, int count)
141142
{
142-
var read = new Float32Array(count);
143-
144-
var samplesFromBuffer = (int)_circularBuffer.Read(read, 0,
145-
System.Math.Min(read.Length, _circularBuffer.Count));
143+
// NAudio reuses the same provider buffer across reads, so cache the
144+
// Float32Array wrapper to avoid a per-call allocation that otherwise
145+
// builds up GC pressure during steady-state playback.
146+
var wrapper = _readWrapper;
147+
if (wrapper == null || wrapper.Data.Array != buffer)
148+
{
149+
wrapper = new Float32Array(buffer);
150+
_readWrapper = wrapper;
151+
}
146152

147-
Buffer.BlockCopy(read.Data.Array!, read.Data.Offset, buffer, offset * sizeof(float),
148-
samplesFromBuffer * sizeof(float));
153+
var samplesFromBuffer = (int)_circularBuffer.Read(wrapper, offset,
154+
System.Math.Min(count, _circularBuffer.Count));
149155

150156
((EventEmitterOfT<double>)SamplesPlayed).Trigger(samplesFromBuffer /
151157
SynthConstants.AudioChannels);

packages/csharp/src/AlphaTab/Core/EcmaScript/Float32Array.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public void Set(Float32Array subarray, double offset)
6868
System.Buffer.BlockCopy(subarray.Data.Array!,
6969
subarray.Data.Offset * sizeof(float),
7070
Data.Array!,
71-
Data.Offset + (int)offset * sizeof(float),
71+
(Data.Offset + (int)offset) * sizeof(float),
7272
subarray.Data.Count * sizeof(float));
7373
}
7474

0 commit comments

Comments
 (0)