UNPKG

echogarden

Version:

An easy-to-use speech toolset. Includes tools for synthesis, recognition, alignment, speech translation, language detection, source separation and more.

211 lines 9.38 kB
import { logToStderr } from '../utilities/Utilities.js'; const log = logToStderr; export function alignDTWWindowed(sequence1, sequence2, costFunction, windowMaxLength, centerIndexes) { windowMaxLength = Math.max(windowMaxLength, 2); if (sequence1.length == 0 || sequence2.length == 0) { return { path: [], pathCost: 0 }; } // Compute accumulated cost matrix (transposed) const { accumulatedCostMatrixTransposed, windowStartOffsets } = computeAccumulatedCostMatrixTransposed(sequence1, sequence2, costFunction, windowMaxLength, centerIndexes); // Find best path for the computed matrix const path = computeBestPathTransposed(accumulatedCostMatrixTransposed, windowStartOffsets); // Best path cost is the bottom right element of the matrix const columnCount = accumulatedCostMatrixTransposed.length; const rowCount = accumulatedCostMatrixTransposed[0].length; const pathCost = accumulatedCostMatrixTransposed[columnCount - 1][rowCount - 1]; // Return return { path, pathCost }; } function computeAccumulatedCostMatrixTransposed(sequence1, sequence2, costFunction, windowMaxLength, centerIndexes) { const halfWindowMaxLength = Math.floor(windowMaxLength / 2); const columnCount = sequence1.length; const rowCount = Math.min(windowMaxLength, sequence2.length); const accumulatedCostMatrixTransposed = new Array(columnCount); // Initialize an array to store window start offsets const windowStartOffsets = new Int32Array(columnCount); // Compute accumulated cost matrix column by column for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) { // Create new column and add it to the matrix const currentColumn = new Float32Array(rowCount); accumulatedCostMatrixTransposed[columnIndex] = currentColumn; // Compute window center, or use given one let windowCenter; if (centerIndexes) { windowCenter = centerIndexes[columnIndex]; } else { windowCenter = Math.floor((columnIndex / columnCount) * sequence2.length); } // Compute window start and end offsets let windowStartOffset = Math.max(windowCenter - halfWindowMaxLength, 0); let windowEndOffset = windowStartOffset + rowCount; if (windowEndOffset > sequence2.length) { windowEndOffset = sequence2.length; windowStartOffset = windowEndOffset - rowCount; } // Store the start offset for this column windowStartOffsets[columnIndex] = windowStartOffset; // Get target sequence1 value const targetSequence1Value = sequence1[columnIndex]; // If this is the first column, fill it only using the 'up' neighbors if (columnIndex == 0) { for (let rowIndex = 1; rowIndex < rowCount; rowIndex++) { const cost = costFunction(targetSequence1Value, sequence2[windowStartOffset + rowIndex]); const upCost = currentColumn[rowIndex - 1]; currentColumn[rowIndex] = cost + upCost; } continue; } // If not first column // Store the column to the left const leftColumn = accumulatedCostMatrixTransposed[columnIndex - 1]; // Compute the delta between the current window start offset // and left column's window offset const windowOffsetDelta = windowStartOffset - windowStartOffsets[columnIndex - 1]; // Compute the accumulated cost for all rows in the window for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) { // Compute the cost for current cell const cost = costFunction(targetSequence1Value, sequence2[windowStartOffset + rowIndex]); // Retrieve the cost for the 'up' (insertion) neighbor let upCost = Infinity; if (rowIndex > 0) { upCost = currentColumn[rowIndex - 1]; } // Retrieve the cost for the 'left' (deletion) neighbor let leftCost = Infinity; const leftRowIndex = rowIndex + windowOffsetDelta; if (leftRowIndex < rowCount) { leftCost = leftColumn[leftRowIndex]; } // Retrieve the cost for the 'up and left' (match) neighbor let upAndLeftCost = Infinity; const upAndLeftRowIndex = leftRowIndex - 1; if (upAndLeftRowIndex >= 0 && upAndLeftRowIndex < rowCount) { upAndLeftCost = leftColumn[upAndLeftRowIndex]; } // Find the minimum of all neighbors let minimumNeighborCost = minimumOf3(upCost, leftCost, upAndLeftCost); // If all neighbors are infinity, then it means there is a "jump" between the window // of the current column and the left column, and they don't have overlapping rows. // In this case, only the cost of the current cell will be used if (minimumNeighborCost === Infinity) { minimumNeighborCost = 0; } // Write cost + minimum neighbor cost to the current column currentColumn[rowIndex] = cost + minimumNeighborCost; } } return { accumulatedCostMatrixTransposed, windowStartOffsets }; } function computeBestPathTransposed(accumulatedCostMatrixTransposed, windowStartOffsets) { const columnCount = accumulatedCostMatrixTransposed.length; const rowCount = accumulatedCostMatrixTransposed[0].length; const bestPath = []; // Start at the bottom right corner and find the best path // towards the top left let columnIndex = columnCount - 1; let rowIndex = rowCount - 1; while (columnIndex > 0 || rowIndex > 0) { const windowStartIndex = windowStartOffsets[columnIndex]; const windowStartDelta = columnIndex > 0 ? windowStartIndex - windowStartOffsets[columnIndex - 1] : 0; // Add the current cell to the best path bestPath.push({ source: columnIndex, dest: windowStartIndex + rowIndex }); // Retrieve the cost for the 'up' (insertion) neighbor const upRowIndex = rowIndex - 1; let upCost = Infinity; if (upRowIndex >= 0) { upCost = accumulatedCostMatrixTransposed[columnIndex][upRowIndex]; } // Retrieve the cost for the 'left' (deletion) neighbor const leftRowIndex = rowIndex + windowStartDelta; const leftColumnIndex = columnIndex - 1; let leftCost = Infinity; if (leftColumnIndex >= 0 && leftRowIndex < rowCount) { leftCost = accumulatedCostMatrixTransposed[leftColumnIndex][leftRowIndex]; } // Retrieve the cost for the 'up and left' (match) neighbor const upAndLeftRowIndex = rowIndex - 1 + windowStartDelta; const upAndLeftColumnIndex = columnIndex - 1; let upAndLeftCost = Infinity; if (upAndLeftColumnIndex >= 0 && upAndLeftRowIndex >= 0 && upAndLeftRowIndex < rowCount) { upAndLeftCost = accumulatedCostMatrixTransposed[upAndLeftColumnIndex][upAndLeftRowIndex]; } // If all neighbors have a cost of infinity, it means // there is a "jump" between the window for the current and previous column if (upCost == Infinity && leftCost == Infinity && upAndLeftCost == Infinity) { // In that case: // // If there are rows above if (upRowIndex >= 0) { // Move upward rowIndex = upRowIndex; } else if (leftColumnIndex >= 0) { // Otherwise, move to the left columnIndex = leftColumnIndex; } else { // Since we know that either columnIndex > 0 or rowIndex > 0, // one of these directions must be available. // This error should never happen throw new Error(`Unexpected state: columnIndex: ${columnIndex}, rowIndex: ${rowIndex}`); } } else { // Choose the direction with the smallest cost const smallestCostDirection = argIndexOfMinimumOf3(upCost, leftCost, upAndLeftCost); if (smallestCostDirection == 1) { // Move upward rowIndex = upRowIndex; // The upper column index stays the same } else if (smallestCostDirection == 2) { // Move to the left rowIndex = leftRowIndex; columnIndex = leftColumnIndex; } else { // Move upward and to the left rowIndex = upAndLeftRowIndex; columnIndex = upAndLeftColumnIndex; } } } bestPath.push({ source: 0, dest: 0 }); return bestPath.reverse(); } function minimumOf3(x1, x2, x3) { if (x1 <= x2 && x1 <= x3) { return x1; } else if (x2 <= x3) { return x2; } else { return x3; } } function argIndexOfMinimumOf3(x1, x2, x3) { if (x1 <= x2 && x1 <= x3) { return 1; } else if (x2 <= x3) { return 2; } else { return 3; } } //# sourceMappingURL=DTWSequenceAlignmentWindowed.js.map