搜索

PythonReactSEOPDF
GitHub Skill

alphatab-gp-files

Use when working with Guitar Pro file viewing, alphaTab integration, or the GP Library - covers service patterns, file handling, scale highlighting, and IndexedDB storage

EvanMcCormick☆ 0 仓库 Stars0 仓库 Forks

安装与使用

请根据来源仓库和你的 AI 工具说明安装此技能。先阅读技能文件和依赖要求,再添加到工作流。

查看来源 ↗使用指南

技能说明

alphaTab and Guitar Pro File Handling

Overview

The app uses alphaTab library to render and play Guitar Pro files (.gp, .gp3-.gp5, .gpx). This skill covers the service architecture, file handling patterns, and integration with scale/chord highlighting.

Core principle: alphaTab manages its own rendering and audio - wrap it in a service with BehaviorSubject state management.

When to Use

  • Modifying GP viewer functionality
  • Adding features to GP library
  • Integrating scale/chord highlighting with sheet music
  • Handling file upload/storage
  • Debugging alphaTab rendering issues

Architecture

Files Structure
components/
  gp-viewer/
    gp-viewer.component.ts         # Main viewer container
    gp-viewer.component.html
    gp-viewer.component.scss
    components/
      gp-scale-highlighter/        # Scale/chord highlight controls
  gp-library/
    gp-library.component.ts        # Library browser
    components/
      gp-file-card/                # File card display
      gp-library-filters/          # Search/filter controls

services/
  alpha-tab.service.ts             # alphaTab API wrapper
  gp-library.service.ts            # IndexedDB storage

models/
  alpha-tab.model.ts               # Interfaces
Service Layer
// alpha-tab.service.ts - Key patterns
@Injectable({ providedIn: 'root' })
export class AlphaTabService {
  private api: any = null;  // alphaTab.AlphaTabApi
  private stateSubject = new BehaviorSubject<AlphaTabState>(DEFAULT_STATE);

  constructor(private ngZone: NgZone) {}

  getState(): Observable<AlphaTabState> {
    return this.stateSubject.asObservable();
  }

  initializeApi(element: HTMLElement, settings: AlphaTabSettings): void {
    // Import alphaTab dynamically (it's large)
    import('@coderline/alphatab').then(alphaTab => {
      this.api = new alphaTab.AlphaTabApi(element, settings);

      // Wrap event handlers with NgZone for change detection
      this.api.scoreLoaded.on((score: any) => {
        this.ngZone.run(() => {
          this.updateState({ isLoaded: true, score });
        });
      });

      this.api.playerPositionChanged.on((args: any) => {
        this.ngZone.run(() => {
          this.updateState({
            timePosition: args.currentTime,
            tickPosition: args.currentTick
          });
        });
      });
    });
  }

  dispose(): void {
    this.api?.destroy();
    this.api = null;
    this.stateSubject.next(DEFAULT_STATE);
  }
}

Build Configuration

alphaTab requires custom webpack for WebWorkers and AudioWorklets, plus asset configuration for fonts and soundfonts.

Webpack Plugin
// custom-webpack.config.js
const { AlphaTabWebPackPlugin } = require('@coderline/alphatab-webpack');

module.exports = {
  plugins: [
    new AlphaTabWebPackPlugin()  // Copies fonts and soundfont during build
  ]
};
Angular.json Configuration

IMPORTANT: The webpack plugin only copies assets during full builds. For ng serve (dev server), you must also configure assets in angular.json:

// angular.json
{
  "architect": {
    "build": {
      "builder": "@angular-builders/custom-webpack:browser",
      "options": {
        "assets": [
          "src/favicon.ico",
          "src/assets",
          {
            "glob": "**/*",
            "input": "node_modules/@coderline/alphatab/dist/font",
            "output": "/font"
          },
          {
            "glob": "**/*",
            "input": "node_modules/@coderline/alphatab/dist/soundfont",
            "output": "/soundfont"
          }
        ],
        "customWebpackConfig": {
          "path": "./custom-webpack.config.js"
        }
      }
    },
    "serve": {
      "builder": "@angular-builders/custom-webpack:dev-server"
    }
  }
}
Asset Paths in Code

