Frammento di codice singolo per eseguire due diverse operazioni

2

Voglio gestire le note musicali in un programma C # e ho scritto un paio di lezioni per questo. Tuttavia, penso di avere problemi nel rispettare il principio di ASCIUTTO. O questo o io sto pensando troppo alle cose. Scusa se la domanda è già stata trattata da qualche altra parte, non ho potuto pensare a parole chiave più esplicite e ancora non ho trovato nulla sul mio problema.
Non sono riuscito a trovare un esempio più semplice che si adattasse alla spiegazione del mio problema e non sapevo cosa includere e cosa lasciare fuori dalla mia domanda, quindi ho scritto una versione rapida e una versione dettagliata della domanda.

Sfondo

Diciamo che esistono sette etichette per descrivere tutte le note musicali (lettere dalla A alla G). Naturalmente, ci sono più di sette note per ogni composizione, quindi solo sette etichette non erano sufficienti. Quindi abbiamo iniziato a metterli su scaffali ( ottave ). Una nota potrebbe condividere la stessa etichetta di un'altra, ma potrebbe essere su uno scaffale diverso (uno più alto o uno più basso). Questo è il motivo per cui troviamo note C che sono più alte di altre note C.
A causa di tutto ciò, le note di etichettatura non sono banali nella rappresentazione del computer: ci sono due informazioni separate (etichetta e scaffale), le etichette si avvolgono quando una nota si sposta da uno scaffale all'altro e deve essere gestita.

Ho la seguente classe che incapsula entrambe le informazioni sull'etichetta e sull'ottava:

public class OctaveNote
{
    // Note is an enum storing my seven labels.
    private Note n;
    private byte o;

    public OctaveNote(Note note, byte octave)
    {
        n = note;
        o = octave;
    }
}

Ho anche queste due funzioni membro utilizzate per inserire una nota in alto o in basso:

public OctaveNote PitchUp(byte tones)
{
    byte octaveShift = (byte)(tones / 7);
    byte newOct = (byte)(o + octaveShift);

    char noteShift = (char)(tones % 7);
    Note newNote = (Note)((((int)(n + noteShift) % 7) + 7) % 7);
    if (newNote < n)
    {
        newOct++;
    }

    return new OctaveNote(newNote, newOct);
}

public OctaveNote PitchDown(byte tones)
{
    byte octaveShift = (byte)(tones / 7);
    byte newOct = (byte)(o - octaveShift);

    byte noteShift = (byte)(tones % 7);
    Note newNote = (Note)((((int)(n - noteShift) % 7) + 7) % 7);
    if (newNote > n)
    {
        newOct--;
    }

    return new OctaveNote(newNote, newOct);
}

Il problema

Il codice è un po 'complicato, quindi diciamo che la logica non è rilevante per ora. Il vero problema è qui: i due metodi sono identici , tranne per le tre dichiarazioni:

n + noteShift    vs.    n - noteShift
newNote < n      vs.    newNote > n
newOct++         vs.    newOct--

Vorrei unire i due metodi per evitare la duplicazione del codice. tones non sarebbe un byte ma un char , e potrebbe assumere valori negativi per esprimere l'inclinazione verso il basso, in questo modo:

n.Pitch(3)   => n.PitchUp(3)
n.Pitch(-4)  => n.PitchDown(4)

La risposta ingenua sarebbe quella di inserire alcune dichiarazioni if qua e là per verificare se tones è negativo ed eseguire le operazioni di conseguenza, ma non c'è un modo più elegante? Cioè, è possibile scrivere un singolo codice frammento che si comporta in modo diverso quando tones < 0 ?

Il numero 7 viene usato molto perché (in questo caso) un'ottava è composta da 7 toni. È usato per trovare lo shift di ottava (divisione intero) e lo spostamento di nota (mod).

L'unica parte difficile è l'assegnazione di newNote , perché n - noteShift può essere negativo e la mod può produrre un valore non valido per tornare indietro nel mio enum, quindi l'ho aggirato aggiungendo 7 prima di modificare di nuovo .

Per i non musicisti là fuori:

  • Le note sono rappresentate da lettere dalla A alla G e si avvolgono su entrambe le estremità: A B C D E F G A B C . ..
  • Quindi, ci possono essere diverse note con la stessa etichetta, ad esempio una nota C più alta di un'altra nota C.
  • Per distinguerli, abbiamo ottave . Ad esempio, C5 è un'ottava sopra C4 .
  • Gli ottavi si spostano dopo aver raggiunto la nota C. Questo è (parzialmente) perché la scala più nota inizia su C, ed è per questo che il mio enum inizia anche su C. La seguente sequenza è nota nel loro ordine naturale: ... G3 A3 B3 C4 D4 E4 F4 G4 A4 B4 C5 D5 . ..
posta qreon 27.12.2017 - 18:58
fonte

3 risposte

5

Ciò che stai descrivendo sembra che tu voglia qualcosa di simile alla relazione tra DateTime e TimeSpan . Forse potresti descriverlo come Note e NoteShift . Penso che chiamarlo Note invece di OctaveNote sia molto più chiaro se si considera che C7 è una nota diversa da C4 (C nella 7a ottava vs C nel 4 °).

Apprendendo ulteriormente dalla relazione tra DateTime e TimeSpan , potresti ridurre la complessità interna della matematica memorizzando la nota come un intero. Sta a te decidere se questo intero rappresenta una frequenza o una posizione sulla scala. Il concetto di come rappresentare Note viene eseguito nelle funzioni di formattazione.

