diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 09b65bec8..48916caa9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ on: jobs: build_web: name: Build and Test Web - runs-on: windows-latest + runs-on: windows-2022 steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 @@ -22,7 +22,7 @@ jobs: build_csharp: name: Build and Test C# - runs-on: windows-latest + runs-on: windows-2022 steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 @@ -39,7 +39,7 @@ jobs: build_kotlin: name: Build and Test Kotlin - runs-on: windows-latest + runs-on: windows-2022 steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 @@ -53,9 +53,9 @@ jobs: path: | ~/.gradle/caches ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-cache-${{ github.job }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + key: ${{ runner.os }}-gradle-cache-v2-${{ github.job }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | - ${{ runner.os }}-gradle-cache-${{ github.job }}- + ${{ runner.os }}-gradle-cache-v2-${{ github.job }}- - run: npm install - run: npm run build-kotlin-ci - run: npm run test-kotlin-ci diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 974ca7abb..9de64121b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,7 +7,7 @@ on: jobs: nighty_web: name: Web - runs-on: windows-latest + runs-on: windows-2022 steps: - name: Create cache file run: | @@ -52,7 +52,7 @@ jobs: nightly_csharp: name: C# - runs-on: windows-latest + runs-on: windows-2022 steps: - name: Create cache file run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f5c211456..19a63387e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: jobs: release_web: name: Web - runs-on: windows-latest + runs-on: windows-2022 steps: # Checkout the repo - uses: actions/checkout@v2 @@ -32,7 +32,7 @@ jobs: release_csharp: name: C# - runs-on: windows-latest + runs-on: windows-2022 steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 diff --git a/.gitignore b/.gitignore index 8fe999293..44e588182 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ bin/ test-results/ debug.log src/generated/VersionInfo.ts -src.kotlin/alphaTab/src/generated/ .gradle build/ diff --git a/package.json b/package.json index 7fe37ec12..07c63bbc9 100644 --- a/package.json +++ b/package.json @@ -42,15 +42,15 @@ "build-ci": "npm run clean && npm run build && npm pack", "build-csharp": "npm run generate-csharp && cd src.csharp && dotnet build -c Release", "build-csharp-ci": "npm run clean && npm run generate-csharp && cd src.csharp && dotnet build -c Release", - "build-kotlin": "npm run generate-kotlin && cd src.kotlin/alphaTab && gradlew assemble", - "build-kotlin-ci": "npm run clean && npm run generate-kotlin && cd src.kotlin/alphaTab && gradlew assemble", + "build-kotlin": "npm run generate-kotlin && cd src.kotlin/alphaTab && gradlew assembleRelease", + "build-kotlin-ci": "npm run clean && npm run generate-kotlin && cd src.kotlin/alphaTab && gradlew assembleRelease", "start": "node scripts/setup-playground.js && npm run build && concurrently --kill-others \"tsc --project tsconfig.build.json --watch\" \"rollup -c rollup.config.js -w\"", "test": "npm run generate-typescript && tsc --project tsconfig.json && concurrently --kill-others \"tsc --project tsconfig.json -w\" \"karma start karma.conf.js --browsers Chrome --no-single-run --reporters spec,kjhtml\"", "test-ci": "npm run generate-typescript && tsc --project tsconfig.json && karma start karma.conf.js --browsers ChromeHeadless --single-run --reporters spec", - "test-csharp": "cd src.csharp && dotnet test", - "test-csharp-ci": "cd src.csharp && dotnet test", - "test-kotlin": "cd src.kotlin/alphaTab && gradlew jvmTest", - "test-kotlin-ci": "cd src.kotlin/alphaTab && gradlew jvmTest" + "test-csharp": "cd src.csharp && dotnet test -c Release", + "test-csharp-ci": "cd src.csharp && dotnet test -c Release", + "test-kotlin": "cd src.kotlin/alphaTab && gradlew testReleaseUnitTest", + "test-kotlin-ci": "cd src.kotlin/alphaTab && gradlew testReleaseUnitTest" }, "devDependencies": { "@rollup/plugin-commonjs": "^21.0.1", diff --git a/src.compiler/AstPrinterBase.ts b/src.compiler/AstPrinterBase.ts index 732551d96..0ae50fedb 100644 --- a/src.compiler/AstPrinterBase.ts +++ b/src.compiler/AstPrinterBase.ts @@ -72,10 +72,13 @@ export default abstract class AstPrinterBase { } } - protected writeCommaSeparated(values: T[], write: (p: T) => void) { + protected writeCommaSeparated(values: T[], write: (p: T) => void, newLine:boolean = false) { values.forEach((v, i) => { if (i > 0) { this.write(', '); + if(newLine) { + this.writeLine(); + } } write(v); }); @@ -202,7 +205,18 @@ export default abstract class AstPrinterBase { this.write('>'); } this.write('('); - this.writeCommaSeparated(expr.arguments, a => this.writeExpression(a)); + if (expr.arguments.length > 5) { + this.writeLine(); + this._indent++; + this.writeCommaSeparated(expr.arguments, a => { + this.writeExpression(a); + this.writeLine(); + }, true); + this._indent--; + this.writeLine(); + } else { + this.writeCommaSeparated(expr.arguments, a => this.writeExpression(a)); + } this.write(')'); } diff --git a/src.compiler/csharp/CSharpAstTransformer.ts b/src.compiler/csharp/CSharpAstTransformer.ts index e96405a9d..d7c76427c 100644 --- a/src.compiler/csharp/CSharpAstTransformer.ts +++ b/src.compiler/csharp/CSharpAstTransformer.ts @@ -25,24 +25,19 @@ export default class CSharpAstTransformer { path.resolve(this._context.compilerOptions.baseUrl!), path.resolve(this._typeScriptFile.fileName) ); - fileName = path.join(context.compilerOptions.outDir!, this.removeExtension(fileName) + this.extension); + fileName = this.buildFileName(fileName, context); this._csharpFile = { parent: null, tsNode: this._typeScriptFile, nodeType: cs.SyntaxKind.SourceFile, fileName: fileName, - usings: [ - { - namespaceOrTypeName: this._context.toPascalCase('system'), - nodeType: cs.SyntaxKind.UsingDeclaration - } as cs.UsingDeclaration, - { - namespaceOrTypeName: - this._context.toPascalCase('alphaTab') + '.' + this._context.toPascalCase('core'), + usings: this._context.getDefaultUsings().map(u => { + return { + namespaceOrTypeName: u, nodeType: cs.SyntaxKind.UsingDeclaration - } as cs.UsingDeclaration - ], + } as cs.UsingDeclaration; + }), namespace: { parent: null, nodeType: cs.SyntaxKind.NamespaceDeclaration, @@ -53,6 +48,10 @@ export default class CSharpAstTransformer { this._csharpFile.namespace.parent = this._csharpFile; } + protected buildFileName(fileName: string, context: CSharpEmitterContext): string { + return path.join(context.compilerOptions.outDir!, this.removeExtension(fileName) + this.extension); + } + public transform() { // if the default export is a class: // - global statements will be put into the static constructor of the class @@ -336,7 +335,6 @@ export default class CSharpAstTransformer { return 'csharp'; } - protected visitEnumDeclaration(node: ts.EnumDeclaration) { const csEnum: cs.EnumDeclaration = { visibility: cs.Visibility.Public, @@ -427,7 +425,7 @@ export default class CSharpAstTransformer { }); } - if(!csInterface.skipEmit){ + if (!csInterface.skipEmit) { node.members.forEach(m => this.visitInterfaceElement(csInterface, m)); } @@ -999,7 +997,7 @@ export default class CSharpAstTransformer { type: this.createUnresolvedTypeNode(null, classElement.type ?? classElement, returnType), skipEmit: this.shouldSkip(classElement, false), tsNode: classElement, - tsSymbol: this._context.getSymbolForDeclaration(classElement), + tsSymbol: this._context.getSymbolForDeclaration(classElement) }; if (this._context.markOverride(classElement)) { @@ -1071,7 +1069,7 @@ export default class CSharpAstTransformer { type: this.createUnresolvedTypeNode(null, classElement.type ?? classElement, returnType), skipEmit: this.shouldSkip(classElement, false), tsNode: classElement, - tsSymbol: this._context.getSymbolForDeclaration(classElement), + tsSymbol: this._context.getSymbolForDeclaration(classElement) }; if (this._context.markOverride(classElement)) { @@ -1435,7 +1433,7 @@ export default class CSharpAstTransformer { nodeType: cs.SyntaxKind.TypeReference, parent: variableStatement, tsNode: s, - reference: this._context.makeTypeName('system.Exception') + reference: this._context.makeExceptionType() } as cs.TypeReference; } else { variableStatement.type = this.createUnresolvedTypeNode(variableStatement, s.type ?? s, type); @@ -2821,8 +2819,40 @@ export default class CSharpAstTransformer { } protected visitArrayLiteralExpression(parent: cs.Node, expression: ts.ArrayLiteralExpression) { - if (this.isMapInitializer(expression)) { + if (this.isMapEntry(expression)) { return this.createMapEntry(parent, expression); + } else if (this.isMapInitializer(expression)) { + const csExpr = { + parent: parent, + tsNode: expression, + nodeType: cs.SyntaxKind.InvocationExpression, + arguments: [], + expression: {} as cs.Expression + } as cs.InvocationExpression; + + csExpr.expression = this.makeMemberAccess( + csExpr, + this._context.makeTypeName('alphaTab.core.TypeHelper'), + this._context.toPascalCase('mapInitializer') + ); + + expression.elements.forEach(e => { + const ex = this.visitExpression(csExpr, e); + if (ex) { + csExpr.arguments.push(ex); + } + }); + + // steal generic from inner element + if(csExpr.arguments.length > 0 && + cs.isInvocationExpression(csExpr.arguments[0]) && + cs.isTypeReference(csExpr.arguments[0].expression)) { + csExpr.typeArguments = [ + csExpr.arguments[0].expression + ]; + } + + return csExpr; } else if (this.isSetInitializer(expression)) { const csExpr = { parent: parent, @@ -2838,6 +2868,11 @@ export default class CSharpAstTransformer { this._context.toPascalCase('setInitializer') ); + const setCreation = expression.parent as ts.NewExpression; + if (setCreation.typeArguments) { + csExpr.typeArguments = setCreation.typeArguments.map(t => this.createUnresolvedTypeNode(csExpr, t)); + } + expression.elements.forEach(e => { const ex = this.visitExpression(csExpr, e); if (ex) { @@ -2895,6 +2930,15 @@ export default class CSharpAstTransformer { } protected isMapInitializer(expression: ts.ArrayLiteralExpression) { + const isCandidate = expression.parent.kind === ts.SyntaxKind.NewExpression; + if (!isCandidate) { + return false; + } + + return this._context.typeChecker.getTypeAtLocation(expression.parent).symbol.name === 'Map'; + } + + protected isMapEntry(expression: ts.ArrayLiteralExpression) { const isCandidate = expression.elements.length === 2 && expression.parent.kind === ts.SyntaxKind.ArrayLiteralExpression && @@ -3237,7 +3281,6 @@ export default class CSharpAstTransformer { } }); - if (expression.typeArguments) { callExpression.typeArguments = []; expression.typeArguments.forEach(a => @@ -3271,14 +3314,6 @@ export default class CSharpAstTransformer { } as cs.NewExpression; newExpression.type.parent = newExpression; - if (expression.arguments) { - expression.arguments.forEach(a => { - const e = this.visitExpression(newExpression, a); - if (e) { - newExpression.arguments.push(e); - } - }); - } if (expression.typeArguments) { csType.typeArguments = []; @@ -3308,6 +3343,15 @@ export default class CSharpAstTransformer { } } + if (expression.arguments) { + expression.arguments.forEach(a => { + const e = this.visitExpression(newExpression, a); + if (e) { + newExpression.arguments.push(e); + } + }); + } + if (type && type.symbol && type.symbol.name === 'ArrayConstructor' && newExpression.arguments.length === 1) { newExpression.arguments[0] = this.makeInt(newExpression.arguments[0]); } @@ -3424,7 +3468,7 @@ export default class CSharpAstTransformer { (node.tsSymbol.flags & ts.SymbolFlags.Variable) === ts.SymbolFlags.Variable || (node.tsSymbol.flags & ts.SymbolFlags.EnumMember) === ts.SymbolFlags.EnumMember || (node.tsSymbol.flags & ts.SymbolFlags.FunctionScopedVariable) === - ts.SymbolFlags.FunctionScopedVariable || + ts.SymbolFlags.FunctionScopedVariable || (node.tsSymbol.flags & ts.SymbolFlags.BlockScopedVariable) === ts.SymbolFlags.BlockScopedVariable ) { let smartCastType = this._context.getSmartCastType(expression); diff --git a/src.compiler/csharp/CSharpEmitterContext.ts b/src.compiler/csharp/CSharpEmitterContext.ts index 81260748a..59464bab2 100644 --- a/src.compiler/csharp/CSharpEmitterContext.ts +++ b/src.compiler/csharp/CSharpEmitterContext.ts @@ -753,6 +753,10 @@ export default class CSharpEmitterContext { return null; } + public makeExceptionType(): cs.TypeReferenceType { + return this.makeTypeName('system.Exception') + } + public makeTypeName(tsName: string): string { const parts = tsName.split('.'); let result = ''; @@ -1490,4 +1494,11 @@ export default class CSharpEmitterContext { } return type.flags & ts.TypeFlags.String || type.flags & ts.TypeFlags.StringLiteral; } + + public getDefaultUsings(): string[] { + return [ + this.toPascalCase('system'), + this.toPascalCase('alphaTab') + '.' + this.toPascalCase('core') + ]; + } } diff --git a/src.compiler/kotlin/KotlinAstTransformer.ts b/src.compiler/kotlin/KotlinAstTransformer.ts index abbb66dfe..c14cb760b 100644 --- a/src.compiler/kotlin/KotlinAstTransformer.ts +++ b/src.compiler/kotlin/KotlinAstTransformer.ts @@ -1,5 +1,6 @@ import * as ts from 'typescript'; import * as cs from '../csharp/CSharpAst'; +import * as path from 'path'; import CSharpEmitterContext from '../csharp/CSharpEmitterContext'; import CSharpAstTransformer from '../csharp/CSharpAstTransformer'; @@ -7,14 +8,13 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { public constructor(typeScript: ts.SourceFile, context: CSharpEmitterContext) { super(typeScript, context); this._testClassAttribute = ''; - this._testMethodAttribute = 'org.junit.Test'; + this._testMethodAttribute = 'kotlin.test.Test'; } public override get extension(): string { return '.kt'; } - public override get targetTag(): string { return 'kotlin'; } @@ -26,6 +26,22 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { return 'param' + name; } + protected override buildFileName(fileName: string, context: CSharpEmitterContext): string { + let parts = this.removeExtension(fileName).split(path.sep); + if (parts.length > 0) { + switch (parts[0]) { + case 'src': + parts[0] = path.join('commonMain', 'generated'); + break; + case 'test': + parts[0] = path.join('commonTest', 'generated'); + break; + } + } + + return path.join(context.compilerOptions.outDir!, parts.join(path.sep) + this.extension); + } + protected override getIdentifierName(identifier: cs.Identifier, expression: ts.Identifier): string { const paramName = super.getIdentifierName(identifier, expression); if ( @@ -199,7 +215,10 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { return el; } - protected override visitConstructorDeclaration(parent: cs.ClassDeclaration, classElement: ts.ConstructorDeclaration) { + protected override visitConstructorDeclaration( + parent: cs.ClassDeclaration, + classElement: ts.ConstructorDeclaration + ) { this._paramReferences.push(new Map()); this._paramsWithAssignment.push(new Set()); @@ -264,7 +283,11 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { return base; } - protected override getSymbolName(parentSymbol: ts.Symbol, symbol: ts.Symbol, expression: cs.Expression): string | null { + protected override getSymbolName( + parentSymbol: ts.Symbol, + symbol: ts.Symbol, + expression: cs.Expression + ): string | null { switch (parentSymbol.name) { case 'String': switch (symbol.name) { @@ -395,7 +418,14 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { expression: {} as cs.Expression } as cs.InvocationExpression; - let mapEntryTypeName = 'MapEntry'; + const type: cs.TypeReference = { + nodeType: cs.SyntaxKind.TypeReference, + parent: csExpr, + reference: '', + tsNode: expression + }; + + type.reference = 'MapEntry'; if (expression.elements.length === 2) { const keyType = this._context.getType(expression.elements[0]); let keyTypeContainerName = this.getContainerTypeName(keyType); @@ -403,19 +433,34 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { const valueType = this._context.getType(expression.elements[1]); let valueTypeContainerName = this.getContainerTypeName(valueType); + + if (!keyTypeContainerName) { + type.typeArguments = type.typeArguments ?? []; + type.typeArguments.push({ + nodeType: cs.SyntaxKind.TypeReference, + parent: type, + reference: this.createUnresolvedTypeNode(type, expression.elements[0], keyType) + } as cs.TypeReference); + } + + if (!valueTypeContainerName) { + type.typeArguments = type.typeArguments ?? []; + type.typeArguments.push({ + nodeType: cs.SyntaxKind.TypeReference, + parent: type, + reference: this.createUnresolvedTypeNode(type, expression.elements[1], valueType) + } as cs.TypeReference); + } + if (keyTypeContainerName || valueTypeContainerName) { keyTypeContainerName = keyTypeContainerName || 'Object'; valueTypeContainerName = valueTypeContainerName || 'Object'; - mapEntryTypeName = keyTypeContainerName + valueTypeContainerName + mapEntryTypeName; + type.reference = keyTypeContainerName + valueTypeContainerName + type.reference; } } - csExpr.expression = { - nodeType: cs.SyntaxKind.Identifier, - text: this._context.makeTypeName(`alphaTab.collections.${mapEntryTypeName}`), - parent: csExpr, - tsNode: expression - } as cs.Identifier; + type.reference = this._context.makeTypeName(`alphaTab.collections.${type.reference}`); + csExpr.expression = type; expression.elements.forEach(e => { const ex = this.visitExpression(csExpr, e); diff --git a/src.compiler/kotlin/KotlinEmitterContext.ts b/src.compiler/kotlin/KotlinEmitterContext.ts index 2356bc2aa..bb8211bf7 100644 --- a/src.compiler/kotlin/KotlinEmitterContext.ts +++ b/src.compiler/kotlin/KotlinEmitterContext.ts @@ -32,6 +32,16 @@ export default class KotlinEmitterContext extends CSharpEmitterContext { return s; } + public override getDefaultUsings(): string[] { + return [ + this.toPascalCase('alphaTab') + '.' + this.toPascalCase('core') + ]; + } + + public override makeExceptionType(): cs.TypeReferenceType { + return this.makeTypeName('kotlin.Throwable') + } + private isSymbolPartial(tsSymbol: ts.Symbol): boolean { if (!tsSymbol.valueDeclaration) { return false; diff --git a/src.csharp/AlphaTab/Collections/MapEntry.cs b/src.csharp/AlphaTab/Collections/MapEntry.cs index b20b5ac4f..176e856ff 100644 --- a/src.csharp/AlphaTab/Collections/MapEntry.cs +++ b/src.csharp/AlphaTab/Collections/MapEntry.cs @@ -25,4 +25,4 @@ public void Deconstruct(out TKey key, out TValue value) value = Value; } } -} +} \ No newline at end of file diff --git a/src.csharp/AlphaTab/Core/TypeHelper.cs b/src.csharp/AlphaTab/Core/TypeHelper.cs index 7d5666617..ad869555e 100644 --- a/src.csharp/AlphaTab/Core/TypeHelper.cs +++ b/src.csharp/AlphaTab/Core/TypeHelper.cs @@ -366,5 +366,11 @@ public static IEnumerable SetInitializer(params T[] items) { return items; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEnumerable MapInitializer(params T[] items) + { + return items; + } } } diff --git a/src.kotlin/alphaTab/.gitignore b/src.kotlin/alphaTab/.gitignore new file mode 100644 index 000000000..bf68fbac1 --- /dev/null +++ b/src.kotlin/alphaTab/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +/local.properties +.idea +.DS_Store +/build +*/build +/captures +.externalNativeBuild +.cxx +local.properties \ No newline at end of file diff --git a/src.kotlin/alphaTab/alphaTabAndroid/build.gradle.kts b/src.kotlin/alphaTab/alphaTabAndroid/build.gradle.kts new file mode 100644 index 000000000..561305bda --- /dev/null +++ b/src.kotlin/alphaTab/alphaTabAndroid/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("com.android.application") + kotlin("android") +} + +dependencies { + implementation(project(":shared")) + implementation("com.google.android.material:material:1.5.0") + implementation("androidx.appcompat:appcompat:1.4.1") + implementation("androidx.constraintlayout:constraintlayout:2.1.3") + implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") +} + +android { + compileSdk = 31 + defaultConfig { + applicationId = "net.alphatab.android" + minSdk = 24 + targetSdk = 31 + versionCode = 1 + versionName = "1.0" + } + signingConfigs { + getByName("debug") { + keyAlias = "key0" + keyPassword = "alphaTab" + storeFile = file("../alphatab.jks") + storePassword = "alphaTab" + } + create("release") { + keyAlias = "key0" + keyPassword = "alphaTab" + storeFile = file("../alphatab.jks") + storePassword = "alphaTab" + } + } + + buildTypes { + getByName("debug") { + isMinifyEnabled = false + signingConfig = signingConfigs.getByName("debug") + } + getByName("release") { + signingConfig = signingConfigs.getByName("release") + } + } +} + diff --git a/src.kotlin/alphaTab/alphaTabAndroid/src/main/AndroidManifest.xml b/src.kotlin/alphaTab/alphaTabAndroid/src/main/AndroidManifest.xml new file mode 100644 index 000000000..67634f754 --- /dev/null +++ b/src.kotlin/alphaTab/alphaTabAndroid/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/src.kotlin/alphaTab/alphaTabAndroid/src/main/java/net/alphatab/android/MainActivity.kt b/src.kotlin/alphaTab/alphaTabAndroid/src/main/java/net/alphatab/android/MainActivity.kt new file mode 100644 index 000000000..85bd42278 --- /dev/null +++ b/src.kotlin/alphaTab/alphaTabAndroid/src/main/java/net/alphatab/android/MainActivity.kt @@ -0,0 +1,118 @@ +package net.alphatab.android + +import alphaTab.AlphaTabView +import alphaTab.core.ecmaScript.Uint8Array +import alphaTab.importer.ScoreLoader +import alphaTab.model.Score +import alphaTab.synth.PlayerState +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.WindowManager +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.floatingactionbutton.FloatingActionButton +import java.io.ByteArrayOutputStream +import kotlin.contracts.ExperimentalContracts + +@ExperimentalContracts +@ExperimentalUnsignedTypes +class MainActivity : AppCompatActivity() { + + private lateinit var _viewModel: ViewScoreViewModel + private lateinit var _alphaTabView: AlphaTabView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + _alphaTabView = findViewById(R.id.alphatab_view) + _viewModel = ViewModelProvider(this).get(ViewScoreViewModel::class.java) + + val playButton = findViewById(R.id.play_button) + + _alphaTabView.api.playerReady.on { + playButton.isEnabled = true + } + + _alphaTabView.api.playerStateChanged.on { + if (it.state == PlayerState.Playing) { + playButton.setImageResource(android.R.drawable.ic_media_pause) + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + playButton.setImageResource(android.R.drawable.ic_media_play) + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + + _alphaTabView.api.playerPositionChanged.on { + _viewModel.currentTickPosition.value = it.currentTick.toInt() + } + + observeViewModel() + + val widthDp = resources.displayMetrics.widthPixels / + resources.displayMetrics.density + _viewModel.updateLayout(widthDp) + + findViewById(R.id.open_file_button).setOnClickListener { + openFile.launch(arrayOf("*/*")) + } + + playButton.setOnClickListener { + _alphaTabView.api.playPause() + } + } + + private fun observeViewModel() { + _viewModel.settings.observe(this, { + _alphaTabView.settings = it + }) + _viewModel.tracks.observe(this, { + _alphaTabView.tracks = it + }) + + val initialPosition = _viewModel.currentTickPosition.value + var shouldSetPosition = true + _alphaTabView.api.playerReady.on { + if (shouldSetPosition && _alphaTabView.tracks == _viewModel.tracks.value) { + _viewModel.currentTickPosition.value = initialPosition + _alphaTabView.api.tickPosition = initialPosition!!.toDouble() + } + shouldSetPosition = false + } + } + + private val openFile = registerForActivityResult(ActivityResultContracts.OpenDocument()) { + val uri = it ?: return@registerForActivityResult + val score: Score + try { + val fileData = readFileData(uri) + score = ScoreLoader.loadScoreFromBytes(fileData, _alphaTabView.settings) + Log.i("AlphaTab", "File loaded: ${score.title}") + } catch (e: Exception) { + Log.e("AlphaTab", "Failed to load file: $e, ${e.stackTraceToString()}") + Toast.makeText(this, R.string.open_failed, Toast.LENGTH_LONG).show() + return@registerForActivityResult + } + + try { + _viewModel.currentTickPosition.value = 0 + _viewModel.tracks.value = arrayListOf(score.tracks[0]) + } catch (e: Exception) { + Log.e("AlphaTab", "Failed to render file: $e, ${e.stackTraceToString()}") + Toast.makeText(this, R.string.open_failed, Toast.LENGTH_LONG).show() + } + } + + private fun readFileData(uri: Uri): Uint8Array { + val inputStream = contentResolver.openInputStream(uri) + inputStream.use { + ByteArrayOutputStream().use { + inputStream!!.copyTo(it) + return Uint8Array(it.toByteArray().asUByteArray()) + } + } + } +} diff --git a/src.kotlin/alphaTab/alphaTabAndroid/src/main/java/net/alphatab/android/ViewScoreViewModel.kt b/src.kotlin/alphaTab/alphaTabAndroid/src/main/java/net/alphatab/android/ViewScoreViewModel.kt new file mode 100644 index 000000000..a658714ba --- /dev/null +++ b/src.kotlin/alphaTab/alphaTabAndroid/src/main/java/net/alphatab/android/ViewScoreViewModel.kt @@ -0,0 +1,35 @@ +package net.alphatab.android + +import alphaTab.LayoutMode +import alphaTab.Settings +import alphaTab.model.Track +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import kotlin.contracts.ExperimentalContracts + +@ExperimentalUnsignedTypes +@ExperimentalContracts +class ViewScoreViewModel : ViewModel() { + public val currentTickPosition = MutableLiveData().apply { + value = 0 + } + + public val tracks = MutableLiveData?>() + public val settings = MutableLiveData().apply { + value = Settings().apply { + this.player.enableCursor = true + this.player.enablePlayer = true + this.player.enableUserInteraction = true + this.display.barCountPerPartial = 4.0 + } + } + + public fun updateLayout(screenWidthDp:Float) { + if (screenWidthDp >= 600f) { + settings.value!!.display.layoutMode = LayoutMode.Page + } else { + settings.value!!.display.layoutMode = LayoutMode.Horizontal + } + settings.value = settings.value // fire change + } +} diff --git a/src.kotlin/alphaTab/alphaTabAndroid/src/main/res/layout/activity_main.xml b/src.kotlin/alphaTab/alphaTabAndroid/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..a476887f6 --- /dev/null +++ b/src.kotlin/alphaTab/alphaTabAndroid/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + diff --git a/src.kotlin/alphaTab/alphaTabAndroid/src/main/res/values/colors.xml b/src.kotlin/alphaTab/alphaTabAndroid/src/main/res/values/colors.xml new file mode 100644 index 000000000..4faecfa80 --- /dev/null +++ b/src.kotlin/alphaTab/alphaTabAndroid/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #6200EE + #3700B3 + #03DAC5 + \ No newline at end of file diff --git a/src.kotlin/alphaTab/alphaTabAndroid/src/main/res/values/strings.xml b/src.kotlin/alphaTab/alphaTabAndroid/src/main/res/values/strings.xml new file mode 100644 index 000000000..35eb32a8f --- /dev/null +++ b/src.kotlin/alphaTab/alphaTabAndroid/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + Open File + Open File failed + alphaTab + diff --git a/src.kotlin/alphaTab/alphaTabAndroid/src/main/res/values/styles.xml b/src.kotlin/alphaTab/alphaTabAndroid/src/main/res/values/styles.xml new file mode 100644 index 000000000..22809560a --- /dev/null +++ b/src.kotlin/alphaTab/alphaTabAndroid/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/src.kotlin/alphaTab/alphatab.jks b/src.kotlin/alphaTab/alphatab.jks new file mode 100644 index 000000000..1ad41baf2 Binary files /dev/null and b/src.kotlin/alphaTab/alphatab.jks differ diff --git a/src.kotlin/alphaTab/build.gradle.kts b/src.kotlin/alphaTab/build.gradle.kts index f954f477c..01b7bf9b1 100644 --- a/src.kotlin/alphaTab/build.gradle.kts +++ b/src.kotlin/alphaTab/build.gradle.kts @@ -1,101 +1,23 @@ -plugins { - kotlin("multiplatform") version "1.6.10" -// id("com.android.library") -// id("kotlin-android-extensions") -} - -group = "net.alphatab" -version = "1.3-SNAPSHOT" - -repositories { - google() - mavenCentral() - maven("https://packages.jetbrains.team/maven/p/skija/maven") -} - -kotlin { - jvm { - compilations.all { - kotlinOptions.jvmTarget = "11" - } - testRuns["test"].executionTask.configure { - useJUnit() - } +buildscript { + repositories { + gradlePluginPortal() + google() + mavenCentral() } -// android() - sourceSets { - - val commonMain by getting { - dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0") - } - } - commonMain.kotlin.srcDirs("../../dist/lib.kotlin/src") - - val os = System.getProperty("os.name") - val target = when { - os == "Mac OS X" -> { - "macos-x64" - } - os.startsWith("Win") -> { - "windows" - } - os.startsWith("Linux") -> { - "linux" - } - else -> { - throw Error("Unsupported OS: $os") - } - } - val jvmMain by getting { - dependencies { - api("org.jetbrains.skija:skija-$target:0.93.6") - } - } - jvmMain.kotlin.srcDirs("src/jvmCommon/kotlin") - // TODO: check if we can control this folder - jvmMain.resources.srcDirs("../../font/").apply { - this.filter.include("**/*.ttf") - this.filter.include("**/*.sf2") - } - - val jvmTest by getting { - dependencies { - implementation(kotlin("test-junit")) - } - } - jvmTest.kotlin.srcDirs("../../dist/lib.kotlin/test") - -// val androidMain by getting { -// dependencies { -// implementation("com.google.android.material:material:1.3.0") -// } -// } -// androidMain.kotlin.srcDirs("src/jvmCommon/kotlin") -// -// val androidTest by getting { -// dependencies { -// implementation(kotlin("test-junit")) -// implementation("junit:junit:4.13") -// } -// } + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31") + classpath("com.android.tools.build:gradle:7.0.4") } } -tasks.named("jvmSourcesJar") { - exclude("*") +allprojects { + repositories { + google() + mavenCentral() + maven("https://packages.jetbrains.team/maven/p/skija/maven") + } } -//android { -// compileSdkVersion(29) -// sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") -// defaultConfig { -// minSdkVersion(24) -// targetSdkVersion(29) -// } -//} - -extensions.findByName("buildScan")?.withGroovyBuilder { - setProperty("termsOfServiceUrl", "https://gradle.com/terms-of-service") - setProperty("termsOfServiceAgree", "yes") +tasks.register("clean", Delete::class) { + delete(rootProject.buildDir) } diff --git a/src.kotlin/alphaTab/gradle.properties b/src.kotlin/alphaTab/gradle.properties index e722036ff..90f3b9ccb 100644 --- a/src.kotlin/alphaTab/gradle.properties +++ b/src.kotlin/alphaTab/gradle.properties @@ -1,9 +1,18 @@ +#Gradle +org.gradle.jvmargs=-Xmx6144M +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.vfs.watch=true + +# Kotlin +kotlin.daemon.jvm.options=-Xmx6144M kotlin.code.style=official -kotlin.mpp.enableGranularSourceSetsMetadata=false -kotlin.native.enableDependencyPropagation=false + +# Android android.useAndroidX=true -org.gradle.parallel=true + +#MPP +kotlin.mpp.enableGranularSourceSetsMetadata=true +kotlin.native.enableDependencyPropagation=false +kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.stability.nowarn=true -org.gradle.vfs.watch=false -org.gradle.jvmargs=-Xmx4096m -org.gradle.caching=true diff --git a/src.kotlin/alphaTab/gradle/wrapper/gradle-wrapper.jar b/src.kotlin/alphaTab/gradle/wrapper/gradle-wrapper.jar index e708b1c02..7454180f2 100644 Binary files a/src.kotlin/alphaTab/gradle/wrapper/gradle-wrapper.jar and b/src.kotlin/alphaTab/gradle/wrapper/gradle-wrapper.jar differ diff --git a/src.kotlin/alphaTab/gradle/wrapper/gradle-wrapper.properties b/src.kotlin/alphaTab/gradle/wrapper/gradle-wrapper.properties index f371643ee..2e6e5897b 100644 --- a/src.kotlin/alphaTab/gradle/wrapper/gradle-wrapper.properties +++ b/src.kotlin/alphaTab/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src.kotlin/alphaTab/gradlew b/src.kotlin/alphaTab/gradlew index 4f906e0c8..c53aefaa5 100644 --- a/src.kotlin/alphaTab/gradlew +++ b/src.kotlin/alphaTab/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/src.kotlin/alphaTab/settings.gradle.kts b/src.kotlin/alphaTab/settings.gradle.kts index 0748356b7..641d1ea78 100644 --- a/src.kotlin/alphaTab/settings.gradle.kts +++ b/src.kotlin/alphaTab/settings.gradle.kts @@ -1,17 +1,11 @@ pluginManagement { repositories { google() - jcenter() gradlePluginPortal() mavenCentral() } - resolutionStrategy { - eachPlugin { - if (requested.id.namespace == "com.android" || requested.id.name == "kotlin-android-extensions") { - useModule("com.android.tools.build:gradle:7.1.1") - } - } - } } rootProject.name = "alphaTab" +include(":alphaTabAndroid") +include(":shared") diff --git a/src.kotlin/alphaTab/shared/build.gradle.kts b/src.kotlin/alphaTab/shared/build.gradle.kts new file mode 100644 index 000000000..d133617b7 --- /dev/null +++ b/src.kotlin/alphaTab/shared/build.gradle.kts @@ -0,0 +1,172 @@ +plugins { + kotlin("multiplatform") +// kotlin("native.cocoapods") + id("com.android.library") +} + +group = "net.alphatab" +version = "1.3-SNAPSHOT" + +kotlin { + android() +// iosX64() +// iosArm64() +// iosSimulatorArm64() sure all ios dependencies support this target + +// cocoapods { +// summary = "Some description for the Shared Module" +// homepage = "Link to the Shared Module homepage" +// ios.deploymentTarget = "14.1" +// framework { +// baseName = "shared" +// } +// } + + sourceSets { + val commonMain by getting { + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") + } + kotlin.srcDirs("../../../dist/lib.kotlin/commonMain/generated") + } + + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + kotlin.srcDirs("../../../dist/lib.kotlin/commonTest/generated") + } + + val androidMain by getting { + dependencies { + implementation("androidx.core:core-ktx:1.7.0") + implementation("androidx.appcompat:appcompat:1.4.1") + implementation("com.google.android.material:material:1.5.0") + implementation("androidx.recyclerview:recyclerview:1.2.1") + implementation("com.google.android.flexbox:flexbox:3.0.0") + } + } + + val os = System.getProperty("os.name") + val target = when { + os == "Mac OS X" -> { + "macos-x64" + } + os.startsWith("Win") -> { + "windows" + } + os.startsWith("Linux") -> { + "linux" + } + else -> { + throw Error("Unsupported OS: $os") + } + } + + val androidTest by getting { + dependencies { + implementation(kotlin("test-junit")) + implementation("junit:junit:4.13.2") + implementation("org.jetbrains.skija:skija-$target:0.93.6") + } + } + +// val iosX64Main by getting +// val iosArm64Main by getting +// val iosSimulatorArm64Main by getting +// val iosMain by creating { +// dependsOn(commonMain) +// iosX64Main.dependsOn(this) +// iosArm64Main.dependsOn(this) +// //iosSimulatorArm64Main.dependsOn(this) +// } +// val iosX64Test by getting +// val iosArm64Test by getting +// //val iosSimulatorArm64Test by getting +// val iosTest by creating { +// dependsOn(commonTest) +// iosX64Test.dependsOn(this) +// iosArm64Test.dependsOn(this) +// //iosSimulatorArm64Test.dependsOn(this) +// } + + } +} + +android { + compileSdk = 31 + + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + sourceSets["main"].assets.srcDirs( + "../../../font/bravura", + "../../../font/sonivox" + ) + sourceSets["test"].manifest.srcFile("src/androidTest/AndroidManifest.xml") + sourceSets["test"].assets.srcDirs( + "../../../test-data", + "../../../font/bravura", + "../../../font/roboto", + "../../../font/ptserif" + ) + androidResources { + ignoreAssetsPattern = arrayOf( + "eot", + "otf", + "svg", + "woff", + "woff2", + "json", + "txt", + "md" + ).joinToString(":") { "!*.${it}" } + } + + defaultConfig { + minSdk = 24 + targetSdk = 31 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} +dependencies { + // To use the androidx.test.core APIs + androidTestImplementation("androidx.test:core:1.4.0") + // Kotlin extensions for androidx.test.core + androidTestImplementation("androidx.test:core-ktx:1.4.0") +} + +val fetchTestResultsTask by tasks.registering { + group = "reporting" + doLast { + exec { + executable = android.adbExecutable.toString() + args = listOf( + "pull", + "/storage/emulated/0/Documents/test-results", + "$buildDir/reports/androidTests/connected/" + ) + } + } +} + +tasks.whenTaskAdded { + if (this.name == "connectedDebugAndroidTest") { + this.finalizedBy(fetchTestResultsTask) + } +} + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs += listOf( + "-Xno-call-assertions", + "-Xno-receiver-assertions", + "-Xno-param-assertions" + ) + } +} diff --git a/src.kotlin/alphaTab/shared/shared.podspec b/src.kotlin/alphaTab/shared/shared.podspec new file mode 100644 index 000000000..6d1d3da84 --- /dev/null +++ b/src.kotlin/alphaTab/shared/shared.podspec @@ -0,0 +1,42 @@ +Pod::Spec.new do |spec| + spec.name = 'shared' + spec.version = '1.0' + spec.homepage = 'Link to the Shared Module homepage' + spec.source = { :git => "Not Published", :tag => "Cocoapods/#{spec.name}/#{spec.version}" } + spec.authors = '' + spec.license = '' + spec.summary = 'Some description for the Shared Module' + + spec.vendored_frameworks = "build\cocoapods\framework/shared.framework" + spec.libraries = "c++" + spec.module_name = "#{spec.name}_umbrella" + + spec.ios.deployment_target = '14.1' + + + + spec.pod_target_xcconfig = { + 'KOTLIN_PROJECT_PATH' => ':shared', + 'PRODUCT_MODULE_NAME' => 'shared', + } + + spec.script_phases = [ + { + :name => 'Build shared', + :execution_position => :before_compile, + :shell_path => '/bin/sh', + :script => <<-SCRIPT + if [ "YES" = "$COCOAPODS_SKIP_KOTLIN_BUILD" ]; then + echo "Skipping Gradle build task invocation due to COCOAPODS_SKIP_KOTLIN_BUILD environment variable set to \"YES\"" + exit 0 + fi + set -ev + REPO_ROOT="$PODS_TARGET_SRCROOT" + "$REPO_ROOT/..\gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \ + -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \ + -Pkotlin.native.cocoapods.archs="$ARCHS" \ + -Pkotlin.native.cocoapods.configuration=$CONFIGURATION + SCRIPT + } + ] +end diff --git a/src.kotlin/alphaTab/shared/src/androidMain/AndroidManifest.xml b/src.kotlin/alphaTab/shared/src/androidMain/AndroidManifest.xml new file mode 100644 index 000000000..6882592d2 --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/AlphaTabView.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/AlphaTabView.kt new file mode 100644 index 000000000..2afe56ce5 --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/AlphaTabView.kt @@ -0,0 +1,125 @@ +package alphaTab + +import alphaTab.collections.DoubleList +import alphaTab.model.Score +import alphaTab.model.Track +import alphaTab.platform.android.* +import alphaTab.rendering.layout.HorizontalScreenLayout +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.widget.HorizontalScrollView +import android.widget.RelativeLayout +import android.widget.ScrollView +import androidx.recyclerview.widget.RecyclerView +import net.alphatab.R +import kotlin.contracts.ExperimentalContracts + +@ExperimentalContracts +@ExperimentalUnsignedTypes +class AlphaTabView : RelativeLayout { + private lateinit var _api: AlphaTabApiBase + + private var _tracks: Iterable? = null + public var tracks: Iterable? + get() = _tracks + set(value) { + _tracks = value + renderTracks() + } + + private var _settings: Settings = Settings().apply { + this.player.enableCursor = true + this.player.enablePlayer = true + this.player.enableUserInteraction = true + } + + public var settings: Settings + get() = _settings + set(value) { + _settings = value + (settingsChanged as EventEmitter).trigger() + } + + public var settingsChanged: IEventEmitter = EventEmitter() + + private var _barCursorFillColor: Int = Color.argb(64, 255, 242, 0) + public var barCursorFillColor: Int + get() = _barCursorFillColor + set(value) { + _barCursorFillColor = value + (barCursorFillColorChanged as EventEmitter).trigger() + } + public val barCursorFillColorChanged: IEventEmitter = EventEmitter() + + private var _beatCursorFillColor: Int = Color.argb(191, 64, 64, 255) + public var beatCursorFillColor: Int + get() = _beatCursorFillColor + set(value) { + _beatCursorFillColor = value + (beatCursorFillColorChanged as EventEmitter).trigger() + } + public val beatCursorFillColorChanged: IEventEmitter = EventEmitter() + + private var _selectionFillColor: Int = Color.argb(25, 64, 64, 255) + public var selectionFillColor: Int + get() = _selectionFillColor + set(value) { + _selectionFillColor = value + (selectionFillColorChanged as EventEmitter).trigger() + } + public val selectionFillColorChanged: IEventEmitter = EventEmitter() + + public val api: AlphaTabApiBase + get() = _api + + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + init(context) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + _api.destroy() + } + + private fun init(context: Context) { + AndroidEnvironment.initializeAndroid(context) + inflate(context, R.layout.alphatab_view, this) + + val outerScroll = findViewById(R.id.outerScroll) + val innerScroll = findViewById(R.id.innerScroll) + val renderSurface = findViewById(R.id.renderSurface) + val renderWrapper = findViewById(R.id.renderWrapper) + _api = + AlphaTabApiBase( + AndroidUiFacade(outerScroll, innerScroll, renderWrapper, renderSurface), + this + ) + } + + public fun renderTracks() { + val tracks = _tracks ?: return + + var score: Score? = null + val trackIndexes = DoubleList() + for (track in tracks) { + if (score == null) { + score = track.score + } + if (score == track.score) { + trackIndexes.push(track.index) + } + } + + if (score != null) { + _api.renderScore(score, trackIndexes) + } + } +} diff --git a/src.kotlin/alphaTab/src/jvmMain/kotlin/alphaTab/EnvironmentPartials.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/EnvironmentPartials.kt similarity index 52% rename from src.kotlin/alphaTab/src/jvmMain/kotlin/alphaTab/EnvironmentPartials.kt rename to src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/EnvironmentPartials.kt index cb0cc0433..02e7eb735 100644 --- a/src.kotlin/alphaTab/src/jvmMain/kotlin/alphaTab/EnvironmentPartials.kt +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/EnvironmentPartials.kt @@ -1,17 +1,18 @@ package alphaTab -import alphaTab.platform.jvm.SkiaCanvas +import alphaTab.collections.Map +import alphaTab.platform.android.AndroidCanvas import kotlin.contracts.ExperimentalContracts @ExperimentalUnsignedTypes @ExperimentalContracts -internal actual fun createPlatformSpecificRenderEngines(engines: alphaTab.collections.Map) { +internal actual fun createPlatformSpecificRenderEngines(engines: Map) { engines.set( - "skia", - RenderEngineFactory(true) { SkiaCanvas() } + "android", + RenderEngineFactory(true) { AndroidCanvas() } ) engines.set( "default", - engines.get("skia")!! + engines.get("android")!! ) } diff --git a/src.kotlin/alphaTab/src/jvmCommon/kotlin/alphaTab/core/GlobalsImpl.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/core/GlobalsImpl.kt similarity index 79% rename from src.kotlin/alphaTab/src/jvmCommon/kotlin/alphaTab/core/GlobalsImpl.kt rename to src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/core/GlobalsImpl.kt index f9db44285..8b921147b 100644 --- a/src.kotlin/alphaTab/src/jvmCommon/kotlin/alphaTab/core/GlobalsImpl.kt +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/core/GlobalsImpl.kt @@ -30,13 +30,20 @@ actual fun UByteArray.decodeToString(encoding: String): String { var invariantDoubleFormat = DecimalFormat().apply { this.minimumFractionDigits = 0 - this.maximumFractionDigits = Int.MAX_VALUE + this.maximumFractionDigits = 12 this.decimalFormatSymbols.decimalSeparator = '.' this.isGroupingUsed = false } actual fun Double.toInvariantString(): String { - return invariantDoubleFormat.format(this) + // TODO: On android/java the DecimalFormat is terribly slow, we need a more efficient + // mechanism to convert doubles to string. + val integerPart = this.toInt(); + val fractionalPart = (this - integerPart) + if(fractionalPart > 0.0000001 || fractionalPart < -0.0000001) { + return invariantDoubleFormat.format(this) + } + return this.toInt().toString(); } actual fun String.toDoubleOrNaN(): Double { diff --git a/src.kotlin/alphaTab/src/jvmCommon/kotlin/alphaTab/core/ecmaScript/Date.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/core/ecmaScript/Date.kt similarity index 86% rename from src.kotlin/alphaTab/src/jvmCommon/kotlin/alphaTab/core/ecmaScript/Date.kt rename to src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/core/ecmaScript/Date.kt index 83f934013..5a979a69b 100644 --- a/src.kotlin/alphaTab/src/jvmCommon/kotlin/alphaTab/core/ecmaScript/Date.kt +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/core/ecmaScript/Date.kt @@ -1,6 +1,6 @@ package alphaTab.core.ecmaScript -actual class Date { +internal actual class Date { actual companion object { public actual fun now(): Double { return System.currentTimeMillis().toDouble() diff --git a/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AlphaTabRenderSurface.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AlphaTabRenderSurface.kt new file mode 100644 index 000000000..0ceee29bd --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AlphaTabRenderSurface.kt @@ -0,0 +1,252 @@ +package alphaTab.platform.android + +import alphaTab.Environment +import alphaTab.collections.ObjectDoubleMap +import alphaTab.rendering.RenderFinishedEventArgs +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import android.view.View +import android.widget.HorizontalScrollView +import android.widget.ScrollView +import java.io.Closeable +import java.lang.RuntimeException +import kotlin.contracts.ExperimentalContracts + +@ExperimentalUnsignedTypes +@ExperimentalContracts +internal class RenderPlaceholder(public var result: RenderFinishedEventArgs) : Closeable { + companion object { + const val STATE_LAYOUT_DONE = 0 + const val STATE_RENDER_REQUESTED = 1 + const val STATE_RENDER_DONE = 2 + } + + public var state: Int = STATE_LAYOUT_DONE + public var isVisible: Boolean = false + public var drawingRect: RectF = RectF( + (result.x * Environment.HighDpiFactor).toFloat(), + (result.y * Environment.HighDpiFactor).toFloat(), + ((result.x + result.width) * Environment.HighDpiFactor).toFloat(), + ((result.y + result.height) * Environment.HighDpiFactor).toFloat() + ) + + override fun close() { + val b = result.renderResult + if (b is Bitmap) { + b.recycle() + result.renderResult = null + } + } +} + +@ExperimentalUnsignedTypes +@ExperimentalContracts +internal class AlphaTabRenderSurface(context: Context, attributeSet: AttributeSet) : + View(context, attributeSet), View.OnScrollChangeListener { + private val _placeholders: ArrayList = arrayListOf() + private val _resultIdToIndex: ObjectDoubleMap = ObjectDoubleMap() + + private var _totalWidth: Int = 0 + private var _totalHeight: Int = 0 + + private val _visibleRect = Rect() + private val _visibleAfterScrollRect = Rect() + private var _layoutDirty = true + private var _visibleRectDirty = true + + // remember items which are shown at each edge of the screen + private var _topVisibleItem: RenderPlaceholder? = null + private var _bottomVisibleItem: RenderPlaceholder? = null + private var _leftVisibleItem: RenderPlaceholder? = null + private var _rightVisibleItem: RenderPlaceholder? = null + + public var requestRender: ((resultId: String) -> Unit)? = null + + public fun clearPlaceholders() { + _resultIdToIndex.clear() + val placeholder = _placeholders + for (p in placeholder) { + p.close(); + } + placeholder.clear() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + val verticalScroll = parent.parent as ScrollView + verticalScroll.setOnScrollChangeListener(this) + val horizontalScroll = parent.parent.parent as HorizontalScrollView + horizontalScroll.setOnScrollChangeListener(this) + _visibleRectDirty = true + } + + public fun addPlaceholder(result: RenderFinishedEventArgs) { + _totalWidth = result.totalWidth.toInt() + _totalHeight = result.totalHeight.toInt() + _placeholders.add(RenderPlaceholder(result)) + _resultIdToIndex.set(result.id, (_placeholders.size - 1).toDouble()) + _layoutDirty = true + requestLayout() + postInvalidate() + } + + public fun fillPlaceholder(result: RenderFinishedEventArgs) { + if (_resultIdToIndex.has(result.id)) { + val index = _resultIdToIndex.get(result.id); + _placeholders[index.toInt()].apply { + this.result = result + this.state = RenderPlaceholder.STATE_RENDER_DONE + this.isVisible = true + } + _layoutDirty = true + requestLayout() + postInvalidate() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + setMeasuredDimension( + MeasureSpec.makeMeasureSpec( + (_totalWidth * Environment.HighDpiFactor).toInt(), + MeasureSpec.EXACTLY + ), + MeasureSpec.makeMeasureSpec( + (_totalHeight * Environment.HighDpiFactor).toInt(), + MeasureSpec.EXACTLY + ) + ) + _visibleRectDirty = true; + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + updateVisibleRect() + + var topItem: RenderPlaceholder? = null + var bottomItem: RenderPlaceholder? = null + var leftItem: RenderPlaceholder? = null + var rightItem: RenderPlaceholder? = null + + for (p in _placeholders) { + val r = p.result + + val result = r.renderResult + + if (_visibleRect.intersects( + p.drawingRect.left.toInt(), + p.drawingRect.top.toInt(), + p.drawingRect.right.toInt(), + p.drawingRect.bottom.toInt() + ) + ) { + // remember which items are on each edge of the screen + // to optimize the checks during scroll if a new item became likely visible + if (topItem == null || (p.drawingRect.top < topItem.drawingRect.top)) { + topItem = p + } + if (bottomItem == null || (p.drawingRect.bottom > bottomItem.drawingRect.bottom)) { + bottomItem = p + } + if (leftItem == null || (p.drawingRect.left < leftItem.drawingRect.left)) { + leftItem = p + } + if (rightItem == null || (p.drawingRect.right > rightItem.drawingRect.right)) { + rightItem = p + } + + if (result == null && p.state != RenderPlaceholder.STATE_RENDER_REQUESTED) { + p.state = RenderPlaceholder.STATE_RENDER_REQUESTED + requestRender?.invoke(r.id) + } + } else if (result is Bitmap) { + p.isVisible = false + p.state = RenderPlaceholder.STATE_LAYOUT_DONE + result.recycle() + r.renderResult = null + } + } + + _topVisibleItem = topItem + _bottomVisibleItem = bottomItem + _leftVisibleItem = leftItem + _rightVisibleItem = rightItem + _layoutDirty = false + } + + private fun updateVisibleRect() { + if (_visibleRectDirty) { + getLocalVisibleRect(_visibleRect) + _visibleAfterScrollRect.set(_visibleRect) + } + _visibleRectDirty = false + } + + override fun onDraw(canvas: Canvas) { + for (p in _placeholders) { + val r = p.result + val result = r.renderResult + if (p.isVisible && result is Bitmap && !result.isRecycled) { + try { + canvas.drawBitmap(result, null, p.drawingRect, null) + } catch (e: RuntimeException) { + // potential race on bitmap recycle + } + } + } + } + + override fun onScrollChange( + v: View?, + scrollX: Int, + scrollY: Int, + oldScrollX: Int, + oldScrollY: Int + ) { + if(!_layoutDirty) { + var anyVisibleItemExceeded = false + + val horizontalScroll = scrollX - oldScrollX + val verticalScroll = scrollY - oldScrollY + _visibleAfterScrollRect.offset(horizontalScroll, verticalScroll) + + val topItem = _topVisibleItem + if (topItem == null) { // no items yet -> do layout + anyVisibleItemExceeded = true + } else { + val bottomItem = _bottomVisibleItem!! + val leftItem = _leftVisibleItem!! + val rightItem = _rightVisibleItem!! + + if (horizontalScroll > 0) { + // scrolling to right (screen goes left) + if (_visibleAfterScrollRect.right > rightItem.drawingRect.right) { + anyVisibleItemExceeded = true + } + } else if (horizontalScroll < 0) { + // scrolling to left (scren goes right) + if (_visibleAfterScrollRect.left < leftItem.drawingRect.left) { + anyVisibleItemExceeded = true + } + } + + if (verticalScroll > 0) { + // scrolling down (screen goes up) + if (_visibleAfterScrollRect.bottom > bottomItem.drawingRect.bottom) { + anyVisibleItemExceeded = true + } + } else if (verticalScroll < 0) { + // scrolling up (screen goes down) + if (_visibleAfterScrollRect.top < bottomItem.drawingRect.top) { + anyVisibleItemExceeded = true + } + } + } + + if (anyVisibleItemExceeded) { + _layoutDirty = true + requestLayout() + postInvalidate() + } + } + } +} diff --git a/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidAudioWorker.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidAudioWorker.kt new file mode 100644 index 000000000..bec7fd95f --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidAudioWorker.kt @@ -0,0 +1,135 @@ +package alphaTab.platform.android + +import android.media.* +import java.util.* +import java.util.concurrent.* +import kotlin.contracts.ExperimentalContracts + +@ExperimentalContracts +@ExperimentalUnsignedTypes +internal class AndroidAudioWorker( + private val _output: AndroidSynthOutput, + sampleRate: Int, + bufferSizeInSamples: Int +) { + private var _updateSchedule: ScheduledFuture<*>? = null + private var _track: AudioTrack + private var _writeThread: Thread? = null + private var _buffer: FloatArray + private var _stopped: Boolean = false + private val _playingSemaphore: Semaphore = Semaphore(1) + private val _updateTimer: ScheduledExecutorService + + init { + val bufferSizeInBytes = bufferSizeInSamples * 4 /*sizeof(float)*/ + + _buffer = FloatArray(bufferSizeInSamples) + _track = AudioTrack( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .build(), + AudioFormat.Builder() + .setSampleRate(sampleRate) + .setEncoding(AudioFormat.ENCODING_PCM_FLOAT) + .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO) + .build(), + bufferSizeInBytes, + AudioTrack.MODE_STREAM, + AudioManager.AUDIO_SESSION_ID_GENERATE + ) + + _track.positionNotificationPeriod = bufferSizeInSamples + _playingSemaphore.acquire() + + _updateTimer = Executors.newScheduledThreadPool(1) + } + + private fun writeSamples() { + while (!_stopped) { + if (_track.playState == AudioTrack.PLAYSTATE_PLAYING) { + _output.read(_buffer, 0, _buffer.size) + if (_previousPosition == -1) { + _previousPosition = _track.playbackHeadPosition + _track.getTimestamp(_timestamp) + } + _track.write(_buffer, 0, _buffer.size, AudioTrack.WRITE_BLOCKING) + } else { + _playingSemaphore.acquire() // wait for playing to start + _playingSemaphore.release() // release semaphore for others + } + } + } + + fun close() { + _playingSemaphore.release() // proceed thread + _stopped = true + _track.stop() + _writeThread!!.interrupt() + _writeThread!!.join() + _track.release() + _updateTimer.shutdown() + } + + fun play() { + if(_track.playState != AudioTrack.PLAYSTATE_PLAYING) { + _previousPosition = _track.playbackHeadPosition + _track.play() + _stopped = false + + _updateSchedule = _updateTimer.scheduleAtFixedRate( + { + this@AndroidAudioWorker.onUpdatePlayedSamples() + }, 0L, 50L, TimeUnit.MILLISECONDS + ) + + _writeThread = Thread { + this@AndroidAudioWorker.writeSamples() + } + _writeThread!!.name = "alphaTab Audio Worker"; + _writeThread!!.start() + _playingSemaphore.release() // proceed thread + } + } + + + fun pause() { + if(_track.playState == AudioTrack.PLAYSTATE_PLAYING) { + _track.pause() + _playingSemaphore.acquire() // block thread + _updateSchedule?.cancel(true) + } + } + + private var _previousPosition: Int = -1 + private val _timestamp = AudioTimestamp() + private val _lastTimestampUpdate: Long = -1L; + + private fun onUpdatePlayedSamples() { + val sinceUpdateInMillis = (System.nanoTime() - _lastTimestampUpdate) / 10e6 + if (sinceUpdateInMillis >= 10000) { + if (!_track.getTimestamp(_timestamp)) { + _timestamp.nanoTime = 0 + _timestamp.framePosition = 0 + } + } + + var samplePosition = _track.playbackHeadPosition + if (_timestamp.nanoTime > 0) { // do we have a timestamp? + samplePosition = (_timestamp.framePosition + + (System.nanoTime() - _timestamp.nanoTime) * _track.sampleRate / 1e9).toInt() + } + + if (_previousPosition == -1) { + return + } + + val playedSamples = samplePosition - _previousPosition + if (playedSamples < 0) { + return + } + + _previousPosition = samplePosition + _output.onSamplesPlayed(playedSamples) + } +} diff --git a/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidCanvas.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidCanvas.kt new file mode 100644 index 000000000..0235226cc --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidCanvas.kt @@ -0,0 +1,343 @@ +package alphaTab.platform.android + +import alphaTab.Environment +import alphaTab.Settings +import alphaTab.core.toCharArray +import alphaTab.model.Color +import alphaTab.model.Font +import alphaTab.model.MusicFontSymbol +import alphaTab.platform.ICanvas +import alphaTab.platform.TextAlign +import alphaTab.platform.TextBaseline +import android.content.Context +import android.graphics.* +import android.util.DisplayMetrics +import java.io.ByteArrayOutputStream +import kotlin.contracts.ExperimentalContracts + +@ExperimentalUnsignedTypes +@ExperimentalContracts +lateinit var MusicFont: Typeface +const val MusicFontSize = 34 + +const val HangingAsPercentOfAscent = 80 + +val CustomTypeFaces = HashMap() + +@ExperimentalUnsignedTypes +@ExperimentalContracts +internal class AndroidCanvas : ICanvas { + companion object { + public fun initialize(context: Context) { + MusicFont = Typeface.createFromAsset(context.assets, "Bravura.ttf") + } + + public fun registerCustomFont(name: String, face: Typeface) { + CustomTypeFaces[customTypeFaceKey(name, face)] = face + } + + private fun customTypeFaceKey(name: String, typeface: Typeface): String { + return customTypeFaceKey(name, typeface.isBold, typeface.isItalic) + } + + private fun customTypeFaceKey( + fontFamily: String, + isBold: Boolean, + isItalic: Boolean + ): String { + return fontFamily.lowercase() + "_" + isBold + "_" + isItalic + } + } + + private lateinit var _surface: Bitmap + private lateinit var _canvas: Canvas + private var _path: Path? = null + private var _typeFaceCache: String = "" + private var _typeFace: Typeface? = null + + public override var color: Color = Color(255.0, 255.0, 255.0) + public override var lineWidth: Double = 1.0 + public override var font: Font = Font("Arial", 10.0) + private val typeFace: Typeface + get() { + if (_typeFaceCache != font.toCssString(settings.display.scale)) { + _typeFaceCache = font.toCssString(settings.display.scale) + + val key = customTypeFaceKey(font.family, font.isBold, font.isItalic) + _typeFace = if (!CustomTypeFaces.containsKey(key)) { + Typeface.create( + font.family, + if (font.isBold && font.isItalic) Typeface.BOLD_ITALIC + else if (font.isBold) Typeface.BOLD + else Typeface.NORMAL + ) + } else { + CustomTypeFaces[key]!! + } + } + return _typeFace!! + } + + + public override var textAlign: TextAlign = TextAlign.Left + public override var textBaseline: TextBaseline = TextBaseline.Top + public override lateinit var settings: Settings + + override fun beginRender(width: Double, height: Double) { + val newImage = Bitmap.createBitmap( + (width * Environment.HighDpiFactor).toInt(), + (height * Environment.HighDpiFactor).toInt(), + Bitmap.Config.ARGB_8888 + ) + newImage.isPremultiplied = true + + _surface = newImage + _canvas = Canvas(_surface) + _canvas.scale(Environment.HighDpiFactor.toFloat(), Environment.HighDpiFactor.toFloat()) + _path?.close() + + textBaseline = TextBaseline.Top + + val path = Path() + path.fillType = Path.FillType.WINDING + _path = path + } + + override fun endRender(): Any { + return _surface + } + + override fun onRenderFinished(): Any? { + return null + } + + override fun fillRect(x: Double, y: Double, w: Double, h: Double) { + createPaint().let { + it.style = Paint.Style.FILL + _canvas.drawRect( + RectF( + x.toInt().toFloat(), + y.toInt().toFloat(), + (x.toInt() + w).toFloat(), + (y.toInt() + h).toFloat() + ), it + ) + } + } + + private fun createPaint(): Paint { + val paint = Paint() + paint.isAntiAlias = true + paint.isDither = false + paint.setARGB(color.a.toInt(), color.r.toInt(), color.g.toInt(), color.b.toInt()) + return paint + } + + override fun strokeRect(x: Double, y: Double, w: Double, h: Double) { + createPaint().let { + it.style = Paint.Style.STROKE + _canvas.drawRect( + RectF( + x.toInt().toFloat(), + y.toInt().toFloat(), + (x.toInt() + w).toFloat(), + (y.toInt() + h).toFloat() + ), it + ) + } + } + + override fun beginPath() { + _path!!.reset() + } + + override fun closePath() { + _path!!.close() + } + + override fun moveTo(x: Double, y: Double) { + _path!!.moveTo(x.toFloat(), y.toFloat()) + } + + override fun lineTo(x: Double, y: Double) { + _path!!.lineTo(x.toFloat(), y.toFloat()) + } + + override fun quadraticCurveTo(cpx: Double, cpy: Double, x: Double, y: Double) { + _path!!.quadTo(cpx.toFloat(), cpy.toFloat(), x.toFloat(), y.toFloat()) + } + + override fun bezierCurveTo( + cp1X: Double, + cp1Y: Double, + cp2X: Double, + cp2Y: Double, + x: Double, + y: Double + ) { + _path!!.cubicTo( + cp1X.toFloat(), + cp1Y.toFloat(), + cp2X.toFloat(), + cp2Y.toFloat(), + x.toFloat(), + y.toFloat() + ) + } + + override fun fillCircle(x: Double, y: Double, radius: Double) { + beginPath() + _path!!.addCircle(x.toFloat(), y.toFloat(), radius.toFloat(), Path.Direction.CW) + closePath() + fill() + } + + + override fun strokeCircle(x: Double, y: Double, radius: Double) { + beginPath() + _path!!.addCircle(x.toFloat(), y.toFloat(), radius.toFloat(), Path.Direction.CW) + closePath() + stroke() + } + + override fun fill() { + createPaint().let { + it.strokeWidth = 0f + it.style = Paint.Style.FILL + _canvas.drawPath(_path!!, it) + } + _path!!.reset() + } + + override fun stroke() { + createPaint().let { + it.strokeWidth = lineWidth.toFloat() + it.style = Paint.Style.STROKE + _canvas.drawPath(_path!!, it) + } + _path!!.reset() + } + + override fun beginGroup(identifier: String) { + } + + override fun endGroup() { + } + + override fun fillText(text: String, x: Double, y: Double) { + textRun(typeFace, font.size * settings.display.scale, fun(paint) { + paint.textAlign = when (textAlign) { + TextAlign.Left -> Paint.Align.LEFT + TextAlign.Center -> Paint.Align.CENTER + TextAlign.Right -> Paint.Align.RIGHT + } + + val fontBaseLine = getFontBaseLine( + textBaseline, + paint + ) + + _canvas.drawText( + text, + x.toFloat(), + y.toFloat() + fontBaseLine, + paint + ) + }) + } + + private fun textRun( + typeFace: Typeface, + size: Double, + action: (paint: Paint) -> Unit + ) { + val paint = createPaint() + paint.style = Paint.Style.FILL + + paint.typeface = typeFace + paint.textSize = size.toFloat() + paint.isSubpixelText = true + paint.hinting = Paint.HINTING_ON + paint.isAntiAlias = true + + action(paint) + } + + private fun getFontBaseLine( + textBaseline: TextBaseline, + paint: Paint + ): Float { + // https://github.com/chromium/chromium/blob/99314be8152e688bafbbf9a615536bdbb289ea87/third_party/blink/renderer/core/html/canvas/text_metrics.cc#L14 + return when (textBaseline) { + TextBaseline.Top -> // kHangingTextBaseline + -paint.fontMetrics.ascent * HangingAsPercentOfAscent / 100.0f + TextBaseline.Middle -> {// kMiddleTextBaseline + (-paint.fontMetrics.ascent - paint.fontMetrics.descent) / 2.0f + } + TextBaseline.Bottom -> {// kBottomTextBaseline + -paint.fontMetrics.descent + } + } + } + + + override fun measureText(text: String): Double { + if (text.isEmpty()) { + return 0.0 + } + var size = 0.0 + + textRun(typeFace, font.size, fun(paint) { + val bounds = Rect() + paint.getTextBounds(text, 0, text.length, bounds) + size = bounds.width().toDouble() + }) + return size + } + + override fun fillMusicFontSymbol( + x: Double, + y: Double, + scale: Double, + symbol: MusicFontSymbol, + centerAtPosition: Boolean? + ) { + fillMusicFontSymbols(x, y, scale, alphaTab.collections.List(symbol), centerAtPosition) + } + + override fun fillMusicFontSymbols( + x: Double, + y: Double, + scale: Double, + symbols: alphaTab.collections.List, + centerAtPosition: Boolean? + ) { + val s = String(symbols + .filter { it != MusicFontSymbol.None } + .map { it.value.toChar() } + .toCharArray() + ) + + textRun(MusicFont, MusicFontSize * scale, fun(paint) { + if (centerAtPosition == true) { + paint.textAlign = Paint.Align.CENTER + } + _canvas.drawText( + s, + x.toFloat(), + y.toFloat(), + paint + ) + }) + } + + override fun beginRotate(centerX: Double, centerY: Double, angle: Double) { + _canvas.save() + _canvas.translate(centerX.toFloat(), centerY.toFloat()) + _canvas.rotate(angle.toFloat()) + } + + override fun endRotate() { + _canvas.restore() + } +} diff --git a/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidEnvironment.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidEnvironment.kt new file mode 100644 index 000000000..622297d6d --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidEnvironment.kt @@ -0,0 +1,23 @@ +package alphaTab.platform.android + +import alphaTab.Environment +import android.util.DisplayMetrics +import kotlin.contracts.ExperimentalContracts + +internal class AndroidEnvironment { + companion object { + private var _isInitialized: Boolean = false + @ExperimentalUnsignedTypes + @ExperimentalContracts + public fun initializeAndroid(context:android.content.Context) { + if(_isInitialized) { + return; + } + _isInitialized = true + + Environment.HighDpiFactor = context.resources.displayMetrics.density.toDouble() + + AndroidCanvas.initialize(context); + } + } +} diff --git a/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidMouseEventArgs.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidMouseEventArgs.kt new file mode 100644 index 000000000..90ab5e5cd --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidMouseEventArgs.kt @@ -0,0 +1,45 @@ +package alphaTab.platform.android + +import alphaTab.Environment +import alphaTab.platform.IContainer +import alphaTab.platform.IMouseEventArgs +import android.view.MotionEvent +import android.view.View +import kotlin.contracts.ExperimentalContracts + +@ExperimentalContracts +@ExperimentalUnsignedTypes +internal class AndroidMouseEventArgs( + private val _event: MotionEvent +) : IMouseEventArgs { + private var _defaultPrevented = false + public val defaultPrevented: Boolean get() = _defaultPrevented + + override val isLeftMouseButton: Boolean + get() = true + + override fun getX(relativeTo: IContainer): Double { + val location = IntArray(2) + if (relativeTo is AndroidRootViewContainer) { + relativeTo.renderSurface.getLocationOnScreen(location) + } else if (relativeTo is AndroidViewContainer) { + relativeTo.view.getLocationOnScreen(location) + } + + return (_event.rawX - location[0]) / Environment.HighDpiFactor + } + + override fun getY(relativeTo: IContainer): Double {val location = IntArray(2) + if (relativeTo is AndroidRootViewContainer) { + relativeTo.renderSurface.getLocationOnScreen(location) + } else if (relativeTo is AndroidViewContainer) { + relativeTo.view.getLocationOnScreen(location) + } + + return (_event.rawY - location[1]) / Environment.HighDpiFactor + } + + override fun preventDefault() { + _defaultPrevented = true + } +} diff --git a/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidRootViewContainer.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidRootViewContainer.kt new file mode 100644 index 000000000..38094779b --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidRootViewContainer.kt @@ -0,0 +1,108 @@ +package alphaTab.platform.android + +import alphaTab.* +import alphaTab.platform.IContainer +import alphaTab.platform.IMouseEventArgs +import android.annotation.SuppressLint +import android.view.View +import android.widget.HorizontalScrollView +import android.widget.ScrollView +import kotlin.contracts.ExperimentalContracts + +@ExperimentalContracts +@ExperimentalUnsignedTypes +@SuppressLint("ClickableViewAccessibility") +internal class AndroidRootViewContainer : IContainer, View.OnLayoutChangeListener { + private val _outerScroll: HorizontalScrollView + private val _innerScroll: ScrollView + internal val renderSurface: AlphaTabRenderSurface + + public constructor( + outerScroll: HorizontalScrollView, + innerScroll: ScrollView, + renderSurface: AlphaTabRenderSurface + ) { + _innerScroll = innerScroll + _outerScroll = outerScroll + this.renderSurface = renderSurface + outerScroll.addOnLayoutChangeListener(this) + } + + fun destroy() { + _outerScroll.removeOnLayoutChangeListener(this) + } + + override fun setBounds(x: Double, y: Double, w: Double, h: Double) { + } + + override var width: Double + get() = (_outerScroll.measuredWidth / Environment.HighDpiFactor) + set(value) { + } + override var height: Double + get() = (_outerScroll.measuredHeight / Environment.HighDpiFactor) + set(value) { + } + override val isVisible: Boolean + get() = _outerScroll.visibility == View.VISIBLE + + override var scrollLeft: Double + get() = _outerScroll.scrollX.toDouble() + set(value) { + _outerScroll.scrollX = value.toInt() + } + override var scrollTop: Double + get() = _innerScroll.scrollY.toDouble() + set(value) { + _innerScroll.scrollY = value.toInt() + } + + override fun appendChild(child: IContainer) { + } + + override fun stopAnimation() { + } + + override fun transitionToX(duration: Double, x: Double) { + } + + override fun clear() { + } + + override var resize: IEventEmitter = EventEmitter() + override var mouseDown: IEventEmitterOfT = EventEmitterOfT() + override var mouseMove: IEventEmitterOfT = EventEmitterOfT() + override var mouseUp: IEventEmitterOfT = EventEmitterOfT() + + override fun onLayoutChange( + v: View?, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + val widthChanged = (right - left) != (oldRight - oldLeft) + val heightChanged = (bottom - top) != (oldTop - oldBottom) + if (widthChanged || heightChanged) { + (resize as EventEmitter).trigger() + } + } + + fun scrollToX(offset: Double) { + _outerScroll.smoothScrollTo( + (offset * Environment.HighDpiFactor).toInt(), + _outerScroll.scrollY + ) + } + + fun scrollToY(offset: Double) { + _innerScroll.smoothScrollTo( + _innerScroll.scrollX, + (offset * Environment.HighDpiFactor).toInt() + ) + } +} diff --git a/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidSynthOutput.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidSynthOutput.kt new file mode 100644 index 000000000..83f86e003 --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidSynthOutput.kt @@ -0,0 +1,121 @@ +package alphaTab.platform.android + +import alphaTab.* +import alphaTab.EventEmitter +import alphaTab.EventEmitterOfT +import alphaTab.synth.ISynthOutput +import alphaTab.synth.ds.CircularSampleBuffer +import kotlin.contracts.ExperimentalContracts +import alphaTab.core.ecmaScript.Float32Array + +@ExperimentalUnsignedTypes +@ExperimentalContracts +internal class AndroidSynthOutput( + private val synthInvoke: (action: (() -> Unit)) -> Unit +) : ISynthOutput { + companion object { + private const val BufferSize = 4096 + private const val PreferredSampleRate = 44100 + private const val TotalBufferTimeInMilliseconds = 5000 + } + + private var _bufferCount = 0 + private var _requestedBufferCount = 0 + + private lateinit var _audioContext: AndroidAudioWorker + private lateinit var _circularBuffer: CircularSampleBuffer + + override val sampleRate: Double + get() = PreferredSampleRate.toDouble() + + override fun activate() { + } + + override fun open() { + _bufferCount = (TotalBufferTimeInMilliseconds * PreferredSampleRate / + 1000 / + BufferSize) + _circularBuffer = CircularSampleBuffer((BufferSize * _bufferCount).toDouble()) + + _audioContext = AndroidAudioWorker( + this, + PreferredSampleRate, + BufferSize + ) + + onReady() + } + + private fun onReady() { + synthInvoke { + (ready as EventEmitter).trigger() + } + } + + override fun destroy() { + _audioContext.close() + _circularBuffer.clear() + } + + override fun play() { + requestBuffers() + _audioContext.play() + } + + override fun pause() { + _audioContext.pause() + } + + override fun addSamples(samples: Float32Array) { + _circularBuffer.write(samples, 0.0, samples.length) + _requestedBufferCount-- + + } + + override fun resetSamples() { + _circularBuffer.clear() + } + + private fun requestBuffers() { + // if we fall under the half of buffers + // we request one half + val halfBufferCount = _bufferCount / 2 + val halfSamples = halfBufferCount * BufferSize + // Issue #631: it can happen that requestBuffers is called multiple times + // before we already get samples via addSamples, therefore we need to + // remember how many buffers have been requested, and consider them as available. + val bufferedSamples = _circularBuffer.count + _requestedBufferCount * BufferSize + if (bufferedSamples < halfSamples) { + for (i in 0 until halfBufferCount) { + onSampleRequest() + _requestedBufferCount++ + } + } + } + + private fun onSampleRequest() { + synthInvoke { + (sampleRequest as EventEmitter).trigger() + } + } + + internal fun onSamplesPlayed(samples: Int) { + synthInvoke { + (samplesPlayed as EventEmitterOfT).trigger(samples.toDouble()) + } + } + + fun read(buffer: FloatArray, offset: Int, sampleCount: Int): Int { + val read = Float32Array(sampleCount.toDouble()) + val actual = _circularBuffer.read(read, 0.0, Math.min(read.length, _circularBuffer.count)) + + read.data.copyInto(buffer, offset, 0, sampleCount) + requestBuffers() + + return actual.toInt() + } + + override val ready: IEventEmitter = EventEmitter() + override val samplesPlayed: IEventEmitterOfT = EventEmitterOfT() + override val sampleRequest: IEventEmitter = EventEmitter() +} diff --git a/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt new file mode 100644 index 000000000..8af94e63d --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt @@ -0,0 +1,297 @@ +package alphaTab.platform.android + +import alphaTab.* +import alphaTab.EventEmitter +import alphaTab.collections.List +import alphaTab.core.ecmaScript.Error +import alphaTab.core.ecmaScript.Uint8Array +import alphaTab.midi.MidiEventType +import alphaTab.midi.MidiFile +import alphaTab.synth.* +import android.util.Log +import java.util.concurrent.BlockingQueue +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import kotlin.contracts.ExperimentalContracts + +@ExperimentalUnsignedTypes +@ExperimentalContracts +internal class AndroidThreadAlphaSynthWorkerPlayer : IAlphaSynth, Runnable { + private val _uiInvoke: (action: (() -> Unit)) -> Unit + + private val _workerThread: Thread + private val _workerQueue: BlockingQueue<() -> Unit> + private val _threadStartedEvent: Semaphore + private var _isCancelled = false + + private var _player: AlphaSynth? = null + private val _output: ISynthOutput + private var _logLevel: LogLevel + + constructor( + logLevel: LogLevel, + output: ISynthOutput, + uiInvoke: (action: (() -> Unit)) -> Unit + ) { + _logLevel = logLevel + _output = output + _uiInvoke = uiInvoke + _threadStartedEvent = Semaphore(1) + _threadStartedEvent.acquire() + _workerQueue = LinkedBlockingQueue() + + _workerThread = Thread(this) + _workerThread.name = "alphaSynthWorkerThread" + _workerThread.isDaemon = true + _workerThread.start() + + _threadStartedEvent.acquire() + + _workerQueue.add { initialize() } + } + + public fun addToWorker(action: () -> Unit) { + _workerQueue.add(action) + } + + override fun destroy() { + _isCancelled = true + _workerThread.interrupt() + _workerThread.join() + } + + override fun run() { + _threadStartedEvent.release() + try { + Log.d("AlphaTab", "AlphaSynth worker started") + do { + val item = _workerQueue.poll(500, TimeUnit.MILLISECONDS) + if (!_isCancelled && item != null) { + item() + } + } while (!_isCancelled) + } catch (e: InterruptedException) { + Log.d("AlphaTab", "AlphaSynth worker stopped") + // finished + } + } + + private fun initialize() { + val player = AlphaSynth(_output) + _player = player + player.positionChanged.on { + _uiInvoke { onPositionChanged(it) } + } + player.stateChanged.on { + _uiInvoke { onStateChanged(it) } + } + player.finished.on { + _uiInvoke { onFinished() } + } + player.soundFontLoaded.on { + _uiInvoke { onSoundFontLoaded() } + } + player.soundFontLoadFailed.on { + _uiInvoke { onSoundFontLoadFailed(it) } + } + player.midiLoaded.on { + _uiInvoke { onMidiLoaded(it) } + } + player.midiLoadFailed.on { + _uiInvoke { onMidiLoadFailed(it) } + } + player.readyForPlayback.on { + _uiInvoke { onReadyForPlayback() } + } + player.midiEventsPlayed.on { + _uiInvoke { onMidiEventsPlayed(it) } + } + + _uiInvoke { onReady() } + } + + override val isReady: Boolean + get() = _player?.isReady ?: false + + override val isReadyForPlayback: Boolean + get() = _player?.isReadyForPlayback ?: false + + override val state: PlayerState + get() = _player?.state ?: PlayerState.Paused + + override var logLevel: LogLevel + get() = _logLevel + set(value) { + _logLevel = value + _workerQueue.add { _player?.logLevel = value } + } + + override var masterVolume: Double + get() = _player?.masterVolume ?: 0.0 + set(value) { + _workerQueue.add { _player?.masterVolume = value } + } + + override var countInVolume: Double + get() = _player?.countInVolume ?: 0.0 + set(value) { + _workerQueue.add { _player?.countInVolume = value } + } + + override var midiEventsPlayedFilter: List + get() = _player?.midiEventsPlayedFilter ?: List() + set(value) { + _workerQueue.add { _player?.midiEventsPlayedFilter = value } + } + + override var metronomeVolume: Double + get() = _player?.metronomeVolume ?: 0.0 + set(value) { + _workerQueue.add { _player?.metronomeVolume = value } + } + + override var playbackSpeed: Double + get() = _player?.playbackSpeed ?: 0.0 + set(value) { + _workerQueue.add { _player?.playbackSpeed = value } + } + + override var tickPosition: Double + get() = _player?.tickPosition ?: 0.0 + set(value) { + _workerQueue.add { _player?.tickPosition = value } + } + + override var timePosition: Double + get() = _player?.timePosition ?: 0.0 + set(value) { + _workerQueue.add { _player?.timePosition = value } + } + + + override var playbackRange: PlaybackRange? + get() = _player?.playbackRange + set(value) { + _workerQueue.add { _player?.playbackRange = value } + } + + + override var isLooping: Boolean + get() = _player?.isLooping ?: false + set(value) { + _workerQueue.add { _player?.isLooping = value } + } + + + override fun play(): Boolean { + if (state == PlayerState.Playing || !isReadyForPlayback) { + return false + } + + _workerQueue.add { _player?.play() } + return true + } + + override fun pause() { + _workerQueue.add { _player?.pause() } + } + + override fun playOneTimeMidiFile(midi: MidiFile) { + _workerQueue.add { _player?.playOneTimeMidiFile(midi) } + } + + override fun playPause() { + _workerQueue.add { _player?.playPause() } + } + + override fun stop() { + _workerQueue.add { _player?.stop() } + } + + override fun resetSoundFonts() { + _workerQueue.add { _player?.resetSoundFonts() } + } + + override fun loadSoundFont(data: Uint8Array, append: Boolean) { + _workerQueue.add { _player?.loadSoundFont(data, append) } + } + + override fun loadMidiFile(midi: MidiFile) { + _workerQueue.add { _player?.loadMidiFile(midi) } + } + + override fun setChannelMute(channel: Double, mute: Boolean) { + _workerQueue.add { _player?.setChannelMute(channel, mute) } + } + + override fun resetChannelStates() { + _workerQueue.add { _player?.resetChannelStates() } + } + + override fun setChannelSolo(channel: Double, solo: Boolean) { + _workerQueue.add { _player?.setChannelSolo(channel, solo) } + } + + override fun setChannelVolume(channel: Double, volume: Double) { + _workerQueue.add { _player?.setChannelVolume(channel, volume) } + } + + override val ready: IEventEmitter = EventEmitter() + override val readyForPlayback: IEventEmitter = EventEmitter() + override val finished: IEventEmitter = EventEmitter() + override val soundFontLoaded: IEventEmitter = EventEmitter() + override val soundFontLoadFailed: IEventEmitterOfT = + EventEmitterOfT() + override val midiLoaded: IEventEmitterOfT = + EventEmitterOfT() + override val midiLoadFailed: IEventEmitterOfT = + EventEmitterOfT() + override val stateChanged: IEventEmitterOfT = + EventEmitterOfT() + override val positionChanged: IEventEmitterOfT = + EventEmitterOfT() + override val midiEventsPlayed: IEventEmitterOfT = + EventEmitterOfT() + + + private fun onReady() { + _uiInvoke { (ready as EventEmitter).trigger() } + } + + private fun onReadyForPlayback() { + _uiInvoke { (readyForPlayback as EventEmitter).trigger() } + } + + private fun onFinished() { + _uiInvoke { (finished as EventEmitter).trigger() } + } + + private fun onSoundFontLoaded() { + _uiInvoke { (soundFontLoaded as EventEmitter).trigger() } + } + + private fun onSoundFontLoadFailed(e: Error) { + _uiInvoke { (soundFontLoadFailed as EventEmitterOfT).trigger(e) } + } + + private fun onMidiLoaded(args: PositionChangedEventArgs) { + _uiInvoke { (midiLoaded as EventEmitterOfT).trigger(args) } + } + + private fun onMidiLoadFailed(e: Error) { + _uiInvoke { (midiLoadFailed as EventEmitterOfT).trigger(e) } + } + + private fun onMidiEventsPlayed(e: MidiEventsPlayedEventArgs) { + _uiInvoke { (midiEventsPlayed as EventEmitterOfT).trigger(e) } + } + + private fun onStateChanged(obj: PlayerStateChangedEventArgs) { + _uiInvoke { (stateChanged as EventEmitterOfT).trigger(obj) } + } + + private fun onPositionChanged(obj: PositionChangedEventArgs) { + _uiInvoke { (positionChanged as EventEmitterOfT).trigger(obj) } + } +} diff --git a/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidThreadScoreRenderer.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidThreadScoreRenderer.kt new file mode 100644 index 000000000..560a3f5eb --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidThreadScoreRenderer.kt @@ -0,0 +1,186 @@ +package alphaTab.platform.android + +import alphaTab.* +import alphaTab.collections.DoubleList +import alphaTab.core.ecmaScript.Error +import alphaTab.model.Score +import alphaTab.rendering.IScoreRenderer +import alphaTab.rendering.RenderFinishedEventArgs +import alphaTab.rendering.ScoreRenderer +import alphaTab.rendering.utils.BoundsLookup +import java.util.concurrent.BlockingQueue +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import kotlin.contracts.ExperimentalContracts + +@ExperimentalContracts +@ExperimentalUnsignedTypes +internal class AndroidThreadScoreRenderer : IScoreRenderer, Runnable { + private val _uiInvoke: ( action: (() -> Unit) ) -> Unit + + private val _workerThread: Thread + private val _workerQueue: BlockingQueue<() -> Unit> + private val _threadStartedEvent: Semaphore + private var _isCancelled = false + public lateinit var renderer: ScoreRenderer + private var _width: Double = 0.0 + + public constructor(settings: Settings, uiInvoke: ( action: (() -> Unit) ) -> Unit) { + _uiInvoke = uiInvoke + _threadStartedEvent = Semaphore(1) + _threadStartedEvent.acquire() + _workerQueue = LinkedBlockingQueue() + + _workerThread = Thread(this) + _workerThread.name = "alphaTabRenderThread" + _workerThread.isDaemon = true + _workerThread.start() + + _threadStartedEvent.acquire() + + _workerQueue.add { initialize(settings) } + } + + override fun run() { + _threadStartedEvent.release() + try { + do { + val item = _workerQueue.poll(500, TimeUnit.MILLISECONDS) + if (!_isCancelled && item != null) { + item() + } + } while (!_isCancelled) + } catch (e: InterruptedException) { + // finished + } + } + + private fun initialize(settings: Settings) { + renderer = ScoreRenderer(settings) + renderer.partialRenderFinished.on { + _uiInvoke { onPartialRenderFinished(it) } + } + renderer.partialLayoutFinished.on { + _uiInvoke { onPartialLayoutFinished(it) } + } + renderer.renderFinished.on { + _uiInvoke { onRenderFinished(it) } + } + renderer.postRenderFinished.on { + _uiInvoke { onPostFinished(renderer.boundsLookup) } + } + renderer.preRender.on { + _uiInvoke { onPreRender(it) } + } + renderer.error.on { + _uiInvoke { onError(it) } + } + } + + private fun onPostFinished(boundsLookup: BoundsLookup?) { + this.boundsLookup = boundsLookup + onPostRenderFinished() + } + + override var boundsLookup: BoundsLookup? = null + override var width: Double + get() = _width + set(value) { + _width = value + if (checkAccess()) { + renderer.width = value + } else { + _workerQueue.add { renderer.width = value } + } + } + + override fun render() { + if (checkAccess()) { + renderer.render() + } else { + _workerQueue.add { render() } + } + } + + override fun renderResult(resultId:String) { + if (checkAccess()) { + renderer.renderResult(resultId) + } else { + _workerQueue.add { renderResult(resultId) } + } + } + + override fun resizeRender() { + if (checkAccess()) { + renderer.resizeRender() + } else { + _workerQueue.add { resizeRender() } + } + } + + override fun renderScore(score: Score?, trackIndexes: DoubleList?) { + if (checkAccess()) { + renderer.renderScore(score, trackIndexes) + } else { + _workerQueue.add { + renderScore( + score, + trackIndexes + ) + } + } + } + + override fun updateSettings(settings: Settings) { + if (checkAccess()) { + renderer.updateSettings(settings) + } else { + _workerQueue.add { updateSettings(settings) } + } + } + + private fun checkAccess(): Boolean { + return Thread.currentThread().id == _workerThread.id + } + + override fun destroy() { + _isCancelled = true + _workerThread.interrupt() + _workerThread.join() + } + + override val preRender: IEventEmitterOfT = EventEmitterOfT() + private fun onPreRender(isResize: Boolean) { + (preRender as EventEmitterOfT).trigger(isResize) + } + + override val renderFinished: IEventEmitterOfT = EventEmitterOfT() + private fun onRenderFinished(args: RenderFinishedEventArgs) { + (renderFinished as EventEmitterOfT).trigger(args) + } + + override val partialRenderFinished: IEventEmitterOfT = + EventEmitterOfT() + + private fun onPartialRenderFinished(args: RenderFinishedEventArgs) { + (partialRenderFinished as EventEmitterOfT).trigger(args) + } + + override val partialLayoutFinished: IEventEmitterOfT = + EventEmitterOfT() + + private fun onPartialLayoutFinished(args: RenderFinishedEventArgs) { + (partialLayoutFinished as EventEmitterOfT).trigger(args) + } + + override val postRenderFinished: IEventEmitter = EventEmitter() + private fun onPostRenderFinished() { + (postRenderFinished as EventEmitter).trigger() + } + + override val error: IEventEmitterOfT = EventEmitterOfT() + private fun onError(e: Error) { + (error as EventEmitterOfT).trigger(e) + } +} diff --git a/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidUiFacade.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidUiFacade.kt new file mode 100644 index 000000000..f4b00f195 --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidUiFacade.kt @@ -0,0 +1,423 @@ +package alphaTab.platform.android + +import alphaTab.* +import alphaTab.EventEmitter +import alphaTab.core.ecmaScript.Error +import alphaTab.core.ecmaScript.Uint8Array +import alphaTab.importer.ScoreLoader +import alphaTab.model.Score +import alphaTab.platform.Cursors +import alphaTab.platform.IContainer +import alphaTab.platform.IMouseEventArgs +import alphaTab.platform.IUiFacade +import alphaTab.rendering.IScoreRenderer +import alphaTab.rendering.RenderFinishedEventArgs +import alphaTab.rendering.utils.Bounds +import alphaTab.synth.IAlphaSynth +import android.annotation.SuppressLint +import android.os.Handler +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.widget.HorizontalScrollView +import android.widget.RelativeLayout +import android.widget.ScrollView +import androidx.core.view.children +import java.io.ByteArrayOutputStream +import java.io.InputStream +import kotlin.contracts.ExperimentalContracts + + +@ExperimentalContracts +@ExperimentalUnsignedTypes +@SuppressLint("ClickableViewAccessibility") +internal class AndroidUiFacade : IUiFacade { + private var _handler: Handler + private var _internalRootContainerBecameVisible: EventEmitter? = EventEmitter() + private val _outerScroll: SuspendableHorizontalScrollView + private val _innerScroll: SuspendableScrollView + private val _renderSurface: AlphaTabRenderSurface + private val _renderWrapper: RelativeLayout + + public constructor( + outerScroll: SuspendableHorizontalScrollView, + innerScroll: SuspendableScrollView, + renderWrapper: RelativeLayout, + renderSurface: AlphaTabRenderSurface + ) { + _outerScroll = outerScroll + _innerScroll = innerScroll + _renderSurface = renderSurface + _renderWrapper = renderWrapper + + rootContainer = AndroidRootViewContainer(outerScroll, innerScroll, renderSurface) + _handler = Handler(outerScroll.context.mainLooper) + + rootContainerBecameVisible = object : IEventEmitter, + ViewTreeObserver.OnGlobalLayoutListener, View.OnLayoutChangeListener { + override fun on(value: () -> Unit) { + if (rootContainer.isVisible) { + value() + } else { + outerScroll.viewTreeObserver.addOnGlobalLayoutListener(this) + outerScroll.addOnLayoutChangeListener(this) + } + } + + override fun off(value: () -> Unit) { + _internalRootContainerBecameVisible?.off(value) + } + + override fun onGlobalLayout() { + outerScroll.viewTreeObserver.removeOnGlobalLayoutListener(this) + outerScroll.removeOnLayoutChangeListener(this) + if (rootContainer.isVisible) { + _internalRootContainerBecameVisible?.trigger() + _internalRootContainerBecameVisible = null + } + } + + override fun onLayoutChange( + v: View?, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + onGlobalLayout() + } + } + } + + + public override val resizeThrottle: Double + get() = 25.0 + + public override val areWorkersSupported: Boolean + get() = true + + override val canRender: Boolean + get() = true + + public lateinit var api: AlphaTabApiBase + public lateinit var settingsContainer: AlphaTabView + + public override fun initialize(api: AlphaTabApiBase, settings: AlphaTabView) { + this.api = api + settingsContainer = settings + api.settings = settings.settings + _renderSurface.requestRender = { + api.renderer.renderResult(it) + } + settings.settingsChanged.on(this::onSettingsChanged) + settingsContainer.barCursorFillColorChanged.on { + (this._cursors?.barCursor as AndroidViewContainer?)?.view?.setBackgroundColor( + settingsContainer.barCursorFillColor + ) + } + settingsContainer.beatCursorFillColorChanged.on { + (this._cursors?.beatCursor as AndroidViewContainer?)?.view?.setBackgroundColor( + settingsContainer.barCursorFillColor + ) + } + settingsContainer.selectionFillColorChanged.on { + val selectionWrapper = (this._cursors?.selectionWrapper as AndroidViewContainer?)?.view + if (selectionWrapper is ViewGroup) { + for (c in selectionWrapper.children) { + c.setBackgroundColor( + settingsContainer.selectionFillColor + ) + } + } + } + } + + private fun onSettingsChanged() { + api.settings = settingsContainer.settings + api.updateSettings() + api.render() + } + + override fun createWorkerRenderer(): IScoreRenderer { + return AndroidThreadScoreRenderer(api.settings, this::beginInvoke) + } + + private fun openDefaultSoundFont(): InputStream { + return settingsContainer.context.assets.open("sonivox.sf2") + } + + override fun createWorkerPlayer(): IAlphaSynth { + var player: AndroidThreadAlphaSynthWorkerPlayer? = null + player = AndroidThreadAlphaSynthWorkerPlayer( + api.settings.core.logLevel, + AndroidSynthOutput { + player!!.addToWorker(it) + }, + this::beginInvoke + ) + player.ready.on { + val soundFont = openDefaultSoundFont() + val bos = ByteArrayOutputStream() + soundFont.use { + soundFont.copyTo(bos) + player.loadSoundFont(Uint8Array(bos.toByteArray().toUByteArray()), false) + } + } + return player + } + + override var rootContainer: IContainer + + private val _canRenderChanged: EventEmitter = EventEmitter() + override val canRenderChanged: IEventEmitter + get() = _canRenderChanged + + override var rootContainerBecameVisible: IEventEmitter + + override fun destroy() { + settingsContainer.settingsChanged.off(this::onSettingsChanged) + (rootContainer as AndroidRootViewContainer).destroy() + } + + override fun triggerEvent( + container: IContainer, + eventName: String, + details: Any?, + originalEvent: IMouseEventArgs? + ) { + } + + override fun initialRender() { + api.renderer.preRender.on { _ -> + _renderSurface.clearPlaceholders() + } + + rootContainerBecameVisible.on { + api.renderer.width = rootContainer.width + api.renderer.updateSettings(api.settings) + renderTracks() + } + } + + private fun renderTracks() { + settingsContainer.renderTracks() + } + + override fun beginAppendRenderResults(renderResults: RenderFinishedEventArgs?) { + _handler.post { + if (renderResults != null) { + _renderSurface.addPlaceholder(renderResults) + } + } + } + + override fun beginUpdateRenderResults(renderResults: RenderFinishedEventArgs) { + _handler.post { + _renderSurface.fillPlaceholder(renderResults) + } + } + + override fun destroyCursors() { + val cursors = _cursors + if (cursors != null) { + (cursors.cursorWrapper as AndroidViewContainer).destroy() + (cursors.beatCursor as AndroidViewContainer).destroy() + (cursors.barCursor as AndroidViewContainer).destroy() + (cursors.selectionWrapper as AndroidViewContainer).destroy() + + _renderWrapper.removeView( + (cursors.cursorWrapper as AndroidViewContainer).view + ) + _renderWrapper.removeView( + (cursors.selectionWrapper as AndroidViewContainer).view + ) + } + } + + private var _cursors: Cursors? = null + override fun createCursors(): Cursors? { + val cursorWrapper = object : RelativeLayout(_renderWrapper.context) { + override fun onTouchEvent(event: MotionEvent?): Boolean { + return false + } + } + cursorWrapper.layoutParams = RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, + RelativeLayout.LayoutParams.MATCH_PARENT + ) + _renderWrapper.addView(cursorWrapper) + + val selectionWrapper = object : RelativeLayout(_renderWrapper.context) { + override fun onTouchEvent(event: MotionEvent?): Boolean { + return false + } + } + selectionWrapper.layoutParams = RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, + RelativeLayout.LayoutParams.MATCH_PARENT + ) + _renderWrapper.addView(selectionWrapper) + + val barCursor = object : View(_renderWrapper.context) { + override fun onTouchEvent(event: MotionEvent?): Boolean { + return false + } + } + barCursor.layoutParams = RelativeLayout.LayoutParams( + 0, + 0 + ) + barCursor.setBackgroundColor(settingsContainer.barCursorFillColor) + cursorWrapper.addView(barCursor) + + val beatCursor = object : View(_renderWrapper.context) { + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent?): Boolean { + return false + } + } + beatCursor.layoutParams = RelativeLayout.LayoutParams( + (3 * Environment.HighDpiFactor).toInt(), + 0 + ) + beatCursor.setBackgroundColor(settingsContainer.beatCursorFillColor) + cursorWrapper.addView(beatCursor) + + _cursors = Cursors( + AndroidViewContainer(cursorWrapper), + AndroidViewContainer(barCursor), + AndroidViewContainer(beatCursor), + AndroidViewContainer(selectionWrapper) + ) + return _cursors + } + + override fun createCanvasElement(): IContainer { + val c = AndroidViewContainer(_renderSurface) + c.enableUserInteraction(_outerScroll, _innerScroll) + return c + } + + override fun beginInvoke(action: () -> Unit) { + _handler.post(action) + } + + override fun removeHighlights() { + } + + override fun createSelectionElement(): IContainer? { + val selection = object : View(_renderWrapper.context) { + override fun onTouchEvent(event: MotionEvent?): Boolean { + return false + } + } + selection.layoutParams = RelativeLayout.LayoutParams( + 0, + 0 + ) + selection.setBackgroundColor(settingsContainer.selectionFillColor) + + return AndroidViewContainer(selection) + } + + override fun highlightElements(groupId: String, masterBarIndex: Double) { + } + + override fun getScrollContainer(): IContainer { + return rootContainer + } + + override fun getOffset(scrollElement: IContainer?, container: IContainer): Bounds { + val bounds = Bounds() + // TODO + bounds.x = 0.0 + bounds.y = 0.0 + bounds.w = container.width + bounds.h = container.height + return bounds + } + + override fun scrollToX(scrollElement: IContainer, offset: Double, speed: Double) { + val view = (scrollElement as AndroidRootViewContainer) + view.scrollToX(offset) + } + + override fun scrollToY(scrollElement: IContainer, offset: Double, speed: Double) { + val view = (scrollElement as AndroidRootViewContainer) + view.scrollToY(offset) + } + + override fun load( + data: Any?, + success: (arg1: Score) -> Unit, + error: (arg1: Error) -> Unit + ): Boolean { + when (data) { + data is Score -> { + success(data as Score) + return true + } + data is ByteArray -> { + success( + ScoreLoader.loadScoreFromBytes( + Uint8Array((data as ByteArray).asUByteArray()), + api.settings + ) + ) + return true + } + data is UByteArray -> { + success( + ScoreLoader.loadScoreFromBytes( + Uint8Array((data as UByteArray)), + api.settings + ) + ) + return true + } + data is InputStream -> { + val bos = ByteArrayOutputStream() + (data as InputStream).copyTo(bos) + success( + ScoreLoader.loadScoreFromBytes( + Uint8Array(bos.toByteArray().asUByteArray()), + api.settings + ) + ) + return true + } + else -> { + return false + } + } + } + + override fun loadSoundFont(data: Any?, append: Boolean): Boolean { + val player = api.player ?: return false + + when (data) { + data is ByteArray -> { + player.loadSoundFont(Uint8Array((data as ByteArray).asUByteArray()), append) + return true + } + data is UByteArray -> { + player.loadSoundFont(Uint8Array((data as UByteArray)), append) + return true + } + data is InputStream -> { + val bos = ByteArrayOutputStream() + (data as InputStream).copyTo(bos) + player.loadSoundFont(Uint8Array(bos.toByteArray().asUByteArray()), append) + return true + } + else -> { + return false + } + } + } +} diff --git a/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidViewContainer.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidViewContainer.kt new file mode 100644 index 000000000..d51d9a05a --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/AndroidViewContainer.kt @@ -0,0 +1,198 @@ +package alphaTab.platform.android + +import alphaTab.* +import alphaTab.platform.IContainer +import alphaTab.platform.IMouseEventArgs +import android.annotation.SuppressLint +import android.os.Handler +import android.util.Log +import android.view.* +import android.view.animation.Animation +import android.view.animation.LinearInterpolator +import android.view.animation.Transformation +import android.widget.RelativeLayout +import kotlin.contracts.ExperimentalContracts +import kotlin.math.abs + +@ExperimentalContracts +@ExperimentalUnsignedTypes +@SuppressLint("ClickableViewAccessibility") +internal class AndroidViewContainer : GestureDetector.SimpleOnGestureListener, IContainer, + View.OnLayoutChangeListener, View.OnTouchListener { + internal val view: View + private var _horizontalScrollView: SuspendableHorizontalScrollView? = null + private var _verticalScrollView: SuspendableScrollView? = null + private var _gestureDetector: GestureDetector? = null + + public constructor(view: View) { + this.view = view + this.view.addOnLayoutChangeListener(this) + } + + public fun enableUserInteraction( + horizontalScrollView: SuspendableHorizontalScrollView, + verticalScrollView: SuspendableScrollView + ) { + _gestureDetector = GestureDetector(view.context!!, this) + view.setOnTouchListener(this) + _horizontalScrollView = horizontalScrollView + _verticalScrollView = verticalScrollView + } + + fun destroy() { + this.view.removeOnLayoutChangeListener(this) + this.view.setOnTouchListener(null) + } + + + private var _isLongDown = false + override fun onLongPress(e: MotionEvent) { + Log.d("AlphaTab", "Long down ${e.rawX} ${e.rawY}") + _isLongDown = true + _horizontalScrollView!!.isUserScrollingEnabled = false + _verticalScrollView!!.isUserScrollingEnabled = false + (mouseDown as EventEmitterOfT).trigger(AndroidMouseEventArgs(e)) + } + + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + val down = AndroidMouseEventArgs(e) + (mouseDown as EventEmitterOfT).trigger(down) + + return if (down.defaultPrevented) { + val up = AndroidMouseEventArgs(e) + (mouseUp as EventEmitterOfT).trigger(up) + + true + } else { + false + } + } + + override fun onTouch(v: View, event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_MOVE -> { + if (_isLongDown) { + val args = AndroidMouseEventArgs(event) + (this.mouseMove as EventEmitterOfT).trigger(args) + } + } + MotionEvent.ACTION_UP -> { + _horizontalScrollView!!.isUserScrollingEnabled = true + _verticalScrollView!!.isUserScrollingEnabled = true + + if (_isLongDown) { // was long press -> raise mouse up + _isLongDown = false + val args = AndroidMouseEventArgs(event) + (this.mouseUp as EventEmitterOfT).trigger(args) + } + } + } + + _gestureDetector!!.onTouchEvent(event) + return true + } + + override fun setBounds(x: Double, y: Double, w: Double, h: Double) { + val params = view.layoutParams + if (params is RelativeLayout.LayoutParams) { + params.setMargins( + (x * Environment.HighDpiFactor).toInt(), + (y * Environment.HighDpiFactor).toInt(), + 0, + 0 + ) + params.width = (w * Environment.HighDpiFactor).toInt() + params.height = (h * Environment.HighDpiFactor).toInt() + } + + view.requestLayout() + } + + override var width: Double + get() = (view.measuredWidth / Environment.HighDpiFactor) + set(value) { + val scaled = (value * Environment.HighDpiFactor).toInt() + val params = view.layoutParams + if (params != null) { + params.width = scaled + } + view.minimumWidth = scaled + } + override var height: Double + get() = view.measuredHeight.toDouble() + set(value) { + val scaled = (value * Environment.HighDpiFactor).toInt() + val params = view.layoutParams + if (params != null) { + params.height = scaled + } + view.minimumHeight = scaled + } + override val isVisible: Boolean + get() = view.visibility == View.VISIBLE && view.width > 0 + override var scrollLeft: Double + get() = 0.0 + set(value) { + } + override var scrollTop: Double + get() = 0.0 + set(value) { + } + + override fun appendChild(child: IContainer) { + val childView = (child as AndroidViewContainer).view + val group = view + if (group is ViewGroup) { + group.addView(childView) + } + } + + override fun stopAnimation() { + view.clearAnimation() + } + + override fun transitionToX(duration: Double, x: Double) { + val params = view.layoutParams as RelativeLayout.LayoutParams + val startX = params.leftMargin + val endX = x * Environment.HighDpiFactor + val a: Animation = object : Animation() { + override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { + params.leftMargin = (startX + ((endX - startX) * interpolatedTime)).toInt() + view.requestLayout() + } + } + a.interpolator = LinearInterpolator() + a.duration = duration.toLong() + view.startAnimation(a) + } + + override fun clear() { + val group = view + if (group is ViewGroup) { + group.removeAllViews() + } + } + + override var resize: IEventEmitter = EventEmitter() + override var mouseDown: IEventEmitterOfT = EventEmitterOfT() + override var mouseMove: IEventEmitterOfT = EventEmitterOfT() + override var mouseUp: IEventEmitterOfT = EventEmitterOfT() + + override fun onLayoutChange( + v: View?, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + val widthChanged = (right - left) != (oldRight - oldLeft) + val heightChanged = (bottom - top) != (oldTop - oldBottom) + if (widthChanged || heightChanged) { + (resize as EventEmitter).trigger() + } + } +} diff --git a/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/SuspendableHorizontalScrollView.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/SuspendableHorizontalScrollView.kt new file mode 100644 index 000000000..7cc39d732 --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/SuspendableHorizontalScrollView.kt @@ -0,0 +1,26 @@ +package alphaTab.platform.android + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.widget.HorizontalScrollView + +internal class SuspendableHorizontalScrollView(context: Context, attributeSet: AttributeSet) : + HorizontalScrollView(context, attributeSet) { + public var isUserScrollingEnabled = true + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(ev: MotionEvent): Boolean { + return when (ev.action) { + MotionEvent.ACTION_DOWN -> + isUserScrollingEnabled && super.onTouchEvent(ev) + else -> + super.onTouchEvent(ev) + } + } + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + return isUserScrollingEnabled && super.onInterceptTouchEvent(ev) + } +} diff --git a/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/SuspendableScrollView.kt b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/SuspendableScrollView.kt new file mode 100644 index 000000000..58d9c5088 --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidMain/kotlin/alphaTab/platform/android/SuspendableScrollView.kt @@ -0,0 +1,27 @@ +package alphaTab.platform.android + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.widget.ScrollView + +internal class SuspendableScrollView(context: Context, attributeSet: AttributeSet) : + ScrollView(context, attributeSet) { + + public var isUserScrollingEnabled = true + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(ev: MotionEvent): Boolean { + return when (ev.action) { + MotionEvent.ACTION_DOWN -> + isUserScrollingEnabled && super.onTouchEvent(ev) + else -> + super.onTouchEvent(ev) + } + } + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + return isUserScrollingEnabled && super.onInterceptTouchEvent(ev) + } +} diff --git a/src.kotlin/alphaTab/shared/src/androidTest/AndroidManifest.xml b/src.kotlin/alphaTab/shared/src/androidTest/AndroidManifest.xml new file mode 100644 index 000000000..0051f423e --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidTest/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/TestPlatformPartialsImpl.kt b/src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/TestPlatformPartialsImpl.kt new file mode 100644 index 000000000..e1ecdfe47 --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/TestPlatformPartialsImpl.kt @@ -0,0 +1,147 @@ +package alphaTab + +import alphaTab.core.ecmaScript.Uint8Array +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues +import android.os.Bundle +import android.os.Environment +import android.provider.MediaStore +import androidx.test.platform.app.InstrumentationRegistry +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.nio.file.Paths +import kotlin.contracts.ExperimentalContracts + + +@ExperimentalUnsignedTypes +@ExperimentalContracts +public class TestPlatformPartials { + companion object { + public fun loadFile(path: String): Uint8Array { + val fs = openFileRead(path) + val ms = ByteArrayOutputStream() + fs.use { + fs.copyTo(ms) + } + return Uint8Array(ms.toByteArray().asUByteArray()) + } + + private val isInstrumented: Boolean by lazy { + try { + InstrumentationRegistry.getInstrumentation().context.toString() + true + } catch (e: IllegalStateException) { + false + } + } + + public val projectRoot: String by lazy { + var path = Paths.get("").toAbsolutePath() + while(!Paths.get(path.toString(), "package.json").toFile().exists()) { + path = path.parent + ?: throw AlphaTabError(AlphaTabErrorType.General, "Could not find project root") + } + println(path.toString()) + path.toString() + } + + private fun openFileRead(path: String): InputStream { + var subpath = Paths.get(path) + subpath = subpath.subpath(1, subpath.nameCount) + + return if (isInstrumented) { + val testContext = InstrumentationRegistry.getInstrumentation().context + val assets = testContext.assets + assets.open(subpath.toString()) + } else { + val filePath = Paths.get(projectRoot, path) + filePath.toFile().inputStream() + } + } + + private fun openFileWrite(path: String): OutputStream { + val subpath = Paths.get("test-results", path) + + return if (isInstrumented) { + val testContext = InstrumentationRegistry.getInstrumentation().context + val values = ContentValues() + + values.put( + MediaStore.Files.FileColumns.DISPLAY_NAME, + subpath.fileName.toString() + ) + + values.put( + MediaStore.Files.FileColumns.MIME_TYPE, + "image/png" + ) + + values.put( + MediaStore.Files.FileColumns.RELATIVE_PATH, + "${Environment.DIRECTORY_DOCUMENTS}/${subpath.parent}" + ) + + val existing = testContext.contentResolver.query( + MediaStore.Files.getContentUri("external"), + arrayOf(MediaStore.Files.FileColumns._ID), + Bundle().apply { + putString( + ContentResolver.QUERY_ARG_SQL_SELECTION, + "${MediaStore.Files.FileColumns.RELATIVE_PATH}=? AND ${MediaStore.Files.FileColumns.DISPLAY_NAME}=?" + ) + putStringArray( + ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf( + values.getAsString(MediaStore.Files.FileColumns.RELATIVE_PATH), + values.getAsString(MediaStore.Files.FileColumns.DISPLAY_NAME) + ) + ) + }, + null + ) + + val uri = if (existing != null && existing.count > 0 && existing.moveToFirst()) { + ContentUris.withAppendedId( + MediaStore.Files.getContentUri("external"), + existing.getLong(0) + ) + } else { + testContext.contentResolver.insert( + MediaStore.Files.getContentUri("external"), values + )!! + } + + Logger.info("Test", "Saving file '$path' to '$uri'") + testContext.contentResolver.openOutputStream(uri)!! + } else { + val fullPath = Paths.get(projectRoot, subpath.toString()) + Logger.info("Test", "Saving file '$path' to '$fullPath'") + fullPath.parent.toFile().mkdirs() + fullPath.toFile().outputStream() + } + } + + public fun saveFile(name: String, data: Uint8Array) { + val fs = openFileWrite(name) + fs.use { + fs.write(data.buffer.asByteArray()) + } + } + + public fun listDirectory(path: String): alphaTab.collections.List { + return if (isInstrumented) { + val testContext = InstrumentationRegistry.getInstrumentation().context + val assets = testContext.assets + alphaTab.collections.List(assets.list(path)!!.asIterable()) + } else { + val dirPath = Paths.get(projectRoot, path) + alphaTab.collections.List(dirPath.toFile() + .listFiles() + ?.filter { it.isFile } + ?.map { it.name } ?: emptyList()) + } + + } + } +} diff --git a/src.kotlin/alphaTab/src/jvmTest/kotlin/alphaTab/model/ComparisonHelpersPartials.kt b/src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/model/ComparisonHelpersPartialsImpl.kt similarity index 86% rename from src.kotlin/alphaTab/src/jvmTest/kotlin/alphaTab/model/ComparisonHelpersPartials.kt rename to src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/model/ComparisonHelpersPartialsImpl.kt index f822a85e3..839cfad73 100644 --- a/src.kotlin/alphaTab/src/jvmTest/kotlin/alphaTab/model/ComparisonHelpersPartials.kt +++ b/src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/model/ComparisonHelpersPartialsImpl.kt @@ -6,9 +6,9 @@ import kotlin.contracts.ExperimentalContracts @ExperimentalUnsignedTypes @ExperimentalContracts -class ComparisonHelpersPartials { - companion object { - public fun compareObjects(expected: Any?, actual: Any?, path: String, ignoreKeys: alphaTab.collections.List?): Boolean { +actual class ComparisonHelpersPartials { + actual companion object { + public actual fun compareObjects(expected: Any?, actual: Any?, path: String, ignoreKeys: alphaTab.collections.List?): Boolean { if (actual is DoubleList && expected is DoubleList) { if (actual.length != expected.length) { Globals.fail("""Double Array Length mismatch on hierarchy: ${path}, ${actual.length} != ${expected.length}""") diff --git a/src.kotlin/alphaTab/src/jvmTest/kotlin/alphaTab/test/Globals.kt b/src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/test/Globals.kt similarity index 60% rename from src.kotlin/alphaTab/src/jvmTest/kotlin/alphaTab/test/Globals.kt rename to src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/test/Globals.kt index b6bcc17da..ce6f3befc 100644 --- a/src.kotlin/alphaTab/src/jvmTest/kotlin/alphaTab/test/Globals.kt +++ b/src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/test/Globals.kt @@ -1,24 +1,18 @@ package alphaTab.test -import org.junit.Assert - -class Globals { +public class Globals { companion object { public fun expect(actual: T): Expector { return Expector(actual) } public fun fail(message: Any?) { - Assert.fail(message.toString()) - } - - public fun fail(message: Throwable) { - Assert.fail(message.toString() + message.stackTraceToString()) + kotlin.test.fail(message.toString()) } } } -class Expector { +public class Expector { private val _actual: T private var _message: String = "" @@ -36,14 +30,14 @@ class Expector { if (exp is Int && _actual is Double) { exp = exp.toDouble() } - Assert.assertEquals(_message + message, exp, _actual) + kotlin.test.assertEquals(exp, _actual, _message + message) } public fun toBeCloseTo(expected:Double, message:String? = null) { if(_actual is Number) { - Assert.assertEquals(_message + message, expected, _actual.toDouble(), 0.001) + kotlin.test.assertEquals(expected, _actual.toDouble(), 0.001, _message + message) } else { - Assert.fail("ToBeCloseTo can only be used with numeric operands") + kotlin.test.fail("ToBeCloseTo can only be used with numeric operands") } } @@ -52,22 +46,22 @@ class Expector { if(exp is Int && _actual is Double) { exp = exp.toDouble() } - Assert.assertEquals(_message, exp, _actual) + kotlin.test.assertEquals(exp, _actual, _message) } public fun toBeTruthy() { - Assert.assertNotNull(_message, _actual) + kotlin.test.assertNotNull(_actual, _message) } public fun toBeTrue() { if(_actual is Boolean) { - Assert.assertTrue(_message, _actual) + kotlin.test.assertTrue(_actual, _message) } else { - Assert.fail("ToBeTrue can only be used on bools: $_message") + kotlin.test.fail("ToBeTrue can only be used on bools: $_message") } } public fun toBeFalsy() { - Assert.assertNull(_message, _actual) + kotlin.test.assertNull(_actual, _message) } } diff --git a/src.kotlin/alphaTab/src/jvmMain/kotlin/alphaTab/platform/jvm/SkiaCanvas.kt b/src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/visualTests/SkiaCanvas.kt similarity index 81% rename from src.kotlin/alphaTab/src/jvmMain/kotlin/alphaTab/platform/jvm/SkiaCanvas.kt rename to src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/visualTests/SkiaCanvas.kt index 877beceb3..a477f0bda 100644 --- a/src.kotlin/alphaTab/src/jvmMain/kotlin/alphaTab/platform/jvm/SkiaCanvas.kt +++ b/src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/visualTests/SkiaCanvas.kt @@ -1,7 +1,7 @@ -package alphaTab.platform.jvm +package alphaTab.visualTests import alphaTab.Settings -import alphaTab.core.BitConverter +import alphaTab.core.ecmaScript.Uint8Array import alphaTab.core.toCharArray import alphaTab.model.Color import alphaTab.model.Font @@ -15,14 +15,9 @@ import java.lang.IllegalStateException import kotlin.contracts.ExperimentalContracts import kotlin.math.floor - -@ExperimentalUnsignedTypes -@ExperimentalContracts -val bravuraTtf = SkiaCanvas::class.java.getResource("/bravura/Bravura.ttf").readBytes() - @ExperimentalUnsignedTypes @ExperimentalContracts -val MusicFont: Typeface = Typeface.makeFromData(Data.makeFromBytes(bravuraTtf)) +lateinit var MusicFont: Typeface const val MusicFontSize = 34 const val HangingAsPercentOfAscent = 80 @@ -36,8 +31,16 @@ val CustomTypeFaces = HashMap(); @ExperimentalContracts public class SkiaCanvas : ICanvas { companion object { - public fun registerCustomFont(data: UByteArray) { - val skData = Data.makeFromBytes(data.asByteArray()) + public fun initialize(bravura: Uint8Array) { + val skData = Data.makeFromBytes(bravura.buffer.asByteArray()) + skData.use { + val face = Typeface.makeFromData(skData) + MusicFont = face + } + } + + public fun registerCustomFont(data: Uint8Array) { + val skData = Data.makeFromBytes(data.buffer.asByteArray()) skData.use { val face = Typeface.makeFromData(skData) CustomTypeFaces[customTypeFaceKey(face)] = face @@ -376,14 +379,21 @@ public class SkiaCanvas : ICanvas { private fun typoAscenderDescender(typeface: Typeface): Pair { val buffer = typeface.getTableData("OS/2") if (buffer != null && buffer.size >= 72) { - val ascender = BitConverter.getInt16(buffer.bytes, 68, false) - val descender = -BitConverter.getInt16(buffer.bytes, 70, false) + val ascender = getInt16(buffer.bytes, 68, false) + val descender = -getInt16(buffer.bytes, 70, false) return Pair(ascender, descender.toShort()) } return Pair(0.toShort(), 0.toShort()) } + private fun getInt16(src: ByteArray, pos: Int, littleEndian: Boolean): Short { + return java.nio.ByteBuffer + .wrap(src) + .order(if (littleEndian) java.nio.ByteOrder.LITTLE_ENDIAN else java.nio.ByteOrder.BIG_ENDIAN) + .getShort(pos) + } + private fun floatAscent(metrics: FontMetrics): Float { return skScalarRoundToScalar(-metrics.ascent) } @@ -462,93 +472,3 @@ public class SkiaCanvas : ICanvas { _surface.canvas.restore() } } - - -//@ExperimentalUnsignedTypes -//class TextBlobBuilderRunHandler : RunHandler, AutoCloseable { -// private val _blobBuilder: TextBlobBuilder = TextBlobBuilder() -// -// private var _offset = Point(0f, 0f) -// private var _currentPosition = Point(0f, 0f) -// private var _maxRunAscent = 0f -// private var _maxRunDescent = 0f -// private var _maxRunLeading = 0f -// private var _clusterOffset = 0.toUInt() -// private var _glyphCount = 0.toUInt() -// private var _clusters = arrayListOf() -// -//// val points = mutableListOf() -//// for(glyphIndex in shaped.glyphs.indices) { -//// val xOffset = shaped.positions[glyphIndex * 2] -//// val yOffset = -shaped.positions[(glyphIndex * 2) + 1] -//// points.add(Point(xOffset, yOffset)) -//// } -//// val skiaBlob = TextBlob.makeFromPos( -//// shaped.glyphs, -//// points.toTypedArray(), -//// font -//// ) -//// -// -// public fun makeTextBlob(): TextBlob { -// return _blobBuilder.build()!! -// } -// -// override fun beginLine() { -// _currentPosition = _offset -// _maxRunAscent = 0f -// _maxRunDescent = 0f -// _maxRunLeading = 0f -// } -// -// override fun runInfo(info: RunInfo?) { -// if (info != null) { -// val metrics = info.font.metrics -// _maxRunAscent = maxOf(_maxRunAscent, metrics.ascent) -// _maxRunDescent = maxOf(_maxRunDescent, metrics.descent) -// _maxRunLeading = maxOf(_maxRunLeading, metrics.leading) -// } -// } -// -// override fun commitRunInfo() { -// _currentPosition = Point(_currentPosition.x, _currentPosition.y - _maxRunAscent) -// } -// -// override fun runOffset(info: RunInfo?): Point { -// if(info != null){ -// val glyphs = ShortArray(info.glyphCount.toInt()) -// -// -// info. -// _glyphCount = info.glyphCount.toUInt() -// val pos = info.points; -// _blobBuilder.appendRunPos(info.font,glyphs ) -// -// } -// -// return _currentPosition -// } -// -// override fun commitRun( -// info: RunInfo?, -// glyphs: ShortArray?, -// positions: Array?, -// clusters: IntArray? -// ) { -// if (info != null) { -// for (i in 0 until _glyphCount) { -// _clusters[i] -= _clusterOffset -// } -// _currentPosition = -// Point(_currentPosition.x + info.advance.x, _currentPosition.x + info.advance.y) -// } -// } -// -// override fun commitLine() { -// _offset = Point(_offset.x, _offset.x + (_maxRunDescent + _maxRunLeading - _maxRunAscent)) -// } -// -// override fun close() { -// _blobBuilder.close() -// } -//} diff --git a/src.kotlin/alphaTab/src/jvmTest/kotlin/alphaTab/visualTests/VisualTestHelperPartials.kt b/src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/visualTests/VisualTestHelperPartials.kt similarity index 83% rename from src.kotlin/alphaTab/src/jvmTest/kotlin/alphaTab/visualTests/VisualTestHelperPartials.kt rename to src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/visualTests/VisualTestHelperPartials.kt index 174441b90..50fa6ed73 100644 --- a/src.kotlin/alphaTab/src/jvmTest/kotlin/alphaTab/visualTests/VisualTestHelperPartials.kt +++ b/src.kotlin/alphaTab/shared/src/androidTest/kotlin/alphaTab/visualTests/VisualTestHelperPartials.kt @@ -1,32 +1,30 @@ package alphaTab.visualTests -import alphaTab.Settings +import alphaTab.* import alphaTab.TestPlatform -import alphaTab.TestPlatformPartials import alphaTab.collections.DoubleList import alphaTab.core.ecmaScript.Uint8Array import alphaTab.core.toInvariantString import alphaTab.importer.AlphaTexImporter import alphaTab.importer.ScoreLoader import alphaTab.io.ByteBuffer -import alphaTab.model.JsonConverter import alphaTab.model.Score -import alphaTab.platform.jvm.SkiaCanvas import alphaTab.rendering.RenderFinishedEventArgs import alphaTab.rendering.ScoreRenderer import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.jetbrains.skija.* import org.junit.Assert +import java.io.ByteArrayOutputStream +import java.nio.file.Paths import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import kotlin.contracts.ExperimentalContracts -import java.nio.file.Paths @ExperimentalContracts @ExperimentalUnsignedTypes -class VisualTestHelperPartials { +public class VisualTestHelperPartials { companion object { public fun runVisualTest( inputFile: String, @@ -52,7 +50,7 @@ class VisualTestHelperPartials { triggerResize ) } catch (e: Throwable) { - Assert.fail("Failed to run visual test $e") + Assert.fail("Failed to run visual test $e ${e.stackTraceToString()}") } } @@ -62,7 +60,8 @@ class VisualTestHelperPartials { settings: Settings? = null, tracks: DoubleList? = null, message: String? = null, - tolerancePercent: Double = 1.0 + tolerancePercent: Double = 1.0, + triggerResize: Boolean = false ) { try { val actualSettings = settings ?: Settings() @@ -76,13 +75,15 @@ class VisualTestHelperPartials { settings, tracks, message, - tolerancePercent + tolerancePercent, + triggerResize ) } catch (e: Throwable) { Assert.fail("Failed to run visual test $e") } } + private var _initialized: Boolean = false public fun runVisualTestScore( score: Score, referenceFileName: String, @@ -92,6 +93,13 @@ class VisualTestHelperPartials { tolerancePercent: Double = 1.0, triggerResize: Boolean = false ) { + if (!_initialized) { + SkiaCanvas.initialize(TestPlatformPartials.loadFile("test-data/../font/bravura/Bravura.ttf")) + Environment.renderEngines.set("skia", RenderEngineFactory(true) { SkiaCanvas() }) + loadFonts() + _initialized = true + } + val actualSettings = settings ?: Settings() val actualTracks = tracks ?: DoubleList() @@ -111,7 +119,6 @@ class VisualTestHelperPartials { actualSettings.display.resources.fingeringFont.family = "PT Serif" actualSettings.display.resources.markerFont.family = "PT Serif" - loadFonts() var actualReferenceFileName = referenceFileName if (!actualReferenceFileName.startsWith("test-data/")) { @@ -120,7 +127,7 @@ class VisualTestHelperPartials { val referenceFileData = TestPlatformPartials.loadFile(actualReferenceFileName) - var result = ArrayList() + val result = ArrayList() var totalWidth = 0.0 var totalHeight = 0.0 var isResizeRender = false @@ -134,9 +141,7 @@ class VisualTestHelperPartials { var error: Throwable? = null renderer.preRender.on { _ -> - result = ArrayList() - totalWidth = 0.0 - totalHeight = 0.0 + result.clear() } renderer.partialRenderFinished.on { e -> result.add(e) @@ -145,13 +150,12 @@ class VisualTestHelperPartials { totalWidth = e.totalWidth totalHeight = e.totalHeight result.add(e) - if(!triggerResize || isResizeRender) { + if (!triggerResize || isResizeRender) { waitHandle.release() - } else if(triggerResize) { + } else { isResizeRender = true renderer.resizeRender() } - } renderer.error.on { e -> error = e @@ -167,7 +171,7 @@ class VisualTestHelperPartials { } } - if (waitHandle.tryAcquire(100000, TimeUnit.MILLISECONDS)) { + if (waitHandle.tryAcquire(2000, TimeUnit.MILLISECONDS)) { if (error != null) { Assert.fail("Rendering failed with error $error ${error?.stackTraceToString()}") } else { @@ -187,34 +191,34 @@ class VisualTestHelperPartials { } } - private var _fontsLoaded = false - private fun loadFonts() - { - if (_fontsLoaded) - { - return; - } - - _fontsLoaded = true; - val fonts = arrayOf( - "font/roboto/Roboto-Regular.ttf", - "font/roboto/Roboto-Italic.ttf", - "font/roboto/Roboto-Bold.ttf", - "font/roboto/Roboto-BoldItalic.ttf", - "font/ptserif/PTSerif-Regular.ttf", - "font/ptserif/PTSerif-Italic.ttf", - "font/ptserif/PTSerif-Bold.ttf", - "font/ptserif/PTSerif-BoldItalic.ttf" + private fun loadFonts() { + SkiaCanvas.registerCustomFont( + TestPlatformPartials.loadFile("test-data/../font/roboto/Roboto-Regular.ttf") + ) + SkiaCanvas.registerCustomFont( + TestPlatformPartials.loadFile("test-data/../font/roboto/Roboto-Italic.ttf") + ) + SkiaCanvas.registerCustomFont( + TestPlatformPartials.loadFile("test-data/../font/roboto/Roboto-Bold.ttf") + ) + SkiaCanvas.registerCustomFont( + TestPlatformPartials.loadFile("test-data/../font/roboto/Roboto-BoldItalic.ttf") ) - for (font in fonts) - { - val data = TestPlatformPartials.loadFile(font) - SkiaCanvas.registerCustomFont(data.buffer.raw) - } + SkiaCanvas.registerCustomFont( + TestPlatformPartials.loadFile("test-data/../font/ptserif/PTSerif-Regular.ttf") + ) + SkiaCanvas.registerCustomFont( + TestPlatformPartials.loadFile("test-data/../font/ptserif/PTSerif-Italic.ttf") + ) + SkiaCanvas.registerCustomFont( + TestPlatformPartials.loadFile("test-data/../font/ptserif/PTSerif-Bold.ttf") + ) + SkiaCanvas.registerCustomFont( + TestPlatformPartials.loadFile("test-data/../font/ptserif/PTSerif-BoldItalic.ttf") + ) } - private fun compareVisualResult( totalWidth: Double, totalHeight: Double, @@ -252,7 +256,7 @@ class VisualTestHelperPartials { dir.toFile().mkdirs() val referenceBitmap = - Image.makeFromEncoded(referenceFileData.buffer.raw.asByteArray()) + Image.makeFromEncoded(referenceFileData.buffer.asByteArray()) var pass:Boolean var msg:String @@ -288,7 +292,7 @@ class VisualTestHelperPartials { val diff = Image.makeRaster( imageInfo, - diffData.buffer.raw.asByteArray(), + diffData.buffer.asByteArray(), imageInfo.minRowBytes ) diff.use { diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/EnvironmentPartials.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/EnvironmentPartials.kt similarity index 94% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/EnvironmentPartials.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/EnvironmentPartials.kt index 5bbe0a094..3b96f5516 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/EnvironmentPartials.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/EnvironmentPartials.kt @@ -1,7 +1,6 @@ package alphaTab import kotlinx.coroutines.* -import kotlinx.coroutines.flow.flow import kotlin.contracts.ExperimentalContracts @ExperimentalContracts @@ -11,7 +10,7 @@ internal expect fun createPlatformSpecificRenderEngines(engines: alphaTab.collec @Suppress("UNUSED_PARAMETER") @kotlin.contracts.ExperimentalContracts @ExperimentalUnsignedTypes -class EnvironmentPartials { +internal class EnvironmentPartials { companion object { internal fun createPlatformSpecificRenderEngines(engines: alphaTab.collections.Map) { alphaTab.createPlatformSpecificRenderEngines(engines) diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/BooleanList.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/BooleanList.kt similarity index 100% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/BooleanList.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/BooleanList.kt diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/DoubleBooleanMap.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/DoubleBooleanMap.kt similarity index 100% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/DoubleBooleanMap.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/DoubleBooleanMap.kt diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/DoubleDoubleMap.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/DoubleDoubleMap.kt similarity index 100% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/DoubleDoubleMap.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/DoubleDoubleMap.kt diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/DoubleList.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/DoubleList.kt similarity index 100% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/DoubleList.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/DoubleList.kt diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/DoubleObjectMap.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/DoubleObjectMap.kt similarity index 97% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/DoubleObjectMap.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/DoubleObjectMap.kt index e812b634b..b9bdbd1e3 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/DoubleObjectMap.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/DoubleObjectMap.kt @@ -23,6 +23,7 @@ public open class DoubleObjectMapEntry { return _value } + @Suppress("UNCHECKED_CAST") public constructor() { _key = 0.0 _value = null as TValue @@ -39,6 +40,7 @@ public class DoubleObjectMapEntryInternal : DoubleObjectMapEntry public override var hashCode: Int = 0 public override var next: Int = 0 + @Suppress("UNCHECKED_CAST") override fun reset() { key = 0.0 value = null as TValue @@ -72,6 +74,7 @@ public class DoubleObjectMap : insert(key, value) } + @Suppress("UNCHECKED_CAST") private fun insert(key: Double, value: TValue) { insertInternal( key, value as Any, diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/HashHelpers.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/HashHelpers.kt similarity index 100% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/HashHelpers.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/HashHelpers.kt diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/KeyNotFoundException.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/KeyNotFoundException.kt similarity index 100% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/KeyNotFoundException.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/KeyNotFoundException.kt diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/List.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/List.kt similarity index 100% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/List.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/List.kt diff --git a/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/Map.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/Map.kt new file mode 100644 index 000000000..18cf3f864 --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/Map.kt @@ -0,0 +1,156 @@ +package alphaTab.collections + +public open class MapEntry { + private var _key: TKey + public var key: TKey + get() = _key + internal set(value) { + _key = value + } + + private var _value: TValue + public var value: TValue + get() = _value + internal set(value) { + _value = value + } + + public operator fun component1(): TKey { + return _key + } + + public operator fun component2(): TValue { + return _value + } + + @Suppress("UNCHECKED_CAST") + public constructor() { + _key = null as TKey + _value = null as TValue + } + + public constructor(key: TKey, value: TValue) { + _key = key + _value = value + } +} + + +public class MapEntryInternal : MapEntry(), + IMapEntryInternal { + public override var hashCode: Int = 0 + public override var next: Int = 0 + + @Suppress("UNCHECKED_CAST") + override fun reset() { + key = null as TKey + value = null as TValue + } +} + +public class Map: + MapBase, MapEntryInternal> { + public constructor() + public constructor(iterable: Iterable>) { + for (it in iterable) { + set(it.key, it.value) + } + } + + @Suppress("UNCHECKED_CAST") + public fun has(key: TKey): Boolean { + return findEntryInternal(key as Any?, + { entry, k -> entry.key == (k as TKey) }) >= 0 + } + + @Suppress("UNCHECKED_CAST") + public fun get(key: TKey): TValue { + val i = findEntryInternal(key as Any?, + { entry, k -> entry.key == (k as TKey) }) + if (i >= 0) { + return entries[i].value + } + throw KeyNotFoundException() + } + + public fun set(key: TKey, value: TValue) { + insert(key, value) + } + + @Suppress("UNCHECKED_CAST") + private fun insert(key: TKey, value: TValue) { + insertInternal( + key as Any?, value as Any?, + { entry, k -> entry.key = k as TKey }, + { entry, v -> entry.value = v as TValue }, + { entry, k -> entry.key == (k as TKey) } + ) + } + + public fun delete(key: TKey) { + deleteInternal(key.hashCode()) + } + + private var _values: ValueCollection? = null + public fun values(): Iterable { + _values = _values ?: ValueCollection(this) + return _values!! + } + + private var _keys: KeyCollection? = null + public fun keys(): Iterable { + _keys = _keys ?: KeyCollection(this) + return _keys!! + } + + override fun createEntries(size: Int): Array> { + return Array(size) { + MapEntryInternal() + } + } + + override fun createEntries( + size: Int, + old: Array> + ): Array> { + return Array(size) { + if (it < old.size) old[it] else MapEntryInternal() + } + } + + private class ValueCollection(private val map: Map) : + Iterable { + override fun iterator(): Iterator { + return ValueIterator(map.iterator()) + } + + private class ValueIterator(private val iterator: Iterator>) : + Iterator { + override fun hasNext(): Boolean { + return iterator.hasNext() + } + + override fun next(): TValue { + return iterator.next().value + } + } + } + + + private class KeyCollection(private val map: Map) : Iterable { + override fun iterator(): Iterator { + return KeyIterator(map.iterator()) + } + + private class KeyIterator(private val iterator: Iterator>) : + Iterator { + override fun hasNext(): Boolean { + return iterator.hasNext() + } + + override fun next(): TKey { + return iterator.next().key + } + } + } +} diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/MapBase.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/MapBase.kt similarity index 99% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/MapBase.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/MapBase.kt index 837b766f9..dc5d3c388 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/MapBase.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/MapBase.kt @@ -179,6 +179,7 @@ public abstract class MapBase= 0) { diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/ObjectBooleanMap.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/ObjectBooleanMap.kt similarity index 96% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/ObjectBooleanMap.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/ObjectBooleanMap.kt index 9b6b21540..52960d671 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/ObjectBooleanMap.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/ObjectBooleanMap.kt @@ -16,6 +16,7 @@ public open class ObjectBooleanMapEntry { } + @Suppress("UNCHECKED_CAST") public constructor() { _key = null as TKey _value = false @@ -40,6 +41,7 @@ public class ObjectBooleanMapEntryInternal : ObjectBooleanMapEntry() public override var hashCode: Int = 0 public override var next: Int = 0 + @Suppress("UNCHECKED_CAST") override fun reset() { key = null as TKey value = false @@ -55,11 +57,13 @@ public class ObjectBooleanMap : } } + @Suppress("UNCHECKED_CAST") public fun has(key: TKey): Boolean { return findEntryInternal(key as Any, { entry, k -> entry.key == (k as TKey) }) >= 0 } + @Suppress("UNCHECKED_CAST") public fun get(key: TKey): Boolean { val i = findEntryInternal(key as Any, { entry, k -> entry.key == (k as TKey) }) @@ -73,6 +77,7 @@ public class ObjectBooleanMap : insert(key, value) } + @Suppress("UNCHECKED_CAST") private fun insert(key: TKey, value: Boolean) { insertInternal( key as Any, value, diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/ObjectDoubleMap.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/ObjectDoubleMap.kt similarity index 96% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/ObjectDoubleMap.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/ObjectDoubleMap.kt index af420d3cf..6a606ec98 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/ObjectDoubleMap.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/collections/ObjectDoubleMap.kt @@ -15,6 +15,7 @@ public open class ObjectDoubleMapEntry { _value = value } + @Suppress("UNCHECKED_CAST") public constructor() { _key = null as TKey _value = 0.0 @@ -31,6 +32,7 @@ public class ObjectDoubleMapEntryInternal : ObjectDoubleMapEntry(), public override var hashCode: Int = 0 public override var next: Int = 0 + @Suppress("UNCHECKED_CAST") override fun reset() { key = null as TKey value = 0.0 @@ -46,11 +48,13 @@ public class ObjectDoubleMap : } } + @Suppress("UNCHECKED_CAST") public fun has(key: TKey): Boolean { return findEntryInternal(key as Any, { entry, k -> entry.key == (k as TKey) }) >= 0 } + @Suppress("UNCHECKED_CAST") public fun get(key: TKey): Double { val i = findEntryInternal(key as Any, { entry, k -> entry.key == (k as TKey) }) @@ -64,6 +68,7 @@ public class ObjectDoubleMap : insert(key, value) } + @Suppress("UNCHECKED_CAST") private fun insert(key: TKey, value: Double) { insertInternal( key as Any, value, diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/Console.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/Console.kt similarity index 96% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/Console.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/Console.kt index 91058a861..4788b1602 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/Console.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/Console.kt @@ -1,6 +1,6 @@ package alphaTab.core -open class Console { +internal open class Console { public open fun debug(format: String, vararg details: Any?) { val message = if (details.isNotEmpty()) "$format,${details.joinToString(",")}" else format println("[AlphaTab Debug] $message") diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/Globals.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/Globals.kt similarity index 53% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/Globals.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/Globals.kt index bec4834d9..26e3d31f3 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/Globals.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/Globals.kt @@ -4,102 +4,88 @@ import alphaTab.collections.List import alphaTab.core.ecmaScript.RegExp @kotlin.ExperimentalUnsignedTypes -expect fun UByteArray.decodeToFloatArray(): FloatArray +internal expect fun UByteArray.decodeToFloatArray(): FloatArray @kotlin.ExperimentalUnsignedTypes -expect fun UByteArray.decodeToDoubleArray(): DoubleArray +internal expect fun UByteArray.decodeToDoubleArray(): DoubleArray @kotlin.ExperimentalUnsignedTypes -expect fun UByteArray.decodeToString(encoding: String): String +internal expect fun UByteArray.decodeToString(encoding: String): String -fun > List.sort(): Unit { +internal fun > List.sort() { this.sort { a,b -> a.compareTo(b).toDouble() } } -fun String.substr(startIndex: Double, length: Double): String { +internal fun String.substr(startIndex: Double, length: Double): String { return this.substring(startIndex.toInt(), (startIndex + length).toInt()) } -fun String.substr(startIndex: Double): String { +internal fun String.substr(startIndex: Double): String { return this.substring(startIndex.toInt()) } -fun String.splitBy(separator:String): List { +internal fun String.splitBy(separator:String): List { return List(this.split(separator)) } -fun String.replace(pattern: RegExp, replacement: String): String { +internal fun String.replace(pattern: RegExp, replacement: String): String { return pattern.replace(this, replacement) } -fun Iterable.toCharArray(): CharArray { - return this.toList().toCharArray() -} - -fun String.indexOfInDouble(item: String): Double { +internal fun String.indexOfInDouble(item: String): Double { return this.indexOf(item).toDouble() } -fun Double.toInvariantString(base: Double): String { +internal fun Double.toInvariantString(base: Double): String { return this.toInt().toString(base.toInt()) } -expect fun Double.toInvariantString(): String -fun IAlphaTabEnum.toInvariantString(): String { +internal expect fun Double.toInvariantString(): String +internal fun IAlphaTabEnum.toInvariantString(): String { return this.toString() } -fun String.lastIndexOfInDouble(item: String): Double { +internal fun String.lastIndexOfInDouble(item: String): Double { return this.lastIndexOf(item).toDouble() } -operator fun Double.plus(str: String): String { - return this.toString() + str +internal operator fun Double.plus(str: String): String { + return this.toInvariantString() + str } -fun String.charAt(index: Double): String { +internal fun String.charAt(index: Double): String { return this.substring(index.toInt(), index.toInt() + 1) } -fun String.charCodeAt(index: Int): Double { +internal fun String.charCodeAt(index: Int): Double { return this[index].code.toDouble() } -fun String.charCodeAt(index: Double): Double { +internal fun String.charCodeAt(index: Double): Double { return this[index.toInt()].code.toDouble() } -fun String.split(delimiter: String): List { +internal fun String.split(delimiter: String): List { @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS") - return alphaTab.collections.List(this.split(delimiters = arrayOf(delimiter), ignoreCase = false, limit = 0)) + return List(this.split(delimiters = arrayOf(delimiter), ignoreCase = false, limit = 0)) } -fun String.substring(startIndex: Double, endIndex: Double): String { +internal fun String.substring(startIndex: Double, endIndex: Double): String { return this.substring(startIndex.toInt(), endIndex.toInt()) } -fun String.substring(startIndex: Double): String { +internal fun String.substring(startIndex: Double): String { return this.substring(startIndex.toInt()) } -operator fun Int.rangeTo(d: Double): IntRange { - return this.rangeTo(d.toInt()) -} - -//fun Any?.toDouble(): Double { -// if (this is Double) { -// return this -// } -// return this.toString().toDouble() -//} -fun IAlphaTabEnum.toDouble(): Double { +internal fun IAlphaTabEnum.toDouble(): Double { return this.value.toDouble() } -fun IAlphaTabEnum?.toDouble(): Double? { +internal fun IAlphaTabEnum?.toDouble(): Double? { return this?.value.toDouble() } -fun Any?.toDouble(): Double { +internal fun Any?.toDouble(): Double { if(this is Double) { return this } @@ -108,14 +94,14 @@ fun Any?.toDouble(): Double { } throw ClassCastException("Cannot cast ${this::class.simpleName} to double") } -fun Int?.toDouble(): Double? { +internal fun Int?.toDouble(): Double? { return this?.toDouble() } -expect fun String.toDoubleOrNaN(): Double; -expect fun String.toIntOrNaN(): Double; +internal expect fun String.toDoubleOrNaN(): Double; +internal expect fun String.toIntOrNaN(): Double; -class Globals { +internal class Globals { companion object { const val NaN: Double = Double.NaN val console = Console() @@ -141,7 +127,7 @@ class Globals { } } -public fun List.toCharArray(): CharArray { +internal fun List.toCharArray(): CharArray { val result = CharArray(length.toInt()) var index = 0 for (element in this) { diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/IAlphaTabEnum.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/IAlphaTabEnum.kt similarity index 60% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/IAlphaTabEnum.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/IAlphaTabEnum.kt index b9d866038..ffe341af9 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/IAlphaTabEnum.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/IAlphaTabEnum.kt @@ -1,5 +1,5 @@ package alphaTab.core -interface IAlphaTabEnum { +public interface IAlphaTabEnum { public val value: Int } diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/TypeHelper.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/TypeHelper.kt similarity index 80% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/TypeHelper.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/TypeHelper.kt index 5a75829a2..a549ea2a3 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/TypeHelper.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/TypeHelper.kt @@ -4,7 +4,7 @@ import alphaTab.core.ecmaScript.RegExp import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract -class TypeHelper { +internal class TypeHelper { companion object { public fun createRegex(pattern: String, flags: String): RegExp { return RegExp(pattern, flags) @@ -47,13 +47,14 @@ class TypeHelper { } } - public fun createMapEntry(k: K, v: V): Pair { - return Pair(k, v) + @Suppress("NOTHING_TO_INLINE") + public inline fun setInitializer(vararg values: T): Iterable { + return values.asIterable() } - public fun setInitializer(vararg values:T) : Iterable - { - return values.map { it } + @Suppress("NOTHING_TO_INLINE") + public inline fun mapInitializer(vararg values: T): Iterable { + return values.asIterable(); } } } diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/dom/TextDecoder.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/dom/TextDecoder.kt similarity index 72% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/dom/TextDecoder.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/dom/TextDecoder.kt index 3783844bd..e1b257205 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/dom/TextDecoder.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/dom/TextDecoder.kt @@ -3,11 +3,11 @@ package alphaTab.core.dom import alphaTab.core.decodeToString import alphaTab.core.ecmaScript.ArrayBuffer -class TextDecoder(encoding:String) { +internal class TextDecoder(encoding:String) { private val _encoding:String = encoding @ExperimentalUnsignedTypes public fun decode(buffer: ArrayBuffer): String { - return buffer.raw.decodeToString(_encoding) + return buffer.decodeToString(_encoding) } } diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/dom/TextEncoder.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/dom/TextEncoder.kt similarity index 88% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/dom/TextEncoder.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/dom/TextEncoder.kt index e0320a8ca..12fa357ef 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/dom/TextEncoder.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/dom/TextEncoder.kt @@ -1,6 +1,6 @@ package alphaTab.core.dom -class TextEncoder { +internal class TextEncoder { @ExperimentalUnsignedTypes public fun encode(str: String): alphaTab.core.ecmaScript.Uint8Array { return alphaTab.core.ecmaScript.Uint8Array(str.encodeToByteArray().toUByteArray()) diff --git a/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Array.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Array.kt new file mode 100644 index 000000000..cd1e9910d --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Array.kt @@ -0,0 +1,13 @@ +package alphaTab.core.ecmaScript + +@Suppress("NOTHING_TO_INLINE") +internal class Array { + companion object { + public inline fun from(x: Iterable): alphaTab.collections.List { + return alphaTab.collections.List(x) + } + public inline fun isArray(x:Any?):Boolean { + return x is alphaTab.collections.List<*> + } + } +} diff --git a/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/ArrayBuffer.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/ArrayBuffer.kt new file mode 100644 index 000000000..f51b8d80b --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/ArrayBuffer.kt @@ -0,0 +1,4 @@ +package alphaTab.core.ecmaScript + +@ExperimentalUnsignedTypes +internal typealias ArrayBuffer = UByteArray diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/CoreString.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/CoreString.kt similarity index 60% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/CoreString.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/CoreString.kt index 533049c38..a27f28579 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/CoreString.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/CoreString.kt @@ -1,8 +1,8 @@ package alphaTab.core.ecmaScript -class CoreString { +internal class CoreString { companion object{ - public fun fromCharCode(code:Double): kotlin.String { + public fun fromCharCode(code:Double): String { return code.toInt().toChar().toString(); } } diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Date.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Date.kt similarity index 77% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Date.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Date.kt index 238b33489..fb4f33d65 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Date.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Date.kt @@ -1,6 +1,6 @@ package alphaTab.core.ecmaScript -expect class Date { +internal expect class Date { companion object { public fun now(): Double } diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Error.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Error.kt similarity index 100% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Error.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Error.kt diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Float32Array.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Float32Array.kt similarity index 74% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Float32Array.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Float32Array.kt index 8e12e4e4f..dc351b234 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Float32Array.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Float32Array.kt @@ -2,11 +2,12 @@ package alphaTab.core.ecmaScript import alphaTab.core.decodeToFloatArray +@Suppress("NOTHING_TO_INLINE") @ExperimentalUnsignedTypes public class Float32Array : Iterable { - private val data: FloatArray + public val data: FloatArray - public val length: Double + public inline val length: Double get() { return data.size.toDouble() } @@ -16,7 +17,7 @@ public class Float32Array : Iterable { } public constructor(x: ArrayBuffer) { - data = x.raw.decodeToFloatArray() + data = x.decodeToFloatArray() } internal constructor(x: FloatArray) { @@ -27,11 +28,11 @@ public class Float32Array : Iterable { this.data = x.map { d -> d.toFloat() }.toFloatArray() } - public operator fun get(idx: Int): Double { + public inline operator fun get(idx: Int): Double { return data[idx].toDouble() } - public operator fun set(idx: Int, value: Double) { + public inline operator fun set(idx: Int, value: Double) { data[idx] = value.toFloat() } @@ -39,7 +40,7 @@ public class Float32Array : Iterable { return data.iterator() } - public fun set(subarray: Float32Array, pos: Double) { + public inline fun set(subarray: Float32Array, pos: Double) { subarray.data.copyInto(data, pos.toInt(), 0, subarray.data.size) } diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Float64Array.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Float64Array.kt similarity index 79% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Float64Array.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Float64Array.kt index 80f3eb1cc..f8d62d1b8 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Float64Array.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Float64Array.kt @@ -3,11 +3,11 @@ package alphaTab.core.ecmaScript import alphaTab.core.decodeToDoubleArray @ExperimentalUnsignedTypes -public class Float64Array : Iterable { +internal class Float64Array : Iterable { private val data: DoubleArray public constructor(x: ArrayBuffer) { - data = x.raw.decodeToDoubleArray() + data = x.decodeToDoubleArray() } public operator fun get(idx: Int): Double { diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Int16Array.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Int16Array.kt similarity index 63% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Int16Array.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Int16Array.kt index 6fdcfd53b..ec2d15309 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Int16Array.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Int16Array.kt @@ -1,6 +1,7 @@ package alphaTab.core.ecmaScript -class Int16Array : Iterable { +@Suppress("NOTHING_TO_INLINE") +internal class Int16Array : Iterable { private val _data: ShortArray public val length: Double @@ -12,19 +13,19 @@ class Int16Array : Iterable { _data = ShortArray(size.toInt()) } - public operator fun get(index: Double): Double { + public inline operator fun get(index: Double): Double { return _data[index.toInt()].toDouble() } - public operator fun set(index: Double, value: Double) { + public inline operator fun set(index: Double, value: Double) { _data[index.toInt()] = value.toInt().toShort() } - public operator fun get(index: Int): Double { + public inline operator fun get(index: Int): Double { return _data[index].toDouble() } - public operator fun set(index: Int, value: Double) { + public inline operator fun set(index: Int, value: Double) { _data[index] = value.toInt().toShort() } diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Int32Array.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Int32Array.kt similarity index 95% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Int32Array.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Int32Array.kt index 6a4609db5..28212f28f 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Int32Array.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Int32Array.kt @@ -1,6 +1,6 @@ package alphaTab.core.ecmaScript -class Int32Array : Iterable { +internal class Int32Array : Iterable { private val _data: IntArray public val length: Double diff --git a/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Iterable.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Iterable.kt new file mode 100644 index 000000000..1f6312312 --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Iterable.kt @@ -0,0 +1,3 @@ +package alphaTab.core.ecmaScript + +internal typealias Iterable = kotlin.collections.Iterable diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Math.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Math.kt similarity index 98% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Math.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Math.kt index 99a7e5c92..07dc4d21a 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Math.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Math.kt @@ -2,7 +2,7 @@ package alphaTab.core.ecmaScript import kotlin.math.pow -class Math { +internal class Math { companion object { public const val PI: Double = kotlin.math.PI public fun log10(x: Double): Double { diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/RegExp.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/RegExp.kt similarity index 98% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/RegExp.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/RegExp.kt index 6ac0011e1..ff9b4bc25 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/RegExp.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/RegExp.kt @@ -3,7 +3,7 @@ package alphaTab.core.ecmaScript private data class RegExpCacheEntry(val pattern: String, val flags: String) private val RegexpCache = HashMap() -class RegExp { +internal class RegExp { private var _regex: Regex private var _global: Boolean diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Set.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Set.kt similarity index 93% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Set.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Set.kt index 7e5b6a9f4..4efd54436 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Set.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Set.kt @@ -1,6 +1,6 @@ package alphaTab.core.ecmaScript -class Set : Iterable { +internal class Set : Iterable { private val _set: HashSet public constructor() { diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Uint32Array.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Uint32Array.kt similarity index 94% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Uint32Array.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Uint32Array.kt index 148500db3..8e48da5ff 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Uint32Array.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Uint32Array.kt @@ -1,7 +1,7 @@ package alphaTab.core.ecmaScript @ExperimentalUnsignedTypes -class Uint32Array : Iterable { +internal class Uint32Array : Iterable { private val _data: UIntArray public val length: Double diff --git a/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Uint8Array.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Uint8Array.kt new file mode 100644 index 000000000..bd5b86f8d --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/core/ecmaScript/Uint8Array.kt @@ -0,0 +1,48 @@ +package alphaTab.core.ecmaScript + +@Suppress("NOTHING_TO_INLINE") +@ExperimentalUnsignedTypes +class Uint8Array : Iterable { + public val buffer: ArrayBuffer + + public constructor(x: Iterable) { + this.buffer = x.map { d -> d.toInt().toUByte() }.toUByteArray() + } + + public constructor(size: Double) { + this.buffer = UByteArray(size.toInt()) + } + + public constructor(data: UByteArray) { + this.buffer = data + } + + public val length: Double + get() { + return this.buffer.size.toDouble() + } + + public inline operator fun get(idx: Int): Double { + return this.buffer[idx].toDouble() + } + + public inline operator fun get(idx: Double): Double { + return this.buffer[idx.toInt()].toDouble() + } + + public inline operator fun set(idx: Int, value: Double) { + this.buffer[idx] = value.toInt().toUByte() + } + + public inline fun set(subarray: Uint8Array, pos: Double) { + subarray.buffer.copyInto(buffer, pos.toInt(), 0, subarray.buffer.size) + } + + override fun iterator(): Iterator { + return buffer.iterator() + } + + public fun subarray(begin: Double, end: Double): Uint8Array { + return Uint8Array(buffer.copyOfRange(begin.toInt(), end.toInt())) + } +} diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/io/JsonHelperPartials.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/io/JsonHelperPartials.kt similarity index 98% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/io/JsonHelperPartials.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/io/JsonHelperPartials.kt index 1774779c4..5c11d9857 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/io/JsonHelperPartials.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/io/JsonHelperPartials.kt @@ -2,6 +2,7 @@ package alphaTab.io import alphaTab.AlphaTabError import alphaTab.AlphaTabErrorType +import alphaTab.collections.Map import kotlin.contracts.ExperimentalContracts import kotlin.jvm.JvmName import kotlin.reflect.KClass diff --git a/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/io/TypeConversions.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/io/TypeConversions.kt new file mode 100644 index 000000000..3a19ae393 --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/io/TypeConversions.kt @@ -0,0 +1,26 @@ +package alphaTab.io + +@ExperimentalUnsignedTypes +internal class TypeConversions { + companion object { + public fun uint16ToInt16(v: Double): Double { + return v.toUInt().toShort().toDouble() + } + + public fun int16ToUint32(v: Double): Double { + return v.toInt().toShort().toUInt().toDouble() + } + + public fun int32ToUint16(v: Double): Double { + return v.toInt().toUShort().toDouble() + } + + public fun int32ToInt16(v: Double): Double { + return v.toInt().toShort().toDouble() + } + + public fun int32ToUint32(v: Double): Double { + return v.toInt().toUInt().toDouble() + } + } +} diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/platform/svg/FontSizesPartials.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/platform/svg/FontSizesPartials.kt similarity index 79% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/platform/svg/FontSizesPartials.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/platform/svg/FontSizesPartials.kt index 7ca3d8c46..5ea63abe5 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/platform/svg/FontSizesPartials.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/platform/svg/FontSizesPartials.kt @@ -3,7 +3,7 @@ package alphaTab.platform.svg import alphaTab.core.ecmaScript.Uint8Array import kotlin.contracts.ExperimentalContracts -class FontSizesPartials { +internal class FontSizesPartials { companion object { @ExperimentalUnsignedTypes @ExperimentalContracts @@ -12,7 +12,7 @@ class FontSizesPartials { return } - // TODO: maybe allow fallback to GDI/Skia based on availability? + // TODO: maybe allow fallback to System Rendering / Skia based on availability? FontSizes.FontSizeLookupTables.set(family, Uint8Array(ubyteArrayOf((8).toUByte()))) } } diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/util/Lazy.kt b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/util/Lazy.kt similarity index 88% rename from src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/util/Lazy.kt rename to src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/util/Lazy.kt index ef73fe6b0..096370a0f 100644 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/util/Lazy.kt +++ b/src.kotlin/alphaTab/shared/src/commonMain/kotlin/alphaTab/util/Lazy.kt @@ -2,7 +2,7 @@ package alphaTab.util internal object UninitializedValue -class Lazy(factory: () -> T) { +internal class Lazy(factory: () -> T) { private val _factory: () -> T = factory private var _value:Any? = UninitializedValue diff --git a/src.kotlin/alphaTab/shared/src/commonTest/kotlin/alphaTab/model/ComparisonHelpersPartials.kt b/src.kotlin/alphaTab/shared/src/commonTest/kotlin/alphaTab/model/ComparisonHelpersPartials.kt new file mode 100644 index 000000000..8e1e1a767 --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/commonTest/kotlin/alphaTab/model/ComparisonHelpersPartials.kt @@ -0,0 +1,12 @@ +package alphaTab.model + +expect class ComparisonHelpersPartials { + companion object { + public fun compareObjects( + expected: Any?, + actual: Any?, + path: String, + ignoreKeys: alphaTab.collections.List? + ): Boolean; + } +} diff --git a/src.kotlin/alphaTab/shared/src/main/res/layout/alphatab_view.xml b/src.kotlin/alphaTab/shared/src/main/res/layout/alphatab_view.xml new file mode 100644 index 000000000..f51d46f2b --- /dev/null +++ b/src.kotlin/alphaTab/shared/src/main/res/layout/alphatab_view.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/src.kotlin/alphaTab/src/androidMain/AndroidManifest.xml b/src.kotlin/alphaTab/src/androidMain/AndroidManifest.xml deleted file mode 100644 index 8c79aa73b..000000000 --- a/src.kotlin/alphaTab/src/androidMain/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/Map.kt b/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/Map.kt deleted file mode 100644 index f0559a9a3..000000000 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/collections/Map.kt +++ /dev/null @@ -1,55 +0,0 @@ -package alphaTab.collections - -public data class MapEntry( - public val key:TKey, - public val value:TValue -) - -public class Map { - private val _data: LinkedHashMap - - public constructor() { - _data = LinkedHashMap() - } - - public constructor(entries: Iterable>) { - _data = LinkedHashMap() - _data.putAll(entries.map { Pair(it.key, it.value) }) - } - - public val size: Double - get() = _data.size.toDouble() - - public fun has(key: TKey): Boolean { - return _data.containsKey(key) - } - - public fun get(key: TKey): TValue { - @Suppress("UNCHECKED_CAST") - return _data[key] as TValue - } - - public fun set(key: TKey, value: TValue) { - _data[key] = value - } - - public fun delete(key: TKey) { - _data.remove(key) - } - - public fun values(): Iterable { - return _data.values - } - - public fun keys(): Iterable { - return _data.keys - } - - public fun clear() { - _data.clear() - } - - public operator fun iterator(): Iterator> { - return _data.map { MapEntry(it.key, it.value) }.iterator() - } -} diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/BitConverter.kt b/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/BitConverter.kt deleted file mode 100644 index a4458e226..000000000 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/BitConverter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package alphaTab.core - -@kotlin.ExperimentalUnsignedTypes -expect class BitConverter { - companion object { - @kotlin.jvm.JvmStatic - fun put(dest: ByteArray, pos: Int, v: UShort, littleEndian: Boolean) - - @kotlin.jvm.JvmStatic - fun put(dest: ByteArray, pos: Int, v: Short, littleEndian: Boolean) - - @kotlin.jvm.JvmStatic - fun put(dest: ByteArray, pos: Int, v: Int, littleEndian: Boolean) - - @kotlin.jvm.JvmStatic - fun getInt16(src: ByteArray, pos: Int, littleEndian: Boolean): Short - - @kotlin.jvm.JvmStatic - fun getUint16(src: ByteArray, pos: Int, littleEndian: Boolean): UShort - - @kotlin.jvm.JvmStatic - fun getUint32(src: ByteArray, pos: Int, littleEndian: Boolean): UInt - - @kotlin.jvm.JvmStatic - fun getInt32(src: ByteArray, pos: Int, littleEndian: Boolean): Int - } -} diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Array.kt b/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Array.kt deleted file mode 100644 index 11a9d1646..000000000 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Array.kt +++ /dev/null @@ -1,13 +0,0 @@ -package alphaTab.core.ecmaScript - -class Array { - companion object { - public fun from(x: Iterable): alphaTab.collections.List { - return alphaTab.collections.List(x) - } - public fun isArray(x:Any?):Boolean { - return x is alphaTab.collections.List<*> - } - - } -} diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/ArrayBuffer.kt b/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/ArrayBuffer.kt deleted file mode 100644 index fe1da381b..000000000 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/ArrayBuffer.kt +++ /dev/null @@ -1,14 +0,0 @@ -package alphaTab.core.ecmaScript - -@ExperimentalUnsignedTypes -class ArrayBuffer { - public val raw: UByteArray - - public constructor(size: Double) { - raw = UByteArray(size.toInt()) - } - - public constructor(raw: UByteArray) { - this.raw = raw - } -} diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Iterable.kt b/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Iterable.kt deleted file mode 100644 index 32e1575c9..000000000 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Iterable.kt +++ /dev/null @@ -1,3 +0,0 @@ -package alphaTab.core.ecmaScript - -typealias Iterable = kotlin.collections.Iterable diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Uint8Array.kt b/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Uint8Array.kt deleted file mode 100644 index b04e582da..000000000 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Uint8Array.kt +++ /dev/null @@ -1,56 +0,0 @@ -package alphaTab.core.ecmaScript - -@ExperimentalUnsignedTypes -class Uint8Array : Iterable { - private val _data: ArrayBuffer - - public val buffer: ArrayBuffer - get() { - return _data - } - - public constructor(x: Iterable) { - this._data = ArrayBuffer(x.map { d -> d.toInt().toUByte() }.toUByteArray()) - } - - public constructor(size: Double) { - this._data = ArrayBuffer(UByteArray(size.toInt())) - } - - public constructor(data: UByteArray) { - this._data = ArrayBuffer(data) - } - - public constructor(data:ArrayBuffer) { - this._data = data - } - - public val length: Double - get() { - return _data.raw.size.toDouble() - } - - public operator fun get(idx: Int): Double { - return _data.raw[idx].toDouble() - } - - public operator fun get(idx: Double): Double { - return _data.raw[idx.toInt()].toDouble() - } - - public operator fun set(idx: Int, value: Double) { - _data.raw[idx] = value.toInt().toUByte() - } - - public fun set(subarray: Uint8Array, pos: Double) { - subarray._data.raw.copyInto(_data.raw, pos.toInt(), 0, subarray._data.raw.size) - } - - override fun iterator(): Iterator { - return _data.raw.iterator() - } - - public fun subarray(begin: Double, end: Double): Uint8Array { - return Uint8Array(_data.raw.copyOfRange(begin.toInt(), end.toInt())) - } -} diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/io/TypeConversions.kt b/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/io/TypeConversions.kt deleted file mode 100644 index dd19f1247..000000000 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/alphaTab/io/TypeConversions.kt +++ /dev/null @@ -1,16 +0,0 @@ -package alphaTab.io - -import alphaTab.core.ecmaScript.Uint8Array - -expect class TypeConversions { - companion object { - public fun float64ToBytes(v: Double): Uint8Array - public fun bytesToFloat64(bytes: Uint8Array): Double - public fun uint16ToInt16(v: Double): Double - public fun int16ToUint32(v: Double): Double - public fun int32ToUint16(v: Double): Double - public fun int32ToInt16(v: Double): Double - public fun int32ToUint32(v: Double): Double - public fun uint8ToInt8(v: Double): Double - } -} diff --git a/src.kotlin/alphaTab/src/commonMain/kotlin/system/Exception.kt b/src.kotlin/alphaTab/src/commonMain/kotlin/system/Exception.kt deleted file mode 100644 index 194d127fa..000000000 --- a/src.kotlin/alphaTab/src/commonMain/kotlin/system/Exception.kt +++ /dev/null @@ -1,3 +0,0 @@ -package system - -typealias Exception = kotlin.Throwable diff --git a/src.kotlin/alphaTab/src/jvmCommon/kotlin/alphaTab/core/BitConverterImpl.kt b/src.kotlin/alphaTab/src/jvmCommon/kotlin/alphaTab/core/BitConverterImpl.kt deleted file mode 100644 index 7faaa4f8b..000000000 --- a/src.kotlin/alphaTab/src/jvmCommon/kotlin/alphaTab/core/BitConverterImpl.kt +++ /dev/null @@ -1,67 +0,0 @@ -package alphaTab.core - -import java.nio.ByteBuffer -import java.nio.ByteOrder - -@kotlin.ExperimentalUnsignedTypes -actual class BitConverter { - actual companion object { - @kotlin.jvm.JvmStatic - public actual fun put(dest: ByteArray, pos: Int, v: UShort, littleEndian: Boolean) { - ByteBuffer - .wrap(dest) - .order(if (littleEndian) ByteOrder.LITTLE_ENDIAN else ByteOrder.BIG_ENDIAN) - .putShort(pos, v.toShort()) - } - - @kotlin.jvm.JvmStatic - public actual fun put(dest: ByteArray, pos: Int, v: Short, littleEndian: Boolean) { - ByteBuffer - .wrap(dest) - .order(if (littleEndian) ByteOrder.LITTLE_ENDIAN else ByteOrder.BIG_ENDIAN) - .putShort(pos, v) - } - - @kotlin.jvm.JvmStatic - public actual fun put(dest: ByteArray, pos: Int, v: Int, littleEndian: Boolean) { - ByteBuffer - .wrap(dest) - .order(if (littleEndian) ByteOrder.LITTLE_ENDIAN else ByteOrder.BIG_ENDIAN) - .putInt(pos, v) - } - - @kotlin.jvm.JvmStatic - public actual fun getInt16(src: ByteArray, pos: Int, littleEndian: Boolean): Short { - return ByteBuffer - .wrap(src) - .order(if (littleEndian) ByteOrder.LITTLE_ENDIAN else ByteOrder.BIG_ENDIAN) - .getShort(pos) - } - - @kotlin.jvm.JvmStatic - public actual fun getUint16(src: ByteArray, pos: Int, littleEndian: Boolean): UShort { - return ByteBuffer - .wrap(src) - .order(if (littleEndian) ByteOrder.LITTLE_ENDIAN else ByteOrder.BIG_ENDIAN) - .getShort(pos) - .toUShort() - } - - @kotlin.jvm.JvmStatic - public actual fun getUint32(src: ByteArray, pos: Int, littleEndian: Boolean): UInt { - return ByteBuffer - .wrap(src) - .order(if (littleEndian) ByteOrder.LITTLE_ENDIAN else ByteOrder.BIG_ENDIAN) - .getInt(pos) - .toUInt() - } - - @kotlin.jvm.JvmStatic - public actual fun getInt32(src: ByteArray, pos: Int, littleEndian: Boolean): Int { - return ByteBuffer - .wrap(src) - .order(if (littleEndian) ByteOrder.LITTLE_ENDIAN else ByteOrder.BIG_ENDIAN) - .getInt(pos) - } - } -} diff --git a/src.kotlin/alphaTab/src/jvmCommon/kotlin/alphaTab/io/TypeConversions.kt b/src.kotlin/alphaTab/src/jvmCommon/kotlin/alphaTab/io/TypeConversions.kt deleted file mode 100644 index 210d796ee..000000000 --- a/src.kotlin/alphaTab/src/jvmCommon/kotlin/alphaTab/io/TypeConversions.kt +++ /dev/null @@ -1,60 +0,0 @@ -package alphaTab.io - -import alphaTab.core.ecmaScript.Uint8Array - -@ExperimentalUnsignedTypes -actual class TypeConversions { - actual companion object { - public actual fun float64ToBytes(v: Double): Uint8Array { - val l = java.lang.Double.doubleToLongBits(v); - return Uint8Array( - ubyteArrayOf( - ((l shl 56) and 0xFF).toUByte(), - ((l shl 48) and 0xFF).toUByte(), - ((l shl 40) and 0xFF).toUByte(), - ((l shl 32) and 0xFF).toUByte(), - ((l shl 24) and 0xFF).toUByte(), - ((l shl 16) and 0xFF).toUByte(), - ((l shl 8) and 0xFF).toUByte(), - ((l shl 0) and 0xFF).toUByte() - ) - ) - } - - public actual fun bytesToFloat64(bytes: Uint8Array): Double { - val l = (bytes.buffer.raw[0].toLong() shl 56) or - (bytes.buffer.raw[1].toLong() shl 48) or - (bytes.buffer.raw[2].toLong() shl 40) or - (bytes.buffer.raw[3].toLong() shl 32) or - (bytes.buffer.raw[4].toLong() shl 24) or - (bytes.buffer.raw[5].toLong() shl 16) or - (bytes.buffer.raw[6].toLong() shl 8) or - (bytes.buffer.raw[7].toLong() shl 0) - return java.lang.Double.longBitsToDouble(l); - } - - public actual fun uint16ToInt16(v: Double): Double { - return v.toUInt().toUShort().toDouble() - } - - public actual fun int16ToUint32(v: Double): Double { - return v.toInt().toShort().toUInt().toDouble() - } - - public actual fun int32ToUint16(v: Double): Double { - return v.toInt().toUShort().toDouble() - } - - public actual fun int32ToInt16(v: Double): Double { - return v.toInt().toShort().toDouble() - } - - public actual fun int32ToUint32(v: Double): Double { - return v.toInt().toUInt().toDouble() - } - - public actual fun uint8ToInt8(v: Double): Double { - return v.toUInt().toUByte().toInt().toDouble() - } - } -} diff --git a/src.kotlin/alphaTab/src/jvmTest/kotlin/alphaTab/TestPlatformPartials.kt b/src.kotlin/alphaTab/src/jvmTest/kotlin/alphaTab/TestPlatformPartials.kt deleted file mode 100644 index 40446a012..000000000 --- a/src.kotlin/alphaTab/src/jvmTest/kotlin/alphaTab/TestPlatformPartials.kt +++ /dev/null @@ -1,53 +0,0 @@ -package alphaTab - -import alphaTab.core.ecmaScript.Uint8Array -import java.io.ByteArrayOutputStream -import java.io.FileInputStream -import java.nio.file.Path -import java.nio.file.Paths -import kotlin.contracts.ExperimentalContracts - -@ExperimentalUnsignedTypes -@ExperimentalContracts -class TestPlatformPartials { - companion object { - public val projectRoot: String = findProjectRoot() - - private fun findProjectRoot():String { - var path = Paths.get("").toAbsolutePath() - while(!Path.of(path.toString(), "package.json").toFile().exists()) { - path = path.parent - ?: throw AlphaTabError(AlphaTabErrorType.General, "Could not find project root") - } - println(path.toString()) - return path.toString() - } - - public fun loadFile(path: String): Uint8Array { - val filePath = Path.of(projectRoot, path) - val fs = FileInputStream(filePath.toString()) - val ms = ByteArrayOutputStream() - fs.use { - fs.copyTo(ms) - } - return Uint8Array(ms.toByteArray().asUByteArray()) - } - - public fun saveFile(name:String, data:Uint8Array) { - val path = Path.of(projectRoot, "test-results", name) - path.parent.toFile().mkdirs() - val fs = path.toFile().outputStream() - fs.use { - fs.write(data.buffer.raw.asByteArray()) - } - } - - public fun listDirectory(path:String): alphaTab.collections.List { - val dirPath = Path.of(projectRoot, path) - return alphaTab.collections.List(dirPath.toFile() - .listFiles() - ?.filter { it.isFile } - ?.map { it.name } ?: emptyList()) - } - } -} diff --git a/src/AlphaTabApiBase.ts b/src/AlphaTabApiBase.ts index f07718875..891e58dc3 100644 --- a/src/AlphaTabApiBase.ts +++ b/src/AlphaTabApiBase.ts @@ -967,6 +967,7 @@ export class AlphaTabApiBase { if (this.settings.player.enableElementHighlighting) { this.uiFacade.removeHighlights(); } + // actively playing? -> animate cursor and highlight items let shouldNotifyBeatChange = false; if (this._playerState === PlayerState.Playing && !stop) { diff --git a/src/rendering/layout/HorizontalScreenLayout.ts b/src/rendering/layout/HorizontalScreenLayout.ts index 51354d459..57311ed7c 100644 --- a/src/rendering/layout/HorizontalScreenLayout.ts +++ b/src/rendering/layout/HorizontalScreenLayout.ts @@ -165,8 +165,6 @@ export class HorizontalScreenLayout extends ScoreLayout { const partialIndex = i; this._group.buildBoundingsLookup(this._group!.x, this._group!.y); this.registerPartial(e, canvas => { - canvas.color = this.renderer.settings.display.resources.mainGlyphColor; - canvas.textAlign = TextAlign.Left; let renderX: number = this._group!.getBarX(partial.masterBars[0].index) + this._group!.accoladeSpacing; if (partialIndex === 0) { renderX -= this._group!.x + this._group!.accoladeSpacing; diff --git a/src/rendering/layout/ScoreLayout.ts b/src/rendering/layout/ScoreLayout.ts index 37aa68ff7..b1824ced8 100644 --- a/src/rendering/layout/ScoreLayout.ts +++ b/src/rendering/layout/ScoreLayout.ts @@ -295,6 +295,7 @@ export abstract class ScoreLayout { ? (this.width - e.width) / 2 : this.firstBarX; e.y = y; + e.totalWidth = this.width; e.totalHeight = y + height; e.firstMasterBarIndex = -1; diff --git a/src/synth/soundfont/Hydra.ts b/src/synth/soundfont/Hydra.ts index b89efdecc..96feaf9e9 100644 --- a/src/synth/soundfont/Hydra.ts +++ b/src/synth/soundfont/Hydra.ts @@ -28,11 +28,10 @@ export class Hydra { const chunkHead: RiffChunk = new RiffChunk(); const chunkFastList: RiffChunk = new RiffChunk(); if (!RiffChunk.load(null, chunkHead, readable) || chunkHead.id !== 'sfbk') { - throw new FormatError("Soundfont is not a valid Soundfont2 file") + throw new FormatError('Soundfont is not a valid Soundfont2 file'); } while (RiffChunk.load(chunkHead, chunkFastList, readable)) { - let chunk: RiffChunk = new RiffChunk(); if (chunkFastList.id === 'pdta') { while (RiffChunk.load(chunkFastList, chunk, readable)) { @@ -145,7 +144,7 @@ export class Hydra { const samples: Float32Array = new Float32Array(samplesLeft); let samplesPos: number = 0; - const sampleBuffer: Uint8Array = new Uint8Array(2048); + const sampleBuffer: Uint8Array = new Uint8Array(16 * 1024); while (samplesLeft > 0) { let samplesToRead: number = Math.min(samplesLeft, (sampleBuffer.length / 2) | 0); reader.read(sampleBuffer, 0, samplesToRead * 2);