Το προφίλ μας στο Google Plus
0

Κατασκευάστε έναν υπολογιστή, μέρος 6: Μουσικές γλώσσες

Το synthesizer είναι έτοιμο, αλλά η κάρτα ήχου συμπεριφέρεται σαν εκείνα τα καρτ-ποστάλ, που όταν τα ανοίγεις παίζουν ξανά και ξανά την ίδια μελωδία. Απαιτείται οπωσδήποτε ένας μηχανισμός για την επικοινωνία της κάρτας με τον υπολογιστή. Πολύ περισσότερο, χρειαζόμαστε ένα εργαλείο που θα επιτρέπει στους μουσικούς να συνθέτουν αριστουργήματα, ειδικά για την κάρτα μας!

Όπως συμβαίνει με την ανάπτυξη κάθε προγράμματος, το firmware του synthesizer δεν γράφτηκε μονομιάς. Χρειάστηκαν αμέτρητες δοκιμές, κατά τη διάρκεια των οποίων συγκεντρώναμε όλη μας την προσοχή στους παράξενους ήχους που έβγαζαν τα ηχεία. Κάθε φορά ζητούσαμε την αναπαραγωγή της κατάλληλης αλληλουχίας από νότες και μ’ αυτόν τον τρόπο τσεκάραμε τις διάρκειες και τους ρυθμούς, τις οκτάβες, τα εφέ, την ταυτόχρονη αναπαραγωγή σε πολλά κανάλια και πάει λέγοντας. Οι δοκιμαστικές μελωδίες τοποθετούνταν σε μια περιοχή της μνήμης flash κι αποτελούσαν τμήμα του προγράμματος (ήταν hard-coded). Με άλλα λόγια, για να τροποποιήσουμε έστω και μία νότα, έπρεπε να εγκαταστήσουμε νέο firmware στον μικροελεγκτή. Αυτός ο τρόπος εργασίας καθυστερούσε σημαντικά τις δοκιμές, αλλά δεν υπήρχε καμία εναλλακτική. Σ’ εκείνη τη φάση το κύκλωμα ήχου δεν μπορούσε να επικοινωνήσει με τον υπολογιστή. Κατά συνέπεια, δεν μπορούσε να λάβει πληροφορίες για κάποια νότα, ούτε και γενικότερες οδηγίες του στιλ “σταμάτα” ή “ξεκίνα” την αναπαραγωγή. Το ίδιο ζήτημα είχαμε αντιμετωπίσει και κατά την ανάπτυξη του κυκλώματος γραφικών. Τελειοποιώντας τον κώδικα που παρήγαγε το σήμα VGA, βρισκόμασταν σ’ ένα στάδιο όπου –τουλάχιστον θεωρητικά– μπορούσαμε να προβάλουμε στην οθόνη οτιδήποτε. Ωστόσο, το κύκλωμά μας εμφάνιζε μόνο ένα συγκεκριμένο μήνυμα, καρφωμένο στο firmware. Η κάρτα γραφικών ολοκληρώθηκε όταν προσθέσαμε τις ρουτίνες για την επικοινωνία με τον υπολογιστή. Έτσι συνέβη και με την κάρτα ήχου.

Τελική συναρμολόγηση
Αν και το καθυστερήσαμε, πιστεύουμε ότι πρέπει να πάρετε τον κώδικα που γράψαμε για την κάρτα ήχου. Το σχετικό πακέτο βρίσκεται στο http://bit.ly/dh053sndfirm. Σημειώστε ότι ο κώδικας είναι γραμμένος για τον assembler της ATMEL, που τρέχει μόνο σε Windows. Οι φίλοι που έχουν αποκλειστικά Linux, μπορούν να χρησιμοποιήσουν τον συμβατό assembler ονόματι avra.

Το synthesizer που εξετάσαμε στο προηγούμενο άρθρο αποτελεί το σημαντικότερο εξάρτημα της κάρτας ήχου. Κι αν λάβουμε υπόψη ότι πρόκειται για πρόγραμμα που τρέχει σ’ έναν μικροελεγκτή χρονισμένο στα 16MΗz, οι δυνατότητες που προσφέρει δεν είναι λίγες. Θα περίμενε κανείς ότι αυτή η διαπίστωση θα εκτόξευε το ηθικό μας στα ύψη. Ε, λοιπόν, στην πράξη μάς προκάλεσε έναν ελαφρύ πονοκέφαλο. Βρισκόμασταν στη φάση που το synthesizer δούλευε άψογα και ο ήχος του ήταν πεντακάθαρος (γκουχ, γκουχ), μόνο που η κάρτα ήχου δεν είχε γίνει ακόμα …κάρτα. Χρειαζόταν έναν κώδικα επικοινωνίας για να δέχεται εντολές από τον επεξεργαστή του υπολογιστή μας. Σε μια προσπάθεια να οργανώσουμε τη δουλειά και να περιορίσουμε τα μελλοντικά προβλήματα στην ανάπτυξη του κώδικα, ξεκινήσαμε καταγράφοντας τις ανάγκες μας.

