package genericmidi import ( "context" "dmxconnect/hardware" "fmt" "regexp" "strconv" "strings" "sync" "time" "github.com/rs/zerolog/log" "gitlab.com/gomidi/rtmididrv" ) // Provider represents how the protocol is defined type Provider struct { wg sync.WaitGroup mu sync.Mutex detected map[string]*Endpoint // Detected endpoints scanEvery time.Duration // Scans endpoints periodically onArrival func(context.Context, hardware.Endpoint) // When a endpoint arrives onRemoval func(context.Context, hardware.Endpoint) // When a endpoint goes away } // NewProvider creates a new MIDI provider func NewProvider(scanEvery time.Duration) *Provider { log.Trace().Str("file", "MIDIProvider").Msg("MIDI provider created") return &Provider{ scanEvery: scanEvery, detected: make(map[string]*Endpoint), } } // OnArrival is the callback function when a new endpoint arrives func (f *Provider) OnArrival(cb func(context.Context, hardware.Endpoint)) { f.onArrival = cb } // OnRemoval i the callback when a endpoint goes away func (f *Provider) OnRemoval(cb func(context.Context, hardware.Endpoint)) { f.onRemoval = cb } // Initialize initializes the MIDI driver func (f *Provider) Initialize() error { log.Trace().Str("file", "MIDIProvider").Msg("MIDI provider initialized") return nil } // Create creates a new endpoint, based on the endpoint information (manually created) func (f *Provider) Create(ctx context.Context, endpointInfo hardware.EndpointInfo) (hardware.EndpointInfo, error) { return hardware.EndpointInfo{}, nil } // Remove removes an existing endpoint (manually created) func (f *Provider) Remove(ctx context.Context, endpoint hardware.Endpoint) error { return nil } // Start starts the provider and search for endpoints func (f *Provider) Start(ctx context.Context) error { f.wg.Add(1) go func() { ticker := time.NewTicker(f.scanEvery) defer ticker.Stop() defer f.wg.Done() for { select { case <-ctx.Done(): return case <-ticker.C: // Scan the endpoints err := f.scanEndpoints(ctx) if err != nil { log.Err(err).Str("file", "MIDIProvider").Msg("unable to scan MIDI endpoints") } } } }() return nil } // WaitStop stops the provider func (f *Provider) WaitStop() error { log.Trace().Str("file", "MIDIProvider").Msg("stopping the MIDI provider...") // Wait for goroutines to stop f.wg.Wait() log.Trace().Str("file", "MIDIProvider").Msg("MIDI provider stopped") return nil } // GetName returns the name of the driver func (f *Provider) GetName() string { return "MIDI" } func splitStringAndNumber(input string) (string, int, error) { // Regular expression to match the text part and the number at the end re := regexp.MustCompile(`^(.*?)(\d+)$`) matches := re.FindStringSubmatch(input) // Check if the regex found both a text part and a number if len(matches) == 3 { // matches[1]: text part (might contain trailing spaces) // matches[2]: numeric part as a string textPart := strings.TrimSpace(matches[1]) // Remove any trailing spaces from the text numberPart, err := strconv.Atoi(matches[2]) if err != nil { return "", 0, err // Return error if the number conversion fails } return textPart, numberPart, nil } // Return an error if no trailing number is found return "", 0, fmt.Errorf("no number found at the end of the string") } // scanEndpoints scans the MIDI endpoints func (f *Provider) scanEndpoints(ctx context.Context) error { currentMap := make(map[string]*Endpoint) drv, err := rtmididrv.New() if err != nil { return fmt.Errorf("unable to open the MIDI driver") } defer drv.Close() // Get MIDI INPUT ports ins, err := drv.Ins() if err != nil { return fmt.Errorf("unable to scan MIDI IN ports: %s", err) } for _, port := range ins { // Exclude microsoft wavetable from the list if strings.Contains(port.String(), "GS Wavetable") { continue } baseName := normalizeName(port.String()) sn := strings.ReplaceAll(strings.ToLower(baseName), " ", "_") if _, ok := currentMap[sn]; !ok { currentMap[sn] = &Endpoint{ info: hardware.EndpointInfo{ Name: baseName, SerialNumber: sn, ProtocolName: "MIDI", }, } } currentMap[sn].inputPorts = append(currentMap[sn].inputPorts, port) log.Info().Any("endpoints", currentMap).Msg("available MIDI IN ports") } // Get MIDI OUTPUT ports outs, err := drv.Outs() if err != nil { return fmt.Errorf("unable to scan MIDI OUT ports: %s", err) } for _, port := range outs { // Exclude microsoft wavetable from the list if strings.Contains(port.String(), "GS Wavetable") { continue } baseName := normalizeName(port.String()) sn := strings.ReplaceAll(strings.ToLower(baseName), " ", "_") if _, ok := currentMap[sn]; !ok { currentMap[sn] = &Endpoint{ info: hardware.EndpointInfo{ Name: baseName, SerialNumber: sn, ProtocolName: "MIDI", }, } } currentMap[sn].outputsPorts = append(currentMap[sn].outputsPorts, port) log.Info().Any("endpoints", currentMap).Msg("available MIDI OUT ports") } log.Debug().Any("value", currentMap).Msg("MIDI endpoints map") // Detect arrivals for sn, discovery := range currentMap { if _, known := f.detected[sn]; !known { endpoint := NewEndpoint(discovery.info, discovery.inputPorts, discovery.outputsPorts) f.detected[sn] = endpoint if f.onArrival != nil { f.onArrival(ctx, discovery) } } } // Detect removals for sn, oldEndpoint := range f.detected { if _, still := currentMap[sn]; !still { log.Info().Str("sn", sn).Str("name", oldEndpoint.GetInfo().Name).Msg("[MIDI] endpoint removed") // Execute the removal callback if f.onRemoval != nil { f.onRemoval(ctx, oldEndpoint) } // Delete it from the detected list delete(f.detected, sn) } } return nil } func normalizeName(raw string) string { name := strings.TrimSpace(raw) // Si parenthèses, prendre le texte à l'intérieur start := strings.Index(name, "(") end := strings.LastIndex(name, ")") if start != -1 && end != -1 && start < end { name = name[start+1 : end] return strings.TrimSpace(name) } // Sinon, supprimer le dernier mot s'il est un entier parts := strings.Fields(name) // découpe en mots if len(parts) > 1 { if _, err := strconv.Atoi(parts[len(parts)-1]); err == nil { parts = parts[:len(parts)-1] // retirer le dernier mot } } return strings.Join(parts, " ") }