diff --git a/src/AudioWorker.ts b/src/AudioWorker.ts index dc64256..d192d53 100644 --- a/src/AudioWorker.ts +++ b/src/AudioWorker.ts @@ -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 }); }); @@ -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; +}); diff --git a/src/export/ExportManager.ts b/src/export/ExportManager.ts index 4c68817..fb9cbd0 100644 --- a/src/export/ExportManager.ts +++ b/src/export/ExportManager.ts @@ -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() { diff --git a/src/export/wave/WaveFile.ts b/src/export/wave/WaveFile.ts index c8deb84..32747cc 100644 --- a/src/export/wave/WaveFile.ts +++ b/src/export/wave/WaveFile.ts @@ -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); diff --git a/src/export/wavePerChannel/TarFile.ts b/src/export/wavePerChannel/TarFile.ts new file mode 100644 index 0000000..8c38ab8 --- /dev/null +++ b/src/export/wavePerChannel/TarFile.ts @@ -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); + } +} diff --git a/src/export/wavePerChannel/WavePerChannelExporter.ts b/src/export/wavePerChannel/WavePerChannelExporter.ts new file mode 100644 index 0000000..be00c3f --- /dev/null +++ b/src/export/wavePerChannel/WavePerChannelExporter.ts @@ -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; + } +} \ No newline at end of file