Η κάρτα ήχου δεν μπορεί να αποφασίζει μόνη της πότε θα παίζει μουσική και πότε θα σταματά. Αυτό είναι αυτονόητο, αλλά έπρεπε να καταγραφεί για να ενταχθεί στο γενικότερο σχεδιασμό. Σκεφτείτε τώρα το εξής: Τι πρέπει να κάνει ο επεξεργαστής, όταν επενδύουμε μουσικά ένα πρόγραμμα; Μήπως πρέπει να στέλνει τις νότες ξεχωριστά κι όταν ολοκληρώνεται η αναπαραγωγή της μίας, να στέλνει την επόμενη; Αν συνέβαινε κάτι τέτοιο, η εκτέλεση μιας μελωδίας θα απαιτούσε την πλήρη προσοχή του επεξεργαστή και το σύστημα δεν θα μπορούσε να κάνει τίποτα άλλο. Περιττό να πούμε ότι αυτή η συμπεριφορά θα ήταν απαράδεκτη ακόμα και για τον ταπεινό υπολογιστή μας. Γι’ αυτό το λόγο καταστρώσαμε έναν μηχανισμό αποθήκευσης και διαχείρισης της μουσικής. Κατ’ αρχάς, χωρίσαμε τη διαθέσιμη μνήμη της κάρτας ήχου (αναφερόμαστε στην SRAM του atmega328) σε τέσσερεις περιοχές. Εκεί αποφασίσαμε ότι θα αποθηκεύονται οι νότες που στέλνει ο επεξεργαστής, ανάλογα με το κανάλι για το οποίο προορίζονται. Έτσι, το synthesizer θα παίρνει τις διαδοχικές νότες από τη μνήμη και η εκάστοτε μελωδία θα ξετυλίγεται χωρίς τη διαρκή παρέμβαση του επεξεργαστή. Μήπως αυτό σημαίνει ότι πρέπει να ακούγονται πάντα όλα τα κανάλια; Κάποιες μελωδίες θα αξιοποιούν ένα κανάλι, ενώ άλλες θα αξιοποιούν περισσότερα. Τα περισσευούμενα κανάλια δεν πρέπει να ακούγονται, ανεξάρτητα από το αν υπάρχουν νότες στις αντίστοιχες περιοχές της μνήμης. Αυτές οι νότες ενδέχεται να έχουν ξεμείνει από κάποια προηγούμενη μελωδία ή να έχουν τοποθετηθεί για κάποια μελωδία που θα παιχτεί αργότερα.

Η παραπάνω παράγραφος ενδέχεται να προκάλεσε ελαφρύ πονοκέφαλο και σε σας. Το σύστημα που πρέπει να στήσουμε όμως δεν διαφέρει ιδιαίτερα από εκείνο της κάρτας γραφικών. Η μόνη ουσιαστική διαφορά είναι ότι σ’ εκείνη την περίπτωση διαχειριζόμασταν έναν αποθηκευτικό χώρο (τη frame buffer), ενώ στην κάρτα ήχου θα έχουμε τέσσερεις τέτοιους χώρους. Κατά τα άλλα, η λογική των δύο προγραμμάτων δεν διαφέρει ιδιαίτερα. Ο κεντρικός βρόχος του προγράμματος βρίσκεται στο αρχείο snd-fw.asm και ξεκινά στη γραμμή 195. Το πρώτο πράγμα που συμβαίνει εκεί είναι η κλήση της get_byte. Η συγκεκριμένη συνάρτηση αναλαμβάνει το χειρισμό των ακροδεκτών, που εμπλέκονται στην επικοινωνία με τον επεξεργαστή. Η λειτουργία της τερματίζεται όταν ληφθεί κάποιο byte και η σχετική τιμή τοποθετείται στον καταχωριστή tmp1. Στη συνέχεια τσεκάρουμε αν η τιμή του tmp1 βρίσκεται εντός αποδεκτών ορίων. Οι εντολές που υποστηρίζονται είναι μόλις οκτώ και τις έχουμε αντιστοιχίσει στις τιμές από 200 έως 207. Αμέσως μετά χρησιμοποιούμε ένα jump list για να καλέσουμε την κατάλληλη συνάρτηση. Μπορείτε να διαβάσετε περισσότερα για τα jump lists στο τρίτο μέρος της σειράς μας, στο τεύχος 048. Ας ρίξουμε μια ματιά στις συναρτήσεις που περιλαμβάνει το jump list και θα κατανοήσετε πλήρως τη λογική του προγράμματός μας.

  • xenable / xdisable. Το όνομά τους δεν αφήνει περιθώρια παρεξήγησης. Αν εξετάσετε τον κώδικα των δύο συναρτήσεων, θα διαπιστώσετε ότι περιλαμβάνουν μια κλήση στη get_byte. Με αυτόν τον τρόπο λαμβάνουν ένα πρόσθετο byte από τον επεξεργαστή: την παράμετρο που δηλώνει ποιο κανάλι θα ενεργοποιηθεί ή απενεργοποιηθεί.
  • xclear. Η συγκεκριμένη συνάρτηση διαγράφει την περιοχή μνήμης που χρησιμοποιείται για τις νότες ενός καναλιού. Η απαιτούμενη παράμετρος –το κανάλι που θα διαγραφεί– λαμβάνεται και πάλι με τη βοήθεια της get_byte.
  • xtempo. Η εν λόγω συνάρτηση δεν κάνει τίποτα περισσότερο από το να λαμβάνει την τιμή του επιθυμητού ρυθμού και να την αποθηκεύει στον καταχωριστή rhythm. Εκεί ανατρέχει και ο μηχανισμός του synthesizer, για να πληροφορηθεί περί του επιλεγμένου tempo.
  • xnotes. Εδώ τα πράγματα περιπλέκονται λίγο, αλλά μην περιμένετε τίποτα φοβερό. Αποστολή αυτής της συνάρτησης είναι η λήψη μιας νότας και η αποθήκευσή της στην κατάλληλη περιοχή μνήμης, σύμφωνα με το κανάλι για το οποίο προορίζεται. Εδώ η get_byte καλείται τρεις φορές: Μία για τη λήψη του καναλιού και άλλες δύο για τη λήψη της νότας (όλες οι νότες περιγράφονται πάντα από δύο bytes). Ανάμεσα σ’ όλα αυτά θα εντοπίσετε και μια κλήση στη συνάρτηση find_marker, η οποία εντοπίζει την τελευταία νότα στην περιοχή μνήμης του επιλεγμένου καναλιού. Με τη βοήθειά της, μαθαίνουμε σε ποια διεύθυνση μνήμης πρέπει να αποθηκευτεί η νέα νότα.
  • xplay, xstop. Η πρώτη ξεκινά το synthesizer (καλεί τη συνάρτηση play), ενώ η δεύτερη δεν κάνει απολύτως τίποτα. Αυτό μπορεί να μοιάζει παράξενο, αλλά αν το καλοσκεφτείτε είναι αναμενόμενο. Εφόσον βρισκόμαστε στον κύριο βρόχο του προγράμματος, το synthesizer δεν έχει ξεκινήσει και η κάρτα ήχου δεν αναπαράγει τίποτα. Η εντολή της διακοπής δεν έχει κανένα νόημα σε αυτή τη φάση. Αν παρ’ όλα αυτά τη στείλει ο επεξεργαστής, το πρόγραμμα πρέπει να την αγνοήσει. Ο ουσιαστικός έλεγχος για τη λήψη αυτής της εντολής πραγματοποιείται μέσα στον κεντρικό βρόχο του synthesizer.

