/* * Piezosummeransteuerung * * Copyright (c) 2012-2022 Michael Buesch * Licensed under the terms of the GNU General Public License version 2. */ #include "buzzer.h" #include "util.h" #include #include #include #include /** Summer Kontext */ struct noteplayer { /** Laenge einer 1/1 Note, in Millisekunden. */ uint16_t basespeed_ms; /** Zeiger auf die aktuellen Noten. * Wenn NULL, dann werden aktuell keine Noten gespielt. */ const note_t __flash *notes; /** Aktueller Index im "notes" array. */ uint16_t notes_index; /** Schleife. */ bool loop; /** Anzahl der verbleibenden Millisekunden der aktuellen Note. */ uint16_t remaining_ms; /** Die vorherige Note. */ note_t prev_note; /** Sharp (Kreuz) aktiv? */ bool sharp; /** Haltebogen aktiv? */ bool tie; /** Die aktuelle Oktavenverschiebung. */ int8_t octave_shift; /** Die aktuelle Lautstaerke. Von 0 bis 15. */ uint8_t volume; }; /** Abspielerkontext. */ static struct noteplayer player; /** Gespeicherte Lautstaerke. */ static uint8_t EEMEM ee_volume = BUZZER_VOLUME_DEFAULT; /** Notentabelle. Frquenzen in Dezihertz (0.1 Hz) */ static const uint16_t __flash note_freqs[] = { [NOTEID_PAUSE] = 0, [NOTEID_C4] = 2616, /* c' */ [NOTEID_D4] = 2937, /* d' */ [NOTEID_E4] = 3296, /* e' */ [NOTEID_F4] = 3492, /* f' */ [NOTEID_G4] = 3919, /* g' */ [NOTEID_A4] = 4400, /* a' */ [NOTEID_B4] = 4939, /* h' */ [NOTEID_C5] = 5233, /* c'' */ [NOTEID_D5] = 5873, /* d'' */ [NOTEID_E5] = 6593, /* e'' */ [NOTEID_F5] = 6985, /* f'' */ [NOTEID_G5] = 7840, /* g'' */ [NOTEID_A5] = 8800, /* a'' */ [NOTEID_B5] = 9878, /* h'' */ }; /** Notentabelle, erhoehte Noten. Frquenzen in Dezihertz (0.1 Hz) */ static const uint16_t __flash note_sharp_freqs[] = { [NOTEID_PAUSE] = 0, [NOTEID_C4] = 2772, /* cis' */ [NOTEID_D4] = 3111, /* dis' */ [NOTEID_E4] = 3296, /* e' */ [NOTEID_F4] = 3700, /* fis' */ [NOTEID_G4] = 4153, /* gis' */ [NOTEID_A4] = 4662, /* ais' */ [NOTEID_B4] = 4939, /* h' */ [NOTEID_C5] = 5544, /* cis'' */ [NOTEID_D5] = 6223, /* dis'' */ [NOTEID_E5] = 6593, /* e'' */ [NOTEID_F5] = 7400, /* fis'' */ [NOTEID_G5] = 8306, /* gis'' */ [NOTEID_A5] = 9323, /* ais'' */ [NOTEID_B5] = 9878, /* h'' */ }; /** ID Nummer aus Note extrahieren. * * \param n Die Note. */ static inline uint8_t note2id(note_t n) { return (n & NOTE_ID_MASK) >> NOTE_ID_SHIFT; } /** Notenwert aus Note extrahieren. * * \param n Die Note. */ static inline uint8_t note2val(note_t n) { return (n & NOTE_VAL_MASK) >> NOTE_VAL_SHIFT; } /** Millisekundendauer einer Note ermitteln. * * \param n Die Note. */ static uint16_t note_to_ms(note_t n) { if (note2id(n) >= NOTEID_FLAGS) return 0; return player.basespeed_ms >> note2val(n); } /** Frequenz einer Note ermitteln. * * \param n Die Note. */ static uint16_t note_to_decihz(note_t n) { uint8_t noteid = note2id(n); int8_t octave_shift; uint16_t decihz; /* Dezihertz Frequenz fuer die Note ermitteln. */ if (player.sharp) decihz = note_sharp_freqs[noteid]; else decihz = note_freqs[noteid]; /* Oktavenverschiebung. * Eine Oktave hoeher -> Frequenz mal 2. * Eine Oktave niedriger -> Frequenz durch 2. * Geteilt wird effizient mit Bitshift. */ octave_shift = player.octave_shift; if (octave_shift < 0) decihz >>= (uint8_t)(-octave_shift); else decihz <<= (uint8_t)octave_shift; return decihz; } /** PWM Teiler zu einer Note ermitteln. * * \param n Die Note. */ static uint16_t note_to_divider(note_t n) { uint32_t base_freq_hz = F_CPU / 64; /* Prescaler 64 */ uint16_t div, decihz; /* Frequenz in Dezihertz ermitteln. */ decihz = note_to_decihz(n); if (!decihz) return 0; /* PWM Teiler ermitteln. (Basisfrequenz in Dezihertz wandeln) */ div = base_freq_hz * 10ul / decihz; return div; } /** Summer elektrisch an/abkoppeln. * * \param connect true -> ankoppeln */ static void buzzer_hw_connect(bool connect) { if (connect) TCCR1A |= (1 << COM1A1) | (1 << COM1A0); else TCCR1A &= ~((1 << COM1A1) | (1 << COM1A0)); } /** Summer PWM Teiler setzen. * * \param divider Der Teilerwert. */ static void buzzer_divider_set(uint16_t divider) { uint8_t volume_shift; uint16_t duty; if (divider) divider -= 1; /* Puls/Pause-Verhaeltnis an Lautstaerke anpassen. */ volume_shift = BUZZER_VOLUME_MAX - player.volume; duty = divider >> (volume_shift + 1); if (divider == 0 || player.volume <= BUZZER_VOLUME_MIN) { /* Summer abschalten. */ if (player.volume <= BUZZER_VOLUME_MIN) buzzer_hw_connect(0); TCCR1B &= ~((1 << CS10) | (1 << CS11) | (1 << CS12)); } else { if (divider != ICR1 || duty != OCR1A) { /* Zaehler ruecksetzen. */ TCNT1 = 0; /* Frequenz setzen. */ ICR1 = divider; /* Puls/Pause-Verhaeltnis setzen. */ OCR1A = duty; } /* Summer einschalten. */ TCCR1B |= (1 << CS10) | (1 << CS11); /* Vorteiler 64 */ buzzer_hw_connect(1); } } /** Summer abschalten. */ static void buzzer_shutdown(void) { /* Notentabelle loeschen. */ player.notes = NULL; player.notes_index = 0; /* Summer abschalten. */ buzzer_divider_set(0); buzzer_hw_connect(0); } /** Note auf Summer ausgeben. * * \param n Die Note. */ static void buzzer_tune_note(note_t n) { uint8_t noteid = note2id(n); uint8_t noteval; uint16_t divider; if (noteid == NOTEID_FLAGS) { /* Spezialnote. */ switch (note2val(n)) { case NOTEVAL_SHARP: /* "Kreuz" Flag setzen. */ player.sharp = 1; break; case NOTEVAL_DOT: /* Punktierung setzen. */ noteval = note2val(player.prev_note); if (noteval < NOTEVAL_1_64) noteval++; player.prev_note &= ~NOTE_VAL_MASK; player.prev_note |= noteval << NOTE_VAL_SHIFT; player.remaining_ms = note_to_ms(player.prev_note); break; case NOTEVAL_TIE: /* "Haltebogen" Flag setzen. */ player.tie = 1; break; case NOTEVAL_OCTAVE_SH_UP: /* Oktave nach oben verschieben. */ player.octave_shift++; break; case NOTEVAL_OCTAVE_SH_DOWN: /* Oktave nach unten verschieben. */ player.octave_shift--; break; } } else { /* Normale Note abspielen. */ divider = note_to_divider(n); buzzer_divider_set(divider); } } /** Naechste Note abspielen. */ static void buzzer_play_next_note(void) { uint16_t ms; note_t note; if (!player.notes) { /* Keine Notentabelle vorhanden. Summer abschalten. */ buzzer_shutdown(); return; } /* Aktuelle Note aus der Tabelle auslesen. */ note = player.notes[player.notes_index]; if (note == note_array_end) { /* Ende der Tabelle erreicht. */ if (player.loop) { /* Loop. -> Wieder von vorne beginnen. */ note = player.notes[0]; player.notes_index = 0; } else { /* Kein Loop. -> Summer abschalten. */ buzzer_shutdown(); return; } } if (note2id(note) != NOTEID_FLAGS && note2id(note) == note2id(player.prev_note)) { /* Note ist gleich der vorhergehenden Note. */ if (!player.tie) { /* Kein Haltebogen. -> Note neu anschlagen. * Dazu Summer abschalten und eine kurze Zeit warten. */ player.remaining_ms = RESTRIKE_MS; buzzer_divider_set(0); /* Haltebogenmerker setzen damit nach der Pause * die Note gespielt wird. */ player.tie = 1; return; } } /* Index fuer Notentabelle inkrementieren. */ player.notes_index++; /* Die Note anschlagen. */ buzzer_tune_note(note); /* Wenn normale Note ... */ if (note2id(note) != NOTEID_FLAGS) { /* ... dann Wartezeit der Note setzen * und ggf. Flags ruecksetzen. */ ms = note_to_ms(note); player.prev_note = note; player.remaining_ms = ms; player.sharp = 0; player.tie = 0; } } /** Notentabelle abspielen. * * \param notes Die Notentabelle. * * \param basespeed_ms Die Basisgeschwindigkeit einer 1/1 Note. * * \param loop 1: Endlosschleife. * 0: Nur einmal spielen. */ void buzzer_play(const note_t __flash *notes, uint16_t basespeed_ms, bool loop) { uint8_t sreg; sreg = irq_disable_save(); /* Basisparameter setzen. */ player.basespeed_ms = basespeed_ms; player.loop = loop; /* Zeiger auf Notentabelle abspeichern. */ player.notes = notes; player.notes_index = 0; /* Playerstatus ruecksetzen. */ player.remaining_ms = 0; player.prev_note = invalid_note; player.sharp = 0; player.tie = 0; player.octave_shift = 0; /* Erste Note anschlagen. */ buzzer_play_next_note(); irq_restore(sreg); } /** Abspielen der aktuellen Notentabelle stoppen. */ void buzzer_stop(void) { uint8_t sreg; sreg = irq_disable_save(); buzzer_play(NULL, 0, 0); buzzer_hw_connect(0); irq_restore(sreg); } /** Wird gerade eine Notentabelle abgespielt? */ bool buzzer_is_playing(void) { uint8_t sreg; bool playing; sreg = irq_disable_save(); playing = player.notes != NULL; irq_restore(sreg); return playing; } /** Lautstaerke setzen. * * \param volume Lautstaerkenwert. Von 0 bis 15. */ void buzzer_volume_set(int8_t volume) { uint8_t sreg; sreg = irq_disable_save(); /* Wert begrenzen. */ player.volume = clamp(volume, BUZZER_VOLUME_MIN, BUZZER_VOLUME_MAX); /* Puls/Pause-Verhaeltnis neu berechnen und auf PWM schreiben. */ buzzer_divider_set(ICR1); /* Lautstaerke in EEPROM schreiben. */ eeprom_update_byte(&ee_volume, player.volume); irq_restore(sreg); } /** Lautstaerke ermitteln. */ int8_t buzzer_volume_get(void) { return player.volume; } /** Periodische Abarbeitung mit 100 Hz. */ void buzzer_100hz_tick(void) { uint8_t sreg; sreg = irq_disable_save(); /* Solange Notentabelle vorhanden ... */ while (player.notes) { /* Wenn Wartezeit aktiv, dann dekrementieren * und bis zum naechsten Tick warten. */ if (player.remaining_ms > 10) { player.remaining_ms -= 10; break; } player.remaining_ms = 0; /* Naechste Note anschlagen. */ buzzer_play_next_note(); /* Wenn Wartezeit gesetzt wurde, abbrechen und * bis zum naechsten Tick warten. */ if (player.remaining_ms) break; } irq_restore(sreg); } /** Summer initialisieren. */ void buzzer_init(void) { uint8_t volume; /* Ports initialisieren. */ PORTB &= ~(1 << PB1); DDRB |= (1 << DDB1); /* Timer abschalten. */ TCCR1B = 0; /* Fast PWM konfigurieren. */ buzzer_divider_set(0); TCNT1 = 0; TCCR1A = (1 << WGM11); TCCR1B = (1 << WGM13) | (1 << WGM12); /* Lautstaerke einstellen. */ volume = eeprom_read_byte(&ee_volume); buzzer_volume_set(volume > INT8_MAX ? BUZZER_VOLUME_DEFAULT : volume); }