スキル一覧に戻る
EvanMcCormick

alphatab-gp-files

by EvanMcCormick

Interactive Music Theory tool

0🍴 0📅 2026年1月20日
GitHubで見るManusで実行

SKILL.md


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

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

IssueCauseSolution
No audioUser hasn't interactedalphaTab auto-handles, but may need click
Missing fontsWebpack plugin not configuredAdd AlphaTabWebPackPlugin
No soundfontPath incorrectCheck /soundfont/sonivox.sf2 exists
Rendering blankContainer too smallEnsure container has dimensions
Memory leakNot disposingCall api.destroy() in ngOnDestroy
UI not updatingEvents outside Angular zoneUse ChangeDetectorRef.detectChanges()
Assets missing in devOnly webpack plugin configuredAdd 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

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です