Ο κώδικας των παραπάνω συναρτήσεων βρίσκεται στο αρχείο api.asm. Για το τέλος αφήσαμε μια απορία δευτερεύουσας σημασίας: Αναρωτιέστε γιατί τα ονόματα των συναρτήσεων ξεκινούν με το γράμμα x; Γι’ αυτό ευθύνεται εν μέρει η παραξενιά του γράφοντα, αλλά υπήρχε κι ένας τεχνικός λόγος. Στην Assembly δεν γίνεται διάκριση μεταξύ των ονομάτων των συναρτήσεων και των labels μέσα στον κώδικα. Για να μην προκαλούνται μπερδέματα, ο προγραμματιστής οφείλει να χρησιμοποιεί διαφορετικά ονόματα. Το γράμμα x προστέθηκε για να αποφύγουμε το conflict με ετικέτες του τύπου play, stop, tempo κ.ά, που ήταν πολύ πιθανό να έχουμε χρησιμοποιήσει στον κώδικα του synthesizer. Ένα τέτοιο λάθος δεν θα περνούσε απαρατήρητο (ο assembler θα έσκουζε!) αλλά θεωρήσαμε φρόνιμο ν’ αποφύγουμε αυτά τα μπερδέματα μ’ έναν “ομοιόμορφο” τρόπο.

Ο κεντρικός βρόχος του firmware της κάρτας ήχου. Όπως και στην περίπτωση της κάρτας γραφικών, το πρόγραμμα περιμένει εντολές από τον επεξεργαστή. Όταν λαμβάνεται ένα byte πραγματοποιούνται ορισμένοι έλεγχοι, για να διαπιστωθεί αν πρόκειται για έγκυρη εντολή. Εφόσον όλα δείχνουν σωστά, αξιοποιείται ένα jumplist για την κλήση της κατάλληλης συνάρτησης. Χωρίς το jumplist, ο κώδικας θα περιλάμβανε μια (άκομψη) σειρά από διαδοχικά IF.

Ο κεντρικός βρόχος του firmware της κάρτας ήχου. Όπως και στην περίπτωση της κάρτας γραφικών, το πρόγραμμα περιμένει εντολές από τον επεξεργαστή. Όταν λαμβάνεται ένα byte πραγματοποιούνται ορισμένοι έλεγχοι, για να διαπιστωθεί αν πρόκειται για έγκυρη εντολή. Εφόσον όλα δείχνουν σωστά, αξιοποιείται ένα jumplist για την κλήση της κατάλληλης συνάρτησης. Χωρίς το jumplist, ο κώδικας θα περιλάμβανε μια (άκομψη) σειρά από διαδοχικά IF.

Προβλέψιμος θόρυβος
Στo προηγούμενο άρθρο της σειράς εξετάσαμε τη λειτουργία του synthesizer με κάθε λεπτομέρεια. Ωστόσο, πέρα από μια γρήγορη αναφορά στο κανάλι του θορύβου, δεν είπαμε τίποτα συγκεκριμένο για τη λειτουργία του. Όπως μπορεί να υποθέσει κανείς, τα δείγματα γι’ αυτό το κανάλι αποτελούν τυχαίες αριθμητικές τιμές. Απλά πράγματα, θα πείτε, μόνο που το πρόγραμμά μας είναι γραμμένο σε Assembly. Δεν υπάρχει καμία έτοιμη συνάρτηση που να λύνει αυτό το πρόβλημα. Πώς υπολογίζονται οι τυχαίες τιμές; Βλέπετε την αντίφαση στην προηγούμενη ερώτηση; Στην πραγματικότητα, τα δείγματα για το κανάλι του θορύβου δεν είναι πραγματικά τυχαία. Πρόκειται για τιμές που μεταβάλλονται κατά τέτοιον τρόπο, ώστε η διαδοχή τους να προσλαμβάνεται από την αντίληψή μας ως απρόβλεπτη. Ακριβώς γι’ αυτό, οι τιμές του είδους χαρακτηρίζονται ψευδοτυχαίες και οι μηχανισμοί που τις παράγουν ονομάζονται γεννήτριες ψευδοτυχαίων αριθμών (PRNG, από το Pseudo-Random Number Generator). Για την υλοποίηση ενός PRNG προσφέρονται πολλοί αλγόριθμοι. Εμείς επιλέξαμε κάποιον που απαιτεί ελάχιστους και απλούς υπολογισμούς, γεγονός που τον καθιστά ιδανικό για τις περιορισμένες επιδόσεις ενός μικροελεγκτή. Ο αλγόριθμος που υλοποιήσαμε ανήκει στη γενική κατηγορία LFSR (Linear Feedback Shift Register) και, πιο συγκεκριμένα, πρόκειται για έναν Fibonacci LFSR.