Tecnicamente parlando ci sono 12 note in scala moderna con A4 essendo 440Hz . Ogni ottava rappresenta un raddoppio della frequenza in modo che A5 sia 880Hz e A3 sia 220Hz. Questi sono dettagli rappresentativi che puoi incorporare come metodo per ottenere la frequenza dalla nota.

Diciamo che devi rappresentare 8 ottave (la tastiera a 88 tasti inizia da A0 e termina a C8). Potresti rappresentare tutto questo come un numero compreso tra 0 e 107. Ciò rende la matematica molto più semplice.

public class Note
{
    public byte MaxOctave = 8;
    public enum Name { C, Csharp, D, Dsharp, E, F, Fsharp, G, Gsharp, A, Asharp, B };
    private final byte note;

    public Note(Name name, byte octave)
    {
        note = ((octave + 1) * ((byte) name)) - 1;
    }

    private Note(byte newNote)
    {
        if (newNote < 0 || newNote > ((byte)Enum.GetNames(typeof(Name)).Length * MaxOctave))
        {
            throw new ArgumentOutOfRangeException(nameof(newNote));
        }

        note = newNote;
    }

    public Note ChangePitch(NoteShift shift)
    {
        return new Note(note + shift.relativeNote);
    }

    public override string ToString()
    {
        var notesPerOctave = (byte)Enum.GetNames(typeof(Name)).Length;
        var nameVal = note % notesPerOctave;
        var octave = note / notesPerOctave;

        var name = (Name)Enum.GetValue(typeof(Name), nameVal);
        return $"{name}{octave}";
    }
}

Il tuo NoteShift avrebbe solo un byte positivo o un byte negativo per l'importo da spostare. Hai la complicazione matematica nella chiamata a ToString() , ma in genere non viene chiamata per tutto il tempo.

public class NoteShift
{
    final byte relativeNote; // package access

    public NoteShift(byte shift)
    {
        relativeNote = shift;
    }

    public static NoteShift AddNotes(byte notes)
    {
        return new NoteShift(notes);
    }

    public static NoteShift AddOctaves(byte octaves)
    {
        return new NoteShift(octaves * (byte)Enum.GetNames(typeof(Note.Name)).Length);
    }
}

Un ulteriore vantaggio di questo accordo è che è possibile impiegare un sovraccarico dell'operatore intelligente per addizione, sottrazione, maggiore, minore e uguale. Oltre all'implementazione di IComparable<Note> e IEquatable<Note> . Questo può rendere la tua libreria molto più espressiva. Assicurati solo che il tipo di operatori che hai sovraccaricato abbia senso. Ad esempio, non esiste un vero motivo per moltiplicare o dividere le note.

    
risposta data 27.12.2017 - 20:45
fonte
1

Il modo più elegante per farlo è rappresentare OctiveNote come un intero. Questo è veloce, ma dovrebbe essere facile da tradurre:

enum Note: Int {
    case c, d, e, f, g, a, b
}

struct OctiveNote {
    let rawValue: Int

    init?(rawValue: Int) {
        precondition(rawValue >= 0)
        self.rawValue = rawValue
    }

    init(n: Note, o: Int) {
        precondition(o >= 0)
        rawValue = o * 7 + n.rawValue
    }

    var note: Note {
        return Note(rawValue: rawValue % 7)!
    }

    var octive: Int {
        return rawValue / 7
    }
}

Con quanto sopra, le tue funzioni di pitch sono semplicemente addizioni e sottrazioni.

extension OctiveNote {
    func pitched(byTones tones: Int) -> OctiveNote {
        precondition(rawValue + tones >= 0)
        return OctiveNote(rawValue: rawValue + tones)!
    }
}

Ora puoi:

var n = OctiveNote(n: .a, o: 5) // n = A5
n = n.pitched(byTones: 1)       // n = B5
n = n.pitched(byTones: 1)       // n = C6
n = n.pitched(byTones: 1)       // n = D6
n = n.pitched(byTones: -3)      // n = A5
    
risposta data 27.12.2017 - 23:06
fonte
0

Puoi sostituire PitchUp e PitchDown con una singola funzione. Aggiungi un secondo parametro che indica la direzione in cui cambiare il tono. Ad esempio:

public enum PitchDirection
{
    Up,
    Down
}

public OctaveNote ChangePitch(byte tones, PitchDirection direction)
{
    byte octaveShift = (byte)(tones / 7);
    byte newOct = direction == PitchDirection.Up
                    ? (byte)(o + octaveShift)
                    : (byte)(o - octaveShift);

    char noteShift = (char)(tones % 7);
    char temp = direction == PitchDirection.Up
                    ? n + noteShift
                    : n - noteShift;
    Note newNote = (Note)((((int)(temp) % 7) + 7) % 7);
    if (direction == PitchDirection.Up && newNote < n)
    {
        newOct++;
    }
    if (direction == PitchDirection.Down && newNote > n)
    {
        newOct--;
    }

    return new OctaveNote(newNote, newOct);
}

Non ho provato questo (l'ho appena estratto dalla cima della mia testa), ma dovrebbe indirizzarti nella giusta direzione.

    
risposta data 27.12.2017 - 19:38
fonte

Leggi altre domande sui tag