When initializing alphaTab, use these paths:

this.alphaTabService.initializeApi(element, {
  core: {
    fontDirectory: '/font/',  // Must match angular.json output
    useWorkers: true
  },
  player: {
    soundFont: '/soundfont/sonivox.sf2'  // Must match angular.json output
  }
});

File Handling

Loading Files
async loadFile(file: File): Promise<void> {
  this.updateState({ loadingState: 'loading' });

  try {
    const arrayBuffer = await file.arrayBuffer();
    this.api.load(arrayBuffer);
  } catch (error) {
    this.updateState({
      loadingState: 'error',
      errorMessage: 'Failed to load file'
    });
  }
}
Drag and Drop
@HostListener('dragover', ['$event'])
onDragOver(event: DragEvent): void {
  event.preventDefault();
  event.stopPropagation();
  this.isDragging = true;
}

@HostListener('drop', ['$event'])
onDrop(event: DragEvent): void {
  event.preventDefault();
  event.stopPropagation();
  this.isDragging = false;

  const files = event.dataTransfer?.files;
  if (files && files.length > 0) {
    const file = files[0];
    if (this.isValidGpFile(file)) {
      this.loadFile(file);
    }
  }
}

private isValidGpFile(file: File): boolean {
  const validExtensions = ['.gp', '.gp3', '.gp4', '.gp5', '.gpx'];
  return validExtensions.some(ext =>
    file.name.toLowerCase().endsWith(ext)
  );
}

GP Library (IndexedDB)

Storage Schema
interface GpLibraryEntry {
  id: string;              // Unique ID
  fileName: string;        // Original filename
  fileSize: number;        // Bytes
  fileData: ArrayBuffer;   // The actual file
  title: string;           // From score metadata
  artist?: string;
  album?: string;
  tempo?: number;
  trackCount?: number;
  key?: number;            // Detected key (0-11)
  detectedScales?: string[];
  detectedChords?: string[];
  dateAdded: Date;
}
Service Pattern
@Injectable({ providedIn: 'root' })
export class GpLibraryService {
  private db: IDBDatabase | null = null;
  private entriesSubject = new BehaviorSubject<GpLibraryEntry[]>([]);

  constructor(private ngZone: NgZone) {
    this.initDatabase();
  }

  async addFile(file: File, metadata: Partial<GpLibraryEntry>): Promise<GpLibraryEntry> {
    const reader = new FileReader();

    return new Promise((resolve, reject) => {
      reader.onload = () => {
        const entry: GpLibraryEntry = {
          id: this.generateId(),
          fileName: file.name,
          fileSize: file.size,
          fileData: reader.result as ArrayBuffer,
          title: metadata.title || file.name,
          dateAdded: new Date(),
          ...metadata
        };

        const transaction = this.db!.transaction('files', 'readwrite');
        const store = transaction.objectStore('files');
        const request = store.add(entry);

        request.onsuccess = () => {
          this.loadAllEntries();  // Refresh list
          resolve(entry);
        };
      };

      reader.readAsArrayBuffer(file);
    });
  }

  async getFileData(id: string): Promise<ArrayBuffer | null> {
    const entry = await this.getEntry(id);
    return entry?.fileData || null;
  }
}

Scale/Chord Highlighting

HighlightConfig Interface
interface HighlightConfig {
  enabled: boolean;
  rootNote: number;      // 0-11 (C=0)
  intervals: number[];   // Semitones from root
  name: string;          // Display name
  type: 'scale' | 'chord';
}
Applying Highlights
onHighlightChange(config: HighlightConfig): void {
  this.highlightConfig = config;
  this.applyHighlighting();
}