Αριθμητική αναπαράσταση
Παρουσιάζοντας τη συνάρτηση xnotes, αναφέραμε ότι κάθε νότα περιγράφεται πάντα από δύο bytes. Ωστόσο, η κοινή λογική λέει ότι για να προσδιορίσουμε έναν μουσικό φθόγγο πρέπει να δηλώσουμε νότα, οκτάβα και διάρκεια. Επιπρόσθετα, στην περίπτωση του synthesizer πρέπει να δηλώσουμε το επιθυμητό εφέ και, αν πρόκειται για τετραγωνικό κύμα, το duty cycle. Αναρωτιέστε πώς στριμώξαμε αυτά τα πέντε μεγέθη σε δύο bytes; Θυμηθείτε την περιγραφή των δυνατοτήτων του synthesizer. Τα εφέ που υποστηρίζονται είναι τρία κι αν προσμετρήσουμε την περίπτωση που δεν θέλουμε κανένα εφέ, έχουμε τέσσερεις διαφορετικές καταστάσεις. Προφανώς, για την περιγραφή τεσσάρων καταστάσεων επαρκούν ακριβώς δύο bits. Για το duty cycle γνωρίζουμε ότι το synthesizer υποστηρίζει δύο στάθμες. Επομένως, για τον προσδιορισμό του χρειαζόμαστε μόνο ένα bit. Τέλος, το πλήθος των υποστηριζόμενων αξιών (διαρκειών) ανέρχεται σε οκτώ, που σημαίνει ότι τρία bits αρκούν. Κάντε την πρόσθεση –αν δεν την έχετε κάνει ήδη– και θα διαπιστώσετε ότι όλες αυτές οι πληροφορίες απαιτούν έξι bits και συνεπώς χωράνε άνετα μέσα σε ένα byte. Τώρα, θυμηθείτε τον τρόπο με τον οποίο υπολογίζονται τα διαδοχικά δείγματα ενός κύματος. Για να σχηματίσουμε ένα κύμα συγκεκριμένης συχνότητας, αρκεί να επιλέξουμε μια κατάλληλη τιμή για το μέγεθος phase delta. Στο προηγούμενο άρθρο, αναφέραμε ότι μέσα στο αρχείο melody.asm (γραμμές 8 έως 95) υπάρχει ένας πίνακας που δίνει για κάθε νότα και για κάθε οκτάβα την αντίστοιχη τιμή του phase delta. Επομένως, για να περιγράψουμε νότα και οκτάβα, αρκεί να προσδιορίσουμε ένα στοιχείο του συγκεκριμένου πίνακα. Εφόσον όλες οι νότες και οι παραλλαγές τους είναι μόλις 82, ένα byte φτάνει και περισσεύει.

Το μπλοκ διάγραμμα ενός Linear Feedback Shift Register. Η συνάρτηση ανάδρασης περιλαμβάνει μόνο την πράξη XOR και γι' αυτό λέμε ότι πρόκειται για έναν Fibonacci LFSR. Σε κάθε βήμα εκτελείται η συνάρτηση ανάδρασης, ολισθαίνουν τα bits κατά μία θέση προς τα αριστερά και στη θέση του πρώτου bit τοποθετείται το αποτέλεσμα της πράξης. Η επιλογή των bits (taps) 14 και 15 δεν ήταν τυχαία. Σύμφωνα με ελέγχους που έχουν κάνει πολλοί πριν από εμάς, η χρήση των συγκεκριμένων taps μεγιστοποιεί το πλήθος των παραγόμενων αριθμών.

Το μπλοκ διάγραμμα ενός Linear Feedback Shift Register. Η συνάρτηση ανάδρασης περιλαμβάνει μόνο την πράξη XOR και γι’ αυτό λέμε ότι πρόκειται για έναν Fibonacci LFSR. Σε κάθε βήμα εκτελείται η συνάρτηση ανάδρασης, ολισθαίνουν τα bits κατά μία θέση προς τα αριστερά και στη θέση του πρώτου bit τοποθετείται το αποτέλεσμα της πράξης. Η επιλογή των bits (taps) 14 και 15 δεν ήταν τυχαία. Σύμφωνα με ελέγχους που έχουν κάνει πολλοί πριν από εμάς, η χρήση των συγκεκριμένων taps μεγιστοποιεί το πλήθος των παραγόμενων αριθμών.

