RP219 CPU pattern
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
package generator
|
package generator
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
var ebu75Colors = [...]YCbCr10{
|
var ebu75Colors = [...]YCbCr10{
|
||||||
{Y: 721, Cb: 512, Cr: 512},
|
{Y: 721, Cb: 512, Cr: 512},
|
||||||
@@ -24,6 +26,16 @@ var ebu100Colors = [...]YCbCr10{
|
|||||||
{Y: 64, Cb: 512, Cr: 512},
|
{Y: 64, Cb: 512, Cr: 512},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var smpte75Colors = [...]YCbCr10{
|
||||||
|
{Y: 721, Cb: 512, Cr: 512},
|
||||||
|
{Y: 674, Cb: 176, Cr: 543},
|
||||||
|
{Y: 581, Cb: 589, Cr: 176},
|
||||||
|
{Y: 534, Cb: 253, Cr: 207},
|
||||||
|
{Y: 251, Cb: 771, Cr: 817},
|
||||||
|
{Y: 204, Cb: 435, Cr: 848},
|
||||||
|
{Y: 111, Cb: 848, Cr: 481},
|
||||||
|
}
|
||||||
|
|
||||||
var grayBarsColors = [...]YCbCr10{
|
var grayBarsColors = [...]YCbCr10{
|
||||||
{Y: 64, Cb: 512, Cr: 512},
|
{Y: 64, Cb: 512, Cr: 512},
|
||||||
{Y: 137, Cb: 512, Cr: 512},
|
{Y: 137, Cb: 512, Cr: 512},
|
||||||
@@ -58,6 +70,16 @@ func NewCPUPatternGenerator(width, height uint, pattern string) (*CPUGenerator,
|
|||||||
baseColor = ebu100BaseColor
|
baseColor = ebu100BaseColor
|
||||||
case "ebu100-move":
|
case "ebu100-move":
|
||||||
baseColor, dynamic = ebu100BaseColor, true
|
baseColor, dynamic = ebu100BaseColor, true
|
||||||
|
case "smpte":
|
||||||
|
if err := validateRP219Size(width, height); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
baseColor = rp219BaseColor
|
||||||
|
case "smpte-move":
|
||||||
|
if err := validateRP219Size(width, height); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
baseColor, dynamic = rp219BaseColor, true
|
||||||
case "gray-bars":
|
case "gray-bars":
|
||||||
baseColor = grayBarsBaseColor
|
baseColor = grayBarsBaseColor
|
||||||
case "gray-bars-move":
|
case "gray-bars-move":
|
||||||
@@ -117,3 +139,106 @@ func renderBasePattern(dst []byte, width, height int, baseColor baseColorFunc) e
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
gray40 = YCbCr10{Y: 414, Cb: 512, Cr: 512}
|
||||||
|
gray15 = YCbCr10{Y: 195, Cb: 512, Cr: 512}
|
||||||
|
black = YCbCr10{Y: 64, Cb: 512, Cr: 512}
|
||||||
|
white = YCbCr10{Y: 940, Cb: 512, Cr: 512}
|
||||||
|
)
|
||||||
|
|
||||||
|
func rp219BaseColor(x, y, width, height int) YCbCr10 {
|
||||||
|
barsWidth := (height / 3) * 4
|
||||||
|
barsStart := (width - barsWidth) / 2
|
||||||
|
barsEnd := barsStart + barsWidth
|
||||||
|
oneBarWidth := barsWidth / 7
|
||||||
|
|
||||||
|
unitH := height / 12
|
||||||
|
section1End := unitH * 7
|
||||||
|
section2End := section1End + unitH
|
||||||
|
section3End := section2End + unitH
|
||||||
|
|
||||||
|
rampStart := barsStart + oneBarWidth
|
||||||
|
rampEnd := barsEnd
|
||||||
|
rampWidth := rampEnd - rampStart
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case y < section1End:
|
||||||
|
if x < barsStart || x >= barsEnd {
|
||||||
|
return gray40
|
||||||
|
}
|
||||||
|
|
||||||
|
bar := min(
|
||||||
|
(x-barsStart)*len(smpte75Colors)/barsWidth,
|
||||||
|
len(smpte75Colors)-1,
|
||||||
|
)
|
||||||
|
return smpte75Colors[bar]
|
||||||
|
|
||||||
|
case y < section2End:
|
||||||
|
switch {
|
||||||
|
case x < barsStart:
|
||||||
|
return YCbCr10{Y: 754, Cb: 615, Cr: 64} // 100% cyan
|
||||||
|
case x < rampStart:
|
||||||
|
return YCbCr10{Y: 244, Cb: 612, Cr: 395} // -I
|
||||||
|
case x >= barsEnd:
|
||||||
|
return YCbCr10{Y: 127, Cb: 960, Cr: 471} // 100% blue
|
||||||
|
default:
|
||||||
|
return YCbCr10{Y: 721, Cb: 512, Cr: 512} // 75% white
|
||||||
|
}
|
||||||
|
|
||||||
|
case y < section3End:
|
||||||
|
switch {
|
||||||
|
case x < barsStart:
|
||||||
|
return YCbCr10{Y: 877, Cb: 64, Cr: 553} // 100% yellow
|
||||||
|
case x < rampStart:
|
||||||
|
return YCbCr10{Y: 141, Cb: 697, Cr: 606} // +Q
|
||||||
|
case x >= barsEnd:
|
||||||
|
return YCbCr10{Y: 250, Cb: 409, Cr: 960} // 75% red
|
||||||
|
default:
|
||||||
|
rampY := 64 + ((x-rampStart)*876)/rampWidth
|
||||||
|
return YCbCr10{Y: uint32(rampY), Cb: 512, Cr: 512}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if x < barsStart || x >= barsEnd {
|
||||||
|
return gray15
|
||||||
|
}
|
||||||
|
|
||||||
|
bar := (x - barsStart) / oneBarWidth
|
||||||
|
switch {
|
||||||
|
case bar == 1:
|
||||||
|
return white
|
||||||
|
case bar == 3:
|
||||||
|
offset := (x - barsStart) % oneBarWidth
|
||||||
|
subBar := offset * 3 / oneBarWidth
|
||||||
|
|
||||||
|
switch subBar {
|
||||||
|
case 0:
|
||||||
|
return YCbCr10{Y: 46, Cb: 512, Cr: 512}
|
||||||
|
case 1:
|
||||||
|
return black
|
||||||
|
default:
|
||||||
|
return YCbCr10{Y: 82, Cb: 512, Cr: 512}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return black
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRP219Size(width, height uint) error {
|
||||||
|
if height < 12 {
|
||||||
|
return fmt.Errorf("cpu RP 219 requires frame height of at least 12, got %d", height)
|
||||||
|
}
|
||||||
|
barsWidth := (height / 3) * 4
|
||||||
|
if barsWidth < 7 || width < barsWidth {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"cpu RP 219 requires frame width %d to fit a 4:3 pattern area for height %d, got %d",
|
||||||
|
barsWidth,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,10 +116,81 @@ func TestCPUMovingPatternUsesOwnBaseColor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCPUSMPTEIsNotFakedByAnotherPattern(t *testing.T) {
|
func TestCPURP219Pattern(t *testing.T) {
|
||||||
_, err := NewCPUPatternGenerator(1920, 1080, "smpte")
|
const width, height = 1920, 1080
|
||||||
if err == nil || !strings.Contains(err.Error(), "not implemented") {
|
g, err := NewCPUPatternGenerator(width, height, "smpte")
|
||||||
t.Fatalf("error = %v, want not implemented error", err)
|
if err != nil {
|
||||||
|
t.Fatalf("NewCPUPatternGenerator: %v", err)
|
||||||
|
}
|
||||||
|
frame := make([]byte, width*height*8/3)
|
||||||
|
if err := g.GenerateFrame(frame, 0); err != nil {
|
||||||
|
t.Fatalf("GenerateFrame: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
x, y int
|
||||||
|
wantY, wantCb, wantCr uint32
|
||||||
|
}{
|
||||||
|
{"top left gray flank", 100, 100, 414, 512, 512},
|
||||||
|
{"top white bar", 300, 100, 721, 512, 512},
|
||||||
|
{"top green bar", 960, 100, 534, 253, 207},
|
||||||
|
{"top right gray flank", 1800, 100, 414, 512, 512},
|
||||||
|
{"section 2 cyan flank", 100, 650, 754, 615, 64},
|
||||||
|
{"section 2 minus I", 300, 650, 244, 612, 395},
|
||||||
|
{"section 2 white", 600, 650, 721, 512, 512},
|
||||||
|
{"section 2 blue flank", 1800, 650, 127, 960, 471},
|
||||||
|
{"section 3 yellow flank", 100, 750, 877, 64, 553},
|
||||||
|
{"section 3 plus Q", 300, 750, 141, 697, 606},
|
||||||
|
{"section 3 red flank", 1800, 750, 250, 409, 960},
|
||||||
|
{"bottom gray flank", 100, 900, 195, 512, 512},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
y, cb, cr := sampleV210(frame, width, tc.x, tc.y)
|
||||||
|
if y != tc.wantY || cb != tc.wantCb || cr != tc.wantCr {
|
||||||
|
t.Fatalf("pixel (%d,%d): got %d/%d/%d, want %d/%d/%d",
|
||||||
|
tc.x, tc.y, y, cb, cr, tc.wantY, tc.wantCb, tc.wantCr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCPURP219MovingSquare(t *testing.T) {
|
||||||
|
const width, height = 1920, 1080
|
||||||
|
g, err := NewCPUPatternGenerator(width, height, "smpte-move")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewCPUPatternGenerator: %v", err)
|
||||||
|
}
|
||||||
|
frame := make([]byte, width*height*8/3)
|
||||||
|
if err := g.GenerateFrame(frame, 0); err != nil {
|
||||||
|
t.Fatalf("GenerateFrame(0): %v", err)
|
||||||
|
}
|
||||||
|
if y, cb, cr := sampleV210(frame, width, 960, 540); y != 470 || cb != 771 || cr != 817 {
|
||||||
|
t.Fatalf("tick 0 center = %d/%d/%d, want inverted green 470/771/817", y, cb, cr)
|
||||||
|
}
|
||||||
|
if err := g.GenerateFrame(frame, 79); err != nil {
|
||||||
|
t.Fatalf("GenerateFrame(79): %v", err)
|
||||||
|
}
|
||||||
|
if y, cb, cr := sampleV210(frame, width, 1840, 540); y != 590 || cb != 512 || cr != 512 {
|
||||||
|
t.Fatalf("tick 79 shifted square = %d/%d/%d, want inverted gray 590/512/512", y, cb, cr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCPURP219RejectsInvalidGeometry(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
width, height uint
|
||||||
|
}{
|
||||||
|
{name: "height too small", width: 1920, height: 11},
|
||||||
|
{name: "canvas too narrow", width: 600, height: 1080},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
_, err := NewCPUPatternGenerator(tc.width, tc.height, "smpte")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "RP 219") {
|
||||||
|
t.Fatalf("error = %v, want RP 219 geometry error", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user