Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support for VU meter values #139

Merged
merged 7 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion packages/mixer-connection/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ import { Easings } from 'soundcraft-ui-connection';

Easings.Linear; // no easing
Easings.EaseIn; // acceleration from zero velocity (slow start)
Easings.EaseOut; // deceleration from zero velocity (slow end)
Easings.EaseOut; // deceleration to zero velocity (slow end)
Easings.EaseInOut; // acceleration until halfway, then deceleration (slow start and end)
```

Expand Down Expand Up @@ -503,6 +503,76 @@ The mixer exposes the following information about the device:
| `conn.deviceInfo.model` | Hardware model (`ui12`, `ui16`, `ui24`) as synchronous value |
| `conn.deviceInfo.firmware$` | Firmware version |

## VU Meter for channel volume levels

Volume levels can be consumed through the `VuProcessor` class, fully separated from the `MasterChannel` class.
The VU information is published through streams, separated by channel.
Please be aware that only channels on the master bus are available.

```ts
conn.vuProcessor.input(1);
conn.vuProcessor.aux(2);
```

| Call on `VuProcessor` | Description |
| --------------------- | ---------------- |
| `master()` | Master |
| `input(2)` | Input 2 |
| `line(1)` | Line Input 1 |
| `player(1)` | Player channel 1 |
| `aux(2)` | AUX channel 2 |
| `fx(3)` | FX channel 3 |
| `sub(3)` | Sub group 3 |

Each of the method calls directly returns an Observable that can be subscribed to.
The streams emit objects with different VU values.
They are always published as linear values between `0` and `1`.

```ts
// for input channels
{
vuPre: 0.5; // input level before processing (EQ, Gate, Comp)
vuPost: 0.5; // level after processing, represented by the blue bars in the Web UI
vuPostFader: 0.5; // actual channel output level, represented by the colored bars in the Web UI
}
```

The `vuPre` field is only available on input channels (input, player and line).
Master, FX and Sub groups publish stereo information, so the object is structured as follows:

```ts
// for FX, sub group and master
{
vuPostL: 0.3,
vuPostR: 0.3,
vuPostFaderL: 0.4,
vuPostFaderR: 0.4,
}
```

Processing of the VU information happens lazily: VU messages from the mixer are ignored until the first VU stream is subscribed to. The messages are only processed if VU information is actually consumed.

### All channels

The `vuData$` field on `VuProcessor` publishes a raw object with all channel VU information available.
This can be used to process all information at once, e.g. for a VU meter dashboard across all channels.

```ts
conn.vuProcessor.vuData$;
```

### VU values in dB

All VU values are linear values between `0` and `1`. To express the level in dB you need to project the value to the dB range of the meter (`-80..0 dB`).
The exported utility function `vuValueToDB()` helps with that task and can be used as follows:

```ts
conn.vuProcessor
.master()
.pipe(map(data => vuValueToDB(data.vuPostFaderL)))
.subscribe(/* ... */);
```

## Working with raw messages and state

The `MixerStore` object exposes raw streams with messages and state data. You can use them for debugging purposes or for integration in other services:
Expand Down
6 changes: 5 additions & 1 deletion packages/mixer-connection/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
export * from './lib/soundcraft-ui';
export * from './lib/facade/index';
export * from './lib/types';
export * from './lib/util';
export * from './lib/utils';
export * from './lib/utils/value-converters';
export { Easings } from './lib/utils/transitions/easings';

export * from './lib/vu/vu-processor';
export * from './lib/vu/vu.types';
export { vuValueToDB } from './lib/vu/vu.utils';
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { SoundcraftUI } from '../soundcraft-ui';

describe('Channel Store Singletons', () => {
let conn: SoundcraftUI;

beforeEach(() => {
conn = new SoundcraftUI('0.0.0.0');
});

// Note: comparing class instances with toBe() directly doesn't work because of circular structure errors.
// This is why we used toBe(true) as a workaround.

describe('should create objects only once and retrieve them from the ChannelStore', () => {
it('MasterChannel', () => {
const ch1 = conn.master.player(2);
const ch2 = conn.master.player(2);
expect(ch1 === ch2).toBe(true);
});

it('DelayableMasterChannel', () => {
const ch1 = conn.master.input(10);
const ch2 = conn.master.input(10);
expect(ch1 === ch2).toBe(true);
});

it('AuxChannel', () => {
const ch1 = conn.aux(2).input(4);
const ch2 = conn.aux(2).input(4);
expect(ch1 === ch2).toBe(true);
});

it('FxChannel', () => {
const ch1 = conn.fx(1).input(2);
const ch2 = conn.fx(1).input(2);
expect(ch1 === ch2).toBe(true);
});

it('VolumeBus', () => {
const bus1 = conn.volume.headphone(1);
const bus2 = conn.volume.headphone(1);
expect(bus1 === bus2).toBe(true);
});

it('MuteGroup', () => {
const bus1 = conn.muteGroup(2);
const bus2 = conn.muteGroup(2);
expect(bus1 === bus2).toBe(true);
});

it('HwChannel', () => {
const ch1 = conn.hw(4);
const ch2 = conn.hw(4);
expect(ch1 === ch2).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SoundcraftUI } from '../soundcraft-ui';
import { setMixerModel } from '../util';
import { setMixerModel } from '../test.utils';

describe('Outbound messages', () => {
let conn: SoundcraftUI;
Expand Down
2 changes: 1 addition & 1 deletion packages/mixer-connection/src/lib/facade/aux-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MixerConnection } from '../mixer-connection';
import { MixerStore } from '../state/mixer-store';
import { select, selectPan, selectStereoIndex } from '../state/state-selectors';
import { ChannelType } from '../types';
import { clamp, getLinkedChannelNumber } from '../util';
import { clamp, getLinkedChannelNumber } from '../utils';
import { PannableChannel } from './interfaces';
import { SendChannel } from './send-channel';

Expand Down
2 changes: 1 addition & 1 deletion packages/mixer-connection/src/lib/facade/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '../state/state-selectors';
import { sourcesToTransition, TransitionSource } from '../transitions';
import { BusType, ChannelType } from '../types';
import { clamp, constructReadableChannelName } from '../util';
import { clamp, constructReadableChannelName } from '../utils';
import { resolveDelayed } from '../utils/async-helpers';
import { Easings } from '../utils/transitions/easings';
import { DBToFaderValue, faderValueToDB } from '../utils/value-converters';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { firstValueFrom } from 'rxjs';
import { SoundcraftUI } from '../soundcraft-ui';
import { setMixerModel } from '../util';
import { setMixerModel } from '../test.utils';

describe('DeviceInfo', () => {
let conn: SoundcraftUI;
Expand Down
2 changes: 1 addition & 1 deletion packages/mixer-connection/src/lib/facade/fx-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { map } from 'rxjs';
import { MixerConnection } from '../mixer-connection';
import { MixerStore } from '../state/mixer-store';
import { ChannelType } from '../types';
import { getLinkedChannelNumber } from '../util';
import { getLinkedChannelNumber } from '../utils';
import { SendChannel } from './send-channel';

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { firstValueFrom } from 'rxjs';
import { SoundcraftUI } from '../soundcraft-ui';
import { setMixerModel } from '../util';
import { setMixerModel } from '../test.utils';
import { HwChannel } from './hw-channel';

describe('AUX Channel', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/mixer-connection/src/lib/facade/hw-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { map, switchMap, take } from 'rxjs';
import { MixerConnection } from '../mixer-connection';
import { MixerStore } from '../state/mixer-store';
import { select, selectGain, selectPhantom } from '../state/state-selectors';
import { clamp } from '../util';
import { clamp } from '../utils';
import {
linearMappingRangeToValue,
linearMappingValueToRange,
Expand Down
2 changes: 1 addition & 1 deletion packages/mixer-connection/src/lib/facade/master-bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
selectMasterValue,
} from '../state/state-selectors';
import { sourcesToTransition, TransitionSource } from '../transitions';
import { clamp } from '../util';
import { clamp } from '../utils';
import { resolveDelayed } from '../utils/async-helpers';
import { Easings } from '../utils/transitions/easings';
import { DBToFaderValue, faderValueToDB } from '../utils/value-converters';
Expand Down
2 changes: 1 addition & 1 deletion packages/mixer-connection/src/lib/facade/master-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MixerConnection } from '../mixer-connection';
import { MixerStore } from '../state/mixer-store';
import { select, selectPan, selectRawValue, selectSolo } from '../state/state-selectors';
import { ChannelType } from '../types';
import { clamp, getLinkedChannelNumber } from '../util';
import { clamp, getLinkedChannelNumber } from '../utils';
import { Channel } from './channel';
import { PannableChannel } from './interfaces';
import { AutomixGroupId } from './automix-controller';
Expand Down
2 changes: 1 addition & 1 deletion packages/mixer-connection/src/lib/facade/volume-bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MixerConnection } from '../mixer-connection';
import { MixerStore } from '../state/mixer-store';
import { select, selectVolumeBusValue } from '../state/state-selectors';
import { sourcesToTransition, TransitionSource } from '../transitions';
import { clamp } from '../util';
import { clamp } from '../utils';
import { resolveDelayed } from '../utils/async-helpers';
import { Easings } from '../utils/transitions/easings';
import { DBToFaderValue, faderValueToDB } from '../utils/value-converters';
Expand Down
7 changes: 3 additions & 4 deletions packages/mixer-connection/src/lib/mixer-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ export class MixerConnection {

/**
* internal message streams.
* can be fed from anywhere inside this class
* but must not be exposed
* can be fed from anywhere inside this class but must not be exposed
*/
private statusSubject$ = new Subject<ConnectionEvent>();
private outboundSubject$ = new Subject<string>();
Expand Down Expand Up @@ -142,7 +141,7 @@ export class MixerConnection {
const match = message.match(/^(3:::)([\s\S]*)/);
return match && match[2];
}),
filter((e): e is string => !!e),
filter(e => e !== null),
mergeMap(message => message.split('\n')) // one message can contain multiple lines with commands. split them into single emissions
);

Expand Down Expand Up @@ -226,7 +225,7 @@ export class MixerConnection {

/**
* Send command to the mixer
* @param msg Message to send
* @param msg Message to send, e.g. `SETD^i.2.mute^1`
*/
sendMessage(msg: string) {
this.outboundSubject$.next(msg);
Expand Down
25 changes: 15 additions & 10 deletions packages/mixer-connection/src/lib/soundcraft-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,44 @@ import { ShowController } from './facade/show-controller';
import { VolumeBus } from './facade/volume-bus';
import { MixerConnection } from './mixer-connection';
import { MixerStore } from './state/mixer-store';
import { VuProcessor } from './vu/vu-processor';

export class SoundcraftUI {
readonly conn = new MixerConnection(this.targetIP);
readonly store = new MixerStore(this.conn);

deviceInfo = new DeviceInfo(this.store);
/** Information about hardware and software of the mixer */
readonly deviceInfo = new DeviceInfo(this.store);

/** Connection status */
status$ = this.conn.status$;
readonly status$ = this.conn.status$;

/** VU meter information for master channels */
readonly vuProcessor = new VuProcessor(this.conn);

/** Master bus */
master = new MasterBus(this.conn, this.store);
readonly master = new MasterBus(this.conn, this.store);

/** Media player */
player = new Player(this.conn, this.store);
readonly player = new Player(this.conn, this.store);

/** 2-track recorder */
recorderDualTrack = new DualTrackRecorder(this.conn, this.store);
readonly recorderDualTrack = new DualTrackRecorder(this.conn, this.store);

/** multitrack recorder */
recorderMultiTrack = new MultiTrackRecorder(this.conn, this.store);
readonly recorderMultiTrack = new MultiTrackRecorder(this.conn, this.store);

/** SOLO and Headphone buses */
volume = {
readonly volume = {
solo: new VolumeBus(this.conn, this.store, 'solovol'),
headphone: (id: number) => new VolumeBus(this.conn, this.store, 'hpvol', id),
};

/** Show controller (Shows, Snapshots, Cues) */
shows = new ShowController(this.conn, this.store);
readonly shows = new ShowController(this.conn, this.store);

/** Automix controller */
automix = new AutomixController(this.conn, this.store);
readonly automix = new AutomixController(this.conn, this.store);

constructor(private targetIP: string) {}

Expand All @@ -66,7 +71,7 @@ export class SoundcraftUI {

/**
* Get MUTE group or related groupings (MUTE ALL and MUTE FX)
* @param id ID of the group: 1..6, all, fx
* @param id ID of the group: `1`..`6`, `all`, `fx`
*/
muteGroup(id: MuteGroupID) {
return new MuteGroup(this.conn, this.store, id);
Expand Down
12 changes: 12 additions & 0 deletions packages/mixer-connection/src/lib/state/channel-store.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ChannelStore } from './channel-store';

describe('Channel Store', () => {
it('should store and retrieve an object', () => {
const store = new ChannelStore();
const myObject = { foo: 'bar', bar: 5 };

store.set('myKey', myObject);

expect(store.get('myKey')).toBe(myObject);
});
});
4 changes: 2 additions & 2 deletions packages/mixer-connection/src/lib/state/mixer-store.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { connectable, filter, map, ReplaySubject, scan, share } from 'rxjs';

import { transformStringValue } from '../util';
import { transformStringValue } from '../utils';
import { MixerConnection } from '../mixer-connection';
import { ChannelStore } from './channel-store';

export class MixerStore {
/** Internal filtered stream of matched SETD and SETS messages */
private setdSetsMessageMatches$ = this.conn.allMessages$.pipe(
map(msg => msg.match(/(SETD|SETS)\^([a-zA-Z0-9.]+)\^(.*)/)),
filter((e): e is RegExpMatchArray => !!e),
filter(e => e !== null),
share()
);

Expand Down
7 changes: 7 additions & 0 deletions packages/mixer-connection/src/lib/test.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SoundcraftUI } from './soundcraft-ui';
import { MixerModel } from './types';

/** Manually set mixer model for testing */
export function setMixerModel(model: MixerModel, conn: SoundcraftUI) {
conn.conn.sendMessage('SETD^model^' + model);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { clamp, playerTimeToString, transformStringValue } from './util';
import { clamp, playerTimeToString, transformStringValue } from './utils';

describe('util', () => {
describe('utils', () => {
describe('clamp', () => {
it('should leave in-range values unchanged', () => {
expect(clamp(400, 0, 500)).toBe(400);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,3 @@ export function constructReadableChannelName(type: ChannelType, channel: number)
return 'PLAYER ' + numberToLR(channel);
}
}

/** Manually set mixer model. Used for testing only! */
export function setMixerModel(model: MixerModel, conn: SoundcraftUI) {
conn.conn.sendMessage('SETD^model^' + model);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clamp } from '../../util';
import { clamp } from '../../utils';
import { dBLinearLUT } from './db-lut';

/**
Expand Down
Loading
Loading