Τώρα ενδέχεται να αναρωτιέστε γιατί μπήκαμε στον κόπο να συμπυκνώσουμε τόσο πολύ την περιγραφή κάθε νότας. Ο λόγος ήταν απλούστατος: Για να κερδίσουμε χώρο στη μνήμη κάθε καναλιού και, κατ’ επέκταση, για να χωράνε μεγαλύτερες μελωδίες. Αν μελετήσετε το περιεχόμενο του αρχείου melody.asm μπορεί να προκύψουν περισσότερα ερωτήματα. Στη γραμμή 148 ορίζουμε μια τιμή για το tempo και μάλιστα εις διπλούν. Από εκεί και κάτω, φαίνεται να έχουμε γράψει μια μελωδία που αξιοποιεί και τα τέσσερα κανάλια. Τι είναι όλ’ αυτά; Κατ’ αρχάς, αυτές οι τιμές αποτελούν μέρος του κώδικα και τοποθετούνται στη μνήμη Flash. Γι’ αυτό το λόγο άλλωστε χρησιμοποιήσαμε και το αρχικό γράμμα f στα ονόματα των ετικετών :) Όταν ξεκινά το πρόγραμμα, όταν ενεργοποιείται η κάρτα ήχου, εκτελείται η συνάρτηση meminit, η οποία μεταφέρει την τιμή του tempo στο σχετικό καταχωριστή (rhythm) κι αντιγράφει τις νότες από τη μνήμη Flash στις αντίστοιχες περιοχές της μνήμης SRAM. Με αυτό το κολπάκι, κάθε φορά που ενεργοποιείται ο DIY υπολογιστής, η κάρτα ήχου φορτώνει μια προεπιλεγμένη μελωδία. Πρόκειται για τη μελωδία του Bubble Bobble και την τοποθετήσαμε για δύο λόγους: Αφενός επειδή αποτελεί μια καλή επίδειξη των δυνατοτήτων του synthesizer κι αφετέρου επειδή μας αρέσει. Τέλος, η αναφορά του tempo εις διπλούν έχει να κάνει με τον τρόπο που δεσμεύεται η μνήμη Flash. O assembler προσπελαύνει τη μνήμη ανά δύο bytes (ανά word των 16bit). Αυτό σημαίνει ότι οι εντολές που δεσμεύουν χώρο για την αποθήκευση αυθαίρετων τιμών (όπως είναι η .db) πρέπει να συνοδεύονται πάντα από άρτιο πλήθος παραμέτρων. Με λίγα λόγια, ήμασταν υποχρεωμένοι να δώσουμε δύο τιμές και αποφασίσαμε να δώσουμε την ίδια εις διπλούν.

Μουσική γραφή
Εσείς προλάβατε την Quick BASIC; Σ’ αυτή την εκδοχή της BASIC, όπως και σε αρκετές ακόμα, ο προγραμματιστής μπορούσε να συνθέσει ήχο με τη βοήθεια της play. Η εν λόγω εντολή δεχόταν σαν παράμετρο ένα string, το περιεχόμενο του οποίου περιέγραφε την επιθυμητή μελωδία. Σ’ αυτό το string επιτρεπόταν η χρήση συγκεκριμένων χαρακτήρων που ερμηνεύονταν είτε σαν νότες είτε σαν διακόπτες (switches), για την επιλογή οκτάβας, διάρκειας, ρυθμού κ.λπ. Ουσιαστικά, για τη σύνταξη αυτού του string ίσχυαν κάποιοι κανόνες, που στο σύνολό τους αποτελούσαν μια γλώσσα περιγραφής της μουσικής (MML από το Music Macro Language). Αν θέλετε να πάρετε μια γεύση αυτής της γλώσσας, μπορείτε να διαβάσετε την τεκμηρίωση για την εντολή play της Quick BASIC (https://en.wikibooks.org/wiki/QBasic/Appendix#PLAY). Μια τέτοια γλώσσα δημιουργήσαμε κι εμείς για την BASIC που τρέχει στον υπολογιστή μας. Κατ’ αρχάς, ορίσαμε τις εντολές mplay και mstop, που είναι πασιφανές τι κάνουν. Επιπρόσθετα, δημιουργήσαμε την εντολή tempo, που δέχεται μια παράμετρο (60, 120, 150 ή 180) και ορίζει το ρυθμό αναπαραγωγής. Το περισσότερο ενδιαφέρον συγκεντρώνεται στην εντολή music, η οποία, όπως και η play, δέχεται σαν παράμετρο ένα ολόκληρο string. Εδώ μπαίνει στο παιχνίδι η MML που επινοήσαμε. Το string έχει πάντα τη μορφή “cmd1 cmd2 … cmdk”, όπου το cmd μπορεί να είναι μία από τις ακόλουθες εντολές:

  • Ax: ενεργοποίηση του καναλιού x
  • Dx: απενεργοποίηση του καναλιού x
  • Cx: διαγραφή του περιεχομένου του καναλιού x
  • Mx OND1 OND2 … ONDk: εισαγωγή στο κανάλι x των μουσικών φθόγγων που περιγράφονται από τις τριάδες OND. Το O δηλώνει την οκτάβα και μπορεί να πάρει τις τιμές από 2 έως 7, ενώ το D δηλώνει την αξία και παίρνει τις τιμές από 1 (αξία 1/2) έως 8 (αξία 1/64). Το Ν δηλώνει τη νότα και μπορεί να πάρει μία από τις ακόλουθες τιμές: a, b, c, d, e, f, g, bb, g#, f#, eb και p (για την παύση).

Ειδικά για τις ανάγκες τις εντολής music, κατασκευάσαμε έναν parser μέσα στον ήδη υπάρχοντα parser της BASIC. Όλα αυτά θα τα δούμε αναλυτικότερα σε επόμενο άρθρο, όταν θα παρουσιάσουμε τον κώδικα που τρέχει στον επεξεργαστή του DIY υπολογιστή μας.

Στο πάνω μπλοκ βλέπουμε ένα μικρό απόσπασμα από την Ωδή στη Χαρά (Ode to Joy), γραμμένη στην MML της Quick BASIC. Στο άλλο μπλοκ φαίνεται ίδιο μουσικό απόσπασμα γραμμένο με τη δική μας MML, στη BASIC που τρέχει ο DIY υπολογιστής μας. Στη γραμμή 20 της δεύτερης εκδοχής, πριν αρχίσουμε να προσθέτουμε νότες, φροντίζουμε να απενεργοποιήσουμε τα κανάλια 2, 3 και 4, όπως επίσης και να καθαρίσουμε το 1. Έτσι είναι… Όσο πιο πλούσιο synthesizer έχεις, τόσο αυξάνονται και οι ευθύνες ;)

