Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/alphatab/src/midi/MidiFileGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
() => {
Expand Down
7 changes: 1 addition & 6 deletions packages/alphatab/src/rendering/utils/BeamingHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
167 changes: 118 additions & 49 deletions packages/alphatab/test/audio/MidiFileGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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', () => {
Expand Down
18 changes: 12 additions & 6 deletions packages/csharp/src/AlphaTab.Windows/NAudioSynthOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class NAudioSynthOutput : WaveProvider32, ISynthOutput, IDisposable
private int _bufferCount;
private int _requestedBufferCount;
private ISynthOutputDevice? _device;
private Float32Array? _readWrapper;

/// <inheritdoc />
public double SampleRate => PreferredSampleRate;
Expand Down Expand Up @@ -139,13 +140,18 @@ private void RequestBuffers()
/// <inheritdoc />
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<double>)SamplesPlayed).Trigger(samplesFromBuffer /
SynthConstants.AudioChannels);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
Loading
Loading