feat: implemented global tempo (bpm) track
This commit is contained in:
+208
-1
@@ -7,6 +7,8 @@ import {
|
||||
} from '../core/global-track';
|
||||
import { KGGlobalRegion } from '../core/region/KGGlobalRegion';
|
||||
import { KGKeySignatureRegion } from '../core/region/KGKeySignatureRegion';
|
||||
import { KGAudioRegion } from '../core/region/KGAudioRegion';
|
||||
import { KGTempoRegion } from '../core/region/KGTempoRegion';
|
||||
|
||||
export const DEFAULT_MARKER_REGION_NAME = 'Marker';
|
||||
|
||||
@@ -34,7 +36,7 @@ export function getSongEndBeat(project: KGProject): number {
|
||||
}
|
||||
|
||||
export function findGlobalTrackByType(project: KGProject, type: GlobalTrackType): KGGlobalTrack | null {
|
||||
return project.getGlobalTracks().find(track => track.getType() === type) ?? null;
|
||||
return project.getGlobalTracks?.().find(track => track.getType() === type) ?? null;
|
||||
}
|
||||
|
||||
export function findGlobalTrackContainingRegion(
|
||||
@@ -90,6 +92,29 @@ export function getSongEndBar(project: KGProject): number {
|
||||
return project.getMaxBars();
|
||||
}
|
||||
|
||||
export function getSortedTempoRegions(track: KGGlobalTrack, beatsPerBar: number): KGTempoRegion[] {
|
||||
return track.getRegions()
|
||||
.filter((region): region is KGTempoRegion => region instanceof KGTempoRegion)
|
||||
.map((region) => {
|
||||
region.syncBarsFromBeats(beatsPerBar);
|
||||
region.syncBeatsFromBars(beatsPerBar);
|
||||
return region;
|
||||
})
|
||||
.sort((left, right) => left.getStartBar() - right.getStartBar());
|
||||
}
|
||||
|
||||
export function cloneTempoRegions(regions: KGTempoRegion[], beatsPerBar: number): KGTempoRegion[] {
|
||||
return regions.map(region => new KGTempoRegion(
|
||||
region.getId(),
|
||||
region.getTrackId(),
|
||||
region.getTrackIndex(),
|
||||
region.getBpm(),
|
||||
region.getStartBar(),
|
||||
region.getLengthBars(),
|
||||
beatsPerBar
|
||||
));
|
||||
}
|
||||
|
||||
export function getSortedKeySignatureRegions(track: KGGlobalTrack, beatsPerBar: number): KGKeySignatureRegion[] {
|
||||
return track.getRegions()
|
||||
.filter((region): region is KGKeySignatureRegion => region instanceof KGKeySignatureRegion)
|
||||
@@ -130,10 +155,192 @@ export function findKeySignatureRegionAtBeat(project: KGProject, beat: number):
|
||||
return findKeySignatureRegionAtBar(project, bar);
|
||||
}
|
||||
|
||||
export function findTempoRegionAtBar(project: KGProject, bar: number): KGTempoRegion | null {
|
||||
const track = findGlobalTrackByType(project, GlobalTrackType.Tempo);
|
||||
if (!track) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const beatsPerBar = project.getTimeSignature().numerator;
|
||||
return getSortedTempoRegions(track, beatsPerBar)
|
||||
.find(region => bar >= region.getStartBar() && bar < region.getEndBar()) ?? null;
|
||||
}
|
||||
|
||||
export function findTempoRegionAtBeat(project: KGProject, beat: number): KGTempoRegion | null {
|
||||
const beatsPerBar = project.getTimeSignature().numerator;
|
||||
const bar = Math.floor(beat / beatsPerBar);
|
||||
return findTempoRegionAtBar(project, bar);
|
||||
}
|
||||
|
||||
export function getEffectiveKeySignatureAtBeat(project: KGProject, beat: number): KeySignature {
|
||||
return findKeySignatureRegionAtBeat(project, beat)?.getKeySignature() ?? project.getKeySignature();
|
||||
}
|
||||
|
||||
export function getEffectiveBpmAtBar(project: KGProject, bar: number): number {
|
||||
return findTempoRegionAtBar(project, bar)?.getBpm() ?? project.getBpm();
|
||||
}
|
||||
|
||||
export function getEffectiveBpmAtBeat(project: KGProject, beat: number): number {
|
||||
return findTempoRegionAtBeat(project, beat)?.getBpm() ?? project.getBpm();
|
||||
}
|
||||
|
||||
export function getClampedKeySignatureRegionEndBar(region: KGKeySignatureRegion, maxBars: number): number {
|
||||
return Math.max(region.getStartBar(), Math.min(region.getEndBar(), maxBars));
|
||||
}
|
||||
|
||||
export function getClampedTempoRegionEndBar(region: KGTempoRegion, maxBars: number): number {
|
||||
return Math.max(region.getStartBar(), Math.min(region.getEndBar(), maxBars));
|
||||
}
|
||||
|
||||
export function normalizeTempoRegionsForProject(project: KGProject): void {
|
||||
const track = findGlobalTrackByType(project, GlobalTrackType.Tempo);
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
|
||||
const beatsPerBar = project.getTimeSignature().numerator;
|
||||
const songEndBar = getSongEndBar(project);
|
||||
const regions = getSortedTempoRegions(track, beatsPerBar);
|
||||
if (regions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized: KGTempoRegion[] = [];
|
||||
let currentBar = 0;
|
||||
|
||||
for (const region of regions) {
|
||||
if (currentBar >= songEndBar) {
|
||||
break;
|
||||
}
|
||||
|
||||
const startBar = Math.max(currentBar, region.getStartBar());
|
||||
const endBar = Math.max(startBar + 1, Math.min(region.getEndBar(), songEndBar));
|
||||
if (endBar <= startBar) {
|
||||
continue;
|
||||
}
|
||||
|
||||
normalized.push(new KGTempoRegion(
|
||||
region.getId(),
|
||||
track.getId(),
|
||||
track.getTrackIndex(),
|
||||
region.getBpm(),
|
||||
startBar,
|
||||
endBar - startBar,
|
||||
beatsPerBar
|
||||
));
|
||||
currentBar = endBar;
|
||||
}
|
||||
|
||||
if (normalized.length === 0) {
|
||||
track.setRegions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstRegion = normalized[0];
|
||||
if (firstRegion.getStartBar() > 0) {
|
||||
firstRegion.setBarRange(0, firstRegion.getEndBar(), beatsPerBar);
|
||||
}
|
||||
|
||||
const lastRegion = normalized[normalized.length - 1];
|
||||
if (lastRegion.getEndBar() < songEndBar) {
|
||||
lastRegion.setLengthBars(songEndBar - lastRegion.getStartBar(), beatsPerBar);
|
||||
} else if (lastRegion.getEndBar() > songEndBar) {
|
||||
lastRegion.setLengthBars(songEndBar - lastRegion.getStartBar(), beatsPerBar);
|
||||
}
|
||||
|
||||
track.setRegions(normalized);
|
||||
}
|
||||
|
||||
export function beatToSeconds(project: KGProject, beat: number): number {
|
||||
const clampedBeat = Math.max(0, beat);
|
||||
const timeSignature = project.getTimeSignature?.();
|
||||
const beatsPerBar = timeSignature?.numerator ?? 4;
|
||||
const tempoTrack = findGlobalTrackByType(project, GlobalTrackType.Tempo);
|
||||
if (!tempoTrack) {
|
||||
return clampedBeat * 60 / project.getBpm();
|
||||
}
|
||||
|
||||
const tempoRegions = getSortedTempoRegions(tempoTrack, beatsPerBar);
|
||||
if (tempoRegions.length === 0) {
|
||||
return clampedBeat * 60 / project.getBpm();
|
||||
}
|
||||
|
||||
let seconds = 0;
|
||||
let traversedBeat = 0;
|
||||
|
||||
for (const region of tempoRegions) {
|
||||
const regionStartBeat = region.getStartBar() * beatsPerBar;
|
||||
const regionEndBeat = region.getEndBar() * beatsPerBar;
|
||||
const effectiveStartBeat = Math.max(traversedBeat, regionStartBeat);
|
||||
if (clampedBeat <= effectiveStartBeat) {
|
||||
return seconds;
|
||||
}
|
||||
|
||||
const coveredEndBeat = Math.min(clampedBeat, regionEndBeat);
|
||||
if (coveredEndBeat > effectiveStartBeat) {
|
||||
seconds += (coveredEndBeat - effectiveStartBeat) * (60 / region.getBpm());
|
||||
}
|
||||
|
||||
if (clampedBeat <= regionEndBeat) {
|
||||
return seconds;
|
||||
}
|
||||
|
||||
traversedBeat = regionEndBeat;
|
||||
}
|
||||
|
||||
return seconds + Math.max(0, clampedBeat - traversedBeat) * (60 / project.getBpm());
|
||||
}
|
||||
|
||||
export function beatRangeToSeconds(project: KGProject, startBeat: number, endBeat: number): number {
|
||||
if (endBeat <= startBeat) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return beatToSeconds(project, endBeat) - beatToSeconds(project, startBeat);
|
||||
}
|
||||
|
||||
export function secondsToBeat(project: KGProject, seconds: number): number {
|
||||
const clampedSeconds = Math.max(0, seconds);
|
||||
const timeSignature = project.getTimeSignature?.();
|
||||
const beatsPerBar = timeSignature?.numerator ?? 4;
|
||||
const tempoTrack = findGlobalTrackByType(project, GlobalTrackType.Tempo);
|
||||
if (!tempoTrack) {
|
||||
return clampedSeconds / (60 / project.getBpm());
|
||||
}
|
||||
|
||||
const tempoRegions = getSortedTempoRegions(tempoTrack, beatsPerBar);
|
||||
if (tempoRegions.length === 0) {
|
||||
return clampedSeconds / (60 / project.getBpm());
|
||||
}
|
||||
|
||||
let remainingSeconds = clampedSeconds;
|
||||
let traversedBeat = 0;
|
||||
|
||||
for (const region of tempoRegions) {
|
||||
const regionStartBeat = region.getStartBar() * beatsPerBar;
|
||||
const regionEndBeat = region.getEndBar() * beatsPerBar;
|
||||
const effectiveStartBeat = Math.max(traversedBeat, regionStartBeat);
|
||||
const regionDurationSeconds = (regionEndBeat - effectiveStartBeat) * (60 / region.getBpm());
|
||||
|
||||
if (remainingSeconds <= regionDurationSeconds) {
|
||||
return effectiveStartBeat + (remainingSeconds / (60 / region.getBpm()));
|
||||
}
|
||||
|
||||
remainingSeconds -= regionDurationSeconds;
|
||||
traversedBeat = regionEndBeat;
|
||||
}
|
||||
|
||||
return traversedBeat + remainingSeconds / (60 / project.getBpm());
|
||||
}
|
||||
|
||||
export function getAudioRegionDisplayLengthBeats(project: KGProject, region: KGAudioRegion): number {
|
||||
if (!project.getTimeSignature?.() || !project.getBpm?.()) {
|
||||
return region.getLength();
|
||||
}
|
||||
|
||||
const startBeat = region.getStartFromBeat();
|
||||
const beatBoundSeconds = beatRangeToSeconds(project, startBeat, startBeat + region.getLength());
|
||||
const availableAudioSeconds = Math.max(0, region.getAudioDurationSeconds() - region.getClipStartOffsetSeconds());
|
||||
const visibleSeconds = Math.min(beatBoundSeconds, availableAudioSeconds);
|
||||
return Math.max(0, secondsToBeat(project, beatToSeconds(project, startBeat) + visibleSeconds) - startBeat);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user