Στο πάνω μπλοκ βλέπουμε ένα μικρό απόσπασμα από την Ωδή στη Χαρά (Ode to Joy), γραμμένη στην MML της Quick BASIC. Στο άλλο μπλοκ φαίνεται ίδιο μουσικό απόσπασμα γραμμένο με τη δική μας MML, στη BASIC που τρέχει ο DIY υπολογιστής μας. Στη γραμμή 20 της δεύτερης εκδοχής, πριν αρχίσουμε να προσθέτουμε νότες, φροντίζουμε να απενεργοποιήσουμε τα κανάλια 2, 3 και 4, όπως επίσης και να καθαρίσουμε το 1. Έτσι είναι… Όσο πιο πλούσιο synthesizer έχεις, τόσο αυξάνονται και οι ευθύνες ;)

Πίσω στο (μεγάλο) PC
Μ’ αυτά και με τ’ άλλα, η κάρτα ήχου ολοκληρώθηκε κι εμείς είχαμε μείνει να την κοιτάμε. Είχαν προηγηθεί τόσες πολλές δοκιμές, που ήμασταν απολύτως βέβαιοι ότι δουλεύει άψογα. Παρ’ όλα αυτά, μην έχοντας τις απαραίτητες μουσικές γνώσεις, ήταν αδύνατο να συνθέσουμε κάτι ενδιαφέρον. Μόνο μερικές απλοϊκές, μονότονες και φάλτσες μελωδίες καταφέρναμε να γράψουμε. Κάπως έτσι αποφασίσαμε να ζητήσουμε τη βοήθεια ενός φίλου μουσικού. Τι θα του λέγαμε όμως; Θα του ζητούσαμε να μάθει την MML ή μήπως θα του δίναμε τον DIY υπολογιστή, για να κάνει δοκιμές γράφοντας προγράμματα σε BASIC; Παρόμοια προβλήματα αντιμετώπιζαν και οι κατασκευαστές των arcade games. Η μουσική γι’ αυτά τα παιχνίδια αποθηκευόταν σε κάποια ROM και, όπως αντιλαμβάνεστε, η εγγραφή απαιτούσε εξειδικευμένα εργαλεία. Ένας μουσικός θα ήταν πρακτικά αδύνατο να κάνει τη δουλειά του, αν για κάθε δοκιμή έπρεπε να προγραμματίζει κάποιο τσιπάκι. Για να διευκολυνθεί αυτή η εργασία, οι προγραμματιστές της εποχής ανέπτυξαν τους λεγόμενους trackers. Πρόκειται για προγράμματα που έτρεχαν σε κανονικούς υπολογιστές και εξομοίωναν πλήρως τα synthesizers των arcades. Ε, λοιπόν, με τον ίδιο τρόπο λύσαμε και το δικό μας πρόβλημα: Κατασκευάσαμε έναν tracker που εξομοιώνει πλήρως το synthesizer της κάρτας ήχου και μπορεί να τρέξει σε οποιοδήποτε σύγχρονο PC. Για την ανάπτυξη του tracker επιλέξαμε τη C#. Έχοντας περάσει μερικούς μήνες αναπτύσσοντας το firmware της κάρτας ήχου σε Assembly, είναι αδύνατο να περιγράψουμε τον ενθουσιασμό που νιώθαμε, στη σκέψη ότι θα χρησιμοποιούσαμε μια τόσο μοντέρνα γλώσσα :D Μπορείτε να κατεβάσετε το πακέτο με το project από το http://bit.ly/dh053dsynth.

Περιμένατε ότι δεν θα υπήρχε; Στην εικόνα φαίνεται ένας tracker για το SID! Πρόκειται για ένα πολύ παλιό πρόγραμμα, που επέτρεπε στους χρήστες του να εξομοιώνουν ένα ακόμα παλιότερο τσιπάκι ήχου.

Περιμένατε ότι δεν θα υπήρχε; Στην εικόνα φαίνεται ένας tracker για το SID! Πρόκειται για ένα πολύ παλιό πρόγραμμα, που επέτρεπε στους χρήστες του να εξομοιώνουν ένα ακόμα παλιότερο τσιπάκι ήχου.

