|  |  |  | @ -10,7 +10,6 @@ import ( | 
		
	
		
			
				|  |  |  |  | 	"fmt" | 
		
	
		
			
				|  |  |  |  | 	"os" | 
		
	
		
			
				|  |  |  |  | 	"os/signal" | 
		
	
		
			
				|  |  |  |  | 	"slices" | 
		
	
		
			
				|  |  |  |  | 	"sync/atomic" | 
		
	
		
			
				|  |  |  |  | 	"syscall" | 
		
	
		
			
				|  |  |  |  | 	"time" | 
		
	
	
		
			
				
					|  |  |  | @ -19,7 +18,7 @@ import ( | 
		
	
		
			
				|  |  |  |  | 	"github.com/gen2brain/malgo" | 
		
	
		
			
				|  |  |  |  | 	"github.com/google/uuid" | 
		
	
		
			
				|  |  |  |  | 	"github.com/hraban/opus" | 
		
	
		
			
				|  |  |  |  | 	"seehuhn.de/go/ncurses" | 
		
	
		
			
				|  |  |  |  |   "git.metznet.ca/MetzNet/go-ncurses" | 
		
	
		
			
				|  |  |  |  | ) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | var decoders = map[pnyx.PeerID]chan[]byte{} | 
		
	
	
		
			
				
					|  |  |  | @ -107,12 +106,7 @@ func mixer(data_chan chan []int16, speaker_chan chan []int16) { | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | func main() { | 
		
	
		
			
				|  |  |  |  |   key_file_arg := flag.String("key", "${HOME}/.pnyx.key", "Path to the private key file to load/save") | 
		
	
		
			
				|  |  |  |  |   generate_key_arg := flag.Bool("genkey", false, "Set to generate a key if none exists") | 
		
	
		
			
				|  |  |  |  |   flag.Parse() | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | func setup_audio() (*malgo.AllocatedContext, *malgo.Device, *malgo.Device) { | 
		
	
		
			
				|  |  |  |  |   ctx, err := malgo.InitContext(nil, malgo.ContextConfig{}, nil) | 
		
	
		
			
				|  |  |  |  |   if err != nil { | 
		
	
		
			
				|  |  |  |  |     panic(err) | 
		
	
	
		
			
				
					|  |  |  | @ -120,9 +114,6 @@ func main() { | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   go mixer(audio_data, speaker) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   defer ctx.Free() | 
		
	
		
			
				|  |  |  |  |   defer ctx.Uninit() | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | 	infos, err := ctx.Devices(malgo.Playback) | 
		
	
		
			
				|  |  |  |  | 	if err != nil { | 
		
	
		
			
				|  |  |  |  |     panic(err) | 
		
	
	
		
			
				
					|  |  |  | @ -205,9 +196,6 @@ func main() { | 
		
	
		
			
				|  |  |  |  |     panic(err) | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   defer outDevice.Uninit() | 
		
	
		
			
				|  |  |  |  |   defer outDevice.Stop() | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | 	onRecvFrames := func(output_samples []byte, input_samples []byte, framecount uint32) { | 
		
	
		
			
				|  |  |  |  |     if encoder != nil { | 
		
	
		
			
				|  |  |  |  |       pcm := make([]int16, len(input_samples)/2) | 
		
	
	
		
			
				
					|  |  |  | @ -241,13 +229,11 @@ func main() { | 
		
	
		
			
				|  |  |  |  |     panic(err) | 
		
	
		
			
				|  |  |  |  | 	} | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   defer inDevice.Uninit() | 
		
	
		
			
				|  |  |  |  |   defer inDevice.Stop() | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   var key ed25519.PrivateKey = nil | 
		
	
		
			
				|  |  |  |  |   return ctx, outDevice, inDevice | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   key_file_path := os.ExpandEnv(*key_file_arg) | 
		
	
		
			
				|  |  |  |  |   key_file_bytes, err := os.ReadFile(key_file_path) | 
		
	
		
			
				|  |  |  |  | func get_private_key(path string, generate bool) ed25519.PrivateKey { | 
		
	
		
			
				|  |  |  |  |   key_file_bytes, err := os.ReadFile(path) | 
		
	
		
			
				|  |  |  |  |   if err == nil { | 
		
	
		
			
				|  |  |  |  |     key_pem, _ := pem.Decode(key_file_bytes) | 
		
	
		
			
				|  |  |  |  |     if key_pem.Type != "PRIVATE KEY" { | 
		
	
	
		
			
				
					|  |  |  | @ -260,12 +246,13 @@ func main() { | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     var ok bool | 
		
	
		
			
				|  |  |  |  |     key, ok = private_key.(ed25519.PrivateKey) | 
		
	
		
			
				|  |  |  |  |     key, ok := private_key.(ed25519.PrivateKey) | 
		
	
		
			
				|  |  |  |  |     if ok == false { | 
		
	
		
			
				|  |  |  |  |       panic("Private key is not ed25519.PrivateKey") | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |   } else if *generate_key_arg { | 
		
	
		
			
				|  |  |  |  |     _, key, err = ed25519.GenerateKey(rand.Reader) | 
		
	
		
			
				|  |  |  |  |     return key | 
		
	
		
			
				|  |  |  |  |   } else if generate { | 
		
	
		
			
				|  |  |  |  |     _, key, err := ed25519.GenerateKey(rand.Reader) | 
		
	
		
			
				|  |  |  |  |     if err != nil { | 
		
	
		
			
				|  |  |  |  |       panic(err) | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
	
		
			
				
					|  |  |  | @ -280,37 +267,35 @@ func main() { | 
		
	
		
			
				|  |  |  |  |       Bytes: key_pkcs8, | 
		
	
		
			
				|  |  |  |  |     }) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     err = os.WriteFile(key_file_path, key_pem, 0o600) | 
		
	
		
			
				|  |  |  |  |     err = os.WriteFile(path, key_pem, 0o600) | 
		
	
		
			
				|  |  |  |  |     if err != nil { | 
		
	
		
			
				|  |  |  |  |       panic(err) | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |     return key | 
		
	
		
			
				|  |  |  |  |   } else { | 
		
	
		
			
				|  |  |  |  |     panic(fmt.Sprintf("Failed to read key from %s", path)) | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   client, err := pnyx.NewClient(key, flag.Arg(0)) | 
		
	
		
			
				|  |  |  |  |   if err != nil { | 
		
	
		
			
				|  |  |  |  |     panic(err) | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | func main_loop(client *pnyx.Client, window ncurses.Window, active *atomic.Bool, packet_chan chan pnyx.Payload, user_chan chan rune) { | 
		
	
		
			
				|  |  |  |  |   max_y := ncurses.GetMaxY.Load()(window) | 
		
	
		
			
				|  |  |  |  |   max_x := ncurses.GetMaxX.Load()(window) | 
		
	
		
			
				|  |  |  |  |   titlebar := ncurses.NewWin.Load()(1, max_x, 0, 0) | 
		
	
		
			
				|  |  |  |  |   channels := ncurses.NewWin.Load()(max_y - 1, max_x / 3, 1, 0) | 
		
	
		
			
				|  |  |  |  |   body := ncurses.NewWin.Load()(max_y - 1, max_x * 2 / 3, 1, max_x / 3) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   go func() { | 
		
	
		
			
				|  |  |  |  |     var buf [1024]byte | 
		
	
		
			
				|  |  |  |  |     for true { | 
		
	
		
			
				|  |  |  |  |       read, _, err := client.Connection.ReadFromUDP(buf[:]) | 
		
	
		
			
				|  |  |  |  |       if err != nil { | 
		
	
		
			
				|  |  |  |  |         break | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |   server_name := client.Connection.RemoteAddr().String() | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |       data, err := pnyx.ParseSessionData(&client.Session, buf[pnyx.COMMAND_LENGTH + pnyx.SESSION_ID_LENGTH:read]) | 
		
	
		
			
				|  |  |  |  |       if err != nil { | 
		
	
		
			
				|  |  |  |  |         continue | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |       packet, err := pnyx.ParsePacket(data) | 
		
	
		
			
				|  |  |  |  |       if err != nil { | 
		
	
		
			
				|  |  |  |  |         continue | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |   ncurses.MvWAddStr.Load()(titlebar, 0, 0, fmt.Sprintf("pnyx client %X:%X", client.Key.Public().(ed25519.PublicKey)[:2], client.Session.ID[:2])) | 
		
	
		
			
				|  |  |  |  |   ncurses.MvWAddStr.Load()(body, 0, max_x-len(server_name), server_name) | 
		
	
		
			
				|  |  |  |  |   ncurses.WRefresh.Load()(titlebar) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   for active.Load() { | 
		
	
		
			
				|  |  |  |  |     select { | 
		
	
		
			
				|  |  |  |  |     case packet := <-packet_chan: | 
		
	
		
			
				|  |  |  |  |       switch packet := packet.(type) { | 
		
	
		
			
				|  |  |  |  |       case pnyx.PingPacket: | 
		
	
		
			
				|  |  |  |  |         _ = client.Send(pnyx.NewPingPacket()) | 
		
	
		
			
				|  |  |  |  |       case pnyx.ChannelCommandPacket: | 
		
	
		
			
				|  |  |  |  |         if packet.Channel == pnyx.ChannelID(0) { | 
		
	
		
			
				|  |  |  |  |           if packet.Mode == pnyx.MODE_AUDIO { | 
		
	
	
		
			
				
					|  |  |  | @ -350,61 +335,120 @@ func main() { | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |       default: | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |     case char := <-user_chan: | 
		
	
		
			
				|  |  |  |  |       ncurses.MvWAddStr.Load()(body, 0, 0, string(char)) | 
		
	
		
			
				|  |  |  |  |       ncurses.WRefresh.Load()(body) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |       ncurses.MvWAddStr.Load()(channels, 0, 0, string(char)) | 
		
	
		
			
				|  |  |  |  |       ncurses.WRefresh.Load()(channels) | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |   }() | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   join_packet := pnyx.NewChannelCommandPacket(uuid.New(), pnyx.ChannelID(0), pnyx.MODE_CHANNEL, pnyx.CHANNEL_COMMAND_JOIN, nil) | 
		
	
		
			
				|  |  |  |  |   err = client.Send(join_packet) | 
		
	
		
			
				|  |  |  |  | func bitmatch(b byte, pattern byte, length int) bool { | 
		
	
		
			
				|  |  |  |  |   mask := ^(byte(1 << (8 - length)) - 1) | 
		
	
		
			
				|  |  |  |  |   return (b ^ pattern) & mask == 0 | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | func ch_listen(active *atomic.Bool, user_chan chan rune) { | 
		
	
		
			
				|  |  |  |  |   b := [4]byte{} | 
		
	
		
			
				|  |  |  |  |   for active.Load() { | 
		
	
		
			
				|  |  |  |  |     os.Stdin.Read(b[0:1]) | 
		
	
		
			
				|  |  |  |  |     if bitmatch(b[0], 0b00000000, 1) { | 
		
	
		
			
				|  |  |  |  |       user_chan <- rune(b[0]) | 
		
	
		
			
				|  |  |  |  |     } // TODO: further utf-8 support
 | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | func udp_listen(client *pnyx.Client, active *atomic.Bool, packet_chan chan pnyx.Payload) { | 
		
	
		
			
				|  |  |  |  |   var buf [1024]byte | 
		
	
		
			
				|  |  |  |  |   for active.Load() { | 
		
	
		
			
				|  |  |  |  |     read, _, err := client.Connection.ReadFromUDP(buf[:]) | 
		
	
		
			
				|  |  |  |  |     if err != nil { | 
		
	
		
			
				|  |  |  |  |     panic(err) | 
		
	
		
			
				|  |  |  |  |       break | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   get_sample_rate_packet := pnyx.NewChannelCommandPacket(uuid.New(), pnyx.ChannelID(0), pnyx.MODE_AUDIO, pnyx.AUDIO_SET_SAMPLE_RATE, []byte{byte(pnyx.SAMPLE_RATE_48KHZ)}) | 
		
	
		
			
				|  |  |  |  |   err = client.Send(get_sample_rate_packet) | 
		
	
		
			
				|  |  |  |  |     data, err := pnyx.ParseSessionData(&client.Session, buf[pnyx.COMMAND_LENGTH + pnyx.SESSION_ID_LENGTH:read]) | 
		
	
		
			
				|  |  |  |  |     if err != nil { | 
		
	
		
			
				|  |  |  |  |     panic(err) | 
		
	
		
			
				|  |  |  |  |       continue | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   go func(){ | 
		
	
		
			
				|  |  |  |  |     packet, err := pnyx.ParsePacket(data) | 
		
	
		
			
				|  |  |  |  |     if err != nil { | 
		
	
		
			
				|  |  |  |  |       continue | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     packet_chan <- packet | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | func process_mic(client *pnyx.Client) { | 
		
	
		
			
				|  |  |  |  |   for true { | 
		
	
		
			
				|  |  |  |  |     data := <- mic | 
		
	
		
			
				|  |  |  |  |       err = client.Send(pnyx.NewDataPacket(pnyx.ChannelID(0), pnyx.MODE_AUDIO, data)) | 
		
	
		
			
				|  |  |  |  |     err := client.Send(pnyx.NewDataPacket(pnyx.ChannelID(0), pnyx.MODE_AUDIO, data)) | 
		
	
		
			
				|  |  |  |  |     if err != nil { | 
		
	
		
			
				|  |  |  |  |       panic(err) | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |   }() | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | func main() { | 
		
	
		
			
				|  |  |  |  |   key_file_arg := flag.String("key", "${HOME}/.pnyx.key", "Path to the private key file to load/save") | 
		
	
		
			
				|  |  |  |  |   generate_key_arg := flag.Bool("genkey", false, "Set to generate a key if none exists") | 
		
	
		
			
				|  |  |  |  |   flag.Parse() | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   ctx, outDevice, inDevice := setup_audio() | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   defer ctx.Free() | 
		
	
		
			
				|  |  |  |  |   defer ctx.Uninit() | 
		
	
		
			
				|  |  |  |  |   defer outDevice.Uninit() | 
		
	
		
			
				|  |  |  |  |   defer outDevice.Stop() | 
		
	
		
			
				|  |  |  |  |   defer inDevice.Uninit() | 
		
	
		
			
				|  |  |  |  |   defer inDevice.Stop() | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   key := get_private_key(os.ExpandEnv(*key_file_arg), *generate_key_arg) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   client, err := pnyx.NewClient(key, flag.Arg(0)) | 
		
	
		
			
				|  |  |  |  |   if err != nil { | 
		
	
		
			
				|  |  |  |  |     panic(err) | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |    | 
		
	
		
			
				|  |  |  |  |   packet_chan := make(chan pnyx.Payload, 1024) | 
		
	
		
			
				|  |  |  |  |   user_chan := make(chan rune, 1024) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   window := ncurses.Init() | 
		
	
		
			
				|  |  |  |  |   active := atomic.Bool{} | 
		
	
		
			
				|  |  |  |  |   active.Store(true) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   go func() { | 
		
	
		
			
				|  |  |  |  |     ncurses.ColorPair(1).Init(ncurses.ColorBlue, ncurses.ColorRed) | 
		
	
		
			
				|  |  |  |  |     window.AddStr("pnyx client") | 
		
	
		
			
				|  |  |  |  |   go udp_listen(client, &active, packet_chan) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     for active.Load() { | 
		
	
		
			
				|  |  |  |  |       window.Refresh() | 
		
	
		
			
				|  |  |  |  |       time.Sleep(200*time.Millisecond) | 
		
	
		
			
				|  |  |  |  |       peers := make([]pnyx.PeerID, 0, len(decoders)) | 
		
	
		
			
				|  |  |  |  |       for peer_id := range(decoders) { | 
		
	
		
			
				|  |  |  |  |         peers = append(peers, peer_id) | 
		
	
		
			
				|  |  |  |  |   join_packet := pnyx.NewChannelCommandPacket(uuid.New(), pnyx.ChannelID(0), pnyx.MODE_CHANNEL, pnyx.CHANNEL_COMMAND_JOIN, nil) | 
		
	
		
			
				|  |  |  |  |   err = client.Send(join_packet) | 
		
	
		
			
				|  |  |  |  |   if err != nil { | 
		
	
		
			
				|  |  |  |  |     panic(err) | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |       slices.SortFunc(peers, func(a, b pnyx.PeerID) int { | 
		
	
		
			
				|  |  |  |  |         return slices.Compare(a[:], b[:]) | 
		
	
		
			
				|  |  |  |  |       }) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |       for i, peer_id := range(peers) { | 
		
	
		
			
				|  |  |  |  |         window.MvAddStr(i+1, 0, fmt.Sprintf("%x", peer_id)) | 
		
	
		
			
				|  |  |  |  |   get_sample_rate_packet := pnyx.NewChannelCommandPacket(uuid.New(), pnyx.ChannelID(0), pnyx.MODE_AUDIO, pnyx.AUDIO_SET_SAMPLE_RATE, []byte{byte(pnyx.SAMPLE_RATE_48KHZ)}) | 
		
	
		
			
				|  |  |  |  |   err = client.Send(get_sample_rate_packet) | 
		
	
		
			
				|  |  |  |  |   if err != nil { | 
		
	
		
			
				|  |  |  |  |     panic(err) | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   go process_mic(client) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   err = ncurses.Init() | 
		
	
		
			
				|  |  |  |  |   if err != nil { | 
		
	
		
			
				|  |  |  |  |     panic(err) | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |   }() | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   window := ncurses.InitScr.Load()() | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   go ch_listen(&active, user_chan) | 
		
	
		
			
				|  |  |  |  |   go main_loop(client, window, &active, packet_chan, user_chan) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   os_sigs := make(chan os.Signal, 1) | 
		
	
		
			
				|  |  |  |  |   signal.Notify(os_sigs, syscall.SIGINT, syscall.SIGABRT) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |   <-os_sigs | 
		
	
		
			
				|  |  |  |  |   active.Store(false) | 
		
	
		
			
				|  |  |  |  |   ncurses.EndWin() | 
		
	
		
			
				|  |  |  |  |   ncurses.EndWin.Load()() | 
		
	
		
			
				|  |  |  |  | } | 
		
	
	
		
			
				
					|  |  |  | 
 |