| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- /**
- * Divisions处理器
- *
- * @description MusicXML divisions是时值计算的基础
- * divisions定义了每个四分音符包含多少个基本时值单位
- *
- * 核心公式:realValue = duration / divisions
- * 其中 realValue 以四分音符为单位(1.0 = 四分音符)
- *
- * @example
- * ```typescript
- * const handler = new DivisionsHandler();
- * handler.setDivisions(256);
- * const realValue = handler.toRealValue(128); // 返回 0.5(八分音符)
- * ```
- */
- /** 默认divisions值(常见值:1, 256, 480, 960) */
- const DEFAULT_DIVISIONS = 256;
- /** 允许的最大divisions值(防止异常大数值导致计算问题) */
- const MAX_DIVISIONS = 10000;
- /** 允许的最小divisions值 */
- const MIN_DIVISIONS = 1;
- /**
- * 时值转换结果
- */
- export interface RealValueResult {
- /** 实际时值(以四分音符为单位) */
- value: number;
- /** 是否有警告 */
- hasWarning: boolean;
- /** 警告信息 */
- warningMessage?: string;
- }
- /**
- * Divisions处理器
- *
- * 负责管理MusicXML的divisions值并提供时值转换功能
- */
- export class DivisionsHandler {
- /** 当前divisions值 */
- private currentDivisions: number = DEFAULT_DIVISIONS;
-
- /** 每个小节的divisions值缓存(小节索引 -> divisions值) */
- private measureDivisionsMap: Map<number, number> = new Map();
-
- /** 是否启用严格模式(严格模式下会抛出错误而不是警告) */
- private strictMode: boolean = false;
- /**
- * 创建Divisions处理器实例
- *
- * @param strictMode 是否启用严格模式(默认false)
- */
- constructor(strictMode: boolean = false) {
- this.strictMode = strictMode;
- }
- /**
- * 设置当前divisions值
- *
- * @param divisions divisions值
- * @throws 当divisions为0且严格模式开启时抛出错误
- */
- setDivisions(divisions: number | null | undefined): void {
- // 处理null/undefined
- if (divisions === null || divisions === undefined) {
- console.warn(`[DivisionsHandler] divisions为${divisions},使用默认值${DEFAULT_DIVISIONS}`);
- this.currentDivisions = DEFAULT_DIVISIONS;
- return;
- }
- // 处理0值
- if (divisions === 0) {
- const errorMsg = 'divisions值不能为0';
- if (this.strictMode) {
- throw new Error(`[DivisionsHandler] ${errorMsg}`);
- }
- console.error(`[DivisionsHandler] ${errorMsg},使用默认值${DEFAULT_DIVISIONS}`);
- this.currentDivisions = DEFAULT_DIVISIONS;
- return;
- }
- // 处理负数
- if (divisions < 0) {
- console.warn(`[DivisionsHandler] divisions为负数(${divisions}),已取绝对值`);
- divisions = Math.abs(divisions);
- }
- // 处理过大值
- if (divisions > MAX_DIVISIONS) {
- console.warn(`[DivisionsHandler] divisions过大(${divisions}),可能存在数据问题`);
- }
- this.currentDivisions = divisions;
- }
- /**
- * 获取当前divisions值
- *
- * @returns 当前divisions值
- */
- getDivisions(): number {
- return this.currentDivisions;
- }
- /**
- * 设置指定小节的divisions值
- *
- * @param measureIndex 小节索引(从0开始)
- * @param divisions divisions值
- */
- setMeasureDivisions(measureIndex: number, divisions: number): void {
- this.measureDivisionsMap.set(measureIndex, divisions);
- // 同时更新当前值
- this.setDivisions(divisions);
- }
- /**
- * 获取指定小节的divisions值
- *
- * @param measureIndex 小节索引(从0开始)
- * @returns 该小节的divisions值,如果未设置则返回当前值
- */
- getMeasureDivisions(measureIndex: number): number {
- return this.measureDivisionsMap.get(measureIndex) ?? this.currentDivisions;
- }
- /**
- * 将MusicXML的duration转换为实际时值
- *
- * @param duration MusicXML中的duration值
- * @returns 实际时值(以四分音符为单位,1.0 = 四分音符)
- *
- * @example
- * ```typescript
- * handler.setDivisions(256);
- * handler.toRealValue(256); // 1.0 (四分音符)
- * handler.toRealValue(128); // 0.5 (八分音符)
- * handler.toRealValue(512); // 2.0 (二分音符)
- * handler.toRealValue(384); // 1.5 (附点四分音符)
- * ```
- */
- toRealValue(duration: number): number {
- // 处理null/undefined
- if (duration === null || duration === undefined) {
- console.warn(`[DivisionsHandler] duration为${duration},返回0`);
- return 0;
- }
- // 处理负数
- if (duration < 0) {
- console.warn(`[DivisionsHandler] duration为负数(${duration}),已取绝对值`);
- duration = Math.abs(duration);
- }
- return duration / this.currentDivisions;
- }
- /**
- * 将MusicXML的duration转换为实际时值(带详细结果)
- *
- * @param duration MusicXML中的duration值
- * @returns 包含时值和警告信息的结果对象
- */
- toRealValueWithInfo(duration: number): RealValueResult {
- let hasWarning = false;
- let warningMessage: string | undefined;
- // 处理null/undefined
- if (duration === null || duration === undefined) {
- warningMessage = `duration为${duration},返回0`;
- return { value: 0, hasWarning: true, warningMessage };
- }
- // 处理负数
- if (duration < 0) {
- warningMessage = `duration为负数(${duration}),已取绝对值`;
- hasWarning = true;
- duration = Math.abs(duration);
- }
- const value = duration / this.currentDivisions;
- return { value, hasWarning, warningMessage };
- }
- /**
- * 将实际时值转换回MusicXML的duration
- *
- * @param realValue 实际时值(以四分音符为单位)
- * @returns MusicXML中的duration值
- */
- toDuration(realValue: number): number {
- return Math.round(realValue * this.currentDivisions);
- }
- /**
- * 计算音符时长(秒)
- *
- * @param duration MusicXML中的duration值
- * @param bpm 每分钟节拍数(四分音符为单位)
- * @returns 音符时长(秒)
- *
- * @example
- * ```typescript
- * handler.setDivisions(256);
- * // BPM=120,一个四分音符=0.5秒
- * handler.toSeconds(256, 120); // 0.5秒
- * handler.toSeconds(128, 120); // 0.25秒(八分音符)
- * ```
- */
- toSeconds(duration: number, bpm: number): number {
- if (bpm <= 0) {
- console.warn(`[DivisionsHandler] BPM为${bpm},使用默认值120`);
- bpm = 120;
- }
- const realValue = this.toRealValue(duration);
- // 四分音符时长(秒)= 60 / BPM
- const quarterNoteDuration = 60 / bpm;
- return realValue * quarterNoteDuration;
- }
- /**
- * 获取音符类型对应的realValue
- *
- * @param noteType 音符类型(如 'quarter', 'eighth', 'half' 等)
- * @returns 对应的realValue
- */
- getNoteTypeRealValue(noteType: string): number {
- const noteTypeMap: Record<string, number> = {
- 'maxima': 32, // 最长音符
- 'long': 16, // 长音符
- 'breve': 8, // 二全音符
- 'whole': 4, // 全音符
- 'half': 2, // 二分音符
- 'quarter': 1, // 四分音符
- 'eighth': 0.5, // 八分音符
- '16th': 0.25, // 十六分音符
- '32nd': 0.125, // 三十二分音符
- '64th': 0.0625, // 六十四分音符
- '128th': 0.03125, // 一百二十八分音符
- '256th': 0.015625, // 二百五十六分音符
- '512th': 0.0078125, // 五百一十二分音符
- '1024th': 0.00390625, // 一千零二十四分音符
- };
- const value = noteTypeMap[noteType.toLowerCase()];
- if (value === undefined) {
- console.warn(`[DivisionsHandler] 未知的音符类型: ${noteType},默认返回1.0(四分音符)`);
- return 1;
- }
- return value;
- }
- /**
- * 计算附点后的时值
- *
- * @param baseRealValue 基础时值
- * @param dotCount 附点数量(1个附点增加50%,2个附点增加75%)
- * @returns 附点后的时值
- */
- applyDots(baseRealValue: number, dotCount: number): number {
- if (dotCount <= 0) {
- return baseRealValue;
- }
- let result = baseRealValue;
- let addition = baseRealValue / 2;
- for (let i = 0; i < dotCount; i++) {
- result += addition;
- addition /= 2;
- }
- return result;
- }
- /**
- * 重置处理器状态
- */
- reset(): void {
- this.currentDivisions = DEFAULT_DIVISIONS;
- this.measureDivisionsMap.clear();
- }
- /**
- * 获取所有小节的divisions映射
- *
- * @returns 小节索引到divisions值的映射
- */
- getAllMeasureDivisions(): Map<number, number> {
- return new Map(this.measureDivisionsMap);
- }
- }
- /**
- * 创建DivisionsHandler实例的工厂函数
- *
- * @param strictMode 是否启用严格模式
- * @returns DivisionsHandler实例
- */
- export function createDivisionsHandler(strictMode: boolean = false): DivisionsHandler {
- return new DivisionsHandler(strictMode);
- }
- /**
- * 快捷函数:将duration转换为realValue
- *
- * @param duration MusicXML中的duration值
- * @param divisions divisions值
- * @returns 实际时值
- */
- export function toRealValue(duration: number, divisions: number): number {
- if (divisions === 0) {
- throw new Error('divisions不能为0');
- }
- return duration / divisions;
- }
- /**
- * 快捷函数:计算附点时值
- *
- * @param baseValue 基础时值
- * @param dots 附点数量
- * @returns 附点后的时值
- */
- export function applyDots(baseValue: number, dots: number): number {
- let result = baseValue;
- let addition = baseValue / 2;
-
- for (let i = 0; i < dots; i++) {
- result += addition;
- addition /= 2;
- }
-
- return result;
- }
|