Το τίμημα της καθαρότητας
Για την κατασκευή του GUI ακολουθήσαμε μια ανορθόδοξη προσέγγιση. Αρχικά χρησιμοποιήσαμε το εργαλείο σχεδίασης που προσφέρει το Visual Studio. Έτσι προέκυψε το αρχείο Form1.Designer.cs, που περιλάμβανε τις δηλώσεις και τις ιδιότητες για τα αντικείμενα (text box, list box, button κ.ά.) που συνθέτουν το περιβάλλον. Στη συνέχεια όμως τροποποιήσαμε το περιεχόμενο του αρχείου με τέτοιο τρόπο, που ο Designer του Visual Studio δεν αναγνώριζε το περιεχόμενο. Καταλαβαίνετε τι σημαίνει αυτό; Αν ανοίξετε το πρόγραμμά μας με οποιοδήποτε IDE, θα διαπιστώσετε ότι δεν μπορεί να σχεδιάσει το GUI. Επομένως, είναι αδύνατο να το τροποποιήσετε χρησιμοποιώντας κάποιο visual εργαλείο. Παρ’ όλα αυτά το πρόγραμμα εκτελείται κανονικά. Γι’ αυτή την αναποδιά ευθύνεται η όρεξή μας για πειράματα.

Το περιβάλλον του προγράμματος μοιάζει με εκείνο των περισσοτέρων trackers: Για κάθε κανάλι του synthesizer, περιλαμβάνει ένα σύνολο από controls που επιτρέπουν στο χρήστη να προσθαφαιρεί και να τροποποιεί νότες. Αυτό σημαίνει ότι τα περισσότερα στοιχεία του περιβάλλοντος επαναλαμβάνονται: Υπάρχουν τέσσερα κουμπιά για την προσθήκη μιας νότας, τέσσερα combos για την επιλογή οκτάβας, άλλα τέσσερα για την επιλογή της διάρκειας και πάει λέγοντας. Ακριβώς αυτή η επανάληψη ήταν που μας ώθησε να επέμβουμε τόσο βάρβαρα στο form1.Designer.cs. Αποφασίσαμε να ομαδοποιήσουμε τα controls με την ίδια λειτουργία και να τα τοποθετήσουμε μέσα σε πίνακες των τεσσάρων στοιχείων (όσα είναι και τα κανάλια). Για παράδειγμα, όλα τα κουμπιά για την προσθήκη μιας νότας τοποθετήθηκαν στον πίνακα insert_note, ενώ όλα τα κουμπιά για την τροποποίηση μιας νότας μπήκαν στον πίνακα edit_note. Αναρωτιέστε τι πετύχαμε με αυτόν τον τρόπο; Σκεφτείτε το εξής: Ας υποθέσουμε ότι έχουμε γράψει μια γενική συνάρτηση, που μπορεί προσθέτει νότες στη λίστα ενός καναλιού. Σε κάθε κλήση της εν λόγω συνάρτησης θα έπρεπε να δίνουμε τουλάχιστον πέντε παραμέτρους, με τα αντικείμενα του επιλεγμένου καναλιού: Τέσσερα combo boxes (για τη νότα, την οκτάβα, τη διάρκεια και το εφέ) κι ένα list box (για την αποθήκευση της νότας). Με τη χρήση των πινάκων, η ίδια συνάρτηση μπορεί να λειτουργεί με μία μόνο παράμετρο: τον αριθμό του καναλιού. Τελικά, με τη βοήθεια των πινάκων καταφέραμε να γράψουμε πολύ πιο καθαρό κώδικα ή, τέλος πάντων, κώδικα που διαβάζεται ευκολότερα από εμάς. Πάντως, το τίμημα του να μη δουλεύουν οι GUI designers είναι αρκετά υψηλό και για να ‘μαστε ειλικρινείς είναι αμφίβολο αν θα ακολουθούσαμε ξανά την ίδια προσέγγιση.

Ένας tracker με τρία κανάλια. Όπως και στην περίπτωση του tracker του SID, το μεγαλύτερο τμήμα του περιβάλλοντος καταλαμβάνεται από τρεις λίστες. Σε αυτές τοποθετούνται οι νότες που θα παίξει κάθε κανάλι. (Screenshot source: Softpedia.)

Ένας tracker με τρία κανάλια. Όπως και στην περίπτωση του tracker του SID, το μεγαλύτερο τμήμα του περιβάλλοντος καταλαμβάνεται από τρεις λίστες. Σε αυτές τοποθετούνται οι νότες που θα παίξει κάθε κανάλι. (Screenshot source: Softpedia.)

