Share the parts the sound devices and timelines repeated (#85)
Round 12 of the clean-up. Duplicate code had grown from 33 clones (249 lines) after round 11 to 140 clones (1263 lines), almost all of it in the panels and faces of the sound FX devices (#379 to #401). It is down to 57 clones (416 lines) now, and src is 816 lines shorter. - deviceParts.ts holds what the device panels built by hand: the head, rows of knobs, the screen style and the row layout for groups of knobs. Twelve panels use it. panel.knobs stays because the debug hooks read it. - DeviceFace is the base of the twelve faces. It finds the node and does wired, preview, commit, set, hide, reload and audition. compressorWired lives there as paramWired. - TextPool replaces ten copies of "make texts as needed, hide the rest" in the wave and video tabs, the rulers, the piano roll and the song and composition timelines. clipStrip.ts is the clip label strip the wave and video tabs share. - core/util/bytes.ts has the concat that ogg, matroska and avcColour each wrote out. - Dead code is gone: clear on the video players (never called since #402), posterFileOf and videoTimeAt (only tests called them) and easeOut (the same as glide(t, 3)). Values only used in their own file lose their export. - Comments over 125 columns are wrapped, and comments that told the history ("as before", "used to") say what is, in src and in the e2e modules. - Left alone on purpose: AnimationEditor and CompositionEditor share only the play button, and the curve panels are built differently. - Chip now places its boxes through put, which rounds. They were whole numbers already. The three row layouts are byte-identical before and after. - README lists the video and composition assets and the Videos and Compositions pages. Tests: - Gate (melektaus-atx): CHECK EXIT 0, 313 files, 4835 tests. - e2e 25, 34, 64, 65, 66, 69 (melektaus-atx): ALL PASSED, 330 checks. - TextPool without keeping its texts and framesToRoom without making room both go red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
df6dbe97c5
commit
0701fe3856
94 files changed
+1602
-2380
No files matched your search
@@ -86,9 +86,9 @@ export function addCompositionItem(
|
||||
|
||||
/**
|
||||
* Copied items put back into a composition: their objects where the copied ones stood, the items
|
||||
* timed as `pastedItems` says, the clips that came with them moved by as much – all moved on together
|
||||
* where one of those clips would lie over a clip already there. Null when the copy holds anything but items – a scene's objects have
|
||||
* no time to land at. `itemIds` are the new items', to pick them afterwards.
|
||||
* and the clips that came with them as `pastedItemsWithClips` places them. Null when the copy holds
|
||||
* anything but items – a scene's objects have no time to land at. `itemIds` are the new items', to
|
||||
* pick them afterwards.
|
||||
*/
|
||||
export function pasteCompositionItems(
|
||||
project: Project,
|
||||
|
||||
@@ -106,7 +106,7 @@ export interface SpriteChange {
|
||||
* container becomes a project asset. Otherwise it stays with its composition while that uses it, else
|
||||
* goes to the first composition that does; every other composition using it gets a copy of its own.
|
||||
*/
|
||||
export function settleCompositionSprites(project: Project): SpriteChange {
|
||||
function settleCompositionSprites(project: Project): SpriteChange {
|
||||
const change: SpriteChange = { added: [], retargets: [], owners: [], dropped: new Set() };
|
||||
const sprites = project.assets.filter(isCompositionSprite);
|
||||
if (sprites.length === 0) return change;
|
||||
|
||||
@@ -375,9 +375,6 @@ export const ACID_ROWS = {
|
||||
glide: chipRow('glide', 'Glide (s)', 0, 0.5, 0.001, 0.05),
|
||||
} satisfies Record<string, FlatDef<'number'>>;
|
||||
|
||||
/** The numbers an Acid takes as its note plays: the rest it fixes when the note starts. */
|
||||
export const ACID_MOVING = ['cutoff', 'resonance', 'envMod'] as const;
|
||||
|
||||
export const InstrumentAcid: InstrumentNodeDef = {
|
||||
type: INSTRUMENT_ACID,
|
||||
label: 'Acid',
|
||||
|
||||
@@ -98,7 +98,10 @@ export function itemSounds(project: Project, comp: CompositionAsset, item: Compo
|
||||
return item.audio !== undefined || itemVideo(project, comp, item) !== null;
|
||||
}
|
||||
|
||||
/** The fades of an item in whole frames as they work: the fade in no longer than the item, the fade out no longer than the rest. */
|
||||
/**
|
||||
* The fades of an item in whole frames as they work: the fade in no longer than the item, the fade
|
||||
* out no longer than the rest.
|
||||
*/
|
||||
export interface ItemFades {
|
||||
fadeIn: number;
|
||||
fadeOut: number;
|
||||
@@ -333,7 +336,7 @@ export function compositionHolder(project: Project, comp: CompositionAsset, fram
|
||||
}
|
||||
|
||||
/** How far ahead of where a composition stands the items about to begin are written out, in seconds. */
|
||||
export const AHEAD_SECONDS = 1;
|
||||
const AHEAD_SECONDS = 1;
|
||||
|
||||
/**
|
||||
* The items that begin within `seconds` after `frame` – once it loops, those at the start too when
|
||||
|
||||
@@ -205,23 +205,8 @@ export function movedClips(
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied items under the ids `ids` gives them, moved so the earliest starts at `at`: on their tracks,
|
||||
* or, given `row`, the topmost on that track and the others as far below as they were, as far as
|
||||
* there are tracks – a song's paste. An item whose track is gone counts as on the first.
|
||||
*/
|
||||
export function pastedItems(
|
||||
items: readonly CompositionItem[],
|
||||
ids: ReadonlyMap<string, string>,
|
||||
at: number,
|
||||
trackIds: readonly string[],
|
||||
row?: number,
|
||||
): CompositionItem[] {
|
||||
return pasted(items, ids, at, trackIds, row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied clips placed as `pastedItems` places items, then moved on frame by frame as long as one
|
||||
* would lie over a clip of `clips` or over another of the copy – clips of a track never overlap.
|
||||
* Copied clips placed as `placing` places what is pasted, then moved on frame by frame as long as
|
||||
* one would lie over a clip of `clips` or over another of the copy – clips of a track never overlap.
|
||||
*/
|
||||
export function pastedClips(
|
||||
copied: readonly CompositionClip[],
|
||||
@@ -232,19 +217,15 @@ export function pastedClips(
|
||||
lengthOf: (clip: CompositionClip) => number,
|
||||
row?: number,
|
||||
): CompositionClip[] {
|
||||
const placed = pasted(copied, ids, at, trackIds, row);
|
||||
if (placed.length === 0) return [];
|
||||
// Past the end of every clip there is room, so the search ends there at the latest.
|
||||
const last = Math.max(0, ...clips.map((c) => c.start + Math.max(1, lengthOf(c))));
|
||||
for (let by = 0; ; by++) {
|
||||
const moved = placed.map((c) => ({ ...c, start: c.start + by }));
|
||||
if (apart([...clips, ...moved], lengthOf) || moved.every((c) => c.start >= last)) return moved;
|
||||
}
|
||||
const place = placing(copied, at, trackIds, row);
|
||||
const placed = copied.map((c) => place(c, ids));
|
||||
const by = framesToRoom(placed, clips, lengthOf);
|
||||
return placed.map((c) => ({ ...c, start: c.start + by }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied items placed as `pastedItems` places them, the copied clips that came with them moved by
|
||||
* as much; then both moved on frame by frame as long as one of the clips would lie over a clip of
|
||||
* Copied items placed as `placing` places them, the copied clips that came with them moved by as
|
||||
* much; then both moved on frame by frame as long as one of the clips would lie over a clip of
|
||||
* `clips` – clips of a track never overlap.
|
||||
*/
|
||||
export function pastedItemsWithClips(
|
||||
@@ -260,26 +241,32 @@ export function pastedItemsWithClips(
|
||||
const place = placing(items, at, trackIds, row);
|
||||
const placedItems = items.map((i) => place(i, ids));
|
||||
const placedClips = copied.map((c) => place(c, ids));
|
||||
const by = framesToRoom(placedClips, clips, lengthOf);
|
||||
return {
|
||||
items: placedItems.map((i) => ({ ...i, start: i.start + by })),
|
||||
clips: placedClips.map((c) => ({ ...c, start: c.start + by })),
|
||||
};
|
||||
}
|
||||
|
||||
/** The frames `placed` moves on until none of it lies over a clip of `clips` or over another of it. */
|
||||
function framesToRoom(
|
||||
placed: readonly CompositionClip[],
|
||||
clips: readonly CompositionClip[],
|
||||
lengthOf: (clip: CompositionClip) => number,
|
||||
): number {
|
||||
// Past the end of every clip there is room, so the search ends there at the latest.
|
||||
const last = Math.max(0, ...clips.map((c) => c.start + Math.max(1, lengthOf(c))));
|
||||
for (let by = 0; ; by++) {
|
||||
const moved = placedClips.map((c) => ({ ...c, start: c.start + by }));
|
||||
if (apart([...clips, ...moved], lengthOf) || moved.every((c) => c.start >= last))
|
||||
return { items: placedItems.map((i) => ({ ...i, start: i.start + by })), clips: moved };
|
||||
const moved = placed.map((c) => ({ ...c, start: c.start + by }));
|
||||
if (apart([...clips, ...moved], lengthOf) || moved.every((c) => c.start >= last)) return by;
|
||||
}
|
||||
}
|
||||
|
||||
function pasted<T extends Placed>(
|
||||
items: readonly T[],
|
||||
ids: ReadonlyMap<string, string>,
|
||||
at: number,
|
||||
trackIds: readonly string[],
|
||||
row?: number,
|
||||
): T[] {
|
||||
const place = placing(items, at, trackIds, row);
|
||||
return items.map((i) => place(i, ids));
|
||||
}
|
||||
|
||||
/** How a paste moves what was copied: the earliest of `items` to `at`, their rows as `pastedItems` says. */
|
||||
/**
|
||||
* How a paste moves what was copied, under the ids `ids` gives it: the earliest of `items` to `at`,
|
||||
* on their tracks, or, given `row`, the topmost on that track and the others as far below as they
|
||||
* were, as far as there are tracks – a song's paste. One whose track is gone counts as on the first.
|
||||
*/
|
||||
function placing(
|
||||
items: readonly Placed[],
|
||||
at: number,
|
||||
|
||||
@@ -64,7 +64,7 @@ const UNNAMING: ReadonlySet<AssetType> = new Set(['sound', 'texture', 'pixelart'
|
||||
* Every text the project holds outside the assets that cannot name a sound. Where a sound is played
|
||||
* whole – a sound field, an item of a song, a Sample, a Reverb's impulse – its id is one of them; a
|
||||
* clip has an id of its own. Looked at as data, not field by field, so no place is missed: one that
|
||||
* only looks like a reference loads the file as before.
|
||||
* only looks like a reference just loads a file nothing plays.
|
||||
*/
|
||||
function namedStrings(project: Project): Set<string> {
|
||||
const seen = new Set<string>();
|
||||
|
||||
@@ -17,11 +17,6 @@ export function isVideoFile(name: string): boolean {
|
||||
return FILE_PATTERN.test(name);
|
||||
}
|
||||
|
||||
/** The still the worker cut from a video (#402), lying beside it. */
|
||||
export function posterFileOf(file: string): string {
|
||||
return file.replace(/\.mp4$/, '.jpg');
|
||||
}
|
||||
|
||||
/** What a texture field names when it names a video: the whole video, or one of its clips. */
|
||||
export interface VideoRef {
|
||||
asset: VideoAsset;
|
||||
@@ -193,12 +188,3 @@ export function drivenTimeOf(
|
||||
if (play.loop) return play.start + (run % length);
|
||||
return play.start + Math.min(run, Math.max(0, length - LAST_PICTURE));
|
||||
}
|
||||
|
||||
/** Where in its file a player is after `elapsed` seconds of playing (#402). */
|
||||
export function videoTimeAt(play: Pick<VideoPlay, 'start' | 'end' | 'loop'>, elapsed: number): number {
|
||||
const length = play.end - play.start;
|
||||
const run = Math.max(0, elapsed);
|
||||
if (!Number.isFinite(length) || length <= 0) return play.start + run;
|
||||
if (!play.loop) return play.start + Math.min(run, length);
|
||||
return play.start + (run % length);
|
||||
}
|
||||
+5
-14
@@ -4,6 +4,7 @@
|
||||
* (`preSkip`) and the last page how many there are, so a decoder gives back exactly what went in,
|
||||
* to the sample.
|
||||
*/
|
||||
import { concatBytes } from '../util/bytes';
|
||||
|
||||
/** Opus always counts at 48 kHz, whatever the rate of what went in. */
|
||||
export const OPUS_RATE = 48000;
|
||||
@@ -101,7 +102,7 @@ export function oggOpus(stream: OpusStream): Uint8Array {
|
||||
if (i === stream.packets.length - 1) page(held, end, 0x04);
|
||||
});
|
||||
if (stream.packets.length === 0) page([], end, 0x04);
|
||||
return concat(pages);
|
||||
return concatBytes(pages);
|
||||
}
|
||||
|
||||
function oggPage(
|
||||
@@ -115,7 +116,7 @@ function oggPage(
|
||||
for (let left = packet.length; left >= 255; left -= 255) lacing.push(255);
|
||||
lacing.push(packet.length % 255);
|
||||
}
|
||||
const body = concat(packets);
|
||||
const body = concatBytes(packets);
|
||||
const page = new Uint8Array(27 + lacing.length + body.length);
|
||||
const view = new DataView(page.buffer);
|
||||
page.set(ascii('OggS'));
|
||||
@@ -179,7 +180,7 @@ export class OggOpusReader {
|
||||
private next = 0;
|
||||
|
||||
push(bytes: Uint8Array): void {
|
||||
let data = this.held.length > 0 ? concat([this.held, bytes]) : bytes;
|
||||
let data = this.held.length > 0 ? concatBytes([this.held, bytes]) : bytes;
|
||||
let at = 0;
|
||||
while (data.length - at >= 27) {
|
||||
if (text(data.subarray(at, at + 4)) !== 'OggS') throw new Error(`no page at byte ${at}`);
|
||||
@@ -209,7 +210,7 @@ export class OggOpusReader {
|
||||
this.open.push(page.subarray(from, from + value));
|
||||
from += value;
|
||||
if (value < 255) {
|
||||
this.packet(concat(this.open));
|
||||
this.packet(concatBytes(this.open));
|
||||
this.open = [];
|
||||
}
|
||||
}
|
||||
@@ -254,13 +255,3 @@ function ascii(value: string): Uint8Array {
|
||||
function text(bytes: Uint8Array): string {
|
||||
return String.fromCharCode(...bytes);
|
||||
}
|
||||
|
||||
function concat(parts: readonly Uint8Array[]): Uint8Array {
|
||||
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
||||
let at = 0;
|
||||
for (const part of parts) {
|
||||
out.set(part, at);
|
||||
at += part.length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* The master routing of a project (#351): a graph with a node per sound, instrument, pattern, song,
|
||||
* video with sound, composition and bus, and the master, its links the sends from one to the next. Its nodes follow the assets: one
|
||||
* that comes gets a node of its own and the links most others of its type have, one that goes takes
|
||||
* its node along.
|
||||
* video with sound, composition and bus, and the master, its links the sends from one to the next.
|
||||
* Its nodes follow the assets: one that comes gets a node of its own and the links most others of
|
||||
* its type have, one that goes takes its node along.
|
||||
*/
|
||||
import { createEmptyGraph, type Graph, type GraphEdge, type GraphNode } from '../graph/Graph';
|
||||
import { newId } from '../ids';
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Sounds streamed rather than decoded whole (#349). A sound with `stream` on is played from a copy as
|
||||
* Opus in Ogg, made in the browser. Every clip of every sound has a copy of its own cut, so no clip
|
||||
* needs the whole file: streamed where its sound streams, decoded where it does not. A bake (#340) is
|
||||
* Opus already and always streams. What a game plays is still named as before – the file, or the cut
|
||||
* a clip is – and the tables here say which of those names to play from which copy.
|
||||
* Opus already and always streams. A game names what it plays by the file, or by the cut a clip is,
|
||||
* and the tables here say which of those names to play from which copy.
|
||||
*/
|
||||
import type { SoundAsset, SoundClip, SoundStream } from '../model/Asset';
|
||||
import type { Project } from '../model/Project';
|
||||
|
||||
@@ -140,9 +140,10 @@ import { Theme } from '../../ui';
|
||||
* produces fractions, and the field rounds the value away rather than hiding it. Without
|
||||
* `assetTypes` an asset picker offers every placeable asset; `none` is what it calls an empty pick,
|
||||
* `master` offers the master bus as a choice of its own, `noFx` "None" as one where empty means
|
||||
* "from the asset" (#352); `uploadedOnly` keeps drawn pictures out
|
||||
* of a texture picker (#186), `moving` lets videos and their clips in (#402). `renderLayers` makes a layer row pick from the project's render layers
|
||||
* instead of its collision layers (#302), with "Inherit" as the first choice of a single layer.
|
||||
* "from the asset" (#352); `uploadedOnly` keeps drawn pictures out of a texture picker (#186),
|
||||
* `moving` lets videos and their clips in (#402). `renderLayers` makes a layer row pick from the
|
||||
* project's render layers instead of its collision layers (#302), with "Inherit" as the first
|
||||
* choice of a single layer.
|
||||
*/
|
||||
type Extras<K extends FlatKind> = K extends 'number'
|
||||
? { min?: number; max?: number; step?: number; decimals?: number }
|
||||
|
||||
@@ -52,7 +52,7 @@ export function textSize(props: PropertyBag, font = ''): { w: number; h: number
|
||||
}
|
||||
|
||||
/** Estimated from the content and font size, without measuring the text. */
|
||||
export function estimatedTextSize(props: PropertyBag): { w: number; h: number } {
|
||||
function estimatedTextSize(props: PropertyBag): { w: number; h: number } {
|
||||
const size = Math.max(1, Number(props.fontSize ?? TEXT_DEFAULTS.fontSize));
|
||||
const lines = textOf(props.text ?? TEXT_DEFAULTS.text).split('\n');
|
||||
const longest = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
||||
|
||||
@@ -22,7 +22,10 @@ export const VIDEO_ROWS: readonly PropertyDef[] = [
|
||||
*/
|
||||
export const VIDEO_CLOCK_KEY = 'videoClock';
|
||||
export const VIDEO_TIME_KEY = 'videoTime';
|
||||
/** How loud a driven video is heard through this sprite, 0–1, on top of the video's own Volume; a composition's fades set it. */
|
||||
/**
|
||||
* How loud a driven video is heard through this sprite, 0–1, on top of the video's own Volume; a
|
||||
* composition's fades set it.
|
||||
*/
|
||||
export const VIDEO_LEVEL_KEY = 'videoVolume';
|
||||
/**
|
||||
* The bus of the mix the video's sound goes into through this sprite: empty along the routing of the
|
||||
@@ -30,7 +33,7 @@ export const VIDEO_LEVEL_KEY = 'videoVolume';
|
||||
*/
|
||||
export const VIDEO_BUS_KEY = 'videoBus';
|
||||
|
||||
export const VIDEO_CLOCK_OPTIONS = [
|
||||
const VIDEO_CLOCK_OPTIONS = [
|
||||
{ value: 'free', label: 'Free' },
|
||||
{ value: 'driven', label: 'Driven' },
|
||||
];
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* Only the SPS changes; the frames stay as they were. With `avc: {format: 'avc'}` the SPS is in the
|
||||
* encoder's `description` (the `avcC`) alone, so that is all there is to rewrite.
|
||||
*/
|
||||
import { concatBytes } from '../util/bytes';
|
||||
import type { MatroskaColour } from './matroska';
|
||||
|
||||
/** What `video_format` says when the SPS had none: "unspecified". */
|
||||
@@ -34,7 +35,7 @@ export function avcConfigWithColour(config: Uint8Array, colour: MatroskaColour):
|
||||
at += 2 + length;
|
||||
}
|
||||
parts.push(config.subarray(at));
|
||||
return concat(parts);
|
||||
return concatBytes(parts);
|
||||
}
|
||||
|
||||
/** The SPS of an `avcC`, as NAL units with their header byte. */
|
||||
@@ -92,7 +93,7 @@ export function spsWithColour(nal: Uint8Array, colour: MatroskaColour): Uint8Arr
|
||||
// No chroma location, timing, HRD, picture structure or bitstream restriction.
|
||||
else out.bits(0, 6);
|
||||
out.bit(1);
|
||||
return concat([nal.subarray(0, 1), escape(out.finish())]);
|
||||
return concatBytes([nal.subarray(0, 1), escape(out.finish())]);
|
||||
}
|
||||
|
||||
/** What the `video_signal_type` of an SPS says; null where it has none. */
|
||||
@@ -267,13 +268,3 @@ class BitWriter {
|
||||
return Uint8Array.from(this.out);
|
||||
}
|
||||
}
|
||||
|
||||
function concat(parts: readonly Uint8Array[]): Uint8Array {
|
||||
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
||||
let at = 0;
|
||||
for (const part of parts) {
|
||||
out.set(part, at);
|
||||
at += part.length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export const EDITOR_RECORDING_RESOLUTIONS: readonly {
|
||||
{ value: '4k', label: '4K', box: { w: 3840, h: 2160 } },
|
||||
];
|
||||
|
||||
export const EDITOR_RECORDING_DEFAULTS: EditorRecordingSettings = {
|
||||
const EDITOR_RECORDING_DEFAULTS: EditorRecordingSettings = {
|
||||
name: '',
|
||||
resolution: 'screen',
|
||||
fps: 60,
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
* length – so the clusters can leave the writer long before.
|
||||
*/
|
||||
import { OPUS_RATE } from '../music/ogg';
|
||||
import { concatBytes } from '../util/bytes';
|
||||
|
||||
export const AVC_CODEC = 'V_MPEG4/ISO/AVC';
|
||||
export const VP9_CODEC = 'V_VP9';
|
||||
export const OPUS_CODEC = 'A_OPUS';
|
||||
const OPUS_CODEC = 'A_OPUS';
|
||||
|
||||
/** Milliseconds per tick of a block's time: the usual 1 ms, so a cluster spans 32 s at most. */
|
||||
const TIMESTAMP_SCALE = 1_000_000;
|
||||
@@ -225,7 +226,7 @@ export class MatroskaWriter {
|
||||
header[0] = 0x80 | block.track;
|
||||
new DataView(header.buffer).setInt16(1, block.ms - cluster.ms);
|
||||
header[3] = block.key ? 0x80 : 0;
|
||||
const element = concat([
|
||||
const element = concatBytes([
|
||||
idBytes(ID.simpleBlock),
|
||||
sizeBytes(header.length + block.data.length),
|
||||
header,
|
||||
@@ -301,7 +302,7 @@ function headOf(head: MatroskaHead, clusterBytes: number): Uint8Array {
|
||||
);
|
||||
const tracks = master(ID.tracks, entries);
|
||||
const segmentSize = info.length + tracks.length + clusterBytes;
|
||||
return concat([ebml, idBytes(ID.segment), sizeBytes(segmentSize, 8), info, tracks]);
|
||||
return concatBytes([ebml, idBytes(ID.segment), sizeBytes(segmentSize, 8), info, tracks]);
|
||||
}
|
||||
|
||||
/** An element id as it is written: its own marker bits are part of the number. */
|
||||
@@ -332,11 +333,11 @@ function sizeLength(size: number): number {
|
||||
}
|
||||
|
||||
function element(id: number, data: Uint8Array): Uint8Array {
|
||||
return concat([idBytes(id), sizeBytes(data.length), data]);
|
||||
return concatBytes([idBytes(id), sizeBytes(data.length), data]);
|
||||
}
|
||||
|
||||
function master(id: number, children: readonly Uint8Array[]): Uint8Array {
|
||||
return element(id, concat(children));
|
||||
return element(id, concatBytes(children));
|
||||
}
|
||||
|
||||
function uint(id: number, value: number): Uint8Array {
|
||||
@@ -362,13 +363,3 @@ function text(id: number, value: string): Uint8Array {
|
||||
function binary(id: number, value: Uint8Array): Uint8Array {
|
||||
return element(id, value);
|
||||
}
|
||||
|
||||
function concat(parts: readonly Uint8Array[]): Uint8Array {
|
||||
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
||||
let at = 0;
|
||||
for (const part of parts) {
|
||||
out.set(part, at);
|
||||
at += part.length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -290,7 +290,7 @@ const PACE_SETTLE_MS = 1000;
|
||||
/** How much of its time a recording is judged over at once. */
|
||||
const PACE_SPAN_MS = 2000;
|
||||
/** The share of frames a recording may miss over a span before it is said to fall behind. */
|
||||
export const PACE_MOST_MISSED_WHILE = 0.1;
|
||||
const PACE_MOST_MISSED_WHILE = 0.1;
|
||||
/** The share of frames a whole recording may miss, once settled, before its end says so. */
|
||||
const PACE_MOST_MISSED = 0.05;
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ export interface AcidKnobs {
|
||||
const LEAST_Q_DB = 0.5;
|
||||
const MOST_Q_DB = 17.5;
|
||||
/** Octaves the envelope opens the filter by at full env mod, and the highest it opens it to. */
|
||||
export const ENV_MOD_OCTAVES = 5;
|
||||
export const TOP_HZ = 18000;
|
||||
const ENV_MOD_OCTAVES = 5;
|
||||
const TOP_HZ = 18000;
|
||||
/** How much of the sweep the sustain holds at full: at sustain 1 the cutoff stays 60 % of the way up. */
|
||||
export const SUSTAIN_SWEEP = 0.6;
|
||||
/**
|
||||
@@ -216,7 +216,10 @@ function blep(p: number, step: number): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** How loud the low-pass passes `hz` with its cutoff at `fc` and its resonance at `resonance`, in dB: what a device draws. */
|
||||
/**
|
||||
* How loud the low-pass passes `hz` with its cutoff at `fc` and its resonance at `resonance`, in dB:
|
||||
* what a device draws.
|
||||
*/
|
||||
export function acidResponseDb(hz: number, fc: number, resonance: number): number {
|
||||
const w = hz / Math.max(LOWEST_HZ, fc);
|
||||
const q = acidQ(resonance);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/** The parts one after another, in one array. */
|
||||
export function concatBytes(parts: readonly Uint8Array[]): Uint8Array {
|
||||
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
||||
let at = 0;
|
||||
for (const part of parts) {
|
||||
out.set(part, at);
|
||||
at += part.length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in new issue
Block a user