diff --git a/apps/octra/src/app/app.info.ts b/apps/octra/src/app/app.info.ts
index 110a85d1e..232e70521 100644
--- a/apps/octra/src/app/app.info.ts
+++ b/apps/octra/src/app/app.info.ts
@@ -1,5 +1,5 @@
import { NavigationExtras } from '@angular/router';
-import { WavFormat } from '@octra/web-media';
+import { MusicMetadataFormat, WavFormat } from '@octra/web-media';
import {
AnnotJSONConverter,
BundleJSONConverter,
@@ -16,7 +16,10 @@ import {
} from '@octra/annotation';
export class AppInfo {
- public static readonly audioformats = [new WavFormat()];
+ public static readonly audioformats = [
+ new WavFormat(),
+ new MusicMetadataFormat(),
+ ];
public static readonly converters: Converter[] = [
new AnnotJSONConverter(),
diff --git a/apps/octra/src/app/core/modals/supportedfiles-modal/supportedfiles-modal.component.html b/apps/octra/src/app/core/modals/supportedfiles-modal/supportedfiles-modal.component.html
index 9723718df..d396179d9 100644
--- a/apps/octra/src/app/core/modals/supportedfiles-modal/supportedfiles-modal.component.html
+++ b/apps/octra/src/app/core/modals/supportedfiles-modal/supportedfiles-modal.component.html
@@ -11,24 +11,31 @@
>
-
{{ 'modal.supported.audio files' | transloco }} (max. 1,9 GB):
-
-
-
-
- @for (elem of AppInfo.audioformats; track elem; let i = $index) {
-
- *{{ elem.extension
- }}{{ i < AppInfo.audioformats.length - 1 ? ',' : '' }}
-
- }
- |
-
-
+ {{ 'modal.supported.audio files' | transloco }}:
+
+
+ @for (format of supportedFormats; track format) {
+ @for (extension of format.supportedFormats; track extension) {
+
+
+ *{{extension.extension}}
+ |
+
+ max. {{extension.maxFileSize | filesize}}
+ |
+
+ @if (extension.info) {
+
+ }
+ |
+
+ }
+ }
+
{{ 'modal.supported.file formats for import' | transloco }}:
-
+
@for (elem of AppInfo.converters; track elem) { @if
(elem.conversion.import) {
diff --git a/apps/octra/src/app/core/modals/supportedfiles-modal/supportedfiles-modal.component.ts b/apps/octra/src/app/core/modals/supportedfiles-modal/supportedfiles-modal.component.ts
index 9d4f34ecc..43bec3c1c 100644
--- a/apps/octra/src/app/core/modals/supportedfiles-modal/supportedfiles-modal.component.ts
+++ b/apps/octra/src/app/core/modals/supportedfiles-modal/supportedfiles-modal.component.ts
@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
import { AppInfo } from '../../../app.info';
import { OctraModal } from '../types';
import { NgbActiveModal, NgbModalOptions } from '@ng-bootstrap/ng-bootstrap';
+import { AudioFormat } from '@octra/web-media';
@Component({
selector: 'octra-supportedfiles-modal',
@@ -16,6 +17,8 @@ export class SupportedFilesModalComponent extends OctraModal {
AppInfo = AppInfo;
+ supportedFormats: AudioFormat[] = AppInfo.audioformats;
+
constructor(protected override activeModal: NgbActiveModal) {
super('supportedFilesModal', activeModal);
}
diff --git a/apps/octra/src/app/core/modals/tools-modal/tools-modal.component.html b/apps/octra/src/app/core/modals/tools-modal/tools-modal.component.html
index 34c9c4738..4ea738fd7 100644
--- a/apps/octra/src/app/core/modals/tools-modal/tools-modal.component.html
+++ b/apps/octra/src/app/core/modals/tools-modal/tools-modal.component.html
@@ -143,149 +143,152 @@
} }
- @if (isToolEnabled('cut-audio')) { @if (
- audio.audioManager.resource.info.extension === '.wav' ) {
-
-
-
+ |
+
- |
-
-
- {{ 'tools.cut audio.name' | transloco }}
-
- |
-
- {{ 'tools.cut audio.description' | transloco }}
-
-
-
- |
-
- } @if ( audio.audioManager.resource.info.extension === '.wav' ) { @if
- (tools.audioCutting.opened) {
-
- |
-
- |
+
+
+ {{ 'tools.cut audio.name' | transloco }}
+
+ |
+
+ {{ 'tools.cut audio.description' | transloco }}
+
+
+
+ |
+
+
+ @if (tools.audioCutting.opened) {
+
+ |
+
+
+ {{"tools.cut audio.warning non wav" | transloco}}
+
+
+
- @if (tools.audioCutting.showConfigurator) {
-
- {{ 'tools.cut audio.hide configurator' | transloco }}
- } @if (!tools.audioCutting.showConfigurator) {
-
- {{ 'tools.cut audio.show configurator' | transloco }}
- }
-
-
-
-
-
- @if (tools.audioCutting.status !== 'running') {
-
- } @if (tools.audioCutting.status === 'running') {
-
- } @if (tools.audioCutting.status !== 'idle') {
-
-
-
+ @if (tools.audioCutting.status !== 'running') {
+
+ } @if (tools.audioCutting.status === 'running') {
+
+ } @if (tools.audioCutting.status !== 'idle') {
+
+
+
-
- {{ tools.audioCutting.progress | number : '1.2-2' }}%
-
+ aria-valuenow="25"
+ aria-valuemin="0"
+ aria-valuemax="100"
+ >
+
+ {{ tools.audioCutting.progress | number : '1.2-2' }}%
+
+
+
+ @if (tools.audioCutting.status === 'failed') {
+
+ {{ tools.audioCutting.message }}
+
+ }
-
- @if (tools.audioCutting.status === 'failed') {
-
- {{ tools.audioCutting.message }}
-
+ } @if ( tools.audioCutting.status !== 'idle' &&
+ tools.audioCutting.status !== 'finished' &&
+ tools.audioCutting.timeLeft > 0 ) { Time left:
+ {{ tools.audioCutting.timeLeft | timespan }}
+ } @if (tools.audioCutting.result.url !== undefined) {
+
+
+ {{ 'g.download' | transloco }}
+
}
+
- } @if ( tools.audioCutting.status !== 'idle' &&
- tools.audioCutting.status !== 'finished' &&
- tools.audioCutting.timeLeft > 0 ) { Time left:
- {{ tools.audioCutting.timeLeft | timespan }}
- } @if (tools.audioCutting.result.url !== undefined) {
-
-
- {{ 'g.download' | transloco }}
-
- }
-
-
- |
-
- } } }
+
+
+ }
+ }
diff --git a/apps/octra/src/app/core/modals/tools-modal/tools-modal.component.ts b/apps/octra/src/app/core/modals/tools-modal/tools-modal.component.ts
index 7cf41f17d..78ca564e7 100644
--- a/apps/octra/src/app/core/modals/tools-modal/tools-modal.component.ts
+++ b/apps/octra/src/app/core/modals/tools-modal/tools-modal.component.ts
@@ -28,7 +28,7 @@ import {
UserInteractionsService,
} from '../../shared/service';
import { OctraAnnotationSegmentLevel } from '@octra/annotation';
-import { IntArray, WavFormat } from '@octra/web-media';
+import { AudioCutter, IntArray } from '@octra/web-media';
import { OctraModal } from '../types';
import { strToU8, zip, zipSync } from 'fflate';
import { OctraModalService } from '../octra-modal.service';
@@ -87,7 +87,7 @@ export class ToolsModalComponent extends OctraModal implements OnDestroy {
cuttingSpeed: number;
cuttingTimeLeft: number;
timeLeft: number;
- wavFormat?: any;
+ cutter?: AudioCutter;
};
combinePhrases: {
opened: boolean;
@@ -130,7 +130,7 @@ export class ToolsModalComponent extends OctraModal implements OnDestroy {
cuttingSpeed: -1,
cuttingTimeLeft: 0,
timeLeft: 0,
- wavFormat: undefined,
+ cutter: undefined,
},
combinePhrases: {
opened: false,
@@ -188,7 +188,7 @@ export class ToolsModalComponent extends OctraModal implements OnDestroy {
this.subscriptionManager.destroy();
if (this.tools.audioCutting.result.url !== undefined) {
- window.URL.revokeObjectURL(this.tools.audioCutting.result.url);
+ window.URL.revokeObjectURL(this.tools.audioCutting.result.url as string);
}
if (this.parentformat.uri !== undefined) {
@@ -258,17 +258,15 @@ export class ToolsModalComponent extends OctraModal implements OnDestroy {
}
// start cutting
- this.tools.audioCutting.wavFormat = new WavFormat();
- this.tools.audioCutting.wavFormat.init(
- this.audio.audioManager.resource.info.fullname,
- this.audio.audioManager.resource.arraybuffer!
+ this.tools.audioCutting.cutter = new AudioCutter(
+ this.audio.audioManager.resource.info
);
let totalSize = 0;
let cuttingStarted = 0;
this.tools.audioCutting.subscriptionIDs[1] = this.subscribe(
- this.tools.audioCutting.wavFormat.onaudiocut,
+ this.tools.audioCutting.cutter.onaudiocut,
{
next: (status: {
finishedSegments: number;
@@ -464,7 +462,6 @@ export class ToolsModalComponent extends OctraModal implements OnDestroy {
);
this.tools.audioCutting.status = 'running';
- this.tools.audioCutting.wavFormat.status = 'running';
this.tools.audioCutting.progressbarType = 'info';
this.getDurationFactorForZipping()
@@ -472,9 +469,9 @@ export class ToolsModalComponent extends OctraModal implements OnDestroy {
this.tools.audioCutting.zippingSpeed = zipFactor;
cuttingStarted = Date.now();
- this.tools.audioCutting.wavFormat.cutAudioFileSequentially(
+ this.tools.audioCutting.cutter.cutChannelDataSequentially(
this.namingConvention.namingConvention,
- this.audio.audioManager.resource.arraybuffer,
+ this.audio.audioManager.channel,
cutList
);
})
@@ -504,8 +501,8 @@ export class ToolsModalComponent extends OctraModal implements OnDestroy {
this.tools.audioCutting.cuttingSpeed = -1;
this.tools.audioCutting.zippingSpeed = -1;
- if (this.tools.audioCutting.wavFormat !== undefined) {
- (this.tools.audioCutting.wavFormat as WavFormat).stopAudioSplitting();
+ if (this.tools.audioCutting.cutter !== undefined) {
+ this.tools.audioCutting.cutter.stopAudioSplitting();
}
}
diff --git a/apps/octra/src/app/core/store/asr/asr.effects.service.ts b/apps/octra/src/app/core/store/asr/asr.effects.service.ts
index ecb01981a..649df5921 100644
--- a/apps/octra/src/app/core/store/asr/asr.effects.service.ts
+++ b/apps/octra/src/app/core/store/asr/asr.effects.service.ts
@@ -21,7 +21,7 @@ import {
ASRStateQueue,
ASRStateQueueItem,
} from './index';
-import { FileInfo, readFileContents, WavFormat } from '@octra/web-media';
+import { AudioCutter, FileInfo, readFileContents } from '@octra/web-media';
import {
AlertService,
AudioService,
@@ -244,16 +244,12 @@ export class AsrEffects {
}
// 1) cut signal
- const format = new WavFormat();
- format.init(
- audioManager.resource.info.fullname,
- audioManager.resource.arraybuffer
- );
+ const cutter = new AudioCutter(audioManager.resource.info);
return from(
- format.cutAudioFile(
+ cutter.cutAudioFileFromChannelData(
`OCTRA_ASRqueueItem_${action.item.id}.wav`,
- audioManager.resource.arraybuffer,
+ audioManager.channel,
{
number: 1,
sampleStart: action.item.time.sampleStart,
diff --git a/apps/octra/src/app/core/store/authentication/authentication.effects.ts b/apps/octra/src/app/core/store/authentication/authentication.effects.ts
index 71e8f1783..968535082 100644
--- a/apps/octra/src/app/core/store/authentication/authentication.effects.ts
+++ b/apps/octra/src/app/core/store/authentication/authentication.effects.ts
@@ -31,7 +31,12 @@ import { SessionFile } from '../../obj/SessionFile';
import { joinURL } from '@octra/utilities';
import { checkAndThrowError } from '../error.handlers';
import { AlertService } from '../../shared/service';
-import { AudioManager, getBaseHrefURL, popupCenter } from '@octra/web-media';
+import {
+ AudioManager,
+ getBaseHrefURL,
+ normalizeMimeType,
+ popupCenter,
+} from '@octra/web-media';
import { ApplicationActions } from '../application/application.actions';
import { IDBActions } from '../idb/idb.actions';
@@ -591,7 +596,7 @@ export class AuthenticationEffects {
file.name,
file.size,
new Date(file.lastModified),
- file.type
+ normalizeMimeType(file.type)
);
};
diff --git a/apps/octra/src/app/core/store/idb/idb-effects.service.ts b/apps/octra/src/app/core/store/idb/idb-effects.service.ts
index 99716bff5..9fea3b465 100644
--- a/apps/octra/src/app/core/store/idb/idb-effects.service.ts
+++ b/apps/octra/src/app/core/store/idb/idb-effects.service.ts
@@ -507,7 +507,7 @@ export class IDBEffects {
catchError((error: Error) =>
of(
ApplicationActions.changeApplicationOption.fail({
- error: error?.message ?? error,
+ error: error?.message ?? error.toString(),
})
)
)
diff --git a/apps/octra/src/app/core/store/login-mode/annotation/annotation.effects.ts b/apps/octra/src/app/core/store/login-mode/annotation/annotation.effects.ts
index 0ee85a5cf..34e970e74 100644
--- a/apps/octra/src/app/core/store/login-mode/annotation/annotation.effects.ts
+++ b/apps/octra/src/app/core/store/login-mode/annotation/annotation.effects.ts
@@ -379,7 +379,7 @@ export class AnnotationEffects {
error: (err) => {
this.store.dispatch(
AnnotationActions.loadAudio.fail({
- error: 'Loading audio file failed
',
+ error: 'Loading audio file failed',
})
);
console.error(err);
diff --git a/apps/octra/src/assets/i18n/de.json b/apps/octra/src/assets/i18n/de.json
index 0f59001f8..d9a8aaaf1 100644
--- a/apps/octra/src/assets/i18n/de.json
+++ b/apps/octra/src/assets/i18n/de.json
@@ -80,7 +80,7 @@
}
},
"dropzone": {
- "drag&drop here": "Audiodatei (*.wav) (+ optional Datei zum Importieren) hier hineinziehen oder hier klicken",
+ "drag&drop here": "Eine Audiodatei + eine Transkript Datei (optional) hier hineinziehen oder hier klicken.",
"file selected": "{{file_label}} ausgewählt"
},
"export": {
@@ -481,6 +481,7 @@
"warning": "Die Begrenzung der Wortanzahl ist nur dann möglich, wenn es sich bei der angewendeten Annotation um eine Wort-Segmentierung handelt. Nach der Anwendung dieses Werkzeugs kann das Resultat mit \"CMD + Z (MacOS) or CTRL + Z (PC)\" rückgängig gemacht werden."
},
"cut audio": {
+ "warning non wav": "Da die Audiodatei nicht im WAVE Format vorliegt, wird sie beim Schneiden in WAVE PCM 16Bit konvertiert.",
"add placeholder": "Platzhalter hinzufügen",
"append meta files": "Meta Dateien hinzufügen",
"description": "Hiermit kann die Audiodatei ahängig von den gesetzten Grenzen geschnitten werden.",
diff --git a/apps/octra/src/assets/i18n/en.json b/apps/octra/src/assets/i18n/en.json
index c13e612f7..f1c0e08b0 100644
--- a/apps/octra/src/assets/i18n/en.json
+++ b/apps/octra/src/assets/i18n/en.json
@@ -80,7 +80,7 @@
}
},
"dropzone": {
- "drag&drop here": "Drag & Drop audio file (*.wav) (+ optional file for import) here or click here.",
+ "drag&drop here": "Drag & Drop one audio file (+ one optional transcript file) here or click here.",
"file selected": "{{file_label}} selected"
},
"export": {
@@ -481,6 +481,7 @@
"warning": "The word limitation is only possible if this tool is applied to an annotation with word segmentation. After running this tool you can revert the changes with hotkeys \"CMD + Z (MacOS) or CTRL + Z (PC)\"."
},
"cut audio": {
+ "warning non wav": "As the audio file is not in WAVE format, it is going to be converted to WAVE PCM 16Bit during cutting.",
"add placeholder": "Add placeholder",
"append meta files": "Append meta files",
"description": "This tool offers to cut the audio file to sequences from the transcription units.",
diff --git a/apps/octra/src/assets/i18n/it.json b/apps/octra/src/assets/i18n/it.json
index 88c34c809..068f334b5 100644
--- a/apps/octra/src/assets/i18n/it.json
+++ b/apps/octra/src/assets/i18n/it.json
@@ -53,7 +53,6 @@
}
},
"dropzone": {
- "drag&drop here": "Trascine & Rilascia il file audio (*.wav) (+ il file opzionale per l'importazione) qui o clicca qui.",
"file selected": "{{file_label}} selezionato"
},
"export": {
diff --git a/apps/octra/src/assets/i18n/ko.json b/apps/octra/src/assets/i18n/ko.json
index c68a65956..fad0c27cc 100644
--- a/apps/octra/src/assets/i18n/ko.json
+++ b/apps/octra/src/assets/i18n/ko.json
@@ -53,7 +53,6 @@
}
},
"dropzone": {
- "drag&drop here": "오디오 파일(*.wav) (+ 가져오기 옵션 파일)을 여기로 끌어서 놓거나 여기를 클릭합니다.",
"file selected": "{{file_label}} 선택됨"
},
"export": {
diff --git a/apps/octra/src/assets/i18n/nl.json b/apps/octra/src/assets/i18n/nl.json
index 088a4b658..0eb51da2e 100644
--- a/apps/octra/src/assets/i18n/nl.json
+++ b/apps/octra/src/assets/i18n/nl.json
@@ -53,7 +53,6 @@
}
},
"dropzone": {
- "drag&drop here": "Drag & Drop audiobestand (*.wav) (+ optioneel bestand om te importeren) hier of klik hier.",
"file selected": "{{file_label}} geselecteerd"
},
"export": {
diff --git a/apps/octra/src/assets/i18n/zh.json b/apps/octra/src/assets/i18n/zh.json
index a615155f2..34180aad1 100644
--- a/apps/octra/src/assets/i18n/zh.json
+++ b/apps/octra/src/assets/i18n/zh.json
@@ -53,7 +53,6 @@
}
},
"dropzone": {
- "drag&drop here": "拖拽音频文件(*.wav)(+ 其他可选文件导入)到此处,或点击此处。",
"file selected": "{{file_label}} 已选中"
},
"export": {
diff --git a/apps/octra/src/index.html b/apps/octra/src/index.html
index 978c7866d..5d726b320 100644
--- a/apps/octra/src/index.html
+++ b/apps/octra/src/index.html
@@ -112,9 +112,9 @@
diff --git a/apps/octra/tsconfig.json b/apps/octra/tsconfig.json
index 174c17243..eabb2dfe3 100644
--- a/apps/octra/tsconfig.json
+++ b/apps/octra/tsconfig.json
@@ -2,12 +2,12 @@
"compilerOptions": {
"target": "es2022",
"useDefineForClassFields": false,
- "forceConsistentCasingInFileNames": true,
- "strict": true,
- "noImplicitOverride": true,
- "noPropertyAccessFromIndexSignature": true,
- "noImplicitReturns": true,
- "noFallthroughCasesInSwitch": true,
+ "forceConsistentCasingInFileNames": false,
+ "strict": false,
+ "noImplicitOverride": false,
+ "noPropertyAccessFromIndexSignature": false,
+ "noImplicitReturns": false,
+ "noFallthroughCasesInSwitch": false,
"allowSyntheticDefaultImports": true
},
"files": [],
@@ -28,6 +28,7 @@
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
- "strictTemplates": true
+ "strictTemplates": true,
+ "commonChunk": false
}
}
diff --git a/build.js b/build.js
index d049f596f..73b366b3a 100644
--- a/build.js
+++ b/build.js
@@ -42,7 +42,7 @@ if (process.argv[4].indexOf("url=") > -1) {
console.log(`Building OCTRA with ${dev}, isUpdate=${isUpdate} for ${baseHref}`);
console.log(`Remove dist...`);
execSync(`rm -rf "./${buildDir}"`);
-let command = ["./node_modules/nx/bin/nx.js", "build", "octra", "--prod", dev, `--base-href=${baseHref}`];
+let command = ["./node_modules/nx/bin/nx.js", "build", "octra", "--prod", dev, `--base-href=${baseHref}`, `--deploy-url=assets/`, "--skip-nx-cache"];
if (dev !== "") {
command.splice(3, 1);
@@ -66,30 +66,6 @@ node.on("error", function(data) {
node.on("exit", function(code) {
console.log("child process exited with code " + code.toString());
- console.log(`Change index.html...`);
- let indexHTML = fs.readFileSync(`${buildDir}index.html`, {
- encoding: "utf8"
- });
-
- indexHTML = indexHTML.replace(/(scripts\.[0-9a-z]*\.js)/g, `${targetFolder}/$1`);
- indexHTML = indexHTML.replace(/(polyfills-es5\.[0-9a-z]*\.js)/g, `${targetFolder}/$1`);
- indexHTML = indexHTML.replace(/(polyfills-es2015\.[0-9a-z]*\.js)/g, `${targetFolder}/$1`);
- indexHTML = indexHTML.replace(/(polyfills\.[0-9a-z]*\.js)/g, `${targetFolder}/$1`);
- indexHTML = indexHTML.replace(/(src=")(-es2015\.[0-9a-z]*\.js)/g, `${targetFolder}/$2`);
- indexHTML = indexHTML.replace(/(src=")(-es5\.[0-9a-z]*\.js)/g, `${targetFolder}/$2`);
- indexHTML = indexHTML.replace(/(main-es2015\.[0-9a-z]*\.js)/g, `${targetFolder}/$1`);
- indexHTML = indexHTML.replace(/(main-es5\.[0-9a-z]*\.js)/g, `${targetFolder}/$1`);
- indexHTML = indexHTML.replace(/(main\.[0-9a-z]*\.js)/g, `${targetFolder}/$1`);
- indexHTML = indexHTML.replace(/(runtime-es2015\.[0-9a-z]*\.js)/g, `${targetFolder}/$1`);
- indexHTML = indexHTML.replace(/(runtime-es5\.[0-9a-z]*\.js)/g, `${targetFolder}/$1`);
- indexHTML = indexHTML.replace(/(runtime\.[0-9a-z]*\.js)/g, `${targetFolder}/$1`);
- indexHTML = indexHTML.replace(/(styles\.[0-9a-z]*\.css)/g, `${targetFolder}/$1`);
- indexHTML = indexHTML.replace(/(vendor\.[0-9a-z]*\.js)/g, `${targetFolder}/$1`);
-
- fs.writeFileSync(`${buildDir}index.html`, indexHTML, {
- encoding: "utf8"
- });
- console.log(`indexed html changed! ${buildDir}`);
if (isUpdate) {
execSync(`rm -rf "./${buildDir}config" "./${buildDir}media" "./${buildDir}.htaccess"`);
diff --git a/libs/assets/src/lib/schemata/inputs_outputs.set.json b/libs/assets/src/lib/schemata/inputs_outputs.set.json
index 5da1f69bf..39c9bf32c 100644
--- a/libs/assets/src/lib/schemata/inputs_outputs.set.json
+++ b/libs/assets/src/lib/schemata/inputs_outputs.set.json
@@ -1,7 +1,7 @@
{
"inputs": {
- "name": "one audio file and one text file",
- "description": "root description",
+ "name": "One audio file (*.wav / *.ogg / *.mp3 / *.flac / *.m4a) and one optional transcript file.",
+ "description": "Needs an audio file (.wav <= 1.9 GB) and an optional supported transcript file",
"combine": {
"type": "and",
"expressions": [
@@ -12,14 +12,12 @@
"with": [
{
"size": "<= 1.9GB",
- "mimeType": [
- "audio/wav",
- "audio/ogg"
- ],
- "extension": [
- ".wav",
- ".ogg"
- ]
+ "mimeType": ["audio/wav", "audio/wave", "audio/x-wav"],
+ "extension": [".wav"]
+ },
+ {
+ "size": "<= 300MB",
+ "extension": [".mp3", ".m4a", ".flac", ".ogg"]
}
]
},
@@ -29,90 +27,36 @@
"description": "Transcript file",
"with": [
{
- "mimeType": [
- "application/json"
- ],
- "extension": [
- "_annot.json"
- ],
- "content": [
- "AnnotJSON"
- ]
+ "mimeType": ["application/json"],
+ "extension": ["_annot.json"]
},
{
- "extension": [
- ".ctm"
- ],
- "content": [
- "CTM"
- ]
+ "extension": [".ctm"]
},
{
- "extension": [
- ".eaf"
- ],
- "content": [
- "ELAN"
- ]
+ "extension": [".eaf"]
},
{
- "extension": [
- ".par"
- ],
- "content": [
- "BASPartitur"
- ]
+ "extension": [".par"]
},
{
- "extension": [
- ".Table"
- ],
- "content": [
- "PraatTable"
- ]
+ "extension": [".Table"]
},
{
- "extension": [
- ".TextGrid"
- ],
- "content": [
- "TextGrid"
- ]
+ "extension": [".TextGrid"]
},
{
- "extension": [
- ".srt"
- ],
- "content": [
- "SRT"
- ]
+ "extension": [".srt"]
},
{
- "extension": [
- ".txt"
- ],
- "mimeType": [
- "text/plain"
- ],
- "content": [
- "Text"
- ]
+ "extension": [".txt"],
+ "mimeType": ["text/plain"]
},
{
- "extension": [
- ".vtt"
- ],
- "content": [
- "WebVTT"
- ]
+ "extension": [".vtt"]
},
{
- "extension": [
- ".json"
- ],
- "content": [
- "WhisperJSON"
- ]
+ "extension": [".json"]
}
]
}
@@ -120,8 +64,8 @@
}
},
"outputs": {
- "name": "one audio file and one text file",
- "description": "root description",
+ "name": "One transcript file",
+ "description": "One transcript file",
"combine": {
"type": "and",
"expressions": [
@@ -130,12 +74,8 @@
"name": "transcript",
"description": "The transcribed content of an audio file",
"with": {
- "mimeType": [
- "application/json"
- ],
- "extension": [
- "_annot.json"
- ],
+ "mimeType": ["application/json"],
+ "extension": ["_annot.json"],
"content": "AnnotJSON"
}
}
diff --git a/libs/media/src/lib/types.ts b/libs/media/src/lib/types.ts
index 296539a3e..5dcc2cafb 100644
--- a/libs/media/src/lib/types.ts
+++ b/libs/media/src/lib/types.ts
@@ -1,5 +1,5 @@
export interface NumeratedSegment {
number: number;
sampleStart: number;
- sampleDur: number;
+ sampleDur?: number;
}
diff --git a/libs/ngx-components/src/lib/components/audio/audio-viewer/audio-viewer.service.ts b/libs/ngx-components/src/lib/components/audio/audio-viewer/audio-viewer.service.ts
index 5112811e3..e1c37f20f 100644
--- a/libs/ngx-components/src/lib/components/audio/audio-viewer/audio-viewer.service.ts
+++ b/libs/ngx-components/src/lib/components/audio/audio-viewer/audio-viewer.service.ts
@@ -2228,14 +2228,19 @@ export class AudioViewerService {
this.computeWholeDisplayData(
this.AudioPxWidth / 2,
this._settings.lineheight,
- this.audioManager.channel as any,
+ this.audioManager.channel!,
{
- start:
+ start: Math.ceil(
this.audioChunk.time.start.samples /
- this.audioManager.channelDataFactor,
- end:
- this.audioChunk.time.end.samples /
- this.audioManager.channelDataFactor,
+ this.audioManager.channelDataFactor
+ ),
+ end: Math.min(
+ this.audioManager.channel!.length,
+ Math.ceil(
+ this.audioChunk.time.end.samples /
+ this.audioManager.channelDataFactor
+ )
+ ),
}
)
.then((result) => {
diff --git a/libs/ngx-utilities/src/lib/octra-utilities.module.ts b/libs/ngx-utilities/src/lib/octra-utilities.module.ts
index d820274a1..2e9b2fb5c 100644
--- a/libs/ngx-utilities/src/lib/octra-utilities.module.ts
+++ b/libs/ngx-utilities/src/lib/octra-utilities.module.ts
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
CapitalLetterPipe,
+ FileSizePipe,
JoinPipe,
LeadingNullPipe,
MapPipe,
@@ -21,6 +22,7 @@ import { SubscriberComponent } from './components';
MapPipe,
UnixDurationPipe,
SubscriberComponent,
+ FileSizePipe,
],
imports: [CommonModule],
exports: [
@@ -32,6 +34,7 @@ import { SubscriberComponent } from './components';
MapPipe,
UnixDurationPipe,
SubscriberComponent,
+ FileSizePipe,
],
})
export class OctraUtilitiesModule {}
diff --git a/libs/ngx-utilities/src/lib/pipes/filesize.pipe.ts b/libs/ngx-utilities/src/lib/pipes/filesize.pipe.ts
new file mode 100644
index 000000000..b568ea5c3
--- /dev/null
+++ b/libs/ngx-utilities/src/lib/pipes/filesize.pipe.ts
@@ -0,0 +1,13 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import { getFileSize } from '@octra/utilities';
+
+@Pipe({
+ name: 'filesize',
+})
+export class FileSizePipe implements PipeTransform {
+ transform(value: number): string {
+ const filesize = getFileSize(value);
+
+ return `${filesize.size} ${filesize.label}`;
+ }
+}
diff --git a/libs/ngx-utilities/src/lib/pipes/index.ts b/libs/ngx-utilities/src/lib/pipes/index.ts
index dc63036ab..265254b2f 100644
--- a/libs/ngx-utilities/src/lib/pipes/index.ts
+++ b/libs/ngx-utilities/src/lib/pipes/index.ts
@@ -5,3 +5,4 @@ export * from './map.pipe';
export * from './timespan.pipe';
export * from './unix-duration.pipe';
export * from './procent.pipe';
+export * from './filesize.pipe';
diff --git a/libs/web-media/src/lib/audio/AudioFormats/audio-format.ts b/libs/web-media/src/lib/audio/AudioFormats/audio-format.ts
index 114ed4885..2263d7d60 100644
--- a/libs/web-media/src/lib/audio/AudioFormats/audio-format.ts
+++ b/libs/web-media/src/lib/audio/AudioFormats/audio-format.ts
@@ -1,14 +1,27 @@
export type IntArray = Uint8Array | Int16Array | Int32Array;
+export interface SupportedAudioFormat {
+ extension: string;
+ maxFileSize: number;
+ variableNumberOfSamples?: boolean;
+ warning?: string;
+ info?: string;
+}
+
export abstract class AudioFormat {
- protected _extension!: string;
+ get mimeType(): string {
+ return this._mimeType;
+ }
public formatConstructor!:
| Uint8ArrayConstructor
| Int16ArrayConstructor
| Int32ArrayConstructor;
- get extension(): string {
- return this._extension;
+ get supportedFormats(): SupportedAudioFormat[] {
+ return this._supportedFormats;
+ }
+ get decoder(): "web-audio" | "octra" {
+ return this._decoder;
}
protected _filename!: string;
@@ -41,38 +54,28 @@ export abstract class AudioFormat {
return this._bitsPerSample;
}
- protected _duration!: number;
+ protected _duration!: {
+ samples: number;
+ seconds: number;
+ };
- get duration(): number {
+ get duration(): {
+ samples: number;
+ seconds: number;
+ } {
return this._duration;
}
- public init(filename: string, buffer: ArrayBuffer) {
+ protected _supportedFormats!: SupportedAudioFormat[];
+ protected _mimeType!: string;
+ protected _decoder: "web-audio" | "octra" = "web-audio";
+
+ public async init(filename: string, mimeType: string, buffer: ArrayBuffer) {
this._filename = filename;
- this.setSampleRate(buffer);
- this.setChannels(buffer);
- this.setBitsPerSample(buffer);
- this.setByteRate(buffer);
- this.setDuration(buffer);
-
- if (this.bitsPerSample === 32) {
- this.formatConstructor = Int32Array;
- } else if (this.bitsPerSample === 16) {
- this.formatConstructor = Int16Array;
- } else if (this.bitsPerSample === 8) {
- this.formatConstructor = Uint8Array;
- }
+ this._mimeType = mimeType;
+ await this.readAudioInformation(buffer);
}
public abstract isValid(buffer: ArrayBuffer): boolean;
-
- protected abstract setSampleRate(buffer: ArrayBuffer): void;
-
- protected abstract setChannels(buffer: ArrayBuffer): void;
-
- protected abstract setBitsPerSample(buffer: ArrayBuffer): void;
-
- protected abstract setByteRate(buffer: ArrayBuffer): void;
-
- protected abstract setDuration(buffer: ArrayBuffer): void;
+ protected abstract readAudioInformation(buffer: ArrayBuffer): Promise;
}
diff --git a/libs/web-media/src/lib/audio/AudioFormats/index.ts b/libs/web-media/src/lib/audio/AudioFormats/index.ts
index 64849fe4e..ff07aea8e 100644
--- a/libs/web-media/src/lib/audio/AudioFormats/index.ts
+++ b/libs/web-media/src/lib/audio/AudioFormats/index.ts
@@ -1,3 +1,3 @@
export * from './audio-format';
-export * from './ogg-format';
export * from './wav-format';
+export * from './music-metadata-format';
diff --git a/libs/web-media/src/lib/audio/AudioFormats/music-metadata-format.ts b/libs/web-media/src/lib/audio/AudioFormats/music-metadata-format.ts
new file mode 100644
index 000000000..3cc7f3255
--- /dev/null
+++ b/libs/web-media/src/lib/audio/AudioFormats/music-metadata-format.ts
@@ -0,0 +1,59 @@
+import { AudioFormat } from './audio-format';
+import { parseBlob } from 'music-metadata';
+
+export class MusicMetadataFormat extends AudioFormat {
+ protected override _decoder: 'web-audio' | 'octra' = 'web-audio';
+
+ constructor() {
+ super();
+ this._supportedFormats = [
+ {
+ extension: '.flac',
+ maxFileSize: 300000000, // 300 MB,
+ info: 'The duration in samples is going to be estimated and may differ with the used application.',
+ },
+ {
+ extension: '.ogg',
+ maxFileSize: 300000000, // 300 MB
+ },
+ {
+ extension: '.mp3',
+ maxFileSize: 300000000, // 300 MB,
+ info: 'The duration in samples is going to be estimated and may differ with the used application.',
+ },
+ {
+ extension: '.m4a',
+ maxFileSize: 300000000, // 300 MB,
+ info: 'The duration in samples is going to be estimated and may differ with the used application.',
+ },
+ ];
+ }
+
+ public isValid(buffer: ArrayBuffer): boolean {
+ return true;
+ }
+
+ override async readAudioInformation(buffer: ArrayBuffer) {
+ const parsed = await parseBlob(
+ new File([buffer], this._filename, { type: this._mimeType })
+ );
+ const format = parsed.format;
+
+ if (
+ !format.sampleRate ||
+ !format.numberOfSamples ||
+ !format.numberOfChannels
+ ) {
+ throw new Error(
+ "Can't read one of the following audio information: sampleRate, numberOfSamples, numberOfChannels."
+ );
+ } else {
+ this._sampleRate = format.sampleRate;
+ this._duration = {
+ samples: format.numberOfSamples,
+ seconds: format.duration!,
+ };
+ this._channels = format.numberOfChannels;
+ }
+ }
+}
diff --git a/libs/web-media/src/lib/audio/AudioFormats/ogg-format.ts b/libs/web-media/src/lib/audio/AudioFormats/ogg-format.ts
deleted file mode 100644
index d7ea82104..000000000
--- a/libs/web-media/src/lib/audio/AudioFormats/ogg-format.ts
+++ /dev/null
@@ -1,348 +0,0 @@
-import {AudioFormat} from './audio-format';
-
-// specification found on https://wiki.xiph.org/OggVorbis
-// https://www.ietf.org/rfc/rfc3533.txt
-// https://web.mit.edu/cfox/share/doc/libvorbis-1.0/vorbis-spec-ref.html
-export class OggFormat extends AudioFormat {
- version!: number;
- headerType!: number;
- /**
- * A granule position is the time marker in Ogg files. It is an abstract value, whose meaning is determined by the codec.
- * It may, for example, be a count of the number of samples, the number of frames or a more complex scheme.
- */
- granulePosition!: number;
- bitStreamLength!: number;
-
- constructor() {
- super();
- this._extension = '.ogg';
- }
-
- public isValid(buffer: ArrayBuffer): boolean {
- const bufferPart = buffer.slice(0, 4);
- let test = String.fromCharCode.apply(
- undefined,
- new Uint8Array(bufferPart) as any
- );
- test = test.slice(0, 6);
- return test === 'OggS';
- }
-
- protected setSampleRate(buffer: ArrayBuffer) {
- const bufferPart = buffer.slice(40, 42);
- const bufferView = new Uint16Array(bufferPart);
- this._sampleRate = bufferView[0];
- }
-
- protected setChannels(buffer: ArrayBuffer) {
- const bufferPart = buffer.slice(39, 40);
- const bufferView = new Uint8Array(bufferPart);
- this._channels = bufferView[1];
- }
-
- protected setBitsPerSample(buffer: ArrayBuffer) {
- const bufferPart = buffer.slice(48, 52);
- const bufferView = new Uint32Array(bufferPart);
-
- this._bitsPerSample = bufferView[0];
- }
-
- protected setByteRate() {
- this._byteRate = 0;
- }
-
- protected setDuration() {
- // TODO implement
- }
-
- override init(filename: string, buffer: ArrayBuffer) {
- super.init(filename, buffer);
- const test = OggVorbisPage.readFromBuffer(buffer);
- const t = '';
- }
-}
-
-enum OggVorbisHeaderType {
- 'IDENTIFICATION' = 1,
- 'COMMENT' = 3,
- 'SETUP' = 5,
-}
-
-class OggVorbisPage {
- capturePattern!: string;
- version!: number;
- headerType!: number;
- granulePosition!: number;
- bitstreamSerialNumber!: number;
- pageSequenceNumber!: number;
- checksum!: number;
- pageSegments!: number;
-
- segmentLengthTable: number[] = [];
- byteLength = 0;
- pointer = 0;
-
- identificationHeader?: {
- packetType: OggVorbisHeaderType;
- vorbis: string;
- vorbisVersion: number;
- audioChannels: number;
- audioSampleRate: number;
- bitrateMaximum: number;
- bitrateNominal: number;
- bitrateMinimum: number;
- blocksize0: number;
- blocksize1: number;
- framingFlag: number;
- };
-
- commentHeader?: {
- vorbis: string;
- framingBit: number;
- fields: {
- name: string;
- value: string;
- }[];
- };
-
- constructor(partial?: Partial) {
- if (partial) {
- Object.assign(this, partial);
- }
- }
-
- readOnePage(buffer: ArrayBuffer, start: number) {
- this.pointer = start;
- this.readCapturePattern(buffer);
- this.readVersion(buffer);
- this.readHeaderType(buffer);
- this.setGranulePosition(buffer);
- this.setBitstreamSerialNumber(buffer);
- this.readPageSequenceNumber(buffer);
- this.readChecksum(buffer);
- this.readPageSegments(buffer);
- this.readSegmentTable(buffer);
-
- this.readIdentificationHeader(buffer);
- this.readCommentHeader(buffer);
- this.readSetupHeader(buffer);
- }
-
- readIdentificationHeader(buffer: ArrayBuffer) {
- const packetType = this.sliceInt(buffer, 1, 'uint8', true, false);
-
- if (packetType === OggVorbisHeaderType.IDENTIFICATION) {
- this.byteLength += 1;
- this.pointer += 1;
- this.identificationHeader = {
- packetType,
- vorbis: this.sliceString(buffer, 6),
- vorbisVersion: this.sliceInt(buffer, 4, 'uint32')!,
- audioChannels: this.sliceInt(buffer, 1, 'uint8')!,
- audioSampleRate: this.sliceInt(buffer, 4, 'uint32')!,
- bitrateMaximum: this.sliceInt(buffer, 4, 'int32')! + 1,
- bitrateNominal: this.sliceInt(buffer, 4, 'int32')!,
- bitrateMinimum: this.sliceInt(buffer, 4, 'int32')! + 1,
- blocksize0: -1,
- blocksize1: -1,
- framingFlag: -1,
- };
-
- const blockSizes = this.sliceBits(buffer, 1, [4, 4], 'uint8');
- this.identificationHeader.blocksize0 = blockSizes[0];
- this.identificationHeader.blocksize1 = blockSizes[1];
- this.identificationHeader.framingFlag = this.sliceInt(
- buffer,
- 1,
- 'uint8'
- )!;
- }
- }
-
- readCommentHeader(buffer: ArrayBuffer) {
- const packetType = this.sliceInt(buffer, 1, 'uint8', true, false);
-
- if (packetType === OggVorbisHeaderType.COMMENT) {
- this.byteLength += 1;
- this.pointer += 1;
- this.commentHeader = {
- vorbis: this.sliceString(buffer, 6),
- framingBit: -1,
- fields: [],
- };
-
- const firstLength = this.sliceInt(buffer, 4, 'uint32')!;
- const vendor = this.sliceString(buffer, firstLength);
- const commentListLength = this.sliceInt(buffer, 4, 'uint32')!;
-
- for (let i = 0; i < commentListLength; i++) {
- const length = this.sliceInt(buffer, 4, 'uint32')!;
- const comment = this.sliceString(buffer, length);
-
- if (comment.indexOf('=') > -1) {
- const values = comment.split('=', 2);
- this.commentHeader.fields.push({
- name: values[0],
- value: values[1],
- });
- }
-
- this.commentHeader.framingBit = this.sliceInt(buffer, 1, 'uint8')!;
-
- if (this.commentHeader.framingBit !== 1) {
- throw new Error(
- 'Error reading comment header, framing bit is unset.'
- );
- }
- const t = '';
- }
-
- const t = '';
- }
- }
-
- readSetupHeader(buffer: ArrayBuffer) {
- const packetType = this.sliceInt(buffer, 1, 'uint8', true, false);
-
- if (packetType === OggVorbisHeaderType.SETUP) {
- this.byteLength += 1;
- this.pointer += 1;
- }
- }
-
- private sliceBytes(
- buffer: ArrayBuffer,
- length: number,
- incrementPointer = true
- ): DataView {
- const view = new DataView(buffer, this.pointer, length);
- if (incrementPointer) {
- this.byteLength += length;
- this.pointer += length;
- }
- return view;
- }
-
- private sliceInt(
- buffer: ArrayBuffer,
- length: number,
- type:
- | 'uint8'
- | 'uint16'
- | 'uint32'
- | 'uint64'
- | 'int8'
- | 'int16'
- | 'int32'
- | 'int64',
- littleEndian = true,
- incrementPointer = true
- ): number | undefined {
- const view = this.sliceBytes(buffer, length, incrementPointer);
-
- switch (type) {
- case 'int8':
- return view.getInt8(0);
- case 'uint8':
- return view.getUint8(0);
- case 'int16':
- return view.getInt16(0, littleEndian);
- case 'uint16':
- return view.getUint16(0, littleEndian);
- case 'int32':
- return view.getInt32(0, littleEndian);
- case 'uint32':
- return view.getUint32(0, littleEndian);
- }
-
- return undefined;
- }
-
- private sliceBits(
- buffer: ArrayBuffer,
- length: number,
- selectedBits: number[],
- type:
- | 'uint8'
- | 'uint16'
- | 'uint32'
- | 'uint64'
- | 'int8'
- | 'int16'
- | 'int32'
- | 'int64'
- ): number[] {
- const result: number[] = [];
- const number = this.sliceInt(buffer, length, type);
- let bitStr = Number(number).toString(2);
- const len = bitStr.length;
- for (let i = len; i < length * 8; i++) {
- bitStr = `0${bitStr}`;
- }
-
- for (const selectedBit of selectedBits) {
- const substr = bitStr.substring(0, selectedBit);
- bitStr = bitStr.substring(selectedBit);
- result.push(parseInt(substr, 2));
- }
-
- return result;
- }
-
- private sliceString(buffer: ArrayBuffer, length: number): string {
- const bufferPart = buffer.slice(this.pointer, this.pointer + length);
- const array = new Uint8Array(bufferPart);
- this.byteLength += length;
- this.pointer += length;
- return String.fromCharCode.apply(undefined, Array.from(array));
- }
-
- private readCapturePattern(buffer: ArrayBuffer) {
- this.capturePattern = this.sliceString(buffer, 4)?.slice(0, 6);
- }
-
- private readVersion(buffer: ArrayBuffer) {
- this.version = this.sliceInt(buffer, 1, 'uint8')!;
- }
-
- private readHeaderType(buffer: ArrayBuffer) {
- this.headerType = this.sliceInt(buffer, 1, 'uint8')!;
- }
-
- private setGranulePosition(buffer: ArrayBuffer) {
- this.granulePosition = this.sliceInt(buffer, 8, 'uint32')!;
- }
-
- private setBitstreamSerialNumber(buffer: ArrayBuffer) {
- this.bitstreamSerialNumber = this.sliceInt(buffer, 4, 'uint32')!;
- }
-
- private readPageSequenceNumber(buffer: ArrayBuffer) {
- this.pageSequenceNumber = this.sliceInt(buffer, 4, 'uint32')!;
- }
-
- private readChecksum(buffer: ArrayBuffer) {
- this.checksum = this.sliceInt(buffer, 4, 'uint32')!;
- }
-
- private readPageSegments(buffer: ArrayBuffer) {
- this.pageSegments = this.sliceInt(buffer, 1, 'uint8')!;
- }
-
- private readSegmentTable(buffer: ArrayBuffer) {
- //length is equal to pageSegments
- for (let i = 0; i < this.pageSegments; i++) {
- this.segmentLengthTable.push(this.sliceInt(buffer, 1, 'uint8')!);
- }
- }
-
- public static readFromBuffer(buffer: ArrayBuffer) {
- const result = new OggVorbisPage();
- result.readOnePage(buffer, 0);
-
- const result2 = new OggVorbisPage();
- result2.readOnePage(buffer, result.byteLength);
-
- return result;
- }
-}
diff --git a/libs/web-media/src/lib/audio/AudioFormats/wav-format.ts b/libs/web-media/src/lib/audio/AudioFormats/wav-format.ts
index 4bfc74485..3e6dbc8b6 100644
--- a/libs/web-media/src/lib/audio/AudioFormats/wav-format.ts
+++ b/libs/web-media/src/lib/audio/AudioFormats/wav-format.ts
@@ -1,22 +1,12 @@
-import {AudioFormat, IntArray} from './audio-format';
-import {Subject} from 'rxjs';
-import {NumeratedSegment} from '@octra/media';
+import { AudioFormat, IntArray } from './audio-format';
// http://soundfile.sapp.org/doc/WaveFormat/
export class WavFormat extends AudioFormat {
- public onaudiocut: Subject<{
- finishedSegments: number;
- fileName: string;
- intArray: IntArray;
- }> = new Subject<{
- finishedSegments: number;
- fileName: string;
- intArray: IntArray;
- }>();
protected dataStart = -1;
private status: 'running' | 'stopRequested' | 'stopped' = 'stopped';
protected _blockAlign!: number;
+ protected override _decoder: 'web-audio' | 'octra' = 'octra';
public get blockAlign() {
return this._blockAlign;
@@ -24,19 +14,37 @@ export class WavFormat extends AudioFormat {
constructor() {
super();
- this._extension = '.wav';
+ this._supportedFormats = [
+ {
+ extension: '.wav',
+ maxFileSize: 1900000000, // 1.9 GB
+ },
+ ];
}
- private static writeString(view: DataView, offset: number, str: string) {
- for (let i = 0; i < str.length; i++) {
- view.setUint8(offset + i, str.charCodeAt(i));
- }
- }
-
- public override init(filename: string, buffer: ArrayBuffer) {
+ public override async init(
+ filename: string,
+ mimeType: string,
+ buffer: ArrayBuffer
+ ) {
this.setDataStart(buffer);
- super.init(filename, buffer);
- this.setBlockAlign(buffer);
+ await super.init(filename, mimeType, buffer);
+ }
+
+ override async readAudioInformation(buffer: ArrayBuffer) {
+ this.setSampleRate(buffer);
+ this.setChannels(buffer);
+ this.setBitsPerSample(buffer);
+ this.setByteRate(buffer);
+ this.setDuration(buffer);
+
+ if (this.bitsPerSample === 32) {
+ this.formatConstructor = Int32Array;
+ } else if (this.bitsPerSample === 16) {
+ this.formatConstructor = Int16Array;
+ } else if (this.bitsPerSample === 8) {
+ this.formatConstructor = Uint8Array;
+ }
}
/***
@@ -63,201 +71,6 @@ export class WavFormat extends AudioFormat {
return test1 + '' === 'RIFF' && test2 === 'WAVE';
}
- /***
- * cut the audio file sequentially
- * @param namingConvention the naming convention for file renaming
- * @param buffer the array buffer of the audio file
- * @param segments the list of segments for cut
- * @param pointer the current segment to be cut
- */
- public cutAudioFileSequentially(
- namingConvention: string,
- buffer: ArrayBuffer,
- segments: NumeratedSegment[],
- pointer = 0
- ): void {
- if (pointer > -1 && pointer < segments.length) {
- const segment = segments[pointer];
-
- this.cutAudioFile(namingConvention, buffer, segment)
- .then(({ fileName, uint8Array }) => {
- this.onaudiocut.next({
- finishedSegments: pointer + 1,
- fileName,
- intArray: uint8Array,
- });
-
- if (pointer < segments.length - 1) {
- // continue
- // const freeSpace = window.performance.memory.totalJSHeapSize - window.performance.memory.usedJSHeapSize;
- // console.log(`${freeSpace / 1024 / 1024} MB left.`);
- if (this.status === 'running') {
- setTimeout(
- () =>
- this.cutAudioFileSequentially(
- namingConvention,
- buffer,
- segments,
- ++pointer
- ),
- 200
- );
- } else {
- this.status = 'stopped';
- }
- } else {
- // stop
- this.onaudiocut.complete();
- }
- })
- .catch((error) => {
- this.onaudiocut.error(error);
- });
- } else {
- this.onaudiocut.error(new Error('pointer is invalid!'));
- }
- }
-
- public cutAudioFile(
- namingConvention: string,
- buffer: ArrayBuffer,
- segment: NumeratedSegment
- ): Promise<{
- fileName: string;
- uint8Array: Uint8Array;
- }> {
- return new Promise<{
- fileName: string;
- uint8Array: Uint8Array;
- }>((resolve, reject) => {
- const fileName = this.getNewFileName(
- namingConvention,
- this._filename,
- segment
- );
-
- if (this.isValid(buffer)) {
- const u8array = new Uint8Array(buffer);
-
- this.extractDataFromArray(
- segment.sampleStart,
- segment.sampleDur,
- u8array
- )
- .then((data: IntArray) => {
- resolve({
- fileName,
- uint8Array: new Uint8Array(
- this.getFileFromBufferPartArrayBuffer(data, this._channels)
- ),
- });
- })
- .catch((error) => {
- reject(error);
- });
- } else {
- reject('no valid wav format!');
- }
- });
- }
-
- public getFileDataView(
- data:
- | Uint8Array
- | Uint16Array
- | Uint32Array
- | Int8Array
- | Int16Array
- | Int32Array,
- channels: number
- ): ArrayBuffer {
- // creates a mono data view
- const blockAlign = (channels * this._bitsPerSample) / 8;
- const subChunk2Size = data.length * blockAlign;
-
- const buffer = new ArrayBuffer(44 + data.byteLength);
- const dataView = new DataView(buffer);
-
- /* RIFF identifier */
- WavFormat.writeString(dataView, 0, 'RIFF');
- /* RIFF chunk length */
- dataView.setUint32(4, 36 + subChunk2Size, true);
- /* RIFF type */
- WavFormat.writeString(dataView, 8, 'WAVE');
- /* format chunk identifier */
- WavFormat.writeString(dataView, 12, 'fmt ');
- /* format chunk length */
- dataView.setUint32(16, 16, true);
- /* sample format (raw) */
- dataView.setUint16(20, 1, true);
- /* channel count */
- dataView.setUint16(22, channels, true);
- /* sample rate */
- dataView.setUint32(24, this._sampleRate, true);
- /* byte rate (sample rate * block align) */
- dataView.setUint32(28, this._sampleRate * blockAlign, true);
- /* block align (channel count * bytes per sample) */
- dataView.setUint16(32, blockAlign, true);
- /* bits per sample */
- dataView.setUint16(34, this._bitsPerSample, true);
- /* data chunk identifier */
- WavFormat.writeString(dataView, 36, 'data');
- /* data chunk length */
- dataView.setUint32(40, subChunk2Size, true);
-
- for (let i = 0; i < data.length; i++) {
- if (data instanceof Uint8Array) {
- dataView.setUint8(44 + i, data[i]);
- } else if (this._bitsPerSample === 16) {
- // little endian must be set!
- dataView.setUint16(44 + i * 2, data[i], true);
- } else {
- //TODO check this
- dataView.setUint32(44 + i * 4, data[i], true);
- }
- }
- return dataView.buffer;
- }
-
- getNewFileName(
- namingConvention: string,
- fileName: string,
- segment: NumeratedSegment
- ) {
- const name = fileName.substring(0, fileName.lastIndexOf('.'));
- const extension = fileName.substring(fileName.lastIndexOf('.'));
-
- let leadingNull = '';
- const maxDecimals = 4;
- const decimals = (segment.number + 1).toString().length;
-
- for (let i = 0; i < maxDecimals - decimals; i++) {
- leadingNull += '0';
- }
-
- return namingConvention.replace(/<([^<>]+)>/g, (g0, g1) => {
- switch (g1) {
- case 'name':
- return name;
- case 'sequNumber':
- return `${leadingNull}${segment.number + 1}`;
- case 'sampleStart':
- return segment.sampleStart;
- case 'sampleDur':
- return segment.sampleDur;
- case 'secondsStart':
- return (
- Math.round((segment.sampleStart / this.sampleRate) * 1000) / 1000
- );
- case 'secondsDur':
- return (
- Math.round((segment.sampleStart / this.sampleRate) * 1000) / 1000
- );
- }
- return g1;
- });
- }
-
/***
* cuts the data part of selected samples from an Uint8Array
* @param sampleStart the start of the extraction
@@ -282,7 +95,6 @@ export class WavFormat extends AudioFormat {
let start = sampleStart * blockAlign;
let dataChunkLength = sampleDur * blockAlign;
- const unsigned = this._bitsPerSample === 8;
let startPos: number;
const divider = this._bitsPerSample / 8;
@@ -342,11 +154,6 @@ export class WavFormat extends AudioFormat {
});
}
- /*
- public calculateFileSize(samples: number): number {
- return 44 + samples * this.blockAlign;
- }*/
-
public stopAudioSplitting() {
if (this.status === 'running') {
this.status = 'stopRequested';
@@ -400,9 +207,17 @@ export class WavFormat extends AudioFormat {
}
protected setDuration(buffer: ArrayBuffer) {
- this._duration =
- (this.getDataChunkSize(buffer) / (this._channels * this._bitsPerSample)) *
- 8;
+ this._duration = {
+ samples:
+ (this.getDataChunkSize(buffer) /
+ (this._channels * this._bitsPerSample)) *
+ 8,
+ seconds:
+ ((this.getDataChunkSize(buffer) /
+ (this._channels * this._bitsPerSample)) *
+ 8) /
+ this._sampleRate,
+ };
}
private setDataStart(buffer: ArrayBuffer) {
@@ -431,67 +246,4 @@ export class WavFormat extends AudioFormat {
this.dataStart = result;
}
}
-
- private getFileFromBufferPart(
- data: IntArray,
- channels: number,
- filename: string
- ): File {
- return new File([this.getFileDataView(data, channels)], `${filename}.wav`, {
- type: 'audio/wav',
- });
- }
-
- private getFileFromBufferPartArrayBuffer(
- data: IntArray,
- channels: number
- ): ArrayBuffer {
- return this.getFileDataView(data, channels);
- }
-
- public splitChannelsToFiles(
- filename: string,
- type: string,
- buffer: ArrayBuffer
- ): Promise {
- return new Promise((resolve, reject) => {
- const result: File[] = [];
-
- if (this.isValid(buffer)) {
- if (this._channels > 1) {
- const u8array = new Uint8Array(buffer);
-
- const promises: Promise[] = [];
- promises.push(
- this.extractDataFromArray(0, this._duration, u8array, 0)
- );
- promises.push(
- this.extractDataFromArray(0, this._duration, u8array, 1)
- );
-
- Promise.all(promises)
- .then((extracts) => {
- for (let i = 0; i < extracts.length; i++) {
- const extract = extracts[i];
- result.push(
- this.getFileFromBufferPart(extract, 1, `${filename}_${i + 1}`)
- );
- }
- resolve(result);
- })
- .catch((error) => {
- reject(error);
- });
- } else {
- reject(
- `can't split audio file because it contains one channel only.`
- );
- }
- } else {
- reject('no valid wav format!');
- }
-
- return result;
- });
- }
}
diff --git a/libs/web-media/src/lib/audio/audio-cutter.ts b/libs/web-media/src/lib/audio/audio-cutter.ts
new file mode 100644
index 000000000..1610159a6
--- /dev/null
+++ b/libs/web-media/src/lib/audio/audio-cutter.ts
@@ -0,0 +1,396 @@
+import { NumeratedSegment } from '@octra/media';
+import { AudioInfo, IntArray } from '@octra/web-media';
+import { Subject } from 'rxjs';
+import { WavWriter } from './binary';
+
+export class AudioCutter {
+ private status: 'running' | 'stopRequested' | 'stopped' = 'stopped';
+ public onaudiocut = new Subject<{
+ finishedSegments: number;
+ fileName: string;
+ intArray: Uint8Array;
+ }>();
+
+ public formatConstructor!:
+ | Uint8ArrayConstructor
+ | Int16ArrayConstructor
+ | Int32ArrayConstructor;
+
+ constructor(private audioInfo: AudioInfo) {
+ if (this.audioInfo.bitrate === 32) {
+ this.formatConstructor = Int32Array;
+ } else if (this.audioInfo.bitrate === 16) {
+ this.formatConstructor = Int16Array;
+ } else if (this.audioInfo.bitrate === 8) {
+ this.formatConstructor = Uint8Array;
+ }
+ }
+
+ public cutAudioFileFromChannelData(
+ namingConvention: string,
+ buffer: Float32Array,
+ segment: NumeratedSegment
+ ): Promise<{
+ fileName: string;
+ uint8Array: Uint8Array;
+ }> {
+ return new Promise<{
+ fileName: string;
+ uint8Array: Uint8Array;
+ }>((resolve, reject) => {
+ const fileName = this.getNewFileName(
+ namingConvention,
+ this.audioInfo.fullname,
+ segment
+ );
+
+ const data = this.extractDataFromChannelData(
+ segment.sampleStart,
+ segment.sampleDur,
+ buffer
+ );
+
+ const wavWriter = new WavWriter();
+ resolve({
+ fileName,
+ uint8Array: wavWriter.write(
+ [data],
+ this.audioInfo.audioBufferInfo?.sampleRate ??
+ this.audioInfo.sampleRate
+ ),
+ });
+ });
+ }
+
+ public getFileDataView(data: Int16Array, channels: number): ArrayBuffer {
+ // creates a mono data view
+ const blockAlign = (channels * this.audioInfo.bitrate) / 8;
+ const subChunk2Size = data.length * blockAlign;
+
+ const buffer = new ArrayBuffer(44 + data.byteLength);
+ const dataView = new DataView(buffer);
+
+ /* RIFF identifier */
+ this.writeString(dataView, 0, 'RIFF');
+ /* RIFF chunk length */
+ dataView.setUint32(4, 36 + subChunk2Size, true);
+ /* RIFF type */
+ this.writeString(dataView, 8, 'WAVE');
+ /* format chunk identifier */
+ this.writeString(dataView, 12, 'fmt ');
+ /* format chunk length */
+ dataView.setUint32(16, 16, true);
+ /* sample format (raw) */
+ dataView.setUint16(20, 1, true);
+ /* channel count */
+ dataView.setUint16(22, channels, true);
+ /* sample rate */
+ dataView.setUint32(24, this.audioInfo.sampleRate, true);
+ /* byte rate (sample rate * block align) */
+ dataView.setUint32(28, this.audioInfo.sampleRate * blockAlign, true);
+ /* block align (channel count * bytes per sample) */
+ dataView.setUint16(32, blockAlign, true);
+ /* bits per sample */
+ dataView.setUint16(34, this.audioInfo.bitrate, true);
+ /* data chunk identifier */
+ this.writeString(dataView, 36, 'data');
+ /* data chunk length */
+ dataView.setUint32(40, subChunk2Size, true);
+
+ for (let i = 0; i < data.length; i++) {
+ if (data instanceof Uint8Array) {
+ dataView.setUint8(44 + i, data[i]);
+ } else if (this.audioInfo.bitrate === 16) {
+ // little endian must be set!
+ dataView.setUint16(44 + i * 2, data[i], true);
+ } else {
+ //TODO check this
+ dataView.setUint32(44 + i * 4, data[i], true);
+ }
+ }
+ return dataView.buffer;
+ }
+
+ private writeString(view: DataView, offset: number, str: string) {
+ for (let i = 0; i < str.length; i++) {
+ view.setUint8(offset + i, str.charCodeAt(i));
+ }
+ }
+
+ public extractDataFromChannelData(
+ sampleStart: number,
+ sampleDur: number | undefined,
+ channelData: Float32Array
+ ): Float32Array {
+ return channelData.slice(
+ sampleStart,
+ sampleDur ? sampleStart + sampleDur : undefined
+ );
+ }
+
+ getNewFileName(
+ namingConvention: string,
+ fileName: string,
+ segment: NumeratedSegment
+ ) {
+ const name = fileName.substring(0, fileName.lastIndexOf('.'));
+ const extension = fileName.substring(fileName.lastIndexOf('.'));
+
+ let leadingNull = '';
+ const maxDecimals = 4;
+ const decimals = (segment.number + 1).toString().length;
+
+ for (let i = 0; i < maxDecimals - decimals; i++) {
+ leadingNull += '0';
+ }
+
+ return namingConvention.replace(/<([^<>]+)>/g, (g0, g1) => {
+ switch (g1) {
+ case 'name':
+ return name;
+ case 'sequNumber':
+ return `${leadingNull}${segment.number + 1}`;
+ case 'sampleStart':
+ return segment.sampleStart;
+ case 'sampleDur':
+ return segment.sampleDur;
+ case 'secondsStart':
+ return (
+ Math.round(
+ (segment.sampleStart / this.audioInfo.sampleRate) * 1000
+ ) / 1000
+ );
+ case 'secondsDur':
+ return (
+ Math.round(
+ (segment.sampleStart / this.audioInfo.sampleRate) * 1000
+ ) / 1000
+ );
+ }
+ return g1;
+ });
+ }
+
+ public cutChannelDataSequentially(
+ namingConvention: string,
+ buffer: Float32Array,
+ segments: NumeratedSegment[],
+ pointer = 0
+ ): void {
+ if (pointer === 0) {
+ this.status = 'running';
+ }
+ if (pointer > -1 && pointer < segments.length) {
+ let segment = { ...segments[pointer] };
+
+ const channelDataFactor =
+ (this.audioInfo.audioBufferInfo?.sampleRate ??
+ this.audioInfo.sampleRate) / this.audioInfo.sampleRate;
+
+ segment = {
+ ...segment,
+ sampleStart: Math.ceil(segment.sampleStart * channelDataFactor),
+ sampleDur:
+ pointer === segments.length - 1
+ ? undefined
+ : Math.ceil(segment.sampleDur! * channelDataFactor),
+ };
+
+ this.cutAudioFileFromChannelData(namingConvention, buffer, segment)
+ .then(({ fileName, uint8Array }) => {
+ this.onaudiocut.next({
+ finishedSegments: pointer + 1,
+ fileName,
+ intArray: uint8Array,
+ });
+
+ if (pointer < segments.length - 1) {
+ // continue
+ // const freeSpace = window.performance.memory.totalJSHeapSize - window.performance.memory.usedJSHeapSize;
+ // console.log(`${freeSpace / 1024 / 1024} MB left.`);
+ if (this.status === 'running') {
+ setTimeout(
+ () =>
+ this.cutChannelDataSequentially(
+ namingConvention,
+ buffer,
+ segments,
+ ++pointer
+ ),
+ 200
+ );
+ } else {
+ this.status = 'stopped';
+ }
+ } else {
+ // stop
+ this.onaudiocut.complete();
+ }
+ })
+ .catch((error) => {
+ this.onaudiocut.error(error);
+ });
+ } else {
+ this.onaudiocut.error(new Error('pointer is invalid!'));
+ }
+ }
+
+ public splitChannelsToFiles(
+ filename: string,
+ type: string,
+ buffer: ArrayBuffer
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ const result: File[] = [];
+
+ if (this.audioInfo.channels > 1) {
+ const wavWriter = new WavWriter();
+ const u8array = new Uint8Array(buffer);
+
+ const promises: Promise[] = [];
+ promises.push(
+ this.extractDataFromArray(
+ 0,
+ this.audioInfo.duration.samples,
+ u8array,
+ 0
+ )
+ );
+ promises.push(
+ this.extractDataFromArray(
+ 0,
+ this.audioInfo.duration.samples,
+ u8array,
+ 1
+ )
+ );
+
+ Promise.all(promises)
+ .then((extracts) => {
+ for (let i = 0; i < extracts.length; i++) {
+ const extract = extracts[i];
+ result.push(
+ this.getFileFromBufferPart(
+ extract as any,
+ 1,
+ `${filename}_${i + 1}`
+ )
+ );
+ }
+ resolve(result);
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ } else {
+ reject(`can't split audio file because it contains one channel only.`);
+ }
+
+ return result;
+ });
+ }
+
+ private getFileFromBufferPart(
+ data: IntArray,
+ channels: number,
+ filename: string
+ ): File {
+ return new File(
+ [this.getFileDataView(data as any, channels)],
+ `${filename}.wav`,
+ {
+ type: 'audio/wav',
+ }
+ );
+ }
+
+ /***
+ * cuts the data part of selected samples from an Uint8Array
+ * @param sampleStart the start of the extraction
+ * @param sampleDur the duration of the extraction
+ * @param uint8Array the array to be read
+ * @param selectedChannel the selected channel
+ */
+ public extractDataFromArray(
+ sampleStart: number,
+ sampleDur: number,
+ uint8Array: Uint8Array,
+ selectedChannel?: number
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ let convertedData: IntArray;
+ let result: IntArray | undefined = undefined;
+
+ // one block contains one sample of each channel
+ // eg. blockAlign = 4 Byte => 2 * 8 Channel1 + 2 * 8 Channel2 = 32Bit = 4 Byte
+ const channels =
+ selectedChannel !== undefined ? 1 : this.audioInfo.channels;
+ const blockAlign = (this.audioInfo.bitrate / 8) * channels;
+
+ let start = sampleStart * blockAlign;
+ let dataChunkLength = sampleDur * blockAlign;
+ let startPos: number;
+
+ const divider = this.audioInfo.bitrate / 8;
+ if ([32, 16, 8].includes(this.audioInfo.bitrate)) {
+ dataChunkLength = Math.round(dataChunkLength / divider);
+ result = new this.formatConstructor(dataChunkLength);
+ convertedData = new this.formatConstructor(
+ uint8Array.buffer,
+ uint8Array.byteOffset,
+ uint8Array.byteLength / divider
+ );
+ start = Math.round(start / divider);
+ startPos = 44 / divider + Math.round(start);
+ }
+
+ if (result) {
+ // start and duration are the position in bytes after the header
+ const endPos = startPos! + Math.round(dataChunkLength);
+
+ if (selectedChannel === undefined || this.audioInfo.channels === 1) {
+ result.set(convertedData!.slice(startPos!, endPos));
+ resolve(result);
+ } else {
+ // get data from selected channel only
+
+ const channelData: IntArray[] = [];
+ const dataStart = 44 / divider;
+
+ for (let i = 0; i < this.audioInfo.channels; i++) {
+ channelData.push(
+ new this.formatConstructor(
+ Math.round(dataStart + dataChunkLength)
+ )
+ );
+ }
+
+ let pointer = 0;
+ for (let i = startPos!; i < endPos * this.audioInfo.channels; i++) {
+ try {
+ for (let j = 0; j < this.audioInfo.channels; j++) {
+ channelData[j][dataStart + pointer] =
+ convertedData![dataStart + i + j];
+ }
+ i++;
+ pointer++;
+ } catch (e) {
+ reject(e);
+ }
+ }
+
+ result = channelData[selectedChannel];
+ resolve(result);
+ }
+ } else {
+ reject('unsupported bitsPerSample');
+ }
+ });
+ }
+
+ public stopAudioSplitting() {
+ if (this.status === 'running') {
+ this.status = 'stopRequested';
+ }
+ }
+}
diff --git a/libs/web-media/src/lib/audio/audio-decoder.ts b/libs/web-media/src/lib/audio/audio-decoder.ts
index ff27a45a5..d6ef5af3b 100644
--- a/libs/web-media/src/lib/audio/audio-decoder.ts
+++ b/libs/web-media/src/lib/audio/audio-decoder.ts
@@ -1,4 +1,4 @@
-import { AudioInfo } from './audio-info';
+import { AudioInfo, calculateChannelDataFactor } from './audio-info';
import { SubscriptionManager } from '@octra/utilities';
import { Subject, timer } from 'rxjs';
import { SampleUnit } from '@octra/media';
@@ -51,17 +51,7 @@ export class AudioDecoder {
private afterChannelDataFinished?: Subject;
get channelDataFactor() {
- let factor: number;
- if (this.audioInfo.sampleRate === 48000) {
- factor = 3;
- // sampleRate = 16000
- } else if (this.audioInfo.sampleRate === 44100) {
- factor = 2;
- } else {
- // sampleRate = 22050
- factor = 1;
- }
- return factor;
+ return calculateChannelDataFactor(this.audioInfo.sampleRate);
}
constructor(
@@ -141,6 +131,11 @@ export class AudioDecoder {
}
}
+ /**
+ * decodes a WAVE audio file and minimizes according to calculated channel data factor.
+ * @param sampleStart
+ * @param sampleDur
+ */
async getChunkedChannelData(sampleStart: SampleUnit, sampleDur: SampleUnit) {
if (this.format instanceof WavFormat) {
// cut the audio file into 10 parts:
diff --git a/libs/web-media/src/lib/audio/audio-info.ts b/libs/web-media/src/lib/audio/audio-info.ts
index 42449e965..74e3fc4ec 100644
--- a/libs/web-media/src/lib/audio/audio-info.ts
+++ b/libs/web-media/src/lib/audio/audio-info.ts
@@ -30,6 +30,21 @@ export class AudioInfo extends FileInfo {
return this._sampleRate;
}
+ get audioBufferInfo(): { samples: number; sampleRate: number } | undefined {
+ return this._audioBufferInfo;
+ }
+
+ set audioBufferInfo(
+ value: { samples: number; sampleRate: number } | undefined
+ ) {
+ this._audioBufferInfo = value;
+ }
+
+ protected _audioBufferInfo?: {
+ samples: number;
+ sampleRate: number;
+ };
+
constructor(
filename: string,
type: string,
@@ -37,12 +52,44 @@ export class AudioInfo extends FileInfo {
sampleRate: number,
durationSamples: number,
channels: number,
- bitrate: number
+ bitrate: number,
+ audioBufferInfo?: {
+ samples: number;
+ sampleRate: number;
+ }
) {
- super(filename, type, size);
+ super(filename, normalizeMimeType(type), size);
this._sampleRate = sampleRate;
this._duration = new SampleUnit(durationSamples, sampleRate);
this._channels = channels;
this._bitrate = bitrate;
+ this._audioBufferInfo = audioBufferInfo;
+ }
+}
+
+
+/**
+ * calculates the channel data factor by a given sample rate. The channel data factor is needed for reducing channel data to draw audio signal
+ * @param sampleRate
+ */
+export function calculateChannelDataFactor(sampleRate: number) {
+ let factor: number;
+ if (sampleRate === 48000) {
+ factor = 3;
+ // sampleRate = 16000
+ } else if (sampleRate === 44100) {
+ factor = 2;
+ } else {
+ // sampleRate = 22050
+ factor = 1;
}
+ return factor;
+}
+
+export function normalizeMimeType(type: string){
+ switch (type) {
+ case "video/ogg": return "audio/ogg";
+ }
+
+ return type;
}
diff --git a/libs/web-media/src/lib/audio/audio-manager.ts b/libs/web-media/src/lib/audio/audio-manager.ts
index f19b675ad..5981a4a91 100644
--- a/libs/web-media/src/lib/audio/audio-manager.ts
+++ b/libs/web-media/src/lib/audio/audio-manager.ts
@@ -5,6 +5,7 @@ import { AudioFormat } from "./AudioFormats";
import { AudioResource } from "./audio-resource";
import { AudioMechanism } from "./audio-mechanism";
import { HtmlAudioMechanism } from "./html-audio-mechanism";
+import { normalizeMimeType } from "./audio-info";
/**
* AudioManager controls the audio file and all of its chunk. Each audio file should have exactly one manager. The AudioManager uses HTML Audio for playback.
@@ -105,7 +106,7 @@ export class AudioManager {
audioformats: AudioFormat[]
): AudioFormat | undefined {
return audioformats.find((a) => {
- return a.extension === extension;
+ return a.supportedFormats.findIndex((a) => a.extension === extension) > -1;
});
}
@@ -127,6 +128,7 @@ export class AudioManager {
audioManager: AudioManager;
progress: number;
}> => {
+ type = normalizeMimeType(type);
// get result;
const result = new AudioManager(audioMechanism);
return result
diff --git a/libs/web-media/src/lib/audio/audio-mechanism.ts b/libs/web-media/src/lib/audio/audio-mechanism.ts
index 6f544f9a8..2fdcb80b7 100644
--- a/libs/web-media/src/lib/audio/audio-mechanism.ts
+++ b/libs/web-media/src/lib/audio/audio-mechanism.ts
@@ -64,7 +64,6 @@ export abstract class AudioMechanism {
abstract get playPosition(): SampleUnit | undefined;
abstract set playPosition(value: SampleUnit | undefined);
-
abstract get playBackRate(): number;
abstract set playBackRate(value: number);
diff --git a/libs/web-media/src/lib/audio/binary/BinaryReader.ts b/libs/web-media/src/lib/audio/binary/BinaryReader.ts
new file mode 100644
index 000000000..541deaf43
--- /dev/null
+++ b/libs/web-media/src/lib/audio/binary/BinaryReader.ts
@@ -0,0 +1,117 @@
+/**
+ * @Author Klaus Jänsch
+ * Original: https://github.com/IPS-LMU/WebSpeechRecorderNg/blob/master/projects/speechrecorderng/src/lib/io/BinaryReader.ts
+ * Extracted: 2024-11-04
+ **/
+
+export class BinaryByteReader {
+ buf: Uint8Array;
+ sbuf: Int8Array;
+ private _pos: number;
+
+ constructor(buf: ArrayBuffer) {
+ this.buf = new Uint8Array(buf);
+ this.sbuf = new Int8Array(buf);
+ this._pos = 0;
+ }
+
+ get pos(): number {
+ return this._pos;
+ }
+
+ set pos(pos: number) {
+ this._pos = pos;
+ }
+
+ length(): number {
+ return this.buf.byteLength;
+ }
+
+ eof(): boolean {
+ return (this._pos >= this.buf.byteLength);
+ }
+
+ skip(byteCound: number) {
+ this.pos += 4;
+ }
+
+ readAscii(size: number): String {
+ let i;
+ let txt = '';
+ for (i = 0; i < size; i++) {
+ txt += String.fromCharCode(this.buf[this._pos++]);
+ }
+ return txt;
+ }
+
+ readInt8(): number {
+ return (this.buf[this._pos++]);
+ }
+
+ readInt16BE(): number {
+ const b0 = this.sbuf[this._pos++];
+ const b1 = this.buf[this._pos++];
+ const val = (b0 << 8) | b1;
+ return val;
+ }
+
+ readInt16LE(): number {
+ const b0 = this.buf[this._pos++];
+ const b1 = this.sbuf[this._pos++];
+ const val = (b1 << 8) | b0;
+ return val;
+ }
+
+ readUint16LE(): number {
+ const seg = new Uint8Array(2);
+ let i: number;
+ for (i = 0; i < 2; i++) {
+ seg[i] = this.buf[this._pos++];
+ }
+ let val = 0;
+ val |= seg[1] << 8;
+ val |= seg[0];
+ return val;
+ }
+
+ readInt32BE(): number {
+ const seg = new Uint8Array(4);
+ let i: number;
+ for (i = 0; i < 4; i++) {
+ seg[i] = this.buf[this._pos++];
+ }
+ const val = seg[0] << 24 | seg[1] << 16 | seg[2] << 8 | seg[3];
+ return val;
+ }
+
+ readUint32LE(): number {
+ // this dircet data view does not work with nodejs: all avlues are zero
+ // var seg=new Uint8Array(this.buf,this._pos,4);
+ // ... copy value by value
+ const seg = new Uint8Array(4);
+ let i: number;
+ for (i = 0; i < 4; i++) {
+ seg[i] = this.buf[this._pos++];
+ }
+ // console.log("len:", seg.length, " ", seg.byteLength, " ", seg[0], " ", seg[1], " ", seg[2], " ", seg[3]);
+ let val = 0;
+ val |= seg[3] << 24;
+ val |= seg[2] << 16;
+ val |= seg[1] << 8;
+ val |= seg[0];
+ // var val = < 24(seg[3] <) | (seg[2] << 16) | (seg[1] << 8) | seg[0];
+ // this._pos=this._pos+4;
+ return val;
+ }
+
+ readFloat32(): number {
+ const seg = new Float32Array(1);
+ let i: number;
+ for (i = 0; i < 4; i++) {
+ seg[i] = this.buf[this._pos++];
+ }
+ return seg[0];
+ }
+
+
+}
diff --git a/libs/web-media/src/lib/audio/binary/BinaryWriter.ts b/libs/web-media/src/lib/audio/binary/BinaryWriter.ts
new file mode 100644
index 000000000..576ae9585
--- /dev/null
+++ b/libs/web-media/src/lib/audio/binary/BinaryWriter.ts
@@ -0,0 +1,104 @@
+/**
+ * @Author Klaus Jänsch
+ * Original: https://github.com/IPS-LMU/WebSpeechRecorderNg/blob/master/projects/speechrecorderng/src/lib/io/BinaryWriter.ts
+ * Extracted: 2024-11-04
+ **/
+
+export class BinaryByteWriter {
+ static DEFAULT_SIZE_INC = 1024;
+ buf: ArrayBuffer;
+ private _pos: number;
+
+ constructor() {
+ const size = BinaryByteWriter.DEFAULT_SIZE_INC;
+ this.buf = new ArrayBuffer(size);
+ this._pos = 0;
+ }
+
+ get pos(): number {
+ return this._pos;
+ }
+
+ ensureCapacity(numBytes: number) {
+
+ while (this._pos + numBytes >= this.buf.byteLength) {
+ // buffer increment
+ let inc = BinaryByteWriter.DEFAULT_SIZE_INC;
+ if (inc < numBytes) {
+ inc = numBytes;
+ }
+ const newSize = this.buf.byteLength + inc;
+
+ const arrOld = new Uint8Array(this.buf, 0, this._pos);
+ const arrNew = new Uint8Array(newSize);
+ arrNew.set(arrOld);
+ this.buf = arrNew.buffer;
+ }
+ }
+
+ writeUint8(val: number): void {
+ this.ensureCapacity(1);
+ const valView = new DataView(this.buf, this._pos, 1);
+ valView.setUint8(0, val);
+ this._pos++;
+ }
+
+ writeUint16(val: number, le: boolean): void {
+ this.ensureCapacity(2);
+ const valView = new DataView(this.buf, this._pos, 2);
+ valView.setUint16(0, val, le);
+ this._pos += 2;
+
+ }
+
+ writeInt16(val: number, le: boolean): void {
+ this.ensureCapacity(2);
+ const valView = new DataView(this.buf, this._pos, 2);
+ valView.setInt16(0, val, le);
+ this._pos += 2;
+ }
+
+
+ writeUint32(val: number, le: boolean): void {
+ this.ensureCapacity(4);
+ const valView = new DataView(this.buf, this._pos, 4);
+ valView.setUint32(0, val, le);
+ this._pos += 4;
+ }
+
+ writeInt32(val: number, le: boolean): void {
+ this.ensureCapacity(4);
+ const valView = new DataView(this.buf, this._pos, 4);
+ valView.setInt32(0, val, le);
+ this._pos += 4;
+ }
+
+ writeFloat(val: number) {
+ this.ensureCapacity(4);
+ const valView = new DataView(this.buf, this._pos, 4);
+ valView.setFloat32(0, val, true);
+ this._pos += 4;
+ }
+
+ finish(): Uint8Array {
+ const finalArr = new Uint8Array(this._pos);
+ const dv = new DataView(this.buf, 0, this._pos);
+ for (let i = 0; i < this._pos; i++) {
+ finalArr[i] = dv.getUint8(i);
+ }
+ return finalArr;
+ }
+
+ writeAscii(text: string): void {
+ let i;
+ for (i = 0; i < text.length; i++) {
+ const asciiCode = text.charCodeAt(i);
+ if (asciiCode < 0 || asciiCode > 255) {
+ throw new Error('Not an ASCII character at char ' + i + ' in ' + text);
+ }
+ this.writeUint8(asciiCode);
+ }
+ }
+
+}
+
diff --git a/libs/web-media/src/lib/audio/binary/format.ts b/libs/web-media/src/lib/audio/binary/format.ts
new file mode 100644
index 000000000..c6ff00f59
--- /dev/null
+++ b/libs/web-media/src/lib/audio/binary/format.ts
@@ -0,0 +1,35 @@
+/**
+ * @Author Klaus Jänsch
+ * Original: https://github.com/IPS-LMU/WebSpeechRecorderNg/blob/master/projects/speechrecorderng/src/lib/audio/format.ts
+ * Extracted: 2024-11-04
+ **/
+
+export class AudioFormat {
+ sampleRate: number;
+ channelCount: number;
+ constructor(sampleRate: number, channelCount: number) {
+ this.sampleRate = sampleRate;
+ this.channelCount = channelCount;
+ }
+}
+
+export class PCMAudioFormat extends AudioFormat {
+ encodingFloat:boolean=false;
+ sampleSize: number;
+ sampleSizeInBits: number;
+ constructor(sampleRate: number, channelCount: number, sampleSize: number, sampleSizeInBits: number,encodingFloat=false) {
+ super(sampleRate, channelCount);
+ this.sampleSize = sampleSize;
+ this.sampleSizeInBits = sampleSizeInBits;
+ this.encodingFloat=encodingFloat;
+ }
+
+ override toString(): string {
+ const encStr=this.encodingFloat?'Encoding: float,':'';
+
+ return 'Audio format: PCM,'+encStr + this.sampleRate + ' Hz,' + this.channelCount + ' channels, ' + this.sampleSizeInBits + ' bits';
+ }
+}
+
+
+
diff --git a/libs/web-media/src/lib/audio/binary/index.ts b/libs/web-media/src/lib/audio/binary/index.ts
new file mode 100644
index 000000000..6c65814b4
--- /dev/null
+++ b/libs/web-media/src/lib/audio/binary/index.ts
@@ -0,0 +1,6 @@
+export * from './format';
+export * from './wavformat';
+export * from './wavwriter';
+export * from './wavreader';
+export * from './BinaryReader';
+export * from './BinaryWriter';
diff --git a/libs/web-media/src/lib/audio/binary/wavformat.ts b/libs/web-media/src/lib/audio/binary/wavformat.ts
new file mode 100644
index 000000000..5e7c71125
--- /dev/null
+++ b/libs/web-media/src/lib/audio/binary/wavformat.ts
@@ -0,0 +1,16 @@
+/**
+ * @author Klaus Jänsch
+ * Original: https://github.com/IPS-LMU/WebSpeechRecorderNg/blob/master/projects/speechrecorderng/src/lib/audio/impl/wavreader.ts
+ * Extracted: 2024-11-04
+ */
+
+export class WavFileFormat {
+ static readonly RIFF_KEY: string = 'RIFF';
+ static readonly WAV_KEY: string = 'WAVE';
+ static readonly PCM: number = 0x0001;
+ static readonly WAVE_FORMAT_IEEE_FLOAT: number = 0x0003;
+}
+
+
+
+
diff --git a/libs/web-media/src/lib/audio/binary/wavreader.ts b/libs/web-media/src/lib/audio/binary/wavreader.ts
new file mode 100644
index 000000000..cb8ebcc4d
--- /dev/null
+++ b/libs/web-media/src/lib/audio/binary/wavreader.ts
@@ -0,0 +1,187 @@
+/**
+ * @author Klaus Jänsch
+ * Original: https://github.com/IPS-LMU/WebSpeechRecorderNg/blob/master/projects/speechrecorderng/src/lib/audio/impl/wavreader.ts
+ * Extracted: 2024-11-04
+ */
+
+import { WavFileFormat } from './wavformat';
+import { PCMAudioFormat } from './format';
+import { BinaryByteReader } from './BinaryReader';
+
+export class WavReader {
+ private br: BinaryByteReader;
+ private format: PCMAudioFormat | null = null;
+ private totalLength: number = 0;
+ private dataChunkLength: number | null = null;
+
+ constructor(data: ArrayBuffer) {
+ this.br = new BinaryByteReader(data);
+ }
+
+ private readHeader() {
+ const rh = this.br.readAscii(4);
+ if (rh !== WavFileFormat.RIFF_KEY) {
+ const errMsg = 'Expected RIFF header, not: ' + rh;
+ throw new Error(errMsg);
+ }
+ const cl = this.br.readUint32LE();
+ if (this.br.pos + cl !== this.br.length()) {
+ throw new Error(
+ 'Wrong chunksize in RIFF header: ' +
+ cl +
+ ' (expected: ' +
+ (this.br.length() - this.br.pos) +
+ ' )'
+ );
+ }
+ this.totalLength = cl;
+
+ const rt = this.br.readAscii(4);
+ if (rt !== WavFileFormat.WAV_KEY) {
+ const errMsg = 'Expected ' + WavFileFormat.WAV_KEY + ' not: ' + rt;
+ throw new Error(errMsg);
+ }
+ }
+
+ readFormat(): PCMAudioFormat | null {
+ this.br.pos = 0;
+ this.readHeader();
+ const s = this.navigateToChunk('fmt ');
+ if (!s) {
+ let errMsg = 'WAV file does not contain a fmt chunk';
+ throw new Error(errMsg);
+ }
+ this.format = this.parseFmtChunk();
+ return this.format;
+ }
+
+ private _frameLength(): number | null {
+ let fl: number | null = null;
+ if (this.format && this.dataChunkLength != null) {
+ fl =
+ this.dataChunkLength /
+ this.format.channelCount /
+ this.format.sampleSize;
+ }
+ return fl;
+ }
+
+ frameLength(): number | null {
+ let fl: number | null = this._frameLength();
+ if (fl === null) {
+ this.readFormat();
+ this.dataChunkLength = this.navigateToChunk('data');
+ fl = this._frameLength();
+ }
+ return fl;
+ }
+
+ // Not tested yet!!!
+ read(): AudioBuffer | null {
+ this.br.pos = 0;
+ let ab: AudioBuffer | null = null;
+ this.readHeader();
+ let s = this.navigateToChunk('fmt ');
+ if (!s) {
+ let errMsg = 'WAV file does not contain a fmt chunk';
+ throw new Error(errMsg);
+ }
+ this.format = this.parseFmtChunk();
+ this.dataChunkLength = this.navigateToChunk('data');
+ let chsArr = this.readData();
+ let sr = this.format?.sampleRate;
+ let nChs = this.format?.channelCount;
+ if (sr && chsArr && nChs && nChs > 0 && nChs == chsArr?.length) {
+ ab = new AudioBuffer({
+ length: chsArr[0].length,
+ numberOfChannels: this.format?.channelCount,
+ sampleRate: sr,
+ });
+ for (let ch = 0; ch < nChs; ch++) {
+ ab.copyToChannel(chsArr[ch], ch);
+ }
+ }
+ return ab;
+ }
+
+ private navigateToChunk(chunkString: string): number {
+ // position after RIFF header
+ // TODO assumes no other chunks except 'data'
+ this.br.pos = 12;
+ let chkStr = null;
+ let chkLen = -1;
+ while (!this.br.eof()) {
+ chkStr = this.br.readAscii(4);
+ chkLen = this.br.readUint32LE();
+ if (chunkString === chkStr) {
+ return chkLen;
+ }
+ this.br.pos += chkLen;
+ }
+ return chkLen;
+ }
+
+ private parseFmtChunk(): PCMAudioFormat | null {
+ let pcmAf: PCMAudioFormat | null = null;
+ const fmt = this.br.readUint16LE();
+ if (
+ fmt === WavFileFormat.PCM ||
+ fmt == WavFileFormat.WAVE_FORMAT_IEEE_FLOAT
+ ) {
+ const channels = this.br.readUint16LE();
+ const sampleRate = this.br.readUint32LE();
+
+ // skip bandwidth
+ this.br.skip(4);
+
+ // frame size
+ const frameSize = this.br.readUint16LE();
+
+ // sample size in bits (PCM format only)
+ const sampleSizeInBits = this.br.readUint16LE();
+
+ pcmAf = new PCMAudioFormat(
+ sampleRate,
+ channels,
+ frameSize / channels,
+ sampleSizeInBits,
+ fmt === WavFileFormat.WAVE_FORMAT_IEEE_FLOAT
+ );
+ }
+ return pcmAf;
+ }
+
+ private readData(): Array | null {
+ let chsArr = null;
+ if (this.format) {
+ chsArr = new Array(this.format.channelCount);
+ const sampleCount =
+ this.totalLength / this.format.channelCount / this.format.sampleSize;
+ for (let ch = 0; ch < this.format.channelCount; ch++) {
+ chsArr[ch] = new Float32Array(sampleCount);
+ }
+ if (this.format.encodingFloat) {
+ // Not tested yet!
+ for (let i = 0; i < this.totalLength / 4; i++) {
+ for (let ch = 0; ch < this.format.channelCount; ch++) {
+ chsArr[ch][i] = this.br.readFloat32();
+ }
+ }
+ } else {
+ if (this.format.sampleSize == 2) {
+ for (let i = 0; i < this.totalLength / 2; i++) {
+ for (let ch = 0; ch < this.format.channelCount; ch++) {
+ const s16Ampl = this.br.readInt16LE();
+ chsArr[ch][i] = s16Ampl / 32768;
+ }
+ }
+ }
+ }
+ }
+ return chsArr;
+ }
+}
+
+
+
+
diff --git a/libs/web-media/src/lib/audio/binary/wavwriter.ts b/libs/web-media/src/lib/audio/binary/wavwriter.ts
new file mode 100644
index 000000000..3078984d4
--- /dev/null
+++ b/libs/web-media/src/lib/audio/binary/wavwriter.ts
@@ -0,0 +1,273 @@
+/**
+ * @author Klaus Jänsch
+ * Original: https://github.com/IPS-LMU/WebSpeechRecorderNg/blob/master/projects/speechrecorderng/src/lib/audio/impl/wavwriter.ts
+ * Original: WorkerHelper: https://github.com/IPS-LMU/WebSpeechRecorderNg/blob/master/projects/speechrecorderng/src/lib/utils/utils.ts
+ * Extracted: 2024-11-04
+ */
+
+import { WavFileFormat } from './wavformat';
+import { BinaryByteWriter } from './BinaryWriter';
+
+declare function postMessage(message: any, transfer: Array): void;
+
+class WorkerHelper {
+ static DEBUG = false;
+
+ static buildWorkerBlobURL(workerFct: Function): string {
+ if (!(workerFct instanceof Function)) {
+ throw new Error('Parameter workerFct is not a function! (XSS attack?).');
+ }
+ let woFctNm = workerFct.name;
+ if (WorkerHelper.DEBUG) {
+ console.info('Worker method name: ' + woFctNm);
+ }
+
+ let woFctStr = workerFct.toString();
+ if (WorkerHelper.DEBUG) {
+ console.info('Worker method string:');
+ console.info(woFctStr);
+ }
+
+ // Make sure code starts with "function()"
+
+ // Chrome, Firefox: "[wofctNm](){...}", Safari: "function [wofctNm](){...}"
+ // we need an anonymous function: "function() {...}"
+ let piWoFctStr = woFctStr.replace(/^function +/, '');
+
+ if (WorkerHelper.DEBUG) {
+ console.info('Worker platform independent function string:');
+ console.info(piWoFctStr);
+ }
+
+ // Convert to anonymous function
+ let anonWoFctStr = piWoFctStr.replace(woFctNm + '()', 'function()');
+ if (WorkerHelper.DEBUG) {
+ console.info('Worker anonymous function string:');
+ console.info(piWoFctStr);
+ }
+ // Self executing
+ let ws = '(' + anonWoFctStr + ')();';
+ if (WorkerHelper.DEBUG) {
+ console.info('Worker self executing anonymous function string:');
+ console.info(anonWoFctStr);
+ }
+ // Build the worker blob
+ let wb = new Blob([ws], { type: 'text/javascript' });
+
+ let workerBlobUrl = window.URL.createObjectURL(wb);
+ return workerBlobUrl;
+ }
+}
+
+export enum SampleSize {
+ INT16 = 16,
+ INT32 = 32,
+}
+
+export class WavWriter {
+ static readonly DEFAULT_SAMPLE_SIZE: SampleSize = SampleSize.INT16;
+ private readonly sampleSizeInBytes: number =
+ WavWriter.DEFAULT_SAMPLE_SIZE.valueOf() / 8;
+ private encodingFloat: boolean = false;
+ private sampleSize = WavWriter.DEFAULT_SAMPLE_SIZE;
+ private sampleSizeInBits = this.sampleSize.valueOf();
+ private bw: BinaryByteWriter;
+ private workerURL: string | null = null;
+
+ constructor(encodingFloat?: boolean, sampleSize?: SampleSize) {
+ //console.debug("WavWriter: "+encodingFloat+", "+sampleSize);
+ if (encodingFloat !== undefined && encodingFloat !== null) {
+ this.encodingFloat = encodingFloat;
+ if (encodingFloat) {
+ this.sampleSize = SampleSize.INT32;
+ } else {
+ if (sampleSize) {
+ this.sampleSize = sampleSize;
+ }
+ }
+ } else if (sampleSize) {
+ this.sampleSize = sampleSize;
+ }
+ this.sampleSizeInBits = this.sampleSize.valueOf();
+ this.sampleSizeInBytes = Math.round(this.sampleSizeInBits / 8);
+
+ this.bw = new BinaryByteWriter();
+ }
+
+ /*
+ * Method used as worker code.
+ */
+ workerFunction() {
+ self.onmessage = function (msg: MessageEvent) {
+ const valView = new DataView(msg.data.buf, msg.data.bufPos);
+ const sampleSizeInbytes = Math.round(msg.data.sampleSizeInBits / 8);
+ let bufPos = 0;
+ const hDynIntRange = 1 << (msg.data.sampleSizeInBits - 1);
+ for (let s = 0; s < msg.data.frameLength; s++) {
+ // interleaved channel data
+
+ for (let ch = 0; ch < msg.data.chs; ch++) {
+ const srcPos = ch * msg.data.frameLength + s;
+ const valFlt = msg.data.audioData[srcPos];
+ if (msg.data.encodingFloat === true) {
+ valView.setFloat32(bufPos, valFlt, true);
+ bufPos += 4;
+ } else {
+ const valInt = Math.round(valFlt * hDynIntRange);
+ if (msg.data.sampleSizeInBits === 32) {
+ valView.setInt32(bufPos, valInt, true);
+ } else {
+ valView.setInt16(bufPos, valInt, true);
+ }
+ bufPos += sampleSizeInbytes;
+ }
+ }
+ }
+ postMessage({ buf: msg.data.buf }, [msg.data.buf]);
+ //self.close()
+ };
+ }
+
+ writeFmtChunk(channelData: Float32Array[], sampleRate: number) {
+ if (this.encodingFloat) {
+ this.bw.writeUint16(WavFileFormat.WAVE_FORMAT_IEEE_FLOAT, true);
+ } else {
+ this.bw.writeUint16(WavFileFormat.PCM, true);
+ }
+ const frameSize = this.sampleSizeInBytes * channelData.length;
+ this.bw.writeUint16(channelData.length, true);
+ this.bw.writeUint32(sampleRate, true);
+ // dwAvgBytesPerSec
+ this.bw.writeUint32(frameSize * sampleRate, true);
+ this.bw.writeUint16(frameSize, true);
+ // sample size in bits (PCM format only)
+ this.bw.writeUint16(this.sampleSizeInBits, true);
+ if (this.encodingFloat) {
+ this.bw.writeUint16(0, true);
+ }
+ }
+
+ writeFactChunk(ch0: Float32Array) {
+ let sampleLen = 0;
+ if (ch0) {
+ sampleLen = ch0.length;
+ }
+ this.bw.writeUint32(sampleLen, true);
+ }
+
+ writeDataChunk(channelData: Float32Array[]) {
+ const dataLen = channelData[0].length;
+ if (this.encodingFloat) {
+ for (let s = 0; s < dataLen; s++) {
+ // interleaved channel data
+ for (let ch = 0; ch < channelData.length; ch++) {
+ const chData = channelData[ch];
+ const valFlt = chData[s];
+ this.bw.writeFloat(valFlt);
+ }
+ }
+ } else {
+ const hDynIntRange = 1 << (this.sampleSizeInBits - 1);
+ for (let s = 0; s < dataLen; s++) {
+ // interleaved channel data
+ for (let ch = 0; ch < channelData.length; ch++) {
+ const chData = channelData[ch];
+ const valFlt = chData[s];
+ const valInt = Math.round(valFlt * hDynIntRange);
+ if (this.sampleSize === SampleSize.INT16) {
+ this.bw.writeInt16(valInt, true);
+ } else if (this.sampleSize === SampleSize.INT32) {
+ this.bw.writeInt32(valInt, true);
+ }
+ }
+ }
+ }
+ }
+
+ writeChunkHeader(name: string, chkLen: number) {
+ this.bw.writeAscii(name);
+ this.bw.writeUint32(chkLen, true);
+ }
+
+ writeAsync(
+ channelData: Float32Array[],
+ numberOfChannels: number,
+ sampleRate: number,
+ callback: (wavFileData: Uint8Array) => any
+ ) {
+ const dataChkByteLen = this.writeHeader(channelData, sampleRate);
+ if (!this.workerURL) {
+ this.workerURL = WorkerHelper.buildWorkerBlobURL(this.workerFunction);
+ }
+ const wo = new Worker(this.workerURL);
+
+ const chs = numberOfChannels;
+ const frameLength = channelData[0].length;
+ const ad = new Float32Array(chs * frameLength);
+ for (let ch = 0; ch < chs; ch++) {
+ ad.set(channelData[ch], ch * frameLength);
+ }
+ // ensureCapacity blocks !!!
+ this.bw.ensureCapacity(dataChkByteLen);
+ wo.onmessage = (me) => {
+ callback(me.data.buf);
+ wo.terminate();
+ };
+
+ wo.postMessage(
+ {
+ encodingFloat: this.encodingFloat,
+ sampleSizeInBits: this.sampleSizeInBits,
+ chs: chs,
+ frameLength: frameLength,
+ audioData: ad,
+ buf: this.bw.buf,
+ bufPos: this.bw.pos,
+ },
+ [ad.buffer, this.bw.buf]
+ );
+ }
+
+ write(channelData: Float32Array[], sampleRate: number): Uint8Array {
+ this.writeHeader(channelData, sampleRate);
+ this.writeDataChunk(channelData);
+ return this.bw.finish();
+ }
+
+ writeHeader(channelData: Float32Array[], sampleRate: number): number {
+ this.bw.writeAscii(WavFileFormat.RIFF_KEY);
+ let dataChkByteLen = 0;
+ //const dataChkByteLen=audioBuffer.getChannelData(0).length*WavWriter.DEFAULT_SAMPLE_SIZE_BYTES*audioBuffer.numberOfChannels;
+ const abChs = channelData.length;
+ if (abChs > 0) {
+ const abCh0 = channelData[0];
+ dataChkByteLen = abCh0.length * this.sampleSizeInBytes * abChs;
+ }
+ let headerCnts = 3; //Wave,fmt and data
+
+ let fmtChunkSize = 16;
+ let factChunkSize = 4;
+ if (this.encodingFloat) {
+ fmtChunkSize = 18;
+ headerCnts++; // fact
+ } // Float encoding requires fmt extension (with zero length)
+ let wavChunkByteLen = (4 + 4) * headerCnts;
+
+ wavChunkByteLen += fmtChunkSize;
+ wavChunkByteLen += factChunkSize;
+ wavChunkByteLen += dataChkByteLen;
+
+ this.bw.writeUint32(wavChunkByteLen, true); // must be set to file length-8 later
+ this.bw.writeAscii(WavFileFormat.WAV_KEY);
+
+ this.writeChunkHeader('fmt ', fmtChunkSize);
+ this.writeFmtChunk(channelData, sampleRate);
+ if (this.encodingFloat) {
+ console.debug("Write WAV header: Write 'fact' chunk.");
+ this.writeChunkHeader('fact', 4);
+ this.writeFactChunk(channelData[0]);
+ }
+ this.writeChunkHeader('data', dataChkByteLen);
+ return dataChkByteLen;
+ }
+}
diff --git a/libs/web-media/src/lib/audio/html-audio-mechanism.ts b/libs/web-media/src/lib/audio/html-audio-mechanism.ts
index d044ce547..b477a9bbd 100644
--- a/libs/web-media/src/lib/audio/html-audio-mechanism.ts
+++ b/libs/web-media/src/lib/audio/html-audio-mechanism.ts
@@ -11,6 +11,7 @@ import {
AudioManager,
AudioResource,
getAudioInfo,
+ MusicMetadataFormat,
WavFormat,
} from '@octra/web-media';
import { SourceType } from '../types';
@@ -22,7 +23,10 @@ export class HtmlAudioMechanism extends AudioMechanism {
private _statusRequest: PlayBackStatus = PlayBackStatus.INITIALIZED;
private callBacksAfterEnded: (() => void)[] = [];
- private audioFormats: AudioFormat[] = [new WavFormat()];
+ private audioFormats: AudioFormat[] = [
+ new WavFormat(),
+ new MusicMetadataFormat(),
+ ];
private decoder?: AudioDecoder;
get playPosition(): SampleUnit | undefined {
@@ -63,6 +67,7 @@ export class HtmlAudioMechanism extends AudioMechanism {
this.prepareAudioChannel(options).pipe(
map(({ decodeProgress }) => {
if (decodeProgress === 1) {
+ const buffer = this._resource!.arraybuffer!;
this.prepareAudioPlayback({
...options,
url: URL.createObjectURL(
@@ -115,93 +120,166 @@ export class HtmlAudioMechanism extends AudioMechanism {
this.audioFormats
);
- if (!audioformat) {
- subj.error(
- `audio format not supported: ${filename.substring(
- filename.lastIndexOf('.')
- )} from ${filename}`
- );
- } else if (!buffer) {
+ if (!buffer) {
subj.error(`buffer is undefined`);
} else {
- audioformat.init(filename, buffer);
+ if (audioformat) {
+ audioformat
+ .init(filename, type, buffer)
+ .then(() => {
+ // audio format contains required information
+
+ let audioInfo = getAudioInfo(audioformat, filename, type, buffer);
+ const bufferLength = buffer.byteLength;
+
+ audioInfo = new AudioInfo(
+ filename,
+ type,
+ bufferLength,
+ audioformat.sampleRate,
+ audioformat.duration.samples,
+ audioformat.channels,
+ audioInfo.bitrate
+ );
+
+ audioInfo.file = new File([buffer], filename, {
+ type,
+ });
- try {
- let audioInfo = getAudioInfo(audioformat, filename, type, buffer);
-
- const bufferLength = buffer.byteLength;
- // decode first 10 seconds
- const sampleDur = new SampleUnit(
- Math.min(audioformat.sampleRate * 30, audioformat.duration),
- audioformat.sampleRate
- );
-
- audioInfo = new AudioInfo(
- filename,
- type,
- bufferLength,
- audioformat.sampleRate,
- (audioformat.sampleRate * audioformat.duration) /
- audioformat.sampleRate,
- audioformat.channels,
- audioInfo.bitrate
- );
- audioInfo.file = new File([buffer], filename, { type: 'audio/wav' });
-
- this.playPosition = new SampleUnit(0, audioInfo.sampleRate);
- this._resource = new AudioResource(
- filename,
- SourceType.ArrayBuffer,
- audioInfo,
- buffer,
- bufferLength,
- url
- );
-
- this.afterDecoded.next(this._resource);
-
- this.statistics.decoding.started = Date.now();
- const subscr = this.decodeAudio(this._resource).subscribe({
- next: (statusItem) => {
- if (statusItem.progress === 1) {
- this.statistics.decoding.duration =
- Date.now() - this.statistics.decoding.started;
-
- this.changeStatus(PlayBackStatus.INITIALIZED);
-
- setTimeout(() => {
- subj.next({
- decodeProgress: 1,
- });
- subj.complete();
- }, 0);
- } else {
- subj.next({
- decodeProgress: statusItem.progress,
- });
+ this.playPosition = new SampleUnit(0, audioInfo.sampleRate);
+ this._resource = new AudioResource(
+ filename,
+ SourceType.ArrayBuffer,
+ audioInfo,
+ buffer,
+ bufferLength,
+ url
+ );
+
+ this.afterDecoded.next(this._resource!);
+
+ if (audioformat.decoder === 'octra') {
+ this.decodeAudioWithOctraDecoder(subj);
+ } else if (audioformat.decoder === 'web-audio') {
+ this.decodeAudioWithWebAPIDecoder(subj);
}
- },
- error: (error) => {
- console.error(error);
- subscr.unsubscribe();
- },
- complete: () => {
- subscr.unsubscribe();
- },
- });
- } catch (err: any) {
- subj.error(err!.message);
+ })
+ .catch((e) => {
+ subj.error(e.message);
+ });
+ } else {
+ subj.error(`Audio format is not supported.`);
}
}
return subj;
}
+ private decodeAudioWithOctraDecoder(
+ subj: Subject<{
+ decodeProgress: number;
+ }>
+ ) {
+ try {
+ if (!this._resource) {
+ subj.error('Missing resource');
+ return;
+ }
+
+ this.statistics.decoding.started = Date.now();
+ const subscr = this.decodeAudio(this._resource!).subscribe({
+ next: (statusItem) => {
+ if (statusItem.progress === 1) {
+ this.statistics.decoding.duration =
+ Date.now() - this.statistics.decoding.started;
+
+ this.changeStatus(PlayBackStatus.INITIALIZED);
+
+ setTimeout(() => {
+ subj.next({
+ decodeProgress: 1,
+ });
+ subj.complete();
+ }, 0);
+ } else {
+ subj.next({
+ decodeProgress: statusItem.progress,
+ });
+ }
+ },
+ error: (error) => {
+ console.error(error);
+ subscr.unsubscribe();
+ },
+ complete: () => {
+ subscr.unsubscribe();
+ },
+ });
+ } catch (err: any) {
+ subj.error(err.message);
+ }
+ }
+
+ private decodeAudioWithWebAPIDecoder(
+ subj: Subject<{
+ decodeProgress: number;
+ }>
+ ) {
+ try {
+ if (!this._resource) {
+ subj.error('Missing resource');
+ return;
+ }
+
+ this.statistics.decoding.started = Date.now();
+ this.initAudioContext();
+ this._audioContext!.decodeAudioData(this._resource.arraybuffer?.slice(0)!)
+ .then((audioBuffer) => {
+ const info = this._resource?.info!;
+
+ info.audioBufferInfo = {
+ sampleRate: audioBuffer.sampleRate,
+ samples: audioBuffer.getChannelData(0).length,
+ };
+
+ if (['.mp3', '.m4a'].includes(info.extension)) {
+ // fix number of samples. web value by web audio api is more exact.
+ info.duration = new SampleUnit(
+ Math.ceil(audioBuffer.duration * info.sampleRate),
+ info.sampleRate
+ );
+ }
+
+ this._channel = audioBuffer.getChannelData(0);
+ this.onChannelDataChange.next();
+ this.onChannelDataChange.complete();
+ this.statistics.decoding.duration =
+ Date.now() - this.statistics.decoding.started;
+ this._channelDataFactor =
+ this._resource?.info.sampleRate! /
+ (this._resource?.info.audioBufferInfo!.sampleRate ??
+ this._resource?.info.sampleRate!);
+
+ this.changeStatus(PlayBackStatus.INITIALIZED);
+
+ setTimeout(() => {
+ subj.next({
+ decodeProgress: 1,
+ });
+ subj.complete();
+ }, 0);
+ })
+ .catch((e) => {
+ subj.error(e);
+ });
+ } catch (e) {}
+ }
+
override decodeAudio(resource: AudioResource) {
const result = new Subject<{ progress: number }>();
const format = new WavFormat();
- format.init(resource.info.name, resource.arraybuffer!);
+ format.init(resource.info.name, resource.info.type, resource.arraybuffer!);
this.decoder = new AudioDecoder(
format,
resource.info,
diff --git a/libs/web-media/src/lib/audio/index.ts b/libs/web-media/src/lib/audio/index.ts
index f239c030a..ca7fe8cd9 100644
--- a/libs/web-media/src/lib/audio/index.ts
+++ b/libs/web-media/src/lib/audio/index.ts
@@ -4,3 +4,5 @@ export * from "./audio-time-calculator";
export * from "./AudioFormats";
export * from "./audio-info";
export * from "./audio-resource";
+export * as Binary from "./binary";
+export * from "./audio-cutter";
diff --git a/libs/web-media/src/lib/functions.ts b/libs/web-media/src/lib/functions.ts
index 24e6703fa..959ad94d3 100644
--- a/libs/web-media/src/lib/functions.ts
+++ b/libs/web-media/src/lib/functions.ts
@@ -224,11 +224,11 @@ export function getAudioInfo(
type,
buffer.byteLength,
format.sampleRate,
- format.duration,
+ format.duration.samples,
format.channels,
format.bitsPerSample
);
} else {
- throw new Error(`Audio file is not a valid ${format.extension} file.`);
+ throw new Error(`Audio file is not a valid file.`);
}
}
diff --git a/libs/web-media/vite.config.ts b/libs/web-media/vite.config.ts
index 925cf4295..cc10f3991 100644
--- a/libs/web-media/vite.config.ts
+++ b/libs/web-media/vite.config.ts
@@ -35,7 +35,7 @@ export default defineConfig({
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
- formats: ['es', 'cjs', 'umd', 'iife'],
+ formats: ['es', 'cjs'],
},
rollupOptions: {
output: {
diff --git a/nx.json b/nx.json
index d46bd9038..84afada1f 100644
--- a/nx.json
+++ b/nx.json
@@ -5,13 +5,8 @@
},
"targetDefaults": {
"build": {
- "dependsOn": [
- "^build"
- ],
- "inputs": [
- "production",
- "^production"
- ],
+ "dependsOn": ["^build"],
+ "inputs": ["production", "^production"],
"cache": true
},
"@nx/eslint:lint": {
@@ -24,11 +19,7 @@
"cache": true
},
"@nx/jest:jest": {
- "inputs": [
- "default",
- "^production",
- "{workspaceRoot}/jest.preset.js"
- ],
+ "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
"cache": true,
"options": {
"passWithNoTests": true
@@ -42,10 +33,7 @@
}
},
"namedInputs": {
- "default": [
- "{projectRoot}/**/*",
- "sharedGlobals"
- ],
+ "default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": [
"default",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
@@ -73,5 +61,10 @@
"style": "scss"
}
},
- "useInferencePlugins": false
+ "useInferencePlugins": false,
+ "pluginsConfig": {
+ "@nx/js": {
+ "analyzeSourceFiles": false
+ }
+ }
}
diff --git a/package-lock.json b/package-lock.json
index d6c41ec8a..2c1b7f286 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -50,6 +50,7 @@
"konva": "^9.3.15",
"luxon": "^3.5.0",
"modernizr": "^3.13.0",
+ "music-metadata": "file:../music-metadata",
"ngrx-wieder": "^13.0.0",
"ngx-jodit": "^3.1.3",
"ngx-webstorage": "^18.0.0",
@@ -124,6 +125,54 @@
"yargs": "^17.7.2"
}
},
+ "../music-metadata": {
+ "version": "10.5.1",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ },
+ {
+ "type": "buymeacoffee",
+ "url": "https://buymeacoffee.com/borewit"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@tokenizer/token": "^0.3.0",
+ "content-type": "^1.0.5",
+ "debug": "^4.3.7",
+ "file-type": "^19.6.0",
+ "media-typer": "^1.1.0",
+ "strtok3": "^9.0.1",
+ "token-types": "^6.0.0",
+ "uint8array-extras": "^1.4.0"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "1.9.3",
+ "@types/chai": "^5.0.0",
+ "@types/chai-as-promised": "^8.0.1",
+ "@types/content-type": "^1.1.8",
+ "@types/debug": "^4.1.12",
+ "@types/media-typer": "^1.1.3",
+ "@types/mocha": "^10.0.9",
+ "@types/node": "^22.7.5",
+ "c8": "^10.1.2",
+ "chai": "^5.1.1",
+ "chai-as-promised": "^8.0.0",
+ "del-cli": "^6.0.0",
+ "mime": "^4.0.4",
+ "mocha": "^10.7.3",
+ "prettier": "^3.3.3",
+ "remark-cli": "^12.0.1",
+ "remark-preset-lint-consistent": "^6.0.0",
+ "ts-node": "^10.9.2",
+ "typescript": "^5.6.3"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/@adobe/css-tools": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz",
@@ -13864,11 +13913,11 @@
}
},
"node_modules/debug": {
- "version": "4.3.5",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
- "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -21456,9 +21505,9 @@
}
},
"node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/msgpackr": {
"version": "1.11.0",
@@ -21503,6 +21552,10 @@
"multicast-dns": "cli.js"
}
},
+ "node_modules/music-metadata": {
+ "resolved": "../music-metadata",
+ "link": true
+ },
"node_modules/mute-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
@@ -22854,9 +22907,9 @@
}
},
"node_modules/peek-readable": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
- "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==",
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.3.1.tgz",
+ "integrity": "sha512-GVlENSDW6KHaXcd9zkZltB7tCLosKB/4Hg0fqBJkAoBgYG2Tn1xtMgXtSUuMU9AK/gCm/tTdT8mgAeF4YNeeqw==",
"dev": true,
"engines": {
"node": ">=14.16"
@@ -26431,11 +26484,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
- "node_modules/send/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- },
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
diff --git a/package.json b/package.json
index 7e881709d..e790424f4 100644
--- a/package.json
+++ b/package.json
@@ -105,6 +105,7 @@
"konva": "^9.3.15",
"luxon": "^3.5.0",
"modernizr": "^3.13.0",
+ "music-metadata": "file:../music-metadata",
"ngrx-wieder": "^13.0.0",
"ngx-jodit": "^3.1.3",
"ngx-webstorage": "^18.0.0",
@@ -177,5 +178,6 @@
"vite-plugin-dts": "~2.3.0",
"vitest": "1.6.0",
"yargs": "^17.7.2"
- }
+ },
+ "packageManager": "yarn@4.3.1"
}