Μηχανισμός τύχης
Οι γεννήτριες LFSR στηρίζονται σ’ έναν καταχωριστή, του οποίου τα ψηφία (bits) ολισθαίνουν διαρκώς προς μια κατεύθυνση. Όπως αντιλαμβάνεστε, κάθε φορά που πραγματοποιείται η ολίσθηση απομακρύνεται ένα bit από τον καταχωριστή και, ταυτόχρονα, δημιουργείται μια κενή θέση. Για παράδειγμα, αν η ολίσθηση έχει κατεύθυνση προς τα’ αριστερά, σε κάθε βήμα της διαδικασίας προκύπτει ένα κενό στο δεξιό άκρο του καταχωριστή. Εκεί τοποθετείται ένα ψηφίο, που προκύπτει από μια γραμμική συνάρτηση κάποιων άλλων bits του καταχωριστή. Η εν λόγω συνάρτηση ονομάζεται συνάρτηση ανάδρασης και μπορεί να αξιοποιεί όσα και όποια bits επιθυμούμε. Η επιλογή των bits (ονομάζονται taps) παίζει καθοριστικό ρόλο για την συμπεριφορά του LFSR. Σκεφτείτε το εξής: Ένας 16μπιτος καταχωριστής μπορεί να πάρει 2^16 (=65536) διαφορετικές τιμές. Παρ’ όλα αυτά, δεν είναι βέβαιο ότι *κάθε* 16μπιτος LFSR θα παράγει τόσες διαφορετικές τιμές. Επιλέγοντας κάποια taps μπορούμε να δημιουργήσουμε μια “φτωχή” εκδοχή, που θα παράγει και θα ανακυκλώνει μερικές μόνο από τις 65536 τιμές. Εξίσου δυνατή είναι και η κατασκευή ενός maximal LFSR, που θα παράγει όλους τους δυνατούς αριθμούς. Σε γενικές γραμμές, η επιλογή των σωστών taps δεν αποτελεί εύκολη υπόθεση. Είναι αδύνατο να προβλέψουμε τη συμπεριφορά ενός LFSR, αν δεν υπολογίσουμε μία προς μία τις τιμές που παράγει. Το πρόβλημα της επιλογής των taps λύνεται πανεύκολα μόνο για τους Fibonacci LFSR. Έτσι ονομάζονται οι LFSR των οποίων η συνάρτηση ανάδρασης αποτελείται μόνο από την πράξη XOR. Όπως υποψιάζεστε, πρόκειται για το δημοφιλέστερο είδος LFSR και είναι πολλοί εκείνοι που έχουν μελετήσει τη συμπεριφορά τους διεξοδικά. Το Internet είναι γεμάτο με λίστες που αναφέρουν τα κατάλληλα taps, για την κατασκευή maximal LFSR με οποιοδήποτε πλήθος bits.

Παίξτε κι εσείς!
Ο κώδικας για τον tracker, αν προσπεράσουμε τις εκτεταμένες επεμβάσεις στο Form1.Designer.cs, εκτείνεται στα αρχεία Form1.cs και synthesizer.cs. Όπως υποψιάζεστε, το πρώτο αρχείο περιλαμβάνει τις συναρτήσεις για το χειρισμό των διαφόρων events του GUI. Το περισσότερο ενδιαφέρον συγκεντρώνεται στο synthesizer.cs, όπου ορίζουμε την κλάση με το (πολύ πρωτότυπο) όνομα synthesizer. Οι μέθοδοι αυτής της κλάσης σχηματίζουν τα κύματα που περιγράφουν οι νότες, υπολογίζοντας ένα προς ένα τα διαδοχικά δείγματα. Ακολούθως, για την αναπαραγωγή των δειγμάτων τοποθετούνται σε ένα αρχείο WAV. Για τον υπολογισμό των δειγμάτων δεν χρειάζεται να πούμε κάτι. Αν έχετε κατανοήσει τον αντίστοιχο μηχανισμό στην Assembly, ο κώδικας σε C# θα σας φανεί περίπατος. Σε κάθε περίπτωση, έχετε κατά νου ότι ο tracker γράφτηκε πολύ βιαστικά και θα ήταν φρόνιμο να υιοθετήσετε τις πρακτικές που ακολουθήσαμε κατά την ανάπτυξή του. Ο μόνος λόγος για τον οποίο σας δίνουμε τον tracker, είναι για να μπορείτε να παίξετε κι εσείς με τους όμορφους, χοντροκομμένους ήχους που βγάζει η κάρτα μας. Κι αν όλα όσα έχετε διαβάσει δεν κατάφεραν να σας ανοίξουν την όρεξη, μπορείτε να κατεβάσετε αυτό το πακέτο από το http://bit.ly/dh053tunes, με δυο διάσημες μελωδίες. Πρόκειται για την εισαγωγή του Bubble Bobble και του Tetris, γραμμένες στον tracker και αποθηκευμένες σε επεξεργάσιμη μορφή. Μπορείτε να τις φορτώσετε, να τις ακούσετε και να τις τροποποιήσετε όσο θέλετε.

Ο δικός μας tracker! Όταν πατάμε το κουμπί play, στην κάτω περιοχή του παραθύρου εμφανίζεται η μουσική με τη μορφή που θα αποθηκευτεί στον μικροελεγκτή της κάρτας ήχου. Αν πάρουμε αυτό το κατεβατό και το τοποθετήσουμε στο αρχείο melody.asm, στη θέση της μελωδίας του Bubble Bobble, θα έχουμε αλλάξει την εργοστασιακή μελωδία που συνοδεύει την κάρτα ήχου. Παρεμπιπτόντως, στην εικόνα φαίνεται η μελωδία του Tetris :)

Ο δικός μας tracker! Όταν πατάμε το κουμπί play, στην κάτω περιοχή του παραθύρου εμφανίζεται η μουσική με τη μορφή που θα αποθηκευτεί στον μικροελεγκτή της κάρτας ήχου. Αν πάρουμε αυτό το κατεβατό και το τοποθετήσουμε στο αρχείο melody.asm, στη θέση της μελωδίας του Bubble Bobble, θα έχουμε αλλάξει την εργοστασιακή μελωδία που συνοδεύει την κάρτα ήχου. Παρεμπιπτόντως, στην εικόνα φαίνεται η μελωδία του Tetris :)

Leave a Reply

You must be logged in to post a comment.

Σύνδεση

Αρχείο δημοσιεύσεων