private applyHighlighting(): void {
  const container = this.alphaTabContainer.nativeElement;

  // Remove existing highlights
  container.querySelectorAll('.scale-highlight').forEach((el: Element) => {
    el.classList.remove('scale-highlight');
  });

  if (!this.highlightConfig?.enabled) return;

  // Calculate notes to highlight (0-11)
  const highlightedNotes = this.highlightConfig.intervals.map(interval =>
    (this.highlightConfig!.rootNote + interval) % 12
  );

  // Find note elements and apply class
  container.querySelectorAll('.at-note').forEach((noteEl: Element) => {
    const noteValue = noteEl.getAttribute('data-note');
    if (noteValue !== null) {
      const noteNum = parseInt(noteValue, 10) % 12;
      if (highlightedNotes.includes(noteNum)) {
        noteEl.classList.add('scale-highlight');
      }
    }
  });
}
Highlight Styles
// In component SCSS
.alphatab-container {
  ::ng-deep .at-note.scale-highlight {
    circle, ellipse {
      fill: rgba($accent-color, 0.3) !important;
      stroke: $accent-color !important;
      stroke-width: 2px !important;
    }
  }
}

Playback Controls

Basic Controls
play(): void {
  this.api?.play();
}

pause(): void {
  this.api?.pause();
}

stop(): void {
  this.api?.stop();
}

setVolume(value: number): void {
  if (this.api) {
    this.api.masterVolume = value;  // 0-1
  }
}

setPlaybackSpeed(multiplier: number): void {
  if (this.api) {
    this.api.playbackSpeed = multiplier;  // 0.25 to 2.0
  }
}
Track Controls
setTrackMute(trackIndex: number, muted: boolean): void {
  const track = this.api?.score?.tracks[trackIndex];
  if (track) {
    this.api.changeTrackMute([track], muted);
  }
}

setTrackSolo(trackIndex: number, solo: boolean): void {
  const track = this.api?.score?.tracks[trackIndex];
  if (track) {
    this.api.changeTrackSolo([track], solo);
  }
}

Common Issues

Issue Cause Solution
No audio User hasn't interacted alphaTab auto-handles, but may need click
Missing fonts Webpack plugin not configured Add AlphaTabWebPackPlugin
No soundfont Path incorrect Check /soundfont/sonivox.sf2 exists
Rendering blank Container too small Ensure container has dimensions
Memory leak Not disposing Call api.destroy() in ngOnDestroy
UI not updating Events outside Angular zone Use ChangeDetectorRef.detectChanges()
Assets missing in dev Only webpack plugin configured Add assets to angular.json

ChangeDetectorRef for UI Updates

alphaTab events fire outside Angular's zone. Even with NgZone.run() in the service, components may need manual change detection:

import { ChangeDetectorRef } from '@angular/core';

export class GpViewerComponent implements OnInit {
  constructor(
    private alphaTabService: AlphaTabService,
    private cdr: ChangeDetectorRef
  ) {}

  ngOnInit(): void {
    this.alphaTabService.getState()
      .pipe(takeUntil(this.destroy$))
      .subscribe(state => {
        this.state = state;
        // CRITICAL: Force change detection for alphaTab events
        this.cdr.detectChanges();
      });
  }
}

Checklist

When modifying GP viewer/library:

  • AlphaTabService wraps all API calls
  • NgZone.run() used for all event handlers in service
  • ChangeDetectorRef.detectChanges() used in component subscriptions
  • Files validated before loading
  • Dispose called in ngOnDestroy
  • IndexedDB transactions properly handled
  • Highlighting re-applied after score renders
  • Loading/error states handled in UI
  • File size considered for IndexedDB storage limits
  • Assets configured in angular.json for dev server
  • Custom webpack config includes AlphaTabWebPackPlugin

使用示例

查看来源仓库中的使用示例与配置说明。

查看来源 ↗

来源历史

收录时间
2026-01-20
索引更新时间
2026-01-26
Branch
main

收录与同步日期属于本站记录。源码修改历史请查看来源仓库。

GitHub ↗

chat Comments (0)

chat_bubble_outline

No comments yet. Be the first to share your thoughts!