주파수를 알람에 활용해보자는 생각에서 진행하게 된 프로젝트!
▲ 중앙일보 기사 캡쳐
2000~5000Hz를 이용해 파이썬으로 주파수 코드를 제작
파이썬(주파수 코드), 리액트 네이티브(앱 개발)
파이썬은 보통 백엔드에 사용하지만 이번에는 코드 작성에 사용했다.
리액트 네이티브는 크로스 플랫폼이라는 장점과 단기간에 개발하기 좋아 사용하게 되었다.
생각했던 것만큼 오래 걸리지 않았고,
간단하게 만들 수 있어서 앱까지 만들게 되었다.
## 파이썬 코드
import os
import numpy as np
import soundfile as sf
import wave
## 알람 코드
def generate_alarm(high_frequency, duration=5, directory=".", filename="alarm.wav"):
# 저주파 설정(Hz)
low_frequency = 100
sample_rate = 44100
# 시간 배열
t = np.linspace(0, duration, int(duration * sample_rate), endpoint=False)
# 저주파, 고주파 사인파 생성
low_signal = 0.5 * np.sin(2 * np.pi * low_frequency * t)
high_signal = 0.5 * np.sin(2 * np.pi * high_frequency * t)
# 저주파, 고주파 합성
alarm_signal = low_signal + high_signal
# 16bit 변환
alarm_signal = np.int16(alarm_signal * 32767)
# 파일 경로
file_path = os.path.join(directory, filename)
if not os.path.exists(directory):
os.makedirs(directory)
with wave.open(file_path, 'wb') as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(sample_rate)
wav_file.writeframes(alarm_signal.tobytes())
# # 재생
# sd.play(alarm_signal, samplerate=44100)
# sd.wait()
# # 파일로 저장
# sf.write(filename, alarm_signal, 44100)
#알람 파일
saved_directory = './alarm_signals'
generate_alarm(2000, directory=saved_directory, filename="alarm_2000.wav")
generate_alarm(3000, directory=saved_directory, filename="alarm_3000.wav")
generate_alarm(4000, directory=saved_directory, filename="alarm_4000.wav")
generate_alarm(5000, directory=saved_directory, filename="alarm_5000.wav")
메인 페이지에서 알람을 설정하려면 + 표시를 눌러야 하고,
알람 설정해둔 시간에 알람 울리면서 AlarmScreen.js으로 이동
"끄기" 버튼을 누르면 메인 페이지로 이동한다.
*리액트 네이티브 특성상 사용자가 앱 내 어느 페이지에 위치해 있는지 알지 못해서 setAlarm.js에서 알람이 울리는 오류가 발생
currentRoute 변수를 사용해 알람 설정 페이지에서는 울리지 않도록 처리함
이미 만들어진 색상 팔레트 값 사용 (출처 사진에 있음)
// 스타일 태그 제외
// 알람 메인화면
export default function MainPage({ navigation }) {
const [alarms, setAlarms] = useState([]);
const isAlarmPlaying = useRef(false);
const hasStoppedAlarm = useRef(false);
const soundRefs = useRef([]);
const isFocused = useIsFocused();
const currentRouteName = useNavigationState(state => state.routes[state.index].name);
// 수정하려고 클릭하면 알람 재생되는 오류 발생함
// + 수정할 때만 그런게 아니라 AlarmScreen.js에서 MainPage.js로 넘어와도 소리남
// 앱 내 위치 표시하고 1초마다 console 띄우기
// SetAlarm.js 들어갈 때 알람 재생 안 되도록 설정
// MainPage.js에서는 사운드 파일 언로드
// 날짜, 요일 확인 후에 알람 실행되도록
useFocusEffect(
useCallback(() => {
loadAlarms();
checkAlarmStopped();
return () => resetAlarm();
}, [])
);
// 알람 불러오기
const loadAlarms = async () => {
try {
const storedAlarms = await AsyncStorage.getItem('alarms');
if (storedAlarms !== null) {
setAlarms(JSON.parse(storedAlarms));
}
} catch (error) {
console.error(error);
}
};
// AlarmScreen.js에서 MainPage.js로 이동 시 ,
// 사운드 파일 종료 안 되는 오류 발생
// 끄기 버튼 눌렀으면 사운드 꺼졌는지 확인
const checkAlarmStopped = async () => {
const alarmStopped = await AsyncStorage.getItem('alarmStopped');
if (alarmStopped === 'true') {
isAlarmPlaying.current = false;
hasStoppedAlarm.current = true;
await AsyncStorage.removeItem('alarmStopped');
stopSound();
}
};
const stopSound = async () => {
try {
for (let sound of soundRefs.current) {
if (sound) {
await sound.stopAsync();
await sound.unloadAsync();
}
}
soundRefs.current = [];
} catch (error) {
console.error('Error stopping sound:', error);
} finally {
isAlarmPlaying.current = false;
}
};
// 알람 옆 토글 스위치 (on/off)
const toggleSwitch = async (id) => {
const updatedAlarms = alarms.map((alarm) =>
alarm.id === id ? { ...alarm, isEnabled: !alarm.isEnabled } : alarm
);
setAlarms(updatedAlarms);
try {
await AsyncStorage.setItem('alarms', JSON.stringify(updatedAlarms));
} catch (error) {
console.error(error);
}
const alarm = updatedAlarms.find(alarm => alarm.id === id);
if (!alarm.isEnabled) {
handleStopAlarm();
}
};
const parseTime = (timeString) => {
try {
const [period, time] = timeString.split(' ');
let [hour, minute] = time.split(':').map(Number);
if (period === '오후' && hour < 12) {
hour += 12;
} else if (period === '오전' && hour === 12) {
hour = 0;
}
return { hour, minute };
} catch (error) {
console.error('시간 파싱 오류:', error, '문자열:', timeString);
return { hour: 0, minute: 0 };
}
};
const checkAlarms = async () => {
if (currentRouteName === 'SetAlarm' || currentRouteName === 'AlarmScreen') {
return;
}
const now = new Date();
alarms.forEach(alarm => {
if (alarm.isEnabled && !isAlarmPlaying.current && !hasStoppedAlarm.current) {
const { hour, minute } = parseTime(alarm.time);
if (now.getHours() === hour && now.getMinutes() === minute) {
isAlarmPlaying.current = true;
navigation.navigate('AlarmScreen');
}
}
});
};
// 알람 꺼졌는지 확인!!!
const handleStopAlarm = async () => {
isAlarmPlaying.current = false;
hasStoppedAlarm.current = true;
await AsyncStorage.setItem('alarmStopped', 'true');
stopSound();
};
// 알람 한번 실행되면 다음 설정한 알람 안 울리는 오류 발생
// 알람 리셋
const resetAlarm = () => {
isAlarmPlaying.current = false;
hasStoppedAlarm.current = false;
};
useEffect(() => {
const interval = setInterval(() => {
if (isFocused) {
checkAlarms();
}
}, 1000);
return () => clearInterval(interval);
}, [alarms, isFocused, currentRouteName]);
useEffect(() => {
const interval = setInterval(() => {
if (isFocused) {
checkAlarms();
}
}, 1000);
return () => clearInterval(interval);
}, [alarms, isFocused]);
return (
<View style={styles.container}>
<FlatList
data={alarms}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => navigation.navigate('SetAlarm', { alarmId: item.id })}>
<View style={styles.alarmContainer}>
<View>
<Text style={styles.alarmText}>{item.time}</Text>
<Text style={styles.alarmName}>{item.name}</Text>
<Text style={styles.alarmDays}>{(item.days || []).join(', ')}</Text>
</View>
<Switch
trackColor={{ false: "#fff", true: "#faf2ea" }}
thumbColor={item.isEnabled ? "#dba69e" : "#faf2ea"}
ios_backgroundColor="#3e3e3e"
value={item.isEnabled}
onValueChange={() => toggleSwitch(item.id)}
/>
</View>
</TouchableOpacity>
)}
keyExtractor={(item) => item.id}
/>
<TouchableOpacity style={styles.addButton} onPress={() => navigation.navigate('Setting')}>
<Text style={styles.addButtonText}>+</Text>
</TouchableOpacity>
</View>
);
}
알람 울릴 때 보여지는 페이지
// 스타일 태그 제외
// 알람 울리면 나타나는 화면
export default function AlarmScreen({ navigation }) {
const soundRef = useRef([]);
const [isStopping, setIsStopping] = useState(false);
useEffect(() => {
playSound();
return () => {
stopSound();
};
}, []);
//assets에 있는 알람 파일 load -> 실행
const playSound = async () => {
const soundFiles = [
require('./assets/alarm_2000.wav'),
require('./assets/alarm_3000.wav'),
require('./assets/alarm_4000.wav'),
require('./assets/alarm_5000.wav')
];
try {
for (const soundFile of soundFiles) {
for (let i = 0; i < 4; i++) {
if (isStopping) return;
const { sound } = await Audio.Sound.createAsync(soundFile);
soundRef.current.push(sound);
await sound.playAsync();
await new Promise(resolve => setTimeout(resolve, 5000));
if (isStopping) {
await sound.stopAsync();
await sound.unloadAsync();
return;
}
await sound.stopAsync();
await sound.unloadAsync();
}
}
} catch (error) {
console.error('Sound playback error:', error);
}
};
// 사운드 파일 멈추기
const stopSound = async () => {
setIsStopping(true);
for (let sound of soundRef.current) {
if (sound && sound._loaded) {
try {
await sound.stopAsync();
await sound.unloadAsync();
} catch (error) {
console.error('Error:', error);
}
}
}
soundRef.current = [];
};
// 사운드 파일 stop 시키면서 메인으로 이동
const handleStopAlarm = async () => {
await stopSound();
await AsyncStorage.setItem('alarmStopped', 'true');
navigation.goBack();
};
return (
<View style={styles.container}>
<Text style={styles.alarmText}>알람 울리는 중!</Text>
<TouchableOpacity style={styles.btn} onPress={handleStopAlarm}>
<Text style={styles.btnText}>끄기</Text>
</TouchableOpacity>
</View>
);
}
금요일 선택한 상태, 제목 작성함
// 스타일 태그 제외
// 알람 수정하는 페이지
export default function SetAlarm({ route = {}, navigation }) {
const { alarmId } = route.params || {};
const [time, setTime] = useState(new Date());
const [days, setDays] = useState([]);
const [name, setName] = useState('');
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
const dayOptions = ['일', '월', '화', '수', '목', '금', '토'];
useEffect(() => {
loadAlarm(alarmId);
}, [alarmId]);
const loadAlarm = async (id) => {
try {
const storedAlarms = await AsyncStorage.getItem('alarms');
if (storedAlarms !== null) {
const alarms = JSON.parse(storedAlarms);
const alarm = alarms.find((alarm) => alarm.id === id);
if (alarm) {
const [ampm, timeString] = alarm.time.split(' ');
const [hours, minutes] = timeString.split(':').map(Number);
const date = new Date();
date.setHours(ampm === '오후' ? hours + 12 : hours, minutes);
setTime(date);
setDays(alarm.days || []);
setName(alarm.name || '');
}
}
} catch (error) {
console.error(error);
}
};
const showDatePicker = () => {
setDatePickerVisibility(true);
};
const hideDatePicker = () => {
setDatePickerVisibility(false);
};
const handleConfirm = (selectedTime) => {
setTime(selectedTime);
hideDatePicker();
};
const formatTime = (date) => {
let hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? '오후' : '오전';
hours = hours % 12;
hours = hours ? hours : 12;
const strTime = `${ampm} ${hours}:${minutes < 10 ? `0${minutes}` : minutes}`;
return strTime;
};
const toggleDay = (day) => {
setDays((prevDays) =>
prevDays.includes(day) ? prevDays.filter((d) => d !== day) : [...prevDays, day]
);
};
const saveAlarm = async () => {
try {
const newAlarm = {
id: alarmId,
time: formatTime(time),
days: days,
name: name,
isEnabled: false,
};
const storedAlarms = await AsyncStorage.getItem('alarms');
const alarms = storedAlarms ? JSON.parse(storedAlarms) : [];
const updatedAlarms = alarms.map((alarm) => (alarm.id === alarmId ? newAlarm : alarm));
await AsyncStorage.setItem('alarms', JSON.stringify(updatedAlarms));
navigation.navigate('Main');
} catch (error) {
console.error(error);
}
};
const deleteAlarm = async () => {
try {
const storedAlarms = await AsyncStorage.getItem('alarms');
if (storedAlarms !== null) {
const alarms = JSON.parse(storedAlarms);
const updatedAlarms = alarms.filter((alarm) => alarm.id !== alarmId);
await AsyncStorage.setItem('alarms', JSON.stringify(updatedAlarms));
navigation.navigate('Main');
}
} catch (error) {
console.error(error);
}
};
return (
<ScrollView style={styles.container}>
<Text style={styles.label}>시간:</Text>
<TouchableOpacity onPress={showDatePicker} style={styles.pickerButton}>
<Text style={styles.pickerButtonText}>{formatTime(time)}</Text>
</TouchableOpacity>
<DateTimePickerModal
isVisible={isDatePickerVisible}
mode="time"
onConfirm={handleConfirm}
onCancel={hideDatePicker}
/>
<Text style={styles.label}>요일:</Text>
<View style={styles.dayContainer}>
{dayOptions.map((day) => (
<TouchableOpacity
key={day}
style={[styles.dayButton, days.includes(day) && styles.selectedDayButton]}
onPress={() => toggleDay(day)}
>
<Text style={[styles.dayText, days.includes(day) && styles.selectedDayText]}>{day}</Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.label}>알람 이름:</Text>
<TextInput
style={styles.textInput}
value={name}
onChangeText={setName}
placeholder="알람 이름"
/>
<View style={styles.buttonContainer}>
<TouchableOpacity style={[styles.button, styles.deleteButton]} onPress={deleteAlarm}>
<Text style={styles.buttonText}>삭제</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={saveAlarm}>
<Text style={styles.buttonText}>저장</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
}
// 스타일 태그 제외
// 알람 생성하는 페이지
// 라이브러리 사용함
export default function Setting({ navigation }) {
const [time, setTime] = useState(new Date());
const [days, setDays] = useState([]);
const [name, setName] = useState('');
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
const dayOptions = ['일', '월', '화', '수', '목', '금', '토'];
const showDatePicker = () => {
setDatePickerVisibility(true);
};
const hideDatePicker = () => {
setDatePickerVisibility(false);
};
const handleConfirm = (selectedTime) => {
setTime(selectedTime);
hideDatePicker();
};
const formatTime = (date) => {
let hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? '오후' : '오전';
hours = hours % 12;
hours = hours ? hours : 12;
const strTime = `${ampm} ${hours}:${minutes < 10 ? `0${minutes}` : minutes}`;
return strTime;
};
const toggleDay = (day) => {
setDays((prevDays) =>
prevDays.includes(day) ? prevDays.filter((d) => d !== day) : [...prevDays, day]
);
};
// 알람 저장
const saveAlarm = async () => {
try {
const newAlarm = {
id: Date.now().toString(),
time: formatTime(time),
days: days,
name: name,
isEnabled: false,
};
const storedAlarms = await AsyncStorage.getItem('alarms');
const alarms = storedAlarms ? JSON.parse(storedAlarms) : [];
alarms.push(newAlarm);
await AsyncStorage.setItem('alarms', JSON.stringify(alarms));
navigation.navigate('Main');
} catch (error) {
console.error(error);
}
};
return (
<ScrollView style={styles.container}>
<Text style={styles.label}>시간:</Text>
<TouchableOpacity onPress={showDatePicker} style={styles.pickerButton}>
<Text style={styles.pickerButtonText}>{formatTime(time)}</Text>
</TouchableOpacity>
<DateTimePickerModal
isVisible={isDatePickerVisible}
mode="time"
onConfirm={handleConfirm}
onCancel={hideDatePicker}
/>
<Text style={styles.label}>요일:</Text>
<View style={styles.dayContainer}>
{dayOptions.map((day) => (
<TouchableOpacity
key={day}
style={[styles.dayButton, days.includes(day) && styles.selectedDayButton]}
onPress={() => toggleDay(day)}
>
<Text style={[styles.dayText, days.includes(day) && styles.selectedDayText]}>{day}</Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.label}>알람 이름:</Text>
<TextInput
style={styles.textInput}
value={name}
onChangeText={setName}
placeholder="알람 이름 입력"
/>
<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.button} onPress={() => navigation.navigate('Main')}>
<Text style={styles.buttonText}>취소</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={saveAlarm}>
<Text style={styles.buttonText}>확인</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
}
졸린 고양이
apk로 다운 받았을 때는 기본 아이콘이 뜬다
이것도 수정해야지
expo 배포는 처음이라 헤매다가 당연히 apk일 줄 알고 앱 생성했는데..!
aab라서 파일 변환시켰다.
이때 jdk가 필요하다는 걸 새롭게 알게 됨
이건 추가하고 싶었던 기능들인데 아쉽게도 오랜만에 리액트를 사용하다보니
오류랑 버그 해결하는데 시간을 많이 투자해서 넣지 못했다.
방학 때 추가해보고 구글플레이에 업로드까지 해볼 생각!
링크텍스트
구글 드라이브에서 다운로드 가능!