Skip to content

Commit

Permalink
Import Ableton Live #7
Browse files Browse the repository at this point in the history
  • Loading branch information
Bludwarf committed Mar 17, 2024
1 parent 1449fb3 commit 7f143ac
Show file tree
Hide file tree
Showing 20 changed files with 67,508 additions and 33 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"rxjs": "7.8",
"tone": "14",
"tslib": "2.3",
"xml-js": "^1.6.11",
"zone.js": "0.14"
},
"devDependencies": {
Expand Down
38 changes: 38 additions & 0 deletions src/app/als/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# als

## Piste audio original

XPath relatif
à `/Ableton/LiveSet/Tracks/AudioTrack[1]/DeviceChain/MainSequencer/Sample/ArrangerAutomation/Events/AudioClip`.

| XPath | Description | Exemple |
|-----------------------------------------|--------------------------------------------------------|---------------------------------------------------------|
| `CurrentStart/@Value` | Début du clip dans l'arrangeur (en BeatTime) | 0 |
| `CurrentEnd/@Value` | Fin du clip dans l'arrangeur (en BeatTime) | 378.36283820346318 |
| `Loop/HiddenLoopStart/@Value` | Début du sample dans le clip (en BeatTime) | -1.1762159715284715 |
| `Loop/HiddenLoopEnd/@Value` | Fin du sample dans le clip (en BeatTime) | 378.36283820346318 |
| `Name/@Value` | Nom du clip (par défaut, nom du sample sans extension) | DIDAFTA PETIT PAPILLON Master Web 24bit 48Khz_02-01 |
| `ColorIndex/@Value` | Couleur du clip | 20 |
| `SampleRef/FileRef/RelativePath` | Sous dossiers du sample relatifs au projet | Samples + lalicornerouge[...] + DIDAFTA - ALBUM |
| `SampleRef/FileRef/SearchHint/PathHint` | Plus complet que `RelativePath` | Musique + Groupes + Didaf'ta + Samples + [...] |
| `SampleRef/FileRef/Name` | Nom du sample avec extension | DIDAFTA PETIT PAPILLON Master Web 24bit 48Khz_02-01.wav |
| `SampleRef/DefaultDuration/@Value` | Durée en échantillons du sample | 9984000 |
| `SampleRef/DefaultSampleRate/@Value` | Fréquence d'échantillonnage du sample | 48000 |

On peut déduire la durée du sample en secondes = DefaultDuration / DefaultSampleRate

## Piste structure

XPath relatif
à `/Ableton/LiveSet/Tracks/MidiTrack[1]/DeviceChain/MainSequencer/ClipTimeable/ArrangerAutomation/Events/MidiClip`.

| XPath | Description | Exemple |
|-----------------------|--------------------------------------------------------|-----------------|
| `CurrentStart/@Value` | Début du clip dans l'arrangeur (en BeatTime) | 0 |
| `CurrentEnd/@Value` | Fin du clip dans l'arrangeur (en BeatTime) | 48 |
| `Name/@Value` | Nom du clip (par défaut, nom du sample sans extension) | Partie bombarde |
| `ColorIndex/@Value` | Couleur du clip | 22 |

# Voir aussi

- https://github.com/Bludwarf/als-player
31 changes: 31 additions & 0 deletions src/app/als/als-extractor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {AlsImporter} from "./als-importer";
import {getKarmaFile} from "../test/test-utils";
import {StructureExtractorFromAls} from "./structure-extractor-from-als";
import {Time} from "../time";

describe('AlsExtractor', () => {

const createExtractorFor = async (filePath: string): Promise<StructureExtractorFromAls> => {
const alsImporter = new AlsImporter();
const blob = await getKarmaFile(filePath)
const alsProject = await alsImporter.loadUnzipped(blob)
return new StructureExtractorFromAls(alsProject)
}

it('should get sample duration from Petit papillon', async () => {
const alsImporter = new AlsImporter();
const filePath = 'src/assets/als/Petit papillon.als.xml';
const extractor = await createExtractorFor(filePath)
expect(extractor.sampleDuration.toSeconds()).toBe(Time.fromValue(208).toSeconds())
});

it('should get JSON structure from Petit papillon', async () => {
const alsImporter = new AlsImporter();
const filePath = 'src/assets/als/Petit papillon.als.xml';
const extractor = await createExtractorFor(filePath)
const jsonStructure = extractor.structureObject
console.log(JSON.stringify(jsonStructure))
expect(jsonStructure).toBeTruthy()
});

});
35 changes: 35 additions & 0 deletions src/app/als/als-importer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {AlsImporter} from "./als-importer";
import {getKarmaFile} from "../test/test-utils";

describe('AlsImporter', () => {

// it('should load Petit papillon.als', (done) => {
// const alsImporter = new AlsImporter();
//
//
// // Source : https://stackoverflow.com/a/57331494/1655155
// const filePath = 'src/assets/als/Petit papillon.als';
// const request: XMLHttpRequest = createRequest(filePath );
//
// request.onload = async r => {
// let blob = new Blob([request.response]);
// const url = URL.createObjectURL(blob);
// await alsImporter.load(blob)
// // expect(data).toBe('expected data');
// done();
// };
//
// // trigger
// request.send(null);
// });

it('should load Petit papillon.als.xml', async (done) => {
const alsImporter = new AlsImporter();
const filePath = 'src/assets/als/Petit papillon.als.xml';
const blob = await getKarmaFile(filePath)
const alsProject = await alsImporter.loadUnzipped(blob)
expect(alsProject).toBeTruthy();
done();
});

});
71 changes: 71 additions & 0 deletions src/app/als/als-importer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// import * as zip from "@zip.js/zip.js";
// import {ZipReaderConstructorOptions} from "@zip.js/zip.js";
// const abletonParser = require('ableton-parser');

// const unzip = require('unzip-js')

import {AlsProject} from "./v10/als-project";

declare function require(name:string): any; // source : https://stackoverflow.com/a/12742371/1655155

var convert = require('xml-js')

// TODO trouver un utilitaire pour dézipper côté client

export class AlsImporter {

// async load(file: Blob): Promise<void> {
// console.log(file.size)
// const options: ZipReaderConstructorOptions = {} as ZipReaderConstructorOptions
//
// // Source : https://github.com/gildas-lormeau/zip.js/blob/gh-pages/demos/demo-read-file.js
//
// const blobReader = new zip.BlobReader(file);
// console.log('blobReader', blobReader)
//
// const zipReader = new zip.ZipReader(blobReader);
// console.log('zipReader', zipReader)
//
// const entries = await zipReader.getEntries(options)
// console.log(entries)
// }

// load(file: Blob) {
// unzip(file, function (err: any, zipFile: any) {
// if (err) {
// return console.error(err)
// }
//
// zipFile.readEntries(function (err: any, entries: any) {
// if (err) {
// return console.error(err)
// }
//
// entries.forEach(function (entry: any) {
// zipFile.readEntryData(entry, false, function (err: any, readStream: any) {
// if (err) {
// return console.error(err)
// }
//
// readStream.on('data', function (chunk: any) {
// })
// readStream.on('error', function (err: any) {
// })
// readStream.on('end', function () {
// })
// })
// })
// })
// })
// }

async loadUnzipped(xmlFile: Blob) {
// TODO utiliser plutôt des stream
const xmlContent = await xmlFile.text()
const parsedXml = convert.xml2json(xmlContent, {
compact: true,
})
return new AlsProject(JSON.parse(parsedXml))
}

}
30 changes: 30 additions & 0 deletions src/app/als/structure-extractor-from-als.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {AlsProject} from "./v10/als-project";
import {AudioTrack} from "./v10/audio-track";
import {Time} from "../time";

export class StructureExtractorFromAls {
constructor(private readonly alsProject: AlsProject) {

}

get originalAudioTrack(): AudioTrack {
return this.alsProject.audioTracks[0]
}

get sampleDuration(): Time {
const duration = this.originalAudioTrack.audioClips[0].duration
const sampleRate = this.originalAudioTrack.audioClips[0].sampleRate
return Time.fromValue(duration / sampleRate)
}

get structureObject(): StructureObject {
return {
sampleDuration: this.sampleDuration.toSeconds(),
}
}

}

export interface StructureObject {
sampleDuration: number
}
11 changes: 11 additions & 0 deletions src/app/als/v10/als-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {AudioTrack} from "./audio-track";

export class AlsProject {
constructor(private readonly parsedXml: any) {

}
get audioTracks(): AudioTrack[] {
return this.parsedXml.Ableton.LiveSet.Tracks.AudioTrack.map((audioTrack: any) => new AudioTrack(audioTrack))
}

}
15 changes: 15 additions & 0 deletions src/app/als/v10/audio-clip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export class AudioClip {

constructor(private readonly xmlAudioClip: any) {

}

get duration(): number {
return +this.xmlAudioClip.SampleRef.DefaultDuration._attributes.Value
}

get sampleRate(): number {
return +this.xmlAudioClip.SampleRef.DefaultSampleRate._attributes.Value
}

}
12 changes: 12 additions & 0 deletions src/app/als/v10/audio-track.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {AudioClip} from "./audio-clip";

export class AudioTrack {
constructor(private readonly xmlObject: any) {
}

get audioClips(): AudioClip[] {
const xmlAudioClip = this.xmlObject.DeviceChain.MainSequencer.Sample.ArrangerAutomation.Events.AudioClip as any;
return [new AudioClip(xmlAudioClip)]
}

}
5 changes: 3 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { RouterModule } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { SandboxComponent } from './sandbox/sandbox.component';
import { CommonModule } from '@angular/common';
import { RythmSandboxComponent } from './rythm-sandbox/rythm-sandbox.component';
import {ConvertComponent} from "./convert/convert.component";

@NgModule({
imports: [
Expand All @@ -15,6 +15,7 @@ import { RythmSandboxComponent } from './rythm-sandbox/rythm-sandbox.component';
ReactiveFormsModule,
RouterModule.forRoot([
{ path: '', component: RythmSandboxComponent },
{ path: 'convert', component: ConvertComponent },
])
],
declarations: [
Expand All @@ -31,4 +32,4 @@ export class AppModule { }
Copyright Google LLC. All Rights Reserved.
Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at https://angular.io/license
*/
*/
2 changes: 2 additions & 0 deletions src/app/convert/convert.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<input type="file" accept=".xml" (change)="uploadFile($event)"/>
<textarea #textarea></textarea>
Empty file.
23 changes: 23 additions & 0 deletions src/app/convert/convert.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { ConvertComponent } from './convert.component';

describe('ConvertComponent', () => {
let component: ConvertComponent;
let fixture: ComponentFixture<ConvertComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ConvertComponent]
})
.compileComponents();

fixture = TestBed.createComponent(ConvertComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
36 changes: 36 additions & 0 deletions src/app/convert/convert.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {Component, ElementRef, ViewChild} from '@angular/core';
import convert from "xml-js";

@Component({
selector: 'app-convert',
standalone: true,
imports: [],
templateUrl: './convert.component.html',
styleUrl: './convert.component.scss'
})
export class ConvertComponent {

@ViewChild('textarea')
textArea?: ElementRef<HTMLTextAreaElement>

async uploadFile(event: Event): Promise<void> {

if (!this.textArea) {
throw new Error('No textArea')
}

const element = event.currentTarget as HTMLInputElement;
let fileList: FileList | null = element.files;
if (!fileList?.length) {
return;
}

const xmlFile = fileList[0]
const xmlContent = await xmlFile.text()
const xmlObject = convert.xml2json(xmlContent, {
compact: true,
})

this.textArea.nativeElement.value = xmlObject
}
}
Loading

0 comments on commit 7f143ac

Please sign in to comment.