Skip to content

Commit

Permalink
Add per channel export
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielPXL committed Mar 13, 2024
1 parent 0543b23 commit e36ecd3
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 3 deletions.
13 changes: 12 additions & 1 deletion src/AudioWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ callables.set("startExport", (data) => {
exportBuffer.push(buffer[i].slice());
}
},
bufferLength: 1024 * 16
bufferLength: 1024 * 16,
activeTracks: data.activeTracks ? data.activeTracks : 0xFFFF
});
});

Expand All @@ -131,3 +132,13 @@ callables.set("exportTickUntilBuffer", (data) => {
exportBuffer = null;
return buffer;
});

callables.set("exportFindAllocatedTracks", (data) => {
let tracks = 0xFFFF;
const cmd = exportRenderer.commands[0];
if (cmd instanceof Audio.Commands.AllocateTracks) {
tracks = cmd.tracks;
}

return tracks;
});
6 changes: 4 additions & 2 deletions src/export/ExportManager.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { StreamSourceController } from "./Exporter";
import { waveExporter } from "./wave/WaveExporter";
import * as ServiceWorkerComms from "../ServiceWorkerComms";
import * as ExportDialog from "./ExportDialog";
import * as ProgressStatus from "./ProgressStatus";
import { ControlSection } from "../ControlSection";
import { waveExporter } from "./wave/WaveExporter";
import { wavePerChannelExporter } from "./wavePerChannel/WavePerChannelExporter";

export const exporters = [
waveExporter
waveExporter,
wavePerChannelExporter
]

export function init() {
Expand Down
4 changes: 4 additions & 0 deletions src/export/wave/WaveFile.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export function getFileSize(numSamples: number, numChannels: number) {
return 44 + numSamples * numChannels * 4;
}

export function header(numSamples: number, numChannels: number, sampleRate: number) {
const buffer = new ArrayBuffer(44);
const view = new DataView(buffer);
Expand Down
76 changes: 76 additions & 0 deletions src/export/wavePerChannel/TarFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
function setCString(view: DataView, offset: number, str: string, length: number) {
for (let i = 0; i < length; i++) {
if (i < str.length) {
view.setUint8(offset + i, str.charCodeAt(i));
} else {
view.setUint8(offset + i, 0);
}
}
}

function numberToOctalString(num: number, length: number) {
const str = num.toString(8);
return "0".repeat(length - str.length) + str;
}

export class TarFile {
constructor() {
this.tarHead = 0;
}

tarHead: number;

fileHeader(filename: string, size: number, mtime: Date) {
const header = new ArrayBuffer(512);
const view = new DataView(header);

setCString(view, 0, filename, 100); // File name
setCString(view, 100, "0000777", 8); // File mode
setCString(view, 108, "0000000", 8); // Owner ID
setCString(view, 116, "0000000", 8); // Group ID
setCString(view, 124, numberToOctalString(size, 11), 12); // File size
const time = Math.floor(mtime.getTime() / 1000);
setCString(view, 136, numberToOctalString(time, 11), 12); // Modification time
setCString(view, 148, " ", 8); // Checksum (filled in later)
setCString(view, 156, "0", 1); // Type (0 = regular file)
setCString(view, 157, "", 100); // Link name
setCString(view, 257, "ustar", 6); // Magic
setCString(view, 263, "00", 2); // Version (no null byte)
setCString(view, 265, "", 32); // Owner name
setCString(view, 297, "", 32); // Group name
setCString(view, 329, "0000000", 8); // Device major number
setCString(view, 337, "0000000", 8); // Device minor number

// Fill in checksum
let checksum = 0;
for (let i = 0; i < 512; i++) {
checksum += view.getUint8(i);
}
setCString(view, 148, numberToOctalString(checksum, 6), 7);

return new Uint8Array(header);
}

fileData(buf: Uint8Array) {
this.tarHead += buf.length;
return buf;
}

fileEnd() {
const remainder = 512 - (this.tarHead % 512);
const block = new ArrayBuffer(remainder);
const view = new DataView(block);
setCString(view, 0, "", remainder); // Fill with null bytes

this.tarHead = 0;

return new Uint8Array(block);
}

tarEnd() {
const block = new ArrayBuffer(1024);
const view = new DataView(block);
setCString(view, 0, "", 1024); // Fill with null bytes
return new Uint8Array(block);
}
}
88 changes: 88 additions & 0 deletions src/export/wavePerChannel/WavePerChannelExporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Exporter, StreamSource } from "../Exporter";
import * as AudioWorkerComms from "../../AudioWorkerComms";
import * as WaveFile from "../wave/WaveFile";
import { TarFile } from "./TarFile";

function trackBitmaskToArray(mask: number) {
const arr: number[] = [];
for (let i = 0; i < 16; i++) {
if (mask & (1 << i)) {
arr.push(i);
}
}
return arr;
}

export const wavePerChannelExporter: Exporter = {
name: "Wave (per channel)",
storageTag: "wavePerChannel",
mimeType: "application/x-tar",
fileExtension: "tar",
getStream(sampleRate, seconds, onProgress, config) {
const numSamples = Math.floor(sampleRate * seconds);
let i = 0;
let tracksToExport: number[];
let trackIndex = -1; // because we start with master.wav
const tar = new TarFile();

const stream: StreamSource = {
async start(controller) {
await AudioWorkerComms.call("startExport", { sampleRate });
const allocatedTracks = await AudioWorkerComms.call("exportFindAllocatedTracks");
tracksToExport = trackBitmaskToArray(allocatedTracks);

const tarHeader = tar.fileHeader("master.wav", WaveFile.getFileSize(numSamples, 2), new Date());
controller.enqueue(tarHeader);

const waveHeader = WaveFile.header(numSamples, 2, sampleRate);
controller.enqueue(tar.fileData(waveHeader));
},
async pull(controller) {
const pcmBuf: Float32Array[] = await AudioWorkerComms.call("exportTickUntilBuffer");
const interleavedBuf = WaveFile.interleave(pcmBuf);

i += pcmBuf[0].length;
if (i > numSamples) {
// Done with the current file, strip the data that's above the size of the file
const samplesToRemove = i - numSamples;
const newBuf = interleavedBuf.slice(0, interleavedBuf.length - samplesToRemove * 2);
const byteBuf = new Uint8Array(newBuf.buffer);

controller.enqueue(tar.fileData(byteBuf));
controller.enqueue(tar.fileEnd());

if (trackIndex < tracksToExport.length - 1) {
// Start the next track
trackIndex++;
await AudioWorkerComms.call("startExport", { sampleRate, activeTracks: 1 << tracksToExport[trackIndex] });
i = 0;

// Add the header for the next track
const trackName = `track${tracksToExport[trackIndex]}.wav`;
const tarHeader = tar.fileHeader(trackName, WaveFile.getFileSize(numSamples, 2), new Date());
controller.enqueue(tarHeader);

const waveHeader = WaveFile.header(numSamples, 2, sampleRate);
controller.enqueue(tar.fileData(waveHeader));
} else {
// All tracks are done
controller.enqueue(tar.tarEnd());
controller.close();
}
} else {
// Not done, add the whole buffer
const byteBuf = new Uint8Array(interleavedBuf.buffer);

// Assumes that the buffer is a multiple of 512 bytes (which it is, see AudioWorker)
controller.enqueue(tar.fileData(byteBuf));
onProgress((i / numSamples + trackIndex + 1) / (tracksToExport.length + 1));
}
},
cancel() {

}
};

return stream;
}
}

0 comments on commit e36ecd3

Please sign in to comment.