/** * NOTICE * * Copyright 2019 Tile Inc. All Rights Reserved. * All code or other information included in the accompanying files ("Tile Source Material") * is PROPRIETARY information of Tile Inc. ("Tile") and access and use of the Tile Source Material * is subject to these terms. The Tile Source Material may only be used for demonstration purposes, * and may not be otherwise distributed or made available to others, including for commercial purposes. * Without limiting the foregoing , you understand and agree that no production use * of the Tile Source Material is allowed without a Tile ID properly obtained under a separate * agreement with Tile. * You also understand and agree that Tile may terminate the limited rights granted under these terms * at any time in its discretion. * All Tile Source Material is provided AS-IS without warranty of any kind. * Tile does not warrant that the Tile Source Material will be error-free or fit for your purposes. * Tile will not be liable for any damages resulting from your use of or inability to use * the Tile Source Material. * * Support: firmware_support@tile.com */ /** * @file tile_player.c * @brief Tile player for Nordic Platform */ #include "sdk_common.h" #if NRF_MODULE_ENABLED(TILE_SUPPORT) #include "tile_config.h" #include "tile_lib.h" uint8_t g_FindActivate_SongPlayed = 0; #if TILE_ENABLE_PLAYER #include #include #include "tile_player.h" #include "tile_storage/tile_storage.h" #include "tile_service/tile_service.h" #include "tile_features/tile_features.h" #include "app_timer.h" #include "nrf_drv_gpiote.h" #include "nrf_drv_timer.h" #include "nrf_drv_ppi.h" #include "nrfx_ppi.h" #include "nrf_log.h" static nrf_ppi_channel_t ppi_channel1; uint32_t compare_evt_addr1; uint32_t gpiote_task_addr1; /* Set PIN_PIEZO for toggle on timer event */ nrf_drv_gpiote_out_config_t config1 = GPIOTE_CONFIG_OUT_TASK_TOGGLE(false); /* Convert a frequency into number of microseconds for a half-pulse */ #define CONV(n) (1000000 / (n) / 2) uint8_t g_inbetween = 0; /* Some defines for accessing the note array properly */ #define NOTE_ARRAY_BASE_NOTE (C3) #define NOTE_ARRAY_MAX_NOTE (B8) #define NOTE_ARRAY_INDEX(n) ((int) (n) - NOTE_ARRAY_BASE_NOTE) /* Values for setting the PWM to the correct frequency for each note. * For our implementation, we only store the notes useful to us, which * is the range from C3 to B8 */ static const uint16_t notes[] = { [NOTE_ARRAY_INDEX(C3)] = CONV(131), [NOTE_ARRAY_INDEX(CS3)] = CONV(138), [NOTE_ARRAY_INDEX(D3)] = CONV(147), [NOTE_ARRAY_INDEX(DS3)] = CONV(156), [NOTE_ARRAY_INDEX(E3)] = CONV(165), [NOTE_ARRAY_INDEX(F3)] = CONV(175), [NOTE_ARRAY_INDEX(FS3)] = CONV(185), [NOTE_ARRAY_INDEX(G3)] = CONV(196), [NOTE_ARRAY_INDEX(GS3)] = CONV(208), [NOTE_ARRAY_INDEX(A3)] = CONV(220), [NOTE_ARRAY_INDEX(AS3)] = CONV(233), [NOTE_ARRAY_INDEX(B3)] = CONV(247), [NOTE_ARRAY_INDEX(C4)] = CONV(262), [NOTE_ARRAY_INDEX(CS4)] = CONV(277), [NOTE_ARRAY_INDEX(D4)] = CONV(294), [NOTE_ARRAY_INDEX(DS4)] = CONV(311), [NOTE_ARRAY_INDEX(E4)] = CONV(330), [NOTE_ARRAY_INDEX(F4)] = CONV(349), [NOTE_ARRAY_INDEX(FS4)] = CONV(370), [NOTE_ARRAY_INDEX(G4)] = CONV(392), [NOTE_ARRAY_INDEX(GS4)] = CONV(415), [NOTE_ARRAY_INDEX(A4)] = CONV(440), [NOTE_ARRAY_INDEX(AS4)] = CONV(466), [NOTE_ARRAY_INDEX(B4)] = CONV(494), [NOTE_ARRAY_INDEX(C5)] = CONV(523), [NOTE_ARRAY_INDEX(CS5)] = CONV(554), [NOTE_ARRAY_INDEX(D5)] = CONV(587), [NOTE_ARRAY_INDEX(DS5)] = CONV(622), [NOTE_ARRAY_INDEX(E5)] = CONV(659), [NOTE_ARRAY_INDEX(F5)] = CONV(698), [NOTE_ARRAY_INDEX(FS5)] = CONV(740), [NOTE_ARRAY_INDEX(G5)] = CONV(784), [NOTE_ARRAY_INDEX(GS5)] = CONV(831), [NOTE_ARRAY_INDEX(A5)] = CONV(880), [NOTE_ARRAY_INDEX(AS5)] = CONV(932), [NOTE_ARRAY_INDEX(B5)] = CONV(988), [NOTE_ARRAY_INDEX(C6)] = CONV(1047), [NOTE_ARRAY_INDEX(CS6)] = CONV(1109), [NOTE_ARRAY_INDEX(D6)] = CONV(1175), [NOTE_ARRAY_INDEX(DS6)] = CONV(1245), [NOTE_ARRAY_INDEX(E6)] = CONV(1319), [NOTE_ARRAY_INDEX(F6)] = CONV(1397), [NOTE_ARRAY_INDEX(FS6)] = CONV(1480), [NOTE_ARRAY_INDEX(G6)] = CONV(1568), [NOTE_ARRAY_INDEX(GS6)] = CONV(1661), [NOTE_ARRAY_INDEX(A6)] = CONV(1760), [NOTE_ARRAY_INDEX(AS6)] = CONV(1865), [NOTE_ARRAY_INDEX(B6)] = CONV(1976), [NOTE_ARRAY_INDEX(C7)] = CONV(2093), [NOTE_ARRAY_INDEX(CS7)] = CONV(2217), [NOTE_ARRAY_INDEX(D7)] = CONV(2349), [NOTE_ARRAY_INDEX(DS7)] = CONV(2489), [NOTE_ARRAY_INDEX(E7)] = CONV(2637), [NOTE_ARRAY_INDEX(F7)] = CONV(2794), [NOTE_ARRAY_INDEX(FS7)] = CONV(2960), [NOTE_ARRAY_INDEX(G7)] = CONV(3136), [NOTE_ARRAY_INDEX(GS7)] = CONV(3322), [NOTE_ARRAY_INDEX(A7)] = CONV(3520), [NOTE_ARRAY_INDEX(AS7)] = CONV(3729), [NOTE_ARRAY_INDEX(B7)] = CONV(3951), [NOTE_ARRAY_INDEX(C8)] = CONV(4186), [NOTE_ARRAY_INDEX(CS8)] = CONV(4435), [NOTE_ARRAY_INDEX(D8)] = CONV(4699), [NOTE_ARRAY_INDEX(DS8)] = CONV(4978), [NOTE_ARRAY_INDEX(E8)] = CONV(5274), [NOTE_ARRAY_INDEX(F8)] = CONV(5588), [NOTE_ARRAY_INDEX(FS8)] = CONV(5920), [NOTE_ARRAY_INDEX(G8)] = CONV(6272), [NOTE_ARRAY_INDEX(GS8)] = CONV(6645), [NOTE_ARRAY_INDEX(A8)] = CONV(7040), [NOTE_ARRAY_INDEX(AS8)] = CONV(7459), [NOTE_ARRAY_INDEX(B8)] = CONV(7902), }; const uint8_t FixedSong0[] = { C3, 1, REST, REST }; // Click Song const uint8_t FixedSong1[] = { D5, 3, FS5, 3, D5, 3, FS5, 3, D5, 3, FS5, 3, D5, 3, FS5, 3, D5, 3, FS5, 6, REST, 3, D6, 13, FS5, 13, G5, 13, A5, 13, D6, 9, REST, 4, A5, 6, REST, 6, A6, 6, REST, 6, A5, 6, REST, 19, FS6, 3, A6, 3, FS6, 3, A6, 3, FS6, 3, A6, 3, REST, 6, D6, 3, FS6, 3, D6, 3, FS6, 3, D6, 3, FS6, 3, REST, 6, G5, 3, B5, 3, G5, 3, B5, 3, G5, 3, B5, 3, G5, 3, B5, 3, G5, 3, B5, 6, REST, 3, G6, 13, B5, 13, C6, 13, D6, 13, G6, 9, REST, 4, D6, 6, REST, 6, D7, 6, REST, 6, D6, 6, REST, 19, B6, 3, D7, 3, B6, 3, D7, 3, B6, 3, D7, 3, B6, 3, D7, 6, REST, 22, A5, 3, CS6, 3, A5, 3, CS6, 3, A5, 3, CS6, 3, A5, 3, CS6, 3, A5, 3, CS6, 6, REST, 3, A6, 13, CS6, 13, D6, 13, E6, 13, A6, 9, REST, 4, E6, 6, REST, 6, E7, 6, REST, 6, E6, 6, REST, 19, CS7, 3, E7, 3, CS7, 3, E7, 3, CS7, 3, E7, 3, REST, 6, A6, 3, CS7, 3, A6, 3, CS7, 3, A6, 3, CS7, 3, REST, 6, D6, 3, FS6, 3, D6, 3, FS6, 3, D6, 3, FS6, 3, D6, 3, FS6, 3, D6, 3, FS6, 6, REST, 3, D7, 13, FS6, 13, G6, 13, A6, 13, D7, 9, REST, 4, A6, 6, REST, 6, A7, 6, REST, 6, A6, 6, REST, 19, FS7, 3, A7, 3, FS7, 3, A7, 3, FS7, 3, A7, 3, FS7, 3, A7, 6, REST, 11, REST, REST, REST, REST }; const uint8_t FixedSong2[] = { // Active Song A5, 5, REST, 7, A6, 2, REST, 11, A5, 2, REST, 23, A5, 2, REST, 11, A6, 2, REST, 11, A5, 2, REST, 23, D6, 13, FS5, 13, G5, 13, A5, 13, D5, 26, D6, 14, REST, REST }; const uint8_t FixedSong3[] = { // Sleep Song A6, 38, D6, 13, G6, 13, FS6, 13, D6, 13, A5, 10, REST, 3, D5, 5, REST, 7, D6, 2, REST, 11, D5, 2, REST, 23, D5, 2, REST, 11, D6, 2, REST, 11, D3, 2, REST, 1, REST, REST }; const uint8_t FixedSong4[] = { // Wake Song D5,38, A5,13, FS5,13, G5,13, A5,13, D6,10, REST,3, A5,5, REST,7, A6,2, REST,11, A5,2, REST,23, A5,2, REST,11, A6,2, REST,11, A5,2, REST, REST }; const uint8_t FixedSong5[] = { FS7,250, FS7,250, FS7,250, FS7,250, REST, REST }; /* Factory Song: For factory test song - 10 seconds of F#7 at 2960Hz */ const uint8_t FixedSong6[] = { C8,1, CS8,1, D8,1, DS8,1, E8,1, F8,1, REST, REST }; const uint8_t FixedSong7[] = { REST, REST }; /* Silent Song */ const uint8_t FixedSong8[] = { REST, REST }; /* Button Song: currently silent */ const uint8_t FixedSong9[] = { A5, 2, REST,11, A6, 2, REST, 11, A5, 2, REST,REST }; /* WakePart Song */ const uint8_t FixedSong10[] = { FS4, 3, REST,10, D5, 11, REST, 2, A5, 3, REST, 10, D6, 12, REST,REST }; /* double tap success Song */ const uint8_t FixedSong11[] = { GS4, 3, REST,10, GS5, 11, REST, 2, D4, 3, REST, 10, GS3, 12, REST,1, REST,REST }; /* double tap failure Song */ const uint8_t FixedSong12[] = { /*C3, 1, REST, 10, C3, 1,*/ REST, REST }; /* 2 clicks song */ const uint8_t FixedSong13[] = { D5, 30, REST, REST }; /* 1 bip Song */ const uint8_t FixedSong14[] = { D5, 30, REST,11, D5, 30, REST, REST }; /* 2 bip Song */ const uint8_t FixedSong15[] = { D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST, REST }; /* 3 bip Song */ const uint8_t FixedSong16[] = { D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST, REST }; /* 4 bip Song */ const uint8_t FixedSong17[] = { D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST, REST }; /* 5 bip Song */ const uint8_t FixedSong18[] = { D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST, REST }; /* 6 bip Song */ const uint8_t FixedSong19[] = { D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST,11, D5, 30, REST, REST }; /* 7 bip Song */ const uint8_t *tile_song_array[] = { FixedSong0, FixedSong1, FixedSong2, FixedSong3, FixedSong4, FixedSong5, FixedSong6, FixedSong7, FixedSong8, FixedSong9, FixedSong10, FixedSong11, FixedSong12, FixedSong13, FixedSong14, FixedSong15, FixedSong16, FixedSong17, FixedSong18, FixedSong19, }; static uint8_t startup_sequence = 0; static nrf_drv_timer_t player_timer = NRF_DRV_TIMER_INSTANCE(PLAYER_TIMER_ID); APP_TIMER_DEF(song_timer_id); APP_TIMER_DEF(song_timer_loop); static void NextNote(void); static void StartupPlayer(void); static song_t current_song = {0}; static song_t next_song = {0}; static uint8_t song_loop_flag = 1; static void song_timer_handler(void *p_context) { /* TODO: guard against calling NextNote when song is not playing */ if(startup_sequence) { StartupPlayer(); } else { NextNote(); } } /** * @brief song_duration_timeout_handler * This is the interrupt handler for the song duration timer. * The idea is that the variable song_loop_flag is set to 1 to ensure * normal song_done() operation at all times. It is only set to 0 if * The song is requested to play forever. If it is requested to play for a * fixed duration, the timer allows the song_loop_flag to stay 0 until it times out. */ static void song_duration_timeout_handler(void *p_context) { song_loop_flag = 1; } /** * @brief Player Boot Config * Allocate ppi Channels for Player once at Boot: * Call once at Boot, so we don't keep on assigning it again and again: * There are limited number of channels, and every channel alloc from same place does not alloc a new channel * It returns error: channel already allocated and moves on without apparent immediate problem, but assigned channels can go out of context, * allocate more than needed channels, and then stop playing song/advertise in some scenarios * So do not call alloc again and again */ void tile_boot_config_player(void) { nrfx_err_t err_code; /* Initialize the gpiote driver if it isn't already */ if(!nrf_drv_gpiote_is_init()) { err_code = nrf_drv_gpiote_init(); APP_ERROR_CHECK(err_code); } err_code = nrf_drv_ppi_channel_alloc(&ppi_channel1); NRF_LOG_INFO("ppi channel 1 alloc gave %u, &ppi_channel1: 0x%08x",err_code,&ppi_channel1); APP_ERROR_CHECK(err_code); err_code = nrf_drv_gpiote_out_init(PIN_PIEZO, &config1); APP_ERROR_CHECK(err_code); } /** * @brief Configure PPI/GPIOTE for the layer */ static void ConfigureBuzzer() { uint32_t compare_evt_addr; uint32_t gpiote_task_addr; nrf_ppi_channel_t ppi_channel; (void) nrf_drv_ppi_channel_alloc(&ppi_channel); /* Set PIN_PIEZO for toggle on timer event */ nrf_drv_gpiote_out_config_t config = GPIOTE_CONFIG_OUT_TASK_TOGGLE(false); (void) nrf_drv_gpiote_out_init(PIN_PIEZO, &config); /* Tie timer events to piezo toggle */ compare_evt_addr = nrf_drv_timer_event_address_get(&player_timer, NRF_TIMER_EVENT_COMPARE0); gpiote_task_addr = nrf_drv_gpiote_out_task_addr_get(PIN_PIEZO); (void) nrf_drv_ppi_channel_assign(ppi_channel, compare_evt_addr, gpiote_task_addr); (void) nrf_drv_ppi_channel_enable(ppi_channel); } /** * @brief Set the GPIO Player PIN as an Out PIN and Start Song after delay */ static void StartupPlayer() { startup_sequence = 0; /* This is the start of the Find Default Song*/ if (current_song.p_firstNote == tile_song_array[TILE_SONG_FIND]) { g_FindActivate_SongPlayed = 1; } /* find length of the song */ if (current_song.duration == TILE_SONG_DURATION_ONCE) { song_loop_flag = 1; } else if (current_song.duration == TILE_SONG_DURATION_FOREVER) { song_loop_flag = 0; } else { /* Create the timer for Song Loop */ NRF_LOG_INFO("Song Request received for duration - %d", current_song.duration); song_loop_flag = 0; (void) app_timer_start(song_timer_loop, APP_TIMER_TICKS(current_song.duration * 1000), NULL); } nrf_gpio_pin_clear(PIN_PIEZO); nrf_drv_gpiote_out_task_enable(PIN_PIEZO); (void) app_timer_start(song_timer_id, APP_TIMER_TICKS(10), NULL); } /** * @brief Set the GPIO Player PIN to default state */ static void ShutdownPlayer() { nrf_drv_gpiote_out_task_disable(PIN_PIEZO); nrf_gpio_pin_clear(PIN_PIEZO); } /** * @brief Nordic SDK requires an interrupt handler for Timer1, even though we do not need one */ void timer_dummy_handler(nrf_timer_event_t event_type, void * p_context){} /** * @brief Function called at the end of a song. * Will start enqueued song if any, otherwise shut everything Off and notify application. */ static void SongDone(void) { NRF_LOG_INFO("Song Done"); ShutdownPlayer(); (void) app_timer_stop(song_timer_loop); g_inbetween = 0; if(NULL != next_song.p_firstNote) { /* Start enqueued Song */ current_song = next_song; memset(&next_song,0,sizeof(song_t)); startup_sequence = 1; /* Give 200 ms between songs */ (void) app_timer_start(song_timer_id, APP_TIMER_TICKS(200), NULL); } else { /* Shut everything Off */ nrf_drv_timer_disable(&player_timer); (void) app_timer_stop(song_timer_id); UninitPlayer(); /********************************************************/ memset(¤t_song,0,sizeof(song_t)); if (g_FindActivate_SongPlayed == 1) { if( (nrf_fstorage_is_busy(&app_data_bank0) == false) && (nrf_fstorage_is_busy(&app_data_bank1) == false) ) // If flash activity on-going, wait { if (BLE_CONN_HANDLE_INVALID == tile_ble_env.conn_handle) // Disconnected { /* These will auto clear to 0 because of reboot, but still add it for logical purposes */ g_FindActivate_SongPlayed = 0; } } } } } /** * @brief Play the next Note */ static void NextNote(void) { uint8_t note = *current_song.p_currentNote++; uint8_t duration = *current_song.p_currentNote++; tile_unchecked->piezoMs += duration; nrf_drv_timer_disable(&player_timer); if(REST == note && REST == duration) { NRF_LOG_INFO("Song Duration: %d\r\n",tile_unchecked->piezoMs); if(song_loop_flag == 1) { /* End of song reached */ SongDone(); return; } else { if (g_inbetween == 0) { NRF_LOG_INFO("song loop len is Non0, g_inbetween is 0"); /* Give 200 ms between songs*/ (void) app_timer_start(song_timer_id, APP_TIMER_TICKS(200), NULL); current_song.p_currentNote -=2; g_inbetween = 1; } else { NRF_LOG_INFO("song loop len is Non0, g_inbetween is 1"); /* Start one more loop */ current_song.p_currentNote = current_song.p_firstNote; note = *current_song.p_currentNote++; duration = *current_song.p_currentNote++; tile_unchecked->piezoMs += duration; g_inbetween = 0; } } } /* We come here if we are in the middle of a song, or we are starting a new loop */ if(REST == note) { /* reached a rest, disable the piezo pin and put it down */ nrf_drv_gpiote_out_task_disable(PIN_PIEZO); nrf_gpio_pin_clear(PIN_PIEZO); } else { /* reached a note, set the Piezo Pin to toggle at the proper frequency */ nrf_drv_timer_clear(&player_timer); nrf_drv_timer_extended_compare(&player_timer, GPIOTE_SOUND_CHANNEL, nrf_drv_timer_us_to_ticks(&player_timer, notes[NOTE_ARRAY_INDEX(note)]), NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false); nrf_drv_gpiote_out_task_enable(PIN_PIEZO); if (nrf_drv_timer_is_enabled(&player_timer) == 0) nrf_drv_timer_enable(&player_timer); } if (g_inbetween == 0) { (void) app_timer_start(song_timer_id, APP_TIMER_TICKS(duration*10), NULL); } } /** * @brief Initialize the Tile player */ void InitPlayer(void) { /* The Tile player uses GPIOTE to toggle the piezo pin, and * Timer1 triggers the toggle using PPI */ nrf_drv_timer_config_t timer_config = NRF_DRV_TIMER_DEFAULT_CONFIG; /* Configure timer */ (void) nrf_drv_timer_init(&player_timer, &timer_config, timer_dummy_handler); ConfigureBuzzer(); /* Create the timer for switching frequencies */ (void) app_timer_create(&song_timer_id, APP_TIMER_MODE_SINGLE_SHOT, song_timer_handler); /* Create the timer for Song Loop */ (void) app_timer_create(&song_timer_loop, APP_TIMER_MODE_SINGLE_SHOT, song_duration_timeout_handler); } /** * @brief Uninitialize the Tile player, for Power Save reasons * nrfx timer consumes ~0.5 mA current on Average */ void UninitPlayer(void) { ret_code_t err_code; NRF_LOG_INFO("UninitPlayer"); /* Do not call nrf_drv_ppi_uninit() or nrf_drv_gpiote_uninit() as they are used by other modules */ nrf_drv_timer_uninit(&player_timer); err_code = nrf_drv_ppi_channel_disable(ppi_channel1); if (err_code != 0) NRF_LOG_INFO("ppi channel 1 disable gave error %u",err_code); // No Need to assert if Disable failed } /** * @brief Play a song. Queue song if necessary */ int PlaySong(uint8_t number, uint8_t strength, uint8_t duration) { NRF_LOG_INFO("Play Song Request received"); if(strength == 0 || duration == 0) { return TILE_ERROR_SUCCESS; } if(number < ARRAY_SIZE(tile_song_array)) { if((NULL != current_song.p_firstNote) && (NULL == next_song.p_firstNote)) { /* A song is currently playing but there is NO enqueued song */ /* SO enqueue the song */ NRF_LOG_INFO("Enqueue Default song"); next_song = (song_t) { tile_song_array[number], tile_song_array[number], duration}; } else if(NULL == current_song.p_firstNote) { /* no song is currently playing, start it right away */ InitPlayer(); song_loop_flag = 1; NRF_LOG_INFO("Play Default song"); current_song = (song_t) { tile_song_array[number], tile_song_array[number], duration}; startup_sequence = 1; StartupPlayer(); } else { /* If queue is full, ignore */ } } return TILE_ERROR_SUCCESS; } /** * @brief Stop currently playing song and remove enqueued songs */ int StopSong(void) { /* Destroy the queue */ if(next_song.p_firstNote != NULL) { memset(&next_song,0,sizeof(song_t)); } /* Turn off the songs */ if(current_song.p_firstNote != NULL) { song_loop_flag = 1; SongDone(); } return TILE_ERROR_SUCCESS; } /** * @brief Return whether a song is playing or not */ bool SongPlaying(void) { return current_song.p_firstNote != NULL; } /** * @brief Return whether a Find song is playing or not */ bool CheckFindSong(void) { /*Return true if the current song note matches to the TILE_SONG_FIND note*/ if ( (current_song.p_firstNote != NULL) && (current_song.p_firstNote == tile_song_array[TILE_SONG_FIND]) ) { return true; } return false; } #else void InitPlayer(void){} void UninitPlayer(void){} int PlaySong(uint8_t number, uint8_t strength, uint8_t duration){return TILE_ERROR_SUCCESS;} int StopSong(void){return TILE_ERROR_SUCCESS;} bool SongPlaying(void){return false;} void tile_boot_config_player(void){} #endif // TILE_ENABLE_PLAYER #endif // NRF_MODULE_ENABLED(TILE_SUPPORT)