Audio #1

Merged
itten merged 11 commits from audio into main 2026-09-16 23:50:47 +03:00
2 changed files with 194 additions and 3 deletions
Showing only changes of commit ce00305c25 - Show all commits
+83 -3
View File
@@ -9,9 +9,10 @@ import (
) )
const ( const (
FormatVideo = "urn:x-nmos:format:video" FormatVideo = "urn:x-nmos:format:video"
FormatAudio = "urn:x-nmos:format:audio" FormatAudio = "urn:x-nmos:format:audio"
MediaTypeV210 = "video/v210" MediaTypeV210 = "video/v210"
MediaTypeFloat32 = "audio/float32"
InterlaceProgressive = "progressive" InterlaceProgressive = "progressive"
ColorSpaceBT709 = "BT709" ColorSpaceBT709 = "BT709"
@@ -32,6 +33,22 @@ type Rational struct {
Denominator uint `json:"denominator"` Denominator uint `json:"denominator"`
} }
func (r *Rational) UnmarshalJSON(data []byte) error {
var value struct {
Numerator uint `json:"numerator"`
Denominator *uint `json:"denominator"`
}
if err := json.Unmarshal(data, &value); err != nil {
return err
}
r.Numerator = value.Numerator
r.Denominator = 1
if value.Denominator != nil {
r.Denominator = *value.Denominator
}
return nil
}
type Video struct { type Video struct {
Common Common
@@ -141,3 +158,66 @@ func (v Video) Validate() error {
} }
return nil return nil
} }
func NewFloat32Audio(
id string,
channels uint,
rate Rational,
) (Audio, error) {
definition := Audio{
Common: Common{
Description: "go-mxl-pattern-gen generated audio",
ID: id,
Tags: map[string][]string{
"urn:x-nmos:tag:grouphint/v1.0": {
"go-mxl-pattern-gen:Audio",
},
},
Format: FormatAudio,
Label: "go-mxl-pattern-gen generated audio",
Parents: []string{},
MediaType: MediaTypeFloat32,
},
SampleRate: rate,
ChannelCount: channels,
BitDepth: 32,
}
if err := definition.Validate(); err != nil {
return Audio{}, err
}
return definition, nil
}
func (a Audio) Validate() error {
if err := uuid.Validate(a.ID); err != nil {
return fmt.Errorf("invalid id %q: %w", a.ID, err)
}
if a.Format != FormatAudio {
return fmt.Errorf("format must be %q, got %q", FormatAudio, a.Format)
}
if a.MediaType != MediaTypeFloat32 {
return fmt.Errorf("media_type must be %q, got %q", MediaTypeFloat32, a.MediaType)
}
if a.ChannelCount == 0 {
return fmt.Errorf("channel_count must be greater than zero")
}
if a.SampleRate.Numerator == 0 || a.SampleRate.Denominator == 0 {
return fmt.Errorf("sample_rate numerator and denominator must be greater than zero, got %d/%d",
a.SampleRate.Numerator, a.SampleRate.Denominator)
}
if a.BitDepth != 32 {
return fmt.Errorf("bit_depth must be 32, got %d", a.BitDepth)
}
return nil
}
func ParseFloat32Audio(data []byte) (Audio, error) {
var definition Audio
if err := json.Unmarshal(data, &definition); err != nil {
return Audio{}, fmt.Errorf("decode audio flow definition: %w", err)
}
if err := definition.Validate(); err != nil {
return Audio{}, fmt.Errorf("invalid audio flow definition: %w", err)
}
return definition, nil
}
+111
View File
@@ -7,6 +7,7 @@ import (
) )
const testVideoID = "5fbec3b1-1b0f-417d-9059-8b94a47197ed" const testVideoID = "5fbec3b1-1b0f-417d-9059-8b94a47197ed"
const testAudioID = "b3bb5be7-9fe9-4324-a5bb-4c70e1084449"
func TestNewV210Video(t *testing.T) { func TestNewV210Video(t *testing.T) {
definition, err := NewV210Video(testVideoID, 1920, 1080, Rational{Numerator: 30000, Denominator: 1001}) definition, err := NewV210Video(testVideoID, 1920, 1080, Rational{Numerator: 30000, Denominator: 1001})
@@ -65,3 +66,113 @@ func TestParseV210VideoRejectsAudio(t *testing.T) {
t.Fatalf("error = %v, want video format error", err) t.Fatalf("error = %v, want video format error", err)
} }
} }
func TestNewFloat32Audio(t *testing.T) {
definition, err := NewFloat32Audio(testAudioID, 2, Rational{Numerator: 48000, Denominator: 1})
if err != nil {
t.Fatalf("NewFloat32Audio: %v", err)
}
if definition.ID != testAudioID {
t.Fatalf("ID = %q, want %q", definition.ID, testAudioID)
}
if definition.Format != FormatAudio || definition.MediaType != MediaTypeFloat32 {
t.Fatalf("format/media type = %q/%q", definition.Format, definition.MediaType)
}
if definition.ChannelCount != 2 || definition.BitDepth != 32 {
t.Fatalf("channels/bit depth = %d/%d, want 2/32", definition.ChannelCount, definition.BitDepth)
}
if definition.SampleRate != (Rational{Numerator: 48000, Denominator: 1}) {
t.Fatalf("sample rate = %+v, want 48000/1", definition.SampleRate)
}
if definition.Parents == nil {
t.Fatal("Parents is nil; want an empty JSON array")
}
}
func TestNewFloat32AudioRejectsZeroChannels(t *testing.T) {
_, err := NewFloat32Audio(testAudioID, 0, Rational{Numerator: 48000, Denominator: 1})
if err == nil || !strings.Contains(err.Error(), "channel_count") {
t.Fatalf("error = %v, want channel_count error", err)
}
}
func TestNewFloat32AudioRejectsInvalidRate(t *testing.T) {
_, err := NewFloat32Audio(testAudioID, 2, Rational{Numerator: 48000, Denominator: 0})
if err == nil || !strings.Contains(err.Error(), "sample_rate") {
t.Fatalf("error = %v, want sample_rate error", err)
}
}
func TestParseFloat32Audio(t *testing.T) {
want, err := NewFloat32Audio(testAudioID, 2, Rational{Numerator: 48000, Denominator: 1})
if err != nil {
t.Fatalf("NewFloat32Audio: %v", err)
}
data, err := json.Marshal(want)
if err != nil {
t.Fatalf("json.Marshal: %v", err)
}
got, err := ParseFloat32Audio(data)
if err != nil {
t.Fatalf("ParseFloat32Audio: %v", err)
}
if got.ID != want.ID || got.ChannelCount != want.ChannelCount || got.SampleRate != want.SampleRate {
t.Fatalf("parsed definition = %+v, want %+v", got, want)
}
}
func TestParseFloat32AudioDefaultsDenominator(t *testing.T) {
data := []byte(`{
"description":"test audio",
"id":"b3bb5be7-9fe9-4324-a5bb-4c70e1084449",
"tags":{},
"format":"urn:x-nmos:format:audio",
"label":"test audio",
"parents":[],
"media_type":"audio/float32",
"sample_rate":{"numerator":48000},
"channel_count":2,
"bit_depth":32
}`)
definition, err := ParseFloat32Audio(data)
if err != nil {
t.Fatalf("ParseFloat32Audio: %v", err)
}
if definition.SampleRate.Denominator != 1 {
t.Fatalf("denominator = %d, want implicit 1", definition.SampleRate.Denominator)
}
}
func TestParseFloat32AudioRejectsExplicitZeroDenominator(t *testing.T) {
data := []byte(`{
"id":"b3bb5be7-9fe9-4324-a5bb-4c70e1084449",
"format":"urn:x-nmos:format:audio",
"media_type":"audio/float32",
"sample_rate":{"numerator":48000,"denominator":0},
"channel_count":2,
"bit_depth":32
}`)
_, err := ParseFloat32Audio(data)
if err == nil || !strings.Contains(err.Error(), "sample_rate") {
t.Fatalf("error = %v, want sample_rate error", err)
}
}
func TestParseFloat32AudioRejectsVideo(t *testing.T) {
definition, err := NewV210Video(testVideoID, 1920, 1080, Rational{Numerator: 25, Denominator: 1})
if err != nil {
t.Fatalf("NewV210Video: %v", err)
}
data, err := json.Marshal(definition)
if err != nil {
t.Fatalf("json.Marshal: %v", err)
}
_, err = ParseFloat32Audio(data)
if err == nil || !strings.Contains(err.Error(), "format must be") {
t.Fatalf("error = %v, want audio format error", err)
}
}