Skip to content

Commit

Permalink
chore: refactoring media pool format conversions
Browse files Browse the repository at this point in the history
  • Loading branch information
Julusian committed Aug 15, 2024
1 parent 46c58fd commit 5e62374
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 290 deletions.
8 changes: 5 additions & 3 deletions src/atem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { InputChannel } from './state/input'
import { DownstreamKeyerGeneral, DownstreamKeyerMask } from './state/video/downstreamKeyers'
import * as DT from './dataTransfer'
import * as Util from './lib/atemUtil'
import { getVideoModeInfo } from './lib/videoMode'
import * as Enums from './enums'
import {
ClassicAudioMonitorChannel,
Expand Down Expand Up @@ -54,6 +55,7 @@ import { TimeCommand } from './commands'
import { TimeInfo } from './state/info'
import { SomeAtemAudioLevels } from './state/levels'
import { generateUploadBufferInfo, UploadBufferInfo } from './dataTransfer/dataTransferUploadBuffer'
import { convertWAVToRaw } from './lib/converters/wavAudio'

export interface AtemOptions {
address?: string
Expand Down Expand Up @@ -776,7 +778,7 @@ export class Atem extends BasicAtem {
options?: DT.UploadStillEncodingOptions
): Promise<void> {
if (!this.state) throw new Error('Unable to check current resolution')
const resolution = Util.getVideoModeInfo(this.state.settings.videoMode)
const resolution = getVideoModeInfo(this.state.settings.videoMode)
if (!resolution) throw new Error('Failed to determine required resolution')

const encodedData = generateUploadBufferInfo(data, resolution, !options?.disableRLE)
Expand All @@ -803,7 +805,7 @@ export class Atem extends BasicAtem {
options?: DT.UploadStillEncodingOptions
): Promise<void> {
if (!this.state) throw new Error('Unable to check current resolution')
const resolution = Util.getVideoModeInfo(this.state.settings.videoMode)
const resolution = getVideoModeInfo(this.state.settings.videoMode)
if (!resolution) throw new Error('Failed to determine required resolution')

const provideFrame = async function* (): AsyncGenerator<UploadBufferInfo> {
Expand All @@ -822,7 +824,7 @@ export class Atem extends BasicAtem {
* @returns Promise which resolves once the clip audio is uploaded
*/
public async uploadAudio(index: number, data: Buffer, name: string): Promise<void> {
return this.dataTransferManager.uploadAudio(index, Util.convertWAVToRaw(data, this.state?.info?.model), name)
return this.dataTransferManager.uploadAudio(index, convertWAVToRaw(data, this.state?.info?.model), name)
}

public async setClassicAudioMixerInputProps(
Expand Down
16 changes: 9 additions & 7 deletions src/dataTransfer/dataTransferUploadBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
import * as crypto from 'crypto'
import { DataTransfer, ProgressTransferResult, DataTransferState } from './dataTransfer'
import debug0 = require('debug')
import * as Util from '../lib/atemUtil'
import { VideoModeInfo } from '../lib/videoMode'
import { convertRGBAToYUV422 } from '../lib/converters/rgbaToYuv422'
import { RLE_HEADER, encodeRLE } from '../lib/converters/rle'

const debug = debug0('atem-connection:data-transfer:upload-buffer')

Expand Down Expand Up @@ -41,18 +43,18 @@ export function generateHashForBuffer(data: Buffer): string {

export function generateUploadBufferInfo(
data: Buffer | UploadBufferInfo,
resolution: Util.VideoModeInfo,
resolution: VideoModeInfo,
shouldEncodeRLE: boolean
): UploadBufferInfo {
const expectedLength = resolution.width * resolution.height * 4
if (Buffer.isBuffer(data)) {
if (data.length !== expectedLength)
throw new Error(`Pixel buffer has incorrect length. Received ${data.length} expected ${expectedLength}`)

const encodedData = Util.convertRGBAToYUV422(resolution.width, resolution.height, data)
const encodedData = convertRGBAToYUV422(resolution.width, resolution.height, data)

return {
encodedData: shouldEncodeRLE ? Util.encodeRLE(encodedData) : encodedData,
encodedData: shouldEncodeRLE ? encodeRLE(encodedData) : encodedData,
rawDataLength: encodedData.length,
isRleEncoded: shouldEncodeRLE,
hash: generateHashForBuffer(encodedData),
Expand All @@ -66,7 +68,7 @@ export function generateUploadBufferInfo(

if (shouldEncodeRLE && !data.isRleEncoded) {
data.isRleEncoded = true
data.encodedData = Util.encodeRLE(data.encodedData)
data.encodedData = encodeRLE(data.encodedData)
}

return result
Expand Down Expand Up @@ -166,10 +168,10 @@ export abstract class DataTransferUploadBuffer extends DataTransfer<void> {
if (chunkSize + this.#bytesSent > this.data.length) {
// The last chunk can't end with a RLE header
shortenBy = this.#bytesSent + chunkSize - this.data.length
} else if (Util.RLE_HEADER === this.data.readBigUint64BE(this.#bytesSent + chunkSize - 8)) {
} else if (RLE_HEADER === this.data.readBigUint64BE(this.#bytesSent + chunkSize - 8)) {
// RLE header starts 8 bytes before the end
shortenBy = 8
} else if (Util.RLE_HEADER === this.data.readBigUint64BE(this.#bytesSent + chunkSize - 16)) {
} else if (RLE_HEADER === this.data.readBigUint64BE(this.#bytesSent + chunkSize - 16)) {
// RLE header starts 16 bytes before the end
shortenBy = 16
}
Expand Down
277 changes: 0 additions & 277 deletions src/lib/atemUtil.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import * as Enums from '../enums'
import WaveFile = require('wavefile')
import type { IDeserializedCommand, ISerializableCommand } from '../commands'

export function bufToBase64String(buffer: Buffer, start: number, length: number): string {
Expand All @@ -12,281 +10,6 @@ export function bufToNullTerminatedString(buffer: Buffer, start: number, length:
return slice.toString('utf8', 0, nullIndex < 0 ? slice.length : nullIndex)
}

/**
* @todo: MINT - 2018-5-24:
* Create util functions that handle proper colour spaces in UHD.
*/
export function convertRGBAToYUV422(width: number, height: number, data: Buffer): Buffer {
// BT.709 or BT.601
const KR = height >= 720 ? 0.2126 : 0.299
const KB = height >= 720 ? 0.0722 : 0.114
const KG = 1 - KR - KB

const KRi = 1 - KR
const KBi = 1 - KB

const YRange = 219
const CbCrRange = 224
const HalfCbCrRange = CbCrRange / 2

const YOffset = 16 << 8
const CbCrOffset = 128 << 8

const KRoKBi = (KR / KBi) * HalfCbCrRange
const KGoKBi = (KG / KBi) * HalfCbCrRange
const KBoKRi = (KB / KRi) * HalfCbCrRange
const KGoKRi = (KG / KRi) * HalfCbCrRange

const genColor = (rawA: number, uv16: number, y16: number): number => {
const a = ((rawA << 2) * 219) / 255 + (16 << 2)
const y = Math.round(y16) >> 6
const uv = Math.round(uv16) >> 6

return (a << 20) + (uv << 10) + y
}

const buffer = Buffer.alloc(width * height * 4)
for (let i = 0; i < width * height * 4; i += 8) {
const r1 = data[i + 0]
const g1 = data[i + 1]
const b1 = data[i + 2]

const r2 = data[i + 4]
const g2 = data[i + 5]
const b2 = data[i + 6]

const a1 = data[i + 3]
const a2 = data[i + 7]

const y16a = YOffset + KR * YRange * r1 + KG * YRange * g1 + KB * YRange * b1
const cb16 = CbCrOffset + (-KRoKBi * r1 - KGoKBi * g1 + HalfCbCrRange * b1)
const y16b = YOffset + KR * YRange * r2 + KG * YRange * g2 + KB * YRange * b2
const cr16 = CbCrOffset + (HalfCbCrRange * r1 - KGoKRi * g1 - KBoKRi * b1)

buffer.writeUInt32BE(genColor(a1, cb16, y16a), i)
buffer.writeUInt32BE(genColor(a2, cr16, y16b), i + 4)
}
return buffer
}

export const RLE_HEADER = 0xfefefefefefefefen

export function encodeRLE(data: Buffer): Buffer {
const result = Buffer.alloc(data.length)
let lastBlock = data.readBigUInt64BE()
let identicalCount = 0
let differentCount = 0
let resultOffset = -8

for (let sourceOffset = 8; sourceOffset < data.length; sourceOffset += 8) {
const block = data.readBigUInt64BE(sourceOffset)

if (block === lastBlock) {
++identicalCount
if (differentCount) {
data.copy(result, resultOffset + 8, sourceOffset - 8 * (differentCount + 1), sourceOffset - 8)
resultOffset += differentCount * 8
differentCount = 0
}
lastBlock = block
continue
}
if (identicalCount > 2) {
result.writeBigUInt64BE(RLE_HEADER, (resultOffset += 8))
result.writeBigUInt64BE(BigInt(identicalCount + 1), (resultOffset += 8))
result.writeBigUInt64BE(lastBlock, (resultOffset += 8))
} else if (identicalCount > 0) {
for (let i = 0; i <= identicalCount; ++i) {
result.writeBigUInt64BE(lastBlock, (resultOffset += 8))
}
} else {
++differentCount
}
lastBlock = block
identicalCount = 0
}

if (identicalCount > 2) {
result.writeBigUInt64BE(RLE_HEADER, (resultOffset += 8))
result.writeBigUInt64BE(BigInt(identicalCount + 1), (resultOffset += 8))
result.writeBigUInt64BE(lastBlock, (resultOffset += 8))
} else if (identicalCount > 0) {
for (let i = 0; i <= identicalCount; ++i) {
result.writeBigUInt64BE(lastBlock, (resultOffset += 8))
}
} else {
++differentCount
data.copy(result, resultOffset + 8, data.length - 8 * differentCount, data.length)
resultOffset += differentCount * 8
}

return result.slice(0, resultOffset + 8)
}

export interface VideoModeInfo {
format: Enums.VideoFormat
width: number
height: number
}

const dimsPAL: Pick<VideoModeInfo, 'width' | 'height' | 'format'> = {
format: Enums.VideoFormat.SD,
width: 720,
height: 576,
}
const dimsNTSC: Pick<VideoModeInfo, 'width' | 'height' | 'format'> = {
format: Enums.VideoFormat.SD,
width: 640,
height: 480,
}
const dims720p: Pick<VideoModeInfo, 'width' | 'height' | 'format'> = {
format: Enums.VideoFormat.HD720,
width: 1280,
height: 720,
}
const dims1080p: Pick<VideoModeInfo, 'width' | 'height' | 'format'> = {
format: Enums.VideoFormat.HD1080,
width: 1920,
height: 1080,
}
const dims4k: Pick<VideoModeInfo, 'width' | 'height' | 'format'> = {
format: Enums.VideoFormat.UHD4K,
width: 3840,
height: 2160,
}
const dims8k: Pick<VideoModeInfo, 'width' | 'height' | 'format'> = {
format: Enums.VideoFormat.UDH8K,
width: 7680,
height: 4260,
}
const VideoModeInfoImpl: { [key in Enums.VideoMode]: VideoModeInfo } = {
[Enums.VideoMode.N525i5994NTSC]: {
...dimsNTSC,
},
[Enums.VideoMode.P625i50PAL]: {
...dimsPAL,
},
[Enums.VideoMode.N525i5994169]: {
...dimsNTSC,
},
[Enums.VideoMode.P625i50169]: {
...dimsPAL,
},

[Enums.VideoMode.P720p50]: {
...dims720p,
},
[Enums.VideoMode.N720p5994]: {
...dims720p,
},
[Enums.VideoMode.P1080i50]: {
...dims1080p,
},
[Enums.VideoMode.N1080i5994]: {
...dims1080p,
},
[Enums.VideoMode.N1080p2398]: {
...dims1080p,
},
[Enums.VideoMode.N1080p24]: {
...dims1080p,
},
[Enums.VideoMode.P1080p25]: {
...dims1080p,
},
[Enums.VideoMode.N1080p2997]: {
...dims1080p,
},
[Enums.VideoMode.P1080p50]: {
...dims1080p,
},
[Enums.VideoMode.N1080p5994]: {
...dims1080p,
},

[Enums.VideoMode.N4KHDp2398]: {
...dims4k,
},
[Enums.VideoMode.N4KHDp24]: {
...dims4k,
},
[Enums.VideoMode.P4KHDp25]: {
...dims4k,
},
[Enums.VideoMode.N4KHDp2997]: {
...dims4k,
},

[Enums.VideoMode.P4KHDp5000]: {
...dims4k,
},
[Enums.VideoMode.N4KHDp5994]: {
...dims4k,
},

[Enums.VideoMode.N8KHDp2398]: {
...dims8k,
},
[Enums.VideoMode.N8KHDp24]: {
...dims8k,
},
[Enums.VideoMode.P8KHDp25]: {
...dims8k,
},
[Enums.VideoMode.N8KHDp2997]: {
...dims8k,
},
[Enums.VideoMode.P8KHDp50]: {
...dims8k,
},
[Enums.VideoMode.N8KHDp5994]: {
...dims8k,
},

[Enums.VideoMode.N1080p30]: {
...dims1080p,
},
[Enums.VideoMode.N1080p60]: {
...dims1080p,
},
}

export function getVideoModeInfo(videoMode: Enums.VideoMode): VideoModeInfo | undefined {
return VideoModeInfoImpl[videoMode]
}

export function convertWAVToRaw(inputBuffer: Buffer, model: Enums.Model | undefined): Buffer {
const wav = new (WaveFile as any)(inputBuffer)

if (wav.fmt.bitsPerSample !== 24) {
throw new Error(`Invalid wav bit bits per sample: ${wav.fmt.bitsPerSample}`)
}

if (wav.fmt.numChannels !== 2) {
throw new Error(`Invalid number of wav channels: ${wav.fmt.numChannel}`)
}

const buffer = Buffer.from(wav.data.samples)
const buffer2 = Buffer.alloc(buffer.length)
for (let i = 0; i < buffer.length; i += 3) {
// 24bit samples, change endian from wavfile to atem requirements
buffer2.writeUIntBE(buffer.readUIntLE(i, 3), i, 3)
}

if (model === undefined || model >= Enums.Model.PS4K) {
// If we don't know the model, assume we want the newer mode as that is more likely
// Newer models want a weird byte order
const buffer3 = Buffer.alloc(buffer2.length)
for (let i = 0; i < buffer.length; i += 4) {
buffer3.writeUIntBE(buffer2.readUIntLE(i, 4), i, 4)
}

return buffer3
} else {
return buffer2
}
}

export function UInt16BEToDecibel(input: number): number {
// 0 = -inf, 32768 = 0, 65381 = +6db
return Math.round(Math.log10(input / 32768) * 20 * 100) / 100
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { encodeRLE } from '../atemUtil'
import { encodeRLE } from '../rle'

describe('RLE', () => {
describe('encodeRLE', () => {
test('no repetitions', () => {
const source = `abababababababab\
cdcdcdcdcdcdcdcd\
Expand Down
Loading

0 comments on commit 5e62374

Please sign in to comment.