audio-source-composer
Version:
Audio Source Composer
598 lines (475 loc) • 22 kB
JavaScript
import * as React from "react";
import {ASUIMenuAction} from "../../components/";
import PromptManager from "../../common/prompt/PromptManager";
import TrackInstructionRowIterator from "./instruction/TrackInstructionRowIterator";
import {InstructionIterator, Values} from "../../song";
import ASCTrackBase from "./ASCTrackBase";
import ASCTrackRenderer from "./ASCTrackRenderer";
// TODO: ASCTrackRowContainer
export default class ASCTrackActions extends ASCTrackRenderer {
getStorageState() {
return {
rowOffset: this.state.rowOffset,
cursorOffset: this.state.cursorOffset,
viewMode: this.state.viewMode,
}
}
getComposer() { return this.props.composer; }
getSong() { return this.props.composer.song; }
getTrackName() { return this.props.trackName; }
getSelectedIndices() { return this.state.selectedIndices || []; }
getCursorOffset() { return this.state.cursorOffset || 0; }
getRowOffset() { return this.state.rowOffset || 0; }
getRowLength() { return typeof this.state.rowLength !== "undefined" ? this.state.rowLength : ASCTrackBase.DEFAULT_ROW_LENGTH; }
getTimeDivision() { return this.state.timeDivision || this.props.composer.song.data.timeDivision; }
getQuantizationTicks() { return this.state.quantizationTicks || this.getTimeDivision(); }
getBeatsPerMinute() { return this.state.beatsPerMinute || this.props.composer.song.data.beatsPerMinute; }
getBeatsPerMeasure() { return this.state.beatsPerMeasure || this.props.composer.song.data.beatsPerMeasure || ASCTrackBase.DEFAULT_BEATS_PER_MEASURE; }
getMeasuresPerSegment() { return this.state.measuresPerSegment || ASCTrackBase.DEFAULT_MEASURES_PER_SEGMENT; }
getBeatsPerSegment() { return this.getBeatsPerMeasure() * this.getMeasuresPerSegment(); }
getSegmentLengthTicks() { return this.getBeatsPerSegment() * this.getTimeDivision(); }
// getProgramID() { return this.state.programID; }
// getCursorOffset() { return this.state.cursorOffset || 0; }
// getCursorPositionTicks() { return this.state.cursorPositionTicks || 0; }
getPlayingIndices() { return this.state.playingIndices || []; }
getDestinationList() { return this.state.destinationList || []; }
getTrackLengthTicks() { return this.state.trackLengthTicks || null; }
getSegmentInfo() { return this.state.segmentInfo || [[0,0]]; }
getStartPosition() { return this.state.startPosition || 0; }
getSongPosition() { return this.state.songPosition || 0; }
isSelectedTrack() {
return this.trackName === this.props.composer.getSelectedTrackName();
}
/** TODO: calculate correct destination **/
// getDestination() {
// if(this.destination)
// return this.destination;
// console.warn('TODO: calculate correct destination');
// return this.destination = this.getComposer().getAudioContext();
// }
cursorGetInfo(cursorOffset=null) {
return this.getCursorInfo(cursorOffset || this.getCursorOffset());
}
// getPositionInfo(positionTicks) {
// return this.getTrackState().getPositionInfo(positionTicks);
// }
// getTrackState() {
// return new TrackState(this.getComposer(), this.getTrackName());
// }
/** Playback **/
playSelectedInstructions() {
return this.playInstructions(this.getSelectedIndices());
}
playInstructions(selectedIndices, stopPlayback=true) {
// console.log("ASCTrack.playInstructions", selectedIndices);
this.getComposer().trackPlay(this.getTrackName(), selectedIndices, stopPlayback)
}
/** Focus **/
focus() {
this.focusRowContainer();
}
focusRowContainer() {
// TODO: if selected?
// console.log('focusRowContainer');
this.ref.rowContainer.current.focus();
}
/** Actions **/
openContextMenu(e, options=null) {
if(e.defaultPrevented || e.altKey)
return;
e.preventDefault();
const state = {
menuOpen: true,
menuOptions: options,
}
if(options)
state.menuOptions = options;
if(e)
state.clientPosition = [e.clientX, e.clientY];
// console.log(e, e.type, 'openContextMenu', state)
this.setState(state)
}
closeContextMenu() {
this.setState({
menuOpen: false,
menuOptions: null,
clientPosition: null,
})
}
toggleViewMode(e) {
let viewMode = this.state.viewMode;
viewMode = viewMode === 'minimize' ? null : 'minimize';
this.setViewMode(viewMode);
}
setViewMode(viewMode) {
this.setState({
viewMode
})
}
setCursorOffset(cursorOffset) {
if(cursorOffset < 0)
cursorOffset = 0;
this.setState({cursorOffset}, () =>
this.focusRowContainer());
}
setRowOffset(rowOffset) {
if(rowOffset < 0)
rowOffset = 0;
// console.log('rowOffset', rowOffset);
this.setState({rowOffset}, () =>
this.focusRowContainer());
}
adjustRowOffset(cursorOffset=null) {
cursorOffset = cursorOffset === null ? this.state.cursorOffset : cursorOffset;
const trackState = this.getTrackState();
const cursorInfo = trackState.getCursorInfo(cursorOffset);
const rowLength = this.getRowLength();
if(cursorInfo.cursorRow > this.getRowOffset() + (rowLength - 1))
this.setRowOffset(cursorInfo.cursorRow - (rowLength - 1))
else if(cursorInfo.cursorRow < this.getRowOffset())
this.setRowOffset(cursorInfo.cursorRow)
// else
// console.log("No adjustment: ", cursorInfo, cursorOffset);
}
instructionPasteAtCursor() {
let {positionTicks: startPositionTicks} = this.getCursorInfo(this.getCursorOffset());
this.getComposer().instructionPasteAtPosition(this.getTrackName(), startPositionTicks);
}
instructionInsertAtCursor(newCommandString, newCommandParams) {
let {positionTicks: startPositionTicks} = this.getCursorInfo(this.getCursorOffset());
this.getComposer().instructionInsertAtPosition(this.getTrackName(), startPositionTicks, newCommandString, newCommandParams);
}
// trackGetState(trackName) {
// return new TrackState(this, trackName);
// }
/** Update Rendering Stats **/
updateRenderingStats() {
const segmentLengthTicks = this.getSegmentLengthTicks();
// const rowOffset = this.getRowOffset();
/** Seek to track end **/
const iterator = this.getIterator();
iterator.seekToEnd();
const trackLengthTicks = iterator.getPositionInTicks() + segmentLengthTicks * 2;
/** Calculate segments **/
const segmentInfo = [];
const rowIterator = this.getRowIterator();
// let lastSegmentPositionTicks = 0;
let nextSegmentPositionTicks = 0;
while(true) {
// const positionTicks = rowIterator.getPositionInTicks();
// if(positionTicks >= trackLengthTicks
// && segmentInfo.length >= ASCTrackBase.DEFAULT_MIN_SEGMENTS)
// break;
// if(segmentInfo.length >= ASCTrackBase.DEFAULT_MAX_SEGMENTS)
// break;
const instructionData = rowIterator.nextCursorPosition();
if(Array.isArray(instructionData)) {
} else {
// Row
const currentRowOffset = rowIterator.getRowCount();
const currentPositionTicks = rowIterator.getPositionInTicks();
if(currentPositionTicks > trackLengthTicks)
break;
if(nextSegmentPositionTicks <= currentPositionTicks) {
// console.log('segmentInfo', currentRowOffset, currentPositionTicks, rowIterator.getPositionInSeconds())
// lastSegmentPositionTicks += segmentLengthTicks;
segmentInfo.push([currentRowOffset, rowIterator.getPositionInSeconds()]);
nextSegmentPositionTicks += segmentLengthTicks;
}
}
}
this.setState({
trackLengthTicks,
segmentInfo
});
}
/** Position **/
updateSongPosition(songPosition) {
const [low, high] = this.renderStats.songPositionRowRange || [0, 0];
if(songPosition < low || songPosition > high) {
// console.log('songPosition < low || songPosition > high', this.getTrackName(), {songPosition, low, high});
const state = {
songPosition
}
const autoScrollToCursor = !this.props.selected
if(autoScrollToCursor) {
let lastSegmentRowOffset = null; //, lastSegmentPositionSeconds=null;
for(let i=0; i<this.state.segmentInfo.length; i++) {
const [segmentRowOffset, segmentPositionSeconds] = this.state.segmentInfo[i];
if(songPosition < segmentPositionSeconds) {
// const perc = (songPosition - lastSegmentPositionSeconds) / (segmentPositionSeconds - lastSegmentPositionSeconds);
// console.log('perc', perc);
break;
}
lastSegmentRowOffset = segmentRowOffset;
// lastSegmentPositionSeconds = segmentPositionSeconds;
}
if(lastSegmentRowOffset !== null)
state.rowOffset = lastSegmentRowOffset;
}
// console.log('updateSongPosition', this.getTrackName(), this.state.segmentInfo, state);
this.setState(state);
} else {
// console.warn('updateSongPosition', this.getTrackName(), this.renderStats.songPositionRange, playbackPositionInSeconds);
}
}
/** Selection **/
selectActive() {
this.getComposer().trackSelect(this.getTrackName());
}
// selectIndicesAndPlay(selectedIndices, clearSelection=true, stopPlayback=true) {
// selectedIndices = this.selectIndices(selectedIndices, clearSelection);
// this.playInstructions(selectedIndices, stopPlayback)
// }
selectIndices(selectedIndices, clearSelection=true, playback=true) {
// console.log('selectedIndices', selectedIndices);
const composer = this.getComposer();
const values = Values.instance;
if (typeof selectedIndices === "string") {
switch (selectedIndices) {
case 'all':
selectedIndices = [];
const maxLength = this.getSong().instructionGetList(this.getTrackName()).length;
for (let i = 0; i < maxLength; i++)
selectedIndices.push(i);
break;
case 'cursor':
const {cursorIndex} = this.cursorGetInfo();
selectedIndices = [cursorIndex];
break;
case 'segment':
const {segmentID} = this.cursorGetInfo();
selectedIndices = this.getComposer()
.instructionGetIndicesInRange(
this.getTrackName(),
segmentID * this.getSegmentLengthTicks(),
(segmentID + 1) * this.getSegmentLengthTicks(),
)
break;
// selectedIndices = [].map.call(this.querySelectorAll('asct-instruction'), (elm => elm.index));
case 'row':
const {positionTicks} = this.cursorGetInfo();
selectedIndices = this.getComposer()
.instructionGetIndicesInRange(
this.getTrackName(),
positionTicks,
positionTicks+1,
)
break;
// throw new Error("Invalid selection: " + selectedIndices);
default:
selectedIndices = selectedIndices.split(/[^0-9]/).map(index => parseInt(index));
}
}
selectedIndices = values.parseSelectedIndices(selectedIndices);
switch(clearSelection) {
case false:
if (this.getSelectedIndices().length > 0)
selectedIndices = values.filterAndSort(selectedIndices.concat(this.getSelectedIndices()));
break;
default:
case true:
break;
case 'toggle':
if (this.getSelectedIndices().length > 0) {
const toggledSelectedIndices = this.getSelectedIndices().slice();
for(const selectedIndex of selectedIndices) {
const p = toggledSelectedIndices.indexOf(selectedIndex);
if(p === -1) {
toggledSelectedIndices.push(selectedIndex);
} else {
toggledSelectedIndices.splice(p, 1);
}
}
selectedIndices = values.filterAndSort(toggledSelectedIndices);
}
break;
}
// const viewKey = this.getTrackViewKey();
composer.trackSelect(this.getTrackName(), selectedIndices);
if(composer.state.playbackOnSelect && playback && (selectedIndices.length > 0)) try {
composer.trackPlay(this.getTrackName(), selectedIndices, false);
} catch (e) {
console.error("Error in playback: ", e);
}
this.setState({
selectedIndices
});
return selectedIndices;
}
async selectIndicesPrompt() {
let selectedIndices = this.getSelectedIndices().join(', ');
selectedIndices = await PromptManager.openPromptDialog(`Select indices for track ${this.getTrackName()}: `, selectedIndices);
this.selectIndices(selectedIndices);
}
updatePlayingIndices(playingIndices) {
this.setState({
playingIndices
});
}
changeQuantization(quantizationTicks) {
if(typeof quantizationTicks === "string")
quantizationTicks = Values.instance.parseDurationAsTicks(quantizationTicks, this.getTimeDivision())
if(typeof quantizationTicks !== "number")
throw new Error("Invalid quantizationTicks: " + quantizationTicks);
this.setState({
quantizationTicks
}, () => this.updateRenderingStats());
}
async changeQuantizationPrompt() {
let quantizationTicks = await PromptManager.openPromptDialog("Enter a quantization:", Values.instance.formatDuration(this.getQuantizationTicks()));
if (typeof quantizationTicks === 'string')
quantizationTicks = Values.instance.parseDurationAsTicks(quantizationTicks, this.getTimeDivision());
this.changeQuantization(quantizationTicks);
}
changeRowLength(rowLength = null) {
if(typeof rowLength !== "number")
throw new Error("Invalid quantizationTicks");
this.setState({
rowLength
}, () => this.updateRenderingStats());
}
async changeRowLengthPrompt() {
const rowLength = await PromptManager.openPromptDialog("Enter a length in rows:", this.state.rowLength);
this.changeRowLength(Number.parseInt(rowLength));
}
/** Menu **/
renderMenuViewOptions() {
const oldViewMode = this.state.viewMode;
return (<>
<ASUIMenuAction disabled={!oldViewMode} onAction={e => this.setViewMode(null)} >Default</ASUIMenuAction>
<ASUIMenuAction disabled={oldViewMode === 'minimize'} onAction={e => this.setViewMode('minimize')} >Minimize</ASUIMenuAction>
<ASUIMenuAction disabled={oldViewMode === 'none'} onAction={e => this.setViewMode('none')} >Hide</ASUIMenuAction>
<ASUIMenuAction disabled={oldViewMode === 'float'} onAction={e => this.setViewMode('float')} >Float Right</ASUIMenuAction>
</>);
}
renderMenuSetQuantization(title = "Select Quantization") {
const quantizationTicks = this.getQuantizationTicks();
return Values.instance.renderMenuSelectDuration(
durationTicks => this.changeQuantization(durationTicks),
this.getTimeDivision(),
quantizationTicks,
title
);
}
renderMenuSetRowLength() {
return (<>
{Values.instance.getTrackerLengthInRows((length, title) =>
<ASUIMenuAction key={length} onAction={(e) => this.changeRowLength(length)}>{title}</ASUIMenuAction>
)}
<ASUIMenuAction onAction={(e) => this.changeRowLengthPrompt()} >Custom Length</ASUIMenuAction>
</>);
}
// renderMenuSetProgramFilter() {
// return this.renderMenuSelectSongProgram(programID => this.trackerChangeProgramFilter(programID));
// }
/** Track Cursor Position **/
/**
* Used when selecting
* @param {Integer} cursorOffset
* @returns {{cursorRow: null, positionTicks: null, nextCursorOffset: *, previousCursorOffset: number, positionSeconds: number, cursorIndex: null}}
*/
getCursorInfo(cursorOffset) {
if(!Number.isInteger(cursorOffset))
throw new Error("Invalid cursorOffset: " + cursorOffset);
// cursorOffset = cursorOffset === null ? trackState.cursorOffset : cursorOffset;
const iterator = this.getRowIterator();
const ret = {
segmentID: null,
cursorIndex: null,
cursorRow: null,
nextCursorOffset: cursorOffset + 1,
previousCursorOffset: cursorOffset > 0 ? cursorOffset - 1 : 0,
positionTicks: null,
positionSeconds: 0,
// cursorRowLow: cursorRow - this.getRowLength(),
// cursorRowHigh: cursorRow - 1,
};
let lastRowPositions=[], positions=[[0]];
// let indexFound = null;
while(positions.length < 3 || positions[2][0] <= cursorOffset) {
const instructionData = iterator.nextCursorPosition();
lastRowPositions.push(iterator.getCursorPosition());
if(cursorOffset === iterator.getCursorPosition()) {
ret.cursorRow = iterator.getRowCount();
ret.positionTicks = iterator.getPositionInTicks();
ret.positionSeconds = iterator.getPositionInSeconds();
}
if(Array.isArray(instructionData)) {
if(cursorOffset === iterator.getCursorPosition()) {
// ret.positionTicks = iterator.getPositionInTicks();
if (iterator.getIndex() !== null)
ret.cursorIndex = iterator.getIndex();
}
} else {
positions.push(lastRowPositions);
if(positions.length > 3)
positions.shift();
lastRowPositions = [];
}
}
const column = positions[1].indexOf(cursorOffset);
ret.nextRowOffset = positions[2][column] || positions[2][positions[2].length-1];
ret.previousRowOffset = positions[0][column] || 0;
if(ret.positionTicks !== null) {
ret.segmentID = Math.floor(ret.positionTicks / this.getSegmentLengthTicks());
}
// console.log(cursorOffset, ret);
return ret;
}
getPositionInfo(positionTicks) {
if(!Number.isInteger(positionTicks))
throw new Error("Invalid positionTicks: " + positionTicks);
const iterator = this.getRowIterator();
iterator.seekToPositionTicks(positionTicks)
// let indexFound = null;
// while(iterator.getPositionInTicks() < positionTicks) {
// iterator.nextQuantizedInstructionRow();
// }
const ret = {
positionTicks,
positionIndex: iterator.getIndex(),
positionSeconds: iterator.getPositionInSeconds(),
cursorOffset: iterator.getCursorPosition(),
rowCount: iterator.getRowCount(),
}
// console.info('getPositionInfo', ret);
return ret;
}
/** Iterator **/
getRowIterator(timeDivision=null, beatsPerMinute=null, quantizationTicks=null) {
const song = this.props.composer.getSong();
timeDivision = timeDivision || this.getTimeDivision();
beatsPerMinute = beatsPerMinute || this.getBeatsPerMinute();
quantizationTicks = quantizationTicks || this.getQuantizationTicks();
return TrackInstructionRowIterator.getIteratorFromSong(
song,
this.props.trackName,
{
quantizationTicks,
timeDivision,
beatsPerMinute,
}
)
}
getIterator(timeDivision=null, beatsPerMinute=null) {
const song = this.props.composer.getSong();
timeDivision = timeDivision || this.getTimeDivision();
beatsPerMinute = beatsPerMinute || this.getBeatsPerMinute();
return InstructionIterator.getIteratorFromSong(
song,
this.props.trackName,
{
timeDivision, // || this.getSong().data.timeDivision,
beatsPerMinute, // || this.getSong().data.beatsPerMinute
}
)
}
/** Format **/
formatTrackDuration(durationTicks) {
return Values.instance.formatDuration(durationTicks, this.getTimeDivision())
}
formatDurationAsDecimal(durationTicks, fractionDigits=2) {
return Values.instance.formatDurationAsDecimal(durationTicks, this.getTimeDivision(), fractionDigits)
}
}