Codifica Dell'Altezza
La notazione di Helmholtz usa apici e pedici per indicare una nota alzata o abbassata di un' ottava e usa le lettere maiuscole solo per le 3 ottave più basse.
Pitch Class -> ignora l'ottava (altezza %12).
Pitch Class Interval -> codifica intervalli, numero che indica l'intervallo tra due PC in semitoni.
Inversione dell'intervallo -> (12-PCI)%12.
Continuous Pitch Class -> tiene conto dell' ottava, si soma al PC l'ottava*12.
Continuous Pitch Class Interval -> differenza tra CPC, con segno.
Name Class -> solo nome delle note (%7), ignorando ottave e alterazioni.
Continuous Name Class -> considera l'ottava.
Interval Class -> minore tra PCI e inverso, è un valore minore uguale a 6.
Trasposizione -> sempre in positivo (12-n), Tn=(PC+n).
Binomial Rapresentation -> per avere piú informazioni <PC,NC>, dove PC indica la dimensione in semitoni e NC la scrittura. (PC*10)+NC.
Gli intervalli hanno distanza e specie.
Per rappresentare anche l'ottava si possono usare le coppie CPC e CNC.
Continuous Binomial Rapresentation -> aggiunge al BR le migliaia per rappresentare l'ottava La trasposizione puó essere diatonica o cromatica.
Si puó operare sulla rappresentazione binomiale (che puó rappresentare sia note che intervalli) -> <a,b>+<c,d>=<(a+c)%12,(b+d)%7>
Si puó calcolare l'intervallo con l'operazione di sottrazione.
L'inversione si fa con <12,7>-<a,b>.
L'inversione melodica rispetto a una nota binomiale si fa con <2PC,2NC>-<a,b>.
Codifica Della Durata
In termini relativi (frazioni) o assoluti (durata in secondi).
BPM permette di passare da una codifica relativa a una assoluta, fissato il valore ritmico di una pulsazione. BPM=60s/pulsazione.
Le pause possono essere rappresentate come note degeneri, usando il segno "-".
Metodi per la rappresentazione numerica relativa:
1. Reciprocal Duration Code: 1/n. Permette valori inesistenti (1/13) e non consente alcuni valori esistenti come punti o legature che non sono esprimibili come frazione.
2. Codifica del risultato della frazione: consente di esprimere valori non esistenti ed é complicato risalire alla scrittura originaria.
3. Struttura dati con frazione e informazioni aggiuntive.
Problematiche: gruppi irregolari, punti e legature (codificare il risultato o la presenza del segno).
Programmazione Ad Oggetti
Una classe può ereditare da una sola classe, ma può implementare più interfacce.
Un'interfaccia può ereditare da più interfacce (con extends).
Le classi abstract possono contenere metodi astratti. Tali classi non possono essere istanziate.
Enumerazioni
public enum nome {val1,...,valn}
enum.values()[i]
valore.ordinal()
Liste E Mappe
Il tipo Generics rende metodi e classi indipendenti dal tipo di dato.
public class Nome<ClasseTipo1,ClaseTipo2>{}
Liste<> (java.util.Collectons): HashSet (nessun ordine), TreeSet (ordine per valore), ArrayList (ordine per inserimento).
Metodi: add,remove,contains,clear,size,isEmpty. Per TreeSet: first,last.
Per ArrayList: add(i,e),set(i,e),get(i),indexOf(e),remove(i).
Mappe<> (java.util.Map): HashMap, TreeMap (chiavi ordinate).
Metodi: put(i,v),remove(e),containsKey(i),containsValue(e), entrySet(),keySet(),values(),clear(),size(),isEmpty(). Per TreeMap: firstEntry(),firstKey(),lastEntry(),lastKey().
Nelle mappe si cicla con K k: map.keySet() e map.get(k) o V v: map.values().
File
La classe
Fileopera su file e directory, ha
File.pathSeparatore
File.separator. Metodi:
canExecute,canRead,canWrite,delete,isDirectory,isFile,exists, isHidden.getName,getParent,getPath,lastModified,length,list
.
La classe
FileReadercon
read(buffer)e
read(buffer,offset,length)legge testo carattere per carattere.
La classe
BufferedReaderlegge flussi di caratteri:
readlegge un carattere,
readLine
una riga,
skip(n)salta n caratteri nella lettura.
BufferedReader(new FileReader(new File("path")));
Lettura con while(line=br.readLine!=null)
Alla fine le risorse allocate per I/O devono essere rilasciate con
close.
Try-with-resources
lo fa automaticamente.
try(//dichiarazzione lettori ){//lettura } [catch/finally {}]
Per la scrittura si usano le classi
BufferedWriter(che ha anche il metodo
newLine)
e
FileWriter.
XML
In Java ci sono più classi che gestiscono lettura e scrittura di file XML.
DOM parser/builder carica l'intero documento in memoria e si basa sull'utilizzo della struttura ad albero del DOM.
DocumentBuilderFactory è una classe che istanzia un parser XML che produce alberi DOM a partire da un file XML.
E' dichiarata come classe astratta perché con newInstance() (chiamata sul parser creato) viene scelto il tipo di implementazione.
Altri metodi sono newDocumentBuilder() e setValidating(t/f).
DocumentBuilder è una classe che consente di ottenere istanze Document a partire da file XML, attraverso il metodo parse(file) chiamato sull'oggetto istanziato;
Ogni oggetto nel DOM è un Nodo. Tipi di nodo: document, comment, element, attribute, text.
L'interfaccia Node ha i metodi getAttributes(), getChilNodes(),
getFirst/LastChild(),getNext/PreviousSibling(),getNodeName/Value(), getOwnerDocument(),getParentNode(),getTextContent() restituisce il testo del nodo e dei suoi discendenti.
L'interfaccia Document estende la classe Node e implementa metodi per leggere e scrivere nodi.
Metodi: getDoctype() restituisce il DTD, getDocumentElement() restituisce il nodo radice, getElementsByTagName() restituisce una NodeList.
Anche le interfacce Element e Attr estendono Node. Metodi:
getAttribute(),getAttributeNode(),hasAttribute(),getElementsByTagName() per Element.
getValue() per Attr.
Scrittura su file XML: si istanziano gli oggetti factory, builder e un documento con cui si crea un file (anche vuoto) a cui si possono aggiungere nodi.
Metodi di Document: createAttribute(), createCDATASection() nodo che ha come valore la stringa passata, createComment(), createDocumentFragment() vuoto, createElement(), createTextNode().
A un nodo possono essere aggiunti figli con appendChild() e attributi con setAttribute().
Per creare un documento vuoto si può usare il metodo newDocument() della classe Document.
File file = new File("nuova_prova.xml");
Result output = new StreamResult(file);
Source input = new DOMSource(document);
TransformerFactory tf = TransformerFactory.newInstance();
Transformer t = tf.newTransformer();
// info di indentazione, opzionale
t.setOutputProperty(OutputKeys.INDENT, "yes");
t.setOutputProperty("{http://xml.apache.org/xslt}indentamount",
"3");
t.transform(input, output);
XPATH
E' un linguaggio per trovare informazioni su file XML.
Si compone di espressioni valutate in un contesto che restituiscono nodi.
Un'espressione è una combinazione di testo (token) separato da caratteri speciali (delimitatori).
Il processo di individuazione dei contenuti avviene per passi (location step) che messi insieme formano il location path. Ad ogni passo vengono esclusi dei nodi dalla selezione.
Per contesto si intende l'insieme dei nodi che ad un dato punto della location path sono selezionati.
La wildcard * permette di selezionare tutti i nodi del contesto.
Delimitatori: / per la localizzazione, [] per i predicati, operazioni di uguaglianza, ::
per l'asse, // se stesso o discendente, @ attributo,. se stesso, .. genitore , | unione di xpath, () sottoespressioni o argomenti , operazioni.
Tipi di dato: stringhe, tipo numerico (floating point) con valore speciale NaN, boolean, nodi.
Un node-set è un insieme di nodi identificati da un location step.
asse::nodetest[predicato]
Nodetest può essere in base al nome o al tipo: text(),node(),comment() oppure nome del nodo.
Assi: indicano la direzione verso la quale si esaminano i nodi precedentemente filtrati.
Ci sono 13 assi: child,parent,descendant,ancestor,descendant-or-self (//), ancestor-or-self,following,preceding,following-sibling, preceding- sibling,attribute(@),namespace,self.
Predicati: filtrano ulteriormente i nodi con espressioni del tipo valore1 operatore valore 2.
Un utilizzo particolare dei predicati è quello di verificare l'esistenza di un determinato nodo figlio del contesto attuale.
E' meglio esplicitare ogni step per contenere i tempi di calcolo.
http://www.freeformatter.com/xpath-tester.html
Funzioni Xpath
Accettano piú argomenti e si distinguono in base al loro tipo o al tipo di valore restituito.
Funzioni nodeset: agiscono su e restituiscono nodeset: last() come length, position(), count(nodeset) accetta xpath come parametro.
Si puó usare nodo[i] o nodo[last()] per accedere a elementi del nodeset.
Funzioni stringa: string() per la conversione in stringa (del primo elemento se è un nodeset), concat(stringhe),start-
with(stringa,val),contains(stringa,val), substring(stringa,da dove,quanto),substring-before/after(stringa,val),
string-length(),normalized-space().
Funzioni booleane: boolean() true per stringhe non vuote e numeri diversi da zero e NaN, not(),true(),false().
Funzioni numeriche: number() converte in tipo numerico se la stringa contiene al massimo uno spazio prima e dopo e un segno meno oltre al numero, un parametro booleano o un nodeset trasformato in stringa, sum(nodeset) somma dei valori dei nodi convertiti in tipi numerici, floor(),ceiling(),round().
In Java si usano le classi XPathFactory e XPath.
XPath myXPath = XPathFactory.newInstance().newXPath();
String myString = myXPath.compile(myExpr).evaluate(myXmlDoc);
Node myNode = (Node) myXPath.compile(myExpr).evaluate(myXmlDoc, XPathConstants.NODE);
NodeList myList = (NodeList) myXPath.evaluate(myExpr, myXmlDoc,XPathConstants.NODESET);
compile() effettua una precompilazione creando un XPathExpression in uscita.
evaluate() senza precompilazione restituisce un Object dopo aver effettuato compilazione e valutazione.
La precompilazione é utile quando l'espressione viene usata piú volte.
Il terzo parametro di evaluate puó essere XPathConstants.{NUMBER|STRING|BOOLEAN|
NODE|NODE_SET}
JSWING
JFrame è il container che ospita componenti.
setDefaultCloseOperation(Jframe.EXIT_ON_CLOSE|HIDE_ON_CLOSE|
DISPOSE_ON_CLOSE|DO_NOTHING_ON_CLOSE);
Si aggiungono componenti al container con il metodo add(Component).
Per sistemare i componenti nella finestra si usano i layout manager. Ad esempio si può indicare il BorderLayout (divide lo spazio in 5 parti) nel metodo add, oppure assegnarlo al contenitore con setLayout, come si fa per FlowLayout.
Un altro layout é GridLayout(row,col) che suddivide lo spazio in righe e colonne: i componenti vengono aggiunti ordinatamente. Inserendo zero righe zero colonne il sistema calcola la dimensione in automatico.
Prima di settare come visibile la finestra è opportuno chiamare il metodo pack. Le classi dei JComponents possono essere estese.
JFrame é un top-level container: é formato da un root pane (ospita barra dei menu e content) che ospita i livelli sottostanti layered, content (ospita i contenuti visivi, getContentPane()) e glass.
Si può modificare il font dei componenti con
setFont(new Font(nome,Font.BOLD/ITALIC/PLAIN,size)).
Si può modifica il colore dei componenti con setBackground() e setForeground() passando come parametro la classe Color.
Componenti
• JLabel
• JTextField
• JButton
• JCheckBox
• JComboBox
• Jpanel
• JScrollPane
• JOptionPane [showMessageDialog,showConfirmDialog,showInputDialog] Nel metodo per chiamarli si indicano almeno il componente e l'oggetto.
Si puó indicare anche il tipo di messaggio tra
ERROR/INFORMATION/WARNING/QUESTION/PLAIN_MESSAGE.
Per il ConfirmDialog ci sono i valori di ritorno YES/NO/CANCEL/OK/CLOSED_OPTION e di tipo YES_NO/YES_NO_CANCEL/OK_CANCEL_OPTION.
Per l'InputDialog vanno inseriti anche i valori selezionabili e la selezione di default.
• JFileChooser
Metodi: getSelectedFile/s, setFileFilter, setFileSelectionMode, setMultiSelectionEnabled, show[Open/Save]Dialog che restituisono CANCEL/APPROVE/ERROR_OPTION.
• JList
Gli elementi vengono memorizzati in un modello (DefaultListModel, add[Element]) Metodi: setModel,setSelectionMode, getSelectedIndices/ValuesList,
isSeletectedIndex, setelectedIndices, setSelectedValue, ensureIndexIsVisible.
Eventi: addListSelectionListener.
Eventi
Bisogna preparare la classe a ricevere gli eventi (implementando l'interfaccia
ActionListener) , indicare quali eventi (aggiungendo il listener al componente, come addActionListener) e il modo in cui rispondere (implementando il metodo
actionPerformed(ActionEvent e) dell'interfaccia).
Implementando le risposte agli eventi, si può usare il metodo e.getSource() per ottenere l'oggetto che ha scatenato l'evento. E' utile per filtrare le azioni su certi componenti (su una certa istanza o su un tipo di componente).
Il metodo setEnabled() abilita gli eventi su un componente.
Si possono associare listeners ai singoli componenti con addActionListener() a cui viene passato come argomento new ActionListener(){ public void
actionPerformed(ActionEvent e){ }}.
Action é un tipo di evento, per gli altri tipi il procedimento é lo stesso.
Il listener implementa l'interfaccia relativa a un tipo di evento attraverso specifici event handler.
Per implementare soltanto alcuni handler di interfacce che hanno diversi, si puó utilizzare la classe astratta Adapter che fornisce un'implementazione vuota dell'interfaccia.
MouseEvent ha i metodi
getButton(),getClickCount(),getX/Y(),getX/YOnScreen().
Grafica
Un componente viene ridisegnato automaticamente attraverso il metodo paint(Graphics). L'oggetto Graphics puó essere controllato per modificare il disegno di un componente facendo l'override di paint.
Il metodo paint viene chiamato indirettamente dal metodo repaint(), che ridisegna il componente e quelli contenuti.
setLocation() e getLocation() permettono di gestire la poszione dei componenti.
setLocation deve essere invocato dopo l'allocazione dei componenti da parte del layout manager.
paintComponent(), nei componenti non top level container, evita di occuparsi di aspetti di basso livello.
Nel caso di JPanel, paintComponent disegna sotto i componenti, paint disegna sopra.
Le operazioni di ridisegno possono essere effettuate all'esterno del metodo paint con getGraphics(), anche se è sconsigliabile perché in certe occasioni il sistema invoca automaticamente repaint annullando le modifiche.
drawString() permette di disegna una stringa di testo su un componente e nel suo costruttore possono essere indicate le coordinate, che a differenza di setLocation si riferiscono all'angolo in basso a sinistra anziché in alto a sinistra.
Su queste stringhe possono essere usati i metodi setFont e setColor (una volta settato il colore questo influisce sui successivi metodi di disegno).
Metodi per il disegno: drawLine/Rect/RoundRect/Oval/Arc, clearRect,
fillRect/RoundRect/Oval/Arc. Gli archi usano angoli espressi in gradi sessagesimali.
Il metodo getFontMetrics() di Graphics permette di ottenere un oggetto che contiene informazioni sul font.
Graphics2D migliora Graphics aggiungendo anche impostazione del tratto e antialiasing. Si puó castare partendo da Graphics di paint, paintComponent e getGraphics.
Impostazione del tratto: setStroke(Stroke) selezione il tipo di linea per le operazioni di disegno successive.
Stroke é un interfaccia implementata da BasicStroke (new BasciStroke([width, cap, join ])).
cap e join indicano terminazione e giunzione tra linee e possono essere:
BasicStroke.CAP_BUTT/CAP_SQUARE/CAP_ROUND/JOIN_ROUND/JOIN_BEVEL/JOI N_MITER.
Antialiasing: usando un oggetto di RenderingHints si puó controllare la qualitá del disegno.
Si possono specificare suggerimenti perché le modalitá potrebbero non essere supportate dalle piattaforme.
((Graphics2D)g).setRenderingHints(new RenderingHints
(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));
Thread
In Swing esiste un thread iniziale che contiene il main.
Un thread event dispatch (EDT) si occupa di gestire eventi e interazioni con l'utente.
Per far gestire MyJFrame dall'EDT:
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
new MyJFrame();
} });
La classe astratta SwingWorker viene usata per le operazioni in background.
SwingWorker worker = new SwingWorker<T, V>() {
@Override
public T doInBackground() {
//operazioni del task // Start
publish("Start");
setProgress(1);
// More work was done
publish("More work was done");
setProgress(10);
// Complete
publish("Complete");
setProgress(100);
return 1;
}
@Override protected void process(List<String> chunks) { // Messaggi ricevuti da doInBackground() // (all’invocazione del metodo publish()) }
@Override protected void done() {
//metodo chiamato dall'EDT, evita conflitti con strutture non thread-safe
// operazioni in conclusione di doInBackground }
};
worker.execute();
(T result = worker.get()) // attende la fine dell'esecuzione
Il thread del worker puó informare sull'esecuzione del task con setProgress(), done() e publish().
Dall'EDT viene invocato process() che raccoglie le informazioni di publish() per non appesantire il worker.
I metodi cancel(), isCancelled(), isDone() servono a gestire lo stato del worker.
Per recuperare informazioni sullo stato del worker dall'esterno:
worker.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent event) { switch (event.getPropertyName()) {
case "progress":
// Valore del progress:
(Integer)event.getNewValue() break;
case "state":
switch ((StateValue) event.getNewValue()) { case PENDING: // Stato iniziale
break;
case STARTED: // Prima di eseguire doInBackground break;
case DONE: // Task concluso try {
Object o = worker.get();
...
} catch (InterruptedException ex) { // Task interrotto
} catch (ExecutionException ex) {
// Errore nell'esecuzione del task }
break;
} break;
} }});
Timer
Timer é uno oggetto utile a impostare nel futuro l'esecuzione di una certa operazione.
E' una classe thread-safe (piú thread possono lavorare sullo stesso oggetto senza collidere).
TimerTask serve a specificare l'operazione da eseguire attraverso Timer, che implementa il metodo run.
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() { new MyTask()
@Override
public void run() {
System.out.println("Task eseguito");
}
}, 5000, 5000);
L'esecuzione puó essere fixed-delay (schedule(TimerTask,delay,[period])) o fixed-rate (scheduleAtFixedRate(TimerTask,delay,period)).
Nel primo caso ogni esecuzione successiva é schedulata a partire dalla precedente, nel secondo caso ogni esecuzione successiva é schedulata a partire dalla prima esecuzione.
Il metodo Timer.cancel() termina l'esecuzione dei task programmati senza interrompere quello in esecuzione, dopodiché il timer non puó essere piú utilizzato per altri task.
I task devono svolgere operazioni semplici entro il tempo indicato per evitare di creare code.
Audio
Ciascun valore di un segnale discreto é chiamato campione.
Quando il segnale discreto è composto da valori ottenuti da istanti a intervalli regolari si dice che è associato a una frequenza di campionamento.
Un segnale digitale, oltre a essere discreto, può assumere soltanto valori appartenenti a un insieme discreto, attraverso la quantizzazione (su n bit).
Bit Depth è il numero di bit utilizzati per la quantizzazione.
La quantizzazione provoca una perdita di informazione.
PCM: è una rappresentazione digitale di un segnale analogico senza compressione dei dati.
WAV è una variante di RIFF, quindi basato su header e chunk (strutture dati con formato comune).
Questi file possono contenere diversi formati audio, tra cui PCM.
I dati nel formato WAV vengono memorizzati in little endian.
Il formato canonico dei file WAV prevede un header seguito da una sequenza di chunk.
Nei formati RAW esiste solo il chunk di dati, non c'é un header.
Lettura di un file binario:
InputStream input=new FileInputStream(file);
Int[] audioData=new int[(int) file.length()];
For(int i=0;i<file.length;i++) audioData[i]=input.read();
Lettura di campioni da file:
DataInputStream data=new DataInputStream(input);
Int bufferSize=(int)(fileSize*8/nbits);
Double[] result=new double[bufferSize];
//lettura della quantità opportuna di bit
Questo metodo vale per valori a 8bit o in big endian.
Negli altri casi bisogna leggere lo stream byte per byte riordinandoli:
Byte[] bytes={…}; //caricamento byte x byte
Short[] samples=new short[bytes.length/2]; //2=bitDepth/8
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer ().get(samples);
Scrittura di campioni su file:
FileOutputStream fos=new FileOutputStream(file);
Short[] samples={…};
Byte[] bytes=new byte[samples.length*2];
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer ().put(sample);
Fos.write(bytes);
Fos.close();
Una quantizzazione a 8 bit utilizza interi positivi, una a 16 bit in complemento a 2.
In Java, byte é con segno, quindi bisogna utilizzare short.
Gli short in little endian si convertono in byte utilizzando gli operatori >> per shiftare i bit e &
per confrontare con 0xFF.
Sintesi Digitale Del Suono
Fissata una frequenza di campionamento e una forma d'onda, bisogna specificare frequenza e ampiezza.
2^BitDepth/2-1 é il valore massimo ottenibile con BitDepth bit.
Processori Di Dinamica
Operazioni sulla dinamica:
• Regolazioni di volume e normalizzazione: la normalizzazione consiste nel rendere massimo il livello di un segnale audio senza introdurre distorsione.
• Compressione/espansione della gamma dinamica in base a una threshold: utilizzo di una funzione non distorcente per effettuare il waveshaping.
• Clipping
• Foldback
Il waveshaping si basa sulla lettura di una tabella che contiene una funzione distorcente.
Se la funzione é una retta non ha luogo distorsione: se la retta é a
π
/4, l'uscita é uguale all'entrata (f(x)=x).Nel caso di f(x)=ax+b, il fattore a comprime o espande il segnale, mentre il fattore b viene aggiunto un offset.
Se la funzione non é una retta si ha una distorsione, quindi vengono introdotte nuove frequenze.
Un espansore é un dispositivo che applica un fattore a>0, altrimenti si parla di compressori.
Gli effetti di compressione vengono utilizzati per esempio per neutralizzare l'attacco delle note, eliminando l'effetto di corda pizzicata sulle chitarre.
Si utilizzano anche per evitare che i picchi distorcano il sistema.
Spesso dopo la compressione si usa un fattore moltiplicativo correttivo per compensare la diminuzione globale del volume.
Gli espansori conferiscono aggressivitá al suono ampliando la gamma dinamica di un segnale schiacciato.
Tra le infinite funzioni non lineari che possono definire il comportamento di un compressore, ci sono alcune definizioni standard:
• La soglia di compressione é il livello in cui un guadagno unitario si trasforma in una retta con pendenza minore, avente un rapporto di compressione a:1.
• La soglia di limitazione é il livello oltre il quale la retta assume pendenza nulla.
Il volume puó variare anche in funzione del tempo, come per il fade-in e il fade-out.
Quando il segnale satura i bit di quantizzazione e non può essere amplificato ulteriormente, ha luogo un taglio e il segnale è detto essere in clipping.
Il segnale extra può essere tagliato, con una risultante tipica assimilabile all'onda quadra.
Questo è quello che succede nell'effetto overdrive.
Il foldback è un'altra forma di distorsione che attua un taglio della forma d'onda a un livello prestabilito di altezza. La forma d'onda in eccesso non viene eliminata ma viene riflessa specularmente.
Sintesi Wavetable
E' utile a riprodurre segnali periodici mediante la ripetizione di una certa forma d'onda: sintesi con forma d'onda fissa.
Il modulo che la realizza é l'oscillatore digitale.
I valori di ampiezza corrispondenti alla discretizzazione possono essere generati tramite funzioni matematiche o letti da una wavetable con il metodo table lookup. Possono essere usate anche wavetable multiple.
E' possibile fissare un numero di punti della tabella, tale numero definisce la raffinatezza della discretizzazione.
La frequenza é determinata dalla velocitá con cui l'oscillatore legge la tabella.
frequenza di campionamento /frequenza del segnale = punti per oscillazione
Se la tabella non contiene abbastanza punti si puó usare il metodo dell'interpolazione.
L'interpolazione diminuisce l'errore di quantizzazione ma richiede tempi di calcolo maggiori.
L'uso di wavetable é una tecnica onerosa in termini di occupazione di memoria, per questo vengono impiegate per suoni singoli e di breve durata.
Una wavetable puó contenere la discretizzazione di un forma d'onda non ciclica.
Se il suono tabulato non é ciclico, la sua discretizzazione é legata a una data frequenza.
Modulazione
Il segnale modulato é detto portante, quello che modula é detto modulante.
Modulazione d'ampiezza: se le frequenze dei due segnali sono abbastanza alte compaiono frequenze laterali.
La modulazione ad anello é una variante della modulazione d'ampiezza e si ha quando il segnale modulante é bipolare.
Il segnale modulante della modulazione di ampiezza si ottiene sommando una costante DC Offset.
Le frequenze laterali che si ottengono sono fp-fm e fp+fm.
Indice di modulazione I=Am/Ap
Quando I=0 é presente solo la portante, quando I=1 la portante é modulata al 100% e gli inviluppi si toccano in un punto, quando I>1 gli inviluppi si sovrappogono
(sovramodulazione).
Nella modulazione ad anello se il segnale modulante é nullo non viene trasmessa neanché la portante perché non c'é DC Offset.
Il risultato di una RM contiene solo le frequenze laterali.
VAM=[Ap+Am·cos(2πfm·t)]·cos(2πfp·t)
Modulazione di frequenza: il segnale modulante bipolare viene sommato alla frequenza del segnale portante.
La deviazione di picco é il massimo mutamento di frequenza che l'oscillatore portante subisce.
Viene generata una serie di coppie di bande laterali date dalle armoniche della modulante.
fp± kfm
Maggiore é la deviazione di picco, piú le bande laterali sono udibili.
VFM=Ap·cos[2πfp·t+Am·cos(2πfm·t)]
Se la frequenza modulante é molto bassa non compaino nuove frequenze ma:
Nel caso della modulazione d'ampiezza si ha il tremolo (soglia a 10Hz).
Nel caso della modulazione di frequenza si ha il vibrato.
Sintesi Additiva
Somme di onde fondamentali per ottenerne una complessa.
La sintesi additiva é una tecnica che crea timbri, cioé forme d'onda complesse, sommando singole forme d'onda semplici, generalmente sinusoidali.
Si basa sul modello di Fourier, secondo il quale ogni funzione periodica puó essere rappresentata come combinazione lineare di funzioni sinusoidali.
Piú sono i termini presi in considerazione nella serie di Fourier, migliore è l'approssimazione dell'onda periodica.
Nel mondo reale i segnali non sono mai periodici, quindi il teorema si applica a porzioni di suoni, su suoni quasi periodici.
La sintesi additiva a spettro fisso si basa sulla decomposizione di un suono periodico complesso in una somma di oscillazioni sinusoidali semplici che non variano nel tempo, cioè i cui parametri degli oscillatori non variano nel tempo.
Il rapporto tra le frequenze degli oscillatori rispetto a una frequenza base può originare un valore armonico o inarmonico.
Variando l'ampiezza delle componenti armoniche nel tempo si può ottenere varietà timbrica.
Le componenti inarmoniche sono importanti perché possono aggiungere interesse a un dato timbro.
Anche la variazione della frequenza degli oscillatori crea spettri variabili, come il passaggio da componenti inarmoniche ad armoniche.
JSyn
E' una libreria per la sintesi del suono, il cui paradigma é quello delle unitá singole che possono essere connesse tra loro per creare suono e ambienti complessi.
Comprende classi dedicate alle componenti swing.
Synthesiser synth=JSyn.createSynthesizer();
synth.start([frameRate],[inputID],[inputCh],[outputID],[outputCh]);
synth.stop();
Si possono selezionare gli ID dei dispositivi usando l'interfaccia AudioDeviceManager o usando AudioDeviceManager.USE_DEFAULT_DEVICE.
Per produrre suoni bisogna creare gli UnitGenerator, che possono essere di diversi tipi e uno dei piú importanti é LineOut, che si occupa di mandare l'audio in output.
SineOscillator osc=new SineOscullator();
synth.add(osc);
LineOut lineOut=new LineOut();
synth.add(lineOut);
Esiste anche il metodo remove(UnitGenerator).
Gli UnitGenerator possono essere connessi utilizzando le proprietá input e output.
Se a un input sono collegati piú output, vengono sommati tra loro.
Per gestire le parti multiple delle porte, si specificano quali devono essere usate come parametri.
osc.output.connect(0,lineOut.input,0);
Per disconnettere gli UnitGenerator si usa disconnect() o disconnectAll().
Gli UnitGenerator, oltre a input e outport, possiedono altre porte che controllano la loro operativitá e possono essere impostate con il meotodo set().
Inoltre possono essere usate per impostare i valori delle porte output di altri UnitGenerator. osc.frequency.set(valore);
Per avviare gli UnitGenerator, basta avviare l'ultimo della catena con start().
Temporizzazione
JSyn incorpora un timer interno che parte con start() e di cui si puó conoscere il valore attraverso getCurrentTime().
Il processo di generazione del synth puó essere bloccato in esecuzione con sleepFor e sleepUntil, gestendo InterruptedException.
Per schedulare le informazioni si usano dei metodi (start,stop,noteOn,noteOff,set) che ricevono in input parametri TimeStamp.
TimeStamp timeStamp = synth.createTimeStamp();
TimeStamp futureTime = timeStamp.makeRelative(5.0);
osc.frequency.set(220.0, futureTime);
E' buona normale distanziare start di synth da start lineOut di mezzo secondo per eludere eventuali tempi di inizializzazione.
Campioni
Per utilizzare campioni audio si usa la calsse AudioSample e le sottoclassi FloatSample e ShortSample.
Per caricare i campioni si usa SampleLoader.loadFloatSample(File|InputStream|URL). Si possono anche creare algoritmicamente usando i costruttori di FloatSample.
Per suonare un campione audio si usano le classi VariableRateMono|StereoReader.
VariableRateMonoReader samplePlayer = new VariableRateMonoReader();
samplePlayer.rate.set(sample.getFrameRate()*2); //legge al doppio del frame rate cioé produce un'ottava sopra
Queste classi leggono ad una frequenza variabile e interpolano i campioni vicini.
Hanno una porta speciale chiamata dataQueue che riceve campioni in ingresso per produrre un suono, con i metodi queue e queueLoop.
Quando si accodano, i campioni possono essere presi da più sorgenti e verranno suonati in ordine di inserimento.
Quando viene impostato un loop senza numero di ripetizione, esso continua fino a quando viene aggiunto un altro campione alla coda.
Inviluppi
Descrivono l'evoluzione di un parametro alla base di un UnitGenerator. Il caso più comune è l'inviluppo di ampiezza che varia nelle fasi ADSR.
La creazione di un inviluppo è un array di double che contiene coppie di valori che descrivono durata e valore, ovvero quanto tempo occorre per arrivare al valore specificato e valore a cui arrivare a partire dal frame precedente.
Una volta costruito l'array lo si passa come parametro ad un oggetto SegmentedEnvelope. Per usare un inviluppo si crea un lettore di inviluppo come VariableRateMonoReader e gli si accodano i dati di inviluppo.
L'output del lettore si collega all'input dello UnitGenerator. Altri UnitGenerator
Per usare l'input del segnale microfonico bisogna specificare il framerate e il numero di canali come parametro quando si avvia il metodo start.
Il microfono viene letto dal LineIn.
PitchDetector stima la frequenza del segnale.
Possiede le porte input, frequency, period e confidence (accuratezza presunta della stima della frequenza fra 0 e 1).
Esistono UnitGenerator dedicati a operazioni aritmetiche come Multiply che ha due porte input che vengono moltiplicate in uscita.
Circuit
E' una classe che serve a creare macrounitá che inscatolino un intero sistema composto da unitá collegate tra loro.
Si definisce una sottoclasse di Circuit all'interno della quale si definiscono le unitá incluse e nel costruttore vengono effettuate le operazioni preliminari:
1. Creazione delle istanze delle unità.
2. Aggiunta delle unitá al circuito.
3. Aggiunta di porte public di ingresso e uscita UnitInput|OutputPort.
Syntona
Serve a creare patch per JSyn per via grafica.
Permette di ascoltare direttamente il risultato ottenibile dalla patch e di generare il codice sorgente Java (File>Export Java Source).
In voice é presente la patch di generazione del suono che puó essere esportata.
JMusic
E' una libreria utilizzabile per comporre, analizzare musica o creare strumenti musicali sintetici.
Si possono importare ed esportare file audio e MIDI.
Le classi possono implementare JMC in modo da poter utilizzare agilmente numerose costanti.
http://explodingart.com/jmusic/jmDocumentation/constant-values.html
Le informazioni in JMusic sono strutturate nella gerarchia Score>Part>Phrase>Note. Phrase puó essere vista come singola voce di uno strumento.
Part accorpa più voci di uno strumento. Ha una descrizione testuale, un canale e uno strumento MIDI.
Nella classe Note, l'altezza é impostabile utilizzando pitch MIDI, costanti o frequenze.
• La durata è un valore double o una costante.
• Il volume varia tra 0 e 127 o una costante.
• Il pan è un valore double tra 0 e 1 e una costante e indica il bilanciamento destra/sinistra.
• Per le pause si può usare la costante REST oppure si può usare il costruttore Rest(). Per ogni parametro sono presenti i metodi get e set.
La classe Phrase può accettare in input uno tempo di inizio in double.
• Ha un flag append, di default a true, che fa sì che quando la frase viene aggiunta a una parte venga accodata.
• Ha un metodo clear, come Score, che rimuove le frasi vuote presenti.
Gli strumenti possono essere specificati sia per le frasi che per le parti e sono definiti dai loro nomi e costanti MIDI.
La classe Mod presenta dei metodi statici per la manipolazione di strutture musicali precedemente create.
MIDI E Grafica
La classe Play presenta alcuni metodi statici per suonare via MIDI le strutture create.
Il metodo midi riceve in input Note, Phrase, Part o Score e li riproduce.
Esiste anche il metodo audio.
La classe Write si usa per salvare le strutture su file con il metodo midi, che può ricevere in input il nome del file oltre che la struttura.
Il metodo au salva la struttura in un file audio.
La classe View permette di usare i suoi metodi statici per visualizzare finestre grafiche di rappresentazione delle strutture.
Questi metodi sono: histogram, internal, print, notate, notation, pianoRoll, show.
Creazione Strumenti
JMusic fornisce una serie di classi per la generazione e il trattamento di segnali audio.
Sul sito https://explodingart.com/jmusic/Instruments.html ci sono molti strumenti giá creati che possono essere importati nel progetto.
Per creare nuovi strumenti, il paradigma usato é quello della creazione di catene di moduli AudioObject che vanno a formare l'output finale.
Va dichiarata una classe che estende Instrument che implementa il metodo createChain, che contiene la sequenza di stadi che porta alla produzione del suono finale.
Ogni stadio, corrispondente all'instanziazione di una classe, per convenzione riceve in ingresso il primo parametro identificante lo stadio precedente a cui collegarsi.
L'utimo stadio é spesso un'istanza di SampleOut, che produce i dati finali sui campioni.
Esempi di AudioObject:
• Oscillator: puó essere usato come oggetto modulato da altri oggetti usando il parametro choice.
• Noise: puó generare vari tipi di rumore.
• Filter: specificando frequenza e tipo.
• Envelope: definisce inviluppi con punti EnvPoint connessi tramite linee.
• EnvPoint: singoli punti di inviluppo.
• Add: somma le unitá in ingresso.
• Volume: imposta il volume dei campioni prodotto dall'oggetto in ingresso.
• Value: usato per passare costanti alle unitá. Ha due costruttori che si diversificano per l'ultimo parametro che puó essere un float per passare un valore fisso o un int che viene legato a un parametro della nota suonata usando una costante.
Realtime
Nell'uso in realtime non si utilizzano le strutture classiche, ma solo la generazione di singole note.
La classe base da estendere é RTLine che riproduce direttamente le note anziché memorizzarle.
Il costruttore accetta in ingresso un array di strumenti assumendo che siano istanze dello stesso strumento.
Uno o piú RTLine devono confluire in un RTMixer che si occupa di fornire l'output audio, la cui generazione avviene richiamando il metodo begin.
Il metodo getNextNote (che restituisce Note) é quello che fornisce le note da suonare al motore realtime.
Per modificare i parametri relativi a una RTLine è possibile fare l'override di externalAction(Object,actionNumber).
L'Object è spesso una componente della GUI e actionNumber è un id che indica l'oggetto passato.
L'RTMixer informa l'RTLine che qualcosa è cambiato attraverso il metodo
mixer.actionLines(Object,actionNumber), chiamato per esempio dal gestore dell'evento della GUI.