tuneflow
Version:
Programmable, extensible music composition & arrangement
648 lines (550 loc) • 21.7 kB
text/typescript
import {
AudioPlugin,
AutomationTarget,
AutomationTargetType,
dbToVolumeValue,
Song,
TrackType,
TuneflowPlugin,
} from '../src';
import type { AutomationValue } from '../src';
import { TrackOutputType, TrackSend, TrackSendPosition } from '../src/models/track';
import type { AuxTrackData, InstrumentInfo } from '../src/models/track';
describe('Track-related Tests', () => {
class TestUtilsPlugin extends TuneflowPlugin {}
const testUtilsPlugin = new TestUtilsPlugin();
let song = new Song();
beforeEach(() => {
song = new Song();
// @ts-ignore
song.setPluginContextInternal(testUtilsPlugin);
song.setResolution(480);
song.createTempoChange({
ticks: 0,
bpm: 120,
});
song.createTempoChange({
ticks: 1440,
bpm: 60,
});
});
describe('Create tracks', () => {
it('Creates MIDI track correctly', async () => {
const track = song.createTrack({
type: TrackType.MIDI_TRACK,
});
expect(track.getVolume()).toBeCloseTo(dbToVolumeValue(0));
expect(track.getPan()).toBe(0);
expect(track.getSolo()).toBe(false);
expect(track.getMuted()).toBe(false);
expect(track.getType()).toBe(TrackType.MIDI_TRACK);
});
it('Creates audio track correctly', async () => {
const track = song.createTrack({
type: TrackType.AUDIO_TRACK,
});
expect(track.getVolume()).toBeCloseTo(dbToVolumeValue(0));
expect(track.getPan()).toBe(0);
expect(track.getSolo()).toBe(false);
expect(track.getMuted()).toBe(false);
expect(track.getType()).toBe(TrackType.AUDIO_TRACK);
});
});
describe('Audio tracks', () => {
it('Cannot set instrument or suggested instruments or sampler plugin', async () => {
const track = song.createTrack({
type: TrackType.AUDIO_TRACK,
});
expect(track.getInstrument()).toBeUndefined();
expect(track.getSamplerPlugin()).toBeUndefined();
expect(track.getSuggestedInstruments()).toEqual([]);
track.setInstrument({
program: 32,
isDrum: false,
});
expect(track.getInstrument()).toBeUndefined();
track.setSamplerPlugin(new AudioPlugin('plugin1', 'manufacturer1', 'VST', '1.1'));
expect(track.getSamplerPlugin()).toBeUndefined();
track.createSuggestedInstrument({
program: 64,
isDrum: false,
});
expect(track.getSuggestedInstruments()).toEqual([]);
});
it('Clone audio track correctly', async () => {
const track = song.createTrack({
type: TrackType.AUDIO_TRACK,
});
track.setInstrument({
program: 32,
isDrum: false,
});
const audioFxPlugin = new AudioPlugin('audioplugin1', 'man1', 'VST3', '1.0.0');
track.setAudioPluginAt(1, audioFxPlugin);
track.setSendAt(
2,
new TrackSend({
outputBusRank: 2,
gainLevel: 0.53,
position: TrackSendPosition.PostFader,
muted: true,
}),
);
track.createSuggestedInstrument({
program: 64,
isDrum: false,
});
track.setVolume(0.1);
track.setPan(60);
track.setSolo(true);
track.setMuted(true);
track.setOutput({
type: TrackOutputType.Track,
trackId: 'track2',
});
const clonedTrack = song.cloneTrack(track);
const clonedTrackInstrument = clonedTrack.getInstrument();
expect(clonedTrackInstrument).toBeUndefined();
expect(clonedTrack.getSuggestedInstruments().length).toBe(0);
const clonedTrackSamplerPlugin = clonedTrack.getSamplerPlugin();
expect(clonedTrackSamplerPlugin).toBeUndefined();
const clonedAudioFxPlugin = clonedTrack.getAudioPluginAt(1);
expect(clonedAudioFxPlugin?.matchesTfId(audioFxPlugin.getTuneflowId())).toBe(true);
const clonedSend = clonedTrack.getSendAt(2);
expect(clonedSend.getOutputBusRank()).toBe(2);
expect(clonedSend.getGainLevel()).toBeCloseTo(0.53);
expect(clonedSend.getPosition()).toBe(TrackSendPosition.PostFader);
expect(clonedSend.getMuted()).toBe(true);
expect(clonedTrack.getVolume()).toBeCloseTo(0.1);
expect(clonedTrack.getPan()).toBe(60);
expect(clonedTrack.getSolo()).toBe(true);
expect(clonedTrack.getMuted()).toBe(true);
expect(clonedTrack.getOutput()?.getType()).toBe(TrackOutputType.Track);
expect(clonedTrack.getOutput()?.getTrackId()).toBe('track2');
});
});
describe('MIDI tracks', () => {
it('Clone midi track correctly', async () => {
const track = song.createTrack({
type: TrackType.MIDI_TRACK,
});
track.setInstrument({
program: 32,
isDrum: false,
});
const audioPlugin = new AudioPlugin('plugin1', 'manufacturer1', 'VST', '1.1');
track.setSamplerPlugin(audioPlugin);
const audioFxPlugin = new AudioPlugin('audioplugin1', 'man1', 'VST3', '1.0.0');
track.setAudioPluginAt(1, audioFxPlugin);
track.setSendAt(
2,
new TrackSend({
outputBusRank: 2,
gainLevel: 0.53,
position: TrackSendPosition.PostFader,
muted: true,
}),
);
track.createSuggestedInstrument({
program: 64,
isDrum: false,
});
track.setVolume(0.1);
track.setPan(60);
track.setSolo(true);
track.setMuted(true);
const trackClip = track.createMIDIClip({
clipStartTick: 0,
clipEndTick: 100,
insertClip: true,
});
trackClip.createNote({
pitch: 64,
velocity: 80,
startTick: 1,
endTick: 10,
});
const volumeAutomationTarget = new AutomationTarget(AutomationTargetType.VOLUME);
track.getAutomation().addAutomation(volumeAutomationTarget);
const automationValue = track
.getAutomation()
.getOrCreateAutomationValueById(volumeAutomationTarget.toTfAutomationTargetId());
automationValue.setDisabled(true);
automationValue.addPoint(3, 0.5, false);
const clonedTrack = song.cloneTrack(track);
const clonedTrackInstrument = clonedTrack.getInstrument();
expect(clonedTrackInstrument).toBeTruthy();
expect((clonedTrackInstrument as InstrumentInfo).getProgram()).toBe(32);
expect((clonedTrackInstrument as InstrumentInfo).getIsDrum()).toBe(false);
expect(clonedTrack.getSuggestedInstruments().length).toBe(1);
expect(clonedTrack.getSuggestedInstruments()[0].getProgram()).toBe(64);
expect(clonedTrack.getSuggestedInstruments()[0].getIsDrum()).toBe(false);
const clonedTrackSamplerPlugin = clonedTrack.getSamplerPlugin();
expect(clonedTrackSamplerPlugin).toBeTruthy();
expect(
(clonedTrackSamplerPlugin as AudioPlugin).matchesTfId(audioPlugin.getTuneflowId()),
).toBe(true);
const clonedAudioFxPlugin = clonedTrack.getAudioPluginAt(1);
expect(clonedAudioFxPlugin?.matchesTfId(audioFxPlugin.getTuneflowId())).toBe(true);
const clonedSend = clonedTrack.getSendAt(2);
expect(clonedSend.getOutputBusRank()).toBe(2);
expect(clonedSend.getGainLevel()).toBeCloseTo(0.53);
expect(clonedSend.getPosition()).toBe(TrackSendPosition.PostFader);
expect(clonedSend.getMuted()).toBe(true);
expect(clonedTrack.getVolume()).toBeCloseTo(0.1);
expect(clonedTrack.getPan()).toBe(60);
expect(clonedTrack.getSolo()).toBe(true);
expect(clonedTrack.getMuted()).toBe(true);
expect(clonedTrack.getClips().length).toBe(1);
expect(clonedTrack.getClips()[0].getNotes().length).toBe(1);
expect(clonedTrack.getClips()[0].getNotes()[0].getPitch()).toBe(64);
expect(clonedTrack.getClips()[0].getNotes()[0].getVelocity()).toBe(80);
expect(clonedTrack.getClips()[0].getNotes()[0].getStartTick()).toBe(1);
expect(clonedTrack.getClips()[0].getNotes()[0].getEndTick()).toBe(10);
expect(clonedTrack.getAutomation().getAutomationTargets().length).toBe(1);
expect(clonedTrack.getAutomation().getAutomationTargets()[0].toTfAutomationTargetId()).toBe(
volumeAutomationTarget.toTfAutomationTargetId(),
);
const clonedAutomationValue = clonedTrack
.getAutomation()
.getAutomationValueById(volumeAutomationTarget.toTfAutomationTargetId()) as AutomationValue;
expect(clonedAutomationValue.getDisabled()).toBe(true);
expect(clonedAutomationValue.getPoints().length).toBe(1);
expect(clonedAutomationValue.getPoints()[0].tick).toBe(3);
expect(clonedAutomationValue.getPoints()[0].value).toBeCloseTo(0.5);
});
}); // end of midi tracks.
describe('Aux tracks', () => {
it('Clone aux track correctly', async () => {
const track = song.createTrack({
type: TrackType.AUX_TRACK,
});
(track.getAuxTrackData() as AuxTrackData).setInputBusRank(3);
const audioFxPlugin = new AudioPlugin('audioplugin1', 'man1', 'VST3', '1.0.0');
track.setAudioPluginAt(1, audioFxPlugin);
track.setSendAt(
2,
new TrackSend({
outputBusRank: 2,
gainLevel: 0.53,
position: TrackSendPosition.PostFader,
muted: true,
}),
);
track.setVolume(0.1);
track.setPan(60);
track.setSolo(true);
track.setMuted(true);
const volumeAutomationTarget = new AutomationTarget(AutomationTargetType.VOLUME);
track.getAutomation().addAutomation(volumeAutomationTarget);
const automationValue = track
.getAutomation()
.getOrCreateAutomationValueById(volumeAutomationTarget.toTfAutomationTargetId());
automationValue.setDisabled(true);
automationValue.addPoint(3, 0.5, false);
const clonedTrack = song.cloneTrack(track);
const clonedTrackInstrument = clonedTrack.getInstrument();
expect(clonedTrackInstrument).toBeUndefined();
const clonedTrackSamplerPlugin = clonedTrack.getSamplerPlugin();
expect(clonedTrackSamplerPlugin).toBeUndefined();
const clonedAuxTrackData = clonedTrack.getAuxTrackData();
expect(clonedAuxTrackData?.getInputBusRank()).toBe(3);
const clonedAudioFxPlugin = clonedTrack.getAudioPluginAt(1);
expect(clonedAudioFxPlugin?.matchesTfId(audioFxPlugin.getTuneflowId())).toBe(true);
const clonedSend = clonedTrack.getSendAt(2);
expect(clonedSend.getOutputBusRank()).toBe(2);
expect(clonedSend.getGainLevel()).toBeCloseTo(0.53);
expect(clonedSend.getPosition()).toBe(TrackSendPosition.PostFader);
expect(clonedSend.getMuted()).toBe(true);
expect(clonedTrack.getVolume()).toBeCloseTo(0.1);
expect(clonedTrack.getPan()).toBe(60);
expect(clonedTrack.getSolo()).toBe(true);
expect(clonedTrack.getMuted()).toBe(true);
expect(clonedTrack.getAutomation().getAutomationTargets().length).toBe(1);
expect(clonedTrack.getAutomation().getAutomationTargets()[0].toTfAutomationTargetId()).toBe(
volumeAutomationTarget.toTfAutomationTargetId(),
);
const clonedAutomationValue = clonedTrack
.getAutomation()
.getAutomationValueById(volumeAutomationTarget.toTfAutomationTargetId()) as AutomationValue;
expect(clonedAutomationValue.getDisabled()).toBe(true);
expect(clonedAutomationValue.getPoints().length).toBe(1);
expect(clonedAutomationValue.getPoints()[0].tick).toBe(3);
expect(clonedAutomationValue.getPoints()[0].value).toBeCloseTo(0.5);
});
});
describe('Audio plugins', () => {
it('CRUD audio plugins on a midi track correctly', async () => {
const track = song.createTrack({
type: TrackType.MIDI_TRACK,
});
expect(track.getAudioPluginCount()).toEqual(0);
track.setAudioPluginAt(0, track.createAudioPlugin(AudioPlugin.DEFAULT_SYNTH_TFID));
expect(track.getAudioPluginCount()).toBe(1);
expect(track.getAudioPluginAt(0)?.getTuneflowId()).toBe(AudioPlugin.DEFAULT_SYNTH_TFID);
track.removeAudioPluginAt(0);
expect(track.getAudioPluginCount()).toEqual(0);
track.setAudioPluginAt(2, track.createAudioPlugin(AudioPlugin.DEFAULT_SYNTH_TFID));
expect(track.getAudioPluginAt(0)).toBeUndefined();
expect(track.getAudioPluginAt(2)?.getTuneflowId()).toBe(AudioPlugin.DEFAULT_SYNTH_TFID);
});
it('CRUD audio plugins on an audio trackcorrectly', async () => {
const track = song.createTrack({
type: TrackType.AUDIO_TRACK,
});
expect(track.getAudioPluginCount()).toEqual(0);
track.setAudioPluginAt(0, track.createAudioPlugin(AudioPlugin.DEFAULT_SYNTH_TFID));
expect(track.getAudioPluginCount()).toBe(1);
expect(track.getAudioPluginAt(0)?.getTuneflowId()).toBe(AudioPlugin.DEFAULT_SYNTH_TFID);
track.removeAudioPluginAt(0);
expect(track.getAudioPluginCount()).toEqual(0);
track.setAudioPluginAt(2, track.createAudioPlugin(AudioPlugin.DEFAULT_SYNTH_TFID));
expect(track.getAudioPluginAt(0)).toBeUndefined();
expect(track.getAudioPluginAt(2)?.getTuneflowId()).toBe(AudioPlugin.DEFAULT_SYNTH_TFID);
});
it('Cannot set audio plugin at slot larger than supported', async () => {
const track = song.createTrack({
type: TrackType.MIDI_TRACK,
});
expect(track.getAudioPluginCount()).toEqual(0);
expect(() =>
track.setAudioPluginAt(1000, track.createAudioPlugin(AudioPlugin.DEFAULT_SYNTH_TFID)),
).toThrow();
});
it('Get audio plugin by instance id correctly', async () => {
const track = song.createTrack({
type: TrackType.MIDI_TRACK,
});
expect(track.getAudioPluginCount()).toEqual(0);
const samplerPlugin = track.createAudioPlugin(AudioPlugin.DEFAULT_SYNTH_TFID);
track.setSamplerPlugin(samplerPlugin);
expect(track.getSamplerPlugin()).toBe(
track.getPluginByInstanceId(samplerPlugin.getInstanceId()),
);
const audioPlugin = track.createAudioPlugin(AudioPlugin.DEFAULT_SYNTH_TFID);
track.setAudioPluginAt(1, audioPlugin);
expect(track.getAudioPluginAt(1)).toBe(
track.getPluginByInstanceId(audioPlugin.getInstanceId()),
);
});
}); // End of audio plugins.
describe('Input bus', () => {
it('Sets/Gets/Removes input bus rank correctly', () => {
const track = song.createTrack({
type: TrackType.AUX_TRACK,
});
expect((track.getAuxTrackData() as AuxTrackData).getInputBusRank()).toBe(1);
(track.getAuxTrackData() as AuxTrackData).setInputBusRank(2);
expect((track.getAuxTrackData() as AuxTrackData).getInputBusRank()).toBe(2);
(track.getAuxTrackData() as AuxTrackData).removeInputBus();
expect((track.getAuxTrackData() as AuxTrackData).getInputBusRank()).toBeUndefined();
});
it('Non-aux track does not contain aux track data', () => {
const track = song.createTrack({
type: TrackType.MIDI_TRACK,
});
expect(track.getAuxTrackData()).toBeUndefined();
});
}); // End of input bus.
describe('Sends', () => {
it('Add send correctly', async () => {
const track = song.createTrack({
type: TrackType.AUX_TRACK,
});
expect(track.getSendCount()).toBe(0);
track.setSendAt(
0,
new TrackSend({
outputBusRank: 32,
gainLevel: 1.0,
position: TrackSendPosition.PreFader,
}),
);
expect(track.getSendCount()).toBe(1);
const addedSend = track.getSendAt(0);
expect(addedSend.getOutputBusRank()).toBe(32);
expect(addedSend.getGainLevel()).toBeCloseTo(1.0);
expect(addedSend.getPosition()).toBe(TrackSendPosition.PreFader);
expect(addedSend.getMuted()).toBe(false);
track.setSendAt(
1,
new TrackSend({
outputBusRank: 6,
gainLevel: 0,
position: TrackSendPosition.PostFader,
muted: true,
}),
);
expect(track.getSendCount()).toBe(2);
const addedSend2 = track.getSendAt(1);
expect(addedSend2.getOutputBusRank()).toBe(6);
expect(addedSend2.getGainLevel()).toBeCloseTo(0);
expect(addedSend2.getPosition()).toBe(TrackSendPosition.PostFader);
expect(addedSend2.getMuted()).toBe(true);
});
it('Reject when adding send for master track', async () => {
const masterTrack = song.getMasterTrack();
expect(masterTrack).toBeTruthy();
expect(() =>
masterTrack.setSendAt(
0,
new TrackSend({
outputBusRank: 2,
gainLevel: 0.5,
position: TrackSendPosition.PreFader,
}),
),
).toThrow();
});
it('Reject when setting invalid gain level', async () => {
const track = song.createTrack({
type: TrackType.AUX_TRACK,
});
expect(track.getSendCount()).toBe(0);
expect(() =>
track.setSendAt(
0,
new TrackSend({
outputBusRank: 32,
gainLevel: 1.1,
position: TrackSendPosition.PreFader,
}),
),
).toThrow();
const newSend = new TrackSend({
outputBusRank: 32,
gainLevel: 1.0,
position: TrackSendPosition.PreFader,
});
track.setSendAt(0, newSend);
expect(() => newSend.setGainLevel(-1)).toThrow();
});
it('Adjust send correctly', async () => {
const track = song.createTrack({
type: TrackType.AUX_TRACK,
});
expect(track.getSendCount()).toBe(0);
track.setSendAt(
0,
new TrackSend({
outputBusRank: 31,
gainLevel: 0.5,
position: TrackSendPosition.PreFader,
}),
);
expect(track.getSendCount()).toBe(1);
const addedSend = track.getSendAt(0);
expect(addedSend.getOutputBusRank()).toBe(31);
expect(addedSend.getGainLevel()).toBeCloseTo(0.5);
expect(addedSend.getPosition()).toBe(TrackSendPosition.PreFader);
expect(addedSend.getMuted()).toBe(false);
addedSend.setOutputBusRank(5);
addedSend.setGainLevel(0);
addedSend.setPosition(TrackSendPosition.PostFader);
addedSend.setMuted(true);
expect(addedSend.getOutputBusRank()).toBe(5);
expect(addedSend.getGainLevel()).toBe(0);
expect(addedSend.getPosition()).toBe(TrackSendPosition.PostFader);
expect(addedSend.getMuted()).toBe(true);
});
it('Remove send correctly', async () => {
const track = song.createTrack({
type: TrackType.AUX_TRACK,
});
expect(track.getSendCount()).toBe(0);
track.setSendAt(
0,
new TrackSend({
outputBusRank: 31,
gainLevel: 1.0,
position: TrackSendPosition.PreFader,
}),
);
expect(track.getSendCount()).toBe(1);
expect(track.getSendAt(0)).toBeTruthy();
track.removeSendAt(0);
expect(track.getSendCount()).toBe(0);
expect(track.getSendAt(0)).toBeUndefined();
});
}); // End of sends.
describe('Track output', () => {
it('Gets/Sets track output correctly', async () => {
const track = song.createTrack({
type: TrackType.AUDIO_TRACK,
});
expect(track.getOutput()).toBeUndefined();
track.setOutput({
type: TrackOutputType.Track,
trackId: 'track1',
});
expect(track.getOutput()?.getType()).toBe(TrackOutputType.Track);
expect(track.getOutput()?.getTrackId()).toBe('track1');
});
it('Rejects if updating to non-track output', async () => {
const track = song.createTrack({
type: TrackType.AUDIO_TRACK,
});
track.setOutput({
type: TrackOutputType.Track,
trackId: 'track1',
});
expect(track.getOutput()?.getType()).toBe(TrackOutputType.Track);
expect(track.getOutput()?.getTrackId()).toBe('track1');
expect(() =>
track.setOutput({
type: TrackOutputType.Device,
trackId: 'track1',
}),
).toThrow();
});
it('Removes track output correctly', async () => {
const track = song.createTrack({
type: TrackType.AUDIO_TRACK,
});
track.setOutput({
type: TrackOutputType.Track,
trackId: 'track1',
});
expect(track.getOutput()?.getType()).toBe(TrackOutputType.Track);
expect(track.getOutput()?.getTrackId()).toBe('track1');
track.removeOutput();
expect(track.getOutput()).toBeUndefined();
});
it('Removes track deletes the track outputs that depend on it', async () => {
const track = song.createTrack({
type: TrackType.AUDIO_TRACK,
});
const track2 = song.createTrack({
type: TrackType.AUX_TRACK,
});
track.setOutput({
type: TrackOutputType.Track,
trackId: track2.getId(),
});
expect(track.getOutput()?.getType()).toBe(TrackOutputType.Track);
expect(track.getOutput()?.getTrackId()).toBe(track2.getId());
track2.deleteFromParent();
expect(track.getOutput()).toBeUndefined();
});
it('Rejects when setting output on master track', async () => {
const track = song.getMasterTrack();
expect(() =>
track.setOutput({
type: TrackOutputType.Device,
trackId: 'sometrack',
}),
).toThrow();
});
it('Rejects when setting output to itself', async () => {
const track = song.createTrack({
type: TrackType.MIDI_TRACK,
});
expect(() =>
track.setOutput({
type: TrackOutputType.Track,
trackId: track.getId(),
}),
).toThrow();
});
}); // End of track output.
});